@i18n-agent/mcp-client 1.1.2 ā 1.4.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 +111 -7
- package/install.js +69 -10
- package/mcp-client.js +465 -146
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,6 +69,11 @@ Check my translation credits
|
|
|
69
69
|
List supported languages with quality ratings
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
### Content Analysis
|
|
73
|
+
```
|
|
74
|
+
Analyze content for translation readiness and get improvement suggestions
|
|
75
|
+
```
|
|
76
|
+
|
|
72
77
|
## š Supported AI IDEs
|
|
73
78
|
|
|
74
79
|
| IDE | Status | Config Location |
|
|
@@ -79,8 +84,6 @@ List supported languages with quality ratings
|
|
|
79
84
|
| **Other MCP IDEs** | š§ Manual setup | Varies |
|
|
80
85
|
|
|
81
86
|
## š Language Support
|
|
82
|
-
|
|
83
|
-
### Tier 1 - Excellent Quality
|
|
84
87
|
- **en**: English
|
|
85
88
|
- **fr**: French
|
|
86
89
|
- **de**: German
|
|
@@ -91,8 +94,6 @@ List supported languages with quality ratings
|
|
|
91
94
|
- **ja**: Japanese
|
|
92
95
|
- **ko**: Korean
|
|
93
96
|
- **zh-CN**: Chinese (Simplified)
|
|
94
|
-
|
|
95
|
-
### Tier 2 - High Quality
|
|
96
97
|
- **nl**: Dutch
|
|
97
98
|
- **pl**: Polish
|
|
98
99
|
- **cs**: Czech
|
|
@@ -104,8 +105,6 @@ List supported languages with quality ratings
|
|
|
104
105
|
- **da**: Danish
|
|
105
106
|
- **no**: Norwegian
|
|
106
107
|
- **fi**: Finnish
|
|
107
|
-
|
|
108
|
-
### Tier 3 - Good Quality
|
|
109
108
|
- **tr**: Turkish
|
|
110
109
|
- **hu**: Hungarian
|
|
111
110
|
- **th**: Thai
|
|
@@ -216,6 +215,50 @@ ls ~/.cursor/
|
|
|
216
215
|
ls ~/.vscode/
|
|
217
216
|
```
|
|
218
217
|
|
|
218
|
+
### MCP Connection Issues
|
|
219
|
+
|
|
220
|
+
**"Failed" status in Claude Code:**
|
|
221
|
+
|
|
222
|
+
This usually happens with Node Version Managers (nvm, fnm, n). The installer now automatically detects nvm and creates a wrapper script. If you still have issues:
|
|
223
|
+
|
|
224
|
+
1. **Check your Node installation:**
|
|
225
|
+
```bash
|
|
226
|
+
which node
|
|
227
|
+
# If output contains .nvm, you're using nvm
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
2. **Manual wrapper script (if auto-detection fails):**
|
|
231
|
+
Create `~/.claude/run-mcp.sh`:
|
|
232
|
+
```bash
|
|
233
|
+
#!/bin/bash
|
|
234
|
+
export PATH="$(dirname $(which node)):$PATH"
|
|
235
|
+
cd ~/.claude
|
|
236
|
+
exec node node_modules/@i18n-agent/mcp-client/mcp-client.js
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Make it executable:
|
|
240
|
+
```bash
|
|
241
|
+
chmod +x ~/.claude/run-mcp.sh
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
3. **Update Claude configuration:**
|
|
245
|
+
Edit `~/.claude.json`:
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"mcpServers": {
|
|
249
|
+
"i18n-agent": {
|
|
250
|
+
"command": "/Users/YOUR_USERNAME/.claude/run-mcp.sh",
|
|
251
|
+
"env": {
|
|
252
|
+
"MCP_SERVER_URL": "https://mcp.i18nagent.ai",
|
|
253
|
+
"API_KEY": "your-api-key"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
4. **Restart Claude Code completely** (not just close window, quit the app)
|
|
261
|
+
|
|
219
262
|
### Runtime Issues
|
|
220
263
|
|
|
221
264
|
**API Key not found:**
|
|
@@ -277,10 +320,71 @@ Copyright (c) 2025 FatCouple OĆ
|
|
|
277
320
|
|
|
278
321
|
## š Support
|
|
279
322
|
|
|
280
|
-
- **Discord**: [Join our community](https://discord.gg/i18nagent)
|
|
281
323
|
- **Email**: support@i18nagent.ai
|
|
282
324
|
- **Documentation**: [docs.i18nagent.ai](https://docs.i18nagent.ai)
|
|
283
325
|
|
|
326
|
+
## š§ Available MCP Tools
|
|
327
|
+
|
|
328
|
+
### translate_text
|
|
329
|
+
Translate text content with cultural adaptation and context awareness.
|
|
330
|
+
|
|
331
|
+
**Parameters:**
|
|
332
|
+
- `texts` (array): Array of strings to translate
|
|
333
|
+
- `targetLanguage` (string): Target language code
|
|
334
|
+
- `targetAudience` (string): Target audience context
|
|
335
|
+
- `industry` (string): Industry context
|
|
336
|
+
- `sourceLanguage` (string, optional): Source language (auto-detected if not provided)
|
|
337
|
+
- `region` (string, optional): Specific region for localization
|
|
338
|
+
|
|
339
|
+
### translate_file
|
|
340
|
+
Translate files while preserving structure and format.
|
|
341
|
+
|
|
342
|
+
**Parameters:**
|
|
343
|
+
- `filePath` or `fileContent` (string): File path or content to translate
|
|
344
|
+
- `fileType` (string): File format (json, yaml, xml, csv, txt, md, etc.)
|
|
345
|
+
- `targetLanguage` (string): Target language code
|
|
346
|
+
- `preserveKeys` (boolean): Whether to preserve object keys/structure
|
|
347
|
+
- `outputFormat` (string): Output format (same, json, yaml, txt)
|
|
348
|
+
|
|
349
|
+
### analyze_content
|
|
350
|
+
Analyze content for translation readiness and get improvement suggestions before translation.
|
|
351
|
+
|
|
352
|
+
**Parameters:**
|
|
353
|
+
- `content` (string/array/object): Content to analyze
|
|
354
|
+
- `targetLanguage` (string): Target language for translation
|
|
355
|
+
- `fileType` (string, optional): File type if content is from a file
|
|
356
|
+
- `sourceLanguage` (string, optional): Source language (auto-detected)
|
|
357
|
+
- `industry` (string): Industry context
|
|
358
|
+
- `targetAudience` (string): Target audience
|
|
359
|
+
- `region` (string, optional): Specific region for localization
|
|
360
|
+
|
|
361
|
+
**Returns:**
|
|
362
|
+
- Source language detection with confidence score
|
|
363
|
+
- Content type and tone analysis
|
|
364
|
+
- Translation readiness score (0-100)
|
|
365
|
+
- Specific improvement suggestions
|
|
366
|
+
- Quality metrics and issues
|
|
367
|
+
- Warnings for potential problems
|
|
368
|
+
- Estimated credits required
|
|
369
|
+
|
|
370
|
+
### list_supported_languages
|
|
371
|
+
Get list of all supported languages with quality ratings.
|
|
372
|
+
|
|
373
|
+
**Parameters:**
|
|
374
|
+
- `includeQuality` (boolean): Include quality ratings (default: true)
|
|
375
|
+
|
|
376
|
+
### get_credits
|
|
377
|
+
Check remaining translation credits and word count estimates.
|
|
378
|
+
|
|
379
|
+
**Parameters:**
|
|
380
|
+
- `apiKey` (string): Your API key
|
|
381
|
+
|
|
382
|
+
### check_translation_status
|
|
383
|
+
Check status of async translation jobs (for large files).
|
|
384
|
+
|
|
385
|
+
**Parameters:**
|
|
386
|
+
- `jobId` (string): Job ID from async translation
|
|
387
|
+
|
|
284
388
|
---
|
|
285
389
|
|
|
286
390
|
Made with ā¤ļø by [FatCouple OĆ](https://fireinbelly.com)
|
package/install.js
CHANGED
|
@@ -74,6 +74,45 @@ function createMCPConfig() {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function detectNodeEnvironment() {
|
|
78
|
+
// Check if using nvm or other version managers
|
|
79
|
+
const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm');
|
|
80
|
+
const nodeVersion = process.version;
|
|
81
|
+
const nodePath = process.execPath;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
isNvm: nodePath.includes('.nvm') || nodePath.includes('nvm'),
|
|
85
|
+
nodePath,
|
|
86
|
+
nodeVersion
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createWrapperScript(targetDir) {
|
|
91
|
+
const nodeEnv = detectNodeEnvironment();
|
|
92
|
+
const wrapperPath = path.join(targetDir, 'run-mcp.sh');
|
|
93
|
+
const mcpClientPath = path.join(targetDir, 'node_modules', '@i18n-agent', 'mcp-client', 'mcp-client.js');
|
|
94
|
+
|
|
95
|
+
let wrapperContent;
|
|
96
|
+
|
|
97
|
+
if (nodeEnv.isNvm) {
|
|
98
|
+
// For nvm users, we need to set up the PATH properly
|
|
99
|
+
wrapperContent = `#!/bin/bash
|
|
100
|
+
# Wrapper script for i18n-agent MCP client (handles nvm environments)
|
|
101
|
+
export PATH="${path.dirname(nodeEnv.nodePath)}:$PATH"
|
|
102
|
+
cd "${targetDir}"
|
|
103
|
+
exec node "${mcpClientPath}"`;
|
|
104
|
+
} else {
|
|
105
|
+
// For system node installations
|
|
106
|
+
wrapperContent = `#!/bin/bash
|
|
107
|
+
# Wrapper script for i18n-agent MCP client
|
|
108
|
+
cd "${targetDir}"
|
|
109
|
+
exec node "${mcpClientPath}"`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(wrapperPath, wrapperContent, { mode: 0o755 });
|
|
113
|
+
return wrapperPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
function updateClaudeConfig(configPath) {
|
|
78
117
|
let config = {};
|
|
79
118
|
|
|
@@ -92,16 +131,36 @@ function updateClaudeConfig(configPath) {
|
|
|
92
131
|
config.mcpServers = {};
|
|
93
132
|
}
|
|
94
133
|
|
|
95
|
-
//
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
// Detect if we need a wrapper script (for nvm users)
|
|
135
|
+
const nodeEnv = detectNodeEnvironment();
|
|
136
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
137
|
+
|
|
138
|
+
if (nodeEnv.isNvm) {
|
|
139
|
+
// Create wrapper script for nvm users
|
|
140
|
+
console.log(' š§ Detected nvm environment, creating wrapper script...');
|
|
141
|
+
const wrapperPath = createWrapperScript(claudeDir);
|
|
142
|
+
|
|
143
|
+
config.mcpServers["i18n-agent"] = {
|
|
144
|
+
command: wrapperPath,
|
|
145
|
+
env: {
|
|
146
|
+
MCP_SERVER_URL: "https://mcp.i18nagent.ai",
|
|
147
|
+
API_KEY: ""
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
} else {
|
|
151
|
+
// Standard configuration for system node
|
|
152
|
+
const mcpClientPath = path.join(claudeDir, 'node_modules', '@i18n-agent', 'mcp-client', 'mcp-client.js');
|
|
153
|
+
|
|
154
|
+
config.mcpServers["i18n-agent"] = {
|
|
155
|
+
command: "node",
|
|
156
|
+
args: [mcpClientPath],
|
|
157
|
+
cwd: claudeDir,
|
|
158
|
+
env: {
|
|
159
|
+
MCP_SERVER_URL: "https://mcp.i18nagent.ai",
|
|
160
|
+
API_KEY: ""
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
105
164
|
|
|
106
165
|
// Write updated config
|
|
107
166
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
package/mcp-client.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Integrates with Claude Code CLI to provide translation capabilities
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const MCP_CLIENT_VERSION = '1.3.0';
|
|
9
|
+
|
|
8
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
12
|
import {
|
|
@@ -17,8 +19,6 @@ import axios from 'axios';
|
|
|
17
19
|
import fs from 'fs';
|
|
18
20
|
import path from 'path';
|
|
19
21
|
|
|
20
|
-
const MCP_CLIENT_VERSION = '1.1.2';
|
|
21
|
-
|
|
22
22
|
const server = new Server(
|
|
23
23
|
{
|
|
24
24
|
name: 'i18n-agent',
|
|
@@ -47,44 +47,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
47
47
|
tools: [
|
|
48
48
|
{
|
|
49
49
|
name: 'translate_text',
|
|
50
|
-
description: 'Translate text
|
|
50
|
+
description: 'Translate text content with cultural adaptation using AI subagents. For large requests (>100 texts or >50,000 characters), returns a jobId for async processing. Use check_translation_status to monitor progress and download results.',
|
|
51
51
|
inputSchema: {
|
|
52
52
|
type: 'object',
|
|
53
53
|
properties: {
|
|
54
54
|
texts: {
|
|
55
55
|
type: 'array',
|
|
56
56
|
items: { type: 'string' },
|
|
57
|
-
description: 'Array of texts to translate',
|
|
57
|
+
description: 'Array of source texts to translate (any language)',
|
|
58
58
|
},
|
|
59
59
|
targetLanguage: {
|
|
60
60
|
type: 'string',
|
|
61
|
-
description: 'Target language code (e.g., "es", "fr", "
|
|
61
|
+
description: 'Target language code (e.g., "es", "fr", "zh-CN")',
|
|
62
62
|
},
|
|
63
63
|
sourceLanguage: {
|
|
64
64
|
type: 'string',
|
|
65
|
-
description: 'Source language code (
|
|
66
|
-
default: 'auto',
|
|
65
|
+
description: 'Source language code (auto-detected if not provided)',
|
|
67
66
|
},
|
|
68
67
|
targetAudience: {
|
|
69
68
|
type: 'string',
|
|
70
|
-
description: 'Target audience (e.g., "
|
|
71
|
-
default: 'general',
|
|
69
|
+
description: 'Target audience for the content (e.g., "software developers", "marketing professionals")',
|
|
72
70
|
},
|
|
73
71
|
industry: {
|
|
74
72
|
type: 'string',
|
|
75
|
-
description: 'Industry context (e.g., "technology", "healthcare", "finance"
|
|
76
|
-
default: 'technology',
|
|
73
|
+
description: 'Industry context (e.g., "technology", "healthcare", "finance")',
|
|
77
74
|
},
|
|
78
75
|
region: {
|
|
79
76
|
type: 'string',
|
|
80
77
|
description: 'Specific region for localization (e.g., "Spain", "Mexico", "Brazil")',
|
|
81
78
|
},
|
|
82
|
-
|
|
79
|
+
context: {
|
|
83
80
|
type: 'string',
|
|
84
81
|
description: 'Optional additional context or instructions for the translation (e.g., "Keep technical terms in English", "Use formal tone")',
|
|
85
82
|
},
|
|
86
83
|
},
|
|
87
|
-
required: ['texts', 'targetLanguage'],
|
|
84
|
+
required: ['texts', 'targetLanguage', 'targetAudience', 'industry'],
|
|
88
85
|
},
|
|
89
86
|
},
|
|
90
87
|
{
|
|
@@ -103,7 +100,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
103
100
|
},
|
|
104
101
|
{
|
|
105
102
|
name: 'translate_file',
|
|
106
|
-
description: 'Translate file content while preserving structure and format. Supports JSON, YAML, XML, CSV, TXT, MD, and other text files',
|
|
103
|
+
description: 'Translate file content while preserving structure and format. Supports JSON, YAML, XML, CSV, TXT, MD, and other text files. For large files (>100KB), returns a jobId for async processing. Use check_translation_status to monitor progress and download results.',
|
|
107
104
|
inputSchema: {
|
|
108
105
|
type: 'object',
|
|
109
106
|
properties: {
|
|
@@ -153,7 +150,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
153
150
|
type: 'string',
|
|
154
151
|
description: 'Specific region for localization (e.g., "Spain", "Mexico", "Brazil")',
|
|
155
152
|
},
|
|
156
|
-
|
|
153
|
+
context: {
|
|
157
154
|
type: 'string',
|
|
158
155
|
description: 'Optional additional context or instructions for the translation (e.g., "Keep technical terms in English", "Use formal tone")',
|
|
159
156
|
},
|
|
@@ -166,10 +163,55 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
166
163
|
description: 'Get remaining credits for the user and approximate word count available at 0.001 credits per word',
|
|
167
164
|
inputSchema: {
|
|
168
165
|
type: 'object',
|
|
169
|
-
properties: {
|
|
166
|
+
properties: {
|
|
167
|
+
apiKey: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'API key to get credits for (optional, will use environment variable if not provided)',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
170
172
|
required: [],
|
|
171
173
|
},
|
|
172
174
|
},
|
|
175
|
+
/*
|
|
176
|
+
* ====================================================================
|
|
177
|
+
* TOKEN USAGE TOOLS - RESTRICTED FROM MCP CLIENT ACCESS
|
|
178
|
+
* ====================================================================
|
|
179
|
+
*
|
|
180
|
+
* HARD LIMIT POLICY: Token usage analytics tools are NOT available
|
|
181
|
+
* through MCP client interfaces for security and privacy reasons.
|
|
182
|
+
*
|
|
183
|
+
* Restricted Tools:
|
|
184
|
+
* - get_token_usage_stats
|
|
185
|
+
* - get_token_usage_by_translation
|
|
186
|
+
* - get_token_usage_by_api_key
|
|
187
|
+
*
|
|
188
|
+
* These tools contain sensitive usage data and billing information
|
|
189
|
+
* that should only be accessible through authenticated web interfaces,
|
|
190
|
+
* not through programmatic MCP access.
|
|
191
|
+
*
|
|
192
|
+
* If you need token usage data, please use:
|
|
193
|
+
* - Web dashboard at https://app.i18nagent.ai
|
|
194
|
+
* - Direct API calls with proper authentication
|
|
195
|
+
* - Admin interfaces (for internal use only)
|
|
196
|
+
*
|
|
197
|
+
* This restriction is enforced at the service level and cannot be
|
|
198
|
+
* bypassed through client modifications.
|
|
199
|
+
* ====================================================================
|
|
200
|
+
*/
|
|
201
|
+
{
|
|
202
|
+
name: 'check_translation_status',
|
|
203
|
+
description: 'Check the status and progress of an async translation job. Returns progress percentage, elapsed time, and downloads completed translation results when finished.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
jobId: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: 'The job ID returned from translate_text (>100 texts or >50,000 chars) or translate_file (>100KB) for async processing',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ['jobId'],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
173
215
|
],
|
|
174
216
|
};
|
|
175
217
|
});
|
|
@@ -192,6 +234,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
192
234
|
case 'get_credits':
|
|
193
235
|
return await handleGetCredits(args);
|
|
194
236
|
|
|
237
|
+
/*
|
|
238
|
+
* TOKEN USAGE TOOLS - BLOCKED FOR SECURITY
|
|
239
|
+
* These cases are intentionally removed to prevent access to sensitive analytics data
|
|
240
|
+
* through MCP interfaces. See tool definition comments above for details.
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
case 'check_translation_status':
|
|
244
|
+
return await handleCheckTranslationStatus(args);
|
|
245
|
+
|
|
195
246
|
default:
|
|
196
247
|
throw new McpError(
|
|
197
248
|
ErrorCode.MethodNotFound,
|
|
@@ -200,15 +251,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
200
251
|
}
|
|
201
252
|
} catch (error) {
|
|
202
253
|
console.error(`Error executing tool ${name}:`, error);
|
|
254
|
+
|
|
255
|
+
// Check if error is about API key or credit issues
|
|
256
|
+
const errorMsg = error.message || '';
|
|
257
|
+
const isAuthError = errorMsg.toLowerCase().includes('api key') ||
|
|
258
|
+
errorMsg.toLowerCase().includes('api_key') ||
|
|
259
|
+
errorMsg.toLowerCase().includes('unauthorized') ||
|
|
260
|
+
errorMsg.includes('(401)');
|
|
261
|
+
const isCreditError = errorMsg.toLowerCase().includes('credit') ||
|
|
262
|
+
errorMsg.toLowerCase().includes('quota') ||
|
|
263
|
+
errorMsg.toLowerCase().includes('limit exceeded') ||
|
|
264
|
+
errorMsg.includes('(402)');
|
|
265
|
+
|
|
266
|
+
let finalErrorMsg = error.message;
|
|
267
|
+
|
|
268
|
+
// Add retry guidance for non-auth/credit errors
|
|
269
|
+
if (!isAuthError && !isCreditError) {
|
|
270
|
+
finalErrorMsg = `${error.message}. Please retry with smaller chunks or split the content into multiple requests.`;
|
|
271
|
+
}
|
|
272
|
+
|
|
203
273
|
throw new McpError(
|
|
204
274
|
ErrorCode.InternalError,
|
|
205
|
-
`Tool execution failed: ${
|
|
275
|
+
`Tool execution failed: ${finalErrorMsg}`
|
|
206
276
|
);
|
|
207
277
|
}
|
|
208
278
|
});
|
|
209
279
|
|
|
210
280
|
async function handleTranslateText(args) {
|
|
211
|
-
const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region,
|
|
281
|
+
const { texts, targetLanguage, sourceLanguage, targetAudience = 'general', industry = 'technology', region, context } = args;
|
|
212
282
|
|
|
213
283
|
if (!texts || !Array.isArray(texts) || texts.length === 0) {
|
|
214
284
|
throw new Error('texts must be a non-empty array');
|
|
@@ -217,18 +287,26 @@ async function handleTranslateText(args) {
|
|
|
217
287
|
if (!targetLanguage) {
|
|
218
288
|
throw new Error('targetLanguage is required');
|
|
219
289
|
}
|
|
290
|
+
|
|
291
|
+
if (!targetAudience) {
|
|
292
|
+
throw new Error('targetAudience is required');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!industry) {
|
|
296
|
+
throw new Error('industry is required');
|
|
297
|
+
}
|
|
220
298
|
|
|
221
299
|
// Check if this is a large translation request
|
|
222
300
|
const totalChars = texts.reduce((sum, text) => sum + text.length, 0);
|
|
223
301
|
const isLargeRequest = texts.length > 100 || totalChars > 50000;
|
|
224
302
|
|
|
225
|
-
// Use MCP JSON-RPC protocol for
|
|
303
|
+
// Use MCP JSON-RPC protocol for translate_text
|
|
226
304
|
const mcpRequest = {
|
|
227
305
|
jsonrpc: '2.0',
|
|
228
306
|
id: Date.now(),
|
|
229
307
|
method: 'tools/call',
|
|
230
308
|
params: {
|
|
231
|
-
name: '
|
|
309
|
+
name: 'translate_text',
|
|
232
310
|
arguments: {
|
|
233
311
|
apiKey: API_KEY,
|
|
234
312
|
texts: texts,
|
|
@@ -237,13 +315,12 @@ async function handleTranslateText(args) {
|
|
|
237
315
|
targetAudience: targetAudience,
|
|
238
316
|
industry: industry,
|
|
239
317
|
region: region,
|
|
240
|
-
|
|
318
|
+
context: context,
|
|
241
319
|
}
|
|
242
320
|
}
|
|
243
321
|
};
|
|
244
322
|
|
|
245
323
|
try {
|
|
246
|
-
console.error(`[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text] Request: ${texts.length} texts, ${totalChars} chars to ${MCP_SERVER_URL}`);
|
|
247
324
|
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
248
325
|
headers: {
|
|
249
326
|
'Content-Type': 'application/json',
|
|
@@ -252,7 +329,19 @@ async function handleTranslateText(args) {
|
|
|
252
329
|
});
|
|
253
330
|
|
|
254
331
|
if (response.data.error) {
|
|
255
|
-
|
|
332
|
+
const errorMsg = response.data.error.message || response.data.error;
|
|
333
|
+
const isAuthError = errorMsg.toString().toLowerCase().includes('api key') ||
|
|
334
|
+
errorMsg.toString().toLowerCase().includes('api_key') ||
|
|
335
|
+
errorMsg.toString().toLowerCase().includes('unauthorized');
|
|
336
|
+
const isCreditError = errorMsg.toString().toLowerCase().includes('credit') ||
|
|
337
|
+
errorMsg.toString().toLowerCase().includes('quota') ||
|
|
338
|
+
errorMsg.toString().toLowerCase().includes('limit exceeded');
|
|
339
|
+
|
|
340
|
+
let finalErrorMsg = `Translation service error: ${errorMsg}`;
|
|
341
|
+
if (!isAuthError && !isCreditError) {
|
|
342
|
+
finalErrorMsg += `. Please retry with a smaller text chunk or split the content into multiple smaller requests.`;
|
|
343
|
+
}
|
|
344
|
+
throw new Error(finalErrorMsg);
|
|
256
345
|
}
|
|
257
346
|
|
|
258
347
|
// Check if we got an async job response
|
|
@@ -308,103 +397,167 @@ async function handleTranslateText(args) {
|
|
|
308
397
|
};
|
|
309
398
|
}
|
|
310
399
|
|
|
311
|
-
//
|
|
400
|
+
// Handle 401 unauthorized - invalid API key
|
|
401
|
+
if (error.response?.status === 401) {
|
|
402
|
+
const authErrorDetails = error.response.data?.message || error.response.data?.result?.content?.[0]?.text || error.message;
|
|
403
|
+
throw new Error(`ā Invalid API key (401)\nDetails: ${authErrorDetails}\nPlease check your API key at https://app.i18nagent.ai\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Handle 402 payment required with user-friendly message
|
|
312
407
|
if (error.response?.status === 402) {
|
|
313
|
-
|
|
314
|
-
throw new Error(`ā ļø Insufficient credits
|
|
408
|
+
const creditErrorDetails = error.response.data?.message || error.response.data?.result?.content?.[0]?.text || error.message;
|
|
409
|
+
throw new Error(`ā ļø Insufficient credits (402)\nDetails: ${creditErrorDetails}\nPlease top up at https://app.i18nagent.ai\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Check if it's a large content issue
|
|
413
|
+
const totalChars = texts.reduce((sum, text) => sum + text.length, 0);
|
|
414
|
+
if (error.response?.status === 413 ||
|
|
415
|
+
(error.response?.status === 503 && totalChars > 50000)) {
|
|
416
|
+
const sizeErrorDetails = error.response?.data?.message || error.response?.data?.result?.content?.[0]?.text || error.message;
|
|
417
|
+
const errorMsg = `Content too large (${totalChars} characters, ${texts.length} texts)\nStatus: ${error.response?.status}\nDetails: ${sizeErrorDetails}\n\nPlease break into smaller batches:\n⢠Split into batches of 50-100 texts\n⢠Keep total size under 50KB per request\n⢠Process sequentially to avoid overload\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`;
|
|
418
|
+
throw new Error(errorMsg);
|
|
315
419
|
}
|
|
316
420
|
|
|
317
|
-
// Check if it's actually a service unavailable error (
|
|
421
|
+
// Check if it's actually a service unavailable error (only for real infrastructure issues)
|
|
318
422
|
if (error.code === 'ECONNREFUSED' ||
|
|
319
423
|
error.code === 'ETIMEDOUT' ||
|
|
320
|
-
error.
|
|
424
|
+
error.code === 'ENOTFOUND' ||
|
|
425
|
+
(error.response?.status === 503 && totalChars <= 50000) ||
|
|
321
426
|
error.response?.status === 502 ||
|
|
322
427
|
error.response?.status === 504) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
throw new Error(`Translation service unavailable [MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]: ${error.message}`);
|
|
428
|
+
const serviceErrorDetails = error.response?.data?.result?.content?.[0]?.text ||
|
|
429
|
+
error.response?.data?.error?.message ||
|
|
430
|
+
error.message;
|
|
431
|
+
const debugInfo = `Code: ${error.code || 'N/A'}\nStatus: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${serviceErrorDetails}\nURL: ${error.config?.url || 'N/A'}\nTimestamp: ${new Date().toISOString()}`;
|
|
432
|
+
throw new Error(`Translation service error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`);
|
|
330
433
|
}
|
|
331
434
|
|
|
332
|
-
// For other errors
|
|
333
|
-
|
|
334
|
-
|
|
435
|
+
// For other errors, include all debug info in the error message
|
|
436
|
+
const generalErrorDetails = error.response?.data?.result?.content?.[0]?.text ||
|
|
437
|
+
error.response?.data?.error?.message ||
|
|
438
|
+
error.message;
|
|
439
|
+
const debugInfo = `Status: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${generalErrorDetails}\nTimestamp: ${new Date().toISOString()}`;
|
|
440
|
+
throw new Error(`Error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`);
|
|
335
441
|
}
|
|
336
442
|
}
|
|
337
443
|
|
|
338
444
|
async function handleListLanguages(args) {
|
|
339
445
|
const { includeQuality = true } = args;
|
|
340
446
|
|
|
341
|
-
//
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
'nl': 'Dutch',
|
|
351
|
-
},
|
|
352
|
-
'Tier 2 - Production Viable (Good Quality 50-75%)': {
|
|
353
|
-
'ru': 'Russian',
|
|
354
|
-
'zh-CN': 'Chinese (Simplified)',
|
|
355
|
-
'ja': 'Japanese',
|
|
356
|
-
'ko': 'Korean',
|
|
357
|
-
'ar': 'Arabic',
|
|
358
|
-
'he': 'Hebrew',
|
|
359
|
-
'hi': 'Hindi',
|
|
360
|
-
'pl': 'Polish',
|
|
361
|
-
'cs': 'Czech',
|
|
362
|
-
},
|
|
363
|
-
'Tier 3 - Basic Support (Use with Caution 20-50%)': {
|
|
364
|
-
'zh-TW': 'Chinese (Traditional)',
|
|
365
|
-
'th': 'Thai',
|
|
366
|
-
'vi': 'Vietnamese',
|
|
367
|
-
'sv': 'Swedish',
|
|
368
|
-
'da': 'Danish',
|
|
369
|
-
'no': 'Norwegian',
|
|
370
|
-
'fi': 'Finnish',
|
|
371
|
-
'tr': 'Turkish',
|
|
372
|
-
'hu': 'Hungarian',
|
|
373
|
-
},
|
|
447
|
+
// Use MCP JSON-RPC protocol for list_supported_languages
|
|
448
|
+
const mcpRequest = {
|
|
449
|
+
jsonrpc: '2.0',
|
|
450
|
+
id: Date.now(),
|
|
451
|
+
method: 'tools/call',
|
|
452
|
+
params: {
|
|
453
|
+
name: 'list_supported_languages',
|
|
454
|
+
arguments: { includeQuality }
|
|
455
|
+
}
|
|
374
456
|
};
|
|
375
|
-
|
|
376
|
-
let content = 'š Supported Languages\n';
|
|
377
|
-
content += '===================\n\n';
|
|
378
457
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
458
|
+
try {
|
|
459
|
+
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
460
|
+
headers: {
|
|
461
|
+
'Content-Type': 'application/json',
|
|
462
|
+
},
|
|
463
|
+
timeout: 30000,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (response.data.error) {
|
|
467
|
+
const errorMsg = response.data.error.message || response.data.error;
|
|
468
|
+
throw new Error(`Languages service error: ${errorMsg}`);
|
|
386
469
|
}
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
470
|
+
|
|
471
|
+
const result = response.data.result;
|
|
472
|
+
if (result && result.content && result.content[0]) {
|
|
473
|
+
const textContent = result.content[0].text;
|
|
474
|
+
|
|
475
|
+
// Try to parse as JSON for structured data
|
|
476
|
+
try {
|
|
477
|
+
const parsed = JSON.parse(textContent);
|
|
478
|
+
|
|
479
|
+
// Format the language data nicely
|
|
480
|
+
let content = 'š Supported Languages\n';
|
|
481
|
+
content += '===================\n\n';
|
|
482
|
+
|
|
483
|
+
if (parsed.languages && Array.isArray(parsed.languages)) {
|
|
484
|
+
if (includeQuality) {
|
|
485
|
+
// Group by quality levels
|
|
486
|
+
const highQuality = parsed.languages.filter(lang => lang.quality === 'high');
|
|
487
|
+
const mediumQuality = parsed.languages.filter(lang => lang.quality === 'medium');
|
|
488
|
+
|
|
489
|
+
if (highQuality.length > 0) {
|
|
490
|
+
content += '## High Quality (Recommended for Production)\n';
|
|
491
|
+
highQuality.forEach(lang => {
|
|
492
|
+
content += `- \`${lang.code}\`: ${lang.name}\n`;
|
|
493
|
+
});
|
|
494
|
+
content += '\n';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (mediumQuality.length > 0) {
|
|
498
|
+
content += '## Medium Quality (Good with Review)\n';
|
|
499
|
+
mediumQuality.forEach(lang => {
|
|
500
|
+
content += `- \`${lang.code}\`: ${lang.name}\n`;
|
|
501
|
+
});
|
|
502
|
+
content += '\n';
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
parsed.languages.forEach(lang => {
|
|
506
|
+
content += `- \`${lang.code}\`: ${lang.name}\n`;
|
|
507
|
+
});
|
|
508
|
+
content += '\n';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
content += `š **Total Languages**: ${parsed.total || parsed.languages.length}\n\n`;
|
|
512
|
+
|
|
513
|
+
if (parsed.qualityLevels) {
|
|
514
|
+
content += `Quality Breakdown:\n`;
|
|
515
|
+
content += `⢠High Quality: ${parsed.qualityLevels.high} languages\n`;
|
|
516
|
+
content += `⢠Medium Quality: ${parsed.qualityLevels.medium} languages\n\n`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
content += 'š” Usage Tips:\n';
|
|
521
|
+
content += '- Use language codes (e.g., "es") or full names (e.g., "Spanish")\n';
|
|
522
|
+
content += '- High quality languages are recommended for production use\n';
|
|
523
|
+
content += '- Medium quality languages work well with human review\n';
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: 'text',
|
|
529
|
+
text: content,
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
} catch {
|
|
534
|
+
// Return raw text if not JSON
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
391
537
|
}
|
|
538
|
+
|
|
539
|
+
return result;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
console.error('List languages error:', error);
|
|
542
|
+
|
|
543
|
+
// Fallback to basic language list if service is unavailable
|
|
544
|
+
const fallbackContent = `š Supported Languages (Fallback)\n` +
|
|
545
|
+
`==================================\n\n` +
|
|
546
|
+
`Service temporarily unavailable. Here are the main supported languages:\n\n` +
|
|
547
|
+
`⢠\`es\`: Spanish\n⢠\`fr\`: French\n⢠\`de\`: German\n⢠\`it\`: Italian\n` +
|
|
548
|
+
`⢠\`pt\`: Portuguese\n⢠\`ja\`: Japanese\n⢠\`ko\`: Korean\n⢠\`zh\`: Chinese\n` +
|
|
549
|
+
`⢠\`ru\`: Russian\n⢠\`ar\`: Arabic\n⢠\`hi\`: Hindi\n⢠\`nl\`: Dutch\n\n` +
|
|
550
|
+
`Error: ${error.message}`;
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
content: [
|
|
554
|
+
{
|
|
555
|
+
type: 'text',
|
|
556
|
+
text: fallbackContent,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
};
|
|
392
560
|
}
|
|
393
|
-
|
|
394
|
-
content += '\nš” Usage Tips:\n';
|
|
395
|
-
content += '- Use language codes (e.g., "es") or full names (e.g., "Spanish")\n';
|
|
396
|
-
content += '- Tier 1 languages are recommended for production use\n';
|
|
397
|
-
content += '- Tier 2 languages work well with human review\n';
|
|
398
|
-
content += '- Tier 3 languages provide basic translation quality\n';
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
content: [
|
|
402
|
-
{
|
|
403
|
-
type: 'text',
|
|
404
|
-
text: content,
|
|
405
|
-
},
|
|
406
|
-
],
|
|
407
|
-
};
|
|
408
561
|
}
|
|
409
562
|
|
|
410
563
|
async function handleTranslateFile(args) {
|
|
@@ -419,7 +572,7 @@ async function handleTranslateFile(args) {
|
|
|
419
572
|
outputFormat = 'same',
|
|
420
573
|
sourceLanguage,
|
|
421
574
|
region,
|
|
422
|
-
|
|
575
|
+
context
|
|
423
576
|
} = args;
|
|
424
577
|
|
|
425
578
|
if (!filePath && !fileContent) {
|
|
@@ -461,7 +614,7 @@ async function handleTranslateFile(args) {
|
|
|
461
614
|
targetAudience,
|
|
462
615
|
industry,
|
|
463
616
|
region,
|
|
464
|
-
|
|
617
|
+
context,
|
|
465
618
|
preserveKeys,
|
|
466
619
|
outputFormat
|
|
467
620
|
}
|
|
@@ -469,7 +622,6 @@ async function handleTranslateFile(args) {
|
|
|
469
622
|
};
|
|
470
623
|
|
|
471
624
|
try {
|
|
472
|
-
console.error(`[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file] Request: ${content?.length || 0} chars to ${MCP_SERVER_URL}`);
|
|
473
625
|
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
474
626
|
headers: {
|
|
475
627
|
'Content-Type': 'application/json',
|
|
@@ -478,7 +630,19 @@ async function handleTranslateFile(args) {
|
|
|
478
630
|
});
|
|
479
631
|
|
|
480
632
|
if (response.data.error) {
|
|
481
|
-
|
|
633
|
+
const errorMsg = response.data.error.message || response.data.error;
|
|
634
|
+
const isAuthError = errorMsg.toString().toLowerCase().includes('api key') ||
|
|
635
|
+
errorMsg.toString().toLowerCase().includes('api_key') ||
|
|
636
|
+
errorMsg.toString().toLowerCase().includes('unauthorized');
|
|
637
|
+
const isCreditError = errorMsg.toString().toLowerCase().includes('credit') ||
|
|
638
|
+
errorMsg.toString().toLowerCase().includes('quota') ||
|
|
639
|
+
errorMsg.toString().toLowerCase().includes('limit exceeded');
|
|
640
|
+
|
|
641
|
+
let finalErrorMsg = `Translation service error: ${errorMsg}`;
|
|
642
|
+
if (!isAuthError && !isCreditError) {
|
|
643
|
+
finalErrorMsg += `. Please retry with a smaller text chunk or split the content into multiple smaller requests.`;
|
|
644
|
+
}
|
|
645
|
+
throw new Error(finalErrorMsg);
|
|
482
646
|
}
|
|
483
647
|
|
|
484
648
|
// Check if we got an async job response
|
|
@@ -502,6 +666,8 @@ async function handleTranslateFile(args) {
|
|
|
502
666
|
return result;
|
|
503
667
|
|
|
504
668
|
} catch (error) {
|
|
669
|
+
// Debug info will be included in error messages for visibility
|
|
670
|
+
|
|
505
671
|
if (error.code === 'ECONNABORTED') {
|
|
506
672
|
return {
|
|
507
673
|
content: [
|
|
@@ -524,30 +690,50 @@ async function handleTranslateFile(args) {
|
|
|
524
690
|
};
|
|
525
691
|
}
|
|
526
692
|
|
|
527
|
-
//
|
|
693
|
+
// Handle 401 unauthorized - invalid API key
|
|
694
|
+
if (error.response?.status === 401) {
|
|
695
|
+
const fileAuthErrorDetails = error.response.data?.message || error.response.data?.result?.content?.[0]?.text || error.message;
|
|
696
|
+
throw new Error(`ā Invalid API key (401)\nDetails: ${fileAuthErrorDetails}\nPlease check your API key at https://app.i18nagent.ai\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Handle 402 payment required with user-friendly message
|
|
528
700
|
if (error.response?.status === 402) {
|
|
529
|
-
|
|
530
|
-
throw new Error(`ā ļø Insufficient credits
|
|
701
|
+
const fileCreditErrorDetails = error.response.data?.message || error.response.data?.result?.content?.[0]?.text || error.message;
|
|
702
|
+
throw new Error(`ā ļø Insufficient credits (402)\nDetails: ${fileCreditErrorDetails}\nPlease top up at https://app.i18nagent.ai\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`);
|
|
531
703
|
}
|
|
532
704
|
|
|
533
|
-
// Check if it's
|
|
705
|
+
// Check if it's a timeout issue (45-second server timeout) or large file issue
|
|
706
|
+
const timeoutErrorDetails = error.response?.data?.result?.content?.[0]?.text ||
|
|
707
|
+
error.response?.data?.error?.message ||
|
|
708
|
+
error.message;
|
|
709
|
+
|
|
710
|
+
if (error.response?.status === 413 ||
|
|
711
|
+
(error.response?.status === 503 && content.length > 50000) ||
|
|
712
|
+
(error.response?.status === 503 && timeoutErrorDetails.includes('timeout after 45 seconds'))) {
|
|
713
|
+
const errorMsg = `File too large or complex (${content.length} characters)\n\nThe server has a 45-second timeout. Your file requires more processing time.\n\nPlease break into smaller chunks:\n⢠Split files over 50KB into multiple parts\n⢠Translate sections separately (e.g., split by top-level keys for JSON)\n⢠Use translate_text for batches of 50-100 strings\n⢠Each chunk should process in under 45 seconds\n\nAlternatively, wait for async job support (coming soon).\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`;
|
|
714
|
+
throw new Error(errorMsg);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Check if it's actually a service unavailable error (only for real infrastructure issues)
|
|
534
718
|
if (error.code === 'ECONNREFUSED' ||
|
|
535
719
|
error.code === 'ETIMEDOUT' ||
|
|
536
|
-
error.
|
|
720
|
+
error.code === 'ENOTFOUND' ||
|
|
721
|
+
(error.response?.status === 503 && content.length <= 50000) ||
|
|
537
722
|
error.response?.status === 502 ||
|
|
538
723
|
error.response?.status === 504) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
});
|
|
545
|
-
throw new Error(`Translation service unavailable [MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]: ${error.message}`);
|
|
724
|
+
const serviceErrorDetails = error.response?.data?.result?.content?.[0]?.text ||
|
|
725
|
+
error.response?.data?.error?.message ||
|
|
726
|
+
error.message;
|
|
727
|
+
const debugInfo = `Code: ${error.code || 'N/A'}\nStatus: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${serviceErrorDetails}\nURL: ${error.config?.url || 'N/A'}\nTimestamp: ${new Date().toISOString()}`;
|
|
728
|
+
throw new Error(`Translation service error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`);
|
|
546
729
|
}
|
|
547
730
|
|
|
548
|
-
// For other errors,
|
|
549
|
-
|
|
550
|
-
|
|
731
|
+
// For other errors, include all debug info in the error message
|
|
732
|
+
const finalErrorDetails = error.response?.data?.result?.content?.[0]?.text ||
|
|
733
|
+
error.response?.data?.error?.message ||
|
|
734
|
+
error.message;
|
|
735
|
+
const debugInfo = `Status: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${finalErrorDetails}\nTimestamp: ${new Date().toISOString()}`;
|
|
736
|
+
throw new Error(`Error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`);
|
|
551
737
|
}
|
|
552
738
|
}
|
|
553
739
|
|
|
@@ -599,7 +785,19 @@ async function pollTranslationJob(jobId, estimatedTime) {
|
|
|
599
785
|
});
|
|
600
786
|
|
|
601
787
|
if (response.data.error) {
|
|
602
|
-
|
|
788
|
+
const errorMsg = response.data.error.message || response.data.error;
|
|
789
|
+
const isAuthError = errorMsg.toString().toLowerCase().includes('api key') ||
|
|
790
|
+
errorMsg.toString().toLowerCase().includes('api_key') ||
|
|
791
|
+
errorMsg.toString().toLowerCase().includes('unauthorized');
|
|
792
|
+
const isCreditError = errorMsg.toString().toLowerCase().includes('credit') ||
|
|
793
|
+
errorMsg.toString().toLowerCase().includes('quota') ||
|
|
794
|
+
errorMsg.toString().toLowerCase().includes('limit exceeded');
|
|
795
|
+
|
|
796
|
+
let finalErrorMsg = `Status check error: ${errorMsg}`;
|
|
797
|
+
if (!isAuthError && !isCreditError) {
|
|
798
|
+
finalErrorMsg += `. Please retry with a smaller chunk or split the content into multiple requests.`;
|
|
799
|
+
}
|
|
800
|
+
throw new Error(finalErrorMsg);
|
|
603
801
|
}
|
|
604
802
|
|
|
605
803
|
const result = response.data.result;
|
|
@@ -609,7 +807,19 @@ async function pollTranslationJob(jobId, estimatedTime) {
|
|
|
609
807
|
if (status.status === 'completed') {
|
|
610
808
|
return status.result;
|
|
611
809
|
} else if (status.status === 'failed') {
|
|
612
|
-
|
|
810
|
+
const errorMsg = status.error;
|
|
811
|
+
const isAuthError = errorMsg.toString().toLowerCase().includes('api key') ||
|
|
812
|
+
errorMsg.toString().toLowerCase().includes('api_key') ||
|
|
813
|
+
errorMsg.toString().toLowerCase().includes('unauthorized');
|
|
814
|
+
const isCreditError = errorMsg.toString().toLowerCase().includes('credit') ||
|
|
815
|
+
errorMsg.toString().toLowerCase().includes('quota') ||
|
|
816
|
+
errorMsg.toString().toLowerCase().includes('limit exceeded');
|
|
817
|
+
|
|
818
|
+
let finalErrorMsg = `Translation failed: ${errorMsg}`;
|
|
819
|
+
if (!isAuthError && !isCreditError) {
|
|
820
|
+
finalErrorMsg += `. Please retry with a smaller chunk or split the content into multiple requests.`;
|
|
821
|
+
}
|
|
822
|
+
throw new Error(finalErrorMsg);
|
|
613
823
|
}
|
|
614
824
|
|
|
615
825
|
// Still processing - continue polling
|
|
@@ -621,43 +831,86 @@ async function pollTranslationJob(jobId, estimatedTime) {
|
|
|
621
831
|
}
|
|
622
832
|
}
|
|
623
833
|
|
|
624
|
-
throw new Error(`Translation job ${jobId} timed out after ${maxPolls * pollInterval / 1000} seconds
|
|
834
|
+
throw new Error(`Translation job ${jobId} timed out after ${maxPolls * pollInterval / 1000} seconds. Please retry with a smaller chunk or split the content into multiple requests.`);
|
|
625
835
|
}
|
|
626
836
|
|
|
627
837
|
async function handleGetCredits(args) {
|
|
838
|
+
const { apiKey } = args;
|
|
839
|
+
const creditsApiKey = apiKey || API_KEY;
|
|
840
|
+
|
|
841
|
+
// Use MCP JSON-RPC protocol for get_credits
|
|
842
|
+
const mcpRequest = {
|
|
843
|
+
jsonrpc: '2.0',
|
|
844
|
+
id: Date.now(),
|
|
845
|
+
method: 'tools/call',
|
|
846
|
+
params: {
|
|
847
|
+
name: 'get_credits',
|
|
848
|
+
arguments: {
|
|
849
|
+
apiKey: creditsApiKey
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
628
854
|
try {
|
|
629
|
-
|
|
630
|
-
const teamResponse = await axios.get(`https://platform.i18nagent.ai/api/teams/by-api-key/${API_KEY}`, {
|
|
855
|
+
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
631
856
|
headers: {
|
|
632
|
-
'Content-Type': 'application/json'
|
|
857
|
+
'Content-Type': 'application/json',
|
|
633
858
|
},
|
|
634
|
-
timeout:
|
|
859
|
+
timeout: 30000,
|
|
635
860
|
});
|
|
636
861
|
|
|
637
|
-
if (
|
|
638
|
-
|
|
862
|
+
if (response.data.error) {
|
|
863
|
+
const errorMsg = response.data.error.message || response.data.error;
|
|
864
|
+
const isAuthError = errorMsg.toString().toLowerCase().includes('api key') ||
|
|
865
|
+
errorMsg.toString().toLowerCase().includes('api_key') ||
|
|
866
|
+
errorMsg.toString().toLowerCase().includes('unauthorized');
|
|
867
|
+
const isCreditError = errorMsg.toString().toLowerCase().includes('credit') ||
|
|
868
|
+
errorMsg.toString().toLowerCase().includes('quota') ||
|
|
869
|
+
errorMsg.toString().toLowerCase().includes('limit exceeded');
|
|
870
|
+
|
|
871
|
+
let finalErrorMsg = `Credits service error: ${errorMsg}`;
|
|
872
|
+
if (!isAuthError && !isCreditError) {
|
|
873
|
+
finalErrorMsg += `. Please check the service status or contact support.`;
|
|
874
|
+
}
|
|
875
|
+
throw new Error(finalErrorMsg);
|
|
639
876
|
}
|
|
640
877
|
|
|
641
|
-
const
|
|
642
|
-
|
|
878
|
+
const result = response.data.result;
|
|
879
|
+
if (result && result.content && result.content[0]) {
|
|
880
|
+
const textContent = result.content[0].text;
|
|
881
|
+
|
|
882
|
+
// Try to parse as JSON for structured data
|
|
883
|
+
try {
|
|
884
|
+
const parsed = JSON.parse(textContent);
|
|
885
|
+
const approximateWordsAvailable = parsed.credits ? Math.floor(parsed.credits * 1000) : 0;
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
content: [
|
|
889
|
+
{
|
|
890
|
+
type: 'text',
|
|
891
|
+
text: `š° **Credits Information**\n\n` +
|
|
892
|
+
`š³ **Credits Remaining**: ${parsed.credits || 'N/A'}\n` +
|
|
893
|
+
`š **Approximate Words Available**: ${approximateWordsAvailable.toLocaleString()}\n` +
|
|
894
|
+
`šµ **Cost per Word**: 0.001 credits\n` +
|
|
895
|
+
`ā° **Last Updated**: ${new Date().toLocaleString()}\n\n` +
|
|
896
|
+
`Note: Word count is approximate and may vary based on actual content complexity and translation requirements.`,
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
};
|
|
900
|
+
} catch {
|
|
901
|
+
// Return raw text if not JSON
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
643
905
|
|
|
644
|
-
return
|
|
645
|
-
content: [
|
|
646
|
-
{
|
|
647
|
-
type: 'text',
|
|
648
|
-
text: `š° **Credits Information**
|
|
649
|
-
|
|
650
|
-
š¢ **Team**: ${teamInfo.name}
|
|
651
|
-
š³ **Credits Remaining**: ${teamInfo.credits}
|
|
652
|
-
š **Approximate Words Available**: ${approximateWordsAvailable.toLocaleString()}
|
|
653
|
-
šµ **Cost per Word**: 0.001 credits
|
|
654
|
-
ā° **Last Updated**: ${new Date().toLocaleString()}
|
|
655
|
-
|
|
656
|
-
Note: Word count is approximate and may vary based on actual content complexity and translation requirements.`,
|
|
657
|
-
},
|
|
658
|
-
],
|
|
659
|
-
};
|
|
906
|
+
return result;
|
|
660
907
|
} catch (error) {
|
|
908
|
+
// Handle 401 unauthorized
|
|
909
|
+
if (error.response?.status === 401) {
|
|
910
|
+
const creditsAuthErrorDetails = error.response.data?.message || error.response.data?.result?.content?.[0]?.text || error.message;
|
|
911
|
+
throw new Error(`ā Invalid API key (401)\nDetails: ${creditsAuthErrorDetails}\nPlease check your API key at https://app.i18nagent.ai\n[MCP v${MCP_CLIENT_VERSION}/STDIO/get_credits]`);
|
|
912
|
+
}
|
|
913
|
+
|
|
661
914
|
console.error('Credits check error:', error);
|
|
662
915
|
throw new Error(`Unable to check credits: ${error.message}`);
|
|
663
916
|
}
|
|
@@ -990,6 +1243,72 @@ function getCodeBlockLanguage(fileType) {
|
|
|
990
1243
|
return languageMap[fileType] || 'text';
|
|
991
1244
|
}
|
|
992
1245
|
|
|
1246
|
+
/*
|
|
1247
|
+
* =====================================================================
|
|
1248
|
+
* TOKEN USAGE HANDLERS - INTENTIONALLY REMOVED FOR SECURITY
|
|
1249
|
+
* =====================================================================
|
|
1250
|
+
*
|
|
1251
|
+
* The following handler functions have been permanently removed from
|
|
1252
|
+
* the MCP client to prevent unauthorized access to sensitive analytics:
|
|
1253
|
+
*
|
|
1254
|
+
* - handleGetTokenUsageStats()
|
|
1255
|
+
* - handleGetTokenUsageByTranslation()
|
|
1256
|
+
* - handleGetTokenUsageByApiKey()
|
|
1257
|
+
*
|
|
1258
|
+
* SECURITY RATIONALE:
|
|
1259
|
+
* Token usage data contains sensitive billing and usage information
|
|
1260
|
+
* that should not be accessible through programmatic MCP clients.
|
|
1261
|
+
* This data includes:
|
|
1262
|
+
* - Detailed usage patterns and costs
|
|
1263
|
+
* - API key performance metrics
|
|
1264
|
+
* - Translation volume analytics
|
|
1265
|
+
* - Billing-related information
|
|
1266
|
+
*
|
|
1267
|
+
* ACCESS ALTERNATIVES:
|
|
1268
|
+
* - Use the web dashboard at https://app.i18nagent.ai
|
|
1269
|
+
* - Contact support for usage reports
|
|
1270
|
+
* - Use admin interfaces (internal only)
|
|
1271
|
+
*
|
|
1272
|
+
* This restriction is enforced as a hard security boundary and
|
|
1273
|
+
* cannot be bypassed through client modifications.
|
|
1274
|
+
* =====================================================================
|
|
1275
|
+
*/
|
|
1276
|
+
|
|
1277
|
+
// Handler for checking translation status
|
|
1278
|
+
async function handleCheckTranslationStatus(args) {
|
|
1279
|
+
const { jobId } = args;
|
|
1280
|
+
|
|
1281
|
+
if (!jobId) {
|
|
1282
|
+
throw new Error('jobId is required');
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const mcpRequest = {
|
|
1286
|
+
jsonrpc: '2.0',
|
|
1287
|
+
id: Date.now(),
|
|
1288
|
+
method: 'tools/call',
|
|
1289
|
+
params: {
|
|
1290
|
+
name: 'check_translation_status',
|
|
1291
|
+
arguments: { jobId }
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
try {
|
|
1296
|
+
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
1297
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1298
|
+
timeout: 30000
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
if (response.data.error) {
|
|
1302
|
+
throw new Error(`Translation status error: ${response.data.error.message || response.data.error}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return response.data.result;
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
console.error('Check translation status error:', error);
|
|
1308
|
+
throw new Error(`Unable to check translation status: ${error.message}`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
993
1312
|
// Start the server
|
|
994
1313
|
async function main() {
|
|
995
1314
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i18n-agent/mcp-client",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP client for i18n-agent translation service - supports Claude, Cursor, VS Code, and other AI IDEs",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "MCP client for i18n-agent translation service with async job support and enhanced progress tracking - supports Claude, Cursor, VS Code, and other AI IDEs",
|
|
5
5
|
"main": "mcp-client.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"i18n-agent-install": "install.js"
|