@l10nmonster/mcp 3.0.0-alpha.16
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 +261 -0
- package/index.js +15 -0
- package/package.json +31 -0
- package/server.js +235 -0
- package/tests/integration.test.js +215 -0
- package/tests/mcpToolValidation.test.js +947 -0
- package/tests/registry.test.js +169 -0
- package/tools/index.js +3 -0
- package/tools/mcpTool.js +214 -0
- package/tools/registry.js +69 -0
- package/tools/sourceQuery.js +88 -0
- package/tools/status.js +665 -0
- package/tools/translate.js +227 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { McpTool, McpInputError, McpNotFoundError, McpProviderError, McpToolError } from './mcpTool.js';
|
|
3
|
+
import { TU } from '@l10nmonster/core';
|
|
4
|
+
|
|
5
|
+
async function createJobForProvider(mm, jobRequest, provider, sourceTUsCount, guids) {
|
|
6
|
+
let assignedJobs;
|
|
7
|
+
try {
|
|
8
|
+
assignedJobs = await mm.dispatcher.createJobs(
|
|
9
|
+
jobRequest,
|
|
10
|
+
{ providerList: [provider], skipGroupCheck: true, skipQualityCheck: true }
|
|
11
|
+
);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new McpProviderError(`Provider "${provider}" rejected the job request`, {
|
|
14
|
+
hints: [
|
|
15
|
+
'Verify the provider supports the requested language pair and content type.',
|
|
16
|
+
'Ensure provider credentials/configuration are valid.'
|
|
17
|
+
],
|
|
18
|
+
details: { jobRequestSize: sourceTUsCount },
|
|
19
|
+
cause: error
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const job = assignedJobs.find(j => j.translationProvider === provider);
|
|
24
|
+
|
|
25
|
+
if (!job) {
|
|
26
|
+
const rejectedJob = assignedJobs.find(j => !j.translationProvider);
|
|
27
|
+
if (rejectedJob && rejectedJob.tus.length > 0) {
|
|
28
|
+
throw new McpProviderError(`Provider "${provider}" did not accept any of the ${guids.length} translation units`, {
|
|
29
|
+
hints: [
|
|
30
|
+
'Double-check language pair support.',
|
|
31
|
+
'Try a different provider or smaller batch size.'
|
|
32
|
+
],
|
|
33
|
+
details: { rejectedGuids: rejectedJob.tus.map(tu => tu.guid) }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
throw new McpProviderError(`Failed to create job with provider "${provider}"`, {
|
|
37
|
+
details: { assignedJobs: assignedJobs.length }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return job;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getTranslatedTUs(completedJob) {
|
|
45
|
+
const inflightGuidsSet = new Set(completedJob.inflight || []);
|
|
46
|
+
return completedJob.tus
|
|
47
|
+
.filter(tu => tu.ntgt && !inflightGuidsSet.has(tu.guid))
|
|
48
|
+
.map(tu => ({
|
|
49
|
+
guid: tu.guid,
|
|
50
|
+
rid: tu.rid,
|
|
51
|
+
sid: tu.sid,
|
|
52
|
+
nsrc: tu.nsrc,
|
|
53
|
+
ntgt: tu.ntgt,
|
|
54
|
+
q: tu.q,
|
|
55
|
+
ts: tu.ts,
|
|
56
|
+
translationProvider: tu.translationProvider || completedJob.translationProvider,
|
|
57
|
+
jobGuid: completedJob.jobGuid
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* MCP tool for translating source translation units by their GUIDs.
|
|
63
|
+
*
|
|
64
|
+
* This tool creates a translation job, assigns it to a provider,
|
|
65
|
+
* starts the job, and returns the translated TUs.
|
|
66
|
+
*/
|
|
67
|
+
export class TranslateTool extends McpTool {
|
|
68
|
+
static metadata = {
|
|
69
|
+
name: 'translate',
|
|
70
|
+
description: `Translate source translation units (TUs) by their GUIDs using a specified provider.
|
|
71
|
+
|
|
72
|
+
This tool:
|
|
73
|
+
1. Fetches source TUs from the channel using the provided GUIDs
|
|
74
|
+
2. Creates a translation job with the fetched source TUs
|
|
75
|
+
3. Assigns the job to the specified translation provider
|
|
76
|
+
4. Starts the job (executes the translation)
|
|
77
|
+
5. Returns the translated TUs with their target text
|
|
78
|
+
|
|
79
|
+
The tool returns translated TUs with:
|
|
80
|
+
- guid: Same as source TU
|
|
81
|
+
- ntgt: Normalized target strings (array of strings)
|
|
82
|
+
- q: Quality score
|
|
83
|
+
- ts: Timestamp`,
|
|
84
|
+
inputSchema: z.object({
|
|
85
|
+
sourceLang: z.string()
|
|
86
|
+
.describe('Source language code (e.g., "en-US")'),
|
|
87
|
+
targetLang: z.string()
|
|
88
|
+
.describe('Target language code (e.g., "es-419")'),
|
|
89
|
+
channelId: z.string()
|
|
90
|
+
.describe('Channel ID to fetch source TUs from'),
|
|
91
|
+
provider: z.string()
|
|
92
|
+
.describe('Translation provider ID to use for translation'),
|
|
93
|
+
guids: z.array(z.string())
|
|
94
|
+
.min(1)
|
|
95
|
+
.describe('Array of TU GUIDs to translate'),
|
|
96
|
+
instructions: z.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe('Optional instructions for the translation provider')
|
|
99
|
+
})
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
static async execute(mm, args) {
|
|
103
|
+
const { sourceLang, targetLang, channelId, provider, guids, instructions } = args;
|
|
104
|
+
|
|
105
|
+
const availableProviders = mm.dispatcher.providers?.map(p => p.id) ?? [];
|
|
106
|
+
if (!availableProviders.includes(provider)) {
|
|
107
|
+
throw new McpInputError(`Unknown translation provider "${provider}"`, {
|
|
108
|
+
hints: [`Available providers: ${availableProviders.join(', ') || 'none registered'}`]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let tm;
|
|
113
|
+
try {
|
|
114
|
+
tm = mm.tmm.getTM(sourceLang, targetLang);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new McpInputError(`Language pair ${sourceLang}→${targetLang} is not configured`, {
|
|
117
|
+
hints: ['Check translation_status include=["coverage"] to list available language pairs.'],
|
|
118
|
+
cause: error
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let fetchedTUs;
|
|
123
|
+
try {
|
|
124
|
+
fetchedTUs = await tm.queryByGuids(guids, channelId);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new McpToolError('Failed to fetch translation units by GUID', {
|
|
127
|
+
code: 'FETCH_TUS_FAILED',
|
|
128
|
+
details: { channelId, guidsCount: guids.length },
|
|
129
|
+
cause: error
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!fetchedTUs || fetchedTUs.length === 0) {
|
|
134
|
+
throw new McpNotFoundError(`No translation units found for the provided GUIDs in channel "${channelId}"`, {
|
|
135
|
+
hints: [
|
|
136
|
+
'Ensure the GUIDs belong to the specified channel.',
|
|
137
|
+
'Call source_query with a WHERE clause on guid to inspect source content.'
|
|
138
|
+
],
|
|
139
|
+
details: { missingGuids: guids }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sourceTUs = fetchedTUs.map(tu => {
|
|
144
|
+
try {
|
|
145
|
+
return TU.asSource(tu);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new McpToolError(`Failed to create source TU for guid ${tu.guid}`, {
|
|
148
|
+
code: 'SOURCE_TU_CONVERSION_FAILED',
|
|
149
|
+
details: { guid: tu.guid },
|
|
150
|
+
cause: error
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Create job with the source TUs
|
|
156
|
+
const jobRequest = {
|
|
157
|
+
sourceLang,
|
|
158
|
+
targetLang,
|
|
159
|
+
tus: sourceTUs
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Create jobs and assign to provider
|
|
163
|
+
const job = await createJobForProvider(mm, jobRequest, provider, sourceTUs.length, guids);
|
|
164
|
+
|
|
165
|
+
// Start the job
|
|
166
|
+
let jobStatuses;
|
|
167
|
+
try {
|
|
168
|
+
jobStatuses = await mm.dispatcher.startJobs(
|
|
169
|
+
[job],
|
|
170
|
+
{ instructions }
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new McpProviderError(`Failed to start job ${job.jobGuid}`, {
|
|
174
|
+
hints: [
|
|
175
|
+
'Inspect provider logs or credentials.',
|
|
176
|
+
'Retry without instructions to rule out formatting issues.'
|
|
177
|
+
],
|
|
178
|
+
details: { jobGuid: job.jobGuid },
|
|
179
|
+
cause: error,
|
|
180
|
+
retryable: true
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const jobStatus = jobStatuses.find(s => s.jobGuid === job.jobGuid);
|
|
185
|
+
if (!jobStatus) {
|
|
186
|
+
throw new McpProviderError(`Provider "${provider}" did not report status for job ${job.jobGuid}`, {
|
|
187
|
+
retryable: true
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Retrieve the job to get translated TUs
|
|
192
|
+
let completedJob;
|
|
193
|
+
try {
|
|
194
|
+
completedJob = await mm.tmm.getJob(job.jobGuid);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw new McpToolError(`Failed to load job ${job.jobGuid} after start`, {
|
|
197
|
+
code: 'JOB_LOOKUP_FAILED',
|
|
198
|
+
cause: error,
|
|
199
|
+
retryable: true
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!completedJob) {
|
|
204
|
+
throw new McpToolError(`Job ${job.jobGuid} not found after completion`, {
|
|
205
|
+
code: 'JOB_MISSING',
|
|
206
|
+
retryable: true
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Extract translated TUs (those with ntgt, excluding inflight ones)
|
|
211
|
+
// Use the inflight array from the job response to filter
|
|
212
|
+
const translatedTUs = getTranslatedTUs(completedJob);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
jobGuid: completedJob.jobGuid,
|
|
216
|
+
sourceLang: completedJob.sourceLang,
|
|
217
|
+
targetLang: completedJob.targetLang,
|
|
218
|
+
translationProvider: completedJob.translationProvider,
|
|
219
|
+
status: completedJob.status,
|
|
220
|
+
translatedCount: translatedTUs.length,
|
|
221
|
+
inflightCount: completedJob.inflight?.length || 0,
|
|
222
|
+
translatedTUs,
|
|
223
|
+
inflightGuids: completedJob.inflight && completedJob.inflight.length > 0 ? completedJob.inflight : undefined
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|