Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/optional-http-sse-deps.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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) |
Expand All @@ -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) |
Expand Down
56 changes: 46 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,39 +100,66 @@
"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
}
},
"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",
Expand All @@ -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",
Expand Down
64 changes: 64 additions & 0 deletions test/stdio-only-imports.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
Loading