@agentproto/runtime 5.13.0 → 5.14.0
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/dist/app-mcp-server.d.ts +69 -0
- package/dist/app-mcp-server.mjs +206 -0
- package/dist/app-mcp-server.mjs.map +1 -0
- package/dist/config-schema.mjs +12 -8
- package/dist/config-schema.mjs.map +1 -1
- package/dist/config.mjs +12 -8
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.ts +197 -19
- package/dist/index.mjs +1485 -327
- package/dist/index.mjs.map +1 -1
- package/dist/resume-strategies.mjs +12 -8
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/session-presence.mjs +12 -8
- package/dist/session-presence.mjs.map +1 -1
- package/docs/mcp-tools/agent_start.md +11 -0
- package/package.json +29 -24
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Publish ONE agentproto app as a standalone MCP App server: the app's own
|
|
6
|
+
* bundled tools (`.agentproto/tools/<id>/TOOL.md` run through its
|
|
7
|
+
* `.agentproto/drivers/*\/DRIVER.md` via AIP-30 `runTool`) plus its UI as a
|
|
8
|
+
* `ui://` resource. No daemon behind it and no other tool: unlike the daemon
|
|
9
|
+
* gateway (local-trust by design), this is the surface that may face the
|
|
10
|
+
* public internet, so it serves exactly the app's declared `ui.tools`, and
|
|
11
|
+
* refuses any of them that is not one of the app's own bundled tools.
|
|
12
|
+
*
|
|
13
|
+
* Multi-tenant: `startAppMcpHttp({ tenants })` serves `/mcp/<tenant>`, each
|
|
14
|
+
* tenant with its own resolved secrets (e.g. one shop's API key per tenant).
|
|
15
|
+
*
|
|
16
|
+
* NO BUILT-IN AUTHENTICATION. Anyone who can reach the port can call the
|
|
17
|
+
* served tools with the (tenant's) secrets. The tenant slug in the URL only
|
|
18
|
+
* SELECTS which secrets apply; it is not a credential and must not be treated
|
|
19
|
+
* as one. For anything beyond loopback, put a front proxy in front that
|
|
20
|
+
* terminates TLS and authenticates the caller (and maps caller -> tenant slug).
|
|
21
|
+
*
|
|
22
|
+
* Built-in hardening: Host/Origin are checked against an allowlist when bound
|
|
23
|
+
* to loopback (DNS-rebinding guard) or when `allowedHosts` is set, request
|
|
24
|
+
* bodies are capped (`maxBodyBytes`), and only POST is served.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
interface PublishedApp {
|
|
28
|
+
readonly appId: string;
|
|
29
|
+
readonly version: string;
|
|
30
|
+
/** The `ui://…/view` uri the rendering tools point at. */
|
|
31
|
+
readonly resourceUri: string;
|
|
32
|
+
/** The tool ids served (the app's `ui.tools`). */
|
|
33
|
+
readonly toolIds: readonly string[];
|
|
34
|
+
/** Secret names the app declares (`requirements.secrets`). */
|
|
35
|
+
readonly secretNames: readonly string[];
|
|
36
|
+
/** Build a fresh McpServer (the stateless HTTP transport is single-use,
|
|
37
|
+
* so every request gets its own server). */
|
|
38
|
+
build(secrets?: Record<string, string>): McpServer;
|
|
39
|
+
}
|
|
40
|
+
declare function loadPublishedApp(dir: string): Promise<PublishedApp>;
|
|
41
|
+
interface StartAppMcpHttpOptions {
|
|
42
|
+
app: PublishedApp;
|
|
43
|
+
/** 0 or omitted = auto-assign. */
|
|
44
|
+
port?: number;
|
|
45
|
+
/** Default 127.0.0.1. Put a TLS reverse proxy in front for public use. */
|
|
46
|
+
host?: string;
|
|
47
|
+
/** Single-tenant secrets, served at `/mcp`. Ignored when `tenants` is set. */
|
|
48
|
+
secrets?: Record<string, string>;
|
|
49
|
+
/** Multi-tenant: tenant slug → its secrets, served at `/mcp/<tenant>`. */
|
|
50
|
+
tenants?: Record<string, Record<string, string>>;
|
|
51
|
+
/**
|
|
52
|
+
* Hostnames (no port) accepted in the `Host` header and, when present, the
|
|
53
|
+
* `Origin` header. Defaults to the loopback names when bound to a loopback
|
|
54
|
+
* address; when bound elsewhere and unset, no Host/Origin check is applied
|
|
55
|
+
* (the front proxy owns that). DNS-rebinding guard.
|
|
56
|
+
*/
|
|
57
|
+
allowedHosts?: readonly string[];
|
|
58
|
+
/** Max request body in bytes (default 1 MiB). Larger → 413. */
|
|
59
|
+
maxBodyBytes?: number;
|
|
60
|
+
}
|
|
61
|
+
interface AppMcpHttpHandle {
|
|
62
|
+
readonly url: string;
|
|
63
|
+
readonly server: Server;
|
|
64
|
+
close(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
declare function isValidTenantSlug(slug: string): boolean;
|
|
67
|
+
declare function startAppMcpHttp(opts: StartAppMcpHttpOptions): Promise<AppMcpHttpHandle>;
|
|
68
|
+
|
|
69
|
+
export { type AppMcpHttpHandle, type PublishedApp, type StartAppMcpHttpOptions, isValidTenantSlug, loadPublishedApp, startAppMcpHttp };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { loadAppHandle, loadAppBundledTools } from '@agentproto/app-kit';
|
|
5
|
+
import { registerUiResource, toMcpTool } from '@agentproto/mcp-server';
|
|
6
|
+
import 'fs';
|
|
7
|
+
import 'fs/promises';
|
|
8
|
+
import 'zod';
|
|
9
|
+
import '@agentproto/app-client/runner-select';
|
|
10
|
+
import '@agentproto/app-client/display-mode';
|
|
11
|
+
import 'child_process';
|
|
12
|
+
import 'crypto';
|
|
13
|
+
import 'os';
|
|
14
|
+
import 'path';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
18
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// src/app-ui-apps.ts
|
|
23
|
+
function appUiToolId(appId) {
|
|
24
|
+
const slug = appId.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "_");
|
|
25
|
+
return `app_ui_${slug}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/app-mcp-server.ts
|
|
29
|
+
async function loadPublishedApp(dir) {
|
|
30
|
+
const handle = await loadAppHandle(dir);
|
|
31
|
+
if (!handle.id) throw new Error(`app at '${dir}' has no id`);
|
|
32
|
+
const appId = handle.id;
|
|
33
|
+
const ui = handle.ui;
|
|
34
|
+
if (!ui) throw new Error(`app "${appId}" has no ui block: nothing to publish as an MCP App`);
|
|
35
|
+
const toolIds = [...ui.tools ?? []];
|
|
36
|
+
if (toolIds.length === 0) {
|
|
37
|
+
throw new Error(`app "${appId}" declares no ui.tools: refusing to publish without an explicit tool allowlist`);
|
|
38
|
+
}
|
|
39
|
+
const { tools, drivers } = await loadAppBundledTools(dir);
|
|
40
|
+
const byId = new Map(tools.map((t) => [t.id, t]));
|
|
41
|
+
const missing = toolIds.filter((id) => !byId.has(id));
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`app "${appId}": ui.tools ${missing.map((m) => `'${m}'`).join(", ")} not bundled as .agentproto/tools/<id>/TOOL.md; standalone publishing serves only the app's own bundled tools (there is no daemon behind it)`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const declaredRenders = ui.renders;
|
|
48
|
+
const renders = new Set(declaredRenders ?? toolIds);
|
|
49
|
+
const resourceUri = `ui://${appUiToolId(appId)}/view`;
|
|
50
|
+
const version = handle.version ?? "0.0.0";
|
|
51
|
+
const csp = ui.csp ? {
|
|
52
|
+
...ui.csp.connectDomains ? { connectDomains: [...ui.csp.connectDomains] } : {},
|
|
53
|
+
...ui.csp.resourceDomains ? { resourceDomains: [...ui.csp.resourceDomains] } : {},
|
|
54
|
+
...ui.csp.frameDomains ? { frameDomains: [...ui.csp.frameDomains] } : {}
|
|
55
|
+
} : void 0;
|
|
56
|
+
return {
|
|
57
|
+
appId,
|
|
58
|
+
version,
|
|
59
|
+
resourceUri,
|
|
60
|
+
toolIds,
|
|
61
|
+
secretNames: [...handle.requirements.secrets],
|
|
62
|
+
build(secrets) {
|
|
63
|
+
const server = new McpServer({ name: appId, version });
|
|
64
|
+
registerUiResource(server, {
|
|
65
|
+
name: `${ui.title ?? handle.name ?? appId} UI`,
|
|
66
|
+
uri: resourceUri,
|
|
67
|
+
html: ui.html,
|
|
68
|
+
...ui.description !== void 0 ? { description: ui.description } : {},
|
|
69
|
+
...csp ? { csp } : {}
|
|
70
|
+
});
|
|
71
|
+
for (const id of toolIds) {
|
|
72
|
+
toMcpTool(server, {
|
|
73
|
+
tool: byId.get(id),
|
|
74
|
+
candidates: drivers,
|
|
75
|
+
...secrets ? { secrets } : {},
|
|
76
|
+
...renders.has(id) ? { ui: { resourceUri } } : {}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return server;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var TENANT_SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
84
|
+
var TENANT_PATH = /^\/mcp\/([a-z0-9][a-z0-9-]{0,62})$/;
|
|
85
|
+
var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];
|
|
86
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
87
|
+
function isValidTenantSlug(slug) {
|
|
88
|
+
return TENANT_SLUG.test(slug);
|
|
89
|
+
}
|
|
90
|
+
function isLoopbackBind(host) {
|
|
91
|
+
return host === "localhost" || host === "::1" || host === "[::1]" || /^127\./.test(host);
|
|
92
|
+
}
|
|
93
|
+
function hostnameOfHostHeader(value) {
|
|
94
|
+
try {
|
|
95
|
+
return new URL(`http://${value}`).hostname;
|
|
96
|
+
} catch {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
var BodyTooLargeError = class extends Error {
|
|
101
|
+
};
|
|
102
|
+
async function readJsonBody(req, limit) {
|
|
103
|
+
const declared = Number(req.headers["content-length"]);
|
|
104
|
+
if (Number.isFinite(declared) && declared > limit) throw new BodyTooLargeError();
|
|
105
|
+
const chunks = [];
|
|
106
|
+
let size = 0;
|
|
107
|
+
for await (const chunk of req) {
|
|
108
|
+
const buf = chunk;
|
|
109
|
+
size += buf.length;
|
|
110
|
+
if (size > limit) throw new BodyTooLargeError();
|
|
111
|
+
chunks.push(buf);
|
|
112
|
+
}
|
|
113
|
+
if (size === 0) return void 0;
|
|
114
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
115
|
+
}
|
|
116
|
+
async function startAppMcpHttp(opts) {
|
|
117
|
+
const host = opts.host ?? "127.0.0.1";
|
|
118
|
+
const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
119
|
+
const allowed = new Set(
|
|
120
|
+
(opts.allowedHosts ?? (isLoopbackBind(host) ? LOOPBACK_HOSTS : [])).map((h) => h.toLowerCase())
|
|
121
|
+
);
|
|
122
|
+
function hostAllowed(req) {
|
|
123
|
+
if (allowed.size === 0) return true;
|
|
124
|
+
const hostHeader = req.headers.host;
|
|
125
|
+
const hostname = hostHeader ? hostnameOfHostHeader(hostHeader) : void 0;
|
|
126
|
+
if (hostname === void 0 || !(allowed.has(hostname) || allowed.has(`[${hostname}]`))) return false;
|
|
127
|
+
const origin = req.headers.origin;
|
|
128
|
+
if (origin !== void 0) {
|
|
129
|
+
const originHost = hostnameOfHostHeader(origin.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ""));
|
|
130
|
+
if (originHost === void 0 || !(allowed.has(originHost) || allowed.has(`[${originHost}]`))) return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
async function handle(req, res) {
|
|
135
|
+
if (!hostAllowed(req)) {
|
|
136
|
+
res.writeHead(403, { "content-type": "text/plain" }).end("forbidden host or origin");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
140
|
+
let secrets;
|
|
141
|
+
if (opts.tenants) {
|
|
142
|
+
const m = TENANT_PATH.exec(path);
|
|
143
|
+
const tenant = m?.[1];
|
|
144
|
+
if (tenant === void 0 || !Object.hasOwn(opts.tenants, tenant)) {
|
|
145
|
+
res.writeHead(404, { "content-type": "text/plain" }).end("unknown tenant");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
secrets = opts.tenants[tenant];
|
|
149
|
+
} else {
|
|
150
|
+
if (path !== "/mcp") {
|
|
151
|
+
res.writeHead(404, { "content-type": "text/plain" }).end("not found");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
secrets = opts.secrets;
|
|
155
|
+
}
|
|
156
|
+
if (req.method !== "POST") {
|
|
157
|
+
res.writeHead(405, { "allow": "POST", "content-type": "text/plain" }).end("method not allowed");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let body;
|
|
161
|
+
try {
|
|
162
|
+
body = await readJsonBody(req, maxBody);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (err instanceof BodyTooLargeError) {
|
|
165
|
+
res.writeHead(413, { "content-type": "text/plain", connection: "close" }).end("request body too large");
|
|
166
|
+
} else {
|
|
167
|
+
res.writeHead(400, { "content-type": "text/plain" }).end("invalid JSON body");
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const server = opts.app.build(secrets);
|
|
172
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
173
|
+
res.on("close", () => {
|
|
174
|
+
void transport.close();
|
|
175
|
+
void server.close();
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
await server.connect(transport);
|
|
179
|
+
await transport.handleRequest(req, res, body);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (!res.headersSent) {
|
|
182
|
+
res.writeHead(500, { "content-type": "text/plain" }).end(err instanceof Error ? err.message : String(err));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const http = createServer((req, res) => {
|
|
187
|
+
void handle(req, res);
|
|
188
|
+
});
|
|
189
|
+
await new Promise((resolve2, reject) => {
|
|
190
|
+
http.once("error", reject);
|
|
191
|
+
http.listen(opts.port ?? 0, host, () => resolve2());
|
|
192
|
+
});
|
|
193
|
+
const { port } = http.address();
|
|
194
|
+
return {
|
|
195
|
+
url: `http://${host}:${port}`,
|
|
196
|
+
server: http,
|
|
197
|
+
close: () => new Promise((resolve2) => {
|
|
198
|
+
http.close(() => resolve2());
|
|
199
|
+
http.closeAllConnections();
|
|
200
|
+
})
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { isValidTenantSlug, loadPublishedApp, startAppMcpHttp };
|
|
205
|
+
//# sourceMappingURL=app-mcp-server.mjs.map
|
|
206
|
+
//# sourceMappingURL=app-mcp-server.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app-ui-apps.ts","../src/app-mcp-server.ts"],"names":["resolve"],"mappings":"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","file":"app-mcp-server.mjs","sourcesContent":["/**\n * Dynamic MCP-app panels for installed `@agentproto/app-kit` apps that ship\n * a `ui` block (`defineApp({ ui: { html, title?, tools?, csp? } })`,\n * `app-tools.ts`'s `performInstall`). Bridges `AppRegistry` → `AgnoMcpApp[]`\n * so `registerMcpApps` (mcp-apps-adapter.ts) can mount them next to the\n * built-in panels (agentproto_sessions, agents_overview, ...).\n *\n * `mcpServerFactory` (index.ts) rebuilds a fresh McpServer per `/mcp`\n * request, so this module never throws — a bad app record must not take\n * down every other request. A collided tool id is still skipped with a\n * `console.warn` (there's nowhere else for a second panel to live). Every\n * OTHER failure — a removed app dir, a missing/unreadable `ui.path`, a\n * failed or in-flight `ui.build` — still mounts the panel, serving a\n * readable HTML placeholder/error page in place of the real bundle\n * (app-ui-placeholder.ts) instead of skipping it outright: a `ui://`\n * resource that silently doesn't exist just shows as a JSON-RPC \"resource\n * not found\" in a host, which is what motivated this file's placeholder\n * page in the first place — a HOST-VISIBLE page, even a stand-in one, beats\n * a resource the host has to explain away as missing.\n */\n\nimport { existsSync } from \"node:fs\"\nimport { readFile, stat } from \"node:fs/promises\"\nimport { z } from \"zod\"\nimport { RUNNER_SELECT_SCRIPT } from \"@agentproto/app-client/runner-select\"\nimport { DISPLAY_MODE_SCRIPT } from \"@agentproto/app-client/display-mode\"\nimport type { AppRegistry } from \"./app-registry.js\"\nimport { resolveAppUiBuildState } from \"./app-ui-build.js\"\nimport { renderAppUiBuildingHtml, renderAppUiErrorHtml } from \"./app-ui-placeholder.js\"\nimport type { AgnoMcpApp } from \"@agentproto/apps\"\n\n/** Derive the MCP tool id for an installed app's UI panel — strips the\n * `@owner/` scope (if any) and maps every non `[a-z0-9]` character to `_`. */\nexport function appUiToolId(appId: string): string {\n const slug = appId.replace(/^@[^/]+\\//, \"\").replace(/[^a-z0-9]/g, \"_\")\n return `app_ui_${slug}`\n}\n\ninterface UiHtmlCache {\n get(path: string, version: string): Promise<string>\n}\n\n/**\n * The MCP Apps wire (spec `2026-01-26`) over `postMessage` JSON-RPC, exposed\n * as `window.McpApp.connect() -> Promise<{ callTool, sendMessage,\n * updateModelContext, openLink, onTeardown }>`. No host in this stack injects\n * an SDK global into the app iframe, so every UI panel that talks through\n * `window.McpApp` (all of them — see each app's `ui.ts` under\n * `packages/apps/src`) would otherwise throw `Cannot read properties of\n * undefined (reading 'connect')` at boot. Injected here, at serve time, so\n * every installed app's panel is covered without each app shipping its own\n * copy of the bridge.\n *\n * Bridge surface:\n * - `callTool(name, args)` — JSON-RPC `tools/call`\n * - `sendMessage(content)` — JSON-RPC `ui/message`\n * - `updateModelContext({content?, structuredContent?})` — JSON-RPC\n * `ui/update-model-context`; each call replaces the previous context.\n * - `openLink(url)` — JSON-RPC `ui/open-link` {url}\n * - `onTeardown(cb)` — register a cleanup callback invoked on\n * `ui/resource-teardown` from the host, at which point the bridge also\n * replies `{result:{}}` automatically.\n * - `getHostContext()` / `onHostContext(cb)` — the host context captured\n * from the `ui/initialize` result and merged on every\n * `ui/notifications/host-context-changed` (the notification carries only\n * the changed keys, per the ext-apps spec).\n * - `getToolInput()` / `onToolInput(cb)` — the arguments the panel's own tool\n * (`app_ui_<slug>`) was invoked with, from `ui/notifications/tool-input`,\n * falling back to the tool's own result (`ui/notifications/tool-result`,\n * which `registerMcpApps` fills with `execute()`'s return value) for a host\n * that forwards results but not inputs. A late subscriber is replayed the\n * last value, same as `onHostContext`. Lets an MCP-hosted panel resolve a\n * deep link (e.g. `{view: \"wallets/my-profile\"}`) the way the browser-served\n * panel resolves `location.hash`.\n * - `displayMode` — `{get, available, request, onChange, mountToggle}`, the\n * shared controller from `@agentproto/app-client/display-mode` (also\n * published as `window.McpApp.displayMode` once connected). It is what\n * mounts the floating ⤢ toggle; an app that wants the control in its own\n * header opts the floating one out (`<meta name=\"agentproto-display-toggle\"\n * content=\"none\">`, or `connect({displayToggle: \"none\"})`) and calls\n * `displayMode.mountToggle(el)` instead.\n */\nexport const MCP_APP_BRIDGE_SCRIPT = `<script>\n(function () {\n window.__AGENTPROTO_UI_TRANSPORT__ = \"mcp\";\n if (window.McpApp) return;\n var nextId = 1, pending = {}, teardownCbs = [];\n var hostContext = null, hostContextCbs = [];\n var toolInput = null, toolInputCbs = [];\n function post(m) { window.parent.postMessage(m, \"*\"); }\n function getHostContext() { return hostContext; }\n function onHostContext(cb) {\n hostContextCbs.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (hostContext) { try { cb(hostContext); } catch (_) {} }\n }\n function setHostContext(ctx) {\n if (!ctx || typeof ctx !== \"object\") return;\n // host-context-changed carries only the changed keys — merge, matching\n // the official ext-apps App behaviour.\n hostContext = Object.assign({}, hostContext || {}, ctx);\n for (var i = 0; i < hostContextCbs.length; i++) {\n try { hostContextCbs[i](hostContext); } catch (_) {}\n }\n }\n function getToolInput() { return toolInput; }\n function onToolInput(cb) {\n toolInputCbs.push(cb);\n // Replay the last known input so a late subscriber isn't stuck blind —\n // same convention as onHostContext.\n if (toolInput) { try { cb(toolInput); } catch (_) {} }\n }\n function setToolInput(input) {\n if (!input || typeof input !== \"object\") return;\n toolInput = input;\n for (var i = 0; i < toolInputCbs.length; i++) {\n try { toolInputCbs[i](toolInput); } catch (_) {}\n }\n }\n // A ui/notifications/tool-result carries the full CallToolResult of the\n // tool call that opened this view (spec 2026-01-26) — the same envelope\n // registerMcpApps wraps execute()'s return value in (JSON text content, or\n // structuredContent). Read as a fallback source of the tool's arguments\n // for a host that forwards results but not tool-input.\n function extractToolResultInput(result) {\n try {\n if (result && result.structuredContent && typeof result.structuredContent === \"object\") {\n return result.structuredContent;\n }\n var c = result && Array.isArray(result.content) ? result.content[0] : null;\n if (c && c.type === \"text\" && typeof c.text === \"string\") return JSON.parse(c.text);\n } catch (_) {}\n return null;\n }\n window.addEventListener(\"message\", function (e) {\n var m = e.data;\n if (typeof m === \"string\") { try { m = JSON.parse(m); } catch (_) { return; } }\n if (!m || m.jsonrpc !== \"2.0\") return;\n if (m.method === \"ui/resource-teardown\") {\n for (var i = 0; i < teardownCbs.length; i++) teardownCbs[i]();\n post({ jsonrpc: \"2.0\", id: m.id, result: {} });\n return;\n }\n if (m.method === \"ui/notifications/host-context-changed\") { setHostContext(m.params || {}); return; }\n if (m.method === \"ui/notifications/tool-input\") { setToolInput((m.params || {}).arguments); return; }\n if (m.method === \"ui/notifications/tool-result\") { setToolInput(extractToolResultInput(m.params)); return; }\n if (m.id != null && pending[m.id]) { var cb = pending[m.id]; delete pending[m.id]; cb(m.result, m.error); }\n });\n function request(method, params) {\n return new Promise(function (resolve, reject) {\n var id = nextId++;\n pending[id] = function (r, err) { if (err) reject(new Error((err && err.message) || String(err))); else resolve(r); };\n post({ jsonrpc: \"2.0\", id: id, method: method, params: params });\n });\n }\n function requestDisplayMode(mode) { return request(\"ui/request-display-mode\", { mode: mode }); }\n // The shared toggle (@agentproto/app-client/display-mode), injected right\n // after this script. Absent only when the app ships its own\n // window.AgentprotoUI namespace, which suppresses that injection — the\n // fallback keeps window.McpApp.displayMode callable either way.\n function installDisplayMode(opts) {\n var api = { getHostContext: getHostContext, onHostContext: onHostContext, requestDisplayMode: requestDisplayMode };\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) {\n return window.AgentprotoUI.installDisplayMode(api, opts);\n }\n return {\n get: function () { return (hostContext && hostContext.displayMode) || \"inline\"; },\n available: function () { return (hostContext && hostContext.availableDisplayModes) || []; },\n request: requestDisplayMode,\n onChange: function (cb) { onHostContext(function (c) { cb((c && c.displayMode) || \"inline\", c); }); return function () {}; },\n mountToggle: function () { return null; }\n };\n }\n window.McpApp = {\n connect: function (opts) {\n if (window.parent === window) return Promise.reject(new Error(\"no host (standalone)\"));\n return request(\"ui/initialize\", {\n appInfo: { name: \"agentproto-app\", version: \"1.0\" },\n appCapabilities: { availableDisplayModes: [\"inline\", \"fullscreen\"] },\n protocolVersion: \"2026-01-26\"\n }).then(function (result) {\n // The initialize result carries the initial hostContext (displayMode,\n // availableDisplayModes, theme, safeAreaInsets) — capture it before\n // notifying the host, so the toggle renders in the right place on\n // its first paint rather than after a host-context-changed.\n if (result && result.hostContext) setHostContext(result.hostContext);\n post({ jsonrpc: \"2.0\", method: \"ui/notifications/initialized\", params: {} });\n var displayMode = installDisplayMode((opts && opts.displayToggle) ? { toggle: opts.displayToggle } : null);\n window.McpApp.displayMode = displayMode;\n return {\n callTool: function (name, args) { return request(\"tools/call\", { name: name, arguments: args || {} }); },\n sendMessage: function (p) {\n var params = typeof p === \"string\" ? { content: p } : (p || {});\n var content = typeof params.content === \"string\" ? [{ type: \"text\", text: params.content }] : (params.content || []);\n return request(\"ui/message\", { role: \"user\", content: content });\n },\n updateModelContext: function (p) { return request(\"ui/update-model-context\", p || {}); },\n openLink: function (url) { return request(\"ui/open-link\", { url: url }); },\n onTeardown: function (cb) { teardownCbs.push(cb); },\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n getToolInput: getToolInput,\n onToolInput: onToolInput,\n displayMode: displayMode\n };\n });\n }\n };\n})();\n</script>\n`\n\n/** Insert `script` right after the earliest structural opening tag the\n * document has (`<head>`, else `<body>`, else `<html>`, else prepend), so\n * the injected bridge is defined before any app script runs. */\nfunction injectAfterStructuralTag(html: string, script: string): string {\n for (const tag of [\"head\", \"body\", \"html\"]) {\n const match = html.match(new RegExp(`<${tag}[^>]*>`, \"i\"))\n if (match?.index !== undefined) {\n const insertAt = match.index + match[0].length\n return html.slice(0, insertAt) + script + html.slice(insertAt)\n }\n }\n return script + html\n}\n\n/** Inject `MCP_APP_BRIDGE_SCRIPT` followed by the two\n * `window.AgentprotoUI` scripts (`DISPLAY_MODE_SCRIPT`, then\n * `RUNNER_SELECT_SCRIPT`) — every installed app UI gets the shared\n * display-mode toggle and `mountRunnerSelect` for free, same as it gets\n * `window.McpApp` (see `injectAfterStructuralTag` for the placement rule).\n * The halves are independently idempotent, so a single\n * `injectAfterStructuralTag` call inserts only whichever ones the document\n * doesn't already define — inserting them one call each would place the\n * second BEFORE the first (all target \"right after the same structural\n * tag\"), which is why they're concatenated first instead. `window.\n * AgentprotoUI` gates BOTH of its scripts as one unit: a document that\n * already defines that namespace brings its own, and the bridge's\n * `installDisplayMode` falls back to a controller with no button rather\n * than fighting it. The bridge guard matches an assignment only — every\n * app panel CONSUMES `window.McpApp.connect()`, so matching any mention\n * would skip injection for exactly the documents that need it, leaving\n * `window.McpApp` undefined in the host iframe. */\nexport function injectMcpAppBridge(html: string): string {\n const hasBridge = /window\\.McpApp\\s*=/.test(html)\n const hasUiNamespace = /window\\.AgentprotoUI\\s*=/.test(html)\n if (hasBridge && hasUiNamespace) return html\n const script =\n (hasBridge ? \"\" : MCP_APP_BRIDGE_SCRIPT) +\n (hasUiNamespace ? \"\" : DISPLAY_MODE_SCRIPT + RUNNER_SELECT_SCRIPT)\n return injectAfterStructuralTag(html, script)\n}\n\n/**\n * The standalone-browser-tab twin of `MCP_APP_BRIDGE_SCRIPT` — same\n * `window.McpApp.connect() -> Promise<{ callTool, sendMessage,\n * updateModelContext, openLink, onTeardown }>` surface, but `callTool` POSTs\n * to the sibling `./tool-call` route (http-server.ts's `POST\n * /apps/:appId/tool-call`) instead of JSON-RPC over `postMessage`, so\n * `GET /apps/:appId/ui` renders a working app with no host iframe at all.\n * `callTool` resolves with the route's body — the same MCP result envelope a\n * postMessage host's `tools/call` reply carries — so an app's unwrapping code\n * (see media-viewer's `unwrapText`) behaves identically in both modes. There\n * is no chat host to receive `ui/message`, so `sendMessage` rejects; there is\n * no context host either, so `updateModelContext` rejects; `openLink` opens a\n * new tab via `window.open`; `onTeardown` is a no-op.\n *\n * `displayMode` is the same shared controller the postMessage bridge builds\n * (`@agentproto/app-client/display-mode`), wired to a host context of\n * `{displayMode:\"inline\", availableDisplayModes:[]}` — an empty list is what\n * keeps the toggle hidden in a plain browser tab, where there is no host to\n * grant fullscreen, while leaving the API callable so an app doesn't have to\n * branch on which bridge it got.\n *\n * Injected INSTEAD OF (not on top of) the postMessage bridge: that bridge's\n * `connect()` rejects when `window.parent === window`, which is exactly the\n * standalone case this one exists for.\n */\nexport const STANDALONE_REST_BRIDGE_SCRIPT = `<script>\n(function () {\n window.__AGENTPROTO_UI_TRANSPORT__ = \"http\";\n if (window.McpApp) return;\n // No host in a standalone tab: inline, nothing else on offer. Fed to the\n // shared display-mode controller so its toggle stays hidden here.\n var hostContext = { displayMode: \"inline\", availableDisplayModes: [] };\n function installDisplayMode(opts) {\n var api = {\n getHostContext: function () { return hostContext; },\n onHostContext: function (cb) { try { cb(hostContext); } catch (_) {} },\n requestDisplayMode: function () {\n return Promise.reject(new Error(\"requestDisplayMode: no host (standalone mode)\"));\n }\n };\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) {\n return window.AgentprotoUI.installDisplayMode(api, opts);\n }\n return {\n get: function () { return \"inline\"; },\n available: function () { return []; },\n request: api.requestDisplayMode,\n onChange: function (cb) { try { cb(\"inline\", hostContext); } catch (_) {} return function () {}; },\n mountToggle: function () { return null; }\n };\n }\n window.McpApp = {\n connect: function (opts) {\n var displayMode = installDisplayMode((opts && opts.displayToggle) ? { toggle: opts.displayToggle } : null);\n window.McpApp.displayMode = displayMode;\n return Promise.resolve({\n callTool: function (name, args) {\n return fetch(\"./tool-call\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ tool: name, args: args || {} })\n }).then(function (res) {\n return res.json().catch(function () {\n throw new Error(\"tool-call failed: HTTP \" + res.status);\n }).then(function (body) {\n if (!res.ok) throw new Error((body && (body.message || body.error)) || (\"tool-call failed: HTTP \" + res.status));\n return body;\n });\n });\n },\n sendMessage: function () {\n return Promise.reject(new Error(\"sendMessage: no chat host (standalone mode)\"));\n },\n updateModelContext: function () {\n return Promise.reject(new Error(\"updateModelContext: no host (standalone mode)\"));\n },\n openLink: function (url) {\n window.open(url, \"_blank\");\n return Promise.resolve({});\n },\n onTeardown: function () {},\n getHostContext: function () { return hostContext; },\n onHostContext: function (cb) { try { cb(hostContext); } catch (_) {} },\n // No host to notify tool input in standalone mode — a browser tab\n // navigates via location.hash instead (see @agentproto/config's ui.ts).\n getToolInput: function () { return null; },\n onToolInput: function () {},\n displayMode: displayMode\n });\n }\n };\n})();\n</script>\n`\n\n/** Inject the standalone REST bridge followed by (idempotently)\n * `DISPLAY_MODE_SCRIPT` + `RUNNER_SELECT_SCRIPT` — same pairing\n * `injectMcpAppBridge` does, gated as one unit on the same\n * `window.AgentprotoUI` guard, in one\n * `injectAfterStructuralTag` call for the same reason (see its doc: two\n * separate calls targeting the same tag would place the second script\n * BEFORE the first). Meant for raw `ui.path` html — the bridge script\n * itself is always (re-)injected, unguarded at the html level, same as\n * before this change; the script's own `if (window.McpApp) return;` guard\n * is the runtime-level idempotency, not this function. */\nexport function injectStandaloneAppBridge(\n html: string,\n baseUrl?: string,\n): string {\n const hasUiNamespace = /window\\.AgentprotoUI\\s*=/.test(html)\n const baseUrlScript = baseUrl\n ? `<script>window.__AGENTPROTO_BASEURL__=${JSON.stringify(baseUrl).replace(/</g, \"\\\\u003c\")};</script>`\n : \"\"\n const script =\n baseUrlScript +\n STANDALONE_REST_BRIDGE_SCRIPT +\n (hasUiNamespace ? \"\" : DISPLAY_MODE_SCRIPT + RUNNER_SELECT_SCRIPT)\n return injectAfterStructuralTag(html, script)\n}\n\n/** Per-path HTML cache keyed on the file's own identity — a request rebuilds\n * the McpServer every time, but re-reading an installed app's `ui.path` off\n * disk on every `/mcp` call would be wasteful when nothing changed. The\n * cache lives at gateway scope (created once in index.ts, outside\n * `mcpServerFactory`) so it actually persists across requests.\n *\n * The `version` argument (the app's `updatedAt`) alone is NOT enough: it\n * only changes on `app_install`, not when the committed `ui/index.html`\n * bundle is rebuilt in place (e.g. a merged PR) — keying on it alone served\n * stale html until the app was reinstalled or the daemon restarted. Each\n * `get` additionally `stat`s the file and folds `mtimeMs:size` into the\n * cache key, so an in-place rewrite invalidates immediately with no\n * reinstall needed; `version` stays in the key too since a stat is already\n * paid for and it still catches the reinstall case for free. A stat per\n * request is cheap compared to the read + bridge injection it usually\n * saves. The cached value is post-bridge-injection, so injection only runs\n * once per (path, stamp) rather than on every served request. */\nexport function createUiHtmlCache(): UiHtmlCache {\n const cache = new Map<string, { stamp: string; html: string }>()\n return {\n async get(path, version) {\n const st = await stat(path)\n const stamp = `${version}:${st.mtimeMs}:${st.size}`\n const cached = cache.get(path)\n if (cached && cached.stamp === stamp) return cached.html\n const raw = await readFile(path, \"utf8\")\n const html = injectMcpAppBridge(raw)\n cache.set(path, { stamp, html })\n return html\n },\n }\n}\n\n/**\n * Build one `AgnoMcpApp` per installed app that has a `ui` block. The only\n * case still SKIPPED (with a `console.warn`) is a derived tool id colliding\n * with `existingToolNames` (built-in panels) or an earlier installed app in\n * this same pass — there's nowhere else for a second panel with the same id\n * to live. Every other failure (removed app dir, missing/unreadable\n * `ui.path`, a failed or still-running `ui.build`) still mounts the panel,\n * with its `html` swapped for a placeholder/error page — see the module\n * doc's rationale and app-ui-placeholder.ts.\n */\nexport async function makeInstalledAppUiApps(\n appRegistry: AppRegistry,\n cache: UiHtmlCache,\n existingToolNames: Set<string>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): Promise<AgnoMcpApp<any, any>[]> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const apps: AgnoMcpApp<any, any>[] = []\n const seen = new Set<string>(existingToolNames)\n\n for (const app of appRegistry.listApps()) {\n const ui = app.ui\n if (!ui) continue\n\n const toolId = appUiToolId(app.appId)\n if (seen.has(toolId)) {\n console.warn(\n `[app-ui-apps] skipping UI panel for app \"${app.appId}\": tool id \"${toolId}\" ` +\n \"collides with an existing tool or another installed app's panel.\",\n )\n continue\n }\n\n const appName = app.name ?? app.appId\n const html = !existsSync(app.dir)\n ? renderAppUiErrorHtml({\n appName,\n message: `The app's directory \"${app.dir}\" no longer exists.`,\n detailPath: app.dir,\n fix: `Reinstall with \\`app_install <dir>\\`, or remove the stale install.`,\n })\n : await (async () => {\n const state = await resolveAppUiBuildState({ dir: app.dir, uiPath: ui.path, build: ui.build })\n if (state.kind === \"building\") {\n return renderAppUiBuildingHtml({ appName, startedAt: state.startedAt, logTail: state.logTail })\n }\n if (state.kind === \"error\") {\n return renderAppUiErrorHtml({\n appName,\n message: state.message,\n detailPath: state.logPath,\n logTail: state.logTail,\n })\n }\n try {\n return await cache.get(ui.path, app.updatedAt)\n } catch (err) {\n return renderAppUiErrorHtml({\n appName,\n message: `Could not read the built ui html at \"${ui.path}\".`,\n detailPath: ui.path,\n logTail: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n\n seen.add(toolId)\n apps.push({\n id: toolId,\n title: ui.title ?? app.name ?? app.appId,\n ...(ui.description ? { description: ui.description } : {}),\n inputSchema: z.object({\n view: z\n .string()\n .max(200)\n .optional()\n .describe(\n \"Deep-link fragment to open the panel at (e.g. \\\"wallets/my-profile\\\"), for an \" +\n \"MCP host that cannot express a URL hash. Ignored by the browser-served panel, \" +\n \"which reads location.hash instead.\",\n ),\n }),\n execute: async (args) => ({ appId: app.appId, tools: ui.tools ?? [], view: args?.view }),\n html,\n ...(ui.csp\n ? {\n csp: {\n ...(ui.csp.connectDomains ? { connectDomains: [...ui.csp.connectDomains] } : {}),\n ...(ui.csp.resourceDomains ? { resourceDomains: [...ui.csp.resourceDomains] } : {}),\n ...(ui.csp.frameDomains ? { frameDomains: [...ui.csp.frameDomains] } : {}),\n },\n }\n : {}),\n })\n }\n\n return apps\n}\n","/**\n * Publish ONE agentproto app as a standalone MCP App server: the app's own\n * bundled tools (`.agentproto/tools/<id>/TOOL.md` run through its\n * `.agentproto/drivers/*\\/DRIVER.md` via AIP-30 `runTool`) plus its UI as a\n * `ui://` resource. No daemon behind it and no other tool: unlike the daemon\n * gateway (local-trust by design), this is the surface that may face the\n * public internet, so it serves exactly the app's declared `ui.tools`, and\n * refuses any of them that is not one of the app's own bundled tools.\n *\n * Multi-tenant: `startAppMcpHttp({ tenants })` serves `/mcp/<tenant>`, each\n * tenant with its own resolved secrets (e.g. one shop's API key per tenant).\n *\n * NO BUILT-IN AUTHENTICATION. Anyone who can reach the port can call the\n * served tools with the (tenant's) secrets. The tenant slug in the URL only\n * SELECTS which secrets apply; it is not a credential and must not be treated\n * as one. For anything beyond loopback, put a front proxy in front that\n * terminates TLS and authenticates the caller (and maps caller -> tenant slug).\n *\n * Built-in hardening: Host/Origin are checked against an allowlist when bound\n * to loopback (DNS-rebinding guard) or when `allowedHosts` is set, request\n * bodies are capped (`maxBodyBytes`), and only POST is served.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\"\nimport type { AddressInfo } from \"node:net\"\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { loadAppBundledTools, loadAppHandle } from \"@agentproto/app-kit\"\nimport { registerUiResource, toMcpTool } from \"@agentproto/mcp-server\"\nimport { appUiToolId } from \"./app-ui-apps.js\"\n\nexport interface PublishedApp {\n readonly appId: string\n readonly version: string\n /** The `ui://…/view` uri the rendering tools point at. */\n readonly resourceUri: string\n /** The tool ids served (the app's `ui.tools`). */\n readonly toolIds: readonly string[]\n /** Secret names the app declares (`requirements.secrets`). */\n readonly secretNames: readonly string[]\n /** Build a fresh McpServer (the stateless HTTP transport is single-use,\n * so every request gets its own server). */\n build(secrets?: Record<string, string>): McpServer\n}\n\nexport async function loadPublishedApp(dir: string): Promise<PublishedApp> {\n const handle = await loadAppHandle(dir)\n if (!handle.id) throw new Error(`app at '${dir}' has no id`)\n const appId = handle.id\n const ui = handle.ui\n if (!ui) throw new Error(`app \"${appId}\" has no ui block: nothing to publish as an MCP App`)\n const toolIds = [...(ui.tools ?? [])]\n if (toolIds.length === 0) {\n throw new Error(`app \"${appId}\" declares no ui.tools: refusing to publish without an explicit tool allowlist`)\n }\n const { tools, drivers } = await loadAppBundledTools(dir)\n const byId = new Map(tools.map(t => [t.id, t]))\n const missing = toolIds.filter(id => !byId.has(id))\n if (missing.length > 0) {\n throw new Error(\n `app \"${appId}\": ui.tools ${missing.map(m => `'${m}'`).join(\", \")} not bundled as .agentproto/tools/<id>/TOOL.md; ` +\n \"standalone publishing serves only the app's own bundled tools (there is no daemon behind it)\",\n )\n }\n // `ui.renders` (G3) narrows which tools open the UI; absent = all of them.\n const declaredRenders = (ui as { readonly renders?: readonly string[] }).renders\n const renders = new Set(declaredRenders ?? toolIds)\n const resourceUri = `ui://${appUiToolId(appId)}/view`\n const version = handle.version ?? \"0.0.0\"\n const csp = ui.csp\n ? {\n ...(ui.csp.connectDomains ? { connectDomains: [...ui.csp.connectDomains] } : {}),\n ...(ui.csp.resourceDomains ? { resourceDomains: [...ui.csp.resourceDomains] } : {}),\n ...(ui.csp.frameDomains ? { frameDomains: [...ui.csp.frameDomains] } : {}),\n }\n : undefined\n\n return {\n appId,\n version,\n resourceUri,\n toolIds,\n secretNames: [...handle.requirements.secrets],\n build(secrets) {\n const server = new McpServer({ name: appId, version })\n registerUiResource(server, {\n name: `${ui.title ?? handle.name ?? appId} UI`,\n uri: resourceUri,\n html: ui.html,\n ...(ui.description !== undefined ? { description: ui.description } : {}),\n ...(csp ? { csp } : {}),\n })\n for (const id of toolIds) {\n toMcpTool(server, {\n tool: byId.get(id)!,\n candidates: drivers,\n ...(secrets ? { secrets } : {}),\n ...(renders.has(id) ? { ui: { resourceUri } } : {}),\n })\n }\n return server\n },\n }\n}\n\nexport interface StartAppMcpHttpOptions {\n app: PublishedApp\n /** 0 or omitted = auto-assign. */\n port?: number\n /** Default 127.0.0.1. Put a TLS reverse proxy in front for public use. */\n host?: string\n /** Single-tenant secrets, served at `/mcp`. Ignored when `tenants` is set. */\n secrets?: Record<string, string>\n /** Multi-tenant: tenant slug → its secrets, served at `/mcp/<tenant>`. */\n tenants?: Record<string, Record<string, string>>\n /**\n * Hostnames (no port) accepted in the `Host` header and, when present, the\n * `Origin` header. Defaults to the loopback names when bound to a loopback\n * address; when bound elsewhere and unset, no Host/Origin check is applied\n * (the front proxy owns that). DNS-rebinding guard.\n */\n allowedHosts?: readonly string[]\n /** Max request body in bytes (default 1 MiB). Larger → 413. */\n maxBodyBytes?: number\n}\n\nexport interface AppMcpHttpHandle {\n readonly url: string\n readonly server: Server\n close(): Promise<void>\n}\n\nconst TENANT_SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/\nconst TENANT_PATH = /^\\/mcp\\/([a-z0-9][a-z0-9-]{0,62})$/\nconst LOOPBACK_HOSTS = [\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]\nconst DEFAULT_MAX_BODY_BYTES = 1024 * 1024\n\nexport function isValidTenantSlug(slug: string): boolean {\n return TENANT_SLUG.test(slug)\n}\n\nfunction isLoopbackBind(host: string): boolean {\n return host === \"localhost\" || host === \"::1\" || host === \"[::1]\" || /^127\\./.test(host)\n}\n\nfunction hostnameOfHostHeader(value: string): string | undefined {\n try {\n return new URL(`http://${value}`).hostname\n } catch {\n return undefined\n }\n}\n\nclass BodyTooLargeError extends Error {}\n\nasync function readJsonBody(req: IncomingMessage, limit: number): Promise<unknown> {\n const declared = Number(req.headers[\"content-length\"])\n if (Number.isFinite(declared) && declared > limit) throw new BodyTooLargeError()\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buf = chunk as Buffer\n size += buf.length\n if (size > limit) throw new BodyTooLargeError()\n chunks.push(buf)\n }\n if (size === 0) return undefined\n return JSON.parse(Buffer.concat(chunks).toString(\"utf8\"))\n}\n\nexport async function startAppMcpHttp(opts: StartAppMcpHttpOptions): Promise<AppMcpHttpHandle> {\n const host = opts.host ?? \"127.0.0.1\"\n const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES\n const allowed = new Set(\n (opts.allowedHosts ?? (isLoopbackBind(host) ? LOOPBACK_HOSTS : [])).map(h => h.toLowerCase()),\n )\n\n function hostAllowed(req: IncomingMessage): boolean {\n if (allowed.size === 0) return true\n const hostHeader = req.headers.host\n const hostname = hostHeader ? hostnameOfHostHeader(hostHeader) : undefined\n if (hostname === undefined || !(allowed.has(hostname) || allowed.has(`[${hostname}]`))) return false\n const origin = req.headers.origin\n if (origin !== undefined) {\n const originHost = hostnameOfHostHeader(origin.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, \"\"))\n if (originHost === undefined || !(allowed.has(originHost) || allowed.has(`[${originHost}]`))) return false\n }\n return true\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n if (!hostAllowed(req)) {\n res.writeHead(403, { \"content-type\": \"text/plain\" }).end(\"forbidden host or origin\")\n return\n }\n const path = (req.url ?? \"/\").split(\"?\")[0] ?? \"/\"\n let secrets: Record<string, string> | undefined\n if (opts.tenants) {\n const m = TENANT_PATH.exec(path)\n const tenant = m?.[1]\n if (tenant === undefined || !Object.hasOwn(opts.tenants, tenant)) {\n res.writeHead(404, { \"content-type\": \"text/plain\" }).end(\"unknown tenant\")\n return\n }\n secrets = opts.tenants[tenant]\n } else {\n if (path !== \"/mcp\") {\n res.writeHead(404, { \"content-type\": \"text/plain\" }).end(\"not found\")\n return\n }\n secrets = opts.secrets\n }\n if (req.method !== \"POST\") {\n res.writeHead(405, { \"allow\": \"POST\", \"content-type\": \"text/plain\" }).end(\"method not allowed\")\n return\n }\n let body: unknown\n try {\n body = await readJsonBody(req, maxBody)\n } catch (err) {\n if (err instanceof BodyTooLargeError) {\n res.writeHead(413, { \"content-type\": \"text/plain\", connection: \"close\" }).end(\"request body too large\")\n } else {\n res.writeHead(400, { \"content-type\": \"text/plain\" }).end(\"invalid JSON body\")\n }\n return\n }\n const server = opts.app.build(secrets)\n const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })\n res.on(\"close\", () => {\n void transport.close()\n void server.close()\n })\n try {\n await server.connect(transport)\n await transport.handleRequest(req, res, body)\n } catch (err) {\n if (!res.headersSent) {\n res.writeHead(500, { \"content-type\": \"text/plain\" }).end(err instanceof Error ? err.message : String(err))\n }\n }\n }\n\n const http = createServer((req, res) => {\n void handle(req, res)\n })\n await new Promise<void>((resolve, reject) => {\n http.once(\"error\", reject)\n http.listen(opts.port ?? 0, host, () => resolve())\n })\n const { port } = http.address() as AddressInfo\n return {\n url: `http://${host}:${port}`,\n server: http,\n close: () =>\n new Promise<void>(resolve => {\n http.close(() => resolve())\n http.closeAllConnections()\n }),\n }\n}\n"]}
|
package/dist/config-schema.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadStream, promises } from 'fs';
|
|
1
|
+
import { createReadStream, promises, existsSync } from 'fs';
|
|
2
2
|
import 'crypto';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { join, resolve } from 'path';
|
|
@@ -901,7 +901,11 @@ async function readFirstJsonLine(path) {
|
|
|
901
901
|
function codexHome() {
|
|
902
902
|
return process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
903
903
|
}
|
|
904
|
-
function codexSessionsDir() {
|
|
904
|
+
function codexSessionsDir(configDir) {
|
|
905
|
+
if (configDir) {
|
|
906
|
+
const isolated = join(configDir, "sessions");
|
|
907
|
+
if (existsSync(isolated)) return isolated;
|
|
908
|
+
}
|
|
905
909
|
return join(codexHome(), "sessions");
|
|
906
910
|
}
|
|
907
911
|
async function walkCodexRollouts(dir) {
|
|
@@ -936,8 +940,8 @@ function codexBlocksText(content) {
|
|
|
936
940
|
}
|
|
937
941
|
return acc;
|
|
938
942
|
}
|
|
939
|
-
async function findCodexRolloutFile(conversationId) {
|
|
940
|
-
const files = await walkCodexRollouts(codexSessionsDir());
|
|
943
|
+
async function findCodexRolloutFile(conversationId, configDir) {
|
|
944
|
+
const files = await walkCodexRollouts(codexSessionsDir(configDir));
|
|
941
945
|
const byName = files.find((f) => f.endsWith(`-${conversationId}.jsonl`));
|
|
942
946
|
if (byName) return byName;
|
|
943
947
|
for (const f of files) {
|
|
@@ -954,11 +958,11 @@ async function findCodexRolloutFile(conversationId) {
|
|
|
954
958
|
}
|
|
955
959
|
return void 0;
|
|
956
960
|
}
|
|
957
|
-
async function exportCodexSession(conversationId) {
|
|
958
|
-
const file = await findCodexRolloutFile(conversationId);
|
|
961
|
+
async function exportCodexSession(conversationId, configDir) {
|
|
962
|
+
const file = await findCodexRolloutFile(conversationId, configDir);
|
|
959
963
|
if (!file) {
|
|
960
964
|
throw new Error(
|
|
961
|
-
`codex: no rollout file for conversation "${conversationId}" under ${codexSessionsDir()}.
|
|
965
|
+
`codex: no rollout file for conversation "${conversationId}" under ${codexSessionsDir(configDir)}.
|
|
962
966
|
The session may predate persistence, or CODEX_HOME points elsewhere.`
|
|
963
967
|
);
|
|
964
968
|
}
|
|
@@ -1608,7 +1612,7 @@ var init_transcript_export = __esm({
|
|
|
1608
1612
|
exportSession: (id) => exportHermesSession(id)
|
|
1609
1613
|
},
|
|
1610
1614
|
codex: {
|
|
1611
|
-
exportSession: (id) => exportCodexSession(id)
|
|
1615
|
+
exportSession: (id, _cwd, configDir) => exportCodexSession(id, configDir)
|
|
1612
1616
|
},
|
|
1613
1617
|
opencode: {
|
|
1614
1618
|
exportSession: (id) => exportOpenCodeSession(id)
|