Repository navigation
fix(utils,auth,protocol): a browser Web Worker gets the browser variants; Node variants go to workerd - #1038
Conversation
webpack's target: 'webworker', Parcel's workers, and esbuild or Rollup given the `worker` condition resolve `worker` and `browser`. @frontmcp/utils mapped `worker` to its Node variants in its imports (#crypto-provider, #async-context, #event-emitter, #env, #runtime-context, #path) and its root export, @frontmcp/auth in its root export, and @frontmcp/protocol in #mcp-streamable-http, #mcp-server and #server-types, so the SDK failed in a browser Web Worker with `process is not defined` or a missing Node built-in. The Node variants now take the `workerd` condition, which wrangler and @cloudflare/vite-plugin pass with `worker` and `browser`, so a Cloudflare Worker resolves the same files as before (a wrangler dry run bundles the same 1,652 inputs into a byte-identical worker.js) and a browser Web Worker gets the browser variants, as a page does. The `worker` keys that already pointed at stubs stay. Fixes #1031
…e files it gives a page The guard listed the splits by hand, so it missed the SDK's #protocol-client and would not catch a new split that maps `worker` to a Node file. It now resolves every imports and exports entry of every workspace package with and without `worker`, for import and require.
… up to 1.10 @frontmcp/auth's root export had no `worker` key on release/1.10.x; it came with the browser builds.
…the Node variants
…ix/g01-web-worker-conditions
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-03T10:29:57.963Z |
|
@coderabbitai review |
Action not completed
|
|
@coderabbitai review |
Action performed
|
Stacked on #1037
What and why
Bug fix. Fixes #1031.
webpack's
target: 'webworker'resolves packages with theworkerandbrowserconditions.@frontmcp/utilsmappedworkerto its Node variants, both in itsimports(#crypto-provider,#async-context,#event-emitter,#env,#runtime-context,#path) and, since #1030, in its root export, where it selects the single-file ESM bundle.@frontmcp/auth's root export did the same, and so did@frontmcp/protocol's#mcp-streamable-http,#mcp-serverand#server-types(which importshttp). So a browser Web Worker gotnode:crypto,AsyncLocalStorage,node:events,node:pathandprocess.env. On the base, webpack'starget: 'webworker'build ofcreate()+ one tool stops on 13 unresolved Node built-ins. With those left empty, the worker throws while it loads.What each runtime passes
I checked each runtime in its bundler's source and built with it where I could.
target: 'webworker'worker,browsernew Worker(new URL(...))) in atarget: 'web'buildworkeronly for the entry request. The worker's imports resolve like the page's.@parcel/node-resolver-core3.7), web and service workersworker,browserplatform: 'browser',conditions: ['worker']worker,browserbrowser: true,exportConditions: ['worker']worker,browserbrowser: truebrowserbrowser: truedefault,module,import)module,browserworkerd,worker,browser@cloudflare/vite-plugin1.67workerd,worker,module,browser@cloudflare/vitest-pool-workers0.23workerd,worker,module,browserworkerd(esbuild,platform: 'node')@vercel/node29edge-light,browser@netlify/edge-bundler16.2)node(esbuild,platform: 'node')@fastly/js-compute4.0)fastly,browserdeno/bun/nodewebpack, wrangler, Vite and esbuild were built with from the repo's
node_modules. Rollup was built with from a scratch install. For Parcel, Next.js, Turbopack,@vercel/node, Netlify, Fastly, OpenNext,@cloudflare/vite-plugin,@cloudflare/vitest-pool-workersand wrangler 2.x and 3.0, I read the published source. Miniflare runs what it is given and doesn't bundle.No
frontmcp buildtarget passesworker.--target cloudflareleaves bundling to wrangler;vercelandlambdabundle with rspacktarget: 'node';browseruses esbuild withbrowser; the SEA andmcpbbuilds use esbuildplatform: 'node'.Two notes in the issue don't hold for these versions:
edge-lightandbrowser, notworker. It got the browser variants before this change and still does.target: 'web'build, webpack's worker entry appliesworkerto the entry request only. Its imports resolve like the page's, so onlytarget: 'webworker'(and Parcel) hit the bug.Rollup without
browser: truebuilds for Node, so theworkerkey never reached it. There's nothing to fix in the packages for that case: the catalog now tells Rollup users to passbrowser: true.What changes
The Node variants are keyed on
workerd, the condition only Cloudflare's bundlers pass, instead ofworker:@frontmcp/utils: the siximportsand the root export.@frontmcp/auth: the root export.@frontmcp/protocol:#mcp-streamable-http,#mcp-serverand#server-types.The
workerkeys that already point at the browser stubs or at the shared file stay, because they're right for any worker: protocol's#stdio-client,#stdio-serverand#client, and the SDK's#sse-transport,#express-hostand#protocol-client. The SDK's root export has noworkerkey. A browser Web Worker now resolves every split the way a page does.Cloudflare keeps exactly the files it resolved before. I ran
wrangler deploy --dry-run --metafile(wrangler 4.100) on a Worker servingcreateFetchHandler+ one tool, built from the packages before and after the change. Both runs bundle the same 1,652 inputs into a byte-identicalworker.js(SHA-199ff8beaf8365db6921abc7bf82f26ba4d0b2461). The same dry run on thefrontmcp build --target cloudflareoutput of demo-e2e-cloudflare's fixture also bundles the same 2,424 inputs into a byte-identicalindex.js.nx test demo-e2e-cloudflare, which runsfrontmcp build --target cloudflareand thenwrangler devon workerd, passes.A Worker bundled by hand with
workerandbrowserbut withoutworkerdnow gets the browser variants. Those variants read no environment variables and have no Web-standard MCP transport. The skippedmanaged-edge-autoupdatee2e bundled that way, and it now passes wrangler's three conditions. The Cloudflare reference tells custom bundlers to passworkerd.Not in this PR
On the base and on this branch, a webpack 5 build of the SDK for a page (
target: 'web') stops on 6node:URIs that webpack can't read without configuration:node:cluster,node:netandnode:pathfromserver/security/unix-socket-guard;node:httpandnode:streamfromshared/unix-socket-fetch;node:osfromshared/shared-files.The first three come from a static chain,
index→provider/create-container→duty/duty.lifecycle(#1014) →server/security/unix-socket-guard. The other three come through theimport('../shared/index.js')inFrontMcpInstance.runShared(#1019), which webpack follows too. The per-module browser build keeps thesenode:imports. Vite replaces them with an empty module and the bundle-size e2e marks them external, but webpack 5 refuses thenode:scheme. That build failure isn't specific to Web Workers. After this change atarget: 'webworker'build fails on the same 6 URIs, down from 13. The e2e's webpack build leaves Node built-ins empty, as a webpack app has to today. This deserves its own issue.Bundle size
nx run-many -t build -p utils auth protocol sdk plugin-webmcp --skip-nx-cache, thennx test demo-e2e-bundle-size:create()+ one tool, esbuild, minifiedcreate()+ WebMcpPlugin + one toolcreate()+ one toolA page resolves
browser, which this change doesn't touch, so the page bundles don't move.User-facing change
@frontmcp/utils,@frontmcp/authand@frontmcp/protocolgive their Node variants to theworkerdcondition instead ofworker. A browser Web Worker now gets the browser variants, as a page does. This covers webpack'starget: 'webworker', Parcel's workers, and esbuild or Rollup givenworkeralongsidebrowser. Cloudflare Workers built by wrangler or@cloudflare/vite-pluginresolve the same files as before.target: 'webworker',create()failed withprocess is not definedor an unresolved Node built-in. It now starts and serves tools.worker+browserand noworkerdused to get the Node variants and now gets the browser ones. It has to passworkerd, as wrangler does.workerdcondition).frontmcp-deployment/references/build-for-browser.md: Web Workers get the browser builds, Rollup needsbrowser: true, esbuild needsplatform: 'browser', plus a troubleshooting row.frontmcp-deployment/references/deploy-to-cloudflare.md: wrangler's conditions, custom bundlers must passworkerd, plus a troubleshooting row.Tests
apps/e2e/demo-e2e-bundle-size/e2e/browser-web-worker.e2e.spec.ts(new). It bundlescreate()+ one tool as a browser Web Worker twice: with webpacktarget: 'webworker', and with esbuildplatform: 'browser'+conditions: ['worker']. Each bundle is checked for the Node variants of utils, auth and protocol, then run in a worker-like context with noprocess,Buffer,globalorsetImmediate, where the worker posts its tool result.AsyncLocalStorage is not a constructor.node:http2.scripts/worker-export-conditions.test.mjs. For everyimportsandexportsentry of every package underlibs/andplugins/, it checks that a browser Web Worker resolves the file a page resolves, forimportandrequire, with and withoutmodule. It also resolves every split and the utils and auth root exports with the condition sets of Cloudflare (workerd,worker,browser), a browser Web Worker (worker,browser), Vercel Edge (edge-light,browser), a browser and Node.scripts/strip-dist-from-pkg.test.mjs. Its subpath-import fixture maps the Node variant withworkerd, as the packages now do.nx test demo-e2e-bundle-size: 10 suites, 44 tests passed, with the sizes above.nx test demo-e2e-cloudflare: 2 suites, 16 tests passed. One suite is skipped as on the base.node --test scripts/*.test.mjs: 88 passed.node scripts/check-worker-isolate-safety.mjs: clean.nx test utils2,117 passed,auth2,855,protocol9,skills249.nx run-many -t lint -p utils auth protocol skills: no errors.tsc --noEmit -p apps/e2e/demo-e2e-bundle-size/tsconfig.e2e.json: no errors.target: 'web'with and without a worker entry, and Rollup withbrowser: true, without it, and withbrowser: true+exportConditions: ['worker'], each before and after the change.Summary by CodeRabbit
Bug Fixes
Documentation