Skip to content

MCP: The Model Context Protocol, Explained

6 min read · updated August 3, 2026

MCP is an open protocol for connecting an LLM application to external tools and data. It is worth understanding precisely, because most of the confusion around it comes from people evaluating it as a framework when it is a wire protocol — and because for a large number of applications, the right answer remains a Python function.

The problem it solves

Before MCP, every integration was written twice: once for the tool and once for the client. Ten clients wanting ten integrations is a hundred bespoke adapters, and each new client starts from zero. That is the N×M problem, and it is the same one that LSP solved for editors and language tooling — the analogy is not incidental, MCP is openly modelled on it.

The fix is an interface in the middle. Write one server for your issue tracker and any conforming client can use it; write one client and it can use every server. N×M becomes N+M. That is the entire value proposition, and it tells you exactly when the protocol is worth its overhead: when either N or M is genuinely greater than one.

The shape of the protocol

  • JSON-RPC 2.0 as the message format — requests with an id, responses, and notifications with no id and no reply. Nothing exotic; you can speak it by hand, which matters when debugging.
  • Host, client, server. The host is the application (an IDE, a chat app, your agent). It creates one client per server, and each client holds a single stateful connection to that server. The one-to-one pairing is deliberate: it is what keeps a server’s capabilities and permissions separable.
  • Two transports. stdio, where the client launches the server as a subprocess and speaks over its standard input and output — the common case for local tools, with no ports and no authentication problem. And Streamable HTTP for remote servers, where a single endpoint handles POSTed requests and may upgrade to server-sent events for streaming.
  • Versioned by date. The protocol version is a date-shaped string exchanged at initialisation, and clients and servers negotiate it. A version mismatch is a first-class, handleable condition rather than a mystery.

Three primitives, three controllers

A server may offer three kinds of thing, and the specification is explicit about who is meant to decide when each is used. This is the most useful part of the design and the part most summaries skip.

PrimitiveDescription
Tools (model-controlled)Functions with a name, a description and a JSON Schema inputSchema. Listed with tools/list, invoked with tools/call. The model decides when to call them, which is why the description is a prompt in the sense of the tool-description page.
Resources (application-controlled)Readable context identified by URI -- a file, a table, a document. resources/list and resources/read, with optional templates and subscriptions. The host application decides what to include; the model does not fetch these on a whim.
Prompts (user-controlled)Named, parameterised message templates that a user explicitly invokes, via prompts/list and prompts/get. Slash commands, essentially. The least used of the three and the most underrated for making a server usable.

There are also features flowing the other way, from server to client: sampling/createMessage lets a server ask the host to run an LLM completion on its behalf (so the server needs no API key of its own), roots let a client tell a server which filesystem or URI boundaries it may operate within, and elicitation lets a server request additional input from the user mid-operation. Change notifications such as notifications/tools/list_changed let a server alter its own catalogue at runtime, which is genuinely useful and worth handling rather than ignoring.

Lifecycle and capability negotiation

Three messages before anything useful happens, and knowing them is what lets you debug a connection that silently does nothing:

client -> server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
  "protocolVersion":"2025-06-18",
  "capabilities":{"roots":{"listChanged":true},"sampling":{}},
  "clientInfo":{"name":"example-host","version":"1.4.0"}}}

server -> client
{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2025-06-18",
  "capabilities":{"tools":{"listChanged":true},"resources":{}},
  "serverInfo":{"name":"issues","version":"0.3.1"}}}

client -> server   (a notification: no id, no reply)
{"jsonrpc":"2.0","method":"notifications/initialized"}

The capabilities objects are the negotiation. A server that does not advertise resources will reject resources/list, and a client that did not advertise sampling must not be sent a sampling request. Both sides are expected to check rather than assume, which is what allows the protocol to grow without breaking older implementations.

After initialisation the client typically calls tools/list, maps each entry’s inputSchema onto whatever tool format its model provider expects, and from then on translates model tool calls into tools/call. That translation layer is thin — which is the point, and also the reason MCP does not replace the loop in the agent loop. It replaces where the tools come from, not what you do with them.

The trust boundary you just created

Installing a third-party MCP server is running third-party code with access to whatever you connected it to. Beyond that obvious point, three properties are specific to this protocol and worth internalising:

  • Tool descriptions are untrusted text in your model’s context. A server supplies its own descriptions, and they are injected into the prompt. A malicious description can contain instructions. This is prompt injection with an install step, and the fact that the server was reputable when you installed it does not constrain what it serves tomorrow — a server can change its catalogue at any time and announce it with a notification.
  • Human approval is a specified expectation, not an implementation detail. The specification’s security guidance is explicit that hosts should obtain user consent before invoking tools and before honouring sampling requests. If you are writing a host, that is your job, and it connects directly to where the approval gate goes.
  • Remote servers inherit web problems. HTTP-transport servers are told to validate the Origin header (DNS rebinding reaches a localhost server from a web page), to bind to localhost rather than all interfaces when local, and not to pass client tokens through to upstream services. A locally bound server on 0.0.0.0 with no origin check is reachable from any page the user visits.

When plain functions are the better answer

MCP buys interoperability, and interoperability has a price: a process boundary, a serialisation hop, a subprocess or an HTTP dependency to supervise, a handshake to debug, and a schema you now maintain in a format rather than in your type system. Pay it when you get something back. Do not pay it when:

  • You are the only client. A tool used by one agent you also wrote gains nothing from a protocol. A decorated function is faster, typed, debuggable in one stack trace, and testable without a transport.
  • The tool needs your application’s internals. If it wants your ORM session, your request context or your in-process cache, the boundary is in the wrong place and you will spend your time serialising things across it.
  • Latency is tight. Every call is a round trip and, over stdio, a subprocess you must keep alive and healthy. For a microsecond function this is pure overhead.
  • The catalogue is large. Connecting five servers with twenty tools each puts a hundred schemas in every request — the arithmetic in how many tools is too many applies with full force, and MCP makes adding tools easy enough that it happens by accident.

The clean rule: use MCP at the boundary between organisations or between applications, and plain functions inside one. If you are publishing a connector for other people’s agents, or consuming somebody else’s, the protocol is doing exactly the job it was designed for. If you are wrapping your own database in a subprocess so your own agent can query it, you have added a hop and gained a specification.

MCP: The Model Context Protocol, Explained · Multigrid