fullcourtdefense-cli 1.2.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.
@@ -23,3 +23,11 @@ export interface DesktopDiscoveryHost {
23
23
  probeMode: 'config' | 'deep';
24
24
  }
25
25
  export declare function discoverCommand(args: DiscoverArgs, config: BotGuardConfig): Promise<void>;
26
+ /** Upload-only discover — used after install-mcp-gateway --upload. */
27
+ export declare function runDiscoverUpload(args: {
28
+ apiKey?: string;
29
+ apiUrl?: string;
30
+ userEmail?: string;
31
+ silent?: boolean;
32
+ connectorName?: string;
33
+ }, config: BotGuardConfig): Promise<void>;
@@ -34,12 +34,14 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.discoverCommand = discoverCommand;
37
+ exports.runDiscoverUpload = runDiscoverUpload;
37
38
  const crypto = __importStar(require("crypto"));
38
39
  const fs = __importStar(require("fs"));
39
40
  const os = __importStar(require("os"));
40
- const path = __importStar(require("path"));
41
41
  const discoverProxy_1 = require("./discoverProxy");
42
42
  const discoverSchedule_1 = require("./discoverSchedule");
43
+ const discoverPaths_1 = require("./discoverPaths");
44
+ const knownMcpServers_1 = require("./knownMcpServers");
43
45
  const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
44
46
  const SECRET_VALUE = /sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{20,}/;
45
47
  const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
@@ -58,68 +60,9 @@ function buildHostMetadata(userEmail, probeMode = 'config') {
58
60
  probeMode,
59
61
  };
60
62
  }
