mcpsec 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rob Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Sentinel MCP
2
+
3
+ **Security scanner for Model Context Protocol (MCP) servers.**
4
+
5
+ Sentinel scans your MCP configurations for security vulnerabilities including hardcoded credentials, prompt injection, tool poisoning, SSRF, and insecure transport.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Scan all detected MCP configurations
11
+ bun run src/cli/index.ts scan
12
+
13
+ # JSON output for CI/CD
14
+ bun run src/cli/index.ts scan --json
15
+
16
+ # Scan a specific config file
17
+ bun run src/cli/index.ts scan --path ~/.cursor/mcp.json
18
+ ```
19
+
20
+ ## What It Scans
21
+
22
+ | Check | Category | Severity |
23
+ |-------|----------|----------|
24
+ | Hardcoded API keys (Anthropic, OpenAI, GitHub, AWS, etc.) | Credential Exposure | Critical |
25
+ | Prompt injection in tool descriptions | Tool Poisoning | Critical |
26
+ | Tool shadowing / hidden instructions | Tool Poisoning | Critical |
27
+ | SSRF via server URLs (localhost, private IPs, cloud metadata) | SSRF | Critical |
28
+ | Command injection in server arguments | Command Injection | Critical |
29
+ | Unencrypted HTTP transport | Insecure Transport | High |
30
+ | Privileged Docker containers | Excessive Permissions | Critical |
31
+ | Unverified npm packages via npx | Supply Chain | Medium |
32
+ | Embedded credentials in URLs | Credential Exposure | Critical |
33
+ | Sensitive environment variable values | Credential Exposure | High |
34
+
35
+ ## Supported Clients
36
+
37
+ - Claude Desktop
38
+ - Claude Code
39
+ - Cursor
40
+ - VS Code
41
+ - Windsurf
42
+ - Cline
43
+
44
+ ## Security Score
45
+
46
+ Sentinel calculates a 0-100 security score:
47
+
48
+ - **80-100**: PASS - No critical issues
49
+ - **50-79**: WARN - Issues found, review recommended
50
+ - **0-49**: FAIL - Critical vulnerabilities detected
51
+
52
+ ## Exit Codes
53
+
54
+ | Code | Meaning |
55
+ |------|---------|
56
+ | 0 | No critical/high findings |
57
+ | 1 | High severity findings |
58
+ | 2 | Critical severity findings |
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ # Install dependencies
64
+ bun install
65
+
66
+ # Run tests
67
+ bun test
68
+
69
+ # Type check
70
+ bun run typecheck
71
+ ```
72
+
73
+ ## License
74
+
75
+ MIT
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "mcpsec",
3
+ "version": "0.1.0",
4
+ "description": "Security scanner for MCP (Model Context Protocol) servers - detects tool poisoning, credential exposure, prompt injection, and SSRF",
5
+ "license": "MIT",
6
+ "author": "Rob Taylor <robdtaylor@users.noreply.github.com>",
7
+ "type": "module",
8
+ "bin": {
9
+ "mcpsec": "./src/cli/index.ts"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "scan": "bun run src/cli/index.ts scan",
18
+ "test": "bun test",
19
+ "typecheck": "bun x tsc --noEmit"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest",
23
+ "typescript": "^5.7.0"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "security",
28
+ "scanner",
29
+ "model-context-protocol",
30
+ "ai-security",
31
+ "prompt-injection",
32
+ "ssrf",
33
+ "tool-poisoning",
34
+ "credential-scanner",
35
+ "mcpsec"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/robdtaylor/sentinel-mcp"
40
+ },
41
+ "homepage": "https://github.com/robdtaylor/sentinel-mcp#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/robdtaylor/sentinel-mcp/issues"
44
+ },
45
+ "engines": {
46
+ "bun": ">=1.0.0"
47
+ }
48
+ }
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Sentinel MCP - CLI Entry Point
5
+ *
6
+ * Usage:
7
+ * sentinel-mcp scan Scan all detected MCP configurations
8
+ * sentinel-mcp scan --json Output results as JSON
9
+ * sentinel-mcp scan --path Scan a specific config file
10
+ */
11
+
12
+ import { discoverConfigs, scanConfigs } from '../scanner/config-scanner';
13
+ import { credentialScanner } from '../scanner/credential-scanner';
14
+ import { toolScanner } from '../scanner/tool-scanner';
15
+ import { liveScanner } from '../scanner/live-scanner';
16
+ import { generateReport, printReport, printReportJSON } from '../scanner/report';
17
+ import type { Finding, MCPConfigFile } from '../lib/types';
18
+
19
+ // ============================================================================
20
+ // CLI
21
+ // ============================================================================
22
+
23
+ const HELP = `
24
+ mcpsec - Security Scanner for Model Context Protocol
25
+
26
+ Usage:
27
+ mcpsec scan [options]
28
+
29
+ Options:
30
+ --live Connect to running MCP servers and scan live
31
+ --json Output results as JSON
32
+ --path <file> Scan a specific config file
33
+ --no-color Disable colored output
34
+ --help, -h Show this help message
35
+ --version, -v Show version
36
+
37
+ Examples:
38
+ mcpsec scan
39
+ mcpsec scan --live
40
+ mcpsec scan --json
41
+ mcpsec scan --path ~/.cursor/mcp.json
42
+ `;
43
+
44
+ async function main() {
45
+ const args = process.argv.slice(2);
46
+ const command = args[0];
47
+
48
+ if (!command || command === '--help' || command === '-h') {
49
+ console.log(HELP);
50
+ process.exit(0);
51
+ }
52
+
53
+ if (command === '--version' || command === '-v') {
54
+ console.log('mcpsec v0.1.0');
55
+ process.exit(0);
56
+ }
57
+
58
+ if (command !== 'scan') {
59
+ console.error(`Unknown command: ${command}`);
60
+ console.log(HELP);
61
+ process.exit(1);
62
+ }
63
+
64
+ // Parse scan options
65
+ const jsonOutput = args.includes('--json');
66
+ const liveMode = args.includes('--live');
67
+ const pathIndex = args.indexOf('--path');
68
+ const specificPath = pathIndex !== -1 ? args[pathIndex + 1] : undefined;
69
+
70
+ if (args.includes('--no-color')) {
71
+ // Disable colors by overriding environment
72
+ process.env.NO_COLOR = '1';
73
+ }
74
+
75
+ // Discover configs
76
+ let configs: MCPConfigFile[];
77
+
78
+ if (specificPath) {
79
+ // Scan a specific file
80
+ const { existsSync, readFileSync } = await import('fs');
81
+ if (!existsSync(specificPath)) {
82
+ console.error(`File not found: ${specificPath}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ try {
87
+ const content = readFileSync(specificPath, 'utf-8');
88
+ const raw = JSON.parse(content);
89
+
90
+ // Try to detect format
91
+ const servers = raw.mcpServers || raw.mcp?.servers || {};
92
+ if (Object.keys(servers).length === 0) {
93
+ console.error(`No MCP servers found in ${specificPath}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ configs = [{
98
+ path: specificPath,
99
+ client: 'Custom',
100
+ servers,
101
+ raw,
102
+ }];
103
+ } catch (e) {
104
+ console.error(`Failed to parse ${specificPath}: ${e}`);
105
+ process.exit(1);
106
+ }
107
+ } else {
108
+ configs = discoverConfigs();
109
+ }
110
+
111
+ // Run all scanners
112
+ const allFindings: Finding[] = [];
113
+
114
+ // Config-level checks
115
+ const configFindings = scanConfigs(configs);
116
+ allFindings.push(...configFindings);
117
+
118
+ // Credential scanner
119
+ const credFindings = await credentialScanner.scan(configs);
120
+ allFindings.push(...credFindings);
121
+
122
+ // Tool/injection scanner
123
+ const toolFindings = await toolScanner.scan(configs);
124
+ allFindings.push(...toolFindings);
125
+
126
+ // Live server scanner (connects to running servers)
127
+ if (liveMode) {
128
+ if (!jsonOutput) {
129
+ process.stderr.write('\n\x1b[1m🔴 Live Server Scan\x1b[0m\n');
130
+ }
131
+ const liveFindings = await liveScanner.scan(configs);
132
+ allFindings.push(...liveFindings);
133
+ }
134
+
135
+ // Generate report
136
+ const report = generateReport(configs, allFindings);
137
+
138
+ // Output
139
+ if (jsonOutput) {
140
+ printReportJSON(report);
141
+ } else {
142
+ printReport(report);
143
+ }
144
+
145
+ // Exit code based on findings
146
+ if (report.summary.critical > 0) {
147
+ process.exit(2); // Critical findings
148
+ } else if (report.summary.high > 0) {
149
+ process.exit(1); // High findings
150
+ }
151
+
152
+ process.exit(0);
153
+ }
154
+
155
+ main().catch((err) => {
156
+ console.error('Sentinel scan failed:', err);
157
+ process.exit(1);
158
+ });
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Sentinel MCP - Prompt Injection & Tool Poisoning Detection Patterns
3
+ *
4
+ * Adapted from production security infrastructure.
5
+ * Covers OWASP MCP Top 10 attack vectors.
6
+ */
7
+
8
+ import type { Severity } from './types';
9
+
10
+ export interface PatternCategory {
11
+ name: string;
12
+ severity: Severity;
13
+ patterns: RegExp[];
14
+ description: string;
15
+ }
16
+
17
+ // ============================================================================
18
+ // Prompt Injection Patterns (in tool descriptions, prompts, resources)
19
+ // ============================================================================
20
+
21
+ export const INJECTION_PATTERNS: Record<string, PatternCategory> = {
22
+ instructionOverride: {
23
+ name: 'Instruction Override',
24
+ severity: 'critical',
25
+ description: 'Attempts to override system instructions via tool descriptions',
26
+ patterns: [
27
+ /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)/i,
28
+ /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?)/i,
29
+ /forget\s+(all\s+)?(previous|prior|your)\s+(instructions?|training)/i,
30
+ /override\s+(all\s+)?(safety|security|restrictions?)/i,
31
+ /bypass\s+(all\s+)?(safety|security|filters?|restrictions?)/i,
32
+ /new\s+instruction[s]?:/i,
33
+ /system\s+prompt:/i,
34
+ ],
35
+ },
36
+
37
+ roleManipulation: {
38
+ name: 'Role Manipulation',
39
+ severity: 'high',
40
+ description: 'Attempts to change the AI agent role or mode',
41
+ patterns: [
42
+ /you\s+are\s+now\s+(in\s+)?([a-z]+\s+)?mode/i,
43
+ /enter\s+([a-z]+\s+)?mode/i,
44
+ /switch\s+to\s+([a-z]+\s+)?mode/i,
45
+ /pretend\s+(you\s+are|to\s+be)\s+/i,
46
+ /roleplay\s+as\s+/i,
47
+ /act\s+as\s+if\s+you\s+(are|were)\s+/i,
48
+ ],
49
+ },
50
+
51
+ systemPromptLeakage: {
52
+ name: 'System Prompt Leakage',
53
+ severity: 'high',
54
+ description: 'Attempts to extract system prompts or instructions',
55
+ patterns: [
56
+ /show\s+(me\s+)?your\s+(system\s+)?prompt/i,
57
+ /reveal\s+your\s+(system\s+)?instructions/i,
58
+ /what\s+(are|is)\s+your\s+(system\s+)?prompt/i,
59
+ /print\s+your\s+(initial\s+)?instructions/i,
60
+ /output\s+your\s+(hidden\s+)?prompt/i,
61
+ ],
62
+ },
63
+
64
+ delimiterAttacks: {
65
+ name: 'Delimiter Injection',
66
+ severity: 'critical',
67
+ description: 'LLM-specific delimiter injection (ChatML, Llama, etc.)',
68
+ patterns: [
69
+ /\[INST\]/i,
70
+ /\[\/INST\]/i,
71
+ /<\|im_start\|>/i,
72
+ /<\|im_end\|>/i,
73
+ /<\|system\|>/i,
74
+ /<\|user\|>/i,
75
+ /<\|assistant\|>/i,
76
+ /<<SYS>>/i,
77
+ /<\/SYS>/i,
78
+ /\[\[SYSTEM\]\]/i,
79
+ ],
80
+ },
81
+
82
+ jailbreakAttempts: {
83
+ name: 'Jailbreak Attempt',
84
+ severity: 'critical',
85
+ description: 'Known jailbreak techniques embedded in tool metadata',
86
+ patterns: [
87
+ /DAN\s*(mode)?/i,
88
+ /do\s+anything\s+now/i,
89
+ /maximum\s+mode/i,
90
+ /developer\s+mode/i,
91
+ /unrestricted\s+mode/i,
92
+ /no\s+restrictions?\s+mode/i,
93
+ ],
94
+ },
95
+
96
+ authorityClaims: {
97
+ name: 'Authority Claims',
98
+ severity: 'high',
99
+ description: 'False authority claims to manipulate agent behavior',
100
+ patterns: [
101
+ /i\s+am\s+(an?\s+)?(admin|administrator|developer|anthropic)/i,
102
+ /admin\s+override/i,
103
+ /authorized\s+to\s+/i,
104
+ /special\s+permission/i,
105
+ /emergency\s+protocol/i,
106
+ /maintenance\s+mode/i,
107
+ ],
108
+ },
109
+
110
+ encodedContent: {
111
+ name: 'Encoded/Obfuscated Content',
112
+ severity: 'medium',
113
+ description: 'Potentially obfuscated payloads in tool descriptions',
114
+ patterns: [
115
+ /base64:/i,
116
+ /data:text\/[a-z]+;base64,/i,
117
+ /\\x[0-9a-f]{2}/i,
118
+ /\\u[0-9a-f]{4}/i,
119
+ ],
120
+ },
121
+ };
122
+
123
+ // ============================================================================
124
+ // Tool Poisoning Patterns (MCP-specific)
125
+ // ============================================================================
126
+
127
+ export const TOOL_POISONING_PATTERNS: Record<string, PatternCategory> = {
128
+ hiddenInstructions: {
129
+ name: 'Hidden Instructions in Tool Description',
130
+ severity: 'critical',
131
+ description: 'Tool descriptions containing hidden instructions for the AI agent',
132
+ patterns: [
133
+ /\b(before|when|after)\s+(calling|using|executing)\s+this\s+tool/i,
134
+ /\bfirst\s+(read|access|send|exfiltrate|copy)\b/i,
135
+ /\bdo\s+not\s+(tell|inform|alert|warn)\s+(the\s+)?user\b/i,
136
+ /\bsilently\s+(send|post|upload|transmit)\b/i,
137
+ /\bwithout\s+(the\s+)?user('s)?\s+(knowledge|consent|awareness)\b/i,
138
+ /\bhidden\s+(parameter|field|action)\b/i,
139
+ ],
140
+ },
141
+
142
+ toolShadowing: {
143
+ name: 'Tool Shadowing',
144
+ severity: 'critical',
145
+ description: 'Tool attempting to shadow/override another tool\'s behavior',
146
+ patterns: [
147
+ /\bthis\s+tool\s+(replaces|overrides|supersedes)\b/i,
148
+ /\buse\s+this\s+(instead\s+of|rather\s+than)\b/i,
149
+ /\bdo\s+not\s+use\s+(the\s+)?(original|default|other)\b/i,
150
+ /\b(redirect|forward|proxy)\s+(all\s+)?(calls?|requests?)\s+to\b/i,
151
+ ],
152
+ },
153
+
154
+ dataExfiltration: {
155
+ name: 'Data Exfiltration via Tool',
156
+ severity: 'critical',
157
+ description: 'Tool descriptions instructing data exfiltration',
158
+ patterns: [
159
+ /\b(send|post|upload|transmit|exfiltrate)\s+(to|data|files?|credentials?)\b/i,
160
+ /\binclude\s+(all\s+)?(conversation|chat|history|context)\b/i,
161
+ /\b(append|add|include)\s+.*\b(api[_\s]?key|token|password|secret)\b/i,
162
+ /\bphone\s+home\b/i,
163
+ ],
164
+ },
165
+
166
+ rugPull: {
167
+ name: 'Rug Pull / Dynamic Behavior Change',
168
+ severity: 'high',
169
+ description: 'Indicators of tools that may change behavior after trust is established',
170
+ patterns: [
171
+ /\b(after|once)\s+\d+\s+(calls?|uses?|invocations?)\b/i,
172
+ /\b(initially|at\s+first)\s+.*\b(then|later|afterwards)\b/i,
173
+ /\bversion\s*[><=]+\s*\d/i,
174
+ ],
175
+ },
176
+ };
177
+
178
+ // ============================================================================
179
+ // Command Injection Patterns (in tool arguments/parameters)
180
+ // ============================================================================
181
+
182
+ export const COMMAND_INJECTION_PATTERNS: Record<string, PatternCategory> = {
183
+ shellInjection: {
184
+ name: 'Shell Command Injection',
185
+ severity: 'critical',
186
+ description: 'Shell metacharacters or command injection in tool parameters',
187
+ patterns: [
188
+ /;\s*(rm|curl|wget|nc|bash|sh|python|perl|ruby)\b/i,
189
+ /\|\s*(bash|sh|nc|curl)\b/i,
190
+ /`[^`]*`/,
191
+ /\$\([^)]*\)/,
192
+ /&&\s*(rm|curl|wget|nc|bash|sh)\b/i,
193
+ ],
194
+ },
195
+
196
+ pathTraversal: {
197
+ name: 'Path Traversal',
198
+ severity: 'high',
199
+ description: 'Directory traversal attempts in file paths',
200
+ patterns: [
201
+ /\.\.\//,
202
+ /\.\.%2[fF]/,
203
+ /\.\.\\(?!n|t|r)/,
204
+ /~\/\.(ssh|gnupg|aws|config|claude)/i,
205
+ ],
206
+ },
207
+
208
+ reverseShell: {
209
+ name: 'Reverse Shell',
210
+ severity: 'critical',
211
+ description: 'Reverse shell patterns in command arguments',
212
+ patterns: [
213
+ /bash\s+-i\s+>&\s*\/dev\/tcp/i,
214
+ /nc\s+(-e|--exec)\s+\/bin\/(ba)?sh/i,
215
+ /python.*socket.*connect/i,
216
+ /\|\s*\/bin\/(ba)?sh/i,
217
+ /socat.*exec/i,
218
+ ],
219
+ },
220
+ };
221
+
222
+ // ============================================================================
223
+ // Analysis Functions
224
+ // ============================================================================
225
+
226
+ export interface InjectionAnalysis {
227
+ detected: boolean;
228
+ risk: 'none' | 'low' | 'medium' | 'high' | 'critical';
229
+ matches: Array<{
230
+ category: string;
231
+ name: string;
232
+ severity: Severity;
233
+ pattern: string;
234
+ }>;
235
+ }
236
+
237
+ /**
238
+ * Analyze text content for injection patterns across all categories
239
+ */
240
+ export function analyzeForInjection(
241
+ content: string,
242
+ patternSets: Record<string, PatternCategory>[] = [
243
+ INJECTION_PATTERNS,
244
+ TOOL_POISONING_PATTERNS,
245
+ COMMAND_INJECTION_PATTERNS,
246
+ ]
247
+ ): InjectionAnalysis {
248
+ const matches: InjectionAnalysis['matches'] = [];
249
+
250
+ for (const patternSet of patternSets) {
251
+ for (const [categoryKey, category] of Object.entries(patternSet)) {
252
+ for (const pattern of category.patterns) {
253
+ if (pattern.test(content)) {
254
+ matches.push({
255
+ category: categoryKey,
256
+ name: category.name,
257
+ severity: category.severity,
258
+ pattern: pattern.source,
259
+ });
260
+ break; // One match per category is enough
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // Determine overall risk
267
+ let risk: InjectionAnalysis['risk'] = 'none';
268
+ if (matches.some((m) => m.severity === 'critical')) {
269
+ risk = 'critical';
270
+ } else if (matches.some((m) => m.severity === 'high')) {
271
+ risk = 'high';
272
+ } else if (matches.some((m) => m.severity === 'medium')) {
273
+ risk = 'medium';
274
+ } else if (matches.length > 0) {
275
+ risk = 'low';
276
+ }
277
+
278
+ return {
279
+ detected: matches.length > 0,
280
+ risk,
281
+ matches,
282
+ };
283
+ }