1. Why a handshake is needed at all
If REST endpoints are a set of separate doors you can knock on by URL, MCP is more like an ongoing conversation over a single channel. The client doesn’t just send isolated requests; it first establishes a session. The handshake is the introduction at the beginning of that session.
In MCP this moment is implemented as a special request, initialize, which the client sends immediately after establishing the transport (STDIO, HTTP/stream, WebSocket—it doesn’t matter). In the request it says: “I speak this version of MCP; here’s what I can do, and here’s who I am.” The server replies: “I support this version and these capabilities—nice to meet you.”
After a successful exchange the client sends the notification notifications/initialized, and only after that does the real work begin: tools/list, resources/list, tools/call, and other useful things.
If you want an analogy, the MCP handshake is like signing a lease before moving a server into a data center. Until you agree on the rules (protocol format, which services the data center provides, who is billed for what), hauling servers back and forth is pointless.
From a practical standpoint, the handshake solves three tasks:
- Verifies protocol version compatibility.
- Announces which MCP “primitives” the server supports: tools, resources, prompts, logging, notifications, etc.
- Provides meta‑information about the client and server—implementation name and version.
2. The MCP connection life cycle: where the handshake lives
To make it less abstract, let’s look at a typical (simplified) connection flow:
sequenceDiagram
participant C as Client (ChatGPT/Inspector)
participant S as MCP server
C->>S: (1) Establish transport (STDIO/HTTP-stream)
C->>S: (2) Request: "initialize"
S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
C->>S: (4) Notification: "notifications/initialized"
C->>S: (5) Request: "tools/list" / "resources/list"
S-->>C: (6) Result: lists of tools/resources
C->>S: (7) Request: "tools/call", etc.
Technically, the steps look like this:
- Transport is established: for example, ChatGPT starts your server as a subprocess and connects over STDIO, or Inspector makes an HTTP/stream request to /mcp.
- The client sends a JSON‑RPC request initialize.
- The server replies with a JSON‑RPC result containing fields protocolVersion, capabilities, and serverInfo.
- The client sends the notification notifications/initialized—a signal: “I’ve read everything; we can start working.”
- The client calls discovery methods (tools/list, resources/list, prompts/list) depending on what it saw in the server’s capabilities.
- The server returns metadata for tools/resources/prompts.
- Then come the “work” requests: tools/call, resources/read, and others.
It’s important to note that the handshake is just a regular JSON‑RPC call, initialize. No magic. After the lecture on MCP message format you already know how to parse such requests; the only difference is that here the method is always one and “special,” and it executes first.
3. What the client sends in initialize
Let’s break down the initialize request. A minimal request might look like this (simplified for the lecture):
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "chatgpt-gift-client",
"version": "2.3.0"
}
}
}
This example is close to what is shown in the official MCP documentation. The main fields in params:
protocolVersion
A string with the MCP specification version, most often in date format, for example "2025-06-18". This is not your application version; it’s the protocol version itself. The client says: “I expect to speak this version of MCP.” The server must either confirm it in the response or return an error if it doesn’t know this version.
This protects against “the client assumes one thing while the server implements another.” If no common version is found, it’s better to honestly tear down the connection than to exchange incompatible messages.
capabilities (client)
An object in which the client declares which MCP features it supports. For example, the ChatGPT client often sets the elicitation key, signaling that it can handle requests to the user (additional input, confirmations, etc.).
Example:
"capabilities": {
"elicitation": {},
"sampling": {}
}
The server can use this information to understand which extended protocol features make sense to use at all. For example, elicitation means the client (ChatGPT) can ask the user clarifying questions and request additional data.
clientInfo
Simple meta‑information: the client’s name and version.
"clientInfo": {
"name": "ChatGPT",
"version": "2.0.0"
}
From a server developer’s perspective, this is gold for logs: you can always see exactly which client connected—ChatGPT, MCP Inspector, your own test client—and what version it is.
4. What the server returns: initialize result
The response to initialize is a regular JSON‑RPC result with the same id, but the result field contains a description of what the server can do.
In the request we looked at capabilities from the client’s side—what the client supports. Now let’s look at the mirror object in the response: the server’s capabilities, i.e., what it supports. Schematically:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {},
"prompts": {},
"logging": {}
},
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.1.0"
}
}
}
You’ll see a similar structure in the official protocol description and/or SDK documentation. The main parts:
protocolVersion in the response
The server either repeats the version proposed by the client or (theoretically) could pick another common version if there are several. In typical implementations it simply confirms the client’s version if the server supports it. If not, the server should return an error and stop the conversation.
serverInfo
Server meta‑information: name, version.
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.1.0"
}
It sounds boring, but these are exactly the fields you’ll filter and search by in logs later: “why doesn’t ChatGPT version X come to an agreement with our server version Y?”
capabilities (server)
The most interesting field. Here the server declares which MCP primitives and extensions it supports: whether it can handle tools/*, resources/*, prompts/*, whether it can send notifications about list changes, and so on.
If capabilities doesn’t contain a tools section, no properly implemented client will call tools/list or tools/call. Likewise, the absence of resources means the client will not send resources/list or resources/read.
Thus, capabilities is a lightweight contract: “what is and isn’t allowed to do with this server.”
5. Capabilities as a “list of superpowers”
From here on we’re only interested in the server’s capabilities—the object that comes back in the initialize response and defines which MCP primitives the server supports at all.
Let’s look in more detail at its structure. An example (simplified but close to the spec):
{
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"prompts": {
"listChanged": false
},
"logging": {}
}
This example is covered in the official MCP architecture. Let’s decode it by sections.
Capabilities.tools
The presence of a tools key says: the server can respond to tools/list and tools/call. If there is also a listChanged: true flag, it means the server may send tools/list_changed notifications in the future when the toolset changes.
This is useful for ChatGPT: you can cache the list of tools and, upon receiving list_changed, refresh it without a full reconnect.
Capabilities.resources
The resources section declares that the server supports working with resources: resources/list, resources/read, sometimes search. Flags inside:
- subscribe: true — the client can subscribe to resource changes (for example, for live logs or file updates).
- listChanged: true — the server can send a resources/list_changed notification if resources are added or removed.
This is especially important for large directories or “live” data that changes constantly.
Capabilities.prompts
If the server registers predefined prompts (for example, templates of model calls tied to your domain), then a prompts key appears in capabilities. It may also have a listChanged flag.
Seeing this section, the client understands that prompts/list and possibly prompts/get are available.
Capabilities.logging and others
Some server implementations also declare logging—this means the server can send structured logs to the client over MCP, for example, for debugging.
Other sections may appear (for example, sampling or specific extensions). Importantly, the protocol was designed to be extensible from the start: you can add new keys to capabilities, and older clients will simply ignore them if they don’t know about them.
Insight
Experimentally it has been established that the ChatGPT App ignores listChanged messages sent to it. As of now, when you build an app you cannot declare one set of tools and then add or remove more tools later—even though the MCP protocol allows it.
At the time of writing this course: at registration time of your app in the ChatGPT Store, ChatGPT requests the list of tools and resources from your app and caches them permanently. The probability that this will change sometime in 2026 is high; the probability that it will change in the first quarter of 2026 is low.
6. Discovery after the handshake: how to fetch the list of tools and resources
The handshake answers the question “what can the server do at all?” The next step is so‑called discovery: via specific methods, the client pulls the details—exactly which tools exist, which resources are available, which prompts are built in.
Discovery methods are used for this: roughly tools/list, resources/list, prompts/list. The MCP architecture documentation suggests explaining it exactly this way: handshake → discovery → tool invocations.
Example request for tools/list:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
The server response contains an array of tools: names, descriptions, JSON Schema for arguments, and sometimes metadata like categories or icons.
After that, ChatGPT (or another client) caches the list and, during the conversation, uses it to:
- pick the right tool for the user’s task;
- verify that a tool name exists;
- validate arguments before sending tools/call.
It’s similar with resources, except resources/list often supports cursor‑based pagination to avoid fetching a million records at once. This is also described in the MCP specification and treated as a typical case for large catalogs.
7. The handshake and capabilities using our GiftGen app as an example
In previous modules we built a learning app that helps pick gifts. We already have a widget, we have a suggest_gifts tool on the backend, and some kind of gift catalog. Now let’s imagine what the handshake looks like for the gift-genius MCP server.
Handshake example for GiftGen
Request from the client:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "ChatGPT",
"version": "2.1.0"
}
}
}
Response from our server:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "listChanged": true },
"prompts": {},
"logging": {}
},
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.2.0"
}
}
}
Essentially we’re almost repeating the examples from the official MCP architecture, just adapting the names to our app.
What the client learns from this response:
- There are tools (tools), and the list can change dynamically (listChanged: true).
- There are resources (our gift catalog, possibly stored in files or a DB).
- There are prompts (for example, a template “Formulate a short description of a gift for user N”).
- The server can send logs (handy for inspectors and debugging).
Next, the client calls tools/list and sees, for example, a tool like this:
{
"name": "suggest_gifts",
"description": "Suggests gift ideas based on the recipient's profile.",
"inputSchema": {
"type": "object",
"properties": {
"age": { "type": "integer" },
"relationship": { "type": "string" },
"budget": { "type": "number" }
},
"required": ["age", "relationship"]
}
}
And now, when the user writes something like: “Suggest a gift for my sister, 25 years old, budget up to 50 dollars,” the model already knows there is a suggest_gifts tool with such‑and‑such arguments, and it can be invoked via tools/call.
8. How the SDK hides the handshake (but why it’s still important to understand it)
In the TypeScript SDK for MCP (the one we’ll use in the next lecture), this whole story with initialize and notifications/initialized is tucked away inside the connect method. Sample code:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "gift-genius",
version: "1.0.0",
});
// Tool registration — based on this, the SDK will configure capabilities.tools automatically
server.tool(
"suggest_gifts",
{
description: "Suggests gift ideas.",
inputSchema: {
type: "object",
properties: {
age: { type: "integer" },
relationship: { type: "string" },
budget: { type: "number" },
},
required: ["age", "relationship"],
},
},
async (input) => {
// ... gift selection logic ...
return { suggestions: [] };
},
);
const transport = new StdioServerTransport();
// Here the SDK:
// 1) receives initialize from the client,
// 2) responds with serverInfo and capabilities,
// 3) waits for notifications/initialized,
// 4) then starts handling tools/* calls.
await server.connect(transport);
The SDK automatically assembles capabilities based on what you registered: if there is at least one server.tool(...), it will add a tools section to capabilities. If you register resources or prompts, resources and prompts will appear.
Understanding the handshake and capabilities isn’t so you can write JSON by hand (don’t do that), but so that you can:
- read MCP logs and understand why the client “doesn’t see” your tools;
- diagnose protocol version incompatibilities;
- implement a custom server or non‑standard transport when needed.
9. Protocol versions and the evolution of capabilities
The protocolVersion field in the handshake is not mere decoration. The MCP specification explicitly emphasizes: it is a way to agree on a compatible protocol version; if no common version is found, it’s better to terminate the connection.
A typical scenario:
- You deploy an MCP server in prod with an SDK that implements MCP version "2025-06-18".
- Over time a new MCP version is released; you update the client, but the server is still old.
- The client sends protocolVersion: "2026-02-01"; the server doesn’t know that version and returns an invalid protocol version error (or similar).
In practice developers often ignore this field and then wonder why the connection doesn’t establish.
The right approach to versions:
- Always know which MCP version your SDK supports (usually in docs/release notes).
- When updating the SDK, deliberately update the protocol version.
- Logs and monitoring should clearly surface initialization errors due to protocolVersion mismatches.
Capability evolution is also tied to versions: new MCP features are added as new keys in capabilities. Older clients ignore them; newer ones can use them. This pattern is described in the official MCP documentation as a way to maintain backward compatibility.
10. The handshake through the eyes of ChatGPT and the inspector
What ChatGPT does when connecting MCP
When you attach an MCP server to ChatGPT in Dev Mode, the platform does roughly the following behind the scenes:
- Opens a transport (usually HTTP/stream at /mcp).
- Sends initialize with protocolVersion, capabilities, and clientInfo (something like “ChatGPT Enterprise, version such‑and‑such”).
- Receives the response and caches the server’s capabilities.
- Calls tools/list, resources/list, prompts/list depending on what it saw in capabilities.
- During the conversation, when the model decides to call a tool, it consults this cache: whether the tool exists, what its argument schema is, and how to format the call.
If the server’s capabilities don’t contain tools, ChatGPT won’t even try to offer your App as a tool. If capabilities contain resources but lack the listChanged flag, ChatGPT may cache the resource list and not wait for change notifications.
How inspectors and MCP Jam help with debugging
Tools like MCP Jam / MCP Inspector do almost the same: establish a connection, perform the handshake, show you the server’s capabilities, and let you manually call tools/list, tools/call, and so on.
From a developer’s perspective this is a must‑have:
- you can see what protocolVersion the server actually returned;
- you can immediately see whether tools, resources, prompts are present in capabilities;
- you can understand why ChatGPT doesn’t see tools (capabilities weren’t declared, or the handshake didn’t complete).
In the last lecture of this module you’ll use such tools more extensively, but even now it’s helpful to understand that they work exactly on top of the handshake we’re discussing.
11. Common mistakes when working with the handshake and capabilities
In theory this all looks straightforward, but in practice the handshake and capability declaration most often become the source of quite primitive bugs—especially in Dev Mode or MCP Inspector. Below are several typical mistakes you will almost certainly run into either in your own code or in colleagues’ logs.
Error #1: Incorrect initialize request format.
A very common problem when hand‑rolling an MCP server without an SDK is to miss some required JSON‑RPC field. For example, forgetting jsonrpc: "2.0", mixing up the method (writing "init" instead of "initialize"), or making capabilities a boolean instead of an object. The MCP specification expects a precise format; any deviation leads to parsing errors and a dropped connection. Documentation and practical guides explicitly recommend first ensuring that initialize strictly matches the spec before looking at anything else.
Error #2: Ignoring protocolVersion.
Sometimes developers just copy an example from the docs and put in an arbitrary string without checking SDK support. As a result the client and server speak different MCP versions and the connection doesn’t establish. The error may masquerade as “the client doesn’t connect at all.” Treat protocolVersion as a real contract: agree on this version between the frontend/agent platform team and the team building the MCP server.
Error #3: Missing capabilities.
A classic scenario: you registered a tool on the server, but in a manual handshake implementation you forgot to add "tools": {} to the capabilities of the initialize response. In the inspector you see that tools exist, but ChatGPT shows “No tools available”—because it trusts capabilities and doesn’t call tools/list if there is no tools section. Troubleshooting guides for the Apps SDK explicitly emphasize: if ChatGPT doesn’t see tools, check capabilities first.
Error #4: Trying to use methods not declared in capabilities.
Sometimes students experiment and, for example, send resources/list to a server whose capabilities lack a resources section. Formally the server may respond with Method not found, but it’s more correct not to call such methods at all. MCP introduces capabilities specifically to protect against such attempts. The client should first check for the corresponding section in capabilities and only then call the methods.
Error #5: The server starts “chatting” before notifications/initialized.
If the server, right after sending a response to initialize, starts sending logs or notifications without waiting for notifications/initialized, some clients may ignore these messages or even drop the connection. The official MCP architecture emphasizes that the handshake must complete first, and only after the initialization notification should “work” begin.
Error #6: Changing tool schemas without signaling a list change.
When you change a tool’s JSON Schema (make a field required, rename an argument) but don’t restart the server or don’t send a notification that the tool list has changed, the client’s cache may hold an old version of the schema. This leads to odd validation errors. The spec suggests using the listChanged flag and the tools/list_changed and resources/list_changed notifications to help the client refresh its cache promptly.
Error #7: Premature optimization and “magic” around capabilities.
Sometimes developers start inventing complex schemes with dynamically generated capabilities, per‑client versioning, and other exotica without understanding the basics. At the start it’s enough to honestly declare what the server supports: tools, resources, prompts, logging. Expand capabilities as real needs emerge, not “for the future.” This is more of an organizational anti‑pattern than a purely protocol mistake, but it’s very common in production projects.
GO TO FULL VERSION