Skip to content

AI

Inside MCP: The Tool Boundary, From Primitives to the Wire

Part two of The Agentic Web: a working tour of the Model Context Protocol's data layer. The three roles of host, client, and server; the split between the data and transport layers; initialization as a capability handshake; the three server primitives (tools, resources, prompts) and the model, application, and user that control each; the client primitives that flow the other way (sampling, elicitation, roots); notifications and the experimental tasks primitive.

Part one drew the map: an agent reaches down to tools over the Model Context Protocol and across to peers over A2A, and the two protocols are shaped by the boundaries they cross. This part goes down. It is a working tour of MCP, the protocol an agent uses to reach a tool, and the goal is concrete: by the end you should be able to read an MCP session and know what every message is for, what the three primitives do, and which direction each one flows.

The good news is that MCP is smaller than its reputation. There is no new transport to learn, no bespoke serialization, no clever framing. It is JSON-RPC 2.0, a handshake, and a short list of primitives.1 Almost everything that confuses people about MCP is a confusion about who is asking whom for what, and once the handshake makes that explicit the rest falls into place. So we will build it up in that order: the roles first, then the handshake, then the primitives in both directions.

Three roles: host, client, and server

MCP names three participants, and keeping them straight is most of the battle.1 The host is the AI application itself, the thing the user actually interacts with: a desktop assistant, an IDE, a chat client. The server is a program that exposes some capability, like a filesystem, a database, or a calendar. The client is the connector in between, and the rule that trips people up is that there is exactly one client per server: the host spins up a separate client for each server it connects to, and each client holds a single dedicated session with its one server.

A language model in the host app drives two MCP clients, each holding a dedicated session with one server (a local filesystem server over stdio and a remote calendar server over HTTP), and each server exposes tools, resources, and prompts back to its client

The thing to notice is the fan-out: one host, many clients, one server each. The host is where the language model lives and where context from all the servers is aggregated; the clients are dumb pipes that keep one conversation each. This is the M-plus-N payoff from part one made concrete. The host implements the protocol once and can then talk to any server; a server implements the protocol once and any host can use it. Note also that “server” is a role, not a location: a server can run locally as a subprocess on your machine or remotely behind an HTTP endpoint, and the diagram shows one of each. Where it runs is a transport question, which is part three; what it exposes is the same either way.

Two layers: data and transport

MCP is deliberately split into two layers, and the split is worth internalizing because it is why the protocol feels stable even as its plumbing changes.1 The data layer is the JSON-RPC protocol itself: the message shapes, the lifecycle, the primitives. The transport layer is how those messages physically move: over standard input and output to a local subprocess, or over HTTP to a remote service. The data layer is the inner layer and the transport is the outer one, and the same JSON-RPC messages ride unchanged over either transport.

This part is entirely about the data layer, because that is the part you reason about when you design a tool. The transport, the authorization that rides with it, and the security model that the transport forces all wait for part three. The clean line between them is a feature: you can write an MCP server against the data layer and let the SDK decide whether it is reached over a pipe or a socket.

Initialization is a capability handshake

Nothing in MCP happens until the two sides agree on what they each support. The session opens with an initialize request from the client carrying a protocol version and the client’s capabilities; the server replies with its own capabilities; the client sends an initialized notification; and only then does real work begin.1 2

A sequence diagram of an MCP session: the client sends initialize with its protocol version and capabilities, the server replies with its capabilities, the client confirms with notifications/initialized, then the client discovers tools with tools/list, invokes one with tools/call, and the server may later push a tools/list_changed notification

Two details in that handshake do real work. The protocol version is a dated string like 2025-06-18, and if the two sides cannot agree on a mutually compatible version the connection is supposed to terminate rather than guess.1 The capabilities object is how each side declares what it can do: a server announces that it offers tools, resources, or prompts, and whether it will send change notifications; a client announces that it supports the reverse-direction primitives we will get to, like sampling or elicitation. Capability negotiation is not ceremony. It is the contract for the rest of the session, and a well-behaved client never calls a method the server did not advertise. Once the handshake completes, the client can discover and use whatever the server offered.

Tools: the actions the model controls

A tool is a function the model can call. Each tool has a name, a human-readable description, and an inputSchema written in JSON Schema that defines its arguments.3 A flight search tool, in MCP’s own running example, looks like this:

{
  "name": "searchFlights",
  "description": "Search for available flights",
  "inputSchema": {
    "type": "object",
    "properties": {
      "origin": { "type": "string", "description": "Departure city" },
      "destination": { "type": "string", "description": "Arrival city" },
      "date": { "type": "string", "format": "date" }
    },
    "required": ["origin", "destination", "date"]
  }
}

The protocol gives tools two operations: tools/list to discover what is available, returning the array of definitions with their schemas, and tools/call to execute one with arguments, returning result content.3 Later spec revisions added an optional outputSchema so a tool can declare the shape of what it returns as well as what it takes, and annotations that hint whether a tool is read-only or destructive, so a client can treat a deleteRecord differently from a searchFlights.4

The defining property of a tool is who decides to call it: tools are model-controlled.3 The application advertises the tools, but the model picks which to invoke and when, based on the conversation. That is exactly the autonomy that makes an agent an agent, and it is also why MCP repeatedly stresses human oversight around tools: applications are expected to surface tool calls in the UI, gate destructive ones behind an approval dialog, and log every execution. The schema guarantees the shape of the call; it never guarantees the call was a good idea, which is a theme part three returns to when a tool’s description turns out to be untrusted input.

Resources: the context the application controls

A resource is read-only data the server exposes for the application to pull in as context.3 Where a tool acts, a resource is read: a file’s contents, a database schema, a calendar’s events, last year’s itinerary. Each resource has a unique URI such as file:///Documents/passport.pdf or calendar://events/2024, and a declared MIME type. The protocol offers resources/list and resources/read, plus resources/subscribe so the application can be notified when a resource changes.

Resources also come in a dynamic flavor, resource templates, whose URIs carry parameters: weather://forecast/{city}/{date} or travel://activities/{city}/{category}.3 These are discoverable and self-documenting, and they support parameter completion, so typing “Par” into a {city} slot can suggest “Paris.” The controlling party here is different again: resources are application-controlled. The host application decides how to retrieve, filter, and present them, whether by a file-tree picker, an embedding search, or automatic inclusion based on the conversation. The protocol deliberately does not mandate a UI; it just hands the application the data and a URI scheme.

Prompts: the templates the user controls

The third server primitive is the one people forget, and it completes a pattern. A prompt is a reusable, parameterized template the server offers, like “Plan a vacation” or “Summarize my meetings.”3 It has a name, a description, and a list of typed arguments:

{
  "name": "plan-vacation",
  "title": "Plan a vacation",
  "arguments": [
    { "name": "destination", "type": "string", "required": true },
    { "name": "duration", "type": "number", "description": "days" },
    { "name": "budget", "type": "number", "required": false }
  ]
}

Discovered with prompts/list and fetched with prompts/get, prompts are user-controlled: they require explicit invocation rather than firing on their own, and applications usually surface them as slash commands (typing / to see /plan-vacation) or command-palette entries.3 That completes the cleanest mental model MCP offers. The three server primitives map exactly onto three controllers: tools are model-controlled, resources are application-controlled, prompts are user-controlled.3 When you are not sure which primitive a capability should be, ask who should decide to use it, and the answer picks the primitive for you. A booking action the model should choose is a tool; background context the app should supply is a resource; a workflow the user should kick off is a prompt.

The primitives that flow the other way

So far everything has flowed down, from the agent to the server. But a tool is dumb on purpose, and there are a few things a server genuinely cannot do for itself, so MCP gives the client a small set of primitives the server can call back into.1 This is the thin upward channel on the down boundary, and it exists for exactly the capabilities a tool lacks.

  • Sampling lets a server ask the client to run a language-model completion on its behalf, via sampling/createMessage. This is how a server gets access to a model without shipping its own model SDK or API key, which keeps the server model-independent.
  • Elicitation lets a server ask the user for more information mid-task, via elicitation/create, when it needs a missing detail or a confirmation. The client owns the UI for that prompt; the server just declares what it needs.
  • Roots let the client tell the server which filesystem or URI roots are in scope, so a server does not go rummaging outside the boundary the host set.
  • Logging lets a server send structured log messages back to the client for debugging.

These are declared in the client’s capabilities at initialization, so a server only uses them if the client advertised support. Two more cross-cutting pieces round out the data layer. Notifications are JSON-RPC messages with no reply: the tools/list_changed in the sequence diagram is one, and it lets a server tell the client its tool list changed so the client re-fetches it, which is how a session stays current without polling.1 And tasks, an experimental addition, wrap a long-running request in a durable handle you can poll for status and retrieve later, for work too slow to answer inline.4 Note the contrast worth carrying into part four: a task here is a thin wrapper bolted onto a call-and-return protocol, whereas in A2A a task is the whole point. Same word, opposite center of gravity, because the boundary on the other side is different.

Takeaways

If you internalize four things from MCP’s data layer, you can read any session:

  1. One client per server, and the host aggregates. The host holds the model and the context; each client is a dedicated pipe to one server. “Server” is a role, not a location, so local and remote servers expose the identical data layer.
  2. The capability handshake is the contract. Nothing is callable until both sides advertise it at initialize. When a method “does not work,” check whether the other side ever declared it, before you check anything else.
  3. The three primitives map onto three controllers. Tools are model-controlled, resources are application-controlled, prompts are user-controlled. To choose a primitive, ask who should decide to use the thing.
  4. The down boundary has a thin channel back up. Sampling, elicitation, roots, and logging exist precisely because a dumb tool has no model, no user, and no sense of scope of its own, so it borrows the client’s. Next part takes this same protocol to the wire: stdio versus Streamable HTTP, OAuth, and the security model that the transport forces.

Footnotes

  1. The roles (host, client, server), the two-layer split (data layer over JSON-RPC 2.0, plus a transport layer), the lifecycle handshake, the primitives, and the experimental tasks wrapper are all defined in the MCP architecture overview. MCP is a stateful protocol negotiated at initialization. 2 3 4 5 6 7

  2. The full initialization and shutdown sequence, including protocol-version negotiation rules, is specified in the lifecycle section of the MCP specification.

  3. Tool, resource, and prompt definitions, their protocol operations (tools/list, tools/call, resources/read, resources/subscribe, prompts/list, prompts/get), the resource-template and parameter-completion mechanics, and the model-controlled / application-controlled / user-controlled framing are all from the MCP server concepts page. The flight-search and plan-vacation examples are MCP’s own. 2 3 4 5 6 7 8

  4. outputSchema (structured tool output) and elicitation landed in the 2025-06-18 revision; tool annotations landed in 2025-03-26; the experimental durable tasks primitive landed in the 2025-11-25 revision. See the spec changelogs: 2025-03-26, 2025-06-18, and 2025-11-25. 2