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.
- package/dist/index.d.ts +251 -0
- package/dist/index.js +332 -15
- package/package.json +2 -1
package/dist/index.d.ts
ADDED
|
@@ -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 = "
|
|
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
|
|
402
|
+
const rawTier = (data.tier || data.plan || "free").toLowerCase();
|
|
400
403
|
const validTiers = ["free", "personal", "starter", "pro", "enterprise"];
|
|
401
|
-
if (validTiers.includes(
|
|
402
|
-
if (
|
|
403
|
-
if (
|
|
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-
|
|
7125
|
-
|
|
7126
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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": "
|
|
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"
|