MCP Architecture Components and Data Flow
How MCP collapses the N×M integration problem into N+M through standardized roles.

MCP solves what Anthropic called the N×M problem. Before the protocol existed, every AI app needed its own custom connector for every external tool, so N apps times M tools meant N×M separate pieces of glue code. Anthropic introduced Model Context Protocol in November 2024 to collapse that math down to N+M: build one server per tool, and any client that speaks the protocol can use it without extra work. In December 2025, Anthropic handed MCP over to the Agentic AI Foundation, a Linux Foundation project co-founded with Block and OpenAI and backed by Google, Microsoft, AWS, Cloudflare, and Bloomberg. That handoff tells you what MCP actually is. It was never meant to stay one company's product, and any team still treating it as an Anthropic thing is behind.
Earlier fixes chipped away at pieces of the same problem: OpenAI's 2023 function-calling API, the ChatGPT plugin framework. Each one locked you into a vendor. MCP's structural move borrows an old trick from software tooling: the Language Server Protocol did the same thing for code editors back when every IDE needed its own custom integration for every programming language. Decouple the two sides, standardize what passes between them, and nobody has to write bespoke glue anymore.
Every design choice downstream exists to make that N+M math hold up no matter which model or tool gets paired: separate roles for host, client, and server, typed primitives, a strict handshake before anything runs. What follows walks through how that actually works, piece by piece.
The three-role architecture: host, client, and server
MCP splits responsibility three ways: host, client, server. Each does a different job, and none of them overlap. Once that division clicks, the rest of the data flow makes a lot more sense. Skipping this part makes the rest of the protocol read like a pile of arbitrary rules instead of a system with a reason behind every seam.
The host is the app the person actually uses: Claude Desktop, Cursor, Raycast, or some agent runtime built in-house. It runs the LLM interactions, decides what the user is and isn't allowed to do, and manages every client connection underneath it. Trust lives in the host. User consent, credential scope, per-tool allow lists, all of it sits with the host, because MCP itself enforces none of it. A single host can hold open connections to several servers at once, and its context window only ever carries intents and handles, never raw credentials. That's the whole reason a compromised server can't just walk off with an API key sitting in context.
The client sits inside the host, sometimes as a separate process, sometimes not, and manages the back-and-forth with a server: discovering what capabilities exist, formatting requests, tracking session state, handling retries and streaming responses. Each client talks to exactly one server, a strict 1:1 relationship. A host runs multiple clients side by side, though, which is how it holds conversations with several servers at once without their states bleeding into each other. IBM's BeeAI, Microsoft Copilot Studio, Claude Desktop, Windsurf Editor, and Postman all implement this client layer, and SDKs exist in TypeScript, Python, Go, and C# for anyone building their own.
The server is an independent process that exposes tools, resources, and prompts in a format any client can discover and call. The language it's written in doesn't matter. Reference implementations ship in TypeScript and Python, with SDKs also available in Go, Java, Kotlin, C#, and other languages. Plenty of servers are thin wrappers: one source pegs a typical REST API adapter at under 200 lines of code. They run locally as a subprocess or sit remotely on something like Vercel or Cloudflare. Cursor, Claude Desktop, and VS Code are the common hosts you'll see pairing with them.
A fourth piece gets skipped over more than it should: the context store. It holds onto persistent or temporary state across tool calls and sessions, things like which files are open, what the user did three steps ago, the current working directory. Without it, every tool call is a blank slate from the model's point of view. With it, a conversation actually builds instead of resetting every turn.
"Host" and "client" get used interchangeably in casual conversation, and that's sloppy. The host is the whole application. The client is just the communication layer living inside it, and mixing the two up is where a lot of security confusion starts.
What servers expose: tools, resources, prompts, and sampling
Servers expose capabilities through a small set of primitives, and each one carries a different governance profile. The risks and controls around a tool are not the same as the ones around a resource. Treating them as interchangeable is how servers end up over-permissioned. This is not a hypothetical; it's the most common misconfiguration in the ecosystem right now.
Tools are executable functions, each with a name, a plain-language description, and a JSON Schema defining its inputs. They're model-controlled: the LLM decides when to call one based on context, no explicit user click required. As of the 2025-06-18 spec update, tools can return structured output and resource links in addition to plain text. Tools are also the primitive everyone implements first. Most community servers out there expose Tools and nothing else. The description field is plain text the model reads directly, and nothing in the protocol sanitizes it. A malicious server can bury instructions inside that description that the model will follow. That's tool poisoning, a real attack surface, and one big enough to deserve its own treatment elsewhere.
Resources are read-only: files, database rows, config values, anything identified by a URI. Some are static, like a config file that never changes. Others are dynamic, like a live database record. Either way, resources are context the model reads, not actions it performs, and they need their own data classification rules given what they might expose. Servers increasingly pair Tools with Resources together, while Prompts and other optional capabilities lag behind in adoption.
Prompts are reusable templates a server hands to the client, shaping how the model asks a question or explains something. The server defines them, and the client decides how to surface them to the user or the model.
Then there's sampling, the primitive that gets left out of most conversations, probably because it flips the usual flow. It lets a server ask the client to run an LLM completion on its behalf, since the client is the one holding the model access and the API keys. A server can kick off a multi-step workflow this way, even one that needs a human to sign off partway through, without ever holding its own model credentials. Sampling gets deprecated in the 2026-07-28 revision, so check which spec version a given implementation targets before building against it.
Elicitation showed up in the 2025-06-18 update to fill a related gap: it lets a server ask the user for more information mid-workflow, without breaking the flow or bypassing the user's control over what they share.
What a server chooses to expose determines what kind of oversight it needs, full stop. A server offering Tools only calls for different guardrails than one handing out Resources full of sensitive records. Any security review that treats them the same is missing the point.
Message encoding and transport: JSON-RPC 2.0 and the transport options
Every MCP message rides on JSON-RPC 2.0, encoded in UTF-8, under the 2025-06-18 spec. That's the whole data-layer contract: requests, responses, and notifications. Three shapes, nothing else counts as valid MCP traffic.
How those messages travel is a separate question, and MCP gives two answers depending on the situation.
Stdio is the local option. The client launches the server as a subprocess and messages pass over standard input and output. It's the default for local setups in Claude Desktop, Claude Code, and Cursor, with essentially no network overhead and no authentication to manage. But it also has no way to reach anything remotely. It lives and dies on the local machine.
Streamable HTTP is built for anything remote. One HTTPS endpoint handles it all: POST for JSON-RPC requests, GET for an optional Server-Sent Events stream carrying responses back. It replaced the older HTTP+SSE transport as the default for remote and multi-tenant servers starting with the 2025-03-26 spec, and it supports bearer tokens, API keys, and custom headers, with OAuth recommended for getting those tokens. This is what lets a server run on Vercel or Cloudflare Workers and serve more than one tenant at a time.
The 2026-07-28 revision made a bigger change, and it's the right one: it dropped protocol-level sessions entirely, along with the Mcp-Session-Id header, and made the transport stateless. Any server instance sitting behind standard HTTP infrastructure can now answer the same request, no sticky sessions required. A server can run inside a single Cloudflare Worker with no stateful infrastructure behind it at all, cutting deployment complexity down substantially. Teams that built session-dependent implementations before this change need to migrate, and the deprecation policy lays out the timeline for that. Holding onto session state was never going to scale cleanly across serverless infrastructure. This wasn't really a choice, more a correction that was overdue.
What makes all of this work together is a deliberate split: the transport layer and the data layer don't touch. The same JSON-RPC 2.0 messages travel over stdio or Streamable HTTP without the application logic caring which one it's on. That separation is what actually makes N+M hold up once real systems are getting deployed rather than designed on a whiteboard.
The three-phase connection lifecycle: initialization, operation, and shutdown
MCP runs through three phases in strict order: initialization, operation, shutdown. No skipping ahead, and no shortcuts that skip capability negotiation to save a round trip.
Initialization starts the moment the transport connection opens. The sequence is fixed:
The client sends an initialize request stating its protocol version and what it supports.
- The server responds with its own version and its own capabilities.
- The client sends a
notifications/initializedmessage confirming it accepted the server's answer and is ready to go.
Capability negotiation happens right here. Whichever optional features are on the table, file system roots, sampling, logging, resource subscriptions, get settled before anything else runs. If the two sides don't agree on a protocol version, the server offers one it does support, and if the client still can't work with that, it disconnects cleanly rather than limping along in an undefined state. Front-loading all this means nothing about the session's scope is ambiguous once real work starts, and the extra round trip is a small price for that certainty.
Operation is where the actual work happens. The client queries the server at runtime to discover what tools, resources, and prompts it exposes. A typical tool call moves like this: the host decides, based on the user's input and the model's reasoning, that it needs some outside capability. The client sends a tool execution request with parameters the LLM generated. The server runs it and sends results back. The client relays those results to the LLM, which reasons over them or generates a final answer. The user sees the result.
That runtime discovery is what makes agents feel adaptive rather than hardcoded. There's no fixed manifest of tools baked in at build time. For anything that takes a while, like transcribing a video, servers stream progress events so the host can keep the UI updated instead of leaving the user staring at a spinner.
Shutdown doesn't get its own protocol message, and that's by design, not an oversight. It happens at the transport level instead. Over stdio, the client closes the server's stdin, waits for it to exit, and escalates to SIGTERM and then SIGKILL if it doesn't. Over HTTP, closing the connection is the signal. Ending things is transport's job, not application code's, and MCP keeps it that way.
Advanced spec features that extend or reshape the data flow
None of what follows is speculative. These features either shipped in the 2026-07-28 spec revision or are active extensions, and any team evaluating MCP for production work needs to account for them now, not later.
Caching arrived in that revision under SEP-2549. List and resource-read results now carry ttlMs and cacheScope fields, modeled on the same idea as HTTP's Cache-Control header. A client knows precisely how long a tools/list response stays fresh and whether it's safe to reuse across different users. That second part is a governance boundary, not a performance detail: cache scope marks a data-classification line, and a resource result scoped to one user should never get served out of cache to somebody else.
Observability, under SEP-414, documents W3C Trace Context propagation inside the _meta field, locking down the exact key names for traceparent, tracestate, and baggage. A trace started in the host application can follow a single tool call through the client SDK, into the MCP server, and out to whatever downstream service handles it, all showing up as one connected span tree in any OpenTelemetry-compatible backend. That beats piecing together an audit log after something's already gone wrong. A postmortem tells you what broke after the fact. A live trace tree catches it while it's happening, and that difference is the whole point.
MCP Apps lets a server ship an actual interactive HTML interface that the host renders inside a sandboxed iframe. The host renders the interface inside a sandboxed iframe, keeping the UI isolated from direct execution. Any action a user takes inside that UI still routes through the host rather than bypassing it. A UI is exactly where users let their guard down, so there's no separate, looser trust model for the interface layer. Amplitude, Asana, Box, Canva, Clay, Figma, Hex, Monday.com, and Slack are named as launch partners for this feature.
The Tasks extension rethinks the lifecycle for a stateless world, letting servers handle long-running or asynchronous work without blocking the connection. The extension is designed to integrate cleanly with the rest of the protocol's capability negotiation model. That combination cuts deployment complexity for teams running asynchronous workflows in serverless environments.


