fullcourtdefense-cli 1.1.17 → 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.
- package/README.md +33 -0
- package/dist/commands/discover.d.ts +22 -0
- package/dist/commands/discover.js +283 -66
- package/dist/commands/discoverPaths.d.ts +18 -0
- package/dist/commands/discoverPaths.js +194 -0
- package/dist/commands/discoverProxy.d.ts +42 -0
- package/dist/commands/discoverProxy.js +256 -0
- package/dist/commands/discoverSchedule.d.ts +6 -0
- package/dist/commands/discoverSchedule.js +132 -0
- package/dist/commands/knownMcpServers.d.ts +8 -0
- package/dist/commands/knownMcpServers.js +68 -0
- package/dist/commands/mcpGateway.d.ts +2 -0
- package/dist/commands/mcpGateway.js +20 -81
- package/dist/index.js +13 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -53,8 +53,41 @@ fullcourtdefense init
|
|
|
53
53
|
- `fullcourtdefense install-claude-code-mcp-gateway ...` — registers the protected MCP gateway in Claude Code.
|
|
54
54
|
- `fullcourtdefense install-claude-desktop-mcp-gateway ...` — registers the protected MCP gateway in Claude Desktop.
|
|
55
55
|
- `fullcourtdefense credits` — checks hosted scan credits for CI/CD API-key scans.
|
|
56
|
+
- `fullcourtdefense discover` — scans local MCP client configs and reports risk; `--upload` sends desktop inventory to AI Inventory.
|
|
56
57
|
- `fullcourtdefense init` — creates a starter config file.
|
|
57
58
|
|
|
59
|
+
## Shadow AI / Desktop Discovery
|
|
60
|
+
|
|
61
|
+
Find MCP servers configured on developer machines without reading chat history or spawning stdio servers by default:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Scan this machine and print results
|
|
65
|
+
fullcourtdefense discover
|
|
66
|
+
|
|
67
|
+
# JSON output (includes host fingerprint + servers)
|
|
68
|
+
fullcourtdefense discover --json
|
|
69
|
+
|
|
70
|
+
# Upload to AI Inventory (requires CI/CD API key with audit:write)
|
|
71
|
+
fullcourtdefense discover --upload --api-key YOUR_KEY
|
|
72
|
+
|
|
73
|
+
# Live-probe HTTP/SSE MCP servers for tools/list (stdio stays config-only)
|
|
74
|
+
fullcourtdefense discover --deep
|
|
75
|
+
|
|
76
|
+
# Fleet / MDM script — silent upload with user attribution
|
|
77
|
+
fullcourtdefense discover --upload --silent --user-email you@company.com --api-key YOUR_KEY
|
|
78
|
+
|
|
79
|
+
# Daily automatic scan on this laptop (uses API key from env or ~/.fullcourtdefense.yml)
|
|
80
|
+
fullcourtdefense discover --schedule daily --user-email you@company.com
|
|
81
|
+
|
|
82
|
+
# Remove daily schedule
|
|
83
|
+
fullcourtdefense discover --unschedule true
|
|
84
|
+
|
|
85
|
+
# Scan an extra config path
|
|
86
|
+
fullcourtdefense discover --path /path/to/mcp.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each upload includes host metadata (`machineId`, `hostname`, `platform`, `user`) so the same MCP server on different laptops appears as separate desktop inventory rows. The CLI is also available as `fcd` or `botguard` after global install.
|
|
90
|
+
|
|
58
91
|
## Config File
|
|
59
92
|
|
|
60
93
|
Create a `.fullcourtdefense.yml` to avoid passing flags every time:
|
|
@@ -7,5 +7,27 @@ export interface DiscoverArgs {
|
|
|
7
7
|
upload?: string;
|
|
8
8
|
connectorName?: string;
|
|
9
9
|
extraPath?: string;
|
|
10
|
+
deep?: string;
|
|
11
|
+
silent?: string;
|
|
12
|
+
userEmail?: string;
|
|
13
|
+
schedule?: string;
|
|
14
|
+
unschedule?: string;
|
|
15
|
+
scheduleHour?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DesktopDiscoveryHost {
|
|
18
|
+
machineId: string;
|
|
19
|
+
hostname: string;
|
|
20
|
+
platform: string;
|
|
21
|
+
user: string;
|
|
22
|
+
scannedAt: string;
|
|
23
|
+
probeMode: 'config' | 'deep';
|
|
10
24
|
}
|
|
11
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,56 +34,35 @@ 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;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
37
39
|
const fs = __importStar(require("fs"));
|
|
38
40
|
const os = __importStar(require("os"));
|
|
39
|
-
const
|
|
41
|
+
const discoverProxy_1 = require("./discoverProxy");
|
|
42
|
+
const discoverSchedule_1 = require("./discoverSchedule");
|
|
43
|
+
const discoverPaths_1 = require("./discoverPaths");
|
|
44
|
+
const knownMcpServers_1 = require("./knownMcpServers");
|
|
40
45
|
const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
|
|
41
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,}/;
|
|
42
47
|
const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
|
|
43
|
-
|
|
48
|
+
function machineFingerprint() {
|
|
49
|
+
const seed = [os.hostname(), os.userInfo().username, os.platform(), os.arch()].join('|');
|
|
50
|
+
return crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
|
|
51
|
+
}
|
|
52
|
+
function buildHostMetadata(userEmail, probeMode = 'config') {
|
|
53
|
+
const info = os.userInfo();
|
|
54
|
+
return {
|
|
55
|
+
machineId: machineFingerprint(),
|
|
56
|
+
hostname: os.hostname(),
|
|
57
|
+
platform: os.platform(),
|
|
58
|
+
user: userEmail || info.username,
|
|
59
|
+
scannedAt: new Date().toISOString(),
|
|
60
|
+
probeMode,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Where MCP client configs live — see discoverPaths.ts for the canonical list. */
|
|
44
64
|
function candidateConfigPaths(cwd, extra) {
|
|
45
|
-
|
|
46
|
-
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
47
|
-
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
48
|
-
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
49
|
-
const list = [
|
|
50
|
-
// Cursor
|
|
51
|
-
{ path: path.join(home, '.cursor', 'mcp.json'), source: 'Cursor (global)' },
|
|
52
|
-
{ path: path.join(cwd, '.cursor', 'mcp.json'), source: 'Cursor (project)' },
|
|
53
|
-
// Claude Desktop
|
|
54
|
-
{ path: path.join(appData, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (win)' },
|
|
55
|
-
{ path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (mac)' },
|
|
56
|
-
{ path: path.join(xdg, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (linux)' },
|
|
57
|
-
// VS Code (project + user settings)
|
|
58
|
-
{ path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)' },
|
|
59
|
-
{ path: path.join(appData, 'Code', 'User', 'settings.json'), source: 'VS Code (user, win)' },
|
|
60
|
-
{ path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), source: 'VS Code (user, mac)' },
|
|
61
|
-
{ path: path.join(xdg, 'Code', 'User', 'settings.json'), source: 'VS Code (user, linux)' },
|
|
62
|
-
// Windsurf
|
|
63
|
-
{ path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), source: 'Windsurf' },
|
|
64
|
-
// Generic / repo-root
|
|
65
|
-
{ path: path.join(cwd, '.mcp.json'), source: 'Repo (.mcp.json)' },
|
|
66
|
-
{ path: path.join(cwd, 'mcp.json'), source: 'Repo (mcp.json)' },
|
|
67
|
-
];
|
|
68
|
-
if (process.platform === 'win32') {
|
|
69
|
-
const packagesDir = path.join(localAppData, 'Packages');
|
|
70
|
-
try {
|
|
71
|
-
for (const packageName of fs.readdirSync(packagesDir)) {
|
|
72
|
-
if (!/^Claude_/i.test(packageName))
|
|
73
|
-
continue;
|
|
74
|
-
list.push({
|
|
75
|
-
path: path.join(packagesDir, packageName, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json'),
|
|
76
|
-
source: 'Claude Desktop (win MSIX)',
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// Missing Packages directory is normal on non-Store installs.
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (extra)
|
|
85
|
-
list.push({ path: path.resolve(extra), source: 'Custom path' });
|
|
86
|
-
return list;
|
|
65
|
+
return (0, discoverPaths_1.discoverScanTargets)(cwd, extra);
|
|
87
66
|
}
|
|
88
67
|
/** MCP servers can be declared under several keys across clients. */
|
|
89
68
|
function extractServerMap(parsed) {
|
|
@@ -137,7 +116,7 @@ function parseConfigFile(filePath, source) {
|
|
|
137
116
|
parsed = JSON.parse(raw);
|
|
138
117
|
}
|
|
139
118
|
catch {
|
|
140
|
-
return [];
|
|
119
|
+
return [];
|
|
141
120
|
}
|
|
142
121
|
const servers = extractServerMap(parsed);
|
|
143
122
|
const out = [];
|
|
@@ -152,7 +131,15 @@ function parseConfigFile(filePath, source) {
|
|
|
152
131
|
? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
|
|
153
132
|
: command ? 'stdio' : 'unknown';
|
|
154
133
|
const tools = Array.isArray(cfg.tools)
|
|
155
|
-
? 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)
|
|
156
143
|
: [];
|
|
157
144
|
const { level, tags } = scoreRisk({ serverName: name, command, args, url });
|
|
158
145
|
const warnings = [];
|
|
@@ -178,10 +165,97 @@ function parseConfigFile(filePath, source) {
|
|
|
178
165
|
riskLevel: level,
|
|
179
166
|
riskTags: tags,
|
|
180
167
|
warnings,
|
|
168
|
+
probeMode: 'config',
|
|
169
|
+
proxyStatus: 'unknown',
|
|
181
170
|
});
|
|
182
171
|
}
|
|
183
172
|
return out;
|
|
184
173
|
}
|
|
174
|
+
async function mcpJsonRpc(endpoint, method, params, timeoutMs, sessionId) {
|
|
175
|
+
const headers = {
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
Accept: 'application/json, text/event-stream',
|
|
178
|
+
};
|
|
179
|
+
if (sessionId)
|
|
180
|
+
headers['Mcp-Session-Id'] = sessionId;
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
183
|
+
try {
|
|
184
|
+
const resp = await fetch(endpoint, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers,
|
|
187
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
|
|
188
|
+
signal: controller.signal,
|
|
189
|
+
});
|
|
190
|
+
const nextSession = resp.headers.get('mcp-session-id') || sessionId;
|
|
191
|
+
const ct = resp.headers.get('content-type') || '';
|
|
192
|
+
if (ct.includes('text/event-stream')) {
|
|
193
|
+
const text = await resp.text();
|
|
194
|
+
const dataLine = text.split('\n').find(line => line.startsWith('data: '));
|
|
195
|
+
return { json: dataLine ? JSON.parse(dataLine.slice(6)) : null, sessionId: nextSession || undefined };
|
|
196
|
+
}
|
|
197
|
+
return { json: await resp.json(), sessionId: nextSession || undefined };
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Best-effort live tools/list for HTTP/SSE MCP servers (stdio skipped — would spawn processes). */
|
|
204
|
+
async function probeHttpMcpTools(url, timeoutMs = 8000) {
|
|
205
|
+
const normalized = url.replace(/\/$/, '');
|
|
206
|
+
const candidates = [...new Set([
|
|
207
|
+
normalized,
|
|
208
|
+
normalized.replace(/\/sse(\?.*)?$/i, ''),
|
|
209
|
+
`${normalized}/mcp`,
|
|
210
|
+
normalized.replace(/\/sse(\?.*)?$/i, '/mcp'),
|
|
211
|
+
])];
|
|
212
|
+
for (const endpoint of candidates) {
|
|
213
|
+
try {
|
|
214
|
+
const init = await mcpJsonRpc(endpoint, 'initialize', {
|
|
215
|
+
protocolVersion: '2024-11-05',
|
|
216
|
+
capabilities: {},
|
|
217
|
+
clientInfo: { name: 'fullcourtdefense-discover', version: '1.0.0' },
|
|
218
|
+
}, timeoutMs);
|
|
219
|
+
const sid = init.sessionId;
|
|
220
|
+
if (init.json?.error)
|
|
221
|
+
continue;
|
|
222
|
+
await mcpJsonRpc(endpoint, 'notifications/initialized', {}, timeoutMs, sid);
|
|
223
|
+
const listed = await mcpJsonRpc(endpoint, 'tools/list', {}, timeoutMs, sid);
|
|
224
|
+
const tools = listed.json?.result?.tools;
|
|
225
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
226
|
+
return tools
|
|
227
|
+
.map((t) => (t && typeof t.name === 'string' ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined } : null))
|
|
228
|
+
.filter(Boolean);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// try next endpoint variant
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
async function enrichWithDeepProbe(servers, silent) {
|
|
238
|
+
for (const server of servers) {
|
|
239
|
+
if (server.transport !== 'http' && server.transport !== 'sse') {
|
|
240
|
+
if (!silent && server.transport === 'stdio') {
|
|
241
|
+
server.warnings.push('Deep probe skipped for stdio server (config-only inventory).');
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!server.url)
|
|
246
|
+
continue;
|
|
247
|
+
const liveTools = await probeHttpMcpTools(server.url);
|
|
248
|
+
if (liveTools.length === 0) {
|
|
249
|
+
server.warnings.push('Deep probe could not list tools (server may require auth or be offline).');
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const byName = new Map(server.tools.map(t => [t.name, t]));
|
|
253
|
+
for (const tool of liveTools)
|
|
254
|
+
byName.set(tool.name, tool);
|
|
255
|
+
server.tools = [...byName.values()];
|
|
256
|
+
server.probeMode = 'deep';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
185
259
|
/** Merge servers found in multiple files; keep richest record, union sources. */
|
|
186
260
|
function dedupe(servers) {
|
|
187
261
|
const byName = new Map();
|
|
@@ -195,8 +269,13 @@ function dedupe(servers) {
|
|
|
195
269
|
existing.source += `, ${s.source}`;
|
|
196
270
|
existing.command = existing.command || s.command;
|
|
197
271
|
existing.url = existing.url || s.url;
|
|
198
|
-
|
|
272
|
+
const toolMap = new Map(existing.tools.map(t => [t.name, t]));
|
|
273
|
+
for (const t of s.tools)
|
|
274
|
+
toolMap.set(t.name, t);
|
|
275
|
+
existing.tools = [...toolMap.values()];
|
|
199
276
|
existing.warnings = Array.from(new Set([...existing.warnings, ...s.warnings]));
|
|
277
|
+
if (s.probeMode === 'deep')
|
|
278
|
+
existing.probeMode = 'deep';
|
|
200
279
|
}
|
|
201
280
|
return [...byName.values()];
|
|
202
281
|
}
|
|
@@ -207,14 +286,59 @@ const COLOR = {
|
|
|
207
286
|
function riskColor(level) {
|
|
208
287
|
return level === 'critical' || level === 'high' ? COLOR.red : level === 'medium' ? COLOR.yellow : COLOR.green;
|
|
209
288
|
}
|
|
210
|
-
|
|
289
|
+
function applyProxyClassification(servers, scanned, cwd) {
|
|
290
|
+
const wrapsByConfig = (0, discoverProxy_1.buildGatewayWrapIndex)(servers);
|
|
291
|
+
for (const server of servers) {
|
|
292
|
+
server.proxyStatus = (0, discoverProxy_1.classifyProxyStatus)(server, wrapsByConfig);
|
|
293
|
+
if (server.proxyStatus === 'fcd_gateway') {
|
|
294
|
+
server.wrapsServerName = (0, discoverProxy_1.extractGatewayDownstream)(server)?.label;
|
|
295
|
+
}
|
|
296
|
+
if (server.proxyStatus === 'direct' && server.riskLevel !== 'low') {
|
|
297
|
+
server.warnings.push('Direct MCP — not proxied through Full Court Defense gateway.');
|
|
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
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return (0, discoverProxy_1.buildClientCoverage)(scanned, servers, cwd);
|
|
311
|
+
}
|
|
312
|
+
function proxyStatusLabel(status) {
|
|
313
|
+
if (status === 'fcd_gateway')
|
|
314
|
+
return 'FCD gateway';
|
|
315
|
+
if (status === 'proxied')
|
|
316
|
+
return 'Proxied';
|
|
317
|
+
if (status === 'direct')
|
|
318
|
+
return 'Direct — needs gateway';
|
|
319
|
+
return 'Unknown';
|
|
320
|
+
}
|
|
321
|
+
async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorName) {
|
|
211
322
|
const payload = {
|
|
212
|
-
connectorName: connectorName ||
|
|
323
|
+
connectorName: connectorName || `Desktop MCP · ${host.hostname}`,
|
|
324
|
+
host,
|
|
325
|
+
clientCoverage,
|
|
213
326
|
servers: servers.map(s => ({
|
|
214
327
|
serverName: s.serverName,
|
|
215
328
|
command: s.command,
|
|
216
329
|
url: s.url,
|
|
217
|
-
|
|
330
|
+
transport: s.transport,
|
|
331
|
+
clientSource: s.source,
|
|
332
|
+
configPath: s.configPath,
|
|
333
|
+
riskLevel: s.riskLevel,
|
|
334
|
+
riskTags: s.riskTags,
|
|
335
|
+
warnings: s.warnings,
|
|
336
|
+
probeMode: s.probeMode,
|
|
337
|
+
proxyStatus: s.proxyStatus,
|
|
338
|
+
wrapsServerName: s.wrapsServerName,
|
|
339
|
+
serverKind: s.serverKind,
|
|
340
|
+
serverDescription: s.serverDescription,
|
|
341
|
+
tools: s.tools,
|
|
218
342
|
})),
|
|
219
343
|
};
|
|
220
344
|
const resp = await fetch(`${apiUrl.replace(/\/$/, '')}/api/cicd/discovery/mcp`, {
|
|
@@ -227,12 +351,30 @@ async function upload(servers, apiUrl, apiKey, connectorName) {
|
|
|
227
351
|
throw new Error(data.error || `Upload failed (${resp.status})`);
|
|
228
352
|
}
|
|
229
353
|
const ingested = data.data?.ingested ?? servers.length;
|
|
230
|
-
console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) to your AI Inventory.${COLOR.reset}`);
|
|
354
|
+
console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) from ${host.hostname} to your AI Inventory.${COLOR.reset}`);
|
|
231
355
|
}
|
|
232
356
|
async function discoverCommand(args, config) {
|
|
233
357
|
const type = (args.type || 'mcp').toLowerCase();
|
|
358
|
+
const silent = args.silent === 'true';
|
|
359
|
+
const deep = args.deep === 'true';
|
|
360
|
+
if (args.unschedule === 'true') {
|
|
361
|
+
(0, discoverSchedule_1.uninstallDailyDiscoverSchedule)();
|
|
362
|
+
if (!silent)
|
|
363
|
+
console.log('Removed daily desktop discovery schedule.');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (args.schedule === 'daily') {
|
|
367
|
+
const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
|
|
368
|
+
(0, discoverSchedule_1.installDailyDiscoverSchedule)({ userEmail: args.userEmail, hour: Number.isFinite(hour) ? hour : 9 });
|
|
369
|
+
if (!silent) {
|
|
370
|
+
console.log(`Daily desktop discovery scheduled (runs discover --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
|
|
371
|
+
console.log('Requires FULLCOURTDEFENSE_API_KEY or apiKey in ~/.fullcourtdefense.yml');
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
234
375
|
if (type !== 'mcp') {
|
|
235
|
-
|
|
376
|
+
if (!silent)
|
|
377
|
+
console.error(`Unsupported discover type "${type}". Supported: mcp`);
|
|
236
378
|
process.exit(1);
|
|
237
379
|
}
|
|
238
380
|
const cwd = process.cwd();
|
|
@@ -240,29 +382,84 @@ async function discoverCommand(args, config) {
|
|
|
240
382
|
const scanned = [];
|
|
241
383
|
let found = [];
|
|
242
384
|
for (const c of candidates) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
385
|
+
scanned.push({ path: c.path, source: c.source });
|
|
386
|
+
if (fs.existsSync(c.path)) {
|
|
387
|
+
found.push(...parseConfigFile(c.path, c.source));
|
|
388
|
+
}
|
|
247
389
|
}
|
|
248
390
|
found = dedupe(found);
|
|
391
|
+
const clientCoverage = applyProxyClassification(found, scanned, cwd);
|
|
392
|
+
if (deep && found.length > 0) {
|
|
393
|
+
if (!silent)
|
|
394
|
+
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
|
|
395
|
+
await enrichWithDeepProbe(found, silent);
|
|
396
|
+
}
|
|
397
|
+
const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
|
|
249
398
|
if (args.json === 'true') {
|
|
250
|
-
console.log(JSON.stringify({
|
|
399
|
+
console.log(JSON.stringify({
|
|
400
|
+
host,
|
|
401
|
+
scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
|
|
402
|
+
clientCoverage,
|
|
403
|
+
servers: found,
|
|
404
|
+
}, null, 2));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (silent) {
|
|
408
|
+
if (args.upload === 'true') {
|
|
409
|
+
const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
|
|
410
|
+
const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
|
|
411
|
+
if (!apiKey)
|
|
412
|
+
process.exit(1);
|
|
413
|
+
if (scanned.length === 0)
|
|
414
|
+
return;
|
|
415
|
+
await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
|
|
416
|
+
}
|
|
251
417
|
return;
|
|
252
418
|
}
|
|
253
419
|
console.log(`\n${COLOR.bold}${COLOR.cyan}MCP server discovery${COLOR.reset}`);
|
|
420
|
+
console.log(`${COLOR.gray}Host ${host.hostname} · ${host.platform} · machine ${host.machineId}${COLOR.reset}`);
|
|
254
421
|
console.log(`${COLOR.gray}Scanned ${candidates.length} known config locations on this machine.${COLOR.reset}\n`);
|
|
255
422
|
if (scanned.length === 0) {
|
|
256
423
|
console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}`);
|
|
257
|
-
console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop, VS Code, Windsurf, and repo-root configs.${COLOR.reset}`);
|
|
424
|
+
console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
|
|
258
425
|
return;
|
|
259
426
|
}
|
|
260
|
-
console.log(`${COLOR.bold}
|
|
261
|
-
for (const s of scanned)
|
|
262
|
-
|
|
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}`}`);
|
|
431
|
+
}
|
|
263
432
|
console.log('');
|
|
433
|
+
if (clientCoverage.length > 0) {
|
|
434
|
+
console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
|
|
435
|
+
for (const row of clientCoverage) {
|
|
436
|
+
const gateway = row.mcpGatewayInstalled ? `${COLOR.green}gateway ✓${COLOR.reset}` : `${COLOR.red}no gateway${COLOR.reset}`;
|
|
437
|
+
const hooks = row.clientKey === 'cursor'
|
|
438
|
+
? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
|
|
439
|
+
: '';
|
|
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
|
+
}
|
|
444
|
+
}
|
|
445
|
+
console.log('');
|
|
446
|
+
}
|
|
264
447
|
if (found.length === 0) {
|
|
265
|
-
console.log(`${COLOR.yellow}
|
|
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
|
+
}
|
|
266
463
|
return;
|
|
267
464
|
}
|
|
268
465
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -271,12 +468,17 @@ async function discoverCommand(args, config) {
|
|
|
271
468
|
for (const s of found) {
|
|
272
469
|
const col = riskColor(s.riskLevel);
|
|
273
470
|
const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
|
|
274
|
-
|
|
471
|
+
const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
|
|
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}`);
|
|
275
475
|
console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
|
|
276
476
|
if (s.riskTags.length)
|
|
277
477
|
console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
|
|
278
|
-
if (s.tools.length)
|
|
279
|
-
|
|
478
|
+
if (s.tools.length) {
|
|
479
|
+
const names = s.tools.map(t => t.name);
|
|
480
|
+
console.log(` ${COLOR.gray}tools:${COLOR.reset} ${names.slice(0, 12).join(', ')}${names.length > 12 ? ' …' : ''}`);
|
|
481
|
+
}
|
|
280
482
|
console.log(` ${COLOR.gray}from:${COLOR.reset} ${s.source}`);
|
|
281
483
|
for (const w of s.warnings)
|
|
282
484
|
console.log(` ${COLOR.red}⚠ ${w}${COLOR.reset}`);
|
|
@@ -292,9 +494,24 @@ async function discoverCommand(args, config) {
|
|
|
292
494
|
process.exit(1);
|
|
293
495
|
}
|
|
294
496
|
console.log('');
|
|
295
|
-
await upload(found, apiUrl, apiKey, args.connectorName);
|
|
497
|
+
await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
|
|
296
498
|
}
|
|
297
499
|
else {
|
|
298
500
|
console.log(`${COLOR.gray}Run with --upload to push these into your AI Inventory.${COLOR.reset}`);
|
|
501
|
+
if (!deep)
|
|
502
|
+
console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers for tools/list.${COLOR.reset}`);
|
|
503
|
+
console.log(`${COLOR.gray}Add --schedule daily for automatic daily upload (MDM/fleet).${COLOR.reset}`);
|
|
299
504
|
}
|
|
300
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[];
|