fullcourtdefense-cli 1.14.13 → 1.14.14
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.
|
@@ -12,6 +12,10 @@ export interface DiscoverArgs {
|
|
|
12
12
|
agentCiGate?: AgentCiGateUpload;
|
|
13
13
|
extraPath?: string;
|
|
14
14
|
deep?: string;
|
|
15
|
+
/** Optional comma-separated dev roots (or "auto") to sweep for project MCP configs in OTHER repos. */
|
|
16
|
+
scanRoot?: string;
|
|
17
|
+
/** Allow deep stdio probing (process spawn) for servers found via --scan-root. Default: off. */
|
|
18
|
+
deepRoots?: string;
|
|
15
19
|
silent?: string;
|
|
16
20
|
userEmail?: string;
|
|
17
21
|
schedule?: string;
|
|
@@ -482,10 +482,18 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
|
|
|
482
482
|
}
|
|
483
483
|
return [];
|
|
484
484
|
}
|
|
485
|
-
async function enrichWithDeepProbe(servers, silent) {
|
|
485
|
+
async function enrichWithDeepProbe(servers, silent, options = {}) {
|
|
486
486
|
for (const server of servers) {
|
|
487
487
|
if (server.transport !== 'http' && server.transport !== 'sse') {
|
|
488
488
|
if (server.transport === 'stdio' && server.command) {
|
|
489
|
+
// Safety: never auto-spawn stdio server processes discovered in OTHER
|
|
490
|
+
// projects via --scan-root; inventory them without live tools unless
|
|
491
|
+
// the user opts in with --deep-roots. (HTTP probing below is fine —
|
|
492
|
+
// it's a network call, not a process spawn.)
|
|
493
|
+
if (options.skipStdioForConfigPaths?.has(path.resolve(server.configPath).toLowerCase())) {
|
|
494
|
+
server.warnings.push('Deep stdio probe skipped for config outside the current project (--scan-root). Pass --deep-roots true to probe these too.');
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
489
497
|
const downstream = (0, discoverProxy_1.isFcdGatewayServer)(server) ? (0, discoverProxy_1.extractGatewayDownstream)(server) : null;
|
|
490
498
|
const command = downstream?.command || server.command;
|
|
491
499
|
const args = downstream?.args || server.args || [];
|
|
@@ -769,8 +777,30 @@ async function discoverCommand(args, config) {
|
|
|
769
777
|
const scanned = [];
|
|
770
778
|
let found = [];
|
|
771
779
|
let clientCoverage = [];
|
|
780
|
+
const scanRootConfigPaths = new Set();
|
|
772
781
|
if (runMcp) {
|
|
773
782
|
const candidates = candidateConfigPaths(cwd, args.extraPath);
|
|
783
|
+
// Optional --scan-root sweep: find project-scoped MCP configs in OTHER repos
|
|
784
|
+
// under the given dev roots (bounded walk — see scanRootsForProjectConfigs).
|
|
785
|
+
// Without the flag, behavior is exactly the known-locations scan.
|
|
786
|
+
const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
|
|
787
|
+
if (scanRoots.length > 0) {
|
|
788
|
+
const sweep = (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots);
|
|
789
|
+
const known = new Set(candidates.map(c => path.resolve(c.path).toLowerCase()));
|
|
790
|
+
let added = 0;
|
|
791
|
+
for (const candidate of sweep.candidates) {
|
|
792
|
+
const key = path.resolve(candidate.path).toLowerCase();
|
|
793
|
+
if (known.has(key))
|
|
794
|
+
continue;
|
|
795
|
+
known.add(key);
|
|
796
|
+
candidates.push(candidate);
|
|
797
|
+
scanRootConfigPaths.add(key);
|
|
798
|
+
added += 1;
|
|
799
|
+
}
|
|
800
|
+
if (!silent) {
|
|
801
|
+
console.log(`${COLOR.gray}Scan-root sweep: checked ${sweep.scannedDirs} folder(s) under ${scanRoots.length} root(s) — found ${added} additional project config(s).${COLOR.reset}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
774
804
|
// Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
|
|
775
805
|
// generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
|
|
776
806
|
// every discovery run converges the fleet to the per-server identity contract.
|
|
@@ -793,7 +823,9 @@ async function discoverCommand(args, config) {
|
|
|
793
823
|
if (deep && found.length > 0) {
|
|
794
824
|
if (!silent)
|
|
795
825
|
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE/stdio tools/list)…${COLOR.reset}`);
|
|
796
|
-
await enrichWithDeepProbe(found, silent
|
|
826
|
+
await enrichWithDeepProbe(found, silent, {
|
|
827
|
+
skipStdioForConfigPaths: args.deepRoots === 'true' ? undefined : scanRootConfigPaths,
|
|
828
|
+
});
|
|
797
829
|
}
|
|
798
830
|
}
|
|
799
831
|
const secrets = surfaces.has('secrets') || surfaces.has('posture')
|
|
@@ -14,5 +14,22 @@ export declare function claudeDesktopLikelyInstalled(): boolean;
|
|
|
14
14
|
export declare function claudeDesktopInstallTargets(configPath?: string): ClaudeDesktopConfigCandidate[];
|
|
15
15
|
/** All MCP client config locations discover / install should check. */
|
|
16
16
|
export declare function candidateConfigPaths(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
17
|
+
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
18
|
+
export declare function defaultScanRoots(): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
21
|
+
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
22
|
+
* this is a repo-root sweep, not a disk scan. Hidden/dependency/build folders
|
|
23
|
+
* are pruned; only the known project config filenames are checked.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanRootsForProjectConfigs(roots: string[], options?: {
|
|
26
|
+
maxDepth?: number;
|
|
27
|
+
maxDirs?: number;
|
|
28
|
+
}): {
|
|
29
|
+
candidates: ConfigPathCandidate[];
|
|
30
|
+
scannedDirs: number;
|
|
31
|
+
};
|
|
32
|
+
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
33
|
+
export declare function resolveScanRoots(flag: string | undefined): string[];
|
|
17
34
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
18
35
|
export declare function discoverScanTargets(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
@@ -38,6 +38,9 @@ exports.claudeDesktopConfigCandidates = claudeDesktopConfigCandidates;
|
|
|
38
38
|
exports.claudeDesktopLikelyInstalled = claudeDesktopLikelyInstalled;
|
|
39
39
|
exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
|
|
40
40
|
exports.candidateConfigPaths = candidateConfigPaths;
|
|
41
|
+
exports.defaultScanRoots = defaultScanRoots;
|
|
42
|
+
exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
|
|
43
|
+
exports.resolveScanRoots = resolveScanRoots;
|
|
41
44
|
exports.discoverScanTargets = discoverScanTargets;
|
|
42
45
|
const fs = __importStar(require("fs"));
|
|
43
46
|
const os = __importStar(require("os"));
|
|
@@ -176,6 +179,122 @@ function candidateConfigPaths(cwd, extra) {
|
|
|
176
179
|
return true;
|
|
177
180
|
});
|
|
178
181
|
}
|
|
182
|
+
/** Project-scoped MCP config files that can sit at any repo root. */
|
|
183
|
+
const PROJECT_CONFIG_FILES = [
|
|
184
|
+
{ rel: path.join('.cursor', 'mcp.json'), source: 'Cursor (project)', clientKey: 'cursor' },
|
|
185
|
+
{ rel: '.mcp.json', source: 'Claude Code (.mcp.json project)', clientKey: 'claude_code' },
|
|
186
|
+
{ rel: path.join('.claude', 'settings.json'), source: 'Claude Code (project)', clientKey: 'claude_code' },
|
|
187
|
+
{ rel: path.join('.vscode', 'mcp.json'), source: 'VS Code (project)', clientKey: 'vscode' },
|
|
188
|
+
{ rel: path.join('.codex', 'config.toml'), source: 'Codex (project)', clientKey: 'codex' },
|
|
189
|
+
{ rel: path.join('.gemini', 'settings.json'), source: 'Gemini CLI (project)', clientKey: 'gemini_cli' },
|
|
190
|
+
{ rel: 'mcp.json', source: 'Repo (mcp.json)', clientKey: 'other' },
|
|
191
|
+
];
|
|
192
|
+
/** Dependency/build/VCS folders that never contain project MCP configs. */
|
|
193
|
+
const SCAN_SKIP_DIRS = new Set([
|
|
194
|
+
'node_modules', 'dist', 'build', 'out', 'vendor', 'venv', 'env', '__pycache__',
|
|
195
|
+
'target', 'coverage', 'tmp', 'temp', 'obj', 'bin', 'packages', 'bower_components',
|
|
196
|
+
'site-packages', 'deps', '.terraform',
|
|
197
|
+
]);
|
|
198
|
+
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
199
|
+
function defaultScanRoots() {
|
|
200
|
+
const home = os.homedir();
|
|
201
|
+
const roots = [
|
|
202
|
+
path.join(home, 'repos'),
|
|
203
|
+
path.join(home, 'projects'),
|
|
204
|
+
path.join(home, 'code'),
|
|
205
|
+
path.join(home, 'dev'),
|
|
206
|
+
path.join(home, 'src'),
|
|
207
|
+
path.join(home, 'work'),
|
|
208
|
+
path.join(home, 'Documents', 'GitHub'),
|
|
209
|
+
path.join(home, 'Documents', 'repos'),
|
|
210
|
+
path.join(home, 'Documents', 'projects'),
|
|
211
|
+
];
|
|
212
|
+
if (process.platform === 'win32') {
|
|
213
|
+
const drive = (process.env.SystemDrive || 'C:') + path.sep;
|
|
214
|
+
for (const name of ['dev', 'src', 'repos', 'code', 'projects', 'work', 'git']) {
|
|
215
|
+
roots.push(path.join(drive, name));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return roots.filter(root => {
|
|
219
|
+
try {
|
|
220
|
+
return fs.statSync(root).isDirectory();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
229
|
+
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
230
|
+
* this is a repo-root sweep, not a disk scan. Hidden/dependency/build folders
|
|
231
|
+
* are pruned; only the known project config filenames are checked.
|
|
232
|
+
*/
|
|
233
|
+
function scanRootsForProjectConfigs(roots, options = {}) {
|
|
234
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
235
|
+
const maxDirs = options.maxDirs ?? 25000;
|
|
236
|
+
const candidates = [];
|
|
237
|
+
const visited = new Set();
|
|
238
|
+
let scannedDirs = 0;
|
|
239
|
+
const queue = [];
|
|
240
|
+
for (const root of roots) {
|
|
241
|
+
try {
|
|
242
|
+
const resolved = path.resolve(root);
|
|
243
|
+
if (fs.statSync(resolved).isDirectory())
|
|
244
|
+
queue.push({ dir: resolved, depth: 0 });
|
|
245
|
+
}
|
|
246
|
+
catch { /* missing root — skip */ }
|
|
247
|
+
}
|
|
248
|
+
while (queue.length > 0 && scannedDirs < maxDirs) {
|
|
249
|
+
const { dir, depth } = queue.shift();
|
|
250
|
+
const key = dir.toLowerCase();
|
|
251
|
+
if (visited.has(key))
|
|
252
|
+
continue;
|
|
253
|
+
visited.add(key);
|
|
254
|
+
scannedDirs += 1;
|
|
255
|
+
for (const config of PROJECT_CONFIG_FILES) {
|
|
256
|
+
const file = path.join(dir, config.rel);
|
|
257
|
+
try {
|
|
258
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
259
|
+
candidates.push({ path: file, source: config.source, clientKey: config.clientKey });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch { /* unreadable — skip */ }
|
|
263
|
+
}
|
|
264
|
+
if (depth >= maxDepth)
|
|
265
|
+
continue;
|
|
266
|
+
let entries;
|
|
267
|
+
try {
|
|
268
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
continue; // permission denied etc.
|
|
272
|
+
}
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
if (!entry.isDirectory())
|
|
275
|
+
continue;
|
|
276
|
+
const name = entry.name;
|
|
277
|
+
if (name.startsWith('.') || SCAN_SKIP_DIRS.has(name.toLowerCase()))
|
|
278
|
+
continue;
|
|
279
|
+
queue.push({ dir: path.join(dir, name), depth: depth + 1 });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { candidates, scannedDirs };
|
|
283
|
+
}
|
|
284
|
+
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
285
|
+
function resolveScanRoots(flag) {
|
|
286
|
+
if (!flag?.trim())
|
|
287
|
+
return [];
|
|
288
|
+
const parts = flag.split(',').map(part => part.trim()).filter(Boolean);
|
|
289
|
+
const roots = [];
|
|
290
|
+
for (const part of parts) {
|
|
291
|
+
if (part.toLowerCase() === 'auto')
|
|
292
|
+
roots.push(...defaultScanRoots());
|
|
293
|
+
else
|
|
294
|
+
roots.push(path.resolve(part));
|
|
295
|
+
}
|
|
296
|
+
return [...new Set(roots.map(root => root))];
|
|
297
|
+
}
|
|
179
298
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
180
299
|
function discoverScanTargets(cwd, extra) {
|
|
181
300
|
const candidates = candidateConfigPaths(cwd, extra);
|
package/dist/index.js
CHANGED
|
@@ -327,6 +327,8 @@ function printHelp() {
|
|
|
327
327
|
$ fullcourtdefense discover --surface secrets
|
|
328
328
|
$ fullcourtdefense discover --surface agent-files
|
|
329
329
|
$ fullcourtdefense discover --deep --upload --silent --user-email you@company.com
|
|
330
|
+
$ fullcourtdefense discover --surface mcp --scan-root auto --upload
|
|
331
|
+
$ fullcourtdefense discover --surface mcp --scan-root "C:\dev,D:\work" --upload
|
|
330
332
|
$ fullcourtdefense discover --type mcp --json
|
|
331
333
|
$ fullcourtdefense install-cursor-hook
|
|
332
334
|
$ fullcourtdefense install-cursor-hook --shadow true # monitor only
|
|
@@ -526,6 +528,8 @@ async function main() {
|
|
|
526
528
|
connectorName: flags['connector-name'],
|
|
527
529
|
extraPath: flags.path,
|
|
528
530
|
deep: flags.deep,
|
|
531
|
+
scanRoot: flags['scan-root'],
|
|
532
|
+
deepRoots: flags['deep-roots'],
|
|
529
533
|
silent: flags.silent,
|
|
530
534
|
userEmail: flags['user-email'],
|
|
531
535
|
schedule: flags.schedule,
|
package/dist/version.json
CHANGED