@mayhemx87/mcpscan 0.3.1 → 0.4.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.4.0
95
95
  hooks:
96
96
  - id: mcpscan
97
97
  ```
@@ -115,11 +115,14 @@ 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 |
120
122
 
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
123
+ Both `mcpServers` and VS Code's `servers` keys are understood, as is Zed's
124
+ `context_servers` key; `command` and `args` are analyzed together, including
125
+ Zed's legacy nested command format; remote (`url`-based) servers are covered. Nested
123
126
  occurrences of these paths are scanned too (any subdirectory can be opened as a
124
127
  workspace root). Only `.git/` and `node_modules/` are skipped; `.gitignore` is
125
128
  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
@@ -88,8 +88,9 @@ export function isEmbeddedHostFile(filePath) {
88
88
  }
89
89
  /** Extract the server map from any known config shape:
90
90
  * - dedicated files: { mcpServers: {...} } or VS Code's { servers: {...} }
91
- * - embedded hosts: { mcpServers: {...} } (Claude/Gemini settings) or
92
- * { mcp: { servers: {...} } } (VS Code settings.json)
91
+ * - embedded hosts: { mcpServers: {...} } (Claude/Gemini settings),
92
+ * { mcp: { servers: {...} } } (VS Code settings.json), or
93
+ * { context_servers: {...} } (Zed settings.json)
93
94
  * Also returns the JSON path to the map so findings can carry line numbers. */
94
95
  function extractServers(config) {
95
96
  if (config.mcpServers && typeof config.mcpServers === 'object') {
@@ -102,6 +103,9 @@ function extractServers(config) {
102
103
  if (mcp && typeof mcp === 'object' && mcp.servers && typeof mcp.servers === 'object') {
103
104
  return { servers: mcp.servers, path: ['mcp', 'servers'] };
104
105
  }
106
+ if (config.context_servers && typeof config.context_servers === 'object') {
107
+ return { servers: config.context_servers, path: ['context_servers'] };
108
+ }
105
109
  return null;
106
110
  }
107
111
  /**
@@ -161,8 +165,18 @@ export function analyzeConfig(filePath, content, isGitTracked) {
161
165
  // Analyze command AND args as one line -- the dominant real-world shape
162
166
  // is {"command": "npx", "args": ["-y", "pkg"]}, invisible to any rule
163
167
  // 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();
168
+ const command = typeof server.command === 'string' ? server.command : server.command?.path ?? '';
169
+ // Legacy Zed command.args and the top-level args are merged into one
170
+ // array -- MCP-004's absolute-path check scans `args` directly, so
171
+ // command.args must live there too or it silently evades detection.
172
+ const commandArgs = typeof server.command === 'object' && Array.isArray(server.command.args)
173
+ ? server.command.args.filter(a => typeof a === 'string')
174
+ : [];
175
+ const args = [
176
+ ...commandArgs,
177
+ ...(Array.isArray(server.args) ? server.args.filter(a => typeof a === 'string') : []),
178
+ ];
179
+ const line = [command, ...args].join(' ').trim();
166
180
  const serverFindings = [];
167
181
  // MCP-001: remote URL or network command execution
168
182
  if (line && (/https?:\/\//.test(line) || /\b(curl|wget)\b/.test(line))) {
@@ -202,7 +216,7 @@ export function analyzeConfig(filePath, content, isGitTracked) {
202
216
  }
203
217
  }
204
218
  // MCP-004: absolute path or path traversal outside repo
205
- const cmd = server.command ?? '';
219
+ const cmd = command;
206
220
  if (cmd.startsWith('/') ||
207
221
  args.some(a => a.startsWith('/')) ||
208
222
  /(?:^|[\s;|&])\.\.\//.test(line)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mayhemx87/mcpscan",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Static scanner for malicious MCP config files in AI-integrated editor repos",
5
5
  "type": "module",
6
6
  "bin": {