TL;DR

MCP is a protocol boundary, not an authorization system or an autonomous-agent framework. In MCP 2026-07-28, every Request is self-describing: it carries its protocol version and relevant Client Capabilities, and any compatible Server replica can process it without a Protocol Session. A Host uses one or more Clients to connect to Servers, which expose Tools, Resources, and Prompts. The application remains responsible for:

  • authenticating the caller;
  • authorizing the exact tool, resource, tenant, and object;
  • validating arguments and result size;
  • limiting cost, concurrency, and side effects;
  • auditing and deleting sensitive data.

This guide explains the vocabulary and lifecycle. For production security, see the MCP production guide; for remote OAuth boundaries, see the enterprise OAuth guide.

Why a Protocol Boundary Helps

Without a shared protocol, every host integrates every external capability through a different adapter. MCP does not eliminate application work, but it gives the connection a common lifecycle and message model:

Concern MCP contributes Application still owns
Connection stdio and Streamable HTTP bindings TLS, proxy, process and network policy
Messages JSON-RPC framing, per-request metadata, result types validation, limits and error mapping
Discovery server/discover and capability list methods provenance, approval and inventory governance
Tools names, descriptions, input schemas identity, authorization and side effects
Resources URI-shaped data references ownership, freshness, classification and deletion
Prompts reusable message templates content review, injection controls and policy

Interoperability is not the same as permission portability. A server that works with multiple hosts still needs an authorization model for each deployment.

Protocol Roles and External Systems

Host

The Host is the user-facing application or agent runtime. It typically:

  • receives the user request;
  • selects or routes a model;
  • manages one or more Clients;
  • presents discovered capabilities to the model;
  • applies host-level approval and display policy.

The Host should not assume that a Server description is trusted instructions. Descriptions, resources, and results are data crossing a trust boundary.

Client

An MCP Client is a protocol adapter managed by the Host. A common topology is one Client per Server so credentials, capability catalogs, cancellation, and failures remain isolated. A dedicated Connection may exist, but it is not a Protocol Session, Conversation, User, or Task. A Host may use a Gateway, which adds another routing, identity, cache, and policy boundary.

Server

A Server exposes capabilities and executes them under server-side policy. It may access a filesystem, database, API, or an in-process service. “Server” describes the protocol role; it does not guarantee a separate OS process, sandbox, or security boundary.

External System

The database, file store, SaaS API, or local operating system is the authority for business data. The MCP Server is an adapter and policy enforcement point, not a replacement for the external system's access control.

Stateless Requests and Discovery

MCP 2026-07-28 has no initialize / notifications/initialized handshake and no Mcp-Session-Id. Every Request carries the selected revision and relevant Client Capabilities in _meta. clientInfo is useful for display and debugging, but it is self-reported and must not be used as an authenticated identity.

sequenceDiagram participant H as "Host" participant C as "MCP Client" participant S as "MCP Server" H->>C: Create connection C->>S: server/discover + per-request metadata S-->>C: supported versions + capabilities + cache hints C->>S: tools/list or another independent Request S-->>C: resultType complete + bounded result C->>S: tools/call with new Request ID S-->>C: complete, input_required, or error C-->>H: bounded observation

Every Server must implement server/discover, but a Client may call another method first and handle UnsupportedProtocolVersionError (-32022). Discovery returns supported versions, Server Capabilities, identity metadata, and cache hints. It proves protocol compatibility, not endpoint provenance, trust, or permission.

An abbreviated discovery request shows the self-describing contract:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "server/discover",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {
        "name": "acme-host",
        "version": "4.2.0"
      },
      "io.modelcontextprotocol/clientCapabilities": {
        "elicitation": {}
      }
    }
  }
}

Capability discovery is not a permission grant. Lists may vary by credentials carried on the current Request, but not because another Request shared the same Connection. A robust Client handles:

  • protocol-version mismatch;
  • discovery timeout and stale cache hints;
  • malformed or oversized messages;
  • cancellation and disconnect;
  • subscribed capability changes;
  • Server shutdown and stream reconnect;
  • duplicate requests and idempotency.

JSON-RPC at the Boundary

MCP uses JSON-RPC concepts for requests, responses, notifications, and errors. A request ID correlates a response; it does not authenticate the request.

typescript
type RequestId = string | number;

type JsonRpcRequest = {
  jsonrpc: "2.0";
  id: RequestId;
  method: string;
  params?: {
    _meta: {
      "io.modelcontextprotocol/protocolVersion": string;
      "io.modelcontextprotocol/clientCapabilities": Record<string, unknown>;
    };
    [key: string]: unknown;
  };
};

type JsonRpcError = {
  code: number;
  message: string;
  data?: unknown;
};

function requireRequest(value: unknown): JsonRpcRequest {
  if (!value || typeof value !== "object") throw new Error("invalid_request");
  const request = value as Record<string, unknown>;
  if (request.jsonrpc !== "2.0" ||
      (typeof request.id !== "string" && typeof request.id !== "number") ||
      typeof request.method !== "string") {
    throw new Error("invalid_request");
  }
  if (request.params !== undefined &&
      (!request.params || typeof request.params !== "object")) {
    throw new Error("invalid_params");
  }
  return request as unknown as JsonRpcRequest;
}

This fragment checks only a subset of framing. A real implementation also validates the pinned protocol Schema, non-null unique Request IDs, required _meta, Result resultType, method-specific data, byte and depth limits, and extension negotiation. JSON Schema defaults to Draft 2020-12; network $ref resolution must be disabled by default and complex composition needs resource bounds.

The Three Capability Families

Tools

Tools are model-controlled operations a Client may ask a Server to perform. Definitions include a name, description, mandatory inputSchema, optional outputSchema, and untrusted annotations. tools/list is paginated and cacheable; tools/call may return Text, media, Resource links, embedded Resources, or schema-validated structuredContent. Treat the model's call as a proposal:

text
model proposes a tool call
    -> host/client applies approval and budget policy
    -> server authenticates and authorizes
    -> server validates arguments
    -> server executes the operation
    -> server returns a bounded result

A good tool is narrow and explicit about:

  • what it does and does not do;
  • input constraints and defaults;
  • read versus write behavior;
  • resource and tenant scope;
  • timeout, retry, idempotency and output limits.

Do not expose a generic run_command, arbitrary SQL string, unrestricted HTTP client, or “manage everything” tool unless the entire capability is intentionally sandboxed and authorized. A strict schema does not make a dangerous capability safe.

Resources

Resources are application-controlled data addressed by URI. resources/list, resources/templates/list, and resources/read support discovery or retrieval; RFC 6570 Templates describe parameterized URI spaces. A URI is an identifier, not proof that the caller may read it. The Server must apply scheme validation, Tenant and object authorization, freshness, MIME, byte, decompression, and path or egress limits.

Resource links can be safer than embedding a large artifact in a Tool result, but the follow-up read must repeat authorization. Never assume that a resource returned by a trusted server is safe to place in a prompt; it may contain instructions or sensitive data.

Prompts

Prompts are user-controlled-by-selection templates authored by a Server. prompts/list returns descriptors and string Argument metadata; prompts/get renders user or assistant messages. Retrieval does not call a model, and an Assistant-role message does not become a System instruction. Treat Prompt content, Arguments, and linked or embedded Resources as untrusted data.

Primitive Primary control convention Main methods Security boundary
Tool Model proposes tools/list, tools/call Validate Schema, domain rules, authorization, side effects, and output
Resource Application selects resources/list, resources/templates/list, resources/read Authorize URI and object; bound bytes, MIME, freshness, traversal, and SSRF
Prompt User selects prompts/list, prompts/get Validate Arguments and content; preserve provenance; resist Prompt Injection

List and Resource Read results carry ttlMs and cacheScope. public is valid only when content is identical and safe across callers; anything filtered by User, Tenant, role, or Token needs private plus an Authorization-context cache key. A notification invalidates a cache entry but never authorizes it.

Results, MRTR, and Notifications

Every successful 2026-07-28 Result declares resultType. "complete" is final. "input_required" means an eligible prompts/get, resources/read, or tools/call needs additional Client input before it can finish.

json
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "approval": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Approve publishing this report?",
          "requestedSchema": {
            "type": "object",
            "properties": { "approved": { "type": "boolean" } },
            "required": ["approved"]
          }
        }
      }
    },
    "requestState": "opaque-integrity-protected-state"
  }
}

MRTR is not a server-initiated JSON-RPC Request. The initial Request ends; the Client gathers only supported input and retries the original method with a new JSON-RPC ID, inputResponses, and the exact opaque requestState. The Server treats State as attacker-controlled, binds it to the Principal, method, salient parameters, policy revision, and short expiry, and enforces single use when replay could duplicate an effect.

