Understanding MCP
MCP Architecture Deep Dive
Three layers sit between the model and your data. When a server misbehaves, the layer it broke in tells you which file to open — so it is worth being able to name them before you write any code.
The three layers of an MCP server
The Three Layers
1. Transport Layer
MCP supports two transport mechanisms:
| Transport | Use Case |
|---|---|
| stdio | Local processes, CLI tools |
| Streamable HTTP | Remote servers, web-based (introduced in spec 2025-03-26) |
Note: The older HTTP+SSE transport was deprecated in the March 2025 spec update in favor of Streamable HTTP, which uses a single HTTP endpoint and optionally upgrades to SSE for streaming. Legacy SSE servers still work but new projects should use Streamable HTTP.
# stdio transport (local)
server = Server(name="my-server")
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
# Streamable HTTP transport (remote)
server = Server(name="my-server")
async with streamable_http_server(port=8000) as (read_stream, write_stream):
await server.run(read_stream, write_stream)
2. Protocol Layer
The protocol layer handles message formatting using JSON-RPC 2.0:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_database",
"arguments": {
"query": "user emails"
}
}
}
3. Capability Layer
This is where your logic lives - the tools, resources, and prompts you expose.
Message Flow
MCP is a stateless protocol. That one sentence explains most of what follows. Everything needed to process a request lives inside that request; the server infers nothing from what came before, even on the same connection.
This is worth pausing on, because the intuitive model is wrong. There is no login, no session, no opening handshake. An open stdio process is not a conversation — a client is free to interleave unrelated requests down the same pipe, and your server must not treat the connection as identity.
What happens on one request — and only that request
The request carries its own protocol version and client capabilities in _meta. Nothing was negotiated earlier because there was no earlier
Required metadata missing is -32602. Needing a capability the client didn't declare is -32021, listing what was missing
While the request is in flight the server may send notifications scoped to it — progress, log messages. Never a request of its own
One result or one error, carrying a resultType of complete or input_required
Two consequences follow directly, and both bite in practice:
- Your server cannot cache per-connection state. Anything that must outlive a single request — a long-running job, an application handle — needs an explicit identifier the client passes back each time. Storing it against the connection will work in testing and fail the moment a client opens a second one.
- Tool descriptions are re-read, not read once. Under the old session model, a bad description was baked in for the conversation. Under the current one, every request stands alone. This is more forgiving of change and less forgiving of assuming the client remembers anything.
Version note. This describes specification revision
2026-07-28. Earlier revisions (through 2025) used aninitializehandshake opening a connection-scoped session, and permitted servers to initiate requests; the current spec defines backward-compatible fallback so both eras interoperate. If you are reading an older tutorial that opens withinitialize, that is why. The specification is authoritative and moves faster than any course.
Security Model
MCP implements security through:
- Capability-based access: Servers only expose what they choose
- User consent: Hosts must confirm before calling tools
- Sandboxing: Servers run in isolated processes
- No direct network access: Servers can't make arbitrary requests
Important: Never expose sensitive operations without proper authentication and authorization in your MCP server.
Next, we'll set up your development environment for MCP. :::
Sign in to rate