lokalise_list_projects
List all accessible localization projects to discover projects, assess translation health, and find project IDs. Optionally include progress and QA statistics.
Instructions
Portfolio overview showing all accessible localization projects. Optional: limit (100), page, includeStats (adds progress/QA data). Use as entry point to discover projects, assess translation health, or find specific project IDs. Returns: Projects with names, IDs, base language, stats. Start here before diving into specific projects.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Number of projects to return (1-500, default: 100) | |
| page | No | Page number for pagination (default: 1) | |
| includeStats | No | Include detailed project statistics |
Implementation Reference
- Core MCP tool handler that receives list projects arguments, delegates to projectsController.listProjects(), and returns a formatted text response.
async function handleListProjects(args: ListProjectsToolArgsType) { const methodLogger = Logger.forContext( "tools/projects.tool.ts", "handleListProjects", ); methodLogger.debug( `Getting Lokalise projects list (limit: ${args.limit || "default"}, page: ${args.page || "1"})...`, args, ); try { // Pass args directly to the controller const result = await projectsController.listProjects(args); methodLogger.debug("Got the response from the controller", result); // Format the response for the MCP tool return { content: [ { type: "text" as const, text: result.content, }, ], }; } catch (error) { methodLogger.error("Error getting Lokalise projects list", error); return formatErrorForMcpTool(error); } } - Zod validation schema for the list projects tool arguments: limit (number, 1-500, optional), page (number, optional), includeStats (boolean, default false).
export const ListProjectsToolArgs = z .object({ limit: z .number() .optional() .describe("Number of projects to return (1-500, default: 100)"), page: z .number() .optional() .describe("Page number for pagination (default: 1)"), includeStats: z .boolean() .optional() .default(false) .describe("Include detailed project statistics"), }) .strict(); - src/domains/projects/projects.tool.ts:252-258 (registration)Registration of the 'lokalise_list_projects' tool with the MCP server, binding the name, description, schema, and handler.
// Register project listing tool server.tool( "lokalise_list_projects", "Portfolio overview showing all accessible localization projects. Optional: limit (100), page, includeStats (adds progress/QA data). Use as entry point to discover projects, assess translation health, or find specific project IDs. Returns: Projects with names, IDs, base language, stats. Start here before diving into specific projects.", ListProjectsToolArgs.shape, handleListProjects, ); - Controller that orchestrates the list projects flow: validates args, calls projectsService to fetch data, and formats via formatProjectsList.
async function listProjects( args: ListProjectsToolArgsType, ): Promise<ControllerResponse> { const methodLogger = Logger.forContext( "controllers/projects.controller.ts", "listProjects", ); methodLogger.debug("Getting Lokalise projects list..."); try { // Apply defaults and validation const options: ApiRequestOptions = { limit: args.limit, page: args.page, }; if ( options.limit !== undefined && (options.limit < 1 || options.limit > 500) ) { // Validate limit if provided throw new McpError( "VALIDATION_ERROR: Invalid limit parameter. Must be between 1 and 500.", ErrorType.VALIDATION_ERROR, ); } if (options.page !== undefined && options.page < 1) { // Validate page if provided throw new McpError( "VALIDATION_ERROR: Invalid page parameter. Must be 1 or greater.", ErrorType.VALIDATION_ERROR, ); } // Note: API key validation is deferred to service layer for lazy loading methodLogger.debug("Getting projects from Lokalise", { originalOptions: args, options, }); 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, }); // Pass pagination metadata to formatter const metadata = { page: args.page, limit: args.limit, }; const formattedContent = formatProjectsList( projects, args.includeStats, metadata, ); 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 }, ), ); } } - Formatter that converts project data into a Markdown string with headings, project info, optional statistics, and dashboard links.
export function formatProjectsList( projects: LokaliseProject[], includeStats = false, metadata?: { page?: number; limit?: number; total?: number }, ): string { const lines: string[] = []; // Add a main heading with optional pagination info const headingParts = [`Lokalise Projects (${projects.length})`]; if (metadata?.page) { headingParts.push(`- Page ${metadata.page}`); } lines.push(formatHeading(headingParts.join(" "), 1)); lines.push(""); if (projects.length === 0) { lines.push( "No projects found. Create your first project in the Lokalise dashboard.", ); lines.push(""); lines.push(formatSeparator()); lines.push(`*List retrieved at ${formatDate(new Date())}*`); return lines.join("\n"); } // Add projects list for (const project of projects) { lines.push(formatHeading(project.name, 2)); lines.push(""); // Add basic project information section lines.push(formatHeading("Project Information", 3)); const projectInfo: Record<string, unknown> = { "Project ID": project.project_id, "Base Language": project.base_language_iso, Created: formatDate(project.created_at), "Created By": project.created_by_email, }; // Add description if available if (project.description) { projectInfo.Description = project.description; } // Add basic stats inline if not detailed if (project.statistics) { projectInfo.Progress = `${project.statistics.progress_total}%`; projectInfo["Total Keys"] = project.statistics.keys_total; projectInfo.Languages = project.statistics.languages?.length; } else { projectInfo.Progress = "N/A"; projectInfo["Total Keys"] = "N/A"; projectInfo.Languages = "N/A"; } lines.push(formatBulletList(projectInfo)); lines.push(""); if (project.statistics && includeStats) { // Add statistics section if available and requested lines.push(formatHeading("Project Statistics", 3)); const statsInfo: Record<string, unknown> = { Progress: `${project.statistics.progress_total}%`, "Total Keys": project.statistics.keys_total, Languages: project.statistics.languages?.length, "Team Members": project.statistics.team, "Base Words": project.statistics.base_words, }; if (project.statistics.qa_issues_total > 0) { statsInfo["QA Issues"] = project.statistics.qa_issues_total; } lines.push(formatBulletList(statsInfo)); lines.push(""); } // Add project URL section lines.push(formatHeading("Dashboard", 3)); const baseUrl = config.getLokaliseHostname("lokalise.com"); const projectUrl = `https://app.${baseUrl}/project/${project.project_id}/?view=multi`; lines.push(`${formatUrl(projectUrl, "View in Lokalise Dashboard")}`); lines.push(""); } // Add a separator lines.push(formatSeparator()); // Add a timestamp footer lines.push(`*List retrieved at ${formatDate(new Date())}*`); return lines.join("\n"); }