Propagate web-search cancellation
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
!Dockerfile
|
||||
!http-entrypoint.mjs
|
||||
!mcp-probe.mjs
|
||||
!patch-mcp-proxy.py
|
||||
!patch-mcp-web-search.mjs
|
||||
!overrides/
|
||||
!overrides/bing.js
|
||||
|
||||
@@ -3,6 +3,7 @@ FROM node:22-bookworm-slim@sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a596
|
||||
ARG MCP_WEB_SEARCH_VERSION=1.3.0
|
||||
ARG MCP_WEB_SEARCH_MAX_BYTES=52428800
|
||||
ARG MCP_PROXY_VERSION=0.12.0
|
||||
ARG MCP_PYTHON_SDK_VERSION=1.28.1
|
||||
|
||||
# Chromium intentionally tracks Debian security updates inside the pinned base
|
||||
# image family; Bing's browser path is more likely to break with stale Chromium
|
||||
@@ -15,8 +16,13 @@ RUN apt-get update \
|
||||
python3-venv \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY patch-mcp-proxy.py /tmp/patch-mcp-proxy.py
|
||||
|
||||
RUN python3 -m venv /opt/mcp-proxy \
|
||||
&& /opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}" \
|
||||
&& /opt/mcp-proxy/bin/pip install --no-cache-dir \
|
||||
"mcp-proxy==${MCP_PROXY_VERSION}" \
|
||||
"mcp==${MCP_PYTHON_SDK_VERSION}" \
|
||||
&& /opt/mcp-proxy/bin/python /tmp/patch-mcp-proxy.py \
|
||||
&& /opt/mcp-proxy/bin/mcp-proxy --version
|
||||
|
||||
COPY patch-mcp-web-search.mjs /tmp/patch-mcp-web-search.mjs
|
||||
@@ -35,7 +41,7 @@ RUN npm install -g "@zhafron/mcp-web-search@${MCP_WEB_SEARCH_VERSION}" \
|
||||
&& cp /tmp/context-kit-fetch/browser-fetch.js /usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/fetch/browser.js \
|
||||
&& cp /tmp/context-kit-fetch/bounds.mjs /usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/fetch/bounds.js \
|
||||
&& node /tmp/patch-mcp-web-search.mjs \
|
||||
&& rm -rf /tmp/patch-mcp-web-search.mjs /tmp/context-kit-bing-provider.js /tmp/context-kit-providers /tmp/context-kit-fetch \
|
||||
&& rm -rf /tmp/patch-mcp-proxy.py /tmp/patch-mcp-web-search.mjs /tmp/context-kit-bing-provider.js /tmp/context-kit-providers /tmp/context-kit-fetch \
|
||||
&& npm cache clean --force
|
||||
|
||||
RUN chmod -R a+rX /usr/local/lib/context-kit \
|
||||
|
||||
@@ -88,13 +88,22 @@ export function createSecureMcpServer({ upstream = defaultUpstream, probe = prob
|
||||
path: request.url,
|
||||
headers: copyRequestHeaders(request.headers, target.host)
|
||||
}, upstreamResponse => {
|
||||
upstreamResponse.on("error", () => response.destroy());
|
||||
response.writeHead(
|
||||
upstreamResponse.statusCode || 502,
|
||||
copyResponseHeaders(upstreamResponse.headers)
|
||||
);
|
||||
upstreamResponse.pipe(response);
|
||||
});
|
||||
const abortUpstream = () => {
|
||||
if (!upstreamRequest.destroyed) upstreamRequest.destroy(new Error("downstream disconnected"));
|
||||
};
|
||||
request.once("aborted", abortUpstream);
|
||||
response.once("close", () => {
|
||||
if (!response.writableEnded) abortUpstream();
|
||||
});
|
||||
upstreamRequest.on("error", error => {
|
||||
if (response.destroyed) return;
|
||||
if (!response.headersSent) response.writeHead(502, { "Content-Type": "text/plain" });
|
||||
response.end(`backend unavailable: ${error.message}`);
|
||||
});
|
||||
@@ -122,6 +131,23 @@ export function superviseBackend({ probe, intervalMs = 10000, onFailure }) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function terminateChild(child, { graceMs = 3000 } = {}) {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||
const gracefulExit = once(child, "exit").then(() => true);
|
||||
child.kill("SIGTERM");
|
||||
const exited = await Promise.race([
|
||||
gracefulExit,
|
||||
delay(graceMs).then(() => false)
|
||||
]);
|
||||
if (exited || child.exitCode !== null || child.signalCode !== null) return;
|
||||
|
||||
const forcedExit = once(child, "exit");
|
||||
if (!child.kill("SIGKILL") && child.exitCode === null && child.signalCode === null) {
|
||||
throw new Error("failed to terminate mcp-proxy child");
|
||||
}
|
||||
await forcedExit;
|
||||
}
|
||||
|
||||
async function waitForBackend(child, url) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 60; attempt += 1) {
|
||||
@@ -152,11 +178,7 @@ async function main() {
|
||||
stopSupervisor();
|
||||
server?.close();
|
||||
server?.closeAllConnections();
|
||||
if (child.exitCode === null) {
|
||||
child.kill("SIGTERM");
|
||||
await Promise.race([once(child, "exit"), delay(3000)]).catch(() => {});
|
||||
if (child.exitCode === null) child.kill("SIGKILL");
|
||||
}
|
||||
await terminateChild(child);
|
||||
process.exitCode = code;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { fileURLToPath } from "node:url";
|
||||
const protocolVersion = "2024-11-05";
|
||||
const expectedTools = ["fetch_url", "search_web"];
|
||||
|
||||
export async function rpc(url, id, method, params = {}, timeoutMs = 5000) {
|
||||
export async function rpc(url, id, method, params = {}, timeoutMs = 5000, signal) {
|
||||
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -12,7 +13,7 @@ export async function rpc(url, id, method, params = {}, timeoutMs = 5000) {
|
||||
"MCP-Protocol-Version": protocolVersion
|
||||
},
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
|
||||
signal: AbortSignal.timeout(timeoutMs)
|
||||
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`${method} returned HTTP ${response.status}: ${text.slice(0, 300)}`);
|
||||
|
||||
@@ -12,61 +12,69 @@ function responseHeaders(headers) {
|
||||
return record;
|
||||
}
|
||||
|
||||
export async function fetchBrowserResource(url, timeoutMs = HTTP_TIMEOUT) {
|
||||
export async function fetchBrowserResource(url, timeoutMs = HTTP_TIMEOUT, signal) {
|
||||
signal?.throwIfAborted();
|
||||
await assertSafeUrl(url);
|
||||
return browserPool.withBrowser(async browser => {
|
||||
const page = await browser.newPage();
|
||||
const devtools = await page.target().createCDPSession();
|
||||
await devtools.send("Network.enable");
|
||||
await devtools.send("Network.setBlockedURLs", {
|
||||
urls: ["ws://*", "wss://*", "file://*", "ftp://*"]
|
||||
});
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
const blockedTransport = name => class {
|
||||
constructor() {
|
||||
throw new DOMException(`${name} is disabled by the safe browser fetcher`, "SecurityError");
|
||||
}
|
||||
};
|
||||
for (const name of ["WebSocket", "WebTransport", "RTCPeerConnection", "webkitRTCPeerConnection"]) {
|
||||
if (name in globalThis) {
|
||||
Object.defineProperty(globalThis, name, {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: blockedTransport(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
await page.setBypassServiceWorker(true);
|
||||
let requests = 0;
|
||||
let totalBytes = 0;
|
||||
let blockedError;
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", request => {
|
||||
void (async () => {
|
||||
try {
|
||||
const requestUrl = new URL(request.url());
|
||||
if (!["http:", "https:"].includes(requestUrl.protocol)) throw new Error("unsupported browser request scheme");
|
||||
if (request.method() !== "GET") throw new Error("browser fetch blocks non-GET requests");
|
||||
requests += 1;
|
||||
if (requests > MAX_BROWSER_REQUESTS) throw new Error("browser request limit exceeded");
|
||||
await assertSafeUrl(requestUrl);
|
||||
const upstream = await fetchViaVettedAddress(requestUrl, timeoutMs);
|
||||
const body = Buffer.from(await upstream.arrayBuffer());
|
||||
totalBytes += body.byteLength;
|
||||
if (totalBytes > MAX_BROWSER_TOTAL_BYTES) throw new Error("browser byte limit exceeded");
|
||||
await request.respond({
|
||||
status: upstream.status,
|
||||
headers: responseHeaders(upstream.headers),
|
||||
body
|
||||
});
|
||||
} catch (error) {
|
||||
blockedError ||= error;
|
||||
await request.abort("blockedbyclient").catch(() => undefined);
|
||||
}
|
||||
})();
|
||||
});
|
||||
const pendingRequests = new Set();
|
||||
const abort = () => void page.close().catch(() => undefined);
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
const devtools = await page.target().createCDPSession();
|
||||
await devtools.send("Network.enable");
|
||||
await devtools.send("Network.setBlockedURLs", {
|
||||
urls: ["ws://*", "wss://*", "file://*", "ftp://*"]
|
||||
});
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
const blockedTransport = name => class {
|
||||
constructor() {
|
||||
throw new DOMException(`${name} is disabled by the safe browser fetcher`, "SecurityError");
|
||||
}
|
||||
};
|
||||
for (const name of ["WebSocket", "WebTransport", "RTCPeerConnection", "webkitRTCPeerConnection"]) {
|
||||
if (name in globalThis) {
|
||||
Object.defineProperty(globalThis, name, {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: blockedTransport(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
await page.setBypassServiceWorker(true);
|
||||
let requests = 0;
|
||||
let totalBytes = 0;
|
||||
let blockedError;
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", request => {
|
||||
const pending = (async () => {
|
||||
try {
|
||||
const requestUrl = new URL(request.url());
|
||||
if (!["http:", "https:"].includes(requestUrl.protocol)) throw new Error("unsupported browser request scheme");
|
||||
if (request.method() !== "GET") throw new Error("browser fetch blocks non-GET requests");
|
||||
requests += 1;
|
||||
if (requests > MAX_BROWSER_REQUESTS) throw new Error("browser request limit exceeded");
|
||||
await assertSafeUrl(requestUrl);
|
||||
const upstream = await fetchViaVettedAddress(requestUrl, timeoutMs, signal);
|
||||
const body = Buffer.from(await upstream.arrayBuffer());
|
||||
totalBytes += body.byteLength;
|
||||
if (totalBytes > MAX_BROWSER_TOTAL_BYTES) throw new Error("browser byte limit exceeded");
|
||||
await request.respond({
|
||||
status: upstream.status,
|
||||
headers: responseHeaders(upstream.headers),
|
||||
body
|
||||
});
|
||||
} catch (error) {
|
||||
blockedError ||= error;
|
||||
await request.abort("blockedbyclient").catch(() => undefined);
|
||||
}
|
||||
})();
|
||||
pendingRequests.add(pending);
|
||||
void pending.finally(() => pendingRequests.delete(pending));
|
||||
});
|
||||
signal?.throwIfAborted();
|
||||
const navigation = await page.goto(url.toString(), {
|
||||
waitUntil: "networkidle2",
|
||||
timeout: timeoutMs
|
||||
@@ -88,8 +96,13 @@ export async function fetchBrowserResource(url, timeoutMs = HTTP_TIMEOUT) {
|
||||
buffer,
|
||||
byteLength: buffer.byteLength
|
||||
};
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted();
|
||||
throw error;
|
||||
} finally {
|
||||
await page.close();
|
||||
signal?.removeEventListener("abort", abort);
|
||||
if (!page.isClosed()) await page.close();
|
||||
await Promise.allSettled(pendingRequests);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,24 +17,22 @@ export async function attemptProvider(provider, query, limit, lang, options = {}
|
||||
const timeoutMs = Math.max(10, Math.min(options.timeoutMs || DEFAULT_TIMEOUT_MS, 60_000));
|
||||
const now = options.now || (() => performance.now());
|
||||
const started = now();
|
||||
options.signal?.throwIfAborted();
|
||||
if (provider.configured === false) {
|
||||
return {
|
||||
items: [],
|
||||
diagnostic: { provider: provider.name, status: "unavailable", duration_ms: 0, result_count: 0 }
|
||||
};
|
||||
}
|
||||
let timer;
|
||||
const controller = new AbortController();
|
||||
const signal = options.signal
|
||||
? AbortSignal.any([options.signal, controller.signal])
|
||||
: controller.signal;
|
||||
const timer = setTimeout(() => {
|
||||
controller.abort(new Error(`provider timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
try {
|
||||
const items = await Promise.race([
|
||||
provider.search(query, limit, lang, controller.signal),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
controller.abort(new Error(`provider timed out after ${timeoutMs}ms`));
|
||||
reject(new Error(`provider timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
})
|
||||
]);
|
||||
const items = await provider.search(query, limit, lang, signal);
|
||||
const bounded = Array.isArray(items) ? items.slice(0, limit) : [];
|
||||
return {
|
||||
items: bounded,
|
||||
@@ -46,6 +44,7 @@ export async function attemptProvider(provider, query, limit, lang, options = {}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
options.signal?.throwIfAborted();
|
||||
return {
|
||||
items: [],
|
||||
diagnostic: {
|
||||
@@ -57,7 +56,6 @@ export async function attemptProvider(provider, query, limit, lang, options = {}
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
controller.abort();
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,19 @@ export class ProviderRegistry {
|
||||
return this.providers.get(name);
|
||||
}
|
||||
|
||||
async searchWithFallback(q, limit, lang, preferredProvider) {
|
||||
async searchWithFallback(q, limit, lang, preferredProvider, signal) {
|
||||
const defaultProvider = preferredProvider || DEFAULT_SEARCH_PROVIDER;
|
||||
const order = [defaultProvider, ...PROVIDERS.filter(name => name !== defaultProvider)].slice(0, MAX_PROVIDER_ATTEMPTS);
|
||||
const attempts = [];
|
||||
const started = performance.now();
|
||||
for (const providerName of order) {
|
||||
signal?.throwIfAborted();
|
||||
const provider = this.providers.get(providerName);
|
||||
if (!provider) continue;
|
||||
const attempt = await attemptProvider(provider, q, limit, lang, { timeoutMs: PROVIDER_TIMEOUT_MS });
|
||||
const attempt = await attemptProvider(provider, q, limit, lang, {
|
||||
timeoutMs: PROVIDER_TIMEOUT_MS,
|
||||
signal
|
||||
});
|
||||
attempts.push(attempt.diagnostic);
|
||||
if (attempt.items.length) {
|
||||
return {
|
||||
|
||||
168
docker/web-search/patch-mcp-proxy.py
Normal file
168
docker/web-search/patch-mcp-proxy.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
from importlib.metadata import version
|
||||
from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EXPECTED_VERSIONS = {
|
||||
"mcp-proxy": "0.12.0",
|
||||
"mcp": "1.28.1",
|
||||
}
|
||||
|
||||
|
||||
def module_path(name: str) -> Path:
|
||||
spec = find_spec(name)
|
||||
if spec is None or spec.origin is None:
|
||||
raise RuntimeError(f"module not found: {name}")
|
||||
return Path(spec.origin)
|
||||
|
||||
|
||||
def replace_once(path: Path, before: str, after: str) -> None:
|
||||
source = path.read_text()
|
||||
count = source.count(before)
|
||||
if count != 1:
|
||||
raise RuntimeError(f"expected one patch target in {path}, found {count}: {before}")
|
||||
path.write_text(source.replace(before, after))
|
||||
|
||||
|
||||
for package, expected in EXPECTED_VERSIONS.items():
|
||||
actual = version(package)
|
||||
if actual != expected:
|
||||
raise RuntimeError(f"expected {package} {expected}, found {actual}")
|
||||
|
||||
|
||||
proxy_path = module_path("mcp_proxy.proxy_server")
|
||||
replace_once(
|
||||
proxy_path,
|
||||
"import logging\nimport typing as t\n\nfrom mcp import server, types",
|
||||
"import logging\nimport typing as t\n\nimport anyio\n\nfrom mcp import server, types",
|
||||
)
|
||||
replace_once(
|
||||
proxy_path,
|
||||
""" result = await remote_app.call_tool(
|
||||
req.params.name,
|
||||
(req.params.arguments or {}),
|
||||
meta=meta_dict,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
""",
|
||||
""" completed = anyio.Event()
|
||||
disconnected = False
|
||||
downstream_request = request_ctx.get().request
|
||||
|
||||
async def watch_downstream_disconnect() -> None:
|
||||
nonlocal disconnected
|
||||
while not completed.is_set():
|
||||
if await downstream_request.is_disconnected():
|
||||
disconnected = True
|
||||
task_group.cancel_scope.cancel()
|
||||
return
|
||||
await anyio.sleep(0.05)
|
||||
|
||||
async with anyio.create_task_group() as task_group:
|
||||
if downstream_request is not None and hasattr(downstream_request, "is_disconnected"):
|
||||
task_group.start_soon(watch_downstream_disconnect)
|
||||
try:
|
||||
result = await remote_app.call_tool(
|
||||
req.params.name,
|
||||
(req.params.arguments or {}),
|
||||
meta=meta_dict,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
finally:
|
||||
completed.set()
|
||||
task_group.cancel_scope.cancel()
|
||||
|
||||
if disconnected:
|
||||
raise ConnectionError("downstream client disconnected")
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
session_path = module_path("mcp.shared.session")
|
||||
replace_once(
|
||||
session_path,
|
||||
""" finally:
|
||||
self._response_streams.pop(request_id, None)
|
||||
self._progress_callbacks.pop(request_id, None)
|
||||
""",
|
||||
""" except anyio.get_cancelled_exc_class():
|
||||
# Context Kit: forward cancellation before abandoning the remote request.
|
||||
with anyio.move_on_after(1, shield=True):
|
||||
try:
|
||||
await self.send_notification(
|
||||
CancelledNotification(
|
||||
params={"requestId": request_id, "reason": "upstream request cancelled"}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
self._response_streams.pop(request_id, None)
|
||||
self._progress_callbacks.pop(request_id, None)
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
streamable_http_path = module_path("mcp.client.streamable_http")
|
||||
replace_once(
|
||||
streamable_http_path,
|
||||
""" self.url = url
|
||||
self.session_id = None
|
||||
self.protocol_version = None
|
||||
""",
|
||||
""" self.url = url
|
||||
self.session_id = None
|
||||
self.protocol_version = None
|
||||
self._request_cancel_scopes: dict[RequestId, anyio.CancelScope] = {}
|
||||
""",
|
||||
)
|
||||
replace_once(
|
||||
streamable_http_path,
|
||||
""" async def handle_request_async():
|
||||
if is_resumption:
|
||||
await self._handle_resumption_request(ctx)
|
||||
else:
|
||||
await self._handle_post_request(ctx)
|
||||
|
||||
# If this is a request, start a new task to handle it
|
||||
if isinstance(message.root, JSONRPCRequest):
|
||||
tg.start_soon(handle_request_async)
|
||||
else:
|
||||
await handle_request_async()
|
||||
""",
|
||||
""" async def handle_request_async(
|
||||
request_context: RequestContext = ctx,
|
||||
resume: bool = is_resumption,
|
||||
) -> None:
|
||||
root = request_context.session_message.message.root
|
||||
request_id = root.id if isinstance(root, JSONRPCRequest) else None
|
||||
with anyio.CancelScope() as request_scope:
|
||||
if request_id is not None:
|
||||
self._request_cancel_scopes[request_id] = request_scope
|
||||
try:
|
||||
if resume:
|
||||
await self._handle_resumption_request(request_context)
|
||||
else:
|
||||
await self._handle_post_request(request_context)
|
||||
finally:
|
||||
if self._request_cancel_scopes.get(request_id) is request_scope:
|
||||
self._request_cancel_scopes.pop(request_id, None)
|
||||
|
||||
# If this is a request, start a new task to handle it
|
||||
if isinstance(message.root, JSONRPCRequest):
|
||||
tg.start_soon(handle_request_async)
|
||||
else:
|
||||
if (
|
||||
isinstance(message.root, JSONRPCNotification)
|
||||
and message.root.method == "notifications/cancelled"
|
||||
):
|
||||
request_scope = self._request_cancel_scopes.get(
|
||||
(message.root.params or {}).get("requestId")
|
||||
)
|
||||
if request_scope is not None:
|
||||
request_scope.cancel()
|
||||
await handle_request_async()
|
||||
""",
|
||||
)
|
||||
@@ -23,6 +23,22 @@ const replacements = [
|
||||
[
|
||||
"Search the web using multiple providers (DuckDuckGo, Bing, SearXNG). Automatically falls back to other providers if the default fails. No API keys required for DuckDuckGo and SearXNG.",
|
||||
"Search the web with bounded provider fallback and per-attempt diagnostics. SearXNG is local; Brave is available when BRAVE_SEARCH_API_KEY is configured."
|
||||
],
|
||||
[
|
||||
'}, async ({ q, limit = DEFAULT_LIMIT, lang = "en", provider }) => {',
|
||||
'}, async ({ q, limit = DEFAULT_LIMIT, lang = "en", provider }, { signal }) => {'
|
||||
],
|
||||
[
|
||||
"providerRegistry.searchWithFallback(q, Math.min(Math.max(1, limit), 50), lang, provider)",
|
||||
"providerRegistry.searchWithFallback(q, Math.min(Math.max(1, limit), 50), lang, provider, signal)"
|
||||
],
|
||||
[
|
||||
'}, async ({ url, format, max_length, start_index, engine, include_links, include_media, include_metadata, include_comments, comment_limit, comment_sort, max_depth, timeout_ms, fresh, download, download_dir, download_ttl_seconds, max_download_bytes }) => {',
|
||||
'}, async ({ url, format, max_length, start_index, engine, include_links, include_media, include_metadata, include_comments, comment_limit, comment_sort, max_depth, timeout_ms, fresh, download, download_dir, download_ttl_seconds, max_download_bytes }, { signal }) => {'
|
||||
],
|
||||
[
|
||||
" max_download_bytes\n });",
|
||||
" max_download_bytes,\n signal\n });"
|
||||
]
|
||||
];
|
||||
|
||||
@@ -39,7 +55,13 @@ const httpPath = "/usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/f
|
||||
let httpSource = fs.readFileSync(httpPath, "utf8");
|
||||
const privateTransport = "async function fetchViaVettedAddress(url, timeoutMs)";
|
||||
if (!httpSource.includes(privateTransport)) throw new Error(`mcp-web-search patch target not found: ${privateTransport}`);
|
||||
httpSource = httpSource.replace(privateTransport, "export async function fetchViaVettedAddress(url, timeoutMs)");
|
||||
httpSource = httpSource.replace(privateTransport, "export async function fetchViaVettedAddress(url, timeoutMs, signal)");
|
||||
const requestTimeout = " timeout: timeoutMs\n }, response => {";
|
||||
if (!httpSource.includes(requestTimeout)) throw new Error(`mcp-web-search patch target not found: ${requestTimeout}`);
|
||||
httpSource = httpSource.replace(requestTimeout, " timeout: timeoutMs,\n signal\n }, response => {");
|
||||
const transportCall = "response = await transport(currentUrl, timeoutMs);";
|
||||
if (!httpSource.includes(transportCall)) throw new Error(`mcp-web-search patch target not found: ${transportCall}`);
|
||||
httpSource = httpSource.replace(transportCall, "response = await transport(currentUrl, timeoutMs, options?.signal);");
|
||||
fs.writeFileSync(httpPath, httpSource);
|
||||
|
||||
const utilityHttpPath = "/usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/utils/http.js";
|
||||
@@ -59,17 +81,20 @@ const extractReplacements = [
|
||||
],
|
||||
[
|
||||
"fetchCache.set(cacheKey, siteResult);\n return siteResult;",
|
||||
"const boundedSiteResult = boundFetchCollections(siteResult);\n fetchCache.set(cacheKey, boundedSiteResult);\n return boundedSiteResult;"
|
||||
"const boundedSiteResult = boundFetchCollections(siteResult);\n options?.signal?.throwIfAborted();\n fetchCache.set(cacheKey, boundedSiteResult);\n return boundedSiteResult;"
|
||||
],
|
||||
[
|
||||
"const resource = await fetchResource(parsedUrl, options?.timeout_ms, transport, options);",
|
||||
'const resource = options?.engine === "browser"\n ? await fetchBrowserResource(parsedUrl, options?.timeout_ms)\n : await fetchResource(parsedUrl, options?.timeout_ms, transport, options);'
|
||||
'const resource = options?.engine === "browser"\n ? await fetchBrowserResource(parsedUrl, options?.timeout_ms, options?.signal)\n : await fetchResource(parsedUrl, options?.timeout_ms, transport, options);\n options?.signal?.throwIfAborted();'
|
||||
],
|
||||
[
|
||||
"fetchCache.set(cacheKey, result);\n return result;",
|
||||
"result = boundFetchCollections(result);\n fetchCache.set(cacheKey, result);\n return result;"
|
||||
"result = boundFetchCollections(result);\n options?.signal?.throwIfAborted();\n fetchCache.set(cacheKey, result);\n return result;"
|
||||
]
|
||||
];
|
||||
const fetchStart = "export async function fetchAndExtract(url, options, transport) {\n const parsedUrl = new URL(url);";
|
||||
if (!extractSource.includes(fetchStart)) throw new Error(`mcp-web-search extract patch target not found: ${fetchStart}`);
|
||||
extractSource = extractSource.replace(fetchStart, "export async function fetchAndExtract(url, options, transport) {\n options?.signal?.throwIfAborted();\n const parsedUrl = new URL(url);");
|
||||
for (const [before, after] of extractReplacements) {
|
||||
if (!extractSource.includes(before)) throw new Error(`mcp-web-search extract patch target not found: ${before}`);
|
||||
extractSource = extractSource.replace(before, after);
|
||||
|
||||
Reference in New Issue
Block a user