/* * Copyright (C) 2025 TomTom Navigation B.V. * * 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. */ import axios, { AxiosInstance } from "axios"; import dotenv from "dotenv"; import { AsyncLocalStorage } from "async_hooks"; import { getAppConfig } from "../../appConfig"; import { logger } from "../../utils/logger"; import { VERSION } from "../../version"; // Variable to track if we're running in HTTP server mode // This will be set to true in indexHttp.ts export let isHttpMode = false; // Load environment variables dotenv.config(); /** * Gets the static TomTom API key from app config. * Re-reads each call so .env loaded after module init is still picked up. */ function getStaticApiKey(): string | undefined { return getAppConfig().tomtomApiKey; } /** * Core Axios client for TomTom API requests * Uses dynamic API key resolution for both environment and session-based keys */ export const tomtomClient: AxiosInstance = axios.create({ baseURL: getAppConfig().tomtomApiBaseUrl, paramsSerializer: { indexes: null }, headers: { // Default to standard user-agent for stdio mode - will be updated if HTTP mode is set "TomTom-User-Agent": `TomTomMCPSDK/${VERSION}`, }, }); // Request interceptor to add API key dynamically tomtomClient.interceptors.request.use( (config) => { // Get API key from session context or environment const apiKey = getSessionApiKey() || getStaticApiKey(); if (apiKey) { if (!config.params?.key) { config.params = { ...config.params, key: apiKey }; } } const { key: _key, ...safeParams } = (config.params ?? {}) as Record; logger.debug( { method: config.method?.toUpperCase(), baseURL: config.baseURL, url: config.url, params: safeParams, }, "→ TomTom API request" ); return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor to log outcome of TomTom API calls tomtomClient.interceptors.response.use( (response) => { logger.info( { method: response.config.method?.toUpperCase(), url: response.config.url, status: response.status, }, "← TomTom API response" ); return response; }, (error) => { const config = error?.config; logger.info( { method: config?.method?.toUpperCase(), url: config?.url, status: error?.response?.status, }, "← TomTom API error" ); return Promise.reject(error); } ); /** * Request context for session-specific configuration */ interface RequestContext { apiKey: string; backend?: "tomtom-maps" | "tomtom-orbis-maps"; } /** * AsyncLocalStorage for proper per-request context isolation * This ensures multiple concurrent HTTP sessions don't interfere with each other */ const requestContext = new AsyncLocalStorage(); /** * Get session-specific API key from current async context */ export function getSessionApiKey(): string | undefined { const context = requestContext.getStore(); return context?.apiKey; } /** * Set session-specific configuration for the current async context */ export function setSessionContext( apiKey: string, backend?: "tomtom-maps" | "tomtom-orbis-maps" ): void { const context = requestContext.getStore(); if (context) { context.apiKey = apiKey; context.backend = backend; } } /** * Run function within a session context (for HTTP requests) */ export function runWithSessionContext( apiKey: string, backend: "tomtom-maps" | "tomtom-orbis-maps", fn: () => T ): T { return requestContext.run({ apiKey, backend }, fn); } /** * Get current session backend */ export function getSessionBackend(): "tomtom-maps" | "tomtom-orbis-maps" | undefined { const context = requestContext.getStore(); return context?.backend; } /** * Get the effective API key (session or environment) */ export function getEffectiveApiKey(): string | undefined { return getSessionApiKey() || getStaticApiKey(); } /** * Helper function to validate that API key exists before making calls * @throws {Error} If the API key is not set * @returns {void} Nothing if validation passes */ export function validateApiKey(): void { const apiKey = getEffectiveApiKey(); if (!apiKey) { throw new Error( "TomTom API key is not set. Please set TOMTOM_API_KEY environment variable or provide via session configuration." ); } } /** * Set the mode to HTTP server mode * This changes the user-agent header to indicate HTTP mode * Uses MCP_TRANSPORT_MODE environment variable if available, otherwise defaults to "TomTomMCPSDKHttp" */ export function setHttpMode(): void { isHttpMode = true; // Get custom MCP transport from environment variable or use default // Check for both undefined and empty string cases const mcpTransportModeType = process.env.MCP_TRANSPORT_MODE && process.env.MCP_TRANSPORT_MODE.trim() ? process.env.MCP_TRANSPORT_MODE.trim() : "TomTomMCPSDKHttp"; // Update the user-agent header to reflect HTTP mode if (tomtomClient.defaults.headers) { tomtomClient.defaults.headers["TomTom-User-Agent"] = `${mcpTransportModeType}/${VERSION}`; } logger.debug( { user_agent: `${mcpTransportModeType}/${VERSION}` }, "TomTom MCP client set to HTTP mode" ); } /** * API version constants for TomTom Maps API * Each API has its own version number which can change independently */ export const API_VERSION = { SEARCH: 2, GEOCODING: 2, ROUTING: 1, TRAFFIC: 5, MAP: 1, } as const; /** * API version constants for TomTom Orbis Maps API * Each API has its own version number which can be different from TomTom Maps API */ export const ORBIS_API_VERSION = { SEARCH: 1, GEOCODING: 1, ROUTING: 2, TRAFFIC: 1, MAP: 1, } as const;