Before MCP, connecting an AI assistant to your tools was an N×M problem and everyone felt it. Every assistant (Claude, an IDE, some internal agent) needed a bespoke integration with every system it should reach (GitHub, Postgres, Jira, your internal API), and each integration was a one-off — different auth, different shapes, all of it reinvented per pair. Build five assistants that each talk to ten systems and you've written fifty integrations that don't compose. The Model Context Protocol is the boring, correct fix: turn N×M into N+M by standardizing the interface in the middle, the way USB-C did for chargers.
MCP is an open protocol that standardizes how applications provide context and tools to large language models. Introduced by Anthropic in late 2024, it spread through 2025 because the problem was universal and the design is deliberately simple — it borrows the proven shape of the Language Server Protocol that did the same thing for editors and language tooling. I'll walk the architecture, the three primitives, the transports, a real tool-call flow, and — most importantly — the security questions it forces you to answer.
The architecture: host, client, server
MCP has three roles, and keeping them straight is most of the battle:
- Host — the AI application the user interacts with (a desktop assistant, an IDE, an agent runtime). It contains the LLM and decides what to connect to.
- Client — a connector living inside the host, one per server, that speaks the MCP protocol. The host spins up a client for each server it wants to use; the client maintains a 1:1 session with that server.
- Server — a separate program that exposes a specific system's capabilities (a GitHub server, a filesystem server, a database server) in MCP's standard vocabulary. Servers are the reusable, shareable unit — write one, and any MCP host can use it.
graph TD
subgraph HOST["Host application (contains the LLM)"]
LLM["LLM"]
CL1["MCP client A"]
CL2["MCP client B"]
end
S1["MCP server: GitHub
(tools, resources, prompts)"]
S2["MCP server: Postgres
(tools, resources, prompts)"]
EXT1["GitHub API"]
EXT2["Database"]
LLM --- CL1
LLM --- CL2
CL1 <-->|"MCP / JSON-RPC"| S1
CL2 <-->|"MCP / JSON-RPC"| S2
S1 --> EXT1
S2 --> EXT2
MCP's three roles. The host holds the model and runs one client per server, each client keeping a 1:1 session with its server over JSON-RPC. Servers wrap real systems and expose them in MCP's standard primitives. Because the interface is standardized, a server written once works with any host — that's the N×M-to-N+M collapse.
The three primitives: tools, resources, prompts
A server exposes its capabilities as three kinds of things, and the distinction between them is genuinely important — it encodes who is in control of each interaction.
| Primitive | What it is | Controlled by | Example |
|---|---|---|---|
| Tools | Functions the model can invoke to take action or fetch data | Model (decides when to call) | create_issue, run_query |
| Resources | Read-only data the host can load into context | Application | A file's contents, a record, a schema |
| Prompts | Reusable, parameterized prompt templates / workflows | User (invokes deliberately) | A "/review-pr" template |
Tools are model-controlled: the model sees their descriptions and chooses to call them — this is the action surface, and the one with teeth. Resources are application-controlled context the host pulls in (a file, a row, an API response) without the model deciding to "act." Prompts are user-controlled templates surfaced as slash-commands or menu items the user picks on purpose. The clean way to hold it: tools are for the model to do, resources are for the app to show, prompts are for the user to start. Mixing them up — exposing a destructive action as a casual "resource," say — is how MCP servers get designed badly.
Transports: JSON-RPC over stdio or HTTP
Underneath, MCP is JSON-RPC 2.0 — a simple request/response (and notification) message format. Client and server exchange JSON-RPC messages to negotiate capabilities, list tools/resources/prompts, and invoke them. There are two main transports:
- stdio — the host launches the server as a local subprocess and talks to it over standard input/output. Simple, fast, no network; ideal for local tools like a filesystem or a local database. This is the most common way local servers run.
- HTTP-based transport — for remote servers reached over the network (with Server-Sent Events for streaming server-to-client messages). This is what lets a hosted, multi-user MCP server exist, and it's where authentication and authorization (OAuth-style flows) become essential.
The session begins with a capability handshake: the client and server each declare what they support, so the protocol can evolve and features degrade gracefully. After that, the client can ask tools/list to discover what's available and tools/call to invoke one. The shape is intentionally familiar to anyone who's implemented an LSP server.
// Client discovers tools
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
// Server responds with tool definitions (name, description, JSON-Schema inputs)
{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [
{ "name": "run_query",
"description": "Run a read-only SQL query against the analytics DB",
"inputSchema": { "type": "object",
"properties": { "sql": { "type": "string" } }, "required": ["sql"] } }
] } }
// Later: the host asks the server to actually invoke it
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "run_query", "arguments": { "sql": "SELECT count(*) FROM orders" } } }
How a tool call actually flows
Putting it together end to end, because the control flow surprises people — the server never talks to the model directly:
- On connect, the client calls
tools/list; the host gives the model the available tool definitions (names, descriptions, input schemas) as part of its context. - The user asks something. The model decides a tool would help and emits a request to call it with arguments.
- The host intercepts that — typically pausing for user approval on anything consequential — then tells the client to issue
tools/callto the server. - The server executes against the real system (hits the GitHub API, runs the SQL) and returns the result over JSON-RPC.
- The host feeds the result back into the model's context, and the model continues — answering, or calling another tool.
The load-bearing detail: the model proposes, the host disposes. The model only ever requests a tool call; the host is the gatekeeper that decides whether to actually execute it. That boundary is exactly where human-in-the-loop approval and policy enforcement live, and it's why the host/client split matters rather than being ceremony.
MCP standardizes the plumbing, not the trust — and that's the part teams skip. An MCP server is code you're letting an AI drive against real systems, and the protocol itself doesn't make that safe. Three things bite. Prompt injection through tool results: content a server returns (a web page, an email, a file) can carry instructions the model then follows — a tool result is untrusted data, not a command. Over-broad tools: a server with a "run any SQL" or "execute shell" tool hands the model the keys; scope tools tightly and prefer read-only. Authentication on remote servers: an HTTP MCP server exposing your systems needs real authn/authz, not an open port. Treat every server you connect like a third-party integration with production access — because that's what it is. Vet it, sandbox it, and keep consequential actions behind explicit approval.
This is the same governance problem that designing auditable multi-agent systems grapples with — a standardized tool interface makes integration easy, which makes controlling what agents can do the actual hard part. MCP gives you the seam to enforce policy at (the host's approval gate); it doesn't enforce it for you.
What to carry away
The Model Context Protocol is an open standard that turns the N×M mess of AI-to-tool integrations into N+M by fixing the interface in the middle. Its architecture is host (the app with the model), client (one per server, inside the host), and server (a reusable wrapper around a real system). Servers expose three primitives that encode who's in control — tools the model calls, resources the app loads, prompts the user invokes. It all rides JSON-RPC over stdio (local subprocess) or HTTP (remote), starting from a capability handshake.
The flow to remember is that the model only ever proposes a tool call and the host decides whether to run it — which is precisely where approval and policy belong. MCP makes connecting capabilities trivial; it deliberately leaves trust to you, so the security work (prompt-injection-aware handling of tool results, tightly scoped tools, real auth on remote servers) is the part that actually determines whether your setup is safe. Write a server once and every host can use it — that reusability is why, a few months after launch, MCP servers are everywhere.