fullcourtdefense-cli 1.1.5 → 1.1.6

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.
@@ -48,18 +48,34 @@ function dbg(obj) {
48
48
  }
49
49
  function readStdin() {
50
50
  return new Promise((resolve) => {
51
- let data = '';
52
51
  const stdin = process.stdin;
53
52
  if (stdin.isTTY) {
54
53
  resolve('');
55
54
  return;
56
55
  }
57
- stdin.setEncoding('utf8');
58
- stdin.on('data', (chunk) => { data += chunk; });
59
- stdin.on('end', () => resolve(data));
60
- stdin.on('error', () => resolve(data));
56
+ // Read raw BYTES (no setEncoding) and decode the whole payload as UTF-8 once.
57
+ // Per-chunk string decoding + concat corrupts multibyte chars (e.g. Hebrew),
58
+ // turning them into Latin-1 mojibake before they ever reach the backend.
59
+ const chunks = [];
60
+ let settled = false;
61
+ const finish = () => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ let buf = Buffer.concat(chunks);
66
+ // Strip UTF-8 BOM at the byte level (Cursor on Windows prepends EF BB BF).
67
+ if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
68
+ buf = buf.subarray(3);
69
+ }
70
+ resolve(buf.toString('utf8'));
71
+ };
72
+ stdin.on('data', (chunk) => {
73
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
74
+ });
75
+ stdin.on('end', finish);
76
+ stdin.on('error', finish);
61
77
  // Safety: if nothing arrives, don't hang forever.
62
- setTimeout(() => resolve(data), 2000).unref?.();
78
+ setTimeout(finish, 2000).unref?.();
63
79
  });
64
80
  }
65
81
  /** Load ~/.fullcourtdefense.yml (machine-wide creds) as a fallback to project config. */
