From e344cbe363f7d3fef09b337dae4a0b3d68ec8933 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Sun, 26 Apr 2026 15:01:57 -0700 Subject: [PATCH] feat(deps): make HTTP/SSE transport deps optional for stdio-only consumers Move `express`, `cors`, `express-rate-limit`, `@hono/node-server`, `raw-body`, `content-type`, `eventsource`, `eventsource-parser`, and `jose` from runtime `dependencies` to optional `peerDependencies`. Drop the unused-at-runtime `hono` package entirely (only referenced by an example). For stdio-only consumers (the most common deployment for local MCP servers) this removes ~22 MB / 60+ transitive packages from the install, eliminating supply-chain alerts for code paths the user never loads. Apps that already depend on Express, Hono, etc. via their own `package.json` continue to work unchanged. Adds a static-analysis test that asserts the stdio transport source files never grow a static import of an HTTP/SSE-only peer dep, so the lazy boundary cannot regress silently. Closes #1924. --- .changeset/optional-http-sse-deps.md | 17 ++++++++ README.md | 30 +++++++++++-- package.json | 56 +++++++++++++++++++----- test/stdio-only-imports.test.ts | 64 ++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 .changeset/optional-http-sse-deps.md create mode 100644 test/stdio-only-imports.test.ts diff --git a/.changeset/optional-http-sse-deps.md b/.changeset/optional-http-sse-deps.md new file mode 100644 index 000000000..b92a7273d --- /dev/null +++ b/.changeset/optional-http-sse-deps.md @@ -0,0 +1,17 @@ +--- +'@modelcontextprotocol/sdk': major +--- + +Move HTTP, SSE, and OAuth transport packages from runtime `dependencies` to optional `peerDependencies`. Stdio-only consumers no longer pay an ~22 MB / 60+ transitive package install for code paths they never load (closes #1924). + +Affected packages, now installed only when the matching transport is used: + +- `express`, `cors`, `express-rate-limit` (Express adapters + OAuth helpers) +- `@hono/node-server` (Node `StreamableHTTPServerTransport`) +- `raw-body`, `content-type` (`SSEServerTransport`) +- `eventsource`, `eventsource-parser` (SSE / Streamable HTTP client transports) +- `jose` (`createPrivateKeyJwtAuth`) + +`hono` is dropped entirely from runtime deps (it was only referenced by an example). + +Existing apps that already depend on Express, Hono, etc. in their own `package.json` continue to work unchanged. Apps that relied on the SDK to install these transitively will receive an `ERR_MODULE_NOT_FOUND` at import time pointing at the missing package; install it and the import resolves. See the updated `README.md` for the per-transport install matrix. diff --git a/README.md b/README.md index 2d2f19ae3..91aa21b8f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,30 @@ npm install @modelcontextprotocol/sdk zod This SDK has a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but maintains backwards compatibility with projects using Zod v3.25 or later. You can use either API in your code by importing from `zod/v3` or `zod/v4`: +### Optional peer dependencies (HTTP / SSE / OAuth transports) + +The HTTP, SSE, and OAuth helpers ship as **optional peer dependencies** so that +stdio-only consumers can install `@modelcontextprotocol/sdk` without pulling in +Express, Hono, or related transitive packages +([#1924](https://github.com/modelcontextprotocol/typescript-sdk/issues/1924)). + +Install only what you actually use: + +| You use… | Install | +| --- | --- | +| `StdioClientTransport`, `StdioServerTransport` | nothing extra (just the SDK + `zod`) | +| `StreamableHTTPServerTransport` (Node) | `@hono/node-server` | +| `SSEServerTransport` | `raw-body content-type` | +| `SSEClientTransport` | `eventsource` | +| `StreamableHTTPClientTransport` | `eventsource-parser` | +| `createMcpExpressApp`, OAuth `mcpAuthRouter`, host-header middleware | `express cors express-rate-limit` | +| `createPrivateKeyJwtAuth` (RFC 7523) | `jose` | + +If you import a transport without its peer installed, Node will throw a clear +`ERR_MODULE_NOT_FOUND` at load time pointing at the missing package — install +it and you're done. Existing apps that already depend on Express / Hono via +their own `package.json` continue to work unchanged. + ## Quick Start To see the SDK in action end-to-end, start from the runnable examples in `src/examples`: @@ -117,7 +141,7 @@ The SDK ships runnable examples under `src/examples`. Use these tables to find t ### Server examples -| Scenario | Description | Example file(s) | Related docs | +| Scenario | Description | Example file(s) | Related docs | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Streamable HTTP server (stateful) | Feature-rich server with tools, resources, prompts, logging, tasks, sampling, and optional OAuth. | [`simpleStreamableHttp.ts`](src/examples/server/simpleStreamableHttp.ts) | [`server.md`](docs/server.md), [`capabilities.md`](docs/capabilities.md) | | Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`simpleStatelessStreamableHttp.ts`](src/examples/server/simpleStatelessStreamableHttp.ts) | [`server.md`](docs/server.md) | @@ -132,8 +156,8 @@ The SDK ships runnable examples under `src/examples`. Use these tables to find t ### Client examples -| Scenario | Description | Example file(s) | Related docs | -| --------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| Scenario | Description | Example file(s) | Related docs | +| --------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | Interactive Streamable HTTP client | CLI client that exercises tools, resources, prompts, elicitation, and tasks. | [`simpleStreamableHttp.ts`](src/examples/client/simpleStreamableHttp.ts) | [`client.md`](docs/client.md) | | Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, then falls back to SSE on 4xx responses. | [`streamableHttpWithSseFallbackClient.ts`](src/examples/client/streamableHttpWithSseFallbackClient.ts) | [`client.md`](docs/client.md), [`server.md`](docs/server.md) | | SSE polling client | Polls a legacy SSE server and demonstrates notification handling. | [`ssePollingClient.ts`](src/examples/client/ssePollingClient.ts) | [`client.md`](docs/client.md) | diff --git a/package.json b/package.json index 22d26ca56..be0498179 100644 --- a/package.json +++ b/package.json @@ -100,32 +100,58 @@ "test:conformance:client:all": "npx @modelcontextprotocol/conformance client --command 'npx tsx test/conformance/src/everythingClient.ts' --suite all --expected-failures test/conformance/conformance-baseline.yml" }, "dependencies": { - "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", + "@hono/node-server": "^1.19.9", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "jose": "^6.1.3", + "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0" }, "peerDependenciesMeta": { "@cfworker/json-schema": { "optional": true }, + "@hono/node-server": { + "optional": true + }, + "content-type": { + "optional": true + }, + "cors": { + "optional": true + }, + "eventsource": { + "optional": true + }, + "eventsource-parser": { + "optional": true + }, + "express": { + "optional": true + }, + "express-rate-limit": { + "optional": true + }, + "jose": { + "optional": true + }, + "raw-body": { + "optional": true + }, "zod": { "optional": false } @@ -133,6 +159,7 @@ "devDependencies": { "@cfworker/json-schema": "^4.1.1", "@eslint/js": "^9.39.1", + "@hono/node-server": "^1.19.9", "@modelcontextprotocol/conformance": "^0.1.14", "@types/content-type": "^1.1.8", "@types/cors": "^2.8.17", @@ -144,10 +171,19 @@ "@types/supertest": "^6.0.2", "@types/ws": "^8.5.12", "@typescript/native-preview": "^7.0.0-dev.20251103.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", "eslint": "^9.8.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-n": "^17.23.1", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "prettier": "3.6.2", + "raw-body": "^3.0.0", "supertest": "^7.0.0", "tsx": "^4.16.5", "typescript": "^5.5.4", diff --git a/test/stdio-only-imports.test.ts b/test/stdio-only-imports.test.ts new file mode 100644 index 000000000..3e10588d9 --- /dev/null +++ b/test/stdio-only-imports.test.ts @@ -0,0 +1,64 @@ +/** + * Verifies that the stdio transport source files do not statically import + * any HTTP/SSE-only optional peer dependencies. + * + * The package marks these dependencies as optional in `peerDependenciesMeta`, + * so stdio-only consumers can install `@modelcontextprotocol/sdk` without them. + * If a stdio source file ever grows a static import of an HTTP-only package, + * stdio-only consumers would crash at module-resolution time. + * + * See https://github.com/modelcontextprotocol/typescript-sdk/issues/1924. + */ + +import { describe, expect, test } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(__dirname, '..'); + +// Packages that consumers of stdio-only transports should not be required to install. +const HTTP_ONLY_PEER_DEPS = [ + '@hono/node-server', + 'hono', + 'express', + 'express-rate-limit', + 'cors', + 'eventsource', + 'eventsource-parser', + 'raw-body', + 'content-type', + 'jose' +]; + +const STDIO_TRANSPORT_FILES = [ + 'src/client/stdio.ts', + 'src/server/stdio.ts', + 'src/shared/stdio.ts', + 'src/shared/transport.ts', + 'src/shared/protocol.ts', + 'src/types.ts', + 'src/inMemory.ts' +]; + +function readSrc(relPath: string): string { + return readFileSync(join(repoRoot, relPath), 'utf-8'); +} + +function importsPackage(source: string, pkg: string): boolean { + // Match common ESM import shapes: `from 'pkg'`, `from "pkg"`, `from 'pkg/sub'`, + // `import('pkg')`, `require('pkg')`. Avoid matching `pkg-suffix` packages. + const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const pattern = new RegExp(`(?:from|import|require)\\s*\\(?\\s*['"]${escaped}(?:/[^'"]*)?['"]`); + return pattern.test(source); +} + +describe('stdio-only consumers should not need HTTP/SSE peer deps', () => { + test.each(STDIO_TRANSPORT_FILES)('%s does not statically import any HTTP-only package', file => { + const source = readSrc(file); + for (const pkg of HTTP_ONLY_PEER_DEPS) { + expect(importsPackage(source, pkg), `${file} unexpectedly imports ${pkg}`).toBe(false); + } + }); +});