@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.
Files changed (2) hide show
  1. package/index.js +309 -0
  2. 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
+ }