| name | mcp-protocol-server-implementation |
| description | Requirements for implementing an MCP (Model Context Protocol) server that works with
the official @modelcontextprotocol/sdk client. Use when: (1) implementing an MCP server
from scratch, (2) getting "Expected initialize response" errors, (3) MCP client fails
to connect or times out, (4) tools/list returns successfully but client doesn't recognize
server, (5) implementing MCP Streamable HTTP transport. Covers initialize method,
notification handling, SSE response format, and session management.
|
| author | Claude Code |
| version | 1.0.0 |
| date | 2026-01-27T00:00:00.000Z |
MCP Protocol Server Implementation
Problem
When implementing a custom MCP server, the official MCP SDK client
(@modelcontextprotocol/sdk) with StreamableHTTPClientTransport fails to connect
or times out, even when tools/list returns correct JSON-RPC responses.
Context / Trigger Conditions
- Building a custom MCP server (Lambda, Express, etc.) from scratch
- Using
StreamableHTTPClientTransport from @modelcontextprotocol/sdk
- Client throws "Expected initialize response" error
- Client times out during connection
- Server returns valid JSON-RPC for
tools/list but client doesn't work
- Direct curl to
tools/list works but SDK client fails
Solution
1. Handle the initialize Method (REQUIRED)
The SDK client sends initialize as its first request. Your server MUST respond:
if (method === "initialize") {
return {
jsonrpc: "2.0",
id,
result: {
protocolVersion: "2024-11-05",
serverInfo: {
name: "your-server-name",
version: "1.0.0",
},
capabilities: {
tools: {},
},
},
};
}
2. Handle Notifications (REQUIRED)
The client sends notifications/initialized after initialize. Per JSON-RPC 2.0 spec,
notifications have NO id field and servers MUST NOT send a response body.
function isNotification(message: McpRequest): boolean {
return message.id === undefined;
}
if (isNotification(mcpRequest)) {
return { statusCode: 202, headers, body: "" };
}
3. SSE Response Format (RECOMMENDED)
When client sends Accept: text/event-stream, respond in SSE format:
function formatAsSSE(response: McpResponse): string {
return `event: message\ndata: ${JSON.stringify(response)}\n\n`;
}
function clientAcceptsSSE(acceptHeader: string | undefined): boolean {
return acceptHeader?.includes("text/event-stream") ?? false;
}
const contentType = useSSE ? "text/event-stream" : "application/json";
4. Session Management (RECOMMENDED)
Track sessions via mcp-session-id header:
const incomingSessionId = headers["mcp-session-id"];
const sessionId = incomingSessionId ?? randomUUID();
responseHeaders["mcp-session-id"] = sessionId;
responseHeaders["Access-Control-Expose-Headers"] = "mcp-session-id";
5. CORS Headers (REQUIRED for browser clients)
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"Content-Type,Authorization,mcp-session-id,mcp-protocol-version",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Expose-Headers": "mcp-session-id",
"Cache-Control": "no-cache",
};
Verification
- Test with curl first:
curl -X POST https://your-server/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'
curl -X POST https://your-server/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
- Then test with MCP SDK client:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "test", version: "1.0.0" });
const transport = new StreamableHTTPClientTransport(
new URL("https://your-server/mcp"),
);
await client.connect(transport);
const tools = await client.listTools();
Example
Complete minimal MCP server handler:
export function handler(event: APIGatewayProxyEvent): APIGatewayProxyResult {
const headers = { "Content-Type": "application/json" };
if (event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers, body: "" };
}
const request = JSON.parse(event.body ?? "{}");
if (request.id === undefined) {
return { statusCode: 202, headers, body: "" };
}
switch (request.method) {
case "initialize":
return {
statusCode: 200,
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2024-11-05",
: { : , : },
: { : {} },
},
}),
};
:
{
: ,
headers,
: .({
: ,
: request.,
: { : },
}),
};
:
;
:
{
: ,
headers,
: .({
: ,
: request.,
: {
: -,
: ,
},
}),
};
}
}
Notes
- Protocol Version: Use
2024-11-05 as of early 2026. Check MCP spec for updates.
- JSON-RPC Error Codes:
-32700: Parse error
-32600: Invalid request
-32601: Method not found
-32603: Internal error
- Method Call Order: Client always sends
initialize first, then notifications/initialized,
then other methods like tools/list.
- Timeout Issues: If client times out, check that
initialize response is correct—this is
the most common cause.
References