contextguard 0.1.7 → 0.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.
Files changed (60) hide show
  1. package/LICENSE +23 -17
  2. package/README.md +157 -109
  3. package/dist/agent.d.ts +24 -0
  4. package/dist/agent.js +369 -0
  5. package/dist/cli.d.ts +11 -0
  6. package/dist/cli.js +266 -0
  7. package/dist/config.d.ts +23 -0
  8. package/dist/config.js +56 -0
  9. package/dist/database.d.ts +116 -0
  10. package/dist/database.js +291 -0
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.js +18 -0
  13. package/dist/init.d.ts +7 -0
  14. package/dist/init.js +173 -0
  15. package/dist/lib/supabase-client.d.ts +27 -0
  16. package/dist/lib/supabase-client.js +97 -0
  17. package/dist/logger.d.ts +36 -0
  18. package/dist/logger.js +145 -0
  19. package/dist/mcp-security-wrapper.d.ts +84 -0
  20. package/dist/mcp-security-wrapper.js +394 -120
  21. package/dist/mcp-traceability-integration.d.ts +118 -0
  22. package/dist/mcp-traceability-integration.js +302 -0
  23. package/dist/policy.d.ts +30 -0
  24. package/dist/policy.js +273 -0
  25. package/dist/premium-features.d.ts +364 -0
  26. package/dist/premium-features.js +950 -0
  27. package/dist/security-logger.d.ts +45 -0
  28. package/dist/security-logger.js +125 -0
  29. package/dist/security-policy.d.ts +55 -0
  30. package/dist/security-policy.js +140 -0
  31. package/dist/semantic-detector.d.ts +21 -0
  32. package/dist/semantic-detector.js +49 -0
  33. package/dist/sse-proxy.d.ts +21 -0
  34. package/dist/sse-proxy.js +276 -0
  35. package/dist/supabase-client.d.ts +27 -0
  36. package/dist/supabase-client.js +89 -0
  37. package/dist/types/database.types.d.ts +220 -0
  38. package/dist/types/database.types.js +8 -0
  39. package/dist/types/mcp.d.ts +27 -0
  40. package/dist/types/mcp.js +15 -0
  41. package/dist/types/types.d.ts +65 -0
  42. package/dist/types/types.js +8 -0
  43. package/dist/types.d.ts +84 -0
  44. package/dist/types.js +8 -0
  45. package/dist/wrapper.d.ts +115 -0
  46. package/dist/wrapper.js +417 -0
  47. package/package.json +35 -10
  48. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -57
  49. package/CONTRIBUTING.md +0 -532
  50. package/SECURITY.md +0 -254
  51. package/assets/demo.mp4 +0 -0
  52. package/eslint.config.mts +0 -23
  53. package/examples/config/config.json +0 -19
  54. package/examples/mcp-server/demo.js +0 -228
  55. package/examples/mcp-server/package-lock.json +0 -978
  56. package/examples/mcp-server/package.json +0 -16
  57. package/examples/mcp-server/pnpm-lock.yaml +0 -745
  58. package/src/mcp-security-wrapper.ts +0 -529
  59. package/test/test-server.ts +0 -295
  60. package/tsconfig.json +0 -16
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * Copyright (c) 2025 Amir Mironi
4
+ * Copyright (c) 2026 Amir Mironi
5
5
  *
6
6
  * This source code is licensed under the MIT license found in the
7
7
  * LICENSE file in the root directory of this source tree.
@@ -40,31 +40,40 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  };
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.SecurityLogger = exports.SecurityPolicy = exports.MCPSecurityWrapper = void 0;
43
+ exports.SecurityLogger = exports.SecurityPolicy = exports.MCPSSEProxy = exports.MCPSecurityWrapper = void 0;
44
44
  const child_process_1 = require("child_process");
45
45
  const fs = __importStar(require("fs"));
46
+ const http = __importStar(require("http"));
47
+ const https = __importStar(require("https"));
46
48
  const crypto_1 = require("crypto");
