@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
JavaScript
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