UNPKG

vex-mcp-server

Version:

MCP server for VEX Robotics Competition data using RobotEvents API

1,041 lines (1,040 loc) 64.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import * as robotevents from "robotevents"; import https from 'https'; import http from 'http'; class VEXMCPServer { server; isAuthenticated = false; constructor() { this.server = new Server({ name: "vex-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }); this.setupRobotEventsAuth(); this.setupTools(); this.setupToolHandlers(); } setupRobotEventsAuth() { const token = process.env.ROBOTEVENTS_TOKEN; if (!token) { console.error("Warning: ROBOTEVENTS_TOKEN environment variable not set"); return; } try { robotevents.authentication.setBearer(token); this.isAuthenticated = true; } catch (error) { console.error("Failed to set RobotEvents authentication:", error); } } // Helper method for web search async searchWeb(query) { console.error(`[DEBUG] ======= WEB SEARCH STARTING =======`); console.error(`[DEBUG] Search query: "${query}"`); // Try multiple search strategies const searchStrategies = [ { name: 'DuckDuckGo HTML', url: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query + ' site:robotevents.com')}` }, { name: 'Bing', url: `https://www.bing.com/search?q=${encodeURIComponent(query + ' site:robotevents.com')}` }, { name: 'Google', url: `https://www.google.com/search?q=${encodeURIComponent(query + ' site:robotevents.com')}` } ]; for (const strategy of searchStrategies) { try { console.error(`[DEBUG] Trying search strategy: ${strategy.name}`); console.error(`[DEBUG] Search URL: ${strategy.url}`); console.error(`[DEBUG] Making HTTP request...`); const response = await this.makeHttpRequest(strategy.url); console.error(`[DEBUG] HTTP response length: ${response.length} characters`); console.error(`[DEBUG] Response preview: ${response.substring(0, 200)}...`); // Check if we got a valid HTML response (not blocked) if (response.includes('<html') && !response.includes('Just a moment')) { console.error(`[DEBUG] ✅ Got valid HTML response from ${strategy.name}`); // Extract robotevents.com URLs from the response console.error(`[DEBUG] Extracting robotevents URLs...`); const robotEventsUrls = this.extractRobotEventsUrls(response); console.error(`[DEBUG] Found ${robotEventsUrls.length} robotevents URLs:`); robotEventsUrls.forEach((url, i) => { console.error(`[DEBUG] ${i + 1}. ${url}`); }); if (robotEventsUrls.length > 0) { console.error(`[DEBUG] ✅ ${strategy.name} found results!`); console.error(`[DEBUG] ======= WEB SEARCH COMPLETED =======`); return robotEventsUrls; } } else { console.error(`[DEBUG] ❌ ${strategy.name} blocked or invalid response`); } } catch (error) { console.error(`[ERROR] ${strategy.name} search failed:`, error instanceof Error ? error.message : String(error)); // Continue to next strategy } } console.error(`[ERROR] All search strategies failed`); console.error(`[DEBUG] ======= WEB SEARCH FAILED =======`); return []; } // HTTP request helper async makeHttpRequest(url) { return new Promise((resolve, reject) => { const urlObj = new URL(url); const client = urlObj.protocol === 'https:' ? https : http; const options = { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'identity', // Don't request compression to avoid decode issues 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } }; console.error(`[DEBUG] HTTP request options:`, JSON.stringify(options.headers)); const req = client.request(url, options, (res) => { console.error(`[DEBUG] HTTP response status: ${res.statusCode}`); console.error(`[DEBUG] HTTP response headers:`, JSON.stringify(res.headers)); let data = ''; // Handle different encodings if (res.headers['content-encoding'] === 'gzip') { console.error(`[DEBUG] Response is gzip encoded, need to decompress`); const zlib = require('zlib'); const gunzip = zlib.createGunzip(); res.pipe(gunzip); gunzip.on('data', (chunk) => data += chunk); gunzip.on('end', () => { console.error(`[DEBUG] Decompressed response length: ${data.length}`); resolve(data); }); gunzip.on('error', reject); } else { res.on('data', (chunk) => data += chunk); res.on('end', () => { console.error(`[DEBUG] Raw response length: ${data.length}`); resolve(data); }); } }); req.on('error', (error) => { console.error(`[DEBUG] HTTP request error:`, error); reject(error); }); req.setTimeout(15000, () => { console.error(`[DEBUG] HTTP request timeout after 15s`); req.destroy(); reject(new Error('Request timeout')); }); req.end(); }); } // Extract robotevents.com URLs from HTML content extractRobotEventsUrls(html) { console.error(`[DEBUG] ======= URL EXTRACTION STARTING =======`); const urls = []; // More comprehensive URL patterns for robotevents.com const patterns = [ // Complete URLs with protocol { name: 'Full event URLs', regex: /https?:\/\/(?:www\.)?robotevents\.com\/robot-competitions\/[^"'\s<>)]+/gi }, { name: 'Events path URLs', regex: /https?:\/\/(?:www\.)?robotevents\.com\/events\/[^"'\s<>)]+/gi }, { name: 'Any robotevents URLs', regex: /https?:\/\/(?:www\.)?robotevents\.com\/[^"'\s<>)]+/gi }, // URLs without protocol { name: 'Protocol-less full', regex: /(?:www\.)?robotevents\.com\/robot-competitions\/[^"'\s<>)]+/gi }, { name: 'Protocol-less events', regex: /(?:www\.)?robotevents\.com\/events\/[^"'\s<>)]+/gi }, { name: 'Protocol-less any', regex: /(?:www\.)?robotevents\.com\/[^"'\s<>)]+/gi }, // Quoted URLs (common in search results) { name: 'Quoted URLs', regex: /"(https?:\/\/(?:www\.)?robotevents\.com\/[^"]+)"/gi }, { name: 'Href attributes', regex: /href\s*=\s*["']([^"']*robotevents\.com[^"']*?)["']/gi }, // Data attributes and JSON (search engines often use these) { name: 'Data URLs', regex: /data-[^=]*=\s*["']([^"']*robotevents\.com[^"']*?)["']/gi }, { name: 'URL parameters', regex: /[?&]url=([^&"'\s]*robotevents\.com[^&"'\s]*)/gi }, // Search engine redirect URLs (DuckDuckGo, Bing, etc.) { name: 'DuckDuckGo redirects', regex: /uddg=([^&"'\s]*robotevents\.com[^&"'\s]*)/gi }, { name: 'Search redirects', regex: /redirect[^=]*=([^&"'\s]*robotevents\.com[^&"'\s]*)/gi } ]; console.error(`[DEBUG] Trying ${patterns.length} URL extraction patterns...`); patterns.forEach((pattern, i) => { console.error(`[DEBUG] Pattern ${i + 1}: ${pattern.name}`); let matches; if (pattern.regex.source.includes('(') && pattern.regex.source.includes(')')) { // Pattern has capture groups - extract the captured URL matches = []; let match; const regex = new RegExp(pattern.regex.source, pattern.regex.flags); while ((match = regex.exec(html)) !== null) { if (match[1]) { let url = match[1]; // Decode URL-encoded strings from search engines try { url = decodeURIComponent(url); } catch (e) { // If decoding fails, use original } matches.push(url); } if (regex.global === false) break; } } else { // Pattern matches the whole URL matches = html.match(pattern.regex) || []; // Also decode these URLs matches = matches.map(url => { try { return decodeURIComponent(url); } catch (e) { return url; } }); } console.error(`[DEBUG] Found ${matches.length} matches`); for (const match of matches) { // Clean up and normalize URL let url = match.trim(); // Remove common trailing characters that shouldn't be part of URL url = url.replace(/[.,;)}>]+$/, ''); // Ensure URL has protocol if (!url.startsWith('http')) { url = 'https://' + url; } // Validate it's actually a robotevents URL and not a search page if (url.includes('robotevents.com') && !urls.includes(url) && this.isValidEventUrl(url)) { urls.push(url); console.error(`[DEBUG] Added URL: ${url}`); } else if (url.includes('robotevents.com')) { console.error(`[DEBUG] Rejected URL (not event page): ${url}`); } } }); // Also search for robotevents mentions in the raw HTML to see if there are any const robotEventsMatches = html.match(/robotevents/gi) || []; console.error(`[DEBUG] Found ${robotEventsMatches.length} mentions of "robotevents" in HTML`); // Try to find URL-like patterns near robotevents mentions if (robotEventsMatches.length > 0 && urls.length === 0) { console.error(`[DEBUG] No URLs found but robotevents mentioned - searching for URL patterns near mentions`); // Look for patterns that might contain event IDs const contextPatterns = [ /robotevents[^"'\s<>]{0,50}[A-Z]{2}-[A-Z]{2,4}-\d{2}-\d+/gi, // SKU patterns /[A-Z]{2}-[A-Z]{2,4}-\d{2}-\d+[^"'\s<>]{0,50}robotevents/gi, // SKU before robotevents /\/(\d{4,})\D/g // Numeric IDs in URLs ]; for (const pattern of contextPatterns) { const matches = html.match(pattern) || []; if (matches.length > 0) { console.error(`[DEBUG] Found potential context matches:`, matches.slice(0, 3)); } } } console.error(`[DEBUG] Total unique URLs found: ${urls.length}`); console.error(`[DEBUG] ======= URL EXTRACTION COMPLETED =======`); return urls; } // Extract event ID from robotevents URL extractEventId(url) { console.error(`[DEBUG] ======= EVENT ID EXTRACTION STARTING =======`); console.error(`[DEBUG] Input URL: ${url}`); // Try different URL patterns for event IDs - ordered by specificity const patterns = [ // Most specific patterns first { name: 'Event SKU in events path', regex: /\/events\/([A-Z]{2}-[A-Z]{2,4}-\d{2}-\d+)/i }, { name: 'Event SKU in competitions path', regex: /\/robot-competitions\/[^\/]*\/([A-Z]{2}-[A-Z]{2,4}-\d{2}-\d+)/i }, { name: 'Event SKU anywhere', regex: /([A-Z]{2}-[A-Z]{2,4}-\d{2}-\d+)/i }, // URL parameter patterns { name: 'Event ID parameter', regex: /[?&]event[_-]?id=([^&\s]+)/i }, { name: 'ID parameter', regex: /[?&]id=([^&\s]+)/i }, { name: 'SKU parameter', regex: /[?&]sku=([^&\s]+)/i }, // Path-based patterns { name: 'Events path numeric', regex: /\/events\/(\d+)/i }, { name: 'Events path alphanumeric', regex: /\/events\/([^\/\?#]+)/i }, { name: 'Competitions path final segment', regex: /\/robot-competitions\/[^\/]*\/([^\/\?#]+)/i }, // Generic patterns (lower priority) { name: 'Trailing numeric ID', regex: /\/(\d{4,})(?:\/|$|\?)/ }, { name: 'Any path segment with dashes', regex: /\/([a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+)/i }, // Hash/fragment patterns { name: 'Hash event ID', regex: /#event[_-]?id[=:]([^&\s]+)/i }, { name: 'Hash ID', regex: /#id[=:]([^&\s]+)/i } ]; console.error(`[DEBUG] Trying ${patterns.length} extraction patterns...`); for (let i = 0; i < patterns.length; i++) { const pattern = patterns[i]; console.error(`[DEBUG] Pattern ${i + 1} (${pattern.name}): ${pattern.regex}`); const match = url.match(pattern.regex); if (match && match[1]) { let eventId = match[1]; console.error(`[DEBUG] ✅ MATCH: Found "${eventId}" using ${pattern.name}`); // Clean up the event ID eventId = this.cleanEventId(eventId); console.error(`[DEBUG] 🧹 Cleaned ID: "${eventId}"`); // Validate the extracted ID if (this.isValidEventId(eventId)) { console.error(`[DEBUG] ✅ SUCCESS: Event ID "${eventId}" is valid`); console.error(`[DEBUG] ======= EVENT ID EXTRACTION COMPLETED =======`); return eventId; } else { console.error(`[DEBUG] ❌ INVALID: Event ID "${eventId}" failed validation`); } } else { console.error(`[DEBUG] ❌ No match with ${pattern.name}`); } } console.error(`[ERROR] No valid event ID found in URL with any pattern`); console.error(`[DEBUG] ======= EVENT ID EXTRACTION FAILED =======`); return null; } // Validate if a URL is a real event page (not search results) isValidEventUrl(url) { console.error(`[DEBUG] Validating URL: ${url}`); // Must contain robotevents.com if (!url.includes('robotevents.com')) { console.error(`[DEBUG] ❌ Not a robotevents.com URL`); return false; } // Reject search result pages and other non-event URLs const invalidPatterns = [ 'google.com', // Google search results 'bing.com', // Bing search results 'duckduckgo.com', // DuckDuckGo search results 'format=rss', // RSS feeds '/search?', // Search pages with query params 'uddg=', // DuckDuckGo redirect parameters (if still present) ]; for (const pattern of invalidPatterns) { if (url.includes(pattern)) { console.error(`[DEBUG] ❌ Contains invalid pattern: ${pattern}`); return false; } } // Must have valid URL structure try { const urlObj = new URL(url); if (!urlObj.hostname) { console.error(`[DEBUG] ❌ Invalid URL structure`); return false; } } catch (error) { console.error(`[DEBUG] ❌ URL parsing failed: ${error instanceof Error ? error.message : String(error)}`); return false; } // Should contain event-related paths const validPatterns = [ '/events/', '/robot-competitions/', '/teams/', // Sometimes team pages link to events ]; const hasValidPattern = validPatterns.some(pattern => url.includes(pattern)); if (hasValidPattern) { console.error(`[DEBUG] ✅ Valid event URL`); return true; } else { console.error(`[DEBUG] ❌ No valid event patterns found`); return false; } } // Clean up extracted event ID cleanEventId(id) { console.error(`[DEBUG] Cleaning event ID: "${id}"`); let cleanId = id.trim(); // Remove common file extensions cleanId = cleanId.replace(/\.(html|htm|php|aspx?)$/i, ''); // Remove leading/trailing slashes and whitespace cleanId = cleanId.replace(/^\/+|\/+$/g, '').trim(); console.error(`[DEBUG] Cleaned result: "${cleanId}"`); return cleanId; } // Validate if an extracted ID looks like a valid event ID isValidEventId(id) { // VEX event IDs typically follow these patterns: // - SKU format: RE-VRC-23-1234, RE-VIQC-23-5678, etc. // - Numeric IDs: usually 4+ digits // - Should not be too generic (like single letters or very short) console.error(`[DEBUG] Validating event ID: "${id}"`); // VEX SKU format (most reliable) if (/^[A-Z]{2}-[A-Z]{2,4}-\d{2}-\d+$/i.test(id)) { console.error(`[DEBUG] ✅ Valid SKU format`); return true; } // Numeric ID (4+ digits) if (/^\d{4,}$/.test(id)) { console.error(`[DEBUG] ✅ Valid numeric ID (${id.length} digits)`); return true; } // Mixed alphanumeric (reasonable length) if (/^[a-zA-Z0-9-_]{6,}$/.test(id) && id.includes('-')) { console.error(`[DEBUG] ✅ Valid mixed alphanumeric ID`); return true; } // Reject too short or too generic IDs if (id.length < 3) { console.error(`[DEBUG] ❌ Too short (${id.length} chars)`); return false; } // Reject common non-ID strings const invalidIds = [ 'event', 'events', 'competition', 'robot', 'vex', 'index', 'home', 'main', 'standings', 'skills', 'teams', 'rankings', 'awards', 'matches', 'webcasts', 'photos', 'documents', 'about', 'contact' ]; if (invalidIds.includes(id.toLowerCase())) { console.error(`[DEBUG] ❌ Generic non-ID string: ${id}`); return false; } console.error(`[DEBUG] ⚠️ Uncertain but accepting: "${id}"`); return true; } // Build search keywords for web search buildSearchKeywords(params) { console.error(`[DEBUG] ======= BUILDING SEARCH KEYWORDS =======`); console.error(`[DEBUG] Input params:`, JSON.stringify(params)); const keywords = []; if (params.name) { keywords.push(`"${params.name}"`); console.error(`[DEBUG] Added name keyword: "${params.name}"`); } if (params.location) { keywords.push(params.location); console.error(`[DEBUG] Added location keyword: ${params.location}`); } // Always add VEX and robotevents to narrow search keywords.push('VEX'); keywords.push('robotevents.com'); console.error(`[DEBUG] Added standard keywords: VEX, robotevents.com`); // Add program if specified if (params.program) { if (typeof params.program === 'string') { keywords.push(params.program); console.error(`[DEBUG] Added program keyword: ${params.program}`); } } // Add time-based keywords keywords.push('competition'); keywords.push('event'); console.error(`[DEBUG] Added time-based keywords: competition, event`); const query = keywords.join(' '); console.error(`[DEBUG] Final search query: "${query}"`); console.error(`[DEBUG] ======= SEARCH KEYWORDS COMPLETED =======`); return query; } setupTools() { const tools = [ { name: "search-teams", description: "Search for VEX teams by number, name, organization, or location", inputSchema: { type: "object", properties: { number: { type: "string", description: "Team number to search for (e.g., '123A')", }, team_name: { type: "string", description: "Team name to search for", }, organization: { type: "string", description: "Organization/school name to search for", }, location: { type: "string", description: "Location (city, region, or country) to search for", }, program: { type: ["string", "number"], description: "Program to filter by (e.g., 'VRC', 'VIQC', 'VEXU' or program ID)", }, grade: { type: "string", description: "Grade level to filter by (e.g., 'Elementary', 'Middle School', 'High School')", }, registered: { type: "boolean", description: "Filter by registration status", }, }, additionalProperties: false, }, }, { name: "get-team-info", description: "Get detailed information about a specific VEX team", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "The team ID to get information for", }, team_number: { type: "string", description: "Alternatively, specify team number (e.g., '123A')", }, }, additionalProperties: false, oneOf: [ { required: ["team_id"] }, { required: ["team_number"] } ], }, }, { name: "search-events", description: "Search for VEX events by name, location, date, or program", inputSchema: { type: "object", properties: { sku: { type: "string", description: "Event SKU to search for", }, name: { type: "string", description: "Event name to search for", }, start: { type: "string", description: "Start date filter (YYYY-MM-DD format)", }, end: { type: "string", description: "End date filter (YYYY-MM-DD format)", }, season: { type: "number", description: "Season ID to filter by", }, program: { type: ["string", "number"], description: "Program to filter by (e.g., 'VRC', 'VIQC', 'VEXU' or program ID)", }, location: { type: "string", description: "Location (city, region, or country) to search for", }, }, additionalProperties: false, }, }, { name: "get-event-details", description: "Get detailed information about a specific VEX event", inputSchema: { type: "object", properties: { event_id: { type: "number", description: "The event ID to get information for", }, sku: { type: "string", description: "Alternatively, specify event SKU", }, }, additionalProperties: false, oneOf: [ { required: ["event_id"] }, { required: ["sku"] } ], }, }, { name: "get-team-rankings", description: "Get team rankings and performance at events", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "The team ID to get rankings for", }, event_id: { type: "number", description: "Optional: specific event ID to get rankings for", }, season: { type: "number", description: "Optional: season ID to filter by", }, }, required: ["team_id"], additionalProperties: false, }, }, { name: "get-skills-scores", description: "Get robot skills scores for teams", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "Optional: specific team ID to get skills for", }, event_id: { type: "number", description: "Optional: specific event ID to get skills for", }, season: { type: "number", description: "Optional: season ID to filter by", }, type: { type: "string", enum: ["driver", "programming"], description: "Optional: filter by skills type (driver or programming)", }, }, additionalProperties: false, }, }, ]; this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools, })); } setupToolHandlers() { this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!this.isAuthenticated) { throw new Error("RobotEvents authentication not set. Please set ROBOTEVENTS_TOKEN environment variable."); } const { name, arguments: args } = request.params; try { switch (name) { case "search-teams": return await this.handleSearchTeams(args); case "get-team-info": return await this.handleGetTeamInfo(args); case "search-events": return await this.handleSearchEvents(args); case "get-event-details": return await this.handleGetEventDetails(args); case "get-team-rankings": return await this.handleGetTeamRankings((args || {})); case "get-skills-scores": return await this.handleGetSkillsScores((args || {})); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { throw new Error(`Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`); } }); } async handleSearchTeams(args) { const searchParams = {}; if (args.number) searchParams["number[]"] = [args.number]; if (args.team_name) searchParams.team_name = args.team_name; if (args.organization) searchParams.organization = args.organization; if (args.location) { searchParams["location.city"] = args.location; } if (args.program) { if (typeof args.program === 'string') { const programMap = { 'VRC': 1, 'VIQC': 41, 'VEXU': 4, }; searchParams["program[]"] = [programMap[args.program.toUpperCase()] || args.program]; } else { searchParams["program[]"] = [args.program]; } } if (args.grade) searchParams.grade = args.grade; if (args.registered !== undefined) searchParams.registered = args.registered; const teams = await robotevents.teams.search(searchParams); return { content: [ { type: "text", text: `Found ${teams.length} teams:\n\n` + teams.map((team) => `**${team.number}** - ${team.team_name}\n` + `Organization: ${team.organization}\n` + `Location: ${team.location?.city || 'N/A'}, ${team.location?.region || 'N/A'}, ${team.location?.country || 'N/A'}\n` + `Program: ${team.program?.name || 'N/A'}\n` + `Grade: ${team.grade || 'N/A'}\n` + `Registered: ${team.registered ? 'Yes' : 'No'}\n`).join('\n'), }, ], }; } async handleGetTeamInfo(args) { let team; if (args.team_id) { team = await robotevents.teams.get(args.team_id); if (!team) { throw new Error(`Team with ID ${args.team_id} not found`); } } else if (args.team_number) { const searchResult = await robotevents.teams.search({ number: [args.team_number] }); if (searchResult.length === 0) { throw new Error(`Team ${args.team_number} not found`); } team = searchResult[0]; } else { throw new Error("Either team_id or team_number must be provided"); } return { content: [ { type: "text", text: `**Team ${team.number}** - ${team.team_name}\n` + `${team.robot_name ? `Robot Name: ${team.robot_name}\n` : ''}` + `Organization: ${team.organization || 'N/A'}\n` + `Program: ${team.program?.name || 'N/A'} (${team.program?.code || 'N/A'})\n` + `Grade: ${team.grade || 'N/A'}\n` + `Location: ${[team.location?.city, team.location?.region, team.location?.country].filter(Boolean).join(', ') || 'N/A'}\n` + `${team.location?.venue ? `Venue: ${team.location.venue}\n` : ''}` + `Registered: ${team.registered ? 'Yes' : 'No'}\n` + `Team ID: ${team.id}`, }, ], }; } async handleSearchEvents(args) { try { // Ensure args is not undefined - this was causing the crash const params = args || {}; console.error(`[DEBUG] Raw args:`, args); console.error(`[DEBUG] Processed params:`, params); // Strategy 1: Try hybrid web search + API approach const webSearchResults = await this.tryWebSearchApproach(params); if (webSearchResults.success) { console.error(`[DEBUG] Web search approach succeeded`); return webSearchResults.response; } console.error(`[DEBUG] Web search approach failed, trying direct API search`); // Strategy 2: Fallback to direct API search (original approach) const apiSearchResults = await this.tryDirectApiSearch(params); if (apiSearchResults.success) { console.error(`[DEBUG] Direct API search succeeded`); return apiSearchResults.response; } // Both approaches failed console.error(`[ERROR] All search approaches failed`); return { content: [ { type: "text", text: `❌ Unable to find events matching your criteria.\n\n` + `Searched for: ${JSON.stringify(params)}\n\n` + `Both web search and direct API search failed. This could be due to:\n` + `- Network connectivity issues\n` + `- Invalid search parameters\n` + `- Temporary service unavailability\n\n` + `Please try again with different search terms.`, }, ], }; } catch (error) { console.error(`[ERROR] Search events failed:`, error); return { content: [ { type: "text", text: `❌ Error searching events: ${error instanceof Error ? error.message : String(error)}\n\n` + `Parameters used: ${JSON.stringify(args)}\n` + `Please check your search parameters and try again.`, }, ], }; } } // Web search + API approach async tryWebSearchApproach(params) { console.error(`[DEBUG] ======= WEB SEARCH APPROACH STARTING =======`); console.error(`[DEBUG] Search parameters:`, JSON.stringify(params)); try { // Build search query console.error(`[DEBUG] Step 1: Building search query...`); const searchQuery = this.buildSearchKeywords(params); if (!searchQuery.trim()) { console.error(`[ERROR] No search query built, cannot proceed with web search`); console.error(`[DEBUG] ======= WEB SEARCH APPROACH ABORTED =======`); return { success: false }; } console.error(`[DEBUG] ✅ Search query ready: "${searchQuery}"`); // Perform web search console.error(`[DEBUG] Step 2: Performing web search...`); const urls = await this.searchWeb(searchQuery); console.error(`[DEBUG] Web search returned ${urls.length} URLs`); if (urls.length === 0) { console.error(`[ERROR] No URLs found in web search results`); console.error(`[DEBUG] ======= WEB SEARCH APPROACH FAILED =======`); return { success: false }; } console.error(`[DEBUG] ✅ Web search successful`); // Extract event IDs and fetch details console.error(`[DEBUG] Step 3: Extracting event IDs and fetching details...`); const events = []; const urlsToProcess = urls.slice(0, 5); // Limit to first 5 URLs console.error(`[DEBUG] Processing ${urlsToProcess.length} URLs (limited from ${urls.length})...`); for (let i = 0; i < urlsToProcess.length; i++) { const url = urlsToProcess[i]; console.error(`[DEBUG] Processing URL ${i + 1}/${urlsToProcess.length}: ${url}`); const eventId = this.extractEventId(url); if (eventId) { console.error(`[DEBUG] ✅ Extracted event ID: "${eventId}"`); try { // Try to get event details using the extracted ID const event = await this.getEventById(eventId); if (event) { events.push(event); console.error(`[DEBUG] ✅ Successfully retrieved event: ${event.name}`); } else { console.error(`[DEBUG] ❌ Event retrieval returned null for ID: ${eventId}`); } } catch (error) { console.error(`[DEBUG] ❌ Failed to get event ${eventId}:`, error instanceof Error ? error.message : String(error)); // Continue with next URL } } else { console.error(`[DEBUG] ❌ Could not extract event ID from URL: ${url}`); } } console.error(`[DEBUG] Event processing completed. Found ${events.length} valid events`); if (events.length === 0) { console.error(`[ERROR] No valid events found from any extracted IDs`); console.error(`[DEBUG] ======= WEB SEARCH APPROACH FAILED =======`); return { success: false }; } // Format response console.error(`[DEBUG] Step 4: Formatting response with ${events.length} events...`); const response = { content: [ { type: "text", text: `🔍 Found ${events.length} events via web search:\n\n` + events.map((event) => `**${event.name}** (${event.sku || 'N/A'})\n` + `Date: ${event.start ? new Date(event.start).toLocaleDateString() : 'N/A'} - ${event.end ? new Date(event.end).toLocaleDateString() : 'N/A'}\n` + `Location: ${[event.location?.city, event.location?.region, event.location?.country].filter(Boolean).join(', ') || 'N/A'}\n` + `Program: ${event.program?.name || 'N/A'}\n` + `Season: ${event.season?.name || 'N/A'}\n` + `Event ID: ${event.id || 'N/A'}\n`).join('\n') + `\n💡 Results found using intelligent web search + API combination.`, }, ], }; console.error(`[DEBUG] ✅ Response formatted successfully`); console.error(`[DEBUG] ======= WEB SEARCH APPROACH COMPLETED =======`); return { success: true, response }; } catch (error) { console.error(`[ERROR] Web search approach failed with exception:`, error instanceof Error ? error.message : String(error)); console.error(`[ERROR] Stack trace:`, error instanceof Error ? error.stack : 'N/A'); console.error(`[DEBUG] ======= WEB SEARCH APPROACH FAILED =======`); return { success: false }; } } // Helper method to get event by ID (try multiple ID formats) async getEventById(eventId) { console.error(`[DEBUG] ======= GET EVENT BY ID STARTING =======`); console.error(`[DEBUG] Attempting to retrieve event: "${eventId}"`); // Try different ways to get the event with improved error handling const attempts = [ { name: 'String ID direct', func: async () => await robotevents.events.get(eventId) }, { name: 'Numeric ID conversion', func: async () => { const numId = parseInt(eventId); if (isNaN(numId)) { throw new Error('Event ID is not numeric'); } return await robotevents.events.get(numId); } }, { name: 'Search by SKU', func: async () => { console.error(`[DEBUG] Trying search by SKU: ${eventId}`); const results = await robotevents.events.search({ sku: [eventId] }); return results && results.length > 0 ? results[0] : null; } } ]; console.error(`[DEBUG] Will try ${attempts.length} different API call methods with retries`); for (let i = 0; i < attempts.length; i++) { const attempt = attempts[i]; console.error(`[DEBUG] Attempt ${i + 1}: ${attempt.name}`); // Retry logic for each attempt for (let retry = 0; retry < 2; retry++) { try { if (retry > 0) { console.error(`[DEBUG] Retry ${retry} for ${attempt.name}`); // Wait briefly before retry await new Promise(resolve => setTimeout(resolve, 1000)); } const event = await this.executeWithTimeout(attempt.func, 30000); // 30s timeout if (event) { console.error(`[DEBUG] ✅ SUCCESS: Retrieved event using ${attempt.name} (retry ${retry})`); console.error(`[DEBUG] Event details: ${event.name} (${event.sku || event.id})`); console.error(`[DEBUG] ======= GET EVENT BY ID COMPLETED =======`); return event; } else { console.error(`[DEBUG] ❌ ${attempt.name} returned null/undefined (retry ${retry})`); break; // No point retrying if result is null } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`[DEBUG] ❌ ${attempt.name} threw error (retry ${retry}):`, errorMsg); // Check for specific error types if (this.isRetryableError(error)) { console.error(`[DEBUG] Error appears retryable, will retry if attempts remain`); if (retry === 1) { // Last retry console.error(`[DEBUG] Max retries reached for ${attempt.name}`); } } else { console.error(`[DEBUG] Error not retryable, skipping to next method`); break; // Don't retry non-retryable errors } } } } console.error(`[ERROR] All ${attempts.length} attempts to get event "${eventId}" failed with retries`); console.error(`[DEBUG] ======= GET EVENT BY ID FAILED =======`); return null; } // Execute a function with timeout async executeWithTimeout(func, timeoutMs) { return Promise.race([ func(), new Promise((_, reject) => setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)) ]); } // Check if an error is retryable isRetryableError(error) { if (!(error instanceof Error)) return false; const errorMsg = error.message.toLowerCase(); // Network-related errors that might be temporary const retryablePatterns = [ 'timeout', 'network', 'connection', 'econnreset', 'enotfound', 'socket hang up', '503', '502', '504', 'rate limit' ]; // Cloudflare protection is usually not retryable quickly const nonRetryablePatterns = [ 'just a moment', 'cloudflare', '403', 'forbidden', 'unauthorized' ]; for (const pattern of nonRetryablePatterns) { if (errorMsg.includes(pattern)) { console.error(`[DEBUG] Non-retryable error detected: ${pattern}`); return false; } } for (const pattern of retryablePatterns) { if (errorMsg.includes(pattern)) { console.error(`[DEBUG] Retryable error detected: ${pattern}`); return true; } } // Default to not retryable for unknown errors return false; } // Direct API search approach (original method) async tryDirectApiSearch(params) { console.error(`[DEBUG] ======= DIRECT API SEARCH STARTING =======`); console.error(`[DEBUG] Input params:`, JSON.stringify(params)); try { const searchParams = {}; // Build search parameters with improved validation console.error(`[DEBUG] Building API search parameters...`); if (params.sku) { searchParams.sku = Array.isArray(params.sku) ? params.sku : [params.sku]; console.error(`[DEBUG] Added SKU parameter:`, searchParams.sku); } // Note: The robotevents API might not support 'name' parameter // Let's try different parameter names if (params.name) { // Try multiple possible name parameters searchParams.name = params.name; console.error(`[DEBUG] Added name parameter: ${params.name}`); } if (params.start) { searchParams.start = params.start; console.error(`[DEBUG] Added start date: ${params.start}`); } if (params.end) { searchParams.end = params.end; console.error(`[DEBUG] Added end date: ${params.end}`); } if (params.season) { searchParams.season = Array.isArray(params.season) ? params.season : [params.season]; console.error(`[DEBUG] Added season parameter:`, searchParams.season); } if (params.program) { if (typeof params.program === 'string') { const programMap = { 'VRC': 1, 'VIQC': 41, 'VEXU': 4, }; const programId = programMap[params.program.toUpperCase()]; if (programId) { searchParams.program = [programId]; console.error(`[DEBUG] Mapped program "${params.program}" to ID: ${programId}`); } else { searchParams.program = [params.program]; console.error(`[DEBUG] Using program parameter as-is: ${params.program}`); } } else { searchParams.program = [params.program]; console.error(`[DEBUG] Added numeric program parameter: ${params.program}`); } } if (params.location) { // Try different location parameter names searchParams.city = params.location; searchParams.region = params.location; console.error(`[DEBUG] Added location parameter as city/region: ${params.location}`); } console.error(`[DEBUG] Final API search parameters:`, JSON.stringify(searchParams)); // Retry the API call with timeout let events; let lastError; for (let attempt = 1; attempt <= 3; attempt++) { try { console.error(`[DEBUG] API call attempt ${attempt}/3...`); events = await this.executeWithTimeout(() => robotevents.events.search(searchParams), 30000 // 30s timeout ); console.error(`[DEBUG] ✅ API call succeeded on attempt ${attempt}`); break; } catch (error) { lastError = error; console.error(`[DEBUG] ❌ API call attempt ${attempt} failed:`, error instanceof Error ? error.message : String(error));