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
JavaScript
/**
* 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();