Skip to main content
Glama
test-comprehensive.js47.1 kB
#!/usr/bin/env node /* * Copyright (C) 2025 TomTom NV * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Comprehensive Test Suite for TomTom MCP Server Tools * * This script thoroughly tests all TomTom MCP server tools with all parameters, * including optional ones, to verify proper functionality. * * Usage: * node test-comprehensive.js [toolName] [--verbose] */ import dotenv from 'dotenv'; import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import { existsSync } from 'fs'; import process from 'process'; import console from 'console'; // Load environment variables dotenv.config(); // Get directory paths and find server const __dirname = dirname(fileURLToPath(import.meta.url)); // Try different possible locations for the server const possibleServerPaths = [ resolve(__dirname, '..', 'bin', 'tomtom-mcp.js'), // if script is in tests/ folder resolve(__dirname, 'bin', 'tomtom-mcp.js'), // if script is in project root resolve(__dirname, '..', 'tomtom-mcp.js'), // if server is one level up resolve(__dirname, 'tomtom-mcp.js'), // if server is in same folder ]; let serverPath = null; for (const path of possibleServerPaths) { if (existsSync(path)) { serverPath = path; break; } } if (!serverPath) { console.error('❌ Could not find TomTom MCP server file!'); console.error('Searched in:'); possibleServerPaths.forEach(path => console.error(` - ${path}`)); console.error('\nPlease ensure your server file exists and update the path in the script.'); process.exit(1); } // Configuration const TEST_TOOL = process.argv[2]?.toLowerCase(); const VERBOSE = process.argv.includes('--verbose'); // Map provider: when MAPS=orbis we must use Orbis-specific parameter types const MAPS_ENV = process.env.MAPS?.toLowerCase() || ''; // Orbis expects traffic as string: 'live' | 'historical', while Genesis uses boolean const TRAFFIC = MAPS_ENV === 'orbis' ? 'live' : true; // More comprehensive test scenarios with all parameters const COMPREHENSIVE_TEST_SCENARIOS = { // Traffic tool tests with all parameters "tomtom-traffic": [ { name: 'Traffic by query with all parameters', params: { bbox: '4.8,52.3,4.95,52.4', // Amsterdam area language: 'en-US', maxResults: 20, // categoryFilter: '0,6,7,8', // Accidents, lane closures, road closures, road works timeFilter: 'present', t: Math.floor(Date.now() / 1000), // Current timestamp // incidentTypes: '0' // All incident types }, expected: { hasResults: true, validStructure: true } }, { name: 'Traffic by bounding box with all parameters', params: { bbox: '4.8,52.3,4.95,52.4', // Amsterdam area language: 'en-US', maxResults: 15, categoryFilter: '0,8', // Accidents and road works timeFilter: 'present', t: 1750881020 // Current timestamp }, expected: { validStructure: true } }, { name: 'Traffic with incident types parameter', params: { bbox: '4.8,52.3,4.95,52.4', // Amsterdam area incidentTypes: '0,5,7', // Accident, Lane Restrictions, Closure maxResults: 10 }, expected: { validStructure: true } }, // Negative test cases for tomtom-traffic { name: 'negative: Missing bbox (required for traffic)', params: { language: 'en-US', maxResults: 10 }, expected: { shouldFail: true } }, { name: 'negative: Invalid maxResults (too high)', params: { bbox: '4.8,52.3,4.95,52.4', maxResults: 2000 }, expected: { shouldFail: true } } ], // Routing tool tests with all parameters "tomtom-routing": [ { name: 'Basic routing with all parameters', params: { origin: { lat: 52.3740, lon: 4.8897 }, // Amsterdam destination: { lat: 52.5200, lon: 13.4050 }, // Berlin travelMode: 'car', routeType: 'fastest', traffic: TRAFFIC, avoid: ['tollRoads', 'unpavedRoads'], departAt: 'now', sectionType: ['toll', 'motorway'], maxAlternatives: 2, instructionsType: 'tagged', vehicleHeading: 90, vehicleCommercial: true, vehicleEngineType: 'combustion', vehicleMaxSpeed: 130, vehicleWeight: 1500, vehicleAxleWeight: 1000, vehicleLength: 4.5, vehicleWidth: 2.0, vehicleHeight: 1.8, language: 'en-US', report: "effectiveSettings", // hilliness: 'normal', // windingness: 'normal', vehicleNumberOfAxles: 2, constantSpeedConsumptionInLitersPerHundredkm: '50,6.3:130,11.5' }, expected: { hasResults: true, hasRoute: true, hasLegs: true } }, { name: 'EV routing with all EV parameters', params: { origin: { lat: 51.5074, lon: -0.1278 }, // London destination: { lat: 52.2053, lon: 0.1218 }, // Cambridge travelMode: 'car', routeType: 'eco', traffic: TRAFFIC, vehicleWeight: 1500, vehicleEngineType: 'electric', vehicleEnergyBudgetInKWh: 30, vehicleCurrentChargeInKWh: 20, vehicleMaxChargeInKWh: 40, vehicleConsumptionInKWhPerHundredKm: 15, avoid: ['motorways'], instructionsType: 'tagged', sectionType: ['tunnel'], constantSpeedConsumptionInkWhPerHundredkm: '50,8.2:130,21.3', auxiliaryPowerInkW: 1.5, accelerationEfficiency: 0.8, decelerationEfficiency: 0.8, uphillEfficiency: 0.9, downhillEfficiency: 0.9, report: "effectiveSettings", vehicleMaxSpeed: 120 }, expected: { hasResults: true, hasRoute: true } }, { name: 'Routing with all advanced parameters', params: { origin: { lat: 52.3740, lon: 4.8897 }, destination: { lat: 52.5200, lon: 13.4050 }, travelMode: 'car', routeType: 'fastest', traffic: TRAFFIC, avoid: ['tollRoads'], departAt: 'now', sectionType: ['toll'], maxAlternatives: 1, instructionsType: 'tagged', vehicleHeading: 90, vehicleCommercial: false, vehicleEngineType: 'combustion', vehicleMaxSpeed: 120, vehicleWeight: 1500, vehicleAxleWeight: 1000, vehicleLength: 4.5, vehicleWidth: 2.0, vehicleHeight: 1.8, language: 'en-US', report: "effectiveSettings", vehicleNumberOfAxles: 2, constantSpeedConsumptionInLitersPerHundredkm: '50,6.3:130,11.5', includeTollPaymentTypes: "all" // Include all toll payment types }, expected: { hasResults: true, hasRoute: true } }, { name: 'Routing with all advanced parameters (hilliness, windingness, includeTollPaymentTypes)', params: { origin: { lat: 49.4447, lon: 7.7690 }, destination: { lat: 49.4847, lon: 8.4767 }, travelMode: 'car', routeType: 'thrilling', traffic: TRAFFIC, avoid: ['tollRoads'], departAt: 'now', sectionType: ['toll'], maxAlternatives: 1, instructionsType: 'tagged', vehicleHeading: 90, vehicleCommercial: false, vehicleEngineType: 'combustion', vehicleMaxSpeed: 120, vehicleWeight: 1500, vehicleAxleWeight: 1000, vehicleLength: 4.5, vehicleWidth: 2.0, vehicleHeight: 1.8, language: 'en-US', report: "effectiveSettings", vehicleNumberOfAxles: 2, constantSpeedConsumptionInLitersPerHundredkm: '50,6.3:130,11.5', hilliness: 'high', windingness: 'normal', includeTollPaymentTypes: "all" // Include all toll payment types }, expected: { hasResults: true, hasRoute: true } }, // Negative test cases for tomtom-routing { name: 'negative: Missing origin', params: { destination: { lat: 52.5200, lon: 13.4050 } }, expected: { shouldFail: true } }, { name: 'negative: Invalid travelMode', params: { origin: { lat: 52.3740, lon: 4.8897 }, destination: { lat: 52.5200, lon: 13.4050 }, travelMode: 'spaceship' }, expected: { shouldFail: true } } ], // Waypoint routing with all parameters "tomtom-waypoint-routing": [ { name: 'Multi-city tour with all parameters', params: { waypoints: [ { lat: 52.3740, lon: 4.8897 }, // Amsterdam { lat: 51.2217, lon: 4.4051 }, // Antwerp ], travelMode: 'car', routeType: 'thrilling', traffic: TRAFFIC, avoid: ['tollRoads','ferries'], departAt: 'now', sectionType: ['toll','motorway','urban'], instructionsType: 'tagged', vehicleCommercial: false, vehicleEngineType: 'combustion', computeBestOrder: false, report: "effectiveSettings", routeRepresentation: 'polyline', language: 'en-US', enhancedNarrative: true, maxAlternatives: 1, vehicleMaxSpeed: 120 }, expected: { hasResults: true, hasRoute: true, hasMultipleLegs: true } }, { name: 'Waypoint routing with includeTollPaymentTypes', params: { waypoints: [ { lat: 52.3740, lon: 4.8897 }, { lat: 51.2217, lon: 4.4051 } ], travelMode: 'car', routeType: 'fastest', traffic: TRAFFIC, avoid: ['tollRoads'], departAt: 'now', sectionType: ['toll'], instructionsType: 'tagged', vehicleCommercial: false, vehicleEngineType: 'combustion', computeBestOrder: false, report: "effectiveSettings", routeRepresentation: 'polyline', language: 'en-US', enhancedNarrative: true, maxAlternatives: 1, vehicleMaxSpeed: 120 }, expected: { hasResults: true, hasRoute: true } }, // Negative test cases for tomtom-waypoint-routing { name: 'negative: Waypoints array too short', params: { waypoints: [{ lat: 52.3740, lon: 4.8897 }] }, expected: { shouldFail: true } } ], // Reachable range with all parameters "tomtom-reachable-range": [ { name: 'Time-based reachable range with all parameters', params: { origin: { lat: 52.3740, lon: 4.8897 }, // Amsterdam timeBudgetInSec: 1800, // 30 minutes travelMode: 'car', vehicleWeight: 2000, routeType: 'thrilling', traffic: TRAFFIC, avoid: ['tollRoads'], departAt: 'now', vehicleEngineType: 'combustion', vehicleFuelEconomyInLiterPerHundredKm: 8.5, report: "effectiveSettings", vehicleMaxSpeed: 120, maxFerryLengthInMeters: 500 }, expected: { hasData: true, hasPolygons: true } }, { name: 'Distance-based reachable range with all parameters', params: { origin: { lat: 51.5074, lon: -0.1278 }, // London distanceBudgetInMeters: 10000, // 10km travelMode: 'car', routeType: 'eco', traffic: TRAFFIC, vehicleWeight: 2000, vehicleEngineType: 'electric', vehicleEnergyBudgetInKWh: 5, vehicleConsumptionInKWhPerHundredKm: 15, report: "effectiveSettings", avoid: ['motorways', 'unpavedRoads'], departAt: 'now', vehicleMaxSpeed: 100, constantSpeedConsumptionInkWhPerHundredkm: '50,8.2:130,21.3', auxiliaryPowerInkW: 1.5 }, expected: { hasData: true, hasPolygons: true } }, { name: 'Energy-based reachable range for EV', params: { origin: { lat: 52.5200, lon: 13.4050 }, // Berlin energyBudgetInkWh: 10, // 10 kWh travelMode: 'car', routeType: 'fastest', vehicleWeight: 2000, traffic: TRAFFIC, vehicleEngineType: 'electric', vehicleConsumptionInKWhPerHundredKm: 15, constantSpeedConsumptionInkWhPerHundredkm: "50,8.2:130,21.3", departAt: 'now', report: "effectiveSettings", avoid: ['unpavedRoads'], auxiliaryPowerInkW: 1.0, accelerationEfficiency: 0.8, decelerationEfficiency: 0.8, uphillEfficiency: 0.9, downhillEfficiency: 0.9, vehicleMaxSpeed: 110 }, expected: { hasData: true } }, { name: 'Reachable range with vehicleCurrentChargeInKWh and vehicleMaxChargeInKWh', params: { origin: { lat: 52.3740, lon: 4.8897 }, timeBudgetInSec: 1800, travelMode: 'car', vehicleWeight: 2000, routeType: 'eco', traffic: TRAFFIC, avoid: ['tollRoads'], departAt: 'now', vehicleEngineType: 'electric', vehicleCurrentChargeInKWh: 15, vehicleMaxChargeInKWh: 40, vehicleConsumptionInKWhPerHundredKm: 15, report: "effectiveSettings", vehicleMaxSpeed: 120 }, expected: { hasData: true } }, // Negative test cases for tomtom-reachable-range { name: 'negative: Missing both timeBudgetInSec and distanceBudgetInMeters', params: { origin: { lat: 52.3740, lon: 4.8897 } }, expected: { shouldFail: true } } ], // Geocoding with all parameters "tomtom-geocode": [ { name: 'Geocode with all parameters', params: { query: 'Amsterdam Central Station, Netherlands', limit: 5, language: 'en-US', extendedPostalCodesFor: 'PAD', countrySet: 'NL', radius: 10000, center: { lat: 52.3740, lon: 4.8897 }, typeahead: true, view: 'Unified', entityTypeSet: 'Country,Municipality', mapcodes: ["Local"], geometries: true, addressRanges: true, topLeft: '52.4,4.8', btmRight: '52.3,4.9' }, expected: { hasResults: true, contains: ['Amsterdam'] } }, // Negative test cases for tomtom-geocode { name: 'negative: Missing query', params: { limit: 5 }, expected: { shouldFail: true } } ], // Reverse geocode with all parameters "tomtom-reverse-geocode": [ { name: 'Reverse geocode with all parameters', params: { lat: 52.3740, lon: 4.8897, limit: 5, language: 'en-US', extendedPostalCodesFor: 'PAD', countrySet: 'NL,BE,DE', radius: 10000, entityTypeSet: 'Country,Municipality,CountrySubdivision', returnMatchType: true, returnSpeedLimit: true, returnRoadUse: true, roadUse: ['Highway', 'Arterial'], allowFreeformNewLine: true, returnAddressNames: true, heading: 90, returnRoadAccessibility: true, returnCommune: true, mapcodes: ["Local"], geometries: true, addressRanges: true }, expected: { hasResults: true, contains: ['Amsterdam'] } }, // Negative test cases for tomtom-reverse-geocode { name: 'negative: Missing lat', params: { lon: 4.8897 }, expected: { shouldFail: true } } ], // Nearby search with all parameters "tomtom-nearby": [ { name: 'Nearby search with all parameters', params: { lat: 52.3740, lon: 4.8897, category: '7315', // Restaurants radius: 2000, limit: 10, language: 'en-US', countrySet: 'NL', openingHours: 'nextSevenDays', timeZone: "iana", relatedPois: 'child', brandSet: '', connectorSet: '', minPowerKW: 50, maxPowerKW: 350, view: 'Unified', entityTypeSet: 'Country', chargingAvailability: true, parkingAvailability: true, fuelAvailability: true, minFuzzyLevel: 1, maxFuzzyLevel: 4, roadUse: true, ofs: 0, sort: 'distance', ext: '', categorySet: '7315' }, expected: { hasResults: true } }, // Negative test cases for tomtom-nearby { name: 'negative: Missing lat/lon', params: { radius: 1000 }, expected: { shouldFail: true } } ], // Fuzzy search with all parameters "tomtom-fuzzy-search": [ { name: 'Fuzzy search with all parameters', params: { query: 'restaurants in Amsterdam', lat: 52.3740, lon: 4.8897, radius: 10000, limit: 10, language: 'en-US', extendedPostalCodesFor: 'PAD', countrySet: 'NL', typeahead: true, categorySet: '7315', brandSet: '', connectorSet: '', minPowerKW: 0, maxPowerKW: 0, fuelSet: 'Petrol,LPG', vehicleTypeSet: "Car", view: 'Unified', entityTypeSet: 'Country,Municipality', maxFuzzyLevel: 4, minFuzzyLevel: 1, ofs: 0, relatedPois: 'child', sort: 'distance', ext: '', openingHours: "nextSevenDays", mapcodes: ["Local"], geometries: true, addressRanges: true, timeZone: 'iana', connectors: true, roadUse: true, topLeft: '52.4,4.8', btmRight: '52.3,4.9' }, expected: { hasResults: true, contains: ['Amsterdam'] } }, // Negative test cases for tomtom-fuzzy-search { name: 'negative: Missing query', params: { lat: 52.3740, lon: 4.8897 }, expected: { shouldFail: true } } ], // Static maps with all parameters "tomtom-static-map": [ { name: 'Static map with all parameters', params: { center: { lat: 52.3740, lon: 4.8897 }, zoom: 12, width: 800, height: 600, layer: 'basic', style: 'main', markers: [ { position: { lat: 52.3740, lon: 4.8897 }, color: 'red', text: 'A' }, { position: { lat: 52.3680, lon: 4.9000 }, color: 'blue', text: 'B' } ], path: { points: [ { lat: 52.3740, lon: 4.8897 }, { lat: 52.3680, lon: 4.9000 }, { lat: 52.3650, lon: 4.8950 } ], color: 'green', width: 4 }, view: 'Unified', baseVersion: 'latest', format: 'png', language: 'en-US', bbox: [4.85, 52.35, 4.95, 52.40] }, expected: { hasImage: true } }, // Negative test cases for tomtom-static-map { name: 'negative: Missing center and bbox', params: { width: 800, height: 600 }, expected: { shouldFail: true } } ], // Dynamic maps with advanced features "tomtom-dynamic-map": [ { name: 'Dynamic map with custom markers', params: { markers: [ { lat: 52.3740, lon: 4.8897, label: "Amsterdam", color: "#ff0000" }, { lat: 48.8566, lon: 2.3522, label: "Paris", color: "#0066cc" } ], showLabels: true, width: 800, height: 600 }, expected: { hasImage: true } }, { name: 'Dynamic map route planning mode', params: { isRoute: true, origin: { lat: 52.3740, lon: 4.8897 }, destination: { lat: 48.8566, lon: 2.3522 }, waypoints: [{ lat: 50.8503, lon: 4.3517 }], // Brussels showLabels: true, use_orbis: false // Test with Genesis maps }, expected: { hasImage: true } }, { name: 'Dynamic map with traffic-aware route', params: { origin: { lat: 52.3740, lon: 4.8897 }, destination: { lat: 52.3680, lon: 4.9000 }, traffic: TRAFFIC, routeType: 'fastest', travelMode: 'car', routeLabel: "Amsterdam Traffic Route", width: 800, height: 600, use_orbis: false // Test with Genesis maps }, expected: { hasImage: true } }, // Test case - should now work with static imports { name: 'Dynamic map with basic markers (static imports)', params: { markers: [{ lat: 52.3740, lon: 4.8897, label: "Amsterdam Test" }], width: 400, height: 300 }, expected: { hasImage: true } } ], }; /** * @typedef {Object} ValidationResult * @property {boolean} valid - Whether the validation passed * @property {string} message - Description of the validation result */ /** * Helper function to validate response structure and handle negative test cases * @param {Object} result - The result object from the MCP tool call * @param {Object} expected - Expected test outcomes * @param {boolean} [expected.shouldFail] - Whether the test is expected to fail * @returns {ValidationResult|null} Validation result object if validation fails, null if validation passes */ function validateResponseStructure(result, expected) { if (!result.content || !result.content[0] || !result.content[0].text) { // If negative test, treat any error/invalid as pass if (expected.shouldFail) { return { valid: true, message: 'Failed as expected (invalid response structure)' }; } return { valid: false, message: 'Invalid response structure' }; } if (expected.shouldFail && result.isError) { return { valid: true, message: `Failed as expected (${result.content[0].text})` }; } return null; // Validation passed, continue with specific checks } /** * Helper function to check for API errors in response data * @param {Object} data - Parsed JSON response data * @param {Object} expected - Expected test outcomes * @param {boolean} [expected.shouldFail] - Whether the test is expected to fail * @returns {ValidationResult|null} Validation result if error is found, null otherwise */ function checkForApiError(data, expected) { if (data.error && typeof data.error === 'string') { // If negative test, treat any error as pass if (expected.shouldFail) { return { valid: true, message: `Failed as expected (${data.error})` }; } // Check if it's a handled API failure if (data.error.includes('Request failed') || data.error.includes('API call failed') || data.error.includes('Invalid arguments')) { return { valid: true, message: `API call failed but handled gracefully: ${data.error}` }; } return { valid: false, message: `API error: ${data.error}` }; } return null; // No error, continue with validation } /** * @typedef {Function} ValidatorFunction * @param {Object} result - The result object from the MCP tool call * @param {Object} expected - Expected test outcomes from test scenario * @returns {ValidationResult} The validation result */ /** * Validators - enhanced for comprehensive testing * @type {Object.<string, ValidatorFunction>} */ const validators = { "tomtom-traffic": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; if (!data.hasOwnProperty('incidents')) { if (expected.shouldFail) { return { valid: true, message: 'Failed as expected (missing incidents array)' }; } return { valid: false, message: 'Missing incidents array in response' }; } if (expected.hasResults && (!data.incidents || data.incidents.length === 0)) { return { valid: true, message: 'No incidents found (which is fine for testing)' }; } return { valid: true, message: `Valid traffic data with ${data.incidents?.length || 0} incidents` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-routing": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; // Check basic structure if (!data.hasOwnProperty('routes') || !Array.isArray(data.routes)) { return { valid: false, message: 'Missing routes array in response' }; } if (expected.hasRoute && (!data.routes || data.routes.length === 0)) { return { valid: true, message: 'No routes found (which is fine for testing)' }; } // Validate structure if there are routes if (data.routes && data.routes.length > 0) { const route = data.routes[0]; // Check required fields on first route but don't fail the test const requiredFields = ['summary', 'legs']; const missingFields = requiredFields.filter(field => !route.hasOwnProperty(field)); if (missingFields.length > 0) { return { valid: true, message: `Route received but missing some fields: ${missingFields.join(', ')}` }; } } return { valid: true, message: `Valid routing data with ${data.routes?.length || 0} routes` + `${data.routes?.[0]?.summary?.lengthInMeters ? ' (' + (data.routes[0].summary.lengthInMeters/1000).toFixed(1) + 'km)' : ''}` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-waypoint-routing": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; // Check basic structure if (!data.hasOwnProperty('routes') || !Array.isArray(data.routes)) { return { valid: false, message: 'Missing routes array in response' }; } if (data.routes.length === 0) { return { valid: true, message: 'No routes found (which is fine for testing)' }; } const route = data.routes[0]; // Check for legs if (!route.legs || !Array.isArray(route.legs)) { return { valid: true, message: 'Missing legs array in route (but route structure exists)' }; } return { valid: true, message: `Valid multi-waypoint route with ${route.legs.length} legs` + `${route.summary?.lengthInMeters ? ' (' + (route.summary.lengthInMeters/1000).toFixed(1) + 'km)' : ''}` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-reachable-range": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; if (!data.hasOwnProperty('reachableRange')) { if (expected.shouldFail) { return { valid: true, message: 'Failed as expected (missing reachableRange)' }; } return { valid: false, message: 'Missing reachableRange in response' }; } // Check for boundary if (!data.reachableRange.hasOwnProperty('boundary')) { return { valid: true, message: 'Missing boundary in reachableRange but response structure exists' }; } // Check for shell polygons if expected (but don't fail the test) if (expected.hasPolygons && (!data.reachableRange.boundary.shell || !data.reachableRange.boundary.shell.length)) { return { valid: true, message: 'No boundary shell polygons but response structure exists' }; } return { valid: true, message: `Valid reachable range data with ${data.reachableRange.boundary.shell?.length || 0} boundary points` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-geocode": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; // Check basic structure if (!data.hasOwnProperty('results') || !Array.isArray(data.results)) { return { valid: false, message: 'Missing results array in response' }; } if (expected.hasResults && data.results.length === 0) { return { valid: true, message: 'No results found (which is fine for testing)' }; } // Check if results contain expected text if specified (but don't fail the test) if (expected.contains && data.results.length > 0) { const addressStr = JSON.stringify(data.results[0]).toLowerCase(); for (const term of expected.contains) { if (!addressStr.toLowerCase().includes(term.toLowerCase())) { return { valid: true, message: `Result doesn't contain "${term}" but structure is valid` }; } } } return { valid: true, message: `Valid geocoding data with ${data.results.length} results` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-reverse-geocode": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; // Check basic structure if (!data.hasOwnProperty('addresses') || !Array.isArray(data.addresses)) { return { valid: false, message: 'Missing `addresses` array in response' }; } if (expected.hasResults && data.addresses.length === 0) { return { valid: true, message: 'No results found (which is fine for testing)' }; } // Check if results contain expected text if specified if (expected.contains && data.addresses.length > 0) { const addressStr = JSON.stringify(data.addresses[0]).toLowerCase(); for (const term of expected.contains) { if (!addressStr.toLowerCase().includes(term.toLowerCase())) { return { valid: true, message: `Result doesn't contain "${term}" but structure is valid` }; } } } return { valid: true, message: `Valid reverse geocoding data with ${data.addresses.length} results` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-nearby": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; // Check basic structure if (!data.hasOwnProperty('results') || !Array.isArray(data.results)) { return { valid: false, message: 'Missing results array in response' }; } if (expected.hasResults && data.results.length === 0) { return { valid: true, message: 'No nearby POIs found (which is fine for testing)' }; } return { valid: true, message: `Valid nearby search data with ${data.results.length} POIs` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-fuzzy-search": (result, expected) => { try { const structureCheck = validateResponseStructure(result, expected); if (structureCheck) return structureCheck; const data = JSON.parse(result.content[0].text); const errorCheck = checkForApiError(data, expected); if (errorCheck) return errorCheck; // Check basic structure if (!data.hasOwnProperty('results') || !Array.isArray(data.results)) { return { valid: false, message: 'Missing results array in response' }; } if (expected.hasResults && data.results.length === 0) { return { valid: true, message: 'No search results found (which is fine for testing)' }; } // Check if results contain expected text if specified if (expected.contains && data.results.length > 0) { const resultStr = JSON.stringify(data.results).toLowerCase(); for (const term of expected.contains) { if (!resultStr.toLowerCase().includes(term.toLowerCase())) { return { valid: true, message: `Results don't contain "${term}" but structure is valid` }; } } } return { valid: true, message: `Valid fuzzy search data with ${data.results.length} results` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-static-map": (result, expected) => { try { // The static map tool returns content with type, data, and mimeType if (!result.content || !result.content[0]) { return { valid: false, message: 'No content in response' }; } if (expected.shouldFail && result.isError) { return { valid: true, message: `Failed as expected (${result.content[0].text})` }; } const firstContent = result.content[0]; // Check for the actual format: {type, data, mimeType} if (firstContent.type && firstContent.data && firstContent.mimeType) { // Validate it's an image if (firstContent.mimeType.startsWith('image/')) { return { valid: true, message: `Static map image generated (${firstContent.mimeType})` }; } else { return { valid: false, message: `Expected image but got: ${firstContent.mimeType}` }; } } // Check for image field (alternative format) if (firstContent.image) { return { valid: true, message: 'Static map image generated successfully' }; } // Check for text content with URL if (firstContent.text && firstContent.text.includes('http')) { return { valid: true, message: 'Map URL generated' }; } // Check if it's an error response if (firstContent.text && firstContent.text.includes('error')) { return { valid: false, message: `Error in response: ${firstContent.text}` }; } return { valid: false, message: `Unexpected content format. Found: ${Object.keys(firstContent).join(', ')}` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } }, "tomtom-dynamic-map": (result, expected) => { try { if (!result.content || !result.content[0]) { if (expected.shouldFail) { return { valid: true, message: 'Failed as expected (no content)' }; } return { valid: false, message: 'No content in response' }; } if (expected.shouldFail && result.isError) { return { valid: true, message: `Failed as expected (${result.content[0].text})` }; } const firstContent = result.content[0]; // Check for error responses (expected for server unavailable tests) if (firstContent.type === 'text' && firstContent.text) { try { const errorData = JSON.parse(firstContent.text); if (errorData.error) { if (expected.shouldFail && expected.expectedError) { if (errorData.error.includes(expected.expectedError)) { return { valid: true, message: `Failed as expected: ${errorData.error}` }; } } // Check if it's a helpful server unavailable error if (errorData.help && errorData.help.includes('Dynamic Map server')) { return { valid: true, message: 'Server unavailable with helpful guidance provided' }; } if (expected.shouldFail) { return { valid: true, message: `Failed as expected: ${errorData.error}` }; } return { valid: false, message: `Dynamic Map error: ${errorData.error}` }; } } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } } // Check for successful image response if (firstContent.type === 'image' && firstContent.data && firstContent.mimeType) { if (expected.shouldFail) { return { valid: false, message: 'Expected failure but got successful image' }; } // Validate it's an image if (firstContent.mimeType.startsWith('image/')) { // Validate base64 data if (firstContent.data && firstContent.data.length > 100) { return { valid: true, message: `Dynamic map image generated (${firstContent.mimeType}, ${Math.round(firstContent.data.length * 0.75 / 1024)}KB)` }; } else { return { valid: false, message: 'Image data seems too small' }; } } else { return { valid: false, message: `Expected image but got: ${firstContent.mimeType}` }; } } if (expected.shouldFail) { return { valid: true, message: 'Failed as expected (unexpected response format)' }; } return { valid: false, message: `Unexpected dynamic map response format. Found: ${Object.keys(firstContent).join(', ')}` }; } catch (error) { return { valid: false, message: `Unexpected error: ${error.message}` }; } } }; // Results tracker class TestResults { constructor() { this.results = []; this.passed = 0; this.failed = 0; this.skipped = 0; } addResult(toolName, name, status, message, duration = null, details = null) { const result = { toolName, name, status, message, duration, details }; this.results.push(result); if (status === 'PASS') { this.passed++; console.log(` ✅ ${name} - ${message}${duration ? ` (${duration}ms)` : ''}`); } else if (status === 'FAIL') { this.failed++; console.log(` ❌ ${name} - ${message}${duration ? ` (${duration}ms)` : ''}`); if (VERBOSE && details) { console.log(` Details: ${JSON.stringify(details, null, 2)}`); } } else if (status === 'SKIP') { this.skipped++; console.log(` ⏭️ ${name} - ${message}`); } } printSummary() { console.log(`\n${'='.repeat(60)}`); console.log(`TEST SUMMARY: ${this.passed + this.failed + this.skipped} tests`); console.log(`${'='.repeat(60)}`); console.log(`✅ Passed: ${this.passed}`); console.log(`❌ Failed: ${this.failed}`); console.log(`⏭️ Skipped: ${this.skipped}`); console.log(`${'='.repeat(60)}`); if (this.failed > 0) { console.log('\nFailed tests:'); this.results .filter(r => r.status === 'FAIL') .forEach(r => console.log(` - ${r.toolName}/${r.name}: ${r.message}`)); } } getPassPercentage() { const total = this.passed + this.failed; return total > 0 ? Math.round((this.passed / total) * 100) : 0; } getResultsByTool() { const byTool = {}; for (const result of this.results) { if (!byTool[result.toolName]) { byTool[result.toolName] = { passed: 0, failed: 0, skipped: 0, total: 0 }; } byTool[result.toolName].total++; if (result.status === 'PASS') { byTool[result.toolName].passed++; } else if (result.status === 'FAIL') { byTool[result.toolName].failed++; } else if (result.status === 'SKIP') { byTool[result.toolName].skipped++; } } return byTool; } printDetailedSummary() { const byTool = this.getResultsByTool(); console.log('\nRESULTS BY TOOL:'); console.log('----------------'); for (const [toolName, counts] of Object.entries(byTool)) { const passRate = counts.total > 0 ? Math.round((counts.passed / (counts.passed + counts.failed)) * 100) : 0; const statusSymbol = counts.failed > 0 ? '❌' : '✅'; console.log(`${statusSymbol} ${toolName}: ${passRate}% passed (${counts.passed}/${counts.passed + counts.failed})`); } } } async function main() { try { // Check if server file exists console.log(`Found server at: ${serverPath}`); // Connect to server via STDIO console.log('Starting MCP server and connecting...'); const client = new McpClient({ name: "TomTom-MCP-Comprehensive-Test", version: "1.0.0" }); // Create transport that will spawn the server const transport = new StdioClientTransport({ command: 'node', args: [serverPath], env: { ...process.env } }); await client.connect(transport); console.log('✓ Connected to MCP server\n'); // Get available tools const toolsResponse = await client.listTools(); const availableTools = toolsResponse.tools.map(t => t.name); console.log(`Available tools: ${availableTools.join(', ')}\n`); // Determine which tools to test const toolsToTest = TEST_TOOL ? [TEST_TOOL] : Object.keys(COMPREHENSIVE_TEST_SCENARIOS); // Track results const results = new TestResults(); // Run tests for each tool for (const toolName of toolsToTest) { // Skip static map tests for Orbis provider (Orbis provides dynamic maps only) if (MAPS_ENV === 'orbis' && toolName === 'tomtom-static-map') { console.log(`\n${toolName.toUpperCase()} TESTS`); console.log('-'.repeat(40)); results.addResult(toolName, 'availability', 'SKIP', `Tool ${toolName} is not available for Orbis provider`); continue; } if (!COMPREHENSIVE_TEST_SCENARIOS[toolName]) { results.addResult(toolName, 'setup', 'SKIP', `No test scenarios defined for tool ${toolName}`); continue; } console.log(`\n${toolName.toUpperCase()} TESTS`); console.log('-'.repeat(40)); if (!availableTools.includes(toolName)) { results.addResult(toolName, 'availability', 'FAIL', `Tool ${toolName} not available on server`); continue; } // Run scenarios for this tool for (const scenario of COMPREHENSIVE_TEST_SCENARIOS[toolName]) { const startTime = Date.now(); try { // Normalize routeType for Orbis vs Genesis differences const ROUTE_TYPE_MAP = { fastest: MAPS_ENV === 'orbis' ? 'fast' : 'fastest', eco: MAPS_ENV === 'orbis' ? 'efficient' : 'eco' }; if (scenario.params && scenario.params.routeType && ROUTE_TYPE_MAP[scenario.params.routeType]) { scenario.params.routeType = ROUTE_TYPE_MAP[scenario.params.routeType]; } // Remove unsupported params for Orbis reachable-range if (MAPS_ENV === 'orbis' && toolName === 'tomtom-reachable-range' && scenario.params && scenario.params.report) { if (VERBOSE) console.log(' Removing unsupported `report` param for Orbis reachable-range'); delete scenario.params.report; } console.log(` Testing: ${scenario.name}...`); if (VERBOSE) { console.log(` Parameters: ${JSON.stringify(scenario.params)}`); } const result = await client.callTool({ name: toolName, arguments: scenario.params }); const duration = Date.now() - startTime; // Validate the result const validator = validators[toolName]; if (validator) { const validation = validator(result, scenario.expected); if (validation.valid) { results.addResult(toolName, scenario.name, 'PASS', validation.message, duration); } else { results.addResult(toolName, scenario.name, 'FAIL', validation.message, duration, result); } } else { results.addResult(toolName, scenario.name, 'PASS', 'No validator available for tool', duration); } } catch (error) { const duration = Date.now() - startTime; results.addResult(toolName, scenario.name, 'FAIL', `Unexpected error: ${error.message}`, duration, { error: error.message }); } } } // Print summary results.printSummary(); results.printDetailedSummary(); // Clean shutdown console.log('\nShutting down...'); await client.close(); // Exit with appropriate code process.exit(results.failed > 0 ? 1 : 0); } catch (error) { console.error(`\n✗ Test execution failed: ${error.message}`); if (VERBOSE) { console.error(error.stack); } process.exit(1); } } // Handle signals to ensure clean shutdown process.on('SIGINT', () => { console.log('\nReceived interrupt signal, shutting down...'); process.exit(1); }); process.on('SIGTERM', () => { console.log('\nReceived terminate signal, shutting down...'); process.exit(1); }); main().catch(err => { console.error(`Unhandled error: ${err.message}`); if (VERBOSE) { console.error(err.stack); } process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tomtom-international/tomtom-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server