Skip to content

refactor(client,server): move stdio transports to ./stdio subpath export#1871

Open
felixweinberger wants to merge 8 commits intomainfrom
fweinberger/browser-stdio-conditional-export
Open

refactor(client,server): move stdio transports to ./stdio subpath export#1871
felixweinberger wants to merge 8 commits intomainfrom
fweinberger/browser-stdio-conditional-export

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger commented Apr 9, 2026

Move stdio transports to a ./stdio subpath export so the package root no longer pulls in node:child_process, node:stream, or cross-spawn. Fixes bundling for browser and Cloudflare Workers consumers. Node.js, Bun, and Deno consumers update one import path.

// before
import { Client, StdioClientTransport } from '@modelcontextprotocol/client';
import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';

// after
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';

Motivation and Context

The v2 root barrel exports StdioClientTransport, which top-level imports cross-spawn and node:child_process. Any bundler targeting browsers fails. v1 avoided this with deep subpath imports; this restores that boundary as a single ./stdio subpath.

How Has This Been Tested?

barrelClean.test.ts in both packages asserts the built dist/index.mjs (and its transitive chunks) contain zero process-spawning runtime imports, and that dist/stdio.mjs exports 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Also moves getDefaultEnvironment, DEFAULT_INHERITED_ENV_VARS, and the StdioServerParameters type to the client ./stdio subpath.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 182156c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 9, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1871

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1871

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1871

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1871

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1871

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1871

commit: 182156c

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/core/src/errors/sdkErrors.ts
@felixweinberger felixweinberger force-pushed the fweinberger/browser-stdio-conditional-export branch from 5350730 to 95b5eca Compare April 9, 2026 16:33
@felixweinberger felixweinberger changed the title fix(client,server): browser-conditional export for stdio transports refactor(client,server): move stdio transports to ./stdio subpath export Apr 9, 2026
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.
…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.
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/server/src/stdio.ts
…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.
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

1 similar comment
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread docs/migration-SKILL.md Outdated
Copy link
Copy Markdown
Contributor

@KKonstantinov KKonstantinov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments, none of them blocking:

  • @types/cross-spawn in packages/server/package.json - could be cleaned-up, not used
  • packages/server/src/server/mcp.examples.ts - still imports from /stdio.js, but this file is synced via pnpm sync:snippets. Users who copy the example will get ./stdio.js, which is a path that only resolves inside the monorepo source tree, not from the published package
  • CLAUDE.md - probably would be good to add an explicit rule to the "Public API Exports" section as the PR establishes a clean invariant. E.g. something like

