How an MCP Session Works: The Initialize Handshake Explained
Every MCP connection follows the same lifecycle: the client sends an initialize request, the server answers with its capabilities, the client sends an initialized notification, and only then do normal requests begin. Knowing this sequence helps you debug connections that fail before any tool is ever listed.
The three phases
- Initialization. The two sides agree on a protocol version and exchange capabilities.
- Operation. Requests and notifications flow, such as
tools/listandtools/call. - Shutdown. The connection closes. For stdio, the client closes the input stream and the server exits. For HTTP, the client ends the session.
Step 1: the initialize request
The client always speaks first. Its request carries the protocol version it wants, the features it supports, and information about itself.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": { "roots": {}, "sampling": {} },
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}
The protocolVersion value is a date string that identifies a revision of the specification. The date above is only an example; real clients send whichever revision they support. The capabilities object lists optional client features. Here the client says it can provide roots and sampling.
Step 2: the server's response
The server replies with the version it will use, its own capabilities and its identity.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": { "listChanged": true },
"resources": {},
"prompts": {}
},
"serverInfo": { "name": "demo", "version": "1.0.0" },
"instructions": "Use search before get."
}
}
If the server supports the requested version, it echoes it. If not, it answers with a version it does support, and the client decides whether it can work with that. If the client cannot, it disconnects.
The capabilities object matters because each side may only use features the other declared. A server that does not declare prompts will not be asked for prompts. The optional instructions string is a hint the host may add to the model's context.
Step 3: the initialized notification
Once the client has accepted the response, it sends a notification, which is a message with no id and no reply.
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
Until the server receives this, it should not expect ordinary requests from the client. The spec does allow pings and some logging before then.
Operation phase
Now the client typically calls tools/list, resources/list and prompts/list for the capabilities the server declared. Later, tool calls and resource reads happen as the user and model need them. Both sides must respect the capabilities negotiated at the start.
Seeing it for yourself
With a stdio server you can type the handshake by hand. Start the server, paste the initialize request as a single line, and read the reply. This TypeScript-built server is an example:
node build/index.js
# then paste on one line and press Enter:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"manual","version":"0.0.1"}}}
The server prints a single line of JSON containing its result. If you see nothing, either the server crashed (check stderr) or the version you sent was rejected.
Typical failures during initialization
- The server exits immediately. An import error or missing dependency. Run the command by hand and read the error.
- Timeout waiting for initialize. The server printed something to stdout before the response, or started slowly because it does heavy work at startup.
- Version mismatch. An old client with a new server or the reverse. Update the SDK on the older side.
- Missing capability. The client asks for
prompts/listbut the server never declared prompts, so the server returns an error.
What the SDKs do for you
When you use McpServer in TypeScript or FastMCP in Python, the handshake is automatic. The SDK builds the capabilities object from the tools, resources and prompts you registered. That is why a server with no registered tools does not declare the tools capability. Knowing the handshake is useful mainly when you debug at the protocol level or write your own client.
Comments
No comments yet. Have a question or a tip? Be the first to share it.