@i18n-agent/mcp-client 1.0.3 → 1.1.1

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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/mcp-client.js +246 -88
  3. package/package.json +3 -3
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
@@ -180,6 +183,16 @@ Create `.cursor/mcp_settings.json` or `.vscode/mcp_settings.json`:
180
183
  - **Preserve Structure**: Keeps original file format and structure
181
184
  - **Output Format**: Convert between formats (JSON ↔ YAML ↔ CSV)
182
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
183
196
 
184
197
  ### Credit Management
185
198
  - **Cost**: 0.001 credits per word
package/mcp-client.js CHANGED
@@ -30,16 +30,14 @@ const server = new Server(
30
30
  );
31
31
 
32
32
  // Configuration
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);
33
+ if (!process.env.MCP_SERVER_URL) {
34
+ throw new Error('MCP_SERVER_URL environment variable is required');
42
35
  }
36
+ if (!process.env.API_KEY) {
37
+ throw new Error('API_KEY environment variable is required');
38
+ }
39
+ const MCP_SERVER_URL = process.env.MCP_SERVER_URL;
40
+ const API_KEY = process.env.API_KEY;
43
41
 
44
42
  // Available tools
45
43
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -208,7 +206,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
208
206
  });
209
207
 
210
208
  async function handleTranslateText(args) {
211
- const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region } = args;
209
+ const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region, notes } = args;
212
210
 
