UNPKG

bottlenecks-mcp-server

Version:

Model Context Protocol server for Bottlenecks database - enables AI agents like Claude to interact with bottleneck data

266 lines (242 loc) โ€ข 8.45 kB
#!/usr/bin/env node /** * Bottlenecks MCP Server CLI * Command-line interface for the Bottlenecks Model Context Protocol server */ import { program } from 'commander'; import { createBottlenecksMCPServer } from '../lib/index.js'; import chalk from 'chalk'; import boxen from 'boxen'; // Auto-discovery function to find local Bottlenecks API async function discoverLocalAPI() { const commonPorts = [3000, 3001, 4000, 5000, 8000, 8080]; for (const port of commonPorts) { const url = `http://localhost:${port}`; try { // Check if it's a Bottlenecks API by testing a known endpoint const response = await fetch(`${url}/api/cards?limit=1`, { method: 'GET', headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(2000), // 2 second timeout }); if (response.ok) { const data = await response.json(); // Validate it's the Bottlenecks API by checking response structure if ( data && typeof data === 'object' && ('cards' in data || 'total' in data) ) { process.stderr.write( `๐Ÿ” Auto-discovered Bottlenecks API at: ${url}\n` ); return url; } } } catch (error) { // Port not available or not Bottlenecks API, continue checking continue; } } // Fallback to default if no API found process.stderr.write( 'โš ๏ธ Could not auto-discover Bottlenecks API, using default: http://localhost:3000\n' ); return 'http://localhost:3000'; } import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse( readFileSync(join(__dirname, '../package.json'), 'utf8') ); program .name('bottlenecks-mcp') .description('Model Context Protocol server for Bottlenecks database') .version(packageJson.version); program .command('start') .description('Start the MCP server') .option( '-k, --api-key <key>', 'API key for authentication', process.env.BOTTLENECKS_API_KEY ) .option( '--api-url <url>', 'Custom API URL (for local development)', process.env.BOTTLENECKS_API_URL ) .option( '--local', 'Local development mode (auto-discovers Bottlenecks API on common ports)' ) .option('--auto', 'Auto-discover local Bottlenecks API (same as --local)') .option('--timeout <ms>', 'Request timeout in milliseconds', '30000') .option('--test', 'Run in test mode with mock configuration') .action(async (options) => { try { // Determine API URL based on mode let apiBaseUrl; if (options.test) { apiBaseUrl = 'http://localhost:3000'; // Test mode uses 3000 } else if (options.apiUrl) { apiBaseUrl = options.apiUrl; // User-specified URL } else if (options.local || options.auto) { apiBaseUrl = await discoverLocalAPI(); // Auto-discover local Bottlenecks API } else { apiBaseUrl = 'https://www.bottlenecksinstitute.com'; // Production } const config = { apiBaseUrl, supabaseUrl: options.test ? 'mock://supabase' : undefined, supabaseKey: options.test ? 'mock-key' : undefined, defaultTimeout: parseInt(options.timeout), }; const server = createBottlenecksMCPServer(config); if (options.apiKey) { server.setApiKey(options.apiKey); } await server.start(); } catch (error) { process.stderr.write(`MCP Server Error: ${error.message}\n`); process.exit(1); } }); program .command('config') .description('Show configuration help') .action(() => { process.stderr.write( boxen( chalk.blue.bold('๐Ÿ”ง Configuration Guide\n\n') + chalk.white('Get your API key:\n') + chalk.gray( '1. Visit: https://www.bottlenecksinstitute.com/api/auth/mcp/authorize\n' ) + chalk.gray('2. Login with your account\n') + chalk.gray('3. Approve MCP access\n') + chalk.gray('4. Copy the generated API key\n\n') + chalk.white('Usage:\n') + chalk.green('bottlenecks-mcp start ') + chalk.gray('# Production mode\n') + chalk.green('bottlenecks-mcp start --local ') + chalk.gray('# Local dev (port 3001)\n') + chalk.green('bottlenecks-mcp start --api-url <url> ') + chalk.gray('# Custom URL\n') + chalk.green('bottlenecks-mcp start --test ') + chalk.gray('# Test mode\n\n') + chalk.white('Claude Desktop Config (Production):\n') + chalk.gray('{\n') + chalk.gray(' "mcpServers": {\n') + chalk.gray(' "bottlenecks": {\n') + chalk.gray(' "command": "npx",\n') + chalk.gray(' "args": ["bottlenecks-mcp-server", "start"],\n') + chalk.gray(' "env": {\n') + chalk.gray( ' "BOTTLENECKS_API_KEY": "bk_your-api-key-here"\n' ) + chalk.gray(' }\n') + chalk.gray(' }\n') + chalk.gray(' }\n') + chalk.gray('}\n\n') + chalk.white('Claude Desktop Config (Local Development):\n') + chalk.gray('{\n') + chalk.gray(' "mcpServers": {\n') + chalk.gray(' "bottlenecks-local": {\n') + chalk.gray(' "command": "npx",\n') + chalk.gray( ' "args": ["bottlenecks-mcp-server", "start", "--local"],\n' ) + chalk.gray(' "env": {\n') + chalk.gray( ' "BOTTLENECKS_API_KEY": "bk_your-local-api-key"\n' ) + chalk.gray(' }\n') + chalk.gray(' }\n') + chalk.gray(' }\n') + chalk.gray('}'), { padding: 1, margin: 1, borderStyle: 'round' } ) + '\n' ); }); program .command('test') .description('Test the MCP server with mock configuration') .action(async () => { try { process.stderr.write(chalk.blue('๐Ÿงช Testing MCP server...\n')); const server = createBottlenecksMCPServer({ apiBaseUrl: 'http://localhost:3000', supabaseUrl: 'mock://supabase', supabaseKey: 'mock-key', defaultTimeout: 5000, }); process.stderr.write(chalk.green('โœ… MCP server created successfully\n')); process.stderr.write(chalk.blue('๐Ÿ“Š Server status:\n')); process.stderr.write(JSON.stringify(server.getStatus(), null, 2) + '\n'); process.stderr.write(chalk.green('โœ… Test completed successfully\n')); } catch (error) { process.stderr.write( chalk.red('โŒ Test failed:') + ` ${error.message}\n` ); process.exit(1); } }); program .command('tools') .description('List available MCP tools') .action(() => { const tools = { discovery: ['agents_start_here', 'get_capabilities'], schema: [ 'get_bottleneck_schema', 'get_taxonomy', 'get_bottleneck_template', 'get_mdx_guide', ], read: ['search_bottlenecks', 'get_bottleneck', 'list_bottlenecks'], write: [ 'create_bottleneck', 'update_bottleneck', 'validate_bottleneck_data', ], files: [ 'upload_file', 'get_file_attachments', 'get_file_content', 'download_file', 'delete_file', ], }; process.stderr.write( boxen( chalk.blue.bold('๐Ÿ› ๏ธ Available MCP Tools\n\n') + Object.entries(tools) .map( ([category, toolList]) => chalk.white.bold( `${category.toUpperCase()} (${toolList.length})\n` ) + toolList.map((tool) => chalk.gray(` โ€ข ${tool}`)).join('\n') ) .join('\n\n'), { padding: 1, margin: 1, borderStyle: 'round' } ) + '\n' ); }); // Handle uncaught errors process.on('uncaughtException', (error) => { process.stderr.write( chalk.red('โŒ Uncaught Exception:') + ` ${error.message}\n` ); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { process.stderr.write( chalk.red('โŒ Unhandled Rejection at:') + ` ${promise} reason: ${reason}\n` ); process.exit(1); }); program.parse();