UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

876 lines (875 loc) โ€ข 36.4 kB
/** * MCP CLI Commands for NeuroLink * Implements comprehensive MCP server management commands * Part of Phase 4.2 - MCP CLI Commands */ import { createExternalServerInfo } from "../../lib/utils/mcpDefaults.js"; import { NeuroLink } from "../../lib/neurolink.js"; import { logger } from "../../lib/utils/logger.js"; import chalk from "chalk"; import ora from "ora"; import fs from "fs"; import path from "path"; /** * Popular MCP servers registry */ const POPULAR_MCP_SERVERS = { filesystem: { command: "npx", args: [ "-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files", ], transport: "stdio", description: "File system operations (read, write, create, list directories)", }, github: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}" }, transport: "stdio", description: "GitHub repository management and file operations", }, postgres: { command: "npx", args: ["-y", "@modelcontextprotocol/server-postgres"], env: { DATABASE_URL: "${DATABASE_URL}" }, transport: "stdio", description: "PostgreSQL database query and management", }, sqlite: { command: "npx", args: ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/database.db"], transport: "stdio", description: "SQLite database operations and queries", }, brave: { command: "npx", args: ["-y", "@modelcontextprotocol/server-brave-search"], env: { BRAVE_API_KEY: "${BRAVE_API_KEY}" }, transport: "stdio", description: "Brave Search API for web search capabilities", }, puppeteer: { command: "npx", args: ["-y", "@modelcontextprotocol/server-puppeteer"], transport: "stdio", description: "Web scraping and browser automation", }, git: { command: "npx", args: ["-y", "@modelcontextprotocol/server-git"], transport: "stdio", description: "Git repository operations and version control", }, memory: { command: "npx", args: ["-y", "@modelcontextprotocol/server-memory"], transport: "stdio", description: "Persistent memory and knowledge storage", }, bitbucket: { command: "npx", args: ["-y", "@nexus2520/bitbucket-mcp-server"], env: { BITBUCKET_USERNAME: "${BITBUCKET_USERNAME}", BITBUCKET_TOKEN: "${BITBUCKET_TOKEN}", BITBUCKET_BASE_URL: "${BITBUCKET_BASE_URL}", }, transport: "stdio", description: "Bitbucket repository management and development workflows", }, }; /** * MCP CLI command factory */ export class MCPCommandFactory { /** * Create the main MCP command with subcommands */ static createMCPCommands() { return { command: "mcp <subcommand>", describe: "Manage Model Context Protocol (MCP) servers", builder: (yargs) => { return yargs .command("list", "List configured MCP servers with status", (yargs) => this.buildListOptions(yargs), (argv) => this.executeList(argv)) .command("install <server>", "Install popular MCP servers", (yargs) => this.buildInstallOptions(yargs), (argv) => this.executeInstall(argv)) .command("add <name> <command>", "Add custom MCP server configuration", (yargs) => this.buildAddOptions(yargs), (argv) => this.executeAdd(argv)) .command("test [server]", "Test connectivity to MCP servers", (yargs) => this.buildTestOptions(yargs), (argv) => this.executeTest(argv)) .command("exec <server> <tool>", "Execute tools from MCP servers", (yargs) => this.buildExecOptions(yargs), (argv) => this.executeExec(argv)) .command("remove <server>", "Remove MCP server configuration", (yargs) => this.buildRemoveOptions(yargs), (argv) => this.executeRemove(argv)) .option("format", { choices: ["table", "json", "compact"], default: "table", description: "Output format", }) .option("output", { type: "string", description: "Save output to file", }) .option("quiet", { type: "boolean", alias: "q", default: false, description: "Suppress non-essential output", }) .option("debug", { type: "boolean", default: false, description: "Enable debug output", }) .demandCommand(1, "Please specify an MCP subcommand") .help(); }, handler: () => { // No-op handler as subcommands handle everything }, }; } /** * Create discover command (top-level command) */ static createDiscoverCommand() { return { command: "discover", describe: "Auto-discover MCP servers from various sources", builder: (yargs) => { return yargs .option("auto-install", { type: "boolean", default: false, description: "Automatically install discovered servers", }) .option("source", { choices: ["claude-desktop", "vscode", "all"], default: "all", description: "Source to discover servers from", }) .option("format", { choices: ["table", "json", "compact"], default: "table", description: "Output format", }) .option("quiet", { type: "boolean", alias: "q", default: false, description: "Suppress non-essential output", }) .example("neurolink discover", "Discover MCP servers from all sources") .example("neurolink discover --source claude-desktop", "Discover from Claude Desktop only") .example("neurolink discover --auto-install", "Discover and auto-install servers"); }, handler: async (argv) => await MCPCommandFactory.executeDiscover(argv), }; } /** * Build options for list command */ static buildListOptions(yargs) { return yargs .option("status", { type: "boolean", default: false, description: "Check server connection status", }) .option("detailed", { type: "boolean", default: false, description: "Show detailed server information", }) .example("neurolink mcp list", "List all configured MCP servers") .example("neurolink mcp list --status", "List servers with connection status") .example("neurolink mcp list --detailed", "Show detailed server information"); } /** * Build options for install command */ static buildInstallOptions(yargs) { return yargs .positional("server", { type: "string", description: "Server name to install from popular registry", choices: Object.keys(POPULAR_MCP_SERVERS), demandOption: true, }) .option("transport", { choices: ["stdio", "sse", "websocket"], default: "stdio", description: "Transport type for MCP communication", }) .option("args", { type: "array", description: "Additional arguments for the server command", }) .option("env", { type: "string", description: "Environment variables as JSON string", }) .example("neurolink mcp install filesystem", "Install filesystem MCP server") .example("neurolink mcp install github", "Install GitHub MCP server") .example("neurolink mcp install postgres", "Install PostgreSQL MCP server"); } /** * Build options for add command */ static buildAddOptions(yargs) { return yargs .positional("name", { type: "string", description: "Name for the custom MCP server", demandOption: true, }) .positional("command", { type: "string", description: "Command to execute the MCP server", demandOption: true, }) .option("transport", { choices: ["stdio", "sse", "websocket"], default: "stdio", description: "Transport type for MCP communication", }) .option("args", { type: "array", description: "Arguments for the server command", }) .option("env", { type: "string", description: "Environment variables as JSON string", }) .example("neurolink mcp add my-server node", "Add custom Node.js MCP server") .example("neurolink mcp add api-server python", "Add custom Python MCP server"); } /** * Build options for test command */ static buildTestOptions(yargs) { return yargs .positional("server", { type: "string", description: "Server name to test (optional - tests all if not specified)", }) .option("timeout", { type: "number", default: 10000, description: "Test timeout in milliseconds", }) .example("neurolink mcp test", "Test all configured servers") .example("neurolink mcp test filesystem", "Test specific server") .example("neurolink mcp test --timeout 5000", "Test with custom timeout"); } /** * Build options for exec command */ static buildExecOptions(yargs) { return yargs .positional("server", { type: "string", description: "MCP server name", demandOption: true, }) .positional("tool", { type: "string", description: "Tool name to execute", demandOption: true, }) .option("params", { type: "string", description: "Tool parameters as JSON string", }) .example("neurolink mcp exec filesystem read_file", "Execute read_file tool") .example("neurolink mcp exec github list_repos", "Execute GitHub list_repos tool"); } /** * Build options for remove command */ static buildRemoveOptions(yargs) { return yargs .positional("server", { type: "string", description: "Server name to remove", demandOption: true, }) .option("force", { type: "boolean", default: false, description: "Force removal without confirmation", }) .example("neurolink mcp remove filesystem", "Remove filesystem server") .example("neurolink mcp remove old-server --force", "Force remove without confirmation"); } /** * Execute list command */ static async executeList(argv) { try { const spinner = argv.quiet ? null : ora("Loading MCP servers...").start(); // Get configured servers from NeuroLink const sdk = new NeuroLink(); const mcpStatus = await sdk.getMCPStatus(); const allServers = await sdk.listMCPServers(); if (spinner) { spinner.succeed(`Found ${allServers.length} MCP servers`); } if (allServers.length === 0) { logger.always(chalk.yellow("No MCP servers configured.")); logger.always(chalk.blue("๐Ÿ’ก Use 'neurolink mcp install <server>' to install popular servers")); logger.always(chalk.blue("๐Ÿ’ก Use 'neurolink discover' to find existing servers")); return; } // Format and display results if (argv.format === "json") { logger.always(JSON.stringify(mcpStatus, null, 2)); } else if (argv.format === "compact") { const allServers = await sdk.listMCPServers(); allServers.forEach((server) => { const status = server.status === "connected" ? chalk.green("โœ“") : chalk.red("โœ—"); logger.always(`${status} ${server.name} - ${server.description || "No description"}`); }); } else { // Table format logger.always(chalk.bold("\n๐Ÿ”ง MCP Servers:\n")); const allServers = await sdk.listMCPServers(); for (const server of allServers) { const status = server.status === "connected" ? chalk.green("CONNECTED") : chalk.red("DISCONNECTED"); logger.always(`${chalk.cyan(server.name)} ${status}`); logger.always(` Command: ${server.command || "Unknown"}`); logger.always(` Tools: ${server.tools?.length || 0} available`); if (argv.detailed && server.tools) { server.tools.forEach((tool) => { logger.always(` โ€ข ${tool.name}: ${tool.description}`); }); } if (server.error) { logger.always(` ${chalk.red("Error:")} ${server.error}`); } logger.always(); } } } catch (_error) { logger.error(chalk.red(`โŒ List command failed: ${_error.message}`)); process.exit(1); } } /** * Execute install command */ static async executeInstall(argv) { try { const serverName = argv.server; const serverConfig = POPULAR_MCP_SERVERS[serverName]; if (!serverConfig) { logger.error(chalk.red(`โŒ Unknown server: ${serverName}`)); logger.always(chalk.blue("Available servers:")); Object.keys(POPULAR_MCP_SERVERS).forEach((name) => { logger.always(` โ€ข ${name}: ${POPULAR_MCP_SERVERS[name].description}`); }); process.exit(1); } const spinner = argv.quiet ? null : ora(`Installing ${serverName} MCP server...`).start(); // Parse environment variables if provided let env = serverConfig.env; if (argv.env) { try { const parsedEnv = JSON.parse(argv.env); env = { ...env, ...parsedEnv }; } catch (_error) { if (spinner) { spinner.fail(); } logger.error(chalk.red("โŒ Invalid JSON in env parameter")); process.exit(1); } } const serverInfo = createExternalServerInfo({ ...serverConfig, id: serverName, name: serverName, }); // Add server to NeuroLink - direct usage, zero transformations! const sdk = new NeuroLink(); await sdk.addInMemoryMCPServer(serverName, serverInfo); if (spinner) { spinner.succeed(chalk.green(`โœ… Successfully installed ${serverName} MCP server`)); } // Display configuration info logger.always(chalk.bold("\n๐Ÿ“‹ Server Configuration:")); logger.always(`Name: ${serverInfo.name}`); logger.always(`Command: ${serverInfo.command}`); if (serverInfo.args?.length) { logger.always(`Args: ${serverInfo.args.join(" ")}`); } if (serverInfo.env) { logger.always(`Environment: ${Object.keys(serverInfo.env).length} variables`); } logger.always(`Transport: ${serverInfo.transport}`); logger.always(`Description: ${serverInfo.description}`); // Test connection logger.always(chalk.blue("\n๐Ÿ” Testing connection...")); try { const rawStatus = await sdk.getMCPStatus(); const status = rawStatus; const installedServer = status.externalMCPServers?.find((s) => s.name === serverName); if (installedServer?.status === "connected") { logger.always(chalk.green("โœ… Server connected successfully")); if (installedServer.tools?.length) { logger.always(`๐Ÿ› ๏ธ Available tools: ${installedServer.tools.length}`); } } else { logger.always(chalk.yellow("โš ๏ธ Server installed but not connected")); if (installedServer?.error) { logger.always(chalk.red(`Error: ${installedServer.error}`)); } } } catch (testError) { logger.always(chalk.yellow("โš ๏ธ Could not test connection")); } } catch (_error) { logger.error(chalk.red(`โŒ Install command failed: ${_error.message}`)); process.exit(1); } } /** * Execute add command */ static async executeAdd(argv) { try { const name = argv.name; const command = argv.command; const spinner = argv.quiet ? null : ora(`Adding custom MCP server: ${name}...`).start(); // Parse environment variables if provided let env; if (argv.env) { try { env = JSON.parse(argv.env); } catch (_error) { if (spinner) { spinner.fail(); } logger.error(chalk.red("โŒ Invalid JSON in env parameter")); process.exit(1); } } const serverInfo = createExternalServerInfo({ id: name, name, command, args: argv.args, env, transport: argv.transport || "stdio", description: command, }); // Add server to NeuroLink using MCPServerInfo directly const sdk = new NeuroLink(); await sdk.addInMemoryMCPServer(name, serverInfo); if (spinner) { spinner.succeed(chalk.green(`โœ… Successfully added ${name} MCP server`)); } // Display configuration logger.always(chalk.bold("\n๐Ÿ“‹ Server Configuration:")); logger.always(`Name: ${serverInfo.name}`); logger.always(`Command: ${serverInfo.command}`); if (serverInfo.args?.length) { logger.always(`Args: ${serverInfo.args.join(" ")}`); } if (serverInfo.env) { logger.always(`Environment: ${Object.keys(serverInfo.env).length} variables`); } logger.always(`Transport: ${serverInfo.transport}`); } catch (_error) { logger.error(chalk.red(`โŒ Add command failed: ${_error.message}`)); process.exit(1); } } /** * Execute test command */ static async executeTest(argv) { try { const targetServer = argv.server; const spinner = argv.quiet ? null : ora("Testing MCP server connections...").start(); const sdk = new NeuroLink(); const rawMcpStatus = await sdk.getMCPStatus(); let serversToTest = await sdk.listMCPServers(); if (targetServer) { serversToTest = serversToTest.filter((s) => s.name === targetServer); if (serversToTest.length === 0) { if (spinner) { spinner.fail(); } logger.error(chalk.red(`โŒ Server not found: ${targetServer}`)); process.exit(1); } } if (spinner) { spinner.succeed(`Testing ${serversToTest.length} servers`); } // Display test results logger.always(chalk.bold("\n๐Ÿงช MCP Server Test Results:\n")); for (const server of serversToTest) { const status = server.status === "connected" ? chalk.green("โœ… CONNECTED") : chalk.red("โŒ DISCONNECTED"); logger.always(`${server.name}: ${status}`); if (server.status === "connected") { logger.always(` Tools: ${server.tools?.length || 0} available`); if (server.tools?.length) { server.tools.slice(0, 3).forEach((tool) => { logger.always(` โ€ข ${tool.name}`); }); if (server.tools.length > 3) { logger.always(` ... and ${server.tools.length - 3} more`); } } } else { if (server.error) { logger.always(` ${chalk.red("Error:")} ${server.error}`); } logger.always(chalk.yellow(" ๐Ÿ’ก Try: neurolink mcp remove && neurolink mcp install")); } logger.always(); } // Summary const connected = serversToTest.filter((s) => s.status === "connected").length; const total = serversToTest.length; if (connected === total) { logger.always(chalk.green(`๐ŸŽ‰ All ${total} servers connected successfully`)); } else { logger.always(chalk.yellow(`โš ๏ธ ${connected}/${total} servers connected`)); } } catch (_error) { logger.error(chalk.red(`โŒ Test command failed: ${_error.message}`)); process.exit(1); } } /** * Execute exec command */ static async executeExec(argv) { try { const serverName = argv.server; const toolName = argv.tool; const spinner = argv.quiet ? null : ora(`Executing ${toolName} on ${serverName}...`).start(); // Parse parameters if provided let params = {}; if (argv.params) { try { params = JSON.parse(argv.params); } catch (_error) { if (spinner) { spinner.fail(); } logger.error(chalk.red("โŒ Invalid JSON in params parameter")); process.exit(1); } } const sdk = new NeuroLink(); // Check if server exists and is connected const allServers = await sdk.listMCPServers(); const server = allServers.find((s) => s.name === serverName); if (!server) { if (spinner) { spinner.fail(); } logger.error(chalk.red(`โŒ Server not found: ${serverName}`)); process.exit(1); } if (server.status !== "connected") { if (spinner) { spinner.fail(); } logger.error(chalk.red(`โŒ Server not connected: ${serverName}`)); logger.always(chalk.yellow("๐Ÿ’ก Try: neurolink mcp test " + serverName)); process.exit(1); } // Check if tool exists const tool = server.tools?.find((t) => t.name === toolName); if (!tool) { if (spinner) { spinner.fail(); } logger.error(chalk.red(`โŒ Tool not found: ${toolName}`)); if (server.tools?.length) { logger.always(chalk.blue("Available tools:")); server.tools.forEach((t) => { logger.always(` โ€ข ${t.name}: ${t.description}`); }); } process.exit(1); } // Execute the tool using the NeuroLink MCP tool registry try { const { toolRegistry } = await import("../../lib/mcp/toolRegistry.js"); const executionResult = await toolRegistry.executeTool(toolName, params, { sessionId: `cli-${Date.now()}`, userId: process.env.USER || "cli-user", config: { domainType: "cli-execution", customData: { serverName }, }, }); const result = { tool: toolName, server: serverName, params, result: executionResult, success: true, timestamp: new Date().toISOString(), }; if (spinner) { spinner.succeed(chalk.green("โœ… Tool executed successfully")); } // Display results if (argv.format === "json") { logger.always(JSON.stringify(result, null, 2)); } else { logger.always(chalk.green("๐Ÿ”ง Tool Execution Results:")); logger.always(` Tool: ${chalk.cyan(toolName)}`); logger.always(` Server: ${chalk.cyan(serverName)}`); logger.always(` Result: ${JSON.stringify(executionResult, null, 2)}`); logger.always(` Timestamp: ${result.timestamp}`); } } catch (toolError) { const errorMessage = toolError instanceof Error ? toolError.message : String(toolError); if (spinner) { spinner.fail(chalk.red("โŒ Tool execution failed")); } const result = { tool: toolName, server: serverName, params, error: errorMessage, success: false, timestamp: new Date().toISOString(), }; if (argv.format === "json") { logger.always(JSON.stringify(result, null, 2)); } else { logger.error(chalk.red("๐Ÿ”ง Tool Execution Failed:")); logger.error(` Tool: ${chalk.cyan(toolName)}`); logger.error(` Server: ${chalk.cyan(serverName)}`); logger.error(` Error: ${chalk.red(errorMessage)}`); } process.exit(1); } } catch (_error) { logger.error(chalk.red(`โŒ Exec command failed: ${_error.message}`)); process.exit(1); } } /** * Execute remove command */ static async executeRemove(argv) { try { const serverName = argv.server; const sdk = new NeuroLink(); const allServers = await sdk.listMCPServers(); const server = allServers.find((s) => s.name === serverName); if (!server) { logger.error(chalk.red(`โŒ Server not found: ${serverName}`)); process.exit(1); } // Confirmation unless forced if (!argv.force) { logger.always(chalk.yellow(`โš ๏ธ This will remove the MCP server: ${serverName}`)); logger.always("Use --force flag to confirm removal"); process.exit(1); } const spinner = argv.quiet ? null : ora(`Removing MCP server: ${serverName}...`).start(); // Remove server using the NeuroLink MCP tool registry try { const { toolRegistry } = await import("../../lib/mcp/toolRegistry.js"); const removed = toolRegistry.unregisterServer(serverName); if (!removed) { throw new Error(`Failed to remove server ${serverName} from registry`); } if (spinner) { spinner.succeed(chalk.green(`โœ… Server ${serverName} removed successfully`)); } logger.always(chalk.green(`๐Ÿ—‘๏ธ Successfully removed MCP server: ${serverName}`)); logger.always(chalk.gray(" All associated tools have been unregistered")); } catch (removalError) { const errorMessage = removalError instanceof Error ? removalError.message : String(removalError); if (spinner) { spinner.fail(chalk.red(`โŒ Failed to remove server ${serverName}`)); } logger.error(chalk.red(`โŒ Server removal failed: ${errorMessage}`)); process.exit(1); } } catch (_error) { logger.error(chalk.red(`โŒ Remove command failed: ${_error.message}`)); process.exit(1); } } /** * Execute discover command */ static async executeDiscover(argv) { try { const spinner = argv.quiet ? null : ora("Discovering MCP servers...").start(); const discovered = []; // Discover from Claude Desktop if (argv.source === "claude-desktop" || argv.source === "all") { const claudeServers = await this.discoverFromClaudeDesktop(); discovered.push(...claudeServers); } // Discover from VS Code if (argv.source === "vscode" || argv.source === "all") { const vscodeServers = await this.discoverFromVSCode(); discovered.push(...vscodeServers); } if (spinner) { spinner.succeed(`Discovered ${discovered.length} MCP servers`); } if (discovered.length === 0) { logger.always(chalk.yellow("No MCP servers discovered.")); logger.always(chalk.blue("๐Ÿ’ก Try installing popular servers: neurolink mcp install filesystem")); return; } // Display discovered servers if (argv.format === "json") { logger.always(JSON.stringify(discovered, null, 2)); } else { logger.always(chalk.bold("\n๐Ÿ” Discovered MCP Servers:\n")); discovered.forEach((server) => { logger.always(`${chalk.cyan(server.name)}`); logger.always(` Command: ${server.command}`); logger.always(` Source: ${server.description || "Unknown"}`); logger.always(` Status: ${server.status}`); logger.always(); }); } // Auto-install if requested if (argv.autoInstall && discovered.length > 0) { logger.always(chalk.blue("๐Ÿš€ Auto-installing discovered servers...")); const sdk = new NeuroLink(); for (const server of discovered) { try { // Use discovered MCPServerInfo directly - zero conversions! await sdk.addInMemoryMCPServer(server.name, server); logger.always(chalk.green(`โœ… Installed ${server.name}`)); } catch (_error) { logger.always(chalk.red(`โŒ Failed to install ${server.name}: ${_error.message}`)); } } } } catch (_error) { logger.error(chalk.red(`โŒ Discover command failed: ${_error.message}`)); process.exit(1); } } /** * Discover servers from Claude Desktop configuration */ static async discoverFromClaudeDesktop() { const servers = []; try { // Common Claude Desktop config paths const possiblePaths = [ path.join(process.env.HOME || "", "Library", "Application Support", "Claude", "claude_desktop_config.json"), path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json"), path.join(process.env.HOME || "", ".config", "claude", "claude_desktop_config.json"), ]; for (const configPath of possiblePaths) { if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, "utf8")); if (config.mcpServers) { Object.entries(config.mcpServers).forEach(([name, serverConfig]) => { const typedConfig = serverConfig; // SMART DEFAULTS: Use utility to eliminate manual MCPServerInfo creation servers.push(createExternalServerInfo({ ...typedConfig, id: name, name, description: "Discovered from Claude Desktop", })); }); } break; // Found config file, stop searching } } } catch (_error) { // Ignore errors in discovery } return servers; } /** * Discover servers from VS Code configuration */ static async discoverFromVSCode() { const servers = []; try { // VS Code settings paths const possiblePaths = [ path.join(process.env.HOME || "", "Library", "Application Support", "Code", "User", "settings.json"), path.join(process.env.APPDATA || "", "Code", "User", "settings.json"), path.join(process.env.HOME || "", ".config", "Code", "User", "settings.json"), ]; for (const settingsPath of possiblePaths) { if (fs.existsSync(settingsPath)) { const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); // Look for MCP-related extensions or configurations if (settings["mcp.servers"]) { Object.entries(settings["mcp.servers"]).forEach(([name, serverConfig]) => { const config = serverConfig; servers.push(createExternalServerInfo({ ...config, id: name, name, description: "Discovered from VS Code", })); }); } break; } } } catch (_error) { // Ignore errors in discovery } return servers; } }