{"owner":"modelcontextprotocol","repo":"typescript-sdk","hasSkills":true,"totalSkillsCount":40,"totalTokensCount":46228,"categories":["claude-rule","anthropic-skill","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","examples/guides/servers/skills/acme/billing/refunds/SKILL.md","examples/guides/servers/skills/git-workflow/SKILL.md","examples/guides/servers/skills/git-workflow/references/REVIEW.md","examples/scoped-tools/README.md","examples/skills/README.md","examples/skills/package.json","examples/skills/skills/acme/billing/refunds/SKILL.md","examples/skills/skills/acme/billing/refunds/escalation/SKILL.md","examples/skills/skills/acme/billing/refunds/templates/refund-email.md","examples/skills/skills/git-workflow/SKILL.md","examples/skills/skills/git-workflow/references/REVIEW.md","examples/tools/README.md","packages/client/README.md","packages/codemod/README.md","packages/codemod/batch-test/README.md","packages/core-internal/src/types/README.md","packages/core/README.md","packages/middleware/README.md","packages/middleware/express/README.md","packages/middleware/fastify/README.md","packages/middleware/hono/README.md","packages/middleware/node/README.md","packages/server-legacy/README.md","packages/server/README.md","skills/mcp-typescript-sdk-skills/SKILL.md","skills/mcp-typescript-sdk-skills/references/client.md","skills/mcp-typescript-sdk-skills/references/server.md","skills/mcp-typescript-sdk-upgrade-to-v2/SKILL.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/auth.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/enhancements.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/errors.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/http-headers.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/imports-transports.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/packaging-runtime.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/protocol-ctx.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/types-schemas.md","skills/mcp-typescript-sdk-upgrade-to-v2/references/unchanged-apis.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThe principles this SDK is built and reviewed by. They are not a checklist: apply them with judgement.\n\n1. **Minimalism** — The SDK should do less, not more: prefer changes that add no API or option unless justified.\n2. **Spec is the anchor** — The spec decides; where it leaves a choice open, check what the other official SDKs do before making your own choice.\n3. **Strict by default** — Accept what the spec allows, and add no leniency for peers that break it.\n4. **Backwards compatible** — New features and changes arrive in backwards compatible ways; when a bug, a spec violation or an unsafe default forces a break, the changeset says what breaks.\n5. **Small changes** — Guard scope hard: a smaller change that lands beats a bigger one that doesn't, and small, independent pull requests can land in any order.\n6. **The words are true** — Changesets, docs and comments say what the code does.\n7. **Test what users see** — Prefer tests that cover the end-to-end usage a user would actually see, where sensible; `test/e2e/requirements.ts` lists those behaviours.\n8. **Brief comments** — Keep inline comments to one line and JSDoc to the public API; the reasoning goes in the commit message or PR description.\n\nStyle belongs to Prettier, ESLint and the compiler.\n","examples/guides/servers/skills/acme/billing/refunds/SKILL.md":"---\nname: refunds\ndescription: Process customer refunds.\n---\n\n# Refunds\n","examples/guides/servers/skills/git-workflow/SKILL.md":"---\nname: git-workflow\ndescription: Branching and review conventions.\n---\n\n# Git workflow\n\nBefore opening a pull request, go through [the review checklist](references/REVIEW.md).\n","examples/guides/servers/skills/git-workflow/references/REVIEW.md":"# Review checklist\n\n- Tests pass.\n","examples/scoped-tools/README.md":"# scoped-tools — per-tool scope enforced in the tool handler\n\nDemonstrates per-tool OAuth scope enforcement on a `createMcpHandler`\ndeployment: the HTTP gate does **bearer-verify + 401 only**, and each tool\nhandler checks `ctx.http?.authInfo?.scopes` for the scope it needs. The scope\ndecision lives next to the code it guards — the handler is the only place that\nauthoritatively knows which tool is executing — instead of in middleware that\nwould have to re-derive the operation from the request body.\n\n`server.ts` runs a minimal demo Authorization Server alongside the MCP Resource\nServer. `client.ts` connects with a `files:read` token, calls `list-files`\n(works), then calls `write-file` → the handler returns `{ isError: true }` with\n`insufficient_scope: requires files:write`.\n\nThe transport's automatic `403 insufficient_scope` **step-up** flow (SEP-2350 —\nscope union, refresh-bypass, `maxStepUpRetries`) applies when the RS responds\n`403` at the HTTP layer; that path is exercised by\n`test/e2e/scenarios/client-auth.test.ts`.\n\n```bash\npnpm --filter @mcp-examples/scoped-tools server -- --http --port 3000\npnpm --filter @mcp-examples/scoped-tools client -- --http http://127.0.0.1:3000/mcp\n```\n\n> DEMO ONLY — the bundled AS auto-approves and grants whatever scope is asked\n> for. Do not deploy.\n","examples/skills/README.md":"# skills\n\nThe server serves the `skills/` folder as MCP skills (SEP-2640) with one\n`loadSkills` call, under the `examples` prefix. The client lists the catalog,\nloads `git-workflow` (its `SKILL.md`, verified for size, digest and\nfrontmatter), follows the reference to `references/REVIEW.md`, browses the\n`refunds` folder, and catches a verification failure.\n\n```bash\npnpm --filter @mcp-examples/skills client            # modern leg over stdio\npnpm --filter @mcp-examples/skills client -- --legacy\npnpm --filter @mcp-examples/skills run server -- --http --port 3000\npnpm --filter @mcp-examples/skills client -- --http http://127.0.0.1:3000/mcp\n```\n\nSee [Serving skills](../../docs/servers/skills.md) and [Reading skills](../../docs/clients/skills.md).\n","examples/skills/package.json":"{\n    \"name\": \"@mcp-examples/skills\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"scripts\": {\n        \"server\": \"tsx server.ts\",\n        \"client\": \"tsx client.ts\"\n    },\n    \"dependencies\": {\n        \"@hono/node-server\": \"catalog:runtimeServerOnly\",\n        \"@mcp-examples/shared\": \"workspace:*\",\n        \"@modelcontextprotocol/client\": \"workspace:*\",\n        \"@modelcontextprotocol/hono\": \"workspace:*\",\n        \"@modelcontextprotocol/server\": \"workspace:*\"\n    },\n    \"devDependencies\": {\n        \"tsx\": \"catalog:devTools\"\n    },\n    \"example\": {\n        \"era\": \"dual\",\n        \"//\": \"Both legs list, load and verify skills, follow a reference and browse a skill folder.\"\n    }\n}\n","examples/skills/skills/acme/billing/refunds/SKILL.md":"---\nname: refunds\ndescription: Process a customer refund end to end.\nmetadata:\n    owner: billing\n---\n\n# Refunds\n\nCheck the order, issue the refund, then send [the refund email](templates/refund-email.md).\nFor disputed orders, use the escalation skill.\n","examples/skills/skills/acme/billing/refunds/escalation/SKILL.md":"---\nname: escalation\ndescription: Escalate a disputed refund to the billing lead.\n---\n\n# Escalation\n\nCollect the order number and the customer's reason, then page the billing lead.\n","examples/skills/skills/acme/billing/refunds/templates/refund-email.md":"Hello {{customer}},\n\nYour refund of {{amount}} is on its way.\n","examples/skills/skills/git-workflow/SKILL.md":"---\nname: git-workflow\ndescription: Branching, commit and review conventions for this team.\n---\n\n# Git workflow\n\n1. Branch from `main` as `feature/<topic>`.\n2. Keep commits small; write messages in the imperative.\n3. Before opening a pull request, go through [the review checklist](references/REVIEW.md).\n","examples/skills/skills/git-workflow/references/REVIEW.md":"# Review checklist\n\n- Tests pass locally.\n- A changeset describes the change.\n","examples/tools/README.md":"# tools\n\n**Start here.** Register tools with `McpServer.registerTool`; the SDK infers the JSON Schema from any Standard-Schema-compatible input (Zod here) and emits `structuredContent` matching `outputSchema`. The client lists tools, inspects schemas and `annotations`, calls them, and\nasserts structured output.\n\n```bash\npnpm tsx examples/tools/client.ts\n```\n","packages/client/README.md":"# `@modelcontextprotocol/client`\n\nThe MCP (Model Context Protocol) TypeScript client SDK. Build MCP clients that connect to MCP servers.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/client\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Client guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/connect)** — connecting, calling tools, OAuth, and middleware\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n","packages/codemod/README.md":"# @modelcontextprotocol/codemod\n\nCodemods for migrating MCP TypeScript SDK code between major versions.\n\n## Usage\n\n```bash\nnpx @modelcontextprotocol/codemod@latest v1-to-v2 .\n\n# or a single source file (manifest changes are reported, not applied)\nnpx @modelcontextprotocol/codemod@latest v1-to-v2 src/server.ts\n```\n\nThe codemod rewrites TypeScript and JavaScript source files\n(`.ts`/`.tsx`/`.mts`/`.cts`/`.js`/`.jsx`/`.mjs`/`.cjs`) in place. Run it on a clean\nworking tree so you can review the diff.\n\n## What `v1-to-v2` covers\n\nThe mechanical rename mappings are the source of truth — see\n`src/migrations/v1-to-v2/mappings/`:\n\n- [`importMap.ts`](./src/migrations/v1-to-v2/mappings/importMap.ts) —\n  `@modelcontextprotocol/sdk/...` import paths → v2 packages\n- [`symbolMap.ts`](./src/migrations/v1-to-v2/mappings/symbolMap.ts) —\n  symbol renames (`McpError` → `ProtocolError`, …)\n- [`schemaToMethodMap.ts`](./src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts) —\n  `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`\n- [`contextPropertyMap.ts`](./src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) —\n  `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*`\n\nTransforms in `src/migrations/v1-to-v2/transforms/` also rewrite `.tool()` →\n`registerTool` (wrapping `inputSchema` / `outputSchema` / `argsSchema` / `uriSchema`\nraw shapes with `z.object()`), drop the result-schema argument from `client.request()`\n/ `client.callTool()` for spec methods, route spec `*Schema` imports to\n`@modelcontextprotocol/core`, rename\n`StreamableHTTPError` → `SdkHttpError` / `IsomorphicHeaders` → `Headers`, rewrite\n`SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`, route\n`ErrorCode.{RequestTimeout,ConnectionClosed}` to `SdkErrorCode` (rewriting an\nall-SDK condition's `instanceof ProtocolError` guard to `SdkError`, and marking\nguards that mix the two enums), add `import { z } from 'zod'` when a wrap needs\nit, rewrite `vi.mock`\n/ `jest.mock` / dynamic `import()` paths, invert optional completable nesting\n(`completable(schema.optional(), cb)` becomes `completable(schema, cb).optional()`),\nand rewrite `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\nclient or server package root.\n\n## `@mcp-codemod-error` markers\n\nWhen the codemod recognizes a v1 pattern but cannot safely rewrite it (ambiguous\ncontext, removed API with no mechanical replacement, signature change requiring\njudgment), it leaves the code unchanged and inserts a comment:\n\n```typescript\n/* @mcp-codemod-error WebSocketClientTransport removed in v2. Use StreamableHTTPClientTransport or StdioClientTransport. */\n```\n\nAfter running the codemod, find every site that needs attention:\n\n```bash\ngrep -rn '@mcp-codemod-error' .\n```\n\n## What it does NOT cover\n\nCJS→ESM / Node 20 pre-flight, header **read** rewrites (`ctx.http?.req?.headers`\nbracket access → `.get()`; sending plain-record headers keeps working), OAuth\nerror-class consolidation (`instanceof InvalidGrantError` → `OAuthError` +\n`OAuthErrorCode`), per-scenario `SdkErrorCode` branch selection, `ctx.mcpReq.send()`\nschema-arg drop, and behavioral adaptation are manual — see the\n[migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2) for what to do after the\ncodemod runs.\n\nThe codemod handles the v1→v2 SDK surface upgrade only. Adopting the 2026-07-28\nprotocol revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`)\nis architectural and not codemod-automatable — see\n[docs/migration/support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28).\n","packages/codemod/batch-test/README.md":"# Codemod Batch Test\n\nTests the v1-to-v2 codemod against real-world repos to find bugs, missing transforms, and gaps.\n\n## How it works\n\nFor each repo in `repos.json`, the batch test:\n\n1. Clones the repo (or resets an existing clone)\n2. Installs dependencies\n3. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy\n4. Runs the codemod — in-process via the programmatic API (`--codemod=local`, the default), or by shelling out to the published CLI via `npx` (`--codemod=published`)\n5. Packs local SDK packages as tarballs and rewrites `package.json` deps to use them, so the test runs against the current SDK branch — only when `--sdk=local` (the default); with `--sdk=published` the v2 deps are installed from npm instead (see [Modes](#modes))\n6. Re-installs dependencies\n7. Re-runs the same checks\n8. Writes structured JSON reports\n\nErrors that appear in step 7 but not step 3 are codemod-introduced regressions.\n\n## Usage\n\n```bash\n# Build all SDK packages first (tarballs need built dist/)\npnpm build:all\n\n# Run the batch test\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# Clean cloned repos, results, and tarballs\npnpm --filter @modelcontextprotocol/codemod batch-test:clean\n```\n\n## Modes\n\nEach run independently chooses where the **SDK packages** and the **codemod** come from:\n\n| Flag                       | Values                 | Default   | Effect                                                                                                              |\n| -------------------------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |\n| `--sdk`                    | `local` \\| `published` | `local`   | `local`: pack local packages and rewrite deps to `file:` tarballs. `published`: install the v2 deps from npm.       |\n| `--codemod`                | `local` \\| `published` | `local`   | `local`: run the working-copy codemod in-process. `published`: `npx` the published CLI.                             |\n| `--codemod-version <spec>` | npm version/tag/range  | `latest`  | Only with `--codemod=published`.                                                                                    |\n| `--sdk-version <spec>`     | npm version/tag/range  | _(unset)_ | Only with `--sdk=published`. Pins each v2 dep to its own resolved version. Unset = use whatever the codemod writes. |\n\nBoth `--flag value` and `--flag=value` forms work.\n\n```bash\n# default (today's behavior)\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# both from npm @latest\npnpm --filter @modelcontextprotocol/codemod batch-test -- --sdk=published --codemod=published\n```\n\nPublished specs are resolved to concrete versions via `npm view` at startup. A failure resolving the codemod version or the representative SDK label aborts the run; a per-package `--sdk-version` miss only warns and leaves that package on the codemod-written range. Each\n`@modelcontextprotocol/*` package is resolved **independently** — they are not released in lockstep. Results are written to a per-run directory keyed on the resolved versions; the SDK segment is the resolved `@modelcontextprotocol/server` version used as a representative label,\nwhile the full per-package set is recorded in `summary.json` → `sdkVersions`:\n\n```\nresults/codemod-local__sdk-local/                  # default (--sdk=local --codemod=local)\nresults/codemod-2.0.0-alpha.2__sdk-2.0.0-alpha.2/  # --codemod=published --sdk=published --sdk-version=2.0.0-alpha.2\nresults/codemod-2.0.0-alpha.2__sdk-from-codemod/   # --codemod=published --sdk=published  (no --sdk-version)\n```\n\nThe segment `sdk-from-codemod` appears when `--sdk=published` with no `--sdk-version` and `--codemod=published` (the SDK version is baked into the published codemod and only known after install; the installed versions are recorded in `summary.json` → `sdkVersions`).\n\n**Limitation:** in published-codemod mode the CLI emits text, not structured diagnostics, so `codemod.diagnostics` is empty and the raw CLI output is captured under `codemod.cli` instead. Diagnostics categorization applies only to local-codemod runs.\n\n## Output\n\nResults are written to a per-run directory keyed on the resolved versions, `batch-test/results/<config>/`, where `<config>` is the `codemod-…__sdk-…` leaf (distinct `--codemod-version`/`--sdk-version` values produce distinct directories even within the same `--sdk`/`--codemod`\nmode; see [Modes](#modes)):\n\n- `results/<config>/summary.json` — overview across all repos: which passed, which failed, error counts, plus the run `config` and the resolved per-package `sdkVersions`\n- `results/<config>/<repo-slug>/report.json` — per-repo detail: baseline vs post-codemod check results, codemod diagnostics, change counts\n\n## Repo manifest (`repos.json`)\n\nAn array of repo entries. Each entry specifies a GitHub repo and one or more packages within it.\n\n```json\n{\n    \"repo\": \"owner/repo-name\",\n    \"ref\": \"main\",\n    \"packages\": [\n        {\n            \"dir\": \"packages/mcp-server\",\n            \"sourceDir\": \"src\",\n            \"checks\": {\n                \"typecheck\": \"npx tsc --noEmit\",\n                \"build\": \"npm run build\",\n                \"test\": \"npm run test\",\n                \"lint\": null\n            }\n        }\n    ]\n}\n```\n\n| Field                  | Required | Default                                | Description                                                       |\n| ---------------------- | -------- | -------------------------------------- | ----------------------------------------------------------------- |\n| `repo`                 | yes      | —                                      | GitHub `owner/name`                                               |\n| `ref`                  | no       | `main`                                 | Branch or tag to clone                                            |\n| `packages`             | no       | `[{ \"dir\": \".\", \"sourceDir\": \"src\" }]` | Package targets within the repo                                   |\n| `packages[].dir`       | yes      | —                                      | Path to package root (where `package.json` lives)                 |\n| `packages[].sourceDir` | no       | `src`                                  | Source directory relative to `dir` (passed to codemod)            |\n| `packages[].checks`    | no       | auto-detect                            | Override check commands; set a value to `null` to skip that check |\n\nWhen `checks` is omitted, the runner auto-detects commands from the package's `package.json` scripts (probing names like `typecheck`, `build`, `test`, `lint`). The package manager is auto-detected from the lockfile at the repo root.\n\n## Analyzing results\n\n`analyze-prompt.md` contains instructions for Claude Code to run the batch test and produce a categorized analysis. Each error is classified as:\n\n| Category            | Meaning                                            |\n| ------------------- | -------------------------------------------------- |\n| `codemod-bug`       | A transform produced incorrect output              |\n| `missing-transform` | The codemod should handle this pattern but doesn't |\n| `manual-migration`  | Expected — requires human judgment                 |\n| `repo-specific`     | Unusual pattern not worth handling in the codemod  |\n\n## Adding a repo\n\n1. Edit `repos.json` and add an entry\n2. Run `pnpm --filter @modelcontextprotocol/codemod batch-test`\n3. Check `results/<config>/<repo-slug>/report.json` for new findings\n\nFor monorepos, list each package that uses `@modelcontextprotocol/sdk` as a separate entry in `packages`.\n\n## Iteration workflow\n\n```\n1. Run the batch test\n2. Review results — identify codemod bugs / missing transforms\n3. Fix the codemod transforms\n4. Run batch-test:clean, then re-run the batch test\n5. Confirm the fixes resolved the issues\n6. Repeat\n```\n","packages/core-internal/src/types/README.md":"# Spec reference types (\"anchors\")\n\nThe `spec.types.<revision>.ts` files in this directory are vendored, verbatim copies of the MCP specification's normative `schema.ts`, one file per protocol revision. Each file is generated by `pnpm run fetch:spec-types [version] [sha]` (`scripts/fetch-spec-types.ts`): the\nupstream schema is fetched at a specific spec commit, a provenance header recording that commit is prepended, and the result is formatted with the project's prettier config — no other transformation.\n\nThey are reference-only test oracles: the comparison suites in `packages/core-internal/test/spec.types.<revision>.test.ts` check the SDK's own types against them. They are not exported from any barrel and must never be imported by runtime code.\n\n## Lifecycle policy\n\n1. **Released revisions are frozen.** Once a protocol revision is published under `schema/<revision>/` in the spec repository, its anchor regenerates only from the pinned spec commit recorded in `RELEASED_REVISION_PINS` (`scripts/fetch-spec-types.ts`) — never from the latest\n   upstream commit. Moving that pin, including the freeze of a newly published revision (when its generation source switches from `schema/draft/` to `schema/<revision>/`), must land atomically with every checked-in oracle derived from that revision.\n\n2. **Draft anchors float only while they have an explicit consumer.** An unreleased revision may track the spec repository's `schema/draft/schema.ts`, but refreshes must go through reviewed PRs and update all derived oracles atomically. No currently supported revision uses this mode.\n\n3. **The bot proposes; it never auto-merges.** Automated refreshes always go through a pull request that a maintainer reviews and merges. No automation pushes anchor changes directly to `main` or merges its own PRs. A refresh PR that breaks the comparison suites is the desired\n   signal — it is fixed in that PR, not bypassed.\n\n4. **Generated twins update atomically with their anchor.** If artifacts derived from an anchor (for example vendored JSON schemas or generated validators) are checked into this repository, any refresh that changes the anchor must regenerate those artifacts in the same commit.\n   The anchor and its derived twins must never be out of sync at any commit on `main`.\n\n    **This clause is OPERATIVE.** The vendored twins are the per-revision `schema.json` copies under `packages/core-internal/test/corpus/schema-twins/` (`<revision>.schema.json` + `manifest.json` recording each twin's source commit and content hashes). They are TEST-ONLY oracles consumed by the\n    schema-twin conformance lock (`test/wire/schemaTwinConformance.test.ts`) — never bundled, never imported by runtime code, and the JSON Schema engines stay optional peer dependencies. A refresh of `spec.types.<revision>.ts` must refresh the matching upstream\n    `schema/<dir>/schema.json` from that twin's recorded source commit and update `manifest.json` in the same change; the spec example corpus manifest (`test/corpus/fixtures/<revision>/manifest.json`) records its own source commit and follows the same atomicity rule when the examples\n    are re-vendored. The conformance lock failing after an anchor-only refresh is the desired loud signal of a missed twin update.\n","packages/core/README.md":"# @modelcontextprotocol/core\n\nCanonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification and OAuth/OpenID **Zod schemas**.\n\nThese are the exact schema constants the SDK validates protocol and OAuth/OpenID payloads against internally. The `@modelcontextprotocol/server` and `@modelcontextprotocol/client` packages keep a Zod-free public surface, so this package exists as the supported place to import the\nraw schemas when you need to validate or parse MCP messages yourself.\n\n## Install\n\n```sh\nnpm install @modelcontextprotocol/core\n```\n\n## Usage\n\n```ts\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\n\n// Throws on invalid input; returns the typed result on success.\nconst result = CallToolResultSchema.parse(payload);\n\n// Or non-throwing:\nconst parsed = CallToolResultSchema.safeParse(payload);\nif (parsed.success) {\n    // parsed.data is a fully typed CallToolResult\n}\n```\n\n## Scope\n\nThis package exports **only** Zod schema constants (`*Schema`), in two groups:\n\n- the MCP **spec** schemas — `CallToolResultSchema`, `ListToolsResultSchema`, …; and\n- the **OAuth/OpenID** auth schemas — `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`, … (the schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js`).\n\nThe corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and\n[`@modelcontextprotocol/client`](https://www.npmjs.com/package/@modelcontextprotocol/client).\n\n> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js` (spec schemas) and `@modelcontextprotocol/sdk/shared/auth.js` (OAuth/OpenID schemas). Point those `*Schema` imports at `@modelcontextprotocol/core` and your existing `.parse()` /\n> `.safeParse()` calls keep working unchanged.\n","packages/middleware/README.md":"# Middleware packages\n\nThe packages in `packages/middleware/*` are **thin integration layers** that help you expose an MCP server in a specific runtime, platform, or web framework.\n\nThey intentionally **do not** add new MCP features or “business logic”. MCP functionality (tools, resources, prompts, transports, auth primitives, etc.) lives in `@modelcontextprotocol/server` (and other core packages). Middleware packages should primarily:\n\n- adapt request/response types to the SDK (e.g. Node.js `IncomingMessage`/`ServerResponse`)\n- provide small framework helpers (e.g. wiring, body parsing hooks)\n- supply safe defaults for common deployment pitfalls (e.g. localhost DNS rebinding protection)\n\n## Packages\n\n- `@modelcontextprotocol/express` — Express helpers (app defaults + Host header validation for DNS rebinding protection).\n- `@modelcontextprotocol/fastify` — Fastify helpers (app defaults + Host header validation).\n- `@modelcontextprotocol/hono` — Hono helpers (app defaults + JSON body parsing hook + Host header validation).\n- `@modelcontextprotocol/node` — Node.js Streamable HTTP transport wrapper for `IncomingMessage`/`ServerResponse`.\n\n## Typical usage\n\nMost servers use:\n\n- `@modelcontextprotocol/server` for the MCP server implementation\n- one middleware package for framework/runtime integration (this folder)\n- (optionally) additional platform/framework dependencies (Express, Hono, etc.)\n","packages/middleware/express/README.md":"# `@modelcontextprotocol/express`\n\nExpress adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Express integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create an Express app with sensible defaults for MCP servers\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n- protect routes with `requireBearerAuth` (validates `Authorization: Bearer …` via your `OAuthTokenVerifier`)\n- serve OAuth Protected Resource Metadata (RFC 9728) via `mcpAuthMetadataRouter`\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/express express\n\n# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):\nnpm install @modelcontextprotocol/node\n```\n\n## Exports\n\n- `createMcpExpressApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n- `requireBearerAuth(options)`\n- `mcpAuthMetadataRouter(options)`\n- `getOAuthProtectedResourceMetadataUrl(serverUrl)`\n- `OAuthTokenVerifier` (interface)\n\n## Usage\n\n### Create an Express app (localhost DNS rebinding protection by default)\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\n\nconst app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled\n```\n\n### Streamable HTTP endpoint (Express)\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpExpressApp();\n\napp.post('/mcp', async (req, res) => {\n    // Stateless example: create a server and a transport per request (with sessions: one pair per session).\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    await transport.handleRequest(req, res, req.body);\n});\n```\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { hostHeaderValidation } from '@modelcontextprotocol/express';\n\napp.use(hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));\n```\n","packages/middleware/fastify/README.md":"# `@modelcontextprotocol/fastify`\n\nFastify adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Fastify integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create a Fastify app with sensible defaults for MCP servers\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/fastify fastify\n\n# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):\nnpm install @modelcontextprotocol/node\n```\n\n## Exports\n\n- `createMcpFastifyApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n\n## Usage\n\n### Create a Fastify app (localhost DNS rebinding protection by default)\n\n```ts\nimport { createMcpFastifyApp } from '@modelcontextprotocol/fastify';\n\nconst app = createMcpFastifyApp(); // default host is 127.0.0.1; protection enabled\n```\n\n### Streamable HTTP endpoint (Fastify)\n\n```ts\nimport { createMcpFastifyApp } from '@modelcontextprotocol/fastify';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpFastifyApp();\n\napp.post('/mcp', async (request, reply) => {\n    // Stateless example: create a server and a transport per request (with sessions: one pair per session).\n    const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await mcpServer.connect(transport);\n\n    // Clean up when the client closes the connection (e.g. during SSE streaming).\n    reply.raw.on('close', () => {\n        transport.close();\n        mcpServer.close();\n    });\n\n    await transport.handleRequest(request.raw, reply.raw, request.body);\n});\n```\n\nTo reject non-POST requests with 405 Method Not Allowed, add routes for GET and DELETE that send a JSON-RPC error response.\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { hostHeaderValidation } from '@modelcontextprotocol/fastify';\n\napp.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));\n```\n","packages/middleware/hono/README.md":"# `@modelcontextprotocol/hono`\n\nHono adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Hono integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create a Hono app with sensible defaults for MCP servers\n- parse JSON request bodies and expose them as `c.get('parsedBody')` for Streamable HTTP transports\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/hono hono\n```\n\n## Exports\n\n- `createMcpHonoApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n\n## Usage\n\n### Streamable HTTP endpoint (Hono)\n\n```ts\nimport { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';\nimport { createMcpHonoApp } from '@modelcontextprotocol/hono';\n\nconst app = createMcpHonoApp();\napp.all('/mcp', async c => {\n    // Stateless example: create a server and a transport per request.\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    return transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') });\n});\n```\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { localhostHostValidation } from '@modelcontextprotocol/hono';\n\nconst app = createMcpHonoApp();\napp.use('*', localhostHostValidation());\n```\n","packages/middleware/node/README.md":"# `@modelcontextprotocol/node`\n\nNode.js adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Node.js integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server). It provides a Streamable HTTP transport that works with Node’s `IncomingMessage` / `ServerResponse`.\n\nFor web‑standard runtimes (Cloudflare Workers, Deno, Bun, etc.), use `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly.\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/node\n```\n\n## Exports\n\n- `NodeStreamableHTTPServerTransport`\n- `StreamableHTTPServerTransportOptions` (type alias for `WebStandardStreamableHTTPServerTransportOptions`)\n- `toNodeHandler(handler, opts?)` — adapt a web-standard `{ fetch }` MCP handler to a Node `(req, res, parsedBody?)` handler\n- `hostHeaderValidation(allowedHostnames)` / `localhostHostValidation()` — `Host` header guards for hand-wired `node:http` servers\n- `originValidation(allowedOriginHostnames)` / `localhostOriginValidation()` — `Origin` header guards for hand-wired `node:http` servers\n- `ToNodeHandlerOptions`, `FetchLikeMcpHandler`, `NodeMcpRequestHandler` (types for `toNodeHandler`)\n- `toWebRequest(req, parsedBody?, opts?)` — the Node `IncomingMessage` → web-standard `Request` conversion `toNodeHandler` performs internally, exported on its own (for example to feed `isLegacyRequest()` from a hand-wired `(req, res)` handler)\n- `ToWebRequestOptions` (options type for `toWebRequest`)\n- `NodeIncomingMessageLike`, `NodeServerResponseLike` (structural Node request/response shapes)\n\n## Usage\n\n### Express + Streamable HTTP\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpExpressApp();\n\napp.post('/mcp', async (req, res) => {\n    // Stateless example: create a server and a transport per request.\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n\n    // If you use Express JSON parsing, pass the pre-parsed body to avoid re-reading the stream.\n    await transport.handleRequest(req, res, req.body);\n});\n```\n\n### Node.js `http` server\n\nPlain `node:http` has no middleware chain, so bind loopback explicitly and\ncompose the `Host`/`Origin` guards in front of the transport — matching the\ndefaults the framework app factories (`createMcpExpressApp`,\n`createMcpHonoApp`, `createMcpFastifyApp`) apply for you. The guards answer\nrejected requests with `403` themselves and return `false`, so the handler\nmust not touch the request further.\n\n```ts\nimport { createServer } from 'node:http';\nimport { localhostHostValidation, localhostOriginValidation, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst validateHost = localhostHostValidation();\nconst validateOrigin = localhostOriginValidation();\n\ncreateServer(async (req, res) => {\n    if (!validateHost(req, res) || !validateOrigin(req, res)) return;\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    await transport.handleRequest(req, res);\n}).listen(3000, '127.0.0.1');\n```\n","packages/server-legacy/README.md":"# @modelcontextprotocol/server-legacy\n\n> **Deprecated** — This package is a frozen copy of v1 code for migration purposes only. It will not receive new features and is planned for removal in v3.\n\nProvides two pieces of v1 server functionality removed in v2:\n\n- **SSE Transport** (`./sse`) — The `SSEServerTransport` class, replaced in v2 by `NodeStreamableHTTPServerTransport` (from `@modelcontextprotocol/node`) or `WebStandardStreamableHTTPServerTransport` (from `@modelcontextprotocol/server`)\n- **OAuth Authorization Server** (`./auth`) — The `mcpAuthRouter` and related helpers, removed in v2 because MCP servers should use dedicated OAuth providers\n\n## Installation\n\n```bash\nnpm install @modelcontextprotocol/server-legacy\n```\n\n## Usage\n\n### SSE Transport (no Express dependency required)\n\n```ts\nimport { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse';\n```\n\n### OAuth Auth Router (requires Express)\n\n```ts\nimport { mcpAuthRouter } from '@modelcontextprotocol/server-legacy/auth';\n```\n\n### Everything (requires Express)\n\n```ts\nimport { SSEServerTransport, mcpAuthRouter } from '@modelcontextprotocol/server-legacy';\n```\n\n## Migration\n\n- **SSE → StreamableHTTP**: Use `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` (Node.js) or `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` (Web Standard / Cloudflare Workers)\n- **Auth router → Dedicated IdP**: Use a dedicated OAuth provider (Auth0, Keycloak, etc.) instead of the built-in OAuth Authorization Server\n","packages/server/README.md":"# `@modelcontextprotocol/server`\n\nThe MCP (Model Context Protocol) TypeScript server SDK. Build MCP servers that expose tools, resources, and prompts.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\nOptional framework adapters: [`@modelcontextprotocol/express`](https://www.npmjs.com/package/@modelcontextprotocol/express), [`@modelcontextprotocol/fastify`](https://www.npmjs.com/package/@modelcontextprotocol/fastify), [`@modelcontextprotocol/hono`](https://www.npmjs.com/package/@modelcontextprotocol/hono),\n[`@modelcontextprotocol/node`](https://www.npmjs.com/package/@modelcontextprotocol/node).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Server guide](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools)** — tools, resources, prompts, and the rest of the server surface\n- **[Serving guide](https://ts.sdk.modelcontextprotocol.io/v2/serving/http)** — stdio, HTTP, the framework adapters, sessions, and authorization\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n","skills/mcp-typescript-sdk-skills/SKILL.md":"---\nname: mcp-typescript-sdk-skills\ndescription: 'Serves and consumes Skills over MCP (SEP-2640, the `io.modelcontextprotocol/skills` extension) with v2 of the MCP TypeScript SDK: publishes a folder of `SKILL.md` skills from an MCP server with `loadSkills` and `SkillsExtension`, and lists, verifies and loads them in a client or host with `SkillsClient`. Use whenever a TypeScript project on `@modelcontextprotocol/server` or `@modelcontextprotocol/client` 2.x wants to expose agent skills from its MCP server, serve `skill://` resources or `skills/list`, or load skills from the MCP servers it connects to, even if the user does not say \"SEP-2640\" or \"extension\". Not for projects still on v1 (`@modelcontextprotocol/sdk`: upgrade them with the `mcp-typescript-sdk-upgrade-to-v2` skill first), for MCP SDKs in other languages (Python, Go, C#), or for writing the content of a `SKILL.md`.'\n---\n\n# Skills over MCP with the TypeScript SDK\n\nSkills over MCP lets an MCP server publish agent skills (folders with a `SKILL.md`) and lets clients list, verify and load them. In v2 of the TypeScript SDK it ships as the `ext/skills` subpaths of `@modelcontextprotocol/server` and `@modelcontextprotocol/client`.\n\nThe goal is working, verified code in the user's project. Use only the APIs shown here and in the references, and skip explaining the protocol unless the user asks.\n\n## Which side the project is on\n\nRead `package.json` and the source before writing anything:\n\n- **Server:** depends on `@modelcontextprotocol/server` and creates an `McpServer`, directly or in a factory passed to `serveStdio` or `createMcpHandler`. It publishes skills: follow [Serve skills](#serve-skills).\n- **Client or host:** depends on `@modelcontextprotocol/client` and creates a `Client`. It loads skills from the servers it connects to: follow [Use skills](#use-skills).\n- **Both** (a host that also runs a server, a gateway): follow both, each against its own code. A `Client` that exists only in the project's tests does not make it a host: that is where [Verify](#verify) runs.\n- **v1** (`@modelcontextprotocol/sdk`): Skills over MCP exists only in v2. Upgrade first with the `mcp-typescript-sdk-upgrade-to-v2` skill, then come back.\n\n## Check that the installed SDK has skills\n\nSkills over MCP ships in the `./ext/skills` export of the v2 packages, and an import of a subpath the installed package lacks fails only at build or run time. Check first, from the package that depends on the SDK (use `client` in place of `server` for a client):\n\n```bash\ngrep -c '\"./ext/skills\"' node_modules/@modelcontextprotocol/server/package.json\n```\n\nIf it prints `0`, check the latest release:\n\n```bash\nnpm view @modelcontextprotocol/server@latest exports --json | grep -c '\"./ext/skills\"'\n```\n\n- **The latest release has it:** upgrade (`server` and `client` to the same version, if the project uses both) and continue.\n- **It does not:** stop here. Tell the user that Skills over MCP is not in a released version of the SDK yet, and that this works once a release exports `@modelcontextprotocol/server/ext/skills`. Do not write `skills/list` handlers or `skill://` resources by hand as a workaround. Clients verify every file against the digests and frontmatter the extension publishes, and the extension also declares the capability, pages the catalog and handles both protocol versions, so a hand-written copy is easy to get wrong in ways those clients refuse, and drifts from the spec.\n\n## Serve skills\n\n**Pick where the skills come from.** Each source gives you an extension that you attach as shown below; only the first three have `reload()`:\n\n- **A folder on disk** (Node): `loadSkills`, which the steps below walk through.\n- **Skills defined in code** (Workers and other runtimes without a file system, tests): `memorySkillFiles`. See [Serve skills defined in code](references/server.md#serve-skills-defined-in-code).\n- **Your own storage** (an object store, a database, a git repository): a `SkillFiles` adapter. See [Serve skills from your own storage](references/server.md#serve-skills-from-your-own-storage).\n- **Skills that differ per caller or are generated per request:** a `SkillSource`. See [Generate skills with a `SkillSource`](references/server.md#generate-skills-with-a-skillsource).\n\n**Lay out the folder.** Each folder holding a `SKILL.md` is one skill. Its frontmatter needs a `name` equal to the folder's name and a `description`. Other folders only group skills in the URI.\n\n**Load the folder once and attach it before the server connects.** `attach` throws on a connected server, and each server takes one skills extension. A server connected by hand keeps its `server.connect(transport)` call, after `attach`.\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\n**Resolve the folder from the module, not the working directory.** A relative path like `'./skills'` resolves against `process.cwd()`, and MCP hosts may start stdio servers from another directory (such as `/`), where the load throws and takes the whole server down with it. Build the path from the module's location instead: `fileURLToPath(new URL('../skills', import.meta.url))` from `src/index.ts`, or `path.join(__dirname, '../skills')` in CommonJS.\n\n**Where a factory builds the server, attach inside it**, so every server it builds serves the same catalog. That covers `serveStdio(createServer)` on stdio as well as `createMcpHandler` over HTTP:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\n**Pick up edits with `reload()`** if the user expects changes to show without a restart. Call it from whatever tells the project the folder changed (a file watcher, an admin route, a deploy hook). A server that stays connected (stdio) notifies its clients itself. Behind `createMcpHandler`, publish the change on the handler, where `changes` is what `await skills.reload()` returned:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nThen tell the user about any load warnings, and [verify](#verify).\n\nRead [references/server.md](references/server.md) for `onWarning`, `strict`, `limits`, `prefix` and `pageSize`.\n\n## Use skills\n\n**Wrap the connected client and list.** Listing reads no skill file: each entry carries the skill's frontmatter and file list, enough to show what is on offer.\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\n`skills.supported` is `false` for a server that does not declare the extension; a host that connects to several servers checks it per server before listing. `list()` throws an `SdkError` if any entry is invalid, so a host listing servers it does not control creates the client with `new SkillsClient(client, { skipInvalid: true })` and logs the refused entries it returns in `invalid`.\n\n**Load a skill only after the user (or the host's policy) approves it.** Read its `SKILL.md`, which checks size, digest and frontmatter, and give `manifest.body` to the model:\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nRead the files `SKILL.md` refers to with `skills.read(entry, path)` when the model needs them. A failed check throws `SkillVerificationError`: do not use that file, and if it is the `SKILL.md`, do not load the skill.\n\nRead [references/client.md](references/client.md) for `skipInvalid`, verification reasons, `readDirectory`, `get`, dynamic skills and caching hints.\n\n## Mistakes to avoid\n\nServing:\n\n- **Writing the protocol by hand.** Serve skills only through `SkillsExtension` (`loadSkills`, `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource`). Never `setRequestHandler('skills/list', …)`, and never register your own resources under `skill://`: a resource at a skill file's URI takes precedence over the file, and clients then fail to verify it.\n- **Missing the skipped skills.** `loadSkills` skips a skill whose `name` differs from its folder, or that contains a symlink, and only warns (on stderr by default) while the others serve. Read the warnings after loading and tell the user which skills were skipped. Pass `strict: true` (in CI, say) to make the load throw instead.\n- **Dot-named entries.** Files and folders whose names start with `.` are skipped with no warning at all, and `strict: true` does not catch them. A skill in a `.name` folder, or a dotfile its `SKILL.md` refers to, is simply missing: rename it.\n- **A catalog outside the factory's servers.** `serveStdio` and `createMcpHandler` build their servers from a factory, and each server `createMcpHandler` builds lives for one request. Attach inside the factory, and behind `createMcpHandler` call `handler.notify.resourcesChanged()` when `reload()` reports `listChanged`, or connected clients never hear of the change.\n- **Expecting edits to show up on their own.** The catalog keeps the digests from load time, so a file edited on disk afterwards is refused with an error until `reload()` runs.\n- **`loadSkills` without a file system.** It reads the disk (`ext/skills/node`). On Workers and similar runtimes, use `memorySkillFiles` or a `SkillFiles` adapter from `@modelcontextprotocol/server/ext/skills`.\n\nLoading:\n\n- **Raw `resources/read`.** Never fetch skill files with `client.readResource()`: it skips the size, digest and frontmatter checks. Use `skills.read()`.\n- **Reading while listing.** Read a skill's files when it is loaded, never while listing.\n- **Skipping the host's duties.** Accept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before loading a skill from a server, and record which server each skill came from, so the instructions given to the model can be traced to their source.\n\nBoth:\n\n- **Invented APIs.** Take skills APIs only from `@modelcontextprotocol/server/ext/skills`, `@modelcontextprotocol/server/ext/skills/node` and `@modelcontextprotocol/client/ext/skills`, using names shown here or in the references. If you need something else, read the package's type declarations instead of guessing.\n\n## Verify\n\n**Server:** list every skill and read every file it lists through a real client. In a script or test, connect a `Client` to the server over an in-memory pair and read each file with `SkillsClient`. `read` throws when a file fails a check, and the loop throws when the server lists no skills:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#verify_inMemory\"\nimport { Client, InMemoryTransport } from '@modelcontextprotocol/client';\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();\nawait server.connect(serverTransport);\nconst client = new Client({ name: 'skills-check', version: '1.0.0' });\nawait client.connect(clientTransport);\n\nconst skillsClient = new SkillsClient(client);\nconst { skills: listed } = await skillsClient.list();\nif (listed.length === 0) throw new Error('the server listed no skills');\nfor (const entry of listed) {\n    const files = entry.resources === 'dynamic' ? [entry.uri] : entry.resources.map(file => file.uri);\n    for (const uri of files) await skillsClient.read(entry, uri);\n}\nconsole.log(`${listed.length} skill(s) listed and verified`);\n```\n\nAn entry whose `resources` is `\"dynamic\"` (only a `SkillSource` serves one) has no file list, so the loop reads only its `SKILL.md`: `read` checks the frontmatter but has no size or digest to check, and returns `verified: false`.\n\nFor a stdio server, the same loop works over `StdioClientTransport` (from `@modelcontextprotocol/client/stdio`) against the server's real start command; set its `cwd` option to another directory (such as `/`), as hosts do, so a path that depends on the working directory fails here and not in the user's host. Compare the number of skills listed with the number of skill folders, and account for each one missing: a warning, or a dot-name.\n\n**Client:** run the host against a server that serves at least one skill (the server path above takes a few lines, or `memorySkillFiles` in a test). Check that it lists the skill, asks before loading it, and reads its `SKILL.md` with `verified: true`.\n\n## References\n\n| Reference file                                          | Read it when                                  |\n| ------------------------------------------------------- | --------------------------------------------- |\n| [Serve skills from an MCP server](references/server.md) | The project serves skills from an MCP server  |\n| [Use skills from MCP servers](references/client.md)     | The project loads skills from its MCP servers |\n","skills/mcp-typescript-sdk-skills/references/client.md":"# Use skills from MCP servers\n\n<!-- #region guide -->\n\n`SkillsClient` lists a server's skills and reads their files, checking each file against what the server published.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\nAgainst a server with two skills, it prints each skill's name and description:\n\n```text\nrefunds: Process customer refunds.\ngit-workflow: Branching and review conventions.\n```\n\nListing reads no skill file: each **entry** carries the skill's frontmatter and its file list with digests, enough to decide which skill to load.\n\n## Handle refused entries\n\nCreate the client with `skipInvalid: true` to keep the valid skills and log the refused entries.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#list_invalid\"\nconst tolerant = new SkillsClient(client, { skipInvalid: true });\nconst { invalid } = await tolerant.list();\nfor (const refused of invalid) console.error(`skills: refused entry #${refused.index} ${refused.uri ?? ''}: ${refused.reason}`);\n```\n\nBy default `list()` throws an `SdkError` naming every entry that fails validation, and every copy of a repeated URI. With `skipInvalid: true` it returns the valid skills and puts those entries in `invalid`, each with `index`, `uri` when it has one, and `reason`. Against this server it is empty, so the loop prints nothing.\n\n## Load a skill\n\nRead the entry's `SKILL.md`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nThe file passes every check, and its frontmatter matches the entry:\n\n```text\ntrue {\n  name: 'git-workflow',\n  description: 'Branching and review conventions.'\n}\n```\n\n`read(entry)` checks the size and SHA-256 digest against the entry, then compares the frontmatter field by field. `manifest.body` holds the instructions to give the model.\n\nThe comparison parses the frontmatter with YAML 1.2 core-schema typing, so `yes` is the string `\"yes\"`, not `true`. If a server builds its entries with a parser that types values differently, pass a matching YAML parser as `parseFrontmatter` to `new SkillsClient`; it receives the YAML between the `---` lines.\n\n## Read the files a skill references\n\nPass the path `SKILL.md` uses.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_reference\"\nconst review = await skills.read(git, 'references/REVIEW.md');\nconsole.log(review.text);\n```\n\n`review.text` holds the verified file:\n\n```text\n# Review checklist\n\n- Tests pass.\n\n```\n\nA relative path resolves against the skill's folder, as a link in `SKILL.md` does, and `read` ignores a fragment. It matches the path to the entry's file list however either side percent-encodes the name, so `references/Q&A.md` works. `bytes` always holds the file's bytes; `text` holds the file's text when the server sent it as text, and always for the skill's own `SKILL.md`.\n\n## Handle verification failures\n\nCatch `SkillVerificationError`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillVerificationError\"\nimport { SkillVerificationError } from '@modelcontextprotocol/client/ext/skills';\n\ntry {\n    await skills.read(git, 'references/MISSING.md');\n} catch (error) {\n    if (!(error instanceof SkillVerificationError)) throw error;\n    console.log(error.reason, error.uri);\n}\n```\n\nThe entry does not list `references/MISSING.md`, so `read` refuses it before sending any request:\n\n```text\nunlisted skill://git-workflow/references/MISSING.md\n```\n\n`reason` is `unlisted`, `size`, `digest` or `frontmatter`; a path that does not parse is `unlisted` too. When a size, digest or frontmatter check fails on a copy the client served from its response cache, `read` fetches the file once more with `cacheMode: 'refresh'` and throws only if the fresh copy fails as well; a copy that came from the server fails at once. On any reason, do not use the file; for `SKILL.md`, do not load the skill.\n\n## Browse a skill folder\n\nList a folder's direct children when the server declares `directoryRead`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#readDirectory\"\nif (skills.directoryRead) {\n    const { resources: children } = await skills.readDirectory('skill://acme/billing/refunds');\n    console.log(children.map(child => `${child.name} ${child.mimeType}`).join('\\n'));\n}\n```\n\nThe `refunds` folder holds its `SKILL.md` and a `templates` folder:\n\n```text\nSKILL.md text/markdown\ntemplates inode/directory\n```\n\n## Get a skill by URI\n\nFetch one entry, listed or not.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#get\"\nconst { skill: refunds } = await skills.get('skill://acme/billing/refunds/SKILL.md');\nconsole.log(refunds.frontmatter.name);\n```\n\nIt prints the skill's name from the entry's frontmatter:\n\n```text\nrefunds\n```\n\nOn 2026-07-28, `list()`, `get()` and `readDirectory()` also return the server's caching hints, `ttlMs` and `cacheScope`, and throw an `SdkError` when a response leaves either out.\n\n::: info Dynamic skills\nAn entry whose `resources` is `\"dynamic\"` has no file list. `read` then skips the size and digest checks and returns `verified: false`, but still compares the `SKILL.md` frontmatter.\n:::\n\n::: warning Host responsibilities\nAccept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before you load a skill from a server, and record which server each skill came from. Read a skill's files when you load it, never while listing.\n:::\n\n## Recap\n\n- `new SkillsClient(client)` wraps a connected client; `list()` returns entries without reading files.\n- `list()` throws on an invalid entry; `skipInvalid: true` keeps the valid skills and returns the refused ones in `invalid`.\n- `read(entry)` verifies `SKILL.md` (size, digest, frontmatter) and returns its frontmatter and body.\n- `read(entry, path)` resolves a path against the skill's folder and verifies that file.\n- A failed check throws `SkillVerificationError` with a `reason`.\n\n<!-- #endregion guide -->\n","skills/mcp-typescript-sdk-skills/references/server.md":"# Serve skills from an MCP server\n\n<!-- #region guide -->\n\n`loadSkills` reads a folder of skills once; `attach` serves it on a server.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\nA **skill** is a folder with a `SKILL.md` and the files it references (SEP-2640). Clients list skills with `skills/list` and read each file with `resources/read`, checking it against the digest your server published.\n\n## Lay out the skills folder\n\nEach folder that contains a `SKILL.md` is one skill; other folders only group skills in the URI.\n\n```text\nskills/\n├── git-workflow/\n│   ├── SKILL.md\n│   └── references/REVIEW.md\n└── acme/billing/refunds/\n    └── SKILL.md\n```\n\n`SKILL.md` starts with YAML frontmatter holding `name` (the same as the skill's folder name) and `description`. Print the URIs the catalog publishes:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#entries\"\nconsole.log(skills.entries.map(skill => skill.uri).join('\\n'));\n```\n\nIt prints one `SKILL.md` URI per skill, sorted:\n\n```text\nskill://acme/billing/refunds/SKILL.md\nskill://git-workflow/SKILL.md\n```\n\n## Serve one catalog from every server\n\nAttach the same catalog to each server your handler creates.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\nThe catalog keeps metadata only (paths, digests, sizes, frontmatter); each read goes to the folder, and the SDK serves the bytes only if they still match the published digest. It sends a file as `text` whenever its bytes are valid UTF-8 (a BOM included), whatever its MIME type, and as a base64 `blob` otherwise.\n\n## Pick up changes with `reload()`\n\nRescan the folder and swap the new catalog in at once.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload\"\nconst changes = await skills.reload();\nconsole.log(changes);\n```\n\nWith nothing changed on disk, `reload()` reports no changes:\n\n```text\n{ added: [], removed: [], changed: [], listChanged: false }\n```\n\nUntil you call `reload()`, the server answers a read of a file edited on disk with an error instead of unverified content. If the rescan fails, `reload()` rejects and the old catalog keeps serving.\n\n`listChanged` is `true` when a skill appears, disappears, or changes its name or description. A server that stays connected (on stdio, or in a stateful 2025-11-25 HTTP session) then sends its clients `notifications/resources/list_changed` itself. A server that `createMcpHandler` builds lives for one request, so publish the change on the handler instead:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nEvery open `subscriptions/listen` stream that opted in to resource list changes receives the notification.\n\n## Serve skills defined in code\n\nPass the files to `memorySkillFiles` where there is no file system, such as Workers or tests.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#memorySkillFiles\"\nimport { memorySkillFiles, SkillsExtension } from '@modelcontextprotocol/server/ext/skills';\n\nconst RELEASE_NOTES_MD = `---\nname: release-notes\ndescription: Write release notes from merged pull requests.\n---\n# Release notes\n`;\n\nconst inline = await SkillsExtension.load(memorySkillFiles({ 'release-notes/SKILL.md': RELEASE_NOTES_MD }));\n```\n\n`memorySkillFiles` re-reads the object you pass whenever the catalog rescans (at load and on `reload()`), so `reload()` sees files you add or change later.\n\n## Serve skills from your own storage\n\nImplement `SkillFiles`, two functions over relative paths, to load from any store.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillFiles_custom\"\nimport type { SkillFiles } from '@modelcontextprotocol/server/ext/skills';\n\n// Two operations over '/'-separated paths, '' being the root: here, a key-value store of objects.\nfunction bucketSkillFiles(bucket: ReadonlyMap<string, Uint8Array>): SkillFiles {\n    return {\n        async list(dir) {\n            const prefix = dir === '' ? '' : `${dir}/`;\n            const children = new Map<string, 'file' | 'directory'>();\n            for (const key of bucket.keys()) {\n                if (!key.startsWith(prefix)) continue;\n                const [name, ...rest] = key.slice(prefix.length).split('/');\n                if (name !== undefined) children.set(name, rest.length > 0 ? 'directory' : 'file');\n            }\n            return [...children].map(([name, kind]) => ({ name, kind }));\n        },\n        async read(key) {\n            const bytes = bucket.get(key);\n            if (bytes === undefined) throw new Error(`no object ${key}`);\n            return bytes;\n        }\n    };\n}\n\nconst fromBucket = await SkillsExtension.load(\n    bucketSkillFiles(new Map([['release-notes/SKILL.md', new TextEncoder().encode(RELEASE_NOTES_MD)]]))\n);\n```\n\nThe catalog logic (discovery, digests, rule checks, paging, verified reads) is the same for every adapter. Report a symlink or other special file as `kind: 'unsupported'`; the catalog skips the skill that contains one.\n\n## Generate skills with a `SkillSource`\n\nBuild entries with `skillEntry` and serve them from a `SkillSource` when the catalog depends on the request.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillSource\"\nimport type { SkillSource } from '@modelcontextprotocol/server/ext/skills';\nimport { skillEntry } from '@modelcontextprotocol/server/ext/skills';\n\nconst ON_CALL_MD = `---\nname: on-call\ndescription: Handle a page while on call.\n---\n# On call\n`;\nconst onCall = await skillEntry('on-call', { 'SKILL.md': ON_CALL_MD });\n\nconst source: SkillSource = {\n    list: async () => ({ skills: [onCall] }),\n    get: async uri => (uri === onCall.uri ? onCall : undefined),\n    read: async uri => (uri === onCall.uri ? { text: ON_CALL_MD, mimeType: 'text/markdown' } : undefined)\n};\nconst generated = new SkillsExtension({ source });\n```\n\nEvery source method receives the request context (`ctx`), so you can list different skills per caller. Add `readDirectory` to serve folder listings, and `listResources` to list skills in `resources/list`.\n\n## Handle invalid skills\n\nRoute warnings where you want them; by default they go to stderr.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#onWarning\"\nconst checked = await loadSkills('./skills', {\n    onWarning: ({ path, message }) => console.error(`skills: ${path}: ${message}`)\n});\n```\n\n`loadSkills` skips, with a warning, any skill that breaks the rules, such as a `name` that differs from its folder or a symlinked file; the other skills still serve. Skills over 512 files or 16 MiB still publish, with a warning; `limits` changes those thresholds. The same options take `prefix` (`'acme'` serves every skill under `skill://acme/…`) and `pageSize` (entries per `skills/list` and directory page, default 100).\n\nSet `strict: true` to make `loadSkills` throw a `SkillCatalogError` listing every problem instead. Under `strict: true`, a symlink or other special file anywhere in the folder fails the load, even one outside every skill; hidden entries (names starting with `.`) are the exception, since the scan skips them entirely.\n\n::: warning\nDo not register your own resources under `skill://`. A resource you register at a skill file's URI takes precedence over that file, and clients then fail to verify it.\n:::\n\n::: info\nSkills work on both protocol versions; the `cacheHint` option applies on 2026-07-28 only — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).\n:::\n\n## Recap\n\n- `loadSkills('./skills')` builds a catalog once; `attach(server)` serves it on any number of servers.\n- Each folder with a `SKILL.md` is a skill; other folders group skills in the URI.\n- `reload()` swaps in a rescanned catalog all at once; behind `createMcpHandler`, call `handler.notify.resourcesChanged()` when it reports `listChanged`.\n- `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource` serve skills without a folder.\n- `loadSkills` skips invalid skills with a warning; `strict: true` makes it throw instead.\n\n<!-- #endregion guide -->\n","skills/mcp-typescript-sdk-upgrade-to-v2/SKILL.md":"---\nname: mcp-typescript-sdk-upgrade-to-v2\ndescription: 'Upgrades TypeScript code from v1 of the MCP TypeScript SDK (`@modelcontextprotocol/sdk`) to v2 (`@modelcontextprotocol/client`, `/server`, `/core` and the middleware packages): runs the official codemod, then works through what it cannot rewrite. Use whenever a project depends on `@modelcontextprotocol/sdk` and the user wants to migrate, upgrade, port or bump it to v2, or is fixing v1-to-v2 breakage such as missing `McpError`, `RequestHandlerExtra` or `*Schema` exports, `@mcp-codemod-error` markers, or changed auth, error, transport or handler behavior, even if they do not say \"migration\".'\n---\n\n<!-- #region intro -->\n\n# Upgrading from v1.x to v2\n\nThis guide covers upgrading from `@modelcontextprotocol/sdk` (v1.x) to the v2 packages.\nIt is written for shell-capable agents and humans alike: run the codemod first, then\nwork through the manual sections for what the codemod can't rewrite.\n\nIf you are already on v2 and want to adopt the **2026-07-28 protocol revision**, see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md) instead.\n\n## TL;DR — quick path\n\n1. **Prerequisites.** Node.js 20+. v2 is ESM-first but ships a CommonJS build too, so\n   both `import` and `require('@modelcontextprotocol/…')` resolve natively.\n2. **Run the codemod.**\n    ```bash\n    npx @modelcontextprotocol/codemod@latest v1-to-v2 .\n    ```\n    Run it at the **package root** (`.`), not `./src` — it also rewrites `package.json`,\n    and real projects import the SDK from `test/`, `scripts/`, and fixtures too.\n3. **Grep for markers.** Anything the codemod recognized but could not safely rewrite is\n   marked in place:\n    ```bash\n    grep -rn '@mcp-codemod-error' .\n    ```\n4. **Type-check.** `tsc --noEmit` (or your build). Remaining errors map to the\n   [manual sections](#manual-changes-what-the-codemod-does-not-handle) below.\n5. **Format.** The codemod rewrites the AST without reformatting — run your formatter on\n   the changed files (`prettier --write` / `eslint --fix` / `biome format --write`); the\n   codemod prints the exact command after it runs.\n6. **Run your tests.**\n\nMigrating a large codebase gradually instead of in one pass? See\n[Migrating in stages (large codebases)](references/packaging-runtime.md#migrating-in-stages-large-codebases).\n\n<!-- #endregion intro -->\n\n<!-- #region codemod -->\n\n## What the codemod handles\n\nThe codemod ([`@modelcontextprotocol/codemod`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md))\nmechanically applies every rename whose mapping is fixed. The mappings are the\n**source of truth** — they live in the codemod package and are not reproduced here:\n\n| Mapping                                                                                   | Source file                                                                                                                                                                  |\n| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk/...` import paths → v2 packages                                | [`mappings/importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)                   |\n| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts)                   |\n| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`                  | [`mappings/schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts)   |\n| `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` property remap                                 | [`mappings/contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) |\n\nIn addition the codemod:\n\n- Updates `package.json` dependencies (`@modelcontextprotocol/sdk` → the v2 packages\n  your imports actually use).\n- Rewrites `.tool()` / `.prompt()` / `.resource()` to `registerTool` / `registerPrompt`\n  / `registerResource` and wraps `inputSchema` / `outputSchema` / `argsSchema` /\n  `uriSchema` raw Zod shapes with `z.object()`, adding `import { z } from 'zod'`\n  when the file has no `z` binding.\n- Drops the result-schema argument from `client.request()` / `client.callTool()` for\n  spec methods.\n- Routes the spec Zod `*Schema` constants imported from `sdk/types.js` to\n  `@modelcontextprotocol/core` (mixed imports are split; `.parse()` / `.safeParse()`\n  calls are left untouched). Task-handler schema constants\n  (`GetTaskRequestSchema` etc.) used as `setRequestHandler` args are **not** rewritten\n  — the experimental tasks feature was removed (SEP-2663), so each such registration\n  is marked with an action-required diagnostic instead (see\n  [Experimental tasks interception removed](references/behavioral-changes.md#experimental-tasks-interception-removed)).\n- Renames `ErrorCode` → `ProtocolErrorCode` and routes the local-only members\n  (`RequestTimeout`, `ConnectionClosed`) to `SdkErrorCode` — rewriting an all-SDK\n  condition's `instanceof ProtocolError` guard to `SdkError`, and marking guards\n  that mix the two enums.\n- Renames every `StreamableHTTPError` reference to `SdkHttpError` and adds the import\n  (constructor calls are marked for review — argument shape changed).\n- Replaces `IsomorphicHeaders` with the Web Standard `Headers` type and drops the\n  import (a warning notes `Headers` uses `.get()`/`.set()`, not bracket access).\n- Rewrites `SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`.\n- Renames `RequestHandlerExtra` → `ServerContext` / `ClientContext` and the `extra`\n  parameter to `ctx`.\n- Rewrites `vi.mock` / `jest.mock` and dynamic `import()` paths.\n- Renames the `ResourceTemplate` **type** imported from `@modelcontextprotocol/sdk/types.js`\n  to `ResourceTemplateType` (the spec wire type). The `ResourceTemplate` URI-template\n  helper **class** from `server/mcp.js` keeps its name and is not renamed.\n- Drops `@modelcontextprotocol/sdk/server/zod-compat.js` imports.\n- Inverts optional completable nesting — `completable(schema.optional(), cb)` becomes\n  `completable(schema, cb).optional()` (see\n  [Standard Schema objects](references/server-registration.md#standard-schema-objects-raw-shapes-deprecated)); shapes it\n  cannot invert get an `@mcp-codemod-error` marker.\n- Rewrites `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\n  client or server package root, like the module's other symbols.\n\n## What the codemod does NOT handle\n\nEach of these maps to a manual section below. The codemod marks every site it\nrecognized but could not safely rewrite with an `@mcp-codemod-error` comment.\n\n- **Node 20 / ESM** — pre-flight, not a code rewrite. → [Packaging & runtime](references/packaging-runtime.md#packaging--runtime)\n- **Header-read `.get()` rewrite** — `IsomorphicHeaders` is renamed to `Headers`\n  and `extra.requestInfo?.headers[…]` is remapped to `ctx.http?.req?.headers[…]`, but\n  converting that bracket access to `.get()` is manual. (Headers you _pass in_ via\n  `requestInit.headers` need no rewrite — plain objects remain valid.)\n  → [HTTP & headers](references/http-headers.md#http--headers)\n- **`ctx.mcpReq.send()` schema-arg drop** — the codemod drops the schema arg from\n  `client.request()` / `client.callTool()` but leaves nested `ctx.mcpReq.send()` calls\n  alone. → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **OAuth error-class consolidation** — `instanceof InvalidGrantError` → `OAuthError` +\n  `OAuthErrorCode` is a judgment rewrite. → [Auth](references/auth.md#auth)\n- **`SdkErrorCode` branch selection** — the codemod renames `StreamableHTTPError` →\n  `SdkHttpError`; deciding which `SdkErrorCode` branch a given catch should match is\n  judgment. → [Errors](references/errors.md#errors)\n- **Namespace schema access** — `import * as t from '…/types.js'` +\n  `t.CallToolResultSchema.parse(…)` can't be split per-symbol; the codemod flags it\n  action-required — re-import the schema from `@modelcontextprotocol/core` by hand.\n  → [Types & schemas](references/types-schemas.md#types--schemas)\n- **Import-less (injected) SDK surfaces** — the codemod is import-driven: a file that\n  receives the SDK surface as a parameter (dependency injection, factory seams) and has\n  no SDK import is never rewritten, and the v1 idioms there fail at **runtime**, not\n  compile time — e.g. the v1 schema-first `setRequestHandler(Schema, …)` form throws a\n  `TypeError` at registration. Grep such seams for v1 API tokens beyond import\n  statements (`setRequestHandler(`, `ErrorCode.`, `extra.`) and apply the\n  [handler-registration](references/protocol-ctx.md#setrequesthandler--setnotificationhandler-use-method-strings)\n  and [Errors](references/errors.md#errors) sections by hand.\n  → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **Behavioral adaptation** — list auto-aggregation, capability empties, lazy validator\n  compilation, output-schema validation rules. → [Behavioral changes](references/behavioral-changes.md#behavioral-changes)\n\n---\n\n## Manual changes (what the codemod does not handle)\n\n<!-- #endregion codemod -->\n\nEach subsystem has its own reference file. Open the one that matches the type error, runtime failure or `@mcp-codemod-error` marker in front of you; most migrations need only a few of them.\n\n| Reference file                                                             | Read it when                                                                                                                                                                                   |\n| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Packaging & runtime](references/packaging-runtime.md)                     | Choosing which v2 packages to install, or dealing with Node/ESM/CommonJS setup, monorepo workspaces, Jest, nested `zod` copies, migrating in stages, or a library that peer-depends on the SDK |\n| [Imports & transports](references/imports-transports.md)                   | A transport import needs a runtime decision (Node vs web-standard Streamable HTTP) or a stdio import moved to a `./stdio` subpath                                                              |\n| [Low-level protocol & handler context (`ctx`)](references/protocol-ctx.md) | Code uses `extra` / `ctx`, `setRequestHandler` / `setNotificationHandler`, `request()`, `ctx.mcpReq.send()` or `callTool()`                                                                    |\n| [Server registration API](references/server-registration.md)               | Code registers tools, prompts or resources, or passes raw Zod shapes as schemas                                                                                                                |\n| [HTTP & headers](references/http-headers.md)                               | Code reads HTTP request headers                                                                                                                                                                |\n| [Errors](references/errors.md)                                             | Code throws or catches `McpError` / `ProtocolError`, `ErrorCode`, `SdkError` or `SdkHttpError`                                                                                                 |\n| [Auth](references/auth.md)                                                 | Code uses OAuth (`auth()`, `OAuthClientProvider`, OAuth error classes) or bearer-token auth                                                                                                    |\n| [Types & schemas](references/types-schemas.md)                             | Code imports Zod `*Schema` constants or removed type aliases, or depends on JSON Schema details                                                                                                |\n| [Behavioral changes](references/behavioral-changes.md)                     | The code compiles but tests or runtime behavior differ                                                                                                                                         |\n| [Enhancements](references/enhancements.md)                                 | Optional: v2 features worth adopting once the upgrade is done                                                                                                                                  |\n| [Unchanged APIs](references/unchanged-apis.md)                             | Checking whether an API changed at all before rewriting it                                                                                                                                     |\n\n<!-- #region need-help -->\n\n## Need help?\n\n- The codemod's [`@mcp-codemod-error`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md) markers point\n  at every site it could not safely rewrite.\n- The [Troubleshooting](https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting.md) page covers common errors and their fixes.\n- Runnable [examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)\n  for every subsystem.\n- Open an issue on [GitHub](https://github.com/modelcontextprotocol/typescript-sdk/issues).\n\n<!-- #endregion need-help -->\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/auth.md":"### Auth\n\n#### OAuth error consolidation\n\nThe individual OAuth error classes are replaced with a single `OAuthError` + `OAuthErrorCode`.\nThe `OAUTH_ERRORS` constant is removed. The codemod does not rewrite `instanceof` checks\non these classes — switch on `error.code` instead.\n\n| v1 class                       | v2 equivalent                                           |\n| ------------------------------ | ------------------------------------------------------- |\n| `InvalidRequestError`          | `OAuthError` + `OAuthErrorCode.InvalidRequest`          |\n| `InvalidClientError`           | `OAuthError` + `OAuthErrorCode.InvalidClient`           |\n| `InvalidGrantError`            | `OAuthError` + `OAuthErrorCode.InvalidGrant`            |\n| `UnauthorizedClientError`      | `OAuthError` + `OAuthErrorCode.UnauthorizedClient`      |\n| `UnsupportedGrantTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedGrantType`    |\n| `InvalidScopeError`            | `OAuthError` + `OAuthErrorCode.InvalidScope`            |\n| `AccessDeniedError`            | `OAuthError` + `OAuthErrorCode.AccessDenied`            |\n| `ServerError`                  | `OAuthError` + `OAuthErrorCode.ServerError`             |\n| `TemporarilyUnavailableError`  | `OAuthError` + `OAuthErrorCode.TemporarilyUnavailable`  |\n| `UnsupportedResponseTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedResponseType` |\n| `UnsupportedTokenTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedTokenType`    |\n| `InvalidTokenError`            | `OAuthError` + `OAuthErrorCode.InvalidToken`            |\n| `MethodNotAllowedError`        | `OAuthError` + `OAuthErrorCode.MethodNotAllowed`        |\n| `TooManyRequestsError`         | `OAuthError` + `OAuthErrorCode.TooManyRequests`         |\n| `InvalidClientMetadataError`   | `OAuthError` + `OAuthErrorCode.InvalidClientMetadata`   |\n| `InsufficientScopeError`       | `OAuthError` + `OAuthErrorCode.InsufficientScope` ¹     |\n| `InvalidTargetError`           | `OAuthError` + `OAuthErrorCode.InvalidTarget`           |\n| `CustomOAuthError`             | `new OAuthError(customCode, message)`                   |\n\n¹ Unrelated to the new transport-layer `InsufficientScopeError` (SEP-2350) exported from\n`@modelcontextprotocol/client`, which carries an RFC 6750 challenge from the resource\nserver and extends `OAuthClientFlowError`, **not** `OAuthError`. Do not rewrite that one.\n\n```typescript\n// v1\nif (error instanceof InvalidClientError) { ... }\n// v2\nimport { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }\n```\n\n⚠ **Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from\n`@modelcontextprotocol/express`, or from `@modelcontextprotocol/server` on\nweb-standard hosts) classifies the error your\n`OAuthTokenVerifier.verifyAccessToken()` throws: a v2\n`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +\n`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from\n`server-legacy`) or a generic `Error` falls through as unexpected — **invalid tokens\nbecome HTTP `500`**. When you re-point `requireBearerAuth` at\n`@modelcontextprotocol/express`, migrate the error classes your verifier throws in the\nsame change.\n\nA frozen copy of the v1 classes (and `mcpAuthRouter`) is available from\n`@modelcontextprotocol/server-legacy/auth` during migration.\n\n#### `AuthProvider` — non-OAuth bearer auth and the widened `authProvider` option\n\nThe transport `authProvider` option is widened to `AuthProvider | OAuthClientProvider`.\n**`AuthProvider`** is a new minimal interface — `{ token(): Promise<string | undefined>;\nonUnauthorized?(ctx): Promise<void> }` — for static-token / non-OAuth bearer auth.\nTransports call `token()` before every request and `onUnauthorized()` on 401 (then retry\nonce). Existing `OAuthClientProvider` implementations need no changes — transports adapt\nthem internally via the new `adaptOAuthProvider()` export. Also exported:\n`isOAuthClientProvider()` (type guard) and `handleOAuthUnauthorized()` (the standard\nOAuth `onUnauthorized` behavior, for composing your own adapter).\n\n#### OAuth client flow — behavioral changes\n\n- **Resolved scope passed to DCR (SEP-835).** `auth()` now computes the resolved scope\n  once (WWW-Authenticate → PRM `scopes_supported` → `clientMetadata.scope`) and passes\n  it to **both** the DCR POST body and the authorization request. `registerClient()`\n  gained an optional `scope` parameter that overrides `clientMetadata.scope` in the\n  registration body.\n- **OAuth error on HTTP 200.** `exchangeAuthorization()` / `refreshAuthorization()` now\n  throw `OAuthError` when the AS returns HTTP 200 with a JSON `{error: ...}` body (e.g.\n  GitHub). v1 surfaced this as a Zod parse failure on the tokens schema.\n- **Metadata discovery falls through on 502.** `discoverAuthorizationServerMetadata()`\n  treats `502 Bad Gateway` like 4xx — fall through to the next candidate URL instead of\n  throwing (fixes path-aware discovery behind reverse proxies). Other 5xx still throw.\n- **Scoped credential invalidation on `invalid_client` / `unauthorized_client`.** The\n  `auth()` retry for these errors now issues two scoped calls —\n  `invalidateCredentials('client')` then `invalidateCredentials('tokens')` — instead of\n  v1's single `invalidateCredentials('all')`, deliberately preserving the stored\n  discovery state so the callback-leg check on retry does not mask the original error.\n  A provider whose `invalidateCredentials()` implementation special-cases the `'all'`\n  scope must handle the split calls.\n- **Token persistence failures after a refresh now propagate.** v1 wrapped both\n  `refreshAuthorization()` and the `saveTokens()` that persists its result in one\n  `try`/`catch`, so a provider's persistence error was discarded alongside AS-side refresh\n  failures and `auth()` fell through to a fresh authorization request, returning\n  `'REDIRECT'`. Only the refresh call is guarded now — persisting runs after it and rejects\n  to the caller. Against an AS that rotates refresh tokens this was destructive rather than\n  merely quiet: the exchange has already succeeded, so the old refresh token is invalidated\n  server-side the moment the new one is issued, and dropping the new token set leaves\n  nothing usable on either side. A provider whose `saveTokens()` can throw (transient\n  storage errors, file-lock contention) must handle the rejection from `auth()` — and from\n  the transport 401-retry paths built on it — where v1 silently re-authorized. Refresh\n  failures themselves keep their control flow: a `ServerError` or an unknown error still\n  falls through to a new authorization request, and `invalid_grant` / `invalid_client` /\n  `unauthorized_client` are still recovered by discarding stored credentials and retrying.\n  Both routes now emit a `console.warn` naming the cause, so an unexplained re-auth prompt\n  can be traced to the failure that triggered it.\n\n#### OAuth client flow errors (new)\n\nThe OAuth client flow now throws dedicated classes from `@modelcontextprotocol/client`\n(all extend `OAuthClientFlowError`, **not** `OAuthError` — `auth()`'s `OAuthError` retry\npath will not catch them):\n\n| Throw site                                                                                                                | v2 class                                                                              |\n| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| `registerClient()` rejected by AS (⚠ `@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) | `RegistrationRejectedError` (`status`, `body`, `submittedMetadata`)                   |\n| Token-exchange / refresh / `fetchToken` / Cross-App grant on a non-`https:` token endpoint                                | `InsecureTokenEndpointError` (`tokenEndpoint`)                                        |\n| RFC 9207 `iss` mismatch / RFC 8414 §3.3 issuer-echo mismatch                                                              | `IssuerMismatchError` (`kind`, `expected`, `received`)                                |\n| Transport 403 `insufficient_scope` with `onInsufficientScope: 'throw'`, or default mode without an `OAuthClientProvider`  | `InsufficientScopeError` (`requiredScope`, `resourceMetadataUrl`, `errorDescription`) |\n| `auth()` callback leg: discovery resolves a different AS than the recorded redirect target                                | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n| `auth()` on a provider that cannot re-register, or `fetchToken()`: client information stamped for a different AS          | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n\n#### Connect-time OAuth retry (`UnauthorizedError`)\n\n`UnauthorizedError` survives in v2 (exported from `@modelcontextprotocol/client` —\nits only appearance in the error table above is the removed `SSEClientTransport.send()`\n401 path), and the v1 connect-time pattern carries over: catch it from `connect()`,\ncomplete the browser flow, call `transport.finishAuth(…)`, reconnect.\n\n```typescript\ntry {\n    await client.connect(transport);\n} catch (error) {\n    if (!(error instanceof UnauthorizedError)) throw error;\n    // provider.redirectToAuthorization() has been called; complete the flow,\n    // then reconnect on a FRESH transport (a started transport cannot be restarted).\n    await transport.finishAuth(new URL(callbackUrl).searchParams);\n    await client.connect(new StreamableHTTPClientTransport(url, { authProvider: provider }));\n}\n```\n\nThis direct `instanceof` check works in every version-negotiation mode: under the\nprobing modes (`versionNegotiation: { mode: 'auto' }`, with or without a pin) the\nconnect-time `UnauthorizedError` also propagates unchanged from `connect()`. Older\nreleases wrapped it as `SdkError(SdkErrorCode.EraNegotiationFailed)` with the error at\n`error.data.cause` — that unwrap is no longer needed. See the\n[client OAuth guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/oauth.md).\n\n#### `auth()` options are now `AuthOptions`\n\nThe inline options object on `auth()` is now the named `AuthOptions` type. New fields:\n`iss?: string` (the form-urldecoded `iss` from the authorization callback — pass it\nalongside `authorizationCode` for RFC 9207 validation), `skipIssuerMetadataValidation?:\nboolean` (security-weakening opt-out of the RFC 8414 §3.3 issuer-echo check), and\n`forceReauthorization?: boolean` (skip the refresh-token branch — set by the transport's\nstep-up path; hosts driving step-up themselves set it under the same condition).\n\n#### Authorization-server mix-up defense (RFC 9207 / RFC 8414 §3.3) — action required\n\n`transport.finishAuth()` and `auth()` now validate `iss` from the authorization callback\nagainst the issuer recorded from validated AS metadata. A mismatched `iss` throws\n`IssuerMismatchError` before the code is exchanged regardless of advertised support; a\n**missing** `iss` throws only when the AS advertised\n`authorization_response_iss_parameter_supported: true`.\n\nPass the callback URL's `URLSearchParams` so the SDK can read `iss` alongside `code`.\nThe SDK does **not** validate `state`; compare it yourself before calling `finishAuth`:\n\n```typescript\nconst params = new URL(callbackUrl).searchParams;\nif (params.get('state') !== expectedState) throw new Error('state mismatch');\nawait transport.finishAuth(params); // SDK reads `code` + `iss`\n```\n\n`transport.finishAuth(code, iss)` remains supported. Do **not** display `error` /\n`error_description` / `error_uri` from a callback that failed `iss` validation — those\nvalues are attacker-controlled in a mix-up attack.\n\n`discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not\nexactly match the URL it was fetched for (RFC 8414 §3.3). Set\n`skipIssuerMetadataValidation: true` only as a temporary workaround for a known-misconfigured AS.\n\n(`@modelcontextprotocol/server-legacy` AS implementers: `mcpAuthRouter()` now advertises\n`authorization_response_iss_parameter_supported: true` by default and the bundled\nauthorize handler appends `iss` to every redirect issued via `res.redirect(...)` on the\nsupplied `res`. If you emit `Location` another way, append `params.issuer` as `iss`\nyourself; if your callback is issued by an upstream AS you proxy to, set\n`authorizationResponseIssParameterSupported = false` so the metadata does not over-claim.)\n\n#### Dynamic Client Registration defaults (SEP-837, SEP-2207)\n\n`auth()` now resolves `provider.clientMetadata` once via `resolveClientMetadata()` and\napplies defaults to the DCR body: `grant_types` defaults to\n`['authorization_code', 'refresh_token']`; `application_type` is derived from\n`redirect_uris` (loopback / custom URI scheme → `'native'`, else `'web'`). A field you\nset explicitly is never overwritten. The `grant_types` default applies to the DCR body\nonly — it does **not** drive the `offline_access` / `prompt=consent` augmentation on the\nauthorize request; statically-registered and CIMD clients that want that augmentation\nmust set `clientMetadata.grant_types` explicitly. Non-interactive providers (no\n`redirectUrl`) get no `grant_types` default. Direct `registerClient()` callers (⚠\n`@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) wanting the same\ndefaults pass `resolveClientMetadata(provider)` as `clientMetadata`. DCR\nrejection now throws `RegistrationRejectedError` (carrying `status`, `body`,\n`submittedMetadata`).\n\n#### Token endpoint must use TLS (SEP-2207)\n\n`exchangeAuthorization()`, `refreshAuthorization()`, `fetchToken()`, and the Cross-App\nAccess helpers throw `InsecureTokenEndpointError` when the token endpoint is not\n`https:` (loopback `localhost` / `*.localhost` / `127.0.0.1` / `::1` exempt). `auth()` surfaces this on\nevery path including refresh — switch any plain-`http:` AS on a non-loopback host to\nTLS; there is no opt-out. Storage confidentiality of `refresh_token` remains your\n`saveTokens()` implementation's responsibility.\n\n#### Scope step-up on `403 insufficient_scope` (SEP-2350)\n\n`StreamableHTTPClientTransport` accepts `onInsufficientScope: 'reauthorize' | 'throw'`\n(default `'reauthorize'`). On `'reauthorize'` the transport re-authorizes with the\n**union** of the previously-requested and challenged scope (`computeScopeUnion`); when\nthat union strictly exceeds the current token's granted scope (`isStrictScopeSuperset`),\nthe SDK bypasses the refresh-token branch and forces a fresh authorization request. On\n`'throw'` the transport raises `InsufficientScopeError` and does not re-authorize — set\nthis for `client_credentials` / m2m clients where re-authorization can't widen scope, or\nto gate the consent prompt behind UX. Step-up retries are hard-capped per send\n(`maxStepUpRetries`, default 1). With a non-OAuth [`AuthProvider`](#authprovider--non-oauth-bearer-auth-and-the-widened-authprovider-option),\na `403 insufficient_scope` now throws `InsufficientScopeError` instead of the previous\n`SdkHttpError(ClientHttpNotImplemented)`. The GET listen-stream open path applies the\nsame handling as the POST send path.\n\n#### Credentials bound to the issuing authorization server (SEP-2352)\n\n`auth()` stamps an `issuer` field onto every value it passes to `saveTokens()` /\n`saveClientInformation()` and threads `{ issuer }` as the `ctx` argument to those\nmethods plus `tokens()` / `clientInformation()`. On read, a stored value whose `issuer`\nnames a different AS is treated as `undefined` and the flow re-registers / re-authorizes\n(or throws `AuthorizationServerMismatchError` when the provider has no `saveClientInformation()`).\n**Round-trip the stored object verbatim and you're protected** — single-slot storage\nworks. Dropping the stamp is easy to miss: a `saveTokens()` implementation that\nrebuilds the object field-by-field and drops `issuer` leaves the value unstamped —\nreads still succeed and refresh keeps working, the per-AS issuer check simply does not\napply to that credential, and every read logs an `[mcp-sdk]` warning (`auth()`\nre-stamps on first use where the provider can persist it). If you see that warning\nrepeating after upgrading, check this first. To hold credentials for several authorization servers at once, key your storage\non `ctx.issuer` (treat **`ctx === undefined` as \"return the most-recently-saved token\nset\"** — the transport's per-request `Authorization: Bearer` read calls `tokens()` with\nno `ctx`). `OAuthTokensSchema` / `OAuthClientInformationSchema` keep the optional `issuer`, so\nreading storage back through them is fine; the `StoredOAuthTokens` / `StoredOAuthClientInformation`\naliases name the stored shape.\n\n`OAuthClientProvider.saveAuthorizationServerUrl()` / `authorizationServerUrl()` are\n`@deprecated` (still written for back-compat, never read by the SDK). The bundled\n`ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivateKeyJwtProvider`, and\n`CrossAppAccessProvider` gain `expectedIssuer?: string` (omitting it is deprecated) and no\nlonger define `saveClientInformation()`. Implement `discoveryState()` / `saveDiscoveryState()` so the\ncallback leg can verify it is exchanging the code at the same AS the redirect targeted;\nwithout it the SDK `console.warn`s once per callback (`discoveryState` must persist with\nthe same durability as `codeVerifier`). Both methods are optional on\n`OAuthClientProvider` and may be sync or async; `OAuthDiscoveryState` (exported from\n`@modelcontextprotocol/client`) extends `OAuthServerInfo` with the optional\n`resourceMetadataUrl` the protected-resource metadata was found at:\n\n```typescript\nimport type { OAuthDiscoveryState } from '@modelcontextprotocol/client';\n\n// On OAuthClientProvider:\nsaveDiscoveryState?(state: OAuthDiscoveryState): void | Promise<void>;\ndiscoveryState?(): OAuthDiscoveryState | undefined | Promise<OAuthDiscoveryState | undefined>;\n```\n\n#### Conformance obligations for `OAuthClientProvider` implementers\n\nThe SDK enforces every authorization MUST that lands in SDK code. The following live in\n**your** implementation and the SDK structurally cannot enforce them:\n\n- **Round-trip the `issuer` stamp** on persisted credentials (SEP-2352). Persist the\n  value verbatim from `saveTokens` / `saveClientInformation` and return it verbatim.\n- **Pass `expectedIssuer`** when constructing static-credential providers (SEP-2352).\n- **Keep refresh tokens confidential in storage** (SEP-2207) — OS keychain or\n  encrypted-at-rest store, never `localStorage` / plain files / logs.\n- **Extract `iss` from the callback URL** and pass it to `finishAuth` (SEP-2468); when\n  `IssuerMismatchError` is thrown, do not render the callback's `error*` values.\n- **Set `application_type` correctly** when overriding the heuristic (SEP-837).\n- **Track cross-request step-up failures yourself** (SEP-2350) — `maxStepUpRetries` is\n  per request; per-session backoff is host state.\n- **Persist discovery state**: implement `discoveryState()` / `saveDiscoveryState()` so\n  the authorization-server metadata your tokens were issued against survives restarts.\n- **Choose the insufficient-scope behavior**: keep the default\n  `onInsufficientScope: 'reauthorize'`, or handle `InsufficientScopeError` yourself.\n- **Resource-server operators: do not advertise `offline_access`** in `WWW-Authenticate`\n  `scope` or PRM `scopes_supported` (SEP-2207).\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md":"### Behavioral changes\n\nThese are runtime-behavior changes that may affect tests and assertions; no source\nrewrite required unless noted.\n\n#### Error-shape changes (every era)\n\n- **Unchanged, for re-baselining relief:** timeout rejections still carry\n  `data.timeout` / `data.maxTotalTimeout` exactly as v1 `McpError` did — v1 assertions\n  on those survive verbatim. The cancelled-on-timeout signal is unchanged on legacy-era\n  connections and on stdio/in-memory at any era; on 2026-era Streamable HTTP the cancel\n  signal is the per-request stream close instead of a `notifications/cancelled` POST\n  (see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The one exemption is the\n  `initialize` handshake: an aborted or timed-out `connect()` still rejects locally, but\n  no `notifications/cancelled` goes on the wire — the spec forbids cancelling\n  `initialize`, and v1 sent one anyway. v1 tests asserting that notification need\n  re-baselining.\n- **Also unchanged: SSE reconnection exhaustion.** `StreamableHTTPClientTransport`'s\n  standalone GET-stream reconnection behavior and its exhaustion signal carry over from\n  v1: when retries run out, the transport emits `onerror` with a plain `Error` whose\n  message is `Maximum reconnection attempts (N) exceeded.` — there is no typed error\n  class for this condition, so monitors that match the message text keep working.\n- **Also unchanged: elicitation response validation.** `elicitInput`'s local validation\n  of elicitation responses against `requestedSchema`, the resulting `-32602` error\n  message wording (`Elicitation response content does not match requested schema: …`),\n  and the `McpServer` / `Client` `jsonSchemaValidator` option carry over from v1 —\n  tests pinning the local-validation message and custom validator wiring need no\n  re-baselining.\n- **Unknown / disabled tool calls now reject** with `ProtocolError(-32602 InvalidParams)`\n  instead of resolving `CallToolResult{isError: true}`. v1 callers that checked\n  `result.isError` for an unknown tool will get an unhandled rejection — catch the\n  rejected promise instead.\n- **The `MCP error <code>: ` message prefix is gone.** v1 prefixed relayed JSON-RPC\n  error messages (`MCP error -32602: …`); v2's `ProtocolError.message` carries the\n  peer's message verbatim. Tests and log scrapers that matched the prefix or the numeric\n  code in rendered text should match `error.code` instead.\n- **In-flight request handlers are aborted on transport close** — `ctx.mcpReq.signal`\n  fires (v1 let them run to completion). `InMemoryTransport.close()` no longer\n  double-fires `onclose` on the initiating side.\n- **`Protocol.request()` with an already-aborted signal** rejects with\n  `SdkError(SdkErrorCode.RequestTimeout, reason)` instead of throwing the raw\n  `signal.reason`, matching the in-flight-abort path.\n- **OAuth discovery (`discoverOAuthProtectedResourceMetadata` / `discoverOAuthMetadata`,\n  transitively `auth()`) throws on fetch `TypeError`** (DNS failure, `ECONNREFUSED`,\n  invalid URL) in Node and Cloudflare Workers instead of swallowing it as a CORS miss\n  → `undefined`. The CORS-swallow remains browser-only.\n\n#### Client connection & dispatch\n\n- **`connect()` skips the `initialize` handshake when the transport already exposes a\n  `sessionId`** — it assumes it is reconnecting to an existing session (unchanged from\n  v1.x, where the same guard has existed since 1.10.0; recorded here because the\n  far-away symptom keeps surprising migrators). A custom or test transport that sets `sessionId` at construction\n  silently skips initialization: `getServerCapabilities()` stays `undefined` and the\n  list verbs return empty results. Expose `sessionId` only after the first request has\n  been sent.\n- **The typed verbs dispatch after async pre-work.** `Protocol.request()` itself still\n  hands the frame to the transport before its first `await` (v1-compatible). The typed\n  verbs on top of it — `callTool()` and the cacheable list verbs — perform async work\n  first (header-mirroring scan, response-cache freshness, output-validator resolution),\n  so an abort fired in the same tick can land before the frame is ever sent: the call\n  rejects with `SdkError(RequestTimeout, reason)` and **no `notifications/cancelled` is\n  emitted** (nothing was in flight). v1 sent the frame synchronously from these verbs.\n  Once the frame is on the wire, aborting still sends `notifications/cancelled` before\n  rejecting.\n- **Protocol-version pinning is a first-class option.**\n  `ProtocolOptions.supportedProtocolVersions` pins the legacy `initialize` handshake:\n  the **first** pre-2026 entry in the list is offered (list order is preference order),\n  a counter-offer is accepted only if it is one of the list's pre-2026 entries, and a\n  list with no pre-2026 entry makes the handshake throw. Under\n  `versionNegotiation: 'auto'` the modern probe candidates are the list's modern\n  entries when it has any (otherwise the SDK's default modern set); a `{ pin }` is\n  honored as given and is not checked against the list (see\n  [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#client-side-versionnegotiation)).\n  v1 had no public equivalent (`SUPPORTED_PROTOCOL_VERSIONS` was a fixed constant) —\n  replace any workaround that patched the offered version with this option.\n- **Also unchanged: HTTP 405 tolerances.** A `405` answering the standalone GET stream\n  open is benign (the client proceeds without the stream), and a `405` answering the\n  session DELETE resolves `terminateSession()` normally — stateless-topology servers\n  that decline both verbs keep working without changes, as in v1.\n\n#### stdio transport\n\n- A configurable `maxBufferSize` (default **10 MB**) caps the stdio read buffer. A\n  single message that would push the buffer past the limit emits `onerror` and\n  **closes the connection** (v1 buffered unbounded). Configure via\n  `new StdioClientTransport({ ..., maxBufferSize })` /\n  `new StdioServerTransport(stdin, stdout, { maxBufferSize })`.\n- `ReadBuffer.readMessage()` now **silently skips non-JSON stdout lines** instead of\n  throwing `SyntaxError` → `onerror`. Hot-reload tools (tsx, nodemon) that write debug\n  output to stdout no longer break the transport. Lines that parse as JSON but fail\n  JSON-RPC schema validation still throw.\n- `StdioClientTransport` always sets `windowsHide: true` when spawning the server\n  process on Windows (previously Electron-only). Prevents stray console windows in\n  non-Electron Windows hosts.\n- Outbound write failures — e.g. the host closing the stdout pipe while a send is\n  pending — now reject the pending `send()` and close the transport through\n  `onerror`/`onclose` instead of surfacing an unhandled stream error; lifecycle\n  tests that pinned a crash-class exit observe a clean shutdown instead.\n\n#### Client list methods\n\n- `listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` return\n  **empty results** when the server didn't advertise the corresponding capability,\n  instead of sending the request. Set `enforceStrictCapabilities: true` in `ClientOptions`\n  to restore the v1 throw.\n- Called **without a `cursor`**, the same methods now **auto-aggregate every page** and\n  return an aggregate with no `nextCursor`. Passing `{ cursor }` still fetches one page. Manual\n  pagination loops keep working (the first iteration returns everything); replace them\n  with the bare no-arg call. The walk is capped at `ClientOptions.listMaxPages` (default\n  64); overrun throws `SdkError(ListPaginationExceeded)`. There is no way to fetch only\n  the **first** page through the typed verbs — for page-level observation\n  (pagination tooling, per-page stats) drop to\n  `client.request({ method: 'tools/list', params })`, which never aggregates.\n- Output-schema validator compilation is now **lazy** — validators compile on the first\n  `callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()`.\n  In v1, `listTools()` threw on an uncompilable `outputSchema`; now `listTools()`\n  succeeds and the compile failure surfaces when `callTool()` is invoked on the affected\n  tool, as `ProtocolError(InvalidParams, \"Tool 'X' has an invalid outputSchema: …\")`,\n  before the request is sent. Validation is never silently skipped.\n- On a 2026-07-28 connection the cacheable verbs honour the server-stamped `ttlMs` /\n  `cacheScope` (SEP-2549) and may return a still-fresh cached entry without a round\n  trip. Per-call override: `{ cacheMode: 'refresh' | 'bypass' }`. New `ClientOptions`:\n  `cachePartition`, `defaultCacheTtlMs`. `ResponseCacheStore` gained `delete(key)`;\n  `InMemoryResponseCacheStore` is now bounded (`{ maxEntries }`, default 512).\n  `CacheEntry.value` (and the `set()` entry value) is the JSON-serialized result\n  document (`string`, not `unknown`): persist and return it verbatim, `JSON.parse`\n  it to inspect.\n\n#### Server (Streamable HTTP transport)\n\n- Resumability behavior (SSE priming events, `closeSSE` / `closeStandaloneSSE`\n  callbacks) is only enabled for protocol versions in the transport's supported-versions\n  list that are `>= 2025-11-25`. Unknown future version strings in an `initialize`\n  request body no longer enable it.\n- Session-ID mismatch still responds `404` with JSON-RPC `-32001` (`Session not found`),\n  unchanged from v1. This `-32001` is an SDK convention, not spec-assigned; client code\n  should key off the HTTP `404` status, not `-32001`.\n\n#### Server (deprecated accessors and app-factory Origin validation)\n\n- `Server.getClientCapabilities()`, `getClientVersion()`, `getNegotiatedProtocolVersion()`\n  are `@deprecated` but functional. On 2026-07-28 requests, prefer `ctx.mcpReq.envelope`.\n- `createMcpExpressApp()` / `createMcpHonoApp()` / `createMcpFastifyApp()` with a\n  localhost-class `host` now also validate the `Origin` header by default. Browser-served\n  clients on a non-localhost origin need `allowedOrigins: [...]` (replaces the default\n  localhost allowlist; validation cannot be disabled for localhost binds). Requests\n  without an `Origin` header are unaffected; a present `Origin` that cannot be parsed\n  — including the opaque **`Origin: null`** sent by sandboxed iframes, `file://` pages,\n  and cross-origin redirects — is **rejected with 403** and cannot be allowlisted via\n  `allowedOrigins`. Framework-agnostic helpers\n  (`validateOriginHeader`, `localhostAllowedOrigins`, `originValidationResponse`) are in\n  `@modelcontextprotocol/server`; `@modelcontextprotocol/node` ships\n  `hostHeaderValidation` / `originValidation` request guards for plain `node:http`.\n\n#### Server (McpServer / Streamable HTTP behavior)\n\n- **Eager capability-handler install.** `McpServer` now installs list/read/call handlers\n  for every primitive capability declared in `ServerOptions.capabilities`, even with\n  zero registrations. `new McpServer(info, { capabilities: { tools: {} } })` with no\n  registered tools answers `tools/list` with `{ tools: [] }` instead of `-32601 Method\nnot found`. Low-level `Server` users remain responsible for registering handlers for\n  declared capabilities — with one exception: declaring the `logging` capability (in\n  the constructor's capabilities or via pre-connect `registerCapabilities()`) installs\n  the `logging/setLevel` handler on the low-level `Server` too, so `logging/setLevel`\n  requests that answered `-32601` in v1 now resolve. Eager install also rewrites the **advertised** capability\n  objects: a declared `tools: {}` / `resources: {}` / `prompts: {}` is advertised with\n  `listChanged: true` at construction, so capability pins and initialize-result golden\n  tests need re-baselining. To advertise without the default, set\n  `listChanged: false` explicitly; capabilities declared on the low-level `Server` are\n  advertised verbatim.\n- **`WebStandardStreamableHTTPServerTransport` store-first `eventStore` semantics.**\n  Request-related events emitted after `closeSSE()` — and the final response when no\n  per-request stream is connected — are now persisted to the configured `eventStore` for\n  replay (v1 dropped them / threw `\"No connection established\"`). Without an\n  `eventStore`, the same condition surfaces via `onerror` and the request id is retired.\n  `NodeStreamableHTTPServerTransport` is a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport`, so this — like every behavioral note on\n  the web-standard transport — applies to the Node transport too.\n- **`registerResource` reserves the `cacheHint` config key.** It is validated\n  (`RangeError` on invalid values) and stripped from the resource's list metadata; v1\n  passed it through verbatim as ordinary metadata. Untyped callers that previously\n  smuggled a `cacheHint` key through resource metadata should rename it.\n\n#### `ctx.mcpReq.log()` is request-related on every era\n\n`ctx.mcpReq.log()` now emits its `notifications/message` request-related (it rides the\nin-flight exchange like progress) on every era. On a 2025-era sessionful Streamable HTTP\ntransport this moves handler-emitted logs from the standalone GET stream onto the\nper-request POST response stream — a spec-conformance correction. The session-scoped\n`logging/setLevel` filter applies as before on 2025-era connections. (On 2026-07-28\nrequests, the per-request `_meta.logLevel` envelope key is the filter — see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#serving-the-2026-07-28-revision).)\n\n#### Wire tightening (every era)\n\n- **`CallToolResult.content` keeps the v1 parse tolerance on the legacy era.** An\n  inbound result without `content` defaults to `[]` (deployed servers omit it\n  alongside `structuredContent`); 2026-07-28 connections stay strict. Authoring is\n  unchanged and era-independent: the TypeScript surface requires `content` on handler\n  results, and a content-less handler result is normalized to `content: []` before it\n  reaches the wire. One sharpening remains: a content-less body carrying another\n  result family's vocabulary (a task handle or an `input_required` round) is still\n  rejected loudly — tolerance never turns a different result kind into a silent empty\n  success. A body whose only foreign key was `resultType` strips to an empty object\n  and defaults, exactly as v1 parsed a payload-free body.\n- **`ElicitResult.content` values are typed and validated as\n  `string | number | boolean | string[]`.** v1's TypeScript surface accepted\n  `Record<string, unknown>` content values; an elicitation handler returning arbitrary\n  objects now fails to compile (and fails schema validation) — narrow to the primitives\n  the elicitation spec allows.\n- **Custom (3-arg) handlers receive `_meta`.** `setRequestHandler(method, {params}, handler)`\n  used to delete `params._meta` before validation; it now passes `_meta` through (minus\n  the reserved `io.modelcontextprotocol/*` envelope keys). If your params schema is\n  strict, add an optional `_meta` member.\n- **`specTypeSchemas` validate the neutral model.** Result entries no longer accept\n  `resultType`; the validators for the 2025-only task message types and\n  `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed accordingly).\n- **Sampling `hasTools` discriminant** now keys on `tools || toolChoice` (previously\n  `tools` only) when selecting the with-tools `CreateMessageResult` variant, on every\n  era.\n- **Inbound frames that fail message-shape validation are not answered.** v2 routes\n  every inbound frame through typed message guards; a frame that matches no JSON-RPC\n  shape (e.g. a hand-built ping with an explicitly-`undefined` `id`, or non-object\n  `params`) is dropped and surfaces only via `onerror` (`Unknown message type: …`) — no\n  response is sent. v1-era test fences that await a reply to a hand-written raw frame\n  hang instead of resolving; send through the typed surface (`client.ping()`,\n  `client.request()`) instead.\n\n#### Experimental tasks interception removed\n\nThe 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`).\nNo mechanical migration; remove usages. Gone: `ProtocolOptions.tasks`,\n`protocol.taskManager`, `RequestOptions.task` / `relatedTask`, `BaseContext.task`,\n`assertTaskCapability` / `assertTaskHandlerCapability`, `*.experimental.tasks.*`\naccessors and `Experimental{Client,Server,McpServer}Tasks`, `requestStream` /\n`callToolStream` / `createMessageStream` / `elicitInputStream` and the `ResponseMessage`\ntypes they yielded, `registerToolTask`, `ToolTaskHandler`, `TaskRequestHandler`,\n`CreateTaskRequestHandler`, `TaskMessageQueue`, `InMemoryTaskMessageQueue`,\n`BaseQueuedMessage` / `Queued*`, `CreateTaskServerContext`, `TaskServerContext`,\n`TaskToolExecution`, `TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`,\nand the `new McpServer(info, { taskStore, taskMessageQueue })` constructor option keys\n(the codemod emits an action-required diagnostic at each — remove the option).\n\nThe task **wire types** remain importable as `@deprecated` vocabulary for 2025-11-25\ninterop — see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#tasks-deprecated-wire-vocabulary).\n\n#### Specification clarifications adopted (no SDK behavior change)\n\nThe 2026-07-28 specification revision includes a number of documentation-only\nclarifications recorded here so an audit of the revision's changelog against this guide\nis complete; nothing in this list requires code changes: per-operation timeout guidance\nremoval (`RequestOptions.timeout` / `DEFAULT_REQUEST_TIMEOUT_MSEC` unchanged); stdio\nshutdown wording; transports-as-bindings reframe; `resources/read` wording (the\n`file://` path-sanitization MUST is server-author guidance — your handler must reject\ntraversal / symlink escapes itself); `PromptMessage` resource links (already in\n`ContentBlock`); completion `ref/resource` URI templates; pagination empty-string\ncursors (already passed through verbatim); sampling host-requirement docs; elicitation\nstatefulness wording; cosmetic schema/JSDoc sweeps.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/enhancements.md":"## Enhancements\n\n### Automatic JSON Schema validator selection by runtime\n\nThe SDK auto-selects the validator: Node.js → AJV; Cloudflare Workers (workerd) →\n`@cfworker/json-schema`. Cloudflare Workers users can remove explicit\n`jsonSchemaValidator` configuration. You don't need to install `ajv`, `ajv-formats`, or\n`@cfworker/json-schema` for the default path. To customize the built-in backend, import\nthe named class from the explicit subpath\n(`@modelcontextprotocol/{client,server}/validators/ajv` or `…/cf-worker`) — importing\nfrom a subpath means the corresponding peer dep must be in your `package.json`.\n\n### `Client.connect(transport, { prior })` — connect from a cached era verdict\n\nProbe once, persist `client.getDiscoverResult()` (`JSON.stringify`), and feed it to\nevery worker as `client.connect(transport, { prior: { kind: 'modern', discover } })`.\nNew exported types\n`ConnectOptions` (extends `RequestOptions` with `prior?: PriorDiscovery`)\nand `PriorDiscovery` — a cached era verdict: the modern arm wraps a `DiscoverResult`\n(zero round trips), the legacy arm (`{ kind: 'legacy' }`) skips the probe and runs the\nplain `initialize` handshake for servers known to be pre-2026. Freshness is the\nsupplying host's responsibility — date cached legacy verdicts in your own storage and\nstop supplying them past your policy horizon (a stale one succeeds silently against an\nupgraded server).\n\n### Serving the 2026-07-28 revision\n\n`createMcpHandler`, `serveStdio`, `versionNegotiation`, multi-round-trip requests\n(`requestState`), client cancellation via stream-close, `subscriptions/listen`,\n`Mcp-Param-*` headers, and per-era wire codecs are covered in\n**[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)** — they are net-new in v2, not v1→v2\nbreaks.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/errors.md":"### Errors\n\nThe SDK now distinguishes three error kinds:\n\n1. **`ProtocolError`** (renamed from `McpError`) — protocol errors that cross the wire\n   as JSON-RPC error responses. Uses `ProtocolErrorCode` (renamed from `ErrorCode`).\n2. **`SdkError`** — local SDK errors that never cross the wire. Uses `SdkErrorCode`.\n3. **`SdkHttpError`** (extends `SdkError`) — HTTP transport errors with typed `.status`\n   and `.statusText`.\n\nThese classes (and `OAuthError`, the client's `SseError`, `UnauthorizedError`, and the\nOAuth-client-flow error family) brand-match under `instanceof`, so checks work across\nseparately bundled copies of the SDK — e.g. a process using both\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server`. Each branded\nhierarchy also exposes the same check as an explicit static guard\n(`SdkError.isInstance(err)`, `ProtocolError.isInstance(err)`, …) that narrows in\nTypeScript — use whichever style your codebase prefers; both read the same brand.\nFine print (applies equally to `instanceof` and `isInstance`):\n\n- **Version skew** — matching needs _both_ copies at a brand-aware release; against an\n  older copy, behavior degrades to plain prototype `instanceof` (false across bundles).\n  During mixed-version rollouts, recognize errors without class identity: match\n  `error.name` plus the class's discriminant field (`code`, `status`), or reconstruct\n  typed protocol errors with `ProtocolError.fromError(code, message, data)`.\n- **Worker boundaries** — `structuredClone`/`postMessage` drop the (symbol-keyed) brand,\n  so a rehydrated error no longer brand-matches; recognize forwarded errors by\n  `code`/`data` instead.\n- **Brands assert identity, not shape** — a matched instance from another SDK version\n  may lack newer fields; read fields defensively.\n- **Re-bundling with property mangling** (`mangle.props` and similar) breaks the brand\n  statics; default esbuild/webpack/terser settings are safe.\n\nThe codemod renames `McpError` → `ProtocolError`, `ErrorCode` → `ProtocolErrorCode`\n(routing `RequestTimeout` / `ConnectionClosed` to `SdkErrorCode`), and\n`StreamableHTTPError` → `SdkHttpError`. After the codemod runs, your `instanceof`\nchecks already name the v2 classes — what's left is choosing which `SdkErrorCode` /\nclass to match per scenario:\n\n| Scenario                                         | v1                                        | v2                                                                 |\n| ------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------ |\n| Request timeout                                  | `McpError` + `ErrorCode.RequestTimeout`   | `SdkError` + `SdkErrorCode.RequestTimeout`                         |\n| Connection closed                                | `McpError` + `ErrorCode.ConnectionClosed` | `SdkError` + `SdkErrorCode.ConnectionClosed`                       |\n| Capability not supported                         | `new Error(...)`                          | `SdkError` + `SdkErrorCode.CapabilityNotSupported`                 |\n| Not connected                                    | `new Error('Not connected')`              | `SdkError` + `SdkErrorCode.NotConnected`                           |\n| Response result fails schema                     | raw `ZodError`                            | `SdkError` + `SdkErrorCode.InvalidResult`                          |\n| Invalid params (server response)                 | `McpError` + `ErrorCode.InvalidParams`    | `ProtocolError` + `ProtocolErrorCode.InvalidParams`                |\n| HTTP transport error                             | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttp*`                        |\n| Failed to open SSE stream                        | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToOpenStream`       |\n| 401 after re-auth (circuit break)                | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| `SSEClientTransport.send()` 401 after re-auth    | `UnauthorizedError`                       | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| 403 `insufficient_scope` after step-up retry cap | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpForbidden`                |\n| Unexpected content type                          | `StreamableHTTPError`                     | `SdkError` + `SdkErrorCode.ClientHttpUnexpectedContent`            |\n| Session termination failed                       | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToTerminateSession` |\n\n```typescript\n// v1\nif (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }\nif (error instanceof StreamableHTTPError) { console.log('HTTP status:', error.code); }\n\n// v2\nimport { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }\nif (error instanceof SdkHttpError) {\n    console.log('HTTP status:', error.status, error.statusText);\n    switch (error.code) {\n        case SdkErrorCode.ClientHttpAuthentication:\n        case SdkErrorCode.ClientHttpForbidden:\n        case SdkErrorCode.ClientHttpFailedToOpenStream:\n        case SdkErrorCode.ClientHttpNotImplemented:\n            break;\n    }\n}\n```\n\n`StreamableHTTPError` is removed.\n\n**Status read off `.code` by duck-typing.** Code that classified HTTP failures by the\nstatus without an `instanceof` — `if ('code' in e && e.code === 403)` — silently stops\nmatching: on `SdkHttpError` the HTTP status moved to `.status` (its `.code` is a\n`SdkErrorCode` string). The codemod renames `instanceof StreamableHTTPError`, but a\nstatus read that never named the class is invisible to it. Watch the inconsistency:\n`SseError` still carries its HTTP status on numeric `.code`, so one duck-typed\n`.code === 401` that caught both transports in v1 now catches only SSE.\n\n```typescript\n// v1 — one duck-typed check caught both Streamable HTTP and SSE\nif ('code' in e && (e.code === 401 || e.code === 403)) reauth();\n// v2 — match each explicitly\nif (e instanceof SdkHttpError && (e.status === 401 || e.status === 403)) reauth(); // Streamable HTTP\nif (e instanceof SseError && (e.code === 401 || e.code === 403)) reauth(); // SSE still uses .code\n```\n\nSilent at runtime (no compile error) — grep for `.code ===` status comparisons.\n\n**Classification keyed on the error class name.** The same import-free classifiers\noften match by name instead of code: telemetry and allowlists keyed on `error.name` or\n`error.constructor.name` against `'McpError'` / `'StreamableHTTPError'` silently stop\nmatching — the v2 classes are named `ProtocolError`, `SdkError`, and `SdkHttpError`,\nand all three assign `.name` accordingly. One v1 asymmetry disappears along the way:\nv1's `StreamableHTTPError` never assigned `.name` (instances reported `'Error'`), so\n`.name`-keyed matchers saw only `'McpError'`; v2's `SdkHttpError` reports\n`'SdkHttpError'`, and assertions pinning `.name === 'Error'` on transport errors need\nre-baselining. Add the v2 names to your match lists; during a\n[staged migration](packaging-runtime.md#migrating-in-stages-large-codebases) keep the v1 names alongside\nfor as long as the v1 package remains installed.\n\n**Status read out of the message text.** Per transport: the Streamable HTTP message\ntext never carried the status (v1 put it on `.code`, v2 puts it on `.status` — read\n`error.status`), and v2's SSE transport still embeds it exactly as v1 did\n(`Error POSTing to endpoint (HTTP 404): …`). The silent break is **switching\ntransports while keeping a message regex**: a status pattern written against SSE\nmatches nothing on Streamable HTTP. Read `error.status` instead of parsing text.\n\n**Raw numeric code comparisons.** The codemod rewrites `ErrorCode.X` symbol references,\nbut a check against the raw JSON-RPC number — `(e as { code?: unknown }).code === -32000`\n— is invisible to it and silently never matches in v2, because the two SDK-local codes\nit usually targeted are now **string** `SdkErrorCode` values:\n\n| v1 numeric                  | v2                                           |\n| --------------------------- | -------------------------------------------- |\n| `-32000` (ConnectionClosed) | `SdkError` + `SdkErrorCode.ConnectionClosed` |\n| `-32001` (RequestTimeout)   | `SdkError` + `SdkErrorCode.RequestTimeout`   |\n\n- Requests that require a session but omit the `Mcp-Session-Id` header still\n  respond `400` with JSON-RPC `-32000` (`Bad Request: Mcp-Session-Id header is\nrequired`), unchanged from v1 — as with `-32001`, the code is an SDK\n  convention; key off the HTTP status.\n\nReplace the literal with the named code. Loud (`TS2367`) when the compared value is\ntyped `SdkErrorCode`; silent when the left side is `unknown` or a cast — grep for\n`=== -32000` / `=== -32001`.\n\n**Dual-role processes: `instanceof` does not cross the packages.**\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` each bundle their own\ncopy of these error classes, so in a process that uses both — a gateway, a host, an\nin-process test — an error constructed by one package fails `instanceof` against the\nclass imported from the other, silently. When an error may originate from the other\npackage, match on stable fields instead of class identity: `error.code` values\n(`SdkErrorCode` strings for SDK errors, numeric JSON-RPC codes for protocol errors,\n`OAuthErrorCode` strings for OAuth errors) plus presence checks like `'status' in e`,\nor reconstruct typed protocol errors with `ProtocolError.fromError(code, message, data)`\n— it exists precisely because `instanceof` does not survive bundle boundaries.\n\n**Constructing the error (test stubs, custom transports).** v1\n`new StreamableHTTPError(code, message)` becomes\n`new SdkHttpError(code, message, data)`: the first argument is now a `SdkErrorCode`\nstring (pick the branch from the scenario table above) and the HTTP status moves into\nthe third argument — `new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented,\n'Not Found', { status: 404, statusText: 'Not Found' })`. v1's implicit\n`Streamable HTTP error: ` message prefix is gone; pass the full message you want.\n\n#### `SdkErrorCode` enum (complete)\n\n| Code                                  | When thrown                                                                |\n| ------------------------------------- | -------------------------------------------------------------------------- |\n| `NotConnected`                        | Transport is not connected                                                 |\n| `AlreadyConnected`                    | Transport is already connected                                             |\n| `NotInitialized`                      | Protocol is not initialized                                                |\n| `CapabilityNotSupported`              | Required capability is not supported                                       |\n| `RequestTimeout`                      | Request timed out waiting for response                                     |\n| `ConnectionClosed`                    | Connection was closed                                                      |\n| `SendFailed`                          | Failed to send message                                                     |\n| `InvalidResult`                       | Response result failed local schema validation                             |\n| `UnsupportedResultType`               | A 2026-era response carried an unrecognized `resultType`                   |\n| `InputRequiredRoundsExceeded`         | Multi-round-trip auto-fulfilment hit `maxRounds`                           |\n| `ListPaginationExceeded`              | No-arg `list*()` aggregate walk hit `listMaxPages`                         |\n| `MethodNotSupportedByProtocolVersion` | Outbound spec method does not exist on the negotiated protocol version     |\n| `EraNegotiationFailed`                | `connect()` could not negotiate a protocol era (probe failed / no overlap) |\n| `ClientHttpNotImplemented`            | HTTP POST request failed                                                   |\n| `ClientHttpAuthentication`            | Server returned 401 after re-authentication                                |\n| `ClientHttpForbidden`                 | Server returned 403 `insufficient_scope` after step-up retry cap           |\n| `ClientHttpUnexpectedContent`         | Unexpected content type in HTTP response                                   |\n| `ClientHttpFailedToOpenStream`        | Failed to open SSE stream                                                  |\n| `ClientHttpFailedToTerminateSession`  | Failed to terminate session                                                |\n\n#### Typed `ProtocolError` subclasses\n\n`ResourceNotFoundError` (carries `.uri`) and `MissingRequiredClientCapabilityError`\n(carries `data.requiredCapabilities`) are new typed `ProtocolError` subclasses.\n`resources/read` for an unknown URI now answers `-32602` on every protocol revision\n(v1.x already emitted `-32602`; an interim `-32002` from earlier v2 alphas is mapped at\nthe encode seam — `2.0.0-alpha.3` and earlier predate the mapping and still emit\n`-32002` on the wire, so accept both if peers may run those alphas; `2.0.0-alpha.4`\nand later emit `-32602`). The encode-seam mapping applies to **your own throws too**: a handler\nthat deliberately throws `ProtocolError(ProtocolErrorCode.ResourceNotFound, …)` reaches\npeers as `-32602` — a server can no longer emit `-32002` on the wire.\n`ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as\nreceive-tolerated vocabulary — accept both `-32602` and `-32002` from peers.\n`ProtocolError.fromError(code, message, data)` reconstructs the typed subclass from\ncode + data alone — the version-agnostic path: it also works on plain wire shapes and\nagainst SDK copies that predate brand-matched `instanceof`.\nThe default message text changed alongside: v1's unknown-resource error read\n`Resource <uri> not found`; v2's `ResourceNotFoundError` default is\n`Resource not found: <uri>` (the code is unchanged). Tests pinning the exact string\nneed re-baselining — prefer matching `error.code` plus a URI substring (or the typed\n`error.uri`).\n\nCustom **non-spec** codes pass through untouched: a handler that throws a\n`ProtocolError` with a custom code (e.g. `-1`) and `data` reaches the peer as a\nJSON-RPC error with that code and `data` unchanged — the encode seam rewrites only the\nlegacy `-32002` code; `data` is sent verbatim for every thrown error (the typed\nsubclasses shape their `data` at construction, not at encode time). Construct via\n`ProtocolError.fromError(code, message, data)`.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/http-headers.md":"### HTTP & headers\n\nHeader **reads** use the Web Standard `Headers` object (`IsomorphicHeaders` is\nremoved): `ctx.http?.req` is a standard Web `Request`, so\n`ctx.http?.req?.headers` takes `.get()` instead of bracket access.\n\n```typescript\n// v1\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = extra.requestInfo?.headers['mcp-session-id'];\n\n// v2 — requestInit is unchanged; only the header *read* changes\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = ctx.http?.req?.headers.get('mcp-session-id');\nconst debug = new URL(ctx.http!.req!.url).searchParams.get('debug');\n```\n\nOn the **write** side, `requestInit` on `StreamableHTTPClientTransport` /\n`SSEClientTransport` options is a standard fetch `RequestInit`, so `headers` accepts\nany `HeadersInit` — a plain object record (as above), a tuple array, or a `Headers`\ninstance all keep working unchanged; the transports normalize whichever form they\nreceive. Wrapping with `new Headers()` is optional, not required.\n\n`StreamableHTTPClientTransport` now **appends** any custom `requestInit.headers.Accept`\nvalue to the spec-required `application/json, text/event-stream` (v1 let it replace\nthem). The required media types are always present; additional types are kept for\nproxy/gateway routing.\n\nTransport-managed headers now take precedence over same-named entries in\n`requestInit.headers`: `Authorization` when `authProvider` yields a token,\n`mcp-protocol-version`, and (Streamable HTTP) `mcp-session-id`. v1 let the configured\nheader win, so a static `Authorization` placeholder kept overriding the OAuth token even\nafter the provider obtained one. A configured `Authorization` value is still sent while\nthe provider has no token, which is what lets a static API key fall back to OAuth.\n\n`hostHeaderValidation()` and `localhostHostValidation()` moved to\n`@modelcontextprotocol/express`. The `(allowedHostnames: string[])` signature is the\nsame as every released v1.x — only the import path changes. Framework-agnostic helpers\n(`validateHostHeader`, `localhostAllowedHostnames`, `hostHeaderValidationResponse`) are\nin `@modelcontextprotocol/server`.\n\nServer entries validate the request `Content-Type` by its **parsed media type**, not a\nsubstring: every POST whose media type is not `application/json` answers\n`415 Unsupported Media Type`. Previously any value merely containing the substring\npassed (for example `text/plain; a=application/json`), case variants were wrongly\nrejected, and the 2026-07-28 entry did not inspect `Content-Type` at all — so\nhand-rolled clients that omit the header (or send a non-JSON type) must now set\n`Content-Type: application/json`. Parameters (`; charset=utf-8`) and unambiguous\nvalues with malformed parameter sections (`application/json;`) keep working; SDK\nclients always sent the correct header and are unaffected. Custom entries that\ncompose `classifyInboundRequest` / `PerRequestHTTPServerTransport` directly must\napply the same validation themselves — use the exported `isJsonContentType(header)`.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/imports-transports.md":"### Imports & transports\n\nThe codemod rewrites every `@modelcontextprotocol/sdk/...` import path via\n[`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).\nA few transports need a decision the codemod can't make:\n\n- **`StreamableHTTPServerTransport` → which runtime?** The codemod renames it to\n  `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. If you deploy\n  to a web-standard runtime (Cloudflare Workers, Deno, Bun), use\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`\n  instead. **Decision rule:** if your handler receives a Node `IncomingMessage` /\n  `ServerResponse`, use `@modelcontextprotocol/node`; if it receives a web-standard\n  `Request` and returns a `Response`, use `@modelcontextprotocol/server`.\n- **stdio transports moved to a `./stdio` subpath.** Import `StdioClientTransport`,\n  `getDefaultEnvironment`, `DEFAULT_INHERITED_ENV_VARS`, and `StdioServerParameters`\n  from `@modelcontextprotocol/client/stdio`; import `StdioServerTransport` from\n  `@modelcontextprotocol/server/stdio`. The package root barrels do **not** export\n  these (the root entries are runtime-neutral so browser/Workers bundlers can consume\n  them). The stdio utilities `ReadBuffer`, `serializeMessage`, `deserializeMessage`\n  stay in the root barrel.\n- **Zod `*Schema` constants → `@modelcontextprotocol/core`.** A mixed\n  `import { CallToolResult, CallToolResultSchema } from '…/types.js'` is split by the\n  codemod — see [Types & schemas](types-schemas.md#types--schemas).\n\n    ```typescript\n    // v1\n    import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n    // v2\n    import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';\n    ```\n\n- **`SSEServerTransport`** is removed. Migrate to Streamable HTTP. A frozen v1 copy is\n  available from `@modelcontextprotocol/server-legacy/sse` as a temporary bridge.\n- **`WebSocketClientTransport`** is removed (WebSocket is not a spec transport). Use\n  `StreamableHTTPClientTransport` for remote servers or `StdioClientTransport` for\n  local servers; the `Transport` interface is exported if you need a custom\n  implementation.\n- **`InMemoryTransport`** is now exported from `@modelcontextprotocol/client` and\n  `@modelcontextprotocol/server` (both re-export it). The two packages bundle separate\n  copies with private state, so the halves of a linked pair must come from the **same\n  package's** import — pick one package per file (per linked pair) rather than mixing\n  the client's `InMemoryTransport` with the server's:\n\n    ```typescript\n    // v1\n    import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';\n    // v2\n    import { InMemoryTransport } from '@modelcontextprotocol/server'; // or /client\n    ```\n\n- **`EventStore`, `StreamId`, `EventId`** are exported from `@modelcontextprotocol/server`\n  only (v1 re-exported them alongside the transport from `sdk/server/streamableHttp.js`;\n  `@modelcontextprotocol/node` does not).\n- **Client fetch middleware moved to the root barrel.** `createMiddleware`,\n  `applyMiddlewares`, `withLogging`, `withOAuth`, and the `Middleware` type (v1:\n  `sdk/client/middleware.js`) are now exported from `@modelcontextprotocol/client`\n  directly, as is `FetchLike` (v1: `sdk/shared/transport.js`). The call signatures are\n  unchanged from v1 (`Middleware` is still `(next: FetchLike) => FetchLike`) — only the\n  import path changes.\n- **Server auth split.** Resource Server helpers (`requireBearerAuth`,\n  `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`)\n  → `@modelcontextprotocol/express`; the runtime-neutral core (`requireBearerAuth`\n  for web-standard `fetch` hosts, `verifyBearerToken`, `bearerAuthChallengeResponse`,\n  `OAuthTokenVerifier`, and the discovery serving `oauthMetadataResponse` /\n  `buildOAuthProtectedResourceMetadata` / `getOAuthProtectedResourceMetadataUrl`)\n  is also exported from `@modelcontextprotocol/server`. Authorization Server helpers (`mcpAuthRouter`,\n  `OAuthServerProvider`, `ProxyOAuthServerProvider`, `allowedMethods`,\n  `authenticateClient`, `metadataHandler`, `createOAuthMetadata`,\n  `authorizationHandler` / `tokenHandler` / `revocationHandler` /\n  `clientRegistrationHandler`) → `@modelcontextprotocol/server-legacy/auth`\n  (deprecated, frozen v1 copy); migrate AS to a dedicated IdP/OAuth library. `AuthInfo`\n  is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.\n\n    The codemod's [`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)\n    routes every `…/server/auth/**` deep path (including\n    `…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`,\n    `…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to\n    `@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` /\n    `…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The\n    AS→`server-legacy` routing is conservative — re-point RS-only call sites\n    (`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand.\n    Staying on the frozen `server-legacy/auth` copy is a supported interim choice when you\n    deliberately want the v1 middleware behavior. If you re-point at\n    `@modelcontextprotocol/express` by hand, also add that package — plus its `express`\n    peer dependency — to your manifest: the codemod's manifest summary reflects only the\n    imports it wrote, not re-points you make afterwards.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/packaging-runtime.md":"### Packaging & runtime\n\nThe single `@modelcontextprotocol/sdk` package is split:\n\n| v1                              | v2                                                                                                                              |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk`     | `@modelcontextprotocol/client` (client implementation)                                                                          |\n|                                 | `@modelcontextprotocol/server` (server implementation)                                                                          |\n|                                 | `@modelcontextprotocol/core` (public Zod `*Schema` constants)                                                                   |\n|                                 | `@modelcontextprotocol/core-internal` (internal — never import directly)                                                        |\n| Built-in HTTP framework support | `@modelcontextprotocol/node` / `@modelcontextprotocol/express` / `@modelcontextprotocol/hono` / `@modelcontextprotocol/fastify` |\n\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared\ntypes from `@modelcontextprotocol/core-internal`, so import types and error classes from\nwhichever package you already depend on. `@modelcontextprotocol/core-internal` is\n`private: true` and is not published — **do not import from it directly.**\n`@modelcontextprotocol/core` is the public Zod-schema package (raw `*Schema` constants\nonly); see [Zod `*Schema` constants moved to `@modelcontextprotocol/core`](types-schemas.md#zod-schema-constants-moved-to-modelcontextprotocolcore) below.\n\nAfter the codemod runs, review the manifest summary it prints: the swap rewrites the\n**nearest** manifest found walking up from the target directory — one manifest total.\nWorkspace-member manifests in a monorepo are never modified; instead the codemod lists\neach member that still declares the v1 SDK together with the exact dependency changes\nit needs (remove the v1 entry, add the v2 packages that member's imports use) — apply\nthose edits yourself, then install. The v2 additions are computed from the final import\nstate of each package's sources, so already-migrated sources still receive the v2\npackages they need when the v1 dependency is removed. In a hoisted monorepo (members\nwithout their own SDK dependency), member usage counts toward the manifest that\ndeclares the v1 SDK, and the summary notes which members contributed. See\n[Monorepo workspace members](#monorepo-workspace-members) for how to decide each\nmember's packages.\n\n#### Monorepo workspace members\n\nDeclare in every member exactly what its own sources import: files importing\n`@modelcontextprotocol/server` (or its subpaths) need `@modelcontextprotocol/server`;\nclient imports need `@modelcontextprotocol/client`; raw `*Schema` constants need\n`@modelcontextprotocol/core`; a framework adapter import (`@modelcontextprotocol/express`\netc.) needs the adapter package **plus the framework itself** in that member (the\nadapter declares it as a peer dependency). Place a package in `dependencies` when\nshipped runtime code imports it and in `devDependencies` when only tests, fixtures, or\nlocal tooling do — when in doubt, use the section where the member previously declared\n`@modelcontextprotocol/sdk`.\n\nA member that never declared the v1 SDK and resolved it through the root can keep\nroot-level declarations (the codemod's root rewrite already adds the union of the\ncontributing members' v2 packages — its hoisting note names them) or move to\nper-member declarations; per-member is recommended, since the v2 package split makes each member's\nactual needs explicit. To answer \"which packages does this member need\" directly, run\nthe codemod against that member's directory with `--dry-run`: the manifest summary is\ncomputed from that member's own imports. (The authoritative import-path routing lives\nin the codemod's [mapping file](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).)\n\nThe framework adapter packages declare their framework as a **peer dependency**\n(`express`, `hono`, `fastify`); v1 shipped them as direct deps. The codemod adds the\n`@modelcontextprotocol/*` packages your imports use, but does not add the framework\npeer — install it explicitly (`pnpm add express` etc.). `@modelcontextprotocol/node`\ndepends on `@hono/node-server` at runtime (Node HTTP ↔ Web Standard conversion) and\ninstalls `hono` as a regular dependency only because `@hono/node-server` requires it as\na peer — the adapter does not use the `hono` framework itself.\n\nv2 requires **Node.js 20+**. It is ESM-first but ships a **CommonJS build alongside\nESM**, so CommonJS projects can `require('@modelcontextprotocol/…')` directly — no\ndynamic `import()` shim required.\n\nRepo-local tooling that encodes the literal v1 package name — dependency-pin lints,\nversion allowlists, CI checks, scripts — fails after the manifest swap and is invisible\nto the codemod (it rewrites sources and manifests, not bespoke gates). Grep for\n`@modelcontextprotocol/sdk` outside `src/` before declaring the migration done. While\ngrepping, also remove v1-era double casts on SDK types (`as unknown as Transport` and\nsimilar, usually annotated to a v1 issue) — v2's types satisfy those contracts\ndirectly, and a surviving cast keeps suppressing type checking that would otherwise\ncatch real errors.\n\nTooling that pins SDK **dist text** (reading a constant out of a built file with\n`require.resolve` + a regex) breaks in two stacked ways: the literal usually lives in a\ncontent-hashed sibling chunk (`dist/sse-<hash>.mjs`), not the subpath's entry module,\nso fixed-path reads do not survive a rebuild — scan the package's `dist/` directory\nfor the literal instead; and the emitted quote style differs from v1, so a\nquote-anchored pattern misses silently — match either quote. The build layout also\nchanged: v2 emits `.mjs`/`.cjs` siblings in a flat `dist/`, so v1's `/dist/cjs/` ↔\n`/dist/esm/` flavor-pair path swaps have no equivalent.\n\n#### Registry availability\n\nAll v2 packages are published on the public npm registry. Two notes:\n\n- As of `2.0.0-beta.1` all v2 packages share one version number (earlier alphas\n  did not). The codemod writes ranges that match what is published, so prefer its\n  manifest output over hand-pinning every package.\n- Environments that resolve through a corporate or private registry mirror may not\n  have synced the newer scoped packages yet (the symptom is \"not found\" for a package\n  that exists on npmjs.org). Point the install at the public registry\n  (`npm install --registry=https://registry.npmjs.org/` or the equivalent `.npmrc`\n  entry), ask your mirror's operators to sync the `@modelcontextprotocol` scope, or —\n  where neither is possible — build a tarball from a checkout of this repository\n  (`pnpm install && pnpm build`, then `pnpm pack` in the package directory) and\n  reference it with a committed `file:` dependency.\n\n#### CommonJS test runners (Jest)\n\nv2 ships a CommonJS build, so CJS test runners resolve the packages natively through the\n`require` export condition — Jest (including `next/jest` setups) no longer needs a\n`moduleNameMapper` workaround to import `@modelcontextprotocol/*`. If you carried a\nv1-era mapping that pinned these packages to their `dist/*.mjs` files, remove it. Vitest\nand native Node ESM are unaffected.\n\n#### Bundlers: nested `zod` copies in zod@3-pinned monorepos\n\nv1's `zod ^3.25 || ^4.0` peer range deduplicated onto a workspace's hoisted zod@3. The\nv2 packages depend on `zod ^4.2.0`, so in a workspace that pins zod@3 the dependency\ncannot dedupe — each installed v2 package resolves its own nested zod@4 copy. Two\nbundler consequences:\n\n- **Path-substring vendor pins capture the nested copies.** Bundler rules that match\n  zod by module path — `manualChunks` pins, vendor-chunk matchers, bundle budgets keyed\n  on a `zod/` path segment — also match `@modelcontextprotocol/*/node_modules/zod`,\n  which can pull the nested copies into an eagerly-loaded vendor chunk and trip a\n  budget gate. Exclude the SDK-nested paths from such pins so the copies ride with the\n  SDK's own (typically lazy) chunks.\n- **Ballpark size cost.** Measured on a large production SPA, adding the v2 client and\n  server packages (with their nested zod@4 copies) alongside a hoisted zod@3 cost\n  roughly +83 KB gzipped of total JS (about +0.7% whole-app). Upgrading the workspace\n  to `zod ^4.2.0` re-dedupes and removes the duplication.\n\n#### Migrating in stages (large codebases)\n\nThe v1 package and the v2 packages have **different names**, so both can be installed\nin one manifest at the same time — nothing forces a one-shot swap. The safe order for\nan incremental migration: (1) add the v2 packages (and the `zod ^4.2.0` bump) while\n**keeping** `@modelcontextprotocol/sdk`; (2) rewrite sources incrementally,\ndirectory-by-directory or package-by-package; (3) remove the v1 dependency only when\nnothing imports it any more (`grep -rn \"@modelcontextprotocol/sdk\" --include=\"*.ts\"`,\nplus a look at `package.json`). The inverse order strands files: swapping the manifest\nfirst leaves every not-yet-rewritten import failing module resolution (TS2307) until it\nis updated.\n\nTwo caveats for the transition window. First, a codemod run against a subdirectory\nstill updates the nearest manifest walking up — including removing the v1 dependency —\nso during a staged pass review or revert that edit until the final stage (or preview\nwith `--dry-run`). Second, v1 and v2 modules each have their own classes and types:\nobjects must not flow between v1-imported and v2-imported code (`instanceof` and\nnominal types do not cross — the same boundary described for dual-role processes in\n[Errors](errors.md#errors)), so stage along process or transport boundaries where the two sides\nshare only the wire format; the two sides negotiate\na protocol version through the ordinary 2025-era `initialize` handshake and settle\non the newest revision both packages support (currently 2025-11-25 — published v1\n1.29.x and v2 ship the same supported-version list).\n\nDependencies you do not control (vendored fixtures, third-party packages) that still\ndeclare `@modelcontextprotocol/sdk` resolve their own v1 copy and need no action. For\n`peerDependencies` declarations, keep the v1 package installed to satisfy the range —\nor point the name at a chosen version via your package manager's\n`overrides`/`resolutions` — until those packages migrate. The same boundary rule\napplies: objects must not flow between their v1-imported code and your v2-imported\ncode.\n\n**Dependencies that compile against the host's v1 SDK.** A stricter variant of the\nabove: a workspace or vendored package that ships TypeScript **source** importing\n`@modelcontextprotocol/sdk` — resolved from the host's `node_modules` rather than its\nown — pins the host. Keep the v1 package installed as a real dependency (not merely a\nsurviving transitive) until that package migrates. The host files that construct or\nhand objects to such a package are part of its v1 boundary and must stay on v1 imports\n— and the codemod cannot see that distinction: it rewrites them like any other file\n(e.g. converting a `setRequestHandler(Schema, …)` call into the v2 method-string form\nagainst what is still a v1 `Server`, which then fails at runtime). Run the codemod with\n`--ignore` glob patterns covering those interfacing files, and migrate them together\nwith the dependency later. The boundary rule above applies unchanged: objects from the\ndependency's v1 modules must never flow into v2-imported code.\n\n#### Library authors: peer-depending on the SDK\n\nIf your package declares `@modelcontextprotocol/sdk` as a `peerDependency`, the v2\npackages are differently **named**, so swapping the peer declaration is itself a\nbreaking change for every consumer — ship it as a semver-major. You can migrate\nahead of your consumers only if no SDK object crosses your public API (the\nv1/v2 boundary rule above applies to your exports too: a v1-constructed `Client`\nor error instance handed to v2-importing consumer code fails `instanceof` and\nnominal checks). Until your consumers migrate, they can keep resolving your peer\nrange with the v1 package installed alongside their own v2 packages — the two\ncoexist under different names.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/protocol-ctx.md":"### Low-level protocol & handler context (`ctx`)\n\nThe second parameter to every request handler — previously the flat `RequestHandlerExtra`\nobject named `extra` — is now a structured **context** object named `ctx`. This is the\n`ctx` that appears throughout the rest of this guide.\n\nThe codemod renames the parameter and remaps property access via\n[`contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts).\nA few mappings need optional-chaining adjustment (the `http` group is `undefined` on\nstdio):\n\n| v1 (`extra.*`)                                    | v2 (`ctx.*`)                   | Note                                                                                                                                                                                                                                                                                                                    |\n| ------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `extra.signal`                                    | `ctx.mcpReq.signal`            |                                                                                                                                                                                                                                                                                                                         |\n| `extra.requestId`                                 | `ctx.mcpReq.id`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra._meta`                                     | `ctx.mcpReq._meta`             |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendRequest(...)`                          | `ctx.mcpReq.send(...)`         |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendNotification(...)`                     | `ctx.mcpReq.notify(...)`       |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sessionId`                                 | `ctx.sessionId`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra.authInfo`                                  | `ctx.http?.authInfo`           | optional — `undefined` on stdio                                                                                                                                                                                                                                                                                         |\n| `extra.requestInfo`                               | `ctx.http?.req`                | a standard Web `Request`; `ServerContext` only                                                                                                                                                                                                                                                                          |\n| `extra.closeSSEStream`                            | `ctx.http?.closeSSE`           | `ServerContext` only; the member itself is also optional — defined only when the transport has an `eventStore` AND the client's negotiated protocol version supports resumable close (2025-11-25+); an `eventStore` transport serving a 2025-06-18 client still leaves it `undefined`. Call as `ctx.http?.closeSSE?.()` |\n| `extra.closeStandaloneSSEStream`                  | `ctx.http?.closeStandaloneSSE` | `ServerContext` only; member optional as above — `ctx.http?.closeStandaloneSSE?.()`                                                                                                                                                                                                                                     |\n| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed_                      | see [Experimental tasks](behavioral-changes.md#experimental-tasks-interception-removed)                                                                                                                                                                                                                                 |\n\nThe transport-level seam behind `ctx.http?.authInfo` is unchanged from v1: a transport\nthat passes `{ authInfo }` as the second argument to `onmessage(message, extra)` — e.g.\nan `InMemoryTransport` test seam — still surfaces it as `ctx.http?.authInfo` on any\ntransport, and `ctx.http` is defined whenever `authInfo` is supplied, even without an\nHTTP transport.\n\n`BaseContext` is the common base; `ServerContext` and `ClientContext` extend it. None\nof the three takes type parameters — v1's `RequestHandlerExtra<TRequest, TNotification>`\narguments selected request/notification unions that the v2 context carries\nintrinsically, so their removal loses no type information; review only handlers that\npassed custom (non-standard) unions, whose `sendRequest` / `sendNotification` typing\nwas narrowed by them. `ServerContext.mcpReq` adds convenience methods that replace\ncalling `server.*` from inside a handler:\n\n| `ctx.mcpReq.*` (new)                           | Replaces (inside a handler)                                                                                                                                                                                                                                                                              |\n| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ctx.mcpReq.log(level, data, logger?)`         | `server.sendLoggingMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577); the notification also becomes request-related on every era — see [§`ctx.mcpReq.log()` is request-related on every era](behavioral-changes.md#ctxmcpreqlog-is-request-related-on-every-era) |\n| `ctx.mcpReq.elicitInput(params, options?)`     | `server.elicitInput(...)`                                                                                                                                                                                                                                                                                |\n| `ctx.mcpReq.requestSampling(params, options?)` | `server.createMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577)                                                                                                                                                                                                  |\n\n#### Deprecated in v2 (SEP-2577)\n\nThe roots, sampling, and logging subsystems are deprecated as of protocol version\n2026-07-28 (SEP-2577). Everything below is **still fully functional in v2** and marked\n`@deprecated` for removal in a later major; on a 2026-07-28 connection prefer the\n[multi-round-trip `input_required` pattern](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#multi-round-trip-requests)\ninstead.\n\n- **Runtime APIs**: `Server.createMessage` / `listRoots` / `sendLoggingMessage`,\n  `McpServer.sendLoggingMessage`, `Client.setLoggingLevel` / `sendRootsListChanged`, and\n  the `ctx.mcpReq.log` / `ctx.mcpReq.requestSampling` handler-context helpers. Outside a\n  handler, `McpServer` users reach the `Server.*` methods via the unchanged\n  [`mcpServer.server` accessor](unchanged-apis.md#unchanged-apis).\n- **Capability fields**: the `roots`, `sampling`, and `logging` capability schema fields.\n- **Type stacks**: the full Logging stack (`LoggingLevel`, `SetLevelRequest`,\n  `LoggingMessageNotification` and params), the full Sampling stack\n  (`CreateMessageRequest`/`Result`, `SamplingMessage`, `ModelPreferences`/`ModelHint`,\n  `ToolChoice`, `ToolUseContent`/`ToolResultContent`, the `includeContext` enum values),\n  and the full Roots stack (`Root`, `ListRootsRequest`/`Result`,\n  `RootsListChangedNotification`).\n- **`registerClient`** (Dynamic Client Registration) — deprecated via spec PR\n  [modelcontextprotocol#2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)\n  rather than SEP-2577 (listed here because the `@deprecated` annotations landed in\n  the same sweep); prefer Client ID Metadata Documents per SEP-991.\n\nThe deprecation is annotation-only — JSDoc `@deprecated` markers were added, nothing\nelse: every deprecated runtime API keeps its v1 call signature (e.g.\n`Server.sendLoggingMessage(params, sessionId?)` keeps the two-argument form) and its\nwire behavior, and remains functional for at least the twelve-month deprecation window.\n\n#### `setRequestHandler` / `setNotificationHandler` use method strings\n\nThe low-level handler registration takes a **method string** instead of a Zod schema.\nThe codemod rewrites every spec-method registration via\n[`schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts).\n\n```typescript\n// v1\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => { ... });\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => { ... });\n```\n\n**Custom (non-spec) methods** use the 3-arg form `(method, { params, result? }, handler)`\nwhere `params` and `result` are any [Standard Schema](https://standardschema.dev). The\nhandler receives the parsed `params` directly (not the full request envelope); `_meta`\nis at `ctx.mcpReq._meta`. The 3-arg notification handler is `(params, notification) => void`.\n\n```typescript\nserver.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => { ... });\n```\n\nThe custom form also covers **spec method names carried with custom payloads**: a v1\nintegration that reused a spec method string for its own payload shape (e.g.\n`notifications/message` notifications carrying a proprietary params object) registers\nit with the 3-arg form and its own schema. The overloads are selected by the arguments'\nshape, not by the method name — a schemas object as the second argument always selects\nthe custom form, which validates against **your** schema (the spec schema is not\napplied) and hands the handler the parsed params rather than the envelope.\n\n**Spec notifications** use the 2-arg form `setNotificationHandler(method, handler)`.\nUnlike the 3-arg custom form, the spec-form handler receives the **full notification\nenvelope** (`{ method, params }`), parsed against the spec schema — read\n`notification.params`:\n\n```typescript\nclient.setNotificationHandler('notifications/tools/list_changed', async notification => {\n    console.log(notification.method, notification.params);\n});\n```\n\nThe two overloads are selected by the method string's **type**: the spec form binds the\nmethod to the `NotificationMethod` union (`RequestMethod` on the request side — both\nexported), so a method string computed at runtime must be typed as `NotificationMethod`\nto select it; an untyped `string` lands on the custom-schema overload and fails to\ncompile without a schemas argument. `Parameters<Client['setNotificationHandler']>[0]`\nalso resolves to the custom `string` overload by design — name `NotificationMethod`\ndirectly instead. The request side has the same trap one slot over:\n`Parameters<Client['setRequestHandler']>` (and `typeof`-indexed casts over the overload\nset) resolve against the 3-arg custom-method overload, so index `[1]` is the\n`{ params, result }` schemas object, **not** the handler — v1 signature-erasing handler\ncasts derived positionally change meaning with no runtime symptom. Name the exported\ntypes (`RequestMethod` and your own handler/param types) instead of deriving them\npositionally. Generic helpers that v1 parameterized on a notification schema need\nthis conversion by hand; the codemod only warns on them.\n\n**Handler returns are spec-typed.** In v1 the handler's return type flowed from the\nschema you registered; v2 types it from the method name (`'tools/list'` →\n`ListToolsResult`, and so on). Tool tables kept as plain object literals surface two\nrecurring compile errors: an unannotated literal widens `type: 'object'` to `string`\nand no longer satisfies the spec type's `type: 'object'` literal member (fix:\n`type: 'object' as const`, or annotate the table as `Tool[]`); and a heterogeneous\ntable whose inferred union carries `prop?: undefined` members does not satisfy the spec\ntypes' `Record<string, JSONValue>` index signatures, since `undefined` is not a\n`JSONValue` (fix: annotate the handler's return type —\n`async (req): Promise<ListToolsResult> => …` — or the table itself, so each literal is\nchecked against the target type instead of being inferred and widened first).\n\n#### `request()`, `ctx.mcpReq.send()`, and `callTool()` no longer require a schema for spec methods\n\nFor **spec** methods, drop the result-schema argument; the SDK resolves it from the\nmethod name. The codemod drops it from `client.request()` and `client.callTool()`; drop\nit from `ctx.mcpReq.send()` by hand.\n\n```typescript\n// v1\nimport { CreateMessageResultSchema } from '@modelcontextprotocol/sdk/types.js';\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n    const r = await extra.sendRequest({ method: 'sampling/createMessage', params: { ... } }, CreateMessageResultSchema);\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => {\n    const r = await ctx.mcpReq.send({ method: 'sampling/createMessage', params: { ... } });\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n```\n\nFor **custom (non-spec)** methods, keep the result-schema argument:\n`await client.request({ method: 'acme/search', params }, SearchResult)` — only drop the\nschema when calling a spec method.\n\n**Forwarding arbitrary methods (gateways / proxies).** Dropping the schema changes\nsemantics, not just the signature: a schema-less spec-method call now **enforces** the\nspec result schema (a non-conforming upstream result is rejected locally with\n`SdkError(SdkErrorCode.InvalidResult)` and a conforming one is re-serialized in schema\nkey order), and a schema-less call for a **non-spec** method throws a `TypeError` at\nthe call site (`'…' is not a spec method; pass a result schema`).\nA relay that forwards `{ method, params }` it does not understand must keep passing an\nexplicit result schema. The v1 idiom survives with an import-path change:\n\n```typescript\nimport { ResultSchema } from '@modelcontextprotocol/core';\nconst result = await upstream.request({ method, params }, ResultSchema); // v1-identical passthrough\n```\n\nFor byte-exact forwarding (member order preserved), pass your own accept-anything\nStandard Schema instead. Check call sites whose `method` is **not a literal** — the\ncodemod may have dropped the schema argument there; restore it.\n\nThe **inbound half** — a relay re-emitting an upstream JSON-RPC error from its own\nhandler — has a supported surface too: reconstruct the typed error with\n`ProtocolError.fromError(code, message, data)` and throw it; the encode seam serializes\nit back to the wire shape (see [Typed `ProtocolError` subclasses](errors.md#typed-protocolerror-subclasses)).\nNote this is typed reconstruction, not byte-exact relay: legacy codes are normalized at\nthe encode seam (`-32002` re-emits as `-32602`) and the typed subclasses keep only their\nschema-defined `data` members, so extra upstream data keys are dropped. Throwing a plain\nobject carrying `.code` / `.message` / `.data` happens to work today, but it is\nunspecified behavior — prefer `fromError`.\n\nThe return type is inferred from the method name via `ResultTypeMap` (e.g.\n`client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`).\nv1 call sites that passed `CreateMessageResultWithToolsSchema` explicitly need no\nreplacement: the schema-less send resolves to\n`CreateMessageResult | CreateMessageResultWithTools`, and validation selects the\nwith-tools variant when the request set `tools` or `toolChoice`.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md":"### Server registration API\n\nThe deprecated variadic `.tool()`, `.prompt()`, `.resource()` are removed. Use\n`registerTool` / `registerPrompt` / `registerResource` with an explicit config object.\nThe codemod converts the call shape and wraps `inputSchema` / `outputSchema` /\n`argsSchema` / `uriSchema` raw shapes.\n\n```typescript\n// v1 — raw shape, variadic\nserver.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n\n// v2 — config object, Standard Schema\nserver.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n```\n\n`registerResource` requires a `metadata` argument — pass `{}` if you have none.\n\nA tool or prompt registered **without** an `inputSchema` / `argsSchema` passes the\ncontext as its callback's single argument — v1 passed `(extra)`, v2 passes `(ctx)`:\n\n```typescript\nserver.registerTool('ping', { description: 'Liveness check' }, async ctx => ({ content: [] }));\n```\n\nA one-parameter callback typechecks under either reading, so remember that the first\nparameter here is the context object, not an args object.\n\n#### Standard Schema objects (raw shapes deprecated)\n\nv2 expects schema objects implementing the [Standard Schema spec](https://standardschema.dev/)\nfor `inputSchema`, `outputSchema`, and `argsSchema`. Raw `{ field: z.string() }` shapes\nare still **accepted via `@deprecated` overloads** on `registerTool`/`registerPrompt`\n(auto-wrapped with `z.object()`), and `completable()` accepts any `StandardSchemaV1`;\nprefer wrapping explicitly. Zod v4, ArkType, and Valibot all implement the spec.\n\nFor **optional completable arguments**, apply `.optional()` to the _result_ of\n`completable()` — `completable(z.string(), cb).optional()`, not\n`completable(z.string().optional(), cb)`. v2 resolves completion metadata on the schema\nfound after unwrapping an outer optional wrapper, so the v1 nesting returns empty\ncompletion lists — nothing errors — and if no argument carries completion metadata in\nthe v2 position, the server does not advertise the `completions` capability at all. The\ncodemod inverts the common nesting automatically and flags shapes it cannot rewrite.\n\n**Zod v3 is no longer supported** (v1 peer was `^3.25 || ^4.0`). Check the **declared\nrange** in your `package.json`, not just the installed version: a zod-3 range that\nsatisfied the v1 peer installs and typechecks cleanly under v2 and only fails at\nruntime — and quietly: registration does not convert the schema, the server starts\nand connects normally, and the first `tools/list` (so `client.listTools()`) answers\nwith an error pointing at `fromJsonSchema()` while the process keeps running. (Only the\ndeprecated unwrapped raw-shape form with zod-3 field values throws at registration,\nwith a message pointing at `zod/v4`.) Zod **≥4.2.0** self-converts via\n`~standard.jsonSchema` — the supported path. Zod **4.0–4.1** lacks it, so the SDK falls\nback to its bundled Zod's `z.toJSONSchema()` with a one-time `[mcp-sdk]` console\nwarning; and because `.describe()` field descriptions live in the _authoring_ Zod's\nregistry, the fallback **drops them** from the generated JSON Schema. Fix ladder:\n(1) upgrade to `zod ^4.2.0`; (2) if you must pin an older or separate Zod, attach a\n`~standard.jsonSchema` provider backed by _your_ Zod's `toJSONSchema` so conversion\n(and descriptions) run through your instance; (3) author the schema as raw JSON Schema\nvia `fromJsonSchema()`. (Raw shapes are wrapped with the SDK's **bundled** Zod — built\nwith a foreign Zod they fail at registration or at the first `tools/list`; pass\n`z.object()`-wrapped schemas from your own Zod instead.)\n\nIn a monorepo that pins zod@3 workspace-wide and cannot bump, step (1) can be applied\n**per workspace member**: add a zod-4 alias dependency to the migrating member only —\n`\"zod-v4\": \"npm:zod@^4.2.0\"` in that member's `package.json` — and author SDK-bound\nschemas with it (`import { z } from 'zod-v4'`), leaving the rest of the workspace, and\nthe member's own zod-3 consumer schemas, untouched. The alias copy does not need to be\nthe same instance as the SDK's bundled zod: conversion runs through the **authoring**\ninstance's `~standard.jsonSchema`, so `.describe()` descriptions are preserved and the\nemitted dialect is 2020-12. Keep the two z's apart — schemas authored with the alias\nare for the SDK; they do not compose with the workspace's zod-3 schemas. (For the\nbundle-side effects of the same pin, see\n[Bundlers: nested `zod` copies](packaging-runtime.md#bundlers-nested-zod-copies-in-zod3-pinned-monorepos).)\n\n**Hosts that forward consumer-authored schemas.** The ladder assumes you author the\nschemas yourself. A host API that accepts raw shapes or schemas written by **its own\nconsumers** — plugin systems, agent frameworks — cannot control the authoring zod\nversion or instance, and v1's built-in conversion of foreign shapes is gone. Convert on\nthe host side and register the result with `fromJsonSchema()`: zod-4 input via zod's\nown `z.toJSONSchema(z.object(shape), { io: 'input', target: 'draft-2020-12' })` (the\nconversion is runtime-structural, so a zod ≥4.2 in the host handles schemas built by a\ndifferent zod-4 copy), zod-3 input via the\n[`zod-to-json-schema`](https://www.npmjs.com/package/zod-to-json-schema) package. Its\ndefault draft-07 `$schema` stamp is fine as-is — the default validator\n[honors declared draft-07/06 dialects](types-schemas.md#json-schema-2020-12-posture-sep-1613-sep-2106).\n\nHow a too-old zod surfaces depends on which entry point your code imports. With\nmain-entry `import { z } from 'zod'` on a zod-3 range, the project **typechecks cleanly\nand fails at the first `tools/list`** (the quiet runtime path above). With\n`import * as z from 'zod/v4'` — or any zod whose _typings_ predate\n`~standard.jsonSchema` (zod 4.0–4.1, and zod 3.25.x via the `zod/v4` subpath) — the\nsame code **runs** through the bundled fallback but **fails to compile**:\n`registerTool`/`registerPrompt` reject the schema with `TS2769: No overload matches\nthis call` listing both overloads. The real cause is buried in the first overload's\nelaboration — `Property 'jsonSchema' is missing in type …` (that property is\n`~standard.jsonSchema`, added in zod 4.2.0) — and a follow-on implicit-`any` error on\nthe handler's arguments usually appears below it. If you see that two-overload error on\na registration call with a zod schema, check the installed zod version before anything\nelse; both symptoms resolve identically with step (1) of the ladder.\n\nProjects that must stay below zod 4.2 and accept the documented runtime fallback can\nresolve the remaining registration compile errors with an explicit assertion to the\nregistration schema type — `inputSchema: schema as unknown as\nStandardSchemaWithJSON<Input, Output>` — or a small typed wrapper that attaches a\n`~standard.jsonSchema` provider (step (2) of the ladder, which changes runtime\nconversion but not the schema's static type) and returns the asserted type. The\nfallback caveats (one-time warning, dropped `.describe()` descriptions) still apply\nunless the provider is attached.\n\nThe forced zod-4 bump also surfaces zod's **own** type-level API changes in consumer\nannotations: `z.ZodTypeDef` no longer exists and `z.ZodType`'s generic parameters\nchanged, so v3-era annotations like `z.ZodType<Output, z.ZodTypeDef, Input>` fail to\ncompile — see [zod's v3-to-v4 changelog](https://zod.dev/v4/changelog). Consumer-only\nschemas can keep compiling via zod's v3 compat subpath (`zod/v3`), but anything passed\nto the SDK must be a zod-4 (or other Standard Schema) schema.\n\nThe deprecated raw-shape overloads exist only on `registerTool` / `registerPrompt`.\n`RegisteredTool.update()` / `RegisteredPrompt.update()` take **schema objects**\n(`paramsSchema` / `outputSchema`: `StandardSchemaWithJSON`) — a raw shape passed to\n`update()` is not auto-wrapped; wrap it with `z.object()` yourself.\n\n```typescript\nimport * as z from 'zod/v4';\nserver.registerTool('greet', { inputSchema: z.object({ name: z.string() }) }, handler);\n\n// ArkType works too\nimport { type } from 'arktype';\nserver.registerTool('greet', { inputSchema: type({ name: 'string' }) }, handler);\n\n// Raw JSON Schema via fromJsonSchema (validator defaults to runtime-appropriate choice)\nimport { fromJsonSchema } from '@modelcontextprotocol/server';\nserver.registerTool('greet', { inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }) }, handler);\n\n// No-parameter tools: z.object({})\n```\n\nRemoved Zod-specific helpers (the codemod marks each call site `@mcp-codemod-error`):\n`schemaToJson` — use `fromJsonSchema()` from `@modelcontextprotocol/server` for raw JSON\nSchema, or your schema library's native JSON-Schema conversion; `parseSchemaAsync` — use\nyour schema library's validation directly (e.g. Zod's `.safeParseAsync()`);\n`getSchemaShape` / `getSchemaDescription` / `isOptionalSchema` / `unwrapOptionalSchema`\nhave no replacement (internal Zod introspection). `SchemaInput<T>` →\n`StandardSchemaWithJSON.InferInput<T>` is rewritten mechanically by the codemod. The\ninternal `standardSchemaToJsonSchema` / `validateStandardSchema` helpers are **not** part\nof the public surface — do not import them.\n\nv1's second compat module, `server/zod-json-schema-compat.js` (`toJsonSchemaCompat`), is\nalso removed — and the codemod does **not** rewrite its import (expect `TS2307`). If you\nbuild `Tool` / `Prompt` advertisements yourself, use your schema library's native\nconversion: zod 4's `z.toJSONSchema(schema, { io: 'input', target: 'draft-2020-12' })`\nproduces the dialect v2 advertises.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/types-schemas.md":"### Types & schemas\n\n#### Zod `*Schema` constants moved to `@modelcontextprotocol/core`\n\nThe Zod schemas (`CallToolResultSchema`, `ListToolsResultSchema`, …) that v1 exported\nfrom `types.js` now live in a separate **`@modelcontextprotocol/core`** package. Neither\n`@modelcontextprotocol/client` nor `@modelcontextprotocol/server` re-exports them — both\npackages stay Zod-free in their public surface.\n\nThe v1→v2 change is just an import-path swap — `.parse()` / `.safeParse()` keep working\nunchanged:\n\n```typescript\n// v1\nimport { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n\n// v2 — same Zod schema, new package\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n```\n\n`@modelcontextprotocol/core` is the canonical home for the spec's Zod schema constants\n(and the OAuth/OpenID metadata schemas). It is runtime-neutral (its only dependency is\n`zod`) and arrives transitively as the shared runtime schema graph of `client` /\n`server` — add it to your own `dependencies` only when you import the raw schemas\ndirectly.\n\nIf you would rather keep your project Zod-free, the **`isSpecType` / `specTypeSchemas`**\nalternatives are exported from `@modelcontextprotocol/client` and `…/server`:\n\n```typescript\nimport { isSpecType, specTypeSchemas } from '@modelcontextprotocol/client';\nif (isSpecType.CallToolResult(value)) { ... }\nconst blocks = mixed.filter(isSpecType.ContentBlock);\nconst result = specTypeSchemas.CallToolResult['~standard'].validate(value);\n```\n\n`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of\nevery named type in the MCP spec — so you get autocomplete and a compile error on typos.\n`specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` (`validate()` is synchronous).\n`validate()` returns `{ value }` or `{ issues }` and never throws — unlike `.parse()` on\nthe real schema; code that caught a `ZodError` should inspect `result.issues` (or keep\n`.parse()` on the schema imported from `@modelcontextprotocol/core`).\nThe pre-existing `isCallToolResult(value)` guard still works.\n\n**`specTypeSchemas.X` is `StandardSchemaV1`, not `ZodType`.** Zod-specific composition\n— `.extend()`, `.pick()`, `.omit()`, `.merge()`, `.shape`, `.passthrough()`,\n`.parseAsync()` — does **not** compile on a `specTypeSchemas` entry; reach for the real\nZod schema from `@modelcontextprotocol/core` when you need to derive a tolerant variant\nof a spec schema (e.g.\n`ListToolsResultSchema.extend({ tools: ToolSchema.omit({ outputSchema: true }).array() })`).\nThe Zod-specific `AnySchema` / `SchemaOutput` types from `…/zod-compat.js` are removed —\nreplace with `StandardSchemaV1` / `StandardSchemaV1.InferOutput<T>` (the codemod's\nremoval message says the same).\n\n**Composing two core schemas.** Zod composition needs a shared zod: deriving from a\nsingle core schema (as above) and combining core schemas with your own `z` typecheck\nwhen your `zod` resolves to the **same copy** `@modelcontextprotocol/core` uses (a\n`zod ^4.2.0` range that dedupes). When it cannot — a zod@3-pinned project nests core's\nown zod@4 — v1 idioms that combined two spec schemas with your `z` no longer compile:\ncore does not export its zod instance, and a foreign zod's `z.union(…)` / `.or(…)`\nrejects core's schema types. For accept-either result parsing, skip composition:\nrequest with the `ResultSchema` passthrough (the same one the\n[gateway note](protocol-ctx.md#request-ctxmcpreqsend-and-calltool-no-longer-require-a-schema-for-spec-methods)\nuses) and discriminate with sequential `safeParse`:\n\n```typescript\n// v1 — one composed schema\nconst result = await client.request(req, z.union([CompatibilityCallToolResultSchema, CreateTaskResultSchema]));\n\n// v2 — passthrough request, then sequential discrimination\nimport { CompatibilityCallToolResultSchema, CreateTaskResultSchema, ResultSchema } from '@modelcontextprotocol/core';\nconst raw = await client.request(req, ResultSchema);\nconst asTask = CreateTaskResultSchema.safeParse(raw);\nconst result = asTask.success ? asTask.data : CompatibilityCallToolResultSchema.parse(raw);\n```\n\nOrder the candidates from most to least specific, and `.parse()` the last one so a\nresult that matches no candidate still fails loudly.\n\nThe role-aggregate unions (`ClientRequest`, `ServerResult`, `ServerRequest`,\n`ClientResult`, `ClientNotification`, `ServerNotification`) and the typed-method maps\n(`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap`) no longer\ninclude task vocabulary; the deprecated `Task*` types remain importable on their own.\n(One published-alpha qualification, like the `-32002` note in [Errors](errors.md#errors): the\n`2.0.0-alpha.3` and earlier typings predate this — the typed maps there still carry the\n`tasks/*` entries, and `ResultTypeMap['tools/call']` still unions `CreateTaskResult`, so\na `client.request({ method: 'tools/call', … })` result does not assign to\n`Promise<CallToolResult>`. If pinned to those alphas, narrow with the\n`isCallToolResult` guard — the recommended discrimination tool anyway, per the next\nparagraph; `2.0.0-alpha.4` and later are unaffected.)\n\n**Discriminating result shapes: use guards, not the `in` operator.** The v2\nzod-inferred result types are passthrough objects — every union member carries an index\nsignature — so v1-idiomatic property discrimination such as\n`if ('content' in result) { … } else { result.toolResult }` no longer narrows: the `in`\ncheck is satisfiable by every member, and the else branch can collapse to `never`\n(surfacing as `TS2339` on the property you then read). Use the exported guards instead:\n`isCallToolResult(result)`, or `isSpecType.GetPromptResult(result)` and friends for any\nother spec type ([above](#zod-schema-constants-moved-to-modelcontextprotocolcore)). An\nadjacent trap when keeping a union for later narrowing: a `const` **annotation** is\ncontrol-flow-narrowed straight back to the initializer's type — after\n`const r: A | B = await fn()`, `r` has `fn`'s return type, not the union — so when you\nneed the wider union (e.g. a `CompatibilityCallToolResult` branch), apply an\n`as A | B` assertion instead of an annotation.\n\n#### Removed type aliases\n\n| Removed                                                         | Replacement                                                     |\n| --------------------------------------------------------------- | --------------------------------------------------------------- |\n| `JSONRPCError`                                                  | `JSONRPCErrorResponse`                                          |\n| `JSONRPCErrorSchema`                                            | `JSONRPCErrorResponseSchema`                                    |\n| `isJSONRPCError`                                                | `isJSONRPCErrorResponse`                                        |\n| `isJSONRPCResponse` (deprecated in v1)                          | `isJSONRPCResultResponse` ²                                     |\n| `JSONRPCResponseSchema` (result-only in v1)                     | `JSONRPCResultResponseSchema` ²                                 |\n| `JSONRPCResponse` (result-only in v1)                           | `JSONRPCResultResponse` ²                                       |\n| `ResourceReference` / `ResourceReferenceSchema`                 | `ResourceTemplateReference` / `ResourceTemplateReferenceSchema` |\n| `IsomorphicHeaders`                                             | Web Standard `Headers`                                          |\n| `RequestHandlerExtra`                                           | `ServerContext` / `ClientContext` / `BaseContext`               |\n| `ResourceTemplate` (the spec wire **type** from `sdk/types.js`) | `ResourceTemplateType` ³                                        |\n\n² v2 introduces **new** `isJSONRPCResponse` / `JSONRPCResponse` / `JSONRPCResponseSchema`\nwith corrected semantics — they match **both** result and error responses (the schema is\n`z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])`). v1's symbols only\nmatched results. To preserve v1 behavior, rename to `isJSONRPCResultResponse` /\n`JSONRPCResultResponse` / `JSONRPCResultResponseSchema` (the codemod does this).\n\n³ The `ResourceTemplate` URI-template helper **class** (from `sdk/server/mcp.js`) is\n**unchanged** — keep `new ResourceTemplate(...)` as-is. Only the like-named spec wire\ntype from `types.js` was renamed to `ResourceTemplateType` to resolve the v1 collision;\nthe codemod scopes the rename to imports from `sdk/types.js` only.\n\nAll other symbols from `@modelcontextprotocol/sdk/types.js` retain their original\nnames — import the TypeScript types, error classes, enums, and type guards from\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the Zod\n`*Schema` constants from `@modelcontextprotocol/core`.\n\nOne type-level narrowing to note: client/server capability `experimental` payloads are\nnow typed as JSON-compatible objects (nested JSON values) rather than arbitrary\nobjects. A payload typed `Record<string, unknown>` no longer assigns (`TS2322`) — give\nthe source a JSON-compatible type or cast at the boundary.\n\nThe `Protocol` base class and `mergeCapabilities` moved: import them from the\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server` package root instead\nof `shared/protocol.js`. Most code should not use `Protocol` directly — `Client` and\n`Server` are the supported surfaces, and for observing unmatched inbound requests\nprefer `client.fallbackRequestHandler` / `server.fallbackRequestHandler`. The codemod\nrewrites `Protocol` and `mergeCapabilities` imports to the package root, like the\nmodule's other symbols. One caveat: the client and server packages each bundle their\nown compiled copy of the class, so the two roots' `Protocol` exports are distinct\nclasses — import it from one package consistently within a process.\n\n#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)\n\nThe default validator dispatches on the schema's declared `$schema`: absent or 2020-12\nvalidates as **JSON Schema 2020-12** — on Node via `Ajv2020` instead of v1's draft-07\n`Ajv` (the Cloudflare Workers default was already 2020-12) — a declared 2019-09\n`$schema` validates with 2019-09 semantics (`Ajv2019`), and a declared draft-07 or\ndraft-06 `$schema` validates with draft-07 semantics. Schemas declaring any other\n`$schema` are rejected with `Error(\"…unsupported dialect…\")`. Two known draft-07 engine\ndifferences: the Node engine (classic Ajv, same as v1's default) evaluates keywords\nadjacent to `$ref`, stricter than draft-07's ignore-siblings rule, while the\nbrowser/Workers engine ignores them per spec; and the browser/Workers engine does not\nresolve a `$ref` inside a `dependencies` entry whose key collides with a JSON Schema\nkeyword (`type`, `default`, `format`, …) — validation throws `Unresolved $ref` when\nthat dependency triggers (surfaced as the SDK's typed validation error), while the\nNode engine handles the same schema correctly.\n\n`CallToolResult.structuredContent` is widened from `{ [k: string]: unknown }` to\n`unknown` (SEP-2106 lifts the `type:\"object\"` root restriction). The presence check is\n`!== undefined`, not falsy (`null` / `0` / `false` / `\"\"` are legal values now). External\n`$ref` is not dereferenced (unchanged from v1; Ajv throws `MissingRefError` at compile,\nsurfaced per-tool on `callTool`).\n\n| v1 pattern                                                                                                   | Mechanical fix                                                                                                                                                                                         |\n| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `result.structuredContent.<key>` / `result.structuredContent?.<k>`                                           | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }`                                                                              |\n| `if (!result.structuredContent)`                                                                             | `if (result.structuredContent === undefined)`                                                                                                                                                          |\n| draft-07 idioms **without** a declared `$schema` (a declared draft-07/06 `$schema` dispatches automatically) | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |\n| undeclared draft-07 idioms via `fromJsonSchema(schema)`                                                      | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas                                 |\n| `outputSchema` / `inputSchema` with absolute-URI `$ref`                                                      | inline under `$defs` and reference with `#/$defs/Name`                                                                                                                                                 |\n\nA tool may now register an `outputSchema` whose root is `type:\"array\"`, `type:\"string\"`,\netc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward\nevery era a non-object `structuredContent` with no `text` block of its own gets a\n`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md › Per-era wire codecs](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#per-era-wire-codecs) for how the codec applies these per era.\n\n**Your advertised tool schemas change shape on the wire.** The same `registerTool`\ncalls produce `tools/list` entries whose generated `inputSchema` differs from v1:\nJSON Schema 2020-12 idioms (zod 4 conversion), different `additionalProperties`\nhandling (no `additionalProperties: false` by default; passthrough objects emit\n`\"additionalProperties\": {}` instead of `true`), and no `execution.taskSupport` member.\nGolden tests, transcript pins, and strict client-side validators of your advertised\ntool list need re-baselining — the new shapes are spec-conformant.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/unchanged-apis.md":"## Unchanged APIs\n\nThe following are unchanged between v1 and v2 apart from the import path — except\nwhere an entry notes its own signature change:\n\n- `Client` constructor and `connect`, `close`, and the typed verbs (`listTools`,\n  `listPrompts`, `listResources`, `readResource`, …) — note `callTool()` and `request()`\n  signatures changed (schema parameter dropped for spec methods).\n- `McpServer` constructor, `server.connect(transport)`, `server.close()`, and the\n  `McpServer.server` accessor — still the supported way to call the low-level\n  `Server`'s push verbs (`createMessage` / `listRoots` / `sendLoggingMessage` — ⚠\n  `@deprecated`, see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) outside a\n  handler context.\n- The server Streamable HTTP transports' **constructor options** (`sessionIdGenerator`,\n  `onsessioninitialized`, `onsessionclosed`, `enableJsonResponse`, `eventStore`,\n  `retryInterval`) and the `handleRequest` surface — only the class name and import\n  moved: `StreamableHTTPServerTransport` is now `NodeStreamableHTTPServerTransport`\n  from `@modelcontextprotocol/node`, a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`,\n  which exposes the same options ([decision rule](imports-transports.md#imports--transports)). The\n  transport-level `closeSSEStream(requestId)` / `closeStandaloneSSEStream()` methods\n  keep their v1 names too — only the handler-context accessors moved to `ctx.http`\n  ([remap table](protocol-ctx.md#low-level-protocol--handler-context-ctx)).\n- `UriTemplate` (v1: `@modelcontextprotocol/sdk/shared/uriTemplate.js`) — `expand` /\n  `match` semantics carry over; import it from `@modelcontextprotocol/server` or\n  `@modelcontextprotocol/client` (top-level export; the codemod rewrites the path).\n- `StreamableHTTPClientTransport`, `SSEClientTransport` constructors and options —\n  including resumability: the per-request `resumptionToken` / `onresumptiontoken`\n  request options carry over from v1 unchanged\n  ([Resume a dropped stream](https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.md#resume-a-dropped-stream)).\n- `StdioClientTransport` and `StdioServerTransport` — **import path moved** to the\n  `./stdio` subpath and gained an optional `maxBufferSize` ([Imports & transports](imports-transports.md#imports--transports)).\n- The **`Transport` interface contract** — `start` / `send` / `close`, `onmessage` /\n  `onclose` / `onerror`, optional `sessionId` and `setProtocolVersion`,\n  `TransportSendOptions`, `MessageExtraInfo`. Hand-rolled v1 transports (recording\n  wrappers, test doubles, decorators) compile and run against v2 with only the import\n  path updated. v2 adds **optional** members only — `hasPerRequestStream` and\n  `setSupportedProtocolVersions` on the interface, `requestSignal` / `headers` /\n  `onRequestStreamEnd` on `TransportSendOptions` — which matter only for 2026-era\n  per-request-stream cancellation and `Mcp-Param-*` header attachment\n  ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)).\n- All TypeScript **type** definitions from `types.ts` (except the aliases listed under\n  [Removed type aliases](types-schemas.md#removed-type-aliases) and the `experimental` capability\n  payload narrowing — see [Types & schemas](types-schemas.md#types--schemas)).\n- Tool, prompt, and resource callback return types.\n\n> The `Server` (low-level) constructor and **most** of its methods are unchanged, but\n> `setRequestHandler` / `setNotificationHandler` and `request()` signatures changed\n> ([Low-level protocol](protocol-ctx.md#low-level-protocol--handler-context-ctx)). In particular,\n> `Server.createElicitationCompletionNotifier()` is unchanged — including its\n> construction-time client-capability check — for 2025-era URL-mode elicitation\n> ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The Zod `*Schema`\n> constants are **not** part of the unchanged surface — they moved to\n> `@modelcontextprotocol/core` ([Types & schemas](types-schemas.md#types--schemas)).\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThe principles this SDK is built and reviewed by. They are not a checklist: apply them with judgement.\n\n1. **Minimalism** — The SDK should do less, not more: prefer changes that add no API or option unless justified.\n2. **Spec is the anchor** — The spec decides; where it leaves a choice open, check what the other official SDKs do before making your own choice.\n3. **Strict by default** — Accept what the spec allows, and add no leniency for peers that break it.\n4. **Backwards compatible** — New features and changes arrive in backwards compatible ways; when a bug, a spec violation or an unsafe default forces a break, the changeset says what breaks.\n5. **Small changes** — Guard scope hard: a smaller change that lands beats a bigger one that doesn't, and small, independent pull requests can land in any order.\n6. **The words are true** — Changesets, docs and comments say what the code does.\n7. **Test what users see** — Prefer tests that cover the end-to-end usage a user would actually see, where sensible; `test/e2e/requirements.ts` lists those behaviours.\n8. **Brief comments** — Keep inline comments to one line and JSDoc to the public API; the reasoning goes in the commit message or PR description.\n\nStyle belongs to Prettier, ESLint and the compiler.\n","examples/guides/servers/skills/acme/billing/refunds/SKILL.md":"---\nname: refunds\ndescription: Process customer refunds.\n---\n\n# Refunds\n","examples/guides/servers/skills/git-workflow/SKILL.md":"---\nname: git-workflow\ndescription: Branching and review conventions.\n---\n\n# Git workflow\n\nBefore opening a pull request, go through [the review checklist](references/REVIEW.md).\n","examples/guides/servers/skills/git-workflow/references/REVIEW.md":"# Review checklist\n\n- Tests pass.\n","examples/scoped-tools/README.md":"# scoped-tools — per-tool scope enforced in the tool handler\n\nDemonstrates per-tool OAuth scope enforcement on a `createMcpHandler`\ndeployment: the HTTP gate does **bearer-verify + 401 only**, and each tool\nhandler checks `ctx.http?.authInfo?.scopes` for the scope it needs. The scope\ndecision lives next to the code it guards — the handler is the only place that\nauthoritatively knows which tool is executing — instead of in middleware that\nwould have to re-derive the operation from the request body.\n\n`server.ts` runs a minimal demo Authorization Server alongside the MCP Resource\nServer. `client.ts` connects with a `files:read` token, calls `list-files`\n(works), then calls `write-file` → the handler returns `{ isError: true }` with\n`insufficient_scope: requires files:write`.\n\nThe transport's automatic `403 insufficient_scope` **step-up** flow (SEP-2350 —\nscope union, refresh-bypass, `maxStepUpRetries`) applies when the RS responds\n`403` at the HTTP layer; that path is exercised by\n`test/e2e/scenarios/client-auth.test.ts`.\n\n```bash\npnpm --filter @mcp-examples/scoped-tools server -- --http --port 3000\npnpm --filter @mcp-examples/scoped-tools client -- --http http://127.0.0.1:3000/mcp\n```\n\n> DEMO ONLY — the bundled AS auto-approves and grants whatever scope is asked\n> for. Do not deploy.\n","examples/skills/README.md":"# skills\n\nThe server serves the `skills/` folder as MCP skills (SEP-2640) with one\n`loadSkills` call, under the `examples` prefix. The client lists the catalog,\nloads `git-workflow` (its `SKILL.md`, verified for size, digest and\nfrontmatter), follows the reference to `references/REVIEW.md`, browses the\n`refunds` folder, and catches a verification failure.\n\n```bash\npnpm --filter @mcp-examples/skills client            # modern leg over stdio\npnpm --filter @mcp-examples/skills client -- --legacy\npnpm --filter @mcp-examples/skills run server -- --http --port 3000\npnpm --filter @mcp-examples/skills client -- --http http://127.0.0.1:3000/mcp\n```\n\nSee [Serving skills](../../docs/servers/skills.md) and [Reading skills](../../docs/clients/skills.md).\n","examples/skills/package.json":"{\n    \"name\": \"@mcp-examples/skills\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"scripts\": {\n        \"server\": \"tsx server.ts\",\n        \"client\": \"tsx client.ts\"\n    },\n    \"dependencies\": {\n        \"@hono/node-server\": \"catalog:runtimeServerOnly\",\n        \"@mcp-examples/shared\": \"workspace:*\",\n        \"@modelcontextprotocol/client\": \"workspace:*\",\n        \"@modelcontextprotocol/hono\": \"workspace:*\",\n        \"@modelcontextprotocol/server\": \"workspace:*\"\n    },\n    \"devDependencies\": {\n        \"tsx\": \"catalog:devTools\"\n    },\n    \"example\": {\n        \"era\": \"dual\",\n        \"//\": \"Both legs list, load and verify skills, follow a reference and browse a skill folder.\"\n    }\n}\n","examples/skills/skills/acme/billing/refunds/SKILL.md":"---\nname: refunds\ndescription: Process a customer refund end to end.\nmetadata:\n    owner: billing\n---\n\n# Refunds\n\nCheck the order, issue the refund, then send [the refund email](templates/refund-email.md).\nFor disputed orders, use the escalation skill.\n","examples/skills/skills/acme/billing/refunds/escalation/SKILL.md":"---\nname: escalation\ndescription: Escalate a disputed refund to the billing lead.\n---\n\n# Escalation\n\nCollect the order number and the customer's reason, then page the billing lead.\n","examples/skills/skills/acme/billing/refunds/templates/refund-email.md":"Hello {{customer}},\n\nYour refund of {{amount}} is on its way.\n","examples/skills/skills/git-workflow/SKILL.md":"---\nname: git-workflow\ndescription: Branching, commit and review conventions for this team.\n---\n\n# Git workflow\n\n1. Branch from `main` as `feature/<topic>`.\n2. Keep commits small; write messages in the imperative.\n3. Before opening a pull request, go through [the review checklist](references/REVIEW.md).\n","examples/skills/skills/git-workflow/references/REVIEW.md":"# Review checklist\n\n- Tests pass locally.\n- A changeset describes the change.\n","examples/tools/README.md":"# tools\n\n**Start here.** Register tools with `McpServer.registerTool`; the SDK infers the JSON Schema from any Standard-Schema-compatible input (Zod here) and emits `structuredContent` matching `outputSchema`. The client lists tools, inspects schemas and `annotations`, calls them, and\nasserts structured output.\n\n```bash\npnpm tsx examples/tools/client.ts\n```\n","packages/client/README.md":"# `@modelcontextprotocol/client`\n\nThe MCP (Model Context Protocol) TypeScript client SDK. Build MCP clients that connect to MCP servers.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/client\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Client guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/connect)** — connecting, calling tools, OAuth, and middleware\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n","packages/codemod/README.md":"# @modelcontextprotocol/codemod\n\nCodemods for migrating MCP TypeScript SDK code between major versions.\n\n## Usage\n\n```bash\nnpx @modelcontextprotocol/codemod@latest v1-to-v2 .\n\n# or a single source file (manifest changes are reported, not applied)\nnpx @modelcontextprotocol/codemod@latest v1-to-v2 src/server.ts\n```\n\nThe codemod rewrites TypeScript and JavaScript source files\n(`.ts`/`.tsx`/`.mts`/`.cts`/`.js`/`.jsx`/`.mjs`/`.cjs`) in place. Run it on a clean\nworking tree so you can review the diff.\n\n## What `v1-to-v2` covers\n\nThe mechanical rename mappings are the source of truth — see\n`src/migrations/v1-to-v2/mappings/`:\n\n- [`importMap.ts`](./src/migrations/v1-to-v2/mappings/importMap.ts) —\n  `@modelcontextprotocol/sdk/...` import paths → v2 packages\n- [`symbolMap.ts`](./src/migrations/v1-to-v2/mappings/symbolMap.ts) —\n  symbol renames (`McpError` → `ProtocolError`, …)\n- [`schemaToMethodMap.ts`](./src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts) —\n  `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`\n- [`contextPropertyMap.ts`](./src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) —\n  `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*`\n\nTransforms in `src/migrations/v1-to-v2/transforms/` also rewrite `.tool()` →\n`registerTool` (wrapping `inputSchema` / `outputSchema` / `argsSchema` / `uriSchema`\nraw shapes with `z.object()`), drop the result-schema argument from `client.request()`\n/ `client.callTool()` for spec methods, route spec `*Schema` imports to\n`@modelcontextprotocol/core`, rename\n`StreamableHTTPError` → `SdkHttpError` / `IsomorphicHeaders` → `Headers`, rewrite\n`SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`, route\n`ErrorCode.{RequestTimeout,ConnectionClosed}` to `SdkErrorCode` (rewriting an\nall-SDK condition's `instanceof ProtocolError` guard to `SdkError`, and marking\nguards that mix the two enums), add `import { z } from 'zod'` when a wrap needs\nit, rewrite `vi.mock`\n/ `jest.mock` / dynamic `import()` paths, invert optional completable nesting\n(`completable(schema.optional(), cb)` becomes `completable(schema, cb).optional()`),\nand rewrite `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\nclient or server package root.\n\n## `@mcp-codemod-error` markers\n\nWhen the codemod recognizes a v1 pattern but cannot safely rewrite it (ambiguous\ncontext, removed API with no mechanical replacement, signature change requiring\njudgment), it leaves the code unchanged and inserts a comment:\n\n```typescript\n/* @mcp-codemod-error WebSocketClientTransport removed in v2. Use StreamableHTTPClientTransport or StdioClientTransport. */\n```\n\nAfter running the codemod, find every site that needs attention:\n\n```bash\ngrep -rn '@mcp-codemod-error' .\n```\n\n## What it does NOT cover\n\nCJS→ESM / Node 20 pre-flight, header **read** rewrites (`ctx.http?.req?.headers`\nbracket access → `.get()`; sending plain-record headers keeps working), OAuth\nerror-class consolidation (`instanceof InvalidGrantError` → `OAuthError` +\n`OAuthErrorCode`), per-scenario `SdkErrorCode` branch selection, `ctx.mcpReq.send()`\nschema-arg drop, and behavioral adaptation are manual — see the\n[migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2) for what to do after the\ncodemod runs.\n\nThe codemod handles the v1→v2 SDK surface upgrade only. Adopting the 2026-07-28\nprotocol revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`)\nis architectural and not codemod-automatable — see\n[docs/migration/support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28).\n","packages/codemod/batch-test/README.md":"# Codemod Batch Test\n\nTests the v1-to-v2 codemod against real-world repos to find bugs, missing transforms, and gaps.\n\n## How it works\n\nFor each repo in `repos.json`, the batch test:\n\n1. Clones the repo (or resets an existing clone)\n2. Installs dependencies\n3. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy\n4. Runs the codemod — in-process via the programmatic API (`--codemod=local`, the default), or by shelling out to the published CLI via `npx` (`--codemod=published`)\n5. Packs local SDK packages as tarballs and rewrites `package.json` deps to use them, so the test runs against the current SDK branch — only when `--sdk=local` (the default); with `--sdk=published` the v2 deps are installed from npm instead (see [Modes](#modes))\n6. Re-installs dependencies\n7. Re-runs the same checks\n8. Writes structured JSON reports\n\nErrors that appear in step 7 but not step 3 are codemod-introduced regressions.\n\n## Usage\n\n```bash\n# Build all SDK packages first (tarballs need built dist/)\npnpm build:all\n\n# Run the batch test\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# Clean cloned repos, results, and tarballs\npnpm --filter @modelcontextprotocol/codemod batch-test:clean\n```\n\n## Modes\n\nEach run independently chooses where the **SDK packages** and the **codemod** come from:\n\n| Flag                       | Values                 | Default   | Effect                                                                                                              |\n| -------------------------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |\n| `--sdk`                    | `local` \\| `published` | `local`   | `local`: pack local packages and rewrite deps to `file:` tarballs. `published`: install the v2 deps from npm.       |\n| `--codemod`                | `local` \\| `published` | `local`   | `local`: run the working-copy codemod in-process. `published`: `npx` the published CLI.                             |\n| `--codemod-version <spec>` | npm version/tag/range  | `latest`  | Only with `--codemod=published`.                                                                                    |\n| `--sdk-version <spec>`     | npm version/tag/range  | _(unset)_ | Only with `--sdk=published`. Pins each v2 dep to its own resolved version. Unset = use whatever the codemod writes. |\n\nBoth `--flag value` and `--flag=value` forms work.\n\n```bash\n# default (today's behavior)\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# both from npm @latest\npnpm --filter @modelcontextprotocol/codemod batch-test -- --sdk=published --codemod=published\n```\n\nPublished specs are resolved to concrete versions via `npm view` at startup. A failure resolving the codemod version or the representative SDK label aborts the run; a per-package `--sdk-version` miss only warns and leaves that package on the codemod-written range. Each\n`@modelcontextprotocol/*` package is resolved **independently** — they are not released in lockstep. Results are written to a per-run directory keyed on the resolved versions; the SDK segment is the resolved `@modelcontextprotocol/server` version used as a representative label,\nwhile the full per-package set is recorded in `summary.json` → `sdkVersions`:\n\n```\nresults/codemod-local__sdk-local/                  # default (--sdk=local --codemod=local)\nresults/codemod-2.0.0-alpha.2__sdk-2.0.0-alpha.2/  # --codemod=published --sdk=published --sdk-version=2.0.0-alpha.2\nresults/codemod-2.0.0-alpha.2__sdk-from-codemod/   # --codemod=published --sdk=published  (no --sdk-version)\n```\n\nThe segment `sdk-from-codemod` appears when `--sdk=published` with no `--sdk-version` and `--codemod=published` (the SDK version is baked into the published codemod and only known after install; the installed versions are recorded in `summary.json` → `sdkVersions`).\n\n**Limitation:** in published-codemod mode the CLI emits text, not structured diagnostics, so `codemod.diagnostics` is empty and the raw CLI output is captured under `codemod.cli` instead. Diagnostics categorization applies only to local-codemod runs.\n\n## Output\n\nResults are written to a per-run directory keyed on the resolved versions, `batch-test/results/<config>/`, where `<config>` is the `codemod-…__sdk-…` leaf (distinct `--codemod-version`/`--sdk-version` values produce distinct directories even within the same `--sdk`/`--codemod`\nmode; see [Modes](#modes)):\n\n- `results/<config>/summary.json` — overview across all repos: which passed, which failed, error counts, plus the run `config` and the resolved per-package `sdkVersions`\n- `results/<config>/<repo-slug>/report.json` — per-repo detail: baseline vs post-codemod check results, codemod diagnostics, change counts\n\n## Repo manifest (`repos.json`)\n\nAn array of repo entries. Each entry specifies a GitHub repo and one or more packages within it.\n\n```json\n{\n    \"repo\": \"owner/repo-name\",\n    \"ref\": \"main\",\n    \"packages\": [\n        {\n            \"dir\": \"packages/mcp-server\",\n            \"sourceDir\": \"src\",\n            \"checks\": {\n                \"typecheck\": \"npx tsc --noEmit\",\n                \"build\": \"npm run build\",\n                \"test\": \"npm run test\",\n                \"lint\": null\n            }\n        }\n    ]\n}\n```\n\n| Field                  | Required | Default                                | Description                                                       |\n| ---------------------- | -------- | -------------------------------------- | ----------------------------------------------------------------- |\n| `repo`                 | yes      | —                                      | GitHub `owner/name`                                               |\n| `ref`                  | no       | `main`                                 | Branch or tag to clone                                            |\n| `packages`             | no       | `[{ \"dir\": \".\", \"sourceDir\": \"src\" }]` | Package targets within the repo                                   |\n| `packages[].dir`       | yes      | —                                      | Path to package root (where `package.json` lives)                 |\n| `packages[].sourceDir` | no       | `src`                                  | Source directory relative to `dir` (passed to codemod)            |\n| `packages[].checks`    | no       | auto-detect                            | Override check commands; set a value to `null` to skip that check |\n\nWhen `checks` is omitted, the runner auto-detects commands from the package's `package.json` scripts (probing names like `typecheck`, `build`, `test`, `lint`). The package manager is auto-detected from the lockfile at the repo root.\n\n## Analyzing results\n\n`analyze-prompt.md` contains instructions for Claude Code to run the batch test and produce a categorized analysis. Each error is classified as:\n\n| Category            | Meaning                                            |\n| ------------------- | -------------------------------------------------- |\n| `codemod-bug`       | A transform produced incorrect output              |\n| `missing-transform` | The codemod should handle this pattern but doesn't |\n| `manual-migration`  | Expected — requires human judgment                 |\n| `repo-specific`     | Unusual pattern not worth handling in the codemod  |\n\n## Adding a repo\n\n1. Edit `repos.json` and add an entry\n2. Run `pnpm --filter @modelcontextprotocol/codemod batch-test`\n3. Check `results/<config>/<repo-slug>/report.json` for new findings\n\nFor monorepos, list each package that uses `@modelcontextprotocol/sdk` as a separate entry in `packages`.\n\n## Iteration workflow\n\n```\n1. Run the batch test\n2. Review results — identify codemod bugs / missing transforms\n3. Fix the codemod transforms\n4. Run batch-test:clean, then re-run the batch test\n5. Confirm the fixes resolved the issues\n6. Repeat\n```\n","packages/core-internal/src/types/README.md":"# Spec reference types (\"anchors\")\n\nThe `spec.types.<revision>.ts` files in this directory are vendored, verbatim copies of the MCP specification's normative `schema.ts`, one file per protocol revision. Each file is generated by `pnpm run fetch:spec-types [version] [sha]` (`scripts/fetch-spec-types.ts`): the\nupstream schema is fetched at a specific spec commit, a provenance header recording that commit is prepended, and the result is formatted with the project's prettier config — no other transformation.\n\nThey are reference-only test oracles: the comparison suites in `packages/core-internal/test/spec.types.<revision>.test.ts` check the SDK's own types against them. They are not exported from any barrel and must never be imported by runtime code.\n\n## Lifecycle policy\n\n1. **Released revisions are frozen.** Once a protocol revision is published under `schema/<revision>/` in the spec repository, its anchor regenerates only from the pinned spec commit recorded in `RELEASED_REVISION_PINS` (`scripts/fetch-spec-types.ts`) — never from the latest\n   upstream commit. Moving that pin, including the freeze of a newly published revision (when its generation source switches from `schema/draft/` to `schema/<revision>/`), must land atomically with every checked-in oracle derived from that revision.\n\n2. **Draft anchors float only while they have an explicit consumer.** An unreleased revision may track the spec repository's `schema/draft/schema.ts`, but refreshes must go through reviewed PRs and update all derived oracles atomically. No currently supported revision uses this mode.\n\n3. **The bot proposes; it never auto-merges.** Automated refreshes always go through a pull request that a maintainer reviews and merges. No automation pushes anchor changes directly to `main` or merges its own PRs. A refresh PR that breaks the comparison suites is the desired\n   signal — it is fixed in that PR, not bypassed.\n\n4. **Generated twins update atomically with their anchor.** If artifacts derived from an anchor (for example vendored JSON schemas or generated validators) are checked into this repository, any refresh that changes the anchor must regenerate those artifacts in the same commit.\n   The anchor and its derived twins must never be out of sync at any commit on `main`.\n\n    **This clause is OPERATIVE.** The vendored twins are the per-revision `schema.json` copies under `packages/core-internal/test/corpus/schema-twins/` (`<revision>.schema.json` + `manifest.json` recording each twin's source commit and content hashes). They are TEST-ONLY oracles consumed by the\n    schema-twin conformance lock (`test/wire/schemaTwinConformance.test.ts`) — never bundled, never imported by runtime code, and the JSON Schema engines stay optional peer dependencies. A refresh of `spec.types.<revision>.ts` must refresh the matching upstream\n    `schema/<dir>/schema.json` from that twin's recorded source commit and update `manifest.json` in the same change; the spec example corpus manifest (`test/corpus/fixtures/<revision>/manifest.json`) records its own source commit and follows the same atomicity rule when the examples\n    are re-vendored. The conformance lock failing after an anchor-only refresh is the desired loud signal of a missed twin update.\n","packages/core/README.md":"# @modelcontextprotocol/core\n\nCanonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification and OAuth/OpenID **Zod schemas**.\n\nThese are the exact schema constants the SDK validates protocol and OAuth/OpenID payloads against internally. The `@modelcontextprotocol/server` and `@modelcontextprotocol/client` packages keep a Zod-free public surface, so this package exists as the supported place to import the\nraw schemas when you need to validate or parse MCP messages yourself.\n\n## Install\n\n```sh\nnpm install @modelcontextprotocol/core\n```\n\n## Usage\n\n```ts\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\n\n// Throws on invalid input; returns the typed result on success.\nconst result = CallToolResultSchema.parse(payload);\n\n// Or non-throwing:\nconst parsed = CallToolResultSchema.safeParse(payload);\nif (parsed.success) {\n    // parsed.data is a fully typed CallToolResult\n}\n```\n\n## Scope\n\nThis package exports **only** Zod schema constants (`*Schema`), in two groups:\n\n- the MCP **spec** schemas — `CallToolResultSchema`, `ListToolsResultSchema`, …; and\n- the **OAuth/OpenID** auth schemas — `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`, … (the schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js`).\n\nThe corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and\n[`@modelcontextprotocol/client`](https://www.npmjs.com/package/@modelcontextprotocol/client).\n\n> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js` (spec schemas) and `@modelcontextprotocol/sdk/shared/auth.js` (OAuth/OpenID schemas). Point those `*Schema` imports at `@modelcontextprotocol/core` and your existing `.parse()` /\n> `.safeParse()` calls keep working unchanged.\n","packages/middleware/README.md":"# Middleware packages\n\nThe packages in `packages/middleware/*` are **thin integration layers** that help you expose an MCP server in a specific runtime, platform, or web framework.\n\nThey intentionally **do not** add new MCP features or “business logic”. MCP functionality (tools, resources, prompts, transports, auth primitives, etc.) lives in `@modelcontextprotocol/server` (and other core packages). Middleware packages should primarily:\n\n- adapt request/response types to the SDK (e.g. Node.js `IncomingMessage`/`ServerResponse`)\n- provide small framework helpers (e.g. wiring, body parsing hooks)\n- supply safe defaults for common deployment pitfalls (e.g. localhost DNS rebinding protection)\n\n## Packages\n\n- `@modelcontextprotocol/express` — Express helpers (app defaults + Host header validation for DNS rebinding protection).\n- `@modelcontextprotocol/fastify` — Fastify helpers (app defaults + Host header validation).\n- `@modelcontextprotocol/hono` — Hono helpers (app defaults + JSON body parsing hook + Host header validation).\n- `@modelcontextprotocol/node` — Node.js Streamable HTTP transport wrapper for `IncomingMessage`/`ServerResponse`.\n\n## Typical usage\n\nMost servers use:\n\n- `@modelcontextprotocol/server` for the MCP server implementation\n- one middleware package for framework/runtime integration (this folder)\n- (optionally) additional platform/framework dependencies (Express, Hono, etc.)\n","packages/middleware/express/README.md":"# `@modelcontextprotocol/express`\n\nExpress adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Express integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create an Express app with sensible defaults for MCP servers\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n- protect routes with `requireBearerAuth` (validates `Authorization: Bearer …` via your `OAuthTokenVerifier`)\n- serve OAuth Protected Resource Metadata (RFC 9728) via `mcpAuthMetadataRouter`\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/express express\n\n# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):\nnpm install @modelcontextprotocol/node\n```\n\n## Exports\n\n- `createMcpExpressApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n- `requireBearerAuth(options)`\n- `mcpAuthMetadataRouter(options)`\n- `getOAuthProtectedResourceMetadataUrl(serverUrl)`\n- `OAuthTokenVerifier` (interface)\n\n## Usage\n\n### Create an Express app (localhost DNS rebinding protection by default)\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\n\nconst app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled\n```\n\n### Streamable HTTP endpoint (Express)\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpExpressApp();\n\napp.post('/mcp', async (req, res) => {\n    // Stateless example: create a server and a transport per request (with sessions: one pair per session).\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    await transport.handleRequest(req, res, req.body);\n});\n```\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { hostHeaderValidation } from '@modelcontextprotocol/express';\n\napp.use(hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));\n```\n","packages/middleware/fastify/README.md":"# `@modelcontextprotocol/fastify`\n\nFastify adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Fastify integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create a Fastify app with sensible defaults for MCP servers\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/fastify fastify\n\n# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):\nnpm install @modelcontextprotocol/node\n```\n\n## Exports\n\n- `createMcpFastifyApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n\n## Usage\n\n### Create a Fastify app (localhost DNS rebinding protection by default)\n\n```ts\nimport { createMcpFastifyApp } from '@modelcontextprotocol/fastify';\n\nconst app = createMcpFastifyApp(); // default host is 127.0.0.1; protection enabled\n```\n\n### Streamable HTTP endpoint (Fastify)\n\n```ts\nimport { createMcpFastifyApp } from '@modelcontextprotocol/fastify';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpFastifyApp();\n\napp.post('/mcp', async (request, reply) => {\n    // Stateless example: create a server and a transport per request (with sessions: one pair per session).\n    const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await mcpServer.connect(transport);\n\n    // Clean up when the client closes the connection (e.g. during SSE streaming).\n    reply.raw.on('close', () => {\n        transport.close();\n        mcpServer.close();\n    });\n\n    await transport.handleRequest(request.raw, reply.raw, request.body);\n});\n```\n\nTo reject non-POST requests with 405 Method Not Allowed, add routes for GET and DELETE that send a JSON-RPC error response.\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { hostHeaderValidation } from '@modelcontextprotocol/fastify';\n\napp.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));\n```\n","packages/middleware/hono/README.md":"# `@modelcontextprotocol/hono`\n\nHono adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Hono integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create a Hono app with sensible defaults for MCP servers\n- parse JSON request bodies and expose them as `c.get('parsedBody')` for Streamable HTTP transports\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/hono hono\n```\n\n## Exports\n\n- `createMcpHonoApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n\n## Usage\n\n### Streamable HTTP endpoint (Hono)\n\n```ts\nimport { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';\nimport { createMcpHonoApp } from '@modelcontextprotocol/hono';\n\nconst app = createMcpHonoApp();\napp.all('/mcp', async c => {\n    // Stateless example: create a server and a transport per request.\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    return transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') });\n});\n```\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { localhostHostValidation } from '@modelcontextprotocol/hono';\n\nconst app = createMcpHonoApp();\napp.use('*', localhostHostValidation());\n```\n","packages/middleware/node/README.md":"# `@modelcontextprotocol/node`\n\nNode.js adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Node.js integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server). It provides a Streamable HTTP transport that works with Node’s `IncomingMessage` / `ServerResponse`.\n\nFor web‑standard runtimes (Cloudflare Workers, Deno, Bun, etc.), use `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly.\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/node\n```\n\n## Exports\n\n- `NodeStreamableHTTPServerTransport`\n- `StreamableHTTPServerTransportOptions` (type alias for `WebStandardStreamableHTTPServerTransportOptions`)\n- `toNodeHandler(handler, opts?)` — adapt a web-standard `{ fetch }` MCP handler to a Node `(req, res, parsedBody?)` handler\n- `hostHeaderValidation(allowedHostnames)` / `localhostHostValidation()` — `Host` header guards for hand-wired `node:http` servers\n- `originValidation(allowedOriginHostnames)` / `localhostOriginValidation()` — `Origin` header guards for hand-wired `node:http` servers\n- `ToNodeHandlerOptions`, `FetchLikeMcpHandler`, `NodeMcpRequestHandler` (types for `toNodeHandler`)\n- `toWebRequest(req, parsedBody?, opts?)` — the Node `IncomingMessage` → web-standard `Request` conversion `toNodeHandler` performs internally, exported on its own (for example to feed `isLegacyRequest()` from a hand-wired `(req, res)` handler)\n- `ToWebRequestOptions` (options type for `toWebRequest`)\n- `NodeIncomingMessageLike`, `NodeServerResponseLike` (structural Node request/response shapes)\n\n## Usage\n\n### Express + Streamable HTTP\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpExpressApp();\n\napp.post('/mcp', async (req, res) => {\n    // Stateless example: create a server and a transport per request.\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n\n    // If you use Express JSON parsing, pass the pre-parsed body to avoid re-reading the stream.\n    await transport.handleRequest(req, res, req.body);\n});\n```\n\n### Node.js `http` server\n\nPlain `node:http` has no middleware chain, so bind loopback explicitly and\ncompose the `Host`/`Origin` guards in front of the transport — matching the\ndefaults the framework app factories (`createMcpExpressApp`,\n`createMcpHonoApp`, `createMcpFastifyApp`) apply for you. The guards answer\nrejected requests with `403` themselves and return `false`, so the handler\nmust not touch the request further.\n\n```ts\nimport { createServer } from 'node:http';\nimport { localhostHostValidation, localhostOriginValidation, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst validateHost = localhostHostValidation();\nconst validateOrigin = localhostOriginValidation();\n\ncreateServer(async (req, res) => {\n    if (!validateHost(req, res) || !validateOrigin(req, res)) return;\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    await transport.handleRequest(req, res);\n}).listen(3000, '127.0.0.1');\n```\n","packages/server-legacy/README.md":"# @modelcontextprotocol/server-legacy\n\n> **Deprecated** — This package is a frozen copy of v1 code for migration purposes only. It will not receive new features and is planned for removal in v3.\n\nProvides two pieces of v1 server functionality removed in v2:\n\n- **SSE Transport** (`./sse`) — The `SSEServerTransport` class, replaced in v2 by `NodeStreamableHTTPServerTransport` (from `@modelcontextprotocol/node`) or `WebStandardStreamableHTTPServerTransport` (from `@modelcontextprotocol/server`)\n- **OAuth Authorization Server** (`./auth`) — The `mcpAuthRouter` and related helpers, removed in v2 because MCP servers should use dedicated OAuth providers\n\n## Installation\n\n```bash\nnpm install @modelcontextprotocol/server-legacy\n```\n\n## Usage\n\n### SSE Transport (no Express dependency required)\n\n```ts\nimport { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse';\n```\n\n### OAuth Auth Router (requires Express)\n\n```ts\nimport { mcpAuthRouter } from '@modelcontextprotocol/server-legacy/auth';\n```\n\n### Everything (requires Express)\n\n```ts\nimport { SSEServerTransport, mcpAuthRouter } from '@modelcontextprotocol/server-legacy';\n```\n\n## Migration\n\n- **SSE → StreamableHTTP**: Use `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` (Node.js) or `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` (Web Standard / Cloudflare Workers)\n- **Auth router → Dedicated IdP**: Use a dedicated OAuth provider (Auth0, Keycloak, etc.) instead of the built-in OAuth Authorization Server\n","packages/server/README.md":"# `@modelcontextprotocol/server`\n\nThe MCP (Model Context Protocol) TypeScript server SDK. Build MCP servers that expose tools, resources, and prompts.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\nOptional framework adapters: [`@modelcontextprotocol/express`](https://www.npmjs.com/package/@modelcontextprotocol/express), [`@modelcontextprotocol/fastify`](https://www.npmjs.com/package/@modelcontextprotocol/fastify), [`@modelcontextprotocol/hono`](https://www.npmjs.com/package/@modelcontextprotocol/hono),\n[`@modelcontextprotocol/node`](https://www.npmjs.com/package/@modelcontextprotocol/node).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Server guide](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools)** — tools, resources, prompts, and the rest of the server surface\n- **[Serving guide](https://ts.sdk.modelcontextprotocol.io/v2/serving/http)** — stdio, HTTP, the framework adapters, sessions, and authorization\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n","skills/mcp-typescript-sdk-skills/SKILL.md":"---\nname: mcp-typescript-sdk-skills\ndescription: 'Serves and consumes Skills over MCP (SEP-2640, the `io.modelcontextprotocol/skills` extension) with v2 of the MCP TypeScript SDK: publishes a folder of `SKILL.md` skills from an MCP server with `loadSkills` and `SkillsExtension`, and lists, verifies and loads them in a client or host with `SkillsClient`. Use whenever a TypeScript project on `@modelcontextprotocol/server` or `@modelcontextprotocol/client` 2.x wants to expose agent skills from its MCP server, serve `skill://` resources or `skills/list`, or load skills from the MCP servers it connects to, even if the user does not say \"SEP-2640\" or \"extension\". Not for projects still on v1 (`@modelcontextprotocol/sdk`: upgrade them with the `mcp-typescript-sdk-upgrade-to-v2` skill first), for MCP SDKs in other languages (Python, Go, C#), or for writing the content of a `SKILL.md`.'\n---\n\n# Skills over MCP with the TypeScript SDK\n\nSkills over MCP lets an MCP server publish agent skills (folders with a `SKILL.md`) and lets clients list, verify and load them. In v2 of the TypeScript SDK it ships as the `ext/skills` subpaths of `@modelcontextprotocol/server` and `@modelcontextprotocol/client`.\n\nThe goal is working, verified code in the user's project. Use only the APIs shown here and in the references, and skip explaining the protocol unless the user asks.\n\n## Which side the project is on\n\nRead `package.json` and the source before writing anything:\n\n- **Server:** depends on `@modelcontextprotocol/server` and creates an `McpServer`, directly or in a factory passed to `serveStdio` or `createMcpHandler`. It publishes skills: follow [Serve skills](#serve-skills).\n- **Client or host:** depends on `@modelcontextprotocol/client` and creates a `Client`. It loads skills from the servers it connects to: follow [Use skills](#use-skills).\n- **Both** (a host that also runs a server, a gateway): follow both, each against its own code. A `Client` that exists only in the project's tests does not make it a host: that is where [Verify](#verify) runs.\n- **v1** (`@modelcontextprotocol/sdk`): Skills over MCP exists only in v2. Upgrade first with the `mcp-typescript-sdk-upgrade-to-v2` skill, then come back.\n\n## Check that the installed SDK has skills\n\nSkills over MCP ships in the `./ext/skills` export of the v2 packages, and an import of a subpath the installed package lacks fails only at build or run time. Check first, from the package that depends on the SDK (use `client` in place of `server` for a client):\n\n```bash\ngrep -c '\"./ext/skills\"' node_modules/@modelcontextprotocol/server/package.json\n```\n\nIf it prints `0`, check the latest release:\n\n```bash\nnpm view @modelcontextprotocol/server@latest exports --json | grep -c '\"./ext/skills\"'\n```\n\n- **The latest release has it:** upgrade (`server` and `client` to the same version, if the project uses both) and continue.\n- **It does not:** stop here. Tell the user that Skills over MCP is not in a released version of the SDK yet, and that this works once a release exports `@modelcontextprotocol/server/ext/skills`. Do not write `skills/list` handlers or `skill://` resources by hand as a workaround. Clients verify every file against the digests and frontmatter the extension publishes, and the extension also declares the capability, pages the catalog and handles both protocol versions, so a hand-written copy is easy to get wrong in ways those clients refuse, and drifts from the spec.\n\n## Serve skills\n\n**Pick where the skills come from.** Each source gives you an extension that you attach as shown below; only the first three have `reload()`:\n\n- **A folder on disk** (Node): `loadSkills`, which the steps below walk through.\n- **Skills defined in code** (Workers and other runtimes without a file system, tests): `memorySkillFiles`. See [Serve skills defined in code](references/server.md#serve-skills-defined-in-code).\n- **Your own storage** (an object store, a database, a git repository): a `SkillFiles` adapter. See [Serve skills from your own storage](references/server.md#serve-skills-from-your-own-storage).\n- **Skills that differ per caller or are generated per request:** a `SkillSource`. See [Generate skills with a `SkillSource`](references/server.md#generate-skills-with-a-skillsource).\n\n**Lay out the folder.** Each folder holding a `SKILL.md` is one skill. Its frontmatter needs a `name` equal to the folder's name and a `description`. Other folders only group skills in the URI.\n\n**Load the folder once and attach it before the server connects.** `attach` throws on a connected server, and each server takes one skills extension. A server connected by hand keeps its `server.connect(transport)` call, after `attach`.\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\n**Resolve the folder from the module, not the working directory.** A relative path like `'./skills'` resolves against `process.cwd()`, and MCP hosts may start stdio servers from another directory (such as `/`), where the load throws and takes the whole server down with it. Build the path from the module's location instead: `fileURLToPath(new URL('../skills', import.meta.url))` from `src/index.ts`, or `path.join(__dirname, '../skills')` in CommonJS.\n\n**Where a factory builds the server, attach inside it**, so every server it builds serves the same catalog. That covers `serveStdio(createServer)` on stdio as well as `createMcpHandler` over HTTP:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\n**Pick up edits with `reload()`** if the user expects changes to show without a restart. Call it from whatever tells the project the folder changed (a file watcher, an admin route, a deploy hook). A server that stays connected (stdio) notifies its clients itself. Behind `createMcpHandler`, publish the change on the handler, where `changes` is what `await skills.reload()` returned:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nThen tell the user about any load warnings, and [verify](#verify).\n\nRead [references/server.md](references/server.md) for `onWarning`, `strict`, `limits`, `prefix` and `pageSize`.\n\n## Use skills\n\n**Wrap the connected client and list.** Listing reads no skill file: each entry carries the skill's frontmatter and file list, enough to show what is on offer.\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\n`skills.supported` is `false` for a server that does not declare the extension; a host that connects to several servers checks it per server before listing. `list()` throws an `SdkError` if any entry is invalid, so a host listing servers it does not control creates the client with `new SkillsClient(client, { skipInvalid: true })` and logs the refused entries it returns in `invalid`.\n\n**Load a skill only after the user (or the host's policy) approves it.** Read its `SKILL.md`, which checks size, digest and frontmatter, and give `manifest.body` to the model:\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nRead the files `SKILL.md` refers to with `skills.read(entry, path)` when the model needs them. A failed check throws `SkillVerificationError`: do not use that file, and if it is the `SKILL.md`, do not load the skill.\n\nRead [references/client.md](references/client.md) for `skipInvalid`, verification reasons, `readDirectory`, `get`, dynamic skills and caching hints.\n\n## Mistakes to avoid\n\nServing:\n\n- **Writing the protocol by hand.** Serve skills only through `SkillsExtension` (`loadSkills`, `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource`). Never `setRequestHandler('skills/list', …)`, and never register your own resources under `skill://`: a resource at a skill file's URI takes precedence over the file, and clients then fail to verify it.\n- **Missing the skipped skills.** `loadSkills` skips a skill whose `name` differs from its folder, or that contains a symlink, and only warns (on stderr by default) while the others serve. Read the warnings after loading and tell the user which skills were skipped. Pass `strict: true` (in CI, say) to make the load throw instead.\n- **Dot-named entries.** Files and folders whose names start with `.` are skipped with no warning at all, and `strict: true` does not catch them. A skill in a `.name` folder, or a dotfile its `SKILL.md` refers to, is simply missing: rename it.\n- **A catalog outside the factory's servers.** `serveStdio` and `createMcpHandler` build their servers from a factory, and each server `createMcpHandler` builds lives for one request. Attach inside the factory, and behind `createMcpHandler` call `handler.notify.resourcesChanged()` when `reload()` reports `listChanged`, or connected clients never hear of the change.\n- **Expecting edits to show up on their own.** The catalog keeps the digests from load time, so a file edited on disk afterwards is refused with an error until `reload()` runs.\n- **`loadSkills` without a file system.** It reads the disk (`ext/skills/node`). On Workers and similar runtimes, use `memorySkillFiles` or a `SkillFiles` adapter from `@modelcontextprotocol/server/ext/skills`.\n\nLoading:\n\n- **Raw `resources/read`.** Never fetch skill files with `client.readResource()`: it skips the size, digest and frontmatter checks. Use `skills.read()`.\n- **Reading while listing.** Read a skill's files when it is loaded, never while listing.\n- **Skipping the host's duties.** Accept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before loading a skill from a server, and record which server each skill came from, so the instructions given to the model can be traced to their source.\n\nBoth:\n\n- **Invented APIs.** Take skills APIs only from `@modelcontextprotocol/server/ext/skills`, `@modelcontextprotocol/server/ext/skills/node` and `@modelcontextprotocol/client/ext/skills`, using names shown here or in the references. If you need something else, read the package's type declarations instead of guessing.\n\n## Verify\n\n**Server:** list every skill and read every file it lists through a real client. In a script or test, connect a `Client` to the server over an in-memory pair and read each file with `SkillsClient`. `read` throws when a file fails a check, and the loop throws when the server lists no skills:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#verify_inMemory\"\nimport { Client, InMemoryTransport } from '@modelcontextprotocol/client';\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();\nawait server.connect(serverTransport);\nconst client = new Client({ name: 'skills-check', version: '1.0.0' });\nawait client.connect(clientTransport);\n\nconst skillsClient = new SkillsClient(client);\nconst { skills: listed } = await skillsClient.list();\nif (listed.length === 0) throw new Error('the server listed no skills');\nfor (const entry of listed) {\n    const files = entry.resources === 'dynamic' ? [entry.uri] : entry.resources.map(file => file.uri);\n    for (const uri of files) await skillsClient.read(entry, uri);\n}\nconsole.log(`${listed.length} skill(s) listed and verified`);\n```\n\nAn entry whose `resources` is `\"dynamic\"` (only a `SkillSource` serves one) has no file list, so the loop reads only its `SKILL.md`: `read` checks the frontmatter but has no size or digest to check, and returns `verified: false`.\n\nFor a stdio server, the same loop works over `StdioClientTransport` (from `@modelcontextprotocol/client/stdio`) against the server's real start command; set its `cwd` option to another directory (such as `/`), as hosts do, so a path that depends on the working directory fails here and not in the user's host. Compare the number of skills listed with the number of skill folders, and account for each one missing: a warning, or a dot-name.\n\n**Client:** run the host against a server that serves at least one skill (the server path above takes a few lines, or `memorySkillFiles` in a test). Check that it lists the skill, asks before loading it, and reads its `SKILL.md` with `verified: true`.\n\n## References\n\n| Reference file                                          | Read it when                                  |\n| ------------------------------------------------------- | --------------------------------------------- |\n| [Serve skills from an MCP server](references/server.md) | The project serves skills from an MCP server  |\n| [Use skills from MCP servers](references/client.md)     | The project loads skills from its MCP servers |\n","skills/mcp-typescript-sdk-skills/references/client.md":"# Use skills from MCP servers\n\n<!-- #region guide -->\n\n`SkillsClient` lists a server's skills and reads their files, checking each file against what the server published.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\nAgainst a server with two skills, it prints each skill's name and description:\n\n```text\nrefunds: Process customer refunds.\ngit-workflow: Branching and review conventions.\n```\n\nListing reads no skill file: each **entry** carries the skill's frontmatter and its file list with digests, enough to decide which skill to load.\n\n## Handle refused entries\n\nCreate the client with `skipInvalid: true` to keep the valid skills and log the refused entries.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#list_invalid\"\nconst tolerant = new SkillsClient(client, { skipInvalid: true });\nconst { invalid } = await tolerant.list();\nfor (const refused of invalid) console.error(`skills: refused entry #${refused.index} ${refused.uri ?? ''}: ${refused.reason}`);\n```\n\nBy default `list()` throws an `SdkError` naming every entry that fails validation, and every copy of a repeated URI. With `skipInvalid: true` it returns the valid skills and puts those entries in `invalid`, each with `index`, `uri` when it has one, and `reason`. Against this server it is empty, so the loop prints nothing.\n\n## Load a skill\n\nRead the entry's `SKILL.md`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nThe file passes every check, and its frontmatter matches the entry:\n\n```text\ntrue {\n  name: 'git-workflow',\n  description: 'Branching and review conventions.'\n}\n```\n\n`read(entry)` checks the size and SHA-256 digest against the entry, then compares the frontmatter field by field. `manifest.body` holds the instructions to give the model.\n\nThe comparison parses the frontmatter with YAML 1.2 core-schema typing, so `yes` is the string `\"yes\"`, not `true`. If a server builds its entries with a parser that types values differently, pass a matching YAML parser as `parseFrontmatter` to `new SkillsClient`; it receives the YAML between the `---` lines.\n\n## Read the files a skill references\n\nPass the path `SKILL.md` uses.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_reference\"\nconst review = await skills.read(git, 'references/REVIEW.md');\nconsole.log(review.text);\n```\n\n`review.text` holds the verified file:\n\n```text\n# Review checklist\n\n- Tests pass.\n\n```\n\nA relative path resolves against the skill's folder, as a link in `SKILL.md` does, and `read` ignores a fragment. It matches the path to the entry's file list however either side percent-encodes the name, so `references/Q&A.md` works. `bytes` always holds the file's bytes; `text` holds the file's text when the server sent it as text, and always for the skill's own `SKILL.md`.\n\n## Handle verification failures\n\nCatch `SkillVerificationError`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillVerificationError\"\nimport { SkillVerificationError } from '@modelcontextprotocol/client/ext/skills';\n\ntry {\n    await skills.read(git, 'references/MISSING.md');\n} catch (error) {\n    if (!(error instanceof SkillVerificationError)) throw error;\n    console.log(error.reason, error.uri);\n}\n```\n\nThe entry does not list `references/MISSING.md`, so `read` refuses it before sending any request:\n\n```text\nunlisted skill://git-workflow/references/MISSING.md\n```\n\n`reason` is `unlisted`, `size`, `digest` or `frontmatter`; a path that does not parse is `unlisted` too. When a size, digest or frontmatter check fails on a copy the client served from its response cache, `read` fetches the file once more with `cacheMode: 'refresh'` and throws only if the fresh copy fails as well; a copy that came from the server fails at once. On any reason, do not use the file; for `SKILL.md`, do not load the skill.\n\n## Browse a skill folder\n\nList a folder's direct children when the server declares `directoryRead`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#readDirectory\"\nif (skills.directoryRead) {\n    const { resources: children } = await skills.readDirectory('skill://acme/billing/refunds');\n    console.log(children.map(child => `${child.name} ${child.mimeType}`).join('\\n'));\n}\n```\n\nThe `refunds` folder holds its `SKILL.md` and a `templates` folder:\n\n```text\nSKILL.md text/markdown\ntemplates inode/directory\n```\n\n## Get a skill by URI\n\nFetch one entry, listed or not.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#get\"\nconst { skill: refunds } = await skills.get('skill://acme/billing/refunds/SKILL.md');\nconsole.log(refunds.frontmatter.name);\n```\n\nIt prints the skill's name from the entry's frontmatter:\n\n```text\nrefunds\n```\n\nOn 2026-07-28, `list()`, `get()` and `readDirectory()` also return the server's caching hints, `ttlMs` and `cacheScope`, and throw an `SdkError` when a response leaves either out.\n\n::: info Dynamic skills\nAn entry whose `resources` is `\"dynamic\"` has no file list. `read` then skips the size and digest checks and returns `verified: false`, but still compares the `SKILL.md` frontmatter.\n:::\n\n::: warning Host responsibilities\nAccept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before you load a skill from a server, and record which server each skill came from. Read a skill's files when you load it, never while listing.\n:::\n\n## Recap\n\n- `new SkillsClient(client)` wraps a connected client; `list()` returns entries without reading files.\n- `list()` throws on an invalid entry; `skipInvalid: true` keeps the valid skills and returns the refused ones in `invalid`.\n- `read(entry)` verifies `SKILL.md` (size, digest, frontmatter) and returns its frontmatter and body.\n- `read(entry, path)` resolves a path against the skill's folder and verifies that file.\n- A failed check throws `SkillVerificationError` with a `reason`.\n\n<!-- #endregion guide -->\n","skills/mcp-typescript-sdk-skills/references/server.md":"# Serve skills from an MCP server\n\n<!-- #region guide -->\n\n`loadSkills` reads a folder of skills once; `attach` serves it on a server.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\nA **skill** is a folder with a `SKILL.md` and the files it references (SEP-2640). Clients list skills with `skills/list` and read each file with `resources/read`, checking it against the digest your server published.\n\n## Lay out the skills folder\n\nEach folder that contains a `SKILL.md` is one skill; other folders only group skills in the URI.\n\n```text\nskills/\n├── git-workflow/\n│   ├── SKILL.md\n│   └── references/REVIEW.md\n└── acme/billing/refunds/\n    └── SKILL.md\n```\n\n`SKILL.md` starts with YAML frontmatter holding `name` (the same as the skill's folder name) and `description`. Print the URIs the catalog publishes:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#entries\"\nconsole.log(skills.entries.map(skill => skill.uri).join('\\n'));\n```\n\nIt prints one `SKILL.md` URI per skill, sorted:\n\n```text\nskill://acme/billing/refunds/SKILL.md\nskill://git-workflow/SKILL.md\n```\n\n## Serve one catalog from every server\n\nAttach the same catalog to each server your handler creates.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\nThe catalog keeps metadata only (paths, digests, sizes, frontmatter); each read goes to the folder, and the SDK serves the bytes only if they still match the published digest. It sends a file as `text` whenever its bytes are valid UTF-8 (a BOM included), whatever its MIME type, and as a base64 `blob` otherwise.\n\n## Pick up changes with `reload()`\n\nRescan the folder and swap the new catalog in at once.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload\"\nconst changes = await skills.reload();\nconsole.log(changes);\n```\n\nWith nothing changed on disk, `reload()` reports no changes:\n\n```text\n{ added: [], removed: [], changed: [], listChanged: false }\n```\n\nUntil you call `reload()`, the server answers a read of a file edited on disk with an error instead of unverified content. If the rescan fails, `reload()` rejects and the old catalog keeps serving.\n\n`listChanged` is `true` when a skill appears, disappears, or changes its name or description. A server that stays connected (on stdio, or in a stateful 2025-11-25 HTTP session) then sends its clients `notifications/resources/list_changed` itself. A server that `createMcpHandler` builds lives for one request, so publish the change on the handler instead:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nEvery open `subscriptions/listen` stream that opted in to resource list changes receives the notification.\n\n## Serve skills defined in code\n\nPass the files to `memorySkillFiles` where there is no file system, such as Workers or tests.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#memorySkillFiles\"\nimport { memorySkillFiles, SkillsExtension } from '@modelcontextprotocol/server/ext/skills';\n\nconst RELEASE_NOTES_MD = `---\nname: release-notes\ndescription: Write release notes from merged pull requests.\n---\n# Release notes\n`;\n\nconst inline = await SkillsExtension.load(memorySkillFiles({ 'release-notes/SKILL.md': RELEASE_NOTES_MD }));\n```\n\n`memorySkillFiles` re-reads the object you pass whenever the catalog rescans (at load and on `reload()`), so `reload()` sees files you add or change later.\n\n## Serve skills from your own storage\n\nImplement `SkillFiles`, two functions over relative paths, to load from any store.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillFiles_custom\"\nimport type { SkillFiles } from '@modelcontextprotocol/server/ext/skills';\n\n// Two operations over '/'-separated paths, '' being the root: here, a key-value store of objects.\nfunction bucketSkillFiles(bucket: ReadonlyMap<string, Uint8Array>): SkillFiles {\n    return {\n        async list(dir) {\n            const prefix = dir === '' ? '' : `${dir}/`;\n            const children = new Map<string, 'file' | 'directory'>();\n            for (const key of bucket.keys()) {\n                if (!key.startsWith(prefix)) continue;\n                const [name, ...rest] = key.slice(prefix.length).split('/');\n                if (name !== undefined) children.set(name, rest.length > 0 ? 'directory' : 'file');\n            }\n            return [...children].map(([name, kind]) => ({ name, kind }));\n        },\n        async read(key) {\n            const bytes = bucket.get(key);\n            if (bytes === undefined) throw new Error(`no object ${key}`);\n            return bytes;\n        }\n    };\n}\n\nconst fromBucket = await SkillsExtension.load(\n    bucketSkillFiles(new Map([['release-notes/SKILL.md', new TextEncoder().encode(RELEASE_NOTES_MD)]]))\n);\n```\n\nThe catalog logic (discovery, digests, rule checks, paging, verified reads) is the same for every adapter. Report a symlink or other special file as `kind: 'unsupported'`; the catalog skips the skill that contains one.\n\n## Generate skills with a `SkillSource`\n\nBuild entries with `skillEntry` and serve them from a `SkillSource` when the catalog depends on the request.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillSource\"\nimport type { SkillSource } from '@modelcontextprotocol/server/ext/skills';\nimport { skillEntry } from '@modelcontextprotocol/server/ext/skills';\n\nconst ON_CALL_MD = `---\nname: on-call\ndescription: Handle a page while on call.\n---\n# On call\n`;\nconst onCall = await skillEntry('on-call', { 'SKILL.md': ON_CALL_MD });\n\nconst source: SkillSource = {\n    list: async () => ({ skills: [onCall] }),\n    get: async uri => (uri === onCall.uri ? onCall : undefined),\n    read: async uri => (uri === onCall.uri ? { text: ON_CALL_MD, mimeType: 'text/markdown' } : undefined)\n};\nconst generated = new SkillsExtension({ source });\n```\n\nEvery source method receives the request context (`ctx`), so you can list different skills per caller. Add `readDirectory` to serve folder listings, and `listResources` to list skills in `resources/list`.\n\n## Handle invalid skills\n\nRoute warnings where you want them; by default they go to stderr.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#onWarning\"\nconst checked = await loadSkills('./skills', {\n    onWarning: ({ path, message }) => console.error(`skills: ${path}: ${message}`)\n});\n```\n\n`loadSkills` skips, with a warning, any skill that breaks the rules, such as a `name` that differs from its folder or a symlinked file; the other skills still serve. Skills over 512 files or 16 MiB still publish, with a warning; `limits` changes those thresholds. The same options take `prefix` (`'acme'` serves every skill under `skill://acme/…`) and `pageSize` (entries per `skills/list` and directory page, default 100).\n\nSet `strict: true` to make `loadSkills` throw a `SkillCatalogError` listing every problem instead. Under `strict: true`, a symlink or other special file anywhere in the folder fails the load, even one outside every skill; hidden entries (names starting with `.`) are the exception, since the scan skips them entirely.\n\n::: warning\nDo not register your own resources under `skill://`. A resource you register at a skill file's URI takes precedence over that file, and clients then fail to verify it.\n:::\n\n::: info\nSkills work on both protocol versions; the `cacheHint` option applies on 2026-07-28 only — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).\n:::\n\n## Recap\n\n- `loadSkills('./skills')` builds a catalog once; `attach(server)` serves it on any number of servers.\n- Each folder with a `SKILL.md` is a skill; other folders group skills in the URI.\n- `reload()` swaps in a rescanned catalog all at once; behind `createMcpHandler`, call `handler.notify.resourcesChanged()` when it reports `listChanged`.\n- `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource` serve skills without a folder.\n- `loadSkills` skips invalid skills with a warning; `strict: true` makes it throw instead.\n\n<!-- #endregion guide -->\n","skills/mcp-typescript-sdk-upgrade-to-v2/SKILL.md":"---\nname: mcp-typescript-sdk-upgrade-to-v2\ndescription: 'Upgrades TypeScript code from v1 of the MCP TypeScript SDK (`@modelcontextprotocol/sdk`) to v2 (`@modelcontextprotocol/client`, `/server`, `/core` and the middleware packages): runs the official codemod, then works through what it cannot rewrite. Use whenever a project depends on `@modelcontextprotocol/sdk` and the user wants to migrate, upgrade, port or bump it to v2, or is fixing v1-to-v2 breakage such as missing `McpError`, `RequestHandlerExtra` or `*Schema` exports, `@mcp-codemod-error` markers, or changed auth, error, transport or handler behavior, even if they do not say \"migration\".'\n---\n\n<!-- #region intro -->\n\n# Upgrading from v1.x to v2\n\nThis guide covers upgrading from `@modelcontextprotocol/sdk` (v1.x) to the v2 packages.\nIt is written for shell-capable agents and humans alike: run the codemod first, then\nwork through the manual sections for what the codemod can't rewrite.\n\nIf you are already on v2 and want to adopt the **2026-07-28 protocol revision**, see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md) instead.\n\n## TL;DR — quick path\n\n1. **Prerequisites.** Node.js 20+. v2 is ESM-first but ships a CommonJS build too, so\n   both `import` and `require('@modelcontextprotocol/…')` resolve natively.\n2. **Run the codemod.**\n    ```bash\n    npx @modelcontextprotocol/codemod@latest v1-to-v2 .\n    ```\n    Run it at the **package root** (`.`), not `./src` — it also rewrites `package.json`,\n    and real projects import the SDK from `test/`, `scripts/`, and fixtures too.\n3. **Grep for markers.** Anything the codemod recognized but could not safely rewrite is\n   marked in place:\n    ```bash\n    grep -rn '@mcp-codemod-error' .\n    ```\n4. **Type-check.** `tsc --noEmit` (or your build). Remaining errors map to the\n   [manual sections](#manual-changes-what-the-codemod-does-not-handle) below.\n5. **Format.** The codemod rewrites the AST without reformatting — run your formatter on\n   the changed files (`prettier --write` / `eslint --fix` / `biome format --write`); the\n   codemod prints the exact command after it runs.\n6. **Run your tests.**\n\nMigrating a large codebase gradually instead of in one pass? See\n[Migrating in stages (large codebases)](references/packaging-runtime.md#migrating-in-stages-large-codebases).\n\n<!-- #endregion intro -->\n\n<!-- #region codemod -->\n\n## What the codemod handles\n\nThe codemod ([`@modelcontextprotocol/codemod`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md))\nmechanically applies every rename whose mapping is fixed. The mappings are the\n**source of truth** — they live in the codemod package and are not reproduced here:\n\n| Mapping                                                                                   | Source file                                                                                                                                                                  |\n| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk/...` import paths → v2 packages                                | [`mappings/importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)                   |\n| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts)                   |\n| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`                  | [`mappings/schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts)   |\n| `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` property remap                                 | [`mappings/contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) |\n\nIn addition the codemod:\n\n- Updates `package.json` dependencies (`@modelcontextprotocol/sdk` → the v2 packages\n  your imports actually use).\n- Rewrites `.tool()` / `.prompt()` / `.resource()` to `registerTool` / `registerPrompt`\n  / `registerResource` and wraps `inputSchema` / `outputSchema` / `argsSchema` /\n  `uriSchema` raw Zod shapes with `z.object()`, adding `import { z } from 'zod'`\n  when the file has no `z` binding.\n- Drops the result-schema argument from `client.request()` / `client.callTool()` for\n  spec methods.\n- Routes the spec Zod `*Schema` constants imported from `sdk/types.js` to\n  `@modelcontextprotocol/core` (mixed imports are split; `.parse()` / `.safeParse()`\n  calls are left untouched). Task-handler schema constants\n  (`GetTaskRequestSchema` etc.) used as `setRequestHandler` args are **not** rewritten\n  — the experimental tasks feature was removed (SEP-2663), so each such registration\n  is marked with an action-required diagnostic instead (see\n  [Experimental tasks interception removed](references/behavioral-changes.md#experimental-tasks-interception-removed)).\n- Renames `ErrorCode` → `ProtocolErrorCode` and routes the local-only members\n  (`RequestTimeout`, `ConnectionClosed`) to `SdkErrorCode` — rewriting an all-SDK\n  condition's `instanceof ProtocolError` guard to `SdkError`, and marking guards\n  that mix the two enums.\n- Renames every `StreamableHTTPError` reference to `SdkHttpError` and adds the import\n  (constructor calls are marked for review — argument shape changed).\n- Replaces `IsomorphicHeaders` with the Web Standard `Headers` type and drops the\n  import (a warning notes `Headers` uses `.get()`/`.set()`, not bracket access).\n- Rewrites `SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`.\n- Renames `RequestHandlerExtra` → `ServerContext` / `ClientContext` and the `extra`\n  parameter to `ctx`.\n- Rewrites `vi.mock` / `jest.mock` and dynamic `import()` paths.\n- Renames the `ResourceTemplate` **type** imported from `@modelcontextprotocol/sdk/types.js`\n  to `ResourceTemplateType` (the spec wire type). The `ResourceTemplate` URI-template\n  helper **class** from `server/mcp.js` keeps its name and is not renamed.\n- Drops `@modelcontextprotocol/sdk/server/zod-compat.js` imports.\n- Inverts optional completable nesting — `completable(schema.optional(), cb)` becomes\n  `completable(schema, cb).optional()` (see\n  [Standard Schema objects](references/server-registration.md#standard-schema-objects-raw-shapes-deprecated)); shapes it\n  cannot invert get an `@mcp-codemod-error` marker.\n- Rewrites `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\n  client or server package root, like the module's other symbols.\n\n## What the codemod does NOT handle\n\nEach of these maps to a manual section below. The codemod marks every site it\nrecognized but could not safely rewrite with an `@mcp-codemod-error` comment.\n\n- **Node 20 / ESM** — pre-flight, not a code rewrite. → [Packaging & runtime](references/packaging-runtime.md#packaging--runtime)\n- **Header-read `.get()` rewrite** — `IsomorphicHeaders` is renamed to `Headers`\n  and `extra.requestInfo?.headers[…]` is remapped to `ctx.http?.req?.headers[…]`, but\n  converting that bracket access to `.get()` is manual. (Headers you _pass in_ via\n  `requestInit.headers` need no rewrite — plain objects remain valid.)\n  → [HTTP & headers](references/http-headers.md#http--headers)\n- **`ctx.mcpReq.send()` schema-arg drop** — the codemod drops the schema arg from\n  `client.request()` / `client.callTool()` but leaves nested `ctx.mcpReq.send()` calls\n  alone. → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **OAuth error-class consolidation** — `instanceof InvalidGrantError` → `OAuthError` +\n  `OAuthErrorCode` is a judgment rewrite. → [Auth](references/auth.md#auth)\n- **`SdkErrorCode` branch selection** — the codemod renames `StreamableHTTPError` →\n  `SdkHttpError`; deciding which `SdkErrorCode` branch a given catch should match is\n  judgment. → [Errors](references/errors.md#errors)\n- **Namespace schema access** — `import * as t from '…/types.js'` +\n  `t.CallToolResultSchema.parse(…)` can't be split per-symbol; the codemod flags it\n  action-required — re-import the schema from `@modelcontextprotocol/core` by hand.\n  → [Types & schemas](references/types-schemas.md#types--schemas)\n- **Import-less (injected) SDK surfaces** — the codemod is import-driven: a file that\n  receives the SDK surface as a parameter (dependency injection, factory seams) and has\n  no SDK import is never rewritten, and the v1 idioms there fail at **runtime**, not\n  compile time — e.g. the v1 schema-first `setRequestHandler(Schema, …)` form throws a\n  `TypeError` at registration. Grep such seams for v1 API tokens beyond import\n  statements (`setRequestHandler(`, `ErrorCode.`, `extra.`) and apply the\n  [handler-registration](references/protocol-ctx.md#setrequesthandler--setnotificationhandler-use-method-strings)\n  and [Errors](references/errors.md#errors) sections by hand.\n  → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **Behavioral adaptation** — list auto-aggregation, capability empties, lazy validator\n  compilation, output-schema validation rules. → [Behavioral changes](references/behavioral-changes.md#behavioral-changes)\n\n---\n\n## Manual changes (what the codemod does not handle)\n\n<!-- #endregion codemod -->\n\nEach subsystem has its own reference file. Open the one that matches the type error, runtime failure or `@mcp-codemod-error` marker in front of you; most migrations need only a few of them.\n\n| Reference file                                                             | Read it when                                                                                                                                                                                   |\n| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Packaging & runtime](references/packaging-runtime.md)                     | Choosing which v2 packages to install, or dealing with Node/ESM/CommonJS setup, monorepo workspaces, Jest, nested `zod` copies, migrating in stages, or a library that peer-depends on the SDK |\n| [Imports & transports](references/imports-transports.md)                   | A transport import needs a runtime decision (Node vs web-standard Streamable HTTP) or a stdio import moved to a `./stdio` subpath                                                              |\n| [Low-level protocol & handler context (`ctx`)](references/protocol-ctx.md) | Code uses `extra` / `ctx`, `setRequestHandler` / `setNotificationHandler`, `request()`, `ctx.mcpReq.send()` or `callTool()`                                                                    |\n| [Server registration API](references/server-registration.md)               | Code registers tools, prompts or resources, or passes raw Zod shapes as schemas                                                                                                                |\n| [HTTP & headers](references/http-headers.md)                               | Code reads HTTP request headers                                                                                                                                                                |\n| [Errors](references/errors.md)                                             | Code throws or catches `McpError` / `ProtocolError`, `ErrorCode`, `SdkError` or `SdkHttpError`                                                                                                 |\n| [Auth](references/auth.md)                                                 | Code uses OAuth (`auth()`, `OAuthClientProvider`, OAuth error classes) or bearer-token auth                                                                                                    |\n| [Types & schemas](references/types-schemas.md)                             | Code imports Zod `*Schema` constants or removed type aliases, or depends on JSON Schema details                                                                                                |\n| [Behavioral changes](references/behavioral-changes.md)                     | The code compiles but tests or runtime behavior differ                                                                                                                                         |\n| [Enhancements](references/enhancements.md)                                 | Optional: v2 features worth adopting once the upgrade is done                                                                                                                                  |\n| [Unchanged APIs](references/unchanged-apis.md)                             | Checking whether an API changed at all before rewriting it                                                                                                                                     |\n\n<!-- #region need-help -->\n\n## Need help?\n\n- The codemod's [`@mcp-codemod-error`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md) markers point\n  at every site it could not safely rewrite.\n- The [Troubleshooting](https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting.md) page covers common errors and their fixes.\n- Runnable [examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)\n  for every subsystem.\n- Open an issue on [GitHub](https://github.com/modelcontextprotocol/typescript-sdk/issues).\n\n<!-- #endregion need-help -->\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/auth.md":"### Auth\n\n#### OAuth error consolidation\n\nThe individual OAuth error classes are replaced with a single `OAuthError` + `OAuthErrorCode`.\nThe `OAUTH_ERRORS` constant is removed. The codemod does not rewrite `instanceof` checks\non these classes — switch on `error.code` instead.\n\n| v1 class                       | v2 equivalent                                           |\n| ------------------------------ | ------------------------------------------------------- |\n| `InvalidRequestError`          | `OAuthError` + `OAuthErrorCode.InvalidRequest`          |\n| `InvalidClientError`           | `OAuthError` + `OAuthErrorCode.InvalidClient`           |\n| `InvalidGrantError`            | `OAuthError` + `OAuthErrorCode.InvalidGrant`            |\n| `UnauthorizedClientError`      | `OAuthError` + `OAuthErrorCode.UnauthorizedClient`      |\n| `UnsupportedGrantTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedGrantType`    |\n| `InvalidScopeError`            | `OAuthError` + `OAuthErrorCode.InvalidScope`            |\n| `AccessDeniedError`            | `OAuthError` + `OAuthErrorCode.AccessDenied`            |\n| `ServerError`                  | `OAuthError` + `OAuthErrorCode.ServerError`             |\n| `TemporarilyUnavailableError`  | `OAuthError` + `OAuthErrorCode.TemporarilyUnavailable`  |\n| `UnsupportedResponseTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedResponseType` |\n| `UnsupportedTokenTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedTokenType`    |\n| `InvalidTokenError`            | `OAuthError` + `OAuthErrorCode.InvalidToken`            |\n| `MethodNotAllowedError`        | `OAuthError` + `OAuthErrorCode.MethodNotAllowed`        |\n| `TooManyRequestsError`         | `OAuthError` + `OAuthErrorCode.TooManyRequests`         |\n| `InvalidClientMetadataError`   | `OAuthError` + `OAuthErrorCode.InvalidClientMetadata`   |\n| `InsufficientScopeError`       | `OAuthError` + `OAuthErrorCode.InsufficientScope` ¹     |\n| `InvalidTargetError`           | `OAuthError` + `OAuthErrorCode.InvalidTarget`           |\n| `CustomOAuthError`             | `new OAuthError(customCode, message)`                   |\n\n¹ Unrelated to the new transport-layer `InsufficientScopeError` (SEP-2350) exported from\n`@modelcontextprotocol/client`, which carries an RFC 6750 challenge from the resource\nserver and extends `OAuthClientFlowError`, **not** `OAuthError`. Do not rewrite that one.\n\n```typescript\n// v1\nif (error instanceof InvalidClientError) { ... }\n// v2\nimport { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }\n```\n\n⚠ **Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from\n`@modelcontextprotocol/express`, or from `@modelcontextprotocol/server` on\nweb-standard hosts) classifies the error your\n`OAuthTokenVerifier.verifyAccessToken()` throws: a v2\n`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +\n`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from\n`server-legacy`) or a generic `Error` falls through as unexpected — **invalid tokens\nbecome HTTP `500`**. When you re-point `requireBearerAuth` at\n`@modelcontextprotocol/express`, migrate the error classes your verifier throws in the\nsame change.\n\nA frozen copy of the v1 classes (and `mcpAuthRouter`) is available from\n`@modelcontextprotocol/server-legacy/auth` during migration.\n\n#### `AuthProvider` — non-OAuth bearer auth and the widened `authProvider` option\n\nThe transport `authProvider` option is widened to `AuthProvider | OAuthClientProvider`.\n**`AuthProvider`** is a new minimal interface — `{ token(): Promise<string | undefined>;\nonUnauthorized?(ctx): Promise<void> }` — for static-token / non-OAuth bearer auth.\nTransports call `token()` before every request and `onUnauthorized()` on 401 (then retry\nonce). Existing `OAuthClientProvider` implementations need no changes — transports adapt\nthem internally via the new `adaptOAuthProvider()` export. Also exported:\n`isOAuthClientProvider()` (type guard) and `handleOAuthUnauthorized()` (the standard\nOAuth `onUnauthorized` behavior, for composing your own adapter).\n\n#### OAuth client flow — behavioral changes\n\n- **Resolved scope passed to DCR (SEP-835).** `auth()` now computes the resolved scope\n  once (WWW-Authenticate → PRM `scopes_supported` → `clientMetadata.scope`) and passes\n  it to **both** the DCR POST body and the authorization request. `registerClient()`\n  gained an optional `scope` parameter that overrides `clientMetadata.scope` in the\n  registration body.\n- **OAuth error on HTTP 200.** `exchangeAuthorization()` / `refreshAuthorization()` now\n  throw `OAuthError` when the AS returns HTTP 200 with a JSON `{error: ...}` body (e.g.\n  GitHub). v1 surfaced this as a Zod parse failure on the tokens schema.\n- **Metadata discovery falls through on 502.** `discoverAuthorizationServerMetadata()`\n  treats `502 Bad Gateway` like 4xx — fall through to the next candidate URL instead of\n  throwing (fixes path-aware discovery behind reverse proxies). Other 5xx still throw.\n- **Scoped credential invalidation on `invalid_client` / `unauthorized_client`.** The\n  `auth()` retry for these errors now issues two scoped calls —\n  `invalidateCredentials('client')` then `invalidateCredentials('tokens')` — instead of\n  v1's single `invalidateCredentials('all')`, deliberately preserving the stored\n  discovery state so the callback-leg check on retry does not mask the original error.\n  A provider whose `invalidateCredentials()` implementation special-cases the `'all'`\n  scope must handle the split calls.\n- **Token persistence failures after a refresh now propagate.** v1 wrapped both\n  `refreshAuthorization()` and the `saveTokens()` that persists its result in one\n  `try`/`catch`, so a provider's persistence error was discarded alongside AS-side refresh\n  failures and `auth()` fell through to a fresh authorization request, returning\n  `'REDIRECT'`. Only the refresh call is guarded now — persisting runs after it and rejects\n  to the caller. Against an AS that rotates refresh tokens this was destructive rather than\n  merely quiet: the exchange has already succeeded, so the old refresh token is invalidated\n  server-side the moment the new one is issued, and dropping the new token set leaves\n  nothing usable on either side. A provider whose `saveTokens()` can throw (transient\n  storage errors, file-lock contention) must handle the rejection from `auth()` — and from\n  the transport 401-retry paths built on it — where v1 silently re-authorized. Refresh\n  failures themselves keep their control flow: a `ServerError` or an unknown error still\n  falls through to a new authorization request, and `invalid_grant` / `invalid_client` /\n  `unauthorized_client` are still recovered by discarding stored credentials and retrying.\n  Both routes now emit a `console.warn` naming the cause, so an unexplained re-auth prompt\n  can be traced to the failure that triggered it.\n\n#### OAuth client flow errors (new)\n\nThe OAuth client flow now throws dedicated classes from `@modelcontextprotocol/client`\n(all extend `OAuthClientFlowError`, **not** `OAuthError` — `auth()`'s `OAuthError` retry\npath will not catch them):\n\n| Throw site                                                                                                                | v2 class                                                                              |\n| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| `registerClient()` rejected by AS (⚠ `@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) | `RegistrationRejectedError` (`status`, `body`, `submittedMetadata`)                   |\n| Token-exchange / refresh / `fetchToken` / Cross-App grant on a non-`https:` token endpoint                                | `InsecureTokenEndpointError` (`tokenEndpoint`)                                        |\n| RFC 9207 `iss` mismatch / RFC 8414 §3.3 issuer-echo mismatch                                                              | `IssuerMismatchError` (`kind`, `expected`, `received`)                                |\n| Transport 403 `insufficient_scope` with `onInsufficientScope: 'throw'`, or default mode without an `OAuthClientProvider`  | `InsufficientScopeError` (`requiredScope`, `resourceMetadataUrl`, `errorDescription`) |\n| `auth()` callback leg: discovery resolves a different AS than the recorded redirect target                                | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n| `auth()` on a provider that cannot re-register, or `fetchToken()`: client information stamped for a different AS          | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n\n#### Connect-time OAuth retry (`UnauthorizedError`)\n\n`UnauthorizedError` survives in v2 (exported from `@modelcontextprotocol/client` —\nits only appearance in the error table above is the removed `SSEClientTransport.send()`\n401 path), and the v1 connect-time pattern carries over: catch it from `connect()`,\ncomplete the browser flow, call `transport.finishAuth(…)`, reconnect.\n\n```typescript\ntry {\n    await client.connect(transport);\n} catch (error) {\n    if (!(error instanceof UnauthorizedError)) throw error;\n    // provider.redirectToAuthorization() has been called; complete the flow,\n    // then reconnect on a FRESH transport (a started transport cannot be restarted).\n    await transport.finishAuth(new URL(callbackUrl).searchParams);\n    await client.connect(new StreamableHTTPClientTransport(url, { authProvider: provider }));\n}\n```\n\nThis direct `instanceof` check works in every version-negotiation mode: under the\nprobing modes (`versionNegotiation: { mode: 'auto' }`, with or without a pin) the\nconnect-time `UnauthorizedError` also propagates unchanged from `connect()`. Older\nreleases wrapped it as `SdkError(SdkErrorCode.EraNegotiationFailed)` with the error at\n`error.data.cause` — that unwrap is no longer needed. See the\n[client OAuth guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/oauth.md).\n\n#### `auth()` options are now `AuthOptions`\n\nThe inline options object on `auth()` is now the named `AuthOptions` type. New fields:\n`iss?: string` (the form-urldecoded `iss` from the authorization callback — pass it\nalongside `authorizationCode` for RFC 9207 validation), `skipIssuerMetadataValidation?:\nboolean` (security-weakening opt-out of the RFC 8414 §3.3 issuer-echo check), and\n`forceReauthorization?: boolean` (skip the refresh-token branch — set by the transport's\nstep-up path; hosts driving step-up themselves set it under the same condition).\n\n#### Authorization-server mix-up defense (RFC 9207 / RFC 8414 §3.3) — action required\n\n`transport.finishAuth()` and `auth()` now validate `iss` from the authorization callback\nagainst the issuer recorded from validated AS metadata. A mismatched `iss` throws\n`IssuerMismatchError` before the code is exchanged regardless of advertised support; a\n**missing** `iss` throws only when the AS advertised\n`authorization_response_iss_parameter_supported: true`.\n\nPass the callback URL's `URLSearchParams` so the SDK can read `iss` alongside `code`.\nThe SDK does **not** validate `state`; compare it yourself before calling `finishAuth`:\n\n```typescript\nconst params = new URL(callbackUrl).searchParams;\nif (params.get('state') !== expectedState) throw new Error('state mismatch');\nawait transport.finishAuth(params); // SDK reads `code` + `iss`\n```\n\n`transport.finishAuth(code, iss)` remains supported. Do **not** display `error` /\n`error_description` / `error_uri` from a callback that failed `iss` validation — those\nvalues are attacker-controlled in a mix-up attack.\n\n`discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not\nexactly match the URL it was fetched for (RFC 8414 §3.3). Set\n`skipIssuerMetadataValidation: true` only as a temporary workaround for a known-misconfigured AS.\n\n(`@modelcontextprotocol/server-legacy` AS implementers: `mcpAuthRouter()` now advertises\n`authorization_response_iss_parameter_supported: true` by default and the bundled\nauthorize handler appends `iss` to every redirect issued via `res.redirect(...)` on the\nsupplied `res`. If you emit `Location` another way, append `params.issuer` as `iss`\nyourself; if your callback is issued by an upstream AS you proxy to, set\n`authorizationResponseIssParameterSupported = false` so the metadata does not over-claim.)\n\n#### Dynamic Client Registration defaults (SEP-837, SEP-2207)\n\n`auth()` now resolves `provider.clientMetadata` once via `resolveClientMetadata()` and\napplies defaults to the DCR body: `grant_types` defaults to\n`['authorization_code', 'refresh_token']`; `application_type` is derived from\n`redirect_uris` (loopback / custom URI scheme → `'native'`, else `'web'`). A field you\nset explicitly is never overwritten. The `grant_types` default applies to the DCR body\nonly — it does **not** drive the `offline_access` / `prompt=consent` augmentation on the\nauthorize request; statically-registered and CIMD clients that want that augmentation\nmust set `clientMetadata.grant_types` explicitly. Non-interactive providers (no\n`redirectUrl`) get no `grant_types` default. Direct `registerClient()` callers (⚠\n`@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) wanting the same\ndefaults pass `resolveClientMetadata(provider)` as `clientMetadata`. DCR\nrejection now throws `RegistrationRejectedError` (carrying `status`, `body`,\n`submittedMetadata`).\n\n#### Token endpoint must use TLS (SEP-2207)\n\n`exchangeAuthorization()`, `refreshAuthorization()`, `fetchToken()`, and the Cross-App\nAccess helpers throw `InsecureTokenEndpointError` when the token endpoint is not\n`https:` (loopback `localhost` / `*.localhost` / `127.0.0.1` / `::1` exempt). `auth()` surfaces this on\nevery path including refresh — switch any plain-`http:` AS on a non-loopback host to\nTLS; there is no opt-out. Storage confidentiality of `refresh_token` remains your\n`saveTokens()` implementation's responsibility.\n\n#### Scope step-up on `403 insufficient_scope` (SEP-2350)\n\n`StreamableHTTPClientTransport` accepts `onInsufficientScope: 'reauthorize' | 'throw'`\n(default `'reauthorize'`). On `'reauthorize'` the transport re-authorizes with the\n**union** of the previously-requested and challenged scope (`computeScopeUnion`); when\nthat union strictly exceeds the current token's granted scope (`isStrictScopeSuperset`),\nthe SDK bypasses the refresh-token branch and forces a fresh authorization request. On\n`'throw'` the transport raises `InsufficientScopeError` and does not re-authorize — set\nthis for `client_credentials` / m2m clients where re-authorization can't widen scope, or\nto gate the consent prompt behind UX. Step-up retries are hard-capped per send\n(`maxStepUpRetries`, default 1). With a non-OAuth [`AuthProvider`](#authprovider--non-oauth-bearer-auth-and-the-widened-authprovider-option),\na `403 insufficient_scope` now throws `InsufficientScopeError` instead of the previous\n`SdkHttpError(ClientHttpNotImplemented)`. The GET listen-stream open path applies the\nsame handling as the POST send path.\n\n#### Credentials bound to the issuing authorization server (SEP-2352)\n\n`auth()` stamps an `issuer` field onto every value it passes to `saveTokens()` /\n`saveClientInformation()` and threads `{ issuer }` as the `ctx` argument to those\nmethods plus `tokens()` / `clientInformation()`. On read, a stored value whose `issuer`\nnames a different AS is treated as `undefined` and the flow re-registers / re-authorizes\n(or throws `AuthorizationServerMismatchError` when the provider has no `saveClientInformation()`).\n**Round-trip the stored object verbatim and you're protected** — single-slot storage\nworks. Dropping the stamp is easy to miss: a `saveTokens()` implementation that\nrebuilds the object field-by-field and drops `issuer` leaves the value unstamped —\nreads still succeed and refresh keeps working, the per-AS issuer check simply does not\napply to that credential, and every read logs an `[mcp-sdk]` warning (`auth()`\nre-stamps on first use where the provider can persist it). If you see that warning\nrepeating after upgrading, check this first. To hold credentials for several authorization servers at once, key your storage\non `ctx.issuer` (treat **`ctx === undefined` as \"return the most-recently-saved token\nset\"** — the transport's per-request `Authorization: Bearer` read calls `tokens()` with\nno `ctx`). `OAuthTokensSchema` / `OAuthClientInformationSchema` keep the optional `issuer`, so\nreading storage back through them is fine; the `StoredOAuthTokens` / `StoredOAuthClientInformation`\naliases name the stored shape.\n\n`OAuthClientProvider.saveAuthorizationServerUrl()` / `authorizationServerUrl()` are\n`@deprecated` (still written for back-compat, never read by the SDK). The bundled\n`ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivateKeyJwtProvider`, and\n`CrossAppAccessProvider` gain `expectedIssuer?: string` (omitting it is deprecated) and no\nlonger define `saveClientInformation()`. Implement `discoveryState()` / `saveDiscoveryState()` so the\ncallback leg can verify it is exchanging the code at the same AS the redirect targeted;\nwithout it the SDK `console.warn`s once per callback (`discoveryState` must persist with\nthe same durability as `codeVerifier`). Both methods are optional on\n`OAuthClientProvider` and may be sync or async; `OAuthDiscoveryState` (exported from\n`@modelcontextprotocol/client`) extends `OAuthServerInfo` with the optional\n`resourceMetadataUrl` the protected-resource metadata was found at:\n\n```typescript\nimport type { OAuthDiscoveryState } from '@modelcontextprotocol/client';\n\n// On OAuthClientProvider:\nsaveDiscoveryState?(state: OAuthDiscoveryState): void | Promise<void>;\ndiscoveryState?(): OAuthDiscoveryState | undefined | Promise<OAuthDiscoveryState | undefined>;\n```\n\n#### Conformance obligations for `OAuthClientProvider` implementers\n\nThe SDK enforces every authorization MUST that lands in SDK code. The following live in\n**your** implementation and the SDK structurally cannot enforce them:\n\n- **Round-trip the `issuer` stamp** on persisted credentials (SEP-2352). Persist the\n  value verbatim from `saveTokens` / `saveClientInformation` and return it verbatim.\n- **Pass `expectedIssuer`** when constructing static-credential providers (SEP-2352).\n- **Keep refresh tokens confidential in storage** (SEP-2207) — OS keychain or\n  encrypted-at-rest store, never `localStorage` / plain files / logs.\n- **Extract `iss` from the callback URL** and pass it to `finishAuth` (SEP-2468); when\n  `IssuerMismatchError` is thrown, do not render the callback's `error*` values.\n- **Set `application_type` correctly** when overriding the heuristic (SEP-837).\n- **Track cross-request step-up failures yourself** (SEP-2350) — `maxStepUpRetries` is\n  per request; per-session backoff is host state.\n- **Persist discovery state**: implement `discoveryState()` / `saveDiscoveryState()` so\n  the authorization-server metadata your tokens were issued against survives restarts.\n- **Choose the insufficient-scope behavior**: keep the default\n  `onInsufficientScope: 'reauthorize'`, or handle `InsufficientScopeError` yourself.\n- **Resource-server operators: do not advertise `offline_access`** in `WWW-Authenticate`\n  `scope` or PRM `scopes_supported` (SEP-2207).\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md":"### Behavioral changes\n\nThese are runtime-behavior changes that may affect tests and assertions; no source\nrewrite required unless noted.\n\n#### Error-shape changes (every era)\n\n- **Unchanged, for re-baselining relief:** timeout rejections still carry\n  `data.timeout` / `data.maxTotalTimeout` exactly as v1 `McpError` did — v1 assertions\n  on those survive verbatim. The cancelled-on-timeout signal is unchanged on legacy-era\n  connections and on stdio/in-memory at any era; on 2026-era Streamable HTTP the cancel\n  signal is the per-request stream close instead of a `notifications/cancelled` POST\n  (see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The one exemption is the\n  `initialize` handshake: an aborted or timed-out `connect()` still rejects locally, but\n  no `notifications/cancelled` goes on the wire — the spec forbids cancelling\n  `initialize`, and v1 sent one anyway. v1 tests asserting that notification need\n  re-baselining.\n- **Also unchanged: SSE reconnection exhaustion.** `StreamableHTTPClientTransport`'s\n  standalone GET-stream reconnection behavior and its exhaustion signal carry over from\n  v1: when retries run out, the transport emits `onerror` with a plain `Error` whose\n  message is `Maximum reconnection attempts (N) exceeded.` — there is no typed error\n  class for this condition, so monitors that match the message text keep working.\n- **Also unchanged: elicitation response validation.** `elicitInput`'s local validation\n  of elicitation responses against `requestedSchema`, the resulting `-32602` error\n  message wording (`Elicitation response content does not match requested schema: …`),\n  and the `McpServer` / `Client` `jsonSchemaValidator` option carry over from v1 —\n  tests pinning the local-validation message and custom validator wiring need no\n  re-baselining.\n- **Unknown / disabled tool calls now reject** with `ProtocolError(-32602 InvalidParams)`\n  instead of resolving `CallToolResult{isError: true}`. v1 callers that checked\n  `result.isError` for an unknown tool will get an unhandled rejection — catch the\n  rejected promise instead.\n- **The `MCP error <code>: ` message prefix is gone.** v1 prefixed relayed JSON-RPC\n  error messages (`MCP error -32602: …`); v2's `ProtocolError.message` carries the\n  peer's message verbatim. Tests and log scrapers that matched the prefix or the numeric\n  code in rendered text should match `error.code` instead.\n- **In-flight request handlers are aborted on transport close** — `ctx.mcpReq.signal`\n  fires (v1 let them run to completion). `InMemoryTransport.close()` no longer\n  double-fires `onclose` on the initiating side.\n- **`Protocol.request()` with an already-aborted signal** rejects with\n  `SdkError(SdkErrorCode.RequestTimeout, reason)` instead of throwing the raw\n  `signal.reason`, matching the in-flight-abort path.\n- **OAuth discovery (`discoverOAuthProtectedResourceMetadata` / `discoverOAuthMetadata`,\n  transitively `auth()`) throws on fetch `TypeError`** (DNS failure, `ECONNREFUSED`,\n  invalid URL) in Node and Cloudflare Workers instead of swallowing it as a CORS miss\n  → `undefined`. The CORS-swallow remains browser-only.\n\n#### Client connection & dispatch\n\n- **`connect()` skips the `initialize` handshake when the transport already exposes a\n  `sessionId`** — it assumes it is reconnecting to an existing session (unchanged from\n  v1.x, where the same guard has existed since 1.10.0; recorded here because the\n  far-away symptom keeps surprising migrators). A custom or test transport that sets `sessionId` at construction\n  silently skips initialization: `getServerCapabilities()` stays `undefined` and the\n  list verbs return empty results. Expose `sessionId` only after the first request has\n  been sent.\n- **The typed verbs dispatch after async pre-work.** `Protocol.request()` itself still\n  hands the frame to the transport before its first `await` (v1-compatible). The typed\n  verbs on top of it — `callTool()` and the cacheable list verbs — perform async work\n  first (header-mirroring scan, response-cache freshness, output-validator resolution),\n  so an abort fired in the same tick can land before the frame is ever sent: the call\n  rejects with `SdkError(RequestTimeout, reason)` and **no `notifications/cancelled` is\n  emitted** (nothing was in flight). v1 sent the frame synchronously from these verbs.\n  Once the frame is on the wire, aborting still sends `notifications/cancelled` before\n  rejecting.\n- **Protocol-version pinning is a first-class option.**\n  `ProtocolOptions.supportedProtocolVersions` pins the legacy `initialize` handshake:\n  the **first** pre-2026 entry in the list is offered (list order is preference order),\n  a counter-offer is accepted only if it is one of the list's pre-2026 entries, and a\n  list with no pre-2026 entry makes the handshake throw. Under\n  `versionNegotiation: 'auto'` the modern probe candidates are the list's modern\n  entries when it has any (otherwise the SDK's default modern set); a `{ pin }` is\n  honored as given and is not checked against the list (see\n  [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#client-side-versionnegotiation)).\n  v1 had no public equivalent (`SUPPORTED_PROTOCOL_VERSIONS` was a fixed constant) —\n  replace any workaround that patched the offered version with this option.\n- **Also unchanged: HTTP 405 tolerances.** A `405` answering the standalone GET stream\n  open is benign (the client proceeds without the stream), and a `405` answering the\n  session DELETE resolves `terminateSession()` normally — stateless-topology servers\n  that decline both verbs keep working without changes, as in v1.\n\n#### stdio transport\n\n- A configurable `maxBufferSize` (default **10 MB**) caps the stdio read buffer. A\n  single message that would push the buffer past the limit emits `onerror` and\n  **closes the connection** (v1 buffered unbounded). Configure via\n  `new StdioClientTransport({ ..., maxBufferSize })` /\n  `new StdioServerTransport(stdin, stdout, { maxBufferSize })`.\n- `ReadBuffer.readMessage()` now **silently skips non-JSON stdout lines** instead of\n  throwing `SyntaxError` → `onerror`. Hot-reload tools (tsx, nodemon) that write debug\n  output to stdout no longer break the transport. Lines that parse as JSON but fail\n  JSON-RPC schema validation still throw.\n- `StdioClientTransport` always sets `windowsHide: true` when spawning the server\n  process on Windows (previously Electron-only). Prevents stray console windows in\n  non-Electron Windows hosts.\n- Outbound write failures — e.g. the host closing the stdout pipe while a send is\n  pending — now reject the pending `send()` and close the transport through\n  `onerror`/`onclose` instead of surfacing an unhandled stream error; lifecycle\n  tests that pinned a crash-class exit observe a clean shutdown instead.\n\n#### Client list methods\n\n- `listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` return\n  **empty results** when the server didn't advertise the corresponding capability,\n  instead of sending the request. Set `enforceStrictCapabilities: true` in `ClientOptions`\n  to restore the v1 throw.\n- Called **without a `cursor`**, the same methods now **auto-aggregate every page** and\n  return an aggregate with no `nextCursor`. Passing `{ cursor }` still fetches one page. Manual\n  pagination loops keep working (the first iteration returns everything); replace them\n  with the bare no-arg call. The walk is capped at `ClientOptions.listMaxPages` (default\n  64); overrun throws `SdkError(ListPaginationExceeded)`. There is no way to fetch only\n  the **first** page through the typed verbs — for page-level observation\n  (pagination tooling, per-page stats) drop to\n  `client.request({ method: 'tools/list', params })`, which never aggregates.\n- Output-schema validator compilation is now **lazy** — validators compile on the first\n  `callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()`.\n  In v1, `listTools()` threw on an uncompilable `outputSchema`; now `listTools()`\n  succeeds and the compile failure surfaces when `callTool()` is invoked on the affected\n  tool, as `ProtocolError(InvalidParams, \"Tool 'X' has an invalid outputSchema: …\")`,\n  before the request is sent. Validation is never silently skipped.\n- On a 2026-07-28 connection the cacheable verbs honour the server-stamped `ttlMs` /\n  `cacheScope` (SEP-2549) and may return a still-fresh cached entry without a round\n  trip. Per-call override: `{ cacheMode: 'refresh' | 'bypass' }`. New `ClientOptions`:\n  `cachePartition`, `defaultCacheTtlMs`. `ResponseCacheStore` gained `delete(key)`;\n  `InMemoryResponseCacheStore` is now bounded (`{ maxEntries }`, default 512).\n  `CacheEntry.value` (and the `set()` entry value) is the JSON-serialized result\n  document (`string`, not `unknown`): persist and return it verbatim, `JSON.parse`\n  it to inspect.\n\n#### Server (Streamable HTTP transport)\n\n- Resumability behavior (SSE priming events, `closeSSE` / `closeStandaloneSSE`\n  callbacks) is only enabled for protocol versions in the transport's supported-versions\n  list that are `>= 2025-11-25`. Unknown future version strings in an `initialize`\n  request body no longer enable it.\n- Session-ID mismatch still responds `404` with JSON-RPC `-32001` (`Session not found`),\n  unchanged from v1. This `-32001` is an SDK convention, not spec-assigned; client code\n  should key off the HTTP `404` status, not `-32001`.\n\n#### Server (deprecated accessors and app-factory Origin validation)\n\n- `Server.getClientCapabilities()`, `getClientVersion()`, `getNegotiatedProtocolVersion()`\n  are `@deprecated` but functional. On 2026-07-28 requests, prefer `ctx.mcpReq.envelope`.\n- `createMcpExpressApp()` / `createMcpHonoApp()` / `createMcpFastifyApp()` with a\n  localhost-class `host` now also validate the `Origin` header by default. Browser-served\n  clients on a non-localhost origin need `allowedOrigins: [...]` (replaces the default\n  localhost allowlist; validation cannot be disabled for localhost binds). Requests\n  without an `Origin` header are unaffected; a present `Origin` that cannot be parsed\n  — including the opaque **`Origin: null`** sent by sandboxed iframes, `file://` pages,\n  and cross-origin redirects — is **rejected with 403** and cannot be allowlisted via\n  `allowedOrigins`. Framework-agnostic helpers\n  (`validateOriginHeader`, `localhostAllowedOrigins`, `originValidationResponse`) are in\n  `@modelcontextprotocol/server`; `@modelcontextprotocol/node` ships\n  `hostHeaderValidation` / `originValidation` request guards for plain `node:http`.\n\n#### Server (McpServer / Streamable HTTP behavior)\n\n- **Eager capability-handler install.** `McpServer` now installs list/read/call handlers\n  for every primitive capability declared in `ServerOptions.capabilities`, even with\n  zero registrations. `new McpServer(info, { capabilities: { tools: {} } })` with no\n  registered tools answers `tools/list` with `{ tools: [] }` instead of `-32601 Method\nnot found`. Low-level `Server` users remain responsible for registering handlers for\n  declared capabilities — with one exception: declaring the `logging` capability (in\n  the constructor's capabilities or via pre-connect `registerCapabilities()`) installs\n  the `logging/setLevel` handler on the low-level `Server` too, so `logging/setLevel`\n  requests that answered `-32601` in v1 now resolve. Eager install also rewrites the **advertised** capability\n  objects: a declared `tools: {}` / `resources: {}` / `prompts: {}` is advertised with\n  `listChanged: true` at construction, so capability pins and initialize-result golden\n  tests need re-baselining. To advertise without the default, set\n  `listChanged: false` explicitly; capabilities declared on the low-level `Server` are\n  advertised verbatim.\n- **`WebStandardStreamableHTTPServerTransport` store-first `eventStore` semantics.**\n  Request-related events emitted after `closeSSE()` — and the final response when no\n  per-request stream is connected — are now persisted to the configured `eventStore` for\n  replay (v1 dropped them / threw `\"No connection established\"`). Without an\n  `eventStore`, the same condition surfaces via `onerror` and the request id is retired.\n  `NodeStreamableHTTPServerTransport` is a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport`, so this — like every behavioral note on\n  the web-standard transport — applies to the Node transport too.\n- **`registerResource` reserves the `cacheHint` config key.** It is validated\n  (`RangeError` on invalid values) and stripped from the resource's list metadata; v1\n  passed it through verbatim as ordinary metadata. Untyped callers that previously\n  smuggled a `cacheHint` key through resource metadata should rename it.\n\n#### `ctx.mcpReq.log()` is request-related on every era\n\n`ctx.mcpReq.log()` now emits its `notifications/message` request-related (it rides the\nin-flight exchange like progress) on every era. On a 2025-era sessionful Streamable HTTP\ntransport this moves handler-emitted logs from the standalone GET stream onto the\nper-request POST response stream — a spec-conformance correction. The session-scoped\n`logging/setLevel` filter applies as before on 2025-era connections. (On 2026-07-28\nrequests, the per-request `_meta.logLevel` envelope key is the filter — see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#serving-the-2026-07-28-revision).)\n\n#### Wire tightening (every era)\n\n- **`CallToolResult.content` keeps the v1 parse tolerance on the legacy era.** An\n  inbound result without `content` defaults to `[]` (deployed servers omit it\n  alongside `structuredContent`); 2026-07-28 connections stay strict. Authoring is\n  unchanged and era-independent: the TypeScript surface requires `content` on handler\n  results, and a content-less handler result is normalized to `content: []` before it\n  reaches the wire. One sharpening remains: a content-less body carrying another\n  result family's vocabulary (a task handle or an `input_required` round) is still\n  rejected loudly — tolerance never turns a different result kind into a silent empty\n  success. A body whose only foreign key was `resultType` strips to an empty object\n  and defaults, exactly as v1 parsed a payload-free body.\n- **`ElicitResult.content` values are typed and validated as\n  `string | number | boolean | string[]`.** v1's TypeScript surface accepted\n  `Record<string, unknown>` content values; an elicitation handler returning arbitrary\n  objects now fails to compile (and fails schema validation) — narrow to the primitives\n  the elicitation spec allows.\n- **Custom (3-arg) handlers receive `_meta`.** `setRequestHandler(method, {params}, handler)`\n  used to delete `params._meta` before validation; it now passes `_meta` through (minus\n  the reserved `io.modelcontextprotocol/*` envelope keys). If your params schema is\n  strict, add an optional `_meta` member.\n- **`specTypeSchemas` validate the neutral model.** Result entries no longer accept\n  `resultType`; the validators for the 2025-only task message types and\n  `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed accordingly).\n- **Sampling `hasTools` discriminant** now keys on `tools || toolChoice` (previously\n  `tools` only) when selecting the with-tools `CreateMessageResult` variant, on every\n  era.\n- **Inbound frames that fail message-shape validation are not answered.** v2 routes\n  every inbound frame through typed message guards; a frame that matches no JSON-RPC\n  shape (e.g. a hand-built ping with an explicitly-`undefined` `id`, or non-object\n  `params`) is dropped and surfaces only via `onerror` (`Unknown message type: …`) — no\n  response is sent. v1-era test fences that await a reply to a hand-written raw frame\n  hang instead of resolving; send through the typed surface (`client.ping()`,\n  `client.request()`) instead.\n\n#### Experimental tasks interception removed\n\nThe 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`).\nNo mechanical migration; remove usages. Gone: `ProtocolOptions.tasks`,\n`protocol.taskManager`, `RequestOptions.task` / `relatedTask`, `BaseContext.task`,\n`assertTaskCapability` / `assertTaskHandlerCapability`, `*.experimental.tasks.*`\naccessors and `Experimental{Client,Server,McpServer}Tasks`, `requestStream` /\n`callToolStream` / `createMessageStream` / `elicitInputStream` and the `ResponseMessage`\ntypes they yielded, `registerToolTask`, `ToolTaskHandler`, `TaskRequestHandler`,\n`CreateTaskRequestHandler`, `TaskMessageQueue`, `InMemoryTaskMessageQueue`,\n`BaseQueuedMessage` / `Queued*`, `CreateTaskServerContext`, `TaskServerContext`,\n`TaskToolExecution`, `TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`,\nand the `new McpServer(info, { taskStore, taskMessageQueue })` constructor option keys\n(the codemod emits an action-required diagnostic at each — remove the option).\n\nThe task **wire types** remain importable as `@deprecated` vocabulary for 2025-11-25\ninterop — see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#tasks-deprecated-wire-vocabulary).\n\n#### Specification clarifications adopted (no SDK behavior change)\n\nThe 2026-07-28 specification revision includes a number of documentation-only\nclarifications recorded here so an audit of the revision's changelog against this guide\nis complete; nothing in this list requires code changes: per-operation timeout guidance\nremoval (`RequestOptions.timeout` / `DEFAULT_REQUEST_TIMEOUT_MSEC` unchanged); stdio\nshutdown wording; transports-as-bindings reframe; `resources/read` wording (the\n`file://` path-sanitization MUST is server-author guidance — your handler must reject\ntraversal / symlink escapes itself); `PromptMessage` resource links (already in\n`ContentBlock`); completion `ref/resource` URI templates; pagination empty-string\ncursors (already passed through verbatim); sampling host-requirement docs; elicitation\nstatefulness wording; cosmetic schema/JSDoc sweeps.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/enhancements.md":"## Enhancements\n\n### Automatic JSON Schema validator selection by runtime\n\nThe SDK auto-selects the validator: Node.js → AJV; Cloudflare Workers (workerd) →\n`@cfworker/json-schema`. Cloudflare Workers users can remove explicit\n`jsonSchemaValidator` configuration. You don't need to install `ajv`, `ajv-formats`, or\n`@cfworker/json-schema` for the default path. To customize the built-in backend, import\nthe named class from the explicit subpath\n(`@modelcontextprotocol/{client,server}/validators/ajv` or `…/cf-worker`) — importing\nfrom a subpath means the corresponding peer dep must be in your `package.json`.\n\n### `Client.connect(transport, { prior })` — connect from a cached era verdict\n\nProbe once, persist `client.getDiscoverResult()` (`JSON.stringify`), and feed it to\nevery worker as `client.connect(transport, { prior: { kind: 'modern', discover } })`.\nNew exported types\n`ConnectOptions` (extends `RequestOptions` with `prior?: PriorDiscovery`)\nand `PriorDiscovery` — a cached era verdict: the modern arm wraps a `DiscoverResult`\n(zero round trips), the legacy arm (`{ kind: 'legacy' }`) skips the probe and runs the\nplain `initialize` handshake for servers known to be pre-2026. Freshness is the\nsupplying host's responsibility — date cached legacy verdicts in your own storage and\nstop supplying them past your policy horizon (a stale one succeeds silently against an\nupgraded server).\n\n### Serving the 2026-07-28 revision\n\n`createMcpHandler`, `serveStdio`, `versionNegotiation`, multi-round-trip requests\n(`requestState`), client cancellation via stream-close, `subscriptions/listen`,\n`Mcp-Param-*` headers, and per-era wire codecs are covered in\n**[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)** — they are net-new in v2, not v1→v2\nbreaks.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/errors.md":"### Errors\n\nThe SDK now distinguishes three error kinds:\n\n1. **`ProtocolError`** (renamed from `McpError`) — protocol errors that cross the wire\n   as JSON-RPC error responses. Uses `ProtocolErrorCode` (renamed from `ErrorCode`).\n2. **`SdkError`** — local SDK errors that never cross the wire. Uses `SdkErrorCode`.\n3. **`SdkHttpError`** (extends `SdkError`) — HTTP transport errors with typed `.status`\n   and `.statusText`.\n\nThese classes (and `OAuthError`, the client's `SseError`, `UnauthorizedError`, and the\nOAuth-client-flow error family) brand-match under `instanceof`, so checks work across\nseparately bundled copies of the SDK — e.g. a process using both\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server`. Each branded\nhierarchy also exposes the same check as an explicit static guard\n(`SdkError.isInstance(err)`, `ProtocolError.isInstance(err)`, …) that narrows in\nTypeScript — use whichever style your codebase prefers; both read the same brand.\nFine print (applies equally to `instanceof` and `isInstance`):\n\n- **Version skew** — matching needs _both_ copies at a brand-aware release; against an\n  older copy, behavior degrades to plain prototype `instanceof` (false across bundles).\n  During mixed-version rollouts, recognize errors without class identity: match\n  `error.name` plus the class's discriminant field (`code`, `status`), or reconstruct\n  typed protocol errors with `ProtocolError.fromError(code, message, data)`.\n- **Worker boundaries** — `structuredClone`/`postMessage` drop the (symbol-keyed) brand,\n  so a rehydrated error no longer brand-matches; recognize forwarded errors by\n  `code`/`data` instead.\n- **Brands assert identity, not shape** — a matched instance from another SDK version\n  may lack newer fields; read fields defensively.\n- **Re-bundling with property mangling** (`mangle.props` and similar) breaks the brand\n  statics; default esbuild/webpack/terser settings are safe.\n\nThe codemod renames `McpError` → `ProtocolError`, `ErrorCode` → `ProtocolErrorCode`\n(routing `RequestTimeout` / `ConnectionClosed` to `SdkErrorCode`), and\n`StreamableHTTPError` → `SdkHttpError`. After the codemod runs, your `instanceof`\nchecks already name the v2 classes — what's left is choosing which `SdkErrorCode` /\nclass to match per scenario:\n\n| Scenario                                         | v1                                        | v2                                                                 |\n| ------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------ |\n| Request timeout                                  | `McpError` + `ErrorCode.RequestTimeout`   | `SdkError` + `SdkErrorCode.RequestTimeout`                         |\n| Connection closed                                | `McpError` + `ErrorCode.ConnectionClosed` | `SdkError` + `SdkErrorCode.ConnectionClosed`                       |\n| Capability not supported                         | `new Error(...)`                          | `SdkError` + `SdkErrorCode.CapabilityNotSupported`                 |\n| Not connected                                    | `new Error('Not connected')`              | `SdkError` + `SdkErrorCode.NotConnected`                           |\n| Response result fails schema                     | raw `ZodError`                            | `SdkError` + `SdkErrorCode.InvalidResult`                          |\n| Invalid params (server response)                 | `McpError` + `ErrorCode.InvalidParams`    | `ProtocolError` + `ProtocolErrorCode.InvalidParams`                |\n| HTTP transport error                             | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttp*`                        |\n| Failed to open SSE stream                        | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToOpenStream`       |\n| 401 after re-auth (circuit break)                | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| `SSEClientTransport.send()` 401 after re-auth    | `UnauthorizedError`                       | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| 403 `insufficient_scope` after step-up retry cap | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpForbidden`                |\n| Unexpected content type                          | `StreamableHTTPError`                     | `SdkError` + `SdkErrorCode.ClientHttpUnexpectedContent`            |\n| Session termination failed                       | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToTerminateSession` |\n\n```typescript\n// v1\nif (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }\nif (error instanceof StreamableHTTPError) { console.log('HTTP status:', error.code); }\n\n// v2\nimport { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }\nif (error instanceof SdkHttpError) {\n    console.log('HTTP status:', error.status, error.statusText);\n    switch (error.code) {\n        case SdkErrorCode.ClientHttpAuthentication:\n        case SdkErrorCode.ClientHttpForbidden:\n        case SdkErrorCode.ClientHttpFailedToOpenStream:\n        case SdkErrorCode.ClientHttpNotImplemented:\n            break;\n    }\n}\n```\n\n`StreamableHTTPError` is removed.\n\n**Status read off `.code` by duck-typing.** Code that classified HTTP failures by the\nstatus without an `instanceof` — `if ('code' in e && e.code === 403)` — silently stops\nmatching: on `SdkHttpError` the HTTP status moved to `.status` (its `.code` is a\n`SdkErrorCode` string). The codemod renames `instanceof StreamableHTTPError`, but a\nstatus read that never named the class is invisible to it. Watch the inconsistency:\n`SseError` still carries its HTTP status on numeric `.code`, so one duck-typed\n`.code === 401` that caught both transports in v1 now catches only SSE.\n\n```typescript\n// v1 — one duck-typed check caught both Streamable HTTP and SSE\nif ('code' in e && (e.code === 401 || e.code === 403)) reauth();\n// v2 — match each explicitly\nif (e instanceof SdkHttpError && (e.status === 401 || e.status === 403)) reauth(); // Streamable HTTP\nif (e instanceof SseError && (e.code === 401 || e.code === 403)) reauth(); // SSE still uses .code\n```\n\nSilent at runtime (no compile error) — grep for `.code ===` status comparisons.\n\n**Classification keyed on the error class name.** The same import-free classifiers\noften match by name instead of code: telemetry and allowlists keyed on `error.name` or\n`error.constructor.name` against `'McpError'` / `'StreamableHTTPError'` silently stop\nmatching — the v2 classes are named `ProtocolError`, `SdkError`, and `SdkHttpError`,\nand all three assign `.name` accordingly. One v1 asymmetry disappears along the way:\nv1's `StreamableHTTPError` never assigned `.name` (instances reported `'Error'`), so\n`.name`-keyed matchers saw only `'McpError'`; v2's `SdkHttpError` reports\n`'SdkHttpError'`, and assertions pinning `.name === 'Error'` on transport errors need\nre-baselining. Add the v2 names to your match lists; during a\n[staged migration](packaging-runtime.md#migrating-in-stages-large-codebases) keep the v1 names alongside\nfor as long as the v1 package remains installed.\n\n**Status read out of the message text.** Per transport: the Streamable HTTP message\ntext never carried the status (v1 put it on `.code`, v2 puts it on `.status` — read\n`error.status`), and v2's SSE transport still embeds it exactly as v1 did\n(`Error POSTing to endpoint (HTTP 404): …`). The silent break is **switching\ntransports while keeping a message regex**: a status pattern written against SSE\nmatches nothing on Streamable HTTP. Read `error.status` instead of parsing text.\n\n**Raw numeric code comparisons.** The codemod rewrites `ErrorCode.X` symbol references,\nbut a check against the raw JSON-RPC number — `(e as { code?: unknown }).code === -32000`\n— is invisible to it and silently never matches in v2, because the two SDK-local codes\nit usually targeted are now **string** `SdkErrorCode` values:\n\n| v1 numeric                  | v2                                           |\n| --------------------------- | -------------------------------------------- |\n| `-32000` (ConnectionClosed) | `SdkError` + `SdkErrorCode.ConnectionClosed` |\n| `-32001` (RequestTimeout)   | `SdkError` + `SdkErrorCode.RequestTimeout`   |\n\n- Requests that require a session but omit the `Mcp-Session-Id` header still\n  respond `400` with JSON-RPC `-32000` (`Bad Request: Mcp-Session-Id header is\nrequired`), unchanged from v1 — as with `-32001`, the code is an SDK\n  convention; key off the HTTP status.\n\nReplace the literal with the named code. Loud (`TS2367`) when the compared value is\ntyped `SdkErrorCode`; silent when the left side is `unknown` or a cast — grep for\n`=== -32000` / `=== -32001`.\n\n**Dual-role processes: `instanceof` does not cross the packages.**\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` each bundle their own\ncopy of these error classes, so in a process that uses both — a gateway, a host, an\nin-process test — an error constructed by one package fails `instanceof` against the\nclass imported from the other, silently. When an error may originate from the other\npackage, match on stable fields instead of class identity: `error.code` values\n(`SdkErrorCode` strings for SDK errors, numeric JSON-RPC codes for protocol errors,\n`OAuthErrorCode` strings for OAuth errors) plus presence checks like `'status' in e`,\nor reconstruct typed protocol errors with `ProtocolError.fromError(code, message, data)`\n— it exists precisely because `instanceof` does not survive bundle boundaries.\n\n**Constructing the error (test stubs, custom transports).** v1\n`new StreamableHTTPError(code, message)` becomes\n`new SdkHttpError(code, message, data)`: the first argument is now a `SdkErrorCode`\nstring (pick the branch from the scenario table above) and the HTTP status moves into\nthe third argument — `new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented,\n'Not Found', { status: 404, statusText: 'Not Found' })`. v1's implicit\n`Streamable HTTP error: ` message prefix is gone; pass the full message you want.\n\n#### `SdkErrorCode` enum (complete)\n\n| Code                                  | When thrown                                                                |\n| ------------------------------------- | -------------------------------------------------------------------------- |\n| `NotConnected`                        | Transport is not connected                                                 |\n| `AlreadyConnected`                    | Transport is already connected                                             |\n| `NotInitialized`                      | Protocol is not initialized                                                |\n| `CapabilityNotSupported`              | Required capability is not supported                                       |\n| `RequestTimeout`                      | Request timed out waiting for response                                     |\n| `ConnectionClosed`                    | Connection was closed                                                      |\n| `SendFailed`                          | Failed to send message                                                     |\n| `InvalidResult`                       | Response result failed local schema validation                             |\n| `UnsupportedResultType`               | A 2026-era response carried an unrecognized `resultType`                   |\n| `InputRequiredRoundsExceeded`         | Multi-round-trip auto-fulfilment hit `maxRounds`                           |\n| `ListPaginationExceeded`              | No-arg `list*()` aggregate walk hit `listMaxPages`                         |\n| `MethodNotSupportedByProtocolVersion` | Outbound spec method does not exist on the negotiated protocol version     |\n| `EraNegotiationFailed`                | `connect()` could not negotiate a protocol era (probe failed / no overlap) |\n| `ClientHttpNotImplemented`            | HTTP POST request failed                                                   |\n| `ClientHttpAuthentication`            | Server returned 401 after re-authentication                                |\n| `ClientHttpForbidden`                 | Server returned 403 `insufficient_scope` after step-up retry cap           |\n| `ClientHttpUnexpectedContent`         | Unexpected content type in HTTP response                                   |\n| `ClientHttpFailedToOpenStream`        | Failed to open SSE stream                                                  |\n| `ClientHttpFailedToTerminateSession`  | Failed to terminate session                                                |\n\n#### Typed `ProtocolError` subclasses\n\n`ResourceNotFoundError` (carries `.uri`) and `MissingRequiredClientCapabilityError`\n(carries `data.requiredCapabilities`) are new typed `ProtocolError` subclasses.\n`resources/read` for an unknown URI now answers `-32602` on every protocol revision\n(v1.x already emitted `-32602`; an interim `-32002` from earlier v2 alphas is mapped at\nthe encode seam — `2.0.0-alpha.3` and earlier predate the mapping and still emit\n`-32002` on the wire, so accept both if peers may run those alphas; `2.0.0-alpha.4`\nand later emit `-32602`). The encode-seam mapping applies to **your own throws too**: a handler\nthat deliberately throws `ProtocolError(ProtocolErrorCode.ResourceNotFound, …)` reaches\npeers as `-32602` — a server can no longer emit `-32002` on the wire.\n`ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as\nreceive-tolerated vocabulary — accept both `-32602` and `-32002` from peers.\n`ProtocolError.fromError(code, message, data)` reconstructs the typed subclass from\ncode + data alone — the version-agnostic path: it also works on plain wire shapes and\nagainst SDK copies that predate brand-matched `instanceof`.\nThe default message text changed alongside: v1's unknown-resource error read\n`Resource <uri> not found`; v2's `ResourceNotFoundError` default is\n`Resource not found: <uri>` (the code is unchanged). Tests pinning the exact string\nneed re-baselining — prefer matching `error.code` plus a URI substring (or the typed\n`error.uri`).\n\nCustom **non-spec** codes pass through untouched: a handler that throws a\n`ProtocolError` with a custom code (e.g. `-1`) and `data` reaches the peer as a\nJSON-RPC error with that code and `data` unchanged — the encode seam rewrites only the\nlegacy `-32002` code; `data` is sent verbatim for every thrown error (the typed\nsubclasses shape their `data` at construction, not at encode time). Construct via\n`ProtocolError.fromError(code, message, data)`.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/http-headers.md":"### HTTP & headers\n\nHeader **reads** use the Web Standard `Headers` object (`IsomorphicHeaders` is\nremoved): `ctx.http?.req` is a standard Web `Request`, so\n`ctx.http?.req?.headers` takes `.get()` instead of bracket access.\n\n```typescript\n// v1\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = extra.requestInfo?.headers['mcp-session-id'];\n\n// v2 — requestInit is unchanged; only the header *read* changes\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = ctx.http?.req?.headers.get('mcp-session-id');\nconst debug = new URL(ctx.http!.req!.url).searchParams.get('debug');\n```\n\nOn the **write** side, `requestInit` on `StreamableHTTPClientTransport` /\n`SSEClientTransport` options is a standard fetch `RequestInit`, so `headers` accepts\nany `HeadersInit` — a plain object record (as above), a tuple array, or a `Headers`\ninstance all keep working unchanged; the transports normalize whichever form they\nreceive. Wrapping with `new Headers()` is optional, not required.\n\n`StreamableHTTPClientTransport` now **appends** any custom `requestInit.headers.Accept`\nvalue to the spec-required `application/json, text/event-stream` (v1 let it replace\nthem). The required media types are always present; additional types are kept for\nproxy/gateway routing.\n\nTransport-managed headers now take precedence over same-named entries in\n`requestInit.headers`: `Authorization` when `authProvider` yields a token,\n`mcp-protocol-version`, and (Streamable HTTP) `mcp-session-id`. v1 let the configured\nheader win, so a static `Authorization` placeholder kept overriding the OAuth token even\nafter the provider obtained one. A configured `Authorization` value is still sent while\nthe provider has no token, which is what lets a static API key fall back to OAuth.\n\n`hostHeaderValidation()` and `localhostHostValidation()` moved to\n`@modelcontextprotocol/express`. The `(allowedHostnames: string[])` signature is the\nsame as every released v1.x — only the import path changes. Framework-agnostic helpers\n(`validateHostHeader`, `localhostAllowedHostnames`, `hostHeaderValidationResponse`) are\nin `@modelcontextprotocol/server`.\n\nServer entries validate the request `Content-Type` by its **parsed media type**, not a\nsubstring: every POST whose media type is not `application/json` answers\n`415 Unsupported Media Type`. Previously any value merely containing the substring\npassed (for example `text/plain; a=application/json`), case variants were wrongly\nrejected, and the 2026-07-28 entry did not inspect `Content-Type` at all — so\nhand-rolled clients that omit the header (or send a non-JSON type) must now set\n`Content-Type: application/json`. Parameters (`; charset=utf-8`) and unambiguous\nvalues with malformed parameter sections (`application/json;`) keep working; SDK\nclients always sent the correct header and are unaffected. Custom entries that\ncompose `classifyInboundRequest` / `PerRequestHTTPServerTransport` directly must\napply the same validation themselves — use the exported `isJsonContentType(header)`.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/imports-transports.md":"### Imports & transports\n\nThe codemod rewrites every `@modelcontextprotocol/sdk/...` import path via\n[`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).\nA few transports need a decision the codemod can't make:\n\n- **`StreamableHTTPServerTransport` → which runtime?** The codemod renames it to\n  `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. If you deploy\n  to a web-standard runtime (Cloudflare Workers, Deno, Bun), use\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`\n  instead. **Decision rule:** if your handler receives a Node `IncomingMessage` /\n  `ServerResponse`, use `@modelcontextprotocol/node`; if it receives a web-standard\n  `Request` and returns a `Response`, use `@modelcontextprotocol/server`.\n- **stdio transports moved to a `./stdio` subpath.** Import `StdioClientTransport`,\n  `getDefaultEnvironment`, `DEFAULT_INHERITED_ENV_VARS`, and `StdioServerParameters`\n  from `@modelcontextprotocol/client/stdio`; import `StdioServerTransport` from\n  `@modelcontextprotocol/server/stdio`. The package root barrels do **not** export\n  these (the root entries are runtime-neutral so browser/Workers bundlers can consume\n  them). The stdio utilities `ReadBuffer`, `serializeMessage`, `deserializeMessage`\n  stay in the root barrel.\n- **Zod `*Schema` constants → `@modelcontextprotocol/core`.** A mixed\n  `import { CallToolResult, CallToolResultSchema } from '…/types.js'` is split by the\n  codemod — see [Types & schemas](types-schemas.md#types--schemas).\n\n    ```typescript\n    // v1\n    import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n    // v2\n    import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';\n    ```\n\n- **`SSEServerTransport`** is removed. Migrate to Streamable HTTP. A frozen v1 copy is\n  available from `@modelcontextprotocol/server-legacy/sse` as a temporary bridge.\n- **`WebSocketClientTransport`** is removed (WebSocket is not a spec transport). Use\n  `StreamableHTTPClientTransport` for remote servers or `StdioClientTransport` for\n  local servers; the `Transport` interface is exported if you need a custom\n  implementation.\n- **`InMemoryTransport`** is now exported from `@modelcontextprotocol/client` and\n  `@modelcontextprotocol/server` (both re-export it). The two packages bundle separate\n  copies with private state, so the halves of a linked pair must come from the **same\n  package's** import — pick one package per file (per linked pair) rather than mixing\n  the client's `InMemoryTransport` with the server's:\n\n    ```typescript\n    // v1\n    import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';\n    // v2\n    import { InMemoryTransport } from '@modelcontextprotocol/server'; // or /client\n    ```\n\n- **`EventStore`, `StreamId`, `EventId`** are exported from `@modelcontextprotocol/server`\n  only (v1 re-exported them alongside the transport from `sdk/server/streamableHttp.js`;\n  `@modelcontextprotocol/node` does not).\n- **Client fetch middleware moved to the root barrel.** `createMiddleware`,\n  `applyMiddlewares`, `withLogging`, `withOAuth`, and the `Middleware` type (v1:\n  `sdk/client/middleware.js`) are now exported from `@modelcontextprotocol/client`\n  directly, as is `FetchLike` (v1: `sdk/shared/transport.js`). The call signatures are\n  unchanged from v1 (`Middleware` is still `(next: FetchLike) => FetchLike`) — only the\n  import path changes.\n- **Server auth split.** Resource Server helpers (`requireBearerAuth`,\n  `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`)\n  → `@modelcontextprotocol/express`; the runtime-neutral core (`requireBearerAuth`\n  for web-standard `fetch` hosts, `verifyBearerToken`, `bearerAuthChallengeResponse`,\n  `OAuthTokenVerifier`, and the discovery serving `oauthMetadataResponse` /\n  `buildOAuthProtectedResourceMetadata` / `getOAuthProtectedResourceMetadataUrl`)\n  is also exported from `@modelcontextprotocol/server`. Authorization Server helpers (`mcpAuthRouter`,\n  `OAuthServerProvider`, `ProxyOAuthServerProvider`, `allowedMethods`,\n  `authenticateClient`, `metadataHandler`, `createOAuthMetadata`,\n  `authorizationHandler` / `tokenHandler` / `revocationHandler` /\n  `clientRegistrationHandler`) → `@modelcontextprotocol/server-legacy/auth`\n  (deprecated, frozen v1 copy); migrate AS to a dedicated IdP/OAuth library. `AuthInfo`\n  is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.\n\n    The codemod's [`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)\n    routes every `…/server/auth/**` deep path (including\n    `…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`,\n    `…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to\n    `@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` /\n    `…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The\n    AS→`server-legacy` routing is conservative — re-point RS-only call sites\n    (`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand.\n    Staying on the frozen `server-legacy/auth` copy is a supported interim choice when you\n    deliberately want the v1 middleware behavior. If you re-point at\n    `@modelcontextprotocol/express` by hand, also add that package — plus its `express`\n    peer dependency — to your manifest: the codemod's manifest summary reflects only the\n    imports it wrote, not re-points you make afterwards.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/packaging-runtime.md":"### Packaging & runtime\n\nThe single `@modelcontextprotocol/sdk` package is split:\n\n| v1                              | v2                                                                                                                              |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk`     | `@modelcontextprotocol/client` (client implementation)                                                                          |\n|                                 | `@modelcontextprotocol/server` (server implementation)                                                                          |\n|                                 | `@modelcontextprotocol/core` (public Zod `*Schema` constants)                                                                   |\n|                                 | `@modelcontextprotocol/core-internal` (internal — never import directly)                                                        |\n| Built-in HTTP framework support | `@modelcontextprotocol/node` / `@modelcontextprotocol/express` / `@modelcontextprotocol/hono` / `@modelcontextprotocol/fastify` |\n\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared\ntypes from `@modelcontextprotocol/core-internal`, so import types and error classes from\nwhichever package you already depend on. `@modelcontextprotocol/core-internal` is\n`private: true` and is not published — **do not import from it directly.**\n`@modelcontextprotocol/core` is the public Zod-schema package (raw `*Schema` constants\nonly); see [Zod `*Schema` constants moved to `@modelcontextprotocol/core`](types-schemas.md#zod-schema-constants-moved-to-modelcontextprotocolcore) below.\n\nAfter the codemod runs, review the manifest summary it prints: the swap rewrites the\n**nearest** manifest found walking up from the target directory — one manifest total.\nWorkspace-member manifests in a monorepo are never modified; instead the codemod lists\neach member that still declares the v1 SDK together with the exact dependency changes\nit needs (remove the v1 entry, add the v2 packages that member's imports use) — apply\nthose edits yourself, then install. The v2 additions are computed from the final import\nstate of each package's sources, so already-migrated sources still receive the v2\npackages they need when the v1 dependency is removed. In a hoisted monorepo (members\nwithout their own SDK dependency), member usage counts toward the manifest that\ndeclares the v1 SDK, and the summary notes which members contributed. See\n[Monorepo workspace members](#monorepo-workspace-members) for how to decide each\nmember's packages.\n\n#### Monorepo workspace members\n\nDeclare in every member exactly what its own sources import: files importing\n`@modelcontextprotocol/server` (or its subpaths) need `@modelcontextprotocol/server`;\nclient imports need `@modelcontextprotocol/client`; raw `*Schema` constants need\n`@modelcontextprotocol/core`; a framework adapter import (`@modelcontextprotocol/express`\netc.) needs the adapter package **plus the framework itself** in that member (the\nadapter declares it as a peer dependency). Place a package in `dependencies` when\nshipped runtime code imports it and in `devDependencies` when only tests, fixtures, or\nlocal tooling do — when in doubt, use the section where the member previously declared\n`@modelcontextprotocol/sdk`.\n\nA member that never declared the v1 SDK and resolved it through the root can keep\nroot-level declarations (the codemod's root rewrite already adds the union of the\ncontributing members' v2 packages — its hoisting note names them) or move to\nper-member declarations; per-member is recommended, since the v2 package split makes each member's\nactual needs explicit. To answer \"which packages does this member need\" directly, run\nthe codemod against that member's directory with `--dry-run`: the manifest summary is\ncomputed from that member's own imports. (The authoritative import-path routing lives\nin the codemod's [mapping file](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).)\n\nThe framework adapter packages declare their framework as a **peer dependency**\n(`express`, `hono`, `fastify`); v1 shipped them as direct deps. The codemod adds the\n`@modelcontextprotocol/*` packages your imports use, but does not add the framework\npeer — install it explicitly (`pnpm add express` etc.). `@modelcontextprotocol/node`\ndepends on `@hono/node-server` at runtime (Node HTTP ↔ Web Standard conversion) and\ninstalls `hono` as a regular dependency only because `@hono/node-server` requires it as\na peer — the adapter does not use the `hono` framework itself.\n\nv2 requires **Node.js 20+**. It is ESM-first but ships a **CommonJS build alongside\nESM**, so CommonJS projects can `require('@modelcontextprotocol/…')` directly — no\ndynamic `import()` shim required.\n\nRepo-local tooling that encodes the literal v1 package name — dependency-pin lints,\nversion allowlists, CI checks, scripts — fails after the manifest swap and is invisible\nto the codemod (it rewrites sources and manifests, not bespoke gates). Grep for\n`@modelcontextprotocol/sdk` outside `src/` before declaring the migration done. While\ngrepping, also remove v1-era double casts on SDK types (`as unknown as Transport` and\nsimilar, usually annotated to a v1 issue) — v2's types satisfy those contracts\ndirectly, and a surviving cast keeps suppressing type checking that would otherwise\ncatch real errors.\n\nTooling that pins SDK **dist text** (reading a constant out of a built file with\n`require.resolve` + a regex) breaks in two stacked ways: the literal usually lives in a\ncontent-hashed sibling chunk (`dist/sse-<hash>.mjs`), not the subpath's entry module,\nso fixed-path reads do not survive a rebuild — scan the package's `dist/` directory\nfor the literal instead; and the emitted quote style differs from v1, so a\nquote-anchored pattern misses silently — match either quote. The build layout also\nchanged: v2 emits `.mjs`/`.cjs` siblings in a flat `dist/`, so v1's `/dist/cjs/` ↔\n`/dist/esm/` flavor-pair path swaps have no equivalent.\n\n#### Registry availability\n\nAll v2 packages are published on the public npm registry. Two notes:\n\n- As of `2.0.0-beta.1` all v2 packages share one version number (earlier alphas\n  did not). The codemod writes ranges that match what is published, so prefer its\n  manifest output over hand-pinning every package.\n- Environments that resolve through a corporate or private registry mirror may not\n  have synced the newer scoped packages yet (the symptom is \"not found\" for a package\n  that exists on npmjs.org). Point the install at the public registry\n  (`npm install --registry=https://registry.npmjs.org/` or the equivalent `.npmrc`\n  entry), ask your mirror's operators to sync the `@modelcontextprotocol` scope, or —\n  where neither is possible — build a tarball from a checkout of this repository\n  (`pnpm install && pnpm build`, then `pnpm pack` in the package directory) and\n  reference it with a committed `file:` dependency.\n\n#### CommonJS test runners (Jest)\n\nv2 ships a CommonJS build, so CJS test runners resolve the packages natively through the\n`require` export condition — Jest (including `next/jest` setups) no longer needs a\n`moduleNameMapper` workaround to import `@modelcontextprotocol/*`. If you carried a\nv1-era mapping that pinned these packages to their `dist/*.mjs` files, remove it. Vitest\nand native Node ESM are unaffected.\n\n#### Bundlers: nested `zod` copies in zod@3-pinned monorepos\n\nv1's `zod ^3.25 || ^4.0` peer range deduplicated onto a workspace's hoisted zod@3. The\nv2 packages depend on `zod ^4.2.0`, so in a workspace that pins zod@3 the dependency\ncannot dedupe — each installed v2 package resolves its own nested zod@4 copy. Two\nbundler consequences:\n\n- **Path-substring vendor pins capture the nested copies.** Bundler rules that match\n  zod by module path — `manualChunks` pins, vendor-chunk matchers, bundle budgets keyed\n  on a `zod/` path segment — also match `@modelcontextprotocol/*/node_modules/zod`,\n  which can pull the nested copies into an eagerly-loaded vendor chunk and trip a\n  budget gate. Exclude the SDK-nested paths from such pins so the copies ride with the\n  SDK's own (typically lazy) chunks.\n- **Ballpark size cost.** Measured on a large production SPA, adding the v2 client and\n  server packages (with their nested zod@4 copies) alongside a hoisted zod@3 cost\n  roughly +83 KB gzipped of total JS (about +0.7% whole-app). Upgrading the workspace\n  to `zod ^4.2.0` re-dedupes and removes the duplication.\n\n#### Migrating in stages (large codebases)\n\nThe v1 package and the v2 packages have **different names**, so both can be installed\nin one manifest at the same time — nothing forces a one-shot swap. The safe order for\nan incremental migration: (1) add the v2 packages (and the `zod ^4.2.0` bump) while\n**keeping** `@modelcontextprotocol/sdk`; (2) rewrite sources incrementally,\ndirectory-by-directory or package-by-package; (3) remove the v1 dependency only when\nnothing imports it any more (`grep -rn \"@modelcontextprotocol/sdk\" --include=\"*.ts\"`,\nplus a look at `package.json`). The inverse order strands files: swapping the manifest\nfirst leaves every not-yet-rewritten import failing module resolution (TS2307) until it\nis updated.\n\nTwo caveats for the transition window. First, a codemod run against a subdirectory\nstill updates the nearest manifest walking up — including removing the v1 dependency —\nso during a staged pass review or revert that edit until the final stage (or preview\nwith `--dry-run`). Second, v1 and v2 modules each have their own classes and types:\nobjects must not flow between v1-imported and v2-imported code (`instanceof` and\nnominal types do not cross — the same boundary described for dual-role processes in\n[Errors](errors.md#errors)), so stage along process or transport boundaries where the two sides\nshare only the wire format; the two sides negotiate\na protocol version through the ordinary 2025-era `initialize` handshake and settle\non the newest revision both packages support (currently 2025-11-25 — published v1\n1.29.x and v2 ship the same supported-version list).\n\nDependencies you do not control (vendored fixtures, third-party packages) that still\ndeclare `@modelcontextprotocol/sdk` resolve their own v1 copy and need no action. For\n`peerDependencies` declarations, keep the v1 package installed to satisfy the range —\nor point the name at a chosen version via your package manager's\n`overrides`/`resolutions` — until those packages migrate. The same boundary rule\napplies: objects must not flow between their v1-imported code and your v2-imported\ncode.\n\n**Dependencies that compile against the host's v1 SDK.** A stricter variant of the\nabove: a workspace or vendored package that ships TypeScript **source** importing\n`@modelcontextprotocol/sdk` — resolved from the host's `node_modules` rather than its\nown — pins the host. Keep the v1 package installed as a real dependency (not merely a\nsurviving transitive) until that package migrates. The host files that construct or\nhand objects to such a package are part of its v1 boundary and must stay on v1 imports\n— and the codemod cannot see that distinction: it rewrites them like any other file\n(e.g. converting a `setRequestHandler(Schema, …)` call into the v2 method-string form\nagainst what is still a v1 `Server`, which then fails at runtime). Run the codemod with\n`--ignore` glob patterns covering those interfacing files, and migrate them together\nwith the dependency later. The boundary rule above applies unchanged: objects from the\ndependency's v1 modules must never flow into v2-imported code.\n\n#### Library authors: peer-depending on the SDK\n\nIf your package declares `@modelcontextprotocol/sdk` as a `peerDependency`, the v2\npackages are differently **named**, so swapping the peer declaration is itself a\nbreaking change for every consumer — ship it as a semver-major. You can migrate\nahead of your consumers only if no SDK object crosses your public API (the\nv1/v2 boundary rule above applies to your exports too: a v1-constructed `Client`\nor error instance handed to v2-importing consumer code fails `instanceof` and\nnominal checks). Until your consumers migrate, they can keep resolving your peer\nrange with the v1 package installed alongside their own v2 packages — the two\ncoexist under different names.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/protocol-ctx.md":"### Low-level protocol & handler context (`ctx`)\n\nThe second parameter to every request handler — previously the flat `RequestHandlerExtra`\nobject named `extra` — is now a structured **context** object named `ctx`. This is the\n`ctx` that appears throughout the rest of this guide.\n\nThe codemod renames the parameter and remaps property access via\n[`contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts).\nA few mappings need optional-chaining adjustment (the `http` group is `undefined` on\nstdio):\n\n| v1 (`extra.*`)                                    | v2 (`ctx.*`)                   | Note                                                                                                                                                                                                                                                                                                                    |\n| ------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `extra.signal`                                    | `ctx.mcpReq.signal`            |                                                                                                                                                                                                                                                                                                                         |\n| `extra.requestId`                                 | `ctx.mcpReq.id`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra._meta`                                     | `ctx.mcpReq._meta`             |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendRequest(...)`                          | `ctx.mcpReq.send(...)`         |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendNotification(...)`                     | `ctx.mcpReq.notify(...)`       |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sessionId`                                 | `ctx.sessionId`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra.authInfo`                                  | `ctx.http?.authInfo`           | optional — `undefined` on stdio                                                                                                                                                                                                                                                                                         |\n| `extra.requestInfo`                               | `ctx.http?.req`                | a standard Web `Request`; `ServerContext` only                                                                                                                                                                                                                                                                          |\n| `extra.closeSSEStream`                            | `ctx.http?.closeSSE`           | `ServerContext` only; the member itself is also optional — defined only when the transport has an `eventStore` AND the client's negotiated protocol version supports resumable close (2025-11-25+); an `eventStore` transport serving a 2025-06-18 client still leaves it `undefined`. Call as `ctx.http?.closeSSE?.()` |\n| `extra.closeStandaloneSSEStream`                  | `ctx.http?.closeStandaloneSSE` | `ServerContext` only; member optional as above — `ctx.http?.closeStandaloneSSE?.()`                                                                                                                                                                                                                                     |\n| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed_                      | see [Experimental tasks](behavioral-changes.md#experimental-tasks-interception-removed)                                                                                                                                                                                                                                 |\n\nThe transport-level seam behind `ctx.http?.authInfo` is unchanged from v1: a transport\nthat passes `{ authInfo }` as the second argument to `onmessage(message, extra)` — e.g.\nan `InMemoryTransport` test seam — still surfaces it as `ctx.http?.authInfo` on any\ntransport, and `ctx.http` is defined whenever `authInfo` is supplied, even without an\nHTTP transport.\n\n`BaseContext` is the common base; `ServerContext` and `ClientContext` extend it. None\nof the three takes type parameters — v1's `RequestHandlerExtra<TRequest, TNotification>`\narguments selected request/notification unions that the v2 context carries\nintrinsically, so their removal loses no type information; review only handlers that\npassed custom (non-standard) unions, whose `sendRequest` / `sendNotification` typing\nwas narrowed by them. `ServerContext.mcpReq` adds convenience methods that replace\ncalling `server.*` from inside a handler:\n\n| `ctx.mcpReq.*` (new)                           | Replaces (inside a handler)                                                                                                                                                                                                                                                                              |\n| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ctx.mcpReq.log(level, data, logger?)`         | `server.sendLoggingMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577); the notification also becomes request-related on every era — see [§`ctx.mcpReq.log()` is request-related on every era](behavioral-changes.md#ctxmcpreqlog-is-request-related-on-every-era) |\n| `ctx.mcpReq.elicitInput(params, options?)`     | `server.elicitInput(...)`                                                                                                                                                                                                                                                                                |\n| `ctx.mcpReq.requestSampling(params, options?)` | `server.createMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577)                                                                                                                                                                                                  |\n\n#### Deprecated in v2 (SEP-2577)\n\nThe roots, sampling, and logging subsystems are deprecated as of protocol version\n2026-07-28 (SEP-2577). Everything below is **still fully functional in v2** and marked\n`@deprecated` for removal in a later major; on a 2026-07-28 connection prefer the\n[multi-round-trip `input_required` pattern](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#multi-round-trip-requests)\ninstead.\n\n- **Runtime APIs**: `Server.createMessage` / `listRoots` / `sendLoggingMessage`,\n  `McpServer.sendLoggingMessage`, `Client.setLoggingLevel` / `sendRootsListChanged`, and\n  the `ctx.mcpReq.log` / `ctx.mcpReq.requestSampling` handler-context helpers. Outside a\n  handler, `McpServer` users reach the `Server.*` methods via the unchanged\n  [`mcpServer.server` accessor](unchanged-apis.md#unchanged-apis).\n- **Capability fields**: the `roots`, `sampling`, and `logging` capability schema fields.\n- **Type stacks**: the full Logging stack (`LoggingLevel`, `SetLevelRequest`,\n  `LoggingMessageNotification` and params), the full Sampling stack\n  (`CreateMessageRequest`/`Result`, `SamplingMessage`, `ModelPreferences`/`ModelHint`,\n  `ToolChoice`, `ToolUseContent`/`ToolResultContent`, the `includeContext` enum values),\n  and the full Roots stack (`Root`, `ListRootsRequest`/`Result`,\n  `RootsListChangedNotification`).\n- **`registerClient`** (Dynamic Client Registration) — deprecated via spec PR\n  [modelcontextprotocol#2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)\n  rather than SEP-2577 (listed here because the `@deprecated` annotations landed in\n  the same sweep); prefer Client ID Metadata Documents per SEP-991.\n\nThe deprecation is annotation-only — JSDoc `@deprecated` markers were added, nothing\nelse: every deprecated runtime API keeps its v1 call signature (e.g.\n`Server.sendLoggingMessage(params, sessionId?)` keeps the two-argument form) and its\nwire behavior, and remains functional for at least the twelve-month deprecation window.\n\n#### `setRequestHandler` / `setNotificationHandler` use method strings\n\nThe low-level handler registration takes a **method string** instead of a Zod schema.\nThe codemod rewrites every spec-method registration via\n[`schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts).\n\n```typescript\n// v1\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => { ... });\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => { ... });\n```\n\n**Custom (non-spec) methods** use the 3-arg form `(method, { params, result? }, handler)`\nwhere `params` and `result` are any [Standard Schema](https://standardschema.dev). The\nhandler receives the parsed `params` directly (not the full request envelope); `_meta`\nis at `ctx.mcpReq._meta`. The 3-arg notification handler is `(params, notification) => void`.\n\n```typescript\nserver.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => { ... });\n```\n\nThe custom form also covers **spec method names carried with custom payloads**: a v1\nintegration that reused a spec method string for its own payload shape (e.g.\n`notifications/message` notifications carrying a proprietary params object) registers\nit with the 3-arg form and its own schema. The overloads are selected by the arguments'\nshape, not by the method name — a schemas object as the second argument always selects\nthe custom form, which validates against **your** schema (the spec schema is not\napplied) and hands the handler the parsed params rather than the envelope.\n\n**Spec notifications** use the 2-arg form `setNotificationHandler(method, handler)`.\nUnlike the 3-arg custom form, the spec-form handler receives the **full notification\nenvelope** (`{ method, params }`), parsed against the spec schema — read\n`notification.params`:\n\n```typescript\nclient.setNotificationHandler('notifications/tools/list_changed', async notification => {\n    console.log(notification.method, notification.params);\n});\n```\n\nThe two overloads are selected by the method string's **type**: the spec form binds the\nmethod to the `NotificationMethod` union (`RequestMethod` on the request side — both\nexported), so a method string computed at runtime must be typed as `NotificationMethod`\nto select it; an untyped `string` lands on the custom-schema overload and fails to\ncompile without a schemas argument. `Parameters<Client['setNotificationHandler']>[0]`\nalso resolves to the custom `string` overload by design — name `NotificationMethod`\ndirectly instead. The request side has the same trap one slot over:\n`Parameters<Client['setRequestHandler']>` (and `typeof`-indexed casts over the overload\nset) resolve against the 3-arg custom-method overload, so index `[1]` is the\n`{ params, result }` schemas object, **not** the handler — v1 signature-erasing handler\ncasts derived positionally change meaning with no runtime symptom. Name the exported\ntypes (`RequestMethod` and your own handler/param types) instead of deriving them\npositionally. Generic helpers that v1 parameterized on a notification schema need\nthis conversion by hand; the codemod only warns on them.\n\n**Handler returns are spec-typed.** In v1 the handler's return type flowed from the\nschema you registered; v2 types it from the method name (`'tools/list'` →\n`ListToolsResult`, and so on). Tool tables kept as plain object literals surface two\nrecurring compile errors: an unannotated literal widens `type: 'object'` to `string`\nand no longer satisfies the spec type's `type: 'object'` literal member (fix:\n`type: 'object' as const`, or annotate the table as `Tool[]`); and a heterogeneous\ntable whose inferred union carries `prop?: undefined` members does not satisfy the spec\ntypes' `Record<string, JSONValue>` index signatures, since `undefined` is not a\n`JSONValue` (fix: annotate the handler's return type —\n`async (req): Promise<ListToolsResult> => …` — or the table itself, so each literal is\nchecked against the target type instead of being inferred and widened first).\n\n#### `request()`, `ctx.mcpReq.send()`, and `callTool()` no longer require a schema for spec methods\n\nFor **spec** methods, drop the result-schema argument; the SDK resolves it from the\nmethod name. The codemod drops it from `client.request()` and `client.callTool()`; drop\nit from `ctx.mcpReq.send()` by hand.\n\n```typescript\n// v1\nimport { CreateMessageResultSchema } from '@modelcontextprotocol/sdk/types.js';\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n    const r = await extra.sendRequest({ method: 'sampling/createMessage', params: { ... } }, CreateMessageResultSchema);\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => {\n    const r = await ctx.mcpReq.send({ method: 'sampling/createMessage', params: { ... } });\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n```\n\nFor **custom (non-spec)** methods, keep the result-schema argument:\n`await client.request({ method: 'acme/search', params }, SearchResult)` — only drop the\nschema when calling a spec method.\n\n**Forwarding arbitrary methods (gateways / proxies).** Dropping the schema changes\nsemantics, not just the signature: a schema-less spec-method call now **enforces** the\nspec result schema (a non-conforming upstream result is rejected locally with\n`SdkError(SdkErrorCode.InvalidResult)` and a conforming one is re-serialized in schema\nkey order), and a schema-less call for a **non-spec** method throws a `TypeError` at\nthe call site (`'…' is not a spec method; pass a result schema`).\nA relay that forwards `{ method, params }` it does not understand must keep passing an\nexplicit result schema. The v1 idiom survives with an import-path change:\n\n```typescript\nimport { ResultSchema } from '@modelcontextprotocol/core';\nconst result = await upstream.request({ method, params }, ResultSchema); // v1-identical passthrough\n```\n\nFor byte-exact forwarding (member order preserved), pass your own accept-anything\nStandard Schema instead. Check call sites whose `method` is **not a literal** — the\ncodemod may have dropped the schema argument there; restore it.\n\nThe **inbound half** — a relay re-emitting an upstream JSON-RPC error from its own\nhandler — has a supported surface too: reconstruct the typed error with\n`ProtocolError.fromError(code, message, data)` and throw it; the encode seam serializes\nit back to the wire shape (see [Typed `ProtocolError` subclasses](errors.md#typed-protocolerror-subclasses)).\nNote this is typed reconstruction, not byte-exact relay: legacy codes are normalized at\nthe encode seam (`-32002` re-emits as `-32602`) and the typed subclasses keep only their\nschema-defined `data` members, so extra upstream data keys are dropped. Throwing a plain\nobject carrying `.code` / `.message` / `.data` happens to work today, but it is\nunspecified behavior — prefer `fromError`.\n\nThe return type is inferred from the method name via `ResultTypeMap` (e.g.\n`client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`).\nv1 call sites that passed `CreateMessageResultWithToolsSchema` explicitly need no\nreplacement: the schema-less send resolves to\n`CreateMessageResult | CreateMessageResultWithTools`, and validation selects the\nwith-tools variant when the request set `tools` or `toolChoice`.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md":"### Server registration API\n\nThe deprecated variadic `.tool()`, `.prompt()`, `.resource()` are removed. Use\n`registerTool` / `registerPrompt` / `registerResource` with an explicit config object.\nThe codemod converts the call shape and wraps `inputSchema` / `outputSchema` /\n`argsSchema` / `uriSchema` raw shapes.\n\n```typescript\n// v1 — raw shape, variadic\nserver.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n\n// v2 — config object, Standard Schema\nserver.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n```\n\n`registerResource` requires a `metadata` argument — pass `{}` if you have none.\n\nA tool or prompt registered **without** an `inputSchema` / `argsSchema` passes the\ncontext as its callback's single argument — v1 passed `(extra)`, v2 passes `(ctx)`:\n\n```typescript\nserver.registerTool('ping', { description: 'Liveness check' }, async ctx => ({ content: [] }));\n```\n\nA one-parameter callback typechecks under either reading, so remember that the first\nparameter here is the context object, not an args object.\n\n#### Standard Schema objects (raw shapes deprecated)\n\nv2 expects schema objects implementing the [Standard Schema spec](https://standardschema.dev/)\nfor `inputSchema`, `outputSchema`, and `argsSchema`. Raw `{ field: z.string() }` shapes\nare still **accepted via `@deprecated` overloads** on `registerTool`/`registerPrompt`\n(auto-wrapped with `z.object()`), and `completable()` accepts any `StandardSchemaV1`;\nprefer wrapping explicitly. Zod v4, ArkType, and Valibot all implement the spec.\n\nFor **optional completable arguments**, apply `.optional()` to the _result_ of\n`completable()` — `completable(z.string(), cb).optional()`, not\n`completable(z.string().optional(), cb)`. v2 resolves completion metadata on the schema\nfound after unwrapping an outer optional wrapper, so the v1 nesting returns empty\ncompletion lists — nothing errors — and if no argument carries completion metadata in\nthe v2 position, the server does not advertise the `completions` capability at all. The\ncodemod inverts the common nesting automatically and flags shapes it cannot rewrite.\n\n**Zod v3 is no longer supported** (v1 peer was `^3.25 || ^4.0`). Check the **declared\nrange** in your `package.json`, not just the installed version: a zod-3 range that\nsatisfied the v1 peer installs and typechecks cleanly under v2 and only fails at\nruntime — and quietly: registration does not convert the schema, the server starts\nand connects normally, and the first `tools/list` (so `client.listTools()`) answers\nwith an error pointing at `fromJsonSchema()` while the process keeps running. (Only the\ndeprecated unwrapped raw-shape form with zod-3 field values throws at registration,\nwith a message pointing at `zod/v4`.) Zod **≥4.2.0** self-converts via\n`~standard.jsonSchema` — the supported path. Zod **4.0–4.1** lacks it, so the SDK falls\nback to its bundled Zod's `z.toJSONSchema()` with a one-time `[mcp-sdk]` console\nwarning; and because `.describe()` field descriptions live in the _authoring_ Zod's\nregistry, the fallback **drops them** from the generated JSON Schema. Fix ladder:\n(1) upgrade to `zod ^4.2.0`; (2) if you must pin an older or separate Zod, attach a\n`~standard.jsonSchema` provider backed by _your_ Zod's `toJSONSchema` so conversion\n(and descriptions) run through your instance; (3) author the schema as raw JSON Schema\nvia `fromJsonSchema()`. (Raw shapes are wrapped with the SDK's **bundled** Zod — built\nwith a foreign Zod they fail at registration or at the first `tools/list`; pass\n`z.object()`-wrapped schemas from your own Zod instead.)\n\nIn a monorepo that pins zod@3 workspace-wide and cannot bump, step (1) can be applied\n**per workspace member**: add a zod-4 alias dependency to the migrating member only —\n`\"zod-v4\": \"npm:zod@^4.2.0\"` in that member's `package.json` — and author SDK-bound\nschemas with it (`import { z } from 'zod-v4'`), leaving the rest of the workspace, and\nthe member's own zod-3 consumer schemas, untouched. The alias copy does not need to be\nthe same instance as the SDK's bundled zod: conversion runs through the **authoring**\ninstance's `~standard.jsonSchema`, so `.describe()` descriptions are preserved and the\nemitted dialect is 2020-12. Keep the two z's apart — schemas authored with the alias\nare for the SDK; they do not compose with the workspace's zod-3 schemas. (For the\nbundle-side effects of the same pin, see\n[Bundlers: nested `zod` copies](packaging-runtime.md#bundlers-nested-zod-copies-in-zod3-pinned-monorepos).)\n\n**Hosts that forward consumer-authored schemas.** The ladder assumes you author the\nschemas yourself. A host API that accepts raw shapes or schemas written by **its own\nconsumers** — plugin systems, agent frameworks — cannot control the authoring zod\nversion or instance, and v1's built-in conversion of foreign shapes is gone. Convert on\nthe host side and register the result with `fromJsonSchema()`: zod-4 input via zod's\nown `z.toJSONSchema(z.object(shape), { io: 'input', target: 'draft-2020-12' })` (the\nconversion is runtime-structural, so a zod ≥4.2 in the host handles schemas built by a\ndifferent zod-4 copy), zod-3 input via the\n[`zod-to-json-schema`](https://www.npmjs.com/package/zod-to-json-schema) package. Its\ndefault draft-07 `$schema` stamp is fine as-is — the default validator\n[honors declared draft-07/06 dialects](types-schemas.md#json-schema-2020-12-posture-sep-1613-sep-2106).\n\nHow a too-old zod surfaces depends on which entry point your code imports. With\nmain-entry `import { z } from 'zod'` on a zod-3 range, the project **typechecks cleanly\nand fails at the first `tools/list`** (the quiet runtime path above). With\n`import * as z from 'zod/v4'` — or any zod whose _typings_ predate\n`~standard.jsonSchema` (zod 4.0–4.1, and zod 3.25.x via the `zod/v4` subpath) — the\nsame code **runs** through the bundled fallback but **fails to compile**:\n`registerTool`/`registerPrompt` reject the schema with `TS2769: No overload matches\nthis call` listing both overloads. The real cause is buried in the first overload's\nelaboration — `Property 'jsonSchema' is missing in type …` (that property is\n`~standard.jsonSchema`, added in zod 4.2.0) — and a follow-on implicit-`any` error on\nthe handler's arguments usually appears below it. If you see that two-overload error on\na registration call with a zod schema, check the installed zod version before anything\nelse; both symptoms resolve identically with step (1) of the ladder.\n\nProjects that must stay below zod 4.2 and accept the documented runtime fallback can\nresolve the remaining registration compile errors with an explicit assertion to the\nregistration schema type — `inputSchema: schema as unknown as\nStandardSchemaWithJSON<Input, Output>` — or a small typed wrapper that attaches a\n`~standard.jsonSchema` provider (step (2) of the ladder, which changes runtime\nconversion but not the schema's static type) and returns the asserted type. The\nfallback caveats (one-time warning, dropped `.describe()` descriptions) still apply\nunless the provider is attached.\n\nThe forced zod-4 bump also surfaces zod's **own** type-level API changes in consumer\nannotations: `z.ZodTypeDef` no longer exists and `z.ZodType`'s generic parameters\nchanged, so v3-era annotations like `z.ZodType<Output, z.ZodTypeDef, Input>` fail to\ncompile — see [zod's v3-to-v4 changelog](https://zod.dev/v4/changelog). Consumer-only\nschemas can keep compiling via zod's v3 compat subpath (`zod/v3`), but anything passed\nto the SDK must be a zod-4 (or other Standard Schema) schema.\n\nThe deprecated raw-shape overloads exist only on `registerTool` / `registerPrompt`.\n`RegisteredTool.update()` / `RegisteredPrompt.update()` take **schema objects**\n(`paramsSchema` / `outputSchema`: `StandardSchemaWithJSON`) — a raw shape passed to\n`update()` is not auto-wrapped; wrap it with `z.object()` yourself.\n\n```typescript\nimport * as z from 'zod/v4';\nserver.registerTool('greet', { inputSchema: z.object({ name: z.string() }) }, handler);\n\n// ArkType works too\nimport { type } from 'arktype';\nserver.registerTool('greet', { inputSchema: type({ name: 'string' }) }, handler);\n\n// Raw JSON Schema via fromJsonSchema (validator defaults to runtime-appropriate choice)\nimport { fromJsonSchema } from '@modelcontextprotocol/server';\nserver.registerTool('greet', { inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }) }, handler);\n\n// No-parameter tools: z.object({})\n```\n\nRemoved Zod-specific helpers (the codemod marks each call site `@mcp-codemod-error`):\n`schemaToJson` — use `fromJsonSchema()` from `@modelcontextprotocol/server` for raw JSON\nSchema, or your schema library's native JSON-Schema conversion; `parseSchemaAsync` — use\nyour schema library's validation directly (e.g. Zod's `.safeParseAsync()`);\n`getSchemaShape` / `getSchemaDescription` / `isOptionalSchema` / `unwrapOptionalSchema`\nhave no replacement (internal Zod introspection). `SchemaInput<T>` →\n`StandardSchemaWithJSON.InferInput<T>` is rewritten mechanically by the codemod. The\ninternal `standardSchemaToJsonSchema` / `validateStandardSchema` helpers are **not** part\nof the public surface — do not import them.\n\nv1's second compat module, `server/zod-json-schema-compat.js` (`toJsonSchemaCompat`), is\nalso removed — and the codemod does **not** rewrite its import (expect `TS2307`). If you\nbuild `Tool` / `Prompt` advertisements yourself, use your schema library's native\nconversion: zod 4's `z.toJSONSchema(schema, { io: 'input', target: 'draft-2020-12' })`\nproduces the dialect v2 advertises.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/types-schemas.md":"### Types & schemas\n\n#### Zod `*Schema` constants moved to `@modelcontextprotocol/core`\n\nThe Zod schemas (`CallToolResultSchema`, `ListToolsResultSchema`, …) that v1 exported\nfrom `types.js` now live in a separate **`@modelcontextprotocol/core`** package. Neither\n`@modelcontextprotocol/client` nor `@modelcontextprotocol/server` re-exports them — both\npackages stay Zod-free in their public surface.\n\nThe v1→v2 change is just an import-path swap — `.parse()` / `.safeParse()` keep working\nunchanged:\n\n```typescript\n// v1\nimport { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n\n// v2 — same Zod schema, new package\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n```\n\n`@modelcontextprotocol/core` is the canonical home for the spec's Zod schema constants\n(and the OAuth/OpenID metadata schemas). It is runtime-neutral (its only dependency is\n`zod`) and arrives transitively as the shared runtime schema graph of `client` /\n`server` — add it to your own `dependencies` only when you import the raw schemas\ndirectly.\n\nIf you would rather keep your project Zod-free, the **`isSpecType` / `specTypeSchemas`**\nalternatives are exported from `@modelcontextprotocol/client` and `…/server`:\n\n```typescript\nimport { isSpecType, specTypeSchemas } from '@modelcontextprotocol/client';\nif (isSpecType.CallToolResult(value)) { ... }\nconst blocks = mixed.filter(isSpecType.ContentBlock);\nconst result = specTypeSchemas.CallToolResult['~standard'].validate(value);\n```\n\n`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of\nevery named type in the MCP spec — so you get autocomplete and a compile error on typos.\n`specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` (`validate()` is synchronous).\n`validate()` returns `{ value }` or `{ issues }` and never throws — unlike `.parse()` on\nthe real schema; code that caught a `ZodError` should inspect `result.issues` (or keep\n`.parse()` on the schema imported from `@modelcontextprotocol/core`).\nThe pre-existing `isCallToolResult(value)` guard still works.\n\n**`specTypeSchemas.X` is `StandardSchemaV1`, not `ZodType`.** Zod-specific composition\n— `.extend()`, `.pick()`, `.omit()`, `.merge()`, `.shape`, `.passthrough()`,\n`.parseAsync()` — does **not** compile on a `specTypeSchemas` entry; reach for the real\nZod schema from `@modelcontextprotocol/core` when you need to derive a tolerant variant\nof a spec schema (e.g.\n`ListToolsResultSchema.extend({ tools: ToolSchema.omit({ outputSchema: true }).array() })`).\nThe Zod-specific `AnySchema` / `SchemaOutput` types from `…/zod-compat.js` are removed —\nreplace with `StandardSchemaV1` / `StandardSchemaV1.InferOutput<T>` (the codemod's\nremoval message says the same).\n\n**Composing two core schemas.** Zod composition needs a shared zod: deriving from a\nsingle core schema (as above) and combining core schemas with your own `z` typecheck\nwhen your `zod` resolves to the **same copy** `@modelcontextprotocol/core` uses (a\n`zod ^4.2.0` range that dedupes). When it cannot — a zod@3-pinned project nests core's\nown zod@4 — v1 idioms that combined two spec schemas with your `z` no longer compile:\ncore does not export its zod instance, and a foreign zod's `z.union(…)` / `.or(…)`\nrejects core's schema types. For accept-either result parsing, skip composition:\nrequest with the `ResultSchema` passthrough (the same one the\n[gateway note](protocol-ctx.md#request-ctxmcpreqsend-and-calltool-no-longer-require-a-schema-for-spec-methods)\nuses) and discriminate with sequential `safeParse`:\n\n```typescript\n// v1 — one composed schema\nconst result = await client.request(req, z.union([CompatibilityCallToolResultSchema, CreateTaskResultSchema]));\n\n// v2 — passthrough request, then sequential discrimination\nimport { CompatibilityCallToolResultSchema, CreateTaskResultSchema, ResultSchema } from '@modelcontextprotocol/core';\nconst raw = await client.request(req, ResultSchema);\nconst asTask = CreateTaskResultSchema.safeParse(raw);\nconst result = asTask.success ? asTask.data : CompatibilityCallToolResultSchema.parse(raw);\n```\n\nOrder the candidates from most to least specific, and `.parse()` the last one so a\nresult that matches no candidate still fails loudly.\n\nThe role-aggregate unions (`ClientRequest`, `ServerResult`, `ServerRequest`,\n`ClientResult`, `ClientNotification`, `ServerNotification`) and the typed-method maps\n(`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap`) no longer\ninclude task vocabulary; the deprecated `Task*` types remain importable on their own.\n(One published-alpha qualification, like the `-32002` note in [Errors](errors.md#errors): the\n`2.0.0-alpha.3` and earlier typings predate this — the typed maps there still carry the\n`tasks/*` entries, and `ResultTypeMap['tools/call']` still unions `CreateTaskResult`, so\na `client.request({ method: 'tools/call', … })` result does not assign to\n`Promise<CallToolResult>`. If pinned to those alphas, narrow with the\n`isCallToolResult` guard — the recommended discrimination tool anyway, per the next\nparagraph; `2.0.0-alpha.4` and later are unaffected.)\n\n**Discriminating result shapes: use guards, not the `in` operator.** The v2\nzod-inferred result types are passthrough objects — every union member carries an index\nsignature — so v1-idiomatic property discrimination such as\n`if ('content' in result) { … } else { result.toolResult }` no longer narrows: the `in`\ncheck is satisfiable by every member, and the else branch can collapse to `never`\n(surfacing as `TS2339` on the property you then read). Use the exported guards instead:\n`isCallToolResult(result)`, or `isSpecType.GetPromptResult(result)` and friends for any\nother spec type ([above](#zod-schema-constants-moved-to-modelcontextprotocolcore)). An\nadjacent trap when keeping a union for later narrowing: a `const` **annotation** is\ncontrol-flow-narrowed straight back to the initializer's type — after\n`const r: A | B = await fn()`, `r` has `fn`'s return type, not the union — so when you\nneed the wider union (e.g. a `CompatibilityCallToolResult` branch), apply an\n`as A | B` assertion instead of an annotation.\n\n#### Removed type aliases\n\n| Removed                                                         | Replacement                                                     |\n| --------------------------------------------------------------- | --------------------------------------------------------------- |\n| `JSONRPCError`                                                  | `JSONRPCErrorResponse`                                          |\n| `JSONRPCErrorSchema`                                            | `JSONRPCErrorResponseSchema`                                    |\n| `isJSONRPCError`                                                | `isJSONRPCErrorResponse`                                        |\n| `isJSONRPCResponse` (deprecated in v1)                          | `isJSONRPCResultResponse` ²                                     |\n| `JSONRPCResponseSchema` (result-only in v1)                     | `JSONRPCResultResponseSchema` ²                                 |\n| `JSONRPCResponse` (result-only in v1)                           | `JSONRPCResultResponse` ²                                       |\n| `ResourceReference` / `ResourceReferenceSchema`                 | `ResourceTemplateReference` / `ResourceTemplateReferenceSchema` |\n| `IsomorphicHeaders`                                             | Web Standard `Headers`                                          |\n| `RequestHandlerExtra`                                           | `ServerContext` / `ClientContext` / `BaseContext`               |\n| `ResourceTemplate` (the spec wire **type** from `sdk/types.js`) | `ResourceTemplateType` ³                                        |\n\n² v2 introduces **new** `isJSONRPCResponse` / `JSONRPCResponse` / `JSONRPCResponseSchema`\nwith corrected semantics — they match **both** result and error responses (the schema is\n`z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])`). v1's symbols only\nmatched results. To preserve v1 behavior, rename to `isJSONRPCResultResponse` /\n`JSONRPCResultResponse` / `JSONRPCResultResponseSchema` (the codemod does this).\n\n³ The `ResourceTemplate` URI-template helper **class** (from `sdk/server/mcp.js`) is\n**unchanged** — keep `new ResourceTemplate(...)` as-is. Only the like-named spec wire\ntype from `types.js` was renamed to `ResourceTemplateType` to resolve the v1 collision;\nthe codemod scopes the rename to imports from `sdk/types.js` only.\n\nAll other symbols from `@modelcontextprotocol/sdk/types.js` retain their original\nnames — import the TypeScript types, error classes, enums, and type guards from\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the Zod\n`*Schema` constants from `@modelcontextprotocol/core`.\n\nOne type-level narrowing to note: client/server capability `experimental` payloads are\nnow typed as JSON-compatible objects (nested JSON values) rather than arbitrary\nobjects. A payload typed `Record<string, unknown>` no longer assigns (`TS2322`) — give\nthe source a JSON-compatible type or cast at the boundary.\n\nThe `Protocol` base class and `mergeCapabilities` moved: import them from the\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server` package root instead\nof `shared/protocol.js`. Most code should not use `Protocol` directly — `Client` and\n`Server` are the supported surfaces, and for observing unmatched inbound requests\nprefer `client.fallbackRequestHandler` / `server.fallbackRequestHandler`. The codemod\nrewrites `Protocol` and `mergeCapabilities` imports to the package root, like the\nmodule's other symbols. One caveat: the client and server packages each bundle their\nown compiled copy of the class, so the two roots' `Protocol` exports are distinct\nclasses — import it from one package consistently within a process.\n\n#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)\n\nThe default validator dispatches on the schema's declared `$schema`: absent or 2020-12\nvalidates as **JSON Schema 2020-12** — on Node via `Ajv2020` instead of v1's draft-07\n`Ajv` (the Cloudflare Workers default was already 2020-12) — a declared 2019-09\n`$schema` validates with 2019-09 semantics (`Ajv2019`), and a declared draft-07 or\ndraft-06 `$schema` validates with draft-07 semantics. Schemas declaring any other\n`$schema` are rejected with `Error(\"…unsupported dialect…\")`. Two known draft-07 engine\ndifferences: the Node engine (classic Ajv, same as v1's default) evaluates keywords\nadjacent to `$ref`, stricter than draft-07's ignore-siblings rule, while the\nbrowser/Workers engine ignores them per spec; and the browser/Workers engine does not\nresolve a `$ref` inside a `dependencies` entry whose key collides with a JSON Schema\nkeyword (`type`, `default`, `format`, …) — validation throws `Unresolved $ref` when\nthat dependency triggers (surfaced as the SDK's typed validation error), while the\nNode engine handles the same schema correctly.\n\n`CallToolResult.structuredContent` is widened from `{ [k: string]: unknown }` to\n`unknown` (SEP-2106 lifts the `type:\"object\"` root restriction). The presence check is\n`!== undefined`, not falsy (`null` / `0` / `false` / `\"\"` are legal values now). External\n`$ref` is not dereferenced (unchanged from v1; Ajv throws `MissingRefError` at compile,\nsurfaced per-tool on `callTool`).\n\n| v1 pattern                                                                                                   | Mechanical fix                                                                                                                                                                                         |\n| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `result.structuredContent.<key>` / `result.structuredContent?.<k>`                                           | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }`                                                                              |\n| `if (!result.structuredContent)`                                                                             | `if (result.structuredContent === undefined)`                                                                                                                                                          |\n| draft-07 idioms **without** a declared `$schema` (a declared draft-07/06 `$schema` dispatches automatically) | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |\n| undeclared draft-07 idioms via `fromJsonSchema(schema)`                                                      | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas                                 |\n| `outputSchema` / `inputSchema` with absolute-URI `$ref`                                                      | inline under `$defs` and reference with `#/$defs/Name`                                                                                                                                                 |\n\nA tool may now register an `outputSchema` whose root is `type:\"array\"`, `type:\"string\"`,\netc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward\nevery era a non-object `structuredContent` with no `text` block of its own gets a\n`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md › Per-era wire codecs](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#per-era-wire-codecs) for how the codec applies these per era.\n\n**Your advertised tool schemas change shape on the wire.** The same `registerTool`\ncalls produce `tools/list` entries whose generated `inputSchema` differs from v1:\nJSON Schema 2020-12 idioms (zod 4 conversion), different `additionalProperties`\nhandling (no `additionalProperties: false` by default; passthrough objects emit\n`\"additionalProperties\": {}` instead of `true`), and no `execution.taskSupport` member.\nGolden tests, transcript pins, and strict client-side validators of your advertised\ntool list need re-baselining — the new shapes are spec-conformant.\n","skills/mcp-typescript-sdk-upgrade-to-v2/references/unchanged-apis.md":"## Unchanged APIs\n\nThe following are unchanged between v1 and v2 apart from the import path — except\nwhere an entry notes its own signature change:\n\n- `Client` constructor and `connect`, `close`, and the typed verbs (`listTools`,\n  `listPrompts`, `listResources`, `readResource`, …) — note `callTool()` and `request()`\n  signatures changed (schema parameter dropped for spec methods).\n- `McpServer` constructor, `server.connect(transport)`, `server.close()`, and the\n  `McpServer.server` accessor — still the supported way to call the low-level\n  `Server`'s push verbs (`createMessage` / `listRoots` / `sendLoggingMessage` — ⚠\n  `@deprecated`, see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) outside a\n  handler context.\n- The server Streamable HTTP transports' **constructor options** (`sessionIdGenerator`,\n  `onsessioninitialized`, `onsessionclosed`, `enableJsonResponse`, `eventStore`,\n  `retryInterval`) and the `handleRequest` surface — only the class name and import\n  moved: `StreamableHTTPServerTransport` is now `NodeStreamableHTTPServerTransport`\n  from `@modelcontextprotocol/node`, a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`,\n  which exposes the same options ([decision rule](imports-transports.md#imports--transports)). The\n  transport-level `closeSSEStream(requestId)` / `closeStandaloneSSEStream()` methods\n  keep their v1 names too — only the handler-context accessors moved to `ctx.http`\n  ([remap table](protocol-ctx.md#low-level-protocol--handler-context-ctx)).\n- `UriTemplate` (v1: `@modelcontextprotocol/sdk/shared/uriTemplate.js`) — `expand` /\n  `match` semantics carry over; import it from `@modelcontextprotocol/server` or\n  `@modelcontextprotocol/client` (top-level export; the codemod rewrites the path).\n- `StreamableHTTPClientTransport`, `SSEClientTransport` constructors and options —\n  including resumability: the per-request `resumptionToken` / `onresumptiontoken`\n  request options carry over from v1 unchanged\n  ([Resume a dropped stream](https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.md#resume-a-dropped-stream)).\n- `StdioClientTransport` and `StdioServerTransport` — **import path moved** to the\n  `./stdio` subpath and gained an optional `maxBufferSize` ([Imports & transports](imports-transports.md#imports--transports)).\n- The **`Transport` interface contract** — `start` / `send` / `close`, `onmessage` /\n  `onclose` / `onerror`, optional `sessionId` and `setProtocolVersion`,\n  `TransportSendOptions`, `MessageExtraInfo`. Hand-rolled v1 transports (recording\n  wrappers, test doubles, decorators) compile and run against v2 with only the import\n  path updated. v2 adds **optional** members only — `hasPerRequestStream` and\n  `setSupportedProtocolVersions` on the interface, `requestSignal` / `headers` /\n  `onRequestStreamEnd` on `TransportSendOptions` — which matter only for 2026-era\n  per-request-stream cancellation and `Mcp-Param-*` header attachment\n  ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)).\n- All TypeScript **type** definitions from `types.ts` (except the aliases listed under\n  [Removed type aliases](types-schemas.md#removed-type-aliases) and the `experimental` capability\n  payload narrowing — see [Types & schemas](types-schemas.md#types--schemas)).\n- Tool, prompt, and resource callback return types.\n\n> The `Server` (low-level) constructor and **most** of its methods are unchanged, but\n> `setRequestHandler` / `setNotificationHandler` and `request()` signatures changed\n> ([Low-level protocol](protocol-ctx.md#low-level-protocol--handler-context-ctx)). In particular,\n> `Server.createElicitationCompletionNotifier()` is unchanged — including its\n> construction-time client-capability check — for 2025-era URL-mode elicitation\n> ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The Zod `*Schema`\n> constants are **not** part of the unchanged surface — they moved to\n> `@modelcontextprotocol/core` ([Types & schemas](types-schemas.md#types--schemas)).\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThe principles this SDK is built and reviewed by. They are not a checklist: apply them with judgement.\n\n1. **Minimalism** — The SDK should do less, not more: prefer changes that add no API or option unless justified.\n2. **Spec is the anchor** — The spec decides; where it leaves a choice open, check what the other official SDKs do before making your own choice.\n3. **Strict by default** — Accept what the spec allows, and add no leniency for peers that break it.\n4. **Backwards compatible** — New features and changes arrive in backwards compatible ways; when a bug, a spec violation or an unsafe default forces a break, the changeset says what breaks.\n5. **Small changes** — Guard scope hard: a smaller change that lands beats a bigger one that doesn't, and small, independent pull requests can land in any order.\n6. **The words are true** — Changesets, docs and comments say what the code does.\n7. **Test what users see** — Prefer tests that cover the end-to-end usage a user would actually see, where sensible; `test/e2e/requirements.ts` lists those behaviours.\n8. **Brief comments** — Keep inline comments to one line and JSDoc to the public API; the reasoning goes in the commit message or PR description.\n\nStyle belongs to Prettier, ESLint and the compiler.\n","isInternal":false,"tokens":297,"sizeBytes":1294},{"name":"SKILL.md","path":"examples/guides/servers/skills/acme/billing/refunds/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/guides/servers/skills/acme/billing/refunds/SKILL.md","title":"refunds","category":"anthropic-skill","format":"markdown","content":"---\nname: refunds\ndescription: Process customer refunds.\n---\n\n# Refunds\n","frontmatter":{"name":"refunds","description":"Process customer refunds."},"isInternal":false,"tokens":16,"sizeBytes":72},{"name":"REVIEW.md","path":"examples/guides/servers/skills/git-workflow/references/REVIEW.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/guides/servers/skills/git-workflow/references/REVIEW.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Review checklist\n\n- Tests pass.\n","isInternal":false,"tokens":8,"sizeBytes":34},{"name":"SKILL.md","path":"examples/guides/servers/skills/git-workflow/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/guides/servers/skills/git-workflow/SKILL.md","title":"git-workflow","category":"anthropic-skill","format":"markdown","content":"---\nname: git-workflow\ndescription: Branching and review conventions.\n---\n\n# Git workflow\n\nBefore opening a pull request, go through [the review checklist](references/REVIEW.md).\n","frontmatter":{"name":"git-workflow","description":"Branching and review conventions."},"isInternal":false,"tokens":39,"sizeBytes":179},{"name":"package.json","path":"examples/skills/package.json","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/package.json","title":"@mcp-examples/skills","category":"anthropic-skill","format":"json","content":"{\n    \"name\": \"@mcp-examples/skills\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"scripts\": {\n        \"server\": \"tsx server.ts\",\n        \"client\": \"tsx client.ts\"\n    },\n    \"dependencies\": {\n        \"@hono/node-server\": \"catalog:runtimeServerOnly\",\n        \"@mcp-examples/shared\": \"workspace:*\",\n        \"@modelcontextprotocol/client\": \"workspace:*\",\n        \"@modelcontextprotocol/hono\": \"workspace:*\",\n        \"@modelcontextprotocol/server\": \"workspace:*\"\n    },\n    \"devDependencies\": {\n        \"tsx\": \"catalog:devTools\"\n    },\n    \"example\": {\n        \"era\": \"dual\",\n        \"//\": \"Both legs list, load and verify skills, follow a reference and browse a skill folder.\"\n    }\n}\n","frontmatter":{"name":"@mcp-examples/skills","private":true,"type":"module","scripts":{"server":"tsx server.ts","client":"tsx client.ts"},"dependencies":{"@hono/node-server":"catalog:runtimeServerOnly","@mcp-examples/shared":"workspace:*","@modelcontextprotocol/client":"workspace:*","@modelcontextprotocol/hono":"workspace:*","@modelcontextprotocol/server":"workspace:*"},"devDependencies":{"tsx":"catalog:devTools"},"example":{"era":"dual","//":"Both legs list, load and verify skills, follow a reference and browse a skill folder."}},"isInternal":false,"tokens":174,"sizeBytes":686},{"name":"README.md","path":"examples/skills/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/README.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# skills\n\nThe server serves the `skills/` folder as MCP skills (SEP-2640) with one\n`loadSkills` call, under the `examples` prefix. The client lists the catalog,\nloads `git-workflow` (its `SKILL.md`, verified for size, digest and\nfrontmatter), follows the reference to `references/REVIEW.md`, browses the\n`refunds` folder, and catches a verification failure.\n\n```bash\npnpm --filter @mcp-examples/skills client            # modern leg over stdio\npnpm --filter @mcp-examples/skills client -- --legacy\npnpm --filter @mcp-examples/skills run server -- --http --port 3000\npnpm --filter @mcp-examples/skills client -- --http http://127.0.0.1:3000/mcp\n```\n\nSee [Serving skills](../../docs/servers/skills.md) and [Reading skills](../../docs/clients/skills.md).\n","isInternal":false,"tokens":213,"sizeBytes":752},{"name":"SKILL.md","path":"examples/skills/skills/acme/billing/refunds/escalation/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/skills/acme/billing/refunds/escalation/SKILL.md","title":"escalation","category":"anthropic-skill","format":"markdown","content":"---\nname: escalation\ndescription: Escalate a disputed refund to the billing lead.\n---\n\n# Escalation\n\nCollect the order number and the customer's reason, then page the billing lead.\n","frontmatter":{"name":"escalation","description":"Escalate a disputed refund to the billing lead."},"isInternal":false,"tokens":40,"sizeBytes":181},{"name":"SKILL.md","path":"examples/skills/skills/acme/billing/refunds/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/skills/acme/billing/refunds/SKILL.md","title":"refunds","category":"anthropic-skill","format":"markdown","content":"---\nname: refunds\ndescription: Process a customer refund end to end.\nmetadata:\n    owner: billing\n---\n\n# Refunds\n\nCheck the order, issue the refund, then send [the refund email](templates/refund-email.md).\nFor disputed orders, use the escalation skill.\n","frontmatter":{"name":"refunds","description":"Process a customer refund end to end.","metadata":{"owner":"billing"}},"isInternal":false,"tokens":57,"sizeBytes":253},{"name":"refund-email.md","path":"examples/skills/skills/acme/billing/refunds/templates/refund-email.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/skills/acme/billing/refunds/templates/refund-email.md","title":"Templates Skill","category":"anthropic-skill","format":"markdown","content":"Hello {{customer}},\n\nYour refund of {{amount}} is on its way.\n","isInternal":false,"tokens":16,"sizeBytes":62},{"name":"REVIEW.md","path":"examples/skills/skills/git-workflow/references/REVIEW.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/skills/git-workflow/references/REVIEW.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Review checklist\n\n- Tests pass locally.\n- A changeset describes the change.\n","isInternal":false,"tokens":17,"sizeBytes":78},{"name":"SKILL.md","path":"examples/skills/skills/git-workflow/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/skills/skills/git-workflow/SKILL.md","title":"git-workflow","category":"anthropic-skill","format":"markdown","content":"---\nname: git-workflow\ndescription: Branching, commit and review conventions for this team.\n---\n\n# Git workflow\n\n1. Branch from `main` as `feature/<topic>`.\n2. Keep commits small; write messages in the imperative.\n3. Before opening a pull request, go through [the review checklist](references/REVIEW.md).\n","frontmatter":{"name":"git-workflow","description":"Branching, commit and review conventions for this team."},"isInternal":false,"tokens":72,"sizeBytes":305},{"name":"client.md","path":"skills/mcp-typescript-sdk-skills/references/client.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-skills/references/client.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Use skills from MCP servers\n\n<!-- #region guide -->\n\n`SkillsClient` lists a server's skills and reads their files, checking each file against what the server published.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\nAgainst a server with two skills, it prints each skill's name and description:\n\n```text\nrefunds: Process customer refunds.\ngit-workflow: Branching and review conventions.\n```\n\nListing reads no skill file: each **entry** carries the skill's frontmatter and its file list with digests, enough to decide which skill to load.\n\n## Handle refused entries\n\nCreate the client with `skipInvalid: true` to keep the valid skills and log the refused entries.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#list_invalid\"\nconst tolerant = new SkillsClient(client, { skipInvalid: true });\nconst { invalid } = await tolerant.list();\nfor (const refused of invalid) console.error(`skills: refused entry #${refused.index} ${refused.uri ?? ''}: ${refused.reason}`);\n```\n\nBy default `list()` throws an `SdkError` naming every entry that fails validation, and every copy of a repeated URI. With `skipInvalid: true` it returns the valid skills and puts those entries in `invalid`, each with `index`, `uri` when it has one, and `reason`. Against this server it is empty, so the loop prints nothing.\n\n## Load a skill\n\nRead the entry's `SKILL.md`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nThe file passes every check, and its frontmatter matches the entry:\n\n```text\ntrue {\n  name: 'git-workflow',\n  description: 'Branching and review conventions.'\n}\n```\n\n`read(entry)` checks the size and SHA-256 digest against the entry, then compares the frontmatter field by field. `manifest.body` holds the instructions to give the model.\n\nThe comparison parses the frontmatter with YAML 1.2 core-schema typing, so `yes` is the string `\"yes\"`, not `true`. If a server builds its entries with a parser that types values differently, pass a matching YAML parser as `parseFrontmatter` to `new SkillsClient`; it receives the YAML between the `---` lines.\n\n## Read the files a skill references\n\nPass the path `SKILL.md` uses.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_reference\"\nconst review = await skills.read(git, 'references/REVIEW.md');\nconsole.log(review.text);\n```\n\n`review.text` holds the verified file:\n\n```text\n# Review checklist\n\n- Tests pass.\n\n```\n\nA relative path resolves against the skill's folder, as a link in `SKILL.md` does, and `read` ignores a fragment. It matches the path to the entry's file list however either side percent-encodes the name, so `references/Q&A.md` works. `bytes` always holds the file's bytes; `text` holds the file's text when the server sent it as text, and always for the skill's own `SKILL.md`.\n\n## Handle verification failures\n\nCatch `SkillVerificationError`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillVerificationError\"\nimport { SkillVerificationError } from '@modelcontextprotocol/client/ext/skills';\n\ntry {\n    await skills.read(git, 'references/MISSING.md');\n} catch (error) {\n    if (!(error instanceof SkillVerificationError)) throw error;\n    console.log(error.reason, error.uri);\n}\n```\n\nThe entry does not list `references/MISSING.md`, so `read` refuses it before sending any request:\n\n```text\nunlisted skill://git-workflow/references/MISSING.md\n```\n\n`reason` is `unlisted`, `size`, `digest` or `frontmatter`; a path that does not parse is `unlisted` too. When a size, digest or frontmatter check fails on a copy the client served from its response cache, `read` fetches the file once more with `cacheMode: 'refresh'` and throws only if the fresh copy fails as well; a copy that came from the server fails at once. On any reason, do not use the file; for `SKILL.md`, do not load the skill.\n\n## Browse a skill folder\n\nList a folder's direct children when the server declares `directoryRead`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#readDirectory\"\nif (skills.directoryRead) {\n    const { resources: children } = await skills.readDirectory('skill://acme/billing/refunds');\n    console.log(children.map(child => `${child.name} ${child.mimeType}`).join('\\n'));\n}\n```\n\nThe `refunds` folder holds its `SKILL.md` and a `templates` folder:\n\n```text\nSKILL.md text/markdown\ntemplates inode/directory\n```\n\n## Get a skill by URI\n\nFetch one entry, listed or not.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#get\"\nconst { skill: refunds } = await skills.get('skill://acme/billing/refunds/SKILL.md');\nconsole.log(refunds.frontmatter.name);\n```\n\nIt prints the skill's name from the entry's frontmatter:\n\n```text\nrefunds\n```\n\nOn 2026-07-28, `list()`, `get()` and `readDirectory()` also return the server's caching hints, `ttlMs` and `cacheScope`, and throw an `SdkError` when a response leaves either out.\n\n::: info Dynamic skills\nAn entry whose `resources` is `\"dynamic\"` has no file list. `read` then skips the size and digest checks and returns `verified: false`, but still compares the `SKILL.md` frontmatter.\n:::\n\n::: warning Host responsibilities\nAccept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before you load a skill from a server, and record which server each skill came from. Read a skill's files when you load it, never while listing.\n:::\n\n## Recap\n\n- `new SkillsClient(client)` wraps a connected client; `list()` returns entries without reading files.\n- `list()` throws on an invalid entry; `skipInvalid: true` keeps the valid skills and returns the refused ones in `invalid`.\n- `read(entry)` verifies `SKILL.md` (size, digest, frontmatter) and returns its frontmatter and body.\n- `read(entry, path)` resolves a path against the skill's folder and verifies that file.\n- A failed check throws `SkillVerificationError` with a `reason`.\n\n<!-- #endregion guide -->\n","isInternal":false,"tokens":1512,"sizeBytes":6402},{"name":"server.md","path":"skills/mcp-typescript-sdk-skills/references/server.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-skills/references/server.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Serve skills from an MCP server\n\n<!-- #region guide -->\n\n`loadSkills` reads a folder of skills once; `attach` serves it on a server.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\nA **skill** is a folder with a `SKILL.md` and the files it references (SEP-2640). Clients list skills with `skills/list` and read each file with `resources/read`, checking it against the digest your server published.\n\n## Lay out the skills folder\n\nEach folder that contains a `SKILL.md` is one skill; other folders only group skills in the URI.\n\n```text\nskills/\n├── git-workflow/\n│   ├── SKILL.md\n│   └── references/REVIEW.md\n└── acme/billing/refunds/\n    └── SKILL.md\n```\n\n`SKILL.md` starts with YAML frontmatter holding `name` (the same as the skill's folder name) and `description`. Print the URIs the catalog publishes:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#entries\"\nconsole.log(skills.entries.map(skill => skill.uri).join('\\n'));\n```\n\nIt prints one `SKILL.md` URI per skill, sorted:\n\n```text\nskill://acme/billing/refunds/SKILL.md\nskill://git-workflow/SKILL.md\n```\n\n## Serve one catalog from every server\n\nAttach the same catalog to each server your handler creates.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\nThe catalog keeps metadata only (paths, digests, sizes, frontmatter); each read goes to the folder, and the SDK serves the bytes only if they still match the published digest. It sends a file as `text` whenever its bytes are valid UTF-8 (a BOM included), whatever its MIME type, and as a base64 `blob` otherwise.\n\n## Pick up changes with `reload()`\n\nRescan the folder and swap the new catalog in at once.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload\"\nconst changes = await skills.reload();\nconsole.log(changes);\n```\n\nWith nothing changed on disk, `reload()` reports no changes:\n\n```text\n{ added: [], removed: [], changed: [], listChanged: false }\n```\n\nUntil you call `reload()`, the server answers a read of a file edited on disk with an error instead of unverified content. If the rescan fails, `reload()` rejects and the old catalog keeps serving.\n\n`listChanged` is `true` when a skill appears, disappears, or changes its name or description. A server that stays connected (on stdio, or in a stateful 2025-11-25 HTTP session) then sends its clients `notifications/resources/list_changed` itself. A server that `createMcpHandler` builds lives for one request, so publish the change on the handler instead:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nEvery open `subscriptions/listen` stream that opted in to resource list changes receives the notification.\n\n## Serve skills defined in code\n\nPass the files to `memorySkillFiles` where there is no file system, such as Workers or tests.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#memorySkillFiles\"\nimport { memorySkillFiles, SkillsExtension } from '@modelcontextprotocol/server/ext/skills';\n\nconst RELEASE_NOTES_MD = `---\nname: release-notes\ndescription: Write release notes from merged pull requests.\n---\n# Release notes\n`;\n\nconst inline = await SkillsExtension.load(memorySkillFiles({ 'release-notes/SKILL.md': RELEASE_NOTES_MD }));\n```\n\n`memorySkillFiles` re-reads the object you pass whenever the catalog rescans (at load and on `reload()`), so `reload()` sees files you add or change later.\n\n## Serve skills from your own storage\n\nImplement `SkillFiles`, two functions over relative paths, to load from any store.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillFiles_custom\"\nimport type { SkillFiles } from '@modelcontextprotocol/server/ext/skills';\n\n// Two operations over '/'-separated paths, '' being the root: here, a key-value store of objects.\nfunction bucketSkillFiles(bucket: ReadonlyMap<string, Uint8Array>): SkillFiles {\n    return {\n        async list(dir) {\n            const prefix = dir === '' ? '' : `${dir}/`;\n            const children = new Map<string, 'file' | 'directory'>();\n            for (const key of bucket.keys()) {\n                if (!key.startsWith(prefix)) continue;\n                const [name, ...rest] = key.slice(prefix.length).split('/');\n                if (name !== undefined) children.set(name, rest.length > 0 ? 'directory' : 'file');\n            }\n            return [...children].map(([name, kind]) => ({ name, kind }));\n        },\n        async read(key) {\n            const bytes = bucket.get(key);\n            if (bytes === undefined) throw new Error(`no object ${key}`);\n            return bytes;\n        }\n    };\n}\n\nconst fromBucket = await SkillsExtension.load(\n    bucketSkillFiles(new Map([['release-notes/SKILL.md', new TextEncoder().encode(RELEASE_NOTES_MD)]]))\n);\n```\n\nThe catalog logic (discovery, digests, rule checks, paging, verified reads) is the same for every adapter. Report a symlink or other special file as `kind: 'unsupported'`; the catalog skips the skill that contains one.\n\n## Generate skills with a `SkillSource`\n\nBuild entries with `skillEntry` and serve them from a `SkillSource` when the catalog depends on the request.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillSource\"\nimport type { SkillSource } from '@modelcontextprotocol/server/ext/skills';\nimport { skillEntry } from '@modelcontextprotocol/server/ext/skills';\n\nconst ON_CALL_MD = `---\nname: on-call\ndescription: Handle a page while on call.\n---\n# On call\n`;\nconst onCall = await skillEntry('on-call', { 'SKILL.md': ON_CALL_MD });\n\nconst source: SkillSource = {\n    list: async () => ({ skills: [onCall] }),\n    get: async uri => (uri === onCall.uri ? onCall : undefined),\n    read: async uri => (uri === onCall.uri ? { text: ON_CALL_MD, mimeType: 'text/markdown' } : undefined)\n};\nconst generated = new SkillsExtension({ source });\n```\n\nEvery source method receives the request context (`ctx`), so you can list different skills per caller. Add `readDirectory` to serve folder listings, and `listResources` to list skills in `resources/list`.\n\n## Handle invalid skills\n\nRoute warnings where you want them; by default they go to stderr.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#onWarning\"\nconst checked = await loadSkills('./skills', {\n    onWarning: ({ path, message }) => console.error(`skills: ${path}: ${message}`)\n});\n```\n\n`loadSkills` skips, with a warning, any skill that breaks the rules, such as a `name` that differs from its folder or a symlinked file; the other skills still serve. Skills over 512 files or 16 MiB still publish, with a warning; `limits` changes those thresholds. The same options take `prefix` (`'acme'` serves every skill under `skill://acme/…`) and `pageSize` (entries per `skills/list` and directory page, default 100).\n\nSet `strict: true` to make `loadSkills` throw a `SkillCatalogError` listing every problem instead. Under `strict: true`, a symlink or other special file anywhere in the folder fails the load, even one outside every skill; hidden entries (names starting with `.`) are the exception, since the scan skips them entirely.\n\n::: warning\nDo not register your own resources under `skill://`. A resource you register at a skill file's URI takes precedence over that file, and clients then fail to verify it.\n:::\n\n::: info\nSkills work on both protocol versions; the `cacheHint` option applies on 2026-07-28 only — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).\n:::\n\n## Recap\n\n- `loadSkills('./skills')` builds a catalog once; `attach(server)` serves it on any number of servers.\n- Each folder with a `SKILL.md` is a skill; other folders group skills in the URI.\n- `reload()` swaps in a rescanned catalog all at once; behind `createMcpHandler`, call `handler.notify.resourcesChanged()` when it reports `listChanged`.\n- `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource` serve skills without a folder.\n- `loadSkills` skips invalid skills with a warning; `strict: true` makes it throw instead.\n\n<!-- #endregion guide -->\n","isInternal":false,"tokens":2044,"sizeBytes":8686},{"name":"SKILL.md","path":"skills/mcp-typescript-sdk-skills/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-skills/SKILL.md","title":"mcp-typescript-sdk-skills","category":"anthropic-skill","format":"markdown","content":"---\nname: mcp-typescript-sdk-skills\ndescription: 'Serves and consumes Skills over MCP (SEP-2640, the `io.modelcontextprotocol/skills` extension) with v2 of the MCP TypeScript SDK: publishes a folder of `SKILL.md` skills from an MCP server with `loadSkills` and `SkillsExtension`, and lists, verifies and loads them in a client or host with `SkillsClient`. Use whenever a TypeScript project on `@modelcontextprotocol/server` or `@modelcontextprotocol/client` 2.x wants to expose agent skills from its MCP server, serve `skill://` resources or `skills/list`, or load skills from the MCP servers it connects to, even if the user does not say \"SEP-2640\" or \"extension\". Not for projects still on v1 (`@modelcontextprotocol/sdk`: upgrade them with the `mcp-typescript-sdk-upgrade-to-v2` skill first), for MCP SDKs in other languages (Python, Go, C#), or for writing the content of a `SKILL.md`.'\n---\n\n# Skills over MCP with the TypeScript SDK\n\nSkills over MCP lets an MCP server publish agent skills (folders with a `SKILL.md`) and lets clients list, verify and load them. In v2 of the TypeScript SDK it ships as the `ext/skills` subpaths of `@modelcontextprotocol/server` and `@modelcontextprotocol/client`.\n\nThe goal is working, verified code in the user's project. Use only the APIs shown here and in the references, and skip explaining the protocol unless the user asks.\n\n## Which side the project is on\n\nRead `package.json` and the source before writing anything:\n\n- **Server:** depends on `@modelcontextprotocol/server` and creates an `McpServer`, directly or in a factory passed to `serveStdio` or `createMcpHandler`. It publishes skills: follow [Serve skills](#serve-skills).\n- **Client or host:** depends on `@modelcontextprotocol/client` and creates a `Client`. It loads skills from the servers it connects to: follow [Use skills](#use-skills).\n- **Both** (a host that also runs a server, a gateway): follow both, each against its own code. A `Client` that exists only in the project's tests does not make it a host: that is where [Verify](#verify) runs.\n- **v1** (`@modelcontextprotocol/sdk`): Skills over MCP exists only in v2. Upgrade first with the `mcp-typescript-sdk-upgrade-to-v2` skill, then come back.\n\n## Check that the installed SDK has skills\n\nSkills over MCP ships in the `./ext/skills` export of the v2 packages, and an import of a subpath the installed package lacks fails only at build or run time. Check first, from the package that depends on the SDK (use `client` in place of `server` for a client):\n\n```bash\ngrep -c '\"./ext/skills\"' node_modules/@modelcontextprotocol/server/package.json\n```\n\nIf it prints `0`, check the latest release:\n\n```bash\nnpm view @modelcontextprotocol/server@latest exports --json | grep -c '\"./ext/skills\"'\n```\n\n- **The latest release has it:** upgrade (`server` and `client` to the same version, if the project uses both) and continue.\n- **It does not:** stop here. Tell the user that Skills over MCP is not in a released version of the SDK yet, and that this works once a release exports `@modelcontextprotocol/server/ext/skills`. Do not write `skills/list` handlers or `skill://` resources by hand as a workaround. Clients verify every file against the digests and frontmatter the extension publishes, and the extension also declares the capability, pages the catalog and handles both protocol versions, so a hand-written copy is easy to get wrong in ways those clients refuse, and drifts from the spec.\n\n## Serve skills\n\n**Pick where the skills come from.** Each source gives you an extension that you attach as shown below; only the first three have `reload()`:\n\n- **A folder on disk** (Node): `loadSkills`, which the steps below walk through.\n- **Skills defined in code** (Workers and other runtimes without a file system, tests): `memorySkillFiles`. See [Serve skills defined in code](references/server.md#serve-skills-defined-in-code).\n- **Your own storage** (an object store, a database, a git repository): a `SkillFiles` adapter. See [Serve skills from your own storage](references/server.md#serve-skills-from-your-own-storage).\n- **Skills that differ per caller or are generated per request:** a `SkillSource`. See [Generate skills with a `SkillSource`](references/server.md#generate-skills-with-a-skillsource).\n\n**Lay out the folder.** Each folder holding a `SKILL.md` is one skill. Its frontmatter needs a `name` equal to the folder's name and a `description`. Other folders only group skills in the URI.\n\n**Load the folder once and attach it before the server connects.** `attach` throws on a connected server, and each server takes one skills extension. A server connected by hand keeps its `server.connect(transport)` call, after `attach`.\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\n**Resolve the folder from the module, not the working directory.** A relative path like `'./skills'` resolves against `process.cwd()`, and MCP hosts may start stdio servers from another directory (such as `/`), where the load throws and takes the whole server down with it. Build the path from the module's location instead: `fileURLToPath(new URL('../skills', import.meta.url))` from `src/index.ts`, or `path.join(__dirname, '../skills')` in CommonJS.\n\n**Where a factory builds the server, attach inside it**, so every server it builds serves the same catalog. That covers `serveStdio(createServer)` on stdio as well as `createMcpHandler` over HTTP:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\n**Pick up edits with `reload()`** if the user expects changes to show without a restart. Call it from whatever tells the project the folder changed (a file watcher, an admin route, a deploy hook). A server that stays connected (stdio) notifies its clients itself. Behind `createMcpHandler`, publish the change on the handler, where `changes` is what `await skills.reload()` returned:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nThen tell the user about any load warnings, and [verify](#verify).\n\nRead [references/server.md](references/server.md) for `onWarning`, `strict`, `limits`, `prefix` and `pageSize`.\n\n## Use skills\n\n**Wrap the connected client and list.** Listing reads no skill file: each entry carries the skill's frontmatter and file list, enough to show what is on offer.\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\n`skills.supported` is `false` for a server that does not declare the extension; a host that connects to several servers checks it per server before listing. `list()` throws an `SdkError` if any entry is invalid, so a host listing servers it does not control creates the client with `new SkillsClient(client, { skipInvalid: true })` and logs the refused entries it returns in `invalid`.\n\n**Load a skill only after the user (or the host's policy) approves it.** Read its `SKILL.md`, which checks size, digest and frontmatter, and give `manifest.body` to the model:\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nRead the files `SKILL.md` refers to with `skills.read(entry, path)` when the model needs them. A failed check throws `SkillVerificationError`: do not use that file, and if it is the `SKILL.md`, do not load the skill.\n\nRead [references/client.md](references/client.md) for `skipInvalid`, verification reasons, `readDirectory`, `get`, dynamic skills and caching hints.\n\n## Mistakes to avoid\n\nServing:\n\n- **Writing the protocol by hand.** Serve skills only through `SkillsExtension` (`loadSkills`, `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource`). Never `setRequestHandler('skills/list', …)`, and never register your own resources under `skill://`: a resource at a skill file's URI takes precedence over the file, and clients then fail to verify it.\n- **Missing the skipped skills.** `loadSkills` skips a skill whose `name` differs from its folder, or that contains a symlink, and only warns (on stderr by default) while the others serve. Read the warnings after loading and tell the user which skills were skipped. Pass `strict: true` (in CI, say) to make the load throw instead.\n- **Dot-named entries.** Files and folders whose names start with `.` are skipped with no warning at all, and `strict: true` does not catch them. A skill in a `.name` folder, or a dotfile its `SKILL.md` refers to, is simply missing: rename it.\n- **A catalog outside the factory's servers.** `serveStdio` and `createMcpHandler` build their servers from a factory, and each server `createMcpHandler` builds lives for one request. Attach inside the factory, and behind `createMcpHandler` call `handler.notify.resourcesChanged()` when `reload()` reports `listChanged`, or connected clients never hear of the change.\n- **Expecting edits to show up on their own.** The catalog keeps the digests from load time, so a file edited on disk afterwards is refused with an error until `reload()` runs.\n- **`loadSkills` without a file system.** It reads the disk (`ext/skills/node`). On Workers and similar runtimes, use `memorySkillFiles` or a `SkillFiles` adapter from `@modelcontextprotocol/server/ext/skills`.\n\nLoading:\n\n- **Raw `resources/read`.** Never fetch skill files with `client.readResource()`: it skips the size, digest and frontmatter checks. Use `skills.read()`.\n- **Reading while listing.** Read a skill's files when it is loaded, never while listing.\n- **Skipping the host's duties.** Accept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before loading a skill from a server, and record which server each skill came from, so the instructions given to the model can be traced to their source.\n\nBoth:\n\n- **Invented APIs.** Take skills APIs only from `@modelcontextprotocol/server/ext/skills`, `@modelcontextprotocol/server/ext/skills/node` and `@modelcontextprotocol/client/ext/skills`, using names shown here or in the references. If you need something else, read the package's type declarations instead of guessing.\n\n## Verify\n\n**Server:** list every skill and read every file it lists through a real client. In a script or test, connect a `Client` to the server over an in-memory pair and read each file with `SkillsClient`. `read` throws when a file fails a check, and the loop throws when the server lists no skills:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#verify_inMemory\"\nimport { Client, InMemoryTransport } from '@modelcontextprotocol/client';\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();\nawait server.connect(serverTransport);\nconst client = new Client({ name: 'skills-check', version: '1.0.0' });\nawait client.connect(clientTransport);\n\nconst skillsClient = new SkillsClient(client);\nconst { skills: listed } = await skillsClient.list();\nif (listed.length === 0) throw new Error('the server listed no skills');\nfor (const entry of listed) {\n    const files = entry.resources === 'dynamic' ? [entry.uri] : entry.resources.map(file => file.uri);\n    for (const uri of files) await skillsClient.read(entry, uri);\n}\nconsole.log(`${listed.length} skill(s) listed and verified`);\n```\n\nAn entry whose `resources` is `\"dynamic\"` (only a `SkillSource` serves one) has no file list, so the loop reads only its `SKILL.md`: `read` checks the frontmatter but has no size or digest to check, and returns `verified: false`.\n\nFor a stdio server, the same loop works over `StdioClientTransport` (from `@modelcontextprotocol/client/stdio`) against the server's real start command; set its `cwd` option to another directory (such as `/`), as hosts do, so a path that depends on the working directory fails here and not in the user's host. Compare the number of skills listed with the number of skill folders, and account for each one missing: a warning, or a dot-name.\n\n**Client:** run the host against a server that serves at least one skill (the server path above takes a few lines, or `memorySkillFiles` in a test). Check that it lists the skill, asks before loading it, and reads its `SKILL.md` with `verified: true`.\n\n## References\n\n| Reference file                                          | Read it when                                  |\n| ------------------------------------------------------- | --------------------------------------------- |\n| [Serve skills from an MCP server](references/server.md) | The project serves skills from an MCP server  |\n| [Use skills from MCP servers](references/client.md)     | The project loads skills from its MCP servers |\n","frontmatter":{"name":"mcp-typescript-sdk-skills","description":"Serves and consumes Skills over MCP (SEP-2640, the `io.modelcontextprotocol/skills` extension) with v2 of the MCP TypeScript SDK: publishes a folder of `SKILL.md` skills from an MCP server with `loadSkills` and `SkillsExtension`, and lists, verifies and loads them in a client or host with `SkillsClient`. Use whenever a TypeScript project on `@modelcontextprotocol/server` or `@modelcontextprotocol/client` 2.x wants to expose agent skills from its MCP server, serve `skill://` resources or `skills/list`, or load skills from the MCP servers it connects to, even if the user does not say \"SEP-2640\" or \"extension\". Not for projects still on v1 (`@modelcontextprotocol/sdk`: upgrade them with the `mcp-typescript-sdk-upgrade-to-v2` skill first), for MCP SDKs in other languages (Python, Go, C#), or for writing the content of a `SKILL.md`."},"isInternal":false,"tokens":3157,"sizeBytes":13637},{"name":"auth.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/auth.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/auth.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Auth\n\n#### OAuth error consolidation\n\nThe individual OAuth error classes are replaced with a single `OAuthError` + `OAuthErrorCode`.\nThe `OAUTH_ERRORS` constant is removed. The codemod does not rewrite `instanceof` checks\non these classes — switch on `error.code` instead.\n\n| v1 class                       | v2 equivalent                                           |\n| ------------------------------ | ------------------------------------------------------- |\n| `InvalidRequestError`          | `OAuthError` + `OAuthErrorCode.InvalidRequest`          |\n| `InvalidClientError`           | `OAuthError` + `OAuthErrorCode.InvalidClient`           |\n| `InvalidGrantError`            | `OAuthError` + `OAuthErrorCode.InvalidGrant`            |\n| `UnauthorizedClientError`      | `OAuthError` + `OAuthErrorCode.UnauthorizedClient`      |\n| `UnsupportedGrantTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedGrantType`    |\n| `InvalidScopeError`            | `OAuthError` + `OAuthErrorCode.InvalidScope`            |\n| `AccessDeniedError`            | `OAuthError` + `OAuthErrorCode.AccessDenied`            |\n| `ServerError`                  | `OAuthError` + `OAuthErrorCode.ServerError`             |\n| `TemporarilyUnavailableError`  | `OAuthError` + `OAuthErrorCode.TemporarilyUnavailable`  |\n| `UnsupportedResponseTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedResponseType` |\n| `UnsupportedTokenTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedTokenType`    |\n| `InvalidTokenError`            | `OAuthError` + `OAuthErrorCode.InvalidToken`            |\n| `MethodNotAllowedError`        | `OAuthError` + `OAuthErrorCode.MethodNotAllowed`        |\n| `TooManyRequestsError`         | `OAuthError` + `OAuthErrorCode.TooManyRequests`         |\n| `InvalidClientMetadataError`   | `OAuthError` + `OAuthErrorCode.InvalidClientMetadata`   |\n| `InsufficientScopeError`       | `OAuthError` + `OAuthErrorCode.InsufficientScope` ¹     |\n| `InvalidTargetError`           | `OAuthError` + `OAuthErrorCode.InvalidTarget`           |\n| `CustomOAuthError`             | `new OAuthError(customCode, message)`                   |\n\n¹ Unrelated to the new transport-layer `InsufficientScopeError` (SEP-2350) exported from\n`@modelcontextprotocol/client`, which carries an RFC 6750 challenge from the resource\nserver and extends `OAuthClientFlowError`, **not** `OAuthError`. Do not rewrite that one.\n\n```typescript\n// v1\nif (error instanceof InvalidClientError) { ... }\n// v2\nimport { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }\n```\n\n⚠ **Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from\n`@modelcontextprotocol/express`, or from `@modelcontextprotocol/server` on\nweb-standard hosts) classifies the error your\n`OAuthTokenVerifier.verifyAccessToken()` throws: a v2\n`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +\n`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from\n`server-legacy`) or a generic `Error` falls through as unexpected — **invalid tokens\nbecome HTTP `500`**. When you re-point `requireBearerAuth` at\n`@modelcontextprotocol/express`, migrate the error classes your verifier throws in the\nsame change.\n\nA frozen copy of the v1 classes (and `mcpAuthRouter`) is available from\n`@modelcontextprotocol/server-legacy/auth` during migration.\n\n#### `AuthProvider` — non-OAuth bearer auth and the widened `authProvider` option\n\nThe transport `authProvider` option is widened to `AuthProvider | OAuthClientProvider`.\n**`AuthProvider`** is a new minimal interface — `{ token(): Promise<string | undefined>;\nonUnauthorized?(ctx): Promise<void> }` — for static-token / non-OAuth bearer auth.\nTransports call `token()` before every request and `onUnauthorized()` on 401 (then retry\nonce). Existing `OAuthClientProvider` implementations need no changes — transports adapt\nthem internally via the new `adaptOAuthProvider()` export. Also exported:\n`isOAuthClientProvider()` (type guard) and `handleOAuthUnauthorized()` (the standard\nOAuth `onUnauthorized` behavior, for composing your own adapter).\n\n#### OAuth client flow — behavioral changes\n\n- **Resolved scope passed to DCR (SEP-835).** `auth()` now computes the resolved scope\n  once (WWW-Authenticate → PRM `scopes_supported` → `clientMetadata.scope`) and passes\n  it to **both** the DCR POST body and the authorization request. `registerClient()`\n  gained an optional `scope` parameter that overrides `clientMetadata.scope` in the\n  registration body.\n- **OAuth error on HTTP 200.** `exchangeAuthorization()` / `refreshAuthorization()` now\n  throw `OAuthError` when the AS returns HTTP 200 with a JSON `{error: ...}` body (e.g.\n  GitHub). v1 surfaced this as a Zod parse failure on the tokens schema.\n- **Metadata discovery falls through on 502.** `discoverAuthorizationServerMetadata()`\n  treats `502 Bad Gateway` like 4xx — fall through to the next candidate URL instead of\n  throwing (fixes path-aware discovery behind reverse proxies). Other 5xx still throw.\n- **Scoped credential invalidation on `invalid_client` / `unauthorized_client`.** The\n  `auth()` retry for these errors now issues two scoped calls —\n  `invalidateCredentials('client')` then `invalidateCredentials('tokens')` — instead of\n  v1's single `invalidateCredentials('all')`, deliberately preserving the stored\n  discovery state so the callback-leg check on retry does not mask the original error.\n  A provider whose `invalidateCredentials()` implementation special-cases the `'all'`\n  scope must handle the split calls.\n- **Token persistence failures after a refresh now propagate.** v1 wrapped both\n  `refreshAuthorization()` and the `saveTokens()` that persists its result in one\n  `try`/`catch`, so a provider's persistence error was discarded alongside AS-side refresh\n  failures and `auth()` fell through to a fresh authorization request, returning\n  `'REDIRECT'`. Only the refresh call is guarded now — persisting runs after it and rejects\n  to the caller. Against an AS that rotates refresh tokens this was destructive rather than\n  merely quiet: the exchange has already succeeded, so the old refresh token is invalidated\n  server-side the moment the new one is issued, and dropping the new token set leaves\n  nothing usable on either side. A provider whose `saveTokens()` can throw (transient\n  storage errors, file-lock contention) must handle the rejection from `auth()` — and from\n  the transport 401-retry paths built on it — where v1 silently re-authorized. Refresh\n  failures themselves keep their control flow: a `ServerError` or an unknown error still\n  falls through to a new authorization request, and `invalid_grant` / `invalid_client` /\n  `unauthorized_client` are still recovered by discarding stored credentials and retrying.\n  Both routes now emit a `console.warn` naming the cause, so an unexplained re-auth prompt\n  can be traced to the failure that triggered it.\n\n#### OAuth client flow errors (new)\n\nThe OAuth client flow now throws dedicated classes from `@modelcontextprotocol/client`\n(all extend `OAuthClientFlowError`, **not** `OAuthError` — `auth()`'s `OAuthError` retry\npath will not catch them):\n\n| Throw site                                                                                                                | v2 class                                                                              |\n| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| `registerClient()` rejected by AS (⚠ `@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) | `RegistrationRejectedError` (`status`, `body`, `submittedMetadata`)                   |\n| Token-exchange / refresh / `fetchToken` / Cross-App grant on a non-`https:` token endpoint                                | `InsecureTokenEndpointError` (`tokenEndpoint`)                                        |\n| RFC 9207 `iss` mismatch / RFC 8414 §3.3 issuer-echo mismatch                                                              | `IssuerMismatchError` (`kind`, `expected`, `received`)                                |\n| Transport 403 `insufficient_scope` with `onInsufficientScope: 'throw'`, or default mode without an `OAuthClientProvider`  | `InsufficientScopeError` (`requiredScope`, `resourceMetadataUrl`, `errorDescription`) |\n| `auth()` callback leg: discovery resolves a different AS than the recorded redirect target                                | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n| `auth()` on a provider that cannot re-register, or `fetchToken()`: client information stamped for a different AS          | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n\n#### Connect-time OAuth retry (`UnauthorizedError`)\n\n`UnauthorizedError` survives in v2 (exported from `@modelcontextprotocol/client` —\nits only appearance in the error table above is the removed `SSEClientTransport.send()`\n401 path), and the v1 connect-time pattern carries over: catch it from `connect()`,\ncomplete the browser flow, call `transport.finishAuth(…)`, reconnect.\n\n```typescript\ntry {\n    await client.connect(transport);\n} catch (error) {\n    if (!(error instanceof UnauthorizedError)) throw error;\n    // provider.redirectToAuthorization() has been called; complete the flow,\n    // then reconnect on a FRESH transport (a started transport cannot be restarted).\n    await transport.finishAuth(new URL(callbackUrl).searchParams);\n    await client.connect(new StreamableHTTPClientTransport(url, { authProvider: provider }));\n}\n```\n\nThis direct `instanceof` check works in every version-negotiation mode: under the\nprobing modes (`versionNegotiation: { mode: 'auto' }`, with or without a pin) the\nconnect-time `UnauthorizedError` also propagates unchanged from `connect()`. Older\nreleases wrapped it as `SdkError(SdkErrorCode.EraNegotiationFailed)` with the error at\n`error.data.cause` — that unwrap is no longer needed. See the\n[client OAuth guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/oauth.md).\n\n#### `auth()` options are now `AuthOptions`\n\nThe inline options object on `auth()` is now the named `AuthOptions` type. New fields:\n`iss?: string` (the form-urldecoded `iss` from the authorization callback — pass it\nalongside `authorizationCode` for RFC 9207 validation), `skipIssuerMetadataValidation?:\nboolean` (security-weakening opt-out of the RFC 8414 §3.3 issuer-echo check), and\n`forceReauthorization?: boolean` (skip the refresh-token branch — set by the transport's\nstep-up path; hosts driving step-up themselves set it under the same condition).\n\n#### Authorization-server mix-up defense (RFC 9207 / RFC 8414 §3.3) — action required\n\n`transport.finishAuth()` and `auth()` now validate `iss` from the authorization callback\nagainst the issuer recorded from validated AS metadata. A mismatched `iss` throws\n`IssuerMismatchError` before the code is exchanged regardless of advertised support; a\n**missing** `iss` throws only when the AS advertised\n`authorization_response_iss_parameter_supported: true`.\n\nPass the callback URL's `URLSearchParams` so the SDK can read `iss` alongside `code`.\nThe SDK does **not** validate `state`; compare it yourself before calling `finishAuth`:\n\n```typescript\nconst params = new URL(callbackUrl).searchParams;\nif (params.get('state') !== expectedState) throw new Error('state mismatch');\nawait transport.finishAuth(params); // SDK reads `code` + `iss`\n```\n\n`transport.finishAuth(code, iss)` remains supported. Do **not** display `error` /\n`error_description` / `error_uri` from a callback that failed `iss` validation — those\nvalues are attacker-controlled in a mix-up attack.\n\n`discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not\nexactly match the URL it was fetched for (RFC 8414 §3.3). Set\n`skipIssuerMetadataValidation: true` only as a temporary workaround for a known-misconfigured AS.\n\n(`@modelcontextprotocol/server-legacy` AS implementers: `mcpAuthRouter()` now advertises\n`authorization_response_iss_parameter_supported: true` by default and the bundled\nauthorize handler appends `iss` to every redirect issued via `res.redirect(...)` on the\nsupplied `res`. If you emit `Location` another way, append `params.issuer` as `iss`\nyourself; if your callback is issued by an upstream AS you proxy to, set\n`authorizationResponseIssParameterSupported = false` so the metadata does not over-claim.)\n\n#### Dynamic Client Registration defaults (SEP-837, SEP-2207)\n\n`auth()` now resolves `provider.clientMetadata` once via `resolveClientMetadata()` and\napplies defaults to the DCR body: `grant_types` defaults to\n`['authorization_code', 'refresh_token']`; `application_type` is derived from\n`redirect_uris` (loopback / custom URI scheme → `'native'`, else `'web'`). A field you\nset explicitly is never overwritten. The `grant_types` default applies to the DCR body\nonly — it does **not** drive the `offline_access` / `prompt=consent` augmentation on the\nauthorize request; statically-registered and CIMD clients that want that augmentation\nmust set `clientMetadata.grant_types` explicitly. Non-interactive providers (no\n`redirectUrl`) get no `grant_types` default. Direct `registerClient()` callers (⚠\n`@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) wanting the same\ndefaults pass `resolveClientMetadata(provider)` as `clientMetadata`. DCR\nrejection now throws `RegistrationRejectedError` (carrying `status`, `body`,\n`submittedMetadata`).\n\n#### Token endpoint must use TLS (SEP-2207)\n\n`exchangeAuthorization()`, `refreshAuthorization()`, `fetchToken()`, and the Cross-App\nAccess helpers throw `InsecureTokenEndpointError` when the token endpoint is not\n`https:` (loopback `localhost` / `*.localhost` / `127.0.0.1` / `::1` exempt). `auth()` surfaces this on\nevery path including refresh — switch any plain-`http:` AS on a non-loopback host to\nTLS; there is no opt-out. Storage confidentiality of `refresh_token` remains your\n`saveTokens()` implementation's responsibility.\n\n#### Scope step-up on `403 insufficient_scope` (SEP-2350)\n\n`StreamableHTTPClientTransport` accepts `onInsufficientScope: 'reauthorize' | 'throw'`\n(default `'reauthorize'`). On `'reauthorize'` the transport re-authorizes with the\n**union** of the previously-requested and challenged scope (`computeScopeUnion`); when\nthat union strictly exceeds the current token's granted scope (`isStrictScopeSuperset`),\nthe SDK bypasses the refresh-token branch and forces a fresh authorization request. On\n`'throw'` the transport raises `InsufficientScopeError` and does not re-authorize — set\nthis for `client_credentials` / m2m clients where re-authorization can't widen scope, or\nto gate the consent prompt behind UX. Step-up retries are hard-capped per send\n(`maxStepUpRetries`, default 1). With a non-OAuth [`AuthProvider`](#authprovider--non-oauth-bearer-auth-and-the-widened-authprovider-option),\na `403 insufficient_scope` now throws `InsufficientScopeError` instead of the previous\n`SdkHttpError(ClientHttpNotImplemented)`. The GET listen-stream open path applies the\nsame handling as the POST send path.\n\n#### Credentials bound to the issuing authorization server (SEP-2352)\n\n`auth()` stamps an `issuer` field onto every value it passes to `saveTokens()` /\n`saveClientInformation()` and threads `{ issuer }` as the `ctx` argument to those\nmethods plus `tokens()` / `clientInformation()`. On read, a stored value whose `issuer`\nnames a different AS is treated as `undefined` and the flow re-registers / re-authorizes\n(or throws `AuthorizationServerMismatchError` when the provider has no `saveClientInformation()`).\n**Round-trip the stored object verbatim and you're protected** — single-slot storage\nworks. Dropping the stamp is easy to miss: a `saveTokens()` implementation that\nrebuilds the object field-by-field and drops `issuer` leaves the value unstamped —\nreads still succeed and refresh keeps working, the per-AS issuer check simply does not\napply to that credential, and every read logs an `[mcp-sdk]` warning (`auth()`\nre-stamps on first use where the provider can persist it). If you see that warning\nrepeating after upgrading, check this first. To hold credentials for several authorization servers at once, key your storage\non `ctx.issuer` (treat **`ctx === undefined` as \"return the most-recently-saved token\nset\"** — the transport's per-request `Authorization: Bearer` read calls `tokens()` with\nno `ctx`). `OAuthTokensSchema` / `OAuthClientInformationSchema` keep the optional `issuer`, so\nreading storage back through them is fine; the `StoredOAuthTokens` / `StoredOAuthClientInformation`\naliases name the stored shape.\n\n`OAuthClientProvider.saveAuthorizationServerUrl()` / `authorizationServerUrl()` are\n`@deprecated` (still written for back-compat, never read by the SDK). The bundled\n`ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivateKeyJwtProvider`, and\n`CrossAppAccessProvider` gain `expectedIssuer?: string` (omitting it is deprecated) and no\nlonger define `saveClientInformation()`. Implement `discoveryState()` / `saveDiscoveryState()` so the\ncallback leg can verify it is exchanging the code at the same AS the redirect targeted;\nwithout it the SDK `console.warn`s once per callback (`discoveryState` must persist with\nthe same durability as `codeVerifier`). Both methods are optional on\n`OAuthClientProvider` and may be sync or async; `OAuthDiscoveryState` (exported from\n`@modelcontextprotocol/client`) extends `OAuthServerInfo` with the optional\n`resourceMetadataUrl` the protected-resource metadata was found at:\n\n```typescript\nimport type { OAuthDiscoveryState } from '@modelcontextprotocol/client';\n\n// On OAuthClientProvider:\nsaveDiscoveryState?(state: OAuthDiscoveryState): void | Promise<void>;\ndiscoveryState?(): OAuthDiscoveryState | undefined | Promise<OAuthDiscoveryState | undefined>;\n```\n\n#### Conformance obligations for `OAuthClientProvider` implementers\n\nThe SDK enforces every authorization MUST that lands in SDK code. The following live in\n**your** implementation and the SDK structurally cannot enforce them:\n\n- **Round-trip the `issuer` stamp** on persisted credentials (SEP-2352). Persist the\n  value verbatim from `saveTokens` / `saveClientInformation` and return it verbatim.\n- **Pass `expectedIssuer`** when constructing static-credential providers (SEP-2352).\n- **Keep refresh tokens confidential in storage** (SEP-2207) — OS keychain or\n  encrypted-at-rest store, never `localStorage` / plain files / logs.\n- **Extract `iss` from the callback URL** and pass it to `finishAuth` (SEP-2468); when\n  `IssuerMismatchError` is thrown, do not render the callback's `error*` values.\n- **Set `application_type` correctly** when overriding the heuristic (SEP-837).\n- **Track cross-request step-up failures yourself** (SEP-2350) — `maxStepUpRetries` is\n  per request; per-session backoff is host state.\n- **Persist discovery state**: implement `discoveryState()` / `saveDiscoveryState()` so\n  the authorization-server metadata your tokens were issued against survives restarts.\n- **Choose the insufficient-scope behavior**: keep the default\n  `onInsufficientScope: 'reauthorize'`, or handle `InsufficientScopeError` yourself.\n- **Resource-server operators: do not advertise `offline_access`** in `WWW-Authenticate`\n  `scope` or PRM `scopes_supported` (SEP-2207).\n","isInternal":false,"tokens":4410,"sizeBytes":19655},{"name":"behavioral-changes.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Behavioral changes\n\nThese are runtime-behavior changes that may affect tests and assertions; no source\nrewrite required unless noted.\n\n#### Error-shape changes (every era)\n\n- **Unchanged, for re-baselining relief:** timeout rejections still carry\n  `data.timeout` / `data.maxTotalTimeout` exactly as v1 `McpError` did — v1 assertions\n  on those survive verbatim. The cancelled-on-timeout signal is unchanged on legacy-era\n  connections and on stdio/in-memory at any era; on 2026-era Streamable HTTP the cancel\n  signal is the per-request stream close instead of a `notifications/cancelled` POST\n  (see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The one exemption is the\n  `initialize` handshake: an aborted or timed-out `connect()` still rejects locally, but\n  no `notifications/cancelled` goes on the wire — the spec forbids cancelling\n  `initialize`, and v1 sent one anyway. v1 tests asserting that notification need\n  re-baselining.\n- **Also unchanged: SSE reconnection exhaustion.** `StreamableHTTPClientTransport`'s\n  standalone GET-stream reconnection behavior and its exhaustion signal carry over from\n  v1: when retries run out, the transport emits `onerror` with a plain `Error` whose\n  message is `Maximum reconnection attempts (N) exceeded.` — there is no typed error\n  class for this condition, so monitors that match the message text keep working.\n- **Also unchanged: elicitation response validation.** `elicitInput`'s local validation\n  of elicitation responses against `requestedSchema`, the resulting `-32602` error\n  message wording (`Elicitation response content does not match requested schema: …`),\n  and the `McpServer` / `Client` `jsonSchemaValidator` option carry over from v1 —\n  tests pinning the local-validation message and custom validator wiring need no\n  re-baselining.\n- **Unknown / disabled tool calls now reject** with `ProtocolError(-32602 InvalidParams)`\n  instead of resolving `CallToolResult{isError: true}`. v1 callers that checked\n  `result.isError` for an unknown tool will get an unhandled rejection — catch the\n  rejected promise instead.\n- **The `MCP error <code>: ` message prefix is gone.** v1 prefixed relayed JSON-RPC\n  error messages (`MCP error -32602: …`); v2's `ProtocolError.message` carries the\n  peer's message verbatim. Tests and log scrapers that matched the prefix or the numeric\n  code in rendered text should match `error.code` instead.\n- **In-flight request handlers are aborted on transport close** — `ctx.mcpReq.signal`\n  fires (v1 let them run to completion). `InMemoryTransport.close()` no longer\n  double-fires `onclose` on the initiating side.\n- **`Protocol.request()` with an already-aborted signal** rejects with\n  `SdkError(SdkErrorCode.RequestTimeout, reason)` instead of throwing the raw\n  `signal.reason`, matching the in-flight-abort path.\n- **OAuth discovery (`discoverOAuthProtectedResourceMetadata` / `discoverOAuthMetadata`,\n  transitively `auth()`) throws on fetch `TypeError`** (DNS failure, `ECONNREFUSED`,\n  invalid URL) in Node and Cloudflare Workers instead of swallowing it as a CORS miss\n  → `undefined`. The CORS-swallow remains browser-only.\n\n#### Client connection & dispatch\n\n- **`connect()` skips the `initialize` handshake when the transport already exposes a\n  `sessionId`** — it assumes it is reconnecting to an existing session (unchanged from\n  v1.x, where the same guard has existed since 1.10.0; recorded here because the\n  far-away symptom keeps surprising migrators). A custom or test transport that sets `sessionId` at construction\n  silently skips initialization: `getServerCapabilities()` stays `undefined` and the\n  list verbs return empty results. Expose `sessionId` only after the first request has\n  been sent.\n- **The typed verbs dispatch after async pre-work.** `Protocol.request()` itself still\n  hands the frame to the transport before its first `await` (v1-compatible). The typed\n  verbs on top of it — `callTool()` and the cacheable list verbs — perform async work\n  first (header-mirroring scan, response-cache freshness, output-validator resolution),\n  so an abort fired in the same tick can land before the frame is ever sent: the call\n  rejects with `SdkError(RequestTimeout, reason)` and **no `notifications/cancelled` is\n  emitted** (nothing was in flight). v1 sent the frame synchronously from these verbs.\n  Once the frame is on the wire, aborting still sends `notifications/cancelled` before\n  rejecting.\n- **Protocol-version pinning is a first-class option.**\n  `ProtocolOptions.supportedProtocolVersions` pins the legacy `initialize` handshake:\n  the **first** pre-2026 entry in the list is offered (list order is preference order),\n  a counter-offer is accepted only if it is one of the list's pre-2026 entries, and a\n  list with no pre-2026 entry makes the handshake throw. Under\n  `versionNegotiation: 'auto'` the modern probe candidates are the list's modern\n  entries when it has any (otherwise the SDK's default modern set); a `{ pin }` is\n  honored as given and is not checked against the list (see\n  [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#client-side-versionnegotiation)).\n  v1 had no public equivalent (`SUPPORTED_PROTOCOL_VERSIONS` was a fixed constant) —\n  replace any workaround that patched the offered version with this option.\n- **Also unchanged: HTTP 405 tolerances.** A `405` answering the standalone GET stream\n  open is benign (the client proceeds without the stream), and a `405` answering the\n  session DELETE resolves `terminateSession()` normally — stateless-topology servers\n  that decline both verbs keep working without changes, as in v1.\n\n#### stdio transport\n\n- A configurable `maxBufferSize` (default **10 MB**) caps the stdio read buffer. A\n  single message that would push the buffer past the limit emits `onerror` and\n  **closes the connection** (v1 buffered unbounded). Configure via\n  `new StdioClientTransport({ ..., maxBufferSize })` /\n  `new StdioServerTransport(stdin, stdout, { maxBufferSize })`.\n- `ReadBuffer.readMessage()` now **silently skips non-JSON stdout lines** instead of\n  throwing `SyntaxError` → `onerror`. Hot-reload tools (tsx, nodemon) that write debug\n  output to stdout no longer break the transport. Lines that parse as JSON but fail\n  JSON-RPC schema validation still throw.\n- `StdioClientTransport` always sets `windowsHide: true` when spawning the server\n  process on Windows (previously Electron-only). Prevents stray console windows in\n  non-Electron Windows hosts.\n- Outbound write failures — e.g. the host closing the stdout pipe while a send is\n  pending — now reject the pending `send()` and close the transport through\n  `onerror`/`onclose` instead of surfacing an unhandled stream error; lifecycle\n  tests that pinned a crash-class exit observe a clean shutdown instead.\n\n#### Client list methods\n\n- `listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` return\n  **empty results** when the server didn't advertise the corresponding capability,\n  instead of sending the request. Set `enforceStrictCapabilities: true` in `ClientOptions`\n  to restore the v1 throw.\n- Called **without a `cursor`**, the same methods now **auto-aggregate every page** and\n  return an aggregate with no `nextCursor`. Passing `{ cursor }` still fetches one page. Manual\n  pagination loops keep working (the first iteration returns everything); replace them\n  with the bare no-arg call. The walk is capped at `ClientOptions.listMaxPages` (default\n  64); overrun throws `SdkError(ListPaginationExceeded)`. There is no way to fetch only\n  the **first** page through the typed verbs — for page-level observation\n  (pagination tooling, per-page stats) drop to\n  `client.request({ method: 'tools/list', params })`, which never aggregates.\n- Output-schema validator compilation is now **lazy** — validators compile on the first\n  `callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()`.\n  In v1, `listTools()` threw on an uncompilable `outputSchema`; now `listTools()`\n  succeeds and the compile failure surfaces when `callTool()` is invoked on the affected\n  tool, as `ProtocolError(InvalidParams, \"Tool 'X' has an invalid outputSchema: …\")`,\n  before the request is sent. Validation is never silently skipped.\n- On a 2026-07-28 connection the cacheable verbs honour the server-stamped `ttlMs` /\n  `cacheScope` (SEP-2549) and may return a still-fresh cached entry without a round\n  trip. Per-call override: `{ cacheMode: 'refresh' | 'bypass' }`. New `ClientOptions`:\n  `cachePartition`, `defaultCacheTtlMs`. `ResponseCacheStore` gained `delete(key)`;\n  `InMemoryResponseCacheStore` is now bounded (`{ maxEntries }`, default 512).\n  `CacheEntry.value` (and the `set()` entry value) is the JSON-serialized result\n  document (`string`, not `unknown`): persist and return it verbatim, `JSON.parse`\n  it to inspect.\n\n#### Server (Streamable HTTP transport)\n\n- Resumability behavior (SSE priming events, `closeSSE` / `closeStandaloneSSE`\n  callbacks) is only enabled for protocol versions in the transport's supported-versions\n  list that are `>= 2025-11-25`. Unknown future version strings in an `initialize`\n  request body no longer enable it.\n- Session-ID mismatch still responds `404` with JSON-RPC `-32001` (`Session not found`),\n  unchanged from v1. This `-32001` is an SDK convention, not spec-assigned; client code\n  should key off the HTTP `404` status, not `-32001`.\n\n#### Server (deprecated accessors and app-factory Origin validation)\n\n- `Server.getClientCapabilities()`, `getClientVersion()`, `getNegotiatedProtocolVersion()`\n  are `@deprecated` but functional. On 2026-07-28 requests, prefer `ctx.mcpReq.envelope`.\n- `createMcpExpressApp()` / `createMcpHonoApp()` / `createMcpFastifyApp()` with a\n  localhost-class `host` now also validate the `Origin` header by default. Browser-served\n  clients on a non-localhost origin need `allowedOrigins: [...]` (replaces the default\n  localhost allowlist; validation cannot be disabled for localhost binds). Requests\n  without an `Origin` header are unaffected; a present `Origin` that cannot be parsed\n  — including the opaque **`Origin: null`** sent by sandboxed iframes, `file://` pages,\n  and cross-origin redirects — is **rejected with 403** and cannot be allowlisted via\n  `allowedOrigins`. Framework-agnostic helpers\n  (`validateOriginHeader`, `localhostAllowedOrigins`, `originValidationResponse`) are in\n  `@modelcontextprotocol/server`; `@modelcontextprotocol/node` ships\n  `hostHeaderValidation` / `originValidation` request guards for plain `node:http`.\n\n#### Server (McpServer / Streamable HTTP behavior)\n\n- **Eager capability-handler install.** `McpServer` now installs list/read/call handlers\n  for every primitive capability declared in `ServerOptions.capabilities`, even with\n  zero registrations. `new McpServer(info, { capabilities: { tools: {} } })` with no\n  registered tools answers `tools/list` with `{ tools: [] }` instead of `-32601 Method\nnot found`. Low-level `Server` users remain responsible for registering handlers for\n  declared capabilities — with one exception: declaring the `logging` capability (in\n  the constructor's capabilities or via pre-connect `registerCapabilities()`) installs\n  the `logging/setLevel` handler on the low-level `Server` too, so `logging/setLevel`\n  requests that answered `-32601` in v1 now resolve. Eager install also rewrites the **advertised** capability\n  objects: a declared `tools: {}` / `resources: {}` / `prompts: {}` is advertised with\n  `listChanged: true` at construction, so capability pins and initialize-result golden\n  tests need re-baselining. To advertise without the default, set\n  `listChanged: false` explicitly; capabilities declared on the low-level `Server` are\n  advertised verbatim.\n- **`WebStandardStreamableHTTPServerTransport` store-first `eventStore` semantics.**\n  Request-related events emitted after `closeSSE()` — and the final response when no\n  per-request stream is connected — are now persisted to the configured `eventStore` for\n  replay (v1 dropped them / threw `\"No connection established\"`). Without an\n  `eventStore`, the same condition surfaces via `onerror` and the request id is retired.\n  `NodeStreamableHTTPServerTransport` is a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport`, so this — like every behavioral note on\n  the web-standard transport — applies to the Node transport too.\n- **`registerResource` reserves the `cacheHint` config key.** It is validated\n  (`RangeError` on invalid values) and stripped from the resource's list metadata; v1\n  passed it through verbatim as ordinary metadata. Untyped callers that previously\n  smuggled a `cacheHint` key through resource metadata should rename it.\n\n#### `ctx.mcpReq.log()` is request-related on every era\n\n`ctx.mcpReq.log()` now emits its `notifications/message` request-related (it rides the\nin-flight exchange like progress) on every era. On a 2025-era sessionful Streamable HTTP\ntransport this moves handler-emitted logs from the standalone GET stream onto the\nper-request POST response stream — a spec-conformance correction. The session-scoped\n`logging/setLevel` filter applies as before on 2025-era connections. (On 2026-07-28\nrequests, the per-request `_meta.logLevel` envelope key is the filter — see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#serving-the-2026-07-28-revision).)\n\n#### Wire tightening (every era)\n\n- **`CallToolResult.content` keeps the v1 parse tolerance on the legacy era.** An\n  inbound result without `content` defaults to `[]` (deployed servers omit it\n  alongside `structuredContent`); 2026-07-28 connections stay strict. Authoring is\n  unchanged and era-independent: the TypeScript surface requires `content` on handler\n  results, and a content-less handler result is normalized to `content: []` before it\n  reaches the wire. One sharpening remains: a content-less body carrying another\n  result family's vocabulary (a task handle or an `input_required` round) is still\n  rejected loudly — tolerance never turns a different result kind into a silent empty\n  success. A body whose only foreign key was `resultType` strips to an empty object\n  and defaults, exactly as v1 parsed a payload-free body.\n- **`ElicitResult.content` values are typed and validated as\n  `string | number | boolean | string[]`.** v1's TypeScript surface accepted\n  `Record<string, unknown>` content values; an elicitation handler returning arbitrary\n  objects now fails to compile (and fails schema validation) — narrow to the primitives\n  the elicitation spec allows.\n- **Custom (3-arg) handlers receive `_meta`.** `setRequestHandler(method, {params}, handler)`\n  used to delete `params._meta` before validation; it now passes `_meta` through (minus\n  the reserved `io.modelcontextprotocol/*` envelope keys). If your params schema is\n  strict, add an optional `_meta` member.\n- **`specTypeSchemas` validate the neutral model.** Result entries no longer accept\n  `resultType`; the validators for the 2025-only task message types and\n  `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed accordingly).\n- **Sampling `hasTools` discriminant** now keys on `tools || toolChoice` (previously\n  `tools` only) when selecting the with-tools `CreateMessageResult` variant, on every\n  era.\n- **Inbound frames that fail message-shape validation are not answered.** v2 routes\n  every inbound frame through typed message guards; a frame that matches no JSON-RPC\n  shape (e.g. a hand-built ping with an explicitly-`undefined` `id`, or non-object\n  `params`) is dropped and surfaces only via `onerror` (`Unknown message type: …`) — no\n  response is sent. v1-era test fences that await a reply to a hand-written raw frame\n  hang instead of resolving; send through the typed surface (`client.ping()`,\n  `client.request()`) instead.\n\n#### Experimental tasks interception removed\n\nThe 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`).\nNo mechanical migration; remove usages. Gone: `ProtocolOptions.tasks`,\n`protocol.taskManager`, `RequestOptions.task` / `relatedTask`, `BaseContext.task`,\n`assertTaskCapability` / `assertTaskHandlerCapability`, `*.experimental.tasks.*`\naccessors and `Experimental{Client,Server,McpServer}Tasks`, `requestStream` /\n`callToolStream` / `createMessageStream` / `elicitInputStream` and the `ResponseMessage`\ntypes they yielded, `registerToolTask`, `ToolTaskHandler`, `TaskRequestHandler`,\n`CreateTaskRequestHandler`, `TaskMessageQueue`, `InMemoryTaskMessageQueue`,\n`BaseQueuedMessage` / `Queued*`, `CreateTaskServerContext`, `TaskServerContext`,\n`TaskToolExecution`, `TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`,\nand the `new McpServer(info, { taskStore, taskMessageQueue })` constructor option keys\n(the codemod emits an action-required diagnostic at each — remove the option).\n\nThe task **wire types** remain importable as `@deprecated` vocabulary for 2025-11-25\ninterop — see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#tasks-deprecated-wire-vocabulary).\n\n#### Specification clarifications adopted (no SDK behavior change)\n\nThe 2026-07-28 specification revision includes a number of documentation-only\nclarifications recorded here so an audit of the revision's changelog against this guide\nis complete; nothing in this list requires code changes: per-operation timeout guidance\nremoval (`RequestOptions.timeout` / `DEFAULT_REQUEST_TIMEOUT_MSEC` unchanged); stdio\nshutdown wording; transports-as-bindings reframe; `resources/read` wording (the\n`file://` path-sanitization MUST is server-author guidance — your handler must reject\ntraversal / symlink escapes itself); `PromptMessage` resource links (already in\n`ContentBlock`); completion `ref/resource` URI templates; pagination empty-string\ncursors (already passed through verbatim); sampling host-requirement docs; elicitation\nstatefulness wording; cosmetic schema/JSDoc sweeps.\n","isInternal":false,"tokens":4337,"sizeBytes":18164},{"name":"enhancements.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/enhancements.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/enhancements.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"## Enhancements\n\n### Automatic JSON Schema validator selection by runtime\n\nThe SDK auto-selects the validator: Node.js → AJV; Cloudflare Workers (workerd) →\n`@cfworker/json-schema`. Cloudflare Workers users can remove explicit\n`jsonSchemaValidator` configuration. You don't need to install `ajv`, `ajv-formats`, or\n`@cfworker/json-schema` for the default path. To customize the built-in backend, import\nthe named class from the explicit subpath\n(`@modelcontextprotocol/{client,server}/validators/ajv` or `…/cf-worker`) — importing\nfrom a subpath means the corresponding peer dep must be in your `package.json`.\n\n### `Client.connect(transport, { prior })` — connect from a cached era verdict\n\nProbe once, persist `client.getDiscoverResult()` (`JSON.stringify`), and feed it to\nevery worker as `client.connect(transport, { prior: { kind: 'modern', discover } })`.\nNew exported types\n`ConnectOptions` (extends `RequestOptions` with `prior?: PriorDiscovery`)\nand `PriorDiscovery` — a cached era verdict: the modern arm wraps a `DiscoverResult`\n(zero round trips), the legacy arm (`{ kind: 'legacy' }`) skips the probe and runs the\nplain `initialize` handshake for servers known to be pre-2026. Freshness is the\nsupplying host's responsibility — date cached legacy verdicts in your own storage and\nstop supplying them past your policy horizon (a stale one succeeds silently against an\nupgraded server).\n\n### Serving the 2026-07-28 revision\n\n`createMcpHandler`, `serveStdio`, `versionNegotiation`, multi-round-trip requests\n(`requestState`), client cancellation via stream-close, `subscriptions/listen`,\n`Mcp-Param-*` headers, and per-era wire codecs are covered in\n**[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)** — they are net-new in v2, not v1→v2\nbreaks.\n","isInternal":false,"tokens":453,"sizeBytes":1825},{"name":"errors.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/errors.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/errors.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Errors\n\nThe SDK now distinguishes three error kinds:\n\n1. **`ProtocolError`** (renamed from `McpError`) — protocol errors that cross the wire\n   as JSON-RPC error responses. Uses `ProtocolErrorCode` (renamed from `ErrorCode`).\n2. **`SdkError`** — local SDK errors that never cross the wire. Uses `SdkErrorCode`.\n3. **`SdkHttpError`** (extends `SdkError`) — HTTP transport errors with typed `.status`\n   and `.statusText`.\n\nThese classes (and `OAuthError`, the client's `SseError`, `UnauthorizedError`, and the\nOAuth-client-flow error family) brand-match under `instanceof`, so checks work across\nseparately bundled copies of the SDK — e.g. a process using both\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server`. Each branded\nhierarchy also exposes the same check as an explicit static guard\n(`SdkError.isInstance(err)`, `ProtocolError.isInstance(err)`, …) that narrows in\nTypeScript — use whichever style your codebase prefers; both read the same brand.\nFine print (applies equally to `instanceof` and `isInstance`):\n\n- **Version skew** — matching needs _both_ copies at a brand-aware release; against an\n  older copy, behavior degrades to plain prototype `instanceof` (false across bundles).\n  During mixed-version rollouts, recognize errors without class identity: match\n  `error.name` plus the class's discriminant field (`code`, `status`), or reconstruct\n  typed protocol errors with `ProtocolError.fromError(code, message, data)`.\n- **Worker boundaries** — `structuredClone`/`postMessage` drop the (symbol-keyed) brand,\n  so a rehydrated error no longer brand-matches; recognize forwarded errors by\n  `code`/`data` instead.\n- **Brands assert identity, not shape** — a matched instance from another SDK version\n  may lack newer fields; read fields defensively.\n- **Re-bundling with property mangling** (`mangle.props` and similar) breaks the brand\n  statics; default esbuild/webpack/terser settings are safe.\n\nThe codemod renames `McpError` → `ProtocolError`, `ErrorCode` → `ProtocolErrorCode`\n(routing `RequestTimeout` / `ConnectionClosed` to `SdkErrorCode`), and\n`StreamableHTTPError` → `SdkHttpError`. After the codemod runs, your `instanceof`\nchecks already name the v2 classes — what's left is choosing which `SdkErrorCode` /\nclass to match per scenario:\n\n| Scenario                                         | v1                                        | v2                                                                 |\n| ------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------ |\n| Request timeout                                  | `McpError` + `ErrorCode.RequestTimeout`   | `SdkError` + `SdkErrorCode.RequestTimeout`                         |\n| Connection closed                                | `McpError` + `ErrorCode.ConnectionClosed` | `SdkError` + `SdkErrorCode.ConnectionClosed`                       |\n| Capability not supported                         | `new Error(...)`                          | `SdkError` + `SdkErrorCode.CapabilityNotSupported`                 |\n| Not connected                                    | `new Error('Not connected')`              | `SdkError` + `SdkErrorCode.NotConnected`                           |\n| Response result fails schema                     | raw `ZodError`                            | `SdkError` + `SdkErrorCode.InvalidResult`                          |\n| Invalid params (server response)                 | `McpError` + `ErrorCode.InvalidParams`    | `ProtocolError` + `ProtocolErrorCode.InvalidParams`                |\n| HTTP transport error                             | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttp*`                        |\n| Failed to open SSE stream                        | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToOpenStream`       |\n| 401 after re-auth (circuit break)                | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| `SSEClientTransport.send()` 401 after re-auth    | `UnauthorizedError`                       | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| 403 `insufficient_scope` after step-up retry cap | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpForbidden`                |\n| Unexpected content type                          | `StreamableHTTPError`                     | `SdkError` + `SdkErrorCode.ClientHttpUnexpectedContent`            |\n| Session termination failed                       | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToTerminateSession` |\n\n```typescript\n// v1\nif (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }\nif (error instanceof StreamableHTTPError) { console.log('HTTP status:', error.code); }\n\n// v2\nimport { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }\nif (error instanceof SdkHttpError) {\n    console.log('HTTP status:', error.status, error.statusText);\n    switch (error.code) {\n        case SdkErrorCode.ClientHttpAuthentication:\n        case SdkErrorCode.ClientHttpForbidden:\n        case SdkErrorCode.ClientHttpFailedToOpenStream:\n        case SdkErrorCode.ClientHttpNotImplemented:\n            break;\n    }\n}\n```\n\n`StreamableHTTPError` is removed.\n\n**Status read off `.code` by duck-typing.** Code that classified HTTP failures by the\nstatus without an `instanceof` — `if ('code' in e && e.code === 403)` — silently stops\nmatching: on `SdkHttpError` the HTTP status moved to `.status` (its `.code` is a\n`SdkErrorCode` string). The codemod renames `instanceof StreamableHTTPError`, but a\nstatus read that never named the class is invisible to it. Watch the inconsistency:\n`SseError` still carries its HTTP status on numeric `.code`, so one duck-typed\n`.code === 401` that caught both transports in v1 now catches only SSE.\n\n```typescript\n// v1 — one duck-typed check caught both Streamable HTTP and SSE\nif ('code' in e && (e.code === 401 || e.code === 403)) reauth();\n// v2 — match each explicitly\nif (e instanceof SdkHttpError && (e.status === 401 || e.status === 403)) reauth(); // Streamable HTTP\nif (e instanceof SseError && (e.code === 401 || e.code === 403)) reauth(); // SSE still uses .code\n```\n\nSilent at runtime (no compile error) — grep for `.code ===` status comparisons.\n\n**Classification keyed on the error class name.** The same import-free classifiers\noften match by name instead of code: telemetry and allowlists keyed on `error.name` or\n`error.constructor.name` against `'McpError'` / `'StreamableHTTPError'` silently stop\nmatching — the v2 classes are named `ProtocolError`, `SdkError`, and `SdkHttpError`,\nand all three assign `.name` accordingly. One v1 asymmetry disappears along the way:\nv1's `StreamableHTTPError` never assigned `.name` (instances reported `'Error'`), so\n`.name`-keyed matchers saw only `'McpError'`; v2's `SdkHttpError` reports\n`'SdkHttpError'`, and assertions pinning `.name === 'Error'` on transport errors need\nre-baselining. Add the v2 names to your match lists; during a\n[staged migration](packaging-runtime.md#migrating-in-stages-large-codebases) keep the v1 names alongside\nfor as long as the v1 package remains installed.\n\n**Status read out of the message text.** Per transport: the Streamable HTTP message\ntext never carried the status (v1 put it on `.code`, v2 puts it on `.status` — read\n`error.status`), and v2's SSE transport still embeds it exactly as v1 did\n(`Error POSTing to endpoint (HTTP 404): …`). The silent break is **switching\ntransports while keeping a message regex**: a status pattern written against SSE\nmatches nothing on Streamable HTTP. Read `error.status` instead of parsing text.\n\n**Raw numeric code comparisons.** The codemod rewrites `ErrorCode.X` symbol references,\nbut a check against the raw JSON-RPC number — `(e as { code?: unknown }).code === -32000`\n— is invisible to it and silently never matches in v2, because the two SDK-local codes\nit usually targeted are now **string** `SdkErrorCode` values:\n\n| v1 numeric                  | v2                                           |\n| --------------------------- | -------------------------------------------- |\n| `-32000` (ConnectionClosed) | `SdkError` + `SdkErrorCode.ConnectionClosed` |\n| `-32001` (RequestTimeout)   | `SdkError` + `SdkErrorCode.RequestTimeout`   |\n\n- Requests that require a session but omit the `Mcp-Session-Id` header still\n  respond `400` with JSON-RPC `-32000` (`Bad Request: Mcp-Session-Id header is\nrequired`), unchanged from v1 — as with `-32001`, the code is an SDK\n  convention; key off the HTTP status.\n\nReplace the literal with the named code. Loud (`TS2367`) when the compared value is\ntyped `SdkErrorCode`; silent when the left side is `unknown` or a cast — grep for\n`=== -32000` / `=== -32001`.\n\n**Dual-role processes: `instanceof` does not cross the packages.**\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` each bundle their own\ncopy of these error classes, so in a process that uses both — a gateway, a host, an\nin-process test — an error constructed by one package fails `instanceof` against the\nclass imported from the other, silently. When an error may originate from the other\npackage, match on stable fields instead of class identity: `error.code` values\n(`SdkErrorCode` strings for SDK errors, numeric JSON-RPC codes for protocol errors,\n`OAuthErrorCode` strings for OAuth errors) plus presence checks like `'status' in e`,\nor reconstruct typed protocol errors with `ProtocolError.fromError(code, message, data)`\n— it exists precisely because `instanceof` does not survive bundle boundaries.\n\n**Constructing the error (test stubs, custom transports).** v1\n`new StreamableHTTPError(code, message)` becomes\n`new SdkHttpError(code, message, data)`: the first argument is now a `SdkErrorCode`\nstring (pick the branch from the scenario table above) and the HTTP status moves into\nthe third argument — `new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented,\n'Not Found', { status: 404, statusText: 'Not Found' })`. v1's implicit\n`Streamable HTTP error: ` message prefix is gone; pass the full message you want.\n\n#### `SdkErrorCode` enum (complete)\n\n| Code                                  | When thrown                                                                |\n| ------------------------------------- | -------------------------------------------------------------------------- |\n| `NotConnected`                        | Transport is not connected                                                 |\n| `AlreadyConnected`                    | Transport is already connected                                             |\n| `NotInitialized`                      | Protocol is not initialized                                                |\n| `CapabilityNotSupported`              | Required capability is not supported                                       |\n| `RequestTimeout`                      | Request timed out waiting for response                                     |\n| `ConnectionClosed`                    | Connection was closed                                                      |\n| `SendFailed`                          | Failed to send message                                                     |\n| `InvalidResult`                       | Response result failed local schema validation                             |\n| `UnsupportedResultType`               | A 2026-era response carried an unrecognized `resultType`                   |\n| `InputRequiredRoundsExceeded`         | Multi-round-trip auto-fulfilment hit `maxRounds`                           |\n| `ListPaginationExceeded`              | No-arg `list*()` aggregate walk hit `listMaxPages`                         |\n| `MethodNotSupportedByProtocolVersion` | Outbound spec method does not exist on the negotiated protocol version     |\n| `EraNegotiationFailed`                | `connect()` could not negotiate a protocol era (probe failed / no overlap) |\n| `ClientHttpNotImplemented`            | HTTP POST request failed                                                   |\n| `ClientHttpAuthentication`            | Server returned 401 after re-authentication                                |\n| `ClientHttpForbidden`                 | Server returned 403 `insufficient_scope` after step-up retry cap           |\n| `ClientHttpUnexpectedContent`         | Unexpected content type in HTTP response                                   |\n| `ClientHttpFailedToOpenStream`        | Failed to open SSE stream                                                  |\n| `ClientHttpFailedToTerminateSession`  | Failed to terminate session                                                |\n\n#### Typed `ProtocolError` subclasses\n\n`ResourceNotFoundError` (carries `.uri`) and `MissingRequiredClientCapabilityError`\n(carries `data.requiredCapabilities`) are new typed `ProtocolError` subclasses.\n`resources/read` for an unknown URI now answers `-32602` on every protocol revision\n(v1.x already emitted `-32602`; an interim `-32002` from earlier v2 alphas is mapped at\nthe encode seam — `2.0.0-alpha.3` and earlier predate the mapping and still emit\n`-32002` on the wire, so accept both if peers may run those alphas; `2.0.0-alpha.4`\nand later emit `-32602`). The encode-seam mapping applies to **your own throws too**: a handler\nthat deliberately throws `ProtocolError(ProtocolErrorCode.ResourceNotFound, …)` reaches\npeers as `-32602` — a server can no longer emit `-32002` on the wire.\n`ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as\nreceive-tolerated vocabulary — accept both `-32602` and `-32002` from peers.\n`ProtocolError.fromError(code, message, data)` reconstructs the typed subclass from\ncode + data alone — the version-agnostic path: it also works on plain wire shapes and\nagainst SDK copies that predate brand-matched `instanceof`.\nThe default message text changed alongside: v1's unknown-resource error read\n`Resource <uri> not found`; v2's `ResourceNotFoundError` default is\n`Resource not found: <uri>` (the code is unchanged). Tests pinning the exact string\nneed re-baselining — prefer matching `error.code` plus a URI substring (or the typed\n`error.uri`).\n\nCustom **non-spec** codes pass through untouched: a handler that throws a\n`ProtocolError` with a custom code (e.g. `-1`) and `data` reaches the peer as a\nJSON-RPC error with that code and `data` unchanged — the encode seam rewrites only the\nlegacy `-32002` code; `data` is sent verbatim for every thrown error (the typed\nsubclasses shape their `data` at construction, not at encode time). Construct via\n`ProtocolError.fromError(code, message, data)`.\n","isInternal":false,"tokens":3244,"sizeBytes":14986},{"name":"http-headers.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/http-headers.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/http-headers.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### HTTP & headers\n\nHeader **reads** use the Web Standard `Headers` object (`IsomorphicHeaders` is\nremoved): `ctx.http?.req` is a standard Web `Request`, so\n`ctx.http?.req?.headers` takes `.get()` instead of bracket access.\n\n```typescript\n// v1\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = extra.requestInfo?.headers['mcp-session-id'];\n\n// v2 — requestInit is unchanged; only the header *read* changes\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = ctx.http?.req?.headers.get('mcp-session-id');\nconst debug = new URL(ctx.http!.req!.url).searchParams.get('debug');\n```\n\nOn the **write** side, `requestInit` on `StreamableHTTPClientTransport` /\n`SSEClientTransport` options is a standard fetch `RequestInit`, so `headers` accepts\nany `HeadersInit` — a plain object record (as above), a tuple array, or a `Headers`\ninstance all keep working unchanged; the transports normalize whichever form they\nreceive. Wrapping with `new Headers()` is optional, not required.\n\n`StreamableHTTPClientTransport` now **appends** any custom `requestInit.headers.Accept`\nvalue to the spec-required `application/json, text/event-stream` (v1 let it replace\nthem). The required media types are always present; additional types are kept for\nproxy/gateway routing.\n\nTransport-managed headers now take precedence over same-named entries in\n`requestInit.headers`: `Authorization` when `authProvider` yields a token,\n`mcp-protocol-version`, and (Streamable HTTP) `mcp-session-id`. v1 let the configured\nheader win, so a static `Authorization` placeholder kept overriding the OAuth token even\nafter the provider obtained one. A configured `Authorization` value is still sent while\nthe provider has no token, which is what lets a static API key fall back to OAuth.\n\n`hostHeaderValidation()` and `localhostHostValidation()` moved to\n`@modelcontextprotocol/express`. The `(allowedHostnames: string[])` signature is the\nsame as every released v1.x — only the import path changes. Framework-agnostic helpers\n(`validateHostHeader`, `localhostAllowedHostnames`, `hostHeaderValidationResponse`) are\nin `@modelcontextprotocol/server`.\n\nServer entries validate the request `Content-Type` by its **parsed media type**, not a\nsubstring: every POST whose media type is not `application/json` answers\n`415 Unsupported Media Type`. Previously any value merely containing the substring\npassed (for example `text/plain; a=application/json`), case variants were wrongly\nrejected, and the 2026-07-28 entry did not inspect `Content-Type` at all — so\nhand-rolled clients that omit the header (or send a non-JSON type) must now set\n`Content-Type: application/json`. Parameters (`; charset=utf-8`) and unambiguous\nvalues with malformed parameter sections (`application/json;`) keep working; SDK\nclients always sent the correct header and are unaffected. Custom entries that\ncompose `classifyInboundRequest` / `PerRequestHTTPServerTransport` directly must\napply the same validation themselves — use the exported `isJsonContentType(header)`.\n","isInternal":false,"tokens":727,"sizeBytes":3194},{"name":"imports-transports.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/imports-transports.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/imports-transports.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Imports & transports\n\nThe codemod rewrites every `@modelcontextprotocol/sdk/...` import path via\n[`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).\nA few transports need a decision the codemod can't make:\n\n- **`StreamableHTTPServerTransport` → which runtime?** The codemod renames it to\n  `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. If you deploy\n  to a web-standard runtime (Cloudflare Workers, Deno, Bun), use\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`\n  instead. **Decision rule:** if your handler receives a Node `IncomingMessage` /\n  `ServerResponse`, use `@modelcontextprotocol/node`; if it receives a web-standard\n  `Request` and returns a `Response`, use `@modelcontextprotocol/server`.\n- **stdio transports moved to a `./stdio` subpath.** Import `StdioClientTransport`,\n  `getDefaultEnvironment`, `DEFAULT_INHERITED_ENV_VARS`, and `StdioServerParameters`\n  from `@modelcontextprotocol/client/stdio`; import `StdioServerTransport` from\n  `@modelcontextprotocol/server/stdio`. The package root barrels do **not** export\n  these (the root entries are runtime-neutral so browser/Workers bundlers can consume\n  them). The stdio utilities `ReadBuffer`, `serializeMessage`, `deserializeMessage`\n  stay in the root barrel.\n- **Zod `*Schema` constants → `@modelcontextprotocol/core`.** A mixed\n  `import { CallToolResult, CallToolResultSchema } from '…/types.js'` is split by the\n  codemod — see [Types & schemas](types-schemas.md#types--schemas).\n\n    ```typescript\n    // v1\n    import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n    // v2\n    import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';\n    ```\n\n- **`SSEServerTransport`** is removed. Migrate to Streamable HTTP. A frozen v1 copy is\n  available from `@modelcontextprotocol/server-legacy/sse` as a temporary bridge.\n- **`WebSocketClientTransport`** is removed (WebSocket is not a spec transport). Use\n  `StreamableHTTPClientTransport` for remote servers or `StdioClientTransport` for\n  local servers; the `Transport` interface is exported if you need a custom\n  implementation.\n- **`InMemoryTransport`** is now exported from `@modelcontextprotocol/client` and\n  `@modelcontextprotocol/server` (both re-export it). The two packages bundle separate\n  copies with private state, so the halves of a linked pair must come from the **same\n  package's** import — pick one package per file (per linked pair) rather than mixing\n  the client's `InMemoryTransport` with the server's:\n\n    ```typescript\n    // v1\n    import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';\n    // v2\n    import { InMemoryTransport } from '@modelcontextprotocol/server'; // or /client\n    ```\n\n- **`EventStore`, `StreamId`, `EventId`** are exported from `@modelcontextprotocol/server`\n  only (v1 re-exported them alongside the transport from `sdk/server/streamableHttp.js`;\n  `@modelcontextprotocol/node` does not).\n- **Client fetch middleware moved to the root barrel.** `createMiddleware`,\n  `applyMiddlewares`, `withLogging`, `withOAuth`, and the `Middleware` type (v1:\n  `sdk/client/middleware.js`) are now exported from `@modelcontextprotocol/client`\n  directly, as is `FetchLike` (v1: `sdk/shared/transport.js`). The call signatures are\n  unchanged from v1 (`Middleware` is still `(next: FetchLike) => FetchLike`) — only the\n  import path changes.\n- **Server auth split.** Resource Server helpers (`requireBearerAuth`,\n  `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`)\n  → `@modelcontextprotocol/express`; the runtime-neutral core (`requireBearerAuth`\n  for web-standard `fetch` hosts, `verifyBearerToken`, `bearerAuthChallengeResponse`,\n  `OAuthTokenVerifier`, and the discovery serving `oauthMetadataResponse` /\n  `buildOAuthProtectedResourceMetadata` / `getOAuthProtectedResourceMetadataUrl`)\n  is also exported from `@modelcontextprotocol/server`. Authorization Server helpers (`mcpAuthRouter`,\n  `OAuthServerProvider`, `ProxyOAuthServerProvider`, `allowedMethods`,\n  `authenticateClient`, `metadataHandler`, `createOAuthMetadata`,\n  `authorizationHandler` / `tokenHandler` / `revocationHandler` /\n  `clientRegistrationHandler`) → `@modelcontextprotocol/server-legacy/auth`\n  (deprecated, frozen v1 copy); migrate AS to a dedicated IdP/OAuth library. `AuthInfo`\n  is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.\n\n    The codemod's [`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)\n    routes every `…/server/auth/**` deep path (including\n    `…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`,\n    `…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to\n    `@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` /\n    `…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The\n    AS→`server-legacy` routing is conservative — re-point RS-only call sites\n    (`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand.\n    Staying on the frozen `server-legacy/auth` copy is a supported interim choice when you\n    deliberately want the v1 middleware behavior. If you re-point at\n    `@modelcontextprotocol/express` by hand, also add that package — plus its `express`\n    peer dependency — to your manifest: the codemod's manifest summary reflects only the\n    imports it wrote, not re-points you make afterwards.\n","isInternal":false,"tokens":1396,"sizeBytes":5717},{"name":"packaging-runtime.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/packaging-runtime.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/packaging-runtime.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Packaging & runtime\n\nThe single `@modelcontextprotocol/sdk` package is split:\n\n| v1                              | v2                                                                                                                              |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk`     | `@modelcontextprotocol/client` (client implementation)                                                                          |\n|                                 | `@modelcontextprotocol/server` (server implementation)                                                                          |\n|                                 | `@modelcontextprotocol/core` (public Zod `*Schema` constants)                                                                   |\n|                                 | `@modelcontextprotocol/core-internal` (internal — never import directly)                                                        |\n| Built-in HTTP framework support | `@modelcontextprotocol/node` / `@modelcontextprotocol/express` / `@modelcontextprotocol/hono` / `@modelcontextprotocol/fastify` |\n\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared\ntypes from `@modelcontextprotocol/core-internal`, so import types and error classes from\nwhichever package you already depend on. `@modelcontextprotocol/core-internal` is\n`private: true` and is not published — **do not import from it directly.**\n`@modelcontextprotocol/core` is the public Zod-schema package (raw `*Schema` constants\nonly); see [Zod `*Schema` constants moved to `@modelcontextprotocol/core`](types-schemas.md#zod-schema-constants-moved-to-modelcontextprotocolcore) below.\n\nAfter the codemod runs, review the manifest summary it prints: the swap rewrites the\n**nearest** manifest found walking up from the target directory — one manifest total.\nWorkspace-member manifests in a monorepo are never modified; instead the codemod lists\neach member that still declares the v1 SDK together with the exact dependency changes\nit needs (remove the v1 entry, add the v2 packages that member's imports use) — apply\nthose edits yourself, then install. The v2 additions are computed from the final import\nstate of each package's sources, so already-migrated sources still receive the v2\npackages they need when the v1 dependency is removed. In a hoisted monorepo (members\nwithout their own SDK dependency), member usage counts toward the manifest that\ndeclares the v1 SDK, and the summary notes which members contributed. See\n[Monorepo workspace members](#monorepo-workspace-members) for how to decide each\nmember's packages.\n\n#### Monorepo workspace members\n\nDeclare in every member exactly what its own sources import: files importing\n`@modelcontextprotocol/server` (or its subpaths) need `@modelcontextprotocol/server`;\nclient imports need `@modelcontextprotocol/client`; raw `*Schema` constants need\n`@modelcontextprotocol/core`; a framework adapter import (`@modelcontextprotocol/express`\netc.) needs the adapter package **plus the framework itself** in that member (the\nadapter declares it as a peer dependency). Place a package in `dependencies` when\nshipped runtime code imports it and in `devDependencies` when only tests, fixtures, or\nlocal tooling do — when in doubt, use the section where the member previously declared\n`@modelcontextprotocol/sdk`.\n\nA member that never declared the v1 SDK and resolved it through the root can keep\nroot-level declarations (the codemod's root rewrite already adds the union of the\ncontributing members' v2 packages — its hoisting note names them) or move to\nper-member declarations; per-member is recommended, since the v2 package split makes each member's\nactual needs explicit. To answer \"which packages does this member need\" directly, run\nthe codemod against that member's directory with `--dry-run`: the manifest summary is\ncomputed from that member's own imports. (The authoritative import-path routing lives\nin the codemod's [mapping file](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).)\n\nThe framework adapter packages declare their framework as a **peer dependency**\n(`express`, `hono`, `fastify`); v1 shipped them as direct deps. The codemod adds the\n`@modelcontextprotocol/*` packages your imports use, but does not add the framework\npeer — install it explicitly (`pnpm add express` etc.). `@modelcontextprotocol/node`\ndepends on `@hono/node-server` at runtime (Node HTTP ↔ Web Standard conversion) and\ninstalls `hono` as a regular dependency only because `@hono/node-server` requires it as\na peer — the adapter does not use the `hono` framework itself.\n\nv2 requires **Node.js 20+**. It is ESM-first but ships a **CommonJS build alongside\nESM**, so CommonJS projects can `require('@modelcontextprotocol/…')` directly — no\ndynamic `import()` shim required.\n\nRepo-local tooling that encodes the literal v1 package name — dependency-pin lints,\nversion allowlists, CI checks, scripts — fails after the manifest swap and is invisible\nto the codemod (it rewrites sources and manifests, not bespoke gates). Grep for\n`@modelcontextprotocol/sdk` outside `src/` before declaring the migration done. While\ngrepping, also remove v1-era double casts on SDK types (`as unknown as Transport` and\nsimilar, usually annotated to a v1 issue) — v2's types satisfy those contracts\ndirectly, and a surviving cast keeps suppressing type checking that would otherwise\ncatch real errors.\n\nTooling that pins SDK **dist text** (reading a constant out of a built file with\n`require.resolve` + a regex) breaks in two stacked ways: the literal usually lives in a\ncontent-hashed sibling chunk (`dist/sse-<hash>.mjs`), not the subpath's entry module,\nso fixed-path reads do not survive a rebuild — scan the package's `dist/` directory\nfor the literal instead; and the emitted quote style differs from v1, so a\nquote-anchored pattern misses silently — match either quote. The build layout also\nchanged: v2 emits `.mjs`/`.cjs` siblings in a flat `dist/`, so v1's `/dist/cjs/` ↔\n`/dist/esm/` flavor-pair path swaps have no equivalent.\n\n#### Registry availability\n\nAll v2 packages are published on the public npm registry. Two notes:\n\n- As of `2.0.0-beta.1` all v2 packages share one version number (earlier alphas\n  did not). The codemod writes ranges that match what is published, so prefer its\n  manifest output over hand-pinning every package.\n- Environments that resolve through a corporate or private registry mirror may not\n  have synced the newer scoped packages yet (the symptom is \"not found\" for a package\n  that exists on npmjs.org). Point the install at the public registry\n  (`npm install --registry=https://registry.npmjs.org/` or the equivalent `.npmrc`\n  entry), ask your mirror's operators to sync the `@modelcontextprotocol` scope, or —\n  where neither is possible — build a tarball from a checkout of this repository\n  (`pnpm install && pnpm build`, then `pnpm pack` in the package directory) and\n  reference it with a committed `file:` dependency.\n\n#### CommonJS test runners (Jest)\n\nv2 ships a CommonJS build, so CJS test runners resolve the packages natively through the\n`require` export condition — Jest (including `next/jest` setups) no longer needs a\n`moduleNameMapper` workaround to import `@modelcontextprotocol/*`. If you carried a\nv1-era mapping that pinned these packages to their `dist/*.mjs` files, remove it. Vitest\nand native Node ESM are unaffected.\n\n#### Bundlers: nested `zod` copies in zod@3-pinned monorepos\n\nv1's `zod ^3.25 || ^4.0` peer range deduplicated onto a workspace's hoisted zod@3. The\nv2 packages depend on `zod ^4.2.0`, so in a workspace that pins zod@3 the dependency\ncannot dedupe — each installed v2 package resolves its own nested zod@4 copy. Two\nbundler consequences:\n\n- **Path-substring vendor pins capture the nested copies.** Bundler rules that match\n  zod by module path — `manualChunks` pins, vendor-chunk matchers, bundle budgets keyed\n  on a `zod/` path segment — also match `@modelcontextprotocol/*/node_modules/zod`,\n  which can pull the nested copies into an eagerly-loaded vendor chunk and trip a\n  budget gate. Exclude the SDK-nested paths from such pins so the copies ride with the\n  SDK's own (typically lazy) chunks.\n- **Ballpark size cost.** Measured on a large production SPA, adding the v2 client and\n  server packages (with their nested zod@4 copies) alongside a hoisted zod@3 cost\n  roughly +83 KB gzipped of total JS (about +0.7% whole-app). Upgrading the workspace\n  to `zod ^4.2.0` re-dedupes and removes the duplication.\n\n#### Migrating in stages (large codebases)\n\nThe v1 package and the v2 packages have **different names**, so both can be installed\nin one manifest at the same time — nothing forces a one-shot swap. The safe order for\nan incremental migration: (1) add the v2 packages (and the `zod ^4.2.0` bump) while\n**keeping** `@modelcontextprotocol/sdk`; (2) rewrite sources incrementally,\ndirectory-by-directory or package-by-package; (3) remove the v1 dependency only when\nnothing imports it any more (`grep -rn \"@modelcontextprotocol/sdk\" --include=\"*.ts\"`,\nplus a look at `package.json`). The inverse order strands files: swapping the manifest\nfirst leaves every not-yet-rewritten import failing module resolution (TS2307) until it\nis updated.\n\nTwo caveats for the transition window. First, a codemod run against a subdirectory\nstill updates the nearest manifest walking up — including removing the v1 dependency —\nso during a staged pass review or revert that edit until the final stage (or preview\nwith `--dry-run`). Second, v1 and v2 modules each have their own classes and types:\nobjects must not flow between v1-imported and v2-imported code (`instanceof` and\nnominal types do not cross — the same boundary described for dual-role processes in\n[Errors](errors.md#errors)), so stage along process or transport boundaries where the two sides\nshare only the wire format; the two sides negotiate\na protocol version through the ordinary 2025-era `initialize` handshake and settle\non the newest revision both packages support (currently 2025-11-25 — published v1\n1.29.x and v2 ship the same supported-version list).\n\nDependencies you do not control (vendored fixtures, third-party packages) that still\ndeclare `@modelcontextprotocol/sdk` resolve their own v1 copy and need no action. For\n`peerDependencies` declarations, keep the v1 package installed to satisfy the range —\nor point the name at a chosen version via your package manager's\n`overrides`/`resolutions` — until those packages migrate. The same boundary rule\napplies: objects must not flow between their v1-imported code and your v2-imported\ncode.\n\n**Dependencies that compile against the host's v1 SDK.** A stricter variant of the\nabove: a workspace or vendored package that ships TypeScript **source** importing\n`@modelcontextprotocol/sdk` — resolved from the host's `node_modules` rather than its\nown — pins the host. Keep the v1 package installed as a real dependency (not merely a\nsurviving transitive) until that package migrates. The host files that construct or\nhand objects to such a package are part of its v1 boundary and must stay on v1 imports\n— and the codemod cannot see that distinction: it rewrites them like any other file\n(e.g. converting a `setRequestHandler(Schema, …)` call into the v2 method-string form\nagainst what is still a v1 `Server`, which then fails at runtime). Run the codemod with\n`--ignore` glob patterns covering those interfacing files, and migrate them together\nwith the dependency later. The boundary rule above applies unchanged: objects from the\ndependency's v1 modules must never flow into v2-imported code.\n\n#### Library authors: peer-depending on the SDK\n\nIf your package declares `@modelcontextprotocol/sdk` as a `peerDependency`, the v2\npackages are differently **named**, so swapping the peer declaration is itself a\nbreaking change for every consumer — ship it as a semver-major. You can migrate\nahead of your consumers only if no SDK object crosses your public API (the\nv1/v2 boundary rule above applies to your exports too: a v1-constructed `Client`\nor error instance handed to v2-importing consumer code fails `instanceof` and\nnominal checks). Until your consumers migrate, they can keep resolving your peer\nrange with the v1 package installed alongside their own v2 packages — the two\ncoexist under different names.\n","isInternal":false,"tokens":2853,"sizeBytes":12632},{"name":"protocol-ctx.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/protocol-ctx.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/protocol-ctx.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Low-level protocol & handler context (`ctx`)\n\nThe second parameter to every request handler — previously the flat `RequestHandlerExtra`\nobject named `extra` — is now a structured **context** object named `ctx`. This is the\n`ctx` that appears throughout the rest of this guide.\n\nThe codemod renames the parameter and remaps property access via\n[`contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts).\nA few mappings need optional-chaining adjustment (the `http` group is `undefined` on\nstdio):\n\n| v1 (`extra.*`)                                    | v2 (`ctx.*`)                   | Note                                                                                                                                                                                                                                                                                                                    |\n| ------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `extra.signal`                                    | `ctx.mcpReq.signal`            |                                                                                                                                                                                                                                                                                                                         |\n| `extra.requestId`                                 | `ctx.mcpReq.id`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra._meta`                                     | `ctx.mcpReq._meta`             |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendRequest(...)`                          | `ctx.mcpReq.send(...)`         |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendNotification(...)`                     | `ctx.mcpReq.notify(...)`       |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sessionId`                                 | `ctx.sessionId`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra.authInfo`                                  | `ctx.http?.authInfo`           | optional — `undefined` on stdio                                                                                                                                                                                                                                                                                         |\n| `extra.requestInfo`                               | `ctx.http?.req`                | a standard Web `Request`; `ServerContext` only                                                                                                                                                                                                                                                                          |\n| `extra.closeSSEStream`                            | `ctx.http?.closeSSE`           | `ServerContext` only; the member itself is also optional — defined only when the transport has an `eventStore` AND the client's negotiated protocol version supports resumable close (2025-11-25+); an `eventStore` transport serving a 2025-06-18 client still leaves it `undefined`. Call as `ctx.http?.closeSSE?.()` |\n| `extra.closeStandaloneSSEStream`                  | `ctx.http?.closeStandaloneSSE` | `ServerContext` only; member optional as above — `ctx.http?.closeStandaloneSSE?.()`                                                                                                                                                                                                                                     |\n| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed_                      | see [Experimental tasks](behavioral-changes.md#experimental-tasks-interception-removed)                                                                                                                                                                                                                                 |\n\nThe transport-level seam behind `ctx.http?.authInfo` is unchanged from v1: a transport\nthat passes `{ authInfo }` as the second argument to `onmessage(message, extra)` — e.g.\nan `InMemoryTransport` test seam — still surfaces it as `ctx.http?.authInfo` on any\ntransport, and `ctx.http` is defined whenever `authInfo` is supplied, even without an\nHTTP transport.\n\n`BaseContext` is the common base; `ServerContext` and `ClientContext` extend it. None\nof the three takes type parameters — v1's `RequestHandlerExtra<TRequest, TNotification>`\narguments selected request/notification unions that the v2 context carries\nintrinsically, so their removal loses no type information; review only handlers that\npassed custom (non-standard) unions, whose `sendRequest` / `sendNotification` typing\nwas narrowed by them. `ServerContext.mcpReq` adds convenience methods that replace\ncalling `server.*` from inside a handler:\n\n| `ctx.mcpReq.*` (new)                           | Replaces (inside a handler)                                                                                                                                                                                                                                                                              |\n| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ctx.mcpReq.log(level, data, logger?)`         | `server.sendLoggingMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577); the notification also becomes request-related on every era — see [§`ctx.mcpReq.log()` is request-related on every era](behavioral-changes.md#ctxmcpreqlog-is-request-related-on-every-era) |\n| `ctx.mcpReq.elicitInput(params, options?)`     | `server.elicitInput(...)`                                                                                                                                                                                                                                                                                |\n| `ctx.mcpReq.requestSampling(params, options?)` | `server.createMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577)                                                                                                                                                                                                  |\n\n#### Deprecated in v2 (SEP-2577)\n\nThe roots, sampling, and logging subsystems are deprecated as of protocol version\n2026-07-28 (SEP-2577). Everything below is **still fully functional in v2** and marked\n`@deprecated` for removal in a later major; on a 2026-07-28 connection prefer the\n[multi-round-trip `input_required` pattern](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#multi-round-trip-requests)\ninstead.\n\n- **Runtime APIs**: `Server.createMessage` / `listRoots` / `sendLoggingMessage`,\n  `McpServer.sendLoggingMessage`, `Client.setLoggingLevel` / `sendRootsListChanged`, and\n  the `ctx.mcpReq.log` / `ctx.mcpReq.requestSampling` handler-context helpers. Outside a\n  handler, `McpServer` users reach the `Server.*` methods via the unchanged\n  [`mcpServer.server` accessor](unchanged-apis.md#unchanged-apis).\n- **Capability fields**: the `roots`, `sampling`, and `logging` capability schema fields.\n- **Type stacks**: the full Logging stack (`LoggingLevel`, `SetLevelRequest`,\n  `LoggingMessageNotification` and params), the full Sampling stack\n  (`CreateMessageRequest`/`Result`, `SamplingMessage`, `ModelPreferences`/`ModelHint`,\n  `ToolChoice`, `ToolUseContent`/`ToolResultContent`, the `includeContext` enum values),\n  and the full Roots stack (`Root`, `ListRootsRequest`/`Result`,\n  `RootsListChangedNotification`).\n- **`registerClient`** (Dynamic Client Registration) — deprecated via spec PR\n  [modelcontextprotocol#2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)\n  rather than SEP-2577 (listed here because the `@deprecated` annotations landed in\n  the same sweep); prefer Client ID Metadata Documents per SEP-991.\n\nThe deprecation is annotation-only — JSDoc `@deprecated` markers were added, nothing\nelse: every deprecated runtime API keeps its v1 call signature (e.g.\n`Server.sendLoggingMessage(params, sessionId?)` keeps the two-argument form) and its\nwire behavior, and remains functional for at least the twelve-month deprecation window.\n\n#### `setRequestHandler` / `setNotificationHandler` use method strings\n\nThe low-level handler registration takes a **method string** instead of a Zod schema.\nThe codemod rewrites every spec-method registration via\n[`schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts).\n\n```typescript\n// v1\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => { ... });\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => { ... });\n```\n\n**Custom (non-spec) methods** use the 3-arg form `(method, { params, result? }, handler)`\nwhere `params` and `result` are any [Standard Schema](https://standardschema.dev). The\nhandler receives the parsed `params` directly (not the full request envelope); `_meta`\nis at `ctx.mcpReq._meta`. The 3-arg notification handler is `(params, notification) => void`.\n\n```typescript\nserver.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => { ... });\n```\n\nThe custom form also covers **spec method names carried with custom payloads**: a v1\nintegration that reused a spec method string for its own payload shape (e.g.\n`notifications/message` notifications carrying a proprietary params object) registers\nit with the 3-arg form and its own schema. The overloads are selected by the arguments'\nshape, not by the method name — a schemas object as the second argument always selects\nthe custom form, which validates against **your** schema (the spec schema is not\napplied) and hands the handler the parsed params rather than the envelope.\n\n**Spec notifications** use the 2-arg form `setNotificationHandler(method, handler)`.\nUnlike the 3-arg custom form, the spec-form handler receives the **full notification\nenvelope** (`{ method, params }`), parsed against the spec schema — read\n`notification.params`:\n\n```typescript\nclient.setNotificationHandler('notifications/tools/list_changed', async notification => {\n    console.log(notification.method, notification.params);\n});\n```\n\nThe two overloads are selected by the method string's **type**: the spec form binds the\nmethod to the `NotificationMethod` union (`RequestMethod` on the request side — both\nexported), so a method string computed at runtime must be typed as `NotificationMethod`\nto select it; an untyped `string` lands on the custom-schema overload and fails to\ncompile without a schemas argument. `Parameters<Client['setNotificationHandler']>[0]`\nalso resolves to the custom `string` overload by design — name `NotificationMethod`\ndirectly instead. The request side has the same trap one slot over:\n`Parameters<Client['setRequestHandler']>` (and `typeof`-indexed casts over the overload\nset) resolve against the 3-arg custom-method overload, so index `[1]` is the\n`{ params, result }` schemas object, **not** the handler — v1 signature-erasing handler\ncasts derived positionally change meaning with no runtime symptom. Name the exported\ntypes (`RequestMethod` and your own handler/param types) instead of deriving them\npositionally. Generic helpers that v1 parameterized on a notification schema need\nthis conversion by hand; the codemod only warns on them.\n\n**Handler returns are spec-typed.** In v1 the handler's return type flowed from the\nschema you registered; v2 types it from the method name (`'tools/list'` →\n`ListToolsResult`, and so on). Tool tables kept as plain object literals surface two\nrecurring compile errors: an unannotated literal widens `type: 'object'` to `string`\nand no longer satisfies the spec type's `type: 'object'` literal member (fix:\n`type: 'object' as const`, or annotate the table as `Tool[]`); and a heterogeneous\ntable whose inferred union carries `prop?: undefined` members does not satisfy the spec\ntypes' `Record<string, JSONValue>` index signatures, since `undefined` is not a\n`JSONValue` (fix: annotate the handler's return type —\n`async (req): Promise<ListToolsResult> => …` — or the table itself, so each literal is\nchecked against the target type instead of being inferred and widened first).\n\n#### `request()`, `ctx.mcpReq.send()`, and `callTool()` no longer require a schema for spec methods\n\nFor **spec** methods, drop the result-schema argument; the SDK resolves it from the\nmethod name. The codemod drops it from `client.request()` and `client.callTool()`; drop\nit from `ctx.mcpReq.send()` by hand.\n\n```typescript\n// v1\nimport { CreateMessageResultSchema } from '@modelcontextprotocol/sdk/types.js';\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n    const r = await extra.sendRequest({ method: 'sampling/createMessage', params: { ... } }, CreateMessageResultSchema);\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => {\n    const r = await ctx.mcpReq.send({ method: 'sampling/createMessage', params: { ... } });\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n```\n\nFor **custom (non-spec)** methods, keep the result-schema argument:\n`await client.request({ method: 'acme/search', params }, SearchResult)` — only drop the\nschema when calling a spec method.\n\n**Forwarding arbitrary methods (gateways / proxies).** Dropping the schema changes\nsemantics, not just the signature: a schema-less spec-method call now **enforces** the\nspec result schema (a non-conforming upstream result is rejected locally with\n`SdkError(SdkErrorCode.InvalidResult)` and a conforming one is re-serialized in schema\nkey order), and a schema-less call for a **non-spec** method throws a `TypeError` at\nthe call site (`'…' is not a spec method; pass a result schema`).\nA relay that forwards `{ method, params }` it does not understand must keep passing an\nexplicit result schema. The v1 idiom survives with an import-path change:\n\n```typescript\nimport { ResultSchema } from '@modelcontextprotocol/core';\nconst result = await upstream.request({ method, params }, ResultSchema); // v1-identical passthrough\n```\n\nFor byte-exact forwarding (member order preserved), pass your own accept-anything\nStandard Schema instead. Check call sites whose `method` is **not a literal** — the\ncodemod may have dropped the schema argument there; restore it.\n\nThe **inbound half** — a relay re-emitting an upstream JSON-RPC error from its own\nhandler — has a supported surface too: reconstruct the typed error with\n`ProtocolError.fromError(code, message, data)` and throw it; the encode seam serializes\nit back to the wire shape (see [Typed `ProtocolError` subclasses](errors.md#typed-protocolerror-subclasses)).\nNote this is typed reconstruction, not byte-exact relay: legacy codes are normalized at\nthe encode seam (`-32002` re-emits as `-32602`) and the typed subclasses keep only their\nschema-defined `data` members, so extra upstream data keys are dropped. Throwing a plain\nobject carrying `.code` / `.message` / `.data` happens to work today, but it is\nunspecified behavior — prefer `fromError`.\n\nThe return type is inferred from the method name via `ResultTypeMap` (e.g.\n`client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`).\nv1 call sites that passed `CreateMessageResultWithToolsSchema` explicitly need no\nreplacement: the schema-less send resolves to\n`CreateMessageResult | CreateMessageResultWithTools`, and validation selects the\nwith-tools variant when the request set `tools` or `toolChoice`.\n","isInternal":false,"tokens":3217,"sizeBytes":17986},{"name":"server-registration.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Server registration API\n\nThe deprecated variadic `.tool()`, `.prompt()`, `.resource()` are removed. Use\n`registerTool` / `registerPrompt` / `registerResource` with an explicit config object.\nThe codemod converts the call shape and wraps `inputSchema` / `outputSchema` /\n`argsSchema` / `uriSchema` raw shapes.\n\n```typescript\n// v1 — raw shape, variadic\nserver.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n\n// v2 — config object, Standard Schema\nserver.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n```\n\n`registerResource` requires a `metadata` argument — pass `{}` if you have none.\n\nA tool or prompt registered **without** an `inputSchema` / `argsSchema` passes the\ncontext as its callback's single argument — v1 passed `(extra)`, v2 passes `(ctx)`:\n\n```typescript\nserver.registerTool('ping', { description: 'Liveness check' }, async ctx => ({ content: [] }));\n```\n\nA one-parameter callback typechecks under either reading, so remember that the first\nparameter here is the context object, not an args object.\n\n#### Standard Schema objects (raw shapes deprecated)\n\nv2 expects schema objects implementing the [Standard Schema spec](https://standardschema.dev/)\nfor `inputSchema`, `outputSchema`, and `argsSchema`. Raw `{ field: z.string() }` shapes\nare still **accepted via `@deprecated` overloads** on `registerTool`/`registerPrompt`\n(auto-wrapped with `z.object()`), and `completable()` accepts any `StandardSchemaV1`;\nprefer wrapping explicitly. Zod v4, ArkType, and Valibot all implement the spec.\n\nFor **optional completable arguments**, apply `.optional()` to the _result_ of\n`completable()` — `completable(z.string(), cb).optional()`, not\n`completable(z.string().optional(), cb)`. v2 resolves completion metadata on the schema\nfound after unwrapping an outer optional wrapper, so the v1 nesting returns empty\ncompletion lists — nothing errors — and if no argument carries completion metadata in\nthe v2 position, the server does not advertise the `completions` capability at all. The\ncodemod inverts the common nesting automatically and flags shapes it cannot rewrite.\n\n**Zod v3 is no longer supported** (v1 peer was `^3.25 || ^4.0`). Check the **declared\nrange** in your `package.json`, not just the installed version: a zod-3 range that\nsatisfied the v1 peer installs and typechecks cleanly under v2 and only fails at\nruntime — and quietly: registration does not convert the schema, the server starts\nand connects normally, and the first `tools/list` (so `client.listTools()`) answers\nwith an error pointing at `fromJsonSchema()` while the process keeps running. (Only the\ndeprecated unwrapped raw-shape form with zod-3 field values throws at registration,\nwith a message pointing at `zod/v4`.) Zod **≥4.2.0** self-converts via\n`~standard.jsonSchema` — the supported path. Zod **4.0–4.1** lacks it, so the SDK falls\nback to its bundled Zod's `z.toJSONSchema()` with a one-time `[mcp-sdk]` console\nwarning; and because `.describe()` field descriptions live in the _authoring_ Zod's\nregistry, the fallback **drops them** from the generated JSON Schema. Fix ladder:\n(1) upgrade to `zod ^4.2.0`; (2) if you must pin an older or separate Zod, attach a\n`~standard.jsonSchema` provider backed by _your_ Zod's `toJSONSchema` so conversion\n(and descriptions) run through your instance; (3) author the schema as raw JSON Schema\nvia `fromJsonSchema()`. (Raw shapes are wrapped with the SDK's **bundled** Zod — built\nwith a foreign Zod they fail at registration or at the first `tools/list`; pass\n`z.object()`-wrapped schemas from your own Zod instead.)\n\nIn a monorepo that pins zod@3 workspace-wide and cannot bump, step (1) can be applied\n**per workspace member**: add a zod-4 alias dependency to the migrating member only —\n`\"zod-v4\": \"npm:zod@^4.2.0\"` in that member's `package.json` — and author SDK-bound\nschemas with it (`import { z } from 'zod-v4'`), leaving the rest of the workspace, and\nthe member's own zod-3 consumer schemas, untouched. The alias copy does not need to be\nthe same instance as the SDK's bundled zod: conversion runs through the **authoring**\ninstance's `~standard.jsonSchema`, so `.describe()` descriptions are preserved and the\nemitted dialect is 2020-12. Keep the two z's apart — schemas authored with the alias\nare for the SDK; they do not compose with the workspace's zod-3 schemas. (For the\nbundle-side effects of the same pin, see\n[Bundlers: nested `zod` copies](packaging-runtime.md#bundlers-nested-zod-copies-in-zod3-pinned-monorepos).)\n\n**Hosts that forward consumer-authored schemas.** The ladder assumes you author the\nschemas yourself. A host API that accepts raw shapes or schemas written by **its own\nconsumers** — plugin systems, agent frameworks — cannot control the authoring zod\nversion or instance, and v1's built-in conversion of foreign shapes is gone. Convert on\nthe host side and register the result with `fromJsonSchema()`: zod-4 input via zod's\nown `z.toJSONSchema(z.object(shape), { io: 'input', target: 'draft-2020-12' })` (the\nconversion is runtime-structural, so a zod ≥4.2 in the host handles schemas built by a\ndifferent zod-4 copy), zod-3 input via the\n[`zod-to-json-schema`](https://www.npmjs.com/package/zod-to-json-schema) package. Its\ndefault draft-07 `$schema` stamp is fine as-is — the default validator\n[honors declared draft-07/06 dialects](types-schemas.md#json-schema-2020-12-posture-sep-1613-sep-2106).\n\nHow a too-old zod surfaces depends on which entry point your code imports. With\nmain-entry `import { z } from 'zod'` on a zod-3 range, the project **typechecks cleanly\nand fails at the first `tools/list`** (the quiet runtime path above). With\n`import * as z from 'zod/v4'` — or any zod whose _typings_ predate\n`~standard.jsonSchema` (zod 4.0–4.1, and zod 3.25.x via the `zod/v4` subpath) — the\nsame code **runs** through the bundled fallback but **fails to compile**:\n`registerTool`/`registerPrompt` reject the schema with `TS2769: No overload matches\nthis call` listing both overloads. The real cause is buried in the first overload's\nelaboration — `Property 'jsonSchema' is missing in type …` (that property is\n`~standard.jsonSchema`, added in zod 4.2.0) — and a follow-on implicit-`any` error on\nthe handler's arguments usually appears below it. If you see that two-overload error on\na registration call with a zod schema, check the installed zod version before anything\nelse; both symptoms resolve identically with step (1) of the ladder.\n\nProjects that must stay below zod 4.2 and accept the documented runtime fallback can\nresolve the remaining registration compile errors with an explicit assertion to the\nregistration schema type — `inputSchema: schema as unknown as\nStandardSchemaWithJSON<Input, Output>` — or a small typed wrapper that attaches a\n`~standard.jsonSchema` provider (step (2) of the ladder, which changes runtime\nconversion but not the schema's static type) and returns the asserted type. The\nfallback caveats (one-time warning, dropped `.describe()` descriptions) still apply\nunless the provider is attached.\n\nThe forced zod-4 bump also surfaces zod's **own** type-level API changes in consumer\nannotations: `z.ZodTypeDef` no longer exists and `z.ZodType`'s generic parameters\nchanged, so v3-era annotations like `z.ZodType<Output, z.ZodTypeDef, Input>` fail to\ncompile — see [zod's v3-to-v4 changelog](https://zod.dev/v4/changelog). Consumer-only\nschemas can keep compiling via zod's v3 compat subpath (`zod/v3`), but anything passed\nto the SDK must be a zod-4 (or other Standard Schema) schema.\n\nThe deprecated raw-shape overloads exist only on `registerTool` / `registerPrompt`.\n`RegisteredTool.update()` / `RegisteredPrompt.update()` take **schema objects**\n(`paramsSchema` / `outputSchema`: `StandardSchemaWithJSON`) — a raw shape passed to\n`update()` is not auto-wrapped; wrap it with `z.object()` yourself.\n\n```typescript\nimport * as z from 'zod/v4';\nserver.registerTool('greet', { inputSchema: z.object({ name: z.string() }) }, handler);\n\n// ArkType works too\nimport { type } from 'arktype';\nserver.registerTool('greet', { inputSchema: type({ name: 'string' }) }, handler);\n\n// Raw JSON Schema via fromJsonSchema (validator defaults to runtime-appropriate choice)\nimport { fromJsonSchema } from '@modelcontextprotocol/server';\nserver.registerTool('greet', { inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }) }, handler);\n\n// No-parameter tools: z.object({})\n```\n\nRemoved Zod-specific helpers (the codemod marks each call site `@mcp-codemod-error`):\n`schemaToJson` — use `fromJsonSchema()` from `@modelcontextprotocol/server` for raw JSON\nSchema, or your schema library's native JSON-Schema conversion; `parseSchemaAsync` — use\nyour schema library's validation directly (e.g. Zod's `.safeParseAsync()`);\n`getSchemaShape` / `getSchemaDescription` / `isOptionalSchema` / `unwrapOptionalSchema`\nhave no replacement (internal Zod introspection). `SchemaInput<T>` →\n`StandardSchemaWithJSON.InferInput<T>` is rewritten mechanically by the codemod. The\ninternal `standardSchemaToJsonSchema` / `validateStandardSchema` helpers are **not** part\nof the public surface — do not import them.\n\nv1's second compat module, `server/zod-json-schema-compat.js` (`toJsonSchemaCompat`), is\nalso removed — and the codemod does **not** rewrite its import (expect `TS2307`). If you\nbuild `Tool` / `Prompt` advertisements yourself, use your schema library's native\nconversion: zod 4's `z.toJSONSchema(schema, { io: 'input', target: 'draft-2020-12' })`\nproduces the dialect v2 advertises.\n","isInternal":false,"tokens":2541,"sizeBytes":9828},{"name":"types-schemas.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/types-schemas.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/types-schemas.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"### Types & schemas\n\n#### Zod `*Schema` constants moved to `@modelcontextprotocol/core`\n\nThe Zod schemas (`CallToolResultSchema`, `ListToolsResultSchema`, …) that v1 exported\nfrom `types.js` now live in a separate **`@modelcontextprotocol/core`** package. Neither\n`@modelcontextprotocol/client` nor `@modelcontextprotocol/server` re-exports them — both\npackages stay Zod-free in their public surface.\n\nThe v1→v2 change is just an import-path swap — `.parse()` / `.safeParse()` keep working\nunchanged:\n\n```typescript\n// v1\nimport { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n\n// v2 — same Zod schema, new package\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n```\n\n`@modelcontextprotocol/core` is the canonical home for the spec's Zod schema constants\n(and the OAuth/OpenID metadata schemas). It is runtime-neutral (its only dependency is\n`zod`) and arrives transitively as the shared runtime schema graph of `client` /\n`server` — add it to your own `dependencies` only when you import the raw schemas\ndirectly.\n\nIf you would rather keep your project Zod-free, the **`isSpecType` / `specTypeSchemas`**\nalternatives are exported from `@modelcontextprotocol/client` and `…/server`:\n\n```typescript\nimport { isSpecType, specTypeSchemas } from '@modelcontextprotocol/client';\nif (isSpecType.CallToolResult(value)) { ... }\nconst blocks = mixed.filter(isSpecType.ContentBlock);\nconst result = specTypeSchemas.CallToolResult['~standard'].validate(value);\n```\n\n`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of\nevery named type in the MCP spec — so you get autocomplete and a compile error on typos.\n`specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` (`validate()` is synchronous).\n`validate()` returns `{ value }` or `{ issues }` and never throws — unlike `.parse()` on\nthe real schema; code that caught a `ZodError` should inspect `result.issues` (or keep\n`.parse()` on the schema imported from `@modelcontextprotocol/core`).\nThe pre-existing `isCallToolResult(value)` guard still works.\n\n**`specTypeSchemas.X` is `StandardSchemaV1`, not `ZodType`.** Zod-specific composition\n— `.extend()`, `.pick()`, `.omit()`, `.merge()`, `.shape`, `.passthrough()`,\n`.parseAsync()` — does **not** compile on a `specTypeSchemas` entry; reach for the real\nZod schema from `@modelcontextprotocol/core` when you need to derive a tolerant variant\nof a spec schema (e.g.\n`ListToolsResultSchema.extend({ tools: ToolSchema.omit({ outputSchema: true }).array() })`).\nThe Zod-specific `AnySchema` / `SchemaOutput` types from `…/zod-compat.js` are removed —\nreplace with `StandardSchemaV1` / `StandardSchemaV1.InferOutput<T>` (the codemod's\nremoval message says the same).\n\n**Composing two core schemas.** Zod composition needs a shared zod: deriving from a\nsingle core schema (as above) and combining core schemas with your own `z` typecheck\nwhen your `zod` resolves to the **same copy** `@modelcontextprotocol/core` uses (a\n`zod ^4.2.0` range that dedupes). When it cannot — a zod@3-pinned project nests core's\nown zod@4 — v1 idioms that combined two spec schemas with your `z` no longer compile:\ncore does not export its zod instance, and a foreign zod's `z.union(…)` / `.or(…)`\nrejects core's schema types. For accept-either result parsing, skip composition:\nrequest with the `ResultSchema` passthrough (the same one the\n[gateway note](protocol-ctx.md#request-ctxmcpreqsend-and-calltool-no-longer-require-a-schema-for-spec-methods)\nuses) and discriminate with sequential `safeParse`:\n\n```typescript\n// v1 — one composed schema\nconst result = await client.request(req, z.union([CompatibilityCallToolResultSchema, CreateTaskResultSchema]));\n\n// v2 — passthrough request, then sequential discrimination\nimport { CompatibilityCallToolResultSchema, CreateTaskResultSchema, ResultSchema } from '@modelcontextprotocol/core';\nconst raw = await client.request(req, ResultSchema);\nconst asTask = CreateTaskResultSchema.safeParse(raw);\nconst result = asTask.success ? asTask.data : CompatibilityCallToolResultSchema.parse(raw);\n```\n\nOrder the candidates from most to least specific, and `.parse()` the last one so a\nresult that matches no candidate still fails loudly.\n\nThe role-aggregate unions (`ClientRequest`, `ServerResult`, `ServerRequest`,\n`ClientResult`, `ClientNotification`, `ServerNotification`) and the typed-method maps\n(`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap`) no longer\ninclude task vocabulary; the deprecated `Task*` types remain importable on their own.\n(One published-alpha qualification, like the `-32002` note in [Errors](errors.md#errors): the\n`2.0.0-alpha.3` and earlier typings predate this — the typed maps there still carry the\n`tasks/*` entries, and `ResultTypeMap['tools/call']` still unions `CreateTaskResult`, so\na `client.request({ method: 'tools/call', … })` result does not assign to\n`Promise<CallToolResult>`. If pinned to those alphas, narrow with the\n`isCallToolResult` guard — the recommended discrimination tool anyway, per the next\nparagraph; `2.0.0-alpha.4` and later are unaffected.)\n\n**Discriminating result shapes: use guards, not the `in` operator.** The v2\nzod-inferred result types are passthrough objects — every union member carries an index\nsignature — so v1-idiomatic property discrimination such as\n`if ('content' in result) { … } else { result.toolResult }` no longer narrows: the `in`\ncheck is satisfiable by every member, and the else branch can collapse to `never`\n(surfacing as `TS2339` on the property you then read). Use the exported guards instead:\n`isCallToolResult(result)`, or `isSpecType.GetPromptResult(result)` and friends for any\nother spec type ([above](#zod-schema-constants-moved-to-modelcontextprotocolcore)). An\nadjacent trap when keeping a union for later narrowing: a `const` **annotation** is\ncontrol-flow-narrowed straight back to the initializer's type — after\n`const r: A | B = await fn()`, `r` has `fn`'s return type, not the union — so when you\nneed the wider union (e.g. a `CompatibilityCallToolResult` branch), apply an\n`as A | B` assertion instead of an annotation.\n\n#### Removed type aliases\n\n| Removed                                                         | Replacement                                                     |\n| --------------------------------------------------------------- | --------------------------------------------------------------- |\n| `JSONRPCError`                                                  | `JSONRPCErrorResponse`                                          |\n| `JSONRPCErrorSchema`                                            | `JSONRPCErrorResponseSchema`                                    |\n| `isJSONRPCError`                                                | `isJSONRPCErrorResponse`                                        |\n| `isJSONRPCResponse` (deprecated in v1)                          | `isJSONRPCResultResponse` ²                                     |\n| `JSONRPCResponseSchema` (result-only in v1)                     | `JSONRPCResultResponseSchema` ²                                 |\n| `JSONRPCResponse` (result-only in v1)                           | `JSONRPCResultResponse` ²                                       |\n| `ResourceReference` / `ResourceReferenceSchema`                 | `ResourceTemplateReference` / `ResourceTemplateReferenceSchema` |\n| `IsomorphicHeaders`                                             | Web Standard `Headers`                                          |\n| `RequestHandlerExtra`                                           | `ServerContext` / `ClientContext` / `BaseContext`               |\n| `ResourceTemplate` (the spec wire **type** from `sdk/types.js`) | `ResourceTemplateType` ³                                        |\n\n² v2 introduces **new** `isJSONRPCResponse` / `JSONRPCResponse` / `JSONRPCResponseSchema`\nwith corrected semantics — they match **both** result and error responses (the schema is\n`z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])`). v1's symbols only\nmatched results. To preserve v1 behavior, rename to `isJSONRPCResultResponse` /\n`JSONRPCResultResponse` / `JSONRPCResultResponseSchema` (the codemod does this).\n\n³ The `ResourceTemplate` URI-template helper **class** (from `sdk/server/mcp.js`) is\n**unchanged** — keep `new ResourceTemplate(...)` as-is. Only the like-named spec wire\ntype from `types.js` was renamed to `ResourceTemplateType` to resolve the v1 collision;\nthe codemod scopes the rename to imports from `sdk/types.js` only.\n\nAll other symbols from `@modelcontextprotocol/sdk/types.js` retain their original\nnames — import the TypeScript types, error classes, enums, and type guards from\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the Zod\n`*Schema` constants from `@modelcontextprotocol/core`.\n\nOne type-level narrowing to note: client/server capability `experimental` payloads are\nnow typed as JSON-compatible objects (nested JSON values) rather than arbitrary\nobjects. A payload typed `Record<string, unknown>` no longer assigns (`TS2322`) — give\nthe source a JSON-compatible type or cast at the boundary.\n\nThe `Protocol` base class and `mergeCapabilities` moved: import them from the\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server` package root instead\nof `shared/protocol.js`. Most code should not use `Protocol` directly — `Client` and\n`Server` are the supported surfaces, and for observing unmatched inbound requests\nprefer `client.fallbackRequestHandler` / `server.fallbackRequestHandler`. The codemod\nrewrites `Protocol` and `mergeCapabilities` imports to the package root, like the\nmodule's other symbols. One caveat: the client and server packages each bundle their\nown compiled copy of the class, so the two roots' `Protocol` exports are distinct\nclasses — import it from one package consistently within a process.\n\n#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)\n\nThe default validator dispatches on the schema's declared `$schema`: absent or 2020-12\nvalidates as **JSON Schema 2020-12** — on Node via `Ajv2020` instead of v1's draft-07\n`Ajv` (the Cloudflare Workers default was already 2020-12) — a declared 2019-09\n`$schema` validates with 2019-09 semantics (`Ajv2019`), and a declared draft-07 or\ndraft-06 `$schema` validates with draft-07 semantics. Schemas declaring any other\n`$schema` are rejected with `Error(\"…unsupported dialect…\")`. Two known draft-07 engine\ndifferences: the Node engine (classic Ajv, same as v1's default) evaluates keywords\nadjacent to `$ref`, stricter than draft-07's ignore-siblings rule, while the\nbrowser/Workers engine ignores them per spec; and the browser/Workers engine does not\nresolve a `$ref` inside a `dependencies` entry whose key collides with a JSON Schema\nkeyword (`type`, `default`, `format`, …) — validation throws `Unresolved $ref` when\nthat dependency triggers (surfaced as the SDK's typed validation error), while the\nNode engine handles the same schema correctly.\n\n`CallToolResult.structuredContent` is widened from `{ [k: string]: unknown }` to\n`unknown` (SEP-2106 lifts the `type:\"object\"` root restriction). The presence check is\n`!== undefined`, not falsy (`null` / `0` / `false` / `\"\"` are legal values now). External\n`$ref` is not dereferenced (unchanged from v1; Ajv throws `MissingRefError` at compile,\nsurfaced per-tool on `callTool`).\n\n| v1 pattern                                                                                                   | Mechanical fix                                                                                                                                                                                         |\n| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `result.structuredContent.<key>` / `result.structuredContent?.<k>`                                           | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }`                                                                              |\n| `if (!result.structuredContent)`                                                                             | `if (result.structuredContent === undefined)`                                                                                                                                                          |\n| draft-07 idioms **without** a declared `$schema` (a declared draft-07/06 `$schema` dispatches automatically) | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |\n| undeclared draft-07 idioms via `fromJsonSchema(schema)`                                                      | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas                                 |\n| `outputSchema` / `inputSchema` with absolute-URI `$ref`                                                      | inline under `$defs` and reference with `#/$defs/Name`                                                                                                                                                 |\n\nA tool may now register an `outputSchema` whose root is `type:\"array\"`, `type:\"string\"`,\netc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward\nevery era a non-object `structuredContent` with no `text` block of its own gets a\n`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md › Per-era wire codecs](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#per-era-wire-codecs) for how the codec applies these per era.\n\n**Your advertised tool schemas change shape on the wire.** The same `registerTool`\ncalls produce `tools/list` entries whose generated `inputSchema` differs from v1:\nJSON Schema 2020-12 idioms (zod 4 conversion), different `additionalProperties`\nhandling (no `additionalProperties: false` by default; passthrough objects emit\n`\"additionalProperties\": {}` instead of `true`), and no `execution.taskSupport` member.\nGolden tests, transcript pins, and strict client-side validators of your advertised\ntool list need re-baselining — the new shapes are spec-conformant.\n","isInternal":false,"tokens":3275,"sizeBytes":14826},{"name":"unchanged-apis.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/references/unchanged-apis.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/references/unchanged-apis.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"## Unchanged APIs\n\nThe following are unchanged between v1 and v2 apart from the import path — except\nwhere an entry notes its own signature change:\n\n- `Client` constructor and `connect`, `close`, and the typed verbs (`listTools`,\n  `listPrompts`, `listResources`, `readResource`, …) — note `callTool()` and `request()`\n  signatures changed (schema parameter dropped for spec methods).\n- `McpServer` constructor, `server.connect(transport)`, `server.close()`, and the\n  `McpServer.server` accessor — still the supported way to call the low-level\n  `Server`'s push verbs (`createMessage` / `listRoots` / `sendLoggingMessage` — ⚠\n  `@deprecated`, see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) outside a\n  handler context.\n- The server Streamable HTTP transports' **constructor options** (`sessionIdGenerator`,\n  `onsessioninitialized`, `onsessionclosed`, `enableJsonResponse`, `eventStore`,\n  `retryInterval`) and the `handleRequest` surface — only the class name and import\n  moved: `StreamableHTTPServerTransport` is now `NodeStreamableHTTPServerTransport`\n  from `@modelcontextprotocol/node`, a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`,\n  which exposes the same options ([decision rule](imports-transports.md#imports--transports)). The\n  transport-level `closeSSEStream(requestId)` / `closeStandaloneSSEStream()` methods\n  keep their v1 names too — only the handler-context accessors moved to `ctx.http`\n  ([remap table](protocol-ctx.md#low-level-protocol--handler-context-ctx)).\n- `UriTemplate` (v1: `@modelcontextprotocol/sdk/shared/uriTemplate.js`) — `expand` /\n  `match` semantics carry over; import it from `@modelcontextprotocol/server` or\n  `@modelcontextprotocol/client` (top-level export; the codemod rewrites the path).\n- `StreamableHTTPClientTransport`, `SSEClientTransport` constructors and options —\n  including resumability: the per-request `resumptionToken` / `onresumptiontoken`\n  request options carry over from v1 unchanged\n  ([Resume a dropped stream](https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.md#resume-a-dropped-stream)).\n- `StdioClientTransport` and `StdioServerTransport` — **import path moved** to the\n  `./stdio` subpath and gained an optional `maxBufferSize` ([Imports & transports](imports-transports.md#imports--transports)).\n- The **`Transport` interface contract** — `start` / `send` / `close`, `onmessage` /\n  `onclose` / `onerror`, optional `sessionId` and `setProtocolVersion`,\n  `TransportSendOptions`, `MessageExtraInfo`. Hand-rolled v1 transports (recording\n  wrappers, test doubles, decorators) compile and run against v2 with only the import\n  path updated. v2 adds **optional** members only — `hasPerRequestStream` and\n  `setSupportedProtocolVersions` on the interface, `requestSignal` / `headers` /\n  `onRequestStreamEnd` on `TransportSendOptions` — which matter only for 2026-era\n  per-request-stream cancellation and `Mcp-Param-*` header attachment\n  ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)).\n- All TypeScript **type** definitions from `types.ts` (except the aliases listed under\n  [Removed type aliases](types-schemas.md#removed-type-aliases) and the `experimental` capability\n  payload narrowing — see [Types & schemas](types-schemas.md#types--schemas)).\n- Tool, prompt, and resource callback return types.\n\n> The `Server` (low-level) constructor and **most** of its methods are unchanged, but\n> `setRequestHandler` / `setNotificationHandler` and `request()` signatures changed\n> ([Low-level protocol](protocol-ctx.md#low-level-protocol--handler-context-ctx)). In particular,\n> `Server.createElicitationCompletionNotifier()` is unchanged — including its\n> construction-time client-capability check — for 2025-era URL-mode elicitation\n> ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The Zod `*Schema`\n> constants are **not** part of the unchanged surface — they moved to\n> `@modelcontextprotocol/core` ([Types & schemas](types-schemas.md#types--schemas)).\n","isInternal":false,"tokens":1041,"sizeBytes":4163},{"name":"SKILL.md","path":"skills/mcp-typescript-sdk-upgrade-to-v2/SKILL.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/skills/mcp-typescript-sdk-upgrade-to-v2/SKILL.md","title":"mcp-typescript-sdk-upgrade-to-v2","category":"anthropic-skill","format":"markdown","content":"---\nname: mcp-typescript-sdk-upgrade-to-v2\ndescription: 'Upgrades TypeScript code from v1 of the MCP TypeScript SDK (`@modelcontextprotocol/sdk`) to v2 (`@modelcontextprotocol/client`, `/server`, `/core` and the middleware packages): runs the official codemod, then works through what it cannot rewrite. Use whenever a project depends on `@modelcontextprotocol/sdk` and the user wants to migrate, upgrade, port or bump it to v2, or is fixing v1-to-v2 breakage such as missing `McpError`, `RequestHandlerExtra` or `*Schema` exports, `@mcp-codemod-error` markers, or changed auth, error, transport or handler behavior, even if they do not say \"migration\".'\n---\n\n<!-- #region intro -->\n\n# Upgrading from v1.x to v2\n\nThis guide covers upgrading from `@modelcontextprotocol/sdk` (v1.x) to the v2 packages.\nIt is written for shell-capable agents and humans alike: run the codemod first, then\nwork through the manual sections for what the codemod can't rewrite.\n\nIf you are already on v2 and want to adopt the **2026-07-28 protocol revision**, see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md) instead.\n\n## TL;DR — quick path\n\n1. **Prerequisites.** Node.js 20+. v2 is ESM-first but ships a CommonJS build too, so\n   both `import` and `require('@modelcontextprotocol/…')` resolve natively.\n2. **Run the codemod.**\n    ```bash\n    npx @modelcontextprotocol/codemod@latest v1-to-v2 .\n    ```\n    Run it at the **package root** (`.`), not `./src` — it also rewrites `package.json`,\n    and real projects import the SDK from `test/`, `scripts/`, and fixtures too.\n3. **Grep for markers.** Anything the codemod recognized but could not safely rewrite is\n   marked in place:\n    ```bash\n    grep -rn '@mcp-codemod-error' .\n    ```\n4. **Type-check.** `tsc --noEmit` (or your build). Remaining errors map to the\n   [manual sections](#manual-changes-what-the-codemod-does-not-handle) below.\n5. **Format.** The codemod rewrites the AST without reformatting — run your formatter on\n   the changed files (`prettier --write` / `eslint --fix` / `biome format --write`); the\n   codemod prints the exact command after it runs.\n6. **Run your tests.**\n\nMigrating a large codebase gradually instead of in one pass? See\n[Migrating in stages (large codebases)](references/packaging-runtime.md#migrating-in-stages-large-codebases).\n\n<!-- #endregion intro -->\n\n<!-- #region codemod -->\n\n## What the codemod handles\n\nThe codemod ([`@modelcontextprotocol/codemod`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md))\nmechanically applies every rename whose mapping is fixed. The mappings are the\n**source of truth** — they live in the codemod package and are not reproduced here:\n\n| Mapping                                                                                   | Source file                                                                                                                                                                  |\n| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk/...` import paths → v2 packages                                | [`mappings/importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)                   |\n| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts)                   |\n| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`                  | [`mappings/schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts)   |\n| `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` property remap                                 | [`mappings/contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) |\n\nIn addition the codemod:\n\n- Updates `package.json` dependencies (`@modelcontextprotocol/sdk` → the v2 packages\n  your imports actually use).\n- Rewrites `.tool()` / `.prompt()` / `.resource()` to `registerTool` / `registerPrompt`\n  / `registerResource` and wraps `inputSchema` / `outputSchema` / `argsSchema` /\n  `uriSchema` raw Zod shapes with `z.object()`, adding `import { z } from 'zod'`\n  when the file has no `z` binding.\n- Drops the result-schema argument from `client.request()` / `client.callTool()` for\n  spec methods.\n- Routes the spec Zod `*Schema` constants imported from `sdk/types.js` to\n  `@modelcontextprotocol/core` (mixed imports are split; `.parse()` / `.safeParse()`\n  calls are left untouched). Task-handler schema constants\n  (`GetTaskRequestSchema` etc.) used as `setRequestHandler` args are **not** rewritten\n  — the experimental tasks feature was removed (SEP-2663), so each such registration\n  is marked with an action-required diagnostic instead (see\n  [Experimental tasks interception removed](references/behavioral-changes.md#experimental-tasks-interception-removed)).\n- Renames `ErrorCode` → `ProtocolErrorCode` and routes the local-only members\n  (`RequestTimeout`, `ConnectionClosed`) to `SdkErrorCode` — rewriting an all-SDK\n  condition's `instanceof ProtocolError` guard to `SdkError`, and marking guards\n  that mix the two enums.\n- Renames every `StreamableHTTPError` reference to `SdkHttpError` and adds the import\n  (constructor calls are marked for review — argument shape changed).\n- Replaces `IsomorphicHeaders` with the Web Standard `Headers` type and drops the\n  import (a warning notes `Headers` uses `.get()`/`.set()`, not bracket access).\n- Rewrites `SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`.\n- Renames `RequestHandlerExtra` → `ServerContext` / `ClientContext` and the `extra`\n  parameter to `ctx`.\n- Rewrites `vi.mock` / `jest.mock` and dynamic `import()` paths.\n- Renames the `ResourceTemplate` **type** imported from `@modelcontextprotocol/sdk/types.js`\n  to `ResourceTemplateType` (the spec wire type). The `ResourceTemplate` URI-template\n  helper **class** from `server/mcp.js` keeps its name and is not renamed.\n- Drops `@modelcontextprotocol/sdk/server/zod-compat.js` imports.\n- Inverts optional completable nesting — `completable(schema.optional(), cb)` becomes\n  `completable(schema, cb).optional()` (see\n  [Standard Schema objects](references/server-registration.md#standard-schema-objects-raw-shapes-deprecated)); shapes it\n  cannot invert get an `@mcp-codemod-error` marker.\n- Rewrites `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\n  client or server package root, like the module's other symbols.\n\n## What the codemod does NOT handle\n\nEach of these maps to a manual section below. The codemod marks every site it\nrecognized but could not safely rewrite with an `@mcp-codemod-error` comment.\n\n- **Node 20 / ESM** — pre-flight, not a code rewrite. → [Packaging & runtime](references/packaging-runtime.md#packaging--runtime)\n- **Header-read `.get()` rewrite** — `IsomorphicHeaders` is renamed to `Headers`\n  and `extra.requestInfo?.headers[…]` is remapped to `ctx.http?.req?.headers[…]`, but\n  converting that bracket access to `.get()` is manual. (Headers you _pass in_ via\n  `requestInit.headers` need no rewrite — plain objects remain valid.)\n  → [HTTP & headers](references/http-headers.md#http--headers)\n- **`ctx.mcpReq.send()` schema-arg drop** — the codemod drops the schema arg from\n  `client.request()` / `client.callTool()` but leaves nested `ctx.mcpReq.send()` calls\n  alone. → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **OAuth error-class consolidation** — `instanceof InvalidGrantError` → `OAuthError` +\n  `OAuthErrorCode` is a judgment rewrite. → [Auth](references/auth.md#auth)\n- **`SdkErrorCode` branch selection** — the codemod renames `StreamableHTTPError` →\n  `SdkHttpError`; deciding which `SdkErrorCode` branch a given catch should match is\n  judgment. → [Errors](references/errors.md#errors)\n- **Namespace schema access** — `import * as t from '…/types.js'` +\n  `t.CallToolResultSchema.parse(…)` can't be split per-symbol; the codemod flags it\n  action-required — re-import the schema from `@modelcontextprotocol/core` by hand.\n  → [Types & schemas](references/types-schemas.md#types--schemas)\n- **Import-less (injected) SDK surfaces** — the codemod is import-driven: a file that\n  receives the SDK surface as a parameter (dependency injection, factory seams) and has\n  no SDK import is never rewritten, and the v1 idioms there fail at **runtime**, not\n  compile time — e.g. the v1 schema-first `setRequestHandler(Schema, …)` form throws a\n  `TypeError` at registration. Grep such seams for v1 API tokens beyond import\n  statements (`setRequestHandler(`, `ErrorCode.`, `extra.`) and apply the\n  [handler-registration](references/protocol-ctx.md#setrequesthandler--setnotificationhandler-use-method-strings)\n  and [Errors](references/errors.md#errors) sections by hand.\n  → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **Behavioral adaptation** — list auto-aggregation, capability empties, lazy validator\n  compilation, output-schema validation rules. → [Behavioral changes](references/behavioral-changes.md#behavioral-changes)\n\n---\n\n## Manual changes (what the codemod does not handle)\n\n<!-- #endregion codemod -->\n\nEach subsystem has its own reference file. Open the one that matches the type error, runtime failure or `@mcp-codemod-error` marker in front of you; most migrations need only a few of them.\n\n| Reference file                                                             | Read it when                                                                                                                                                                                   |\n| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Packaging & runtime](references/packaging-runtime.md)                     | Choosing which v2 packages to install, or dealing with Node/ESM/CommonJS setup, monorepo workspaces, Jest, nested `zod` copies, migrating in stages, or a library that peer-depends on the SDK |\n| [Imports & transports](references/imports-transports.md)                   | A transport import needs a runtime decision (Node vs web-standard Streamable HTTP) or a stdio import moved to a `./stdio` subpath                                                              |\n| [Low-level protocol & handler context (`ctx`)](references/protocol-ctx.md) | Code uses `extra` / `ctx`, `setRequestHandler` / `setNotificationHandler`, `request()`, `ctx.mcpReq.send()` or `callTool()`                                                                    |\n| [Server registration API](references/server-registration.md)               | Code registers tools, prompts or resources, or passes raw Zod shapes as schemas                                                                                                                |\n| [HTTP & headers](references/http-headers.md)                               | Code reads HTTP request headers                                                                                                                                                                |\n| [Errors](references/errors.md)                                             | Code throws or catches `McpError` / `ProtocolError`, `ErrorCode`, `SdkError` or `SdkHttpError`                                                                                                 |\n| [Auth](references/auth.md)                                                 | Code uses OAuth (`auth()`, `OAuthClientProvider`, OAuth error classes) or bearer-token auth                                                                                                    |\n| [Types & schemas](references/types-schemas.md)                             | Code imports Zod `*Schema` constants or removed type aliases, or depends on JSON Schema details                                                                                                |\n| [Behavioral changes](references/behavioral-changes.md)                     | The code compiles but tests or runtime behavior differ                                                                                                                                         |\n| [Enhancements](references/enhancements.md)                                 | Optional: v2 features worth adopting once the upgrade is done                                                                                                                                  |\n| [Unchanged APIs](references/unchanged-apis.md)                             | Checking whether an API changed at all before rewriting it                                                                                                                                     |\n\n<!-- #region need-help -->\n\n## Need help?\n\n- The codemod's [`@mcp-codemod-error`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md) markers point\n  at every site it could not safely rewrite.\n- The [Troubleshooting](https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting.md) page covers common errors and their fixes.\n- Runnable [examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)\n  for every subsystem.\n- Open an issue on [GitHub](https://github.com/modelcontextprotocol/typescript-sdk/issues).\n\n<!-- #endregion need-help -->\n","frontmatter":{"name":"mcp-typescript-sdk-upgrade-to-v2","description":"Upgrades TypeScript code from v1 of the MCP TypeScript SDK (`@modelcontextprotocol/sdk`) to v2 (`@modelcontextprotocol/client`, `/server`, `/core` and the middleware packages): runs the official codemod, then works through what it cannot rewrite. Use whenever a project depends on `@modelcontextprotocol/sdk` and the user wants to migrate, upgrade, port or bump it to v2, or is fixing v1-to-v2 breakage such as missing `McpError`, `RequestHandlerExtra` or `*Schema` exports, `@mcp-codemod-error` markers, or changed auth, error, transport or handler behavior, even if they do not say \"migration\"."},"isInternal":false,"tokens":3001,"sizeBytes":14218},{"name":"README.md","path":"examples/scoped-tools/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/scoped-tools/README.md","title":"scoped-tools Documentation","category":"plugin-manifest","format":"markdown","content":"# scoped-tools — per-tool scope enforced in the tool handler\n\nDemonstrates per-tool OAuth scope enforcement on a `createMcpHandler`\ndeployment: the HTTP gate does **bearer-verify + 401 only**, and each tool\nhandler checks `ctx.http?.authInfo?.scopes` for the scope it needs. The scope\ndecision lives next to the code it guards — the handler is the only place that\nauthoritatively knows which tool is executing — instead of in middleware that\nwould have to re-derive the operation from the request body.\n\n`server.ts` runs a minimal demo Authorization Server alongside the MCP Resource\nServer. `client.ts` connects with a `files:read` token, calls `list-files`\n(works), then calls `write-file` → the handler returns `{ isError: true }` with\n`insufficient_scope: requires files:write`.\n\nThe transport's automatic `403 insufficient_scope` **step-up** flow (SEP-2350 —\nscope union, refresh-bypass, `maxStepUpRetries`) applies when the RS responds\n`403` at the HTTP layer; that path is exercised by\n`test/e2e/scenarios/client-auth.test.ts`.\n\n```bash\npnpm --filter @mcp-examples/scoped-tools server -- --http --port 3000\npnpm --filter @mcp-examples/scoped-tools client -- --http http://127.0.0.1:3000/mcp\n```\n\n> DEMO ONLY — the bundled AS auto-approves and grants whatever scope is asked\n> for. Do not deploy.\n","isInternal":false,"tokens":331,"sizeBytes":1315},{"name":"README.md","path":"examples/tools/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/examples/tools/README.md","title":"tools Documentation","category":"plugin-manifest","format":"markdown","content":"# tools\n\n**Start here.** Register tools with `McpServer.registerTool`; the SDK infers the JSON Schema from any Standard-Schema-compatible input (Zod here) and emits `structuredContent` matching `outputSchema`. The client lists tools, inspects schemas and `annotations`, calls them, and\nasserts structured output.\n\n```bash\npnpm tsx examples/tools/client.ts\n```\n","isInternal":false,"tokens":84,"sizeBytes":360},{"name":"README.md","path":"packages/client/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/client/README.md","title":"client Documentation","category":"plugin-manifest","format":"markdown","content":"# `@modelcontextprotocol/client`\n\nThe MCP (Model Context Protocol) TypeScript client SDK. Build MCP clients that connect to MCP servers.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/client\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Client guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/connect)** — connecting, calling tools, OAuth, and middleware\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n","isInternal":false,"tokens":324,"sizeBytes":1326},{"name":"README.md","path":"packages/codemod/batch-test/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/codemod/batch-test/README.md","title":"batch-test Documentation","category":"plugin-manifest","format":"markdown","content":"# Codemod Batch Test\n\nTests the v1-to-v2 codemod against real-world repos to find bugs, missing transforms, and gaps.\n\n## How it works\n\nFor each repo in `repos.json`, the batch test:\n\n1. Clones the repo (or resets an existing clone)\n2. Installs dependencies\n3. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy\n4. Runs the codemod — in-process via the programmatic API (`--codemod=local`, the default), or by shelling out to the published CLI via `npx` (`--codemod=published`)\n5. Packs local SDK packages as tarballs and rewrites `package.json` deps to use them, so the test runs against the current SDK branch — only when `--sdk=local` (the default); with `--sdk=published` the v2 deps are installed from npm instead (see [Modes](#modes))\n6. Re-installs dependencies\n7. Re-runs the same checks\n8. Writes structured JSON reports\n\nErrors that appear in step 7 but not step 3 are codemod-introduced regressions.\n\n## Usage\n\n```bash\n# Build all SDK packages first (tarballs need built dist/)\npnpm build:all\n\n# Run the batch test\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# Clean cloned repos, results, and tarballs\npnpm --filter @modelcontextprotocol/codemod batch-test:clean\n```\n\n## Modes\n\nEach run independently chooses where the **SDK packages** and the **codemod** come from:\n\n| Flag                       | Values                 | Default   | Effect                                                                                                              |\n| -------------------------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |\n| `--sdk`                    | `local` \\| `published` | `local`   | `local`: pack local packages and rewrite deps to `file:` tarballs. `published`: install the v2 deps from npm.       |\n| `--codemod`                | `local` \\| `published` | `local`   | `local`: run the working-copy codemod in-process. `published`: `npx` the published CLI.                             |\n| `--codemod-version <spec>` | npm version/tag/range  | `latest`  | Only with `--codemod=published`.                                                                                    |\n| `--sdk-version <spec>`     | npm version/tag/range  | _(unset)_ | Only with `--sdk=published`. Pins each v2 dep to its own resolved version. Unset = use whatever the codemod writes. |\n\nBoth `--flag value` and `--flag=value` forms work.\n\n```bash\n# default (today's behavior)\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# both from npm @latest\npnpm --filter @modelcontextprotocol/codemod batch-test -- --sdk=published --codemod=published\n```\n\nPublished specs are resolved to concrete versions via `npm view` at startup. A failure resolving the codemod version or the representative SDK label aborts the run; a per-package `--sdk-version` miss only warns and leaves that package on the codemod-written range. Each\n`@modelcontextprotocol/*` package is resolved **independently** — they are not released in lockstep. Results are written to a per-run directory keyed on the resolved versions; the SDK segment is the resolved `@modelcontextprotocol/server` version used as a representative label,\nwhile the full per-package set is recorded in `summary.json` → `sdkVersions`:\n\n```\nresults/codemod-local__sdk-local/                  # default (--sdk=local --codemod=local)\nresults/codemod-2.0.0-alpha.2__sdk-2.0.0-alpha.2/  # --codemod=published --sdk=published --sdk-version=2.0.0-alpha.2\nresults/codemod-2.0.0-alpha.2__sdk-from-codemod/   # --codemod=published --sdk=published  (no --sdk-version)\n```\n\nThe segment `sdk-from-codemod` appears when `--sdk=published` with no `--sdk-version` and `--codemod=published` (the SDK version is baked into the published codemod and only known after install; the installed versions are recorded in `summary.json` → `sdkVersions`).\n\n**Limitation:** in published-codemod mode the CLI emits text, not structured diagnostics, so `codemod.diagnostics` is empty and the raw CLI output is captured under `codemod.cli` instead. Diagnostics categorization applies only to local-codemod runs.\n\n## Output\n\nResults are written to a per-run directory keyed on the resolved versions, `batch-test/results/<config>/`, where `<config>` is the `codemod-…__sdk-…` leaf (distinct `--codemod-version`/`--sdk-version` values produce distinct directories even within the same `--sdk`/`--codemod`\nmode; see [Modes](#modes)):\n\n- `results/<config>/summary.json` — overview across all repos: which passed, which failed, error counts, plus the run `config` and the resolved per-package `sdkVersions`\n- `results/<config>/<repo-slug>/report.json` — per-repo detail: baseline vs post-codemod check results, codemod diagnostics, change counts\n\n## Repo manifest (`repos.json`)\n\nAn array of repo entries. Each entry specifies a GitHub repo and one or more packages within it.\n\n```json\n{\n    \"repo\": \"owner/repo-name\",\n    \"ref\": \"main\",\n    \"packages\": [\n        {\n            \"dir\": \"packages/mcp-server\",\n            \"sourceDir\": \"src\",\n            \"checks\": {\n                \"typecheck\": \"npx tsc --noEmit\",\n                \"build\": \"npm run build\",\n                \"test\": \"npm run test\",\n                \"lint\": null\n            }\n        }\n    ]\n}\n```\n\n| Field                  | Required | Default                                | Description                                                       |\n| ---------------------- | -------- | -------------------------------------- | ----------------------------------------------------------------- |\n| `repo`                 | yes      | —                                      | GitHub `owner/name`                                               |\n| `ref`                  | no       | `main`                                 | Branch or tag to clone                                            |\n| `packages`             | no       | `[{ \"dir\": \".\", \"sourceDir\": \"src\" }]` | Package targets within the repo                                   |\n| `packages[].dir`       | yes      | —                                      | Path to package root (where `package.json` lives)                 |\n| `packages[].sourceDir` | no       | `src`                                  | Source directory relative to `dir` (passed to codemod)            |\n| `packages[].checks`    | no       | auto-detect                            | Override check commands; set a value to `null` to skip that check |\n\nWhen `checks` is omitted, the runner auto-detects commands from the package's `package.json` scripts (probing names like `typecheck`, `build`, `test`, `lint`). The package manager is auto-detected from the lockfile at the repo root.\n\n## Analyzing results\n\n`analyze-prompt.md` contains instructions for Claude Code to run the batch test and produce a categorized analysis. Each error is classified as:\n\n| Category            | Meaning                                            |\n| ------------------- | -------------------------------------------------- |\n| `codemod-bug`       | A transform produced incorrect output              |\n| `missing-transform` | The codemod should handle this pattern but doesn't |\n| `manual-migration`  | Expected — requires human judgment                 |\n| `repo-specific`     | Unusual pattern not worth handling in the codemod  |\n\n## Adding a repo\n\n1. Edit `repos.json` and add an entry\n2. Run `pnpm --filter @modelcontextprotocol/codemod batch-test`\n3. Check `results/<config>/<repo-slug>/report.json` for new findings\n\nFor monorepos, list each package that uses `@modelcontextprotocol/sdk` as a separate entry in `packages`.\n\n## Iteration workflow\n\n```\n1. Run the batch test\n2. Review results — identify codemod bugs / missing transforms\n3. Fix the codemod transforms\n4. Run batch-test:clean, then re-run the batch test\n5. Confirm the fixes resolved the issues\n6. Repeat\n```\n","isInternal":false,"tokens":1801,"sizeBytes":7939},{"name":"README.md","path":"packages/codemod/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/codemod/README.md","title":"codemod Documentation","category":"plugin-manifest","format":"markdown","content":"# @modelcontextprotocol/codemod\n\nCodemods for migrating MCP TypeScript SDK code between major versions.\n\n## Usage\n\n```bash\nnpx @modelcontextprotocol/codemod@latest v1-to-v2 .\n\n# or a single source file (manifest changes are reported, not applied)\nnpx @modelcontextprotocol/codemod@latest v1-to-v2 src/server.ts\n```\n\nThe codemod rewrites TypeScript and JavaScript source files\n(`.ts`/`.tsx`/`.mts`/`.cts`/`.js`/`.jsx`/`.mjs`/`.cjs`) in place. Run it on a clean\nworking tree so you can review the diff.\n\n## What `v1-to-v2` covers\n\nThe mechanical rename mappings are the source of truth — see\n`src/migrations/v1-to-v2/mappings/`:\n\n- [`importMap.ts`](./src/migrations/v1-to-v2/mappings/importMap.ts) —\n  `@modelcontextprotocol/sdk/...` import paths → v2 packages\n- [`symbolMap.ts`](./src/migrations/v1-to-v2/mappings/symbolMap.ts) —\n  symbol renames (`McpError` → `ProtocolError`, …)\n- [`schemaToMethodMap.ts`](./src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts) —\n  `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`\n- [`contextPropertyMap.ts`](./src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) —\n  `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*`\n\nTransforms in `src/migrations/v1-to-v2/transforms/` also rewrite `.tool()` →\n`registerTool` (wrapping `inputSchema` / `outputSchema` / `argsSchema` / `uriSchema`\nraw shapes with `z.object()`), drop the result-schema argument from `client.request()`\n/ `client.callTool()` for spec methods, route spec `*Schema` imports to\n`@modelcontextprotocol/core`, rename\n`StreamableHTTPError` → `SdkHttpError` / `IsomorphicHeaders` → `Headers`, rewrite\n`SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`, route\n`ErrorCode.{RequestTimeout,ConnectionClosed}` to `SdkErrorCode` (rewriting an\nall-SDK condition's `instanceof ProtocolError` guard to `SdkError`, and marking\nguards that mix the two enums), add `import { z } from 'zod'` when a wrap needs\nit, rewrite `vi.mock`\n/ `jest.mock` / dynamic `import()` paths, invert optional completable nesting\n(`completable(schema.optional(), cb)` becomes `completable(schema, cb).optional()`),\nand rewrite `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\nclient or server package root.\n\n## `@mcp-codemod-error` markers\n\nWhen the codemod recognizes a v1 pattern but cannot safely rewrite it (ambiguous\ncontext, removed API with no mechanical replacement, signature change requiring\njudgment), it leaves the code unchanged and inserts a comment:\n\n```typescript\n/* @mcp-codemod-error WebSocketClientTransport removed in v2. Use StreamableHTTPClientTransport or StdioClientTransport. */\n```\n\nAfter running the codemod, find every site that needs attention:\n\n```bash\ngrep -rn '@mcp-codemod-error' .\n```\n\n## What it does NOT cover\n\nCJS→ESM / Node 20 pre-flight, header **read** rewrites (`ctx.http?.req?.headers`\nbracket access → `.get()`; sending plain-record headers keeps working), OAuth\nerror-class consolidation (`instanceof InvalidGrantError` → `OAuthError` +\n`OAuthErrorCode`), per-scenario `SdkErrorCode` branch selection, `ctx.mcpReq.send()`\nschema-arg drop, and behavioral adaptation are manual — see the\n[migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2) for what to do after the\ncodemod runs.\n\nThe codemod handles the v1→v2 SDK surface upgrade only. Adopting the 2026-07-28\nprotocol revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`)\nis architectural and not codemod-automatable — see\n[docs/migration/support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28).\n","isInternal":false,"tokens":984,"sizeBytes":3643},{"name":"README.md","path":"packages/core-internal/src/types/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/core-internal/src/types/README.md","title":"types Documentation","category":"plugin-manifest","format":"markdown","content":"# Spec reference types (\"anchors\")\n\nThe `spec.types.<revision>.ts` files in this directory are vendored, verbatim copies of the MCP specification's normative `schema.ts`, one file per protocol revision. Each file is generated by `pnpm run fetch:spec-types [version] [sha]` (`scripts/fetch-spec-types.ts`): the\nupstream schema is fetched at a specific spec commit, a provenance header recording that commit is prepended, and the result is formatted with the project's prettier config — no other transformation.\n\nThey are reference-only test oracles: the comparison suites in `packages/core-internal/test/spec.types.<revision>.test.ts` check the SDK's own types against them. They are not exported from any barrel and must never be imported by runtime code.\n\n## Lifecycle policy\n\n1. **Released revisions are frozen.** Once a protocol revision is published under `schema/<revision>/` in the spec repository, its anchor regenerates only from the pinned spec commit recorded in `RELEASED_REVISION_PINS` (`scripts/fetch-spec-types.ts`) — never from the latest\n   upstream commit. Moving that pin, including the freeze of a newly published revision (when its generation source switches from `schema/draft/` to `schema/<revision>/`), must land atomically with every checked-in oracle derived from that revision.\n\n2. **Draft anchors float only while they have an explicit consumer.** An unreleased revision may track the spec repository's `schema/draft/schema.ts`, but refreshes must go through reviewed PRs and update all derived oracles atomically. No currently supported revision uses this mode.\n\n3. **The bot proposes; it never auto-merges.** Automated refreshes always go through a pull request that a maintainer reviews and merges. No automation pushes anchor changes directly to `main` or merges its own PRs. A refresh PR that breaks the comparison suites is the desired\n   signal — it is fixed in that PR, not bypassed.\n\n4. **Generated twins update atomically with their anchor.** If artifacts derived from an anchor (for example vendored JSON schemas or generated validators) are checked into this repository, any refresh that changes the anchor must regenerate those artifacts in the same commit.\n   The anchor and its derived twins must never be out of sync at any commit on `main`.\n\n    **This clause is OPERATIVE.** The vendored twins are the per-revision `schema.json` copies under `packages/core-internal/test/corpus/schema-twins/` (`<revision>.schema.json` + `manifest.json` recording each twin's source commit and content hashes). They are TEST-ONLY oracles consumed by the\n    schema-twin conformance lock (`test/wire/schemaTwinConformance.test.ts`) — never bundled, never imported by runtime code, and the JSON Schema engines stay optional peer dependencies. A refresh of `spec.types.<revision>.ts` must refresh the matching upstream\n    `schema/<dir>/schema.json` from that twin's recorded source commit and update `manifest.json` in the same change; the spec example corpus manifest (`test/corpus/fixtures/<revision>/manifest.json`) records its own source commit and follows the same atomicity rule when the examples\n    are re-vendored. The conformance lock failing after an anchor-only refresh is the desired loud signal of a missed twin update.\n","isInternal":false,"tokens":701,"sizeBytes":3268},{"name":"README.md","path":"packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"# @modelcontextprotocol/core\n\nCanonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification and OAuth/OpenID **Zod schemas**.\n\nThese are the exact schema constants the SDK validates protocol and OAuth/OpenID payloads against internally. The `@modelcontextprotocol/server` and `@modelcontextprotocol/client` packages keep a Zod-free public surface, so this package exists as the supported place to import the\nraw schemas when you need to validate or parse MCP messages yourself.\n\n## Install\n\n```sh\nnpm install @modelcontextprotocol/core\n```\n\n## Usage\n\n```ts\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\n\n// Throws on invalid input; returns the typed result on success.\nconst result = CallToolResultSchema.parse(payload);\n\n// Or non-throwing:\nconst parsed = CallToolResultSchema.safeParse(payload);\nif (parsed.success) {\n    // parsed.data is a fully typed CallToolResult\n}\n```\n\n## Scope\n\nThis package exports **only** Zod schema constants (`*Schema`), in two groups:\n\n- the MCP **spec** schemas — `CallToolResultSchema`, `ListToolsResultSchema`, …; and\n- the **OAuth/OpenID** auth schemas — `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`, … (the schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js`).\n\nThe corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and\n[`@modelcontextprotocol/client`](https://www.npmjs.com/package/@modelcontextprotocol/client).\n\n> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js` (spec schemas) and `@modelcontextprotocol/sdk/shared/auth.js` (OAuth/OpenID schemas). Point those `*Schema` imports at `@modelcontextprotocol/core` and your existing `.parse()` /\n> `.safeParse()` calls keep working unchanged.\n","isInternal":false,"tokens":439,"sizeBytes":1940},{"name":"README.md","path":"packages/middleware/express/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/middleware/express/README.md","title":"express Documentation","category":"plugin-manifest","format":"markdown","content":"# `@modelcontextprotocol/express`\n\nExpress adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Express integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create an Express app with sensible defaults for MCP servers\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n- protect routes with `requireBearerAuth` (validates `Authorization: Bearer …` via your `OAuthTokenVerifier`)\n- serve OAuth Protected Resource Metadata (RFC 9728) via `mcpAuthMetadataRouter`\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/express express\n\n# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):\nnpm install @modelcontextprotocol/node\n```\n\n## Exports\n\n- `createMcpExpressApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n- `requireBearerAuth(options)`\n- `mcpAuthMetadataRouter(options)`\n- `getOAuthProtectedResourceMetadataUrl(serverUrl)`\n- `OAuthTokenVerifier` (interface)\n\n## Usage\n\n### Create an Express app (localhost DNS rebinding protection by default)\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\n\nconst app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled\n```\n\n### Streamable HTTP endpoint (Express)\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpExpressApp();\n\napp.post('/mcp', async (req, res) => {\n    // Stateless example: create a server and a transport per request (with sessions: one pair per session).\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    await transport.handleRequest(req, res, req.body);\n});\n```\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { hostHeaderValidation } from '@modelcontextprotocol/express';\n\napp.use(hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));\n```\n","isInternal":false,"tokens":531,"sizeBytes":2325},{"name":"README.md","path":"packages/middleware/fastify/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/middleware/fastify/README.md","title":"fastify Documentation","category":"plugin-manifest","format":"markdown","content":"# `@modelcontextprotocol/fastify`\n\nFastify adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Fastify integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create a Fastify app with sensible defaults for MCP servers\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/fastify fastify\n\n# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):\nnpm install @modelcontextprotocol/node\n```\n\n## Exports\n\n- `createMcpFastifyApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n\n## Usage\n\n### Create a Fastify app (localhost DNS rebinding protection by default)\n\n```ts\nimport { createMcpFastifyApp } from '@modelcontextprotocol/fastify';\n\nconst app = createMcpFastifyApp(); // default host is 127.0.0.1; protection enabled\n```\n\n### Streamable HTTP endpoint (Fastify)\n\n```ts\nimport { createMcpFastifyApp } from '@modelcontextprotocol/fastify';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpFastifyApp();\n\napp.post('/mcp', async (request, reply) => {\n    // Stateless example: create a server and a transport per request (with sessions: one pair per session).\n    const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await mcpServer.connect(transport);\n\n    // Clean up when the client closes the connection (e.g. during SSE streaming).\n    reply.raw.on('close', () => {\n        transport.close();\n        mcpServer.close();\n    });\n\n    await transport.handleRequest(request.raw, reply.raw, request.body);\n});\n```\n\nTo reject non-POST requests with 405 Method Not Allowed, add routes for GET and DELETE that send a JSON-RPC error response.\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { hostHeaderValidation } from '@modelcontextprotocol/fastify';\n\napp.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));\n```\n","isInternal":false,"tokens":539,"sizeBytes":2330},{"name":"README.md","path":"packages/middleware/hono/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/middleware/hono/README.md","title":"hono Documentation","category":"plugin-manifest","format":"markdown","content":"# `@modelcontextprotocol/hono`\n\nHono adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Hono integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).\n\nIt does **not** implement MCP itself. Instead, it helps you:\n\n- create a Hono app with sensible defaults for MCP servers\n- parse JSON request bodies and expose them as `c.get('parsedBody')` for Streamable HTTP transports\n- add DNS rebinding protection via Host header validation (recommended for localhost servers)\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/hono hono\n```\n\n## Exports\n\n- `createMcpHonoApp(options?)`\n- `hostHeaderValidation(allowedHostnames)`\n- `localhostHostValidation()`\n\n## Usage\n\n### Streamable HTTP endpoint (Hono)\n\n```ts\nimport { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';\nimport { createMcpHonoApp } from '@modelcontextprotocol/hono';\n\nconst app = createMcpHonoApp();\napp.all('/mcp', async c => {\n    // Stateless example: create a server and a transport per request.\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    return transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') });\n});\n```\n\n### Host header validation (DNS rebinding protection)\n\n```ts\nimport { localhostHostValidation } from '@modelcontextprotocol/hono';\n\nconst app = createMcpHonoApp();\napp.use('*', localhostHostValidation());\n```\n","isInternal":false,"tokens":380,"sizeBytes":1646},{"name":"README.md","path":"packages/middleware/node/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/middleware/node/README.md","title":"node Documentation","category":"plugin-manifest","format":"markdown","content":"# `@modelcontextprotocol/node`\n\nNode.js adapters for the MCP TypeScript server SDK.\n\nThis package is a thin Node.js integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server). It provides a Streamable HTTP transport that works with Node’s `IncomingMessage` / `ServerResponse`.\n\nFor web‑standard runtimes (Cloudflare Workers, Deno, Bun, etc.), use `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly.\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server @modelcontextprotocol/node\n```\n\n## Exports\n\n- `NodeStreamableHTTPServerTransport`\n- `StreamableHTTPServerTransportOptions` (type alias for `WebStandardStreamableHTTPServerTransportOptions`)\n- `toNodeHandler(handler, opts?)` — adapt a web-standard `{ fetch }` MCP handler to a Node `(req, res, parsedBody?)` handler\n- `hostHeaderValidation(allowedHostnames)` / `localhostHostValidation()` — `Host` header guards for hand-wired `node:http` servers\n- `originValidation(allowedOriginHostnames)` / `localhostOriginValidation()` — `Origin` header guards for hand-wired `node:http` servers\n- `ToNodeHandlerOptions`, `FetchLikeMcpHandler`, `NodeMcpRequestHandler` (types for `toNodeHandler`)\n- `toWebRequest(req, parsedBody?, opts?)` — the Node `IncomingMessage` → web-standard `Request` conversion `toNodeHandler` performs internally, exported on its own (for example to feed `isLegacyRequest()` from a hand-wired `(req, res)` handler)\n- `ToWebRequestOptions` (options type for `toWebRequest`)\n- `NodeIncomingMessageLike`, `NodeServerResponseLike` (structural Node request/response shapes)\n\n## Usage\n\n### Express + Streamable HTTP\n\n```ts\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst app = createMcpExpressApp();\n\napp.post('/mcp', async (req, res) => {\n    // Stateless example: create a server and a transport per request.\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n\n    // If you use Express JSON parsing, pass the pre-parsed body to avoid re-reading the stream.\n    await transport.handleRequest(req, res, req.body);\n});\n```\n\n### Node.js `http` server\n\nPlain `node:http` has no middleware chain, so bind loopback explicitly and\ncompose the `Host`/`Origin` guards in front of the transport — matching the\ndefaults the framework app factories (`createMcpExpressApp`,\n`createMcpHonoApp`, `createMcpFastifyApp`) apply for you. The guards answer\nrejected requests with `403` themselves and return `false`, so the handler\nmust not touch the request further.\n\n```ts\nimport { createServer } from 'node:http';\nimport { localhostHostValidation, localhostOriginValidation, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst validateHost = localhostHostValidation();\nconst validateOrigin = localhostOriginValidation();\n\ncreateServer(async (req, res) => {\n    if (!validateHost(req, res) || !validateOrigin(req, res)) return;\n    const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n    const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });\n    await server.connect(transport);\n    await transport.handleRequest(req, res);\n}).listen(3000, '127.0.0.1');\n```\n","isInternal":false,"tokens":843,"sizeBytes":3606},{"name":"README.md","path":"packages/middleware/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/middleware/README.md","title":"middleware Documentation","category":"plugin-manifest","format":"markdown","content":"# Middleware packages\n\nThe packages in `packages/middleware/*` are **thin integration layers** that help you expose an MCP server in a specific runtime, platform, or web framework.\n\nThey intentionally **do not** add new MCP features or “business logic”. MCP functionality (tools, resources, prompts, transports, auth primitives, etc.) lives in `@modelcontextprotocol/server` (and other core packages). Middleware packages should primarily:\n\n- adapt request/response types to the SDK (e.g. Node.js `IncomingMessage`/`ServerResponse`)\n- provide small framework helpers (e.g. wiring, body parsing hooks)\n- supply safe defaults for common deployment pitfalls (e.g. localhost DNS rebinding protection)\n\n## Packages\n\n- `@modelcontextprotocol/express` — Express helpers (app defaults + Host header validation for DNS rebinding protection).\n- `@modelcontextprotocol/fastify` — Fastify helpers (app defaults + Host header validation).\n- `@modelcontextprotocol/hono` — Hono helpers (app defaults + JSON body parsing hook + Host header validation).\n- `@modelcontextprotocol/node` — Node.js Streamable HTTP transport wrapper for `IncomingMessage`/`ServerResponse`.\n\n## Typical usage\n\nMost servers use:\n\n- `@modelcontextprotocol/server` for the MCP server implementation\n- one middleware package for framework/runtime integration (this folder)\n- (optionally) additional platform/framework dependencies (Express, Hono, etc.)\n","isInternal":false,"tokens":296,"sizeBytes":1423},{"name":"README.md","path":"packages/server-legacy/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/server-legacy/README.md","title":"server-legacy Documentation","category":"plugin-manifest","format":"markdown","content":"# @modelcontextprotocol/server-legacy\n\n> **Deprecated** — This package is a frozen copy of v1 code for migration purposes only. It will not receive new features and is planned for removal in v3.\n\nProvides two pieces of v1 server functionality removed in v2:\n\n- **SSE Transport** (`./sse`) — The `SSEServerTransport` class, replaced in v2 by `NodeStreamableHTTPServerTransport` (from `@modelcontextprotocol/node`) or `WebStandardStreamableHTTPServerTransport` (from `@modelcontextprotocol/server`)\n- **OAuth Authorization Server** (`./auth`) — The `mcpAuthRouter` and related helpers, removed in v2 because MCP servers should use dedicated OAuth providers\n\n## Installation\n\n```bash\nnpm install @modelcontextprotocol/server-legacy\n```\n\n## Usage\n\n### SSE Transport (no Express dependency required)\n\n```ts\nimport { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse';\n```\n\n### OAuth Auth Router (requires Express)\n\n```ts\nimport { mcpAuthRouter } from '@modelcontextprotocol/server-legacy/auth';\n```\n\n### Everything (requires Express)\n\n```ts\nimport { SSEServerTransport, mcpAuthRouter } from '@modelcontextprotocol/server-legacy';\n```\n\n## Migration\n\n- **SSE → StreamableHTTP**: Use `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` (Node.js) or `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` (Web Standard / Cloudflare Workers)\n- **Auth router → Dedicated IdP**: Use a dedicated OAuth provider (Auth0, Keycloak, etc.) instead of the built-in OAuth Authorization Server\n","isInternal":false,"tokens":357,"sizeBytes":1542},{"name":"README.md","path":"packages/server/README.md","rawUrl":"https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/HEAD/packages/server/README.md","title":"server Documentation","category":"plugin-manifest","format":"markdown","content":"# `@modelcontextprotocol/server`\n\nThe MCP (Model Context Protocol) TypeScript server SDK. Build MCP servers that expose tools, resources, and prompts.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/server\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\nOptional framework adapters: [`@modelcontextprotocol/express`](https://www.npmjs.com/package/@modelcontextprotocol/express), [`@modelcontextprotocol/fastify`](https://www.npmjs.com/package/@modelcontextprotocol/fastify), [`@modelcontextprotocol/hono`](https://www.npmjs.com/package/@modelcontextprotocol/hono),\n[`@modelcontextprotocol/node`](https://www.npmjs.com/package/@modelcontextprotocol/node).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Server guide](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools)** — tools, resources, prompts, and the rest of the server surface\n- **[Serving guide](https://ts.sdk.modelcontextprotocol.io/v2/serving/http)** — stdio, HTTP, the framework adapters, sessions, and authorization\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n","isInternal":false,"tokens":461,"sizeBytes":1900}],"systemPromptSnippet":"<yakaai_skills repo=\"modelcontextprotocol/typescript-sdk\">\n<!-- File: CLAUDE.md (Tokens: ~297 | Category: claude-rule) -->\n# CLAUDE.md\n\nThe principles this SDK is built and reviewed by. They are not a checklist: apply them with judgement.\n\n1. **Minimalism** — The SDK should do less, not more: prefer changes that add no API or option unless justified.\n2. **Spec is the anchor** — The spec decides; where it leaves a choice open, check what the other official SDKs do before making your own choice.\n3. **Strict by default** — Accept what the spec allows, and add no leniency for peers that break it.\n4. **Backwards compatible** — New features and changes arrive in backwards compatible ways; when a bug, a spec violation or an unsafe default forces a break, the changeset says what breaks.\n5. **Small changes** — Guard scope hard: a smaller change that lands beats a bigger one that doesn't, and small, independent pull requests can land in any order.\n6. **The words are true** — Changesets, docs and comments say what the code does.\n7. **Test what users see** — Prefer tests that cover the end-to-end usage a user would actually see, where sensible; `test/e2e/requirements.ts` lists those behaviours.\n8. **Brief comments** — Keep inline comments to one line and JSDoc to the public API; the reasoning goes in the commit message or PR description.\n\nStyle belongs to Prettier, ESLint and the compiler.\n\n<!-- File: examples/guides/servers/skills/acme/billing/refunds/SKILL.md (Tokens: ~16 | Category: anthropic-skill) -->\n---\nname: refunds\ndescription: Process customer refunds.\n---\n\n# Refunds\n\n<!-- File: examples/guides/servers/skills/git-workflow/references/REVIEW.md (Tokens: ~8 | Category: anthropic-skill) -->\n# Review checklist\n\n- Tests pass.\n\n<!-- File: examples/guides/servers/skills/git-workflow/SKILL.md (Tokens: ~39 | Category: anthropic-skill) -->\n---\nname: git-workflow\ndescription: Branching and review conventions.\n---\n\n# Git workflow\n\nBefore opening a pull request, go through [the review checklist](references/REVIEW.md).\n\n<!-- File: examples/skills/package.json (Tokens: ~174 | Category: anthropic-skill) -->\n{\n    \"name\": \"@mcp-examples/skills\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"scripts\": {\n        \"server\": \"tsx server.ts\",\n        \"client\": \"tsx client.ts\"\n    },\n    \"dependencies\": {\n        \"@hono/node-server\": \"catalog:runtimeServerOnly\",\n        \"@mcp-examples/shared\": \"workspace:*\",\n        \"@modelcontextprotocol/client\": \"workspace:*\",\n        \"@modelcontextprotocol/hono\": \"workspace:*\",\n        \"@modelcontextprotocol/server\": \"workspace:*\"\n    },\n    \"devDependencies\": {\n        \"tsx\": \"catalog:devTools\"\n    },\n    \"example\": {\n        \"era\": \"dual\",\n        \"//\": \"Both legs list, load and verify skills, follow a reference and browse a skill folder.\"\n    }\n}\n\n<!-- File: examples/skills/README.md (Tokens: ~213 | Category: anthropic-skill) -->\n# skills\n\nThe server serves the `skills/` folder as MCP skills (SEP-2640) with one\n`loadSkills` call, under the `examples` prefix. The client lists the catalog,\nloads `git-workflow` (its `SKILL.md`, verified for size, digest and\nfrontmatter), follows the reference to `references/REVIEW.md`, browses the\n`refunds` folder, and catches a verification failure.\n\n```bash\npnpm --filter @mcp-examples/skills client            # modern leg over stdio\npnpm --filter @mcp-examples/skills client -- --legacy\npnpm --filter @mcp-examples/skills run server -- --http --port 3000\npnpm --filter @mcp-examples/skills client -- --http http://127.0.0.1:3000/mcp\n```\n\nSee [Serving skills](../../docs/servers/skills.md) and [Reading skills](../../docs/clients/skills.md).\n\n<!-- File: examples/skills/skills/acme/billing/refunds/escalation/SKILL.md (Tokens: ~40 | Category: anthropic-skill) -->\n---\nname: escalation\ndescription: Escalate a disputed refund to the billing lead.\n---\n\n# Escalation\n\nCollect the order number and the customer's reason, then page the billing lead.\n\n<!-- File: examples/skills/skills/acme/billing/refunds/SKILL.md (Tokens: ~57 | Category: anthropic-skill) -->\n---\nname: refunds\ndescription: Process a customer refund end to end.\nmetadata:\n    owner: billing\n---\n\n# Refunds\n\nCheck the order, issue the refund, then send [the refund email](templates/refund-email.md).\nFor disputed orders, use the escalation skill.\n\n<!-- File: examples/skills/skills/acme/billing/refunds/templates/refund-email.md (Tokens: ~16 | Category: anthropic-skill) -->\nHello {{customer}},\n\nYour refund of {{amount}} is on its way.\n\n<!-- File: examples/skills/skills/git-workflow/references/REVIEW.md (Tokens: ~17 | Category: anthropic-skill) -->\n# Review checklist\n\n- Tests pass locally.\n- A changeset describes the change.\n\n<!-- File: examples/skills/skills/git-workflow/SKILL.md (Tokens: ~72 | Category: anthropic-skill) -->\n---\nname: git-workflow\ndescription: Branching, commit and review conventions for this team.\n---\n\n# Git workflow\n\n1. Branch from `main` as `feature/<topic>`.\n2. Keep commits small; write messages in the imperative.\n3. Before opening a pull request, go through [the review checklist](references/REVIEW.md).\n\n<!-- File: skills/mcp-typescript-sdk-skills/references/client.md (Tokens: ~1512 | Category: anthropic-skill) -->\n# Use skills from MCP servers\n\n<!-- #region guide -->\n\n`SkillsClient` lists a server's skills and reads their files, checking each file against what the server published.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\nAgainst a server with two skills, it prints each skill's name and description:\n\n```text\nrefunds: Process customer refunds.\ngit-workflow: Branching and review conventions.\n```\n\nListing reads no skill file: each **entry** carries the skill's frontmatter and its file list with digests, enough to decide which skill to load.\n\n## Handle refused entries\n\nCreate the client with `skipInvalid: true` to keep the valid skills and log the refused entries.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#list_invalid\"\nconst tolerant = new SkillsClient(client, { skipInvalid: true });\nconst { invalid } = await tolerant.list();\nfor (const refused of invalid) console.error(`skills: refused entry #${refused.index} ${refused.uri ?? ''}: ${refused.reason}`);\n```\n\nBy default `list()` throws an `SdkError` naming every entry that fails validation, and every copy of a repeated URI. With `skipInvalid: true` it returns the valid skills and puts those entries in `invalid`, each with `index`, `uri` when it has one, and `reason`. Against this server it is empty, so the loop prints nothing.\n\n## Load a skill\n\nRead the entry's `SKILL.md`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nThe file passes every check, and its frontmatter matches the entry:\n\n```text\ntrue {\n  name: 'git-workflow',\n  description: 'Branching and review conventions.'\n}\n```\n\n`read(entry)` checks the size and SHA-256 digest against the entry, then compares the frontmatter field by field. `manifest.body` holds the instructions to give the model.\n\nThe comparison parses the frontmatter with YAML 1.2 core-schema typing, so `yes` is the string `\"yes\"`, not `true`. If a server builds its entries with a parser that types values differently, pass a matching YAML parser as `parseFrontmatter` to `new SkillsClient`; it receives the YAML between the `---` lines.\n\n## Read the files a skill references\n\nPass the path `SKILL.md` uses.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#read_reference\"\nconst review = await skills.read(git, 'references/REVIEW.md');\nconsole.log(review.text);\n```\n\n`review.text` holds the verified file:\n\n```text\n# Review checklist\n\n- Tests pass.\n\n```\n\nA relative path resolves against the skill's folder, as a link in `SKILL.md` does, and `read` ignores a fragment. It matches the path to the entry's file list however either side percent-encodes the name, so `references/Q&A.md` works. `bytes` always holds the file's bytes; `text` holds the file's text when the server sent it as text, and always for the skill's own `SKILL.md`.\n\n## Handle verification failures\n\nCatch `SkillVerificationError`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#SkillVerificationError\"\nimport { SkillVerificationError } from '@modelcontextprotocol/client/ext/skills';\n\ntry {\n    await skills.read(git, 'references/MISSING.md');\n} catch (error) {\n    if (!(error instanceof SkillVerificationError)) throw error;\n    console.log(error.reason, error.uri);\n}\n```\n\nThe entry does not list `references/MISSING.md`, so `read` refuses it before sending any request:\n\n```text\nunlisted skill://git-workflow/references/MISSING.md\n```\n\n`reason` is `unlisted`, `size`, `digest` or `frontmatter`; a path that does not parse is `unlisted` too. When a size, digest or frontmatter check fails on a copy the client served from its response cache, `read` fetches the file once more with `cacheMode: 'refresh'` and throws only if the fresh copy fails as well; a copy that came from the server fails at once. On any reason, do not use the file; for `SKILL.md`, do not load the skill.\n\n## Browse a skill folder\n\nList a folder's direct children when the server declares `directoryRead`.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#readDirectory\"\nif (skills.directoryRead) {\n    const { resources: children } = await skills.readDirectory('skill://acme/billing/refunds');\n    console.log(children.map(child => `${child.name} ${child.mimeType}`).join('\\n'));\n}\n```\n\nThe `refunds` folder holds its `SKILL.md` and a `templates` folder:\n\n```text\nSKILL.md text/markdown\ntemplates inode/directory\n```\n\n## Get a skill by URI\n\nFetch one entry, listed or not.\n\n```ts source=\"../../../examples/guides/clients/skills.examples.ts#get\"\nconst { skill: refunds } = await skills.get('skill://acme/billing/refunds/SKILL.md');\nconsole.log(refunds.frontmatter.name);\n```\n\nIt prints the skill's name from the entry's frontmatter:\n\n```text\nrefunds\n```\n\nOn 2026-07-28, `list()`, `get()` and `readDirectory()` also return the server's caching hints, `ttlMs` and `cacheScope`, and throw an `SdkError` when a response leaves either out.\n\n::: info Dynamic skills\nAn entry whose `resources` is `\"dynamic\"` has no file list. `read` then skips the size and digest checks and returns `verified: false`, but still compares the `SKILL.md` frontmatter.\n:::\n\n::: warning Host responsibilities\nAccept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before you load a skill from a server, and record which server each skill came from. Read a skill's files when you load it, never while listing.\n:::\n\n## Recap\n\n- `new SkillsClient(client)` wraps a connected client; `list()` returns entries without reading files.\n- `list()` throws on an invalid entry; `skipInvalid: true` keeps the valid skills and returns the refused ones in `invalid`.\n- `read(entry)` verifies `SKILL.md` (size, digest, frontmatter) and returns its frontmatter and body.\n- `read(entry, path)` resolves a path against the skill's folder and verifies that file.\n- A failed check throws `SkillVerificationError` with a `reason`.\n\n<!-- #endregion guide -->\n\n<!-- File: skills/mcp-typescript-sdk-skills/references/server.md (Tokens: ~2044 | Category: anthropic-skill) -->\n# Serve skills from an MCP server\n\n<!-- #region guide -->\n\n`loadSkills` reads a folder of skills once; `attach` serves it on a server.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\nA **skill** is a folder with a `SKILL.md` and the files it references (SEP-2640). Clients list skills with `skills/list` and read each file with `resources/read`, checking it against the digest your server published.\n\n## Lay out the skills folder\n\nEach folder that contains a `SKILL.md` is one skill; other folders only group skills in the URI.\n\n```text\nskills/\n├── git-workflow/\n│   ├── SKILL.md\n│   └── references/REVIEW.md\n└── acme/billing/refunds/\n    └── SKILL.md\n```\n\n`SKILL.md` starts with YAML frontmatter holding `name` (the same as the skill's folder name) and `description`. Print the URIs the catalog publishes:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#entries\"\nconsole.log(skills.entries.map(skill => skill.uri).join('\\n'));\n```\n\nIt prints one `SKILL.md` URI per skill, sorted:\n\n```text\nskill://acme/billing/refunds/SKILL.md\nskill://git-workflow/SKILL.md\n```\n\n## Serve one catalog from every server\n\nAttach the same catalog to each server your handler creates.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\nThe catalog keeps metadata only (paths, digests, sizes, frontmatter); each read goes to the folder, and the SDK serves the bytes only if they still match the published digest. It sends a file as `text` whenever its bytes are valid UTF-8 (a BOM included), whatever its MIME type, and as a base64 `blob` otherwise.\n\n## Pick up changes with `reload()`\n\nRescan the folder and swap the new catalog in at once.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload\"\nconst changes = await skills.reload();\nconsole.log(changes);\n```\n\nWith nothing changed on disk, `reload()` reports no changes:\n\n```text\n{ added: [], removed: [], changed: [], listChanged: false }\n```\n\nUntil you call `reload()`, the server answers a read of a file edited on disk with an error instead of unverified content. If the rescan fails, `reload()` rejects and the old catalog keeps serving.\n\n`listChanged` is `true` when a skill appears, disappears, or changes its name or description. A server that stays connected (on stdio, or in a stateful 2025-11-25 HTTP session) then sends its clients `notifications/resources/list_changed` itself. A server that `createMcpHandler` builds lives for one request, so publish the change on the handler instead:\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nEvery open `subscriptions/listen` stream that opted in to resource list changes receives the notification.\n\n## Serve skills defined in code\n\nPass the files to `memorySkillFiles` where there is no file system, such as Workers or tests.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#memorySkillFiles\"\nimport { memorySkillFiles, SkillsExtension } from '@modelcontextprotocol/server/ext/skills';\n\nconst RELEASE_NOTES_MD = `---\nname: release-notes\ndescription: Write release notes from merged pull requests.\n---\n# Release notes\n`;\n\nconst inline = await SkillsExtension.load(memorySkillFiles({ 'release-notes/SKILL.md': RELEASE_NOTES_MD }));\n```\n\n`memorySkillFiles` re-reads the object you pass whenever the catalog rescans (at load and on `reload()`), so `reload()` sees files you add or change later.\n\n## Serve skills from your own storage\n\nImplement `SkillFiles`, two functions over relative paths, to load from any store.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillFiles_custom\"\nimport type { SkillFiles } from '@modelcontextprotocol/server/ext/skills';\n\n// Two operations over '/'-separated paths, '' being the root: here, a key-value store of objects.\nfunction bucketSkillFiles(bucket: ReadonlyMap<string, Uint8Array>): SkillFiles {\n    return {\n        async list(dir) {\n            const prefix = dir === '' ? '' : `${dir}/`;\n            const children = new Map<string, 'file' | 'directory'>();\n            for (const key of bucket.keys()) {\n                if (!key.startsWith(prefix)) continue;\n                const [name, ...rest] = key.slice(prefix.length).split('/');\n                if (name !== undefined) children.set(name, rest.length > 0 ? 'directory' : 'file');\n            }\n            return [...children].map(([name, kind]) => ({ name, kind }));\n        },\n        async read(key) {\n            const bytes = bucket.get(key);\n            if (bytes === undefined) throw new Error(`no object ${key}`);\n            return bytes;\n        }\n    };\n}\n\nconst fromBucket = await SkillsExtension.load(\n    bucketSkillFiles(new Map([['release-notes/SKILL.md', new TextEncoder().encode(RELEASE_NOTES_MD)]]))\n);\n```\n\nThe catalog logic (discovery, digests, rule checks, paging, verified reads) is the same for every adapter. Report a symlink or other special file as `kind: 'unsupported'`; the catalog skips the skill that contains one.\n\n## Generate skills with a `SkillSource`\n\nBuild entries with `skillEntry` and serve them from a `SkillSource` when the catalog depends on the request.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#skillSource\"\nimport type { SkillSource } from '@modelcontextprotocol/server/ext/skills';\nimport { skillEntry } from '@modelcontextprotocol/server/ext/skills';\n\nconst ON_CALL_MD = `---\nname: on-call\ndescription: Handle a page while on call.\n---\n# On call\n`;\nconst onCall = await skillEntry('on-call', { 'SKILL.md': ON_CALL_MD });\n\nconst source: SkillSource = {\n    list: async () => ({ skills: [onCall] }),\n    get: async uri => (uri === onCall.uri ? onCall : undefined),\n    read: async uri => (uri === onCall.uri ? { text: ON_CALL_MD, mimeType: 'text/markdown' } : undefined)\n};\nconst generated = new SkillsExtension({ source });\n```\n\nEvery source method receives the request context (`ctx`), so you can list different skills per caller. Add `readDirectory` to serve folder listings, and `listResources` to list skills in `resources/list`.\n\n## Handle invalid skills\n\nRoute warnings where you want them; by default they go to stderr.\n\n```ts source=\"../../../examples/guides/servers/skills.examples.ts#onWarning\"\nconst checked = await loadSkills('./skills', {\n    onWarning: ({ path, message }) => console.error(`skills: ${path}: ${message}`)\n});\n```\n\n`loadSkills` skips, with a warning, any skill that breaks the rules, such as a `name` that differs from its folder or a symlinked file; the other skills still serve. Skills over 512 files or 16 MiB still publish, with a warning; `limits` changes those thresholds. The same options take `prefix` (`'acme'` serves every skill under `skill://acme/…`) and `pageSize` (entries per `skills/list` and directory page, default 100).\n\nSet `strict: true` to make `loadSkills` throw a `SkillCatalogError` listing every problem instead. Under `strict: true`, a symlink or other special file anywhere in the folder fails the load, even one outside every skill; hidden entries (names starting with `.`) are the exception, since the scan skips them entirely.\n\n::: warning\nDo not register your own resources under `skill://`. A resource you register at a skill file's URI takes precedence over that file, and clients then fail to verify it.\n:::\n\n::: info\nSkills work on both protocol versions; the `cacheHint` option applies on 2026-07-28 only — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).\n:::\n\n## Recap\n\n- `loadSkills('./skills')` builds a catalog once; `attach(server)` serves it on any number of servers.\n- Each folder with a `SKILL.md` is a skill; other folders group skills in the URI.\n- `reload()` swaps in a rescanned catalog all at once; behind `createMcpHandler`, call `handler.notify.resourcesChanged()` when it reports `listChanged`.\n- `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource` serve skills without a folder.\n- `loadSkills` skips invalid skills with a warning; `strict: true` makes it throw instead.\n\n<!-- #endregion guide -->\n\n<!-- File: skills/mcp-typescript-sdk-skills/SKILL.md (Tokens: ~3157 | Category: anthropic-skill) -->\n---\nname: mcp-typescript-sdk-skills\ndescription: 'Serves and consumes Skills over MCP (SEP-2640, the `io.modelcontextprotocol/skills` extension) with v2 of the MCP TypeScript SDK: publishes a folder of `SKILL.md` skills from an MCP server with `loadSkills` and `SkillsExtension`, and lists, verifies and loads them in a client or host with `SkillsClient`. Use whenever a TypeScript project on `@modelcontextprotocol/server` or `@modelcontextprotocol/client` 2.x wants to expose agent skills from its MCP server, serve `skill://` resources or `skills/list`, or load skills from the MCP servers it connects to, even if the user does not say \"SEP-2640\" or \"extension\". Not for projects still on v1 (`@modelcontextprotocol/sdk`: upgrade them with the `mcp-typescript-sdk-upgrade-to-v2` skill first), for MCP SDKs in other languages (Python, Go, C#), or for writing the content of a `SKILL.md`.'\n---\n\n# Skills over MCP with the TypeScript SDK\n\nSkills over MCP lets an MCP server publish agent skills (folders with a `SKILL.md`) and lets clients list, verify and load them. In v2 of the TypeScript SDK it ships as the `ext/skills` subpaths of `@modelcontextprotocol/server` and `@modelcontextprotocol/client`.\n\nThe goal is working, verified code in the user's project. Use only the APIs shown here and in the references, and skip explaining the protocol unless the user asks.\n\n## Which side the project is on\n\nRead `package.json` and the source before writing anything:\n\n- **Server:** depends on `@modelcontextprotocol/server` and creates an `McpServer`, directly or in a factory passed to `serveStdio` or `createMcpHandler`. It publishes skills: follow [Serve skills](#serve-skills).\n- **Client or host:** depends on `@modelcontextprotocol/client` and creates a `Client`. It loads skills from the servers it connects to: follow [Use skills](#use-skills).\n- **Both** (a host that also runs a server, a gateway): follow both, each against its own code. A `Client` that exists only in the project's tests does not make it a host: that is where [Verify](#verify) runs.\n- **v1** (`@modelcontextprotocol/sdk`): Skills over MCP exists only in v2. Upgrade first with the `mcp-typescript-sdk-upgrade-to-v2` skill, then come back.\n\n## Check that the installed SDK has skills\n\nSkills over MCP ships in the `./ext/skills` export of the v2 packages, and an import of a subpath the installed package lacks fails only at build or run time. Check first, from the package that depends on the SDK (use `client` in place of `server` for a client):\n\n```bash\ngrep -c '\"./ext/skills\"' node_modules/@modelcontextprotocol/server/package.json\n```\n\nIf it prints `0`, check the latest release:\n\n```bash\nnpm view @modelcontextprotocol/server@latest exports --json | grep -c '\"./ext/skills\"'\n```\n\n- **The latest release has it:** upgrade (`server` and `client` to the same version, if the project uses both) and continue.\n- **It does not:** stop here. Tell the user that Skills over MCP is not in a released version of the SDK yet, and that this works once a release exports `@modelcontextprotocol/server/ext/skills`. Do not write `skills/list` handlers or `skill://` resources by hand as a workaround. Clients verify every file against the digests and frontmatter the extension publishes, and the extension also declares the capability, pages the catalog and handles both protocol versions, so a hand-written copy is easy to get wrong in ways those clients refuse, and drifts from the spec.\n\n## Serve skills\n\n**Pick where the skills come from.** Each source gives you an extension that you attach as shown below; only the first three have `reload()`:\n\n- **A folder on disk** (Node): `loadSkills`, which the steps below walk through.\n- **Skills defined in code** (Workers and other runtimes without a file system, tests): `memorySkillFiles`. See [Serve skills defined in code](references/server.md#serve-skills-defined-in-code).\n- **Your own storage** (an object store, a database, a git repository): a `SkillFiles` adapter. See [Serve skills from your own storage](references/server.md#serve-skills-from-your-own-storage).\n- **Skills that differ per caller or are generated per request:** a `SkillSource`. See [Generate skills with a `SkillSource`](references/server.md#generate-skills-with-a-skillsource).\n\n**Lay out the folder.** Each folder holding a `SKILL.md` is one skill. Its frontmatter needs a `name` equal to the folder's name and a `description`. Other folders only group skills in the URI.\n\n**Load the folder once and attach it before the server connects.** `attach` throws on a connected server, and each server takes one skills extension. A server connected by hand keeps its `server.connect(transport)` call, after `attach`.\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#loadSkills_basic\"\nimport { McpServer } from '@modelcontextprotocol/server';\nimport { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';\n\nconst skills = await loadSkills('./skills');\n\nconst server = new McpServer({ name: 'team-skills', version: '1.0.0' });\nskills.attach(server);\n```\n\n**Resolve the folder from the module, not the working directory.** A relative path like `'./skills'` resolves against `process.cwd()`, and MCP hosts may start stdio servers from another directory (such as `/`), where the load throws and takes the whole server down with it. Build the path from the module's location instead: `fileURLToPath(new URL('../skills', import.meta.url))` from `src/index.ts`, or `path.join(__dirname, '../skills')` in CommonJS.\n\n**Where a factory builds the server, attach inside it**, so every server it builds serves the same catalog. That covers `serveStdio(createServer)` on stdio as well as `createMcpHandler` over HTTP:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#attach_perRequest\"\nimport { createMcpHandler } from '@modelcontextprotocol/server';\n\nconst handler = createMcpHandler(() => {\n    const mcp = new McpServer({ name: 'team-skills', version: '1.0.0' });\n    skills.attach(mcp);\n    return mcp;\n});\n```\n\n**Pick up edits with `reload()`** if the user expects changes to show without a restart. Call it from whatever tells the project the folder changed (a file watcher, an admin route, a deploy hook). A server that stays connected (stdio) notifies its clients itself. Behind `createMcpHandler`, publish the change on the handler, where `changes` is what `await skills.reload()` returned:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#reload_notify\"\nif (changes.listChanged) handler.notify.resourcesChanged();\n```\n\nThen tell the user about any load warnings, and [verify](#verify).\n\nRead [references/server.md](references/server.md) for `onWarning`, `strict`, `limits`, `prefix` and `pageSize`.\n\n## Use skills\n\n**Wrap the connected client and list.** Listing reads no skill file: each entry carries the skill's frontmatter and file list, enough to show what is on offer.\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#SkillsClient_list\"\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst skills = new SkillsClient(client);\nconst { skills: entries } = await skills.list();\nconsole.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\\n'));\n```\n\n`skills.supported` is `false` for a server that does not declare the extension; a host that connects to several servers checks it per server before listing. `list()` throws an `SdkError` if any entry is invalid, so a host listing servers it does not control creates the client with `new SkillsClient(client, { skipInvalid: true })` and logs the refused entries it returns in `invalid`.\n\n**Load a skill only after the user (or the host's policy) approves it.** Read its `SKILL.md`, which checks size, digest and frontmatter, and give `manifest.body` to the model:\n\n```ts source=\"../../examples/guides/clients/skills.examples.ts#read_manifest\"\nconst git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;\nconst manifest = await skills.read(git);\nconsole.log(manifest.verified, manifest.frontmatter);\n```\n\nRead the files `SKILL.md` refers to with `skills.read(entry, path)` when the model needs them. A failed check throws `SkillVerificationError`: do not use that file, and if it is the `SKILL.md`, do not load the skill.\n\nRead [references/client.md](references/client.md) for `skipInvalid`, verification reasons, `readDirectory`, `get`, dynamic skills and caching hints.\n\n## Mistakes to avoid\n\nServing:\n\n- **Writing the protocol by hand.** Serve skills only through `SkillsExtension` (`loadSkills`, `memorySkillFiles`, a `SkillFiles` adapter or a `SkillSource`). Never `setRequestHandler('skills/list', …)`, and never register your own resources under `skill://`: a resource at a skill file's URI takes precedence over the file, and clients then fail to verify it.\n- **Missing the skipped skills.** `loadSkills` skips a skill whose `name` differs from its folder, or that contains a symlink, and only warns (on stderr by default) while the others serve. Read the warnings after loading and tell the user which skills were skipped. Pass `strict: true` (in CI, say) to make the load throw instead.\n- **Dot-named entries.** Files and folders whose names start with `.` are skipped with no warning at all, and `strict: true` does not catch them. A skill in a `.name` folder, or a dotfile its `SKILL.md` refers to, is simply missing: rename it.\n- **A catalog outside the factory's servers.** `serveStdio` and `createMcpHandler` build their servers from a factory, and each server `createMcpHandler` builds lives for one request. Attach inside the factory, and behind `createMcpHandler` call `handler.notify.resourcesChanged()` when `reload()` reports `listChanged`, or connected clients never hear of the change.\n- **Expecting edits to show up on their own.** The catalog keeps the digests from load time, so a file edited on disk afterwards is refused with an error until `reload()` runs.\n- **`loadSkills` without a file system.** It reads the disk (`ext/skills/node`). On Workers and similar runtimes, use `memorySkillFiles` or a `SkillFiles` adapter from `@modelcontextprotocol/server/ext/skills`.\n\nLoading:\n\n- **Raw `resources/read`.** Never fetch skill files with `client.readResource()`: it skips the size, digest and frontmatter checks. Use `skills.read()`.\n- **Reading while listing.** Read a skill's files when it is loaded, never while listing.\n- **Skipping the host's duties.** Accept skills up to `SKILL_LIMITS` (512 files, 16 MiB) and explain why you decline one, get approval before loading a skill from a server, and record which server each skill came from, so the instructions given to the model can be traced to their source.\n\nBoth:\n\n- **Invented APIs.** Take skills APIs only from `@modelcontextprotocol/server/ext/skills`, `@modelcontextprotocol/server/ext/skills/node` and `@modelcontextprotocol/client/ext/skills`, using names shown here or in the references. If you need something else, read the package's type declarations instead of guessing.\n\n## Verify\n\n**Server:** list every skill and read every file it lists through a real client. In a script or test, connect a `Client` to the server over an in-memory pair and read each file with `SkillsClient`. `read` throws when a file fails a check, and the loop throws when the server lists no skills:\n\n```ts source=\"../../examples/guides/servers/skills.examples.ts#verify_inMemory\"\nimport { Client, InMemoryTransport } from '@modelcontextprotocol/client';\nimport { SkillsClient } from '@modelcontextprotocol/client/ext/skills';\n\nconst [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();\nawait server.connect(serverTransport);\nconst client = new Client({ name: 'skills-check', version: '1.0.0' });\nawait client.connect(clientTransport);\n\nconst skillsClient = new SkillsClient(client);\nconst { skills: listed } = await skillsClient.list();\nif (listed.length === 0) throw new Error('the server listed no skills');\nfor (const entry of listed) {\n    const files = entry.resources === 'dynamic' ? [entry.uri] : entry.resources.map(file => file.uri);\n    for (const uri of files) await skillsClient.read(entry, uri);\n}\nconsole.log(`${listed.length} skill(s) listed and verified`);\n```\n\nAn entry whose `resources` is `\"dynamic\"` (only a `SkillSource` serves one) has no file list, so the loop reads only its `SKILL.md`: `read` checks the frontmatter but has no size or digest to check, and returns `verified: false`.\n\nFor a stdio server, the same loop works over `StdioClientTransport` (from `@modelcontextprotocol/client/stdio`) against the server's real start command; set its `cwd` option to another directory (such as `/`), as hosts do, so a path that depends on the working directory fails here and not in the user's host. Compare the number of skills listed with the number of skill folders, and account for each one missing: a warning, or a dot-name.\n\n**Client:** run the host against a server that serves at least one skill (the server path above takes a few lines, or `memorySkillFiles` in a test). Check that it lists the skill, asks before loading it, and reads its `SKILL.md` with `verified: true`.\n\n## References\n\n| Reference file                                          | Read it when                                  |\n| ------------------------------------------------------- | --------------------------------------------- |\n| [Serve skills from an MCP server](references/server.md) | The project serves skills from an MCP server  |\n| [Use skills from MCP servers](references/client.md)     | The project loads skills from its MCP servers |\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/auth.md (Tokens: ~4410 | Category: anthropic-skill) -->\n### Auth\n\n#### OAuth error consolidation\n\nThe individual OAuth error classes are replaced with a single `OAuthError` + `OAuthErrorCode`.\nThe `OAUTH_ERRORS` constant is removed. The codemod does not rewrite `instanceof` checks\non these classes — switch on `error.code` instead.\n\n| v1 class                       | v2 equivalent                                           |\n| ------------------------------ | ------------------------------------------------------- |\n| `InvalidRequestError`          | `OAuthError` + `OAuthErrorCode.InvalidRequest`          |\n| `InvalidClientError`           | `OAuthError` + `OAuthErrorCode.InvalidClient`           |\n| `InvalidGrantError`            | `OAuthError` + `OAuthErrorCode.InvalidGrant`            |\n| `UnauthorizedClientError`      | `OAuthError` + `OAuthErrorCode.UnauthorizedClient`      |\n| `UnsupportedGrantTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedGrantType`    |\n| `InvalidScopeError`            | `OAuthError` + `OAuthErrorCode.InvalidScope`            |\n| `AccessDeniedError`            | `OAuthError` + `OAuthErrorCode.AccessDenied`            |\n| `ServerError`                  | `OAuthError` + `OAuthErrorCode.ServerError`             |\n| `TemporarilyUnavailableError`  | `OAuthError` + `OAuthErrorCode.TemporarilyUnavailable`  |\n| `UnsupportedResponseTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedResponseType` |\n| `UnsupportedTokenTypeError`    | `OAuthError` + `OAuthErrorCode.UnsupportedTokenType`    |\n| `InvalidTokenError`            | `OAuthError` + `OAuthErrorCode.InvalidToken`            |\n| `MethodNotAllowedError`        | `OAuthError` + `OAuthErrorCode.MethodNotAllowed`        |\n| `TooManyRequestsError`         | `OAuthError` + `OAuthErrorCode.TooManyRequests`         |\n| `InvalidClientMetadataError`   | `OAuthError` + `OAuthErrorCode.InvalidClientMetadata`   |\n| `InsufficientScopeError`       | `OAuthError` + `OAuthErrorCode.InsufficientScope` ¹     |\n| `InvalidTargetError`           | `OAuthError` + `OAuthErrorCode.InvalidTarget`           |\n| `CustomOAuthError`             | `new OAuthError(customCode, message)`                   |\n\n¹ Unrelated to the new transport-layer `InsufficientScopeError` (SEP-2350) exported from\n`@modelcontextprotocol/client`, which carries an RFC 6750 challenge from the resource\nserver and extends `OAuthClientFlowError`, **not** `OAuthError`. Do not rewrite that one.\n\n```typescript\n// v1\nif (error instanceof InvalidClientError) { ... }\n// v2\nimport { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }\n```\n\n⚠ **Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from\n`@modelcontextprotocol/express`, or from `@modelcontextprotocol/server` on\nweb-standard hosts) classifies the error your\n`OAuthTokenVerifier.verifyAccessToken()` throws: a v2\n`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +\n`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from\n`server-legacy`) or a generic `Error` falls through as unexpected — **invalid tokens\nbecome HTTP `500`**. When you re-point `requireBearerAuth` at\n`@modelcontextprotocol/express`, migrate the error classes your verifier throws in the\nsame change.\n\nA frozen copy of the v1 classes (and `mcpAuthRouter`) is available from\n`@modelcontextprotocol/server-legacy/auth` during migration.\n\n#### `AuthProvider` — non-OAuth bearer auth and the widened `authProvider` option\n\nThe transport `authProvider` option is widened to `AuthProvider | OAuthClientProvider`.\n**`AuthProvider`** is a new minimal interface — `{ token(): Promise<string | undefined>;\nonUnauthorized?(ctx): Promise<void> }` — for static-token / non-OAuth bearer auth.\nTransports call `token()` before every request and `onUnauthorized()` on 401 (then retry\nonce). Existing `OAuthClientProvider` implementations need no changes — transports adapt\nthem internally via the new `adaptOAuthProvider()` export. Also exported:\n`isOAuthClientProvider()` (type guard) and `handleOAuthUnauthorized()` (the standard\nOAuth `onUnauthorized` behavior, for composing your own adapter).\n\n#### OAuth client flow — behavioral changes\n\n- **Resolved scope passed to DCR (SEP-835).** `auth()` now computes the resolved scope\n  once (WWW-Authenticate → PRM `scopes_supported` → `clientMetadata.scope`) and passes\n  it to **both** the DCR POST body and the authorization request. `registerClient()`\n  gained an optional `scope` parameter that overrides `clientMetadata.scope` in the\n  registration body.\n- **OAuth error on HTTP 200.** `exchangeAuthorization()` / `refreshAuthorization()` now\n  throw `OAuthError` when the AS returns HTTP 200 with a JSON `{error: ...}` body (e.g.\n  GitHub). v1 surfaced this as a Zod parse failure on the tokens schema.\n- **Metadata discovery falls through on 502.** `discoverAuthorizationServerMetadata()`\n  treats `502 Bad Gateway` like 4xx — fall through to the next candidate URL instead of\n  throwing (fixes path-aware discovery behind reverse proxies). Other 5xx still throw.\n- **Scoped credential invalidation on `invalid_client` / `unauthorized_client`.** The\n  `auth()` retry for these errors now issues two scoped calls —\n  `invalidateCredentials('client')` then `invalidateCredentials('tokens')` — instead of\n  v1's single `invalidateCredentials('all')`, deliberately preserving the stored\n  discovery state so the callback-leg check on retry does not mask the original error.\n  A provider whose `invalidateCredentials()` implementation special-cases the `'all'`\n  scope must handle the split calls.\n- **Token persistence failures after a refresh now propagate.** v1 wrapped both\n  `refreshAuthorization()` and the `saveTokens()` that persists its result in one\n  `try`/`catch`, so a provider's persistence error was discarded alongside AS-side refresh\n  failures and `auth()` fell through to a fresh authorization request, returning\n  `'REDIRECT'`. Only the refresh call is guarded now — persisting runs after it and rejects\n  to the caller. Against an AS that rotates refresh tokens this was destructive rather than\n  merely quiet: the exchange has already succeeded, so the old refresh token is invalidated\n  server-side the moment the new one is issued, and dropping the new token set leaves\n  nothing usable on either side. A provider whose `saveTokens()` can throw (transient\n  storage errors, file-lock contention) must handle the rejection from `auth()` — and from\n  the transport 401-retry paths built on it — where v1 silently re-authorized. Refresh\n  failures themselves keep their control flow: a `ServerError` or an unknown error still\n  falls through to a new authorization request, and `invalid_grant` / `invalid_client` /\n  `unauthorized_client` are still recovered by discarding stored credentials and retrying.\n  Both routes now emit a `console.warn` naming the cause, so an unexplained re-auth prompt\n  can be traced to the failure that triggered it.\n\n#### OAuth client flow errors (new)\n\nThe OAuth client flow now throws dedicated classes from `@modelcontextprotocol/client`\n(all extend `OAuthClientFlowError`, **not** `OAuthError` — `auth()`'s `OAuthError` retry\npath will not catch them):\n\n| Throw site                                                                                                                | v2 class                                                                              |\n| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| `registerClient()` rejected by AS (⚠ `@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) | `RegistrationRejectedError` (`status`, `body`, `submittedMetadata`)                   |\n| Token-exchange / refresh / `fetchToken` / Cross-App grant on a non-`https:` token endpoint                                | `InsecureTokenEndpointError` (`tokenEndpoint`)                                        |\n| RFC 9207 `iss` mismatch / RFC 8414 §3.3 issuer-echo mismatch                                                              | `IssuerMismatchError` (`kind`, `expected`, `received`)                                |\n| Transport 403 `insufficient_scope` with `onInsufficientScope: 'throw'`, or default mode without an `OAuthClientProvider`  | `InsufficientScopeError` (`requiredScope`, `resourceMetadataUrl`, `errorDescription`) |\n| `auth()` callback leg: discovery resolves a different AS than the recorded redirect target                                | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n| `auth()` on a provider that cannot re-register, or `fetchToken()`: client information stamped for a different AS          | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`)                |\n\n#### Connect-time OAuth retry (`UnauthorizedError`)\n\n`UnauthorizedError` survives in v2 (exported from `@modelcontextprotocol/client` —\nits only appearance in the error table above is the removed `SSEClientTransport.send()`\n401 path), and the v1 connect-time pattern carries over: catch it from `connect()`,\ncomplete the browser flow, call `transport.finishAuth(…)`, reconnect.\n\n```typescript\ntry {\n    await client.connect(transport);\n} catch (error) {\n    if (!(error instanceof UnauthorizedError)) throw error;\n    // provider.redirectToAuthorization() has been called; complete the flow,\n    // then reconnect on a FRESH transport (a started transport cannot be restarted).\n    await transport.finishAuth(new URL(callbackUrl).searchParams);\n    await client.connect(new StreamableHTTPClientTransport(url, { authProvider: provider }));\n}\n```\n\nThis direct `instanceof` check works in every version-negotiation mode: under the\nprobing modes (`versionNegotiation: { mode: 'auto' }`, with or without a pin) the\nconnect-time `UnauthorizedError` also propagates unchanged from `connect()`. Older\nreleases wrapped it as `SdkError(SdkErrorCode.EraNegotiationFailed)` with the error at\n`error.data.cause` — that unwrap is no longer needed. See the\n[client OAuth guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/oauth.md).\n\n#### `auth()` options are now `AuthOptions`\n\nThe inline options object on `auth()` is now the named `AuthOptions` type. New fields:\n`iss?: string` (the form-urldecoded `iss` from the authorization callback — pass it\nalongside `authorizationCode` for RFC 9207 validation), `skipIssuerMetadataValidation?:\nboolean` (security-weakening opt-out of the RFC 8414 §3.3 issuer-echo check), and\n`forceReauthorization?: boolean` (skip the refresh-token branch — set by the transport's\nstep-up path; hosts driving step-up themselves set it under the same condition).\n\n#### Authorization-server mix-up defense (RFC 9207 / RFC 8414 §3.3) — action required\n\n`transport.finishAuth()` and `auth()` now validate `iss` from the authorization callback\nagainst the issuer recorded from validated AS metadata. A mismatched `iss` throws\n`IssuerMismatchError` before the code is exchanged regardless of advertised support; a\n**missing** `iss` throws only when the AS advertised\n`authorization_response_iss_parameter_supported: true`.\n\nPass the callback URL's `URLSearchParams` so the SDK can read `iss` alongside `code`.\nThe SDK does **not** validate `state`; compare it yourself before calling `finishAuth`:\n\n```typescript\nconst params = new URL(callbackUrl).searchParams;\nif (params.get('state') !== expectedState) throw new Error('state mismatch');\nawait transport.finishAuth(params); // SDK reads `code` + `iss`\n```\n\n`transport.finishAuth(code, iss)` remains supported. Do **not** display `error` /\n`error_description` / `error_uri` from a callback that failed `iss` validation — those\nvalues are attacker-controlled in a mix-up attack.\n\n`discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not\nexactly match the URL it was fetched for (RFC 8414 §3.3). Set\n`skipIssuerMetadataValidation: true` only as a temporary workaround for a known-misconfigured AS.\n\n(`@modelcontextprotocol/server-legacy` AS implementers: `mcpAuthRouter()` now advertises\n`authorization_response_iss_parameter_supported: true` by default and the bundled\nauthorize handler appends `iss` to every redirect issued via `res.redirect(...)` on the\nsupplied `res`. If you emit `Location` another way, append `params.issuer` as `iss`\nyourself; if your callback is issued by an upstream AS you proxy to, set\n`authorizationResponseIssParameterSupported = false` so the metadata does not over-claim.)\n\n#### Dynamic Client Registration defaults (SEP-837, SEP-2207)\n\n`auth()` now resolves `provider.clientMetadata` once via `resolveClientMetadata()` and\napplies defaults to the DCR body: `grant_types` defaults to\n`['authorization_code', 'refresh_token']`; `application_type` is derived from\n`redirect_uris` (loopback / custom URI scheme → `'native'`, else `'web'`). A field you\nset explicitly is never overwritten. The `grant_types` default applies to the DCR body\nonly — it does **not** drive the `offline_access` / `prompt=consent` augmentation on the\nauthorize request; statically-registered and CIMD clients that want that augmentation\nmust set `clientMetadata.grant_types` explicitly. Non-interactive providers (no\n`redirectUrl`) get no `grant_types` default. Direct `registerClient()` callers (⚠\n`@deprecated` — see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) wanting the same\ndefaults pass `resolveClientMetadata(provider)` as `clientMetadata`. DCR\nrejection now throws `RegistrationRejectedError` (carrying `status`, `body`,\n`submittedMetadata`).\n\n#### Token endpoint must use TLS (SEP-2207)\n\n`exchangeAuthorization()`, `refreshAuthorization()`, `fetchToken()`, and the Cross-App\nAccess helpers throw `InsecureTokenEndpointError` when the token endpoint is not\n`https:` (loopback `localhost` / `*.localhost` / `127.0.0.1` / `::1` exempt). `auth()` surfaces this on\nevery path including refresh — switch any plain-`http:` AS on a non-loopback host to\nTLS; there is no opt-out. Storage confidentiality of `refresh_token` remains your\n`saveTokens()` implementation's responsibility.\n\n#### Scope step-up on `403 insufficient_scope` (SEP-2350)\n\n`StreamableHTTPClientTransport` accepts `onInsufficientScope: 'reauthorize' | 'throw'`\n(default `'reauthorize'`). On `'reauthorize'` the transport re-authorizes with the\n**union** of the previously-requested and challenged scope (`computeScopeUnion`); when\nthat union strictly exceeds the current token's granted scope (`isStrictScopeSuperset`),\nthe SDK bypasses the refresh-token branch and forces a fresh authorization request. On\n`'throw'` the transport raises `InsufficientScopeError` and does not re-authorize — set\nthis for `client_credentials` / m2m clients where re-authorization can't widen scope, or\nto gate the consent prompt behind UX. Step-up retries are hard-capped per send\n(`maxStepUpRetries`, default 1). With a non-OAuth [`AuthProvider`](#authprovider--non-oauth-bearer-auth-and-the-widened-authprovider-option),\na `403 insufficient_scope` now throws `InsufficientScopeError` instead of the previous\n`SdkHttpError(ClientHttpNotImplemented)`. The GET listen-stream open path applies the\nsame handling as the POST send path.\n\n#### Credentials bound to the issuing authorization server (SEP-2352)\n\n`auth()` stamps an `issuer` field onto every value it passes to `saveTokens()` /\n`saveClientInformation()` and threads `{ issuer }` as the `ctx` argument to those\nmethods plus `tokens()` / `clientInformation()`. On read, a stored value whose `issuer`\nnames a different AS is treated as `undefined` and the flow re-registers / re-authorizes\n(or throws `AuthorizationServerMismatchError` when the provider has no `saveClientInformation()`).\n**Round-trip the stored object verbatim and you're protected** — single-slot storage\nworks. Dropping the stamp is easy to miss: a `saveTokens()` implementation that\nrebuilds the object field-by-field and drops `issuer` leaves the value unstamped —\nreads still succeed and refresh keeps working, the per-AS issuer check simply does not\napply to that credential, and every read logs an `[mcp-sdk]` warning (`auth()`\nre-stamps on first use where the provider can persist it). If you see that warning\nrepeating after upgrading, check this first. To hold credentials for several authorization servers at once, key your storage\non `ctx.issuer` (treat **`ctx === undefined` as \"return the most-recently-saved token\nset\"** — the transport's per-request `Authorization: Bearer` read calls `tokens()` with\nno `ctx`). `OAuthTokensSchema` / `OAuthClientInformationSchema` keep the optional `issuer`, so\nreading storage back through them is fine; the `StoredOAuthTokens` / `StoredOAuthClientInformation`\naliases name the stored shape.\n\n`OAuthClientProvider.saveAuthorizationServerUrl()` / `authorizationServerUrl()` are\n`@deprecated` (still written for back-compat, never read by the SDK). The bundled\n`ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivateKeyJwtProvider`, and\n`CrossAppAccessProvider` gain `expectedIssuer?: string` (omitting it is deprecated) and no\nlonger define `saveClientInformation()`. Implement `discoveryState()` / `saveDiscoveryState()` so the\ncallback leg can verify it is exchanging the code at the same AS the redirect targeted;\nwithout it the SDK `console.warn`s once per callback (`discoveryState` must persist with\nthe same durability as `codeVerifier`). Both methods are optional on\n`OAuthClientProvider` and may be sync or async; `OAuthDiscoveryState` (exported from\n`@modelcontextprotocol/client`) extends `OAuthServerInfo` with the optional\n`resourceMetadataUrl` the protected-resource metadata was found at:\n\n```typescript\nimport type { OAuthDiscoveryState } from '@modelcontextprotocol/client';\n\n// On OAuthClientProvider:\nsaveDiscoveryState?(state: OAuthDiscoveryState): void | Promise<void>;\ndiscoveryState?(): OAuthDiscoveryState | undefined | Promise<OAuthDiscoveryState | undefined>;\n```\n\n#### Conformance obligations for `OAuthClientProvider` implementers\n\nThe SDK enforces every authorization MUST that lands in SDK code. The following live in\n**your** implementation and the SDK structurally cannot enforce them:\n\n- **Round-trip the `issuer` stamp** on persisted credentials (SEP-2352). Persist the\n  value verbatim from `saveTokens` / `saveClientInformation` and return it verbatim.\n- **Pass `expectedIssuer`** when constructing static-credential providers (SEP-2352).\n- **Keep refresh tokens confidential in storage** (SEP-2207) — OS keychain or\n  encrypted-at-rest store, never `localStorage` / plain files / logs.\n- **Extract `iss` from the callback URL** and pass it to `finishAuth` (SEP-2468); when\n  `IssuerMismatchError` is thrown, do not render the callback's `error*` values.\n- **Set `application_type` correctly** when overriding the heuristic (SEP-837).\n- **Track cross-request step-up failures yourself** (SEP-2350) — `maxStepUpRetries` is\n  per request; per-session backoff is host state.\n- **Persist discovery state**: implement `discoveryState()` / `saveDiscoveryState()` so\n  the authorization-server metadata your tokens were issued against survives restarts.\n- **Choose the insufficient-scope behavior**: keep the default\n  `onInsufficientScope: 'reauthorize'`, or handle `InsufficientScopeError` yourself.\n- **Resource-server operators: do not advertise `offline_access`** in `WWW-Authenticate`\n  `scope` or PRM `scopes_supported` (SEP-2207).\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md (Tokens: ~4337 | Category: anthropic-skill) -->\n### Behavioral changes\n\nThese are runtime-behavior changes that may affect tests and assertions; no source\nrewrite required unless noted.\n\n#### Error-shape changes (every era)\n\n- **Unchanged, for re-baselining relief:** timeout rejections still carry\n  `data.timeout` / `data.maxTotalTimeout` exactly as v1 `McpError` did — v1 assertions\n  on those survive verbatim. The cancelled-on-timeout signal is unchanged on legacy-era\n  connections and on stdio/in-memory at any era; on 2026-era Streamable HTTP the cancel\n  signal is the per-request stream close instead of a `notifications/cancelled` POST\n  (see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The one exemption is the\n  `initialize` handshake: an aborted or timed-out `connect()` still rejects locally, but\n  no `notifications/cancelled` goes on the wire — the spec forbids cancelling\n  `initialize`, and v1 sent one anyway. v1 tests asserting that notification need\n  re-baselining.\n- **Also unchanged: SSE reconnection exhaustion.** `StreamableHTTPClientTransport`'s\n  standalone GET-stream reconnection behavior and its exhaustion signal carry over from\n  v1: when retries run out, the transport emits `onerror` with a plain `Error` whose\n  message is `Maximum reconnection attempts (N) exceeded.` — there is no typed error\n  class for this condition, so monitors that match the message text keep working.\n- **Also unchanged: elicitation response validation.** `elicitInput`'s local validation\n  of elicitation responses against `requestedSchema`, the resulting `-32602` error\n  message wording (`Elicitation response content does not match requested schema: …`),\n  and the `McpServer` / `Client` `jsonSchemaValidator` option carry over from v1 —\n  tests pinning the local-validation message and custom validator wiring need no\n  re-baselining.\n- **Unknown / disabled tool calls now reject** with `ProtocolError(-32602 InvalidParams)`\n  instead of resolving `CallToolResult{isError: true}`. v1 callers that checked\n  `result.isError` for an unknown tool will get an unhandled rejection — catch the\n  rejected promise instead.\n- **The `MCP error <code>: ` message prefix is gone.** v1 prefixed relayed JSON-RPC\n  error messages (`MCP error -32602: …`); v2's `ProtocolError.message` carries the\n  peer's message verbatim. Tests and log scrapers that matched the prefix or the numeric\n  code in rendered text should match `error.code` instead.\n- **In-flight request handlers are aborted on transport close** — `ctx.mcpReq.signal`\n  fires (v1 let them run to completion). `InMemoryTransport.close()` no longer\n  double-fires `onclose` on the initiating side.\n- **`Protocol.request()` with an already-aborted signal** rejects with\n  `SdkError(SdkErrorCode.RequestTimeout, reason)` instead of throwing the raw\n  `signal.reason`, matching the in-flight-abort path.\n- **OAuth discovery (`discoverOAuthProtectedResourceMetadata` / `discoverOAuthMetadata`,\n  transitively `auth()`) throws on fetch `TypeError`** (DNS failure, `ECONNREFUSED`,\n  invalid URL) in Node and Cloudflare Workers instead of swallowing it as a CORS miss\n  → `undefined`. The CORS-swallow remains browser-only.\n\n#### Client connection & dispatch\n\n- **`connect()` skips the `initialize` handshake when the transport already exposes a\n  `sessionId`** — it assumes it is reconnecting to an existing session (unchanged from\n  v1.x, where the same guard has existed since 1.10.0; recorded here because the\n  far-away symptom keeps surprising migrators). A custom or test transport that sets `sessionId` at construction\n  silently skips initialization: `getServerCapabilities()` stays `undefined` and the\n  list verbs return empty results. Expose `sessionId` only after the first request has\n  been sent.\n- **The typed verbs dispatch after async pre-work.** `Protocol.request()` itself still\n  hands the frame to the transport before its first `await` (v1-compatible). The typed\n  verbs on top of it — `callTool()` and the cacheable list verbs — perform async work\n  first (header-mirroring scan, response-cache freshness, output-validator resolution),\n  so an abort fired in the same tick can land before the frame is ever sent: the call\n  rejects with `SdkError(RequestTimeout, reason)` and **no `notifications/cancelled` is\n  emitted** (nothing was in flight). v1 sent the frame synchronously from these verbs.\n  Once the frame is on the wire, aborting still sends `notifications/cancelled` before\n  rejecting.\n- **Protocol-version pinning is a first-class option.**\n  `ProtocolOptions.supportedProtocolVersions` pins the legacy `initialize` handshake:\n  the **first** pre-2026 entry in the list is offered (list order is preference order),\n  a counter-offer is accepted only if it is one of the list's pre-2026 entries, and a\n  list with no pre-2026 entry makes the handshake throw. Under\n  `versionNegotiation: 'auto'` the modern probe candidates are the list's modern\n  entries when it has any (otherwise the SDK's default modern set); a `{ pin }` is\n  honored as given and is not checked against the list (see\n  [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#client-side-versionnegotiation)).\n  v1 had no public equivalent (`SUPPORTED_PROTOCOL_VERSIONS` was a fixed constant) —\n  replace any workaround that patched the offered version with this option.\n- **Also unchanged: HTTP 405 tolerances.** A `405` answering the standalone GET stream\n  open is benign (the client proceeds without the stream), and a `405` answering the\n  session DELETE resolves `terminateSession()` normally — stateless-topology servers\n  that decline both verbs keep working without changes, as in v1.\n\n#### stdio transport\n\n- A configurable `maxBufferSize` (default **10 MB**) caps the stdio read buffer. A\n  single message that would push the buffer past the limit emits `onerror` and\n  **closes the connection** (v1 buffered unbounded). Configure via\n  `new StdioClientTransport({ ..., maxBufferSize })` /\n  `new StdioServerTransport(stdin, stdout, { maxBufferSize })`.\n- `ReadBuffer.readMessage()` now **silently skips non-JSON stdout lines** instead of\n  throwing `SyntaxError` → `onerror`. Hot-reload tools (tsx, nodemon) that write debug\n  output to stdout no longer break the transport. Lines that parse as JSON but fail\n  JSON-RPC schema validation still throw.\n- `StdioClientTransport` always sets `windowsHide: true` when spawning the server\n  process on Windows (previously Electron-only). Prevents stray console windows in\n  non-Electron Windows hosts.\n- Outbound write failures — e.g. the host closing the stdout pipe while a send is\n  pending — now reject the pending `send()` and close the transport through\n  `onerror`/`onclose` instead of surfacing an unhandled stream error; lifecycle\n  tests that pinned a crash-class exit observe a clean shutdown instead.\n\n#### Client list methods\n\n- `listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` return\n  **empty results** when the server didn't advertise the corresponding capability,\n  instead of sending the request. Set `enforceStrictCapabilities: true` in `ClientOptions`\n  to restore the v1 throw.\n- Called **without a `cursor`**, the same methods now **auto-aggregate every page** and\n  return an aggregate with no `nextCursor`. Passing `{ cursor }` still fetches one page. Manual\n  pagination loops keep working (the first iteration returns everything); replace them\n  with the bare no-arg call. The walk is capped at `ClientOptions.listMaxPages` (default\n  64); overrun throws `SdkError(ListPaginationExceeded)`. There is no way to fetch only\n  the **first** page through the typed verbs — for page-level observation\n  (pagination tooling, per-page stats) drop to\n  `client.request({ method: 'tools/list', params })`, which never aggregates.\n- Output-schema validator compilation is now **lazy** — validators compile on the first\n  `callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()`.\n  In v1, `listTools()` threw on an uncompilable `outputSchema`; now `listTools()`\n  succeeds and the compile failure surfaces when `callTool()` is invoked on the affected\n  tool, as `ProtocolError(InvalidParams, \"Tool 'X' has an invalid outputSchema: …\")`,\n  before the request is sent. Validation is never silently skipped.\n- On a 2026-07-28 connection the cacheable verbs honour the server-stamped `ttlMs` /\n  `cacheScope` (SEP-2549) and may return a still-fresh cached entry without a round\n  trip. Per-call override: `{ cacheMode: 'refresh' | 'bypass' }`. New `ClientOptions`:\n  `cachePartition`, `defaultCacheTtlMs`. `ResponseCacheStore` gained `delete(key)`;\n  `InMemoryResponseCacheStore` is now bounded (`{ maxEntries }`, default 512).\n  `CacheEntry.value` (and the `set()` entry value) is the JSON-serialized result\n  document (`string`, not `unknown`): persist and return it verbatim, `JSON.parse`\n  it to inspect.\n\n#### Server (Streamable HTTP transport)\n\n- Resumability behavior (SSE priming events, `closeSSE` / `closeStandaloneSSE`\n  callbacks) is only enabled for protocol versions in the transport's supported-versions\n  list that are `>= 2025-11-25`. Unknown future version strings in an `initialize`\n  request body no longer enable it.\n- Session-ID mismatch still responds `404` with JSON-RPC `-32001` (`Session not found`),\n  unchanged from v1. This `-32001` is an SDK convention, not spec-assigned; client code\n  should key off the HTTP `404` status, not `-32001`.\n\n#### Server (deprecated accessors and app-factory Origin validation)\n\n- `Server.getClientCapabilities()`, `getClientVersion()`, `getNegotiatedProtocolVersion()`\n  are `@deprecated` but functional. On 2026-07-28 requests, prefer `ctx.mcpReq.envelope`.\n- `createMcpExpressApp()` / `createMcpHonoApp()` / `createMcpFastifyApp()` with a\n  localhost-class `host` now also validate the `Origin` header by default. Browser-served\n  clients on a non-localhost origin need `allowedOrigins: [...]` (replaces the default\n  localhost allowlist; validation cannot be disabled for localhost binds). Requests\n  without an `Origin` header are unaffected; a present `Origin` that cannot be parsed\n  — including the opaque **`Origin: null`** sent by sandboxed iframes, `file://` pages,\n  and cross-origin redirects — is **rejected with 403** and cannot be allowlisted via\n  `allowedOrigins`. Framework-agnostic helpers\n  (`validateOriginHeader`, `localhostAllowedOrigins`, `originValidationResponse`) are in\n  `@modelcontextprotocol/server`; `@modelcontextprotocol/node` ships\n  `hostHeaderValidation` / `originValidation` request guards for plain `node:http`.\n\n#### Server (McpServer / Streamable HTTP behavior)\n\n- **Eager capability-handler install.** `McpServer` now installs list/read/call handlers\n  for every primitive capability declared in `ServerOptions.capabilities`, even with\n  zero registrations. `new McpServer(info, { capabilities: { tools: {} } })` with no\n  registered tools answers `tools/list` with `{ tools: [] }` instead of `-32601 Method\nnot found`. Low-level `Server` users remain responsible for registering handlers for\n  declared capabilities — with one exception: declaring the `logging` capability (in\n  the constructor's capabilities or via pre-connect `registerCapabilities()`) installs\n  the `logging/setLevel` handler on the low-level `Server` too, so `logging/setLevel`\n  requests that answered `-32601` in v1 now resolve. Eager install also rewrites the **advertised** capability\n  objects: a declared `tools: {}` / `resources: {}` / `prompts: {}` is advertised with\n  `listChanged: true` at construction, so capability pins and initialize-result golden\n  tests need re-baselining. To advertise without the default, set\n  `listChanged: false` explicitly; capabilities declared on the low-level `Server` are\n  advertised verbatim.\n- **`WebStandardStreamableHTTPServerTransport` store-first `eventStore` semantics.**\n  Request-related events emitted after `closeSSE()` — and the final response when no\n  per-request stream is connected — are now persisted to the configured `eventStore` for\n  replay (v1 dropped them / threw `\"No connection established\"`). Without an\n  `eventStore`, the same condition surfaces via `onerror` and the request id is retired.\n  `NodeStreamableHTTPServerTransport` is a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport`, so this — like every behavioral note on\n  the web-standard transport — applies to the Node transport too.\n- **`registerResource` reserves the `cacheHint` config key.** It is validated\n  (`RangeError` on invalid values) and stripped from the resource's list metadata; v1\n  passed it through verbatim as ordinary metadata. Untyped callers that previously\n  smuggled a `cacheHint` key through resource metadata should rename it.\n\n#### `ctx.mcpReq.log()` is request-related on every era\n\n`ctx.mcpReq.log()` now emits its `notifications/message` request-related (it rides the\nin-flight exchange like progress) on every era. On a 2025-era sessionful Streamable HTTP\ntransport this moves handler-emitted logs from the standalone GET stream onto the\nper-request POST response stream — a spec-conformance correction. The session-scoped\n`logging/setLevel` filter applies as before on 2025-era connections. (On 2026-07-28\nrequests, the per-request `_meta.logLevel` envelope key is the filter — see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#serving-the-2026-07-28-revision).)\n\n#### Wire tightening (every era)\n\n- **`CallToolResult.content` keeps the v1 parse tolerance on the legacy era.** An\n  inbound result without `content` defaults to `[]` (deployed servers omit it\n  alongside `structuredContent`); 2026-07-28 connections stay strict. Authoring is\n  unchanged and era-independent: the TypeScript surface requires `content` on handler\n  results, and a content-less handler result is normalized to `content: []` before it\n  reaches the wire. One sharpening remains: a content-less body carrying another\n  result family's vocabulary (a task handle or an `input_required` round) is still\n  rejected loudly — tolerance never turns a different result kind into a silent empty\n  success. A body whose only foreign key was `resultType` strips to an empty object\n  and defaults, exactly as v1 parsed a payload-free body.\n- **`ElicitResult.content` values are typed and validated as\n  `string | number | boolean | string[]`.** v1's TypeScript surface accepted\n  `Record<string, unknown>` content values; an elicitation handler returning arbitrary\n  objects now fails to compile (and fails schema validation) — narrow to the primitives\n  the elicitation spec allows.\n- **Custom (3-arg) handlers receive `_meta`.** `setRequestHandler(method, {params}, handler)`\n  used to delete `params._meta` before validation; it now passes `_meta` through (minus\n  the reserved `io.modelcontextprotocol/*` envelope keys). If your params schema is\n  strict, add an optional `_meta` member.\n- **`specTypeSchemas` validate the neutral model.** Result entries no longer accept\n  `resultType`; the validators for the 2025-only task message types and\n  `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed accordingly).\n- **Sampling `hasTools` discriminant** now keys on `tools || toolChoice` (previously\n  `tools` only) when selecting the with-tools `CreateMessageResult` variant, on every\n  era.\n- **Inbound frames that fail message-shape validation are not answered.** v2 routes\n  every inbound frame through typed message guards; a frame that matches no JSON-RPC\n  shape (e.g. a hand-built ping with an explicitly-`undefined` `id`, or non-object\n  `params`) is dropped and surfaces only via `onerror` (`Unknown message type: …`) — no\n  response is sent. v1-era test fences that await a reply to a hand-written raw frame\n  hang instead of resolving; send through the typed surface (`client.ping()`,\n  `client.request()`) instead.\n\n#### Experimental tasks interception removed\n\nThe 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`).\nNo mechanical migration; remove usages. Gone: `ProtocolOptions.tasks`,\n`protocol.taskManager`, `RequestOptions.task` / `relatedTask`, `BaseContext.task`,\n`assertTaskCapability` / `assertTaskHandlerCapability`, `*.experimental.tasks.*`\naccessors and `Experimental{Client,Server,McpServer}Tasks`, `requestStream` /\n`callToolStream` / `createMessageStream` / `elicitInputStream` and the `ResponseMessage`\ntypes they yielded, `registerToolTask`, `ToolTaskHandler`, `TaskRequestHandler`,\n`CreateTaskRequestHandler`, `TaskMessageQueue`, `InMemoryTaskMessageQueue`,\n`BaseQueuedMessage` / `Queued*`, `CreateTaskServerContext`, `TaskServerContext`,\n`TaskToolExecution`, `TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`,\nand the `new McpServer(info, { taskStore, taskMessageQueue })` constructor option keys\n(the codemod emits an action-required diagnostic at each — remove the option).\n\nThe task **wire types** remain importable as `@deprecated` vocabulary for 2025-11-25\ninterop — see [support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#tasks-deprecated-wire-vocabulary).\n\n#### Specification clarifications adopted (no SDK behavior change)\n\nThe 2026-07-28 specification revision includes a number of documentation-only\nclarifications recorded here so an audit of the revision's changelog against this guide\nis complete; nothing in this list requires code changes: per-operation timeout guidance\nremoval (`RequestOptions.timeout` / `DEFAULT_REQUEST_TIMEOUT_MSEC` unchanged); stdio\nshutdown wording; transports-as-bindings reframe; `resources/read` wording (the\n`file://` path-sanitization MUST is server-author guidance — your handler must reject\ntraversal / symlink escapes itself); `PromptMessage` resource links (already in\n`ContentBlock`); completion `ref/resource` URI templates; pagination empty-string\ncursors (already passed through verbatim); sampling host-requirement docs; elicitation\nstatefulness wording; cosmetic schema/JSDoc sweeps.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/enhancements.md (Tokens: ~453 | Category: anthropic-skill) -->\n## Enhancements\n\n### Automatic JSON Schema validator selection by runtime\n\nThe SDK auto-selects the validator: Node.js → AJV; Cloudflare Workers (workerd) →\n`@cfworker/json-schema`. Cloudflare Workers users can remove explicit\n`jsonSchemaValidator` configuration. You don't need to install `ajv`, `ajv-formats`, or\n`@cfworker/json-schema` for the default path. To customize the built-in backend, import\nthe named class from the explicit subpath\n(`@modelcontextprotocol/{client,server}/validators/ajv` or `…/cf-worker`) — importing\nfrom a subpath means the corresponding peer dep must be in your `package.json`.\n\n### `Client.connect(transport, { prior })` — connect from a cached era verdict\n\nProbe once, persist `client.getDiscoverResult()` (`JSON.stringify`), and feed it to\nevery worker as `client.connect(transport, { prior: { kind: 'modern', discover } })`.\nNew exported types\n`ConnectOptions` (extends `RequestOptions` with `prior?: PriorDiscovery`)\nand `PriorDiscovery` — a cached era verdict: the modern arm wraps a `DiscoverResult`\n(zero round trips), the legacy arm (`{ kind: 'legacy' }`) skips the probe and runs the\nplain `initialize` handshake for servers known to be pre-2026. Freshness is the\nsupplying host's responsibility — date cached legacy verdicts in your own storage and\nstop supplying them past your policy horizon (a stale one succeeds silently against an\nupgraded server).\n\n### Serving the 2026-07-28 revision\n\n`createMcpHandler`, `serveStdio`, `versionNegotiation`, multi-round-trip requests\n(`requestState`), client cancellation via stream-close, `subscriptions/listen`,\n`Mcp-Param-*` headers, and per-era wire codecs are covered in\n**[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)** — they are net-new in v2, not v1→v2\nbreaks.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/errors.md (Tokens: ~3244 | Category: anthropic-skill) -->\n### Errors\n\nThe SDK now distinguishes three error kinds:\n\n1. **`ProtocolError`** (renamed from `McpError`) — protocol errors that cross the wire\n   as JSON-RPC error responses. Uses `ProtocolErrorCode` (renamed from `ErrorCode`).\n2. **`SdkError`** — local SDK errors that never cross the wire. Uses `SdkErrorCode`.\n3. **`SdkHttpError`** (extends `SdkError`) — HTTP transport errors with typed `.status`\n   and `.statusText`.\n\nThese classes (and `OAuthError`, the client's `SseError`, `UnauthorizedError`, and the\nOAuth-client-flow error family) brand-match under `instanceof`, so checks work across\nseparately bundled copies of the SDK — e.g. a process using both\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server`. Each branded\nhierarchy also exposes the same check as an explicit static guard\n(`SdkError.isInstance(err)`, `ProtocolError.isInstance(err)`, …) that narrows in\nTypeScript — use whichever style your codebase prefers; both read the same brand.\nFine print (applies equally to `instanceof` and `isInstance`):\n\n- **Version skew** — matching needs _both_ copies at a brand-aware release; against an\n  older copy, behavior degrades to plain prototype `instanceof` (false across bundles).\n  During mixed-version rollouts, recognize errors without class identity: match\n  `error.name` plus the class's discriminant field (`code`, `status`), or reconstruct\n  typed protocol errors with `ProtocolError.fromError(code, message, data)`.\n- **Worker boundaries** — `structuredClone`/`postMessage` drop the (symbol-keyed) brand,\n  so a rehydrated error no longer brand-matches; recognize forwarded errors by\n  `code`/`data` instead.\n- **Brands assert identity, not shape** — a matched instance from another SDK version\n  may lack newer fields; read fields defensively.\n- **Re-bundling with property mangling** (`mangle.props` and similar) breaks the brand\n  statics; default esbuild/webpack/terser settings are safe.\n\nThe codemod renames `McpError` → `ProtocolError`, `ErrorCode` → `ProtocolErrorCode`\n(routing `RequestTimeout` / `ConnectionClosed` to `SdkErrorCode`), and\n`StreamableHTTPError` → `SdkHttpError`. After the codemod runs, your `instanceof`\nchecks already name the v2 classes — what's left is choosing which `SdkErrorCode` /\nclass to match per scenario:\n\n| Scenario                                         | v1                                        | v2                                                                 |\n| ------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------ |\n| Request timeout                                  | `McpError` + `ErrorCode.RequestTimeout`   | `SdkError` + `SdkErrorCode.RequestTimeout`                         |\n| Connection closed                                | `McpError` + `ErrorCode.ConnectionClosed` | `SdkError` + `SdkErrorCode.ConnectionClosed`                       |\n| Capability not supported                         | `new Error(...)`                          | `SdkError` + `SdkErrorCode.CapabilityNotSupported`                 |\n| Not connected                                    | `new Error('Not connected')`              | `SdkError` + `SdkErrorCode.NotConnected`                           |\n| Response result fails schema                     | raw `ZodError`                            | `SdkError` + `SdkErrorCode.InvalidResult`                          |\n| Invalid params (server response)                 | `McpError` + `ErrorCode.InvalidParams`    | `ProtocolError` + `ProtocolErrorCode.InvalidParams`                |\n| HTTP transport error                             | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttp*`                        |\n| Failed to open SSE stream                        | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToOpenStream`       |\n| 401 after re-auth (circuit break)                | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| `SSEClientTransport.send()` 401 after re-auth    | `UnauthorizedError`                       | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication`           |\n| 403 `insufficient_scope` after step-up retry cap | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpForbidden`                |\n| Unexpected content type                          | `StreamableHTTPError`                     | `SdkError` + `SdkErrorCode.ClientHttpUnexpectedContent`            |\n| Session termination failed                       | `StreamableHTTPError`                     | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToTerminateSession` |\n\n```typescript\n// v1\nif (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }\nif (error instanceof StreamableHTTPError) { console.log('HTTP status:', error.code); }\n\n// v2\nimport { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/client';\nif (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }\nif (error instanceof SdkHttpError) {\n    console.log('HTTP status:', error.status, error.statusText);\n    switch (error.code) {\n        case SdkErrorCode.ClientHttpAuthentication:\n        case SdkErrorCode.ClientHttpForbidden:\n        case SdkErrorCode.ClientHttpFailedToOpenStream:\n        case SdkErrorCode.ClientHttpNotImplemented:\n            break;\n    }\n}\n```\n\n`StreamableHTTPError` is removed.\n\n**Status read off `.code` by duck-typing.** Code that classified HTTP failures by the\nstatus without an `instanceof` — `if ('code' in e && e.code === 403)` — silently stops\nmatching: on `SdkHttpError` the HTTP status moved to `.status` (its `.code` is a\n`SdkErrorCode` string). The codemod renames `instanceof StreamableHTTPError`, but a\nstatus read that never named the class is invisible to it. Watch the inconsistency:\n`SseError` still carries its HTTP status on numeric `.code`, so one duck-typed\n`.code === 401` that caught both transports in v1 now catches only SSE.\n\n```typescript\n// v1 — one duck-typed check caught both Streamable HTTP and SSE\nif ('code' in e && (e.code === 401 || e.code === 403)) reauth();\n// v2 — match each explicitly\nif (e instanceof SdkHttpError && (e.status === 401 || e.status === 403)) reauth(); // Streamable HTTP\nif (e instanceof SseError && (e.code === 401 || e.code === 403)) reauth(); // SSE still uses .code\n```\n\nSilent at runtime (no compile error) — grep for `.code ===` status comparisons.\n\n**Classification keyed on the error class name.** The same import-free classifiers\noften match by name instead of code: telemetry and allowlists keyed on `error.name` or\n`error.constructor.name` against `'McpError'` / `'StreamableHTTPError'` silently stop\nmatching — the v2 classes are named `ProtocolError`, `SdkError`, and `SdkHttpError`,\nand all three assign `.name` accordingly. One v1 asymmetry disappears along the way:\nv1's `StreamableHTTPError` never assigned `.name` (instances reported `'Error'`), so\n`.name`-keyed matchers saw only `'McpError'`; v2's `SdkHttpError` reports\n`'SdkHttpError'`, and assertions pinning `.name === 'Error'` on transport errors need\nre-baselining. Add the v2 names to your match lists; during a\n[staged migration](packaging-runtime.md#migrating-in-stages-large-codebases) keep the v1 names alongside\nfor as long as the v1 package remains installed.\n\n**Status read out of the message text.** Per transport: the Streamable HTTP message\ntext never carried the status (v1 put it on `.code`, v2 puts it on `.status` — read\n`error.status`), and v2's SSE transport still embeds it exactly as v1 did\n(`Error POSTing to endpoint (HTTP 404): …`). The silent break is **switching\ntransports while keeping a message regex**: a status pattern written against SSE\nmatches nothing on Streamable HTTP. Read `error.status` instead of parsing text.\n\n**Raw numeric code comparisons.** The codemod rewrites `ErrorCode.X` symbol references,\nbut a check against the raw JSON-RPC number — `(e as { code?: unknown }).code === -32000`\n— is invisible to it and silently never matches in v2, because the two SDK-local codes\nit usually targeted are now **string** `SdkErrorCode` values:\n\n| v1 numeric                  | v2                                           |\n| --------------------------- | -------------------------------------------- |\n| `-32000` (ConnectionClosed) | `SdkError` + `SdkErrorCode.ConnectionClosed` |\n| `-32001` (RequestTimeout)   | `SdkError` + `SdkErrorCode.RequestTimeout`   |\n\n- Requests that require a session but omit the `Mcp-Session-Id` header still\n  respond `400` with JSON-RPC `-32000` (`Bad Request: Mcp-Session-Id header is\nrequired`), unchanged from v1 — as with `-32001`, the code is an SDK\n  convention; key off the HTTP status.\n\nReplace the literal with the named code. Loud (`TS2367`) when the compared value is\ntyped `SdkErrorCode`; silent when the left side is `unknown` or a cast — grep for\n`=== -32000` / `=== -32001`.\n\n**Dual-role processes: `instanceof` does not cross the packages.**\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` each bundle their own\ncopy of these error classes, so in a process that uses both — a gateway, a host, an\nin-process test — an error constructed by one package fails `instanceof` against the\nclass imported from the other, silently. When an error may originate from the other\npackage, match on stable fields instead of class identity: `error.code` values\n(`SdkErrorCode` strings for SDK errors, numeric JSON-RPC codes for protocol errors,\n`OAuthErrorCode` strings for OAuth errors) plus presence checks like `'status' in e`,\nor reconstruct typed protocol errors with `ProtocolError.fromError(code, message, data)`\n— it exists precisely because `instanceof` does not survive bundle boundaries.\n\n**Constructing the error (test stubs, custom transports).** v1\n`new StreamableHTTPError(code, message)` becomes\n`new SdkHttpError(code, message, data)`: the first argument is now a `SdkErrorCode`\nstring (pick the branch from the scenario table above) and the HTTP status moves into\nthe third argument — `new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented,\n'Not Found', { status: 404, statusText: 'Not Found' })`. v1's implicit\n`Streamable HTTP error: ` message prefix is gone; pass the full message you want.\n\n#### `SdkErrorCode` enum (complete)\n\n| Code                                  | When thrown                                                                |\n| ------------------------------------- | -------------------------------------------------------------------------- |\n| `NotConnected`                        | Transport is not connected                                                 |\n| `AlreadyConnected`                    | Transport is already connected                                             |\n| `NotInitialized`                      | Protocol is not initialized                                                |\n| `CapabilityNotSupported`              | Required capability is not supported                                       |\n| `RequestTimeout`                      | Request timed out waiting for response                                     |\n| `ConnectionClosed`                    | Connection was closed                                                      |\n| `SendFailed`                          | Failed to send message                                                     |\n| `InvalidResult`                       | Response result failed local schema validation                             |\n| `UnsupportedResultType`               | A 2026-era response carried an unrecognized `resultType`                   |\n| `InputRequiredRoundsExceeded`         | Multi-round-trip auto-fulfilment hit `maxRounds`                           |\n| `ListPaginationExceeded`              | No-arg `list*()` aggregate walk hit `listMaxPages`                         |\n| `MethodNotSupportedByProtocolVersion` | Outbound spec method does not exist on the negotiated protocol version     |\n| `EraNegotiationFailed`                | `connect()` could not negotiate a protocol era (probe failed / no overlap) |\n| `ClientHttpNotImplemented`            | HTTP POST request failed                                                   |\n| `ClientHttpAuthentication`            | Server returned 401 after re-authentication                                |\n| `ClientHttpForbidden`                 | Server returned 403 `insufficient_scope` after step-up retry cap           |\n| `ClientHttpUnexpectedContent`         | Unexpected content type in HTTP response                                   |\n| `ClientHttpFailedToOpenStream`        | Failed to open SSE stream                                                  |\n| `ClientHttpFailedToTerminateSession`  | Failed to terminate session                                                |\n\n#### Typed `ProtocolError` subclasses\n\n`ResourceNotFoundError` (carries `.uri`) and `MissingRequiredClientCapabilityError`\n(carries `data.requiredCapabilities`) are new typed `ProtocolError` subclasses.\n`resources/read` for an unknown URI now answers `-32602` on every protocol revision\n(v1.x already emitted `-32602`; an interim `-32002` from earlier v2 alphas is mapped at\nthe encode seam — `2.0.0-alpha.3` and earlier predate the mapping and still emit\n`-32002` on the wire, so accept both if peers may run those alphas; `2.0.0-alpha.4`\nand later emit `-32602`). The encode-seam mapping applies to **your own throws too**: a handler\nthat deliberately throws `ProtocolError(ProtocolErrorCode.ResourceNotFound, …)` reaches\npeers as `-32602` — a server can no longer emit `-32002` on the wire.\n`ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as\nreceive-tolerated vocabulary — accept both `-32602` and `-32002` from peers.\n`ProtocolError.fromError(code, message, data)` reconstructs the typed subclass from\ncode + data alone — the version-agnostic path: it also works on plain wire shapes and\nagainst SDK copies that predate brand-matched `instanceof`.\nThe default message text changed alongside: v1's unknown-resource error read\n`Resource <uri> not found`; v2's `ResourceNotFoundError` default is\n`Resource not found: <uri>` (the code is unchanged). Tests pinning the exact string\nneed re-baselining — prefer matching `error.code` plus a URI substring (or the typed\n`error.uri`).\n\nCustom **non-spec** codes pass through untouched: a handler that throws a\n`ProtocolError` with a custom code (e.g. `-1`) and `data` reaches the peer as a\nJSON-RPC error with that code and `data` unchanged — the encode seam rewrites only the\nlegacy `-32002` code; `data` is sent verbatim for every thrown error (the typed\nsubclasses shape their `data` at construction, not at encode time). Construct via\n`ProtocolError.fromError(code, message, data)`.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/http-headers.md (Tokens: ~727 | Category: anthropic-skill) -->\n### HTTP & headers\n\nHeader **reads** use the Web Standard `Headers` object (`IsomorphicHeaders` is\nremoved): `ctx.http?.req` is a standard Web `Request`, so\n`ctx.http?.req?.headers` takes `.get()` instead of bracket access.\n\n```typescript\n// v1\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = extra.requestInfo?.headers['mcp-session-id'];\n\n// v2 — requestInit is unchanged; only the header *read* changes\nconst transport = new StreamableHTTPClientTransport(url, {\n    requestInit: { headers: { Authorization: 'Bearer token' } }\n});\nconst sessionId = ctx.http?.req?.headers.get('mcp-session-id');\nconst debug = new URL(ctx.http!.req!.url).searchParams.get('debug');\n```\n\nOn the **write** side, `requestInit` on `StreamableHTTPClientTransport` /\n`SSEClientTransport` options is a standard fetch `RequestInit`, so `headers` accepts\nany `HeadersInit` — a plain object record (as above), a tuple array, or a `Headers`\ninstance all keep working unchanged; the transports normalize whichever form they\nreceive. Wrapping with `new Headers()` is optional, not required.\n\n`StreamableHTTPClientTransport` now **appends** any custom `requestInit.headers.Accept`\nvalue to the spec-required `application/json, text/event-stream` (v1 let it replace\nthem). The required media types are always present; additional types are kept for\nproxy/gateway routing.\n\nTransport-managed headers now take precedence over same-named entries in\n`requestInit.headers`: `Authorization` when `authProvider` yields a token,\n`mcp-protocol-version`, and (Streamable HTTP) `mcp-session-id`. v1 let the configured\nheader win, so a static `Authorization` placeholder kept overriding the OAuth token even\nafter the provider obtained one. A configured `Authorization` value is still sent while\nthe provider has no token, which is what lets a static API key fall back to OAuth.\n\n`hostHeaderValidation()` and `localhostHostValidation()` moved to\n`@modelcontextprotocol/express`. The `(allowedHostnames: string[])` signature is the\nsame as every released v1.x — only the import path changes. Framework-agnostic helpers\n(`validateHostHeader`, `localhostAllowedHostnames`, `hostHeaderValidationResponse`) are\nin `@modelcontextprotocol/server`.\n\nServer entries validate the request `Content-Type` by its **parsed media type**, not a\nsubstring: every POST whose media type is not `application/json` answers\n`415 Unsupported Media Type`. Previously any value merely containing the substring\npassed (for example `text/plain; a=application/json`), case variants were wrongly\nrejected, and the 2026-07-28 entry did not inspect `Content-Type` at all — so\nhand-rolled clients that omit the header (or send a non-JSON type) must now set\n`Content-Type: application/json`. Parameters (`; charset=utf-8`) and unambiguous\nvalues with malformed parameter sections (`application/json;`) keep working; SDK\nclients always sent the correct header and are unaffected. Custom entries that\ncompose `classifyInboundRequest` / `PerRequestHTTPServerTransport` directly must\napply the same validation themselves — use the exported `isJsonContentType(header)`.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/imports-transports.md (Tokens: ~1396 | Category: anthropic-skill) -->\n### Imports & transports\n\nThe codemod rewrites every `@modelcontextprotocol/sdk/...` import path via\n[`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).\nA few transports need a decision the codemod can't make:\n\n- **`StreamableHTTPServerTransport` → which runtime?** The codemod renames it to\n  `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. If you deploy\n  to a web-standard runtime (Cloudflare Workers, Deno, Bun), use\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`\n  instead. **Decision rule:** if your handler receives a Node `IncomingMessage` /\n  `ServerResponse`, use `@modelcontextprotocol/node`; if it receives a web-standard\n  `Request` and returns a `Response`, use `@modelcontextprotocol/server`.\n- **stdio transports moved to a `./stdio` subpath.** Import `StdioClientTransport`,\n  `getDefaultEnvironment`, `DEFAULT_INHERITED_ENV_VARS`, and `StdioServerParameters`\n  from `@modelcontextprotocol/client/stdio`; import `StdioServerTransport` from\n  `@modelcontextprotocol/server/stdio`. The package root barrels do **not** export\n  these (the root entries are runtime-neutral so browser/Workers bundlers can consume\n  them). The stdio utilities `ReadBuffer`, `serializeMessage`, `deserializeMessage`\n  stay in the root barrel.\n- **Zod `*Schema` constants → `@modelcontextprotocol/core`.** A mixed\n  `import { CallToolResult, CallToolResultSchema } from '…/types.js'` is split by the\n  codemod — see [Types & schemas](types-schemas.md#types--schemas).\n\n    ```typescript\n    // v1\n    import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n    // v2\n    import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';\n    ```\n\n- **`SSEServerTransport`** is removed. Migrate to Streamable HTTP. A frozen v1 copy is\n  available from `@modelcontextprotocol/server-legacy/sse` as a temporary bridge.\n- **`WebSocketClientTransport`** is removed (WebSocket is not a spec transport). Use\n  `StreamableHTTPClientTransport` for remote servers or `StdioClientTransport` for\n  local servers; the `Transport` interface is exported if you need a custom\n  implementation.\n- **`InMemoryTransport`** is now exported from `@modelcontextprotocol/client` and\n  `@modelcontextprotocol/server` (both re-export it). The two packages bundle separate\n  copies with private state, so the halves of a linked pair must come from the **same\n  package's** import — pick one package per file (per linked pair) rather than mixing\n  the client's `InMemoryTransport` with the server's:\n\n    ```typescript\n    // v1\n    import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';\n    // v2\n    import { InMemoryTransport } from '@modelcontextprotocol/server'; // or /client\n    ```\n\n- **`EventStore`, `StreamId`, `EventId`** are exported from `@modelcontextprotocol/server`\n  only (v1 re-exported them alongside the transport from `sdk/server/streamableHttp.js`;\n  `@modelcontextprotocol/node` does not).\n- **Client fetch middleware moved to the root barrel.** `createMiddleware`,\n  `applyMiddlewares`, `withLogging`, `withOAuth`, and the `Middleware` type (v1:\n  `sdk/client/middleware.js`) are now exported from `@modelcontextprotocol/client`\n  directly, as is `FetchLike` (v1: `sdk/shared/transport.js`). The call signatures are\n  unchanged from v1 (`Middleware` is still `(next: FetchLike) => FetchLike`) — only the\n  import path changes.\n- **Server auth split.** Resource Server helpers (`requireBearerAuth`,\n  `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`)\n  → `@modelcontextprotocol/express`; the runtime-neutral core (`requireBearerAuth`\n  for web-standard `fetch` hosts, `verifyBearerToken`, `bearerAuthChallengeResponse`,\n  `OAuthTokenVerifier`, and the discovery serving `oauthMetadataResponse` /\n  `buildOAuthProtectedResourceMetadata` / `getOAuthProtectedResourceMetadataUrl`)\n  is also exported from `@modelcontextprotocol/server`. Authorization Server helpers (`mcpAuthRouter`,\n  `OAuthServerProvider`, `ProxyOAuthServerProvider`, `allowedMethods`,\n  `authenticateClient`, `metadataHandler`, `createOAuthMetadata`,\n  `authorizationHandler` / `tokenHandler` / `revocationHandler` /\n  `clientRegistrationHandler`) → `@modelcontextprotocol/server-legacy/auth`\n  (deprecated, frozen v1 copy); migrate AS to a dedicated IdP/OAuth library. `AuthInfo`\n  is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.\n\n    The codemod's [`importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)\n    routes every `…/server/auth/**` deep path (including\n    `…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`,\n    `…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to\n    `@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` /\n    `…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The\n    AS→`server-legacy` routing is conservative — re-point RS-only call sites\n    (`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand.\n    Staying on the frozen `server-legacy/auth` copy is a supported interim choice when you\n    deliberately want the v1 middleware behavior. If you re-point at\n    `@modelcontextprotocol/express` by hand, also add that package — plus its `express`\n    peer dependency — to your manifest: the codemod's manifest summary reflects only the\n    imports it wrote, not re-points you make afterwards.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/packaging-runtime.md (Tokens: ~2853 | Category: anthropic-skill) -->\n### Packaging & runtime\n\nThe single `@modelcontextprotocol/sdk` package is split:\n\n| v1                              | v2                                                                                                                              |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk`     | `@modelcontextprotocol/client` (client implementation)                                                                          |\n|                                 | `@modelcontextprotocol/server` (server implementation)                                                                          |\n|                                 | `@modelcontextprotocol/core` (public Zod `*Schema` constants)                                                                   |\n|                                 | `@modelcontextprotocol/core-internal` (internal — never import directly)                                                        |\n| Built-in HTTP framework support | `@modelcontextprotocol/node` / `@modelcontextprotocol/express` / `@modelcontextprotocol/hono` / `@modelcontextprotocol/fastify` |\n\n`@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared\ntypes from `@modelcontextprotocol/core-internal`, so import types and error classes from\nwhichever package you already depend on. `@modelcontextprotocol/core-internal` is\n`private: true` and is not published — **do not import from it directly.**\n`@modelcontextprotocol/core` is the public Zod-schema package (raw `*Schema` constants\nonly); see [Zod `*Schema` constants moved to `@modelcontextprotocol/core`](types-schemas.md#zod-schema-constants-moved-to-modelcontextprotocolcore) below.\n\nAfter the codemod runs, review the manifest summary it prints: the swap rewrites the\n**nearest** manifest found walking up from the target directory — one manifest total.\nWorkspace-member manifests in a monorepo are never modified; instead the codemod lists\neach member that still declares the v1 SDK together with the exact dependency changes\nit needs (remove the v1 entry, add the v2 packages that member's imports use) — apply\nthose edits yourself, then install. The v2 additions are computed from the final import\nstate of each package's sources, so already-migrated sources still receive the v2\npackages they need when the v1 dependency is removed. In a hoisted monorepo (members\nwithout their own SDK dependency), member usage counts toward the manifest that\ndeclares the v1 SDK, and the summary notes which members contributed. See\n[Monorepo workspace members](#monorepo-workspace-members) for how to decide each\nmember's packages.\n\n#### Monorepo workspace members\n\nDeclare in every member exactly what its own sources import: files importing\n`@modelcontextprotocol/server` (or its subpaths) need `@modelcontextprotocol/server`;\nclient imports need `@modelcontextprotocol/client`; raw `*Schema` constants need\n`@modelcontextprotocol/core`; a framework adapter import (`@modelcontextprotocol/express`\netc.) needs the adapter package **plus the framework itself** in that member (the\nadapter declares it as a peer dependency). Place a package in `dependencies` when\nshipped runtime code imports it and in `devDependencies` when only tests, fixtures, or\nlocal tooling do — when in doubt, use the section where the member previously declared\n`@modelcontextprotocol/sdk`.\n\nA member that never declared the v1 SDK and resolved it through the root can keep\nroot-level declarations (the codemod's root rewrite already adds the union of the\ncontributing members' v2 packages — its hoisting note names them) or move to\nper-member declarations; per-member is recommended, since the v2 package split makes each member's\nactual needs explicit. To answer \"which packages does this member need\" directly, run\nthe codemod against that member's directory with `--dry-run`: the manifest summary is\ncomputed from that member's own imports. (The authoritative import-path routing lives\nin the codemod's [mapping file](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).)\n\nThe framework adapter packages declare their framework as a **peer dependency**\n(`express`, `hono`, `fastify`); v1 shipped them as direct deps. The codemod adds the\n`@modelcontextprotocol/*` packages your imports use, but does not add the framework\npeer — install it explicitly (`pnpm add express` etc.). `@modelcontextprotocol/node`\ndepends on `@hono/node-server` at runtime (Node HTTP ↔ Web Standard conversion) and\ninstalls `hono` as a regular dependency only because `@hono/node-server` requires it as\na peer — the adapter does not use the `hono` framework itself.\n\nv2 requires **Node.js 20+**. It is ESM-first but ships a **CommonJS build alongside\nESM**, so CommonJS projects can `require('@modelcontextprotocol/…')` directly — no\ndynamic `import()` shim required.\n\nRepo-local tooling that encodes the literal v1 package name — dependency-pin lints,\nversion allowlists, CI checks, scripts — fails after the manifest swap and is invisible\nto the codemod (it rewrites sources and manifests, not bespoke gates). Grep for\n`@modelcontextprotocol/sdk` outside `src/` before declaring the migration done. While\ngrepping, also remove v1-era double casts on SDK types (`as unknown as Transport` and\nsimilar, usually annotated to a v1 issue) — v2's types satisfy those contracts\ndirectly, and a surviving cast keeps suppressing type checking that would otherwise\ncatch real errors.\n\nTooling that pins SDK **dist text** (reading a constant out of a built file with\n`require.resolve` + a regex) breaks in two stacked ways: the literal usually lives in a\ncontent-hashed sibling chunk (`dist/sse-<hash>.mjs`), not the subpath's entry module,\nso fixed-path reads do not survive a rebuild — scan the package's `dist/` directory\nfor the literal instead; and the emitted quote style differs from v1, so a\nquote-anchored pattern misses silently — match either quote. The build layout also\nchanged: v2 emits `.mjs`/`.cjs` siblings in a flat `dist/`, so v1's `/dist/cjs/` ↔\n`/dist/esm/` flavor-pair path swaps have no equivalent.\n\n#### Registry availability\n\nAll v2 packages are published on the public npm registry. Two notes:\n\n- As of `2.0.0-beta.1` all v2 packages share one version number (earlier alphas\n  did not). The codemod writes ranges that match what is published, so prefer its\n  manifest output over hand-pinning every package.\n- Environments that resolve through a corporate or private registry mirror may not\n  have synced the newer scoped packages yet (the symptom is \"not found\" for a package\n  that exists on npmjs.org). Point the install at the public registry\n  (`npm install --registry=https://registry.npmjs.org/` or the equivalent `.npmrc`\n  entry), ask your mirror's operators to sync the `@modelcontextprotocol` scope, or —\n  where neither is possible — build a tarball from a checkout of this repository\n  (`pnpm install && pnpm build`, then `pnpm pack` in the package directory) and\n  reference it with a committed `file:` dependency.\n\n#### CommonJS test runners (Jest)\n\nv2 ships a CommonJS build, so CJS test runners resolve the packages natively through the\n`require` export condition — Jest (including `next/jest` setups) no longer needs a\n`moduleNameMapper` workaround to import `@modelcontextprotocol/*`. If you carried a\nv1-era mapping that pinned these packages to their `dist/*.mjs` files, remove it. Vitest\nand native Node ESM are unaffected.\n\n#### Bundlers: nested `zod` copies in zod@3-pinned monorepos\n\nv1's `zod ^3.25 || ^4.0` peer range deduplicated onto a workspace's hoisted zod@3. The\nv2 packages depend on `zod ^4.2.0`, so in a workspace that pins zod@3 the dependency\ncannot dedupe — each installed v2 package resolves its own nested zod@4 copy. Two\nbundler consequences:\n\n- **Path-substring vendor pins capture the nested copies.** Bundler rules that match\n  zod by module path — `manualChunks` pins, vendor-chunk matchers, bundle budgets keyed\n  on a `zod/` path segment — also match `@modelcontextprotocol/*/node_modules/zod`,\n  which can pull the nested copies into an eagerly-loaded vendor chunk and trip a\n  budget gate. Exclude the SDK-nested paths from such pins so the copies ride with the\n  SDK's own (typically lazy) chunks.\n- **Ballpark size cost.** Measured on a large production SPA, adding the v2 client and\n  server packages (with their nested zod@4 copies) alongside a hoisted zod@3 cost\n  roughly +83 KB gzipped of total JS (about +0.7% whole-app). Upgrading the workspace\n  to `zod ^4.2.0` re-dedupes and removes the duplication.\n\n#### Migrating in stages (large codebases)\n\nThe v1 package and the v2 packages have **different names**, so both can be installed\nin one manifest at the same time — nothing forces a one-shot swap. The safe order for\nan incremental migration: (1) add the v2 packages (and the `zod ^4.2.0` bump) while\n**keeping** `@modelcontextprotocol/sdk`; (2) rewrite sources incrementally,\ndirectory-by-directory or package-by-package; (3) remove the v1 dependency only when\nnothing imports it any more (`grep -rn \"@modelcontextprotocol/sdk\" --include=\"*.ts\"`,\nplus a look at `package.json`). The inverse order strands files: swapping the manifest\nfirst leaves every not-yet-rewritten import failing module resolution (TS2307) until it\nis updated.\n\nTwo caveats for the transition window. First, a codemod run against a subdirectory\nstill updates the nearest manifest walking up — including removing the v1 dependency —\nso during a staged pass review or revert that edit until the final stage (or preview\nwith `--dry-run`). Second, v1 and v2 modules each have their own classes and types:\nobjects must not flow between v1-imported and v2-imported code (`instanceof` and\nnominal types do not cross — the same boundary described for dual-role processes in\n[Errors](errors.md#errors)), so stage along process or transport boundaries where the two sides\nshare only the wire format; the two sides negotiate\na protocol version through the ordinary 2025-era `initialize` handshake and settle\non the newest revision both packages support (currently 2025-11-25 — published v1\n1.29.x and v2 ship the same supported-version list).\n\nDependencies you do not control (vendored fixtures, third-party packages) that still\ndeclare `@modelcontextprotocol/sdk` resolve their own v1 copy and need no action. For\n`peerDependencies` declarations, keep the v1 package installed to satisfy the range —\nor point the name at a chosen version via your package manager's\n`overrides`/`resolutions` — until those packages migrate. The same boundary rule\napplies: objects must not flow between their v1-imported code and your v2-imported\ncode.\n\n**Dependencies that compile against the host's v1 SDK.** A stricter variant of the\nabove: a workspace or vendored package that ships TypeScript **source** importing\n`@modelcontextprotocol/sdk` — resolved from the host's `node_modules` rather than its\nown — pins the host. Keep the v1 package installed as a real dependency (not merely a\nsurviving transitive) until that package migrates. The host files that construct or\nhand objects to such a package are part of its v1 boundary and must stay on v1 imports\n— and the codemod cannot see that distinction: it rewrites them like any other file\n(e.g. converting a `setRequestHandler(Schema, …)` call into the v2 method-string form\nagainst what is still a v1 `Server`, which then fails at runtime). Run the codemod with\n`--ignore` glob patterns covering those interfacing files, and migrate them together\nwith the dependency later. The boundary rule above applies unchanged: objects from the\ndependency's v1 modules must never flow into v2-imported code.\n\n#### Library authors: peer-depending on the SDK\n\nIf your package declares `@modelcontextprotocol/sdk` as a `peerDependency`, the v2\npackages are differently **named**, so swapping the peer declaration is itself a\nbreaking change for every consumer — ship it as a semver-major. You can migrate\nahead of your consumers only if no SDK object crosses your public API (the\nv1/v2 boundary rule above applies to your exports too: a v1-constructed `Client`\nor error instance handed to v2-importing consumer code fails `instanceof` and\nnominal checks). Until your consumers migrate, they can keep resolving your peer\nrange with the v1 package installed alongside their own v2 packages — the two\ncoexist under different names.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/protocol-ctx.md (Tokens: ~3217 | Category: anthropic-skill) -->\n### Low-level protocol & handler context (`ctx`)\n\nThe second parameter to every request handler — previously the flat `RequestHandlerExtra`\nobject named `extra` — is now a structured **context** object named `ctx`. This is the\n`ctx` that appears throughout the rest of this guide.\n\nThe codemod renames the parameter and remaps property access via\n[`contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts).\nA few mappings need optional-chaining adjustment (the `http` group is `undefined` on\nstdio):\n\n| v1 (`extra.*`)                                    | v2 (`ctx.*`)                   | Note                                                                                                                                                                                                                                                                                                                    |\n| ------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `extra.signal`                                    | `ctx.mcpReq.signal`            |                                                                                                                                                                                                                                                                                                                         |\n| `extra.requestId`                                 | `ctx.mcpReq.id`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra._meta`                                     | `ctx.mcpReq._meta`             |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendRequest(...)`                          | `ctx.mcpReq.send(...)`         |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sendNotification(...)`                     | `ctx.mcpReq.notify(...)`       |                                                                                                                                                                                                                                                                                                                         |\n| `extra.sessionId`                                 | `ctx.sessionId`                |                                                                                                                                                                                                                                                                                                                         |\n| `extra.authInfo`                                  | `ctx.http?.authInfo`           | optional — `undefined` on stdio                                                                                                                                                                                                                                                                                         |\n| `extra.requestInfo`                               | `ctx.http?.req`                | a standard Web `Request`; `ServerContext` only                                                                                                                                                                                                                                                                          |\n| `extra.closeSSEStream`                            | `ctx.http?.closeSSE`           | `ServerContext` only; the member itself is also optional — defined only when the transport has an `eventStore` AND the client's negotiated protocol version supports resumable close (2025-11-25+); an `eventStore` transport serving a 2025-06-18 client still leaves it `undefined`. Call as `ctx.http?.closeSSE?.()` |\n| `extra.closeStandaloneSSEStream`                  | `ctx.http?.closeStandaloneSSE` | `ServerContext` only; member optional as above — `ctx.http?.closeStandaloneSSE?.()`                                                                                                                                                                                                                                     |\n| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed_                      | see [Experimental tasks](behavioral-changes.md#experimental-tasks-interception-removed)                                                                                                                                                                                                                                 |\n\nThe transport-level seam behind `ctx.http?.authInfo` is unchanged from v1: a transport\nthat passes `{ authInfo }` as the second argument to `onmessage(message, extra)` — e.g.\nan `InMemoryTransport` test seam — still surfaces it as `ctx.http?.authInfo` on any\ntransport, and `ctx.http` is defined whenever `authInfo` is supplied, even without an\nHTTP transport.\n\n`BaseContext` is the common base; `ServerContext` and `ClientContext` extend it. None\nof the three takes type parameters — v1's `RequestHandlerExtra<TRequest, TNotification>`\narguments selected request/notification unions that the v2 context carries\nintrinsically, so their removal loses no type information; review only handlers that\npassed custom (non-standard) unions, whose `sendRequest` / `sendNotification` typing\nwas narrowed by them. `ServerContext.mcpReq` adds convenience methods that replace\ncalling `server.*` from inside a handler:\n\n| `ctx.mcpReq.*` (new)                           | Replaces (inside a handler)                                                                                                                                                                                                                                                                              |\n| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ctx.mcpReq.log(level, data, logger?)`         | `server.sendLoggingMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577); the notification also becomes request-related on every era — see [§`ctx.mcpReq.log()` is request-related on every era](behavioral-changes.md#ctxmcpreqlog-is-request-related-on-every-era) |\n| `ctx.mcpReq.elicitInput(params, options?)`     | `server.elicitInput(...)`                                                                                                                                                                                                                                                                                |\n| `ctx.mcpReq.requestSampling(params, options?)` | `server.createMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577)                                                                                                                                                                                                  |\n\n#### Deprecated in v2 (SEP-2577)\n\nThe roots, sampling, and logging subsystems are deprecated as of protocol version\n2026-07-28 (SEP-2577). Everything below is **still fully functional in v2** and marked\n`@deprecated` for removal in a later major; on a 2026-07-28 connection prefer the\n[multi-round-trip `input_required` pattern](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#multi-round-trip-requests)\ninstead.\n\n- **Runtime APIs**: `Server.createMessage` / `listRoots` / `sendLoggingMessage`,\n  `McpServer.sendLoggingMessage`, `Client.setLoggingLevel` / `sendRootsListChanged`, and\n  the `ctx.mcpReq.log` / `ctx.mcpReq.requestSampling` handler-context helpers. Outside a\n  handler, `McpServer` users reach the `Server.*` methods via the unchanged\n  [`mcpServer.server` accessor](unchanged-apis.md#unchanged-apis).\n- **Capability fields**: the `roots`, `sampling`, and `logging` capability schema fields.\n- **Type stacks**: the full Logging stack (`LoggingLevel`, `SetLevelRequest`,\n  `LoggingMessageNotification` and params), the full Sampling stack\n  (`CreateMessageRequest`/`Result`, `SamplingMessage`, `ModelPreferences`/`ModelHint`,\n  `ToolChoice`, `ToolUseContent`/`ToolResultContent`, the `includeContext` enum values),\n  and the full Roots stack (`Root`, `ListRootsRequest`/`Result`,\n  `RootsListChangedNotification`).\n- **`registerClient`** (Dynamic Client Registration) — deprecated via spec PR\n  [modelcontextprotocol#2858](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858)\n  rather than SEP-2577 (listed here because the `@deprecated` annotations landed in\n  the same sweep); prefer Client ID Metadata Documents per SEP-991.\n\nThe deprecation is annotation-only — JSDoc `@deprecated` markers were added, nothing\nelse: every deprecated runtime API keeps its v1 call signature (e.g.\n`Server.sendLoggingMessage(params, sessionId?)` keeps the two-argument form) and its\nwire behavior, and remains functional for at least the twelve-month deprecation window.\n\n#### `setRequestHandler` / `setNotificationHandler` use method strings\n\nThe low-level handler registration takes a **method string** instead of a Zod schema.\nThe codemod rewrites every spec-method registration via\n[`schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts).\n\n```typescript\n// v1\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => { ... });\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => { ... });\n```\n\n**Custom (non-spec) methods** use the 3-arg form `(method, { params, result? }, handler)`\nwhere `params` and `result` are any [Standard Schema](https://standardschema.dev). The\nhandler receives the parsed `params` directly (not the full request envelope); `_meta`\nis at `ctx.mcpReq._meta`. The 3-arg notification handler is `(params, notification) => void`.\n\n```typescript\nserver.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => { ... });\n```\n\nThe custom form also covers **spec method names carried with custom payloads**: a v1\nintegration that reused a spec method string for its own payload shape (e.g.\n`notifications/message` notifications carrying a proprietary params object) registers\nit with the 3-arg form and its own schema. The overloads are selected by the arguments'\nshape, not by the method name — a schemas object as the second argument always selects\nthe custom form, which validates against **your** schema (the spec schema is not\napplied) and hands the handler the parsed params rather than the envelope.\n\n**Spec notifications** use the 2-arg form `setNotificationHandler(method, handler)`.\nUnlike the 3-arg custom form, the spec-form handler receives the **full notification\nenvelope** (`{ method, params }`), parsed against the spec schema — read\n`notification.params`:\n\n```typescript\nclient.setNotificationHandler('notifications/tools/list_changed', async notification => {\n    console.log(notification.method, notification.params);\n});\n```\n\nThe two overloads are selected by the method string's **type**: the spec form binds the\nmethod to the `NotificationMethod` union (`RequestMethod` on the request side — both\nexported), so a method string computed at runtime must be typed as `NotificationMethod`\nto select it; an untyped `string` lands on the custom-schema overload and fails to\ncompile without a schemas argument. `Parameters<Client['setNotificationHandler']>[0]`\nalso resolves to the custom `string` overload by design — name `NotificationMethod`\ndirectly instead. The request side has the same trap one slot over:\n`Parameters<Client['setRequestHandler']>` (and `typeof`-indexed casts over the overload\nset) resolve against the 3-arg custom-method overload, so index `[1]` is the\n`{ params, result }` schemas object, **not** the handler — v1 signature-erasing handler\ncasts derived positionally change meaning with no runtime symptom. Name the exported\ntypes (`RequestMethod` and your own handler/param types) instead of deriving them\npositionally. Generic helpers that v1 parameterized on a notification schema need\nthis conversion by hand; the codemod only warns on them.\n\n**Handler returns are spec-typed.** In v1 the handler's return type flowed from the\nschema you registered; v2 types it from the method name (`'tools/list'` →\n`ListToolsResult`, and so on). Tool tables kept as plain object literals surface two\nrecurring compile errors: an unannotated literal widens `type: 'object'` to `string`\nand no longer satisfies the spec type's `type: 'object'` literal member (fix:\n`type: 'object' as const`, or annotate the table as `Tool[]`); and a heterogeneous\ntable whose inferred union carries `prop?: undefined` members does not satisfy the spec\ntypes' `Record<string, JSONValue>` index signatures, since `undefined` is not a\n`JSONValue` (fix: annotate the handler's return type —\n`async (req): Promise<ListToolsResult> => …` — or the table itself, so each literal is\nchecked against the target type instead of being inferred and widened first).\n\n#### `request()`, `ctx.mcpReq.send()`, and `callTool()` no longer require a schema for spec methods\n\nFor **spec** methods, drop the result-schema argument; the SDK resolves it from the\nmethod name. The codemod drops it from `client.request()` and `client.callTool()`; drop\nit from `ctx.mcpReq.send()` by hand.\n\n```typescript\n// v1\nimport { CreateMessageResultSchema } from '@modelcontextprotocol/sdk/types.js';\nserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n    const r = await extra.sendRequest({ method: 'sampling/createMessage', params: { ... } }, CreateMessageResultSchema);\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n\n// v2\nserver.setRequestHandler('tools/call', async (request, ctx) => {\n    const r = await ctx.mcpReq.send({ method: 'sampling/createMessage', params: { ... } });\n    return { content: [{ type: 'text', text: 'done' }] };\n});\n```\n\nFor **custom (non-spec)** methods, keep the result-schema argument:\n`await client.request({ method: 'acme/search', params }, SearchResult)` — only drop the\nschema when calling a spec method.\n\n**Forwarding arbitrary methods (gateways / proxies).** Dropping the schema changes\nsemantics, not just the signature: a schema-less spec-method call now **enforces** the\nspec result schema (a non-conforming upstream result is rejected locally with\n`SdkError(SdkErrorCode.InvalidResult)` and a conforming one is re-serialized in schema\nkey order), and a schema-less call for a **non-spec** method throws a `TypeError` at\nthe call site (`'…' is not a spec method; pass a result schema`).\nA relay that forwards `{ method, params }` it does not understand must keep passing an\nexplicit result schema. The v1 idiom survives with an import-path change:\n\n```typescript\nimport { ResultSchema } from '@modelcontextprotocol/core';\nconst result = await upstream.request({ method, params }, ResultSchema); // v1-identical passthrough\n```\n\nFor byte-exact forwarding (member order preserved), pass your own accept-anything\nStandard Schema instead. Check call sites whose `method` is **not a literal** — the\ncodemod may have dropped the schema argument there; restore it.\n\nThe **inbound half** — a relay re-emitting an upstream JSON-RPC error from its own\nhandler — has a supported surface too: reconstruct the typed error with\n`ProtocolError.fromError(code, message, data)` and throw it; the encode seam serializes\nit back to the wire shape (see [Typed `ProtocolError` subclasses](errors.md#typed-protocolerror-subclasses)).\nNote this is typed reconstruction, not byte-exact relay: legacy codes are normalized at\nthe encode seam (`-32002` re-emits as `-32602`) and the typed subclasses keep only their\nschema-defined `data` members, so extra upstream data keys are dropped. Throwing a plain\nobject carrying `.code` / `.message` / `.data` happens to work today, but it is\nunspecified behavior — prefer `fromError`.\n\nThe return type is inferred from the method name via `ResultTypeMap` (e.g.\n`client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`).\nv1 call sites that passed `CreateMessageResultWithToolsSchema` explicitly need no\nreplacement: the schema-less send resolves to\n`CreateMessageResult | CreateMessageResultWithTools`, and validation selects the\nwith-tools variant when the request set `tools` or `toolChoice`.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md (Tokens: ~2541 | Category: anthropic-skill) -->\n### Server registration API\n\nThe deprecated variadic `.tool()`, `.prompt()`, `.resource()` are removed. Use\n`registerTool` / `registerPrompt` / `registerResource` with an explicit config object.\nThe codemod converts the call shape and wraps `inputSchema` / `outputSchema` /\n`argsSchema` / `uriSchema` raw shapes.\n\n```typescript\n// v1 — raw shape, variadic\nserver.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n\n// v2 — config object, Standard Schema\nserver.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {\n    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };\n});\n```\n\n`registerResource` requires a `metadata` argument — pass `{}` if you have none.\n\nA tool or prompt registered **without** an `inputSchema` / `argsSchema` passes the\ncontext as its callback's single argument — v1 passed `(extra)`, v2 passes `(ctx)`:\n\n```typescript\nserver.registerTool('ping', { description: 'Liveness check' }, async ctx => ({ content: [] }));\n```\n\nA one-parameter callback typechecks under either reading, so remember that the first\nparameter here is the context object, not an args object.\n\n#### Standard Schema objects (raw shapes deprecated)\n\nv2 expects schema objects implementing the [Standard Schema spec](https://standardschema.dev/)\nfor `inputSchema`, `outputSchema`, and `argsSchema`. Raw `{ field: z.string() }` shapes\nare still **accepted via `@deprecated` overloads** on `registerTool`/`registerPrompt`\n(auto-wrapped with `z.object()`), and `completable()` accepts any `StandardSchemaV1`;\nprefer wrapping explicitly. Zod v4, ArkType, and Valibot all implement the spec.\n\nFor **optional completable arguments**, apply `.optional()` to the _result_ of\n`completable()` — `completable(z.string(), cb).optional()`, not\n`completable(z.string().optional(), cb)`. v2 resolves completion metadata on the schema\nfound after unwrapping an outer optional wrapper, so the v1 nesting returns empty\ncompletion lists — nothing errors — and if no argument carries completion metadata in\nthe v2 position, the server does not advertise the `completions` capability at all. The\ncodemod inverts the common nesting automatically and flags shapes it cannot rewrite.\n\n**Zod v3 is no longer supported** (v1 peer was `^3.25 || ^4.0`). Check the **declared\nrange** in your `package.json`, not just the installed version: a zod-3 range that\nsatisfied the v1 peer installs and typechecks cleanly under v2 and only fails at\nruntime — and quietly: registration does not convert the schema, the server starts\nand connects normally, and the first `tools/list` (so `client.listTools()`) answers\nwith an error pointing at `fromJsonSchema()` while the process keeps running. (Only the\ndeprecated unwrapped raw-shape form with zod-3 field values throws at registration,\nwith a message pointing at `zod/v4`.) Zod **≥4.2.0** self-converts via\n`~standard.jsonSchema` — the supported path. Zod **4.0–4.1** lacks it, so the SDK falls\nback to its bundled Zod's `z.toJSONSchema()` with a one-time `[mcp-sdk]` console\nwarning; and because `.describe()` field descriptions live in the _authoring_ Zod's\nregistry, the fallback **drops them** from the generated JSON Schema. Fix ladder:\n(1) upgrade to `zod ^4.2.0`; (2) if you must pin an older or separate Zod, attach a\n`~standard.jsonSchema` provider backed by _your_ Zod's `toJSONSchema` so conversion\n(and descriptions) run through your instance; (3) author the schema as raw JSON Schema\nvia `fromJsonSchema()`. (Raw shapes are wrapped with the SDK's **bundled** Zod — built\nwith a foreign Zod they fail at registration or at the first `tools/list`; pass\n`z.object()`-wrapped schemas from your own Zod instead.)\n\nIn a monorepo that pins zod@3 workspace-wide and cannot bump, step (1) can be applied\n**per workspace member**: add a zod-4 alias dependency to the migrating member only —\n`\"zod-v4\": \"npm:zod@^4.2.0\"` in that member's `package.json` — and author SDK-bound\nschemas with it (`import { z } from 'zod-v4'`), leaving the rest of the workspace, and\nthe member's own zod-3 consumer schemas, untouched. The alias copy does not need to be\nthe same instance as the SDK's bundled zod: conversion runs through the **authoring**\ninstance's `~standard.jsonSchema`, so `.describe()` descriptions are preserved and the\nemitted dialect is 2020-12. Keep the two z's apart — schemas authored with the alias\nare for the SDK; they do not compose with the workspace's zod-3 schemas. (For the\nbundle-side effects of the same pin, see\n[Bundlers: nested `zod` copies](packaging-runtime.md#bundlers-nested-zod-copies-in-zod3-pinned-monorepos).)\n\n**Hosts that forward consumer-authored schemas.** The ladder assumes you author the\nschemas yourself. A host API that accepts raw shapes or schemas written by **its own\nconsumers** — plugin systems, agent frameworks — cannot control the authoring zod\nversion or instance, and v1's built-in conversion of foreign shapes is gone. Convert on\nthe host side and register the result with `fromJsonSchema()`: zod-4 input via zod's\nown `z.toJSONSchema(z.object(shape), { io: 'input', target: 'draft-2020-12' })` (the\nconversion is runtime-structural, so a zod ≥4.2 in the host handles schemas built by a\ndifferent zod-4 copy), zod-3 input via the\n[`zod-to-json-schema`](https://www.npmjs.com/package/zod-to-json-schema) package. Its\ndefault draft-07 `$schema` stamp is fine as-is — the default validator\n[honors declared draft-07/06 dialects](types-schemas.md#json-schema-2020-12-posture-sep-1613-sep-2106).\n\nHow a too-old zod surfaces depends on which entry point your code imports. With\nmain-entry `import { z } from 'zod'` on a zod-3 range, the project **typechecks cleanly\nand fails at the first `tools/list`** (the quiet runtime path above). With\n`import * as z from 'zod/v4'` — or any zod whose _typings_ predate\n`~standard.jsonSchema` (zod 4.0–4.1, and zod 3.25.x via the `zod/v4` subpath) — the\nsame code **runs** through the bundled fallback but **fails to compile**:\n`registerTool`/`registerPrompt` reject the schema with `TS2769: No overload matches\nthis call` listing both overloads. The real cause is buried in the first overload's\nelaboration — `Property 'jsonSchema' is missing in type …` (that property is\n`~standard.jsonSchema`, added in zod 4.2.0) — and a follow-on implicit-`any` error on\nthe handler's arguments usually appears below it. If you see that two-overload error on\na registration call with a zod schema, check the installed zod version before anything\nelse; both symptoms resolve identically with step (1) of the ladder.\n\nProjects that must stay below zod 4.2 and accept the documented runtime fallback can\nresolve the remaining registration compile errors with an explicit assertion to the\nregistration schema type — `inputSchema: schema as unknown as\nStandardSchemaWithJSON<Input, Output>` — or a small typed wrapper that attaches a\n`~standard.jsonSchema` provider (step (2) of the ladder, which changes runtime\nconversion but not the schema's static type) and returns the asserted type. The\nfallback caveats (one-time warning, dropped `.describe()` descriptions) still apply\nunless the provider is attached.\n\nThe forced zod-4 bump also surfaces zod's **own** type-level API changes in consumer\nannotations: `z.ZodTypeDef` no longer exists and `z.ZodType`'s generic parameters\nchanged, so v3-era annotations like `z.ZodType<Output, z.ZodTypeDef, Input>` fail to\ncompile — see [zod's v3-to-v4 changelog](https://zod.dev/v4/changelog). Consumer-only\nschemas can keep compiling via zod's v3 compat subpath (`zod/v3`), but anything passed\nto the SDK must be a zod-4 (or other Standard Schema) schema.\n\nThe deprecated raw-shape overloads exist only on `registerTool` / `registerPrompt`.\n`RegisteredTool.update()` / `RegisteredPrompt.update()` take **schema objects**\n(`paramsSchema` / `outputSchema`: `StandardSchemaWithJSON`) — a raw shape passed to\n`update()` is not auto-wrapped; wrap it with `z.object()` yourself.\n\n```typescript\nimport * as z from 'zod/v4';\nserver.registerTool('greet', { inputSchema: z.object({ name: z.string() }) }, handler);\n\n// ArkType works too\nimport { type } from 'arktype';\nserver.registerTool('greet', { inputSchema: type({ name: 'string' }) }, handler);\n\n// Raw JSON Schema via fromJsonSchema (validator defaults to runtime-appropriate choice)\nimport { fromJsonSchema } from '@modelcontextprotocol/server';\nserver.registerTool('greet', { inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }) }, handler);\n\n// No-parameter tools: z.object({})\n```\n\nRemoved Zod-specific helpers (the codemod marks each call site `@mcp-codemod-error`):\n`schemaToJson` — use `fromJsonSchema()` from `@modelcontextprotocol/server` for raw JSON\nSchema, or your schema library's native JSON-Schema conversion; `parseSchemaAsync` — use\nyour schema library's validation directly (e.g. Zod's `.safeParseAsync()`);\n`getSchemaShape` / `getSchemaDescription` / `isOptionalSchema` / `unwrapOptionalSchema`\nhave no replacement (internal Zod introspection). `SchemaInput<T>` →\n`StandardSchemaWithJSON.InferInput<T>` is rewritten mechanically by the codemod. The\ninternal `standardSchemaToJsonSchema` / `validateStandardSchema` helpers are **not** part\nof the public surface — do not import them.\n\nv1's second compat module, `server/zod-json-schema-compat.js` (`toJsonSchemaCompat`), is\nalso removed — and the codemod does **not** rewrite its import (expect `TS2307`). If you\nbuild `Tool` / `Prompt` advertisements yourself, use your schema library's native\nconversion: zod 4's `z.toJSONSchema(schema, { io: 'input', target: 'draft-2020-12' })`\nproduces the dialect v2 advertises.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/types-schemas.md (Tokens: ~3275 | Category: anthropic-skill) -->\n### Types & schemas\n\n#### Zod `*Schema` constants moved to `@modelcontextprotocol/core`\n\nThe Zod schemas (`CallToolResultSchema`, `ListToolsResultSchema`, …) that v1 exported\nfrom `types.js` now live in a separate **`@modelcontextprotocol/core`** package. Neither\n`@modelcontextprotocol/client` nor `@modelcontextprotocol/server` re-exports them — both\npackages stay Zod-free in their public surface.\n\nThe v1→v2 change is just an import-path swap — `.parse()` / `.safeParse()` keep working\nunchanged:\n\n```typescript\n// v1\nimport { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n\n// v2 — same Zod schema, new package\nimport { CallToolResultSchema } from '@modelcontextprotocol/core';\nif (CallToolResultSchema.safeParse(value).success) { ... }\n```\n\n`@modelcontextprotocol/core` is the canonical home for the spec's Zod schema constants\n(and the OAuth/OpenID metadata schemas). It is runtime-neutral (its only dependency is\n`zod`) and arrives transitively as the shared runtime schema graph of `client` /\n`server` — add it to your own `dependencies` only when you import the raw schemas\ndirectly.\n\nIf you would rather keep your project Zod-free, the **`isSpecType` / `specTypeSchemas`**\nalternatives are exported from `@modelcontextprotocol/client` and `…/server`:\n\n```typescript\nimport { isSpecType, specTypeSchemas } from '@modelcontextprotocol/client';\nif (isSpecType.CallToolResult(value)) { ... }\nconst blocks = mixed.filter(isSpecType.ContentBlock);\nconst result = specTypeSchemas.CallToolResult['~standard'].validate(value);\n```\n\n`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of\nevery named type in the MCP spec — so you get autocomplete and a compile error on typos.\n`specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` (`validate()` is synchronous).\n`validate()` returns `{ value }` or `{ issues }` and never throws — unlike `.parse()` on\nthe real schema; code that caught a `ZodError` should inspect `result.issues` (or keep\n`.parse()` on the schema imported from `@modelcontextprotocol/core`).\nThe pre-existing `isCallToolResult(value)` guard still works.\n\n**`specTypeSchemas.X` is `StandardSchemaV1`, not `ZodType`.** Zod-specific composition\n— `.extend()`, `.pick()`, `.omit()`, `.merge()`, `.shape`, `.passthrough()`,\n`.parseAsync()` — does **not** compile on a `specTypeSchemas` entry; reach for the real\nZod schema from `@modelcontextprotocol/core` when you need to derive a tolerant variant\nof a spec schema (e.g.\n`ListToolsResultSchema.extend({ tools: ToolSchema.omit({ outputSchema: true }).array() })`).\nThe Zod-specific `AnySchema` / `SchemaOutput` types from `…/zod-compat.js` are removed —\nreplace with `StandardSchemaV1` / `StandardSchemaV1.InferOutput<T>` (the codemod's\nremoval message says the same).\n\n**Composing two core schemas.** Zod composition needs a shared zod: deriving from a\nsingle core schema (as above) and combining core schemas with your own `z` typecheck\nwhen your `zod` resolves to the **same copy** `@modelcontextprotocol/core` uses (a\n`zod ^4.2.0` range that dedupes). When it cannot — a zod@3-pinned project nests core's\nown zod@4 — v1 idioms that combined two spec schemas with your `z` no longer compile:\ncore does not export its zod instance, and a foreign zod's `z.union(…)` / `.or(…)`\nrejects core's schema types. For accept-either result parsing, skip composition:\nrequest with the `ResultSchema` passthrough (the same one the\n[gateway note](protocol-ctx.md#request-ctxmcpreqsend-and-calltool-no-longer-require-a-schema-for-spec-methods)\nuses) and discriminate with sequential `safeParse`:\n\n```typescript\n// v1 — one composed schema\nconst result = await client.request(req, z.union([CompatibilityCallToolResultSchema, CreateTaskResultSchema]));\n\n// v2 — passthrough request, then sequential discrimination\nimport { CompatibilityCallToolResultSchema, CreateTaskResultSchema, ResultSchema } from '@modelcontextprotocol/core';\nconst raw = await client.request(req, ResultSchema);\nconst asTask = CreateTaskResultSchema.safeParse(raw);\nconst result = asTask.success ? asTask.data : CompatibilityCallToolResultSchema.parse(raw);\n```\n\nOrder the candidates from most to least specific, and `.parse()` the last one so a\nresult that matches no candidate still fails loudly.\n\nThe role-aggregate unions (`ClientRequest`, `ServerResult`, `ServerRequest`,\n`ClientResult`, `ClientNotification`, `ServerNotification`) and the typed-method maps\n(`RequestMethod`, `RequestTypeMap`, `ResultTypeMap`, `NotificationTypeMap`) no longer\ninclude task vocabulary; the deprecated `Task*` types remain importable on their own.\n(One published-alpha qualification, like the `-32002` note in [Errors](errors.md#errors): the\n`2.0.0-alpha.3` and earlier typings predate this — the typed maps there still carry the\n`tasks/*` entries, and `ResultTypeMap['tools/call']` still unions `CreateTaskResult`, so\na `client.request({ method: 'tools/call', … })` result does not assign to\n`Promise<CallToolResult>`. If pinned to those alphas, narrow with the\n`isCallToolResult` guard — the recommended discrimination tool anyway, per the next\nparagraph; `2.0.0-alpha.4` and later are unaffected.)\n\n**Discriminating result shapes: use guards, not the `in` operator.** The v2\nzod-inferred result types are passthrough objects — every union member carries an index\nsignature — so v1-idiomatic property discrimination such as\n`if ('content' in result) { … } else { result.toolResult }` no longer narrows: the `in`\ncheck is satisfiable by every member, and the else branch can collapse to `never`\n(surfacing as `TS2339` on the property you then read). Use the exported guards instead:\n`isCallToolResult(result)`, or `isSpecType.GetPromptResult(result)` and friends for any\nother spec type ([above](#zod-schema-constants-moved-to-modelcontextprotocolcore)). An\nadjacent trap when keeping a union for later narrowing: a `const` **annotation** is\ncontrol-flow-narrowed straight back to the initializer's type — after\n`const r: A | B = await fn()`, `r` has `fn`'s return type, not the union — so when you\nneed the wider union (e.g. a `CompatibilityCallToolResult` branch), apply an\n`as A | B` assertion instead of an annotation.\n\n#### Removed type aliases\n\n| Removed                                                         | Replacement                                                     |\n| --------------------------------------------------------------- | --------------------------------------------------------------- |\n| `JSONRPCError`                                                  | `JSONRPCErrorResponse`                                          |\n| `JSONRPCErrorSchema`                                            | `JSONRPCErrorResponseSchema`                                    |\n| `isJSONRPCError`                                                | `isJSONRPCErrorResponse`                                        |\n| `isJSONRPCResponse` (deprecated in v1)                          | `isJSONRPCResultResponse` ²                                     |\n| `JSONRPCResponseSchema` (result-only in v1)                     | `JSONRPCResultResponseSchema` ²                                 |\n| `JSONRPCResponse` (result-only in v1)                           | `JSONRPCResultResponse` ²                                       |\n| `ResourceReference` / `ResourceReferenceSchema`                 | `ResourceTemplateReference` / `ResourceTemplateReferenceSchema` |\n| `IsomorphicHeaders`                                             | Web Standard `Headers`                                          |\n| `RequestHandlerExtra`                                           | `ServerContext` / `ClientContext` / `BaseContext`               |\n| `ResourceTemplate` (the spec wire **type** from `sdk/types.js`) | `ResourceTemplateType` ³                                        |\n\n² v2 introduces **new** `isJSONRPCResponse` / `JSONRPCResponse` / `JSONRPCResponseSchema`\nwith corrected semantics — they match **both** result and error responses (the schema is\n`z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])`). v1's symbols only\nmatched results. To preserve v1 behavior, rename to `isJSONRPCResultResponse` /\n`JSONRPCResultResponse` / `JSONRPCResultResponseSchema` (the codemod does this).\n\n³ The `ResourceTemplate` URI-template helper **class** (from `sdk/server/mcp.js`) is\n**unchanged** — keep `new ResourceTemplate(...)` as-is. Only the like-named spec wire\ntype from `types.js` was renamed to `ResourceTemplateType` to resolve the v1 collision;\nthe codemod scopes the rename to imports from `sdk/types.js` only.\n\nAll other symbols from `@modelcontextprotocol/sdk/types.js` retain their original\nnames — import the TypeScript types, error classes, enums, and type guards from\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the Zod\n`*Schema` constants from `@modelcontextprotocol/core`.\n\nOne type-level narrowing to note: client/server capability `experimental` payloads are\nnow typed as JSON-compatible objects (nested JSON values) rather than arbitrary\nobjects. A payload typed `Record<string, unknown>` no longer assigns (`TS2322`) — give\nthe source a JSON-compatible type or cast at the boundary.\n\nThe `Protocol` base class and `mergeCapabilities` moved: import them from the\n`@modelcontextprotocol/client` or `@modelcontextprotocol/server` package root instead\nof `shared/protocol.js`. Most code should not use `Protocol` directly — `Client` and\n`Server` are the supported surfaces, and for observing unmatched inbound requests\nprefer `client.fallbackRequestHandler` / `server.fallbackRequestHandler`. The codemod\nrewrites `Protocol` and `mergeCapabilities` imports to the package root, like the\nmodule's other symbols. One caveat: the client and server packages each bundle their\nown compiled copy of the class, so the two roots' `Protocol` exports are distinct\nclasses — import it from one package consistently within a process.\n\n#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)\n\nThe default validator dispatches on the schema's declared `$schema`: absent or 2020-12\nvalidates as **JSON Schema 2020-12** — on Node via `Ajv2020` instead of v1's draft-07\n`Ajv` (the Cloudflare Workers default was already 2020-12) — a declared 2019-09\n`$schema` validates with 2019-09 semantics (`Ajv2019`), and a declared draft-07 or\ndraft-06 `$schema` validates with draft-07 semantics. Schemas declaring any other\n`$schema` are rejected with `Error(\"…unsupported dialect…\")`. Two known draft-07 engine\ndifferences: the Node engine (classic Ajv, same as v1's default) evaluates keywords\nadjacent to `$ref`, stricter than draft-07's ignore-siblings rule, while the\nbrowser/Workers engine ignores them per spec; and the browser/Workers engine does not\nresolve a `$ref` inside a `dependencies` entry whose key collides with a JSON Schema\nkeyword (`type`, `default`, `format`, …) — validation throws `Unresolved $ref` when\nthat dependency triggers (surfaced as the SDK's typed validation error), while the\nNode engine handles the same schema correctly.\n\n`CallToolResult.structuredContent` is widened from `{ [k: string]: unknown }` to\n`unknown` (SEP-2106 lifts the `type:\"object\"` root restriction). The presence check is\n`!== undefined`, not falsy (`null` / `0` / `false` / `\"\"` are legal values now). External\n`$ref` is not dereferenced (unchanged from v1; Ajv throws `MissingRefError` at compile,\nsurfaced per-tool on `callTool`).\n\n| v1 pattern                                                                                                   | Mechanical fix                                                                                                                                                                                         |\n| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `result.structuredContent.<key>` / `result.structuredContent?.<k>`                                           | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }`                                                                              |\n| `if (!result.structuredContent)`                                                                             | `if (result.structuredContent === undefined)`                                                                                                                                                          |\n| draft-07 idioms **without** a declared `$schema` (a declared draft-07/06 `$schema` dispatches automatically) | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |\n| undeclared draft-07 idioms via `fromJsonSchema(schema)`                                                      | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas                                 |\n| `outputSchema` / `inputSchema` with absolute-URI `$ref`                                                      | inline under `$defs` and reference with `#/$defs/Name`                                                                                                                                                 |\n\nA tool may now register an `outputSchema` whose root is `type:\"array\"`, `type:\"string\"`,\netc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward\nevery era a non-object `structuredContent` with no `text` block of its own gets a\n`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md › Per-era wire codecs](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md#per-era-wire-codecs) for how the codec applies these per era.\n\n**Your advertised tool schemas change shape on the wire.** The same `registerTool`\ncalls produce `tools/list` entries whose generated `inputSchema` differs from v1:\nJSON Schema 2020-12 idioms (zod 4 conversion), different `additionalProperties`\nhandling (no `additionalProperties: false` by default; passthrough objects emit\n`\"additionalProperties\": {}` instead of `true`), and no `execution.taskSupport` member.\nGolden tests, transcript pins, and strict client-side validators of your advertised\ntool list need re-baselining — the new shapes are spec-conformant.\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/references/unchanged-apis.md (Tokens: ~1041 | Category: anthropic-skill) -->\n## Unchanged APIs\n\nThe following are unchanged between v1 and v2 apart from the import path — except\nwhere an entry notes its own signature change:\n\n- `Client` constructor and `connect`, `close`, and the typed verbs (`listTools`,\n  `listPrompts`, `listResources`, `readResource`, …) — note `callTool()` and `request()`\n  signatures changed (schema parameter dropped for spec methods).\n- `McpServer` constructor, `server.connect(transport)`, `server.close()`, and the\n  `McpServer.server` accessor — still the supported way to call the low-level\n  `Server`'s push verbs (`createMessage` / `listRoots` / `sendLoggingMessage` — ⚠\n  `@deprecated`, see [§Deprecated in v2](protocol-ctx.md#deprecated-in-v2-sep-2577)) outside a\n  handler context.\n- The server Streamable HTTP transports' **constructor options** (`sessionIdGenerator`,\n  `onsessioninitialized`, `onsessionclosed`, `enableJsonResponse`, `eventStore`,\n  `retryInterval`) and the `handleRequest` surface — only the class name and import\n  moved: `StreamableHTTPServerTransport` is now `NodeStreamableHTTPServerTransport`\n  from `@modelcontextprotocol/node`, a thin wrapper over\n  `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server`,\n  which exposes the same options ([decision rule](imports-transports.md#imports--transports)). The\n  transport-level `closeSSEStream(requestId)` / `closeStandaloneSSEStream()` methods\n  keep their v1 names too — only the handler-context accessors moved to `ctx.http`\n  ([remap table](protocol-ctx.md#low-level-protocol--handler-context-ctx)).\n- `UriTemplate` (v1: `@modelcontextprotocol/sdk/shared/uriTemplate.js`) — `expand` /\n  `match` semantics carry over; import it from `@modelcontextprotocol/server` or\n  `@modelcontextprotocol/client` (top-level export; the codemod rewrites the path).\n- `StreamableHTTPClientTransport`, `SSEClientTransport` constructors and options —\n  including resumability: the per-request `resumptionToken` / `onresumptiontoken`\n  request options carry over from v1 unchanged\n  ([Resume a dropped stream](https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.md#resume-a-dropped-stream)).\n- `StdioClientTransport` and `StdioServerTransport` — **import path moved** to the\n  `./stdio` subpath and gained an optional `maxBufferSize` ([Imports & transports](imports-transports.md#imports--transports)).\n- The **`Transport` interface contract** — `start` / `send` / `close`, `onmessage` /\n  `onclose` / `onerror`, optional `sessionId` and `setProtocolVersion`,\n  `TransportSendOptions`, `MessageExtraInfo`. Hand-rolled v1 transports (recording\n  wrappers, test doubles, decorators) compile and run against v2 with only the import\n  path updated. v2 adds **optional** members only — `hasPerRequestStream` and\n  `setSupportedProtocolVersions` on the interface, `requestSignal` / `headers` /\n  `onRequestStreamEnd` on `TransportSendOptions` — which matter only for 2026-era\n  per-request-stream cancellation and `Mcp-Param-*` header attachment\n  ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)).\n- All TypeScript **type** definitions from `types.ts` (except the aliases listed under\n  [Removed type aliases](types-schemas.md#removed-type-aliases) and the `experimental` capability\n  payload narrowing — see [Types & schemas](types-schemas.md#types--schemas)).\n- Tool, prompt, and resource callback return types.\n\n> The `Server` (low-level) constructor and **most** of its methods are unchanged, but\n> `setRequestHandler` / `setNotificationHandler` and `request()` signatures changed\n> ([Low-level protocol](protocol-ctx.md#low-level-protocol--handler-context-ctx)). In particular,\n> `Server.createElicitationCompletionNotifier()` is unchanged — including its\n> construction-time client-capability check — for 2025-era URL-mode elicitation\n> ([support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md)). The Zod `*Schema`\n> constants are **not** part of the unchanged surface — they moved to\n> `@modelcontextprotocol/core` ([Types & schemas](types-schemas.md#types--schemas)).\n\n<!-- File: skills/mcp-typescript-sdk-upgrade-to-v2/SKILL.md (Tokens: ~3001 | Category: anthropic-skill) -->\n---\nname: mcp-typescript-sdk-upgrade-to-v2\ndescription: 'Upgrades TypeScript code from v1 of the MCP TypeScript SDK (`@modelcontextprotocol/sdk`) to v2 (`@modelcontextprotocol/client`, `/server`, `/core` and the middleware packages): runs the official codemod, then works through what it cannot rewrite. Use whenever a project depends on `@modelcontextprotocol/sdk` and the user wants to migrate, upgrade, port or bump it to v2, or is fixing v1-to-v2 breakage such as missing `McpError`, `RequestHandlerExtra` or `*Schema` exports, `@mcp-codemod-error` markers, or changed auth, error, transport or handler behavior, even if they do not say \"migration\".'\n---\n\n<!-- #region intro -->\n\n# Upgrading from v1.x to v2\n\nThis guide covers upgrading from `@modelcontextprotocol/sdk` (v1.x) to the v2 packages.\nIt is written for shell-capable agents and humans alike: run the codemod first, then\nwork through the manual sections for what the codemod can't rewrite.\n\nIf you are already on v2 and want to adopt the **2026-07-28 protocol revision**, see\n[support-2026-07-28.md](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.md) instead.\n\n## TL;DR — quick path\n\n1. **Prerequisites.** Node.js 20+. v2 is ESM-first but ships a CommonJS build too, so\n   both `import` and `require('@modelcontextprotocol/…')` resolve natively.\n2. **Run the codemod.**\n    ```bash\n    npx @modelcontextprotocol/codemod@latest v1-to-v2 .\n    ```\n    Run it at the **package root** (`.`), not `./src` — it also rewrites `package.json`,\n    and real projects import the SDK from `test/`, `scripts/`, and fixtures too.\n3. **Grep for markers.** Anything the codemod recognized but could not safely rewrite is\n   marked in place:\n    ```bash\n    grep -rn '@mcp-codemod-error' .\n    ```\n4. **Type-check.** `tsc --noEmit` (or your build). Remaining errors map to the\n   [manual sections](#manual-changes-what-the-codemod-does-not-handle) below.\n5. **Format.** The codemod rewrites the AST without reformatting — run your formatter on\n   the changed files (`prettier --write` / `eslint --fix` / `biome format --write`); the\n   codemod prints the exact command after it runs.\n6. **Run your tests.**\n\nMigrating a large codebase gradually instead of in one pass? See\n[Migrating in stages (large codebases)](references/packaging-runtime.md#migrating-in-stages-large-codebases).\n\n<!-- #endregion intro -->\n\n<!-- #region codemod -->\n\n## What the codemod handles\n\nThe codemod ([`@modelcontextprotocol/codemod`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md))\nmechanically applies every rename whose mapping is fixed. The mappings are the\n**source of truth** — they live in the codemod package and are not reproduced here:\n\n| Mapping                                                                                   | Source file                                                                                                                                                                  |\n| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@modelcontextprotocol/sdk/...` import paths → v2 packages                                | [`mappings/importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)                   |\n| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts)                   |\n| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)`                  | [`mappings/schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts)   |\n| `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` property remap                                 | [`mappings/contextPropertyMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) |\n\nIn addition the codemod:\n\n- Updates `package.json` dependencies (`@modelcontextprotocol/sdk` → the v2 packages\n  your imports actually use).\n- Rewrites `.tool()` / `.prompt()` / `.resource()` to `registerTool` / `registerPrompt`\n  / `registerResource` and wraps `inputSchema` / `outputSchema` / `argsSchema` /\n  `uriSchema` raw Zod shapes with `z.object()`, adding `import { z } from 'zod'`\n  when the file has no `z` binding.\n- Drops the result-schema argument from `client.request()` / `client.callTool()` for\n  spec methods.\n- Routes the spec Zod `*Schema` constants imported from `sdk/types.js` to\n  `@modelcontextprotocol/core` (mixed imports are split; `.parse()` / `.safeParse()`\n  calls are left untouched). Task-handler schema constants\n  (`GetTaskRequestSchema` etc.) used as `setRequestHandler` args are **not** rewritten\n  — the experimental tasks feature was removed (SEP-2663), so each such registration\n  is marked with an action-required diagnostic instead (see\n  [Experimental tasks interception removed](references/behavioral-changes.md#experimental-tasks-interception-removed)).\n- Renames `ErrorCode` → `ProtocolErrorCode` and routes the local-only members\n  (`RequestTimeout`, `ConnectionClosed`) to `SdkErrorCode` — rewriting an all-SDK\n  condition's `instanceof ProtocolError` guard to `SdkError`, and marking guards\n  that mix the two enums.\n- Renames every `StreamableHTTPError` reference to `SdkHttpError` and adds the import\n  (constructor calls are marked for review — argument shape changed).\n- Replaces `IsomorphicHeaders` with the Web Standard `Headers` type and drops the\n  import (a warning notes `Headers` uses `.get()`/`.set()`, not bracket access).\n- Rewrites `SchemaInput<T>` → `StandardSchemaWithJSON.InferInput<T>`.\n- Renames `RequestHandlerExtra` → `ServerContext` / `ClientContext` and the `extra`\n  parameter to `ctx`.\n- Rewrites `vi.mock` / `jest.mock` and dynamic `import()` paths.\n- Renames the `ResourceTemplate` **type** imported from `@modelcontextprotocol/sdk/types.js`\n  to `ResourceTemplateType` (the spec wire type). The `ResourceTemplate` URI-template\n  helper **class** from `server/mcp.js` keeps its name and is not renamed.\n- Drops `@modelcontextprotocol/sdk/server/zod-compat.js` imports.\n- Inverts optional completable nesting — `completable(schema.optional(), cb)` becomes\n  `completable(schema, cb).optional()` (see\n  [Standard Schema objects](references/server-registration.md#standard-schema-objects-raw-shapes-deprecated)); shapes it\n  cannot invert get an `@mcp-codemod-error` marker.\n- Rewrites `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the\n  client or server package root, like the module's other symbols.\n\n## What the codemod does NOT handle\n\nEach of these maps to a manual section below. The codemod marks every site it\nrecognized but could not safely rewrite with an `@mcp-codemod-error` comment.\n\n- **Node 20 / ESM** — pre-flight, not a code rewrite. → [Packaging & runtime](references/packaging-runtime.md#packaging--runtime)\n- **Header-read `.get()` rewrite** — `IsomorphicHeaders` is renamed to `Headers`\n  and `extra.requestInfo?.headers[…]` is remapped to `ctx.http?.req?.headers[…]`, but\n  converting that bracket access to `.get()` is manual. (Headers you _pass in_ via\n  `requestInit.headers` need no rewrite — plain objects remain valid.)\n  → [HTTP & headers](references/http-headers.md#http--headers)\n- **`ctx.mcpReq.send()` schema-arg drop** — the codemod drops the schema arg from\n  `client.request()` / `client.callTool()` but leaves nested `ctx.mcpReq.send()` calls\n  alone. → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **OAuth error-class consolidation** — `instanceof InvalidGrantError` → `OAuthError` +\n  `OAuthErrorCode` is a judgment rewrite. → [Auth](references/auth.md#auth)\n- **`SdkErrorCode` branch selection** — the codemod renames `StreamableHTTPError` →\n  `SdkHttpError`; deciding which `SdkErrorCode` branch a given catch should match is\n  judgment. → [Errors](references/errors.md#errors)\n- **Namespace schema access** — `import * as t from '…/types.js'` +\n  `t.CallToolResultSchema.parse(…)` can't be split per-symbol; the codemod flags it\n  action-required — re-import the schema from `@modelcontextprotocol/core` by hand.\n  → [Types & schemas](references/types-schemas.md#types--schemas)\n- **Import-less (injected) SDK surfaces** — the codemod is import-driven: a file that\n  receives the SDK surface as a parameter (dependency injection, factory seams) and has\n  no SDK import is never rewritten, and the v1 idioms there fail at **runtime**, not\n  compile time — e.g. the v1 schema-first `setRequestHandler(Schema, …)` form throws a\n  `TypeError` at registration. Grep such seams for v1 API tokens beyond import\n  statements (`setRequestHandler(`, `ErrorCode.`, `extra.`) and apply the\n  [handler-registration](references/protocol-ctx.md#setrequesthandler--setnotificationhandler-use-method-strings)\n  and [Errors](references/errors.md#errors) sections by hand.\n  → [Low-level protocol](references/protocol-ctx.md#low-level-protocol--handler-context-ctx)\n- **Behavioral adaptation** — list auto-aggregation, capability empties, lazy validator\n  compilation, output-schema validation rules. → [Behavioral changes](references/behavioral-changes.md#behavioral-changes)\n\n---\n\n## Manual changes (what the codemod does not handle)\n\n<!-- #endregion codemod -->\n\nEach subsystem has its own reference file. Open the one that matches the type error, runtime failure or `@mcp-codemod-error` marker in front of you; most migrations need only a few of them.\n\n| Reference file                                                             | Read it when                                                                                                                                                                                   |\n| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Packaging & runtime](references/packaging-runtime.md)                     | Choosing which v2 packages to install, or dealing with Node/ESM/CommonJS setup, monorepo workspaces, Jest, nested `zod` copies, migrating in stages, or a library that peer-depends on the SDK |\n| [Imports & transports](references/imports-transports.md)                   | A transport import needs a runtime decision (Node vs web-standard Streamable HTTP) or a stdio import moved to a `./stdio` subpath                                                              |\n| [Low-level protocol & handler context (`ctx`)](references/protocol-ctx.md) | Code uses `extra` / `ctx`, `setRequestHandler` / `setNotificationHandler`, `request()`, `ctx.mcpReq.send()` or `callTool()`                                                                    |\n| [Server registration API](references/server-registration.md)               | Code registers tools, prompts or resources, or passes raw Zod shapes as schemas                                                                                                                |\n| [HTTP & headers](references/http-headers.md)                               | Code reads HTTP request headers                                                                                                                                                                |\n| [Errors](references/errors.md)                                             | Code throws or catches `McpError` / `ProtocolError`, `ErrorCode`, `SdkError` or `SdkHttpError`                                                                                                 |\n| [Auth](references/auth.md)                                                 | Code uses OAuth (`auth()`, `OAuthClientProvider`, OAuth error classes) or bearer-token auth                                                                                                    |\n| [Types & schemas](references/types-schemas.md)                             | Code imports Zod `*Schema` constants or removed type aliases, or depends on JSON Schema details                                                                                                |\n| [Behavioral changes](references/behavioral-changes.md)                     | The code compiles but tests or runtime behavior differ                                                                                                                                         |\n| [Enhancements](references/enhancements.md)                                 | Optional: v2 features worth adopting once the upgrade is done                                                                                                                                  |\n| [Unchanged APIs](references/unchanged-apis.md)                             | Checking whether an API changed at all before rewriting it                                                                                                                                     |\n\n<!-- #region need-help -->\n\n## Need help?\n\n- The codemod's [`@mcp-codemod-error`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/README.md) markers point\n  at every site it could not safely rewrite.\n- The [Troubleshooting](https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting.md) page covers common errors and their fixes.\n- Runnable [examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)\n  for every subsystem.\n- Open an issue on [GitHub](https://github.com/modelcontextprotocol/typescript-sdk/issues).\n\n<!-- #endregion need-help -->\n\n<!-- File: examples/scoped-tools/README.md (Tokens: ~331 | Category: plugin-manifest) -->\n# scoped-tools — per-tool scope enforced in the tool handler\n\nDemonstrates per-tool OAuth scope enforcement on a `createMcpHandler`\ndeployment: the HTTP gate does **bearer-verify + 401 only**, and each tool\nhandler checks `ctx.http?.authInfo?.scopes` for the scope it needs. The scope\ndecision lives next to the code it guards — the handler is the only place that\nauthoritatively knows which tool is executing — instead of in middleware that\nwould have to re-derive the operation from the request body.\n\n`server.ts` runs a minimal demo Authorization Server alongside the MCP Resource\nServer. `client.ts` connects with a `files:read` token, calls `list-files`\n(works), then calls `write-file` → the handler returns `{ isError: true }` with\n`insufficient_scope: requires files:write`.\n\nThe transport's automatic `403 insufficient_scope` **step-up** flow (SEP-2350 —\nscope union, refresh-bypass, `maxStepUpRetries`) applies when the RS responds\n`403` at the HTTP layer; that path is exercised by\n`test/e2e/scenarios/client-auth.test.ts`.\n\n```bash\npnpm --filter @mcp-examples/scoped-tools server -- --http --port 3000\npnpm --filter @mcp-examples/scoped-tools client -- --http http://127.0.0.1:3000/mcp\n```\n\n> DEMO ONLY — the bundled AS auto-approves and grants whatever scope is asked\n> for. Do not deploy.\n\n<!-- File: examples/tools/README.md (Tokens: ~84 | Category: plugin-manifest) -->\n# tools\n\n**Start here.** Register tools with `McpServer.registerTool`; the SDK infers the JSON Schema from any Standard-Schema-compatible input (Zod here) and emits `structuredContent` matching `outputSchema`. The client lists tools, inspects schemas and `annotations`, calls them, and\nasserts structured output.\n\n```bash\npnpm tsx examples/tools/client.ts\n```\n\n<!-- File: packages/client/README.md (Tokens: ~324 | Category: plugin-manifest) -->\n# `@modelcontextprotocol/client`\n\nThe MCP (Model Context Protocol) TypeScript client SDK. Build MCP clients that connect to MCP servers.\n\n> **This is v2 of the MCP TypeScript SDK, the stable release line**, implementing the [2026-07-28 MCP spec](https://modelcontextprotocol.io/specification/2026-07-28). It replaces the single `@modelcontextprotocol/sdk` package from v1. Start with the [documentation](https://ts.sdk.modelcontextprotocol.io/v2/); coming from v1, see the [migration guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2). Found a problem? [Open an issue](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml).\n\n## Install\n\n```bash\nnpm install @modelcontextprotocol/client\n```\n\nTypeScript ≥6.0 no longer auto-includes `@types/*` — add `\"types\": [\"node\"]` to your `tsconfig.json` `compilerOptions` (the published `.d.mts` references `Buffer`).\n\n## Documentation\n\n- **[Repository README](https://github.com/modelcontextprotocol/typescript-sdk#readme)** — overview, package layout, examples\n- **[Client guide](https://ts.sdk.modelcontextprotocol.io/v2/clients/connect)** — connecting, calling tools, OAuth, and middleware\n- **[API reference](https://ts.sdk.modelcontextprotocol.io/v2/)**\n- **[MCP specification](https://modelcontextprotocol.io)**\n\n<!-- File: packages/codemod/batch-test/README.md (Tokens: ~1801 | Category: plugin-manifest) -->\n# Codemod Batch Test\n\nTests the v1-to-v2 codemod against real-world repos to find bugs, missing transforms, and gaps.\n\n## How it works\n\nFor each repo in `repos.json`, the batch test:\n\n1. Clones the repo (or resets an existing clone)\n2. Installs dependencies\n3. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy\n4. Runs the codemod — in-process via the programmatic API (`--codemod=local`, the default), or by shelling out to the published CLI via `npx` (`--codemod=published`)\n5. Packs local SDK packages as tarballs and rewrites `package.json` deps to use them, so the test runs against the current SDK branch — only when `--sdk=local` (the default); with `--sdk=published` the v2 deps are installed from npm instead (see [Modes](#modes))\n6. Re-installs dependencies\n7. Re-runs the same checks\n8. Writes structured JSON reports\n\nErrors that appear in step 7 but not step 3 are codemod-introduced regressions.\n\n## Usage\n\n```bash\n# Build all SDK packages first (tarballs need built dist/)\npnpm build:all\n\n# Run the batch test\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# Clean cloned repos, results, and tarballs\npnpm --filter @modelcontextprotocol/codemod batch-test:clean\n```\n\n## Modes\n\nEach run independently chooses where the **SDK packages** and the **codemod** come from:\n\n| Flag                       | Values                 | Default   | Effect                                                                                                              |\n| -------------------------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |\n| `--sdk`                    | `local` \\| `published` | `local`   | `local`: pack local packages and rewrite deps to `file:` tarballs. `published`: install the v2 deps from npm.       |\n| `--codemod`                | `local` \\| `published` | `local`   | `local`: run the working-copy codemod in-process. `published`: `npx` the published CLI.                             |\n| `--codemod-version <spec>` | npm version/tag/range  | `latest`  | Only with `--codemod=published`.                                                                                    |\n| `--sdk-version <spec>`     | npm version/tag/range  | _(unset)_ | Only with `--sdk=published`. Pins each v2 dep to its own resolved version. Unset = use whatever the codemod writes. |\n\nBoth `--flag value` and `--flag=value` forms work.\n\n```bash\n# default (today's behavior)\npnpm --filter @modelcontextprotocol/codemod batch-test\n\n# both from npm @latest\npnpm --filter @modelcontextprotocol/codemod batch-test -- --sdk=published --codemod=published\n```\n\nPublished specs are resolved to concrete versions via `npm view` at startup. A failure resolving the codemod version or the representative SDK label aborts the run; a per-package `--sdk-version` miss only warns and leaves that package on the codemod-written range. Each\n`@modelcontextprotocol/*` package is resolved **independently** — they are not released in lockstep. Results are written to a per-run directory keyed on the resolved versions; the SDK segment is the resolved `@modelcontextprotocol/server` version used as a representative label,\nwhile the full per-package set is recorded in `summary.json` → `sdkVersions`:\n\n```\nresults/codemod-local__sdk-local/                  # default (--sdk=local --codemod=local)\nresults/codemod-2.0.0-alpha.2__sdk-2.0.0-alpha.2/  # --codemod=published --sdk=published --sdk-version=2.0.0-alpha.2\nresults/codemod-2.0.0-alpha.2__sdk-from-codemod/   # --codemod=published --sdk=published  (no --sdk-version)\n```\n\nThe segment `sdk-from-codemod` appears when `--sdk=published` with no `--sdk-version` and `--codemod=published` (the SDK version is baked into the published codemod and only known after install; the installed versions are recorded in `summary.json` → `sdkVersions`).\n\n**Limitation:** in published-codemod mode the CLI emits text, not structured diagnostics, so `codemod.diagnostics` is empty and the raw CLI output is captured under `codemod.cli` instead. Diagnostics categorization applies only to local-codemod runs.\n\n## Output\n\nResults are written to a per-run directory keyed on the resolved versions, `batch-test/results/<config>/`, where `<config>` is the `codemod-…__sdk-…` leaf (distinct `--codemod-version`/`--sdk-version` values produce distinct directories even within the same `--sdk`/`--codemod`\nmode; see [Modes](#modes)):\n\n- `results/<config>/summary.json` — overview across all repos: which passed, which failed, error counts, plus the run `config` and the resolved per-package `sdkVersions`\n- `results/<config>/<repo-slug>/report.json` — per-repo detail: baseline vs post-codemod check results, codemod diagnostics, change counts\n\n## Repo manifest (`repos.json`)\n\nAn array of repo entries. Each entry specifies a GitHub repo and one or more packages within it.\n\n```json\n{\n    \"repo\": \"owner/repo-name\",\n    \"ref\": \"main\",\n    \"packages\": [\n        {\n            \"dir\": \"packages/mcp-server\",\n            \"sourceDir\": \"src\",\n            \"checks\": {\n                \"typecheck\": \"npx tsc --noEmit\",\n                \"build\": \"npm run build\",\n                \"test\": \"npm run test\",\n                \"lint\": null\n            }\n        }\n    ]\n}\n```\n\n| Field                  | Required | Default                                | Description                                                       |\n| ---------------------- | -------- | -------------------------------------- | ----------------------------------------------------------------- |\n| `repo`                 | yes      | —                                      | GitHub `owner/name`                                               |\n| `ref`                  | no       | `main`                                 | Branch or tag to clone                                            |\n| `packages`             | no       | `[{ \"dir\": \".\", \"sourceDir\": \"src\" }]` | Package targets within the repo                                   |\n| `packages[].dir`       | yes      | —                                      | Path to package root (where `package.json` lives)                 |\n| `packages[].sourceDir` | no       | `src`                                  | Source directory relative to `dir` (passed to codemod)            |\n| `packages[].checks`    | no       | auto-detect                            | Override check commands; set a value to `null` to skip that check |\n\nWhen `checks` is omitted, the runner auto-detects commands from the package's `package.json` scripts (probing names like `typecheck`, `build`, `test`, `lint`). The package manager is auto-detected from the lockfile at the repo root.\n\n## Analyzing results\n\n`analyze-prompt.md` contains instructions for Claude Code to run the batch test and produce a categorized analysis. Each error is classified as:\n\n| Category            | Meaning                                            |\n| ------------------- | -------------------------------------------------- |\n| `codemod-bug`       | A transform produced incorrect output              |\n| `missing-transform` | The codemod should handle this pattern but doesn't |\n| `manual-migration`  | Expected — requires human judgment                 |\n| `repo-specific`     | Unusual pattern not worth handling in the codemod  |\n\n## Adding a repo\n\n1. Edit `repos.json` and add an entry\n2. Run `pnpm --filter @modelcontextprotocol/codemod batch-test`\n3. Check `results/<config>/<repo-slug>/report.json` for new findings\n\nFor monorepos, list each package that uses `@modelcontextprotocol/sdk` as a separate entry in `packages`.\n\n## Iteration workflow\n\n```\n1. Run the batch test\n2. Review results — identify codemod bugs / missing transforms\n3. Fix the codemod transforms\n4. Run batch-test:clean, then re-run the batch test\n5. Confirm the fixes resolved the issues\n6. Repeat\n```\n\n<!-- ... +10 more modular skills available via /api/skills/modelcontextprotocol/typescript-sdk?path={path} -->\n</yakaai_skills>"}