diff --git a/CLAUDE.md b/CLAUDE.md index b027791d8e..041c5e11ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,284 +1,14 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build & Test Commands - -```sh -pnpm install # Install all workspace dependencies - -pnpm build:all # Build all packages -pnpm lint:all # Run ESLint + Prettier checks across all packages -pnpm lint:fix:all # Auto-fix lint and formatting issues across all packages -pnpm typecheck:all # Type-check all packages -pnpm test:all # Run all tests (vitest) across all packages -pnpm check:all # typecheck + lint across all packages - -# Run a single package script (examples) -# Run a single package script from the repo root with pnpm filter -pnpm --filter @modelcontextprotocol/core-internal test # vitest run (core) -pnpm --filter @modelcontextprotocol/core-internal test:watch # vitest (watch) -pnpm --filter @modelcontextprotocol/core-internal test -- path/to/file.test.ts -pnpm --filter @modelcontextprotocol/core-internal test -- -t "test name" -``` - -## Breaking Changes - -When making breaking changes, add to the relevant subsystem section in -`docs/migration/upgrade-to-v2.md` (or `docs/migration/support-2026-07-28.md` if the -change is 2026-07-28-only). Mechanical renames go in -`packages/codemod/src/migrations/v1-to-v2/mappings/` and the codemod handles them — do -not reproduce mapping tables in the guide; link to the mapping file instead. - -Include what changed, why, and how to migrate. Search for related sections and group related changes together rather than adding new standalone sections. - -## Code Style Guidelines - -- **TypeScript**: Strict type checking, ES modules, explicit return types -- **Naming**: PascalCase for classes/types, camelCase for functions/variables -- **Files**: Lowercase with hyphens, test files with `.test.ts` suffix -- **Imports**: ES module style, no `.js` extension on relative imports (project uses `moduleResolution: bundler`), group imports logically -- **Formatting**: 2-space indentation, semicolons required, single quotes preferred -- **Testing**: Place tests under each package's `test/` directory (vitest only includes `test/**/*.test.ts`), use descriptive test names -- **Comments**: JSDoc for public APIs, inline comments for complex logic - -### JSDoc `@example` Code Snippets - -JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `client.ts` → `client.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks; region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `applyMiddlewares_basicUsage`, `Client_connect_basicUsage`). For whole-file inclusion (any file type), omit the `#regionName`. - -Run `pnpm sync:snippets` to sync example content into JSDoc comments and markdown files. - -## Architecture Overview - -### Core Layers - -The SDK is organized into three main layers: - -1. **Types Layer** (`packages/core-internal/src/types/types.ts`) - Protocol types generated from the MCP specification. All JSON-RPC message types, schemas, and protocol constants are defined here using Zod v4. - -2. **Protocol Layer** (`packages/core-internal/src/shared/protocol.ts`) - The abstract `Protocol` class that handles JSON-RPC message routing, request/response correlation, capability negotiation, and transport management. Both `Client` and `Server` extend this class. - -3. **High-Level APIs**: - - `Client` (`packages/client/src/client/client.ts`) - Client implementation extending Protocol with typed methods for MCP operations - - `Server` (`packages/server/src/server/server.ts`) - Server implementation extending Protocol with request handler registration - - `McpServer` (`packages/server/src/server/mcp.ts`) - High-level server API with simplified resource/tool/prompt registration - -### Public API Exports - -The SDK separates internal code from the public API surface: - -- **`@modelcontextprotocol/core-internal`** (main entry, `packages/core-internal/src/index.ts`) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (`private: true`). -- **`@modelcontextprotocol/core-internal/public`** (`packages/core-internal/src/exports/public/index.ts`) — Curated public API. Exports TypeScript types, error classes, constants, guards, and the `Protocol` base class (+ `mergeCapabilities`). Re-exported by client and server packages. -- **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core-internal/public`. -- **`@modelcontextprotocol/core`** (`packages/core/src/index.ts`) — Public Zod-schema package and the canonical home of the schema source modules (`src/schemas.ts`, `src/auth.ts`, `src/constants.ts`). The root entry re-exports **only** the `*Schema` Zod constants (MCP spec + OAuth/OpenID) — the published home for raw runtime validation (`CallToolResultSchema.parse(...)`); runtime-neutral (`zod` is its only dependency). The `./internal` subpath re-exports the schema modules wholesale for the sibling packages: `core-internal` re-exports them at the old module paths, and the `client`/`server`/`server-legacy` bundles resolve `@modelcontextprotocol/core/internal` as a real external dependency instead of carrying their own schema copies (their public surfaces stay Zod-free). - -When modifying exports: - -- Use explicit named exports, not `export *`, in package `index.ts` files and `core-internal/public`. -- Adding a symbol to a package `index.ts` makes it public API — do so intentionally. -- Internal helpers should stay in the core internal barrel and not be added to `core-internal/public` or package index files. -- The package root entry must stay runtime-neutral so browser and Cloudflare Workers bundlers can consume it. Exports whose module graph transitively touches unpolyfillable Node builtins (`node:child_process`, `node:net`, `cross-spawn`, etc.) must live at a named subpath export (e.g. `./stdio`) and be covered by a `barrelClean` test in that package. - -### Transport System - -Transports (`packages/core-internal/src/shared/transport.ts`) provide the communication layer: - -- **Streamable HTTP** (`packages/server/src/server/streamableHttp.ts`, `packages/client/src/client/streamableHttp.ts`) - Recommended transport for remote servers, supports SSE for streaming -- **SSE** (`packages/server/src/server/sse.ts`, `packages/client/src/client/sse.ts`) - Legacy HTTP+SSE transport for backwards compatibility -- **stdio** (`packages/server/src/server/stdio.ts`, `packages/client/src/client/stdio.ts`) - For local process-spawned integrations - -### Server-Side Features - -- **Tools/Resources/Prompts**: Registered via `McpServer.tool()`, `.resource()`, `.prompt()` methods -- **OAuth/Auth**: Full OAuth 2.0 server implementation in `packages/server/src/server/auth/` -- **Completions**: Auto-completion support via `packages/server/src/server/completable.ts` - -### Client-Side Features - -- **Auth**: OAuth client support in `packages/client/src/client/auth.ts` and `packages/client/src/client/auth-extensions.ts` -- **Client middleware**: Request middleware in `packages/client/src/client/middleware.ts` (unrelated to the framework adapter packages below) -- **Sampling**: Clients can handle `sampling/createMessage` requests from servers (LLM completions) -- **Elicitation**: Clients can handle `elicitation/create` requests for user input (form or URL mode) -- **Roots**: Clients can expose filesystem roots to servers via `roots/list` - -### Middleware packages (framework/runtime adapters) - -The repo also ships “middleware” packages under `packages/middleware/` (e.g. `@modelcontextprotocol/express`, `@modelcontextprotocol/hono`, `@modelcontextprotocol/node`). These are thin integration layers for specific frameworks/runtimes and should not add new MCP functionality. - -### Experimental Features - -Located in `packages/*/src/experimental/`. Currently empty. - -### Zod Schemas - -The SDK uses `zod/v4` internally. Schema utilities live in: - -- `packages/core-internal/src/util/schema.ts` - AnySchema alias and helpers for inspecting Zod objects - -### Validation - -Pluggable JSON Schema validation (`packages/core-internal/src/validators/`): - -- `ajvProvider.ts` - Default Ajv-based validator -- `cfWorkerProvider.ts` - Cloudflare Workers-compatible alternative - -### Examples - -Runnable examples in `examples//{server.ts,client.ts}` — each story is its own -`@mcp-examples/` workspace package and a self-verifying e2e test (the client connects, -asserts results, exits non-zero on mismatch). `pnpm run:examples` runs every story over its -configured transport×era legs; the `examples (build + e2e)` CI job is part of the per-PR gate -basket. See `examples/README.md` for the full story matrix. - -- `examples/shared/` — `@mcp-examples/shared` package. Root export is args-only (`parseExampleArgs`, `check`, `siblingPath`); the demo OAuth provider and `InMemoryEventStore` live at the `@mcp-examples/shared/auth` subpath so non-auth stories don't eagerly evaluate better-auth/express/better-sqlite3. Stories import only this plumbing and inline the SDK transport setup themselves — see `examples/CONTRIBUTING.md`. -- `scripts/examples/` — runner (`run-examples.ts`) -- `examples/guides/` — per-page snippet companions for the `docs/` guide pages (one `
/.examples.ts` per page); fences sync via `pnpm sync:snippets`, and the runnable ones are executed in CI by `pnpm docs:examples` - -## Message Flow (Bidirectional Protocol) - -MCP is bidirectional: both client and server can send requests. Understanding this flow is essential when implementing new request types. - -### Class Hierarchy - -``` -Protocol (abstract base) -├── Client (packages/client/src/client/client.ts) - can send requests TO server, handle requests FROM server -└── Server (packages/server/src/server/server.ts) - can send requests TO client, handle requests FROM client - └── McpServer (packages/server/src/server/mcp.ts) - high-level wrapper around Server -``` - -### Outbound Flow: Sending Requests - -When code calls `client.callTool()` or `server.createMessage()`: - -1. **High-level method** (e.g., `Client.callTool()`) calls `this.request()` -2. **`Protocol.request()`**: - - Assigns unique message ID - - Checks capabilities via `assertCapabilityForMethod()` (abstract, implemented by Client/Server) - - Creates response handler promise - - Calls `transport.send()` with JSON-RPC request - - Waits for response handler to resolve -3. **Transport** serializes and sends over wire (HTTP, stdio, etc.) -4. **`Protocol._onresponse()`** resolves the promise when response arrives - -### Inbound Flow: Handling Requests - -When a request arrives from the remote side: - -1. **Transport** receives message, calls `transport.onmessage()` -2. **`Protocol.connect()`** routes to `_onrequest()`, `_onresponse()`, or `_onnotification()` -3. **`Protocol._onrequest()`**: - - Looks up handler in `_requestHandlers` map (keyed by method name) - - Creates `BaseContext` with `signal`, `sessionId`, `sendNotification`, `sendRequest`, etc. - - Calls `buildContext()` to let subclasses enrich the context (e.g., Server adds HTTP request info) - - Invokes handler, sends JSON-RPC response back via transport -4. **Handler** was registered via `setRequestHandler('method', handler)` - -### Handler Registration - -```typescript -// In Client (for server→client requests like sampling, elicitation) -client.setRequestHandler('sampling/createMessage', async (request, ctx) => { - // Handle sampling request from server - return { role: "assistant", content: {...}, model: "..." }; -}); - -// In Server (for client→server requests like tools/call) -server.setRequestHandler('tools/call', async (request, ctx) => { - // Handle tool call from client - return { content: [...] }; -}); -``` - -### Request Handler Context - -The `ctx` parameter in handlers provides a structured context: - -**`BaseContext`** (common to both Server and Client), fields organized into nested groups: - -- `sessionId?`: Transport session identifier -- `mcpReq`: Request-level concerns - - `id`: JSON-RPC message ID - - `method`: Request method string (e.g., 'tools/call') - - `_meta?`: Request metadata - - `signal`: AbortSignal for cancellation - - `send(request, schema, options?)`: Send related request (for bidirectional flows) - - `notify(notification)`: Send related notification back -- `http?`: HTTP transport info (undefined for stdio) - - `authInfo?`: Validated auth token info - -**`ServerContext`** extends `BaseContext.mcpReq` and `BaseContext.http?` via type intersection: - -- `mcpReq` adds: `log(level, data, logger?)`, `elicitInput(params, options?)`, `requestSampling(params, options?)` -- `http?` adds: `req?` (HTTP request info), `closeSSE?`, `closeStandaloneSSE?` - -**`ClientContext`** is currently identical to `BaseContext`. - -### Capability Checking - -Both sides declare capabilities during initialization. The SDK enforces these: - -- **Client→Server**: `Client.assertCapabilityForMethod()` checks `_serverCapabilities` -- **Server→Client**: `Server.assertCapabilityForMethod()` checks `_clientCapabilities` -- **Handler registration**: `assertRequestHandlerCapability()` validates local capabilities - -### Adding a New Request Type - -1. **Define schema** in `src/types.ts` (request params, result schema) -2. **Add capability** to `ClientCapabilities` or `ServerCapabilities` in types -3. **Implement sender** method in Client or Server class -4. **Add capability check** in the appropriate `assertCapabilityForMethod()` -5. **Register handler** on the receiving side with `setRequestHandler()` -6. **For McpServer**: Add high-level wrapper method if needed - -### Server-Initiated Requests (Sampling, Elicitation) - -Server can request actions from client (requires client capability): - -```typescript -// Server sends sampling request to client -const result = await server.createMessage({ - messages: [...], - maxTokens: 100 -}); - -// Client must have registered handler: -client.setRequestHandler('sampling/createMessage', async (request, extra) => { - // Client-side LLM call - return { role: "assistant", content: {...} }; -}); -``` - -## Key Patterns - -### Request Handler Registration (Low-Level Server) - -```typescript -server.setRequestHandler('tools/call', async (request, extra) => { - // extra contains sessionId, authInfo, sendNotification, etc. - return { - /* result */ - }; -}); -``` - -### Tool Registration (High-Level McpServer) - -```typescript -mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => { - return { content: [{ type: 'text', text: 'result' }] }; -}); -``` - -### Transport Connection - -```typescript -// Server -// (Node.js IncomingMessage/ServerResponse wrapper; exported by @modelcontextprotocol/node) -const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); -await server.connect(transport); - -// Client -const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp')); -await client.connect(transport); -``` +The principles this SDK is built and reviewed by. They are not a checklist: apply them with judgement. + +1. **Minimalism** — The SDK should do less, not more: prefer changes that add no API or option unless justified. +2. **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. +3. **Strict by default** — Accept what the spec allows, and add no leniency for peers that break it. +4. **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. +5. **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. +6. **The words are true** — Changesets, docs and comments say what the code does. +7. **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. +8. **Brief comments** — Keep inline comments to one line and JSDoc to the public API; the reasoning goes in the commit message or PR description. + +Style belongs to Prettier, ESLint and the compiler. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3a01784ce..0291cd4e40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -110,6 +110,16 @@ Then: 4. Run `pnpm test:all` to verify all tests pass 5. Submit a pull request +### Good to know + +Details about this repository that neither the code nor the tools will tell you. + +- **Spec** — The full spec text is at `https://modelcontextprotocol.io/llms-full.txt`, and the schema for a protocol revision is `schema//schema.ts` in `modelcontextprotocol/modelcontextprotocol`. +- **Tests** — Tests live under each package's `test/` directory; a test file next to its source never runs. +- **Adapters** — The packages under `packages/middleware/` are thin adapters, versioned separately from client and server, so a user can run a new server with an older adapter. +- **Package roots** — A package's root entry stays runtime-neutral so browser and Workers bundlers can use it; Node-only code lives at a subpath export such as `./stdio`. +- **Migration guide** — `docs/migration/upgrade-to-v2.md` covers the upgrade from v1 only; mechanical renames go in `packages/codemod/src/migrations/v1-to-v2/mappings/`. + ### Running Examples See [`examples/README.md`](examples/README.md) for the full list of runnable examples — one self-verifying client/server pair per directory. diff --git a/REVIEW.md b/REVIEW.md index 65b4c35935..9320ba9ff0 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -1,95 +1,5 @@ -# typescript-sdk Review Conventions +# Reviewing typescript-sdk -Guidance for reviewing pull requests on this repository. The first three sections are -stable principles; the **Recurring Catches** section is auto-maintained from past human -review rounds and grows over time. +Review by the principles in `CLAUDE.md`, with judgement: they are not a checklist. -## Guiding Principles - -1. **Minimalism** — The SDK should do less, not more. Protocol correctness, transport - lifecycle, types, and clean handler context belong in the SDK. Middleware engines, - registry managers, builder patterns, and content helpers belong in userland. -2. **Burden of proof is on addition** — The default answer to "should we add this?" is - no. Removing something from the public API is far harder than not adding it. -3. **Justify with concrete evidence** — Every new abstraction needs a concrete consumer - today. Ask for real issues, benchmarks, real-world examples; apply the same standard - to your own review (link spec sections, link code, show the simpler alternative). -4. **Spec is the anchor** — The SDK implements the protocol spec. The further a feature - drifts from the spec, the stronger the justification needs to be. -5. **Kill at the highest level** — If the design is wrong, don't review the - implementation. Lead with the highest-level concern; specific bugs are supporting - detail. -6. **Decompose by default** — A PR doing multiple things should be multiple PRs unless - there's a strong reason to bundle. - -## Review Ordering - -1. **Design justification** — Is the overall approach sound? Is the complexity warranted? -2. **Structural concerns** — Is the architecture right? Are abstractions justified? -3. **Correctness** — Bugs, regressions, missing functionality. -4. **Style and naming** — Nits, conventions, documentation. - -## Checklist - -**Protocol & spec** -- Types match the schema for the protocol revision being changed (`schema//schema.ts` for released revisions; `schema/draft/schema.ts` only for unreleased work) -- Correct `ProtocolError` codes (enum `ProtocolErrorCode`); HTTP status codes match spec (e.g., 404 vs 410) -- Works for both stdio and Streamable HTTP transports — no transport-specific assumptions -- Cross-SDK consistency: check what `python-sdk` does for the same feature - -**API surface** -- Every new export is intentional (see CLAUDE.md § Public API Exports); helpers users can write themselves belong in a cookbook, not the SDK -- New abstractions have at least one concrete callsite in the PR -- One way to do things — improving an existing API beats adding a parallel one - -**Correctness** -- Async: race conditions, cleanup on cancellation, unhandled rejections, missing `await` -- Error propagation: caught/rethrown properly, resources cleaned up on error paths -- Type safety: no unjustified `any`, no unsafe `as` assertions -- Backwards compat: public-interface changes, default changes, removed exports — flagged and justified - -**Tests & docs** -- New behavior has vitest coverage including error paths -- Breaking changes documented in `docs/migration/upgrade-to-v2.md` (or `docs/migration/support-2026-07-28.md` if 2026-only); mechanical renames added to `packages/codemod/src/migrations/v1-to-v2/mappings/` -- Bugfix or behavior change: check whether `docs/**/*.md` describes the old behavior and needs updating; flag prose that now contradicts the implementation -- New feature: verify prose documentation is added (not just JSDoc), and assess whether `examples/` needs a new or updated example -- Behavior change: assess whether existing `examples/` still compile and demonstrate the current API - -## Reference - -When verifying spec compliance, consult the spec directly rather than relying on memory: - -- MCP documentation server: `https://modelcontextprotocol.io/mcp` -- Full spec text (single file, LLM-friendly): `https://modelcontextprotocol.io/llms-full.txt` — fetch to a temp file and grep for the relevant section -- Schema source of truth: the revision-matched `schema.ts` in `modelcontextprotocol/modelcontextprotocol` (`schema//schema.ts` for released revisions; `schema/draft/schema.ts` only for unreleased work) - -## Recurring Catches - -### HTTP Transport - -- When validating `Mcp-Session-Id`, return **400** for a missing header and **404** for an unknown/expired session — never conflate `!sessionId || !transports[sessionId]` into one status, because the client needs to distinguish "fix your request" from "start a new session". Flag any diff that branches on session-id presence/lookup with a single 4xx. (#1707, #1770) - -### Error Handling - -- Broad `catch` blocks must not emit client-fault JSON-RPC codes (`-32700` ParseError, `-32602` InvalidParams) for server-internal failures like stream setup, task-store misses, or polling errors — map those to `-32603` InternalError so clients don't retry/reformat pointlessly. Flag any catch-all that hard-codes ParseError/InvalidParams without discriminating the thrown cause. (#1752, #1769) - -### Schema Compliance - -- When editing Zod protocol schemas in `schemas.ts`, verify unknown-key handling matches the spec `schema.ts`: if the spec type has no `additionalProperties: false`, the SDK schema must use `z.looseObject()` / `.catchall(z.unknown())` rather than implicit strict — over-strict Zod (incl. `z.literal('object')` on `type`) rejects spec-valid payloads from other SDKs. Also confirm the `spec.types.*.test.ts` comparisons still pass bidirectionally. (#1768, #1849, #1169) - -### Async / Lifecycle - -- In `close()` / shutdown paths, wrap user-supplied or chained callbacks (`onclose?.()`, cancel fns) in `try/finally` so a throw can't skip the remaining teardown (`abort()`, `_onclose()`, map clears) — otherwise the transport is left half-open. (#1735, #1763) -- Deferred callbacks (`setTimeout`, `.finally()`, reconnect closures) must check closed/aborted state before mutating `this._*` or starting I/O — a callback scheduled pre-close can fire after close/reconnect and corrupt the new connection's state (e.g., delete the new request's `AbortController`). (#1735, #1763) - -### Completeness - -- When a PR replaces a pattern (error class, auth-flow step, catch shape), grep the package for surviving instances of the old form — partial migrations leave sibling code paths with the very bug the PR claims to fix. Flag every leftover site. (#1657, #1761, #1595) - -### Documentation & Changesets - -- Read added `.changeset/*.md` text and new inline comments against the implementation in the same diff — prose that promises behavior the code no longer ships misleads consumers and contradicts stated intent. Flag any claim the diff doesn't back. (#1718, #1838) - -### CI & GitHub Actions - -- Do **not** assert that a third-party GitHub Action or publish toolchain will fail or needs extra permissions/tokens without verifying its docs or source — `pnpm publish` delegates to the system npm CLI (so npm OIDC works), and `changesets/action` in publish mode has no PR-comment step requiring `pull-requests: write`. For diffs under `.github/workflows/`, confirm claimed behavior in the action's README/source before flagging. (#1838, #1836) +- **Style is not a review topic** — Prettier, ESLint and the compiler own it.