// Minimal Streamable-HTTP MCP client used to read discovery resources from an // openwork-cloud connection. Extracted from connect-skill-catalog so the skill // index and the Automation index speak to the same connection the same way. export type McpFetch = (input: string, init?: RequestInit) => Promise; export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function stringHeaders(value: unknown): Record { if (!isRecord(value)) return {}; return Object.fromEntries( Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"), ); } function parseJsonOrText(raw: string): unknown { try { return JSON.parse(raw); } catch { return raw; } } export async function readMcpPayload(response: Response): Promise { const raw = await response.text(); if (!raw.trim()) return null; if (!response.headers.get("content-type")?.toLowerCase().includes("text/event-stream")) return parseJsonOrText(raw); for (const frame of raw.split(/\r?\n\r?\n/)) { const data = frame.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n"); if (data) return parseJsonOrText(data); } return null; } export function jsonRpcResult(payload: unknown): Record | null { const record = Array.isArray(payload) ? payload.find(isRecord) : payload; if (!isRecord(record) || record.error !== undefined || !isRecord(record.result)) return null; return record.result; } export async function mcpPost(fetcher: McpFetch, url: string, headers: Record, body: unknown) { const response = await fetcher(url, { method: "POST", headers: { accept: "application/json, text/event-stream", "content-type": "application/json", ...headers }, body: JSON.stringify(body), signal: AbortSignal.timeout(5_000), }); return { response, payload: await readMcpPayload(response) }; } /** * Reads one JSON resource from an openwork-cloud config. Returns the resource * text, or null when the config is unusable (invalid URL, disabled, auth * rejected, transport or protocol error) so callers can try another candidate. */ export async function readMcpResourceText(input: { config: Record; uri: string; fetcher: McpFetch; clientName: string; }): Promise { const url = typeof input.config.url === "string" ? input.config.url : ""; if (!/^https?:\/\//.test(url) || input.config.enabled === false) return null; const baseHeaders = stringHeaders(input.config.headers); const initialized = await mcpPost(input.fetcher, url, baseHeaders, { id: 1, jsonrpc: "2.0", method: "initialize", params: { capabilities: {}, clientInfo: { name: input.clientName, version: "1.0.0" }, protocolVersion: "2025-06-18", }, }); if (!initialized.response.ok || !jsonRpcResult(initialized.payload)) return null; const sessionHeaders = { ...baseHeaders, ...(initialized.response.headers.get("mcp-session-id") ? { "mcp-session-id": initialized.response.headers.get("mcp-session-id")! } : {}), ...(initialized.response.headers.get("mcp-protocol-version") ? { "mcp-protocol-version": initialized.response.headers.get("mcp-protocol-version")! } : {}), }; await mcpPost(input.fetcher, url, sessionHeaders, { jsonrpc: "2.0", method: "notifications/initialized", params: {} }); const resource = await mcpPost(input.fetcher, url, sessionHeaders, { id: 2, jsonrpc: "2.0", method: "resources/read", params: { uri: input.uri }, }); if (!resource.response.ok) return null; const contents = jsonRpcResult(resource.payload)?.contents; if (!Array.isArray(contents)) return null; const text = contents.find((item) => isRecord(item) && item.uri === input.uri && typeof item.text === "string")?.text; return typeof text === "string" ? text : null; } export function escapeXml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }