@i18n-agent/mcp-client 1.1.3 ā 1.4.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.
- package/README.md +111 -7
- package/install.js +69 -10
- package/mcp-client.js +442 -143
- 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,7 +5,7 @@
|
|
|
5
5
|
* Integrates with Claude Code CLI to provide translation capabilities
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const MCP_CLIENT_VERSION = '1.1
|
|
8
|
+
const MCP_CLIENT_VERSION = '1.4.1';
|
|
9
9
|
|
|
10
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -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 (but not for status checking)
|
|
269
|
+
if (!isAuthError && !isCreditError && name !== 'check_translation_status') {
|
|
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,7 +315,7 @@ 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
|
};
|
|
@@ -251,7 +329,19 @@ async function handleTranslateText(args) {
|
|
|
251
329
|
});
|
|
252
330
|
|
|
253
331
|
if (response.data.error) {
|
|
254
|
-
|
|
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);
|
|
255
345
|
}
|
|
256
346
|
|
|
257
347
|
// Check if we got an async job response
|
|
@@ -309,22 +399,22 @@ async function handleTranslateText(args) {
|
|
|
309
399
|
|
|
310
400
|
// Handle 401 unauthorized - invalid API key
|
|
311
401
|
if (error.response?.status === 401) {
|
|
312
|
-
const
|
|
313
|
-
throw new Error(`ā Invalid API key (401)\nDetails: ${
|
|
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]`);
|
|
314
404
|
}
|
|
315
405
|
|
|
316
406
|
// Handle 402 payment required with user-friendly message
|
|
317
407
|
if (error.response?.status === 402) {
|
|
318
|
-
const
|
|
319
|
-
throw new Error(`ā ļø Insufficient credits (402)\nDetails: ${
|
|
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]`);
|
|
320
410
|
}
|
|
321
411
|
|
|
322
412
|
// Check if it's a large content issue
|
|
323
413
|
const totalChars = texts.reduce((sum, text) => sum + text.length, 0);
|
|
324
414
|
if (error.response?.status === 413 ||
|
|
325
415
|
(error.response?.status === 503 && totalChars > 50000)) {
|
|
326
|
-
const
|
|
327
|
-
const errorMsg = `Content too large (${totalChars} characters, ${texts.length} texts)\nStatus: ${error.response?.status}\nDetails: ${
|
|
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]`;
|
|
328
418
|
throw new Error(errorMsg);
|
|
329
419
|
}
|
|
330
420
|
|
|
@@ -335,18 +425,18 @@ async function handleTranslateText(args) {
|
|
|
335
425
|
(error.response?.status === 503 && totalChars <= 50000) ||
|
|
336
426
|
error.response?.status === 502 ||
|
|
337
427
|
error.response?.status === 504) {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const debugInfo = `Code: ${error.code || 'N/A'}\nStatus: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${
|
|
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()}`;
|
|
342
432
|
throw new Error(`Translation service error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`);
|
|
343
433
|
}
|
|
344
434
|
|
|
345
435
|
// For other errors, include all debug info in the error message
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const debugInfo = `Status: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${
|
|
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()}`;
|
|
350
440
|
throw new Error(`Error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_text]`);
|
|
351
441
|
}
|
|
352
442
|
}
|
|
@@ -354,73 +444,120 @@ async function handleTranslateText(args) {
|
|
|
354
444
|
async function handleListLanguages(args) {
|
|
355
445
|
const { includeQuality = true } = args;
|
|
356
446
|
|
|
357
|
-
//
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'nl': 'Dutch',
|
|
367
|
-
},
|
|
368
|
-
'Tier 2 - Production Viable (Good Quality 50-75%)': {
|
|
369
|
-
'ru': 'Russian',
|
|
370
|
-
'zh-CN': 'Chinese (Simplified)',
|
|
371
|
-
'ja': 'Japanese',
|
|
372
|
-
'ko': 'Korean',
|
|
373
|
-
'ar': 'Arabic',
|
|
374
|
-
'he': 'Hebrew',
|
|
375
|
-
'hi': 'Hindi',
|
|
376
|
-
'pl': 'Polish',
|
|
377
|
-
'cs': 'Czech',
|
|
378
|
-
},
|
|
379
|
-
'Tier 3 - Basic Support (Use with Caution 20-50%)': {
|
|
380
|
-
'zh-TW': 'Chinese (Traditional)',
|
|
381
|
-
'th': 'Thai',
|
|
382
|
-
'vi': 'Vietnamese',
|
|
383
|
-
'sv': 'Swedish',
|
|
384
|
-
'da': 'Danish',
|
|
385
|
-
'no': 'Norwegian',
|
|
386
|
-
'fi': 'Finnish',
|
|
387
|
-
'tr': 'Turkish',
|
|
388
|
-
'hu': 'Hungarian',
|
|
389
|
-
},
|
|
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
|
+
}
|
|
390
456
|
};
|
|
391
|
-
|
|
392
|
-
let content = 'š Supported Languages\n';
|
|
393
|
-
content += '===================\n\n';
|
|
394
457
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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}`);
|
|
402
469
|
}
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
}
|
|
407
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
|
+
};
|
|
408
560
|
}
|
|
409
|
-
|
|
410
|
-
content += '\nš” Usage Tips:\n';
|
|
411
|
-
content += '- Use language codes (e.g., "es") or full names (e.g., "Spanish")\n';
|
|
412
|
-
content += '- Tier 1 languages are recommended for production use\n';
|
|
413
|
-
content += '- Tier 2 languages work well with human review\n';
|
|
414
|
-
content += '- Tier 3 languages provide basic translation quality\n';
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
content: [
|
|
418
|
-
{
|
|
419
|
-
type: 'text',
|
|
420
|
-
text: content,
|
|
421
|
-
},
|
|
422
|
-
],
|
|
423
|
-
};
|
|
424
561
|
}
|
|
425
562
|
|
|
426
563
|
async function handleTranslateFile(args) {
|
|
@@ -435,7 +572,7 @@ async function handleTranslateFile(args) {
|
|
|
435
572
|
outputFormat = 'same',
|
|
436
573
|
sourceLanguage,
|
|
437
574
|
region,
|
|
438
|
-
|
|
575
|
+
context
|
|
439
576
|
} = args;
|
|
440
577
|
|
|
441
578
|
if (!filePath && !fileContent) {
|
|
@@ -477,7 +614,7 @@ async function handleTranslateFile(args) {
|
|
|
477
614
|
targetAudience,
|
|
478
615
|
industry,
|
|
479
616
|
region,
|
|
480
|
-
|
|
617
|
+
context,
|
|
481
618
|
preserveKeys,
|
|
482
619
|
outputFormat
|
|
483
620
|
}
|
|
@@ -493,7 +630,19 @@ async function handleTranslateFile(args) {
|
|
|
493
630
|
});
|
|
494
631
|
|
|
495
632
|
if (response.data.error) {
|
|
496
|
-
|
|
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);
|
|
497
646
|
}
|
|
498
647
|
|
|
499
648
|
// Check if we got an async job response
|
|
@@ -543,24 +692,24 @@ async function handleTranslateFile(args) {
|
|
|
543
692
|
|
|
544
693
|
// Handle 401 unauthorized - invalid API key
|
|
545
694
|
if (error.response?.status === 401) {
|
|
546
|
-
const
|
|
547
|
-
throw new Error(`ā Invalid API key (401)\nDetails: ${
|
|
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]`);
|
|
548
697
|
}
|
|
549
698
|
|
|
550
699
|
// Handle 402 payment required with user-friendly message
|
|
551
700
|
if (error.response?.status === 402) {
|
|
552
|
-
const
|
|
553
|
-
throw new Error(`ā ļø Insufficient credits (402)\nDetails: ${
|
|
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]`);
|
|
554
703
|
}
|
|
555
704
|
|
|
556
705
|
// Check if it's a timeout issue (45-second server timeout) or large file issue
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
706
|
+
const timeoutErrorDetails = error.response?.data?.result?.content?.[0]?.text ||
|
|
707
|
+
error.response?.data?.error?.message ||
|
|
708
|
+
error.message;
|
|
560
709
|
|
|
561
710
|
if (error.response?.status === 413 ||
|
|
562
711
|
(error.response?.status === 503 && content.length > 50000) ||
|
|
563
|
-
(error.response?.status === 503 &&
|
|
712
|
+
(error.response?.status === 503 && timeoutErrorDetails.includes('timeout after 45 seconds'))) {
|
|
564
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]`;
|
|
565
714
|
throw new Error(errorMsg);
|
|
566
715
|
}
|
|
@@ -572,18 +721,18 @@ async function handleTranslateFile(args) {
|
|
|
572
721
|
(error.response?.status === 503 && content.length <= 50000) ||
|
|
573
722
|
error.response?.status === 502 ||
|
|
574
723
|
error.response?.status === 504) {
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const debugInfo = `Code: ${error.code || 'N/A'}\nStatus: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${
|
|
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()}`;
|
|
579
728
|
throw new Error(`Translation service error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`);
|
|
580
729
|
}
|
|
581
730
|
|
|
582
731
|
// For other errors, include all debug info in the error message
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const debugInfo = `Status: ${error.response?.status || 'N/A'}\nStatusText: ${error.response?.statusText || 'N/A'}\nDetails: ${
|
|
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()}`;
|
|
587
736
|
throw new Error(`Error\n${debugInfo}\n[MCP v${MCP_CLIENT_VERSION}/STDIO/translate_file]`);
|
|
588
737
|
}
|
|
589
738
|
}
|
|
@@ -636,7 +785,19 @@ async function pollTranslationJob(jobId, estimatedTime) {
|
|
|
636
785
|
});
|
|
637
786
|
|
|
638
787
|
if (response.data.error) {
|
|
639
|
-
|
|
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);
|
|
640
801
|
}
|
|
641
802
|
|
|
642
803
|
const result = response.data.result;
|
|
@@ -646,7 +807,19 @@ async function pollTranslationJob(jobId, estimatedTime) {
|
|
|
646
807
|
if (status.status === 'completed') {
|
|
647
808
|
return status.result;
|
|
648
809
|
} else if (status.status === 'failed') {
|
|
649
|
-
|
|
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);
|
|
650
823
|
}
|
|
651
824
|
|
|
652
825
|
// Still processing - continue polling
|
|
@@ -658,43 +831,86 @@ async function pollTranslationJob(jobId, estimatedTime) {
|
|
|
658
831
|
}
|
|
659
832
|
}
|
|
660
833
|
|
|
661
|
-
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.`);
|
|
662
835
|
}
|
|
663
836
|
|
|
664
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
|
+
|
|
665
854
|
try {
|
|
666
|
-
|
|
667
|
-
const teamResponse = await axios.get(`https://app.i18nagent.ai/api/teams/by-api-key/${API_KEY}`, {
|
|
855
|
+
const response = await axios.post(MCP_SERVER_URL, mcpRequest, {
|
|
668
856
|
headers: {
|
|
669
|
-
'Content-Type': 'application/json'
|
|
857
|
+
'Content-Type': 'application/json',
|
|
670
858
|
},
|
|
671
|
-
timeout:
|
|
859
|
+
timeout: 30000,
|
|
672
860
|
});
|
|
673
861
|
|
|
674
|
-
if (
|
|
675
|
-
|
|
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);
|
|
676
876
|
}
|
|
677
877
|
|
|
678
|
-
const
|
|
679
|
-
|
|
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
|
+
}
|
|
680
905
|
|
|
681
|
-
return
|
|
682
|
-
content: [
|
|
683
|
-
{
|
|
684
|
-
type: 'text',
|
|
685
|
-
text: `š° **Credits Information**
|
|
686
|
-
|
|
687
|
-
š¢ **Team**: ${teamInfo.name}
|
|
688
|
-
š³ **Credits Remaining**: ${teamInfo.credits}
|
|
689
|
-
š **Approximate Words Available**: ${approximateWordsAvailable.toLocaleString()}
|
|
690
|
-
šµ **Cost per Word**: 0.001 credits
|
|
691
|
-
ā° **Last Updated**: ${new Date().toLocaleString()}
|
|
692
|
-
|
|
693
|
-
Note: Word count is approximate and may vary based on actual content complexity and translation requirements.`,
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
};
|
|
906
|
+
return result;
|
|
697
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
|
+
|
|
698
914
|
console.error('Credits check error:', error);
|
|
699
915
|
throw new Error(`Unable to check credits: ${error.message}`);
|
|
700
916
|
}
|
|
@@ -1027,6 +1243,89 @@ function getCodeBlockLanguage(fileType) {
|
|
|
1027
1243
|
return languageMap[fileType] || 'text';
|
|
1028
1244
|
}
|
|
1029
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
|
+
|
|
1309
|
+
// Handle 503 service unavailable
|
|
1310
|
+
if (error.response?.status === 503) {
|
|
1311
|
+
throw new Error(`Translation service is temporarily unavailable (503). Please try again in a few moments.`);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Handle 404 not found
|
|
1315
|
+
if (error.response?.status === 404) {
|
|
1316
|
+
throw new Error(`Translation job ${jobId} not found. The job may have expired or the ID is incorrect.`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Handle timeout
|
|
1320
|
+
if (error.code === 'ECONNABORTED') {
|
|
1321
|
+
throw new Error(`Status check timed out. The service may be experiencing high load. Please try again.`);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Generic error
|
|
1325
|
+
throw new Error(`Unable to check translation status: ${error.message}`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1030
1329
|
// Start the server
|
|
1031
1330
|
async function main() {
|
|
1032
1331
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i18n-agent/mcp-client",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "MCP client for i18n-agent translation service - supports Claude, Cursor, VS Code, and other AI IDEs",
|
|
3
|
+
"version": "1.4.1",
|
|
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"
|