@syntholoc/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,850 @@
1
+ // src/build-server.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+
4
+ // src/prompts.ts
5
+ import { z } from "zod";
6
+ function userMessage(text) {
7
+ return { messages: [{ role: "user", content: { type: "text", text } }] };
8
+ }
9
+ function registerPrompts(server) {
10
+ server.registerPrompt(
11
+ "localize_changed",
12
+ {
13
+ title: "Localize changed keys",
14
+ description: "Sync changed source strings from the local diff, translate them, and write back.",
15
+ argsSchema: { projectId: z.string().optional() }
16
+ },
17
+ ({ projectId }) => userMessage(
18
+ [
19
+ "Localize my recent source-string changes via Syntholoc:",
20
+ "1. Read `.syntholoc/config.json` for the source file(s) and project.",
21
+ projectId ? ` (Use project ${projectId} if config is missing.)` : "",
22
+ "2. From `git diff` on the source file(s), collect added/changed keys and `upsert_keys` (dryRun first to preview).",
23
+ "3. Call `get_config` for `maxBulkSize`. For EACH target locale check `locale_progress` \u2192 `untranslated`.",
24
+ " - If untranslated >= maxBulkSize: `translate_locale` (scope UNTRANSLATED) and `wait_for_job`.",
25
+ " - If below the threshold: tell me the counts and ASK whether to translate now or accumulate.",
26
+ "4. `export_locale` for each translated locale and write the result back into the mapped local files.",
27
+ "5. Summarize keys added, locales translated, and the cost from the job usage."
28
+ ].filter(Boolean).join("\n")
29
+ )
30
+ );
31
+ server.registerPrompt(
32
+ "review_locale",
33
+ {
34
+ title: "Review a locale",
35
+ description: "Surface low-confidence and QA-flagged translations for a locale and help fix them.",
36
+ argsSchema: { projectId: z.string(), locale: z.string() }
37
+ },
38
+ ({ projectId, locale }) => userMessage(
39
+ [
40
+ `Review the "${locale}" locale in project ${projectId}:`,
41
+ "1. Resolve the locale id via `list_locales`.",
42
+ "2. `list_qa_issues` and `list_translations` (focus on low qaScore / non-approved).",
43
+ "3. For each problem, propose a fix; apply with `set_translation`, then `approve_translation`.",
44
+ "4. Summarize what changed and what still needs human attention."
45
+ ].join("\n")
46
+ )
47
+ );
48
+ server.registerPrompt(
49
+ "bootstrap_project",
50
+ {
51
+ title: "Bootstrap a project from this repo",
52
+ description: "Detect i18n files, create a Syntholoc project, seed locales, and write .syntholoc/config.json.",
53
+ argsSchema: {}
54
+ },
55
+ () => userMessage(
56
+ [
57
+ "Set up Syntholoc for this repository:",
58
+ "1. Find the i18n source files (e.g. messages/en.json, locales/en/*.json) and infer the source locale.",
59
+ "2. `create_project`, then `seed_starter_locales` (or `add_locale`) for the target languages.",
60
+ "3. `import_source_strings` from the source file(s).",
61
+ "4. Write `.syntholoc/config.json` mapping source \u2192 translation file patterns and the project id.",
62
+ "5. Offer to run the localize_changed flow next."
63
+ ].join("\n")
64
+ )
65
+ );
66
+ }
67
+
68
+ // src/resources.ts
69
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
70
+ import { CONFIG_RESOURCE, PROJECTS_RESOURCE, PROJECT_RESOURCE } from "@syntholoc/sdk";
71
+ function json(uri, data) {
72
+ return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }] };
73
+ }
74
+ function registerResources(server, gql) {
75
+ server.registerResource(
76
+ "config",
77
+ "syntholoc://config",
78
+ {
79
+ title: "Syntholoc config",
80
+ description: "Server metadata incl. maxBulkSize (auto-translate batch threshold).",
81
+ mimeType: "application/json"
82
+ },
83
+ async (uri) => json(uri, (await gql.request(CONFIG_RESOURCE)).appInfo)
84
+ );
85
+ server.registerResource(
86
+ "projects",
87
+ "syntholoc://projects",
88
+ {
89
+ title: "Projects",
90
+ description: "All localization projects in the active organization.",
91
+ mimeType: "application/json"
92
+ },
93
+ async (uri) => json(uri, (await gql.request(PROJECTS_RESOURCE)).projects)
94
+ );
95
+ server.registerResource(
96
+ "project",
97
+ new ResourceTemplate("syntholoc://project/{projectId}", { list: void 0 }),
98
+ {
99
+ title: "Project overview",
100
+ description: "A project with its locales and per-locale progress.",
101
+ mimeType: "application/json"
102
+ },
103
+ async (uri, { projectId }) => json(uri, await gql.request(PROJECT_RESOURCE, { id: String(projectId) }))
104
+ );
105
+ }
106
+
107
+ // src/tools/analytics.ts
108
+ import { z as z2 } from "zod";
109
+ import { COST_ANALYTICS, LOCALE_PROGRESS, PROJECT_ANALYTICS, USAGE } from "@syntholoc/sdk";
110
+
111
+ // src/util.ts
112
+ function jsonResult(data) {
113
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
114
+ }
115
+
116
+ // src/tools/analytics.ts
117
+ function registerAnalyticsTools(server, gql) {
118
+ server.registerTool(
119
+ "project_analytics",
120
+ {
121
+ title: "Project analytics",
122
+ description: "Real-time quality + progress metrics for a project (per-locale progress, open issues).",
123
+ inputSchema: { projectId: z2.string() },
124
+ annotations: { readOnlyHint: true, openWorldHint: true }
125
+ },
126
+ async ({ projectId }) => jsonResult((await gql.request(PROJECT_ANALYTICS, { projectId })).projectAnalytics)
127
+ );
128
+ server.registerTool(
129
+ "cost_analytics",
130
+ {
131
+ title: "Cost analytics",
132
+ description: "LLM token/cost time-series and per-model breakdown over a window (default 30 days).",
133
+ inputSchema: {
134
+ projectId: z2.string().optional(),
135
+ days: z2.number().int().min(1).max(365).optional()
136
+ },
137
+ annotations: { readOnlyHint: true, openWorldHint: true }
138
+ },
139
+ async (vars) => jsonResult((await gql.request(COST_ANALYTICS, vars)).costAnalytics)
140
+ );
141
+ server.registerTool(
142
+ "usage",
143
+ {
144
+ title: "Org usage & plan",
145
+ description: "Organization plan, hosted words and seat usage.",
146
+ inputSchema: {},
147
+ annotations: { readOnlyHint: true, openWorldHint: true }
148
+ },
149
+ async () => jsonResult((await gql.request(USAGE)).usage)
150
+ );
151
+ server.registerTool(
152
+ "locale_progress",
153
+ {
154
+ title: "Locale progress",
155
+ description: "Progress counts for one locale: total / untranslated / autoTranslated / humanApproved. The `untranslated` count drives the batch threshold (see get_config).",
156
+ inputSchema: { projectId: z2.string(), localeId: z2.string() },
157
+ annotations: { readOnlyHint: true, openWorldHint: true }
158
+ },
159
+ async (vars) => jsonResult((await gql.request(LOCALE_PROGRESS, vars)).localeProgress)
160
+ );
161
+ }
162
+
163
+ // src/tools/config.ts
164
+ import { APP_INFO } from "@syntholoc/sdk";
165
+ function registerConfigTools(server, gql) {
166
+ server.registerTool(
167
+ "get_config",
168
+ {
169
+ title: "Get Syntholoc config",
170
+ description: "Server metadata incl. `maxBulkSize` \u2014 the max keys per LLM batch. Use it as the auto-translate threshold: translate proactively only when pending untranslated keys reach this number; otherwise ask the user (an explicit translate request always runs).",
171
+ inputSchema: {},
172
+ annotations: { readOnlyHint: true, openWorldHint: true }
173
+ },
174
+ async () => jsonResult((await gql.request(APP_INFO)).appInfo)
175
+ );
176
+ }
177
+
178
+ // src/tools/git.ts
179
+ import { z as z3 } from "zod";
180
+ import {
181
+ LIST_REPOSITORIES,
182
+ PULL_TRANSLATIONS,
183
+ PUSH_TRANSLATIONS,
184
+ SYNC_REPOSITORY
185
+ } from "@syntholoc/sdk";
186
+ function registerGitTools(server, gql) {
187
+ server.registerTool(
188
+ "list_repositories",
189
+ {
190
+ title: "List repositories",
191
+ description: "Lists git repositories connected to a project.",
192
+ inputSchema: { projectId: z3.string() },
193
+ annotations: { readOnlyHint: true, openWorldHint: true }
194
+ },
195
+ async ({ projectId }) => jsonResult((await gql.request(LIST_REPOSITORIES, { projectId })).repositories)
196
+ );
197
+ server.registerTool(
198
+ "sync_repository",
199
+ {
200
+ title: "Sync repository",
201
+ description: "Queues an immediate sync of a connected repository (pull \u2192 translate \u2192 PR).",
202
+ inputSchema: { projectId: z3.string(), repositoryId: z3.string() },
203
+ annotations: { openWorldHint: true }
204
+ },
205
+ async ({ projectId, repositoryId }) => jsonResult((await gql.request(SYNC_REPOSITORY, { projectId, id: repositoryId })).syncRepository)
206
+ );
207
+ server.registerTool(
208
+ "push_translations",
209
+ {
210
+ title: "Push translations (PR)",
211
+ description: "Queues write-back of a locale: export approved translations and open a PR.",
212
+ inputSchema: { projectId: z3.string(), repositoryId: z3.string(), localeId: z3.string() },
213
+ annotations: { openWorldHint: true }
214
+ },
215
+ async (vars) => jsonResult((await gql.request(PUSH_TRANSLATIONS, vars)).pushTranslations)
216
+ );
217
+ server.registerTool(
218
+ "pull_translations",
219
+ {
220
+ title: "Pull translations",
221
+ description: "Pulls translation files from the connected repo and imports them per locale. `autoApprove` lands them as APPROVED; `conflict` is OVERWRITE | SKIP_EXISTING | KEEP_APPROVED.",
222
+ inputSchema: {
223
+ projectId: z3.string(),
224
+ repositoryId: z3.string(),
225
+ autoApprove: z3.boolean().default(false),
226
+ conflict: z3.enum(["OVERWRITE", "SKIP_EXISTING", "KEEP_APPROVED"]).default("OVERWRITE")
227
+ },
228
+ annotations: { openWorldHint: true }
229
+ },
230
+ async ({ projectId, repositoryId, autoApprove, conflict }) => jsonResult(
231
+ (await gql.request(PULL_TRANSLATIONS, { projectId, repositoryId, input: { autoApprove, conflict } })).pullTranslations
232
+ )
233
+ );
234
+ }
235
+
236
+ // src/tools/glossary.ts
237
+ import { z as z4 } from "zod";
238
+ import {
239
+ CREATE_GLOSSARY_TERM,
240
+ DELETE_GLOSSARY_TERM,
241
+ LIST_GLOSSARY,
242
+ SUGGEST_GLOSSARY_TERMS,
243
+ UPDATE_GLOSSARY_TERM
244
+ } from "@syntholoc/sdk";
245
+ function registerGlossaryTools(server, gql) {
246
+ server.registerTool(
247
+ "list_glossary",
248
+ {
249
+ title: "List glossary terms",
250
+ description: "Lists glossary terms (project-scoped if projectId given, else org-wide).",
251
+ inputSchema: {
252
+ projectId: z4.string().optional(),
253
+ skip: z4.number().int().min(0).default(0),
254
+ take: z4.number().int().min(1).max(100).default(25)
255
+ },
256
+ annotations: { readOnlyHint: true, openWorldHint: true }
257
+ },
258
+ async (vars) => jsonResult((await gql.request(LIST_GLOSSARY, vars)).glossaryTerms)
259
+ );
260
+ server.registerTool(
261
+ "upsert_glossary_term",
262
+ {
263
+ title: "Create or update glossary term",
264
+ description: 'Creates a glossary term, or updates it when `termId` is given. `translations` is a per-locale map, e.g. {"de-DE":"\u2026"}. `forbidden` lists disallowed renderings.',
265
+ inputSchema: {
266
+ termId: z4.string().optional().describe("Provide to update an existing term"),
267
+ projectId: z4.string().optional().describe("Project scope; omit for org-wide (create only)"),
268
+ term: z4.string().optional(),
269
+ description: z4.string().optional(),
270
+ partOfSpeech: z4.string().optional(),
271
+ doNotTranslate: z4.boolean().optional(),
272
+ forbidden: z4.array(z4.string()).optional(),
273
+ translations: z4.record(z4.string(), z4.string()).optional()
274
+ },
275
+ annotations: { idempotentHint: true, openWorldHint: true }
276
+ },
277
+ async ({ termId, projectId, ...rest }) => {
278
+ if (termId) {
279
+ const data2 = await gql.request(UPDATE_GLOSSARY_TERM, { id: termId, input: rest });
280
+ return jsonResult(data2.updateGlossaryTerm);
281
+ }
282
+ if (!rest.term) throw new Error("`term` is required when creating a glossary term");
283
+ const data = await gql.request(CREATE_GLOSSARY_TERM, {
284
+ input: { term: rest.term, ...rest, projectId }
285
+ });
286
+ return jsonResult(data.createGlossaryTerm);
287
+ }
288
+ );
289
+ server.registerTool(
290
+ "delete_glossary_term",
291
+ {
292
+ title: "Delete glossary term",
293
+ description: "Deletes a glossary term. Destructive \u2014 confirm first.",
294
+ inputSchema: { termId: z4.string() },
295
+ annotations: { destructiveHint: true, openWorldHint: true }
296
+ },
297
+ async ({ termId }) => jsonResult((await gql.request(DELETE_GLOSSARY_TERM, { id: termId })).deleteGlossaryTerm)
298
+ );
299
+ server.registerTool(
300
+ "suggest_glossary_terms",
301
+ {
302
+ title: "Suggest glossary terms",
303
+ description: "AI-suggested glossary terms extracted from the project source strings.",
304
+ inputSchema: { projectId: z4.string() },
305
+ annotations: { readOnlyHint: true, openWorldHint: true }
306
+ },
307
+ async ({ projectId }) => jsonResult((await gql.request(SUGGEST_GLOSSARY_TERMS, { projectId })).suggestGlossaryTerms)
308
+ );
309
+ }
310
+
311
+ // src/tools/jobs.ts
312
+ import { z as z5 } from "zod";
313
+ import { GET_JOB, LIST_JOBS, waitForJob } from "@syntholoc/sdk";
314
+ function registerJobTools(server, gql) {
315
+ server.registerTool(
316
+ "get_job",
317
+ {
318
+ title: "Get job",
319
+ description: "Status, progress, token usage and cost of a background job by id.",
320
+ inputSchema: { jobId: z5.string(), projectId: z5.string() },
321
+ annotations: { readOnlyHint: true, openWorldHint: true }
322
+ },
323
+ async ({ jobId, projectId }) => jsonResult((await gql.request(GET_JOB, { id: jobId, projectId })).job)
324
+ );
325
+ server.registerTool(
326
+ "list_jobs",
327
+ {
328
+ title: "List jobs",
329
+ description: "Recent background jobs for a project (translate / QA / git sync).",
330
+ inputSchema: {
331
+ projectId: z5.string(),
332
+ skip: z5.number().int().min(0).default(0),
333
+ take: z5.number().int().min(1).max(100).default(25)
334
+ },
335
+ annotations: { readOnlyHint: true, openWorldHint: true }
336
+ },
337
+ async (vars) => jsonResult((await gql.request(LIST_JOBS, vars)).jobs)
338
+ );
339
+ server.registerTool(
340
+ "wait_for_job",
341
+ {
342
+ title: "Wait for job",
343
+ description: "Polls a job until it reaches a terminal state (COMPLETED/FAILED/CANCELLED) or the timeout elapses, then returns its final detail. Use after translate/QA to follow a run.",
344
+ inputSchema: {
345
+ jobId: z5.string(),
346
+ projectId: z5.string(),
347
+ timeoutSec: z5.number().int().min(1).max(900).default(180),
348
+ pollIntervalSec: z5.number().int().min(1).max(30).default(2)
349
+ },
350
+ annotations: { readOnlyHint: true, openWorldHint: true }
351
+ },
352
+ async ({ jobId, projectId, timeoutSec, pollIntervalSec }) => {
353
+ const { job, timedOut } = await waitForJob(gql, projectId, jobId, { timeoutSec, pollIntervalSec });
354
+ return jsonResult({ ...job, timedOut });
355
+ }
356
+ );
357
+ }
358
+
359
+ // src/tools/keys.ts
360
+ import { z as z6 } from "zod";
361
+ import {
362
+ DELETE_KEY,
363
+ IMPORT_SOURCE_STRINGS,
364
+ LIST_KEYS,
365
+ UPDATE_KEY,
366
+ UPSERT_KEYS
367
+ } from "@syntholoc/sdk";
368
+ var FORMATS = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS"];
369
+ function registerKeyTools(server, gql) {
370
+ server.registerTool(
371
+ "list_keys",
372
+ {
373
+ title: "List keys",
374
+ description: "Lists source keys. With `localeId` + `status` (untranslated | flagged | approved) the result `totalCount` is the pending count for that status \u2014 use it for the batch threshold.",
375
+ inputSchema: {
376
+ projectId: z6.string(),
377
+ localeId: z6.string().optional(),
378
+ status: z6.enum(["untranslated", "flagged", "approved"]).optional(),
379
+ search: z6.string().optional(),
380
+ skip: z6.number().int().min(0).default(0),
381
+ take: z6.number().int().min(1).max(200).default(25)
382
+ },
383
+ annotations: { readOnlyHint: true, openWorldHint: true }
384
+ },
385
+ async (vars) => jsonResult((await gql.request(LIST_KEYS, vars)).keys)
386
+ );
387
+ server.registerTool(
388
+ "upsert_keys",
389
+ {
390
+ title: "Upsert keys (bulk)",
391
+ description: "Bulk-upserts source keys by name (delta sync from local i18n files). Keys not listed are left untouched. Returns added/updated/unchanged counts; set `dryRun` to preview.",
392
+ inputSchema: {
393
+ projectId: z6.string(),
394
+ keys: z6.array(
395
+ z6.object({
396
+ key: z6.string(),
397
+ sourceText: z6.string(),
398
+ context: z6.string().optional(),
399
+ maxLength: z6.number().int().positive().optional()
400
+ })
401
+ ).min(1),
402
+ dryRun: z6.boolean().default(false)
403
+ },
404
+ annotations: { idempotentHint: true, openWorldHint: true }
405
+ },
406
+ async ({ projectId, keys, dryRun }) => jsonResult((await gql.request(UPSERT_KEYS, { projectId, input: keys, dryRun })).upsertKeys)
407
+ );
408
+ server.registerTool(
409
+ "update_key",
410
+ {
411
+ title: "Update key",
412
+ description: "Updates a single key (source text, context, max length).",
413
+ inputSchema: {
414
+ projectId: z6.string(),
415
+ keyId: z6.string(),
416
+ sourceText: z6.string().optional(),
417
+ context: z6.string().optional(),
418
+ maxLength: z6.number().int().positive().optional()
419
+ },
420
+ annotations: { idempotentHint: true, openWorldHint: true }
421
+ },
422
+ async ({ projectId, keyId, ...input }) => jsonResult((await gql.request(UPDATE_KEY, { projectId, id: keyId, input })).updateKey)
423
+ );
424
+ server.registerTool(
425
+ "delete_key",
426
+ {
427
+ title: "Delete key",
428
+ description: "Deletes a key and its translations. Destructive \u2014 confirm first.",
429
+ inputSchema: { projectId: z6.string(), keyId: z6.string() },
430
+ annotations: { destructiveHint: true, openWorldHint: true }
431
+ },
432
+ async ({ projectId, keyId }) => jsonResult((await gql.request(DELETE_KEY, { projectId, id: keyId })).deleteKey)
433
+ );
434
+ server.registerTool(
435
+ "import_source_strings",
436
+ {
437
+ title: "Import source strings",
438
+ description: "Imports source strings from a file (paste the raw content) and returns the diff summary. Use `dryRun` to preview. For a structured delta prefer upsert_keys.",
439
+ inputSchema: {
440
+ projectId: z6.string(),
441
+ content: z6.string(),
442
+ format: z6.enum(FORMATS),
443
+ dryRun: z6.boolean().default(false)
444
+ },
445
+ annotations: { idempotentHint: true, openWorldHint: true }
446
+ },
447
+ async ({ projectId, content, format, dryRun }) => jsonResult(
448
+ (await gql.request(IMPORT_SOURCE_STRINGS, { projectId, input: { content, format, dryRun } })).importSourceStrings
449
+ )
450
+ );
451
+ }
452
+
453
+ // src/tools/locales.ts
454
+ import { z as z7 } from "zod";
455
+ import { ADD_LOCALE, LIST_LOCALES, REMOVE_LOCALE, SEED_STARTER_LOCALES } from "@syntholoc/sdk";
456
+ function registerLocaleTools(server, gql) {
457
+ server.registerTool(
458
+ "list_locales",
459
+ {
460
+ title: "List locales",
461
+ description: "Lists the target locales configured for a project.",
462
+ inputSchema: { projectId: z7.string() },
463
+ annotations: { readOnlyHint: true, openWorldHint: true }
464
+ },
465
+ async ({ projectId }) => jsonResult((await gql.request(LIST_LOCALES, { projectId })).locales)
466
+ );
467
+ server.registerTool(
468
+ "add_locale",
469
+ {
470
+ title: "Add locale",
471
+ description: "Adds a target locale (BCP-47 code) to a project.",
472
+ inputSchema: {
473
+ projectId: z7.string(),
474
+ code: z7.string().describe('BCP-47 code, e.g. "de-DE"'),
475
+ name: z7.string().optional(),
476
+ formality: z7.string().optional(),
477
+ gender: z7.string().optional(),
478
+ variant: z7.string().optional()
479
+ },
480
+ annotations: { openWorldHint: true }
481
+ },
482
+ async ({ projectId, ...input }) => jsonResult((await gql.request(ADD_LOCALE, { projectId, input })).addLocale)
483
+ );
484
+ server.registerTool(
485
+ "seed_starter_locales",
486
+ {
487
+ title: "Seed starter locales",
488
+ description: "Adds a set of common starter locales to a project (omit codes for the defaults).",
489
+ inputSchema: { projectId: z7.string(), codes: z7.array(z7.string()).optional() },
490
+ annotations: { openWorldHint: true }
491
+ },
492
+ async ({ projectId, codes }) => jsonResult((await gql.request(SEED_STARTER_LOCALES, { projectId, codes })).seedStarterLocales)
493
+ );
494
+ server.registerTool(
495
+ "remove_locale",
496
+ {
497
+ title: "Remove locale",
498
+ description: "Removes a target locale and its translations. Destructive \u2014 confirm first.",
499
+ inputSchema: { projectId: z7.string(), localeId: z7.string() },
500
+ annotations: { destructiveHint: true, openWorldHint: true }
501
+ },
502
+ async ({ projectId, localeId }) => jsonResult((await gql.request(REMOVE_LOCALE, { projectId, id: localeId })).removeLocale)
503
+ );
504
+ }
505
+
506
+ // src/tools/projects.ts
507
+ import { z as z8 } from "zod";
508
+ import { CREATE_PROJECT, GET_PROJECT, LIST_PROJECTS, UPDATE_PROJECT } from "@syntholoc/sdk";
509
+ function registerProjectTools(server, gql) {
510
+ server.registerTool(
511
+ "list_projects",
512
+ {
513
+ title: "List projects",
514
+ description: "Lists localization projects in the active organization (paginated).",
515
+ inputSchema: {
516
+ skip: z8.number().int().min(0).default(0),
517
+ take: z8.number().int().min(1).max(100).default(25)
518
+ },
519
+ annotations: { readOnlyHint: true, openWorldHint: true }
520
+ },
521
+ async ({ skip, take }) => jsonResult((await gql.request(LIST_PROJECTS, { skip, take })).projects)
522
+ );
523
+ server.registerTool(
524
+ "get_project",
525
+ {
526
+ title: "Get project",
527
+ description: "Project settings plus analytics, including per-locale progress with the `untranslated` count used to decide the batch threshold (see get_config).",
528
+ inputSchema: { projectId: z8.string() },
529
+ annotations: { readOnlyHint: true, openWorldHint: true }
530
+ },
531
+ async ({ projectId }) => jsonResult(await gql.request(GET_PROJECT, { id: projectId }))
532
+ );
533
+ server.registerTool(
534
+ "create_project",
535
+ {
536
+ title: "Create project",
537
+ description: 'Creates a localization project. `sourceLocale` defaults to "en".',
538
+ inputSchema: {
539
+ name: z8.string(),
540
+ sourceLocale: z8.string().default("en"),
541
+ description: z8.string().optional(),
542
+ context: z8.string().optional(),
543
+ requireReview: z8.boolean().optional(),
544
+ qaCriticEnabled: z8.boolean().optional()
545
+ },
546
+ annotations: { openWorldHint: true }
547
+ },
548
+ async (input) => jsonResult((await gql.request(CREATE_PROJECT, { input })).createProject)
549
+ );
550
+ server.registerTool(
551
+ "update_project",
552
+ {
553
+ title: "Update project",
554
+ description: "Updates project settings (name, source locale, review/QA flags, context).",
555
+ inputSchema: {
556
+ projectId: z8.string(),
557
+ name: z8.string().optional(),
558
+ sourceLocale: z8.string().optional(),
559
+ description: z8.string().optional(),
560
+ context: z8.string().optional(),
561
+ requireReview: z8.boolean().optional(),
562
+ qaCriticEnabled: z8.boolean().optional(),
563
+ exportApprovedOnly: z8.boolean().optional(),
564
+ sortKeysAlphabetically: z8.boolean().optional(),
565
+ status: z8.string().optional()
566
+ },
567
+ annotations: { idempotentHint: true, openWorldHint: true }
568
+ },
569
+ async ({ projectId, ...input }) => jsonResult((await gql.request(UPDATE_PROJECT, { id: projectId, input })).updateProject)
570
+ );
571
+ }
572
+
573
+ // src/tools/providers.ts
574
+ import { z as z9 } from "zod";
575
+ import { CONFIGURE_LLM_PROVIDER, LIST_LLM_PROVIDERS } from "@syntholoc/sdk";
576
+ function registerProviderTools(server, gql) {
577
+ server.registerTool(
578
+ "list_llm_providers",
579
+ {
580
+ title: "List LLM providers",
581
+ description: "Configured LLM providers for the org (never exposes the API key).",
582
+ inputSchema: {},
583
+ annotations: { readOnlyHint: true, openWorldHint: true }
584
+ },
585
+ async () => jsonResult((await gql.request(LIST_LLM_PROVIDERS)).llmProviders)
586
+ );
587
+ server.registerTool(
588
+ "configure_llm_provider",
589
+ {
590
+ title: "Configure LLM provider",
591
+ description: "Adds a BYOK LLM provider (OPENROUTER | OPENAI | ANTHROPIC). The key is stored encrypted at rest.",
592
+ inputSchema: {
593
+ type: z9.enum(["OPENROUTER", "OPENAI", "ANTHROPIC"]),
594
+ label: z9.string(),
595
+ apiKey: z9.string(),
596
+ baseUrl: z9.string().optional(),
597
+ defaultModel: z9.string().optional()
598
+ },
599
+ annotations: { openWorldHint: true }
600
+ },
601
+ async (input) => jsonResult((await gql.request(CONFIGURE_LLM_PROVIDER, { input })).configureLlmProvider)
602
+ );
603
+ }
604
+
605
+ // src/tools/qa.ts
606
+ import { z as z10 } from "zod";
607
+ import { LIST_QA_ISSUES, RUN_LOCALE_QA, RUN_QA } from "@syntholoc/sdk";
608
+ function registerQaTools(server, gql) {
609
+ server.registerTool(
610
+ "run_locale_qa",
611
+ {
612
+ title: "Run locale QA",
613
+ description: "Queues an LLM QA-critic pass over a locale's non-approved translations. Returns tracking `jobIds`.",
614
+ inputSchema: { projectId: z10.string(), localeId: z10.string() },
615
+ annotations: { openWorldHint: true }
616
+ },
617
+ async (vars) => jsonResult((await gql.request(RUN_LOCALE_QA, vars)).runLocaleQa)
618
+ );
619
+ server.registerTool(
620
+ "run_qa",
621
+ {
622
+ title: "Run QA on a translation",
623
+ description: "Runs QA on one translation and returns its score and issues (synchronous).",
624
+ inputSchema: { projectId: z10.string(), translationId: z10.string() },
625
+ annotations: { openWorldHint: true }
626
+ },
627
+ async (vars) => jsonResult((await gql.request(RUN_QA, vars)).runQa)
628
+ );
629
+ server.registerTool(
630
+ "list_qa_issues",
631
+ {
632
+ title: "List QA issues",
633
+ description: "Open QA issues for a locale, plus the issue-type facets (filter tabs).",
634
+ inputSchema: {
635
+ projectId: z10.string(),
636
+ localeId: z10.string(),
637
+ type: z10.string().optional(),
638
+ skip: z10.number().int().min(0).default(0),
639
+ take: z10.number().int().min(1).max(100).default(25)
640
+ },
641
+ annotations: { readOnlyHint: true, openWorldHint: true }
642
+ },
643
+ async (vars) => jsonResult(await gql.request(LIST_QA_ISSUES, vars))
644
+ );
645
+ }
646
+
647
+ // src/tools/styleguides.ts
648
+ import { z as z11 } from "zod";
649
+ import { CREATE_STYLE_GUIDE, GENERATE_STYLE_GUIDE, LIST_STYLE_GUIDES } from "@syntholoc/sdk";
650
+ function registerStyleGuideTools(server, gql) {
651
+ server.registerTool(
652
+ "list_style_guides",
653
+ {
654
+ title: "List style guides",
655
+ description: "Lists style guides (project-scoped if projectId given, else org-wide).",
656
+ inputSchema: { projectId: z11.string().optional() },
657
+ annotations: { readOnlyHint: true, openWorldHint: true }
658
+ },
659
+ async ({ projectId }) => jsonResult((await gql.request(LIST_STYLE_GUIDES, { projectId })).styleGuides)
660
+ );
661
+ server.registerTool(
662
+ "generate_style_guide",
663
+ {
664
+ title: "Generate style guide (draft)",
665
+ description: "Drafts a tone/rules/prompt-fragment style guide from the project (optionally per locale). Returns a draft \u2014 persist it with create_style_guide.",
666
+ inputSchema: { projectId: z11.string(), localeCode: z11.string().optional() },
667
+ annotations: { openWorldHint: true }
668
+ },
669
+ async ({ projectId, localeCode }) => jsonResult((await gql.request(GENERATE_STYLE_GUIDE, { input: { projectId, localeCode } })).generateStyleGuide)
670
+ );
671
+ server.registerTool(
672
+ "create_style_guide",
673
+ {
674
+ title: "Create style guide",
675
+ description: "Persists a style guide (tone, rules JSON, prompt fragment), project- or locale-scoped.",
676
+ inputSchema: {
677
+ name: z11.string(),
678
+ projectId: z11.string().optional(),
679
+ localeCode: z11.string().optional(),
680
+ tone: z11.string().optional(),
681
+ promptFragment: z11.string().optional(),
682
+ rules: z11.array(z11.string()).optional()
683
+ },
684
+ annotations: { openWorldHint: true }
685
+ },
686
+ async (input) => jsonResult((await gql.request(CREATE_STYLE_GUIDE, { input })).createStyleGuide)
687
+ );
688
+ }
689
+
690
+ // src/tools/translations.ts
691
+ import { z as z12 } from "zod";
692
+ import {
693
+ APPROVE_TRANSLATION,
694
+ EXPORT_LOCALE,
695
+ IMPORT_TRANSLATIONS,
696
+ LIST_TRANSLATIONS,
697
+ REJECT_TRANSLATION,
698
+ SET_TRANSLATION,
699
+ TRANSLATE_KEY,
700
+ TRANSLATE_LOCALE
701
+ } from "@syntholoc/sdk";
702
+ var FORMATS2 = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS"];
703
+ function registerTranslationTools(server, gql) {
704
+ server.registerTool(
705
+ "translate_locale",
706
+ {
707
+ title: "Translate locale",
708
+ description: "Queues AI translation of a locale (scope ALL | UNAPPROVED | UNTRANSLATED) and returns the tracking `jobIds`. Executes immediately and unconditionally \u2014 the batch threshold is a client-side policy for PROACTIVE syncs, not for this tool.",
709
+ inputSchema: {
710
+ projectId: z12.string(),
711
+ localeId: z12.string(),
712
+ scope: z12.enum(["ALL", "UNAPPROVED", "UNTRANSLATED"]).default("UNTRANSLATED")
713
+ },
714
+ annotations: { openWorldHint: true }
715
+ },
716
+ async (vars) => jsonResult((await gql.request(TRANSLATE_LOCALE, vars)).translateLocale)
717
+ );
718
+ server.registerTool(
719
+ "translate_key",
720
+ {
721
+ title: "Translate key",
722
+ description: "Queues AI translation of a single key into a locale. Returns tracking `jobIds`.",
723
+ inputSchema: { projectId: z12.string(), keyId: z12.string(), localeId: z12.string() },
724
+ annotations: { openWorldHint: true }
725
+ },
726
+ async (vars) => jsonResult((await gql.request(TRANSLATE_KEY, vars)).translateKey)
727
+ );
728
+ server.registerTool(
729
+ "set_translation",
730
+ {
731
+ title: "Set translation",
732
+ description: "Manually sets the translation text for a key in a locale.",
733
+ inputSchema: {
734
+ projectId: z12.string(),
735
+ keyId: z12.string(),
736
+ localeId: z12.string(),
737
+ text: z12.string(),
738
+ origin: z12.string().optional()
739
+ },
740
+ annotations: { idempotentHint: true, openWorldHint: true }
741
+ },
742
+ async ({ projectId, keyId, localeId, text, origin }) => jsonResult(
743
+ (await gql.request(SET_TRANSLATION, { projectId, input: { keyId, localeId, text, origin } })).setTranslation
744
+ )
745
+ );
746
+ server.registerTool(
747
+ "list_translations",
748
+ {
749
+ title: "List translations",
750
+ description: "Lists translations for a locale with status, origin, model and QA score.",
751
+ inputSchema: {
752
+ projectId: z12.string(),
753
+ localeId: z12.string(),
754
+ skip: z12.number().int().min(0).default(0),
755
+ take: z12.number().int().min(1).max(200).default(25)
756
+ },
757
+ annotations: { readOnlyHint: true, openWorldHint: true }
758
+ },
759
+ async (vars) => jsonResult((await gql.request(LIST_TRANSLATIONS, vars)).translations)
760
+ );
761
+ server.registerTool(
762
+ "approve_translation",
763
+ {
764
+ title: "Approve translation",
765
+ description: "Marks a translation APPROVED.",
766
+ inputSchema: { projectId: z12.string(), translationId: z12.string() },
767
+ annotations: { idempotentHint: true, openWorldHint: true }
768
+ },
769
+ async ({ projectId, translationId }) => jsonResult((await gql.request(APPROVE_TRANSLATION, { projectId, id: translationId })).approveTranslation)
770
+ );
771
+ server.registerTool(
772
+ "reject_translation",
773
+ {
774
+ title: "Reject translation",
775
+ description: "Marks a translation REJECTED (queued for re-translation).",
776
+ inputSchema: { projectId: z12.string(), translationId: z12.string() },
777
+ annotations: { idempotentHint: true, openWorldHint: true }
778
+ },
779
+ async ({ projectId, translationId }) => jsonResult((await gql.request(REJECT_TRANSLATION, { projectId, id: translationId })).rejectTranslation)
780
+ );
781
+ server.registerTool(
782
+ "export_locale",
783
+ {
784
+ title: "Export locale",
785
+ description: "Exports a locale to a file string in the chosen format \u2014 the basis for writing translations back into local files. Set `onlyApproved` to exclude drafts.",
786
+ inputSchema: {
787
+ projectId: z12.string(),
788
+ localeId: z12.string(),
789
+ format: z12.enum(FORMATS2).default("JSON"),
790
+ onlyApproved: z12.boolean().optional()
791
+ },
792
+ annotations: { readOnlyHint: true, openWorldHint: true }
793
+ },
794
+ async (vars) => jsonResult({ content: (await gql.request(EXPORT_LOCALE, vars)).exportLocale })
795
+ );
796
+ server.registerTool(
797
+ "import_translations",
798
+ {
799
+ title: "Import translations",
800
+ description: "Imports ready-made translations for a locale (paste file content). `autoApprove` lands them as APPROVED; `conflict` is OVERWRITE | SKIP_EXISTING | KEEP_APPROVED; `dryRun` previews.",
801
+ inputSchema: {
802
+ projectId: z12.string(),
803
+ localeId: z12.string(),
804
+ content: z12.string(),
805
+ format: z12.enum(FORMATS2),
806
+ autoApprove: z12.boolean().default(false),
807
+ conflict: z12.enum(["OVERWRITE", "SKIP_EXISTING", "KEEP_APPROVED"]).default("OVERWRITE"),
808
+ dryRun: z12.boolean().default(false)
809
+ },
810
+ annotations: { idempotentHint: true, openWorldHint: true }
811
+ },
812
+ async ({ projectId, localeId, content, format, autoApprove, conflict, dryRun }) => jsonResult(
813
+ (await gql.request(IMPORT_TRANSLATIONS, {
814
+ projectId,
815
+ localeId,
816
+ input: { content, format, autoApprove, conflict, dryRun }
817
+ })).importTranslations
818
+ )
819
+ );
820
+ }
821
+
822
+ // src/tools/index.ts
823
+ function registerAllTools(server, gql) {
824
+ registerConfigTools(server, gql);
825
+ registerProjectTools(server, gql);
826
+ registerLocaleTools(server, gql);
827
+ registerKeyTools(server, gql);
828
+ registerTranslationTools(server, gql);
829
+ registerQaTools(server, gql);
830
+ registerGlossaryTools(server, gql);
831
+ registerStyleGuideTools(server, gql);
832
+ registerGitTools(server, gql);
833
+ registerProviderTools(server, gql);
834
+ registerJobTools(server, gql);
835
+ registerAnalyticsTools(server, gql);
836
+ }
837
+
838
+ // src/build-server.ts
839
+ function buildMcpServer(gql) {
840
+ const server = new McpServer({ name: "syntholoc", version: "0.1.0" });
841
+ registerAllTools(server, gql);
842
+ registerResources(server, gql);
843
+ registerPrompts(server);
844
+ return server;
845
+ }
846
+
847
+ export {
848
+ buildMcpServer
849
+ };
850
+ //# sourceMappingURL=chunk-N4PXYXAP.js.map