@agimon-ai/doompi-mcp 0.0.1-alpha.67 → 0.0.1-alpha.69

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("../node/configSources.cjs"),t=require("../node/mcpRuntime.cjs"),n=require("../process/sessionConfig.cjs"),r=require("../../services/mcpCatalog.cjs"),i=require("../process/directToolsEnvironment.cjs"),a=require("./mcpTools.cjs"),o=`The MCP runtime has not started yet.`;function s(e){let t=e=>Array.isArray(e)?e.map(t):typeof e!=`object`||!e?e:Object.fromEntries(Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,n])=>[e,t(n)]));return JSON.stringify(t(e))}function c(e){return s({enabled:e.enabled??!0,repoRoot:e.repoRoot,stagingDirectory:e.stagingDirectory,generatedConfigPath:e.generatedConfigPath,pluginConfigPaths:e.pluginConfigPaths??[],sources:e.sources??[],allowlist:{servers:[...e.allowlist?.servers??[]].sort((e,t)=>e.localeCompare(t)),proxy:[...e.allowlist?.proxy??[]].sort((e,t)=>e.localeCompare(t))}})}function l(e){return s({serverName:e.serverName,toolName:e.toolName,inputSchema:e.inputSchema})}var u=class{pi;runtime;catalog;groups={shared:{configSources:[],configPaths:[],serverNames:[]},sessionLocal:{configSources:[],configPaths:[],serverNames:[]},droppedServers:[],diagnostics:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;browserAuthorizationRequests=new Set;disconnectedServers=new Set;lifecycleGeneration=0;configurationFingerprint;registeredToolFingerprints=new Map;historicallyOwnedNames=new Set;incompatibleNames=new Set;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){this.applyActiveTools()}createCatalog(){return new r.McpCatalog(i.readDirectToolFilter(this.options.environment))}install(e=n.readSessionConfig(this.options.environment)){return this.bindConfiguration(e),this.catalog.toSnapshot()}async reconfigure(e){if(c(e)===this.configurationFingerprint){this.runtime?.getServices()||this.startDetached();return}let t=++this.lifecycleGeneration,n=this.runtime;this.runtime=void 0,await n?.dispose(),t===this.lifecycleGeneration&&(this.resourceCache.clear(),this.authorizationUrls.clear(),this.browserAuthorizationRequests.clear(),this.bindConfiguration(e),this.applyActiveTools(),this.emitChange(),this.startDetached())}bindConfiguration(n){this.configurationFingerprint=c(n),this.groups=e.buildMcpConfigGroups(n),this.disconnectedServers.clear(),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];this.catalog.seed(t.readCachedCatalog(this.configSources()),r);for(let e of this.groups.diagnostics)this.catalog.addDiagnostic(e);this.registerTools(this.catalog.allTools())}registerTools(e){let t=[];for(let n of e){let e=l(n),r=this.registeredToolFingerprints.get(n.piName);if(this.historicallyOwnedNames.add(n.piName),r===void 0){this.registeredToolFingerprints.set(n.piName,e),this.incompatibleNames.delete(n.piName),t.push(n);continue}if(r===e){this.incompatibleNames.delete(n.piName);continue}this.incompatibleNames.add(n.piName),this.catalog.addDiagnostic(`MCP tool "${n.piName}" changed identity or input schema and is hidden until Pi is relaunched.`)}a.registerNewTools(this.pi,this.clientManager,t,e=>this.isToolAvailable(e))}isToolAvailable(e){if(this.disconnectedServers.has(e.serverName)||this.incompatibleNames.has(e.piName))return!1;let t=this.catalog.findTool(e.piName);return t!==void 0&&l(t)===l(e)}applyActiveTools(){let e=new Set(this.incompatibleNames);for(let t of this.catalog.allTools())this.disconnectedServers.has(t.serverName)&&e.add(t.piName);a.applyActiveTools(this.pi,this.catalog,this.historicallyOwnedNames,e)}startDetached(){this.start().catch(e=>{this.catalog.addDiagnostic(`Could not start the MCP runtime: ${e instanceof Error?e.message:String(e)}`),this.emitChange()})}async start(){let e=++this.lifecycleGeneration;if(this.disconnectedServers.clear(),this.configuredServerNames().length===0)return;let[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([Promise.resolve().then(()=>require("../node/keyringTokenStore.cjs")),Promise.resolve().then(()=>require("../node/mcpRuntime.cjs"))]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configSources(),a=this.options.tokenStore??await t();await r.start({configSources:i,tokenStore:a,onAuthorizationUrl:(t,n)=>this.onAuthorizationUrl(t,n,e),onServerStateChange:t=>this.onServerStateChange(t,e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t,n){if(n!==this.lifecycleGeneration||this.disconnectedServers.has(t))return;this.authorizationUrls.set(t,e.toString()),this.emitChange();let r=this.browserAuthorizationRequests.has(t);Promise.resolve().then(()=>this.options.onAuthorizationUrl?.(e,t,{openBrowser:r})).catch(e=>{if(n!==this.lifecycleGeneration)return;let r=e instanceof Error?e.message:String(e);this.catalog.addDiagnostic(`Could not open authorization page for "${t}": ${r}`),this.emitChange()})}configSources(){return[...this.groups.sessionLocal.configSources,...this.groups.shared.configSources]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async disconnect(e){if(!this.configuredServerNames().includes(e))throw Error(`Unknown MCP server: ${e}`);let t=this.clientManager();if(!t)throw Error(o);let n=this.lifecycleGeneration;this.disconnectedServers.add(e);try{await t.disconnectServer(e)}catch(t){throw n===this.lifecycleGeneration&&this.disconnectedServers.delete(e),t}n===this.lifecycleGeneration&&(this.resourceCache.delete(e),this.authorizationUrls.delete(e),this.browserAuthorizationRequests.delete(e),this.catalog.applyStateChange({serverName:e,state:`closed`},[]),this.applyActiveTools(),this.emitChange())}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(o);this.disconnectedServers.delete(e),this.resourceCache.delete(e),this.authorizationUrls.delete(e)&&this.emitChange(),this.browserAuthorizationRequests.add(e);try{await t.disconnectServer(e),await t.ensureConnected(e)}finally{this.browserAuthorizationRequests.delete(e)}}async openAuthorizationPage(e){let t=this.authorizationUrls.get(e);if(!t)throw Error(`No authorization page is waiting for this MCP server.`);let n=this.options.onAuthorizationUrl;if(!n)throw Error(`This host cannot open authorization pages.`);await n(new URL(t),e,{openBrowser:!0})}getSnapshot(){return this.catalog.toSnapshot()}resolveToolSelectors(e){return this.catalog.resolveToolSelectors(e)}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,t){this.catalog.setDisabled(e,!t),this.applyActiveTools(),this.emitChange()}async listResources(e,t={}){if(this.disconnectedServers.has(e))throw Error(`MCP server is disconnected: ${e}`);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(o);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,this.browserAuthorizationRequests.clear(),this.authorizationUrls.clear(),this.resourceCache.clear();let e=this.runtime;this.runtime=void 0,await e?.dispose()}onServerStateChange(e,t){if(t!==this.lifecycleGeneration||this.disconnectedServers.has(e.serverName))return;e.state!==`connecting`&&e.state!==`needs-auth`&&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(t!==this.lifecycleGeneration||!n?.isCurrent(r)||this.disconnectedServers.has(e.serverName))return;let a=this.catalog.applyStateChange(e,i);this.registerTools(a),this.applyActiveTools(),this.emitChange()})}};exports.McpSession=u,exports.mcpConfigurationFingerprint=c;
1
+ const e=require("../node/configSources.cjs"),t=require("../node/mcpRuntime.cjs"),n=require("../process/sessionConfig.cjs"),r=require("../../services/mcpCatalog.cjs"),i=require("../process/directToolsEnvironment.cjs"),a=require("./mcpTools.cjs"),o=`The MCP runtime has not started yet.`;function s(e){let t=e=>Array.isArray(e)?e.map(t):typeof e!=`object`||!e?e:Object.fromEntries(Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,n])=>[e,t(n)]));return JSON.stringify(t(e))}function c(e){return s({enabled:e.enabled??!0,repoRoot:e.repoRoot,stagingDirectory:e.stagingDirectory,generatedConfigPath:e.generatedConfigPath,pluginConfigPaths:e.pluginConfigPaths??[],sources:e.sources??[],allowlist:{servers:[...e.allowlist?.servers??[]].sort((e,t)=>e.localeCompare(t)),proxy:[...e.allowlist?.proxy??[]].sort((e,t)=>e.localeCompare(t))}})}function l(e){return s({serverName:e.serverName,toolName:e.toolName,inputSchema:e.inputSchema})}var u=class{pi;runtime;catalog;groups={shared:{configSources:[],configPaths:[],serverNames:[]},sessionLocal:{configSources:[],configPaths:[],serverNames:[]},droppedServers:[],diagnostics:[]};options;tokenStore;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;browserAuthorizationRequests=new Set;disconnectedServers=new Set;lifecycleGeneration=0;configurationFingerprint;registeredToolFingerprints=new Map;historicallyOwnedNames=new Set;incompatibleNames=new Set;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){this.applyActiveTools()}createCatalog(){return new r.McpCatalog(i.readDirectToolFilter(this.options.environment))}install(e=n.readSessionConfig(this.options.environment)){return this.bindConfiguration(e),this.catalog.toSnapshot()}async reconfigure(e){if(c(e)===this.configurationFingerprint){this.runtime?.getServices()||this.startDetached();return}let t=++this.lifecycleGeneration,n=this.runtime;this.runtime=void 0,await n?.dispose(),t===this.lifecycleGeneration&&(this.resourceCache.clear(),this.authorizationUrls.clear(),this.browserAuthorizationRequests.clear(),this.bindConfiguration(e),this.applyActiveTools(),this.emitChange(),this.startDetached())}bindConfiguration(n){this.configurationFingerprint=c(n),this.groups=e.buildMcpConfigGroups(n),this.disconnectedServers.clear(),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];this.catalog.seed(t.readCachedCatalog(this.configSources()),r);for(let e of this.groups.diagnostics)this.catalog.addDiagnostic(e);this.registerTools(this.catalog.allTools())}registerTools(e){let t=[];for(let n of e){let e=l(n),r=this.registeredToolFingerprints.get(n.piName);if(this.historicallyOwnedNames.add(n.piName),r===void 0){this.registeredToolFingerprints.set(n.piName,e),this.incompatibleNames.delete(n.piName),t.push(n);continue}if(r===e){this.incompatibleNames.delete(n.piName);continue}this.incompatibleNames.add(n.piName),this.catalog.addDiagnostic(`MCP tool "${n.piName}" changed identity or input schema and is hidden until Pi is relaunched.`)}a.registerNewTools(this.pi,this.clientManager,t,e=>this.isToolAvailable(e))}isToolAvailable(e){if(this.disconnectedServers.has(e.serverName)||this.incompatibleNames.has(e.piName))return!1;let t=this.catalog.findTool(e.piName);return t!==void 0&&l(t)===l(e)}applyActiveTools(){let e=new Set(this.incompatibleNames);for(let t of this.catalog.allTools())this.disconnectedServers.has(t.serverName)&&e.add(t.piName);a.applyActiveTools(this.pi,this.catalog,this.historicallyOwnedNames,e)}startDetached(){this.start().catch(e=>{this.catalog.addDiagnostic(`Could not start the MCP runtime: ${e instanceof Error?e.message:String(e)}`),this.emitChange()})}async start(){let e=++this.lifecycleGeneration;if(this.disconnectedServers.clear(),this.configuredServerNames().length===0)return;let[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([Promise.resolve().then(()=>require("../node/keyringTokenStore.cjs")),Promise.resolve().then(()=>require("../node/mcpRuntime.cjs"))]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configSources(),a=this.options.tokenStore??await t();this.tokenStore=a,await r.start({configSources:i,tokenStore:a,onAuthorizationUrl:(t,n)=>this.onAuthorizationUrl(t,n,e),onServerStateChange:t=>this.onServerStateChange(t,e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t,n){if(n!==this.lifecycleGeneration||this.disconnectedServers.has(t))return;this.authorizationUrls.set(t,e.toString()),this.emitChange();let r=this.browserAuthorizationRequests.has(t);Promise.resolve().then(()=>this.options.onAuthorizationUrl?.(e,t,{openBrowser:r})).catch(e=>{if(n!==this.lifecycleGeneration)return;let r=e instanceof Error?e.message:String(e);this.catalog.addDiagnostic(`Could not open authorization page for "${t}": ${r}`),this.emitChange()})}configSources(){return[...this.groups.sessionLocal.configSources,...this.groups.shared.configSources]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async disconnect(e){if(!this.configuredServerNames().includes(e))throw Error(`Unknown MCP server: ${e}`);let t=this.clientManager();if(!t)throw Error(o);let n=this.lifecycleGeneration;this.disconnectedServers.add(e);try{await t.disconnectServer(e)}catch(t){throw n===this.lifecycleGeneration&&this.disconnectedServers.delete(e),t}n===this.lifecycleGeneration&&(this.resourceCache.delete(e),this.authorizationUrls.delete(e),this.browserAuthorizationRequests.delete(e),this.catalog.applyStateChange({serverName:e,state:`closed`},[]),this.applyActiveTools(),this.emitChange())}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(o);this.disconnectedServers.delete(e),this.resourceCache.delete(e),this.authorizationUrls.delete(e)&&this.emitChange(),this.browserAuthorizationRequests.add(e);try{await t.disconnectServer(e),await this.tokenStore?.clear(e),await t.ensureConnected(e)}finally{this.browserAuthorizationRequests.delete(e)}}async openAuthorizationPage(e){let t=this.authorizationUrls.get(e);if(!t)throw Error(`No authorization page is waiting for this MCP server.`);let n=this.options.onAuthorizationUrl;if(!n)throw Error(`This host cannot open authorization pages.`);await n(new URL(t),e,{openBrowser:!0})}getSnapshot(){return this.catalog.toSnapshot()}resolveToolSelectors(e){return this.catalog.resolveToolSelectors(e)}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,t){this.catalog.setDisabled(e,!t),this.applyActiveTools(),this.emitChange()}async listResources(e,t={}){if(this.disconnectedServers.has(e))throw Error(`MCP server is disconnected: ${e}`);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(o);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,this.browserAuthorizationRequests.clear(),this.authorizationUrls.clear(),this.resourceCache.clear();let e=this.runtime;this.runtime=void 0,await e?.dispose()}onServerStateChange(e,t){if(t!==this.lifecycleGeneration||this.disconnectedServers.has(e.serverName))return;e.state!==`connecting`&&e.state!==`needs-auth`&&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(t!==this.lifecycleGeneration||!n?.isCurrent(r)||this.disconnectedServers.has(e.serverName))return;let a=this.catalog.applyStateChange(e,i);this.registerTools(a),this.applyActiveTools(),this.emitChange()})}};exports.McpSession=u,exports.mcpConfigurationFingerprint=c;
2
2
  //# sourceMappingURL=mcpSession.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcpSession.cjs","names":["McpCatalog","readDirectToolFilter","readSessionConfig","buildMcpConfigGroups","readCachedCatalog"],"sources":["../../../src/adapters/pi/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { DoomMcpResolvedToolSelection } from '@agimon-ai/doompi-extension-contracts/mcp-tool-resolver';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { type CatalogTool, McpCatalog } from '../../services/mcpCatalog.ts';\nimport type { McpResourceView, McpServerView } from '../../types/mcp.ts';\nimport type { McpConfigGroups, McpConfigSource, McpSessionConfig } from '../../types/mcpConfig.ts';\nimport { buildMcpConfigGroups } from '../node/configSources.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from '../node/mcpRuntime.ts';\nimport { readDirectToolFilter } from '../process/directToolsEnvironment.ts';\nimport { readSessionConfig } from '../process/sessionConfig.ts';\nimport { applyActiveTools, registerNewTools } from './mcpTools.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.';\nconst AUTHORIZATION_URL_NOT_READY = 'No authorization page is waiting for this MCP server.';\n\nfunction canonicalJson(value: unknown): string {\n const normalize = (candidate: unknown): unknown => {\n if (Array.isArray(candidate)) return candidate.map(normalize);\n if (typeof candidate !== 'object' || candidate === null) return candidate;\n return Object.fromEntries(\n Object.entries(candidate as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => [key, normalize(child)]),\n );\n };\n return JSON.stringify(normalize(value));\n}\n\nexport function mcpConfigurationFingerprint(configuration: McpSessionConfig): string {\n return canonicalJson({\n enabled: configuration.enabled ?? true,\n repoRoot: configuration.repoRoot,\n stagingDirectory: configuration.stagingDirectory,\n generatedConfigPath: configuration.generatedConfigPath,\n pluginConfigPaths: configuration.pluginConfigPaths ?? [],\n sources: configuration.sources ?? [],\n allowlist: {\n servers: [...(configuration.allowlist?.servers ?? [])].sort((left, right) => left.localeCompare(right)),\n proxy: [...(configuration.allowlist?.proxy ?? [])].sort((left, right) => left.localeCompare(right)),\n },\n });\n}\n\nfunction toolRegistrationFingerprint(tool: CatalogTool): string {\n return canonicalJson({\n serverName: tool.serverName,\n toolName: tool.toolName,\n inputSchema: tool.inputSchema,\n });\n}\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Surfaces an OAuth URL, opening it when an explicit authorization requested that. */\n onAuthorizationUrl?: (url: URL, serverName: string, options: { openBrowser: boolean }) => void | Promise<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: { configSources: [], configPaths: [], serverNames: [] },\n sessionLocal: { configSources: [], configPaths: [], serverNames: [] },\n droppedServers: [],\n diagnostics: [],\n };\n private readonly options: McpSessionOptions;\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 /** Servers whose explicit auth action should launch the browser when its URL arrives. */\n private readonly browserAuthorizationRequests = new Set<string>();\n /** Explicit session disconnects fence late discovery and stale tool invocations. */\n private readonly disconnectedServers = new Set<string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n private configurationFingerprint: string | undefined;\n /** Pi wrappers survive configuration changes because the host cannot unregister them. */\n private readonly registeredToolFingerprints = new Map<string, string>();\n private readonly historicallyOwnedNames = new Set<string>();\n private readonly incompatibleNames = new Set<string>();\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.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 this.applyActiveTools();\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.bindConfiguration(configuration);\n return this.catalog.toSnapshot();\n }\n\n /** Replaces the complete MCP projection and retires the previous live owner. */\n async reconfigure(configuration: McpSessionConfig): Promise<void> {\n const fingerprint = mcpConfigurationFingerprint(configuration);\n if (fingerprint === this.configurationFingerprint) {\n if (!this.runtime?.getServices()) this.startDetached();\n return;\n }\n\n const generation = ++this.lifecycleGeneration;\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.dispose();\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.clear();\n this.authorizationUrls.clear();\n this.browserAuthorizationRequests.clear();\n this.bindConfiguration(configuration);\n this.applyActiveTools();\n this.emitChange();\n this.startDetached();\n }\n\n private bindConfiguration(configuration: McpSessionConfig): void {\n this.configurationFingerprint = mcpConfigurationFingerprint(configuration);\n this.groups = buildMcpConfigGroups(configuration);\n this.disconnectedServers.clear();\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configSources()), configuredServers);\n for (const diagnostic of this.groups.diagnostics) this.catalog.addDiagnostic(diagnostic);\n this.registerTools(this.catalog.allTools());\n }\n\n private registerTools(tools: readonly CatalogTool[]): void {\n const unseen: CatalogTool[] = [];\n for (const tool of tools) {\n const fingerprint = toolRegistrationFingerprint(tool);\n const registered = this.registeredToolFingerprints.get(tool.piName);\n this.historicallyOwnedNames.add(tool.piName);\n if (registered === undefined) {\n this.registeredToolFingerprints.set(tool.piName, fingerprint);\n this.incompatibleNames.delete(tool.piName);\n unseen.push(tool);\n continue;\n }\n if (registered === fingerprint) {\n this.incompatibleNames.delete(tool.piName);\n continue;\n }\n this.incompatibleNames.add(tool.piName);\n this.catalog.addDiagnostic(\n `MCP tool \"${tool.piName}\" changed identity or input schema and is hidden until Pi is relaunched.`,\n );\n }\n registerNewTools(this.pi, this.clientManager, unseen, (tool) => this.isToolAvailable(tool));\n }\n\n private isToolAvailable(registeredTool: CatalogTool): boolean {\n if (this.disconnectedServers.has(registeredTool.serverName) || this.incompatibleNames.has(registeredTool.piName))\n return false;\n const current = this.catalog.findTool(registeredTool.piName);\n return (\n current !== undefined && toolRegistrationFingerprint(current) === toolRegistrationFingerprint(registeredTool)\n );\n }\n\n private applyActiveTools(): void {\n const unavailable = new Set(this.incompatibleNames);\n for (const tool of this.catalog.allTools()) {\n if (this.disconnectedServers.has(tool.serverName)) unavailable.add(tool.piName);\n }\n applyActiveTools(this.pi, this.catalog, this.historicallyOwnedNames, unavailable);\n }\n\n private startDetached(): void {\n void this.start().catch((error: unknown) => {\n this.catalog.addDiagnostic(\n `Could not start the MCP runtime: ${error instanceof Error ? error.message : String(error)}`,\n );\n this.emitChange();\n });\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 this.disconnectedServers.clear();\n // Explicit disabled and empty Doom projections must not instantiate any\n // runtime collaborator, including the keyring-backed token store.\n if (this.configuredServerNames().length === 0) return;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('../node/keyringTokenStore.ts'),\n import('../node/mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\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 const configSources = this.configSources();\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configSources,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName, generation),\n onServerStateChange: (change) => this.onServerStateChange(change, generation),\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. Host work is\n * detached so a failed desktop browser launch never cancels a still-usable OAuth\n * callback; the overlay retains a clickable fallback and a retry action.\n */\n private onAuthorizationUrl(url: URL, serverName: string, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(serverName)) return;\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n const openBrowser = this.browserAuthorizationRequests.has(serverName);\n void Promise.resolve()\n .then(() => this.options.onAuthorizationUrl?.(url, serverName, { openBrowser }))\n .catch((error: unknown) => {\n if (generation !== this.lifecycleGeneration) return;\n const detail = error instanceof Error ? error.message : String(error);\n this.catalog.addDiagnostic(`Could not open authorization page for \"${serverName}\": ${detail}`);\n this.emitChange();\n });\n }\n\n /** Ordered layers for the one in-process client container. */\n private configSources(): McpConfigSource[] {\n return [...this.groups.sessionLocal.configSources, ...this.groups.shared.configSources];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /** Closes only this session's connection. Saved OAuth credentials are untouched. */\n async disconnect(serverName: string): Promise<void> {\n if (!this.configuredServerNames().includes(serverName)) throw new Error(`Unknown MCP server: ${serverName}`);\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n const generation = this.lifecycleGeneration;\n this.disconnectedServers.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n } catch (error) {\n if (generation === this.lifecycleGeneration) this.disconnectedServers.delete(serverName);\n throw error;\n }\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.delete(serverName);\n this.authorizationUrls.delete(serverName);\n this.browserAuthorizationRequests.delete(serverName);\n this.catalog.applyStateChange({ serverName, state: 'closed' }, []);\n this.applyActiveTools();\n this.emitChange();\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 this.disconnectedServers.delete(serverName);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n if (this.authorizationUrls.delete(serverName)) this.emitChange();\n this.browserAuthorizationRequests.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n } finally {\n this.browserAuthorizationRequests.delete(serverName);\n }\n }\n\n /** Reopens the URL already reserved for a flow without restarting that flow. */\n async openAuthorizationPage(serverName: string): Promise<void> {\n const authorizationUrl = this.authorizationUrls.get(serverName);\n if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);\n const onAuthorizationUrl = this.options.onAuthorizationUrl;\n if (!onAuthorizationUrl) throw new Error('This host cannot open authorization pages.');\n await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n resolveToolSelectors(selectors: readonly string[]): readonly DoomMcpResolvedToolSelection[] {\n return this.catalog.resolveToolSelectors(selectors);\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: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\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 this.applyActiveTools();\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 (this.disconnectedServers.has(serverName)) throw new Error(`MCP server is disconnected: ${serverName}`);\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 this.browserAuthorizationRequests.clear();\n this.authorizationUrls.clear();\n this.resourceCache.clear();\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.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, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(change.serverName)) return;\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' && change.state !== 'needs-auth') {\n this.authorizationUrls.delete(change.serverName);\n }\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 (\n generation !== this.lifecycleGeneration ||\n !runtime?.isCurrent(services) ||\n this.disconnectedServers.has(change.serverName)\n )\n return;\n const added = this.catalog.applyStateChange(change, tools);\n this.registerTools(added);\n this.applyActiveTools();\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcpSession.cjs","names":["McpCatalog","readDirectToolFilter","readSessionConfig","buildMcpConfigGroups","readCachedCatalog"],"sources":["../../../src/adapters/pi/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { DoomMcpResolvedToolSelection } from '@agimon-ai/doompi-extension-contracts/mcp-tool-resolver';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { type CatalogTool, McpCatalog } from '../../services/mcpCatalog.ts';\nimport type { McpResourceView, McpServerView } from '../../types/mcp.ts';\nimport type { McpConfigGroups, McpConfigSource, McpSessionConfig } from '../../types/mcpConfig.ts';\nimport { buildMcpConfigGroups } from '../node/configSources.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from '../node/mcpRuntime.ts';\nimport { readDirectToolFilter } from '../process/directToolsEnvironment.ts';\nimport { readSessionConfig } from '../process/sessionConfig.ts';\nimport { applyActiveTools, registerNewTools } from './mcpTools.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.';\nconst AUTHORIZATION_URL_NOT_READY = 'No authorization page is waiting for this MCP server.';\n\nfunction canonicalJson(value: unknown): string {\n const normalize = (candidate: unknown): unknown => {\n if (Array.isArray(candidate)) return candidate.map(normalize);\n if (typeof candidate !== 'object' || candidate === null) return candidate;\n return Object.fromEntries(\n Object.entries(candidate as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => [key, normalize(child)]),\n );\n };\n return JSON.stringify(normalize(value));\n}\n\nexport function mcpConfigurationFingerprint(configuration: McpSessionConfig): string {\n return canonicalJson({\n enabled: configuration.enabled ?? true,\n repoRoot: configuration.repoRoot,\n stagingDirectory: configuration.stagingDirectory,\n generatedConfigPath: configuration.generatedConfigPath,\n pluginConfigPaths: configuration.pluginConfigPaths ?? [],\n sources: configuration.sources ?? [],\n allowlist: {\n servers: [...(configuration.allowlist?.servers ?? [])].sort((left, right) => left.localeCompare(right)),\n proxy: [...(configuration.allowlist?.proxy ?? [])].sort((left, right) => left.localeCompare(right)),\n },\n });\n}\n\nfunction toolRegistrationFingerprint(tool: CatalogTool): string {\n return canonicalJson({\n serverName: tool.serverName,\n toolName: tool.toolName,\n inputSchema: tool.inputSchema,\n });\n}\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Surfaces an OAuth URL, opening it when an explicit authorization requested that. */\n onAuthorizationUrl?: (url: URL, serverName: string, options: { openBrowser: boolean }) => void | Promise<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: { configSources: [], configPaths: [], serverNames: [] },\n sessionLocal: { configSources: [], configPaths: [], serverNames: [] },\n droppedServers: [],\n diagnostics: [],\n };\n private readonly options: McpSessionOptions;\n /** Credential persistence used by the active runtime and explicit reauthorization. */\n private tokenStore: TokenStore | 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 /** Servers whose explicit auth action should launch the browser when its URL arrives. */\n private readonly browserAuthorizationRequests = new Set<string>();\n /** Explicit session disconnects fence late discovery and stale tool invocations. */\n private readonly disconnectedServers = new Set<string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n private configurationFingerprint: string | undefined;\n /** Pi wrappers survive configuration changes because the host cannot unregister them. */\n private readonly registeredToolFingerprints = new Map<string, string>();\n private readonly historicallyOwnedNames = new Set<string>();\n private readonly incompatibleNames = new Set<string>();\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.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 this.applyActiveTools();\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.bindConfiguration(configuration);\n return this.catalog.toSnapshot();\n }\n\n /** Replaces the complete MCP projection and retires the previous live owner. */\n async reconfigure(configuration: McpSessionConfig): Promise<void> {\n const fingerprint = mcpConfigurationFingerprint(configuration);\n if (fingerprint === this.configurationFingerprint) {\n if (!this.runtime?.getServices()) this.startDetached();\n return;\n }\n\n const generation = ++this.lifecycleGeneration;\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.dispose();\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.clear();\n this.authorizationUrls.clear();\n this.browserAuthorizationRequests.clear();\n this.bindConfiguration(configuration);\n this.applyActiveTools();\n this.emitChange();\n this.startDetached();\n }\n\n private bindConfiguration(configuration: McpSessionConfig): void {\n this.configurationFingerprint = mcpConfigurationFingerprint(configuration);\n this.groups = buildMcpConfigGroups(configuration);\n this.disconnectedServers.clear();\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configSources()), configuredServers);\n for (const diagnostic of this.groups.diagnostics) this.catalog.addDiagnostic(diagnostic);\n this.registerTools(this.catalog.allTools());\n }\n\n private registerTools(tools: readonly CatalogTool[]): void {\n const unseen: CatalogTool[] = [];\n for (const tool of tools) {\n const fingerprint = toolRegistrationFingerprint(tool);\n const registered = this.registeredToolFingerprints.get(tool.piName);\n this.historicallyOwnedNames.add(tool.piName);\n if (registered === undefined) {\n this.registeredToolFingerprints.set(tool.piName, fingerprint);\n this.incompatibleNames.delete(tool.piName);\n unseen.push(tool);\n continue;\n }\n if (registered === fingerprint) {\n this.incompatibleNames.delete(tool.piName);\n continue;\n }\n this.incompatibleNames.add(tool.piName);\n this.catalog.addDiagnostic(\n `MCP tool \"${tool.piName}\" changed identity or input schema and is hidden until Pi is relaunched.`,\n );\n }\n registerNewTools(this.pi, this.clientManager, unseen, (tool) => this.isToolAvailable(tool));\n }\n\n private isToolAvailable(registeredTool: CatalogTool): boolean {\n if (this.disconnectedServers.has(registeredTool.serverName) || this.incompatibleNames.has(registeredTool.piName))\n return false;\n const current = this.catalog.findTool(registeredTool.piName);\n return (\n current !== undefined && toolRegistrationFingerprint(current) === toolRegistrationFingerprint(registeredTool)\n );\n }\n\n private applyActiveTools(): void {\n const unavailable = new Set(this.incompatibleNames);\n for (const tool of this.catalog.allTools()) {\n if (this.disconnectedServers.has(tool.serverName)) unavailable.add(tool.piName);\n }\n applyActiveTools(this.pi, this.catalog, this.historicallyOwnedNames, unavailable);\n }\n\n private startDetached(): void {\n void this.start().catch((error: unknown) => {\n this.catalog.addDiagnostic(\n `Could not start the MCP runtime: ${error instanceof Error ? error.message : String(error)}`,\n );\n this.emitChange();\n });\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 this.disconnectedServers.clear();\n // Explicit disabled and empty Doom projections must not instantiate any\n // runtime collaborator, including the keyring-backed token store.\n if (this.configuredServerNames().length === 0) return;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('../node/keyringTokenStore.ts'),\n import('../node/mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\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 const configSources = this.configSources();\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n this.tokenStore = tokenStore;\n await runtime.start({\n configSources,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName, generation),\n onServerStateChange: (change) => this.onServerStateChange(change, generation),\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. Host work is\n * detached so a failed desktop browser launch never cancels a still-usable OAuth\n * callback; the overlay retains a clickable fallback and a retry action.\n */\n private onAuthorizationUrl(url: URL, serverName: string, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(serverName)) return;\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n const openBrowser = this.browserAuthorizationRequests.has(serverName);\n void Promise.resolve()\n .then(() => this.options.onAuthorizationUrl?.(url, serverName, { openBrowser }))\n .catch((error: unknown) => {\n if (generation !== this.lifecycleGeneration) return;\n const detail = error instanceof Error ? error.message : String(error);\n this.catalog.addDiagnostic(`Could not open authorization page for \"${serverName}\": ${detail}`);\n this.emitChange();\n });\n }\n\n /** Ordered layers for the one in-process client container. */\n private configSources(): McpConfigSource[] {\n return [...this.groups.sessionLocal.configSources, ...this.groups.shared.configSources];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /** Closes only this session's connection. Saved OAuth credentials are untouched. */\n async disconnect(serverName: string): Promise<void> {\n if (!this.configuredServerNames().includes(serverName)) throw new Error(`Unknown MCP server: ${serverName}`);\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n const generation = this.lifecycleGeneration;\n this.disconnectedServers.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n } catch (error) {\n if (generation === this.lifecycleGeneration) this.disconnectedServers.delete(serverName);\n throw error;\n }\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.delete(serverName);\n this.authorizationUrls.delete(serverName);\n this.browserAuthorizationRequests.delete(serverName);\n this.catalog.applyStateChange({ serverName, state: 'closed' }, []);\n this.applyActiveTools();\n this.emitChange();\n }\n\n /**\n * Reconnects one server with a fresh OAuth registration.\n *\n * The connection is dropped before credentials are cleared so a failed disconnect\n * does not silently sign the user out. Clearing both tokens and dynamic client\n * information prevents a server-side registration reset from replaying a stale\n * `client_id` forever.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n this.disconnectedServers.delete(serverName);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n if (this.authorizationUrls.delete(serverName)) this.emitChange();\n this.browserAuthorizationRequests.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n await this.tokenStore?.clear(serverName);\n await clientManager.ensureConnected(serverName);\n } finally {\n this.browserAuthorizationRequests.delete(serverName);\n }\n }\n\n /** Reopens the URL already reserved for a flow without restarting that flow. */\n async openAuthorizationPage(serverName: string): Promise<void> {\n const authorizationUrl = this.authorizationUrls.get(serverName);\n if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);\n const onAuthorizationUrl = this.options.onAuthorizationUrl;\n if (!onAuthorizationUrl) throw new Error('This host cannot open authorization pages.');\n await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n resolveToolSelectors(selectors: readonly string[]): readonly DoomMcpResolvedToolSelection[] {\n return this.catalog.resolveToolSelectors(selectors);\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: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\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 this.applyActiveTools();\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 (this.disconnectedServers.has(serverName)) throw new Error(`MCP server is disconnected: ${serverName}`);\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 this.browserAuthorizationRequests.clear();\n this.authorizationUrls.clear();\n this.resourceCache.clear();\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.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, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(change.serverName)) return;\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' && change.state !== 'needs-auth') {\n this.authorizationUrls.delete(change.serverName);\n }\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 (\n generation !== this.lifecycleGeneration ||\n !runtime?.isCurrent(services) ||\n this.disconnectedServers.has(change.serverName)\n )\n return;\n const added = this.catalog.applyStateChange(change, tools);\n this.registerTools(added);\n this.applyActiveTools();\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{buildMcpConfigGroups as e}from"../node/configSources.mjs";import{readCachedCatalog as t}from"../node/mcpRuntime.mjs";import{readSessionConfig as n}from"../process/sessionConfig.mjs";import{McpCatalog as r}from"../../services/mcpCatalog.mjs";import{readDirectToolFilter as i}from"../process/directToolsEnvironment.mjs";import{applyActiveTools as a,registerNewTools as o}from"./mcpTools.mjs";const s=`The MCP runtime has not started yet.`;function c(e){let t=e=>Array.isArray(e)?e.map(t):typeof e!=`object`||!e?e:Object.fromEntries(Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,n])=>[e,t(n)]));return JSON.stringify(t(e))}function l(e){return c({enabled:e.enabled??!0,repoRoot:e.repoRoot,stagingDirectory:e.stagingDirectory,generatedConfigPath:e.generatedConfigPath,pluginConfigPaths:e.pluginConfigPaths??[],sources:e.sources??[],allowlist:{servers:[...e.allowlist?.servers??[]].sort((e,t)=>e.localeCompare(t)),proxy:[...e.allowlist?.proxy??[]].sort((e,t)=>e.localeCompare(t))}})}function u(e){return c({serverName:e.serverName,toolName:e.toolName,inputSchema:e.inputSchema})}var d=class{pi;runtime;catalog;groups={shared:{configSources:[],configPaths:[],serverNames:[]},sessionLocal:{configSources:[],configPaths:[],serverNames:[]},droppedServers:[],diagnostics:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;browserAuthorizationRequests=new Set;disconnectedServers=new Set;lifecycleGeneration=0;configurationFingerprint;registeredToolFingerprints=new Map;historicallyOwnedNames=new Set;incompatibleNames=new Set;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){this.applyActiveTools()}createCatalog(){return new r(i(this.options.environment))}install(e=n(this.options.environment)){return this.bindConfiguration(e),this.catalog.toSnapshot()}async reconfigure(e){if(l(e)===this.configurationFingerprint){this.runtime?.getServices()||this.startDetached();return}let t=++this.lifecycleGeneration,n=this.runtime;this.runtime=void 0,await n?.dispose(),t===this.lifecycleGeneration&&(this.resourceCache.clear(),this.authorizationUrls.clear(),this.browserAuthorizationRequests.clear(),this.bindConfiguration(e),this.applyActiveTools(),this.emitChange(),this.startDetached())}bindConfiguration(n){this.configurationFingerprint=l(n),this.groups=e(n),this.disconnectedServers.clear(),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];this.catalog.seed(t(this.configSources()),r);for(let e of this.groups.diagnostics)this.catalog.addDiagnostic(e);this.registerTools(this.catalog.allTools())}registerTools(e){let t=[];for(let n of e){let e=u(n),r=this.registeredToolFingerprints.get(n.piName);if(this.historicallyOwnedNames.add(n.piName),r===void 0){this.registeredToolFingerprints.set(n.piName,e),this.incompatibleNames.delete(n.piName),t.push(n);continue}if(r===e){this.incompatibleNames.delete(n.piName);continue}this.incompatibleNames.add(n.piName),this.catalog.addDiagnostic(`MCP tool "${n.piName}" changed identity or input schema and is hidden until Pi is relaunched.`)}o(this.pi,this.clientManager,t,e=>this.isToolAvailable(e))}isToolAvailable(e){if(this.disconnectedServers.has(e.serverName)||this.incompatibleNames.has(e.piName))return!1;let t=this.catalog.findTool(e.piName);return t!==void 0&&u(t)===u(e)}applyActiveTools(){let e=new Set(this.incompatibleNames);for(let t of this.catalog.allTools())this.disconnectedServers.has(t.serverName)&&e.add(t.piName);a(this.pi,this.catalog,this.historicallyOwnedNames,e)}startDetached(){this.start().catch(e=>{this.catalog.addDiagnostic(`Could not start the MCP runtime: ${e instanceof Error?e.message:String(e)}`),this.emitChange()})}async start(){let e=++this.lifecycleGeneration;if(this.disconnectedServers.clear(),this.configuredServerNames().length===0)return;let[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([import(`../node/keyringTokenStore.mjs`),import(`../node/mcpRuntime.mjs`)]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configSources(),a=this.options.tokenStore??await t();await r.start({configSources:i,tokenStore:a,onAuthorizationUrl:(t,n)=>this.onAuthorizationUrl(t,n,e),onServerStateChange:t=>this.onServerStateChange(t,e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t,n){if(n!==this.lifecycleGeneration||this.disconnectedServers.has(t))return;this.authorizationUrls.set(t,e.toString()),this.emitChange();let r=this.browserAuthorizationRequests.has(t);Promise.resolve().then(()=>this.options.onAuthorizationUrl?.(e,t,{openBrowser:r})).catch(e=>{if(n!==this.lifecycleGeneration)return;let r=e instanceof Error?e.message:String(e);this.catalog.addDiagnostic(`Could not open authorization page for "${t}": ${r}`),this.emitChange()})}configSources(){return[...this.groups.sessionLocal.configSources,...this.groups.shared.configSources]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async disconnect(e){if(!this.configuredServerNames().includes(e))throw Error(`Unknown MCP server: ${e}`);let t=this.clientManager();if(!t)throw Error(s);let n=this.lifecycleGeneration;this.disconnectedServers.add(e);try{await t.disconnectServer(e)}catch(t){throw n===this.lifecycleGeneration&&this.disconnectedServers.delete(e),t}n===this.lifecycleGeneration&&(this.resourceCache.delete(e),this.authorizationUrls.delete(e),this.browserAuthorizationRequests.delete(e),this.catalog.applyStateChange({serverName:e,state:`closed`},[]),this.applyActiveTools(),this.emitChange())}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(s);this.disconnectedServers.delete(e),this.resourceCache.delete(e),this.authorizationUrls.delete(e)&&this.emitChange(),this.browserAuthorizationRequests.add(e);try{await t.disconnectServer(e),await t.ensureConnected(e)}finally{this.browserAuthorizationRequests.delete(e)}}async openAuthorizationPage(e){let t=this.authorizationUrls.get(e);if(!t)throw Error(`No authorization page is waiting for this MCP server.`);let n=this.options.onAuthorizationUrl;if(!n)throw Error(`This host cannot open authorization pages.`);await n(new URL(t),e,{openBrowser:!0})}getSnapshot(){return this.catalog.toSnapshot()}resolveToolSelectors(e){return this.catalog.resolveToolSelectors(e)}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,t){this.catalog.setDisabled(e,!t),this.applyActiveTools(),this.emitChange()}async listResources(e,t={}){if(this.disconnectedServers.has(e))throw Error(`MCP server is disconnected: ${e}`);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(s);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,this.browserAuthorizationRequests.clear(),this.authorizationUrls.clear(),this.resourceCache.clear();let e=this.runtime;this.runtime=void 0,await e?.dispose()}onServerStateChange(e,t){if(t!==this.lifecycleGeneration||this.disconnectedServers.has(e.serverName))return;e.state!==`connecting`&&e.state!==`needs-auth`&&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(t!==this.lifecycleGeneration||!n?.isCurrent(r)||this.disconnectedServers.has(e.serverName))return;let a=this.catalog.applyStateChange(e,i);this.registerTools(a),this.applyActiveTools(),this.emitChange()})}};export{d as McpSession,l as mcpConfigurationFingerprint};
1
+ import{buildMcpConfigGroups as e}from"../node/configSources.mjs";import{readCachedCatalog as t}from"../node/mcpRuntime.mjs";import{readSessionConfig as n}from"../process/sessionConfig.mjs";import{McpCatalog as r}from"../../services/mcpCatalog.mjs";import{readDirectToolFilter as i}from"../process/directToolsEnvironment.mjs";import{applyActiveTools as a,registerNewTools as o}from"./mcpTools.mjs";const s=`The MCP runtime has not started yet.`;function c(e){let t=e=>Array.isArray(e)?e.map(t):typeof e!=`object`||!e?e:Object.fromEntries(Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,n])=>[e,t(n)]));return JSON.stringify(t(e))}function l(e){return c({enabled:e.enabled??!0,repoRoot:e.repoRoot,stagingDirectory:e.stagingDirectory,generatedConfigPath:e.generatedConfigPath,pluginConfigPaths:e.pluginConfigPaths??[],sources:e.sources??[],allowlist:{servers:[...e.allowlist?.servers??[]].sort((e,t)=>e.localeCompare(t)),proxy:[...e.allowlist?.proxy??[]].sort((e,t)=>e.localeCompare(t))}})}function u(e){return c({serverName:e.serverName,toolName:e.toolName,inputSchema:e.inputSchema})}var d=class{pi;runtime;catalog;groups={shared:{configSources:[],configPaths:[],serverNames:[]},sessionLocal:{configSources:[],configPaths:[],serverNames:[]},droppedServers:[],diagnostics:[]};options;tokenStore;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;browserAuthorizationRequests=new Set;disconnectedServers=new Set;lifecycleGeneration=0;configurationFingerprint;registeredToolFingerprints=new Map;historicallyOwnedNames=new Set;incompatibleNames=new Set;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){this.applyActiveTools()}createCatalog(){return new r(i(this.options.environment))}install(e=n(this.options.environment)){return this.bindConfiguration(e),this.catalog.toSnapshot()}async reconfigure(e){if(l(e)===this.configurationFingerprint){this.runtime?.getServices()||this.startDetached();return}let t=++this.lifecycleGeneration,n=this.runtime;this.runtime=void 0,await n?.dispose(),t===this.lifecycleGeneration&&(this.resourceCache.clear(),this.authorizationUrls.clear(),this.browserAuthorizationRequests.clear(),this.bindConfiguration(e),this.applyActiveTools(),this.emitChange(),this.startDetached())}bindConfiguration(n){this.configurationFingerprint=l(n),this.groups=e(n),this.disconnectedServers.clear(),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];this.catalog.seed(t(this.configSources()),r);for(let e of this.groups.diagnostics)this.catalog.addDiagnostic(e);this.registerTools(this.catalog.allTools())}registerTools(e){let t=[];for(let n of e){let e=u(n),r=this.registeredToolFingerprints.get(n.piName);if(this.historicallyOwnedNames.add(n.piName),r===void 0){this.registeredToolFingerprints.set(n.piName,e),this.incompatibleNames.delete(n.piName),t.push(n);continue}if(r===e){this.incompatibleNames.delete(n.piName);continue}this.incompatibleNames.add(n.piName),this.catalog.addDiagnostic(`MCP tool "${n.piName}" changed identity or input schema and is hidden until Pi is relaunched.`)}o(this.pi,this.clientManager,t,e=>this.isToolAvailable(e))}isToolAvailable(e){if(this.disconnectedServers.has(e.serverName)||this.incompatibleNames.has(e.piName))return!1;let t=this.catalog.findTool(e.piName);return t!==void 0&&u(t)===u(e)}applyActiveTools(){let e=new Set(this.incompatibleNames);for(let t of this.catalog.allTools())this.disconnectedServers.has(t.serverName)&&e.add(t.piName);a(this.pi,this.catalog,this.historicallyOwnedNames,e)}startDetached(){this.start().catch(e=>{this.catalog.addDiagnostic(`Could not start the MCP runtime: ${e instanceof Error?e.message:String(e)}`),this.emitChange()})}async start(){let e=++this.lifecycleGeneration;if(this.disconnectedServers.clear(),this.configuredServerNames().length===0)return;let[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([import(`../node/keyringTokenStore.mjs`),import(`../node/mcpRuntime.mjs`)]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configSources(),a=this.options.tokenStore??await t();this.tokenStore=a,await r.start({configSources:i,tokenStore:a,onAuthorizationUrl:(t,n)=>this.onAuthorizationUrl(t,n,e),onServerStateChange:t=>this.onServerStateChange(t,e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t,n){if(n!==this.lifecycleGeneration||this.disconnectedServers.has(t))return;this.authorizationUrls.set(t,e.toString()),this.emitChange();let r=this.browserAuthorizationRequests.has(t);Promise.resolve().then(()=>this.options.onAuthorizationUrl?.(e,t,{openBrowser:r})).catch(e=>{if(n!==this.lifecycleGeneration)return;let r=e instanceof Error?e.message:String(e);this.catalog.addDiagnostic(`Could not open authorization page for "${t}": ${r}`),this.emitChange()})}configSources(){return[...this.groups.sessionLocal.configSources,...this.groups.shared.configSources]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async disconnect(e){if(!this.configuredServerNames().includes(e))throw Error(`Unknown MCP server: ${e}`);let t=this.clientManager();if(!t)throw Error(s);let n=this.lifecycleGeneration;this.disconnectedServers.add(e);try{await t.disconnectServer(e)}catch(t){throw n===this.lifecycleGeneration&&this.disconnectedServers.delete(e),t}n===this.lifecycleGeneration&&(this.resourceCache.delete(e),this.authorizationUrls.delete(e),this.browserAuthorizationRequests.delete(e),this.catalog.applyStateChange({serverName:e,state:`closed`},[]),this.applyActiveTools(),this.emitChange())}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(s);this.disconnectedServers.delete(e),this.resourceCache.delete(e),this.authorizationUrls.delete(e)&&this.emitChange(),this.browserAuthorizationRequests.add(e);try{await t.disconnectServer(e),await this.tokenStore?.clear(e),await t.ensureConnected(e)}finally{this.browserAuthorizationRequests.delete(e)}}async openAuthorizationPage(e){let t=this.authorizationUrls.get(e);if(!t)throw Error(`No authorization page is waiting for this MCP server.`);let n=this.options.onAuthorizationUrl;if(!n)throw Error(`This host cannot open authorization pages.`);await n(new URL(t),e,{openBrowser:!0})}getSnapshot(){return this.catalog.toSnapshot()}resolveToolSelectors(e){return this.catalog.resolveToolSelectors(e)}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,t){this.catalog.setDisabled(e,!t),this.applyActiveTools(),this.emitChange()}async listResources(e,t={}){if(this.disconnectedServers.has(e))throw Error(`MCP server is disconnected: ${e}`);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(s);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,this.browserAuthorizationRequests.clear(),this.authorizationUrls.clear(),this.resourceCache.clear();let e=this.runtime;this.runtime=void 0,await e?.dispose()}onServerStateChange(e,t){if(t!==this.lifecycleGeneration||this.disconnectedServers.has(e.serverName))return;e.state!==`connecting`&&e.state!==`needs-auth`&&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(t!==this.lifecycleGeneration||!n?.isCurrent(r)||this.disconnectedServers.has(e.serverName))return;let a=this.catalog.applyStateChange(e,i);this.registerTools(a),this.applyActiveTools(),this.emitChange()})}};export{d as McpSession,l as mcpConfigurationFingerprint};
2
2
  //# sourceMappingURL=mcpSession.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../../src/adapters/pi/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { DoomMcpResolvedToolSelection } from '@agimon-ai/doompi-extension-contracts/mcp-tool-resolver';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { type CatalogTool, McpCatalog } from '../../services/mcpCatalog.ts';\nimport type { McpResourceView, McpServerView } from '../../types/mcp.ts';\nimport type { McpConfigGroups, McpConfigSource, McpSessionConfig } from '../../types/mcpConfig.ts';\nimport { buildMcpConfigGroups } from '../node/configSources.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from '../node/mcpRuntime.ts';\nimport { readDirectToolFilter } from '../process/directToolsEnvironment.ts';\nimport { readSessionConfig } from '../process/sessionConfig.ts';\nimport { applyActiveTools, registerNewTools } from './mcpTools.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.';\nconst AUTHORIZATION_URL_NOT_READY = 'No authorization page is waiting for this MCP server.';\n\nfunction canonicalJson(value: unknown): string {\n const normalize = (candidate: unknown): unknown => {\n if (Array.isArray(candidate)) return candidate.map(normalize);\n if (typeof candidate !== 'object' || candidate === null) return candidate;\n return Object.fromEntries(\n Object.entries(candidate as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => [key, normalize(child)]),\n );\n };\n return JSON.stringify(normalize(value));\n}\n\nexport function mcpConfigurationFingerprint(configuration: McpSessionConfig): string {\n return canonicalJson({\n enabled: configuration.enabled ?? true,\n repoRoot: configuration.repoRoot,\n stagingDirectory: configuration.stagingDirectory,\n generatedConfigPath: configuration.generatedConfigPath,\n pluginConfigPaths: configuration.pluginConfigPaths ?? [],\n sources: configuration.sources ?? [],\n allowlist: {\n servers: [...(configuration.allowlist?.servers ?? [])].sort((left, right) => left.localeCompare(right)),\n proxy: [...(configuration.allowlist?.proxy ?? [])].sort((left, right) => left.localeCompare(right)),\n },\n });\n}\n\nfunction toolRegistrationFingerprint(tool: CatalogTool): string {\n return canonicalJson({\n serverName: tool.serverName,\n toolName: tool.toolName,\n inputSchema: tool.inputSchema,\n });\n}\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Surfaces an OAuth URL, opening it when an explicit authorization requested that. */\n onAuthorizationUrl?: (url: URL, serverName: string, options: { openBrowser: boolean }) => void | Promise<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: { configSources: [], configPaths: [], serverNames: [] },\n sessionLocal: { configSources: [], configPaths: [], serverNames: [] },\n droppedServers: [],\n diagnostics: [],\n };\n private readonly options: McpSessionOptions;\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 /** Servers whose explicit auth action should launch the browser when its URL arrives. */\n private readonly browserAuthorizationRequests = new Set<string>();\n /** Explicit session disconnects fence late discovery and stale tool invocations. */\n private readonly disconnectedServers = new Set<string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n private configurationFingerprint: string | undefined;\n /** Pi wrappers survive configuration changes because the host cannot unregister them. */\n private readonly registeredToolFingerprints = new Map<string, string>();\n private readonly historicallyOwnedNames = new Set<string>();\n private readonly incompatibleNames = new Set<string>();\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.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 this.applyActiveTools();\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.bindConfiguration(configuration);\n return this.catalog.toSnapshot();\n }\n\n /** Replaces the complete MCP projection and retires the previous live owner. */\n async reconfigure(configuration: McpSessionConfig): Promise<void> {\n const fingerprint = mcpConfigurationFingerprint(configuration);\n if (fingerprint === this.configurationFingerprint) {\n if (!this.runtime?.getServices()) this.startDetached();\n return;\n }\n\n const generation = ++this.lifecycleGeneration;\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.dispose();\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.clear();\n this.authorizationUrls.clear();\n this.browserAuthorizationRequests.clear();\n this.bindConfiguration(configuration);\n this.applyActiveTools();\n this.emitChange();\n this.startDetached();\n }\n\n private bindConfiguration(configuration: McpSessionConfig): void {\n this.configurationFingerprint = mcpConfigurationFingerprint(configuration);\n this.groups = buildMcpConfigGroups(configuration);\n this.disconnectedServers.clear();\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configSources()), configuredServers);\n for (const diagnostic of this.groups.diagnostics) this.catalog.addDiagnostic(diagnostic);\n this.registerTools(this.catalog.allTools());\n }\n\n private registerTools(tools: readonly CatalogTool[]): void {\n const unseen: CatalogTool[] = [];\n for (const tool of tools) {\n const fingerprint = toolRegistrationFingerprint(tool);\n const registered = this.registeredToolFingerprints.get(tool.piName);\n this.historicallyOwnedNames.add(tool.piName);\n if (registered === undefined) {\n this.registeredToolFingerprints.set(tool.piName, fingerprint);\n this.incompatibleNames.delete(tool.piName);\n unseen.push(tool);\n continue;\n }\n if (registered === fingerprint) {\n this.incompatibleNames.delete(tool.piName);\n continue;\n }\n this.incompatibleNames.add(tool.piName);\n this.catalog.addDiagnostic(\n `MCP tool \"${tool.piName}\" changed identity or input schema and is hidden until Pi is relaunched.`,\n );\n }\n registerNewTools(this.pi, this.clientManager, unseen, (tool) => this.isToolAvailable(tool));\n }\n\n private isToolAvailable(registeredTool: CatalogTool): boolean {\n if (this.disconnectedServers.has(registeredTool.serverName) || this.incompatibleNames.has(registeredTool.piName))\n return false;\n const current = this.catalog.findTool(registeredTool.piName);\n return (\n current !== undefined && toolRegistrationFingerprint(current) === toolRegistrationFingerprint(registeredTool)\n );\n }\n\n private applyActiveTools(): void {\n const unavailable = new Set(this.incompatibleNames);\n for (const tool of this.catalog.allTools()) {\n if (this.disconnectedServers.has(tool.serverName)) unavailable.add(tool.piName);\n }\n applyActiveTools(this.pi, this.catalog, this.historicallyOwnedNames, unavailable);\n }\n\n private startDetached(): void {\n void this.start().catch((error: unknown) => {\n this.catalog.addDiagnostic(\n `Could not start the MCP runtime: ${error instanceof Error ? error.message : String(error)}`,\n );\n this.emitChange();\n });\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 this.disconnectedServers.clear();\n // Explicit disabled and empty Doom projections must not instantiate any\n // runtime collaborator, including the keyring-backed token store.\n if (this.configuredServerNames().length === 0) return;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('../node/keyringTokenStore.ts'),\n import('../node/mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\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 const configSources = this.configSources();\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configSources,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName, generation),\n onServerStateChange: (change) => this.onServerStateChange(change, generation),\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. Host work is\n * detached so a failed desktop browser launch never cancels a still-usable OAuth\n * callback; the overlay retains a clickable fallback and a retry action.\n */\n private onAuthorizationUrl(url: URL, serverName: string, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(serverName)) return;\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n const openBrowser = this.browserAuthorizationRequests.has(serverName);\n void Promise.resolve()\n .then(() => this.options.onAuthorizationUrl?.(url, serverName, { openBrowser }))\n .catch((error: unknown) => {\n if (generation !== this.lifecycleGeneration) return;\n const detail = error instanceof Error ? error.message : String(error);\n this.catalog.addDiagnostic(`Could not open authorization page for \"${serverName}\": ${detail}`);\n this.emitChange();\n });\n }\n\n /** Ordered layers for the one in-process client container. */\n private configSources(): McpConfigSource[] {\n return [...this.groups.sessionLocal.configSources, ...this.groups.shared.configSources];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /** Closes only this session's connection. Saved OAuth credentials are untouched. */\n async disconnect(serverName: string): Promise<void> {\n if (!this.configuredServerNames().includes(serverName)) throw new Error(`Unknown MCP server: ${serverName}`);\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n const generation = this.lifecycleGeneration;\n this.disconnectedServers.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n } catch (error) {\n if (generation === this.lifecycleGeneration) this.disconnectedServers.delete(serverName);\n throw error;\n }\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.delete(serverName);\n this.authorizationUrls.delete(serverName);\n this.browserAuthorizationRequests.delete(serverName);\n this.catalog.applyStateChange({ serverName, state: 'closed' }, []);\n this.applyActiveTools();\n this.emitChange();\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 this.disconnectedServers.delete(serverName);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n if (this.authorizationUrls.delete(serverName)) this.emitChange();\n this.browserAuthorizationRequests.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n } finally {\n this.browserAuthorizationRequests.delete(serverName);\n }\n }\n\n /** Reopens the URL already reserved for a flow without restarting that flow. */\n async openAuthorizationPage(serverName: string): Promise<void> {\n const authorizationUrl = this.authorizationUrls.get(serverName);\n if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);\n const onAuthorizationUrl = this.options.onAuthorizationUrl;\n if (!onAuthorizationUrl) throw new Error('This host cannot open authorization pages.');\n await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n resolveToolSelectors(selectors: readonly string[]): readonly DoomMcpResolvedToolSelection[] {\n return this.catalog.resolveToolSelectors(selectors);\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: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\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 this.applyActiveTools();\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 (this.disconnectedServers.has(serverName)) throw new Error(`MCP server is disconnected: ${serverName}`);\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 this.browserAuthorizationRequests.clear();\n this.authorizationUrls.clear();\n this.resourceCache.clear();\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.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, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(change.serverName)) return;\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' && change.state !== 'needs-auth') {\n this.authorizationUrls.delete(change.serverName);\n }\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 (\n generation !== this.lifecycleGeneration ||\n !runtime?.isCurrent(services) ||\n this.disconnectedServers.has(change.serverName)\n )\n return;\n const added = this.catalog.applyStateChange(change, tools);\n this.registerTools(added);\n this.applyActiveTools();\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../../src/adapters/pi/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { DoomMcpResolvedToolSelection } from '@agimon-ai/doompi-extension-contracts/mcp-tool-resolver';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { type CatalogTool, McpCatalog } from '../../services/mcpCatalog.ts';\nimport type { McpResourceView, McpServerView } from '../../types/mcp.ts';\nimport type { McpConfigGroups, McpConfigSource, McpSessionConfig } from '../../types/mcpConfig.ts';\nimport { buildMcpConfigGroups } from '../node/configSources.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from '../node/mcpRuntime.ts';\nimport { readDirectToolFilter } from '../process/directToolsEnvironment.ts';\nimport { readSessionConfig } from '../process/sessionConfig.ts';\nimport { applyActiveTools, registerNewTools } from './mcpTools.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.';\nconst AUTHORIZATION_URL_NOT_READY = 'No authorization page is waiting for this MCP server.';\n\nfunction canonicalJson(value: unknown): string {\n const normalize = (candidate: unknown): unknown => {\n if (Array.isArray(candidate)) return candidate.map(normalize);\n if (typeof candidate !== 'object' || candidate === null) return candidate;\n return Object.fromEntries(\n Object.entries(candidate as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => [key, normalize(child)]),\n );\n };\n return JSON.stringify(normalize(value));\n}\n\nexport function mcpConfigurationFingerprint(configuration: McpSessionConfig): string {\n return canonicalJson({\n enabled: configuration.enabled ?? true,\n repoRoot: configuration.repoRoot,\n stagingDirectory: configuration.stagingDirectory,\n generatedConfigPath: configuration.generatedConfigPath,\n pluginConfigPaths: configuration.pluginConfigPaths ?? [],\n sources: configuration.sources ?? [],\n allowlist: {\n servers: [...(configuration.allowlist?.servers ?? [])].sort((left, right) => left.localeCompare(right)),\n proxy: [...(configuration.allowlist?.proxy ?? [])].sort((left, right) => left.localeCompare(right)),\n },\n });\n}\n\nfunction toolRegistrationFingerprint(tool: CatalogTool): string {\n return canonicalJson({\n serverName: tool.serverName,\n toolName: tool.toolName,\n inputSchema: tool.inputSchema,\n });\n}\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Surfaces an OAuth URL, opening it when an explicit authorization requested that. */\n onAuthorizationUrl?: (url: URL, serverName: string, options: { openBrowser: boolean }) => void | Promise<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: { configSources: [], configPaths: [], serverNames: [] },\n sessionLocal: { configSources: [], configPaths: [], serverNames: [] },\n droppedServers: [],\n diagnostics: [],\n };\n private readonly options: McpSessionOptions;\n /** Credential persistence used by the active runtime and explicit reauthorization. */\n private tokenStore: TokenStore | 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 /** Servers whose explicit auth action should launch the browser when its URL arrives. */\n private readonly browserAuthorizationRequests = new Set<string>();\n /** Explicit session disconnects fence late discovery and stale tool invocations. */\n private readonly disconnectedServers = new Set<string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n private configurationFingerprint: string | undefined;\n /** Pi wrappers survive configuration changes because the host cannot unregister them. */\n private readonly registeredToolFingerprints = new Map<string, string>();\n private readonly historicallyOwnedNames = new Set<string>();\n private readonly incompatibleNames = new Set<string>();\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.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 this.applyActiveTools();\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.bindConfiguration(configuration);\n return this.catalog.toSnapshot();\n }\n\n /** Replaces the complete MCP projection and retires the previous live owner. */\n async reconfigure(configuration: McpSessionConfig): Promise<void> {\n const fingerprint = mcpConfigurationFingerprint(configuration);\n if (fingerprint === this.configurationFingerprint) {\n if (!this.runtime?.getServices()) this.startDetached();\n return;\n }\n\n const generation = ++this.lifecycleGeneration;\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.dispose();\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.clear();\n this.authorizationUrls.clear();\n this.browserAuthorizationRequests.clear();\n this.bindConfiguration(configuration);\n this.applyActiveTools();\n this.emitChange();\n this.startDetached();\n }\n\n private bindConfiguration(configuration: McpSessionConfig): void {\n this.configurationFingerprint = mcpConfigurationFingerprint(configuration);\n this.groups = buildMcpConfigGroups(configuration);\n this.disconnectedServers.clear();\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configSources()), configuredServers);\n for (const diagnostic of this.groups.diagnostics) this.catalog.addDiagnostic(diagnostic);\n this.registerTools(this.catalog.allTools());\n }\n\n private registerTools(tools: readonly CatalogTool[]): void {\n const unseen: CatalogTool[] = [];\n for (const tool of tools) {\n const fingerprint = toolRegistrationFingerprint(tool);\n const registered = this.registeredToolFingerprints.get(tool.piName);\n this.historicallyOwnedNames.add(tool.piName);\n if (registered === undefined) {\n this.registeredToolFingerprints.set(tool.piName, fingerprint);\n this.incompatibleNames.delete(tool.piName);\n unseen.push(tool);\n continue;\n }\n if (registered === fingerprint) {\n this.incompatibleNames.delete(tool.piName);\n continue;\n }\n this.incompatibleNames.add(tool.piName);\n this.catalog.addDiagnostic(\n `MCP tool \"${tool.piName}\" changed identity or input schema and is hidden until Pi is relaunched.`,\n );\n }\n registerNewTools(this.pi, this.clientManager, unseen, (tool) => this.isToolAvailable(tool));\n }\n\n private isToolAvailable(registeredTool: CatalogTool): boolean {\n if (this.disconnectedServers.has(registeredTool.serverName) || this.incompatibleNames.has(registeredTool.piName))\n return false;\n const current = this.catalog.findTool(registeredTool.piName);\n return (\n current !== undefined && toolRegistrationFingerprint(current) === toolRegistrationFingerprint(registeredTool)\n );\n }\n\n private applyActiveTools(): void {\n const unavailable = new Set(this.incompatibleNames);\n for (const tool of this.catalog.allTools()) {\n if (this.disconnectedServers.has(tool.serverName)) unavailable.add(tool.piName);\n }\n applyActiveTools(this.pi, this.catalog, this.historicallyOwnedNames, unavailable);\n }\n\n private startDetached(): void {\n void this.start().catch((error: unknown) => {\n this.catalog.addDiagnostic(\n `Could not start the MCP runtime: ${error instanceof Error ? error.message : String(error)}`,\n );\n this.emitChange();\n });\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 this.disconnectedServers.clear();\n // Explicit disabled and empty Doom projections must not instantiate any\n // runtime collaborator, including the keyring-backed token store.\n if (this.configuredServerNames().length === 0) return;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('../node/keyringTokenStore.ts'),\n import('../node/mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\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 const configSources = this.configSources();\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n this.tokenStore = tokenStore;\n await runtime.start({\n configSources,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName, generation),\n onServerStateChange: (change) => this.onServerStateChange(change, generation),\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. Host work is\n * detached so a failed desktop browser launch never cancels a still-usable OAuth\n * callback; the overlay retains a clickable fallback and a retry action.\n */\n private onAuthorizationUrl(url: URL, serverName: string, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(serverName)) return;\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n const openBrowser = this.browserAuthorizationRequests.has(serverName);\n void Promise.resolve()\n .then(() => this.options.onAuthorizationUrl?.(url, serverName, { openBrowser }))\n .catch((error: unknown) => {\n if (generation !== this.lifecycleGeneration) return;\n const detail = error instanceof Error ? error.message : String(error);\n this.catalog.addDiagnostic(`Could not open authorization page for \"${serverName}\": ${detail}`);\n this.emitChange();\n });\n }\n\n /** Ordered layers for the one in-process client container. */\n private configSources(): McpConfigSource[] {\n return [...this.groups.sessionLocal.configSources, ...this.groups.shared.configSources];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /** Closes only this session's connection. Saved OAuth credentials are untouched. */\n async disconnect(serverName: string): Promise<void> {\n if (!this.configuredServerNames().includes(serverName)) throw new Error(`Unknown MCP server: ${serverName}`);\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n const generation = this.lifecycleGeneration;\n this.disconnectedServers.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n } catch (error) {\n if (generation === this.lifecycleGeneration) this.disconnectedServers.delete(serverName);\n throw error;\n }\n if (generation !== this.lifecycleGeneration) return;\n this.resourceCache.delete(serverName);\n this.authorizationUrls.delete(serverName);\n this.browserAuthorizationRequests.delete(serverName);\n this.catalog.applyStateChange({ serverName, state: 'closed' }, []);\n this.applyActiveTools();\n this.emitChange();\n }\n\n /**\n * Reconnects one server with a fresh OAuth registration.\n *\n * The connection is dropped before credentials are cleared so a failed disconnect\n * does not silently sign the user out. Clearing both tokens and dynamic client\n * information prevents a server-side registration reset from replaying a stale\n * `client_id` forever.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n this.disconnectedServers.delete(serverName);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n if (this.authorizationUrls.delete(serverName)) this.emitChange();\n this.browserAuthorizationRequests.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n await this.tokenStore?.clear(serverName);\n await clientManager.ensureConnected(serverName);\n } finally {\n this.browserAuthorizationRequests.delete(serverName);\n }\n }\n\n /** Reopens the URL already reserved for a flow without restarting that flow. */\n async openAuthorizationPage(serverName: string): Promise<void> {\n const authorizationUrl = this.authorizationUrls.get(serverName);\n if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);\n const onAuthorizationUrl = this.options.onAuthorizationUrl;\n if (!onAuthorizationUrl) throw new Error('This host cannot open authorization pages.');\n await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n resolveToolSelectors(selectors: readonly string[]): readonly DoomMcpResolvedToolSelection[] {\n return this.catalog.resolveToolSelectors(selectors);\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: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\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 this.applyActiveTools();\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 (this.disconnectedServers.has(serverName)) throw new Error(`MCP server is disconnected: ${serverName}`);\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 this.browserAuthorizationRequests.clear();\n this.authorizationUrls.clear();\n this.resourceCache.clear();\n const retiredRuntime = this.runtime;\n this.runtime = undefined;\n await retiredRuntime?.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, generation: number): void {\n if (generation !== this.lifecycleGeneration || this.disconnectedServers.has(change.serverName)) return;\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' && change.state !== 'needs-auth') {\n this.authorizationUrls.delete(change.serverName);\n }\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 (\n generation !== this.lifecycleGeneration ||\n !runtime?.isCurrent(services) ||\n this.disconnectedServers.has(change.serverName)\n )\n return;\n const added = this.catalog.applyStateChange(change, tools);\n this.registerTools(added);\n this.applyActiveTools();\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-mcp",
3
- "version": "0.0.1-alpha.67",
3
+ "version": "0.0.1-alpha.69",
4
4
  "description": "Domain-aware MCP server selection and access boundaries for Pi sessions composed with DoomPi.",
5
5
  "keywords": [
6
6
  "ai",
@@ -34,7 +34,8 @@
34
34
  "src/prompts",
35
35
  "README.md",
36
36
  "LICENSE",
37
- "package.json"
37
+ "package.json",
38
+ "!src/web/**/*.stories.tsx"
38
39
  ],
39
40
  "type": "module",
40
41
  "main": "./dist/index.cjs",
@@ -69,11 +70,11 @@
69
70
  "open": "11.0.2",
70
71
  "typebox": "1.3.25",
71
72
  "yaml": "2.9.0",
72
- "@agimon-ai/doompi-config": "0.0.1-alpha.66",
73
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.66",
74
- "@agimon-ai/doompi-ui": "0.0.1-alpha.67",
75
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.26",
76
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.29"
73
+ "@agimon-ai/doompi-config": "0.0.1-alpha.67",
74
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.67",
75
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.30",
76
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.68",
77
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.27"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@earendil-works/pi-coding-agent": "0.85.1",
@@ -84,6 +85,7 @@
84
85
  "@types/react": "19.2.18",
85
86
  "@vitest/coverage-v8": "5.0.0",
86
87
  "react": "19.2.8",
88
+ "react-dom": "19.2.8",
87
89
  "tsdown": "0.23.0",
88
90
  "typescript": "7.0.2",
89
91
  "vitest": "5.0.0"
@@ -52,7 +52,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
52
52
  }, [authorizationId, authorizationStatus, repositoryId, request]);
