fullcourtdefense-cli 1.14.8 → 1.14.10

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.
@@ -39,6 +39,7 @@ const crypto = __importStar(require("crypto"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const os = __importStar(require("os"));
41
41
  const path = __importStar(require("path"));
42
+ const child_process_1 = require("child_process");
42
43
  const config_1 = require("../config");
43
44
  const discoverProxy_1 = require("./discoverProxy");
44
45
  const discoverSchedule_1 = require("./discoverSchedule");
@@ -361,7 +362,94 @@ async function mcpJsonRpc(endpoint, method, params, timeoutMs, sessionId) {
361
362
  clearTimeout(timer);
362
363
  }
363
364
  }
364
- /** Best-effort live tools/list for HTTP/SSE MCP servers (stdio skipped — would spawn processes). */
365
+ function extractJsonRpcMessages(chunk) {
366
+ const messages = [];
367
+ let buffer = chunk;
368
+ while (true) {
369
+ const headerEnd = buffer.indexOf('\r\n\r\n');
370
+ if (headerEnd < 0)
371
+ break;
372
+ const header = buffer.slice(0, headerEnd);
373
+ const match = header.match(/Content-Length:\s*(\d+)/i);
374
+ if (!match)
375
+ break;
376
+ const length = Number(match[1]);
377
+ const bodyStart = headerEnd + 4;
378
+ const bodyEnd = bodyStart + length;
379
+ if (buffer.length < bodyEnd)
380
+ break;
381
+ try {
382
+ messages.push(JSON.parse(buffer.slice(bodyStart, bodyEnd)));
383
+ }
384
+ catch {
385
+ // Ignore malformed frames from noisy local servers.
386
+ }
387
+ buffer = buffer.slice(bodyEnd);
388
+ }
389
+ for (const line of chunk.split(/\r?\n/)) {
390
+ const trimmed = line.trim();
391
+ if (!trimmed.startsWith('{'))
392
+ continue;
393
+ try {
394
+ messages.push(JSON.parse(trimmed));
395
+ }
396
+ catch {
397
+ // Ignore logs or partial lines.
398
+ }
399
+ }
400
+ return messages;
401
+ }
402
+ function normalizeMcpToolList(tools) {
403
+ if (!Array.isArray(tools))
404
+ return [];
405
+ return tools
406
+ .map((t) => (t && typeof t.name === 'string'
407
+ ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined }
408
+ : null))
409
+ .filter(Boolean);
410
+ }
411
+ async function probeStdioMcpTools(command, args = [], timeoutMs = 8000) {
412
+ return new Promise(resolve => {
413
+ const child = (0, child_process_1.spawn)(command, args, {
414
+ cwd: process.cwd(),
415
+ env: process.env,
416
+ stdio: ['pipe', 'pipe', 'pipe'],
417
+ windowsHide: true,
418
+ });
419
+ let done = false;
420
+ const finish = (tools = []) => {
421
+ if (done)
422
+ return;
423
+ done = true;
424
+ clearTimeout(timer);
425
+ child.kill();
426
+ resolve(tools);
427
+ };
428
+ const timer = setTimeout(() => finish([]), timeoutMs);
429
+ child.on('error', () => finish([]));
430
+ child.on('exit', () => finish([]));
431
+ child.stdout.setEncoding('utf8');
432
+ child.stdout.on('data', chunk => {
433
+ for (const message of extractJsonRpcMessages(String(chunk))) {
434
+ if (message?.id !== 2)
435
+ continue;
436
+ const tools = normalizeMcpToolList(message?.result?.tools);
437
+ finish(tools);
438
+ }
439
+ });
440
+ const send = (id, method, params = {}) => {
441
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
442
+ };
443
+ send(1, 'initialize', {
444
+ protocolVersion: '2024-11-05',
445
+ capabilities: {},
446
+ clientInfo: { name: 'fullcourtdefense-discover', version: '1.0.0' },
447
+ });
448
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n');
449
+ send(2, 'tools/list');
450
+ });
451
+ }
452
+ /** Best-effort live tools/list for HTTP/SSE MCP servers. */
365
453
  async function probeHttpMcpTools(url, timeoutMs = 8000) {
366
454
  const normalized = url.replace(/\/$/, '');
367
455
  const candidates = [...new Set([
@@ -384,9 +472,7 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
384
472
  const listed = await mcpJsonRpc(endpoint, 'tools/list', {}, timeoutMs, sid);
385
473
  const tools = listed.json?.result?.tools;
386
474
  if (Array.isArray(tools) && tools.length > 0) {
387
- return tools
388
- .map((t) => (t && typeof t.name === 'string' ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined } : null))
389
- .filter(Boolean);
475
+ return normalizeMcpToolList(tools);
390
476
  }
391
477
  }
392
478
  catch {
@@ -398,8 +484,23 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
398
484
  async function enrichWithDeepProbe(servers, silent) {
399
485
  for (const server of servers) {
400
486
  if (server.transport !== 'http' && server.transport !== 'sse') {
401
- if (!silent && server.transport === 'stdio') {
402
- server.warnings.push('Deep probe skipped for stdio server (config-only inventory).');
487
+ if (server.transport === 'stdio' && server.command) {
488
+ const downstream = (0, discoverProxy_1.isFcdGatewayServer)(server) ? (0, discoverProxy_1.extractGatewayDownstream)(server) : null;
489
+ const command = downstream?.command || server.command;
490
+ const args = downstream?.args || server.args || [];
491
+ const liveTools = await probeStdioMcpTools(command, args);
492
+ if (liveTools.length > 0) {
493
+ const byName = new Map(server.tools.map(t => [t.name, t]));
494
+ for (const tool of liveTools)
495
+ byName.set(tool.name, tool);
496
+ server.tools = [...byName.values()];
497
+ server.probeMode = 'deep';
498
+ continue;
499
+ }
500
+ server.warnings.push('Deep probe could not list tools from stdio server (server may require auth or failed to start).');
501
+ }
502
+ else if (!silent && server.transport === 'stdio') {
503
+ server.warnings.push('Deep probe skipped for stdio server without a command.');
403
504
  }
404
505
  continue;
405
506
  }
@@ -638,7 +739,8 @@ function printPostureReport(posture, silent) {
638
739
  async function discoverCommand(args, config) {
639
740
  const surfaces = parseSurfaces(args);
640
741
  const silent = args.silent === 'true';
641
- const deep = args.deep === 'true';
742
+ const uploadRequested = args.upload === 'true';
743
+ const deep = args.deep === 'false' ? false : (args.deep === 'true' || uploadRequested);
642
744
  if (args.unschedule === 'true') {
643
745
  (0, discoverSchedule_1.uninstallDailyDiscoverSchedule)();
644
746
  if (!silent)
@@ -656,7 +758,7 @@ async function discoverCommand(args, config) {
656
758
  });
657
759
  if (!silent) {
658
760
  const when = args.schedule === 'logon' ? 'at logon' : `daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00`;
659
- console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
761
+ console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --deep --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
660
762
  console.log('Uses login Shield credentials or an explicit org API key.');
661
763
  }
662
764
  return;
@@ -678,7 +780,7 @@ async function discoverCommand(args, config) {
678
780
  clientCoverage = applyProxyClassification(found, scanned, cwd);
679
781
  if (deep && found.length > 0) {
680
782
  if (!silent)
681
- console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
783
+ console.log(`${COLOR.gray}Running deep probe (HTTP/SSE/stdio tools/list)…${COLOR.reset}`);
682
784
  await enrichWithDeepProbe(found, silent);
683
785
  }
684
786
  }
@@ -736,7 +838,7 @@ async function discoverCommand(args, config) {
736
838
  },
737
839
  };
738
840
  async function maybeUpload() {
739
- if (args.upload !== 'true')
841
+ if (!uploadRequested)
740
842
  return;
741
843
  const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
742
844
  // Either an org API key OR an enrolled Shield key (from `login`) is enough.
@@ -750,7 +852,7 @@ async function discoverCommand(args, config) {
750
852
  await upload(found, host, clientCoverage, creds.apiUrl, { apiKey: creds.apiKey, shieldId: creds.shieldId, shieldKey: creds.shieldKey, preferApiKey: !!args.apiKey }, args.connectorName, uploadExtras);
751
853
  }
752
854
  if (silent) {
753
- if (args.upload === 'true') {
855
+ if (uploadRequested) {
754
856
  const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
755
857
  if (!creds.apiKey && !(creds.shieldId && creds.shieldKey))
756
858
  process.exit(1);
@@ -802,14 +904,14 @@ async function discoverCommand(args, config) {
802
904
  printAgentFilesReport(agentFiles, silent);
803
905
  if (posture)
804
906
  printPostureReport(posture, silent);
805
- if (args.upload === 'true') {
907
+ if (uploadRequested) {
806
908
  console.log('');
807
909
  await maybeUpload();
808
910
  }
809
911
  else {
810
912
  console.log(`${COLOR.gray}Run with --upload to push results into AI Fleet.${COLOR.reset}`);
811
913
  if (runMcp && !deep)
812
- console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE MCP servers.${COLOR.reset}`);
914
+ console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE/stdio MCP servers.${COLOR.reset}`);
813
915
  if (!surfaces.has('secrets'))
814
916
  console.log(`${COLOR.gray}Add --surface secrets for .env / credential store scan (local-only).${COLOR.reset}`);
815
917
  if (surfaces.size < 4)
@@ -49,7 +49,7 @@ function discoverCommandLine(userEmail, surface) {
49
49
  const node = process.execPath;
50
50
  const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
51
51
  const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
52
- const parts = [q(node), q(script), 'discover', '--upload', '--silent'];
52
+ const parts = [q(node), q(script), 'discover', '--upload', '--deep', '--silent'];
53
53
  if (surface)
54
54
  parts.push('--surface', q(surface));
55
55
  if (userEmail)
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.8"
2
+ "version": "1.14.10"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.8",
3
+ "version": "1.14.10",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "test:secret-posture": "npm run build && node scripts/test-secret-posture-fixtures.js",
24
24
  "test:agent-file-posture": "npm run build && node scripts/test-agent-file-posture.js",
25
25
  "test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
26
+ "test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
26
27
  "test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
27
28
  "test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
28
29
  "prepublishOnly": "npm run build"