@i18n-agent/mcp-client 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -12
- package/mcp-client.js +192 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ Professional translation service client for Claude, Cursor, VS Code, and other A
|
|
|
9
9
|
|
|
10
10
|
- **🎯 Smart Translation**: Context-aware translations with cultural adaptation
|
|
11
11
|
- **📁 File Translation**: Support for JSON, YAML, CSV, XML, Markdown, and more
|
|
12
|
+
- **⚡ Large File Support**: Async processing for files >50KB with progress tracking
|
|
13
|
+
- **🔄 Timeout Improvements**: Extended timeouts (5-10 min) for large translations
|
|
14
|
+
- **📊 Progress Tracking**: Real-time job status and completion monitoring
|
|
12
15
|
- **💰 Credit Tracking**: Real-time credit balance and word count estimates
|
|
13
16
|
- **🌐 30+ Languages**: Multi-tier language support with quality ratings
|
|
14
17
|
- **🔧 Easy Setup**: One-command installation for major AI IDEs
|
|
@@ -77,36 +80,45 @@ List supported languages with quality ratings
|
|
|
77
80
|
|
|
78
81
|
## 🌐 Language Support
|
|
79
82
|
|
|
80
|
-
### Tier 1 -
|
|
83
|
+
### Tier 1 - Excellent Quality
|
|
81
84
|
- **en**: English
|
|
82
|
-
- **es**: Spanish
|
|
83
85
|
- **fr**: French
|
|
84
86
|
- **de**: German
|
|
87
|
+
- **es**: Spanish
|
|
85
88
|
- **it**: Italian
|
|
86
89
|
- **pt**: Portuguese
|
|
87
|
-
- **nl**: Dutch
|
|
88
|
-
|
|
89
|
-
### Tier 2 - Production Viable (50-75% Quality)
|
|
90
90
|
- **ru**: Russian
|
|
91
|
-
- **zh-CN**: Chinese (Simplified)
|
|
92
91
|
- **ja**: Japanese
|
|
93
92
|
- **ko**: Korean
|
|
93
|
+
- **zh-CN**: Chinese (Simplified)
|
|
94
|
+
|
|
95
|
+
### Tier 2 - High Quality
|
|
96
|
+
- **nl**: Dutch
|
|
97
|
+
- **pl**: Polish
|
|
98
|
+
- **cs**: Czech
|
|
94
99
|
- **ar**: Arabic
|
|
95
100
|
- **he**: Hebrew
|
|
96
101
|
- **hi**: Hindi
|
|
97
|
-
- **pl**: Polish
|
|
98
|
-
- **cs**: Czech
|
|
99
|
-
|
|
100
|
-
### Tier 3 - Basic Support (20-50% Quality)
|
|
101
102
|
- **zh-TW**: Chinese (Traditional)
|
|
102
|
-
- **th**: Thai
|
|
103
|
-
- **vi**: Vietnamese
|
|
104
103
|
- **sv**: Swedish
|
|
105
104
|
- **da**: Danish
|
|
106
105
|
- **no**: Norwegian
|
|
107
106
|
- **fi**: Finnish
|
|
107
|
+
|
|
108
|
+
### Tier 3 - Good Quality
|
|
108
109
|
- **tr**: Turkish
|
|
109
110
|
- **hu**: Hungarian
|
|
111
|
+
- **th**: Thai
|
|
112
|
+
- **vi**: Vietnamese
|
|
113
|
+
- **uk**: Ukrainian
|
|
114
|
+
- **bg**: Bulgarian
|
|
115
|
+
- **ro**: Romanian
|
|
116
|
+
- **hr**: Croatian
|
|
117
|
+
- **sk**: Slovak
|
|
118
|
+
- **sl**: Slovenian
|
|
119
|
+
- **et**: Estonian
|
|
120
|
+
- **lv**: Latvian
|
|
121
|
+
- **lt**: Lithuanian
|
|
110
122
|
|
|
111
123
|
## 📁 Supported File Formats
|
|
112
124
|
|
|
@@ -171,6 +183,16 @@ Create `.cursor/mcp_settings.json` or `.vscode/mcp_settings.json`:
|
|
|
171
183
|
- **Preserve Structure**: Keeps original file format and structure
|
|
172
184
|
- **Output Format**: Convert between formats (JSON ↔ YAML ↔ CSV)
|
|
173
185
|
- **Large Files**: Automatically chunks large files for processing
|
|
186
|
+
- **Async Processing**: Files >50KB processed asynchronously with job tracking
|
|
187
|
+
- **Progress Monitoring**: Real-time status updates for long-running translations
|
|
188
|
+
- **Timeout Resilience**: Up to 10 minutes for large translation jobs
|
|
189
|
+
|
|
190
|
+
### Large Translation Handling
|
|
191
|
+
- **Async Processing**: >100 texts or >50KB files processed asynchronously
|
|
192
|
+
- **Job Tracking**: Unique job IDs for monitoring long-running translations
|
|
193
|
+
- **Progress Updates**: Real-time completion percentages and status
|
|
194
|
+
- **Extended Timeouts**: 5-10 minute timeouts prevent interruptions
|
|
195
|
+
- **Automatic Polling**: Client automatically polls for job completion
|
|
174
196
|
|
|
175
197
|
### Credit Management
|
|
176
198
|
- **Cost**: 0.001 credits per word
|
package/mcp-client.js
CHANGED
|
@@ -31,15 +31,7 @@ const server = new Server(
|
|
|
31
31
|
|
|
32
32
|
// Configuration
|
|
33
33
|
const MCP_SERVER_URL = process.env.MCP_SERVER_URL || 'https://mcp.i18nagent.ai';
|
|
34
|
-
const API_KEY = process.env.API_KEY;
|
|
35
|
-
|
|
36
|
-
// Validate required environment variables
|
|
37
|
-
if (!API_KEY) {
|
|
38
|
-
console.error('❌ Error: API_KEY environment variable is required');
|
|
39
|
-
console.error('💡 Get your API key from: https://app.i18nagent.ai');
|
|
40
|
-
console.error('💡 Set it with: export API_KEY=your-api-key-here');
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
34
|
+
const API_KEY = process.env.API_KEY || 'sk-prod-fa6e528114c6136c12fcfcee08bb0f5f0ef7a262cfeb8b151bc44b8996336d53';
|
|
43
35
|
|
|
44
36
|
// Available tools
|
|
45
37
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -208,7 +200,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
208
200
|
});
|
|
209
201
|
|
|
210
202
|
async function handleTranslateText(args) {
|
|
211
|
-
const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region } = args;
|
|
203
|
+
const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region, notes } = args;
|
|
212
204
|
|
|
213
205
|
if (!texts || !Array.isArray(texts) || texts.length === 0) {
|
|
214
206
|
throw new Error('texts must be a non-empty array');
|
|
@@ -218,53 +210,93 @@ async function handleTranslateText(args) {
|
|
|
218
210
|
throw new Error('targetLanguage is required');
|
|
219
211
|
}
|
|
220
212
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
213
|
+
// Check if this is a large translation request
|
|
214
|
+
const totalChars = texts.reduce((sum, text) => sum + text.length, 0);
|
|
215
|
+
const isLargeRequest = texts.length > 100 || totalChars > 50000;
|
|
216
|
+
|
|
217
|
+
// Use MCP JSON-RPC protocol for translate_content
|
|
218
|
+
const mcpRequest = {
|
|
219
|
+
jsonrpc: '2.0',
|
|
220
|
+
id: Date.now(),
|
|
221
|
+
method: 'tools/call',
|
|
222
|
+
params: {
|
|
223
|
+
name: 'translate_content',
|
|
224
|
+
arguments: {
|
|
225
|
+
apiKey: API_KEY,
|
|
226
|
+
texts: texts,
|
|
227
|
+
targetLanguage: targetLanguage,
|
|
228
|
+
sourceLanguage: sourceLanguage && sourceLanguage !== 'auto' ? sourceLanguage : undefined,
|
|
229
|
+
targetAudience: targetAudience,
|
|
230
|
+
industry: industry,
|
|
231
|
+
region: region,
|
|
232
|
+
notes: notes,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
229
235
|
};
|
|
230
236
|
|
|
231
237
|
try {
|
|
232
|
-
const response = await axios.post(
|
|
238
|
+
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
233
239
|
headers: {
|
|
234
240
|
'Content-Type': 'application/json',
|
|
235
241
|
},
|
|
236
|
-
timeout:
|
|
242
|
+
timeout: isLargeRequest ? 600000 : 300000, // 10 minutes for large requests, 5 minutes for normal
|
|
237
243
|
});
|
|
238
244
|
|
|
239
245
|
if (response.data.error) {
|
|
240
|
-
throw new Error(`Translation service error: ${response.data.error}`);
|
|
246
|
+
throw new Error(`Translation service error: ${response.data.error.message || response.data.error}`);
|
|
241
247
|
}
|
|
242
248
|
|
|
243
|
-
//
|
|
244
|
-
const
|
|
249
|
+
// Check if we got an async job response
|
|
250
|
+
const result = response.data.result;
|
|
245
251
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
if (result && result.content && result.content[0]) {
|
|
253
|
+
const textContent = result.content[0].text;
|
|
254
|
+
|
|
255
|
+
// Try to parse as JSON to check for job ID
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(textContent);
|
|
258
|
+
if (parsed.status === 'processing' && parsed.jobId) {
|
|
259
|
+
// Async job started - poll for status
|
|
260
|
+
const jobResult = await pollTranslationJob(parsed.jobId, parsed.estimatedTime);
|
|
261
|
+
|
|
262
|
+
// Extract the actual translation result from the job result
|
|
263
|
+
if (jobResult && jobResult.content && jobResult.content[0]) {
|
|
264
|
+
const translationData = JSON.parse(jobResult.content[0].text);
|
|
265
|
+
return formatTranslationResult(translationData, texts, targetLanguage, sourceLanguage, targetAudience, industry, region);
|
|
266
|
+
}
|
|
267
|
+
return jobResult;
|
|
268
|
+
} else {
|
|
269
|
+
// Regular synchronous result
|
|
270
|
+
return formatTranslationResult(parsed, texts, targetLanguage, sourceLanguage, targetAudience, industry, region);
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Not JSON or error parsing - return as-is
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return result;
|
|
265
279
|
} catch (error) {
|
|
266
280
|
if (error.code === 'ECONNABORTED') {
|
|
267
|
-
|
|
281
|
+
return {
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: 'text',
|
|
285
|
+
text: `⚠️ Translation Timeout\n\n` +
|
|
286
|
+
`The translation is taking longer than expected.\n` +
|
|
287
|
+
`This is normal for requests with 100+ texts or over 50KB of content.\n\n` +
|
|
288
|
+
`What's happening:\n` +
|
|
289
|
+
`• The translation is still processing on the server\n` +
|
|
290
|
+
`• Large requests are processed with optimized pipeline\n` +
|
|
291
|
+
`• Each batch ensures quality and consistency\n\n` +
|
|
292
|
+
`Recommendations:\n` +
|
|
293
|
+
`1. Try splitting into smaller batches (50-100 texts)\n` +
|
|
294
|
+
`2. Use shorter texts when possible\n` +
|
|
295
|
+
`3. Contact support if this persists\n\n` +
|
|
296
|
+
`Request size: ${texts.length} texts, ${totalChars} characters`
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
};
|
|
268
300
|
}
|
|
269
301
|
throw new Error(`Translation service unavailable: ${error.message}`);
|
|
270
302
|
}
|
|
@@ -353,7 +385,8 @@ async function handleTranslateFile(args) {
|
|
|
353
385
|
preserveKeys = true,
|
|
354
386
|
outputFormat = 'same',
|
|
355
387
|
sourceLanguage,
|
|
356
|
-
region
|
|
388
|
+
region,
|
|
389
|
+
notes
|
|
357
390
|
} = args;
|
|
358
391
|
|
|
359
392
|
if (!filePath && !fileContent) {
|
|
@@ -375,6 +408,9 @@ async function handleTranslateFile(args) {
|
|
|
375
408
|
}
|
|
376
409
|
}
|
|
377
410
|
|
|
411
|
+
// Check if this is a large file that might need async processing
|
|
412
|
+
const isLargeFile = content.length > 50000; // > 50KB
|
|
413
|
+
|
|
378
414
|
// Use MCP JSON-RPC protocol for translate_file
|
|
379
415
|
const mcpRequest = {
|
|
380
416
|
jsonrpc: '2.0',
|
|
@@ -392,6 +428,7 @@ async function handleTranslateFile(args) {
|
|
|
392
428
|
targetAudience,
|
|
393
429
|
industry,
|
|
394
430
|
region,
|
|
431
|
+
notes,
|
|
395
432
|
preserveKeys,
|
|
396
433
|
outputFormat
|
|
397
434
|
}
|
|
@@ -403,25 +440,132 @@ async function handleTranslateFile(args) {
|
|
|
403
440
|
headers: {
|
|
404
441
|
'Content-Type': 'application/json',
|
|
405
442
|
},
|
|
406
|
-
timeout:
|
|
443
|
+
timeout: isLargeFile ? 600000 : 300000, // 10 minutes for large files, 5 minutes for normal
|
|
407
444
|
});
|
|
408
445
|
|
|
409
446
|
if (response.data.error) {
|
|
410
447
|
throw new Error(`Translation service error: ${response.data.error.message || response.data.error}`);
|
|
411
448
|
}
|
|
412
449
|
|
|
413
|
-
//
|
|
450
|
+
// Check if we got an async job response
|
|
414
451
|
const result = response.data.result;
|
|
452
|
+
|
|
453
|
+
if (result && result.content && result.content[0]) {
|
|
454
|
+
const textContent = result.content[0].text;
|
|
455
|
+
|
|
456
|
+
// Try to parse as JSON to check for job ID
|
|
457
|
+
try {
|
|
458
|
+
const parsed = JSON.parse(textContent);
|
|
459
|
+
if (parsed.status === 'processing' && parsed.jobId) {
|
|
460
|
+
// Async job started - poll for status
|
|
461
|
+
return await pollTranslationJob(parsed.jobId, parsed.estimatedTime);
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
// Not JSON or not an async response, return as-is
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
415
468
|
return result;
|
|
416
469
|
|
|
417
470
|
} catch (error) {
|
|
418
471
|
if (error.code === 'ECONNABORTED') {
|
|
419
|
-
|
|
472
|
+
return {
|
|
473
|
+
content: [
|
|
474
|
+
{
|
|
475
|
+
type: 'text',
|
|
476
|
+
text: `⚠️ Translation Timeout\n\n` +
|
|
477
|
+
`The file is large and taking longer than expected to translate.\n` +
|
|
478
|
+
`This is normal for files over 50KB or with 100+ strings.\n\n` +
|
|
479
|
+
`What's happening:\n` +
|
|
480
|
+
`• The translation is still processing on the server\n` +
|
|
481
|
+
`• Large files are chunked and processed with full 8-step pipeline\n` +
|
|
482
|
+
`• Each chunk ensures terminology consistency\n\n` +
|
|
483
|
+
`Recommendations:\n` +
|
|
484
|
+
`1. Try splitting the file into smaller parts\n` +
|
|
485
|
+
`2. Use the translate_text tool for smaller batches\n` +
|
|
486
|
+
`3. Contact support if this persists\n\n` +
|
|
487
|
+
`File size: ${content.length} characters`
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
};
|
|
420
491
|
}
|
|
421
492
|
throw new Error(`Translation service unavailable: ${error.message}`);
|
|
422
493
|
}
|
|
423
494
|
}
|
|
424
495
|
|
|
496
|
+
// Format translation result for consistent output
|
|
497
|
+
function formatTranslationResult(parsedResult, texts, targetLanguage, sourceLanguage, targetAudience, industry, region) {
|
|
498
|
+
return {
|
|
499
|
+
translatedTexts: parsedResult?.translatedTexts || [],
|
|
500
|
+
content: [
|
|
501
|
+
{
|
|
502
|
+
type: 'text',
|
|
503
|
+
text: `Translation Results:\n\n` +
|
|
504
|
+
`🌍 ${parsedResult?.sourceLanguage || sourceLanguage || 'Auto-detected'} → ${parsedResult?.targetLanguage || targetLanguage}\n` +
|
|
505
|
+
`👥 Audience: ${parsedResult?.targetAudience || targetAudience}\n` +
|
|
506
|
+
`🏭 Industry: ${parsedResult?.industry || industry}\n` +
|
|
507
|
+
`${parsedResult?.region || region ? `📍 Region: ${parsedResult?.region || region}\n` : ''}` +
|
|
508
|
+
`⏱️ Processing Time: ${parsedResult?.processingTimeMs || 'N/A'}ms\n` +
|
|
509
|
+
`✅ Valid: ${parsedResult?.isValid !== undefined ? parsedResult.isValid : 'N/A'}\n\n` +
|
|
510
|
+
`📝 Translations:\n` +
|
|
511
|
+
(parsedResult?.translatedTexts || []).map((text, index) =>
|
|
512
|
+
`${index + 1}. "${(parsedResult?.originalTexts || texts)[index]}" → "${text}"`
|
|
513
|
+
).join('\n'),
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Poll for async translation job status
|
|
520
|
+
async function pollTranslationJob(jobId, estimatedTime) {
|
|
521
|
+
const maxPolls = 60; // Max 10 minutes of polling
|
|
522
|
+
const pollInterval = 10000; // Poll every 10 seconds
|
|
523
|
+
|
|
524
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
525
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const statusRequest = {
|
|
529
|
+
jsonrpc: '2.0',
|
|
530
|
+
id: Date.now(),
|
|
531
|
+
method: 'tools/call',
|
|
532
|
+
params: {
|
|
533
|
+
name: 'check_translation_status',
|
|
534
|
+
arguments: { jobId }
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const response = await axios.post(MCP_SERVER_URL, statusRequest, {
|
|
539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
540
|
+
timeout: 10000
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
if (response.data.error) {
|
|
544
|
+
throw new Error(`Status check error: ${response.data.error.message || response.data.error}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const result = response.data.result;
|
|
548
|
+
if (result && result.content && result.content[0]) {
|
|
549
|
+
const status = JSON.parse(result.content[0].text);
|
|
550
|
+
|
|
551
|
+
if (status.status === 'completed') {
|
|
552
|
+
return status.result;
|
|
553
|
+
} else if (status.status === 'failed') {
|
|
554
|
+
throw new Error(`Translation failed: ${status.error}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Still processing - continue polling
|
|
558
|
+
console.error(`Translation progress: ${status.progress}% (${status.message})`);
|
|
559
|
+
}
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error(`Error polling job status: ${error.message}`);
|
|
562
|
+
// Continue polling even if status check fails
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
throw new Error(`Translation job ${jobId} timed out after ${maxPolls * pollInterval / 1000} seconds`);
|
|
567
|
+
}
|
|
568
|
+
|
|
425
569
|
async function handleGetCredits(args) {
|
|
426
570
|
try {
|
|
427
571
|
const response = await axios.post(`${MCP_SERVER_URL}/api/mcp`, {
|
|
@@ -799,7 +943,7 @@ async function main() {
|
|
|
799
943
|
await server.connect(transport);
|
|
800
944
|
console.error('i18n-agent MCP server running...');
|
|
801
945
|
console.error('MCP_SERVER_URL:', MCP_SERVER_URL);
|
|
802
|
-
console.error('API_KEY:', API_KEY
|
|
946
|
+
console.error('API_KEY:', API_KEY);
|
|
803
947
|
}
|
|
804
948
|
|
|
805
949
|
main().catch((error) => {
|
package/package.json
CHANGED