Skip to content

Commit

Permalink
feat: add uninstall command and env var configuration
Browse files Browse the repository at this point in the history
- Add uninstall command to remove MCP packages
- Implement environment variable configuration for packages
- Add automatic Claude desktop app restart functionality
- Add dotenv dependency for environment variable management
- Update documentation with uninstall instructions
- Add package helper types for configuration management
- Improve package installation with user prompts and validation
  • Loading branch information
michaellatman committed Nov 27, 2024
1 parent fa56cf2 commit 0aa035e
Show file tree
Hide file tree
Showing 13 changed files with 356 additions and 24 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ npx @michaellatman/mcp-get list
```

This will display a list of all available packages with their details.

## Uninstall a Package

To uninstall a package using `mcp-get`, run the following command:

```
npx @michaellatman/mcp-get uninstall {PACKAGE}
```

Replace `{PACKAGE}` with the name of the package you want to uninstall.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
"start": "node dist/index.js",
"publish": "npm publish",
"test:list": "node --loader ts-node/esm src/index.ts list",
"test:install": "ts-node src/index.ts install",
"extract": "node --loader ts-node/esm src/extractors/modelcontextprotocol-extractor.ts"
"test:install": "node --loader ts-node/esm src/index.ts install",
"test:installed": "node --loader ts-node/esm src/index.ts installed",
"extract": "node --loader ts-node/esm src/extractors/modelcontextprotocol-extractor.ts",
"test:uninstall": "node --loader ts-node/esm src/index.ts uninstall"
},
"bin": {
"mcp-get": "dist/index.js"
},
"dependencies": {
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"dotenv": "^16.4.5",
"fuzzy": "^0.1.3",
"inquirer": "^8.2.4",
"inquirer-autocomplete-prompt": "^2.0.0",
Expand Down
12 changes: 12 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PackageHelpers } from '../types/index.js';

export const packageHelpers: PackageHelpers = {
'@modelcontextprotocol/server-brave-search': {
requiredEnvVars: {
BRAVE_API_KEY: {
description: 'API key for Brave Search',
required: true
}
}
}
};
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { install } from './install.js';
import { list } from './list.js';
import inquirer from 'inquirer';
import autocomplete from 'inquirer-autocomplete-prompt';
import { listInstalledPackages } from './installed';
import { uninstall } from './uninstall.js';

inquirer.registerPrompt('autocomplete', autocomplete);

Expand All @@ -19,10 +21,21 @@ switch (command) {
}
install(packageName);
break;
case 'uninstall':
const pkgToUninstall = args[1];
if (!pkgToUninstall) {
console.error('Please provide a package name to uninstall.');
process.exit(1);
}
uninstall(pkgToUninstall);
break;
case 'ls':
case 'list':
list();
break;
case 'installed':
listInstalledPackages();
break;
default:
console.error(`Unknown command: ${command}`);
process.exit(1);
Expand Down
129 changes: 113 additions & 16 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,106 @@ import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { Package } from './types/index.js';
import { installMCPServer } from './utils/config';
import inquirer from 'inquirer';
import { exec } from 'child_process';
import { promisify } from 'util';
import { packageHelpers } from './helpers';

const execAsync = promisify(exec);

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const packageListPath = join(__dirname, '../packages/package-list.json');

export async function installPackage(packageName: string): Promise<void> {
async function handlePackageHelper(packageName: string): Promise<Record<string, string> | undefined> {
const helper = packageHelpers[packageName];
if (!helper?.requiredEnvVars) return undefined;

const envVars: Record<string, string> = {};

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

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?`,
default: true
}]);

if (useExisting) {
envVars[envVar] = existingValue;
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
}
]);

