fullcourtdefense-cli 1.7.5 → 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 +295 -8
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -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(
|
|
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,17 @@ 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; }
|
|
1021
|
+
.localTable { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; overflow: hidden; border-radius: 12px; }
|
|
1022
|
+
.localTable th, .localTable td { border-bottom: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }
|
|
1023
|
+
.localTable th { color: #475569; background: #f8fafc; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
|
|
1024
|
+
.copyBox { margin-top: 10px; }
|
|
1014
1025
|
.hidden { display: none; }
|
|
1015
1026
|
@media (max-width: 800px) { .grid, .two { grid-template-columns: 1fr; } }
|
|
1016
1027
|
</style>
|
|
@@ -1049,7 +1060,8 @@ function compactScanHtml(args) {
|
|
|
1049
1060
|
<div class="step" id="step-dot-3">3. Run red-team scan</div>
|
|
1050
1061
|
</div>
|
|
1051
1062
|
<div class="grid" id="cards">
|
|
1052
|
-
<button class="card active" data-target="
|
|
1063
|
+
<button class="card active" data-target="runner"><b>Run MCP Tool</b><span>Direct local call through proxy</span></button>
|
|
1064
|
+
<button class="card" data-target="endpoint"><b>Endpoint</b><span>Internal agent/chat API</span></button>
|
|
1053
1065
|
<button class="card" data-target="rag"><b>RAG</b><span>Retrieval URL or corpus path</span></button>
|
|
1054
1066
|
<button class="card" data-target="mcp"><b>MCP</b><span>Tool server via stdio or HTTP</span></button>
|
|
1055
1067
|
<button class="card" data-target="help"><b>Options</b><span>Same modes as web red-team scan</span></button>
|
|
@@ -1064,7 +1076,44 @@ function compactScanHtml(args) {
|
|
|
1064
1076
|
<div class="muted">Choose Endpoint, RAG, or MCP. This runs from inside your organization.</div>
|
|
1065
1077
|
</div>
|
|
1066
1078
|
</div>
|
|
1067
|
-
<div id="
|
|
1079
|
+
<div id="runner-fields">
|
|
1080
|
+
<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>
|
|
1081
|
+
<div class="two">
|
|
1082
|
+
<div>
|
|
1083
|
+
<label>Protected MCP HTTP/SSE URL</label>
|
|
1084
|
+
<input id="runnerMcpUrl" placeholder="http://127.0.0.1:5066/mcp" />
|
|
1085
|
+
</div>
|
|
1086
|
+
<div>
|
|
1087
|
+
<label>Tool name</label>
|
|
1088
|
+
<input id="runnerTool" placeholder="lookup_customer" />
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
<div class="two">
|
|
1092
|
+
<div><label>Or stdio command</label><input id="runnerMcpCommand" placeholder="fullcourtdefense" /></div>
|
|
1093
|
+
<div><label>Command args</label><input id="runnerMcpArgs" placeholder='mcp-gateway --mcp-command node --mcp-args "["./server.js"]"' /></div>
|
|
1094
|
+
</div>
|
|
1095
|
+
<label>Tool arguments JSON</label>
|
|
1096
|
+
<textarea id="runnerToolArgs" spellcheck="false">{
|
|
1097
|
+
"customerId": "cus_123"
|
|
1098
|
+
}</textarea>
|
|
1099
|
+
<div class="actions">
|
|
1100
|
+
<button class="primary" id="runMcpTool">Run MCP tool</button>
|
|
1101
|
+
<button class="secondary" id="copyMcpResult">Copy result</button>
|
|
1102
|
+
</div>
|
|
1103
|
+
<div id="mcpResultCard" class="resultCard hidden">
|
|
1104
|
+
<div class="resultHead">
|
|
1105
|
+
<div class="resultTitle">MCP tool result</div>
|
|
1106
|
+
<span id="mcpResultStatus" class="statusTag">READY</span>
|
|
1107
|
+
</div>
|
|
1108
|
+
<div id="mcpResultTable"></div>
|
|
1109
|
+
<div class="copyBox">
|
|
1110
|
+
<label>Copyable JSON</label>
|
|
1111
|
+
<textarea id="mcpResultJson" spellcheck="false" readonly></textarea>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
<div id="endpoint-fields" class="hidden">
|
|
1068
1117
|
<label>Internal endpoint URL</label>
|
|
1069
1118
|
<input id="endpoint" placeholder="http://127.0.0.1:3000/chat" />
|
|
1070
1119
|
<div class="two">
|
|
@@ -1098,7 +1147,7 @@ function compactScanHtml(args) {
|
|
|
1098
1147
|
<div id="help-fields" class="hidden">
|
|
1099
1148
|
<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
1149
|
</div>
|
|
1101
|
-
<div class="actions">
|
|
1150
|
+
<div class="actions" id="scanActions">
|
|
1102
1151
|
<button class="secondary" id="command">Show CLI command</button>
|
|
1103
1152
|
<button class="primary" id="connectNext">Continue to scan type</button>
|
|
1104
1153
|
</div>
|
|
@@ -1170,14 +1219,15 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1170
1219
|
</section>
|
|
1171
1220
|
</main>
|
|
1172
1221
|
<script>
|
|
1173
|
-
let target = '
|
|
1222
|
+
let target = 'runner';
|
|
1174
1223
|
let step = 1;
|
|
1175
1224
|
const qs = new URLSearchParams(location.search);
|
|
1176
|
-
if (['endpoint','rag','mcp'].includes(qs.get('target'))) target = qs.get('target');
|
|
1225
|
+
if (['runner','endpoint','rag','mcp'].includes(qs.get('target'))) target = qs.get('target');
|
|
1177
1226
|
const byId = id => document.getElementById(id);
|
|
1178
1227
|
const sync = () => {
|
|
1179
1228
|
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));
|
|
1229
|
+
['runner','endpoint','rag','mcp','help'].forEach(id => byId(id + '-fields').classList.toggle('hidden', id !== target));
|
|
1230
|
+
byId('scanActions').classList.toggle('hidden', target === 'runner');
|
|
1181
1231
|
};
|
|
1182
1232
|
const setStep = next => {
|
|
1183
1233
|
step = next;
|
|
@@ -1195,12 +1245,23 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1195
1245
|
byId('mcpUrl').value = qs.get('mcpUrl') || '';
|
|
1196
1246
|
byId('mcpCommand').value = qs.get('mcpCommand') || '';
|
|
1197
1247
|
byId('mcpArgs').value = qs.get('mcpArgs') || '';
|
|
1248
|
+
byId('runnerMcpUrl').value = qs.get('mcpUrl') || '';
|
|
1249
|
+
byId('runnerMcpCommand').value = qs.get('mcpCommand') || 'fullcourtdefense';
|
|
1250
|
+
byId('runnerMcpArgs').value = qs.get('mcpArgs') || '';
|
|
1251
|
+
byId('runnerTool').value = qs.get('mcpTool') || '';
|
|
1198
1252
|
const payload = () => ({
|
|
1199
1253
|
target, mode: byId('mode').value, format: byId('format').value,
|
|
1200
1254
|
endpoint: byId('endpoint').value, inputField: byId('inputField').value, outputField: byId('outputField').value,
|
|
1201
1255
|
ragUrl: byId('ragUrl').value, ragPath: byId('ragPath').value, ragInputField: byId('ragInputField').value, ragOutputField: byId('ragOutputField').value,
|
|
1202
1256
|
mcpUrl: byId('mcpUrl').value, mcpCommand: byId('mcpCommand').value, mcpArgs: byId('mcpArgs').value, mcpTool: byId('mcpTool').value
|
|
1203
1257
|
});
|
|
1258
|
+
const mcpRunnerPayload = () => ({
|
|
1259
|
+
mcpUrl: byId('runnerMcpUrl').value,
|
|
1260
|
+
mcpCommand: byId('runnerMcpCommand').value,
|
|
1261
|
+
mcpArgs: byId('runnerMcpArgs').value,
|
|
1262
|
+
toolName: byId('runnerTool').value,
|
|
1263
|
+
toolArgs: byId('runnerToolArgs').value
|
|
1264
|
+
});
|
|
1204
1265
|
const renderCommand = p => {
|
|
1205
1266
|
const parts = ['fullcourtdefense scan --local --type', p.target, '--mode', p.mode, '--format', p.format, '--no-open'];
|
|
1206
1267
|
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 +1316,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1255
1316
|
: 'No saved web report URL found in this output.';
|
|
1256
1317
|
};
|
|
1257
1318
|
const resetForNewScan = () => {
|
|
1258
|
-
target = '
|
|
1319
|
+
target = 'runner';
|
|
1259
1320
|
sync();
|
|
1260
1321
|
setStep(1);
|
|
1261
1322
|
byId('endpoint').value = '';
|
|
@@ -1269,6 +1330,14 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1269
1330
|
byId('mcpCommand').value = '';
|
|
1270
1331
|
byId('mcpArgs').value = '';
|
|
1271
1332
|
byId('mcpTool').value = 'all';
|
|
1333
|
+
byId('runnerMcpUrl').value = '';
|
|
1334
|
+
byId('runnerMcpCommand').value = 'fullcourtdefense';
|
|
1335
|
+
byId('runnerMcpArgs').value = '';
|
|
1336
|
+
byId('runnerTool').value = '';
|
|
1337
|
+
byId('runnerToolArgs').value = '{\\n "customerId": "cus_123"\\n}';
|
|
1338
|
+
byId('mcpResultCard').classList.add('hidden');
|
|
1339
|
+
byId('mcpResultJson').value = '';
|
|
1340
|
+
byId('mcpResultTable').innerHTML = '';
|
|
1272
1341
|
byId('mode').value = 'quick';
|
|
1273
1342
|
byId('format').value = 'report';
|
|
1274
1343
|
clearScanSummary();
|
|
@@ -1331,6 +1400,116 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1331
1400
|
};
|
|
1332
1401
|
byId('checkBackend').onclick = checkBackend;
|
|
1333
1402
|
checkBackend().catch(() => {});
|
|
1403
|
+
function mcpRows(value) {
|
|
1404
|
+
if (Array.isArray(value)) return value;
|
|
1405
|
+
const content = value && value.content;
|
|
1406
|
+
if (Array.isArray(content)) {
|
|
1407
|
+
const textItem = content.find(item => item && typeof item.text === 'string');
|
|
1408
|
+
if (textItem) {
|
|
1409
|
+
try {
|
|
1410
|
+
const parsed = JSON.parse(textItem.text);
|
|
1411
|
+
if (Array.isArray(parsed)) return parsed;
|
|
1412
|
+
if (parsed && Array.isArray(parsed.data)) return parsed.data;
|
|
1413
|
+
if (parsed && Array.isArray(parsed.results)) return parsed.results;
|
|
1414
|
+
} catch {}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (value && Array.isArray(value.data)) return value.data;
|
|
1418
|
+
if (value && Array.isArray(value.results)) return value.results;
|
|
1419
|
+
return null;
|
|
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
|
+
}
|
|
1465
|
+
function renderMcpResult(data) {
|
|
1466
|
+
byId('mcpResultCard').classList.remove('hidden');
|
|
1467
|
+
byId('mcpResultStatus').textContent = data.blocked ? 'BLOCKED' : data.success ? 'ALLOW / RESULT' : 'ERROR';
|
|
1468
|
+
byId('mcpResultStatus').className = data.success ? 'statusTag pass' : 'statusTag fail';
|
|
1469
|
+
byId('mcpResultJson').value = JSON.stringify(data, null, 2);
|
|
1470
|
+
const rows = data.success ? mcpRows(data.result) : null;
|
|
1471
|
+
if (rows && rows.length && rows.every(row => row && typeof row === 'object' && !Array.isArray(row))) {
|
|
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>');
|
|
1478
|
+
} else {
|
|
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>';
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
byId('copyMcpResult').onclick = async () => {
|
|
1486
|
+
await navigator.clipboard?.writeText(byId('mcpResultJson').value || '').catch(() => undefined);
|
|
1487
|
+
byId('copyMcpResult').textContent = 'Copied';
|
|
1488
|
+
setTimeout(() => { byId('copyMcpResult').textContent = 'Copy result'; }, 1200);
|
|
1489
|
+
};
|
|
1490
|
+
byId('runMcpTool').onclick = async () => {
|
|
1491
|
+
byId('runMcpTool').disabled = true;
|
|
1492
|
+
byId('runMcpTool').textContent = 'Running...';
|
|
1493
|
+
byId('mcpResultCard').classList.add('hidden');
|
|
1494
|
+
setConsole('Calling MCP tool locally through configured proxy/server...\\n', 'Running MCP');
|
|
1495
|
+
try {
|
|
1496
|
+
const res = await fetch('/mcp/run', {
|
|
1497
|
+
method: 'POST',
|
|
1498
|
+
headers: { 'content-type': 'application/json' },
|
|
1499
|
+
body: JSON.stringify(mcpRunnerPayload())
|
|
1500
|
+
});
|
|
1501
|
+
const data = await res.json();
|
|
1502
|
+
renderMcpResult(data);
|
|
1503
|
+
setConsole(JSON.stringify(data, null, 2), data.success ? 'MCP complete' : 'MCP blocked/error');
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
const data = { success: false, error: error && error.message ? error.message : String(error) };
|
|
1506
|
+
renderMcpResult(data);
|
|
1507
|
+
setConsole(JSON.stringify(data, null, 2), 'MCP error');
|
|
1508
|
+
} finally {
|
|
1509
|
+
byId('runMcpTool').disabled = false;
|
|
1510
|
+
byId('runMcpTool').textContent = 'Run MCP tool';
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1334
1513
|
byId('run').onclick = async () => {
|
|
1335
1514
|
if (byId('run').disabled) return;
|
|
1336
1515
|
byId('run').disabled = true;
|
|
@@ -1455,6 +1634,107 @@ async function runCliFromBrowser(body, onChunk) {
|
|
|
1455
1634
|
});
|
|
1456
1635
|
});
|
|
1457
1636
|
}
|
|
1637
|
+
function createMcpClient(options) {
|
|
1638
|
+
if (options.transport === 'sse')
|
|
1639
|
+
return new SseMcpClient(options);
|
|
1640
|
+
if (options.transport === 'http')
|
|
1641
|
+
return new HttpMcpClient(options);
|
|
1642
|
+
return new StdioMcpClient(options.command || '', options.args || []);
|
|
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
|
+
}
|
|
1672
|
+
async function runMcpToolFromBrowser(body) {
|
|
1673
|
+
const rawUrl = typeof body.mcpUrl === 'string' ? body.mcpUrl.trim() : '';
|
|
1674
|
+
const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
|
|
1675
|
+
const rawArgs = typeof body.mcpArgs === 'string' ? body.mcpArgs.trim() : '';
|
|
1676
|
+
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
1677
|
+
if (!toolName)
|
|
1678
|
+
throw new Error('Tool name is required.');
|
|
1679
|
+
if (!rawUrl && !rawCommand)
|
|
1680
|
+
throw new Error('Provide either a protected MCP URL or stdio command.');
|
|
1681
|
+
const transport = rawUrl
|
|
1682
|
+
? (/\/sse\/?$/.test(new URL(rawUrl).pathname) ? 'sse' : 'http')
|
|
1683
|
+
: 'stdio';
|
|
1684
|
+
const toolArgs = parseJsonObject(typeof body.toolArgs === 'string' ? body.toolArgs : JSON.stringify(body.toolArgs || {}));
|
|
1685
|
+
const options = {
|
|
1686
|
+
transport,
|
|
1687
|
+
url: rawUrl || undefined,
|
|
1688
|
+
command: rawCommand || undefined,
|
|
1689
|
+
args: parseArgsList(rawArgs),
|
|
1690
|
+
authType: 'none',
|
|
1691
|
+
toolName,
|
|
1692
|
+
toolArgs,
|
|
1693
|
+
};
|
|
1694
|
+
const client = createMcpClient(options);
|
|
1695
|
+
const startedAt = Date.now();
|
|
1696
|
+
try {
|
|
1697
|
+
await client.initialize();
|
|
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
|
+
}
|
|
1713
|
+
return {
|
|
1714
|
+
success: true,
|
|
1715
|
+
toolName,
|
|
1716
|
+
transport,
|
|
1717
|
+
destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
|
|
1718
|
+
elapsedMs: Date.now() - startedAt,
|
|
1719
|
+
result,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
return {
|
|
1724
|
+
success: false,
|
|
1725
|
+
blocked: /policy|blocked|approval|not allowed|denied/i.test(error instanceof Error ? error.message : String(error)),
|
|
1726
|
+
toolName,
|
|
1727
|
+
transport,
|
|
1728
|
+
destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
|
|
1729
|
+
elapsedMs: Date.now() - startedAt,
|
|
1730
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1731
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
finally {
|
|
1735
|
+
client.close();
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1458
1738
|
function startCompactWebServer(args) {
|
|
1459
1739
|
const server = http.createServer(async (req, res) => {
|
|
1460
1740
|
try {
|
|
@@ -1504,6 +1784,13 @@ function startCompactWebServer(args) {
|
|
|
1504
1784
|
res.end(JSON.stringify(check));
|
|
1505
1785
|
return;
|
|
1506
1786
|
}
|
|
1787
|
+
if (req.method === 'POST' && url.pathname === '/mcp/run') {
|
|
1788
|
+
const body = await readRequestBody(req);
|
|
1789
|
+
const result = await runMcpToolFromBrowser(body);
|
|
1790
|
+
res.writeHead(result.success ? 200 : 400, { 'content-type': 'application/json; charset=utf-8' });
|
|
1791
|
+
res.end(JSON.stringify(result));
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1507
1794
|
if (req.method === 'POST' && url.pathname === '/run') {
|
|
1508
1795
|
const body = await readRequestBody(req);
|
|
1509
1796
|
res.writeHead(200, {
|
package/dist/version.json
CHANGED