53
53
 
54
54
  if (!repository) {
55
- return <p className="py-3 text-[10px] text-doom-faint">Select a repository to inspect its synced MCP catalog.</p>;
55
+ return <p className="py-3 text-xs text-doom-faint">Select a repository to inspect its synced MCP catalog.</p>;
56
56
  }
57
57
 
58
58
  const catalog = state.repositoryId === repository.id ? state.catalog : undefined;
@@ -63,7 +63,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
63
63
  <div className="flex flex-col gap-3" data-testid="mcp-repository-settings">
64
64
  <div className="flex flex-col items-stretch gap-2 rounded border border-doom-border bg-doom-panel px-3 py-2 min-[560px]:flex-row min-[560px]:items-center">
65
65
  <Badge>{catalog?.sync.fresh ? 'synced' : 'sync required'}</Badge>
66
- <span className="min-w-0 text-[10px] leading-relaxed text-doom-muted min-[560px]:flex-1">
66
+ <span className="min-w-0 text-xs leading-relaxed text-doom-muted min-[560px]:flex-1">
67
67
  {catalog?.sync.fresh
68
68
  ? 'Cached inspection is local. Discovery contacts the configured servers.'
69
69
  : catalog
@@ -76,7 +76,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
76
76
  loading={busy === 'loading'}
77
77
  disabled={busy !== undefined}
78
78
  onClick={() => void loadMcpSettings(request, repository.id)}
79
- className="w-full text-[10px] min-[560px]:w-auto"
79
+ className="w-full text-xs min-[560px]:w-auto"
80
80
  >
81
81
  refresh cache
82
82
  </Button>
@@ -86,7 +86,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
86
86
  loading={busy === 'discovering'}
87
87
  disabled={busy !== undefined || !catalog?.sync.fresh}
88
88
  onClick={() => setConfirmDiscovery(true)}
89
- className="w-full text-[10px] min-[560px]:w-auto"
89
+ className="w-full text-xs min-[560px]:w-auto"
90
90
  >
91
91
  discover live
92
92
  </Button>
@@ -94,7 +94,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
94
94
 
95
95
  {!confirmDiscovery ? null : (
96
96
  <div className="flex flex-col items-stretch gap-2 rounded border border-doom-accent/50 bg-doom-accent/5 px-3 py-2 min-[560px]:flex-row min-[560px]:items-center">
97
- <span className="min-w-0 text-[10px] leading-relaxed text-doom-hi min-[560px]:flex-1">
97
+ <span className="min-w-0 text-xs leading-relaxed text-doom-hi min-[560px]:flex-1">
98
98
  Discovery starts configured processes and contacts remote MCP servers. Continue?
99
99
  </span>
100
100
  <Button
@@ -104,7 +104,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
104
104
  setConfirmDiscovery(false);
105
105
  void discoverMcpSettings(requestWithStepUp, repository.id);
106
106
  }}
107
- className="w-full text-[10px] min-[560px]:w-auto"
107
+ className="w-full text-xs min-[560px]:w-auto"
108
108
  >
109
109
  confirm discovery
110
110
  </Button>
@@ -112,23 +112,23 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
112
112
  variant="ghost"
113
113
  size="xs"
114
114
  onClick={() => setConfirmDiscovery(false)}
115
- className="w-full text-[10px] min-[560px]:w-auto"
115
+ className="w-full text-xs min-[560px]:w-auto"
116
116
  >
117
117
  cancel
118
118
  </Button>
119
119
  </div>
120
120
  )}
121
121
 
122
- {!error ? null : <p className="rounded border border-doom-red/40 px-3 py-2 text-[10px] text-doom-red">{error}</p>}
122
+ {!error ? null : <p className="rounded border border-doom-red/40 px-3 py-2 text-xs text-doom-red">{error}</p>}
123
123
 
124
124
  {authorization ? (
125
125
  <div className="flex flex-col gap-2 rounded border border-doom-border bg-doom-panel px-3 py-2">
126
126
  <div className="flex flex-wrap items-center gap-2">
127
- <span className="min-w-0 basis-full text-[10px] font-bold text-doom-hi min-[560px]:basis-auto">
127
+ <span className="min-w-0 basis-full text-xs font-bold text-doom-hi min-[560px]:basis-auto">
128
128
  {authorization.serverName}
129
129
  </span>
130
130
  <Badge>{authorization.status}</Badge>
131
- <span className="min-w-0 flex-1 text-[9px] text-doom-faint">authorization flow</span>
131
+ <span className="min-w-0 flex-1 text-2xs text-doom-faint">authorization flow</span>
132
132
  {TERMINAL_AUTHORIZATION.has(authorization.status) ? null : (
133
133
  <Button
134
134
  variant="ghost"
@@ -136,7 +136,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
136
136
  loading={busy === 'cancelling'}
137
137
  disabled={busy !== undefined}
138
138
  onClick={() => void cancelMcpFlow(requestWithStepUp, repository.id, authorization.id)}
139
- className="w-full text-[10px] min-[560px]:w-auto"
139
+ className="w-full text-xs min-[560px]:w-auto"
140
140
  >
141
141
  cancel
142
142
  </Button>
@@ -147,17 +147,17 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
147
147
  href={authorization.authorizationUrl}
148
148
  target="_blank"
149
149
  rel="noreferrer noopener"
150
- className="w-fit text-[10px] text-doom-accent underline underline-offset-2"
150
+ className="w-fit text-xs text-doom-accent underline underline-offset-2"
151
151
  >
152
152
  open authorization page
153
153
  </a>
154
154
  )}
155
- {!authorization.error ? null : <p className="text-[9px] text-doom-red">{authorization.error}</p>}
155
+ {!authorization.error ? null : <p className="text-2xs text-doom-red">{authorization.error}</p>}
156
156
  </div>
157
157
  ) : null}
