@evolvedqube/gmcp 0.1.0-alpha.5

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.
@@ -0,0 +1,525 @@
1
+ import inquirer from 'inquirer';
2
+ import { searchServers } from '../api-client.js';
3
+ import { transformToRegistryEntries, filterRemoteOnlyServers } from '../transform.js';
4
+ import { promptForPackageType } from '../package-selection.js';
5
+ import { readUserConfig } from '../config.js';
6
+ import { getAdapter } from '../adapters/index.js';
7
+ /**
8
+ * DUPLICATE DETECTION WORKFLOW:
9
+ *
10
+ * When adding a server (registry or manual), we detect duplicates by normalizing
11
+ * package names from command arguments and comparing across all platforms.
12
+ *
13
+ * Flow:
14
+ * 1. User runs `gmcp add <name>` or `gmcp add --manual`
15
+ * 2. Collect env vars (registry) or prompt for details (manual)
16
+ * 3. Call findAllDuplicates() to scan all platforms
17
+ * 4. If duplicates found:
18
+ * a. Show warning with duplicate names and platforms
19
+ * b. Prompt: Skip or Replace
20
+ * c. Skip: exit without changes
21
+ * d. Replace: show confirmation, then remove duplicates and add new entry
22
+ * 5. If no duplicates: proceed with normal add logic
23
+ *
24
+ * Package Extraction (Normalization):
25
+ * - Extract first positional arg by skipping known runner flags (e.g., -y, --yes, -p)
26
+ * - Strip version suffixes (e.g., "@org/pkg@latest" → "@org/pkg")
27
+ * - Works for npx/npm/uvx/uv with or without flags, handles trailing args
28
+ * - Use last segment of package (e.g., "@org/server-name" → "server-name")
29
+ * - Fallback to command+args fingerprint if extraction fails (unknown runtimes)
30
+ */
31
+ /**
32
+ * Known runner flags for npx/npm and uvx/uv commands.
33
+ * Used to skip flags when extracting the package name from arguments.
34
+ */
35
+ const NPX_BOOLEAN_FLAGS = ['-y', '--yes', '-q', '--quiet', '--no-install', '--prefer-online', '--prefer-offline'];
36
+ const NPX_VALUE_FLAGS = ['-p', '--package', '-c', '--call'];
37
+ const UVX_BOOLEAN_FLAGS = ['--quiet', '--verbose'];
38
+ const UVX_VALUE_FLAGS = ['--python', '--from'];
39
+ /**
40
+ * Sanitize a registry server name to be compatible with all platforms.
41
+ *
42
+ * Copilot CLI enforces strict validation: server names must only contain
43
+ * alphanumeric characters, underscores, and hyphens.
44
+ *
45
+ * This function:
46
+ * 1. Extracts the last segment after the final '/'
47
+ * 2. Replaces '.' with '-' to ensure compatibility
48
+ * 3. Removes any remaining invalid characters
49
+ *
50
+ * Examples:
51
+ * - "io.github.upstash/context7" → "context7"
52
+ * - "ai.smithery/example-server" → "example-server"
53
+ * - "server.name" → "server-name"
54
+ *
55
+ * @param registryName - The full registry name (e.g., "io.github.upstash/context7")
56
+ * @returns Sanitized server name safe for all platforms
57
+ */
58
+ function sanitizeServerName(registryName) {
59
+ // Extract last segment after final '/'
60
+ const segments = registryName.split('/');
61
+ let name = segments[segments.length - 1];
62
+ // Replace '.' with '-' for compatibility
63
+ name = name.replace(/\./g, '-');
64
+ // Remove any remaining invalid characters (keep only alphanumeric, underscore, hyphen)
65
+ name = name.replace(/[^a-zA-Z0-9_-]/g, '');
66
+ return name;
67
+ }
68
+ /**
69
+ * Normalize package name from command arguments using first-positional-arg detection.
70
+ *
71
+ * This function extracts the package name by:
72
+ * 1. Identifying the runtime (npx/npm, uvx/uv, or unknown)
73
+ * 2. Skipping known runner flags (boolean flags and value flags with their values)
74
+ * 3. Extracting the first positional argument (the package name)
75
+ * 4. Stripping version suffixes (e.g., @latest, @^1.0.0) to normalize versioning
76
+ *
77
+ * Examples:
78
+ * - ["@upstash/context7-mcp"] → "@upstash/context7-mcp"
79
+ * - ["-y", "@org/pkg@latest"] → "@org/pkg"
80
+ * - ["--yes", "@playwright/mcp"] → "@playwright/mcp"
81
+ * - ["mcp-server", "--key", "VALUE"] → "mcp-server" (uvx with trailing flags)
82
+ *
83
+ * @param command - The runtime command (e.g., "npx", "uvx")
84
+ * @param args - The argument array
85
+ * @returns Normalized package name, or null for unknown runtimes/no args
86
+ */
87
+ function normalizePackageName(command, args) {
88
+ // Identify runtime and its flags
89
+ let booleanFlags;
90
+ let valueFlags;
91
+ if (command === 'npx' || command === 'npm') {
92
+ booleanFlags = NPX_BOOLEAN_FLAGS;
93
+ valueFlags = NPX_VALUE_FLAGS;
94
+ }
95
+ else if (command === 'uvx' || command === 'uv') {
96
+ booleanFlags = UVX_BOOLEAN_FLAGS;
97
+ valueFlags = UVX_VALUE_FLAGS;
98
+ }
99
+ else {
100
+ // Unknown runtime - return null for fingerprint fallback
101
+ return null;
102
+ }
103
+ // Skip known flags and extract first positional arg
104
+ let i = 0;
105
+ while (i < args.length) {
106
+ const arg = args[i];
107
+ // Skip boolean flags
108
+ if (booleanFlags.includes(arg)) {
109
+ i++;
110
+ continue;
111
+ }
112
+ // Skip value flags and their values
113
+ if (valueFlags.includes(arg)) {
114
+ i += 2; // Skip the flag and its value
115
+ continue;
116
+ }
117
+ // Skip any other flags (start with -)
118
+ if (arg.startsWith('-')) {
119
+ i++;
120
+ continue;
121
+ }
122
+ // First positional arg found - this is the package
123
+ let packageName = arg;
124
+ // Strip version suffix: @latest, @^1.0.0, etc.
125
+ // Only strip trailing @<version>, not the scope @org
126
+ packageName = packageName.replace(/@[^/@]+$/, '');
127
+ return packageName;
128
+ }
129
+ // No positional args found
130
+ return null;
131
+ }
132
+ /**
133
+ * Extract package name from command arguments using normalization.
134
+ *
135
+ * This function:
136
+ * 1. Normalizes the package name (strips version suffixes, handles flags)
137
+ * 2. Extracts the last segment (after final /) for compatibility with registry entries
138
+ *
139
+ * The normalization handles:
140
+ * - npx/npm with or without -y/--yes flags
141
+ * - uvx/uv with trailing arguments/flags
142
+ * - Version suffixes like @latest, @^1.0.0
143
+ *
144
+ * Returns the last segment of the package name for duplicate detection.
145
+ * Returns null for unknown runtimes or extraction failures.
146
+ */
147
+ function extractPackageFromArgs(command, args) {
148
+ // Normalize the package name (handles flags, version suffixes)
149
+ const normalized = normalizePackageName(command, args);
150
+ if (!normalized) {
151
+ // Unknown runtime or no args - return null for fingerprint fallback
152
+ return null;
153
+ }
154
+ // Extract last segment for compatibility with registry entries
155
+ // e.g., "@modelcontextprotocol/server-fetch" → "server-fetch"
156
+ const lastSegment = normalized.split('/').pop();
157
+ return lastSegment || null;
158
+ }
159
+ /**
160
+ * Check if two server entries are duplicates by comparing extracted package names.
161
+ * Falls back to exact command+args fingerprint if package extraction fails.
162
+ */
163
+ function isDuplicateServer(target, existing) {
164
+ // Extract package names from both entries
165
+ const targetPackage = extractPackageFromArgs(target.runtime, target.args);
166
+ const existingPackage = extractPackageFromArgs(existing.command, existing.args);
167
+ // Compare extracted package names (last segment)
168
+ if (targetPackage && existingPackage && targetPackage === existingPackage) {
169
+ return true;
170
+ }
171
+ // Fallback to exact command+args fingerprint if extraction failed
172
+ const targetFingerprint = `${target.runtime}|${target.args.join('|')}`;
173
+ const existingFingerprint = `${existing.command}|${existing.args.join('|')}`;
174
+ return targetFingerprint === existingFingerprint;
175
+ }
176
+ /**
177
+ * Find all duplicate servers across all platforms.
178
+ * Returns array of all duplicate instances with platform, name, and entry info.
179
+ */
180
+ function findAllDuplicates(targetEntry, platforms) {
181
+ const duplicates = [];
182
+ for (const platform of platforms) {
183
+ const adapter = getAdapter(platform);
184
+ if (!adapter)
185
+ continue;
186
+ const servers = adapter.readServers();
187
+ for (const [name, entry] of servers.entries()) {
188
+ if (isDuplicateServer(targetEntry, entry)) {
189
+ duplicates.push({ platform, name, entry });
190
+ }
191
+ }
192
+ }
193
+ return duplicates;
194
+ }
195
+ /**
196
+ * Interactive prompt for handling duplicate servers.
197
+ * Shows all duplicates with platform info and offers Skip or Replace options.
198
+ * Returns: 'skip', 'replace', or 'cancel' (if user declines replace confirmation)
199
+ */
200
+ async function handleDuplicatePrompt(duplicates, newName, allPlatforms) {
201
+ // Extract package name for display
202
+ const firstDupe = duplicates[0];
203
+ const packageName = extractPackageFromArgs(firstDupe.entry.command, firstDupe.entry.args) || 'unknown';
204
+ // Format duplicate list
205
+ console.log('\nWarning: This server is already installed:');
206
+ for (const dupe of duplicates) {
207
+ console.log(`- "${dupe.name}" on ${dupe.platform}`);
208
+ }
209
+ console.log(`Package: ${packageName}\n`);
210
+ // Prompt for action using rawlist (shows numbered options upfront)
211
+ const actionAnswer = await inquirer.prompt([
212
+ {
213
+ type: 'rawlist',
214
+ name: 'action',
215
+ message: 'What would you like to do?',
216
+ choices: [
217
+ {
218
+ name: 'Skip (don\'t add, keep existing)',
219
+ value: 'skip',
220
+ },
221
+ {
222
+ name: 'Replace (remove existing, add new)',
223
+ value: 'replace',
224
+ },
225
+ ],
226
+ default: 0,
227
+ },
228
+ ]);
229
+ if (actionAnswer.action === 'skip') {
230
+ return 'skip';
231
+ }
232
+ // Replace option - show confirmation
233
+ if (actionAnswer.action === 'replace') {
234
+ console.log('\nReplace will:');
235
+ for (const dupe of duplicates) {
236
+ console.log(`- Remove "${dupe.name}" from ${dupe.platform}`);
237
+ }
238
+ console.log(`- Add "${newName}" to all configured platforms (${allPlatforms.join(', ')})\n`);
239
+ const confirmAnswer = await inquirer.prompt([
240
+ {
241
+ type: 'confirm',
242
+ name: 'confirm',
243
+ message: 'Proceed?',
244
+ default: false,
245
+ },
246
+ ]);
247
+ if (confirmAnswer.confirm) {
248
+ return 'replace';
249
+ }
250
+ else {
251
+ return 'cancel';
252
+ }
253
+ }
254
+ return 'cancel';
255
+ }
256
+ /**
257
+ * Replace duplicates: remove all duplicate entries, then add new entry to all platforms.
258
+ * Phase 1: Remove duplicates from their respective platforms
259
+ * Phase 2: Add new entry to all configured platforms
260
+ */
261
+ async function replaceDuplicates(duplicates, newName, newEntry, env, allPlatforms) {
262
+ // Phase 1: Remove duplicates
263
+ for (const dupe of duplicates) {
264
+ const adapter = getAdapter(dupe.platform);
265
+ if (!adapter) {
266
+ console.error(`Error: Could not get adapter for ${dupe.platform}`);
267
+ continue;
268
+ }
269
+ try {
270
+ adapter.removeServer(dupe.name);
271
+ console.log(`- Removed "${dupe.name}" from ${dupe.platform}`);
272
+ }
273
+ catch (error) {
274
+ console.error(`Error removing "${dupe.name}" from ${dupe.platform}:`, error);
275
+ // Continue with other removals even if one fails
276
+ }
277
+ }
278
+ // Phase 2: Add new entry to all platforms
279
+ let addedCount = 0;
280
+ for (const platformName of allPlatforms) {
281
+ const adapter = getAdapter(platformName);
282
+ if (!adapter) {
283
+ console.error(`Error: Could not get adapter for ${platformName}`);
284
+ continue;
285
+ }
286
+ try {
287
+ adapter.addServer(newName, newEntry, env);
288
+ console.log(`- Added "${newName}" to ${platformName}`);
289
+ addedCount++;
290
+ }
291
+ catch (error) {
292
+ console.error(`Error adding "${newName}" to ${platformName}:`, error);
293
+ }
294
+ }
295
+ console.log(`\nReplaced ${duplicates.length} duplicate(s), added "${newName}" to ${addedCount} platform(s).`);
296
+ }
297
+ export async function addCommand(name, options) {
298
+ if (options.manual) {
299
+ await addManualServer(options.only);
300
+ return;
301
+ }
302
+ // Search for server using API
303
+ let apiServers;
304
+ try {
305
+ apiServers = await searchServers(name);
306
+ }
307
+ catch (error) {
308
+ console.error(`Error: ${error instanceof Error ? error.message : 'Failed to search registry'}`);
309
+ process.exit(1);
310
+ }
311
+ // Filter remote-only servers
312
+ const localServers = filterRemoteOnlyServers(apiServers);
313
+ // Find exact match by name
314
+ const exactMatch = localServers.find(s => s.name === name);
315
+ if (!exactMatch) {
316
+ console.error(`Server "${name}" not found in registry.`);
317
+ console.error('Use `gmcp search <query>` to find servers or `gmcp add --manual` to add a custom server.');
318
+ process.exit(1);
319
+ }
320
+ // Transform to RegistryEntries
321
+ const entries = transformToRegistryEntries(exactMatch);
322
+ if (entries.length === 0) {
323
+ console.error(`Server "${name}" has no supported package types (npm or Docker).`);
324
+ process.exit(1);
325
+ }
326
+ // Let user select package type if multiple options
327
+ const entry = await promptForPackageType(entries);
328
+ // Sanitize the server name for platform compatibility
329
+ // Copilot CLI requires alphanumeric, underscores, and hyphens only
330
+ const sanitizedName = sanitizeServerName(name);
331
+ const userConfig = readUserConfig();
332
+ if (!userConfig) {
333
+ console.error('No platforms configured. Run `gmcp init` first.');
334
+ process.exit(1);
335
+ }
336
+ let targetPlatforms = userConfig.platforms;
337
+ if (options.only) {
338
+ const onlyPlatforms = options.only.split(',').map(p => p.trim()).filter(p => p.length > 0);
339
+ const unconfigured = onlyPlatforms.filter(p => !userConfig.platforms.includes(p));
340
+ if (unconfigured.length > 0) {
341
+ console.warn(`Warning: ${unconfigured.join(', ')} not configured. Run \`gmcp init\` to add them.`);
342
+ }
343
+ targetPlatforms = onlyPlatforms.filter(p => userConfig.platforms.includes(p));
344
+ if (targetPlatforms.length === 0) {
345
+ console.error('No valid platforms specified.');
346
+ process.exit(1);
347
+ }
348
+ }
349
+ const env = {};
350
+ for (const envVar of entry.env) {
351
+ if (envVar.required || await shouldPromptOptional(envVar.name, envVar.description)) {
352
+ const answer = await inquirer.prompt([
353
+ {
354
+ type: 'input',
355
+ name: 'value',
356
+ message: `${envVar.name}: ${envVar.description}`,
357
+ validate: (input) => {
358
+ if (envVar.required && !input.trim()) {
359
+ return 'This environment variable is required.';
360
+ }
361
+ return true;
362
+ },
363
+ },
364
+ ]);
365
+ if (answer.value.trim()) {
366
+ env[envVar.name] = answer.value.trim();
367
+ }
368
+ }
369
+ }
370
+ // Check for duplicates by package name before adding
371
+ const duplicates = findAllDuplicates(entry, targetPlatforms);
372
+ if (duplicates.length > 0) {
373
+ const action = await handleDuplicatePrompt(duplicates, sanitizedName, targetPlatforms);
374
+ if (action === 'skip') {
375
+ const dupeList = duplicates.map(d => `"${d.name}" on ${d.platform}`).join(', ');
376
+ console.log(`\nSkipped (already installed as ${dupeList})`);
377
+ return;
378
+ }
379
+ if (action === 'replace') {
380
+ await replaceDuplicates(duplicates, sanitizedName, entry, env, targetPlatforms);
381
+ return;
382
+ }
383
+ if (action === 'cancel') {
384
+ console.log('\nCancelled');
385
+ return;
386
+ }
387
+ }
388
+ // Original per-platform logic for name collision checking
389
+ let addedCount = 0;
390
+ let skippedCount = 0;
391
+ for (const platformName of targetPlatforms) {
392
+ const adapter = getAdapter(platformName);
393
+ if (!adapter)
394
+ continue;
395
+ const servers = adapter.readServers();
396
+ if (servers.has(sanitizedName)) {
397
+ console.log(`- ${platformName}: already installed (skipped)`);
398
+ skippedCount++;
399
+ continue;
400
+ }
401
+ adapter.addServer(sanitizedName, entry, env);
402
+ console.log(`- ${platformName}: added`);
403
+ addedCount++;
404
+ }
405
+ console.log(`\nAdded "${sanitizedName}" to ${addedCount} platform(s), skipped ${skippedCount}.`);
406
+ }
407
+ async function shouldPromptOptional(name, description) {
408
+ const answer = await inquirer.prompt([
409
+ {
410
+ type: 'confirm',
411
+ name: 'prompt',
412
+ message: `Set optional environment variable ${name}?`,
413
+ default: false,
414
+ },
415
+ ]);
416
+ return answer.prompt;
417
+ }
418
+ async function addManualServer(only) {
419
+ const answers = await inquirer.prompt([
420
+ {
421
+ type: 'input',
422
+ name: 'name',
423
+ message: 'Server name:',
424
+ validate: (input) => input.trim() ? true : 'Name is required',
425
+ },
426
+ {
427
+ type: 'input',
428
+ name: 'command',
429
+ message: 'Command (e.g., npx, node):',
430
+ validate: (input) => input.trim() ? true : 'Command is required',
431
+ },
432
+ {
433
+ type: 'input',
434
+ name: 'args',
435
+ message: 'Arguments (space-separated):',
436
+ },
437
+ ]);
438
+ const env = {};
439
+ let addMore = true;
440
+ while (addMore) {
441
+ const envAnswer = await inquirer.prompt([
442
+ {
443
+ type: 'input',
444
+ name: 'key',
445
+ message: 'Environment variable name (leave empty to skip):',
446
+ },
447
+ ]);
448
+ if (!envAnswer.key.trim()) {
449
+ addMore = false;
450
+ break;
451
+ }
452
+ const valueAnswer = await inquirer.prompt([
453
+ {
454
+ type: 'input',
455
+ name: 'value',
456
+ message: `Value for ${envAnswer.key}:`,
457
+ },
458
+ ]);
459
+ env[envAnswer.key] = valueAnswer.value;
460
+ }
461
+ const userConfig = readUserConfig();
462
+ if (!userConfig) {
463
+ console.error('No platforms configured. Run `gmcp init` first.');
464
+ process.exit(1);
465
+ }
466
+ let targetPlatforms = userConfig.platforms;
467
+ if (only) {
468
+ const onlyPlatforms = only.split(',').map(p => p.trim()).filter(p => p.length > 0);
469
+ const unconfigured = onlyPlatforms.filter(p => !userConfig.platforms.includes(p));
470
+ if (unconfigured.length > 0) {
471
+ console.warn(`Warning: ${unconfigured.join(', ')} not configured. Run \`gmcp init\` to add them.`);
472
+ }
473
+ targetPlatforms = onlyPlatforms.filter(p => userConfig.platforms.includes(p));
474
+ if (targetPlatforms.length === 0) {
475
+ console.error('No valid platforms specified.');
476
+ process.exit(1);
477
+ }
478
+ }
479
+ // Sanitize the server name for platform compatibility
480
+ const sanitizedName = sanitizeServerName(answers.name);
481
+ const manualEntry = {
482
+ name: sanitizedName,
483
+ description: 'Manually added server',
484
+ runtime: answers.command,
485
+ args: answers.args.trim().split(/\s+/),
486
+ env: [],
487
+ tags: [],
488
+ };
489
+ // Check for duplicates by package name before adding
490
+ const duplicates = findAllDuplicates(manualEntry, targetPlatforms);
491
+ if (duplicates.length > 0) {
492
+ const action = await handleDuplicatePrompt(duplicates, sanitizedName, targetPlatforms);
493
+ if (action === 'skip') {
494
+ const dupeList = duplicates.map(d => `"${d.name}" on ${d.platform}`).join(', ');
495
+ console.log(`\nSkipped (already installed as ${dupeList})`);
496
+ return;
497
+ }
498
+ if (action === 'replace') {
499
+ await replaceDuplicates(duplicates, sanitizedName, manualEntry, env, targetPlatforms);
500
+ return;
501
+ }
502
+ if (action === 'cancel') {
503
+ console.log('\nCancelled');
504
+ return;
505
+ }
506
+ }
507
+ // Original add logic with name collision checking
508
+ let addedCount = 0;
509
+ let skippedCount = 0;
510
+ for (const platformName of targetPlatforms) {
511
+ const adapter = getAdapter(platformName);
512
+ if (!adapter)
513
+ continue;
514
+ const servers = adapter.readServers();
515
+ if (servers.has(sanitizedName)) {
516
+ console.log(`- ${platformName}: already installed (skipped)`);
517
+ skippedCount++;
518
+ continue;
519
+ }
520
+ adapter.addServer(sanitizedName, manualEntry, env);
521
+ console.log(`- ${platformName}: added`);
522
+ addedCount++;
523
+ }
524
+ console.log(`\nAdded "${sanitizedName}" to ${addedCount} platform(s), skipped ${skippedCount}.`);
525
+ }
@@ -0,0 +1,72 @@
1
+ import { readUserConfig } from '../config.js';
2
+ import { getAdapter } from '../adapters/index.js';
3
+ export async function enableCommand(name) {
4
+ const userConfig = readUserConfig();
5
+ if (!userConfig) {
6
+ console.error('No platforms configured. Run `gmcp init` first.');
7
+ process.exit(1);
8
+ }
9
+ let found = false;
10
+ let enabledCount = 0;
11
+ let unsupportedCount = 0;
12
+ for (const platformName of userConfig.platforms) {
13
+ const adapter = getAdapter(platformName);
14
+ if (!adapter)
15
+ continue;
16
+ const servers = adapter.readServers();
17
+ if (!servers.has(name))
18
+ continue;
19
+ found = true;
20
+ const result = adapter.enableServer(name);
21
+ if (result === null) {
22
+ console.log(`- ${platformName}: does not support enable/disable — server is already active`);
23
+ unsupportedCount++;
24
+ }
25
+ else {
26
+ console.log(`- ${platformName}: enabled`);
27
+ enabledCount++;
28
+ }
29
+ }
30
+ if (!found) {
31
+ console.log(`${name} is not installed on any platform. Use \`gmcp add ${name}\` to install.`);
32
+ return;
33
+ }
34
+ if (enabledCount > 0) {
35
+ console.log(`\nEnabled ${name} on ${enabledCount} platform(s).`);
36
+ }
37
+ }
38
+ export async function disableCommand(name) {
39
+ const userConfig = readUserConfig();
40
+ if (!userConfig) {
41
+ console.error('No platforms configured. Run `gmcp init` first.');
42
+ process.exit(1);
43
+ }
44
+ let found = false;
45
+ let disabledCount = 0;
46
+ let unsupportedCount = 0;
47
+ for (const platformName of userConfig.platforms) {
48
+ const adapter = getAdapter(platformName);
49
+ if (!adapter)
50
+ continue;
51
+ const servers = adapter.readServers();
52
+ if (!servers.has(name))
53
+ continue;
54
+ found = true;
55
+ const result = adapter.disableServer(name);
56
+ if (result === null) {
57
+ console.log(`- ${platformName}: does not support disable — server remains active`);
58
+ unsupportedCount++;
59
+ }
60
+ else {
61
+ console.log(`- ${platformName}: disabled`);
62
+ disabledCount++;
63
+ }
64
+ }
65
+ if (!found) {
66
+ console.log(`${name} is not installed on any platform.`);
67
+ return;
68
+ }
69
+ if (disabledCount > 0) {
70
+ console.log(`\nDisabled ${name} on ${disabledCount} platform(s).`);
71
+ }
72
+ }
@@ -0,0 +1,26 @@
1
+ import inquirer from 'inquirer';
2
+ import { getAllAdapters } from '../adapters/index.js';
3
+ import { writeUserConfig } from '../config.js';
4
+ export async function initCommand() {
5
+ const adapters = getAllAdapters();
6
+ const choices = adapters.map(adapter => ({
7
+ name: adapter.name,
8
+ value: adapter.name,
9
+ checked: adapter.detect(),
10
+ }));
11
+ const answers = await inquirer.prompt([
12
+ {
13
+ type: 'checkbox',
14
+ name: 'platforms',
15
+ message: 'Select platforms to manage:',
16
+ choices,
17
+ },
18
+ ]);
19
+ if (answers.platforms.length === 0) {
20
+ console.log('No platforms selected. Run `gmcp init` again to configure.');
21
+ return;
22
+ }
23
+ writeUserConfig({ platforms: answers.platforms });
24
+ console.log(`\nConfigured platforms: ${answers.platforms.join(', ')}`);
25
+ console.log('You can now use gmcp to manage MCP servers across these platforms.');
26
+ }
@@ -0,0 +1,65 @@
1
+ import { readUserConfig } from '../config.js';
2
+ import { getAdapter } from '../adapters/index.js';
3
+ function padRight(str, width) {
4
+ return str + ' '.repeat(Math.max(0, width - str.length));
5
+ }
6
+ export async function listCommand() {
7
+ const userConfig = readUserConfig();
8
+ if (!userConfig) {
9
+ console.error('No platforms configured. Run `gmcp init` first.');
10
+ process.exit(1);
11
+ }
12
+ const allServers = new Map();
13
+ for (const platformName of userConfig.platforms) {
14
+ const adapter = getAdapter(platformName);
15
+ if (!adapter)
16
+ continue;
17
+ const servers = adapter.readServers();
18
+ for (const [name, entry] of servers.entries()) {
19
+ if (!allServers.has(name)) {
20
+ allServers.set(name, new Map());
21
+ }
22
+ allServers.get(name).set(platformName, entry);
23
+ }
24
+ }
25
+ if (allServers.size === 0) {
26
+ console.log('No MCP servers found on any platform.');
27
+ return;
28
+ }
29
+ // Build table data
30
+ const rows = [];
31
+ const headers = ['Server', ...userConfig.platforms];
32
+ rows.push(headers);
33
+ for (const [serverName, platforms] of allServers.entries()) {
34
+ const row = [serverName];
35
+ for (const platformName of userConfig.platforms) {
36
+ const entry = platforms.get(platformName);
37
+ if (!entry) {
38
+ row.push('—');
39
+ }
40
+ else if (entry.disabled) {
41
+ row.push('disabled');
42
+ }
43
+ else {
44
+ row.push('active');
45
+ }
46
+ }
47
+ rows.push(row);
48
+ }
49
+ // Calculate column widths
50
+ const colWidths = headers.map((_, colIndex) => {
51
+ return Math.max(...rows.map(row => row[colIndex].length));
52
+ });
53
+ // Print table
54
+ console.log('\nMCP Servers:\n');
55
+ // Print header
56
+ const headerRow = headers.map((header, i) => padRight(header, colWidths[i])).join(' ');
57
+ console.log(headerRow);
58
+ console.log('─'.repeat(headerRow.length));
59
+ // Print data rows
60
+ for (let i = 1; i < rows.length; i++) {
61
+ const row = rows[i].map((cell, j) => padRight(cell, colWidths[j])).join(' ');
62
+ console.log(row);
63
+ }
64
+ console.log('');
65
+ }