Skip to content

fix(middleware): stop bundling workspace type graphs into .d.ts (avoids dts-gen OOM)#2339

Merged
felixweinberger merged 2 commits into
mainfrom
fweinberger/tsdown-dts-external-workspace
Jun 23, 2026
Merged

fix(middleware): stop bundling workspace type graphs into .d.ts (avoids dts-gen OOM)#2339
felixweinberger merged 2 commits into
mainfrom
fweinberger/tsdown-dts-external-workspace

Conversation

@felixweinberger

Copy link
Copy Markdown
Contributor

The four packages/middleware/* tsdown builds were inlining the entire @modelcontextprotocol/server (and transitively core) type graph into their bundled .d.mts instead of emitting an external import for the peer dependency. On main this is ~14s and a 540 KB sourcemap for @modelcontextprotocol/node (a ~200-line wrapper); once core's public type surface grows past a fairly low threshold the dts step OOMs at the default Node heap.

Motivation and Context

rolldown-plugin-dts (the dts bundler tsdown uses) decides external-vs-bundle by testing the resolved path for node_modules. With pnpm workspace symlinks the realpath is packages/server/... — no node_modules in it — so the peer dep is treated as local source and inlined. The dev tsconfig.json paths (which map @modelcontextprotocol/* to workspace source for IDE/typecheck) compound this by routing the resolver straight at source files. The explicit dts.compilerOptions.paths overrides that were already in these configs were dead — wrong relative depth — so they never did anything.

This is fixed upstream in rolldown-plugin-dts@0.21.0 (sxzz/rolldown-plugin-dts@03998d41), which honours rolldown's external resolution before the node_modules path test. We're pinned one minor behind via tsdown: ^0.18.0. Bumping tsdown is the right long-term fix but the same upstream release also removed the dts.resolve option that packages/{server,client}/tsdown.config.ts rely on, so the bump is a separate, larger change — TODO left in packages/middleware/node/tsdown.config.ts.

How Has This Been Tested?

  • @modelcontextprotocol/node build: 14s → 0.8s; dist/index.d.mts 22.77 KB → 4.90 KB (sourcemap 539 KB → 0.37 KB). Output now starts with import { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport, WebStandardStreamableHTTPServerTransportOptions } from "@modelcontextprotocol/server".
  • hono / fastify dist/index.d.mts are byte-identical to main (they only import from their framework peer); express differs only by replacing inlined server types with the external import.
  • JS bundle (dist/index.mjs) is byte-identical for all four — the change is scoped to dts.compilerOptions.
  • Consumer typecheck against built dist: NodeStreamableHTTPServerTransport is assignable to Transport from @modelcontextprotocol/server and accepted by McpServer.connect().
  • Verified the OOM no longer reproduces on a branch with a larger core type surface that previously crashed with node::OOMErrorHandler / exit 134.
  • pnpm typecheck:all, pnpm build:all, middleware test suites (126 tests) all pass.

Breaking Changes

None. @modelcontextprotocol/server is already a required peerDependency of every middleware package, so the external type import resolves for all existing consumers.

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 retargets packages/middleware/node/src/streamableHttp.ts's type-only import from the private @modelcontextprotocol/core package to @modelcontextprotocol/server, which re-exports the same five types via core/public. Middleware packages should only depend on their declared peer dep.
  • Because the peer dep is externalised by path-match before its types are read, middleware dts emit does not require packages/server/dist to exist — output is byte-identical with it absent, so there is no new build-order constraint.
  • Follow-up: bump tsdown (pulls rolldown-plugin-dts >=0.21.0) and drop this override; rework the dts.resolve: ['ajv', 'ajv-formats'] usage in server/client at the same time.

…ds dts-gen OOM)

The four middleware packages were inlining the entire @modelcontextprotocol/server
(and transitively core) type graph into their bundled .d.mts instead of emitting
an external `import` for the peerDependency. On main this is ~14s and a 540 KB
sourcemap for a 200-line wrapper; once core's public surface grows past a fairly
low threshold the tsdown dts step OOMs at the default Node heap.

Root cause: rolldown-plugin-dts decides external-vs-bundle by checking the
resolved path for `node_modules`. With pnpm workspace symlinks the realpath is
`packages/server/...` (no `node_modules`), and the dev tsconfig `paths` map the
specifier to source, so the peer dep is treated as local and inlined.

Fix: clear `paths` for dts emit and set `preserveSymlinks: true` so the resolved
path keeps `node_modules` and the plugin externalises it. The emitted .d.mts now
references `@modelcontextprotocol/server` directly (correct, since it's a peer
dep). Also switch the @modelcontextprotocol/node source import from the private
`core` package to `server`, which re-exports the same types.

Note: middleware dts emit now reads server's built `dist/*.d.mts` rather than
its source. `pnpm -r build` orders this correctly; a standalone middleware build
needs server built first.

Claude-Session: https://claude.ai/code/session_01PHVERaA4Sw7Ui3zk88xSWC
…ide TODO

rolldown-plugin-dts@0.21.0 (03998d41) honours rolldown's `external` resolution
before the node_modules path test, which makes this override unnecessary once
tsdown is bumped past ^0.18. The bump also removes `dts.resolve`, so it needs
to land together with a server/client config rework — tracked separately.

Claude-Session: https://claude.ai/code/session_01PHVERaA4Sw7Ui3zk88xSWC
@felixweinberger felixweinberger requested a review from a team as a code owner June 22, 2026 21:16
@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: acd22fe

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@2339

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2339

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@2339

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-legacy@2339

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@2339

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2339

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@2339

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@2339

commit: acd22fe

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — build-config-only change scoped to dts emit, with the type-only import correctly retargeted to the declared peer dep (all six types are re-exported from @modelcontextprotocol/server).

Extended reasoning...

Overview

This PR only touches the four packages/middleware/* tsdown configs (replacing dead baseUrl/paths overrides with paths: {} + preserveSymlinks: true under dts.compilerOptions) and retargets a type-only import in packages/middleware/node/src/streamableHttp.ts from the private @modelcontextprotocol/core package to @modelcontextprotocol/server. There is no runtime logic change — the JS bundles are unaffected, and the only behavioral difference is that the bundled .d.mts files now emit external imports for the workspace peer dependency instead of inlining its type graph.

Verification

I confirmed that all of AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, and Transport are exported from @modelcontextprotocol/server (via its export * from '@modelcontextprotocol/core/public', which re-exports types/types.ts and shared/transport.ts), and WebStandardStreamableHTTPServerTransportOptions is exported directly — so the consolidated import in streamableHttp.ts resolves. @modelcontextprotocol/server is already a required peerDependency of every middleware package, so the externalized type import resolves for existing consumers.

Security risks

None — this is build/declaration-emit configuration plus an import-source swap for type-only imports. No auth, protocol, or transport runtime code paths change.

Level of scrutiny

Low-to-moderate: it does change the shape of published .d.mts artifacts for four packages, but in the direction of correctness (peer dep types referenced externally rather than duplicated/inlined), and the rationale plus a TODO for the upstream rolldown-plugin-dts fix are well documented in the config comment. The packages are at 2.0.0-alpha, limiting downstream blast radius.

Other factors

The bug-hunting pass found no issues, the repository is already at this PR's head commit with typecheck/build passing, and the author's PR description documents byte-level comparisons of dist output. The only open item is the changeset-bot notice (no changeset), which the maintainers can decide on; it doesn't affect correctness.

@felixweinberger felixweinberger merged commit 6312f2a into main Jun 23, 2026
21 checks passed
@felixweinberger felixweinberger deleted the fweinberger/tsdown-dts-external-workspace branch June 23, 2026 09:26
@claude claude Bot mentioned this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants