fullcourtdefense-cli 1.4.0 → 1.5.1

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.
@@ -58,6 +58,12 @@ export interface InstallMcpGatewayArgs extends McpGatewayArgs {
58
58
  export declare function mcpGatewayCommand(args: McpGatewayArgs, config: BotGuardConfig): Promise<void>;
59
59
  export declare function installCursorMcpGatewayCommand(args: InstallCursorMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
60
60
  export declare function installMcpGatewayCommand(args: InstallMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
61
+ export interface ProtectAllArgs extends McpGatewayArgs {
62
+ dryRun?: string;
63
+ config?: string;
64
+ }
65
+ export declare function protectAllCommand(args: ProtectAllArgs, config: BotGuardConfig): Promise<void>;
66
+ export declare function unprotectAllCommand(args: ProtectAllArgs, config: BotGuardConfig): Promise<void>;
61
67
  export declare function installClaudeCodeMcpGatewayCommand(args: InstallClaudeCodeMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
62
68
  export declare function installClaudeDesktopMcpGatewayCommand(args: InstallClaudeDesktopMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
63
69
  export declare function installCodexMcpGatewayCommand(args: InstallCodexMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
@@ -37,6 +37,8 @@ exports.ALL_GATEWAY_CLIENTS = void 0;
37
37
  exports.mcpGatewayCommand = mcpGatewayCommand;
38
38
  exports.installCursorMcpGatewayCommand = installCursorMcpGatewayCommand;
39
39
  exports.installMcpGatewayCommand = installMcpGatewayCommand;
40
+ exports.protectAllCommand = protectAllCommand;
41
+ exports.unprotectAllCommand = unprotectAllCommand;
40
42
  exports.installClaudeCodeMcpGatewayCommand = installClaudeCodeMcpGatewayCommand;
41
43
  exports.installClaudeDesktopMcpGatewayCommand = installClaudeDesktopMcpGatewayCommand;
42
44
  exports.installCodexMcpGatewayCommand = installCodexMcpGatewayCommand;
@@ -940,6 +942,377 @@ async function installMcpGatewayCommand(args, config) {
940
942
  console.log('Add --upload to refresh AI Fleet after install.');
941
943
  }
942
944
  }
945
+ const CLIENT_KEY_TO_AGENT = {
946
+ cursor: 'cursor',
947
+ claude_code: 'claude-code',
948
+ claude_desktop: 'claude-desktop',
949
+ codex: 'codex',
950
+ gemini_cli: 'gemini-cli',
951
+ windsurf: 'windsurf',
952
+ vscode: 'vscode',
953
+ };
954
+ function newWrapStats() { return { wrapped: [], skippedManaged: [], skippedRemote: [] }; }
955
+ /** True when an MCP server entry already runs through the AgentGuard gateway. */
956
+ function isGatewayWrappedEntry(entry) {
957
+ if (!entry || typeof entry !== 'object')
958
+ return false;
959
+ const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
960
+ return args.includes('mcp-gateway');
961
+ }
962
+ /** Recover the original downstream command+args from a wrapped entry's args. */
963
+ function extractWrappedDownstream(entry) {
964
+ const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
965
+ const ci = args.indexOf('--mcp-command');
966
+ if (ci === -1 || ci + 1 >= args.length)
967
+ return null;
968
+ const command = args[ci + 1];
969
+ let downstreamArgs = [];
970
+ const ai = args.indexOf('--mcp-args');
971
+ if (ai !== -1 && args[ai + 1]) {
972
+ try {
973
+ const parsed = JSON.parse(args[ai + 1]);
974
+ if (Array.isArray(parsed))
975
+ downstreamArgs = parsed.map(String);
976
+ }
977
+ catch { /* leave empty */ }
978
+ }
979
+ return { command, args: downstreamArgs };
980
+ }
981
+ /** All server maps inside a parsed JSON config, across every known client shape. */
982
+ function collectJsonServerMaps(json) {
983
+ const maps = [];
984
+ const pushIfMap = (m) => { if (m && typeof m === 'object' && !Array.isArray(m))
985
+ maps.push(m); };
986
+ if (json && typeof json === 'object') {
987
+ pushIfMap(json.mcpServers); // Cursor, Claude, Gemini, Windsurf, Kiro, Codex mcp.json
988
+ pushIfMap(json.servers); // VS Code .vscode/mcp.json
989
+ if (json.mcp && typeof json.mcp === 'object')
990
+ pushIfMap(json.mcp.servers); // VS Code settings.json
991
+ if (json.projects && typeof json.projects === 'object') {
992
+ for (const proj of Object.values(json.projects))
993
+ pushIfMap(proj?.mcpServers); // Claude Code .claude.json
994
+ }
995
+ }
996
+ return maps;
997
+ }
998
+ function backupFile(file) {
999
+ try {
1000
+ const bak = `${file}.fcd-backup-${Date.now()}`;
1001
+ fs.copyFileSync(file, bak);
1002
+ }
1003
+ catch { /* best effort */ }
1004
+ }
1005
+ function perServerAgentName(developerName, agentClient, serverName) {
1006
+ return `${safeIdentityPart(developerName)}-${agentClient}-${safeIdentityPart(serverName)}`;
1007
+ }
1008
+ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
1009
+ const stats = newWrapStats();
1010
+ let json;
1011
+ try {
1012
+ json = JSON.parse(fs.readFileSync(file, 'utf8'));
1013
+ }
1014
+ catch {
1015
+ return null; // not JSON / unreadable — skip silently
1016
+ }
1017
+ if (!json || typeof json !== 'object')
1018
+ return null;
1019
+ const maps = collectJsonServerMaps(json);
1020
+ if (maps.length === 0)
1021
+ return stats;
1022
+ const nodeExe = process.execPath;
1023
+ let changed = false;
1024
+ for (const map of maps) {
1025
+ for (const [name, entry] of Object.entries(map)) {
1026
+ if (!entry || typeof entry !== 'object')
1027
+ continue;
1028
+ if (name === MANAGED_SERVER_NAME || isGatewayWrappedEntry(entry)) {
1029
+ stats.skippedManaged.push(name);
1030
+ continue;
1031
+ }
1032
+ if (typeof entry.command !== 'string' || !entry.command) {
1033
+ stats.skippedRemote.push(name);
1034
+ continue;
1035
+ }
1036
+ const downstreamCommand = entry.command;
1037
+ const downstreamArgs = Array.isArray(entry.args) ? entry.args.map(String) : [];
1038
+ const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
1039
+ const wrappedArgs = buildGatewayCommandArgs(perServer, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
1040
+ entry.command = nodeExe;
1041
+ entry.args = wrappedArgs;
1042
+ // entry.env and any other fields are preserved as-is.
1043
+ stats.wrapped.push(name);
1044
+ changed = true;
1045
+ }
1046
+ }
1047
+ if (changed && !dryRun) {
1048
+ backupFile(file);
1049
+ fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
1050
+ }
1051
+ return stats;
1052
+ }
1053
+ function unwrapJsonConfigFile(file, dryRun) {
1054
+ let json;
1055
+ try {
1056
+ json = JSON.parse(fs.readFileSync(file, 'utf8'));
1057
+ }
1058
+ catch {
1059
+ return null;
1060
+ }
1061
+ if (!json || typeof json !== 'object')
1062
+ return null;
1063
+ const maps = collectJsonServerMaps(json);
1064
+ const restored = [];
1065
+ let changed = false;
1066
+ for (const map of maps) {
1067
+ for (const [name, entry] of Object.entries(map)) {
1068
+ if (name === MANAGED_SERVER_NAME) {
1069
+ delete map[name];
1070
+ changed = true;
1071
+ continue;
1072
+ }
1073
+ if (!isGatewayWrappedEntry(entry))
1074
+ continue;
1075
+ const downstream = extractWrappedDownstream(entry);
1076
+ if (!downstream)
1077
+ continue;
1078
+ entry.command = downstream.command;
1079
+ entry.args = downstream.args;
1080
+ restored.push(name);
1081
+ changed = true;
1082
+ }
1083
+ }
1084
+ if (changed && !dryRun) {
1085
+ backupFile(file);
1086
+ fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
1087
+ }
1088
+ return { restored };
1089
+ }
1090
+ /** Parse a single-line TOML scalar string value: command = "x" or command = 'x'. */
1091
+ function parseTomlInlineString(line) {
1092
+ const rhs = line.slice(line.indexOf('=') + 1).trim();
1093
+ const basic = rhs.match(/^"((?:[^"\\]|\\.)*)"/);
1094
+ if (basic) {
1095
+ try {
1096
+ return JSON.parse(`"${basic[1]}"`);
1097
+ }
1098
+ catch {
1099
+ return basic[1];
1100
+ }
1101
+ }
1102
+ const literal = rhs.match(/^'([^']*)'/); // TOML literal string — no escapes
1103
+ if (literal)
1104
+ return literal[1];
1105
+ return undefined;
1106
+ }
1107
+ /** Parse a single-line TOML string array: args = ["a", 'b'] (mixed quotes ok). */
1108
+ function parseTomlInlineStringArray(line) {
1109
+ const rhs = line.slice(line.indexOf('=') + 1).trim();
1110
+ if (!rhs.startsWith('[') || !rhs.endsWith(']'))
1111
+ return undefined; // multi-line / not an inline array
1112
+ const inner = rhs.slice(1, -1).trim();
1113
+ if (inner === '')
1114
+ return [];
1115
+ const out = [];
1116
+ const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'/g;
1117
+ let match;
1118
+ while ((match = re.exec(inner)) !== null) {
1119
+ if (match[1] !== undefined) {
1120
+ try {
1121
+ out.push(JSON.parse(`"${match[1]}"`));
1122
+ }
1123
+ catch {
1124
+ out.push(match[1]);
1125
+ }
1126
+ }
1127
+ else
1128
+ out.push(match[2] ?? '');
1129
+ }
1130
+ return out;
1131
+ }
1132
+ function tomlStringArrayLiteral(values) {
1133
+ return `[${values.map(v => JSON.stringify(v)).join(', ')}]`;
1134
+ }
1135
+ /**
1136
+ * Wrap/unwrap Codex config.toml in place, line by line, so [mcp_servers.NAME.env]
1137
+ * subtables and unrelated config are preserved. Only single-line command/args
1138
+ * are handled; multi-line arrays are skipped (rare for MCP servers).
1139
+ */
1140
+ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
1141
+ let content;
1142
+ try {
1143
+ content = fs.readFileSync(file, 'utf8');
1144
+ }
1145
+ catch {
1146
+ return null;
1147
+ }
1148
+ const lines = content.split(/\r?\n/);
1149
+ const stats = newWrapStats();
1150
+ const nodeExe = process.execPath;
1151
+ const sections = [];
1152
+ let current = null;
1153
+ for (let i = 0; i < lines.length; i++) {
1154
+ const trimmed = lines[i].trim();
1155
+ const header = trimmed.match(/^\[(.+?)\]\s*$/);
1156
+ if (header) {
1157
+ const tablePath = header[1];
1158
+ const isServer = /^mcp_servers\.[^.]+$/.test(tablePath);
1159
+ if (isServer) {
1160
+ current = { name: tablePath.slice('mcp_servers.'.length), cmdLine: -1, argsLine: -1 };
1161
+ sections.push(current);
1162
+ }
1163
+ else
1164
+ current = null;
1165
+ continue;
1166
+ }
1167
+ if (current) {
1168
+ if (/^command\s*=/.test(trimmed) && current.cmdLine === -1) {
1169
+ current.cmdLine = i;
1170
+ current.command = parseTomlInlineString(lines[i]);
1171
+ }
1172
+ else if (/^args\s*=/.test(trimmed) && current.argsLine === -1) {
1173
+ current.argsLine = i;
1174
+ current.args = parseTomlInlineStringArray(lines[i]);
1175
+ }
1176
+ }
1177
+ }
1178
+ let changed = false;
1179
+ for (const section of sections) {
1180
+ if (section.cmdLine === -1 || !section.command) {
1181
+ continue;
1182
+ }
1183
+ const wrappedAlready = section.command === nodeExe && (section.args || []).includes('mcp-gateway');
1184
+ if (mode === 'wrap') {
1185
+ if (section.name === MANAGED_SERVER_NAME || wrappedAlready) {
1186
+ stats.skippedManaged.push(section.name);
1187
+ continue;
1188
+ }
1189
+ const perServer = { ...gatewayConfig, agentClient: 'codex', agentName: perServerAgentName(gatewayConfig.developerName, 'codex', section.name) };
1190
+ const wrappedArgs = buildGatewayCommandArgs(perServer, section.command, section.args || [], INSTALL_GATEWAY_ARGS);
1191
+ const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
1192
+ lines[section.cmdLine] = `${indent}command = ${JSON.stringify(nodeExe)}`;
1193
+ const argsLineText = `${indent}args = ${tomlStringArrayLiteral(wrappedArgs)}`;
1194
+ if (section.argsLine !== -1)
1195
+ lines[section.argsLine] = argsLineText;
1196
+ else
1197
+ lines.splice(section.cmdLine + 1, 0, argsLineText);
1198
+ stats.wrapped.push(section.name);
1199
+ changed = true;
1200
+ }
1201
+ else {
1202
+ if (!wrappedAlready)
1203
+ continue;
1204
+ const downstream = extractWrappedDownstream({ args: section.args });
1205
+ if (!downstream)
1206
+ continue;
1207
+ const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
1208
+ lines[section.cmdLine] = `${indent}command = ${JSON.stringify(downstream.command)}`;
1209
+ if (section.argsLine !== -1)
1210
+ lines[section.argsLine] = `${indent}args = ${tomlStringArrayLiteral(downstream.args)}`;
1211
+ stats.wrapped.push(section.name);
1212
+ changed = true;
1213
+ }
1214
+ }
1215
+ if (changed && !dryRun) {
1216
+ backupFile(file);
1217
+ fs.writeFileSync(file, lines.join('\n'), 'utf8');
1218
+ }
1219
+ return stats;
1220
+ }
1221
+ function protectAllTargetFiles(extra) {
1222
+ const cwd = process.cwd();
1223
+ const seen = new Set();
1224
+ const files = [];
1225
+ for (const candidate of (0, discoverPaths_1.candidateConfigPaths)(cwd, extra)) {
1226
+ const key = path.resolve(candidate.path).toLowerCase();
1227
+ if (seen.has(key))
1228
+ continue;
1229
+ if (!fs.existsSync(candidate.path))
1230
+ continue;
1231
+ seen.add(key);
1232
+ files.push(candidate);
1233
+ }
1234
+ for (const target of (0, discoverPaths_1.claudeDesktopInstallTargets)()) {
1235
+ const key = path.resolve(target.file).toLowerCase();
1236
+ if (target.exists && !seen.has(key)) {
1237
+ seen.add(key);
1238
+ files.push({ path: target.file, source: target.source, clientKey: 'claude_desktop' });
1239
+ }
1240
+ }
1241
+ return files;
1242
+ }
1243
+ async function protectAllCommand(args, config) {
1244
+ (0, config_1.requireCliSetup)(config, { shieldId: args.shieldId, shieldKey: args.shieldKey, apiUrl: args.apiUrl }, { requireOrganizationId: false });
1245
+ const gatewayConfig = resolveGatewayConfig(args, config);
1246
+ const dryRun = args.dryRun === 'true';
1247
+ const files = protectAllTargetFiles(args.config);
1248
+ console.log('\n\x1b[1m\x1b[36mFullCourtDefense — protect every MCP server in every client\x1b[0m');
1249
+ console.log(`\x1b[2mRuntime identity: ${gatewayConfig.developerName} | Shield: ${gatewayConfig.shieldId}${dryRun ? ' | DRY RUN' : ''}\x1b[0m\n`);
1250
+ if (files.length === 0) {
1251
+ console.log('No MCP client config files found on this machine. Configure an MCP server in any client, then re-run.');
1252
+ return;
1253
+ }
1254
+ let totalWrapped = 0;
1255
+ let totalManaged = 0;
1256
+ let totalRemote = 0;
1257
+ for (const file of files) {
1258
+ const agentClient = CLIENT_KEY_TO_AGENT[file.clientKey] || 'cursor';
1259
+ const isToml = /\.toml$/i.test(file.path);
1260
+ const stats = isToml
1261
+ ? transformCodexToml(file.path, 'wrap', gatewayConfig, dryRun)
1262
+ : wrapJsonConfigFile(file.path, gatewayConfig, agentClient, dryRun);
1263
+ if (!stats)
1264
+ continue;
1265
+ if (stats.wrapped.length === 0 && stats.skippedManaged.length === 0 && stats.skippedRemote.length === 0)
1266
+ continue;
1267
+ totalWrapped += stats.wrapped.length;
1268
+ totalManaged += stats.skippedManaged.length;
1269
+ totalRemote += stats.skippedRemote.length;
1270
+ console.log(`\x1b[1m${file.source}\x1b[0m \x1b[2m${file.path}\x1b[0m`);
1271
+ if (stats.wrapped.length)
1272
+ console.log(` \x1b[32m✓ wrapped:\x1b[0m ${stats.wrapped.join(', ')}`);
1273
+ if (stats.skippedManaged.length)
1274
+ console.log(` \x1b[2m• already protected:\x1b[0m ${stats.skippedManaged.join(', ')}`);
1275
+ if (stats.skippedRemote.length)
1276
+ console.log(` \x1b[33m• skipped (remote/no command):\x1b[0m ${stats.skippedRemote.join(', ')}`);
1277
+ console.log('');
1278
+ }
1279
+ console.log('\x1b[1mSummary\x1b[0m');
1280
+ console.log(` ${dryRun ? 'would wrap' : 'wrapped'}: ${totalWrapped} already protected: ${totalManaged} skipped remote: ${totalRemote}`);
1281
+ console.log(` Each protected server enforces your org Action Policies (allow / block / wait-for-human-approval) on its tool calls.`);
1282
+ if (!dryRun && totalWrapped > 0)
1283
+ console.log(' Backups saved next to each edited file (*.fcd-backup-*). Restart each client to load the protected servers.');
1284
+ console.log(' Reverse anytime with: \x1b[1mfullcourtdefense unprotect-all\x1b[0m');
1285
+ }
1286
+ async function unprotectAllCommand(args, config) {
1287
+ const dryRun = args.dryRun === 'true';
1288
+ const files = protectAllTargetFiles(args.config);
1289
+ let gatewayConfig;
1290
+ try {
1291
+ gatewayConfig = resolveGatewayConfig(args, config);
1292
+ }
1293
+ catch { /* unwrap doesn't need creds */ }
1294
+ const fallbackConfig = gatewayConfig || { developerName: 'local-user' };
1295
+ console.log('\n\x1b[1m\x1b[36mFullCourtDefense — remove gateway wrapping from all clients\x1b[0m\n');
1296
+ let total = 0;
1297
+ for (const file of files) {
1298
+ const isToml = /\.toml$/i.test(file.path);
1299
+ if (isToml) {
1300
+ const stats = transformCodexToml(file.path, 'unwrap', fallbackConfig, dryRun);
1301
+ if (stats && stats.wrapped.length) {
1302
+ total += stats.wrapped.length;
1303
+ console.log(`\x1b[1m${file.source}\x1b[0m restored: ${stats.wrapped.join(', ')}`);
1304
+ }
1305
+ }
1306
+ else {
1307
+ const res = unwrapJsonConfigFile(file.path, dryRun);
1308
+ if (res && res.restored.length) {
1309
+ total += res.restored.length;
1310
+ console.log(`\x1b[1m${file.source}\x1b[0m restored: ${res.restored.join(', ')}`);
1311
+ }
1312
+ }
1313
+ }
1314
+ console.log(`\n${dryRun ? 'Would restore' : 'Restored'} ${total} server(s) to their original commands. Restart each client.`);
1315
+ }
943
1316
  async function installClaudeCodeMcpGatewayCommand(args, config) {
944
1317
  const gatewayConfig = resolveGatewayConfig(args, config, {
945
1318
  agentClient: 'claude-code',
package/dist/index.js CHANGED
@@ -124,6 +124,12 @@ function printHelp() {
124
124
  are allowed, blocked, or paused for human approval per policy.
125
125
  uninstall-cursor-hook
126
126
  Removes the FullCourtDefense Cursor hook entries.
127
+ protect-all Auto-detect EVERY MCP server already configured in EVERY client
128
+ (Cursor, Claude Code/Desktop, Codex, Gemini, Windsurf, VS Code)
129
+ and wrap each through the Shield proxy — no --mcp-command needed.
130
+ Enforces your org Action Policies on every tool. --dry-run to preview.
131
+ unprotect-all
132
+ Reverse protect-all: restore every server to its original command.
127
133
  install-mcp-gateway
128
134
  Install MCP gateway into selected clients (--clients all for every tool).
129
135
  install-cursor-mcp-gateway
@@ -499,6 +505,26 @@ async function main() {
499
505
  await (0, mcpGateway_1.installMcpGatewayCommand)(buildInstallArgs(), config);
500
506
  break;
501
507
  }
508
+ case 'protect-all':
509
+ case 'wrap-all': {
510
+ const args = {
511
+ ...buildGatewayArgs(),
512
+ dryRun: flags['dry-run'],
513
+ config: flags.config,
514
+ };
515
+ await (0, mcpGateway_1.protectAllCommand)(args, config);
516
+ break;
517
+ }
518
+ case 'unprotect-all':
519
+ case 'unwrap-all': {
520
+ const args = {
521
+ ...buildGatewayArgs(),
522
+ dryRun: flags['dry-run'],
523
+ config: flags.config,
524
+ };
525
+ await (0, mcpGateway_1.unprotectAllCommand)(args, config);
526
+ break;
527
+ }
502
528
  case 'install-cursor-mcp-gateway': {
503
529
  const args = {
504
530
  ...buildGatewayArgs(),
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.4.0"
2
+ "version": "1.5.1"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {