@mcpmarket/mcp-auto-install
Version:
MCP server that helps install other MCP servers automatically
569 lines (562 loc) • 23.7 kB
JavaScript
import { promises as fs } from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';
import { exec as execCb } from 'node:child_process';
import { promisify } from 'node:util';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { npxFinder } from 'npx-scope-finder';
import { z } from 'zod';
import { createErrorResponse, createSuccessResponse, createServerResponse, } from './utils/response.js';
const exec = promisify(execCb);
// Set path
const SETTINGS_PATH = process.env.MCP_REGISTRY_PATH
? process.env.MCP_REGISTRY_PATH
: path.join(homedir(), 'mcp', 'mcp-registry.json');
// Default package scopes
const DEFAULT_PACKAGE_SCOPES = ['@modelcontextprotocol'];
// Parse package scopes from environment variable
const PACKAGE_SCOPES = process.env.MCP_PACKAGE_SCOPES
? process.env.MCP_PACKAGE_SCOPES.split(',')
.map(scope => scope.trim())
.filter(Boolean)
: DEFAULT_PACKAGE_SCOPES;
// Server settings
let serverSettings = { servers: [] };
/**
* Simple Zod to JSON Schema conversion function
*/
function simpleZodToJsonSchema(schema) {
// For simplicity, we only handle basic types
if (schema instanceof z.ZodString) {
return { type: 'string' };
}
if (schema instanceof z.ZodNumber) {
return { type: 'number' };
}
if (schema instanceof z.ZodBoolean) {
return { type: 'boolean' };
}
if (schema instanceof z.ZodArray) {
return {
type: 'array',
items: simpleZodToJsonSchema(schema._def.type),
};
}
if (schema instanceof z.ZodObject) {
const properties = {};
const required = [];
for (const [key, value] of Object.entries(schema.shape)) {
properties[key] = simpleZodToJsonSchema(value);
if (!(value instanceof z.ZodOptional)) {
required.push(key);
}
}
return {
type: 'object',
properties,
required,
};
}
if (schema instanceof z.ZodOptional) {
return simpleZodToJsonSchema(schema._def.innerType);
}
// Default return
return { type: 'object' };
}
/**
* Preload MCP package information to local registry file
*/
async function preloadMCPPackages() {
try {
// Get all available packages from configured scopes concurrently
const scopePromises = PACKAGE_SCOPES.map(scope => npxFinder(scope, {
timeout: 15000,
retries: 3,
retryDelay: 1000,
}).catch(error => {
console.error(`Error fetching packages for scope ${scope}:`, error);
return []; // Return empty array on error to continue processing
}));
const results = await Promise.allSettled(scopePromises);
const allPackages = results.reduce((acc, result) => {
if (result.status === 'fulfilled') {
acc.push(...result.value);
}
return acc;
}, []);
// Filter and process package information
for (const pkg of allPackages) {
if (!pkg.name || pkg.name === '@modelcontextprotocol/sdk') {
continue; // Skip SDK itself
}
try {
// Extract server type (from package name)
const nameParts = pkg.name.split('/');
const serverName = nameParts[nameParts.length - 1];
const serverType = serverName.replace('mcp-', '');
// Build server information
const serverInfo = {
name: pkg.name,
repo: pkg.links?.repository || '',
command: `npx ${pkg.name}`,
description: pkg.description || `MCP ${serverType} server`,
keywords: [...(pkg.keywords || []), serverType, 'mcp'],
};
// Get README content directly from npxFinder returned data and add to serverInfo
if (pkg.original?.readme) {
serverInfo.readme = pkg.original.readme;
}
// Check if server is already registered
const existingServer = serverSettings.servers.find(s => s.name === pkg.name);
if (!existingServer) {
serverSettings.servers.push(serverInfo);
}
else {
// Update existing server's readme (if available)
if (serverInfo.readme && !existingServer.readme) {
existingServer.readme = serverInfo.readme;
}
}
}
catch (pkgError) {
console.error(`Error processing package ${pkg.name}:`, pkgError);
// Silently handle package errors
}
}
// Save updated settings
await saveSettings();
}
catch (error) {
console.error('Error preloading MCP packages:', error);
// Silently handle errors
}
}
// Create MCP server instance
/**
* Initialize settings
*/
async function initSettings() {
try {
// Create settings directory
const settingsDir = path.dirname(SETTINGS_PATH);
await fs.mkdir(settingsDir, { recursive: true });
// Try to load existing settings
try {
const data = await fs.readFile(SETTINGS_PATH, 'utf-8');
serverSettings = JSON.parse(data);
}
catch (error) {
console.error(error);
// If file doesn't exist, use default settings
serverSettings = { servers: [] };
// Save default settings
await saveSettings();
}
}
catch (error) {
console.error('Failed to initialize settings:', error);
}
}
/**
* Save settings
*/
async function saveSettings() {
try {
// Ensure directory exists
const settingsDir = path.dirname(SETTINGS_PATH);
await fs.mkdir(settingsDir, { recursive: true });
// Save settings file
await fs.writeFile(SETTINGS_PATH, JSON.stringify(serverSettings, null, 2), 'utf-8');
}
catch (error) {
console.error('Failed to save settings:', error);
throw new Error('Failed to save settings');
}
}
/**
* Find server
*/
async function findServer(name) {
// Ensure settings are loaded
await initSettings();
return serverSettings.servers.find(s => s.name.includes(name.toLowerCase()));
}
const createServer = (jsonOnly = false) => {
console.error('jsonOnly', jsonOnly);
const server = new Server({
name: 'mcp-auto-install',
version: '0.1.8',
}, {
capabilities: {
tools: {},
},
});
// Register tools list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'mcp_auto_install_getAvailableServers',
description: 'List all available MCP servers that can be installed. Returns a list of server names and their basic information. Use this to discover what MCP servers are available before installing or configuring them.',
inputSchema: simpleZodToJsonSchema(z.object({
random_string: z.string().describe('Dummy parameter for no-parameter tools'),
})),
},
{
name: 'mcp_auto_install_removeServer',
description: "Remove a registered MCP server from the local registry. This will unregister the server but won't uninstall it. Provide the exact server name to remove. Use getAvailableServers first to see registered servers.",
inputSchema: simpleZodToJsonSchema(z.object({
serverName: z
.string()
.describe('The exact name of the server to remove from registry'),
})),
},
{
name: 'mcp_auto_install_configureServer',
description: 'Get detailed configuration help for a specific MCP server. Provides README content, configuration instructions, and suggested commands. Optionally specify a purpose or specific configuration question.',
inputSchema: simpleZodToJsonSchema(z.object({
serverName: z.string().describe('The exact name of the server to configure'),
})),
},
{
name: 'mcp_auto_install_saveCommand',
description: 'Save an npx command configuration for an MCP server. This stores the command, arguments and environment variables in both the MCP settings and LLM configuration files. Use this to persist server-specific command configurations.',
inputSchema: simpleZodToJsonSchema(z.object({
serverName: z
.string()
.describe('The exact name of the server to save command configuration for'),
command: z
.string()
.describe("The main command to execute (e.g., 'npx', 'node', 'npm', 'yarn', 'pnpm','cmd', 'powershell', 'bash', 'sh', 'zsh', 'fish', 'tcsh', 'csh', 'cmd', 'powershell', 'pwsh', 'cmd.exe', 'powershell.exe', 'cmd.ps1', 'powershell.ps1')"),
args: z
.array(z.string())
.describe("Array of command arguments (e.g., ['--port', '3000', '--config', 'config.json'])"),
env: z
.record(z.string())
.describe("Environment variables object for the command (e.g., { 'NODE_ENV': 'production', 'DEBUG': 'true' })")
.optional(),
description: z
.string()
.describe('A description of the functionality and purpose of the server to which the command configuration needs to be saved'),
})),
},
// {
// name: 'mcp_auto_install_parseJsonConfig',
// description:
// 'Parse and validate a JSON configuration string for MCP servers. This tool processes server configurations, validates their format, and merges them with existing configurations. Use this for bulk server configuration.',
// inputSchema: simpleZodToJsonSchema(
// z.object({
// config: z
// .string()
// .describe(
// "JSON string containing server configurations in the format: { 'mcpServers': { 'serverName': { 'command': 'string', 'args': ['string'] } } }",
// ),
// }),
// ),
// },
],
};
});
// Register tool call handler
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args = {} } = req.params;
switch (name) {
case 'mcp_auto_install_getAvailableServers': {
const servers = await getRegisteredServers();
return {
content: [
{
type: 'text',
text: `📋 Found ${servers.length} MCP servers`,
},
{
type: 'text',
text: servers
.map(s => `• ${s.name}${s.description ? `: ${s.description}` : ''}`)
.join('\n'),
},
],
success: true,
};
}
case 'mcp_auto_install_removeServer': {
const result = await handleRemoveServer(args);
return createServerResponse(result, false);
}
case 'mcp_auto_install_configureServer': {
const result = await handleConfigureServer(args);
return createServerResponse(result, false);
}
case 'mcp_auto_install_saveCommand': {
const result = await saveCommandToExternalConfig(args.serverName, args.command, args.args, args.description, jsonOnly, args.env);
return createServerResponse(result, jsonOnly);
}
// case 'mcp_auto_install_parseJsonConfig': {
// const result = await handleParseConfig(args as unknown as { config: string }, jsonOnly);
// return createServerResponse(result, jsonOnly);
// }
default:
throw new Error(`Unknown tool: ${name}`);
}
});
return server;
};
/**
* Start MCP server
*/
export async function startServer(jsonOnly = false) {
console.error('Initializing MCP server...');
await initSettings();
console.error('Loading MCP packages...');
await preloadMCPPackages();
const server = createServer(jsonOnly);
console.error('Connecting to transport...');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server started and ready');
}
/**
* Get list of registered servers
*/
export async function getRegisteredServers() {
// Ensure settings are loaded
await initSettings();
return serverSettings.servers;
}
/**
* The following are interface functions for CLI tools
*/
export async function handleInstallServer(args) {
const { serverName } = args;
const server = await findServer(serverName);
if (!server) {
return createErrorResponse(`❌ Server '${serverName}' not found. Use 'getAvailableServers' to see options.`);
}
try {
// Install using git clone
const repoName = server.repo.split('/').pop()?.replace('.git', '') || serverName;
const cloneDir = path.join(homedir(), '.mcp', 'servers', repoName);
// Create directory
await fs.mkdir(path.join(homedir(), '.mcp', 'servers'), {
recursive: true,
});
// Clone repository
await exec(`git clone ${server.repo} ${cloneDir}`);
// Install dependencies
await exec(`cd ${cloneDir} && npm install`);
if (server.installCommands && server.installCommands.length > 0) {
// Run custom installation commands
for (const cmd of server.installCommands) {
await exec(`cd ${cloneDir} && ${cmd}`);
}
}
return createSuccessResponse(`✅ Installed '${serverName}'. Path: ${cloneDir}`, {
installPath: cloneDir,
serverName: server.name,
description: server.description,
});
}
catch (error) {
return createErrorResponse(`⚠️ Install failed: ${error.message}`);
}
}
export async function handleRegisterServer(serverInfo) {
// Check if server already exists
const existingIndex = serverSettings.servers.findIndex(s => s.name === serverInfo.name);
if (existingIndex !== -1) {
// Update existing server
serverSettings.servers[existingIndex] = serverInfo;
}
else {
// Add new server
serverSettings.servers.push(serverInfo);
}
// Save updated settings
await saveSettings();
const action = existingIndex !== -1 ? 'updated' : 'registered';
return createSuccessResponse(`✅ Server '${serverInfo.name}' ${action} successfully.`);
}
export async function handleRemoveServer(args) {
const { serverName } = args;
const initialLength = serverSettings.servers.length;
// Remove specified server
serverSettings.servers = serverSettings.servers.filter(s => s.name !== serverName);
if (serverSettings.servers.length === initialLength) {
return createErrorResponse(`❌ Server '${serverName}' not found.`);
}
// Save updated settings
await saveSettings();
return createSuccessResponse(`✅ Server '${serverName}' removed.`);
}
export async function handleConfigureServer(args) {
const { serverName } = args;
const server = await findServer(serverName);
if (!server) {
return createErrorResponse(`❌ Server '${serverName}' not found.`);
}
// Get README content
const readmeResult = await handleGetServerReadme({ serverName });
if (!readmeResult.message || !readmeResult.data) {
return createErrorResponse(`⚠️ README not available: ${readmeResult.message?.[0] || 'Unknown error'}`);
}
const messages = [
`📝 Config guide for '${serverName}'`,
server.description ? `Description: ${server.description}` : '',
readmeResult.data,
];
return createSuccessResponse(messages);
}
/**
* Get server README content
*/
export async function handleGetServerReadme(args) {
const { serverName } = args;
const server = await findServer(serverName);
if (!server) {
return createErrorResponse(`❌ Server '${serverName}' not found in the registry.`);
}
try {
// Get README content (directly from server object)
const readmeContent = server.readme || 'No README content available for this server.';
// Add prompts to guide LLM in summarizing content and guiding parameter configuration
const promptedReadme = `# ${serverName} README
${server.description ? `\n> ${server.description}\n` : ''}
${readmeContent}
---
<CURRENT_CURSOR_POSITION>
Summary: What does this MCP server do? (1-2 sentences)
Setup: List required/optional parameters, env vars needed.
Examples:
- Working npx command example
- JSON config example for integration
Next steps: How to get started quickly?
Note any unclear/missing information.
`;
return createSuccessResponse('README fetch successful', promptedReadme);
}
catch (error) {
return createErrorResponse(`⚠️ Failed to fetch README: ${error.message}`);
}
}
/**
* Handle user configuration parsing
*/
export async function handleParseConfig(args, jsonOnly = false) {
try {
// Parse the JSON string sent by the user
const userConfig = JSON.parse(args.config);
// Ensure mcpServers field exists
if (!userConfig.mcpServers) {
userConfig.mcpServers = {};
}
// Validate each server's configuration format
for (const [serverName, serverConfig] of Object.entries(userConfig.mcpServers)) {
const config = serverConfig;
// Validate required fields
if (!config.command || !Array.isArray(config.args)) {
return createErrorResponse(`❌ Invalid config for '${serverName}'. Require 'command' and 'args' fields.`);
}
}
// If jsonOnly is true, just return the parsed config without saving
if (jsonOnly) {
return createSuccessResponse('✅ Config parsed', userConfig);
}
// Save configuration to external file
const externalConfigPath = process.env.MCP_SETTINGS_PATH;
if (!externalConfigPath) {
return createErrorResponse('❌ MCP_SETTINGS_PATH not set. Set this to save config.');
}
// Read existing configuration (if any)
let existingConfig = {};
try {
const existingData = await fs.readFile(externalConfigPath, 'utf-8');
existingConfig = JSON.parse(existingData);
}
catch (error) {
return createErrorResponse(`⚠️ Parse error: ${error.message}`);
}
// Merge configurations
const mergedConfig = {
...existingConfig,
mcpServers: {
...(existingConfig.mcpServers || {}),
...userConfig.mcpServers,
},
};
// Save merged configuration
await fs.writeFile(externalConfigPath, JSON.stringify(mergedConfig, null, 2), 'utf-8');
return createSuccessResponse('✅ Config saved');
}
catch (error) {
return createErrorResponse(`⚠️ Parse error: ${error.message}`);
}
}
/**
* Save command to external configuration file (e.g., Claude's configuration file)
* @param serverName MCP server name
* @param command User input command, e.g., "npx @modelcontextprotocol/server-name --arg1 value1 --arg2 value2"
* @param args Array of command arguments, e.g., ['--port', '3000', '--config', 'config.json']
* @param env Environment variables object for the command, e.g., { 'NODE_ENV': 'production', 'DEBUG': 'true' }
* @param jsonOnly If true, only return the command configuration without saving to files
* @param description Optional description for the command
* @returns Operation result
*/
export async function saveCommandToExternalConfig(serverName, command, args, description, jsonOnly = false, env) {
try {
if (!command) {
return createErrorResponse('❌ Command cannot be empty');
}
// Check if server exists (in our MCP server registry)
const server = await findServer(serverName);
if (!server) {
return createErrorResponse(`❌ Server '${serverName}' not found`);
}
// Create command configuration
const commandConfig = {
name: server?.name || serverName,
command,
args,
env: env || {},
description: description || server.description || '',
};
// If jsonOnly is true, just return the configuration without saving
if (jsonOnly) {
return createSuccessResponse('✅ Command config generated', commandConfig);
}
// Check environment variable - points to LLM (e.g., Claude) config file path
const externalConfigPath = process.env.MCP_SETTINGS_PATH;
if (!externalConfigPath) {
return createErrorResponse('❌ MCP_SETTINGS_PATH not set. Please set it to your LLM config path.');
}
try {
// Read external LLM configuration file
const configData = await fs.readFile(externalConfigPath, 'utf-8');
const config = JSON.parse(configData);
// Ensure mcpServers field exists
if (!config.mcpServers) {
config.mcpServers = {};
}
// Add/update server configuration to LLM config file
config.mcpServers[serverName] = commandConfig;
// Save configuration to LLM config file
await fs.writeFile(externalConfigPath, JSON.stringify(config, null, 2), 'utf-8');
// Also update internal server configuration - save to our MCP server registry
server.commandConfig = commandConfig;
// Update server description if provided
if (description) {
server.description = description;
}
await saveSettings();
return createSuccessResponse(`✅ Command saved for '${serverName}'`);
}
catch (error) {
return createErrorResponse(`⚠️ Config file error: ${error.message}`);
}
}
catch (error) {
return createErrorResponse(`⚠️ Command save error: ${error.message}`);
}
}