213
211
  if (!texts || !Array.isArray(texts) || texts.length === 0) {
214
212
  throw new Error('texts must be a non-empty array');
@@ -218,102 +216,144 @@ async function handleTranslateText(args) {
218
216
  throw new Error('targetLanguage is required');
219
217
  }
220
218
 
221
- const requestData = {
222
- apiKey: API_KEY,
223
- texts: texts,
224
- targetLanguage: targetLanguage,
225
- sourceLanguage: sourceLanguage && sourceLanguage !== 'auto' ? sourceLanguage : undefined,
226
- targetAudience: targetAudience,
227
- industry: industry,
228
- region: region,
219
+ // Check if this is a large translation request
220
+ const totalChars = texts.reduce((sum, text) => sum + text.length, 0);
221
+ const isLargeRequest = texts.length > 100 || totalChars > 50000;
222
+
223
+ // Use MCP JSON-RPC protocol for translate_content
224
+ const mcpRequest = {
225
+ jsonrpc: '2.0',
226
+ id: Date.now(),
227
+ method: 'tools/call',
228
+ params: {
229
+ name: 'translate_content',
230
+ arguments: {
231
+ apiKey: API_KEY,
232
+ texts: texts,
233
+ targetLanguage: targetLanguage,
234
+ sourceLanguage: sourceLanguage && sourceLanguage !== 'auto' ? sourceLanguage : undefined,
235
+ targetAudience: targetAudience,
236
+ industry: industry,
237
+ region: region,
238
+ notes: notes,
239
+ }
240
+ }
229
241
  };
230
242
 
231
243
  try {
232
- const response = await axios.post(`${MCP_SERVER_URL}/translate`, requestData, {
244
+ const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
233
245
  headers: {
234
246
  'Content-Type': 'application/json',
235
247
  },
236
- timeout: 60000, // 60 second timeout
248
+ timeout: isLargeRequest ? 600000 : 300000, // 10 minutes for large requests, 5 minutes for normal
237
249
  });
238
250
 
239
251
  if (response.data.error) {
240
- throw new Error(`Translation service error: ${response.data.error}`);
252
+ throw new Error(`Translation service error: ${response.data.error.message || response.data.error}`);
241
253
  }
242
254
 
243
- // Direct API response format: { translatedTexts: [...], ... }
244
- const parsedResult = response.data;
255
+ // Check if we got an async job response
256
+ const result = response.data.result;
245
257
 
246
- return {
247
- translatedTexts: parsedResult?.translatedTexts || [],
248
- content: [
249
- {
250
- type: 'text',
251
- text: `Translation Results:\n\n` +
252
- `🌍 ${parsedResult?.sourceLanguage || sourceLanguage || 'Auto-detected'} ${parsedResult?.targetLanguage || targetLanguage}\n` +
253
- `👥 Audience: ${parsedResult?.targetAudience || targetAudience}\n` +
254
- `🏭 Industry: ${parsedResult?.industry || industry}\n` +
255
- `${parsedResult?.region || region ? `📍 Region: ${parsedResult?.region || region}\n` : ''}` +
256
- `⏱️ Processing Time: ${parsedResult?.processingTimeMs || 'N/A'}ms\n` +
257
- `✅ Valid: ${parsedResult?.isValid !== undefined ? parsedResult.isValid : 'N/A'}\n\n` +
258
- `📝 Translations:\n` +
259
- (parsedResult?.translatedTexts || []).map((text, index) =>
260
- `${index + 1}. "${(parsedResult?.originalTexts || texts)[index]}" → "${text}"`
261
- ).join('\n'),
262
- },
263
- ],
264
- };
258
+ if (result && result.content && result.content[0]) {
259
+ const textContent = result.content[0].text;
260
+
261
+ // Try to parse as JSON to check for job ID
262
+ try {
263
+ const parsed = JSON.parse(textContent);
264
+ if (parsed.status === 'processing' && parsed.jobId) {
265
+ // Async job started - poll for status
266
+ const jobResult = await pollTranslationJob(parsed.jobId, parsed.estimatedTime);
267
+
268
+ // Extract the actual translation result from the job result
269
+ if (jobResult && jobResult.content && jobResult.content[0]) {
270
+ const translationData = JSON.parse(jobResult.content[0].text);
271
+ return formatTranslationResult(translationData, texts, targetLanguage, sourceLanguage, targetAudience, industry, region);
272
+ }
273
+ return jobResult;
274
+ } else {
275
+ // Regular synchronous result
276
+ return formatTranslationResult(parsed, texts, targetLanguage, sourceLanguage, targetAudience, industry, region);
277
+ }
278
+ } catch {
279
+ // Not JSON or error parsing - return as-is
280
+ return result;
281
+ }
282
+ }
283
+
284
+ return result;
265
285
  } catch (error) {
266
286
  if (error.code === 'ECONNABORTED') {
267
- throw new Error('Translation request timed out. The service may be processing a large request.');
287
+ return {
288
+ content: [
289
+ {
290
+ type: 'text',
291
+ text: `⚠️ Translation Timeout\n\n` +
292
+ `The translation is taking longer than expected.\n` +
293
+ `This is normal for requests with 100+ texts or over 50KB of content.\n\n` +
294
+ `What's happening:\n` +
295
+ `• The translation is still processing on the server\n` +
296
+ `• Large requests are processed with optimized pipeline\n` +
297
+ `• Each batch ensures quality and consistency\n\n` +
298
+ `Recommendations:\n` +
299
+ `1. Try splitting into smaller batches (50-100 texts)\n` +
300
+ `2. Use shorter texts when possible\n` +
301
+ `3. Contact support if this persists\n\n` +
302
+ `Request size: ${texts.length} texts, ${totalChars} characters`
303
+ }
304
+ ]
305
+ };
268
306
  }
269
- throw new Error(`Translation service unavailable: ${error.message}`);
307
+
308
+ // Check if it's actually a service unavailable error (503, timeout, connection issues)
309
+ if (error.code === 'ECONNREFUSED' ||
310
+ error.code === 'ETIMEDOUT' ||
311
+ error.response?.status === 503 ||
312
+ error.response?.status === 502 ||
313
+ error.response?.status === 504) {
314
+ throw new Error(`Translation service unavailable: ${error.message}`);
315
+ }
316
+
317
+ // For other errors (401, 402, 404, etc), throw them as-is without "unavailable" keyword
318
+ throw error;
270
319
  }
271
320
  }
272
321
 
273
322
  async function handleListLanguages(args) {
274
323
  const { includeQuality = true } = args;
275
324
 
276
- // Language support matrix based on translation quality
325
+ // Language support matrix based on GPT-OSS analysis
277
326
  const languages = {
278
- 'Tier 1 - Excellent Quality': {
327
+ 'Tier 1 - Production Ready (Excellent Quality 80-90%)': {
279
328
  'en': 'English',
329
+ 'es': 'Spanish',
280
330
  'fr': 'French',
281
331
  'de': 'German',
282
- 'es': 'Spanish',
283
332
  'it': 'Italian',
284
333
  'pt': 'Portuguese',
334
+ 'nl': 'Dutch',
335
+ },
336
+ 'Tier 2 - Production Viable (Good Quality 50-75%)': {
285
337
  'ru': 'Russian',
338
+ 'zh-CN': 'Chinese (Simplified)',
286
339
  'ja': 'Japanese',
287
340
  'ko': 'Korean',
288
- 'zh-CN': 'Chinese (Simplified)',
289
- },
290
- 'Tier 2 - High Quality': {
291
- 'nl': 'Dutch',
292
- 'pl': 'Polish',
293
- 'cs': 'Czech',
294
341
  'ar': 'Arabic',
295
342
  'he': 'Hebrew',
296
343
  'hi': 'Hindi',
344
+ 'pl': 'Polish',
345
+ 'cs': 'Czech',
346
+ },
347
+ 'Tier 3 - Basic Support (Use with Caution 20-50%)': {
297
348
  'zh-TW': 'Chinese (Traditional)',
349
+ 'th': 'Thai',
350
+ 'vi': 'Vietnamese',
298
351
  'sv': 'Swedish',
299
352
  'da': 'Danish',
300
353
  'no': 'Norwegian',
301
354
  'fi': 'Finnish',
302
- },
303
- 'Tier 3 - Good Quality': {
304
355
  'tr': 'Turkish',
305
356
  'hu': 'Hungarian',
306
- 'th': 'Thai',
307
- 'vi': 'Vietnamese',
308
- 'uk': 'Ukrainian',
309
- 'bg': 'Bulgarian',
310
- 'ro': 'Romanian',
311
- 'hr': 'Croatian',
312
- 'sk': 'Slovak',
313
- 'sl': 'Slovenian',
314
- 'et': 'Estonian',
315
- 'lv': 'Latvian',
316
- 'lt': 'Lithuanian',
317
357
  },
318
358
  };
319
359
 
@@ -362,7 +402,8 @@ async function handleTranslateFile(args) {
362
402
  preserveKeys = true,
363
403
  outputFormat = 'same',
364
404
  sourceLanguage,
365
- region
405
+ region,
406
+ notes
366
407
  } = args;
367
408
 
368
409
  if (!filePath && !fileContent) {
@@ -384,6 +425,9 @@ async function handleTranslateFile(args) {
384
425
  }
385
426
  }
386
427
 
428
+ // Check if this is a large file that might need async processing
429
+ const isLargeFile = content.length > 50000; // > 50KB
430
+
387
431
  // Use MCP JSON-RPC protocol for translate_file
388
432
  const mcpRequest = {
389
433
  jsonrpc: '2.0',
@@ -401,6 +445,7 @@ async function handleTranslateFile(args) {
401
445
  targetAudience,
402
446
  industry,
403
447
  region,
448
+ notes,
404
449
  preserveKeys,
405
450
  outputFormat
406
451
  }
@@ -412,46 +457,159 @@ async function handleTranslateFile(args) {
412
457
  headers: {
413
458
  'Content-Type': 'application/json',
414
459
  },
415
- timeout: 60000,
460
+ timeout: isLargeFile ? 600000 : 300000, // 10 minutes for large files, 5 minutes for normal
416
461
  });
417
462
 
418
463
  if (response.data.error) {
419
464
  throw new Error(`Translation service error: ${response.data.error.message || response.data.error}`);
420
465
  }
421
466
 
422
- // MCP response format
467
+ // Check if we got an async job response
423
468
  const result = response.data.result;
469
+
470
+ if (result && result.content && result.content[0]) {
471
+ const textContent = result.content[0].text;
472
+
473
+ // Try to parse as JSON to check for job ID
474
+ try {
475
+ const parsed = JSON.parse(textContent);
476
+ if (parsed.status === 'processing' && parsed.jobId) {
477
+ // Async job started - poll for status
478
+ return await pollTranslationJob(parsed.jobId, parsed.estimatedTime);
479
+ }
480
+ } catch {
481
+ // Not JSON or not an async response, return as-is
482
+ }
483
+ }
484
+
424
485
  return result;
425
486
 
426
487
  } catch (error) {
427
488
  if (error.code === 'ECONNABORTED') {
428
- throw new Error('Translation request timed out. The service may be processing a large request.');
489
+ return {
490
+ content: [
491
+ {
492
+ type: 'text',
493
+ text: `⚠️ Translation Timeout\n\n` +
494
+ `The file is large and taking longer than expected to translate.\n` +
495
+ `This is normal for files over 50KB or with 100+ strings.\n\n` +
496
+ `What's happening:\n` +
497
+ `• The translation is still processing on the server\n` +
498
+ `• Large files are chunked and processed with full 8-step pipeline\n` +
499
+ `• Each chunk ensures terminology consistency\n\n` +
500
+ `Recommendations:\n` +
501
+ `1. Try splitting the file into smaller parts\n` +
502
+ `2. Use the translate_text tool for smaller batches\n` +
503
+ `3. Contact support if this persists\n\n` +
504
+ `File size: ${content.length} characters`
505
+ }
506
+ ]
507
+ };
508
+ }
509
+
510
+ // Check if it's actually a service unavailable error
511
+ if (error.code === 'ECONNREFUSED' ||
512
+ error.code === 'ETIMEDOUT' ||
513
+ error.response?.status === 503 ||
514
+ error.response?.status === 502 ||
515
+ error.response?.status === 504) {
516
+ throw new Error(`Translation service unavailable: ${error.message}`);
517
+ }
518
+
519
+ // For other errors, throw them as-is without "unavailable" keyword
520
+ throw error;
521
+ }
522
+ }
523
+
524
+ // Format translation result for consistent output
525
+ function formatTranslationResult(parsedResult, texts, targetLanguage, sourceLanguage, targetAudience, industry, region) {
526
+ return {
527
+ translatedTexts: parsedResult?.translatedTexts || [],
528
+ content: [
529
+ {
530
+ type: 'text',
531
+ text: `Translation Results:\n\n` +
532
+ `🌍 ${parsedResult?.sourceLanguage || sourceLanguage || 'Auto-detected'} → ${parsedResult?.targetLanguage || targetLanguage}\n` +
533
+ `👥 Audience: ${parsedResult?.targetAudience || targetAudience}\n` +
534
+ `🏭 Industry: ${parsedResult?.industry || industry}\n` +
535
+ `${parsedResult?.region || region ? `📍 Region: ${parsedResult?.region || region}\n` : ''}` +
536
+ `⏱️ Processing Time: ${parsedResult?.processingTimeMs || 'N/A'}ms\n` +
537
+ `✅ Valid: ${parsedResult?.isValid !== undefined ? parsedResult.isValid : 'N/A'}\n\n` +
538
+ `📝 Translations:\n` +
539
+ (parsedResult?.translatedTexts || []).map((text, index) =>
540
+ `${index + 1}. "${(parsedResult?.originalTexts || texts)[index]}" → "${text}"`
541
+ ).join('\n'),
542
+ },
543
+ ],
544
+ };
545
+ }
546
+
547
+ // Poll for async translation job status
548
+ async function pollTranslationJob(jobId, estimatedTime) {
549
+ const maxPolls = 60; // Max 10 minutes of polling
550
+ const pollInterval = 10000; // Poll every 10 seconds
551
+
552
+ for (let i = 0; i < maxPolls; i++) {
553
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
554
+
555
+ try {
556
+ const statusRequest = {
557
+ jsonrpc: '2.0',
558
+ id: Date.now(),
559
+ method: 'tools/call',
560
+ params: {
561
+ name: 'check_translation_status',
562
+ arguments: { jobId }
563
+ }
564
+ };
565
+
566
+ const response = await axios.post(MCP_SERVER_URL, statusRequest, {
567
+ headers: { 'Content-Type': 'application/json' },
568
+ timeout: 10000
569
+ });
570
+
571
+ if (response.data.error) {
572
+ throw new Error(`Status check error: ${response.data.error.message || response.data.error}`);
573
+ }
574
+
575
+ const result = response.data.result;
576
+ if (result && result.content && result.content[0]) {
577
+ const status = JSON.parse(result.content[0].text);
578
+
579
+ if (status.status === 'completed') {
580
+ return status.result;
581
+ } else if (status.status === 'failed') {
582
+ throw new Error(`Translation failed: ${status.error}`);
583
+ }
584
+
585
+ // Still processing - continue polling
586
+ console.error(`Translation progress: ${status.progress}% (${status.message})`);
587
+ }
588
+ } catch (error) {
589
+ console.error(`Error polling job status: ${error.message}`);
590
+ // Continue polling even if status check fails
429
591
  }
430
- throw new Error(`Translation service unavailable: ${error.message}`);
431
592
  }
593
+
594
+ throw new Error(`Translation job ${jobId} timed out after ${maxPolls * pollInterval / 1000} seconds`);
432
595
  }
433
596
 
434
597
  async function handleGetCredits(args) {
435
598
  try {
436
- const response = await axios.post(`${MCP_SERVER_URL}/api/mcp`, {
437
- name: 'get_credits',
438
- arguments: {
439
- apiKey: API_KEY,
440
- }
441
- }, {
599
+ // Get team info first using the API key
600
+ const teamResponse = await axios.get(`https://platform.i18nagent.ai/api/teams/by-api-key/${API_KEY}`, {
442
601
  headers: {
443
602
  'Content-Type': 'application/json'
444
603
  },
445
604
  timeout: 10000
446
605
  });
447
606
 
448
- const result = response.data;
449
-
450
- if (result.isError) {
451
- throw new Error(result.content[0].text);
607
+ if (!teamResponse.data.success) {
608
+ throw new Error(teamResponse.data.error || 'Failed to get team information');
452
609
  }
453
610
 
454
- const creditsInfo = JSON.parse(result.content[0].text);
611
+ const teamInfo = teamResponse.data.data;
612
+ const approximateWordsAvailable = Math.floor(teamInfo.credits * 1000); // 0.001 credits per word
455
613
 
456
614
  return {
457
615
  content: [
@@ -459,11 +617,11 @@ async function handleGetCredits(args) {
459
617
  type: 'text',
460
618
  text: `💰 **Credits Information**
461
619
 
462
- 🏢 **Team**: ${creditsInfo.teamName}
463
- 💳 **Credits Remaining**: ${creditsInfo.creditsRemaining}
464
- 📝 **Approximate Words Available**: ${creditsInfo.approximateWordsAvailable.toLocaleString()}
465
- 💵 **Cost per Word**: ${creditsInfo.costPerWord} credits
466
- ⏰ **Last Updated**: ${new Date(creditsInfo.timestamp).toLocaleString()}
620
+ 🏢 **Team**: ${teamInfo.name}
621
+ 💳 **Credits Remaining**: ${teamInfo.credits}
622
+ 📝 **Approximate Words Available**: ${approximateWordsAvailable.toLocaleString()}
623
+ 💵 **Cost per Word**: 0.001 credits
624
+ ⏰ **Last Updated**: ${new Date().toLocaleString()}
467
625
 
468
626
  Note: Word count is approximate and may vary based on actual content complexity and translation requirements.`,
469
627
  },
@@ -808,7 +966,7 @@ async function main() {
808
966
  await server.connect(transport);
809
967
  console.error('i18n-agent MCP server running...');
810
968
  console.error('MCP_SERVER_URL:', MCP_SERVER_URL);
811
- console.error('API_KEY:', API_KEY ? 'Set ✓' : 'Not set ✗');
969
+ console.error('API_KEY:', API_KEY);
812
970
  }
813
971
 
814
972
  main().catch((error) => {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@i18n-agent/mcp-client",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "MCP client for i18n-agent translation service - supports Claude, Cursor, VS Code, and other AI IDEs",
5
5
  "main": "mcp-client.js",
6
6
  "bin": {
7
- "i18n-agent-install": "./install.js"
7
+ "i18n-agent-install": "install.js"
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
@@ -50,4 +50,4 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  }
53
- }
53
+ }