url-builder-mcp
Version:
URL Builder MCP Server - Professional URL construction with intelligent parameter handling for Claude Desktop
345 lines (317 loc) • 12 kB
text/typescript
#!/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);
}