@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.
- package/README.md +245 -0
- package/dist/adapters/claude-code.js +48 -0
- package/dist/adapters/copilot-cli.js +49 -0
- package/dist/adapters/index.js +14 -0
- package/dist/adapters/windsurf.js +59 -0
- package/dist/api-client.js +88 -0
- package/dist/cli.js +94 -0
- package/dist/commands/add.js +525 -0
- package/dist/commands/enable-disable.js +72 -0
- package/dist/commands/init.js +26 -0
- package/dist/commands/list.js +65 -0
- package/dist/commands/market.js +84 -0
- package/dist/commands/platforms.js +71 -0
- package/dist/commands/remove.js +60 -0
- package/dist/commands/search.js +86 -0
- package/dist/commands/sync.js +84 -0
- package/dist/commands/update.js +34 -0
- package/dist/config.js +34 -0
- package/dist/package-selection.js +35 -0
- package/dist/pagination.js +57 -0
- package/dist/transform.js +78 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +33 -0
- package/package.json +42 -0
|
@@ -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
|
+
}
|