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 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.0.0";
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.0.0";
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 (!server.process?.stdin?.writable) return;
50
- const json = JSON.stringify(message);
51
- const payload = `Content-Length: ${Buffer.byteLength(json)}\r
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
- server.process.stdin.write(payload);
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
- async function spawnDownstream(name, config) {
102
- const { spawn } = __require("child_process");
103
- const server = {
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
- sendToDownstream(server, { jsonrpc: "2.0", method: "notifications/initialized" });
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]) => spawnDownstream(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
- if (server.process) {
252
- log(`[proxy] Shutting down ${name}`);
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.0.0",
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": {