@botbotgo/agent-harness 0.0.308 → 0.0.309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1178,6 +1178,7 @@ ACP transport notes:
1178
1178
  - ACP transport validation now covers the reference-client core flow: capability discovery, request submit, session lookup, request lookup, invalid-JSON handling, notification calls without response ids, stdio JSON-RPC, and HTTP plus SSE runtime notifications.
1179
1179
  - Cross-protocol conformance now has an explicit regression gate as well: ACP submission, A2A task lookup and continuation, and runtime MCP inspection must all project the same persisted `sessionId` / `requestId` runtime records instead of drifting into surface-specific identifiers or side stores.
1180
1180
  - For the thinnest editor or CLI starter, begin with `agent-harness acp serve --workspace . --transport stdio` and mirror the `examples/03_protocol-surfaces/app/acp-stdio/main.mjs` wire shape. Applications that want an in-process reference client can use `createAcpStdioClient(...)` to issue JSON-RPC requests and route runtime notifications without hand-rolling line parsing.
1181
+ - When a local ACP HTTP endpoint should stay up beyond one foreground shell, use `botbotgo acp start --workspace . --host 127.0.0.1 --port 8787` and later `botbotgo acp stop --workspace .`. The CLI records the managed process under `.botbotgo/services/` so local operators can start and stop it without a second process manager.
1181
1182
  - `botbotgo` is the shortest local terminal entrypoint for a published install. It defaults the workspace to the current directory, so teams can `cd` into their own runtime workspace and run `botbotgo` or `botbotgo "..."` directly. `agent-harness` keeps the same implicit-chat behavior, and `-w/--workspace` still lets one shell point at another workspace when needed.
1182
1183
  - `agent-harness chat --workspace .` still acts as a local terminal shell over the shared `HarnessClient` contract when you want the explicit subcommand form: the default `stdio` path runs directly in-process against the workspace runtime, while `--transport http --host <host> --port <port>` can still target an already-running ACP HTTP endpoint. One-shot use can pass `--message`, while interactive mode supports `/context`, `/new`, `/agent <agentId>`, `/sessions`, `/requests`, `/resume <sessionId>`, `/approvals`, `/approve`, `/reject`, `/cancel`, `/events`, `/trace`, `/health`, `/overview`, `/session`, `/request [requestId]`, and `/exit`. `/resume` now validates the target session, restores its latest request id, and rehydrates the active agent context; `/request <requestId>` can switch the active request context to a persisted request; `/new` clears the current session/request context without leaving the shell. Terminal chat now keeps one operator-facing flow: it streams tool and progress events as they happen and still prints the full final answer, instead of switching to a separate request-tree-only mode.
1183
1184
  - Local repo usage differs from the published binary on purpose. After cloning the repo, use `npm run chat -- --workspace ./config` or `npm run agent-harness -- chat --workspace ./config`. The extra `--` matters: without it, npm treats `--workspace` as npm's own workspace flag. The standalone `agent-harness ...` command only works after the package has been installed or linked so the `bin` entry is on your `PATH`.
@@ -1186,8 +1187,9 @@ ACP transport notes:
1186
1187
  - Chat startup now also performs a lightweight workspace-model preflight for the local Ollama path. If the configured endpoint is unreachable or responds with `404 page not found`, the shell prints that warning before the first prompt so endpoint mismatches show up immediately instead of only after the first failed message.
1187
1188
  - The interactive prompt now carries the live `agent`, `session`, and short `request` identifier together, so after each reply and during shell-history navigation the user stays anchored to the current runtime turn instead of dropping back to a bare input prompt.
