import { getNonEmptyString, toRecord } from "./common"; /** * Parse a qualified MCP tool name of the form `server:tool`. * * Returns `{ server, tool }` when the string contains exactly one colon with * non-empty text on both sides; otherwise returns `null`. */ export function parseQualifiedMcpToolName( value: string, ): { server: string; tool: string } | null { const trimmed = value.trim(); if (!trimmed) { return null; } const colonIndex = trimmed.indexOf(":"); if (colonIndex <= 0 || colonIndex >= trimmed.length - 1) { return null; } const server = trimmed.slice(0, colonIndex).trim(); const tool = trimmed.slice(colonIndex + 1).trim(); if (!server || !tool) { return null; } return { server, tool }; } function addDerivedMcpServerTargets( toolName: string, configuredServerNames: readonly string[], pushTarget: (value: string | null) => void, ): void { const trimmedToolName = toolName.trim(); if (!trimmedToolName) { return; } for (const serverName of configuredServerNames) { const trimmedServerName = serverName.trim(); if (!trimmedServerName) { continue; } if (!trimmedToolName.endsWith(`_${trimmedServerName}`)) { continue; } if (trimmedToolName.startsWith(`${trimmedServerName}_`)) { continue; } pushTarget(`${trimmedServerName}_${trimmedToolName}`); pushTarget(`${trimmedServerName}:${trimmedToolName}`); pushTarget(trimmedServerName); } } function pushMcpToolPermissionTargets( rawReference: string, serverHint: string | null, configuredServerNames: readonly string[], pushTarget: (value: string | null) => void, ): void { const qualified = parseQualifiedMcpToolName(rawReference); const resolvedServer = serverHint ?? qualified?.server ?? null; const resolvedTool = qualified?.tool ?? rawReference; if (resolvedServer) { pushTarget(`${resolvedServer}_${resolvedTool}`); pushTarget(`${resolvedServer}:${resolvedTool}`); pushTarget(resolvedServer); } else { addDerivedMcpServerTargets(resolvedTool, configuredServerNames, pushTarget); } pushTarget(resolvedTool); pushTarget(rawReference); } /** * Derive the ordered list of MCP permission-lookup candidates from a raw MCP * tool invocation input. * * Candidates are ordered from most-specific to least-specific so that * `evaluateFirst()` stops at the first non-default match. */ export function createMcpPermissionTargets( input: unknown, configuredServerNames: readonly string[] = [], ): string[] { const record = toRecord(input); const tool = getNonEmptyString(record.tool); const server = getNonEmptyString(record.server); const connect = getNonEmptyString(record.connect); const describe = getNonEmptyString(record.describe); const search = getNonEmptyString(record.search); const targets: string[] = []; const pushTarget = (value: string | null) => { if (!value) { return; } if (!targets.includes(value)) { targets.push(value); } }; if (tool) { pushMcpToolPermissionTargets( tool, server, configuredServerNames, pushTarget, ); pushTarget("mcp_call"); return targets; } if (connect) { pushTarget(`mcp_connect_${connect}`); pushTarget(connect); pushTarget("mcp_connect"); return targets; } if (describe) { pushMcpToolPermissionTargets( describe, server, configuredServerNames, pushTarget, ); pushTarget("mcp_describe"); return targets; } if (search) { if (server) { pushTarget(`mcp_server_${server}`); pushTarget(server); } pushTarget(search); pushTarget("mcp_search"); return targets; } if (server) { pushTarget(`mcp_server_${server}`); pushTarget(server); pushTarget("mcp_list"); return targets; } pushTarget("mcp_status"); return targets; }