Skip to content

Commit

Permalink
Some final changes
Browse files Browse the repository at this point in the history
  • Loading branch information
michaellatman committed Nov 27, 2024
1 parent 0aa035e commit f7e41d8
Show file tree
Hide file tree
Showing 8 changed files with 458 additions and 124 deletions.
40 changes: 40 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,45 @@ export const packageHelpers: PackageHelpers = {
required: true
}
}
},
'@modelcontextprotocol/server-github': {
requiredEnvVars: {
GITHUB_PERSONAL_ACCESS_TOKEN: {
description: 'Personal access token for GitHub API access',
required: true
}
}
},
'@modelcontextprotocol/server-gitlab': {
requiredEnvVars: {
GITLAB_PERSONAL_ACCESS_TOKEN: {
description: 'Personal access token for GitLab API access',
required: true
},
GITLAB_API_URL: {
description: 'GitLab API URL (optional, for self-hosted instances)',
required: false
}
}
},
'@modelcontextprotocol/server-google-maps': {
requiredEnvVars: {
GOOGLE_MAPS_API_KEY: {
description: 'API key for Google Maps services',
required: true
}
}
},
'@modelcontextprotocol/server-slack': {
requiredEnvVars: {
SLACK_BOT_TOKEN: {
description: 'Slack Bot User OAuth Token (starts with xoxb-)',
required: true
},
SLACK_TEAM_ID: {
description: 'Slack Team/Workspace ID',
required: true
}
}
}
};
70 changes: 50 additions & 20 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import inquirer from 'inquirer';
import { exec } from 'child_process';
import { promisify } from 'util';
import { packageHelpers } from './helpers';
import chalk from 'chalk';

const execAsync = promisify(exec);

Expand All @@ -21,46 +22,75 @@ async function handlePackageHelper(packageName: string): Promise<Record<string,
if (!helper?.requiredEnvVars) return undefined;

const envVars: Record<string, string> = {};
console.log(chalk.cyan('\nEnvironment Variable Configuration:'));

// First check if env vars are needed and get user confirmation
console.log(chalk.yellow('\nThis package requires configuration of environment variables.'));
const { proceed } = await inquirer.prompt<{ proceed: boolean }>([{
type: 'confirm',
name: 'proceed',
message: 'Would you like to configure them now?',
default: true
}]);

if (!proceed) {
console.log(chalk.yellow('\nInstallation cancelled. Package requires environment configuration.'));
process.exit(0);
}

