fullcourtdefense-cli 1.7.6 → 1.7.7

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.
@@ -1011,6 +1011,13 @@ 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
+ .mcpBanner { margin-top: 10px; border-radius: 14px; padding: 12px; font-size: 13px; line-height: 1.45; }
1015
+ .mcpBanner.ok { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
1016
+ .mcpBanner.blocked { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
1017
+ .mcpCards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 10px; }
1018
+ .mcpCard { border: 1px solid #e2e8f0; background: white; border-radius: 12px; padding: 10px; }
1019
+ .mcpCard b { display: block; color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }
1020
+ .mcpCard span { color: #0f172a; font-size: 13px; font-weight: 750; overflow-wrap: anywhere; }
1014
1021
  .localTable { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; overflow: hidden; border-radius: 12px; }
1015
1022
  .localTable th, .localTable td { border-bottom: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }
1016
1023
  .localTable th { color: #475569; background: #f8fafc; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
@@ -1411,19 +1418,68 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1411
1418
  if (value && Array.isArray(value.results)) return value.results;
1412
1419
  return null;
1413
1420
  }
1421
+ function mcpText(value) {
1422
+ if (value == null) return '';
1423
+ if (typeof value === 'string') return value;
1424
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
1425
+ const content = value.content;
1426
+ if (Array.isArray(content)) {
1427
+ return content
1428
+ .map(item => item && typeof item.text === 'string' ? item.text : '')
1429
+ .filter(Boolean)
1430
+ .join('\\n');
1431
+ }
1432
+ return '';
1433
+ }
1434
+ function mcpObject(value) {
1435
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
1436
+ const contentText = mcpText(value);
1437
+ if (contentText) {
1438
+ try {
1439
+ const parsed = JSON.parse(contentText);
1440
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
1441
+ } catch {}
1442
+ }
1443
+ if (value.data && typeof value.data === 'object' && !Array.isArray(value.data)) return value.data;
1444
+ if (value.result && typeof value.result === 'object' && !Array.isArray(value.result)) return value.result;
1445
+ return value;
1446
+ }
1447
+ function cellValue(value) {
1448
+ if (value == null) return '';
1449
+ if (typeof value === 'object') return JSON.stringify(value);
1450
+ return String(value);
1451
+ }
1452
+ function renderObjectCards(obj) {
1453
+ const preferred = ['id', 'customerId', 'name', 'title', 'status', 'plan', 'email', 'decision', 'matchedPolicy', 'reason', 'ok'];
1454
+ const keys = Array.from(new Set([...preferred.filter(k => Object.prototype.hasOwnProperty.call(obj, k)), ...Object.keys(obj)])).slice(0, 12);
1455
+ return '<div class="mcpCards">' + keys.map(k => '<div class="mcpCard"><b>' + htmlEscape(k) + '</b><span>' + htmlEscape(cellValue(obj[k])) + '</span></div>').join('') + '</div>';
1456
+ }
1457
+ function renderMcpTable(rows) {
1458
+ const preferred = ['id', 'customerId', 'name', 'title', 'status', 'plan', 'email', 'createdAt', 'updatedAt'];
1459
+ 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);
1460
+ return '<table class="localTable"><thead><tr>' + keys.map(k => '<th>' + htmlEscape(k) + '</th>').join('') + '</tr></thead><tbody>' +
1461
+ rows.slice(0, 50).map(row => '<tr>' + keys.map(k => '<td>' + htmlEscape(cellValue(row[k])) + '</td>').join('') + '</tr>').join('') +
1462
+ '</tbody></table>' +
1463
+ (rows.length > 50 ? '<div class="resultMeta">Showing first 50 rows. Full result is in raw JSON below.</div>' : '');
1464
+ }
1414
1465
  function renderMcpResult(data) {
1415
1466
  byId('mcpResultCard').classList.remove('hidden');
1416
- byId('mcpResultStatus').textContent = data.success ? 'ALLOW / RESULT' : 'ERROR / BLOCK';
1467
+ byId('mcpResultStatus').textContent = data.blocked ? 'BLOCKED' : data.success ? 'ALLOW / RESULT' : 'ERROR';
1417
1468
  byId('mcpResultStatus').className = data.success ? 'statusTag pass' : 'statusTag fail';
1418
1469
  byId('mcpResultJson').value = JSON.stringify(data, null, 2);
1419
1470
  const rows = data.success ? mcpRows(data.result) : null;
1420
1471
  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>';
1472
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner ok"><b>Allowed.</b> Tool returned ' + rows.length + ' row(s).</div>' + renderMcpTable(rows);
1473
+ } else if (data.success) {
1474
+ const obj = mcpObject(data.result);
1475
+ const text = mcpText(data.result);
1476
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner ok"><b>Allowed.</b> Tool call completed through the configured MCP server/proxy.</div>' +
1477
+ (obj ? renderObjectCards(obj) : '<div class="resultMeta">' + htmlEscape(text || 'Result is shown as raw JSON below.') + '</div>');
1425
1478
  } else {
1426
- byId('mcpResultTable').innerHTML = '<div class="resultMeta">' + (data.success ? 'Result is shown as copyable JSON below.' : htmlEscape(data.error || 'MCP call failed.')) + '</div>';
1479
+ const blockedCopy = data.blocked
1480
+ ? 'Blocked because a FullCourtDefense policy or local safety rule is defined for this tool/action.'
1481
+ : 'The MCP call failed before a successful tool result was returned.';
1482
+ 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
1483
  }
1428
1484
  }
1429
1485
  byId('copyMcpResult').onclick = async () => {
@@ -1585,6 +1641,34 @@ function createMcpClient(options) {
1585
1641
  return new HttpMcpClient(options);
1586
1642
  return new StdioMcpClient(options.command || '', options.args || []);
1587
1643
  }
1644
+ function parseMcpErrorResult(result) {
1645
+ if (!result || typeof result !== 'object' || !result.isError)
1646
+ return null;
1647
+ const content = result.content;
1648
+ const text = Array.isArray(content)
1649
+ ? content.map(item => item && typeof item === 'object' && typeof item.text === 'string' ? item.text : '').filter(Boolean).join('\n')
1650
+ : '';
1651
+ if (text) {
1652
+ try {
1653
+ const parsed = JSON.parse(text);
1654
+ if (parsed && typeof parsed === 'object') {
1655
+ return {
1656
+ blocked: parsed.blocked === true || /policy|blocked|approval/i.test(text),
1657
+ reason: typeof parsed.reason === 'string' ? parsed.reason : text,
1658
+ details: parsed,
1659
+ };
1660
+ }
1661
+ }
1662
+ catch {
1663
+ // plain-text MCP error
1664
+ }
1665
+ }
1666
+ return {
1667
+ blocked: /policy|blocked|approval|not allowed|denied/i.test(text),
1668
+ reason: text || 'The MCP server returned an error result.',
1669
+ details: result,
1670
+ };
1671
+ }
1588
1672
  async function runMcpToolFromBrowser(body) {
1589
1673
  const rawUrl = typeof body.mcpUrl === 'string' ? body.mcpUrl.trim() : '';
1590
1674
  const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
@@ -1612,6 +1696,20 @@ async function runMcpToolFromBrowser(body) {
1612
1696
  try {
1613
1697
  await client.initialize();
1614
1698
  const result = await client.callTool(toolName, toolArgs);
1699
+ const errorResult = parseMcpErrorResult(result);
1700
+ if (errorResult) {
1701
+ return {
1702
+ success: false,
1703
+ blocked: errorResult.blocked,
1704
+ toolName,
1705
+ transport,
1706
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1707
+ elapsedMs: Date.now() - startedAt,
1708
+ reason: errorResult.reason,
1709
+ result,
1710
+ details: errorResult.details,
1711
+ };
1712
+ }
1615
1713
  return {
1616
1714
  success: true,
1617
1715
  toolName,
@@ -1624,10 +1722,12 @@ async function runMcpToolFromBrowser(body) {
1624
1722
  catch (error) {
1625
1723
  return {
1626
1724
  success: false,
1725
+ blocked: /policy|blocked|approval|not allowed|denied/i.test(error instanceof Error ? error.message : String(error)),
1627
1726
  toolName,
1628
1727
  transport,
1629
1728
  destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1630
1729
  elapsedMs: Date.now() - startedAt,
1730
+ reason: error instanceof Error ? error.message : String(error),
1631
1731
  error: error instanceof Error ? error.message : String(error),
1632
1732
  };
1633
1733
  }
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.7.6"
2
+ "version": "1.7.7"
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.7",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {