Skip to main content
Glama
transport.util.ts9.48 kB
import { config } from "./config.util.js"; import { classifyApiError, createApiError, createAuthInvalidError, createNetworkError, createUnexpectedError, McpError, } from "./error.util.js"; import { Logger } from "./logger.util.js"; // Create a contextualized logger for this file const transportLogger = Logger.forContext("utils/transport.util.ts"); // Log transport utility initialization transportLogger.debug("Transport utility initialized"); /** * Interface for IP API credentials. * Note: API token is optional for the free tier. */ export interface IpApiCredentials { apiToken?: string; } /** * Interface for Lokalise API credentials. * Note: API token is required for all operations. */ export interface LokaliseApiCredentials { apiToken: string; } /** * Interface for HTTP request options */ export interface RequestOptions { method?: "GET" | "POST" | "PUT" | "DELETE"; headers?: Record<string, string>; body?: unknown; } /** * Retrieves IP API credentials from configuration. * Specifically checks for IPAPI_API_TOKEN. * @returns IpApiCredentials object containing the API token if found. */ export function getIpApiCredentials(): IpApiCredentials { const methodLogger = Logger.forContext( "utils/transport.util.ts", "getIpApiCredentials", ); const apiToken = config.get("IPAPI_API_TOKEN"); if (!apiToken) { methodLogger.debug( "No IP API token found (IPAPI_API_TOKEN). Using free tier.", ); return {}; // Return empty object if no token } methodLogger.debug("Using IP API token from configuration."); return { apiToken }; } /** * Retrieves Lokalise API credentials from configuration. * Specifically checks for LOKALISE_API_KEY. * @returns LokaliseApiCredentials object containing the API token. * @throws {McpError} If no API token is found. */ export function getLokaliseApiCredentials(): LokaliseApiCredentials { const methodLogger = Logger.forContext( "utils/transport.util.ts", "getLokaliseApiCredentials", ); const apiToken = config.get("LOKALISE_API_KEY"); if (!apiToken) { methodLogger.error("No Lokalise API token found (LOKALISE_API_KEY)"); throw createAuthInvalidError( "Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ); } methodLogger.debug("Using Lokalise API token from configuration."); return { apiToken }; } /** * Fetches data specifically from the ip-api.com endpoint. * Handles URL construction, authentication (if token provided), and query parameters. * Relies on the generic fetchApi function for the actual HTTP request. * * @param path The specific IP address or path component (e.g., "8.8.8.8"). Empty string for current IP. * @param options Additional options like HTTP method, headers, body, and ip-api specific params. * @param options.useHttps - Use HTTPS (requires paid plan for ip-api.com). Defaults to false. * @param options.fields - Specific fields to request from ip-api.com. * @param options.lang - Language code for response data. * @returns The response data parsed as type T. * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues. */ export async function fetchIpApi<T>( path: string, options: RequestOptions & { useHttps?: boolean; fields?: string[]; lang?: string; } = {}, ): Promise<T> { const methodLogger = Logger.forContext( "utils/transport.util.ts", "fetchIpApi", ); // Get credentials (token might be undefined) const credentials = getIpApiCredentials(); // Determine protocol based on options const protocol = options.useHttps ? "https" : "http"; const baseUrl = `${protocol}://ip-api.com/json`; // Format path for URL const normalizedPath = path ? `/${path}` : ""; let url = `${baseUrl}${normalizedPath}`; // Build query parameters const queryParams = new URLSearchParams(); // Add API token if present if (credentials.apiToken) { queryParams.set("key", credentials.apiToken); methodLogger.debug("API token added to query parameters."); } // Add fields parameter if (options.fields?.length) { queryParams.set("fields", options.fields.join(",")); methodLogger.debug(`Requesting fields: ${options.fields.join(",")}`); } // Add language parameter if (options.lang) { queryParams.set("lang", options.lang); methodLogger.debug(`Requesting language: ${options.lang}`); } // Append query string if needed const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; } methodLogger.debug(`Constructed URL: ${url}`); // Delegate the actual fetch call to the generic fetchApi return fetchApi<T>(url, { method: options.method, headers: options.headers, body: options.body, }); } /** * Fetches data specifically from the Lokalise API endpoint. * Handles URL construction, authentication, and query parameters. * Relies on the generic fetchApi function for the actual HTTP request. * * @param path The API path (e.g., "/projects", "/languages"). * @param options Additional options like HTTP method, headers, and body. * @returns The response data parsed as type T. * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues. */ export async function fetchLokaliseApi<T>( path: string, options: RequestOptions = {}, ): Promise<T> { const methodLogger = Logger.forContext( "utils/transport.util.ts", "fetchLokaliseApi", ); // Get credentials (token is required) const credentials = getLokaliseApiCredentials(); // Get the API hostname from configuration const apiHostname = config.get("LOKALISE_API_HOSTNAME") || "https://api.lokalise.com/api2/"; // Ensure the hostname ends with a slash and remove any duplicate slashes const baseUrl = apiHostname.endsWith("/") ? apiHostname.slice(0, -1) : apiHostname; const normalizedPath = path.startsWith("/") ? path : `/${path}`; const url = `${baseUrl}${normalizedPath}`; methodLogger.debug(`Constructed Lokalise API URL: ${url}`); // Delegate the actual fetch call to the generic fetchApi with Lokalise headers return fetchApi<T>(url, { method: options.method, headers: { "X-Api-Token": credentials.apiToken, ...options.headers, }, body: options.body, }); } /** * Generic and reusable function to fetch data from any API endpoint. * Handles standard HTTP request setup, response checking, basic error handling, and logging. * * @param url The full URL to fetch data from. * @param options Request options including method, headers, and body. * @returns The response data parsed as type T. * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues. */ export async function fetchApi<T>( url: string, options: RequestOptions = {}, ): Promise<T> { const methodLogger = Logger.forContext("utils/transport.util.ts", "fetchApi"); // Prepare standard request options const requestOptions: RequestInit = { method: options.method || "GET", headers: { // Standard headers, allow overrides via options.headers "Content-Type": "application/json", Accept: "application/json", ...options.headers, }, body: options.body ? JSON.stringify(options.body) : undefined, }; methodLogger.debug(`Executing API call: ${requestOptions.method} ${url}`); const startTime = performance.now(); // Track performance try { const response = await fetch(url, requestOptions); const endTime = performance.now(); const duration = (endTime - startTime).toFixed(2); methodLogger.debug( `API call completed in ${duration}ms with status: ${response.status} ${response.statusText}`, { url, status: response.status, }, ); // Check if the response status is OK (2xx) if (!response.ok) { const errorText = await response.text(); // Get error body for context methodLogger.error(`API error response (${response.status}):`, errorText); // Use the new error classification system throw classifyApiError( errorText, response.status, `API request failed with status ${response.status}: ${response.statusText}`, ); } // Attempt to parse the response body as JSON try { const responseData = await response.json(); methodLogger.debug("Response body successfully parsed as JSON."); // methodLogger.debug('Response Data:', responseData); // Uncomment for full response logging return responseData as T; } catch (parseError) { methodLogger.error("Failed to parse API response JSON:", parseError); // Throw a specific error for JSON parsing failure throw createApiError( `Failed to parse API response JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, response.status, // Include original status for context parseError, ); } } catch (error) { const endTime = performance.now(); const duration = (endTime - startTime).toFixed(2); methodLogger.error( `API call failed after ${duration}ms for ${url}:`, error, ); // Rethrow if it's already an McpError (e.g., from status checks or parsing) if (error instanceof McpError) { throw error; } // Handle potential network errors (TypeError in fetch) if (error instanceof TypeError) { throw createNetworkError( `Network error during API call: ${error.message}`, error, ); } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected error during API call: ${error instanceof Error ? error.message : String(error)}`, error, ); } }

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/AbdallahAHO/lokalise-mcp'

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