49
+ // ─── Security Policy ─────────────────────────────────────────────────────────
47
50
  class SecurityPolicy {
48
51
  constructor(config) {
49
52
  this.config = {
50
53
  maxToolCallsPerMinute: 30,
54
+ enablePromptInjectionDetection: true,
55
+ enableSensitiveDataDetection: true,
56
+ enablePathTraversalPrevention: true,
57
+ mode: "monitor", // Safe default — just log, don't block
51
58
  blockedPatterns: [],
52
59
  allowedFilePaths: [],
53
60
  alertThreshold: 5,
54
- enablePromptInjectionDetection: true,
55
- enableSensitiveDataDetection: true,
61
+ logPath: "./mcp_security.log",
62
+ logLevel: "info",
63
+ transport: "stdio",
64
+ port: 3100,
65
+ targetUrl: "",
56
66
  ...config,
57
67
  };
58
- // Sensitive data patterns
59
68
  this.sensitiveDataPatterns = [
60
69
  /(?:password|secret|api[_-]?key|token)\s*[:=]\s*['"]?[\w\-.]+['"]?/gi,
61
- /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Email
62
- /\b\d{3}-\d{2}-\d{4}\b/g, // SSN
63
- /sk-[a-zA-Z0-9]{20,}/g, // OpenAI API keys (20+ chars)
64
- /ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
65
- /AKIA[0-9A-Z]{16}/g, // AWS Access Keys
70
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
71
+ /\b\d{3}-\d{2}-\d{4}\b/g,
72
+ /sk-[a-zA-Z0-9]{20,}/g,
73
+ /ghp_[a-zA-Z0-9]{36}/g,
74
+ /AKIA[0-9A-Z]{16}/g,
75
+ /sk_(live|test)_[a-zA-Z0-9]{24,}/g,
66
76
  ];
67
- // Prompt injection patterns
68
77
  this.promptInjectionPatterns = [
69
78
  /ignore\s+(previous|all)\s+(instructions|prompts)/gi,
70
79
  /system:\s*you\s+are\s+now/gi,
@@ -76,6 +85,12 @@ class SecurityPolicy {
76
85
  /override\s+previous/gi,
77
86
  ];
78
87
  }
88
+ get isBlockingMode() {
89
+ return this.config.mode === "block";
90
+ }
91
+ get rawConfig() {
92
+ return this.config;
93
+ }
79
94
  checkPromptInjection(text) {
80
95
  if (!this.config.enablePromptInjectionDetection)
81
96
  return [];
@@ -101,6 +116,8 @@ class SecurityPolicy {
101
116
  return violations;
102
117
  }
103
118
  checkFileAccess(filePath) {
119
+ if (!this.config.enablePathTraversalPrevention)
120
+ return [];
104
121
  const violations = [];
105
122
  if (filePath.includes("..")) {
106
123
  violations.push(`Path traversal attempt detected: ${filePath}`);
@@ -112,11 +129,10 @@ class SecurityPolicy {
112
129
  "/proc",
113
130
  "C:\\Windows\\System32",
114
131
  ];
115
- if (dangerousPaths.some((dangerous) => filePath.startsWith(dangerous))) {
132
+ if (dangerousPaths.some((d) => filePath.startsWith(d))) {
116
133
  violations.push(`Access to dangerous path detected: ${filePath}`);
117
134
  }
118
- if (this.config.allowedFilePaths &&
119
- this.config.allowedFilePaths.length > 0) {
135
+ if (this.config.allowedFilePaths.length > 0) {
120
136
  const isAllowed = this.config.allowedFilePaths.some((allowed) => filePath.startsWith(allowed));
121
137
  if (!isAllowed) {
122
138
  violations.push(`File path not in allowed list: ${filePath}`);
@@ -127,12 +143,13 @@ class SecurityPolicy {
127
143
  checkRateLimit(timestamps) {
128
144
  const oneMinuteAgo = Date.now() - 60000;
129
145
  const recentCalls = timestamps.filter((t) => t > oneMinuteAgo);
130
- return recentCalls.length < (this.config.maxToolCallsPerMinute || 30);
146
+ return recentCalls.length < this.config.maxToolCallsPerMinute;
131
147
  }
132
148
  }
133
149
  exports.SecurityPolicy = SecurityPolicy;
150
+ // ─── Security Logger ──────────────────────────────────────────────────────────
134
151
  class SecurityLogger {
135
- constructor(logFile = "mcp_security.log") {
152
+ constructor(logFile = "./mcp_security.log") {
136
153
  this.events = [];
137
154
  this.logFile = logFile;
138
155
  }
@@ -168,6 +185,15 @@ class SecurityLogger {
168
185
  }
169
186
  }
170
187
  exports.SecurityLogger = SecurityLogger;
188
+ // ─── Violation Handler (respects monitor vs block mode) ───────────────────────
189
+ function buildErrorResponse(msg, code, message, violations) {
190
+ return (JSON.stringify({
191
+ jsonrpc: msg.jsonrpc,
192
+ id: msg.id,
193
+ error: { code, message, data: { violations } },
194
+ }) + "\n");
195
+ }
196
+ // ─── stdio Transport ─────────────────────────────────────────────────────────
171
197
  class MCPSecurityWrapper {
172
198
  constructor(serverCommand, policy, logger) {
173
199
  this.process = null;
@@ -192,31 +218,23 @@ class MCPSecurityWrapper {
192
218
  this.logger.logEvent("SERVER_START", "LOW", {
193
219
  command: this.serverCommand.join(" "),
194
220
  pid: this.process.pid,
221
+ mode: this.policy.rawConfig.mode,
195
222
  }, this.sessionId);
196
223
  this.process.stderr.pipe(process.stderr);
197
224
  this.process.stdout.on("data", (data) => {
198
- const output = data.toString();
199
- this.handleServerOutput(output);
225
+ this.handleServerOutput(data.toString());
200
226
  });
201
227
  process.stdin.on("data", (data) => {
202
- const input = data.toString();
203
- this.handleClientInput(input);
228
+ this.handleClientInput(data.toString());
204
229
  });
205
230
  this.process.on("exit", (code) => {
206
- this.logger.logEvent("SERVER_EXIT", "MEDIUM", {
207
- exitCode: code,
208
- }, this.sessionId);
231
+ this.logger.logEvent("SERVER_EXIT", "MEDIUM", { exitCode: code }, this.sessionId);
209
232
  console.error("\n=== MCP Security Statistics ===");
210
233
  console.error(JSON.stringify(this.logger.getStatistics(), null, 2));
211
- // Use setImmediate to allow pending I/O to complete
212
- setImmediate(() => {
213
- process.exit(code || 0);
214
- });
234
+ setImmediate(() => process.exit(code || 0));
215
235
  });
216
236
  this.process.on("error", (err) => {
217
- this.logger.logEvent("SERVER_ERROR", "HIGH", {
218
- error: err.message,
219
- }, this.sessionId);
237
+ this.logger.logEvent("SERVER_ERROR", "HIGH", { error: err.message }, this.sessionId);
220
238
  console.error("Server process error:", err);
221
239
  });
222
240
  }
@@ -225,9 +243,8 @@ class MCPSecurityWrapper {
225
243
  const lines = this.clientMessageBuffer.split("\n");
226
244
  this.clientMessageBuffer = lines.pop() || "";
227
245
  for (const line of lines) {
228
- if (line.trim()) {
246
+ if (line.trim())
229
247
  this.processClientMessage(line);
230
- }
231
248
  }
232
249
  }
233
250
  processClientMessage(line) {
@@ -237,157 +254,414 @@ class MCPSecurityWrapper {
237
254
  method: message.method,
238
255
  id: message.id,
239
256
  }, this.sessionId);
240
- const violations = [];
241
- let shouldBlock = false;
242
- if (message.method === "tools/call") {
243
- const now = Date.now();
244
- this.toolCallTimestamps.push(now);
245
- // Clean up old timestamps to prevent memory leak
246
- const oneMinuteAgo = now - 60000;
247
- this.toolCallTimestamps = this.toolCallTimestamps.filter((t) => t > oneMinuteAgo);
248
- if (!this.policy.checkRateLimit(this.toolCallTimestamps)) {
249
- violations.push("Rate limit exceeded for tool calls");
250
- shouldBlock = true;
251
- this.logger.logEvent("RATE_LIMIT_EXCEEDED", "HIGH", {
252
- method: message.method,
253
- toolName: message.params?.name,
254
- }, this.sessionId);
255
- }
256
- const paramsStr = JSON.stringify(message.params);
257
- const injectionViolations = this.policy.checkPromptInjection(paramsStr);
258
- violations.push(...injectionViolations);
259
- const sensitiveViolations = this.policy.checkSensitiveData(paramsStr);
260
- violations.push(...sensitiveViolations);
261
- // Check multiple possible file path parameter locations
262
- const filePathParams = [
263
- message.params?.arguments?.path,
264
- message.params?.arguments?.filePath,
265
- message.params?.arguments?.file,
266
- message.params?.arguments?.directory,
267
- message.params?.path,
268
- message.params?.filePath,
269
- ].filter((path) => typeof path === "string");
270
- for (const filePath of filePathParams) {
271
- const fileViolations = this.policy.checkFileAccess(filePath);
272
- violations.push(...fileViolations);
273
- }
274
- this.logger.logEvent("TOOL_CALL", violations.length > 0 ? "HIGH" : "LOW", {
275
- toolName: message.params?.name,
276
- hasViolations: violations.length > 0,
277
- violations,
278
- }, this.sessionId);
279
- }
257
+ const { violations, shouldBlock } = this.inspectOutbound(message);
280
258
  if (violations.length > 0) {
281
259
  this.logger.logEvent("SECURITY_VIOLATION", "CRITICAL", {
282
260
  violations,
283
- message: message,
284
261
  blocked: shouldBlock,
285
262
  }, this.sessionId);
286
263
  console.error(`\n⚠️ SECURITY VIOLATIONS DETECTED:\n${violations.join("\n")}\n`);
287
264
  if (shouldBlock) {
288
265
  console.error("🚫 REQUEST BLOCKED\n");
289
- // Send error response back to client
290
266
  if (message.id !== undefined) {
291
- const errorResponse = {
292
- jsonrpc: message.jsonrpc,
293
- id: message.id,
294
- error: {
295
- code: -32000,
296
- message: "Security violation: Request blocked",
297
- data: { violations },
298
- },
299
- };
300
- process.stdout.write(JSON.stringify(errorResponse) + "\n");
267
+ process.stdout.write(buildErrorResponse(message, -32000, "Security violation: Request blocked", violations));
301
268
  }
302
- return; // Don't forward to server
269
+ return;
303
270
  }
271
+ // monitor mode: log and warn but still forward
272
+ console.error('⚠️ MONITOR MODE — request forwarded (set mode: "block" to block)\n');
304
273
  }
305
- if (this.process && this.process.stdin) {
306
- this.process.stdin.write(line + "\n");
307
- }
274
+ this.process?.stdin?.write(line + "\n");
308
275
  }
309
276
  catch (err) {
310
277
  this.logger.logEvent("PARSE_ERROR", "MEDIUM", {
311
278
  error: err instanceof Error ? err.message : String(err),
312
279
  line: line.substring(0, 100),
313
280
  }, this.sessionId);
314
- console.error(`Failed to parse client message: ${err}`);
315
- // Forward unparseable messages
316
- if (this.process && this.process.stdin) {
317
- this.process.stdin.write(line + "\n");
281
+ this.process?.stdin?.write(line + "\n");
282
+ }
283
+ }
284
+ /** Returns violations + whether to block (block only if mode==="block") */
285
+ inspectOutbound(message) {
286
+ const violations = [];
287
+ let shouldBlock = false;
288
+ if (message.method === "tools/call") {
289
+ const now = Date.now();
290
+ this.toolCallTimestamps.push(now);
291
+ this.toolCallTimestamps = this.toolCallTimestamps.filter((t) => t > now - 60000);
292
+ if (!this.policy.checkRateLimit(this.toolCallTimestamps)) {
293
+ violations.push("Rate limit exceeded for tool calls");
294
+ shouldBlock = this.policy.isBlockingMode; // only hard-block in block mode
295
+ this.logger.logEvent("RATE_LIMIT_EXCEEDED", "HIGH", {
296
+ toolName: message.params?.name,
297
+ }, this.sessionId);
298
+ }
299
+ const paramsStr = JSON.stringify(message.params);
300
+ violations.push(...this.policy.checkPromptInjection(paramsStr));
301
+ violations.push(...this.policy.checkSensitiveData(paramsStr));
302
+ const filePathParams = [
303
+ message.params?.arguments?.path,
304
+ message.params?.arguments?.filePath,
305
+ message.params?.arguments?.file,
306
+ message.params?.arguments?.directory,
307
+ message.params?.path,
308
+ message.params?.filePath,
309
+ ].filter((p) => typeof p === "string");
310
+ for (const fp of filePathParams) {
311
+ violations.push(...this.policy.checkFileAccess(fp));
312
+ }
313
+ this.logger.logEvent("TOOL_CALL", violations.length > 0 ? "HIGH" : "LOW", {
314
+ toolName: message.params?.name,
315
+ hasViolations: violations.length > 0,
316
+ violations,
317
+ }, this.sessionId);
318
+ // In blocking mode, any violation blocks
319
+ if (violations.length > 0 && this.policy.isBlockingMode) {
320
+ shouldBlock = true;
318
321
  }
319
322
  }
323
+ return { violations, shouldBlock };
320
324
  }
321
325
  handleServerOutput(output) {
322
- // Forward output immediately
323
- process.stdout.write(output);
324
- // Buffer and parse for logging
325
326
  this.serverMessageBuffer += output;
326
327
  const lines = this.serverMessageBuffer.split("\n");
327
328
  this.serverMessageBuffer = lines.pop() || "";
328
329
  for (const line of lines) {
329
- if (line.trim()) {
330
- try {
331
- const message = JSON.parse(line);
330
+ if (!line.trim())
331
+ continue;
332
+ let shouldForward = true;
333
+ try {
334
+ const message = JSON.parse(line);
335
+ const violations = this.policy.checkSensitiveData(JSON.stringify(message.result || message));
336
+ if (violations.length > 0) {
337
+ this.logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
338
+ violations,
339
+ responseId: message.id,
340
+ }, this.sessionId);
341
+ console.error(`\n🚨 SENSITIVE DATA DETECTED IN RESPONSE:\n${violations.join("\n")}\n`);
342
+ if (this.policy.isBlockingMode) {
343
+ console.error("🚫 RESPONSE BLOCKED\n");
344
+ if (message.id !== undefined) {
345
+ process.stdout.write(buildErrorResponse(message, -32001, "Security violation: Response contains sensitive data", violations));
346
+ }
347
+ shouldForward = false;
348
+ }
349
+ else {
350
+ console.error("⚠️ MONITOR MODE — response forwarded\n");
351
+ }
352
+ }
353
+ else {
332
354
  this.logger.logEvent("SERVER_RESPONSE", "LOW", {
333
355
  id: message.id,
334
356
  hasError: !!message.error,
335
357
  }, this.sessionId);
336
358
  }
337
- catch (err) {
338
- // Log parse errors for server output
339
- this.logger.logEvent("SERVER_PARSE_ERROR", "LOW", {
340
- error: err instanceof Error ? err.message : String(err),
341
- line: line.substring(0, 100),
342
- }, this.sessionId);
343
- }
344
359
  }
360
+ catch (err) {
361
+ this.logger.logEvent("SERVER_PARSE_ERROR", "LOW", {
362
+ error: err instanceof Error ? err.message : String(err),
363
+ line: line.substring(0, 100),
364
+ }, this.sessionId);
365
+ }
366
+ if (shouldForward)
367
+ process.stdout.write(line + "\n");
345
368
  }
346
369
  }
347
370
  }
348
371
  exports.MCPSecurityWrapper = MCPSecurityWrapper;
372
+ // ─── SSE / HTTP Transport ────────────────────────────────────────────────────
373
+ /**
374
+ * SSE proxy: listens on `port`, forwards every request to `targetUrl`,
375
+ * scanning both request body (outbound) and response body (inbound).
376
+ *
377
+ * Supports:
378
+ * - Standard JSON-RPC over HTTP POST (MCP HTTP transport)
379
+ * - Server-Sent Events streams (MCP SSE transport)
380
+ */
381
+ class MCPSSEProxy {
382
+ constructor(policy, logger) {
383
+ this.toolCallTimestamps = [];
384
+ this.policy = policy;
385
+ this.logger = logger;
386
+ this.sessionId = (0, crypto_1.createHash)("md5")
387
+ .update(Date.now().toString())
388
+ .digest("hex")
389
+ .substring(0, 8);
390
+ }
391
+ start(port, targetUrl) {
392
+ const target = new URL(targetUrl);
393
+ const isHttps = target.protocol === "https:";
394
+ const server = http.createServer((clientReq, clientRes) => {
395
+ // ── 1. Collect request body ──────────────────────────────────────────
396
+ const chunks = [];
397
+ clientReq.on("data", (chunk) => chunks.push(chunk));
398
+ clientReq.on("end", () => {
399
+ const rawBody = Buffer.concat(chunks);
400
+ const bodyStr = rawBody.toString();
401
+ // ── 2. Inspect request ──────────────────────────────────────────────
402
+ let violations = [];
403
+ let shouldBlock = false;
404
+ if (bodyStr) {
405
+ try {
406
+ const msg = JSON.parse(bodyStr);
407
+ const result = this.inspectOutbound(msg);
408
+ violations = result.violations;
409
+ shouldBlock = result.shouldBlock;
410
+ if (violations.length > 0) {
411
+ this.logger.logEvent("SECURITY_VIOLATION", "CRITICAL", {
412
+ violations,
413
+ blocked: shouldBlock,
414
+ }, this.sessionId);
415
+ console.error(`\n⚠️ SECURITY VIOLATIONS DETECTED:\n${violations.join("\n")}\n`);
416
+ }
417
+ if (shouldBlock) {
418
+ console.error("🚫 REQUEST BLOCKED\n");
419
+ clientRes.writeHead(403, { "Content-Type": "application/json" });
420
+ clientRes.end(JSON.stringify({
421
+ jsonrpc: "2.0",
422
+ id: msg.id,
423
+ error: {
424
+ code: -32000,
425
+ message: "Security violation: Request blocked",
426
+ data: { violations },
427
+ },
428
+ }));
429
+ return;
430
+ }
431
+ }
432
+ catch {
433
+ // non-JSON body — forward as-is
434
+ }
435
+ }
436
+ // ── 3. Forward to upstream ──────────────────────────────────────────
437
+ const options = {
438
+ hostname: target.hostname,
439
+ port: target.port || (isHttps ? 443 : 80),
440
+ path: target.pathname + clientReq.url?.replace(/^[^?]*/, "") ||
441
+ target.pathname,
442
+ method: clientReq.method,
443
+ headers: {
444
+ ...clientReq.headers,
445
+ host: target.host,
446
+ },
447
+ };
448
+ const lib = isHttps ? https : http;
449
+ const proxyReq = lib.request(options, (proxyRes) => {
450
+ const isSSE = proxyRes.headers["content-type"]?.includes("text/event-stream");
451
+ clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
452
+ if (isSSE) {
453
+ // Stream SSE events, scanning each data line
454
+ let sseBuffer = "";
455
+ proxyRes.on("data", (chunk) => {
456
+ sseBuffer += chunk.toString();
457
+ const lines = sseBuffer.split("\n");
458
+ sseBuffer = lines.pop() || "";
459
+ for (const line of lines) {
460
+ if (line.startsWith("data:")) {
461
+ const data = line.slice(5).trim();
462
+ try {
463
+ const msg = JSON.parse(data);
464
+ const leakViolations = this.policy.checkSensitiveData(JSON.stringify(msg.result || msg));
465
+ if (leakViolations.length > 0) {
466
+ this.logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
467
+ violations: leakViolations,
468
+ }, this.sessionId);
469
+ console.error(`\n🚨 SENSITIVE DATA IN SSE STREAM:\n${leakViolations.join("\n")}\n`);
470
+ if (this.policy.isBlockingMode) {
471
+ // Replace event with error event
472
+ clientRes.write(`data: ${JSON.stringify({
473
+ jsonrpc: "2.0",
474
+ id: msg.id,
475
+ error: {
476
+ code: -32001,
477
+ message: "Sensitive data blocked",
478
+ },
479
+ })}\n\n`);
480
+ continue;
481
+ }
482
+ }
483
+ }
484
+ catch {
485
+ /* non-JSON SSE data */
486
+ }
487
+ }
488
+ clientRes.write(line + "\n");
489
+ }
490
+ });
491
+ proxyRes.on("end", () => {
492
+ if (sseBuffer)
493
+ clientRes.write(sseBuffer);
494
+ clientRes.end();
495
+ });
496
+ }
497
+ else {
498
+ // Regular HTTP — collect response, scan, forward
499
+ const resChunks = [];
500
+ proxyRes.on("data", (chunk) => resChunks.push(chunk));
501
+ proxyRes.on("end", () => {
502
+ const resBody = Buffer.concat(resChunks).toString();
503
+ try {
504
+ const msg = JSON.parse(resBody);
505
+ const leakViolations = this.policy.checkSensitiveData(JSON.stringify(msg.result || msg));
506
+ if (leakViolations.length > 0) {
507
+ this.logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
508
+ violations: leakViolations,
509
+ }, this.sessionId);
510
+ if (this.policy.isBlockingMode && msg.id !== undefined) {
511
+ clientRes.end(JSON.stringify({
512
+ jsonrpc: "2.0",
513
+ id: msg.id,
514
+ error: {
515
+ code: -32001,
516
+ message: "Sensitive data blocked",
517
+ },
518
+ }));
519
+ return;
520
+ }
521
+ }
522
+ }
523
+ catch {
524
+ /* non-JSON */
525
+ }
526
+ clientRes.end(resBody);
527
+ });
528
+ }
529
+ });
530
+ proxyReq.on("error", (err) => {
531
+ this.logger.logEvent("PROXY_ERROR", "HIGH", { error: err.message }, this.sessionId);
532
+ clientRes.writeHead(502);
533
+ clientRes.end("Bad Gateway");
534
+ });
535
+ if (rawBody.length > 0)
536
+ proxyReq.write(rawBody);
537
+ proxyReq.end();
538
+ });
539
+ });
540
+ server.listen(port, () => {
541
+ this.logger.logEvent("SSE_PROXY_START", "LOW", {
542
+ port,
543
+ targetUrl,
544
+ mode: this.policy.rawConfig.mode,
545
+ }, this.sessionId);
546
+ console.error(`🛡️ ContextGuard SSE/HTTP proxy listening on port ${port}`);
547
+ console.error(` → Forwarding to: ${targetUrl}`);
548
+ console.error(` → Mode: ${this.policy.rawConfig.mode}\n`);
549
+ });
550
+ }
551
+ inspectOutbound(message) {
552
+ const violations = [];
553
+ let shouldBlock = false;
554
+ if (message.method === "tools/call") {
555
+ const now = Date.now();
556
+ this.toolCallTimestamps.push(now);
557
+ this.toolCallTimestamps = this.toolCallTimestamps.filter((t) => t > now - 60000);
558
+ if (!this.policy.checkRateLimit(this.toolCallTimestamps)) {
559
+ violations.push("Rate limit exceeded");
560
+ if (this.policy.isBlockingMode)
561
+ shouldBlock = true;
562
+ }
563
+ const paramsStr = JSON.stringify(message.params);
564
+ violations.push(...this.policy.checkPromptInjection(paramsStr));
565
+ violations.push(...this.policy.checkSensitiveData(paramsStr));
566
+ const filePathParams = [
567
+ message.params?.arguments?.path,
568
+ message.params?.arguments?.filePath,
569
+ ].filter((p) => typeof p === "string");
570
+ for (const fp of filePathParams) {
571
+ violations.push(...this.policy.checkFileAccess(fp));
572
+ }
573
+ if (violations.length > 0 && this.policy.isBlockingMode)
574
+ shouldBlock = true;
575
+ }
576
+ return { violations, shouldBlock };
577
+ }
578
+ }
579
+ exports.MCPSSEProxy = MCPSSEProxy;
580
+ // ─── CLI Entry Point ──────────────────────────────────────────────────────────
349
581
  async function main() {
350
582
  const args = process.argv.slice(2);
351
583
  if (args.length === 0 || args.includes("--help")) {
352
584
  console.log(`
353
- MCP Security Wrapper - MVP
585
+ MCP Security Wrapper
354
586
 
355
587
  Usage:
588
+ # stdio transport (Claude Desktop)
356
589
  contextguard --server "node server.js" --config config.json
357
590
 
591
+ # SSE/HTTP transport proxy
592
+ contextguard --transport sse --port 3100 --target http://localhost:3000 --config config.json
593
+
358
594
  Options:
359
- --server <command> Command to start the MCP server (required)
360
- --config <file> Path to security config JSON file (optional)
361
- --help Show this help message
595
+ --server <cmd> Command to start MCP server (stdio mode)
596
+ --transport <type> stdio | sse | http (default: stdio)
597
+ --port <n> Proxy listen port (SSE/HTTP mode, default: 3100)
598
+ --target <url> Upstream server URL (SSE/HTTP mode)
599
+ --config <file> Path to security config JSON (optional)
600
+ --help Show this help
362
601
 
602
+ Config options (config.json):
603
+ mode "monitor" (log only) | "block" (block + log) default: monitor
604
+ maxToolCallsPerMinute number default: 30
605
+ enablePromptInjectionDetection boolean default: true
606
+ enableSensitiveDataDetection boolean default: true
607
+ enablePathTraversalPrevention boolean default: true
608
+ allowedFilePaths string[]
609
+ logPath string default: ./mcp_security.log
363
610
  `);
364
611
  process.exit(0);
365
612
  }
366
613
  let serverCommand = "";
367
614
  let configFile = "";
615
+ let transport = "stdio";
616
+ let port = 3100;
617
+ let targetUrl = "";
368
618
  for (let i = 0; i < args.length; i++) {
369
619
  if (args[i] === "--server" && args[i + 1]) {
370
- serverCommand = args[i + 1];
371
- i++;
620
+ serverCommand = args[++i];
372
621
  }
373
622
  else if (args[i] === "--config" && args[i + 1]) {
374
- configFile = args[i + 1];
375
- i++;
623
+ configFile = args[++i];
624
+ }
625
+ else if (args[i] === "--transport" && args[i + 1]) {
626
+ transport = args[++i];
627
+ }
628
+ else if (args[i] === "--port" && args[i + 1]) {
629
+ port = parseInt(args[++i], 10);
630
+ }
631
+ else if (args[i] === "--target" && args[i + 1]) {
632
+ targetUrl = args[++i];
376
633
  }
377
- }
378
- if (!serverCommand) {
379
- console.error("Error: --server argument is required");
380
- process.exit(1);
381
634
  }
382
635
  let config = {};
383
636
  if (configFile && fs.existsSync(configFile)) {
384
637
  config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
385
638
  }
639
+ // CLI flags override config file
640
+ if (transport !== "stdio")
641
+ config.transport = transport;
642
+ if (port !== 3100)
643
+ config.port = port;
644
+ if (targetUrl)
645
+ config.targetUrl = targetUrl;
386
646
  const policy = new SecurityPolicy(config);
387
647
  const logger = new SecurityLogger(config.logPath);
388
- const wrapper = new MCPSecurityWrapper(serverCommand.split(" "), policy, logger);
389
- // console.log("ContextGuard is running");
390
- await wrapper.start();
648
+ const effectiveTransport = config.transport || transport;
649
+ if (effectiveTransport === "sse" || effectiveTransport === "http") {
650
+ if (!config.targetUrl && !targetUrl) {
651
+ console.error("Error: --target <url> is required for SSE/HTTP transport");
652
+ process.exit(1);
653
+ }
654
+ const proxy = new MCPSSEProxy(policy, logger);
655
+ proxy.start(config.port || port, config.targetUrl || targetUrl);
656
+ }
657
+ else {
658
+ if (!serverCommand) {
659
+ console.error("Error: --server argument is required for stdio transport");
660
+ process.exit(1);
661
+ }
662
+ const wrapper = new MCPSecurityWrapper(serverCommand.split(" "), policy, logger);
663
+ await wrapper.start();
664
+ }
391
665
  }
392
666
  if (require.main === module) {
393
667
  main().catch((err) => {