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
|
-
|
|
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 "["./server.js"]"' /></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
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
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