@agimon-ai/po-mcp 0.0.0-alpha.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/apiContracts.cjs +269 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +137 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +258 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/extensions/mcp-ui.mjs +31 -0
  10. package/dist/extensions/mcp-ui.mjs.map +1 -0
  11. package/dist/extensions/mcp.cjs +104 -0
  12. package/dist/extensions/mcp.cjs.map +1 -0
  13. package/dist/extensions/mcp.d.mts +5 -0
  14. package/dist/extensions/mcp.d.mts.map +1 -0
  15. package/dist/extensions/mcp.mjs +99 -0
  16. package/dist/extensions/mcp.mjs.map +1 -0
  17. package/dist/extensions/server.cjs +2852 -0
  18. package/dist/extensions/server.cjs.map +1 -0
  19. package/dist/extensions/server.d.mts +5 -0
  20. package/dist/extensions/server.d.mts.map +1 -0
  21. package/dist/extensions/server.mjs +2839 -0
  22. package/dist/extensions/server.mjs.map +1 -0
  23. package/dist/extensions/web.mjs +2200 -0
  24. package/dist/extensions/web.mjs.map +1 -0
  25. package/dist/index.cjs +18 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.d.mts +89 -0
  28. package/dist/index.d.mts.map +1 -0
  29. package/dist/index.mjs +9 -0
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/mcpCatalog-6Tpt6Gqn.cjs +900 -0
  32. package/dist/mcpCatalog-6Tpt6Gqn.cjs.map +1 -0
  33. package/dist/mcpCatalog-SlqWS-sb.mjs +806 -0
  34. package/dist/mcpCatalog-SlqWS-sb.mjs.map +1 -0
  35. package/dist/mcpConfig-BYESy-zP.d.mts +57 -0
  36. package/dist/mcpConfig-BYESy-zP.d.mts.map +1 -0
  37. package/dist/projection-gyAA2jRB.mjs +43 -0
  38. package/dist/projection-gyAA2jRB.mjs.map +1 -0
  39. package/dist/projection-w3XxXmVi.cjs +51 -0
  40. package/dist/projection-w3XxXmVi.cjs.map +1 -0
  41. package/dist/projection.cjs +17 -0
  42. package/dist/projection.d.mts +12 -0
  43. package/dist/projection.d.mts.map +1 -0
  44. package/dist/projection.mjs +4 -0
  45. package/dist/rolldown-runtime-C0BPl7ul.cjs +43 -0
  46. package/llms.txt +9 -0
  47. package/package.json +157 -0
  48. package/src/prompts/po-use-mcp/SKILL.md +79 -0
@@ -0,0 +1,2852 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ const require_rolldown_runtime = require("../rolldown-runtime-C0BPl7ul.cjs");
6
+ const require_mcpCatalog = require("../mcpCatalog-6Tpt6Gqn.cjs");
7
+ const require_projection = require("../projection-w3XxXmVi.cjs");
8
+ let node_crypto = require("node:crypto");
9
+ let node_fs = require("node:fs");
10
+ node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
11
+ let node_path = require("node:path");
12
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
13
+ let node_os = require("node:os");
14
+ node_os = require_rolldown_runtime.__toESM(node_os, 1);
15
+ let _agimon_ai_po_core_mcpProjection = require("@agimon-ai/po-core/mcpProjection");
16
+ let _agimon_ai_po_core_serverFacet = require("@agimon-ai/po-core/serverFacet");
17
+ let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
18
+ let _agimon_ai_po_core_childSession = require("@agimon-ai/po-core/childSession");
19
+ let _agimon_ai_po_core_headless = require("@agimon-ai/po-core/headless");
20
+ let _agimon_ai_po_core_mcpStatus = require("@agimon-ai/po-core/mcpStatus");
21
+ let _agimon_ai_po_core_mcpToolResolver = require("@agimon-ai/po-core/mcpToolResolver");
22
+ let typebox_value = require("typebox/value");
23
+ let typebox = require("typebox");
24
+ let node_dns_promises = require("node:dns/promises");
25
+ node_dns_promises = require_rolldown_runtime.__toESM(node_dns_promises, 1);
26
+ let node_https = require("node:https");
27
+ node_https = require_rolldown_runtime.__toESM(node_https, 1);
28
+ let node_net = require("node:net");
29
+ node_net = require_rolldown_runtime.__toESM(node_net, 1);
30
+ let node_fs_promises = require("node:fs/promises");
31
+ node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises, 1);
32
+ let _agimon_ai_po_core_pluginProtocol = require("@agimon-ai/po-core/pluginProtocol");
33
+ let _agimon_ai_po_config_harnessStore = require("@agimon-ai/po-config/harnessStore");
34
+ let _agimon_ai_po_core_contextApi = require("@agimon-ai/po-core/contextApi");
35
+ let _agimon_ai_mcp_proxy = require("@agimon-ai/mcp-proxy");
36
+ let _agimon_ai_po_core_web = require("@agimon-ai/po-core/web");
37
+ let _agimon_ai_po_core_help = require("@agimon-ai/po-core/help");
38
+ //#region src/schemas/fileTransfer.ts
39
+ const ShareFileParameters = typebox.Type.Object({ path: typebox.Type.String({ minLength: 1 }) }, { additionalProperties: false });
40
+ const SaveFileParameters = typebox.Type.Object({
41
+ file: typebox.Type.Union([typebox.Type.Object({
42
+ download_url: typebox.Type.String(),
43
+ file_name: typebox.Type.String(),
44
+ file_id: typebox.Type.Optional(typebox.Type.String()),
45
+ mime_type: typebox.Type.Optional(typebox.Type.String())
46
+ }, { additionalProperties: false }), typebox.Type.Object({
47
+ name: typebox.Type.String(),
48
+ data_base64: typebox.Type.String()
49
+ }, { additionalProperties: false })]),
50
+ path: typebox.Type.Optional(typebox.Type.String())
51
+ }, { additionalProperties: false });
52
+ //#endregion
53
+ //#region src/services/fileTransfer/download.ts
54
+ const MAX_BYTES = 26214400;
55
+ /** Conservative public-address allowlist. IPv6 is limited to global unicast, excluding special ranges. */
56
+ function isPublicAddress(address) {
57
+ if (node_net.default.isIP(address) === 4) {
58
+ const [a, b] = address.split(".").map(Number);
59
+ return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && (b === 168 || b === 0 || b === 2) || a === 198 && (b === 18 || b === 19 || b === 51) || a === 203 && b === 0);
60
+ }
61
+ return node_net.default.isIP(address) === 6 && /^[23]/u.test(address) && !/^(2001:|2002:|3fff:)/iu.test(address);
62
+ }
63
+ async function downloadPublicFile(input, cancellation) {
64
+ const signal = AbortSignal.any([AbortSignal.timeout(3e4), ...cancellation ? [cancellation] : []]);
65
+ let url = new URL(input);
66
+ for (let redirects = 0;; redirects += 1) {
67
+ signal.throwIfAborted();
68
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("Only public HTTPS URLs without credentials are accepted.");
69
+ const hostname = url.hostname.replace(/^\[|\]$/gu, "");
70
+ const addresses = node_net.default.isIP(hostname) ? [{
71
+ address: hostname,
72
+ family: node_net.default.isIP(hostname)
73
+ }] : await new Promise((resolve, reject) => {
74
+ const abort = () => reject(signal.reason);
75
+ signal.addEventListener("abort", abort, { once: true });
76
+ node_dns_promises.default.lookup(hostname, { all: true }).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
77
+ if (signal.aborted) abort();
78
+ });
79
+ signal.throwIfAborted();
80
+ if (!addresses.length || addresses.some(({ address }) => !isPublicAddress(address))) throw new Error("URL must resolve only to public addresses.");
81
+ const pinned = addresses[0];
82
+ const result = await new Promise((resolve, reject) => {
83
+ node_https.default.get(url, {
84
+ signal,
85
+ agent: false,
86
+ lookup: (_host, options, callback) => {
87
+ if (typeof options === "object" && options.all) callback(null, [pinned]);
88
+ else callback(null, pinned.address, pinned.family);
89
+ }
90
+ }, (response) => {
91
+ const status = response.statusCode ?? 0;
92
+ if ([
93
+ 301,
94
+ 302,
95
+ 303,
96
+ 307,
97
+ 308
98
+ ].includes(status)) {
99
+ response.destroy();
100
+ if (redirects >= 3 || !response.headers.location) reject(/* @__PURE__ */ new Error("Too many redirects or missing redirect location."));
101
+ else try {
102
+ resolve(new URL(response.headers.location, url));
103
+ } catch (error) {
104
+ reject(error);
105
+ }
106
+ return;
107
+ }
108
+ if (status !== 200 || Number(response.headers["content-length"]) > MAX_BYTES) {
109
+ response.destroy();
110
+ reject(/* @__PURE__ */ new Error("Download refused: status or size limit."));
111
+ return;
112
+ }
113
+ const chunks = [];
114
+ let size = 0;
115
+ response.on("data", (chunk) => {
116
+ size += chunk.length;
117
+ if (size > MAX_BYTES) response.destroy(/* @__PURE__ */ new Error("Download exceeds 25 MiB."));
118
+ else chunks.push(chunk);
119
+ });
120
+ response.on("error", reject);
121
+ response.on("aborted", () => reject(/* @__PURE__ */ new Error("Download response was interrupted.")));
122
+ response.on("close", () => {
123
+ if (!response.complete) reject(/* @__PURE__ */ new Error("Download response was interrupted."));
124
+ });
125
+ response.on("end", () => resolve(Buffer.concat(chunks, size)));
126
+ }).on("error", reject);
127
+ });
128
+ if (Buffer.isBuffer(result)) return result;
129
+ url = result;
130
+ }
131
+ }
132
+ function decodeFileBase64(data) {
133
+ if (data.length > 4 * Math.ceil(5242880 / 3) || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(data)) throw new Error("Invalid or oversized base64 file.");
134
+ const size = data.length / 4 * 3 - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
135
+ if (size < 0 || data.includes("=") && data.length < 4) throw new Error("Invalid base64 padding.");
136
+ if (size > 5242880) throw new Error("Base64 file exceeds 5 MiB.");
137
+ return Buffer.from(data, "base64");
138
+ }
139
+ //#endregion
140
+ //#region src/services/fileTransfer/storage.ts
141
+ /** Existing parents only, pinned by identity before and after exclusive creation. */
142
+ async function writeCwdFile(cwd, relative, bytes) {
143
+ if (!relative || node_path.default.isAbsolute(relative) || relative.includes("\\") || relative.includes("\0") || relative.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("Path must be relative without traversal.");
144
+ const root = await node_fs_promises.default.realpath(cwd);
145
+ const target = node_path.default.join(root, relative);
146
+ const parents = [root];
147
+ for (const segment of relative.split("/").slice(0, -1)) parents.push(node_path.default.join(parents.at(-1), segment));
148
+ const identities = await Promise.all(parents.map(async (parent) => {
149
+ const stat = await node_fs_promises.default.lstat(parent);
150
+ if (!stat.isDirectory() || stat.isSymbolicLink() || await node_fs_promises.default.realpath(parent) !== parent) throw new Error("Unsafe parent directory.");
151
+ return stat;
152
+ }));
153
+ const check = async () => {
154
+ for (let i = 0; i < parents.length; i += 1) {
155
+ const stat = await node_fs_promises.default.lstat(parents[i]);
156
+ if (stat.dev !== identities[i].dev || stat.ino !== identities[i].ino || await node_fs_promises.default.realpath(parents[i]) !== parents[i]) throw new Error("Parent directory changed.");
157
+ }
158
+ };
159
+ await check();
160
+ const handle = await node_fs_promises.default.open(target, node_fs.constants.O_WRONLY | node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_NOFOLLOW, 384);
161
+ try {
162
+ const identity = await handle.stat();
163
+ const checkFile = async () => {
164
+ const named = await node_fs_promises.default.lstat(target);
165
+ if (named.dev !== identity.dev || named.ino !== identity.ino || await node_fs_promises.default.realpath(target) !== target) throw new Error("File identity changed.");
166
+ };
167
+ await check();
168
+ await checkFile();
169
+ await handle.writeFile(bytes);
170
+ await check();
171
+ await checkFile();
172
+ return target;
173
+ } catch (error) {
174
+ await handle.truncate(0);
175
+ throw error;
176
+ } finally {
177
+ await handle.close();
178
+ }
179
+ }
180
+ //#endregion
181
+ //#region src/services/fileTransfer/index.ts
182
+ function createFileTransferTools(owner) {
183
+ return [{
184
+ name: "share_file",
185
+ description: "Publish a session file as a short-lived public download link. No consent prompt.",
186
+ parameters: ShareFileParameters,
187
+ _meta: { "doompi/fileTransfer": true },
188
+ async execute(_id, input, signal) {
189
+ const executionSignal = AbortSignal.any([...signal ? [signal] : [], ...owner?.signal ? [owner.signal] : []]);
190
+ executionSignal.throwIfAborted();
191
+ if (!typebox_value.Value.Check(ShareFileParameters, input)) throw new Error("Invalid share_file parameters.");
192
+ const host = owner?.host.context;
193
+ if (!host?.shareFile) throw new Error("File sharing is unavailable.");
194
+ const shared = await host.shareFile(input.path, "share_file");
195
+ if (executionSignal.aborted) {
196
+ shared.revoke();
197
+ executionSignal.throwIfAborted();
198
+ }
199
+ const result = {
200
+ url: shared.url,
201
+ fileName: shared.fileName,
202
+ mimeType: shared.mimeType,
203
+ size: shared.size,
204
+ expiresAt: shared.expiresAt,
205
+ maxDownloads: shared.maxDownloads
206
+ };
207
+ return {
208
+ content: [{
209
+ type: "text",
210
+ text: JSON.stringify(result)
211
+ }],
212
+ structuredContent: { ...result }
213
+ };
214
+ }
215
+ }, {
216
+ name: "save_file",
217
+ description: "Save a public HTTPS file (25 MiB maximum) or base64 file (5 MiB maximum) to session attachments or a new relative cwd path.",
218
+ parameters: SaveFileParameters,
219
+ _meta: {
220
+ "doompi/fileTransfer": true,
221
+ "openai/fileParams": ["file"]
222
+ },
223
+ async execute(_id, input, cancellation) {
224
+ if (!typebox_value.Value.Check(SaveFileParameters, input)) throw new Error("Provide exactly one file form.");
225
+ const context = owner?.agent?.context;
226
+ if (!context) throw new Error("Session file storage is unavailable.");
227
+ const signal = AbortSignal.any([...cancellation ? [cancellation] : [], ...owner?.signal ? [owner.signal] : []]);
228
+ signal.throwIfAborted();
229
+ const file = input.file;
230
+ const bytes = "download_url" in file ? await downloadPublicFile(file.download_url, signal) : decodeFileBase64(file.data_base64);
231
+ signal.throwIfAborted();
232
+ const name = "download_url" in file ? file.file_name : file.name;
233
+ const result = input.path === void 0 ? await (0, _agimon_ai_po_core_serverFacet.writeSessionAttachment)(context.sessionId, name, bytes, context.environment) : {
234
+ path: await writeCwdFile(context.cwd, input.path, bytes),
235
+ name: node_path.default.basename(input.path),
236
+ size: bytes.length,
237
+ mimeType: "application/octet-stream"
238
+ };
239
+ return {
240
+ content: [{
241
+ type: "text",
242
+ text: JSON.stringify(result)
243
+ }],
244
+ structuredContent: { ...result }
245
+ };
246
+ }
247
+ }];
248
+ }
249
+ //#endregion
250
+ //#region src/schemas/mcpApps.ts
251
+ const Lease = typebox.Type.String({
252
+ minLength: 1,
253
+ maxLength: 128
254
+ });
255
+ const JsonObject = typebox.Type.Record(typebox.Type.String(), typebox.Type.Unknown());
256
+ const Empty = typebox.Type.Object({}, { additionalProperties: false });
257
+ const Result = typebox.Type.Object({
258
+ content: typebox.Type.Array(typebox.Type.Unknown()),
259
+ structuredContent: typebox.Type.Optional(typebox.Type.Unknown()),
260
+ _meta: typebox.Type.Optional(JsonObject),
261
+ isError: typebox.Type.Optional(typebox.Type.Boolean())
262
+ });
263
+ const Address = {
264
+ service: "mcp.apps",
265
+ scope: "session",
266
+ direction: "client-to-server"
267
+ };
268
+ const mcpAppOpenMethod = (0, _agimon_ai_po_core_pluginProtocol.defineDoomPluginMethod)({
269
+ ...Address,
270
+ method: "open",
271
+ input: typebox.Type.Object({ toolCallId: typebox.Type.String({
272
+ minLength: 1,
273
+ maxLength: 512
274
+ }) }, { additionalProperties: false }),
275
+ output: typebox.Type.Object({
276
+ leaseId: Lease,
277
+ html: typebox.Type.String(),
278
+ resourceUri: typebox.Type.String(),
279
+ protocol: typebox.Type.Union([typebox.Type.Literal("mcp"), typebox.Type.Literal("openai")]),
280
+ resourceMeta: JsonObject,
281
+ tool: typebox.Type.Object({
282
+ name: typebox.Type.String(),
283
+ description: typebox.Type.String(),
284
+ inputSchema: typebox.Type.Object({ type: typebox.Type.Literal("object") }, { additionalProperties: true }),
285
+ _meta: JsonObject
286
+ }),
287
+ args: JsonObject,
288
+ result: Result,
289
+ state: typebox.Type.Unknown(),
290
+ readOnly: typebox.Type.Boolean()
291
+ })
292
+ });
293
+ const mcpAppActivateMethod = (0, _agimon_ai_po_core_pluginProtocol.defineDoomPluginMethod)({
294
+ ...Address,
295
+ method: "activate",
296
+ input: typebox.Type.Object({ leaseId: Lease }, { additionalProperties: false }),
297
+ output: typebox.Type.Object({ readOnly: typebox.Type.Literal(false) })
298
+ });
299
+ const mcpAppCallToolMethod = (0, _agimon_ai_po_core_pluginProtocol.defineDoomPluginMethod)({
300
+ ...Address,
301
+ method: "callTool",
302
+ input: typebox.Type.Object({
303
+ leaseId: Lease,
304
+ name: typebox.Type.String({
305
+ minLength: 1,
306
+ maxLength: 256
307
+ }),
308
+ arguments: JsonObject
309
+ }, { additionalProperties: false }),
310
+ output: Result
311
+ });
312
+ const mcpAppSetStateMethod = (0, _agimon_ai_po_core_pluginProtocol.defineDoomPluginMethod)({
313
+ ...Address,
314
+ method: "setState",
315
+ input: typebox.Type.Object({
316
+ leaseId: Lease,
317
+ state: typebox.Type.Unknown()
318
+ }, { additionalProperties: false }),
319
+ output: Empty
320
+ });
321
+ const mcpAppFollowUpMethod = (0, _agimon_ai_po_core_pluginProtocol.defineDoomPluginMethod)({
322
+ ...Address,
323
+ method: "followUp",
324
+ input: typebox.Type.Object({
325
+ leaseId: Lease,
326
+ prompt: typebox.Type.String({
327
+ minLength: 1,
328
+ maxLength: 16e3
329
+ })
330
+ }, { additionalProperties: false }),
331
+ output: Empty
332
+ });
333
+ const mcpAppCloseMethod = (0, _agimon_ai_po_core_pluginProtocol.defineDoomPluginMethod)({
334
+ ...Address,
335
+ method: "close",
336
+ input: typebox.Type.Object({ leaseId: Lease }, { additionalProperties: false }),
337
+ output: Empty
338
+ });
339
+ //#endregion
340
+ //#region src/services/mcpFileParams/index.ts
341
+ /** ChatGPT Apps metadata naming the top-level arguments that carry a file. */
342
+ const FILE_PARAMS_META_KEY = "openai/fileParams";
343
+ const MAX_FILE_PARAMS = 16;
344
+ const FILE_ID_PREFIX = "doomfile_";
345
+ const FILE_SHARE_ENTRY_TYPE = "mcp.file.share";
346
+ const CONSENT_TIMEOUT_MS = 12e4;
347
+ const NO_PUBLISHER = "This MCP tool takes a file, which needs the Po web host with remote access on.";
348
+ function record$1(value) {
349
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
350
+ }
351
+ function filePath(value) {
352
+ const path = record$1(value)?.path;
353
+ return typeof path === "string" ? path : void 0;
354
+ }
355
+ /** Declared file parameters that the input schema really has at its top level. */
356
+ function fileParamNames(tool) {
357
+ const declared = tool._meta?.[FILE_PARAMS_META_KEY];
358
+ const properties = record$1(tool.inputSchema.properties);
359
+ if (!Array.isArray(declared) || properties === void 0) return [];
360
+ const names = declared.filter((name) => typeof name === "string" && Object.hasOwn(properties, name));
361
+ return [...new Set(names)].slice(0, MAX_FILE_PARAMS);
362
+ }
363
+ /** What the model is asked for: a `{path}` object in place of each server file shape. */
364
+ function modelInputSchema(tool) {
365
+ if (Object.keys(tool.inputSchema).length === 0) return {
366
+ type: "object",
367
+ properties: {}
368
+ };
369
+ const names = fileParamNames(tool);
370
+ if (names.length === 0) return tool.inputSchema;
371
+ const properties = { ...record$1(tool.inputSchema.properties) };
372
+ for (const name of names) {
373
+ const description = record$1(properties[name])?.description;
374
+ properties[name] = {
375
+ type: "object",
376
+ description: `${typeof description === "string" ? description : "A file."} Pass {"path": "<file in the working directory, or the path from an Attached file note>"}.`,
377
+ properties: { path: { type: "string" } },
378
+ required: ["path"],
379
+ additionalProperties: false
380
+ };
381
+ }
382
+ return {
383
+ ...tool.inputSchema,
384
+ properties
385
+ };
386
+ }
387
+ /**
388
+ * Swaps each `{path}` file argument for a published link, on a copy.
389
+ *
390
+ * Runs before the server is dialled, so a refused share never reaches it. Values that
391
+ * are not `{path}` objects pass through, including a file a widget already shaped.
392
+ */
393
+ async function publishFileParams(tool, params, publish) {
394
+ const names = fileParamNames(tool).filter((name) => filePath(params[name]) !== void 0);
395
+ if (names.length === 0) return {
396
+ outbound: params,
397
+ release: () => void 0
398
+ };
399
+ if (publish === void 0) throw new Error(NO_PUBLISHER);
400
+ const outbound = { ...params };
401
+ const shared = [];
402
+ const release = () => {
403
+ for (const file of shared.splice(0)) file.revoke();
404
+ };
405
+ try {
406
+ for (const name of names) {
407
+ const file = await publish(filePath(params[name]));
408
+ shared.push(file);
409
+ outbound[name] = {
410
+ file_id: `${FILE_ID_PREFIX}${crypto.randomUUID()}`,
411
+ download_url: file.url,
412
+ file_name: file.fileName,
413
+ mime_type: file.mimeType
414
+ };
415
+ }
416
+ } catch (error) {
417
+ release();
418
+ throw error;
419
+ }
420
+ return {
421
+ outbound,
422
+ release
423
+ };
424
+ }
425
+ /**
426
+ * Publishes a file only after the user approves it for this server and tool.
427
+ *
428
+ * The host mints first so the dialog can name the file and its size; the link leaves
429
+ * this function only on approval. No client, no answer within two minutes, a
430
+ * cancelled call, or a failed audit all revoke it.
431
+ */
432
+ function createConsentedFilePublisher(options) {
433
+ return async (path, tool, signal) => {
434
+ const label = `${tool.serverName} / ${tool.toolName}`;
435
+ const shared = await options.shareFile(path, label);
436
+ try {
437
+ const host = options.host();
438
+ if (host === void 0) throw new Error(`There is no Po client to approve sharing "${shared.fileName}".`);
439
+ const deadline = new AbortController();
440
+ const timer = setTimeout(() => deadline.abort(/* @__PURE__ */ new Error("No answer within two minutes.")), CONSENT_TIMEOUT_MS);
441
+ const abort = AbortSignal.any([
442
+ deadline.signal,
443
+ signal,
444
+ options.signal
445
+ ].filter((candidate) => candidate !== void 0));
446
+ const abandoned = new Promise((_resolve, reject) => {
447
+ if (abort.aborted) reject(abort.reason);
448
+ abort.addEventListener("abort", () => reject(abort.reason), { once: true });
449
+ });
450
+ const answer = await Promise.race([host.context.client.request({
451
+ kind: "confirm",
452
+ title: "Share a file with an MCP server?",
453
+ message: `Share "${shared.fileName}" (${shared.size} bytes) with ${label}? Po publishes it at a link on your remote-access tunnel until this call finishes (10 minutes at most). Cloudflare and ${tool.serverName} can read the file.`
454
+ }, abort), abandoned]).then((accepted) => accepted === true ? "shared" : "declined", () => "unanswered").finally(() => clearTimeout(timer));
455
+ await host.context.session.appendCustomEntry(FILE_SHARE_ENTRY_TYPE, {
456
+ file: shared.fileName,
457
+ server: tool.serverName,
458
+ tool: tool.toolName,
459
+ outcome: answer
460
+ });
461
+ if (answer !== "shared") throw new Error(`The user declined to share "${shared.fileName}".`);
462
+ return shared;
463
+ } catch (error) {
464
+ shared.revoke();
465
+ throw error;
466
+ }
467
+ };
468
+ }
469
+ //#endregion
470
+ //#region src/services/mcpTools/index.ts
471
+ const MAX_APP_RESULT_BYTES = 1048576;
472
+ const appSnapshots = /* @__PURE__ */ new WeakMap();
473
+ /** Clone before output guards can replace structured data or mutate the source. */
474
+ function captureMcpAppResult(tool, result) {
475
+ const app = require_mcpCatalog.normalizeMcpAppMetadata(tool._meta);
476
+ if (!app.appVisible && !app.resourceUri) return void 0;
477
+ try {
478
+ const json = JSON.stringify(result);
479
+ if (Buffer.byteLength(json, "utf8") > MAX_APP_RESULT_BYTES) return void 0;
480
+ return {
481
+ version: 1,
482
+ ...app.resourceUri === void 0 ? {} : {
483
+ resourceUri: app.resourceUri,
484
+ protocol: app.protocol
485
+ },
486
+ result: JSON.parse(json)
487
+ };
488
+ } catch {
489
+ return;
490
+ }
491
+ }
492
+ /** Historical details are untrusted JSON; reject malformed or unbounded snapshots. */
493
+ function parseMcpAppSnapshot(value) {
494
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
495
+ const record = value;
496
+ if (record.version !== 1 || typeof record.result !== "object" || record.result === null || Array.isArray(record.result)) return void 0;
497
+ if (record.resourceUri !== void 0 && (typeof record.resourceUri !== "string" || !record.resourceUri.startsWith("ui://") || record.resourceUri.length > 8192)) return void 0;
498
+ if (record.protocol !== void 0 && record.protocol !== "mcp" && record.protocol !== "openai") return void 0;
499
+ if (record.resourceUri === void 0 !== (record.protocol === void 0)) return void 0;
500
+ const result = record.result;
501
+ if (!Array.isArray(result.content) || result.isError !== void 0 && typeof result.isError !== "boolean") return void 0;
502
+ if (result.content.some((block) => typeof block !== "object" || block === null || !("type" in block) || typeof block.type !== "string")) return void 0;
503
+ try {
504
+ const json = JSON.stringify(value);
505
+ if (Buffer.byteLength(json, "utf8") > MAX_APP_RESULT_BYTES) return void 0;
506
+ return JSON.parse(json);
507
+ } catch {
508
+ return;
509
+ }
510
+ }
511
+ /** Associate the original private payload with the guarded result without widening MCP wire output. */
512
+ function retainMcpAppResult(result, snapshot) {
513
+ if (snapshot) appSnapshots.set(result, snapshot);
514
+ }
515
+ /** The text of a downstream result: its text blocks joined, which is what the model reads. */
516
+ function resultText(result) {
517
+ return (result.content ?? []).flatMap((block) => block.type === "text" ? [block.text] : []).join("\n").trim();
518
+ }
519
+ /** The image blocks, re-emitted as Pi's own image content so the model and the cockpit both see them. */
520
+ function resultImages(result) {
521
+ return (result.content ?? []).flatMap((block) => block.type === "image" ? [{
522
+ type: "image",
523
+ data: block.data,
524
+ mimeType: block.mimeType
525
+ }] : []);
526
+ }
527
+ /**
528
+ * Everything else a downstream result carried: audio, resource links, embedded
529
+ * resources, and the structured content. Pi's content cannot hold them, so
530
+ * they ride the result's details for the cockpit's MCP message to render.
531
+ */
532
+ function resultBlocks(result) {
533
+ const blocks = [];
534
+ for (const block of result.content ?? []) if (block.type === "audio") blocks.push({
535
+ type: "audio",
536
+ data: block.data,
537
+ mimeType: block.mimeType
538
+ });
539
+ else if (block.type === "resource_link") blocks.push({
540
+ type: "resource_link",
541
+ uri: block.uri,
542
+ name: block.name,
543
+ ...block.title === void 0 ? {} : { title: block.title },
544
+ ...block.description === void 0 ? {} : { description: block.description },
545
+ ...block.mimeType === void 0 ? {} : { mimeType: block.mimeType }
546
+ });
547
+ else if (block.type === "resource") {
548
+ const resource = block.resource;
549
+ blocks.push({
550
+ type: "resource",
551
+ uri: resource.uri,
552
+ ...resource.mimeType === void 0 ? {} : { mimeType: resource.mimeType },
553
+ ..."text" in resource && typeof resource.text === "string" ? { text: resource.text } : {},
554
+ ..."blob" in resource && typeof resource.blob === "string" ? { blob: resource.blob } : {}
555
+ });
556
+ }
557
+ const structured = structuredRecord(result);
558
+ if (structured !== void 0) blocks.push({
559
+ type: "structured",
560
+ value: structured
561
+ });
562
+ return blocks;
563
+ }
564
+ /**
565
+ * MCP v2 allows any JSON value as structured content; Po's contract carries an object, which
566
+ * is all v1 permitted. Any other value still reaches the model through the text fallback.
567
+ */
568
+ function structuredRecord(result) {
569
+ const value = result.structuredContent;
570
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
571
+ }
572
+ /** Remote MCP preserves error flags and public structured results instead of Pi's thrown-error convention. */
573
+ function toHeadlessToolResult(tool, result) {
574
+ const text = resultText(result) || (result.structuredContent === void 0 ? "No output." : JSON.stringify(result.structuredContent));
575
+ const blocks = resultBlocks(result);
576
+ const structured = structuredRecord(result);
577
+ const app = appSnapshots.get(result) ?? (require_mcpCatalog.normalizeMcpAppMetadata(tool._meta).resourceUri === void 0 ? void 0 : captureMcpAppResult(tool, result));
578
+ return {
579
+ content: [{
580
+ type: "text",
581
+ text
582
+ }, ...resultImages(result)],
583
+ ...structured === void 0 ? {} : { structuredContent: structured },
584
+ ...result.isError === void 0 ? {} : { isError: result.isError },
585
+ ...result._meta === void 0 ? {} : { _meta: result._meta },
586
+ details: {
587
+ server: tool.serverName,
588
+ tool: tool.toolName,
589
+ ...blocks.length > 0 ? { blocks } : {},
590
+ ...app === void 0 ? {} : { app }
591
+ }
592
+ };
593
+ }
594
+ //#endregion
595
+ //#region src/services/mcpApps/index.ts
596
+ const STATE_ENTRY = "mcp.app.state";
597
+ const MAX_HTML_BYTES = 2097152;
598
+ const MAX_RESULT_BYTES = 1048576;
599
+ const MAX_STATE_BYTES = 65536;
600
+ const MAX_LEASES = 64;
601
+ function record(value) {
602
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
603
+ }
604
+ function boundedJson(value, limit, label) {
605
+ const encoded = JSON.stringify(value);
606
+ if (encoded === void 0 || Buffer.byteLength(encoded) > limit) throw new Error(`${label} exceeds its size limit.`);
607
+ return JSON.parse(encoded);
608
+ }
609
+ /** Browser inputs identify a persisted invocation, never an upstream server or resource. */
610
+ function createMcpAppsService(tools, getHost, lifetime) {
611
+ const leases = /* @__PURE__ */ new Map();
612
+ let disposed = false;
613
+ let opening = 0;
614
+ const revoke = (lease) => {
615
+ leases.delete(lease.id);
616
+ lease.controller.abort();
617
+ };
618
+ const check = (lease) => {
619
+ lease.signal.throwIfAborted();
620
+ if (disposed || getHost() !== lease.host) throw new Error("App session is no longer available.");
621
+ lease.host.assertActive();
622
+ const surface = lease.host.toolSurface;
623
+ const current = surface?.readSurface();
624
+ if (!surface || current?.revision !== lease.revision || tools.runtimeRevision() !== lease.runtimeRevision) throw new Error("App permissions changed. Reopen the App.");
625
+ const source = tools.snapshot().find((tool) => tool.serverName === lease.server && tool.toolName === lease.tool);
626
+ if (!source || require_mcpCatalog.normalizeMcpAppMetadata(source._meta).resourceUri !== lease.resourceUri) throw new Error("App resource is no longer available.");
627
+ if (!current.tools.some((tool) => tool.name === source.piName || tool.name === "mcp")) throw new Error("The original App tool is not admitted in this session.");
628
+ return surface;
629
+ };
630
+ const owned = (id, caller) => {
631
+ caller.signal?.throwIfAborted();
632
+ const lease = leases.get(id);
633
+ if (!lease || !caller.connectionId || lease.connectionId !== caller.connectionId) throw new Error("App lease is not owned by this connection.");
634
+ try {
635
+ check(lease);
636
+ } catch (error) {
637
+ revoke(lease);
638
+ throw error;
639
+ }
640
+ return lease;
641
+ };
642
+ const operation = async (id, caller, write, run) => {
643
+ const lease = owned(id, caller);
644
+ if (write && lease.readOnly) throw new Error("Enable App interactions before performing this action.");
645
+ if (lease.busy) throw new Error("An App action is already in progress.");
646
+ if (++lease.actions > 256) throw new Error("App action limit reached. Reopen the App.");
647
+ lease.busy = true;
648
+ try {
649
+ return await run(lease);
650
+ } finally {
651
+ lease.busy = false;
652
+ }
653
+ };
654
+ const confirm = async (lease, title, message) => {
655
+ check(lease);
656
+ const accepted = await lease.host.context.client.request({
657
+ kind: "confirm",
658
+ title,
659
+ message
660
+ }, AbortSignal.any([lease.signal, AbortSignal.timeout(12e4)]));
661
+ check(lease);
662
+ if (accepted !== true) throw new Error("App action was not approved.");
663
+ };
664
+ const invocation = async (host, toolCallId) => {
665
+ const entries = await host.context.session.entries({ type: "message" });
666
+ let call;
667
+ let result;
668
+ for (const entry of entries) {
669
+ const message = record(entry.message);
670
+ if (message?.role === "assistant" && Array.isArray(message.content)) for (const block of message.content) {
671
+ const candidate = record(block);
672
+ if (candidate?.type === "toolCall" && candidate.id === toolCallId) call = candidate;
673
+ }
674
+ if (message?.role === "toolResult" && message.toolCallId === toolCallId) result = message;
675
+ }
676
+ const details = record(result?.details);
677
+ const app = parseMcpAppSnapshot(details?.app);
678
+ if (!call || !app?.resourceUri || !app.protocol || typeof details?.server !== "string" || typeof details.tool !== "string") throw new Error("This invocation has no retained App result.");
679
+ return {
680
+ call,
681
+ details,
682
+ app
683
+ };
684
+ };
685
+ const stopTools = tools.onChange(() => {
686
+ for (const lease of leases.values()) try {
687
+ check(lease);
688
+ } catch {
689
+ revoke(lease);
690
+ }
691
+ });
692
+ const dispose = () => {
693
+ disposed = true;
694
+ stopTools();
695
+ for (const lease of leases.values()) revoke(lease);
696
+ };
697
+ lifetime?.addEventListener("abort", dispose, { once: true });
698
+ return {
699
+ dispose,
700
+ methods: [
701
+ (0, _agimon_ai_po_core_serverFacet.defineServerMethod)(mcpAppOpenMethod, async ({ toolCallId }, caller) => {
702
+ if (!caller.connectionId || !caller.signal) throw new Error("An authenticated live connection is required.");
703
+ caller.signal.throwIfAborted();
704
+ if (disposed || lifetime?.aborted) throw new Error("App session is no longer available.");
705
+ if (leases.size + opening >= MAX_LEASES) throw new Error("Too many open Apps. Close an App before opening another.");
706
+ opening++;
707
+ try {
708
+ const host = getHost();
709
+ if (!host?.toolSurface) throw new Error("App execution is unavailable in this session.");
710
+ host.assertActive();
711
+ const revision = host.toolSurface.readSurface().revision;
712
+ const runtimeRevision = tools.runtimeRevision();
713
+ const { call, details, app } = await invocation(host, toolCallId);
714
+ const source = tools.snapshot().find((tool) => tool.serverName === details.server && tool.toolName === details.tool);
715
+ const declared = require_mcpCatalog.normalizeMcpAppMetadata(source?._meta);
716
+ if (!source || declared.resourceUri !== app.resourceUri || declared.protocol !== app.protocol) throw new Error("The original App resource is no longer declared.");
717
+ const args = record(call.arguments) ?? {};
718
+ const generic = call.name === "mcp" && args.server === source.serverName && args.tool === source.toolName;
719
+ if (call.name !== source.piName && !generic) throw new Error("App invocation identity does not match its tool.");
720
+ const controller = new AbortController();
721
+ const lease = {
722
+ id: (0, node_crypto.randomUUID)(),
723
+ connectionId: caller.connectionId,
724
+ host,
725
+ toolCallId,
726
+ server: source.serverName,
727
+ tool: source.toolName,
728
+ resourceUri: app.resourceUri,
729
+ revision,
730
+ runtimeRevision,
731
+ controller,
732
+ signal: AbortSignal.any([
733
+ controller.signal,
734
+ caller.signal,
735
+ ...lifetime ? [lifetime] : []
736
+ ]),
737
+ readOnly: true,
738
+ busy: false,
739
+ actions: 0
740
+ };
741
+ check(lease);
742
+ const resource = await tools.readAppResource(lease.server, lease.tool, lease.resourceUri, lease.signal);
743
+ check(lease);
744
+ const content = resource.contents.find((item) => item.uri === lease.resourceUri && "text" in item);
745
+ if (!content || !("text" in content) || typeof content.text !== "string") throw new Error("App resource must contain HTML text.");
746
+ if (content.mimeType !== "text/html;profile=mcp-app" && content.mimeType !== "text/html+skybridge") throw new Error("Unsupported App resource MIME type.");
747
+ if (Buffer.byteLength(content.text) > MAX_HTML_BYTES) throw new Error("App HTML exceeds its size limit.");
748
+ const states = await host.context.session.entries({
749
+ type: "custom",
750
+ customType: STATE_ENTRY
751
+ });
752
+ let state = null;
753
+ for (const entry of states) {
754
+ const data = record(entry.data);
755
+ if (data?.version === 1 && data.toolCallId === toolCallId && data.server === lease.server && data.tool === lease.tool && data.resourceUri === lease.resourceUri) state = data.state ?? null;
756
+ }
757
+ check(lease);
758
+ if (leases.size >= MAX_LEASES) throw new Error("Too many open Apps.");
759
+ const opened = {
760
+ leaseId: lease.id,
761
+ html: content.text,
762
+ resourceUri: lease.resourceUri,
763
+ protocol: app.protocol,
764
+ resourceMeta: boundedJson(record(content._meta) ?? record(resource._meta) ?? {}, MAX_STATE_BYTES, "Resource metadata"),
765
+ tool: {
766
+ name: source.toolName,
767
+ description: source.description ?? "",
768
+ inputSchema: boundedJson({
769
+ ...source.inputSchema,
770
+ type: "object"
771
+ }, MAX_STATE_BYTES, "Tool schema"),
772
+ _meta: boundedJson(source._meta ?? {}, MAX_STATE_BYTES, "Tool metadata")
773
+ },
774
+ args: boundedJson(generic ? record(args.arguments) ?? {} : args, MAX_STATE_BYTES, "Tool input"),
775
+ result: app.result,
776
+ state: boundedJson(state, MAX_STATE_BYTES, "App state"),
777
+ readOnly: true
778
+ };
779
+ leases.set(lease.id, lease);
780
+ lease.signal.addEventListener("abort", () => leases.delete(lease.id), { once: true });
781
+ return opened;
782
+ } finally {
783
+ opening--;
784
+ }
785
+ }),
786
+ (0, _agimon_ai_po_core_serverFacet.defineServerMethod)(mcpAppActivateMethod, ({ leaseId }, caller) => operation(leaseId, caller, false, async (lease) => {
787
+ await confirm(lease, `Enable ${lease.server} App interactions?`, "This App may request tool calls and follow-up messages. Each action still requires approval.");
788
+ lease.readOnly = false;
789
+ return { readOnly: false };
790
+ })),
791
+ (0, _agimon_ai_po_core_serverFacet.defineServerMethod)(mcpAppCallToolMethod, ({ leaseId, name, arguments: args }, caller) => operation(leaseId, caller, true, async (lease) => {
792
+ boundedJson(args, MAX_STATE_BYTES, "Tool arguments");
793
+ const target = tools.snapshot().find((tool) => tool.serverName === lease.server && tool.toolName === name && require_mcpCatalog.isAppVisibleMcpTool(tool));
794
+ if (!target) throw new Error("This tool is not available to this App.");
795
+ const surface = check(lease);
796
+ const exposed = surface.readSurface().tools;
797
+ const direct = exposed.some((tool) => tool.name === target.piName);
798
+ if (!direct && !exposed.some((tool) => tool.name === "mcp")) throw new Error("This tool is not admitted in this session.");
799
+ await confirm(lease, `Allow ${lease.server} App to call ${name}?`, JSON.stringify(args, null, 2));
800
+ const result = await surface.invokeTool({
801
+ revision: lease.revision,
802
+ name: direct ? target.piName : "mcp",
803
+ arguments: direct ? args : {
804
+ server: lease.server,
805
+ tool: name,
806
+ arguments: args
807
+ },
808
+ signal: AbortSignal.any([lease.signal, AbortSignal.timeout(12e4)]),
809
+ authorize: () => {
810
+ check(lease);
811
+ }
812
+ });
813
+ check(lease);
814
+ return parseMcpAppSnapshot(record(result.details)?.app)?.result ?? boundedJson({
815
+ content: result.content,
816
+ ...result.structuredContent === void 0 ? {} : { structuredContent: result.structuredContent },
817
+ ...result._meta === void 0 ? {} : { _meta: result._meta },
818
+ ...result.isError === void 0 ? {} : { isError: result.isError }
819
+ }, MAX_RESULT_BYTES, "Tool result");
820
+ })),
821
+ (0, _agimon_ai_po_core_serverFacet.defineServerMethod)(mcpAppSetStateMethod, ({ leaseId, state }, caller) => operation(leaseId, caller, false, async (lease) => {
822
+ const saved = boundedJson(state, MAX_STATE_BYTES, "App state");
823
+ await lease.host.context.session.appendCustomEntry(STATE_ENTRY, {
824
+ version: 1,
825
+ toolCallId: lease.toolCallId,
826
+ server: lease.server,
827
+ tool: lease.tool,
828
+ resourceUri: lease.resourceUri,
829
+ state: saved
830
+ });
831
+ check(lease);
832
+ return {};
833
+ })),
834
+ (0, _agimon_ai_po_core_serverFacet.defineServerMethod)(mcpAppFollowUpMethod, ({ leaseId, prompt }, caller) => operation(leaseId, caller, true, async (lease) => {
835
+ const session = lease.host.context.session;
836
+ if (!session.admitPrompt) throw new Error("App follow-up messages are unavailable in this session.");
837
+ await confirm(lease, `Send a follow-up from ${lease.server}?`, prompt);
838
+ await session.admitPrompt(`[MCP App ${lease.server}/${lease.tool}, invocation ${lease.toolCallId}]\n${prompt}`, "followUp");
839
+ check(lease);
840
+ return {};
841
+ })),
842
+ (0, _agimon_ai_po_core_serverFacet.defineServerMethod)(mcpAppCloseMethod, ({ leaseId }, caller) => {
843
+ const lease = leases.get(leaseId);
844
+ if (lease && lease.connectionId !== caller.connectionId) throw new Error("App lease is not owned by this connection.");
845
+ if (lease) revoke(lease);
846
+ return {};
847
+ })
848
+ ]
849
+ };
850
+ }
851
+ //#endregion
852
+ //#region src/services/mcpHeadlessTools/index.ts
853
+ const MCP_HEADLESS_TOOLS_SERVICE = "doom/mcp-headless-tools";
854
+ function mountMcpHeadlessTools(tools) {
855
+ return (context) => {
856
+ context.plugin((providerContext) => {
857
+ providerContext.provide(MCP_HEADLESS_TOOLS_SERVICE, { get: (name) => tools.find((tool) => tool.name === name) });
858
+ });
859
+ };
860
+ }
861
+ //#endregion
862
+ //#region src/schemas/mcpHeadlessTool.ts
863
+ const McpHeadlessToolParameters = typebox.Type.Object({
864
+ server: typebox.Type.String({ minLength: 1 }),
865
+ tool: typebox.Type.String({ minLength: 1 }),
866
+ arguments: typebox.Type.Optional(typebox.Type.Record(typebox.Type.String(), typebox.Type.Unknown()))
867
+ });
868
+ //#endregion
869
+ //#region src/services/mcpSessionTools/index.ts
870
+ const MCP_SESSION_TOOLS_SERVICE = "doom/mcp-session-tools";
871
+ function createMcpSessionToolsService(session, generation, assertAvailable = () => void 0) {
872
+ let disposed = false;
873
+ const listeners = /* @__PURE__ */ new Set();
874
+ const retained = /* @__PURE__ */ new Map();
875
+ const snapshot = () => {
876
+ try {
877
+ if (disposed) return [];
878
+ assertAvailable();
879
+ return session.activeToolDefinitions();
880
+ } catch {
881
+ return [];
882
+ }
883
+ };
884
+ const project = () => {
885
+ const definitions = snapshot();
886
+ const names = new Set(definitions.map((tool) => tool.piName));
887
+ for (const [name, entry] of retained) {
888
+ if (names.has(name)) continue;
889
+ entry.retire();
890
+ retained.delete(name);
891
+ }
892
+ return definitions.map((definition) => {
893
+ const fingerprint = JSON.stringify([session.toolGeneration, definition]);
894
+ const previous = retained.get(definition.piName);
895
+ if (previous?.fingerprint === fingerprint) return previous.tool;
896
+ previous?.retire();
897
+ let retired = false;
898
+ const invoke = session.bindToolInvocation(definition);
899
+ const tool = {
900
+ name: definition.piName,
901
+ label: `${definition.serverName}: ${definition.toolName}`,
902
+ description: definition.description ?? `${definition.toolName} on the ${definition.serverName} MCP server.`,
903
+ ...definition.annotations === void 0 ? {} : { annotations: definition.annotations },
904
+ ...definition._meta === void 0 ? {} : { _meta: require_mcpCatalog.mcpToolExecutionMetadata(definition) },
905
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
906
+ parameters: modelInputSchema(definition),
907
+ async execute(_toolCallId, parameters, signal) {
908
+ assertAvailable();
909
+ if (retired) throw new Error(`MCP tool ${definition.piName} is no longer available.`);
910
+ signal?.throwIfAborted();
911
+ const arguments_ = parameters === void 0 ? {} : parameters;
912
+ if (!typebox_value.Value.Check(tool.parameters, arguments_)) throw new Error(`Invalid arguments for MCP tool ${definition.piName}.`);
913
+ const result = await invoke(arguments_, signal);
914
+ signal?.throwIfAborted();
915
+ return result;
916
+ }
917
+ };
918
+ retained.set(definition.piName, {
919
+ fingerprint,
920
+ tool,
921
+ retire: () => {
922
+ retired = true;
923
+ }
924
+ });
925
+ return tool;
926
+ });
927
+ };
928
+ const refresh = () => {
929
+ project();
930
+ for (const listener of listeners) listener();
931
+ };
932
+ const stopSession = session.onChange(refresh);
933
+ return Object.freeze({
934
+ generation,
935
+ runtimeRevision: () => session.toolGeneration,
936
+ readAppResource: async (server, tool, resourceUri, signal) => {
937
+ assertAvailable();
938
+ if (disposed) throw new Error("The MCP session is no longer available.");
939
+ const result = await session.readAppResource(server, tool, resourceUri, signal);
940
+ assertAvailable();
941
+ if (disposed) throw new Error("The MCP session is no longer available.");
942
+ return result;
943
+ },
944
+ snapshot,
945
+ project,
946
+ refresh,
947
+ dispose: () => {
948
+ disposed = true;
949
+ stopSession();
950
+ refresh();
951
+ listeners.clear();
952
+ },
953
+ resolveSelectors: (selectors) => {
954
+ const active = new Set(snapshot().map((tool) => tool.piName));
955
+ return session.resolveToolSelectors(selectors).map((tool) => tool.name).filter((name) => active.has(name));
956
+ },
957
+ onChange: (listener) => {
958
+ listeners.add(listener);
959
+ return () => {
960
+ listeners.delete(listener);
961
+ };
962
+ },
963
+ invoke: async (name, parameters, signal) => {
964
+ assertAvailable();
965
+ const tool = project().find((candidate) => candidate.name === name);
966
+ if (!tool) throw new Error(`MCP tool ${name} is not available in this session.`);
967
+ return tool.execute("mcp", parameters, signal);
968
+ }
969
+ });
970
+ }
971
+ /** Borrow an existing MCP runtime; a child never starts or disposes its own upstream connections. */
972
+ function createMcpChildTool(invoke, catalog) {
973
+ return {
974
+ name: "mcp",
975
+ ...catalog === void 0 ? {} : { catalog },
976
+ description: "Call a tool exposed by a connected MCP server.",
977
+ parameters: McpHeadlessToolParameters,
978
+ async execute(_toolCallId, parameters, signal) {
979
+ signal?.throwIfAborted();
980
+ if (!typebox_value.Value.Check(McpHeadlessToolParameters, parameters)) throw new Error("Invalid MCP tool arguments.");
981
+ const result = await invoke(parameters, signal);
982
+ signal?.throwIfAborted();
983
+ return result;
984
+ }
985
+ };
986
+ }
987
+ //#endregion
988
+ //#region src/constants/mcp.ts
989
+ const SERVER_COMMAND_DESCRIPTION = "Show MCP configuration or begin an authorization request.";
990
+ //#endregion
991
+ //#region src/constants/piMcp.ts
992
+ const PACKAGE_SOURCE = "@agimon-ai/po-mcp";
993
+ /**
994
+ * Footer status carrying the session's MCP server names, comma separated.
995
+ *
996
+ * MCP tools are registered under `<server>_<tool>`, a name only known at
997
+ * runtime, so the cockpit's web plugin reads this status to recognise which
998
+ * tool calls are MCP calls. The web half duplicates the literal (it may only
999
+ * import src/types and src/constants), and a test keeps the two in step.
1000
+ */
1001
+ const MCP_STATUS_KEY = "doom-mcp";
1002
+ //#endregion
1003
+ //#region src/types/webMcp.ts
1004
+ const MCP_SESSION_AUTH_STATUS_KEY = "doom-mcp-session-auth";
1005
+ new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "gu");
1006
+ const MAX_AUTHORIZATION_URL_LENGTH = 8192;
1007
+ const MAX_SERVER_TOOLS = 512;
1008
+ const MAX_TOOL_NAME_LENGTH = 256;
1009
+ const SESSION_AUTH_STATES = [
1010
+ "not-connected",
1011
+ "connecting",
1012
+ "connected",
1013
+ "degraded",
1014
+ "needs-auth",
1015
+ "failed",
1016
+ "closed",
1017
+ "disabled"
1018
+ ];
1019
+ function isSessionAuthState(value) {
1020
+ return SESSION_AUTH_STATES.some((state) => state === value);
1021
+ }
1022
+ /** Session route for one reachable tool's description and schema. */
1023
+ const MCP_SESSION_TOOL_API_PATH = "/tool";
1024
+ const MCP_SESSION_TOOL_SERVER_QUERY = "server";
1025
+ const MCP_SESSION_TOOL_NAME_QUERY = "tool";
1026
+ /** Keeps servers visible before discovery and after failures, without diagnostics or credentials. */
1027
+ function formatMcpSessionAuthStatus(servers) {
1028
+ const items = servers.flatMap((server) => {
1029
+ if (!isSessionAuthState(server.state)) return [];
1030
+ const tools = (server.tools ?? []).filter((tool) => tool.active && isToolName(tool.toolName)).map((tool) => ({
1031
+ name: tool.toolName,
1032
+ ...isToolName(tool.piName) ? { piName: tool.piName } : {},
1033
+ ...isTokenCount(tool.tokens) ? { tokens: tool.tokens } : {}
1034
+ })).slice(0, MAX_SERVER_TOOLS);
1035
+ return [{
1036
+ name: server.name,
1037
+ state: server.state,
1038
+ ...isAuthorizationUrl(server.authorizationUrl) ? { authorizationUrl: server.authorizationUrl } : {},
1039
+ ...tools.length === 0 ? {} : { tools }
1040
+ }];
1041
+ });
1042
+ return items.length === 0 ? void 0 : JSON.stringify(items);
1043
+ }
1044
+ function hasControlCharacter(value) {
1045
+ for (const character of value) {
1046
+ const codePoint = character.codePointAt(0);
1047
+ if (codePoint !== void 0 && (codePoint < 32 || codePoint >= 127 && codePoint <= 159)) return true;
1048
+ }
1049
+ return false;
1050
+ }
1051
+ function isAuthorizationUrl(value) {
1052
+ if (typeof value !== "string" || value.length > MAX_AUTHORIZATION_URL_LENGTH || hasControlCharacter(value)) return false;
1053
+ try {
1054
+ const url = new URL(value);
1055
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ }
1060
+ function isToolName(value) {
1061
+ return typeof value === "string" && value.trim() !== "" && value.length <= MAX_TOOL_NAME_LENGTH && !hasControlCharacter(value);
1062
+ }
1063
+ function isTokenCount(value) {
1064
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
1065
+ }
1066
+ /** Hub API paths for repository-scoped MCP management. */
1067
+ const MCP_REPOSITORY_API_PATH = "/repository";
1068
+ const MCP_DISCOVERY_API_PATH = "/repository/discover";
1069
+ const MCP_AUTHORIZATION_API_PATH = "/repository/authorize";
1070
+ /** The repository every route is asked about, and the flow segment the two flow routes carry. */
1071
+ const MCP_REPOSITORY_ID_QUERY = "repositoryId";
1072
+ const MCP_FLOW_ID_PARAM = "flowId";
1073
+ //#endregion
1074
+ //#region src/services/mcpCommand/index.ts
1075
+ /** One line per server: its state, and how many tools it currently offers. */
1076
+ function formatStatus(session) {
1077
+ const { servers } = session.getSnapshot();
1078
+ if (servers.length === 0) return "No MCP servers are configured for this session.";
1079
+ return [...servers.map((server) => {
1080
+ const detail = server.error ? ` (${server.error})` : "";
1081
+ return `${server.name}: ${server.state} · ${server.tools.length} tools${detail}`;
1082
+ }), ...session.getDiagnostics()].join("\n");
1083
+ }
1084
+ //#endregion
1085
+ //#region src/services/directTools/index.ts
1086
+ const SELECTOR_SEPARATOR = "/";
1087
+ /**
1088
+ * Which downstream tools a child may see.
1089
+ *
1090
+ * A selector is either a whole server (`pencil`) or one tool of it
1091
+ * (`pencil/get_screenshot`).
1092
+ */
1093
+ var DirectToolFilter = class {
1094
+ servers = /* @__PURE__ */ new Set();
1095
+ tools = /* @__PURE__ */ new Map();
1096
+ constructor(selectors) {
1097
+ for (const raw of selectors) {
1098
+ const selector = raw.trim().replace(/\/+$/, "");
1099
+ if (!selector) continue;
1100
+ const separator = selector.indexOf(SELECTOR_SEPARATOR);
1101
+ if (separator === -1) {
1102
+ this.servers.add(selector);
1103
+ continue;
1104
+ }
1105
+ const server = selector.slice(0, separator);
1106
+ const tool = selector.slice(separator + 1);
1107
+ if (!server) continue;
1108
+ if (!tool) {
1109
+ this.servers.add(server);
1110
+ continue;
1111
+ }
1112
+ const selected = this.tools.get(server) ?? /* @__PURE__ */ new Set();
1113
+ selected.add(tool);
1114
+ this.tools.set(server, selected);
1115
+ }
1116
+ }
1117
+ allows(serverName, toolName) {
1118
+ if (this.servers.has(serverName)) return true;
1119
+ return this.tools.get(serverName)?.has(toolName) ?? false;
1120
+ }
1121
+ };
1122
+ //#endregion
1123
+ //#region src/services/directToolsEnvironment/index.ts
1124
+ /** Reads a child agent's MCP tool selection from the process environment. */
1125
+ function readDirectToolFilter(environment = process.env) {
1126
+ const raw = environment[require_mcpCatalog.DIRECT_TOOLS_ENV];
1127
+ if (raw === void 0 || raw === "") return void 0;
1128
+ if (raw === "__none__") return new DirectToolFilter([]);
1129
+ return new DirectToolFilter(raw.split(","));
1130
+ }
1131
+ //#endregion
1132
+ //#region src/services/toolVisibility/index.ts
1133
+ /**
1134
+ * Hides every wrapper this extension owns that the catalog does not currently offer.
1135
+ *
1136
+ * The surface recomputes from the whole registered inventory, so this only ever
1137
+ * removes: a server that reconnected is already in `incoming` and nothing has to
1138
+ * put it back. Historical ownership matters after reconfiguration because Pi
1139
+ * cannot unregister a wrapper the new domain no longer selects.
1140
+ *
1141
+ * The visible set is read once, here, so the returned function stays pure and a
1142
+ * later re-apply by another owner cannot see half a catalog change.
1143
+ */
1144
+ function mcpToolRestriction(catalog, historicallyOwnedNames = new Set(catalog.allTools().map((tool) => tool.piName)), incompatibleNames = /* @__PURE__ */ new Set()) {
1145
+ const visible = new Set(catalog.activeToolNames().filter((name) => !incompatibleNames.has(name)));
1146
+ return (incoming) => incoming.filter((name) => !historicallyOwnedNames.has(name) || visible.has(name));
1147
+ }
1148
+ //#endregion
1149
+ //#region src/services/mcpSession/index.ts
1150
+ /** Raised by anything needing a live container before one has been built. */
1151
+ const RUNTIME_NOT_STARTED = "The MCP runtime has not started yet.";
1152
+ const AUTHORIZATION_URL_NOT_READY = "No authorization page is waiting for this MCP server.";
1153
+ function canonicalJson(value) {
1154
+ const normalize = (candidate) => {
1155
+ if (Array.isArray(candidate)) return candidate.map(normalize);
1156
+ if (typeof candidate !== "object" || candidate === null) return candidate;
1157
+ return Object.fromEntries(Object.entries(candidate).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, normalize(child)]));
1158
+ };
1159
+ return JSON.stringify(normalize(value));
1160
+ }
1161
+ function mcpConfigurationFingerprint(configuration) {
1162
+ return canonicalJson({
1163
+ enabled: configuration.enabled ?? true,
1164
+ repoRoot: configuration.repoRoot,
1165
+ stagingDirectory: configuration.stagingDirectory,
1166
+ generatedConfigPath: configuration.generatedConfigPath,
1167
+ pluginConfigPaths: configuration.pluginConfigPaths ?? [],
1168
+ sources: configuration.sources ?? [],
1169
+ allowlist: {
1170
+ servers: [...configuration.allowlist?.servers ?? []].sort((left, right) => left.localeCompare(right)),
1171
+ proxy: [...configuration.allowlist?.proxy ?? []].sort((left, right) => left.localeCompare(right))
1172
+ }
1173
+ });
1174
+ }
1175
+ function toolRegistrationFingerprint(tool) {
1176
+ return canonicalJson({
1177
+ serverName: tool.serverName,
1178
+ toolName: tool.toolName,
1179
+ inputSchema: tool.inputSchema,
1180
+ _meta: tool._meta
1181
+ });
1182
+ }
1183
+ /**
1184
+ * The MCP picture for one Pi session.
1185
+ *
1186
+ * Install is synchronous and cheap: the previous run's catalog is read off disk and
1187
+ * its tools registered, so Pi's first turn already has them. Connecting happens on
1188
+ * a detached promise afterwards, and each server folds into the active tool set as
1189
+ * it lands. Nothing on this path waits for a socket.
1190
+ */
1191
+ var McpSession = class {
1192
+ /** Created after session_start, never while Pi is importing extensions. */
1193
+ runtime;
1194
+ catalog;
1195
+ groups = {
1196
+ shared: {
1197
+ configSources: [],
1198
+ configPaths: [],
1199
+ serverNames: []
1200
+ },
1201
+ sessionLocal: {
1202
+ configSources: [],
1203
+ configPaths: [],
1204
+ serverNames: []
1205
+ },
1206
+ droppedServers: [],
1207
+ diagnostics: []
1208
+ };
1209
+ options;
1210
+ /** Credential persistence used by the active runtime and explicit reauthorization. */
1211
+ tokenStore;
1212
+ /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */
1213
+ resourceCache = /* @__PURE__ */ new Map();
1214
+ changeListeners = /* @__PURE__ */ new Set();
1215
+ /** Authorization URLs waiting to be opened, by server. */
1216
+ authorizationUrls = /* @__PURE__ */ new Map();
1217
+ /** Servers whose explicit auth action should launch the browser when its URL arrives. */
1218
+ browserAuthorizationRequests = /* @__PURE__ */ new Set();
1219
+ /** Explicit session disconnects fence late discovery and stale tool invocations. */
1220
+ disconnectedServers = /* @__PURE__ */ new Set();
1221
+ /** Bounded, session-local diagnostics. Never tool-result values or cached credentials. */
1222
+ outputSchemaWarnings = [];
1223
+ /** Invalidates deferred startup work when a session reloads or shuts down. */
1224
+ lifecycleGeneration = 0;
1225
+ configurationFingerprint;
1226
+ workspaceRoot;
1227
+ executionCwd;
1228
+ /** Pi wrappers survive configuration changes because the host cannot unregister them. */
1229
+ retainedTools = /* @__PURE__ */ new Map();
1230
+ registeredToolFingerprints = /* @__PURE__ */ new Map();
1231
+ historicallyOwnedNames = /* @__PURE__ */ new Set();
1232
+ incompatibleNames = /* @__PURE__ */ new Set();
1233
+ /** The session's slot in the tool surface, held only while a session fiber is up. */
1234
+ toolRestriction;
1235
+ /** Resolved per tool call, so a registration outlives the container it was made under. */
1236
+ getClientManager = () => this.runtime?.getServices()?.clientManager;
1237
+ constructor(options) {
1238
+ this.options = options;
1239
+ this.catalog = this.createCatalog();
1240
+ }
1241
+ /**
1242
+ * Puts this session's tool visibility under the Doom tool surface.
1243
+ *
1244
+ * Registering applies the current restriction immediately, which is the first
1245
+ * point the session can express that nothing has connected yet: the surface
1246
+ * only exists once a session fiber is up, while the cached tools were
1247
+ * registered back at install. Dropping the handle restores every wrapper, so
1248
+ * nothing here keeps a snapshot of the tool list.
1249
+ */
1250
+ bindToolSurface(surface) {
1251
+ const handle = surface.register({
1252
+ source: PACKAGE_SOURCE,
1253
+ restrict: this.currentRestriction()
1254
+ });
1255
+ this.toolRestriction = handle;
1256
+ return () => {
1257
+ if (this.toolRestriction !== handle) return;
1258
+ this.toolRestriction = void 0;
1259
+ handle.dispose();
1260
+ };
1261
+ }
1262
+ createCatalog() {
1263
+ return new require_mcpCatalog.McpCatalog(readDirectToolFilter(this.options.environment));
1264
+ }
1265
+ /**
1266
+ * Registers the cached catalog with Pi. Runs at extension install.
1267
+ *
1268
+ * Returns the servers it seeded, so a caller can report what the session started
1269
+ * with before anything connected.
1270
+ */
1271
+ install(configuration = require_mcpCatalog.readSessionConfig(this.options.environment)) {
1272
+ this.executionCwd = configuration.repoRoot;
1273
+ this.bindConfiguration(configuration);
1274
+ this.emitChange();
1275
+ return this.catalog.toSnapshot();
1276
+ }
1277
+ /** Replaces the complete MCP projection and retires the previous live owner. */
1278
+ async reconfigure(configuration, executionCwd = configuration.repoRoot, workspaceRoot = configuration.repoRoot) {
1279
+ if (mcpConfigurationFingerprint(configuration) === this.configurationFingerprint && executionCwd === this.executionCwd && workspaceRoot === this.workspaceRoot) {
1280
+ if (!this.runtime?.getServices()) this.startDetached();
1281
+ return;
1282
+ }
1283
+ const generation = ++this.lifecycleGeneration;
1284
+ const retiredRuntime = this.runtime;
1285
+ this.runtime = void 0;
1286
+ await retiredRuntime?.dispose();
1287
+ if (generation !== this.lifecycleGeneration) return;
1288
+ this.resourceCache.clear();
1289
+ this.authorizationUrls.clear();
1290
+ this.browserAuthorizationRequests.clear();
1291
+ this.executionCwd = executionCwd;
1292
+ this.bindConfiguration(configuration);
1293
+ this.workspaceRoot = workspaceRoot;
1294
+ this.emitChange();
1295
+ this.updateToolVisibility();
1296
+ this.startDetached();
1297
+ }
1298
+ bindConfiguration(configuration) {
1299
+ this.workspaceRoot = configuration.repoRoot;
1300
+ this.configurationFingerprint = mcpConfigurationFingerprint(configuration);
1301
+ this.groups = require_mcpCatalog.buildMcpConfigGroups(configuration);
1302
+ this.disconnectedServers.clear();
1303
+ this.catalog = this.createCatalog();
1304
+ const configuredServers = [.../* @__PURE__ */ new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];
1305
+ this.catalog.seed(require_mcpCatalog.readCachedCatalog(this.configSources()), configuredServers);
1306
+ for (const diagnostic of this.groups.diagnostics) this.catalog.addDiagnostic(diagnostic);
1307
+ this.trackTools(this.catalog.allTools());
1308
+ }
1309
+ trackTools(tools) {
1310
+ for (const tool of tools) {
1311
+ const fingerprint = toolRegistrationFingerprint(tool);
1312
+ const registered = this.registeredToolFingerprints.get(tool.piName);
1313
+ this.historicallyOwnedNames.add(tool.piName);
1314
+ if (registered === void 0) {
1315
+ this.registeredToolFingerprints.set(tool.piName, fingerprint);
1316
+ this.incompatibleNames.delete(tool.piName);
1317
+ this.retainedTools.set(tool.piName, tool);
1318
+ continue;
1319
+ }
1320
+ if (registered === fingerprint) {
1321
+ this.incompatibleNames.delete(tool.piName);
1322
+ continue;
1323
+ }
1324
+ this.incompatibleNames.add(tool.piName);
1325
+ this.catalog.addDiagnostic(`MCP tool "${tool.piName}" changed identity, App metadata, or input schema and is hidden until Pi is relaunched.`);
1326
+ }
1327
+ }
1328
+ /** Tools the session currently permits on both local and remote surfaces. */
1329
+ activeToolDefinitions() {
1330
+ const active = new Set(this.catalog.activeToolNames());
1331
+ return this.toolDefinitions().flatMap((tool) => {
1332
+ const current = this.catalog.findTool(tool.piName);
1333
+ return active.has(tool.piName) && current !== void 0 ? [current] : [];
1334
+ });
1335
+ }
1336
+ toolDefinitions() {
1337
+ return [...this.retainedTools.values()].filter((tool) => this.catalog.findTool(tool.piName) !== void 0 && !this.incompatibleNames.has(tool.piName) && !this.disconnectedServers.has(tool.serverName));
1338
+ }
1339
+ isToolAvailable(registeredTool) {
1340
+ if (!this.catalog.activeToolNames().includes(registeredTool.piName)) return false;
1341
+ const current = this.catalog.findTool(registeredTool.piName);
1342
+ return current !== void 0 && toolRegistrationFingerprint(current) === toolRegistrationFingerprint(registeredTool);
1343
+ }
1344
+ get toolGeneration() {
1345
+ return this.lifecycleGeneration;
1346
+ }
1347
+ /** Capture a declaration's runtime, so retired wrappers cannot reach a same-name replacement. */
1348
+ bindToolInvocation(tool) {
1349
+ const generation = this.lifecycleGeneration;
1350
+ return async (parameters, signal) => {
1351
+ if (generation !== this.lifecycleGeneration || !this.isToolAvailable(tool)) throw new Error(`MCP tool ${tool.piName} is no longer available in the current session configuration.`);
1352
+ return toHeadlessToolResult(tool, await this.callTool(tool, parameters, signal));
1353
+ };
1354
+ }
1355
+ /** Executes through this session's existing manager, never a remote replacement runtime. */
1356
+ async invokeTool(name, parameters, signal) {
1357
+ const tool = this.activeToolDefinitions().find((candidate) => candidate.piName === name);
1358
+ if (!tool || !this.isToolAvailable(tool)) throw new Error(`MCP tool ${name} is not available in the current session configuration.`);
1359
+ return this.bindToolInvocation(tool)(parameters, signal);
1360
+ }
1361
+ /** Shared raw execution for Pi and headless, before their distinct result conventions. */
1362
+ async callTool(tool, parameters, signal) {
1363
+ const name = tool.piName;
1364
+ if (!this.isToolAvailable(tool)) throw new Error(`MCP tool ${name} is not available in the current session configuration.`);
1365
+ const runtime = this.runtime;
1366
+ const services = runtime?.getServices();
1367
+ if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);
1368
+ signal?.throwIfAborted();
1369
+ const { publishFile } = this.options;
1370
+ const files = await publishFileParams(tool, parameters, publishFile && ((path) => publishFile(path, tool, signal)));
1371
+ try {
1372
+ signal?.throwIfAborted();
1373
+ const connection = await services.clientManager.ensureConnected(tool.serverName);
1374
+ signal?.throwIfAborted();
1375
+ if (!runtime.isCurrent(services) || !this.isToolAvailable(tool)) throw new Error(`MCP tool ${name} is no longer available in the current session configuration.`);
1376
+ const timeout = services.clientManager.getServerRequestTimeout(tool.serverName);
1377
+ this.clearToolWarnings(tool.serverName, "tools/call", tool.toolName);
1378
+ let snapshot;
1379
+ const result = await connection.callTool(tool.toolName, files.outbound, {
1380
+ ...timeout === void 0 ? {} : { timeout },
1381
+ ...signal === void 0 ? {} : { signal },
1382
+ onResult: (original) => {
1383
+ snapshot = captureMcpAppResult(tool, original);
1384
+ }
1385
+ });
1386
+ signal?.throwIfAborted();
1387
+ if (!runtime.isCurrent(services) || !this.isToolAvailable(tool)) throw new Error(`MCP tool ${name} is no longer available in the current session configuration.`);
1388
+ retainMcpAppResult(result, snapshot);
1389
+ return result;
1390
+ } finally {
1391
+ files.release();
1392
+ }
1393
+ }
1394
+ /**
1395
+ * What this extension hides right now.
1396
+ *
1397
+ * Rebuilt from scratch every time, so a server that failed drops out and one
1398
+ * that reconnected comes back.
1399
+ */
1400
+ currentRestriction() {
1401
+ const unavailable = new Set(this.incompatibleNames);
1402
+ for (const tool of this.catalog.allTools()) if (this.disconnectedServers.has(tool.serverName)) unavailable.add(tool.piName);
1403
+ return mcpToolRestriction(this.catalog, this.historicallyOwnedNames, unavailable);
1404
+ }
1405
+ updateToolVisibility() {
1406
+ this.toolRestriction?.update(this.currentRestriction());
1407
+ }
1408
+ startDetached() {
1409
+ this.start().catch((error) => {
1410
+ this.catalog.addDiagnostic(`Could not start the MCP runtime: ${error instanceof Error ? error.message : String(error)}`);
1411
+ this.emitChange();
1412
+ });
1413
+ }
1414
+ /**
1415
+ * Starts connecting. Never awaited on Pi's critical path.
1416
+ *
1417
+ * Resolves once the container exists, not once servers are reachable. A start
1418
+ * that a newer generation superseded resolves to nothing and registers nothing.
1419
+ */
1420
+ async start() {
1421
+ const generation = ++this.lifecycleGeneration;
1422
+ this.disconnectedServers.clear();
1423
+ this.outputSchemaWarnings = [];
1424
+ this.emitChange();
1425
+ if (this.configuredServerNames().length === 0) return;
1426
+ const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([Promise.resolve().then(() => keyringTokenStore_exports), Promise.resolve().then(() => require("../mcpCatalog-6Tpt6Gqn.cjs")).then((n) => n.mcpRuntime_exports)]);
1427
+ if (generation !== this.lifecycleGeneration) return;
1428
+ this.runtime ??= new McpRuntimeOwner();
1429
+ const runtime = this.runtime;
1430
+ this.resourceCache.clear();
1431
+ const configSources = this.configSources();
1432
+ const tokenStore = this.options.tokenStore ?? await createTokenStore();
1433
+ this.tokenStore = tokenStore;
1434
+ await runtime.start({
1435
+ configSources,
1436
+ workspaceRoot: this.workspaceRoot,
1437
+ executionCwd: this.executionCwd,
1438
+ environment: this.options.environment,
1439
+ tokenStore,
1440
+ onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName, generation),
1441
+ onServerStateChange: (change) => this.onServerStateChange(change, generation),
1442
+ onOutputSchemaWarning: (warning) => {
1443
+ if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(warning.serverName) || !this.configuredServerNames().includes(warning.serverName)) return;
1444
+ if (this.outputSchemaWarnings.length >= 64) return;
1445
+ if (this.outputSchemaWarnings.some((current) => canonicalJson(current) === canonicalJson(warning))) return;
1446
+ this.outputSchemaWarnings.push(warning);
1447
+ this.emitChange();
1448
+ }
1449
+ });
1450
+ if (generation !== this.lifecycleGeneration) await runtime.dispose();
1451
+ }
1452
+ /**
1453
+ * Records the URL a flow is waiting on, then passes it to the host.
1454
+ *
1455
+ * Kept here because the status panel a user starts the flow from is where the
1456
+ * link is shown; hosts do not repeat it in the transcript. Host work is
1457
+ * detached so a failed desktop browser launch never cancels a still-usable OAuth
1458
+ * callback; the overlay retains a clickable fallback and a retry action.
1459
+ */
1460
+ onAuthorizationUrl(url, serverName, generation) {
1461
+ if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(serverName)) return;
1462
+ this.authorizationUrls.set(serverName, url.toString());
1463
+ this.emitChange();
1464
+ const openBrowser = this.browserAuthorizationRequests.has(serverName);
1465
+ Promise.resolve().then(() => {
1466
+ if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(serverName)) return;
1467
+ return this.options.onAuthorizationUrl?.(url, serverName, { openBrowser });
1468
+ }).catch((error) => {
1469
+ if (generation !== this.lifecycleGeneration) return;
1470
+ const detail = error instanceof Error ? error.message : String(error);
1471
+ this.catalog.addDiagnostic(`Could not open authorization page for "${serverName}": ${detail}`);
1472
+ this.emitChange();
1473
+ });
1474
+ }
1475
+ /** Ordered layers for the one in-process client container. */
1476
+ configSources() {
1477
+ return [...this.groups.sessionLocal.configSources, ...this.groups.shared.configSources];
1478
+ }
1479
+ configuredServerNames() {
1480
+ return [.../* @__PURE__ */ new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];
1481
+ }
1482
+ /** Closes only this session's connection. Saved OAuth credentials are untouched. */
1483
+ async disconnect(serverName) {
1484
+ if (!this.configuredServerNames().includes(serverName)) throw new Error(`Unknown MCP server: ${serverName}`);
1485
+ const clientManager = this.getClientManager();
1486
+ if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);
1487
+ const generation = this.lifecycleGeneration;
1488
+ this.disconnectedServers.add(serverName);
1489
+ try {
1490
+ await clientManager.disconnectServer(serverName);
1491
+ } catch (error) {
1492
+ if (generation === this.lifecycleGeneration) this.disconnectedServers.delete(serverName);
1493
+ throw error;
1494
+ }
1495
+ if (generation !== this.lifecycleGeneration) return;
1496
+ this.clearToolWarnings(serverName);
1497
+ this.resourceCache.delete(serverName);
1498
+ this.authorizationUrls.delete(serverName);
1499
+ this.browserAuthorizationRequests.delete(serverName);
1500
+ this.catalog.applyStateChange({
1501
+ serverName,
1502
+ state: "closed"
1503
+ }, []);
1504
+ this.emitChange();
1505
+ this.updateToolVisibility();
1506
+ }
1507
+ /**
1508
+ * Reconnects one server with a fresh OAuth registration.
1509
+ *
1510
+ * The connection is dropped before credentials are cleared so a failed disconnect
1511
+ * does not silently sign the user out. Clearing both tokens and dynamic client
1512
+ * information prevents a server-side registration reset from replaying a stale
1513
+ * `client_id` forever.
1514
+ */
1515
+ async reauthorize(serverName) {
1516
+ const clientManager = this.getClientManager();
1517
+ if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);
1518
+ const generation = this.lifecycleGeneration;
1519
+ const tokenStore = this.tokenStore;
1520
+ this.disconnectedServers.delete(serverName);
1521
+ this.resourceCache.delete(serverName);
1522
+ if (this.authorizationUrls.delete(serverName)) this.emitChange();
1523
+ this.browserAuthorizationRequests.add(serverName);
1524
+ try {
1525
+ await clientManager.disconnectServer(serverName);
1526
+ if (generation !== this.lifecycleGeneration) return;
1527
+ await tokenStore?.clear(serverName);
1528
+ if (generation !== this.lifecycleGeneration) return;
1529
+ await clientManager.ensureConnected(serverName);
1530
+ } finally {
1531
+ if (generation === this.lifecycleGeneration) this.browserAuthorizationRequests.delete(serverName);
1532
+ }
1533
+ }
1534
+ /** Reopens the URL already reserved for a flow without restarting that flow. */
1535
+ async openAuthorizationPage(serverName) {
1536
+ const authorizationUrl = this.authorizationUrls.get(serverName);
1537
+ if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);
1538
+ const onAuthorizationUrl = this.options.onAuthorizationUrl;
1539
+ if (!onAuthorizationUrl) throw new Error("This host cannot open authorization pages.");
1540
+ await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });
1541
+ }
1542
+ getSnapshot() {
1543
+ return this.catalog.toSnapshot();
1544
+ }
1545
+ resolveToolSelectors(selectors) {
1546
+ return this.catalog.resolveToolSelectors(selectors);
1547
+ }
1548
+ /** The fuller picture the overlay renders, with tool names and descriptions. */
1549
+ getServers() {
1550
+ return this.catalog.toView().map((server) => {
1551
+ const authorizationUrl = this.authorizationUrls.get(server.name);
1552
+ return authorizationUrl ? {
1553
+ ...server,
1554
+ authorizationUrl
1555
+ } : server;
1556
+ });
1557
+ }
1558
+ /** Both the proxy tool and direct tools point to the same live warning origins. */
1559
+ getToolWarnings() {
1560
+ const entries = this.activeToolDefinitions().filter(require_mcpCatalog.isModelVisibleMcpTool).flatMap((tool) => {
1561
+ const warnings = this.outputSchemaWarnings.filter((warning) => warning.serverName === tool.serverName && warning.toolName === tool.toolName).map((warning) => ({
1562
+ source: `${warning.serverName}/${warning.toolName} (${warning.method})`,
1563
+ path: warning.path,
1564
+ message: warning.message
1565
+ }));
1566
+ return warnings.length === 0 ? [] : [[tool.piName, warnings]];
1567
+ });
1568
+ if (entries.length === 0) return {};
1569
+ return Object.fromEntries([...entries, ["mcp_use", entries.flatMap(([, warnings]) => warnings)]]);
1570
+ }
1571
+ clearToolWarnings(serverName, method, toolName) {
1572
+ const remaining = this.outputSchemaWarnings.filter((warning) => warning.serverName !== serverName || method !== void 0 && warning.method !== method || toolName !== void 0 && warning.toolName !== toolName);
1573
+ if (remaining.length === this.outputSchemaWarnings.length) return;
1574
+ this.outputSchemaWarnings = remaining;
1575
+ this.emitChange();
1576
+ }
1577
+ /**
1578
+ * Hides or restores a server's tools for the rest of this session.
1579
+ *
1580
+ * Deliberately does not disconnect: this is a session visibility control, so a
1581
+ * later enable is instant and does not discard an authenticated live connection.
1582
+ * Because each catalog row is now an actual downstream server, the control acts
1583
+ * on only that server's tools rather than on an aggregate proxy group.
1584
+ *
1585
+ * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so
1586
+ * every tool stays registered and only the restriction moves.
1587
+ */
1588
+ setEnabled(serverName, enabled) {
1589
+ this.catalog.setDisabled(serverName, !enabled);
1590
+ this.emitChange();
1591
+ this.updateToolVisibility();
1592
+ }
1593
+ /**
1594
+ * Lists one server's resources, from cache unless asked for a fresh call.
1595
+ *
1596
+ * The cache lives here rather than in the overlay so closing and reopening the
1597
+ * panel does not re-dial. A rejection is never cached, so a server that has no
1598
+ * `resources/list` capability, or one that was briefly unreachable, is retried by
1599
+ * the next request rather than remembered as empty.
1600
+ */
1601
+ async listResources(serverName, options = {}) {
1602
+ if (this.disconnectedServers.has(serverName)) throw new Error(`MCP server is disconnected: ${serverName}`);
1603
+ if (!options.refresh) {
1604
+ const cached = this.resourceCache.get(serverName);
1605
+ if (cached) return cached;
1606
+ }
1607
+ const runtime = this.runtime;
1608
+ const services = runtime?.getServices();
1609
+ if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);
1610
+ const resources = await (await services.clientManager.ensureConnected(serverName)).listResources();
1611
+ if (!runtime.isCurrent(services)) return resources;
1612
+ this.resourceCache.set(serverName, resources);
1613
+ this.catalog.setResourceCount(serverName, resources.length);
1614
+ this.emitChange();
1615
+ return resources;
1616
+ }
1617
+ /** Reads only the resource advertised by a currently admitted tool, on its existing server. */
1618
+ async readAppResource(server, tool, resourceUri, signal) {
1619
+ const declaration = this.activeToolDefinitions().find((candidate) => candidate.serverName === server && candidate.toolName === tool);
1620
+ if (!declaration || require_mcpCatalog.normalizeMcpAppMetadata(declaration._meta).resourceUri !== resourceUri) throw new Error("This App resource is not available in the current session.");
1621
+ const runtime = this.runtime;
1622
+ const services = runtime?.getServices();
1623
+ const generation = this.lifecycleGeneration;
1624
+ if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);
1625
+ signal?.throwIfAborted();
1626
+ const connection = await services.clientManager.ensureConnected(server);
1627
+ const check = () => {
1628
+ signal?.throwIfAborted();
1629
+ if (generation !== this.lifecycleGeneration || !runtime.isCurrent(services) || !this.isToolAvailable(declaration)) throw new Error("This App resource belongs to a retired MCP runtime.");
1630
+ };
1631
+ check();
1632
+ const options = {
1633
+ timeout: services.clientManager.getServerRequestTimeout(server),
1634
+ signal
1635
+ };
1636
+ const result = await connection.readResource(resourceUri, options);
1637
+ check();
1638
+ return result;
1639
+ }
1640
+ /** Fires whenever the server picture changes. Returns its own disposer. */
1641
+ onChange(listener) {
1642
+ this.changeListeners.add(listener);
1643
+ return () => {
1644
+ this.changeListeners.delete(listener);
1645
+ };
1646
+ }
1647
+ /**
1648
+ * Announces a change to whoever is watching.
1649
+ *
1650
+ * Listener failures are contained rather than propagated: this runs inside the
1651
+ * continuation that folds a server into Pi's tool set, and a throw escaping here
1652
+ * would abandon that fold, leaving the session silently short of tools because
1653
+ * something failed to repaint.
1654
+ */
1655
+ emitChange() {
1656
+ for (const listener of this.changeListeners) try {
1657
+ listener();
1658
+ } catch (error) {
1659
+ this.catalog.addDiagnostic(`An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`);
1660
+ }
1661
+ }
1662
+ getDiagnostics() {
1663
+ return this.catalog.getDiagnostics();
1664
+ }
1665
+ async dispose() {
1666
+ ++this.lifecycleGeneration;
1667
+ this.browserAuthorizationRequests.clear();
1668
+ this.authorizationUrls.clear();
1669
+ this.resourceCache.clear();
1670
+ this.outputSchemaWarnings = [];
1671
+ const retiredRuntime = this.runtime;
1672
+ this.runtime = void 0;
1673
+ await retiredRuntime?.dispose();
1674
+ }
1675
+ /**
1676
+ * Folds one server transition into Pi's tool set.
1677
+ *
1678
+ * The tool list is read from the live connection rather than the state event,
1679
+ * which carries no tools; a server that is not readable contributes none and its
1680
+ * cached tools simply stay inactive.
1681
+ *
1682
+ * `ensureConnected` rather than `getClient`: the manager announces `connected`
1683
+ * before it registers the client, so a listener that looks the client up
1684
+ * synchronously finds nothing and the server lands with an empty tool list.
1685
+ * This handler runs detached from the connect, so awaiting it cannot deadlock.
1686
+ */
1687
+ onServerStateChange(change, generation) {
1688
+ if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(change.serverName)) return;
1689
+ if (change.state === "connecting") this.clearToolWarnings(change.serverName);
1690
+ if (change.state !== "connecting" && change.state !== "needs-auth") this.authorizationUrls.delete(change.serverName);
1691
+ const runtime = this.runtime;
1692
+ const services = runtime?.getServices();
1693
+ (change.state === "connected" && services ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools()) : Promise.resolve([])).then((tools) => ({
1694
+ tools,
1695
+ error: void 0
1696
+ }), () => ({
1697
+ tools: [],
1698
+ error: `Could not discover tools for MCP server "${change.serverName}".`
1699
+ })).then(({ tools, error }) => {
1700
+ if (generation !== this.lifecycleGeneration || !runtime?.isCurrent(services) || this.disconnectedServers.has(change.serverName)) return;
1701
+ if (error) this.catalog.addDiagnostic(error);
1702
+ this.catalog.applyStateChange(error ? {
1703
+ ...change,
1704
+ error
1705
+ } : change, tools);
1706
+ this.trackTools(this.catalog.allTools());
1707
+ this.emitChange();
1708
+ this.updateToolVisibility();
1709
+ });
1710
+ }
1711
+ };
1712
+ //#endregion
1713
+ //#region src/services/mcpToolCost/index.ts
1714
+ /**
1715
+ * What one downstream tool would cost if it were added to the model's tool list directly: its
1716
+ * name, description and input schema, priced the way the context panel prices every other tool.
1717
+ */
1718
+ function mcpToolTokens(tool, countTokens) {
1719
+ return countTokens(JSON.stringify({
1720
+ name: tool.piName,
1721
+ ...tool.description === void 0 ? {} : { description: tool.description },
1722
+ parameters: tool.inputSchema
1723
+ }));
1724
+ }
1725
+ //#endregion
1726
+ //#region src/services/mcpSessionApi/index.ts
1727
+ const JSON_HEADERS$1 = { "content-type": "application/json" };
1728
+ function json$1(body, status = 200) {
1729
+ return new Response(JSON.stringify(body), {
1730
+ status,
1731
+ headers: JSON_HEADERS$1
1732
+ });
1733
+ }
1734
+ /**
1735
+ * This package's session mount, `mcp-session`: one reachable tool's description and schema.
1736
+ *
1737
+ * Read-only and bounded to available tools, so a page can inspect what the agent may
1738
+ * call without the status line carrying every schema.
1739
+ */
1740
+ function createMcpSessionApi(source, countTokens) {
1741
+ return {
1742
+ basePath: "mcp-session",
1743
+ start() {
1744
+ return {
1745
+ async fetch(request) {
1746
+ const url = new URL(request.url);
1747
+ if (request.method !== "GET" || url.pathname !== "/tool") return json$1({ error: "Not found." }, 404);
1748
+ const server = url.searchParams.get("server") ?? "";
1749
+ const name = url.searchParams.get("tool") ?? "";
1750
+ if (!server || !name) return json$1({ error: "A tool is named by its server and tool name." }, 400);
1751
+ const tool = source.activeToolDefinitions().find((candidate) => candidate.serverName === server && candidate.toolName === name);
1752
+ if (!tool) return json$1({ error: `MCP tool ${server}/${name} is not available in this session.` }, 404);
1753
+ const warnings = source.getToolWarnings?.()[tool.piName];
1754
+ return json$1({
1755
+ server,
1756
+ tool: name,
1757
+ piName: tool.piName,
1758
+ ...warnings?.length ? { warnings } : {},
1759
+ ...tool.description === void 0 ? {} : { description: tool.description },
1760
+ inputSchema: tool.inputSchema,
1761
+ ...tool._meta === void 0 ? {} : { _meta: tool._meta },
1762
+ tokens: mcpToolTokens(tool, await countTokens())
1763
+ });
1764
+ },
1765
+ close: () => void 0
1766
+ };
1767
+ }
1768
+ };
1769
+ }
1770
+ //#endregion
1771
+ //#region src/services/serverRuntime/index.ts
1772
+ function sessionConfiguration(execution, workspaceRoot) {
1773
+ if (workspaceRoot !== void 0) {
1774
+ const loaded = (0, _agimon_ai_po_config_harnessStore.loadHarnessState)({ ...execution.environment });
1775
+ const projection = loaded.state.mcpProjection;
1776
+ if (loaded.filePath && loaded.state.root === workspaceRoot && execution.repoRoot === workspaceRoot && loaded.state.mcp && loaded.state.majorMode === execution.selection.majorMode && JSON.stringify(loaded.state.domains) === JSON.stringify(execution.selection.domains) && JSON.stringify(loaded.state.layers) === JSON.stringify(execution.selection.activeLayers) && (0, _agimon_ai_po_core_mcpProjection.isDoomMcpProjection)(projection) && projection.repoRoot === workspaceRoot) return require_projection.mcpSessionConfigFromProjection(projection, execution.cwd);
1777
+ return {
1778
+ enabled: false,
1779
+ repoRoot: execution.cwd,
1780
+ stagingDirectory: node_path.default.join(execution.cwd, ".po", "mcp-disabled"),
1781
+ sources: []
1782
+ };
1783
+ }
1784
+ return require_mcpCatalog.readSessionConfig(execution.environment, execution.cwd);
1785
+ }
1786
+ function createMcpServerRuntime(environment = {}, workspaceRoot, getResolver, publishFile) {
1787
+ let active;
1788
+ let currentSelection;
1789
+ let staleSelection = false;
1790
+ let cleanup;
1791
+ let transition = Promise.resolve();
1792
+ const authorizing = /* @__PURE__ */ new Map();
1793
+ const session = new McpSession({
1794
+ environment: { ...environment },
1795
+ ...publishFile === void 0 ? {} : { publishFile }
1796
+ });
1797
+ const assertAvailable = () => {
1798
+ if (!active) throw new Error("The MCP runtime has not started yet.");
1799
+ if (staleSelection) throw new Error("The MCP projection is stale for the current selection.");
1800
+ };
1801
+ const sessionTools = createMcpSessionToolsService(session, crypto.randomUUID(), assertAvailable);
1802
+ const refresh = (execution, selection) => {
1803
+ staleSelection = true;
1804
+ sessionTools.refresh();
1805
+ authorizing.clear();
1806
+ transition = transition.catch(() => {}).then(async () => {
1807
+ if (active !== execution) return;
1808
+ const previousCleanup = cleanup;
1809
+ cleanup = void 0;
1810
+ try {
1811
+ await session.dispose();
1812
+ } finally {
1813
+ session.install({
1814
+ enabled: false,
1815
+ repoRoot: execution.cwd,
1816
+ stagingDirectory: execution.cwd
1817
+ });
1818
+ await previousCleanup?.();
1819
+ }
1820
+ if (active !== execution) return;
1821
+ const resolver = getResolver?.();
1822
+ const loaded = workspaceRoot ? (0, _agimon_ai_po_config_harnessStore.loadHarnessState)({ ...execution.environment }) : void 0;
1823
+ const authorized = workspaceRoot && loaded?.filePath && loaded.state.root === workspaceRoot && execution.repoRoot === workspaceRoot && loaded.state.mcp && (0, _agimon_ai_po_core_mcpProjection.isDoomMcpProjection)(loaded.state.mcpProjection) && loaded.state.mcpProjection.repoRoot === workspaceRoot && loaded.state.mcpProjection.enabled;
1824
+ if (!resolver || !authorized) {
1825
+ if (active === execution && selection === currentSelection) {
1826
+ if (workspaceRoot === void 0 || JSON.stringify(execution.selection.domains) === JSON.stringify(selection.domains)) await session.reconfigure(sessionConfiguration(execution, workspaceRoot), execution.cwd, execution.repoRoot);
1827
+ staleSelection = workspaceRoot !== void 0 && JSON.stringify(execution.selection.domains) !== JSON.stringify(selection.domains);
1828
+ }
1829
+ return;
1830
+ }
1831
+ const candidate = await resolver.resolve(selection.domains);
1832
+ if (active !== execution || selection !== currentSelection) {
1833
+ await candidate.cleanup();
1834
+ return;
1835
+ }
1836
+ try {
1837
+ if (!(0, _agimon_ai_po_core_mcpProjection.isDoomMcpProjection)(candidate.projection) || candidate.projection.repoRoot !== workspaceRoot || !candidate.projection.enabled) throw new Error("The selected domains returned an invalid MCP projection.");
1838
+ await session.reconfigure(require_projection.mcpSessionConfigFromProjection(candidate.projection, execution.cwd), execution.cwd, execution.repoRoot);
1839
+ cleanup = () => candidate.cleanup();
1840
+ if (active === execution && selection === currentSelection) staleSelection = false;
1841
+ } catch (error) {
1842
+ await candidate.cleanup();
1843
+ throw error;
1844
+ }
1845
+ }).then(() => sessionTools.refresh()).catch(async (error) => {
1846
+ if (active !== execution || selection !== currentSelection) return;
1847
+ await execution.client.notify({
1848
+ title: "Po MCP unavailable",
1849
+ body: error instanceof Error ? error.message : String(error),
1850
+ level: "warning"
1851
+ });
1852
+ });
1853
+ return transition;
1854
+ };
1855
+ let countTokens;
1856
+ let tokenizer;
1857
+ const loadTokenizer = () => {
1858
+ tokenizer ??= import("gpt-tokenizer").then((module) => {
1859
+ countTokens = module.countTokens;
1860
+ return module.countTokens;
1861
+ });
1862
+ return tokenizer;
1863
+ };
1864
+ const toolTokens = /* @__PURE__ */ new Map();
1865
+ /** Each reachable tool's schema cost, keyed by its registered name, once the tokenizer is loaded. */
1866
+ const reachableTokens = () => {
1867
+ if (countTokens === void 0) return toolTokens;
1868
+ for (const tool of session.activeToolDefinitions()) if (!toolTokens.has(tool.piName)) toolTokens.set(tool.piName, mcpToolTokens(tool, countTokens));
1869
+ return toolTokens;
1870
+ };
1871
+ const api = createMcpSessionApi(session, loadTokenizer);
1872
+ const activity = {
1873
+ name: "po-mcp-runtime",
1874
+ async start(execution) {
1875
+ active = execution;
1876
+ currentSelection = execution.selection;
1877
+ staleSelection = false;
1878
+ const reported = /* @__PURE__ */ new Set();
1879
+ const publish = () => {
1880
+ execution.client.setStatus(MCP_STATUS_KEY, session.getServers().map((server) => server.name).join(","));
1881
+ const tokens = reachableTokens();
1882
+ execution.client.setStatus(MCP_SESSION_AUTH_STATUS_KEY, formatMcpSessionAuthStatus(session.getServers().map((server) => ({
1883
+ ...server,
1884
+ tools: server.tools.map((tool) => {
1885
+ const cost = tokens.get(tool.piName);
1886
+ return cost === void 0 ? tool : {
1887
+ ...tool,
1888
+ tokens: cost
1889
+ };
1890
+ })
1891
+ }))));
1892
+ const warnings = session.getToolWarnings();
1893
+ execution.client.setStatus(_agimon_ai_po_core_contextApi.CONTEXT_TOOL_WARNINGS_STATUS_KEY, Object.keys(warnings).length === 0 ? void 0 : JSON.stringify(warnings));
1894
+ for (const diagnostic of session.getDiagnostics()) {
1895
+ if (reported.has(diagnostic)) continue;
1896
+ reported.add(diagnostic);
1897
+ execution.client.notify({
1898
+ title: "Po MCP",
1899
+ body: diagnostic,
1900
+ level: "warning"
1901
+ });
1902
+ }
1903
+ };
1904
+ const stopPublishing = session.onChange(() => {
1905
+ toolTokens.clear();
1906
+ publish();
1907
+ });
1908
+ loadTokenizer().then(() => {
1909
+ if (active === execution) publish();
1910
+ });
1911
+ try {
1912
+ await refresh(execution, execution.selection);
1913
+ publish();
1914
+ } catch (error) {
1915
+ await execution.client.notify({
1916
+ title: "Po MCP unavailable",
1917
+ body: error instanceof Error ? error.message : String(error),
1918
+ level: "warning"
1919
+ });
1920
+ }
1921
+ return async () => {
1922
+ stopPublishing();
1923
+ if (active !== execution) return;
1924
+ active = void 0;
1925
+ currentSelection = void 0;
1926
+ staleSelection = true;
1927
+ sessionTools.refresh();
1928
+ authorizing.clear();
1929
+ await transition;
1930
+ const retiredCleanup = cleanup;
1931
+ cleanup = void 0;
1932
+ try {
1933
+ await session.dispose();
1934
+ } finally {
1935
+ session.install({
1936
+ enabled: false,
1937
+ repoRoot: execution.cwd,
1938
+ stagingDirectory: execution.cwd
1939
+ });
1940
+ await retiredCleanup?.();
1941
+ execution.client.setStatus(MCP_STATUS_KEY, void 0);
1942
+ execution.client.setStatus(MCP_SESSION_AUTH_STATUS_KEY, void 0);
1943
+ execution.client.setStatus(_agimon_ai_po_core_contextApi.CONTEXT_TOOL_WARNINGS_STATUS_KEY, void 0);
1944
+ }
1945
+ };
1946
+ }
1947
+ };
1948
+ const invoke = async (parameters, signal) => {
1949
+ assertAvailable();
1950
+ const selected = sessionTools.snapshot().filter(require_mcpCatalog.isModelVisibleMcpTool).find((candidate) => candidate.serverName === parameters.server && candidate.toolName === parameters.tool);
1951
+ if (!selected) throw new Error(`MCP tool ${parameters.server}/${parameters.tool} is not available in this session.`);
1952
+ return sessionTools.invoke(selected.piName, parameters.arguments ?? {}, signal);
1953
+ };
1954
+ const childTool = createMcpChildTool(invoke, {
1955
+ snapshot: () => sessionTools.project().filter(require_mcpCatalog.isModelVisibleMcpTool),
1956
+ resolveSelectors: (selectors) => sessionTools.resolveSelectors(selectors),
1957
+ subscribe: (listener) => sessionTools.onChange(listener)
1958
+ });
1959
+ return {
1960
+ session,
1961
+ sessionTools,
1962
+ api: [api],
1963
+ activities: [activity],
1964
+ commands: [{
1965
+ name: "mcp",
1966
+ description: SERVER_COMMAND_DESCRIPTION,
1967
+ async execute(args, execution) {
1968
+ const [subcommand = "status", ...rest] = args.trim().split(/\s+/u).filter(Boolean);
1969
+ const serverName = rest.join(" ");
1970
+ const notify = (body, level = "info") => execution.client.notify({
1971
+ title: "Po MCP",
1972
+ body,
1973
+ level
1974
+ });
1975
+ try {
1976
+ if (subcommand === "status") {
1977
+ await notify(formatStatus(session));
1978
+ return;
1979
+ }
1980
+ if (!active) throw new Error("The MCP runtime has not started yet.");
1981
+ if (staleSelection) throw new Error("The MCP projection is stale for the current selection.");
1982
+ if (subcommand === "auth" || subcommand === "disconnect") {
1983
+ if (!serverName) throw new Error(`Name the server, for example /mcp ${subcommand} <server>.`);
1984
+ const server = session.getServers().find((candidate) => candidate.name === serverName);
1985
+ if (!server) throw new Error(`Unknown MCP server: ${serverName}`);
1986
+ if (subcommand === "disconnect") {
1987
+ await session.disconnect(serverName);
1988
+ authorizing.delete(serverName);
1989
+ await notify(`${serverName} is disconnected from this session. Saved credentials were kept.`);
1990
+ return;
1991
+ }
1992
+ if (server.authorizationUrl) {
1993
+ await notify(`${serverName} is waiting for sign-in. Open it from the MCP servers panel.`);
1994
+ return;
1995
+ }
1996
+ if (authorizing.has(serverName)) return;
1997
+ const request = Symbol(serverName);
1998
+ authorizing.set(serverName, request);
1999
+ const owner = active;
2000
+ session.reauthorize(serverName).then(async () => {
2001
+ if (active !== owner || authorizing.get(serverName) !== request) return;
2002
+ authorizing.delete(serverName);
2003
+ await notify(`${serverName} is authorized.`);
2004
+ }, async (error) => {
2005
+ if (active !== owner || authorizing.get(serverName) !== request) return;
2006
+ authorizing.delete(serverName);
2007
+ await notify(`Could not authorize ${serverName}: ${error instanceof Error ? error.message : String(error)}`, "warning");
2008
+ });
2009
+ return;
2010
+ }
2011
+ if (subcommand === "reload") {
2012
+ if (!currentSelection) throw new Error("The MCP runtime has not started yet.");
2013
+ await refresh(active, currentSelection);
2014
+ if (!staleSelection) await notify("Reconnecting MCP servers.");
2015
+ return;
2016
+ }
2017
+ throw new Error(`Unknown /mcp subcommand "${subcommand}". Use status, auth, disconnect, reload.`);
2018
+ } catch (error) {
2019
+ await notify(error instanceof Error ? error.message : String(error), "warning");
2020
+ }
2021
+ }
2022
+ }],
2023
+ tools: [{
2024
+ name: "mcp_use",
2025
+ label: "MCP use",
2026
+ description: "Call a tool exposed by a connected MCP server.",
2027
+ parameters: McpHeadlessToolParameters,
2028
+ executionMode: "serial",
2029
+ async execute(_toolCallId, parameters, signal) {
2030
+ try {
2031
+ return await invoke(parameters, signal);
2032
+ } catch (error) {
2033
+ return {
2034
+ content: [{
2035
+ type: "text",
2036
+ text: error instanceof Error ? error.message : String(error)
2037
+ }],
2038
+ isError: true
2039
+ };
2040
+ }
2041
+ }
2042
+ }],
2043
+ childTool,
2044
+ async onSelectionChange(selection) {
2045
+ if (!active || workspaceRoot === void 0) return;
2046
+ if (JSON.stringify(currentSelection) === JSON.stringify(selection)) return;
2047
+ currentSelection = selection;
2048
+ await refresh(active, selection);
2049
+ }
2050
+ };
2051
+ }
2052
+ //#endregion
2053
+ //#region src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts
2054
+ const createMcpServerRoot = (context) => {
2055
+ let resolver;
2056
+ let appHost = context?.agent;
2057
+ const hostContext = context?.host.context;
2058
+ const publishFile = hostContext?.shareFile === void 0 ? void 0 : createConsentedFilePublisher({
2059
+ shareFile: (path, label) => hostContext.shareFile(path, label),
2060
+ host: () => appHost,
2061
+ signal: context?.signal
2062
+ });
2063
+ const runtime = createMcpServerRuntime(context?.agent?.context?.environment, context?.host.context.workspaceRoot, () => resolver, publishFile);
2064
+ const generation = `${context?.agent?.context?.sessionId ?? crypto.randomUUID()}:mcp-server`;
2065
+ const { session } = runtime;
2066
+ const tools = [...runtime.tools, ...createFileTransferTools(context)];
2067
+ const apps = createMcpAppsService(runtime.sessionTools, () => appHost, context?.signal);
2068
+ const mountSession = (cordis) => {
2069
+ cordis.inject([_agimon_ai_po_core_mcpProjection.DOOM_MCP_PROJECTION_RESOLVER_SERVICE], (resolverContext) => {
2070
+ resolver = resolverContext.get(_agimon_ai_po_core_mcpProjection.DOOM_MCP_PROJECTION_RESOLVER_SERVICE);
2071
+ return () => {
2072
+ resolver = void 0;
2073
+ };
2074
+ });
2075
+ cordis.inject([_agimon_ai_po_core_headless.DOOM_HEADLESS_HOST_SERVICE], (hostContext) => {
2076
+ const host = hostContext.get(_agimon_ai_po_core_headless.DOOM_HEADLESS_HOST_SERVICE);
2077
+ appHost = host;
2078
+ const registrations = /* @__PURE__ */ new Map();
2079
+ const reconcile = () => {
2080
+ const tools = runtime.sessionTools.project();
2081
+ const names = new Set(tools.map((tool) => tool.name));
2082
+ for (const [name, entry] of registrations) {
2083
+ if (names.has(name)) continue;
2084
+ entry.registration.dispose();
2085
+ registrations.delete(name);
2086
+ }
2087
+ for (const tool of tools) {
2088
+ const previous = registrations.get(tool.name);
2089
+ if (previous?.tool === tool) continue;
2090
+ previous?.registration.dispose();
2091
+ registrations.set(tool.name, {
2092
+ tool,
2093
+ registration: host.registerTool(tool)
2094
+ });
2095
+ }
2096
+ };
2097
+ const stopTools = runtime.sessionTools.onChange(reconcile);
2098
+ const stopSelection = host.subscribeSelection((selection) => runtime.onSelectionChange(selection));
2099
+ reconcile();
2100
+ return () => {
2101
+ if (appHost === host) appHost = void 0;
2102
+ stopTools();
2103
+ stopSelection();
2104
+ for (const entry of registrations.values()) entry.registration.dispose();
2105
+ registrations.clear();
2106
+ };
2107
+ });
2108
+ cordis.plugin((provider) => {
2109
+ provider.provide(_agimon_ai_po_core_mcpStatus.DOOM_MCP_STATUS_SERVICE, {
2110
+ generation,
2111
+ getSnapshot: () => session.getSnapshot(),
2112
+ onChange: (listener) => session.onChange(listener)
2113
+ });
2114
+ provider.provide(_agimon_ai_po_core_mcpToolResolver.DOOM_MCP_TOOL_RESOLVER_SERVICE, {
2115
+ generation,
2116
+ resolve: (selectors) => session.resolveToolSelectors(selectors)
2117
+ });
2118
+ provider.provide(MCP_SESSION_TOOLS_SERVICE, runtime.sessionTools);
2119
+ provider.provide(_agimon_ai_po_core_childSession.DOOM_CHILD_SESSION_MCP_TOOL_SERVICE, runtime.childTool);
2120
+ return () => runtime.sessionTools.dispose();
2121
+ });
2122
+ };
2123
+ return {
2124
+ value: {
2125
+ ...runtime,
2126
+ appMethods: apps.methods
2127
+ },
2128
+ activities: runtime.activities,
2129
+ services: [mountMcpHeadlessTools(tools), mountSession],
2130
+ onDispose: apps.dispose
2131
+ };
2132
+ };
2133
+ //#endregion
2134
+ //#region src/extensions/workspaces/sessions/(backend)/root.server.ts
2135
+ var root_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoot)(createMcpServerRoot);
2136
+ //#endregion
2137
+ //#region src/services/keyringTokenStore/index.ts
2138
+ var keyringTokenStore_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
2139
+ KEYRING_SERVICE: () => KEYRING_SERVICE,
2140
+ KeyringTokenStore: () => KeyringTokenStore,
2141
+ createTokenStore: () => createTokenStore
2142
+ });
2143
+ /** Keyring service name. One credential per downstream MCP server. */
2144
+ const KEYRING_SERVICE = "ai.agimon.doom-mcp.oauth";
2145
+ /**
2146
+ * OAuth credentials in the OS keyring, with an owner-only file store behind it.
2147
+ *
2148
+ * The keyring is a native module and the platform service it talks to may be absent
2149
+ * (a headless Linux box with no secret service, an unsupported architecture). A
2150
+ * missing credential is reported as `undefined` rather than raised, so a rejection
2151
+ * means the keyring itself is unusable and the store degrades to the file fallback
2152
+ * for the rest of the session instead of failing the run.
2153
+ */
2154
+ var KeyringTokenStore = class {
2155
+ createEntry;
2156
+ fallback;
2157
+ degraded = false;
2158
+ constructor(createEntry, fallback = new _agimon_ai_mcp_proxy.FileTokenStore()) {
2159
+ this.createEntry = createEntry;
2160
+ this.fallback = fallback;
2161
+ }
2162
+ /** True once the keyring has failed and the file store has taken over. */
2163
+ get isDegraded() {
2164
+ return this.degraded;
2165
+ }
2166
+ async read(serverName) {
2167
+ if (this.degraded) return this.fallback.read(serverName);
2168
+ let raw;
2169
+ try {
2170
+ raw = await this.createEntry(KEYRING_SERVICE, serverName).getPassword();
2171
+ } catch {
2172
+ this.degraded = true;
2173
+ return this.fallback.read(serverName);
2174
+ }
2175
+ if (!raw) return void 0;
2176
+ try {
2177
+ return JSON.parse(raw);
2178
+ } catch {
2179
+ return;
2180
+ }
2181
+ }
2182
+ async write(serverName, entry) {
2183
+ if (!this.degraded) try {
2184
+ await this.createEntry(KEYRING_SERVICE, serverName).setPassword(JSON.stringify(entry));
2185
+ return;
2186
+ } catch {
2187
+ this.degraded = true;
2188
+ }
2189
+ await this.fallback.write(serverName, entry);
2190
+ }
2191
+ /**
2192
+ * Removes the credential from both stores.
2193
+ *
2194
+ * A session that degraded part way through can have left a copy in either place,
2195
+ * and "signed out" has to mean no credential survives anywhere. A keyring that
2196
+ * refuses the delete is reported rather than absorbed: the caller was told the
2197
+ * credential is gone, and it is not. The fallback is still cleared first so the
2198
+ * failure leaves one surviving copy instead of two.
2199
+ */
2200
+ async clear(serverName) {
2201
+ let keyringFailure;
2202
+ if (!this.degraded) try {
2203
+ await this.createEntry(KEYRING_SERVICE, serverName).deletePassword();
2204
+ } catch (error) {
2205
+ keyringFailure = error;
2206
+ }
2207
+ await this.fallback.clear(serverName);
2208
+ if (keyringFailure !== void 0) {
2209
+ const detail = keyringFailure instanceof Error ? keyringFailure.message : JSON.stringify(keyringFailure);
2210
+ throw new Error(`Keyring credential for "${serverName}" could not be removed: ${detail}`, { cause: keyringFailure });
2211
+ }
2212
+ }
2213
+ };
2214
+ /**
2215
+ * Builds the token store for this host, falling back when the native module is absent.
2216
+ *
2217
+ * doom-mcp loads inside Pi, so a platform without a prebuilt keyring binary must
2218
+ * still get a working session rather than a failed extension install.
2219
+ */
2220
+ async function createTokenStore(fallback = new _agimon_ai_mcp_proxy.FileTokenStore()) {
2221
+ try {
2222
+ const { AsyncEntry } = await import("@napi-rs/keyring");
2223
+ return new KeyringTokenStore((service, account) => new AsyncEntry(service, account), fallback);
2224
+ } catch {
2225
+ return fallback;
2226
+ }
2227
+ }
2228
+ //#endregion
2229
+ //#region src/services/mcpSettingsManager/index.ts
2230
+ const DISCOVERY_TIMEOUT_MS = 3e4;
2231
+ const AUTHORIZATION_TIMEOUT_MS = 3e5;
2232
+ const TERMINAL_FLOW_RETENTION_MS = 3e5;
2233
+ const EMPTY_SYNC = {
2234
+ fresh: false,
2235
+ reasons: ["never-synced"]
2236
+ };
2237
+ function configuredServerNames(groups) {
2238
+ return [.../* @__PURE__ */ new Set([...groups.sessionLocal.serverNames, ...groups.shared.serverNames])].sort((left, right) => left.localeCompare(right));
2239
+ }
2240
+ function configSources(groups) {
2241
+ return [...groups.sessionLocal.configSources, ...groups.shared.configSources];
2242
+ }
2243
+ function toolView(serverName, tool) {
2244
+ return {
2245
+ name: tool.name,
2246
+ piName: require_mcpCatalog.toPiToolName(serverName, tool.name),
2247
+ ...tool.description ? { description: tool.description } : {}
2248
+ };
2249
+ }
2250
+ function sanitizeDiagnostic(message, repositoryRoot, stagingDirectory) {
2251
+ let safe = message;
2252
+ for (const value of [
2253
+ stagingDirectory,
2254
+ repositoryRoot,
2255
+ node_os.default.homedir()
2256
+ ]) if (value) safe = safe.split(value).join(value === node_os.default.homedir() ? "~" : "<repository>");
2257
+ return safe.replace(/[A-Za-z]:\\[^\s,;)'"`]+/gu, "<path>").replace(/(^|\s)\/(?!\/)[^\s,;)'"`]+/gu, "$1<path>");
2258
+ }
2259
+ function publicFlow(flow) {
2260
+ return {
2261
+ id: flow.id,
2262
+ repositoryId: flow.repositoryId,
2263
+ serverName: flow.serverName,
2264
+ status: flow.status,
2265
+ ...flow.authorizationUrl ? { authorizationUrl: flow.authorizationUrl } : {},
2266
+ ...flow.error ? { error: flow.error } : {},
2267
+ expiresAt: flow.expiresAt
2268
+ };
2269
+ }
2270
+ function isTerminal(status) {
2271
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "expired";
2272
+ }
2273
+ function safeAuthorizationUrl(url) {
2274
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.toString().length > 4096) return void 0;
2275
+ return url.toString();
2276
+ }
2277
+ var McpSettingsManager = class {
2278
+ discoveryTimeoutMs;
2279
+ authorizationTimeoutMs;
2280
+ injectedTokenStore;
2281
+ busyRepositories = /* @__PURE__ */ new Set();
2282
+ flows = /* @__PURE__ */ new Map();
2283
+ constructor(options = {}) {
2284
+ this.discoveryTimeoutMs = options.discoveryTimeoutMs ?? DISCOVERY_TIMEOUT_MS;
2285
+ this.authorizationTimeoutMs = options.authorizationTimeoutMs ?? AUTHORIZATION_TIMEOUT_MS;
2286
+ this.injectedTokenStore = options.tokenStore;
2287
+ }
2288
+ async tokenStore() {
2289
+ return this.injectedTokenStore ?? await createTokenStore();
2290
+ }
2291
+ async credentialPresence(serverNames) {
2292
+ const presence = /* @__PURE__ */ new Map();
2293
+ try {
2294
+ const tokenStore = await this.tokenStore();
2295
+ await Promise.all(serverNames.map(async (serverName) => {
2296
+ try {
2297
+ presence.set(serverName, await tokenStore.read(serverName) !== void 0);
2298
+ } catch {
2299
+ presence.set(serverName, false);
2300
+ }
2301
+ }));
2302
+ } catch {
2303
+ for (const serverName of serverNames) presence.set(serverName, false);
2304
+ }
2305
+ return presence;
2306
+ }
2307
+ prepare(repositoryId, repositoryRoot, sync, onlyServer) {
2308
+ if (!sync?.mcpProjection) {
2309
+ const readiness = sync ?? EMPTY_SYNC;
2310
+ return {
2311
+ catalog: {
2312
+ repositoryId,
2313
+ sync: {
2314
+ fresh: readiness.fresh,
2315
+ reasons: [...readiness.reasons]
2316
+ },
2317
+ servers: [],
2318
+ droppedServers: [],
2319
+ diagnostics: []
2320
+ },
2321
+ configSources: [],
2322
+ groups: {
2323
+ shared: {
2324
+ configSources: [],
2325
+ configPaths: [],
2326
+ serverNames: []
2327
+ },
2328
+ sessionLocal: {
2329
+ configSources: [],
2330
+ configPaths: [],
2331
+ serverNames: []
2332
+ },
2333
+ droppedServers: [],
2334
+ diagnostics: []
2335
+ }
2336
+ };
2337
+ }
2338
+ const stagingDirectory = node_fs.default.mkdtempSync(node_path.default.join(node_os.default.tmpdir(), "po-mcp-settings-"));
2339
+ node_fs.default.chmodSync(stagingDirectory, 448);
2340
+ try {
2341
+ const configuration = {
2342
+ ...require_projection.mcpSessionConfigFromProjection(sync.mcpProjection),
2343
+ repoRoot: repositoryRoot,
2344
+ stagingDirectory,
2345
+ ...onlyServer ? { allowlist: {
2346
+ servers: [onlyServer],
2347
+ proxy: []
2348
+ } } : {}
2349
+ };
2350
+ const groups = require_mcpCatalog.buildMcpConfigGroups(configuration);
2351
+ const sources = configSources(groups);
2352
+ const names = configuredServerNames(groups);
2353
+ const cachedByServer = new Map(require_mcpCatalog.readCachedCatalog(sources).servers.map((server) => [server.name, server.tools]));
2354
+ const servers = names.map((name) => {
2355
+ const tools = cachedByServer.get(name) ?? [];
2356
+ return {
2357
+ name,
2358
+ state: "not-connected",
2359
+ source: tools.length === 0 ? "configured" : "cached",
2360
+ credentialPresent: false,
2361
+ tools: tools.map((tool) => toolView(name, tool))
2362
+ };
2363
+ });
2364
+ return {
2365
+ catalog: {
2366
+ repositoryId,
2367
+ sync: {
2368
+ fresh: sync.fresh,
2369
+ reasons: [...sync.reasons]
2370
+ },
2371
+ servers,
2372
+ droppedServers: [...groups.droppedServers],
2373
+ diagnostics: groups.diagnostics.map((diagnostic) => sanitizeDiagnostic(diagnostic, repositoryRoot, stagingDirectory))
2374
+ },
2375
+ configSources: sources,
2376
+ groups,
2377
+ stagingDirectory
2378
+ };
2379
+ } catch (error) {
2380
+ node_fs.default.rmSync(stagingDirectory, {
2381
+ recursive: true,
2382
+ force: true
2383
+ });
2384
+ throw error;
2385
+ }
2386
+ }
2387
+ cleanupPrepared(prepared) {
2388
+ if (prepared.stagingDirectory) node_fs.default.rmSync(prepared.stagingDirectory, {
2389
+ recursive: true,
2390
+ force: true
2391
+ });
2392
+ }
2393
+ async readCatalog(repositoryId, repositoryRoot, sync) {
2394
+ const prepared = this.prepare(repositoryId, repositoryRoot, sync);
2395
+ try {
2396
+ const presence = await this.credentialPresence(prepared.catalog.servers.map((server) => server.name));
2397
+ return {
2398
+ ...prepared.catalog,
2399
+ servers: prepared.catalog.servers.map((server) => ({
2400
+ ...server,
2401
+ credentialPresent: presence.get(server.name) ?? false
2402
+ }))
2403
+ };
2404
+ } finally {
2405
+ this.cleanupPrepared(prepared);
2406
+ }
2407
+ }
2408
+ async discover(repositoryId, repositoryRoot, sync) {
2409
+ if (this.busyRepositories.has(repositoryId)) throw new Error("MCP management is already active for this repository.");
2410
+ const prepared = this.prepare(repositoryId, repositoryRoot, sync);
2411
+ if (!prepared.catalog.sync.fresh) {
2412
+ this.cleanupPrepared(prepared);
2413
+ throw new Error("Sync this repository before discovering MCP capabilities.");
2414
+ }
2415
+ if (prepared.configSources.length === 0) {
2416
+ this.cleanupPrepared(prepared);
2417
+ return prepared.catalog;
2418
+ }
2419
+ this.busyRepositories.add(repositoryId);
2420
+ const owner = new require_mcpCatalog.McpRuntimeOwner();
2421
+ const liveStates = /* @__PURE__ */ new Map();
2422
+ try {
2423
+ const handle = await owner.start({
2424
+ configSources: prepared.configSources,
2425
+ workspaceRoot: repositoryRoot,
2426
+ executionCwd: repositoryRoot,
2427
+ tokenStore: await this.tokenStore(),
2428
+ onAuthorizationUrl: (_url, serverName) => {
2429
+ liveStates.set(serverName, {
2430
+ serverName,
2431
+ state: "needs-auth"
2432
+ });
2433
+ },
2434
+ onServerStateChange: (change) => liveStates.set(change.serverName, change)
2435
+ });
2436
+ if (handle) await Promise.race([handle.connectionsSettled, new Promise((resolve) => setTimeout(resolve, this.discoveryTimeoutMs))]);
2437
+ const manager = handle?.services.clientManager;
2438
+ const cachedByServer = new Map(require_mcpCatalog.readCachedCatalog(prepared.configSources).servers.map((server) => [server.name, server.tools]));
2439
+ const presence = await this.credentialPresence(prepared.catalog.servers.map((server) => server.name));
2440
+ const servers = await Promise.all(prepared.catalog.servers.map(async (server) => {
2441
+ const change = liveStates.get(server.name);
2442
+ const state = change?.state ?? manager?.getServerState(server.name) ?? server.state;
2443
+ let tools = cachedByServer.get(server.name) ?? [];
2444
+ let source = tools.length === 0 ? "configured" : "cached";
2445
+ if (state === "connected" && manager) try {
2446
+ tools = await manager.ensureConnected(server.name).then(async (connection) => await connection.listTools());
2447
+ source = "live";
2448
+ } catch {}
2449
+ return {
2450
+ name: server.name,
2451
+ state,
2452
+ source,
2453
+ credentialPresent: presence.get(server.name) ?? false,
2454
+ tools: tools.map((tool) => toolView(server.name, tool)),
2455
+ ...change?.error ? { error: "The server reported a connection error." } : {}
2456
+ };
2457
+ }));
2458
+ return {
2459
+ ...prepared.catalog,
2460
+ servers
2461
+ };
2462
+ } finally {
2463
+ await owner.dispose();
2464
+ this.cleanupPrepared(prepared);
2465
+ this.busyRepositories.delete(repositoryId);
2466
+ }
2467
+ }
2468
+ async authorize(repositoryId, repositoryRoot, sync, serverName, redirect) {
2469
+ this.sweepFlows();
2470
+ if (this.busyRepositories.has(repositoryId)) throw new Error("MCP management is already active for this repository.");
2471
+ const full = this.prepare(repositoryId, repositoryRoot, sync);
2472
+ const configured = full.catalog.servers.some((server) => server.name === serverName);
2473
+ const fresh = full.catalog.sync.fresh;
2474
+ this.cleanupPrepared(full);
2475
+ if (!fresh) throw new Error("Sync this repository before authorizing an MCP server.");
2476
+ if (!configured) throw new Error("That MCP server is not enabled for this repository.");
2477
+ const prepared = this.prepare(repositoryId, repositoryRoot, sync, serverName);
2478
+ const flow = {
2479
+ id: (0, node_crypto.randomUUID)(),
2480
+ repositoryId,
2481
+ serverName,
2482
+ status: "starting",
2483
+ expiresAt: Date.now() + this.authorizationTimeoutMs,
2484
+ stagingDirectory: prepared.stagingDirectory
2485
+ };
2486
+ this.flows.set(flow.id, flow);
2487
+ this.busyRepositories.add(repositoryId);
2488
+ this.runAuthorization(flow, prepared.configSources, repositoryRoot, redirect);
2489
+ return publicFlow(flow);
2490
+ }
2491
+ getAuthorization(flowId, repositoryId) {
2492
+ this.sweepFlows();
2493
+ const flow = this.flows.get(flowId);
2494
+ return flow?.repositoryId === repositoryId ? publicFlow(flow) : void 0;
2495
+ }
2496
+ async cancelAuthorization(flowId, repositoryId) {
2497
+ const flow = this.flows.get(flowId);
2498
+ if (!flow || flow.repositoryId !== repositoryId) return void 0;
2499
+ if (!isTerminal(flow.status)) {
2500
+ flow.status = "cancelled";
2501
+ await this.finishFlow(flow);
2502
+ }
2503
+ return publicFlow(flow);
2504
+ }
2505
+ async runAuthorization(flow, sources, repositoryRoot, redirect) {
2506
+ const owner = new require_mcpCatalog.McpRuntimeOwner();
2507
+ flow.owner = owner;
2508
+ flow.timeout = setTimeout(() => {
2509
+ if (isTerminal(flow.status)) return;
2510
+ flow.status = "expired";
2511
+ flow.error = "Authorization timed out.";
2512
+ this.finishFlow(flow);
2513
+ }, this.authorizationTimeoutMs);
2514
+ try {
2515
+ const handle = await owner.start({
2516
+ configSources: sources,
2517
+ workspaceRoot: repositoryRoot,
2518
+ executionCwd: repositoryRoot,
2519
+ tokenStore: await this.tokenStore(),
2520
+ ...redirect ? { callbackServer: require_mcpCatalog.hubCallbackSink(redirect) } : {},
2521
+ onAuthorizationUrl: (url, serverName) => {
2522
+ if (serverName !== flow.serverName || isTerminal(flow.status)) return;
2523
+ const safeUrl = safeAuthorizationUrl(url);
2524
+ if (!safeUrl) {
2525
+ flow.status = "failed";
2526
+ flow.error = "The server returned an invalid authorization URL.";
2527
+ this.finishFlow(flow);
2528
+ return;
2529
+ }
2530
+ flow.authorizationUrl = safeUrl;
2531
+ flow.status = "waiting";
2532
+ },
2533
+ onServerStateChange: (change) => this.applyAuthorizationState(flow, change)
2534
+ });
2535
+ if (!handle || isTerminal(flow.status)) return;
2536
+ const manager = handle.services.clientManager;
2537
+ await manager.disconnectServer(flow.serverName);
2538
+ await manager.ensureConnected(flow.serverName);
2539
+ if (!isTerminal(flow.status)) flow.status = "completed";
2540
+ } catch {
2541
+ if (!isTerminal(flow.status)) {
2542
+ flow.status = "failed";
2543
+ flow.error = "The MCP server could not complete authorization.";
2544
+ }
2545
+ } finally {
2546
+ if (!isTerminal(flow.status)) flow.status = "completed";
2547
+ await this.finishFlow(flow);
2548
+ }
2549
+ }
2550
+ applyAuthorizationState(flow, change) {
2551
+ if (change.serverName !== flow.serverName || isTerminal(flow.status)) return;
2552
+ if (change.state === "connected") flow.status = "completed";
2553
+ else if (change.state === "needs-auth" && flow.status === "starting") flow.status = "waiting";
2554
+ else if (change.state === "failed") {
2555
+ flow.status = "failed";
2556
+ flow.error = "The MCP server rejected the authorization attempt.";
2557
+ }
2558
+ }
2559
+ async finishFlow(flow) {
2560
+ if (flow.timeout) clearTimeout(flow.timeout);
2561
+ flow.timeout = void 0;
2562
+ const owner = flow.owner;
2563
+ flow.owner = void 0;
2564
+ await owner?.dispose();
2565
+ if (flow.stagingDirectory) node_fs.default.rmSync(flow.stagingDirectory, {
2566
+ recursive: true,
2567
+ force: true
2568
+ });
2569
+ flow.stagingDirectory = void 0;
2570
+ this.busyRepositories.delete(flow.repositoryId);
2571
+ flow.retainedUntil = Date.now() + TERMINAL_FLOW_RETENTION_MS;
2572
+ }
2573
+ sweepFlows() {
2574
+ const now = Date.now();
2575
+ for (const [id, flow] of this.flows) if (flow.retainedUntil !== void 0 && flow.retainedUntil <= now) this.flows.delete(id);
2576
+ }
2577
+ async dispose() {
2578
+ await Promise.all([...this.flows.values()].map(async (flow) => {
2579
+ if (!isTerminal(flow.status)) flow.status = "cancelled";
2580
+ await this.finishFlow(flow);
2581
+ }));
2582
+ this.flows.clear();
2583
+ this.busyRepositories.clear();
2584
+ }
2585
+ };
2586
+ //#endregion
2587
+ //#region src/types/apiRoutes.ts
2588
+ /** One authorization flow, addressed by id below the flow collection. */
2589
+ const AUTHORIZATION_FLOW_PATH = `${MCP_AUTHORIZATION_API_PATH}/:${MCP_FLOW_ID_PARAM}`;
2590
+ /**
2591
+ * This package's routes, as data.
2592
+ *
2593
+ * No scope and no base path. The build reads both off
2594
+ * `src/extensions/(backend)/api/mcp/`, so the mount is stated once, by the
2595
+ * folder that creates it. The sub-paths come from `webMcp`, which already owned
2596
+ * them, so this table names where a route sits and nothing else.
2597
+ *
2598
+ * This is the one file the hub handler and the settings panel both read, so a
2599
+ * path cannot move on one side alone. The handler matches on these strings
2600
+ * rather than parsing its own copies of them.
2601
+ *
2602
+ * `:flowId` is the client's parameter syntax; the handler reads the segment off
2603
+ * the same path, so the two cannot disagree about where the id sits.
2604
+ */
2605
+ var apiRoutes_default = (0, _agimon_ai_po_core_web.defineApiRoutes)({
2606
+ catalog: {
2607
+ method: "GET",
2608
+ path: MCP_REPOSITORY_API_PATH,
2609
+ query: [MCP_REPOSITORY_ID_QUERY],
2610
+ response: (0, _agimon_ai_po_core_web.apiResponse)()
2611
+ },
2612
+ discover: {
2613
+ method: "POST",
2614
+ path: MCP_DISCOVERY_API_PATH,
2615
+ response: (0, _agimon_ai_po_core_web.apiResponse)()
2616
+ },
2617
+ authorize: {
2618
+ method: "POST",
2619
+ path: MCP_AUTHORIZATION_API_PATH,
2620
+ response: (0, _agimon_ai_po_core_web.apiResponse)()
2621
+ },
2622
+ readAuthorization: {
2623
+ method: "GET",
2624
+ path: AUTHORIZATION_FLOW_PATH,
2625
+ query: [MCP_REPOSITORY_ID_QUERY],
2626
+ response: (0, _agimon_ai_po_core_web.apiResponse)()
2627
+ },
2628
+ /** The same path as `readAuthorization`; the method is the whole of the difference. */
2629
+ cancelAuthorization: {
2630
+ method: "DELETE",
2631
+ path: AUTHORIZATION_FLOW_PATH,
2632
+ query: [MCP_REPOSITORY_ID_QUERY],
2633
+ response: (0, _agimon_ai_po_core_web.apiResponse)()
2634
+ },
2635
+ /** Session scope: one available tool's registered name, description and schema. */
2636
+ sessionTool: {
2637
+ method: "GET",
2638
+ path: MCP_SESSION_TOOL_API_PATH,
2639
+ query: [MCP_SESSION_TOOL_SERVER_QUERY, MCP_SESSION_TOOL_NAME_QUERY],
2640
+ response: (0, _agimon_ai_po_core_web.apiResponse)()
2641
+ }
2642
+ });
2643
+ //#endregion
2644
+ //#region src/services/mcpHubApi/index.ts
2645
+ /**
2646
+ * What sits above a flow id, read off the flow route itself.
2647
+ *
2648
+ * This handler matches paths by hand rather than through a router, so the one
2649
+ * route carrying a parameter is split here: everything before `:flowId` is the
2650
+ * prefix a request must start with, and what follows it is the id.
2651
+ */
2652
+ const AUTHORIZATION_FLOW_PREFIX = apiRoutes_default.readAuthorization.path.replace(`:${MCP_FLOW_ID_PARAM}`, "");
2653
+ const JSON_HEADERS = { "content-type": "application/json" };
2654
+ const REPOSITORY_ID = /^(?:[a-f0-9]{32}|repo-[A-Za-z0-9_-]{24}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/u;
2655
+ const FLOW_ID = /^[a-f0-9-]{36}$/u;
2656
+ const MAX_BODY_BYTES = 8192;
2657
+ const MAX_SERVER_NAME = 160;
2658
+ const manager = new McpSettingsManager();
2659
+ function json(body, status = 200) {
2660
+ return new Response(JSON.stringify(body), {
2661
+ status,
2662
+ headers: JSON_HEADERS
2663
+ });
2664
+ }
2665
+ function isRecord(value) {
2666
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2667
+ }
2668
+ async function bodyOf(request) {
2669
+ const text = await request.text();
2670
+ if (text.length > MAX_BODY_BYTES) return void 0;
2671
+ try {
2672
+ const parsed = JSON.parse(text);
2673
+ return isRecord(parsed) ? parsed : void 0;
2674
+ } catch {
2675
+ return;
2676
+ }
2677
+ }
2678
+ function repositoryIdOf(value) {
2679
+ return typeof value === "string" && REPOSITORY_ID.test(value) ? value : void 0;
2680
+ }
2681
+ function serverNameOf(value) {
2682
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_SERVER_NAME && !Array.from(value).some((character) => character.charCodeAt(0) <= 31) ? value : void 0;
2683
+ }
2684
+ function resolveRepository(context, repositoryId) {
2685
+ return context.resolveRepository?.(repositoryId);
2686
+ }
2687
+ function errorStatus(error) {
2688
+ const message = error instanceof Error ? error.message : "";
2689
+ if (message.includes("already active")) return {
2690
+ status: 409,
2691
+ message
2692
+ };
2693
+ if (message.startsWith("Sync this repository")) return {
2694
+ status: 409,
2695
+ message
2696
+ };
2697
+ if (message.includes("not enabled")) return {
2698
+ status: 404,
2699
+ message
2700
+ };
2701
+ return {
2702
+ status: 500,
2703
+ message: "MCP management could not complete that request."
2704
+ };
2705
+ }
2706
+ async function handleCatalog(request, context) {
2707
+ const repositoryId = repositoryIdOf(new URL(request.url).searchParams.get(MCP_REPOSITORY_ID_QUERY));
2708
+ if (!repositoryId) return json({ error: "A valid repositoryId is required." }, 400);
2709
+ const repositoryRoot = resolveRepository(context, repositoryId);
2710
+ if (!repositoryRoot) return json({ error: "That repository is not available to this hub." }, 404);
2711
+ try {
2712
+ return json(await manager.readCatalog(repositoryId, repositoryRoot, await context.readRepositorySync?.(repositoryId)));
2713
+ } catch {
2714
+ return json({ error: "The synced MCP catalog could not be read." }, 500);
2715
+ }
2716
+ }
2717
+ async function handleDiscovery(request, context) {
2718
+ const repositoryId = repositoryIdOf((await bodyOf(request))?.repositoryId);
2719
+ if (!repositoryId) return json({ error: "A valid repositoryId is required." }, 400);
2720
+ const repositoryRoot = resolveRepository(context, repositoryId);
2721
+ if (!repositoryRoot) return json({ error: "That repository is not available to this hub." }, 404);
2722
+ try {
2723
+ return json(await manager.discover(repositoryId, repositoryRoot, await context.readRepositorySync?.(repositoryId)));
2724
+ } catch (error) {
2725
+ const failure = errorStatus(error);
2726
+ return json({ error: failure.message }, failure.status);
2727
+ }
2728
+ }
2729
+ async function handleAuthorize(request, context) {
2730
+ const body = await bodyOf(request);
2731
+ const repositoryId = repositoryIdOf(body?.repositoryId);
2732
+ const serverName = serverNameOf(body?.serverName);
2733
+ if (!repositoryId || !serverName) return json({ error: "A valid repositoryId and serverName are required." }, 400);
2734
+ const repositoryRoot = resolveRepository(context, repositoryId);
2735
+ if (!repositoryRoot) return json({ error: "That repository is not available to this hub." }, 404);
2736
+ try {
2737
+ return json(await manager.authorize(repositoryId, repositoryRoot, await context.readRepositorySync?.(repositoryId), serverName, context.oauthRedirect?.()), 202);
2738
+ } catch (error) {
2739
+ const failure = errorStatus(error);
2740
+ return json({ error: failure.message }, failure.status);
2741
+ }
2742
+ }
2743
+ async function handleAuthorizationFlow(request, context, path) {
2744
+ const flowId = path.slice(AUTHORIZATION_FLOW_PREFIX.length);
2745
+ const repositoryId = repositoryIdOf(new URL(request.url).searchParams.get(MCP_REPOSITORY_ID_QUERY));
2746
+ if (!repositoryId || !FLOW_ID.test(flowId)) return json({ error: "A valid repositoryId and flow id are required." }, 400);
2747
+ if (!resolveRepository(context, repositoryId)) return json({ error: "That repository is not available to this hub." }, 404);
2748
+ if (request.method === apiRoutes_default.cancelAuthorization.method) {
2749
+ const flow = await manager.cancelAuthorization(flowId, repositoryId);
2750
+ return flow ? json(flow) : json({ error: "That authorization flow was not found." }, 404);
2751
+ }
2752
+ const flow = manager.getAuthorization(flowId, repositoryId);
2753
+ return flow ? json(flow) : json({ error: "That authorization flow was not found." }, 404);
2754
+ }
2755
+ //#endregion
2756
+ //#region src/extensions/(backend)/api/mcp/route.server.ts
2757
+ /**
2758
+ * Mounts this package's HTTP API at `mcp`, which the folder names.
2759
+ *
2760
+ * The folder is the declaration: the build reads the base path off it and hands
2761
+ * the browser a client bound to the same segment, so the settings panel can no
2762
+ * longer spell a workspace mount by hand.
2763
+ */
2764
+ var route_server_default$1 = (0, _agimon_ai_po_core_extensionFile.defineRoute)({
2765
+ basePath: "mcp",
2766
+ start(context) {
2767
+ return {
2768
+ async fetch(request) {
2769
+ const path = new URL(request.url).pathname;
2770
+ if (request.method === apiRoutes_default.catalog.method && path === apiRoutes_default.catalog.path) return await handleCatalog(request, context);
2771
+ if (request.method === apiRoutes_default.discover.method && path === apiRoutes_default.discover.path) return await handleDiscovery(request, context);
2772
+ if (request.method === apiRoutes_default.authorize.method && path === apiRoutes_default.authorize.path) return await handleAuthorize(request, context);
2773
+ if ((request.method === apiRoutes_default.readAuthorization.method || request.method === apiRoutes_default.cancelAuthorization.method) && path.startsWith(AUTHORIZATION_FLOW_PREFIX)) return await handleAuthorizationFlow(request, context, path);
2774
+ return json({ error: "Not found." }, 404);
2775
+ },
2776
+ close() {
2777
+ manager.dispose();
2778
+ }
2779
+ };
2780
+ }
2781
+ });
2782
+ //#endregion
2783
+ //#region src/extensions/workspaces/sessions/(backend)/api/mcp-session/route.server.ts
2784
+ /** Mounts the session's `mcp-session` API: the reachable tool detail the context panel opens. */
2785
+ var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)((context) => context.root.api[0]);
2786
+ //#endregion
2787
+ //#region src/extensions/workspaces/sessions/(backend)/command/mcp.server.ts
2788
+ var mcp_server_default = (0, _agimon_ai_po_core_extensionFile.defineServerCommand)((context) => context.root.commands[0]);
2789
+ //#endregion
2790
+ //#region src/extensions/workspaces/sessions/(backend)/method/apps.server.ts
2791
+ var apps_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoutedContribution)((context) => context.root.appMethods, { cardinality: "many" });
2792
+ //#endregion
2793
+ //#region src/extensions/workspaces/sessions/(backend)/resource/po-use-mcp.server.ts
2794
+ var po_use_mcp_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)({
2795
+ name: "po-use-mcp",
2796
+ description: "Inspect domain-scoped MCP servers, authenticate, reload configuration, and troubleshoot tool availability.",
2797
+ when: _agimon_ai_po_core_help.DOOM_HELP_WHEN,
2798
+ kind: "skill",
2799
+ read: () => (0, _agimon_ai_po_core_serverFacet.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/po-use-mcp/SKILL.md")
2800
+ });
2801
+ //#endregion
2802
+ //#region generated/server.ts
2803
+ const at = (value, context) => typeof value === "function" ? value(context) : value;
2804
+ const many = (value, context) => {
2805
+ const resolved = typeof value === "function" ? value(context) : value;
2806
+ return Array.isArray(resolved) ? resolved : [resolved];
2807
+ };
2808
+ const via = (identity, value) => value === void 0 ? void 0 : {
2809
+ ...identity,
2810
+ ...value
2811
+ };
2812
+ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2813
+ name: "@agimon-ai/po-mcp",
2814
+ providerOverrides: (0, _agimon_ai_po_core_serverFacet.composeServerProviderOverrides)(root_server_default),
2815
+ global: (context) => ({ get api() {
2816
+ return [at(route_server_default$1, context)];
2817
+ } }),
2818
+ workspace: (context) => ({ get api() {
2819
+ return [at(route_server_default$1, context)];
2820
+ } }),
2821
+ session: async (context) => {
2822
+ const scopeSession = await root_server_default(context);
2823
+ const contextSession = {
2824
+ ...context,
2825
+ root: scopeSession.value
2826
+ };
2827
+ return {
2828
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeSession),
2829
+ get api() {
2830
+ return [at(route_server_default$1, context), at(route_server_default, contextSession)];
2831
+ },
2832
+ get commands() {
2833
+ return [via({ name: "mcp" }, at(mcp_server_default, contextSession))];
2834
+ },
2835
+ get methods() {
2836
+ return [...many(apps_server_default, contextSession)];
2837
+ },
2838
+ get resources() {
2839
+ return [at(po_use_mcp_server_default, contextSession)];
2840
+ },
2841
+ services: [...scopeSession.services ?? []],
2842
+ get activities() {
2843
+ return [...scopeSession.activities ?? []];
2844
+ }
2845
+ };
2846
+ }
2847
+ });
2848
+ //#endregion
2849
+ exports.default = facet;
2850
+ exports.facet = facet;
2851
+
2852
+ //# sourceMappingURL=server.cjs.map