@agimon-ai/doompi-mcp 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/commands/mcpCommand.cjs +3 -0
- package/dist/commands/mcpCommand.cjs.map +1 -0
- package/dist/commands/mcpCommand.mjs +3 -0
- package/dist/commands/mcpCommand.mjs.map +1 -0
- package/dist/constants.cjs +2 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.mjs +2 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/extension.cjs +2 -0
- package/dist/extension.cjs.map +1 -0
- package/dist/extension.d.cts +18 -0
- package/dist/extension.d.cts.map +1 -0
- package/dist/extension.d.mts +18 -0
- package/dist/extension.d.mts.map +1 -0
- package/dist/extension.mjs +2 -0
- package/dist/extension.mjs.map +1 -0
- package/dist/extensions/doom.cjs +2 -0
- package/dist/extensions/doom.cjs.map +1 -0
- package/dist/extensions/doom.d.cts +6 -0
- package/dist/extensions/doom.d.cts.map +1 -0
- package/dist/extensions/doom.d.mts +7 -0
- package/dist/extensions/doom.d.mts.map +1 -0
- package/dist/extensions/doom.mjs +2 -0
- package/dist/extensions/doom.mjs.map +1 -0
- package/dist/extensions/pi.cjs +2 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +2 -0
- package/dist/extensions/pi.d.mts +2 -0
- package/dist/extensions/pi.mjs +2 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/leader.cjs +2 -0
- package/dist/leader.cjs.map +1 -0
- package/dist/leader.mjs +2 -0
- package/dist/leader.mjs.map +1 -0
- package/dist/services/configSources.cjs +2 -0
- package/dist/services/configSources.cjs.map +1 -0
- package/dist/services/configSources.d.cts +65 -0
- package/dist/services/configSources.d.cts.map +1 -0
- package/dist/services/configSources.d.mts +65 -0
- package/dist/services/configSources.d.mts.map +1 -0
- package/dist/services/configSources.mjs +2 -0
- package/dist/services/configSources.mjs.map +1 -0
- package/dist/services/directTools.cjs +2 -0
- package/dist/services/directTools.cjs.map +1 -0
- package/dist/services/directTools.d.cts +14 -0
- package/dist/services/directTools.d.cts.map +1 -0
- package/dist/services/directTools.d.mts +14 -0
- package/dist/services/directTools.d.mts.map +1 -0
- package/dist/services/directTools.mjs +2 -0
- package/dist/services/directTools.mjs.map +1 -0
- package/dist/services/keyringTokenStore.cjs +2 -0
- package/dist/services/keyringTokenStore.cjs.map +1 -0
- package/dist/services/keyringTokenStore.mjs +2 -0
- package/dist/services/keyringTokenStore.mjs.map +1 -0
- package/dist/services/mcpCatalog.cjs +2 -0
- package/dist/services/mcpCatalog.cjs.map +1 -0
- package/dist/services/mcpCatalog.d.cts +17 -0
- package/dist/services/mcpCatalog.d.cts.map +1 -0
- package/dist/services/mcpCatalog.d.mts +17 -0
- package/dist/services/mcpCatalog.d.mts.map +1 -0
- package/dist/services/mcpCatalog.mjs +2 -0
- package/dist/services/mcpCatalog.mjs.map +1 -0
- package/dist/services/mcpRuntime.cjs +2 -0
- package/dist/services/mcpRuntime.cjs.map +1 -0
- package/dist/services/mcpRuntime.d.cts +26 -0
- package/dist/services/mcpRuntime.d.cts.map +1 -0
- package/dist/services/mcpRuntime.d.mts +26 -0
- package/dist/services/mcpRuntime.d.mts.map +1 -0
- package/dist/services/mcpRuntime.mjs +2 -0
- package/dist/services/mcpRuntime.mjs.map +1 -0
- package/dist/services/mcpSession.cjs +2 -0
- package/dist/services/mcpSession.cjs.map +1 -0
- package/dist/services/mcpSession.mjs +2 -0
- package/dist/services/mcpSession.mjs.map +1 -0
- package/dist/services/sessionConfig.cjs +2 -0
- package/dist/services/sessionConfig.cjs.map +1 -0
- package/dist/services/sessionConfig.d.cts +25 -0
- package/dist/services/sessionConfig.d.cts.map +1 -0
- package/dist/services/sessionConfig.d.mts +25 -0
- package/dist/services/sessionConfig.d.mts.map +1 -0
- package/dist/services/sessionConfig.mjs +2 -0
- package/dist/services/sessionConfig.mjs.map +1 -0
- package/dist/services/sharedRuntime.cjs +2 -0
- package/dist/services/sharedRuntime.cjs.map +1 -0
- package/dist/services/sharedRuntime.mjs +2 -0
- package/dist/services/sharedRuntime.mjs.map +1 -0
- package/dist/tools/mcpTools.cjs +3 -0
- package/dist/tools/mcpTools.cjs.map +1 -0
- package/dist/tools/mcpTools.mjs +3 -0
- package/dist/tools/mcpTools.mjs.map +1 -0
- package/dist/tui/mcpOverlay.cjs +2 -0
- package/dist/tui/mcpOverlay.cjs.map +1 -0
- package/dist/tui/mcpOverlay.mjs +2 -0
- package/dist/tui/mcpOverlay.mjs.map +1 -0
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/package.json +81 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharedRuntime.mjs","names":[],"sources":["../../src/services/sharedRuntime.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { findHealthyExistingRuntime, prestartHttpRuntime, resolveWorkspaceRoot } from '@agimon-ai/mcp-proxy';\nimport { PROXY_SERVER_NAME } from './configSources.ts';\nimport { readCachedCatalog } from './mcpRuntime.ts';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst RUNTIME_HOST = 'localhost';\n/** Flat mode is what `.mcp.json` already asked the proxy for, so tool names are unchanged. */\nconst RUNTIME_PROXY_MODE = 'flat';\n/** Read by the spawned runtime; versions of mcp-proxy without it ignore the value. */\nconst IDLE_SHUTDOWN_ENV = 'MCP_PROXY_IDLE_SHUTDOWN_MINUTES';\n/**\n * Deliberately longer than the proxy's own 30 minute session sweep.\n *\n * A runtime is idle only once it holds no session, and the sweep is what closes\n * the last quiet one. Matching the two would retire the runtime at the same\n * moment, so a session returning after a long pause would always pay a cold\n * start; the extra window lets it rejoin a warm runtime instead.\n */\nconst DEFAULT_IDLE_SHUTDOWN_MINUTES = '45';\n/** Staged layers this package writes: `shared-<hash>.json` and `mcp-<hash>.json`. */\nconst STAGING_FILE_PATTERN = /^(?:shared|mcp)-[0-9a-f]{16}\\.json$/;\nconst STAGING_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport interface SharedRuntimeOptions {\n repoRoot: string;\n /** The worktree-level config the runtime is started from, normally `mcp-config.yaml`. */\n configPaths: string[];\n}\n\nexport interface SharedRuntimeLayer {\n /** Generated `.mcp.json` layer pointing the proxy entry at the live runtime. */\n configPath: string;\n endpoint: string;\n reusedExistingRuntime: boolean;\n}\n\n/**\n * Where the generated layer for a workspace lives.\n *\n * Stable across runs on purpose: the definitions cache is keyed on the ordered\n * list of config paths, so a per-run filename would mint a new cache every start\n * and the tools would never be available before the first connection.\n */\nexport function sharedLayerPath(workspaceRoot: string): string {\n const digest = createHash('sha256').update(workspaceRoot).digest('hex').slice(0, 16);\n return path.join(os.tmpdir(), 'doom-mcp', `shared-${digest}.json`);\n}\n\n/**\n * Starts or joins the worktree's shared MCP runtime and returns a config layer for it.\n *\n * One runtime serves every doom-pi session in the worktree, so a healthy one is\n * always reused rather than respawned; only the first session in a worktree pays\n * to start it. The runtime is reached as an ordinary HTTP server named\n * `agiflow-proxy`, which is what `.mcp.json` already calls it, so the tool names\n * an agent sees are the same as before this package existed.\n *\n * Returns undefined when there is nothing to share, or when the runtime could not\n * be reached: losing the shared upstreams is better than losing the session.\n */\nexport async function ensureSharedRuntime(options: SharedRuntimeOptions): Promise<SharedRuntimeLayer | undefined> {\n if (options.configPaths.length === 0) return undefined;\n\n try {\n const workspaceRoot = resolveWorkspaceRoot(options.repoRoot);\n const existing = await findHealthyExistingRuntime(workspaceRoot);\n const runtime = existing ?? (await startRuntime(options.configPaths));\n const endpoint = `http://${runtime.host}:${runtime.port}/mcp`;\n\n const configPath = sharedLayerPath(workspaceRoot);\n fs.mkdirSync(path.dirname(configPath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n fs.writeFileSync(\n configPath,\n `${JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: { type: 'http', url: endpoint } } }, null, 2)}\\n`,\n { mode: PRIVATE_FILE_MODE },\n );\n\n return { configPath, endpoint, reusedExistingRuntime: existing !== null };\n } catch {\n return undefined;\n }\n}\n\nasync function startRuntime(configPaths: string[]): Promise<{ host: string; port: number }> {\n // The runtime inherits this environment, so the value reaches it whichever\n // mcp-proxy version is installed. It bounds how long a runtime outlives its\n // last session; an operator's explicit setting wins.\n process.env[IDLE_SHUTDOWN_ENV] ??= DEFAULT_IDLE_SHUTDOWN_MINUTES;\n const result = await prestartHttpRuntime({\n host: RUNTIME_HOST,\n config: configPaths,\n cache: true,\n clearDefinitionsCache: false,\n proxyMode: RUNTIME_PROXY_MODE,\n // Outliving this session is the point: the next one joins it instead of paying\n // to start the same downstream children again. The idle shutdown above is what\n // keeps that from meaning forever.\n runtimeLifecycle: 'manual',\n });\n return { host: result.host, port: result.port };\n}\n\n/**\n * Best-effort maintenance for the resources shared runtimes leave on disk.\n *\n * Runs detached from session start, so a session neither waits for it nor fails\n * because of it. Two jobs: drop staged config layers no session has refreshed\n * within the TTL (every live session rewrites its own layers at start, so age is\n * ownership), and let mcp-proxy prune registry rows whose processes are gone,\n * when the installed version exposes that.\n */\nexport async function maintainSharedResources(stagingDirectory: string, now: number = Date.now()): Promise<void> {\n sweepStagingDirectory(stagingDirectory, now);\n await collectRegistryGarbage();\n}\n\nfunction sweepStagingDirectory(stagingDirectory: string, now: number): void {\n let entries: string[];\n try {\n entries = fs.readdirSync(stagingDirectory);\n } catch {\n // Nothing has been staged yet, so there is nothing to sweep.\n return;\n }\n for (const entry of entries) {\n if (!STAGING_FILE_PATTERN.test(entry)) continue;\n const target = path.join(stagingDirectory, entry);\n try {\n if (now - fs.statSync(target).mtimeMs > STAGING_TTL_MS) fs.unlinkSync(target);\n } catch {\n // Another session touching the same file concurrently is not a failure.\n }\n }\n}\n\nasync function collectRegistryGarbage(): Promise<void> {\n try {\n // Feature-detected: the pinned mcp-proxy may predate the export.\n const proxy = (await import('@agimon-ai/mcp-proxy')) as unknown as { cleanupStaleRegistries?: () => Promise<void> };\n await proxy.cleanupStaleRegistries?.();\n } catch {\n // Garbage collection never gets to break a session.\n }\n}\n\n/**\n * The proxy tool names a domain's upstream allowlist permits.\n *\n * The runtime is shared, so its own configuration cannot be narrowed per session;\n * the allowlist is applied where this session registers tools instead. Attribution\n * comes from the catalog the runtime itself wrote, which records which upstream\n * each tool came from, rather than from parsing the proxied description text.\n *\n * Returns undefined when every upstream is allowed, which needs no filtering.\n */\nexport function resolveAllowedUpstreamTools(\n sharedConfigPaths: string[],\n allowedUpstreams: string[] | undefined,\n): Set<string> | undefined {\n if (!allowedUpstreams || allowedUpstreams.length === 0) return undefined;\n const allowed = new Set(allowedUpstreams);\n const catalog = readCachedCatalog(sharedConfigPaths);\n const names = new Set<string>();\n const clashing = findClashingToolNames(catalog.servers);\n for (const server of catalog.servers) {\n if (!allowed.has(server.name)) continue;\n for (const tool of server.tools) {\n // Flat mode only prefixes a tool whose name two upstreams both use.\n names.add(clashing.has(tool.name) ? `${server.name}__${tool.name}` : tool.name);\n }\n }\n return names;\n}\n\nfunction findClashingToolNames(servers: ReadonlyArray<{ tools: ReadonlyArray<{ name: string }> }>): Set<string> {\n const seen = new Set<string>();\n const clashing = new Set<string>();\n for (const server of servers) {\n for (const tool of server.tools) {\n if (seen.has(tool.name)) clashing.add(tool.name);\n seen.add(tool.name);\n }\n }\n return clashing;\n}\n"],"mappings":"kVAQA,MAiBM,EAAuB,sCAuB7B,SAAgB,EAAgB,EAA+B,CAC7D,IAAM,EAAS,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAa,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EACnF,OAAO,EAAK,KAAK,EAAG,OAAO,EAAG,WAAY,UAAU,EAAO,MAAM,CACnE,CAcA,eAAsB,EAAoB,EAAwE,CAC5G,KAAQ,YAAY,SAAW,EAEnC,GAAI,CACF,IAAM,EAAgB,EAAqB,EAAQ,QAAQ,EACrD,EAAW,MAAM,EAA2B,CAAa,EACzD,EAAU,GAAa,MAAM,EAAa,EAAQ,WAAW,EAC7D,EAAW,UAAU,EAAQ,KAAK,GAAG,EAAQ,KAAK,MAElD,EAAa,EAAgB,CAAa,EAQhD,OAPA,EAAG,UAAU,EAAK,QAAQ,CAAU,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,EAAG,cACD,EACA,GAAG,KAAK,UAAU,CAAE,WAAY,EAAG,GAAoB,CAAE,KAAM,OAAQ,IAAK,CAAS,CAAE,CAAE,EAAG,KAAM,CAAC,EAAE,IACrG,CAAE,KAAM,GAAkB,CAC5B,EAEO,CAAE,aAAY,WAAU,sBAAuB,IAAa,IAAK,CAC1E,MAAQ,CACN,MACF,CACF,CAEA,eAAe,EAAa,EAAgE,CAI1F,QAAQ,IAAI,kCAAuB,KACnC,IAAM,EAAS,MAAM,EAAoB,CACvC,KAAM,YACN,OAAQ,EACR,MAAO,GACP,sBAAuB,GACvB,UAAW,OAIX,iBAAkB,QACpB,CAAC,EACD,MAAO,CAAE,KAAM,EAAO,KAAM,KAAM,EAAO,IAAK,CAChD,CAWA,eAAsB,EAAwB,EAA0B,EAAc,KAAK,IAAI,EAAkB,CAC/G,EAAsB,EAAkB,CAAG,EAC3C,MAAM,EAAuB,CAC/B,CAEA,SAAS,EAAsB,EAA0B,EAAmB,CAC1E,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,CAAgB,CAC3C,MAAQ,CAEN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAqB,KAAK,CAAK,EAAG,SACvC,IAAM,EAAS,EAAK,KAAK,EAAkB,CAAK,EAChD,GAAI,CACE,EAAM,EAAG,SAAS,CAAM,CAAC,CAAC,QAAU,QAAgB,EAAG,WAAW,CAAM,CAC9E,MAAQ,CAER,CACF,CACF,CAEA,eAAe,GAAwC,CACrD,GAAI,CAGF,MAAM,MADe,OAAO,wBAAA,CAChB,yBAAyB,CACvC,MAAQ,CAER,CACF,CAYA,SAAgB,EACd,EACA,EACyB,CACzB,GAAI,CAAC,GAAoB,EAAiB,SAAW,EAAG,OACxD,IAAM,EAAU,IAAI,IAAI,CAAgB,EAClC,EAAU,EAAkB,CAAiB,EAC7C,EAAQ,IAAI,IACZ,EAAW,EAAsB,EAAQ,OAAO,EACtD,IAAK,IAAM,KAAU,EAAQ,QACtB,KAAQ,IAAI,EAAO,IAAI,EAC5B,IAAK,IAAM,KAAQ,EAAO,MAExB,EAAM,IAAI,EAAS,IAAI,EAAK,IAAI,EAAI,GAAG,EAAO,KAAK,IAAI,EAAK,OAAS,EAAK,IAAI,EAGlF,OAAO,CACT,CAEA,SAAS,EAAsB,EAAiF,CAC9G,IAAM,EAAO,IAAI,IACX,EAAW,IAAI,IACrB,IAAK,IAAM,KAAU,EACnB,IAAK,IAAM,KAAQ,EAAO,MACpB,EAAK,IAAI,EAAK,IAAI,GAAG,EAAS,IAAI,EAAK,IAAI,EAC/C,EAAK,IAAI,EAAK,IAAI,EAGtB,OAAO,CACT"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
let e=require("typebox");const t={type:`object`,properties:{}};function n(e){return(e.content??[]).filter(e=>e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(`
|
|
2
|
+
`).trim()}function r(e,t){let r=n(t)||`No output.`;if(t.isError)throw Error(r);return{content:[{type:`text`,text:r}],details:{server:e.serverName,tool:e.toolName}}}function i(n,i,a){n.registerTool({name:a.piName,label:`${a.serverName}: ${a.toolName}`,description:a.description??`${a.toolName} on the ${a.serverName} MCP server.`,parameters:e.Type.Unsafe(Object.keys(a.inputSchema).length>0?a.inputSchema:t),async execute(e,t){let n=i();if(!n)throw Error(`The MCP runtime is not ready, so ${a.serverName} cannot be reached yet.`);let o=await n.ensureConnected(a.serverName),s=n.getServerRequestTimeout(a.serverName);return r(a,await o.callTool(a.toolName,t??{},s===void 0?void 0:{timeout:s}))}})}function a(e,t,n){for(let r of n)i(e,t,r)}function o(e,t){let n=new Set(t.allTools().map(e=>e.piName)),r=e.getActiveTools().filter(e=>!n.has(e));e.setActiveTools([...r,...t.activeToolNames()])}exports.applyActiveTools=o,exports.registerMcpTool=i,exports.registerNewTools=a,exports.toAgentToolResult=r;
|
|
3
|
+
//# sourceMappingURL=mcpTools.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpTools.cjs","names":["Type"],"sources":["../../src/tools/mcpTools.ts"],"sourcesContent":["import type { McpClientManagerService } from '@agimon-ai/mcp-proxy';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolResult, ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { Type } from 'typebox';\nimport type { CatalogTool, McpCatalog } from '../services/mcpCatalog.ts';\n\n/** Pi requires a schema; a downstream tool that declares none takes any object. */\nconst ANY_OBJECT_SCHEMA = { type: 'object', properties: {} };\n\ninterface McpToolDetails {\n server: string;\n tool: string;\n [key: string]: unknown;\n}\n\n/** Reads the text of a downstream result, dropping blocks Pi cannot render. */\nfunction resultText(result: CallToolResult): string {\n const blocks = (result.content ?? []) as Array<{ type: string; text?: string }>;\n return blocks\n .filter((block) => block.type === 'text' && typeof block.text === 'string')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n}\n\n/**\n * Turns a downstream result into a Pi tool result.\n *\n * `isError` is raised rather than returned: Pi shows a thrown message as a failed\n * call, and returning the text would present a server-side error as a success.\n */\nexport function toAgentToolResult(tool: CatalogTool, result: CallToolResult): AgentToolResult<McpToolDetails> {\n const text = resultText(result) || 'No output.';\n if (result.isError) throw new Error(text);\n return { content: [{ type: 'text', text }], details: { server: tool.serverName, tool: tool.toolName } };\n}\n\n/**\n * Supplies the live client manager, or nothing before the runtime has started.\n *\n * A getter rather than the manager itself: tools are registered from the cached\n * catalog before any container exists, and the same registration has to reach\n * whichever container is live when the tool is finally called.\n */\nexport type ClientManagerSource = () => McpClientManagerService | undefined;\n\n/**\n * Registers one downstream tool with Pi.\n *\n * Registration is permanent for the session: Pi 0.84 has no `unregisterTool`, so\n * visibility is controlled through the active list instead. The connection is\n * resolved at execution time rather than captured, so a server that reconnects\n * underneath keeps working.\n */\nexport function registerMcpTool(pi: ExtensionAPI, clientManagerSource: ClientManagerSource, tool: CatalogTool): void {\n pi.registerTool({\n name: tool.piName,\n label: `${tool.serverName}: ${tool.toolName}`,\n description: tool.description ?? `${tool.toolName} on the ${tool.serverName} MCP server.`,\n // The schema arrives as JSON Schema at runtime, so it is wrapped rather than\n // built: TypeBox validates the shape without it having to be declared here.\n parameters: Type.Unsafe<Record<string, unknown>>(\n Object.keys(tool.inputSchema).length > 0 ? tool.inputSchema : ANY_OBJECT_SCHEMA,\n ),\n async execute(_toolCallId, params) {\n const clientManager = clientManagerSource();\n if (!clientManager) {\n throw new Error(`The MCP runtime is not ready, so ${tool.serverName} cannot be reached yet.`);\n }\n const connection = await clientManager.ensureConnected(tool.serverName);\n const timeout = clientManager.getServerRequestTimeout(tool.serverName);\n const result = await connection.callTool(\n tool.toolName,\n (params ?? {}) as Record<string, unknown>,\n timeout === undefined ? undefined : { timeout },\n );\n return toAgentToolResult(tool, result);\n },\n });\n}\n\n/**\n * Registers tools that are new to Pi.\n *\n * Separate from the active list because Pi allows `registerTool` during extension\n * loading but not `getActiveTools`/`setActiveTools`, which throw until the runtime\n * is bound. This half is therefore safe to run at install; `applyActiveTools` is\n * not, and waits for the session to start.\n */\nexport function registerNewTools(\n pi: ExtensionAPI,\n clientManagerSource: ClientManagerSource,\n newTools: readonly CatalogTool[],\n): void {\n for (const tool of newTools) registerMcpTool(pi, clientManagerSource, tool);\n}\n\n/**\n * Recomputes which of this extension's tools Pi exposes.\n *\n * Rebuilt from scratch every time, so a server that failed drops out and one that\n * reconnected comes back. Only valid once the session has started.\n */\nexport function applyActiveTools(pi: ExtensionAPI, catalog: McpCatalog): void {\n const mcpNames = new Set(catalog.allTools().map((tool) => tool.piName));\n // Tools this extension does not own are carried through untouched: the active\n // list is global, and rebuilding it from MCP alone would hide everything else.\n const others = pi.getActiveTools().filter((name) => !mcpNames.has(name));\n pi.setActiveTools([...others, ...catalog.activeToolNames()]);\n}\n"],"mappings":"yBAOA,MAAM,EAAoB,CAAE,KAAM,SAAU,WAAY,CAAC,CAAE,EAS3D,SAAS,EAAW,EAAgC,CAElD,OADgB,EAAO,SAAW,CAAC,EAAA,CAEhC,OAAQ,GAAU,EAAM,OAAS,QAAU,OAAO,EAAM,MAAS,QAAQ,CAAC,CAC1E,IAAK,GAAU,EAAM,IAAI,CAAC,CAC1B,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CAQA,SAAgB,EAAkB,EAAmB,EAAyD,CAC5G,IAAM,EAAO,EAAW,CAAM,GAAK,aACnC,GAAI,EAAO,QAAS,MAAU,MAAM,CAAI,EACxC,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,QAAS,CAAE,OAAQ,EAAK,WAAY,KAAM,EAAK,QAAS,CAAE,CACxG,CAmBA,SAAgB,EAAgB,EAAkB,EAA0C,EAAyB,CACnH,EAAG,aAAa,CACd,KAAM,EAAK,OACX,MAAO,GAAG,EAAK,WAAW,IAAI,EAAK,WACnC,YAAa,EAAK,aAAe,GAAG,EAAK,SAAS,UAAU,EAAK,WAAW,cAG5E,WAAYA,EAAAA,KAAK,OACf,OAAO,KAAK,EAAK,WAAW,CAAC,CAAC,OAAS,EAAI,EAAK,YAAc,CAChE,EACA,MAAM,QAAQ,EAAa,EAAQ,CACjC,IAAM,EAAgB,EAAoB,EAC1C,GAAI,CAAC,EACH,MAAU,MAAM,oCAAoC,EAAK,WAAW,wBAAwB,EAE9F,IAAM,EAAa,MAAM,EAAc,gBAAgB,EAAK,UAAU,EAChE,EAAU,EAAc,wBAAwB,EAAK,UAAU,EAMrE,OAAO,EAAkB,EAAM,MALV,EAAW,SAC9B,EAAK,SACJ,GAAU,CAAC,EACZ,IAAY,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,CAChD,CACqC,CACvC,CACF,CAAC,CACH,CAUA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAQ,EAAU,EAAgB,EAAI,EAAqB,CAAI,CAC5E,CAQA,SAAgB,EAAiB,EAAkB,EAA2B,CAC5E,IAAM,EAAW,IAAI,IAAI,EAAQ,SAAS,CAAC,CAAC,IAAK,GAAS,EAAK,MAAM,CAAC,EAGhE,EAAS,EAAG,eAAe,CAAC,CAAC,OAAQ,GAAS,CAAC,EAAS,IAAI,CAAI,CAAC,EACvE,EAAG,eAAe,CAAC,GAAG,EAAQ,GAAG,EAAQ,gBAAgB,CAAC,CAAC,CAC7D"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{Type as e}from"typebox";const t={type:`object`,properties:{}};function n(e){return(e.content??[]).filter(e=>e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(`
|
|
2
|
+
`).trim()}function r(e,t){let r=n(t)||`No output.`;if(t.isError)throw Error(r);return{content:[{type:`text`,text:r}],details:{server:e.serverName,tool:e.toolName}}}function i(n,i,a){n.registerTool({name:a.piName,label:`${a.serverName}: ${a.toolName}`,description:a.description??`${a.toolName} on the ${a.serverName} MCP server.`,parameters:e.Unsafe(Object.keys(a.inputSchema).length>0?a.inputSchema:t),async execute(e,t){let n=i();if(!n)throw Error(`The MCP runtime is not ready, so ${a.serverName} cannot be reached yet.`);let o=await n.ensureConnected(a.serverName),s=n.getServerRequestTimeout(a.serverName);return r(a,await o.callTool(a.toolName,t??{},s===void 0?void 0:{timeout:s}))}})}function a(e,t,n){for(let r of n)i(e,t,r)}function o(e,t){let n=new Set(t.allTools().map(e=>e.piName)),r=e.getActiveTools().filter(e=>!n.has(e));e.setActiveTools([...r,...t.activeToolNames()])}export{o as applyActiveTools,i as registerMcpTool,a as registerNewTools,r as toAgentToolResult};
|
|
3
|
+
//# sourceMappingURL=mcpTools.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpTools.mjs","names":[],"sources":["../../src/tools/mcpTools.ts"],"sourcesContent":["import type { McpClientManagerService } from '@agimon-ai/mcp-proxy';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolResult, ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { Type } from 'typebox';\nimport type { CatalogTool, McpCatalog } from '../services/mcpCatalog.ts';\n\n/** Pi requires a schema; a downstream tool that declares none takes any object. */\nconst ANY_OBJECT_SCHEMA = { type: 'object', properties: {} };\n\ninterface McpToolDetails {\n server: string;\n tool: string;\n [key: string]: unknown;\n}\n\n/** Reads the text of a downstream result, dropping blocks Pi cannot render. */\nfunction resultText(result: CallToolResult): string {\n const blocks = (result.content ?? []) as Array<{ type: string; text?: string }>;\n return blocks\n .filter((block) => block.type === 'text' && typeof block.text === 'string')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n}\n\n/**\n * Turns a downstream result into a Pi tool result.\n *\n * `isError` is raised rather than returned: Pi shows a thrown message as a failed\n * call, and returning the text would present a server-side error as a success.\n */\nexport function toAgentToolResult(tool: CatalogTool, result: CallToolResult): AgentToolResult<McpToolDetails> {\n const text = resultText(result) || 'No output.';\n if (result.isError) throw new Error(text);\n return { content: [{ type: 'text', text }], details: { server: tool.serverName, tool: tool.toolName } };\n}\n\n/**\n * Supplies the live client manager, or nothing before the runtime has started.\n *\n * A getter rather than the manager itself: tools are registered from the cached\n * catalog before any container exists, and the same registration has to reach\n * whichever container is live when the tool is finally called.\n */\nexport type ClientManagerSource = () => McpClientManagerService | undefined;\n\n/**\n * Registers one downstream tool with Pi.\n *\n * Registration is permanent for the session: Pi 0.84 has no `unregisterTool`, so\n * visibility is controlled through the active list instead. The connection is\n * resolved at execution time rather than captured, so a server that reconnects\n * underneath keeps working.\n */\nexport function registerMcpTool(pi: ExtensionAPI, clientManagerSource: ClientManagerSource, tool: CatalogTool): void {\n pi.registerTool({\n name: tool.piName,\n label: `${tool.serverName}: ${tool.toolName}`,\n description: tool.description ?? `${tool.toolName} on the ${tool.serverName} MCP server.`,\n // The schema arrives as JSON Schema at runtime, so it is wrapped rather than\n // built: TypeBox validates the shape without it having to be declared here.\n parameters: Type.Unsafe<Record<string, unknown>>(\n Object.keys(tool.inputSchema).length > 0 ? tool.inputSchema : ANY_OBJECT_SCHEMA,\n ),\n async execute(_toolCallId, params) {\n const clientManager = clientManagerSource();\n if (!clientManager) {\n throw new Error(`The MCP runtime is not ready, so ${tool.serverName} cannot be reached yet.`);\n }\n const connection = await clientManager.ensureConnected(tool.serverName);\n const timeout = clientManager.getServerRequestTimeout(tool.serverName);\n const result = await connection.callTool(\n tool.toolName,\n (params ?? {}) as Record<string, unknown>,\n timeout === undefined ? undefined : { timeout },\n );\n return toAgentToolResult(tool, result);\n },\n });\n}\n\n/**\n * Registers tools that are new to Pi.\n *\n * Separate from the active list because Pi allows `registerTool` during extension\n * loading but not `getActiveTools`/`setActiveTools`, which throw until the runtime\n * is bound. This half is therefore safe to run at install; `applyActiveTools` is\n * not, and waits for the session to start.\n */\nexport function registerNewTools(\n pi: ExtensionAPI,\n clientManagerSource: ClientManagerSource,\n newTools: readonly CatalogTool[],\n): void {\n for (const tool of newTools) registerMcpTool(pi, clientManagerSource, tool);\n}\n\n/**\n * Recomputes which of this extension's tools Pi exposes.\n *\n * Rebuilt from scratch every time, so a server that failed drops out and one that\n * reconnected comes back. Only valid once the session has started.\n */\nexport function applyActiveTools(pi: ExtensionAPI, catalog: McpCatalog): void {\n const mcpNames = new Set(catalog.allTools().map((tool) => tool.piName));\n // Tools this extension does not own are carried through untouched: the active\n // list is global, and rebuilding it from MCP alone would hide everything else.\n const others = pi.getActiveTools().filter((name) => !mcpNames.has(name));\n pi.setActiveTools([...others, ...catalog.activeToolNames()]);\n}\n"],"mappings":"+BAOA,MAAM,EAAoB,CAAE,KAAM,SAAU,WAAY,CAAC,CAAE,EAS3D,SAAS,EAAW,EAAgC,CAElD,OADgB,EAAO,SAAW,CAAC,EAAA,CAEhC,OAAQ,GAAU,EAAM,OAAS,QAAU,OAAO,EAAM,MAAS,QAAQ,CAAC,CAC1E,IAAK,GAAU,EAAM,IAAI,CAAC,CAC1B,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CAQA,SAAgB,EAAkB,EAAmB,EAAyD,CAC5G,IAAM,EAAO,EAAW,CAAM,GAAK,aACnC,GAAI,EAAO,QAAS,MAAU,MAAM,CAAI,EACxC,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,QAAS,CAAE,OAAQ,EAAK,WAAY,KAAM,EAAK,QAAS,CAAE,CACxG,CAmBA,SAAgB,EAAgB,EAAkB,EAA0C,EAAyB,CACnH,EAAG,aAAa,CACd,KAAM,EAAK,OACX,MAAO,GAAG,EAAK,WAAW,IAAI,EAAK,WACnC,YAAa,EAAK,aAAe,GAAG,EAAK,SAAS,UAAU,EAAK,WAAW,cAG5E,WAAY,EAAK,OACf,OAAO,KAAK,EAAK,WAAW,CAAC,CAAC,OAAS,EAAI,EAAK,YAAc,CAChE,EACA,MAAM,QAAQ,EAAa,EAAQ,CACjC,IAAM,EAAgB,EAAoB,EAC1C,GAAI,CAAC,EACH,MAAU,MAAM,oCAAoC,EAAK,WAAW,wBAAwB,EAE9F,IAAM,EAAa,MAAM,EAAc,gBAAgB,EAAK,UAAU,EAChE,EAAU,EAAc,wBAAwB,EAAK,UAAU,EAMrE,OAAO,EAAkB,EAAM,MALV,EAAW,SAC9B,EAAK,SACJ,GAAU,CAAC,EACZ,IAAY,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,CAChD,CACqC,CACvC,CACF,CAAC,CACH,CAUA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAQ,EAAU,EAAgB,EAAI,EAAqB,CAAI,CAC5E,CAQA,SAAgB,EAAiB,EAAkB,EAA2B,CAC5E,IAAM,EAAW,IAAI,IAAI,EAAQ,SAAS,CAAC,CAAC,IAAK,GAAS,EAAK,MAAM,CAAC,EAGhE,EAAS,EAAG,eAAe,CAAC,CAAC,OAAQ,GAAS,CAAC,EAAS,IAAI,CAAI,CAAC,EACvE,EAAG,eAAe,CAAC,GAAG,EAAQ,GAAG,EAAQ,gBAAgB,CAAC,CAAC,CAC7D"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("@earendil-works/pi-tui"),t=require("@agimon-ai/doompi-ui/components/doomOverlay");const n=[[`↑↓`,`server`],[`⇧JK`,`scroll`],[`tab`,`pane`],[`a`,`auth`],[`d`,`disable`],[`e`,`enable`],[`r`,`reload`],[`esc`,`close`]],r=n.map(([e,t])=>`${e} ${t}`).join(` · `),i=[`tools`,`resources`],a=[`auth`,`disable`,`enable`],o={auth:`a`,disable:`d`,enable:`e`},s={auth:`reconnect`,disable:`session`,enable:void 0},c=new Map(a.map(e=>[o[e],e]));function l(e){return e?e.enabled?{auth:e.state!==`connecting`,disable:!0,enable:!1}:{auth:!1,disable:!1,enable:!0}:{auth:!1,disable:!1,enable:!1}}function u(e,t){return t?t.enabled?e===`enable`?`enable unavailable · ${t.name} is already enabled`:`${e} unavailable · ${t.name} is ${t.state}`:`${e} unavailable · ${t.name} is disabled for this session`:`${e} unavailable · no server is selected`}function d(e,t){return e.enabled?e.state===`connected`?t.fg(`success`,`●`):e.state===`connecting`?t.fg(`accent`,`◐`):e.state===`degraded`?t.fg(`warning`,`▲`):e.state===`needs-auth`?t.fg(`warning`,`✗`):e.state===`failed`?t.fg(`error`,`✗`):t.fg(`muted`,`◦`):t.fg(`dim`,`■`)}function f(e){return e.enabled?e.state:`disabled`}function p(t,n){let r=(0,e.truncateToWidth)(t,Math.max(0,n),`…`);return r+` `.repeat(Math.max(0,n-(0,e.visibleWidth)(r)))}function m(t,n,r){let i=(0,e.visibleWidth)(n),a=Math.max(0,r-i-1);return p(t,a)+` `.repeat(Math.max(1,r-a-i))+p(n,i)}function h(e,t){if(t<=0)return[];let n=[],r=``;for(let i of e.split(/\s+/).filter(Boolean))r.length===0?r=i:r.length+1+i.length<=t?r+=` ${i}`:(n.push(r),r=i);return r.length>0&&n.push(r),n}var g=class extends t.DoomOverlay{target;done;servers=[];selected=0;selectedName;detailTab=`tools`;detailScroll=0;detailLineCount=0;detailViewportHeight=8;bodyHeight=8;actionNotice;disposed=!1;resourcePanes=new Map;unsubscribe;constructor(e,t,n,r){super(e,t),this.target=n,this.done=r,this.refresh(),this.unsubscribe=this.target.onChange(()=>{if(this.disposed)return;this.refresh();let e=this.current();e?.authorizationUrl&&(this.actionNotice=`auth ${e.name} · waiting for you to open the URL`),this.tui.requestRender()})}dispose(){this.disposed=!0,this.unsubscribe()}refresh(){let e=this.servers[this.selected]?.name??this.selectedName;this.servers=this.target.getServers();let t=e?this.servers.findIndex(t=>t.name===e):-1;this.selected=t>=0?t:Math.min(this.selected,Math.max(0,this.servers.length-1)),this.selectedName=this.servers[this.selected]?.name}current(){return this.servers[this.selected]}handleInput(t){if((0,e.matchesKey)(t,`escape`)||(0,e.matchesKey)(t,`ctrl+c`)||(0,e.matchesKey)(t,`q`)){this.done(void 0);return}if((0,e.matchesKey)(t,e.Key.shift(`k`)))return this.scrollDetail(-1);if((0,e.matchesKey)(t,e.Key.shift(`j`)))return this.scrollDetail(1);if((0,e.matchesKey)(t,`up`)||(0,e.matchesKey)(t,`k`))return this.moveSelection(-1);if((0,e.matchesKey)(t,`down`)||(0,e.matchesKey)(t,`j`))return this.moveSelection(1);if((0,e.matchesKey)(t,`pageUp`))return this.scrollDetail(-this.detailViewportHeight);if((0,e.matchesKey)(t,`pageDown`))return this.scrollDetail(this.detailViewportHeight);if((0,e.matchesKey)(t,`tab`))return this.toggleTab();if((0,e.matchesKey)(t,`ctrl+r`))return this.loadResources({refresh:!0});if((0,e.matchesKey)(t,`r`))return this.reload();let n=c.get(t.toLowerCase());n&&this.triggerControl(n)}moveSelection(e){this.servers.length!==0&&(this.selected=Math.max(0,Math.min(this.servers.length-1,this.selected+e)),this.selectedName=this.servers[this.selected]?.name,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender())}scrollDetail(e){let t=Math.max(0,this.detailLineCount-this.detailViewportHeight);this.detailScroll=Math.max(0,Math.min(t,this.detailScroll+e)),this.tui.requestRender()}toggleTab(){this.detailTab=this.detailTab===`tools`?`resources`:`tools`,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender()}reload(){this.setNotice(`reload · dispatching…`),this.target.start().then(()=>{this.disposed||this.setNotice(`reload · reconnecting`)}).catch(e=>{this.disposed||this.setNotice(`reload · failed · ${_(e)}`)})}maybeLoadResources(){if(this.detailTab!==`resources`)return;let e=this.current();!e||!e.enabled||e.state!==`connected`||this.resourcePanes.has(e.name)||this.loadResources()}loadResources(e={}){let t=this.current();t&&(this.resourcePanes.set(t.name,{status:`loading`}),this.tui.requestRender(),this.target.listResources(t.name,e).then(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`ready`,items:e}),this.tui.requestRender())}).catch(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`error`,error:_(e)}),this.tui.requestRender())}))}triggerControl(e){let t=this.current();if(!l(t)[e]){this.setNotice(u(e,t));return}if(!t)return;if(e===`auth`){this.authorize(t.name);return}let n=e===`enable`;try{this.target.setEnabled(t.name,n),this.refresh(),this.setNotice(`${e} ${t.name} · ${n?`tools restored`:`tools withheld for this session`}`)}catch(n){this.setNotice(`${e} ${t.name} · failed · ${_(n)}`)}}async authorize(e){this.setNotice(`auth ${e} · dispatching…`);try{if(await this.target.reauthorize(e),this.disposed)return;this.resourcePanes.delete(e),this.refresh(),this.setNotice(`auth ${e} · authorized`)}catch(t){if(this.disposed)return;this.setNotice(`auth ${e} · failed · ${_(t)}`)}}setNotice(e){this.actionNotice=e,this.tui.requestRender()}rosterLines(e){if(this.servers.length===0)return[this.theme.fg(`dim`,`No servers`)];let t=Math.max(0,Math.min(this.selected-this.bodyHeight+1,Math.max(0,this.servers.length-this.bodyHeight)));return this.servers.slice(t,t+this.bodyHeight).map((n,r)=>{let i=t+r,a=i===this.selected?this.theme.fg(`accent`,`›`):` `,o=n.enabled?i===this.selected?this.theme.bold(n.name):n.name:this.theme.fg(`dim`,n.name);return m(`${a} ${d(n,this.theme)} ${o}`,this.theme.fg(`dim`,f(n)),e)})}authorizationLines(e,t){let n=Math.max(1,t-2),r=[];for(let t=0;t<e.length;t+=n)r.push(e.slice(t,t+n));return[this.theme.fg(`accent`,p(` Open this to authorize:`,t)),...r.map(e=>this.theme.fg(`text`,p(` ${e}`,t)))]}field(e,t,n){let r=Math.max(1,n-10);return h(t,r).map((t,n)=>`${this.theme.fg(`dim`,p(n===0?e:``,10))}${p(t,r)}`)}controlLine(t,n){let r=l(t),i=a.map(e=>{let t=s[e],n=`${e}${t?` (${t})`:``}`;return this.keyCap(o[e],n,r[e])});return(0,e.truncateToWidth)(` ${i.join(` `)}`,n)}keyCap(e,t,n=!0){return`${n?this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${e} `)):this.theme.fg(`dim`,` ${e} `)}${this.theme.fg(`dim`,` ${t}`)}`}tabStrip(e,t){let n=e=>e===this.detailTab?this.theme.fg(`accent`,this.theme.bold(`[ ${e} ]`)):this.theme.fg(`dim`,` ${e} `),r=e&&this.detailTab===`tools`?`${e.tools.filter(e=>e.active).length}/${e.tools.length}`:`ctrl+r refresh`;return m(` ${i.map(n).join(``)}`,this.theme.fg(`dim`,r),t)}toolLines(e,t){if(e.tools.length===0)return[this.theme.fg(`dim`,` No tools`)];let n=[];for(let r of e.tools){let i=r.active&&e.enabled;if(n.push(this.theme.fg(i?`text`:`dim`,p(` ${r.toolName}`,t))),r.description)for(let e of h(r.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${e}`))}return n}resourceLines(e,t){let n=this.resourcePanes.get(e.name);if(!n)return[this.theme.fg(`dim`,` ${e.state===`connected`&&e.enabled?`Press ctrl+r to list resources`:`${e.name} is ${f(e)} · press ctrl+r to try anyway`}`)];if(n.status===`loading`)return[this.theme.fg(`dim`,` Listing resources…`)];if(n.status===`error`)return h(`Could not list resources: ${n.error??`unknown error`}`,Math.max(1,t-2)).map(e=>this.theme.fg(`warning`,` ${e}`));let r=n.items??[];return r.length===0?[this.theme.fg(`dim`,` No resources`)]:r.flatMap(e=>{let n=[m(` ${e.name??e.uri}`,this.theme.fg(`dim`,e.mimeType??``),t)];if(e.name&&n.push(this.theme.fg(`dim`,p(` ${e.uri}`,t))),e.description)for(let r of h(e.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${r}`));return n})}detailLines(e,t){let n=this.current();if(!n)return[this.theme.fg(`dim`,`No MCP servers are configured for this session.`)];let r=[f(n),`${n.tools.length} ${n.tools.length===1?`tool`:`tools`}`,`${n.resourceCount} res`].join(` · `),i=[this.theme.bold(n.name),``,...this.field(`State`,r,e),...this.field(`Error`,n.error??`—`,e),...n.authorizationUrl?[``,...this.authorizationLines(n.authorizationUrl,e)]:[],``,this.controlLine(n,e),``,this.tabStrip(n,e),``],a=this.detailTab===`tools`?this.toolLines(n,e):this.resourceLines(n,e);this.detailLineCount=a.length,this.detailViewportHeight=Math.max(1,t-i.length);let o=Math.min(this.detailScroll,Math.max(0,a.length-this.detailViewportHeight));return[...i,...a.slice(o,o+this.detailViewportHeight)]}getChrome(){let e=this.servers.filter(e=>e.enabled&&e.state===`connected`).length;return{title:`MCP`,accent:t.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › e / m · mcp`,headerRight:`${e}/${this.servers.length} connected · esc close`,footer:r,footerHints:n}}noticeRow(e){return this.theme.fg(`warning`,p(` ${this.actionNotice??``}`,e))}renderBody(t,n){let r=this.actionNotice?Math.max(1,n-1):n,i=this.actionNotice?[this.noticeRow(t)]:[];if(this.bodyHeight=r,t<48)return[...this.renderStacked(t,r),...i];let a=Math.max(18,Math.floor((t-1)*.3333333333333333)),o=Math.max(1,t-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),u=this.detailLines(c,r),d=this.theme.fg(`borderMuted`,`│`);return[...Array.from({length:r},(n,r)=>{let i=`${p(l[r]??``,s)} ${d} ${p(u[r]??``,c)}`;return(0,e.truncateToWidth)(i,t,`…`)}),...i]}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1);this.bodyHeight=n;let i=this.rosterLines(e),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}};function _(e){return e instanceof Error?e.message:String(e)}async function v(e,n){await e.ui.custom((e,t,r,i)=>new g(e,t,n,i),t.DOOM_FULLSCREEN_UI_OPTIONS)}exports.McpOverlayComponent=g,exports.openMcpOverlay=v;
|
|
2
|
+
//# sourceMappingURL=mcpOverlay.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpOverlay.cjs","names":["truncateToWidth","visibleWidth","DoomOverlay","matchesKey","Key","DOOM_OVERLAY_ACCENT","DOOM_FULLSCREEN_UI_OPTIONS"],"sources":["../../src/tui/mcpOverlay.ts"],"sourcesContent":["/**\n * MCP browser: the `SPC e m` overlay.\n *\n * A roster of this session's MCP servers on the left, and on the right what the\n * selected one offers plus the controls that act on it. It replaces a `/mcp status`\n * text blob that could be read but not acted on.\n *\n * DESIGN PATTERNS:\n * - Controls always list every action, unavailable ones dimmed rather than hidden,\n * so the key map never shifts under the user\n * - Every collaborator arrives through `McpOverlayTarget`, so the component renders\n * and dispatches in a test with no terminal and no session behind it\n *\n * The roster stays a flat list and the detail pane a tab strip rather than a second\n * navigation level: selection is one index, and every row is assertable as a string.\n *\n * Repainting is driven by `target.onChange`, not a timer. Servers connect on their\n * own schedule and the session already announces each one as it folds in, so a poll\n * here would only be sampling an event that had already fired.\n *\n * Disabling is a visibility control, not a permission boundary: Pi 0.84 cannot\n * unregister a tool, so a disabled server's tools leave the active list but stay\n * registered and callable. `DirectToolFilter` has the same property.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { McpOverlayTarget, McpResourceView, McpServerView } from '../types.ts';\n\nconst TITLE = 'MCP';\nconst BREADCRUMB = 'SPC › e / m · mcp';\n/**\n * Ordered by what a user reaches for first, because the chrome drops trailing\n * hints rather than shrinking them when the terminal is narrow.\n *\n * Scrolling earns a slot: a long tool list is the common case (the proxy alone\n * carries twelve), and without a legend entry the keys that reach the rest of it\n * are undiscoverable.\n */\nconst HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'server'],\n ['⇧JK', 'scroll'],\n ['tab', 'pane'],\n ['a', 'auth'],\n ['d', 'disable'],\n ['e', 'enable'],\n ['r', 'reload'],\n ['esc', 'close'],\n];\n/** Derived so the plain-text fallback cannot drift from the caps above it. */\nconst FOOTER_FALLBACK = HINTS.map(([key, label]) => `${key} ${label}`).join(' · ');\n/** Matches the chrome's spacing between footer caps, so both legends read alike. */\nconst CAP_SEPARATOR = ' ';\n\nconst MIN_WIDTH = 48;\n/** A third for the roster, two thirds for what the server offers. */\nconst ROSTER_PANE_RATIO = 1 / 3;\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\nconst ELLIPSIS = '…';\nconst LABEL_COLUMN = 10;\nconst FIELD_FALLBACK = '—';\nconst EMPTY_MESSAGE = 'No MCP servers are configured for this session.';\nconst TOOL_INDENT = ' ';\n\nexport type McpDetailTab = 'tools' | 'resources';\nconst DETAIL_TABS: readonly McpDetailTab[] = ['tools', 'resources'];\n\ntype McpActionName = 'auth' | 'disable' | 'enable';\n\nconst CONTROL_ORDER: readonly McpActionName[] = ['auth', 'disable', 'enable'];\nconst CONTROL_KEY: Record<McpActionName, string> = { auth: 'a', disable: 'd', enable: 'e' };\n/** Short qualifier where the verb alone would understate what the key does. */\nconst CONTROL_HINT: Record<McpActionName, string | undefined> = {\n auth: 'reconnect',\n disable: 'session',\n enable: undefined,\n};\nconst CONTROL_BY_KEY = new Map<string, McpActionName>(CONTROL_ORDER.map((action) => [CONTROL_KEY[action], action]));\n\n/**\n * What each control can do to a server in its current condition.\n *\n * `auth` reconnects, so it is offered wherever a reconnect is meaningful, including\n * a server that has not been dialled yet. It is withheld only mid-connect, because\n * reauthorizing disconnects first and would tear down the attempt in flight.\n */\nfunction controlAvailability(server: McpServerView | undefined): Record<McpActionName, boolean> {\n if (!server) return { auth: false, disable: false, enable: false };\n if (!server.enabled) return { auth: false, disable: false, enable: true };\n return { auth: server.state !== 'connecting', disable: true, enable: false };\n}\n\nfunction unavailableReason(action: McpActionName, server: McpServerView | undefined): string {\n if (!server) return `${action} unavailable · no server is selected`;\n if (!server.enabled) return `${action} unavailable · ${server.name} is disabled for this session`;\n if (action === 'enable') return `enable unavailable · ${server.name} is already enabled`;\n return `${action} unavailable · ${server.name} is ${server.state}`;\n}\n\nfunction statusGlyph(server: McpServerView, theme: Theme): string {\n if (!server.enabled) return theme.fg('dim', '■');\n if (server.state === 'connected') return theme.fg('success', '●');\n if (server.state === 'connecting') return theme.fg('accent', '◐');\n if (server.state === 'degraded') return theme.fg('warning', '▲');\n if (server.state === 'needs-auth') return theme.fg('warning', '✗');\n if (server.state === 'failed') return theme.fg('error', '✗');\n return theme.fg('muted', '◦');\n}\n\n/** What the roster row and the detail header both call the server's condition. */\nfunction stateLabel(server: McpServerView): string {\n return server.enabled ? server.state : 'disabled';\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (current.length + 1 + word.length <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** What the resources pane knows about one server. Absent until the pane asks. */\ninterface ResourcePane {\n status: 'loading' | 'ready' | 'error';\n items?: readonly McpResourceView[];\n error?: string;\n}\n\nexport class McpOverlayComponent extends DoomOverlay {\n private servers: readonly McpServerView[] = [];\n private selected = 0;\n private selectedName: string | undefined;\n private detailTab: McpDetailTab = 'tools';\n private detailScroll = 0;\n private detailLineCount = 0;\n private detailViewportHeight = 8;\n private bodyHeight = 8;\n private actionNotice: string | undefined;\n private disposed = false;\n private readonly resourcePanes = new Map<string, ResourcePane>();\n private readonly unsubscribe: () => void;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly target: McpOverlayTarget,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n this.refresh();\n this.unsubscribe = this.target.onChange(() => {\n if (this.disposed) return;\n this.refresh();\n // A flow that has surfaced its URL is no longer dispatching, it is waiting on\n // the person reading this, and the notice has to stop saying otherwise.\n const server = this.current();\n if (server?.authorizationUrl) this.actionNotice = `auth ${server.name} · waiting for you to open the URL`;\n this.tui.requestRender();\n });\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribe();\n }\n\n /** Re-reads the roster, keeping the cursor on the server it was already on. */\n private refresh(): void {\n const previousName = this.servers[this.selected]?.name ?? this.selectedName;\n this.servers = this.target.getServers();\n const preserved = previousName ? this.servers.findIndex((server) => server.name === previousName) : -1;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.servers.length - 1));\n this.selectedName = this.servers[this.selected]?.name;\n }\n\n private current(): McpServerView | undefined {\n return this.servers[this.selected];\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) return this.scrollDetail(-1);\n if (matchesKey(data, Key.shift('j'))) return this.scrollDetail(1);\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) return this.moveSelection(-1);\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) return this.moveSelection(1);\n if (matchesKey(data, 'pageUp')) return this.scrollDetail(-this.detailViewportHeight);\n if (matchesKey(data, 'pageDown')) return this.scrollDetail(this.detailViewportHeight);\n if (matchesKey(data, 'tab')) return this.toggleTab();\n // Checked before the single-letter controls: ctrl+r is the deliberate path that\n // discards what a server said about its resources and asks again.\n if (matchesKey(data, 'ctrl+r')) return this.loadResources({ refresh: true });\n if (matchesKey(data, 'r')) return this.reload();\n const action = CONTROL_BY_KEY.get(data.toLowerCase());\n if (action) this.triggerControl(action);\n }\n\n private moveSelection(delta: number): void {\n if (this.servers.length === 0) return;\n this.selected = Math.max(0, Math.min(this.servers.length - 1, this.selected + delta));\n this.selectedName = this.servers[this.selected]?.name;\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private scrollDetail(delta: number): void {\n const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);\n this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));\n this.tui.requestRender();\n }\n\n private toggleTab(): void {\n this.detailTab = this.detailTab === 'tools' ? 'resources' : 'tools';\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private reload(): void {\n this.setNotice('reload · dispatching…');\n // Not awaited: a server that never answers must not wedge the overlay. The\n // roster repaints through onChange as each one lands.\n void this.target\n .start()\n .then(() => {\n if (!this.disposed) this.setNotice('reload · reconnecting');\n })\n .catch((cause: unknown) => {\n if (!this.disposed) this.setNotice(`reload · failed · ${message(cause)}`);\n });\n }\n\n /**\n * Lists the selected server's resources, but only where a listing is meaningful.\n *\n * A server that has not connected is left alone: turning a `tab` keypress into a\n * silent dial-out is not what switching panes should mean.\n */\n private maybeLoadResources(): void {\n if (this.detailTab !== 'resources') return;\n const server = this.current();\n if (!server || !server.enabled || server.state !== 'connected') return;\n if (this.resourcePanes.has(server.name)) return;\n this.loadResources();\n }\n\n private loadResources(options: { refresh?: boolean } = {}): void {\n const server = this.current();\n if (!server) return;\n this.resourcePanes.set(server.name, { status: 'loading' });\n this.tui.requestRender();\n void this.target\n .listResources(server.name, options)\n .then((items) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'ready', items });\n this.tui.requestRender();\n })\n .catch((cause: unknown) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'error', error: message(cause) });\n this.tui.requestRender();\n });\n }\n\n private triggerControl(action: McpActionName): void {\n const server = this.current();\n if (!controlAvailability(server)[action]) {\n this.setNotice(unavailableReason(action, server));\n return;\n }\n if (!server) return;\n if (action === 'auth') {\n void this.authorize(server.name);\n return;\n }\n const enabled = action === 'enable';\n try {\n this.target.setEnabled(server.name, enabled);\n this.refresh();\n this.setNotice(`${action} ${server.name} · ${enabled ? 'tools restored' : 'tools withheld for this session'}`);\n } catch (cause) {\n this.setNotice(`${action} ${server.name} · failed · ${message(cause)}`);\n }\n }\n\n private async authorize(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · dispatching…`);\n try {\n await this.target.reauthorize(serverName);\n if (this.disposed) return;\n // The listing is about whatever the reconnect just settled on.\n this.resourcePanes.delete(serverName);\n this.refresh();\n this.setNotice(`auth ${serverName} · authorized`);\n } catch (cause) {\n if (this.disposed) return;\n this.setNotice(`auth ${serverName} · failed · ${message(cause)}`);\n }\n }\n\n private setNotice(notice: string): void {\n this.actionNotice = notice;\n this.tui.requestRender();\n }\n\n private rosterLines(width: number): string[] {\n if (this.servers.length === 0) return [this.theme.fg('dim', 'No servers')];\n const start = Math.max(\n 0,\n Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.servers.length - this.bodyHeight)),\n );\n return this.servers.slice(start, start + this.bodyHeight).map((server, offset) => {\n const index = start + offset;\n const marker = index === this.selected ? this.theme.fg('accent', '›') : ' ';\n // Dimming a disabled server states in the roster what the detail pane would\n // otherwise have to be visited to learn.\n const name = server.enabled\n ? index === this.selected\n ? this.theme.bold(server.name)\n : server.name\n : this.theme.fg('dim', server.name);\n const left = `${marker} ${statusGlyph(server, this.theme)} ${name}`;\n return rightAligned(left, this.theme.fg('dim', stateLabel(server)), width);\n });\n }\n\n /**\n * The authorization URL, wrapped across as many rows as it needs.\n *\n * Broken on width rather than truncated, and given the accent the controls use,\n * because it is the only thing in this overlay that cannot be acted on with a\n * keystroke: the user has to read it and open it somewhere else.\n */\n private authorizationLines(url: string, width: number): string[] {\n const indent = TOOL_INDENT;\n const body = Math.max(1, width - indent.length);\n const chunks: string[] = [];\n for (let start = 0; start < url.length; start += body) chunks.push(url.slice(start, start + body));\n return [\n this.theme.fg('accent', fit(`${indent}Open this to authorize:`, width)),\n ...chunks.map((chunk) => this.theme.fg('text', fit(`${indent}${chunk}`, width))),\n ];\n }\n\n private field(label: string, value: string, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) => `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${fit(line, valueWidth)}`,\n );\n }\n\n /**\n * Runtime controls as filled key caps, matching the footer legend.\n *\n * Every action is always listed; an unavailable one loses its cap fill rather\n * than its place, so the key map never shifts under the user and a control that\n * cannot act still reads as a key rather than as prose.\n */\n private controlLine(server: McpServerView | undefined, width: number): string {\n const availability = controlAvailability(server);\n const parts = CONTROL_ORDER.map((action) => {\n const hint = CONTROL_HINT[action];\n const label = `${action}${hint ? ` (${hint})` : ''}`;\n return this.keyCap(CONTROL_KEY[action], label, availability[action]);\n });\n return truncateToWidth(` ${parts.join(CAP_SEPARATOR)}`, width);\n }\n\n /** One filled key cap and its label. Unfilled when the key would do nothing. */\n private keyCap(key: string, label: string, enabled = true): string {\n const cap = enabled\n ? this.theme.bg('selectedBg', this.theme.fg('text', ` ${key} `))\n : this.theme.fg('dim', ` ${key} `);\n return `${cap}${this.theme.fg('dim', ` ${label}`)}`;\n }\n\n private tabStrip(server: McpServerView | undefined, width: number): string {\n const label = (name: McpDetailTab): string =>\n name === this.detailTab\n ? this.theme.fg('accent', this.theme.bold(`[ ${name} ]`))\n : this.theme.fg('dim', ` ${name} `);\n const counts =\n server && this.detailTab === 'tools'\n ? `${server.tools.filter((tool) => tool.active).length}/${server.tools.length}`\n : 'ctrl+r refresh';\n return rightAligned(` ${DETAIL_TABS.map(label).join('')}`, this.theme.fg('dim', counts), width);\n }\n\n private toolLines(server: McpServerView, width: number): string[] {\n if (server.tools.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No tools`)];\n const lines: string[] = [];\n for (const tool of server.tools) {\n // Colour separates what the model can call from what is merely registered,\n // so a withheld tool never has to be selected to be recognised.\n const usable = tool.active && server.enabled;\n lines.push(this.theme.fg(usable ? 'text' : 'dim', fit(`${TOOL_INDENT}${tool.toolName}`, width)));\n if (tool.description) {\n for (const line of wrap(tool.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n }\n return lines;\n }\n\n private resourceLines(server: McpServerView, width: number): string[] {\n const pane = this.resourcePanes.get(server.name);\n if (!pane) {\n return [\n this.theme.fg(\n 'dim',\n `${TOOL_INDENT}${server.state === 'connected' && server.enabled ? 'Press ctrl+r to list resources' : `${server.name} is ${stateLabel(server)} · press ctrl+r to try anyway`}`,\n ),\n ];\n }\n if (pane.status === 'loading') return [this.theme.fg('dim', `${TOOL_INDENT}Listing resources…`)];\n if (pane.status === 'error') {\n return wrap(\n `Could not list resources: ${pane.error ?? 'unknown error'}`,\n Math.max(1, width - TOOL_INDENT.length),\n ).map((line) => this.theme.fg('warning', `${TOOL_INDENT}${line}`));\n }\n const items = pane.items ?? [];\n if (items.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No resources`)];\n return items.flatMap((resource) => {\n const lines = [\n rightAligned(\n `${TOOL_INDENT}${resource.name ?? resource.uri}`,\n this.theme.fg('dim', resource.mimeType ?? ''),\n width,\n ),\n ];\n if (resource.name) lines.push(this.theme.fg('dim', fit(`${TOOL_INDENT}${TOOL_INDENT}${resource.uri}`, width)));\n if (resource.description) {\n for (const line of wrap(resource.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n return lines;\n });\n }\n\n private detailLines(width: number, height: number): string[] {\n const server = this.current();\n if (!server) return [this.theme.fg('dim', EMPTY_MESSAGE)];\n\n const summary = [\n stateLabel(server),\n `${server.tools.length} ${server.tools.length === 1 ? 'tool' : 'tools'}`,\n `${server.resourceCount} res`,\n ].join(' · ');\n const header = [\n this.theme.bold(server.name),\n '',\n ...this.field('State', summary, width),\n ...this.field('Error', server.error ?? FIELD_FALLBACK, width),\n // The one thing on this pane the user has to act on outside the terminal, so\n // it is never abbreviated: a truncated URL cannot be opened.\n ...(server.authorizationUrl ? ['', ...this.authorizationLines(server.authorizationUrl, width)] : []),\n '',\n this.controlLine(server, width),\n '',\n this.tabStrip(server, width),\n '',\n ];\n\n const body = this.detailTab === 'tools' ? this.toolLines(server, width) : this.resourceLines(server, width);\n this.detailLineCount = body.length;\n this.detailViewportHeight = Math.max(1, height - header.length);\n const start = Math.min(this.detailScroll, Math.max(0, body.length - this.detailViewportHeight));\n return [...header, ...body.slice(start, start + this.detailViewportHeight)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const connected = this.servers.filter((server) => server.enabled && server.state === 'connected').length;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${connected}/${this.servers.length} connected · esc close`,\n // The chrome renders the caps and decides what fits; `footer` is only the\n // plain-sentence fallback it uses when hints are absent.\n footer: FOOTER_FALLBACK,\n footerHints: HINTS,\n };\n }\n\n /**\n * The notice line, on its own full-width row at the foot of the body.\n *\n * Not `footerRight`: the chrome drops a right-hand status whole when the row\n * cannot spare the columns, and the hint legend always claims them, so an action's\n * only feedback would be silently discarded on every terminal.\n */\n private noticeRow(width: number): string {\n return this.theme.fg('warning', fit(` ${this.actionNotice ?? ''}`, width));\n }\n\n protected renderBody(width: number, height: number): string[] {\n const paneHeight = this.actionNotice ? Math.max(1, height - 1) : height;\n const notice = this.actionNotice ? [this.noticeRow(width)] : [];\n this.bodyHeight = paneHeight;\n if (width < MIN_WIDTH) return [...this.renderStacked(width, paneHeight), ...notice];\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * ROSTER_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n const left = this.rosterLines(leftContent);\n const right = this.detailLines(rightContent, paneHeight);\n\n const divider = this.theme.fg('borderMuted', '│');\n const rows = Array.from({ length: paneHeight }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n return [...rows, ...notice];\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n this.bodyHeight = topHeight;\n const top = this.rosterLines(width);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', '─'.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n}\n\nfunction message(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nexport async function openMcpOverlay(ctx: ExtensionContext, target: McpOverlayTarget): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new McpOverlayComponent(tui, theme, target, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Key as e,matchesKey as t,truncateToWidth as n,visibleWidth as r}from"@earendil-works/pi-tui";import{DOOM_FULLSCREEN_UI_OPTIONS as i,DOOM_OVERLAY_ACCENT as a,DoomOverlay as o}from"@agimon-ai/doompi-ui/components/doomOverlay";const s=[[`↑↓`,`server`],[`⇧JK`,`scroll`],[`tab`,`pane`],[`a`,`auth`],[`d`,`disable`],[`e`,`enable`],[`r`,`reload`],[`esc`,`close`]],c=s.map(([e,t])=>`${e} ${t}`).join(` · `),l=[`tools`,`resources`],u=[`auth`,`disable`,`enable`],d={auth:`a`,disable:`d`,enable:`e`},f={auth:`reconnect`,disable:`session`,enable:void 0},p=new Map(u.map(e=>[d[e],e]));function m(e){return e?e.enabled?{auth:e.state!==`connecting`,disable:!0,enable:!1}:{auth:!1,disable:!1,enable:!0}:{auth:!1,disable:!1,enable:!1}}function h(e,t){return t?t.enabled?e===`enable`?`enable unavailable · ${t.name} is already enabled`:`${e} unavailable · ${t.name} is ${t.state}`:`${e} unavailable · ${t.name} is disabled for this session`:`${e} unavailable · no server is selected`}function g(e,t){return e.enabled?e.state===`connected`?t.fg(`success`,`●`):e.state===`connecting`?t.fg(`accent`,`◐`):e.state===`degraded`?t.fg(`warning`,`▲`):e.state===`needs-auth`?t.fg(`warning`,`✗`):e.state===`failed`?t.fg(`error`,`✗`):t.fg(`muted`,`◦`):t.fg(`dim`,`■`)}function _(e){return e.enabled?e.state:`disabled`}function v(e,t){let i=n(e,Math.max(0,t),`…`);return i+` `.repeat(Math.max(0,t-r(i)))}function y(e,t,n){let i=r(t),a=Math.max(0,n-i-1);return v(e,a)+` `.repeat(Math.max(1,n-a-i))+v(t,i)}function b(e,t){if(t<=0)return[];let n=[],r=``;for(let i of e.split(/\s+/).filter(Boolean))r.length===0?r=i:r.length+1+i.length<=t?r+=` ${i}`:(n.push(r),r=i);return r.length>0&&n.push(r),n}var x=class extends o{target;done;servers=[];selected=0;selectedName;detailTab=`tools`;detailScroll=0;detailLineCount=0;detailViewportHeight=8;bodyHeight=8;actionNotice;disposed=!1;resourcePanes=new Map;unsubscribe;constructor(e,t,n,r){super(e,t),this.target=n,this.done=r,this.refresh(),this.unsubscribe=this.target.onChange(()=>{if(this.disposed)return;this.refresh();let e=this.current();e?.authorizationUrl&&(this.actionNotice=`auth ${e.name} · waiting for you to open the URL`),this.tui.requestRender()})}dispose(){this.disposed=!0,this.unsubscribe()}refresh(){let e=this.servers[this.selected]?.name??this.selectedName;this.servers=this.target.getServers();let t=e?this.servers.findIndex(t=>t.name===e):-1;this.selected=t>=0?t:Math.min(this.selected,Math.max(0,this.servers.length-1)),this.selectedName=this.servers[this.selected]?.name}current(){return this.servers[this.selected]}handleInput(n){if(t(n,`escape`)||t(n,`ctrl+c`)||t(n,`q`)){this.done(void 0);return}if(t(n,e.shift(`k`)))return this.scrollDetail(-1);if(t(n,e.shift(`j`)))return this.scrollDetail(1);if(t(n,`up`)||t(n,`k`))return this.moveSelection(-1);if(t(n,`down`)||t(n,`j`))return this.moveSelection(1);if(t(n,`pageUp`))return this.scrollDetail(-this.detailViewportHeight);if(t(n,`pageDown`))return this.scrollDetail(this.detailViewportHeight);if(t(n,`tab`))return this.toggleTab();if(t(n,`ctrl+r`))return this.loadResources({refresh:!0});if(t(n,`r`))return this.reload();let r=p.get(n.toLowerCase());r&&this.triggerControl(r)}moveSelection(e){this.servers.length!==0&&(this.selected=Math.max(0,Math.min(this.servers.length-1,this.selected+e)),this.selectedName=this.servers[this.selected]?.name,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender())}scrollDetail(e){let t=Math.max(0,this.detailLineCount-this.detailViewportHeight);this.detailScroll=Math.max(0,Math.min(t,this.detailScroll+e)),this.tui.requestRender()}toggleTab(){this.detailTab=this.detailTab===`tools`?`resources`:`tools`,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender()}reload(){this.setNotice(`reload · dispatching…`),this.target.start().then(()=>{this.disposed||this.setNotice(`reload · reconnecting`)}).catch(e=>{this.disposed||this.setNotice(`reload · failed · ${S(e)}`)})}maybeLoadResources(){if(this.detailTab!==`resources`)return;let e=this.current();!e||!e.enabled||e.state!==`connected`||this.resourcePanes.has(e.name)||this.loadResources()}loadResources(e={}){let t=this.current();t&&(this.resourcePanes.set(t.name,{status:`loading`}),this.tui.requestRender(),this.target.listResources(t.name,e).then(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`ready`,items:e}),this.tui.requestRender())}).catch(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`error`,error:S(e)}),this.tui.requestRender())}))}triggerControl(e){let t=this.current();if(!m(t)[e]){this.setNotice(h(e,t));return}if(!t)return;if(e===`auth`){this.authorize(t.name);return}let n=e===`enable`;try{this.target.setEnabled(t.name,n),this.refresh(),this.setNotice(`${e} ${t.name} · ${n?`tools restored`:`tools withheld for this session`}`)}catch(n){this.setNotice(`${e} ${t.name} · failed · ${S(n)}`)}}async authorize(e){this.setNotice(`auth ${e} · dispatching…`);try{if(await this.target.reauthorize(e),this.disposed)return;this.resourcePanes.delete(e),this.refresh(),this.setNotice(`auth ${e} · authorized`)}catch(t){if(this.disposed)return;this.setNotice(`auth ${e} · failed · ${S(t)}`)}}setNotice(e){this.actionNotice=e,this.tui.requestRender()}rosterLines(e){if(this.servers.length===0)return[this.theme.fg(`dim`,`No servers`)];let t=Math.max(0,Math.min(this.selected-this.bodyHeight+1,Math.max(0,this.servers.length-this.bodyHeight)));return this.servers.slice(t,t+this.bodyHeight).map((n,r)=>{let i=t+r,a=i===this.selected?this.theme.fg(`accent`,`›`):` `,o=n.enabled?i===this.selected?this.theme.bold(n.name):n.name:this.theme.fg(`dim`,n.name);return y(`${a} ${g(n,this.theme)} ${o}`,this.theme.fg(`dim`,_(n)),e)})}authorizationLines(e,t){let n=Math.max(1,t-2),r=[];for(let t=0;t<e.length;t+=n)r.push(e.slice(t,t+n));return[this.theme.fg(`accent`,v(` Open this to authorize:`,t)),...r.map(e=>this.theme.fg(`text`,v(` ${e}`,t)))]}field(e,t,n){let r=Math.max(1,n-10);return b(t,r).map((t,n)=>`${this.theme.fg(`dim`,v(n===0?e:``,10))}${v(t,r)}`)}controlLine(e,t){let r=m(e),i=u.map(e=>{let t=f[e],n=`${e}${t?` (${t})`:``}`;return this.keyCap(d[e],n,r[e])});return n(` ${i.join(` `)}`,t)}keyCap(e,t,n=!0){return`${n?this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${e} `)):this.theme.fg(`dim`,` ${e} `)}${this.theme.fg(`dim`,` ${t}`)}`}tabStrip(e,t){let n=e=>e===this.detailTab?this.theme.fg(`accent`,this.theme.bold(`[ ${e} ]`)):this.theme.fg(`dim`,` ${e} `),r=e&&this.detailTab===`tools`?`${e.tools.filter(e=>e.active).length}/${e.tools.length}`:`ctrl+r refresh`;return y(` ${l.map(n).join(``)}`,this.theme.fg(`dim`,r),t)}toolLines(e,t){if(e.tools.length===0)return[this.theme.fg(`dim`,` No tools`)];let n=[];for(let r of e.tools){let i=r.active&&e.enabled;if(n.push(this.theme.fg(i?`text`:`dim`,v(` ${r.toolName}`,t))),r.description)for(let e of b(r.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${e}`))}return n}resourceLines(e,t){let n=this.resourcePanes.get(e.name);if(!n)return[this.theme.fg(`dim`,` ${e.state===`connected`&&e.enabled?`Press ctrl+r to list resources`:`${e.name} is ${_(e)} · press ctrl+r to try anyway`}`)];if(n.status===`loading`)return[this.theme.fg(`dim`,` Listing resources…`)];if(n.status===`error`)return b(`Could not list resources: ${n.error??`unknown error`}`,Math.max(1,t-2)).map(e=>this.theme.fg(`warning`,` ${e}`));let r=n.items??[];return r.length===0?[this.theme.fg(`dim`,` No resources`)]:r.flatMap(e=>{let n=[y(` ${e.name??e.uri}`,this.theme.fg(`dim`,e.mimeType??``),t)];if(e.name&&n.push(this.theme.fg(`dim`,v(` ${e.uri}`,t))),e.description)for(let r of b(e.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${r}`));return n})}detailLines(e,t){let n=this.current();if(!n)return[this.theme.fg(`dim`,`No MCP servers are configured for this session.`)];let r=[_(n),`${n.tools.length} ${n.tools.length===1?`tool`:`tools`}`,`${n.resourceCount} res`].join(` · `),i=[this.theme.bold(n.name),``,...this.field(`State`,r,e),...this.field(`Error`,n.error??`—`,e),...n.authorizationUrl?[``,...this.authorizationLines(n.authorizationUrl,e)]:[],``,this.controlLine(n,e),``,this.tabStrip(n,e),``],a=this.detailTab===`tools`?this.toolLines(n,e):this.resourceLines(n,e);this.detailLineCount=a.length,this.detailViewportHeight=Math.max(1,t-i.length);let o=Math.min(this.detailScroll,Math.max(0,a.length-this.detailViewportHeight));return[...i,...a.slice(o,o+this.detailViewportHeight)]}getChrome(){let e=this.servers.filter(e=>e.enabled&&e.state===`connected`).length;return{title:`MCP`,accent:a,breadcrumb:`SPC › e / m · mcp`,headerRight:`${e}/${this.servers.length} connected · esc close`,footer:c,footerHints:s}}noticeRow(e){return this.theme.fg(`warning`,v(` ${this.actionNotice??``}`,e))}renderBody(e,t){let r=this.actionNotice?Math.max(1,t-1):t,i=this.actionNotice?[this.noticeRow(e)]:[];if(this.bodyHeight=r,e<48)return[...this.renderStacked(e,r),...i];let a=Math.max(18,Math.floor((e-1)*.3333333333333333)),o=Math.max(1,e-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),u=this.detailLines(c,r),d=this.theme.fg(`borderMuted`,`│`);return[...Array.from({length:r},(t,r)=>{let i=`${v(l[r]??``,s)} ${d} ${v(u[r]??``,c)}`;return n(i,e,`…`)}),...i]}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1);this.bodyHeight=n;let i=this.rosterLines(e),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}};function S(e){return e instanceof Error?e.message:String(e)}async function C(e,t){await e.ui.custom((e,n,r,i)=>new x(e,n,t,i),i)}export{x as McpOverlayComponent,C as openMcpOverlay};
|
|
2
|
+
//# sourceMappingURL=mcpOverlay.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpOverlay.mjs","names":[],"sources":["../../src/tui/mcpOverlay.ts"],"sourcesContent":["/**\n * MCP browser: the `SPC e m` overlay.\n *\n * A roster of this session's MCP servers on the left, and on the right what the\n * selected one offers plus the controls that act on it. It replaces a `/mcp status`\n * text blob that could be read but not acted on.\n *\n * DESIGN PATTERNS:\n * - Controls always list every action, unavailable ones dimmed rather than hidden,\n * so the key map never shifts under the user\n * - Every collaborator arrives through `McpOverlayTarget`, so the component renders\n * and dispatches in a test with no terminal and no session behind it\n *\n * The roster stays a flat list and the detail pane a tab strip rather than a second\n * navigation level: selection is one index, and every row is assertable as a string.\n *\n * Repainting is driven by `target.onChange`, not a timer. Servers connect on their\n * own schedule and the session already announces each one as it folds in, so a poll\n * here would only be sampling an event that had already fired.\n *\n * Disabling is a visibility control, not a permission boundary: Pi 0.84 cannot\n * unregister a tool, so a disabled server's tools leave the active list but stay\n * registered and callable. `DirectToolFilter` has the same property.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { McpOverlayTarget, McpResourceView, McpServerView } from '../types.ts';\n\nconst TITLE = 'MCP';\nconst BREADCRUMB = 'SPC › e / m · mcp';\n/**\n * Ordered by what a user reaches for first, because the chrome drops trailing\n * hints rather than shrinking them when the terminal is narrow.\n *\n * Scrolling earns a slot: a long tool list is the common case (the proxy alone\n * carries twelve), and without a legend entry the keys that reach the rest of it\n * are undiscoverable.\n */\nconst HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'server'],\n ['⇧JK', 'scroll'],\n ['tab', 'pane'],\n ['a', 'auth'],\n ['d', 'disable'],\n ['e', 'enable'],\n ['r', 'reload'],\n ['esc', 'close'],\n];\n/** Derived so the plain-text fallback cannot drift from the caps above it. */\nconst FOOTER_FALLBACK = HINTS.map(([key, label]) => `${key} ${label}`).join(' · ');\n/** Matches the chrome's spacing between footer caps, so both legends read alike. */\nconst CAP_SEPARATOR = ' ';\n\nconst MIN_WIDTH = 48;\n/** A third for the roster, two thirds for what the server offers. */\nconst ROSTER_PANE_RATIO = 1 / 3;\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\nconst ELLIPSIS = '…';\nconst LABEL_COLUMN = 10;\nconst FIELD_FALLBACK = '—';\nconst EMPTY_MESSAGE = 'No MCP servers are configured for this session.';\nconst TOOL_INDENT = ' ';\n\nexport type McpDetailTab = 'tools' | 'resources';\nconst DETAIL_TABS: readonly McpDetailTab[] = ['tools', 'resources'];\n\ntype McpActionName = 'auth' | 'disable' | 'enable';\n\nconst CONTROL_ORDER: readonly McpActionName[] = ['auth', 'disable', 'enable'];\nconst CONTROL_KEY: Record<McpActionName, string> = { auth: 'a', disable: 'd', enable: 'e' };\n/** Short qualifier where the verb alone would understate what the key does. */\nconst CONTROL_HINT: Record<McpActionName, string | undefined> = {\n auth: 'reconnect',\n disable: 'session',\n enable: undefined,\n};\nconst CONTROL_BY_KEY = new Map<string, McpActionName>(CONTROL_ORDER.map((action) => [CONTROL_KEY[action], action]));\n\n/**\n * What each control can do to a server in its current condition.\n *\n * `auth` reconnects, so it is offered wherever a reconnect is meaningful, including\n * a server that has not been dialled yet. It is withheld only mid-connect, because\n * reauthorizing disconnects first and would tear down the attempt in flight.\n */\nfunction controlAvailability(server: McpServerView | undefined): Record<McpActionName, boolean> {\n if (!server) return { auth: false, disable: false, enable: false };\n if (!server.enabled) return { auth: false, disable: false, enable: true };\n return { auth: server.state !== 'connecting', disable: true, enable: false };\n}\n\nfunction unavailableReason(action: McpActionName, server: McpServerView | undefined): string {\n if (!server) return `${action} unavailable · no server is selected`;\n if (!server.enabled) return `${action} unavailable · ${server.name} is disabled for this session`;\n if (action === 'enable') return `enable unavailable · ${server.name} is already enabled`;\n return `${action} unavailable · ${server.name} is ${server.state}`;\n}\n\nfunction statusGlyph(server: McpServerView, theme: Theme): string {\n if (!server.enabled) return theme.fg('dim', '■');\n if (server.state === 'connected') return theme.fg('success', '●');\n if (server.state === 'connecting') return theme.fg('accent', '◐');\n if (server.state === 'degraded') return theme.fg('warning', '▲');\n if (server.state === 'needs-auth') return theme.fg('warning', '✗');\n if (server.state === 'failed') return theme.fg('error', '✗');\n return theme.fg('muted', '◦');\n}\n\n/** What the roster row and the detail header both call the server's condition. */\nfunction stateLabel(server: McpServerView): string {\n return server.enabled ? server.state : 'disabled';\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (current.length + 1 + word.length <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** What the resources pane knows about one server. Absent until the pane asks. */\ninterface ResourcePane {\n status: 'loading' | 'ready' | 'error';\n items?: readonly McpResourceView[];\n error?: string;\n}\n\nexport class McpOverlayComponent extends DoomOverlay {\n private servers: readonly McpServerView[] = [];\n private selected = 0;\n private selectedName: string | undefined;\n private detailTab: McpDetailTab = 'tools';\n private detailScroll = 0;\n private detailLineCount = 0;\n private detailViewportHeight = 8;\n private bodyHeight = 8;\n private actionNotice: string | undefined;\n private disposed = false;\n private readonly resourcePanes = new Map<string, ResourcePane>();\n private readonly unsubscribe: () => void;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly target: McpOverlayTarget,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n this.refresh();\n this.unsubscribe = this.target.onChange(() => {\n if (this.disposed) return;\n this.refresh();\n // A flow that has surfaced its URL is no longer dispatching, it is waiting on\n // the person reading this, and the notice has to stop saying otherwise.\n const server = this.current();\n if (server?.authorizationUrl) this.actionNotice = `auth ${server.name} · waiting for you to open the URL`;\n this.tui.requestRender();\n });\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribe();\n }\n\n /** Re-reads the roster, keeping the cursor on the server it was already on. */\n private refresh(): void {\n const previousName = this.servers[this.selected]?.name ?? this.selectedName;\n this.servers = this.target.getServers();\n const preserved = previousName ? this.servers.findIndex((server) => server.name === previousName) : -1;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.servers.length - 1));\n this.selectedName = this.servers[this.selected]?.name;\n }\n\n private current(): McpServerView | undefined {\n return this.servers[this.selected];\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) return this.scrollDetail(-1);\n if (matchesKey(data, Key.shift('j'))) return this.scrollDetail(1);\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) return this.moveSelection(-1);\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) return this.moveSelection(1);\n if (matchesKey(data, 'pageUp')) return this.scrollDetail(-this.detailViewportHeight);\n if (matchesKey(data, 'pageDown')) return this.scrollDetail(this.detailViewportHeight);\n if (matchesKey(data, 'tab')) return this.toggleTab();\n // Checked before the single-letter controls: ctrl+r is the deliberate path that\n // discards what a server said about its resources and asks again.\n if (matchesKey(data, 'ctrl+r')) return this.loadResources({ refresh: true });\n if (matchesKey(data, 'r')) return this.reload();\n const action = CONTROL_BY_KEY.get(data.toLowerCase());\n if (action) this.triggerControl(action);\n }\n\n private moveSelection(delta: number): void {\n if (this.servers.length === 0) return;\n this.selected = Math.max(0, Math.min(this.servers.length - 1, this.selected + delta));\n this.selectedName = this.servers[this.selected]?.name;\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private scrollDetail(delta: number): void {\n const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);\n this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));\n this.tui.requestRender();\n }\n\n private toggleTab(): void {\n this.detailTab = this.detailTab === 'tools' ? 'resources' : 'tools';\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private reload(): void {\n this.setNotice('reload · dispatching…');\n // Not awaited: a server that never answers must not wedge the overlay. The\n // roster repaints through onChange as each one lands.\n void this.target\n .start()\n .then(() => {\n if (!this.disposed) this.setNotice('reload · reconnecting');\n })\n .catch((cause: unknown) => {\n if (!this.disposed) this.setNotice(`reload · failed · ${message(cause)}`);\n });\n }\n\n /**\n * Lists the selected server's resources, but only where a listing is meaningful.\n *\n * A server that has not connected is left alone: turning a `tab` keypress into a\n * silent dial-out is not what switching panes should mean.\n */\n private maybeLoadResources(): void {\n if (this.detailTab !== 'resources') return;\n const server = this.current();\n if (!server || !server.enabled || server.state !== 'connected') return;\n if (this.resourcePanes.has(server.name)) return;\n this.loadResources();\n }\n\n private loadResources(options: { refresh?: boolean } = {}): void {\n const server = this.current();\n if (!server) return;\n this.resourcePanes.set(server.name, { status: 'loading' });\n this.tui.requestRender();\n void this.target\n .listResources(server.name, options)\n .then((items) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'ready', items });\n this.tui.requestRender();\n })\n .catch((cause: unknown) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'error', error: message(cause) });\n this.tui.requestRender();\n });\n }\n\n private triggerControl(action: McpActionName): void {\n const server = this.current();\n if (!controlAvailability(server)[action]) {\n this.setNotice(unavailableReason(action, server));\n return;\n }\n if (!server) return;\n if (action === 'auth') {\n void this.authorize(server.name);\n return;\n }\n const enabled = action === 'enable';\n try {\n this.target.setEnabled(server.name, enabled);\n this.refresh();\n this.setNotice(`${action} ${server.name} · ${enabled ? 'tools restored' : 'tools withheld for this session'}`);\n } catch (cause) {\n this.setNotice(`${action} ${server.name} · failed · ${message(cause)}`);\n }\n }\n\n private async authorize(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · dispatching…`);\n try {\n await this.target.reauthorize(serverName);\n if (this.disposed) return;\n // The listing is about whatever the reconnect just settled on.\n this.resourcePanes.delete(serverName);\n this.refresh();\n this.setNotice(`auth ${serverName} · authorized`);\n } catch (cause) {\n if (this.disposed) return;\n this.setNotice(`auth ${serverName} · failed · ${message(cause)}`);\n }\n }\n\n private setNotice(notice: string): void {\n this.actionNotice = notice;\n this.tui.requestRender();\n }\n\n private rosterLines(width: number): string[] {\n if (this.servers.length === 0) return [this.theme.fg('dim', 'No servers')];\n const start = Math.max(\n 0,\n Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.servers.length - this.bodyHeight)),\n );\n return this.servers.slice(start, start + this.bodyHeight).map((server, offset) => {\n const index = start + offset;\n const marker = index === this.selected ? this.theme.fg('accent', '›') : ' ';\n // Dimming a disabled server states in the roster what the detail pane would\n // otherwise have to be visited to learn.\n const name = server.enabled\n ? index === this.selected\n ? this.theme.bold(server.name)\n : server.name\n : this.theme.fg('dim', server.name);\n const left = `${marker} ${statusGlyph(server, this.theme)} ${name}`;\n return rightAligned(left, this.theme.fg('dim', stateLabel(server)), width);\n });\n }\n\n /**\n * The authorization URL, wrapped across as many rows as it needs.\n *\n * Broken on width rather than truncated, and given the accent the controls use,\n * because it is the only thing in this overlay that cannot be acted on with a\n * keystroke: the user has to read it and open it somewhere else.\n */\n private authorizationLines(url: string, width: number): string[] {\n const indent = TOOL_INDENT;\n const body = Math.max(1, width - indent.length);\n const chunks: string[] = [];\n for (let start = 0; start < url.length; start += body) chunks.push(url.slice(start, start + body));\n return [\n this.theme.fg('accent', fit(`${indent}Open this to authorize:`, width)),\n ...chunks.map((chunk) => this.theme.fg('text', fit(`${indent}${chunk}`, width))),\n ];\n }\n\n private field(label: string, value: string, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) => `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${fit(line, valueWidth)}`,\n );\n }\n\n /**\n * Runtime controls as filled key caps, matching the footer legend.\n *\n * Every action is always listed; an unavailable one loses its cap fill rather\n * than its place, so the key map never shifts under the user and a control that\n * cannot act still reads as a key rather than as prose.\n */\n private controlLine(server: McpServerView | undefined, width: number): string {\n const availability = controlAvailability(server);\n const parts = CONTROL_ORDER.map((action) => {\n const hint = CONTROL_HINT[action];\n const label = `${action}${hint ? ` (${hint})` : ''}`;\n return this.keyCap(CONTROL_KEY[action], label, availability[action]);\n });\n return truncateToWidth(` ${parts.join(CAP_SEPARATOR)}`, width);\n }\n\n /** One filled key cap and its label. Unfilled when the key would do nothing. */\n private keyCap(key: string, label: string, enabled = true): string {\n const cap = enabled\n ? this.theme.bg('selectedBg', this.theme.fg('text', ` ${key} `))\n : this.theme.fg('dim', ` ${key} `);\n return `${cap}${this.theme.fg('dim', ` ${label}`)}`;\n }\n\n private tabStrip(server: McpServerView | undefined, width: number): string {\n const label = (name: McpDetailTab): string =>\n name === this.detailTab\n ? this.theme.fg('accent', this.theme.bold(`[ ${name} ]`))\n : this.theme.fg('dim', ` ${name} `);\n const counts =\n server && this.detailTab === 'tools'\n ? `${server.tools.filter((tool) => tool.active).length}/${server.tools.length}`\n : 'ctrl+r refresh';\n return rightAligned(` ${DETAIL_TABS.map(label).join('')}`, this.theme.fg('dim', counts), width);\n }\n\n private toolLines(server: McpServerView, width: number): string[] {\n if (server.tools.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No tools`)];\n const lines: string[] = [];\n for (const tool of server.tools) {\n // Colour separates what the model can call from what is merely registered,\n // so a withheld tool never has to be selected to be recognised.\n const usable = tool.active && server.enabled;\n lines.push(this.theme.fg(usable ? 'text' : 'dim', fit(`${TOOL_INDENT}${tool.toolName}`, width)));\n if (tool.description) {\n for (const line of wrap(tool.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n }\n return lines;\n }\n\n private resourceLines(server: McpServerView, width: number): string[] {\n const pane = this.resourcePanes.get(server.name);\n if (!pane) {\n return [\n this.theme.fg(\n 'dim',\n `${TOOL_INDENT}${server.state === 'connected' && server.enabled ? 'Press ctrl+r to list resources' : `${server.name} is ${stateLabel(server)} · press ctrl+r to try anyway`}`,\n ),\n ];\n }\n if (pane.status === 'loading') return [this.theme.fg('dim', `${TOOL_INDENT}Listing resources…`)];\n if (pane.status === 'error') {\n return wrap(\n `Could not list resources: ${pane.error ?? 'unknown error'}`,\n Math.max(1, width - TOOL_INDENT.length),\n ).map((line) => this.theme.fg('warning', `${TOOL_INDENT}${line}`));\n }\n const items = pane.items ?? [];\n if (items.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No resources`)];\n return items.flatMap((resource) => {\n const lines = [\n rightAligned(\n `${TOOL_INDENT}${resource.name ?? resource.uri}`,\n this.theme.fg('dim', resource.mimeType ?? ''),\n width,\n ),\n ];\n if (resource.name) lines.push(this.theme.fg('dim', fit(`${TOOL_INDENT}${TOOL_INDENT}${resource.uri}`, width)));\n if (resource.description) {\n for (const line of wrap(resource.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n return lines;\n });\n }\n\n private detailLines(width: number, height: number): string[] {\n const server = this.current();\n if (!server) return [this.theme.fg('dim', EMPTY_MESSAGE)];\n\n const summary = [\n stateLabel(server),\n `${server.tools.length} ${server.tools.length === 1 ? 'tool' : 'tools'}`,\n `${server.resourceCount} res`,\n ].join(' · ');\n const header = [\n this.theme.bold(server.name),\n '',\n ...this.field('State', summary, width),\n ...this.field('Error', server.error ?? FIELD_FALLBACK, width),\n // The one thing on this pane the user has to act on outside the terminal, so\n // it is never abbreviated: a truncated URL cannot be opened.\n ...(server.authorizationUrl ? ['', ...this.authorizationLines(server.authorizationUrl, width)] : []),\n '',\n this.controlLine(server, width),\n '',\n this.tabStrip(server, width),\n '',\n ];\n\n const body = this.detailTab === 'tools' ? this.toolLines(server, width) : this.resourceLines(server, width);\n this.detailLineCount = body.length;\n this.detailViewportHeight = Math.max(1, height - header.length);\n const start = Math.min(this.detailScroll, Math.max(0, body.length - this.detailViewportHeight));\n return [...header, ...body.slice(start, start + this.detailViewportHeight)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const connected = this.servers.filter((server) => server.enabled && server.state === 'connected').length;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${connected}/${this.servers.length} connected · esc close`,\n // The chrome renders the caps and decides what fits; `footer` is only the\n // plain-sentence fallback it uses when hints are absent.\n footer: FOOTER_FALLBACK,\n footerHints: HINTS,\n };\n }\n\n /**\n * The notice line, on its own full-width row at the foot of the body.\n *\n * Not `footerRight`: the chrome drops a right-hand status whole when the row\n * cannot spare the columns, and the hint legend always claims them, so an action's\n * only feedback would be silently discarded on every terminal.\n */\n private noticeRow(width: number): string {\n return this.theme.fg('warning', fit(` ${this.actionNotice ?? ''}`, width));\n }\n\n protected renderBody(width: number, height: number): string[] {\n const paneHeight = this.actionNotice ? Math.max(1, height - 1) : height;\n const notice = this.actionNotice ? [this.noticeRow(width)] : [];\n this.bodyHeight = paneHeight;\n if (width < MIN_WIDTH) return [...this.renderStacked(width, paneHeight), ...notice];\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * ROSTER_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n const left = this.rosterLines(leftContent);\n const right = this.detailLines(rightContent, paneHeight);\n\n const divider = this.theme.fg('borderMuted', '│');\n const rows = Array.from({ length: paneHeight }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n return [...rows, ...notice];\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n this.bodyHeight = topHeight;\n const top = this.rosterLines(width);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', '─'.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n}\n\nfunction message(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nexport async function openMcpOverlay(ctx: ExtensionContext, target: McpOverlayTarget): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new McpOverlayComponent(tui, theme, target, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
|
package/dist/types.d.cts
ADDED
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agimon-ai/doompi-mcp",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "MCP server tools for Doompi",
|
|
5
|
+
"homepage": "https://agimon.ai",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Vuong Ngo",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"jsnext:main": "./dist/index.mjs",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"require": "./dist/index.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./extensions/pi": {
|
|
23
|
+
"types": "./dist/extensions/pi.d.mts",
|
|
24
|
+
"import": "./dist/extensions/pi.mjs",
|
|
25
|
+
"require": "./dist/extensions/pi.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./extensions/doom": {
|
|
28
|
+
"types": "./dist/extensions/doom.d.mts",
|
|
29
|
+
"import": "./dist/extensions/doom.mjs",
|
|
30
|
+
"require": "./dist/extensions/doom.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
39
|
+
"@napi-rs/keyring": "1.3.0",
|
|
40
|
+
"typebox": "1.1.38",
|
|
41
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.0",
|
|
42
|
+
"@agimon-ai/mcp-proxy": "0.30.9",
|
|
43
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
47
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
48
|
+
"@types/node": "26.1.1",
|
|
49
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
50
|
+
"tsdown": "0.22.14",
|
|
51
|
+
"typescript": "6.0.3",
|
|
52
|
+
"vitest": "4.1.10"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
56
|
+
"@earendil-works/pi-tui": "0.84.1"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@earendil-works/pi-coding-agent": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@earendil-works/pi-tui": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22.19.0"
|
|
68
|
+
},
|
|
69
|
+
"pi": {
|
|
70
|
+
"extensions": [
|
|
71
|
+
"./dist/extensions/pi.mjs"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "tsdown",
|
|
76
|
+
"test": "vitest --run",
|
|
77
|
+
"typecheck": "tsc --noEmit",
|
|
78
|
+
"lint": "oxlint . && oxfmt . --check",
|
|
79
|
+
"fixcode": "oxlint . --fix && oxfmt ."
|
|
80
|
+
}
|
|
81
|
+
}
|