1188
1189
  - `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and A2A v1.0 PascalCase methods such as `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetAgentCard`, `GetExtendedAgentCard`, and task push-notification config methods onto the existing session/request runtime surface. The bridge now advertises both `1.0` and `0.3` JSON-RPC interfaces, answers `HEAD` / `OPTIONS` discovery on the agent-card path, sets supported-version discovery headers, can optionally expose registry URLs plus detached signed-card metadata for surrounding discovery systems, validates `A2A-Version`, records `A2A-Extensions` into runtime invocation metadata, publishes `TASK_STATE_*` statuses plus the `{ task }` `SendMessage` wrapper, streams an initial `{ task }` snapshot plus later `{ statusUpdate }` payloads over SSE for v1 streaming methods, and can send best-effort webhook task snapshots for configured push notification receivers.
1190
+ - For a managed local bridge, use `botbotgo a2a start --workspace . --host 127.0.0.1 --port 8080` and later `botbotgo a2a stop --workspace .`. This uses the same `.botbotgo/services/` state directory as ACP so both local protocol bridges follow one terminal-first workflow.
1189
1191
  - `serveAgUiHttp(runtime)` exposes an AG-UI-compatible HTTP SSE bridge that projects runtime lifecycle, safe progress commentary, text output, upstream thinking, step progress, and tool calls onto `RUN_*`, `STATUS_UPDATE`, `TEXT_MESSAGE_*`, `THINKING_TEXT_MESSAGE_*`, `STEP_*`, and `TOOL_CALL_*` events for UI clients.
1190
- - `createRuntimeMcpServer(runtime)` and `serveRuntimeMcpOverStdio(runtime)` expose the persisted runtime control surface itself as MCP tools, including sessions, requests, approvals, artifacts, events, and package export helpers.
1192
+ - `createRuntimeMcpServer(runtime)`, `serveRuntimeMcpOverStdio(runtime)`, and `serveRuntimeMcpOverStreamableHttp(runtime)` expose the persisted runtime control surface itself as MCP tools, including sessions, requests, approvals, artifacts, events, and package export helpers. `botbotgo mcp serve --transport streamable-http --host 127.0.0.1 --port 8090` serves the same tool surface over Streamable HTTP, and `botbotgo mcp start|stop` manages that background endpoint for one workspace.
1191
1193
  - `listRequestEvents(...)`, `listRequestTraceItems(...)`, and `exportRequestPackage(...)` are the request-first inspection helpers.
1192
1194
  - `exportRequestPackage(...)` and `exportSessionPackage(...)` package stable runtime records, transcript, approvals, events, artifacts, and governance evidence for operator tooling without reaching into persistence internals.
1193
1195
  - `runtime/default.governance.remoteMcp` can now deny or allow specific MCP servers, raise approval requirements by transport, and stamp transport-based risk tiers into runtime governance bundles. MCP server catalogs can also declare trust tier, access mode, tenant scope, approval policy, prompt-injection risk, and OAuth scope metadata so governance bundles capture why one remote tool is treated as high-risk. Tool policy overrides can also set `decisionMode: manual | auto-approve | auto-reject | deny-and-continue` so operator evidence and execution behavior stay aligned.
package/README.zh.md CHANGED
@@ -1144,6 +1144,7 @@ ACP transport 说明:
1144
1144
  - ACP transport 现已覆盖核心参考客户端流程验证:capability discovery、request submit、session lookup、request lookup、invalid JSON 处理、无 id notification 不返回响应,以及 stdio JSON-RPC 与 HTTP + SSE runtime notifications。
1145
1145
  - 现在还额外有一条跨协议一致性回归门:ACP 发起的 request、A2A 读取或继续的 task,以及 runtime MCP 暴露的检查结果,必须始终指向同一组持久化 `sessionId` / `requestId` 运行时记录,不能漂移成各协议各自维护的标识体系。
1146
1146
  - 如果要从最薄的一层 editor / CLI starter 开始,优先用 `agent-harness acp serve --workspace . --transport stdio`,并直接参考 `examples/03_protocol-surfaces/app/acp-stdio/main.mjs` 的 wire shape。需要在应用内使用 reference client 时,可直接用 `createAcpStdioClient(...)` 发起 JSON-RPC 请求并分流 runtime notifications,避免每个 sidecar 自己重写 line parsing。
1147
+ - 如果本地 ACP HTTP endpoint 需要在前台 shell 之外持续运行,可使用 `botbotgo acp start --workspace . --host 127.0.0.1 --port 8787`,之后再用 `botbotgo acp stop --workspace .` 停止。CLI 会把受管进程记录到 `.botbotgo/services/` 下,因此本地运维不必再额外引入第二套进程管理器。
1147
1148
  - 对发布后的安装包来说,`botbotgo` 是最短的本地终端入口。它会默认把当前目录当作 workspace,所以团队只需要 `cd` 到自己的 runtime workspace,然后直接运行 `botbotgo` 或 `botbotgo "..."`。`agent-harness` 也保留同样的隐式 chat 行为;如果要从同一个 shell 指向别的 workspace,继续用 `-w/--workspace` 即可。
1148
1149
  - `agent-harness chat --workspace .` 则继续保留为显式子命令形式的本地终端壳:默认 `stdio` 路径会直接在当前进程里连接 workspace runtime,而 `--transport http --host <host> --port <port>` 仍然可以直连已运行的 ACP HTTP endpoint。一次性调用可用 `--message`,交互模式则支持 `/context`、`/new`、`/agent <agentId>`、`/sessions`、`/requests`、`/resume <sessionId>`、`/approvals`、`/approve`、`/reject`、`/cancel`、`/events`、`/trace`、`/health`、`/overview`、`/session`、`/request [requestId]` 与 `/exit`。`/resume` 现在会先校验目标 session,恢复它的 latest request id,并同步 active agent context;`/request <requestId>` 可以把当前上下文切到某个已持久化 request;`/new` 则会在不退出 shell 的情况下清空当前 session/request context。终端 chat 现在统一走一条面向 operator 的输出路径:tool 和进度事件会实时打印,最终完整答复也会继续输出,而不再切到单独的 request-tree-only 模式。
1149
1150
  - 如果是在仓库源码目录里直接运行,请使用 `npm run chat -- --workspace ./config`,或者 `npm run agent-harness -- chat --workspace ./config`。这里额外的 `--` 不能省略,否则 npm 会把 `--workspace` 当成它自己的 workspace 参数。只有在包已经被安装或 `npm link` 到系统环境后,`agent-harness ...` 这个独立命令才会出现在 `PATH` 里。
@@ -1152,8 +1153,9 @@ ACP transport 说明:
1152
1153
  - chat 启动时现在还会对本地 Ollama 路径做一次轻量 preflight。如果当前 endpoint 不可达,或者直接返回 `404 page not found`,shell 会在第一条 prompt 之前先打印这条告警,这样 endpoint 配错的问题会在启动时立即暴露,而不必等第一条消息失败后才知道。
1153
1154
  - 交互式 prompt 现在会同时显示当前 `agent`、`session` 和短 `request` 标识,因此每轮回复之后以及使用 shell 历史记录时,用户都还能清楚地看到自己正处在哪个 runtime turn 上,而不会掉回一个没有上下文的裸输入提示。
1154
1155
  - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask`、`GetAgentCard`、`GetExtendedAgentCard` 与 task push-notification config 这类 A2A v1.0 方法,并统一映射到现有 session/request 运行记录。bridge 现在会同时声明 `1.0` 与 `0.3` 两个 JSON-RPC interface、在 agent card 路径上响应 `HEAD` / `OPTIONS` discovery、写出支持版本的 discovery headers、可选暴露 registry URL 与 detached signed-card metadata 供外围发现系统使用、校验 `A2A-Version`、把 `A2A-Extensions` 记录进 runtime invocation metadata、发布 `TASK_STATE_*` 状态与 `SendMessage` 的 `{ task }` wrapper、在 v1 streaming 方法上先通过 SSE 输出 `{ task }` 初始快照,再输出 `{ statusUpdate }` 增量状态,并可向已配置的 webhook receiver 发送 best-effort task push notifications。
1156
+ - 如果要把本地 A2A bridge 作为受管后台服务运行,可使用 `botbotgo a2a start --workspace . --host 127.0.0.1 --port 8080`,之后再用 `botbotgo a2a stop --workspace .` 停止。它与 ACP 共用 `.botbotgo/services/` 状态目录,因此两个本地协议桥都可以沿用同一套终端优先运维流程。
1155
1157
  - `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、安全进度播报、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`STATUS_UPDATE`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
1156
- - `createRuntimeMcpServer(runtime)` 与 `serveRuntimeMcpOverStdio(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。
1158
+ - `createRuntimeMcpServer(runtime)`、`serveRuntimeMcpOverStdio(runtime)` 与 `serveRuntimeMcpOverStreamableHttp(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。`botbotgo mcp serve --transport streamable-http --host 127.0.0.1 --port 8090` 会把同一套控制面作为 Streamable HTTP 暴露出去,而 `botbotgo mcp start|stop` 可直接托管该后台 endpoint
1157
1159
  - `listRequestEvents(...)`、`listRequestTraceItems(...)` 与 `exportRequestPackage(...)` 是 request-first 的检查 helper。
1158
1160
  - `exportRequestPackage(...)` 与 `exportSessionPackage(...)` 可把稳定 runtime 记录、transcript、approvals、events、artifacts 与 governance evidence 一起打包给管理工具,而不必直接访问 persistence 内部实现。
1159
1161
  - `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。MCP server catalog 也可以声明 trust tier、access mode、tenant scope、approval policy、prompt-injection risk 与 OAuth scope 元数据,让治理快照能解释为什么某个远端工具被视为高风险。tool policy override 也可以声明 `decisionMode: manual | auto-approve | auto-reject | deny-and-continue`,让治理快照与实际执行路径保持一致。
package/dist/api.d.ts CHANGED
@@ -2,9 +2,10 @@ import type { ArtifactListing, CancelOptions, InvocationEnvelope, ListMemoriesIn
2
2
  import { AgentHarnessRuntime } from "./runtime/harness.js";
3
3
  import type { InventoryAgentRecord, InventorySkillRecord } from "./runtime/harness/system/inventory.js";
4
4
  import type { RequirementAssessmentOptions } from "./runtime/harness/system/skill-requirements.js";
5
- import type { RuntimeMcpServerOptions, ToolMcpServerOptions } from "./mcp.js";
5
+ import type { RuntimeMcpServerOptions, RuntimeMcpStreamableHttpServerOptions, ToolMcpServerOptions } from "./mcp.js";
6
6
  export { AgentHarnessAcpServer, createAcpServer } from "./acp.js";
7
7
  export { createAcpStdioClient } from "./protocol/acp/client.js";
8
+ export type { RuntimeMcpStreamableHttpServer, RuntimeMcpStreamableHttpServerOptions } from "./mcp.js";
8
9
  export type { AcpApproval, AcpArtifact, AcpEventNotification, AcpNotification, AcpJsonRpcError, AcpJsonRpcRequest, AcpJsonRpcResponse, AcpJsonRpcSuccess, AcpRequestRecord, AcpRequestParams, AcpServerCapabilities, AcpSessionRecord, AcpStreamNotification, } from "./acp.js";
9
10
  export { AgentHarnessRuntime } from "./runtime/harness.js";
10
11
  export { createUpstreamTimelineReducer } from "./upstream-events.js";
@@ -208,3 +209,4 @@ export declare function createToolMcpServer(runtime: AgentHarnessRuntime, option
208
209
  export declare function serveToolsOverStdio(runtime: AgentHarnessRuntime, options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
209
210
  export declare function createRuntimeMcpServer(runtime: AgentHarnessRuntime, options?: RuntimeMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
210
211
  export declare function serveRuntimeMcpOverStdio(runtime: AgentHarnessRuntime, options?: RuntimeMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
212
+ export declare function serveRuntimeMcpOverStreamableHttp(runtime: AgentHarnessRuntime, options?: RuntimeMcpStreamableHttpServerOptions): Promise<import("./mcp.js").RuntimeMcpStreamableHttpServer>;
package/dist/api.js CHANGED
@@ -387,6 +387,9 @@ export async function createRuntimeMcpServer(runtime, options) {
387
387
  export async function serveRuntimeMcpOverStdio(runtime, options) {
388
388
  return runtime.serveRuntimeMcpOverStdio(options);
389
389
  }
390
+ export async function serveRuntimeMcpOverStreamableHttp(runtime, options) {
391
+ return runtime.serveRuntimeMcpOverStreamableHttp(options);
392
+ }
390
393
  function normalizeUserChatMessage(message) {
391
394
  if (message.role !== "user") {
392
395
  throw new Error("normalizeUserChatInput only accepts user-role chat messages.");
package/dist/cli.d.ts CHANGED
@@ -8,7 +8,7 @@ import { serveA2aOverHttp } from "./protocol/a2a/http.js";
8
8
  import { serveAgUiOverHttp } from "./protocol/ag-ui/http.js";
9
9
  import { serveAcpOverHttp } from "./protocol/acp/http.js";
10
10
  import { serveAcpOverStdio } from "./protocol/acp/stdio.js";
11
- import { serveRuntimeMcpOverStdio } from "./mcp.js";
11
+ import { serveRuntimeMcpOverStdio, serveRuntimeMcpOverStreamableHttp } from "./mcp.js";
12
12
  type CliIo = {
13
13
  cwd?: string;
14
14
  stdin?: Readable;
@@ -34,6 +34,17 @@ type CliDeps = {
34
34
  serveAcpOverHttp?: typeof serveAcpOverHttp;
35
35
  serveAcpOverStdio?: typeof serveAcpOverStdio;
36
36
  serveRuntimeMcpOverStdio?: typeof serveRuntimeMcpOverStdio;
37
+ serveRuntimeMcpOverStreamableHttp?: typeof serveRuntimeMcpOverStreamableHttp;
38
+ spawnManagedCliProcess?: (input: {
39
+ args: string[];
40
+ cwd: string;
41
+ stdoutPath: string;
42
+ stderrPath: string;
43
+ }) => Promise<{
44
+ pid?: number;
45
+ }>;
46
+ isManagedProcessRunning?: (pid: number) => boolean;
47
+ signalManagedProcess?: (pid: number, signal?: NodeJS.Signals) => void;
37
48
  };
38
49
  export declare function renderChatPromptLine(input: {
39
50
  agentId?: string;
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import { EventEmitter } from "node:events";
3
- import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
4
+ import { existsSync, openSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
5
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
6
  import { createInterface as createReadlineInterface } from "node:readline";
5
7
  import { Writable } from "node:stream";
6
8
  import path from "node:path";
@@ -14,7 +16,7 @@ import { serveA2aOverHttp } from "./protocol/a2a/http.js";
14
16
  import { serveAgUiOverHttp } from "./protocol/ag-ui/http.js";
15
17
  import { serveAcpOverHttp } from "./protocol/acp/http.js";
16
18
  import { serveAcpOverStdio } from "./protocol/acp/stdio.js";
17
- import { serveRuntimeMcpOverStdio } from "./mcp.js";
19
+ import { serveRuntimeMcpOverStdio, serveRuntimeMcpOverStreamableHttp } from "./mcp.js";
18
20
  import { interpolateEnvPlaceholders } from "./workspace/yaml-object-reader.js";
19
21
  function renderUsage() {
20
22
  return `Usage:
@@ -23,7 +25,11 @@ function renderUsage() {
23
25
  agent-harness [-w <path>] [prompt]
24
26
  botbotgo [-w <path>] [prompt]
25
27
  agent-harness acp serve [--workspace <path>] [--transport stdio|http] [--host <hostname>] [--port <port>]
28
+ agent-harness acp start [--workspace <path>] [--host <hostname>] [--port <port>]
29
+ agent-harness acp stop [--workspace <path>]
26
30
  agent-harness a2a serve [--workspace <path>] [--host <hostname>] [--port <port>]
31
+ agent-harness a2a start [--workspace <path>] [--host <hostname>] [--port <port>]
32
+ agent-harness a2a stop [--workspace <path>]
27
33
  agent-harness ag-ui serve [--workspace <path>] [--host <hostname>] [--port <port>]
28
34
  agent-harness runtime overview [--workspace <path>] [--limit <n>] [--json]
29
35
  agent-harness runtime health [--workspace <path>] [--json]
@@ -34,7 +40,12 @@ function renderUsage() {
34
40
  agent-harness runtime scheduled-run --workspace <path> --schedule <scheduleId>
35
41
  agent-harness runtime export request --workspace <path> --session <sessionId> --request <requestId> [--artifacts] [--artifact-contents] [--health] [--json]
36
42
  agent-harness runtime export session --workspace <path> --session <sessionId> [--artifacts] [--artifact-contents] [--health] [--json]
37
- agent-harness runtime-mcp serve [--workspace <path>]
43
+ agent-harness runtime-mcp serve [--workspace <path>] [--transport stdio|streamable-http] [--host <hostname>] [--port <port>]
44
+ agent-harness runtime-mcp start [--workspace <path>] [--host <hostname>] [--port <port>]
45
+ agent-harness runtime-mcp stop [--workspace <path>]
46
+ agent-harness mcp serve [--workspace <path>] [--transport stdio|streamable-http] [--host <hostname>] [--port <port>]
47
+ agent-harness mcp start [--workspace <path>] [--host <hostname>] [--port <port>]
48
+ agent-harness mcp stop [--workspace <path>]
38
49
 
39
50
  Run botbotgo or agent-harness from any folder.
40
51
  If ./config/ is absent, the runtime falls back to the bundled system defaults and bundled resources.
@@ -80,9 +91,9 @@ function parseInitOptions(args) {
80
91
  }
81
92
  return { options };
82
93
  }
83
- function parseAcpServeOptions(args) {
94
+ function parseAcpServeOptions(args, defaultTransport = "stdio") {
84
95
  let workspaceRoot;
85
- let transport = "stdio";
96
+ let transport = defaultTransport;
86
97
  let hostname;
87
98
  let port;
88
99
  for (let index = 0; index < args.length; index += 1) {
@@ -134,6 +145,23 @@ function parseAcpServeOptions(args) {
134
145
  }
135
146
  return { workspaceRoot, transport, hostname, port };
136
147
  }
148
+ function parseWorkspaceOnlyOptions(args) {
149
+ let workspaceRoot;
150
+ for (let index = 0; index < args.length; index += 1) {
151
+ const arg = args[index];
152
+ if (arg === "--workspace") {
153
+ const value = args[index + 1];
154
+ if (!value) {
155
+ return { error: "Missing value for --workspace" };
156
+ }
157
+ workspaceRoot = value;
158
+ index += 1;
159
+ continue;
160
+ }
161
+ return { error: `Unknown option: ${arg}` };
162
+ }
163
+ return { workspaceRoot };
164
+ }
137
165
  function parseHttpServeOptions(args, serviceLabel = "HTTP") {
138
166
  let workspaceRoot;
139
167
  let hostname;
@@ -175,6 +203,60 @@ function parseHttpServeOptions(args, serviceLabel = "HTTP") {
175
203
  }
176
204
  return { workspaceRoot, hostname, port };
177
205
  }
206
+ function parseRuntimeMcpServeOptions(args, defaultTransport = "stdio") {
207
+ let workspaceRoot;
208
+ let transport = defaultTransport;
209
+ let hostname;
210
+ let port;
211
+ for (let index = 0; index < args.length; index += 1) {
212
+ const arg = args[index];
213
+ if (arg === "--workspace") {
214
+ const value = args[index + 1];
215
+ if (!value) {
216
+ return { transport, hostname, port, error: "Missing value for --workspace" };
217
+ }
218
+ workspaceRoot = value;
219
+ index += 1;
220
+ continue;
221
+ }
222
+ if (arg === "--transport") {
223
+ const value = args[index + 1];
224
+ if (!value) {
225
+ return { transport, hostname, port, error: "Missing value for --transport" };
226
+ }
227
+ if (value !== "stdio" && value !== "streamable-http") {
228
+ return { transport, hostname, port, error: `Unsupported runtime MCP transport: ${value}` };
229
+ }
230
+ transport = value;
231
+ index += 1;
232
+ continue;
233
+ }
234
+ if (arg === "--host") {
235
+ const value = args[index + 1];
236
+ if (!value) {
237
+ return { transport, hostname, port, error: "Missing value for --host" };
238
+ }
239
+ hostname = value;
240
+ index += 1;
241
+ continue;
242
+ }
243
+ if (arg === "--port") {
244
+ const value = args[index + 1];
245
+ if (!value) {
246
+ return { transport, hostname, port, error: "Missing value for --port" };
247
+ }
248
+ const parsedPort = Number.parseInt(value, 10);
249
+ if (!Number.isFinite(parsedPort) || parsedPort < 0) {
250
+ return { transport, hostname, port, error: `Invalid runtime MCP port: ${value}` };
251
+ }
252
+ port = parsedPort;
253
+ index += 1;
254
+ continue;
255
+ }
256
+ return { transport, hostname, port, error: `Unknown option: ${arg}` };
257
+ }
258
+ return { workspaceRoot, transport, hostname, port };
259
+ }
178
260
  function parseChatOptions(args) {
179
261
  let workspaceRoot;
180
262
  let agentId;
@@ -250,9 +332,66 @@ function isTopLevelCliCommand(value) {
250
332
  || value === "acp"
251
333
  || value === "a2a"
252
334
  || value === "ag-ui"
335
+ || value === "mcp"
253
336
  || value === "runtime"
254
337
  || value === "runtime-mcp";
255
338
  }
339
+ function resolveManagedServiceRoot(workspaceRoot) {
340
+ return path.join(workspaceRoot, ".botbotgo", "services");
341
+ }
342
+ function managedServiceStatePath(workspaceRoot, service) {
343
+ return path.join(resolveManagedServiceRoot(workspaceRoot), `${service}.json`);
344
+ }
345
+ function managedServiceLogPath(workspaceRoot, service, stream) {
346
+ return path.join(resolveManagedServiceRoot(workspaceRoot), `${service}.${stream}.log`);
347
+ }
348
+ async function readManagedServiceState(workspaceRoot, service) {
349
+ const statePath = managedServiceStatePath(workspaceRoot, service);
350
+ if (!existsSync(statePath)) {
351
+ return null;
352
+ }
353
+ try {
354
+ return JSON.parse(await readFile(statePath, "utf8"));
355
+ }
356
+ catch {
357
+ return null;
358
+ }
359
+ }
360
+ async function writeManagedServiceState(state) {
361
+ const statePath = managedServiceStatePath(state.workspaceRoot, state.service);
362
+ await mkdir(path.dirname(statePath), { recursive: true });
363
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
364
+ }
365
+ async function clearManagedServiceState(workspaceRoot, service) {
366
+ await rm(managedServiceStatePath(workspaceRoot, service), { force: true });
367
+ }
368
+ function defaultIsManagedProcessRunning(pid) {
369
+ try {
370
+ process.kill(pid, 0);
371
+ return true;
372
+ }
373
+ catch {
374
+ return false;
375
+ }
376
+ }
377
+ function defaultSignalManagedProcess(pid, signal) {
378
+ process.kill(pid, signal);
379
+ }
380
+ async function defaultSpawnManagedCliProcess(input) {
381
+ await mkdir(path.dirname(input.stdoutPath), { recursive: true });
382
+ const stdoutFd = openSync(input.stdoutPath, "a");
383
+ const stderrFd = openSync(input.stderrPath, "a");
384
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...input.args], {
385
+ cwd: input.cwd,
386
+ detached: true,
387
+ stdio: ["ignore", stdoutFd, stderrFd],
388
+ });
389
+ child.unref();
390
+ return { pid: child.pid };
391
+ }
392
+ async function sleep(ms) {
393
+ await new Promise((resolve) => setTimeout(resolve, ms));
394
+ }
256
395
  function parseRuntimeInspectOptions(args) {
257
396
  let workspaceRoot;
258
397
  let json = false;
@@ -821,11 +960,6 @@ function renderHealthSnapshot(snapshot, workspacePath) {
821
960
  }
822
961
  return `${lines.join("\n")}\n`;
823
962
  }
824
- async function sleep(ms) {
825
- await new Promise((resolve) => {
826
- setTimeout(resolve, ms);
827
- });
828
- }
829
963
  function renderApprovalList(approvals) {
830
964
  if (approvals.length === 0) {
831
965
  return "No approvals matched.\n";
@@ -2041,6 +2175,10 @@ export async function runCli(argv, io = {}, deps = {}) {
2041
2175
  const serveAcpHttp = deps.serveAcpOverHttp ?? serveAcpOverHttp;
2042
2176
  const serveAcp = deps.serveAcpOverStdio ?? serveAcpOverStdio;
2043
2177
  const serveRuntimeMcp = deps.serveRuntimeMcpOverStdio ?? serveRuntimeMcpOverStdio;
2178
+ const serveRuntimeMcpStreamableHttp = deps.serveRuntimeMcpOverStreamableHttp ?? serveRuntimeMcpOverStreamableHttp;
2179
+ const spawnManagedCliProcess = deps.spawnManagedCliProcess ?? defaultSpawnManagedCliProcess;
2180
+ const isManagedProcessRunning = deps.isManagedProcessRunning ?? defaultIsManagedProcessRunning;
2181
+ const signalManagedProcess = deps.signalManagedProcess ?? defaultSignalManagedProcess;
2044
2182
  const createChatClient = deps.createChatClient ?? (async (input) => {
2045
2183
  if (input.transport === "http") {
2046
2184
  return createAcpHttpHarnessClient({
@@ -2342,6 +2480,68 @@ export async function runCli(argv, io = {}, deps = {}) {
2342
2480
  return 1;
2343
2481
  }
2344
2482
  };
2483
+ const startManagedHttpService = async (service, workspacePath, args) => {
2484
+ const existing = await readManagedServiceState(workspacePath, service);
2485
+ if (existing && isManagedProcessRunning(existing.pid)) {
2486
+ stderr(`${service.toUpperCase()} service is already running for ${workspacePath} (pid ${existing.pid}).\n`);
2487
+ return 0;
2488
+ }
2489
+ if (existing) {
2490
+ await clearManagedServiceState(workspacePath, service);
2491
+ }
2492
+ const stdoutPath = managedServiceLogPath(workspacePath, service, "stdout");
2493
+ const stderrPath = managedServiceLogPath(workspacePath, service, "stderr");
2494
+ const spawned = await spawnManagedCliProcess({
2495
+ args,
2496
+ cwd: workspacePath,
2497
+ stdoutPath,
2498
+ stderrPath,
2499
+ });
2500
+ if (!spawned.pid || spawned.pid <= 0) {
2501
+ stderr(`Failed to start ${service.toUpperCase()} service for ${workspacePath}.\n`);
2502
+ return 1;
2503
+ }
2504
+ await writeManagedServiceState({
2505
+ pid: spawned.pid,
2506
+ service,
2507
+ workspaceRoot: workspacePath,
2508
+ transport: service === "mcp" ? "streamable-http" : "http",
2509
+ startedAt: new Date().toISOString(),
2510
+ stdoutPath,
2511
+ stderrPath,
2512
+ });
2513
+ await sleep(250);
2514
+ if (!isManagedProcessRunning(spawned.pid)) {
2515
+ await clearManagedServiceState(workspacePath, service);
2516
+ stderr(`Failed to keep ${service.toUpperCase()} service running for ${workspacePath}. Check ${stderrPath}.\n`);
2517
+ return 1;
2518
+ }
2519
+ stderr(`Started ${service.toUpperCase()} service for ${workspacePath} (pid ${spawned.pid}). Logs: ${stdoutPath}, ${stderrPath}\n`);
2520
+ return 0;
2521
+ };
2522
+ const stopManagedHttpService = async (service, workspacePath) => {
2523
+ const existing = await readManagedServiceState(workspacePath, service);
2524
+ if (!existing) {
2525
+ stderr(`No managed ${service.toUpperCase()} service is recorded for ${workspacePath}.\n`);
2526
+ return 0;
2527
+ }
2528
+ if (!isManagedProcessRunning(existing.pid)) {
2529
+ await clearManagedServiceState(workspacePath, service);
2530
+ stderr(`Managed ${service.toUpperCase()} service for ${workspacePath} is already stopped.\n`);
2531
+ return 0;
2532
+ }
2533
+ signalManagedProcess(existing.pid, "SIGTERM");
2534
+ for (let attempt = 0; attempt < 20; attempt += 1) {
2535
+ await sleep(100);
2536
+ if (!isManagedProcessRunning(existing.pid)) {
2537
+ await clearManagedServiceState(workspacePath, service);
2538
+ stderr(`Stopped ${service.toUpperCase()} service for ${workspacePath}.\n`);
2539
+ return 0;
2540
+ }
2541
+ }
2542
+ stderr(`Timed out while stopping ${service.toUpperCase()} service for ${workspacePath} (pid ${existing.pid}).\n`);
2543
+ return 1;
2544
+ };
2345
2545
  if (command === "init") {
2346
2546
  if (!projectName?.trim()) {
2347
2547
  stderr(renderUsage());
@@ -2392,28 +2592,58 @@ export async function runCli(argv, io = {}, deps = {}) {
2392
2592
  }
2393
2593
  if (command === "acp") {
2394
2594
  const [subcommand, ...subcommandArgs] = [projectName, ...rest];
2395
- if (subcommand !== "serve") {
2595
+ if (subcommand !== "serve" && subcommand !== "start" && subcommand !== "stop") {
2396
2596
  stderr(renderUsage());
2397
2597
  return 1;
2398
2598
  }
2399
- const parsed = parseAcpServeOptions(subcommandArgs);
2400
- if (parsed.error) {
2401
- stderr(`${parsed.error}\n`);
2599
+ const parsedStop = subcommand === "stop" ? parseWorkspaceOnlyOptions(subcommandArgs) : undefined;
2600
+ const parsedServe = subcommand !== "stop"
2601
+ ? parseAcpServeOptions(subcommandArgs, subcommand === "start" ? "http" : "stdio")
2602
+ : undefined;
2603
+ const parseError = parsedStop?.error ?? parsedServe?.error;
2604
+ if (parseError) {
2605
+ stderr(`${parseError}\n`);
2402
2606
  stderr(renderUsage());
2403
2607
  return 1;
2404
2608
  }
2405
2609
  try {
2406
- const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2407
- const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2610
+ const workspaceRoot = parsedStop?.workspaceRoot ?? parsedServe?.workspaceRoot;
2611
+ const workspacePath = resolveCliWorkspaceRoot(cwd, workspaceRoot);
2612
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, workspaceRoot);
2408
2613
  if (workspaceError) {
2409
2614
  stderr(`${workspaceError}\n`);
2410
2615
  return 1;
2411
2616
  }
2617
+ if (subcommand === "start") {
2618
+ if (!parsedServe) {
2619
+ stderr(renderUsage());
2620
+ return 1;
2621
+ }
2622
+ if (parsedServe.transport !== "http") {
2623
+ stderr("ACP start only supports --transport http. Use `acp serve --transport stdio` for stdio clients.\n");
2624
+ return 1;
2625
+ }
2626
+ const args = ["acp", "serve", "--workspace", workspacePath, "--transport", "http"];
2627
+ if (parsedServe.hostname) {
2628
+ args.push("--host", parsedServe.hostname);
2629
+ }
2630
+ if (typeof parsedServe.port === "number") {
2631
+ args.push("--port", String(parsedServe.port));
2632
+ }
2633
+ return startManagedHttpService("acp", workspacePath, args);
2634
+ }
2635
+ if (subcommand === "stop") {
2636
+ return stopManagedHttpService("acp", workspacePath);
2637
+ }
2638
+ if (!parsedServe) {
2639
+ stderr(renderUsage());
2640
+ return 1;
2641
+ }
2412
2642
  const runtime = await createHarness(workspacePath);
2413
- if (parsed.transport === "http") {
2643
+ if (parsedServe.transport === "http") {
2414
2644
  const server = await serveAcpHttp(runtime, {
2415
- hostname: parsed.hostname,
2416
- port: parsed.port,
2645
+ hostname: parsedServe.hostname,
2646
+ port: parsedServe.port,
2417
2647
  });
2418
2648
  stderr(`Serving ACP over http from ${workspacePath} at ${server.rpcUrl} (events ${server.eventsUrl})\n`);
2419
2649
  await server.completed;
@@ -2469,27 +2699,47 @@ export async function runCli(argv, io = {}, deps = {}) {
2469
2699
  }
2470
2700
  if (command === "a2a") {
2471
2701
  const [subcommand, ...subcommandArgs] = [projectName, ...rest];
2472
- if (subcommand !== "serve") {
2702
+ if (subcommand !== "serve" && subcommand !== "start" && subcommand !== "stop") {
2473
2703
  stderr(renderUsage());
2474
2704
  return 1;
2475
2705
  }
2476
- const parsed = parseHttpServeOptions(subcommandArgs, "A2A");
2477
- if (parsed.error) {
2478
- stderr(`${parsed.error}\n`);
2706
+ const parsedStop = subcommand === "stop" ? parseWorkspaceOnlyOptions(subcommandArgs) : undefined;
2707
+ const parsedServe = subcommand !== "stop" ? parseHttpServeOptions(subcommandArgs, "A2A") : undefined;
2708
+ const parseError = parsedStop?.error ?? parsedServe?.error;
2709
+ if (parseError) {
2710
+ stderr(`${parseError}\n`);
2479
2711
  stderr(renderUsage());
2480
2712
  return 1;
2481
2713
  }
2482
2714
  try {
2483
- const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2484
- const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2715
+ const workspaceRoot = parsedStop?.workspaceRoot ?? parsedServe?.workspaceRoot;
2716
+ const workspacePath = resolveCliWorkspaceRoot(cwd, workspaceRoot);
2717
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, workspaceRoot);
2485
2718
  if (workspaceError) {
2486
2719
  stderr(`${workspaceError}\n`);
2487
2720
  return 1;
2488
2721
  }
2722
+ if (subcommand === "start") {
2723
+ const args = ["a2a", "serve", "--workspace", workspacePath];
2724
+ if (parsedServe?.hostname) {
2725
+ args.push("--host", parsedServe.hostname);
2726
+ }
2727
+ if (typeof parsedServe?.port === "number") {
2728
+ args.push("--port", String(parsedServe.port));
2729
+ }
2730
+ return startManagedHttpService("a2a", workspacePath, args);
2731
+ }
2732
+ if (subcommand === "stop") {
2733
+ return stopManagedHttpService("a2a", workspacePath);
2734
+ }
2735
+ if (!parsedServe) {
2736
+ stderr(renderUsage());
2737
+ return 1;
2738
+ }
2489
2739
  const runtime = await createHarness(workspacePath);
2490
2740
  const server = await serveA2a(runtime, {
2491
- hostname: parsed.hostname,
2492
- port: parsed.port,
2741
+ hostname: parsedServe.hostname,
2742
+ port: parsedServe.port,
2493
2743
  });
2494
2744
  stderr(`Serving A2A over http from ${workspacePath} at ${server.rpcUrl} (card ${server.agentCardUrl})\n`);
2495
2745
  await server.completed;
@@ -2502,33 +2752,73 @@ export async function runCli(argv, io = {}, deps = {}) {
2502
2752
  return 1;
2503
2753
  }
2504
2754
  }
2505
- if (command === "runtime-mcp") {
2755
+ if (command === "mcp" || command === "runtime-mcp") {
2506
2756
  const [subcommand, ...subcommandArgs] = [projectName, ...rest];
2507
- if (subcommand !== "serve") {
2757
+ if (subcommand !== "serve" && subcommand !== "start" && subcommand !== "stop") {
2508
2758
  stderr(renderUsage());
2509
2759
  return 1;
2510
2760
  }
2511
- const parsed = parseHttpServeOptions(subcommandArgs, "runtime MCP");
2512
- if (parsed.error) {
2513
- stderr(`${parsed.error}\n`);
2761
+ const parsedStop = subcommand === "stop" ? parseWorkspaceOnlyOptions(subcommandArgs) : undefined;
2762
+ const parsedServe = subcommand !== "stop"
2763
+ ? parseRuntimeMcpServeOptions(subcommandArgs, subcommand === "start" ? "streamable-http" : "stdio")
2764
+ : undefined;
2765
+ const parseError = parsedStop?.error ?? parsedServe?.error;
2766
+ if (parseError) {
2767
+ stderr(`${parseError}\n`);
2514
2768
  stderr(renderUsage());
2515
2769
  return 1;
2516
2770
  }
2517
2771
  try {
2518
- const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2519
- const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2772
+ const workspaceRoot = parsedStop?.workspaceRoot ?? parsedServe?.workspaceRoot;
2773
+ const workspacePath = resolveCliWorkspaceRoot(cwd, workspaceRoot);
2774
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, workspaceRoot);
2520
2775
  if (workspaceError) {
2521
2776
  stderr(`${workspaceError}\n`);
2522
2777
  return 1;
2523
2778
  }
2779
+ if (subcommand === "start") {
2780
+ if (!parsedServe) {
2781
+ stderr(renderUsage());
2782
+ return 1;
2783
+ }
2784
+ if (parsedServe.transport !== "streamable-http") {
2785
+ stderr("MCP start only supports --transport streamable-http. Use `mcp serve --transport stdio` for stdio clients.\n");
2786
+ return 1;
2787
+ }
2788
+ const args = ["mcp", "serve", "--workspace", workspacePath, "--transport", "streamable-http"];
2789
+ if (parsedServe.hostname) {
2790
+ args.push("--host", parsedServe.hostname);
2791
+ }
2792
+ if (typeof parsedServe.port === "number") {
2793
+ args.push("--port", String(parsedServe.port));
2794
+ }
2795
+ return startManagedHttpService("mcp", workspacePath, args);
2796
+ }
2797
+ if (subcommand === "stop") {
2798
+ return stopManagedHttpService("mcp", workspacePath);
2799
+ }
2800
+ if (!parsedServe) {
2801
+ stderr(renderUsage());
2802
+ return 1;
2803
+ }
2524
2804
  const runtime = await createHarness(workspacePath);
2525
- stderr(`Serving runtime MCP over stdio from ${workspacePath}\n`);
2526
- const server = await serveRuntimeMcp(runtime);
2527
- await new Promise((resolve, reject) => {
2528
- process.stdin.on("end", resolve);
2529
- process.stdin.on("error", reject);
2530
- });
2531
- await server.close();
2805
+ if (parsedServe.transport === "streamable-http") {
2806
+ const server = await serveRuntimeMcpStreamableHttp(runtime, {
2807
+ hostname: parsedServe.hostname,
2808
+ port: parsedServe.port,
2809
+ });
2810
+ stderr(`Serving runtime MCP over streamable HTTP from ${workspacePath} at ${server.url}\n`);
2811
+ await server.completed;
2812
+ }
2813
+ else {
2814
+ stderr(`Serving runtime MCP over stdio from ${workspacePath}\n`);
2815
+ const server = await serveRuntimeMcp(runtime);
2816
+ await new Promise((resolve, reject) => {
2817
+ process.stdin.on("end", resolve);
2818
+ process.stdin.on("error", reject);
2819
+ });
2820
+ await server.close();
2821
+ }
2532
2822
  await runtime.stop();
2533
2823
  return 0;
2534
2824
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { AgentHarnessAcpServer, AgentHarnessRuntime, cancelRequest, createAgentHarness, createAcpServer, createAcpStdioClient, createRuntimeMcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, exportFlow, exportSequence, exportRequestPackage, exportSessionPackage, replayEvaluationBundle, getArtifact, getAgent, getApproval, getOperatorOverview, getRequestPlanState, getRequest, getHealth, listMemories, listRequestTraceItems, getSession, listAgentSkills, listRequestArtifacts, listApprovals, listRequests, listRequestEvents, listSessionSummaries, listSessions, memorize, normalizeUserChatInput, recordArtifact, request, recall, removeMemory, resolveApproval, serveA2aHttp, serveAcpHttp, serveAcpStdio, serveAgUiHttp, serveRuntimeMcpOverStdio, serveToolsOverStdio, subscribe, stop, updateMemory, } from "./api.js";
1
+ export { AgentHarnessAcpServer, AgentHarnessRuntime, cancelRequest, createAgentHarness, createAcpServer, createAcpStdioClient, createRuntimeMcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, exportFlow, exportSequence, exportRequestPackage, exportSessionPackage, replayEvaluationBundle, getArtifact, getAgent, getApproval, getOperatorOverview, getRequestPlanState, getRequest, getHealth, listMemories, listRequestTraceItems, getSession, listAgentSkills, listRequestArtifacts, listApprovals, listRequests, listRequestEvents, listSessionSummaries, listSessions, memorize, normalizeUserChatInput, recordArtifact, request, recall, removeMemory, resolveApproval, serveA2aHttp, serveAcpHttp, serveAcpStdio, serveAgUiHttp, serveRuntimeMcpOverStdio, serveRuntimeMcpOverStreamableHttp, serveToolsOverStdio, subscribe, stop, updateMemory, } from "./api.js";
2
2
  export { AcpHarnessClient, InProcessHarnessClient, createAcpHarnessClient, createAcpHttpHarnessClient, createAcpStdioHarnessClient, createAgentHarnessClient, createInProcessHarnessClient, } from "./client.js";
3
3
  export { createKnowledgeModule, readKnowledgeRuntimeConfig } from "./knowledge/index.js";
4
4
  export { readProceduralMemoryRuntimeConfig } from "./procedural/index.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { AgentHarnessAcpServer, AgentHarnessRuntime, cancelRequest, createAgentHarness, createAcpServer, createAcpStdioClient, createRuntimeMcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, exportFlow, exportSequence, exportRequestPackage, exportSessionPackage, replayEvaluationBundle, getArtifact, getAgent, getApproval, getOperatorOverview, getRequestPlanState, getRequest, getHealth, listMemories, listRequestTraceItems, getSession, listAgentSkills, listRequestArtifacts, listApprovals, listRequests, listRequestEvents, listSessionSummaries, listSessions, memorize, normalizeUserChatInput, recordArtifact, request, recall, removeMemory, resolveApproval, serveA2aHttp, serveAcpHttp, serveAcpStdio, serveAgUiHttp, serveRuntimeMcpOverStdio, serveToolsOverStdio, subscribe, stop, updateMemory, } from "./api.js";
1
+ export { AgentHarnessAcpServer, AgentHarnessRuntime, cancelRequest, createAgentHarness, createAcpServer, createAcpStdioClient, createRuntimeMcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, exportFlow, exportSequence, exportRequestPackage, exportSessionPackage, replayEvaluationBundle, getArtifact, getAgent, getApproval, getOperatorOverview, getRequestPlanState, getRequest, getHealth, listMemories, listRequestTraceItems, getSession, listAgentSkills, listRequestArtifacts, listApprovals, listRequests, listRequestEvents, listSessionSummaries, listSessions, memorize, normalizeUserChatInput, recordArtifact, request, recall, removeMemory, resolveApproval, serveA2aHttp, serveAcpHttp, serveAcpStdio, serveAgUiHttp, serveRuntimeMcpOverStdio, serveRuntimeMcpOverStreamableHttp, serveToolsOverStdio, subscribe, stop, updateMemory, } from "./api.js";
2
2
  export { AcpHarnessClient, InProcessHarnessClient, createAcpHarnessClient, createAcpHttpHarnessClient, createAcpStdioHarnessClient, createAgentHarnessClient, createInProcessHarnessClient, } from "./client.js";
3
3
  export { createKnowledgeModule, readKnowledgeRuntimeConfig } from "./knowledge/index.js";
4
4
  export { readProceduralMemoryRuntimeConfig } from "./procedural/index.js";
package/dist/mcp.d.ts CHANGED
@@ -19,6 +19,19 @@ export type RuntimeMcpServerOptions = {
19
19
  version?: string;
20
20
  };
21
21
  };
22
+ export type RuntimeMcpStreamableHttpServerOptions = RuntimeMcpServerOptions & {
23
+ hostname?: string;
24
+ port?: number;
25
+ path?: string;
26
+ };
27
+ export type RuntimeMcpStreamableHttpServer = {
28
+ hostname: string;
29
+ port: number;
30
+ path: string;
31
+ url: string;
32
+ completed: Promise<void>;
33
+ close: () => Promise<void>;
34
+ };
22
35
  type RuntimeMcpRuntime = {
23
36
  listSessions: (filter?: {
24
37
  agentId?: string;
@@ -59,4 +72,5 @@ export declare function createToolMcpServerFromTools(tools: ToolMcpServerTool[],
59
72
  export declare function serveToolsOverStdioFromHarness(tools: ToolMcpServerTool[], options: ToolMcpServerOptions): Promise<McpServer>;
60
73
  export declare function createRuntimeMcpServer(runtime: RuntimeMcpRuntime, options?: RuntimeMcpServerOptions): Promise<McpServer>;
61
74
  export declare function serveRuntimeMcpOverStdio(runtime: RuntimeMcpRuntime, options?: RuntimeMcpServerOptions): Promise<McpServer>;
75
+ export declare function serveRuntimeMcpOverStreamableHttp(runtime: RuntimeMcpRuntime, options?: RuntimeMcpStreamableHttpServerOptions): Promise<RuntimeMcpStreamableHttpServer>;
62
76
  export {};
package/dist/mcp.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { randomUUID } from "node:crypto";
5
+ import { createServer } from "node:http";
3
6
  import { pathToFileURL } from "node:url";
4
7
  import { z } from "zod";
5
8
  import { AGENT_HARNESS_VERSION } from "./package-version.js";
@@ -46,6 +49,33 @@ function renderToolOutput(output) {
46
49
  }
47
50
  return JSON.stringify(output, null, 2);
48
51
  }
52
+ function normalizePath(value, fallback) {
53
+ const source = typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
54
+ return source.startsWith("/") ? source : `/${source}`;
55
+ }
56
+ function writeJson(response, statusCode, payload) {
57
+ response.statusCode = statusCode;
58
+ response.setHeader("content-type", "application/json; charset=utf-8");
59
+ response.end(JSON.stringify(payload));
60
+ }
61
+ function readRequestBody(request) {
62
+ return new Promise((resolve, reject) => {
63
+ const chunks = [];
64
+ request.on("data", (chunk) => {
65
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
66
+ });
67
+ request.on("end", () => {
68
+ resolve(Buffer.concat(chunks).toString("utf8"));
69
+ });
70
+ request.on("error", reject);
71
+ });
72
+ }
73
+ function isInitializeRequest(payload) {
74
+ return typeof payload === "object"
75
+ && payload !== null
76
+ && !Array.isArray(payload)
77
+ && payload.method === "initialize";
78
+ }
49
79
  function jsonSchemaObjectToRawShape(schema) {
50
80
  if (!schema || schema.type !== "object") {
51
81
  return undefined;
@@ -251,3 +281,148 @@ export async function serveRuntimeMcpOverStdio(runtime, options = {}) {
251
281
  await server.connect(new StdioServerTransport());
252
282
  return server;
253
283
  }
284
+ export async function serveRuntimeMcpOverStreamableHttp(runtime, options = {}) {
285
+ const hostname = options.hostname?.trim() || "127.0.0.1";
286
+ const port = typeof options.port === "number" && Number.isFinite(options.port) ? options.port : 0;
287
+ const mcpPath = normalizePath(options.path, "/mcp");
288
+ const sessions = new Map();
289
+ const closeSession = async (sessionId) => {
290
+ const session = sessions.get(sessionId);
291
+ if (!session) {
292
+ return;
293
+ }
294
+ sessions.delete(sessionId);
295
+ await Promise.allSettled([
296
+ session.transport.close(),
297
+ session.server.close(),
298
+ ]);
299
+ };
300
+ const httpServer = createServer(async (request, response) => {
301
+ try {
302
+ const requestUrl = new URL(request.url ?? "/", `http://${hostname}`);
303
+ if (requestUrl.pathname !== mcpPath) {
304
+ writeJson(response, 404, {
305
+ error: "Not Found",
306
+ path: mcpPath,
307
+ });
308
+ return;
309
+ }
310
+ const headerValue = request.headers["mcp-session-id"];
311
+ const sessionId = Array.isArray(headerValue) ? headerValue[0] : headerValue;
312
+ if (request.method === "POST") {
313
+ const body = await readRequestBody(request);
314
+ let payload;
315
+ try {
316
+ payload = JSON.parse(body);
317
+ }
318
+ catch {
319
+ writeJson(response, 400, {
320
+ jsonrpc: "2.0",
321
+ id: null,
322
+ error: {
323
+ code: -32700,
324
+ message: "Invalid JSON payload.",
325
+ },
326
+ });
327
+ return;
328
+ }
329
+ const existingSession = sessionId ? sessions.get(sessionId) : undefined;
330
+ if (existingSession) {
331
+ await existingSession.transport.handleRequest(request, response, payload);
332
+ return;
333
+ }
334
+ if (sessionId || !isInitializeRequest(payload)) {
335
+ writeJson(response, 400, {
336
+ jsonrpc: "2.0",
337
+ id: null,
338
+ error: {
339
+ code: -32000,
340
+ message: "Bad Request: No valid MCP session was provided.",
341
+ },
342
+ });
343
+ return;
344
+ }
345
+ let createdTransport;
346
+ const server = await createRuntimeMcpServer(runtime, options);
347
+ const transport = new StreamableHTTPServerTransport({
348
+ sessionIdGenerator: () => randomUUID(),
349
+ onsessioninitialized: (initializedSessionId) => {
350
+ sessions.set(initializedSessionId, { transport, server });
351
+ },
352
+ });
353
+ createdTransport = transport;
354
+ transport.onclose = () => {
355
+ const activeSessionId = createdTransport?.sessionId;
356
+ if (activeSessionId) {
357
+ const active = sessions.get(activeSessionId);
358
+ if (active?.transport === createdTransport) {
359
+ sessions.delete(activeSessionId);
360
+ }
361
+ }
362
+ };
363
+ await server.connect(transport);
364
+ await transport.handleRequest(request, response, payload);
365
+ return;
366
+ }
367
+ if (request.method === "GET" || request.method === "DELETE") {
368
+ if (!sessionId || !sessions.has(sessionId)) {
369
+ writeJson(response, 400, {
370
+ jsonrpc: "2.0",
371
+ id: null,
372
+ error: {
373
+ code: -32000,
374
+ message: "Bad Request: No valid MCP session was provided.",
375
+ },
376
+ });
377
+ return;
378
+ }
379
+ const session = sessions.get(sessionId);
380
+ await session.transport.handleRequest(request, response);
381
+ if (request.method === "DELETE") {
382
+ await closeSession(sessionId);
383
+ }
384
+ return;
385
+ }
386
+ response.statusCode = 405;
387
+ response.setHeader("allow", "GET, POST, DELETE");
388
+ response.end();
389
+ }
390
+ catch (error) {
391
+ writeJson(response, 500, {
392
+ error: error instanceof Error ? error.message : "Runtime MCP Streamable HTTP transport failed.",
393
+ });
394
+ }
395
+ });
396
+ const completed = new Promise((resolve, reject) => {
397
+ httpServer.once("close", resolve);
398
+ httpServer.once("error", reject);
399
+ });
400
+ await new Promise((resolve, reject) => {
401
+ httpServer.listen(port, hostname, () => resolve());
402
+ httpServer.once("error", reject);
403
+ });
404
+ const address = httpServer.address();
405
+ const resolvedPort = typeof address === "object" && address ? address.port : port;
406
+ return {
407
+ hostname,
408
+ port: resolvedPort,
409
+ path: mcpPath,
410
+ url: `http://${hostname}:${resolvedPort}${mcpPath}`,
411
+ completed,
412
+ close: async () => {
413
+ const sessionIds = Array.from(sessions.keys());
414
+ for (const sessionId of sessionIds) {
415
+ await closeSession(sessionId);
416
+ }
417
+ await new Promise((resolve, reject) => {
418
+ httpServer.close((error) => {
419
+ if (error) {
420
+ reject(error);
421
+ return;
422
+ }
423
+ resolve();
424
+ });
425
+ });
426
+ },
427
+ };
428
+ }
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.307";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.308";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.307";
1
+ export const AGENT_HARNESS_VERSION = "0.0.308";
@@ -125,6 +125,7 @@ export declare class AgentHarnessRuntime {
125
125
  serveToolsOverStdio(options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
126
126
  createRuntimeMcpServer(options?: RuntimeMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
127
127
  serveRuntimeMcpOverStdio(options?: RuntimeMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
128
+ serveRuntimeMcpOverStreamableHttp(options?: import("../mcp.js").RuntimeMcpStreamableHttpServerOptions): Promise<import("../mcp.js").RuntimeMcpStreamableHttpServer>;
128
129
  routeAgent(input: MessageContent, options?: {
129
130
  sessionId?: string;
130
131
  }): Promise<string>;
@@ -1049,6 +1049,10 @@ export class AgentHarnessRuntime {
1049
1049
  async serveRuntimeMcpOverStdio(options = {}) {
1050
1050
  return serveRuntimeMcpOverStdio(this, options);
1051
1051
  }
1052
+ async serveRuntimeMcpOverStreamableHttp(options = {}) {
1053
+ const { serveRuntimeMcpOverStreamableHttp } = await import("../mcp.js");
1054
+ return serveRuntimeMcpOverStreamableHttp(this, options);
1055
+ }
1052
1056
  async routeAgent(input, options = {}) {
1053
1057
  return routeAgentId({
1054
1058
  workspace: this.workspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.308",
3
+ "version": "0.0.309",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",