refactor(client,server): move stdio transports to ./stdio subpath export#1871
refactor(client,server): move stdio transports to ./stdio subpath export#1871felixweinberger wants to merge 4 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: c2b85db The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
@claude review |
There was a problem hiding this comment.
LGTM — clean implementation following the existing _shims conditional-export pattern.
Extended reasoning...
Overview
The PR adds browser and workerd export conditions to the client and server package roots that swap the real stdio transports for throwing stubs. This prevents browser/CF Workers bundlers from pulling in node:child_process, node:stream, and cross-spawn. Changes span 14 files: two new index.browser.ts barrel files, two new stdioStub.ts files, updated package.json exports, build config updates, tests, a changeset, and a new SdkErrorCode.TransportNotSupported enum value in core.
Security Risks
None. The stubs throw a helpful error on use rather than silently failing. No auth, crypto, or permissions code is touched.
Level of Scrutiny
Moderate — package.json exports conditions are critical for correct module resolution across runtimes, but the change is additive (new conditions prepended before the existing default) and non-breaking. A diff of the browser vs. node barrel files shows the only difference is the stdio import source, confirming the mirrors are accurate.
Other Factors
The only reported bug is a cosmetic duplicate comment label in sdkErrors.ts, which has no functional impact. @modelcontextprotocol/core is a private (unpublished) workspace package, so the absence of a core entry in the changeset is correct. Tests verify the browser bundle excludes Node-only imports while still exporting the stub class. The approach follows the existing _shims conditional-export pattern already in the repo.
5350730 to
95b5eca
Compare
Stdio transports require a process-spawning runtime (Node.js, Bun, Deno). Exporting them from the package root meant browser and Cloudflare Workers bundlers pulled in node:child_process, node:stream, and cross-spawn. Move StdioClientTransport, getDefaultEnvironment, DEFAULT_INHERITED_ENV_VARS, and StdioServerParameters to @modelcontextprotocol/client/stdio, and StdioServerTransport to @modelcontextprotocol/server/stdio. The root entry is now browser-safe; stdio consumers update one import path.
95b5eca to
46fe6d2
Compare
…stdio in quickstart tsconfig paths
- chunkImportsOf now BFS-walks the chunk import graph so the test name
('transitively imported') matches the implementation
- beforeAll builds the package if dist/ is missing (CI test job runs
pnpm test:all without a build step)
- examples/{client,server}-quickstart/tsconfig.json: add /stdio to paths
so tsc --noEmit resolves the new subpath without a prior build
…ntegration CI test:all runs without building dist; vite-tsconfig-paths needs the /stdio mapping to resolve to src/stdio.ts.
|
@claude review |
…arify subpath comment Server stdio uses only type-level node:stream imports (erased), so the NODE_ONLY regex cannot detect re-export regressions. Add an explicit symbol-absence check and correct the misleading comment in src/stdio.ts.
|
@claude review |
1 similar comment
|
@claude review |
| | `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | ||
| | `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | ||
| | `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | ||
| | `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client/stdio` or `@modelcontextprotocol/server/stdio` | |
There was a problem hiding this comment.
🔴 The shared/stdio.js row should keep pointing at the package root, not the new /stdio subpath. v1's shared/stdio.js exported ReadBuffer, serializeMessage, and deserializeMessage, which in v2 are still re-exported from the root of @modelcontextprotocol/client and @modelcontextprotocol/server (via core/public); the new ./stdio subpaths export only the transport classes. Following this row as written produces import { ReadBuffer } from '@modelcontextprotocol/client/stdio', which fails to resolve.
Extended reasoning...
What the bug is
This PR updates three rows in the docs/migration-SKILL.md import-mapping table to point at the new ./stdio subpath. Two of those updates (sdk/client/stdio.js → @modelcontextprotocol/client/stdio and sdk/server/stdio.js → @modelcontextprotocol/server/stdio) are correct. The third — @modelcontextprotocol/sdk/shared/stdio.js → @modelcontextprotocol/client/stdio or @modelcontextprotocol/server/stdio — is wrong and is a regression introduced by this PR.
Why it's wrong
In v1, @modelcontextprotocol/sdk/shared/stdio.js exported the message-framing utilities, not transports. The current source at packages/core/src/shared/stdio.ts confirms the export set: ReadBuffer (line 7), deserializeMessage (line 44), and serializeMessage (line 48).
In v2, those three symbols are re-exported from packages/core/src/exports/public/index.ts:68:
export { deserializeMessage, ReadBuffer, serializeMessage } from '../../shared/stdio.js';Both packages/client/src/index.ts and packages/server/src/index.ts end with export * from '@modelcontextprotocol/core/public';, so these symbols are available from the root of @modelcontextprotocol/client and @modelcontextprotocol/server — exactly what the pre-PR table row said.
The new ./stdio subpaths created in this PR do not re-export these utilities. packages/client/src/stdio.ts exports only StdioServerParameters, DEFAULT_INHERITED_ENV_VARS, getDefaultEnvironment, and StdioClientTransport. packages/server/src/stdio.ts exports only StdioServerTransport. Neither exports ReadBuffer, serializeMessage, or deserializeMessage.
Why nothing catches this
migration-SKILL.md is an LLM-consumed mapping table, not type-checked code. The barrelClean tests added in this PR verify the bundle contents but say nothing about which v2 module each v1 path's symbols actually live in, so the doc regression slips through.
Impact
This file is explicitly designed to be loaded as a skill so an LLM can mechanically rewrite imports during a v1→v2 migration. A user with v1 code like import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/sdk/shared/stdio.js'; (custom transport authors are the typical consumers) will have it rewritten to import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/client/stdio';, which fails with "Module has no exported member 'ReadBuffer'". The pre-PR mapping (root package) worked; this PR breaks it.
Step-by-step proof
- v1 source:
import { ReadBuffer } from '@modelcontextprotocol/sdk/shared/stdio.js'; - Migration skill consults the table at
docs/migration-SKILL.md:69and rewrites toimport { ReadBuffer } from '@modelcontextprotocol/client/stdio'; @modelcontextprotocol/client/stdioresolves (perpackages/client/package.jsonexports) todist/stdio.mjs, built frompackages/client/src/stdio.ts- That entry exports only
StdioClientTransport,getDefaultEnvironment,DEFAULT_INHERITED_ENV_VARS, and theStdioServerParameterstype — noReadBuffer - tsc / bundler fails:
Module '"@modelcontextprotocol/client/stdio"' has no exported member 'ReadBuffer' - The correct rewrite (and the pre-PR table value) is
import { ReadBuffer } from '@modelcontextprotocol/client';, which resolves viacore/public/index.ts:68
How to fix
Revert just this one row to its pre-PR value:
| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |(Alternatively, also re-export ReadBuffer/serializeMessage/deserializeMessage from both ./stdio subpaths — but since these are pure framing utilities with no Node-only deps, keeping them at the root is consistent with the rest of the "shared" rows and avoids forcing a Node-only subpath import where none is needed.)
Move stdio transports to a
./stdiosubpath export so the package root no longer pulls innode:child_process,node:stream, orcross-spawn. Fixes bundling for browser and Cloudflare Workers consumers. Node.js, Bun, and Deno consumers update one import path.Motivation and Context
The v2 root barrel exports
StdioClientTransport, which top-level importscross-spawnandnode:child_process. Any bundler targeting browsers fails. v1 avoided this with deep subpath imports; this restores that boundary as a single./stdiosubpath.How Has This Been Tested?
barrelClean.test.tsin both packages asserts the builtdist/index.mjs(and its transitive chunks) contain zero process-spawning runtime imports, and thatdist/stdio.mjsexports the moved symbols.Breaking Changes
Yes — stdio symbols are no longer exported from the package root. Import from
@modelcontextprotocol/{client,server}/stdio.Types of changes
Checklist
Additional context
Also moves
getDefaultEnvironment,DEFAULT_INHERITED_ENV_VARS, and theStdioServerParameterstype to the client./stdiosubpath.