if (configure && 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}.`);
}
}
}

return Object.keys(envVars).length > 0 ? envVars : undefined;
}

export async function installPackage(pkg: Package | string): Promise<void> {
try {
console.log(`Installing package: ${packageName}`);
console.log(`Description: ${packageName}`);
console.log(`Vendor: ${packageName}`);
console.log(`Source URL: ${packageName}`);
console.log(`License: ${packageName}`);

// Here you can add the logic to download and install the package from the sourceUrl

// After successful installation, update the config
if (packageName.startsWith('@modelcontextprotocol/server-')) {
installMCPServer(packageName);
console.log('Updated Claude desktop configuration');
const packageName = typeof pkg === 'string' ? pkg : pkg.name;

// Handle package-specific configuration and get environment variables
const env = await handlePackageHelper(packageName);

// After successful installation, update the config with env variables
installMCPServer(packageName, env);
console.log('Updated Claude desktop configuration');

// Prompt user about restarting Claude
const { shouldRestart } = await inquirer.prompt<{ shouldRestart: boolean }>([
{
type: 'confirm',
name: 'shouldRestart',
message: 'Would you like to restart the Claude desktop app to apply changes?',
default: true
}
]);

if (shouldRestart) {
console.log('Restarting Claude desktop app...');
try {
// Kill existing Claude process
await execAsync('pkill -x Claude || true');
// Wait 2 seconds before restarting
console.log('Waiting for Claude to close...');
await new Promise(resolve => setTimeout(resolve, 2000));
// Start Claude again
await execAsync('open -a Claude');
console.log('Claude desktop app has been restarted');
} catch (error) {
console.warn('Failed to restart Claude desktop app automatically. Please restart it manually.');
}
} else {
console.log('Please restart the Claude desktop app manually to apply changes.');
}

} catch (error) {
Expand All @@ -37,9 +117,26 @@ export async function install(packageName: string): Promise<void> {

const pkg = packageList.find(p => p.name === packageName);
if (!pkg) {
console.error(`Package ${packageName} not found.`);
process.exit(1);
console.warn(`Package ${packageName} not found in the curated list.`);

const { proceedWithInstall } = await inquirer.prompt<{ proceedWithInstall: boolean }>([
{
type: 'confirm',
name: 'proceedWithInstall',
message: `Would you like to try installing ${packageName} anyway? This package hasn't been verified.`,
default: false
}
]);

if (proceedWithInstall) {
console.log(`Proceeding with installation of ${packageName}...`);
await installPackage(packageName);
} else {
console.log('Installation cancelled.');
process.exit(1);
}
return;
}

await installPackage(pkg.name);
await installPackage(pkg);
}
58 changes: 58 additions & 0 deletions src/installed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import inquirer from 'inquirer';
import { readConfig, writeConfig } from './utils/config';
import { formatPackageInfo } from './utils/display';
import { uninstallPackage } from './utils/package-management';

export async function listInstalledPackages(): Promise<void> {
const config = readConfig();
const installedServers = config.mcpServers || {};
const serverNames = Object.keys(installedServers);

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

console.log('Installed MCP servers:\n');
serverNames.forEach(name => {
console.log(`- ${name}`);
});

const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: 'Uninstall a server', value: 'uninstall' },
{ name: 'Exit', value: 'exit' }
]
}
]);

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

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

if (confirmUninstall) {
await uninstallPackage(packageToUninstall);
} else {
console.log('Uninstallation cancelled.');
}
}
}
2 changes: 1 addition & 1 deletion src/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { Package } from './types/index.js';
import { displayPackageDetails } from './utils/display.js';
import { installPackage } from './install.js';
import { installPackage } from './utils/package-management';
import { createInterface } from 'readline';
import Table from 'cli-table3'; // Import cli-table3
import stringWidth from 'string-width'; // Import string-width
Expand Down
14 changes: 14 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,18 @@ export interface Package {
sourceUrl: string;
homepage: string;
license: string;
}

export interface PackageHelper {
requiredEnvVars?: {
[key: string]: {
description: string;
required: boolean;
}
};
configureEnv?: (config: any) => Promise<void>;
}

export interface PackageHelpers {
[packageName: string]: PackageHelper;
}
25 changes: 25 additions & 0 deletions src/uninstall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import inquirer from 'inquirer';
import { uninstallPackage } from './utils/package-management';

export async function uninstall(packageName: string): Promise<void> {
try {
const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([
{
type: 'confirm',
name: 'confirmUninstall',
message: `Are you sure you want to uninstall ${packageName}?`,
default: false
}
]);

if (confirmUninstall) {
await uninstallPackage(packageName);
console.log(`Successfully uninstalled ${packageName}`);
} else {
console.log('Uninstallation cancelled.');
}
} catch (error) {
console.error('Failed to uninstall package:', error);
throw error;
}
}
Loading

0 comments on commit 0aa035e

Please sign in to comment.