@tonygeez/mcp-conf 1.0.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.
- package/index.js +309 -0
- package/package.json +26 -0
package/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
// Color codes for terminal styling
|
|
8
|
+
const colors = {
|
|
9
|
+
reset: '\x1b[0m',
|
|
10
|
+
bright: '\x1b[1m',
|
|
11
|
+
dim: '\x1b[2m',
|
|
12
|
+
underscore: '\x1b[4m',
|
|
13
|
+
blink: '\x1b[5m',
|
|
14
|
+
reverse: '\x1b[7m',
|
|
15
|
+
hidden: '\x1b[8m',
|
|
16
|
+
|
|
17
|
+
fg: {
|
|
18
|
+
black: '\x1b[30m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
green: '\x1b[32m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
blue: '\x1b[34m',
|
|
23
|
+
magenta: '\x1b[35m',
|
|
24
|
+
cyan: '\x1b[36m',
|
|
25
|
+
white: '\x1b[37m',
|
|
26
|
+
crimson: '\x1b[38m'
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
bg: {
|
|
30
|
+
black: '\x1b[40m',
|
|
31
|
+
red: '\x1b[41m',
|
|
32
|
+
green: '\x1b[42m',
|
|
33
|
+
yellow: '\x1b[43m',
|
|
34
|
+
blue: '\x1b[44m',
|
|
35
|
+
magenta: '\x1b[45m',
|
|
36
|
+
cyan: '\x1b[46m',
|
|
37
|
+
white: '\x1b[47m',
|
|
38
|
+
crimson: '\x1b[48m'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Logger utility with colors
|
|
43
|
+
const logger = {
|
|
44
|
+
info: (msg) => console.log(`${colors.fg.blue}ℹ${colors.reset} ${msg}`),
|
|
45
|
+
success: (msg) => console.log(`${colors.fg.green}✔${colors.reset} ${msg}`),
|
|
46
|
+
error: (msg) => console.log(`${colors.fg.red}✖${colors.reset} ${msg}`),
|
|
47
|
+
warning: (msg) => console.log(`${colors.fg.yellow}⚠${colors.reset} ${msg}`),
|
|
48
|
+
highlight: (msg) => console.log(`${colors.fg.cyan}${colors.bright}${msg}${colors.reset}`),
|
|
49
|
+
prompt: (msg) => `${colors.fg.magenta}?${colors.reset} ${colors.bright}${msg}${colors.reset}`,
|
|
50
|
+
enum: (options) => options.map((opt, i) => ` ${colors.fg.cyan}${i + 1}${colors.reset}) ${opt}`).join('\n'),
|
|
51
|
+
header: (msg) => console.log(`\n${colors.fg.cyan}${colors.bright}═══ ${msg} ═══${colors.reset}\n`),
|
|
52
|
+
divider: () => console.log(`${colors.dim}${'─'.repeat(50)}${colors.reset}`)
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Create readline interface
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Promise-based question prompt
|
|
62
|
+
function ask(question, defaultValue = '') {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const promptText = defaultValue
|
|
65
|
+
? `${question} ${colors.dim}(${defaultValue})${colors.reset}: `
|
|
66
|
+
: `${question}: `;
|
|
67
|
+
|
|
68
|
+
rl.question(promptText, (answer) => {
|
|
69
|
+
resolve(answer.trim() || defaultValue);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Enum selection prompt
|
|
75
|
+
async function askEnum(question, options) {
|
|
76
|
+
console.log(logger.prompt(question));
|
|
77
|
+
console.log(logger.enum(options));
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const answer = await ask('Select', '1');
|
|
81
|
+
const index = parseInt(answer) - 1;
|
|
82
|
+
|
|
83
|
+
if (index >= 0 && index < options.length) {
|
|
84
|
+
return options[index];
|
|
85
|
+
}
|
|
86
|
+
logger.error('Invalid selection. Please try again.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Yes/No confirmation
|
|
91
|
+
async function confirm(question, defaultYes = false) {
|
|
92
|
+
const defaultText = defaultYes ? 'Y/n' : 'y/N';
|
|
93
|
+
const answer = await ask(`${question} ${colors.dim}[${defaultText}]${colors.reset}`, defaultYes ? 'yes' : 'no');
|
|
94
|
+
return answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate environment variable name
|
|
98
|
+
function isValidEnvName(name) {
|
|
99
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Collect environment variables recursively
|
|
103
|
+
async function collectEnvVars(env = {}) {
|
|
104
|
+
const setEnv = await confirm('Set environment variables for this server?', false);
|
|
105
|
+
|
|
106
|
+
if (!setEnv) {
|
|
107
|
+
return env;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const envName = await ask('Environment variable name');
|
|
111
|
+
|
|
112
|
+
if (!envName) {
|
|
113
|
+
logger.warning('Empty variable name, skipping...');
|
|
114
|
+
return await collectEnvVars(env);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!isValidEnvName(envName)) {
|
|
118
|
+
logger.error('Invalid environment variable name. Use letters, numbers, and underscores only.');
|
|
119
|
+
return await collectEnvVars(env);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (env[envName] !== undefined) {
|
|
123
|
+
const overwrite = await confirm(`Variable "${envName}" already exists. Overwrite?`, false);
|
|
124
|
+
if (!overwrite) {
|
|
125
|
+
return await collectEnvVars(env);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const envValue = await ask(`Value for ${envName}`);
|
|
130
|
+
env[envName] = envValue;
|
|
131
|
+
|
|
132
|
+
logger.success(`Added: ${envName}=${envValue}`);
|
|
133
|
+
|
|
134
|
+
const addAnother = await confirm('Add another environment variable?', false);
|
|
135
|
+
if (addAnother) {
|
|
136
|
+
return await collectEnvVars(env);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return env;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Main configuration collection
|
|
143
|
+
async function collectServerConfig() {
|
|
144
|
+
logger.header('MCP Server Configuration');
|
|
145
|
+
|
|
146
|
+
// Server name
|
|
147
|
+
const serverName = await ask('MCP Server name');
|
|
148
|
+
if (!serverName) {
|
|
149
|
+
throw new Error('Server name is required');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Command selection
|
|
153
|
+
const commandOptions = ['npx', 'node', 'other'];
|
|
154
|
+
const selectedCommand = await askEnum('Command', commandOptions);
|
|
155
|
+
|
|
156
|
+
let command;
|
|
157
|
+
if (selectedCommand === 'other') {
|
|
158
|
+
command = await ask('Enter custom command');
|
|
159
|
+
if (!command) {
|
|
160
|
+
throw new Error('Command is required');
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
command = selectedCommand;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Arguments
|
|
167
|
+
const argsInput = await ask('Arguments (space-separated, e.g., -y @modelcontextprotocol/server-filesystem)');
|
|
168
|
+
const args = argsInput ? argsInput.split(/\s+/).filter(arg => arg.length > 0) : [];
|
|
169
|
+
|
|
170
|
+
// Environment variables
|
|
171
|
+
const env = await collectEnvVars({});
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
name: serverName,
|
|
175
|
+
config: {
|
|
176
|
+
command,
|
|
177
|
+
args,
|
|
178
|
+
env
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Read existing .mcp.json or return default structure
|
|
184
|
+
async function readExistingConfig(filePath) {
|
|
185
|
+
try {
|
|
186
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
187
|
+
const parsed = JSON.parse(content);
|
|
188
|
+
|
|
189
|
+
if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {
|
|
190
|
+
logger.warning('Existing file has invalid structure. Creating backup and reinitializing...');
|
|
191
|
+
await fs.writeFile(`${filePath}.backup`, content);
|
|
192
|
+
return { mcpServers: {} };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return parsed;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.code === 'ENOENT') {
|
|
198
|
+
return null; // File doesn't exist
|
|
199
|
+
}
|
|
200
|
+
if (error instanceof SyntaxError) {
|
|
201
|
+
logger.error('Existing .mcp.json contains invalid JSON');
|
|
202
|
+
throw new Error('Please fix or remove the existing .mcp.json file');
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Write configuration to file
|
|
209
|
+
async function writeConfig(filePath, config) {
|
|
210
|
+
const content = JSON.stringify(config, null, 2);
|
|
211
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Display server preview
|
|
215
|
+
function displayPreview(serverName, config) {
|
|
216
|
+
logger.divider();
|
|
217
|
+
logger.highlight('Configuration Preview:');
|
|
218
|
+
console.log(`${colors.fg.cyan}Server Name:${colors.reset} ${serverName}`);
|
|
219
|
+
console.log(`${colors.fg.cyan}Command:${colors.reset} ${config.command}`);
|
|
220
|
+
console.log(`${colors.fg.cyan}Arguments:${colors.reset} ${config.args.length > 0 ? JSON.stringify(config.args) : colors.dim + '(none)' + colors.reset}`);
|
|
221
|
+
|
|
222
|
+
const envKeys = Object.keys(config.env);
|
|
223
|
+
if (envKeys.length > 0) {
|
|
224
|
+
console.log(`${colors.fg.cyan}Environment Variables:${colors.reset}`);
|
|
225
|
+
envKeys.forEach(key => {
|
|
226
|
+
const maskedValue = config.env[key].length > 0 ? '••••••' : '(empty)';
|
|
227
|
+
console.log(` ${colors.fg.yellow}${key}${colors.reset}=${maskedValue}`);
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
console.log(`${colors.fg.cyan}Environment Variables:${colors.reset} ${colors.dim}(none)${colors.reset}`);
|
|
231
|
+
}
|
|
232
|
+
logger.divider();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Main execution
|
|
236
|
+
async function main() {
|
|
237
|
+
try {
|
|
238
|
+
const cwd = process.cwd();
|
|
239
|
+
const filePath = path.join(cwd, '.mcp.json');
|
|
240
|
+
|
|
241
|
+
logger.header('MCP Configuration Tool');
|
|
242
|
+
|
|
243
|
+
// Check existing file
|
|
244
|
+
const existingConfig = await readExistingConfig(filePath);
|
|
245
|
+
|
|
246
|
+
if (existingConfig === null) {
|
|
247
|
+
logger.info('No existing .mcp.json found. Will create new file.');
|
|
248
|
+
} else {
|
|
249
|
+
const serverCount = Object.keys(existingConfig.mcpServers).length;
|
|
250
|
+
logger.info(`Found existing .mcp.json with ${serverCount} server(s). Will append to it.`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Collect new server configuration
|
|
254
|
+
const { name, config } = await collectServerConfig();
|
|
255
|
+
|
|
256
|
+
// Check for duplicate names
|
|
257
|
+
if (existingConfig && existingConfig.mcpServers[name]) {
|
|
258
|
+
const overwrite = await confirm(`Server "${name}" already exists. Overwrite?`, false);
|
|
259
|
+
if (!overwrite) {
|
|
260
|
+
logger.info('Operation cancelled.');
|
|
261
|
+
rl.close();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
logger.warning(`Overwriting existing server: ${name}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Show preview
|
|
268
|
+
displayPreview(name, config);
|
|
269
|
+
|
|
270
|
+
// Final confirmation
|
|
271
|
+
const proceed = await confirm('Save this configuration?', true);
|
|
272
|
+
if (!proceed) {
|
|
273
|
+
logger.info('Operation cancelled. No changes made.');
|
|
274
|
+
rl.close();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Merge and write
|
|
279
|
+
const finalConfig = existingConfig || { mcpServers: {} };
|
|
280
|
+
finalConfig.mcpServers[name] = config;
|
|
281
|
+
|
|
282
|
+
await writeConfig(filePath, finalConfig);
|
|
283
|
+
|
|
284
|
+
// Success message
|
|
285
|
+
logger.success(`Configuration saved to: ${filePath}`);
|
|
286
|
+
console.log(`${colors.fg.green}${colors.bright}✓${colors.reset} Server "${colors.fg.cyan}${name}${colors.reset}" ${existingConfig && existingConfig.mcpServers[name] ? 'updated' : 'added'} successfully.`);
|
|
287
|
+
|
|
288
|
+
const totalServers = Object.keys(finalConfig.mcpServers).length;
|
|
289
|
+
logger.info(`Total servers in configuration: ${totalServers}`);
|
|
290
|
+
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error(`Error: ${error.message}`);
|
|
293
|
+
if (error.stack) {
|
|
294
|
+
console.error(`${colors.dim}${error.stack}${colors.reset}`);
|
|
295
|
+
}
|
|
296
|
+
process.exit(1);
|
|
297
|
+
} finally {
|
|
298
|
+
rl.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Handle graceful shutdown
|
|
303
|
+
process.on('SIGINT', () => {
|
|
304
|
+
console.log(`\n${colors.fg.yellow}Operation cancelled by user.${colors.reset}`);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Run main
|
|
309
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tonygeez/mcp-conf",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Interactive CLI tool for managing MCP server configurations",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcpconf": "./index.js",
|
|
8
|
+
"mcpc": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"cli",
|
|
17
|
+
"config",
|
|
18
|
+
"interactive",
|
|
19
|
+
"model-context-protocol"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|