test-comprehensive.js•47.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);
});