Start here: the basics
What is an MCP client?
The app you type into is the host. The client is the smaller piece inside it that actually speaks MCP to one server at a time.
An MCP client is the piece of code inside an AI app that opens a connection to one MCP server and speaks the Model Context Protocol on the app's behalf. It is the side that asks: it requests the server's tools, resources and prompts, and it can offer to do things back, such as asking the user a question or running a model completion for the server. The app around it, Claude Desktop, an IDE, your own agent, is called the host.
Host, client and server
The spec's Architecture page draws three roles. The host is the container: it creates clients, controls which servers they may connect to, enforces security policy, handles the user's authorization decisions, and coordinates the AI model. The client is created by the host and talks to exactly one server: it attaches the client's protocol version and capabilities to its requests, routes messages in both directions, manages subscriptions and notifications, and keeps that one server's connection isolated from any other.
A host connected to three MCP servers runs three client instances, one per server. Each client only ever knows about its own server. The host is the thing that aggregates what the clients bring back and decides what the model actually sees, which is also why a server cannot read the rest of the conversation or see into another server's connection: the spec puts that isolation at the host, not the client.
In most chat apps the host and the client are bundled into the same binary, so "client" and "the app" get used for the same thing. The distinction matters once an app connects to more than one server, or once you write code that calls a server directly: that code is the client, whatever else it does.
What a client does when it connects
A client's first job is the handshake the spec calls initialization. The client sends an initialize request with the protocol version it supports, its capabilities, and its own name and version. The server answers with its capabilities, its name and version, and the protocol version it is willing to use, the same one the client offered if it supports it, otherwise another one it does support. The client then sends an initialized notification, and only after that may either side send requests other than a ping.
Capabilities are declared on both sides, and each one unlocks specific messages. On the client side the spec lists roots, sampling, elicitation, tasks and experimental. On the server side it lists prompts, resources, tools, logging, completions, tasks and experimental. A server that never declared tools cannot receive a tools/call, and a client that never declared sampling will never be asked for one.
The spec's newest revision, 2026-07-28, changes this: it describes MCP as stateless, with every request carrying its own protocol version and capabilities, and capability discovery done through a server/discover call instead of a single initialize exchange. The Python SDK 2.3.0 supports both. Its Client class, in its default mode, probes the server with server/discover first and falls back to the initialize handshake if the server is older. A client that sends initialize gets 2025-11-25, the newest handshake version that SDK knows. The exchange below is that handshake, written by hand, because it shows both sides declaring capabilities in one round trip.
Three things a client can offer back
A server can only ask a client for these if the client declared the matching capability during initialization.
Sampling lets a server ask the client to run a model completion, so the server does not need its own model access or its own API key. The client stays in control: it can show the user the prompt the server wants to send, let them edit or refuse it, and show them the result before it goes back to the server.
Roots let a client tell a server which filesystem directories it should work in, as file:// URIs. Roots are advisory. The spec is explicit that servers "SHOULD respect root boundaries," not that they must enforce them, because a server runs code the client cannot inspect. The actual boundary, if you need one, is the operating system: file permissions or a sandbox.
Elicitation lets a server pause mid-task and ask the user a question through the client's own interface, with a schema the client validates the answer against. The spec says elicitation must never be used to request a password or an API key, and that clients should warn the user about a request that looks suspicious.
A client that declares none of these three still works for ordinary tool calls. It just means a server can never ask it for a completion, a filesystem boundary or a mid-task answer.
A real initialize exchange
This is a real exchange with a local server. The server is a few lines of Python with one tool, using mcp 2.3.0 (MCPServer, the name FastMCP had in mcp 1.x), run over stdio with uv run. The client here is a small script that opens the subprocess and writes the JSON-RPC messages by hand, so you can see exactly what crosses the pipe.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {},
"elicitation": {}
},
"clientInfo": {
"name": "demo-raw-client",
"version": "0.1.0"
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"prompts": { "listChanged": false },
"resources": { "listChanged": false, "subscribe": false },
"tools": { "listChanged": false }
},
"protocolVersion": "2025-11-25",
"serverInfo": {
"name": "demo-server",
"version": ""
}
}
}The client offered 2025-11-25 and the server echoed it back, so no fallback negotiation was needed. The client also declared roots, sampling and elicitation, which makes all three available to the server. Nothing in the server's response says whether it will use them. After this, the client sends notifications/initialized and only then follows up with a tools/list request, which this server answers with one tool, add, and its input and output schemas.
Real MCP clients
The protocol's own client extension matrix lists, as of October 2026: Claude (web), Claude Desktop, VS Code GitHub Copilot, Microsoft 365 Copilot, Goose, Postman, MCPJam, ChatGPT, Cursor, Archestra.AI, PostHog Code, fast-agent, MCP Inspector and mcpc MCP CLI. That same page is a matrix of which of them support each optional extension (MCP Apps, OAuth client credentials, enterprise-managed authorization, skills over MCP). Not every client supports every extension, and new ones are added as projects submit a pull request against the page.
Where Statio fits
A client does not know whether the thing on the other end of its one connection is the server itself or a gateway in front of it. Connect a client to a server through Statio and it still looks like one server with one list of tools. Statio decides, per tool, whether the connecting agent's identity may call it, with a deny always overriding a grant, and it injects the credential for that call at the time it is made rather than handing the key to the agent. Each agent gets its own identity with a token that expires after 5 minutes, and every call lands in an activity log kept 30 days, with an audit log kept 90 days (longer on Enterprise plans).
Next
What is an MCP server? covers the whole picture this article zooms into. MCP vs APIs explains what a server does with the requests a client sends it. Local vs remote MCP servers covers the other side of the connection a client opens. How to add MCP servers to Claude Code and How to set up MCP servers in Cursor are two real hosts configuring their clients.
Questions
initialize handshake, one version in the protocolVersion field, usually the newest its SDK supports for that handshake (2025-11-25 in the Python SDK 2.3.0). Under the current 2026-07-28 revision, every request carries its own protocol version instead.