61
- /** Where MCP client configs live, by client + OS. */
63
+ /** Where MCP client configs live see discoverPaths.ts for the canonical list. */
62
64
  function candidateConfigPaths(cwd, extra) {
63
- const home = os.homedir();
64
- const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
65
- const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
66
- const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
67
- const list = [
68
- // Cursor
69
- { path: path.join(home, '.cursor', 'mcp.json'), source: 'Cursor (global)' },
70
- { path: path.join(cwd, '.cursor', 'mcp.json'), source: 'Cursor (project)' },
71
- // Cursor managed (enterprise)
72
- { path: path.join(appData, 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, win)' },
73
- { path: path.join(home, 'Library', 'Application Support', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, mac)' },
74
- { path: '/etc/cursor/managed-mcp.json', source: 'Cursor (managed, linux)' },
75
- // Claude Desktop
76
- { path: path.join(appData, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (win)' },
77
- { path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (mac)' },
78
- { path: path.join(xdg, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (linux)' },
79
- // Claude Code
80
- { path: path.join(home, '.claude.json'), source: 'Claude Code' },
81
- { path: path.join(home, '.claude', 'settings.json'), source: 'Claude Code (global)' },
82
- { path: path.join(cwd, '.claude', 'settings.json'), source: 'Claude Code (project)' },
83
- { path: path.join(cwd, '.mcp.json'), source: 'Claude Code (.mcp.json project)' },
84
- // Gemini CLI
85
- { path: path.join(home, '.gemini', 'settings.json'), source: 'Gemini CLI' },
86
- { path: path.join(xdg, 'gemini', 'settings.json'), source: 'Gemini CLI (xdg)' },
87
- // OpenAI Codex
88
- { path: path.join(home, '.codex', 'mcp.json'), source: 'Codex' },
89
- { path: path.join(xdg, 'codex', 'mcp.json'), source: 'Codex (xdg)' },
90
- // VS Code (project + user settings)
91
- { path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)' },
92
- { path: path.join(appData, 'Code', 'User', 'settings.json'), source: 'VS Code (user, win)' },
93
- { path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), source: 'VS Code (user, mac)' },
94
- { path: path.join(xdg, 'Code', 'User', 'settings.json'), source: 'VS Code (user, linux)' },
95
- // Windsurf
96
- { path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), source: 'Windsurf' },
97
- // Kiro / generic
98
- { path: path.join(home, '.kiro', 'mcp.json'), source: 'Kiro' },
99
- // Generic / repo-root
100
- { path: path.join(cwd, '.mcp.json'), source: 'Repo (.mcp.json)' },
101
- { path: path.join(cwd, 'mcp.json'), source: 'Repo (mcp.json)' },
102
- ];
103
- if (process.platform === 'win32') {
104
- list.push({ path: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, program data)' });
105
- const packagesDir = path.join(localAppData, 'Packages');
106
- try {
107
- for (const packageName of fs.readdirSync(packagesDir)) {
108
- if (!/^Claude_/i.test(packageName))
109
- continue;
110
- list.push({
111
- path: path.join(packagesDir, packageName, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json'),
112
- source: 'Claude Desktop (win MSIX)',
113
- });
114
- }
115
- }
116
- catch {
117
- // Missing Packages directory is normal on non-Store installs.
118
- }
119
- }
120
- if (extra)
121
- list.push({ path: path.resolve(extra), source: 'Custom path' });
122
- return list;
65
+ return (0, discoverPaths_1.discoverScanTargets)(cwd, extra);
123
66
  }
124
67
  /** MCP servers can be declared under several keys across clients. */
125
68
  function extractServerMap(parsed) {
@@ -160,6 +103,55 @@ function scoreRisk(s) {
160
103
  }
161
104
  return { level, tags };
162
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
+ }
163
155
  function parseConfigFile(filePath, source) {
164
156
  let raw;
165
157
  try {
@@ -175,56 +167,21 @@ function parseConfigFile(filePath, source) {
175
167
  catch {
176
168
  return [];
177
169
  }
178
- const servers = extractServerMap(parsed);
179
170
  const out = [];
180
- for (const [name, cfgRaw] of Object.entries(servers)) {
181
- const cfg = (cfgRaw || {});
182
- const command = typeof cfg.command === 'string' ? cfg.command : undefined;
183
- const args = Array.isArray(cfg.args) ? cfg.args.map(String) : undefined;
184
- const url = typeof cfg.url === 'string' ? cfg.url : (typeof cfg.serverUrl === 'string' ? cfg.serverUrl : undefined);
185
- const env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {};
186
- const envKeys = Object.keys(env);
187
- const transport = url
188
- ? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
189
- : command ? 'stdio' : 'unknown';
190
- const tools = Array.isArray(cfg.tools)
191
- ? cfg.tools
192
- .map((t) => {
193
- if (typeof t === 'string')
194
- return { name: t };
195
- if (t && typeof t.name === 'string')
196
- return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
197
- return null;
198
- })
199
- .filter(Boolean)
200
- : [];
201
- const { level, tags } = scoreRisk({ serverName: name, command, args, url });
202
- const warnings = [];
203
- const blob = `${command || ''} ${(args || []).join(' ')} ${JSON.stringify(env)}`;
204
- if (SECRET_VALUE.test(blob))
205
- warnings.push('Hardcoded secret/API token detected in config (rotate + use env var).');
206
- for (const [k, v] of Object.entries(env)) {
207
- const val = String(v ?? '');
208
- if (SECRET_KEY.test(k) && val && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val)) {
209
- 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);
210
183
  }
211
184
  }
212
- out.push({
213
- serverName: name,
214
- command,
215
- args,
216
- url,
217
- transport,
218
- envKeys,
219
- tools,
220
- source,
221
- configPath: filePath,
222
- riskLevel: level,
223
- riskTags: tags,
224
- warnings,
225
- probeMode: 'config',
226
- proxyStatus: 'unknown',
227
- });
228
185
  }
229
186
  return out;
230
187
  }
@@ -313,13 +270,14 @@ async function enrichWithDeepProbe(servers, silent) {
313
270
  server.probeMode = 'deep';
314
271
  }
315
272
  }
316
- /** 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). */
317
274
  function dedupe(servers) {
318
- const byName = new Map();
275
+ const byKey = new Map();
319
276
  for (const s of servers) {
320
- const existing = byName.get(s.serverName);
277
+ const key = `${s.configPath.toLowerCase()}::${s.serverName.toLowerCase()}`;
278
+ const existing = byKey.get(key);
321
279
  if (!existing) {
322
- byName.set(s.serverName, { ...s, source: s.source });
280
+ byKey.set(key, { ...s });
323
281
  continue;
324
282
  }
325
283
  if (!existing.source.includes(s.source))
@@ -334,7 +292,7 @@ function dedupe(servers) {
334
292
  if (s.probeMode === 'deep')
335
293
  existing.probeMode = 'deep';
336
294
  }
337
- return [...byName.values()];
295
+ return [...byKey.values()];
338
296
  }
339
297
  const COLOR = {
340
298
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
@@ -353,6 +311,16 @@ function applyProxyClassification(servers, scanned, cwd) {
353
311
  if (server.proxyStatus === 'direct' && server.riskLevel !== 'low') {
354
312
  server.warnings.push('Direct MCP — not proxied through Full Court Defense gateway.');
355
313
  }
314
+ const info = (0, knownMcpServers_1.knownMcpServerInfo)(server.serverName, server.command);
315
+ if (info) {
316
+ server.serverKind = info.category;
317
+ server.serverDescription = info.description;
318
+ }
319
+ if (server.proxyStatus === 'direct') {
320
+ const hint = (0, knownMcpServers_1.directServerFixHint)(server.serverName);
321
+ if (hint && !server.warnings.includes(hint))
322
+ server.warnings.push(hint);
323
+ }
356
324
  }
357
325
  return (0, discoverProxy_1.buildClientCoverage)(scanned, servers, cwd);
358
326
  }
@@ -383,6 +351,8 @@ async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorNa
383
351
  probeMode: s.probeMode,
384
352
  proxyStatus: s.proxyStatus,
385
353
  wrapsServerName: s.wrapsServerName,
354
+ serverKind: s.serverKind,
355
+ serverDescription: s.serverDescription,
386
356
  tools: s.tools,
387
357
  })),
388
358
  };
@@ -427,10 +397,10 @@ async function discoverCommand(args, config) {
427
397
  const scanned = [];
428
398
  let found = [];
429
399
  for (const c of candidates) {
430
- if (!fs.existsSync(c.path))
431
- continue;
432
400
  scanned.push({ path: c.path, source: c.source });
433
- found.push(...parseConfigFile(c.path, c.source));
401
+ if (fs.existsSync(c.path)) {
402
+ found.push(...parseConfigFile(c.path, c.source));
403
+ }
434
404
  }
435
405
  found = dedupe(found);
436
406
  const clientCoverage = applyProxyClassification(found, scanned, cwd);
@@ -455,7 +425,7 @@ async function discoverCommand(args, config) {
455
425
  const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
456
426
  if (!apiKey)
457
427
  process.exit(1);
458
- if (found.length === 0)
428
+ if (scanned.length === 0)
459
429
  return;
460
430
  await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
461
431
  }
@@ -469,16 +439,12 @@ async function discoverCommand(args, config) {
469
439
  console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
470
440
  return;
471
441
  }
472
- console.log(`${COLOR.bold}Config files found:${COLOR.reset}`);
473
- for (const s of scanned)
474
- console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} ${s.path}`);
475
- console.log('');
476
- if (found.length === 0) {
477
- console.log(`${COLOR.yellow}Config files exist but declare no MCP servers.${COLOR.reset}`);
478
- return;
442
+ console.log(`${COLOR.bold}AI clients checked (${scanned.length}):${COLOR.reset}`);
443
+ for (const s of scanned) {
444
+ const exists = fs.existsSync(s.path);
445
+ console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} → ${s.path}${exists ? '' : ` ${COLOR.yellow}(not created yet)${COLOR.reset}`}`);
479
446
  }
480
- const order = { critical: 0, high: 1, medium: 2, low: 3 };
481
- found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
447
+ console.log('');
482
448
  if (clientCoverage.length > 0) {
483
449
  console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
484
450
  for (const row of clientCoverage) {
@@ -487,15 +453,40 @@ async function discoverCommand(args, config) {
487
453
  ? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
488
454
  : '';
489
455
  console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} — ${row.mcpServerCount} MCP · ${gateway}${hooks ? ` · ${hooks}` : ''}`);
456
+ if (row.clientKey === 'claude_desktop' && !row.mcpGatewayInstalled && row.mcpServerCount === 0) {
457
+ console.log(` ${COLOR.gray}Install: fullcourtdefense install-claude-desktop-mcp-gateway --shield-id ... --mcp-command npm --mcp-args "run mcp"${COLOR.reset}`);
458
+ }
490
459
  }
491
460
  console.log('');
492
461
  }
462
+ if (found.length === 0) {
463
+ console.log(`${COLOR.yellow}No MCP servers configured in these clients yet.${COLOR.reset}`);
464
+ console.log(`${COLOR.gray}Install gateway everywhere: fullcourtdefense install-mcp-gateway --clients all --shield-id ... --shield-key ... --mcp-command npm --mcp-args "run mcp"${COLOR.reset}`);
465
+ if (args.upload === 'true') {
466
+ const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
467
+ const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
468
+ if (!apiKey) {
469
+ console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset}`);
470
+ process.exit(1);
471
+ }
472
+ console.log('');
473
+ await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
474
+ }
475
+ else {
476
+ console.log(`${COLOR.gray}Run with --upload to push client coverage into AI Inventory.${COLOR.reset}`);
477
+ }
478
+ return;
479
+ }
480
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
481
+ found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
493
482
  console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
