sovr-mcp-proxy 2.0.0 → 2.1.0
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.
Potentially problematic release.
This version of sovr-mcp-proxy might be problematic. Click here for more details.
- package/dist/index.d.ts +17 -2
- package/dist/index.js +301 -17
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,15 @@ interface PolicyRule {
|
|
|
49
49
|
priority: number;
|
|
50
50
|
enabled: boolean;
|
|
51
51
|
}
|
|
52
|
+
/** Request payload for the evaluate() function */
|
|
53
|
+
interface EvalRequest {
|
|
54
|
+
channel: Channel;
|
|
55
|
+
action: string;
|
|
56
|
+
resource: string;
|
|
57
|
+
context?: McpContext;
|
|
58
|
+
}
|
|
59
|
+
/** Context object passed to policy evaluation */
|
|
60
|
+
type McpContext = Record<string, unknown>;
|
|
52
61
|
interface EvalResult {
|
|
53
62
|
verdict: Verdict;
|
|
54
63
|
risk_score: number;
|
|
@@ -71,10 +80,16 @@ interface AuditEntry {
|
|
|
71
80
|
}
|
|
72
81
|
declare let rules: PolicyRule[];
|
|
73
82
|
declare const auditLog: AuditEntry[];
|
|
74
|
-
declare const VERSION = "2.
|
|
83
|
+
declare const VERSION = "2.1.0";
|
|
84
|
+
type DownstreamTransport = "stdio" | "sse" | "streamable-http";
|
|
75
85
|
interface DownstreamServer {
|
|
76
86
|
name: string;
|
|
87
|
+
transportType: DownstreamTransport;
|
|
77
88
|
process: ReturnType<typeof node_child_process.spawn> | null;
|
|
89
|
+
remoteUrl?: string;
|
|
90
|
+
remoteHeaders?: Record<string, string>;
|
|
91
|
+
remotePostUrl?: string;
|
|
92
|
+
sseAbort?: AbortController;
|
|
78
93
|
tools: Array<{
|
|
79
94
|
name: string;
|
|
80
95
|
description?: string;
|
|
@@ -248,4 +263,4 @@ declare class McpProxy extends EventEmitter {
|
|
|
248
263
|
*/
|
|
249
264
|
declare function proxyCli(args: string[]): Promise<void>;
|
|
250
265
|
|
|
251
|
-
export { type BlockedCallInfo, type EscalatedCallInfo, type InterceptInfo, McpProxy, type McpProxyConfig, type ProxyStats, type SingleUpstreamConfig, TOOLS, VERSION, auditLog, downstreamServers, evaluate, filterToolsByTier, getProxyTools, handleToolCall, initProxy, main, parseCommand, parseSQL, proxyCli, proxyEnabled, proxyToolCall, proxyToolMap, rules, shutdownProxy, tierHasAccess };
|
|
266
|
+
export { type BlockedCallInfo, type Channel, type EscalatedCallInfo, type EvalRequest, type EvalResult, type InterceptInfo, type McpContext, McpProxy, type McpProxyConfig, type PolicyRule, type ProxyStats, type RiskLevel, type SingleUpstreamConfig, TOOLS, VERSION, type Verdict, auditLog, McpProxy as default, downstreamServers, evaluate, filterToolsByTier, getProxyTools, handleToolCall, initProxy, main, parseCommand, parseSQL, proxyCli, proxyEnabled, proxyToolCall, proxyToolMap, rules, shutdownProxy, tierHasAccess };
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ var BUILT_IN_RULES = [
|
|
|
30
30
|
var rules = BUILT_IN_RULES.map((r) => ({ ...r, conditions: [...r.conditions] }));
|
|
31
31
|
var auditLog = [];
|
|
32
32
|
var MAX_AUDIT = 500;
|
|
33
|
-
var VERSION = "2.
|
|
33
|
+
var VERSION = "2.1.0";
|
|
34
34
|
var downstreamServers = /* @__PURE__ */ new Map();
|
|
35
35
|
var proxyToolMap = /* @__PURE__ */ new Map();
|
|
36
36
|
var proxyEnabled = false;
|
|
@@ -46,14 +46,36 @@ function getProxyConfig() {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
function sendToDownstream(server, message) {
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if (server.transportType === "stdio") {
|
|
50
|
+
if (!server.process?.stdin?.writable) return;
|
|
51
|
+
const json = JSON.stringify(message);
|
|
52
|
+
const payload = `Content-Length: ${Buffer.byteLength(json)}\r
|
|
52
53
|
\r
|
|
53
54
|
${json}`;
|
|
54
|
-
|
|
55
|
+
server.process.stdin.write(payload);
|
|
56
|
+
} else if (server.transportType === "sse") {
|
|
57
|
+
const postUrl = server.remotePostUrl;
|
|
58
|
+
if (!postUrl) {
|
|
59
|
+
log(`[proxy:${server.name}] SSE post URL not yet established`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const json = JSON.stringify(message);
|
|
63
|
+
fetch(postUrl, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json", ...server.remoteHeaders ?? {} },
|
|
66
|
+
body: json,
|
|
67
|
+
signal: AbortSignal.timeout(3e4)
|
|
68
|
+
}).catch((err) => log(`[proxy:${server.name}] SSE POST error: ${err}`));
|
|
69
|
+
} else if (server.transportType === "streamable-http") {
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
function requestFromDownstream(server, method, params) {
|
|
73
|
+
if (server.transportType === "streamable-http") {
|
|
74
|
+
return requestFromDownstreamHttp(server, method, params);
|
|
75
|
+
}
|
|
76
|
+
if (server.transportType === "sse") {
|
|
77
|
+
return requestFromDownstreamSse(server, method, params);
|
|
78
|
+
}
|
|
57
79
|
return new Promise((resolve, reject) => {
|
|
58
80
|
const id = server.nextId++;
|
|
59
81
|
const TIMEOUT_MS = 3e4;
|
|
@@ -65,6 +87,79 @@ function requestFromDownstream(server, method, params) {
|
|
|
65
87
|
sendToDownstream(server, { jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
66
88
|
});
|
|
67
89
|
}
|
|
90
|
+
function requestFromDownstreamSse(server, method, params) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const id = server.nextId++;
|
|
93
|
+
const TIMEOUT_MS = 3e4;
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
server.pendingRequests.delete(id);
|
|
96
|
+
reject(new Error(`[proxy] Timeout waiting for ${server.name} SSE response to ${method}`));
|
|
97
|
+
}, TIMEOUT_MS);
|
|
98
|
+
server.pendingRequests.set(id, { resolve, reject, timer });
|
|
99
|
+
const postUrl = server.remotePostUrl;
|
|
100
|
+
if (!postUrl) {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
server.pendingRequests.delete(id);
|
|
103
|
+
reject(new Error(`[proxy:${server.name}] SSE post URL not established`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const json = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
107
|
+
fetch(postUrl, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json", ...server.remoteHeaders ?? {} },
|
|
110
|
+
body: json,
|
|
111
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
112
|
+
}).catch((err) => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
server.pendingRequests.delete(id);
|
|
115
|
+
reject(new Error(`[proxy:${server.name}] SSE POST failed: ${err}`));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function requestFromDownstreamHttp(server, method, params) {
|
|
120
|
+
const id = server.nextId++;
|
|
121
|
+
const url = server.remoteUrl;
|
|
122
|
+
if (!url) throw new Error(`[proxy:${server.name}] No remote URL configured`);
|
|
123
|
+
const json = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
124
|
+
const TIMEOUT_MS = 3e4;
|
|
125
|
+
const resp = await fetch(url, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
"Accept": "application/json, text/event-stream",
|
|
130
|
+
...server.remoteHeaders ?? {}
|
|
131
|
+
},
|
|
132
|
+
body: json,
|
|
133
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
134
|
+
});
|
|
135
|
+
if (!resp.ok) {
|
|
136
|
+
throw new Error(`[proxy:${server.name}] HTTP ${resp.status}: ${resp.statusText}`);
|
|
137
|
+
}
|
|
138
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
139
|
+
if (contentType.includes("application/json")) {
|
|
140
|
+
const data2 = await resp.json();
|
|
141
|
+
if (data2.error) throw new Error(data2.error.message ?? "Downstream error");
|
|
142
|
+
return data2.result;
|
|
143
|
+
}
|
|
144
|
+
if (contentType.includes("text/event-stream")) {
|
|
145
|
+
const text = await resp.text();
|
|
146
|
+
for (const line of text.split("\n")) {
|
|
147
|
+
if (!line.startsWith("data: ")) continue;
|
|
148
|
+
try {
|
|
149
|
+
const msg = JSON.parse(line.slice(6));
|
|
150
|
+
if (msg.id === id) {
|
|
151
|
+
if (msg.error) throw new Error(msg.error.message ?? "Downstream error");
|
|
152
|
+
return msg.result;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`[proxy:${server.name}] No matching response in SSE stream`);
|
|
158
|
+
}
|
|
159
|
+
const data = await resp.json();
|
|
160
|
+
if (data.error) throw new Error(data.error.message ?? "Downstream error");
|
|
161
|
+
return data.result;
|
|
162
|
+
}
|
|
68
163
|
function handleDownstreamData(server, chunk) {
|
|
69
164
|
server.buffer += chunk;
|
|
70
165
|
while (true) {
|
|
@@ -98,10 +193,20 @@ function handleDownstreamData(server, chunk) {
|
|
|
98
193
|
}
|
|
99
194
|
}
|
|
100
195
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
196
|
+
function resolveTransport(config) {
|
|
197
|
+
if (config.transport) return config.transport;
|
|
198
|
+
if ("command" in config && config.command) return "stdio";
|
|
199
|
+
if ("url" in config && config.url) {
|
|
200
|
+
const u = config.url;
|
|
201
|
+
if (u.includes("/sse")) return "sse";
|
|
202
|
+
return "streamable-http";
|
|
203
|
+
}
|
|
204
|
+
return "stdio";
|
|
205
|
+
}
|
|
206
|
+
function createDownstreamServer(name, transport) {
|
|
207
|
+
return {
|
|
104
208
|
name,
|
|
209
|
+
transportType: transport,
|
|
105
210
|
process: null,
|
|
106
211
|
tools: [],
|
|
107
212
|
ready: false,
|
|
@@ -109,6 +214,10 @@ async function spawnDownstream(name, config) {
|
|
|
109
214
|
pendingRequests: /* @__PURE__ */ new Map(),
|
|
110
215
|
nextId: 1
|
|
111
216
|
};
|
|
217
|
+
}
|
|
218
|
+
async function connectStdioDownstream(name, config) {
|
|
219
|
+
const { spawn } = __require("child_process");
|
|
220
|
+
const server = createDownstreamServer(name, "stdio");
|
|
112
221
|
const env = { ...process.env, ...config.env ?? {} };
|
|
113
222
|
delete env.SOVR_PROXY_CONFIG;
|
|
114
223
|
const proc = spawn(config.command, config.args ?? [], {
|
|
@@ -133,24 +242,188 @@ async function spawnDownstream(name, config) {
|
|
|
133
242
|
}
|
|
134
243
|
server.pendingRequests.clear();
|
|
135
244
|
});
|
|
245
|
+
await initializeMcpHandshake(server);
|
|
246
|
+
return server;
|
|
247
|
+
}
|
|
248
|
+
async function connectSseDownstream(name, config) {
|
|
249
|
+
const server = createDownstreamServer(name, "sse");
|
|
250
|
+
server.remoteUrl = config.url;
|
|
251
|
+
server.remoteHeaders = { ...config.headers ?? {} };
|
|
252
|
+
if (config.env) {
|
|
253
|
+
for (const [k, v] of Object.entries(config.env)) {
|
|
254
|
+
if (k.toLowerCase().includes("token") || k.toLowerCase().includes("key") || k.toLowerCase().includes("auth")) {
|
|
255
|
+
if (!server.remoteHeaders["Authorization"]) {
|
|
256
|
+
server.remoteHeaders["Authorization"] = `Bearer ${v}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const abort = new AbortController();
|
|
262
|
+
server.sseAbort = abort;
|
|
263
|
+
log(`[proxy:${name}] Connecting to SSE endpoint: ${config.url}`);
|
|
264
|
+
try {
|
|
265
|
+
const resp = await fetch(config.url, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
headers: {
|
|
268
|
+
"Accept": "text/event-stream",
|
|
269
|
+
...server.remoteHeaders
|
|
270
|
+
},
|
|
271
|
+
signal: abort.signal
|
|
272
|
+
});
|
|
273
|
+
if (!resp.ok) {
|
|
274
|
+
throw new Error(`SSE connection failed: HTTP ${resp.status} ${resp.statusText}`);
|
|
275
|
+
}
|
|
276
|
+
if (!resp.body) {
|
|
277
|
+
throw new Error("SSE response has no body");
|
|
278
|
+
}
|
|
279
|
+
const reader = resp.body.getReader();
|
|
280
|
+
const decoder = new TextDecoder();
|
|
281
|
+
let sseBuf = "";
|
|
282
|
+
const readLoop = async () => {
|
|
283
|
+
try {
|
|
284
|
+
while (true) {
|
|
285
|
+
const { done, value } = await reader.read();
|
|
286
|
+
if (done) {
|
|
287
|
+
log(`[proxy:${name}] SSE stream ended`);
|
|
288
|
+
server.ready = false;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
sseBuf += decoder.decode(value, { stream: true });
|
|
292
|
+
const events = sseBuf.split("\n\n");
|
|
293
|
+
sseBuf = events.pop() ?? "";
|
|
294
|
+
for (const event of events) {
|
|
295
|
+
if (!event.trim()) continue;
|
|
296
|
+
let eventType = "message";
|
|
297
|
+
let eventData = "";
|
|
298
|
+
for (const line of event.split("\n")) {
|
|
299
|
+
if (line.startsWith("event: ")) {
|
|
300
|
+
eventType = line.slice(7).trim();
|
|
301
|
+
} else if (line.startsWith("data: ")) {
|
|
302
|
+
eventData += (eventData ? "\n" : "") + line.slice(6);
|
|
303
|
+
} else if (line.startsWith("data:")) {
|
|
304
|
+
eventData += (eventData ? "\n" : "") + line.slice(5);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (eventType === "endpoint" && eventData) {
|
|
308
|
+
const endpointUrl = eventData.trim();
|
|
309
|
+
try {
|
|
310
|
+
const base = new URL(config.url);
|
|
311
|
+
server.remotePostUrl = new URL(endpointUrl, base).toString();
|
|
312
|
+
} catch {
|
|
313
|
+
server.remotePostUrl = endpointUrl;
|
|
314
|
+
}
|
|
315
|
+
log(`[proxy:${name}] SSE message endpoint: ${server.remotePostUrl}`);
|
|
316
|
+
} else if (eventType === "message" && eventData) {
|
|
317
|
+
try {
|
|
318
|
+
const msg = JSON.parse(eventData);
|
|
319
|
+
if (msg.id !== void 0 && server.pendingRequests.has(msg.id)) {
|
|
320
|
+
const pending = server.pendingRequests.get(msg.id);
|
|
321
|
+
clearTimeout(pending.timer);
|
|
322
|
+
server.pendingRequests.delete(msg.id);
|
|
323
|
+
if (msg.error) {
|
|
324
|
+
pending.reject(new Error(msg.error.message ?? "Downstream SSE error"));
|
|
325
|
+
} else {
|
|
326
|
+
pending.resolve(msg.result);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
log(`[proxy:${name}] Failed to parse SSE message: ${eventData.substring(0, 200)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (!abort.signal.aborted) {
|
|
337
|
+
log(`[proxy:${name}] SSE read error: ${err}`);
|
|
338
|
+
server.ready = false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
readLoop();
|
|
343
|
+
const endpointTimeout = 15e3;
|
|
344
|
+
const startWait = Date.now();
|
|
345
|
+
while (!server.remotePostUrl && Date.now() - startWait < endpointTimeout) {
|
|
346
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
347
|
+
}
|
|
348
|
+
if (!server.remotePostUrl) {
|
|
349
|
+
throw new Error(`SSE server did not send endpoint URL within ${endpointTimeout}ms`);
|
|
350
|
+
}
|
|
351
|
+
await initializeMcpHandshake(server);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
log(`[proxy:${name}] SSE connection failed: ${err}`);
|
|
354
|
+
abort.abort();
|
|
355
|
+
}
|
|
356
|
+
return server;
|
|
357
|
+
}
|
|
358
|
+
async function connectHttpDownstream(name, config) {
|
|
359
|
+
const server = createDownstreamServer(name, "streamable-http");
|
|
360
|
+
server.remoteUrl = config.url;
|
|
361
|
+
server.remoteHeaders = { ...config.headers ?? {} };
|
|
362
|
+
if (config.env) {
|
|
363
|
+
for (const [k, v] of Object.entries(config.env)) {
|
|
364
|
+
if (k.toLowerCase().includes("token") || k.toLowerCase().includes("key") || k.toLowerCase().includes("auth")) {
|
|
365
|
+
if (!server.remoteHeaders["Authorization"]) {
|
|
366
|
+
server.remoteHeaders["Authorization"] = `Bearer ${v}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
log(`[proxy:${name}] Connecting to Streamable HTTP endpoint: ${config.url}`);
|
|
372
|
+
try {
|
|
373
|
+
await initializeMcpHandshake(server);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log(`[proxy:${name}] Streamable HTTP connection failed: ${err}`);
|
|
376
|
+
}
|
|
377
|
+
return server;
|
|
378
|
+
}
|
|
379
|
+
async function initializeMcpHandshake(server) {
|
|
136
380
|
try {
|
|
137
381
|
await requestFromDownstream(server, "initialize", {
|
|
138
382
|
protocolVersion: "2024-11-05",
|
|
139
383
|
capabilities: {},
|
|
140
384
|
clientInfo: { name: "sovr-proxy", version: VERSION }
|
|
141
385
|
});
|
|
142
|
-
|
|
386
|
+
if (server.transportType === "stdio") {
|
|
387
|
+
sendToDownstream(server, { jsonrpc: "2.0", method: "notifications/initialized" });
|
|
388
|
+
} else if (server.transportType === "sse" && server.remotePostUrl) {
|
|
389
|
+
fetch(server.remotePostUrl, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { "Content-Type": "application/json", ...server.remoteHeaders ?? {} },
|
|
392
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
393
|
+
}).catch(() => {
|
|
394
|
+
});
|
|
395
|
+
} else if (server.transportType === "streamable-http" && server.remoteUrl) {
|
|
396
|
+
fetch(server.remoteUrl, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "Content-Type": "application/json", ...server.remoteHeaders ?? {} },
|
|
399
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
400
|
+
}).catch(() => {
|
|
401
|
+
});
|
|
402
|
+
}
|
|
143
403
|
const toolsResult = await requestFromDownstream(server, "tools/list", {});
|
|
144
404
|
server.tools = toolsResult?.tools ?? [];
|
|
145
405
|
server.ready = true;
|
|
146
406
|
for (const tool of server.tools) {
|
|
147
|
-
proxyToolMap.set(tool.name, name);
|
|
407
|
+
proxyToolMap.set(tool.name, server.name);
|
|
148
408
|
}
|
|
149
|
-
log(`[proxy] ${name}: ${server.tools.length} tools discovered`);
|
|
409
|
+
log(`[proxy] ${server.name}: ${server.tools.length} tools discovered (${server.transportType})`);
|
|
150
410
|
} catch (err) {
|
|
151
|
-
log(`[proxy] Failed to initialize ${name}: ${err}`);
|
|
411
|
+
log(`[proxy] Failed to initialize ${server.name}: ${err}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function connectDownstream(name, config) {
|
|
415
|
+
const transport = resolveTransport(config);
|
|
416
|
+
log(`[proxy] Connecting to ${name} via ${transport}...`);
|
|
417
|
+
switch (transport) {
|
|
418
|
+
case "stdio":
|
|
419
|
+
return connectStdioDownstream(name, config);
|
|
420
|
+
case "sse":
|
|
421
|
+
return connectSseDownstream(name, config);
|
|
422
|
+
case "streamable-http":
|
|
423
|
+
return connectHttpDownstream(name, config);
|
|
424
|
+
default:
|
|
425
|
+
throw new Error(`Unknown transport: ${transport}`);
|
|
152
426
|
}
|
|
153
|
-
return server;
|
|
154
427
|
}
|
|
155
428
|
async function initProxy() {
|
|
156
429
|
const config = getProxyConfig();
|
|
@@ -158,7 +431,7 @@ async function initProxy() {
|
|
|
158
431
|
proxyEnabled = true;
|
|
159
432
|
log(`[proxy] Initializing transparent interception for ${Object.keys(config.downstream).length} downstream servers...`);
|
|
160
433
|
const results = await Promise.allSettled(
|
|
161
|
-
Object.entries(config.downstream).map(([name, cfg]) =>
|
|
434
|
+
Object.entries(config.downstream).map(([name, cfg]) => connectDownstream(name, cfg))
|
|
162
435
|
);
|
|
163
436
|
for (const result of results) {
|
|
164
437
|
if (result.status === "fulfilled" && result.value.ready) {
|
|
@@ -248,10 +521,17 @@ async function proxyToolCall(toolName, args) {
|
|
|
248
521
|
}
|
|
249
522
|
function shutdownProxy() {
|
|
250
523
|
for (const [name, server] of downstreamServers) {
|
|
251
|
-
|
|
252
|
-
|
|
524
|
+
log(`[proxy] Shutting down ${name} (${server.transportType})`);
|
|
525
|
+
if (server.transportType === "stdio" && server.process) {
|
|
253
526
|
server.process.kill("SIGTERM");
|
|
527
|
+
} else if (server.sseAbort) {
|
|
528
|
+
server.sseAbort.abort();
|
|
254
529
|
}
|
|
530
|
+
for (const [, pending] of server.pendingRequests) {
|
|
531
|
+
clearTimeout(pending.timer);
|
|
532
|
+
pending.reject(new Error(`Downstream ${name} shutting down`));
|
|
533
|
+
}
|
|
534
|
+
server.pendingRequests.clear();
|
|
255
535
|
}
|
|
256
536
|
downstreamServers.clear();
|
|
257
537
|
proxyToolMap.clear();
|
|
@@ -7172,7 +7452,9 @@ PROXY MODE (transparent interception):
|
|
|
7172
7452
|
{
|
|
7173
7453
|
"downstream": {
|
|
7174
7454
|
"stripe": { "command": "npx", "args": ["@stripe/agent-toolkit"] },
|
|
7175
|
-
"github": { "command": "npx", "args": ["@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "..." } }
|
|
7455
|
+
"github": { "command": "npx", "args": ["@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "..." } },
|
|
7456
|
+
"remote-sse": { "transport": "sse", "url": "https://mcp.example.com/sse", "headers": { "Authorization": "Bearer ..." } },
|
|
7457
|
+
"remote-http": { "transport": "streamable-http", "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer ..." } }
|
|
7176
7458
|
}
|
|
7177
7459
|
}
|
|
7178
7460
|
|
|
@@ -7546,11 +7828,13 @@ async function proxyCli(args) {
|
|
|
7546
7828
|
});
|
|
7547
7829
|
await proxy.start();
|
|
7548
7830
|
}
|
|
7831
|
+
var index_default = McpProxy;
|
|
7549
7832
|
export {
|
|
7550
7833
|
McpProxy,
|
|
7551
7834
|
TOOLS,
|
|
7552
7835
|
VERSION,
|
|
7553
7836
|
auditLog,
|
|
7837
|
+
index_default as default,
|
|
7554
7838
|
downstreamServers,
|
|
7555
7839
|
evaluate,
|
|
7556
7840
|
filterToolsByTier,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sovr-mcp-proxy",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Transparent MCP Proxy that intercepts all agent tool calls with policy gate-check before forwarding. Superset of sovr-mcp-server.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Transparent MCP Proxy that intercepts all agent tool calls with policy gate-check before forwarding. Supports stdio, SSE, and Streamable HTTP transports. Superset of sovr-mcp-server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"model-context-protocol",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"audit",
|
|
14
14
|
"trust",
|
|
15
15
|
"sovr",
|
|
16
|
-
"mcp-proxy"
|
|
16
|
+
"mcp-proxy",
|
|
17
|
+
"sse",
|
|
18
|
+
"streamable-http",
|
|
19
|
+
"remote-mcp"
|
|
17
20
|
],
|
|
18
21
|
"homepage": "https://sovr.inc",
|
|
19
22
|
"repository": {
|