UNPKG

lokalise-mcp

Version:

The Lokalise MCP Server brings Lokalise's localization power to Claude and AI assistants—manage projects, keys, and translations by chat.

189 lines (188 loc) • 8.91 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"); /** * Retrieves IP API credentials from configuration. * Specifically checks for IPAPI_API_TOKEN. * @returns IpApiCredentials object containing the API token if found. */ export function getIpApiCredentials() { 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() { 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(path, options = {}) { 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(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(path, options = {}) { const methodLogger = Logger.forContext("utils/transport.util.ts", "fetchLokaliseApi"); // Get credentials (token is required) const credentials = getLokaliseApiCredentials(); // Construct the full URL const baseUrl = "https://api.stage.lokalise.cloud/api2//api2"; const url = `${baseUrl}${path}`; methodLogger.debug(`Constructed Lokalise API URL: ${url}`); // Delegate the actual fetch call to the generic fetchApi with Lokalise headers return fetchApi(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(url, options = {}) { const methodLogger = Logger.forContext("utils/transport.util.ts", "fetchApi"); // Prepare standard request options const requestOptions = { 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; } 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); } }