@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,169 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { registry } from '../tools/registry.js';
|
|
4
|
+
import { McpTool } from '../tools/mcpTool.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// Mock tool classes for testing
|
|
8
|
+
class TestTool1 extends McpTool {
|
|
9
|
+
static metadata = {
|
|
10
|
+
name: 'test_tool_1',
|
|
11
|
+
description: 'Test tool 1',
|
|
12
|
+
inputSchema: z.object({})
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
static async execute() {
|
|
16
|
+
return { result: 'test1' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static handler() {
|
|
20
|
+
return async () => this.formatResult({ result: 'test1' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TestTool2 extends McpTool {
|
|
25
|
+
static metadata = {
|
|
26
|
+
name: 'test_tool_2',
|
|
27
|
+
description: 'Test tool 2',
|
|
28
|
+
inputSchema: z.object({})
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
static async execute() {
|
|
32
|
+
return { result: 'test2' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static handler() {
|
|
36
|
+
return async () => this.formatResult({ result: 'test2' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class OverrideTool extends McpTool {
|
|
41
|
+
static metadata = {
|
|
42
|
+
name: 'test_tool_1', // Same name as TestTool1
|
|
43
|
+
description: 'Override tool',
|
|
44
|
+
inputSchema: z.object({})
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
static async execute() {
|
|
48
|
+
return { result: 'override' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static handler() {
|
|
52
|
+
return async () => this.formatResult({ result: 'override' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('MCP Registry', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
// Clear registry before each test
|
|
59
|
+
registry.clear();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should register a single tool', () => {
|
|
63
|
+
registry.registerTool(TestTool1);
|
|
64
|
+
|
|
65
|
+
assert.strictEqual(registry.hasTool('test_tool_1'), true);
|
|
66
|
+
assert.strictEqual(registry.getTool('test_tool_1'), TestTool1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should register multiple tools', () => {
|
|
70
|
+
registry.registerTools([TestTool1, TestTool2]);
|
|
71
|
+
|
|
72
|
+
assert.strictEqual(registry.hasTool('test_tool_1'), true);
|
|
73
|
+
assert.strictEqual(registry.hasTool('test_tool_2'), true);
|
|
74
|
+
|
|
75
|
+
const allTools = registry.getAllTools();
|
|
76
|
+
assert.strictEqual(allTools.length, 2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should override existing tool with same name', () => {
|
|
80
|
+
registry.registerTool(TestTool1);
|
|
81
|
+
registry.registerTool(OverrideTool);
|
|
82
|
+
|
|
83
|
+
const tool = registry.getTool('test_tool_1');
|
|
84
|
+
assert.strictEqual(tool, OverrideTool);
|
|
85
|
+
assert.notStrictEqual(tool, TestTool1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return all registered tools', () => {
|
|
89
|
+
registry.registerTools([TestTool1, TestTool2]);
|
|
90
|
+
|
|
91
|
+
const allTools = registry.getAllTools();
|
|
92
|
+
assert.strictEqual(allTools.length, 2);
|
|
93
|
+
assert.ok(allTools.includes(TestTool1));
|
|
94
|
+
assert.ok(allTools.includes(TestTool2));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return undefined for non-existent tool', () => {
|
|
98
|
+
const tool = registry.getTool('non_existent');
|
|
99
|
+
assert.strictEqual(tool, undefined);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return false for non-existent tool check', () => {
|
|
103
|
+
assert.strictEqual(registry.hasTool('non_existent'), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should clear all registered tools', () => {
|
|
107
|
+
registry.registerTools([TestTool1, TestTool2]);
|
|
108
|
+
assert.strictEqual(registry.getAllTools().length, 2);
|
|
109
|
+
|
|
110
|
+
registry.clear();
|
|
111
|
+
assert.strictEqual(registry.getAllTools().length, 0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should throw error for invalid tool class', () => {
|
|
115
|
+
assert.throws(
|
|
116
|
+
() => registry.registerTool(null),
|
|
117
|
+
/ToolClass must be a class\/function/
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
assert.throws(
|
|
121
|
+
() => registry.registerTool('not a class'),
|
|
122
|
+
/ToolClass must be a class\/function/
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should throw error for tool without metadata', () => {
|
|
127
|
+
class InvalidTool {}
|
|
128
|
+
|
|
129
|
+
assert.throws(
|
|
130
|
+
() => registry.registerTool(InvalidTool),
|
|
131
|
+
/must have static metadata property/
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should throw error for tool without name in metadata', () => {
|
|
136
|
+
class InvalidTool {
|
|
137
|
+
static metadata = {
|
|
138
|
+
description: 'No name'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
assert.throws(
|
|
143
|
+
() => registry.registerTool(InvalidTool),
|
|
144
|
+
/metadata must have a string 'name' property/
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should throw error for tool without handler method', () => {
|
|
149
|
+
class InvalidTool {
|
|
150
|
+
static metadata = {
|
|
151
|
+
name: 'invalid',
|
|
152
|
+
description: 'No handler'
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
assert.throws(
|
|
157
|
+
() => registry.registerTool(InvalidTool),
|
|
158
|
+
/must have static handler method/
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should throw error when registerTools receives non-array', () => {
|
|
163
|
+
assert.throws(
|
|
164
|
+
() => registry.registerTools('not an array'),
|
|
165
|
+
/toolClasses must be an array/
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
package/tools/index.js
ADDED
package/tools/mcpTool.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base error type for MCP tools with structured metadata.
|
|
5
|
+
*/
|
|
6
|
+
export class McpToolError extends Error {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} message Error message
|
|
10
|
+
* @param {Object} [options]
|
|
11
|
+
* @param {string} [options.code] Stable machine readable error code
|
|
12
|
+
* @param {boolean} [options.retryable=false] Whether the caller can retry safely
|
|
13
|
+
* @param {string[]} [options.hints] Recovery hints for the caller
|
|
14
|
+
* @param {unknown} [options.details] Arbitrary structured payload with extra context
|
|
15
|
+
* @param {Error} [options.cause] Underlying error
|
|
16
|
+
*/
|
|
17
|
+
constructor(message, { code, retryable = false, hints, details, cause } = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = this.constructor.name;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.retryable = retryable;
|
|
22
|
+
this.hints = hints;
|
|
23
|
+
this.details = details;
|
|
24
|
+
if (cause) {
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static wrap(error, defaults = {}) {
|
|
30
|
+
if (error instanceof McpToolError) {
|
|
31
|
+
return error;
|
|
32
|
+
}
|
|
33
|
+
return new McpToolError(error?.message ?? 'Unexpected error', {
|
|
34
|
+
cause: error,
|
|
35
|
+
...defaults
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class McpInputError extends McpToolError {
|
|
41
|
+
constructor(message, options = {}) {
|
|
42
|
+
super(message, { code: 'INVALID_INPUT', ...options });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class McpNotFoundError extends McpToolError {
|
|
47
|
+
constructor(message, options = {}) {
|
|
48
|
+
super(message, { code: 'NOT_FOUND', ...options });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class McpProviderError extends McpToolError {
|
|
53
|
+
constructor(message, options = {}) {
|
|
54
|
+
super(message, { code: 'PROVIDER_ERROR', ...options });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Base class for MCP tools that call underlying MonsterManager functions directly.
|
|
60
|
+
*
|
|
61
|
+
* MCP tools are separate from CLI actions and:
|
|
62
|
+
* - Return structured data (not console output)
|
|
63
|
+
* - Have MCP-optimized schemas (not CLI-optimized)
|
|
64
|
+
* - Call underlying MonsterManager methods directly
|
|
65
|
+
*
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
export class McpTool {
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Static metadata object containing:
|
|
72
|
+
* - name: Tool identifier
|
|
73
|
+
* - description: Tool description for MCP
|
|
74
|
+
* - inputSchema: Zod schema for input validation
|
|
75
|
+
*
|
|
76
|
+
* Subclasses must define this static property.
|
|
77
|
+
*/
|
|
78
|
+
static metadata = {
|
|
79
|
+
name: '',
|
|
80
|
+
description: '',
|
|
81
|
+
inputSchema: z.object({})
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute the tool with validated arguments
|
|
86
|
+
* @param {Object} mm - MonsterManager instance
|
|
87
|
+
* @param {Object} args - Validated arguments from Zod schema
|
|
88
|
+
* @returns {Promise<*>} Result from tool execution (will be formatted by handler)
|
|
89
|
+
*/
|
|
90
|
+
// eslint-disable-next-line no-unused-vars
|
|
91
|
+
static async execute(mm, args) {
|
|
92
|
+
throw new Error('Subclasses must implement execute()');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns an async handler function for MCP tool registration
|
|
97
|
+
* @param {Object} mm - MonsterManager instance
|
|
98
|
+
* @returns {Function} Async handler function
|
|
99
|
+
*/
|
|
100
|
+
static handler(mm) {
|
|
101
|
+
const schema = this.metadata.inputSchema;
|
|
102
|
+
|
|
103
|
+
return async (args) => {
|
|
104
|
+
try {
|
|
105
|
+
const validatedArgs = schema.parse(args);
|
|
106
|
+
const result = await this.execute(mm, validatedArgs);
|
|
107
|
+
return this.formatResult(result);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return this.formatError(error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format a result for MCP response
|
|
116
|
+
* @param {*} result - Result from execute()
|
|
117
|
+
* @returns {Object} MCP-formatted response
|
|
118
|
+
*/
|
|
119
|
+
static formatResult(result) {
|
|
120
|
+
if (result && typeof result === 'object') {
|
|
121
|
+
// Pass through responses that already conform to MCP response shape
|
|
122
|
+
if (Array.isArray(result.content)) {
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: [{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: JSON.stringify(result, null, 2)
|
|
129
|
+
}]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const text = typeof result === 'string' ? result : String(result);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
content: [{
|
|
137
|
+
type: 'text',
|
|
138
|
+
text
|
|
139
|
+
}]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format an error for MCP response
|
|
145
|
+
* @param {Error} error - Error that occurred
|
|
146
|
+
* @returns {Object} MCP-formatted error response
|
|
147
|
+
*/
|
|
148
|
+
static formatError(error) {
|
|
149
|
+
const normalized = McpToolError.wrap(error, { code: 'UNKNOWN_ERROR' });
|
|
150
|
+
const summary = `Error executing ${this.metadata.name}: ${normalized.message}`;
|
|
151
|
+
|
|
152
|
+
const payload = {
|
|
153
|
+
name: normalized.name,
|
|
154
|
+
message: normalized.message,
|
|
155
|
+
code: normalized.code ?? 'UNKNOWN_ERROR',
|
|
156
|
+
retryable: Boolean(normalized.retryable),
|
|
157
|
+
hints: normalized.hints ?? [],
|
|
158
|
+
details: normalized.details
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const cause = normalized.cause;
|
|
162
|
+
if (cause && typeof cause === 'object') {
|
|
163
|
+
let causeName;
|
|
164
|
+
if ('name' in cause && typeof cause.name === 'string') {
|
|
165
|
+
causeName = cause.name;
|
|
166
|
+
} else if (cause.constructor && typeof cause.constructor.name === 'string') {
|
|
167
|
+
causeName = cause.constructor.name;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let causeMessage;
|
|
171
|
+
if ('message' in cause && typeof cause.message === 'string') {
|
|
172
|
+
causeMessage = cause.message;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
payload.cause = {
|
|
176
|
+
name: causeName,
|
|
177
|
+
message: causeMessage
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (error instanceof z.ZodError) {
|
|
182
|
+
payload.code = 'INVALID_INPUT';
|
|
183
|
+
payload.details = {
|
|
184
|
+
issues: error.issues.map(issue => ({
|
|
185
|
+
path: issue.path.join('.') || '(root)',
|
|
186
|
+
message: issue.message,
|
|
187
|
+
code: issue.code
|
|
188
|
+
}))
|
|
189
|
+
};
|
|
190
|
+
if (!payload.hints || payload.hints.length === 0) {
|
|
191
|
+
payload.hints = [
|
|
192
|
+
'Inspect the schema and ensure all required fields are provided.',
|
|
193
|
+
'Check enum values and optional defaults.'
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (typeof normalized.stack === 'string') {
|
|
199
|
+
payload.stack = normalized.stack;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: summary
|
|
206
|
+
}, {
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: JSON.stringify(payload, null, 2)
|
|
209
|
+
}],
|
|
210
|
+
isError: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Registry
|
|
3
|
+
*
|
|
4
|
+
* Provides a registration mechanism for extending MCP with custom tools.
|
|
5
|
+
* External packages can register their own McpTool subclasses which will be
|
|
6
|
+
* automatically discovered and registered when the MCP server starts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class Registry {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.registeredTools = new Map(); // toolName -> ToolClass
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerTool(ToolClass) {
|
|
15
|
+
if (!ToolClass || typeof ToolClass !== 'function') {
|
|
16
|
+
throw new Error('registerTool: ToolClass must be a class/function');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!ToolClass.metadata || typeof ToolClass.metadata !== 'object') {
|
|
20
|
+
throw new Error(`registerTool: ${ToolClass.name} must have static metadata property`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!ToolClass.metadata.name || typeof ToolClass.metadata.name !== 'string') {
|
|
24
|
+
throw new Error(`registerTool: ${ToolClass.name} metadata must have a string 'name' property`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof ToolClass.handler !== 'function') {
|
|
28
|
+
throw new Error(`registerTool: ${ToolClass.name} must have static handler method`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const toolName = ToolClass.metadata.name;
|
|
32
|
+
|
|
33
|
+
if (this.registeredTools.has(toolName)) {
|
|
34
|
+
console.warn(`MCP tool "${toolName}" is already registered. Overwriting with new implementation.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.registeredTools.set(toolName, ToolClass);
|
|
38
|
+
console.info(`Registered MCP tool: ${toolName}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
registerTools(toolClasses) {
|
|
42
|
+
if (!Array.isArray(toolClasses)) {
|
|
43
|
+
throw new Error('registerTools: toolClasses must be an array');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const ToolClass of toolClasses) {
|
|
47
|
+
this.registerTool(ToolClass);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getAllTools() {
|
|
52
|
+
return Array.from(this.registeredTools.values());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getTool(toolName) {
|
|
56
|
+
return this.registeredTools.get(toolName);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
hasTool(toolName) {
|
|
60
|
+
return this.registeredTools.has(toolName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear() {
|
|
64
|
+
this.registeredTools.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const registry = new Registry();
|
|
69
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { McpTool, McpInputError, McpNotFoundError, McpToolError } from './mcpTool.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP tool for querying source content and translation memory.
|
|
6
|
+
*
|
|
7
|
+
* This tool calls the underlying MonsterManager methods directly,
|
|
8
|
+
* avoiding CLI-specific concerns like console logging and file I/O.
|
|
9
|
+
*/
|
|
10
|
+
export class SourceQueryTool extends McpTool {
|
|
11
|
+
static metadata = {
|
|
12
|
+
name: 'source_query',
|
|
13
|
+
description: `Query sources in the source snapshot.
|
|
14
|
+
You can write your own where conditions using SQL syntaxt against the following columns:
|
|
15
|
+
- channel: Channel id
|
|
16
|
+
- prj: Project id
|
|
17
|
+
- rid: Resource id
|
|
18
|
+
- sid: Segment id
|
|
19
|
+
- guid: Segment guid
|
|
20
|
+
- nsrc: Normalized source
|
|
21
|
+
- minQ: Desired minimum quality
|
|
22
|
+
- ntgt: Normalized translation (if available)
|
|
23
|
+
- q: Quality score (if translation is available)
|
|
24
|
+
- notes: Notes object (if any)
|
|
25
|
+
- mf: Message format id
|
|
26
|
+
- segProps: Non-standard segment properties object (if any)
|
|
27
|
+
- words: Word count
|
|
28
|
+
- chars: Character count`,
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
sourceLang: z.string()
|
|
31
|
+
.describe('Source language code (e.g., "en-US")'),
|
|
32
|
+
targetLang: z.string()
|
|
33
|
+
.describe('Target language code (e.g., "es-419")'),
|
|
34
|
+
channel: z.string()
|
|
35
|
+
.describe('Channel ID to query sources from'),
|
|
36
|
+
whereCondition: z.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('SQL WHERE condition against sources (default: "true" to match all)'),
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
static async execute(mm, args) {
|
|
43
|
+
const { sourceLang, targetLang, channel: channelId } = args;
|
|
44
|
+
const whereCondition = args.whereCondition ?? 'true';
|
|
45
|
+
|
|
46
|
+
const availableChannels = mm.rm.channelIds ?? [];
|
|
47
|
+
if (availableChannels.length > 0 && !availableChannels.includes(channelId)) {
|
|
48
|
+
throw new McpNotFoundError(`Channel "${channelId}" not found`, {
|
|
49
|
+
hints: [`Available channels: ${availableChannels.join(', ')}`]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get translation memory and query sources
|
|
54
|
+
let tm;
|
|
55
|
+
try {
|
|
56
|
+
tm = mm.tmm.getTM(sourceLang, targetLang);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new McpInputError(`Language pair ${sourceLang}→${targetLang} is not available`, {
|
|
59
|
+
hints: ['Call translation_status with include=["coverage"] to inspect available language pairs.'],
|
|
60
|
+
cause: error
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let tus;
|
|
65
|
+
try {
|
|
66
|
+
tus = await tm.querySource(channelId, whereCondition);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new McpToolError('Failed to execute query against source snapshot', {
|
|
69
|
+
code: 'QUERY_FAILED',
|
|
70
|
+
hints: [
|
|
71
|
+
'Verify that your SQL WHERE clause only references supported columns.',
|
|
72
|
+
'Escaping: wrap string literals in single quotes.'
|
|
73
|
+
],
|
|
74
|
+
details: { channelId, whereCondition },
|
|
75
|
+
cause: error
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
message: tus.length === 0 ? 'No content returned for the specified query' : `Found ${tus.length} translation units`,
|
|
81
|
+
sourceLang,
|
|
82
|
+
targetLang,
|
|
83
|
+
translationUnits: tus,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|