{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/core/mcp-foundation/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiKH,wBAAgB,aAAa,IAAI,MAAM,CAOtC;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CA8EvE","sourcesContent":["/**\n * MCP Client Foundation — CLI (2.13.0).\n *\n * `jensen mcp inspect|tools|call|health <server-config.json>`\n *\n * One-shot, process-lifetime honest operations: connect, perform the requested\n * operation, record evidence (for calls), and disconnect. There is no fake\n * cross-process persistent session; the Worker daemon is a later layer.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport { getAgentDir } from \"../../config.js\";\nimport { EvidenceFileStore } from \"../context-runtime/evidence-archive.js\";\nimport { McpClientService } from \"./mcp-client-service.js\";\nimport { McpClientError } from \"./mcp-error.js\";\nimport type { McpServerDefinition, McpServerSession, McpSessionHealth, McpToolResult } from \"./mcp-types.js\";\nimport { parseMcpServerConfigJson } from \"./server-config.js\";\n\nconst SUBCOMMANDS = new Set([\"inspect\", \"tools\", \"call\", \"health\"]);\n\nfunction buildService(): McpClientService {\n\tconst archive = new EvidenceFileStore(join(getAgentDir(), \"context-evidence\"));\n\treturn new McpClientService({ evidenceArchive: archive });\n}\n\nfunction loadServerConfig(configPath: string): McpServerDefinition {\n\tlet text: string;\n\ttry {\n\t\ttext = readFileSync(configPath, \"utf8\");\n\t} catch (error) {\n\t\tthrow new McpClientError(\n\t\t\t\"MCP_SERVER_NOT_FOUND\",\n\t\t\t`cannot read server config \"${configPath}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t{ detail: configPath },\n\t\t);\n\t}\n\treturn parseMcpServerConfigJson(text);\n}\n\nfunction valueArgs(args: string[]): string[] {\n\treturn args.filter((a) => !a.startsWith(\"--\"));\n}\n\nfunction flag(args: string[], name: string): boolean {\n\treturn args.includes(name);\n}\n\nfunction printJson(payload: unknown): void {\n\tprocess.stdout.write(`${JSON.stringify(payload, null, 2)}\\n`);\n}\n\nfunction codeOf(error: unknown): string | undefined {\n\tif (typeof error === \"object\" && error !== null && \"code\" in error) {\n\t\treturn String((error as { code: unknown }).code);\n\t}\n\treturn undefined;\n}\n\nfunction renderError(error: unknown): void {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tconst code = codeOf(error);\n\tprocess.stderr.write(`${chalk.red(code ? `${code}: ${message}` : message)}\\n`);\n}\n\ninterface ParsedArgs {\n\targs: Record<string, unknown>;\n\ttimeoutMs?: number;\n}\n\nfunction parseCallArgs(raw: string[]): ParsedArgs {\n\tconst args: Record<string, unknown> = {};\n\tlet timeoutMs: number | undefined;\n\n\tfor (let i = 0; i < raw.length; i++) {\n\t\tconst token = raw[i];\n\t\tif (token === \"--arg\" || token === \"--arg-json\") {\n\t\t\tconst pair = raw[i + 1];\n\t\t\tif (pair === undefined || pair.startsWith(\"--\")) {\n\t\t\t\tthrow new McpClientError(\"MCP_INVALID_TOOL_CALL\", `${token} requires a key=value argument`);\n\t\t\t}\n\t\t\tconst eq = pair.indexOf(\"=\");\n\t\t\tif (eq <= 0) throw new McpClientError(\"MCP_INVALID_TOOL_CALL\", `${token} requires key=value`);\n\t\t\tconst key = pair.slice(0, eq);\n\t\t\tconst valueText = pair.slice(eq + 1);\n\t\t\tif (token === \"--arg\") {\n\t\t\t\targs[key] = valueText;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\targs[key] = JSON.parse(valueText) as unknown;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthrow new McpClientError(\n\t\t\t\t\t\t\"MCP_INVALID_TOOL_CALL\",\n\t\t\t\t\t\t`--arg-json ${key} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--timeout-ms\") {\n\t\t\tconst value = raw[i + 1];\n\t\t\tif (value === undefined) throw new McpClientError(\"MCP_INVALID_TOOL_CALL\", \"--timeout-ms requires a number\");\n\t\t\tconst parsed = Number(value);\n\t\t\tif (!Number.isSafeInteger(parsed) || parsed <= 0) {\n\t\t\t\tthrow new McpClientError(\"MCP_INVALID_TOOL_CALL\", \"--timeout-ms must be a positive integer\");\n\t\t\t}\n\t\t\ttimeoutMs = parsed;\n\t\t\ti += 1;\n\t\t}\n\t}\n\n\treturn { args, timeoutMs };\n}\n\nfunction renderCapabilityKeys(capabilities: Record<string, unknown> | undefined): string {\n\tif (!capabilities) return \"(unknown)\";\n\tconst keys = Object.keys(capabilities).sort();\n\treturn keys.length > 0 ? keys.join(\", \") : \"(none)\";\n}\n\nfunction renderSession(session: McpServerSession): void {\n\tprocess.stdout.write(`MCP SERVER\\n`);\n\tprocess.stdout.write(`  server: ${session.serverId}\\n`);\n\tprocess.stdout.write(`  session: ${session.sessionId}\\n`);\n\tprocess.stdout.write(`  state: ${session.state}\\n`);\n\tif (session.identity) process.stdout.write(`  identity: ${session.identity.name}@${session.identity.version}\\n`);\n\tif (session.protocolVersion) process.stdout.write(`  protocol: ${session.protocolVersion}\\n`);\n\tif (session.protocolEra) process.stdout.write(`  protocol era: ${session.protocolEra}\\n`);\n\tprocess.stdout.write(`  capabilities: ${renderCapabilityKeys(session.capabilities)}\\n`);\n\tif (session.pid !== undefined) process.stdout.write(`  pid: ${session.pid}\\n`);\n\tif (session.failure) process.stdout.write(`  failure: ${session.failure.code}: ${session.failure.message}\\n`);\n}\n\nfunction renderHealth(health: McpSessionHealth): void {\n\tprocess.stdout.write(`MCP HEALTH\\n`);\n\tprocess.stdout.write(`  server: ${health.serverId}\\n`);\n\tprocess.stdout.write(`  session: ${health.sessionId}\\n`);\n\tprocess.stdout.write(`  state: ${health.state}\\n`);\n\tprocess.stdout.write(`  connected: ${health.connected}\\n`);\n\tprocess.stdout.write(`  processAlive: ${health.processAlive}\\n`);\n\tif (health.protocolVersion) process.stdout.write(`  protocol: ${health.protocolVersion}\\n`);\n\tif (health.protocolEra) process.stdout.write(`  protocol era: ${health.protocolEra}\\n`);\n\tif (health.failed && health.failure)\n\t\tprocess.stdout.write(`  failure: ${health.failure.code}: ${health.failure.message}\\n`);\n}\n\nfunction renderToolResult(result: McpToolResult): void {\n\tprocess.stdout.write(`MCP TOOL CALL\\n`);\n\tprocess.stdout.write(`  server: ${result.serverId}\\n`);\n\tprocess.stdout.write(`  tool: ${result.toolName}\\n`);\n\tprocess.stdout.write(`  status: ${result.status}\\n`);\n\tif (result.errorCode)\n\t\tprocess.stdout.write(`  error: ${result.errorCode}${result.errorMessage ? `: ${result.errorMessage}` : \"\"}\\n`);\n\tif (result.evidenceId) process.stdout.write(`  evidence: ${result.evidenceId}\\n`);\n\tfor (const item of result.content) {\n\t\tif (item.type === \"text\" && typeof item.text === \"string\") {\n\t\t\tprocess.stdout.write(`  text: ${item.text}\\n`);\n\t\t} else {\n\t\t\tprocess.stdout.write(`  content: ${item.type}\\n`);\n\t\t}\n\t}\n}\n\n// =============================================================================\n// Handler\n// =============================================================================\n\nexport function printMcpUsage(): string {\n\treturn [\n\t\t\"  mcp inspect <server-config.json> [--json]\",\n\t\t\"  mcp tools <server-config.json> [--json]\",\n\t\t\"  mcp call <server-config.json> <tool> [--arg key=value]... [--arg-json key=json]... [--timeout-ms N] [--json]\",\n\t\t\"  mcp health <server-config.json> [--json]\",\n\t].join(\"\\n\");\n}\n\nexport async function handleMcpCommand(args: string[]): Promise<boolean> {\n\tif (args[0] !== \"mcp\") return false;\n\tconst sub = args[1];\n\tif (!sub || !SUBCOMMANDS.has(sub)) return false;\n\n\tconst json = flag(args, \"--json\");\n\tconst configPath = valueArgs(args)[2];\n\tif (!configPath) {\n\t\tprocess.stderr.write(\"missing server config path\\n\");\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n\n\tconst service = buildService();\n\n\ttry {\n\t\tconst definition = loadServerConfig(configPath);\n\t\tconst session = await service.connect(definition);\n\n\t\ttry {\n\t\t\tswitch (sub) {\n\t\t\t\tcase \"inspect\": {\n\t\t\t\t\tif (json) printJson(session);\n\t\t\t\t\telse renderSession(session);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tcase \"tools\": {\n\t\t\t\t\tconst tools = await service.listTools(session.sessionId);\n\t\t\t\t\tif (json) printJson({ serverId: session.serverId, sessionId: session.sessionId, tools });\n\t\t\t\t\telse {\n\t\t\t\t\t\tif (tools.length === 0) process.stdout.write(\"(no tools)\\n\");\n\t\t\t\t\t\tfor (const tool of tools) {\n\t\t\t\t\t\t\tprocess.stdout.write(`${tool.name}${tool.description ? `  ${tool.description}` : \"\"}\\n`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tcase \"call\": {\n\t\t\t\t\tconst toolName = valueArgs(args)[3];\n\t\t\t\t\tif (!toolName) {\n\t\t\t\t\t\tprocess.stderr.write(\"missing tool name\\n\");\n\t\t\t\t\t\tprocess.exitCode = 1;\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tconst parsed = parseCallArgs(args);\n\t\t\t\t\tconst result = await service.callTool(session.sessionId, {\n\t\t\t\t\t\ttoolName,\n\t\t\t\t\t\targuments: parsed.args,\n\t\t\t\t\t\ttimeoutMs: parsed.timeoutMs,\n\t\t\t\t\t});\n\t\t\t\t\tif (json) printJson(result);\n\t\t\t\t\telse renderToolResult(result);\n\t\t\t\t\tif (result.status !== \"success\") process.exitCode = 1;\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tcase \"health\": {\n\t\t\t\t\tconst health = await service.health(session.sessionId);\n\t\t\t\t\tif (json) printJson(health);\n\t\t\t\t\telse renderHealth(health);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tdefault:\n\t\t\t\t\treturn false;\n\t\t\t}\n\t\t} finally {\n\t\t\tawait service.disconnect(session.sessionId).catch(() => {\n\t\t\t\t// Disconnect failures must not mask the primary operation result.\n\t\t\t});\n\t\t}\n\t} catch (error) {\n\t\trenderError(error);\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n}\n"]}