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
- downstreamCommand;
519
- downstreamArgs;
651
+ downstreamSpec;
520
652
  downstream;
521
653
  downstreamReady;
522
654
  buffer = '';
523
655
  api;
524
- constructor(gatewayConfig, downstreamCommand, downstreamArgs) {
656
+ constructor(gatewayConfig, downstreamSpec) {
525
657
  this.gatewayConfig = gatewayConfig;
526
- this.downstreamCommand = downstreamCommand;
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 = new StdioMcpClient(this.downstreamCommand, this.downstreamArgs);
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND || '';
726
- const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
727
- const server = new McpGatewayServer(gatewayConfig, downstreamCommand, downstreamArgs);
728
- process.stderr.write(`AgentGuard MCP Gateway running for ${gatewayConfig.agentName}. Downstream: ${downstreamCommand} ${downstreamArgs.join(' ')}\n`);
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
- function buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, options = {}) {
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
- '--mcp-command', downstreamCommand,
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
982
- if (!downstreamCommand)
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: ${downstreamCommand} ${downstreamArgs.join(' ')}`);
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+args from a wrapped entry's args. */
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.command, downstream.args, INSTALL_GATEWAY_ARGS);
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
- stats.skippedRemote.push(name);
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 perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
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
- entry.command = downstream.command;
1373
- entry.args = downstream.args;
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
- if (!downstream)
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
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, downstreamCommand, downstreamArgs, {
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: ${downstreamCommand} ${downstreamArgs.join(' ')}`);
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: ${downstreamCommand} ${downstreamArgs.join(' ')}`);
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
1727
- if (!downstreamCommand)
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: ${downstreamCommand} ${downstreamArgs.join(' ')}`);
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
1754
- if (!downstreamCommand)
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
1776
- if (!downstreamCommand)
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
1797
- if (!downstreamCommand)
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 downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
1818
- if (!downstreamCommand)
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.12"
2
+ "version": "1.14.13"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.12",
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"