All articles

Model Context Protocol: Capability Contracts and Security Boundaries

Model Context Protocol standardises AI tool and context exchange, but capability negotiation cannot replace consent, validation, isolation or authorisation.

An AI host connects through isolated client sessions to resource, prompt and tool servers behind explicit security gates.

Connecting a model to an external system involves two contracts. The first describes how software exchanges context and invokes functions. The second decides who may see data or cause an action. Model Context Protocol (MCP) addresses the first contract and supplies hooks for parts of the second; it does not make an integration trustworthy by virtue of being interoperable.

The 2025-03-26 revision defines that boundary precisely. A host coordinates isolated clients, servers expose context and operations, and JSON-RPC messages carry a negotiated set of features. Those mechanisms replace bespoke connector plumbing. They leave deployment security with the applications on either side.

A host contains separate clients for three servers, with capability negotiation inside each session and consent, validation and authorisation enforced at the trust boundaries.
MCP revision 2025-03-26 gives each client one isolated, stateful server session. Shared message formats do not merge server state or remove the host’s enforcement duties.

A host coordinates separate trust domains

MCP is not a direct model-to-tool wire protocol. The host is the LLM application: it creates clients, manages their lifecycles, coordinates model integration and controls context aggregation. Each client maintains one stateful session with one server. A host using a filesystem service and an issue tracker therefore has two clients and two isolated connections, not one shared pool of server access.

This separation constrains composition. Servers can offer focused capabilities without receiving the whole conversation or visibility into other servers. The host decides what context crosses each connection. A result returned by one server does not automatically become input to another; the host must route it, and its consent policy must permit that disclosure.

The design also assigns complexity deliberately. Servers implement resources, prompts or tools, while the host handles orchestration and user decisions. A server may request model sampling only when the client declares that capability. Even then, the user should control whether sampling occurs, which prompt is sent and what the server may see. A server-initiated request is not licence to take over the application.

Statefulness makes the session more than a stateless function endpoint. Initialisation, subscriptions and in-flight requests belong to a continuing relationship. Isolation, however, is architectural rather than a sandbox guarantee. A local server still runs code with whatever operating-system permissions it receives, and a remote server still needs its own tenant and credential boundaries.

Negotiation defines a session, not permission

Every connection begins with an initialize request. The client proposes a protocol version, declares capabilities such as roots or sampling, and identifies its implementation. The server answers with a supported version, its implementation details and capabilities such as resources, prompts, tools or logging. After a successful exchange, the client sends the notifications/initialized notification before normal operation.

Capability objects can be more specific than feature names. A resources capability may advertise subscriptions or list-change notifications; a tools capability may advertise changes to the tool list. Both sides should use only negotiated features. If the server answers with a version the client does not support, the client should disconnect rather than continue on hopeful assumptions.

This negotiation is a compatibility mechanism. Advertising tools establishes that tool methods may be used; it does not establish that a particular person may invoke every tool. Likewise, a declared sampling capability does not approve every prompt. Treat negotiated capabilities as an allowlist of protocol behaviour, then apply identity and policy separately.

JSON-RPC 2.0 supplies the message discipline. Requests carry identifiers that match responses to calls; notifications do not expect responses; structured errors report protocol failures. The transport must preserve the lifecycle, and implementations should apply request timeouts. These rules make interactions observable and cancellable at the protocol level. They do not prove that an operation was authorised, or that cancelling a request undid work already performed.

Server primitives carry different control models

MCP distinguishes three kinds of server capability because applications use them differently. Resources expose data by URI. The host application decides whether a user selects a resource, a search retrieves it or application logic includes it as context. Optional subscriptions report changes, while resource templates describe parameterised URIs. A readable URI still requires validation and an access check.

Prompts expose reusable message templates and arguments, normally for explicit user selection. They let a server package a workflow without hiding its operation inside a tool description. Arguments and returned content still require validation: a template is data supplied across a trust boundary, not inherently safe instruction text.

Tools are model-controlled functions intended for discovery and invocation through tools/list and tools/call. Each definition supplies a unique name, a description and a JSON Schema inputSchema. The schema gives the model and client a structured calling convention. Tool results can contain text, images, audio or embedded resources, allowing one invocation to return several content items.

The contract stops short of semantic correctness. A schema can require a string for an issue identifier; it cannot establish that the issue exists, is visible to the user or should be modified. Descriptions and optional annotations also come from the server. Clients must treat annotations as untrusted unless the server is trusted. A label such as read-only is not a security control.

Error shape deserves equal attention. Unknown tools and invalid arguments use JSON-RPC protocol errors. Failures inside an otherwise valid tool execution return a tool result with isError: true. A host that inspects only transport success can feed an API failure or business-rule rejection to the model as though the call worked. Production handlers must preserve this distinction in logs, retries and user feedback.

Transport determines where enforcement lives

The revision standardises two transports. With stdio, the client launches the server as a subprocess. Newline-delimited JSON-RPC messages use standard input and output; standard error remains available for logs. Nothing except valid MCP messages may use the protocol streams. Process launch, environment credentials and filesystem permissions become part of the security design.

Streamable HTTP supports independent remote servers through one endpoint accepting POST and GET. A POST can receive either a JSON response or a Server-Sent Events stream; an optional GET stream permits server messages outside a client request. Session identifiers are optional, and event identifiers can support resumability after disconnection. These features improve connection management, not exactly-once execution. A caller uncertain whether a side effect completed needs an application-level idempotency strategy.

Remote deployment adds explicit web threats. Servers must validate the Origin header to resist DNS rebinding, and local servers should bind to localhost rather than every interface. Authentication is recommended for connections. A reachable local HTTP endpoint is not private merely because its URL contains localhost.

Authorisation remains a separate decision

MCP’s HTTP authorisation framework is optional in this revision. When implemented, it uses OAuth 2.1 with authorisation-server metadata, dynamic client registration where supported, bearer tokens and PKCE for all clients. stdio implementations should obtain credentials from the environment instead. OAuth defines how a client obtains and presents access tokens; it does not decide which tool call deserves user confirmation.

Keep those decisions distinct. Authenticate the client or user, authorise the requested operation against current policy, and show the consequential action and its arguments when consent is needed. Apply least-privilege credentials at the server rather than relying on the host to hide dangerous definitions. Validate inputs on the server even if a client already checked the schema, and validate results before returning them to the model.

The specification’s production guidance is correspondingly concrete: explicit consent before tool invocation, the ability to deny calls, rate limits, timeouts, output sanitisation and audit logging. Hosts should show which tools are exposed and when they run. Servers must validate tool inputs and enforce access controls. Neither side can outsource these checks to JSON-RPC compliance.

MCP’s useful promise is narrower than universal trust: independently built components can discover and use capabilities through the same session protocol. A sound deployment preserves that interoperability while keeping each server isolated, each call authorised and each piece of returned content untrusted until checked. The protocol connects trust domains; it does not dissolve them.

Sources and further reading