494
483
  for (const s of found) {
495
484
  const col = riskColor(s.riskLevel);
496
485
  const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
497
486
  const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
498
487
  console.log(` ${col}${COLOR.bold}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${s.serverName}${COLOR.reset} ${COLOR.gray}(${s.transport}${s.probeMode === 'deep' ? ', deep' : ''})${COLOR.reset} ${proxyCol}${proxyStatusLabel(s.proxyStatus)}${COLOR.reset}`);
488
+ if (s.serverDescription)
489
+ console.log(` ${COLOR.gray}about:${COLOR.reset} ${s.serverDescription}`);
499
490
  console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
500
491
  if (s.riskTags.length)
501
492
  console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
@@ -527,3 +518,15 @@ async function discoverCommand(args, config) {
527
518
  console.log(`${COLOR.gray}Add --schedule daily for automatic daily upload (MDM/fleet).${COLOR.reset}`);
528
519
  }
529
520
  }
521
+ /** Upload-only discover — used after install-mcp-gateway --upload. */
522
+ async function runDiscoverUpload(args, config) {
523
+ await discoverCommand({
524
+ type: 'mcp',
525
+ upload: 'true',
526
+ silent: args.silent ? 'true' : undefined,
527
+ apiKey: args.apiKey,
528
+ apiUrl: args.apiUrl,
529
+ userEmail: args.userEmail,
530
+ connectorName: args.connectorName,
531
+ }, config);
532
+ }
@@ -0,0 +1,18 @@
1
+ export interface ConfigPathCandidate {
2
+ path: string;
3
+ source: string;
4
+ clientKey: string;
5
+ }
6
+ export interface ClaudeDesktopConfigCandidate {
7
+ file: string;
8
+ source: string;
9
+ exists: boolean;
10
+ }
11
+ export declare function claudeDesktopMsixCandidates(): ClaudeDesktopConfigCandidate[];
12
+ export declare function claudeDesktopConfigCandidates(): ClaudeDesktopConfigCandidate[];
13
+ export declare function claudeDesktopLikelyInstalled(): boolean;
14
+ export declare function claudeDesktopInstallTargets(configPath?: string): ClaudeDesktopConfigCandidate[];
15
+ /** All MCP client config locations discover / install should check. */
16
+ export declare function candidateConfigPaths(cwd: string, extra?: string): ConfigPathCandidate[];
17
+ /** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
18
+ export declare function discoverScanTargets(cwd: string, extra?: string): ConfigPathCandidate[];
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.claudeDesktopMsixCandidates = claudeDesktopMsixCandidates;
37
+ exports.claudeDesktopConfigCandidates = claudeDesktopConfigCandidates;
38
+ exports.claudeDesktopLikelyInstalled = claudeDesktopLikelyInstalled;
39
+ exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
40
+ exports.candidateConfigPaths = candidateConfigPaths;
41
+ exports.discoverScanTargets = discoverScanTargets;
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ function claudeDesktopDocumentedPath() {
46
+ const home = os.homedir();
47
+ if (process.platform === 'win32') {
48
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
49
+ return path.join(appData, 'Claude', 'claude_desktop_config.json');
50
+ }
51
+ if (process.platform === 'darwin') {
52
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
53
+ }
54
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
55
+ return path.join(xdg, 'Claude', 'claude_desktop_config.json');
56
+ }
57
+ function claudeDesktopMsixCandidates() {
58
+ if (process.platform !== 'win32')
59
+ return [];
60
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
61
+ const packagesDir = path.join(localAppData, 'Packages');
62
+ try {
63
+ return fs.readdirSync(packagesDir)
64
+ .filter(name => /^Claude_/i.test(name))
65
+ .map(name => ({
66
+ file: path.join(packagesDir, name, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json'),
67
+ source: 'Claude Desktop (win MSIX)',
68
+ exists: fs.existsSync(path.join(packagesDir, name, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json')),
69
+ }));
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ function claudeDesktopConfigCandidates() {
76
+ const documented = claudeDesktopDocumentedPath();
77
+ const normal = {
78
+ file: documented,
79
+ source: process.platform === 'win32'
80
+ ? 'Claude Desktop (win)'
81
+ : process.platform === 'darwin'
82
+ ? 'Claude Desktop (mac)'
83
+ : 'Claude Desktop (linux)',
84
+ exists: fs.existsSync(documented),
85
+ };
86
+ const seen = new Set();
87
+ return [...claudeDesktopMsixCandidates(), normal].filter(candidate => {
88
+ const key = path.resolve(candidate.file).toLowerCase();
89
+ if (seen.has(key))
90
+ return false;
91
+ seen.add(key);
92
+ return true;
93
+ });
94
+ }
95
+ function claudeDesktopLikelyInstalled() {
96
+ if (claudeDesktopConfigCandidates().some(candidate => candidate.exists))
97
+ return true;
98
+ if (claudeDesktopMsixCandidates().length > 0)
99
+ return true;
100
+ if (process.platform === 'darwin' && fs.existsSync('/Applications/Claude.app'))
101
+ return true;
102
+ if (process.platform === 'win32') {
103
+ const pf = process.env.ProgramFiles || 'C:\\Program Files';
104
+ if (fs.existsSync(path.join(pf, 'Claude', 'Claude.exe')))
105
+ return true;
106
+ }
107
+ return fs.existsSync(path.dirname(claudeDesktopDocumentedPath()));
108
+ }
109
+ function claudeDesktopInstallTargets(configPath) {
110
+ if (configPath) {
111
+ const file = path.resolve(configPath);
112
+ return [{ file, source: 'Claude Desktop (custom)', exists: fs.existsSync(file) }];
113
+ }
114
+ const candidates = claudeDesktopConfigCandidates();
115
+ const existing = candidates.filter(candidate => candidate.exists);
116
+ if (existing.length > 0)
117
+ return existing;
118
+ const msix = candidates.find(candidate => candidate.source.includes('MSIX'));
119
+ if (msix)
120
+ return [msix];
121
+ return [candidates[candidates.length - 1] || {
122
+ file: claudeDesktopDocumentedPath(),
123
+ source: 'Claude Desktop',
124
+ exists: false,
125
+ }];
126
+ }
127
+ /** All MCP client config locations discover / install should check. */
128
+ function candidateConfigPaths(cwd, extra) {
129
+ const home = os.homedir();
130
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
131
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
132
+ const list = [
133
+ { path: path.join(home, '.cursor', 'mcp.json'), source: 'Cursor (global)', clientKey: 'cursor' },
134
+ { path: path.join(cwd, '.cursor', 'mcp.json'), source: 'Cursor (project)', clientKey: 'cursor' },
135
+ { path: path.join(appData, 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, win)', clientKey: 'cursor' },
136
+ { path: path.join(home, 'Library', 'Application Support', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, mac)', clientKey: 'cursor' },
137
+ { path: '/etc/cursor/managed-mcp.json', source: 'Cursor (managed, linux)', clientKey: 'cursor' },
138
+ { path: path.join(home, '.claude.json'), source: 'Claude Code', clientKey: 'claude_code' },
139
+ { path: path.join(home, '.claude', 'settings.json'), source: 'Claude Code (global)', clientKey: 'claude_code' },
140
+ { path: path.join(cwd, '.claude', 'settings.json'), source: 'Claude Code (project)', clientKey: 'claude_code' },
141
+ { path: path.join(cwd, '.mcp.json'), source: 'Claude Code (.mcp.json project)', clientKey: 'claude_code' },
142
+ { path: path.join(home, '.gemini', 'settings.json'), source: 'Gemini CLI', clientKey: 'gemini_cli' },
143
+ { path: path.join(xdg, 'gemini', 'settings.json'), source: 'Gemini CLI (xdg)', clientKey: 'gemini_cli' },
144
+ { path: path.join(home, '.codex', 'mcp.json'), source: 'Codex', clientKey: 'codex' },
145
+ { path: path.join(xdg, 'codex', 'mcp.json'), source: 'Codex (xdg)', clientKey: 'codex' },
146
+ { path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)', clientKey: 'vscode' },
147
+ { path: path.join(appData, 'Code', 'User', 'settings.json'), source: 'VS Code (user, win)', clientKey: 'vscode' },
148
+ { path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), source: 'VS Code (user, mac)', clientKey: 'vscode' },
149
+ { path: path.join(xdg, 'Code', 'User', 'settings.json'), source: 'VS Code (user, linux)', clientKey: 'vscode' },
150
+ { path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), source: 'Windsurf', clientKey: 'windsurf' },
151
+ { path: path.join(home, '.kiro', 'mcp.json'), source: 'Kiro', clientKey: 'kiro' },
152
+ { path: path.join(cwd, '.mcp.json'), source: 'Repo (.mcp.json)', clientKey: 'other' },
153
+ { path: path.join(cwd, 'mcp.json'), source: 'Repo (mcp.json)', clientKey: 'other' },
154
+ ];
155
+ for (const candidate of claudeDesktopConfigCandidates()) {
156
+ list.push({ path: candidate.file, source: candidate.source, clientKey: 'claude_desktop' });
157
+ }
158
+ if (process.platform === 'win32') {
159
+ list.push({
160
+ path: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Cursor', 'managed-mcp.json'),
161
+ source: 'Cursor (managed, program data)',
162
+ clientKey: 'cursor',
163
+ });
164
+ }
165
+ if (extra) {
166
+ list.push({ path: path.resolve(extra), source: 'Custom path', clientKey: 'other' });
167
+ }
168
+ const seen = new Set();
169
+ return list.filter(item => {
170
+ const key = path.resolve(item.path).toLowerCase();
171
+ if (seen.has(key))
172
+ return false;
173
+ seen.add(key);
174
+ return true;
175
+ });
176
+ }
177
+ /** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
178
+ function discoverScanTargets(cwd, extra) {
179
+ const candidates = candidateConfigPaths(cwd, extra);
180
+ const existing = candidates.filter(item => fs.existsSync(item.path));
181
+ const implicit = [];
182
+ if (claudeDesktopLikelyInstalled()) {
183
+ for (const target of claudeDesktopInstallTargets()) {
184
+ if (!existing.some(item => path.resolve(item.path).toLowerCase() === path.resolve(target.file).toLowerCase())) {
185
+ implicit.push({ path: target.file, source: target.source, clientKey: 'claude_desktop' });
186
+ }
187
+ }
188
+ }
189
+ const cursorGlobal = path.join(os.homedir(), '.cursor', 'mcp.json');
190
+ if (fs.existsSync(path.join(os.homedir(), '.cursor')) && !existing.some(item => path.resolve(item.path).toLowerCase() === cursorGlobal.toLowerCase())) {
191
+ implicit.push({ path: cursorGlobal, source: 'Cursor (global)', clientKey: 'cursor' });
192
+ }
193
+ return [...existing, ...implicit];
194
+ }
@@ -0,0 +1,8 @@
1
+ export interface KnownMcpServerInfo {
2
+ title: string;
3
+ description: string;
4
+ category: 'browser' | 'filesystem' | 'database' | 'gateway' | 'other';
5
+ recommendGateway: boolean;
6
+ }
7
+ export declare function knownMcpServerInfo(serverName: string, command?: string): KnownMcpServerInfo | undefined;
8
+ export declare function directServerFixHint(serverName: string): string | undefined;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.knownMcpServerInfo = knownMcpServerInfo;
4
+ exports.directServerFixHint = directServerFixHint;
5
+ const KNOWN = [
6
+ {
7
+ match: /playwright/i,
8
+ info: {
9
+ title: 'Playwright',
10
+ description: 'Browser automation — AI can open sites, click, and act as the logged-in user.',
11
+ category: 'browser',
12
+ recommendGateway: true,
13
+ },
14
+ },
15
+ {
16
+ match: /puppeteer|selenium|browser/i,
17
+ info: {
18
+ title: 'Browser automation',
19
+ description: 'Controls a web browser on the developer machine.',
20
+ category: 'browser',
21
+ recommendGateway: true,
22
+ },
23
+ },
24
+ {
25
+ match: /filesystem|server-filesystem/i,
26
+ info: {
27
+ title: 'Filesystem',
28
+ description: 'Reads and writes local files — high exfiltration risk.',
29
+ category: 'filesystem',
30
+ recommendGateway: true,
31
+ },
32
+ },
33
+ {
34
+ match: /postgres|mysql|sqlite|database|supabase/i,
35
+ info: {
36
+ title: 'Database',
37
+ description: 'Queries databases directly from the AI client.',
38
+ category: 'database',
39
+ recommendGateway: true,
40
+ },
41
+ },
42
+ {
43
+ match: /agentguard-gateway|fullcourtdefense-gateway|fcd-gateway/i,
44
+ info: {
45
+ title: 'FCD MCP gateway',
46
+ description: 'Full Court Defense proxy — tool calls checked by Shield and Action Policies.',
47
+ category: 'gateway',
48
+ recommendGateway: false,
49
+ },
50
+ },
51
+ ];
52
+ function knownMcpServerInfo(serverName, command) {
53
+ const hay = `${serverName} ${command || ''}`;
54
+ for (const entry of KNOWN) {
55
+ if (entry.match.test(hay))
56
+ return entry.info;
57
+ }
58
+ return undefined;
59
+ }
60
+ function directServerFixHint(serverName) {
61
+ const info = knownMcpServerInfo(serverName);
62
+ if (!info?.recommendGateway)
63
+ return undefined;
64
+ if (/playwright/i.test(serverName)) {
65
+ return 'Remove Playwright from mcp.json or route it through: fullcourtdefense install-mcp-gateway --clients all --mcp-command ...';
66
+ }
67
+ return 'Route through FCD gateway: fullcourtdefense install-mcp-gateway --clients all --mcp-command npm --mcp-args "run mcp"';
68
+ }
@@ -34,6 +34,8 @@ export interface InstallMcpGatewayArgs extends McpGatewayArgs {
34
34
  cursorProject?: string;
35
35
  claudeCodeScope?: string;
36
36
  serverName?: string;
37
+ upload?: string;
38
+ apiKey?: string;
37
39
  }
38
40
  export declare function mcpGatewayCommand(args: McpGatewayArgs, config: BotGuardConfig): Promise<void>;
39
41
  export declare function installCursorMcpGatewayCommand(args: InstallCursorMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
@@ -42,6 +42,8 @@ const fs = __importStar(require("fs"));
42
42
  const os = __importStar(require("os"));
43
43
  const path = __importStar(require("path"));
44
44
  const child_process_1 = require("child_process");
45
+ const discoverPaths_1 = require("./discoverPaths");
46
+ const discover_1 = require("./discover");
45
47
  const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
46
48
  const MANAGED_SERVER_NAME = 'agentguard-gateway';
47
49
  function parseArgsList(value) {
@@ -672,83 +674,6 @@ function writeMcpServerConfig(file, serverName, nodeExe, commandArgs) {
672
674
  fs.mkdirSync(path.dirname(file), { recursive: true });
673
675
  fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
674
676
  }
675
- function dedupePaths(candidates) {
676
- const seen = new Set();
677
- return candidates.filter(candidate => {
678
- const key = path.resolve(candidate.file).toLowerCase();
679
- if (seen.has(key))
680
- return false;
681
- seen.add(key);
682
- return true;
683
- });
684
- }
685
- function claudeDesktopMcpPath() {
686
- const home = os.homedir();
687
- if (process.platform === 'win32') {
688
- const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
689
- return path.join(appData, 'Claude', 'claude_desktop_config.json');
690
- }
691
- if (process.platform === 'darwin') {
692
- return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
693
- }
694
- const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
695
- return path.join(xdg, 'Claude', 'claude_desktop_config.json');
696
- }
697
- function claudeDesktopMsixCandidates() {
698
- if (process.platform !== 'win32')
699
- return [];
700
- const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
701
- const packagesDir = path.join(localAppData, 'Packages');
702
- let packageNames = [];
703
- try {
704
- packageNames = fs.readdirSync(packagesDir)
705
- .filter(name => /^Claude_/i.test(name));
706
- }
707
- catch {
708
- return [];
709
- }
710
- return packageNames.map(name => {
711
- const file = path.join(packagesDir, name, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json');
712
- return {
713
- file,
714
- source: 'Claude Desktop (Microsoft Store/MSIX)',
715
- exists: fs.existsSync(file),
716
- };
717
- });
718
- }
719
- function claudeDesktopMcpCandidates() {
720
- const documented = claudeDesktopMcpPath();
721
- const normal = {
722
- file: documented,
723
- source: process.platform === 'win32'
724
- ? 'Claude Desktop (Windows documented AppData)'
725
- : process.platform === 'darwin'
726
- ? 'Claude Desktop (macOS)'
727
- : 'Claude Desktop (Linux/XDG)',
728
- exists: fs.existsSync(documented),
729
- };
730
- // Store/MSIX Claude Desktop reads a virtualized AppData path. Prefer it when present.
731
- return dedupePaths([
732
- ...claudeDesktopMsixCandidates(),
733
- normal,
734
- ]);
735
- }
736
- function claudeDesktopInstallTargets(configPath) {
737
- if (configPath) {
738
- const file = path.resolve(configPath);
739
- return [{ file, source: 'Claude Desktop (custom config path)', exists: fs.existsSync(file) }];
740
- }
741
- const candidates = claudeDesktopMcpCandidates();
742
- const existing = candidates.filter(candidate => candidate.exists);
743
- if (existing.length > 0)
744
- return existing;
745
- const msix = candidates.find(candidate => candidate.source.includes('MSIX'));
746
- return [msix || candidates[candidates.length - 1] || {
747
- file: claudeDesktopMcpPath(),
748
- source: 'Claude Desktop',
749
- exists: false,
750
- }];
751
- }
752
677
  function normalizeClaudeCodeScope(scope) {
753
678
  if (scope === 'project' || scope === 'user' || scope === 'local')
754
679
  return scope;
@@ -791,9 +716,8 @@ function detectInstallClients(cursorProject) {
791
716
  clients.push('cursor');
792
717
  if (commandAvailable('claude'))
793
718
  clients.push('claude-code');
794
- if (claudeDesktopMcpCandidates().some(candidate => candidate.exists) || fs.existsSync(path.dirname(claudeDesktopMcpPath()))) {
719
+ if ((0, discoverPaths_1.claudeDesktopLikelyInstalled)())
795
720
  clients.push('claude-desktop');
796
- }
797
721
  return clients;
798
722
  }
799
723
  function clientInstallArgs(args) {
@@ -841,7 +765,9 @@ async function installCursorMcpGatewayCommand(args, config) {
841
765
  async function installMcpGatewayCommand(args, config) {
842
766
  const cursorProject = args.cursorProject === 'true';
843
767
  const selection = parseClientList(args.clients);
844
- const clients = selection === 'auto' ? detectInstallClients(cursorProject) : selection;
768
+ const clients = selection === 'auto'
769
+ ? detectInstallClients(cursorProject)
770
+ : selection;
845
771
  if (clients.length === 0) {
846
772
  throw new Error('No supported MCP clients detected. Re-run with --clients all or a comma list such as cursor,claude-desktop.');
847
773
  }
@@ -876,6 +802,19 @@ async function installMcpGatewayCommand(args, config) {
876
802
  console.log(`Runtime identity: ${detectedDeveloperName(args)}`);
877
803
  console.log('Restart/reload each client to use the protected tools.');
878
804
  console.log('Smoke test prompt: "Use lookup_customer to look up customer C-1001."');
805
+ if (args.upload === 'true') {
806
+ console.log('');
807
+ console.log('Uploading desktop inventory to AI Inventory…');
808
+ await (0, discover_1.runDiscoverUpload)({
809
+ apiKey: args.apiKey || config.apiKey,
810
+ apiUrl: args.apiUrl,
811
+ userEmail: args.developerName,
812
+ silent: false,
813
+ }, config);
814
+ }
815
+ else {
816
+ console.log('Run with --upload to refresh AI Inventory / Desktop MCP coverage in the app.');
817
+ }
879
818
  }
880
819
  async function installClaudeCodeMcpGatewayCommand(args, config) {
881
820
  const gatewayConfig = resolveGatewayConfig(args, config, {
@@ -940,7 +879,7 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
940
879
  agentNamePrefix: 'claude-desktop',
941
880
  userObjective: 'Developer requested this action from Claude Desktop.',
942
881
  });
943
- const targets = claudeDesktopInstallTargets(args.configPath);
882
+ const targets = (0, discoverPaths_1.claudeDesktopInstallTargets)(args.configPath);
944
883
  const nodeExe = process.execPath;
945
884
  const serverName = args.serverName || MANAGED_SERVER_NAME;
946
885
  const downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
package/dist/index.js CHANGED
@@ -208,7 +208,7 @@ function printHelp() {
208
208
  $ fullcourtdefense install-cursor-hook --project true # this repo only
209
209
  $ fullcourtdefense uninstall-cursor-hook
210
210
  $ fullcourtdefense install-mcp-gateway --clients auto --shield-id <id> --shield-key <key> --mcp-command npm --mcp-args "run mcp"
211
- $ fullcourtdefense install-mcp-gateway --clients all --mcp-command node --mcp-args ./server.js
211
+ $ fullcourtdefense install-mcp-gateway --clients all --shield-id <id> --shield-key <key> --mcp-command npm --mcp-args "run mcp" --upload --api-key <key>
212
212
  $ fullcourtdefense install-cursor-mcp-gateway --project true --shield-id <id> --shield-key <key> --mcp-command npm --mcp-args "run mcp"
213
213
  $ fullcourtdefense install-claude-code-mcp-gateway --scope local --shield-id <id> --shield-key <key> --mcp-command npm --mcp-args "run mcp"
214
214
  $ fullcourtdefense install-claude-code-mcp-gateway --scope project --shield-id <id> --mcp-command npm --mcp-args "run mcp"
@@ -406,6 +406,8 @@ async function main() {
406
406
  approvalPollMs: flags['approval-poll-ms'],
407
407
  scanResponse: flags['scan-response'],
408
408
  failClosed: flags['fail-closed'],
409
+ upload: flags.upload,
410
+ apiKey: flags['api-key'],
409
411
  };
410
412
  await (0, mcpGateway_1.installMcpGatewayCommand)(args, config);
411
413
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.2.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": {