cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

283 lines
9.8 KiB
TypeScript

import { lookup } from "node:dns/promises";
import type { LookupFunction } from "node:net";
import { isIP } from "node:net";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { ConnectorTool } from "@rakazo/adapter-kit";
import { Agent, fetch as undiciFetch } from "undici";
import { combineSignals } from "./connector-safety.js";
import {
createAddressCheckedLookup,
isCloudMetadataAddress,
isPrivateAddress,
isTailscaleAddress,
type ResolvedAddress,
type ResolveHostname,
withPinnedDnsLookup,
} from "./network-address.js";
const MAX_MCP_TOOLS = 250;
const MAX_MCP_PAGES = 20;
const MCP_TIMEOUT_MS = 30_000;
const MAX_RESULT_BYTES = 1_000_000;
export type { ResolveHostname } from "./network-address.js";
export interface RemoteTransportDependencies {
fetch?: typeof globalThis.fetch;
resolveHostname?: ResolveHostname;
}
export interface RemoteMcpOptions extends RemoteTransportDependencies {
endpoint: string;
headers?: Record<string, string>;
signal?: AbortSignal;
}
export interface SafeRemoteFetch {
(input: string | URL | Request, init?: RequestInit): Promise<Response>;
close(): Promise<void>;
}
const resolveHostname: ResolveHostname = (hostname) =>
lookup(hostname, { all: true, verbatim: true });
export async function listRemoteMcpTools(options: RemoteMcpOptions): Promise<ConnectorTool[]> {
return withRemoteMcpClient(options, async (client, signal) => {
const tools: ConnectorTool[] = [];
let cursor: string | undefined;
for (let page = 0; page < MAX_MCP_PAGES && tools.length < MAX_MCP_TOOLS; page += 1) {
const result = await client.listTools(cursor ? { cursor } : undefined, {
signal,
timeout: MCP_TIMEOUT_MS,
});
for (const tool of result.tools) {
if (tools.length >= MAX_MCP_TOOLS) break;
tools.push({
name: tool.name,
description: tool.description ?? tool.title ?? tool.name,
inputSchema: tool.inputSchema,
readOnly: tool.annotations?.readOnlyHint,
});
}
cursor = result.nextCursor;
if (!cursor) break;
}
return tools;
});
}
export async function callRemoteMcpTool(
options: RemoteMcpOptions,
toolName: string,
args: Record<string, unknown>,
): Promise<unknown> {
return withRemoteMcpClient(options, async (client, signal) => {
const result = await client.callTool({ name: toolName, arguments: args }, undefined, {
signal,
timeout: MCP_TIMEOUT_MS,
});
return limitRemoteMcpPayload({
content: result.content,
structuredContent: result.structuredContent,
isError: result.isError ?? false,
_meta: result._meta,
});
});
}
async function withRemoteMcpClient<T>(
options: RemoteMcpOptions,
run: (client: Client, signal: AbortSignal) => Promise<T>,
): Promise<T> {
const endpoint = await assertSafeRemoteUrl(
options.endpoint,
options.resolveHostname ?? resolveHostname,
);
const signal = combineSignals(options.signal, AbortSignal.timeout(MCP_TIMEOUT_MS));
const safeFetch = createSafeRemoteFetch(
options.fetch,
options.resolveHostname ?? resolveHostname,
);
const transport = new StreamableHTTPClientTransport(endpoint, {
requestInit: {
headers: options.headers,
redirect: "manual",
signal,
},
fetch: safeFetch,
});
const client = new Client({ name: "rakazo", version: "0.1.0" }, { capabilities: {} });
try {
await client.connect(transport, { signal, timeout: MCP_TIMEOUT_MS });
return await run(client, signal);
} finally {
await client.close().catch(() => undefined);
await safeFetch.close().catch(() => undefined);
}
}
export async function assertSafeRemoteUrl(
value: string,
resolve: ResolveHostname = resolveHostname,
): Promise<URL> {
return (await inspectSafeRemoteUrl(value, resolve)).url;
}
async function inspectSafeRemoteUrl(
value: string,
resolve: ResolveHostname,
): Promise<{ url: URL; addresses: ResolvedAddress[] }> {
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error("Connector URL is invalid");
}
if (url.protocol !== "https:") throw new Error("Connector URL must use HTTPS");
if (url.username || url.password) throw new Error("Connector URL must not contain credentials");
if (url.hash) throw new Error("Connector URL must not contain a fragment");
const hostname = url.hostname.replace(/^\[|\]$/g, "");
if (isPrivateHostname(hostname)) throw new Error("Connector URL targets a private host");
const addresses = await resolve(hostname);
assertPublicAddresses(addresses, hostname);
return { url, addresses };
}
/** Drive the package `Agent` with that same undici's fetch. Node 22's
* built-in fetch is an older undici major, so handing it a package Agent as
* `dispatcher` throws `invalid onRequestStart` before any socket opens.
* Captured Node fetch is paired the same way. Injected fetches are not given
* that Agent; they keep the original hostname for TLS/SNI and pin TCP to the
* already-validated address through `dns.lookup`. */
const packageFetch = undiciFetch as unknown as typeof globalThis.fetch;
const nodeFetch = globalThis.fetch;
function requestInitWithHost(url: URL, init: RequestInit): RequestInit {
const headers = new Headers(init.headers);
headers.set("host", url.host);
return { ...init, headers };
}
export function createSafeRemoteFetch(
baseFetch?: typeof globalThis.fetch,
resolve: ResolveHostname = resolveHostname,
): SafeRemoteFetch {
const dispatcher = new Agent({ connect: { lookup: createSafeLookup(resolve) } });
const usePackageFetch =
baseFetch == null || baseFetch === nodeFetch || baseFetch === packageFetch;
const safeFetch = async (input: string | URL | Request, init?: RequestInit) => {
if (typeof input !== "string" && !(input instanceof URL)) {
throw new Error("Connector fetch requires a URL, not a Request");
}
const { url, addresses } = await inspectSafeRemoteUrl(String(input), resolve);
let response: Response;
try {
const requestInit = { ...init, redirect: "manual" as const };
response = usePackageFetch
? await packageFetch(url, {
...requestInit,
dispatcher,
} as RequestInit & { dispatcher: Agent })
: await withPinnedDnsLookup(url.hostname, addresses, () =>
baseFetch!(url, requestInitWithHost(url, requestInit)),
);
} catch (error) {
const detail = transportFailureDetail(error);
throw new Error(`Could not reach ${url.host}${detail ? `: ${detail}` : ""}`, {
cause: error,
});
}
if (response.status >= 300 && response.status < 400) {
throw new Error("Connector redirects are not allowed");
}
return response;
};
const result = safeFetch as SafeRemoteFetch;
result.close = () => dispatcher.close();
return result;
}
const MAX_CAUSE_DEPTH = 5;
/** undici reports refused ports, unreachable hosts, DNS misses and TLS errors
* alike as `TypeError: fetch failed` and keeps the actionable reason in `cause`
* (or in the per-address errors of a happy-eyeballs AggregateError). */
function transportFailureDetail(error: unknown, depth = 0): string | undefined {
if (depth >= MAX_CAUSE_DEPTH || !(error instanceof Error)) return undefined;
if (error instanceof AggregateError) {
for (const inner of error.errors) {
const detail = transportFailureDetail(inner, depth + 1);
if (detail) return detail;
}
}
return (
transportFailureDetail(error.cause, depth + 1) ??
(error.message === "fetch failed" ? undefined : error.message)
);
}
export function createSafeLookup(resolve: ResolveHostname = resolveHostname): LookupFunction {
return createAddressCheckedLookup(resolve, assertPublicAddresses);
}
/** Tailscale MagicDNS names (*.ts.net) are public DNS names, not private IP literals. */
function isTailscaleMagicDnsHostname(hostname: string): boolean {
const normalized = hostname.toLowerCase().replace(/\.$/, "");
return normalized === "ts.net" || normalized.endsWith(".ts.net");
}
function isPrivateHostname(hostname: string): boolean {
const normalized = hostname.toLowerCase().replace(/\.$/, "");
if (isTailscaleMagicDnsHostname(normalized)) return false;
return (
normalized === "localhost" ||
normalized.endsWith(".localhost") ||
normalized.endsWith(".local") ||
normalized.endsWith(".internal") ||
normalized === "metadata.google.internal" ||
(isIP(normalized) !== 0 && isPrivateAddress(normalized))
);
}
function assertPublicAddresses(addresses: ResolvedAddress[], hostname?: string): void {
if (addresses.length === 0) {
throw new Error("Connector URL resolves to a private address");
}
const magicDns = hostname != null && isTailscaleMagicDnsHostname(hostname);
if (
addresses.some((entry) => {
if (isCloudMetadataAddress(entry.address)) return true;
if (!isPrivateAddress(entry.address)) return false;
// Allow only Tailscale ranges for MagicDNS; keep other private ranges blocked.
return !(magicDns && isTailscaleAddress(entry.address));
})
) {
throw new Error("Connector URL resolves to a private address");
}
}
export function limitRemoteMcpPayload(value: unknown): unknown {
const serialized = JSON.stringify(value);
if (serialized === undefined) return value;
const bytes = Buffer.from(serialized, "utf8");
if (bytes.byteLength <= MAX_RESULT_BYTES) return value;
return {
truncated: true,
content: decodeUtf8Prefix(bytes, MAX_RESULT_BYTES),
};
}
function decodeUtf8Prefix(bytes: Uint8Array, maxBytes: number): string {
const decoder = new TextDecoder("utf-8", { fatal: true });
let end = Math.min(bytes.byteLength, maxBytes);
while (end > 0) {
try {
return decoder.decode(bytes.subarray(0, end));
} catch {
end -= 1;
}
}
return "";
}