Long-lived change events use subscriptions/listen. A Client selects filters such as toolsListChanged, promptsListChanged, resourcesListChanged, or specific Resource subscriptions. The Server acknowledges the accepted subset and tags each Notification with a Subscription ID. Reconnection creates a new subscription; events invalidate cached data but do not carry permission.

Transport Choices

stdio

stdio is useful when the Host launches a local process. Review inherited environment variables, filesystem access, network egress, shell execution, OS user, and process lifetime. A local process is not automatically sandboxed.

Streamable HTTP

For a new remote deployment on 2026-07-28, use Streamable HTTP. Every message is an independent POST to one MCP endpoint. A Request response is either one JSON object or a Request-scoped SSE stream containing related Notifications and the final Response. Closing that stream signals cancellation; it does not prove a downstream side effect rolled back.

Each POST includes MCP-Protocol-Version and Mcp-Method; tools/call, resources/read, and prompts/get also include Mcp-Name. The JSON-RPC body remains authoritative, and the Server rejects Header/Body mismatches. Validate Origin, authenticate every Request, disable proxy buffering for SSE, and never place Secrets in Mcp-Param-* headers.

http
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json,text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: reports.read

{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{"name":"reports.read","arguments":{"reportId":"rpt_72"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}

Legacy SSE Compatibility

MCP 2025-11-25 and earlier use connection-scoped initialization, may use Mcp-Session-Id, and allow Server-initiated Requests. The 2024-11-05 HTTP+SSE Transport uses a separate SSE endpoint plus POST message endpoint. If you support these revisions:

  • isolate and version the compatibility route;
  • apply the exact revision's Initialize, Session, and message-direction rules;
  • authenticate the SSE request and each message request;
  • bound event queues and response sizes;
  • define ordering, cancellation, reconnect, and duplicate behavior;
  • test through the actual proxy path.

Do not leak Legacy capability state into the modern path or describe legacy SSE as the current streaming model.

HTTP Authorization Boundaries

Authorization is optional in MCP, but a protected Streamable HTTP Server acts as an OAuth Resource Server and the Client acts as an OAuth Client. The Client discovers Protected Resource and Authorization Server metadata, registers through Client ID Metadata Documents or pre-registration, uses PKCE and state, validates the authorization response issuer, and includes the RFC 8707 Resource Indicator in authorization and token requests.

The Server validates the token's issuer, Audience, expiry, signature, and challenged Scope on every Request. Tokens travel only in the Authorization: Bearer header, never in a URI. Token Passthrough is forbidden: a Server must not accept or forward a token intended for another Resource.

OAuth Scope is a coarse gate. The Server still authorizes the exact Principal, Tenant, Tool or Resource, object, arguments, purpose, and side effect. Use 401 for absent or invalid credentials and 403 with an insufficient_scope challenge for an authenticated caller that lacks the current operation's Scope. The HTTP profile does not apply to stdio; local Servers obtain credentials from their constrained execution environment.

Security Model: What MCP Does Not Guarantee

MCP standardizes communication. It does not guarantee:

  • user authentication;
  • tenant isolation;
  • object ownership;
  • safe tool behavior;
  • prompt-injection resistance;
  • confidentiality of transport or telemetry;
  • deletion of data copied into caches or logs.

At minimum, a remote server needs:

  1. trusted token or workload authentication;
  2. issuer, audience/resource, expiry, algorithm and key-rotation validation;
  3. per-call scope and object authorization;
  4. request, result, concurrency, timeout and cost limits;
  5. idempotency and cancellation for side effects;
  6. redacted audit events and trace context;
  7. tests for cross-tenant access, tool-result injection and replay.

Annotations such as read-only or destructive hints help hosts present risk, but the server must verify actual behavior and enforce policy.

A Minimal Server Design

Keep protocol adaptation, policy, and business logic separate:

text
Transport adapter
  -> JSON-RPC and method validation
  -> Principal and tenant context
  -> Tool/resource policy
  -> Domain service
  -> bounded result and audit event

The domain service should receive a trusted context rather than infer identity from model arguments:

typescript
type ExecutionContext = {
  requestId: string;
  principalId: string;
  tenantId: string;
  scopes: ReadonlySet<string>;
};

type ReportRequest = {
  reportId: string;
};

async function getReport(
  context: ExecutionContext,
  request: ReportRequest,
  authorize: (context: ExecutionContext, reportId: string) => Promise<boolean>,
  repository: { read: (tenantId: string, reportId: string) => Promise<unknown> },
) {
  if (!context.scopes.has("reports.read")) throw new Error("insufficient_scope");
  if (!(await authorize(context, request.reportId))) throw new Error("access_denied");
  const report = await repository.read(context.tenantId, request.reportId);
  return { status: "ok", report };
}

This is a core fragment, not a complete SDK integration. Add repository-level tenant constraints, result redaction, size limits, tracing, error mapping, and cancellation in the application.

Testing and Operations

Test contracts at five levels:

Level Examples
Protocol server/discover, per-request _meta, version mismatch, Result Type, malformed JSON-RPC
Policy missing scope, wrong tenant, object ownership, expired token
Reliability timeout, cancellation, broken SSE, subscription reconnect, queue overflow, duplicate effect
Abuse Tool/Prompt/Resource injection, oversized Schema, SSRF, path traversal, Token Passthrough
Compatibility modern-to-modern, modern probe of Legacy, legacy fallback, unsupported revision

Observe low-cardinality events:

  • protocol and SDK version;
  • Request, subscription and trace identifiers;
  • method, stable Server identity and capability revision;
  • argument digest, result class and byte size;
  • policy, approval, cache and MRTR decisions;
  • queue/upstream latency, retries, cancellation, budget and final effect status.

Do not store raw tokens, private prompts, full tool results, or hidden reasoning by default.

Choosing an SDK

Choose a maintained SDK when it covers the required revision, transport, cancellation, capability negotiation, authorization hooks, and security updates. A hand-written adapter can be valuable for learning or a narrow compatibility boundary, but it creates a long-term conformance and incident-response obligation.

Evaluate an SDK with:

  • protocol conformance tests;
  • dependency and release history;
  • transport and proxy behavior;
  • error and cancellation semantics;
  • extension points for authorization and redaction;
  • observability and deletion support;
  • migration and rollback strategy.

Production Checklist

  • [ ] Pin the MCP revision and SDK version.
  • [ ] Document Host, Client, Server, external systems, and trust boundaries.
  • [ ] Send version and Client Capabilities on every Request; support or probe server/discover.
  • [ ] Use stdio or current Streamable HTTP; isolate Legacy Initialize, Sessions, GET streams, and HTTP+SSE.
  • [ ] Validate JSON-RPC framing, Result Type, routing headers and method-specific Schemas.
  • [ ] Authenticate and authorize every operation.
  • [ ] Bind resources and tools to tenant and object policy.
  • [ ] Isolate cache entries by Server, revision, Authorization Context and cacheScope.
  • [ ] Protect MRTR requestState; treat Sampling and Roots as deprecated migration paths.
  • [ ] Bound request, result, queue, concurrency, timeout and cost budgets.
  • [ ] Define cancellation, subscription reconnect, duplicate-effect and shutdown behavior.
  • [ ] Treat descriptions, prompts, resources and results as untrusted data.
  • [ ] Redact telemetry and test deletion propagation.

Frequently Asked Questions

What is MCP?

An open protocol for a Host to connect model-facing Clients to Servers that expose capabilities. It standardizes lifecycle and messages, not business authorization.

What is Host, Client, and Server?

The Host coordinates the user and model, a Client adapts the protocol for one Server, and the Server advertises and executes capabilities under its own policy. A Connection is a Transport resource, not a Protocol Session or authenticated identity.

Are MCP tools function calling?

They overlap at the structured call boundary, but MCP also defines cross-process discovery, lifecycle, transport, and result exchange. A server still validates and authorizes every call.

Which transport should a new remote server use?

Use 2026-07-28 Streamable HTTP when all peers support it: independent POST Requests with JSON or Request-scoped SSE Responses. Keep earlier Streamable HTTP and HTTP+SSE on explicit compatibility routes.

Is MCP Sampling still recommended?

No. MCP Sampling remains functional during its deprecation window, but new implementations should integrate directly with an LLM provider API. Existing 2026-07-28 compatibility uses MRTR rather than an unsolicited Server Request.

Conclusion

MCP 2026-07-28 is a Stateless context-exchange protocol: each Request identifies its revision and Client Capabilities, Servers expose discoverable Tools, Resources, and Prompts, and explicit MRTR or subscription patterns cover interactions that need more than one response. This simplifies horizontal scaling, but it does not grant trust. Pin the revision, validate every boundary, keep authorization on the Server, isolate Legacy behavior, and treat every descriptor and payload as untrusted.

Primary Sources