Skip to content

Rsbuild audit: MCP Apps compiler path, Workbench, docsite builder #572

Description

@ScriptedAlchemy

Dedicated Rsbuild audit, deeper than the combined survey in #566. Findings already filed there are not restated: #566 §1 P3 (MCP Apps overrideBrowserslist), §5 (Workbench filenameHash/cache headers, .map files entry, browserslist) and its "Already compliant" list are out of scope here and referenced by section where a new finding builds on them.

Audited: main @ d88cc10 (fix: Node 26 transform flag; … (#554)), in a detached worktree, pnpm install --frozen-lockfile && pnpm build, Node v22.23.2, pnpm 11.23.0. Nothing was committed; the worktree was removed.

Versions (installed → latest): @rsbuild/core 2.2.1 → 2.2.3 (bump already implied by #566 §1 P1, not re-filed), @rsbuild/plugin-react 2.1.0 → 2.1.0, @rspack/core 2.2.1 (and 2.1.10 via @rslib/core 0.23.2, per #566) → 2.2.2, @rspress/core 2.0.21 → 2.0.21, rsbuild-plugin-rsc 0.1.1 → 0.1.1. @rsdoctor/rspack-plugin 1.6.3 was installed only as audit scratch in the worktree; main does not depend on it. Full table in §6.

What was actually built and measured (all scratch output under /tmp):

  • examples/mcp-app view through the real composeMcpAppsRsbuildConfig/compileMcpApps (same code path as agent-bundle build), plus Rsdoctor (brief JSON) and Rspack stats composition; the CLI itself against the example and against a deliberately broken copy.
  • Two throwaway fixtures through compileMcpApps: a .ts entry importing a .tsx component, and the same entry as .tsx; seven tools.rsbuild hatch variants.
  • Workbench production build through createWorkbenchConfig (3 cold runs, 3 warm runs with performance.buildCache, 3 rsbuild build CLI runs), dev-server first compile (3 runs), Rsdoctor brief JSON on the production build.
  • Docsite: two full rspress build runs (cold, then warm with Rspress's default performance.buildCache populated) through an untracked wrapper config that dumps the resolved Rsbuild and Rspack config.

Skills applied: rsbuild-best-practices, rsbuild-v2-upgrade, rspack-best-practices, rspack-split-chunks, rspack-debugging, rspack-tracing, rsdoctor-analysis; option semantics verified against the installed @rsbuild/core@2.2.1 typings (dist/types/config.d.ts) and source (dist/626.js), not memory.


1. MCP Apps compiler path (packages/agent-bundle/src/build/mcp-apps.ts)

This is what every plugin author's app ships through, so it is ordered first.

P1 · packages/agent-bundle/src/build/mcp-apps.ts:49,189 · rsbuild-best-practices: register @rsbuild/plugin-react for the build, not per file — the automatic JSX runtime is a project-level SWC setting (swcReactOptions.runtime) · usesReactSyntax decides from the entry extension alone. A .ts entry that imports a .tsx component (the natural layout: views/status.ts + views/StatusPanel.tsx) gets no React plugin, so Rsbuild's bare SWC transform lowers the component's JSX to the classic factory. Verified with a fixture through the real compileMcpApps: the emitted HTML contains return React.createElement("strong",{className:"w"},t) with a free React identifier, the build succeeds with no warning, and the app throws ReferenceError: React is not defined the moment the host renders it; the identical fixture with a .tsx entry compiles to (0,e.jsx)(...) via react/jsx-runtime. Same trap for .js.jsx. rslib.ts:623-628 already documents this exact failure for Rslib surfaces and applies pluginReact({ fastRefresh: false }) unconditionally. · Apply pluginReact() to every app environment unconditionally (it is inert for JSX-free code: splitChunks is already false, fast refresh is dev-only), or keep the switch but decide on the module graph (stats.modules containing \.[jt]sx$) and fail with a diagnostic when JSX was compiled without the plugin. Add a unit test: .ts entry importing a .tsx module, assert !/[^.\w]React\.createElement/.test(html). · Risk: none; both fixture outputs are ~190 kB either way.

P1 · packages/agent-bundle/src/build/mcp-apps.ts:196 (logLevel: 'silent'), :281-298 · rspack-debugging: read stats.errors/stats.warnings and surface them with file and line; rsbuild-best-practices: never swallow compiler diagnostics · Rsbuild 2.2.1's build() (@rsbuild/core/dist/626.js, RSPACK_BUILD_ERROR) rejects with a bare Error('Rspack build failed.') when buildState.hasErrors; the per-file errors exist only in the logger output, which 'silent' mutes. Nothing on this path reads stats.errors or stats.warnings (collectBundledOutputEvidence requests neither; there is no ignoreWarnings, so warnings are not filtered — they are simply never looked at). Verified with the real CLI on a copy of examples/mcp-app: a syntax error in views/status-panel.ts, an unresolved import, and a tsconfig.json whose extends target is missing all print exactly [{"code":"AB5000","message":"Rspack build failed.","severity":"error"}] — no file, line, or message. With logLevel: 'error' the same build prints × Tsconfig not found /tmp/tsconfig.json / × [html-rspack-plugin]: Child compilation failed. Every Rslib surface uses logLevel: 'error' (cli-bins.ts:201, entries.ts:517, package-build.ts:327, rslib.ts:759), so MCP Apps is the only compile path in the product that is fully mute, and it is the one plugin authors iterate on most. · Keep 'silent' (one structured channel) but add a profile plugin: api.onAfterEnvironmentCompile(({ environment, stats }) => …) fires with stats even when stats.hasErrors(); collect stats.toJson({ all: false, errors: true, warnings: true, moduleTrace: true }), and throw a DiagnosticError with a dedicated code (a new AB47xx, "MCP App view failed to compile", registered in docs/diagnostics.md so the hosts/diagnostics pages regenerate) whose message lists app · file:line · message per error; emit warnings as severity: 'warning' diagnostics. · Risk: low — the Workbench and CLI already render diagnostics; the only behaviour change is a specific code instead of the AB5000 catch-all.

P2 · packages/agent-bundle/src/build/mcp-apps.ts:196 + default performance.printFileSize · rsdoctor-analysis: know the composition before shipping; rsbuild-best-practices: watch output size · Because logging is silent, Rsbuild's file-size table never prints and the CLI summary is only Built <name> to <dir>; the manifest carries sizes but nothing shows them. Measured: the 3.2 kB examples/mcp-app view compiles to a 437 kB HTML (104 kB gzip). Composition (Rspack stats, pre-minify): zod 1.42 MB across 190 modules (v3 types.js 128 kB and v4 core — @modelcontextprotocol/sdk@1.30's zod-compat pulls both), @modelcontextprotocol/sdk 263 kB (types.js 72 kB, shared/protocol.js 51 kB), zod-to-json-schema 98 kB, @modelcontextprotocol/ext-apps 65 kB, author code 3.2 kB. This floor is inherent to @modelcontextprotocol/ext-apps@1.7.5 (peer @modelcontextprotocol/sdk ^1.29, zod) — no Rsbuild knob removes it — but authors are never told, and a "why is my 2 kB view 440 kB" report is predictable. · Print per-app inlined size and gzip size in the build summary (the evidence pass already touches every emitted asset), and state the expected floor in website/docs/en/guide/authoring/mcp.mdx (+ zh) with the reason. · Risk: none.

P2 · packages/agent-bundle/src/build/mcp-apps.ts:190-193 · Rsbuild html config docs (html.title default 'Rsbuild App', html.mountId default 'root', built-in template in @rsbuild/core/dist/626.js) · When an app declares no template, the emitted document is Rsbuild's built-in shell. Verified output: <!DOCTYPE html><html><head><title>Rsbuild App</title><meta charset="utf-8"><meta name="viewport" …></head><body><div id="root"></div><script defer>…. Every template-less MCP App therefore ships into hosts titled "Rsbuild App", with no lang on <html>, and mounts at #root — a convention documented nowhere in website/docs/en/guide/authoring/mcp.mdx, so an author who renders into #app gets a blank view with a green build. · Set html.title to the app name (or meta.name), html.mountId explicitly, html.tags/template lang, and document the mount id and title on the authoring page (+ zh); alternatively ship a framework default template. · Risk: none.

P2 · packages/agent-bundle/src/build/mcp-apps.ts:216-219 (enforceInvariants alias agent-bundle/meta$) · Rsbuild resolve.aliasStrategy docs (default 'prefer-tsconfig': tsconfig.json paths win over resolve.alias) · The framework's identity module is delivered by an alias to a virtual module, but the profile leaves aliasStrategy at its default, so the author's tsconfig can shadow it. Verified: a fixture tsconfig.json with paths: { "agent-bundle/meta": ["./stub-meta.ts"] } builds green and the HTML contains the stub's string instead of the framework identity — the app reports a wrong name/version inside the host with no diagnostic. paths entries for agent-bundle/* are a plausible editor-resolution workaround for authors. · Pin resolve.aliasStrategy: 'prefer-alias' in enforceInvariants (and assert it in assertResolvedViewConfig), or fail validation when the author's paths cover agent-bundle/meta. The same check applies to the Rslib surfaces' framework aliases (rslib.ts), out of scope here. · Risk: none for compliant projects.

P3 · packages/agent-bundle/src/build/mcp-apps.ts:189 + @rsbuild/plugin-react/dist/index.js:75-128 · Rsbuild resolve.dedupe docs; rspack-split-chunks: with splitChunks: false the plugin's react cache group never applies · pluginReact() does not set resolve.dedupe, and the profile does not either, so a plugin author whose tree has a nested react/react-dom (monorepo, ext-apps peer skew) can ship two React copies inside one self-contained HTML — "Invalid hook call" at runtime. Rsdoctor on examples/mcp-app shows one copy, so this is latent, not observed. · When the React plugin is applied, set resolve.dedupe: ['react', 'react-dom', 'scheduler'] on that environment. · Risk: none.

P3 · packages/agent-bundle/src/build/mcp-apps.ts:190-193 · Rsbuild html.templateParameters docs (default exposes compilation, rspackConfig, htmlPlugin, assetPrefix to <%= %>) · Author templates are handed the full html-rspack-plugin parameter set; nothing documents which parameters are stable, and a template that interpolates compilation or rspackConfig can dump absolute paths into the shipped HTML. · Pass a minimal templateParameters allow-list (e.g. app name, meta) and document it. · Risk: rare; sharp when hit.

P3 · packages/agent-bundle/src/build/mcp-apps.ts:91-93 · rspack-debugging: an assertion that fails must name the artefact that failed it · assertSelfContainedViews throws "Rsbuild emitted files beyond the stable self-contained MCP App HTML output." without listing the files. Verified: tools.rsbuild = { output: { sourceMap: { js: 'source-map' } } } and tools.rsbuild = { mode: 'development' } both hit it (the extra file is the .map), while { js: 'inline-source-map' } passes and grows the HTML to 1.35 MB. · Include the unexpected relative paths in the message and a hint that only inline source maps stay self-contained. · Risk: none.

P3 · packages/agent-bundle/src/build/mcp-apps.ts:51-83 · rsbuild-best-practices: pin production invariants and verify them on the resolved config (inspectConfig) · assertResolvedViewConfig pins cleanDistPath, filenameHash, inlineScripts, inlineStyles, dataUriLimit, splitChunks, asyncChunks and output.path, but not mode, output.minify or performance.buildCache. The profile's mode: 'production' (#566 "compliant") is therefore a default, not an invariant: tools.rsbuild = { mode: 'development' } only fails today as a side effect of the .map file (previous finding); with an inline source map it would ship process.env.NODE_ENV === 'development' React and the react-refresh runtime into hosts, because pluginReact() is called bare (:189) while rslib.ts:628 and the framework-plugins.ts:16-18 comment say fastRefresh: false. performance.buildCache is likewise unpinned (Rslib pins it false at rslib.ts:618-622 for concurrent-build safety; several agent-bundle build processes can share one project's node_modules/.cache). · Assert mode === 'production' and performance.buildCache === false in the same loop, pass pluginReact({ fastRefresh: false }), and comment beside the assertion which of minify/sourceMap the hatch may change. · Risk: none.

2. MCP Apps dev loop in the Workbench

P1 · packages/agent-bundle/src/dev/artifacts/artifact-service.ts:80-85dev/coordinator.ts:491,578packages/workbench/src/overview-model.ts:100-103 · rspack-debugging: the first thing a failing build must give you is the compiler's own error; rsbuild-best-practices "Debugging" · This is the dev-loop face of §1 P1. When a view fails to compile under agent-bundle dev, the coordinator wraps the bare Error('Rspack build failed.') as AB7100 · "Unable to compile the build: Rspack build failed.", which is what the Overview Diagnostics table and the build.failed Dev Log entry show; the previous good epoch stays pinned (correct), but the author has no file, line or message anywhere in the Workbench. Rslib surfaces are no better inside the Workbench: their logLevel: 'error' output goes to the agent-bundle process stderr, which dev-log-producers.ts:154-163 does not capture (only MCP session mcp.stderr is). AB7100 is emitted at artifact-service.ts:81,144 but is not listed in docs/diagnostics.md, so the generated diagnostics reference page does not document the code authors actually see. · Once §1 P1 attaches the Rspack errors to a structured diagnostic, propagate it unchanged through artifact-service.ts:75-79 (it already forwards DiagnosticError.diagnostics) so the Overview shows app · file:line · message; add AB7100 to docs/diagnostics.md. · Risk: none.

P2 · packages/agent-bundle/src/build/mcp-apps.ts:196-207 + website/docs/en/guide/authoring/mcp.mdx, website/docs/en/guide/development/workbench.mdx:179-180 · rsbuild-best-practices "CLI": rsbuild (dev mode, source maps, readable output) for local development, rsbuild build for production · agent-bundle dev compiles MCP App views through the identical production profile (mode: 'production', minified, output.sourceMap: false, silent) — verified path coordinator.ts:540 → artifact-service.ts:208 → build.ts:402 → mcp-apps.ts:268. No Rsbuild dev server or createDevServer exists anywhere under packages/agent-bundle/src (only the Workbench's own in workbench-server.ts:546), which is a deliberate design (workbench.mdx:179-180), but it means the preview sandbox shows minified code with no source maps and the only escape hatch — tools.rsbuild = { output: { sourceMap: { js: 'inline-source-map' } } }, verified working, HTML 437 kB → 1.35 MB — is documented nowhere; mode: 'development' and external maps fail opaquely (§1 P3). · Either add a dev-only overlay in compileMcpApps (inline source maps + minify: false when mode === 'development' is passed from the coordinator — the self-contained assertion still holds for inline maps), or document the working hatch and DEBUG=rsbuild in mcp.mdx + zh and in the Workbench MCP Apps page. · Risk: inline maps triple the preview HTML (dev only); external .map files must stay forbidden for resource packaging.

P3 · packages/agent-bundle/src/dev/mcp-apps/mcp-app-preview-service.ts:322, dev/mcp-session/mcp-session.ts:138-146 · rsbuild-best-practices: dev loop = edit → rebuild → the view refreshes · After a successful rebuild artifact.available publishes a new epoch, but an open MCP session stays pinned to its epoch and the preview HTML was loaded once via bridge.loadResource(); nothing re-loads the resource into the sandbox iframe. The author must close and re-open Open App preview (or start a new session) to see the rebuilt view — the loop is edit → rebuild → notice nothing changed → re-open. The rebuild trigger itself is fine (ProjectWatcher watches the whole project root, watcher.ts:50-54, so imported components and templates invalidate, not only the entry). · Offer an opt-in "reload preview on new epoch" (the Workbench already receives artifact.available over SSE) or state the re-open step next to the preview button and in workbench.mdx. · Risk: an automatic swap must stay opt-in so epoch evidence guarantees hold.

3. Workbench (packages/workbench/rsbuild.config.ts)

P1 · packages/workbench/src/mcp/mcp-route-client.ts:592-595, packages/agent-bundle/src/dev/foreground-server.ts:746, 804-808, 821-827 vs packages/workbench/rsbuild.config.ts:40 and website/docs/en/guide/development/workbench.mdx:170-178 · rsbuild-best-practices "CLI: use rsbuild (dev server) for local development"; Rsbuild server.proxy docs (changeOrigin rewrites Host only, never Origin) · The documented contributor HMR loop — AGENT_BUNDLE_WORKBENCH_API_PROXY=http://127.0.0.1:3100 pnpm --filter agent-bundle-workbench dev, proxying /api — cannot complete a session. The browser sits on the Rsbuild origin (http://localhost:3000); /api/project/session answers origin: this.url (http://127.0.0.1:3100) and the client throws AB8003 "Foreground session bootstrap origin does not match this browser." because location.origin !== body.origin. Independently, every mutation POST carries Origin: http://localhost:3000, which #assertMutationSession rejects with AB8003 (origin !== this.url); changeOrigin: true (Rsbuild default, asserted in tests/rsbuild-workbench.test.ts:43) does not help since http-proxy rewrites Host, not Origin. Read-only GETs and the SSE stream work, so the loop looks alive until the first MCP page or mutation. Verified by reading both sides; not run. · Make the foreground server dev-proxy aware: accept an explicitly configured proxy origin (e.g. AGENT_BUNDLE_WORKBENCH_DEV_ORIGIN) in #assertMutationSession/#assertSessionBootstrapOrigin and echo it in the bootstrap origin, or serve the Rsbuild dev middleware from the foreground origin (rsbuild.createDevServer() + server.middlewareMode). Add a test that drives the proxied path end to end, and fix workbench.mdx (+ zh) once it works. · Risk: relaxing only the client-side check still leaves protected POSTs failing; the allowed origin must be an explicit opt-in, never derived from the request.

P2 · packages/workbench/tests/mcp-app-frame.test.ts:130-151 and 7 siblings (lifecycles-page.browser.test.tsx:47, mcp-json-input.test.ts:47, mcp-app-preview-browser.test.ts:75, mcp-page-app-browser.test.ts:149, evals-real.e2e.test.ts:91, comparisons-page-client-scope-browser.test.ts:66, runtime-consent-dialog.test.ts:58) · rsbuild-best-practices "Configuration: one rsbuild.config.ts/defineConfig, reuse it"; AGENTS.md "look for the helper before writing it" · Eight browser fixtures each re-own createRsbuild + bare pluginReact() + workbenchBrowserAliases instead of createWorkbenchConfig, and they already diverge: distPath.assets vs production static, no mode pin (rsbuild.config.ts:49 pins it), extensionAlias only in mcp-app-frame.test.ts:146. Four more (discovery-atoms-disposal.test.ts:185, runtime-document-atoms-disposal.test.ts:135, route-editor-atoms-disposal.test.ts:133, runtime-inspector.test.ts:85) call createWorkbenchConfig() and mutate it, inheriting the notices output.copy and — if AGENT_BUNDLE_WORKBENCH_API_PROXY happens to be set in the shell — the proxy. · One tests/support/workbench-fixture-config.ts (mode: 'production', pluginReact(), aliases, temp distPath/entry, optional extensionAlias) used by all twelve. · Risk: none beyond a mechanical refactor.

P2 · packages/workbench/rsbuild.config.ts:40 + website/docs/en/guide/development/workbench.mdx:176, website/docs/en/contributing/index.mdx · Rsbuild ServerConfig (config.d.ts:317-390: port 3000, host localhost, strictPort false, open false) · The HMR docs give the env + one command; nothing says which URL opens (Rsbuild picks 3000, or the next free port silently — strictPort false), that only /api is proxied, or that MCP App / runtime iframes come from separate loopback origins the proxy does not cover. contributing/index.mdx has no Workbench text at all. · Document the default URL and the proxy scope once P1 above works; consider server.strictPort: true for the contributor config so a port collision fails loudly instead of moving the UI. · Risk: none.

P3 · packages/agent-bundle/src/dev/workbench-assets.ts:42 + packages/workbench/rsbuild.config.ts:20-21 · Rsbuild output.copy / asset management · contentTypeFor falls back to application/octet-stream for extensionless paths, so the two files output.copy ships — THIRD_PARTY_NOTICES and src/mcp/APP-RENDERER-LICENSE — are served as binary downloads by #serveAsset (foreground-server.ts:953). Nothing in packages/workbench/src links them (they are the packaging contract in packages/agent-bundle/NOTICE:11 and tests/dev-workbench-packaging.test.ts:46), so this is cosmetic. · Map extensionless basenames to text/plain; charset=utf-8. · Risk: none.

4. Docsite builder (website/rspress.config.ts, no builderConfig)

P2 · .github/workflows/docs.yml:48-61 + Rspress initRsbuild.js:197-215 · Rsbuild performance.buildCache docs (persistent cache only pays off when the cache directory survives between builds) · Rspress enables performance.buildCache by default (node_modules/.cache, ≈89 MB after one build here), but the Docs job restores only the pnpm store, so every CI build pays cold compile plus the cache write. Measured locally: cold 196.6 s vs warm 188.8 s (−4 %) — the wall time is TypeDoc + SSG of 1906 pages, not Rspack, so restoring the cache via actions/cache would buy little and cost a ~90 MB artefact per key. · Set RSPRESS_PERSISTENT_CACHE=false in docs.yml (skips the write) unless a measured actions/cache restore shows a real gain; do not cache blindly. · Risk: none (the cache is dead weight in CI today).

No other builderConfig change is justified. With no builderConfig (website/rspress.config.ts:54-194), the resolved web environment is mode: 'production', target: web, assetPrefix: '/agent-bundle/' (from base), filenameHash: true, JS sourceMap: false (0 .map files in doc_build), legalComments: 'linked' (three .LICENSE.txt, ~2 kB), polyfill: 'off', dataUriLimit 4 KiB, printFileSize.compressed, security.sri off (same-origin Pages), no tools.rspack beyond Rspress's own, source.define only Rspress SSR/LLMS flags. Route chunks already get <link rel="preload"> from Rspress's RouteChunkAssetsPlugin; blanket performance.prefetch over ~1900 async chunks would hurt. OG/description meta come from Rspress SSG (rspress.config.ts:57-61), so theme-color etc. belong in Rspress head, not html.meta. website/plugins/*.ts use Rspress config hooks only, no builderConfig/modifyRsbuildConfig (mirror-api-locale.ts:87-114, generated-reference.ts:911-921).

5. Other @rsbuild/core / createRsbuild users

P2 · examples/rsc-agent-runtime/rsbuild.config.ts:203-206 + examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts:563-568 · rspack-debugging: surface stats.errors text; Rsbuild onAfterDevCompile hook docs · Same shape as §1 P1 on the third compile path in the product: on stats.hasErrors() the observer fails the attempt with a fixed Error('RSC runtime compile reported errors.') and the session emits AB8206 "RSC runtime source build failed."; stats.toJson({ errors: true }) is never read, warnings never inspected (hasWarnings unused), logLevel left at 'info' so the real message lands only on the process console. The README advertises a Workbench compile-error capture for this example. · Attach the stats errors (file:line:message) to the AB8206 diagnostic and set logLevel: 'error'. · Risk: none.

P2 · examples/rsc-agent-runtime/rsbuild.config.ts:362-403 vs packages/agent-bundle/src/build/mcp-apps.ts:198-214 · rsbuild-best-practices asset/bundle guidance; rspack-split-chunks: web default preset still extracts lib-react under pluginReact · The app environment claims self-contained HTML with inlineScripts/inlineStyles + dynamicImportMode: 'eager', but unlike composeMcpAppsRsbuildConfig it sets neither splitChunks: false, dataUriLimit: Number.MAX_SAFE_INTEGER, asyncChunks: false nor an assertion like assertResolvedViewConfig; the default 4 KiB dataUriLimit means any asset >4 KiB becomes a sibling file, and legalComments: 'linked' already emits lib-react.js.LICENSE.txt beside the HTML (a host test expects it). · Reuse the mcp-apps invariant set (or its assertion) for app, and legalComments: 'inline' if siblings are unwanted. · Risk: none; today's HTML-only checks pass.

P3 · examples/rsc-agent-runtime/rsbuild.config.ts:288, 299, 368-369 · Rsbuild mode docs; @rsbuild/plugin-react fastRefresh default true · mode: 'production' is pinned even when options.mode === 'development' (rsbuild-runtime-session.ts:845), so the long-lived agent-bundle dev provider ships production React (no dev warnings) and Fast Refresh can never engage even though pluginReact() is called without { fastRefresh: false } (Rslib and the framework-plugins.ts:16-18 comment say refresh is off). · Either let mode follow options.mode for the web envs, or pass pluginReact({ fastRefresh: false }) and document that "dev" is production-compiled with onAppReload only. · Risk: none.

P3 · examples/rsc-agent-runtime/rsbuild.config.ts:290, 373-387 · Rsbuild dev.writeToDisk, output.cleanDistPath, output.filenameHash docs · Development forces dev.writeToDisk: true with cleanDistPath: false on all envs, but filenameHash: false is applied only in the production app branch — so a long dev session accumulates stale hashed app assets on disk and in staged checkpoints. · Apply filenameHash: false to development app too. · Risk: none.

P3 · examples/rsc-agent-runtime/rsbuild.config.ts:148-149 · Rsbuild output.cleanDistPath docs · emitRuntimeManifest does rm(dirname(environments.rsc.distPath)) — i.e. dist/ — wiping sibling widget/app output to compensate for cleanDistPath: false; apply: 'build' so it never races writeToDisk. · rm each environment's own distPath. · Risk: none.

P3 · examples/rsc-agent-runtime/rsbuild.config.ts:341-360, 362-403 · rsbuild-best-practices "keep browserslist aligned" (output.overrideBrowserslist) · Neither widget nor app pins targets — the same gap #566 §1 P3 files for mcp-apps.ts, on a second surface. · Pin the same list the framework adopts. · Risk: none.

P3 · packages/create-agent-bundle/templates/** · (informational) · Templates ship no Rsbuild config, plugins or scripts; mcp-server/tsconfig.json:15 sets jsx: react-jsx for route .tsx compiled by the package build. No MCP App views exist in templates, so nothing to change until one is added — at which point §1 P1 (.ts entry + .tsx component) is the trap to avoid.

6. Rsbuild 2.x currency

P2 · packages/rsc-runtime/package.json:94 · rsbuild-v2-upgrade step 4: bump every Rsbuild/Rspack package in lockstep (taze major --include /rsbuild/) · The exact devDependency pin "@rspack/core": "2.2.1" mirrors the ^2.2.0-0 peer at :78; nothing under packages/rsc-runtime/ imports @rspack/core. @rsbuild/core@2.2.3 (the #566 §1 P1 target) depends on @rspack/core ~2.2.2, so landing the bump without moving this line splits the tree into @rspack/core 2.2.1 + 2.2.2 (two native @rspack/binding-* downloads) and lets @rspack/plugin-react-refresh@2.0.2 peer-resolve against whichever copy pnpm picks. pnpm why @rspack/core already shows two versions today (2.1.10 via @rslib/core@0.23.2, 2.2.1 via @rsbuild/core@2.2.1 and this pin); #566 collapses the 2.1.x copy only if :94 moves too. · Bump :94 to 2.2.2 in the same PR, or drop the devDependency and rely on the peer. · Risk: none (no import).

P3 · packages/agent-bundle/src/core/types.ts:259 + packages/agent-bundle/src/config/validate.ts:1835-1882 · rsbuild-v2-upgrade "Migrate performance.chunkSplitsplitChunks" · The tools.rsbuild hatch is typed as @rsbuild/core@2.2.1's EnvironmentConfig, which still admits the two @deprecated keys performance.chunkSplit (config.d.ts:584) and output.sourceMap.extract.js (config.d.ts:816); validateTools (AB4720–AB4724) checks shape and plugin collisions only. Because both framework profiles set splitChunks: false (mcp-apps.ts:210, rslib.ts:632), a consumer chunkSplit hits Rsbuild's "Both performance.chunkSplit and splitChunks are set" branch (dist/626.js:9425) and is discarded — via logger.warn, below the 'silent'/'error' levels the framework builds with, so the author sees nothing. · Reject both keys in validateTools with an AB472x diagnostic pointing at the v2 replacement, and mention it on the tools-hatch docs page (en + zh). · Risk: none for compliant configs.

P3 · pnpm-workspace.yaml:24 · pnpm docs "minimumReleaseAge / minimumReleaseAgeExclude" · minimumReleaseAgeExclude: ['@rsbuild/core@2.2.1'] was added in #20 because 2.2.1 was <24 h old at the time; pnpm 11's built-in release-age window has long passed for 2.2.1, and both @rsbuild/core@2.2.3 and @rslib/core@1.0.0 (published 2026-09-03) are already outside it, so the entry neither helps nor blocks the #566 bump — it is dead config that will name a version no longer in the lockfile. · Delete the line in the bump PR, or set minimumReleaseAgeExcludePrune: true (pnpm ≥11.22; repo is on 11.23.0). · Risk: none.

No deprecated Rsbuild 2.x option is in use anywhere. The only @deprecated keys in the installed typings are performance.chunkSplit, output.sourceMap.extract.js and dev.setupMiddlewares; git grep across packages/agent-bundle/src, packages/workbench, examples, rstest.*.ts, website/rspress.config.ts and website/plugins finds none of them nor any v1 leftovers (source.alias, dev.startUrl, tools.htmlPlugin, output.enableLatestDecorators, source.transformImport, dev.progressBar, output.distPath.server, proxy context/onProxy*, onDevCompileDone, htmlWebpackPlugin template params) outside a correct migration comment at rslib.ts:629. Every hook and JS-API call in use is current (onAfterEnvironmentCompile, onBeforeDevCompile, onAfterDevCompile, startDevServer({ getPortSilently }), loadEnv, mergeRsbuildConfig). Peer ranges are satisfied by 2.2.1 and by 2.2.3: @rsbuild/plugin-react@2.1.0 (^2.0.0, latest), rsbuild-plugin-rsc@0.1.1 (^2.0.0), rsbuild-plugin-publint@1.0.0, @rstest/core@0.11.10 (~2.2.0; latest 0.11.12 wants ~2.2.2), @rspress/core@2.0.21 (^2.1.13). Not re-filed: the two-@rsbuild/core tree (2.1.13 via @rslib/core@0.23.2, 2.2.1 elsewhere) is #566 §1 P1.

package installed latest declared in
@rsbuild/core 2.2.1 (+ 2.1.13 via @rslib/core@0.23.2) 2.2.3 package.json:68, packages/agent-bundle/package.json:101, packages/workbench/package.json:30, examples/rsc-agent-runtime/package.json:28
@rsbuild/plugin-react 2.1.0 2.1.0 same four files
@rspack/core 2.2.1 (+ 2.1.10 via @rsbuild/core@2.1.13) 2.2.2 packages/rsc-runtime/package.json:94 (exact)
@rslib/core 0.23.2 1.0.0 (#566) root, agent-bundle, create-agent-bundle, rsc-markdown-stream, rsc-runtime
@rstest/core, adapter-rslib, browser, browser-react, playwright 0.11.10 0.11.12 root + packages + examples
@rslint/core 0.8.2 0.9.1 (<24 h old at audit time) package.json:72, packages/agent-bundle/package.json:104
@rspress/core + plugin-{llms,sitemap,twoslash,typedoc} 2.0.21 2.0.21 website/package.json:18-22
rsbuild-plugin-rsc 0.1.1 0.1.1 examples/rsc-agent-runtime/package.json:36
rsbuild-plugin-publint 1.0.0 1.0.0 package.json:84
@rsdoctor/rspack-plugin not a dependency of main — installed only as audit scratch in the worktree 1.6.3

Measurements

MCP Apps — examples/mcp-app view (views/status-panel.ts + template)

Metric Value
Compile (programmatic compileMcpApps, 1 view, warm process) ≈1.1 s; agent-bundle build for the whole example 6.0 s wall
Emitted 1 file, mcp-apps/status.html, 437.3 kB, 104.4 kB gzip
Author code in it 3.2 kB (views/status-panel.ts) + 0.3 kB generated agent-bundle/meta
Pre-minify composition (Rspack stats, incl. concatenated modules) zod 1423 kB / 190 modules (v3 + v4), @modelcontextprotocol/sdk 263 kB / 10, zod-to-json-schema 98 kB / 63, @modelcontextprotocol/ext-apps 65 kB / 2
Largest modules zod/v3/types.js 128 kB, zod/v4/core/schemas.js 96 kB, zod/v4/core/compile.js 85 kB, @modelcontextprotocol/sdk/dist/esm/types.js 72 kB, …/shared/protocol.js 51 kB, ext-apps/dist/src/app.js 33 kB
Duplicate packages (Rsdoctor E1001) none; 5 packages, 163 modules, 1 chunk
Warnings / errors 0 / 0 (nothing reads them — see §1 P1)
Fixture .ts entry → .tsx component 190.0 kB HTML, classic React.createElement with free React (broken at runtime)
Fixture .tsx entry → .tsx component 190.6 kB HTML, automatic runtime (react/jsx-runtime)
tools.rsbuild hatch probes inline-source-map OK (1.35 MB), source-map FAIL (opaque), mode: 'development' FAIL (opaque), minify: false OK (575 kB), removeConsole OK, overrideBrowserslist OK, source.define OK

Workbench — production build, Rsdoctor (rsdoctor-data.json, brief mode)

Metric Value
Assets 31 (25 JS, 1 CSS, index.html, 2 .LICENSE.txt, THIRD_PARTY_NOTICES, src/mcp/APP-RENDERER-LICENSE)
JS total 2790 kB (605 kB gzip) across 25 files
CSS total 76.9 kB (13.2 kB gzip), one static/css/index.css
Initial JS (all three loaded on every start) 1367 kB / 376 kB gzip: static/js/463.js 634 kB (176 kB gz, default vendors group), static/js/index.js 549 kB (142 kB gz, app code), static/js/lib-react.js 185 kB (58 kB gz)
Async chunks 22, largest async/743.js 237 kB (markdown stack), four Shiki grammars at 171–177 kB each (typescript, jsx, tsx, javascript), async/763.js 138 kB
Packages / modules / chunks 108 / 837 / 25
Duplicate package versions none (Rsdoctor E1001 empty; one react, one react-dom, one effect, one zod)
Largest packages (parsed) @shikijs/langs 1005 kB (async), Workbench source 542 kB, zod 332 kB (134 kB in the initial vendors chunk), react-dom 174 kB, @modelcontextprotocol/client@2.0.0 157 kB, effect 119 kB + 62 kB, @modelcontextprotocol/sdk@1.30.0 31 kB, @modelcontextprotocol/ext-apps 28 kB
Initial vendors chunk 463.js composition @modelcontextprotocol/client 157 kB + 40 kB, zod 134 kB, effect 119 kB + 62 kB, @modelcontextprotocol/sdk 31 kB, ext-apps 28 kB, micromark/hast/property-information ≈ 70 kB
Largest modules @shikijs/langs/dist/typescript.mjs 177 kB, jsx.mjs 174 kB, tsx.mjs 171 kB, javascript.mjs 171 kB, react-dom-client.production.js 170 kB, @modelcontextprotocol/client/dist/src-*.mjs 94 kB, src/mcp/mcp-page.tsx 45 kB, effect/dist/internal/effect.js 43 kB
Rsdoctor build costs bootstrap→beforeCompile 0.73 s, compile 3.28 s, minify 1.05 s, afterCompile→done 0.53 s (with the Rsdoctor plugin attached: 4.8–5.8 s total)

Workbench — timings (3 runs each, medians; same machine, shared with other agents)

Scenario Runs Median
Production build, fresh process, no persistent cache (what pnpm build does) 1284 / 1386 / 1387 ms 1386 ms compile (rsbuild build CLI wall incl. Node + pnpm start-up: 2.24 / 2.26 / 2.72 s → 2.26 s)
Production build with performance.buildCache populated (not enabled in the repo; measured to size the option) 514 / 518 / 545 ms 518 ms (−63 %); populate run 1497 ms
Dev server start → first compile done (startDevServer, onDevCompileDone) 629 / 611 / 706 ms 629 ms

Docsite — rspress build (Rspress 2.0.21 on Rsbuild 2.2.1; TypeDoc + twoslash + SSG of 1906 pages)

Metric Value
Cold build 196.6 s wall, 8.36 GB max RSS
Warm build (Rspress's default performance.buildCache populated) 188.8 s wall, 8.35 GB — the persistent cache buys ≈4 %, the time is TypeDoc + SSG, not Rspack
doc_build 623 MB; HTML 602 MB over 1907 pages (avg 316 kB, max 386 kB — each page inlines the SSG-rendered sidebar with 921 API items; content-level, #566 §4 territory); static/js 14.75 MB / 1916 files; CSS 80 kB
Entry chunk static/js/index.<hash>.js 2.23 MB (373 kB gzip), dominated by route/sidebar metadata for ~1900 API pages; lib-react 189 kB, lib-router 35 kB; largest async route chunk 286 kB
Resolved Rsbuild (web env) mode: production, target: ['web','es2017'], assetPrefix: '/agent-bundle/', filenameHash: true, sourceMap: false (JS), legalComments: 'linked', polyfill: 'off', minify on, dataUriLimit 4096, html.inject: 'head', scriptLoading: 'defer', performance.buildCache on (Rspress default, buildDependencies = Rspress initRsbuild.js + the config), printFileSize.compressed, Lightning CSS targets chrome ≥107, edge ≥107, firefox ≥104, safari ≥16, chunk split chunks: 'all' with lib-react/lib-router/styles cache groups

Already compliant (do not re-audit)

In addition to #566's list:

  • MCP AppscomposeToolsLayers order profile → tools.rsbuildtools.rspack → invariants (compose-layers.ts:32-36); enforceInvariants runs last and pins asyncChunks: false, the meta alias and VirtualModulesPlugin (mcp-apps.ts:212-238); one Rsbuild environment per app (:188-195); server.publicDir: false (:209); filenameHash: false + distPath.html: 'mcp-apps' → stable [name].html, names validated kebab-case (validate.ts:513) and duplicate names must share identity (mcp-apps.ts:136-145); createRsbuild({ cwd: projectRoot }) so a config in a subdirectory still resolves from the project root (mcp-apps.ts:268, build.ts:402, normalize.ts:852-854); result.close() on success (:296-298) and Rsbuild closes the compiler itself before rejecting; defaults output.charset utf8, polyfill: 'off', injectStyles: false, output.module: false; source.include default transpiles TS/JSX in dependencies but not plain JS; source.tsconfigPath auto-set only when <root>/tsconfig.json exists; provenance ignores the virtual module (:289-291, meta.ts:27-31); Rsdoctor: zero duplicate packages in the example app; security.nonce/sri correctly unset — the sandbox CSP allows 'unsafe-inline' (mcp-app-sandbox.ts:22-32) and there are no external srcs to hash; the tools.rsbuild hatch honours removeConsole, overrideBrowserslist and source.define (verified).
  • MCP Apps dev loop — no Rsbuild dev server is used for app preview, by design (workbench.mdx:179-180); ProjectWatcher watches the project root so entry, imported modules and templates all invalidate (watcher.ts:50-54, coordinator.ts:331-335); a failed rebuild keeps the last good epoch (coordinator.ts:578); collectBundledOutputEvidence replaces planned sourceInputs with the real module graph after each compile (mcp-apps.ts:283-304, provenance.ts:181-248).
  • WorkbenchdefineConfig + exported createWorkbenchConfig, proxy only when the env is set (rsbuild.config.ts:13, 40, 54; scripts/dev.mjs:3); changeOrigin: true is the Rsbuild 2.2.1 default and asserted in tests/rsbuild-workbench.test.ts:43; no path rewrite or ws: true needed — every typed client route is under /api/…, transport is fetch + EventSource, no WebSocket; Rsbuild's compress middleware skips text/event-stream, so SSE survives the proxy; non-/api traffic is intentional (hash routing main.tsx:342, blob: downloads, absolute sandbox iframe origins, data: media); hash routing only, #serveAsset maps /index.html, no SPA fallback needed, HEAD supported (foreground-server.ts:216, 947); #serveAsset sets only content-type, no CSP conflict with favicon="data:," / defer scripts; index.html owns lang, charset, viewport, title, favicon and the app never writes document.title; pluginReact() defaults (fast refresh in dev only, no React Compiler) are right for a SPA; 11 plain .css files, zero .module.css, no PostCSS/Tailwind, nothing relying on Lightning CSS-specific transforms; Rsdoctor: zero duplicate packages (one react, react-dom, effect, zod), 25 chunks; output.copy targets match the packaging contract (NOTICE:11, dev-workbench-packaging.test.ts:46-47); zero console.* in packages/workbench/src, so performance.removeConsole has nothing to do.
  • Docsite — see §4: every resolved Rsbuild default matches the skills for a same-origin GitHub Pages site; baseassetPrefix/publicPath /agent-bundle/; no .map published; legalComments: 'linked' kept for licence compliance; route preloads come from Rspress, not performance.preload.
  • rsc-agent-runtime — per-env tools.rspack.name for MultiStats matching (rsbuild.config.ts:318-320, 359, 399-400); splitChunks: false on the node env matches Rsbuild 2.2's server default (multi-entry chunks: 'all', same rationale as rslib.ts:629-632); widget entries all on client-anchor.ts per the rsbuild-plugin-rsc environments contract; app hmr: false/liveReload: false with a custom reload plugin; emitRuntimeManifest is apply: 'build' so it never races writeToDisk; documented hooks only; server.port: 0/host: '127.0.0.1'/printUrls: false + getPortSilently + server.close(); all four integration tests reuse createRscRuntimeRsbuildConfig; create-agent-bundle templates ship no Rsbuild config or deps; services/mcp-run.ts:199-203 uses loadEnv only.
  • Currency — no deprecated 2.x option in use; all peers satisfied by 2.2.1 and by 2.2.3; @rsbuild/plugin-react, @rspress/*, rsbuild-plugin-rsc, rsbuild-plugin-publint at latest.

Not assessed / caveats

  • Timings were taken on a shared machine with other agents running; medians of three are reported, single runs are noted. The Workbench "warm" number measures performance.buildCache, which the repo does not enable — it sizes the option, it does not describe today's pnpm build.
  • The contributor-HMR origin finding (§3 P1) was established by reading both sides (client check at mcp-route-client.ts:592-595, server checks at foreground-server.ts:804-827, browsers always send Origin on POST, http-proxy changeOrigin rewrites Host only); it was not reproduced live.
  • The .ts.tsx React finding (§1 P1) and the aliasStrategy finding (§1 P2) were reproduced with throwaway fixtures through the real compileMcpApps; the error-surfacing finding was reproduced with the real CLI against a copy of examples/mcp-app.
  • Docsite memory: the Docs job sets no NODE_OPTIONS; peak RSS 8.36 GB fits public ubuntu-latest (16 GB) with headroom, but the same build would OOM on a private-repo 8 GB runner. No Rsbuild knob applies; it is SSG/runner sizing.
  • Not assessed: Rslib packaging surfaces (rslib.ts, entries.ts, cli-bins.ts, package-build.ts) beyond their logLevel for comparison — Rstack skills audit: build compiler, package builds, tests, docsite #566 §1/§2; Rspress content (sidebar size, page count, TypeDoc) — Rstack skills audit: build compiler, package builds, tests, docsite #566 §4; the Rstest runtime-playground configs — Rstack skills audit: build compiler, package builds, tests, docsite #566 §3 deletes them; whether performance.printFileSize still walks assets under logLevel: 'silent' (CPU cost not measured); whether the aliasStrategy shadowing also affects the Rslib surfaces' framework aliases.
  • The audit worktree carried uncommitted scratch (@rsdoctor/rspack-plugin added to the root package.json/lockfile for Rsdoctor runs; an untracked website/rspress.audit.config.ts); main has neither, and nothing was committed or pushed. The worktree has been removed.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions