@agimon-ai/doompi-mcp 0.0.1-alpha.2 → 0.0.1-alpha.4
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/dist/services/configSources.cjs +1 -1
- package/dist/services/configSources.cjs.map +1 -1
- package/dist/services/configSources.d.cts +15 -32
- package/dist/services/configSources.d.cts.map +1 -1
- package/dist/services/configSources.d.mts +15 -32
- package/dist/services/configSources.d.mts.map +1 -1
- package/dist/services/configSources.mjs +1 -1
- package/dist/services/configSources.mjs.map +1 -1
- package/dist/services/mcpCatalog.cjs.map +1 -1
- package/dist/services/mcpCatalog.mjs.map +1 -1
- package/dist/services/mcpRuntime.cjs +1 -1
- package/dist/services/mcpRuntime.mjs +1 -1
- package/dist/services/mcpSession.cjs +1 -1
- package/dist/services/mcpSession.cjs.map +1 -1
- package/dist/services/mcpSession.mjs +1 -1
- package/dist/services/mcpSession.mjs.map +1 -1
- package/package.json +5 -4
- package/dist/services/sharedRuntime.cjs +0 -2
- package/dist/services/sharedRuntime.cjs.map +0 -1
- package/dist/services/sharedRuntime.mjs +0 -2
- package/dist/services/sharedRuntime.mjs.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:crypto"),n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("yaml");const a=`mcp-proxy`,o=new Set([a,`agiflow-proxy`]);function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){let t=r.default.extname(e).toLowerCase();return t===`.yaml`||t===`.yml`}function l(e){try{let t=n.default.readFileSync(e,`utf8`),r=c(e)?(0,i.parse)(t):JSON.parse(t);return s(r)?r:void 0}catch{return}}function u(e){return s(e?.mcpServers)?e.mcpServers:{}}function d(e){return!s(e)||e.disabled!==!0}function f(e){return o.has(e)}function p(e){let t=u(l(r.default.join(e,`.mcp.json`))),i=[...o].map(e=>t[e]).find(e=>s(e))?.args,a=Array.isArray(i)?i:[],c=a.indexOf(`--config`),d=a[c+1],f=c>=0&&typeof d==`string`?r.default.resolve(e,d):r.default.join(e,`mcp-config.yaml`);return n.default.existsSync(f)?f:void 0}function m(e,t){return!t||t.length===0||t.includes(e)}function h(e,n,i){let a=(0,t.createHash)(`sha256`).update(e).update(`\0`).update(n).digest(`hex`).slice(0,16),o=c(e)?`.yaml`:`.json`;return r.default.join(i,`mcp-${a}${o}`)}function g(e,t){return c(e)?(0,i.stringify)(t):`${JSON.stringify(t,null,2)}\n`}function _(e,t,r,i){let a=l(e);if(!a)return;let o=u(a),s={},c=[];for(let[e,n]of Object.entries(o))i.stripProxyWrappers&&f(e)||!m(e,t)?c.push(e):s[e]=n;let p=Object.entries(s).filter(([,e])=>d(e)).map(([e])=>e);if(c.length===0)return{path:e,kept:p,dropped:c};n.default.mkdirSync(r,{mode:448,recursive:!0});let _=g(e,{...a,mcpServers:s}),v=h(e,_,r);return n.default.writeFileSync(v,_,{mode:384}),{path:v,kept:p,dropped:c}}function v(e){let t=[r.default.join(e.repoRoot,`.mcp.json`),...(e.pluginConfigPaths??[]).map(t=>r.default.resolve(e.repoRoot,t))].filter(e=>n.default.existsSync(e)),i=[],a=[],o=[];for(let n of t){let t=_(n,e.allowlist?.servers,e.stagingDirectory,{stripProxyWrappers:!0});t&&(i.push(t.path),a.push(...t.kept),o.push(...t.dropped))}let s=p(e.repoRoot),c=s?_(s,e.allowlist?.proxy,e.stagingDirectory,{stripProxyWrappers:!0}):void 0;return c&&o.push(...c.dropped),{shared:{configPaths:c?[c.path]:[],serverNames:c?c.kept:[]},sessionLocal:{configPaths:i,serverNames:[...new Set(a)]},droppedServers:[...new Set(o)]}}exports.PROXY_SERVER_NAME=a,exports.buildMcpConfigGroups=v,exports.resolveSharedConfigPath=p;
|
|
2
2
|
//# sourceMappingURL=configSources.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configSources.cjs","names":["fs","path","createHash"],"sources":["../../src/services/configSources.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/** Server name in `.mcp.json` whose upstreams are declared in `mcp-config.yaml`. */\nexport const PROXY_SERVER_NAME = 'agiflow-proxy';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst DEFAULT_SHARED_CONFIG = 'mcp-config.yaml';\nconst CONFIG_ARGUMENT = '--config';\n\ntype JsonObject = Record<string, unknown>;\n\n/** Domain policy. An absent or empty list means \"keep everything\". */\nexport interface McpAllowlist {\n /** Servers declared across the `.mcp.json` layers. */\n servers?: string[];\n /** Upstreams declared in `mcp-config.yaml`. */\n proxy?: string[];\n}\n\nexport interface McpConfigGroupsInput {\n repoRoot: string;\n /** Absolute paths to the `.mcp.json` of each plugin selected for this session. */\n pluginConfigPaths?: string[];\n allowlist?: McpAllowlist;\n /** Where filtered copies land. Only written to when the allowlist drops a server. */\n stagingDirectory: string;\n}\n\n/**\n * Worktree-level servers, reached through the shared HTTP runtime.\n *\n * The runtime is shared by every session in the worktree, so its config must stay\n * identical no matter which domains a session selected. The allowlist therefore\n * travels as `allowedUpstreams` and is applied when tools are registered, not by\n * rewriting the config the runtime starts from.\n */\nexport interface SharedConfigGroup {\n configPaths: string[];\n allowedUpstreams?: string[];\n}\n\n/**\n * Servers this session connects in process.\n *\n * Plugin servers live here rather than in the shared runtime: sessions in one\n * worktree routinely select different plugin sets, and folding them into the\n * shared runtime would let one session's plugins leak into another's.\n */\nexport interface SessionConfigGroup {\n configPaths: string[];\n /**\n * Servers these layers declare, after filtering.\n *\n * Reported so a session can list what it is about to connect to before anything\n * has answered, rather than looking empty until the first server lands.\n */\n serverNames: string[];\n}\n\nexport interface McpConfigGroups {\n shared: SharedConfigGroup;\n sessionLocal: SessionConfigGroup;\n /** Servers removed before mcp-proxy saw them, for status reporting. */\n droppedServers: string[];\n}\n\nfunction readJsonObject(filePath: string): JsonObject | undefined {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as JsonObject) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolves the proxy config the shared runtime is started from.\n *\n * `.mcp.json` is the source of truth: its `agiflow-proxy` entry already names the\n * file, so a repo that renamed or relocated it keeps working.\n */\nexport function resolveSharedConfigPath(repoRoot: string): string | undefined {\n const rootConfig = readJsonObject(path.join(repoRoot, '.mcp.json'));\n const servers = (rootConfig?.mcpServers ?? {}) as Record<string, JsonObject | undefined>;\n const rawArgs: unknown = servers[PROXY_SERVER_NAME]?.args;\n const args = Array.isArray(rawArgs) ? (rawArgs as unknown[]) : [];\n const declared = args[args.indexOf(CONFIG_ARGUMENT) + 1];\n const candidate =\n args.includes(CONFIG_ARGUMENT) && typeof declared === 'string'\n ? path.resolve(repoRoot, declared)\n : path.join(repoRoot, DEFAULT_SHARED_CONFIG);\n return fs.existsSync(candidate) ? candidate : undefined;\n}\n\n/** Empty and absent allowlists both mean \"keep everything\". */\nfunction keepsServer(name: string, allowed: string[] | undefined): boolean {\n if (name === PROXY_SERVER_NAME) return false;\n if (!allowed || allowed.length === 0) return true;\n return allowed.includes(name);\n}\n\n/**\n * Rewrites one `.mcp.json` layer with the disallowed servers removed.\n *\n * Entries are dropped from the raw document rather than marked disabled so a\n * filtered stdio server is never spawned at all. Only keys of `mcpServers` are\n * touched, so nothing else in the layer can be lost in translation.\n */\nfunction filterLayer(\n filePath: string,\n allowed: string[] | undefined,\n stagingDirectory: string,\n): { path: string; kept: string[]; dropped: string[] } | undefined {\n const config = readJsonObject(filePath);\n if (!config) return undefined;\n const servers = (config.mcpServers ?? {}) as Record<string, unknown>;\n const kept: Record<string, unknown> = {};\n const dropped: string[] = [];\n for (const [name, definition] of Object.entries(servers)) {\n if (keepsServer(name, allowed)) kept[name] = definition;\n else dropped.push(name);\n }\n const keptNames = Object.keys(kept);\n if (dropped.length === 0) return { path: filePath, kept: keptNames, dropped };\n\n fs.mkdirSync(stagingDirectory, { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n // Hashed so two plugins named `.mcp.json` cannot overwrite each other's copy.\n const digest = createHash('sha256').update(filePath).digest('hex').slice(0, 16);\n const stagedPath = path.join(stagingDirectory, `mcp-${digest}.json`);\n fs.writeFileSync(stagedPath, `${JSON.stringify({ ...config, mcpServers: kept }, null, 2)}\\n`, {\n mode: PRIVATE_FILE_MODE,\n });\n return { path: stagedPath, kept: keptNames, dropped };\n}\n\n/**\n * Splits this session's MCP configuration into the two groups doom-mcp connects\n * differently, applying the domain allowlist where each group allows it.\n *\n * The `agiflow-proxy` entry is always removed: doom-mcp embeds mcp-proxy, so\n * honouring that entry would spawn a second proxy and load the shared upstreams\n * twice. `.mcp.json` keeps the entry for `claude.sh` and `codex.sh`.\n */\nexport function buildMcpConfigGroups(input: McpConfigGroupsInput): McpConfigGroups {\n const layers = [\n path.join(input.repoRoot, '.mcp.json'),\n ...(input.pluginConfigPaths ?? []).map((candidate) => path.resolve(input.repoRoot, candidate)),\n ].filter((candidate) => fs.existsSync(candidate));\n\n const configPaths: string[] = [];\n const serverNames: string[] = [];\n const droppedServers: string[] = [];\n for (const layer of layers) {\n const filtered = filterLayer(layer, input.allowlist?.servers, input.stagingDirectory);\n if (!filtered) continue;\n configPaths.push(filtered.path);\n serverNames.push(...filtered.kept);\n droppedServers.push(...filtered.dropped);\n }\n\n const sharedConfigPath = resolveSharedConfigPath(input.repoRoot);\n const allowedUpstreams = input.allowlist?.proxy?.length ? input.allowlist.proxy : undefined;\n\n return {\n shared: { configPaths: sharedConfigPath ? [sharedConfigPath] : [], allowedUpstreams },\n // A later layer overriding an earlier one contributes the same name twice.\n sessionLocal: { configPaths, serverNames: [...new Set(serverNames)] },\n droppedServers: [...new Set(droppedServers)],\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"configSources.cjs","names":["path","fs","parseYaml","createHash","stringifyYaml"],"sources":["../../src/services/configSources.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\n/** Wrapper entry used by non-Doom clients to expose `mcp-config.yaml` as one MCP. */\nexport const PROXY_SERVER_NAME = 'mcp-proxy';\n\nconst PROXY_SERVER_ALIASES = new Set([PROXY_SERVER_NAME, 'agiflow-proxy']);\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst DEFAULT_UPSTREAM_CONFIG = 'mcp-config.yaml';\nconst CONFIG_ARGUMENT = '--config';\n\ntype JsonObject = Record<string, unknown>;\n\n/** Domain policy. An absent or empty list means \"keep everything\". */\nexport interface McpAllowlist {\n /** Servers declared across the `.mcp.json` layers. */\n servers?: string[];\n /** Servers declared in the config referenced by the proxy wrapper. */\n proxy?: string[];\n}\n\nexport interface McpConfigGroupsInput {\n repoRoot: string;\n /** Absolute paths to the `.mcp.json` of each plugin selected for this session. */\n pluginConfigPaths?: string[];\n allowlist?: McpAllowlist;\n /** Where filtered copies land. Written whenever policy or wrapper removal changes a layer. */\n stagingDirectory: string;\n}\n\n/** Worktree-level upstreams that Doom connects directly in process. */\nexport interface SharedConfigGroup {\n configPaths: string[];\n /** Enabled servers declared by these layers, after filtering. */\n serverNames: string[];\n}\n\n/** Plugin and repository servers that Doom connects directly in process. */\nexport interface SessionConfigGroup {\n configPaths: string[];\n /** Enabled servers declared by these layers, after filtering. */\n serverNames: string[];\n}\n\nexport interface McpConfigGroups {\n shared: SharedConfigGroup;\n sessionLocal: SessionConfigGroup;\n /** Servers removed before the embedded MCP client saw them, for diagnostics. */\n droppedServers: string[];\n}\n\ninterface FilteredLayer {\n path: string;\n kept: string[];\n dropped: string[];\n}\n\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isYamlPath(filePath: string): boolean {\n const extension = path.extname(filePath).toLowerCase();\n return extension === '.yaml' || extension === '.yml';\n}\n\nfunction readConfigObject(filePath: string): JsonObject | undefined {\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n const parsed: unknown = isYamlPath(filePath) ? parseYaml(content) : JSON.parse(content);\n return isObject(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction serverDefinitions(config: JsonObject | undefined): Record<string, unknown> {\n return isObject(config?.mcpServers) ? config.mcpServers : {};\n}\n\nfunction isEnabledDefinition(definition: unknown): boolean {\n return !isObject(definition) || definition.disabled !== true;\n}\n\nfunction isProxyWrapper(name: string): boolean {\n return PROXY_SERVER_ALIASES.has(name);\n}\n\n/**\n * Resolves the upstream config referenced by the repository proxy wrapper.\n *\n * The wrapper remains useful to Claude, Codex, and other external clients. Doom\n * embeds the same client/runtime code, so it reads this path and connects each\n * declared server itself instead of starting the wrapper as a nested MCP.\n */\nexport function resolveSharedConfigPath(repoRoot: string): string | undefined {\n const servers = serverDefinitions(readConfigObject(path.join(repoRoot, '.mcp.json')));\n const proxy = [...PROXY_SERVER_ALIASES]\n .map((name) => servers[name])\n .find((definition): definition is JsonObject => isObject(definition));\n const rawArgs: unknown = proxy?.args;\n const args = Array.isArray(rawArgs) ? (rawArgs as unknown[]) : [];\n const configArgumentIndex = args.indexOf(CONFIG_ARGUMENT);\n const declared = args[configArgumentIndex + 1];\n const candidate =\n configArgumentIndex >= 0 && typeof declared === 'string'\n ? path.resolve(repoRoot, declared)\n : path.join(repoRoot, DEFAULT_UPSTREAM_CONFIG);\n return fs.existsSync(candidate) ? candidate : undefined;\n}\n\n/** Empty and absent allowlists both mean \"keep everything\". */\nfunction isAllowed(name: string, allowed: string[] | undefined): boolean {\n return !allowed || allowed.length === 0 || allowed.includes(name);\n}\n\nfunction stagedPath(filePath: string, contents: string, stagingDirectory: string): string {\n // The filtered bytes are part of the identity because two live sessions can\n // select different domains. Sharing a path would let the later one rewrite the\n // configuration underneath the first one's deferred startup.\n const digest = createHash('sha256').update(filePath).update('\\0').update(contents).digest('hex').slice(0, 16);\n const extension = isYamlPath(filePath) ? '.yaml' : '.json';\n return path.join(stagingDirectory, `mcp-${digest}${extension}`);\n}\n\nfunction serializeConfig(filePath: string, config: JsonObject): string {\n return isYamlPath(filePath) ? stringifyYaml(config) : `${JSON.stringify(config, null, 2)}\\n`;\n}\n\n/**\n * Rewrites one config layer with disallowed servers removed.\n *\n * Proxy wrapper entries are stripped from `.mcp.json` layers because Doom reaches\n * their upstream config directly. Entries are removed rather than marked disabled,\n * so a filtered stdio server is never spawned at all.\n */\nfunction filterLayer(\n filePath: string,\n allowed: string[] | undefined,\n stagingDirectory: string,\n options: { stripProxyWrappers: boolean },\n): FilteredLayer | undefined {\n const config = readConfigObject(filePath);\n if (!config) return undefined;\n const servers = serverDefinitions(config);\n const kept: Record<string, unknown> = {};\n const dropped: string[] = [];\n\n for (const [name, definition] of Object.entries(servers)) {\n const shouldDrop = (options.stripProxyWrappers && isProxyWrapper(name)) || !isAllowed(name, allowed);\n if (shouldDrop) dropped.push(name);\n else kept[name] = definition;\n }\n\n const keptNames = Object.entries(kept)\n .filter(([, definition]) => isEnabledDefinition(definition))\n .map(([name]) => name);\n if (dropped.length === 0) return { path: filePath, kept: keptNames, dropped };\n\n fs.mkdirSync(stagingDirectory, { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const contents = serializeConfig(filePath, { ...config, mcpServers: kept });\n const target = stagedPath(filePath, contents, stagingDirectory);\n fs.writeFileSync(target, contents, { mode: PRIVATE_FILE_MODE });\n return { path: target, kept: keptNames, dropped };\n}\n\n/**\n * Resolves every config layer for this session and applies its domain allowlist.\n *\n * All returned layers go into one embedded `@agimon-ai/mcp-proxy` client container.\n * The package is used as a library only: its wrapper MCP never becomes a Doom MCP\n * server, so status, enable/disable, resources, and OAuth stay attached to each\n * actual downstream server.\n */\nexport function buildMcpConfigGroups(input: McpConfigGroupsInput): McpConfigGroups {\n const layers = [\n path.join(input.repoRoot, '.mcp.json'),\n ...(input.pluginConfigPaths ?? []).map((candidate) => path.resolve(input.repoRoot, candidate)),\n ].filter((candidate) => fs.existsSync(candidate));\n\n const configPaths: string[] = [];\n const serverNames: string[] = [];\n const droppedServers: string[] = [];\n for (const layer of layers) {\n const filtered = filterLayer(layer, input.allowlist?.servers, input.stagingDirectory, {\n stripProxyWrappers: true,\n });\n if (!filtered) continue;\n configPaths.push(filtered.path);\n serverNames.push(...filtered.kept);\n droppedServers.push(...filtered.dropped);\n }\n\n const upstreamPath = resolveSharedConfigPath(input.repoRoot);\n const upstream = upstreamPath\n ? filterLayer(upstreamPath, input.allowlist?.proxy, input.stagingDirectory, { stripProxyWrappers: true })\n : undefined;\n if (upstream) droppedServers.push(...upstream.dropped);\n\n return {\n shared: {\n configPaths: upstream ? [upstream.path] : [],\n serverNames: upstream ? upstream.kept : [],\n },\n // A later layer overriding an earlier one contributes the same name twice.\n sessionLocal: { configPaths, serverNames: [...new Set(serverNames)] },\n droppedServers: [...new Set(droppedServers)],\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/services/configSources.d.ts
|
|
2
|
-
/**
|
|
3
|
-
declare const PROXY_SERVER_NAME = "
|
|
2
|
+
/** Wrapper entry used by non-Doom clients to expose `mcp-config.yaml` as one MCP. */
|
|
3
|
+
declare const PROXY_SERVER_NAME = "mcp-proxy";
|
|
4
4
|
/** Domain policy. An absent or empty list means "keep everything". */
|
|
5
5
|
interface McpAllowlist {
|
|
6
6
|
/** Servers declared across the `.mcp.json` layers. */
|
|
7
7
|
servers?: string[];
|
|
8
|
-
/**
|
|
8
|
+
/** Servers declared in the config referenced by the proxy wrapper. */
|
|
9
9
|
proxy?: string[];
|
|
10
10
|
}
|
|
11
11
|
interface McpConfigGroupsInput {
|
|
@@ -13,51 +13,34 @@ interface McpConfigGroupsInput {
|
|
|
13
13
|
/** Absolute paths to the `.mcp.json` of each plugin selected for this session. */
|
|
14
14
|
pluginConfigPaths?: string[];
|
|
15
15
|
allowlist?: McpAllowlist;
|
|
16
|
-
/** Where filtered copies land.
|
|
16
|
+
/** Where filtered copies land. Written whenever policy or wrapper removal changes a layer. */
|
|
17
17
|
stagingDirectory: string;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Worktree-level servers, reached through the shared HTTP runtime.
|
|
21
|
-
*
|
|
22
|
-
* The runtime is shared by every session in the worktree, so its config must stay
|
|
23
|
-
* identical no matter which domains a session selected. The allowlist therefore
|
|
24
|
-
* travels as `allowedUpstreams` and is applied when tools are registered, not by
|
|
25
|
-
* rewriting the config the runtime starts from.
|
|
26
|
-
*/
|
|
19
|
+
/** Worktree-level upstreams that Doom connects directly in process. */
|
|
27
20
|
interface SharedConfigGroup {
|
|
28
21
|
configPaths: string[];
|
|
29
|
-
|
|
22
|
+
/** Enabled servers declared by these layers, after filtering. */
|
|
23
|
+
serverNames: string[];
|
|
30
24
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Servers this session connects in process.
|
|
33
|
-
*
|
|
34
|
-
* Plugin servers live here rather than in the shared runtime: sessions in one
|
|
35
|
-
* worktree routinely select different plugin sets, and folding them into the
|
|
36
|
-
* shared runtime would let one session's plugins leak into another's.
|
|
37
|
-
*/
|
|
25
|
+
/** Plugin and repository servers that Doom connects directly in process. */
|
|
38
26
|
interface SessionConfigGroup {
|
|
39
27
|
configPaths: string[];
|
|
40
|
-
/**
|
|
41
|
-
* Servers these layers declare, after filtering.
|
|
42
|
-
*
|
|
43
|
-
* Reported so a session can list what it is about to connect to before anything
|
|
44
|
-
* has answered, rather than looking empty until the first server lands.
|
|
45
|
-
*/
|
|
28
|
+
/** Enabled servers declared by these layers, after filtering. */
|
|
46
29
|
serverNames: string[];
|
|
47
30
|
}
|
|
48
31
|
interface McpConfigGroups {
|
|
49
32
|
shared: SharedConfigGroup;
|
|
50
33
|
sessionLocal: SessionConfigGroup;
|
|
51
|
-
/** Servers removed before
|
|
34
|
+
/** Servers removed before the embedded MCP client saw them, for diagnostics. */
|
|
52
35
|
droppedServers: string[];
|
|
53
36
|
}
|
|
54
37
|
/**
|
|
55
|
-
*
|
|
56
|
-
* differently, applying the domain allowlist where each group allows it.
|
|
38
|
+
* Resolves every config layer for this session and applies its domain allowlist.
|
|
57
39
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
40
|
+
* All returned layers go into one embedded `@agimon-ai/mcp-proxy` client container.
|
|
41
|
+
* The package is used as a library only: its wrapper MCP never becomes a Doom MCP
|
|
42
|
+
* server, so status, enable/disable, resources, and OAuth stay attached to each
|
|
43
|
+
* actual downstream server.
|
|
61
44
|
*/
|
|
62
45
|
declare function buildMcpConfigGroups(input: McpConfigGroupsInput): McpConfigGroups;
|
|
63
46
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configSources.d.cts","names":[],"sources":["../../src/services/configSources.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"configSources.d.cts","names":[],"sources":["../../src/services/configSources.ts"],"mappings":";;cAMa;;UAWI;;EAEf;;EAEA;;UAGe;EACf;;EAEA;EACA,YAAY;;EAEZ;;;UAIe;EACf;;EAEA;;;UAIe;EACf;;EAEA;;UAGe;EACf,QAAQ;EACR,cAAc;;EAEd;;;;;;;;;;iBA8Hc,qBAAqB,OAAO,uBAAuB"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/services/configSources.d.ts
|
|
2
|
-
/**
|
|
3
|
-
declare const PROXY_SERVER_NAME = "
|
|
2
|
+
/** Wrapper entry used by non-Doom clients to expose `mcp-config.yaml` as one MCP. */
|
|
3
|
+
declare const PROXY_SERVER_NAME = "mcp-proxy";
|
|
4
4
|
/** Domain policy. An absent or empty list means "keep everything". */
|
|
5
5
|
interface McpAllowlist {
|
|
6
6
|
/** Servers declared across the `.mcp.json` layers. */
|
|
7
7
|
servers?: string[];
|
|
8
|
-
/**
|
|
8
|
+
/** Servers declared in the config referenced by the proxy wrapper. */
|
|
9
9
|
proxy?: string[];
|
|
10
10
|
}
|
|
11
11
|
interface McpConfigGroupsInput {
|
|
@@ -13,51 +13,34 @@ interface McpConfigGroupsInput {
|
|
|
13
13
|
/** Absolute paths to the `.mcp.json` of each plugin selected for this session. */
|
|
14
14
|
pluginConfigPaths?: string[];
|
|
15
15
|
allowlist?: McpAllowlist;
|
|
16
|
-
/** Where filtered copies land.
|
|
16
|
+
/** Where filtered copies land. Written whenever policy or wrapper removal changes a layer. */
|
|
17
17
|
stagingDirectory: string;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Worktree-level servers, reached through the shared HTTP runtime.
|
|
21
|
-
*
|
|
22
|
-
* The runtime is shared by every session in the worktree, so its config must stay
|
|
23
|
-
* identical no matter which domains a session selected. The allowlist therefore
|
|
24
|
-
* travels as `allowedUpstreams` and is applied when tools are registered, not by
|
|
25
|
-
* rewriting the config the runtime starts from.
|
|
26
|
-
*/
|
|
19
|
+
/** Worktree-level upstreams that Doom connects directly in process. */
|
|
27
20
|
interface SharedConfigGroup {
|
|
28
21
|
configPaths: string[];
|
|
29
|
-
|
|
22
|
+
/** Enabled servers declared by these layers, after filtering. */
|
|
23
|
+
serverNames: string[];
|
|
30
24
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Servers this session connects in process.
|
|
33
|
-
*
|
|
34
|
-
* Plugin servers live here rather than in the shared runtime: sessions in one
|
|
35
|
-
* worktree routinely select different plugin sets, and folding them into the
|
|
36
|
-
* shared runtime would let one session's plugins leak into another's.
|
|
37
|
-
*/
|
|
25
|
+
/** Plugin and repository servers that Doom connects directly in process. */
|
|
38
26
|
interface SessionConfigGroup {
|
|
39
27
|
configPaths: string[];
|
|
40
|
-
/**
|
|
41
|
-
* Servers these layers declare, after filtering.
|
|
42
|
-
*
|
|
43
|
-
* Reported so a session can list what it is about to connect to before anything
|
|
44
|
-
* has answered, rather than looking empty until the first server lands.
|
|
45
|
-
*/
|
|
28
|
+
/** Enabled servers declared by these layers, after filtering. */
|
|
46
29
|
serverNames: string[];
|
|
47
30
|
}
|
|
48
31
|
interface McpConfigGroups {
|
|
49
32
|
shared: SharedConfigGroup;
|
|
50
33
|
sessionLocal: SessionConfigGroup;
|
|
51
|
-
/** Servers removed before
|
|
34
|
+
/** Servers removed before the embedded MCP client saw them, for diagnostics. */
|
|
52
35
|
droppedServers: string[];
|
|
53
36
|
}
|
|
54
37
|
/**
|
|
55
|
-
*
|
|
56
|
-
* differently, applying the domain allowlist where each group allows it.
|
|
38
|
+
* Resolves every config layer for this session and applies its domain allowlist.
|
|
57
39
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
40
|
+
* All returned layers go into one embedded `@agimon-ai/mcp-proxy` client container.
|
|
41
|
+
* The package is used as a library only: its wrapper MCP never becomes a Doom MCP
|
|
42
|
+
* server, so status, enable/disable, resources, and OAuth stay attached to each
|
|
43
|
+
* actual downstream server.
|
|
61
44
|
*/
|
|
62
45
|
declare function buildMcpConfigGroups(input: McpConfigGroupsInput): McpConfigGroups;
|
|
63
46
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configSources.d.mts","names":[],"sources":["../../src/services/configSources.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"configSources.d.mts","names":[],"sources":["../../src/services/configSources.ts"],"mappings":";;cAMa;;UAWI;;EAEf;;EAEA;;UAGe;EACf;;EAEA;EACA,YAAY;;EAEZ;;;UAIe;EACf;;EAEA;;;UAIe;EACf;;EAEA;;UAGe;EACf,QAAQ;EACR,cAAc;;EAEd;;;;;;;;;;iBA8Hc,qBAAqB,OAAO,uBAAuB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e
|
|
1
|
+
import{createHash as e}from"node:crypto";import t from"node:fs";import n from"node:path";import{parse as r,stringify as i}from"yaml";const a=`mcp-proxy`,o=new Set([a,`agiflow-proxy`]);function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){let t=n.extname(e).toLowerCase();return t===`.yaml`||t===`.yml`}function l(e){try{let n=t.readFileSync(e,`utf8`),i=c(e)?r(n):JSON.parse(n);return s(i)?i:void 0}catch{return}}function u(e){return s(e?.mcpServers)?e.mcpServers:{}}function d(e){return!s(e)||e.disabled!==!0}function f(e){return o.has(e)}function p(e){let r=u(l(n.join(e,`.mcp.json`))),i=[...o].map(e=>r[e]).find(e=>s(e))?.args,a=Array.isArray(i)?i:[],c=a.indexOf(`--config`),d=a[c+1],f=c>=0&&typeof d==`string`?n.resolve(e,d):n.join(e,`mcp-config.yaml`);return t.existsSync(f)?f:void 0}function m(e,t){return!t||t.length===0||t.includes(e)}function h(t,r,i){let a=e(`sha256`).update(t).update(`\0`).update(r).digest(`hex`).slice(0,16),o=c(t)?`.yaml`:`.json`;return n.join(i,`mcp-${a}${o}`)}function g(e,t){return c(e)?i(t):`${JSON.stringify(t,null,2)}\n`}function _(e,n,r,i){let a=l(e);if(!a)return;let o=u(a),s={},c=[];for(let[e,t]of Object.entries(o))i.stripProxyWrappers&&f(e)||!m(e,n)?c.push(e):s[e]=t;let p=Object.entries(s).filter(([,e])=>d(e)).map(([e])=>e);if(c.length===0)return{path:e,kept:p,dropped:c};t.mkdirSync(r,{mode:448,recursive:!0});let _=g(e,{...a,mcpServers:s}),v=h(e,_,r);return t.writeFileSync(v,_,{mode:384}),{path:v,kept:p,dropped:c}}function v(e){let r=[n.join(e.repoRoot,`.mcp.json`),...(e.pluginConfigPaths??[]).map(t=>n.resolve(e.repoRoot,t))].filter(e=>t.existsSync(e)),i=[],a=[],o=[];for(let t of r){let n=_(t,e.allowlist?.servers,e.stagingDirectory,{stripProxyWrappers:!0});n&&(i.push(n.path),a.push(...n.kept),o.push(...n.dropped))}let s=p(e.repoRoot),c=s?_(s,e.allowlist?.proxy,e.stagingDirectory,{stripProxyWrappers:!0}):void 0;return c&&o.push(...c.dropped),{shared:{configPaths:c?[c.path]:[],serverNames:c?c.kept:[]},sessionLocal:{configPaths:i,serverNames:[...new Set(a)]},droppedServers:[...new Set(o)]}}export{a as PROXY_SERVER_NAME,v as buildMcpConfigGroups,p as resolveSharedConfigPath};
|
|
2
2
|
//# sourceMappingURL=configSources.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configSources.mjs","names":[],"sources":["../../src/services/configSources.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/** Server name in `.mcp.json` whose upstreams are declared in `mcp-config.yaml`. */\nexport const PROXY_SERVER_NAME = 'agiflow-proxy';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst DEFAULT_SHARED_CONFIG = 'mcp-config.yaml';\nconst CONFIG_ARGUMENT = '--config';\n\ntype JsonObject = Record<string, unknown>;\n\n/** Domain policy. An absent or empty list means \"keep everything\". */\nexport interface McpAllowlist {\n /** Servers declared across the `.mcp.json` layers. */\n servers?: string[];\n /** Upstreams declared in `mcp-config.yaml`. */\n proxy?: string[];\n}\n\nexport interface McpConfigGroupsInput {\n repoRoot: string;\n /** Absolute paths to the `.mcp.json` of each plugin selected for this session. */\n pluginConfigPaths?: string[];\n allowlist?: McpAllowlist;\n /** Where filtered copies land. Only written to when the allowlist drops a server. */\n stagingDirectory: string;\n}\n\n/**\n * Worktree-level servers, reached through the shared HTTP runtime.\n *\n * The runtime is shared by every session in the worktree, so its config must stay\n * identical no matter which domains a session selected. The allowlist therefore\n * travels as `allowedUpstreams` and is applied when tools are registered, not by\n * rewriting the config the runtime starts from.\n */\nexport interface SharedConfigGroup {\n configPaths: string[];\n allowedUpstreams?: string[];\n}\n\n/**\n * Servers this session connects in process.\n *\n * Plugin servers live here rather than in the shared runtime: sessions in one\n * worktree routinely select different plugin sets, and folding them into the\n * shared runtime would let one session's plugins leak into another's.\n */\nexport interface SessionConfigGroup {\n configPaths: string[];\n /**\n * Servers these layers declare, after filtering.\n *\n * Reported so a session can list what it is about to connect to before anything\n * has answered, rather than looking empty until the first server lands.\n */\n serverNames: string[];\n}\n\nexport interface McpConfigGroups {\n shared: SharedConfigGroup;\n sessionLocal: SessionConfigGroup;\n /** Servers removed before mcp-proxy saw them, for status reporting. */\n droppedServers: string[];\n}\n\nfunction readJsonObject(filePath: string): JsonObject | undefined {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as JsonObject) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolves the proxy config the shared runtime is started from.\n *\n * `.mcp.json` is the source of truth: its `agiflow-proxy` entry already names the\n * file, so a repo that renamed or relocated it keeps working.\n */\nexport function resolveSharedConfigPath(repoRoot: string): string | undefined {\n const rootConfig = readJsonObject(path.join(repoRoot, '.mcp.json'));\n const servers = (rootConfig?.mcpServers ?? {}) as Record<string, JsonObject | undefined>;\n const rawArgs: unknown = servers[PROXY_SERVER_NAME]?.args;\n const args = Array.isArray(rawArgs) ? (rawArgs as unknown[]) : [];\n const declared = args[args.indexOf(CONFIG_ARGUMENT) + 1];\n const candidate =\n args.includes(CONFIG_ARGUMENT) && typeof declared === 'string'\n ? path.resolve(repoRoot, declared)\n : path.join(repoRoot, DEFAULT_SHARED_CONFIG);\n return fs.existsSync(candidate) ? candidate : undefined;\n}\n\n/** Empty and absent allowlists both mean \"keep everything\". */\nfunction keepsServer(name: string, allowed: string[] | undefined): boolean {\n if (name === PROXY_SERVER_NAME) return false;\n if (!allowed || allowed.length === 0) return true;\n return allowed.includes(name);\n}\n\n/**\n * Rewrites one `.mcp.json` layer with the disallowed servers removed.\n *\n * Entries are dropped from the raw document rather than marked disabled so a\n * filtered stdio server is never spawned at all. Only keys of `mcpServers` are\n * touched, so nothing else in the layer can be lost in translation.\n */\nfunction filterLayer(\n filePath: string,\n allowed: string[] | undefined,\n stagingDirectory: string,\n): { path: string; kept: string[]; dropped: string[] } | undefined {\n const config = readJsonObject(filePath);\n if (!config) return undefined;\n const servers = (config.mcpServers ?? {}) as Record<string, unknown>;\n const kept: Record<string, unknown> = {};\n const dropped: string[] = [];\n for (const [name, definition] of Object.entries(servers)) {\n if (keepsServer(name, allowed)) kept[name] = definition;\n else dropped.push(name);\n }\n const keptNames = Object.keys(kept);\n if (dropped.length === 0) return { path: filePath, kept: keptNames, dropped };\n\n fs.mkdirSync(stagingDirectory, { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n // Hashed so two plugins named `.mcp.json` cannot overwrite each other's copy.\n const digest = createHash('sha256').update(filePath).digest('hex').slice(0, 16);\n const stagedPath = path.join(stagingDirectory, `mcp-${digest}.json`);\n fs.writeFileSync(stagedPath, `${JSON.stringify({ ...config, mcpServers: kept }, null, 2)}\\n`, {\n mode: PRIVATE_FILE_MODE,\n });\n return { path: stagedPath, kept: keptNames, dropped };\n}\n\n/**\n * Splits this session's MCP configuration into the two groups doom-mcp connects\n * differently, applying the domain allowlist where each group allows it.\n *\n * The `agiflow-proxy` entry is always removed: doom-mcp embeds mcp-proxy, so\n * honouring that entry would spawn a second proxy and load the shared upstreams\n * twice. `.mcp.json` keeps the entry for `claude.sh` and `codex.sh`.\n */\nexport function buildMcpConfigGroups(input: McpConfigGroupsInput): McpConfigGroups {\n const layers = [\n path.join(input.repoRoot, '.mcp.json'),\n ...(input.pluginConfigPaths ?? []).map((candidate) => path.resolve(input.repoRoot, candidate)),\n ].filter((candidate) => fs.existsSync(candidate));\n\n const configPaths: string[] = [];\n const serverNames: string[] = [];\n const droppedServers: string[] = [];\n for (const layer of layers) {\n const filtered = filterLayer(layer, input.allowlist?.servers, input.stagingDirectory);\n if (!filtered) continue;\n configPaths.push(filtered.path);\n serverNames.push(...filtered.kept);\n droppedServers.push(...filtered.dropped);\n }\n\n const sharedConfigPath = resolveSharedConfigPath(input.repoRoot);\n const allowedUpstreams = input.allowlist?.proxy?.length ? input.allowlist.proxy : undefined;\n\n return {\n shared: { configPaths: sharedConfigPath ? [sharedConfigPath] : [], allowedUpstreams },\n // A later layer overriding an earlier one contributes the same name twice.\n sessionLocal: { configPaths, serverNames: [...new Set(serverNames)] },\n droppedServers: [...new Set(droppedServers)],\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"configSources.mjs","names":["parseYaml","stringifyYaml"],"sources":["../../src/services/configSources.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\n/** Wrapper entry used by non-Doom clients to expose `mcp-config.yaml` as one MCP. */\nexport const PROXY_SERVER_NAME = 'mcp-proxy';\n\nconst PROXY_SERVER_ALIASES = new Set([PROXY_SERVER_NAME, 'agiflow-proxy']);\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst DEFAULT_UPSTREAM_CONFIG = 'mcp-config.yaml';\nconst CONFIG_ARGUMENT = '--config';\n\ntype JsonObject = Record<string, unknown>;\n\n/** Domain policy. An absent or empty list means \"keep everything\". */\nexport interface McpAllowlist {\n /** Servers declared across the `.mcp.json` layers. */\n servers?: string[];\n /** Servers declared in the config referenced by the proxy wrapper. */\n proxy?: string[];\n}\n\nexport interface McpConfigGroupsInput {\n repoRoot: string;\n /** Absolute paths to the `.mcp.json` of each plugin selected for this session. */\n pluginConfigPaths?: string[];\n allowlist?: McpAllowlist;\n /** Where filtered copies land. Written whenever policy or wrapper removal changes a layer. */\n stagingDirectory: string;\n}\n\n/** Worktree-level upstreams that Doom connects directly in process. */\nexport interface SharedConfigGroup {\n configPaths: string[];\n /** Enabled servers declared by these layers, after filtering. */\n serverNames: string[];\n}\n\n/** Plugin and repository servers that Doom connects directly in process. */\nexport interface SessionConfigGroup {\n configPaths: string[];\n /** Enabled servers declared by these layers, after filtering. */\n serverNames: string[];\n}\n\nexport interface McpConfigGroups {\n shared: SharedConfigGroup;\n sessionLocal: SessionConfigGroup;\n /** Servers removed before the embedded MCP client saw them, for diagnostics. */\n droppedServers: string[];\n}\n\ninterface FilteredLayer {\n path: string;\n kept: string[];\n dropped: string[];\n}\n\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isYamlPath(filePath: string): boolean {\n const extension = path.extname(filePath).toLowerCase();\n return extension === '.yaml' || extension === '.yml';\n}\n\nfunction readConfigObject(filePath: string): JsonObject | undefined {\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n const parsed: unknown = isYamlPath(filePath) ? parseYaml(content) : JSON.parse(content);\n return isObject(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction serverDefinitions(config: JsonObject | undefined): Record<string, unknown> {\n return isObject(config?.mcpServers) ? config.mcpServers : {};\n}\n\nfunction isEnabledDefinition(definition: unknown): boolean {\n return !isObject(definition) || definition.disabled !== true;\n}\n\nfunction isProxyWrapper(name: string): boolean {\n return PROXY_SERVER_ALIASES.has(name);\n}\n\n/**\n * Resolves the upstream config referenced by the repository proxy wrapper.\n *\n * The wrapper remains useful to Claude, Codex, and other external clients. Doom\n * embeds the same client/runtime code, so it reads this path and connects each\n * declared server itself instead of starting the wrapper as a nested MCP.\n */\nexport function resolveSharedConfigPath(repoRoot: string): string | undefined {\n const servers = serverDefinitions(readConfigObject(path.join(repoRoot, '.mcp.json')));\n const proxy = [...PROXY_SERVER_ALIASES]\n .map((name) => servers[name])\n .find((definition): definition is JsonObject => isObject(definition));\n const rawArgs: unknown = proxy?.args;\n const args = Array.isArray(rawArgs) ? (rawArgs as unknown[]) : [];\n const configArgumentIndex = args.indexOf(CONFIG_ARGUMENT);\n const declared = args[configArgumentIndex + 1];\n const candidate =\n configArgumentIndex >= 0 && typeof declared === 'string'\n ? path.resolve(repoRoot, declared)\n : path.join(repoRoot, DEFAULT_UPSTREAM_CONFIG);\n return fs.existsSync(candidate) ? candidate : undefined;\n}\n\n/** Empty and absent allowlists both mean \"keep everything\". */\nfunction isAllowed(name: string, allowed: string[] | undefined): boolean {\n return !allowed || allowed.length === 0 || allowed.includes(name);\n}\n\nfunction stagedPath(filePath: string, contents: string, stagingDirectory: string): string {\n // The filtered bytes are part of the identity because two live sessions can\n // select different domains. Sharing a path would let the later one rewrite the\n // configuration underneath the first one's deferred startup.\n const digest = createHash('sha256').update(filePath).update('\\0').update(contents).digest('hex').slice(0, 16);\n const extension = isYamlPath(filePath) ? '.yaml' : '.json';\n return path.join(stagingDirectory, `mcp-${digest}${extension}`);\n}\n\nfunction serializeConfig(filePath: string, config: JsonObject): string {\n return isYamlPath(filePath) ? stringifyYaml(config) : `${JSON.stringify(config, null, 2)}\\n`;\n}\n\n/**\n * Rewrites one config layer with disallowed servers removed.\n *\n * Proxy wrapper entries are stripped from `.mcp.json` layers because Doom reaches\n * their upstream config directly. Entries are removed rather than marked disabled,\n * so a filtered stdio server is never spawned at all.\n */\nfunction filterLayer(\n filePath: string,\n allowed: string[] | undefined,\n stagingDirectory: string,\n options: { stripProxyWrappers: boolean },\n): FilteredLayer | undefined {\n const config = readConfigObject(filePath);\n if (!config) return undefined;\n const servers = serverDefinitions(config);\n const kept: Record<string, unknown> = {};\n const dropped: string[] = [];\n\n for (const [name, definition] of Object.entries(servers)) {\n const shouldDrop = (options.stripProxyWrappers && isProxyWrapper(name)) || !isAllowed(name, allowed);\n if (shouldDrop) dropped.push(name);\n else kept[name] = definition;\n }\n\n const keptNames = Object.entries(kept)\n .filter(([, definition]) => isEnabledDefinition(definition))\n .map(([name]) => name);\n if (dropped.length === 0) return { path: filePath, kept: keptNames, dropped };\n\n fs.mkdirSync(stagingDirectory, { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const contents = serializeConfig(filePath, { ...config, mcpServers: kept });\n const target = stagedPath(filePath, contents, stagingDirectory);\n fs.writeFileSync(target, contents, { mode: PRIVATE_FILE_MODE });\n return { path: target, kept: keptNames, dropped };\n}\n\n/**\n * Resolves every config layer for this session and applies its domain allowlist.\n *\n * All returned layers go into one embedded `@agimon-ai/mcp-proxy` client container.\n * The package is used as a library only: its wrapper MCP never becomes a Doom MCP\n * server, so status, enable/disable, resources, and OAuth stay attached to each\n * actual downstream server.\n */\nexport function buildMcpConfigGroups(input: McpConfigGroupsInput): McpConfigGroups {\n const layers = [\n path.join(input.repoRoot, '.mcp.json'),\n ...(input.pluginConfigPaths ?? []).map((candidate) => path.resolve(input.repoRoot, candidate)),\n ].filter((candidate) => fs.existsSync(candidate));\n\n const configPaths: string[] = [];\n const serverNames: string[] = [];\n const droppedServers: string[] = [];\n for (const layer of layers) {\n const filtered = filterLayer(layer, input.allowlist?.servers, input.stagingDirectory, {\n stripProxyWrappers: true,\n });\n if (!filtered) continue;\n configPaths.push(filtered.path);\n serverNames.push(...filtered.kept);\n droppedServers.push(...filtered.dropped);\n }\n\n const upstreamPath = resolveSharedConfigPath(input.repoRoot);\n const upstream = upstreamPath\n ? filterLayer(upstreamPath, input.allowlist?.proxy, input.stagingDirectory, { stripProxyWrappers: true })\n : undefined;\n if (upstream) droppedServers.push(...upstream.dropped);\n\n return {\n shared: {\n configPaths: upstream ? [upstream.path] : [],\n serverNames: upstream ? upstream.kept : [],\n },\n // A later layer overriding an earlier one contributes the same name twice.\n sessionLocal: { configPaths, serverNames: [...new Set(serverNames)] },\n droppedServers: [...new Set(droppedServers)],\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpCatalog.cjs","names":[],"sources":["../../src/services/mcpCatalog.ts"],"sourcesContent":["import type { McpServerSnapshot, McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpServerStateChange, McpToolInfo } from '@agimon-ai/mcp-proxy';\nimport type { McpServerView } from '../types.ts';\nimport type { DirectToolFilter } from './directTools.ts';\nimport type { CachedCatalog } from './mcpRuntime.ts';\n\n/** One downstream tool, with the name Pi will know it by. */\nexport interface CatalogTool {\n /** Name registered with Pi, prefixed by server. */\n piName: string;\n /** Name the downstream server knows. */\n toolName: string;\n serverName: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface CatalogEntry {\n name: string;\n /**\n * The last transition the server actually reported.\n *\n * Never `disabled`: a session disable is an override the user applied, not\n * something the server did, so it lives in its own field. Writing it into\n * `state` would destroy the value re-enabling has to return to.\n */\n state: McpServerSnapshot['state'];\n /** Session-only override. Survives nothing; a restart returns to configuration. */\n disabled?: boolean;\n tools: CatalogTool[];\n resourceCount: number;\n error?: string;\n}\n\n/**\n * States where calling a tool cannot succeed, so its tools are withheld.\n *\n * `closed` is absent deliberately: a server closed after an idle timeout is\n * reconnected by the next call rather than gone. `disabled` is absent because it is\n * no longer a reachable transition; `entry.disabled` carries that instead.\n */\nconst UNAVAILABLE_STATES = new Set<McpServerSnapshot['state']>(['failed', 'needs-auth']);\n\n/** Where a server starts: known to the session, not yet dialled. */\nconst NOT_CONNECTED: McpServerSnapshot['state'] = 'not-connected';\nconst DISABLED: McpServerSnapshot['state'] = 'disabled';\n\n/**\n * Pi's tool name for a downstream tool.\n *\n * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names\n * are a contract with agent frontmatter selectors, transcripts, and user habit, so\n * they stay stable across the swap rather than following mcp-proxy's internal\n * scheme, which prefixes only on a clash.\n */\nexport function toPiToolName(serverName: string, toolName: string): string {\n return `${serverName.replace(/[^a-zA-Z0-9]/g, '_')}_${toolName}`;\n}\n\nfunction toCatalogTool(serverName: string, tool: McpToolInfo): CatalogTool {\n return {\n piName: toPiToolName(serverName, tool.name),\n toolName: tool.name,\n serverName,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: tool.inputSchema,\n };\n}\n\n/**\n * The session's MCP servers and the tools registered for each.\n *\n * Seeded from the previous run's catalog so tools exist before any socket opens,\n * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no\n * way to unregister one, so a tool whose server later fails stays registered and is\n * simply left out of the active set.\n */\nexport class McpCatalog {\n private readonly entries = new Map<string, CatalogEntry>();\n private readonly diagnostics: string[] = [];\n /** Pi names already claimed, so two servers cannot register the same one. */\n private readonly claimed = new Map<string, string>();\n\n /**\n * @param directTools - A child agent's tool selection. Undefined in a normal\n * session, where every configured tool is available.\n */\n constructor(private readonly directTools?: DirectToolFilter) {}\n\n /**\n * Seeds the servers this session will connect to, all still `not-connected`.\n *\n * Configured names come first so a run with no cache still reports what it is\n * about to reach rather than looking empty; the cache then fills in their tools.\n * A cached server that configuration has since dropped is not seeded at all.\n */\n seed(catalog: CachedCatalog, configuredServers: readonly string[] = []): void {\n for (const name of configuredServers) {\n this.entries.set(name, { name, state: NOT_CONNECTED, tools: [], resourceCount: 0 });\n }\n for (const server of catalog.servers) {\n if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;\n this.entries.set(server.name, {\n name: server.name,\n state: NOT_CONNECTED,\n tools: this.claimTools(server.name, server.tools),\n resourceCount: 0,\n });\n }\n }\n\n /**\n * Hides or restores a server's tools for this session.\n *\n * Only the flag moves: `state` keeps tracking what the server reports, so\n * re-enabling lands on the current truth rather than on a remembered value that\n * may have gone stale while the server was hidden.\n */\n setDisabled(serverName: string, disabled: boolean): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.disabled = disabled;\n else\n this.entries.set(serverName, { name: serverName, state: NOT_CONNECTED, disabled, tools: [], resourceCount: 0 });\n }\n\n /** Records how many resources a server offered the last time they were listed. */\n setResourceCount(serverName: string, resourceCount: number): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.resourceCount = resourceCount;\n }\n\n /**\n * Folds a live server transition into the catalog.\n *\n * Returns the tools that are new to this catalog, which is what the caller has to\n * register with Pi. A reconnect returns nothing, since its tools already exist.\n */\n applyStateChange(change: McpServerStateChange, tools: McpToolInfo[] = [], resourceCount?: number): CatalogTool[] {\n const entry = this.entries.get(change.serverName) ?? {\n name: change.serverName,\n state: change.state,\n tools: [],\n resourceCount: 0,\n };\n entry.state = change.state;\n if (change.error) entry.error = change.error;\n else delete entry.error;\n if (resourceCount !== undefined) entry.resourceCount = resourceCount;\n\n const known = new Set(entry.tools.map((tool) => tool.toolName));\n const added = this.claimTools(\n change.serverName,\n tools.filter((tool) => !known.has(tool.name)),\n );\n entry.tools = [...entry.tools, ...added];\n this.entries.set(change.serverName, entry);\n return added;\n }\n\n /**\n * Tool names Pi should currently expose.\n *\n * Optimistic: a server that has not reported yet still contributes its cached\n * tools, because calling one connects it on demand. mcp-proxy skips the eager\n * connection pass whenever it has a usable definitions cache, so requiring a\n * `connected` state first would leave a warm session with no tools at all.\n *\n * Only a server that is known to be unusable, or one the user disabled, is held\n * back.\n */\n activeToolNames(): string[] {\n const names: string[] = [];\n for (const entry of this.entries.values()) {\n if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;\n for (const tool of entry.tools) {\n if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;\n names.push(tool.piName);\n }\n }\n return names;\n }\n\n /** Every tool ever registered, whether or not its server is reachable. */\n allTools(): CatalogTool[] {\n return [...this.entries.values()].flatMap((entry) => entry.tools);\n }\n\n findTool(piName: string): CatalogTool | undefined {\n return this.allTools().find((tool) => tool.piName === piName);\n }\n\n /**\n * The cross-extension status shape.\n *\n * A disabled server reports `disabled` as its state, because that is the one word\n * the wire contract has for \"configured but contributing nothing\", and consumers\n * outside this process have no use for the transition underneath it.\n */\n toSnapshot(): McpStatusSnapshot {\n return {\n servers: [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.disabled ? DISABLED : entry.state,\n tools: entry.tools.map((tool) => tool.piName),\n resourceCount: entry.resourceCount,\n ...(entry.error ? { error: entry.error } : {}),\n })),\n };\n }\n\n /**\n * The fuller picture the overlay renders.\n *\n * Carries what the snapshot cannot: each tool's downstream name and description,\n * whether a child agent's selection withholds it, and `enabled` as its own field\n * so the overlay can offer disable and enable against the real server state.\n */\n toView(): McpServerView[] {\n return [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.state,\n tools: entry.tools.map((tool) => ({\n piName: tool.piName,\n toolName: tool.toolName,\n ...(tool.description ? { description: tool.description } : {}),\n active: !this.directTools || this.directTools.allows(entry.name, tool.toolName),\n })),\n resourceCount: entry.resourceCount,\n enabled: !entry.disabled,\n ...(entry.error ? { error: entry.error } : {}),\n }));\n }\n\n /** Problems worth surfacing to the user. Reported, never thrown. */\n getDiagnostics(): readonly string[] {\n return this.diagnostics;\n }\n\n /**\n * Records a problem for the user without interrupting whatever hit it.\n *\n * Deduplicated: a fault that recurs on every repaint would otherwise grow this\n * list for as long as the session runs, and the second copy tells nobody\n * anything the first did not.\n */\n addDiagnostic(diagnostic: string): void {\n if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);\n }\n\n /**\n * Assigns Pi names, dropping any that another server already owns.\n *\n * Two server names that normalize to the same prefix (`code-intel` and\n * `code_intel`) would otherwise have one silently shadow the other's tool.\n */\n private claimTools(serverName: string, tools: McpToolInfo[]): CatalogTool[] {\n const claimed: CatalogTool[] = [];\n for (const tool of tools) {\n const candidate = toCatalogTool(serverName, tool);\n const owner = this.claimed.get(candidate.piName);\n if (owner !== undefined && owner !== serverName) {\n this.diagnostics.push(\n `MCP tool \"${candidate.piName}\" from \"${serverName}\" was dropped: \"${owner}\" already registered that name.`,\n );\n continue;\n }\n this.claimed.set(candidate.piName, serverName);\n claimed.push(candidate);\n }\n return claimed;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcpCatalog.cjs","names":[],"sources":["../../src/services/mcpCatalog.ts"],"sourcesContent":["import type { McpServerSnapshot, McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpServerStateChange, McpToolInfo } from '@agimon-ai/mcp-proxy';\nimport type { McpServerView } from '../types.ts';\nimport type { DirectToolFilter } from './directTools.ts';\nimport type { CachedCatalog } from './mcpRuntime.ts';\n\n/** One downstream tool, with the name Pi will know it by. */\nexport interface CatalogTool {\n /** Name registered with Pi, prefixed by server. */\n piName: string;\n /** Name the downstream server knows. */\n toolName: string;\n serverName: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface CatalogEntry {\n name: string;\n /**\n * The last transition the server actually reported.\n *\n * Never `disabled`: a session disable is an override the user applied, not\n * something the server did, so it lives in its own field. Writing it into\n * `state` would destroy the value re-enabling has to return to.\n */\n state: McpServerSnapshot['state'];\n /** Session-only override. Survives nothing; a restart returns to configuration. */\n disabled?: boolean;\n tools: CatalogTool[];\n resourceCount: number;\n error?: string;\n}\n\n/**\n * States where calling a tool cannot succeed, so its tools are withheld.\n *\n * `closed` is absent deliberately: a server closed after an idle timeout is\n * reconnected by the next call rather than gone. `disabled` is absent because it is\n * no longer a reachable transition; `entry.disabled` carries that instead.\n */\nconst UNAVAILABLE_STATES = new Set<McpServerSnapshot['state']>(['failed', 'needs-auth']);\n\n/** Where a server starts: known to the session, not yet dialled. */\nconst NOT_CONNECTED: McpServerSnapshot['state'] = 'not-connected';\nconst DISABLED: McpServerSnapshot['state'] = 'disabled';\n\n/**\n * Pi's tool name for a downstream tool.\n *\n * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names\n * are a contract with agent frontmatter selectors, transcripts, and user habit, so\n * they stay stable across the swap rather than following mcp-proxy's internal\n * scheme, which prefixes only on a clash.\n */\nexport function toPiToolName(serverName: string, toolName: string): string {\n return `${serverName.replace(/[^a-zA-Z0-9]/g, '_')}_${toolName}`;\n}\n\nfunction toCatalogTool(serverName: string, tool: McpToolInfo): CatalogTool {\n return {\n piName: toPiToolName(serverName, tool.name),\n toolName: tool.name,\n serverName,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: tool.inputSchema,\n };\n}\n\n/**\n * The session's MCP servers and the tools registered for each.\n *\n * Seeded from the previous run's catalog so tools exist before any socket opens,\n * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no\n * way to unregister one, so a tool whose server later fails stays registered and is\n * simply left out of the active set.\n */\nexport class McpCatalog {\n private readonly entries = new Map<string, CatalogEntry>();\n private readonly diagnostics: string[] = [];\n /** Pi names already claimed, so two servers cannot register the same one. */\n private readonly claimed = new Map<string, string>();\n\n /**\n * @param directTools - A child agent's tool selection. Undefined in a normal\n * session, where every configured tool is available.\n */\n constructor(private readonly directTools?: DirectToolFilter) {}\n\n /**\n * Seeds the servers this session will connect to, all still `not-connected`.\n *\n * Configured names come first so a run with no cache still reports what it is\n * about to reach rather than looking empty; the cache then fills in their tools.\n * A cached server that configuration has since dropped is not seeded at all.\n */\n seed(catalog: CachedCatalog, configuredServers: readonly string[] = []): void {\n for (const name of configuredServers) {\n this.entries.set(name, { name, state: NOT_CONNECTED, tools: [], resourceCount: 0 });\n }\n for (const server of catalog.servers) {\n if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;\n this.entries.set(server.name, {\n name: server.name,\n state: NOT_CONNECTED,\n tools: this.claimTools(server.name, server.tools),\n resourceCount: 0,\n });\n }\n }\n\n /**\n * Hides or restores a server's tools for this session.\n *\n * Only the flag moves: `state` keeps tracking what the server reports, so\n * re-enabling lands on the current truth rather than on a remembered value that\n * may have gone stale while the server was hidden.\n */\n setDisabled(serverName: string, disabled: boolean): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.disabled = disabled;\n else\n this.entries.set(serverName, { name: serverName, state: NOT_CONNECTED, disabled, tools: [], resourceCount: 0 });\n }\n\n /** Records how many resources a server offered the last time they were listed. */\n setResourceCount(serverName: string, resourceCount: number): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.resourceCount = resourceCount;\n }\n\n /**\n * Folds a live server transition into the catalog.\n *\n * Returns the tools that are new to this catalog, which is what the caller has to\n * register with Pi. A reconnect returns nothing, since its tools already exist.\n */\n applyStateChange(change: McpServerStateChange, tools: McpToolInfo[] = [], resourceCount?: number): CatalogTool[] {\n const entry = this.entries.get(change.serverName) ?? {\n name: change.serverName,\n state: change.state,\n tools: [],\n resourceCount: 0,\n };\n entry.state = change.state;\n if (change.error) entry.error = change.error;\n else delete entry.error;\n if (resourceCount !== undefined) entry.resourceCount = resourceCount;\n\n const known = new Set(entry.tools.map((tool) => tool.toolName));\n const added = this.claimTools(\n change.serverName,\n tools.filter((tool) => !known.has(tool.name)),\n );\n entry.tools = [...entry.tools, ...added];\n this.entries.set(change.serverName, entry);\n return added;\n }\n\n /**\n * Tool names Pi should currently expose.\n *\n * Optimistic: a server that has not reported yet still contributes its cached\n * tools, because calling one connects it on demand. The live background pass can\n * take longer than Pi startup, so requiring `connected` first would make a warm\n * session needlessly start with no tools.\n *\n * Only a server that is known to be unusable, or one the user disabled, is held\n * back.\n */\n activeToolNames(): string[] {\n const names: string[] = [];\n for (const entry of this.entries.values()) {\n if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;\n for (const tool of entry.tools) {\n if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;\n names.push(tool.piName);\n }\n }\n return names;\n }\n\n /** Every tool ever registered, whether or not its server is reachable. */\n allTools(): CatalogTool[] {\n return [...this.entries.values()].flatMap((entry) => entry.tools);\n }\n\n findTool(piName: string): CatalogTool | undefined {\n return this.allTools().find((tool) => tool.piName === piName);\n }\n\n /**\n * The cross-extension status shape.\n *\n * A disabled server reports `disabled` as its state, because that is the one word\n * the wire contract has for \"configured but contributing nothing\", and consumers\n * outside this process have no use for the transition underneath it.\n */\n toSnapshot(): McpStatusSnapshot {\n return {\n servers: [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.disabled ? DISABLED : entry.state,\n tools: entry.tools.map((tool) => tool.piName),\n resourceCount: entry.resourceCount,\n ...(entry.error ? { error: entry.error } : {}),\n })),\n };\n }\n\n /**\n * The fuller picture the overlay renders.\n *\n * Carries what the snapshot cannot: each tool's downstream name and description,\n * whether a child agent's selection withholds it, and `enabled` as its own field\n * so the overlay can offer disable and enable against the real server state.\n */\n toView(): McpServerView[] {\n return [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.state,\n tools: entry.tools.map((tool) => ({\n piName: tool.piName,\n toolName: tool.toolName,\n ...(tool.description ? { description: tool.description } : {}),\n active: !this.directTools || this.directTools.allows(entry.name, tool.toolName),\n })),\n resourceCount: entry.resourceCount,\n enabled: !entry.disabled,\n ...(entry.error ? { error: entry.error } : {}),\n }));\n }\n\n /** Problems worth surfacing to the user. Reported, never thrown. */\n getDiagnostics(): readonly string[] {\n return this.diagnostics;\n }\n\n /**\n * Records a problem for the user without interrupting whatever hit it.\n *\n * Deduplicated: a fault that recurs on every repaint would otherwise grow this\n * list for as long as the session runs, and the second copy tells nobody\n * anything the first did not.\n */\n addDiagnostic(diagnostic: string): void {\n if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);\n }\n\n /**\n * Assigns Pi names, dropping any that another server already owns.\n *\n * Two server names that normalize to the same prefix (`code-intel` and\n * `code_intel`) would otherwise have one silently shadow the other's tool.\n */\n private claimTools(serverName: string, tools: McpToolInfo[]): CatalogTool[] {\n const claimed: CatalogTool[] = [];\n for (const tool of tools) {\n const candidate = toCatalogTool(serverName, tool);\n const owner = this.claimed.get(candidate.piName);\n if (owner !== undefined && owner !== serverName) {\n this.diagnostics.push(\n `MCP tool \"${candidate.piName}\" from \"${serverName}\" was dropped: \"${owner}\" already registered that name.`,\n );\n continue;\n }\n this.claimed.set(candidate.piName, serverName);\n claimed.push(candidate);\n }\n return claimed;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpCatalog.mjs","names":[],"sources":["../../src/services/mcpCatalog.ts"],"sourcesContent":["import type { McpServerSnapshot, McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpServerStateChange, McpToolInfo } from '@agimon-ai/mcp-proxy';\nimport type { McpServerView } from '../types.ts';\nimport type { DirectToolFilter } from './directTools.ts';\nimport type { CachedCatalog } from './mcpRuntime.ts';\n\n/** One downstream tool, with the name Pi will know it by. */\nexport interface CatalogTool {\n /** Name registered with Pi, prefixed by server. */\n piName: string;\n /** Name the downstream server knows. */\n toolName: string;\n serverName: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface CatalogEntry {\n name: string;\n /**\n * The last transition the server actually reported.\n *\n * Never `disabled`: a session disable is an override the user applied, not\n * something the server did, so it lives in its own field. Writing it into\n * `state` would destroy the value re-enabling has to return to.\n */\n state: McpServerSnapshot['state'];\n /** Session-only override. Survives nothing; a restart returns to configuration. */\n disabled?: boolean;\n tools: CatalogTool[];\n resourceCount: number;\n error?: string;\n}\n\n/**\n * States where calling a tool cannot succeed, so its tools are withheld.\n *\n * `closed` is absent deliberately: a server closed after an idle timeout is\n * reconnected by the next call rather than gone. `disabled` is absent because it is\n * no longer a reachable transition; `entry.disabled` carries that instead.\n */\nconst UNAVAILABLE_STATES = new Set<McpServerSnapshot['state']>(['failed', 'needs-auth']);\n\n/** Where a server starts: known to the session, not yet dialled. */\nconst NOT_CONNECTED: McpServerSnapshot['state'] = 'not-connected';\nconst DISABLED: McpServerSnapshot['state'] = 'disabled';\n\n/**\n * Pi's tool name for a downstream tool.\n *\n * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names\n * are a contract with agent frontmatter selectors, transcripts, and user habit, so\n * they stay stable across the swap rather than following mcp-proxy's internal\n * scheme, which prefixes only on a clash.\n */\nexport function toPiToolName(serverName: string, toolName: string): string {\n return `${serverName.replace(/[^a-zA-Z0-9]/g, '_')}_${toolName}`;\n}\n\nfunction toCatalogTool(serverName: string, tool: McpToolInfo): CatalogTool {\n return {\n piName: toPiToolName(serverName, tool.name),\n toolName: tool.name,\n serverName,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: tool.inputSchema,\n };\n}\n\n/**\n * The session's MCP servers and the tools registered for each.\n *\n * Seeded from the previous run's catalog so tools exist before any socket opens,\n * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no\n * way to unregister one, so a tool whose server later fails stays registered and is\n * simply left out of the active set.\n */\nexport class McpCatalog {\n private readonly entries = new Map<string, CatalogEntry>();\n private readonly diagnostics: string[] = [];\n /** Pi names already claimed, so two servers cannot register the same one. */\n private readonly claimed = new Map<string, string>();\n\n /**\n * @param directTools - A child agent's tool selection. Undefined in a normal\n * session, where every configured tool is available.\n */\n constructor(private readonly directTools?: DirectToolFilter) {}\n\n /**\n * Seeds the servers this session will connect to, all still `not-connected`.\n *\n * Configured names come first so a run with no cache still reports what it is\n * about to reach rather than looking empty; the cache then fills in their tools.\n * A cached server that configuration has since dropped is not seeded at all.\n */\n seed(catalog: CachedCatalog, configuredServers: readonly string[] = []): void {\n for (const name of configuredServers) {\n this.entries.set(name, { name, state: NOT_CONNECTED, tools: [], resourceCount: 0 });\n }\n for (const server of catalog.servers) {\n if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;\n this.entries.set(server.name, {\n name: server.name,\n state: NOT_CONNECTED,\n tools: this.claimTools(server.name, server.tools),\n resourceCount: 0,\n });\n }\n }\n\n /**\n * Hides or restores a server's tools for this session.\n *\n * Only the flag moves: `state` keeps tracking what the server reports, so\n * re-enabling lands on the current truth rather than on a remembered value that\n * may have gone stale while the server was hidden.\n */\n setDisabled(serverName: string, disabled: boolean): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.disabled = disabled;\n else\n this.entries.set(serverName, { name: serverName, state: NOT_CONNECTED, disabled, tools: [], resourceCount: 0 });\n }\n\n /** Records how many resources a server offered the last time they were listed. */\n setResourceCount(serverName: string, resourceCount: number): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.resourceCount = resourceCount;\n }\n\n /**\n * Folds a live server transition into the catalog.\n *\n * Returns the tools that are new to this catalog, which is what the caller has to\n * register with Pi. A reconnect returns nothing, since its tools already exist.\n */\n applyStateChange(change: McpServerStateChange, tools: McpToolInfo[] = [], resourceCount?: number): CatalogTool[] {\n const entry = this.entries.get(change.serverName) ?? {\n name: change.serverName,\n state: change.state,\n tools: [],\n resourceCount: 0,\n };\n entry.state = change.state;\n if (change.error) entry.error = change.error;\n else delete entry.error;\n if (resourceCount !== undefined) entry.resourceCount = resourceCount;\n\n const known = new Set(entry.tools.map((tool) => tool.toolName));\n const added = this.claimTools(\n change.serverName,\n tools.filter((tool) => !known.has(tool.name)),\n );\n entry.tools = [...entry.tools, ...added];\n this.entries.set(change.serverName, entry);\n return added;\n }\n\n /**\n * Tool names Pi should currently expose.\n *\n * Optimistic: a server that has not reported yet still contributes its cached\n * tools, because calling one connects it on demand. mcp-proxy skips the eager\n * connection pass whenever it has a usable definitions cache, so requiring a\n * `connected` state first would leave a warm session with no tools at all.\n *\n * Only a server that is known to be unusable, or one the user disabled, is held\n * back.\n */\n activeToolNames(): string[] {\n const names: string[] = [];\n for (const entry of this.entries.values()) {\n if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;\n for (const tool of entry.tools) {\n if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;\n names.push(tool.piName);\n }\n }\n return names;\n }\n\n /** Every tool ever registered, whether or not its server is reachable. */\n allTools(): CatalogTool[] {\n return [...this.entries.values()].flatMap((entry) => entry.tools);\n }\n\n findTool(piName: string): CatalogTool | undefined {\n return this.allTools().find((tool) => tool.piName === piName);\n }\n\n /**\n * The cross-extension status shape.\n *\n * A disabled server reports `disabled` as its state, because that is the one word\n * the wire contract has for \"configured but contributing nothing\", and consumers\n * outside this process have no use for the transition underneath it.\n */\n toSnapshot(): McpStatusSnapshot {\n return {\n servers: [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.disabled ? DISABLED : entry.state,\n tools: entry.tools.map((tool) => tool.piName),\n resourceCount: entry.resourceCount,\n ...(entry.error ? { error: entry.error } : {}),\n })),\n };\n }\n\n /**\n * The fuller picture the overlay renders.\n *\n * Carries what the snapshot cannot: each tool's downstream name and description,\n * whether a child agent's selection withholds it, and `enabled` as its own field\n * so the overlay can offer disable and enable against the real server state.\n */\n toView(): McpServerView[] {\n return [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.state,\n tools: entry.tools.map((tool) => ({\n piName: tool.piName,\n toolName: tool.toolName,\n ...(tool.description ? { description: tool.description } : {}),\n active: !this.directTools || this.directTools.allows(entry.name, tool.toolName),\n })),\n resourceCount: entry.resourceCount,\n enabled: !entry.disabled,\n ...(entry.error ? { error: entry.error } : {}),\n }));\n }\n\n /** Problems worth surfacing to the user. Reported, never thrown. */\n getDiagnostics(): readonly string[] {\n return this.diagnostics;\n }\n\n /**\n * Records a problem for the user without interrupting whatever hit it.\n *\n * Deduplicated: a fault that recurs on every repaint would otherwise grow this\n * list for as long as the session runs, and the second copy tells nobody\n * anything the first did not.\n */\n addDiagnostic(diagnostic: string): void {\n if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);\n }\n\n /**\n * Assigns Pi names, dropping any that another server already owns.\n *\n * Two server names that normalize to the same prefix (`code-intel` and\n * `code_intel`) would otherwise have one silently shadow the other's tool.\n */\n private claimTools(serverName: string, tools: McpToolInfo[]): CatalogTool[] {\n const claimed: CatalogTool[] = [];\n for (const tool of tools) {\n const candidate = toCatalogTool(serverName, tool);\n const owner = this.claimed.get(candidate.piName);\n if (owner !== undefined && owner !== serverName) {\n this.diagnostics.push(\n `MCP tool \"${candidate.piName}\" from \"${serverName}\" was dropped: \"${owner}\" already registered that name.`,\n );\n continue;\n }\n this.claimed.set(candidate.piName, serverName);\n claimed.push(candidate);\n }\n return claimed;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcpCatalog.mjs","names":[],"sources":["../../src/services/mcpCatalog.ts"],"sourcesContent":["import type { McpServerSnapshot, McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpServerStateChange, McpToolInfo } from '@agimon-ai/mcp-proxy';\nimport type { McpServerView } from '../types.ts';\nimport type { DirectToolFilter } from './directTools.ts';\nimport type { CachedCatalog } from './mcpRuntime.ts';\n\n/** One downstream tool, with the name Pi will know it by. */\nexport interface CatalogTool {\n /** Name registered with Pi, prefixed by server. */\n piName: string;\n /** Name the downstream server knows. */\n toolName: string;\n serverName: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface CatalogEntry {\n name: string;\n /**\n * The last transition the server actually reported.\n *\n * Never `disabled`: a session disable is an override the user applied, not\n * something the server did, so it lives in its own field. Writing it into\n * `state` would destroy the value re-enabling has to return to.\n */\n state: McpServerSnapshot['state'];\n /** Session-only override. Survives nothing; a restart returns to configuration. */\n disabled?: boolean;\n tools: CatalogTool[];\n resourceCount: number;\n error?: string;\n}\n\n/**\n * States where calling a tool cannot succeed, so its tools are withheld.\n *\n * `closed` is absent deliberately: a server closed after an idle timeout is\n * reconnected by the next call rather than gone. `disabled` is absent because it is\n * no longer a reachable transition; `entry.disabled` carries that instead.\n */\nconst UNAVAILABLE_STATES = new Set<McpServerSnapshot['state']>(['failed', 'needs-auth']);\n\n/** Where a server starts: known to the session, not yet dialled. */\nconst NOT_CONNECTED: McpServerSnapshot['state'] = 'not-connected';\nconst DISABLED: McpServerSnapshot['state'] = 'disabled';\n\n/**\n * Pi's tool name for a downstream tool.\n *\n * Always prefixed, matching what `pi-mcp-adapter` produced by default. Tool names\n * are a contract with agent frontmatter selectors, transcripts, and user habit, so\n * they stay stable across the swap rather than following mcp-proxy's internal\n * scheme, which prefixes only on a clash.\n */\nexport function toPiToolName(serverName: string, toolName: string): string {\n return `${serverName.replace(/[^a-zA-Z0-9]/g, '_')}_${toolName}`;\n}\n\nfunction toCatalogTool(serverName: string, tool: McpToolInfo): CatalogTool {\n return {\n piName: toPiToolName(serverName, tool.name),\n toolName: tool.name,\n serverName,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: tool.inputSchema,\n };\n}\n\n/**\n * The session's MCP servers and the tools registered for each.\n *\n * Seeded from the previous run's catalog so tools exist before any socket opens,\n * then folded forward as servers report in. Tools are never removed: Pi 0.84 has no\n * way to unregister one, so a tool whose server later fails stays registered and is\n * simply left out of the active set.\n */\nexport class McpCatalog {\n private readonly entries = new Map<string, CatalogEntry>();\n private readonly diagnostics: string[] = [];\n /** Pi names already claimed, so two servers cannot register the same one. */\n private readonly claimed = new Map<string, string>();\n\n /**\n * @param directTools - A child agent's tool selection. Undefined in a normal\n * session, where every configured tool is available.\n */\n constructor(private readonly directTools?: DirectToolFilter) {}\n\n /**\n * Seeds the servers this session will connect to, all still `not-connected`.\n *\n * Configured names come first so a run with no cache still reports what it is\n * about to reach rather than looking empty; the cache then fills in their tools.\n * A cached server that configuration has since dropped is not seeded at all.\n */\n seed(catalog: CachedCatalog, configuredServers: readonly string[] = []): void {\n for (const name of configuredServers) {\n this.entries.set(name, { name, state: NOT_CONNECTED, tools: [], resourceCount: 0 });\n }\n for (const server of catalog.servers) {\n if (configuredServers.length > 0 && !this.entries.has(server.name)) continue;\n this.entries.set(server.name, {\n name: server.name,\n state: NOT_CONNECTED,\n tools: this.claimTools(server.name, server.tools),\n resourceCount: 0,\n });\n }\n }\n\n /**\n * Hides or restores a server's tools for this session.\n *\n * Only the flag moves: `state` keeps tracking what the server reports, so\n * re-enabling lands on the current truth rather than on a remembered value that\n * may have gone stale while the server was hidden.\n */\n setDisabled(serverName: string, disabled: boolean): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.disabled = disabled;\n else\n this.entries.set(serverName, { name: serverName, state: NOT_CONNECTED, disabled, tools: [], resourceCount: 0 });\n }\n\n /** Records how many resources a server offered the last time they were listed. */\n setResourceCount(serverName: string, resourceCount: number): void {\n const entry = this.entries.get(serverName);\n if (entry) entry.resourceCount = resourceCount;\n }\n\n /**\n * Folds a live server transition into the catalog.\n *\n * Returns the tools that are new to this catalog, which is what the caller has to\n * register with Pi. A reconnect returns nothing, since its tools already exist.\n */\n applyStateChange(change: McpServerStateChange, tools: McpToolInfo[] = [], resourceCount?: number): CatalogTool[] {\n const entry = this.entries.get(change.serverName) ?? {\n name: change.serverName,\n state: change.state,\n tools: [],\n resourceCount: 0,\n };\n entry.state = change.state;\n if (change.error) entry.error = change.error;\n else delete entry.error;\n if (resourceCount !== undefined) entry.resourceCount = resourceCount;\n\n const known = new Set(entry.tools.map((tool) => tool.toolName));\n const added = this.claimTools(\n change.serverName,\n tools.filter((tool) => !known.has(tool.name)),\n );\n entry.tools = [...entry.tools, ...added];\n this.entries.set(change.serverName, entry);\n return added;\n }\n\n /**\n * Tool names Pi should currently expose.\n *\n * Optimistic: a server that has not reported yet still contributes its cached\n * tools, because calling one connects it on demand. The live background pass can\n * take longer than Pi startup, so requiring `connected` first would make a warm\n * session needlessly start with no tools.\n *\n * Only a server that is known to be unusable, or one the user disabled, is held\n * back.\n */\n activeToolNames(): string[] {\n const names: string[] = [];\n for (const entry of this.entries.values()) {\n if (entry.disabled || UNAVAILABLE_STATES.has(entry.state)) continue;\n for (const tool of entry.tools) {\n if (this.directTools && !this.directTools.allows(entry.name, tool.toolName)) continue;\n names.push(tool.piName);\n }\n }\n return names;\n }\n\n /** Every tool ever registered, whether or not its server is reachable. */\n allTools(): CatalogTool[] {\n return [...this.entries.values()].flatMap((entry) => entry.tools);\n }\n\n findTool(piName: string): CatalogTool | undefined {\n return this.allTools().find((tool) => tool.piName === piName);\n }\n\n /**\n * The cross-extension status shape.\n *\n * A disabled server reports `disabled` as its state, because that is the one word\n * the wire contract has for \"configured but contributing nothing\", and consumers\n * outside this process have no use for the transition underneath it.\n */\n toSnapshot(): McpStatusSnapshot {\n return {\n servers: [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.disabled ? DISABLED : entry.state,\n tools: entry.tools.map((tool) => tool.piName),\n resourceCount: entry.resourceCount,\n ...(entry.error ? { error: entry.error } : {}),\n })),\n };\n }\n\n /**\n * The fuller picture the overlay renders.\n *\n * Carries what the snapshot cannot: each tool's downstream name and description,\n * whether a child agent's selection withholds it, and `enabled` as its own field\n * so the overlay can offer disable and enable against the real server state.\n */\n toView(): McpServerView[] {\n return [...this.entries.values()].map((entry) => ({\n name: entry.name,\n state: entry.state,\n tools: entry.tools.map((tool) => ({\n piName: tool.piName,\n toolName: tool.toolName,\n ...(tool.description ? { description: tool.description } : {}),\n active: !this.directTools || this.directTools.allows(entry.name, tool.toolName),\n })),\n resourceCount: entry.resourceCount,\n enabled: !entry.disabled,\n ...(entry.error ? { error: entry.error } : {}),\n }));\n }\n\n /** Problems worth surfacing to the user. Reported, never thrown. */\n getDiagnostics(): readonly string[] {\n return this.diagnostics;\n }\n\n /**\n * Records a problem for the user without interrupting whatever hit it.\n *\n * Deduplicated: a fault that recurs on every repaint would otherwise grow this\n * list for as long as the session runs, and the second copy tells nobody\n * anything the first did not.\n */\n addDiagnostic(diagnostic: string): void {\n if (!this.diagnostics.includes(diagnostic)) this.diagnostics.push(diagnostic);\n }\n\n /**\n * Assigns Pi names, dropping any that another server already owns.\n *\n * Two server names that normalize to the same prefix (`code-intel` and\n * `code_intel`) would otherwise have one silently shadow the other's tool.\n */\n private claimTools(serverName: string, tools: McpToolInfo[]): CatalogTool[] {\n const claimed: CatalogTool[] = [];\n for (const tool of tools) {\n const candidate = toCatalogTool(serverName, tool);\n const owner = this.claimed.get(candidate.piName);\n if (owner !== undefined && owner !== serverName) {\n this.diagnostics.push(\n `MCP tool \"${candidate.piName}\" from \"${serverName}\" was dropped: \"${owner}\" already registered that name.`,\n );\n continue;\n }\n this.claimed.set(candidate.piName, serverName);\n claimed.push(candidate);\n }\n return claimed;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let e=require("node:
|
|
1
|
+
let e=require("node:crypto"),t=require("node:fs"),n=require("node:path"),r=require("node:os");function i(e){return(0,n.resolve)(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function a(t){if(t.length===0)throw Error(`definitionsCachePath requires at least one config path`);let a=i(t[0]),o=t.length===1?``:`.${(0,e.createHash)(`sha256`).update(t.map(e=>(0,n.resolve)(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return(0,n.join)((0,r.homedir)(),`.mcp-proxy`,`${a}${o}.definitions-cache.json`)}function o(e,n=a){if(e.length===0)return{servers:[]};try{let r=n(e),i=JSON.parse((0,t.readFileSync)(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var s=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};exports.McpRuntimeOwner=s,exports.definitionsCachePath=a,exports.readCachedCatalog=o;
|
|
2
2
|
//# sourceMappingURL=mcpRuntime.cjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{createHash as e}from"node:crypto";import{readFileSync as t}from"node:fs";import{join as n,resolve as r}from"node:path";import{homedir as i}from"node:os";function a(e){return r(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function o(t){if(t.length===0)throw Error(`definitionsCachePath requires at least one config path`);let o=a(t[0]),s=t.length===1?``:`.${e(`sha256`).update(t.map(e=>r(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return n(i(),`.mcp-proxy`,`${o}${s}.definitions-cache.json`)}function s(e,n=o){if(e.length===0)return{servers:[]};try{let r=n(e),i=JSON.parse(t(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var c=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};export{c as McpRuntimeOwner,o as definitionsCachePath,s as readCachedCatalog};
|
|
2
2
|
//# sourceMappingURL=mcpRuntime.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../
|
|
1
|
+
const e=require("../tools/mcpTools.cjs"),t=require("./configSources.cjs"),n=require("./directTools.cjs"),r=require("./mcpCatalog.cjs"),i=require("./mcpRuntime.cjs"),a=require("./sessionConfig.cjs"),o=`The MCP runtime has not started yet.`;var s=class{pi;runtime;catalog;groups={shared:{configPaths:[],serverNames:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){e.applyActiveTools(this.pi,this.catalog)}createCatalog(){return new r.McpCatalog(n.readDirectToolFilter(this.options.environment))}install(n=a.readSessionConfig(this.options.environment)){this.groups=t.buildMcpConfigGroups(n),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];return this.catalog.seed(i.readCachedCatalog(this.configPaths()),r),e.registerNewTools(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([Promise.resolve().then(()=>require("./keyringTokenStore.cjs")),Promise.resolve().then(()=>require("./mcpRuntime.cjs"))]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configPaths();if(this.configuredServerNames().length===0)return;let a=this.options.tokenStore??await t();await r.start({configPaths:i,tokenStore:a,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange(),this.options.onAuthorizationUrl?.(e,t)}configPaths(){return[...this.groups.sessionLocal.configPaths,...this.groups.shared.configPaths]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(o);this.resourceCache.delete(e),await t.disconnectServer(e),await t.ensureConnected(e)}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(t,n){this.catalog.setDisabled(t,!n),e.applyActiveTools(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime,r=n?.getServices();if(!n||!r)throw Error(o);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){++this.lifecycleGeneration,await this.runtime?.dispose()}onServerStateChange(t){t.state!==`connecting`&&this.authorizationUrls.delete(t.serverName);let n=this.runtime,r=n?.getServices();(t.state===`connected`&&r?r.clientManager.ensureConnected(t.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(i=>{if(!n?.isCurrent(r))return;let a=this.catalog.applyStateChange(t,i);e.registerNewTools(this.pi,this.clientManager,a),e.applyActiveTools(this.pi,this.catalog),this.emitChange()})}};exports.McpSession=s;
|
|
2
2
|
//# sourceMappingURL=mcpSession.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpSession.cjs","names":["readSessionConfig","McpCatalog","readDirectToolFilter","buildMcpConfigGroups","sharedLayerPath","PROXY_SERVER_NAME","readCachedCatalog","fs"],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, McpToolInfo, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { McpResourceView, McpServerView } from '../types.ts';\nimport { applyActiveTools, registerNewTools } from '../tools/mcpTools.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups, PROXY_SERVER_NAME } from './configSources.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { readSessionConfig, type McpSessionConfig } from './sessionConfig.ts';\nimport { sharedLayerPath } from './sharedRuntime.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n private configuration: McpSessionConfig;\n /** Generated layer pointing at the worktree runtime; absent when there is none. */\n private sharedLayerPath: string | undefined;\n /** Proxy tool names the domain permits. Undefined when every upstream is allowed. */\n private allowedUpstreamTools: Set<string> | undefined;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.configuration = readSessionConfig(options.environment);\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.configuration = configuration;\n this.groups = buildMcpConfigGroups(configuration);\n // The shared layer's path is stable per worktree, so a previous run's file is\n // already in the ordered list the definitions cache is keyed on. Including it\n // here is what makes the proxy's tools available before anything connects.\n this.sharedLayerPath =\n this.groups.shared.configPaths.length > 0 ? sharedLayerPath(configuration.repoRoot) : undefined;\n this.catalog = this.createCatalog();\n // The proxy is a configured server of this session even though it is absent\n // from `.mcp.json`'s filtered layer: doom-mcp rewrites that entry to point at\n // the worktree runtime rather than spawning a second proxy.\n const configuredServers = [\n ...this.groups.sessionLocal.serverNames,\n ...(this.sharedLayerPath ? [PROXY_SERVER_NAME] : []),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }, sharedRuntime] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n import('./sharedRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n const runtime = (this.runtime ??= new McpRuntimeOwner());\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n // Detached: every session start doubles as a sweep of staged layers and dead\n // registry rows left by sessions that never got to clean up after themselves.\n void sharedRuntime.maintainSharedResources(this.configuration.stagingDirectory);\n // Joining the worktree's runtime first, so its endpoint is written before the\n // container reads the layer. Only the first session in a worktree pays to\n // start it; the rest reuse it after one health probe.\n const shared = await sharedRuntime.ensureSharedRuntime({\n repoRoot: this.configuration.repoRoot,\n configPaths: this.groups.shared.configPaths,\n });\n if (generation !== this.lifecycleGeneration) return;\n this.allowedUpstreamTools = sharedRuntime.resolveAllowedUpstreamTools(\n this.groups.shared.configPaths,\n this.groups.shared.allowedUpstreams,\n );\n\n const configPaths = this.configPaths(shared !== undefined);\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe and a proxy on a\n // session that has nothing to connect to.\n if (this.groups.sessionLocal.serverNames.length === 0 && !shared) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /**\n * The ordered layers this session connects, session-local first.\n *\n * The shared layer is included only when its file exists, so a first run does\n * not key the definitions cache on a path that carries nothing.\n */\n private configPaths(sharedIsLive = false): string[] {\n const shared = this.sharedLayerPath;\n const includeShared = shared !== undefined && (sharedIsLive || fs.existsSync(shared));\n return [...this.groups.sessionLocal.configPaths, ...(includeShared ? [shared] : [])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect. The proxy entry points at a runtime shared\n * across every session in the worktree, so dropping its connection here would\n * reach into sessions the user was not looking at. Withholding the tools from\n * Pi's active list is the whole of what \"disabled for this session\" means, and it\n * is instant and reversible in both directions.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, this.permittedTools(change.serverName, tools));\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n\n /**\n * Drops proxy tools whose upstream this session's domain does not allow.\n *\n * Filtered before they reach the catalog, so a disallowed upstream's tool is\n * never registered rather than registered and hidden. The runtime is shared, so\n * this is the only place the per-session policy can apply.\n */\n private permittedTools(serverName: string, tools: McpToolInfo[]): McpToolInfo[] {\n const allowed = this.allowedUpstreamTools;\n if (!allowed || serverName !== PROXY_SERVER_NAME) return tools;\n return tools.filter((tool) => allowed.has(tool.name));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcpSession.cjs","names":["McpCatalog","readDirectToolFilter","readSessionConfig","buildMcpConfigGroups","readCachedCatalog"],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { applyActiveTools, registerNewTools } from '../tools/mcpTools.ts';\nimport type { McpResourceView, McpServerView } from '../types.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups } from './configSources.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { type McpSessionConfig, readSessionConfig } from './sessionConfig.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [], serverNames: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.groups = buildMcpConfigGroups(configuration);\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n const configPaths = this.configPaths();\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe on a session that\n // has nothing to connect to.\n if (this.configuredServerNames().length === 0) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /** Ordered layers for the one in-process client container. */\n private configPaths(): string[] {\n return [...this.groups.sessionLocal.configPaths, ...this.groups.shared.configPaths];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, tools);\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{applyActiveTools as e,registerNewTools as t}from"../tools/mcpTools.mjs";import{
|
|
1
|
+
import{applyActiveTools as e,registerNewTools as t}from"../tools/mcpTools.mjs";import{buildMcpConfigGroups as n}from"./configSources.mjs";import{readDirectToolFilter as r}from"./directTools.mjs";import{McpCatalog as i}from"./mcpCatalog.mjs";import{readCachedCatalog as a}from"./mcpRuntime.mjs";import{readSessionConfig as o}from"./sessionConfig.mjs";const s=`The MCP runtime has not started yet.`;var c=class{pi;runtime;catalog;groups={shared:{configPaths:[],serverNames:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){e(this.pi,this.catalog)}createCatalog(){return new i(r(this.options.environment))}install(e=o(this.options.environment)){this.groups=n(e),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];return this.catalog.seed(a(this.configPaths()),r),t(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([import(`./keyringTokenStore.mjs`),import(`./mcpRuntime.mjs`)]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configPaths();if(this.configuredServerNames().length===0)return;let a=this.options.tokenStore??await t();await r.start({configPaths:i,tokenStore:a,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange(),this.options.onAuthorizationUrl?.(e,t)}configPaths(){return[...this.groups.sessionLocal.configPaths,...this.groups.shared.configPaths]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(s);this.resourceCache.delete(e),await t.disconnectServer(e),await t.ensureConnected(e)}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(t,n){this.catalog.setDisabled(t,!n),e(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime,r=n?.getServices();if(!n||!r)throw Error(s);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){++this.lifecycleGeneration,await this.runtime?.dispose()}onServerStateChange(n){n.state!==`connecting`&&this.authorizationUrls.delete(n.serverName);let r=this.runtime,i=r?.getServices();(n.state===`connected`&&i?i.clientManager.ensureConnected(n.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(a=>{if(!r?.isCurrent(i))return;let o=this.catalog.applyStateChange(n,a);t(this.pi,this.clientManager,o),e(this.pi,this.catalog),this.emitChange()})}};export{c as McpSession};
|
|
2
2
|
//# sourceMappingURL=mcpSession.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, McpToolInfo, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { McpResourceView, McpServerView } from '../types.ts';\nimport { applyActiveTools, registerNewTools } from '../tools/mcpTools.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups, PROXY_SERVER_NAME } from './configSources.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { readSessionConfig, type McpSessionConfig } from './sessionConfig.ts';\nimport { sharedLayerPath } from './sharedRuntime.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n private configuration: McpSessionConfig;\n /** Generated layer pointing at the worktree runtime; absent when there is none. */\n private sharedLayerPath: string | undefined;\n /** Proxy tool names the domain permits. Undefined when every upstream is allowed. */\n private allowedUpstreamTools: Set<string> | undefined;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.configuration = readSessionConfig(options.environment);\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.configuration = configuration;\n this.groups = buildMcpConfigGroups(configuration);\n // The shared layer's path is stable per worktree, so a previous run's file is\n // already in the ordered list the definitions cache is keyed on. Including it\n // here is what makes the proxy's tools available before anything connects.\n this.sharedLayerPath =\n this.groups.shared.configPaths.length > 0 ? sharedLayerPath(configuration.repoRoot) : undefined;\n this.catalog = this.createCatalog();\n // The proxy is a configured server of this session even though it is absent\n // from `.mcp.json`'s filtered layer: doom-mcp rewrites that entry to point at\n // the worktree runtime rather than spawning a second proxy.\n const configuredServers = [\n ...this.groups.sessionLocal.serverNames,\n ...(this.sharedLayerPath ? [PROXY_SERVER_NAME] : []),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }, sharedRuntime] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n import('./sharedRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n const runtime = (this.runtime ??= new McpRuntimeOwner());\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n // Detached: every session start doubles as a sweep of staged layers and dead\n // registry rows left by sessions that never got to clean up after themselves.\n void sharedRuntime.maintainSharedResources(this.configuration.stagingDirectory);\n // Joining the worktree's runtime first, so its endpoint is written before the\n // container reads the layer. Only the first session in a worktree pays to\n // start it; the rest reuse it after one health probe.\n const shared = await sharedRuntime.ensureSharedRuntime({\n repoRoot: this.configuration.repoRoot,\n configPaths: this.groups.shared.configPaths,\n });\n if (generation !== this.lifecycleGeneration) return;\n this.allowedUpstreamTools = sharedRuntime.resolveAllowedUpstreamTools(\n this.groups.shared.configPaths,\n this.groups.shared.allowedUpstreams,\n );\n\n const configPaths = this.configPaths(shared !== undefined);\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe and a proxy on a\n // session that has nothing to connect to.\n if (this.groups.sessionLocal.serverNames.length === 0 && !shared) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /**\n * The ordered layers this session connects, session-local first.\n *\n * The shared layer is included only when its file exists, so a first run does\n * not key the definitions cache on a path that carries nothing.\n */\n private configPaths(sharedIsLive = false): string[] {\n const shared = this.sharedLayerPath;\n const includeShared = shared !== undefined && (sharedIsLive || fs.existsSync(shared));\n return [...this.groups.sessionLocal.configPaths, ...(includeShared ? [shared] : [])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect. The proxy entry points at a runtime shared\n * across every session in the worktree, so dropping its connection here would\n * reach into sessions the user was not looking at. Withholding the tools from\n * Pi's active list is the whole of what \"disabled for this session\" means, and it\n * is instant and reversible in both directions.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, this.permittedTools(change.serverName, tools));\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n\n /**\n * Drops proxy tools whose upstream this session's domain does not allow.\n *\n * Filtered before they reach the catalog, so a disallowed upstream's tool is\n * never registered rather than registered and hidden. The runtime is shared, so\n * this is the only place the per-session policy can apply.\n */\n private permittedTools(serverName: string, tools: McpToolInfo[]): McpToolInfo[] {\n const allowed = this.allowedUpstreamTools;\n if (!allowed || serverName !== PROXY_SERVER_NAME) return tools;\n return tools.filter((tool) => allowed.has(tool.name));\n }\n}\n"],"mappings":"kcAcA,MAAM,EAAsB,uCAmB5B,IAAa,EAAb,KAAwB,CACtB,GAEA,QACA,QACA,OAAkC,CAChC,OAAQ,CAAE,YAAa,CAAC,CAAE,EAC1B,aAAc,CAAE,YAAa,CAAC,EAAG,YAAa,CAAC,CAAE,EACjD,eAAgB,CAAC,CACnB,EACA,QACA,cAEA,gBAEA,qBAEA,cAAiC,IAAI,IACrC,gBAAmC,IAAI,IAEvC,kBAAqC,IAAI,IAEzC,oBAA8B,EAG9B,kBACE,KAAK,SAAS,YAAY,CAAC,EAAE,cAE/B,YAAY,EAA4B,CACtC,KAAK,GAAK,EAAQ,GAClB,KAAK,QAAU,EACf,KAAK,cAAgB,EAAkB,EAAQ,WAAW,EAC1D,KAAK,QAAU,KAAK,cAAc,CACpC,CASA,UAAiB,CACf,EAAiB,KAAK,GAAI,KAAK,OAAO,CACxC,CAEA,eAAoC,CAClC,OAAO,IAAI,EAAW,EAAqB,KAAK,QAAQ,WAAW,CAAC,CACtE,CAQA,QAAQ,EAAkC,EAAkB,KAAK,QAAQ,WAAW,EAAsB,CACxG,KAAK,cAAgB,EACrB,KAAK,OAAS,EAAqB,CAAa,EAIhD,KAAK,gBACH,KAAK,OAAO,OAAO,YAAY,OAAS,EAAI,EAAgB,EAAc,QAAQ,EAAI,IAAA,GACxF,KAAK,QAAU,KAAK,cAAc,EAIlC,IAAM,EAAoB,CACxB,GAAG,KAAK,OAAO,aAAa,YAC5B,GAAI,KAAK,gBAAkB,CAAC,CAAiB,EAAI,CAAC,CACpD,EAMA,OALA,KAAK,QAAQ,KAAK,EAAkB,KAAK,YAAY,CAAC,EAAG,CAAiB,EAI1E,EAAiB,KAAK,GAAI,KAAK,cAAe,KAAK,QAAQ,SAAS,CAAC,EAC9D,KAAK,QAAQ,WAAW,CACjC,CAQA,MAAM,OAAuB,CAC3B,IAAM,EAAa,EAAE,KAAK,oBACpB,CAAC,CAAE,oBAAoB,CAAE,mBAAmB,GAAiB,MAAM,QAAQ,IAAI,CACnF,OAAO,2BACP,OAAO,oBACP,OAAO,sBACT,CAAC,EACD,GAAI,IAAe,KAAK,oBAAqB,OAC7C,IAAM,EAAW,KAAK,UAAY,IAAI,EAGtC,KAAK,cAAc,MAAM,EAGzB,EAAmB,wBAAwB,KAAK,cAAc,gBAAgB,EAI9E,IAAM,EAAS,MAAM,EAAc,oBAAoB,CACrD,SAAU,KAAK,cAAc,SAC7B,YAAa,KAAK,OAAO,OAAO,WAClC,CAAC,EACD,GAAI,IAAe,KAAK,oBAAqB,OAC7C,KAAK,qBAAuB,EAAc,4BACxC,KAAK,OAAO,OAAO,YACnB,KAAK,OAAO,OAAO,gBACrB,EAEA,IAAM,EAAc,KAAK,YAAY,IAAW,IAAA,EAAS,EAIzD,GAAI,KAAK,OAAO,aAAa,YAAY,SAAW,GAAK,CAAC,EAAQ,OAClE,IAAM,EAAa,KAAK,QAAQ,YAAe,MAAM,EAAiB,EACtE,MAAM,EAAQ,MAAM,CAClB,cACA,aACA,oBAAqB,EAAK,IAAe,KAAK,mBAAmB,EAAK,CAAU,EAChF,oBAAsB,GAAW,KAAK,oBAAoB,CAAM,CAClE,CAAC,EAGG,IAAe,KAAK,qBAAqB,MAAM,EAAQ,QAAQ,CACrE,CAQA,mBAA2B,EAAU,EAA0B,CAC7D,KAAK,kBAAkB,IAAI,EAAY,EAAI,SAAS,CAAC,EACrD,KAAK,WAAW,EAChB,KAAK,QAAQ,qBAAqB,EAAK,CAAU,CACnD,CAQA,YAAoB,EAAe,GAAiB,CAClD,IAAM,EAAS,KAAK,gBACd,EAAgB,IAAW,IAAA,KAAc,GAAgB,EAAG,WAAW,CAAM,GACnF,MAAO,CAAC,GAAG,KAAK,OAAO,aAAa,YAAa,GAAI,EAAgB,CAAC,CAAM,EAAI,CAAC,CAAE,CACrF,CAQA,MAAM,YAAY,EAAmC,CACnD,IAAM,EAAgB,KAAK,cAAc,EACzC,GAAI,CAAC,EAAe,MAAU,MAAM,CAAmB,EAEvD,KAAK,cAAc,OAAO,CAAU,EACpC,MAAM,EAAc,iBAAiB,CAAU,EAC/C,MAAM,EAAc,gBAAgB,CAAU,CAChD,CAEA,aAAiC,CAC/B,OAAO,KAAK,QAAQ,WAAW,CACjC,CAGA,YAAuC,CACrC,OAAO,KAAK,QAAQ,OAAO,CAAC,CAAC,IAAK,GAAW,CAC3C,IAAM,EAAmB,KAAK,kBAAkB,IAAI,EAAO,IAAI,EAC/D,OAAO,EAAmB,CAAE,GAAG,EAAQ,kBAAiB,EAAI,CAC9D,CAAC,CACH,CAcA,WAAW,EAAoB,EAAwB,CACrD,KAAK,QAAQ,YAAY,EAAY,CAAC,CAAO,EAC7C,EAAiB,KAAK,GAAI,KAAK,OAAO,EACtC,KAAK,WAAW,CAClB,CAUA,MAAM,cAAc,EAAoB,EAAiC,CAAC,EAAwC,CAChH,GAAI,CAAC,EAAQ,QAAS,CACpB,IAAM,EAAS,KAAK,cAAc,IAAI,CAAU,EAChD,GAAI,EAAQ,OAAO,CACrB,CACA,IAAM,EAAU,KAAK,QACf,EAAW,GAAS,YAAY,EACtC,GAAI,CAAC,GAAW,CAAC,EAAU,MAAU,MAAM,CAAmB,EAE9D,IAAM,EAAY,MAAM,MADC,EAAS,cAAc,gBAAgB,CAAU,EAAA,CACvC,cAAc,EAOjD,OAJK,EAAQ,UAAU,CAAQ,GAC/B,KAAK,cAAc,IAAI,EAAY,CAAS,EAC5C,KAAK,QAAQ,iBAAiB,EAAY,EAAU,MAAM,EAC1D,KAAK,WAAW,EACT,GAJkC,CAK3C,CAGA,SAAS,EAAkC,CAEzC,OADA,KAAK,gBAAgB,IAAI,CAAQ,MACpB,CACX,KAAK,gBAAgB,OAAO,CAAQ,CACtC,CACF,CAUA,YAA2B,CACzB,IAAK,IAAM,KAAY,KAAK,gBAC1B,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CAGd,KAAK,QAAQ,cACX,kCAAkC,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GACzF,CACF,CAEJ,CAEA,gBAAoC,CAClC,OAAO,KAAK,QAAQ,eAAe,CACrC,CAEA,MAAM,SAAyB,CAC7B,EAAE,KAAK,oBACP,MAAM,KAAK,SAAS,QAAQ,CAC9B,CAcA,oBAA4B,EAAoC,CAG1D,EAAO,QAAU,cAAc,KAAK,kBAAkB,OAAO,EAAO,UAAU,EAClF,IAAM,EAAU,KAAK,QACf,EAAW,GAAS,YAAY,GAEpC,EAAO,QAAU,aAAe,EAC5B,EAAS,cAAc,gBAAgB,EAAO,UAAU,CAAC,CAAC,KAAM,GAAe,EAAW,UAAU,CAAC,EACrG,QAAQ,QAAQ,CAAC,CAAC,EAAA,CAErB,UAAY,CAAC,CAAC,CAAC,CACf,KAAM,GAAU,CACf,GAAI,CAAC,GAAS,UAAU,CAAQ,EAAG,OACnC,IAAM,EAAQ,KAAK,QAAQ,iBAAiB,EAAQ,KAAK,eAAe,EAAO,WAAY,CAAK,CAAC,EACjG,EAAiB,KAAK,GAAI,KAAK,cAAe,CAAK,EACnD,EAAiB,KAAK,GAAI,KAAK,OAAO,EACtC,KAAK,WAAW,CAClB,CAAC,CACL,CASA,eAAuB,EAAoB,EAAqC,CAC9E,IAAM,EAAU,KAAK,qBAErB,MADI,CAAC,GAAW,IAAA,gBAAyC,EAClD,EAAM,OAAQ,GAAS,EAAQ,IAAI,EAAK,IAAI,CAAC,CACtD,CACF"}
|
|
1
|
+
{"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { applyActiveTools, registerNewTools } from '../tools/mcpTools.ts';\nimport type { McpResourceView, McpServerView } from '../types.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups } from './configSources.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { type McpSessionConfig, readSessionConfig } from './sessionConfig.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [], serverNames: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.groups = buildMcpConfigGroups(configuration);\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n const configPaths = this.configPaths();\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe on a session that\n // has nothing to connect to.\n if (this.configuredServerNames().length === 0) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /** Ordered layers for the one in-process client container. */\n private configPaths(): string[] {\n return [...this.groups.sessionLocal.configPaths, ...this.groups.shared.configPaths];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, tools);\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-mcp",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"description": "MCP server tools for Doompi",
|
|
5
5
|
"homepage": "https://agimon.ai",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
39
39
|
"@napi-rs/keyring": "1.3.0",
|
|
40
40
|
"typebox": "1.1.38",
|
|
41
|
-
"
|
|
42
|
-
"@agimon-ai/
|
|
43
|
-
"@agimon-ai/
|
|
41
|
+
"yaml": "2.9.0",
|
|
42
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.4",
|
|
43
|
+
"@agimon-ai/mcp-proxy": "0.30.13",
|
|
44
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.4"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./configSources.cjs"),n=require("./mcpRuntime.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:crypto"),a=require("node:path");a=e.__toESM(a,1);let o=require("node:os");o=e.__toESM(o,1);const s=/^(?:shared|mcp)-[0-9a-f]{16}\.json$/;function c(e){let t=(0,i.createHash)(`sha256`).update(e).digest(`hex`).slice(0,16);return a.default.join(o.default.tmpdir(),`doom-mcp`,`shared-${t}.json`)}async function l(e){if(e.configPaths.length!==0)try{let{findHealthyExistingRuntime:n,resolveWorkspaceRoot:i}=await import(`@agimon-ai/mcp-proxy`),o=i(e.repoRoot),s=await n(o),l=s??await u(e.configPaths),d=`http://${l.host}:${l.port}/mcp`,f=c(o);return r.default.mkdirSync(a.default.dirname(f),{mode:448,recursive:!0}),r.default.writeFileSync(f,`${JSON.stringify({mcpServers:{[t.PROXY_SERVER_NAME]:{type:`http`,url:d}}},null,2)}\n`,{mode:384}),{configPath:f,endpoint:d,reusedExistingRuntime:s!==null}}catch{return}}async function u(e){process.env.MCP_PROXY_IDLE_SHUTDOWN_MINUTES??=`45`;let{prestartHttpRuntime:t}=await import(`@agimon-ai/mcp-proxy`),n=await t({host:`localhost`,config:e,cache:!0,clearDefinitionsCache:!1,proxyMode:`flat`,runtimeLifecycle:`manual`});return{host:n.host,port:n.port}}async function d(e,t=Date.now()){f(e,t),await p()}function f(e,t){let n;try{n=r.default.readdirSync(e)}catch{return}for(let i of n){if(!s.test(i))continue;let n=a.default.join(e,i);try{t-r.default.statSync(n).mtimeMs>6048e5&&r.default.unlinkSync(n)}catch{}}}async function p(){try{await(await import(`@agimon-ai/mcp-proxy`)).cleanupStaleRegistries?.()}catch{}}function m(e,t,r){if(!t||t.length===0)return;let i=new Set(t),a=n.readCachedCatalog(e,r),o=new Set,s=h(a.servers);for(let e of a.servers)if(i.has(e.name))for(let t of e.tools)o.add(s.has(t.name)?`${e.name}__${t.name}`:t.name);return o}function h(e){let t=new Set,n=new Set;for(let r of e)for(let e of r.tools)t.has(e.name)&&n.add(e.name),t.add(e.name);return n}exports.ensureSharedRuntime=l,exports.maintainSharedResources=d,exports.resolveAllowedUpstreamTools=m,exports.sharedLayerPath=c;
|
|
2
|
-
//# sourceMappingURL=sharedRuntime.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sharedRuntime.cjs","names":["createHash","path","os","PROXY_SERVER_NAME","fs","readCachedCatalog"],"sources":["../../src/services/sharedRuntime.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { PROXY_SERVER_NAME } from './configSources.ts';\nimport { readCachedCatalog } from './mcpRuntime.ts';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst RUNTIME_HOST = 'localhost';\n/** Flat mode is what `.mcp.json` already asked the proxy for, so tool names are unchanged. */\nconst RUNTIME_PROXY_MODE = 'flat';\n/** Read by the spawned runtime; versions of mcp-proxy without it ignore the value. */\nconst IDLE_SHUTDOWN_ENV = 'MCP_PROXY_IDLE_SHUTDOWN_MINUTES';\n/**\n * Deliberately longer than the proxy's own 30 minute session sweep.\n *\n * A runtime is idle only once it holds no session, and the sweep is what closes\n * the last quiet one. Matching the two would retire the runtime at the same\n * moment, so a session returning after a long pause would always pay a cold\n * start; the extra window lets it rejoin a warm runtime instead.\n */\nconst DEFAULT_IDLE_SHUTDOWN_MINUTES = '45';\n/** Staged layers this package writes: `shared-<hash>.json` and `mcp-<hash>.json`. */\nconst STAGING_FILE_PATTERN = /^(?:shared|mcp)-[0-9a-f]{16}\\.json$/;\nconst STAGING_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport interface SharedRuntimeOptions {\n repoRoot: string;\n /** The worktree-level config the runtime is started from, normally `mcp-config.yaml`. */\n configPaths: string[];\n}\n\nexport interface SharedRuntimeLayer {\n /** Generated `.mcp.json` layer pointing the proxy entry at the live runtime. */\n configPath: string;\n endpoint: string;\n reusedExistingRuntime: boolean;\n}\n\n/**\n * Where the generated layer for a workspace lives.\n *\n * Stable across runs on purpose: the definitions cache is keyed on the ordered\n * list of config paths, so a per-run filename would mint a new cache every start\n * and the tools would never be available before the first connection.\n */\nexport function sharedLayerPath(workspaceRoot: string): string {\n const digest = createHash('sha256').update(workspaceRoot).digest('hex').slice(0, 16);\n return path.join(os.tmpdir(), 'doom-mcp', `shared-${digest}.json`);\n}\n\n/**\n * Starts or joins the worktree's shared MCP runtime and returns a config layer for it.\n *\n * One runtime serves every doom-pi session in the worktree, so a healthy one is\n * always reused rather than respawned; only the first session in a worktree pays\n * to start it. The runtime is reached as an ordinary HTTP server named\n * `agiflow-proxy`, which is what `.mcp.json` already calls it, so the tool names\n * an agent sees are the same as before this package existed.\n *\n * Returns undefined when there is nothing to share, or when the runtime could not\n * be reached: losing the shared upstreams is better than losing the session.\n */\nexport async function ensureSharedRuntime(options: SharedRuntimeOptions): Promise<SharedRuntimeLayer | undefined> {\n if (options.configPaths.length === 0) return undefined;\n\n try {\n const { findHealthyExistingRuntime, resolveWorkspaceRoot } = await import('@agimon-ai/mcp-proxy');\n const workspaceRoot = resolveWorkspaceRoot(options.repoRoot);\n const existing = await findHealthyExistingRuntime(workspaceRoot);\n const runtime = existing ?? (await startRuntime(options.configPaths));\n const endpoint = `http://${runtime.host}:${runtime.port}/mcp`;\n\n const configPath = sharedLayerPath(workspaceRoot);\n fs.mkdirSync(path.dirname(configPath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n fs.writeFileSync(\n configPath,\n `${JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: { type: 'http', url: endpoint } } }, null, 2)}\\n`,\n { mode: PRIVATE_FILE_MODE },\n );\n\n return { configPath, endpoint, reusedExistingRuntime: existing !== null };\n } catch {\n return undefined;\n }\n}\n\nasync function startRuntime(configPaths: string[]): Promise<{ host: string; port: number }> {\n // The runtime inherits this environment, so the value reaches it whichever\n // mcp-proxy version is installed. It bounds how long a runtime outlives its\n // last session; an operator's explicit setting wins.\n process.env[IDLE_SHUTDOWN_ENV] ??= DEFAULT_IDLE_SHUTDOWN_MINUTES;\n const { prestartHttpRuntime } = await import('@agimon-ai/mcp-proxy');\n const result = await prestartHttpRuntime({\n host: RUNTIME_HOST,\n config: configPaths,\n cache: true,\n clearDefinitionsCache: false,\n proxyMode: RUNTIME_PROXY_MODE,\n // Outliving this session is the point: the next one joins it instead of paying\n // to start the same downstream children again. The idle shutdown above is what\n // keeps that from meaning forever.\n runtimeLifecycle: 'manual',\n });\n return { host: result.host, port: result.port };\n}\n\n/**\n * Best-effort maintenance for the resources shared runtimes leave on disk.\n *\n * Runs detached from session start, so a session neither waits for it nor fails\n * because of it. Two jobs: drop staged config layers no session has refreshed\n * within the TTL (every live session rewrites its own layers at start, so age is\n * ownership), and let mcp-proxy prune registry rows whose processes are gone,\n * when the installed version exposes that.\n */\nexport async function maintainSharedResources(stagingDirectory: string, now: number = Date.now()): Promise<void> {\n sweepStagingDirectory(stagingDirectory, now);\n await collectRegistryGarbage();\n}\n\nfunction sweepStagingDirectory(stagingDirectory: string, now: number): void {\n let entries: string[];\n try {\n entries = fs.readdirSync(stagingDirectory);\n } catch {\n // Nothing has been staged yet, so there is nothing to sweep.\n return;\n }\n for (const entry of entries) {\n if (!STAGING_FILE_PATTERN.test(entry)) continue;\n const target = path.join(stagingDirectory, entry);\n try {\n if (now - fs.statSync(target).mtimeMs > STAGING_TTL_MS) fs.unlinkSync(target);\n } catch {\n // Another session touching the same file concurrently is not a failure.\n }\n }\n}\n\nasync function collectRegistryGarbage(): Promise<void> {\n try {\n // Feature-detected: the pinned mcp-proxy may predate the export.\n const proxy = (await import('@agimon-ai/mcp-proxy')) as unknown as { cleanupStaleRegistries?: () => Promise<void> };\n await proxy.cleanupStaleRegistries?.();\n } catch {\n // Garbage collection never gets to break a session.\n }\n}\n\n/**\n * The proxy tool names a domain's upstream allowlist permits.\n *\n * The runtime is shared, so its own configuration cannot be narrowed per session;\n * the allowlist is applied where this session registers tools instead. Attribution\n * comes from the catalog the runtime itself wrote, which records which upstream\n * each tool came from, rather than from parsing the proxied description text.\n *\n * Returns undefined when every upstream is allowed, which needs no filtering.\n */\nexport function resolveAllowedUpstreamTools(\n sharedConfigPaths: string[],\n allowedUpstreams: string[] | undefined,\n cachePathFor?: (paths: string[]) => string,\n): Set<string> | undefined {\n if (!allowedUpstreams || allowedUpstreams.length === 0) return undefined;\n const allowed = new Set(allowedUpstreams);\n const catalog = readCachedCatalog(sharedConfigPaths, cachePathFor);\n const names = new Set<string>();\n const clashing = findClashingToolNames(catalog.servers);\n for (const server of catalog.servers) {\n if (!allowed.has(server.name)) continue;\n for (const tool of server.tools) {\n // Flat mode only prefixes a tool whose name two upstreams both use.\n names.add(clashing.has(tool.name) ? `${server.name}__${tool.name}` : tool.name);\n }\n }\n return names;\n}\n\nfunction findClashingToolNames(servers: ReadonlyArray<{ tools: ReadonlyArray<{ name: string }> }>): Set<string> {\n const seen = new Set<string>();\n const clashing = new Set<string>();\n for (const server of servers) {\n for (const tool of server.tools) {\n if (seen.has(tool.name)) clashing.add(tool.name);\n seen.add(tool.name);\n }\n }\n return clashing;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{PROXY_SERVER_NAME as e}from"./configSources.mjs";import{readCachedCatalog as t}from"./mcpRuntime.mjs";import n from"node:fs";import{createHash as r}from"node:crypto";import i from"node:path";import a from"node:os";const o=/^(?:shared|mcp)-[0-9a-f]{16}\.json$/;function s(e){let t=r(`sha256`).update(e).digest(`hex`).slice(0,16);return i.join(a.tmpdir(),`doom-mcp`,`shared-${t}.json`)}async function c(t){if(t.configPaths.length!==0)try{let{findHealthyExistingRuntime:r,resolveWorkspaceRoot:a}=await import(`@agimon-ai/mcp-proxy`),o=a(t.repoRoot),c=await r(o),u=c??await l(t.configPaths),d=`http://${u.host}:${u.port}/mcp`,f=s(o);return n.mkdirSync(i.dirname(f),{mode:448,recursive:!0}),n.writeFileSync(f,`${JSON.stringify({mcpServers:{[e]:{type:`http`,url:d}}},null,2)}\n`,{mode:384}),{configPath:f,endpoint:d,reusedExistingRuntime:c!==null}}catch{return}}async function l(e){process.env.MCP_PROXY_IDLE_SHUTDOWN_MINUTES??=`45`;let{prestartHttpRuntime:t}=await import(`@agimon-ai/mcp-proxy`),n=await t({host:`localhost`,config:e,cache:!0,clearDefinitionsCache:!1,proxyMode:`flat`,runtimeLifecycle:`manual`});return{host:n.host,port:n.port}}async function u(e,t=Date.now()){d(e,t),await f()}function d(e,t){let r;try{r=n.readdirSync(e)}catch{return}for(let a of r){if(!o.test(a))continue;let r=i.join(e,a);try{t-n.statSync(r).mtimeMs>6048e5&&n.unlinkSync(r)}catch{}}}async function f(){try{await(await import(`@agimon-ai/mcp-proxy`)).cleanupStaleRegistries?.()}catch{}}function p(e,n,r){if(!n||n.length===0)return;let i=new Set(n),a=t(e,r),o=new Set,s=m(a.servers);for(let e of a.servers)if(i.has(e.name))for(let t of e.tools)o.add(s.has(t.name)?`${e.name}__${t.name}`:t.name);return o}function m(e){let t=new Set,n=new Set;for(let r of e)for(let e of r.tools)t.has(e.name)&&n.add(e.name),t.add(e.name);return n}export{c as ensureSharedRuntime,u as maintainSharedResources,p as resolveAllowedUpstreamTools,s as sharedLayerPath};
|
|
2
|
-
//# sourceMappingURL=sharedRuntime.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sharedRuntime.mjs","names":[],"sources":["../../src/services/sharedRuntime.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { PROXY_SERVER_NAME } from './configSources.ts';\nimport { readCachedCatalog } from './mcpRuntime.ts';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst RUNTIME_HOST = 'localhost';\n/** Flat mode is what `.mcp.json` already asked the proxy for, so tool names are unchanged. */\nconst RUNTIME_PROXY_MODE = 'flat';\n/** Read by the spawned runtime; versions of mcp-proxy without it ignore the value. */\nconst IDLE_SHUTDOWN_ENV = 'MCP_PROXY_IDLE_SHUTDOWN_MINUTES';\n/**\n * Deliberately longer than the proxy's own 30 minute session sweep.\n *\n * A runtime is idle only once it holds no session, and the sweep is what closes\n * the last quiet one. Matching the two would retire the runtime at the same\n * moment, so a session returning after a long pause would always pay a cold\n * start; the extra window lets it rejoin a warm runtime instead.\n */\nconst DEFAULT_IDLE_SHUTDOWN_MINUTES = '45';\n/** Staged layers this package writes: `shared-<hash>.json` and `mcp-<hash>.json`. */\nconst STAGING_FILE_PATTERN = /^(?:shared|mcp)-[0-9a-f]{16}\\.json$/;\nconst STAGING_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport interface SharedRuntimeOptions {\n repoRoot: string;\n /** The worktree-level config the runtime is started from, normally `mcp-config.yaml`. */\n configPaths: string[];\n}\n\nexport interface SharedRuntimeLayer {\n /** Generated `.mcp.json` layer pointing the proxy entry at the live runtime. */\n configPath: string;\n endpoint: string;\n reusedExistingRuntime: boolean;\n}\n\n/**\n * Where the generated layer for a workspace lives.\n *\n * Stable across runs on purpose: the definitions cache is keyed on the ordered\n * list of config paths, so a per-run filename would mint a new cache every start\n * and the tools would never be available before the first connection.\n */\nexport function sharedLayerPath(workspaceRoot: string): string {\n const digest = createHash('sha256').update(workspaceRoot).digest('hex').slice(0, 16);\n return path.join(os.tmpdir(), 'doom-mcp', `shared-${digest}.json`);\n}\n\n/**\n * Starts or joins the worktree's shared MCP runtime and returns a config layer for it.\n *\n * One runtime serves every doom-pi session in the worktree, so a healthy one is\n * always reused rather than respawned; only the first session in a worktree pays\n * to start it. The runtime is reached as an ordinary HTTP server named\n * `agiflow-proxy`, which is what `.mcp.json` already calls it, so the tool names\n * an agent sees are the same as before this package existed.\n *\n * Returns undefined when there is nothing to share, or when the runtime could not\n * be reached: losing the shared upstreams is better than losing the session.\n */\nexport async function ensureSharedRuntime(options: SharedRuntimeOptions): Promise<SharedRuntimeLayer | undefined> {\n if (options.configPaths.length === 0) return undefined;\n\n try {\n const { findHealthyExistingRuntime, resolveWorkspaceRoot } = await import('@agimon-ai/mcp-proxy');\n const workspaceRoot = resolveWorkspaceRoot(options.repoRoot);\n const existing = await findHealthyExistingRuntime(workspaceRoot);\n const runtime = existing ?? (await startRuntime(options.configPaths));\n const endpoint = `http://${runtime.host}:${runtime.port}/mcp`;\n\n const configPath = sharedLayerPath(workspaceRoot);\n fs.mkdirSync(path.dirname(configPath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n fs.writeFileSync(\n configPath,\n `${JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: { type: 'http', url: endpoint } } }, null, 2)}\\n`,\n { mode: PRIVATE_FILE_MODE },\n );\n\n return { configPath, endpoint, reusedExistingRuntime: existing !== null };\n } catch {\n return undefined;\n }\n}\n\nasync function startRuntime(configPaths: string[]): Promise<{ host: string; port: number }> {\n // The runtime inherits this environment, so the value reaches it whichever\n // mcp-proxy version is installed. It bounds how long a runtime outlives its\n // last session; an operator's explicit setting wins.\n process.env[IDLE_SHUTDOWN_ENV] ??= DEFAULT_IDLE_SHUTDOWN_MINUTES;\n const { prestartHttpRuntime } = await import('@agimon-ai/mcp-proxy');\n const result = await prestartHttpRuntime({\n host: RUNTIME_HOST,\n config: configPaths,\n cache: true,\n clearDefinitionsCache: false,\n proxyMode: RUNTIME_PROXY_MODE,\n // Outliving this session is the point: the next one joins it instead of paying\n // to start the same downstream children again. The idle shutdown above is what\n // keeps that from meaning forever.\n runtimeLifecycle: 'manual',\n });\n return { host: result.host, port: result.port };\n}\n\n/**\n * Best-effort maintenance for the resources shared runtimes leave on disk.\n *\n * Runs detached from session start, so a session neither waits for it nor fails\n * because of it. Two jobs: drop staged config layers no session has refreshed\n * within the TTL (every live session rewrites its own layers at start, so age is\n * ownership), and let mcp-proxy prune registry rows whose processes are gone,\n * when the installed version exposes that.\n */\nexport async function maintainSharedResources(stagingDirectory: string, now: number = Date.now()): Promise<void> {\n sweepStagingDirectory(stagingDirectory, now);\n await collectRegistryGarbage();\n}\n\nfunction sweepStagingDirectory(stagingDirectory: string, now: number): void {\n let entries: string[];\n try {\n entries = fs.readdirSync(stagingDirectory);\n } catch {\n // Nothing has been staged yet, so there is nothing to sweep.\n return;\n }\n for (const entry of entries) {\n if (!STAGING_FILE_PATTERN.test(entry)) continue;\n const target = path.join(stagingDirectory, entry);\n try {\n if (now - fs.statSync(target).mtimeMs > STAGING_TTL_MS) fs.unlinkSync(target);\n } catch {\n // Another session touching the same file concurrently is not a failure.\n }\n }\n}\n\nasync function collectRegistryGarbage(): Promise<void> {\n try {\n // Feature-detected: the pinned mcp-proxy may predate the export.\n const proxy = (await import('@agimon-ai/mcp-proxy')) as unknown as { cleanupStaleRegistries?: () => Promise<void> };\n await proxy.cleanupStaleRegistries?.();\n } catch {\n // Garbage collection never gets to break a session.\n }\n}\n\n/**\n * The proxy tool names a domain's upstream allowlist permits.\n *\n * The runtime is shared, so its own configuration cannot be narrowed per session;\n * the allowlist is applied where this session registers tools instead. Attribution\n * comes from the catalog the runtime itself wrote, which records which upstream\n * each tool came from, rather than from parsing the proxied description text.\n *\n * Returns undefined when every upstream is allowed, which needs no filtering.\n */\nexport function resolveAllowedUpstreamTools(\n sharedConfigPaths: string[],\n allowedUpstreams: string[] | undefined,\n cachePathFor?: (paths: string[]) => string,\n): Set<string> | undefined {\n if (!allowedUpstreams || allowedUpstreams.length === 0) return undefined;\n const allowed = new Set(allowedUpstreams);\n const catalog = readCachedCatalog(sharedConfigPaths, cachePathFor);\n const names = new Set<string>();\n const clashing = findClashingToolNames(catalog.servers);\n for (const server of catalog.servers) {\n if (!allowed.has(server.name)) continue;\n for (const tool of server.tools) {\n // Flat mode only prefixes a tool whose name two upstreams both use.\n names.add(clashing.has(tool.name) ? `${server.name}__${tool.name}` : tool.name);\n }\n }\n return names;\n}\n\nfunction findClashingToolNames(servers: ReadonlyArray<{ tools: ReadonlyArray<{ name: string }> }>): Set<string> {\n const seen = new Set<string>();\n const clashing = new Set<string>();\n for (const server of servers) {\n for (const tool of server.tools) {\n if (seen.has(tool.name)) clashing.add(tool.name);\n seen.add(tool.name);\n }\n }\n return clashing;\n}\n"],"mappings":"6NAOA,MAiBM,EAAuB,sCAuB7B,SAAgB,EAAgB,EAA+B,CAC7D,IAAM,EAAS,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAa,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EACnF,OAAO,EAAK,KAAK,EAAG,OAAO,EAAG,WAAY,UAAU,EAAO,MAAM,CACnE,CAcA,eAAsB,EAAoB,EAAwE,CAC5G,KAAQ,YAAY,SAAW,EAEnC,GAAI,CACF,GAAM,CAAE,6BAA4B,wBAAyB,MAAM,OAAO,wBACpE,EAAgB,EAAqB,EAAQ,QAAQ,EACrD,EAAW,MAAM,EAA2B,CAAa,EACzD,EAAU,GAAa,MAAM,EAAa,EAAQ,WAAW,EAC7D,EAAW,UAAU,EAAQ,KAAK,GAAG,EAAQ,KAAK,MAElD,EAAa,EAAgB,CAAa,EAQhD,OAPA,EAAG,UAAU,EAAK,QAAQ,CAAU,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,EAAG,cACD,EACA,GAAG,KAAK,UAAU,CAAE,WAAY,EAAG,GAAoB,CAAE,KAAM,OAAQ,IAAK,CAAS,CAAE,CAAE,EAAG,KAAM,CAAC,EAAE,IACrG,CAAE,KAAM,GAAkB,CAC5B,EAEO,CAAE,aAAY,WAAU,sBAAuB,IAAa,IAAK,CAC1E,MAAQ,CACN,MACF,CACF,CAEA,eAAe,EAAa,EAAgE,CAI1F,QAAQ,IAAI,kCAAuB,KACnC,GAAM,CAAE,uBAAwB,MAAM,OAAO,wBACvC,EAAS,MAAM,EAAoB,CACvC,KAAM,YACN,OAAQ,EACR,MAAO,GACP,sBAAuB,GACvB,UAAW,OAIX,iBAAkB,QACpB,CAAC,EACD,MAAO,CAAE,KAAM,EAAO,KAAM,KAAM,EAAO,IAAK,CAChD,CAWA,eAAsB,EAAwB,EAA0B,EAAc,KAAK,IAAI,EAAkB,CAC/G,EAAsB,EAAkB,CAAG,EAC3C,MAAM,EAAuB,CAC/B,CAEA,SAAS,EAAsB,EAA0B,EAAmB,CAC1E,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,CAAgB,CAC3C,MAAQ,CAEN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAqB,KAAK,CAAK,EAAG,SACvC,IAAM,EAAS,EAAK,KAAK,EAAkB,CAAK,EAChD,GAAI,CACE,EAAM,EAAG,SAAS,CAAM,CAAC,CAAC,QAAU,QAAgB,EAAG,WAAW,CAAM,CAC9E,MAAQ,CAER,CACF,CACF,CAEA,eAAe,GAAwC,CACrD,GAAI,CAGF,MAAM,MADe,OAAO,wBAAA,CAChB,yBAAyB,CACvC,MAAQ,CAER,CACF,CAYA,SAAgB,EACd,EACA,EACA,EACyB,CACzB,GAAI,CAAC,GAAoB,EAAiB,SAAW,EAAG,OACxD,IAAM,EAAU,IAAI,IAAI,CAAgB,EAClC,EAAU,EAAkB,EAAmB,CAAY,EAC3D,EAAQ,IAAI,IACZ,EAAW,EAAsB,EAAQ,OAAO,EACtD,IAAK,IAAM,KAAU,EAAQ,QACtB,KAAQ,IAAI,EAAO,IAAI,EAC5B,IAAK,IAAM,KAAQ,EAAO,MAExB,EAAM,IAAI,EAAS,IAAI,EAAK,IAAI,EAAI,GAAG,EAAO,KAAK,IAAI,EAAK,OAAS,EAAK,IAAI,EAGlF,OAAO,CACT,CAEA,SAAS,EAAsB,EAAiF,CAC9G,IAAM,EAAO,IAAI,IACX,EAAW,IAAI,IACrB,IAAK,IAAM,KAAU,EACnB,IAAK,IAAM,KAAQ,EAAO,MACpB,EAAK,IAAI,EAAK,IAAI,GAAG,EAAS,IAAI,EAAK,IAAI,EAC/C,EAAK,IAAI,EAAK,IAAI,EAGtB,OAAO,CACT"}
|