First Principles · 11 min mission

MCP Protocol Deep Dive: The Standard Behind Every Server

Read the Model Context Protocol itself — architecture, primitives, transports, and auth — so every tool's MCP screen becomes a thin skin over machinery you understand.

mcpprotocolfoundationsarchitectureoauthtransportssdkFact-checked 2026-06-15
On this page

The Model Context Protocol (MCP) is an open, JSON-RPC 2.0 standard for connecting AI applications to external tools and data. This guide covers the protocol itself — architecture, the six primitives, the session lifecycle, transports, and OAuth 2.1 auth — so you can build one server that runs in every MCP host instead of a bespoke connector per tool.

MCP uses a client-host-server architecture and is a stateful session protocol. Build one server and any MCP-compatible host can use it (N+M integrations instead of N×M).

RoleWhat it isResponsibilities
HostThe AI application (container + coordinator)Creates/manages clients, controls connection permissions and lifecycle, enforces consent, handles user authorization, coordinates LLM sampling, aggregates context across all clients
ClientCreated by the host; 1:1 with a single serverRuns one stateful session per server, negotiates the protocol, exchanges capabilities, routes messages bidirectionally, maintains the security boundary
ServerExposes a focused set of capabilities; local process or remote serviceServes resources, tools, prompts; operates independently; can request an LLM completion back *through* its client via sampling
The three roles. A host runs many clients; each client holds a 1:1 session with one server.
PrimitiveSideKey methodsControl model
ToolsServertools/list, tools/callModel-controlled — the model decides when to call
ResourcesServerresources/list, resources/read, resources/subscribeApplication-driven — the app chooses what to attach
PromptsServerprompts/list, prompts/getUser-controlled — surfaced for explicit selection (slash commands)
SamplingClientsampling/createMessageServer asks the client for an LLM completion
ElicitationClientelicitation/createServer asks the user for input mid-task
RootsClientroots/listClient exposes workspace `file://` boundaries to the server
The six primitives. Three are exposed by servers; three by clients. The control model says who invokes each.
PrimitiveFieldsIdentifier / resultRule to remember
Toolsname, title, description, inputSchema, outputSchema, annotations, icons, execution.taskSupportResult carries content[] (unstructured) and/or structuredContent (validated against outputSchema)Business failures return isError: true in the result so the model self-corrects; only unknown-tool / malformed-request is a JSON-RPC protocol error
Resourcessubscribe + listChanged sub-capabilities; templates use RFC 6570 URI templatesAddressed by RFC 3986 URI (https://, file://, git://, custom); content is text or base64 blobApplication-driven: the host decides which resources to pull into context
Promptsname, title, description, arguments[], iconsprompts/get returns messages[] with roles (user/assistant) + text/image/audio/embedded-resource contentPublished for explicit user selection — typically surfaced as slash commands
Server primitive fields and rules (2025-11-25).
PrimitiveMethodDirectionDetails
Samplingsampling/createMessageserver → client → LLMServer borrows the host's model (no own API keys). modelPreferences = hints[] + costPriority/speedPriority/intelligencePriority (0–1). New in 2025-11-25: tool calling via tools[] + toolChoice {mode: "auto"\|"required"\|"none"} (client must declare sampling.tools); stopReason can be toolUse or endTurn. Human SHOULD be able to deny.
Elicitationelicitation/createserver → client → userTwo modes, capability {form, url}. Form mode: flat primitive requestedSchema, MUST NOT request secrets. URL mode (new in 2025-11-25): out-of-band navigation for auth/payment — adds elicitationId, notifications/elicitation/complete, and URLElicitationRequiredError (code -32042). Responses: accept, decline, cancel.
Rootsroots/listserver → clientClient tells the server which filesystem locations are in scope. A root is { uri, name? }; uri MUST be a `file://` URI.
Client primitives — the bidirectional half of the protocol (2025-11-25).

Build a minimal server

The Python SDK ships a high-level FastMCP class whose decorators hide the JSON-RPC plumbing — a function's type hints become the tool's inputSchema automatically. Install with pip install "mcp[cli]" or uv add "mcp[cli]"; the mcp package is 1.27.2 on PyPI (v2 is alpha). The server below exposes one tool, one resource, and one prompt over stdio. The TypeScript SDK (@modelcontextprotocol/sdk, 1.29.0 on npm) models the same shape with a Zod input schema.

server.py — a three-primitive MCP server with FastMCP (pip install "mcp[cli]")
python
from mcp.server.fastmcp import FastMCP
 
mcp = FastMCP("weather-demo")
 
 
# A TOOL: model-controlled action. The type hints below become the
# tool's inputSchema; the return value becomes its result content.
@mcp.tool()
def get_forecast(city: str, days: int = 1) -> str:
    """Return a short weather forecast for a city."""
    # Real code would call a weather API here.
    return f"{city}: sunny, 24C for the next {days} day(s)."
 
 
# A RESOURCE: application-driven data, addressed by an RFC 3986 URI.
@mcp.resource("weather://stations")
def list_stations() -> str:
    """The set of stations this server knows about."""
    return "KSFO, KJFK, EGLL"
 
 
# A PROMPT: user-controlled workflow, surfaced as a slash command.
@mcp.prompt()
def trip_briefing(city: str) -> str:
    """Draft a travel weather briefing for a city."""
    return f"Write a concise weather briefing for a trip to {city}."
 
 
if __name__ == "__main__":
    # stdio transport: the host launches this file as a subprocess and
    # speaks newline-delimited JSON-RPC over stdin/stdout.
    mcp.run(transport="stdio")
server.ts — the same tool with the TypeScript SDK (npm install @modelcontextprotocol/sdk)
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
 
// @modelcontextprotocol/sdk is 1.29.0 on npm as of 2026-06-15.
const server = new McpServer({ name: "weather-demo", version: "1.0.0" });
 
// A tool. The Zod schema is published to clients as the JSON-Schema
// inputSchema, so the model knows exactly what arguments to send.
server.registerTool(
  "get_forecast",
  {
    title: "Get forecast",
    description: "Return a short weather forecast for a city.",
    inputSchema: { city: z.string(), days: z.number().default(1) },
  },
  async ({ city, days }) => ({
    content: [{ type: "text", text: `${city}: sunny, 24C for ${days} day(s).` }],
  }),
);
 
// stdio transport — connect and start serving.
await server.connect(new StdioServerTransport());

Session lifecycle: initialize → operation → shutdown

  1. Client sends `initialize`

    Carries protocolVersion, capabilities, and clientInfo (name/title/version/description/icons/websiteUrl). The session is born here — no other request may precede it.

  2. Negotiate the version

    The client sends its latest supported version. If the server supports it, the server echoes the same version; otherwise it replies with another version it supports (SHOULD be its latest). If the client cannot speak the server's response, it SHOULD disconnect.

  3. Server returns capabilities

    Server responds with protocolVersion, capabilities, serverInfo, and optional instructions. Client capability keys: roots, sampling, elicitation, tasks, experimental. Server keys: prompts, resources, tools, logging, completions, tasks, experimental. Sub-capabilities: listChanged (prompts/resources/tools) and subscribe (resources only).

  4. Client sends `notifications/initialized`

    The session enters the operation phase, where tools/call, resources/read, sampling/createMessage, and the rest flow.

  5. Shut down cleanly

    For stdio, close the subprocess. For Streamable HTTP, the client can DELETE the session (the server MAY answer 405 if it disallows client-initiated termination).

The two standard transports

stdio (local)

Client launches the server as a subprocess and exchanges newline-delimited JSON-RPC over stdin/stdout — no embedded newlines, nothing but valid MCP messages on those streams. stderr is free-form logging; clients MUST NOT treat stderr output as a failure signal. Clients SHOULD support stdio whenever possible. Right transport for local tools needing direct system access.

Streamable HTTP (remote)

A single MCP endpoint (e.g. https://example.com/mcp) serves both POST and GET. The client POSTs each JSON-RPC message with Accept: application/json, text/event-stream; a request gets either one JSON response or an SSE stream, and a notification/response yields HTTP 202 Accepted with no body. A server-initiated SSE stream opens via GET (server returns text/event-stream, or 405 if unsupported). Introduced 2025-03-26; production transport from 2025-06-18.

Streamable HTTP initialize handshake (the wire view)
… scroll to run this session
`MCP-Session-Id` is assigned in the initialize result and MUST be echoed on every later request (missing on a non-init request → 400; terminated session → 404, and the client re-initializes). `MCP-Protocol-Version` is REQUIRED after init; if absent and otherwise unknown the server SHOULD assume 2025-03-26. Resumability rides on SSE event ids replayed via a Last-Event-ID header.

Authorization: OAuth 2.1, HTTP only

Authorization is OPTIONAL and applies only to HTTP-based transports. stdio servers SHOULD NOT use it — they take credentials from the environment they launched in. The roles are fixed: the MCP server is an OAuth 2.1 Resource Server, the MCP client is the OAuth 2.1 client, and the Authorization Server is separate (may be co-hosted).

ItemSpec / valueRule
Core grantOAuth 2.1 (draft-ietf-oauth-v2-1-13)PKCE with S256 is a MUST
DiscoveryRFC 9728 Protected Resource Metadata; RFC 8414 / OIDC Discovery 1.0 for the ASServer returns 401 with WWW-Authenticate pointing at PRM, or client falls back to /.well-known/oauth-protected-resource
Client registrationOAuth Client ID Metadata Documents; RFC 7591 Dynamic Client RegistrationPriority: pre-registration → Client ID Metadata Documents (recommended) → DCR (backwards compat)
Audience binding`resource` parameter (RFC 8707)MUST be sent on both authorization and token requests, naming the canonical server URI — even if the AS ignores it
Token transportAuthorization: Bearer <token>Never in a query string
The OAuth 2.1 standards subset and mandatory client behaviors.

Knowledge check

A teammate says: "My MCP server calls the GitHub API, so I just forward the OAuth token the client sent me — one token, less code." What is the correct response?

Registry and ecosystem

The official MCP Registry (https://registry.modelcontextprotocol.io/) is a community catalog built around a server.json manifest — $schema, name, description, version, and a packages[] array declaring each package's registry type (npm, pypi, …), identifiers, versions, and transport. It launched in preview on 2025-09-08 and remains preview as of 2026-06-15 (breaking changes and data resets possible; GA unannounced). Its OpenAPI spec is open so other registries can implement a compatible interface; forking the official codebase to self-host is not a supported pattern.

Reach the end and this star joins your charted sky.