mcp-gov 1.0.0
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/LICENSE +21 -0
- package/README.md +1247 -0
- package/bin/mcp-gov-proxy.js +362 -0
- package/bin/mcp-gov-unwrap.js +442 -0
- package/bin/mcp-gov-wrap.js +766 -0
- package/package.json +53 -0
- package/postinstall.js +24 -0
- package/src/index.js +199 -0
- package/src/operation-detector.js +67 -0
- package/src/operation-keywords.js +75 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mcp-gov-unwrap - MCP Server Unwrapper
|
|
5
|
+
* Unwraps MCP servers by restoring original configuration from _original field
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs';
|
|
10
|
+
import { exec } from 'node:child_process';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse command line arguments
|
|
17
|
+
* @returns {{ config: string, tool: string, help: boolean }}
|
|
18
|
+
*/
|
|
19
|
+
function parseCliArgs() {
|
|
20
|
+
try {
|
|
21
|
+
const { values } = parseArgs({
|
|
22
|
+
options: {
|
|
23
|
+
config: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
short: 'c',
|
|
26
|
+
},
|
|
27
|
+
tool: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
short: 't',
|
|
30
|
+
},
|
|
31
|
+
help: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
short: 'h',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
allowPositionals: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return values;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Error parsing arguments: ${error.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Show usage information
|
|
48
|
+
*/
|
|
49
|
+
function showUsage() {
|
|
50
|
+
console.log(`
|
|
51
|
+
Usage: mcp-gov-unwrap --config <config.json> [--tool <command>]
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--config, -c Path to MCP config file (e.g., ~/.config/claude/config.json)
|
|
55
|
+
--tool, -t Tool command to execute after unwrapping (optional, e.g., "claude chat")
|
|
56
|
+
--help, -h Show this help message
|
|
57
|
+
|
|
58
|
+
Description:
|
|
59
|
+
Unwraps MCP servers by restoring original configuration from _original field.
|
|
60
|
+
Creates a timestamped backup of the config file before modification.
|
|
61
|
+
Supports both Claude Code format (projects.mcpServers) and flat format (mcpServers).
|
|
62
|
+
Only unwraps servers that have the _original field (previously wrapped by mcp-gov-wrap).
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
# Unwrap all wrapped servers
|
|
66
|
+
mcp-gov-unwrap --config ~/.config/claude/config.json
|
|
67
|
+
|
|
68
|
+
# Unwrap servers and launch Claude Code
|
|
69
|
+
mcp-gov-unwrap --config ~/.config/claude/config.json --tool "claude chat"
|
|
70
|
+
`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate required arguments
|
|
76
|
+
* @param {{ config?: string, tool?: string }} args
|
|
77
|
+
*/
|
|
78
|
+
function validateArgs(args) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
|
|
81
|
+
if (!args.config) {
|
|
82
|
+
errors.push('--config argument is required');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --tool is optional for unwrap
|
|
86
|
+
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
console.error('Error: Missing required arguments\n');
|
|
89
|
+
errors.forEach(err => console.error(` ${err}`));
|
|
90
|
+
console.error('\nUse --help for usage information');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load and parse config file with format detection
|
|
97
|
+
* @param {string} configPath - Path to config.json
|
|
98
|
+
* @returns {{ allMcpServers: Array<{path: string, servers: Object}>, format: string, rawConfig: Object }} Config data and detected format
|
|
99
|
+
*/
|
|
100
|
+
function loadConfig(configPath) {
|
|
101
|
+
// Check if file exists
|
|
102
|
+
if (!existsSync(configPath)) {
|
|
103
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Read and parse JSON
|
|
107
|
+
let configData;
|
|
108
|
+
try {
|
|
109
|
+
const content = readFileSync(configPath, 'utf8');
|
|
110
|
+
configData = JSON.parse(content);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof SyntaxError) {
|
|
113
|
+
throw new Error(`Invalid JSON in config file: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Failed to read config file: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Detect format and extract ALL mcpServers sections
|
|
119
|
+
let allMcpServers = [];
|
|
120
|
+
let format;
|
|
121
|
+
|
|
122
|
+
if (configData.projects && typeof configData.projects === 'object') {
|
|
123
|
+
// Multi-project format: { projects: { "/path1": { mcpServers: {...} }, "/path2": { mcpServers: {...} } } }
|
|
124
|
+
format = 'multi-project';
|
|
125
|
+
for (const [projectPath, projectConfig] of Object.entries(configData.projects)) {
|
|
126
|
+
if (projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
|
|
127
|
+
allMcpServers.push({
|
|
128
|
+
path: projectPath,
|
|
129
|
+
servers: projectConfig.mcpServers
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else if (configData.mcpServers) {
|
|
134
|
+
// Flat format: { mcpServers: {...} }
|
|
135
|
+
format = 'flat';
|
|
136
|
+
allMcpServers.push({
|
|
137
|
+
path: 'root',
|
|
138
|
+
servers: configData.mcpServers
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (allMcpServers.length === 0) {
|
|
143
|
+
throw new Error('No mcpServers found in config. Config must contain "mcpServers" (flat) or "projects[*].mcpServers" (multi-project)');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { allMcpServers, format, rawConfig: configData };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a server is wrapped (has _original field)
|
|
151
|
+
* @param {Object} serverConfig - Server configuration object
|
|
152
|
+
* @returns {boolean} True if wrapped (has _original field)
|
|
153
|
+
*/
|
|
154
|
+
function isServerWrapped(serverConfig) {
|
|
155
|
+
return serverConfig._original !== undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Detect wrapped servers in config
|
|
160
|
+
* @param {Object} mcpServers - MCP servers configuration
|
|
161
|
+
* @returns {{ wrapped: string[], unwrapped: string[], malformed: string[] }} Lists of wrapped, unwrapped, and malformed server names
|
|
162
|
+
*/
|
|
163
|
+
function detectWrappedServers(mcpServers) {
|
|
164
|
+
const wrapped = [];
|
|
165
|
+
const unwrapped = [];
|
|
166
|
+
const malformed = [];
|
|
167
|
+
|
|
168
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
169
|
+
// Check if server looks wrapped (mcp-gov-proxy) but missing _original
|
|
170
|
+
if (serverConfig.command && serverConfig.command.includes('mcp-gov-proxy') && !serverConfig._original) {
|
|
171
|
+
malformed.push(serverName);
|
|
172
|
+
} else if (isServerWrapped(serverConfig)) {
|
|
173
|
+
wrapped.push(serverName);
|
|
174
|
+
} else {
|
|
175
|
+
unwrapped.push(serverName);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { wrapped, unwrapped, malformed };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Unwrap a server configuration by restoring from _original
|
|
184
|
+
* @param {Object} serverConfig - Wrapped server configuration
|
|
185
|
+
* @returns {Object} Unwrapped server configuration
|
|
186
|
+
*/
|
|
187
|
+
function unwrapServer(serverConfig) {
|
|
188
|
+
if (!serverConfig._original) {
|
|
189
|
+
throw new Error('Server does not have _original field, cannot unwrap');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Restore original command and args
|
|
193
|
+
const unwrappedConfig = {
|
|
194
|
+
command: serverConfig._original.command,
|
|
195
|
+
args: serverConfig._original.args || []
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Preserve environment variables if they exist
|
|
199
|
+
if (serverConfig.env) {
|
|
200
|
+
unwrappedConfig.env = { ...serverConfig.env };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return unwrappedConfig;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a timestamped backup of the config file
|
|
208
|
+
* @param {string} configPath - Path to config file
|
|
209
|
+
* @returns {string} Path to backup file
|
|
210
|
+
*/
|
|
211
|
+
function createBackup(configPath) {
|
|
212
|
+
const now = new Date();
|
|
213
|
+
|
|
214
|
+
// Format: YYYYMMDD-HHMMSS
|
|
215
|
+
const year = now.getFullYear();
|
|
216
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
217
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
218
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
219
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
220
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
221
|
+
|
|
222
|
+
const timestamp = `${year}${month}${day}-${hour}${minute}${second}`;
|
|
223
|
+
const backupPath = `${configPath}.backup-${timestamp}`;
|
|
224
|
+
|
|
225
|
+
copyFileSync(configPath, backupPath);
|
|
226
|
+
|
|
227
|
+
return backupPath;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Unwrap wrapped servers in the config
|
|
232
|
+
* @param {Object} config - Full config object with mcpServers
|
|
233
|
+
* @param {string[]} wrappedNames - Names of servers to unwrap
|
|
234
|
+
* @returns {Object} Modified config with unwrapped servers
|
|
235
|
+
*/
|
|
236
|
+
function unwrapServers(config, wrappedNames) {
|
|
237
|
+
const modifiedConfig = JSON.parse(JSON.stringify(config.rawConfig));
|
|
238
|
+
|
|
239
|
+
// Get reference to mcpServers in the modified config
|
|
240
|
+
let mcpServers;
|
|
241
|
+
if (config.format === 'claude-code') {
|
|
242
|
+
mcpServers = modifiedConfig.projects.mcpServers;
|
|
243
|
+
} else {
|
|
244
|
+
mcpServers = modifiedConfig.mcpServers;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Unwrap each wrapped server
|
|
248
|
+
for (const serverName of wrappedNames) {
|
|
249
|
+
const originalConfig = mcpServers[serverName];
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
mcpServers[serverName] = unwrapServer(originalConfig);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.warn(`Warning: Cannot unwrap ${serverName}: ${error.message}`);
|
|
255
|
+
// Skip this server, leave it as-is
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return modifiedConfig;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Main entry point
|
|
264
|
+
*/
|
|
265
|
+
async function main() {
|
|
266
|
+
const args = parseCliArgs();
|
|
267
|
+
|
|
268
|
+
if (args.help) {
|
|
269
|
+
showUsage();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
validateArgs(args);
|
|
273
|
+
|
|
274
|
+
// Load config file
|
|
275
|
+
let config;
|
|
276
|
+
try {
|
|
277
|
+
config = loadConfig(args.config);
|
|
278
|
+
console.log(`Loaded config in ${config.format} format`);
|
|
279
|
+
|
|
280
|
+
// Count total servers across all projects
|
|
281
|
+
const totalServersCount = config.allMcpServers.reduce((sum, item) =>
|
|
282
|
+
sum + Object.keys(item.servers).length, 0);
|
|
283
|
+
|
|
284
|
+
if (config.format === 'multi-project') {
|
|
285
|
+
console.log(`Found ${config.allMcpServers.length} project(s) with ${totalServersCount} total MCP servers`);
|
|
286
|
+
} else {
|
|
287
|
+
console.log(`Found ${totalServersCount} MCP servers`);
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error(`Error loading config: ${error.message}`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Detect wrapped servers across ALL projects
|
|
295
|
+
let allWrapped = [];
|
|
296
|
+
let allUnwrapped = [];
|
|
297
|
+
let allMalformed = [];
|
|
298
|
+
|
|
299
|
+
for (const { path: projectPath, servers } of config.allMcpServers) {
|
|
300
|
+
const { wrapped, unwrapped, malformed } = detectWrappedServers(servers);
|
|
301
|
+
allWrapped.push(...wrapped.map(name => ({ project: projectPath, name })));
|
|
302
|
+
allUnwrapped.push(...unwrapped.map(name => ({ project: projectPath, name })));
|
|
303
|
+
allMalformed.push(...malformed.map(name => ({ project: projectPath, name })));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const totalServers = allWrapped.length + allUnwrapped.length + allMalformed.length;
|
|
307
|
+
|
|
308
|
+
if (totalServers === 0) {
|
|
309
|
+
console.log('No servers found in config');
|
|
310
|
+
} else {
|
|
311
|
+
console.log(`\nServer status (across all projects):`);
|
|
312
|
+
console.log(` Total: ${totalServers}`);
|
|
313
|
+
console.log(` Wrapped (can unwrap): ${allWrapped.length}`);
|
|
314
|
+
console.log(` Already unwrapped: ${allUnwrapped.length}`);
|
|
315
|
+
|
|
316
|
+
if (allMalformed.length > 0) {
|
|
317
|
+
console.log(` Warning - cannot unwrap (missing _original): ${allMalformed.length}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (allUnwrapped.length > 0) {
|
|
321
|
+
console.log(`\nAlready unwrapped servers:`);
|
|
322
|
+
allUnwrapped.forEach(({ project, name }) => {
|
|
323
|
+
if (config.format === 'multi-project') {
|
|
324
|
+
console.log(` - ${name} (${project})`);
|
|
325
|
+
} else {
|
|
326
|
+
console.log(` - ${name}`);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (allMalformed.length > 0) {
|
|
332
|
+
console.log(`\nWarning: These servers appear wrapped but are missing _original field (cannot unwrap):`);
|
|
333
|
+
allMalformed.forEach(({ project, name }) => {
|
|
334
|
+
if (config.format === 'multi-project') {
|
|
335
|
+
console.log(` - ${name} (${project})`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log(` - ${name}`);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (allWrapped.length > 0) {
|
|
343
|
+
console.log(`\nServers to unwrap:`);
|
|
344
|
+
allWrapped.forEach(({ project, name }) => {
|
|
345
|
+
if (config.format === 'multi-project') {
|
|
346
|
+
console.log(` - ${name} (${project})`);
|
|
347
|
+
} else {
|
|
348
|
+
console.log(` - ${name}`);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
} else if (allMalformed.length === 0) {
|
|
352
|
+
console.log(`\nAll servers already unwrapped, no action needed`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Unwrap servers if needed
|
|
357
|
+
if (allWrapped.length > 0) {
|
|
358
|
+
console.log(`\nUnwrapping ${allWrapped.length} server(s)...`);
|
|
359
|
+
|
|
360
|
+
// Create backup before modifying
|
|
361
|
+
try {
|
|
362
|
+
const backupPath = createBackup(args.config);
|
|
363
|
+
console.log(`ā Created backup: ${backupPath}`);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error(`Error creating backup: ${error.message}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Unwrap servers in each project
|
|
370
|
+
const modifiedConfig = JSON.parse(JSON.stringify(config.rawConfig));
|
|
371
|
+
|
|
372
|
+
for (const { path: projectPath, servers } of config.allMcpServers) {
|
|
373
|
+
const { wrapped } = detectWrappedServers(servers);
|
|
374
|
+
|
|
375
|
+
if (wrapped.length > 0) {
|
|
376
|
+
// Get reference to this project's mcpServers
|
|
377
|
+
let targetServers;
|
|
378
|
+
if (config.format === 'multi-project') {
|
|
379
|
+
targetServers = modifiedConfig.projects[projectPath].mcpServers;
|
|
380
|
+
} else {
|
|
381
|
+
targetServers = modifiedConfig.mcpServers;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Unwrap wrapped servers in this project
|
|
385
|
+
for (const serverName of wrapped) {
|
|
386
|
+
const originalConfig = targetServers[serverName];
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
targetServers[serverName] = unwrapServer(originalConfig);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.warn(`Warning: Cannot unwrap ${serverName}: ${error.message}`);
|
|
392
|
+
// Skip this server, leave it as-is
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Write updated config
|
|
399
|
+
try {
|
|
400
|
+
writeFileSync(args.config, JSON.stringify(modifiedConfig, null, 2) + '\n');
|
|
401
|
+
console.log(`ā Updated config file: ${args.config}`);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(`Error writing config file: ${error.message}`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Execute tool command if provided
|
|
409
|
+
if (args.tool) {
|
|
410
|
+
console.log(`\nExecuting tool command: ${args.tool}`);
|
|
411
|
+
try {
|
|
412
|
+
const { stdout, stderr } = await execAsync(args.tool, {
|
|
413
|
+
shell: true,
|
|
414
|
+
encoding: 'utf8'
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (stdout) {
|
|
418
|
+
console.log(stdout);
|
|
419
|
+
}
|
|
420
|
+
if (stderr) {
|
|
421
|
+
console.error(stderr);
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
// exec throws on non-zero exit codes, but we still want to show output
|
|
425
|
+
if (error.stdout) {
|
|
426
|
+
console.log(error.stdout);
|
|
427
|
+
}
|
|
428
|
+
if (error.stderr) {
|
|
429
|
+
console.error(error.stderr);
|
|
430
|
+
}
|
|
431
|
+
console.error(`\nTool command exited with code ${error.code || 'unknown'}`);
|
|
432
|
+
process.exit(error.code || 1);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
console.log(`\nā Unwrapping complete!`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
main().catch(error => {
|
|
440
|
+
console.error(`Fatal error: ${error.message}`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
});
|