@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
package/tools/status.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { McpTool, McpNotFoundError } from './mcpTool.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a language pair key in the format "sourceLang→targetLang"
|
|
6
|
+
* @param {string} sourceLang Source language code
|
|
7
|
+
* @param {string} targetLang Target language code
|
|
8
|
+
* @returns {string} Formatted language pair key
|
|
9
|
+
*/
|
|
10
|
+
function makeLangPairKey(sourceLang, targetLang) {
|
|
11
|
+
return `${sourceLang}→${targetLang}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get channel statistics and warm caches.
|
|
16
|
+
* @param {*} mm MonsterManager instance
|
|
17
|
+
* @param {Object} filters Filter object with optional channel property
|
|
18
|
+
* @returns {Promise<{channelStats: Object, channelIds: string[]}>}
|
|
19
|
+
*/
|
|
20
|
+
async function getChannelStats(mm, filters) {
|
|
21
|
+
const channelIds = filters.channel ?
|
|
22
|
+
mm.rm.channelIds.filter(id => id === filters.channel) :
|
|
23
|
+
mm.rm.channelIds;
|
|
24
|
+
|
|
25
|
+
if (filters.channel && channelIds.length === 0) {
|
|
26
|
+
throw new McpNotFoundError(`Channel "${filters.channel}" not found`, {
|
|
27
|
+
hints: [`Available channels: ${mm.rm.channelIds.join(', ')}`]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Warm caches by calling methods that trigger initialization
|
|
32
|
+
const channelStats = {};
|
|
33
|
+
for (const channelId of channelIds) {
|
|
34
|
+
// This call warms the cache
|
|
35
|
+
channelStats[channelId] = await mm.rm.getActiveContentStats(channelId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { channelStats, channelIds };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get desired language pairs from channels.
|
|
43
|
+
* @param {*} mm MonsterManager instance
|
|
44
|
+
* @param {string[]} channelIds List of channel IDs to process
|
|
45
|
+
* @param {Object} filters Filter object with optional sourceLang and targetLang properties
|
|
46
|
+
* @returns {Promise<{desiredPairs: Object}>}
|
|
47
|
+
*/
|
|
48
|
+
async function getDesiredLangPairs(mm, channelIds, filters) {
|
|
49
|
+
const desiredPairs = {};
|
|
50
|
+
const allLangPairs = new Set();
|
|
51
|
+
|
|
52
|
+
for (const channelId of channelIds) {
|
|
53
|
+
const channelLangPairs = await mm.rm.getDesiredLangPairs(channelId);
|
|
54
|
+
for (const [sourceLang, targetLang] of channelLangPairs) {
|
|
55
|
+
// Apply language filters
|
|
56
|
+
if (filters.sourceLang && sourceLang !== filters.sourceLang) continue;
|
|
57
|
+
if (filters.targetLang && targetLang !== filters.targetLang) continue;
|
|
58
|
+
|
|
59
|
+
const pairKey = makeLangPairKey(sourceLang, targetLang);
|
|
60
|
+
if (!allLangPairs.has(pairKey)) {
|
|
61
|
+
allLangPairs.add(pairKey);
|
|
62
|
+
desiredPairs[sourceLang] ??= [];
|
|
63
|
+
desiredPairs[sourceLang].push(targetLang);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { desiredPairs };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get available translation providers with their status.
|
|
73
|
+
* @param {*} mm MonsterManager instance
|
|
74
|
+
* @param {Object} filters Filter object with optional provider property
|
|
75
|
+
* @returns {Promise<Array>} List of provider objects
|
|
76
|
+
*/
|
|
77
|
+
async function getAvailableProviders(mm, filters) {
|
|
78
|
+
const providers = [];
|
|
79
|
+
for (const provider of mm.dispatcher.providers) {
|
|
80
|
+
// Apply provider filter
|
|
81
|
+
if (filters.provider && provider.id !== filters.provider) continue;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const info = await provider.info();
|
|
85
|
+
providers.push({
|
|
86
|
+
id: info.id,
|
|
87
|
+
type: info.type,
|
|
88
|
+
quality: info.quality ?? 'dynamic',
|
|
89
|
+
costPerWord: info.costPerWord ?? 0,
|
|
90
|
+
costPerMChar: info.costPerMChar ?? 0,
|
|
91
|
+
supportedPairs: info.supportedPairs ?? 'any',
|
|
92
|
+
available: true
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
providers.push({
|
|
96
|
+
id: provider.id,
|
|
97
|
+
available: false,
|
|
98
|
+
error: error.message
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (filters.provider && providers.length === 0) {
|
|
104
|
+
throw new McpNotFoundError(`Provider "${filters.provider}" not found`, {
|
|
105
|
+
hints: [`Available providers: ${mm.dispatcher.providers.map(p => p.id).join(', ')}`]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return providers;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get supported language pairs from providers.
|
|
114
|
+
* @param {Array} providers List of provider objects
|
|
115
|
+
* @param {Object} filters Filter object with optional sourceLang and targetLang properties
|
|
116
|
+
* @returns {Object} Supported language pairs by source language
|
|
117
|
+
*/
|
|
118
|
+
function getSupportedLangPairs(providers, filters) {
|
|
119
|
+
const supportedPairs = {};
|
|
120
|
+
const allSupportedPairs = new Set();
|
|
121
|
+
|
|
122
|
+
if (!Array.isArray(providers)) {
|
|
123
|
+
return supportedPairs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const provider of providers) {
|
|
127
|
+
if (!provider.available || !provider.supportedPairs) continue;
|
|
128
|
+
|
|
129
|
+
// Handle different formats of supportedPairs
|
|
130
|
+
if (provider.supportedPairs === 'any') {
|
|
131
|
+
// Provider supports any language pair, we can't enumerate them
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(provider.supportedPairs)) {
|
|
136
|
+
// Format: [["en", "es"], ["en", "fr"], ...]
|
|
137
|
+
for (const [sourceLang, targetLang] of provider.supportedPairs) {
|
|
138
|
+
// Apply language filters
|
|
139
|
+
if (filters.sourceLang && sourceLang !== filters.sourceLang) continue;
|
|
140
|
+
if (filters.targetLang && targetLang !== filters.targetLang) continue;
|
|
141
|
+
|
|
142
|
+
const pairKey = makeLangPairKey(sourceLang, targetLang);
|
|
143
|
+
if (!allSupportedPairs.has(pairKey)) {
|
|
144
|
+
allSupportedPairs.add(pairKey);
|
|
145
|
+
supportedPairs[sourceLang] ??= [];
|
|
146
|
+
if (!supportedPairs[sourceLang].includes(targetLang)) {
|
|
147
|
+
supportedPairs[sourceLang].push(targetLang);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return supportedPairs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get translation memory statistics for available language pairs.
|
|
159
|
+
* @param {*} mm MonsterManager instance
|
|
160
|
+
* @param {Object} filters Filter object with optional provider, sourceLang, and targetLang properties
|
|
161
|
+
* @returns {Promise<{tmStats: Object, availableLangPairs: Array}>}
|
|
162
|
+
*/
|
|
163
|
+
async function getTranslationMemoryStats(mm, filters) {
|
|
164
|
+
const tmStats = {};
|
|
165
|
+
const allAvailableLangPairs = await mm.tmm.getAvailableLangPairs();
|
|
166
|
+
|
|
167
|
+
// Filter language pairs
|
|
168
|
+
const availableLangPairs = allAvailableLangPairs.filter(([sourceLang, targetLang]) => {
|
|
169
|
+
if (filters.sourceLang && sourceLang !== filters.sourceLang) return false;
|
|
170
|
+
if (filters.targetLang && targetLang !== filters.targetLang) return false;
|
|
171
|
+
return true;
|
|
172
|
+
}).sort();
|
|
173
|
+
|
|
174
|
+
for (const [sourceLang, targetLang] of availableLangPairs) {
|
|
175
|
+
const tm = mm.tmm.getTM(sourceLang, targetLang);
|
|
176
|
+
const stats = await tm.getStats();
|
|
177
|
+
const pairKey = makeLangPairKey(sourceLang, targetLang);
|
|
178
|
+
|
|
179
|
+
// Apply provider filter to TM stats
|
|
180
|
+
const filteredStats = filters.provider ?
|
|
181
|
+
stats.filter(s => s.translationProvider === filters.provider) :
|
|
182
|
+
stats;
|
|
183
|
+
|
|
184
|
+
tmStats[pairKey] = filteredStats.map(s => ({
|
|
185
|
+
translationProvider: s.translationProvider,
|
|
186
|
+
status: s.status,
|
|
187
|
+
jobCount: s.jobCount,
|
|
188
|
+
tuCount: s.tuCount,
|
|
189
|
+
redundancy: s.distinctGuids > 0 ? (s.tuCount / s.distinctGuids - 1) : 0
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { tmStats, availableLangPairs };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get translation status filtered by channel and language.
|
|
198
|
+
* @param {*} mm MonsterManager instance
|
|
199
|
+
* @param {Object} filters Filter object with optional channel, sourceLang, and targetLang properties
|
|
200
|
+
* @returns {Promise<{translationStatus: Object}>}
|
|
201
|
+
*/
|
|
202
|
+
async function getTranslationStatus(mm, filters) {
|
|
203
|
+
// Structure: channelId -> sourceLang -> targetLang -> prj -> { details, pairSummary, pairSummaryByStatus }
|
|
204
|
+
const translationStatusRaw = await mm.getTranslationStatus();
|
|
205
|
+
|
|
206
|
+
// Transform to sourceLang -> targetLang -> channelId -> prj structure for easier processing
|
|
207
|
+
// Apply filters during transformation
|
|
208
|
+
const translationStatus = {};
|
|
209
|
+
for (const [channelId, channelData] of Object.entries(translationStatusRaw)) {
|
|
210
|
+
// Apply channel filter
|
|
211
|
+
if (filters.channel && channelId !== filters.channel) continue;
|
|
212
|
+
|
|
213
|
+
for (const [sourceLang, sourceData] of Object.entries(channelData)) {
|
|
214
|
+
// Apply source language filter
|
|
215
|
+
if (filters.sourceLang && sourceLang !== filters.sourceLang) continue;
|
|
216
|
+
|
|
217
|
+
translationStatus[sourceLang] ??= {};
|
|
218
|
+
for (const [targetLang, targetData] of Object.entries(sourceData)) {
|
|
219
|
+
// Apply target language filter
|
|
220
|
+
if (filters.targetLang && targetLang !== filters.targetLang) continue;
|
|
221
|
+
|
|
222
|
+
translationStatus[sourceLang][targetLang] ??= {};
|
|
223
|
+
translationStatus[sourceLang][targetLang][channelId] = targetData;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { translationStatus };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Compute coverage metrics from translation status.
|
|
233
|
+
* @param {Object} translationStatus Filtered translation status structure
|
|
234
|
+
* @returns {Object} Coverage metrics including overall and by-pair breakdowns
|
|
235
|
+
*/
|
|
236
|
+
function computeCoverage(translationStatus) {
|
|
237
|
+
let totalSegments = 0;
|
|
238
|
+
let translatedSegments = 0;
|
|
239
|
+
let untranslatedSegments = 0;
|
|
240
|
+
let inFlightSegments = 0;
|
|
241
|
+
let lowQualitySegments = 0;
|
|
242
|
+
|
|
243
|
+
const coverageByPair = {};
|
|
244
|
+
|
|
245
|
+
for (const [sourceLang, sourceStatus] of Object.entries(translationStatus)) {
|
|
246
|
+
for (const [targetLang, channelStatus] of Object.entries(sourceStatus)) {
|
|
247
|
+
let pairSegments = 0;
|
|
248
|
+
let pairTranslated = 0;
|
|
249
|
+
let pairUntranslated = 0;
|
|
250
|
+
let pairInFlight = 0;
|
|
251
|
+
let pairLowQuality = 0;
|
|
252
|
+
|
|
253
|
+
for (const projectStatus of Object.values(channelStatus)) {
|
|
254
|
+
for (const { pairSummary, pairSummaryByStatus } of Object.values(projectStatus)) {
|
|
255
|
+
pairSegments += pairSummary.segs;
|
|
256
|
+
pairTranslated += pairSummaryByStatus.translated || 0;
|
|
257
|
+
pairUntranslated += pairSummaryByStatus.untranslated || 0;
|
|
258
|
+
pairInFlight += pairSummaryByStatus['in flight'] || 0;
|
|
259
|
+
pairLowQuality += pairSummaryByStatus['low quality'] || 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
totalSegments += pairSegments;
|
|
264
|
+
translatedSegments += pairTranslated;
|
|
265
|
+
untranslatedSegments += pairUntranslated;
|
|
266
|
+
inFlightSegments += pairInFlight;
|
|
267
|
+
lowQualitySegments += pairLowQuality;
|
|
268
|
+
|
|
269
|
+
const coverage = pairSegments > 0 ? pairTranslated / pairSegments : 0;
|
|
270
|
+
const pairKey = makeLangPairKey(sourceLang, targetLang);
|
|
271
|
+
coverageByPair[pairKey] = {
|
|
272
|
+
total: pairSegments,
|
|
273
|
+
translated: pairTranslated,
|
|
274
|
+
untranslated: pairUntranslated,
|
|
275
|
+
inFlight: pairInFlight,
|
|
276
|
+
lowQuality: pairLowQuality,
|
|
277
|
+
coverage
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const overallCoverage = totalSegments > 0 ? translatedSegments / totalSegments : 0;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
overall: {
|
|
286
|
+
total: totalSegments,
|
|
287
|
+
translated: translatedSegments,
|
|
288
|
+
untranslated: untranslatedSegments,
|
|
289
|
+
inFlight: inFlightSegments,
|
|
290
|
+
lowQuality: lowQualitySegments,
|
|
291
|
+
coverage: overallCoverage
|
|
292
|
+
},
|
|
293
|
+
byPair: coverageByPair,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get job summaries including pending and recent jobs.
|
|
299
|
+
* @param {*} mm MonsterManager instance
|
|
300
|
+
* @param {Object} filters Filter object with optional provider property
|
|
301
|
+
* @param {Array} availableLangPairs List of [sourceLang, targetLang] pairs
|
|
302
|
+
* @returns {Promise<{pendingJobs: Array, recentJobs: Array, jobCountsByPair: Object}>}
|
|
303
|
+
*/
|
|
304
|
+
async function getJobSummaries(mm, filters, availableLangPairs) {
|
|
305
|
+
const pendingJobs = [];
|
|
306
|
+
const recentJobs = [];
|
|
307
|
+
const jobCountsByPair = {};
|
|
308
|
+
|
|
309
|
+
for (const [sourceLang, targetLang] of availableLangPairs) {
|
|
310
|
+
const allJobs = await mm.tmm.getJobTOCByLangPair(sourceLang, targetLang);
|
|
311
|
+
|
|
312
|
+
// Apply provider filter to jobs
|
|
313
|
+
const filteredJobs = filters.provider ?
|
|
314
|
+
allJobs.filter(job => job.translationProvider === filters.provider) :
|
|
315
|
+
allJobs;
|
|
316
|
+
|
|
317
|
+
const unfinished = filteredJobs.filter(job => job.status !== 'done');
|
|
318
|
+
const recent = filteredJobs
|
|
319
|
+
.filter(job => {
|
|
320
|
+
const updatedAt = new Date(job.updatedAt);
|
|
321
|
+
const weekAgo = new Date();
|
|
322
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
323
|
+
return updatedAt >= weekAgo;
|
|
324
|
+
})
|
|
325
|
+
.slice(0, 10);
|
|
326
|
+
|
|
327
|
+
const pairKey = makeLangPairKey(sourceLang, targetLang);
|
|
328
|
+
jobCountsByPair[pairKey] = {
|
|
329
|
+
total: filteredJobs.length,
|
|
330
|
+
unfinished: unfinished.length,
|
|
331
|
+
recent: recent.length
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (unfinished.length > 0) {
|
|
335
|
+
pendingJobs.push(...unfinished.map(job => ({
|
|
336
|
+
pair: pairKey,
|
|
337
|
+
jobGuid: job.jobGuid,
|
|
338
|
+
status: job.status,
|
|
339
|
+
translationProvider: job.translationProvider,
|
|
340
|
+
updatedAt: job.updatedAt
|
|
341
|
+
})));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (recent.length > 0) {
|
|
345
|
+
recentJobs.push(...recent.map(job => ({
|
|
346
|
+
pair: pairKey,
|
|
347
|
+
jobGuid: job.jobGuid,
|
|
348
|
+
status: job.status,
|
|
349
|
+
translationProvider: job.translationProvider,
|
|
350
|
+
updatedAt: job.updatedAt
|
|
351
|
+
})));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { pendingJobs, recentJobs, jobCountsByPair };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function createChannelsMap(channelStats, withDetails) {
|
|
359
|
+
const channelsMap = {};
|
|
360
|
+
for (const [channelId, stats] of Object.entries(channelStats)) {
|
|
361
|
+
channelsMap[channelId] = stats.map(s => ({
|
|
362
|
+
project: s.prj ?? 'default',
|
|
363
|
+
sourceLang: s.sourceLang,
|
|
364
|
+
targetLangs: s.targetLangs || [],
|
|
365
|
+
segmentCount: s.segmentCount,
|
|
366
|
+
lastModified: s.lastModified,
|
|
367
|
+
...(withDetails && { resourceCount: s.resCount })
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
return channelsMap;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function createLanguagePairsMap(desiredPairs, supportedPairs) {
|
|
374
|
+
const result = {};
|
|
375
|
+
|
|
376
|
+
if (Object.keys(desiredPairs).length > 0) {
|
|
377
|
+
const desiredMap = {};
|
|
378
|
+
for (const [sourceLang, targetLangs] of Object.entries(desiredPairs)) {
|
|
379
|
+
desiredMap[sourceLang] = targetLangs;
|
|
380
|
+
}
|
|
381
|
+
result.desired = desiredMap;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (Object.keys(supportedPairs).length > 0) {
|
|
385
|
+
const supportedMap = {};
|
|
386
|
+
for (const [sourceLang, targetLangs] of Object.entries(supportedPairs)) {
|
|
387
|
+
supportedMap[sourceLang] = targetLangs;
|
|
388
|
+
}
|
|
389
|
+
result.supported = supportedMap;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function createProvidersMap(providers, withDetails) {
|
|
396
|
+
const providersMap = {};
|
|
397
|
+
for (const p of providers) {
|
|
398
|
+
providersMap[p.id] = {
|
|
399
|
+
type: p.type,
|
|
400
|
+
available: p.available,
|
|
401
|
+
supportedPairs: p.supportedPairs,
|
|
402
|
+
...(withDetails && {
|
|
403
|
+
quality: p.quality,
|
|
404
|
+
costPerWord: p.costPerWord,
|
|
405
|
+
costPerMChar: p.costPerMChar
|
|
406
|
+
}),
|
|
407
|
+
...(p.error && { error: p.error })
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return providersMap;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function createTranslationMemoryMap(tmStats, withDetails) {
|
|
414
|
+
const translationMemoryMap = {};
|
|
415
|
+
for (const [pairKey, stats] of Object.entries(tmStats)) {
|
|
416
|
+
const [sourceLang, targetLang] = pairKey.split('→');
|
|
417
|
+
const langPairKey = makeLangPairKey(sourceLang, targetLang);
|
|
418
|
+
|
|
419
|
+
if (withDetails) {
|
|
420
|
+
const providerMap = {};
|
|
421
|
+
for (const s of stats) {
|
|
422
|
+
providerMap[s.translationProvider] = {
|
|
423
|
+
translationUnitCount: s.tuCount,
|
|
424
|
+
status: s.status,
|
|
425
|
+
jobCount: s.jobCount,
|
|
426
|
+
redundancy: s.redundancy
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
translationMemoryMap[langPairKey] = providerMap;
|
|
430
|
+
} else {
|
|
431
|
+
const totalUnits = stats.reduce((sum, s) => sum + s.tuCount, 0);
|
|
432
|
+
translationMemoryMap[langPairKey] = totalUnits;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return translationMemoryMap;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function createCoverageData(coverage, withDetails) {
|
|
439
|
+
const coverageMap = {};
|
|
440
|
+
for (const [pairKey, pairCoverage] of Object.entries(coverage.byPair)) {
|
|
441
|
+
const [sourceLang, targetLang] = pairKey.split('→');
|
|
442
|
+
const langPairKey = makeLangPairKey(sourceLang, targetLang);
|
|
443
|
+
coverageMap[langPairKey] = {
|
|
444
|
+
totalSegments: pairCoverage.total,
|
|
445
|
+
translated: pairCoverage.translated,
|
|
446
|
+
untranslated: pairCoverage.untranslated,
|
|
447
|
+
coveragePercentage: (pairCoverage.coverage * 100).toFixed(2),
|
|
448
|
+
...(withDetails && {
|
|
449
|
+
inFlight: pairCoverage.inFlight,
|
|
450
|
+
lowQuality: pairCoverage.lowQuality
|
|
451
|
+
})
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
map: coverageMap,
|
|
457
|
+
summary: withDetails ? coverage.overall : undefined
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function createJobsData(jobSummaries) {
|
|
462
|
+
const jobsMap = {};
|
|
463
|
+
const allJobs = jobSummaries.pendingJobs.concat(jobSummaries.recentJobs);
|
|
464
|
+
for (const job of allJobs) {
|
|
465
|
+
const [sourceLang, targetLang] = job.pair.split('→');
|
|
466
|
+
const langPairKey = makeLangPairKey(sourceLang, targetLang);
|
|
467
|
+
if (!jobsMap[langPairKey]) {
|
|
468
|
+
jobsMap[langPairKey] = [];
|
|
469
|
+
}
|
|
470
|
+
jobsMap[langPairKey].push({
|
|
471
|
+
jobGuid: job.jobGuid,
|
|
472
|
+
status: job.status,
|
|
473
|
+
translationProvider: job.translationProvider,
|
|
474
|
+
updatedAt: job.updatedAt
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
jobsMap,
|
|
480
|
+
counts: jobSummaries.jobCountsByPair
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function applyOptionalSections(result, { requestedSections, providers, coverage, jobSummaries, withDetails, channelStats, mm }) {
|
|
485
|
+
if (requestedSections.has('providers') && Array.isArray(providers)) {
|
|
486
|
+
result.providers = createProvidersMap(providers, withDetails);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (requestedSections.has('coverage') && coverage) {
|
|
490
|
+
const coverageData = createCoverageData(coverage, withDetails);
|
|
491
|
+
result.coverage = coverageData.map;
|
|
492
|
+
if (coverageData.summary) {
|
|
493
|
+
result.coverageSummary = coverageData.summary;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (requestedSections.has('jobs') && jobSummaries) {
|
|
498
|
+
const jobsData = createJobsData(jobSummaries);
|
|
499
|
+
result.jobs = jobsData.jobsMap;
|
|
500
|
+
if (jobsData.counts) {
|
|
501
|
+
result.jobCounts = jobsData.counts;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (withDetails) {
|
|
506
|
+
result.details = {
|
|
507
|
+
channelCount: Object.keys(channelStats).length,
|
|
508
|
+
autoSnap: mm.rm.autoSnap
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Build the final response object.
|
|
515
|
+
* @param {Object} params Parameters object
|
|
516
|
+
* @param {Object} params.channelStats Channel statistics
|
|
517
|
+
* @param {Object} params.desiredPairs Desired language pairs by source language
|
|
518
|
+
* @param {Object} params.supportedPairs Supported language pairs by source language from providers
|
|
519
|
+
* @param {Array} params.providers List of providers
|
|
520
|
+
* @param {Object} params.tmStats Translation memory statistics
|
|
521
|
+
* @param {Object} params.coverage Coverage metrics
|
|
522
|
+
* @param {Object} params.jobSummaries Job summaries
|
|
523
|
+
* @param {boolean} params.withDetails Whether to include detailed information
|
|
524
|
+
* @param {Set<string>} params.includeSet Requested optional sections
|
|
525
|
+
* @param {*} params.mm MonsterManager instance
|
|
526
|
+
* @returns {Object} Complete response object
|
|
527
|
+
*/
|
|
528
|
+
function buildResponse({ channelStats, desiredPairs, supportedPairs, providers, tmStats, coverage, jobSummaries, withDetails, includeSet, mm }) {
|
|
529
|
+
const requestedSections = includeSet instanceof Set ? includeSet : new Set(includeSet ?? []);
|
|
530
|
+
|
|
531
|
+
const result = {
|
|
532
|
+
timestamp: new Date().toISOString()
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Add base sections if requested
|
|
536
|
+
if (requestedSections.has('channels')) {
|
|
537
|
+
result.channels = createChannelsMap(channelStats, withDetails);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (requestedSections.has('languagePairs')) {
|
|
541
|
+
result.languagePairs = createLanguagePairsMap(desiredPairs, supportedPairs);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (requestedSections.has('translationMemory')) {
|
|
545
|
+
result.translationMemory = createTranslationMemoryMap(tmStats, withDetails);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Add optional sections
|
|
549
|
+
applyOptionalSections(result, {
|
|
550
|
+
requestedSections,
|
|
551
|
+
providers,
|
|
552
|
+
coverage,
|
|
553
|
+
jobSummaries,
|
|
554
|
+
withDetails,
|
|
555
|
+
channelStats,
|
|
556
|
+
mm
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* MCP tool for assembling the status of the localization system.
|
|
565
|
+
*
|
|
566
|
+
* This tool chains together functionality from monster, source_list (status mode),
|
|
567
|
+
* and ops_jobs to provide a comprehensive overview including:
|
|
568
|
+
* - Channel statistics
|
|
569
|
+
* - Language pairs
|
|
570
|
+
* - Translation memory and coverage
|
|
571
|
+
* - Provider availability
|
|
572
|
+
* - Job summaries
|
|
573
|
+
*
|
|
574
|
+
* Supports optional filtering by:
|
|
575
|
+
* - channel: Filter to specific channel ID (default: all channels)
|
|
576
|
+
* - provider: Filter to specific translation provider (default: all providers)
|
|
577
|
+
* - sourceLang: Filter to specific source language (default: all languages)
|
|
578
|
+
* - targetLang: Filter to specific target language (default: all languages)
|
|
579
|
+
*/
|
|
580
|
+
export class StatusTool extends McpTool {
|
|
581
|
+
static metadata = {
|
|
582
|
+
name: 'status',
|
|
583
|
+
description: `Assemble the status of the l10nmonster translation system as a whole or specific parts of it.
|
|
584
|
+
The result may include any of these sections: translation channels, projects, jobs, memory, coverage, providers and supported language pairs.
|
|
585
|
+
The result can be further refined by filtering by channel, provider, source language, and target language.
|
|
586
|
+
`,
|
|
587
|
+
inputSchema: z.object({
|
|
588
|
+
detailLevel: z.enum(['summary', 'detailed'])
|
|
589
|
+
.default('summary')
|
|
590
|
+
.describe('Controls response verbosity: "summary" omits secondary fields, "detailed" includes them'),
|
|
591
|
+
include: z.array(z.enum(['channels', 'providers', 'languagePairs', 'translationMemory', 'coverage', 'jobs']))
|
|
592
|
+
.default(['channels', 'providers', 'languagePairs'])
|
|
593
|
+
.describe('Optional sections to include in the response. Defaults to ["channels", "providers", "languagePairs"]'),
|
|
594
|
+
channel: z.string()
|
|
595
|
+
.optional()
|
|
596
|
+
.describe('Optional channel ID to filter results. Defaults to all channels.'),
|
|
597
|
+
provider: z.string()
|
|
598
|
+
.optional()
|
|
599
|
+
.describe('Optional provider ID to filter results. Defaults to all providers.'),
|
|
600
|
+
sourceLang: z.string()
|
|
601
|
+
.optional()
|
|
602
|
+
.describe('Optional source language to filter results. Defaults to all source languages.'),
|
|
603
|
+
targetLang: z.string()
|
|
604
|
+
.optional()
|
|
605
|
+
.describe('Optional target language to filter results. Defaults to all target languages.'),
|
|
606
|
+
})
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
static async execute(mm, args) {
|
|
610
|
+
const filters = {
|
|
611
|
+
channel: args.channel,
|
|
612
|
+
provider: args.provider,
|
|
613
|
+
sourceLang: args.sourceLang,
|
|
614
|
+
targetLang: args.targetLang
|
|
615
|
+
};
|
|
616
|
+
const withDetails = args.detailLevel === 'detailed';
|
|
617
|
+
const includeSet = new Set(args.include);
|
|
618
|
+
|
|
619
|
+
// Gather all data using helper functions
|
|
620
|
+
const { channelStats, channelIds } = await getChannelStats(mm, filters);
|
|
621
|
+
|
|
622
|
+
// Get providers if needed for languagePairs:supported or providers section
|
|
623
|
+
const needsProviders = includeSet.has('providers') || includeSet.has('languagePairs');
|
|
624
|
+
const providers = needsProviders ?
|
|
625
|
+
await getAvailableProviders(mm, filters) :
|
|
626
|
+
null;
|
|
627
|
+
|
|
628
|
+
const desiredPairs = includeSet.has('languagePairs') ?
|
|
629
|
+
(await getDesiredLangPairs(mm, channelIds, filters)).desiredPairs :
|
|
630
|
+
{};
|
|
631
|
+
|
|
632
|
+
const supportedPairs = includeSet.has('languagePairs') ?
|
|
633
|
+
getSupportedLangPairs(providers, filters) :
|
|
634
|
+
{};
|
|
635
|
+
|
|
636
|
+
// Get TM stats if translationMemory, coverage, or jobs is requested
|
|
637
|
+
const needsTmStats = includeSet.has('translationMemory') || includeSet.has('coverage') || includeSet.has('jobs');
|
|
638
|
+
const { tmStats, availableLangPairs } = needsTmStats ?
|
|
639
|
+
await getTranslationMemoryStats(mm, filters) :
|
|
640
|
+
{ tmStats: {}, availableLangPairs: [] };
|
|
641
|
+
|
|
642
|
+
const coverage = includeSet.has('coverage') ?
|
|
643
|
+
computeCoverage((await getTranslationStatus(mm, filters)).translationStatus) :
|
|
644
|
+
null;
|
|
645
|
+
|
|
646
|
+
const jobSummaries = includeSet.has('jobs') ?
|
|
647
|
+
await getJobSummaries(mm, filters, availableLangPairs) :
|
|
648
|
+
null;
|
|
649
|
+
|
|
650
|
+
// Build and return response
|
|
651
|
+
return buildResponse({
|
|
652
|
+
channelStats,
|
|
653
|
+
desiredPairs,
|
|
654
|
+
supportedPairs,
|
|
655
|
+
providers,
|
|
656
|
+
tmStats,
|
|
657
|
+
coverage,
|
|
658
|
+
jobSummaries,
|
|
659
|
+
withDetails,
|
|
660
|
+
includeSet,
|
|
661
|
+
mm
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|