{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/core/mcp.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\n\nexport type McpTransport = \"stdio\" | \"sse\" | \"streamable_http\";\nexport type McpHealth = \"configured\" | \"starting\" | \"ready\" | \"degraded\" | \"reconnecting\" | \"failed\" | \"stopped\";\n\nexport interface McpServerIdentity {\n\tserverId: string;\n\tconfigIdentity: string;\n\ttransport: McpTransport;\n\tendpointIdentity: string;\n\tprocessIdentity?: string;\n\tconnectedAt?: string;\n\tprotocolVersion?: string;\n\tcapabilitiesHash?: string;\n}\n\nexport interface McpServerConfig {\n\tserverId: string;\n\ttransport: McpTransport;\n\tcommand?: string;\n\targs?: string[];\n\turl?: string;\n\tenvRefs?: Record<string, string>;\n\theaderRefs?: Record<string, string>;\n\tstartupTimeoutMs?: number;\n\trequestTimeoutMs?: number;\n\tmaxReconnects?: number;\n}\n\nexport interface McpToolSchema {\n\tname: string;\n\tdescription?: string;\n\tinputSchema: Record<string, unknown>;\n\toutputSchema?: Record<string, unknown>;\n}\n\nexport interface McpCapabilitySnapshot {\n\tserverId: string;\n\tprotocolVersion?: string;\n\ttools: McpToolSchema[];\n\tresources: Array<{ uri: string; name?: string; mimeType?: string }>;\n\tresourceTemplates: Array<{ uriTemplate: string; name?: string }>;\n\tprompts: Array<{ name: string; description?: string }>;\n\tcapabilities: Record<string, boolean>;\n\tsnapshotHash: string;\n\tretrievedAt: string;\n}\n\nexport type McpEffectClass = \"read_only\" | \"writes_workspace\" | \"mutates_external_state\" | \"unknown\";\n\nexport interface McpToolEffects {\n\tclassification: McpEffectClass;\n\tmutatesExternalState: boolean | \"unknown\";\n\tparallelSafe: boolean;\n\trequiresApproval: boolean;\n\tsource: \"configured\" | \"administrator\" | \"builtin\" | \"inferred\" | \"unknown\";\n}\n\nexport interface McpValidationIssue {\n\tcode: string;\n\tmessage: string;\n\ttoolName?: string;\n}\n\nexport interface McpValidationResult {\n\tvalid: boolean;\n\ttools: McpToolSchema[];\n\trejected: McpValidationIssue[];\n}\n\nconst MAX_SCHEMA_BYTES = 128 * 1024;\nconst MAX_SCHEMA_DEPTH = 12;\nconst MAX_DESCRIPTION_LENGTH = 8 * 1024;\nconst MAX_TOOLS = 256;\nconst URI_SCHEMES = new Set([\"https:\", \"http:\"]);\n\nfunction canonical(value: unknown): string {\n\tif (value === null || typeof value !== \"object\") return JSON.stringify(value);\n\tif (Array.isArray(value)) return `[${value.map(canonical).join(\",\")}]`;\n\tconst record = value as Record<string, unknown>;\n\treturn `{${Object.keys(record)\n\t\t.sort()\n\t\t.map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`)\n\t\t.join(\",\")}}`;\n}\n\nexport function canonicalizeMcp(value: unknown): string {\n\treturn canonical(value);\n}\n\nexport function mcpSha256(value: unknown): string {\n\treturn createHash(\"sha256\").update(canonical(value)).digest(\"hex\");\n}\n\nfunction depth(value: unknown): number {\n\tif (!value || typeof value !== \"object\") return 0;\n\tlet maximum = 0;\n\tconst pending: Array<{ value: unknown; level: number }> = [{ value, level: 0 }];\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop();\n\t\tif (!current || !current.value || typeof current.value !== \"object\") continue;\n\t\tmaximum = Math.max(maximum, current.level);\n\t\tif (maximum > MAX_SCHEMA_DEPTH) return maximum;\n\t\tconst children = Array.isArray(current.value)\n\t\t\t? current.value\n\t\t\t: Object.values(current.value as Record<string, unknown>);\n\t\tfor (const child of children) pending.push({ value: child, level: current.level + 1 });\n\t}\n\treturn maximum;\n}\n\nfunction isJsonSchema(schema: unknown): schema is Record<string, unknown> {\n\tif (!schema || typeof schema !== \"object\" || Array.isArray(schema)) return false;\n\tconst type = (schema as Record<string, unknown>).type;\n\treturn type === undefined || typeof type === \"string\" || Array.isArray(type);\n}\n\nexport function validateMcpToolSchemas(tools: McpToolSchema[]): McpValidationResult {\n\tconst rejected: McpValidationIssue[] = [];\n\tconst valid: McpToolSchema[] = [];\n\tconst names = new Set<string>();\n\tif (tools.length > MAX_TOOLS)\n\t\trejected.push({ code: \"tool_count_limit\", message: `maximum ${MAX_TOOLS} tools exceeded` });\n\tfor (const tool of tools.slice(0, MAX_TOOLS)) {\n\t\tif (!tool || typeof tool.name !== \"string\" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(tool.name)) {\n\t\t\trejected.push({ code: \"invalid_tool_name\", message: \"tool name is invalid\", toolName: tool?.name });\n\t\t\tcontinue;\n\t\t}\n\t\tif (names.has(tool.name)) {\n\t\t\trejected.push({ code: \"tool_name_collision\", message: \"duplicate tool name\", toolName: tool.name });\n\t\t\tcontinue;\n\t\t}\n\t\tnames.add(tool.name);\n\t\tif (!isJsonSchema(tool.inputSchema) || (tool.outputSchema !== undefined && !isJsonSchema(tool.outputSchema))) {\n\t\t\trejected.push({\n\t\t\t\tcode: \"invalid_json_schema\",\n\t\t\t\tmessage: \"input/output schema is not a JSON object schema\",\n\t\t\t\ttoolName: tool.name,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst schemaDepth = depth(tool.inputSchema);\n\t\tif (schemaDepth > MAX_SCHEMA_DEPTH) {\n\t\t\trejected.push({\n\t\t\t\tcode: \"schema_depth_limit\",\n\t\t\t\tmessage: \"schema exceeds safety limits\",\n\t\t\t\ttoolName: tool.name,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst schemaBytes = Buffer.byteLength(canonical(tool.inputSchema));\n\t\tif (schemaBytes > MAX_SCHEMA_BYTES) {\n\t\t\trejected.push({\n\t\t\t\tcode: schemaBytes > MAX_SCHEMA_BYTES ? \"schema_size_limit\" : \"schema_depth_limit\",\n\t\t\t\tmessage: \"schema exceeds safety limits\",\n\t\t\t\ttoolName: tool.name,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (\n\t\t\ttool.description !== undefined &&\n\t\t\t(typeof tool.description !== \"string\" || tool.description.length > MAX_DESCRIPTION_LENGTH)\n\t\t) {\n\t\t\trejected.push({\n\t\t\t\tcode: \"description_limit\",\n\t\t\t\tmessage: \"tool description exceeds safety limits\",\n\t\t\t\ttoolName: tool.name,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tvalid.push({ ...tool, description: tool.description?.slice(0, MAX_DESCRIPTION_LENGTH) });\n\t}\n\treturn { valid: rejected.length === 0, tools: valid, rejected };\n}\n\nexport function classifyMcpToolEffects(tool: McpToolSchema, configured?: McpToolEffects): McpToolEffects {\n\tif (configured?.classification === \"unknown\" || configured?.mutatesExternalState === true)\n\t\treturn { ...configured, requiresApproval: true, parallelSafe: false };\n\tif (configured) return configured;\n\tconst text = `${tool.name} ${tool.description ?? \"\"}`.toLowerCase();\n\tif (/(delete|remove|write|edit|update|send|publish|execute|deploy|create)/.test(text))\n\t\treturn {\n\t\t\tclassification: \"unknown\",\n\t\t\tmutatesExternalState: \"unknown\",\n\t\t\tparallelSafe: false,\n\t\t\trequiresApproval: true,\n\t\t\tsource: \"inferred\",\n\t\t};\n\tif (/\\b(read|list|search|fetch|get|inspect|status)\\b/.test(text))\n\t\treturn {\n\t\t\tclassification: \"read_only\",\n\t\t\tmutatesExternalState: false,\n\t\t\tparallelSafe: true,\n\t\t\trequiresApproval: false,\n\t\t\tsource: \"inferred\",\n\t\t};\n\treturn {\n\t\tclassification: \"unknown\",\n\t\tmutatesExternalState: \"unknown\",\n\t\tparallelSafe: false,\n\t\trequiresApproval: true,\n\t\tsource: \"unknown\",\n\t};\n}\n\nexport function validateMcpConfig(config: McpServerConfig): McpValidationIssue[] {\n\tconst issues: McpValidationIssue[] = [];\n\tif (!/^[A-Za-z0-9_.:-]{1,128}$/.test(config.serverId))\n\t\tissues.push({ code: \"invalid_server_id\", message: \"server ID is invalid\" });\n\tif (config.transport === \"stdio\" && (!config.command || config.url))\n\t\tissues.push({ code: \"stdio_command_required\", message: \"stdio requires command and no URL\" });\n\tif (config.transport !== \"stdio\" && (!config.url || !/^https?:\\/\\//i.test(config.url)))\n\t\tissues.push({ code: \"remote_url_required\", message: \"remote transport requires an HTTP(S) URL\" });\n\tif (config.url) {\n\t\ttry {\n\t\t\tconst parsed = new URL(config.url);\n\t\t\tif (parsed.protocol === \"http:\" && process.env.JENSEN_ALLOW_INSECURE_MCP !== \"1\")\n\t\t\t\tissues.push({\n\t\t\t\t\tcode: \"https_required\",\n\t\t\t\t\tmessage: \"HTTPS is required unless explicitly enabled for local fixtures\",\n\t\t\t\t});\n\t\t\tif (parsed.username || parsed.password || parsed.search || parsed.hash)\n\t\t\t\tissues.push({\n\t\t\t\t\tcode: \"secret_or_volatile_url\",\n\t\t\t\t\tmessage: \"credentials, query strings and fragments are not accepted in MCP identity\",\n\t\t\t\t});\n\t\t} catch {\n\t\t\tissues.push({ code: \"invalid_url\", message: \"MCP URL is invalid\" });\n\t\t}\n\t}\n\tif (config.startupTimeoutMs !== undefined && (config.startupTimeoutMs < 100 || config.startupTimeoutMs > 120_000))\n\t\tissues.push({ code: \"startup_timeout_limit\", message: \"startup timeout is outside bounds\" });\n\tif (config.requestTimeoutMs !== undefined && (config.requestTimeoutMs < 100 || config.requestTimeoutMs > 600_000))\n\t\tissues.push({ code: \"request_timeout_limit\", message: \"request timeout is outside bounds\" });\n\tif (config.maxReconnects !== undefined && (config.maxReconnects < 0 || config.maxReconnects > 10))\n\t\tissues.push({ code: \"reconnect_limit\", message: \"reconnect attempts are outside bounds\" });\n\treturn issues;\n}\n\nexport function createMcpIdentity(config: McpServerConfig): McpServerIdentity {\n\tconst endpointIdentity = config.transport === \"stdio\" ? (config.command ?? \"\") : new URL(config.url ?? \"\").origin;\n\treturn {\n\t\tserverId: config.serverId,\n\t\tconfigIdentity: mcpSha256(config),\n\t\ttransport: config.transport,\n\t\tendpointIdentity,\n\t};\n}\n\nexport function createMcpCapabilitySnapshot(\n\tinput: Omit<McpCapabilitySnapshot, \"snapshotHash\" | \"retrievedAt\">,\n): McpCapabilitySnapshot {\n\tconst snapshot = { ...input, snapshotHash: \"\", retrievedAt: new Date().toISOString() };\n\treturn { ...snapshot, snapshotHash: mcpSha256(snapshot) };\n}\n\nexport function detectCapabilityDrift(previous: McpCapabilitySnapshot, next: McpCapabilitySnapshot): string[] {\n\tconst changes: string[] = [];\n\tif (previous.protocolVersion !== next.protocolVersion) changes.push(\"protocol_version_changed\");\n\tconst before = new Map(previous.tools.map((tool) => [tool.name, mcpSha256(tool)]));\n\tconst after = new Map(next.tools.map((tool) => [tool.name, mcpSha256(tool)]));\n\tfor (const name of new Set([...before.keys(), ...after.keys()])) {\n\t\tif (!before.has(name)) changes.push(`tool_added:${name}`);\n\t\telse if (!after.has(name)) changes.push(`tool_removed:${name}`);\n\t\telse if (before.get(name) !== after.get(name)) changes.push(`tool_changed:${name}`);\n\t}\n\treturn changes.sort();\n}\n\nexport function validateMcpResourceUri(uri: string): boolean {\n\ttry {\n\t\tconst parsed = new URL(uri);\n\t\treturn URI_SCHEMES.has(parsed.protocol) && !parsed.username && !parsed.password;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst MCP_SECRET_VALUE =\n\t/(?:bearer\\s+|sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}|-----BEGIN [A-Z ]+-----|eyJ[A-Za-z0-9_-]{8,}\\.)/i;\nexport function redactMcpDiagnostics(value: unknown): unknown {\n\tif (typeof value === \"string\") return MCP_SECRET_VALUE.test(value) ? \"[REDACTED]\" : value;\n\tif (Array.isArray(value)) return value.map(redactMcpDiagnostics);\n\tif (!value || typeof value !== \"object\") return value;\n\tconst out: Record<string, unknown> = {};\n\tfor (const [key, child] of Object.entries(value as Record<string, unknown>))\n\t\tout[key] = /(authorization|token|secret|password|cookie|api[-_]?key|private[-_]?key)/i.test(key)\n\t\t\t? \"[REDACTED]\"\n\t\t\t: redactMcpDiagnostics(child);\n\treturn out;\n}\n"]}