UNPKG

bottlenecks-mcp-server

Version:

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

246 lines (220 loc) โ€ข 7.48 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'; // 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(() => { // For MCP compatibility, avoid fancy formatting when stdout might be used for JSON const configText = '๐Ÿ”ง Configuration Guide\n\n' + 'Get your API key:\n' + '1. Visit: https://www.bottlenecksinstitute.com/api/auth/mcp/authorize\n' + '2. Login with your account\n' + '3. Approve MCP access\n' + '4. Copy the generated API key\n\n' + 'Usage:\n' + 'bottlenecks-mcp start # Production mode\n' + 'bottlenecks-mcp start --local # Local dev (port 3001)\n' + 'bottlenecks-mcp start --api-url <url> # Custom URL\n' + 'bottlenecks-mcp start --test # Test mode\n\n' + 'Claude Desktop Config (Production):\n' + '{\n' + ' "mcpServers": {\n' + ' "bottlenecks": {\n' + ' "command": "npx",\n' + ' "args": ["bottlenecks-mcp-server", "start"],\n' + ' "env": {\n' + ' "BOTTLENECKS_API_KEY": "bk_your-api-key-here"\n' + ' }\n' + ' }\n' + ' }\n' + '}\n\n' + 'Claude Desktop Config (Local Development):\n' + '{\n' + ' "mcpServers": {\n' + ' "bottlenecks-local": {\n' + ' "command": "npx",\n' + ' "args": ["bottlenecks-mcp-server", "start", "--local"],\n' + ' "env": {\n' + ' "BOTTLENECKS_API_KEY": "bk_your-local-api-key"\n' + ' }\n' + ' }\n' + ' }\n' + '}\n'; process.stderr.write(configText); }); program .command('test') .description('Test the MCP server with mock configuration') .action(async () => { try { process.stderr.write('๐Ÿงช Testing MCP server...\n'); const server = createBottlenecksMCPServer({ apiBaseUrl: 'http://localhost:3000', supabaseUrl: 'mock://supabase', supabaseKey: 'mock-key', defaultTimeout: 5000, }); process.stderr.write('โœ… MCP server created successfully\n'); process.stderr.write('๐Ÿ“Š Server status:\n'); process.stderr.write(JSON.stringify(server.getStatus(), null, 2) + '\n'); process.stderr.write('โœ… Test completed successfully\n'); } catch (error) { process.stderr.write(`โŒ 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', ], }; // For MCP compatibility, avoid fancy formatting when stdout might be used for JSON const toolsText = '๐Ÿ› ๏ธ Available MCP Tools\n\n' + Object.entries(tools) .map( ([category, toolList]) => `${category.toUpperCase()} (${toolList.length})\n` + toolList.map((tool) => ` โ€ข ${tool}`).join('\n') ) .join('\n\n') + '\n'; process.stderr.write(toolsText); }); // Handle uncaught errors process.on('uncaughtException', (error) => { process.stderr.write(`โŒ Uncaught Exception: ${error.message}\n`); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { process.stderr.write( `โŒ Unhandled Rejection at: ${promise} reason: ${reason}\n` ); process.exit(1); }); program.parse();