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