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.

385 lines (384 loc) • 20 kB
import { config } from "../../shared/utils/config.util.js"; import { ErrorType, McpError } from "../../shared/utils/error.util.js"; import { buildErrorContext, handleControllerError, } from "../../shared/utils/error-handler.util.js"; import { Logger } from "../../shared/utils/logger.util.js"; import { formatCreateProjectResult, formatDeleteProjectResult, formatEmptyProjectResult, formatProjectDetails, formatProjectsList, formatUpdateProjectResult, } from "./projects.formatter.js"; import * as projectsService from "./projects.service.js"; /** * @namespace ProjectsController * @description Controller responsible for handling Lokalise Projects API operations. * It orchestrates calls to the projects service, applies defaults, * maps options, and formats the response using the formatter. */ /** * @function listProjects * @description Fetches a list of Lokalise projects accessible with the current API token. * Handles pagination options and applies configuration defaults. * @memberof ProjectsController * @param {ListProjectsToolArgsType} args - Arguments containing pagination options * @returns {Promise<ControllerResponse>} A promise that resolves to the standard controller response containing the formatted projects list in Markdown. * @throws {McpError} Throws an McpError (handled by `handleControllerError`) if the service call fails or returns an error. */ async function listProjects(args) { const methodLogger = Logger.forContext("controllers/projects.controller.ts", "listProjects"); methodLogger.debug("Getting Lokalise projects list..."); try { // Detect if we're running in a test environment const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined; // Apply defaults and validation const options = { limit: args.limit, page: args.page, }; if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) { // Validate limit if provided throw new McpError("Invalid limit parameter. Must be between 1 and 500.", ErrorType.API_ERROR); } if (options.page !== undefined && options.page < 1) { // Validate page if provided throw new McpError("Invalid page parameter. Must be 1 or greater.", ErrorType.API_ERROR); } // Check for API token const hasApiToken = Boolean(config.get("LOKALISE_API_KEY")); if (!hasApiToken && !isTestEnvironment) { throw new McpError("Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ErrorType.AUTH_MISSING); } methodLogger.debug("Getting projects from Lokalise", { originalOptions: args, options, isTestEnvironment, hasApiToken, }); try { // Call the service with the options const projects = await projectsService.default.getProjects(options); methodLogger.debug("Got the response from the service", { projectCount: projects.length, }); const formattedContent = formatProjectsList(projects, args.includeStats); return { content: formattedContent }; } catch (error) { // Handle specific Lokalise API errors if (error instanceof McpError && (error.message.includes("Unauthorized") || error.message.includes("Invalid API token"))) { methodLogger.error("Lokalise API authentication failed"); throw new McpError("Lokalise API authentication failed. Please check your API token.", ErrorType.AUTH_INVALID); } // For other errors, rethrow throw error; } } catch (error) { throw handleControllerError(error, buildErrorContext("Lokalise Project", "listProjects", "controllers/projects.controller.ts@listProjects", `limit: ${args.limit}, page: ${args.page}`, { args })); } } /** * @function getProjectDetails * @description Gets detailed information about a specific Lokalise project. * @memberof ProjectsController * @param {GetProjectDetailsToolArgsType} args - Arguments containing project details options * @returns {Promise<ControllerResponse>} A promise that resolves to the standard controller response containing the formatted project details in Markdown. * @throws {McpError} Throws an McpError (handled by `handleControllerError`) if the service call fails or returns an error. */ async function getProjectDetails(args) { const methodLogger = Logger.forContext("controllers/projects.controller.ts", "getProjectDetails"); methodLogger.debug("Getting Lokalise project details...", args); try { // Detect if we're running in a test environment const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined; // Validate project ID if (!args.projectId || typeof args.projectId !== "string") { throw new McpError("Project ID is required and must be a string.", ErrorType.API_ERROR); } // Check for API token const hasApiToken = Boolean(config.get("LOKALISE_API_KEY")); if (!hasApiToken && !isTestEnvironment) { throw new McpError("Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ErrorType.AUTH_MISSING); } methodLogger.debug("Getting project details from Lokalise", { projectId: args.projectId, isTestEnvironment, hasApiToken, }); try { // Call the service with the project ID const project = await projectsService.default.getProjectDetails(args.projectId); methodLogger.debug("Got the project details from the service", { projectId: project.project_id, projectName: project.name, }); const formattedContent = formatProjectDetails(project, args.includeLanguages, args.includeKeysSummary); return { content: formattedContent }; } catch (error) { // Handle specific Lokalise API errors if (error instanceof McpError && (error.message.includes("Unauthorized") || error.message.includes("Invalid API token"))) { methodLogger.error("Lokalise API authentication failed"); throw new McpError("Lokalise API authentication failed. Please check your API token.", ErrorType.AUTH_INVALID); } if (error instanceof McpError && (error.message.includes("Not Found") || error.message.includes("404"))) { methodLogger.error("Project not found", { projectId: args.projectId }); throw new McpError(`Project with ID '${args.projectId}' not found. Please check the project ID.`, ErrorType.API_ERROR); } // For other errors, rethrow throw error; } } catch (error) { throw handleControllerError(error, buildErrorContext("Lokalise Project", "getProjectDetails", "controllers/projects.controller.ts@getProjectDetails", `projectId: ${args.projectId}`, { args })); } } /** * @function createProject * @description Creates a new Lokalise project with specified settings. * @memberof ProjectsController * @param {CreateProjectToolArgsType} args - Arguments containing project creation options * @returns {Promise<ControllerResponse>} A promise that resolves to the standard controller response containing the formatted project creation result. * @throws {McpError} Throws an McpError (handled by `handleControllerError`) if the service call fails or returns an error. */ async function createProject(args) { const methodLogger = Logger.forContext("controllers/projects.controller.ts", "createProject"); methodLogger.debug("Creating new Lokalise project...", args); try { const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined; // Validate project name if (!args.name || typeof args.name !== "string" || args.name.trim().length === 0) { throw new McpError("Project name is required and must be a non-empty string.", ErrorType.API_ERROR); } if (args.name.length > 100) { throw new McpError("Project name must be 100 characters or less.", ErrorType.API_ERROR); } // Check for API token const hasApiToken = Boolean(config.get("LOKALISE_API_KEY")); if (!hasApiToken && !isTestEnvironment) { throw new McpError("Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ErrorType.AUTH_MISSING); } methodLogger.debug("Creating project in Lokalise", { name: args.name, hasDescription: Boolean(args.description), baseLang: args.base_lang_iso || "en", isTestEnvironment, hasApiToken, }); const projectData = { name: args.name.trim(), description: args.description?.trim(), base_lang_iso: args.base_lang_iso || "en", }; try { const project = await projectsService.default.createProject(projectData); methodLogger.debug("Project created successfully", { projectId: project.project_id, projectName: project.name, }); const formattedContent = formatCreateProjectResult(project); return { content: formattedContent }; } catch (error) { if (error instanceof McpError && (error.message.includes("Unauthorized") || error.message.includes("Invalid API token"))) { methodLogger.error("Lokalise API authentication failed"); throw new McpError("Lokalise API authentication failed. Please check your API token.", ErrorType.AUTH_INVALID); } throw error; } } catch (error) { throw handleControllerError(error, buildErrorContext("Lokalise Project", "createProject", "controllers/projects.controller.ts@createProject", `name: ${args.name}`, { args })); } } /** * @function updateProject * @description Updates an existing Lokalise project. * @memberof ProjectsController * @param {UpdateProjectToolArgsType} args - Arguments containing project update options * @returns {Promise<ControllerResponse>} A promise that resolves to the standard controller response containing the formatted project update result. * @throws {McpError} Throws an McpError (handled by `handleControllerError`) if the service call fails or returns an error. */ async function updateProject(args) { const methodLogger = Logger.forContext("controllers/projects.controller.ts", "updateProject"); methodLogger.debug("Updating Lokalise project...", args); try { const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined; // Validate project ID if (!args.projectId || typeof args.projectId !== "string") { throw new McpError("Project ID is required and must be a string.", ErrorType.API_ERROR); } // Validate project data if (!args.projectData || typeof args.projectData !== "object") { throw new McpError("Project data is required and must be an object.", ErrorType.API_ERROR); } // Check that at least one field is provided for update if (Object.keys(args.projectData).length === 0) { throw new McpError("At least one field must be provided to update (name or description).", ErrorType.API_ERROR); } // Check for API token const hasApiToken = Boolean(config.get("LOKALISE_API_KEY")); if (!hasApiToken && !isTestEnvironment) { throw new McpError("Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ErrorType.AUTH_MISSING); } methodLogger.debug("Updating project in Lokalise", { projectId: args.projectId, updateFields: Object.keys(args.projectData), isTestEnvironment, hasApiToken, }); try { const project = await projectsService.default.updateProject(args.projectId, args.projectData); methodLogger.debug("Project updated successfully", { projectId: project.project_id, projectName: project.name, }); const formattedContent = formatUpdateProjectResult(project); return { content: formattedContent }; } catch (error) { if (error instanceof McpError && (error.message.includes("Unauthorized") || error.message.includes("Invalid API token"))) { methodLogger.error("Lokalise API authentication failed"); throw new McpError("Lokalise API authentication failed. Please check your API token.", ErrorType.AUTH_INVALID); } if (error instanceof McpError && (error.message.includes("Not Found") || error.message.includes("404"))) { methodLogger.error("Project not found", { projectId: args.projectId }); throw new McpError(`Project with ID '${args.projectId}' not found. Please check the project ID.`, ErrorType.API_ERROR); } throw error; } } catch (error) { throw handleControllerError(error, buildErrorContext("Lokalise Project", "updateProject", "controllers/projects.controller.ts@updateProject", `projectId: ${args.projectId}`, { args: { ...args, projectData: "[projectData object]" } })); } } /** * @function deleteProject * @description Deletes a Lokalise project. * @memberof ProjectsController * @param {DeleteProjectToolArgsType} args - Arguments containing project deletion options * @returns {Promise<ControllerResponse>} A promise that resolves to the standard controller response containing the formatted project deletion result. * @throws {McpError} Throws an McpError (handled by `handleControllerError`) if the service call fails or returns an error. */ async function deleteProject(args) { const methodLogger = Logger.forContext("controllers/projects.controller.ts", "deleteProject"); methodLogger.debug("Deleting Lokalise project...", args); try { const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined; // Validate project ID if (!args.projectId || typeof args.projectId !== "string") { throw new McpError("Project ID is required and must be a string.", ErrorType.API_ERROR); } // Check for API token const hasApiToken = Boolean(config.get("LOKALISE_API_KEY")); if (!hasApiToken && !isTestEnvironment) { throw new McpError("Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ErrorType.AUTH_MISSING); } methodLogger.debug("Deleting project in Lokalise", { projectId: args.projectId, isTestEnvironment, hasApiToken, }); try { await projectsService.default.deleteProject(args.projectId); methodLogger.debug("Project deleted successfully", { projectId: args.projectId, }); const formattedContent = formatDeleteProjectResult(args.projectId); return { content: formattedContent }; } catch (error) { if (error instanceof McpError && (error.message.includes("Unauthorized") || error.message.includes("Invalid API token"))) { methodLogger.error("Lokalise API authentication failed"); throw new McpError("Lokalise API authentication failed. Please check your API token.", ErrorType.AUTH_INVALID); } if (error instanceof McpError && (error.message.includes("Not Found") || error.message.includes("404"))) { methodLogger.error("Project not found", { projectId: args.projectId }); throw new McpError(`Project with ID '${args.projectId}' not found. Please check the project ID.`, ErrorType.API_ERROR); } throw error; } } catch (error) { throw handleControllerError(error, buildErrorContext("Lokalise Project", "deleteProject", "controllers/projects.controller.ts@deleteProject", `projectId: ${args.projectId}`, { args })); } } /** * @function emptyProject * @description Empties a Lokalise project (removes all keys and translations). * @memberof ProjectsController * @param {EmptyProjectToolArgsType} args - Arguments containing project empty options * @returns {Promise<ControllerResponse>} A promise that resolves to the standard controller response containing the formatted project empty result. * @throws {McpError} Throws an McpError (handled by `handleControllerError`) if the service call fails or returns an error. */ async function emptyProject(args) { const methodLogger = Logger.forContext("controllers/projects.controller.ts", "emptyProject"); methodLogger.debug("Emptying Lokalise project...", args); try { const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined; // Validate project ID if (!args.projectId || typeof args.projectId !== "string") { throw new McpError("Project ID is required and must be a string.", ErrorType.API_ERROR); } // Check for API token const hasApiToken = Boolean(config.get("LOKALISE_API_KEY")); if (!hasApiToken && !isTestEnvironment) { throw new McpError("Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.", ErrorType.AUTH_MISSING); } methodLogger.debug("Emptying project in Lokalise", { projectId: args.projectId, isTestEnvironment, hasApiToken, }); try { await projectsService.default.emptyProject(args.projectId); methodLogger.debug("Project emptied successfully", { projectId: args.projectId, }); const formattedContent = formatEmptyProjectResult(args.projectId); return { content: formattedContent }; } catch (error) { if (error instanceof McpError && (error.message.includes("Unauthorized") || error.message.includes("Invalid API token"))) { methodLogger.error("Lokalise API authentication failed"); throw new McpError("Lokalise API authentication failed. Please check your API token.", ErrorType.AUTH_INVALID); } if (error instanceof McpError && (error.message.includes("Not Found") || error.message.includes("404"))) { methodLogger.error("Project not found", { projectId: args.projectId }); throw new McpError(`Project with ID '${args.projectId}' not found. Please check the project ID.`, ErrorType.API_ERROR); } throw error; } } catch (error) { throw handleControllerError(error, buildErrorContext("Lokalise Project", "emptyProject", "controllers/projects.controller.ts@emptyProject", `projectId: ${args.projectId}`, { args })); } } export default { listProjects, getProjectDetails, createProject, updateProject, deleteProject, emptyProject, };