fullcourtdefense-cli 1.2.0 → 1.3.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.
@@ -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) {
@@ -353,6 +296,16 @@ function applyProxyClassification(servers, scanned, cwd) {
353
296
  if (server.proxyStatus === 'direct' && server.riskLevel !== 'low') {
354
297
  server.warnings.push('Direct MCP — not proxied through Full Court Defense gateway.');
355
298
  }
299
+ const info = (0, knownMcpServers_1.knownMcpServerInfo)(server.serverName, server.command);
300
+ if (info) {
301
+ server.serverKind = info.category;
302
+ server.serverDescription = info.description;
303
+ }
304
+ if (server.proxyStatus === 'direct') {
305
+ const hint = (0, knownMcpServers_1.directServerFixHint)(server.serverName);
306
+ if (hint && !server.warnings.includes(hint))
307
+ server.warnings.push(hint);
308
+ }
356
309
  }
357
310
  return (0, discoverProxy_1.buildClientCoverage)(scanned, servers, cwd);
358
311
  }
@@ -383,6 +336,8 @@ async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorNa
383
336
  probeMode: s.probeMode,
384
337
  proxyStatus: s.proxyStatus,
385
338
  wrapsServerName: s.wrapsServerName,
339
+ serverKind: s.serverKind,
340
+ serverDescription: s.serverDescription,
386
341
  tools: s.tools,
387
342
  })),
388
343
  };
@@ -427,10 +382,10 @@ async function discoverCommand(args, config) {
427
382
  const scanned = [];
428
383
  let found = [];
429
384
  for (const c of candidates) {
430
- if (!fs.existsSync(c.path))
431
- continue;
432
385
  scanned.push({ path: c.path, source: c.source });
433
- found.push(...parseConfigFile(c.path, c.source));
386
+ if (fs.existsSync(c.path)) {
387
+ found.push(...parseConfigFile(c.path, c.source));
388
+ }
434
389
  }
435
390
  found = dedupe(found);
436
391
  const clientCoverage = applyProxyClassification(found, scanned, cwd);
@@ -455,7 +410,7 @@ async function discoverCommand(args, config) {
455
410
  const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
456
411
  if (!apiKey)
457
412
  process.exit(1);
458
- if (found.length === 0)
413
+ if (scanned.length === 0)
459
414
  return;
460
415
  await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
461
416
  }
@@ -469,16 +424,12 @@ async function discoverCommand(args, config) {
469
424
  console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
470
425
  return;
471
426
  }
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;
427
+ console.log(`${COLOR.bold}AI clients checked (${scanned.length}):${COLOR.reset}`);
428
+ for (const s of scanned) {
429
+ const exists = fs.existsSync(s.path);
430
+ console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} → ${s.path}${exists ? '' : ` ${COLOR.yellow}(not created yet)${COLOR.reset}`}`);
479
431
  }
480
- const order = { critical: 0, high: 1, medium: 2, low: 3 };
481
- found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
432
+ console.log('');
482
433
  if (clientCoverage.length > 0) {
483
434
  console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
484
435
  for (const row of clientCoverage) {
@@ -487,15 +438,40 @@ async function discoverCommand(args, config) {
487
438
  ? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
488
439
  : '';
489
440
  console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} — ${row.mcpServerCount} MCP · ${gateway}${hooks ? ` · ${hooks}` : ''}`);
441
+ if (row.clientKey === 'claude_desktop' && !row.mcpGatewayInstalled && row.mcpServerCount === 0) {
442
+ console.log(` ${COLOR.gray}Install: fullcourtdefense install-claude-desktop-mcp-gateway --shield-id ... --mcp-command npm --mcp-args "run mcp"${COLOR.reset}`);
443
+ }
490
444
  }
491
445
  console.log('');
492
446
  }
447
+ if (found.length === 0) {
448
+ console.log(`${COLOR.yellow}No MCP servers configured in these clients yet.${COLOR.reset}`);
449
+ 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}`);
450
+ if (args.upload === 'true') {
451
+ const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
452
+ const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
453
+ if (!apiKey) {
454
+ console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset}`);
455
+ process.exit(1);
456
+ }
457
+ console.log('');
458
+ await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
459
+ }
460
+ else {
461
+ console.log(`${COLOR.gray}Run with --upload to push client coverage into AI Inventory.${COLOR.reset}`);
462
+ }
463
+ return;
464
+ }
465
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
466
+ found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
493
467
  console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
494
468
  for (const s of found) {
495
469
  const col = riskColor(s.riskLevel);
496
470
  const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
497
471
  const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
498
472
  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}`);
473
+ if (s.serverDescription)
474
+ console.log(` ${COLOR.gray}about:${COLOR.reset} ${s.serverDescription}`);
499
475
  console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
500
476
  if (s.riskTags.length)
501
477
  console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
@@ -527,3 +503,15 @@ async function discoverCommand(args, config) {
527
503
  console.log(`${COLOR.gray}Add --schedule daily for automatic daily upload (MDM/fleet).${COLOR.reset}`);
528
504
  }
529
505
  }
506
+ /** Upload-only discover — used after install-mcp-gateway --upload. */
507
+ async function runDiscoverUpload(args, config) {
508
+ await discoverCommand({
509
+ type: 'mcp',
510
+ upload: 'true',
511
+ silent: args.silent ? 'true' : undefined,
512
+ apiKey: args.apiKey,
513
+ apiUrl: args.apiUrl,
514
+ userEmail: args.userEmail,
515
+ connectorName: args.connectorName,
516
+ }, config);
517
+ }
@@ -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.0",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {