sovr-mcp-proxy 1.0.0 → 2.0.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.

@@ -0,0 +1,251 @@
1
+ import * as node_child_process from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ /**
5
+ * sovr-mcp-proxy v2.0.0 — Full SDK Coverage + Programmable Proxy API
6
+ *
7
+ * Complete MCP interface covering ALL SOVR SDK methods
8
+ * organized into 286 tools with operation-based routing.
9
+ *
10
+ * Two modes:
11
+ * 1. **Local** (free) — Built-in policy engine with 15 rules
12
+ * 2. **Cloud** (SOVR Cloud) — Full SDK access via API proxy
13
+ *
14
+ * Zero external dependencies. Self-contained stdio MCP transport.
15
+ *
16
+ * @example
17
+ * ```json
18
+ * {
19
+ * "mcpServers": {
20
+ * "sovr": {
21
+ * "command": "npx",
22
+ * "args": ["sovr-mcp-server"],
23
+ * "env": {
24
+ * "SOVR_API_KEY": "sovr_sk_...",
25
+ * "SOVR_ENDPOINT": "https://your-sovr-instance.com"
26
+ * }
27
+ * }
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ type Channel = "mcp" | "http" | "sql" | "exec";
33
+ type Verdict = "allow" | "deny" | "escalate";
34
+ type RiskLevel = "none" | "low" | "medium" | "high" | "critical";
35
+ interface PolicyRule {
36
+ id: string;
37
+ description: string;
38
+ channels: Channel[];
39
+ action_pattern: string;
40
+ resource_pattern: string;
41
+ conditions: Array<{
42
+ field: string;
43
+ operator: string;
44
+ value: string;
45
+ }>;
46
+ effect: Verdict;
47
+ risk_level: RiskLevel;
48
+ require_approval: boolean;
49
+ priority: number;
50
+ enabled: boolean;
51
+ }
52
+ interface EvalResult {
53
+ verdict: Verdict;
54
+ risk_score: number;
55
+ matched_rules: string[];
56
+ reason: string;
57
+ decision_id: string;
58
+ timestamp: number;
59
+ channel: Channel;
60
+ [key: string]: unknown;
61
+ }
62
+ interface AuditEntry {
63
+ decision_id: string;
64
+ timestamp: number;
65
+ channel: Channel;
66
+ action: string;
67
+ resource: string;
68
+ verdict: Verdict;
69
+ risk_score: number;
70
+ matched_rules: string[];
71
+ }
72
+ declare let rules: PolicyRule[];
73
+ declare const auditLog: AuditEntry[];
74
+ declare const VERSION = "2.0.0";
75
+ interface DownstreamServer {
76
+ name: string;
77
+ process: ReturnType<typeof node_child_process.spawn> | null;
78
+ tools: Array<{
79
+ name: string;
80
+ description?: string;
81
+ inputSchema?: unknown;
82
+ }>;
83
+ ready: boolean;
84
+ buffer: string;
85
+ pendingRequests: Map<number, {
86
+ resolve: (v: unknown) => void;
87
+ reject: (e: Error) => void;
88
+ timer: ReturnType<typeof setTimeout>;
89
+ }>;
90
+ nextId: number;
91
+ }
92
+ declare const downstreamServers: Map<string, DownstreamServer>;
93
+ declare const proxyToolMap: Map<string, string>;
94
+ declare let proxyEnabled: boolean;
95
+ declare function initProxy(): Promise<void>;
96
+ declare function getProxyTools(): Array<{
97
+ name: string;
98
+ description?: string;
99
+ inputSchema?: unknown;
100
+ }>;
101
+ declare function proxyToolCall(toolName: string, args: Record<string, unknown>): Promise<{
102
+ content: Array<{
103
+ type: string;
104
+ text: string;
105
+ }>;
106
+ isError?: boolean;
107
+ }>;
108
+ declare function shutdownProxy(): void;
109
+ declare function evaluate(channel: Channel, action: string, resource: string, context?: Record<string, unknown>): EvalResult;
110
+ interface ParsedCommand {
111
+ command: string;
112
+ subCommand: string | null;
113
+ args: string[];
114
+ hasSudo: boolean;
115
+ hasPipe: boolean;
116
+ hasChain: boolean;
117
+ riskIndicators: string[];
118
+ }
119
+ declare function parseCommand(raw: string): ParsedCommand;
120
+ interface ParsedSQL {
121
+ type: string;
122
+ tables: string[];
123
+ hasWhereClause: boolean;
124
+ isMultiStatement: boolean;
125
+ raw: string;
126
+ }
127
+ declare function parseSQL(raw: string): ParsedSQL;
128
+ type Tier = "free" | "personal" | "starter" | "pro" | "enterprise";
129
+ declare function tierHasAccess(userTier: Tier, toolName: string): boolean;
130
+ declare function filterToolsByTier<T extends {
131
+ name: string;
132
+ }>(tools: T[], tier: Tier): T[];
133
+ interface McpToolDef {
134
+ name: string;
135
+ description: string;
136
+ inputSchema: {
137
+ type: "object";
138
+ properties: Record<string, unknown>;
139
+ required?: string[];
140
+ };
141
+ }
142
+ declare const TOOLS: McpToolDef[];
143
+ type ToolResult = {
144
+ content: Array<{
145
+ type: "text";
146
+ text: string;
147
+ }>;
148
+ };
149
+ declare function handleToolCall(name: string, args: Record<string, unknown>): Promise<ToolResult>;
150
+ declare function main(): Promise<void>;
151
+ /** How to connect to the upstream MCP server (single-upstream mode) */
152
+ interface SingleUpstreamConfig {
153
+ /** Command to spawn the upstream MCP server (stdio transport) */
154
+ command: string;
155
+ /** Arguments for the command */
156
+ args?: string[];
157
+ /** Environment variables for the upstream process */
158
+ env?: Record<string, string>;
159
+ /** Working directory */
160
+ cwd?: string;
161
+ }
162
+ /** Proxy configuration for the McpProxy class */
163
+ interface McpProxyConfig {
164
+ /** Upstream MCP server (stdio) */
165
+ upstream: SingleUpstreamConfig;
166
+ /** Custom policy rules (defaults to built-in 15 rules) */
167
+ customRules?: PolicyRule[];
168
+ /** Server name for identification */
169
+ serverName?: string;
170
+ /** Whether to log intercepted calls */
171
+ verbose?: boolean;
172
+ /** Callback when a call is blocked */
173
+ onBlocked?: (info: BlockedCallInfo) => void | Promise<void>;
174
+ /** Callback when a call is escalated */
175
+ onEscalated?: (info: EscalatedCallInfo) => void | Promise<void>;
176
+ /** Callback for all intercepted calls (for audit) */
177
+ onIntercept?: (info: InterceptInfo) => void | Promise<void>;
178
+ }
179
+ interface BlockedCallInfo {
180
+ method: string;
181
+ toolName: string;
182
+ arguments: Record<string, unknown>;
183
+ decision: EvalResult;
184
+ timestamp: number;
185
+ }
186
+ interface EscalatedCallInfo {
187
+ method: string;
188
+ toolName: string;
189
+ arguments: Record<string, unknown>;
190
+ decision: EvalResult;
191
+ timestamp: number;
192
+ }
193
+ interface InterceptInfo {
194
+ method: string;
195
+ toolName: string;
196
+ arguments: Record<string, unknown>;
197
+ decision: EvalResult;
198
+ forwarded: boolean;
199
+ timestamp: number;
200
+ }
201
+ /** Proxy statistics */
202
+ interface ProxyStats {
203
+ totalCalls: number;
204
+ allowedCalls: number;
205
+ blockedCalls: number;
206
+ escalatedCalls: number;
207
+ upstreamErrors: number;
208
+ startedAt: number;
209
+ }
210
+ declare class McpProxy extends EventEmitter {
211
+ private upstreamConfig;
212
+ private upstream;
213
+ private _serverName;
214
+ private _verbose;
215
+ private _onBlocked?;
216
+ private _onEscalated?;
217
+ private _onIntercept?;
218
+ private _stats;
219
+ private _customRules;
220
+ constructor(config: McpProxyConfig);
221
+ /**
222
+ * Start the proxy in stdio mode.
223
+ * Reads JSON-RPC messages from stdin, intercepts tool calls,
224
+ * and forwards approved calls to the upstream MCP server.
225
+ */
226
+ start(): Promise<void>;
227
+ /** Stop the proxy and kill the upstream process. */
228
+ stop(): void;
229
+ /** Get proxy statistics. */
230
+ getStats(): ProxyStats;
231
+ private handleAgentMessage;
232
+ private handleUpstreamMessage;
233
+ private interceptToolCall;
234
+ /**
235
+ * Extract danger signals from tool arguments for rule matching.
236
+ * Normalizes common patterns across different MCP servers.
237
+ */
238
+ private extractDangerSignals;
239
+ private sendBlockedResponse;
240
+ private sendEscalatedResponse;
241
+ private forwardToUpstream;
242
+ }
243
+ /**
244
+ * CLI entry point for single-upstream proxy mode.
245
+ * Usage:
246
+ * sovr-mcp-proxy --upstream "npx -y @modelcontextprotocol/server-filesystem /tmp"
247
+ * sovr-mcp-proxy --upstream "node my-mcp-server.js" --rules ./policy.json
248
+ */
249
+ declare function proxyCli(args: string[]): Promise<void>;
250
+
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 };
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/index.ts
10
+ import { spawn as nodeSpawn } from "child_process";
11
+ import { createInterface } from "readline";
12
+ import { EventEmitter } from "events";
10
13
  var BUILT_IN_RULES = [
11
14
  { id: "exec-destructive-commands", description: "Block destructive shell commands (rm -rf, mkfs, dd, shred)", channels: ["exec"], action_pattern: "rm|mkfs|dd|shred|wipefs", resource_pattern: "*", conditions: [], effect: "deny", risk_level: "critical", require_approval: true, priority: 100, enabled: true },
12
15
  { id: "exec-kubernetes-destructive", description: "Escalate destructive Kubernetes operations", channels: ["exec"], action_pattern: "kubectl_delete|kubectl_drain|kubectl_cordon", resource_pattern: "*", conditions: [], effect: "escalate", risk_level: "high", require_approval: true, priority: 90, enabled: true },
@@ -27,7 +30,7 @@ var BUILT_IN_RULES = [
27
30
  var rules = BUILT_IN_RULES.map((r) => ({ ...r, conditions: [...r.conditions] }));
28
31
  var auditLog = [];
29
32
  var MAX_AUDIT = 500;
30
- var VERSION = "6.0.0";
33
+ var VERSION = "2.0.0";
31
34
  var downstreamServers = /* @__PURE__ */ new Map();
32
35
  var proxyToolMap = /* @__PURE__ */ new Map();
33
36
  var proxyEnabled = false;
@@ -396,11 +399,11 @@ async function verifyKeyTier() {
396
399
  return "free";
397
400
  }
398
401
  const data = await resp.json();
399
- const tier = (data.tier || data.plan || "free").toLowerCase();
402
+ const rawTier = (data.tier || data.plan || "free").toLowerCase();
400
403
  const validTiers = ["free", "personal", "starter", "pro", "enterprise"];
401
- if (validTiers.includes(tier)) return tier;
402
- if (tier === "basic" || tier === "individual") return "personal";
403
- if (tier === "team" || tier === "business") return "pro";
404
+ if (validTiers.includes(rawTier)) return rawTier;
405
+ if (rawTier === "basic" || rawTier === "individual") return "personal";
406
+ if (rawTier === "team" || rawTier === "business") return "pro";
404
407
  return "personal";
405
408
  } catch (err) {
406
409
  log(`[tier] Cloud verification error: ${err instanceof Error ? err.message : String(err)}`);
@@ -7121,13 +7124,12 @@ function startStdioTransport() {
7121
7124
  async function main() {
7122
7125
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
7123
7126
  console.log(`
7124
- sovr-mcp-server v${VERSION} \u2014 Full SDK Coverage Edition
7125
-
7126
- The complete MCP interface for the SOVR Responsibility Layer.
7127
- 286 tools covering ALL SOVR SDK methods.
7127
+ sovr-mcp-proxy v${VERSION} \u2014 Execution Firewall for AI Agents
7128
+ The complete MCP interface + programmable proxy for the SOVR Responsibility Layer.
7129
+ 286 tools + McpProxy class for custom integrations.
7128
7130
 
7129
7131
  USAGE:
7130
- npx sovr-mcp-server
7132
+ npx sovr-mcp-proxy
7131
7133
 
7132
7134
  ENVIRONMENT:
7133
7135
  SOVR_API_KEY Connect to SOVR Cloud for full SDK access
@@ -7137,7 +7139,7 @@ ENVIRONMENT:
7137
7139
  LOCAL MODE (free, 15 built-in rules):
7138
7140
  {
7139
7141
  "mcpServers": {
7140
- "sovr": { "command": "npx", "args": ["sovr-mcp-server"] }
7142
+ "sovr": { "command": "npx", "args": ["sovr-mcp-proxy"] }
7141
7143
  }
7142
7144
  }
7143
7145
 
@@ -7146,7 +7148,7 @@ CLOUD MODE (286 tools, full SDK):
7146
7148
  "mcpServers": {
7147
7149
  "sovr": {
7148
7150
  "command": "npx",
7149
- "args": ["sovr-mcp-server"],
7151
+ "args": ["sovr-mcp-proxy"],
7150
7152
  "env": { "SOVR_API_KEY": "sovr_sk_..." }
7151
7153
  }
7152
7154
  }
@@ -7157,7 +7159,7 @@ PROXY MODE (transparent interception):
7157
7159
  "mcpServers": {
7158
7160
  "sovr": {
7159
7161
  "command": "npx",
7160
- "args": ["sovr-mcp-server"],
7162
+ "args": ["sovr-mcp-proxy"],
7161
7163
  "env": {
7162
7164
  "SOVR_API_KEY": "sovr_sk_...",
7163
7165
  "SOVR_PROXY_CONFIG": "/path/to/proxy.json"
@@ -7174,6 +7176,18 @@ PROXY MODE (transparent interception):
7174
7176
  }
7175
7177
  }
7176
7178
 
7179
+ SINGLE UPSTREAM PROXY MODE (programmable):
7180
+ sovr-mcp-proxy --upstream "npx -y @modelcontextprotocol/server-filesystem /tmp"
7181
+ sovr-mcp-proxy --upstream "node my-server.js" --rules ./policy.json --verbose
7182
+
7183
+ PROGRAMMATIC API:
7184
+ import { McpProxy } from 'sovr-mcp-proxy';
7185
+ const proxy = new McpProxy({
7186
+ upstream: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'] },
7187
+ onBlocked: (info) => console.log('Blocked:', info.toolName),
7188
+ });
7189
+ await proxy.start();
7190
+
7177
7191
  Learn more: https://sovr.inc
7178
7192
  `);
7179
7193
  process.exit(0);
@@ -7227,11 +7241,313 @@ Learn more: https://sovr.inc
7227
7241
  startStdioTransport();
7228
7242
  }
7229
7243
  }
7230
- var isMain = typeof process !== "undefined" && process.argv[1] && (process.argv[1].includes("sovr-mcp-server") || process.argv[1].endsWith("index.js") || process.argv[1].endsWith("index.mjs") || process.argv[1].endsWith("index.cjs"));
7244
+ var isMain = typeof process !== "undefined" && process.argv[1] && (process.argv[1].includes("sovr-mcp-server") || process.argv[1].includes("sovr-mcp-proxy") || process.argv[1].endsWith("index.js") || process.argv[1].endsWith("index.mjs") || process.argv[1].endsWith("index.cjs"));
7231
7245
  if (isMain) {
7232
- main();
7246
+ if (process.argv.includes("--upstream") || process.argv.includes("-u")) {
7247
+ proxyCli(process.argv.slice(2));
7248
+ } else {
7249
+ main();
7250
+ }
7251
+ }
7252
+ var McpProxy = class extends EventEmitter {
7253
+ upstreamConfig;
7254
+ upstream = null;
7255
+ _serverName;
7256
+ _verbose;
7257
+ _onBlocked;
7258
+ _onEscalated;
7259
+ _onIntercept;
7260
+ _stats;
7261
+ _customRules;
7262
+ constructor(config) {
7263
+ super();
7264
+ this.upstreamConfig = config.upstream;
7265
+ this._serverName = config.serverName ?? "sovr-mcp-proxy";
7266
+ this._verbose = config.verbose ?? false;
7267
+ this._onBlocked = config.onBlocked;
7268
+ this._onEscalated = config.onEscalated;
7269
+ this._onIntercept = config.onIntercept;
7270
+ this._customRules = config.customRules ?? [];
7271
+ this._stats = {
7272
+ totalCalls: 0,
7273
+ allowedCalls: 0,
7274
+ blockedCalls: 0,
7275
+ escalatedCalls: 0,
7276
+ upstreamErrors: 0,
7277
+ startedAt: Date.now()
7278
+ };
7279
+ if (this._customRules.length > 0) {
7280
+ for (const r of this._customRules) {
7281
+ rules.push({ ...r, conditions: [...r.conditions || []], enabled: true });
7282
+ }
7283
+ }
7284
+ }
7285
+ /**
7286
+ * Start the proxy in stdio mode.
7287
+ * Reads JSON-RPC messages from stdin, intercepts tool calls,
7288
+ * and forwards approved calls to the upstream MCP server.
7289
+ */
7290
+ async start() {
7291
+ this.upstream = nodeSpawn(
7292
+ this.upstreamConfig.command,
7293
+ this.upstreamConfig.args ?? [],
7294
+ {
7295
+ stdio: ["pipe", "pipe", "pipe"],
7296
+ env: { ...process.env, ...this.upstreamConfig.env },
7297
+ cwd: this.upstreamConfig.cwd
7298
+ }
7299
+ );
7300
+ if (!this.upstream.stdout || !this.upstream.stdin) {
7301
+ throw new Error("Failed to spawn upstream MCP server");
7302
+ }
7303
+ const upstreamReader = createInterface({ input: this.upstream.stdout });
7304
+ upstreamReader.on("line", (line) => {
7305
+ this.handleUpstreamMessage(line);
7306
+ });
7307
+ this.upstream.stderr?.on("data", (data) => {
7308
+ if (this._verbose) {
7309
+ process.stderr.write(`[sovr-mcp-proxy] upstream stderr: ${data}`);
7310
+ }
7311
+ });
7312
+ this.upstream.on("exit", (code) => {
7313
+ if (this._verbose) {
7314
+ process.stderr.write(`[sovr-mcp-proxy] upstream exited with code ${code}
7315
+ `);
7316
+ }
7317
+ this.emit("upstream-exit", code);
7318
+ });
7319
+ const agentReader = createInterface({ input: process.stdin });
7320
+ agentReader.on("line", (line) => {
7321
+ this.handleAgentMessage(line);
7322
+ });
7323
+ process.stdin.on("end", () => {
7324
+ this.stop();
7325
+ });
7326
+ if (this._verbose) {
7327
+ process.stderr.write(`[sovr-mcp-proxy] started, proxying to ${this.upstreamConfig.command}
7328
+ `);
7329
+ }
7330
+ }
7331
+ /** Stop the proxy and kill the upstream process. */
7332
+ stop() {
7333
+ if (this.upstream) {
7334
+ this.upstream.kill();
7335
+ this.upstream = null;
7336
+ }
7337
+ }
7338
+ /** Get proxy statistics. */
7339
+ getStats() {
7340
+ return { ...this._stats };
7341
+ }
7342
+ // ---------- Internal ----------
7343
+ handleAgentMessage(line) {
7344
+ let msg;
7345
+ try {
7346
+ msg = JSON.parse(line);
7347
+ } catch {
7348
+ this.forwardToUpstream(line);
7349
+ return;
7350
+ }
7351
+ if (msg.method === "tools/call") {
7352
+ this.interceptToolCall(msg);
7353
+ } else {
7354
+ this.forwardToUpstream(line);
7355
+ }
7356
+ }
7357
+ handleUpstreamMessage(line) {
7358
+ process.stdout.write(line + "\n");
7359
+ }
7360
+ interceptToolCall(request) {
7361
+ this._stats.totalCalls++;
7362
+ const params = request.params ?? {};
7363
+ const toolName = params.name ?? "unknown";
7364
+ const toolArgs = params.arguments ?? {};
7365
+ const dangerSignals = this.extractDangerSignals(toolName, toolArgs);
7366
+ const decision = evaluate("mcp", toolName, toolName, {
7367
+ tool_name: toolName,
7368
+ server_name: this._serverName,
7369
+ arguments: toolArgs,
7370
+ ...dangerSignals
7371
+ });
7372
+ const interceptInfo = {
7373
+ method: request.method,
7374
+ toolName,
7375
+ arguments: toolArgs,
7376
+ decision,
7377
+ forwarded: decision.verdict === "allow",
7378
+ timestamp: Date.now()
7379
+ };
7380
+ if (this._onIntercept) {
7381
+ Promise.resolve(this._onIntercept(interceptInfo)).catch(() => {
7382
+ });
7383
+ }
7384
+ this.emit("intercept", interceptInfo);
7385
+ if (decision.verdict === "deny") {
7386
+ this._stats.blockedCalls++;
7387
+ this.sendBlockedResponse(request, decision);
7388
+ if (this._onBlocked) {
7389
+ Promise.resolve(this._onBlocked({
7390
+ method: request.method,
7391
+ toolName,
7392
+ arguments: toolArgs,
7393
+ decision,
7394
+ timestamp: Date.now()
7395
+ })).catch(() => {
7396
+ });
7397
+ }
7398
+ if (this._verbose) {
7399
+ process.stderr.write(`[sovr-mcp-proxy] BLOCKED: ${toolName} \u2014 ${decision.reason}
7400
+ `);
7401
+ }
7402
+ } else if (decision.verdict === "escalate") {
7403
+ this._stats.escalatedCalls++;
7404
+ this.sendEscalatedResponse(request, decision);
7405
+ if (this._onEscalated) {
7406
+ Promise.resolve(this._onEscalated({
7407
+ method: request.method,
7408
+ toolName,
7409
+ arguments: toolArgs,
7410
+ decision,
7411
+ timestamp: Date.now()
7412
+ })).catch(() => {
7413
+ });
7414
+ }
7415
+ if (this._verbose) {
7416
+ process.stderr.write(`[sovr-mcp-proxy] ESCALATED: ${toolName} \u2014 ${decision.reason}
7417
+ `);
7418
+ }
7419
+ } else {
7420
+ this._stats.allowedCalls++;
7421
+ this.forwardToUpstream(JSON.stringify(request));
7422
+ if (this._verbose) {
7423
+ process.stderr.write(`[sovr-mcp-proxy] ALLOWED: ${toolName} (risk: ${decision.risk_score})
7424
+ `);
7425
+ }
7426
+ }
7427
+ }
7428
+ /**
7429
+ * Extract danger signals from tool arguments for rule matching.
7430
+ * Normalizes common patterns across different MCP servers.
7431
+ */
7432
+ extractDangerSignals(toolName, args) {
7433
+ const signals = {};
7434
+ if (toolName.includes("file") || toolName.includes("write") || toolName.includes("read")) {
7435
+ signals.is_file_operation = true;
7436
+ if (args.path) signals.file_path = args.path;
7437
+ }
7438
+ if (toolName.includes("shell") || toolName.includes("exec") || toolName.includes("run")) {
7439
+ signals.is_shell_operation = true;
7440
+ if (args.command) signals.shell_command = args.command;
7441
+ }
7442
+ if (toolName.includes("db") || toolName.includes("sql") || toolName.includes("query")) {
7443
+ signals.is_db_operation = true;
7444
+ if (args.query || args.sql) signals.sql_query = args.query || args.sql;
7445
+ }
7446
+ if (toolName.includes("fetch") || toolName.includes("http") || toolName.includes("request")) {
7447
+ signals.is_network_operation = true;
7448
+ if (args.url) signals.target_url = args.url;
7449
+ }
7450
+ return signals;
7451
+ }
7452
+ sendBlockedResponse(request, decision) {
7453
+ const response = {
7454
+ jsonrpc: "2.0",
7455
+ id: request.id,
7456
+ error: {
7457
+ code: -32001,
7458
+ message: `[SOVR] Action blocked by policy: ${decision.reason}`,
7459
+ data: {
7460
+ sovr_decision_id: decision.decision_id,
7461
+ sovr_verdict: decision.verdict,
7462
+ sovr_risk_score: decision.risk_score,
7463
+ sovr_matched_rules: decision.matched_rules
7464
+ }
7465
+ }
7466
+ };
7467
+ process.stdout.write(JSON.stringify(response) + "\n");
7468
+ }
7469
+ sendEscalatedResponse(request, decision) {
7470
+ const response = {
7471
+ jsonrpc: "2.0",
7472
+ id: request.id,
7473
+ error: {
7474
+ code: -32002,
7475
+ message: `[SOVR] Action requires human approval: ${decision.reason}`,
7476
+ data: {
7477
+ sovr_decision_id: decision.decision_id,
7478
+ sovr_verdict: decision.verdict,
7479
+ sovr_risk_score: decision.risk_score,
7480
+ sovr_matched_rules: decision.matched_rules,
7481
+ sovr_requires_approval: true
7482
+ }
7483
+ }
7484
+ };
7485
+ process.stdout.write(JSON.stringify(response) + "\n");
7486
+ }
7487
+ forwardToUpstream(data) {
7488
+ if (!this.upstream?.stdin) {
7489
+ process.stderr.write("[sovr-mcp-proxy] ERROR: upstream not connected\n");
7490
+ this._stats.upstreamErrors++;
7491
+ return;
7492
+ }
7493
+ this.upstream.stdin.write(data + "\n");
7494
+ }
7495
+ };
7496
+ async function proxyCli(args) {
7497
+ let upstreamCmd = "";
7498
+ let upstreamArgs = [];
7499
+ let rulesFile = null;
7500
+ let verbose = false;
7501
+ for (let i = 0; i < args.length; i++) {
7502
+ switch (args[i]) {
7503
+ case "--upstream":
7504
+ case "-u": {
7505
+ const parts = (args[++i] ?? "").split(" ");
7506
+ upstreamCmd = parts[0];
7507
+ upstreamArgs = parts.slice(1);
7508
+ break;
7509
+ }
7510
+ case "--rules":
7511
+ case "-r":
7512
+ rulesFile = args[++i];
7513
+ break;
7514
+ case "--verbose":
7515
+ case "-v":
7516
+ verbose = true;
7517
+ break;
7518
+ }
7519
+ }
7520
+ if (!upstreamCmd) {
7521
+ return main();
7522
+ }
7523
+ let customRules = [];
7524
+ if (rulesFile) {
7525
+ const fs = await import("fs");
7526
+ const content = fs.readFileSync(rulesFile, "utf-8");
7527
+ const parsed = JSON.parse(content);
7528
+ customRules = parsed.rules ?? parsed;
7529
+ }
7530
+ const proxy = new McpProxy({
7531
+ upstream: { command: upstreamCmd, args: upstreamArgs },
7532
+ customRules,
7533
+ verbose,
7534
+ onBlocked: (info) => {
7535
+ process.stderr.write(
7536
+ `[BLOCKED] ${info.toolName}: ${info.decision.reason}
7537
+ `
7538
+ );
7539
+ },
7540
+ onEscalated: (info) => {
7541
+ process.stderr.write(
7542
+ `[ESCALATED] ${info.toolName}: ${info.decision.reason}
7543
+ `
7544
+ );
7545
+ }
7546
+ });
7547
+ await proxy.start();
7233
7548
  }
7234
7549
  export {
7550
+ McpProxy,
7235
7551
  TOOLS,
7236
7552
  VERSION,
7237
7553
  auditLog,
@@ -7244,6 +7560,7 @@ export {
7244
7560
  main,
7245
7561
  parseCommand,
7246
7562
  parseSQL,
7563
+ proxyCli,
7247
7564
  proxyEnabled,
7248
7565
  proxyToolCall,
7249
7566
  proxyToolMap,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sovr-mcp-proxy",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Transparent MCP Proxy that intercepts all agent tool calls with policy gate-check before forwarding. Superset of sovr-mcp-server.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "mcpName": "io.github.xie38388/sovr-proxy",
44
44
  "devDependencies": {
45
+ "@types/node": "^25.3.0",
45
46
  "tsup": "^8.5.1",
46
47
  "tsx": "^4.0.0",
47
48
  "typescript": "^5.9.3"