@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,84 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { fetchServers } from '../api-client.js';
|
|
3
|
+
import { filterRemoteOnlyServers } from '../transform.js';
|
|
4
|
+
import { addCommand } from './add.js';
|
|
5
|
+
const EXIT_OPTION = '✓ Done (exit)';
|
|
6
|
+
const SHOW_MORE_OPTION = '→ Show next 10 servers';
|
|
7
|
+
const PAGE_SIZE = 10;
|
|
8
|
+
/**
|
|
9
|
+
* Browse and install MCP servers from the registry
|
|
10
|
+
*/
|
|
11
|
+
export async function marketCommand() {
|
|
12
|
+
try {
|
|
13
|
+
console.log('Fetching servers from MCP Registry...');
|
|
14
|
+
// Fetch all servers from API
|
|
15
|
+
const allServers = await fetchServers();
|
|
16
|
+
// Filter out remote-only servers
|
|
17
|
+
const localServers = filterRemoteOnlyServers(allServers);
|
|
18
|
+
if (localServers.length === 0) {
|
|
19
|
+
console.log('No servers available.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Deduplicate by server name (API may return multiple entries for same server with different packages)
|
|
23
|
+
const uniqueServers = Array.from(new Map(localServers.map(server => [server.name, server])).values());
|
|
24
|
+
// Sort alphabetically
|
|
25
|
+
const sortedServers = [...uniqueServers].sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
console.log(`\nFound ${sortedServers.length} unique server(s):\n`);
|
|
27
|
+
let currentPage = 0;
|
|
28
|
+
// Interactive loop with pagination
|
|
29
|
+
while (true) {
|
|
30
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
31
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, sortedServers.length);
|
|
32
|
+
const currentPageServers = sortedServers.slice(startIndex, endIndex);
|
|
33
|
+
const hasMore = endIndex < sortedServers.length;
|
|
34
|
+
console.log(`\nShowing ${startIndex + 1}-${endIndex} of ${sortedServers.length} servers:\n`);
|
|
35
|
+
// Format choices for current page
|
|
36
|
+
const choices = currentPageServers.map(server => ({
|
|
37
|
+
name: `${server.name} - ${server.description}`,
|
|
38
|
+
value: server.name,
|
|
39
|
+
}));
|
|
40
|
+
// Build options list
|
|
41
|
+
const allChoices = [
|
|
42
|
+
{ name: EXIT_OPTION, value: EXIT_OPTION },
|
|
43
|
+
...choices,
|
|
44
|
+
];
|
|
45
|
+
// Add "Show more" option if there are more results
|
|
46
|
+
if (hasMore) {
|
|
47
|
+
allChoices.push({ name: SHOW_MORE_OPTION, value: SHOW_MORE_OPTION });
|
|
48
|
+
}
|
|
49
|
+
const answer = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'rawlist',
|
|
52
|
+
name: 'server',
|
|
53
|
+
message: 'Select an MCP server to install:',
|
|
54
|
+
choices: allChoices,
|
|
55
|
+
pageSize: 15,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
// Handle exit
|
|
59
|
+
if (answer.server === EXIT_OPTION) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Handle show more
|
|
63
|
+
if (answer.server === SHOW_MORE_OPTION) {
|
|
64
|
+
currentPage++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Install selected server
|
|
68
|
+
try {
|
|
69
|
+
await addCommand(answer.server, {});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Installation failed:', error instanceof Error ? error.message : String(error));
|
|
73
|
+
}
|
|
74
|
+
console.log(''); // Add spacing
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof Error) {
|
|
79
|
+
console.error(`Error: ${error.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 platformsCommand() {
|
|
7
|
+
// Task 2.1: Load config
|
|
8
|
+
const userConfig = readUserConfig();
|
|
9
|
+
// Tasks 2.2 & 2.3: Handle missing or empty config
|
|
10
|
+
if (!userConfig || !userConfig.platforms || userConfig.platforms.length === 0) {
|
|
11
|
+
console.log('No platforms configured. Run `gmcp init` first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// Task 2.4: Extract platforms array
|
|
15
|
+
const configuredPlatforms = userConfig.platforms;
|
|
16
|
+
// Task 4.2: Platform ID to human-readable name mapping
|
|
17
|
+
const platformNames = {
|
|
18
|
+
'claude-code': 'Claude Code',
|
|
19
|
+
'copilot-cli': 'Copilot CLI',
|
|
20
|
+
'windsurf': 'Windsurf',
|
|
21
|
+
};
|
|
22
|
+
const tableRows = [];
|
|
23
|
+
// Tasks 3.1-3.4: Process each configured platform
|
|
24
|
+
for (const platformId of configuredPlatforms) {
|
|
25
|
+
const humanName = platformNames[platformId] || platformId;
|
|
26
|
+
// Task 3.2: Get adapter for this platform
|
|
27
|
+
const adapter = getAdapter(platformId);
|
|
28
|
+
let detectedStatus;
|
|
29
|
+
if (!adapter) {
|
|
30
|
+
// Task 4.4: Handle unrecognized platform
|
|
31
|
+
detectedStatus = 'Unknown platform';
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Task 3.3: Call adapter's detect() method with error handling
|
|
35
|
+
try {
|
|
36
|
+
// Task 3.4: Wrap in try-catch for graceful error handling
|
|
37
|
+
const isDetected = adapter.detect();
|
|
38
|
+
// Task 4.3: Format with symbols
|
|
39
|
+
detectedStatus = isDetected ? '✓' : '✗';
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// Task 4.4: Handle adapter errors
|
|
43
|
+
detectedStatus = 'Error';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
tableRows.push({
|
|
47
|
+
platform: humanName,
|
|
48
|
+
configured: '✓',
|
|
49
|
+
detected: detectedStatus,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Task 4.5: Print formatted table
|
|
53
|
+
const headers = ['Platform', 'Configured', 'Detected'];
|
|
54
|
+
const rows = [headers, ...tableRows.map(r => [r.platform, r.configured, r.detected])];
|
|
55
|
+
// Calculate column widths
|
|
56
|
+
const colWidths = headers.map((_, colIndex) => {
|
|
57
|
+
return Math.max(...rows.map(row => row[colIndex].length));
|
|
58
|
+
});
|
|
59
|
+
// Print table
|
|
60
|
+
console.log('\nConfigured Platforms:\n');
|
|
61
|
+
// Print header
|
|
62
|
+
const headerRow = headers.map((header, i) => padRight(header, colWidths[i])).join(' ');
|
|
63
|
+
console.log(headerRow);
|
|
64
|
+
console.log('─'.repeat(headerRow.length));
|
|
65
|
+
// Print data rows
|
|
66
|
+
for (let i = 1; i < rows.length; i++) {
|
|
67
|
+
const row = rows[i].map((cell, j) => padRight(cell, colWidths[j])).join(' ');
|
|
68
|
+
console.log(row);
|
|
69
|
+
}
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { readUserConfig } from '../config.js';
|
|
3
|
+
import { getAdapter } from '../adapters/index.js';
|
|
4
|
+
export async function removeCommand(name, options) {
|
|
5
|
+
const userConfig = readUserConfig();
|
|
6
|
+
if (!userConfig) {
|
|
7
|
+
console.error('No platforms configured. Run `gmcp init` first.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
let targetPlatforms = userConfig.platforms;
|
|
11
|
+
if (options.only) {
|
|
12
|
+
const onlyPlatforms = options.only.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
13
|
+
const unconfigured = onlyPlatforms.filter(p => !userConfig.platforms.includes(p));
|
|
14
|
+
if (unconfigured.length > 0) {
|
|
15
|
+
console.warn(`Warning: ${unconfigured.join(', ')} not configured. Run \`gmcp init\` to add them.`);
|
|
16
|
+
}
|
|
17
|
+
targetPlatforms = onlyPlatforms.filter(p => userConfig.platforms.includes(p));
|
|
18
|
+
if (targetPlatforms.length === 0) {
|
|
19
|
+
console.error('No valid platforms specified.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const installedOn = [];
|
|
24
|
+
for (const platformName of targetPlatforms) {
|
|
25
|
+
const adapter = getAdapter(platformName);
|
|
26
|
+
if (!adapter)
|
|
27
|
+
continue;
|
|
28
|
+
const servers = adapter.readServers();
|
|
29
|
+
if (servers.has(name)) {
|
|
30
|
+
installedOn.push(platformName);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (installedOn.length === 0) {
|
|
34
|
+
console.log(`${name} is not installed on any platform.`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(`${name} is installed on: ${installedOn.join(', ')}`);
|
|
38
|
+
const answer = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'confirm',
|
|
41
|
+
name: 'confirm',
|
|
42
|
+
message: `Remove ${name} from ${installedOn.length} platform(s)?`,
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
if (!answer.confirm) {
|
|
47
|
+
console.log('Cancelled.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let removedCount = 0;
|
|
51
|
+
for (const platformName of installedOn) {
|
|
52
|
+
const adapter = getAdapter(platformName);
|
|
53
|
+
if (!adapter)
|
|
54
|
+
continue;
|
|
55
|
+
adapter.removeServer(name);
|
|
56
|
+
console.log(`- ${platformName}: removed`);
|
|
57
|
+
removedCount++;
|
|
58
|
+
}
|
|
59
|
+
console.log(`\nRemoved ${name} from ${removedCount} platform(s).`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { searchServers } from '../api-client.js';
|
|
3
|
+
import { filterRemoteOnlyServers } from '../transform.js';
|
|
4
|
+
import { addCommand } from './add.js';
|
|
5
|
+
const EXIT_OPTION = '✓ Done (exit)';
|
|
6
|
+
const SHOW_MORE_OPTION = '→ Show next 10 servers';
|
|
7
|
+
const PAGE_SIZE = 10;
|
|
8
|
+
/**
|
|
9
|
+
* Search for MCP servers by name and optionally install
|
|
10
|
+
*/
|
|
11
|
+
export async function searchCommand(query) {
|
|
12
|
+
try {
|
|
13
|
+
console.log(`Searching for "${query}"...`);
|
|
14
|
+
// Search using API
|
|
15
|
+
const results = await searchServers(query);
|
|
16
|
+
// Filter out remote-only servers
|
|
17
|
+
const localServers = filterRemoteOnlyServers(results);
|
|
18
|
+
if (localServers.length === 0) {
|
|
19
|
+
console.log('No servers found.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Deduplicate by server name (API may return multiple entries for same server with different packages)
|
|
23
|
+
const uniqueServers = Array.from(new Map(localServers.map(server => [server.name, server])).values());
|
|
24
|
+
// Sort alphabetically
|
|
25
|
+
const sortedServers = [...uniqueServers].sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
console.log(`\nFound ${sortedServers.length} server(s) matching '${query}':\n`);
|
|
27
|
+
let currentPage = 0;
|
|
28
|
+
// Interactive loop with pagination
|
|
29
|
+
while (true) {
|
|
30
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
31
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, sortedServers.length);
|
|
32
|
+
const currentPageServers = sortedServers.slice(startIndex, endIndex);
|
|
33
|
+
const hasMore = endIndex < sortedServers.length;
|
|
34
|
+
if (currentPage > 0) {
|
|
35
|
+
console.log(`\nShowing ${startIndex + 1}-${endIndex} of ${sortedServers.length} servers:\n`);
|
|
36
|
+
}
|
|
37
|
+
// Format choices for current page
|
|
38
|
+
const choices = currentPageServers.map(server => ({
|
|
39
|
+
name: `${server.name} - ${server.description}`,
|
|
40
|
+
value: server.name,
|
|
41
|
+
}));
|
|
42
|
+
// Build options list
|
|
43
|
+
const allChoices = [
|
|
44
|
+
{ name: EXIT_OPTION, value: EXIT_OPTION },
|
|
45
|
+
...choices,
|
|
46
|
+
];
|
|
47
|
+
// Add "Show more" option if there are more results
|
|
48
|
+
if (hasMore) {
|
|
49
|
+
allChoices.push({ name: SHOW_MORE_OPTION, value: SHOW_MORE_OPTION });
|
|
50
|
+
}
|
|
51
|
+
const answer = await inquirer.prompt([
|
|
52
|
+
{
|
|
53
|
+
type: 'rawlist',
|
|
54
|
+
name: 'server',
|
|
55
|
+
message: 'Select an MCP server to install (or exit):',
|
|
56
|
+
choices: allChoices,
|
|
57
|
+
pageSize: 15,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
// Handle exit
|
|
61
|
+
if (answer.server === EXIT_OPTION) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Handle show more
|
|
65
|
+
if (answer.server === SHOW_MORE_OPTION) {
|
|
66
|
+
currentPage++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Install selected server
|
|
70
|
+
try {
|
|
71
|
+
await addCommand(answer.server, {});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Installation failed:', error instanceof Error ? error.message : String(error));
|
|
75
|
+
}
|
|
76
|
+
console.log(''); // Add spacing
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
console.error(`Error: ${error.message}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { readUserConfig } from '../config.js';
|
|
3
|
+
import { getAdapter } from '../adapters/index.js';
|
|
4
|
+
export async function syncCommand() {
|
|
5
|
+
const userConfig = readUserConfig();
|
|
6
|
+
if (!userConfig) {
|
|
7
|
+
console.error('No platforms configured. Run `gmcp init` first.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const allServers = new Map();
|
|
11
|
+
for (const platformName of userConfig.platforms) {
|
|
12
|
+
const adapter = getAdapter(platformName);
|
|
13
|
+
if (!adapter)
|
|
14
|
+
continue;
|
|
15
|
+
const servers = adapter.readServers();
|
|
16
|
+
for (const [name, entry] of servers.entries()) {
|
|
17
|
+
if (!allServers.has(name)) {
|
|
18
|
+
allServers.set(name, new Map());
|
|
19
|
+
}
|
|
20
|
+
allServers.get(name).set(platformName, entry);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const drift = [];
|
|
24
|
+
for (const [serverName, platforms] of allServers.entries()) {
|
|
25
|
+
const missingFrom = userConfig.platforms.filter(p => !platforms.has(p));
|
|
26
|
+
if (missingFrom.length > 0) {
|
|
27
|
+
const sourcePlatform = Array.from(platforms.keys())[0];
|
|
28
|
+
const sourceEntry = platforms.get(sourcePlatform);
|
|
29
|
+
drift.push({
|
|
30
|
+
serverName,
|
|
31
|
+
missingFrom,
|
|
32
|
+
sourceEntry,
|
|
33
|
+
sourcePlatform,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (drift.length === 0) {
|
|
38
|
+
console.log('All platforms are in sync.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`\nFound ${drift.length} server(s) with drift:\n`);
|
|
42
|
+
for (const entry of drift) {
|
|
43
|
+
console.log(`${entry.serverName} — missing from: ${entry.missingFrom.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
const choices = drift.map(d => ({
|
|
46
|
+
name: `${d.serverName} → ${d.missingFrom.join(', ')}`,
|
|
47
|
+
value: d.serverName,
|
|
48
|
+
checked: true,
|
|
49
|
+
}));
|
|
50
|
+
const answers = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'checkbox',
|
|
53
|
+
name: 'servers',
|
|
54
|
+
message: 'Select servers to sync:',
|
|
55
|
+
choices,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
if (answers.servers.length === 0) {
|
|
59
|
+
console.log('No servers selected.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let syncedCount = 0;
|
|
63
|
+
for (const serverName of answers.servers) {
|
|
64
|
+
const driftEntry = drift.find(d => d.serverName === serverName);
|
|
65
|
+
for (const platformName of driftEntry.missingFrom) {
|
|
66
|
+
const adapter = getAdapter(platformName);
|
|
67
|
+
if (!adapter)
|
|
68
|
+
continue;
|
|
69
|
+
const mockEntry = {
|
|
70
|
+
name: serverName,
|
|
71
|
+
description: 'Synced from another platform',
|
|
72
|
+
runtime: driftEntry.sourceEntry.command,
|
|
73
|
+
args: driftEntry.sourceEntry.args,
|
|
74
|
+
env: [],
|
|
75
|
+
tags: [],
|
|
76
|
+
};
|
|
77
|
+
const env = driftEntry.sourceEntry.env || {};
|
|
78
|
+
adapter.addServer(serverName, mockEntry, env);
|
|
79
|
+
console.log(`- ${serverName} → ${platformName}: synced`);
|
|
80
|
+
syncedCount++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.log(`\nSynced ${syncedCount} server(s) across platforms.`);
|
|
84
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn, exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
async function getInstalledVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout } = await execAsync('npm list -g gmcp --depth=0 --json');
|
|
7
|
+
const data = JSON.parse(stdout);
|
|
8
|
+
return data.dependencies?.gmcp?.version || null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function updateCommand() {
|
|
15
|
+
console.log('Updating gmcp...\n');
|
|
16
|
+
const child = spawn('npm', ['update', '-g', 'gmcp'], {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
shell: true,
|
|
19
|
+
});
|
|
20
|
+
child.on('close', async (code) => {
|
|
21
|
+
if (code === 0) {
|
|
22
|
+
console.log('\ngmcp has been updated successfully.');
|
|
23
|
+
// Display the installed version
|
|
24
|
+
const version = await getInstalledVersion();
|
|
25
|
+
if (version) {
|
|
26
|
+
console.log(`Installed version: ${version}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error('\nFailed to update gmcp.');
|
|
31
|
+
process.exit(code || 1);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
const GMCP_DIR = path.join(os.homedir(), '.gmcp');
|
|
5
|
+
const CONFIG_FILE = path.join(GMCP_DIR, 'config.json');
|
|
6
|
+
export function ensureGmcpDir() {
|
|
7
|
+
if (!fs.existsSync(GMCP_DIR)) {
|
|
8
|
+
fs.mkdirSync(GMCP_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function readUserConfig() {
|
|
12
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new Error(`Failed to read config at ${CONFIG_FILE}: ${error}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function writeUserConfig(config) {
|
|
24
|
+
ensureGmcpDir();
|
|
25
|
+
try {
|
|
26
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Error(`Failed to write config at ${CONFIG_FILE}: ${error}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function isInitialized() {
|
|
33
|
+
return fs.existsSync(CONFIG_FILE);
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
/**
|
|
3
|
+
* Prompt user to select between multiple package types (npm vs Docker)
|
|
4
|
+
* Returns the selected RegistryEntry
|
|
5
|
+
*/
|
|
6
|
+
export async function promptForPackageType(entries) {
|
|
7
|
+
// If only one package type, return it without prompting
|
|
8
|
+
if (entries.length === 1) {
|
|
9
|
+
return entries[0];
|
|
10
|
+
}
|
|
11
|
+
// Build choices for inquirer
|
|
12
|
+
const choices = entries.map((entry, index) => {
|
|
13
|
+
const label = entry.runtime === 'npx' ? 'npm (via npx)' : 'Docker (via OCI)';
|
|
14
|
+
return {
|
|
15
|
+
name: `${index + 1}. ${label}`,
|
|
16
|
+
value: index,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
console.log('\nThis server offers multiple runtimes:');
|
|
20
|
+
const answer = await inquirer.prompt([
|
|
21
|
+
{
|
|
22
|
+
type: 'rawlist',
|
|
23
|
+
name: 'selection',
|
|
24
|
+
message: 'Select runtime:',
|
|
25
|
+
choices,
|
|
26
|
+
validate: (input) => {
|
|
27
|
+
if (input >= 0 && input < entries.length) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return 'Please select a valid option';
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
return entries[answer.selection];
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
/**
|
|
3
|
+
* Display results with pagination (20 per page)
|
|
4
|
+
* Shows "Show more? (y/n)" prompt when there are more results
|
|
5
|
+
*/
|
|
6
|
+
export async function displayWithPagination(items, pageSize = 20, contextMessage) {
|
|
7
|
+
if (items.length === 0) {
|
|
8
|
+
console.log('No servers found.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
let currentIndex = 0;
|
|
12
|
+
while (currentIndex < items.length) {
|
|
13
|
+
const endIndex = Math.min(currentIndex + pageSize, items.length);
|
|
14
|
+
const currentPage = items.slice(currentIndex, endIndex);
|
|
15
|
+
// Display header
|
|
16
|
+
if (contextMessage) {
|
|
17
|
+
console.log(`\nShowing ${currentIndex + 1}-${endIndex} of ${items.length} ${contextMessage}:`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(`\nShowing ${currentIndex + 1}-${endIndex} of ${items.length} servers:`);
|
|
21
|
+
}
|
|
22
|
+
// Display items
|
|
23
|
+
console.log('');
|
|
24
|
+
currentPage.forEach((item, index) => {
|
|
25
|
+
const number = currentIndex + index + 1;
|
|
26
|
+
const description = truncateDescription(item.description);
|
|
27
|
+
console.log(`${number}. ${item.name} - ${description}`);
|
|
28
|
+
});
|
|
29
|
+
// Move to next page
|
|
30
|
+
currentIndex = endIndex;
|
|
31
|
+
// If more results exist, prompt to continue
|
|
32
|
+
if (currentIndex < items.length) {
|
|
33
|
+
console.log('');
|
|
34
|
+
const answer = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'confirm',
|
|
37
|
+
name: 'showMore',
|
|
38
|
+
message: 'Show more?',
|
|
39
|
+
default: true,
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
if (!answer.showMore) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
console.log('');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Truncate description to 80 characters if needed
|
|
51
|
+
*/
|
|
52
|
+
function truncateDescription(description, maxLength = 80) {
|
|
53
|
+
if (description.length <= maxLength) {
|
|
54
|
+
return description;
|
|
55
|
+
}
|
|
56
|
+
return description.substring(0, maxLength - 3) + '...';
|
|
57
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform API server data to internal RegistryEntry format
|
|
3
|
+
* Returns an array because one server can have multiple package types
|
|
4
|
+
* Filters out remote-only servers (no local packages)
|
|
5
|
+
*/
|
|
6
|
+
export function transformToRegistryEntries(apiServer) {
|
|
7
|
+
const entries = [];
|
|
8
|
+
// Skip servers with no packages or empty packages array
|
|
9
|
+
if (!apiServer.packages || apiServer.packages.length === 0) {
|
|
10
|
+
return entries;
|
|
11
|
+
}
|
|
12
|
+
// Transform each package to a RegistryEntry
|
|
13
|
+
for (const pkg of apiServer.packages) {
|
|
14
|
+
// Only support npm and oci package types
|
|
15
|
+
if (pkg.registryType !== 'npm' && pkg.registryType !== 'oci') {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const entry = {
|
|
19
|
+
name: apiServer.name,
|
|
20
|
+
description: apiServer.description || '',
|
|
21
|
+
runtime: transformRuntime(pkg.registryType),
|
|
22
|
+
args: transformArgs(pkg.registryType, pkg.identifier),
|
|
23
|
+
env: transformEnvVars(pkg.environmentVariables || []),
|
|
24
|
+
tags: [],
|
|
25
|
+
};
|
|
26
|
+
entries.push(entry);
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Transform registryType to runtime command
|
|
32
|
+
*/
|
|
33
|
+
function transformRuntime(registryType) {
|
|
34
|
+
switch (registryType) {
|
|
35
|
+
case 'npm':
|
|
36
|
+
return 'npx';
|
|
37
|
+
case 'oci':
|
|
38
|
+
return 'docker';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Transform package identifier to command arguments
|
|
43
|
+
*/
|
|
44
|
+
function transformArgs(registryType, identifier) {
|
|
45
|
+
switch (registryType) {
|
|
46
|
+
case 'npm':
|
|
47
|
+
return ['-y', identifier];
|
|
48
|
+
case 'oci':
|
|
49
|
+
return ['run', '-i', '--rm', identifier];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Transform API environment variables to RegistryEntry format
|
|
54
|
+
*/
|
|
55
|
+
function transformEnvVars(apiEnvVars) {
|
|
56
|
+
return apiEnvVars.map(env => ({
|
|
57
|
+
name: env.name,
|
|
58
|
+
description: env.description,
|
|
59
|
+
required: env.isRequired || false,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if server has local packages (npm or oci)
|
|
64
|
+
*/
|
|
65
|
+
export function hasLocalPackages(apiServer) {
|
|
66
|
+
if (!apiServer.packages || apiServer.packages.length === 0) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Check if any package is npm or oci
|
|
70
|
+
return apiServer.packages.some(pkg => pkg.registryType === 'npm' || pkg.registryType === 'oci');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Filter out servers that only offer remote transport (HTTP/SSE)
|
|
74
|
+
* Returns only servers with local packages (npm/oci)
|
|
75
|
+
*/
|
|
76
|
+
export function filterRemoteOnlyServers(apiServers) {
|
|
77
|
+
return apiServers.filter(hasLocalPackages);
|
|
78
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
export function readJsonFile(filePath) {
|
|
5
|
+
if (!fs.existsSync(filePath)) {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
10
|
+
return JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new Error(`Failed to parse JSON from ${filePath}: ${error}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function writeJsonFile(filePath, data) {
|
|
17
|
+
const dir = path.dirname(filePath);
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(`Failed to write JSON to ${filePath}: ${error}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function expandHomeDir(filePath) {
|
|
29
|
+
if (filePath.startsWith('~/')) {
|
|
30
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
31
|
+
}
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|