contextguard 0.1.8 → 0.2.1

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 +376 -0
  5. package/dist/cli.d.ts +11 -0
  6. package/dist/cli.js +298 -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 +178 -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 +389 -147
  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 -11
  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 -570
  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,189 +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
- // Buffer and parse for security scanning
323
326
  this.serverMessageBuffer += output;
324
327
  const lines = this.serverMessageBuffer.split("\n");
325
328
  this.serverMessageBuffer = lines.pop() || "";
326
329
  for (const line of lines) {
327
- if (line.trim()) {
328
- let shouldForward = true;
329
- try {
330
- const message = JSON.parse(line);
331
- // Check for sensitive data in response
332
- const violations = [];
333
- const responseStr = JSON.stringify(message.result || message);
334
- const sensitiveViolations = this.policy.checkSensitiveData(responseStr);
335
- violations.push(...sensitiveViolations);
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`);
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) {
342
343
  console.error("🚫 RESPONSE BLOCKED\n");
343
- // Send sanitized error response instead
344
344
  if (message.id !== undefined) {
345
- const errorResponse = {
346
- jsonrpc: message.jsonrpc,
347
- id: message.id,
348
- error: {
349
- code: -32001,
350
- message: "Security violation: Response contains sensitive data",
351
- data: { violations },
352
- },
353
- };
354
- process.stdout.write(JSON.stringify(errorResponse) + "\n");
345
+ process.stdout.write(buildErrorResponse(message, -32001, "Security violation: Response contains sensitive data", violations));
355
346
  }
356
347
  shouldForward = false;
357
348
  }
358
349
  else {
359
- this.logger.logEvent("SERVER_RESPONSE", "LOW", {
360
- id: message.id,
361
- hasError: !!message.error,
362
- }, this.sessionId);
350
+ console.error("⚠️ MONITOR MODE — response forwarded\n");
363
351
  }
364
352
  }
365
- catch (err) {
366
- // Log parse errors for server output
367
- this.logger.logEvent("SERVER_PARSE_ERROR", "LOW", {
368
- error: err instanceof Error ? err.message : String(err),
369
- line: line.substring(0, 100),
353
+ else {
354
+ this.logger.logEvent("SERVER_RESPONSE", "LOW", {
355
+ id: message.id,
356
+ hasError: !!message.error,
370
357
  }, this.sessionId);
371
358
  }
372
- // Forward the line if not blocked
373
- if (shouldForward) {
374
- process.stdout.write(line + "\n");
375
- }
376
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");
377
368
  }
378
369
  }
379
370
  }
380
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 ──────────────────────────────────────────────────────────
381
581
  async function main() {
382
582
  const args = process.argv.slice(2);
383
583
  if (args.length === 0 || args.includes("--help")) {
384
584
  console.log(`
385
- MCP Security Wrapper - MVP
585
+ MCP Security Wrapper
386
586
 
387
587
  Usage:
588
+ # stdio transport (Claude Desktop)
388
589
  contextguard --server "node server.js" --config config.json
389
590
 
591
+ # SSE/HTTP transport proxy
592
+ contextguard --transport sse --port 3100 --target http://localhost:3000 --config config.json
593
+
390
594
  Options:
391
- --server <command> Command to start the MCP server (required)
392
- --config <file> Path to security config JSON file (optional)
393
- --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
394
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
395
610
  `);
396
611
  process.exit(0);
397
612
  }
398
613
  let serverCommand = "";
399
614
  let configFile = "";
615
+ let transport = "stdio";
616
+ let port = 3100;
617
+ let targetUrl = "";
400
618
  for (let i = 0; i < args.length; i++) {
401
619
  if (args[i] === "--server" && args[i + 1]) {
402
- serverCommand = args[i + 1];
403
- i++;
620
+ serverCommand = args[++i];
404
621
  }
405
622
  else if (args[i] === "--config" && args[i + 1]) {
406
- configFile = args[i + 1];
407
- 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];
408
633
  }
409
- }
410
- if (!serverCommand) {
411
- console.error("Error: --server argument is required");
412
- process.exit(1);
413
634
  }
414
635
  let config = {};
415
636
  if (configFile && fs.existsSync(configFile)) {
416
637
  config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
417
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;
418
646
  const policy = new SecurityPolicy(config);
419
647
  const logger = new SecurityLogger(config.logPath);
420
- const wrapper = new MCPSecurityWrapper(serverCommand.split(" "), policy, logger);
421
- // console.log("ContextGuard is running");
422
- 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
+ }
423
665
  }
424
666
  if (require.main === module) {
425
667
  main().catch((err) => {