UNPKG

@tanstack/ai-mcp

Version:

Host-side Model Context Protocol client for TanStack AI: discover and run MCP server tools, resources, and prompts in any adapter's chat() loop, with generated end-to-end types.

93 lines (92 loc) • 3.7 kB
import { isRecord } from "./events/value.js"; import { SSEClientTransport, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; //#region src/transport.ts /** * Return true when `input` is already a Transport, not a config object. */ function isTransportInstance(input) { return "start" in input && typeof input.start === "function"; } /** * SDK 2.1.0 does not send `Mcp-Name` for custom `events/*` methods. * SEP-3415 requires it on Streamable HTTP for name-scoped events requests. */ async function mcpNameFromRpc(request) { if (request.method !== "POST") return void 0; let json; try { json = await request.clone().json(); } catch { return; } if (!isRecord(json)) return void 0; if (json.method !== "events/subscribe" && json.method !== "events/unsubscribe") return; if (!isRecord(json.params) || typeof json.params.name !== "string") return; return json.params.name; } var MCP_NAME_SENTINEL_PREFIX = "=?base64?"; var MCP_NAME_SENTINEL_SUFFIX = "?="; /** * Copy of SDK 2.1.0 `needsBase64` / `encodeMcpParamValue` (not exported). * Printable ASCII with no leading/trailing whitespace is sent as-is. * Anything else uses `=?base64?{b64-of-utf8}?=` so `Headers.set` stays ASCII. */ function needsBase64(value) { if (value.length === 0) return true; if (value.startsWith(MCP_NAME_SENTINEL_PREFIX) && value.endsWith(MCP_NAME_SENTINEL_SUFFIX)) return true; if (value !== value.trim()) return true; for (let i = 0; i < value.length; i++) { const code = value.codePointAt(i); if (code === 9 || code !== void 0 && code >= 32 && code <= 126) continue; return true; } return false; } function utf8ToBase64(value) { const bytes = new TextEncoder().encode(value); let binary = ""; for (const byte of bytes) binary += String.fromCodePoint(byte); return btoa(binary); } function encodeMcpName(value) { return needsBase64(value) ? `${MCP_NAME_SENTINEL_PREFIX}${utf8ToBase64(value)}${MCP_NAME_SENTINEL_SUFFIX}` : value; } function fetchWithMcpEventName(baseFetch = fetch) { return async (input, init) => { const request = new Request(input, init); if (request.headers.has("mcp-name")) return baseFetch(input, init); const name = await mcpNameFromRpc(request); if (name === void 0) return baseFetch(input, init); const headers = new Headers(request.headers); headers.set("Mcp-Name", encodeMcpName(name)); return baseFetch(new Request(request, { headers })); }; } /** * Build a Transport from HTTP config, SSE config, or an existing Transport. * * For stdio, build the Transport with `stdioTransport` and pass that instance. * Throws an Error when the config type is `stdio` or is not a known type. * With `events`, HTTP and SSE requests for `events/*` get the `Mcp-Name` header. */ async function resolveTransport(input, options) { if (isTransportInstance(input)) return input; const withEvents = (base) => options?.events ? fetchWithMcpEventName(base) : base; switch (input.type) { case "http": return new StreamableHTTPClientTransport(new URL(input.url), { requestInit: { headers: input.headers }, fetch: withEvents(input.fetch), authProvider: input.authProvider }); case "sse": return new SSEClientTransport(new URL(input.url), { requestInit: { headers: input.headers }, fetch: withEvents(input.fetch), authProvider: input.authProvider }); case "stdio": throw new Error("stdio transport must be created via '@tanstack/ai-mcp/stdio': import { stdioTransport } from '@tanstack/ai-mcp/stdio' and pass the result as `transport`."); default: throw new Error(`Unknown MCP transport config: ${JSON.stringify(input)}`); } } //#endregion export { isTransportInstance, resolveTransport }; //# sourceMappingURL=transport.js.map