Skip to main content
Glama
MatrixTool.ts9.84 kB
import { URLSearchParams } from 'url'; import { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; // API documentation: https://docs.mapbox.com/api/navigation/matrix/ const MatrixInputSchema = z.object({ coordinates: z .array( z.object({ longitude: z .number() .min(-180, 'Longitude must be between -180 and 180 degrees') .max(180, 'Longitude must be between -180 and 180 degrees'), latitude: z .number() .min(-90, 'Latitude must be between -90 and 90 degrees') .max(90, 'Latitude must be between -90 and 90 degrees') }) ) .min(2, 'At least two coordinate pairs are required.') .max( 25, 'Up to 25 coordinate pairs are supported for most profiles (10 for driving-traffic).' ) .describe( 'Array of coordinate objects with longitude and latitude properties. ' + 'Must include at least 2 coordinate pairs. ' + 'Up to 25 coordinates total are supported for most profiles (10 for driving-traffic).' ), profile: z .enum(['driving-traffic', 'driving', 'walking', 'cycling']) .describe( 'Routing profile for different modes of transport. Options: \n' + '- driving-traffic: automotive with current traffic conditions (limited to 10 coordinates)\n' + '- driving: automotive based on typical traffic\n' + '- walking: pedestrian/hiking\n' + '- cycling: bicycle' ), annotations: z .enum(['duration', 'distance', 'duration,distance', 'distance,duration']) .optional() .describe( 'Specifies the resulting matrices. Possible values are: duration (default), distance, or both values separated by a comma.' ), approaches: z .string() .optional() .describe( 'A semicolon-separated list indicating the side of the road from which to approach waypoints. ' + 'Accepts "unrestricted" (default, route can arrive at the waypoint from either side of the road) ' + 'or "curb" (route will arrive at the waypoint on the driving_side of the region). ' + 'If provided, the number of approaches must be the same as the number of waypoints. ' + 'You can skip a coordinate and show its position with the ; separator.' ), bearings: z .string() .optional() .describe( 'A semicolon-separated list of headings and allowed deviation indicating the direction of movement. ' + 'Input as two comma-separated values per location: a heading course measured clockwise from true north ' + 'between 0 and 360, and the range of degrees by which the angle can deviate (recommended value is 45° or 90°), ' + 'formatted as {angle,degrees}. If provided, the number of bearings must equal the number of coordinates. ' + 'You can skip a coordinate and show its position in the list with the ; separator.' ), destinations: z .string() .optional() .describe( 'Use the coordinates at given indices as destinations. ' + 'Possible values are: a semicolon-separated list of 0-based indices, or "all" (default). ' + 'The option "all" allows using all coordinates as destinations.' ), sources: z .string() .optional() .describe( 'Use the coordinates at given indices as sources. ' + 'Possible values are: a semicolon-separated list of 0-based indices, or "all" (default). ' + 'The option "all" allows using all coordinates as sources.' ) }); export class MatrixTool extends MapboxApiBasedTool<typeof MatrixInputSchema> { name = 'matrix_tool'; description = 'Calculates travel times and distances between multiple points using Mapbox Matrix API.'; constructor() { super({ inputSchema: MatrixInputSchema }); } protected async execute( input: z.infer<typeof MatrixInputSchema>, accessToken: string ): Promise<any> { // Validate input based on profile type if (input.profile === 'driving-traffic' && input.coordinates.length > 10) { throw new Error( 'The driving-traffic profile supports a maximum of 10 coordinate pairs.' ); } // Validate approaches parameter if provided if ( input.approaches && input.approaches.split(';').length !== input.coordinates.length ) { throw new Error( 'When provided, the number of approaches (including empty/skipped) must match the number of coordinates.' ); } // Validate that all approaches values are either "curb" or "unrestricted" if ( input.approaches && input.approaches .split(';') .some( (approach) => approach !== '' && approach !== 'curb' && approach !== 'unrestricted' ) ) { throw new Error( 'Approaches parameter contains invalid values. Each value must be either "curb" or "unrestricted".' ); } // Validate bearings parameter if provided if ( input.bearings && input.bearings.split(';').length !== input.coordinates.length ) { throw new Error( 'When provided, the number of bearings (including empty/skipped) must match the number of coordinates.' ); } // Additional validation for bearings values if (input.bearings) { const bearingsArr = input.bearings.split(';'); bearingsArr.forEach((bearing, idx) => { if (bearing.trim() === '') return; // allow skipped const parts = bearing.split(','); if (parts.length !== 2) { throw new Error( `Invalid bearings format at index ${idx}: '${bearing}'. Each bearing must be two comma-separated numbers (angle,degrees).` ); } const angle = Number(parts[0]); const degrees = Number(parts[1]); if (isNaN(angle) || angle < 0 || angle > 360) { throw new Error( `Invalid bearing angle at index ${idx}: '${parts[0]}'. Angle must be a number between 0 and 360.` ); } if (isNaN(degrees) || degrees < 0 || degrees > 180) { throw new Error( `Invalid bearing degrees at index ${idx}: '${parts[1]}'. Degrees must be a number between 0 and 180.` ); } }); } // Validate sources parameter if provided - ensure all indices are valid if ( input.sources && input.sources !== 'all' && input.sources.split(';').some((index) => { const parsedIndex = parseInt(index, 10); return ( isNaN(parsedIndex) || parsedIndex < 0 || parsedIndex >= input.coordinates.length ); }) ) { throw new Error( 'Sources parameter contains invalid indices. All indices must be between 0 and ' + (input.coordinates.length - 1) + '.' ); } // Validate destinations parameter if provided - ensure all indices are valid if ( input.destinations && input.destinations !== 'all' && input.destinations.split(';').some((index) => { const parsedIndex = parseInt(index, 10); return ( isNaN(parsedIndex) || parsedIndex < 0 || parsedIndex >= input.coordinates.length ); }) ) { throw new Error( 'Destinations parameter contains invalid indices. All indices must be between 0 and ' + (input.coordinates.length - 1) + '.' ); } // Validate that when specifying both sources and destinations, all coordinates are used if ( input.sources && input.sources !== 'all' && input.destinations && input.destinations !== 'all' ) { // Get all unique coordinate indices that are used const sourcesIndices = input.sources .split(';') .map((idx) => parseInt(idx, 10)); const destinationsIndices = input.destinations .split(';') .map((idx) => parseInt(idx, 10)); const usedIndices = new Set([...sourcesIndices, ...destinationsIndices]); // Check if all coordinate indices are used if (usedIndices.size < input.coordinates.length) { throw new Error( 'When specifying both sources and destinations, all coordinates must be used as either a source or destination.' ); } } // Format coordinates for API request const joined = input.coordinates .map(({ longitude, latitude }) => `${longitude},${latitude}`) .join(';'); // Build query parameters const queryParams = new URLSearchParams(); queryParams.append('access_token', accessToken); // Add annotations parameter if specified if (input.annotations) { queryParams.append('annotations', input.annotations); } // Add approaches parameter if specified if (input.approaches) { queryParams.append('approaches', input.approaches); } // Add bearings parameter if specified if (input.bearings) { queryParams.append('bearings', input.bearings); } // Add destinations parameter if specified if (input.destinations) { queryParams.append('destinations', input.destinations); } // Add sources parameter if specified if (input.sources) { queryParams.append('sources', input.sources); } // Construct the URL for the Matrix API request const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}directions-matrix/v1/mapbox/${input.profile}/${joined}?${queryParams.toString()}`; // Make the request const response = await fetch(url); if (!response.ok) { throw new Error( `Request failed with status ${response.status}: ${response.statusText}` ); } // Return the matrix data const data = await response.json(); return data; } }

Latest Blog Posts

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/Waldzell-Agentics/mcp-server'

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