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

917 lines (916 loc) โ€ข 37.4 kB
/** * MCP CLI Commands for NeuroLink * Implements comprehensive MCP server management commands * Part of Phase 4.2 - MCP CLI Commands */ 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", }, }; /** * Convert SDK MCPStatus to CLI format with server list */ function convertMCPStatusForCLI(status, // Use flexible object type sdk) { // Create server list from in-memory servers and discovered servers const servers = []; // Add in-memory servers const inMemoryServers = sdk.getInMemoryServers(); inMemoryServers.forEach((config, name) => { servers.push({ name, connected: true, // In-memory servers are always "connected" description: config.server?.title || "In-memory MCP server", tools: [], // Could extract from config.server.tools if needed }); }); // Add auto-discovered servers if (status.autoDiscoveredServers && Array.isArray(status.autoDiscoveredServers)) { status.autoDiscoveredServers.forEach((server) => { servers.push({ name: server.name || server.id, connected: server.status === "connected", description: server.source ? `Auto-discovered from ${server.source}` : `Auto-discovered server`, tools: [], error: server.status === "failed" ? "Connection failed" : undefined, }); }); } return { mcpInitialized: status.mcpInitialized, totalServers: status.totalServers, availableServers: status.availableServers, autoDiscoveredCount: status.autoDiscoveredCount, totalTools: status.totalTools, customToolsCount: status.customToolsCount, inMemoryServersCount: status.inMemoryServersCount, error: status.error, servers, }; } /** * 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", "ws"], 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", "ws"], 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 rawMcpStatus = await sdk.getMCPStatus(); const mcpStatus = convertMCPStatusForCLI(rawMcpStatus, sdk); if (spinner) { spinner.succeed(`Found ${mcpStatus.servers.length} MCP servers`); } if (mcpStatus.servers.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 && argv.format === "compact") { mcpStatus.servers.forEach((server) => { const status = server.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")); for (const server of mcpStatus.servers) { const status = server.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); } } // Create server configuration const config = { name: serverName, command: serverConfig.command, args: argv.args || serverConfig.args, env, transport: (argv.transport && (argv.transport === "websocket" ? "ws" : argv.transport)) || serverConfig.transport, description: serverConfig.description, installed: true, status: "unknown", }; // Add server to NeuroLink (using in-memory MCP server for now) const sdk = new NeuroLink(); await sdk.addInMemoryMCPServer(serverName, { server: { title: serverConfig.description, tools: {}, // Empty tools for external servers description: serverConfig.description, }, metadata: { command: config.command, args: config.args, env: config.env, transport: config.transport, }, }); if (spinner) { spinner.succeed(chalk.green(`โœ… Successfully installed ${serverName} MCP server`)); } // Display configuration info logger.always(chalk.bold("\n๐Ÿ“‹ Server Configuration:")); logger.always(`Name: ${config.name}`); logger.always(`Command: ${config.command}`); if (config.args?.length) { logger.always(`Args: ${config.args.join(" ")}`); } if (config.env) { logger.always(`Environment: ${Object.keys(config.env).length} variables`); } logger.always(`Transport: ${config.transport}`); logger.always(`Description: ${config.description}`); // Test connection logger.always(chalk.blue("\n๐Ÿ” Testing connection...")); try { const rawStatus = await sdk.getMCPStatus(); const status = convertMCPStatusForCLI(rawStatus, sdk); const installedServer = status.servers.find((s) => s.name === serverName); if (installedServer?.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); } } // Create server configuration const config = { name, command, args: argv.args, env, transport: (argv.transport === "websocket" ? "ws" : argv.transport) || "stdio", installed: true, status: "unknown", }; // Add server to NeuroLink const sdk = new NeuroLink(); await sdk.addInMemoryMCPServer(name, { server: { title: name, tools: {}, // Empty tools for external servers description: `Custom MCP server: ${command}`, }, metadata: { command: config.command, args: config.args, env: config.env, transport: config.transport, }, }); if (spinner) { spinner.succeed(chalk.green(`โœ… Successfully added ${name} MCP server`)); } // Display configuration logger.always(chalk.bold("\n๐Ÿ“‹ Server Configuration:")); logger.always(`Name: ${config.name}`); logger.always(`Command: ${config.command}`); if (config.args?.length) { logger.always(`Args: ${config.args.join(" ")}`); } if (config.env) { logger.always(`Environment: ${Object.keys(config.env).length} variables`); } logger.always(`Transport: ${config.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(); const mcpStatus = convertMCPStatusForCLI(rawMcpStatus, sdk); let serversToTest = mcpStatus.servers; if (targetServer) { serversToTest = mcpStatus.servers.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.connected ? chalk.green("โœ… CONNECTED") : chalk.red("โŒ DISCONNECTED"); logger.always(`${server.name}: ${status}`); if (server.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.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 rawMcpStatus = await sdk.getMCPStatus(); const mcpStatus = convertMCPStatusForCLI(rawMcpStatus, sdk); const server = mcpStatus.servers.find((s) => s.name === serverName); if (!server) { if (spinner) { spinner.fail(); } logger.error(chalk.red(`โŒ Server not found: ${serverName}`)); process.exit(1); } if (!server.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 (This would need actual MCP execution logic) // For now, showing a placeholder implementation const result = { tool: toolName, server: serverName, params, result: "Tool execution not yet implemented in NeuroLink SDK", 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.bold("\n๐Ÿ› ๏ธ Tool Execution Result:\n")); logger.always(`Tool: ${toolName}`); logger.always(`Server: ${serverName}`); if (Object.keys(params).length > 0) { logger.always(`Params: ${JSON.stringify(params)}`); } logger.always(`Result: ${result.result}`); logger.always(`Time: ${result.timestamp}`); } } 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 rawMcpStatus = await sdk.getMCPStatus(); const mcpStatus = convertMCPStatusForCLI(rawMcpStatus, sdk); const server = mcpStatus.servers.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 (This would need actual removal logic in NeuroLink SDK) // For now, showing a placeholder logger.always(chalk.yellow("โš ๏ธ Server removal not yet implemented in NeuroLink SDK")); if (spinner) { spinner.succeed(chalk.green(`โœ… Server ${serverName} removed successfully`)); } } 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 { await sdk.addInMemoryMCPServer(server.name, { server: { title: server.name, tools: {}, description: server.description, }, metadata: { command: server.command, args: server.args, env: server.env, transport: server.transport, }, }); 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 config = serverConfig; servers.push({ name, command: config.command, args: config.args, env: config.env, transport: "stdio", description: "Discovered from Claude Desktop", installed: false, status: "unknown", }); }); } 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({ name, command: config.command || "unknown", args: config.args, env: config.env, transport: "stdio", description: "Discovered from VS Code", installed: false, status: "unknown", }); }); } break; } } } catch (error) { // Ignore errors in discovery } return servers; } }