for (const [envVar, config] of Object.entries(helper.requiredEnvVars)) {
const envConfig = config as { description: string; required: boolean };
const envConfig = config as { description: string; required: boolean; default?: string };
const existingValue = process.env[envVar];

console.log(chalk.gray(`\n${envVar}:`));
console.log(chalk.gray(`Description: ${envConfig.description}`));
if (envConfig.default) {
console.log(chalk.gray(`Default: ${envConfig.default}`));
}

if (existingValue) {
const { useExisting } = await inquirer.prompt<{ useExisting: boolean }>([{
type: 'confirm',
name: 'useExisting',
message: `Found existing ${envVar} in your environment. Would you like to use it?`,
message: `Found existing ${envVar} in environment. Use this value?`,
default: true
}]);

if (useExisting) {
envVars[envVar] = existingValue;
console.log(chalk.green(`Using existing ${envVar}`));
continue;
}
}

if (envConfig.required) {
const { value, configure } = await inquirer.prompt([
{
type: 'confirm',
name: 'configure',
message: `${envVar} is required for ${packageName}. Would you like to configure it now?`,
default: true
},
{
type: 'input',
name: 'value',
message: `Please enter your ${envVar} (${envConfig.description}):`,
when: (answers) => answers.configure
}
]);
const { configure } = await inquirer.prompt([{
type: 'confirm',
name: 'configure',
message: `Would you like to configure ${envVar}${envConfig.required ? ' (required)' : ' (optional)'}?`,
default: envConfig.required
}]);

if (configure && value) {
if (configure) {
const { value } = await inquirer.prompt([{
type: 'input',
name: 'value',
message: `Enter value for ${envVar}:`,
default: envConfig.default,
validate: (input) => {
if (envConfig.required && !input) {
return `${envVar} is required`;
}
return true;
}
}]);

if (value) {
envVars[envVar] = value;
} else if (envConfig.required) {
console.log(`\nSkipping ${envVar} configuration. You'll need to set it in your environment before using ${packageName}.`);
console.log(chalk.green(`✓ ${envVar} configured`));
}
} else if (envConfig.required) {
console.log(chalk.yellow(`\n⚠️ Warning: ${envVar} is required but not configured. You'll need to set it manually.`));
process.exit(0);
}
}

Expand Down
143 changes: 106 additions & 37 deletions src/installed.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,127 @@
import inquirer from 'inquirer';
import { readConfig, writeConfig } from './utils/config';
import { formatPackageInfo } from './utils/display';
import chalk from 'chalk';
import { readConfig } from './utils/config';
import { displayPackageDetailsWithActions } from './utils/display';
import { uninstallPackage } from './utils/package-management';
import { Package } from './types';
import { readFileSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import AutocompletePrompt from 'inquirer-autocomplete-prompt';
import fuzzy from 'fuzzy';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageListPath = join(__dirname, '../packages/package-list.json');

inquirer.registerPrompt('autocomplete', AutocompletePrompt);

export async function listInstalledPackages(): Promise<void> {
// Read full package list
const allPackages: Package[] = JSON.parse(readFileSync(packageListPath, 'utf-8'));

// Get installed packages from config
const config = readConfig();
const installedServers = config.mcpServers || {};
const serverNames = Object.keys(installedServers);

if (serverNames.length === 0) {
console.log('No MCP servers are currently installed.');
console.log(chalk.yellow('\nNo MCP servers are currently installed.'));
return;
}

console.log('Installed MCP servers:\n');
serverNames.forEach(name => {
console.log(`- ${name}`);
});
// Filter for only installed packages
const installedPackages = allPackages.filter(pkg =>
serverNames.includes(pkg.name.replace(/\//g, '-'))
);

console.log(chalk.bold.cyan('\n📦 Installed Packages'));
console.log(chalk.gray(`Found ${installedPackages.length} installed packages\n`));

// Prepare choices for inquirer using table-like format
const choices = installedPackages.map(pkg => ({
name: `${pkg.name.padEnd(24)}${
pkg.description.length > 47 ? `${pkg.description.slice(0, 44)}...` : pkg.description.padEnd(49)
}${pkg.vendor.padEnd(19)}${pkg.license.padEnd(14)}`,
value: pkg,
short: pkg.name
}));

const { action } = await inquirer.prompt([
const answer = await inquirer.prompt<{ selectedPackage: Package }>([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: 'Uninstall a server', value: 'uninstall' },
{ name: 'Exit', value: 'exit' }
]
type: 'autocomplete',
name: 'selectedPackage',
message: 'Search and select a package:',
source: async (_answersSoFar: any, input: string) => {
if (!input) return choices;

return fuzzy
.filter(input.toLowerCase(), choices, {
extract: (choice) => `${choice.value.name} ${choice.value.description} ${choice.value.vendor}`.toLowerCase()
})
.map(result => result.original);
},
pageSize: 10
}
]);

if (action === 'uninstall') {
const { packageToUninstall } = await inquirer.prompt([
{
type: 'list',
name: 'packageToUninstall',
message: 'Select a server to uninstall:',
choices: serverNames
const displayPackages = answer.selectedPackage ? [answer.selectedPackage] : installedPackages;

if (displayPackages.length === 0) {
console.log(chalk.yellow('\nNo packages found matching your search.'));
return;
}

console.log(chalk.bold.white(`\nShowing ${displayPackages.length} package(s):`));
displayPackages.forEach(displayPackageDetailsWithActions);

if (displayPackages.length === 1) {
const pkg = displayPackages[0];
await handleSelectedPackage(pkg);
}
}

async function handleSelectedPackage(pkg: Package) {
const action = await displayPackageDetailsWithActions(pkg);

switch (action) {
case 'uninstall':
const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([
{
type: 'confirm',
name: 'confirmUninstall',
message: `Are you sure you want to uninstall ${pkg.name}?`,
default: false
}
]);

if (confirmUninstall) {
await uninstallPackage(pkg.name);
console.log(chalk.green(`Successfully uninstalled ${pkg.name}`));
// Return to installed packages list
await listInstalledPackages();
} else {
console.log('Uninstallation cancelled.');
// Show actions again
await handleSelectedPackage(pkg);
}
]);

const { confirmUninstall } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmUninstall',
message: `Are you sure you want to uninstall ${packageToUninstall}?`,
default: false
break;
case 'open':
if (pkg.sourceUrl) {
const open = (await import('open')).default;
await open(pkg.sourceUrl);
console.log(chalk.green(`\nOpened ${pkg.sourceUrl} in your browser`));
} else {
console.log(chalk.yellow('\nNo source URL available for this package'));
}
]);

if (confirmUninstall) {
await uninstallPackage(packageToUninstall);
} else {
console.log('Uninstallation cancelled.');
}
// Show actions again after opening URL
await handleSelectedPackage(pkg);
break;
case 'back':
await listInstalledPackages();
return;
case 'exit':
process.exit(0);
}
}
Loading

0 comments on commit f7e41d8

Please sign in to comment.