abhigyanpatwari/GitNexus · error
-32000
-32000
Error message
First request must be initialize. No session ID provided.
What it means
Thrown by the GitNexus MCP streamable-HTTP endpoint (POST /mcp) when a request arrives with no mcp-session-id header and its JSON-RPC body (single message or batch) contains no initialize request. The MCP protocol requires the very first request on a fresh connection to be initialize, because that is the only path that allocates a Server + Transport pair and assigns a session ID. Rejecting anything else up front prevents orphaned Server instances that the TTL sweep could never reclaim.
Source
Thrown at gitnexus/src/mcp/http-transport.ts:276
const session = sessions.get(sessionId)!;
session.lastActivity = Date.now();
await session.transport.handleRequest(req, res, req.body);
} else if (sessionId) {
// Unknown / expired session ID — tell the client to re-initialize (per MCP spec).
res.status(404).json({
jsonrpc: '2.0',
error: { code: -32001, message: 'Session not found. Re-initialize.' },
id: null,
});
} else if (req.method === 'POST') {
// No session ID — new client. Only accept initialize requests to avoid
// orphaned Server instances that can never be reclaimed by the TTL sweep.
// Use the SDK's isInitializeRequest so a single-element JSON-RPC batch is
// recognised too, rather than a brittle `body.method === 'initialize'` check.
const body = req.body as unknown;
const messages = Array.isArray(body) ? body : [body];
if (!messages.some(isInitializeRequest)) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'First request must be initialize. No session ID provided.',
},
id: null,
});
return;
}
// Reject when the session cap is reached — prevents memory exhaustion via
// an initialize flood (each session holds a live Server + Transport).
if (sessions.size >= MAX_SESSIONS) {
res.status(503).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Server at session capacity. Try again later.' },
id: null,
});View on GitHub (pinned to aac7515d2a)
Solutions
- Send a proper JSON-RPC initialize request first: POST /mcp with Content-Type: application/json and Accept: application/json, text/event-stream, body {jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion, capabilities:{}, clientInfo:{...}}}
- Read the mcp-session-id response header and include it as a request header on every subsequent POST/GET/DELETE to /mcp
- If using the MCP TypeScript SDK, let StreamableHTTPClientTransport manage the handshake and session header instead of hand-rolling fetch calls
- If batching the first request, make sure the array contains the initialize message (the server checks Array.isArray(body) ? body : [body] with the SDK's isInitializeRequest)
Example fix
// before
await fetch(`${base}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: 1 }),
}); // 400 -32000
// after
const initRes = await fetch(`${base}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({
jsonrpc: '2.0', id: 1, method: 'initialize',
params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'ci', version: '1.0.0' } },
}),
});
const sessionId = initRes.headers.get('mcp-session-id');
await fetch(`${base}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', 'mcp-session-id': sessionId! },
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
}); Defensive patterns
Strategy: validation
Validate before calling
// Before the first POST to /mcp, verify the message is an initialize request.
function buildFirstMessage(): { jsonrpc: '2.0'; id: number; method: string; params?: unknown } {
return { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'my-client', version: '1.0.0' } } };
}
const first = buildFirstMessage();
if (first.method !== 'initialize') throw new Error('first message must be initialize');
const res = await fetch(`${base}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }, body: JSON.stringify(first) });
const sessionId = res.headers.get('mcp-session-id');
if (!sessionId) throw new Error(`handshake rejected: HTTP ${res.status} ${await res.text()}`); Type guard
function isInitializeRequest(m: unknown): m is { jsonrpc: '2.0'; method: 'initialize' } {
return (
typeof m === 'object' && m !== null &&
(m as Record<string, unknown>).jsonrpc === '2.0' &&
(m as Record<string, unknown>).method === 'initialize'
);
} Try / catch
const res = await postToMcp(msg);
if (res.status === 400) {
const body = await res.json();
if (body.error?.code === -32000 && /First request must be initialize/.test(body.error.message)) {
sessionId = await initialize(); // re-run handshake, capture new mcp-session-id
return postToMcp(msg, sessionId); // retry the original call exactly once
}
} Prevention
- Always open a streamable MCP session with an initialize request before anything else
- Persist the mcp-session-id response header and attach it to every subsequent request
- Use the official MCP SDK's StreamableHTTPClientTransport instead of hand-rolled HTTP calls
- Never batch non-initialize messages into the very first request
When it happens
Trigger: POST to /mcp whose body is e.g. {jsonrpc:'2.0',method:'tools/list',id:1} with no mcp-session-id header; a JSON-RPC batch whose elements include no initialize message; a hand-rolled fetch/axios script that skips the handshake; a proxy or gateway that strips the mcp-session-id header, making every request look like a new client.
Common situations: Porting a stdio-based MCP client to streamable HTTP without adding the initialize handshake; forgetting to persist the mcp-session-id response header between requests; sending the notifications/initialized notification before the initialize response arrives; testing the endpoint with curl using a random JSON-RPC method.
Related errors
- Missing Content-Length header from MCP client
- Invalid Content-Length header from MCP client
- Content-Length ${contentLength} exceeds maximum allowed size
- Refusing to start the MCP HTTP server on a non-loopback host
- list_repos: "${field}" must be an integer ${bound} (received
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/9d48cd64fa3b2480.
Report an issue: GitHub.