fullcourtdefense-cli 1.7.5 → 1.7.6

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.
@@ -972,7 +972,7 @@ function compactScanHtml(args) {
972
972
  .eyebrow { color: #1d4ed8; font-size: 12px; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; }
973
973
  h1 { margin: 6px 0 8px; font-size: 26px; line-height: 1.15; }
974
974
  p { color: #475569; line-height: 1.55; }
975
- .grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
975
+ .grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
976
976
  .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 18px; }
977
977
  .step { border: 1px solid #e2e8f0; background: white; color: #64748b; border-radius: 16px; padding: 12px; font-size: 12px; font-weight: 900; }
978
978
  .step.active { border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
@@ -1011,6 +1011,10 @@ function compactScanHtml(args) {
1011
1011
  .resultMetric span { font-size: 16px; font-weight: 800; color: #0f172a; }
1012
1012
  .resultMeta { margin-top: 10px; color: #334155; font-size: 12px; line-height: 1.5; }
1013
1013
  .resultMeta a { color: #1d4ed8; }
1014
+ .localTable { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; overflow: hidden; border-radius: 12px; }
1015
+ .localTable th, .localTable td { border-bottom: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }
1016
+ .localTable th { color: #475569; background: #f8fafc; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
1017
+ .copyBox { margin-top: 10px; }
1014
1018
  .hidden { display: none; }
1015
1019
  @media (max-width: 800px) { .grid, .two { grid-template-columns: 1fr; } }
1016
1020
  </style>
@@ -1049,7 +1053,8 @@ function compactScanHtml(args) {
1049
1053
  <div class="step" id="step-dot-3">3. Run red-team scan</div>
1050
1054
  </div>
1051
1055
  <div class="grid" id="cards">
1052
- <button class="card active" data-target="endpoint"><b>Endpoint</b><span>Internal agent/chat API</span></button>
1056
+ <button class="card active" data-target="runner"><b>Run MCP Tool</b><span>Direct local call through proxy</span></button>
1057
+ <button class="card" data-target="endpoint"><b>Endpoint</b><span>Internal agent/chat API</span></button>
1053
1058
  <button class="card" data-target="rag"><b>RAG</b><span>Retrieval URL or corpus path</span></button>
1054
1059
  <button class="card" data-target="mcp"><b>MCP</b><span>Tool server via stdio or HTTP</span></button>
1055
1060
  <button class="card" data-target="help"><b>Options</b><span>Same modes as web red-team scan</span></button>
@@ -1064,7 +1069,44 @@ function compactScanHtml(args) {
1064
1069
  <div class="muted">Choose Endpoint, RAG, or MCP. This runs from inside your organization.</div>
1065
1070
  </div>
1066
1071
  </div>
1067
- <div id="endpoint-fields">
1072
+ <div id="runner-fields">
1073
+ <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>
1074
+ <div class="two">
1075
+ <div>
1076
+ <label>Protected MCP HTTP/SSE URL</label>
1077
+ <input id="runnerMcpUrl" placeholder="http://127.0.0.1:5066/mcp" />
1078
+ </div>
1079
+ <div>
1080
+ <label>Tool name</label>
1081
+ <input id="runnerTool" placeholder="lookup_customer" />
1082
+ </div>
1083
+ </div>
1084
+ <div class="two">
1085
+ <div><label>Or stdio command</label><input id="runnerMcpCommand" placeholder="fullcourtdefense" /></div>
1086
+ <div><label>Command args</label><input id="runnerMcpArgs" placeholder='mcp-gateway --mcp-command node --mcp-args "[&quot;./server.js&quot;]"' /></div>
1087
+ </div>
1088
+ <label>Tool arguments JSON</label>
1089
+ <textarea id="runnerToolArgs" spellcheck="false">{
1090
+ "customerId": "cus_123"
1091
+ }</textarea>
1092
+ <div class="actions">
1093
+ <button class="primary" id="runMcpTool">Run MCP tool</button>
1094
+ <button class="secondary" id="copyMcpResult">Copy result</button>
1095
+ </div>
1096
+ <div id="mcpResultCard" class="resultCard hidden">
1097
+ <div class="resultHead">
1098
+ <div class="resultTitle">MCP tool result</div>
1099
+ <span id="mcpResultStatus" class="statusTag">READY</span>
1100
+ </div>
1101
+ <div id="mcpResultTable"></div>
1102
+ <div class="copyBox">
1103
+ <label>Copyable JSON</label>
1104
+ <textarea id="mcpResultJson" spellcheck="false" readonly></textarea>
1105
+ </div>
1106
+ </div>
1107
+ </div>
1108
+
1109
+ <div id="endpoint-fields" class="hidden">
1068
1110
  <label>Internal endpoint URL</label>
1069
1111
  <input id="endpoint" placeholder="http://127.0.0.1:3000/chat" />
1070
1112
  <div class="two">
@@ -1098,7 +1140,7 @@ function compactScanHtml(args) {
1098
1140
  <div id="help-fields" class="hidden">
1099
1141
  <div class="note">Use Endpoint for internal chat APIs, RAG for retrieval/corpus injection checks, and MCP for tool output and tool-abuse red-team scans. Reports save back to the normal web report history when Shield key is configured.</div>
1100
1142
  </div>
1101
- <div class="actions">
1143
+ <div class="actions" id="scanActions">
1102
1144
  <button class="secondary" id="command">Show CLI command</button>
1103
1145
  <button class="primary" id="connectNext">Continue to scan type</button>
1104
1146
  </div>
@@ -1170,14 +1212,15 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1170
1212
  </section>
1171
1213
  </main>
1172
1214
  <script>
1173
- let target = 'endpoint';
1215
+ let target = 'runner';
1174
1216
  let step = 1;
1175
1217
  const qs = new URLSearchParams(location.search);
1176
- if (['endpoint','rag','mcp'].includes(qs.get('target'))) target = qs.get('target');
1218
+ if (['runner','endpoint','rag','mcp'].includes(qs.get('target'))) target = qs.get('target');
1177
1219
  const byId = id => document.getElementById(id);
1178
1220
  const sync = () => {
1179
1221
  document.querySelectorAll('.card').forEach(c => c.classList.toggle('active', c.dataset.target === target));
1180
- ['endpoint','rag','mcp','help'].forEach(id => byId(id + '-fields').classList.toggle('hidden', id !== target));
1222
+ ['runner','endpoint','rag','mcp','help'].forEach(id => byId(id + '-fields').classList.toggle('hidden', id !== target));
1223
+ byId('scanActions').classList.toggle('hidden', target === 'runner');
1181
1224
  };
1182
1225
  const setStep = next => {
1183
1226
  step = next;
@@ -1195,12 +1238,23 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1195
1238
  byId('mcpUrl').value = qs.get('mcpUrl') || '';
1196
1239
  byId('mcpCommand').value = qs.get('mcpCommand') || '';
1197
1240
  byId('mcpArgs').value = qs.get('mcpArgs') || '';
1241
+ byId('runnerMcpUrl').value = qs.get('mcpUrl') || '';
1242
+ byId('runnerMcpCommand').value = qs.get('mcpCommand') || 'fullcourtdefense';
1243
+ byId('runnerMcpArgs').value = qs.get('mcpArgs') || '';
1244
+ byId('runnerTool').value = qs.get('mcpTool') || '';
1198
1245
  const payload = () => ({
1199
1246
  target, mode: byId('mode').value, format: byId('format').value,
1200
1247
  endpoint: byId('endpoint').value, inputField: byId('inputField').value, outputField: byId('outputField').value,
1201
1248
  ragUrl: byId('ragUrl').value, ragPath: byId('ragPath').value, ragInputField: byId('ragInputField').value, ragOutputField: byId('ragOutputField').value,
1202
1249
  mcpUrl: byId('mcpUrl').value, mcpCommand: byId('mcpCommand').value, mcpArgs: byId('mcpArgs').value, mcpTool: byId('mcpTool').value
1203
1250
  });
1251
+ const mcpRunnerPayload = () => ({
1252
+ mcpUrl: byId('runnerMcpUrl').value,
1253
+ mcpCommand: byId('runnerMcpCommand').value,
1254
+ mcpArgs: byId('runnerMcpArgs').value,
1255
+ toolName: byId('runnerTool').value,
1256
+ toolArgs: byId('runnerToolArgs').value
1257
+ });
1204
1258
  const renderCommand = p => {
1205
1259
  const parts = ['fullcourtdefense scan --local --type', p.target, '--mode', p.mode, '--format', p.format, '--no-open'];
1206
1260
  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');
@@ -1255,7 +1309,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1255
1309
  : 'No saved web report URL found in this output.';
1256
1310
  };
1257
1311
  const resetForNewScan = () => {
1258
- target = 'endpoint';
1312
+ target = 'runner';
1259
1313
  sync();
1260
1314
  setStep(1);
1261
1315
  byId('endpoint').value = '';
@@ -1269,6 +1323,14 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1269
1323
  byId('mcpCommand').value = '';
1270
1324
  byId('mcpArgs').value = '';
1271
1325
  byId('mcpTool').value = 'all';
1326
+ byId('runnerMcpUrl').value = '';
1327
+ byId('runnerMcpCommand').value = 'fullcourtdefense';
1328
+ byId('runnerMcpArgs').value = '';
1329
+ byId('runnerTool').value = '';
1330
+ byId('runnerToolArgs').value = '{\\n "customerId": "cus_123"\\n}';
1331
+ byId('mcpResultCard').classList.add('hidden');
1332
+ byId('mcpResultJson').value = '';
1333
+ byId('mcpResultTable').innerHTML = '';
1272
1334
  byId('mode').value = 'quick';
1273
1335
  byId('format').value = 'report';
1274
1336
  clearScanSummary();
@@ -1331,6 +1393,67 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1331
1393
  };
1332
1394
  byId('checkBackend').onclick = checkBackend;
1333
1395
  checkBackend().catch(() => {});
1396
+ function mcpRows(value) {
1397
+ if (Array.isArray(value)) return value;
1398
+ const content = value && value.content;
1399
+ if (Array.isArray(content)) {
1400
+ const textItem = content.find(item => item && typeof item.text === 'string');
1401
+ if (textItem) {
1402
+ try {
1403
+ const parsed = JSON.parse(textItem.text);
1404
+ if (Array.isArray(parsed)) return parsed;
1405
+ if (parsed && Array.isArray(parsed.data)) return parsed.data;
1406
+ if (parsed && Array.isArray(parsed.results)) return parsed.results;
1407
+ } catch {}
1408
+ }
1409
+ }
1410
+ if (value && Array.isArray(value.data)) return value.data;
1411
+ if (value && Array.isArray(value.results)) return value.results;
1412
+ return null;
1413
+ }
1414
+ function renderMcpResult(data) {
1415
+ byId('mcpResultCard').classList.remove('hidden');
1416
+ byId('mcpResultStatus').textContent = data.success ? 'ALLOW / RESULT' : 'ERROR / BLOCK';
1417
+ byId('mcpResultStatus').className = data.success ? 'statusTag pass' : 'statusTag fail';
1418
+ byId('mcpResultJson').value = JSON.stringify(data, null, 2);
1419
+ const rows = data.success ? mcpRows(data.result) : null;
1420
+ 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>';
1425
+ } else {
1426
+ byId('mcpResultTable').innerHTML = '<div class="resultMeta">' + (data.success ? 'Result is shown as copyable JSON below.' : htmlEscape(data.error || 'MCP call failed.')) + '</div>';
1427
+ }
1428
+ }
1429
+ byId('copyMcpResult').onclick = async () => {
1430
+ await navigator.clipboard?.writeText(byId('mcpResultJson').value || '').catch(() => undefined);
1431
+ byId('copyMcpResult').textContent = 'Copied';
1432
+ setTimeout(() => { byId('copyMcpResult').textContent = 'Copy result'; }, 1200);
1433
+ };
1434
+ byId('runMcpTool').onclick = async () => {
1435
+ byId('runMcpTool').disabled = true;
1436
+ byId('runMcpTool').textContent = 'Running...';
1437
+ byId('mcpResultCard').classList.add('hidden');
1438
+ setConsole('Calling MCP tool locally through configured proxy/server...\\n', 'Running MCP');
1439
+ try {
1440
+ const res = await fetch('/mcp/run', {
1441
+ method: 'POST',
1442
+ headers: { 'content-type': 'application/json' },
1443
+ body: JSON.stringify(mcpRunnerPayload())
1444
+ });
1445
+ const data = await res.json();
1446
+ renderMcpResult(data);
1447
+ setConsole(JSON.stringify(data, null, 2), data.success ? 'MCP complete' : 'MCP blocked/error');
1448
+ } catch (error) {
1449
+ const data = { success: false, error: error && error.message ? error.message : String(error) };
1450
+ renderMcpResult(data);
1451
+ setConsole(JSON.stringify(data, null, 2), 'MCP error');
1452
+ } finally {
1453
+ byId('runMcpTool').disabled = false;
1454
+ byId('runMcpTool').textContent = 'Run MCP tool';
1455
+ }
1456
+ };
1334
1457
  byId('run').onclick = async () => {
1335
1458
  if (byId('run').disabled) return;
1336
1459
  byId('run').disabled = true;
@@ -1455,6 +1578,63 @@ async function runCliFromBrowser(body, onChunk) {
1455
1578
  });
1456
1579
  });
1457
1580
  }
1581
+ function createMcpClient(options) {
1582
+ if (options.transport === 'sse')
1583
+ return new SseMcpClient(options);
1584
+ if (options.transport === 'http')
1585
+ return new HttpMcpClient(options);
1586
+ return new StdioMcpClient(options.command || '', options.args || []);
1587
+ }
1588
+ async function runMcpToolFromBrowser(body) {
1589
+ const rawUrl = typeof body.mcpUrl === 'string' ? body.mcpUrl.trim() : '';
1590
+ const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
1591
+ const rawArgs = typeof body.mcpArgs === 'string' ? body.mcpArgs.trim() : '';
1592
+ const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
1593
+ if (!toolName)
1594
+ throw new Error('Tool name is required.');
1595
+ if (!rawUrl && !rawCommand)
1596
+ throw new Error('Provide either a protected MCP URL or stdio command.');
1597
+ const transport = rawUrl
1598
+ ? (/\/sse\/?$/.test(new URL(rawUrl).pathname) ? 'sse' : 'http')
1599
+ : 'stdio';
1600
+ const toolArgs = parseJsonObject(typeof body.toolArgs === 'string' ? body.toolArgs : JSON.stringify(body.toolArgs || {}));
1601
+ const options = {
1602
+ transport,
1603
+ url: rawUrl || undefined,
1604
+ command: rawCommand || undefined,
1605
+ args: parseArgsList(rawArgs),
1606
+ authType: 'none',
1607
+ toolName,
1608
+ toolArgs,
1609
+ };
1610
+ const client = createMcpClient(options);
1611
+ const startedAt = Date.now();
1612
+ try {
1613
+ await client.initialize();
1614
+ const result = await client.callTool(toolName, toolArgs);
1615
+ return {
1616
+ success: true,
1617
+ toolName,
1618
+ transport,
1619
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1620
+ elapsedMs: Date.now() - startedAt,
1621
+ result,
1622
+ };
1623
+ }
1624
+ catch (error) {
1625
+ return {
1626
+ success: false,
1627
+ toolName,
1628
+ transport,
1629
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1630
+ elapsedMs: Date.now() - startedAt,
1631
+ error: error instanceof Error ? error.message : String(error),
1632
+ };
1633
+ }
1634
+ finally {
1635
+ client.close();
1636
+ }
1637
+ }
1458
1638
  function startCompactWebServer(args) {
1459
1639
  const server = http.createServer(async (req, res) => {
1460
1640
  try {
@@ -1504,6 +1684,13 @@ function startCompactWebServer(args) {
1504
1684
  res.end(JSON.stringify(check));
1505
1685
  return;
1506
1686
  }
1687
+ if (req.method === 'POST' && url.pathname === '/mcp/run') {
1688
+ const body = await readRequestBody(req);
1689
+ const result = await runMcpToolFromBrowser(body);
1690
+ res.writeHead(result.success ? 200 : 400, { 'content-type': 'application/json; charset=utf-8' });
1691
+ res.end(JSON.stringify(result));
1692
+ return;
1693
+ }
1507
1694
  if (req.method === 'POST' && url.pathname === '/run') {
1508
1695
  const body = await readRequestBody(req);
1509
1696
  res.writeHead(200, {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.7.5"
2
+ "version": "1.7.6"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.7.5",
3
+ "version": "1.7.6",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {