@agimon-ai/doompi-mcp 0.0.1-alpha.0 → 0.0.1-alpha.2

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.
@@ -1,2 +1,2 @@
1
- const e=require("./constants.cjs"),t=require("./commands/mcpCommand.cjs"),n=require("./leader.cjs"),r=require("./services/sessionConfig.cjs"),i=require("./services/mcpSession.cjs"),a=require("./tui/mcpOverlay.cjs");let o=require("@agimon-ai/doompi-extension-contracts/mcp-status"),s=require("@agimon-ai/doompi-extension-contracts/protocol");function c(c,l={}){let u,d,f=new i.McpSession({pi:c,onAuthorizationUrl:(e,t)=>{u?.hasUI||u?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),p=r.readSessionConfig();f.install(l.configurationSource===`doom`?p:{repoRoot:p.repoRoot,stagingDirectory:p.stagingDirectory}),t.registerCommand(c,f,{openOverlay:e=>a.openMcpOverlay(e,f)}),n.registerLeaderContribution(c),c.on(`session_start`,(t,n)=>{u=n,l.configurationSource!==`doom`&&f.install(r.readSessionConfig({},n.cwd));let i=(0,s.createProtocolRuntime)({bus:c.events,source:e.PACKAGE_SOURCE,sessionId:n.sessionManager.getSessionId()});d?.(),d=i.provide(o.McpStatusQueryProtocol,()=>f.getSnapshot()),f.activate();for(let e of f.getDiagnostics())n.ui?.notify(e,`warning`);f.start()}),c.on(`session_shutdown`,()=>{d?.(),d=void 0,u=void 0,f.dispose()})}exports.registerMcpExtension=c;
1
+ const e=require("./constants.cjs"),t=require("./commands/mcpCommand.cjs"),n=require("./leader.cjs"),r=require("./services/sessionConfig.cjs"),i=require("./services/mcpSession.cjs");let a=require("@agimon-ai/doompi-extension-contracts/mcp-status"),o=require("@agimon-ai/doompi-extension-contracts/protocol");function s(s,c={}){let l,u,d=new i.McpSession({pi:s,onAuthorizationUrl:(e,t)=>{l?.hasUI||l?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),f=r.readSessionConfig();d.install(c.configurationSource===`doom`?f:{repoRoot:f.repoRoot,stagingDirectory:f.stagingDirectory}),t.registerCommand(s,d,{openOverlay:async e=>{let{openMcpOverlay:t}=await Promise.resolve().then(()=>require("./tui/mcpOverlay.cjs"));return t(e,d)}}),n.registerLeaderContribution(s),s.on(`session_start`,(t,n)=>{l=n,c.configurationSource!==`doom`&&d.install(r.readSessionConfig({},n.cwd));let i=(0,o.createProtocolRuntime)({bus:s.events,source:e.PACKAGE_SOURCE,sessionId:n.sessionManager.getSessionId()});u?.(),u=i.provide(a.McpStatusQueryProtocol,()=>d.getSnapshot()),d.activate();for(let e of d.getDiagnostics())n.ui?.notify(e,`warning`);d.start()}),s.on(`session_shutdown`,()=>{u?.(),u=void 0,l=void 0,d.dispose()})}exports.registerMcpExtension=s;
2
2
  //# sourceMappingURL=extension.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.cjs","names":["McpSession","readSessionConfig","openMcpOverlay","createProtocolRuntime","PACKAGE_SOURCE","McpStatusQueryProtocol"],"sources":["../src/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerCommand } from './commands/mcpCommand.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { PACKAGE_SOURCE } from './constants.ts';\nimport { McpSession } from './services/mcpSession.ts';\nimport { readSessionConfig } from './services/sessionConfig.ts';\nimport { openMcpOverlay } from './tui/mcpOverlay.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | 'doom';\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: (url, serverName) => {\n // The overlay carries the URL for anyone who can see one, and it is where the\n // flow was started from. Announcing it here as well would only put it in the\n // transcript underneath a fullscreen panel. A session with no UI has no\n // overlay to read, so that is the one case still worth notifying.\n if (activeContext?.hasUI) return;\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === 'doom'\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, { openOverlay: (ctx) => openMcpOverlay(ctx, session) });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== 'doom') session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"qVA4BA,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAIA,EAAAA,WAAW,CAC7B,KACA,oBAAqB,EAAK,IAAe,CAKnC,GAAe,OACnB,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuBC,EAAAA,kBAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,OAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAA,gBAAgB,EAAI,EAAS,CAAE,YAAc,GAAQC,EAAAA,eAAe,EAAK,CAAO,CAAE,CAAC,EACnF,EAAA,2BAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,QAAQ,EAAQ,QAAQD,EAAAA,kBAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC1F,IAAM,GAAA,EAAWE,EAAAA,sBAAAA,CAAsB,CACrC,IAAK,EAAG,OACR,OAAQC,EAAAA,eACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQC,EAAAA,2BAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,SAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
1
+ {"version":3,"file":"extension.cjs","names":["McpSession","readSessionConfig","createProtocolRuntime","PACKAGE_SOURCE","McpStatusQueryProtocol"],"sources":["../src/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerCommand } from './commands/mcpCommand.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { PACKAGE_SOURCE } from './constants.ts';\nimport { McpSession } from './services/mcpSession.ts';\nimport { readSessionConfig } from './services/sessionConfig.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | 'doom';\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: (url, serverName) => {\n // The overlay carries the URL for anyone who can see one, and it is where the\n // flow was started from. Announcing it here as well would only put it in the\n // transcript underneath a fullscreen panel. A session with no UI has no\n // overlay to read, so that is the one case still worth notifying.\n if (activeContext?.hasUI) return;\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === 'doom'\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, {\n openOverlay: async (ctx) => {\n const { openMcpOverlay } = await import('./tui/mcpOverlay.ts');\n return openMcpOverlay(ctx, session);\n },\n });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== 'doom') session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"mTA2BA,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAIA,EAAAA,WAAW,CAC7B,KACA,oBAAqB,EAAK,IAAe,CAKnC,GAAe,OACnB,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuBC,EAAAA,kBAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,OAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAA,gBAAgB,EAAI,EAAS,CAC3B,YAAa,KAAO,IAAQ,CAC1B,GAAM,CAAE,kBAAmB,MAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAM,sBAAA,CAAA,EACjC,OAAO,EAAe,EAAK,CAAO,CACpC,CACF,CAAC,EACD,EAAA,2BAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,QAAQ,EAAQ,QAAQA,EAAAA,kBAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC1F,IAAM,GAAA,EAAWC,EAAAA,sBAAAA,CAAsB,CACrC,IAAK,EAAG,OACR,OAAQC,EAAAA,eACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQC,EAAAA,2BAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,SAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.cts","names":[],"sources":["../src/extension.ts"],"mappings":";;UAeiB;;EAEf;;;;;;;;;;iBAWc,qBAAqB,IAAI,cAAc,UAAS"}
1
+ {"version":3,"file":"extension.d.cts","names":[],"sources":["../src/extension.ts"],"mappings":";;UAciB;;EAEf;;;;;;;;;;iBAWc,qBAAqB,IAAI,cAAc,UAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.mts","names":[],"sources":["../src/extension.ts"],"mappings":";;UAeiB;;EAEf;;;;;;;;;;iBAWc,qBAAqB,IAAI,cAAc,UAAS"}
1
+ {"version":3,"file":"extension.d.mts","names":[],"sources":["../src/extension.ts"],"mappings":";;UAciB;;EAEf;;;;;;;;;;iBAWc,qBAAqB,IAAI,cAAc,UAAS"}
@@ -1,2 +1,2 @@
1
- import{PACKAGE_SOURCE as e}from"./constants.mjs";import{registerCommand as t}from"./commands/mcpCommand.mjs";import{registerLeaderContribution as n}from"./leader.mjs";import{readSessionConfig as r}from"./services/sessionConfig.mjs";import{McpSession as i}from"./services/mcpSession.mjs";import{openMcpOverlay as a}from"./tui/mcpOverlay.mjs";import{McpStatusQueryProtocol as o}from"@agimon-ai/doompi-extension-contracts/mcp-status";import{createProtocolRuntime as s}from"@agimon-ai/doompi-extension-contracts/protocol";function c(c,l={}){let u,d,f=new i({pi:c,onAuthorizationUrl:(e,t)=>{u?.hasUI||u?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),p=r();f.install(l.configurationSource===`doom`?p:{repoRoot:p.repoRoot,stagingDirectory:p.stagingDirectory}),t(c,f,{openOverlay:e=>a(e,f)}),n(c),c.on(`session_start`,(t,n)=>{u=n,l.configurationSource!==`doom`&&f.install(r({},n.cwd));let i=s({bus:c.events,source:e,sessionId:n.sessionManager.getSessionId()});d?.(),d=i.provide(o,()=>f.getSnapshot()),f.activate();for(let e of f.getDiagnostics())n.ui?.notify(e,`warning`);f.start()}),c.on(`session_shutdown`,()=>{d?.(),d=void 0,u=void 0,f.dispose()})}export{c as registerMcpExtension};
1
+ import{PACKAGE_SOURCE as e}from"./constants.mjs";import{registerCommand as t}from"./commands/mcpCommand.mjs";import{registerLeaderContribution as n}from"./leader.mjs";import{readSessionConfig as r}from"./services/sessionConfig.mjs";import{McpSession as i}from"./services/mcpSession.mjs";import{McpStatusQueryProtocol as a}from"@agimon-ai/doompi-extension-contracts/mcp-status";import{createProtocolRuntime as o}from"@agimon-ai/doompi-extension-contracts/protocol";function s(s,c={}){let l,u,d=new i({pi:s,onAuthorizationUrl:(e,t)=>{l?.hasUI||l?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),f=r();d.install(c.configurationSource===`doom`?f:{repoRoot:f.repoRoot,stagingDirectory:f.stagingDirectory}),t(s,d,{openOverlay:async e=>{let{openMcpOverlay:t}=await import(`./tui/mcpOverlay.mjs`);return t(e,d)}}),n(s),s.on(`session_start`,(t,n)=>{l=n,c.configurationSource!==`doom`&&d.install(r({},n.cwd));let i=o({bus:s.events,source:e,sessionId:n.sessionManager.getSessionId()});u?.(),u=i.provide(a,()=>d.getSnapshot()),d.activate();for(let e of d.getDiagnostics())n.ui?.notify(e,`warning`);d.start()}),s.on(`session_shutdown`,()=>{u?.(),u=void 0,l=void 0,d.dispose()})}export{s as registerMcpExtension};
2
2
  //# sourceMappingURL=extension.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.mjs","names":[],"sources":["../src/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerCommand } from './commands/mcpCommand.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { PACKAGE_SOURCE } from './constants.ts';\nimport { McpSession } from './services/mcpSession.ts';\nimport { readSessionConfig } from './services/sessionConfig.ts';\nimport { openMcpOverlay } from './tui/mcpOverlay.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | 'doom';\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: (url, serverName) => {\n // The overlay carries the URL for anyone who can see one, and it is where the\n // flow was started from. Announcing it here as well would only put it in the\n // transcript underneath a fullscreen panel. A session with no UI has no\n // overlay to read, so that is the one case still worth notifying.\n if (activeContext?.hasUI) return;\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === 'doom'\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, { openOverlay: (ctx) => openMcpOverlay(ctx, session) });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== 'doom') session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"sgBA4BA,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAI,EAAW,CAC7B,KACA,oBAAqB,EAAK,IAAe,CAKnC,GAAe,OACnB,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuB,EAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,OAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAgB,EAAI,EAAS,CAAE,YAAc,GAAQ,EAAe,EAAK,CAAO,CAAE,CAAC,EACnF,EAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,QAAQ,EAAQ,QAAQ,EAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC1F,IAAM,EAAW,EAAsB,CACrC,IAAK,EAAG,OACR,OAAQ,EACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQ,MAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,SAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
1
+ {"version":3,"file":"extension.mjs","names":[],"sources":["../src/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerCommand } from './commands/mcpCommand.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { PACKAGE_SOURCE } from './constants.ts';\nimport { McpSession } from './services/mcpSession.ts';\nimport { readSessionConfig } from './services/sessionConfig.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | 'doom';\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: (url, serverName) => {\n // The overlay carries the URL for anyone who can see one, and it is where the\n // flow was started from. Announcing it here as well would only put it in the\n // transcript underneath a fullscreen panel. A session with no UI has no\n // overlay to read, so that is the one case still worth notifying.\n if (activeContext?.hasUI) return;\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === 'doom'\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, {\n openOverlay: async (ctx) => {\n const { openMcpOverlay } = await import('./tui/mcpOverlay.ts');\n return openMcpOverlay(ctx, session);\n },\n });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== 'doom') session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"gdA2BA,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAI,EAAW,CAC7B,KACA,oBAAqB,EAAK,IAAe,CAKnC,GAAe,OACnB,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuB,EAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,OAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAgB,EAAI,EAAS,CAC3B,YAAa,KAAO,IAAQ,CAC1B,GAAM,CAAE,kBAAmB,MAAM,OAAO,wBACxC,OAAO,EAAe,EAAK,CAAO,CACpC,CACF,CAAC,EACD,EAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,QAAQ,EAAQ,QAAQ,EAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC1F,IAAM,EAAW,EAAsB,CACrC,IAAK,EAAG,OACR,OAAQ,EACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQ,MAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,SAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
package/dist/leader.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./constants.cjs");let t=require("@agimon-ai/doompi-extension-contracts/protocol"),n=require("@agimon-ai/doompi-extension-contracts/leader");function r(e){e instanceof t.DoomProtocolError&&e.code===t.DOOM_PROTOCOL_ERROR_CODE.timeout||queueMicrotask(()=>{throw e instanceof Error?e:Error(String(e))})}function i(i){let a=0;i.on(`session_start`,(o,s)=>{let c=++a,l=(0,t.createProtocolRuntime)({bus:i.events,source:e.PACKAGE_SOURCE,sessionId:s.sessionManager.getSessionId()});l.request(n.LeaderRegistrationProtocol,{contribution:{source:e.PACKAGE_SOURCE,bindings:[{id:`${e.LEADER_BINDING_PREFIX}.open`,path:[e.LEADER_GROUP,{key:`m`,label:`mcp`,detail:e.LEADER_DETAIL}],command:{name:`mcp`}}]},generation:String(c)},{timeoutMs:250}).then(e=>{a!==c&&l.request(n.LeaderUnregistrationProtocol,{handleId:e.handleId},{timeoutMs:250}).catch(r)}).catch(r)}),i.on(`session_shutdown`,()=>{++a})}exports.registerLeaderContribution=i;
1
+ const e=require("./constants.cjs");let t=require("@agimon-ai/doompi-extension-contracts/leader");function n(n){(0,t.registerDoomLeaderContribution)(n,{source:e.PACKAGE_SOURCE,bindings:[{id:`${e.LEADER_BINDING_PREFIX}.open`,path:[e.LEADER_GROUP,{key:`m`,label:`mcp`,detail:e.LEADER_DETAIL}],command:{name:`mcp`}}]})}exports.registerLeaderContribution=n;
2
2
  //# sourceMappingURL=leader.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"leader.cjs","names":["DoomProtocolError","DOOM_PROTOCOL_ERROR_CODE","createProtocolRuntime","PACKAGE_SOURCE","LeaderRegistrationProtocol","LEADER_BINDING_PREFIX","LEADER_GROUP","LEADER_DETAIL","LeaderUnregistrationProtocol"],"sources":["../src/leader.ts"],"sourcesContent":["import { LeaderRegistrationProtocol, LeaderUnregistrationProtocol } from '@agimon-ai/doompi-extension-contracts/leader';\nimport {\n createProtocolRuntime,\n DOOM_PROTOCOL_ERROR_CODE,\n DoomProtocolError,\n} from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport {\n COMMAND_NAME,\n LEADER_BINDING_PREFIX,\n LEADER_DETAIL,\n LEADER_GROUP,\n LEADER_KEY,\n LEADER_LABEL,\n PACKAGE_SOURCE,\n} from './constants.ts';\n\nconst LEADER_PROVIDER_TIMEOUT_MS = 250;\n\nfunction handleOptionalLeaderFailure(error: unknown): void {\n if (error instanceof DoomProtocolError && error.code === DOOM_PROTOCOL_ERROR_CODE.timeout) return;\n queueMicrotask(() => {\n throw error instanceof Error ? error : new Error(String(error));\n });\n}\n\nexport function registerLeaderContribution(pi: ExtensionAPI): void {\n let generation = 0;\n\n pi.on('session_start', (_event, ctx) => {\n const activeGeneration = ++generation;\n const runtime = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n void runtime\n .request(\n LeaderRegistrationProtocol,\n {\n contribution: {\n source: PACKAGE_SOURCE,\n bindings: [\n {\n id: `${LEADER_BINDING_PREFIX}.open`,\n path: [LEADER_GROUP, { key: LEADER_KEY, label: LEADER_LABEL, detail: LEADER_DETAIL }],\n command: { name: COMMAND_NAME },\n },\n ],\n },\n generation: String(activeGeneration),\n },\n { timeoutMs: LEADER_PROVIDER_TIMEOUT_MS },\n )\n .then((registration) => {\n if (generation === activeGeneration) return;\n void runtime\n .request(\n LeaderUnregistrationProtocol,\n { handleId: registration.handleId },\n { timeoutMs: LEADER_PROVIDER_TIMEOUT_MS },\n )\n .catch(handleOptionalLeaderFailure);\n })\n .catch(handleOptionalLeaderFailure);\n });\n\n pi.on('session_shutdown', () => {\n ++generation;\n });\n}\n"],"mappings":"6JAmBA,SAAS,EAA4B,EAAsB,CACrD,aAAiBA,EAAAA,mBAAqB,EAAM,OAASC,EAAAA,yBAAyB,SAClF,mBAAqB,CACnB,MAAM,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CAChE,CAAC,CACH,CAEA,SAAgB,EAA2B,EAAwB,CACjE,IAAI,EAAa,EAEjB,EAAG,GAAG,iBAAkB,EAAQ,IAAQ,CACtC,IAAM,EAAmB,EAAE,EACrB,GAAA,EAAUC,EAAAA,sBAAAA,CAAsB,CACpC,IAAK,EAAG,OACR,OAAQC,EAAAA,eACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,EACG,QACCC,EAAAA,2BACA,CACE,aAAc,CACZ,OAAQD,EAAAA,eACR,SAAU,CACR,CACE,GAAI,GAAGE,EAAAA,sBAAsB,OAC7B,KAAM,CAACC,EAAAA,aAAc,CAAE,IAAA,IAAiB,MAAA,MAAqB,OAAQC,EAAAA,aAAc,CAAC,EACpF,QAAS,CAAE,KAAA,KAAmB,CAChC,CACF,CACF,EACA,WAAY,OAAO,CAAgB,CACrC,EACA,CAAE,UAAW,GAA2B,CAC1C,CAAC,CACA,KAAM,GAAiB,CAClB,IAAe,GACnB,EACG,QACCC,EAAAA,6BACA,CAAE,SAAU,EAAa,QAAS,EAClC,CAAE,UAAW,GAA2B,CAC1C,CAAC,CACA,MAAM,CAA2B,CACtC,CAAC,CAAC,CACD,MAAM,CAA2B,CACtC,CAAC,EAED,EAAG,GAAG,uBAA0B,CAC9B,EAAE,CACJ,CAAC,CACH"}
1
+ {"version":3,"file":"leader.cjs","names":["PACKAGE_SOURCE","LEADER_BINDING_PREFIX","LEADER_GROUP","LEADER_DETAIL"],"sources":["../src/leader.ts"],"sourcesContent":["import { registerDoomLeaderContribution } from '@agimon-ai/doompi-extension-contracts/leader';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport {\n COMMAND_NAME,\n LEADER_BINDING_PREFIX,\n LEADER_DETAIL,\n LEADER_GROUP,\n LEADER_KEY,\n LEADER_LABEL,\n PACKAGE_SOURCE,\n} from './constants.ts';\n\nexport function registerLeaderContribution(pi: ExtensionAPI): void {\n registerDoomLeaderContribution(pi, {\n source: PACKAGE_SOURCE,\n bindings: [\n {\n id: `${LEADER_BINDING_PREFIX}.open`,\n path: [LEADER_GROUP, { key: LEADER_KEY, label: LEADER_LABEL, detail: LEADER_DETAIL }],\n command: { name: COMMAND_NAME },\n },\n ],\n });\n}\n"],"mappings":"iGAYA,SAAgB,EAA2B,EAAwB,EACjE,EAAA,EAAA,+BAAA,CAA+B,EAAI,CACjC,OAAQA,EAAAA,eACR,SAAU,CACR,CACE,GAAI,GAAGC,EAAAA,sBAAsB,OAC7B,KAAM,CAACC,EAAAA,aAAc,CAAE,IAAA,IAAiB,MAAA,MAAqB,OAAQC,EAAAA,aAAc,CAAC,EACpF,QAAS,CAAE,KAAA,KAAmB,CAChC,CACF,CACF,CAAC,CACH"}
package/dist/leader.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{LEADER_BINDING_PREFIX as e,LEADER_DETAIL as t,LEADER_GROUP as n,PACKAGE_SOURCE as r}from"./constants.mjs";import{DOOM_PROTOCOL_ERROR_CODE as i,DoomProtocolError as a,createProtocolRuntime as o}from"@agimon-ai/doompi-extension-contracts/protocol";import{LeaderRegistrationProtocol as s,LeaderUnregistrationProtocol as c}from"@agimon-ai/doompi-extension-contracts/leader";function l(e){e instanceof a&&e.code===i.timeout||queueMicrotask(()=>{throw e instanceof Error?e:Error(String(e))})}function u(i){let a=0;i.on(`session_start`,(u,d)=>{let f=++a,p=o({bus:i.events,source:r,sessionId:d.sessionManager.getSessionId()});p.request(s,{contribution:{source:r,bindings:[{id:`${e}.open`,path:[n,{key:`m`,label:`mcp`,detail:t}],command:{name:`mcp`}}]},generation:String(f)},{timeoutMs:250}).then(e=>{a!==f&&p.request(c,{handleId:e.handleId},{timeoutMs:250}).catch(l)}).catch(l)}),i.on(`session_shutdown`,()=>{++a})}export{u as registerLeaderContribution};
1
+ import{LEADER_BINDING_PREFIX as e,LEADER_DETAIL as t,LEADER_GROUP as n,PACKAGE_SOURCE as r}from"./constants.mjs";import{registerDoomLeaderContribution as i}from"@agimon-ai/doompi-extension-contracts/leader";function a(a){i(a,{source:r,bindings:[{id:`${e}.open`,path:[n,{key:`m`,label:`mcp`,detail:t}],command:{name:`mcp`}}]})}export{a as registerLeaderContribution};
2
2
  //# sourceMappingURL=leader.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"leader.mjs","names":[],"sources":["../src/leader.ts"],"sourcesContent":["import { LeaderRegistrationProtocol, LeaderUnregistrationProtocol } from '@agimon-ai/doompi-extension-contracts/leader';\nimport {\n createProtocolRuntime,\n DOOM_PROTOCOL_ERROR_CODE,\n DoomProtocolError,\n} from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport {\n COMMAND_NAME,\n LEADER_BINDING_PREFIX,\n LEADER_DETAIL,\n LEADER_GROUP,\n LEADER_KEY,\n LEADER_LABEL,\n PACKAGE_SOURCE,\n} from './constants.ts';\n\nconst LEADER_PROVIDER_TIMEOUT_MS = 250;\n\nfunction handleOptionalLeaderFailure(error: unknown): void {\n if (error instanceof DoomProtocolError && error.code === DOOM_PROTOCOL_ERROR_CODE.timeout) return;\n queueMicrotask(() => {\n throw error instanceof Error ? error : new Error(String(error));\n });\n}\n\nexport function registerLeaderContribution(pi: ExtensionAPI): void {\n let generation = 0;\n\n pi.on('session_start', (_event, ctx) => {\n const activeGeneration = ++generation;\n const runtime = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n void runtime\n .request(\n LeaderRegistrationProtocol,\n {\n contribution: {\n source: PACKAGE_SOURCE,\n bindings: [\n {\n id: `${LEADER_BINDING_PREFIX}.open`,\n path: [LEADER_GROUP, { key: LEADER_KEY, label: LEADER_LABEL, detail: LEADER_DETAIL }],\n command: { name: COMMAND_NAME },\n },\n ],\n },\n generation: String(activeGeneration),\n },\n { timeoutMs: LEADER_PROVIDER_TIMEOUT_MS },\n )\n .then((registration) => {\n if (generation === activeGeneration) return;\n void runtime\n .request(\n LeaderUnregistrationProtocol,\n { handleId: registration.handleId },\n { timeoutMs: LEADER_PROVIDER_TIMEOUT_MS },\n )\n .catch(handleOptionalLeaderFailure);\n })\n .catch(handleOptionalLeaderFailure);\n });\n\n pi.on('session_shutdown', () => {\n ++generation;\n });\n}\n"],"mappings":"yXAmBA,SAAS,EAA4B,EAAsB,CACrD,aAAiB,GAAqB,EAAM,OAAS,EAAyB,SAClF,mBAAqB,CACnB,MAAM,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CAChE,CAAC,CACH,CAEA,SAAgB,EAA2B,EAAwB,CACjE,IAAI,EAAa,EAEjB,EAAG,GAAG,iBAAkB,EAAQ,IAAQ,CACtC,IAAM,EAAmB,EAAE,EACrB,EAAU,EAAsB,CACpC,IAAK,EAAG,OACR,OAAQ,EACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,EACG,QACC,EACA,CACE,aAAc,CACZ,OAAQ,EACR,SAAU,CACR,CACE,GAAI,GAAG,EAAsB,OAC7B,KAAM,CAAC,EAAc,CAAE,IAAA,IAAiB,MAAA,MAAqB,OAAQ,CAAc,CAAC,EACpF,QAAS,CAAE,KAAA,KAAmB,CAChC,CACF,CACF,EACA,WAAY,OAAO,CAAgB,CACrC,EACA,CAAE,UAAW,GAA2B,CAC1C,CAAC,CACA,KAAM,GAAiB,CAClB,IAAe,GACnB,EACG,QACC,EACA,CAAE,SAAU,EAAa,QAAS,EAClC,CAAE,UAAW,GAA2B,CAC1C,CAAC,CACA,MAAM,CAA2B,CACtC,CAAC,CAAC,CACD,MAAM,CAA2B,CACtC,CAAC,EAED,EAAG,GAAG,uBAA0B,CAC9B,EAAE,CACJ,CAAC,CACH"}
1
+ {"version":3,"file":"leader.mjs","names":[],"sources":["../src/leader.ts"],"sourcesContent":["import { registerDoomLeaderContribution } from '@agimon-ai/doompi-extension-contracts/leader';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport {\n COMMAND_NAME,\n LEADER_BINDING_PREFIX,\n LEADER_DETAIL,\n LEADER_GROUP,\n LEADER_KEY,\n LEADER_LABEL,\n PACKAGE_SOURCE,\n} from './constants.ts';\n\nexport function registerLeaderContribution(pi: ExtensionAPI): void {\n registerDoomLeaderContribution(pi, {\n source: PACKAGE_SOURCE,\n bindings: [\n {\n id: `${LEADER_BINDING_PREFIX}.open`,\n path: [LEADER_GROUP, { key: LEADER_KEY, label: LEADER_LABEL, detail: LEADER_DETAIL }],\n command: { name: COMMAND_NAME },\n },\n ],\n });\n}\n"],"mappings":"+MAYA,SAAgB,EAA2B,EAAwB,CACjE,EAA+B,EAAI,CACjC,OAAQ,EACR,SAAU,CACR,CACE,GAAI,GAAG,EAAsB,OAC7B,KAAM,CAAC,EAAc,CAAE,IAAA,IAAiB,MAAA,MAAqB,OAAQ,CAAc,CAAC,EACpF,QAAS,CAAE,KAAA,KAAmB,CAChC,CACF,CACF,CAAC,CACH"}
@@ -1,2 +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;
1
+ let e=require("node:fs"),t=require("node:crypto"),n=require("node:path"),r=require("node:os");function i(e){return(0,n.resolve)(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function a(e){if(e.length===0)throw Error(`definitionsCachePath requires at least one config path`);let a=i(e[0]),o=e.length===1?``:`.${(0,t.createHash)(`sha256`).update(e.map(e=>(0,n.resolve)(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return(0,n.join)((0,r.homedir)(),`.mcp-proxy`,`${a}${o}.definitions-cache.json`)}function o(t,n=a){if(t.length===0)return{servers:[]};try{let r=n(t),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 s=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{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),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)}};exports.McpRuntimeOwner=s,exports.definitionsCachePath=a,exports.readCachedCatalog=o;
2
2
  //# sourceMappingURL=mcpRuntime.cjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"mcpRuntime.cjs","names":["resolve","createHash","join","homedir","readFileSync"],"sources":["../../src/services/mcpRuntime.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport type { McpServerStateChange, McpToolInfo, SharedServices, TokenStore } 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\ninterface DefinitionsCacheFile {\n servers?: Record<string, { serverName: string; tools?: McpToolInfo[] }>;\n}\n\nfunction sanitizeConfigPathForFilename(configFilePath: string): string {\n const normalized = resolve(configFilePath).replace(/^([A-Z]):/, (_match, drive: string) => `${drive.toLowerCase()}:`);\n return normalized.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');\n}\n\n/** Lightweight equivalent of mcp-proxy's cache path calculation. */\nexport function definitionsCachePath(configPaths: string[]): string {\n if (configPaths.length === 0) throw new Error('definitionsCachePath requires at least one config path');\n const sanitized = sanitizeConfigPathForFilename(configPaths[0]!);\n const suffix =\n configPaths.length === 1\n ? ''\n : `.${createHash('sha256')\n .update(configPaths.map((configPath) => resolve(configPath)).join('\\0'))\n .digest('hex')\n .slice(0, 12)}`;\n return join(homedir(), '.mcp-proxy', `${sanitized}${suffix}.definitions-cache.json`);\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(\n configPaths: string[],\n cachePathFor: (paths: string[]) => string = definitionsCachePath,\n): CachedCatalog {\n if (configPaths.length === 0) return { servers: [] };\n try {\n const cachePath = cachePathFor(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 // The proxy container pulls in transports, OAuth, schema conversion, and\n // server implementations. Cached tool registration needs none of those, so\n // load the runtime only after session_start has yielded control to Pi.\n const { createProxyContainer } = await import('@agimon-ai/mcp-proxy');\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":"8FAmCA,SAAS,EAA8B,EAAgC,CAErE,OAAA,EADmBA,EAAAA,QAAAA,CAAQ,CAAc,CAAC,CAAC,QAAQ,aAAc,EAAQ,IAAkB,GAAG,EAAM,YAAY,EAAE,EAClG,CAAC,CAAC,QAAQ,oBAAqB,GAAG,CAAC,CAAC,QAAQ,WAAY,EAAE,CAC5E,CAGA,SAAgB,EAAqB,EAA+B,CAClE,GAAI,EAAY,SAAW,EAAG,MAAU,MAAM,wDAAwD,EACtG,IAAM,EAAY,EAA8B,EAAY,EAAG,EACzD,EACJ,EAAY,SAAW,EACnB,GACA,KAAA,EAAIC,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CACrB,OAAO,EAAY,IAAK,IAAA,EAAeD,EAAAA,QAAAA,CAAQ,CAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CACvE,OAAO,KAAK,CAAC,CACb,MAAM,EAAG,EAAE,IACpB,OAAA,EAAOE,EAAAA,KAAAA,EAAAA,EAAKC,EAAAA,QAAAA,CAAQ,EAAG,aAAc,GAAG,IAAY,EAAO,wBAAwB,CACrF,CAcA,SAAgB,EACd,EACA,EAA4C,EAC7B,CACf,GAAI,EAAY,SAAW,EAAG,MAAO,CAAE,QAAS,CAAC,CAAE,EACnD,GAAI,CACF,IAAM,EAAY,EAAa,CAAW,EACpC,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,OAKtC,GAAM,CAAE,wBAAyB,MAAM,OAAO,wBACxC,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"}
@@ -20,7 +20,7 @@ interface CachedCatalog {
20
20
  * since been removed simply never reaches `connected` and is dropped from the
21
21
  * active tool set when its state lands.
22
22
  */
23
- declare function readCachedCatalog(configPaths: string[]): CachedCatalog;
23
+ declare function readCachedCatalog(configPaths: string[], cachePathFor?: (paths: string[]) => string): CachedCatalog;
24
24
  //#endregion
25
25
  export { CachedCatalog, CatalogServer, readCachedCatalog };
26
26
  //# sourceMappingURL=mcpRuntime.d.cts.map
@@ -1 +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"}
1
+ {"version":3,"file":"mcpRuntime.d.cts","names":[],"sources":["../../src/services/mcpRuntime.ts"],"mappings":";;UAqBiB;EACf;EACA,OAAO;;;UAIQ;EACf,SAAS;;;;;;;;;;;;;;iBAsCK,kBACd,uBACA,gBAAe,6BACd"}
@@ -20,7 +20,7 @@ interface CachedCatalog {
20
20
  * since been removed simply never reaches `connected` and is dropped from the
21
21
  * active tool set when its state lands.
22
22
  */
23
- declare function readCachedCatalog(configPaths: string[]): CachedCatalog;
23
+ declare function readCachedCatalog(configPaths: string[], cachePathFor?: (paths: string[]) => string): CachedCatalog;
24
24
  //#endregion
25
25
  export { CachedCatalog, CatalogServer, readCachedCatalog };
26
26
  //# sourceMappingURL=mcpRuntime.d.mts.map
@@ -1 +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"}
1
+ {"version":3,"file":"mcpRuntime.d.mts","names":[],"sources":["../../src/services/mcpRuntime.ts"],"mappings":";;UAqBiB;EACf;EACA,OAAO;;;UAIQ;EACf,SAAS;;;;;;;;;;;;;;iBAsCK,kBACd,uBACA,gBAAe,6BACd"}
@@ -1,2 +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};
1
+ import{readFileSync as e}from"node:fs";import{createHash as t}from"node:crypto";import{join as n,resolve as r}from"node:path";import{homedir as i}from"node:os";function a(e){return r(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function o(e){if(e.length===0)throw Error(`definitionsCachePath requires at least one config path`);let o=a(e[0]),s=e.length===1?``:`.${t(`sha256`).update(e.map(e=>r(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return n(i(),`.mcp-proxy`,`${o}${s}.definitions-cache.json`)}function s(t,n=o){if(t.length===0)return{servers:[]};try{let r=n(t),i=JSON.parse(e(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var c=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{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),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{c as McpRuntimeOwner,o as definitionsCachePath,s as readCachedCatalog};
2
2
  //# sourceMappingURL=mcpRuntime.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"mcpRuntime.mjs","names":[],"sources":["../../src/services/mcpRuntime.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport type { McpServerStateChange, McpToolInfo, SharedServices, TokenStore } 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\ninterface DefinitionsCacheFile {\n servers?: Record<string, { serverName: string; tools?: McpToolInfo[] }>;\n}\n\nfunction sanitizeConfigPathForFilename(configFilePath: string): string {\n const normalized = resolve(configFilePath).replace(/^([A-Z]):/, (_match, drive: string) => `${drive.toLowerCase()}:`);\n return normalized.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');\n}\n\n/** Lightweight equivalent of mcp-proxy's cache path calculation. */\nexport function definitionsCachePath(configPaths: string[]): string {\n if (configPaths.length === 0) throw new Error('definitionsCachePath requires at least one config path');\n const sanitized = sanitizeConfigPathForFilename(configPaths[0]!);\n const suffix =\n configPaths.length === 1\n ? ''\n : `.${createHash('sha256')\n .update(configPaths.map((configPath) => resolve(configPath)).join('\\0'))\n .digest('hex')\n .slice(0, 12)}`;\n return join(homedir(), '.mcp-proxy', `${sanitized}${suffix}.definitions-cache.json`);\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(\n configPaths: string[],\n cachePathFor: (paths: string[]) => string = definitionsCachePath,\n): CachedCatalog {\n if (configPaths.length === 0) return { servers: [] };\n try {\n const cachePath = cachePathFor(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 // The proxy container pulls in transports, OAuth, schema conversion, and\n // server implementations. Cached tool registration needs none of those, so\n // load the runtime only after session_start has yielded control to Pi.\n const { createProxyContainer } = await import('@agimon-ai/mcp-proxy');\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":"gKAmCA,SAAS,EAA8B,EAAgC,CAErE,OADmB,EAAQ,CAAc,CAAC,CAAC,QAAQ,aAAc,EAAQ,IAAkB,GAAG,EAAM,YAAY,EAAE,EAClG,CAAC,CAAC,QAAQ,oBAAqB,GAAG,CAAC,CAAC,QAAQ,WAAY,EAAE,CAC5E,CAGA,SAAgB,EAAqB,EAA+B,CAClE,GAAI,EAAY,SAAW,EAAG,MAAU,MAAM,wDAAwD,EACtG,IAAM,EAAY,EAA8B,EAAY,EAAG,EACzD,EACJ,EAAY,SAAW,EACnB,GACA,IAAI,EAAW,QAAQ,CAAC,CACrB,OAAO,EAAY,IAAK,GAAe,EAAQ,CAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CACvE,OAAO,KAAK,CAAC,CACb,MAAM,EAAG,EAAE,IACpB,OAAO,EAAK,EAAQ,EAAG,aAAc,GAAG,IAAY,EAAO,wBAAwB,CACrF,CAcA,SAAgB,EACd,EACA,EAA4C,EAC7B,CACf,GAAI,EAAY,SAAW,EAAG,MAAO,CAAE,QAAS,CAAC,CAAE,EACnD,GAAI,CACF,IAAM,EAAY,EAAa,CAAW,EACpC,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,OAKtC,GAAM,CAAE,wBAAyB,MAAM,OAAO,wBACxC,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"}
@@ -1,2 +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;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../tools/mcpTools.cjs"),n=require("./configSources.cjs"),r=require("./directTools.cjs"),i=require("./mcpCatalog.cjs"),a=require("./mcpRuntime.cjs"),o=require("./sessionConfig.cjs"),s=require("./sharedRuntime.cjs");let c=require("node:fs");c=e.__toESM(c,1);const l=`The MCP runtime has not started yet.`;var u=class{pi;runtime;catalog;groups={shared:{configPaths:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;configuration;sharedLayerPath;allowedUpstreamTools;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.configuration=o.readSessionConfig(e.environment),this.catalog=this.createCatalog()}activate(){t.applyActiveTools(this.pi,this.catalog)}createCatalog(){return new i.McpCatalog(r.readDirectToolFilter(this.options.environment))}install(e=o.readSessionConfig(this.options.environment)){this.configuration=e,this.groups=n.buildMcpConfigGroups(e),this.sharedLayerPath=this.groups.shared.configPaths.length>0?s.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(a.readCachedCatalog(this.configPaths()),r),t.registerNewTools(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n},r]=await Promise.all([Promise.resolve().then(()=>require("./keyringTokenStore.cjs")),Promise.resolve().then(()=>require("./mcpRuntime.cjs")),Promise.resolve().then(()=>require("./sharedRuntime.cjs"))]);if(e!==this.lifecycleGeneration)return;let i=this.runtime??=new n;this.resourceCache.clear(),r.maintainSharedResources(this.configuration.stagingDirectory);let a=await r.ensureSharedRuntime({repoRoot:this.configuration.repoRoot,configPaths:this.groups.shared.configPaths});if(e!==this.lifecycleGeneration)return;this.allowedUpstreamTools=r.resolveAllowedUpstreamTools(this.groups.shared.configPaths,this.groups.shared.allowedUpstreams);let o=this.configPaths(a!==void 0);if(this.groups.sessionLocal.serverNames.length===0&&!a)return;let s=this.options.tokenStore??await t();await i.start({configPaths:o,tokenStore:s,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await i.dispose()}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||c.default.existsSync(t));return[...this.groups.sessionLocal.configPaths,...n?[t]:[]]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(l);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,r=n?.getServices();if(!n||!r)throw Error(l);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}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(){++this.lifecycleGeneration,await this.runtime?.dispose()}onServerStateChange(e){e.state!==`connecting`&&this.authorizationUrls.delete(e.serverName);let n=this.runtime,r=n?.getServices();(e.state===`connected`&&r?r.clientManager.ensureConnected(e.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(i=>{if(!n?.isCurrent(r))return;let a=this.catalog.applyStateChange(e,this.permittedTools(e.serverName,i));t.registerNewTools(this.pi,this.clientManager,a),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=u;
2
2
  //# sourceMappingURL=mcpSession.cjs.map
@@ -1 +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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"}
1
+ {"version":3,"file":"mcpSession.cjs","names":["readSessionConfig","McpCatalog","readDirectToolFilter","buildMcpConfigGroups","sharedLayerPath","PROXY_SERVER_NAME","readCachedCatalog","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 { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { readSessionConfig, type McpSessionConfig } from './sessionConfig.ts';\nimport { sharedLayerPath } 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 /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\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 /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n 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 const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }, sharedRuntime] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n import('./sharedRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n const runtime = (this.runtime ??= new McpRuntimeOwner());\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 sharedRuntime.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 sharedRuntime.ensureSharedRuntime({\n repoRoot: this.configuration.repoRoot,\n configPaths: this.groups.shared.configPaths,\n });\n if (generation !== this.lifecycleGeneration) return;\n this.allowedUpstreamTools = sharedRuntime.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 runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\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 runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !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 (!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 ++this.lifecycleGeneration;\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 runtime = this.runtime;\n const services = 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 (!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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"}
@@ -1,2 +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};
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{readDirectToolFilter as i}from"./directTools.mjs";import{McpCatalog as a}from"./mcpCatalog.mjs";import{readCachedCatalog as o}from"./mcpRuntime.mjs";import{readSessionConfig as s}from"./sessionConfig.mjs";import{sharedLayerPath as c}from"./sharedRuntime.mjs";import l from"node:fs";const u=`The MCP runtime has not started yet.`;var d=class{pi;runtime;catalog;groups={shared:{configPaths:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;configuration;sharedLayerPath;allowedUpstreamTools;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.configuration=s(e.environment),this.catalog=this.createCatalog()}activate(){e(this.pi,this.catalog)}createCatalog(){return new a(i(this.options.environment))}install(e=s(this.options.environment)){this.configuration=e,this.groups=r(e),this.sharedLayerPath=this.groups.shared.configPaths.length>0?c(e.repoRoot):void 0,this.catalog=this.createCatalog();let i=[...this.groups.sessionLocal.serverNames,...this.sharedLayerPath?[n]:[]];return this.catalog.seed(o(this.configPaths()),i),t(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n},r]=await Promise.all([import(`./keyringTokenStore.mjs`),import(`./mcpRuntime.mjs`),import(`./sharedRuntime.mjs`)]);if(e!==this.lifecycleGeneration)return;let i=this.runtime??=new n;this.resourceCache.clear(),r.maintainSharedResources(this.configuration.stagingDirectory);let a=await r.ensureSharedRuntime({repoRoot:this.configuration.repoRoot,configPaths:this.groups.shared.configPaths});if(e!==this.lifecycleGeneration)return;this.allowedUpstreamTools=r.resolveAllowedUpstreamTools(this.groups.shared.configPaths,this.groups.shared.allowedUpstreams);let o=this.configPaths(a!==void 0);if(this.groups.sessionLocal.serverNames.length===0&&!a)return;let s=this.options.tokenStore??await t();await i.start({configPaths:o,tokenStore:s,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await i.dispose()}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.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(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,r=n?.getServices();if(!n||!r)throw Error(u);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}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(){++this.lifecycleGeneration,await this.runtime?.dispose()}onServerStateChange(n){n.state!==`connecting`&&this.authorizationUrls.delete(n.serverName);let r=this.runtime,i=r?.getServices();(n.state===`connected`&&i?i.clientManager.ensureConnected(n.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(a=>{if(!r?.isCurrent(i))return;let o=this.catalog.applyStateChange(n,this.permittedTools(n.serverName,a));t(this.pi,this.clientManager,o),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{d as McpSession};
2
2
  //# sourceMappingURL=mcpSession.mjs.map
@@ -1 +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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"}
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 { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { readSessionConfig, type McpSessionConfig } from './sessionConfig.ts';\nimport { sharedLayerPath } 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 /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\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 /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n 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 const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }, sharedRuntime] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n import('./sharedRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n const runtime = (this.runtime ??= new McpRuntimeOwner());\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 sharedRuntime.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 sharedRuntime.ensureSharedRuntime({\n repoRoot: this.configuration.repoRoot,\n configPaths: this.groups.shared.configPaths,\n });\n if (generation !== this.lifecycleGeneration) return;\n this.allowedUpstreamTools = sharedRuntime.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 runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\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 runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !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 (!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 ++this.lifecycleGeneration;\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 runtime = this.runtime;\n const services = 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 (!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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"}
@@ -1,2 +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;
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("node:os");o=e.__toESM(o,1);const s=/^(?:shared|mcp)-[0-9a-f]{16}\.json$/;function c(e){let t=(0,i.createHash)(`sha256`).update(e).digest(`hex`).slice(0,16);return a.default.join(o.default.tmpdir(),`doom-mcp`,`shared-${t}.json`)}async function l(e){if(e.configPaths.length!==0)try{let{findHealthyExistingRuntime:n,resolveWorkspaceRoot:i}=await import(`@agimon-ai/mcp-proxy`),o=i(e.repoRoot),s=await n(o),l=s??await u(e.configPaths),d=`http://${l.host}:${l.port}/mcp`,f=c(o);return r.default.mkdirSync(a.default.dirname(f),{mode:448,recursive:!0}),r.default.writeFileSync(f,`${JSON.stringify({mcpServers:{[t.PROXY_SERVER_NAME]:{type:`http`,url:d}}},null,2)}\n`,{mode:384}),{configPath:f,endpoint:d,reusedExistingRuntime:s!==null}}catch{return}}async function u(e){process.env.MCP_PROXY_IDLE_SHUTDOWN_MINUTES??=`45`;let{prestartHttpRuntime:t}=await import(`@agimon-ai/mcp-proxy`),n=await t({host:`localhost`,config:e,cache:!0,clearDefinitionsCache:!1,proxyMode:`flat`,runtimeLifecycle:`manual`});return{host:n.host,port:n.port}}async function d(e,t=Date.now()){f(e,t),await p()}function f(e,t){let n;try{n=r.default.readdirSync(e)}catch{return}for(let i of n){if(!s.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 p(){try{await(await import(`@agimon-ai/mcp-proxy`)).cleanupStaleRegistries?.()}catch{}}function m(e,t,r){if(!t||t.length===0)return;let i=new Set(t),a=n.readCachedCatalog(e,r),o=new Set,s=h(a.servers);for(let e of a.servers)if(i.has(e.name))for(let t of e.tools)o.add(s.has(t.name)?`${e.name}__${t.name}`:t.name);return o}function h(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=l,exports.maintainSharedResources=d,exports.resolveAllowedUpstreamTools=m,exports.sharedLayerPath=c;
2
2
  //# sourceMappingURL=sharedRuntime.cjs.map
@@ -1 +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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"}
1
+ {"version":3,"file":"sharedRuntime.cjs","names":["createHash","path","os","PROXY_SERVER_NAME","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 { 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 { findHealthyExistingRuntime, resolveWorkspaceRoot } = await import('@agimon-ai/mcp-proxy');\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 { prestartHttpRuntime } = await import('@agimon-ai/mcp-proxy');\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 cachePathFor?: (paths: string[]) => string,\n): Set<string> | undefined {\n if (!allowedUpstreams || allowedUpstreams.length === 0) return undefined;\n const allowed = new Set(allowedUpstreams);\n const catalog = readCachedCatalog(sharedConfigPaths, cachePathFor);\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":"6QAOA,MAiBM,EAAuB,sCAuB7B,SAAgB,EAAgB,EAA+B,CAC7D,IAAM,GAAA,EAASA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAa,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EACnF,OAAOC,EAAAA,QAAK,KAAKC,EAAAA,QAAG,OAAO,EAAG,WAAY,UAAU,EAAO,MAAM,CACnE,CAcA,eAAsB,EAAoB,EAAwE,CAC5G,KAAQ,YAAY,SAAW,EAEnC,GAAI,CACF,GAAM,CAAE,6BAA4B,wBAAyB,MAAM,OAAO,wBACpE,EAAgB,EAAqB,EAAQ,QAAQ,EACrD,EAAW,MAAM,EAA2B,CAAa,EACzD,EAAU,GAAa,MAAM,EAAa,EAAQ,WAAW,EAC7D,EAAW,UAAU,EAAQ,KAAK,GAAG,EAAQ,KAAK,MAElD,EAAa,EAAgB,CAAa,EAQhD,OAPA,EAAA,QAAG,UAAUD,EAAAA,QAAK,QAAQ,CAAU,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,EAAA,QAAG,cACD,EACA,GAAG,KAAK,UAAU,CAAE,WAAY,EAAGE,EAAAA,mBAAoB,CAAE,KAAM,OAAQ,IAAK,CAAS,CAAE,CAAE,EAAG,KAAM,CAAC,EAAE,IACrG,CAAE,KAAM,GAAkB,CAC5B,EAEO,CAAE,aAAY,WAAU,sBAAuB,IAAa,IAAK,CAC1E,MAAQ,CACN,MACF,CACF,CAEA,eAAe,EAAa,EAAgE,CAI1F,QAAQ,IAAI,kCAAuB,KACnC,GAAM,CAAE,uBAAwB,MAAM,OAAO,wBACvC,EAAS,MAAM,EAAoB,CACvC,KAAM,YACN,OAAQ,EACR,MAAO,GACP,sBAAuB,GACvB,UAAW,OAIX,iBAAkB,QACpB,CAAC,EACD,MAAO,CAAE,KAAM,EAAO,KAAM,KAAM,EAAO,IAAK,CAChD,CAWA,eAAsB,EAAwB,EAA0B,EAAc,KAAK,IAAI,EAAkB,CAC/G,EAAsB,EAAkB,CAAG,EAC3C,MAAM,EAAuB,CAC/B,CAEA,SAAS,EAAsB,EAA0B,EAAmB,CAC1E,IAAI,EACJ,GAAI,CACF,EAAUC,EAAAA,QAAG,YAAY,CAAgB,CAC3C,MAAQ,CAEN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAqB,KAAK,CAAK,EAAG,SACvC,IAAM,EAASH,EAAAA,QAAK,KAAK,EAAkB,CAAK,EAChD,GAAI,CACE,EAAMG,EAAAA,QAAG,SAAS,CAAM,CAAC,CAAC,QAAU,QAAgB,EAAA,QAAG,WAAW,CAAM,CAC9E,MAAQ,CAER,CACF,CACF,CAEA,eAAe,GAAwC,CACrD,GAAI,CAGF,MAAM,MADe,OAAO,wBAAA,CAChB,yBAAyB,CACvC,MAAQ,CAER,CACF,CAYA,SAAgB,EACd,EACA,EACA,EACyB,CACzB,GAAI,CAAC,GAAoB,EAAiB,SAAW,EAAG,OACxD,IAAM,EAAU,IAAI,IAAI,CAAgB,EAClC,EAAUC,EAAAA,kBAAkB,EAAmB,CAAY,EAC3D,EAAQ,IAAI,IACZ,EAAW,EAAsB,EAAQ,OAAO,EACtD,IAAK,IAAM,KAAU,EAAQ,QACtB,KAAQ,IAAI,EAAO,IAAI,EAC5B,IAAK,IAAM,KAAQ,EAAO,MAExB,EAAM,IAAI,EAAS,IAAI,EAAK,IAAI,EAAI,GAAG,EAAO,KAAK,IAAI,EAAK,OAAS,EAAK,IAAI,EAGlF,OAAO,CACT,CAEA,SAAS,EAAsB,EAAiF,CAC9G,IAAM,EAAO,IAAI,IACX,EAAW,IAAI,IACrB,IAAK,IAAM,KAAU,EACnB,IAAK,IAAM,KAAQ,EAAO,MACpB,EAAK,IAAI,EAAK,IAAI,GAAG,EAAS,IAAI,EAAK,IAAI,EAC/C,EAAK,IAAI,EAAK,IAAI,EAGtB,OAAO,CACT"}
@@ -1,2 +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};
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 a from"node:os";const o=/^(?:shared|mcp)-[0-9a-f]{16}\.json$/;function s(e){let t=r(`sha256`).update(e).digest(`hex`).slice(0,16);return i.join(a.tmpdir(),`doom-mcp`,`shared-${t}.json`)}async function c(t){if(t.configPaths.length!==0)try{let{findHealthyExistingRuntime:r,resolveWorkspaceRoot:a}=await import(`@agimon-ai/mcp-proxy`),o=a(t.repoRoot),c=await r(o),u=c??await l(t.configPaths),d=`http://${u.host}:${u.port}/mcp`,f=s(o);return n.mkdirSync(i.dirname(f),{mode:448,recursive:!0}),n.writeFileSync(f,`${JSON.stringify({mcpServers:{[e]:{type:`http`,url:d}}},null,2)}\n`,{mode:384}),{configPath:f,endpoint:d,reusedExistingRuntime:c!==null}}catch{return}}async function l(e){process.env.MCP_PROXY_IDLE_SHUTDOWN_MINUTES??=`45`;let{prestartHttpRuntime:t}=await import(`@agimon-ai/mcp-proxy`),n=await t({host:`localhost`,config:e,cache:!0,clearDefinitionsCache:!1,proxyMode:`flat`,runtimeLifecycle:`manual`});return{host:n.host,port:n.port}}async function u(e,t=Date.now()){d(e,t),await f()}function d(e,t){let r;try{r=n.readdirSync(e)}catch{return}for(let a of r){if(!o.test(a))continue;let r=i.join(e,a);try{t-n.statSync(r).mtimeMs>6048e5&&n.unlinkSync(r)}catch{}}}async function f(){try{await(await import(`@agimon-ai/mcp-proxy`)).cleanupStaleRegistries?.()}catch{}}function p(e,n,r){if(!n||n.length===0)return;let i=new Set(n),a=t(e,r),o=new Set,s=m(a.servers);for(let e of a.servers)if(i.has(e.name))for(let t of e.tools)o.add(s.has(t.name)?`${e.name}__${t.name}`:t.name);return o}function m(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{c as ensureSharedRuntime,u as maintainSharedResources,p as resolveAllowedUpstreamTools,s as sharedLayerPath};
2
2
  //# sourceMappingURL=sharedRuntime.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sharedRuntime.mjs","names":[],"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":"kVAQA,MAiBM,EAAuB,sCAuB7B,SAAgB,EAAgB,EAA+B,CAC7D,IAAM,EAAS,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAa,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EACnF,OAAO,EAAK,KAAK,EAAG,OAAO,EAAG,WAAY,UAAU,EAAO,MAAM,CACnE,CAcA,eAAsB,EAAoB,EAAwE,CAC5G,KAAQ,YAAY,SAAW,EAEnC,GAAI,CACF,IAAM,EAAgB,EAAqB,EAAQ,QAAQ,EACrD,EAAW,MAAM,EAA2B,CAAa,EACzD,EAAU,GAAa,MAAM,EAAa,EAAQ,WAAW,EAC7D,EAAW,UAAU,EAAQ,KAAK,GAAG,EAAQ,KAAK,MAElD,EAAa,EAAgB,CAAa,EAQhD,OAPA,EAAG,UAAU,EAAK,QAAQ,CAAU,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,EAAG,cACD,EACA,GAAG,KAAK,UAAU,CAAE,WAAY,EAAG,GAAoB,CAAE,KAAM,OAAQ,IAAK,CAAS,CAAE,CAAE,EAAG,KAAM,CAAC,EAAE,IACrG,CAAE,KAAM,GAAkB,CAC5B,EAEO,CAAE,aAAY,WAAU,sBAAuB,IAAa,IAAK,CAC1E,MAAQ,CACN,MACF,CACF,CAEA,eAAe,EAAa,EAAgE,CAI1F,QAAQ,IAAI,kCAAuB,KACnC,IAAM,EAAS,MAAM,EAAoB,CACvC,KAAM,YACN,OAAQ,EACR,MAAO,GACP,sBAAuB,GACvB,UAAW,OAIX,iBAAkB,QACpB,CAAC,EACD,MAAO,CAAE,KAAM,EAAO,KAAM,KAAM,EAAO,IAAK,CAChD,CAWA,eAAsB,EAAwB,EAA0B,EAAc,KAAK,IAAI,EAAkB,CAC/G,EAAsB,EAAkB,CAAG,EAC3C,MAAM,EAAuB,CAC/B,CAEA,SAAS,EAAsB,EAA0B,EAAmB,CAC1E,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,CAAgB,CAC3C,MAAQ,CAEN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAqB,KAAK,CAAK,EAAG,SACvC,IAAM,EAAS,EAAK,KAAK,EAAkB,CAAK,EAChD,GAAI,CACE,EAAM,EAAG,SAAS,CAAM,CAAC,CAAC,QAAU,QAAgB,EAAG,WAAW,CAAM,CAC9E,MAAQ,CAER,CACF,CACF,CAEA,eAAe,GAAwC,CACrD,GAAI,CAGF,MAAM,MADe,OAAO,wBAAA,CAChB,yBAAyB,CACvC,MAAQ,CAER,CACF,CAYA,SAAgB,EACd,EACA,EACyB,CACzB,GAAI,CAAC,GAAoB,EAAiB,SAAW,EAAG,OACxD,IAAM,EAAU,IAAI,IAAI,CAAgB,EAClC,EAAU,EAAkB,CAAiB,EAC7C,EAAQ,IAAI,IACZ,EAAW,EAAsB,EAAQ,OAAO,EACtD,IAAK,IAAM,KAAU,EAAQ,QACtB,KAAQ,IAAI,EAAO,IAAI,EAC5B,IAAK,IAAM,KAAQ,EAAO,MAExB,EAAM,IAAI,EAAS,IAAI,EAAK,IAAI,EAAI,GAAG,EAAO,KAAK,IAAI,EAAK,OAAS,EAAK,IAAI,EAGlF,OAAO,CACT,CAEA,SAAS,EAAsB,EAAiF,CAC9G,IAAM,EAAO,IAAI,IACX,EAAW,IAAI,IACrB,IAAK,IAAM,KAAU,EACnB,IAAK,IAAM,KAAQ,EAAO,MACpB,EAAK,IAAI,EAAK,IAAI,GAAG,EAAS,IAAI,EAAK,IAAI,EAC/C,EAAK,IAAI,EAAK,IAAI,EAGtB,OAAO,CACT"}
1
+ {"version":3,"file":"sharedRuntime.mjs","names":[],"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 { 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 { findHealthyExistingRuntime, resolveWorkspaceRoot } = await import('@agimon-ai/mcp-proxy');\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 { prestartHttpRuntime } = await import('@agimon-ai/mcp-proxy');\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 cachePathFor?: (paths: string[]) => string,\n): Set<string> | undefined {\n if (!allowedUpstreams || allowedUpstreams.length === 0) return undefined;\n const allowed = new Set(allowedUpstreams);\n const catalog = readCachedCatalog(sharedConfigPaths, cachePathFor);\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":"6NAOA,MAiBM,EAAuB,sCAuB7B,SAAgB,EAAgB,EAA+B,CAC7D,IAAM,EAAS,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAa,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EACnF,OAAO,EAAK,KAAK,EAAG,OAAO,EAAG,WAAY,UAAU,EAAO,MAAM,CACnE,CAcA,eAAsB,EAAoB,EAAwE,CAC5G,KAAQ,YAAY,SAAW,EAEnC,GAAI,CACF,GAAM,CAAE,6BAA4B,wBAAyB,MAAM,OAAO,wBACpE,EAAgB,EAAqB,EAAQ,QAAQ,EACrD,EAAW,MAAM,EAA2B,CAAa,EACzD,EAAU,GAAa,MAAM,EAAa,EAAQ,WAAW,EAC7D,EAAW,UAAU,EAAQ,KAAK,GAAG,EAAQ,KAAK,MAElD,EAAa,EAAgB,CAAa,EAQhD,OAPA,EAAG,UAAU,EAAK,QAAQ,CAAU,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,EAAG,cACD,EACA,GAAG,KAAK,UAAU,CAAE,WAAY,EAAG,GAAoB,CAAE,KAAM,OAAQ,IAAK,CAAS,CAAE,CAAE,EAAG,KAAM,CAAC,EAAE,IACrG,CAAE,KAAM,GAAkB,CAC5B,EAEO,CAAE,aAAY,WAAU,sBAAuB,IAAa,IAAK,CAC1E,MAAQ,CACN,MACF,CACF,CAEA,eAAe,EAAa,EAAgE,CAI1F,QAAQ,IAAI,kCAAuB,KACnC,GAAM,CAAE,uBAAwB,MAAM,OAAO,wBACvC,EAAS,MAAM,EAAoB,CACvC,KAAM,YACN,OAAQ,EACR,MAAO,GACP,sBAAuB,GACvB,UAAW,OAIX,iBAAkB,QACpB,CAAC,EACD,MAAO,CAAE,KAAM,EAAO,KAAM,KAAM,EAAO,IAAK,CAChD,CAWA,eAAsB,EAAwB,EAA0B,EAAc,KAAK,IAAI,EAAkB,CAC/G,EAAsB,EAAkB,CAAG,EAC3C,MAAM,EAAuB,CAC/B,CAEA,SAAS,EAAsB,EAA0B,EAAmB,CAC1E,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,CAAgB,CAC3C,MAAQ,CAEN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAqB,KAAK,CAAK,EAAG,SACvC,IAAM,EAAS,EAAK,KAAK,EAAkB,CAAK,EAChD,GAAI,CACE,EAAM,EAAG,SAAS,CAAM,CAAC,CAAC,QAAU,QAAgB,EAAG,WAAW,CAAM,CAC9E,MAAQ,CAER,CACF,CACF,CAEA,eAAe,GAAwC,CACrD,GAAI,CAGF,MAAM,MADe,OAAO,wBAAA,CAChB,yBAAyB,CACvC,MAAQ,CAER,CACF,CAYA,SAAgB,EACd,EACA,EACA,EACyB,CACzB,GAAI,CAAC,GAAoB,EAAiB,SAAW,EAAG,OACxD,IAAM,EAAU,IAAI,IAAI,CAAgB,EAClC,EAAU,EAAkB,EAAmB,CAAY,EAC3D,EAAQ,IAAI,IACZ,EAAW,EAAsB,EAAQ,OAAO,EACtD,IAAK,IAAM,KAAU,EAAQ,QACtB,KAAQ,IAAI,EAAO,IAAI,EAC5B,IAAK,IAAM,KAAQ,EAAO,MAExB,EAAM,IAAI,EAAS,IAAI,EAAK,IAAI,EAAI,GAAG,EAAO,KAAK,IAAI,EAAK,OAAS,EAAK,IAAI,EAGlF,OAAO,CACT,CAEA,SAAS,EAAsB,EAAiF,CAC9G,IAAM,EAAO,IAAI,IACX,EAAW,IAAI,IACrB,IAAK,IAAM,KAAU,EACnB,IAAK,IAAM,KAAQ,EAAO,MACpB,EAAK,IAAI,EAAK,IAAI,GAAG,EAAS,IAAI,EAAK,IAAI,EAC/C,EAAK,IAAI,EAAK,IAAI,EAGtB,OAAO,CACT"}
@@ -1,3 +1,3 @@
1
- let e=require("typebox");const t={type:`object`,properties:{}};function n(e){return(e.content??[]).filter(e=>e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(`
2
- `).trim()}function r(e,t){let r=n(t)||`No output.`;if(t.isError)throw Error(r);return{content:[{type:`text`,text:r}],details:{server:e.serverName,tool:e.toolName}}}function i(n,i,a){n.registerTool({name:a.piName,label:`${a.serverName}: ${a.toolName}`,description:a.description??`${a.toolName} on the ${a.serverName} MCP server.`,parameters:e.Type.Unsafe(Object.keys(a.inputSchema).length>0?a.inputSchema:t),async execute(e,t){let n=i();if(!n)throw Error(`The MCP runtime is not ready, so ${a.serverName} cannot be reached yet.`);let o=await n.ensureConnected(a.serverName),s=n.getServerRequestTimeout(a.serverName);return r(a,await o.callTool(a.toolName,t??{},s===void 0?void 0:{timeout:s}))}})}function a(e,t,n){for(let r of n)i(e,t,r)}function o(e,t){let n=new Set(t.allTools().map(e=>e.piName)),r=e.getActiveTools().filter(e=>!n.has(e));e.setActiveTools([...r,...t.activeToolNames()])}exports.applyActiveTools=o,exports.registerMcpTool=i,exports.registerNewTools=a,exports.toAgentToolResult=r;
1
+ const e={type:`object`,properties:{}};function t(e){return(e.content??[]).filter(e=>e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(`
2
+ `).trim()}function n(e,n){let r=t(n)||`No output.`;if(n.isError)throw Error(r);return{content:[{type:`text`,text:r}],details:{server:e.serverName,tool:e.toolName}}}function r(t,r,i){t.registerTool({name:i.piName,label:`${i.serverName}: ${i.toolName}`,description:i.description??`${i.toolName} on the ${i.serverName} MCP server.`,parameters:Object.keys(i.inputSchema).length>0?i.inputSchema:e,async execute(e,t){let a=r();if(!a)throw Error(`The MCP runtime is not ready, so ${i.serverName} cannot be reached yet.`);let o=await a.ensureConnected(i.serverName),s=a.getServerRequestTimeout(i.serverName);return n(i,await o.callTool(i.toolName,t??{},s===void 0?void 0:{timeout:s}))}})}function i(e,t,n){for(let i of n)r(e,t,i)}function a(e,t){let n=new Set(t.allTools().map(e=>e.piName)),r=e.getActiveTools().filter(e=>!n.has(e));e.setActiveTools([...r,...t.activeToolNames()])}exports.applyActiveTools=a,exports.registerMcpTool=r,exports.registerNewTools=i,exports.toAgentToolResult=n;
3
3
  //# sourceMappingURL=mcpTools.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcpTools.cjs","names":["Type"],"sources":["../../src/tools/mcpTools.ts"],"sourcesContent":["import type { McpClientManagerService } from '@agimon-ai/mcp-proxy';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolResult, ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { Type } from 'typebox';\nimport type { CatalogTool, McpCatalog } from '../services/mcpCatalog.ts';\n\n/** Pi requires a schema; a downstream tool that declares none takes any object. */\nconst ANY_OBJECT_SCHEMA = { type: 'object', properties: {} };\n\ninterface McpToolDetails {\n server: string;\n tool: string;\n [key: string]: unknown;\n}\n\n/** Reads the text of a downstream result, dropping blocks Pi cannot render. */\nfunction resultText(result: CallToolResult): string {\n const blocks = (result.content ?? []) as Array<{ type: string; text?: string }>;\n return blocks\n .filter((block) => block.type === 'text' && typeof block.text === 'string')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n}\n\n/**\n * Turns a downstream result into a Pi tool result.\n *\n * `isError` is raised rather than returned: Pi shows a thrown message as a failed\n * call, and returning the text would present a server-side error as a success.\n */\nexport function toAgentToolResult(tool: CatalogTool, result: CallToolResult): AgentToolResult<McpToolDetails> {\n const text = resultText(result) || 'No output.';\n if (result.isError) throw new Error(text);\n return { content: [{ type: 'text', text }], details: { server: tool.serverName, tool: tool.toolName } };\n}\n\n/**\n * Supplies the live client manager, or nothing before the runtime has started.\n *\n * A getter rather than the manager itself: tools are registered from the cached\n * catalog before any container exists, and the same registration has to reach\n * whichever container is live when the tool is finally called.\n */\nexport type ClientManagerSource = () => McpClientManagerService | undefined;\n\n/**\n * Registers one downstream tool with Pi.\n *\n * Registration is permanent for the session: Pi 0.84 has no `unregisterTool`, so\n * visibility is controlled through the active list instead. The connection is\n * resolved at execution time rather than captured, so a server that reconnects\n * underneath keeps working.\n */\nexport function registerMcpTool(pi: ExtensionAPI, clientManagerSource: ClientManagerSource, tool: CatalogTool): void {\n pi.registerTool({\n name: tool.piName,\n label: `${tool.serverName}: ${tool.toolName}`,\n description: tool.description ?? `${tool.toolName} on the ${tool.serverName} MCP server.`,\n // The schema arrives as JSON Schema at runtime, so it is wrapped rather than\n // built: TypeBox validates the shape without it having to be declared here.\n parameters: Type.Unsafe<Record<string, unknown>>(\n Object.keys(tool.inputSchema).length > 0 ? tool.inputSchema : ANY_OBJECT_SCHEMA,\n ),\n async execute(_toolCallId, params) {\n const clientManager = clientManagerSource();\n if (!clientManager) {\n throw new Error(`The MCP runtime is not ready, so ${tool.serverName} cannot be reached yet.`);\n }\n const connection = await clientManager.ensureConnected(tool.serverName);\n const timeout = clientManager.getServerRequestTimeout(tool.serverName);\n const result = await connection.callTool(\n tool.toolName,\n (params ?? {}) as Record<string, unknown>,\n timeout === undefined ? undefined : { timeout },\n );\n return toAgentToolResult(tool, result);\n },\n });\n}\n\n/**\n * Registers tools that are new to Pi.\n *\n * Separate from the active list because Pi allows `registerTool` during extension\n * loading but not `getActiveTools`/`setActiveTools`, which throw until the runtime\n * is bound. This half is therefore safe to run at install; `applyActiveTools` is\n * not, and waits for the session to start.\n */\nexport function registerNewTools(\n pi: ExtensionAPI,\n clientManagerSource: ClientManagerSource,\n newTools: readonly CatalogTool[],\n): void {\n for (const tool of newTools) registerMcpTool(pi, clientManagerSource, tool);\n}\n\n/**\n * Recomputes which of this extension's tools Pi exposes.\n *\n * Rebuilt from scratch every time, so a server that failed drops out and one that\n * reconnected comes back. Only valid once the session has started.\n */\nexport function applyActiveTools(pi: ExtensionAPI, catalog: McpCatalog): void {\n const mcpNames = new Set(catalog.allTools().map((tool) => tool.piName));\n // Tools this extension does not own are carried through untouched: the active\n // list is global, and rebuilding it from MCP alone would hide everything else.\n const others = pi.getActiveTools().filter((name) => !mcpNames.has(name));\n pi.setActiveTools([...others, ...catalog.activeToolNames()]);\n}\n"],"mappings":"yBAOA,MAAM,EAAoB,CAAE,KAAM,SAAU,WAAY,CAAC,CAAE,EAS3D,SAAS,EAAW,EAAgC,CAElD,OADgB,EAAO,SAAW,CAAC,EAAA,CAEhC,OAAQ,GAAU,EAAM,OAAS,QAAU,OAAO,EAAM,MAAS,QAAQ,CAAC,CAC1E,IAAK,GAAU,EAAM,IAAI,CAAC,CAC1B,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CAQA,SAAgB,EAAkB,EAAmB,EAAyD,CAC5G,IAAM,EAAO,EAAW,CAAM,GAAK,aACnC,GAAI,EAAO,QAAS,MAAU,MAAM,CAAI,EACxC,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,QAAS,CAAE,OAAQ,EAAK,WAAY,KAAM,EAAK,QAAS,CAAE,CACxG,CAmBA,SAAgB,EAAgB,EAAkB,EAA0C,EAAyB,CACnH,EAAG,aAAa,CACd,KAAM,EAAK,OACX,MAAO,GAAG,EAAK,WAAW,IAAI,EAAK,WACnC,YAAa,EAAK,aAAe,GAAG,EAAK,SAAS,UAAU,EAAK,WAAW,cAG5E,WAAYA,EAAAA,KAAK,OACf,OAAO,KAAK,EAAK,WAAW,CAAC,CAAC,OAAS,EAAI,EAAK,YAAc,CAChE,EACA,MAAM,QAAQ,EAAa,EAAQ,CACjC,IAAM,EAAgB,EAAoB,EAC1C,GAAI,CAAC,EACH,MAAU,MAAM,oCAAoC,EAAK,WAAW,wBAAwB,EAE9F,IAAM,EAAa,MAAM,EAAc,gBAAgB,EAAK,UAAU,EAChE,EAAU,EAAc,wBAAwB,EAAK,UAAU,EAMrE,OAAO,EAAkB,EAAM,MALV,EAAW,SAC9B,EAAK,SACJ,GAAU,CAAC,EACZ,IAAY,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,CAChD,CACqC,CACvC,CACF,CAAC,CACH,CAUA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAQ,EAAU,EAAgB,EAAI,EAAqB,CAAI,CAC5E,CAQA,SAAgB,EAAiB,EAAkB,EAA2B,CAC5E,IAAM,EAAW,IAAI,IAAI,EAAQ,SAAS,CAAC,CAAC,IAAK,GAAS,EAAK,MAAM,CAAC,EAGhE,EAAS,EAAG,eAAe,CAAC,CAAC,OAAQ,GAAS,CAAC,EAAS,IAAI,CAAI,CAAC,EACvE,EAAG,eAAe,CAAC,GAAG,EAAQ,GAAG,EAAQ,gBAAgB,CAAC,CAAC,CAC7D"}
1
+ {"version":3,"file":"mcpTools.cjs","names":[],"sources":["../../src/tools/mcpTools.ts"],"sourcesContent":["import type { McpClientManagerService } from '@agimon-ai/mcp-proxy';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolResult, ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { TSchema } from 'typebox';\nimport type { CatalogTool, McpCatalog } from '../services/mcpCatalog.ts';\n\n/** Pi requires a schema; a downstream tool that declares none takes any object. */\nconst ANY_OBJECT_SCHEMA = { type: 'object', properties: {} };\n\ninterface McpToolDetails {\n server: string;\n tool: string;\n [key: string]: unknown;\n}\n\n/** Reads the text of a downstream result, dropping blocks Pi cannot render. */\nfunction resultText(result: CallToolResult): string {\n const blocks = (result.content ?? []) as Array<{ type: string; text?: string }>;\n return blocks\n .filter((block) => block.type === 'text' && typeof block.text === 'string')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n}\n\n/**\n * Turns a downstream result into a Pi tool result.\n *\n * `isError` is raised rather than returned: Pi shows a thrown message as a failed\n * call, and returning the text would present a server-side error as a success.\n */\nexport function toAgentToolResult(tool: CatalogTool, result: CallToolResult): AgentToolResult<McpToolDetails> {\n const text = resultText(result) || 'No output.';\n if (result.isError) throw new Error(text);\n return { content: [{ type: 'text', text }], details: { server: tool.serverName, tool: tool.toolName } };\n}\n\n/**\n * Supplies the live client manager, or nothing before the runtime has started.\n *\n * A getter rather than the manager itself: tools are registered from the cached\n * catalog before any container exists, and the same registration has to reach\n * whichever container is live when the tool is finally called.\n */\nexport type ClientManagerSource = () => McpClientManagerService | undefined;\n\n/**\n * Registers one downstream tool with Pi.\n *\n * Registration is permanent for the session: Pi 0.84 has no `unregisterTool`, so\n * visibility is controlled through the active list instead. The connection is\n * resolved at execution time rather than captured, so a server that reconnects\n * underneath keeps working.\n */\nexport function registerMcpTool(pi: ExtensionAPI, clientManagerSource: ClientManagerSource, tool: CatalogTool): void {\n pi.registerTool({\n name: tool.piName,\n label: `${tool.serverName}: ${tool.toolName}`,\n description: tool.description ?? `${tool.toolName} on the ${tool.serverName} MCP server.`,\n // The schema arrives as JSON Schema at runtime, so it is wrapped rather than\n // built: TypeBox validates the shape without it having to be declared here.\n // Type.Unsafe only brands this runtime JSON Schema object. A type cast keeps\n // cached stub registration from evaluating the whole TypeBox package.\n parameters: (Object.keys(tool.inputSchema).length > 0 ? tool.inputSchema : ANY_OBJECT_SCHEMA) as TSchema,\n async execute(_toolCallId, params) {\n const clientManager = clientManagerSource();\n if (!clientManager) {\n throw new Error(`The MCP runtime is not ready, so ${tool.serverName} cannot be reached yet.`);\n }\n const connection = await clientManager.ensureConnected(tool.serverName);\n const timeout = clientManager.getServerRequestTimeout(tool.serverName);\n const result = await connection.callTool(\n tool.toolName,\n (params ?? {}) as Record<string, unknown>,\n timeout === undefined ? undefined : { timeout },\n );\n return toAgentToolResult(tool, result);\n },\n });\n}\n\n/**\n * Registers tools that are new to Pi.\n *\n * Separate from the active list because Pi allows `registerTool` during extension\n * loading but not `getActiveTools`/`setActiveTools`, which throw until the runtime\n * is bound. This half is therefore safe to run at install; `applyActiveTools` is\n * not, and waits for the session to start.\n */\nexport function registerNewTools(\n pi: ExtensionAPI,\n clientManagerSource: ClientManagerSource,\n newTools: readonly CatalogTool[],\n): void {\n for (const tool of newTools) registerMcpTool(pi, clientManagerSource, tool);\n}\n\n/**\n * Recomputes which of this extension's tools Pi exposes.\n *\n * Rebuilt from scratch every time, so a server that failed drops out and one that\n * reconnected comes back. Only valid once the session has started.\n */\nexport function applyActiveTools(pi: ExtensionAPI, catalog: McpCatalog): void {\n const mcpNames = new Set(catalog.allTools().map((tool) => tool.piName));\n // Tools this extension does not own are carried through untouched: the active\n // list is global, and rebuilding it from MCP alone would hide everything else.\n const others = pi.getActiveTools().filter((name) => !mcpNames.has(name));\n pi.setActiveTools([...others, ...catalog.activeToolNames()]);\n}\n"],"mappings":"AAOA,MAAM,EAAoB,CAAE,KAAM,SAAU,WAAY,CAAC,CAAE,EAS3D,SAAS,EAAW,EAAgC,CAElD,OADgB,EAAO,SAAW,CAAC,EAAA,CAEhC,OAAQ,GAAU,EAAM,OAAS,QAAU,OAAO,EAAM,MAAS,QAAQ,CAAC,CAC1E,IAAK,GAAU,EAAM,IAAI,CAAC,CAC1B,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CAQA,SAAgB,EAAkB,EAAmB,EAAyD,CAC5G,IAAM,EAAO,EAAW,CAAM,GAAK,aACnC,GAAI,EAAO,QAAS,MAAU,MAAM,CAAI,EACxC,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,QAAS,CAAE,OAAQ,EAAK,WAAY,KAAM,EAAK,QAAS,CAAE,CACxG,CAmBA,SAAgB,EAAgB,EAAkB,EAA0C,EAAyB,CACnH,EAAG,aAAa,CACd,KAAM,EAAK,OACX,MAAO,GAAG,EAAK,WAAW,IAAI,EAAK,WACnC,YAAa,EAAK,aAAe,GAAG,EAAK,SAAS,UAAU,EAAK,WAAW,cAK5E,WAAa,OAAO,KAAK,EAAK,WAAW,CAAC,CAAC,OAAS,EAAI,EAAK,YAAc,EAC3E,MAAM,QAAQ,EAAa,EAAQ,CACjC,IAAM,EAAgB,EAAoB,EAC1C,GAAI,CAAC,EACH,MAAU,MAAM,oCAAoC,EAAK,WAAW,wBAAwB,EAE9F,IAAM,EAAa,MAAM,EAAc,gBAAgB,EAAK,UAAU,EAChE,EAAU,EAAc,wBAAwB,EAAK,UAAU,EAMrE,OAAO,EAAkB,EAAM,MALV,EAAW,SAC9B,EAAK,SACJ,GAAU,CAAC,EACZ,IAAY,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,CAChD,CACqC,CACvC,CACF,CAAC,CACH,CAUA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAQ,EAAU,EAAgB,EAAI,EAAqB,CAAI,CAC5E,CAQA,SAAgB,EAAiB,EAAkB,EAA2B,CAC5E,IAAM,EAAW,IAAI,IAAI,EAAQ,SAAS,CAAC,CAAC,IAAK,GAAS,EAAK,MAAM,CAAC,EAGhE,EAAS,EAAG,eAAe,CAAC,CAAC,OAAQ,GAAS,CAAC,EAAS,IAAI,CAAI,CAAC,EACvE,EAAG,eAAe,CAAC,GAAG,EAAQ,GAAG,EAAQ,gBAAgB,CAAC,CAAC,CAC7D"}
@@ -1,3 +1,3 @@
1
- import{Type as e}from"typebox";const t={type:`object`,properties:{}};function n(e){return(e.content??[]).filter(e=>e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(`
2
- `).trim()}function r(e,t){let r=n(t)||`No output.`;if(t.isError)throw Error(r);return{content:[{type:`text`,text:r}],details:{server:e.serverName,tool:e.toolName}}}function i(n,i,a){n.registerTool({name:a.piName,label:`${a.serverName}: ${a.toolName}`,description:a.description??`${a.toolName} on the ${a.serverName} MCP server.`,parameters:e.Unsafe(Object.keys(a.inputSchema).length>0?a.inputSchema:t),async execute(e,t){let n=i();if(!n)throw Error(`The MCP runtime is not ready, so ${a.serverName} cannot be reached yet.`);let o=await n.ensureConnected(a.serverName),s=n.getServerRequestTimeout(a.serverName);return r(a,await o.callTool(a.toolName,t??{},s===void 0?void 0:{timeout:s}))}})}function a(e,t,n){for(let r of n)i(e,t,r)}function o(e,t){let n=new Set(t.allTools().map(e=>e.piName)),r=e.getActiveTools().filter(e=>!n.has(e));e.setActiveTools([...r,...t.activeToolNames()])}export{o as applyActiveTools,i as registerMcpTool,a as registerNewTools,r as toAgentToolResult};
1
+ const e={type:`object`,properties:{}};function t(e){return(e.content??[]).filter(e=>e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(`
2
+ `).trim()}function n(e,n){let r=t(n)||`No output.`;if(n.isError)throw Error(r);return{content:[{type:`text`,text:r}],details:{server:e.serverName,tool:e.toolName}}}function r(t,r,i){t.registerTool({name:i.piName,label:`${i.serverName}: ${i.toolName}`,description:i.description??`${i.toolName} on the ${i.serverName} MCP server.`,parameters:Object.keys(i.inputSchema).length>0?i.inputSchema:e,async execute(e,t){let a=r();if(!a)throw Error(`The MCP runtime is not ready, so ${i.serverName} cannot be reached yet.`);let o=await a.ensureConnected(i.serverName),s=a.getServerRequestTimeout(i.serverName);return n(i,await o.callTool(i.toolName,t??{},s===void 0?void 0:{timeout:s}))}})}function i(e,t,n){for(let i of n)r(e,t,i)}function a(e,t){let n=new Set(t.allTools().map(e=>e.piName)),r=e.getActiveTools().filter(e=>!n.has(e));e.setActiveTools([...r,...t.activeToolNames()])}export{a as applyActiveTools,r as registerMcpTool,i as registerNewTools,n as toAgentToolResult};
3
3
  //# sourceMappingURL=mcpTools.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcpTools.mjs","names":[],"sources":["../../src/tools/mcpTools.ts"],"sourcesContent":["import type { McpClientManagerService } from '@agimon-ai/mcp-proxy';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolResult, ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { Type } from 'typebox';\nimport type { CatalogTool, McpCatalog } from '../services/mcpCatalog.ts';\n\n/** Pi requires a schema; a downstream tool that declares none takes any object. */\nconst ANY_OBJECT_SCHEMA = { type: 'object', properties: {} };\n\ninterface McpToolDetails {\n server: string;\n tool: string;\n [key: string]: unknown;\n}\n\n/** Reads the text of a downstream result, dropping blocks Pi cannot render. */\nfunction resultText(result: CallToolResult): string {\n const blocks = (result.content ?? []) as Array<{ type: string; text?: string }>;\n return blocks\n .filter((block) => block.type === 'text' && typeof block.text === 'string')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n}\n\n/**\n * Turns a downstream result into a Pi tool result.\n *\n * `isError` is raised rather than returned: Pi shows a thrown message as a failed\n * call, and returning the text would present a server-side error as a success.\n */\nexport function toAgentToolResult(tool: CatalogTool, result: CallToolResult): AgentToolResult<McpToolDetails> {\n const text = resultText(result) || 'No output.';\n if (result.isError) throw new Error(text);\n return { content: [{ type: 'text', text }], details: { server: tool.serverName, tool: tool.toolName } };\n}\n\n/**\n * Supplies the live client manager, or nothing before the runtime has started.\n *\n * A getter rather than the manager itself: tools are registered from the cached\n * catalog before any container exists, and the same registration has to reach\n * whichever container is live when the tool is finally called.\n */\nexport type ClientManagerSource = () => McpClientManagerService | undefined;\n\n/**\n * Registers one downstream tool with Pi.\n *\n * Registration is permanent for the session: Pi 0.84 has no `unregisterTool`, so\n * visibility is controlled through the active list instead. The connection is\n * resolved at execution time rather than captured, so a server that reconnects\n * underneath keeps working.\n */\nexport function registerMcpTool(pi: ExtensionAPI, clientManagerSource: ClientManagerSource, tool: CatalogTool): void {\n pi.registerTool({\n name: tool.piName,\n label: `${tool.serverName}: ${tool.toolName}`,\n description: tool.description ?? `${tool.toolName} on the ${tool.serverName} MCP server.`,\n // The schema arrives as JSON Schema at runtime, so it is wrapped rather than\n // built: TypeBox validates the shape without it having to be declared here.\n parameters: Type.Unsafe<Record<string, unknown>>(\n Object.keys(tool.inputSchema).length > 0 ? tool.inputSchema : ANY_OBJECT_SCHEMA,\n ),\n async execute(_toolCallId, params) {\n const clientManager = clientManagerSource();\n if (!clientManager) {\n throw new Error(`The MCP runtime is not ready, so ${tool.serverName} cannot be reached yet.`);\n }\n const connection = await clientManager.ensureConnected(tool.serverName);\n const timeout = clientManager.getServerRequestTimeout(tool.serverName);\n const result = await connection.callTool(\n tool.toolName,\n (params ?? {}) as Record<string, unknown>,\n timeout === undefined ? undefined : { timeout },\n );\n return toAgentToolResult(tool, result);\n },\n });\n}\n\n/**\n * Registers tools that are new to Pi.\n *\n * Separate from the active list because Pi allows `registerTool` during extension\n * loading but not `getActiveTools`/`setActiveTools`, which throw until the runtime\n * is bound. This half is therefore safe to run at install; `applyActiveTools` is\n * not, and waits for the session to start.\n */\nexport function registerNewTools(\n pi: ExtensionAPI,\n clientManagerSource: ClientManagerSource,\n newTools: readonly CatalogTool[],\n): void {\n for (const tool of newTools) registerMcpTool(pi, clientManagerSource, tool);\n}\n\n/**\n * Recomputes which of this extension's tools Pi exposes.\n *\n * Rebuilt from scratch every time, so a server that failed drops out and one that\n * reconnected comes back. Only valid once the session has started.\n */\nexport function applyActiveTools(pi: ExtensionAPI, catalog: McpCatalog): void {\n const mcpNames = new Set(catalog.allTools().map((tool) => tool.piName));\n // Tools this extension does not own are carried through untouched: the active\n // list is global, and rebuilding it from MCP alone would hide everything else.\n const others = pi.getActiveTools().filter((name) => !mcpNames.has(name));\n pi.setActiveTools([...others, ...catalog.activeToolNames()]);\n}\n"],"mappings":"+BAOA,MAAM,EAAoB,CAAE,KAAM,SAAU,WAAY,CAAC,CAAE,EAS3D,SAAS,EAAW,EAAgC,CAElD,OADgB,EAAO,SAAW,CAAC,EAAA,CAEhC,OAAQ,GAAU,EAAM,OAAS,QAAU,OAAO,EAAM,MAAS,QAAQ,CAAC,CAC1E,IAAK,GAAU,EAAM,IAAI,CAAC,CAC1B,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CAQA,SAAgB,EAAkB,EAAmB,EAAyD,CAC5G,IAAM,EAAO,EAAW,CAAM,GAAK,aACnC,GAAI,EAAO,QAAS,MAAU,MAAM,CAAI,EACxC,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,QAAS,CAAE,OAAQ,EAAK,WAAY,KAAM,EAAK,QAAS,CAAE,CACxG,CAmBA,SAAgB,EAAgB,EAAkB,EAA0C,EAAyB,CACnH,EAAG,aAAa,CACd,KAAM,EAAK,OACX,MAAO,GAAG,EAAK,WAAW,IAAI,EAAK,WACnC,YAAa,EAAK,aAAe,GAAG,EAAK,SAAS,UAAU,EAAK,WAAW,cAG5E,WAAY,EAAK,OACf,OAAO,KAAK,EAAK,WAAW,CAAC,CAAC,OAAS,EAAI,EAAK,YAAc,CAChE,EACA,MAAM,QAAQ,EAAa,EAAQ,CACjC,IAAM,EAAgB,EAAoB,EAC1C,GAAI,CAAC,EACH,MAAU,MAAM,oCAAoC,EAAK,WAAW,wBAAwB,EAE9F,IAAM,EAAa,MAAM,EAAc,gBAAgB,EAAK,UAAU,EAChE,EAAU,EAAc,wBAAwB,EAAK,UAAU,EAMrE,OAAO,EAAkB,EAAM,MALV,EAAW,SAC9B,EAAK,SACJ,GAAU,CAAC,EACZ,IAAY,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,CAChD,CACqC,CACvC,CACF,CAAC,CACH,CAUA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAQ,EAAU,EAAgB,EAAI,EAAqB,CAAI,CAC5E,CAQA,SAAgB,EAAiB,EAAkB,EAA2B,CAC5E,IAAM,EAAW,IAAI,IAAI,EAAQ,SAAS,CAAC,CAAC,IAAK,GAAS,EAAK,MAAM,CAAC,EAGhE,EAAS,EAAG,eAAe,CAAC,CAAC,OAAQ,GAAS,CAAC,EAAS,IAAI,CAAI,CAAC,EACvE,EAAG,eAAe,CAAC,GAAG,EAAQ,GAAG,EAAQ,gBAAgB,CAAC,CAAC,CAC7D"}
1
+ {"version":3,"file":"mcpTools.mjs","names":[],"sources":["../../src/tools/mcpTools.ts"],"sourcesContent":["import type { McpClientManagerService } from '@agimon-ai/mcp-proxy';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolResult, ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { TSchema } from 'typebox';\nimport type { CatalogTool, McpCatalog } from '../services/mcpCatalog.ts';\n\n/** Pi requires a schema; a downstream tool that declares none takes any object. */\nconst ANY_OBJECT_SCHEMA = { type: 'object', properties: {} };\n\ninterface McpToolDetails {\n server: string;\n tool: string;\n [key: string]: unknown;\n}\n\n/** Reads the text of a downstream result, dropping blocks Pi cannot render. */\nfunction resultText(result: CallToolResult): string {\n const blocks = (result.content ?? []) as Array<{ type: string; text?: string }>;\n return blocks\n .filter((block) => block.type === 'text' && typeof block.text === 'string')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n}\n\n/**\n * Turns a downstream result into a Pi tool result.\n *\n * `isError` is raised rather than returned: Pi shows a thrown message as a failed\n * call, and returning the text would present a server-side error as a success.\n */\nexport function toAgentToolResult(tool: CatalogTool, result: CallToolResult): AgentToolResult<McpToolDetails> {\n const text = resultText(result) || 'No output.';\n if (result.isError) throw new Error(text);\n return { content: [{ type: 'text', text }], details: { server: tool.serverName, tool: tool.toolName } };\n}\n\n/**\n * Supplies the live client manager, or nothing before the runtime has started.\n *\n * A getter rather than the manager itself: tools are registered from the cached\n * catalog before any container exists, and the same registration has to reach\n * whichever container is live when the tool is finally called.\n */\nexport type ClientManagerSource = () => McpClientManagerService | undefined;\n\n/**\n * Registers one downstream tool with Pi.\n *\n * Registration is permanent for the session: Pi 0.84 has no `unregisterTool`, so\n * visibility is controlled through the active list instead. The connection is\n * resolved at execution time rather than captured, so a server that reconnects\n * underneath keeps working.\n */\nexport function registerMcpTool(pi: ExtensionAPI, clientManagerSource: ClientManagerSource, tool: CatalogTool): void {\n pi.registerTool({\n name: tool.piName,\n label: `${tool.serverName}: ${tool.toolName}`,\n description: tool.description ?? `${tool.toolName} on the ${tool.serverName} MCP server.`,\n // The schema arrives as JSON Schema at runtime, so it is wrapped rather than\n // built: TypeBox validates the shape without it having to be declared here.\n // Type.Unsafe only brands this runtime JSON Schema object. A type cast keeps\n // cached stub registration from evaluating the whole TypeBox package.\n parameters: (Object.keys(tool.inputSchema).length > 0 ? tool.inputSchema : ANY_OBJECT_SCHEMA) as TSchema,\n async execute(_toolCallId, params) {\n const clientManager = clientManagerSource();\n if (!clientManager) {\n throw new Error(`The MCP runtime is not ready, so ${tool.serverName} cannot be reached yet.`);\n }\n const connection = await clientManager.ensureConnected(tool.serverName);\n const timeout = clientManager.getServerRequestTimeout(tool.serverName);\n const result = await connection.callTool(\n tool.toolName,\n (params ?? {}) as Record<string, unknown>,\n timeout === undefined ? undefined : { timeout },\n );\n return toAgentToolResult(tool, result);\n },\n });\n}\n\n/**\n * Registers tools that are new to Pi.\n *\n * Separate from the active list because Pi allows `registerTool` during extension\n * loading but not `getActiveTools`/`setActiveTools`, which throw until the runtime\n * is bound. This half is therefore safe to run at install; `applyActiveTools` is\n * not, and waits for the session to start.\n */\nexport function registerNewTools(\n pi: ExtensionAPI,\n clientManagerSource: ClientManagerSource,\n newTools: readonly CatalogTool[],\n): void {\n for (const tool of newTools) registerMcpTool(pi, clientManagerSource, tool);\n}\n\n/**\n * Recomputes which of this extension's tools Pi exposes.\n *\n * Rebuilt from scratch every time, so a server that failed drops out and one that\n * reconnected comes back. Only valid once the session has started.\n */\nexport function applyActiveTools(pi: ExtensionAPI, catalog: McpCatalog): void {\n const mcpNames = new Set(catalog.allTools().map((tool) => tool.piName));\n // Tools this extension does not own are carried through untouched: the active\n // list is global, and rebuilding it from MCP alone would hide everything else.\n const others = pi.getActiveTools().filter((name) => !mcpNames.has(name));\n pi.setActiveTools([...others, ...catalog.activeToolNames()]);\n}\n"],"mappings":"AAOA,MAAM,EAAoB,CAAE,KAAM,SAAU,WAAY,CAAC,CAAE,EAS3D,SAAS,EAAW,EAAgC,CAElD,OADgB,EAAO,SAAW,CAAC,EAAA,CAEhC,OAAQ,GAAU,EAAM,OAAS,QAAU,OAAO,EAAM,MAAS,QAAQ,CAAC,CAC1E,IAAK,GAAU,EAAM,IAAI,CAAC,CAC1B,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CAQA,SAAgB,EAAkB,EAAmB,EAAyD,CAC5G,IAAM,EAAO,EAAW,CAAM,GAAK,aACnC,GAAI,EAAO,QAAS,MAAU,MAAM,CAAI,EACxC,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,QAAS,CAAE,OAAQ,EAAK,WAAY,KAAM,EAAK,QAAS,CAAE,CACxG,CAmBA,SAAgB,EAAgB,EAAkB,EAA0C,EAAyB,CACnH,EAAG,aAAa,CACd,KAAM,EAAK,OACX,MAAO,GAAG,EAAK,WAAW,IAAI,EAAK,WACnC,YAAa,EAAK,aAAe,GAAG,EAAK,SAAS,UAAU,EAAK,WAAW,cAK5E,WAAa,OAAO,KAAK,EAAK,WAAW,CAAC,CAAC,OAAS,EAAI,EAAK,YAAc,EAC3E,MAAM,QAAQ,EAAa,EAAQ,CACjC,IAAM,EAAgB,EAAoB,EAC1C,GAAI,CAAC,EACH,MAAU,MAAM,oCAAoC,EAAK,WAAW,wBAAwB,EAE9F,IAAM,EAAa,MAAM,EAAc,gBAAgB,EAAK,UAAU,EAChE,EAAU,EAAc,wBAAwB,EAAK,UAAU,EAMrE,OAAO,EAAkB,EAAM,MALV,EAAW,SAC9B,EAAK,SACJ,GAAU,CAAC,EACZ,IAAY,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,CAChD,CACqC,CACvC,CACF,CAAC,CACH,CAUA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAQ,EAAU,EAAgB,EAAI,EAAqB,CAAI,CAC5E,CAQA,SAAgB,EAAiB,EAAkB,EAA2B,CAC5E,IAAM,EAAW,IAAI,IAAI,EAAQ,SAAS,CAAC,CAAC,IAAK,GAAS,EAAK,MAAM,CAAC,EAGhE,EAAS,EAAG,eAAe,CAAC,CAAC,OAAQ,GAAS,CAAC,EAAS,IAAI,CAAI,CAAC,EACvE,EAAG,eAAe,CAAC,GAAG,EAAQ,GAAG,EAAQ,gBAAgB,CAAC,CAAC,CAC7D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-mcp",
3
- "version": "0.0.1-alpha.0",
3
+ "version": "0.0.1-alpha.2",
4
4
  "description": "MCP server tools for Doompi",
5
5
  "homepage": "https://agimon.ai",
6
6
  "license": "MIT",
@@ -38,9 +38,9 @@
38
38
  "@modelcontextprotocol/sdk": "1.29.0",
39
39
  "@napi-rs/keyring": "1.3.0",
40
40
  "typebox": "1.1.38",
41
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.0",
42
- "@agimon-ai/mcp-proxy": "0.30.9",
43
- "@agimon-ai/doompi-ui": "0.0.1-alpha.0"
41
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.2",
42
+ "@agimon-ai/mcp-proxy": "0.30.11",
43
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "0.84.1",