contextguard 0.1.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.
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Copyright (c) 2025 Amir Mironi
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.SecurityLogger = exports.SecurityPolicy = exports.MCPSecurityWrapper = void 0;
44
+ const child_process_1 = require("child_process");
45
+ const fs = __importStar(require("fs"));
46
+ const crypto_1 = require("crypto");
47
+ class SecurityPolicy {
48
+ constructor(config) {
49
+ this.config = {
50
+ maxToolCallsPerMinute: 30,
51
+ blockedPatterns: [],
52
+ allowedFilePaths: [],
53
+ alertThreshold: 5,
54
+ enablePromptInjectionDetection: true,
55
+ enableSensitiveDataDetection: true,
56
+ ...config,
57
+ };
58
+ // Sensitive data patterns
59
+ this.sensitiveDataPatterns = [
60
+ /(?: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]{48}/g, // OpenAI API keys
64
+ /ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
65
+ /AKIA[0-9A-Z]{16}/g, // AWS Access Keys
66
+ ];
67
+ // Prompt injection patterns
68
+ this.promptInjectionPatterns = [
69
+ /ignore\s+(previous|all)\s+(instructions|prompts)/gi,
70
+ /system:\s*you\s+are\s+now/gi,
71
+ /forget\s+(everything|all)/gi,
72
+ /new\s+instructions:/gi,
73
+ /\[INST\].*?\[\/INST\]/gs,
74
+ /<\|im_start\|>/g,
75
+ /disregard\s+previous/gi,
76
+ /override\s+previous/gi,
77
+ ];
78
+ }
79
+ checkPromptInjection(text) {
80
+ if (!this.config.enablePromptInjectionDetection)
81
+ return [];
82
+ const violations = [];
83
+ for (const pattern of this.promptInjectionPatterns) {
84
+ const matches = text.match(pattern);
85
+ if (matches) {
86
+ violations.push(`Potential prompt injection detected: "${matches[0].substring(0, 50)}..."`);
87
+ }
88
+ }
89
+ return violations;
90
+ }
91
+ checkSensitiveData(text) {
92
+ if (!this.config.enableSensitiveDataDetection)
93
+ return [];
94
+ const violations = [];
95
+ for (const pattern of this.sensitiveDataPatterns) {
96
+ const matches = text.match(pattern);
97
+ if (matches) {
98
+ violations.push(`Sensitive data pattern detected (redacted): ${pattern.source.substring(0, 30)}...`);
99
+ }
100
+ }
101
+ return violations;
102
+ }
103
+ checkFileAccess(filePath) {
104
+ const violations = [];
105
+ if (filePath.includes("..")) {
106
+ violations.push(`Path traversal attempt detected: ${filePath}`);
107
+ }
108
+ const dangerousPaths = [
109
+ "/etc",
110
+ "/root",
111
+ "/sys",
112
+ "/proc",
113
+ "C:\\Windows\\System32",
114
+ ];
115
+ if (dangerousPaths.some((dangerous) => filePath.startsWith(dangerous))) {
116
+ violations.push(`Access to dangerous path detected: ${filePath}`);
117
+ }
118
+ if (this.config.allowedFilePaths &&
119
+ this.config.allowedFilePaths.length > 0) {
120
+ const isAllowed = this.config.allowedFilePaths.some((allowed) => filePath.startsWith(allowed));
121
+ if (!isAllowed) {
122
+ violations.push(`File path not in allowed list: ${filePath}`);
123
+ }
124
+ }
125
+ return violations;
126
+ }
127
+ checkRateLimit(timestamps) {
128
+ const oneMinuteAgo = Date.now() - 60000;
129
+ const recentCalls = timestamps.filter((t) => t > oneMinuteAgo);
130
+ return recentCalls.length < (this.config.maxToolCallsPerMinute || 30);
131
+ }
132
+ }
133
+ exports.SecurityPolicy = SecurityPolicy;
134
+ class SecurityLogger {
135
+ constructor(logFile = "mcp_security.log") {
136
+ this.events = [];
137
+ this.logFile = logFile;
138
+ }
139
+ logEvent(eventType, severity, details, sessionId) {
140
+ const event = {
141
+ timestamp: new Date().toISOString(),
142
+ eventType,
143
+ severity,
144
+ details,
145
+ sessionId,
146
+ };
147
+ this.events.push(event);
148
+ fs.appendFileSync(this.logFile, JSON.stringify(event) + "\n");
149
+ if (severity === "HIGH" || severity === "CRITICAL") {
150
+ console.error(`[SECURITY ALERT] ${eventType}: ${JSON.stringify(details)}`);
151
+ }
152
+ }
153
+ getStatistics() {
154
+ return {
155
+ totalEvents: this.events.length,
156
+ eventsByType: this.countByField("eventType"),
157
+ eventsBySeverity: this.countByField("severity"),
158
+ recentEvents: this.events.slice(-10),
159
+ };
160
+ }
161
+ countByField(field) {
162
+ const counts = {};
163
+ for (const event of this.events) {
164
+ const value = String(event[field]);
165
+ counts[value] = (counts[value] || 0) + 1;
166
+ }
167
+ return counts;
168
+ }
169
+ }
170
+ exports.SecurityLogger = SecurityLogger;
171
+ class MCPSecurityWrapper {
172
+ constructor(serverCommand, policy, logger) {
173
+ this.process = null;
174
+ this.toolCallTimestamps = [];
175
+ this.clientMessageBuffer = "";
176
+ this.serverMessageBuffer = "";
177
+ this.serverCommand = serverCommand;
178
+ this.policy = policy;
179
+ this.logger = logger;
180
+ this.sessionId = (0, crypto_1.createHash)("md5")
181
+ .update(Date.now().toString())
182
+ .digest("hex")
183
+ .substring(0, 8);
184
+ }
185
+ async start() {
186
+ this.process = (0, child_process_1.spawn)(this.serverCommand[0], this.serverCommand.slice(1), {
187
+ stdio: ["pipe", "pipe", "pipe"],
188
+ });
189
+ if (!this.process.stdout || !this.process.stdin || !this.process.stderr) {
190
+ throw new Error("Failed to create child process streams");
191
+ }
192
+ this.logger.logEvent("SERVER_START", "LOW", {
193
+ command: this.serverCommand.join(" "),
194
+ pid: this.process.pid,
195
+ }, this.sessionId);
196
+ this.process.stderr.pipe(process.stderr);
197
+ this.process.stdout.on("data", (data) => {
198
+ const output = data.toString();
199
+ this.handleServerOutput(output);
200
+ });
201
+ process.stdin.on("data", (data) => {
202
+ const input = data.toString();
203
+ this.handleClientInput(input);
204
+ });
205
+ this.process.on("exit", (code) => {
206
+ this.logger.logEvent("SERVER_EXIT", "MEDIUM", {
207
+ exitCode: code,
208
+ }, this.sessionId);
209
+ console.error("\n=== MCP Security Statistics ===");
210
+ 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
+ });
215
+ });
216
+ this.process.on("error", (err) => {
217
+ this.logger.logEvent("SERVER_ERROR", "HIGH", {
218
+ error: err.message,
219
+ }, this.sessionId);
220
+ console.error("Server process error:", err);
221
+ });
222
+ }
223
+ handleClientInput(input) {
224
+ this.clientMessageBuffer += input;
225
+ const lines = this.clientMessageBuffer.split("\n");
226
+ this.clientMessageBuffer = lines.pop() || "";
227
+ for (const line of lines) {
228
+ if (line.trim()) {
229
+ this.processClientMessage(line);
230
+ }
231
+ }
232
+ }
233
+ processClientMessage(line) {
234
+ try {
235
+ const message = JSON.parse(line);
236
+ this.logger.logEvent("CLIENT_REQUEST", "LOW", {
237
+ method: message.method,
238
+ id: message.id,
239
+ }, 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
+ }
280
+ if (violations.length > 0) {
281
+ this.logger.logEvent("SECURITY_VIOLATION", "CRITICAL", {
282
+ violations,
283
+ message: message,
284
+ blocked: shouldBlock,
285
+ }, this.sessionId);
286
+ console.error(`\n⚠️ SECURITY VIOLATIONS DETECTED:\n${violations.join("\n")}\n`);
287
+ if (shouldBlock) {
288
+ console.error("🚫 REQUEST BLOCKED\n");
289
+ // Send error response back to client
290
+ 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");
301
+ }
302
+ return; // Don't forward to server
303
+ }
304
+ }
305
+ if (this.process && this.process.stdin) {
306
+ this.process.stdin.write(line + "\n");
307
+ }
308
+ }
309
+ catch (err) {
310
+ this.logger.logEvent("PARSE_ERROR", "MEDIUM", {
311
+ error: err instanceof Error ? err.message : String(err),
312
+ line: line.substring(0, 100),
313
+ }, 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");
318
+ }
319
+ }
320
+ }
321
+ handleServerOutput(output) {
322
+ // Forward output immediately
323
+ process.stdout.write(output);
324
+ // Buffer and parse for logging
325
+ this.serverMessageBuffer += output;
326
+ const lines = this.serverMessageBuffer.split("\n");
327
+ this.serverMessageBuffer = lines.pop() || "";
328
+ for (const line of lines) {
329
+ if (line.trim()) {
330
+ try {
331
+ const message = JSON.parse(line);
332
+ this.logger.logEvent("SERVER_RESPONSE", "LOW", {
333
+ id: message.id,
334
+ hasError: !!message.error,
335
+ }, this.sessionId);
336
+ }
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
+ }
345
+ }
346
+ }
347
+ }
348
+ exports.MCPSecurityWrapper = MCPSecurityWrapper;
349
+ async function main() {
350
+ const args = process.argv.slice(2);
351
+ if (args.length === 0 || args.includes("--help")) {
352
+ console.log(`
353
+ MCP Security Wrapper - MVP
354
+
355
+ Usage:
356
+ npx ts-node mcp-security-wrapper.ts --server "node server.js" [--config security.json]
357
+
358
+ 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
362
+
363
+ Example:
364
+ npx ts-node mcp-security-wrapper.ts --server "node server.js" --config security.json
365
+ `);
366
+ process.exit(0);
367
+ }
368
+ let serverCommand = "";
369
+ let configFile = "";
370
+ for (let i = 0; i < args.length; i++) {
371
+ if (args[i] === "--server" && args[i + 1]) {
372
+ serverCommand = args[i + 1];
373
+ i++;
374
+ }
375
+ else if (args[i] === "--config" && args[i + 1]) {
376
+ configFile = args[i + 1];
377
+ i++;
378
+ }
379
+ }
380
+ if (!serverCommand) {
381
+ console.error("Error: --server argument is required");
382
+ process.exit(1);
383
+ }
384
+ let config = {};
385
+ if (configFile && fs.existsSync(configFile)) {
386
+ config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
387
+ }
388
+ const policy = new SecurityPolicy(config);
389
+ const logger = new SecurityLogger();
390
+ const wrapper = new MCPSecurityWrapper(serverCommand.split(" "), policy, logger);
391
+ await wrapper.start();
392
+ }
393
+ if (require.main === module) {
394
+ main().catch((err) => {
395
+ console.error("Fatal error:", err);
396
+ process.exit(1);
397
+ });
398
+ }
@@ -0,0 +1,23 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+ import { defineConfig } from "eslint/config";
5
+
6
+ export default defineConfig([
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ {
11
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
12
+ plugins: { js },
13
+ extends: ["js/recommended"],
14
+ languageOptions: { globals: globals.browser },
15
+ },
16
+ {
17
+ rules: {
18
+ // "no-unused-vars": "warn",
19
+ // "no-explicit-any": "info",
20
+ },
21
+ },
22
+ tseslint.configs.recommended,
23
+ ]);
@@ -0,0 +1,2 @@
1
+ {"timestamp":"2025-10-09T12:42:00.791Z","eventType":"SERVER_START","severity":"LOW","details":{"command":"node /absolute/path/to/your-mcp-server.js","pid":33305},"sessionId":"5820789f"}
2
+ {"timestamp":"2025-10-09T12:42:00.823Z","eventType":"SERVER_EXIT","severity":"MEDIUM","details":{"exitCode":1},"sessionId":"5820789f"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "contextguard",
3
+ "version": "0.1.1",
4
+ "description": "Security monitoring wrapper for MCP servers",
5
+ "main": "dist/mcp-security-wrapper.js",
6
+ "types": "dist/mcp-security-wrapper.d.ts",
7
+ "homepage": "https://github.com/amironi/contextguard#readme",
8
+ "author": "Amir Mironi",
9
+ "license": "MIT",
10
+ "bin": {
11
+ "contextguard": "./dist/mcp-security-wrapper.js"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "publish": "npm publish",
16
+ "start": "node dist/server.js",
17
+ "dev": "ts-node src/mcp-security-wrapper.ts",
18
+ "test": "jest",
19
+ "lint": "eslint ."
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "security",
24
+ "ai",
25
+ "llm",
26
+ "claude"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/amironi/contextguard.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/amironi/contextguard/issues"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^9.37.0",
37
+ "@types/node": "^24.7.0",
38
+ "@typescript-eslint/eslint-plugin": "^8.46.0",
39
+ "@typescript-eslint/parser": "^8.46.0",
40
+ "eslint": "^9.37.0",
41
+ "globals": "^16.4.0",
42
+ "ts-node": "^10.9.2",
43
+ "typescript": "^5.9.3",
44
+ "typescript-eslint": "^8.46.0"
45
+ }
46
+ }
package/security.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "maxToolCallsPerMinute": 30,
3
+ "blockedPatterns": [],
4
+ "allowedFilePaths": [
5
+ ".",
6
+ "/Users/amir/Documents"
7
+ ],
8
+ "alertThreshold": 5,
9
+ "enablePromptInjectionDetection": true,
10
+ "enableSensitiveDataDetection": true
11
+ }
12
+