fullcourtdefense-cli 1.7.6 → 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);
@@ -1011,6 +1134,13 @@ function compactScanHtml(args) {
1011
1134
  .resultMetric span { font-size: 16px; font-weight: 800; color: #0f172a; }
1012
1135
  .resultMeta { margin-top: 10px; color: #334155; font-size: 12px; line-height: 1.5; }
1013
1136
  .resultMeta a { color: #1d4ed8; }
1137
+ .mcpBanner { margin-top: 10px; border-radius: 14px; padding: 12px; font-size: 13px; line-height: 1.45; }
1138
+ .mcpBanner.ok { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
1139
+ .mcpBanner.blocked { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
1140
+ .mcpCards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 10px; }
1141
+ .mcpCard { border: 1px solid #e2e8f0; background: white; border-radius: 12px; padding: 10px; }
1142
+ .mcpCard b { display: block; color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }
1143
+ .mcpCard span { color: #0f172a; font-size: 13px; font-weight: 750; overflow-wrap: anywhere; }
1014
1144
  .localTable { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; overflow: hidden; border-radius: 12px; }
1015
1145
  .localTable th, .localTable td { border-bottom: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }
1016
1146
  .localTable th { color: #475569; background: #f8fafc; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
@@ -1071,6 +1201,10 @@ function compactScanHtml(args) {
1071
1201
  </div>
1072
1202
  <div id="runner-fields">
1073
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>
1074
1208
  <div class="two">
1075
1209
  <div>
1076
1210
  <label>Protected MCP HTTP/SSE URL</label>
@@ -1081,6 +1215,16 @@ function compactScanHtml(args) {
1081
1215
  <input id="runnerTool" placeholder="lookup_customer" />
1082
1216
  </div>
1083
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>
1084
1228
  <div class="two">
1085
1229
  <div><label>Or stdio command</label><input id="runnerMcpCommand" placeholder="fullcourtdefense" /></div>
1086
1230
  <div><label>Command args</label><input id="runnerMcpArgs" placeholder='mcp-gateway --mcp-command node --mcp-args "[&quot;./server.js&quot;]"' /></div>
@@ -1242,6 +1386,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1242
1386
  byId('runnerMcpCommand').value = qs.get('mcpCommand') || 'fullcourtdefense';
1243
1387
  byId('runnerMcpArgs').value = qs.get('mcpArgs') || '';
1244
1388
  byId('runnerTool').value = qs.get('mcpTool') || '';
1389
+ byId('runnerOperation').value = qs.get('operation') || '';
1245
1390
  const payload = () => ({
1246
1391
  target, mode: byId('mode').value, format: byId('format').value,
1247
1392
  endpoint: byId('endpoint').value, inputField: byId('inputField').value, outputField: byId('outputField').value,
@@ -1253,8 +1398,30 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1253
1398
  mcpCommand: byId('runnerMcpCommand').value,
1254
1399
  mcpArgs: byId('runnerMcpArgs').value,
1255
1400
  toolName: byId('runnerTool').value,
1401
+ operation: byId('runnerOperation').value,
1256
1402
  toolArgs: byId('runnerToolArgs').value
1257
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
+ }
1258
1425
  const renderCommand = p => {
1259
1426
  const parts = ['fullcourtdefense scan --local --type', p.target, '--mode', p.mode, '--format', p.format, '--no-open'];
1260
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');
@@ -1327,6 +1494,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1327
1494
  byId('runnerMcpCommand').value = 'fullcourtdefense';
1328
1495
  byId('runnerMcpArgs').value = '';
1329
1496
  byId('runnerTool').value = '';
1497
+ byId('runnerOperation').value = '';
1330
1498
  byId('runnerToolArgs').value = '{\\n "customerId": "cus_123"\\n}';
1331
1499
  byId('mcpResultCard').classList.add('hidden');
1332
1500
  byId('mcpResultJson').value = '';
@@ -1392,6 +1560,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1392
1560
  return data.ok;
1393
1561
  };
1394
1562
  byId('checkBackend').onclick = checkBackend;
1563
+ loadMcpServers();
1395
1564
  checkBackend().catch(() => {});
1396
1565
  function mcpRows(value) {
1397
1566
  if (Array.isArray(value)) return value;
@@ -1411,19 +1580,68 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1411
1580
  if (value && Array.isArray(value.results)) return value.results;
1412
1581
  return null;
1413
1582
  }
1583
+ function mcpText(value) {
1584
+ if (value == null) return '';
1585
+ if (typeof value === 'string') return value;
1586
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
1587
+ const content = value.content;
1588
+ if (Array.isArray(content)) {
1589
+ return content
1590
+ .map(item => item && typeof item.text === 'string' ? item.text : '')
1591
+ .filter(Boolean)
1592
+ .join('\\n');
1593
+ }
1594
+ return '';
1595
+ }
1596
+ function mcpObject(value) {
1597
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
1598
+ const contentText = mcpText(value);
1599
+ if (contentText) {
1600
+ try {
1601
+ const parsed = JSON.parse(contentText);
1602
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
1603
+ } catch {}
1604
+ }
1605
+ if (value.data && typeof value.data === 'object' && !Array.isArray(value.data)) return value.data;
1606
+ if (value.result && typeof value.result === 'object' && !Array.isArray(value.result)) return value.result;
1607
+ return value;
1608
+ }
1609
+ function cellValue(value) {
1610
+ if (value == null) return '';
1611
+ if (typeof value === 'object') return JSON.stringify(value);
1612
+ return String(value);
1613
+ }
1614
+ function renderObjectCards(obj) {
1615
+ const preferred = ['id', 'customerId', 'name', 'title', 'status', 'plan', 'email', 'decision', 'matchedPolicy', 'reason', 'ok'];
1616
+ const keys = Array.from(new Set([...preferred.filter(k => Object.prototype.hasOwnProperty.call(obj, k)), ...Object.keys(obj)])).slice(0, 12);
1617
+ return '<div class="mcpCards">' + keys.map(k => '<div class="mcpCard"><b>' + htmlEscape(k) + '</b><span>' + htmlEscape(cellValue(obj[k])) + '</span></div>').join('') + '</div>';
1618
+ }
1619
+ function renderMcpTable(rows) {
1620
+ const preferred = ['id', 'customerId', 'name', 'title', 'status', 'plan', 'email', 'createdAt', 'updatedAt'];
1621
+ const keys = Array.from(new Set([...preferred.filter(k => rows.some(row => row && Object.prototype.hasOwnProperty.call(row, k))), ...rows.flatMap(row => Object.keys(row || {}))])).slice(0, 10);
1622
+ return '<table class="localTable"><thead><tr>' + keys.map(k => '<th>' + htmlEscape(k) + '</th>').join('') + '</tr></thead><tbody>' +
1623
+ rows.slice(0, 50).map(row => '<tr>' + keys.map(k => '<td>' + htmlEscape(cellValue(row[k])) + '</td>').join('') + '</tr>').join('') +
1624
+ '</tbody></table>' +
1625
+ (rows.length > 50 ? '<div class="resultMeta">Showing first 50 rows. Full result is in raw JSON below.</div>' : '');
1626
+ }
1414
1627
  function renderMcpResult(data) {
1415
1628
  byId('mcpResultCard').classList.remove('hidden');
1416
- byId('mcpResultStatus').textContent = data.success ? 'ALLOW / RESULT' : 'ERROR / BLOCK';
1629
+ byId('mcpResultStatus').textContent = data.blocked ? 'BLOCKED' : data.success ? 'ALLOW / RESULT' : 'ERROR';
1417
1630
  byId('mcpResultStatus').className = data.success ? 'statusTag pass' : 'statusTag fail';
1418
1631
  byId('mcpResultJson').value = JSON.stringify(data, null, 2);
1419
1632
  const rows = data.success ? mcpRows(data.result) : null;
1420
1633
  if (rows && rows.length && rows.every(row => row && typeof row === 'object' && !Array.isArray(row))) {
1421
- const keys = Array.from(new Set(rows.flatMap(row => Object.keys(row)))).slice(0, 8);
1422
- byId('mcpResultTable').innerHTML = '<table class="localTable"><thead><tr>' + keys.map(k => '<th>' + htmlEscape(k) + '</th>').join('') + '</tr></thead><tbody>' +
1423
- rows.slice(0, 25).map(row => '<tr>' + keys.map(k => '<td>' + htmlEscape(typeof row[k] === 'object' ? JSON.stringify(row[k]) : String(row[k] ?? '')) + '</td>').join('') + '</tr>').join('') +
1424
- '</tbody></table>';
1634
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner ok"><b>Allowed.</b> Tool returned ' + rows.length + ' row(s).</div>' + renderMcpTable(rows);
1635
+ } else if (data.success) {
1636
+ const obj = mcpObject(data.result);
1637
+ const text = mcpText(data.result);
1638
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner ok"><b>Allowed.</b> Tool call completed through the configured MCP server/proxy.</div>' +
1639
+ (obj ? renderObjectCards(obj) : '<div class="resultMeta">' + htmlEscape(text || 'Result is shown as raw JSON below.') + '</div>');
1425
1640
  } else {
1426
- byId('mcpResultTable').innerHTML = '<div class="resultMeta">' + (data.success ? 'Result is shown as copyable JSON below.' : htmlEscape(data.error || 'MCP call failed.')) + '</div>';
1641
+ const blockedCopy = data.blocked
1642
+ ? 'Blocked because a FullCourtDefense policy or local safety rule is defined for this tool/action.'
1643
+ : 'The MCP call failed before a successful tool result was returned.';
1644
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner blocked"><b>' + (data.blocked ? 'Blocked by policy.' : 'MCP error.') + '</b> ' + htmlEscape(blockedCopy) + '<br/><span>' + htmlEscape(data.reason || data.error || 'No reason returned.') + '</span></div>';
1427
1645
  }
1428
1646
  }
1429
1647
  byId('copyMcpResult').onclick = async () => {
@@ -1585,11 +1803,40 @@ function createMcpClient(options) {
1585
1803
  return new HttpMcpClient(options);
1586
1804
  return new StdioMcpClient(options.command || '', options.args || []);
1587
1805
  }
1806
+ function parseMcpErrorResult(result) {
1807
+ if (!result || typeof result !== 'object' || !result.isError)
1808
+ return null;
1809
+ const content = result.content;
1810
+ const text = Array.isArray(content)
1811
+ ? content.map(item => item && typeof item === 'object' && typeof item.text === 'string' ? item.text : '').filter(Boolean).join('\n')
1812
+ : '';
1813
+ if (text) {
1814
+ try {
1815
+ const parsed = JSON.parse(text);
1816
+ if (parsed && typeof parsed === 'object') {
1817
+ return {
1818
+ blocked: parsed.blocked === true || /policy|blocked|approval/i.test(text),
1819
+ reason: typeof parsed.reason === 'string' ? parsed.reason : text,
1820
+ details: parsed,
1821
+ };
1822
+ }
1823
+ }
1824
+ catch {
1825
+ // plain-text MCP error
1826
+ }
1827
+ }
1828
+ return {
1829
+ blocked: /policy|blocked|approval|not allowed|denied/i.test(text),
1830
+ reason: text || 'The MCP server returned an error result.',
1831
+ details: result,
1832
+ };
1833
+ }
1588
1834
  async function runMcpToolFromBrowser(body) {
1589
1835
  const rawUrl = typeof body.mcpUrl === 'string' ? body.mcpUrl.trim() : '';
1590
1836
  const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
1591
1837
  const rawArgs = typeof body.mcpArgs === 'string' ? body.mcpArgs.trim() : '';
1592
1838
  const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
1839
+ const operation = typeof body.operation === 'string' && body.operation.trim() ? body.operation.trim() : undefined;
1593
1840
  if (!toolName)
1594
1841
  throw new Error('Tool name is required.');
1595
1842
  if (!rawUrl && !rawCommand)
@@ -1611,11 +1858,27 @@ async function runMcpToolFromBrowser(body) {
1611
1858
  const startedAt = Date.now();
1612
1859
  try {
1613
1860
  await client.initialize();
1614
- const result = await client.callTool(toolName, toolArgs);
1861
+ const result = await client.callTool(toolName, toolArgs, operation);
1862
+ const errorResult = parseMcpErrorResult(result);
1863
+ if (errorResult) {
1864
+ return {
1865
+ success: false,
1866
+ blocked: errorResult.blocked,
1867
+ toolName,
1868
+ transport,
1869
+ operation,
1870
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1871
+ elapsedMs: Date.now() - startedAt,
1872
+ reason: errorResult.reason,
1873
+ result,
1874
+ details: errorResult.details,
1875
+ };
1876
+ }
1615
1877
  return {
1616
1878
  success: true,
1617
1879
  toolName,
1618
1880
  transport,
1881
+ operation,
1619
1882
  destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1620
1883
  elapsedMs: Date.now() - startedAt,
1621
1884
  result,
@@ -1624,10 +1887,13 @@ async function runMcpToolFromBrowser(body) {
1624
1887
  catch (error) {
1625
1888
  return {
1626
1889
  success: false,
1890
+ blocked: /policy|blocked|approval|not allowed|denied/i.test(error instanceof Error ? error.message : String(error)),
1627
1891
  toolName,
1628
1892
  transport,
1893
+ operation,
1629
1894
  destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1630
1895
  elapsedMs: Date.now() - startedAt,
1896
+ reason: error instanceof Error ? error.message : String(error),
1631
1897
  error: error instanceof Error ? error.message : String(error),
1632
1898
  };
1633
1899
  }
@@ -1654,6 +1920,11 @@ function startCompactWebServer(args) {
1654
1920
  }));
1655
1921
  return;
1656
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
+ }
1657
1928
  if (req.method === 'POST' && url.pathname === '/auth/save') {
1658
1929
  const body = await readRequestBody(req);
1659
1930
  const shieldId = typeof body.shieldId === 'string' ? body.shieldId.trim() : '';
@@ -1912,8 +2183,8 @@ class StdioMcpClient {
1912
2183
  });
1913
2184
  this.notify('notifications/initialized', {});
1914
2185
  }
1915
- async callTool(name, args) {
1916
- 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 } : {}) });
1917
2188
  }
1918
2189
  async listTools() {
1919
2190
  const result = await this.request('tools/list', {});
@@ -2050,8 +2321,8 @@ class HttpMcpClient {
2050
2321
  });
2051
2322
  await this.notify('notifications/initialized', {});
2052
2323
  }
2053
- async callTool(name, args) {
2054
- 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 } : {}) });
2055
2326
  }
2056
2327
  async listTools() {
2057
2328
  const result = await this.request('tools/list', {});
@@ -2144,8 +2415,8 @@ class SseMcpClient {
2144
2415
  });
2145
2416
  await this.notify('notifications/initialized', {});
2146
2417
  }
2147
- async callTool(name, args) {
2148
- 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 } : {}) });
2149
2420
  }
2150
2421
  async listTools() {
2151
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.6"
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.6",
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": {