fullcourtdefense-cli 1.3.0 → 1.3.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.
@@ -103,6 +103,55 @@ function scoreRisk(s) {
103
103
  }
104
104
  return { level, tags };
105
105
  }
106
+ function pushDiscoveredServer(out, name, cfgRaw, source, configPath) {
107
+ const cfg = (cfgRaw || {});
108
+ const command = typeof cfg.command === 'string' ? cfg.command : undefined;
109
+ const args = Array.isArray(cfg.args) ? cfg.args.map(String) : undefined;
110
+ const url = typeof cfg.url === 'string' ? cfg.url : (typeof cfg.serverUrl === 'string' ? cfg.serverUrl : undefined);
111
+ const env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {};
112
+ const envKeys = Object.keys(env);
113
+ const transport = url
114
+ ? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
115
+ : command ? 'stdio' : 'unknown';
116
+ const tools = Array.isArray(cfg.tools)
117
+ ? cfg.tools
118
+ .map((t) => {
119
+ if (typeof t === 'string')
120
+ return { name: t };
121
+ if (t && typeof t.name === 'string')
122
+ return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
123
+ return null;
124
+ })
125
+ .filter(Boolean)
126
+ : [];
127
+ const { level, tags } = scoreRisk({ serverName: name, command, args, url });
128
+ const warnings = [];
129
+ const blob = `${command || ''} ${(args || []).join(' ')} ${JSON.stringify(env)}`;
130
+ if (SECRET_VALUE.test(blob))
131
+ warnings.push('Hardcoded secret/API token detected in config (rotate + use env var).');
132
+ for (const [k, v] of Object.entries(env)) {
133
+ const val = String(v ?? '');
134
+ if (SECRET_KEY.test(k) && val && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val)) {
135
+ warnings.push(`Env "${k}" appears to contain a literal secret value.`);
136
+ }
137
+ }
138
+ out.push({
139
+ serverName: name,
140
+ command,
141
+ args,
142
+ url,
143
+ transport,
144
+ envKeys,
145
+ tools,
146
+ source,
147
+ configPath,
148
+ riskLevel: level,
149
+ riskTags: tags,
150
+ warnings,
151
+ probeMode: 'config',
152
+ proxyStatus: 'unknown',
153
+ });
154
+ }
106
155
  function parseConfigFile(filePath, source) {
107
156
  let raw;
108
157
  try {
@@ -118,56 +167,21 @@ function parseConfigFile(filePath, source) {
118
167
  catch {
119
168
  return [];
120
169
  }
121
- const servers = extractServerMap(parsed);
122
170
  const out = [];
123
- for (const [name, cfgRaw] of Object.entries(servers)) {
124
- const cfg = (cfgRaw || {});
125
- const command = typeof cfg.command === 'string' ? cfg.command : undefined;
126
- const args = Array.isArray(cfg.args) ? cfg.args.map(String) : undefined;
127
- const url = typeof cfg.url === 'string' ? cfg.url : (typeof cfg.serverUrl === 'string' ? cfg.serverUrl : undefined);
128
- const env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {};
129
- const envKeys = Object.keys(env);
130
- const transport = url
131
- ? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
132
- : command ? 'stdio' : 'unknown';
133
- const tools = Array.isArray(cfg.tools)
134
- ? cfg.tools
135
- .map((t) => {
136
- if (typeof t === 'string')
137
- return { name: t };
138
- if (t && typeof t.name === 'string')
139
- return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
140
- return null;
141
- })
142
- .filter(Boolean)
143
- : [];
144
- const { level, tags } = scoreRisk({ serverName: name, command, args, url });
145
- const warnings = [];
146
- const blob = `${command || ''} ${(args || []).join(' ')} ${JSON.stringify(env)}`;
147
- if (SECRET_VALUE.test(blob))
148
- warnings.push('Hardcoded secret/API token detected in config (rotate + use env var).');
149
- for (const [k, v] of Object.entries(env)) {
150
- const val = String(v ?? '');
151
- if (SECRET_KEY.test(k) && val && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val)) {
152
- warnings.push(`Env "${k}" appears to contain a literal secret value.`);
171
+ for (const [name, cfgRaw] of Object.entries(extractServerMap(parsed))) {
172
+ pushDiscoveredServer(out, name, cfgRaw, source, filePath);
173
+ }
174
+ // Claude Code stores MCP per-project inside ~/.claude.json
175
+ if (/claude code/i.test(source) && parsed?.projects && typeof parsed.projects === 'object') {
176
+ for (const [projectPath, projectCfg] of Object.entries(parsed.projects)) {
177
+ const projectServers = projectCfg?.mcpServers;
178
+ if (!projectServers || typeof projectServers !== 'object')
179
+ continue;
180
+ const projectLabel = projectPath.split(/[/\\]/).filter(Boolean).slice(-1)[0] || projectPath;
181
+ for (const [name, cfgRaw] of Object.entries(projectServers)) {
182
+ pushDiscoveredServer(out, name, cfgRaw, `${source} · ${projectLabel}`, filePath);
153
183
  }
154
184
  }
155
- out.push({
156
- serverName: name,
157
- command,
158
- args,
159
- url,
160
- transport,
161
- envKeys,
162
- tools,
163
- source,
164
- configPath: filePath,
165
- riskLevel: level,
166
- riskTags: tags,
167
- warnings,
168
- probeMode: 'config',
169
- proxyStatus: 'unknown',
170
- });
171
185
  }
172
186
  return out;
173
187
  }
@@ -256,13 +270,14 @@ async function enrichWithDeepProbe(servers, silent) {
256
270
  server.probeMode = 'deep';
257
271
  }
258
272
  }
259
- /** Merge servers found in multiple files; keep richest record, union sources. */
273
+ /** Dedupe within the same config file + server name only (keep separate entries per client config path). */
260
274
  function dedupe(servers) {
261
- const byName = new Map();
275
+ const byKey = new Map();
262
276
  for (const s of servers) {
263
- const existing = byName.get(s.serverName);
277
+ const key = `${s.configPath.toLowerCase()}::${s.serverName.toLowerCase()}`;
278
+ const existing = byKey.get(key);
264
279
  if (!existing) {
265
- byName.set(s.serverName, { ...s, source: s.source });
280
+ byKey.set(key, { ...s });
266
281
  continue;
267
282
  }
268
283
  if (!existing.source.includes(s.source))
@@ -277,7 +292,7 @@ function dedupe(servers) {
277
292
  if (s.probeMode === 'deep')
278
293
  existing.probeMode = 'deep';
279
294
  }
280
- return [...byName.values()];
295
+ return [...byKey.values()];
281
296
  }
282
297
  const COLOR = {
283
298
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {