@i18n-agent/mcp-client 1.0.0 → 1.0.2

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/mcp-client.js ADDED
@@ -0,0 +1,808 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Client for i18n-agent Translation Service
5
+ * Integrates with Claude Code CLI to provide translation capabilities
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import {
11
+ CallToolRequestSchema,
12
+ ErrorCode,
13
+ ListToolsRequestSchema,
14
+ McpError,
15
+ } from '@modelcontextprotocol/sdk/types.js';
16
+ import axios from 'axios';
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+
20
+ const server = new Server(
21
+ {
22
+ name: 'i18n-agent',
23
+ version: '1.0.0',
24
+ },
25
+ {
26
+ capabilities: {
27
+ tools: {},
28
+ },
29
+ }
30
+ );
31
+
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);
42
+ }
43
+
44
+ // Available tools
45
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
46
+ return {
47
+ tools: [
48
+ {
49
+ name: 'translate_text',
50
+ description: 'Translate text from one language to another with cultural context',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ texts: {
55
+ type: 'array',
56
+ items: { type: 'string' },
57
+ description: 'Array of texts to translate',
58
+ },
59
+ targetLanguage: {
60
+ type: 'string',
61
+ description: 'Target language code (e.g., "es", "fr", "ja", "de") or full name (e.g., "Spanish", "French")',
62
+ },
63
+ sourceLanguage: {
64
+ type: 'string',
65
+ description: 'Source language code (optional, auto-detected if not provided)',
66
+ default: 'auto',
67
+ },
68
+ targetAudience: {
69
+ type: 'string',
70
+ description: 'Target audience (e.g., "general", "technical", "casual", "formal")',
71
+ default: 'general',
72
+ },
73
+ industry: {
74
+ type: 'string',
75
+ description: 'Industry context (e.g., "technology", "healthcare", "finance", "education")',
76
+ default: 'technology',
77
+ },
78
+ region: {
79
+ type: 'string',
80
+ description: 'Specific region for localization (e.g., "Spain", "Mexico", "Brazil")',
81
+ },
82
+ notes: {
83
+ type: 'string',
84
+ description: 'Optional additional context or instructions for the translation (e.g., "Keep technical terms in English", "Use formal tone")',
85
+ },
86
+ },
87
+ required: ['texts', 'targetLanguage'],
88
+ },
89
+ },
90
+ {
91
+ name: 'list_supported_languages',
92
+ description: 'Get list of supported languages with quality ratings',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ includeQuality: {
97
+ type: 'boolean',
98
+ description: 'Include quality ratings for each language',
99
+ default: true,
100
+ },
101
+ },
102
+ },
103
+ },
104
+ {
105
+ name: 'translate_file',
106
+ description: 'Translate file content while preserving structure and format. Supports JSON, YAML, XML, CSV, TXT, MD, and other text files',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ filePath: {
111
+ type: 'string',
112
+ description: 'Path to the file to translate (required if fileContent is not provided)',
113
+ },
114
+ fileContent: {
115
+ type: 'string',
116
+ description: 'File content as string (required if filePath is not provided)',
117
+ },
118
+ fileType: {
119
+ type: 'string',
120
+ description: 'File type: json, yaml, yml, xml, csv, txt, md, html, properties',
121
+ enum: ['json', 'yaml', 'yml', 'xml', 'csv', 'txt', 'md', 'html', 'properties', 'auto'],
122
+ default: 'auto',
123
+ },
124
+ targetLanguage: {
125
+ type: 'string',
126
+ description: 'Target language code or name',
127
+ },
128
+ targetAudience: {
129
+ type: 'string',
130
+ description: 'Target audience',
131
+ default: 'general',
132
+ },
133
+ industry: {
134
+ type: 'string',
135
+ description: 'Industry context',
136
+ default: 'technology',
137
+ },
138
+ preserveKeys: {
139
+ type: 'boolean',
140
+ description: 'Whether to preserve keys/structure (for structured files)',
141
+ default: true,
142
+ },
143
+ outputFormat: {
144
+ type: 'string',
145
+ description: 'Output format: same, json, yaml, txt',
146
+ default: 'same',
147
+ },
148
+ sourceLanguage: {
149
+ type: 'string',
150
+ description: 'Source language code (auto-detected if not provided)',
151
+ },
152
+ region: {
153
+ type: 'string',
154
+ description: 'Specific region for localization (e.g., "Spain", "Mexico", "Brazil")',
155
+ },
156
+ notes: {
157
+ type: 'string',
158
+ description: 'Optional additional context or instructions for the translation (e.g., "Keep technical terms in English", "Use formal tone")',
159
+ },
160
+ },
161
+ required: ['targetLanguage'],
162
+ },
163
+ },
164
+ {
165
+ name: 'get_credits',
166
+ description: 'Get remaining credits for the user and approximate word count available at 0.001 credits per word',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {},
170
+ required: [],
171
+ },
172
+ },
173
+ ],
174
+ };
175
+ });
176
+
177
+ // Tool execution handler
178
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
179
+ const { name, arguments: args } = request.params;
180
+
181
+ try {
182
+ switch (name) {
183
+ case 'translate_text':
184
+ return await handleTranslateText(args);
185
+
186
+ case 'list_supported_languages':
187
+ return await handleListLanguages(args);
188
+
189
+ case 'translate_file':
190
+ return await handleTranslateFile(args);
191
+
192
+ case 'get_credits':
193
+ return await handleGetCredits(args);
194
+
195
+ default:
196
+ throw new McpError(
197
+ ErrorCode.MethodNotFound,
198
+ `Unknown tool: ${name}`
199
+ );
200
+ }
201
+ } catch (error) {
202
+ console.error(`Error executing tool ${name}:`, error);
203
+ throw new McpError(
204
+ ErrorCode.InternalError,
205
+ `Tool execution failed: ${error.message}`
206
+ );
207
+ }
208
+ });
209
+
210
+ async function handleTranslateText(args) {
211
+ const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region } = args;
212
+
213
+ if (!texts || !Array.isArray(texts) || texts.length === 0) {
214
+ throw new Error('texts must be a non-empty array');
215
+ }
216
+
217
+ if (!targetLanguage) {
218
+ throw new Error('targetLanguage is required');
219
+ }
220
+
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,
229
+ };
230
+
231
+ try {
232
+ const response = await axios.post(`${MCP_SERVER_URL}/translate`, requestData, {
233
+ headers: {
234
+ 'Content-Type': 'application/json',
235
+ },
236
+ timeout: 60000, // 60 second timeout
237
+ });
238
+
239
+ if (response.data.error) {
240
+ throw new Error(`Translation service error: ${response.data.error}`);
241
+ }
242
+
243
+ // Direct API response format: { translatedTexts: [...], ... }
244
+ const parsedResult = response.data;
245
+
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
+ };
265
+ } catch (error) {
266
+ if (error.code === 'ECONNABORTED') {
267
+ throw new Error('Translation request timed out. The service may be processing a large request.');
268
+ }
269
+ throw new Error(`Translation service unavailable: ${error.message}`);
270
+ }
271
+ }
272
+
273
+ async function handleListLanguages(args) {
274
+ const { includeQuality = true } = args;
275
+
276
+ // Language support matrix based on GPT-OSS analysis
277
+ const languages = {
278
+ 'Tier 1 - Production Ready (Excellent Quality 80-90%)': {
279
+ 'en': 'English',
280
+ 'es': 'Spanish',
281
+ 'fr': 'French',
282
+ 'de': 'German',
283
+ 'it': 'Italian',
284
+ 'pt': 'Portuguese',
285
+ 'nl': 'Dutch',
286
+ },
287
+ 'Tier 2 - Production Viable (Good Quality 50-75%)': {
288
+ 'ru': 'Russian',
289
+ 'zh-CN': 'Chinese (Simplified)',
290
+ 'ja': 'Japanese',
291
+ 'ko': 'Korean',
292
+ 'ar': 'Arabic',
293
+ 'he': 'Hebrew',
294
+ 'hi': 'Hindi',
295
+ 'pl': 'Polish',
296
+ 'cs': 'Czech',
297
+ },
298
+ 'Tier 3 - Basic Support (Use with Caution 20-50%)': {
299
+ 'zh-TW': 'Chinese (Traditional)',
300
+ 'th': 'Thai',
301
+ 'vi': 'Vietnamese',
302
+ 'sv': 'Swedish',
303
+ 'da': 'Danish',
304
+ 'no': 'Norwegian',
305
+ 'fi': 'Finnish',
306
+ 'tr': 'Turkish',
307
+ 'hu': 'Hungarian',
308
+ },
309
+ };
310
+
311
+ let content = '🌍 Supported Languages\n';
312
+ content += '===================\n\n';
313
+
314
+ if (includeQuality) {
315
+ for (const [tier, langs] of Object.entries(languages)) {
316
+ content += `## ${tier}\n`;
317
+ for (const [code, name] of Object.entries(langs)) {
318
+ content += `- \`${code}\`: ${name}\n`;
319
+ }
320
+ content += '\n';
321
+ }
322
+ } else {
323
+ const allLanguages = Object.values(languages).reduce((acc, tier) => ({ ...acc, ...tier }), {});
324
+ for (const [code, name] of Object.entries(allLanguages)) {
325
+ content += `- \`${code}\`: ${name}\n`;
326
+ }
327
+ }
328
+
329
+ content += '\n💡 Usage Tips:\n';
330
+ content += '- Use language codes (e.g., "es") or full names (e.g., "Spanish")\n';
331
+ content += '- Tier 1 languages are recommended for production use\n';
332
+ content += '- Tier 2 languages work well with human review\n';
333
+ content += '- Tier 3 languages provide basic translation quality\n';
334
+
335
+ return {
336
+ content: [
337
+ {
338
+ type: 'text',
339
+ text: content,
340
+ },
341
+ ],
342
+ };
343
+ }
344
+
345
+ async function handleTranslateFile(args) {
346
+ const {
347
+ filePath,
348
+ fileContent,
349
+ fileType = 'auto',
350
+ targetLanguage,
351
+ targetAudience = 'general',
352
+ industry = 'technology',
353
+ preserveKeys = true,
354
+ outputFormat = 'same',
355
+ sourceLanguage,
356
+ region
357
+ } = args;
358
+
359
+ if (!filePath && !fileContent) {
360
+ throw new Error('Either filePath or fileContent must be provided');
361
+ }
362
+
363
+ if (!targetLanguage) {
364
+ throw new Error('targetLanguage is required');
365
+ }
366
+
367
+ // Read file content if path provided and no content given
368
+ let content = fileContent;
369
+
370
+ if (filePath && !fileContent) {
371
+ try {
372
+ content = fs.readFileSync(filePath, 'utf8');
373
+ } catch (error) {
374
+ throw new Error(`Failed to read file: ${error.message}`);
375
+ }
376
+ }
377
+
378
+ // Use MCP JSON-RPC protocol for translate_file
379
+ const mcpRequest = {
380
+ jsonrpc: '2.0',
381
+ id: Date.now(),
382
+ method: 'tools/call',
383
+ params: {
384
+ name: 'translate_file',
385
+ arguments: {
386
+ apiKey: API_KEY,
387
+ filePath,
388
+ fileContent: content,
389
+ fileType,
390
+ targetLanguage,
391
+ sourceLanguage,
392
+ targetAudience,
393
+ industry,
394
+ region,
395
+ preserveKeys,
396
+ outputFormat
397
+ }
398
+ }
399
+ };
400
+
401
+ try {
402
+ const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
403
+ headers: {
404
+ 'Content-Type': 'application/json',
405
+ },
406
+ timeout: 60000,
407
+ });
408
+
409
+ if (response.data.error) {
410
+ throw new Error(`Translation service error: ${response.data.error.message || response.data.error}`);
411
+ }
412
+
413
+ // MCP response format
414
+ const result = response.data.result;
415
+ return result;
416
+
417
+ } catch (error) {
418
+ if (error.code === 'ECONNABORTED') {
419
+ throw new Error('Translation request timed out. The service may be processing a large request.');
420
+ }
421
+ throw new Error(`Translation service unavailable: ${error.message}`);
422
+ }
423
+ }
424
+
425
+ async function handleGetCredits(args) {
426
+ try {
427
+ const response = await axios.post(`${MCP_SERVER_URL}/api/mcp`, {
428
+ name: 'get_credits',
429
+ arguments: {
430
+ apiKey: API_KEY,
431
+ }
432
+ }, {
433
+ headers: {
434
+ 'Content-Type': 'application/json'
435
+ },
436
+ timeout: 10000
437
+ });
438
+
439
+ const result = response.data;
440
+
441
+ if (result.isError) {
442
+ throw new Error(result.content[0].text);
443
+ }
444
+
445
+ const creditsInfo = JSON.parse(result.content[0].text);
446
+
447
+ return {
448
+ content: [
449
+ {
450
+ type: 'text',
451
+ text: `💰 **Credits Information**
452
+
453
+ 🏢 **Team**: ${creditsInfo.teamName}
454
+ 💳 **Credits Remaining**: ${creditsInfo.creditsRemaining}
455
+ 📝 **Approximate Words Available**: ${creditsInfo.approximateWordsAvailable.toLocaleString()}
456
+ 💵 **Cost per Word**: ${creditsInfo.costPerWord} credits
457
+ ⏰ **Last Updated**: ${new Date(creditsInfo.timestamp).toLocaleString()}
458
+
459
+ Note: Word count is approximate and may vary based on actual content complexity and translation requirements.`,
460
+ },
461
+ ],
462
+ };
463
+ } catch (error) {
464
+ console.error('Credits check error:', error);
465
+ throw new Error(`Unable to check credits: ${error.message}`);
466
+ }
467
+ }
468
+
469
+ function detectFileType(filePath, content) {
470
+ const ext = path.extname(filePath).toLowerCase();
471
+
472
+ switch (ext) {
473
+ case '.json': return 'json';
474
+ case '.yaml': case '.yml': return 'yaml';
475
+ case '.xml': case '.svg': return 'xml';
476
+ case '.csv': return 'csv';
477
+ case '.md': return 'md';
478
+ case '.html': case '.htm': return 'html';
479
+ case '.properties': return 'properties';
480
+ case '.txt': return 'txt';
481
+ default: return detectFileTypeFromContent(content);
482
+ }
483
+ }
484
+
485
+ function detectFileTypeFromContent(content) {
486
+ const trimmed = content.trim();
487
+
488
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
489
+ try { JSON.parse(trimmed); return 'json'; } catch {}
490
+ }
491
+
492
+ if (trimmed.match(/^---\s*$|^\s*\w+:\s*[|\->\s]|^\s*\w+:\s*.+$/m)) {
493
+ return 'yaml';
494
+ }
495
+
496
+ if (trimmed.startsWith('<') && trimmed.includes('>')) {
497
+ return 'xml';
498
+ }
499
+
500
+ if (trimmed.includes(',') && trimmed.split('\n').length > 1) {
501
+ return 'csv';
502
+ }
503
+
504
+ if (trimmed.includes('#') || trimmed.includes('**') || trimmed.includes('`')) {
505
+ return 'md';
506
+ }
507
+
508
+ if (trimmed.includes('=') && trimmed.split('\n').some(line => line.includes('='))) {
509
+ return 'properties';
510
+ }
511
+
512
+ return 'txt';
513
+ }
514
+
515
+ async function extractTextsFromFile(content, fileType, preserveKeys) {
516
+ const texts = [];
517
+ let structure = {};
518
+
519
+ switch (fileType) {
520
+ case 'json':
521
+ const jsonData = JSON.parse(content);
522
+ structure = { type: 'json', keys: [] };
523
+ extractFromJson(jsonData, texts, structure.keys);
524
+ break;
525
+
526
+ case 'yaml':
527
+ case 'yml':
528
+ // Simple YAML parsing - extract values after colons
529
+ structure = { type: 'yaml', lines: content.split('\n') };
530
+ const yamlLines = content.split('\n');
531
+ yamlLines.forEach((line, index) => {
532
+ const match = line.match(/^(\s*)([^:]+):\s*(.+)$/);
533
+ if (match && match[3] && !match[3].match(/^[|\->]/)) {
534
+ const value = match[3].replace(/^["']|["']$/g, '');
535
+ if (value && !isNumericOrBoolean(value)) {
536
+ texts.push(value);
537
+ structure.lines[index] = { original: line, textIndex: texts.length - 1 };
538
+ }
539
+ }
540
+ });
541
+ break;
542
+
543
+ case 'xml':
544
+ case 'html':
545
+ structure = { type: fileType, content: content };
546
+ // Extract text content between tags
547
+ const xmlMatches = content.matchAll(/>([^<]+)</g);
548
+ for (const match of xmlMatches) {
549
+ const text = match[1].trim();
550
+ if (text && !isNumericOrBoolean(text)) {
551
+ texts.push(text);
552
+ }
553
+ }
554
+ break;
555
+
556
+ case 'csv':
557
+ structure = { type: 'csv', rows: [] };
558
+ const csvLines = content.split('\n');
559
+ csvLines.forEach((line, rowIndex) => {
560
+ if (line.trim()) {
561
+ const cells = parseCsvLine(line);
562
+ structure.rows[rowIndex] = [];
563
+ cells.forEach((cell, colIndex) => {
564
+ if (cell && !isNumericOrBoolean(cell)) {
565
+ texts.push(cell);
566
+ structure.rows[rowIndex][colIndex] = texts.length - 1;
567
+ } else {
568
+ structure.rows[rowIndex][colIndex] = null;
569
+ }
570
+ });
571
+ }
572
+ });
573
+ break;
574
+
575
+ case 'properties':
576
+ structure = { type: 'properties', lines: content.split('\n') };
577
+ const propLines = content.split('\n');
578
+ propLines.forEach((line, index) => {
579
+ const match = line.match(/^([^=]+)=(.*)$/);
580
+ if (match && match[2]) {
581
+ texts.push(match[2]);
582
+ structure.lines[index] = { key: match[1], textIndex: texts.length - 1 };
583
+ }
584
+ });
585
+ break;
586
+
587
+ case 'md':
588
+ structure = { type: 'md', content: content };
589
+ // Extract text content, avoiding code blocks
590
+ const mdText = content
591
+ .replace(/```[\s\S]*?```/g, '') // Remove code blocks
592
+ .replace(/`[^`]+`/g, '') // Remove inline code
593
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Extract link text
594
+ .replace(/[#*_`]/g, '') // Remove markdown formatting
595
+ .split('\n')
596
+ .filter(line => line.trim())
597
+ .join(' ');
598
+
599
+ if (mdText.trim()) {
600
+ texts.push(mdText.trim());
601
+ }
602
+ break;
603
+
604
+ default: // txt and others
605
+ structure = { type: 'txt', content: content };
606
+ const cleanText = content.trim();
607
+ if (cleanText) {
608
+ texts.push(cleanText);
609
+ }
610
+ break;
611
+ }
612
+
613
+ return { texts, structure };
614
+ }
615
+
616
+ function extractFromJson(obj, texts, keys, prefix = '') {
617
+ for (const [key, value] of Object.entries(obj)) {
618
+ const fullKey = prefix ? `${prefix}.${key}` : key;
619
+ if (typeof value === 'string' && value.trim()) {
620
+ texts.push(value);
621
+ keys.push(fullKey);
622
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
623
+ extractFromJson(value, texts, keys, fullKey);
624
+ } else if (Array.isArray(value)) {
625
+ value.forEach((item, index) => {
626
+ if (typeof item === 'string' && item.trim()) {
627
+ texts.push(item);
628
+ keys.push(`${fullKey}[${index}]`);
629
+ } else if (typeof item === 'object' && item !== null) {
630
+ extractFromJson(item, texts, keys, `${fullKey}[${index}]`);
631
+ }
632
+ });
633
+ }
634
+ }
635
+ }
636
+
637
+ function extractTranslatedTexts(translationResult) {
638
+ const translatedTexts = [];
639
+ const lines = translationResult.split('\n');
640
+ const translationSection = lines.findIndex(line => line.includes('📝 Translations:'));
641
+
642
+ if (translationSection !== -1) {
643
+ for (let i = translationSection + 1; i < lines.length; i++) {
644
+ const match = lines[i].match(/\d+\. ".*?" → "(.*)"/);
645
+ if (match) {
646
+ translatedTexts.push(match[1]);
647
+ }
648
+ }
649
+ }
650
+
651
+ return translatedTexts;
652
+ }
653
+
654
+ async function reconstructFile(originalContent, translatedTexts, structure, fileType, outputFormat) {
655
+ const format = outputFormat === 'same' ? fileType : outputFormat;
656
+ let textIndex = 0;
657
+
658
+ switch (format) {
659
+ case 'json':
660
+ if (structure.type === 'json') {
661
+ const jsonData = JSON.parse(originalContent);
662
+ replaceJsonStrings(jsonData, translatedTexts, textIndex);
663
+ return JSON.stringify(jsonData, null, 2);
664
+ } else {
665
+ // Convert other formats to JSON
666
+ const jsonObj = {};
667
+ translatedTexts.forEach((text, i) => {
668
+ jsonObj[`text_${i + 1}`] = text;
669
+ });
670
+ return JSON.stringify(jsonObj, null, 2);
671
+ }
672
+
673
+ case 'yaml':
674
+ if (structure.type === 'yaml') {
675
+ const lines = [...structure.lines];
676
+ lines.forEach((lineInfo, index) => {
677
+ if (typeof lineInfo === 'object' && lineInfo.textIndex !== undefined) {
678
+ const match = lineInfo.original.match(/^(\s*)([^:]+):\s*(.+)$/);
679
+ if (match) {
680
+ lines[index] = `${match[1]}${match[2]}: "${translatedTexts[lineInfo.textIndex]}"`;
681
+ }
682
+ } else if (typeof lineInfo === 'string') {
683
+ lines[index] = lineInfo;
684
+ }
685
+ });
686
+ return lines.join('\n');
687
+ }
688
+ break;
689
+
690
+ case 'csv':
691
+ if (structure.type === 'csv') {
692
+ return structure.rows.map(row => {
693
+ return row.map(cellIndex => {
694
+ return cellIndex !== null ? `"${translatedTexts[cellIndex]}"` : '';
695
+ }).join(',');
696
+ }).join('\n');
697
+ }
698
+ break;
699
+
700
+ case 'properties':
701
+ if (structure.type === 'properties') {
702
+ const lines = structure.lines.map(lineInfo => {
703
+ if (typeof lineInfo === 'object' && lineInfo.textIndex !== undefined) {
704
+ return `${lineInfo.key}=${translatedTexts[lineInfo.textIndex]}`;
705
+ }
706
+ return lineInfo;
707
+ });
708
+ return lines.join('\n');
709
+ }
710
+ break;
711
+
712
+ case 'txt':
713
+ return translatedTexts.join('\n\n');
714
+
715
+ default:
716
+ return translatedTexts.join('\n');
717
+ }
718
+
719
+ // Fallback for unsupported combinations
720
+ return translatedTexts.join('\n');
721
+ }
722
+
723
+ function replaceJsonStrings(obj, translatedTexts, startIndex = 0) {
724
+ let currentIndex = startIndex;
725
+
726
+ for (const [key, value] of Object.entries(obj)) {
727
+ if (typeof value === 'string' && value.trim()) {
728
+ if (currentIndex < translatedTexts.length) {
729
+ obj[key] = translatedTexts[currentIndex];
730
+ currentIndex++;
731
+ }
732
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
733
+ currentIndex = replaceJsonStrings(value, translatedTexts, currentIndex);
734
+ } else if (Array.isArray(value)) {
735
+ value.forEach((item, index) => {
736
+ if (typeof item === 'string' && item.trim()) {
737
+ if (currentIndex < translatedTexts.length) {
738
+ value[index] = translatedTexts[currentIndex];
739
+ currentIndex++;
740
+ }
741
+ } else if (typeof item === 'object' && item !== null) {
742
+ currentIndex = replaceJsonStrings(item, translatedTexts, currentIndex);
743
+ }
744
+ });
745
+ }
746
+ }
747
+
748
+ return currentIndex;
749
+ }
750
+
751
+ function parseCsvLine(line) {
752
+ const result = [];
753
+ let current = '';
754
+ let inQuotes = false;
755
+
756
+ for (let i = 0; i < line.length; i++) {
757
+ const char = line[i];
758
+ if (char === '"') {
759
+ inQuotes = !inQuotes;
760
+ } else if (char === ',' && !inQuotes) {
761
+ result.push(current.trim().replace(/^"|"$/g, ''));
762
+ current = '';
763
+ } else {
764
+ current += char;
765
+ }
766
+ }
767
+
768
+ result.push(current.trim().replace(/^"|"$/g, ''));
769
+ return result;
770
+ }
771
+
772
+ function isNumericOrBoolean(value) {
773
+ return /^\d+$/.test(value) ||
774
+ /^\d+\.\d+$/.test(value) ||
775
+ value === 'true' ||
776
+ value === 'false' ||
777
+ value === 'null' ||
778
+ value === 'undefined';
779
+ }
780
+
781
+ function getCodeBlockLanguage(fileType) {
782
+ const languageMap = {
783
+ 'json': 'json',
784
+ 'yaml': 'yaml',
785
+ 'yml': 'yaml',
786
+ 'xml': 'xml',
787
+ 'html': 'html',
788
+ 'csv': 'csv',
789
+ 'md': 'markdown',
790
+ 'properties': 'properties',
791
+ 'txt': 'text'
792
+ };
793
+ return languageMap[fileType] || 'text';
794
+ }
795
+
796
+ // Start the server
797
+ async function main() {
798
+ const transport = new StdioServerTransport();
799
+ await server.connect(transport);
800
+ console.error('i18n-agent MCP server running...');
801
+ console.error('MCP_SERVER_URL:', MCP_SERVER_URL);
802
+ console.error('API_KEY:', API_KEY ? 'Set ✓' : 'Not set ✗');
803
+ }
804
+
805
+ main().catch((error) => {
806
+ console.error('Failed to start server:', error);
807
+ process.exit(1);
808
+ });