@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,900 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
+ let node_crypto = require("node:crypto");
3
+ let node_fs = require("node:fs");
4
+ node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
5
+ let node_path = require("node:path");
6
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
7
+ let _agimon_ai_po_config_agentPluginMcp = require("@agimon-ai/po-config/agentPluginMcp");
8
+ let yaml = require("yaml");
9
+ let node_os = require("node:os");
10
+ node_os = require_rolldown_runtime.__toESM(node_os, 1);
11
+ let _agimon_ai_mcp_proxy_package_json = require("@agimon-ai/mcp-proxy/package.json");
12
+ _agimon_ai_mcp_proxy_package_json = require_rolldown_runtime.__toESM(_agimon_ai_mcp_proxy_package_json, 1);
13
+ let _agimon_ai_po_core_mcpSession = require("@agimon-ai/po-core/mcpSession");
14
+ //#region src/services/configSources/index.ts
15
+ /** Wrapper entry used by non-Doom clients to expose `mcp-config.yaml` as one MCP. */
16
+ const PROXY_SERVER_NAME = "mcp-proxy";
17
+ const PROXY_SERVER_ALIASES = /* @__PURE__ */ new Set([PROXY_SERVER_NAME, "agiflow-proxy"]);
18
+ const PRIVATE_FILE_MODE = 384;
19
+ const PRIVATE_DIRECTORY_MODE = 448;
20
+ const DEFAULT_UPSTREAM_CONFIG = "mcp-config.yaml";
21
+ const CONFIG_ARGUMENT = "--config";
22
+ function isObject(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ function isYamlPath(filePath) {
26
+ const extension = node_path.default.extname(filePath).toLowerCase();
27
+ return extension === ".yaml" || extension === ".yml";
28
+ }
29
+ function readConfigDocument(filePath) {
30
+ try {
31
+ const contents = node_fs.default.readFileSync(filePath, "utf8");
32
+ const parsed = isYamlPath(filePath) ? (0, yaml.parse)(contents) : JSON.parse(contents);
33
+ return isObject(parsed) ? {
34
+ config: parsed,
35
+ contents
36
+ } : void 0;
37
+ } catch {
38
+ return;
39
+ }
40
+ }
41
+ function readConfigObject(filePath) {
42
+ return readConfigDocument(filePath)?.config;
43
+ }
44
+ function serverDefinitions(config) {
45
+ return isObject(config?.mcpServers) ? config.mcpServers : {};
46
+ }
47
+ function isEnabledDefinition(definition) {
48
+ return !isObject(definition) || definition.disabled !== true;
49
+ }
50
+ function isProxyWrapper(name) {
51
+ return PROXY_SERVER_ALIASES.has(name);
52
+ }
53
+ /**
54
+ * Resolves the upstream config referenced by the repository proxy wrapper.
55
+ *
56
+ * The wrapper remains useful to Claude, Codex, and other external clients. Doom
57
+ * embeds the same client/runtime code, so it reads this path and connects each
58
+ * declared server itself instead of starting the wrapper as a nested MCP.
59
+ */
60
+ function resolveSharedConfigPathFrom(repoRoot, repositoryConfigPath) {
61
+ const servers = serverDefinitions(readConfigObject(repositoryConfigPath));
62
+ const rawArgs = [...PROXY_SERVER_ALIASES].map((name) => servers[name]).find((definition) => isObject(definition))?.args;
63
+ const args = Array.isArray(rawArgs) ? rawArgs : [];
64
+ const configArgumentIndex = args.indexOf(CONFIG_ARGUMENT);
65
+ const declared = args[configArgumentIndex + 1];
66
+ const candidate = configArgumentIndex >= 0 && typeof declared === "string" ? node_path.default.resolve(repoRoot, declared) : node_path.default.join(repoRoot, DEFAULT_UPSTREAM_CONFIG);
67
+ return node_fs.default.existsSync(candidate) ? candidate : void 0;
68
+ }
69
+ function resolveSharedConfigPath(repoRoot) {
70
+ return resolveSharedConfigPathFrom(repoRoot, node_path.default.join(repoRoot, ".mcp.json"));
71
+ }
72
+ /** Empty and absent allowlists both mean "keep everything". */
73
+ function isAllowed(name, allowed) {
74
+ return !allowed || allowed.length === 0 || allowed.includes(name);
75
+ }
76
+ function stagedPath(filePath, contents, stagingDirectory) {
77
+ const digest = (0, node_crypto.createHash)("sha256").update(filePath).update("\0").update(contents).digest("hex").slice(0, 16);
78
+ const extension = isYamlPath(filePath) ? ".yaml" : ".json";
79
+ return node_path.default.join(stagingDirectory, `mcp-${digest}${extension}`);
80
+ }
81
+ function serializeConfig(filePath, config) {
82
+ return isYamlPath(filePath) ? (0, yaml.stringify)(config) : `${JSON.stringify(config, null, 2)}\n`;
83
+ }
84
+ function layerCacheKey(filePath, format, originalContents, effectiveContents, sourceIdentity) {
85
+ return (0, node_crypto.createHash)("sha256").update(format).update("\0").update(node_fs.default.realpathSync(filePath)).update("\0").update(sourceIdentity ?? "unprojected").update("\0").update(originalContents).update("\0").update(effectiveContents).digest("hex");
86
+ }
87
+ /**
88
+ * Rewrites one config layer with disallowed servers removed.
89
+ *
90
+ * Proxy wrapper entries are stripped from `.mcp.json` layers because Doom reaches
91
+ * their upstream config directly. Entries are removed rather than marked disabled,
92
+ * so a filtered stdio server is never spawned at all.
93
+ */
94
+ function filterLayer(filePath, allowed, stagingDirectory, options) {
95
+ const document = readConfigDocument(filePath);
96
+ if (!document) return void 0;
97
+ const { config, contents: originalContents } = document;
98
+ const servers = serverDefinitions(config);
99
+ const kept = {};
100
+ const dropped = [];
101
+ for (const [name, definition] of Object.entries(servers)) if (options.stripProxyWrappers && isProxyWrapper(name) || !isAllowed(name, allowed)) dropped.push(name);
102
+ else kept[name] = definition;
103
+ const keptNames = Object.entries(kept).filter(([, definition]) => isEnabledDefinition(definition)).map(([name]) => name);
104
+ if (dropped.length === 0) return {
105
+ source: {
106
+ path: filePath,
107
+ format: "claude",
108
+ cacheKey: layerCacheKey(filePath, "claude", originalContents, originalContents, options.sourceIdentity)
109
+ },
110
+ kept: keptNames,
111
+ dropped
112
+ };
113
+ node_fs.default.mkdirSync(stagingDirectory, {
114
+ mode: PRIVATE_DIRECTORY_MODE,
115
+ recursive: true
116
+ });
117
+ const contents = serializeConfig(filePath, {
118
+ ...config,
119
+ mcpServers: kept
120
+ });
121
+ const target = stagedPath(filePath, contents, stagingDirectory);
122
+ node_fs.default.writeFileSync(target, contents, { mode: PRIVATE_FILE_MODE });
123
+ return {
124
+ source: {
125
+ path: target,
126
+ format: "claude",
127
+ cacheKey: layerCacheKey(filePath, "claude", originalContents, contents, options.sourceIdentity)
128
+ },
129
+ kept: keptNames,
130
+ dropped
131
+ };
132
+ }
133
+ function digestMatches(source) {
134
+ try {
135
+ return (0, node_crypto.createHash)("sha256").update(node_fs.default.readFileSync(source.configPath)).digest("hex") === source.contentDigest;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ function emptyGroups(diagnostics = []) {
141
+ return {
142
+ shared: {
143
+ configSources: [],
144
+ configPaths: [],
145
+ serverNames: []
146
+ },
147
+ sessionLocal: {
148
+ configSources: [],
149
+ configPaths: [],
150
+ serverNames: []
151
+ },
152
+ droppedServers: [],
153
+ diagnostics
154
+ };
155
+ }
156
+ /**
157
+ * Resolves every config layer for this session and applies its domain allowlist.
158
+ *
159
+ * All returned layers go into one embedded `@agimon-ai/mcp-proxy` client container.
160
+ * The package is used as a library only: its wrapper MCP never becomes a Doom MCP
161
+ * server, so status, enable/disable, resources, and OAuth stay attached to each
162
+ * actual downstream server.
163
+ */
164
+ function buildMcpConfigGroups(input) {
165
+ if (input.enabled === false) return emptyGroups();
166
+ const projectedSources = input.sources;
167
+ const legacyLayers = projectedSources ? [] : [node_path.default.join(input.repoRoot, ".mcp.json"), ...(input.pluginConfigPaths ?? []).map((candidate) => node_path.default.resolve(input.repoRoot, candidate))].filter((candidate) => node_fs.default.existsSync(candidate));
168
+ const configSources = [];
169
+ const serverNames = [];
170
+ const droppedServers = [];
171
+ const diagnostics = [];
172
+ for (const layer of legacyLayers) {
173
+ const filtered = filterLayer(layer, input.allowlist?.servers, input.stagingDirectory, { stripProxyWrappers: true });
174
+ if (!filtered) continue;
175
+ configSources.push(filtered.source);
176
+ serverNames.push(...filtered.kept);
177
+ droppedServers.push(...filtered.dropped);
178
+ }
179
+ for (const source of projectedSources ?? []) {
180
+ if (source.format === "agent-plugin-v1") {
181
+ const normalized = (0, _agimon_ai_po_config_agentPluginMcp.normalizeAgentPluginMcpSource)(source, {
182
+ stagingDirectory: input.stagingDirectory,
183
+ allowedServers: input.allowlist?.servers
184
+ });
185
+ if (normalized.configSource) configSources.push(normalized.configSource);
186
+ serverNames.push(...normalized.serverNames);
187
+ droppedServers.push(...normalized.droppedServers);
188
+ diagnostics.push(...normalized.diagnostics);
189
+ continue;
190
+ }
191
+ if (!digestMatches(source)) {
192
+ diagnostics.push(`Native MCP source "${source.sourceId}" was disabled: the file is missing or its content digest changed.`);
193
+ continue;
194
+ }
195
+ const filtered = filterLayer(source.configPath, input.allowlist?.servers, input.stagingDirectory, {
196
+ stripProxyWrappers: true,
197
+ sourceIdentity: `${source.sourceId}:${source.contentDigest}`
198
+ });
199
+ if (!filtered) {
200
+ diagnostics.push(`Native MCP source "${source.sourceId}" was disabled: the config could not be parsed.`);
201
+ continue;
202
+ }
203
+ configSources.push(filtered.source);
204
+ serverNames.push(...filtered.kept);
205
+ droppedServers.push(...filtered.dropped);
206
+ }
207
+ const repositorySource = projectedSources?.find((source) => source.format === "native" && source.owner === "repository");
208
+ const upstreamPath = projectedSources ? repositorySource && digestMatches(repositorySource) ? resolveSharedConfigPathFrom(input.repoRoot, repositorySource.configPath) : void 0 : resolveSharedConfigPath(input.repoRoot);
209
+ const upstream = upstreamPath ? filterLayer(upstreamPath, input.allowlist?.proxy, input.stagingDirectory, { stripProxyWrappers: true }) : void 0;
210
+ if (upstream) droppedServers.push(...upstream.dropped);
211
+ return {
212
+ shared: {
213
+ configSources: upstream ? [upstream.source] : [],
214
+ configPaths: upstream ? [upstream.source.path] : [],
215
+ serverNames: upstream ? upstream.kept : []
216
+ },
217
+ sessionLocal: {
218
+ configSources,
219
+ configPaths: configSources.map((source) => source.path),
220
+ serverNames: [...new Set(serverNames)]
221
+ },
222
+ droppedServers: [...new Set(droppedServers)],
223
+ diagnostics
224
+ };
225
+ }
226
+ //#endregion
227
+ //#region src/services/mcpRuntime/index.ts
228
+ var mcpRuntime_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
229
+ McpRuntimeOwner: () => McpRuntimeOwner,
230
+ definitionsCachePath: () => definitionsCachePath,
231
+ hubCallbackSink: () => hubCallbackSink,
232
+ readCachedCatalog: () => readCachedCatalog
233
+ });
234
+ /**
235
+ * Presents the hub's redirect surface as the proxy's callback collaborator.
236
+ *
237
+ * `listen` binds nothing: the hub already serves the address this advertises,
238
+ * so opening a loopback port would only create one nobody redirects to.
239
+ */
240
+ function hubCallbackSink(redirect) {
241
+ return {
242
+ redirectUri: () => redirect.redirectUri,
243
+ getPort: () => void 0,
244
+ listen: (preferredPort) => Promise.resolve(preferredPort),
245
+ reserve: (state, timeoutMs) => redirect.reserve(state, timeoutMs),
246
+ waitForCallback: (state, timeoutMs) => redirect.wait(state, timeoutMs),
247
+ cancel: (state) => redirect.cancel(state)
248
+ };
249
+ }
250
+ const MCP_APP_CLIENT_CAPABILITIES = { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: ["text/html;profile=mcp-app", "text/html+skybridge"] } } };
251
+ /**
252
+ * Definitions cache path shared by Doom's synchronous warm reader and mcp-proxy.
253
+ *
254
+ * Generated staging paths are deliberately absent: projected source content must
255
+ * invalidate a reused path, while identical portable content must survive a new
256
+ * private staging directory. Stable source origins are carried inside each cache
257
+ * key; format and merge behavior remain part of the ordered identity.
258
+ */
259
+ function definitionsCachePath(configSources) {
260
+ if (configSources.length === 0) throw new Error("definitionsCachePath requires at least one config source");
261
+ const digest = (0, node_crypto.createHash)("sha256").update(JSON.stringify(configSources.map((source) => ({
262
+ cacheKey: source.cacheKey,
263
+ format: source.format,
264
+ optional: source.optional ?? false,
265
+ mergeStrategy: source.mergeStrategy ?? "remote-priority"
266
+ })))).update("\0").update(_agimon_ai_mcp_proxy_package_json.default.version).update("\0").update(JSON.stringify(MCP_APP_CLIENT_CAPABILITIES)).digest("hex");
267
+ return (0, node_path.join)((0, node_os.homedir)(), ".mcp-proxy", `po-${digest}.definitions-cache.json`);
268
+ }
269
+ /**
270
+ * Reads the catalog the previous run left behind.
271
+ *
272
+ * Synchronous on purpose: Pi has to be able to register these tools during
273
+ * extension install, before it takes its first turn, and awaiting a file read
274
+ * would let that turn start with an empty tool set. A miss is normal on a first
275
+ * run and resolves to an empty catalog rather than an error.
276
+ *
277
+ * The file path is content-addressed from the current ordered layers, and its proxy
278
+ * version is checked before any schema is registered. Servers filtered out of the
279
+ * current configuration are also excluded later by `McpCatalog.seed`.
280
+ */
281
+ function readCachedCatalog(configSources, cachePathFor = definitionsCachePath) {
282
+ if (configSources.length === 0) return { servers: [] };
283
+ try {
284
+ const cachePath = cachePathFor(configSources);
285
+ const cache = JSON.parse((0, node_fs.readFileSync)(cachePath, "utf8"));
286
+ if (cache.oneMcpVersion !== _agimon_ai_mcp_proxy_package_json.default.version) return { servers: [] };
287
+ return { servers: Object.values(cache.servers ?? {}).map((server) => ({
288
+ name: server.serverName,
289
+ tools: server.tools ?? []
290
+ })) };
291
+ } catch {
292
+ return { servers: [] };
293
+ }
294
+ }
295
+ function toProxyConfigSource(source) {
296
+ return {
297
+ path: source.path,
298
+ format: source.format,
299
+ ...source.optional === void 0 ? {} : { optional: source.optional },
300
+ ...source.mergeStrategy === void 0 ? {} : { mergeStrategy: source.mergeStrategy }
301
+ };
302
+ }
303
+ /**
304
+ * Owns the one live mcp-proxy container for this extension.
305
+ *
306
+ * Pi reloads extensions in process, so a connect that lands after a session restart
307
+ * or a `/domains` switch would otherwise fold tools into a dead context. Every start
308
+ * takes the next generation; anything an older generation produces is dropped and its
309
+ * container disposed.
310
+ */
311
+ var McpRuntimeOwner = class {
312
+ generation = 0;
313
+ active;
314
+ unsubscribe;
315
+ get currentGeneration() {
316
+ return this.generation;
317
+ }
318
+ /** The live container, or undefined before the first start and after dispose. */
319
+ getServices() {
320
+ return this.active;
321
+ }
322
+ /**
323
+ * Whether a container a caller captured earlier is still the live one.
324
+ *
325
+ * An async continuation that started under one container has to check this
326
+ * before writing its result, or a `/domains` switch would fold stale tools into
327
+ * the new session.
328
+ */
329
+ isCurrent(services) {
330
+ return services !== void 0 && services === this.active;
331
+ }
332
+ /**
333
+ * Starts a container for the current configuration, replacing any predecessor.
334
+ *
335
+ * Returns undefined when a newer generation started while this one was building,
336
+ * which is the signal for the caller to abandon its half-finished work.
337
+ */
338
+ async start(options) {
339
+ const generation = ++this.generation;
340
+ await this.retire();
341
+ if (options.configSources.length === 0) return void 0;
342
+ const { createProxyContainer } = await import("@agimon-ai/mcp-proxy");
343
+ const cachePath = definitionsCachePath(options.configSources);
344
+ const services = await createProxyContainer({
345
+ configSources: options.configSources.map(toProxyConfigSource),
346
+ workspaceRoot: options.workspaceRoot,
347
+ executionCwd: options.executionCwd,
348
+ environment: options.environment,
349
+ definitionsCachePath: cachePath,
350
+ startupMode: "background",
351
+ clientCapabilities: MCP_APP_CLIENT_CAPABILITIES,
352
+ outputSchemaValidation: {
353
+ mode: "warn",
354
+ onWarning: (warning) => {
355
+ if (generation === this.generation) options.onOutputSchemaWarning?.(warning);
356
+ }
357
+ },
358
+ auth: {
359
+ tokenStore: options.tokenStore,
360
+ onAuthorizationUrl: options.onAuthorizationUrl,
361
+ ...options.callbackServer ? { callbackServer: options.callbackServer } : {}
362
+ }
363
+ });
364
+ if (generation !== this.generation) {
365
+ await services.dispose().catch(() => void 0);
366
+ return;
367
+ }
368
+ this.active = services;
369
+ const forward = options.onServerStateChange;
370
+ if (forward) this.unsubscribe = services.clientManager.onServerStateChange((change) => {
371
+ if (generation === this.generation) forward(change);
372
+ });
373
+ return {
374
+ generation,
375
+ services,
376
+ connectionsSettled: services.connectionsSettled
377
+ };
378
+ }
379
+ /** Tears the container down and invalidates every generation still in flight. */
380
+ async dispose() {
381
+ ++this.generation;
382
+ await this.retire();
383
+ }
384
+ async retire() {
385
+ this.unsubscribe?.();
386
+ this.unsubscribe = void 0;
387
+ const services = this.active;
388
+ this.active = void 0;
389
+ if (services) await services.dispose().catch(() => void 0);
390
+ }
391
+ };
392
+ //#endregion
393
+ //#region src/services/sessionConfig/index.ts
394
+ function asStringArray(value) {
395
+ if (!Array.isArray(value)) return void 0;
396
+ const strings = value.filter((entry) => typeof entry === "string" && entry.length > 0);
397
+ return strings.length > 0 ? strings : void 0;
398
+ }
399
+ function readAllowlist(value) {
400
+ if (typeof value !== "object" || value === null) return void 0;
401
+ const { servers, proxy } = value;
402
+ const allowlist = {};
403
+ const allowedServers = asStringArray(servers);
404
+ const allowedProxy = asStringArray(proxy);
405
+ if (allowedServers) allowlist.servers = allowedServers;
406
+ if (allowedProxy) allowlist.proxy = allowedProxy;
407
+ return allowlist.servers || allowlist.proxy ? allowlist : void 0;
408
+ }
409
+ /**
410
+ * Resolves what this session should connect to.
411
+ *
412
+ * Every field falls back to a working default so the extension is usable outside
413
+ * po: a bare Pi session in a repository still gets that repository's
414
+ * `.mcp.json` with no allowlist. A malformed value is treated as absent rather
415
+ * than fatal, because losing MCP is better than losing the session.
416
+ */
417
+ function readSessionConfig(env = process.env, defaultRepoRoot = process.cwd()) {
418
+ const fallback = {
419
+ repoRoot: defaultRepoRoot,
420
+ stagingDirectory: node_path.default.join(node_os.default.tmpdir(), "doom-mcp")
421
+ };
422
+ const strict = (0, _agimon_ai_po_core_mcpSession.readDoomMcpSessionConfig)(env);
423
+ if (strict) {
424
+ const { allowlist, pluginConfigPaths, ...required } = strict;
425
+ const strictAllowlist = readAllowlist(allowlist);
426
+ return {
427
+ ...required,
428
+ ...pluginConfigPaths?.length ? { pluginConfigPaths: [...pluginConfigPaths] } : {},
429
+ ...strictAllowlist ? { allowlist: strictAllowlist } : {}
430
+ };
431
+ }
432
+ const raw = env[_agimon_ai_po_core_mcpSession.DOOM_MCP_SESSION_ENV_VAR];
433
+ if (!raw) return fallback;
434
+ let parsed;
435
+ try {
436
+ parsed = JSON.parse(raw);
437
+ } catch {
438
+ return fallback;
439
+ }
440
+ if (typeof parsed !== "object" || parsed === null) return fallback;
441
+ const { repoRoot, generatedConfigPath, pluginConfigPaths, allowlist, stagingDirectory } = parsed;
442
+ const resolvedRoot = typeof repoRoot === "string" && repoRoot ? repoRoot : fallback.repoRoot;
443
+ const generatedConfig = typeof generatedConfigPath === "string" && generatedConfigPath ? generatedConfigPath : void 0;
444
+ const plugins = asStringArray(pluginConfigPaths);
445
+ const resolvedAllowlist = readAllowlist(allowlist);
446
+ return {
447
+ repoRoot: resolvedRoot,
448
+ stagingDirectory: typeof stagingDirectory === "string" && stagingDirectory ? stagingDirectory : fallback.stagingDirectory,
449
+ ...generatedConfig ? { generatedConfigPath: generatedConfig } : {},
450
+ ...plugins ? { pluginConfigPaths: plugins } : {},
451
+ ...resolvedAllowlist ? { allowlist: resolvedAllowlist } : {}
452
+ };
453
+ }
454
+ /** Serializes the configuration for the Pi child process po launches. */
455
+ function sessionConfigEnvironment(config) {
456
+ return (0, _agimon_ai_po_core_mcpSession.doomMcpSessionEnvironment)(config);
457
+ }
458
+ //#endregion
459
+ //#region src/schemas/directTools.ts
460
+ /** Environment variable carrying a child agent's MCP tool selection. */
461
+ const DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
462
+ /** Explicit "no MCP tools", distinct from an unset variable meaning "no limit". */
463
+ const NO_DIRECT_TOOLS = "__none__";
464
+ //#endregion
465
+ //#region src/services/mcpCatalog/index.ts
466
+ function metadataRecord(value) {
467
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
468
+ }
469
+ /** Standard fields take precedence, including explicitly invalid fields (fail closed). */
470
+ function normalizeMcpAppMetadata(meta) {
471
+ const ui = metadataRecord(meta?.ui);
472
+ const standardUri = ui && "resourceUri" in ui ? ui.resourceUri : meta?.["ui/resourceUri"];
473
+ const legacyUri = meta?.["openai/outputTemplate"];
474
+ const standard = standardUri !== void 0;
475
+ const candidate = standard ? standardUri : legacyUri;
476
+ const resourceUri = typeof candidate === "string" && candidate.startsWith("ui://") && candidate.length <= 8192 && !Array.from(candidate).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) ? candidate : void 0;
477
+ const visibility = ui?.visibility;
478
+ const validVisibility = Array.isArray(visibility) && visibility.every((value) => value === "model" || value === "app");
479
+ const legacy = legacyUri !== void 0 || meta?.["openai/widgetAccessible"] !== void 0 || meta?.["openai/visibility"] !== void 0;
480
+ const modelVisible = visibility === void 0 ? standard || meta?.["openai/visibility"] !== "private" : validVisibility && visibility.includes("model");
481
+ const appVisible = visibility === void 0 ? standard || !legacy ? true : meta?.["openai/widgetAccessible"] === true : validVisibility && visibility.includes("app");
482
+ return {
483
+ ...resourceUri === void 0 ? {} : {
484
+ resourceUri,
485
+ protocol: standard ? "mcp" : "openai"
486
+ },
487
+ modelVisible,
488
+ appVisible
489
+ };
490
+ }
491
+ function isModelVisibleMcpTool(tool) {
492
+ return normalizeMcpAppMetadata(tool._meta).modelVisible;
493
+ }
494
+ /** Execution projections carry normalized visibility; discovery retains the untouched metadata. */
495
+ function mcpToolExecutionMetadata(tool) {
496
+ if (tool._meta === void 0) return void 0;
497
+ const normalized = normalizeMcpAppMetadata(tool._meta);
498
+ return {
499
+ ...tool._meta,
500
+ ui: {
501
+ ...metadataRecord(tool._meta.ui),
502
+ visibility: [...normalized.modelVisible ? ["model"] : [], ...normalized.appVisible ? ["app"] : []]
503
+ }
504
+ };
505
+ }
506
+ function isAppVisibleMcpTool(tool) {
507
+ return normalizeMcpAppMetadata(tool._meta).appVisible;
508
+ }
509
+ /**
510
+ * States where calling a tool cannot succeed, so its tools are withheld.
511
+ *
512
+ * `closed` is absent deliberately: a server closed after an idle timeout is
513
+ * reconnected by the next call rather than gone. `disabled` is absent because it is
514
+ * no longer a reachable transition; `entry.disabled` carries that instead.
515
+ */
516
+ const UNAVAILABLE_STATES = /* @__PURE__ */ new Set(["failed", "needs-auth"]);
517
+ /** Where a server starts: known to the session, not yet dialled. */
518
+ const NOT_CONNECTED = "not-connected";
519
+ const DISABLED = "disabled";
520
+ const ALL_SERVERS_SELECTOR = "*";
521
+ const SELECTOR_SEPARATOR = "/";
522
+ /**
523
+ * Pi's tool name for a downstream tool.
524
+ *
525
+ * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names
526
+ * are a contract with agent frontmatter selectors, transcripts, and user habit, so
527
+ * they stay stable across the swap rather than following mcp-proxy's internal
528
+ * scheme, which prefixes only on a clash.
529
+ */
530
+ function toPiToolName(serverName, toolName) {
531
+ return `${serverName.replace(/[^a-zA-Z0-9]/g, "_")}_${toolName}`;
532
+ }
533
+ function toCatalogTool(serverName, tool) {
534
+ return {
535
+ piName: toPiToolName(serverName, tool.name),
536
+ toolName: tool.name,
537
+ serverName,
538
+ ...tool.description ? { description: tool.description } : {},
539
+ inputSchema: tool.inputSchema,
540
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations },
541
+ ...tool.outputSchema === void 0 ? {} : { outputSchema: tool.outputSchema },
542
+ ...tool._meta === void 0 ? {} : { _meta: tool._meta }
543
+ };
544
+ }
545
+ /**
546
+ * The session's MCP servers and the tools registered for each.
547
+ *
548
+ * Seeded from the previous run's catalog so tools exist before any socket opens,
549
+ * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no
550
+ * way to unregister one, so a tool whose server later fails stays registered and is
551
+ * simply left out of the active set.
552
+ */
553
+ var McpCatalog = class {
554
+ directTools;
555
+ entries = /* @__PURE__ */ new Map();
556
+ diagnostics = [];
557
+ /** Pi names already claimed, so two servers cannot register the same one. */
558
+ claimed = /* @__PURE__ */ new Map();
559
+ /**
560
+ * @param directTools - A child agent's tool selection. Undefined in a normal
561
+ * session, where every configured tool is available.
562
+ */
563
+ constructor(directTools) {
564
+ this.directTools = directTools;
565
+ }
566
+ /**
567
+ * Seeds the servers this session will connect to, all still `not-connected`.
568
+ *
569
+ * Configured names come first so a run with no cache still reports what it is
570
+ * about to reach rather than looking empty; the cache then fills in their tools.
571
+ * A cached server that configuration has since dropped is not seeded at all.
572
+ */
573
+ seed(catalog, configuredServers = []) {
574
+ for (const name of configuredServers) this.entries.set(name, {
575
+ name,
576
+ state: NOT_CONNECTED,
577
+ tools: [],
578
+ resourceCount: 0
579
+ });
580
+ for (const server of catalog.servers) {
581
+ if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;
582
+ this.entries.set(server.name, {
583
+ name: server.name,
584
+ state: NOT_CONNECTED,
585
+ tools: this.claimTools(server.name, server.tools),
586
+ resourceCount: 0
587
+ });
588
+ }
589
+ }
590
+ /**
591
+ * Hides or restores a server's tools for this session.
592
+ *
593
+ * Only the flag moves: `state` keeps tracking what the server reports, so
594
+ * re-enabling lands on the current truth rather than on a remembered value that
595
+ * may have gone stale while the server was hidden.
596
+ */
597
+ setDisabled(serverName, disabled) {
598
+ const entry = this.entries.get(serverName);
599
+ if (entry) entry.disabled = disabled;
600
+ else this.entries.set(serverName, {
601
+ name: serverName,
602
+ state: NOT_CONNECTED,
603
+ disabled,
604
+ tools: [],
605
+ resourceCount: 0
606
+ });
607
+ }
608
+ /** Records how many resources a server offered the last time they were listed. */
609
+ setResourceCount(serverName, resourceCount) {
610
+ const entry = this.entries.get(serverName);
611
+ if (entry) entry.resourceCount = resourceCount;
612
+ }
613
+ /**
614
+ * Folds a live server transition into the catalog.
615
+ *
616
+ * Returns the tools that are new to this catalog, which is what the caller has to
617
+ * register with Pi. A reconnect returns nothing, since its tools already exist.
618
+ */
619
+ applyStateChange(change, tools, resourceCount) {
620
+ const entry = this.entries.get(change.serverName) ?? {
621
+ name: change.serverName,
622
+ state: change.state,
623
+ tools: [],
624
+ resourceCount: 0
625
+ };
626
+ entry.state = change.state;
627
+ if (change.error) entry.error = change.error;
628
+ else delete entry.error;
629
+ if (resourceCount !== void 0) entry.resourceCount = resourceCount;
630
+ if (change.state === "connected" && tools !== void 0) {
631
+ const known = new Map(entry.tools.map((tool) => [tool.toolName, tool]));
632
+ const next = [];
633
+ const added = [];
634
+ for (const tool of tools) {
635
+ if (known.get(tool.name) !== void 0) {
636
+ next.push(toCatalogTool(change.serverName, tool));
637
+ continue;
638
+ }
639
+ const [claimed] = this.claimTools(change.serverName, [tool]);
640
+ if (claimed !== void 0) {
641
+ next.push(claimed);
642
+ added.push(claimed);
643
+ }
644
+ }
645
+ entry.tools = next;
646
+ this.entries.set(change.serverName, entry);
647
+ return added;
648
+ }
649
+ this.entries.set(change.serverName, entry);
650
+ return [];
651
+ }
652
+ /**
653
+ * Tool names Pi should currently expose.
654
+ *
655
+ * Optimistic: a server that has not reported yet still contributes its cached
656
+ * tools, because calling one connects it on demand. The live background pass can
657
+ * take longer than Pi startup, so requiring `connected` first would make a warm
658
+ * session needlessly start with no tools.
659
+ *
660
+ * Only a server that is known to be unusable, or one the user disabled, is held
661
+ * back.
662
+ */
663
+ activeToolNames() {
664
+ const names = [];
665
+ for (const entry of this.entries.values()) {
666
+ if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;
667
+ for (const tool of entry.tools) {
668
+ if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;
669
+ names.push(tool.piName);
670
+ }
671
+ }
672
+ return names;
673
+ }
674
+ /** Every tool ever registered, whether or not its server is reachable. */
675
+ allTools() {
676
+ return [...this.entries.values()].flatMap((entry) => entry.tools);
677
+ }
678
+ /** Resolve configured selectors only to tool names this authoritative catalog confirms. */
679
+ resolveToolSelectors(selectors) {
680
+ let allServers = false;
681
+ const selectedServers = /* @__PURE__ */ new Set();
682
+ const selectedTools = /* @__PURE__ */ new Map();
683
+ for (const selection of selectors) {
684
+ const item = selection.trim().replace(/\/+$/u, "");
685
+ if (!item) continue;
686
+ if (item === ALL_SERVERS_SELECTOR) {
687
+ allServers = true;
688
+ continue;
689
+ }
690
+ const separator = item.indexOf(SELECTOR_SEPARATOR);
691
+ if (separator === -1) {
692
+ selectedServers.add(item);
693
+ continue;
694
+ }
695
+ const server = item.slice(0, separator);
696
+ const tool = item.slice(separator + 1);
697
+ if (!server) continue;
698
+ if (!tool) {
699
+ selectedServers.add(server);
700
+ continue;
701
+ }
702
+ const names = selectedTools.get(server) ?? /* @__PURE__ */ new Set();
703
+ names.add(tool);
704
+ selectedTools.set(server, names);
705
+ }
706
+ const resolved = [];
707
+ const seen = /* @__PURE__ */ new Set();
708
+ for (const tool of this.allTools()) {
709
+ if (!isModelVisibleMcpTool(tool)) continue;
710
+ if (!(allServers || selectedServers.has(tool.serverName)) && !selectedTools.get(tool.serverName)?.has(tool.toolName)) continue;
711
+ if (seen.has(tool.piName)) continue;
712
+ seen.add(tool.piName);
713
+ resolved.push({
714
+ name: tool.piName,
715
+ selector: `${tool.serverName}${SELECTOR_SEPARATOR}${tool.toolName}`
716
+ });
717
+ }
718
+ return resolved;
719
+ }
720
+ findTool(piName) {
721
+ return this.allTools().find((tool) => tool.piName === piName);
722
+ }
723
+ /**
724
+ * The cross-extension status shape.
725
+ *
726
+ * A disabled server reports `disabled` as its state, because that is the one word
727
+ * the wire contract has for "configured but contributing nothing", and consumers
728
+ * outside this process have no use for the transition underneath it.
729
+ */
730
+ toSnapshot() {
731
+ return { servers: [...this.entries.values()].map((entry) => ({
732
+ name: entry.name,
733
+ state: entry.disabled ? DISABLED : entry.state,
734
+ tools: entry.tools.filter(isModelVisibleMcpTool).map((tool) => tool.piName),
735
+ resourceCount: entry.resourceCount,
736
+ ...entry.error ? { error: entry.error } : {}
737
+ })) };
738
+ }
739
+ /**
740
+ * The fuller picture the overlay renders.
741
+ *
742
+ * Carries what the snapshot cannot: each tool's downstream name and description,
743
+ * whether a child agent's selection withholds it, and `enabled` as its own field
744
+ * so the overlay can offer disable and enable against the real server state.
745
+ */
746
+ toView() {
747
+ return [...this.entries.values()].map((entry) => ({
748
+ name: entry.name,
749
+ state: entry.state,
750
+ tools: entry.tools.map((tool) => ({
751
+ piName: tool.piName,
752
+ toolName: tool.toolName,
753
+ ...tool.description ? { description: tool.description } : {},
754
+ active: isModelVisibleMcpTool(tool) && (!this.directTools || this.directTools.allows(entry.name, tool.toolName))
755
+ })),
756
+ resourceCount: entry.resourceCount,
757
+ enabled: !entry.disabled,
758
+ ...entry.error ? { error: entry.error } : {}
759
+ }));
760
+ }
761
+ /** Problems worth surfacing to the user. Reported, never thrown. */
762
+ getDiagnostics() {
763
+ return this.diagnostics;
764
+ }
765
+ /**
766
+ * Records a problem for the user without interrupting whatever hit it.
767
+ *
768
+ * Deduplicated: a fault that recurs on every repaint would otherwise grow this
769
+ * list for as long as the session runs, and the second copy tells nobody
770
+ * anything the first did not.
771
+ */
772
+ addDiagnostic(diagnostic) {
773
+ if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);
774
+ }
775
+ /**
776
+ * Assigns Pi names, dropping any that another server already owns.
777
+ *
778
+ * Two server names that normalize to the same prefix (`code-intel` and
779
+ * `code_intel`) would otherwise have one silently shadow the other's tool.
780
+ */
781
+ claimTools(serverName, tools) {
782
+ const claimed = [];
783
+ for (const tool of tools) {
784
+ const candidate = toCatalogTool(serverName, tool);
785
+ const owner = this.claimed.get(candidate.piName);
786
+ if (owner !== void 0 && owner !== serverName) {
787
+ this.diagnostics.push(`MCP tool "${candidate.piName}" from "${serverName}" was dropped: "${owner}" already registered that name.`);
788
+ continue;
789
+ }
790
+ this.claimed.set(candidate.piName, serverName);
791
+ claimed.push(candidate);
792
+ }
793
+ return claimed;
794
+ }
795
+ };
796
+ //#endregion
797
+ Object.defineProperty(exports, "DIRECT_TOOLS_ENV", {
798
+ enumerable: true,
799
+ get: function() {
800
+ return DIRECT_TOOLS_ENV;
801
+ }
802
+ });
803
+ Object.defineProperty(exports, "McpCatalog", {
804
+ enumerable: true,
805
+ get: function() {
806
+ return McpCatalog;
807
+ }
808
+ });
809
+ Object.defineProperty(exports, "McpRuntimeOwner", {
810
+ enumerable: true,
811
+ get: function() {
812
+ return McpRuntimeOwner;
813
+ }
814
+ });
815
+ Object.defineProperty(exports, "NO_DIRECT_TOOLS", {
816
+ enumerable: true,
817
+ get: function() {
818
+ return NO_DIRECT_TOOLS;
819
+ }
820
+ });
821
+ Object.defineProperty(exports, "PROXY_SERVER_NAME", {
822
+ enumerable: true,
823
+ get: function() {
824
+ return PROXY_SERVER_NAME;
825
+ }
826
+ });
827
+ Object.defineProperty(exports, "buildMcpConfigGroups", {
828
+ enumerable: true,
829
+ get: function() {
830
+ return buildMcpConfigGroups;
831
+ }
832
+ });
833
+ Object.defineProperty(exports, "definitionsCachePath", {
834
+ enumerable: true,
835
+ get: function() {
836
+ return definitionsCachePath;
837
+ }
838
+ });
839
+ Object.defineProperty(exports, "hubCallbackSink", {
840
+ enumerable: true,
841
+ get: function() {
842
+ return hubCallbackSink;
843
+ }
844
+ });
845
+ Object.defineProperty(exports, "isAppVisibleMcpTool", {
846
+ enumerable: true,
847
+ get: function() {
848
+ return isAppVisibleMcpTool;
849
+ }
850
+ });
851
+ Object.defineProperty(exports, "isModelVisibleMcpTool", {
852
+ enumerable: true,
853
+ get: function() {
854
+ return isModelVisibleMcpTool;
855
+ }
856
+ });
857
+ Object.defineProperty(exports, "mcpRuntime_exports", {
858
+ enumerable: true,
859
+ get: function() {
860
+ return mcpRuntime_exports;
861
+ }
862
+ });
863
+ Object.defineProperty(exports, "mcpToolExecutionMetadata", {
864
+ enumerable: true,
865
+ get: function() {
866
+ return mcpToolExecutionMetadata;
867
+ }
868
+ });
869
+ Object.defineProperty(exports, "normalizeMcpAppMetadata", {
870
+ enumerable: true,
871
+ get: function() {
872
+ return normalizeMcpAppMetadata;
873
+ }
874
+ });
875
+ Object.defineProperty(exports, "readCachedCatalog", {
876
+ enumerable: true,
877
+ get: function() {
878
+ return readCachedCatalog;
879
+ }
880
+ });
881
+ Object.defineProperty(exports, "readSessionConfig", {
882
+ enumerable: true,
883
+ get: function() {
884
+ return readSessionConfig;
885
+ }
886
+ });
887
+ Object.defineProperty(exports, "sessionConfigEnvironment", {
888
+ enumerable: true,
889
+ get: function() {
890
+ return sessionConfigEnvironment;
891
+ }
892
+ });
893
+ Object.defineProperty(exports, "toPiToolName", {
894
+ enumerable: true,
895
+ get: function() {
896
+ return toPiToolName;
897
+ }
898
+ });
899
+
900
+ //# sourceMappingURL=mcpCatalog-6Tpt6Gqn.cjs.map