UNPKG

@translated/lara-mcp

Version:

Lara API official MCP server

459 lines (458 loc) 21.6 kB
import { LaraApiError, TimeoutError as LaraTimeoutError } from "@translated/lara"; import * as z from "zod/v4"; import { addTranslation, addTranslationSchema, addTranslationOutputSchema, } from "./tools/add_translation.js"; import { checkImportStatus, checkImportStatusSchema, checkImportStatusOutputSchema, } from "./tools/check_import_status.js"; import { createMemory, createMemorySchema, createMemoryOutputSchema, } from "./tools/create_memory.js"; import { deleteMemory, deleteMemorySchema, deleteMemoryOutputSchema, } from "./tools/delete_memory.js"; import { deleteTranslation, deleteTranslationSchema, deleteTranslationOutputSchema, } from "./tools/delete_translation.js"; import { importTmx, importTmxSchema, importTmxOutputSchema, } from "./tools/import_tmx.js"; import { listLanguages, listLanguagesSchema, listLanguagesOutputSchema, } from "./tools/list_languages.js"; import { listMemories, listMemoriesSchema, listMemoriesOutputSchema, } from "./tools/list_memories.js"; import { listGlossaries, listGlossariesSchema, listGlossariesOutputSchema, } from "./tools/list_glossaries.js"; import { getGlossary, getGlossarySchema, getGlossaryOutputSchema, } from "./tools/get_glossary.js"; import { createGlossary, createGlossarySchema, createGlossaryOutputSchema, } from "./tools/create_glossary.js"; import { updateGlossary, updateGlossarySchema, updateGlossaryOutputSchema, } from "./tools/update_glossary.js"; import { deleteGlossary, deleteGlossarySchema, deleteGlossaryOutputSchema, } from "./tools/delete_glossary.js"; import { importGlossaryCsv, importGlossaryCsvSchema, importGlossaryCsvOutputSchema, } from "./tools/import_glossary_csv.js"; import { checkGlossaryImportStatus, checkGlossaryImportStatusSchema, checkGlossaryImportStatusOutputSchema, } from "./tools/check_glossary_import_status.js"; import { exportGlossary, exportGlossarySchema, exportGlossaryOutputSchema, } from "./tools/export_glossary.js"; import { getGlossaryCounts, getGlossaryCountsSchema, getGlossaryCountsOutputSchema, } from "./tools/get_glossary_counts.js"; import { addGlossaryEntry, addGlossaryEntrySchema, addGlossaryEntryOutputSchema, } from "./tools/add_glossary_entry.js"; import { deleteGlossaryEntry, deleteGlossaryEntrySchema, deleteGlossaryEntryOutputSchema, } from "./tools/delete_glossary_entry.js"; import { detectLanguage, detectLanguageSchema, detectLanguageOutputSchema, } from "./tools/detect_language.js"; import { translateHandler, translateSchema, translateOutputSchema, } from "./tools/translate.js"; import { updateMemory, updateMemorySchema, updateMemoryOutputSchema, } from "./tools/update_memory.tool.js"; import { InvalidInputError } from "#exception"; import { logger } from "#logger"; const handlers = { detect_language: detectLanguage, translate: translateHandler, create_memory: createMemory, delete_memory: deleteMemory, update_memory: updateMemory, add_translation: addTranslation, delete_translation: deleteTranslation, import_tmx: importTmx, check_import_status: checkImportStatus, get_glossary: getGlossary, create_glossary: createGlossary, update_glossary: updateGlossary, delete_glossary: deleteGlossary, import_glossary_csv: importGlossaryCsv, check_glossary_import_status: checkGlossaryImportStatus, export_glossary: exportGlossary, get_glossary_counts: getGlossaryCounts, add_glossary_entry: addGlossaryEntry, delete_glossary_entry: deleteGlossaryEntry, }; const listers = { list_memories: listMemories, list_languages: listLanguages, list_glossaries: listGlossaries, }; function toStructuredContent(result) { if (Array.isArray(result)) return { items: result }; if (result !== null && result !== undefined && typeof result === "object") { return result; } // Normalize undefined to null so the wire payload always carries the key // explicitly (JSON.stringify drops keys whose value is undefined). return { value: result ?? null }; } function invocationMeta(invoking, invoked) { return { "openai/toolInvocation/invoking": invoking, "openai/toolInvocation/invoked": invoked, }; } function narrate(name, args, result) { switch (name) { case "translate": return Array.isArray(result) ? `Translated ${result.length} segments${args?.target ? " to " + args.target : ""}` : `Translated text${args?.target ? " to " + args.target : ""}`; case "detect_language": return Array.isArray(result) ? `Detected language for ${result.length} inputs` : `Detected language: ${result?.language ?? "unknown"}`; case "list_languages": return `Retrieved ${Array.isArray(result) ? result.length : 0} supported languages`; case "list_memories": return `Found ${Array.isArray(result) ? result.length : 0} translation memories`; case "create_memory": return `Created translation memory "${result?.name ?? args?.name ?? ""}"`; case "update_memory": return `Renamed translation memory to "${result?.name ?? args?.name ?? ""}"`; case "delete_memory": return `Deleted translation memory ${result?.id ?? args?.id ?? ""}`; case "add_translation": return "Added translation unit to memory"; case "delete_translation": return "Deleted translation unit from memory"; case "import_tmx": return `Queued TMX import${result?.id ? " (job " + result.id + ")" : ""}`; case "check_import_status": return `TMX import status: ${result?.status ?? "unknown"}`; case "list_glossaries": return `Found ${Array.isArray(result) ? result.length : 0} glossaries`; case "get_glossary": return `Retrieved glossary "${result?.glossary?.name ?? args?.id ?? ""}"`; case "create_glossary": return `Created glossary "${result?.name ?? args?.name ?? ""}"`; case "update_glossary": return `Renamed glossary to "${result?.name ?? args?.name ?? ""}"`; case "delete_glossary": return `Deleted glossary ${result?.id ?? args?.id ?? ""}`; case "add_glossary_entry": return "Added entry to glossary"; case "delete_glossary_entry": return "Deleted entry from glossary"; case "import_glossary_csv": return `Queued glossary CSV import${result?.id ? " (job " + result.id + ")" : ""}`; case "check_glossary_import_status": return `Glossary import status: ${result?.status ?? "unknown"}`; case "export_glossary": return "Exported glossary as CSV"; case "get_glossary_counts": return `Glossary entry count: ${result?.unidirectional ?? result?.multidirectional ?? "retrieved"}`; default: return `${name} completed`; } } async function CallTool(request, lara) { const { name, arguments: args } = request.params; logger.debug({ toolName: name }, "Tool called"); try { let result; if (name in handlers) { result = await handlers[name](args, lara); } else if (name in listers) { result = await listers[name](lara); } else { logger.warn(`Requested a tool with name ${name}, but it was not found`); throw new InvalidInputError(`Tool ${name} not found`); } const structuredContent = toStructuredContent(result); return { structuredContent, // MCP spec: when outputSchema is declared, the server SHOULD also return // the serialized JSON in a TextContent block for legacy clients that // don't read structuredContent. The narration stays as the first block // so display-oriented clients keep their short summary. content: [ { type: "text", text: narrate(name, args, result) }, { type: "text", text: JSON.stringify(structuredContent) }, ], }; } catch (error) { if (error instanceof z.ZodError) { const fieldErrors = error.issues .map(i => { const field = i.path.length > 0 ? i.path.join('.') : 'arguments'; return `${field}: ${i.message}`; }) .join('; '); throw new InvalidInputError(`Invalid input: ${fieldErrors}`); } // Preserve existing InvalidInputError instances (and their messages) if (error instanceof InvalidInputError) { throw error; } if (error instanceof LaraApiError) { throw new InvalidInputError(error.message); } if (error instanceof LaraTimeoutError) { throw new InvalidInputError("The translation request timed out. Try again or increase the timeout."); } // Log full error internally for debugging logger.error({ error, toolName: name }, "Tool execution error"); // Return generic error to client for unexpected errors throw new InvalidInputError("An error occurred while processing your request"); } } // Anthropic Software Directory policy requires every tool to advertise title, // readOnlyHint, and destructiveHint so clients can render labels and warn // before destructive calls. // https://support.claude.com/en/articles/13145358-anthropic-software-directory-policy const toolDefinitions = [ { name: "detect_language", description: "Detects the language of the provided text. Returns the detected language, content type, and a list of predictions with confidence scores. Accepts a single string or an array of strings (up to 128 elements).", inputSchema: z.toJSONSchema(detectLanguageSchema), outputSchema: z.toJSONSchema(detectLanguageOutputSchema), annotations: { title: "Detect language", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Detecting language…", "Language detected"), }, { name: "translate", description: "Translate text between languages using Lara Translate. Supports language detection, context-aware translations, translation memories, and glossaries. " + "The optional 'instructions' parameter accepts short localization directives (e.g., 'Translate formally') — only provide them when the content specifically requires tone, formality, or terminology adjustments.", inputSchema: z.toJSONSchema(translateSchema), outputSchema: z.toJSONSchema(translateOutputSchema), annotations: { title: "Translate text", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Translating…", "Translation ready"), }, { name: "create_memory", description: "Create a translation memory with a custom name in your Lara Translate account. Translation memories store pairs of source and target text segments (translation units) for reuse in future translations.", inputSchema: z.toJSONSchema(createMemorySchema), outputSchema: z.toJSONSchema(createMemoryOutputSchema), annotations: { title: "Create translation memory", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, }, { name: "delete_memory", description: "Deletes a translation memory from your Lara Translate account.", inputSchema: z.toJSONSchema(deleteMemorySchema), outputSchema: z.toJSONSchema(deleteMemoryOutputSchema), annotations: { title: "Delete translation memory", readOnlyHint: false, destructiveHint: true, openWorldHint: false, }, }, { name: "update_memory", description: "Updates a translation memory in your Lara Translate account.", inputSchema: z.toJSONSchema(updateMemorySchema), outputSchema: z.toJSONSchema(updateMemoryOutputSchema), annotations: { title: "Rename translation memory", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, }, { name: "add_translation", description: "Adds a translation to a translation memory in your Lara Translate account.", inputSchema: z.toJSONSchema(addTranslationSchema), outputSchema: z.toJSONSchema(addTranslationOutputSchema), annotations: { title: "Add translation unit to memory", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, }, { name: "delete_translation", description: "Deletes a translation from a translation memory in your Lara Translate account.", inputSchema: z.toJSONSchema(deleteTranslationSchema), outputSchema: z.toJSONSchema(deleteTranslationOutputSchema), annotations: { title: "Delete translation unit from memory", readOnlyHint: false, destructiveHint: true, openWorldHint: false, }, }, { name: "import_tmx", description: "Imports a TMX file into a translation memory. This is an async operation that returns an import job object containing an import_id. Poll with check_import_status using the returned import_id until the import is complete.", inputSchema: z.toJSONSchema(importTmxSchema), outputSchema: z.toJSONSchema(importTmxOutputSchema), annotations: { title: "Import TMX file", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Queuing TMX import…", "TMX import queued"), }, { name: "check_import_status", description: "Checks the status of a TMX import job started by import_tmx. Poll this tool with the import_id returned from import_tmx until the import is complete. The response includes a progress field to track completion.", inputSchema: z.toJSONSchema(checkImportStatusSchema), outputSchema: z.toJSONSchema(checkImportStatusOutputSchema), annotations: { title: "Check TMX import status", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Checking import status…", "Status retrieved"), }, { name: "list_memories", description: "Lists all translation memories in your Lara Translate account.", inputSchema: z.toJSONSchema(listMemoriesSchema), outputSchema: z.toJSONSchema(listMemoriesOutputSchema), annotations: { title: "List translation memories", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, { name: "list_languages", description: "Lists all supported languages in your Lara Translate account.", inputSchema: z.toJSONSchema(listLanguagesSchema), outputSchema: z.toJSONSchema(listLanguagesOutputSchema), annotations: { title: "List supported languages", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, { name: "list_glossaries", description: "Lists all glossaries in your Lara Translate account. Glossaries are collections of terms with their translations that enforce specific terminology during translation.", inputSchema: z.toJSONSchema(listGlossariesSchema), outputSchema: z.toJSONSchema(listGlossariesOutputSchema), annotations: { title: "List glossaries", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, { name: "get_glossary", description: "Retrieves a specific glossary by ID from your Lara Translate account. Returns null if the glossary is not found.", inputSchema: z.toJSONSchema(getGlossarySchema), outputSchema: z.toJSONSchema(getGlossaryOutputSchema), annotations: { title: "Get glossary", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, { name: "create_glossary", description: "Create a glossary with a custom name in your Lara Translate account. Glossaries enforce specific terminology during translation.", inputSchema: z.toJSONSchema(createGlossarySchema), outputSchema: z.toJSONSchema(createGlossaryOutputSchema), annotations: { title: "Create glossary", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, }, { name: "update_glossary", description: "Updates the name of a glossary in your Lara Translate account.", inputSchema: z.toJSONSchema(updateGlossarySchema), outputSchema: z.toJSONSchema(updateGlossaryOutputSchema), annotations: { title: "Rename glossary", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, }, { name: "delete_glossary", description: "Deletes a glossary from your Lara Translate account.", inputSchema: z.toJSONSchema(deleteGlossarySchema), outputSchema: z.toJSONSchema(deleteGlossaryOutputSchema), annotations: { title: "Delete glossary", readOnlyHint: false, destructiveHint: true, openWorldHint: false, }, }, { name: "import_glossary_csv", description: "Imports a CSV file into a glossary. Supports unidirectional and multidirectional formats. This is an async operation that returns an import job object containing an import_id. Poll with check_glossary_import_status using the returned import_id until the import is complete.", inputSchema: z.toJSONSchema(importGlossaryCsvSchema), outputSchema: z.toJSONSchema(importGlossaryCsvOutputSchema), annotations: { title: "Import glossary CSV", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Queuing glossary import…", "Glossary import queued"), }, { name: "check_glossary_import_status", description: "Checks the status of a glossary CSV import job started by import_glossary_csv. Poll this tool with the import_id returned from import_glossary_csv until the import is complete.", inputSchema: z.toJSONSchema(checkGlossaryImportStatusSchema), outputSchema: z.toJSONSchema(checkGlossaryImportStatusOutputSchema), annotations: { title: "Check glossary import status", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Checking glossary import status…", "Status retrieved"), }, { name: "export_glossary", description: "Exports a glossary as CSV from your Lara Translate account. Supports unidirectional and multidirectional formats.", inputSchema: z.toJSONSchema(exportGlossarySchema), outputSchema: z.toJSONSchema(exportGlossaryOutputSchema), annotations: { title: "Export glossary as CSV", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, _meta: invocationMeta("Exporting glossary…", "Glossary exported"), }, { name: "get_glossary_counts", description: "Retrieves the term and language counts for a glossary in your Lara Translate account.", inputSchema: z.toJSONSchema(getGlossaryCountsSchema), outputSchema: z.toJSONSchema(getGlossaryCountsOutputSchema), annotations: { title: "Get glossary entry count", readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, { name: "add_glossary_entry", description: "Adds or replaces an entry in a glossary in your Lara Translate account. Supports both monodirectional and multidirectional glossaries.", inputSchema: z.toJSONSchema(addGlossaryEntrySchema), outputSchema: z.toJSONSchema(addGlossaryEntryOutputSchema), annotations: { title: "Add or replace glossary entry", readOnlyHint: false, destructiveHint: false, openWorldHint: false, }, }, { name: "delete_glossary_entry", description: "Deletes an entry from a glossary in your Lara Translate account. Use term for monodirectional glossaries or guid for multidirectional glossaries.", inputSchema: z.toJSONSchema(deleteGlossaryEntrySchema), outputSchema: z.toJSONSchema(deleteGlossaryEntryOutputSchema), annotations: { title: "Delete glossary entry", readOnlyHint: false, destructiveHint: true, openWorldHint: false, }, }, ]; async function ListTools() { return { tools: toolDefinitions }; } export { CallTool, ListTools };