{"version":3,"file":"mcp-direct-tool-allowlist.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/mcp-direct-tool-allowlist.ts"],"names":[],"mappings":"AA4BA,UAAU,WAAW;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAC;CACjC;AAgCD,MAAM,WAAW,8BAA8B;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,8BAA8B,CAC7C,cAAc,EAAE,MAAM,EAAE,GAAG,SAAS,EACpC,GAAG,SAAgB,GACjB,8BAA8B,EAAE,CAWlC;AAwKD,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,SAAgB,GAAG,MAAM,EAAE,CAE7G;AA4BD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,WAAW,GAAG,MAAM,CAkBpE","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { getAgentDir, getProjectConfigDir } from \"../../shared/utils.ts\";\n\nconst CACHE_VERSION = 1;\nconst CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;\nconst BUILTIN_TOOL_NAMES = new Set([\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"mcp\"]);\nconst GENERIC_GLOBAL_CONFIG_PATH = path.join(os.homedir(), \".config\", \"mcp\", \"mcp.json\");\nconst IMPORT_PATHS = {\n\tcursor: [path.join(os.homedir(), \".cursor\", \"mcp.json\")],\n\t\"claude-code\": [\n\t\tpath.join(os.homedir(), \".claude\", \"mcp.json\"),\n\t\tpath.join(os.homedir(), \".claude.json\"),\n\t\tpath.join(os.homedir(), \".claude\", \"claude_desktop_config.json\"),\n\t],\n\t\"claude-desktop\": [\n\t\tpath.join(os.homedir(), \"Library\", \"Application Support\", \"Claude\", \"claude_desktop_config.json\"),\n\t],\n\tcodex: [path.join(os.homedir(), \".codex\", \"config.json\")],\n\twindsurf: [path.join(os.homedir(), \".windsurf\", \"mcp.json\")],\n\tvscode: [\".vscode/mcp.json\"],\n} as const;\n\ntype ToolPrefix = \"server\" | \"none\" | \"short\";\ntype ImportKind = keyof typeof IMPORT_PATHS;\n\ninterface ServerEntry {\n\tcommand?: string;\n\targs?: string[];\n\tsocket?: string;\n\tenv?: Record<string, string>;\n\tcwd?: string;\n\turl?: string;\n\theaders?: Record<string, string>;\n\tauth?: \"oauth\" | \"bearer\" | false;\n\tbearerToken?: string;\n\tbearerTokenEnv?: string;\n\texposeResources?: boolean;\n\tincludeTools?: string[];\n\texcludeTools?: string[];\n\tprotocolVersion?: string;\n\tdirectTools?: boolean | string[];\n}\n\ninterface McpConfig {\n\tmcpServers: Record<string, ServerEntry>;\n\timports?: ImportKind[];\n\tsettings?: {\n\t\ttoolPrefix?: ToolPrefix;\n\t\tdirectTools?: boolean;\n\t};\n}\n\ninterface CachedTool {\n\tname?: string;\n}\n\ninterface CachedResource {\n\turi?: string;\n\tname?: string;\n}\n\ninterface ServerCacheEntry {\n\tconfigHash?: string;\n\ttools?: CachedTool[];\n\tresources?: CachedResource[];\n\tcachedAt?: number;\n}\n\ninterface MetadataCache {\n\tversion: number;\n\tservers: Record<string, ServerCacheEntry>;\n}\n\nexport interface ResolvedMcpDirectToolSelection {\n\tname: string;\n\tselector: string;\n}\n\nexport function resolveMcpDirectToolSelections(\n\tmcpDirectTools: string[] | undefined,\n\tcwd = process.cwd(),\n): ResolvedMcpDirectToolSelection[] {\n\tif (!mcpDirectTools?.length) return [];\n\n\ttry {\n\t\tconst config = loadMcpConfig(cwd);\n\t\tconst cache = loadMetadataCache();\n\t\tif (!cache) return [];\n\t\treturn resolveDirectToolSelections(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools);\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction loadMetadataCache(): MetadataCache | null {\n\tconst cachePath = path.join(getAgentDir(), \"mcp-cache.json\");\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(fs.readFileSync(cachePath, \"utf-8\"));\n\t} catch {\n\t\treturn null;\n\t}\n\n\tif (!parsed || typeof parsed !== \"object\") return null;\n\tconst raw = parsed as Record<string, unknown>;\n\tif (raw.version !== CACHE_VERSION || !raw.servers || typeof raw.servers !== \"object\" || Array.isArray(raw.servers)) {\n\t\treturn null;\n\t}\n\treturn raw as unknown as MetadataCache;\n}\n\nfunction loadMcpConfig(cwd: string): McpConfig {\n\tlet config: McpConfig = { mcpServers: {} };\n\tfor (const sourcePath of getConfigPaths(cwd)) {\n\t\tconst loaded = readConfig(sourcePath);\n\t\tif (!loaded) continue;\n\t\tconfig = mergeConfigs(config, expandImports(loaded, cwd));\n\t}\n\treturn config;\n}\n\nfunction getConfigPaths(cwd: string): string[] {\n\tconst piGlobalPath = path.join(getAgentDir(), \"mcp.json\");\n\tconst projectPath = path.resolve(cwd, \".mcp.json\");\n\tconst projectPiPath = path.resolve(getProjectConfigDir(cwd), \"mcp.json\");\n\tconst sources: string[] = [];\n\tif (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath) sources.push(GENERIC_GLOBAL_CONFIG_PATH);\n\tsources.push(piGlobalPath);\n\tif (projectPath !== piGlobalPath) sources.push(projectPath);\n\tif (projectPiPath !== piGlobalPath && projectPiPath !== projectPath) sources.push(projectPiPath);\n\treturn sources;\n}\n\nfunction readConfig(configPath: string): McpConfig | null {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(fs.readFileSync(configPath, \"utf-8\"));\n\t} catch {\n\t\treturn null;\n\t}\n\treturn validateConfig(parsed);\n}\n\nfunction validateConfig(raw: unknown): McpConfig {\n\tif (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return { mcpServers: {} };\n\tconst obj = raw as Record<string, unknown>;\n\tconst servers = obj.mcpServers ?? obj[\"mcp-servers\"] ?? {};\n\treturn {\n\t\tmcpServers:\n\t\t\tservers && typeof servers === \"object\" && !Array.isArray(servers)\n\t\t\t\t? (servers as Record<string, ServerEntry>)\n\t\t\t\t: {},\n\t\timports: Array.isArray(obj.imports)\n\t\t\t? obj.imports.filter((value): value is ImportKind => isImportKind(value))\n\t\t\t: undefined,\n\t\tsettings:\n\t\t\tobj.settings && typeof obj.settings === \"object\" && !Array.isArray(obj.settings)\n\t\t\t\t? (obj.settings as McpConfig[\"settings\"])\n\t\t\t\t: undefined,\n\t};\n}\n\nfunction mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {\n\tconst imports = [...(base.imports ?? []), ...(next.imports ?? [])];\n\treturn {\n\t\tmcpServers: { ...base.mcpServers, ...next.mcpServers },\n\t\timports: imports.length ? [...new Set(imports)] : undefined,\n\t\tsettings: next.settings ? { ...base.settings, ...next.settings } : base.settings,\n\t};\n}\n\nfunction expandImports(config: McpConfig, cwd: string): McpConfig {\n\tif (!config.imports?.length) return config;\n\n\tconst importedServers: Record<string, ServerEntry> = {};\n\tfor (const importKind of config.imports) {\n\t\tconst importPath = resolveImportPath(importKind, cwd);\n\t\tif (!importPath) continue;\n\t\tlet imported: unknown;\n\t\ttry {\n\t\t\timported = JSON.parse(fs.readFileSync(importPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const [name, definition] of Object.entries(extractServers(imported, importKind))) {\n\t\t\tif (!importedServers[name]) importedServers[name] = definition;\n\t\t}\n\t}\n\n\treturn {\n\t\timports: config.imports,\n\t\tsettings: config.settings,\n\t\tmcpServers: { ...importedServers, ...config.mcpServers },\n\t};\n}\n\nfunction resolveImportPath(importKind: ImportKind, cwd: string): string | null {\n\tfor (const candidate of IMPORT_PATHS[importKind]) {\n\t\tconst fullPath = candidate.startsWith(\".\") ? path.resolve(cwd, candidate) : candidate;\n\t\tif (fs.existsSync(fullPath)) return fullPath;\n\t}\n\treturn null;\n}\n\nfunction extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {\n\tif (!config || typeof config !== \"object\" || Array.isArray(config)) return {};\n\tconst obj = config as Record<string, unknown>;\n\tconst servers =\n\t\tkind === \"cursor\" || kind === \"windsurf\" || kind === \"vscode\"\n\t\t\t? (obj.mcpServers ?? obj[\"mcp-servers\"])\n\t\t\t: obj.mcpServers;\n\treturn servers && typeof servers === \"object\" && !Array.isArray(servers)\n\t\t? (servers as Record<string, ServerEntry>)\n\t\t: {};\n}\n\nfunction resolveDirectToolSelections(\n\tconfig: McpConfig,\n\tcache: MetadataCache,\n\tprefix: ToolPrefix,\n\tenvOverride: string[],\n): ResolvedMcpDirectToolSelection[] {\n\tconst names: ResolvedMcpDirectToolSelection[] = [];\n\tconst seenNames = new Set<string>();\n\tconst { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);\n\n\tfor (const [serverName, definition] of Object.entries(config.mcpServers)) {\n\t\tconst serverCache = cache.servers[serverName];\n\t\tif (!isServerCacheValid(serverCache, definition)) continue;\n\n\t\tconst toolFilter = selectedServers.has(serverName) ? true : selectedTools.get(serverName);\n\t\tif (!toolFilter) continue;\n\n\t\tfor (const tool of Array.isArray(serverCache.tools) ? serverCache.tools : []) {\n\t\t\tif (typeof tool?.name !== \"string\" || !tool.name) continue;\n\t\t\tif (toolFilter !== true && !toolFilter.has(tool.name)) continue;\n\t\t\tif (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;\n\t\t\tconst prefixedName = formatToolName(tool.name, serverName, prefix);\n\t\t\tif (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;\n\t\t\tseenNames.add(prefixedName);\n\t\t\tnames.push({ name: prefixedName, selector: `${serverName}/${tool.name}` });\n\t\t}\n\n\t\tif (definition.exposeResources === false) continue;\n\t\tfor (const resource of Array.isArray(serverCache.resources) ? serverCache.resources : []) {\n\t\t\tif (typeof resource?.name !== \"string\" || !resource.name || typeof resource.uri !== \"string\" || !resource.uri)\n\t\t\t\tcontinue;\n\t\t\tconst baseName = `get_${resourceNameToToolName(resource.name)}`;\n\t\t\tif (toolFilter !== true && !toolFilter.has(baseName)) continue;\n\t\t\tif (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;\n\t\t\tconst prefixedName = formatToolName(baseName, serverName, prefix);\n\t\t\tif (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;\n\t\t\tseenNames.add(prefixedName);\n\t\t\tnames.push({ name: prefixedName, selector: `${serverName}/${baseName}` });\n\t\t}\n\t}\n\n\treturn names;\n}\n\nexport function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {\n\treturn resolveMcpDirectToolSelections(mcpDirectTools, cwd).map((selection) => selection.name);\n}\n\nfunction parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {\n\tconst servers = new Set<string>();\n\tconst tools = new Map<string, Set<string>>();\n\tfor (let item of selections) {\n\t\titem = item.replace(/\\/+$/, \"\");\n\t\tif (item.includes(\"/\")) {\n\t\t\tconst [server, tool] = item.split(\"/\", 2);\n\t\t\tif (server && tool) {\n\t\t\t\tif (!tools.has(server)) tools.set(server, new Set());\n\t\t\t\ttools.get(server)!.add(tool);\n\t\t\t} else if (server) {\n\t\t\t\tservers.add(server);\n\t\t\t}\n\t\t} else if (item) {\n\t\t\tservers.add(item);\n\t\t}\n\t}\n\treturn { servers, tools };\n}\n\nfunction isServerCacheValid(entry: ServerCacheEntry | undefined, definition: ServerEntry): entry is ServerCacheEntry {\n\tif (!entry || entry.configHash !== computeMcpServerHash(definition)) return false;\n\tif (!entry.cachedAt || typeof entry.cachedAt !== \"number\") return false;\n\treturn Date.now() - entry.cachedAt <= CACHE_MAX_AGE_MS;\n}\n\nexport function computeMcpServerHash(definition: ServerEntry): string {\n\tconst identity: Record<string, unknown> = {\n\t\tcommand: definition.command,\n\t\targs: definition.args,\n\t\tsocket: resolveConfigPath(definition.socket),\n\t\tenv: interpolateEnvRecord(definition.env),\n\t\tcwd: resolveConfigPath(definition.cwd),\n\t\turl: resolveServerUrl(definition),\n\t\theaders: interpolateEnvRecord(definition.headers),\n\t\tauth: definition.auth,\n\t\tbearerToken: resolveBearerToken(definition),\n\t\tbearerTokenEnv: definition.bearerTokenEnv,\n\t\texposeResources: definition.exposeResources,\n\t\tincludeTools: definition.includeTools,\n\t\texcludeTools: definition.excludeTools,\n\t\tprotocolVersion: definition.protocolVersion,\n\t};\n\treturn createHash(\"sha256\").update(stableStringify(identity)).digest(\"hex\");\n}\n\nfunction getToolPrefix(value: unknown): ToolPrefix {\n\treturn value === \"none\" || value === \"short\" || value === \"server\" ? value : \"server\";\n}\n\nfunction isImportKind(value: unknown): value is ImportKind {\n\treturn typeof value === \"string\" && Object.hasOwn(IMPORT_PATHS, value);\n}\n\nfunction getServerPrefix(serverName: string, mode: ToolPrefix): string {\n\tif (mode === \"none\") return \"\";\n\tif (mode === \"short\") {\n\t\tconst short = serverName.replace(/-?mcp$/i, \"\").replace(/-/g, \"_\");\n\t\treturn short || \"mcp\";\n\t}\n\treturn serverName.replace(/-/g, \"_\");\n}\n\nfunction formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string {\n\tconst serverPrefix = getServerPrefix(serverName, prefix);\n\treturn serverPrefix ? `${serverPrefix}_${toolName}` : toolName;\n}\n\nfunction isToolExcluded(toolName: string, serverName: string, prefix: ToolPrefix, excludeTools: unknown): boolean {\n\tif (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;\n\tconst candidates = new Set([\n\t\tnormalizeToolName(toolName),\n\t\tnormalizeToolName(formatToolName(toolName, serverName, prefix)),\n\t\tnormalizeToolName(formatToolName(toolName, serverName, \"server\")),\n\t\tnormalizeToolName(formatToolName(toolName, serverName, \"short\")),\n\t]);\n\treturn excludeTools.some((excluded) => typeof excluded === \"string\" && candidates.has(normalizeToolName(excluded)));\n}\n\nfunction normalizeToolName(value: string): string {\n\treturn value.replace(/-/g, \"_\");\n}\n\nfunction resourceNameToToolName(name: string): string {\n\tlet result = name\n\t\t.replace(/[^a-zA-Z0-9]/g, \"_\")\n\t\t.replace(/_+/g, \"_\")\n\t\t.replace(/^_+/, \"\")\n\t\t.replace(/_+$/, \"\")\n\t\t.toLowerCase();\n\tif (!result || /^\\d/.test(result)) result = `resource${result ? `_${result}` : \"\"}`;\n\treturn result;\n}\n\nfunction interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {\n\tif (!values) return undefined;\n\treturn Object.fromEntries(Object.entries(values).map(([key, value]) => [key, interpolateSecretExpression(value)]));\n}\n\nfunction interpolateEnvVars(value: string): string {\n\treturn value\n\t\t.replace(/\\$\\{(\\w+)\\}/g, (_, name: string) => process.env[name] ?? \"\")\n\t\t.replace(/\\$env:(\\w+)/g, (_, name: string) => process.env[name] ?? \"\")\n\t\t.replace(/\\{env:(\\w+)\\}/g, (_, name: string) => process.env[name] ?? \"\");\n}\n\nfunction interpolateSecretExpression(value: string): string {\n\tif (value.startsWith(\"!!\")) return interpolateEnvVars(value.slice(1));\n\treturn value.startsWith(\"!\") ? value : interpolateEnvVars(value);\n}\n\nfunction getMissingEnvVars(value: string): string[] {\n\tconst missing = new Set<string>();\n\tfor (const match of value.matchAll(/\\$\\{(\\w+)\\}|\\$env:(\\w+)|\\{env:(\\w+)\\}/g)) {\n\t\tconst name = match[1] ?? match[2] ?? match[3];\n\t\tif (name && process.env[name] === undefined) missing.add(name);\n\t}\n\treturn [...missing];\n}\n\nfunction resolveServerUrl(definition: Pick<ServerEntry, \"url\">): string | undefined {\n\tif (definition.url == null) return undefined;\n\tif (typeof definition.url !== \"string\") throw new Error(\"MCP server URL must be a string\");\n\n\tconst missing = getMissingEnvVars(definition.url);\n\tif (missing.length > 0) {\n\t\tthrow new Error(\n\t\t\t`Missing environment variable${missing.length === 1 ? \"\" : \"s\"} in MCP server URL: ${missing.join(\", \")}`,\n\t\t);\n\t}\n\n\tconst resolved = interpolateEnvVars(definition.url);\n\ttry {\n\t\tnew URL(resolved);\n\t} catch (error) {\n\t\tthrow new Error(`Invalid MCP server URL after environment interpolation: ${resolved}`, { cause: error });\n\t}\n\treturn resolved;\n}\n\nfunction resolveConfigPath(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst resolved = interpolateEnvVars(value);\n\tif (resolved === \"~\") return os.homedir();\n\tif (resolved.startsWith(\"~/\") || resolved.startsWith(\"~\\\\\")) return path.join(os.homedir(), resolved.slice(2));\n\treturn resolved;\n}\n\nfunction resolveBearerToken(definition: Pick<ServerEntry, \"bearerToken\" | \"bearerTokenEnv\">): string | undefined {\n\tif (definition.bearerToken !== undefined) return interpolateSecretExpression(definition.bearerToken);\n\treturn definition.bearerTokenEnv ? process.env[definition.bearerTokenEnv] : undefined;\n}\n\nfunction stableStringify(value: unknown): string {\n\tif (value === null || value === undefined || typeof value !== \"object\") {\n\t\tconst serialized = JSON.stringify(value);\n\t\treturn serialized === undefined ? \"undefined\" : serialized;\n\t}\n\tif (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(\",\")}]`;\n\tconst obj = value as Record<string, unknown>;\n\treturn `{${Object.keys(obj)\n\t\t.sort()\n\t\t.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`)\n\t\t.join(\",\")}}`;\n}\n"]}