Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dc36064
web-host: extract the shared host core from serve-app (#564, lane 1)
ScriptedAlchemy Sep 5, 2026
79c91bd
feat: share browser MCP App relay
ScriptedAlchemy Sep 5, 2026
b4bb3d9
feat: add web config and manifest surface
ScriptedAlchemy Sep 5, 2026
b6248e3
Remove agent-bundle/serve-app-command and advertise the web surface.
ScriptedAlchemy Sep 5, 2026
3fc8a61
feat(agent-bundle): emit web command in artifact bins
ScriptedAlchemy Sep 5, 2026
10ffc49
feat(web-host): the `<plugin> web` command runtime — resolveWebLaunch…
ScriptedAlchemy Sep 5, 2026
d831967
feat(cli-entry): dispatch the framework-owned `web` command before th…
ScriptedAlchemy Sep 5, 2026
0a0c588
feat(dev): serve exposed web apps
ScriptedAlchemy Sep 5, 2026
e309b65
test: retire the serve-app-command tests and fixture for <plugin> web…
ScriptedAlchemy Sep 5, 2026
5e9b83a
test: acceptance proofs for the <plugin> web command (#564)
ScriptedAlchemy Sep 5, 2026
a54a73f
docs: document the web surface, <plugin> web, AB4341, and serve-app-c…
ScriptedAlchemy Sep 5, 2026
4edf98c
test(web-command): canonical input type in the fake selection (#564 i…
ScriptedAlchemy Sep 5, 2026
9cb2572
build(bin): a web-only bin imports no @agent-bundle/runtime; the web …
ScriptedAlchemy Sep 5, 2026
a1ce5c5
docs(site): publish agent-bundle/web-host in the API reference (#564)
ScriptedAlchemy Sep 5, 2026
415bdce
fix(web): web never displaces an authored executable (AB4341 names th…
ScriptedAlchemy Sep 5, 2026
b36333b
fix(config): optional generatedCli access in web bin ownership rule
ScriptedAlchemy Sep 5, 2026
ab86e06
fix(dev): watcher ignores output staging dirs (spurious rebuild after…
ScriptedAlchemy Sep 5, 2026
505aa73
chore: changeset PR number
ScriptedAlchemy Sep 5, 2026
74bb06a
chore(doctor): replace stale web-surface plan comment with the lenien…
ScriptedAlchemy Sep 5, 2026
6756481
chore(564): deslop pass over the web surface diff
ScriptedAlchemy Sep 5, 2026
67047c4
fix(564): isolate web-host teardown listeners; web-only bins skip sta…
ScriptedAlchemy Sep 5, 2026
ef3a25d
fix(build): a web-only bin is judged on the web capability row, not cli
ScriptedAlchemy Sep 5, 2026
4a37174
Merge origin/main into feat/564-web-surface
ScriptedAlchemy Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/558-serve-app-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"agent-bundle": patch
---

Add `agent-bundle/serve-app-command`, a dependency-free entry a routed CLI command (or any other generated executable) imports to serve a built MCP App without importing the compiler: `spawnServeApp(options)` lowers the `serveApp` options to `agent-bundle serve-app` argv (`serveAppArgv`), resolves the framework CLI installed at or above the project root (`locateFrameworkCli`), spawns it with its stdout relayed to stderr so the route keeps stdout for its JSON result, resolves with `{ url, port, tool, server, pid, closed, close() }` once the CLI prints its ready line (`parseServeAppReadyLine`), tears the server down when the route's `signal` aborts, and rejects with `ServeAppCommandError` (`framework-not-installed`, `artifact-missing`, `spawn-failed`, `exited-before-ready`, `aborted`, `stop-failed`). Report the new `AB4837` diagnostic from `inspect`, `validate`, `build`, and `dev` when a route module, layout, or provider — or a module it reaches through relative imports — value-imports a compiler-carrying framework entry (`agent-bundle`, `agent-bundle/api`, `agent-bundle/config`, `agent-bundle/eval`, `agent-bundle/rstest`, `agent-bundle/test`, `agent-bundle/test/browser`), naming the file, the specifier, and the helper, instead of failing inside the bundler with `Can't resolve '../events'`; `import type` and type-only usage are not reported (#582)
Report the new `AB4837` diagnostic from `inspect`, `validate`, `build`, and `dev` when a route module, layout, or provider — or a module it reaches through relative imports — value-imports a compiler-carrying framework entry (`agent-bundle`, `agent-bundle/api`, `agent-bundle/config`, `agent-bundle/eval`, `agent-bundle/rstest`, `agent-bundle/test`, `agent-bundle/test/browser`), naming the file, the specifier, and the helper, instead of failing inside the bundler with `Can't resolve '../events'`; `import type` and type-only usage are not reported (#582)
5 changes: 5 additions & 0 deletions .changeset/564-web-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Add the `web` config key to expose declared MCP Apps in a browser: `web.apps` selects `<server>/<app>` entries already under `mcp.servers.<id>.apps`, `web.open` is `browser` or `never` (default `never`), and invalid exposure reports `AB4341`. When `web` is configured, the composite artifact's `agent-bundle.manifest.json` gains a `web` section (each App carries its server's `entry`, `args`, and `env`; Apps scoped to unselected targets are omitted) and `bin/<plugin>.mjs` carries the framework-owned `<plugin> web` command even without `src/cli/**`; `agent-bundle dev` serves the same host at `/web/<server>/<app>`. Host adapters publish a `web` capability row that gates the web-only bin like `cli` gates a routed CLI; a target without it is an `AB4341` warning, and an authored command or alias spelled `web` is `AB4341`. Remove `agent-bundle/serve-app-command` (`spawnServeApp`, `serveAppArgv`, `locateFrameworkCli`, `ServeAppCommandError`); the supported path from an installed artifact is `<plugin> web`. `web` never displaces an authored executable: a hand-written `src/cli.ts`, a `bin` entry claiming the plugin name, or `bin: false` keeps its bin and `AB4341` reports the web surface with nowhere to live. Fix `agent-bundle dev` rebuilding the epoch it just produced whenever the project has a `dist/` package build: the watcher now ignores the build's `.dist.stage-*` staging directory. (#620)
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
packages/agent-bundle/web-host-dist/
.agent-bundle/
.worktrees/
coverage/
Expand Down
15 changes: 13 additions & 2 deletions docs/diagnostics.md

Large diffs are not rendered by default.

78 changes: 48 additions & 30 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,10 @@ skills, and script routes ship with the **plugin root**, so the build also
emits the same compiled command graph into that root whenever a selected host's
adapter publishes the `cli` capability — all built-in hosts do (`claude`,
`codex`, `cursor`, `portable`) — because the artifact root is
already a plain directory Node executes `mcp/` and `scripts/` files from:
already a plain directory Node executes `mcp/` and `scripts/` files from.
The same `bin/<plugin-name>.mjs` is also emitted when `web` is configured,
even if `src/cli/**` compiled no commands (the bin then carries only the
framework-owned `web` command, or both surfaces when routed commands exist):

```text
artifact/
Expand Down Expand Up @@ -1502,33 +1505,48 @@ resolve '../events'`). The route graph reports such an import first, as
(`src/routes/framework-imports.ts`; the compiler-carrying entries are
`agent-bundle`, `agent-bundle/api`, `agent-bundle/config`,
`agent-bundle/eval`, `agent-bundle/rstest`, `agent-bundle/test`, and
`agent-bundle/test/browser`, matched exactly, so the bundle-safe entries —
`agent-bundle/app` among them — are never reported; `import type` and
type-only usage are not reported either), while an external bare import
(`AB6005 uses unsupported specifier`) or a non-literal `import(spec)` (`AB6005 has a
non-literal dynamic import`) still fails artifact validation. The sanctioned
shape is `spawnServeApp` from `agent-bundle/serve-app-command`
(`src/serve-app-command.ts`, #558) — plain Node with no dependencies, so the
bundler inlines it into the self-contained executable the way it inlines
`agent-bundle/launch-env`. It lowers the `serveApp` options to `serve-app`
argv (`serveAppArgv`; every `ServeAppOptions` key except the host-process-only
`logger`, `registry`, `openBrowser`, `targets`, and `timeoutMs`), resolves the
framework CLI from the `agent-bundle` package installed at or above `root`
(`locateFrameworkCli`, through `src/core/dependency-manifest.ts`), spawns it
with the child's stdout piped and its stderr inherited, relays every stdout
line to stderr so the routed CLI keeps stdout for its result, resolves once
the child prints the ready line `MCP App <app> at <url> (tool <tool>; Ctrl-C
stops the server)` — the CLI writes it and the helper parses it through one
module, `src/serve-app/command-contract.ts` — and turns the route `signal`
into the child's `SIGTERM`. The result is `{ app, url, tool, server, port,
pid, closed, close() }`; failures are `ServeAppCommandError` with `code`
`framework-not-installed`, `artifact-missing`, `spawn-failed`,
`exited-before-ready` (carrying the child's `exit`), `aborted`, or
`stop-failed` (the running child refused the signal `close()` or the abort
sent; it is still running). It is a
checkout command: an installed host pack has neither `node_modules/agent-bundle`
nor the artifact, and the first two codes say so before anything is spawned.
The worked example is in the MCP Apps guide, "Serving an App standalone".
`agent-bundle/test/browser`, matched exactly; `import type` and type-only
usage are not reported), while an external bare import (`AB6005 uses
unsupported specifier`) or a non-literal `import(spec)` (`AB6005 has a
non-literal dynamic import`) still fails artifact validation. From an
installed artifact the supported command is `<plugin> web` on
`bin/<plugin>.mjs` (emitted when `web` is configured, even with no
`src/cli/**` commands). It reads the manifest `web` section beside `bin/`,
launches the plugin's own packed MCP server, and prints the same ready line
`MCP App <server>/<app> at <url> (tool <tool>; Ctrl-C stops the server)` —
`src/serve-app/command-contract.ts` is the shared contract for that line
and for `--json` `{ app, server, tool, url, port, resourceUri, sandboxOrigin }`.
`agent-bundle/serve-app-command` (`spawnServeApp`) is removed. The worked
example is in the MCP Apps guide, "Exposing an App in the browser".

## `<plugin> web`

```sh
node <root>/bin/<plugin>.mjs web [<server>/<app>] [--port N]
[--open|--no-open] [--tool T] [--input JSON] [--allow <cap>]...
[--profile portable|claude|chatgpt] [--json]
```

The framework-owned `web` command. Config is exposure/policy only:

```ts
web?: {
apps: ReadonlyArray<string | {
app: string;
tool?: string;
input?: Record<string, unknown>;
allow?: McpAppConsentCapability[];
}>;
open?: 'browser' | 'never'; // default 'never'
}
```

`apps[]` selects among Apps already declared under `mcp.servers.<id>.apps`.
Invalid `web` is `AB4341`. When configured, `agent-bundle.manifest.json`
gains a `web` section and the bin exists even without authored CLI commands.
`agent-bundle dev` serves the same host at `GET /web/<server>/<app>` (404
for Apps not listed in `web.apps`). There is no `web/` directory in the
artifact.

## `agent-bundle/app` — the App-side bridge client

Expand All @@ -1549,10 +1567,10 @@ mapping cannot replace the framework runtime. It is not one of the
compiler-carrying entries `AB4837` rejects.

The other half stays where it is: the host page, sandbox proxy, frame relay
(`McpAppFrameRelay` in the Workbench, the inline relay in `serve-app`),
(`McpAppFrameRelay` in `src/web-host/browser/frame-relay.ts`, shared by the Workbench, `serve-app`, dev `/web`, and `<plugin> web`),
`/api/mcp/...` routes, consent authority, and `createMcpAppBridge`
(`src/dev/mcp-apps/mcp-app-bridge.ts`) are host-side and owned by the
Workbench, `serve-app`, and #564's production host; #594 adds no host bridge
Workbench, `serve-app`, and the `<plugin> web` host (#564); #594 adds no host bridge
and moves none of those modules. The one host-side behavior it adds is
cancellation: `createMcpAppBridge` now honors the client's
`notifications/cancelled` and threads the abort through the binding service
Expand Down
9 changes: 5 additions & 4 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,11 @@ own packed server, launched as `mcp run` launches it. `serveApp` in
the Workbench, tests, a plugin's own scripts — never the MCP shell, and a
local preview host, not a deployment target. A plugin's own "open the
dashboard" CLI route cannot import it — the routed CLI bin is self-contained,
and the route graph reports the value import as `AB4837` — so it calls
`spawnServeApp` from `agent-bundle/serve-app-command`, which spawns
`agent-bundle serve-app` as a child process (#558); see
[Entry conventions](entry-conventions.md#agent-bundle-serve-app).
and the route graph reports the value import as `AB4837`. From an installed
artifact the supported command is `<plugin> web` on `bin/<plugin>.mjs`,
emitted when the `web` config key lists declared Apps; `agent-bundle dev`
serves the same host at `/web/<server>/<app>`. See
[Entry conventions](entry-conventions.md#plugin-web).

The compiler statically reads `config`, imports schemas and implementations
only into generated entries, installs `runAgentRequest`, and derives the real
Expand Down
1 change: 1 addition & 0 deletions examples/mcp-app/agent-bundle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export default defineConfig({
'check-service-fixture': './src/scripts/check-service-fixture.ts',
},
targets: ['portable', 'codex', 'claude'],
web: { apps: [{ app: 'status/status', tool: 'show-status', allow: ['call-tool'] }] },
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"lint": "rslint .",
"bench:hook-cold-start": "node scripts/measure-hook-cold-start.mjs",
"bench:preflight-cold-start": "node scripts/measure-preflight-cold-start.mjs",
"typecheck": "node scripts/check-dist-fresh.mjs && tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json && tsc --project packages/rsc-markdown-stream/tsconfig.json && pnpm --filter @agent-bundle/docs typecheck",
"typecheck": "node scripts/check-dist-fresh.mjs && tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json && tsc --project packages/rsc-markdown-stream/tsconfig.json && tsc --project packages/agent-bundle/tsconfig.web-host.json && pnpm --filter @agent-bundle/docs typecheck",
"check": "pnpm build && pnpm test:unit && pnpm test:route-unit && pnpm test:projection && pnpm test:integration:run && pnpm lint && pnpm typecheck",
"check:local-ci": "node scripts/local-ci.mjs",
"check:host-cli": "node scripts/host-cli-pins.mjs verify",
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

25 changes: 25 additions & 0 deletions packages/agent-bundle/fixtures/web-surface/agent-bundle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Plain object export keeps this packed fixture independent of the package build (#564).
export default {
mcp: {
servers: {
status: {
apps: {
status: {
entry: './views/status.ts',
resourceUri: 'ui://web-surface-fixture/status.html',
targets: ['portable'],
template: './views/status.html',
},
},
},
},
},
plugin: {
description: 'A plugin whose MCP App is exposed through web.apps and opened with <plugin> web.',
name: 'web-surface-fixture',
version: '1.0.0',
},
targets: ['portable'],
// Omitting `tool` proves unique live-server resolution.
web: { apps: [{ allow: ['call-tool'], app: 'status/status' }] },
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "serve-app-command-fixture",
"name": "web-surface-fixture",
"private": true,
"type": "module",
"devDependencies": {
Expand Down
14 changes: 14 additions & 0 deletions packages/agent-bundle/fixtures/web-surface/src/cli/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { CliRouteConfig } from 'agent-bundle';
import { z } from 'zod';

export const config = {
description: 'Report that the dashboard command is wired into the plugin bin.',
} satisfies CliRouteConfig;

export const inputSchema = z.object({}).strict();

export const resultSchema = z.object({ ok: z.literal(true) }).strict();

export default async function dashboard() {
return { ok: true as const };
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import { name, version } from 'agent-bundle/meta';
const app = apps[0];
if (app === undefined) throw new Error('Expected the status MCP App.');

/**
* Default-exported server factory: `agent-bundle build` wraps it in the
* framework stdio lifecycle shell. One App resource and one tool that opens
* it — the pair `agent-bundle serve-app status/status --tool status` binds.
* The tool takes no input, so the opening call `serve-app` makes needs none.
*/
export default function createStatusServer(): McpServer {
const server = new McpServer({ name, version });

Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/fixtures/web-surface/views/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
document.querySelector('#view')!.textContent = 'web-surface fixture status';
11 changes: 6 additions & 5 deletions packages/agent-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"node": ">=22.19.0"
},
"scripts": {
"build": "node ../../scripts/sync-license-files.mjs && pnpm build:workbench && rslib build",
"build": "node ../../scripts/sync-license-files.mjs && pnpm build:workbench && pnpm build:web-host && rslib build",
"build:web-host": "rsbuild build -c rsbuild.web-host.config.ts",
"build:workbench": "pnpm --filter agent-bundle-workbench build"
},
"files": [
Expand Down Expand Up @@ -87,10 +88,6 @@
"types": "./dist/rstest/index.d.ts",
"import": "./dist/rstest.js"
},
"./serve-app-command": {
"types": "./dist/serve-app-command.d.ts",
"import": "./dist/serve-app-command.js"
},
"./test": {
"types": "./dist/test/index.d.ts",
"import": "./dist/test.js"
Expand All @@ -99,6 +96,10 @@
"types": "./dist/test/browser.d.ts",
"import": "./dist/test/browser.js"
},
"./web-host": {
"types": "./dist/web-host.d.ts",
"import": "./dist/web-host.js"
},
"./package.json": "./package.json"
},
"dependencies": {
Expand Down
34 changes: 34 additions & 0 deletions packages/agent-bundle/rsbuild.web-host.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { resolve } from 'node:path';

import { defineConfig } from '@rsbuild/core';

export default defineConfig({
mode: 'production',
output: {
distPath: {
js: './',
jsAsync: './',
root: 'web-host-dist',
},
filename: {
js: 'page.js',
},
filenameHash: false,
target: 'web',
},
performance: {
chunkSplit: {
strategy: 'all-in-one',
},
},
root: import.meta.dirname,
source: {
entry: {
page: resolve(import.meta.dirname, 'src/web-host/browser/main.ts'),
},
tsconfigPath: './tsconfig.web-host.json',
},
tools: {
htmlPlugin: false,
},
});
6 changes: 2 additions & 4 deletions packages/agent-bundle/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default defineConfig({
cleanDistPath: true,
copy: [
{ from: resolve(import.meta.dirname, '../workbench/dist'), to: 'workbench', info: { minimized: true } },
{ from: resolve(import.meta.dirname, 'web-host-dist'), to: 'web-host', info: { minimized: true } },
],
filenameHash: false,
legalComments: 'linked',
Expand Down Expand Up @@ -175,13 +176,10 @@ export default defineConfig({
// into its generated bundle.
routes: './src/routes/public.ts',
rstest: './src/rstest/index.ts',
// Plain Node (#558): a routed command serves an MCP App by spawning
// `agent-bundle serve-app` through this entry instead of importing the
// compiler, so it must bundle into a self-contained executable.
'serve-app-command': './src/serve-app-command.ts',
'terminal-capability': './src/terminal-capability.ts',
test: './src/test/index.ts',
'test/browser': './src/test/browser.ts',
'web-host': './src/web-host.ts',
},
},
});
Loading
Loading