sovr-mcp-proxy 7.0.0 → 7.2.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.
@@ -0,0 +1,579 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/mcpProxyInterceptor.ts
21
+ var mcpProxyInterceptor_exports = {};
22
+ __export(mcpProxyInterceptor_exports, {
23
+ McpProxyInterceptor: () => McpProxyInterceptor,
24
+ PRESET_POLICIES: () => PRESET_POLICIES,
25
+ createSecureInterceptor: () => createSecureInterceptor
26
+ });
27
+ module.exports = __toCommonJS(mcpProxyInterceptor_exports);
28
+ var SlidingWindowRateLimiter = class {
29
+ windows = /* @__PURE__ */ new Map();
30
+ windowSizeMs = 6e4;
31
+ // 1 minute
32
+ check(key, limit) {
33
+ const now = Date.now();
34
+ const cutoff = now - this.windowSizeMs;
35
+ const timestamps = this.windows.get(key) || [];
36
+ const recent = timestamps.filter((t) => t > cutoff);
37
+ this.windows.set(key, recent);
38
+ return recent.length < limit;
39
+ }
40
+ record(key) {
41
+ const timestamps = this.windows.get(key) || [];
42
+ timestamps.push(Date.now());
43
+ this.windows.set(key, timestamps);
44
+ }
45
+ getCount(key) {
46
+ const now = Date.now();
47
+ const cutoff = now - this.windowSizeMs;
48
+ const timestamps = this.windows.get(key) || [];
49
+ return timestamps.filter((t) => t > cutoff).length;
50
+ }
51
+ cleanup() {
52
+ const now = Date.now();
53
+ const cutoff = now - this.windowSizeMs;
54
+ for (const [key, timestamps] of this.windows) {
55
+ const recent = timestamps.filter((t) => t > cutoff);
56
+ if (recent.length === 0) {
57
+ this.windows.delete(key);
58
+ } else {
59
+ this.windows.set(key, recent);
60
+ }
61
+ }
62
+ }
63
+ };
64
+ function matchGlob(pattern, text) {
65
+ const regex = new RegExp(
66
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
67
+ );
68
+ return regex.test(text);
69
+ }
70
+ function evaluateCondition(condition, args) {
71
+ const value = getNestedValue(args, condition.field);
72
+ switch (condition.operator) {
73
+ case "exists":
74
+ return value !== void 0 && value !== null;
75
+ case "not-exists":
76
+ return value === void 0 || value === null;
77
+ case "equals":
78
+ return value === condition.value;
79
+ case "contains":
80
+ return typeof value === "string" && value.includes(String(condition.value));
81
+ case "matches":
82
+ return typeof value === "string" && new RegExp(String(condition.value)).test(value);
83
+ case "gt":
84
+ return typeof value === "number" && value > Number(condition.value);
85
+ case "lt":
86
+ return typeof value === "number" && value < Number(condition.value);
87
+ default:
88
+ return false;
89
+ }
90
+ }
91
+ function getNestedValue(obj, path) {
92
+ return path.split(".").reduce((current, key) => {
93
+ if (current && typeof current === "object" && key in current) {
94
+ return current[key];
95
+ }
96
+ return void 0;
97
+ }, obj);
98
+ }
99
+ function applyTransform(args, transform) {
100
+ const result = JSON.parse(JSON.stringify(args));
101
+ const parts = transform.field.split(".");
102
+ const lastKey = parts.pop();
103
+ let target = result;
104
+ for (const part of parts) {
105
+ if (target[part] && typeof target[part] === "object") {
106
+ target = target[part];
107
+ } else {
108
+ return result;
109
+ }
110
+ }
111
+ switch (transform.type) {
112
+ case "redact":
113
+ target[lastKey] = "[REDACTED]";
114
+ break;
115
+ case "mask":
116
+ if (typeof target[lastKey] === "string") {
117
+ const val = target[lastKey];
118
+ target[lastKey] = val.slice(0, 2) + "*".repeat(Math.max(val.length - 4, 0)) + val.slice(-2);
119
+ }
120
+ break;
121
+ case "replace":
122
+ target[lastKey] = transform.replacement ?? "";
123
+ break;
124
+ case "remove":
125
+ delete target[lastKey];
126
+ break;
127
+ }
128
+ return result;
129
+ }
130
+ var HIGH_RISK_TOOLS = /* @__PURE__ */ new Set([
131
+ "bash",
132
+ "shell",
133
+ "exec",
134
+ "run_command",
135
+ "execute",
136
+ "write_file",
137
+ "delete_file",
138
+ "move_file",
139
+ "sql_query",
140
+ "database_query",
141
+ "send_email",
142
+ "send_message",
143
+ "deploy",
144
+ "publish",
145
+ "payment",
146
+ "transfer",
147
+ "checkout"
148
+ ]);
149
+ var DANGEROUS_ARG_PATTERNS = [
150
+ /rm\s+-rf/i,
151
+ /DROP\s+TABLE/i,
152
+ /DELETE\s+FROM/i,
153
+ /TRUNCATE/i,
154
+ /sudo\s+/i,
155
+ /chmod\s+777/i,
156
+ /curl\s+.*\|\s*sh/i,
157
+ /eval\s*\(/i,
158
+ /exec\s*\(/i,
159
+ /password|secret|token|api.?key/i
160
+ ];
161
+ function calculateRiskScore(toolName, args) {
162
+ let score = 0;
163
+ if (HIGH_RISK_TOOLS.has(toolName.toLowerCase())) {
164
+ score += 30;
165
+ }
166
+ const argsStr = JSON.stringify(args);
167
+ for (const pattern of DANGEROUS_ARG_PATTERNS) {
168
+ if (pattern.test(argsStr)) {
169
+ score += 20;
170
+ }
171
+ }
172
+ if (argsStr.length > 5e3) {
173
+ score += 10;
174
+ }
175
+ const pipeCount = (argsStr.match(/\|/g) || []).length;
176
+ if (pipeCount > 2) {
177
+ score += 15;
178
+ }
179
+ return Math.min(score, 100);
180
+ }
181
+ var McpProxyInterceptor = class {
182
+ config;
183
+ rateLimiter;
184
+ stats;
185
+ auditLog = [];
186
+ maxAuditLogSize = 1e4;
187
+ activeCalls = 0;
188
+ cleanupInterval = null;
189
+ constructor(config = {}) {
190
+ this.config = {
191
+ mode: config.mode ?? "block",
192
+ maxConcurrent: config.maxConcurrent ?? 50,
193
+ rateLimits: config.rateLimits ?? {},
194
+ globalRateLimit: config.globalRateLimit ?? 200,
195
+ toolPolicies: config.toolPolicies ?? [],
196
+ enableSemanticAnalysis: config.enableSemanticAnalysis ?? false,
197
+ onApprovalRequired: config.onApprovalRequired,
198
+ onAuditEvent: config.onAuditEvent,
199
+ upstreamTimeout: config.upstreamTimeout ?? 3e4,
200
+ maxRetries: config.maxRetries ?? 2
201
+ };
202
+ this.rateLimiter = new SlidingWindowRateLimiter();
203
+ this.stats = {
204
+ totalCalls: 0,
205
+ allowed: 0,
206
+ blocked: 0,
207
+ approvalRequested: 0,
208
+ rateLimited: 0,
209
+ errors: 0,
210
+ avgLatencyMs: 0,
211
+ callsByTool: {},
212
+ blocksByPolicy: {},
213
+ riskDistribution: { safe: 0, suspicious: 0, dangerous: 0, critical: 0 }
214
+ };
215
+ this.cleanupInterval = setInterval(() => {
216
+ this.rateLimiter.cleanup();
217
+ this.trimAuditLog();
218
+ }, 6e4);
219
+ }
220
+ /**
221
+ * Intercept a tool call — the core entry point.
222
+ * Returns the decision and optionally transformed arguments.
223
+ */
224
+ async intercept(toolName, args) {
225
+ const startTime = Date.now();
226
+ const callId = `ic_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
227
+ this.stats.totalCalls++;
228
+ this.stats.callsByTool[toolName] = (this.stats.callsByTool[toolName] || 0) + 1;
229
+ if (this.activeCalls >= this.config.maxConcurrent) {
230
+ const decision2 = {
231
+ action: "block",
232
+ reason: `Concurrency limit exceeded (${this.config.maxConcurrent})`,
233
+ riskScore: 0
234
+ };
235
+ this.stats.blocked++;
236
+ this.emitAudit(callId, "block", toolName, args, decision2, Date.now() - startTime);
237
+ return { id: callId, timestamp: startTime, toolName, arguments: args, matchedPolicies: [], decision: decision2 };
238
+ }
239
+ if (!this.rateLimiter.check("__global__", this.config.globalRateLimit)) {
240
+ const decision2 = {
241
+ action: "block",
242
+ reason: `Global rate limit exceeded (${this.config.globalRateLimit}/min)`,
243
+ riskScore: 0
244
+ };
245
+ this.stats.rateLimited++;
246
+ this.emitAudit(callId, "rate-limited", toolName, args, decision2, Date.now() - startTime);
247
+ return { id: callId, timestamp: startTime, toolName, arguments: args, matchedPolicies: [], decision: decision2 };
248
+ }
249
+ const toolLimit = this.config.rateLimits[toolName];
250
+ if (toolLimit && !this.rateLimiter.check(`tool:${toolName}`, toolLimit)) {
251
+ const decision2 = {
252
+ action: "block",
253
+ reason: `Tool rate limit exceeded for '${toolName}' (${toolLimit}/min)`,
254
+ riskScore: 0
255
+ };
256
+ this.stats.rateLimited++;
257
+ this.emitAudit(callId, "rate-limited", toolName, args, decision2, Date.now() - startTime);
258
+ return { id: callId, timestamp: startTime, toolName, arguments: args, matchedPolicies: [], decision: decision2 };
259
+ }
260
+ const matchedPolicies = this.config.toolPolicies.filter((p) => matchGlob(p.toolPattern, toolName)).filter((p) => {
261
+ if (!p.conditions || p.conditions.length === 0) return true;
262
+ return p.conditions.every((c) => evaluateCondition(c, args));
263
+ }).sort((a, b) => b.priority - a.priority);
264
+ const riskScore = calculateRiskScore(toolName, args);
265
+ let decision;
266
+ if (matchedPolicies.length > 0) {
267
+ const topPolicy = matchedPolicies[0];
268
+ switch (topPolicy.action) {
269
+ case "block":
270
+ decision = {
271
+ action: "block",
272
+ reason: topPolicy.description,
273
+ triggeringPolicy: topPolicy,
274
+ riskScore
275
+ };
276
+ this.stats.blocked++;
277
+ this.stats.blocksByPolicy[topPolicy.description] = (this.stats.blocksByPolicy[topPolicy.description] || 0) + 1;
278
+ break;
279
+ case "require-approval":
280
+ decision = {
281
+ action: "require-approval",
282
+ reason: topPolicy.description,
283
+ triggeringPolicy: topPolicy,
284
+ riskScore
285
+ };
286
+ this.stats.approvalRequested++;
287
+ if (this.config.onApprovalRequired) {
288
+ const call = {
289
+ id: callId,
290
+ timestamp: startTime,
291
+ toolName,
292
+ arguments: args,
293
+ matchedPolicies,
294
+ decision
295
+ };
296
+ try {
297
+ const approval = await this.config.onApprovalRequired(call);
298
+ if (approval.approved) {
299
+ decision = { action: "allow", reason: `Approved by ${approval.approver || "human"}`, riskScore };
300
+ this.emitAudit(callId, "approval-granted", toolName, args, decision, Date.now() - startTime);
301
+ } else {
302
+ decision = { action: "block", reason: `Denied by ${approval.approver || "human"}: ${approval.reason || "no reason"}`, riskScore };
303
+ this.emitAudit(callId, "approval-denied", toolName, args, decision, Date.now() - startTime);
304
+ }
305
+ } catch (err) {
306
+ decision = { action: "block", reason: "Approval system unavailable (fail-closed)", riskScore };
307
+ this.stats.errors++;
308
+ }
309
+ }
310
+ break;
311
+ case "transform":
312
+ let transformed = { ...args };
313
+ for (const transform of topPolicy.transforms || []) {
314
+ transformed = applyTransform(transformed, transform);
315
+ }
316
+ decision = {
317
+ action: "transform",
318
+ reason: topPolicy.description,
319
+ triggeringPolicy: topPolicy,
320
+ riskScore,
321
+ transformedArguments: transformed
322
+ };
323
+ this.stats.allowed++;
324
+ break;
325
+ case "allow":
326
+ default:
327
+ decision = {
328
+ action: "allow",
329
+ reason: topPolicy.description,
330
+ triggeringPolicy: topPolicy,
331
+ riskScore
332
+ };
333
+ this.stats.allowed++;
334
+ break;
335
+ }
336
+ } else {
337
+ switch (this.config.mode) {
338
+ case "block":
339
+ decision = {
340
+ action: "block",
341
+ reason: "No matching policy (default deny)",
342
+ riskScore
343
+ };
344
+ this.stats.blocked++;
345
+ break;
346
+ case "warn":
347
+ decision = {
348
+ action: "allow",
349
+ reason: "No matching policy (warn mode \u2014 allowed with warning)",
350
+ riskScore
351
+ };
352
+ this.stats.allowed++;
353
+ break;
354
+ case "audit":
355
+ decision = {
356
+ action: "allow",
357
+ reason: "No matching policy (audit mode \u2014 allowed, logged)",
358
+ riskScore
359
+ };
360
+ this.stats.allowed++;
361
+ break;
362
+ }
363
+ }
364
+ this.rateLimiter.record("__global__");
365
+ this.rateLimiter.record(`tool:${toolName}`);
366
+ if (riskScore < 25) this.stats.riskDistribution.safe++;
367
+ else if (riskScore < 50) this.stats.riskDistribution.suspicious++;
368
+ else if (riskScore < 75) this.stats.riskDistribution.dangerous++;
369
+ else this.stats.riskDistribution.critical++;
370
+ const duration = Date.now() - startTime;
371
+ this.stats.avgLatencyMs = (this.stats.avgLatencyMs * (this.stats.totalCalls - 1) + duration) / this.stats.totalCalls;
372
+ const eventType = decision.action === "allow" || decision.action === "transform" ? "allow" : "block";
373
+ this.emitAudit(callId, eventType, toolName, args, decision, duration);
374
+ return {
375
+ id: callId,
376
+ timestamp: startTime,
377
+ toolName,
378
+ arguments: args,
379
+ matchedPolicies,
380
+ decision
381
+ };
382
+ }
383
+ /**
384
+ * Wrap an upstream tool call with interception.
385
+ * If allowed, executes the upstream function; if blocked, returns error.
386
+ */
387
+ async wrapToolCall(toolName, args, upstream) {
388
+ const intercepted = await this.intercept(toolName, args);
389
+ if (intercepted.decision.action === "block" || intercepted.decision.action === "require-approval") {
390
+ return {
391
+ intercepted,
392
+ error: `SOVR blocked: ${intercepted.decision.reason}`
393
+ };
394
+ }
395
+ const finalArgs = intercepted.decision.transformedArguments || args;
396
+ this.activeCalls++;
397
+ try {
398
+ const result = await Promise.race([
399
+ upstream(finalArgs),
400
+ new Promise(
401
+ (_, reject) => setTimeout(() => reject(new Error("Upstream timeout")), this.config.upstreamTimeout)
402
+ )
403
+ ]);
404
+ return { result, intercepted };
405
+ } catch (err) {
406
+ this.stats.errors++;
407
+ const errorMsg = err instanceof Error ? err.message : String(err);
408
+ this.emitAudit(
409
+ intercepted.id,
410
+ "error",
411
+ toolName,
412
+ args,
413
+ intercepted.decision,
414
+ 0,
415
+ errorMsg
416
+ );
417
+ return { intercepted, error: errorMsg };
418
+ } finally {
419
+ this.activeCalls--;
420
+ }
421
+ }
422
+ /** Get current statistics */
423
+ getStats() {
424
+ return { ...this.stats };
425
+ }
426
+ /** Get recent audit log */
427
+ getAuditLog(limit = 100) {
428
+ return this.auditLog.slice(-limit);
429
+ }
430
+ /** Export audit log as CSV */
431
+ exportAuditCSV() {
432
+ const headers = ["id", "timestamp", "type", "toolName", "decision", "riskScore", "reason", "duration", "error"];
433
+ const rows = this.auditLog.map((e) => [
434
+ e.id,
435
+ new Date(e.timestamp).toISOString(),
436
+ e.type,
437
+ e.toolName,
438
+ e.decision.action,
439
+ e.decision.riskScore,
440
+ `"${e.decision.reason.replace(/"/g, '""')}"`,
441
+ e.duration ?? "",
442
+ e.error ? `"${e.error.replace(/"/g, '""')}"` : ""
443
+ ].join(","));
444
+ return [headers.join(","), ...rows].join("\n");
445
+ }
446
+ /** Add a policy at runtime */
447
+ addPolicy(policy) {
448
+ this.config.toolPolicies.push(policy);
449
+ this.config.toolPolicies.sort((a, b) => b.priority - a.priority);
450
+ }
451
+ /** Remove policies matching a pattern */
452
+ removePolicies(toolPattern) {
453
+ const before = this.config.toolPolicies.length;
454
+ this.config.toolPolicies = this.config.toolPolicies.filter((p) => p.toolPattern !== toolPattern);
455
+ return before - this.config.toolPolicies.length;
456
+ }
457
+ /** Reset statistics */
458
+ resetStats() {
459
+ this.stats = {
460
+ totalCalls: 0,
461
+ allowed: 0,
462
+ blocked: 0,
463
+ approvalRequested: 0,
464
+ rateLimited: 0,
465
+ errors: 0,
466
+ avgLatencyMs: 0,
467
+ callsByTool: {},
468
+ blocksByPolicy: {},
469
+ riskDistribution: { safe: 0, suspicious: 0, dangerous: 0, critical: 0 }
470
+ };
471
+ }
472
+ /** Cleanup resources */
473
+ destroy() {
474
+ if (this.cleanupInterval) {
475
+ clearInterval(this.cleanupInterval);
476
+ this.cleanupInterval = null;
477
+ }
478
+ }
479
+ // ─── Private ─────────────────────────────────────────────────────────────
480
+ emitAudit(id, type, toolName, args, decision, duration, error) {
481
+ const event = {
482
+ id,
483
+ timestamp: Date.now(),
484
+ type,
485
+ toolName,
486
+ arguments: args,
487
+ decision,
488
+ duration,
489
+ error
490
+ };
491
+ this.auditLog.push(event);
492
+ this.config.onAuditEvent?.(event);
493
+ }
494
+ trimAuditLog() {
495
+ if (this.auditLog.length > this.maxAuditLogSize) {
496
+ this.auditLog = this.auditLog.slice(-this.maxAuditLogSize);
497
+ }
498
+ }
499
+ };
500
+ var PRESET_POLICIES = {
501
+ /** Block all destructive file operations */
502
+ noDestructiveFiles: {
503
+ toolPattern: "*",
504
+ action: "block",
505
+ conditions: [
506
+ { field: "command", operator: "matches", value: "rm\\s+-rf|rmdir|del\\s+/|Remove-Item" }
507
+ ],
508
+ priority: 100,
509
+ description: "Block destructive file deletion commands"
510
+ },
511
+ /** Block all database write operations */
512
+ readOnlyDatabase: {
513
+ toolPattern: "*sql*",
514
+ action: "block",
515
+ conditions: [
516
+ { field: "query", operator: "matches", value: "INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE" }
517
+ ],
518
+ priority: 90,
519
+ description: "Block database write operations (read-only mode)"
520
+ },
521
+ /** Require approval for payment operations */
522
+ approvePayments: {
523
+ toolPattern: "*payment*",
524
+ action: "require-approval",
525
+ priority: 95,
526
+ description: "Require human approval for payment operations"
527
+ },
528
+ /** Redact secrets in tool arguments */
529
+ redactSecrets: {
530
+ toolPattern: "*",
531
+ action: "transform",
532
+ conditions: [
533
+ { field: "command", operator: "matches", value: "password|secret|token|api.?key" }
534
+ ],
535
+ transforms: [
536
+ { field: "password", type: "redact" },
537
+ { field: "secret", type: "redact" },
538
+ { field: "token", type: "mask" }
539
+ ],
540
+ priority: 80,
541
+ description: "Redact sensitive values in tool arguments"
542
+ },
543
+ /** Rate limit shell commands */
544
+ rateLimitShell: {
545
+ toolPattern: "bash",
546
+ action: "rate-limit",
547
+ priority: 70,
548
+ description: "Rate limit shell command execution"
549
+ },
550
+ /** Allow read-only operations */
551
+ allowReadOnly: {
552
+ toolPattern: "*",
553
+ action: "allow",
554
+ conditions: [
555
+ { field: "command", operator: "matches", value: "^(ls|cat|head|tail|grep|find|echo|pwd|whoami|date|wc)\\b" }
556
+ ],
557
+ priority: 60,
558
+ description: "Allow read-only shell commands"
559
+ }
560
+ };
561
+ function createSecureInterceptor(overrides = {}) {
562
+ return new McpProxyInterceptor({
563
+ mode: "block",
564
+ maxConcurrent: 20,
565
+ globalRateLimit: 100,
566
+ rateLimits: { bash: 30, shell: 30, exec: 30 },
567
+ toolPolicies: Object.values(PRESET_POLICIES),
568
+ enableSemanticAnalysis: false,
569
+ upstreamTimeout: 3e4,
570
+ maxRetries: 2,
571
+ ...overrides
572
+ });
573
+ }
574
+ // Annotate the CommonJS export names for ESM import in node:
575
+ 0 && (module.exports = {
576
+ McpProxyInterceptor,
577
+ PRESET_POLICIES,
578
+ createSecureInterceptor
579
+ });