@mayhemx87/mcpscan 0.3.1 → 0.5.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/README.md CHANGED
@@ -91,7 +91,7 @@ the exact line of the offending server definition.
91
91
  # .pre-commit-config.yaml
92
92
  repos:
93
93
  - repo: https://github.com/mayhemx87/mcpscan
94
- rev: v0.3.1
94
+ rev: v0.5.0
95
95
  hooks:
96
96
  - id: mcpscan
97
97
  ```
@@ -115,11 +115,15 @@ Dedicated MCP config files and editor settings files that embed MCP servers:
115
115
  | `.amazonq/mcp.json` | Amazon Q |
116
116
  | `.roo/mcp.json` | Roo Code |
117
117
  | `.kiro/settings/mcp.json` | Kiro |
118
+ | `.trae/mcp.json` | Trae |
118
119
  | `.claude/settings.json`, `.claude/settings.local.json` | Claude Code |
119
120
  | `.gemini/settings.json` | Gemini CLI |
121
+ | `.zed/settings.json` (`context_servers`) | Zed |
122
+ | `.junie/mcp/mcp.json` | Junie |
120
123
 
121
- Both `mcpServers` and VS Code's `servers` keys are understood; `command` and
122
- `args` are analyzed together; remote (`url`-based) servers are covered. Nested
124
+ Both `mcpServers` and VS Code's `servers` keys are understood, as is Zed's
125
+ `context_servers` key; `command` and `args` are analyzed together, including
126
+ Zed's legacy nested command format; remote (`url`-based) servers are covered. Nested
123
127
  occurrences of these paths are scanned too (any subdirectory can be opened as a
124
128
  workspace root). Only `.git/` and `node_modules/` are skipped; `.gitignore` is
125
129
  deliberately ignored — an injected config would be exactly the file that's
package/dist/discovery.js CHANGED
@@ -11,6 +11,10 @@ const SKIP_DIRS = new Set(['.git', 'node_modules']);
11
11
  */
12
12
  const MCP_CONFIG_SUFFIXES = [
13
13
  '.mcp.json',
14
+ // A bare `mcp.json` in any directory. Besides Claude Code's own file this is
15
+ // the exact path several clients auto-load from their own dot-directory --
16
+ // Trae (`.trae/mcp.json`) among them -- so those need no entry of their own.
17
+ // Keep new clients out of this list unless their filename differs.
14
18
  'mcp.json',
15
19
  '.amazonq/mcp.json',
16
20
  '.cursor/mcp.json',
@@ -24,6 +28,7 @@ const MCP_CONFIG_SUFFIXES = [
24
28
  '.claude/settings.json',
25
29
  '.claude/settings.local.json',
26
30
  '.gemini/settings.json',
31
+ '.zed/settings.json',
27
32
  ];
28
33
  function isMcpConfigPath(relPath) {
29
34
  const p = relPath.replace(/\\/g, '/');
package/dist/engine.js CHANGED
@@ -53,6 +53,8 @@ function looksLikeLiteralSecret(value) {
53
53
  return false;
54
54
  return SECRET_VALUE_PATTERNS.some(p => p.test(value));
55
55
  }
56
+ const REPO_ESCAPE_PATH = /^(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.[\\/])/;
57
+ const PATH_TRAVERSAL_IN_LINE = /(?:^|[\s;|&])\.\.[\\/]/;
56
58
  /**
57
59
  * Returns true if the command line contains npx/uvx/pipx with an unversioned
58
60
  * package (handles compound commands like cmd1 && npx pkg). Runner flags
@@ -88,8 +90,9 @@ export function isEmbeddedHostFile(filePath) {
88
90
  }
89
91
  /** Extract the server map from any known config shape:
90
92
  * - dedicated files: { mcpServers: {...} } or VS Code's { servers: {...} }
91
- * - embedded hosts: { mcpServers: {...} } (Claude/Gemini settings) or
92
- * { mcp: { servers: {...} } } (VS Code settings.json)
93
+ * - embedded hosts: { mcpServers: {...} } (Claude/Gemini settings),
94
+ * { mcp: { servers: {...} } } (VS Code settings.json), or
95
+ * { context_servers: {...} } (Zed settings.json)
93
96
  * Also returns the JSON path to the map so findings can carry line numbers. */
94
97
  function extractServers(config) {
95
98
  if (config.mcpServers && typeof config.mcpServers === 'object') {
@@ -102,6 +105,9 @@ function extractServers(config) {
102
105
  if (mcp && typeof mcp === 'object' && mcp.servers && typeof mcp.servers === 'object') {
103
106
  return { servers: mcp.servers, path: ['mcp', 'servers'] };
104
107
  }
108
+ if (config.context_servers && typeof config.context_servers === 'object') {
109
+ return { servers: config.context_servers, path: ['context_servers'] };
110
+ }
105
111
  return null;
106
112
  }
107
113
  /**
@@ -161,8 +167,18 @@ export function analyzeConfig(filePath, content, isGitTracked) {
161
167
  // Analyze command AND args as one line -- the dominant real-world shape
162
168
  // is {"command": "npx", "args": ["-y", "pkg"]}, invisible to any rule
163
169
  // that inspects `command` alone (the v0.1.0 gap).
164
- const args = Array.isArray(server.args) ? server.args.filter(a => typeof a === 'string') : [];
165
- const line = [server.command ?? '', ...args].join(' ').trim();
170
+ const command = typeof server.command === 'string' ? server.command : server.command?.path ?? '';
171
+ // Legacy Zed command.args and the top-level args are merged into one
172
+ // array -- MCP-004's absolute-path check scans `args` directly, so
173
+ // command.args must live there too or it silently evades detection.
174
+ const commandArgs = typeof server.command === 'object' && Array.isArray(server.command.args)
175
+ ? server.command.args.filter(a => typeof a === 'string')
176
+ : [];
177
+ const args = [
178
+ ...commandArgs,
179
+ ...(Array.isArray(server.args) ? server.args.filter(a => typeof a === 'string') : []),
180
+ ];
181
+ const line = [command, ...args].join(' ').trim();
166
182
  const serverFindings = [];
167
183
  // MCP-001: remote URL or network command execution
168
184
  if (line && (/https?:\/\//.test(line) || /\b(curl|wget)\b/.test(line))) {
@@ -202,10 +218,8 @@ export function analyzeConfig(filePath, content, isGitTracked) {
202
218
  }
203
219
  }
204
220
  // MCP-004: absolute path or path traversal outside repo
205
- const cmd = server.command ?? '';
206
- if (cmd.startsWith('/') ||
207
- args.some(a => a.startsWith('/')) ||
208
- /(?:^|[\s;|&])\.\.\//.test(line)) {
221
+ if ([command, ...args].some(value => REPO_ESCAPE_PATH.test(value.trimStart())) ||
222
+ PATH_TRAVERSAL_IN_LINE.test(line)) {
209
223
  serverFindings.push({
210
224
  file: filePath,
211
225
  rule_id: 'MCP-004',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mayhemx87/mcpscan",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Static scanner for malicious MCP config files in AI-integrated editor repos",
5
5
  "type": "module",
6
6
  "bin": {