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.
- package/dist/commands/local-scan.js +106 -6
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
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