@agimon-ai/doompi-mcp 0.0.1-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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/commands/mcpCommand.cjs +3 -0
  5. package/dist/commands/mcpCommand.cjs.map +1 -0
  6. package/dist/commands/mcpCommand.mjs +3 -0
  7. package/dist/commands/mcpCommand.mjs.map +1 -0
  8. package/dist/constants.cjs +2 -0
  9. package/dist/constants.cjs.map +1 -0
  10. package/dist/constants.mjs +2 -0
  11. package/dist/constants.mjs.map +1 -0
  12. package/dist/extension.cjs +2 -0
  13. package/dist/extension.cjs.map +1 -0
  14. package/dist/extension.d.cts +18 -0
  15. package/dist/extension.d.cts.map +1 -0
  16. package/dist/extension.d.mts +18 -0
  17. package/dist/extension.d.mts.map +1 -0
  18. package/dist/extension.mjs +2 -0
  19. package/dist/extension.mjs.map +1 -0
  20. package/dist/extensions/doom.cjs +2 -0
  21. package/dist/extensions/doom.cjs.map +1 -0
  22. package/dist/extensions/doom.d.cts +6 -0
  23. package/dist/extensions/doom.d.cts.map +1 -0
  24. package/dist/extensions/doom.d.mts +7 -0
  25. package/dist/extensions/doom.d.mts.map +1 -0
  26. package/dist/extensions/doom.mjs +2 -0
  27. package/dist/extensions/doom.mjs.map +1 -0
  28. package/dist/extensions/pi.cjs +2 -0
  29. package/dist/extensions/pi.cjs.map +1 -0
  30. package/dist/extensions/pi.d.cts +2 -0
  31. package/dist/extensions/pi.d.mts +2 -0
  32. package/dist/extensions/pi.mjs +2 -0
  33. package/dist/extensions/pi.mjs.map +1 -0
  34. package/dist/index.cjs +1 -0
  35. package/dist/index.d.cts +7 -0
  36. package/dist/index.d.mts +7 -0
  37. package/dist/index.mjs +1 -0
  38. package/dist/leader.cjs +2 -0
  39. package/dist/leader.cjs.map +1 -0
  40. package/dist/leader.mjs +2 -0
  41. package/dist/leader.mjs.map +1 -0
  42. package/dist/services/configSources.cjs +2 -0
  43. package/dist/services/configSources.cjs.map +1 -0
  44. package/dist/services/configSources.d.cts +65 -0
  45. package/dist/services/configSources.d.cts.map +1 -0
  46. package/dist/services/configSources.d.mts +65 -0
  47. package/dist/services/configSources.d.mts.map +1 -0
  48. package/dist/services/configSources.mjs +2 -0
  49. package/dist/services/configSources.mjs.map +1 -0
  50. package/dist/services/directTools.cjs +2 -0
  51. package/dist/services/directTools.cjs.map +1 -0
  52. package/dist/services/directTools.d.cts +14 -0
  53. package/dist/services/directTools.d.cts.map +1 -0
  54. package/dist/services/directTools.d.mts +14 -0
  55. package/dist/services/directTools.d.mts.map +1 -0
  56. package/dist/services/directTools.mjs +2 -0
  57. package/dist/services/directTools.mjs.map +1 -0
  58. package/dist/services/keyringTokenStore.cjs +2 -0
  59. package/dist/services/keyringTokenStore.cjs.map +1 -0
  60. package/dist/services/keyringTokenStore.mjs +2 -0
  61. package/dist/services/keyringTokenStore.mjs.map +1 -0
  62. package/dist/services/mcpCatalog.cjs +2 -0
  63. package/dist/services/mcpCatalog.cjs.map +1 -0
  64. package/dist/services/mcpCatalog.d.cts +17 -0
  65. package/dist/services/mcpCatalog.d.cts.map +1 -0
  66. package/dist/services/mcpCatalog.d.mts +17 -0
  67. package/dist/services/mcpCatalog.d.mts.map +1 -0
  68. package/dist/services/mcpCatalog.mjs +2 -0
  69. package/dist/services/mcpCatalog.mjs.map +1 -0
  70. package/dist/services/mcpRuntime.cjs +2 -0
  71. package/dist/services/mcpRuntime.cjs.map +1 -0
  72. package/dist/services/mcpRuntime.d.cts +26 -0
  73. package/dist/services/mcpRuntime.d.cts.map +1 -0
  74. package/dist/services/mcpRuntime.d.mts +26 -0
  75. package/dist/services/mcpRuntime.d.mts.map +1 -0
  76. package/dist/services/mcpRuntime.mjs +2 -0
  77. package/dist/services/mcpRuntime.mjs.map +1 -0
  78. package/dist/services/mcpSession.cjs +2 -0
  79. package/dist/services/mcpSession.cjs.map +1 -0
  80. package/dist/services/mcpSession.mjs +2 -0
  81. package/dist/services/mcpSession.mjs.map +1 -0
  82. package/dist/services/sessionConfig.cjs +2 -0
  83. package/dist/services/sessionConfig.cjs.map +1 -0
  84. package/dist/services/sessionConfig.d.cts +25 -0
  85. package/dist/services/sessionConfig.d.cts.map +1 -0
  86. package/dist/services/sessionConfig.d.mts +25 -0
  87. package/dist/services/sessionConfig.d.mts.map +1 -0
  88. package/dist/services/sessionConfig.mjs +2 -0
  89. package/dist/services/sessionConfig.mjs.map +1 -0
  90. package/dist/services/sharedRuntime.cjs +2 -0
  91. package/dist/services/sharedRuntime.cjs.map +1 -0
  92. package/dist/services/sharedRuntime.mjs +2 -0
  93. package/dist/services/sharedRuntime.mjs.map +1 -0
  94. package/dist/tools/mcpTools.cjs +3 -0
  95. package/dist/tools/mcpTools.cjs.map +1 -0
  96. package/dist/tools/mcpTools.mjs +3 -0
  97. package/dist/tools/mcpTools.mjs.map +1 -0
  98. package/dist/tui/mcpOverlay.cjs +2 -0
  99. package/dist/tui/mcpOverlay.cjs.map +1 -0
  100. package/dist/tui/mcpOverlay.mjs +2 -0
  101. package/dist/tui/mcpOverlay.mjs.map +1 -0
  102. package/dist/types.d.cts +2 -0
  103. package/dist/types.d.mts +2 -0
  104. package/package.json +81 -0
@@ -0,0 +1,2 @@
1
+ const e=`MCP_DIRECT_TOOLS`,t=`__none__`;var n=class{servers=new Set;tools=new Map;constructor(e){for(let t of e){let e=t.trim().replace(/\/+$/,``);if(!e)continue;let n=e.indexOf(`/`);if(n===-1){this.servers.add(e);continue}let r=e.slice(0,n),i=e.slice(n+1);if(!r)continue;if(!i){this.servers.add(r);continue}let a=this.tools.get(r)??new Set;a.add(i),this.tools.set(r,a)}}allows(e,t){return this.servers.has(e)?!0:this.tools.get(e)?.has(t)??!1}};function r(t=process.env){let r=t[e];if(r!==void 0&&r!==``)return r===`__none__`?new n([]):new n(r.split(`,`))}export{e as DIRECT_TOOLS_ENV,n as DirectToolFilter,t as NO_DIRECT_TOOLS,r as readDirectToolFilter};
2
+ //# sourceMappingURL=directTools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directTools.mjs","names":[],"sources":["../../src/services/directTools.ts"],"sourcesContent":["/**\n * Environment contract for a child agent's MCP tool selection.\n *\n * Doom Team launches children with a strict tool allowlist and publishes the\n * selectors it resolved here. The name is inherited from `pi-mcp-adapter` so the\n * launcher side did not have to change when this package replaced it.\n */\nexport const DIRECT_TOOLS_ENV = 'MCP_DIRECT_TOOLS';\n\n/** Explicit \"no MCP tools\", distinct from an unset variable meaning \"no limit\". */\nexport const NO_DIRECT_TOOLS = '__none__';\n\nconst SELECTOR_SEPARATOR = '/';\n\n/**\n * Which downstream tools a child may see.\n *\n * A selector is either a whole server (`pencil`) or one tool of it\n * (`pencil/get_screenshot`).\n */\nexport class DirectToolFilter {\n private readonly servers = new Set<string>();\n private readonly tools = new Map<string, Set<string>>();\n\n constructor(selectors: readonly string[]) {\n for (const raw of selectors) {\n const selector = raw.trim().replace(/\\/+$/, '');\n if (!selector) continue;\n const separator = selector.indexOf(SELECTOR_SEPARATOR);\n if (separator === -1) {\n this.servers.add(selector);\n continue;\n }\n const server = selector.slice(0, separator);\n const tool = selector.slice(separator + 1);\n if (!server) continue;\n if (!tool) {\n this.servers.add(server);\n continue;\n }\n const selected = this.tools.get(server) ?? new Set<string>();\n selected.add(tool);\n this.tools.set(server, selected);\n }\n }\n\n allows(serverName: string, toolName: string): boolean {\n if (this.servers.has(serverName)) return true;\n return this.tools.get(serverName)?.has(toolName) ?? false;\n }\n}\n\n/**\n * Reads the child's tool selection from the environment.\n *\n * An unset variable means this is not a restricted child, so every configured\n * tool is available. The explicit sentinel means the launcher resolved nothing\n * and the child gets no MCP tools at all, which is not the same as unset.\n */\nexport function readDirectToolFilter(environment: NodeJS.ProcessEnv = process.env): DirectToolFilter | undefined {\n const raw = environment[DIRECT_TOOLS_ENV];\n if (raw === undefined || raw === '') return undefined;\n if (raw === NO_DIRECT_TOOLS) return new DirectToolFilter([]);\n return new DirectToolFilter(raw.split(','));\n}\n"],"mappings":"AAOA,MAAa,EAAmB,mBAGnB,EAAkB,WAU/B,IAAa,EAAb,KAA8B,CAC5B,QAA2B,IAAI,IAC/B,MAAyB,IAAI,IAE7B,YAAY,EAA8B,CACxC,IAAK,IAAM,KAAO,EAAW,CAC3B,IAAM,EAAW,EAAI,KAAK,CAAC,CAAC,QAAQ,OAAQ,EAAE,EAC9C,GAAI,CAAC,EAAU,SACf,IAAM,EAAY,EAAS,QAAQ,GAAkB,EACrD,GAAI,IAAc,GAAI,CACpB,KAAK,QAAQ,IAAI,CAAQ,EACzB,QACF,CACA,IAAM,EAAS,EAAS,MAAM,EAAG,CAAS,EACpC,EAAO,EAAS,MAAM,EAAY,CAAC,EACzC,GAAI,CAAC,EAAQ,SACb,GAAI,CAAC,EAAM,CACT,KAAK,QAAQ,IAAI,CAAM,EACvB,QACF,CACA,IAAM,EAAW,KAAK,MAAM,IAAI,CAAM,GAAK,IAAI,IAC/C,EAAS,IAAI,CAAI,EACjB,KAAK,MAAM,IAAI,EAAQ,CAAQ,CACjC,CACF,CAEA,OAAO,EAAoB,EAA2B,CAEpD,OADI,KAAK,QAAQ,IAAI,CAAU,EAAU,GAClC,KAAK,MAAM,IAAI,CAAU,CAAC,EAAE,IAAI,CAAQ,GAAK,EACtD,CACF,EASA,SAAgB,EAAqB,EAAiC,QAAQ,IAAmC,CAC/G,IAAM,EAAM,EAAY,GACpB,OAAQ,IAAA,IAAa,IAAQ,GAEjC,OADI,IAAA,WAAgC,IAAI,EAAiB,CAAC,CAAC,EACpD,IAAI,EAAiB,EAAI,MAAM,GAAG,CAAC,CAC5C"}
@@ -0,0 +1,2 @@
1
+ let e=require("@agimon-ai/mcp-proxy");const t=`ai.agimon.doom-mcp.oauth`;var n=class{createEntry;fallback;degraded=!1;constructor(t,n=new e.FileTokenStore){this.createEntry=t,this.fallback=n}get isDegraded(){return this.degraded}async read(e){if(this.degraded)return this.fallback.read(e);let n;try{n=await this.createEntry(t,e).getPassword()}catch{return this.degraded=!0,this.fallback.read(e)}if(n)try{return JSON.parse(n)}catch{return}}async write(e,n){if(!this.degraded)try{await this.createEntry(t,e).setPassword(JSON.stringify(n));return}catch{this.degraded=!0}await this.fallback.write(e,n)}async clear(e){let n;if(!this.degraded)try{await this.createEntry(t,e).deletePassword()}catch(e){n=e}if(await this.fallback.clear(e),n!==void 0){let t=n instanceof Error?n.message:JSON.stringify(n);throw Error(`Keyring credential for "${e}" could not be removed: ${t}`,{cause:n})}}};async function r(t=new e.FileTokenStore){try{let{AsyncEntry:e}=await import(`@napi-rs/keyring`);return new n((t,n)=>new e(t,n),t)}catch{return t}}exports.KEYRING_SERVICE=t,exports.KeyringTokenStore=n,exports.createTokenStore=r;
2
+ //# sourceMappingURL=keyringTokenStore.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyringTokenStore.cjs","names":["FileTokenStore"],"sources":["../../src/services/keyringTokenStore.ts"],"sourcesContent":["import { type AuthEntry, FileTokenStore, type TokenStore } from '@agimon-ai/mcp-proxy';\n\n/** Keyring service name. One credential per downstream MCP server. */\nexport const KEYRING_SERVICE = 'ai.agimon.doom-mcp.oauth';\n\n/** The slice of `@napi-rs/keyring`'s `AsyncEntry` this store uses. */\nexport interface KeyringEntry {\n getPassword(): Promise<string | undefined>;\n setPassword(password: string): Promise<void>;\n deletePassword(): Promise<unknown>;\n}\n\nexport type KeyringEntryFactory = (service: string, account: string) => KeyringEntry;\n\n/**\n * OAuth credentials in the OS keyring, with an owner-only file store behind it.\n *\n * The keyring is a native module and the platform service it talks to may be absent\n * (a headless Linux box with no secret service, an unsupported architecture). A\n * missing credential is reported as `undefined` rather than raised, so a rejection\n * means the keyring itself is unusable and the store degrades to the file fallback\n * for the rest of the session instead of failing the run.\n */\nexport class KeyringTokenStore implements TokenStore {\n private degraded = false;\n\n constructor(\n private readonly createEntry: KeyringEntryFactory,\n private readonly fallback: TokenStore = new FileTokenStore(),\n ) {}\n\n /** True once the keyring has failed and the file store has taken over. */\n get isDegraded(): boolean {\n return this.degraded;\n }\n\n async read(serverName: string): Promise<AuthEntry | undefined> {\n if (this.degraded) return this.fallback.read(serverName);\n let raw: string | undefined;\n try {\n raw = await this.createEntry(KEYRING_SERVICE, serverName).getPassword();\n } catch {\n this.degraded = true;\n return this.fallback.read(serverName);\n }\n if (!raw) return undefined;\n try {\n return JSON.parse(raw) as AuthEntry;\n } catch {\n // A credential we cannot parse is treated as absent so the next authorization\n // overwrites it, rather than wedging the server on every start.\n return undefined;\n }\n }\n\n async write(serverName: string, entry: AuthEntry): Promise<void> {\n if (!this.degraded) {\n try {\n await this.createEntry(KEYRING_SERVICE, serverName).setPassword(JSON.stringify(entry));\n return;\n } catch {\n this.degraded = true;\n }\n }\n await this.fallback.write(serverName, entry);\n }\n\n /**\n * Removes the credential from both stores.\n *\n * A session that degraded part way through can have left a copy in either place,\n * and \"signed out\" has to mean no credential survives anywhere. A keyring that\n * refuses the delete is reported rather than absorbed: the caller was told the\n * credential is gone, and it is not. The fallback is still cleared first so the\n * failure leaves one surviving copy instead of two.\n */\n async clear(serverName: string): Promise<void> {\n let keyringFailure: unknown;\n if (!this.degraded) {\n try {\n await this.createEntry(KEYRING_SERVICE, serverName).deletePassword();\n } catch (error) {\n keyringFailure = error;\n }\n }\n await this.fallback.clear(serverName);\n if (keyringFailure !== undefined) {\n const detail = keyringFailure instanceof Error ? keyringFailure.message : JSON.stringify(keyringFailure);\n throw new Error(`Keyring credential for \"${serverName}\" could not be removed: ${detail}`, {\n cause: keyringFailure,\n });\n }\n }\n}\n\n/**\n * Builds the token store for this host, falling back when the native module is absent.\n *\n * doom-mcp loads inside Pi, so a platform without a prebuilt keyring binary must\n * still get a working session rather than a failed extension install.\n */\nexport async function createTokenStore(fallback: TokenStore = new FileTokenStore()): Promise<TokenStore> {\n try {\n const { AsyncEntry } = await import('@napi-rs/keyring');\n return new KeyringTokenStore((service, account) => new AsyncEntry(service, account), fallback);\n } catch {\n return fallback;\n }\n}\n"],"mappings":"sCAGA,MAAa,EAAkB,2BAoB/B,IAAa,EAAb,KAAqD,CAIhC,YACA,SAJnB,SAAmB,GAEnB,YACE,EACA,EAAwC,IAAIA,EAAAA,eAC5C,CAFiB,KAAA,YAAA,EACA,KAAA,SAAA,CAChB,CAGH,IAAI,YAAsB,CACxB,OAAO,KAAK,QACd,CAEA,MAAM,KAAK,EAAoD,CAC7D,GAAI,KAAK,SAAU,OAAO,KAAK,SAAS,KAAK,CAAU,EACvD,IAAI,EACJ,GAAI,CACF,EAAM,MAAM,KAAK,YAAY,EAAiB,CAAU,CAAC,CAAC,YAAY,CACxE,MAAQ,CAEN,MADA,MAAK,SAAW,GACT,KAAK,SAAS,KAAK,CAAU,CACtC,CACK,KACL,GAAI,CACF,OAAO,KAAK,MAAM,CAAG,CACvB,MAAQ,CAGN,MACF,CACF,CAEA,MAAM,MAAM,EAAoB,EAAiC,CAC/D,GAAI,CAAC,KAAK,SACR,GAAI,CACF,MAAM,KAAK,YAAY,EAAiB,CAAU,CAAC,CAAC,YAAY,KAAK,UAAU,CAAK,CAAC,EACrF,MACF,MAAQ,CACN,KAAK,SAAW,EAClB,CAEF,MAAM,KAAK,SAAS,MAAM,EAAY,CAAK,CAC7C,CAWA,MAAM,MAAM,EAAmC,CAC7C,IAAI,EACJ,GAAI,CAAC,KAAK,SACR,GAAI,CACF,MAAM,KAAK,YAAY,EAAiB,CAAU,CAAC,CAAC,eAAe,CACrE,OAAS,EAAO,CACd,EAAiB,CACnB,CAGF,GADA,MAAM,KAAK,SAAS,MAAM,CAAU,EAChC,IAAmB,IAAA,GAAW,CAChC,IAAM,EAAS,aAA0B,MAAQ,EAAe,QAAU,KAAK,UAAU,CAAc,EACvG,MAAU,MAAM,2BAA2B,EAAW,0BAA0B,IAAU,CACxF,MAAO,CACT,CAAC,CACH,CACF,CACF,EAQA,eAAsB,EAAiB,EAAuB,IAAIA,EAAAA,eAAuC,CACvG,GAAI,CACF,GAAM,CAAE,cAAe,MAAM,OAAO,oBACpC,OAAO,IAAI,GAAmB,EAAS,IAAY,IAAI,EAAW,EAAS,CAAO,EAAG,CAAQ,CAC/F,MAAQ,CACN,OAAO,CACT,CACF"}
@@ -0,0 +1,2 @@
1
+ import{FileTokenStore as e}from"@agimon-ai/mcp-proxy";const t=`ai.agimon.doom-mcp.oauth`;var n=class{createEntry;fallback;degraded=!1;constructor(t,n=new e){this.createEntry=t,this.fallback=n}get isDegraded(){return this.degraded}async read(e){if(this.degraded)return this.fallback.read(e);let n;try{n=await this.createEntry(t,e).getPassword()}catch{return this.degraded=!0,this.fallback.read(e)}if(n)try{return JSON.parse(n)}catch{return}}async write(e,n){if(!this.degraded)try{await this.createEntry(t,e).setPassword(JSON.stringify(n));return}catch{this.degraded=!0}await this.fallback.write(e,n)}async clear(e){let n;if(!this.degraded)try{await this.createEntry(t,e).deletePassword()}catch(e){n=e}if(await this.fallback.clear(e),n!==void 0){let t=n instanceof Error?n.message:JSON.stringify(n);throw Error(`Keyring credential for "${e}" could not be removed: ${t}`,{cause:n})}}};async function r(t=new e){try{let{AsyncEntry:e}=await import(`@napi-rs/keyring`);return new n((t,n)=>new e(t,n),t)}catch{return t}}export{t as KEYRING_SERVICE,n as KeyringTokenStore,r as createTokenStore};
2
+ //# sourceMappingURL=keyringTokenStore.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyringTokenStore.mjs","names":[],"sources":["../../src/services/keyringTokenStore.ts"],"sourcesContent":["import { type AuthEntry, FileTokenStore, type TokenStore } from '@agimon-ai/mcp-proxy';\n\n/** Keyring service name. One credential per downstream MCP server. */\nexport const KEYRING_SERVICE = 'ai.agimon.doom-mcp.oauth';\n\n/** The slice of `@napi-rs/keyring`'s `AsyncEntry` this store uses. */\nexport interface KeyringEntry {\n getPassword(): Promise<string | undefined>;\n setPassword(password: string): Promise<void>;\n deletePassword(): Promise<unknown>;\n}\n\nexport type KeyringEntryFactory = (service: string, account: string) => KeyringEntry;\n\n/**\n * OAuth credentials in the OS keyring, with an owner-only file store behind it.\n *\n * The keyring is a native module and the platform service it talks to may be absent\n * (a headless Linux box with no secret service, an unsupported architecture). A\n * missing credential is reported as `undefined` rather than raised, so a rejection\n * means the keyring itself is unusable and the store degrades to the file fallback\n * for the rest of the session instead of failing the run.\n */\nexport class KeyringTokenStore implements TokenStore {\n private degraded = false;\n\n constructor(\n private readonly createEntry: KeyringEntryFactory,\n private readonly fallback: TokenStore = new FileTokenStore(),\n ) {}\n\n /** True once the keyring has failed and the file store has taken over. */\n get isDegraded(): boolean {\n return this.degraded;\n }\n\n async read(serverName: string): Promise<AuthEntry | undefined> {\n if (this.degraded) return this.fallback.read(serverName);\n let raw: string | undefined;\n try {\n raw = await this.createEntry(KEYRING_SERVICE, serverName).getPassword();\n } catch {\n this.degraded = true;\n return this.fallback.read(serverName);\n }\n if (!raw) return undefined;\n try {\n return JSON.parse(raw) as AuthEntry;\n } catch {\n // A credential we cannot parse is treated as absent so the next authorization\n // overwrites it, rather than wedging the server on every start.\n return undefined;\n }\n }\n\n async write(serverName: string, entry: AuthEntry): Promise<void> {\n if (!this.degraded) {\n try {\n await this.createEntry(KEYRING_SERVICE, serverName).setPassword(JSON.stringify(entry));\n return;\n } catch {\n this.degraded = true;\n }\n }\n await this.fallback.write(serverName, entry);\n }\n\n /**\n * Removes the credential from both stores.\n *\n * A session that degraded part way through can have left a copy in either place,\n * and \"signed out\" has to mean no credential survives anywhere. A keyring that\n * refuses the delete is reported rather than absorbed: the caller was told the\n * credential is gone, and it is not. The fallback is still cleared first so the\n * failure leaves one surviving copy instead of two.\n */\n async clear(serverName: string): Promise<void> {\n let keyringFailure: unknown;\n if (!this.degraded) {\n try {\n await this.createEntry(KEYRING_SERVICE, serverName).deletePassword();\n } catch (error) {\n keyringFailure = error;\n }\n }\n await this.fallback.clear(serverName);\n if (keyringFailure !== undefined) {\n const detail = keyringFailure instanceof Error ? keyringFailure.message : JSON.stringify(keyringFailure);\n throw new Error(`Keyring credential for \"${serverName}\" could not be removed: ${detail}`, {\n cause: keyringFailure,\n });\n }\n }\n}\n\n/**\n * Builds the token store for this host, falling back when the native module is absent.\n *\n * doom-mcp loads inside Pi, so a platform without a prebuilt keyring binary must\n * still get a working session rather than a failed extension install.\n */\nexport async function createTokenStore(fallback: TokenStore = new FileTokenStore()): Promise<TokenStore> {\n try {\n const { AsyncEntry } = await import('@napi-rs/keyring');\n return new KeyringTokenStore((service, account) => new AsyncEntry(service, account), fallback);\n } catch {\n return fallback;\n }\n}\n"],"mappings":"sDAGA,MAAa,EAAkB,2BAoB/B,IAAa,EAAb,KAAqD,CAIhC,YACA,SAJnB,SAAmB,GAEnB,YACE,EACA,EAAwC,IAAI,EAC5C,CAFiB,KAAA,YAAA,EACA,KAAA,SAAA,CAChB,CAGH,IAAI,YAAsB,CACxB,OAAO,KAAK,QACd,CAEA,MAAM,KAAK,EAAoD,CAC7D,GAAI,KAAK,SAAU,OAAO,KAAK,SAAS,KAAK,CAAU,EACvD,IAAI,EACJ,GAAI,CACF,EAAM,MAAM,KAAK,YAAY,EAAiB,CAAU,CAAC,CAAC,YAAY,CACxE,MAAQ,CAEN,MADA,MAAK,SAAW,GACT,KAAK,SAAS,KAAK,CAAU,CACtC,CACK,KACL,GAAI,CACF,OAAO,KAAK,MAAM,CAAG,CACvB,MAAQ,CAGN,MACF,CACF,CAEA,MAAM,MAAM,EAAoB,EAAiC,CAC/D,GAAI,CAAC,KAAK,SACR,GAAI,CACF,MAAM,KAAK,YAAY,EAAiB,CAAU,CAAC,CAAC,YAAY,KAAK,UAAU,CAAK,CAAC,EACrF,MACF,MAAQ,CACN,KAAK,SAAW,EAClB,CAEF,MAAM,KAAK,SAAS,MAAM,EAAY,CAAK,CAC7C,CAWA,MAAM,MAAM,EAAmC,CAC7C,IAAI,EACJ,GAAI,CAAC,KAAK,SACR,GAAI,CACF,MAAM,KAAK,YAAY,EAAiB,CAAU,CAAC,CAAC,eAAe,CACrE,OAAS,EAAO,CACd,EAAiB,CACnB,CAGF,GADA,MAAM,KAAK,SAAS,MAAM,CAAU,EAChC,IAAmB,IAAA,GAAW,CAChC,IAAM,EAAS,aAA0B,MAAQ,EAAe,QAAU,KAAK,UAAU,CAAc,EACvG,MAAU,MAAM,2BAA2B,EAAW,0BAA0B,IAAU,CACxF,MAAO,CACT,CAAC,CACH,CACF,CACF,EAQA,eAAsB,EAAiB,EAAuB,IAAI,EAAuC,CACvG,GAAI,CACF,GAAM,CAAE,cAAe,MAAM,OAAO,oBACpC,OAAO,IAAI,GAAmB,EAAS,IAAY,IAAI,EAAW,EAAS,CAAO,EAAG,CAAQ,CAC/F,MAAQ,CACN,OAAO,CACT,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=new Set([`failed`,`needs-auth`]),t=`not-connected`;function n(e,t){return`${e.replace(/[^a-zA-Z0-9]/g,`_`)}_${t}`}function r(e,t){return{piName:n(e,t.name),toolName:t.name,serverName:e,...t.description?{description:t.description}:{},inputSchema:t.inputSchema}}var i=class{directTools;entries=new Map;diagnostics=[];claimed=new Map;constructor(e){this.directTools=e}seed(e,n=[]){for(let e of n)this.entries.set(e,{name:e,state:t,tools:[],resourceCount:0});for(let r of e.servers)n.length>0&&!this.entries.has(r.name)||this.entries.set(r.name,{name:r.name,state:t,tools:this.claimTools(r.name,r.tools),resourceCount:0})}setDisabled(e,n){let r=this.entries.get(e);r?r.disabled=n:this.entries.set(e,{name:e,state:t,disabled:n,tools:[],resourceCount:0})}setResourceCount(e,t){let n=this.entries.get(e);n&&(n.resourceCount=t)}applyStateChange(e,t=[],n){let r=this.entries.get(e.serverName)??{name:e.serverName,state:e.state,tools:[],resourceCount:0};r.state=e.state,e.error?r.error=e.error:delete r.error,n!==void 0&&(r.resourceCount=n);let i=new Set(r.tools.map(e=>e.toolName)),a=this.claimTools(e.serverName,t.filter(e=>!i.has(e.name)));return r.tools=[...r.tools,...a],this.entries.set(e.serverName,r),a}activeToolNames(){let t=[];for(let n of this.entries.values())if(!(n.disabled||e.has(n.state)))for(let e of n.tools)this.directTools&&!this.directTools.allows(n.name,e.toolName)||t.push(e.piName);return t}allTools(){return[...this.entries.values()].flatMap(e=>e.tools)}findTool(e){return this.allTools().find(t=>t.piName===e)}toSnapshot(){return{servers:[...this.entries.values()].map(e=>({name:e.name,state:e.disabled?`disabled`:e.state,tools:e.tools.map(e=>e.piName),resourceCount:e.resourceCount,...e.error?{error:e.error}:{}}))}}toView(){return[...this.entries.values()].map(e=>({name:e.name,state:e.state,tools:e.tools.map(t=>({piName:t.piName,toolName:t.toolName,...t.description?{description:t.description}:{},active:!this.directTools||this.directTools.allows(e.name,t.toolName)})),resourceCount:e.resourceCount,enabled:!e.disabled,...e.error?{error:e.error}:{}}))}getDiagnostics(){return this.diagnostics}addDiagnostic(e){this.diagnostics.includes(e)||this.diagnostics.push(e)}claimTools(e,t){let n=[];for(let i of t){let t=r(e,i),a=this.claimed.get(t.piName);if(a!==void 0&&a!==e){this.diagnostics.push(`MCP tool "${t.piName}" from "${e}" was dropped: "${a}" already registered that name.`);continue}this.claimed.set(t.piName,e),n.push(t)}return n}};exports.McpCatalog=i,exports.toPiToolName=n;
2
+ //# sourceMappingURL=mcpCatalog.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpCatalog.cjs","names":[],"sources":["../../src/services/mcpCatalog.ts"],"sourcesContent":["import type { McpServerSnapshot, McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpServerStateChange, McpToolInfo } from '@agimon-ai/mcp-proxy';\nimport type { McpServerView } from '../types.ts';\nimport type { DirectToolFilter } from './directTools.ts';\nimport type { CachedCatalog } from './mcpRuntime.ts';\n\n/** One downstream tool, with the name Pi will know it by. */\nexport interface CatalogTool {\n /** Name registered with Pi, prefixed by server. */\n piName: string;\n /** Name the downstream server knows. */\n toolName: string;\n serverName: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface CatalogEntry {\n name: string;\n /**\n * The last transition the server actually reported.\n *\n * Never `disabled`: a session disable is an override the user applied, not\n * something the server did, so it lives in its own field. Writing it into\n * `state` would destroy the value re-enabling has to return to.\n */\n state: McpServerSnapshot['state'];\n /** Session-only override. Survives nothing; a restart returns to configuration. */\n disabled?: boolean;\n tools: CatalogTool[];\n resourceCount: number;\n error?: string;\n}\n\n/**\n * States where calling a tool cannot succeed, so its tools are withheld.\n *\n * `closed` is absent deliberately: a server closed after an idle timeout is\n * reconnected by the next call rather than gone. `disabled` is absent because it is\n * no longer a reachable transition; `entry.disabled` carries that instead.\n */\nconst UNAVAILABLE_STATES = new Set<McpServerSnapshot['state']>(['failed', 'needs-auth']);\n\n/** Where a server starts: known to the session, not yet dialled. */\nconst NOT_CONNECTED: McpServerSnapshot['state'] = 'not-connected';\nconst DISABLED: McpServerSnapshot['state'] = 'disabled';\n\n/**\n * Pi's tool name for a downstream tool.\n *\n * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names\n * are a contract with agent frontmatter selectors, transcripts, and user habit, so\n * they stay stable across the swap rather than following mcp-proxy's internal\n * scheme, which prefixes only on a clash.\n */\nexport function toPiToolName(serverName: string, toolName: string): string {\n return `${serverName.replace(/[^a-zA-Z0-9]/g, '_')}_${toolName}`;\n}\n\nfunction toCatalogTool(serverName: string, tool: McpToolInfo): CatalogTool {\n return {\n piName: toPiToolName(serverName, tool.name),\n toolName: tool.name,\n serverName,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: tool.inputSchema,\n };\n}\n\n/**\n * The session's MCP servers and the tools registered for each.\n *\n * Seeded from the previous run's catalog so tools exist before any socket opens,\n * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no\n * way to unregister one, so a tool whose server later fails stays registered and is\n * simply left out of the active set.\n */\nexport class McpCatalog {\n private readonly entries = new Map<string, CatalogEntry>();\n private readonly diagnostics: string[] = [];\n /** Pi names already claimed, so two servers cannot register the same one. */\n private readonly claimed = new Map<string, string>();\n\n /**\n * @param directTools - A child agent's tool selection. Undefined in a normal\n * session, where every configured tool is available.\n */\n constructor(private readonly directTools?: DirectToolFilter) {}\n\n /**\n * Seeds the servers this session will connect to, all still `not-connected`.\n *\n * Configured names come first so a run with no cache still reports what it is\n * about to reach rather than looking empty; the cache then fills in their tools.\n * A cached server that configuration has since dropped is not seeded at all.\n */\n seed(catalog: CachedCatalog, configuredServers: readonly string[] = []): void {\n for (const name of configuredServers) {\n this.entries.set(name, { name, state: NOT_CONNECTED, tools: [], resourceCount: 0 });\n }\n for (const server of catalog.servers) {\n if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;\n this.entries.set(server.name, {\n name: server.name,\n state: NOT_CONNECTED,\n tools: this.claimTools(server.name, server.tools),\n resourceCount: 0,\n });\n }\n }\n\n /**\n * Hides or restores a server's tools for this session.\n *\n * Only the flag moves: `state` keeps tracking what the server reports, so\n * re-enabling lands on the current truth rather than on a remembered value that\n * may have gone stale while the server was hidden.\n */\n setDisabled(serverName: string, disabled: boolean): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.disabled = disabled;\n else\n this.entries.set(serverName, { name: serverName, state: NOT_CONNECTED, disabled, tools: [], resourceCount: 0 });\n }\n\n /** Records how many resources a server offered the last time they were listed. */\n setResourceCount(serverName: string, resourceCount: number): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.resourceCount = resourceCount;\n }\n\n /**\n * Folds a live server transition into the catalog.\n *\n * Returns the tools that are new to this catalog, which is what the caller has to\n * register with Pi. A reconnect returns nothing, since its tools already exist.\n */\n applyStateChange(change: McpServerStateChange, tools: McpToolInfo[] = [], resourceCount?: number): CatalogTool[] {\n const entry = this.entries.get(change.serverName) ?? {\n name: change.serverName,\n state: change.state,\n tools: [],\n resourceCount: 0,\n };\n entry.state = change.state;\n if (change.error) entry.error = change.error;\n else delete entry.error;\n if (resourceCount !== undefined) entry.resourceCount = resourceCount;\n\n const known = new Set(entry.tools.map((tool) => tool.toolName));\n const added = this.claimTools(\n change.serverName,\n tools.filter((tool) => !known.has(tool.name)),\n );\n entry.tools = [...entry.tools, ...added];\n this.entries.set(change.serverName, entry);\n return added;\n }\n\n /**\n * Tool names Pi should currently expose.\n *\n * Optimistic: a server that has not reported yet still contributes its cached\n * tools, because calling one connects it on demand. mcp-proxy skips the eager\n * connection pass whenever it has a usable definitions cache, so requiring a\n * `connected` state first would leave a warm session with no tools at all.\n *\n * Only a server that is known to be unusable, or one the user disabled, is held\n * back.\n */\n activeToolNames(): string[] {\n const names: string[] = [];\n for (const entry of this.entries.values()) {\n if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;\n for (const tool of entry.tools) {\n if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;\n names.push(tool.piName);\n }\n }\n return names;\n }\n\n /** Every tool ever registered, whether or not its server is reachable. */\n allTools(): CatalogTool[] {\n return [...this.entries.values()].flatMap((entry) => entry.tools);\n }\n\n findTool(piName: string): CatalogTool | undefined {\n return this.allTools().find((tool) => tool.piName === piName);\n }\n\n /**\n * The cross-extension status shape.\n *\n * A disabled server reports `disabled` as its state, because that is the one word\n * the wire contract has for \"configured but contributing nothing\", and consumers\n * outside this process have no use for the transition underneath it.\n */\n toSnapshot(): McpStatusSnapshot {\n return {\n servers: [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.disabled ? DISABLED : entry.state,\n tools: entry.tools.map((tool) => tool.piName),\n resourceCount: entry.resourceCount,\n ...(entry.error ? { error: entry.error } : {}),\n })),\n };\n }\n\n /**\n * The fuller picture the overlay renders.\n *\n * Carries what the snapshot cannot: each tool's downstream name and description,\n * whether a child agent's selection withholds it, and `enabled` as its own field\n * so the overlay can offer disable and enable against the real server state.\n */\n toView(): McpServerView[] {\n return [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.state,\n tools: entry.tools.map((tool) => ({\n piName: tool.piName,\n toolName: tool.toolName,\n ...(tool.description ? { description: tool.description } : {}),\n active: !this.directTools || this.directTools.allows(entry.name, tool.toolName),\n })),\n resourceCount: entry.resourceCount,\n enabled: !entry.disabled,\n ...(entry.error ? { error: entry.error } : {}),\n }));\n }\n\n /** Problems worth surfacing to the user. Reported, never thrown. */\n getDiagnostics(): readonly string[] {\n return this.diagnostics;\n }\n\n /**\n * Records a problem for the user without interrupting whatever hit it.\n *\n * Deduplicated: a fault that recurs on every repaint would otherwise grow this\n * list for as long as the session runs, and the second copy tells nobody\n * anything the first did not.\n */\n addDiagnostic(diagnostic: string): void {\n if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);\n }\n\n /**\n * Assigns Pi names, dropping any that another server already owns.\n *\n * Two server names that normalize to the same prefix (`code-intel` and\n * `code_intel`) would otherwise have one silently shadow the other's tool.\n */\n private claimTools(serverName: string, tools: McpToolInfo[]): CatalogTool[] {\n const claimed: CatalogTool[] = [];\n for (const tool of tools) {\n const candidate = toCatalogTool(serverName, tool);\n const owner = this.claimed.get(candidate.piName);\n if (owner !== undefined && owner !== serverName) {\n this.diagnostics.push(\n `MCP tool \"${candidate.piName}\" from \"${serverName}\" was dropped: \"${owner}\" already registered that name.`,\n );\n continue;\n }\n this.claimed.set(candidate.piName, serverName);\n claimed.push(candidate);\n }\n return claimed;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import "../types.cjs";
2
+ import "./mcpRuntime.cjs";
3
+ import "@agimon-ai/doompi-extension-contracts/mcp-status";
4
+ import { McpToolInfo } from "@agimon-ai/mcp-proxy";
5
+ //#region src/services/mcpCatalog.d.ts
6
+ /**
7
+ * Pi's tool name for a downstream tool.
8
+ *
9
+ * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names
10
+ * are a contract with agent frontmatter selectors, transcripts, and user habit, so
11
+ * they stay stable across the swap rather than following mcp-proxy's internal
12
+ * scheme, which prefixes only on a clash.
13
+ */
14
+ declare function toPiToolName(serverName: string, toolName: string): string;
15
+ //#endregion
16
+ export { toPiToolName };
17
+ //# sourceMappingURL=mcpCatalog.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpCatalog.d.cts","names":[],"sources":["../../src/services/mcpCatalog.ts"],"mappings":";;;;;;;;;;;;;iBAuDgB,aAAa,oBAAoB"}
@@ -0,0 +1,17 @@
1
+ import "../types.mjs";
2
+ import "./mcpRuntime.mjs";
3
+ import "@agimon-ai/doompi-extension-contracts/mcp-status";
4
+ import { McpToolInfo } from "@agimon-ai/mcp-proxy";
5
+ //#region src/services/mcpCatalog.d.ts
6
+ /**
7
+ * Pi's tool name for a downstream tool.
8
+ *
9
+ * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names
10
+ * are a contract with agent frontmatter selectors, transcripts, and user habit, so
11
+ * they stay stable across the swap rather than following mcp-proxy's internal
12
+ * scheme, which prefixes only on a clash.
13
+ */
14
+ declare function toPiToolName(serverName: string, toolName: string): string;
15
+ //#endregion
16
+ export { toPiToolName };
17
+ //# sourceMappingURL=mcpCatalog.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpCatalog.d.mts","names":[],"sources":["../../src/services/mcpCatalog.ts"],"mappings":";;;;;;;;;;;;;iBAuDgB,aAAa,oBAAoB"}
@@ -0,0 +1,2 @@
1
+ const e=new Set([`failed`,`needs-auth`]),t=`not-connected`;function n(e,t){return`${e.replace(/[^a-zA-Z0-9]/g,`_`)}_${t}`}function r(e,t){return{piName:n(e,t.name),toolName:t.name,serverName:e,...t.description?{description:t.description}:{},inputSchema:t.inputSchema}}var i=class{directTools;entries=new Map;diagnostics=[];claimed=new Map;constructor(e){this.directTools=e}seed(e,n=[]){for(let e of n)this.entries.set(e,{name:e,state:t,tools:[],resourceCount:0});for(let r of e.servers)n.length>0&&!this.entries.has(r.name)||this.entries.set(r.name,{name:r.name,state:t,tools:this.claimTools(r.name,r.tools),resourceCount:0})}setDisabled(e,n){let r=this.entries.get(e);r?r.disabled=n:this.entries.set(e,{name:e,state:t,disabled:n,tools:[],resourceCount:0})}setResourceCount(e,t){let n=this.entries.get(e);n&&(n.resourceCount=t)}applyStateChange(e,t=[],n){let r=this.entries.get(e.serverName)??{name:e.serverName,state:e.state,tools:[],resourceCount:0};r.state=e.state,e.error?r.error=e.error:delete r.error,n!==void 0&&(r.resourceCount=n);let i=new Set(r.tools.map(e=>e.toolName)),a=this.claimTools(e.serverName,t.filter(e=>!i.has(e.name)));return r.tools=[...r.tools,...a],this.entries.set(e.serverName,r),a}activeToolNames(){let t=[];for(let n of this.entries.values())if(!(n.disabled||e.has(n.state)))for(let e of n.tools)this.directTools&&!this.directTools.allows(n.name,e.toolName)||t.push(e.piName);return t}allTools(){return[...this.entries.values()].flatMap(e=>e.tools)}findTool(e){return this.allTools().find(t=>t.piName===e)}toSnapshot(){return{servers:[...this.entries.values()].map(e=>({name:e.name,state:e.disabled?`disabled`:e.state,tools:e.tools.map(e=>e.piName),resourceCount:e.resourceCount,...e.error?{error:e.error}:{}}))}}toView(){return[...this.entries.values()].map(e=>({name:e.name,state:e.state,tools:e.tools.map(t=>({piName:t.piName,toolName:t.toolName,...t.description?{description:t.description}:{},active:!this.directTools||this.directTools.allows(e.name,t.toolName)})),resourceCount:e.resourceCount,enabled:!e.disabled,...e.error?{error:e.error}:{}}))}getDiagnostics(){return this.diagnostics}addDiagnostic(e){this.diagnostics.includes(e)||this.diagnostics.push(e)}claimTools(e,t){let n=[];for(let i of t){let t=r(e,i),a=this.claimed.get(t.piName);if(a!==void 0&&a!==e){this.diagnostics.push(`MCP tool "${t.piName}" from "${e}" was dropped: "${a}" already registered that name.`);continue}this.claimed.set(t.piName,e),n.push(t)}return n}};export{i as McpCatalog,n as toPiToolName};
2
+ //# sourceMappingURL=mcpCatalog.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpCatalog.mjs","names":[],"sources":["../../src/services/mcpCatalog.ts"],"sourcesContent":["import type { McpServerSnapshot, McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpServerStateChange, McpToolInfo } from '@agimon-ai/mcp-proxy';\nimport type { McpServerView } from '../types.ts';\nimport type { DirectToolFilter } from './directTools.ts';\nimport type { CachedCatalog } from './mcpRuntime.ts';\n\n/** One downstream tool, with the name Pi will know it by. */\nexport interface CatalogTool {\n /** Name registered with Pi, prefixed by server. */\n piName: string;\n /** Name the downstream server knows. */\n toolName: string;\n serverName: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface CatalogEntry {\n name: string;\n /**\n * The last transition the server actually reported.\n *\n * Never `disabled`: a session disable is an override the user applied, not\n * something the server did, so it lives in its own field. Writing it into\n * `state` would destroy the value re-enabling has to return to.\n */\n state: McpServerSnapshot['state'];\n /** Session-only override. Survives nothing; a restart returns to configuration. */\n disabled?: boolean;\n tools: CatalogTool[];\n resourceCount: number;\n error?: string;\n}\n\n/**\n * States where calling a tool cannot succeed, so its tools are withheld.\n *\n * `closed` is absent deliberately: a server closed after an idle timeout is\n * reconnected by the next call rather than gone. `disabled` is absent because it is\n * no longer a reachable transition; `entry.disabled` carries that instead.\n */\nconst UNAVAILABLE_STATES = new Set<McpServerSnapshot['state']>(['failed', 'needs-auth']);\n\n/** Where a server starts: known to the session, not yet dialled. */\nconst NOT_CONNECTED: McpServerSnapshot['state'] = 'not-connected';\nconst DISABLED: McpServerSnapshot['state'] = 'disabled';\n\n/**\n * Pi's tool name for a downstream tool.\n *\n * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names\n * are a contract with agent frontmatter selectors, transcripts, and user habit, so\n * they stay stable across the swap rather than following mcp-proxy's internal\n * scheme, which prefixes only on a clash.\n */\nexport function toPiToolName(serverName: string, toolName: string): string {\n return `${serverName.replace(/[^a-zA-Z0-9]/g, '_')}_${toolName}`;\n}\n\nfunction toCatalogTool(serverName: string, tool: McpToolInfo): CatalogTool {\n return {\n piName: toPiToolName(serverName, tool.name),\n toolName: tool.name,\n serverName,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: tool.inputSchema,\n };\n}\n\n/**\n * The session's MCP servers and the tools registered for each.\n *\n * Seeded from the previous run's catalog so tools exist before any socket opens,\n * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no\n * way to unregister one, so a tool whose server later fails stays registered and is\n * simply left out of the active set.\n */\nexport class McpCatalog {\n private readonly entries = new Map<string, CatalogEntry>();\n private readonly diagnostics: string[] = [];\n /** Pi names already claimed, so two servers cannot register the same one. */\n private readonly claimed = new Map<string, string>();\n\n /**\n * @param directTools - A child agent's tool selection. Undefined in a normal\n * session, where every configured tool is available.\n */\n constructor(private readonly directTools?: DirectToolFilter) {}\n\n /**\n * Seeds the servers this session will connect to, all still `not-connected`.\n *\n * Configured names come first so a run with no cache still reports what it is\n * about to reach rather than looking empty; the cache then fills in their tools.\n * A cached server that configuration has since dropped is not seeded at all.\n */\n seed(catalog: CachedCatalog, configuredServers: readonly string[] = []): void {\n for (const name of configuredServers) {\n this.entries.set(name, { name, state: NOT_CONNECTED, tools: [], resourceCount: 0 });\n }\n for (const server of catalog.servers) {\n if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;\n this.entries.set(server.name, {\n name: server.name,\n state: NOT_CONNECTED,\n tools: this.claimTools(server.name, server.tools),\n resourceCount: 0,\n });\n }\n }\n\n /**\n * Hides or restores a server's tools for this session.\n *\n * Only the flag moves: `state` keeps tracking what the server reports, so\n * re-enabling lands on the current truth rather than on a remembered value that\n * may have gone stale while the server was hidden.\n */\n setDisabled(serverName: string, disabled: boolean): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.disabled = disabled;\n else\n this.entries.set(serverName, { name: serverName, state: NOT_CONNECTED, disabled, tools: [], resourceCount: 0 });\n }\n\n /** Records how many resources a server offered the last time they were listed. */\n setResourceCount(serverName: string, resourceCount: number): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.resourceCount = resourceCount;\n }\n\n /**\n * Folds a live server transition into the catalog.\n *\n * Returns the tools that are new to this catalog, which is what the caller has to\n * register with Pi. A reconnect returns nothing, since its tools already exist.\n */\n applyStateChange(change: McpServerStateChange, tools: McpToolInfo[] = [], resourceCount?: number): CatalogTool[] {\n const entry = this.entries.get(change.serverName) ?? {\n name: change.serverName,\n state: change.state,\n tools: [],\n resourceCount: 0,\n };\n entry.state = change.state;\n if (change.error) entry.error = change.error;\n else delete entry.error;\n if (resourceCount !== undefined) entry.resourceCount = resourceCount;\n\n const known = new Set(entry.tools.map((tool) => tool.toolName));\n const added = this.claimTools(\n change.serverName,\n tools.filter((tool) => !known.has(tool.name)),\n );\n entry.tools = [...entry.tools, ...added];\n this.entries.set(change.serverName, entry);\n return added;\n }\n\n /**\n * Tool names Pi should currently expose.\n *\n * Optimistic: a server that has not reported yet still contributes its cached\n * tools, because calling one connects it on demand. mcp-proxy skips the eager\n * connection pass whenever it has a usable definitions cache, so requiring a\n * `connected` state first would leave a warm session with no tools at all.\n *\n * Only a server that is known to be unusable, or one the user disabled, is held\n * back.\n */\n activeToolNames(): string[] {\n const names: string[] = [];\n for (const entry of this.entries.values()) {\n if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;\n for (const tool of entry.tools) {\n if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;\n names.push(tool.piName);\n }\n }\n return names;\n }\n\n /** Every tool ever registered, whether or not its server is reachable. */\n allTools(): CatalogTool[] {\n return [...this.entries.values()].flatMap((entry) => entry.tools);\n }\n\n findTool(piName: string): CatalogTool | undefined {\n return this.allTools().find((tool) => tool.piName === piName);\n }\n\n /**\n * The cross-extension status shape.\n *\n * A disabled server reports `disabled` as its state, because that is the one word\n * the wire contract has for \"configured but contributing nothing\", and consumers\n * outside this process have no use for the transition underneath it.\n */\n toSnapshot(): McpStatusSnapshot {\n return {\n servers: [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.disabled ? DISABLED : entry.state,\n tools: entry.tools.map((tool) => tool.piName),\n resourceCount: entry.resourceCount,\n ...(entry.error ? { error: entry.error } : {}),\n })),\n };\n }\n\n /**\n * The fuller picture the overlay renders.\n *\n * Carries what the snapshot cannot: each tool's downstream name and description,\n * whether a child agent's selection withholds it, and `enabled` as its own field\n * so the overlay can offer disable and enable against the real server state.\n */\n toView(): McpServerView[] {\n return [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.state,\n tools: entry.tools.map((tool) => ({\n piName: tool.piName,\n toolName: tool.toolName,\n ...(tool.description ? { description: tool.description } : {}),\n active: !this.directTools || this.directTools.allows(entry.name, tool.toolName),\n })),\n resourceCount: entry.resourceCount,\n enabled: !entry.disabled,\n ...(entry.error ? { error: entry.error } : {}),\n }));\n }\n\n /** Problems worth surfacing to the user. Reported, never thrown. */\n getDiagnostics(): readonly string[] {\n return this.diagnostics;\n }\n\n /**\n * Records a problem for the user without interrupting whatever hit it.\n *\n * Deduplicated: a fault that recurs on every repaint would otherwise grow this\n * list for as long as the session runs, and the second copy tells nobody\n * anything the first did not.\n */\n addDiagnostic(diagnostic: string): void {\n if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);\n }\n\n /**\n * Assigns Pi names, dropping any that another server already owns.\n *\n * Two server names that normalize to the same prefix (`code-intel` and\n * `code_intel`) would otherwise have one silently shadow the other's tool.\n */\n private claimTools(serverName: string, tools: McpToolInfo[]): CatalogTool[] {\n const claimed: CatalogTool[] = [];\n for (const tool of tools) {\n const candidate = toCatalogTool(serverName, tool);\n const owner = this.claimed.get(candidate.piName);\n if (owner !== undefined && owner !== serverName) {\n this.diagnostics.push(\n `MCP tool \"${candidate.piName}\" from \"${serverName}\" was dropped: \"${owner}\" already registered that name.`,\n );\n continue;\n }\n this.claimed.set(candidate.piName, serverName);\n claimed.push(candidate);\n }\n return claimed;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ let e=require("node:fs"),t=require("@agimon-ai/mcp-proxy");function n(n){if(n.length===0)return{servers:[]};try{let r=t.DefinitionsCacheService.getDefaultCachePath(n),i=JSON.parse((0,e.readFileSync)(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var r=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let n=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let r=await(0,t.createProxyContainer)({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(n!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{n===this.generation&&i(e)})),{generation:n,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};exports.McpRuntimeOwner=r,exports.readCachedCatalog=n;
2
+ //# sourceMappingURL=mcpRuntime.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpRuntime.cjs","names":["DefinitionsCacheService","readFileSync","createProxyContainer"],"sources":["../../src/services/mcpRuntime.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport {\n createProxyContainer,\n type DefinitionsCacheFile,\n DefinitionsCacheService,\n type McpServerStateChange,\n type McpToolInfo,\n type SharedServices,\n type TokenStore,\n} from '@agimon-ai/mcp-proxy';\n\nexport interface McpRuntimeOptions {\n /** Ordered config layers. Later layers override earlier ones. */\n configPaths: string[];\n tokenStore?: TokenStore;\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n onServerStateChange?: (change: McpServerStateChange) => void;\n}\n\nexport interface McpRuntimeHandle {\n generation: number;\n services: SharedServices;\n /** Resolves once the first connection pass settles. Never rejects. */\n connectionsSettled: Promise<void>;\n}\n\nexport interface CatalogServer {\n name: string;\n tools: McpToolInfo[];\n}\n\n/** Server definitions from the previous run, available before any socket opens. */\nexport interface CachedCatalog {\n servers: CatalogServer[];\n}\n\n/**\n * Reads the catalog the previous run left behind.\n *\n * Synchronous on purpose: Pi has to be able to register these tools during\n * extension install, before it takes its first turn, and awaiting a file read\n * would let that turn start with an empty tool set. A miss is normal on a first\n * run and resolves to an empty catalog rather than an error.\n *\n * The catalog is not checked against the current configuration: a server that has\n * since been removed simply never reaches `connected` and is dropped from the\n * active tool set when its state lands.\n */\nexport function readCachedCatalog(configPaths: string[]): CachedCatalog {\n if (configPaths.length === 0) return { servers: [] };\n try {\n const cachePath = DefinitionsCacheService.getDefaultCachePath(configPaths);\n const cache = JSON.parse(readFileSync(cachePath, 'utf8')) as Partial<DefinitionsCacheFile>;\n const servers = Object.values(cache.servers ?? {}).map((server) => ({\n name: server.serverName,\n tools: server.tools ?? [],\n }));\n return { servers };\n } catch {\n return { servers: [] };\n }\n}\n\n/**\n * Owns the one live mcp-proxy container for this extension.\n *\n * Pi reloads extensions in process, so a connect that lands after a session restart\n * or a `/domains` switch would otherwise fold tools into a dead context. Every start\n * takes the next generation; anything an older generation produces is dropped and its\n * container disposed.\n */\nexport class McpRuntimeOwner {\n private generation = 0;\n private active: SharedServices | undefined;\n private unsubscribe: (() => void) | undefined;\n\n get currentGeneration(): number {\n return this.generation;\n }\n\n /** The live container, or undefined before the first start and after dispose. */\n getServices(): SharedServices | undefined {\n return this.active;\n }\n\n /**\n * Whether a container a caller captured earlier is still the live one.\n *\n * An async continuation that started under one container has to check this\n * before writing its result, or a `/domains` switch would fold stale tools into\n * the new session.\n */\n isCurrent(services: SharedServices | undefined): boolean {\n return services !== undefined && services === this.active;\n }\n\n /**\n * Starts a container for the current configuration, replacing any predecessor.\n *\n * Returns undefined when a newer generation started while this one was building,\n * which is the signal for the caller to abandon its half-finished work.\n */\n async start(options: McpRuntimeOptions): Promise<McpRuntimeHandle | undefined> {\n const generation = ++this.generation;\n // Retired before the replacement is built so two containers never hold the same\n // stdio children open at once.\n await this.retire();\n if (options.configPaths.length === 0) return undefined;\n\n const services = await createProxyContainer({\n configFilePath: options.configPaths,\n // Never blocking: an unreachable server must not delay Pi's first prompt.\n startupMode: 'background',\n auth: { tokenStore: options.tokenStore, onAuthorizationUrl: options.onAuthorizationUrl },\n });\n\n if (generation !== this.generation) {\n await services.dispose().catch(() => undefined);\n return undefined;\n }\n\n this.active = services;\n const forward = options.onServerStateChange;\n if (forward) {\n this.unsubscribe = services.clientManager.onServerStateChange((change) => {\n if (generation === this.generation) forward(change);\n });\n }\n\n return { generation, services, connectionsSettled: services.connectionsSettled };\n }\n\n /** Tears the container down and invalidates every generation still in flight. */\n async dispose(): Promise<void> {\n ++this.generation;\n await this.retire();\n }\n\n private async retire(): Promise<void> {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n const services = this.active;\n this.active = undefined;\n // A failed teardown must not leave the owner believing a dead container is live.\n if (services) await services.dispose().catch(() => undefined);\n }\n}\n"],"mappings":"2DAgDA,SAAgB,EAAkB,EAAsC,CACtE,GAAI,EAAY,SAAW,EAAG,MAAO,CAAE,QAAS,CAAC,CAAE,EACnD,GAAI,CACF,IAAM,EAAYA,EAAAA,wBAAwB,oBAAoB,CAAW,EACnE,EAAQ,KAAK,OAAA,EAAMC,EAAAA,aAAAA,CAAa,EAAW,MAAM,CAAC,EAKxD,MAAO,CAAE,QAJO,OAAO,OAAO,EAAM,SAAW,CAAC,CAAC,CAAC,CAAC,IAAK,IAAY,CAClE,KAAM,EAAO,WACb,MAAO,EAAO,OAAS,CAAC,CAC1B,EACe,CAAE,CACnB,MAAQ,CACN,MAAO,CAAE,QAAS,CAAC,CAAE,CACvB,CACF,CAUA,IAAa,EAAb,KAA6B,CAC3B,WAAqB,EACrB,OACA,YAEA,IAAI,mBAA4B,CAC9B,OAAO,KAAK,UACd,CAGA,aAA0C,CACxC,OAAO,KAAK,MACd,CASA,UAAU,EAA+C,CACvD,OAAO,IAAa,IAAA,IAAa,IAAa,KAAK,MACrD,CAQA,MAAM,MAAM,EAAmE,CAC7E,IAAM,EAAa,EAAE,KAAK,WAI1B,GADA,MAAM,KAAK,OAAO,EACd,EAAQ,YAAY,SAAW,EAAG,OAEtC,IAAM,EAAW,MAAA,EAAMC,EAAAA,qBAAAA,CAAqB,CAC1C,eAAgB,EAAQ,YAExB,YAAa,aACb,KAAM,CAAE,WAAY,EAAQ,WAAY,mBAAoB,EAAQ,kBAAmB,CACzF,CAAC,EAED,GAAI,IAAe,KAAK,WAAY,CAClC,MAAM,EAAS,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,EAC9C,MACF,CAEA,KAAK,OAAS,EACd,IAAM,EAAU,EAAQ,oBAOxB,OANI,IACF,KAAK,YAAc,EAAS,cAAc,oBAAqB,GAAW,CACpE,IAAe,KAAK,YAAY,EAAQ,CAAM,CACpD,CAAC,GAGI,CAAE,aAAY,WAAU,mBAAoB,EAAS,kBAAmB,CACjF,CAGA,MAAM,SAAyB,CAC7B,EAAE,KAAK,WACP,MAAM,KAAK,OAAO,CACpB,CAEA,MAAc,QAAwB,CACpC,KAAK,cAAc,EACnB,KAAK,YAAc,IAAA,GACnB,IAAM,EAAW,KAAK,OACtB,KAAK,OAAS,IAAA,GAEV,GAAU,MAAM,EAAS,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,CAC9D,CACF"}
@@ -0,0 +1,26 @@
1
+ import { McpToolInfo } from "@agimon-ai/mcp-proxy";
2
+ //#region src/services/mcpRuntime.d.ts
3
+ interface CatalogServer {
4
+ name: string;
5
+ tools: McpToolInfo[];
6
+ }
7
+ /** Server definitions from the previous run, available before any socket opens. */
8
+ interface CachedCatalog {
9
+ servers: CatalogServer[];
10
+ }
11
+ /**
12
+ * Reads the catalog the previous run left behind.
13
+ *
14
+ * Synchronous on purpose: Pi has to be able to register these tools during
15
+ * extension install, before it takes its first turn, and awaiting a file read
16
+ * would let that turn start with an empty tool set. A miss is normal on a first
17
+ * run and resolves to an empty catalog rather than an error.
18
+ *
19
+ * The catalog is not checked against the current configuration: a server that has
20
+ * since been removed simply never reaches `connected` and is dropped from the
21
+ * active tool set when its state lands.
22
+ */
23
+ declare function readCachedCatalog(configPaths: string[]): CachedCatalog;
24
+ //#endregion
25
+ export { CachedCatalog, CatalogServer, readCachedCatalog };
26
+ //# sourceMappingURL=mcpRuntime.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpRuntime.d.cts","names":[],"sources":["../../src/services/mcpRuntime.ts"],"mappings":";;UA0BiB;EACf;EACA,OAAO;;;UAIQ;EACf,SAAS;;;;;;;;;;;;;;iBAeK,kBAAkB,wBAAwB"}
@@ -0,0 +1,26 @@
1
+ import { McpToolInfo } from "@agimon-ai/mcp-proxy";
2
+ //#region src/services/mcpRuntime.d.ts
3
+ interface CatalogServer {
4
+ name: string;
5
+ tools: McpToolInfo[];
6
+ }
7
+ /** Server definitions from the previous run, available before any socket opens. */
8
+ interface CachedCatalog {
9
+ servers: CatalogServer[];
10
+ }
11
+ /**
12
+ * Reads the catalog the previous run left behind.
13
+ *
14
+ * Synchronous on purpose: Pi has to be able to register these tools during
15
+ * extension install, before it takes its first turn, and awaiting a file read
16
+ * would let that turn start with an empty tool set. A miss is normal on a first
17
+ * run and resolves to an empty catalog rather than an error.
18
+ *
19
+ * The catalog is not checked against the current configuration: a server that has
20
+ * since been removed simply never reaches `connected` and is dropped from the
21
+ * active tool set when its state lands.
22
+ */
23
+ declare function readCachedCatalog(configPaths: string[]): CachedCatalog;
24
+ //#endregion
25
+ export { CachedCatalog, CatalogServer, readCachedCatalog };
26
+ //# sourceMappingURL=mcpRuntime.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpRuntime.d.mts","names":[],"sources":["../../src/services/mcpRuntime.ts"],"mappings":";;UA0BiB;EACf;EACA,OAAO;;;UAIQ;EACf,SAAS;;;;;;;;;;;;;;iBAeK,kBAAkB,wBAAwB"}
@@ -0,0 +1,2 @@
1
+ import{readFileSync as e}from"node:fs";import{DefinitionsCacheService as t,createProxyContainer as n}from"@agimon-ai/mcp-proxy";function r(n){if(n.length===0)return{servers:[]};try{let r=t.getDefaultCachePath(n),i=JSON.parse(e(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var i=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};export{i as McpRuntimeOwner,r as readCachedCatalog};
2
+ //# sourceMappingURL=mcpRuntime.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpRuntime.mjs","names":[],"sources":["../../src/services/mcpRuntime.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport {\n createProxyContainer,\n type DefinitionsCacheFile,\n DefinitionsCacheService,\n type McpServerStateChange,\n type McpToolInfo,\n type SharedServices,\n type TokenStore,\n} from '@agimon-ai/mcp-proxy';\n\nexport interface McpRuntimeOptions {\n /** Ordered config layers. Later layers override earlier ones. */\n configPaths: string[];\n tokenStore?: TokenStore;\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n onServerStateChange?: (change: McpServerStateChange) => void;\n}\n\nexport interface McpRuntimeHandle {\n generation: number;\n services: SharedServices;\n /** Resolves once the first connection pass settles. Never rejects. */\n connectionsSettled: Promise<void>;\n}\n\nexport interface CatalogServer {\n name: string;\n tools: McpToolInfo[];\n}\n\n/** Server definitions from the previous run, available before any socket opens. */\nexport interface CachedCatalog {\n servers: CatalogServer[];\n}\n\n/**\n * Reads the catalog the previous run left behind.\n *\n * Synchronous on purpose: Pi has to be able to register these tools during\n * extension install, before it takes its first turn, and awaiting a file read\n * would let that turn start with an empty tool set. A miss is normal on a first\n * run and resolves to an empty catalog rather than an error.\n *\n * The catalog is not checked against the current configuration: a server that has\n * since been removed simply never reaches `connected` and is dropped from the\n * active tool set when its state lands.\n */\nexport function readCachedCatalog(configPaths: string[]): CachedCatalog {\n if (configPaths.length === 0) return { servers: [] };\n try {\n const cachePath = DefinitionsCacheService.getDefaultCachePath(configPaths);\n const cache = JSON.parse(readFileSync(cachePath, 'utf8')) as Partial<DefinitionsCacheFile>;\n const servers = Object.values(cache.servers ?? {}).map((server) => ({\n name: server.serverName,\n tools: server.tools ?? [],\n }));\n return { servers };\n } catch {\n return { servers: [] };\n }\n}\n\n/**\n * Owns the one live mcp-proxy container for this extension.\n *\n * Pi reloads extensions in process, so a connect that lands after a session restart\n * or a `/domains` switch would otherwise fold tools into a dead context. Every start\n * takes the next generation; anything an older generation produces is dropped and its\n * container disposed.\n */\nexport class McpRuntimeOwner {\n private generation = 0;\n private active: SharedServices | undefined;\n private unsubscribe: (() => void) | undefined;\n\n get currentGeneration(): number {\n return this.generation;\n }\n\n /** The live container, or undefined before the first start and after dispose. */\n getServices(): SharedServices | undefined {\n return this.active;\n }\n\n /**\n * Whether a container a caller captured earlier is still the live one.\n *\n * An async continuation that started under one container has to check this\n * before writing its result, or a `/domains` switch would fold stale tools into\n * the new session.\n */\n isCurrent(services: SharedServices | undefined): boolean {\n return services !== undefined && services === this.active;\n }\n\n /**\n * Starts a container for the current configuration, replacing any predecessor.\n *\n * Returns undefined when a newer generation started while this one was building,\n * which is the signal for the caller to abandon its half-finished work.\n */\n async start(options: McpRuntimeOptions): Promise<McpRuntimeHandle | undefined> {\n const generation = ++this.generation;\n // Retired before the replacement is built so two containers never hold the same\n // stdio children open at once.\n await this.retire();\n if (options.configPaths.length === 0) return undefined;\n\n const services = await createProxyContainer({\n configFilePath: options.configPaths,\n // Never blocking: an unreachable server must not delay Pi's first prompt.\n startupMode: 'background',\n auth: { tokenStore: options.tokenStore, onAuthorizationUrl: options.onAuthorizationUrl },\n });\n\n if (generation !== this.generation) {\n await services.dispose().catch(() => undefined);\n return undefined;\n }\n\n this.active = services;\n const forward = options.onServerStateChange;\n if (forward) {\n this.unsubscribe = services.clientManager.onServerStateChange((change) => {\n if (generation === this.generation) forward(change);\n });\n }\n\n return { generation, services, connectionsSettled: services.connectionsSettled };\n }\n\n /** Tears the container down and invalidates every generation still in flight. */\n async dispose(): Promise<void> {\n ++this.generation;\n await this.retire();\n }\n\n private async retire(): Promise<void> {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n const services = this.active;\n this.active = undefined;\n // A failed teardown must not leave the owner believing a dead container is live.\n if (services) await services.dispose().catch(() => undefined);\n }\n}\n"],"mappings":"gIAgDA,SAAgB,EAAkB,EAAsC,CACtE,GAAI,EAAY,SAAW,EAAG,MAAO,CAAE,QAAS,CAAC,CAAE,EACnD,GAAI,CACF,IAAM,EAAY,EAAwB,oBAAoB,CAAW,EACnE,EAAQ,KAAK,MAAM,EAAa,EAAW,MAAM,CAAC,EAKxD,MAAO,CAAE,QAJO,OAAO,OAAO,EAAM,SAAW,CAAC,CAAC,CAAC,CAAC,IAAK,IAAY,CAClE,KAAM,EAAO,WACb,MAAO,EAAO,OAAS,CAAC,CAC1B,EACe,CAAE,CACnB,MAAQ,CACN,MAAO,CAAE,QAAS,CAAC,CAAE,CACvB,CACF,CAUA,IAAa,EAAb,KAA6B,CAC3B,WAAqB,EACrB,OACA,YAEA,IAAI,mBAA4B,CAC9B,OAAO,KAAK,UACd,CAGA,aAA0C,CACxC,OAAO,KAAK,MACd,CASA,UAAU,EAA+C,CACvD,OAAO,IAAa,IAAA,IAAa,IAAa,KAAK,MACrD,CAQA,MAAM,MAAM,EAAmE,CAC7E,IAAM,EAAa,EAAE,KAAK,WAI1B,GADA,MAAM,KAAK,OAAO,EACd,EAAQ,YAAY,SAAW,EAAG,OAEtC,IAAM,EAAW,MAAM,EAAqB,CAC1C,eAAgB,EAAQ,YAExB,YAAa,aACb,KAAM,CAAE,WAAY,EAAQ,WAAY,mBAAoB,EAAQ,kBAAmB,CACzF,CAAC,EAED,GAAI,IAAe,KAAK,WAAY,CAClC,MAAM,EAAS,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,EAC9C,MACF,CAEA,KAAK,OAAS,EACd,IAAM,EAAU,EAAQ,oBAOxB,OANI,IACF,KAAK,YAAc,EAAS,cAAc,oBAAqB,GAAW,CACpE,IAAe,KAAK,YAAY,EAAQ,CAAM,CACpD,CAAC,GAGI,CAAE,aAAY,WAAU,mBAAoB,EAAS,kBAAmB,CACjF,CAGA,MAAM,SAAyB,CAC7B,EAAE,KAAK,WACP,MAAM,KAAK,OAAO,CACpB,CAEA,MAAc,QAAwB,CACpC,KAAK,cAAc,EACnB,KAAK,YAAc,IAAA,GACnB,IAAM,EAAW,KAAK,OACtB,KAAK,OAAS,IAAA,GAEV,GAAU,MAAM,EAAS,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,CAC9D,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../tools/mcpTools.cjs"),n=require("./configSources.cjs"),r=require("./keyringTokenStore.cjs"),i=require("./directTools.cjs"),a=require("./mcpCatalog.cjs"),o=require("./mcpRuntime.cjs"),s=require("./sessionConfig.cjs"),c=require("./sharedRuntime.cjs");let l=require("node:fs");l=e.__toESM(l,1);const u=`The MCP runtime has not started yet.`;var d=class{pi;runtime=new o.McpRuntimeOwner;catalog;groups={shared:{configPaths:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;configuration;sharedLayerPath;allowedUpstreamTools;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;clientManager=()=>this.runtime.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.configuration=s.readSessionConfig(e.environment),this.catalog=this.createCatalog()}activate(){t.applyActiveTools(this.pi,this.catalog)}createCatalog(){return new a.McpCatalog(i.readDirectToolFilter(this.options.environment))}install(e=s.readSessionConfig(this.options.environment)){this.configuration=e,this.groups=n.buildMcpConfigGroups(e),this.sharedLayerPath=this.groups.shared.configPaths.length>0?c.sharedLayerPath(e.repoRoot):void 0,this.catalog=this.createCatalog();let r=[...this.groups.sessionLocal.serverNames,...this.sharedLayerPath?[n.PROXY_SERVER_NAME]:[]];return this.catalog.seed(o.readCachedCatalog(this.configPaths()),r),t.registerNewTools(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){this.resourceCache.clear(),c.maintainSharedResources(this.configuration.stagingDirectory);let e=await c.ensureSharedRuntime({repoRoot:this.configuration.repoRoot,configPaths:this.groups.shared.configPaths});this.allowedUpstreamTools=c.resolveAllowedUpstreamTools(this.groups.shared.configPaths,this.groups.shared.allowedUpstreams);let t=this.configPaths(e!==void 0);if(this.groups.sessionLocal.serverNames.length===0&&!e)return;let n=this.options.tokenStore??await r.createTokenStore();await this.runtime.start({configPaths:t,tokenStore:n,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)})}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange(),this.options.onAuthorizationUrl?.(e,t)}configPaths(e=!1){let t=this.sharedLayerPath,n=t!==void 0&&(e||l.default.existsSync(t));return[...this.groups.sessionLocal.configPaths,...n?[t]:[]]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(u);this.resourceCache.delete(e),await t.disconnectServer(e),await t.ensureConnected(e)}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,n){this.catalog.setDisabled(e,!n),t.applyActiveTools(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime.getServices();if(!n)throw Error(u);let r=await(await n.clientManager.ensureConnected(e)).listResources();return this.runtime.isCurrent(n)?(this.resourceCache.set(e,r),this.catalog.setResourceCount(e,r.length),this.emitChange(),r):r}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){await this.runtime.dispose()}onServerStateChange(e){e.state!==`connecting`&&this.authorizationUrls.delete(e.serverName);let n=this.runtime.getServices();(e.state===`connected`&&n?n.clientManager.ensureConnected(e.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(r=>{if(!this.runtime.isCurrent(n))return;let i=this.catalog.applyStateChange(e,this.permittedTools(e.serverName,r));t.registerNewTools(this.pi,this.clientManager,i),t.applyActiveTools(this.pi,this.catalog),this.emitChange()})}permittedTools(e,t){let n=this.allowedUpstreamTools;return!n||e!==`agiflow-proxy`?t:t.filter(e=>n.has(e.name))}};exports.McpSession=d;
2
+ //# sourceMappingURL=mcpSession.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpSession.cjs","names":["McpRuntimeOwner","readSessionConfig","McpCatalog","readDirectToolFilter","buildMcpConfigGroups","sharedLayerPath","PROXY_SERVER_NAME","readCachedCatalog","maintainSharedResources","ensureSharedRuntime","resolveAllowedUpstreamTools","createTokenStore","fs"],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, McpToolInfo, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { McpResourceView, McpServerView } from '../types.ts';\nimport { applyActiveTools, registerNewTools } from '../tools/mcpTools.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups, PROXY_SERVER_NAME } from './configSources.ts';\nimport { createTokenStore } from './keyringTokenStore.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { readSessionConfig, type McpSessionConfig } from './sessionConfig.ts';\nimport {\n ensureSharedRuntime,\n maintainSharedResources,\n resolveAllowedUpstreamTools,\n sharedLayerPath,\n} from './sharedRuntime.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n private readonly runtime = new McpRuntimeOwner();\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n private configuration: McpSessionConfig;\n /** Generated layer pointing at the worktree runtime; absent when there is none. */\n private sharedLayerPath: string | undefined;\n /** Proxy tool names the domain permits. Undefined when every upstream is allowed. */\n private allowedUpstreamTools: Set<string> | undefined;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined => this.runtime.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.configuration = readSessionConfig(options.environment);\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.configuration = configuration;\n this.groups = buildMcpConfigGroups(configuration);\n // The shared layer's path is stable per worktree, so a previous run's file is\n // already in the ordered list the definitions cache is keyed on. Including it\n // here is what makes the proxy's tools available before anything connects.\n this.sharedLayerPath =\n this.groups.shared.configPaths.length > 0 ? sharedLayerPath(configuration.repoRoot) : undefined;\n this.catalog = this.createCatalog();\n // The proxy is a configured server of this session even though it is absent\n // from `.mcp.json`'s filtered layer: doom-mcp rewrites that entry to point at\n // the worktree runtime rather than spawning a second proxy.\n const configuredServers = [\n ...this.groups.sessionLocal.serverNames,\n ...(this.sharedLayerPath ? [PROXY_SERVER_NAME] : []),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n // Detached: every session start doubles as a sweep of staged layers and dead\n // registry rows left by sessions that never got to clean up after themselves.\n void maintainSharedResources(this.configuration.stagingDirectory);\n // Joining the worktree's runtime first, so its endpoint is written before the\n // container reads the layer. Only the first session in a worktree pays to\n // start it; the rest reuse it after one health probe.\n const shared = await ensureSharedRuntime({\n repoRoot: this.configuration.repoRoot,\n configPaths: this.groups.shared.configPaths,\n });\n this.allowedUpstreamTools = resolveAllowedUpstreamTools(\n this.groups.shared.configPaths,\n this.groups.shared.allowedUpstreams,\n );\n\n const configPaths = this.configPaths(shared !== undefined);\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe and a proxy on a\n // session that has nothing to connect to.\n if (this.groups.sessionLocal.serverNames.length === 0 && !shared) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await this.runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /**\n * The ordered layers this session connects, session-local first.\n *\n * The shared layer is included only when its file exists, so a first run does\n * not key the definitions cache on a path that carries nothing.\n */\n private configPaths(sharedIsLive = false): string[] {\n const shared = this.sharedLayerPath;\n const includeShared = shared !== undefined && (sharedIsLive || fs.existsSync(shared));\n return [...this.groups.sessionLocal.configPaths, ...(includeShared ? [shared] : [])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect. The proxy entry points at a runtime shared\n * across every session in the worktree, so dropping its connection here would\n * reach into sessions the user was not looking at. Withholding the tools from\n * Pi's active list is the whole of what \"disabled for this session\" means, and it\n * is instant and reversible in both directions.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const services = this.runtime.getServices();\n if (!services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!this.runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n await this.runtime.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const services = this.runtime.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!this.runtime.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, this.permittedTools(change.serverName, tools));\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n\n /**\n * Drops proxy tools whose upstream this session's domain does not allow.\n *\n * Filtered before they reach the catalog, so a disallowed upstream's tool is\n * never registered rather than registered and hidden. The runtime is shared, so\n * this is the only place the per-session policy can apply.\n */\n private permittedTools(serverName: string, tools: McpToolInfo[]): McpToolInfo[] {\n const allowed = this.allowedUpstreamTools;\n if (!allowed || serverName !== PROXY_SERVER_NAME) return tools;\n return tools.filter((tool) => allowed.has(tool.name));\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{applyActiveTools as e,registerNewTools as t}from"../tools/mcpTools.mjs";import{PROXY_SERVER_NAME as n,buildMcpConfigGroups as r}from"./configSources.mjs";import{createTokenStore as i}from"./keyringTokenStore.mjs";import{readDirectToolFilter as a}from"./directTools.mjs";import{McpCatalog as o}from"./mcpCatalog.mjs";import{McpRuntimeOwner as s,readCachedCatalog as c}from"./mcpRuntime.mjs";import{readSessionConfig as l}from"./sessionConfig.mjs";import{ensureSharedRuntime as u,maintainSharedResources as d,resolveAllowedUpstreamTools as f,sharedLayerPath as p}from"./sharedRuntime.mjs";import m from"node:fs";const h=`The MCP runtime has not started yet.`;var g=class{pi;runtime=new s;catalog;groups={shared:{configPaths:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;configuration;sharedLayerPath;allowedUpstreamTools;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;clientManager=()=>this.runtime.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.configuration=l(e.environment),this.catalog=this.createCatalog()}activate(){e(this.pi,this.catalog)}createCatalog(){return new o(a(this.options.environment))}install(e=l(this.options.environment)){this.configuration=e,this.groups=r(e),this.sharedLayerPath=this.groups.shared.configPaths.length>0?p(e.repoRoot):void 0,this.catalog=this.createCatalog();let i=[...this.groups.sessionLocal.serverNames,...this.sharedLayerPath?[n]:[]];return this.catalog.seed(c(this.configPaths()),i),t(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){this.resourceCache.clear(),d(this.configuration.stagingDirectory);let e=await u({repoRoot:this.configuration.repoRoot,configPaths:this.groups.shared.configPaths});this.allowedUpstreamTools=f(this.groups.shared.configPaths,this.groups.shared.allowedUpstreams);let t=this.configPaths(e!==void 0);if(this.groups.sessionLocal.serverNames.length===0&&!e)return;let n=this.options.tokenStore??await i();await this.runtime.start({configPaths:t,tokenStore:n,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)})}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange(),this.options.onAuthorizationUrl?.(e,t)}configPaths(e=!1){let t=this.sharedLayerPath,n=t!==void 0&&(e||m.existsSync(t));return[...this.groups.sessionLocal.configPaths,...n?[t]:[]]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(h);this.resourceCache.delete(e),await t.disconnectServer(e),await t.ensureConnected(e)}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(t,n){this.catalog.setDisabled(t,!n),e(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime.getServices();if(!n)throw Error(h);let r=await(await n.clientManager.ensureConnected(e)).listResources();return this.runtime.isCurrent(n)?(this.resourceCache.set(e,r),this.catalog.setResourceCount(e,r.length),this.emitChange(),r):r}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){await this.runtime.dispose()}onServerStateChange(n){n.state!==`connecting`&&this.authorizationUrls.delete(n.serverName);let r=this.runtime.getServices();(n.state===`connected`&&r?r.clientManager.ensureConnected(n.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(i=>{if(!this.runtime.isCurrent(r))return;let a=this.catalog.applyStateChange(n,this.permittedTools(n.serverName,i));t(this.pi,this.clientManager,a),e(this.pi,this.catalog),this.emitChange()})}permittedTools(e,t){let n=this.allowedUpstreamTools;return!n||e!==`agiflow-proxy`?t:t.filter(e=>n.has(e.name))}};export{g as McpSession};
2
+ //# sourceMappingURL=mcpSession.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, McpToolInfo, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { McpResourceView, McpServerView } from '../types.ts';\nimport { applyActiveTools, registerNewTools } from '../tools/mcpTools.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups, PROXY_SERVER_NAME } from './configSources.ts';\nimport { createTokenStore } from './keyringTokenStore.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { readSessionConfig, type McpSessionConfig } from './sessionConfig.ts';\nimport {\n ensureSharedRuntime,\n maintainSharedResources,\n resolveAllowedUpstreamTools,\n sharedLayerPath,\n} from './sharedRuntime.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n private readonly runtime = new McpRuntimeOwner();\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n private configuration: McpSessionConfig;\n /** Generated layer pointing at the worktree runtime; absent when there is none. */\n private sharedLayerPath: string | undefined;\n /** Proxy tool names the domain permits. Undefined when every upstream is allowed. */\n private allowedUpstreamTools: Set<string> | undefined;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined => this.runtime.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.configuration = readSessionConfig(options.environment);\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.configuration = configuration;\n this.groups = buildMcpConfigGroups(configuration);\n // The shared layer's path is stable per worktree, so a previous run's file is\n // already in the ordered list the definitions cache is keyed on. Including it\n // here is what makes the proxy's tools available before anything connects.\n this.sharedLayerPath =\n this.groups.shared.configPaths.length > 0 ? sharedLayerPath(configuration.repoRoot) : undefined;\n this.catalog = this.createCatalog();\n // The proxy is a configured server of this session even though it is absent\n // from `.mcp.json`'s filtered layer: doom-mcp rewrites that entry to point at\n // the worktree runtime rather than spawning a second proxy.\n const configuredServers = [\n ...this.groups.sessionLocal.serverNames,\n ...(this.sharedLayerPath ? [PROXY_SERVER_NAME] : []),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n // Detached: every session start doubles as a sweep of staged layers and dead\n // registry rows left by sessions that never got to clean up after themselves.\n void maintainSharedResources(this.configuration.stagingDirectory);\n // Joining the worktree's runtime first, so its endpoint is written before the\n // container reads the layer. Only the first session in a worktree pays to\n // start it; the rest reuse it after one health probe.\n const shared = await ensureSharedRuntime({\n repoRoot: this.configuration.repoRoot,\n configPaths: this.groups.shared.configPaths,\n });\n this.allowedUpstreamTools = resolveAllowedUpstreamTools(\n this.groups.shared.configPaths,\n this.groups.shared.allowedUpstreams,\n );\n\n const configPaths = this.configPaths(shared !== undefined);\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe and a proxy on a\n // session that has nothing to connect to.\n if (this.groups.sessionLocal.serverNames.length === 0 && !shared) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await this.runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /**\n * The ordered layers this session connects, session-local first.\n *\n * The shared layer is included only when its file exists, so a first run does\n * not key the definitions cache on a path that carries nothing.\n */\n private configPaths(sharedIsLive = false): string[] {\n const shared = this.sharedLayerPath;\n const includeShared = shared !== undefined && (sharedIsLive || fs.existsSync(shared));\n return [...this.groups.sessionLocal.configPaths, ...(includeShared ? [shared] : [])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect. The proxy entry points at a runtime shared\n * across every session in the worktree, so dropping its connection here would\n * reach into sessions the user was not looking at. Withholding the tools from\n * Pi's active list is the whole of what \"disabled for this session\" means, and it\n * is instant and reversible in both directions.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const services = this.runtime.getServices();\n if (!services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!this.runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n await this.runtime.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const services = this.runtime.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!this.runtime.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, this.permittedTools(change.serverName, tools));\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n\n /**\n * Drops proxy tools whose upstream this session's domain does not allow.\n *\n * Filtered before they reach the catalog, so a disallowed upstream's tool is\n * never registered rather than registered and hidden. The runtime is shared, so\n * this is the only place the per-session policy can apply.\n */\n private permittedTools(serverName: string, tools: McpToolInfo[]): McpToolInfo[] {\n const allowed = this.allowedUpstreamTools;\n if (!allowed || serverName !== PROXY_SERVER_NAME) return tools;\n return tools.filter((tool) => allowed.has(tool.name));\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:os");n=e.__toESM(n,1);const r=`DOOM_MCP_SESSION`;function i(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`&&e.length>0);return t.length>0?t:void 0}function a(e){if(typeof e!=`object`||!e)return;let{servers:t,proxy:n}=e,r={},a=i(t),o=i(n);return a&&(r.servers=a),o&&(r.proxy=o),r.servers||r.proxy?r:void 0}function o(e=process.env,o=process.cwd()){let s={repoRoot:o,stagingDirectory:t.default.join(n.default.tmpdir(),`doom-mcp`)},c=e[r];if(!c)return s;let l;try{l=JSON.parse(c)}catch{return s}if(typeof l!=`object`||!l)return s;let{repoRoot:u,pluginConfigPaths:d,allowlist:f,stagingDirectory:p}=l,m=typeof u==`string`&&u?u:s.repoRoot,h=i(d),g=a(f);return{repoRoot:m,stagingDirectory:typeof p==`string`&&p?p:s.stagingDirectory,...h?{pluginConfigPaths:h}:{},...g?{allowlist:g}:{}}}function s(e){return{[r]:JSON.stringify(e)}}exports.SESSION_ENV_VAR=r,exports.readSessionConfig=o,exports.sessionConfigEnvironment=s;
2
+ //# sourceMappingURL=sessionConfig.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionConfig.cjs","names":["path","os"],"sources":["../../src/services/sessionConfig.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\nimport type { McpAllowlist, McpConfigGroupsInput } from './configSources.ts';\n\n/**\n * Environment variable carrying this session's MCP configuration as JSON.\n *\n * doom-pi resolves the plugin set and the domain allowlist before it launches Pi,\n * and the extension runs in that child process, so the resolved answer travels in\n * the environment rather than being recomputed here from `.doom/domains.yaml`.\n */\nexport const SESSION_ENV_VAR = 'DOOM_MCP_SESSION';\n\nexport type McpSessionConfig = McpConfigGroupsInput;\n\nfunction asStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const strings = value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);\n return strings.length > 0 ? strings : undefined;\n}\n\nfunction readAllowlist(value: unknown): McpAllowlist | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const { servers, proxy } = value as { servers?: unknown; proxy?: unknown };\n const allowlist: McpAllowlist = {};\n const allowedServers = asStringArray(servers);\n const allowedProxy = asStringArray(proxy);\n if (allowedServers) allowlist.servers = allowedServers;\n if (allowedProxy) allowlist.proxy = allowedProxy;\n return allowlist.servers || allowlist.proxy ? allowlist : undefined;\n}\n\n/**\n * Resolves what this session should connect to.\n *\n * Every field falls back to a working default so the extension is usable outside\n * doom-pi: a bare Pi session in a repository still gets that repository's\n * `.mcp.json` with no allowlist. A malformed value is treated as absent rather\n * than fatal, because losing MCP is better than losing the session.\n */\nexport function readSessionConfig(\n env: NodeJS.ProcessEnv = process.env,\n defaultRepoRoot: string = process.cwd(),\n): McpSessionConfig {\n const fallback: McpSessionConfig = {\n repoRoot: defaultRepoRoot,\n stagingDirectory: path.join(os.tmpdir(), 'doom-mcp'),\n };\n\n const raw = env[SESSION_ENV_VAR];\n if (!raw) return fallback;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return fallback;\n }\n if (typeof parsed !== 'object' || parsed === null) return fallback;\n\n const { repoRoot, pluginConfigPaths, allowlist, stagingDirectory } = parsed as Record<string, unknown>;\n const resolvedRoot = typeof repoRoot === 'string' && repoRoot ? repoRoot : fallback.repoRoot;\n const plugins = asStringArray(pluginConfigPaths);\n const resolvedAllowlist = readAllowlist(allowlist);\n const staging =\n typeof stagingDirectory === 'string' && stagingDirectory ? stagingDirectory : fallback.stagingDirectory;\n\n return {\n repoRoot: resolvedRoot,\n stagingDirectory: staging,\n ...(plugins ? { pluginConfigPaths: plugins } : {}),\n ...(resolvedAllowlist ? { allowlist: resolvedAllowlist } : {}),\n };\n}\n\n/** Serializes the configuration for the Pi child process doom-pi launches. */\nexport function sessionConfigEnvironment(config: McpSessionConfig): Record<string, string> {\n return { [SESSION_ENV_VAR]: JSON.stringify(config) };\n}\n"],"mappings":"2IAWA,MAAa,EAAkB,mBAI/B,SAAS,EAAc,EAAsC,CAC3D,GAAI,CAAC,MAAM,QAAQ,CAAK,EAAG,OAC3B,IAAM,EAAU,EAAM,OAAQ,GAA2B,OAAO,GAAU,UAAY,EAAM,OAAS,CAAC,EACtG,OAAO,EAAQ,OAAS,EAAI,EAAU,IAAA,EACxC,CAEA,SAAS,EAAc,EAA0C,CAC/D,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,GAAM,CAAE,UAAS,SAAU,EACrB,EAA0B,CAAC,EAC3B,EAAiB,EAAc,CAAO,EACtC,EAAe,EAAc,CAAK,EAGxC,OAFI,IAAgB,EAAU,QAAU,GACpC,IAAc,EAAU,MAAQ,GAC7B,EAAU,SAAW,EAAU,MAAQ,EAAY,IAAA,EAC5D,CAUA,SAAgB,EACd,EAAyB,QAAQ,IACjC,EAA0B,QAAQ,IAAI,EACpB,CAClB,IAAM,EAA6B,CACjC,SAAU,EACV,iBAAkBA,EAAAA,QAAK,KAAKC,EAAAA,QAAG,OAAO,EAAG,UAAU,CACrD,EAEM,EAAM,EAAI,GAChB,GAAI,CAAC,EAAK,OAAO,EAEjB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,CAAG,CACzB,MAAQ,CACN,OAAO,CACT,CACA,GAAI,OAAO,GAAW,WAAY,EAAiB,OAAO,EAE1D,GAAM,CAAE,WAAU,oBAAmB,YAAW,oBAAqB,EAC/D,EAAe,OAAO,GAAa,UAAY,EAAW,EAAW,EAAS,SAC9E,EAAU,EAAc,CAAiB,EACzC,EAAoB,EAAc,CAAS,EAIjD,MAAO,CACL,SAAU,EACV,iBAJA,OAAO,GAAqB,UAAY,EAAmB,EAAmB,EAAS,iBAKvF,GAAI,EAAU,CAAE,kBAAmB,CAAQ,EAAI,CAAC,EAChD,GAAI,EAAoB,CAAE,UAAW,CAAkB,EAAI,CAAC,CAC9D,CACF,CAGA,SAAgB,EAAyB,EAAkD,CACzF,MAAO,EAAG,GAAkB,KAAK,UAAU,CAAM,CAAE,CACrD"}
@@ -0,0 +1,25 @@
1
+ import { McpConfigGroupsInput } from "./configSources.cjs";
2
+ //#region src/services/sessionConfig.d.ts
3
+ /**
4
+ * Environment variable carrying this session's MCP configuration as JSON.
5
+ *
6
+ * doom-pi resolves the plugin set and the domain allowlist before it launches Pi,
7
+ * and the extension runs in that child process, so the resolved answer travels in
8
+ * the environment rather than being recomputed here from `.doom/domains.yaml`.
9
+ */
10
+ declare const SESSION_ENV_VAR = "DOOM_MCP_SESSION";
11
+ type McpSessionConfig = McpConfigGroupsInput;
12
+ /**
13
+ * Resolves what this session should connect to.
14
+ *
15
+ * Every field falls back to a working default so the extension is usable outside
16
+ * doom-pi: a bare Pi session in a repository still gets that repository's
17
+ * `.mcp.json` with no allowlist. A malformed value is treated as absent rather
18
+ * than fatal, because losing MCP is better than losing the session.
19
+ */
20
+ declare function readSessionConfig(env?: NodeJS.ProcessEnv, defaultRepoRoot?: string): McpSessionConfig;
21
+ /** Serializes the configuration for the Pi child process doom-pi launches. */
22
+ declare function sessionConfigEnvironment(config: McpSessionConfig): Record<string, string>;
23
+ //#endregion
24
+ export { McpSessionConfig, SESSION_ENV_VAR, readSessionConfig, sessionConfigEnvironment };
25
+ //# sourceMappingURL=sessionConfig.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionConfig.d.cts","names":[],"sources":["../../src/services/sessionConfig.ts"],"mappings":";;;;;;;;;cAWa;KAED,mBAAmB;;;;;;;;;iBA2Bf,kBACd,MAAK,OAAO,YACZ,2BACC;;iBAiCa,yBAAyB,QAAQ,mBAAmB"}
@@ -0,0 +1,25 @@
1
+ import { McpConfigGroupsInput } from "./configSources.mjs";
2
+ //#region src/services/sessionConfig.d.ts
3
+ /**
4
+ * Environment variable carrying this session's MCP configuration as JSON.
5
+ *
6
+ * doom-pi resolves the plugin set and the domain allowlist before it launches Pi,
7
+ * and the extension runs in that child process, so the resolved answer travels in
8
+ * the environment rather than being recomputed here from `.doom/domains.yaml`.
9
+ */
10
+ declare const SESSION_ENV_VAR = "DOOM_MCP_SESSION";
11
+ type McpSessionConfig = McpConfigGroupsInput;
12
+ /**
13
+ * Resolves what this session should connect to.
14
+ *
15
+ * Every field falls back to a working default so the extension is usable outside
16
+ * doom-pi: a bare Pi session in a repository still gets that repository's
17
+ * `.mcp.json` with no allowlist. A malformed value is treated as absent rather
18
+ * than fatal, because losing MCP is better than losing the session.
19
+ */
20
+ declare function readSessionConfig(env?: NodeJS.ProcessEnv, defaultRepoRoot?: string): McpSessionConfig;
21
+ /** Serializes the configuration for the Pi child process doom-pi launches. */
22
+ declare function sessionConfigEnvironment(config: McpSessionConfig): Record<string, string>;
23
+ //#endregion
24
+ export { McpSessionConfig, SESSION_ENV_VAR, readSessionConfig, sessionConfigEnvironment };
25
+ //# sourceMappingURL=sessionConfig.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionConfig.d.mts","names":[],"sources":["../../src/services/sessionConfig.ts"],"mappings":";;;;;;;;;cAWa;KAED,mBAAmB;;;;;;;;;iBA2Bf,kBACd,MAAK,OAAO,YACZ,2BACC;;iBAiCa,yBAAyB,QAAQ,mBAAmB"}
@@ -0,0 +1,2 @@
1
+ import e from"node:path";import t from"node:os";const n=`DOOM_MCP_SESSION`;function r(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`&&e.length>0);return t.length>0?t:void 0}function i(e){if(typeof e!=`object`||!e)return;let{servers:t,proxy:n}=e,i={},a=r(t),o=r(n);return a&&(i.servers=a),o&&(i.proxy=o),i.servers||i.proxy?i:void 0}function a(a=process.env,o=process.cwd()){let s={repoRoot:o,stagingDirectory:e.join(t.tmpdir(),`doom-mcp`)},c=a[n];if(!c)return s;let l;try{l=JSON.parse(c)}catch{return s}if(typeof l!=`object`||!l)return s;let{repoRoot:u,pluginConfigPaths:d,allowlist:f,stagingDirectory:p}=l,m=typeof u==`string`&&u?u:s.repoRoot,h=r(d),g=i(f);return{repoRoot:m,stagingDirectory:typeof p==`string`&&p?p:s.stagingDirectory,...h?{pluginConfigPaths:h}:{},...g?{allowlist:g}:{}}}function o(e){return{[n]:JSON.stringify(e)}}export{n as SESSION_ENV_VAR,a as readSessionConfig,o as sessionConfigEnvironment};
2
+ //# sourceMappingURL=sessionConfig.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionConfig.mjs","names":[],"sources":["../../src/services/sessionConfig.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\nimport type { McpAllowlist, McpConfigGroupsInput } from './configSources.ts';\n\n/**\n * Environment variable carrying this session's MCP configuration as JSON.\n *\n * doom-pi resolves the plugin set and the domain allowlist before it launches Pi,\n * and the extension runs in that child process, so the resolved answer travels in\n * the environment rather than being recomputed here from `.doom/domains.yaml`.\n */\nexport const SESSION_ENV_VAR = 'DOOM_MCP_SESSION';\n\nexport type McpSessionConfig = McpConfigGroupsInput;\n\nfunction asStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const strings = value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);\n return strings.length > 0 ? strings : undefined;\n}\n\nfunction readAllowlist(value: unknown): McpAllowlist | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const { servers, proxy } = value as { servers?: unknown; proxy?: unknown };\n const allowlist: McpAllowlist = {};\n const allowedServers = asStringArray(servers);\n const allowedProxy = asStringArray(proxy);\n if (allowedServers) allowlist.servers = allowedServers;\n if (allowedProxy) allowlist.proxy = allowedProxy;\n return allowlist.servers || allowlist.proxy ? allowlist : undefined;\n}\n\n/**\n * Resolves what this session should connect to.\n *\n * Every field falls back to a working default so the extension is usable outside\n * doom-pi: a bare Pi session in a repository still gets that repository's\n * `.mcp.json` with no allowlist. A malformed value is treated as absent rather\n * than fatal, because losing MCP is better than losing the session.\n */\nexport function readSessionConfig(\n env: NodeJS.ProcessEnv = process.env,\n defaultRepoRoot: string = process.cwd(),\n): McpSessionConfig {\n const fallback: McpSessionConfig = {\n repoRoot: defaultRepoRoot,\n stagingDirectory: path.join(os.tmpdir(), 'doom-mcp'),\n };\n\n const raw = env[SESSION_ENV_VAR];\n if (!raw) return fallback;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return fallback;\n }\n if (typeof parsed !== 'object' || parsed === null) return fallback;\n\n const { repoRoot, pluginConfigPaths, allowlist, stagingDirectory } = parsed as Record<string, unknown>;\n const resolvedRoot = typeof repoRoot === 'string' && repoRoot ? repoRoot : fallback.repoRoot;\n const plugins = asStringArray(pluginConfigPaths);\n const resolvedAllowlist = readAllowlist(allowlist);\n const staging =\n typeof stagingDirectory === 'string' && stagingDirectory ? stagingDirectory : fallback.stagingDirectory;\n\n return {\n repoRoot: resolvedRoot,\n stagingDirectory: staging,\n ...(plugins ? { pluginConfigPaths: plugins } : {}),\n ...(resolvedAllowlist ? { allowlist: resolvedAllowlist } : {}),\n };\n}\n\n/** Serializes the configuration for the Pi child process doom-pi launches. */\nexport function sessionConfigEnvironment(config: McpSessionConfig): Record<string, string> {\n return { [SESSION_ENV_VAR]: JSON.stringify(config) };\n}\n"],"mappings":"gDAWA,MAAa,EAAkB,mBAI/B,SAAS,EAAc,EAAsC,CAC3D,GAAI,CAAC,MAAM,QAAQ,CAAK,EAAG,OAC3B,IAAM,EAAU,EAAM,OAAQ,GAA2B,OAAO,GAAU,UAAY,EAAM,OAAS,CAAC,EACtG,OAAO,EAAQ,OAAS,EAAI,EAAU,IAAA,EACxC,CAEA,SAAS,EAAc,EAA0C,CAC/D,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,GAAM,CAAE,UAAS,SAAU,EACrB,EAA0B,CAAC,EAC3B,EAAiB,EAAc,CAAO,EACtC,EAAe,EAAc,CAAK,EAGxC,OAFI,IAAgB,EAAU,QAAU,GACpC,IAAc,EAAU,MAAQ,GAC7B,EAAU,SAAW,EAAU,MAAQ,EAAY,IAAA,EAC5D,CAUA,SAAgB,EACd,EAAyB,QAAQ,IACjC,EAA0B,QAAQ,IAAI,EACpB,CAClB,IAAM,EAA6B,CACjC,SAAU,EACV,iBAAkB,EAAK,KAAK,EAAG,OAAO,EAAG,UAAU,CACrD,EAEM,EAAM,EAAI,GAChB,GAAI,CAAC,EAAK,OAAO,EAEjB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,CAAG,CACzB,MAAQ,CACN,OAAO,CACT,CACA,GAAI,OAAO,GAAW,WAAY,EAAiB,OAAO,EAE1D,GAAM,CAAE,WAAU,oBAAmB,YAAW,oBAAqB,EAC/D,EAAe,OAAO,GAAa,UAAY,EAAW,EAAW,EAAS,SAC9E,EAAU,EAAc,CAAiB,EACzC,EAAoB,EAAc,CAAS,EAIjD,MAAO,CACL,SAAU,EACV,iBAJA,OAAO,GAAqB,UAAY,EAAmB,EAAmB,EAAS,iBAKvF,GAAI,EAAU,CAAE,kBAAmB,CAAQ,EAAI,CAAC,EAChD,GAAI,EAAoB,CAAE,UAAW,CAAkB,EAAI,CAAC,CAC9D,CACF,CAGA,SAAgB,EAAyB,EAAkD,CACzF,MAAO,EAAG,GAAkB,KAAK,UAAU,CAAM,CAAE,CACrD"}
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./configSources.cjs"),n=require("./mcpRuntime.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:crypto"),a=require("node:path");a=e.__toESM(a,1);let o=require("@agimon-ai/mcp-proxy"),s=require("node:os");s=e.__toESM(s,1);const c=/^(?:shared|mcp)-[0-9a-f]{16}\.json$/;function l(e){let t=(0,i.createHash)(`sha256`).update(e).digest(`hex`).slice(0,16);return a.default.join(s.default.tmpdir(),`doom-mcp`,`shared-${t}.json`)}async function u(e){if(e.configPaths.length!==0)try{let n=(0,o.resolveWorkspaceRoot)(e.repoRoot),i=await(0,o.findHealthyExistingRuntime)(n),s=i??await d(e.configPaths),c=`http://${s.host}:${s.port}/mcp`,u=l(n);return r.default.mkdirSync(a.default.dirname(u),{mode:448,recursive:!0}),r.default.writeFileSync(u,`${JSON.stringify({mcpServers:{[t.PROXY_SERVER_NAME]:{type:`http`,url:c}}},null,2)}\n`,{mode:384}),{configPath:u,endpoint:c,reusedExistingRuntime:i!==null}}catch{return}}async function d(e){process.env.MCP_PROXY_IDLE_SHUTDOWN_MINUTES??=`45`;let t=await(0,o.prestartHttpRuntime)({host:`localhost`,config:e,cache:!0,clearDefinitionsCache:!1,proxyMode:`flat`,runtimeLifecycle:`manual`});return{host:t.host,port:t.port}}async function f(e,t=Date.now()){p(e,t),await m()}function p(e,t){let n;try{n=r.default.readdirSync(e)}catch{return}for(let i of n){if(!c.test(i))continue;let n=a.default.join(e,i);try{t-r.default.statSync(n).mtimeMs>6048e5&&r.default.unlinkSync(n)}catch{}}}async function m(){try{await(await import(`@agimon-ai/mcp-proxy`)).cleanupStaleRegistries?.()}catch{}}function h(e,t){if(!t||t.length===0)return;let r=new Set(t),i=n.readCachedCatalog(e),a=new Set,o=g(i.servers);for(let e of i.servers)if(r.has(e.name))for(let t of e.tools)a.add(o.has(t.name)?`${e.name}__${t.name}`:t.name);return a}function g(e){let t=new Set,n=new Set;for(let r of e)for(let e of r.tools)t.has(e.name)&&n.add(e.name),t.add(e.name);return n}exports.ensureSharedRuntime=u,exports.maintainSharedResources=f,exports.resolveAllowedUpstreamTools=h,exports.sharedLayerPath=l;
2
+ //# sourceMappingURL=sharedRuntime.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedRuntime.cjs","names":["createHash","path","os","resolveWorkspaceRoot","findHealthyExistingRuntime","PROXY_SERVER_NAME","prestartHttpRuntime","fs","readCachedCatalog"],"sources":["../../src/services/sharedRuntime.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { findHealthyExistingRuntime, prestartHttpRuntime, resolveWorkspaceRoot } from '@agimon-ai/mcp-proxy';\nimport { PROXY_SERVER_NAME } from './configSources.ts';\nimport { readCachedCatalog } from './mcpRuntime.ts';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst RUNTIME_HOST = 'localhost';\n/** Flat mode is what `.mcp.json` already asked the proxy for, so tool names are unchanged. */\nconst RUNTIME_PROXY_MODE = 'flat';\n/** Read by the spawned runtime; versions of mcp-proxy without it ignore the value. */\nconst IDLE_SHUTDOWN_ENV = 'MCP_PROXY_IDLE_SHUTDOWN_MINUTES';\n/**\n * Deliberately longer than the proxy's own 30 minute session sweep.\n *\n * A runtime is idle only once it holds no session, and the sweep is what closes\n * the last quiet one. Matching the two would retire the runtime at the same\n * moment, so a session returning after a long pause would always pay a cold\n * start; the extra window lets it rejoin a warm runtime instead.\n */\nconst DEFAULT_IDLE_SHUTDOWN_MINUTES = '45';\n/** Staged layers this package writes: `shared-<hash>.json` and `mcp-<hash>.json`. */\nconst STAGING_FILE_PATTERN = /^(?:shared|mcp)-[0-9a-f]{16}\\.json$/;\nconst STAGING_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport interface SharedRuntimeOptions {\n repoRoot: string;\n /** The worktree-level config the runtime is started from, normally `mcp-config.yaml`. */\n configPaths: string[];\n}\n\nexport interface SharedRuntimeLayer {\n /** Generated `.mcp.json` layer pointing the proxy entry at the live runtime. */\n configPath: string;\n endpoint: string;\n reusedExistingRuntime: boolean;\n}\n\n/**\n * Where the generated layer for a workspace lives.\n *\n * Stable across runs on purpose: the definitions cache is keyed on the ordered\n * list of config paths, so a per-run filename would mint a new cache every start\n * and the tools would never be available before the first connection.\n */\nexport function sharedLayerPath(workspaceRoot: string): string {\n const digest = createHash('sha256').update(workspaceRoot).digest('hex').slice(0, 16);\n return path.join(os.tmpdir(), 'doom-mcp', `shared-${digest}.json`);\n}\n\n/**\n * Starts or joins the worktree's shared MCP runtime and returns a config layer for it.\n *\n * One runtime serves every doom-pi session in the worktree, so a healthy one is\n * always reused rather than respawned; only the first session in a worktree pays\n * to start it. The runtime is reached as an ordinary HTTP server named\n * `agiflow-proxy`, which is what `.mcp.json` already calls it, so the tool names\n * an agent sees are the same as before this package existed.\n *\n * Returns undefined when there is nothing to share, or when the runtime could not\n * be reached: losing the shared upstreams is better than losing the session.\n */\nexport async function ensureSharedRuntime(options: SharedRuntimeOptions): Promise<SharedRuntimeLayer | undefined> {\n if (options.configPaths.length === 0) return undefined;\n\n try {\n const workspaceRoot = resolveWorkspaceRoot(options.repoRoot);\n const existing = await findHealthyExistingRuntime(workspaceRoot);\n const runtime = existing ?? (await startRuntime(options.configPaths));\n const endpoint = `http://${runtime.host}:${runtime.port}/mcp`;\n\n const configPath = sharedLayerPath(workspaceRoot);\n fs.mkdirSync(path.dirname(configPath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n fs.writeFileSync(\n configPath,\n `${JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: { type: 'http', url: endpoint } } }, null, 2)}\\n`,\n { mode: PRIVATE_FILE_MODE },\n );\n\n return { configPath, endpoint, reusedExistingRuntime: existing !== null };\n } catch {\n return undefined;\n }\n}\n\nasync function startRuntime(configPaths: string[]): Promise<{ host: string; port: number }> {\n // The runtime inherits this environment, so the value reaches it whichever\n // mcp-proxy version is installed. It bounds how long a runtime outlives its\n // last session; an operator's explicit setting wins.\n process.env[IDLE_SHUTDOWN_ENV] ??= DEFAULT_IDLE_SHUTDOWN_MINUTES;\n const result = await prestartHttpRuntime({\n host: RUNTIME_HOST,\n config: configPaths,\n cache: true,\n clearDefinitionsCache: false,\n proxyMode: RUNTIME_PROXY_MODE,\n // Outliving this session is the point: the next one joins it instead of paying\n // to start the same downstream children again. The idle shutdown above is what\n // keeps that from meaning forever.\n runtimeLifecycle: 'manual',\n });\n return { host: result.host, port: result.port };\n}\n\n/**\n * Best-effort maintenance for the resources shared runtimes leave on disk.\n *\n * Runs detached from session start, so a session neither waits for it nor fails\n * because of it. Two jobs: drop staged config layers no session has refreshed\n * within the TTL (every live session rewrites its own layers at start, so age is\n * ownership), and let mcp-proxy prune registry rows whose processes are gone,\n * when the installed version exposes that.\n */\nexport async function maintainSharedResources(stagingDirectory: string, now: number = Date.now()): Promise<void> {\n sweepStagingDirectory(stagingDirectory, now);\n await collectRegistryGarbage();\n}\n\nfunction sweepStagingDirectory(stagingDirectory: string, now: number): void {\n let entries: string[];\n try {\n entries = fs.readdirSync(stagingDirectory);\n } catch {\n // Nothing has been staged yet, so there is nothing to sweep.\n return;\n }\n for (const entry of entries) {\n if (!STAGING_FILE_PATTERN.test(entry)) continue;\n const target = path.join(stagingDirectory, entry);\n try {\n if (now - fs.statSync(target).mtimeMs > STAGING_TTL_MS) fs.unlinkSync(target);\n } catch {\n // Another session touching the same file concurrently is not a failure.\n }\n }\n}\n\nasync function collectRegistryGarbage(): Promise<void> {\n try {\n // Feature-detected: the pinned mcp-proxy may predate the export.\n const proxy = (await import('@agimon-ai/mcp-proxy')) as unknown as { cleanupStaleRegistries?: () => Promise<void> };\n await proxy.cleanupStaleRegistries?.();\n } catch {\n // Garbage collection never gets to break a session.\n }\n}\n\n/**\n * The proxy tool names a domain's upstream allowlist permits.\n *\n * The runtime is shared, so its own configuration cannot be narrowed per session;\n * the allowlist is applied where this session registers tools instead. Attribution\n * comes from the catalog the runtime itself wrote, which records which upstream\n * each tool came from, rather than from parsing the proxied description text.\n *\n * Returns undefined when every upstream is allowed, which needs no filtering.\n */\nexport function resolveAllowedUpstreamTools(\n sharedConfigPaths: string[],\n allowedUpstreams: string[] | undefined,\n): Set<string> | undefined {\n if (!allowedUpstreams || allowedUpstreams.length === 0) return undefined;\n const allowed = new Set(allowedUpstreams);\n const catalog = readCachedCatalog(sharedConfigPaths);\n const names = new Set<string>();\n const clashing = findClashingToolNames(catalog.servers);\n for (const server of catalog.servers) {\n if (!allowed.has(server.name)) continue;\n for (const tool of server.tools) {\n // Flat mode only prefixes a tool whose name two upstreams both use.\n names.add(clashing.has(tool.name) ? `${server.name}__${tool.name}` : tool.name);\n }\n }\n return names;\n}\n\nfunction findClashingToolNames(servers: ReadonlyArray<{ tools: ReadonlyArray<{ name: string }> }>): Set<string> {\n const seen = new Set<string>();\n const clashing = new Set<string>();\n for (const server of servers) {\n for (const tool of server.tools) {\n if (seen.has(tool.name)) clashing.add(tool.name);\n seen.add(tool.name);\n }\n }\n return clashing;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{PROXY_SERVER_NAME as e}from"./configSources.mjs";import{readCachedCatalog as t}from"./mcpRuntime.mjs";import n from"node:fs";import{createHash as r}from"node:crypto";import i from"node:path";import{findHealthyExistingRuntime as a,prestartHttpRuntime as o,resolveWorkspaceRoot as s}from"@agimon-ai/mcp-proxy";import c from"node:os";const l=/^(?:shared|mcp)-[0-9a-f]{16}\.json$/;function u(e){let t=r(`sha256`).update(e).digest(`hex`).slice(0,16);return i.join(c.tmpdir(),`doom-mcp`,`shared-${t}.json`)}async function d(t){if(t.configPaths.length!==0)try{let r=s(t.repoRoot),o=await a(r),c=o??await f(t.configPaths),l=`http://${c.host}:${c.port}/mcp`,d=u(r);return n.mkdirSync(i.dirname(d),{mode:448,recursive:!0}),n.writeFileSync(d,`${JSON.stringify({mcpServers:{[e]:{type:`http`,url:l}}},null,2)}\n`,{mode:384}),{configPath:d,endpoint:l,reusedExistingRuntime:o!==null}}catch{return}}async function f(e){process.env.MCP_PROXY_IDLE_SHUTDOWN_MINUTES??=`45`;let t=await o({host:`localhost`,config:e,cache:!0,clearDefinitionsCache:!1,proxyMode:`flat`,runtimeLifecycle:`manual`});return{host:t.host,port:t.port}}async function p(e,t=Date.now()){m(e,t),await h()}function m(e,t){let r;try{r=n.readdirSync(e)}catch{return}for(let a of r){if(!l.test(a))continue;let r=i.join(e,a);try{t-n.statSync(r).mtimeMs>6048e5&&n.unlinkSync(r)}catch{}}}async function h(){try{await(await import(`@agimon-ai/mcp-proxy`)).cleanupStaleRegistries?.()}catch{}}function g(e,n){if(!n||n.length===0)return;let r=new Set(n),i=t(e),a=new Set,o=_(i.servers);for(let e of i.servers)if(r.has(e.name))for(let t of e.tools)a.add(o.has(t.name)?`${e.name}__${t.name}`:t.name);return a}function _(e){let t=new Set,n=new Set;for(let r of e)for(let e of r.tools)t.has(e.name)&&n.add(e.name),t.add(e.name);return n}export{d as ensureSharedRuntime,p as maintainSharedResources,g as resolveAllowedUpstreamTools,u as sharedLayerPath};
2
+ //# sourceMappingURL=sharedRuntime.mjs.map