Exports whose module graph transitively touches unpolyfillable Node builtins (node:child_process, node:net, node:stream's runtime APIs, cross-spawn, etc.) must live at a named subpath export (e.g. ./stdio) and be covered by a barrelClean test in that package. The package root must stay runtime-neutral so browser and Cloudflare Workers bundlers can consume it.

  • Five tsconfigs now mirror thw ./stdio subpath by hand - it adds a drift risk, we could do some small follow-up lint/script that reads packages/{client,server}/package.json exports, derives the expected paths entries, and fails if any of the many tsconfigs are missing ones. Not needed for this PR, just an idea as the list grows longer

Comment thread packages/server/test/server/barrelClean.test.ts Outdated
@felixweinberger felixweinberger marked this pull request as ready for review April 15, 2026 19:39
@felixweinberger felixweinberger requested a review from a team as a code owner April 15, 2026 19:39
@felixweinberger felixweinberger added the v2-bc v2 backwards-compatibility series label Apr 15, 2026
Comment thread packages/server/src/index.ts Outdated
@felixweinberger felixweinberger added this to the v2.0.0-bc milestone Apr 15, 2026
…er index comment, align barrelClean NODE_ONLY regex
felixweinberger added a commit that referenced this pull request Apr 17, 2026
felixweinberger added a commit that referenced this pull request Apr 17, 2026
…ods+zod-schemas, take F1 callTool wording); SKILL.md --ours (preserves #1871/A1a/E-track content)
@felixweinberger felixweinberger marked this pull request as draft April 17, 2026 10:56
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread .changeset/stdio-subpath-export.md Outdated
@felixweinberger felixweinberger marked this pull request as ready for review April 27, 2026 14:41
Comment on lines +31 to +33
// StdioServerTransport is exported from the './stdio' subpath — server stdio has only type-level Node
// imports (erased at compile time), but matching the client's `./stdio` subpath gives consumers a
// consistent shape across packages.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The top-level README.md quickstart (line 98) still imports StdioServerTransport from the package root: import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';. After this PR removes that export from the root barrel, anyone copy-pasting the README snippet will hit Module '@modelcontextprotocol/server' has no exported member 'StdioServerTransport'. Every other doc surface (docs/server*.md, docs/client*.md, docs/migration*.md, all examples/**) was migrated to the /stdio subpath — split this line into import { McpServer } from '@modelcontextprotocol/server'; + import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; to match.

Extended reasoning...

What the bug is

README.md:98 reads:

import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';

This is the pre-PR root-barrel import. The PR replaces export { StdioServerTransport } from './server/stdio.js'; in packages/server/src/index.ts with a comment (lines 31-33) directing consumers to the new ./stdio subpath. Once this PR lands, StdioServerTransport is no longer exported from @modelcontextprotocol/server, so the README snippet no longer compiles.

The specific code path that triggers it

A user lands on the GitHub repo or the npm package page (both render the top-level README.md), scrolls to Getting Started, and copies the first code block. tsc (or any bundler) resolves @modelcontextprotocol/server via packages/server/package.jsonexports["."]dist/index.mjs / dist/index.d.mts, built from packages/server/src/index.ts. That entry no longer re-exports StdioServerTransport, so the import fails with:

Module '"@modelcontextprotocol/server"' has no exported member 'StdioServerTransport'.

Why existing code doesn't prevent it

The PR migrated every other doc and example surface — docs/server.md, docs/server-quickstart.md, docs/client.md, docs/client-quickstart.md, docs/migration.md, docs/migration-SKILL.md, and every file under examples/** and test/integration/** — to the /stdio subpath. A repo-wide grep for Stdio(Server|Client)Transport.*from\s+['"]@modelcontextprotocol/(client|server)['"] confirms README.md is the only remaining file with the old root-barrel pattern. README.md is not in the PR's 33-file changed-files list, and unlike the docs/*-quickstart.md snippets it has no source= annotation tying it to a type-checked example file, so neither tsc nor pnpm sync:snippets flags it. The new barrelClean.test.ts checks bundle contents, not documentation.

Impact

The README is the most visible entry point for the project — it is the GitHub landing page and the npm package page. A broken first code block there is worse than in any of the docs/** files that were updated: it is the snippet most likely to be copy-pasted by a new user evaluating the SDK, and the resulting compile error directly contradicts the "Getting Started" framing.

Step-by-step proof

  1. README.md:98import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';
  2. packages/server/package.json exports["."]dist/index.d.mts (built from src/index.ts)
  3. PR diff for packages/server/src/index.ts: -export { StdioServerTransport } from './server/stdio.js'; → replaced by the comment at lines 31-33
  4. dist/index.d.mts therefore has no StdioServerTransport export (and barrelClean.test.ts line 39 explicitly asserts the symbol is absent from dist/index.mjs)
  5. tsc on the README snippet → TS2305: Module '"@modelcontextprotocol/server"' has no exported member 'StdioServerTransport'.
  6. The correct import — used everywhere else in this PR — is import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';, which resolves via the new exports["./stdio"] entry added to packages/server/package.json.

How to fix

Split README.md:98 into two lines, matching docs/server-quickstart.md:130-131 and examples/server-quickstart/src/index.ts:2-3:

import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2-bc v2 backwards-compatibility series

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants