Skip to content

Commit e344cbe

Browse files
committed
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.
1 parent bf1e022 commit e344cbe

4 files changed

Lines changed: 154 additions & 13 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@modelcontextprotocol/sdk': major
3+
---
4+
5+
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).
6+
7+
Affected packages, now installed only when the matching transport is used:
8+
9+
- `express`, `cors`, `express-rate-limit` (Express adapters + OAuth helpers)
10+
- `@hono/node-server` (Node `StreamableHTTPServerTransport`)
11+
- `raw-body`, `content-type` (`SSEServerTransport`)
12+
- `eventsource`, `eventsource-parser` (SSE / Streamable HTTP client transports)
13+
- `jose` (`createPrivateKeyJwtAuth`)
14+
15+
`hono` is dropped entirely from runtime deps (it was only referenced by an example).
16+
17+
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.

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,30 @@ npm install @modelcontextprotocol/sdk zod
3131

3232
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`:
3333

34+
### Optional peer dependencies (HTTP / SSE / OAuth transports)
35+
36+
The HTTP, SSE, and OAuth helpers ship as **optional peer dependencies** so that
37+
stdio-only consumers can install `@modelcontextprotocol/sdk` without pulling in
38+
Express, Hono, or related transitive packages
39+
([#1924](https://github.com/modelcontextprotocol/typescript-sdk/issues/1924)).
40+
41+
Install only what you actually use:
42+
43+
| You use… | Install |
44+
| --- | --- |
45+
| `StdioClientTransport`, `StdioServerTransport` | nothing extra (just the SDK + `zod`) |
46+
| `StreamableHTTPServerTransport` (Node) | `@hono/node-server` |
47+
| `SSEServerTransport` | `raw-body content-type` |
48+
| `SSEClientTransport` | `eventsource` |
49+
| `StreamableHTTPClientTransport` | `eventsource-parser` |
50+
| `createMcpExpressApp`, OAuth `mcpAuthRouter`, host-header middleware | `express cors express-rate-limit` |
51+
| `createPrivateKeyJwtAuth` (RFC 7523) | `jose` |
52+
53+
If you import a transport without its peer installed, Node will throw a clear
54+
`ERR_MODULE_NOT_FOUND` at load time pointing at the missing package — install
55+
it and you're done. Existing apps that already depend on Express / Hono via
56+
their own `package.json` continue to work unchanged.
57+
3458
## Quick Start
3559

3660
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
117141

118142
### Server examples
119143

120-
| Scenario | Description | Example file(s) | Related docs |
144+
| Scenario | Description | Example file(s) | Related docs |
121145
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
122146
| 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) |
123147
| 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
132156

133157
### Client examples
134158

135-
| Scenario | Description | Example file(s) | Related docs |
136-
| --------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
159+
| Scenario | Description | Example file(s) | Related docs |
160+
| --------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
137161
| 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) |
138162
| 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) |
139163
| SSE polling client | Polls a legacy SSE server and demonstrates notification handling. | [`ssePollingClient.ts`](src/examples/client/ssePollingClient.ts) | [`client.md`](docs/client.md) |

package.json

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,39 +100,66 @@
100100
"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"
101101
},
102102
"dependencies": {
103-
"@hono/node-server": "^1.19.9",
104103
"ajv": "^8.17.1",
105104
"ajv-formats": "^3.0.1",
106-
"content-type": "^1.0.5",
107-
"cors": "^2.8.5",
108105
"cross-spawn": "^7.0.5",
109-
"eventsource": "^3.0.2",
110-
"eventsource-parser": "^3.0.0",
111-
"express": "^5.2.1",
112-
"express-rate-limit": "^8.2.1",
113-
"hono": "^4.11.4",
114-
"jose": "^6.1.3",
115106
"json-schema-typed": "^8.0.2",
116107
"pkce-challenge": "^5.0.0",
117-
"raw-body": "^3.0.0",
118108
"zod": "^3.25 || ^4.0",
119109
"zod-to-json-schema": "^3.25.1"
120110
},
121111
"peerDependencies": {
122112
"@cfworker/json-schema": "^4.1.1",
113+
"@hono/node-server": "^1.19.9",
114+
"content-type": "^1.0.5",
115+
"cors": "^2.8.5",
116+
"eventsource": "^3.0.2",
117+
"eventsource-parser": "^3.0.0",
118+
"express": "^5.2.1",
119+
"express-rate-limit": "^8.2.1",
120+
"jose": "^6.1.3",
121+
"raw-body": "^3.0.0",
123122
"zod": "^3.25 || ^4.0"
124123
},
125124
"peerDependenciesMeta": {
126125
"@cfworker/json-schema": {
127126
"optional": true
128127
},
128+
"@hono/node-server": {
129+
"optional": true
130+
},
131+
"content-type": {
132+
"optional": true
133+
},
134+
"cors": {
135+
"optional": true
136+
},
137+
"eventsource": {
138+
"optional": true
139+
},
140+
"eventsource-parser": {
141+
"optional": true
142+
},
143+
"express": {
144+
"optional": true
145+
},
146+
"express-rate-limit": {
147+
"optional": true
148+
},
149+
"jose": {
150+
"optional": true
151+
},
152+
"raw-body": {
153+
"optional": true
154+
},
129155
"zod": {
130156
"optional": false
131157
}
132158
},
133159
"devDependencies": {
134160
"@cfworker/json-schema": "^4.1.1",
135161
"@eslint/js": "^9.39.1",
162+
"@hono/node-server": "^1.19.9",
136163
"@modelcontextprotocol/conformance": "^0.1.14",
137164
"@types/content-type": "^1.1.8",
138165
"@types/cors": "^2.8.17",
@@ -144,10 +171,19 @@
144171
"@types/supertest": "^6.0.2",
145172
"@types/ws": "^8.5.12",
146173
"@typescript/native-preview": "^7.0.0-dev.20251103.1",
174+
"content-type": "^1.0.5",
175+
"cors": "^2.8.5",
147176
"eslint": "^9.8.0",
148177
"eslint-config-prettier": "^10.1.8",
149178
"eslint-plugin-n": "^17.23.1",
179+
"eventsource": "^3.0.2",
180+
"eventsource-parser": "^3.0.0",
181+
"express": "^5.2.1",
182+
"express-rate-limit": "^8.2.1",
183+
"hono": "^4.11.4",
184+
"jose": "^6.1.3",
150185
"prettier": "3.6.2",
186+
"raw-body": "^3.0.0",
151187
"supertest": "^7.0.0",
152188
"tsx": "^4.16.5",
153189
"typescript": "^5.5.4",

test/stdio-only-imports.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Verifies that the stdio transport source files do not statically import
3+
* any HTTP/SSE-only optional peer dependencies.
4+
*
5+
* The package marks these dependencies as optional in `peerDependenciesMeta`,
6+
* so stdio-only consumers can install `@modelcontextprotocol/sdk` without them.
7+
* If a stdio source file ever grows a static import of an HTTP-only package,
8+
* stdio-only consumers would crash at module-resolution time.
9+
*
10+
* See https://github.com/modelcontextprotocol/typescript-sdk/issues/1924.
11+
*/
12+
13+
import { describe, expect, test } from 'vitest';
14+
import { readFileSync } from 'node:fs';
15+
import { fileURLToPath } from 'node:url';
16+
import { dirname, join } from 'node:path';
17+
18+
const __dirname = dirname(fileURLToPath(import.meta.url));
19+
const repoRoot = join(__dirname, '..');
20+
21+
// Packages that consumers of stdio-only transports should not be required to install.
22+
const HTTP_ONLY_PEER_DEPS = [
23+
'@hono/node-server',
24+
'hono',
25+
'express',
26+
'express-rate-limit',
27+
'cors',
28+
'eventsource',
29+
'eventsource-parser',
30+
'raw-body',
31+
'content-type',
32+
'jose'
33+
];
34+
35+
const STDIO_TRANSPORT_FILES = [
36+
'src/client/stdio.ts',
37+
'src/server/stdio.ts',
38+
'src/shared/stdio.ts',
39+
'src/shared/transport.ts',
40+
'src/shared/protocol.ts',
41+
'src/types.ts',
42+
'src/inMemory.ts'
43+
];
44+
45+
function readSrc(relPath: string): string {
46+
return readFileSync(join(repoRoot, relPath), 'utf-8');
47+
}
48+
49+
function importsPackage(source: string, pkg: string): boolean {
50+
// Match common ESM import shapes: `from 'pkg'`, `from "pkg"`, `from 'pkg/sub'`,
51+
// `import('pkg')`, `require('pkg')`. Avoid matching `pkg-suffix` packages.
52+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53+
const pattern = new RegExp(`(?:from|import|require)\\s*\\(?\\s*['"]${escaped}(?:/[^'"]*)?['"]`);
54+
return pattern.test(source);
55+
}
56+
57+
describe('stdio-only consumers should not need HTTP/SSE peer deps', () => {
58+
test.each(STDIO_TRANSPORT_FILES)('%s does not statically import any HTTP-only package', file => {
59+
const source = readSrc(file);
60+
for (const pkg of HTTP_ONLY_PEER_DEPS) {
61+
expect(importsPackage(source, pkg), `${file} unexpectedly imports ${pkg}`).toBe(false);
62+
}
63+
});
64+
});

0 commit comments

Comments
 (0)