ruflo 3.71.3 → 3.71.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/mcp-bridge/test-local-browser-guard.mjs +13 -1
- package/src/ruvocal/mcp-bridge/index.js +38 -0
- package/src/ruvocal/src/lib/server/mcp/clientPool.ts +0 -0
- package/src/ruvocal/src/lib/server/mcp/serverTrust.ts +25 -0
- package/src/ruvocal/src/lib/server/mcp/tools.ts +3 -0
- package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +2 -2
- package/src/ruvocal/src/lib/server/urlSafety.spec.ts +39 -0
- package/src/ruvocal/src/lib/server/urlSafety.ts +57 -6
- package/src/ruvocal/src/routes/api/fetch-url/+server.ts +2 -33
- package/src/ruvocal/src/routes/api/mcp/health/+server.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruflo",
|
|
3
|
-
"version": "3.71.
|
|
3
|
+
"version": "3.71.4",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration platform. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "bin/ruflo.js",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"package:rvf": "bash src/scripts/package-rvf.sh"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@claude-flow/cli": "3.71.
|
|
43
|
+
"@claude-flow/cli": "3.71.4"
|
|
44
44
|
},
|
|
45
45
|
"optionalDependencies": {},
|
|
46
46
|
"overrides": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* G6. an Origin listed in MCP_CORS_ORIGIN is still accepted
|
|
12
12
|
* G7. with MCP_AUTH_TOKEN set the guard is off and bearer auth decides
|
|
13
13
|
*
|
|
14
|
+
* Runs against both bridges (src/mcp-bridge and src/ruvocal/mcp-bridge).
|
|
15
|
+
*
|
|
14
16
|
* node test-local-browser-guard.mjs
|
|
15
17
|
*/
|
|
16
18
|
import { spawn } from "node:child_process";
|
|
@@ -19,7 +21,13 @@ import path from "node:path";
|
|
|
19
21
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
20
22
|
import { fileURLToPath } from "node:url";
|
|
21
23
|
|
|
22
|
-
const
|
|
24
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
// Both bridges carry the same guard; ruvocal's copy missed #4226 until it was ported.
|
|
26
|
+
const ENTRIES = [
|
|
27
|
+
{ label: "src/mcp-bridge", entry: path.join(HERE, "index.js") },
|
|
28
|
+
{ label: "src/ruvocal/mcp-bridge", entry: path.join(HERE, "..", "ruvocal", "mcp-bridge", "index.js") },
|
|
29
|
+
];
|
|
30
|
+
let ENTRY = ENTRIES[0].entry;
|
|
23
31
|
const QUIET = {
|
|
24
32
|
MCP_GROUP_INTELLIGENCE: "false",
|
|
25
33
|
MCP_GROUP_AGENTS: "false",
|
|
@@ -64,6 +72,9 @@ function request(port, { method = "POST", path: p = "/mcp", headers = {}, body }
|
|
|
64
72
|
const initialize = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
|
|
65
73
|
const json = { "Content-Type": "application/json" };
|
|
66
74
|
|
|
75
|
+
for (const { label, entry } of ENTRIES) {
|
|
76
|
+
ENTRY = entry;
|
|
77
|
+
console.log(`\n[${label}]`);
|
|
67
78
|
console.log("local-only mode (no MCP_AUTH_TOKEN)");
|
|
68
79
|
await withBridge({ MCP_CORS_ORIGIN: "" }, async (port) => {
|
|
69
80
|
check((await request(port, { headers: json, body: initialize })).status === 200, "G1 native client reaches /mcp");
|
|
@@ -82,6 +93,7 @@ await withBridge({ MCP_AUTH_TOKEN: "t0ken" }, async (port) => {
|
|
|
82
93
|
const res = await request(port, { headers: { ...json, Origin: "https://example.com", Authorization: "Bearer t0ken" }, body: initialize });
|
|
83
94
|
check(res.status === 200, "G7 bearer token decides when set");
|
|
84
95
|
});
|
|
96
|
+
}
|
|
85
97
|
|
|
86
98
|
console.log(failed ? `\n${failed} check(s) failed` : "\nall checks passed");
|
|
87
99
|
process.exit(failed ? 1 : 0);
|
|
@@ -1069,6 +1069,7 @@ app.use((req, res, next) => {
|
|
|
1069
1069
|
const CORS_ALLOWLIST = (process.env.MCP_CORS_ORIGIN || "*")
|
|
1070
1070
|
.split(",").map(s => s.trim()).filter(Boolean);
|
|
1071
1071
|
const CORS_WILDCARD = CORS_ALLOWLIST.length === 1 && CORS_ALLOWLIST[0] === "*";
|
|
1072
|
+
app.use((req, res, next) => localBrowserGuard(req, res, next));
|
|
1072
1073
|
app.use((req, res, next) => {
|
|
1073
1074
|
const origin = req.get("origin") || "";
|
|
1074
1075
|
if (CORS_WILDCARD) {
|
|
@@ -1087,6 +1088,43 @@ app.use((req, res, next) => {
|
|
|
1087
1088
|
// ---------- Auth middleware ----------
|
|
1088
1089
|
// No-op in local-only mode (MCP_AUTH_TOKEN unset). Enforces 401 when token is set.
|
|
1089
1090
|
const MCP_TOKEN = process.env.MCP_AUTH_TOKEN || "";
|
|
1091
|
+
|
|
1092
|
+
// ---------- Local-only browser guard (#4226; same guard as ruflo/src/mcp-bridge) ----------
|
|
1093
|
+
// Binding to loopback does not keep web pages out: any page the user opens
|
|
1094
|
+
// can send requests to 127.0.0.1, and a DNS-rebinding page can even make the
|
|
1095
|
+
// request look same-origin. CORS only decides whether the page may READ the
|
|
1096
|
+
// reply; a "simple" cross-origin POST runs without a preflight. So when there
|
|
1097
|
+
// is no MCP_AUTH_TOKEN, refuse requests that a browser sent on behalf of a
|
|
1098
|
+
// foreign page:
|
|
1099
|
+
// - the Host header must name a loopback address (stops DNS rebinding);
|
|
1100
|
+
// - a request carrying an Origin header must come from a loopback origin or
|
|
1101
|
+
// from an origin listed explicitly in MCP_CORS_ORIGIN.
|
|
1102
|
+
// Native MCP clients (CLI, IDE, curl) send no Origin and a loopback Host.
|
|
1103
|
+
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
1104
|
+
function hostnameOf(value) {
|
|
1105
|
+
try {
|
|
1106
|
+
return new URL(value.includes("://") ? value : `http://${value}`).hostname.toLowerCase();
|
|
1107
|
+
} catch {
|
|
1108
|
+
return "";
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function isLoopbackHostname(hostname) {
|
|
1112
|
+
return LOOPBACK_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost");
|
|
1113
|
+
}
|
|
1114
|
+
function localBrowserGuard(req, res, next) {
|
|
1115
|
+
if (MCP_TOKEN) return next();
|
|
1116
|
+
if (!isLoopbackHostname(hostnameOf(req.get("host") || ""))) {
|
|
1117
|
+
return res.status(403).json({ error: "forbidden host" });
|
|
1118
|
+
}
|
|
1119
|
+
const origin = req.get("origin");
|
|
1120
|
+
if (origin !== undefined) {
|
|
1121
|
+
const explicitlyAllowed = !CORS_WILDCARD && CORS_ALLOWLIST.includes(origin);
|
|
1122
|
+
if (!explicitlyAllowed && !isLoopbackHostname(hostnameOf(origin))) {
|
|
1123
|
+
return res.status(403).json({ error: "forbidden origin" });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
next();
|
|
1127
|
+
}
|
|
1090
1128
|
function requireAuth(req, res, next) {
|
|
1091
1129
|
if (req.path === "/health") return next();
|
|
1092
1130
|
if (!MCP_TOKEN) return next();
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isValidUrl, ssrfSafeFetch } from "$lib/server/urlSafety";
|
|
2
|
+
import { getMcpServers } from "./registry";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An MCP server URL the operator configured (MCP_SERVERS). Only these may use
|
|
6
|
+
* loopback, Docker-internal or other private addresses; a URL supplied in a
|
|
7
|
+
* request — including by an anonymous chat session — must be public.
|
|
8
|
+
*/
|
|
9
|
+
export function isConfiguredMcpUrl(url: string): boolean {
|
|
10
|
+
return getMcpServers().some((s) => s.url === url);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Whether an MCP server URL may be contacted at all. */
|
|
14
|
+
export function isAllowedMcpUrl(url: string): boolean {
|
|
15
|
+
return isValidUrl(url, { allowPrivate: isConfiguredMcpUrl(url) });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The fetch an MCP transport should use: the default for operator-configured
|
|
20
|
+
* servers, and one pinned to public addresses at connect time for everything
|
|
21
|
+
* else, so a public hostname that resolves to a private address is refused.
|
|
22
|
+
*/
|
|
23
|
+
export function mcpFetchFor(url: string): typeof fetch | undefined {
|
|
24
|
+
return isConfiguredMcpUrl(url) ? undefined : ssrfSafeFetch;
|
|
25
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client";
|
|
|
2
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
3
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
4
|
import type { McpServerConfig } from "./httpClient";
|
|
5
|
+
import { mcpFetchFor } from "./serverTrust";
|
|
5
6
|
import { logger } from "$lib/server/logger";
|
|
6
7
|
// use console.* for lightweight diagnostics in production logs
|
|
7
8
|
|
|
@@ -71,11 +72,13 @@ async function listServerTools(
|
|
|
71
72
|
try {
|
|
72
73
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
73
74
|
requestInit: { headers: server.headers, signal: opts.signal },
|
|
75
|
+
fetch: mcpFetchFor(server.url),
|
|
74
76
|
});
|
|
75
77
|
await client.connect(transport);
|
|
76
78
|
} catch {
|
|
77
79
|
const transport = new SSEClientTransport(url, {
|
|
78
80
|
requestInit: { headers: server.headers, signal: opts.signal },
|
|
81
|
+
fetch: mcpFetchFor(server.url),
|
|
79
82
|
});
|
|
80
83
|
await client.connect(transport);
|
|
81
84
|
}
|
|
@@ -2,7 +2,7 @@ import { config } from "$lib/server/config";
|
|
|
2
2
|
import { mergeMcpServers } from "./mergeServers";
|
|
3
3
|
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
|
|
4
4
|
import { getMcpServers } from "$lib/server/mcp/registry";
|
|
5
|
-
import {
|
|
5
|
+
import { isAllowedMcpUrl } from "$lib/server/mcp/serverTrust";
|
|
6
6
|
import { resetMcpToolsCache } from "$lib/server/mcp/tools";
|
|
7
7
|
import { getOpenAiToolsForMcp } from "$lib/server/mcp/tools";
|
|
8
8
|
import type {
|
|
@@ -144,7 +144,7 @@ export async function* runMcpFlow({
|
|
|
144
144
|
const before = servers.slice();
|
|
145
145
|
servers = servers.filter((s) => {
|
|
146
146
|
try {
|
|
147
|
-
return
|
|
147
|
+
return isAllowedMcpUrl(s.url);
|
|
148
148
|
} catch {
|
|
149
149
|
return false;
|
|
150
150
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isValidUrl } from "./urlSafety";
|
|
3
|
+
|
|
4
|
+
// URLs from a request (custom MCP servers, fetch-url) must be public HTTPS.
|
|
5
|
+
// The local-development allowances apply only to operator-configured URLs.
|
|
6
|
+
describe("isValidUrl", () => {
|
|
7
|
+
it("accepts public HTTPS URLs", () => {
|
|
8
|
+
expect(isValidUrl("https://mcp.example.com/mcp")).toBe(true);
|
|
9
|
+
expect(isValidUrl("https://8.8.8.8/")).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("refuses loopback, Docker-internal, dotless and private targets by default", () => {
|
|
13
|
+
for (const url of [
|
|
14
|
+
"http://localhost:3001/mcp",
|
|
15
|
+
"https://localhost/mcp",
|
|
16
|
+
"http://127.0.0.1:3001/mcp",
|
|
17
|
+
"https://[::1]/",
|
|
18
|
+
"http://host.docker.internal:3001/mcp",
|
|
19
|
+
"http://mongodb:27017/",
|
|
20
|
+
"http://metadata/computeMetadata/v1/",
|
|
21
|
+
"https://metadata/",
|
|
22
|
+
"https://10.0.0.5/",
|
|
23
|
+
"https://192.168.1.10/",
|
|
24
|
+
"https://169.254.169.254/latest/meta-data/",
|
|
25
|
+
"https://[fd00::1]/",
|
|
26
|
+
"https://bridge.localhost/",
|
|
27
|
+
"https://metadata.google.internal/",
|
|
28
|
+
"http://mcp.example.com/mcp",
|
|
29
|
+
]) {
|
|
30
|
+
expect(isValidUrl(url), url).toBe(false);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps the local allowances for operator-configured URLs", () => {
|
|
35
|
+
expect(isValidUrl("http://localhost:3001/mcp", { allowPrivate: true })).toBe(true);
|
|
36
|
+
expect(isValidUrl("http://mcp-bridge:3001/mcp", { allowPrivate: true })).toBe(true);
|
|
37
|
+
expect(isValidUrl("https://10.0.0.5/", { allowPrivate: true })).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Address4, Address6 } from "ip-address";
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
|
+
import dns from "node:dns";
|
|
4
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
3
5
|
|
|
4
6
|
const UNSAFE_IPV4_SUBNETS = [
|
|
5
7
|
"0.0.0.0/8",
|
|
8
|
+
"10.0.0.0/8",
|
|
6
9
|
"100.64.0.0/10",
|
|
7
10
|
"127.0.0.0/8",
|
|
8
11
|
"169.254.0.0/16",
|
|
@@ -25,30 +28,41 @@ function isUnsafeIp(address: string): boolean {
|
|
|
25
28
|
const v4 = addr.to4();
|
|
26
29
|
return UNSAFE_IPV4_SUBNETS.some((subnet) => v4.isInSubnet(subnet));
|
|
27
30
|
}
|
|
28
|
-
|
|
31
|
+
// Unique-local (fc00::/7) is private address space, like 10/8 for IPv4.
|
|
32
|
+
const ula = addr.isInSubnet(new Address6("fc00::/7"));
|
|
33
|
+
return addr.isLoopback() || addr.isLinkLocal() || ula || cleanIsUnspecified(address);
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
return true; // Unknown format → block
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
function cleanIsUnspecified(address: string): boolean {
|
|
40
|
+
return /^(::|0:0:0:0:0:0:0:0)$/.test(address);
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
/**
|
|
35
44
|
* Synchronous URL validation: checks protocol and hostname string.
|
|
45
|
+
*
|
|
46
|
+
* By default only public HTTPS URLs pass: no loopback or Docker-internal
|
|
47
|
+
* names, no dotless hosts, no private IP literals. `allowPrivate` restores the
|
|
48
|
+
* local-development allowances and is only for URLs the operator configured
|
|
49
|
+
* (MCP_SERVERS), never for URLs taken from a request.
|
|
36
50
|
*/
|
|
37
|
-
export function isValidUrl(urlString: string): boolean {
|
|
51
|
+
export function isValidUrl(urlString: string, opts: { allowPrivate?: boolean } = {}): boolean {
|
|
38
52
|
try {
|
|
39
53
|
const url = new URL(urlString.trim());
|
|
40
54
|
const hostname = url.hostname.toLowerCase();
|
|
41
55
|
// Allow HTTP for localhost/loopback/Docker-internal (dev & local MCP bridge)
|
|
42
|
-
if (
|
|
56
|
+
if (opts.allowPrivate && (
|
|
43
57
|
hostname === "localhost" ||
|
|
44
58
|
hostname === "127.0.0.1" ||
|
|
45
59
|
hostname === "::1" ||
|
|
46
60
|
hostname === "host.docker.internal"
|
|
47
|
-
) {
|
|
61
|
+
)) {
|
|
48
62
|
return url.protocol === "http:" || url.protocol === "https:";
|
|
49
63
|
}
|
|
50
64
|
// Allow HTTP for Docker-internal service names (no dots = private network)
|
|
51
|
-
if (!hostname.includes(".") && url.protocol === "http:") {
|
|
65
|
+
if (opts.allowPrivate && !hostname.includes(".") && url.protocol === "http:") {
|
|
52
66
|
return true;
|
|
53
67
|
}
|
|
54
68
|
if (url.protocol !== "https:") {
|
|
@@ -57,7 +71,10 @@ export function isValidUrl(urlString: string): boolean {
|
|
|
57
71
|
// If the hostname is a raw IP literal, validate it
|
|
58
72
|
const cleanHostname = hostname.replace(/^\[|]$/g, "");
|
|
59
73
|
if (isIP(cleanHostname)) {
|
|
60
|
-
return !isUnsafeIp(cleanHostname);
|
|
74
|
+
return opts.allowPrivate || !isUnsafeIp(cleanHostname);
|
|
75
|
+
}
|
|
76
|
+
if (!opts.allowPrivate && (!hostname.includes(".") || hostname.endsWith(".localhost") || hostname.endsWith(".internal") || hostname.endsWith(".local"))) {
|
|
77
|
+
return false;
|
|
61
78
|
}
|
|
62
79
|
return true;
|
|
63
80
|
} catch {
|
|
@@ -75,3 +92,37 @@ export function assertSafeIp(address: string, hostname: string): void {
|
|
|
75
92
|
throw new Error(`Resolved IP for ${hostname} is internal (${address})`);
|
|
76
93
|
}
|
|
77
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Undici dispatcher that validates resolved IPs at connection time,
|
|
98
|
+
* preventing TOCTOU DNS rebinding attacks.
|
|
99
|
+
*/
|
|
100
|
+
export const ssrfSafeAgent = new Agent({
|
|
101
|
+
connect: {
|
|
102
|
+
lookup: (hostname, options, callback) => {
|
|
103
|
+
dns.lookup(hostname, options, (err, address, family) => {
|
|
104
|
+
if (err) return callback(err, "", 4);
|
|
105
|
+
if (typeof address === "string") {
|
|
106
|
+
try {
|
|
107
|
+
assertSafeIp(address, hostname);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return callback(e as Error, "", 4);
|
|
110
|
+
}
|
|
111
|
+
} else if (Array.isArray(address)) {
|
|
112
|
+
for (const entry of address) {
|
|
113
|
+
try {
|
|
114
|
+
assertSafeIp(entry.address, hostname);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return callback(e as Error, "", 4);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return callback(null, address, family);
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** fetch() whose connections may only reach public addresses (see ssrfSafeAgent). */
|
|
127
|
+
export const ssrfSafeFetch = ((input: string | URL, init?: RequestInit) =>
|
|
128
|
+
undiciFetch(input, { ...(init as object), dispatcher: ssrfSafeAgent } as Parameters<typeof undiciFetch>[1])) as unknown as typeof fetch;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { error } from "@sveltejs/kit";
|
|
2
2
|
import { logger } from "$lib/server/logger.js";
|
|
3
|
-
import {
|
|
4
|
-
import { isValidUrl,
|
|
5
|
-
import dns from "node:dns";
|
|
3
|
+
import { fetch } from "undici";
|
|
4
|
+
import { isValidUrl, ssrfSafeAgent } from "$lib/server/urlSafety";
|
|
6
5
|
|
|
7
6
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
8
7
|
const FETCH_TIMEOUT = 30000; // 30 seconds
|
|
@@ -16,36 +15,6 @@ const SECURITY_HEADERS: HeadersInit = {
|
|
|
16
15
|
"Referrer-Policy": "no-referrer",
|
|
17
16
|
};
|
|
18
17
|
|
|
19
|
-
/**
|
|
20
|
-
* Undici dispatcher that validates resolved IPs at connection time,
|
|
21
|
-
* preventing TOCTOU DNS rebinding attacks.
|
|
22
|
-
*/
|
|
23
|
-
const ssrfSafeAgent = new Agent({
|
|
24
|
-
connect: {
|
|
25
|
-
lookup: (hostname, options, callback) => {
|
|
26
|
-
dns.lookup(hostname, options, (err, address, family) => {
|
|
27
|
-
if (err) return callback(err, "", 4);
|
|
28
|
-
if (typeof address === "string") {
|
|
29
|
-
try {
|
|
30
|
-
assertSafeIp(address, hostname);
|
|
31
|
-
} catch (e) {
|
|
32
|
-
return callback(e as Error, "", 4);
|
|
33
|
-
}
|
|
34
|
-
} else if (Array.isArray(address)) {
|
|
35
|
-
for (const entry of address) {
|
|
36
|
-
try {
|
|
37
|
-
assertSafeIp(entry.address, hostname);
|
|
38
|
-
} catch (e) {
|
|
39
|
-
return callback(e as Error, "", 4);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return callback(null, address, family);
|
|
44
|
-
});
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
|
|
49
18
|
export async function GET({ url }) {
|
|
50
19
|
const targetUrl = url.searchParams.get("url");
|
|
51
20
|
|
|
@@ -5,7 +5,7 @@ import type { KeyValuePair } from "$lib/types/Tool";
|
|
|
5
5
|
import { config } from "$lib/server/config";
|
|
6
6
|
import { logger } from "$lib/server/logger";
|
|
7
7
|
import type { RequestHandler } from "./$types";
|
|
8
|
-
import {
|
|
8
|
+
import { isAllowedMcpUrl, mcpFetchFor } from "$lib/server/mcp/serverTrust";
|
|
9
9
|
import { isStrictHfMcpLogin, hasNonEmptyToken, isExaMcpServer } from "$lib/server/mcp/hf";
|
|
10
10
|
|
|
11
11
|
interface HealthCheckRequest {
|
|
@@ -40,7 +40,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|
|
40
40
|
|
|
41
41
|
// URL validation handled above
|
|
42
42
|
|
|
43
|
-
if (!
|
|
43
|
+
if (!isAllowedMcpUrl(url)) {
|
|
44
44
|
return new Response(
|
|
45
45
|
JSON.stringify({
|
|
46
46
|
ready: false,
|
|
@@ -111,7 +111,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|
|
111
111
|
version: "1.0.0",
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
const transport = new StreamableHTTPClientTransport(baseUrl, { requestInit });
|
|
114
|
+
const transport = new StreamableHTTPClientTransport(baseUrl, { requestInit, fetch: mcpFetchFor(url) });
|
|
115
115
|
logger.info({}, `[MCP Health] Connecting to ${url}...`);
|
|
116
116
|
await client.connect(transport);
|
|
117
117
|
logger.info({}, `[MCP Health] Connected successfully via HTTP`);
|
|
@@ -174,7 +174,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|
|
174
174
|
version: "1.0.0",
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
const sseTransport = new SSEClientTransport(baseUrl, { requestInit });
|
|
177
|
+
const sseTransport = new SSEClientTransport(baseUrl, { requestInit, fetch: mcpFetchFor(url) });
|
|
178
178
|
logger.info({}, `[MCP Health] Connecting via SSE...`);
|
|
179
179
|
await client.connect(sseTransport);
|
|
180
180
|
logger.info({}, `[MCP Health] Connected successfully via SSE`);
|