openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
122 lines (121 loc) • 5.13 kB
JavaScript
import { a as loadUndiciRuntimeDeps } from "./undici-runtime-B6tpBayG.mjs";
import { b as ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "./ssrf-D5TD9F1s.mjs";
import { f as wrapGuardedBodyStream, i as fetchWithSsrFGuard } from "./fetch-guard-CIAMeSj8.mjs";
import fs from "node:fs";
//#region src/agents/mcp-http-fetch.ts
/**
* MCP HTTP fetch wrappers.
* Adds SSRF protection, scoped TLS/client-cert dispatchers, response cleanup,
* and same-origin header handling around the MCP SDK fetch contract.
*/
/** Default MCP HTTP fetch backed by lazy-loaded undici runtime deps. */
const fetchWithUndici = async (url, init) => await loadUndiciRuntimeDeps().fetch(url, init);
const fetchWithUndiciGuard = async (input, init) => await fetchWithUndici(input instanceof Request ? input.url : input, init);
const MCP_HTTP_MAX_REDIRECTS = 20;
function resolveFetchRequest(input, init) {
if (input instanceof Request) {
const request = new Request(input, init);
const body = request.body ?? void 0;
return {
url: request.url,
signal: request.signal,
init: {
method: request.method,
headers: request.headers,
body,
redirect: request.redirect,
...body ? { duplex: "half" } : {}
}
};
}
const { signal, ...requestInit } = init ?? {};
return {
url: input instanceof URL ? input.toString() : input,
signal: signal ?? void 0,
init: init ? requestInit : void 0
};
}
async function buildManagedMcpResponse(response, release, refreshTimeout) {
if (!response.body) release();
return new Response(response.body ? wrapGuardedBodyStream({
body: response.body,
cleanup: release,
refreshTimeout
}) : null, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
function buildMcpHttpFetchWithRedirectPolicy(params, redirectPolicy) {
const needsCustomDispatcher = params.sslVerify === false || Boolean(params.clientCert || params.clientKey);
const scopedOrigin = params.resourceUrl ? new URL(params.resourceUrl).origin : void 0;
const policy = params.resourceUrl ? ssrfPolicyFromHttpBaseUrlAllowedOrigin(params.resourceUrl) : void 0;
let customConnect;
const resolveCustomDispatcherPolicy = (url) => {
if (!needsCustomDispatcher || !scopedOrigin || url.origin !== scopedOrigin) return;
customConnect ??= {
...params.sslVerify === false ? { rejectUnauthorized: false } : {},
...params.clientCert ? { cert: fs.readFileSync(params.clientCert, "utf-8") } : {},
...params.clientKey ? { key: fs.readFileSync(params.clientKey, "utf-8") } : {}
};
return {
mode: "direct",
connect: customConnect
};
};
return async (url, init) => {
const request = resolveFetchRequest(url, init);
const guardedFetchOptions = {
url: request.url,
init: request.init,
fetchImpl: fetchWithUndiciGuard,
maxRedirects: MCP_HTTP_MAX_REDIRECTS,
...redirectPolicy === "reject" ? { rejectCrossOriginUnsafeRedirectReplay: true } : { allowCrossOriginUnsafeRedirectReplay: true },
auditContext: "mcp-http",
useEnvProxyForEligibleUrls: true,
beforeRequest: params.beforeRequest,
...request.signal ? { signal: request.signal } : {},
...params.timeoutMs !== void 0 ? { timeoutMs: params.timeoutMs } : {},
...policy ? { policy } : {},
...needsCustomDispatcher ? { resolveDispatcherPolicy: resolveCustomDispatcherPolicy } : {}
};
const guarded = await fetchWithSsrFGuard(guardedFetchOptions);
return await buildManagedMcpResponse(guarded.response, guarded.release, guarded.refreshTimeout);
};
}
/** Builds an MCP resource fetch with optional TLS/client-cert dispatcher support. */
function buildMcpHttpFetch(params) {
return buildMcpHttpFetchWithRedirectPolicy(params, "replay");
}
/** Builds an OAuth fetch with scoped resource headers and fail-closed redirect replay. */
function buildMcpOAuthHttpFetch(params) {
const { headers, ...fetchParams } = params;
return withSameOriginMcpHttpHeaders({
fetchFn: buildMcpHttpFetchWithRedirectPolicy(fetchParams, "reject"),
headers: withoutMcpAuthorizationHeader(headers),
resourceUrl: params.resourceUrl
});
}
/** Removes Authorization from MCP headers before forwarding to non-authorized paths. */
function withoutMcpAuthorizationHeader(headers) {
if (!headers) return;
const entries = Object.entries(headers).filter(([key]) => key.toLowerCase() !== "authorization");
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
}
/** Wraps MCP fetch so configured headers are applied only to the resource origin. */
function withSameOriginMcpHttpHeaders(params) {
if (!params.headers || Object.keys(params.headers).length === 0) return params.fetchFn;
const resourceOrigin = new URL(params.resourceUrl).origin;
return (url, init) => {
if (new URL(url).origin !== resourceOrigin) return params.fetchFn(url, init);
const headers = new Headers(params.headers);
for (const [key, value] of new Headers(init?.headers)) headers.set(key, value);
return params.fetchFn(url, {
...init,
headers
});
};
}
//#endregion
export { withoutMcpAuthorizationHeader as i, buildMcpOAuthHttpFetch as n, withSameOriginMcpHttpHeaders as r, buildMcpHttpFetch as t };