fullcourtdefense-cli 1.7.7 → 1.7.9

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.
@@ -61,7 +61,10 @@ async function installAllCommand(args, config) {
61
61
  const discoverArgs = {
62
62
  surface: 'all',
63
63
  upload: 'true',
64
- apiKey: creds.apiKey,
64
+ // Do not forward the saved org API key by default. `discover` treats a
65
+ // present apiKey as an explicit opt-in to the CICD API-key path, while
66
+ // the zero-paste customer flow should upload via the enrolled Shield key.
67
+ apiKey: args.apiKey,
65
68
  apiUrl: creds.apiUrl,
66
69
  userEmail: args.developerName,
67
70
  };
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
43
43
  const yaml_1 = require("yaml");
44
44
  const output_1 = require("../output");
45
45
  const config_1 = require("../config");
46
+ const discoverPaths_1 = require("./discoverPaths");
46
47
  const DEFAULT_ATTACKS = [
47
48
  {
48
49
  id: 'local_prompt_injection_1',
@@ -954,6 +955,128 @@ function currentShieldConfig(args) {
954
955
  apiUrl: (args.apiUrl || args.configApiUrl || config.apiUrl || process.env.FULLCOURTDEFENSE_API_URL || DEFAULT_API_URL).replace(/\/$/, ''),
955
956
  };
956
957
  }
958
+ function parseTomlString(value) {
959
+ const trimmed = value.trim();
960
+ if (!trimmed)
961
+ return undefined;
962
+ try {
963
+ return JSON.parse(trimmed);
964
+ }
965
+ catch {
966
+ return trimmed.replace(/^"|"$/g, '').replace(/^'|'$/g, '');
967
+ }
968
+ }
969
+ function parseTomlStringArray(value) {
970
+ const trimmed = value.trim();
971
+ if (!trimmed)
972
+ return [];
973
+ try {
974
+ const parsed = JSON.parse(trimmed);
975
+ return Array.isArray(parsed) ? parsed.map(String) : [];
976
+ }
977
+ catch {
978
+ return trimmed.replace(/^\[|\]$/g, '').split(',').map(part => parseTomlString(part) || '').filter(Boolean);
979
+ }
980
+ }
981
+ function parseCodexMcpServers(raw) {
982
+ const servers = {};
983
+ let currentName = '';
984
+ for (const line of raw.split(/\r?\n/)) {
985
+ const section = line.match(/^\s*\[mcp_servers\.([^\]]+)\]\s*$/);
986
+ if (section) {
987
+ currentName = section[1].replace(/^"|"$/g, '');
988
+ servers[currentName] = servers[currentName] || {};
989
+ continue;
990
+ }
991
+ if (!currentName)
992
+ continue;
993
+ const trimmed = line.trim();
994
+ if (!trimmed || trimmed.startsWith('#'))
995
+ continue;
996
+ const eq = trimmed.indexOf('=');
997
+ if (eq < 0)
998
+ continue;
999
+ const key = trimmed.slice(0, eq).trim();
1000
+ const value = trimmed.slice(eq + 1).trim();
1001
+ if (key === 'command')
1002
+ servers[currentName].command = parseTomlString(value);
1003
+ else if (key === 'args')
1004
+ servers[currentName].args = parseTomlStringArray(value);
1005
+ else if (key === 'url' || key === 'serverUrl')
1006
+ servers[currentName][key] = parseTomlString(value);
1007
+ }
1008
+ return servers;
1009
+ }
1010
+ function extractMcpServerMap(parsed) {
1011
+ if (!parsed || typeof parsed !== 'object')
1012
+ return {};
1013
+ if (parsed.mcpServers && typeof parsed.mcpServers === 'object')
1014
+ return parsed.mcpServers;
1015
+ if (parsed.servers && typeof parsed.servers === 'object')
1016
+ return parsed.servers;
1017
+ if (parsed.mcp?.servers && typeof parsed.mcp.servers === 'object')
1018
+ return parsed.mcp.servers;
1019
+ if (parsed['mcp.servers'] && typeof parsed['mcp.servers'] === 'object')
1020
+ return parsed['mcp.servers'];
1021
+ return {};
1022
+ }
1023
+ function gatewayArgValue(args, flag) {
1024
+ const index = args.indexOf(flag);
1025
+ return index >= 0 ? args[index + 1] : undefined;
1026
+ }
1027
+ function localMcpServerOptions() {
1028
+ const out = [];
1029
+ const seen = new Set();
1030
+ for (const target of (0, discoverPaths_1.discoverScanTargets)(process.cwd())) {
1031
+ if (!fs.existsSync(target.path))
1032
+ continue;
1033
+ let raw = '';
1034
+ try {
1035
+ raw = fs.readFileSync(target.path, 'utf8');
1036
+ }
1037
+ catch {
1038
+ continue;
1039
+ }
1040
+ let parsed;
1041
+ try {
1042
+ parsed = JSON.parse(raw);
1043
+ }
1044
+ catch {
1045
+ parsed = /\.toml$/i.test(target.path) || /codex/i.test(target.source)
1046
+ ? { mcpServers: parseCodexMcpServers(raw) }
1047
+ : null;
1048
+ }
1049
+ const maps = [{ servers: extractMcpServerMap(parsed) }];
1050
+ if (/claude code/i.test(target.source) && parsed?.projects && typeof parsed.projects === 'object') {
1051
+ for (const [projectPath, projectConfig] of Object.entries(parsed.projects)) {
1052
+ maps.push({ label: path.basename(projectPath), servers: extractMcpServerMap(projectConfig) });
1053
+ }
1054
+ }
1055
+ for (const item of maps) {
1056
+ for (const [name, cfg] of Object.entries(item.servers)) {
1057
+ const command = typeof cfg?.command === 'string' ? cfg.command : '';
1058
+ const args = Array.isArray(cfg?.args) ? cfg.args.map(String) : [];
1059
+ if (!command)
1060
+ continue;
1061
+ const protectedServer = args.includes('mcp-gateway') || `${command} ${args.join(' ')}`.toLowerCase().includes('mcp-gateway');
1062
+ const id = `${target.path}::${item.label || ''}::${name}`;
1063
+ if (seen.has(id))
1064
+ continue;
1065
+ seen.add(id);
1066
+ out.push({
1067
+ id,
1068
+ name: item.label ? `${name} (${item.label})` : name,
1069
+ source: target.source,
1070
+ command,
1071
+ args,
1072
+ protected: protectedServer,
1073
+ agentName: protectedServer ? gatewayArgValue(args, '--agent-name') : undefined,
1074
+ });
1075
+ }
1076
+ }
1077
+ }
1078
+ return out.sort((a, b) => Number(b.protected) - Number(a.protected) || a.name.localeCompare(b.name));
1079
+ }
957
1080
  function compactScanHtml(args) {
958
1081
  const auth = currentShieldConfig(args);
959
1082
  const isConfigured = Boolean(auth.shieldId && auth.shieldKey);
@@ -1078,6 +1201,10 @@ function compactScanHtml(args) {
1078
1201
  </div>
1079
1202
  <div id="runner-fields">
1080
1203
  <div class="note">Run one MCP tool call from this machine. Use a protected proxy/gateway command or local MCP URL. Results stay local in this browser page.</div>
1204
+ <label>Saved/protected MCP server</label>
1205
+ <select id="runnerServer">
1206
+ <option value="">Manual connection...</option>
1207
+ </select>
1081
1208
  <div class="two">
1082
1209
  <div>
1083
1210
  <label>Protected MCP HTTP/SSE URL</label>
@@ -1088,6 +1215,16 @@ function compactScanHtml(args) {
1088
1215
  <input id="runnerTool" placeholder="lookup_customer" />
1089
1216
  </div>
1090
1217
  </div>
1218
+ <div class="two">
1219
+ <div>
1220
+ <label>Operation/action override</label>
1221
+ <input id="runnerOperation" placeholder="optional, e.g. read / write / delete / payment:write" />
1222
+ </div>
1223
+ <div>
1224
+ <label>Why use this?</label>
1225
+ <input value="Use when one tool supports multiple actions" readonly />
1226
+ </div>
1227
+ </div>
1091
1228
  <div class="two">
1092
1229
  <div><label>Or stdio command</label><input id="runnerMcpCommand" placeholder="fullcourtdefense" /></div>
1093
1230
  <div><label>Command args</label><input id="runnerMcpArgs" placeholder='mcp-gateway --mcp-command node --mcp-args "[&quot;./server.js&quot;]"' /></div>
@@ -1249,6 +1386,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1249
1386
  byId('runnerMcpCommand').value = qs.get('mcpCommand') || 'fullcourtdefense';
1250
1387
  byId('runnerMcpArgs').value = qs.get('mcpArgs') || '';
1251
1388
  byId('runnerTool').value = qs.get('mcpTool') || '';
1389
+ byId('runnerOperation').value = qs.get('operation') || '';
1252
1390
  const payload = () => ({
1253
1391
  target, mode: byId('mode').value, format: byId('format').value,
1254
1392
  endpoint: byId('endpoint').value, inputField: byId('inputField').value, outputField: byId('outputField').value,
@@ -1260,8 +1398,30 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1260
1398
  mcpCommand: byId('runnerMcpCommand').value,
1261
1399
  mcpArgs: byId('runnerMcpArgs').value,
1262
1400
  toolName: byId('runnerTool').value,
1401
+ operation: byId('runnerOperation').value,
1263
1402
  toolArgs: byId('runnerToolArgs').value
1264
1403
  });
1404
+ async function loadMcpServers() {
1405
+ try {
1406
+ const res = await fetch('/mcp/servers');
1407
+ const data = await res.json();
1408
+ const select = byId('runnerServer');
1409
+ const options = data.servers || [];
1410
+ select.innerHTML = '<option value="">Manual connection...</option>' + options.map((server, index) =>
1411
+ '<option value="' + index + '">' + htmlEscape((server.protected ? 'Protected: ' : 'Direct: ') + server.name + ' - ' + server.source + (server.agentName ? ' - ' + server.agentName : '')) + '</option>'
1412
+ ).join('');
1413
+ select.onchange = () => {
1414
+ const server = options[Number(select.value)];
1415
+ if (!server) return;
1416
+ byId('runnerMcpUrl').value = '';
1417
+ byId('runnerMcpCommand').value = server.command || '';
1418
+ byId('runnerMcpArgs').value = Array.isArray(server.args) ? server.args.map(part => /\\s/.test(part) ? JSON.stringify(part) : part).join(' ') : '';
1419
+ setConsole('Loaded saved MCP server: ' + server.name + '\\n' + (server.protected ? 'This entry is already routed through FullCourtDefense gateway.' : 'Direct MCP entry. Wrap it with protect-all for policy enforcement.'), 'Server loaded');
1420
+ };
1421
+ } catch {
1422
+ // Local server picker is convenience only; manual fields still work.
1423
+ }
1424
+ }
1265
1425
  const renderCommand = p => {
1266
1426
  const parts = ['fullcourtdefense scan --local --type', p.target, '--mode', p.mode, '--format', p.format, '--no-open'];
1267
1427
  if (p.target === 'endpoint') parts.push('--endpoint', JSON.stringify(p.endpoint), '--method POST --request-format custom --input-field', p.inputField || 'message', '--output-field', p.outputField || 'response');
@@ -1334,6 +1494,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1334
1494
  byId('runnerMcpCommand').value = 'fullcourtdefense';
1335
1495
  byId('runnerMcpArgs').value = '';
1336
1496
  byId('runnerTool').value = '';
1497
+ byId('runnerOperation').value = '';
1337
1498
  byId('runnerToolArgs').value = '{\\n "customerId": "cus_123"\\n}';
1338
1499
  byId('mcpResultCard').classList.add('hidden');
1339
1500
  byId('mcpResultJson').value = '';
@@ -1399,6 +1560,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1399
1560
  return data.ok;
1400
1561
  };
1401
1562
  byId('checkBackend').onclick = checkBackend;
1563
+ loadMcpServers();
1402
1564
  checkBackend().catch(() => {});
1403
1565
  function mcpRows(value) {
1404
1566
  if (Array.isArray(value)) return value;
@@ -1674,6 +1836,7 @@ async function runMcpToolFromBrowser(body) {
1674
1836
  const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
1675
1837
  const rawArgs = typeof body.mcpArgs === 'string' ? body.mcpArgs.trim() : '';
1676
1838
  const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
1839
+ const operation = typeof body.operation === 'string' && body.operation.trim() ? body.operation.trim() : undefined;
1677
1840
  if (!toolName)
1678
1841
  throw new Error('Tool name is required.');
1679
1842
  if (!rawUrl && !rawCommand)
@@ -1695,7 +1858,7 @@ async function runMcpToolFromBrowser(body) {
1695
1858
  const startedAt = Date.now();
1696
1859
  try {
1697
1860
  await client.initialize();
1698
- const result = await client.callTool(toolName, toolArgs);
1861
+ const result = await client.callTool(toolName, toolArgs, operation);
1699
1862
  const errorResult = parseMcpErrorResult(result);
1700
1863
  if (errorResult) {
1701
1864
  return {
@@ -1703,6 +1866,7 @@ async function runMcpToolFromBrowser(body) {
1703
1866
  blocked: errorResult.blocked,
1704
1867
  toolName,
1705
1868
  transport,
1869
+ operation,
1706
1870
  destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1707
1871
  elapsedMs: Date.now() - startedAt,
1708
1872
  reason: errorResult.reason,
@@ -1714,6 +1878,7 @@ async function runMcpToolFromBrowser(body) {
1714
1878
  success: true,
1715
1879
  toolName,
1716
1880
  transport,
1881
+ operation,
1717
1882
  destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1718
1883
  elapsedMs: Date.now() - startedAt,
1719
1884
  result,
@@ -1725,6 +1890,7 @@ async function runMcpToolFromBrowser(body) {
1725
1890
  blocked: /policy|blocked|approval|not allowed|denied/i.test(error instanceof Error ? error.message : String(error)),
1726
1891
  toolName,
1727
1892
  transport,
1893
+ operation,
1728
1894
  destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1729
1895
  elapsedMs: Date.now() - startedAt,
1730
1896
  reason: error instanceof Error ? error.message : String(error),
@@ -1754,6 +1920,11 @@ function startCompactWebServer(args) {
1754
1920
  }));
1755
1921
  return;
1756
1922
  }
1923
+ if (req.method === 'GET' && url.pathname === '/mcp/servers') {
1924
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
1925
+ res.end(JSON.stringify({ servers: localMcpServerOptions() }));
1926
+ return;
1927
+ }
1757
1928
  if (req.method === 'POST' && url.pathname === '/auth/save') {
1758
1929
  const body = await readRequestBody(req);
1759
1930
  const shieldId = typeof body.shieldId === 'string' ? body.shieldId.trim() : '';
@@ -2012,8 +2183,8 @@ class StdioMcpClient {
2012
2183
  });
2013
2184
  this.notify('notifications/initialized', {});
2014
2185
  }
2015
- async callTool(name, args) {
2016
- return this.request('tools/call', { name, arguments: args });
2186
+ async callTool(name, args, operation) {
2187
+ return this.request('tools/call', { name, arguments: args, ...(operation ? { operation } : {}) });
2017
2188
  }
2018
2189
  async listTools() {
2019
2190
  const result = await this.request('tools/list', {});
@@ -2150,8 +2321,8 @@ class HttpMcpClient {
2150
2321
  });
2151
2322
  await this.notify('notifications/initialized', {});
2152
2323
  }
2153
- async callTool(name, args) {
2154
- return this.request('tools/call', { name, arguments: args });
2324
+ async callTool(name, args, operation) {
2325
+ return this.request('tools/call', { name, arguments: args, ...(operation ? { operation } : {}) });
2155
2326
  }
2156
2327
  async listTools() {
2157
2328
  const result = await this.request('tools/list', {});
@@ -2244,8 +2415,8 @@ class SseMcpClient {
2244
2415
  });
2245
2416
  await this.notify('notifications/initialized', {});
2246
2417
  }
2247
- async callTool(name, args) {
2248
- return this.request('tools/call', { name, arguments: args });
2418
+ async callTool(name, args, operation) {
2419
+ return this.request('tools/call', { name, arguments: args, ...(operation ? { operation } : {}) });
2249
2420
  }
2250
2421
  async listTools() {
2251
2422
  const result = await this.request('tools/list', {});
@@ -588,12 +588,12 @@ class McpGatewayServer {
588
588
  throw new Error('Missing MCP tool name.');
589
589
  const toolArgs = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
590
590
  let approvalActionId;
591
- let operation;
591
+ let operation = typeof params.operation === 'string' && params.operation.trim() ? params.operation.trim() : undefined;
592
592
  try {
593
593
  const localBlock = (0, deterministicGuard_1.scanDeterministicToolCall)(toolName, toolArgs);
594
594
  if (localBlock)
595
595
  throw new Error(`${localBlock.reason} (${localBlock.ruleId}: ${localBlock.evidence})`);
596
- const preflight = await this.api.checkToolCall({ toolName, toolArgs });
596
+ const preflight = await this.api.checkToolCall({ toolName, operation, toolArgs });
597
597
  operation = preflight.operation;
598
598
  if (!preflight.allowed) {
599
599
  if (preflight.decision === 'approval' && this.gatewayConfig.approvalMode === 'wait') {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.7.7"
2
+ "version": "1.7.9"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.7.7",
3
+ "version": "1.7.9",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {