/**
* GrabMaps MCP Server
* Provides Places and Routes API tools for Model Context Protocol
*/
import dotenv from 'dotenv';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { placeActions } from './actions/places.js';
import { routeActions } from './actions/routes.js';
// Load environment variables
dotenv.config();
// Define the config schema for passing secrets securely
export const configSchema = z.object({
grabMapsApiKey: z.string().describe('GrabMaps API key'),
awsAccessKeyId: z.string().describe('AWS Access Key ID'),
awsSecretAccessKey: z.string().describe('AWS Secret Access Key'),
awsRegion: z.string().optional().describe('AWS Region (default: ap-southeast-5)'),
});
/**
* Creates a stateless MCP server for the GrabMaps API
*/
export default function createStatelessServer({
config: _config,
}: {
config: z.infer<typeof configSchema>;
}) {
const server = new McpServer({
name: 'grabmaps',
description: 'GrabMaps API integration for Model Context Protocol',
version: '1.0.0',
});
// Set API key from config
process.env.GRABMAPS_API_KEY = _config.grabMapsApiKey;
// Set AWS credentials for GrabMaps integration via AWS Location Service
process.env.AWS_ACCESS_KEY_ID = _config.awsAccessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = _config.awsSecretAccessKey;
// Set AWS region if provided, otherwise use default
process.env.AWS_REGION = _config.awsRegion || 'ap-southeast-5';
console.log('GrabMaps MCP Server initialized with credentials');
// Register Places API tools
server.registerTool(
'searchPlaceIndexForText',
{
description: `Search for places using text query.
IMPORTANT INSTRUCTION FOR AI MODELS:
1. GrabMaps ONLY supports eight countries in Southeast Asia: Malaysia (MYS), Singapore (SGP), Thailand (THA), Myanmar (MMR), Cambodia (KHM), Vietnam (VNM), Philippines (PHL), and Indonesia (IDN). Searches outside these countries will not return accurate results.
2. AI models MUST analyze the user's query to determine the country and ALWAYS include the appropriate country code in the request.
For example:
- If user asks about "City Square Mall", determine it's in Singapore and use country: "SGP"
- If user asks about "Songklanagarind Hospital", determine it's in Thailand and use country: "THA"
This is CRITICAL for returning accurate search results.`,
inputSchema: {
query: z.string().describe('Search query text'),
country: z.string()
.describe('REQUIRED: Three-letter ISO country code (e.g., "SGP" for Singapore, "MYS" for Malaysia, "THA" for Thailand). AI MODELS MUST ALWAYS INCLUDE THIS PARAMETER.')
.optional(), // Technically optional but strongly encouraged
maxResults: z.number().optional().describe('Maximum results to return'),
language: z.string().optional().describe('Language code'),
}
},
placeActions.searchPlaceIndexForText
);
server.registerTool(
'searchPlaceIndexForPosition',
{
description: 'Search for places by coordinates (reverse geocoding). Note: GrabMaps ONLY supports eight countries in Southeast Asia: Malaysia (MYS), Singapore (SGP), Thailand (THA), Myanmar (MMR), Cambodia (KHM), Vietnam (VNM), Philippines (PHL), and Indonesia (IDN). Searches outside these countries will not return accurate results.',
inputSchema: {
longitude: z.number().describe('Longitude coordinate'),
latitude: z.number().describe('Latitude coordinate'),
maxResults: z.number().optional().describe('Maximum results to return'),
language: z.string().optional().describe('Language code'),
},
},
placeActions.searchPlaceIndexForPosition
);
server.registerTool(
'searchPlaceIndexForSuggestions',
{
description: `Get place suggestions based on partial text input.
IMPORTANT INSTRUCTION FOR AI MODELS:
1. GrabMaps ONLY supports eight countries in Southeast Asia: Malaysia (MYS), Singapore (SGP), Thailand (THA), Myanmar (MMR), Cambodia (KHM), Vietnam (VNM), Philippines (PHL), and Indonesia (IDN). Searches outside these countries will not return accurate results.
2. AI models MUST analyze the user's query to determine the country and ALWAYS include the appropriate country code in the request.
For example:
- If user asks about "City Square Mall", determine it's in Singapore and use country: "SGP"
- If user asks about "Songklanagarind Hospital", determine it's in Thailand and use country: "THA"
This is CRITICAL for returning accurate search results.`,
inputSchema: {
query: z.string().describe('Search query text'),
country: z.string()
.describe('REQUIRED: Three-letter ISO country code (e.g., "SGP" for Singapore, "MYS" for Malaysia, "THA" for Thailand). AI MODELS MUST ALWAYS INCLUDE THIS PARAMETER.')
.optional(), // Technically optional but strongly encouraged
maxResults: z.number().optional().describe('Maximum results to return'),
language: z.string().optional().describe('Language code'),
}
},
placeActions.searchPlaceIndexForSuggestions
);
server.registerTool(
'getPlace',
{
description: 'Get place details by place ID. Note: GrabMaps ONLY supports eight countries in Southeast Asia: Malaysia (MYS), Singapore (SGP), Thailand (THA), Myanmar (MMR), Cambodia (KHM), Vietnam (VNM), Philippines (PHL), and Indonesia (IDN).',
inputSchema: {
placeId: z.string().describe('ID of the place to retrieve'),
language: z.string().optional().describe('Language code'),
},
},
placeActions.getPlace
);
// Register Routes API tools
server.registerTool(
'calculateRoute',
{
description: 'Calculate a route between two points',
inputSchema: {
origin: z.object({ longitude: z.number(), latitude: z.number() }),
destination: z.object({ longitude: z.number(), latitude: z.number() }),
travelMode: z.enum(['Car', 'Truck', 'Walking', 'Bicycle', 'Motorcycle']).optional(),
distanceUnit: z.enum(['Kilometers', 'Miles']).optional(),
},
},
routeActions.calculateRoute
);
type RouteMatrixParams = {
departurePositions: number[][];
destinationPositions: number[][];
travelMode?: 'Car' | 'Truck' | 'Walking' | 'Bicycle' | 'Motorcycle';
departureTime?: string;
distanceUnit?: 'Kilometers' | 'Miles';
};
server.registerTool(
'calculateRouteMatrix',
{
description: 'Calculates a route matrix between multiple origins and destinations.',
inputSchema: {
departurePositions: z.array(z.array(z.number())),
destinationPositions: z.array(z.array(z.number())),
travelMode: z.enum(['Car', 'Truck', 'Walking', 'Bicycle', 'Motorcycle']).optional(),
distanceUnit: z.enum(['Kilometers', 'Miles']).optional(),
departureTime: z.string().optional(),
},
},
routeActions.calculateRouteMatrix
);
return server;
}