158
158
 
159
159
  {!catalog || catalog.servers.length === 0 ? (
160
- <p className="rounded border border-dashed border-doom-border px-3 py-4 text-[10px] text-doom-faint">
160
+ <p className="rounded border border-dashed border-doom-border px-3 py-4 text-xs text-doom-faint">
161
161
  {busy === 'loading' ? 'Reading the cached catalog.' : 'No MCP servers are enabled in the synced selection.'}
162
162
  </p>
163
163
  ) : (
@@ -168,7 +168,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
168
168
  className="flex flex-col gap-2 rounded border border-doom-border bg-doom-panel px-3 py-2"
169
169
  >
170
170
  <div className="flex flex-wrap items-center gap-2">
171
- <span className="min-w-0 basis-full truncate text-[11px] font-bold text-doom-hi min-[560px]:basis-auto min-[560px]:flex-1">
171
+ <span className="min-w-0 basis-full truncate text-sm font-bold text-doom-hi min-[560px]:basis-auto min-[560px]:flex-1">
172
172
  {server.name}
173
173
  </span>
174
174
  <Badge>{server.state}</Badge>
@@ -179,14 +179,14 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
179
179
  size="xs"
180
180
  disabled={busy !== undefined || !catalog.sync.fresh}
181
181
  onClick={() => setConfirmAuthorization(server.name)}
182
- className="w-full text-[10px] min-[560px]:w-auto"
182
+ className="w-full text-xs min-[560px]:w-auto"
183
183
  >
184
184
  authorize
185
185
  </Button>
186
186
  </div>
187
187
  {confirmAuthorization !== server.name ? null : (
188
188
  <div className="flex flex-col items-stretch gap-2 border-t border-doom-border pt-2 min-[560px]:flex-row min-[560px]:items-center">
189
- <span className="min-w-0 text-[9px] leading-relaxed text-doom-muted min-[560px]:flex-1">
189
+ <span className="min-w-0 text-2xs leading-relaxed text-doom-muted min-[560px]:flex-1">
190
190
  Start a bounded OAuth attempt for this server? A passkey may be required.
191
191
  </span>
192
192
  <Button
@@ -196,7 +196,7 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
196
196
  setConfirmAuthorization(undefined);
197
197
  void authorizeMcpServer(requestWithStepUp, repository.id, server.name);
198
198
  }}
199
- className="w-full text-[10px] min-[560px]:w-auto"
199
+ className="w-full text-xs min-[560px]:w-auto"
200
200
  >
201
201
  continue
202
202
  </Button>
@@ -204,22 +204,22 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
204
204
  variant="ghost"
205
205
  size="xs"
206
206
  onClick={() => setConfirmAuthorization(undefined)}
207
- className="w-full text-[10px] min-[560px]:w-auto"
207
+ className="w-full text-xs min-[560px]:w-auto"
208
208
  >
209
209
  cancel
210
210
  </Button>
211
211
  </div>
212
212
  )}
213
213
  <details>
214
- <summary className="cursor-pointer text-[9px] text-doom-muted">
214
+ <summary className="cursor-pointer text-2xs text-doom-muted">
215
215
  {String(server.tools.length)} cached {server.tools.length === 1 ? 'tool' : 'tools'}
216
216
  </summary>
217
217
  <div className="mt-2 flex flex-col gap-1 border-t border-doom-border pt-2">
218
218
  {server.tools.length === 0 ? (
219
- <span className="text-[9px] text-doom-faint">Run discovery to populate capabilities.</span>
219
+ <span className="text-2xs text-doom-faint">Run discovery to populate capabilities.</span>
220
220
  ) : (
221
221
  server.tools.map((tool) => (
222
- <div key={tool.piName} className="flex min-w-0 gap-2 text-[9px]">
222
+ <div key={tool.piName} className="flex min-w-0 gap-2 text-2xs">
223
223
  <code className="shrink-0 text-doom-hi">{tool.name}</code>
224
224
  <span className="truncate text-doom-faint">{tool.description ?? tool.piName}</span>
225
225
  </div>
@@ -227,19 +227,19 @@ export function McpRepositorySettingsPanel({ repository, request, requestWithSte
227
227
  )}
228
228
  </div>
229
229
  </details>
230
- {!server.error ? null : <p className="text-[9px] text-doom-red">{server.error}</p>}
230
+ {!server.error ? null : <p className="text-2xs text-doom-red">{server.error}</p>}
231
231
  </div>
232
232
  ))}
233
233
  </div>
234
234
  )}