@@ -0,0 +1,21 @@
1
+ import { BotGuardConfig } from '../config';
2
+ export interface McpGatewayArgs {
3
+ mcpCommand?: string;
4
+ mcpArgs?: string;
5
+ agentName?: string;
6
+ shieldId?: string;
7
+ shieldKey?: string;
8
+ apiUrl?: string;
9
+ environment?: string;
10
+ approvalMode?: string;
11
+ approvalTimeoutMs?: string;
12
+ approvalPollMs?: string;
13
+ scanResponse?: string;
14
+ failClosed?: string;
15
+ }
16
+ export interface InstallCursorMcpGatewayArgs extends McpGatewayArgs {
17
+ project?: string;
18
+ serverName?: string;
19
+ }
20
+ export declare function mcpGatewayCommand(args: McpGatewayArgs, config: BotGuardConfig): Promise<void>;
21
+ export declare function installCursorMcpGatewayCommand(args: InstallCursorMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.mcpGatewayCommand = mcpGatewayCommand;
37
+ exports.installCursorMcpGatewayCommand = installCursorMcpGatewayCommand;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const child_process_1 = require("child_process");
42
+ const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
43
+ const MANAGED_SERVER_NAME = 'agentguard-gateway';
44
+ function parseArgsList(value) {
45
+ if (!value?.trim())
46
+ return [];
47
+ const trimmed = value.trim();
48
+ if (trimmed.startsWith('[')) {
49
+ const parsed = JSON.parse(trimmed);
50
+ if (!Array.isArray(parsed) || parsed.some(item => typeof item !== 'string')) {
51
+ throw new Error('MCP args JSON must be an array of strings.');
52
+ }
53
+ return parsed;
54
+ }
55
+ return trimmed.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(part => part.replace(/^"|"$/g, '')) ?? [];
56
+ }
57
+ function loadHomeShield() {
58
+ try {
59
+ for (const name of ['.fullcourtdefense.yml', '.fullcourtdefense.yaml']) {
60
+ const file = path.join(os.homedir(), name);
61
+ if (!fs.existsSync(file))
62
+ continue;
63
+ const text = fs.readFileSync(file, 'utf8');
64
+ const pick = (key) => {
65
+ const match = text.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)\\s*$`, 'm'));
66
+ return match?.[1]?.replace(/^["']|["']$/g, '').trim() || undefined;
67
+ };
68
+ return { shieldId: pick('shieldId'), shieldKey: pick('shieldKey'), apiUrl: pick('apiUrl') };
69
+ }
70
+ }
71
+ catch {
72
+ // Ignore config read errors; command-line flags/env can still provide config.
73
+ }
74
+ return {};
75
+ }
76
+ function resolveGatewayConfig(args, config) {
77
+ const home = loadHomeShield();
78
+ const shieldId = args.shieldId || process.env.FCD_SHIELD_ID || process.env.FULLCOURTDEFENSE_SHIELD_ID || process.env.AGENTGUARD_SHIELD_ID || config.shieldId || home.shieldId;
79
+ if (!shieldId)
80
+ throw new Error('Missing Shield ID. Pass --shield-id or run fullcourtdefense configure.');
81
+ return {
82
+ shieldId,
83
+ shieldKey: args.shieldKey || process.env.FCD_SHIELD_KEY || process.env.FULLCOURTDEFENSE_SHIELD_KEY || process.env.AGENTGUARD_SHIELD_KEY || config.shieldKey || home.shieldKey,
84
+ apiUrl: (args.apiUrl || process.env.FCD_API_URL || process.env.FULLCOURTDEFENSE_API_URL || config.apiUrl || home.apiUrl || DEFAULT_API_URL).replace(/\/$/, ''),
85
+ agentName: args.agentName || process.env.FCD_AGENT_NAME || 'cursor-local-agent',
86
+ environment: args.environment || process.env.FCD_ENVIRONMENT || 'developer-workstation',
87
+ approvalMode: args.approvalMode === 'block' ? 'block' : 'wait',
88
+ approvalTimeoutMs: Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 300000,
89
+ approvalPollMs: Math.max(1000, Number(args.approvalPollMs) > 0 ? Number(args.approvalPollMs) : 3000),
90
+ scanResponse: args.scanResponse === 'true',
91
+ failClosed: args.failClosed !== 'false',
92
+ };
93
+ }
94
+ function machineMetadata() {
95
+ return {
96
+ machineName: os.hostname(),
97
+ os: `${os.platform()} ${os.release()}`,
98
+ username: safeUserName(),
99
+ cwd: process.cwd(),
100
+ workspacePath: process.env.WORKSPACE_PATH || process.cwd(),
101
+ agentClient: 'cursor',
102
+ gateway: 'agentguard-mcp-gateway',
103
+ };
104
+ }
105
+ function safeUserName() {
106
+ try {
107
+ return os.userInfo().username;
108
+ }
109
+ catch {
110
+ return 'unknown';
111
+ }
112
+ }
113
+ function summarizeToolArgs(args) {
114
+ const out = { ...machineMetadata() };
115
+ for (const [key, value] of Object.entries(args || {})) {
116
+ if (/token|secret|password|credential|key/i.test(key)) {
117
+ out[key] = '[redacted]';
118
+ }
119
+ else if (typeof value === 'string') {
120
+ out[key] = value.length > 500 ? `${value.slice(0, 500)}...` : value;
121
+ }
122
+ else if (typeof value === 'number' || typeof value === 'boolean') {
123
+ out[key] = value;
124
+ }
125
+ else if (value === null || value === undefined) {
126
+ out[key] = value;
127
+ }
128
+ else {
129
+ out[key] = `[${Array.isArray(value) ? 'array' : 'object'}]`;
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ function resultSummary(result) {
135
+ const text = contentToText(result);
136
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text || 'Tool executed after policy check.';
137
+ }
138
+ function contentToText(result) {
139
+ if (typeof result === 'string')
140
+ return result;
141
+ const content = result?.content;
142
+ if (Array.isArray(content)) {
143
+ return content
144
+ .filter(item => item.type === 'text' && typeof item.text === 'string')
145
+ .map(item => item.text)
146
+ .join('\n\n');
147
+ }
148
+ try {
149
+ return JSON.stringify(result);
150
+ }
151
+ catch {
152
+ return String(result);
153
+ }
154
+ }
155
+ class StdioMcpClient {
156
+ child;
157
+ buffer = '';
158
+ nextId = 1;
159
+ pending = new Map();
160
+ constructor(command, args) {
161
+ if (!command)
162
+ throw new Error('Missing downstream MCP command. Pass --mcp-command.');
163
+ this.child = (0, child_process_1.spawn)(command, args, {
164
+ stdio: ['pipe', 'pipe', 'pipe'],
165
+ shell: process.platform === 'win32',
166
+ });
167
+ this.child.stdout.setEncoding('utf8');
168
+ this.child.stdout.on('data', chunk => this.onData(chunk));
169
+ this.child.stderr.on('data', chunk => process.stderr.write(String(chunk)));
170
+ this.child.on('error', error => this.rejectAll(error));
171
+ this.child.on('exit', code => this.rejectAll(new Error(`Downstream MCP server exited (${code ?? 'signal'}).`)));
172
+ }
173
+ async initialize() {
174
+ await this.request('initialize', {
175
+ protocolVersion: '2024-11-05',
176
+ capabilities: {},
177
+ clientInfo: { name: 'agentguard-mcp-gateway', version: '0.1.0' },
178
+ });
179
+ this.notify('notifications/initialized', {});
180
+ }
181
+ async listTools() {
182
+ const result = await this.request('tools/list', {});
183
+ const tools = result?.tools;
184
+ if (!Array.isArray(tools))
185
+ return [];
186
+ return tools
187
+ .filter(tool => typeof tool.name === 'string')
188
+ .map(tool => ({
189
+ name: String(tool.name),
190
+ description: typeof tool.description === 'string'
191
+ ? String(tool.description)
192
+ : undefined,
193
+ inputSchema: tool.inputSchema,
194
+ }));
195
+ }
196
+ async callTool(name, args) {
197
+ return this.request('tools/call', { name, arguments: args });
198
+ }
199
+ close() {
200
+ this.child.kill();
201
+ }
202
+ request(method, params) {
203
+ const id = this.nextId++;
204
+ this.write({ jsonrpc: '2.0', id, method, params });
205
+ return new Promise((resolve, reject) => {
206
+ const timeout = setTimeout(() => {
207
+ this.pending.delete(id);
208
+ reject(new Error(`MCP request timed out: ${method}`));
209
+ }, 60000);
210
+ this.pending.set(id, { resolve, reject, timeout });
211
+ });
212
+ }
213
+ notify(method, params) {
214
+ this.write({ jsonrpc: '2.0', method, params });
215
+ }
216
+ write(message) {
217
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
218
+ }
219
+ onData(chunk) {
220
+ this.buffer += chunk;
221
+ while (true) {
222
+ const lineEnd = this.buffer.indexOf('\n');
223
+ if (lineEnd === -1)
224
+ return;
225
+ const raw = this.buffer.slice(0, lineEnd).trim();
226
+ this.buffer = this.buffer.slice(lineEnd + 1);
227
+ if (!raw)
228
+ continue;
229
+ if (!raw.startsWith('{')) {
230
+ process.stderr.write(`[downstream] ${raw}\n`);
231
+ continue;
232
+ }
233
+ let message;
234
+ try {
235
+ message = JSON.parse(raw);
236
+ }
237
+ catch {
238
+ process.stderr.write(`[downstream] ${raw}\n`);
239
+ continue;
240
+ }
241
+ if (typeof message.id !== 'number')
242
+ continue;
243
+ const pending = this.pending.get(message.id);
244
+ if (!pending)
245
+ continue;
246
+ clearTimeout(pending.timeout);
247
+ this.pending.delete(message.id);
248
+ if (message.error)
249
+ pending.reject(new Error(message.error.message || 'MCP request failed'));
250
+ else
251
+ pending.resolve(message.result);
252
+ }
253
+ }
254
+ rejectAll(error) {
255
+ for (const pending of this.pending.values()) {
256
+ clearTimeout(pending.timeout);
257
+ pending.reject(error);
258
+ }
259
+ this.pending.clear();
260
+ }
261
+ }
262
+ class AgentGuardApi {
263
+ config;
264
+ constructor(config) {
265
+ this.config = config;
266
+ }
267
+ async checkToolCall(input) {
268
+ const result = await this.post('/api/agent-security/runtime/check-tool-call', {
269
+ shieldId: this.config.shieldId,
270
+ agentName: this.config.agentName,
271
+ toolName: input.toolName,
272
+ operation: input.operation,
273
+ toolArgs: input.toolArgs,
274
+ argsSummary: summarizeToolArgs(input.toolArgs),
275
+ environment: this.config.environment,
276
+ source: 'runtime_sdk',
277
+ });
278
+ if (!result.success || !result.data) {
279
+ throw new Error(result.error || 'Tool-call policy check failed.');
280
+ }
281
+ return result.data;
282
+ }
283
+ async recordToolCall(input) {
284
+ await this.post('/api/agent-security/runtime/tool-call', {
285
+ shieldId: this.config.shieldId,
286
+ agentName: this.config.agentName,
287
+ toolName: input.toolName,
288
+ operation: input.operation,
289
+ toolArgs: input.toolArgs,
290
+ argsSummary: summarizeToolArgs(input.toolArgs),
291
+ environment: this.config.environment,
292
+ decision: input.decision,
293
+ reason: input.reason,
294
+ source: 'runtime_sdk',
295
+ });
296
+ }
297
+ async waitForApproval(actionId) {
298
+ const deadline = Date.now() + this.config.approvalTimeoutMs;
299
+ while (Date.now() <= deadline) {
300
+ const status = await this.get(`/api/agent-security/runtime/approvals/${encodeURIComponent(actionId)}?shieldId=${encodeURIComponent(this.config.shieldId)}`);
301
+ const approval = status.data?.approval;
302
+ if (approval?.approvalStatus === 'approved') {
303
+ if (approval.executionStatus === 'executed')
304
+ throw new Error(`Approval ${actionId} was already executed.`);
305
+ return;
306
+ }
307
+ if (approval?.approvalStatus === 'rejected')
308
+ throw new Error(`Approval rejected for ${approval.toolName}.`);
309
+ if (approval?.approvalStatus === 'expired')
310
+ throw new Error(`Approval expired for ${approval.toolName}.`);
311
+ await new Promise(resolve => setTimeout(resolve, Math.min(this.config.approvalPollMs, Math.max(0, deadline - Date.now()))));
312
+ }
313
+ throw new Error(`Approval timed out. Request ${actionId} is still waiting for review.`);
314
+ }
315
+ async markApprovalExecuted(actionId, input) {
316
+ await this.post(`/api/agent-security/runtime/approvals/${encodeURIComponent(actionId)}/executed`, {
317
+ shieldId: this.config.shieldId,
318
+ ...input,
319
+ });
320
+ }
321
+ async scanToolResponse(input) {
322
+ const text = contentToText(input.result);
323
+ const response = await this.post(`/api/mcp/proxy/${encodeURIComponent(this.config.shieldId)}`, {
324
+ toolResponse: text,
325
+ toolName: input.toolName,
326
+ agentName: this.config.agentName,
327
+ operation: input.operation,
328
+ toolArgs: input.toolArgs,
329
+ });
330
+ if (!response.success && response.error)
331
+ throw new Error(response.error);
332
+ const data = response.data;
333
+ if (data?.blocked) {
334
+ throw new Error(data.reason || 'MCP tool response blocked by Shield.');
335
+ }
336
+ return data?.safeResponse
337
+ ? { content: [{ type: 'text', text: data.safeResponse }] }
338
+ : input.result;
339
+ }
340
+ async post(pathValue, body) {
341
+ const resp = await fetch(`${this.config.apiUrl}${pathValue}`, {
342
+ method: 'POST',
343
+ headers: this.headers(),
344
+ body: JSON.stringify(body),
345
+ signal: AbortSignal.timeout(120000),
346
+ });
347
+ const data = await resp.json().catch(() => ({}));
348
+ if (!resp.ok)
349
+ return { success: false, error: data.error || `AgentGuard API error (${resp.status})` };
350
+ return data;
351
+ }
352
+ async get(pathValue) {
353
+ const resp = await fetch(`${this.config.apiUrl}${pathValue}`, {
354
+ method: 'GET',
355
+ headers: this.headers(),
356
+ signal: AbortSignal.timeout(120000),
357
+ });
358
+ const data = await resp.json().catch(() => ({}));
359
+ if (!resp.ok)
360
+ return { success: false, error: data.error || `AgentGuard API error (${resp.status})` };
361
+ return data;
362
+ }
363
+ headers() {
364
+ return {
365
+ 'Content-Type': 'application/json',
366
+ ...(this.config.shieldKey ? { 'x-shield-key': this.config.shieldKey } : {}),
367
+ };
368
+ }
369
+ }
370
+ class McpGatewayServer {
371
+ gatewayConfig;
372
+ downstreamCommand;
373
+ downstreamArgs;
374
+ downstream;
375
+ downstreamReady;
376
+ buffer = '';
377
+ api;
378
+ constructor(gatewayConfig, downstreamCommand, downstreamArgs) {
379
+ this.gatewayConfig = gatewayConfig;
380
+ this.downstreamCommand = downstreamCommand;
381
+ this.downstreamArgs = downstreamArgs;
382
+ this.api = new AgentGuardApi(gatewayConfig);
383
+ }
384
+ start() {
385
+ process.stdin.setEncoding('utf8');
386
+ process.stdin.on('data', chunk => this.onData(String(chunk)));
387
+ process.stdin.on('end', () => this.downstream?.close());
388
+ }
389
+ onData(chunk) {
390
+ this.buffer += chunk;
391
+ while (true) {
392
+ const lineEnd = this.buffer.indexOf('\n');
393
+ if (lineEnd === -1)
394
+ return;
395
+ const raw = this.buffer.slice(0, lineEnd).trim();
396
+ this.buffer = this.buffer.slice(lineEnd + 1);
397
+ if (!raw)
398
+ continue;
399
+ void this.handleMessage(JSON.parse(raw));
400
+ }
401
+ }
402
+ async handleMessage(message) {
403
+ if (!message.id && message.method?.startsWith('notifications/'))
404
+ return;
405
+ try {
406
+ if (message.method === 'initialize') {
407
+ this.respond(message.id, {
408
+ protocolVersion: '2024-11-05',
409
+ capabilities: { tools: {} },
410
+ serverInfo: { name: 'agentguard-mcp-gateway', version: '0.1.0' },
411
+ });
412
+ return;
413
+ }
414
+ if (message.method === 'tools/list') {
415
+ await this.ensureDownstream();
416
+ const tools = await this.downstream.listTools();
417
+ this.respond(message.id, {
418
+ tools: tools.map(tool => ({
419
+ ...tool,
420
+ description: `[AgentGuard protected] ${tool.description || ''}`.trim(),
421
+ })),
422
+ });
423
+ return;
424
+ }
425
+ if (message.method === 'tools/call') {
426
+ const result = await this.handleToolCall(message.params || {});
427
+ this.respond(message.id, result);
428
+ return;
429
+ }
430
+ this.error(message.id, -32601, `Unsupported MCP method: ${message.method || 'unknown'}`);
431
+ }
432
+ catch (error) {
433
+ this.error(message.id, -32000, error instanceof Error ? error.message : String(error));
434
+ }
435
+ }
436
+ async ensureDownstream() {
437
+ if (!this.downstreamReady) {
438
+ this.downstream = new StdioMcpClient(this.downstreamCommand, this.downstreamArgs);
439
+ this.downstreamReady = this.downstream.initialize();
440
+ }
441
+ await this.downstreamReady;
442
+ }
443
+ async handleToolCall(params) {
444
+ await this.ensureDownstream();
445
+ const toolName = String(params.name || '');
446
+ if (!toolName)
447
+ throw new Error('Missing MCP tool name.');
448
+ const toolArgs = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
449
+ let approvalActionId;
450
+ let operation;
451
+ try {
452
+ const preflight = await this.api.checkToolCall({ toolName, toolArgs });
453
+ operation = preflight.operation;
454
+ if (!preflight.allowed) {
455
+ if (preflight.decision === 'approval' && this.gatewayConfig.approvalMode === 'wait') {
456
+ approvalActionId = preflight.approvalActionId;
457
+ if (!approvalActionId)
458
+ throw new Error('Policy requires approval, but no approval request id was returned.');
459
+ process.stderr.write(`AgentGuard approval required for ${toolName}. Waiting for human review (${approvalActionId})...\n`);
460
+ await this.api.waitForApproval(approvalActionId);
461
+ }
462
+ else {
463
+ const reason = preflight.actionPolicy?.reason || preflight.intentEvaluation?.reasons?.[0] || `Tool call ${preflight.decision}.`;
464
+ throw new Error(reason);
465
+ }
466
+ }
467
+ const rawResult = await this.downstream.callTool(toolName, toolArgs);
468
+ const finalResult = this.gatewayConfig.scanResponse
469
+ ? await this.api.scanToolResponse({ toolName, operation, toolArgs, result: rawResult })
470
+ : rawResult;
471
+ if (approvalActionId) {
472
+ await this.api.markApprovalExecuted(approvalActionId, { success: true, resultSummary: resultSummary(finalResult) });
473
+ }
474
+ else {
475
+ await this.api.recordToolCall({ toolName, operation, toolArgs, decision: 'allow' });
476
+ }
477
+ return finalResult;
478
+ }
479
+ catch (error) {
480
+ const message = error instanceof Error ? error.message : String(error);
481
+ if (approvalActionId) {
482
+ await this.api.markApprovalExecuted(approvalActionId, { success: false, error: message }).catch(() => undefined);
483
+ }
484
+ else {
485
+ await this.api.recordToolCall({ toolName, operation, toolArgs, decision: 'block', reason: message }).catch(() => undefined);
486
+ }
487
+ return {
488
+ isError: true,
489
+ content: [{ type: 'text', text: JSON.stringify({ blocked: true, toolName, reason: message }, null, 2) }],
490
+ };
491
+ }
492
+ }
493
+ respond(id, result) {
494
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
495
+ }
496
+ error(id, code, message) {
497
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
498
+ }
499
+ }
500
+ async function mcpGatewayCommand(args, config) {
501
+ const gatewayConfig = resolveGatewayConfig(args, config);
502
+ const downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND || '';
503
+ const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
504
+ const server = new McpGatewayServer(gatewayConfig, downstreamCommand, downstreamArgs);
505
+ process.stderr.write(`AgentGuard MCP Gateway running for ${gatewayConfig.agentName}. Downstream: ${downstreamCommand} ${downstreamArgs.join(' ')}\n`);
506
+ server.start();
507
+ }
508
+ function cursorMcpPath(projectScope) {
509
+ return projectScope
510
+ ? path.join(process.cwd(), '.cursor', 'mcp.json')
511
+ : path.join(os.homedir(), '.cursor', 'mcp.json');
512
+ }
513
+ function readJson(file) {
514
+ if (!fs.existsSync(file))
515
+ return { mcpServers: {} };
516
+ try {
517
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
518
+ if (!parsed || typeof parsed !== 'object')
519
+ return { mcpServers: {} };
520
+ if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object')
521
+ parsed.mcpServers = {};
522
+ return parsed;
523
+ }
524
+ catch {
525
+ return { mcpServers: {} };
526
+ }
527
+ }
528
+ async function installCursorMcpGatewayCommand(args, config) {
529
+ const gatewayConfig = resolveGatewayConfig(args, config);
530
+ const projectScope = args.project === 'true';
531
+ const file = cursorMcpPath(projectScope);
532
+ const nodeExe = process.execPath;
533
+ const scriptPath = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
534
+ const serverName = args.serverName || MANAGED_SERVER_NAME;
535
+ const downstreamCommand = args.mcpCommand || process.env.FCD_MCP_COMMAND;
536
+ if (!downstreamCommand)
537
+ throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
538
+ const commandArgs = [
539
+ scriptPath,
540
+ 'mcp-gateway',
541
+ '--mcp-command', downstreamCommand,
542
+ '--mcp-args', JSON.stringify(parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS)),
543
+ '--agent-name', gatewayConfig.agentName,
544
+ '--shield-id', gatewayConfig.shieldId,
545
+ '--approval-mode', gatewayConfig.approvalMode,
546
+ '--approval-timeout-ms', String(gatewayConfig.approvalTimeoutMs),
547
+ '--approval-poll-ms', String(gatewayConfig.approvalPollMs),
548
+ '--scan-response', String(gatewayConfig.scanResponse),
549
+ '--fail-closed', String(gatewayConfig.failClosed),
550
+ ];
551
+ if (gatewayConfig.shieldKey)
552
+ commandArgs.push('--shield-key', gatewayConfig.shieldKey);
553
+ if (gatewayConfig.apiUrl)
554
+ commandArgs.push('--api-url', gatewayConfig.apiUrl);
555
+ if (gatewayConfig.environment)
556
+ commandArgs.push('--environment', gatewayConfig.environment);
557
+ const json = readJson(file);
558
+ json.mcpServers[serverName] = {
559
+ command: nodeExe,
560
+ args: commandArgs,
561
+ };
562
+ fs.mkdirSync(path.dirname(file), { recursive: true });
563
+ fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
564
+ console.log(`AgentGuard MCP Gateway installed for Cursor (${projectScope ? 'project' : 'global'}).`);
565
+ console.log(`Config: ${file}`);
566
+ console.log(`Server: ${serverName}`);
567
+ console.log(`Downstream: ${downstreamCommand} ${parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS).join(' ')}`);
568
+ console.log('Restart Cursor or reload MCP servers to use the protected tools.');
569
+ }
package/dist/index.js CHANGED
@@ -10,7 +10,8 @@ const configure_1 = require("./commands/configure");
10
10
  const discover_1 = require("./commands/discover");
11
11
  const hook_1 = require("./commands/hook");
12
12
  const installCursorHook_1 = require("./commands/installCursorHook");
13
- const VERSION = '1.1.5';
13
+ const mcpGateway_1 = require("./commands/mcpGateway");
14
+ const VERSION = '1.1.6';
14
15
  function parseArgs(argv) {
15
16
  const flags = {};
16
17
  let command = '';
@@ -72,6 +73,11 @@ function printHelp() {
72
73
  (shell, MCP) on this machine is scanned by your Shield.
73
74
  uninstall-cursor-hook
74
75
  Removes the FullCourtDefense Cursor hook entries.
76
+ mcp-gateway
77
+ Runs a local MCP gateway that checks AgentGuard runtime/action
78
+ policies before forwarding tool calls to a real MCP server.
79
+ install-cursor-mcp-gateway
80
+ Adds the AgentGuard MCP Gateway to Cursor's mcp.json.
75
81
  hook Internal: invoked by Cursor per agent event (reads stdin JSON,
76
82
  returns an allow/deny verdict). Not run by hand.
77
83
  credits Shows hosted scan credits for CI/CD API-key scans.
@@ -183,6 +189,8 @@ function printHelp() {
183
189
  $ fullcourtdefense install-cursor-hook --events prompt,shell,mcp,file
184
190
  $ fullcourtdefense install-cursor-hook --project true # this repo only
185
191
  $ fullcourtdefense uninstall-cursor-hook
192
+ $ fullcourtdefense install-cursor-mcp-gateway --project true --shield-id <id> --shield-key <key> --mcp-command npm --mcp-args "run mcp"
193
+ $ fullcourtdefense mcp-gateway --shield-id <id> --mcp-command npm --mcp-args "run mcp"
186
194
  $ fullcourtdefense configure
187
195
  $ fullcourtdefense doctor
188
196
  $ fullcourtdefense scan --system-prompt ./prompts/system.md --fail-threshold 80
@@ -326,6 +334,44 @@ async function main() {
326
334
  await (0, hook_1.hookCommand)(args, config);
327
335
  break;
328
336
  }
337
+ case 'mcp-gateway': {
338
+ const args = {
339
+ mcpCommand: flags['mcp-command'],
340
+ mcpArgs: flags['mcp-args'],
341
+ agentName: flags['agent-name'],
342
+ shieldId: flags['shield-id'],
343
+ shieldKey: flags['shield-key'],
344
+ apiUrl: flags['api-url'],
345
+ environment: flags.environment,
346
+ approvalMode: flags['approval-mode'],
347
+ approvalTimeoutMs: flags['approval-timeout-ms'],
348
+ approvalPollMs: flags['approval-poll-ms'],
349
+ scanResponse: flags['scan-response'],
350
+ failClosed: flags['fail-closed'],
351
+ };
352
+ await (0, mcpGateway_1.mcpGatewayCommand)(args, config);
353
+ break;
354
+ }
355
+ case 'install-cursor-mcp-gateway': {
356
+ const args = {
357
+ project: flags.project,
358
+ serverName: flags['server-name'],
359
+ mcpCommand: flags['mcp-command'],
360
+ mcpArgs: flags['mcp-args'],
361
+ agentName: flags['agent-name'],
362
+ shieldId: flags['shield-id'],
363
+ shieldKey: flags['shield-key'],
364
+ apiUrl: flags['api-url'],
365
+ environment: flags.environment,
366
+ approvalMode: flags['approval-mode'],
367
+ approvalTimeoutMs: flags['approval-timeout-ms'],
368
+ approvalPollMs: flags['approval-poll-ms'],
369
+ scanResponse: flags['scan-response'],
370
+ failClosed: flags['fail-closed'],
371
+ };
372
+ await (0, mcpGateway_1.installCursorMcpGatewayCommand)(args, config);
373
+ break;
374
+ }
329
375
  case 'init': {
330
376
  await (0, init_1.initCommand)();
331
377
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {