fullcourtdefense-cli 1.14.12 → 1.14.13
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.
|
@@ -25,6 +25,7 @@ export declare function isFcdGatewayServer(server: ProxyClassifiableServer): boo
|
|
|
25
25
|
export declare function extractGatewayDownstream(server: ProxyClassifiableServer): {
|
|
26
26
|
command?: string;
|
|
27
27
|
args?: string[];
|
|
28
|
+
url?: string;
|
|
28
29
|
label?: string;
|
|
29
30
|
} | null;
|
|
30
31
|
/** Per-client downstream targets wrapped by FCD gateway entries in the same config file. */
|
|
@@ -64,6 +64,16 @@ function isFcdGatewayServer(server) {
|
|
|
64
64
|
}
|
|
65
65
|
function extractGatewayDownstream(server) {
|
|
66
66
|
const args = server.args || [];
|
|
67
|
+
const urlIdx = args.indexOf('--mcp-url');
|
|
68
|
+
if (urlIdx >= 0 && args[urlIdx + 1]) {
|
|
69
|
+
const url = args[urlIdx + 1];
|
|
70
|
+
let label;
|
|
71
|
+
try {
|
|
72
|
+
label = new URL(url).hostname;
|
|
73
|
+
}
|
|
74
|
+
catch { /* leave undefined */ }
|
|
75
|
+
return { url, label };
|
|
76
|
+
}
|
|
67
77
|
const cmdIdx = args.indexOf('--mcp-command');
|
|
68
78
|
if (cmdIdx < 0 || !args[cmdIdx + 1])
|
|
69
79
|
return null;
|
|
@@ -138,6 +148,8 @@ function buildGatewayWrapIndex(servers) {
|
|
|
138
148
|
wrapped.add(downstream.label.toLowerCase());
|
|
139
149
|
if (downstream?.command)
|
|
140
150
|
wrapped.add(downstreamFingerprint(downstream.command, downstream.args));
|
|
151
|
+
if (downstream?.url)
|
|
152
|
+
wrapped.add(downstream.url.toLowerCase());
|
|
141
153
|
wrapped.add(server.serverName.toLowerCase());
|
|
142
154
|
}
|
|
143
155
|
wrapsByConfig.set(configPath, wrapped);
|
|
@@ -5,6 +5,10 @@ export declare const ALL_GATEWAY_CLIENTS: GatewayClient[];
|
|
|
5
5
|
export interface McpGatewayArgs {
|
|
6
6
|
mcpCommand?: string;
|
|
7
7
|
mcpArgs?: string;
|
|
8
|
+
/** Remote MCP server URL (Streamable HTTP / SSE) — alternative to --mcp-command. */
|
|
9
|
+
mcpUrl?: string;
|
|
10
|
+
/** Optional JSON object of extra HTTP headers for the remote MCP (e.g. auth). */
|
|
11
|
+
mcpHeaders?: string;
|
|
8
12
|
agentName?: string;
|
|
9
13
|
agentClient?: string;
|
|
10
14
|
developerName?: string;
|
|
@@ -55,6 +59,20 @@ export interface InstallMcpGatewayArgs extends McpGatewayArgs {
|
|
|
55
59
|
upload?: string;
|
|
56
60
|
apiKey?: string;
|
|
57
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* How the gateway reaches the real MCP server: spawn a local process (stdio)
|
|
64
|
+
* or speak Streamable HTTP/SSE to a remote URL. Both flow through the exact
|
|
65
|
+
* same policy enforcement in McpGatewayServer.
|
|
66
|
+
*/
|
|
67
|
+
export type DownstreamSpec = {
|
|
68
|
+
kind: 'stdio';
|
|
69
|
+
command: string;
|
|
70
|
+
args: string[];
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'http';
|
|
73
|
+
url: string;
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
};
|
|
58
76
|
export declare function mcpGatewayCommand(args: McpGatewayArgs, config: BotGuardConfig): Promise<void>;
|
|
59
77
|
export declare function installCursorMcpGatewayCommand(args: InstallCursorMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
|
|
60
78
|
export declare function installMcpGatewayCommand(args: InstallMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -79,6 +79,18 @@ const CLIENT_LABELS = {
|
|
|
79
79
|
windsurf: 'Windsurf',
|
|
80
80
|
vscode: 'VS Code',
|
|
81
81
|
};
|
|
82
|
+
function parseHeadersFlag(value) {
|
|
83
|
+
if (!value?.trim())
|
|
84
|
+
return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(value);
|
|
87
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
88
|
+
return Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { /* fall through */ }
|
|
92
|
+
throw new Error('--mcp-headers must be a JSON object, e.g. {"Authorization":"Bearer ..."}');
|
|
93
|
+
}
|
|
82
94
|
function parseArgsList(value) {
|
|
83
95
|
if (!value?.trim())
|
|
84
96
|
return [];
|
|
@@ -399,6 +411,127 @@ class StdioMcpClient {
|
|
|
399
411
|
this.pending.clear();
|
|
400
412
|
}
|
|
401
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Downstream client for remote HTTP/SSE MCP servers (MCP Streamable HTTP
|
|
416
|
+
* transport). JSON-RPC messages are POSTed to the server; responses may come
|
|
417
|
+
* back as plain JSON or as a text/event-stream body. Session continuity uses
|
|
418
|
+
* the Mcp-Session-Id header. Endpoint variants (base, /mcp, stripped /sse)
|
|
419
|
+
* are tried on initialize — the first one that answers wins.
|
|
420
|
+
*/
|
|
421
|
+
class HttpMcpClient {
|
|
422
|
+
url;
|
|
423
|
+
extraHeaders;
|
|
424
|
+
endpoint;
|
|
425
|
+
sessionId;
|
|
426
|
+
nextId = 1;
|
|
427
|
+
constructor(url, extraHeaders = {}) {
|
|
428
|
+
this.url = url;
|
|
429
|
+
this.extraHeaders = extraHeaders;
|
|
430
|
+
if (!url)
|
|
431
|
+
throw new Error('Missing downstream MCP URL. Pass --mcp-url.');
|
|
432
|
+
}
|
|
433
|
+
async initialize() {
|
|
434
|
+
const normalized = this.url.replace(/\/$/, '');
|
|
435
|
+
const candidates = [...new Set([
|
|
436
|
+
normalized,
|
|
437
|
+
normalized.replace(/\/sse(\?.*)?$/i, ''),
|
|
438
|
+
`${normalized}/mcp`,
|
|
439
|
+
normalized.replace(/\/sse(\?.*)?$/i, '/mcp'),
|
|
440
|
+
])].filter(Boolean);
|
|
441
|
+
let lastError;
|
|
442
|
+
for (const endpoint of candidates) {
|
|
443
|
+
try {
|
|
444
|
+
const init = await this.post(endpoint, 'initialize', {
|
|
445
|
+
protocolVersion: '2024-11-05',
|
|
446
|
+
capabilities: {},
|
|
447
|
+
clientInfo: { name: 'agentguard-mcp-gateway', version: '0.1.0' },
|
|
448
|
+
}, 15000);
|
|
449
|
+
if (init?.error) {
|
|
450
|
+
lastError = new Error(init.error.message || `MCP initialize failed at ${endpoint}`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
this.endpoint = endpoint;
|
|
454
|
+
await this.post(endpoint, 'notifications/initialized', {}, 15000, true).catch(() => undefined);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
throw lastError || new Error(`Could not initialize remote MCP server at ${this.url}`);
|
|
462
|
+
}
|
|
463
|
+
async listTools() {
|
|
464
|
+
const response = await this.request('tools/list', {});
|
|
465
|
+
const tools = response?.tools;
|
|
466
|
+
if (!Array.isArray(tools))
|
|
467
|
+
return [];
|
|
468
|
+
return tools
|
|
469
|
+
.filter(tool => typeof tool.name === 'string')
|
|
470
|
+
.map(tool => ({
|
|
471
|
+
name: String(tool.name),
|
|
472
|
+
description: typeof tool.description === 'string'
|
|
473
|
+
? String(tool.description)
|
|
474
|
+
: undefined,
|
|
475
|
+
inputSchema: tool.inputSchema,
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
async callTool(name, args) {
|
|
479
|
+
return this.request('tools/call', { name, arguments: args });
|
|
480
|
+
}
|
|
481
|
+
close() {
|
|
482
|
+
// Stateless HTTP transport — nothing to tear down.
|
|
483
|
+
}
|
|
484
|
+
async request(method, params) {
|
|
485
|
+
if (!this.endpoint)
|
|
486
|
+
throw new Error('Remote MCP client not initialized.');
|
|
487
|
+
const message = await this.post(this.endpoint, method, params, 60000);
|
|
488
|
+
if (message?.error)
|
|
489
|
+
throw new Error(message.error.message || `MCP request failed: ${method}`);
|
|
490
|
+
return message?.result;
|
|
491
|
+
}
|
|
492
|
+
async post(endpoint, method, params, timeoutMs, isNotification = false) {
|
|
493
|
+
const headers = {
|
|
494
|
+
'Content-Type': 'application/json',
|
|
495
|
+
Accept: 'application/json, text/event-stream',
|
|
496
|
+
...this.extraHeaders,
|
|
497
|
+
};
|
|
498
|
+
if (this.sessionId)
|
|
499
|
+
headers['Mcp-Session-Id'] = this.sessionId;
|
|
500
|
+
const body = { jsonrpc: '2.0', method, params };
|
|
501
|
+
if (!isNotification)
|
|
502
|
+
body.id = this.nextId++;
|
|
503
|
+
const controller = new AbortController();
|
|
504
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
505
|
+
try {
|
|
506
|
+
const resp = await fetch(endpoint, {
|
|
507
|
+
method: 'POST',
|
|
508
|
+
headers,
|
|
509
|
+
body: JSON.stringify(body),
|
|
510
|
+
signal: controller.signal,
|
|
511
|
+
});
|
|
512
|
+
const nextSession = resp.headers.get('mcp-session-id');
|
|
513
|
+
if (nextSession)
|
|
514
|
+
this.sessionId = nextSession;
|
|
515
|
+
if (isNotification)
|
|
516
|
+
return null;
|
|
517
|
+
if (!resp.ok && resp.status !== 202) {
|
|
518
|
+
throw new Error(`Remote MCP server returned HTTP ${resp.status} for ${method}`);
|
|
519
|
+
}
|
|
520
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
521
|
+
if (contentType.includes('text/event-stream')) {
|
|
522
|
+
const text = await resp.text();
|
|
523
|
+
const dataLine = text.split('\n').find(line => line.startsWith('data: '));
|
|
524
|
+
if (!dataLine)
|
|
525
|
+
throw new Error(`Remote MCP server sent an empty event stream for ${method}`);
|
|
526
|
+
return JSON.parse(dataLine.slice(6));
|
|
527
|
+
}
|
|
528
|
+
return await resp.json();
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
clearTimeout(timer);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
402
535
|
class AgentGuardApi {
|
|
403
536
|
config;
|
|
404
537
|
constructor(config) {
|
|
@@ -515,16 +648,14 @@ class AgentGuardApi {
|
|
|
515
648
|
}
|
|
516
649
|
class McpGatewayServer {
|
|
517
650
|
gatewayConfig;
|
|
518
|
-
|
|
519
|
-
downstreamArgs;
|
|
651
|
+
downstreamSpec;
|
|
520
652
|
downstream;
|
|
521
653
|
downstreamReady;
|
|
522
654
|
buffer = '';
|
|
523
655
|
api;
|
|
524
|
-
constructor(gatewayConfig,
|
|
656
|
+
constructor(gatewayConfig, downstreamSpec) {
|
|
525
657
|
this.gatewayConfig = gatewayConfig;
|
|
526
|
-
this.
|
|
527
|
-
this.downstreamArgs = downstreamArgs;
|
|
658
|
+
this.downstreamSpec = downstreamSpec;
|
|
528
659
|
this.api = new AgentGuardApi(gatewayConfig);
|
|
529
660
|
}
|
|
530
661
|
start() {
|
|
@@ -581,7 +712,9 @@ class McpGatewayServer {
|
|
|
581
712
|
}
|
|
582
713
|
async ensureDownstream() {
|
|
583
714
|
if (!this.downstreamReady) {
|
|
584
|
-
this.downstream =
|
|
715
|
+
this.downstream = this.downstreamSpec.kind === 'http'
|
|
716
|
+
? new HttpMcpClient(this.downstreamSpec.url, this.downstreamSpec.headers)
|
|
717
|
+
: new StdioMcpClient(this.downstreamSpec.command, this.downstreamSpec.args);
|
|
585
718
|
this.downstreamReady = this.downstream.initialize();
|
|
586
719
|
}
|
|
587
720
|
await this.downstreamReady;
|
|
@@ -722,10 +855,17 @@ class McpGatewayServer {
|
|
|
722
855
|
}
|
|
723
856
|
async function mcpGatewayCommand(args, config) {
|
|
724
857
|
const gatewayConfig = resolveGatewayConfig(args, config);
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
858
|
+
const downstreamUrl = args.mcpUrl || process.env.FCD_MCP_URL || '';
|
|
859
|
+
const spec = downstreamUrl
|
|
860
|
+
? { kind: 'http', url: downstreamUrl, headers: parseHeadersFlag(args.mcpHeaders || process.env.FCD_MCP_HEADERS) }
|
|
861
|
+
: {
|
|
862
|
+
kind: 'stdio',
|
|
863
|
+
command: args.mcpCommand || process.env.FCD_MCP_COMMAND || '',
|
|
864
|
+
args: parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS),
|
|
865
|
+
};
|
|
866
|
+
const server = new McpGatewayServer(gatewayConfig, spec);
|
|
867
|
+
const downstreamLabel = spec.kind === 'http' ? spec.url : `${spec.command} ${spec.args.join(' ')}`;
|
|
868
|
+
process.stderr.write(`AgentGuard MCP Gateway running for ${gatewayConfig.agentName}. Downstream: ${downstreamLabel}\n`);
|
|
729
869
|
server.start();
|
|
730
870
|
}
|
|
731
871
|
function cursorMcpPath(projectScope) {
|
|
@@ -751,12 +891,38 @@ function readJson(file) {
|
|
|
751
891
|
function gatewayScriptPath() {
|
|
752
892
|
return path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
|
|
753
893
|
}
|
|
754
|
-
|
|
894
|
+
/**
|
|
895
|
+
* Resolve which real MCP server an installer should protect: a local stdio
|
|
896
|
+
* command (--mcp-command/--mcp-args) or a remote HTTP/SSE URL (--mcp-url,
|
|
897
|
+
* optionally with --mcp-headers for auth).
|
|
898
|
+
*/
|
|
899
|
+
function resolveDownstreamSpec(args) {
|
|
900
|
+
const url = args.mcpUrl || process.env.FCD_MCP_URL;
|
|
901
|
+
if (url) {
|
|
902
|
+
return { kind: 'http', url, headers: parseHeadersFlag(args.mcpHeaders || process.env.FCD_MCP_HEADERS) };
|
|
903
|
+
}
|
|
904
|
+
const command = args.mcpCommand || process.env.FCD_MCP_COMMAND;
|
|
905
|
+
if (!command) {
|
|
906
|
+
throw new Error('Pass --mcp-command (local stdio MCP) or --mcp-url (remote HTTP/SSE MCP) for the server you want to protect.');
|
|
907
|
+
}
|
|
908
|
+
return { kind: 'stdio', command, args: parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS) };
|
|
909
|
+
}
|
|
910
|
+
function downstreamSpecLabel(spec) {
|
|
911
|
+
return spec.kind === 'http' ? spec.url : `${spec.command} ${spec.args.join(' ')}`.trim();
|
|
912
|
+
}
|
|
913
|
+
function buildGatewayCommandArgs(gatewayConfig, downstream, options = {}) {
|
|
914
|
+
const downstreamFlags = downstream.kind === 'http'
|
|
915
|
+
? [
|
|
916
|
+
'--mcp-url', downstream.url,
|
|
917
|
+
...(downstream.headers && Object.keys(downstream.headers).length > 0
|
|
918
|
+
? ['--mcp-headers', JSON.stringify(downstream.headers)]
|
|
919
|
+
: []),
|
|
920
|
+
]
|
|
921
|
+
: ['--mcp-command', downstream.command, '--mcp-args', JSON.stringify(downstream.args)];
|
|
755
922
|
const commandArgs = [
|
|
756
923
|
gatewayScriptPath(),
|
|
757
924
|
'mcp-gateway',
|
|
758
|
-
|
|
759
|
-
'--mcp-args', JSON.stringify(downstreamArgs),
|
|
925
|
+
...downstreamFlags,
|
|
760
926
|
'--agent-name', gatewayConfig.agentName,
|
|
761
927
|
'--agent-client', gatewayConfig.agentClient,
|
|
762
928
|
'--developer-name', gatewayConfig.developerName,
|
|
@@ -978,16 +1144,13 @@ async function installCursorMcpGatewayCommand(args, config) {
|
|
|
978
1144
|
const nodeExe = process.execPath;
|
|
979
1145
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
980
1146
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
984
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
985
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1147
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1148
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
986
1149
|
writeMcpServerConfig(file, serverName, nodeExe, commandArgs);
|
|
987
1150
|
console.log(`AgentGuard MCP Gateway installed for Cursor (${projectScope ? 'project' : 'global'}).`);
|
|
988
1151
|
console.log(`Config: ${file}`);
|
|
989
1152
|
console.log(`Server: ${serverName}`);
|
|
990
|
-
console.log(`Downstream: ${
|
|
1153
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
991
1154
|
console.log('Restart Cursor or reload MCP servers to use the protected tools.');
|
|
992
1155
|
}
|
|
993
1156
|
async function installMcpGatewayCommand(args, config) {
|
|
@@ -1092,9 +1255,21 @@ function wrappedEntryNeedsHeal(entry) {
|
|
|
1092
1255
|
const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
|
|
1093
1256
|
return args.includes('--shield-id') && !args.includes('--shield-key');
|
|
1094
1257
|
}
|
|
1095
|
-
/** Recover the original downstream command
|
|
1258
|
+
/** Recover the original downstream server (stdio command or remote URL) from a wrapped entry's args. */
|
|
1096
1259
|
function extractWrappedDownstream(entry) {
|
|
1097
1260
|
const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
|
|
1261
|
+
const ui = args.indexOf('--mcp-url');
|
|
1262
|
+
if (ui !== -1 && args[ui + 1]) {
|
|
1263
|
+
let headers;
|
|
1264
|
+
const hi = args.indexOf('--mcp-headers');
|
|
1265
|
+
if (hi !== -1 && args[hi + 1]) {
|
|
1266
|
+
try {
|
|
1267
|
+
headers = parseHeadersFlag(args[hi + 1]);
|
|
1268
|
+
}
|
|
1269
|
+
catch { /* leave undefined */ }
|
|
1270
|
+
}
|
|
1271
|
+
return { kind: 'http', url: args[ui + 1], headers };
|
|
1272
|
+
}
|
|
1098
1273
|
const ci = args.indexOf('--mcp-command');
|
|
1099
1274
|
if (ci === -1 || ci + 1 >= args.length)
|
|
1100
1275
|
return null;
|
|
@@ -1109,7 +1284,7 @@ function extractWrappedDownstream(entry) {
|
|
|
1109
1284
|
}
|
|
1110
1285
|
catch { /* leave empty */ }
|
|
1111
1286
|
}
|
|
1112
|
-
return { command, args: downstreamArgs };
|
|
1287
|
+
return { kind: 'stdio', command, args: downstreamArgs };
|
|
1113
1288
|
}
|
|
1114
1289
|
/** All server maps inside a parsed JSON config, across every known client shape. */
|
|
1115
1290
|
function collectJsonServerMaps(json) {
|
|
@@ -1314,7 +1489,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1314
1489
|
if (downstream) {
|
|
1315
1490
|
const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
|
|
1316
1491
|
entry.command = nodeExe;
|
|
1317
|
-
entry.args = buildGatewayCommandArgs(perServer, downstream
|
|
1492
|
+
entry.args = buildGatewayCommandArgs(perServer, downstream, INSTALL_GATEWAY_ARGS);
|
|
1318
1493
|
stats.healed.push(name);
|
|
1319
1494
|
changed = true;
|
|
1320
1495
|
continue;
|
|
@@ -1323,14 +1498,35 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1323
1498
|
stats.skippedManaged.push(name);
|
|
1324
1499
|
continue;
|
|
1325
1500
|
}
|
|
1501
|
+
const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
|
|
1326
1502
|
if (typeof entry.command !== 'string' || !entry.command) {
|
|
1327
|
-
|
|
1503
|
+
// Remote HTTP/SSE MCP: wrap the URL behind a local stdio gateway that
|
|
1504
|
+
// enforces policies, then forwards over Streamable HTTP. Original url,
|
|
1505
|
+
// headers, and transport type are baked into the args for full unwrap.
|
|
1506
|
+
const remoteUrl = typeof entry.url === 'string' && entry.url
|
|
1507
|
+
? entry.url
|
|
1508
|
+
: (typeof entry.serverUrl === 'string' ? entry.serverUrl : '');
|
|
1509
|
+
if (!remoteUrl || !/^https?:\/\//i.test(remoteUrl)) {
|
|
1510
|
+
stats.skippedRemote.push(name);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const headers = entry.headers && typeof entry.headers === 'object' && !Array.isArray(entry.headers)
|
|
1514
|
+
? Object.fromEntries(Object.entries(entry.headers).map(([k, v]) => [k, String(v)]))
|
|
1515
|
+
: undefined;
|
|
1516
|
+
entry.command = nodeExe;
|
|
1517
|
+
entry.args = buildGatewayCommandArgs(perServer, { kind: 'http', url: remoteUrl, headers }, INSTALL_GATEWAY_ARGS);
|
|
1518
|
+
delete entry.url;
|
|
1519
|
+
delete entry.serverUrl;
|
|
1520
|
+
delete entry.headers;
|
|
1521
|
+
if (typeof entry.type === 'string')
|
|
1522
|
+
entry.type = 'stdio'; // VS Code-style configs must match the new transport
|
|
1523
|
+
stats.wrapped.push(name);
|
|
1524
|
+
changed = true;
|
|
1328
1525
|
continue;
|
|
1329
1526
|
}
|
|
1330
1527
|
const downstreamCommand = entry.command;
|
|
1331
1528
|
const downstreamArgs = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
1332
|
-
const
|
|
1333
|
-
const wrappedArgs = buildGatewayCommandArgs(perServer, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1529
|
+
const wrappedArgs = buildGatewayCommandArgs(perServer, { kind: 'stdio', command: downstreamCommand, args: downstreamArgs }, INSTALL_GATEWAY_ARGS);
|
|
1334
1530
|
entry.command = nodeExe;
|
|
1335
1531
|
entry.args = wrappedArgs;
|
|
1336
1532
|
// entry.env and any other fields are preserved as-is.
|
|
@@ -1369,8 +1565,19 @@ function unwrapJsonConfigFile(file, dryRun) {
|
|
|
1369
1565
|
const downstream = extractWrappedDownstream(entry);
|
|
1370
1566
|
if (!downstream)
|
|
1371
1567
|
continue;
|
|
1372
|
-
|
|
1373
|
-
|
|
1568
|
+
if (downstream.kind === 'http') {
|
|
1569
|
+
delete entry.command;
|
|
1570
|
+
delete entry.args;
|
|
1571
|
+
entry.url = downstream.url;
|
|
1572
|
+
if (downstream.headers && Object.keys(downstream.headers).length > 0)
|
|
1573
|
+
entry.headers = downstream.headers;
|
|
1574
|
+
if (typeof entry.type === 'string')
|
|
1575
|
+
entry.type = /\/sse(\?|$)/i.test(downstream.url) ? 'sse' : 'http';
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
entry.command = downstream.command;
|
|
1579
|
+
entry.args = downstream.args;
|
|
1580
|
+
}
|
|
1374
1581
|
restored.push(name);
|
|
1375
1582
|
changed = true;
|
|
1376
1583
|
}
|
|
@@ -1481,7 +1688,7 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
1481
1688
|
continue;
|
|
1482
1689
|
}
|
|
1483
1690
|
const perServer = { ...gatewayConfig, agentClient: 'codex', agentName: perServerAgentName(gatewayConfig.developerName, 'codex', section.name) };
|
|
1484
|
-
const wrappedArgs = buildGatewayCommandArgs(perServer, section.command, section.args || [], INSTALL_GATEWAY_ARGS);
|
|
1691
|
+
const wrappedArgs = buildGatewayCommandArgs(perServer, { kind: 'stdio', command: section.command, args: section.args || [] }, INSTALL_GATEWAY_ARGS);
|
|
1485
1692
|
const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
|
|
1486
1693
|
lines[section.cmdLine] = `${indent}command = ${JSON.stringify(nodeExe)}`;
|
|
1487
1694
|
const argsLineText = `${indent}args = ${tomlStringArrayLiteral(wrappedArgs)}`;
|
|
@@ -1496,7 +1703,8 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
1496
1703
|
if (!wrappedAlready)
|
|
1497
1704
|
continue;
|
|
1498
1705
|
const downstream = extractWrappedDownstream({ args: section.args });
|
|
1499
|
-
|
|
1706
|
+
// Codex TOML wraps are always stdio (remote URLs never get wrapped into TOML).
|
|
1707
|
+
if (!downstream || downstream.kind !== 'stdio')
|
|
1500
1708
|
continue;
|
|
1501
1709
|
const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
|
|
1502
1710
|
lines[section.cmdLine] = `${indent}command = ${JSON.stringify(downstream.command)}`;
|
|
@@ -1656,12 +1864,9 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1656
1864
|
const nodeExe = process.execPath;
|
|
1657
1865
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1658
1866
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1659
|
-
const
|
|
1660
|
-
if (!downstreamCommand)
|
|
1661
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1662
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1867
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1663
1868
|
const includeShieldKey = false;
|
|
1664
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig,
|
|
1869
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, {
|
|
1665
1870
|
...INSTALL_GATEWAY_ARGS,
|
|
1666
1871
|
includeShieldKey,
|
|
1667
1872
|
});
|
|
@@ -1691,7 +1896,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1691
1896
|
if (result.status === 0) {
|
|
1692
1897
|
console.log(`AgentGuard MCP Gateway installed for Claude Code (${scope}).`);
|
|
1693
1898
|
console.log(`Server: ${serverName}`);
|
|
1694
|
-
console.log(`Downstream: ${
|
|
1899
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
1695
1900
|
if (!includeShieldKey && gatewayConfig.shieldKey) {
|
|
1696
1901
|
console.log('Note: Shield key was loaded from local config/env and omitted from project .mcp.json. Runtime will use the developer machine config/env.');
|
|
1697
1902
|
}
|
|
@@ -1704,7 +1909,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1704
1909
|
console.log('Claude Code CLI was not available, so project config was written directly.');
|
|
1705
1910
|
console.log(`Config: ${file}`);
|
|
1706
1911
|
console.log(`Server: ${serverName}`);
|
|
1707
|
-
console.log(`Downstream: ${
|
|
1912
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
1708
1913
|
if (!includeShieldKey && gatewayConfig.shieldKey) {
|
|
1709
1914
|
console.log('Note: Shield key was loaded from local config/env and omitted from project .mcp.json. Runtime will use the developer machine config/env.');
|
|
1710
1915
|
}
|
|
@@ -1723,11 +1928,8 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
|
|
|
1723
1928
|
const nodeExe = process.execPath;
|
|
1724
1929
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1725
1930
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1729
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1730
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1931
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1932
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1731
1933
|
for (const target of targets) {
|
|
1732
1934
|
writeMcpServerConfig(target.file, serverName, nodeExe, commandArgs);
|
|
1733
1935
|
}
|
|
@@ -1736,7 +1938,7 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
|
|
|
1736
1938
|
console.log(`Config: ${target.file} (${target.source})`);
|
|
1737
1939
|
}
|
|
1738
1940
|
console.log(`Server: ${serverName}`);
|
|
1739
|
-
console.log(`Downstream: ${
|
|
1941
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
1740
1942
|
console.log('Restart Claude Desktop to load the protected tools.');
|
|
1741
1943
|
}
|
|
1742
1944
|
async function installCodexMcpGatewayCommand(args, config) {
|
|
@@ -1750,11 +1952,8 @@ async function installCodexMcpGatewayCommand(args, config) {
|
|
|
1750
1952
|
const nodeExe = process.execPath;
|
|
1751
1953
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1752
1954
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1756
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1757
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1955
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1956
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1758
1957
|
writeCodexMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1759
1958
|
console.log(`AgentGuard MCP Gateway installed for Codex (${projectScope ? 'project' : 'global'}).`);
|
|
1760
1959
|
console.log(`Config: ${file}`);
|
|
@@ -1772,11 +1971,8 @@ async function installGeminiMcpGatewayCommand(args, config) {
|
|
|
1772
1971
|
const nodeExe = process.execPath;
|
|
1773
1972
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1774
1973
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1775
|
-
const
|
|
1776
|
-
|
|
1777
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1778
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1779
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1974
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1975
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1780
1976
|
writeJsonMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1781
1977
|
console.log(`AgentGuard MCP Gateway installed for Gemini CLI (${projectScope ? 'project' : 'user'}).`);
|
|
1782
1978
|
console.log(`Config: ${file}`);
|
|
@@ -1793,11 +1989,8 @@ async function installWindsurfMcpGatewayCommand(args, config) {
|
|
|
1793
1989
|
const nodeExe = process.execPath;
|
|
1794
1990
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1795
1991
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1799
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1800
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1992
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1993
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1801
1994
|
writeJsonMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1802
1995
|
console.log('AgentGuard MCP Gateway installed for Windsurf.');
|
|
1803
1996
|
console.log(`Config: ${file}`);
|
|
@@ -1814,11 +2007,8 @@ async function installVscodeMcpGatewayCommand(args, config) {
|
|
|
1814
2007
|
const nodeExe = process.execPath;
|
|
1815
2008
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1816
2009
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1817
|
-
const
|
|
1818
|
-
|
|
1819
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1820
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1821
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
2010
|
+
const downstream = resolveDownstreamSpec(args);
|
|
2011
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1822
2012
|
const targets = [];
|
|
1823
2013
|
if (projectScope)
|
|
1824
2014
|
targets.push(vscodeMcpPath(true));
|
package/dist/index.js
CHANGED
|
@@ -342,7 +342,9 @@ function printHelp() {
|
|
|
342
342
|
$ fullcourtdefense install-claude-code-mcp-gateway --scope local --mcp-command npm --mcp-args "run mcp"
|
|
343
343
|
$ fullcourtdefense install-claude-code-mcp-gateway --scope project --mcp-command npm --mcp-args "run mcp"
|
|
344
344
|
$ fullcourtdefense install-claude-desktop-mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
345
|
+
$ fullcourtdefense install-cursor-mcp-gateway --server-name my-remote --mcp-url https://mcp.company.com/mcp
|
|
345
346
|
$ fullcourtdefense mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
347
|
+
$ fullcourtdefense mcp-gateway --mcp-url https://mcp.company.com/mcp --mcp-headers "{\"Authorization\":\"Bearer TOKEN\"}"
|
|
346
348
|
$ fullcourtdefense login --token <fleet-enrollment-token>
|
|
347
349
|
$ fullcourtdefense install-all
|
|
348
350
|
$ fullcourtdefense install-all --auto-protect true --upload
|
|
@@ -366,6 +368,8 @@ async function main() {
|
|
|
366
368
|
serverName: flags['server-name'],
|
|
367
369
|
mcpCommand: flags['mcp-command'],
|
|
368
370
|
mcpArgs: flags['mcp-args'],
|
|
371
|
+
mcpUrl: flags['mcp-url'],
|
|
372
|
+
mcpHeaders: flags['mcp-headers'],
|
|
369
373
|
agentName: flags['agent-name'],
|
|
370
374
|
developerName: flags['developer-name'],
|
|
371
375
|
shieldId: flags['shield-id'],
|
|
@@ -385,6 +389,8 @@ async function main() {
|
|
|
385
389
|
const buildGatewayArgs = () => ({
|
|
386
390
|
mcpCommand: flags['mcp-command'],
|
|
387
391
|
mcpArgs: flags['mcp-args'],
|
|
392
|
+
mcpUrl: flags['mcp-url'],
|
|
393
|
+
mcpHeaders: flags['mcp-headers'],
|
|
388
394
|
agentName: flags['agent-name'],
|
|
389
395
|
agentClient: flags['agent-client'],
|
|
390
396
|
developerName: flags['developer-name'],
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.13",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
|
|
26
26
|
"test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
|
|
27
27
|
"test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
|
|
28
|
+
"test:remote-mcp-gateway": "npm run build && node scripts/test-remote-mcp-gateway.js",
|
|
28
29
|
"test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
|
|
29
30
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
30
31
|
"prepublishOnly": "npm run build"
|