Model Context Protocol, MCP, is an open specification for how an application that hosts a language model connects to the outside systems that model needs context from or wants to act on. It was published by Anthropic and is now maintained as an open, vendor-neutral project. The problem it solves is combinatorial: without a shared protocol, connecting three model-hosting applications to four data sources means writing something close to twelve separate integrations, each with its own authentication, schema and error handling. MCP gives both sides one interface to build against instead.
The three roles, and why they are kept separate
MCP defines a client-server relationship with three named participants, and the separation between them is the part of the design worth understanding, because it is what determines who trusts whom.
- The host is the application a person is actually using, a chat client, an editor, an IDE extension. It is the one that talks to the language model.
- The client is a component the host creates, one per connection, that speaks MCP to a single server and keeps that connection alive.
- The server is a separate program that exposes context or capability, a connection to a filesystem, a database, an internal ticketing system, over MCP.
A host that connects to several servers, say a filesystem server and a company wiki server, creates a separate client for each one. Each server is a distinct program that can run locally on the same machine, communicating over standard input and output, or remotely over HTTP. The host never talks to a server directly; every exchange passes through the client instance dedicated to that connection. That indirection matters operationally more than it looks: it means a compromised or badly written server is contained to the client that owns its connection, rather than having a direct channel into the host’s own state.
What a server can actually offer
MCP defines a small number of things a server can expose, and keeping them distinct is deliberate because each has different implications for how a client should treat it.
| Primitive | What it is | Practical example |
|---|---|---|
| Tools | Functions the model can ask the host to invoke | A tool that runs a SQL query against a specific database |
| Resources | Data the client can read and add to context | The current contents of a configuration file |
| Prompts | Reusable templates for structuring a particular interaction | A template with the few-shot examples a support-ticket tool needs to be used correctly |
A client discovers what a server offers by asking for it, rather than the connection carrying a fixed, hardcoded list. This is what makes the ecosystem composable: an application does not need to know in advance which servers a user will connect, only how to ask any MCP server what it provides and how to call what it finds.
The sequence below is what one tool call actually looks like end to end, from the moment a client connects to a server through to the model receiving a usable result.
One tool call, from connection to result
Client connects to server
A dedicated connection, one client per server
A host talking to several servers keeps them isolated; a compromised server cannot reach another client’s connection
Discovery
The server’s supported capabilities and protocol version
Skipping this and guessing capabilities is how a client ends up calling a method the server never declared
Tool list request
The names, descriptions and argument schemas the server currently offers
A stale cached list served after the tools changed sends the model information the server has already withdrawn
Tool call
A specific tool invoked with arguments matching its schema
The server executes this, not the model; the model only ever supplied the request
Result returned to the model
The tool’s output folded back into the conversation as context
The model treats this as fact once it is in context, so a wrong or manipulated result is taken at face value
The transport is separate from the content
MCP separates two concerns that are easy to conflate: the format of the messages exchanged, which is fixed, and the channel they travel over, which is chosen per connection. Every exchange uses JSON-RPC, a simple, well established remote procedure call format, regardless of transport.
The specification, as of revision 2026-07-28, defines two standard transports and calls them bindings, on the grounds that a binding decides how messages are framed and delivered rather than what they mean:
| Transport | How it works | Typical use |
|---|---|---|
| stdio | Newline-delimited messages over the standard streams of a subprocess the client launches | A server running on the same machine as the host |
| Streamable HTTP | Each message is an HTTP POST to a single endpoint; the reply is either a JSON object or a request-scoped event stream | A remote or hosted server |
It is worth being precise about the second, because older material describes an HTTP transport paired with a separate server-sent-events transport. That is no longer the shape. There is one HTTP binding, and server-sent events are one of the two forms its reply can take, not a transport of their own.
The specification also states that protocol semantics are identical on every transport, and that custom bindings are permitted provided they preserve the message format and the per-request metadata model. So a local filesystem server and a remote, hosted incident management server look identical to the code that calls them: the same request and response shapes, over a different pipe.
What the messages actually look like
The abstraction above is easier to trust once you have seen the traffic. Discovery is a tools/list
request, and the server answers with the schema of everything it currently offers:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"tools": [
{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
}
}
],
"ttlMs": 300000,
"cacheScope": "public"
}
}
Invocation is tools/call, naming one of those tools and supplying arguments that satisfy its
schema:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "New York" } }
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "Current weather in New York: 72F, partly cloudy" }],
"isError": false
}
}
Four details in that exchange are load-bearing and are easy to miss.
Those examples are incomplete, deliberately. The specification omits request metadata from its
own examples for brevity, while requiring it: every request must carry _meta fields giving the
protocol version, client information and client capabilities. A client that sends the traffic
exactly as printed above will not interoperate.
isError is part of the result, not a JSON-RPC error. A tool that fails in the ordinary course
of business, a query returning nothing, an API refusing, reports that inside a successful response
so the model can read what went wrong and react. Protocol-level errors are a different channel. The
distinction matters when writing a client, because treating a tool failure as a transport failure
loses the information the model needed.
The tool list is cacheable and ordered on purpose. The response carries ttlMs and
cacheScope, and the specification asks servers to return tools in a deterministic order, partly so
clients can cache and partly to improve prompt cache hit rates when the list is placed in model
context. Tool descriptions are tokens you pay for on every turn.
The set may vary by authorisation but not by connection. A server may return only the tools a caller’s granted scopes permit, because credentials are treated as per-request input. It may not vary the set as a side effect of other requests on the same connection.
Names collide when you aggregate
Tool names are unique within a server, and that is the only scope the specification guarantees. A
client or proxy connecting to several servers can therefore meet two search tools, and the
specification explicitly says such clients should implement a disambiguation strategy such as
prefixing with a server identifier. Anyone building an aggregating host should design for this
before it happens, because the failure is a model calling the wrong system with well-formed
arguments.
The standard does not supply the trust decision
The specification defines how a tool is described and called. It does not, and cannot, decide whether a given server should be trusted, and that gap is exactly where the risk sits. Connecting a host to an MCP server is functionally the same event as installing a dependency: the server’s author, not the host’s author, wrote the code that will run when the model asks it to. An MCP server can misdescribe what a tool does, ask for more access than the task requires, or place instructions inside a resource’s content aimed at the model rather than at the person reading it, none of which the protocol itself detects.
The consequence, worked through in more detail in excessive agency and supply chain, is that adopting MCP does not remove the need to apply the same scrutiny to a server that a team would apply to any other third-party code: reviewing what it actually does, scoping the credentials it is handed to the minimum the task needs, and treating anything it returns as untrusted input to the model rather than as verified fact.
The specification agrees, and says so normatively rather than as advice. On the metadata a server attaches to describe how its tools behave, it states that clients must consider tool annotations untrusted unless they come from a trusted server. A server’s own account of whether a tool is read-only or destructive is a claim by the party being assessed.
On execution, it states that for trust and safety there should always be a human in the loop with the ability to deny tool invocations, and asks applications to make clear which tools are exposed, to indicate visibly when one is invoked, and to present confirmation prompts. That is the protocol pointing at a control it cannot itself implement, and it is worth reading alongside what is known about how such gates behave over time, which is covered under human in the loop.
When it is not the right tool
MCP earns its cost through reuse across a boundary. Where that boundary does not exist, the indirection is overhead.
| Reach for MCP when | Call the function directly when |
|---|---|
| The capability is consumed by more than one host or framework | One application owns both the caller and the callee |
| The server is maintained separately from the client, by another team or vendor | The code lives in the same repository and ships together |
| Tools need to be discovered at runtime rather than compiled in | The tool set is fixed and known when the application is built |
| Isolating the integration from the host is worth a process boundary | Latency budget cannot absorb an extra hop and a serialisation round trip |
The honest summary is that an MCP server wrapping a function only your own application calls has bought you a subprocess, a serialisation format and a version-compatibility surface, in exchange for an interoperability benefit nobody is collecting. That trade is worth making the moment a second consumer appears, and not usually before.
Where this sits relative to the frameworks
MCP standardises how tools are described and reached. It says nothing about how an application decides which tool to call next, retries a failed step, or keeps state across a long-running task, that is the job of an orchestration layer built on top, covered in LangChain and LangGraph. The two concerns compose: a framework can call tools that happen to be exposed over MCP exactly as it would call a function defined in its own codebase, because from the model’s point of view a tool is a tool regardless of which side of an MCP connection implements it.
Where Model Context Protocol sits
How this component relates to the rest of the stack. Items without a link are not covered here yet.
Alternatives
Solves a similar problem differently
- Bespoke tool APIs