fullcourtdefense-cli 1.14.12 → 1.14.14
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.d.ts +4 -0
- package/dist/commands/discover.js +34 -2
- package/dist/commands/discoverPaths.d.ts +17 -0
- package/dist/commands/discoverPaths.js +119 -0
- package/dist/commands/discoverProxy.d.ts +1 -0
- package/dist/commands/discoverProxy.js +12 -0
- package/dist/commands/mcpGateway.d.ts +18 -0
- package/dist/commands/mcpGateway.js +252 -62
- package/dist/index.js +10 -0
- package/dist/version.json +1 -1
- package/package.json +2 -1
|
@@ -12,6 +12,10 @@ export interface DiscoverArgs {
|
|
|
12
12
|
agentCiGate?: AgentCiGateUpload;
|
|
13
13
|
extraPath?: string;
|
|
14
14
|
deep?: string;
|
|
15
|
+
/** Optional comma-separated dev roots (or "auto") to sweep for project MCP configs in OTHER repos. */
|
|
16
|
+
scanRoot?: string;
|
|
17
|
+
/** Allow deep stdio probing (process spawn) for servers found via --scan-root. Default: off. */
|
|
18
|
+
deepRoots?: string;
|
|
15
19
|
silent?: string;
|
|
16
20
|
userEmail?: string;
|
|
17
21
|
schedule?: string;
|
|
@@ -482,10 +482,18 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
|
|
|
482
482
|
}
|
|
483
483
|
return [];
|
|
484
484
|
}
|
|
485
|
-
async function enrichWithDeepProbe(servers, silent) {
|
|
485
|
+
async function enrichWithDeepProbe(servers, silent, options = {}) {
|
|
486
486
|
for (const server of servers) {
|
|
487
487
|
if (server.transport !== 'http' && server.transport !== 'sse') {
|
|
488
488
|
if (server.transport === 'stdio' && server.command) {
|
|
489
|
+
// Safety: never auto-spawn stdio server processes discovered in OTHER
|
|
490
|
+
// projects via --scan-root; inventory them without live tools unless
|
|
491
|
+
// the user opts in with --deep-roots. (HTTP probing below is fine —
|
|
492
|
+
// it's a network call, not a process spawn.)
|
|
493
|
+
if (options.skipStdioForConfigPaths?.has(path.resolve(server.configPath).toLowerCase())) {
|
|
494
|
+
server.warnings.push('Deep stdio probe skipped for config outside the current project (--scan-root). Pass --deep-roots true to probe these too.');
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
489
497
|
const downstream = (0, discoverProxy_1.isFcdGatewayServer)(server) ? (0, discoverProxy_1.extractGatewayDownstream)(server) : null;
|
|
490
498
|
const command = downstream?.command || server.command;
|
|
491
499
|
const args = downstream?.args || server.args || [];
|
|
@@ -769,8 +777,30 @@ async function discoverCommand(args, config) {
|
|
|
769
777
|
const scanned = [];
|
|
770
778
|
let found = [];
|
|
771
779
|
let clientCoverage = [];
|
|
780
|
+
const scanRootConfigPaths = new Set();
|
|
772
781
|
if (runMcp) {
|
|
773
782
|
const candidates = candidateConfigPaths(cwd, args.extraPath);
|
|
783
|
+
// Optional --scan-root sweep: find project-scoped MCP configs in OTHER repos
|
|
784
|
+
// under the given dev roots (bounded walk — see scanRootsForProjectConfigs).
|
|
785
|
+
// Without the flag, behavior is exactly the known-locations scan.
|
|
786
|
+
const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
|
|
787
|
+
if (scanRoots.length > 0) {
|
|
788
|
+
const sweep = (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots);
|
|
789
|
+
const known = new Set(candidates.map(c => path.resolve(c.path).toLowerCase()));
|
|
790
|
+
let added = 0;
|
|
791
|
+
for (const candidate of sweep.candidates) {
|
|
792
|
+
const key = path.resolve(candidate.path).toLowerCase();
|
|
793
|
+
if (known.has(key))
|
|
794
|
+
continue;
|
|
795
|
+
known.add(key);
|
|
796
|
+
candidates.push(candidate);
|
|
797
|
+
scanRootConfigPaths.add(key);
|
|
798
|
+
added += 1;
|
|
799
|
+
}
|
|
800
|
+
if (!silent) {
|
|
801
|
+
console.log(`${COLOR.gray}Scan-root sweep: checked ${sweep.scannedDirs} folder(s) under ${scanRoots.length} root(s) — found ${added} additional project config(s).${COLOR.reset}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
774
804
|
// Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
|
|
775
805
|
// generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
|
|
776
806
|
// every discovery run converges the fleet to the per-server identity contract.
|
|
@@ -793,7 +823,9 @@ async function discoverCommand(args, config) {
|
|
|
793
823
|
if (deep && found.length > 0) {
|
|
794
824
|
if (!silent)
|
|
795
825
|
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE/stdio tools/list)…${COLOR.reset}`);
|
|
796
|
-
await enrichWithDeepProbe(found, silent
|
|
826
|
+
await enrichWithDeepProbe(found, silent, {
|
|
827
|
+
skipStdioForConfigPaths: args.deepRoots === 'true' ? undefined : scanRootConfigPaths,
|
|
828
|
+
});
|
|
797
829
|
}
|
|
798
830
|
}
|
|
799
831
|
const secrets = surfaces.has('secrets') || surfaces.has('posture')
|
|
@@ -14,5 +14,22 @@ export declare function claudeDesktopLikelyInstalled(): boolean;
|
|
|
14
14
|
export declare function claudeDesktopInstallTargets(configPath?: string): ClaudeDesktopConfigCandidate[];
|
|
15
15
|
/** All MCP client config locations discover / install should check. */
|
|
16
16
|
export declare function candidateConfigPaths(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
17
|
+
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
18
|
+
export declare function defaultScanRoots(): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
21
|
+
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
22
|
+
* this is a repo-root sweep, not a disk scan. Hidden/dependency/build folders
|
|
23
|
+
* are pruned; only the known project config filenames are checked.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanRootsForProjectConfigs(roots: string[], options?: {
|
|
26
|
+
maxDepth?: number;
|
|
27
|
+
maxDirs?: number;
|
|
28
|
+
}): {
|
|
29
|
+
candidates: ConfigPathCandidate[];
|
|
30
|
+
scannedDirs: number;
|
|
31
|
+
};
|
|
32
|
+
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
33
|
+
export declare function resolveScanRoots(flag: string | undefined): string[];
|
|
17
34
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
18
35
|
export declare function discoverScanTargets(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
@@ -38,6 +38,9 @@ exports.claudeDesktopConfigCandidates = claudeDesktopConfigCandidates;
|
|
|
38
38
|
exports.claudeDesktopLikelyInstalled = claudeDesktopLikelyInstalled;
|
|
39
39
|
exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
|
|
40
40
|
exports.candidateConfigPaths = candidateConfigPaths;
|
|
41
|
+
exports.defaultScanRoots = defaultScanRoots;
|
|
42
|
+
exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
|
|
43
|
+
exports.resolveScanRoots = resolveScanRoots;
|
|
41
44
|
exports.discoverScanTargets = discoverScanTargets;
|
|
42
45
|
const fs = __importStar(require("fs"));
|
|
43
46
|
const os = __importStar(require("os"));
|
|
@@ -176,6 +179,122 @@ function candidateConfigPaths(cwd, extra) {
|
|
|
176
179
|
return true;
|
|
177
180
|
});
|
|
178
181
|
}
|
|
182
|
+
/** Project-scoped MCP config files that can sit at any repo root. */
|
|
183
|
+
const PROJECT_CONFIG_FILES = [
|
|
184
|
+
{ rel: path.join('.cursor', 'mcp.json'), source: 'Cursor (project)', clientKey: 'cursor' },
|
|
185
|
+
{ rel: '.mcp.json', source: 'Claude Code (.mcp.json project)', clientKey: 'claude_code' },
|
|
186
|
+
{ rel: path.join('.claude', 'settings.json'), source: 'Claude Code (project)', clientKey: 'claude_code' },
|
|
187
|
+
{ rel: path.join('.vscode', 'mcp.json'), source: 'VS Code (project)', clientKey: 'vscode' },
|
|
188
|
+
{ rel: path.join('.codex', 'config.toml'), source: 'Codex (project)', clientKey: 'codex' },
|
|
189
|
+
{ rel: path.join('.gemini', 'settings.json'), source: 'Gemini CLI (project)', clientKey: 'gemini_cli' },
|
|
190
|
+
{ rel: 'mcp.json', source: 'Repo (mcp.json)', clientKey: 'other' },
|
|
191
|
+
];
|
|
192
|
+
/** Dependency/build/VCS folders that never contain project MCP configs. */
|
|
193
|
+
const SCAN_SKIP_DIRS = new Set([
|
|
194
|
+
'node_modules', 'dist', 'build', 'out', 'vendor', 'venv', 'env', '__pycache__',
|
|
195
|
+
'target', 'coverage', 'tmp', 'temp', 'obj', 'bin', 'packages', 'bower_components',
|
|
196
|
+
'site-packages', 'deps', '.terraform',
|
|
197
|
+
]);
|
|
198
|
+
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
199
|
+
function defaultScanRoots() {
|
|
200
|
+
const home = os.homedir();
|
|
201
|
+
const roots = [
|
|
202
|
+
path.join(home, 'repos'),
|
|
203
|
+
path.join(home, 'projects'),
|
|
204
|
+
path.join(home, 'code'),
|
|
205
|
+
path.join(home, 'dev'),
|
|
206
|
+
path.join(home, 'src'),
|
|
207
|
+
path.join(home, 'work'),
|
|
208
|
+
path.join(home, 'Documents', 'GitHub'),
|
|
209
|
+
path.join(home, 'Documents', 'repos'),
|
|
210
|
+
path.join(home, 'Documents', 'projects'),
|
|
211
|
+
];
|
|
212
|
+
if (process.platform === 'win32') {
|
|
213
|
+
const drive = (process.env.SystemDrive || 'C:') + path.sep;
|
|
214
|
+
for (const name of ['dev', 'src', 'repos', 'code', 'projects', 'work', 'git']) {
|
|
215
|
+
roots.push(path.join(drive, name));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return roots.filter(root => {
|
|
219
|
+
try {
|
|
220
|
+
return fs.statSync(root).isDirectory();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
229
|
+
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
230
|
+
* this is a repo-root sweep, not a disk scan. Hidden/dependency/build folders
|
|
231
|
+
* are pruned; only the known project config filenames are checked.
|
|
232
|
+
*/
|
|
233
|
+
function scanRootsForProjectConfigs(roots, options = {}) {
|
|
234
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
235
|
+
const maxDirs = options.maxDirs ?? 25000;
|
|
236
|
+
const candidates = [];
|
|
237
|
+
const visited = new Set();
|
|
238
|
+
let scannedDirs = 0;
|
|
239
|
+
const queue = [];
|
|
240
|
+
for (const root of roots) {
|
|
241
|
+
try {
|
|
242
|
+
const resolved = path.resolve(root);
|
|
243
|
+
if (fs.statSync(resolved).isDirectory())
|
|
244
|
+
queue.push({ dir: resolved, depth: 0 });
|
|
245
|
+
}
|
|
246
|
+
catch { /* missing root — skip */ }
|
|
247
|
+
}
|
|
248
|
+
while (queue.length > 0 && scannedDirs < maxDirs) {
|
|
249
|
+
const { dir, depth } = queue.shift();
|
|
250
|
+
const key = dir.toLowerCase();
|
|
251
|
+
if (visited.has(key))
|
|
252
|
+
continue;
|
|
253
|
+
visited.add(key);
|
|
254
|
+
scannedDirs += 1;
|
|
255
|
+
for (const config of PROJECT_CONFIG_FILES) {
|
|
256
|
+
const file = path.join(dir, config.rel);
|
|
257
|
+
try {
|
|
258
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
259
|
+
candidates.push({ path: file, source: config.source, clientKey: config.clientKey });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch { /* unreadable — skip */ }
|
|
263
|
+
}
|
|
264
|
+
if (depth >= maxDepth)
|
|
265
|
+
continue;
|
|
266
|
+
let entries;
|
|
267
|
+
try {
|
|
268
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
continue; // permission denied etc.
|
|
272
|
+
}
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
if (!entry.isDirectory())
|
|
275
|
+
continue;
|
|
276
|
+
const name = entry.name;
|
|
277
|
+
if (name.startsWith('.') || SCAN_SKIP_DIRS.has(name.toLowerCase()))
|
|
278
|
+
continue;
|
|
279
|
+
queue.push({ dir: path.join(dir, name), depth: depth + 1 });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { candidates, scannedDirs };
|
|
283
|
+
}
|
|
284
|
+
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
285
|
+
function resolveScanRoots(flag) {
|
|
286
|
+
if (!flag?.trim())
|
|
287
|
+
return [];
|
|
288
|
+
const parts = flag.split(',').map(part => part.trim()).filter(Boolean);
|
|
289
|
+
const roots = [];
|
|
290
|
+
for (const part of parts) {
|
|
291
|
+
if (part.toLowerCase() === 'auto')
|
|
292
|
+
roots.push(...defaultScanRoots());
|
|
293
|
+
else
|
|
294
|
+
roots.push(path.resolve(part));
|
|
295
|
+
}
|
|
296
|
+
return [...new Set(roots.map(root => root))];
|
|
297
|
+
}
|
|
179
298
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
180
299
|
function discoverScanTargets(cwd, extra) {
|
|
181
300
|
const candidates = candidateConfigPaths(cwd, extra);
|
|
@@ -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
|
@@ -327,6 +327,8 @@ function printHelp() {
|
|
|
327
327
|
$ fullcourtdefense discover --surface secrets
|
|
328
328
|
$ fullcourtdefense discover --surface agent-files
|
|
329
329
|
$ fullcourtdefense discover --deep --upload --silent --user-email you@company.com
|
|
330
|
+
$ fullcourtdefense discover --surface mcp --scan-root auto --upload
|
|
331
|
+
$ fullcourtdefense discover --surface mcp --scan-root "C:\dev,D:\work" --upload
|
|
330
332
|
$ fullcourtdefense discover --type mcp --json
|
|
331
333
|
$ fullcourtdefense install-cursor-hook
|
|
332
334
|
$ fullcourtdefense install-cursor-hook --shadow true # monitor only
|
|
@@ -342,7 +344,9 @@ function printHelp() {
|
|
|
342
344
|
$ fullcourtdefense install-claude-code-mcp-gateway --scope local --mcp-command npm --mcp-args "run mcp"
|
|
343
345
|
$ fullcourtdefense install-claude-code-mcp-gateway --scope project --mcp-command npm --mcp-args "run mcp"
|
|
344
346
|
$ fullcourtdefense install-claude-desktop-mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
347
|
+
$ fullcourtdefense install-cursor-mcp-gateway --server-name my-remote --mcp-url https://mcp.company.com/mcp
|
|
345
348
|
$ fullcourtdefense mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
349
|
+
$ fullcourtdefense mcp-gateway --mcp-url https://mcp.company.com/mcp --mcp-headers "{\"Authorization\":\"Bearer TOKEN\"}"
|
|
346
350
|
$ fullcourtdefense login --token <fleet-enrollment-token>
|
|
347
351
|
$ fullcourtdefense install-all
|
|
348
352
|
$ fullcourtdefense install-all --auto-protect true --upload
|
|
@@ -366,6 +370,8 @@ async function main() {
|
|
|
366
370
|
serverName: flags['server-name'],
|
|
367
371
|
mcpCommand: flags['mcp-command'],
|
|
368
372
|
mcpArgs: flags['mcp-args'],
|
|
373
|
+
mcpUrl: flags['mcp-url'],
|
|
374
|
+
mcpHeaders: flags['mcp-headers'],
|
|
369
375
|
agentName: flags['agent-name'],
|
|
370
376
|
developerName: flags['developer-name'],
|
|
371
377
|
shieldId: flags['shield-id'],
|
|
@@ -385,6 +391,8 @@ async function main() {
|
|
|
385
391
|
const buildGatewayArgs = () => ({
|
|
386
392
|
mcpCommand: flags['mcp-command'],
|
|
387
393
|
mcpArgs: flags['mcp-args'],
|
|
394
|
+
mcpUrl: flags['mcp-url'],
|
|
395
|
+
mcpHeaders: flags['mcp-headers'],
|
|
388
396
|
agentName: flags['agent-name'],
|
|
389
397
|
agentClient: flags['agent-client'],
|
|
390
398
|
developerName: flags['developer-name'],
|
|
@@ -520,6 +528,8 @@ async function main() {
|
|
|
520
528
|
connectorName: flags['connector-name'],
|
|
521
529
|
extraPath: flags.path,
|
|
522
530
|
deep: flags.deep,
|
|
531
|
+
scanRoot: flags['scan-root'],
|
|
532
|
+
deepRoots: flags['deep-roots'],
|
|
523
533
|
silent: flags.silent,
|
|
524
534
|
userEmail: flags['user-email'],
|
|
525
535
|
schedule: flags.schedule,
|
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.14",
|
|
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"
|