From 06317fbf3d58cc0a8143e80827cf9614e12d3a18 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 22:01:27 +0000 Subject: [PATCH 1/2] docs(mcp): serveApp is a host-process API; a routed CLI command spawns agent-bundle serve-app (#558) The #537 docs showed a plugin route doing await import('agent-bundle/api'). That does not build: routed CLI bins are self-contained, so the bundler inlines the compiler and fails on its runtime-relative module references, and the external or non-literal escapes are AB6005. State the real audience of serveApp (CLI, Workbench, tests, a plugin's own scripts), name the constraint, and show the child-process spawn pattern cargo-hauler shipped, with #558 linked. --- .changeset/514-serve-app.md | 2 +- docs/entry-conventions.md | 22 +++- docs/framework-mode.md | 10 +- packages/agent-bundle/src/api.ts | 9 +- website/docs/en/guide/authoring/mcp.mdx | 132 ++++++++++++++++++------ website/docs/en/reference/api.mdx | 2 +- website/docs/en/reference/cli.mdx | 4 +- website/docs/zh/guide/authoring/mcp.mdx | 122 ++++++++++++++++------ website/docs/zh/reference/api.mdx | 2 +- website/docs/zh/reference/cli.mdx | 3 +- 10 files changed, 228 insertions(+), 80 deletions(-) diff --git a/.changeset/514-serve-app.md b/.changeset/514-serve-app.md index 826416ae3..a97e36d8e 100644 --- a/.changeset/514-serve-app.md +++ b/.changeset/514-serve-app.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Add `agent-bundle serve-app /` and `serveApp` in `agent-bundle/api`: serve one built MCP App standalone in a browser, bound to the plugin's own packed MCP server. The server launches exactly as `mcp run` does (same artifact resolution, `.env` layering, and plugin-data root), the App is hosted through the Workbench's MCP App host stack (sandbox proxy, consent authority, bridge) on `127.0.0.1` behind a per-launch token (`AB8003` / `AB8004` on refusal), and the App's tool is called once so it opens populated. `--tool`, `--input`, `--port`, `--profile`, `--allow `, `--open`, and the `mcp run` environment flags select the binding; `serveApp` returns `{ url, close, closed }` so a plugin's own CLI route can offer an "open the dashboard" command. Fixes #514. (#537) +Add `agent-bundle serve-app /` and `serveApp` in `agent-bundle/api`: serve one built MCP App standalone in a browser, bound to the plugin's own packed MCP server. The server launches exactly as `mcp run` does (same artifact resolution, `.env` layering, and plugin-data root), the App is hosted through the Workbench's MCP App host stack (sandbox proxy, consent authority, bridge) on `127.0.0.1` behind a per-launch token (`AB8003` / `AB8004` on refusal), and the App's tool is called once so it opens populated. `--tool`, `--input`, `--port`, `--profile`, `--allow `, `--open`, and the `mcp run` environment flags select the binding; `serveApp` returns `{ url, close, closed }` for scripts and tests — a plugin's own "open the dashboard" CLI route spawns `agent-bundle serve-app` instead, since the self-contained routed CLI bin cannot import `agent-bundle/api` (`AB6005`; #558). Fixes #514. (#537) diff --git a/docs/entry-conventions.md b/docs/entry-conventions.md index 515299c12..897133701 100644 --- a/docs/entry-conventions.md +++ b/docs/entry-conventions.md @@ -1414,7 +1414,21 @@ and the bridge exposes only the selected server. This is a local preview host, not a deployment target. `serveApp` in `agent-bundle/api` is the programmatic form (`{ url, close, -closed }`) for a plugin's own routed CLI (`hauler dashboard`). It belongs to -the plugin's dev-time / CLI process — import it lazily from the route that -needs it — never to the MCP server shell, so emitted artifacts stay free of -the host runtime. +closed }`). It is a host-process API: it belongs to processes the framework +does not compile — the first-party CLI, the Workbench, tests, a plugin's own +`package.json` scripts or a hand-written `.mjs` run from the checkout — and +never to the MCP server shell. A routed CLI command inside the artifact +cannot import it today: routed CLI bins are self-contained (#387), so the +bundler inlines `agent-bundle/dist/api.js` into the bin and fails on the +framework's runtime-relative module references (`Module not found: Can't +resolve '../events'`), while an external bare import (`AB6005 uses +unsupported specifier`) or a non-literal `import(spec)` (`AB6005 has a +non-literal dynamic import`) fails artifact validation. The pattern that +builds is a plain routed command that spawns `agent-bundle serve-app` as a +child process — resolving the framework CLI from `node_modules/agent-bundle` +by path, relaying the child's `MCP App at ` line to stderr so the +routed CLI keeps stdout for its result, and turning the route `signal` into +the child's `SIGTERM` — which makes it a checkout-only command (an installed +host pack has neither `node_modules` nor the artifact). A framework helper +for that plumbing is tracked in #558; the worked example is in the MCP Apps +guide, "Serving an App standalone". diff --git a/docs/framework-mode.md b/docs/framework-mode.md index 05d79d380..eb0eee573 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -97,10 +97,12 @@ A built App is previewed in the Workbench MCP page, or served standalone in a plain browser tab with `agent-bundle serve-app /` — the same host stack (sandbox proxy, consent authority, bridge) bound to the plugin's own packed server, launched as `mcp run` launches it. `serveApp` in -`agent-bundle/api` is the programmatic form for a plugin's own "open the -dashboard" CLI route; it runs in the plugin's dev-time / CLI process, never -in the MCP shell, and is a local preview host, not a deployment target. See -[Entry conventions](entry-conventions.md#agent-bundle-serve-app). +`agent-bundle/api` is the programmatic form for host processes — the CLI, +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; +`AB6005`) and spawns `agent-bundle serve-app` instead; see +[Entry conventions](entry-conventions.md#agent-bundle-serve-app) and #558. The compiler statically reads `config`, imports schemas and implementations only into generated entries, installs `runAgentRequest`, and derives the real diff --git a/packages/agent-bundle/src/api.ts b/packages/agent-bundle/src/api.ts index c42cc39d6..6d2f77440 100644 --- a/packages/agent-bundle/src/api.ts +++ b/packages/agent-bundle/src/api.ts @@ -1494,9 +1494,12 @@ const scopedThrowawayArtifact = ( * `close()` to tear down the host and the server; `closed` settles when the * server connection ends for any reason. * - * This runs in a dev-time or CLI process — a plugin's own routed CLI can - * call it from a `hauler dashboard`-style route — never inside the MCP - * server shell. + * This is a host-process API: it runs in processes the framework does not + * compile — the first-party CLI, the Workbench, tests, a plugin's own + * scripts — never inside the MCP server shell, and not from a routed CLI + * command inside the artifact, whose self-contained bin cannot import + * `agent-bundle/api` (`AB6005`); such a route spawns `agent-bundle + * serve-app` instead (issue #558). */ export const serveApp = async (options: ServeAppOptions): Promise => { const registry = registryFor(options); diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index 2252cf53e..223d8907a 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -649,55 +649,119 @@ exposes only that server through the bridge; the App document runs on a second l the framework's sandbox. It is a local preview host, not a deployment target. Every option is in the [CLI reference](../../reference/cli.mdx#serve-app). -The same host is available programmatically as `serveApp` in `agent-bundle/api`, so a plugin's -own routed CLI can offer the command. It returns `{ url, close, closed }`: `close()` tears down the -host and the server, `closed` settles when the server connection ends for any reason. +The same host is available programmatically as `serveApp` in `agent-bundle/api`. It returns +`{ url, close, closed }`: `close()` tears down the host and the server, `closed` settles when the +server connection ends for any reason — always `close()` when you are done, because `closed` +settling means the server connection ended, not that the host was torn down. `autoApprove` grants +the listed consent capabilities on the operator's behalf as the App requests them (`call-tool` +lets a polling dashboard refresh without a prompt); anything else waits for an Allow/Deny decision +in the host page, as in the Workbench. + +`serveApp` is a **host-process API**. It belongs to processes the framework does not compile — +the first-party CLI, the Workbench, tests, a plugin's own `package.json` scripts or a hand-written +`.mjs` run from the checkout — and it needs `agent-bundle` resolvable where that process runs. +Never call it from the MCP server shell. + +A routed CLI command inside the plugin artifact cannot import it today. Routed CLI bins are +self-contained (`bin/.mjs` in every host pack, `dist/bin/.js` in the package +build), so a route with `await import('agent-bundle/api')` makes the bundler inline the whole +compiler into the bin, where it fails on the framework's runtime-relative module references +(`Module not found: Can't resolve '../events'`); leaving the import external is +`AB6005 … uses unsupported specifier "agent-bundle/api"`, and a non-literal `import(spec)` is +`AB6005 … has a non-literal dynamic import`. A helper a routed command can use is tracked in +[#558](https://github.com/ScriptedAlchemy/agent-bundle/issues/558). + +The pattern that builds is a plain routed command that spawns `agent-bundle serve-app` as a child +process, as cargo-hauler's `hauler dashboard` does. It is a **checkout command**: it needs +`agent-bundle` under `node_modules` and the built `artifact/` beside the CLI, neither of which an +installed host pack has, so it says so instead of failing inside the child. ```ts // src/cli/dashboard.ts — `hauler dashboard`: open the App against the running daemon. +import { spawn } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; import { z } from 'zod'; export const config = { - description: 'Open the cargo-hauler dashboard in a browser.', + description: 'Open the cargo-hauler dashboard in a browser (from the plugin checkout).', + exitCode: 'result', } satisfies CliRouteConfig; -export const inputSchema = z.object({ open: z.boolean().default(true) }).strict(); - -export const resultSchema = z.object({ url: z.string() }).strict(); +export const inputSchema = z.object({ noOpen: z.boolean().optional() }).strict(); + +export const resultSchema = z.object({ + exitCode: z.number().int(), + message: z.string(), + url: z.string().nullable(), +}).strict(); + +// The framework CLI, read from the `bin` of the nearest `node_modules/agent-bundle` at or +// above the plugin root. Located by path, never imported: an `import()` of the package +// would pull the framework into the bin. +const frameworkCli = (root: string): string | undefined => { + for (let directory = root; ; directory = dirname(directory)) { + const manifestPath = join(directory, 'node_modules', 'agent-bundle', 'package.json'); + if (existsSync(manifestPath)) { + const { bin } = JSON.parse(readFileSync(manifestPath, 'utf8')) as { + bin?: string | Record; + }; + const relative = typeof bin === 'string' ? bin : bin?.['agent-bundle']; + if (relative === undefined) return undefined; + return resolve(dirname(manifestPath), relative); + } + if (directory === dirname(directory)) return undefined; + } +}; export default async function dashboard({ input, signal }: CliRouteProps) { - const { serveApp } = await import('agent-bundle/api'); - const served = await serveApp({ - app: 'hauler/dashboard', - artifact: new URL('../../artifact', import.meta.url).pathname, - autoApprove: ['call-tool'], - open: input.open, - root: process.cwd(), - target: 'cursor', - tool: 'hauler_status', - }); - signal.addEventListener('abort', () => { void served.close(); }, { once: true }); - try { - // Settles on Ctrl-C (through the signal) or when the daemon exits on its own. - await served.closed; - } finally { - // `closed` tracks only the server connection; close() also releases the - // host, the sandbox proxy, and any throwaway artifact. - await served.close(); + // `dist/bin/.js` sits two levels under the checkout, which holds `artifact/`. + const root = fileURLToPath(new URL('../../', import.meta.url)); + const cli = frameworkCli(root); + const artifact = join(root, 'artifact'); + if (cli === undefined || !existsSync(join(artifact, 'agent-bundle.manifest.json'))) { + return { + exitCode: 1, + message: 'hauler dashboard runs from the plugin checkout (pnpm install, then ' + + 'agent-bundle build); in an MCP host, call hauler_status instead.', + url: null, + }; } - return { url: served.url }; + return new Promise>((done, fail) => { + const child = spawn(process.execPath, [ + cli, 'serve-app', 'hauler/dashboard', '--root', root, + '--artifact', artifact, '--target', 'portable', + '--tool', 'hauler_status', '--allow', 'call-tool', + input.noOpen === true ? '--no-open' : '--open', + ], { stdio: ['ignore', 'pipe', 'inherit'] }); + let url: string | null = null; + child.stdout.on('data', (chunk: Buffer) => { + // The child prints `MCP App at (…)`; relay it to stderr so the routed + // CLI keeps stdout for its JSON result. + const text = chunk.toString('utf8'); + process.stderr.write(text); + url ??= /\bat (https?:\/\/\S+)/u.exec(text)?.[1] ?? null; + }); + // Ctrl-C reaching the routed CLI becomes the child's SIGTERM. + signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true }); + child.once('error', fail); + child.once('exit', (code) => done({ + exitCode: code ?? 1, + message: code === 0 + ? 'dashboard closed' + : `agent-bundle serve-app exited with ${String(code)}`, + url, + })); + }); } ``` -`serveApp` runs in the plugin's dev-time or CLI process, never inside the MCP server shell: import -it lazily from the route that needs it, as above, so the emitted artifact stays free of the host -runtime, and expect `agent-bundle` to be resolvable where that CLI runs. Always `close()` when -you are done — `closed` settling means the server connection ended, not that the host was torn -down. `autoApprove` grants the -listed consent capabilities on the operator's behalf as the App requests them (`call-tool` lets a -polling dashboard refresh without a prompt); anything else waits for an Allow/Deny decision in the -host page, as in the Workbench. +Every `serve-app` option — `--port`, `--input`, `--profile`, `--env-file`, `--plugin-root` — +passes through as argv, and the host packs stay self-contained because the framework is spawned, +never bundled. ## Server modes diff --git a/website/docs/en/reference/api.mdx b/website/docs/en/reference/api.mdx index 53e224efc..95b3f1fb4 100644 --- a/website/docs/en/reference/api.mdx +++ b/website/docs/en/reference/api.mdx @@ -14,7 +14,7 @@ Every public entry point is documented from its declarations: | Entry point | Contents | | --- | --- | | `agent-bundle` | The authoring and orchestration surface: `defineSkill`, `canonicalAgentEvents`, `startDevServer`, `runEvals`, `compareEvals`, the eval harness factories, and the artifact-manifest helpers. | -| `agent-bundle/api` | The programmatic compiler: `build`, `validate`, `inspect`, `prepack`, their option and result types, the `AgentComponentKind` / `componentKindCapability` component-kind helpers, and the artifact operations `listMcp`, `invokeMcp`, `runMcp`, `serveApp` (a built MCP App served standalone in a browser), `listHooks`, and `simulateHook`. | +| `agent-bundle/api` | The programmatic compiler: `build`, `validate`, `inspect`, `prepack`, their option and result types, the `AgentComponentKind` / `componentKindCapability` component-kind helpers, and the artifact operations `listMcp`, `invokeMcp`, `runMcp`, `serveApp` (a built MCP App served standalone in a browser — a host-process API for scripts and tests; a routed CLI command inside the artifact cannot import it, see [Serving an App standalone](../guide/authoring/mcp.mdx#serving-an-app-standalone)), `listHooks`, and `simulateHook`. | | `agent-bundle/config` | `defineConfig` and the configuration types. | | `agent-bundle/test` | The route-testing harness, matchers, and contract matrices. | | `agent-bundle/test/browser` | The MCP App bridge harness for browser-rendered views. | diff --git a/website/docs/en/reference/cli.mdx b/website/docs/en/reference/cli.mdx index 3428ffb00..2a6015505 100644 --- a/website/docs/en/reference/cli.mdx +++ b/website/docs/en/reference/cli.mdx @@ -99,7 +99,9 @@ The host binds to `127.0.0.1` only, serves one document and the authenticated `/ routes (a per-launch token plus same-origin checks; `AB8003` / `AB8004` on refusal), and exposes only the selected server through the bridge. The App document itself runs on a second loopback origin inside the framework's sandbox. It is a local preview host, not a deployment target. The -programmatic form is `serveApp` in `agent-bundle/api`; see +programmatic form for scripts and tests is `serveApp` in `agent-bundle/api`; a plugin's own routed +CLI command spawns this command instead, because the self-contained bin cannot import +`agent-bundle/api` — see [Serving an App standalone](../guide/authoring/mcp.mdx#serving-an-app-standalone). ## build and prepack diff --git a/website/docs/zh/guide/authoring/mcp.mdx b/website/docs/zh/guide/authoring/mcp.mdx index e8001edd9..5cdcebccd 100644 --- a/website/docs/zh/guide/authoring/mcp.mdx +++ b/website/docs/zh/guide/authoring/mcp.mdx @@ -579,51 +579,113 @@ npx agent-bundle serve-app status/status --artifact artifact --input '{"service" 桥接暴露那一个服务器;App 文档运行在框架沙箱内的第二个 loopback origin 上。它是本地预览宿主,不是部署 目标。全部选项见[命令行参考](../../reference/cli.mdx#serve-app)。 -同一套宿主以 `agent-bundle/api` 中的 `serveApp` 提供编程形式,因此插件自己的路由式 CLI 可以提供这条 -命令。它返回 `{ url, close, closed }`:`close()` 拆除宿主与服务器,`closed` 在服务器连接因任何原因 -结束时落定。 +同一套宿主以 `agent-bundle/api` 中的 `serveApp` 提供编程形式。它返回 `{ url, close, closed }`: +`close()` 拆除宿主与服务器,`closed` 在服务器连接因任何原因结束时落定——用完后务必 `close()`,因为 +`closed` 落定只表示服务器连接结束,并不表示宿主已被拆除。`autoApprove` 会在 App 请求时代操作者批准 +列出的同意能力(`call-tool` 让轮询型仪表盘无需弹窗即可刷新);其余能力与 Workbench 一样,在宿主页面里 +等待 Allow/Deny 决定。 + +`serveApp` 是一个**宿主进程 API**。它属于框架不编译的那些进程——第一方 CLI、Workbench、测试、插件自己的 +`package.json` 脚本或从 checkout 里运行的手写 `.mjs`——并且要求 `agent-bundle` 在该进程运行之处可被 +解析。绝不要在 MCP 服务器外壳里调用它。 + +插件产物内部的路由式 CLI 命令今天无法导入它。路由式 CLI 的 bin 是自包含的(每个宿主包里的 +`bin/.mjs`,以及 package build 里的 `dist/bin/.js`),因此一条写着 +`await import('agent-bundle/api')` 的路由会让打包器把整个编译器内联进 bin,并在框架的运行时相对模块引用上 +失败(`Module not found: Can't resolve '../events'`);把该导入留作外部依赖会得到 +`AB6005 … uses unsupported specifier "agent-bundle/api"`,而非字面量的 `import(spec)` 则是 +`AB6005 … has a non-literal dynamic import`。可供路由式命令使用的辅助函数在 +[#558](https://github.com/ScriptedAlchemy/agent-bundle/issues/558) 中跟踪。 + +能够构建的模式,是一条普通的路由式命令把 `agent-bundle serve-app` 作为子进程启动——cargo-hauler 的 +`hauler dashboard` 正是这么做的。它是一条 **checkout 命令**:需要 `node_modules` 下的 `agent-bundle` +和 CLI 旁边已构建的 `artifact/`,而已安装的宿主包两者都没有,所以它直接说明这一点,而不是在子进程里 +失败。 ```ts // src/cli/dashboard.ts —— `hauler dashboard`:针对正在运行的守护进程打开 App。 +import { spawn } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; import { z } from 'zod'; export const config = { - description: 'Open the cargo-hauler dashboard in a browser.', + description: 'Open the cargo-hauler dashboard in a browser (from the plugin checkout).', + exitCode: 'result', } satisfies CliRouteConfig; -export const inputSchema = z.object({ open: z.boolean().default(true) }).strict(); - -export const resultSchema = z.object({ url: z.string() }).strict(); +export const inputSchema = z.object({ noOpen: z.boolean().optional() }).strict(); + +export const resultSchema = z.object({ + exitCode: z.number().int(), + message: z.string(), + url: z.string().nullable(), +}).strict(); + +// 框架 CLI:从插件根目录向上找到最近的 `node_modules/agent-bundle`,读取其 `bin`。 +// 按路径定位、绝不导入:对该包做 `import()` 会把框架拖进 bin。 +const frameworkCli = (root: string): string | undefined => { + for (let directory = root; ; directory = dirname(directory)) { + const manifestPath = join(directory, 'node_modules', 'agent-bundle', 'package.json'); + if (existsSync(manifestPath)) { + const { bin } = JSON.parse(readFileSync(manifestPath, 'utf8')) as { + bin?: string | Record; + }; + const relative = typeof bin === 'string' ? bin : bin?.['agent-bundle']; + if (relative === undefined) return undefined; + return resolve(dirname(manifestPath), relative); + } + if (directory === dirname(directory)) return undefined; + } +}; export default async function dashboard({ input, signal }: CliRouteProps) { - const { serveApp } = await import('agent-bundle/api'); - const served = await serveApp({ - app: 'hauler/dashboard', - artifact: new URL('../../artifact', import.meta.url).pathname, - autoApprove: ['call-tool'], - open: input.open, - root: process.cwd(), - target: 'cursor', - tool: 'hauler_status', - }); - signal.addEventListener('abort', () => { void served.close(); }, { once: true }); - try { - // 在 Ctrl-C(经由 signal)或守护进程自行退出时落定。 - await served.closed; - } finally { - // `closed` 只跟踪服务器连接;close() 还会释放宿主、沙箱代理与任何一次性产物。 - await served.close(); + // `dist/bin/.js` 位于 checkout 下两层;checkout 的 `artifact/` 已构建。 + const root = fileURLToPath(new URL('../../', import.meta.url)); + const cli = frameworkCli(root); + const artifact = join(root, 'artifact'); + if (cli === undefined || !existsSync(join(artifact, 'agent-bundle.manifest.json'))) { + return { + exitCode: 1, + message: 'hauler dashboard runs from the plugin checkout (pnpm install, then ' + + 'agent-bundle build); in an MCP host, call hauler_status instead.', + url: null, + }; } - return { url: served.url }; + return new Promise>((done, fail) => { + const child = spawn(process.execPath, [ + cli, 'serve-app', 'hauler/dashboard', '--root', root, + '--artifact', artifact, '--target', 'portable', + '--tool', 'hauler_status', '--allow', 'call-tool', + input.noOpen === true ? '--no-open' : '--open', + ], { stdio: ['ignore', 'pipe', 'inherit'] }); + let url: string | null = null; + child.stdout.on('data', (chunk: Buffer) => { + // 子进程打印 `MCP App at (…)`;转发到 stderr, + // 让路由式 CLI 的 stdout 留给 JSON 结果。 + const text = chunk.toString('utf8'); + process.stderr.write(text); + url ??= /\bat (https?:\/\/\S+)/u.exec(text)?.[1] ?? null; + }); + // 抵达路由式 CLI 的 Ctrl-C 变成子进程的 SIGTERM。 + signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true }); + child.once('error', fail); + child.once('exit', (code) => done({ + exitCode: code ?? 1, + message: code === 0 + ? 'dashboard closed' + : `agent-bundle serve-app exited with ${String(code)}`, + url, + })); + }); } ``` -`serveApp` 运行在插件的开发期或 CLI 进程里,绝不在 MCP 服务器外壳之内:像上面那样,在需要它的路由中 -惰性导入,这样输出的产物就不携带宿主运行时;同时要保证 `agent-bundle` 在该 CLI 运行之处可被解析。 -用完后务必 `close()`——`closed` 落定只表示服务器连接结束,并不表示宿主已被拆除。 -`autoApprove` 会在 App 请求时代操作者批准列出的同意能力(`call-tool` 让轮询型仪表盘无需弹窗即可刷新); -其余能力与 Workbench 一样,在宿主页面里等待 Allow/Deny 决定。 +`serve-app` 的每个选项——`--port`、`--input`、`--profile`、`--env-file`、`--plugin-root`——都以 argv +形式透传;宿主包保持自包含,因为框架是被启动的,而不是被打包进去的。 ## 服务器模式 diff --git a/website/docs/zh/reference/api.mdx b/website/docs/zh/reference/api.mdx index 41a4b2560..337145328 100644 --- a/website/docs/zh/reference/api.mdx +++ b/website/docs/zh/reference/api.mdx @@ -13,7 +13,7 @@ description: '生成的 agent-bundle 类型 API:它覆盖哪些入口点、如 | 入口点 | 内容 | | --- | --- | | `agent-bundle` | 编写与编排表面:`defineSkill`、`canonicalAgentEvents`、`startDevServer`、`runEvals`、`compareEvals`、eval harness 工厂,以及产物清单辅助函数。 | -| `agent-bundle/api` | 程序化编译器:`build`、`validate`、`inspect`、`prepack`,及其选项与结果类型,`AgentComponentKind` / `componentKindCapability` 组件类型辅助,以及产物操作 `listMcp`、`invokeMcp`、`runMcp`、`serveApp`(在浏览器里独立提供一个已构建的 MCP App)、`listHooks` 与 `simulateHook`。 | +| `agent-bundle/api` | 程序化编译器:`build`、`validate`、`inspect`、`prepack`,及其选项与结果类型,`AgentComponentKind` / `componentKindCapability` 组件类型辅助,以及产物操作 `listMcp`、`invokeMcp`、`runMcp`、`serveApp`(在浏览器里独立提供一个已构建的 MCP App——面向脚本与测试的宿主进程 API;产物内部的路由式 CLI 命令无法导入它,见[独立提供 App](../guide/authoring/mcp.mdx#独立提供-app))、`listHooks` 与 `simulateHook`。 | | `agent-bundle/config` | `defineConfig` 与配置类型。 | | `agent-bundle/test` | 路由测试 harness、匹配器与契约矩阵。 | | `agent-bundle/test/browser` | 面向浏览器渲染视图的 MCP App bridge harness。 | diff --git a/website/docs/zh/reference/cli.mdx b/website/docs/zh/reference/cli.mdx index 5f41df15c..e74e56c53 100644 --- a/website/docs/zh/reference/cli.mdx +++ b/website/docs/zh/reference/cli.mdx @@ -95,7 +95,8 @@ Workbench:该命令以与 `mcp run` 完全相同的方式启动插件打包好 宿主只绑定 `127.0.0.1`,只提供一份文档与经过认证的 `/api/mcp/...` 路由(每次启动一个令牌,加同源检查; 拒绝时为 `AB8003` / `AB8004`),并且只通过桥接暴露所选的那个服务器。App 文档本身运行在框架沙箱内的第二个 -loopback origin 上。它是本地预览宿主,不是部署目标。编程形式是 `agent-bundle/api` 中的 `serveApp`;见 +loopback origin 上。它是本地预览宿主,不是部署目标。面向脚本与测试的编程形式是 `agent-bundle/api` 中的 +`serveApp`;插件自己的路由式 CLI 命令则改为启动这条命令,因为自包含的 bin 无法导入 `agent-bundle/api`——见 [独立提供 App](../guide/authoring/mcp.mdx#独立提供-app)。 ## build 与 prepack From d6c4971e28fe38c02e34def5e02a7e29e24e6e44 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 22:13:22 +0000 Subject: [PATCH 2/2] docs(mcp): parse the serve-app URL from whole stdout lines in the dashboard sample A child stdout write can arrive split across data events; buffer to newlines before matching, as cargo-hauler's route does. Addresses the Codex P2 thread on #560. --- website/docs/en/guide/authoring/mcp.mdx | 11 +++++++++-- website/docs/zh/guide/authoring/mcp.mdx | 12 +++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index 223d8907a..44657c169 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -738,12 +738,19 @@ export default async function dashboard({ input, signal }: CliRouteProps { // The child prints `MCP App at (…)`; relay it to stderr so the routed - // CLI keeps stdout for its JSON result. + // CLI keeps stdout for its JSON result, and parse whole lines only — one write can + // arrive split across chunks. const text = chunk.toString('utf8'); process.stderr.write(text); - url ??= /\bat (https?:\/\/\S+)/u.exec(text)?.[1] ?? null; + pending += text; + const lines = pending.split('\n'); + pending = lines.pop() ?? ''; + for (const line of lines) { + url ??= /\bat (https?:\/\/\S+)/u.exec(line)?.[1] ?? null; + } }); // Ctrl-C reaching the routed CLI becomes the child's SIGTERM. signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true }); diff --git a/website/docs/zh/guide/authoring/mcp.mdx b/website/docs/zh/guide/authoring/mcp.mdx index 5cdcebccd..f4430c2d7 100644 --- a/website/docs/zh/guide/authoring/mcp.mdx +++ b/website/docs/zh/guide/authoring/mcp.mdx @@ -663,12 +663,18 @@ export default async function dashboard({ input, signal }: CliRouteProps { - // 子进程打印 `MCP App at (…)`;转发到 stderr, - // 让路由式 CLI 的 stdout 留给 JSON 结果。 + // 子进程打印 `MCP App at (…)`;转发到 stderr,让路由式 CLI 的 stdout + // 留给 JSON 结果,并且只解析完整的行——一次写入可能被拆成多个 chunk 到达。 const text = chunk.toString('utf8'); process.stderr.write(text); - url ??= /\bat (https?:\/\/\S+)/u.exec(text)?.[1] ?? null; + pending += text; + const lines = pending.split('\n'); + pending = lines.pop() ?? ''; + for (const line of lines) { + url ??= /\bat (https?:\/\/\S+)/u.exec(line)?.[1] ?? null; + } }); // 抵达路由式 CLI 的 Ctrl-C 变成子进程的 SIGTERM。 signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true });