fullcourtdefense-cli 1.14.11 → 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.
- package/dist/commands/discover.js +12 -0
- package/dist/commands/discoverProxy.d.ts +1 -0
- package/dist/commands/discoverProxy.js +12 -0
- package/dist/commands/mcpGateway.d.ts +34 -0
- package/dist/commands/mcpGateway.js +393 -64
- package/dist/index.js +6 -0
- package/dist/version.json +1 -1
- package/package.json +3 -1
|
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
|
|
|
43
43
|
const config_1 = require("../config");
|
|
44
44
|
const discoverProxy_1 = require("./discoverProxy");
|
|
45
45
|
const discoverSchedule_1 = require("./discoverSchedule");
|
|
46
|
+
const mcpGateway_1 = require("./mcpGateway");
|
|
46
47
|
const discoverPaths_1 = require("./discoverPaths");
|
|
47
48
|
const knownMcpServers_1 = require("./knownMcpServers");
|
|
48
49
|
const discoverAgentFiles_1 = require("./discoverAgentFiles");
|
|
@@ -770,6 +771,17 @@ async function discoverCommand(args, config) {
|
|
|
770
771
|
let clientCoverage = [];
|
|
771
772
|
if (runMcp) {
|
|
772
773
|
const candidates = candidateConfigPaths(cwd, args.extraPath);
|
|
774
|
+
// Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
|
|
775
|
+
// generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
|
|
776
|
+
// every discovery run converges the fleet to the per-server identity contract.
|
|
777
|
+
const healedIdentities = (0, mcpGateway_1.healGatewayAgentIdentities)(candidates.map(c => c.path).filter(p => fs.existsSync(p)));
|
|
778
|
+
if (healedIdentities.length > 0 && !silent) {
|
|
779
|
+
console.log(`${COLOR.yellow}Healed ${healedIdentities.length} stale gateway identit${healedIdentities.length === 1 ? 'y' : 'ies'} (old CLI defaults):${COLOR.reset}`);
|
|
780
|
+
for (const heal of healedIdentities) {
|
|
781
|
+
console.log(` ${COLOR.dim}•${COLOR.reset} ${heal.server}: ${heal.from} → ${heal.to} (${heal.file})`);
|
|
782
|
+
}
|
|
783
|
+
console.log(`${COLOR.gray}Restart the affected AI clients to pick up the corrected identity.${COLOR.reset}\n`);
|
|
784
|
+
}
|
|
773
785
|
for (const c of candidates) {
|
|
774
786
|
scanned.push({ path: c.path, source: c.source });
|
|
775
787
|
if (fs.existsSync(c.path)) {
|
|
@@ -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>;
|
|
@@ -63,6 +81,22 @@ export interface ProtectAllArgs extends McpGatewayArgs {
|
|
|
63
81
|
config?: string;
|
|
64
82
|
clients?: string;
|
|
65
83
|
}
|
|
84
|
+
export interface GatewayIdentityHealResult {
|
|
85
|
+
file: string;
|
|
86
|
+
server: string;
|
|
87
|
+
from: string;
|
|
88
|
+
to: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* SELF-HEAL for the policy-identity contract. Scans MCP client config files (JSON and Codex
|
|
92
|
+
* TOML) for FCD-gateway-wrapped servers whose baked-in `--agent-name` is a stale generic
|
|
93
|
+
* `<developer>-<client>` default, and rewrites it to `<developer>-<server>` so dashboard
|
|
94
|
+
* policies targeting the MCP server (`agentName contains <server>`) match at runtime.
|
|
95
|
+
*
|
|
96
|
+
* Runs automatically from `discover` and `protect-all`, so a customer machine with configs
|
|
97
|
+
* written by an old CLI version converges to the correct identity without manual edits.
|
|
98
|
+
*/
|
|
99
|
+
export declare function healGatewayAgentIdentities(files: string[], dryRun?: boolean): GatewayIdentityHealResult[];
|
|
66
100
|
export declare function protectAllCommand(args: ProtectAllArgs, config: BotGuardConfig): Promise<void>;
|
|
67
101
|
export declare function unprotectAllCommand(args: ProtectAllArgs, config: BotGuardConfig): Promise<void>;
|
|
68
102
|
export declare function installClaudeCodeMcpGatewayCommand(args: InstallClaudeCodeMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -37,6 +37,7 @@ exports.ALL_GATEWAY_CLIENTS = void 0;
|
|
|
37
37
|
exports.mcpGatewayCommand = mcpGatewayCommand;
|
|
38
38
|
exports.installCursorMcpGatewayCommand = installCursorMcpGatewayCommand;
|
|
39
39
|
exports.installMcpGatewayCommand = installMcpGatewayCommand;
|
|
40
|
+
exports.healGatewayAgentIdentities = healGatewayAgentIdentities;
|
|
40
41
|
exports.protectAllCommand = protectAllCommand;
|
|
41
42
|
exports.unprotectAllCommand = unprotectAllCommand;
|
|
42
43
|
exports.installClaudeCodeMcpGatewayCommand = installClaudeCodeMcpGatewayCommand;
|
|
@@ -78,6 +79,18 @@ const CLIENT_LABELS = {
|
|
|
78
79
|
windsurf: 'Windsurf',
|
|
79
80
|
vscode: 'VS Code',
|
|
80
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
|
+
}
|
|
81
94
|
function parseArgsList(value) {
|
|
82
95
|
if (!value?.trim())
|
|
83
96
|
return [];
|
|
@@ -398,6 +411,127 @@ class StdioMcpClient {
|
|
|
398
411
|
this.pending.clear();
|
|
399
412
|
}
|
|
400
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
|
+
}
|
|
401
535
|
class AgentGuardApi {
|
|
402
536
|
config;
|
|
403
537
|
constructor(config) {
|
|
@@ -514,16 +648,14 @@ class AgentGuardApi {
|
|
|
514
648
|
}
|
|
515
649
|
class McpGatewayServer {
|
|
516
650
|
gatewayConfig;
|
|
517
|
-
|
|
518
|
-
downstreamArgs;
|
|
651
|
+
downstreamSpec;
|
|
519
652
|
downstream;
|
|
520
653
|
downstreamReady;
|
|
521
654
|
buffer = '';
|
|
522
655
|
api;
|
|
523
|
-
constructor(gatewayConfig,
|
|
656
|
+
constructor(gatewayConfig, downstreamSpec) {
|
|
524
657
|
this.gatewayConfig = gatewayConfig;
|
|
525
|
-
this.
|
|
526
|
-
this.downstreamArgs = downstreamArgs;
|
|
658
|
+
this.downstreamSpec = downstreamSpec;
|
|
527
659
|
this.api = new AgentGuardApi(gatewayConfig);
|
|
528
660
|
}
|
|
529
661
|
start() {
|
|
@@ -580,7 +712,9 @@ class McpGatewayServer {
|
|
|
580
712
|
}
|
|
581
713
|
async ensureDownstream() {
|
|
582
714
|
if (!this.downstreamReady) {
|
|
583
|
-
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);
|
|
584
718
|
this.downstreamReady = this.downstream.initialize();
|
|
585
719
|
}
|
|
586
720
|
await this.downstreamReady;
|
|
@@ -721,10 +855,17 @@ class McpGatewayServer {
|
|
|
721
855
|
}
|
|
722
856
|
async function mcpGatewayCommand(args, config) {
|
|
723
857
|
const gatewayConfig = resolveGatewayConfig(args, config);
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
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`);
|
|
728
869
|
server.start();
|
|
729
870
|
}
|
|
730
871
|
function cursorMcpPath(projectScope) {
|
|
@@ -750,12 +891,38 @@ function readJson(file) {
|
|
|
750
891
|
function gatewayScriptPath() {
|
|
751
892
|
return path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
|
|
752
893
|
}
|
|
753
|
-
|
|
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)];
|
|
754
922
|
const commandArgs = [
|
|
755
923
|
gatewayScriptPath(),
|
|
756
924
|
'mcp-gateway',
|
|
757
|
-
|
|
758
|
-
'--mcp-args', JSON.stringify(downstreamArgs),
|
|
925
|
+
...downstreamFlags,
|
|
759
926
|
'--agent-name', gatewayConfig.agentName,
|
|
760
927
|
'--agent-client', gatewayConfig.agentClient,
|
|
761
928
|
'--developer-name', gatewayConfig.developerName,
|
|
@@ -977,16 +1144,13 @@ async function installCursorMcpGatewayCommand(args, config) {
|
|
|
977
1144
|
const nodeExe = process.execPath;
|
|
978
1145
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
979
1146
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
983
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
984
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1147
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1148
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
985
1149
|
writeMcpServerConfig(file, serverName, nodeExe, commandArgs);
|
|
986
1150
|
console.log(`AgentGuard MCP Gateway installed for Cursor (${projectScope ? 'project' : 'global'}).`);
|
|
987
1151
|
console.log(`Config: ${file}`);
|
|
988
1152
|
console.log(`Server: ${serverName}`);
|
|
989
|
-
console.log(`Downstream: ${
|
|
1153
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
990
1154
|
console.log('Restart Cursor or reload MCP servers to use the protected tools.');
|
|
991
1155
|
}
|
|
992
1156
|
async function installMcpGatewayCommand(args, config) {
|
|
@@ -1091,9 +1255,21 @@ function wrappedEntryNeedsHeal(entry) {
|
|
|
1091
1255
|
const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
|
|
1092
1256
|
return args.includes('--shield-id') && !args.includes('--shield-key');
|
|
1093
1257
|
}
|
|
1094
|
-
/** Recover the original downstream command
|
|
1258
|
+
/** Recover the original downstream server (stdio command or remote URL) from a wrapped entry's args. */
|
|
1095
1259
|
function extractWrappedDownstream(entry) {
|
|
1096
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
|
+
}
|
|
1097
1273
|
const ci = args.indexOf('--mcp-command');
|
|
1098
1274
|
if (ci === -1 || ci + 1 >= args.length)
|
|
1099
1275
|
return null;
|
|
@@ -1108,7 +1284,7 @@ function extractWrappedDownstream(entry) {
|
|
|
1108
1284
|
}
|
|
1109
1285
|
catch { /* leave empty */ }
|
|
1110
1286
|
}
|
|
1111
|
-
return { command, args: downstreamArgs };
|
|
1287
|
+
return { kind: 'stdio', command, args: downstreamArgs };
|
|
1112
1288
|
}
|
|
1113
1289
|
/** All server maps inside a parsed JSON config, across every known client shape. */
|
|
1114
1290
|
function collectJsonServerMaps(json) {
|
|
@@ -1142,6 +1318,129 @@ function backupFile(file) {
|
|
|
1142
1318
|
function perServerAgentName(developerName, _agentClient, serverName) {
|
|
1143
1319
|
return `${safeIdentityPart(developerName)}-${safeIdentityPart(serverName)}`;
|
|
1144
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Generic `<developer>-<client>` identities that pre-1.14.11 per-server installs baked into
|
|
1323
|
+
* configs. These are the ONLY values self-heal is allowed to rewrite — a custom --agent-name
|
|
1324
|
+
* an admin chose on purpose is never touched.
|
|
1325
|
+
*/
|
|
1326
|
+
const GENERIC_CLIENT_AGENT_SUFFIXES = ['cursor', 'claude-code', 'claude-desktop', 'codex', 'gemini', 'gemini-cli', 'windsurf', 'vscode'];
|
|
1327
|
+
/**
|
|
1328
|
+
* Rewrite a wrapped entry's `--agent-name` to the per-server contract (`<developer>-<server>`)
|
|
1329
|
+
* when it still carries a known-buggy generic client default. Mutates `args` in place.
|
|
1330
|
+
*/
|
|
1331
|
+
function healAgentNameInArgs(args, serverName) {
|
|
1332
|
+
const nameIdx = args.indexOf('--agent-name');
|
|
1333
|
+
if (nameIdx === -1 || nameIdx + 1 >= args.length)
|
|
1334
|
+
return { changed: false };
|
|
1335
|
+
const current = String(args[nameIdx + 1]);
|
|
1336
|
+
const devIdx = args.indexOf('--developer-name');
|
|
1337
|
+
const developerName = devIdx !== -1 && args[devIdx + 1] ? String(args[devIdx + 1]) : machineScopedUser();
|
|
1338
|
+
const devPart = safeIdentityPart(developerName);
|
|
1339
|
+
const expected = `${devPart}-${safeIdentityPart(serverName)}`;
|
|
1340
|
+
if (current === expected)
|
|
1341
|
+
return { changed: false };
|
|
1342
|
+
const isGenericDefault = GENERIC_CLIENT_AGENT_SUFFIXES.some(suffix => current === `${devPart}-${suffix}`);
|
|
1343
|
+
if (!isGenericDefault)
|
|
1344
|
+
return { changed: false };
|
|
1345
|
+
args[nameIdx + 1] = expected;
|
|
1346
|
+
return { changed: true, from: current, to: expected };
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* SELF-HEAL for the policy-identity contract. Scans MCP client config files (JSON and Codex
|
|
1350
|
+
* TOML) for FCD-gateway-wrapped servers whose baked-in `--agent-name` is a stale generic
|
|
1351
|
+
* `<developer>-<client>` default, and rewrites it to `<developer>-<server>` so dashboard
|
|
1352
|
+
* policies targeting the MCP server (`agentName contains <server>`) match at runtime.
|
|
1353
|
+
*
|
|
1354
|
+
* Runs automatically from `discover` and `protect-all`, so a customer machine with configs
|
|
1355
|
+
* written by an old CLI version converges to the correct identity without manual edits.
|
|
1356
|
+
*/
|
|
1357
|
+
function healGatewayAgentIdentities(files, dryRun = false) {
|
|
1358
|
+
const healed = [];
|
|
1359
|
+
const seen = new Set();
|
|
1360
|
+
for (const file of files) {
|
|
1361
|
+
let resolvedKey;
|
|
1362
|
+
try {
|
|
1363
|
+
resolvedKey = path.resolve(file).toLowerCase();
|
|
1364
|
+
}
|
|
1365
|
+
catch {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (seen.has(resolvedKey))
|
|
1369
|
+
continue;
|
|
1370
|
+
seen.add(resolvedKey);
|
|
1371
|
+
if (/\.toml$/i.test(file)) {
|
|
1372
|
+
healed.push(...healCodexTomlAgentIdentities(file, dryRun));
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
let raw;
|
|
1376
|
+
try {
|
|
1377
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
let json;
|
|
1383
|
+
try {
|
|
1384
|
+
json = JSON.parse(raw);
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const maps = collectJsonServerMaps(json);
|
|
1390
|
+
let changed = false;
|
|
1391
|
+
for (const map of maps) {
|
|
1392
|
+
for (const [name, entry] of Object.entries(map)) {
|
|
1393
|
+
if (name === MANAGED_SERVER_NAME || !isGatewayWrappedEntry(entry))
|
|
1394
|
+
continue;
|
|
1395
|
+
const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
1396
|
+
const result = healAgentNameInArgs(args, name);
|
|
1397
|
+
if (result.changed) {
|
|
1398
|
+
entry.args = args;
|
|
1399
|
+
healed.push({ file, server: name, from: result.from, to: result.to });
|
|
1400
|
+
changed = true;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (changed && !dryRun) {
|
|
1405
|
+
backupFile(file);
|
|
1406
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return healed;
|
|
1410
|
+
}
|
|
1411
|
+
/** Codex config.toml variant of the identity self-heal (args are written as a JSON-style array). */
|
|
1412
|
+
function healCodexTomlAgentIdentities(file, dryRun) {
|
|
1413
|
+
let raw;
|
|
1414
|
+
try {
|
|
1415
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
1416
|
+
}
|
|
1417
|
+
catch {
|
|
1418
|
+
return [];
|
|
1419
|
+
}
|
|
1420
|
+
const healed = [];
|
|
1421
|
+
const updated = raw.replace(/\[mcp_servers\.("?)([^\]"]+)\1\]([\s\S]*?)(?=\n\[|$)/g, (section, _q, serverName, body) => {
|
|
1422
|
+
if (serverName === MANAGED_SERVER_NAME || !body.includes('mcp-gateway'))
|
|
1423
|
+
return section;
|
|
1424
|
+
const newBody = body.replace(/("--agent-name",\s*")([^"]+)(")/, (m, pre, current, post) => {
|
|
1425
|
+
const devMatch = body.match(/"--developer-name",\s*"([^"]+)"/);
|
|
1426
|
+
const devPart = safeIdentityPart(devMatch?.[1] || machineScopedUser());
|
|
1427
|
+
const expected = `${devPart}-${safeIdentityPart(serverName)}`;
|
|
1428
|
+
if (current === expected)
|
|
1429
|
+
return m;
|
|
1430
|
+
const isGenericDefault = GENERIC_CLIENT_AGENT_SUFFIXES.some(suffix => current === `${devPart}-${suffix}`);
|
|
1431
|
+
if (!isGenericDefault)
|
|
1432
|
+
return m;
|
|
1433
|
+
healed.push({ file, server: serverName, from: current, to: expected });
|
|
1434
|
+
return `${pre}${expected}${post}`;
|
|
1435
|
+
});
|
|
1436
|
+
return section.replace(body, newBody);
|
|
1437
|
+
});
|
|
1438
|
+
if (healed.length > 0 && !dryRun) {
|
|
1439
|
+
backupFile(file);
|
|
1440
|
+
fs.writeFileSync(file, updated, 'utf8');
|
|
1441
|
+
}
|
|
1442
|
+
return healed;
|
|
1443
|
+
}
|
|
1145
1444
|
/**
|
|
1146
1445
|
* Per-server installs (`install-*-mcp-gateway --server-name foo`) must report the SAME
|
|
1147
1446
|
* runtime-agnostic agent identity that protect-all/auto-protect produce (`<developer>-<server>`).
|
|
@@ -1190,7 +1489,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1190
1489
|
if (downstream) {
|
|
1191
1490
|
const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
|
|
1192
1491
|
entry.command = nodeExe;
|
|
1193
|
-
entry.args = buildGatewayCommandArgs(perServer, downstream
|
|
1492
|
+
entry.args = buildGatewayCommandArgs(perServer, downstream, INSTALL_GATEWAY_ARGS);
|
|
1194
1493
|
stats.healed.push(name);
|
|
1195
1494
|
changed = true;
|
|
1196
1495
|
continue;
|
|
@@ -1199,14 +1498,35 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1199
1498
|
stats.skippedManaged.push(name);
|
|
1200
1499
|
continue;
|
|
1201
1500
|
}
|
|
1501
|
+
const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
|
|
1202
1502
|
if (typeof entry.command !== 'string' || !entry.command) {
|
|
1203
|
-
|
|
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;
|
|
1204
1525
|
continue;
|
|
1205
1526
|
}
|
|
1206
1527
|
const downstreamCommand = entry.command;
|
|
1207
1528
|
const downstreamArgs = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
1208
|
-
const
|
|
1209
|
-
const wrappedArgs = buildGatewayCommandArgs(perServer, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1529
|
+
const wrappedArgs = buildGatewayCommandArgs(perServer, { kind: 'stdio', command: downstreamCommand, args: downstreamArgs }, INSTALL_GATEWAY_ARGS);
|
|
1210
1530
|
entry.command = nodeExe;
|
|
1211
1531
|
entry.args = wrappedArgs;
|
|
1212
1532
|
// entry.env and any other fields are preserved as-is.
|
|
@@ -1245,8 +1565,19 @@ function unwrapJsonConfigFile(file, dryRun) {
|
|
|
1245
1565
|
const downstream = extractWrappedDownstream(entry);
|
|
1246
1566
|
if (!downstream)
|
|
1247
1567
|
continue;
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
+
}
|
|
1250
1581
|
restored.push(name);
|
|
1251
1582
|
changed = true;
|
|
1252
1583
|
}
|
|
@@ -1357,7 +1688,7 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
1357
1688
|
continue;
|
|
1358
1689
|
}
|
|
1359
1690
|
const perServer = { ...gatewayConfig, agentClient: 'codex', agentName: perServerAgentName(gatewayConfig.developerName, 'codex', section.name) };
|
|
1360
|
-
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);
|
|
1361
1692
|
const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
|
|
1362
1693
|
lines[section.cmdLine] = `${indent}command = ${JSON.stringify(nodeExe)}`;
|
|
1363
1694
|
const argsLineText = `${indent}args = ${tomlStringArrayLiteral(wrappedArgs)}`;
|
|
@@ -1372,7 +1703,8 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
1372
1703
|
if (!wrappedAlready)
|
|
1373
1704
|
continue;
|
|
1374
1705
|
const downstream = extractWrappedDownstream({ args: section.args });
|
|
1375
|
-
|
|
1706
|
+
// Codex TOML wraps are always stdio (remote URLs never get wrapped into TOML).
|
|
1707
|
+
if (!downstream || downstream.kind !== 'stdio')
|
|
1376
1708
|
continue;
|
|
1377
1709
|
const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
|
|
1378
1710
|
lines[section.cmdLine] = `${indent}command = ${JSON.stringify(downstream.command)}`;
|
|
@@ -1430,6 +1762,15 @@ async function protectAllCommand(args, config) {
|
|
|
1430
1762
|
return;
|
|
1431
1763
|
}
|
|
1432
1764
|
console.log(`\x1b[2mAuto-searching every known MCP client on this machine — ${files.length} config file(s) found.\x1b[0m\n`);
|
|
1765
|
+
// Converge stale generic gateway identities (old CLI defaults) to the per-server
|
|
1766
|
+
// contract before wrapping, so policy matching works on already-wrapped entries too.
|
|
1767
|
+
const healedIdentities = healGatewayAgentIdentities(files.map(f => f.path), dryRun);
|
|
1768
|
+
if (healedIdentities.length > 0) {
|
|
1769
|
+
console.log(`\x1b[32m✓ ${dryRun ? 'would fix' : 'fixed'} ${healedIdentities.length} stale gateway identit${healedIdentities.length === 1 ? 'y' : 'ies'}:\x1b[0m`);
|
|
1770
|
+
for (const heal of healedIdentities)
|
|
1771
|
+
console.log(` \x1b[2m•\x1b[0m ${heal.server}: ${heal.from} → ${heal.to}`);
|
|
1772
|
+
console.log('');
|
|
1773
|
+
}
|
|
1433
1774
|
let totalWrapped = 0;
|
|
1434
1775
|
let totalHealed = 0;
|
|
1435
1776
|
let totalManaged = 0;
|
|
@@ -1523,17 +1864,20 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1523
1864
|
const nodeExe = process.execPath;
|
|
1524
1865
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1525
1866
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1526
|
-
const
|
|
1527
|
-
if (!downstreamCommand)
|
|
1528
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1529
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1867
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1530
1868
|
const includeShieldKey = false;
|
|
1531
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig,
|
|
1869
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, {
|
|
1532
1870
|
...INSTALL_GATEWAY_ARGS,
|
|
1533
1871
|
includeShieldKey,
|
|
1534
1872
|
});
|
|
1535
1873
|
const claudeArgs = ['mcp', 'add', '--scope', scope, '--transport', 'stdio', serverName, '--', nodeExe, ...commandArgs];
|
|
1536
|
-
|
|
1874
|
+
// On Windows `claude` is a .cmd shim, so spawnSync needs shell:true — but with shell:true
|
|
1875
|
+
// Node does NOT quote args. Unquoted args with spaces/JSON (e.g. --mcp-args '["C:\\..."]',
|
|
1876
|
+
// --user-objective 'Developer requested...') get word-split by cmd.exe and written mangled
|
|
1877
|
+
// into the config. Quote them ourselves before handing the line to the shell.
|
|
1878
|
+
const shellQuote = (value) => (/[\s"]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value);
|
|
1879
|
+
const spawnClaudeArgs = process.platform === 'win32' ? claudeArgs.map(shellQuote) : claudeArgs;
|
|
1880
|
+
let result = (0, child_process_1.spawnSync)('claude', spawnClaudeArgs, {
|
|
1537
1881
|
stdio: 'inherit',
|
|
1538
1882
|
shell: process.platform === 'win32',
|
|
1539
1883
|
});
|
|
@@ -1543,7 +1887,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1543
1887
|
shell: process.platform === 'win32',
|
|
1544
1888
|
});
|
|
1545
1889
|
if (removeResult.status === 0) {
|
|
1546
|
-
result = (0, child_process_1.spawnSync)('claude',
|
|
1890
|
+
result = (0, child_process_1.spawnSync)('claude', spawnClaudeArgs, {
|
|
1547
1891
|
stdio: 'inherit',
|
|
1548
1892
|
shell: process.platform === 'win32',
|
|
1549
1893
|
});
|
|
@@ -1552,7 +1896,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1552
1896
|
if (result.status === 0) {
|
|
1553
1897
|
console.log(`AgentGuard MCP Gateway installed for Claude Code (${scope}).`);
|
|
1554
1898
|
console.log(`Server: ${serverName}`);
|
|
1555
|
-
console.log(`Downstream: ${
|
|
1899
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
1556
1900
|
if (!includeShieldKey && gatewayConfig.shieldKey) {
|
|
1557
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.');
|
|
1558
1902
|
}
|
|
@@ -1565,7 +1909,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1565
1909
|
console.log('Claude Code CLI was not available, so project config was written directly.');
|
|
1566
1910
|
console.log(`Config: ${file}`);
|
|
1567
1911
|
console.log(`Server: ${serverName}`);
|
|
1568
|
-
console.log(`Downstream: ${
|
|
1912
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
1569
1913
|
if (!includeShieldKey && gatewayConfig.shieldKey) {
|
|
1570
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.');
|
|
1571
1915
|
}
|
|
@@ -1584,11 +1928,8 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
|
|
|
1584
1928
|
const nodeExe = process.execPath;
|
|
1585
1929
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1586
1930
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1590
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1591
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1931
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1932
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1592
1933
|
for (const target of targets) {
|
|
1593
1934
|
writeMcpServerConfig(target.file, serverName, nodeExe, commandArgs);
|
|
1594
1935
|
}
|
|
@@ -1597,7 +1938,7 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
|
|
|
1597
1938
|
console.log(`Config: ${target.file} (${target.source})`);
|
|
1598
1939
|
}
|
|
1599
1940
|
console.log(`Server: ${serverName}`);
|
|
1600
|
-
console.log(`Downstream: ${
|
|
1941
|
+
console.log(`Downstream: ${downstreamSpecLabel(downstream)}`);
|
|
1601
1942
|
console.log('Restart Claude Desktop to load the protected tools.');
|
|
1602
1943
|
}
|
|
1603
1944
|
async function installCodexMcpGatewayCommand(args, config) {
|
|
@@ -1611,11 +1952,8 @@ async function installCodexMcpGatewayCommand(args, config) {
|
|
|
1611
1952
|
const nodeExe = process.execPath;
|
|
1612
1953
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1613
1954
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1617
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1618
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1955
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1956
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1619
1957
|
writeCodexMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1620
1958
|
console.log(`AgentGuard MCP Gateway installed for Codex (${projectScope ? 'project' : 'global'}).`);
|
|
1621
1959
|
console.log(`Config: ${file}`);
|
|
@@ -1633,11 +1971,8 @@ async function installGeminiMcpGatewayCommand(args, config) {
|
|
|
1633
1971
|
const nodeExe = process.execPath;
|
|
1634
1972
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1635
1973
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1639
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1640
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1974
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1975
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1641
1976
|
writeJsonMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1642
1977
|
console.log(`AgentGuard MCP Gateway installed for Gemini CLI (${projectScope ? 'project' : 'user'}).`);
|
|
1643
1978
|
console.log(`Config: ${file}`);
|
|
@@ -1654,11 +1989,8 @@ async function installWindsurfMcpGatewayCommand(args, config) {
|
|
|
1654
1989
|
const nodeExe = process.execPath;
|
|
1655
1990
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1656
1991
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1660
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1661
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1992
|
+
const downstream = resolveDownstreamSpec(args);
|
|
1993
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1662
1994
|
writeJsonMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1663
1995
|
console.log('AgentGuard MCP Gateway installed for Windsurf.');
|
|
1664
1996
|
console.log(`Config: ${file}`);
|
|
@@ -1675,11 +2007,8 @@ async function installVscodeMcpGatewayCommand(args, config) {
|
|
|
1675
2007
|
const nodeExe = process.execPath;
|
|
1676
2008
|
const serverName = args.serverName || MANAGED_SERVER_NAME;
|
|
1677
2009
|
const gatewayConfig = applyPerServerAgentName(baseGatewayConfig, args, serverName);
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1681
|
-
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1682
|
-
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
2010
|
+
const downstream = resolveDownstreamSpec(args);
|
|
2011
|
+
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstream, INSTALL_GATEWAY_ARGS);
|
|
1683
2012
|
const targets = [];
|
|
1684
2013
|
if (projectScope)
|
|
1685
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": {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"test:agent-file-posture": "npm run build && node scripts/test-agent-file-posture.js",
|
|
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
|
+
"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",
|
|
27
29
|
"test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
|
|
28
30
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
29
31
|
"prepublishOnly": "npm run build"
|