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
text/typescript
/**
* 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;
}