Skip to content

Commit b71a045

Browse files
feat: add CLAUDE.md for project guidance, update dependencies and add glossaries support (#17)
* feat: add CLAUDE.md for project guidance and update dependencies - Introduced CLAUDE.md to provide comprehensive guidance on the Lara Translate MCP Server, including setup, development commands, architecture, and error handling. - Updated @translated/lara dependency version from 1.4.0 to 1.7.3 in package.json and pnpm-lock.yaml. - Added new tools for glossary management: list_glossaries and get_glossary, along with corresponding tests. - Enhanced translate tool to support glossaries, no_trace, priority, and timeout_in_millis parameters. * refactor: update documentation and enhance error handling in MCP tools - Updated CLAUDE.md to reflect changes in transport modes and environment variable requirements. - Added a security note in README.md regarding API credentials in HTTP mode. - Enhanced error handling in MCP tools to provide clearer validation messages and prevent exposure of technical details. - Added additional validation rules for glossary IDs and timeout parameters in the translate tool. - Improved logging for privacy-sensitive translation requests. * fix: copilot comments * fix: copilot review * fix: claude.md specs
1 parent 9e882fe commit b71a045

14 files changed

Lines changed: 619 additions & 16 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ out/
2121
# Env
2222
.env
2323

24+
# Claude Code local settings
25+
.claude/settings.local.json
26+
2427
# mpeltonen/sbt-idea plugin
2528
.idea_modules/
2629

CLAUDE.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
Lara Translate MCP Server is a Model Context Protocol (MCP) server that provides translation capabilities through the Lara Translate API. The server supports both STDIO and HTTP transport modes.
8+
9+
## Development Commands
10+
11+
### Setup
12+
```bash
13+
# Install dependencies
14+
pnpm install
15+
16+
# Build the project
17+
pnpm run build
18+
```
19+
20+
### Development
21+
```bash
22+
# Run in development mode with hot reload
23+
pnpm run dev
24+
25+
# Start the built server
26+
pnpm run start
27+
```
28+
29+
### Testing
30+
```bash
31+
# Run all tests once
32+
pnpm test
33+
34+
# Run tests in watch mode
35+
pnpm test:watch
36+
37+
# Run tests with coverage report
38+
pnpm test:coverage
39+
```
40+
41+
### Docker Development
42+
```bash
43+
# Build Docker image
44+
docker build -t lara-mcp .
45+
```
46+
47+
## Architecture
48+
49+
### Server Modes
50+
51+
The server operates in two transport modes determined by the `TRANSPORT` environment variable:
52+
53+
1. **STDIO Mode** (`src/index.ts:56-75`): Direct MCP server using stdio transport, requires `LARA_ACCESS_KEY_ID` and `LARA_ACCESS_KEY_SECRET` environment variables.
54+
55+
2. **HTTP Mode** (`src/index.ts:42-54`): REST API server with MCP protocol endpoint at `/v1`
56+
57+
### Core Components
58+
59+
#### MCP Server (`src/mcp/server.ts`)
60+
61+
The server factory function `getMcpServer(accessKeyId, accessKeySecret)` creates an MCP server instance with:
62+
- `accessKeyId`: The Lara Translate API access key ID
63+
- `accessKeySecret`: The Lara Translate API access key secret
64+
65+
The server initializes a `Translator` instance from the `@translated/lara` SDK and configures MCP request handlers for tools and resources.
66+
67+
#### Tools (`src/mcp/tools/`)
68+
69+
All MCP tools are organized in individual files under `src/mcp/tools/`:
70+
71+
**Translation Tools:**
72+
- `translate.ts` - Main translation with context, instructions, memory support, and glossaries
73+
- Advanced options: `glossaries` (array of glossary IDs, max 10), `no_trace` (privacy flag), `priority` (normal/background), `timeout_in_millis` (max 300000ms)
74+
- Validation includes format checks for glossary IDs (`gls_*` pattern) and timeout limits
75+
76+
**Glossary Management Tools:**
77+
- `list_glossaries.ts` - List all glossaries
78+
- `get_glossary.ts` - Get glossary by ID (returns null if not found)
79+
- Validates glossary ID format with regex `/^gls_[a-zA-Z0-9_-]+$/`
80+
81+
**Memory Management Tools:**
82+
- `list_memories.ts` - List all translation memories
83+
- `create_memory.ts` - Create new memory (supports MyMemory import via `external_id`)
84+
- `update_memory.tool.ts` - Update memory name
85+
- `delete_memory.ts` - Delete memory
86+
- `add_translation.ts` - Add translation unit to memory
87+
- `delete_translation.ts` - Remove translation unit from memory
88+
- `import_tmx.ts` - Import TMX file (supports gzip compression)
89+
- `check_import_status.ts` - Check TMX import job status
90+
91+
**Language Support:**
92+
- `list_languages.ts` - List supported languages
93+
94+
Each tool exports a handler function and a Zod validation schema. Tool registration happens in `src/mcp/tools.ts` which maintains two handler maps:
95+
- `handlers` - Tools with arguments (e.g., translate, create_memory)
96+
- `listers` - Tools without arguments (e.g., list_memories, list_languages)
97+
98+
#### Resources (`src/mcp/resources.ts`)
99+
100+
The MCP server exposes translation memories as resources:
101+
- Resource URI format: `memory://{memoryId}`
102+
- Resource template: `memory://{memoryId}` for listing memories
103+
104+
### Path Aliases
105+
106+
The project uses path aliases (configured in `tsconfig.json` and `package.json` imports):
107+
- `#env``src/env.js`
108+
- `#exception``src/exception.js`
109+
- `#logger``src/logger.js`
110+
- `#rest/server``src/rest/server.js`
111+
- `#mcp/server``src/mcp/server.js`
112+
113+
### Environment Variables
114+
115+
Core configuration (`src/env.ts`):
116+
- `TRANSPORT` - Server mode: `stdio` or `http` (default: `stdio`)
117+
- `HOST` / `PORT` - HTTP server binding (default: `0.0.0.0:3000`)
118+
- `LARA_ACCESS_KEY_ID` / `LARA_ACCESS_KEY_SECRET` - API credentials (required for STDIO mode)
119+
- `LOGGING_LEVEL` - Log level: `debug`, `info`, `warn`, `error` (default: `info`)
120+
121+
### Error Handling
122+
123+
Custom exception classes (`src/exception.ts`):
124+
- `ServerException` - Base exception with error code
125+
- `InvalidInputError` - Invalid request parameters (code: -32600)
126+
- `InvalidCredentialsError` - Authentication failure (code: -32600)
127+
- `MethodNotAllowedError` - HTTP method not allowed (code: -32601)
128+
129+
Error handling in `src/mcp/tools.ts`:
130+
- Zod validation errors return specific field names (not full error details for security)
131+
- Existing `InvalidInputError` instances are preserved and re-thrown
132+
- Other unexpected errors are logged internally and returned as generic "An error occurred while processing your request" message
133+
- Privacy-sensitive translations (with `no_trace=true`) are logged for audit purposes
134+
135+
### Logging
136+
137+
The server uses Pino structured logging (`src/logger.ts`). Log level is controlled by `LOGGING_LEVEL` environment variable.
138+
139+
## Testing
140+
141+
Tests are located in `src/__tests__/` and mirror the source structure:
142+
- `tools/` - Individual tool tests (71 total tests)
143+
- `server/` - REST server tests
144+
- `utils/mocks.ts` - Shared test utilities with Vitest mocks
145+
146+
Tests use Vitest with coverage reporting (v8 provider).
147+
148+
## Security Features
149+
150+
- **Input validation**: All glossary IDs validated with regex, timeout capped at 300000ms, max 10 glossaries per request
151+
- **Error sanitization**: Zod errors filtered to show only field names, SDK errors hidden behind generic messages
152+
- **Audit logging**: Privacy-sensitive requests (no_trace=true) logged for compliance
153+
- **Credential protection**: Access key ID never logged in debug mode
154+
155+
## Important Notes
156+
157+
- When adding new tools, update both the handler in `src/mcp/tools/{tool}.ts` and register it in `src/mcp/tools.ts`.
158+
- All file imports must use the `.js` extension even though source files are `.ts` (ES module resolution requirement).
159+
- The `translate` tool builds options object dynamically - only includes non-empty arrays and defined values to avoid passing `undefined` to SDK.
160+
- Semicolons are consistently used throughout the codebase.

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,33 @@ Lara also lowers the cost of using models like GPT-4 in non-English workflows. S
8282
- `context` (optional string): Additional context to improve translation quality
8383
- `instructions` (optional string[]): Instructions to adjust translation behavior
8484
- `source_hint` (optional string): Guidance for language detection
85+
- `glossaries` (optional string[]): Array of glossary IDs to enforce terminology (e.g., ['gls_xyz123'])
86+
- `no_trace` (optional boolean): Privacy flag - if true, request won't be traced/logged
87+
- `priority` (optional string): Translation priority - 'normal' or 'background'
88+
- `timeout_in_millis` (optional number): Custom timeout in milliseconds
8589

8690
**Returns**: Translated text blocks maintaining the original structure
8791
</details>
8892

93+
### Glossaries Tools
94+
95+
<details>
96+
<summary><strong>list_glossaries</strong> - List all glossaries</summary>
97+
98+
**Inputs**: None
99+
100+
**Returns**: Array of glossaries with their details (id, name, createdAt, updatedAt, ownerId)
101+
</details>
102+
103+
<details>
104+
<summary><strong>get_glossary</strong> - Get a specific glossary by ID</summary>
105+
106+
**Inputs**:
107+
- `id` (string): The glossary ID (e.g., 'gls_xyz123')
108+
109+
**Returns**: Glossary object or null if not found
110+
</details>
111+
89112
### Translation Memories Tools
90113

91114
<details>
@@ -181,6 +204,18 @@ Lara supports both the STDIO and streamable HTTP protocols. For a hassle-free se
181204

182205
You'll find setup instructions for both protocols in the sections below.
183206

207+
## ⚠️ Security Note
208+
209+
**Important:** When running your own HTTP server instance (not using the remote `https://mcp.laratranslate.com/v1`), all connected clients share the same Lara API credentials configured via `LARA_ACCESS_KEY_ID` and `LARA_ACCESS_KEY_SECRET` environment variables.
210+
211+
This server is designed for:
212+
- ✅ Single-user deployments
213+
- ✅ Trusted-environment deployments (e.g., internal tools)
214+
215+
For multi-tenant scenarios, either:
216+
- Use the remote server at `https://mcp.laratranslate.com/v1` where each client provides their own credentials via headers
217+
- Deploy separate MCP server instances per user with isolated credentials
218+
184219
### HTTP Server 🌐
185220
<details>
186221
<summary><strong>❌ Clients NOT supporting <code>url</code> configuration (e.g., Claude, OpenAI)</strong></summary>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
],
5353
"dependencies": {
5454
"@modelcontextprotocol/sdk": "^1.12.3",
55-
"@translated/lara": "^1.4.0",
55+
"@translated/lara": "^1.7.3",
5656
"cors": "^2.8.5",
5757
"express": "^5.1.0",
5858
"helmet": "^8.1.0",

pnpm-lock.yaml

Lines changed: 18 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { getGlossary, getGlossarySchema } from '../../mcp/tools/get_glossary.js';
3+
import { getMockTranslator, setupTranslatorMock, type MockTranslator } from '../utils/mocks.js';
4+
import { Translator } from '@translated/lara';
5+
6+
// Setup mocks
7+
setupTranslatorMock();
8+
9+
describe('getGlossarySchema', () => {
10+
it('should validate correct input', () => {
11+
expect(() => getGlossarySchema.parse({ id: 'gls_xyz123' })).not.toThrow();
12+
});
13+
14+
it('should reject missing id', () => {
15+
expect(() => getGlossarySchema.parse({})).toThrow();
16+
});
17+
18+
it('should reject empty glossary ID', () => {
19+
expect(() => getGlossarySchema.parse({ id: '' })).toThrow();
20+
});
21+
22+
it('should reject very long glossary ID', () => {
23+
const longId = 'gls_' + 'a'.repeat(300);
24+
expect(() => getGlossarySchema.parse({ id: longId })).toThrow();
25+
});
26+
27+
it('should reject invalid glossary ID format', () => {
28+
expect(() => getGlossarySchema.parse({ id: 'invalid-id' })).toThrow();
29+
});
30+
});
31+
32+
describe('getGlossary', () => {
33+
let mockTranslator: MockTranslator;
34+
35+
beforeEach(() => {
36+
mockTranslator = getMockTranslator();
37+
});
38+
39+
it('should call lara.glossaries.get with correct id', async () => {
40+
const mockGlossary = {
41+
id: 'gls_xyz123',
42+
name: 'Test Glossary',
43+
createdAt: 1234567890,
44+
updatedAt: 1234567890,
45+
ownerId: 'user1'
46+
};
47+
48+
mockTranslator.glossaries.get.mockResolvedValue(mockGlossary);
49+
50+
const result = await getGlossary({ id: 'gls_xyz123' }, mockTranslator as any as Translator);
51+
52+
expect(mockTranslator.glossaries.get).toHaveBeenCalledWith('gls_xyz123');
53+
expect(result).toEqual(mockGlossary);
54+
});
55+
56+
it('should return null for non-existent glossary', async () => {
57+
mockTranslator.glossaries.get.mockResolvedValue(null);
58+
59+
const result = await getGlossary({ id: 'gls_nonexistent' }, mockTranslator as any as Translator);
60+
61+
expect(result).toBeNull();
62+
});
63+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { listGlossaries, listGlossariesSchema } from '../../mcp/tools/list_glossaries.js';
3+
import { getMockTranslator, setupTranslatorMock, type MockTranslator } from '../utils/mocks.js';
4+
import { Translator } from '@translated/lara';
5+
6+
// Setup mocks
7+
setupTranslatorMock();
8+
9+
describe('listGlossariesSchema', () => {
10+
it('should validate empty object', () => {
11+
expect(() => listGlossariesSchema.parse({})).not.toThrow();
12+
});
13+
});
14+
15+
describe('listGlossaries', () => {
16+
let mockTranslator: MockTranslator;
17+
18+
beforeEach(() => {
19+
mockTranslator = getMockTranslator();
20+
});
21+
22+
it('should call lara.glossaries.list and return the result', async () => {
23+
const mockGlossariesList = [
24+
{ id: 'gls_xyz123', name: 'glossary1', createdAt: 1234567890, updatedAt: 1234567890, ownerId: 'user1' },
25+
{ id: 'gls_abc456', name: 'glossary2', createdAt: 1234567890, updatedAt: 1234567890, ownerId: 'user1' }
26+
];
27+
28+
mockTranslator.glossaries.list.mockResolvedValue(mockGlossariesList);
29+
30+
const result = await listGlossaries(mockTranslator as any as Translator);
31+
32+
expect(mockTranslator.glossaries.list).toHaveBeenCalled();
33+
expect(result).toEqual(mockGlossariesList);
34+
});
35+
});

0 commit comments

Comments
 (0)