UNPKG

url-builder-mcp

Version:

URL Builder MCP Server - Professional URL construction with intelligent parameter handling for Claude Desktop

345 lines (317 loc) 12 kB
#!/usr/bin/env node /** * URL Builder MCP Server with HTTP and stdio transport support * * MCP server that provides URL building tools following project-specific rules * Supports both stdio and HTTP/SSE transports for different deployment scenarios */ import express from 'express'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { buildBookingUrl, validateUrlParams } from './urlBuilder.js'; import { UrlParams } from './types.js'; class UrlBuilderServer { private server: Server; constructor() { this.server = new Server({ name: 'url-builder-mcp', version: '1.0.0', }); this.setupToolHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); process.on('SIGTERM', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'build_booking_url', description: 'Build external booking URL with query parameters', inputSchema: { type: 'object', properties: { checkIn: { type: 'string', description: 'Check-in date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, checkOut: { type: 'string', description: 'Check-out date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, guestName: { type: 'string', description: 'Guest name (required)' }, guestPhone: { type: 'string', description: 'Guest phone (required)' }, adults: { type: 'number', description: 'Number of adults (optional, default: 2)', minimum: 1, maximum: 20 }, children: { type: 'number', description: 'Number of children (optional, default: 0)', minimum: 0, maximum: 10 }, roomType: { type: 'string', description: 'Room type name (optional, e.g., "garden-deluxe")' }, guestEmail: { type: 'string', description: 'Guest email (optional)' } }, required: ['checkIn', 'checkOut', 'guestName', 'guestPhone'] } }, { name: 'validate_url_params', description: 'Validate URL parameters without building URLs', inputSchema: { type: 'object', properties: { checkIn: { type: 'string', description: 'Check-in date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, checkOut: { type: 'string', description: 'Check-out date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, adults: { type: 'number', description: 'Number of adults (optional)', minimum: 1, maximum: 20 }, children: { type: 'number', description: 'Number of children (optional)', minimum: 0, maximum: 10 }, roomType: { type: 'string', description: 'Room type name (optional, e.g., "garden-deluxe")' }, guestName: { type: 'string', description: 'Guest name (optional)' }, guestEmail: { type: 'string', description: 'Guest email (optional)' }, guestPhone: { type: 'string', description: 'Guest phone (optional)' } }, required: ['checkIn', 'checkOut'] } } ] })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'build_booking_url': { const params = args as unknown as UrlParams; const url = buildBookingUrl(params); return { content: [ { type: 'text', text: `Booking URL: ${url}` } ] }; } case 'validate_url_params': { const params = args as unknown as UrlParams; const validation = validateUrlParams(params); return { content: [ { type: 'text', text: validation.valid ? 'Parameters are valid' : `Validation errors: ${validation.errors.join(', ')}` } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } }); } async runStdio(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('URL Builder MCP server running on stdio'); } async runHttp(port: number = 8000): Promise<void> { const app = express(); let servers: Server[] = []; // Health check endpoint for Coolify app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', service: 'url-builder-mcp', version: '1.0.0', timestamp: new Date().toISOString() }); }); // SSE endpoint for MCP connections app.get('/sse', async (req, res) => { console.error('Got new SSE connection'); const transport = new SSEServerTransport('/message', res); const server = new Server({ name: 'url-builder-mcp', version: '1.0.0', }); // Copy tool handlers to the new server instance this.copyHandlersTo(server); servers.push(server); server.onclose = () => { console.error('SSE connection closed'); servers = servers.filter((s) => s !== server); }; await server.connect(transport); }); // Message endpoint for SSE transport app.post('/message', async (req, res) => { const sessionId = req.query.sessionId as string; const transport = servers .map((s) => s.transport) .find((t) => (t as any).sessionId === sessionId) as SSEServerTransport; if (!transport) { res.status(404).send('Session not found'); return; } await transport.handlePostMessage(req, res); }); app.listen(port, '0.0.0.0', () => { console.error(`URL Builder MCP server running on http://0.0.0.0:${port}`); console.error(`Health check available at http://0.0.0.0:${port}/health`); console.error(`SSE endpoint available at http://0.0.0.0:${port}/sse`); }); } private copyHandlersTo(server: Server): void { // Copy the tool handlers to the new server instance server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'build_booking_url', description: 'Build external booking URL with query parameters', inputSchema: { type: 'object', properties: { checkIn: { type: 'string', description: 'Check-in date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, checkOut: { type: 'string', description: 'Check-out date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, guestName: { type: 'string', description: 'Guest name (required)' }, guestPhone: { type: 'string', description: 'Guest phone (required)' }, adults: { type: 'number', description: 'Number of adults (optional, default: 2)', minimum: 1, maximum: 20 }, children: { type: 'number', description: 'Number of children (optional, default: 0)', minimum: 0, maximum: 10 }, roomType: { type: 'string', description: 'Room type name (optional, e.g., "garden-deluxe")' }, guestEmail: { type: 'string', description: 'Guest email (optional)' } }, required: ['checkIn', 'checkOut', 'guestName', 'guestPhone'] } }, { name: 'validate_url_params', description: 'Validate URL parameters without building URLs', inputSchema: { type: 'object', properties: { checkIn: { type: 'string', description: 'Check-in date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, checkOut: { type: 'string', description: 'Check-out date in YYYY-MM-DD format', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, adults: { type: 'number', description: 'Number of adults (optional)', minimum: 1, maximum: 20 }, children: { type: 'number', description: 'Number of children (optional)', minimum: 0, maximum: 10 }, roomType: { type: 'string', description: 'Room type name (optional, e.g., "garden-deluxe")' }, guestName: { type: 'string', description: 'Guest name (optional)' }, guestEmail: { type: 'string', description: 'Guest email (optional)' }, guestPhone: { type: 'string', description: 'Guest phone (optional)' } }, required: ['checkIn', 'checkOut'] } } ] })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'build_booking_url': { const params = args as unknown as UrlParams; const url = buildBookingUrl(params); return { content: [{ type: 'text', text: `Booking URL: ${url}` }] }; } case 'validate_url_params': { const params = args as unknown as UrlParams; const validation = validateUrlParams(params); return { content: [{ type: 'text', text: validation.valid ? 'Parameters are valid' : `Validation errors: ${validation.errors.join(', ')}` }] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } }); } } // Parse command line arguments const args = process.argv.slice(2); const transport = args.includes('--transport') ? args[args.indexOf('--transport') + 1] : 'stdio'; const port = args.includes('--port') ? parseInt(args[args.indexOf('--port') + 1]) : 8000; const server = new UrlBuilderServer(); if (transport === 'http' || transport === 'sse') { server.runHttp(port).catch(console.error); } else { server.runStdio().catch(console.error); }