235
235
 
236
236
  {catalog?.droppedServers.length ? (
237
- <p className="text-[9px] text-doom-faint">
237
+ <p className="text-2xs text-doom-faint">
238
238
  Policy omitted {String(catalog.droppedServers.length)} configured servers.
239
239
  </p>
240
240
  ) : null}
241
241
  {catalog?.diagnostics.map((diagnostic) => (
242
- <p key={diagnostic} className="text-[9px] text-doom-red">
242
+ <p key={diagnostic} className="text-2xs text-doom-red">
243
243
  {diagnostic}
244
244
  </p>
245
245
  ))}
@@ -102,8 +102,8 @@ export function McpSessionAuthSection({
102
102
  {servers.length === 0 ? null : (
103
103
  <section data-testid="context-mcp-auth" className="flex flex-col gap-2 border-b border-doom-border px-3 py-3">
104
104
  <div className="flex flex-col gap-0.5">
105
- <p className="text-[9px] font-bold uppercase tracking-wide text-doom-faint">MCP servers</p>
106
- <p className="text-[9px] leading-relaxed text-doom-muted">
105
+ <p className="text-2xs font-bold uppercase tracking-wide text-doom-faint">MCP servers</p>
106
+ <p className="text-2xs leading-relaxed text-doom-muted">
107
107
  Open sign-in in a new tab, or copy the link from the authorization dialog.
108
108
  </p>
109
109
  </div>
@@ -115,8 +115,8 @@ export function McpSessionAuthSection({
115
115
  return (
116
116
  <li key={item.name} className="flex min-w-0 flex-col gap-1 px-1 py-0.5">
117
117
  <div className="flex min-w-0 items-center gap-2">
118
- <span className="min-w-0 flex-1 truncate text-[10px] text-doom-hi">{item.name}</span>
119
- <span className="text-[9px] text-doom-muted">{item.state.replace(/-/g, ' ')}</span>
118
+ <span className="min-w-0 flex-1 truncate text-xs text-doom-hi">{item.name}</span>
119
+ <span className="text-2xs text-doom-muted">{item.state.replace(/-/g, ' ')}</span>
120
120
  <Button
121
121
  variant="subtle"
122
122
  size="xs"
@@ -133,23 +133,23 @@ export function McpSessionAuthSection({
133
133
  setTarget({ sessionId, name: item.name });
134
134
  }
135
135
  }}
136
- className="shrink-0 text-[8px] font-bold"
136
+ className="shrink-0 text-2xs font-bold"
137
137
  >
138
138
  {item.state === 'connected' ? 'manage' : 'authorize'}
139
139
  </Button>
140
140
  </div>
141
141
  {tools.length === 0 ? (
142
- <p className="pl-3 text-[9px] text-doom-faint">no tools reported</p>
142
+ <p className="pl-3 text-2xs text-doom-faint">no tools reported</p>
143
143
  ) : (
144
144
  <ul aria-label={`${item.name} tools`} className="flex flex-col">
145
145
  {tools.map((tool) => (
146
146
  <li key={tool.name} className="flex min-w-0 items-center gap-2 py-px pl-3">
147
147
  <span
148
- className={`min-w-0 flex-1 truncate text-[9px] ${tool.active ? 'text-doom-muted' : 'text-doom-faint'}`}
148
+ className={`min-w-0 flex-1 truncate text-2xs ${tool.active ? 'text-doom-muted' : 'text-doom-faint'}`}
149
149
  >
150
150
  {tool.name}
151
151
  </span>
152
- <span className="w-14 shrink-0 text-right text-[9px] text-doom-faint">
152
+ <span className="w-14 shrink-0 text-right text-2xs text-doom-faint">
153
153
  {tool.active ? tokenEstimate(tool.tokens) : `(${tokenEstimate(tool.tokens)})`}
154
154
  </span>
155
155
  </li>
@@ -174,7 +174,7 @@ export function McpSessionAuthSection({
174
174
  <DialogContent data-testid="mcp-authorization-dialog">
175
175
  <DialogHeader className="items-start px-4 py-4 sm:px-5">
176
176
  <div className="flex min-w-0 flex-1 flex-col gap-1.5">
177
- <DialogTitle className="break-words text-[14px] leading-snug">
177
+ <DialogTitle className="break-words text-base leading-snug">
178
178
  {server?.state === 'connected' ? 'Manage' : 'Authorize'} {selected?.name}
179
179
  </DialogTitle>
180
180
  <DialogDescription className="max-w-2xl">
@@ -185,7 +185,7 @@ export function McpSessionAuthSection({
185
185
  </div>
186
186
  </DialogHeader>
187
187
  <DialogBody className="px-4 py-4 sm:px-5 sm:py-5">
188
- <p role="status" className="text-[11px] text-doom-muted">
188
+ <p role="status" className="text-sm text-doom-muted">
189
189
  {server?.state === 'connected'
190
190
  ? 'Authorization complete. This server is connected.'
191
191
  : server?.state === 'closed'
@@ -199,7 +199,7 @@ export function McpSessionAuthSection({
199
199
  : 'This server is no longer available in the session.'}
200
200
  </p>
201
201
  {popupBlocked ? (
202
- <p className="text-[10px] text-doom-muted">
202
+ <p className="text-xs text-doom-muted">
203
203
  The browser blocked the new tab. Open the link below when it is ready.
204
204
  </p>
205
205
  ) : null}
@@ -215,7 +215,7 @@ export function McpSessionAuthSection({
215
215
  href={authorizationUrl}
216
216
  target="_blank"
217
217
  rel="noopener noreferrer"
218
- className="text-[11px] text-doom-accent underline underline-offset-2"
218
+ className="text-sm text-doom-accent underline underline-offset-2"
219
219
  >
220
220
  open authorization page
221
221
  </a>
@@ -225,7 +225,7 @@ export function McpSessionAuthSection({
225
225
  </div>
226
226
  ) : null}
227
227
  {copyFeedback ? (
228
- <p role="status" className="text-[10px] text-doom-muted">
228
+ <p role="status" className="text-xs text-doom-muted">
229
229
  {copyFeedback}
230
230
  </p>
231
231
  ) : null}