mcp-installer

by anaisbetts
Verified
#!/usr/bin/env node 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 * as os from "os"; import * as fs from "fs"; import * as path from "path"; import { spawnPromise } from "spawn-rx"; const server = new Server( { name: "mcp-installer", version: "0.5.0", }, { capabilities: { tools: {}, }, } ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "install_repo_mcp_server", description: "Install an MCP server via npx or uvx", inputSchema: { type: "object", properties: { name: { type: "string", description: "The package name of the MCP server", }, args: { type: "array", items: { type: "string" }, description: "The arguments to pass along", }, env: { type: "array", items: { type: "string" }, description: "The environment variables to set, delimited by =", }, }, required: ["name"], }, }, { name: "install_local_mcp_server", description: "Install an MCP server whose code is cloned locally on your computer", inputSchema: { type: "object", properties: { path: { type: "string", description: "The path to the MCP server code cloned on your computer", }, args: { type: "array", items: { type: "string" }, description: "The arguments to pass along", }, env: { type: "array", items: { type: "string" }, description: "The environment variables to set, delimited by =", }, }, required: ["path"], }, }, ], }; }); async function hasNodeJs() { try { await spawnPromise("node", ["--version"]); return true; } catch (e) { return false; } } async function hasUvx() { try { await spawnPromise("uvx", ["--version"]); return true; } catch (e) { return false; } } async function isNpmPackage(name: string) { try { await spawnPromise("npm", ["view", name, "version"]); return true; } catch (e) { return false; } } function installToClaudeDesktop( name: string, cmd: string, args: string[], env?: string[] ) { const configPath = process.platform === "win32" ? path.join( os.homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json" ) : path.join( os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json" ); let config: any; try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch (e) { config = {}; } const envObj = (env ?? []).reduce((acc, val) => { const [key, value] = val.split("="); acc[key] = value; return acc; }, {} as Record<string, string>); const newServer = { command: cmd, args: args, ...(env ? { env: envObj } : {}), }; const mcpServers = config.mcpServers ?? {}; mcpServers[name] = newServer; config.mcpServers = mcpServers; fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } function installRepoWithArgsToClaudeDesktop( name: string, npmIfTrueElseUvx: boolean, args?: string[], env?: string[] ) { // If the name is in a scoped package, we need to remove the scope const serverName = /^@.*\//i.test(name) ? name.split("/")[1] : name; installToClaudeDesktop( serverName, npmIfTrueElseUvx ? "npx" : "uvx", [name, ...(args ?? [])], env ); } async function attemptNodeInstall( directory: string ): Promise<Record<string, string>> { await spawnPromise("npm", ["install"], { cwd: directory }); // Run down package.json looking for bins const pkg = JSON.parse( fs.readFileSync(path.join(directory, "package.json"), "utf-8") ); if (pkg.bin) { return Object.keys(pkg.bin).reduce((acc, key) => { acc[key] = path.resolve(directory, pkg.bin[key]); return acc; }, {} as Record<string, string>); } if (pkg.main) { return { [pkg.name]: path.resolve(directory, pkg.main) }; } return {}; } async function installLocalMcpServer( dirPath: string, args?: string[], env?: string[] ) { if (!fs.existsSync(dirPath)) { return { content: [ { type: "text", text: `Path ${dirPath} does not exist locally!`, }, ], isError: true, }; } if (fs.existsSync(path.join(dirPath, "package.json"))) { const servers = await attemptNodeInstall(dirPath); Object.keys(servers).forEach((name) => { installToClaudeDesktop( name, "node", [servers[name], ...(args ?? [])], env ); }); return { content: [ { type: "text", text: `Installed the following servers via npm successfully! ${Object.keys( servers ).join(";")} Tell the user to restart the app`, }, ], }; } return { content: [ { type: "text", text: `Can't figure out how to install ${dirPath}`, }, ], isError: true, }; } async function installRepoMcpServer( name: string, args?: string[], env?: string[] ) { if (!(await hasNodeJs())) { return { content: [ { type: "text", text: `Node.js is not installed, please install it!`, }, ], isError: true, }; } if (await isNpmPackage(name)) { installRepoWithArgsToClaudeDesktop(name, true, args, env); return { content: [ { type: "text", text: "Installed MCP server via npx successfully! Tell the user to restart the app", }, ], }; } if (!(await hasUvx())) { return { content: [ { type: "text", text: `Python uv is not installed, please install it! Tell users to go to https://docs.astral.sh/uv`, }, ], isError: true, }; } installRepoWithArgsToClaudeDesktop(name, false, args, env); return { content: [ { type: "text", text: "Installed MCP server via uvx successfully! Tell the user to restart the app", }, ], }; } server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (request.params.name === "install_repo_mcp_server") { const { name, args, env } = request.params.arguments as { name: string; args?: string[]; env?: string[]; }; return await installRepoMcpServer(name, args, env); } if (request.params.name === "install_local_mcp_server") { const dirPath = request.params.arguments!.path as string; const { args, env } = request.params.arguments as { args?: string[]; env?: string[]; }; return await installLocalMcpServer(dirPath, args, env); } throw new Error(`Unknown tool: ${request.params.name}`); } catch (err) { return { content: [ { type: "text", text: `Error setting up package: ${err}`, }, ], isError: true, }; } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); } runServer().catch(console.error);