UNPKG

url-builder-mcp

Version:

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

363 lines (298 loc) 11.5 kB
/** * URL Builder MCP - Core Implementation * * Constructs URLs following project-specific rules */ import { UrlParams, BookingUrlParams, ValidationResult, UrlBuilderConfig, UtmParams, UrlOptions } from './types'; /** * Get configuration from environment variables */ function getConfig(): UrlBuilderConfig { // Use BASE_DOMAIN as the primary configuration // Fall back to BOOKING_URL_BASE for backward compatibility const baseDomain = process.env.BASE_DOMAIN || process.env.MCP_BASE_DOMAIN || process.env.BOOKING_URL_BASE || 'http://localhost:8080'; return { bookingUrlBase: baseDomain, defaultCurrency: process.env.DEFAULT_CURRENCY || 'eur', baseDomain: baseDomain, }; } /** * Generate intelligent UTM parameters for MCP traffic tracking * * @param customUtm - Custom UTM parameters to override defaults * @returns UTM parameters object */ function generateUtmParams(customUtm?: Partial<UtmParams>): UtmParams { const defaultUtm: UtmParams = { utm_source: 'claude-desktop', utm_medium: 'ai-assistant', utm_campaign: 'url-builder-mcp', utm_content: 'mcp-generated-link', }; return { ...defaultUtm, ...customUtm }; } /** * Add UTM parameters to a URLSearchParams object * * @param searchParams - URLSearchParams to modify * @param utmParams - UTM parameters to add */ function addUtmToSearchParams(searchParams: URLSearchParams, utmParams: UtmParams): void { Object.entries(utmParams).forEach(([key, value]) => { if (value) { searchParams.append(key, value); } }); } /** * Validate URL parameters before building URLs * * @param params - URL parameters to validate * @returns ValidationResult with valid flag and error messages */ export function validateUrlParams(params: UrlParams): ValidationResult { const errors: string[] = []; // Validate required fields if (!params.checkIn || !params.checkOut) { errors.push('checkIn y checkOut son requeridos'); } // Validate date format (YYYY-MM-DD) const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (params.checkIn && !dateRegex.test(params.checkIn)) { errors.push('Formato de checkIn inválido. Use YYYY-MM-DD'); } if (params.checkOut && !dateRegex.test(params.checkOut)) { errors.push('Formato de checkOut inválido. Use YYYY-MM-DD'); } // Validate date range if (params.checkIn && params.checkOut) { const checkInDate = new Date(params.checkIn + 'T00:00:00.000Z'); const checkOutDate = new Date(params.checkOut + 'T00:00:00.000Z'); const today = new Date(); const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // Check if check-in date is in the past if (checkInDate < todayStart) { errors.push('La fecha de entrada no puede ser anterior a hoy'); } // Check if check-out date is in the past if (checkOutDate < todayStart) { errors.push('La fecha de salida no puede ser anterior a hoy'); } if (checkInDate >= checkOutDate) { errors.push('La fecha de salida debe ser posterior a la de entrada'); } } // Validate optional ranges if (params.adults !== undefined && (params.adults < 1 || params.adults > 20)) { errors.push('Número de adultos debe estar entre 1 y 20'); } if (params.children !== undefined && (params.children < 0 || params.children > 10)) { errors.push('Número de niños debe estar entre 0 y 10'); } return { valid: errors.length === 0, errors, }; } /** * Validate booking URL parameters specifically * * @param params - Booking URL parameters to validate * @returns ValidationResult with valid flag and error messages */ function validateBookingUrlParams(params: UrlParams): ValidationResult { const errors: string[] = []; // Validate required fields for booking if (!params.checkIn || !params.checkOut) { errors.push('checkIn y checkOut son requeridos'); } if (!params.guestName) { errors.push('guestName es requerido para URLs de reserva'); } if (!params.guestPhone) { errors.push('guestPhone es requerido para URLs de reserva'); } // Use base validation for the rest (including date validation) const baseValidation = validateUrlParams(params); errors.push(...baseValidation.errors.filter(error => !error.includes('checkIn y checkOut son requeridos'))); return { valid: errors.length === 0, errors, }; } /** * Build external booking URL with query parameters * * @param params - URL parameters * @returns Formatted booking URL with query parameters */ export function buildBookingUrl(params: UrlParams, options?: UrlOptions): string { // Validate parameters specifically for booking const validation = validateBookingUrlParams(params); if (!validation.valid) { throw new Error(`Parámetros inválidos: ${validation.errors.join(', ')}`); } // Type assertion after validation const bookingParams = params as BookingUrlParams; const config = getConfig(); const baseUrl = config.bookingUrlBase; // Determine if this is a localhost URL or external URL const isLocalhost = baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1'); const isExternal = baseUrl.includes('cloudbeds.com') || (!isLocalhost && baseUrl.startsWith('https://')); // Build parameters const searchParams = new URLSearchParams(); if (isExternal) { // External URLs (Cloudbeds) use hash fragment format with specific parameter names if (bookingParams.roomType) { searchParams.append('roomType', bookingParams.roomType); } searchParams.append('checkin', bookingParams.checkIn); // Note: 'checkin' not 'checkIn' searchParams.append('checkout', bookingParams.checkOut); // Note: 'checkout' not 'checkOut' searchParams.append('currency', config.defaultCurrency); searchParams.append('adults', (bookingParams.adults || 2).toString()); if (bookingParams.children && bookingParams.children > 0) { searchParams.append('children', bookingParams.children.toString()); } searchParams.append('guestName', bookingParams.guestName); if (bookingParams.guestEmail) { searchParams.append('guestEmail', bookingParams.guestEmail); } searchParams.append('guestPhone', bookingParams.guestPhone); // Add UTM tracking if enabled (default: true for external URLs) if (options?.addUtmTracking !== false) { const utmParams = generateUtmParams(options?.customUtm); addUtmToSearchParams(searchParams, utmParams); } return `${baseUrl}#${searchParams.toString()}`; } else { // Localhost URLs use query parameters format with /accommodation/reservations path const localhostBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; if (bookingParams.roomType) { searchParams.append('roomType', bookingParams.roomType); } searchParams.append('checkIn', bookingParams.checkIn); searchParams.append('checkOut', bookingParams.checkOut); searchParams.append('adults', (bookingParams.adults || 2).toString()); if (bookingParams.children && bookingParams.children > 0) { searchParams.append('children', bookingParams.children.toString()); } searchParams.append('guestName', bookingParams.guestName); if (bookingParams.guestEmail) { searchParams.append('guestEmail', bookingParams.guestEmail); } searchParams.append('guestPhone', bookingParams.guestPhone); // Add UTM tracking if enabled (default: true) if (options?.addUtmTracking !== false) { const utmParams = generateUtmParams(options?.customUtm); addUtmToSearchParams(searchParams, utmParams); } return `${localhostBase}/accommodation/reservations?${searchParams.toString()}`; } } /** * Build internal project URL with query parameters * * @param params - URL parameters * @param options - Options for URL building * @returns Formatted internal URL with query parameters */ export function buildInternalUrl(params: UrlParams, options?: UrlOptions): string { // Validate parameters const validation = validateUrlParams(params); if (!validation.valid) { throw new Error(`Parámetros inválidos: ${validation.errors.join(', ')}`); } const config = getConfig(); let baseUrl = '/accommodation/reservations'; // Use base domain if requested if (options?.useBaseDomain) { baseUrl = `${config.baseDomain}/accommodation/reservations`; } // Build search parameters in the specified order const searchParams = new URLSearchParams(); // Add required date parameters searchParams.append('checkIn', params.checkIn); searchParams.append('checkOut', params.checkOut); // Add optional parameters in order if (params.roomType) { searchParams.append('roomType', params.roomType); } if (params.adults !== undefined) { searchParams.append('adults', params.adults.toString()); } if (params.children !== undefined && params.children > 0) { searchParams.append('children', params.children.toString()); } if (params.guestName) { searchParams.append('guestName', params.guestName); } if (params.guestEmail) { searchParams.append('guestEmail', params.guestEmail); } if (params.guestPhone) { searchParams.append('guestPhone', params.guestPhone); } // Add UTM tracking if enabled (default: true) if (options?.addUtmTracking !== false) { const utmParams = generateUtmParams(options?.customUtm); addUtmToSearchParams(searchParams, utmParams); } return `${baseUrl}?${searchParams.toString()}`; } /** * Build complete URL using configurable base domain * * @param relativePath - Relative path to append to base domain * @param params - Optional URL parameters * @returns Complete URL with base domain and optional parameters */ export function buildCompleteUrl(relativePath: string, params?: UrlParams, options?: UrlOptions): string { const config = getConfig(); // Validate parameters if provided if (params) { const validation = validateUrlParams(params); if (!validation.valid) { throw new Error(`Parámetros inválidos: ${validation.errors.join(', ')}`); } } // Ensure relativePath starts with / and handle base domain trailing slash const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`; const baseDomain = config.baseDomain.endsWith('/') ? config.baseDomain.slice(0, -1) : config.baseDomain; let url = `${baseDomain}${path}`; // Add parameters if provided if (params) { const searchParams = new URLSearchParams(); if (params.checkIn) { searchParams.append('checkIn', params.checkIn); } if (params.checkOut) { searchParams.append('checkOut', params.checkOut); } if (params.adults !== undefined) { searchParams.append('adults', params.adults.toString()); } if (params.children !== undefined && params.children > 0) { searchParams.append('children', params.children.toString()); } if (params.guestName) { searchParams.append('guestName', params.guestName); } if (params.guestEmail) { searchParams.append('guestEmail', params.guestEmail); } if (params.guestPhone) { searchParams.append('guestPhone', params.guestPhone); } // Add UTM tracking if enabled (default: true) if (options?.addUtmTracking !== false) { const utmParams = generateUtmParams(options?.customUtm); addUtmToSearchParams(searchParams, utmParams); } if (searchParams.toString()) { url += `?${searchParams.toString()}`; } } return url; }