@syntholoc/mcp 0.3.1 → 0.5.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.
- package/README.md +21 -0
- package/dist/build-server.js +1 -1
- package/dist/{chunk-MUY33R6O.js → chunk-V5AAF2RJ.js} +174 -86
- package/dist/index.js +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -113,6 +113,27 @@ Translation runs asynchronously and returns a job id; a `wait_for_job` tool
|
|
|
113
113
|
blocks until completion, so you can chain "translate → wait → export" in a
|
|
114
114
|
single request.
|
|
115
115
|
|
|
116
|
+
## Contributing: adding a tool
|
|
117
|
+
|
|
118
|
+
Tools are authored by hand — the curated descriptions, input aliases, and
|
|
119
|
+
validation are what make an LLM use them well, so they are deliberately not
|
|
120
|
+
generated one-per-operation. Two mechanisms keep the manual layer honest:
|
|
121
|
+
|
|
122
|
+
- **Generated input shapes.** `bun run codegen` emits zod schemas for every
|
|
123
|
+
GraphQL input type into `src/generated/input-schemas.ts` from the API's
|
|
124
|
+
committed SDL. Compose a schema's `.shape` instead of re-typing a mutation's
|
|
125
|
+
fields, then curate on top — see `configure_llm_provider` in
|
|
126
|
+
`src/tools/providers.ts`, which spreads `ConfigureProviderInputSchema().shape`
|
|
127
|
+
and tightens the API's free-form `type: String` down to a concrete enum.
|
|
128
|
+
- **Drift guard.** `src/operation-coverage.spec.ts` fails the build when an
|
|
129
|
+
operation exported by `@syntholoc/sdk` is not surfaced by any tool or resource
|
|
130
|
+
(or explicitly exempted). Add an operation to the SDK and you must either wire
|
|
131
|
+
up a tool/resource or record why it is MCP-exempt.
|
|
132
|
+
|
|
133
|
+
Every tool set has contract tests (`src/tools/*.spec.ts`) that pin both the
|
|
134
|
+
GraphQL operation + variables a tool emits and the JSON it returns, via an
|
|
135
|
+
in-memory MCP client against a mock GraphQL client (`src/spec-helpers.ts`).
|
|
136
|
+
|
|
116
137
|
## License
|
|
117
138
|
|
|
118
139
|
MIT
|
package/dist/build-server.js
CHANGED
|
@@ -115,6 +115,12 @@ function registerPrompts(server) {
|
|
|
115
115
|
` - If a locale's ON-DISK code differs from its BCP-47 code (e.g. "zh-CN" stored as "cn", "pt-BR" as "pt"),`,
|
|
116
116
|
' add `languagesMapping`, e.g. { "two_letters_code": { "zh-CN": "cn", "pt-BR": "pt" } }.',
|
|
117
117
|
' - Do NOT invent fields like `i18nDir`, `format: "nested-dirs"`, or `localeMapping` \u2014 they are not read.',
|
|
118
|
+
" - CRITICAL: `.syntholoc/config.json` MUST be committed and tracked \u2014 it is the mapping source of",
|
|
119
|
+
" truth the server reads from the branch. NEVER add `.syntholoc` (or the config file) to `.gitignore`.",
|
|
120
|
+
" If `.gitignore` already ignores it (a common reflex, next to `.claude`), remove that line. After",
|
|
121
|
+
" committing, VERIFY it is tracked (e.g. `git ls-files .syntholoc/config.json` returns the path, or",
|
|
122
|
+
" `git check-ignore` reports nothing) \u2014 an ignored config means every sync/pull/write-back resolves",
|
|
123
|
+
" zero files and silently opens no PR.",
|
|
118
124
|
" c. Connect it with `connect_repository`. Ask the user which method they prefer:",
|
|
119
125
|
" - GitHub App (no token, recommended): `get_github_app_install_url` \u2192 give the URL to the user",
|
|
120
126
|
" to open and authorize (you cannot click it for them; you may offer to open it via the shell).",
|
|
@@ -300,6 +306,9 @@ import { COST_ANALYTICS, LOCALE_PROGRESS, PROJECT_ANALYTICS as PROJECT_ANALYTICS
|
|
|
300
306
|
function jsonResult(data) {
|
|
301
307
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
302
308
|
}
|
|
309
|
+
function errorResult(message) {
|
|
310
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
311
|
+
}
|
|
303
312
|
|
|
304
313
|
// src/tools/analytics.ts
|
|
305
314
|
function registerAnalyticsTools(server, gql) {
|
|
@@ -440,8 +449,12 @@ function registerGitTools(server, gql) {
|
|
|
440
449
|
provider: z3.enum(["GITHUB", "GITLAB", "BITBUCKET"]).default("GITHUB"),
|
|
441
450
|
fullName: z3.string().describe('owner/repo, e.g. "acme/webapp".'),
|
|
442
451
|
branch: z3.string().optional().describe('Source branch (default "main").'),
|
|
443
|
-
files: z3.array(fileMapping).optional().describe(
|
|
444
|
-
|
|
452
|
+
files: z3.array(fileMapping).optional().describe(
|
|
453
|
+
"Crowdin-style source\u2192translation mappings. Fallback only \u2014 an in-repo `.syntholoc/config.json` wins."
|
|
454
|
+
),
|
|
455
|
+
paths: z3.array(z3.string()).optional().describe(
|
|
456
|
+
"Simple mode: source file paths/globs to track (use instead of `files`). Fallback for in-repo config."
|
|
457
|
+
),
|
|
445
458
|
installationId: z3.number().int().optional().describe("GitHub App installation id. Mutually exclusive with `token`."),
|
|
446
459
|
token: z3.string().optional().describe("Access token (PAT mode), stored encrypted. Mutually exclusive with `installationId`."),
|
|
447
460
|
translationBranch: z3.string().optional().describe("Branch translations are committed to (default l10n_<branch>)."),
|
|
@@ -507,7 +520,7 @@ function registerGitTools(server, gql) {
|
|
|
507
520
|
"pull_translations",
|
|
508
521
|
{
|
|
509
522
|
title: "Pull translations",
|
|
510
|
-
description: "
|
|
523
|
+
description: "Queues a pull of translation files from the connected repo, importing them per locale. Returns the tracking job ids \u2014 the import itself runs in the background. `autoApprove` lands them as APPROVED; `conflict` is OVERWRITE | SKIP_EXISTING | KEEP_APPROVED.",
|
|
511
524
|
inputSchema: {
|
|
512
525
|
projectId: z3.string(),
|
|
513
526
|
repositoryId: z3.string(),
|
|
@@ -654,7 +667,8 @@ import {
|
|
|
654
667
|
UPDATE_KEY,
|
|
655
668
|
UPSERT_KEYS
|
|
656
669
|
} from "@syntholoc/sdk";
|
|
657
|
-
var FORMATS = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS", "NJK"];
|
|
670
|
+
var FORMATS = ["JSON", "YAML", "MARKDOWN", "MDX", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS", "NJK"];
|
|
671
|
+
var DOCUMENT_FORMATS = /* @__PURE__ */ new Set(["MARKDOWN", "MDX"]);
|
|
658
672
|
function registerKeyTools(server, gql) {
|
|
659
673
|
server.registerTool(
|
|
660
674
|
"list_keys",
|
|
@@ -724,7 +738,7 @@ function registerKeyTools(server, gql) {
|
|
|
724
738
|
"import_source_strings",
|
|
725
739
|
{
|
|
726
740
|
title: "Import source strings",
|
|
727
|
-
description: 'Imports source strings from ONE file (paste the raw content) and returns the diff summary. For initial setup of a repo-hosted project, PREFER connecting the repo and running `sync_repository` \u2014 it imports every source file server-side in one pass; use this tool only for file-based projects with no Git integration, or for one-off single-file imports. For a multi-file / namespaced project (a file per namespace, e.g. messages/en/home.json), pass `keyPrefix` (the namespace, e.g. "home") so keys become `home.title` and files with overlapping top-level keys do not collide \u2014 call once per file. Use `dryRun` to preview.',
|
|
741
|
+
description: 'Imports source strings from ONE file (paste the raw content) and returns the diff summary. For initial setup of a repo-hosted project, PREFER connecting the repo and running `sync_repository` \u2014 it imports every source file server-side in one pass; use this tool only for file-based projects with no Git integration, or for one-off single-file imports. For a multi-file / namespaced project (a file per namespace, e.g. messages/en/home.json), pass `keyPrefix` (the namespace, e.g. "home") so keys become `home.title` and files with overlapping top-level keys do not collide \u2014 call once per file. Use `dryRun` to preview. For MARKDOWN and MDX `sourceFile` is REQUIRED: the document is split into per-block segments, and without the file path a re-import cannot tell a moved paragraph from an edited one.',
|
|
728
742
|
inputSchema: {
|
|
729
743
|
projectId: z6.string(),
|
|
730
744
|
content: z6.string(),
|
|
@@ -737,12 +751,17 @@ function registerKeyTools(server, gql) {
|
|
|
737
751
|
},
|
|
738
752
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
739
753
|
},
|
|
740
|
-
async ({ projectId, content, format, keyPrefix, sourceFile, dryRun }) =>
|
|
741
|
-
(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
754
|
+
async ({ projectId, content, format, keyPrefix, sourceFile, dryRun }) => {
|
|
755
|
+
if (DOCUMENT_FORMATS.has(format) && !sourceFile?.trim()) {
|
|
756
|
+
return errorResult(`${format} needs a sourceFile \u2014 a document's segments only have an identity within a file.`);
|
|
757
|
+
}
|
|
758
|
+
return jsonResult(
|
|
759
|
+
(await gql.request(IMPORT_SOURCE_STRINGS, {
|
|
760
|
+
projectId,
|
|
761
|
+
input: { content, format, keyPrefix, sourceFile, dryRun }
|
|
762
|
+
})).importSourceStrings
|
|
763
|
+
);
|
|
764
|
+
}
|
|
746
765
|
);
|
|
747
766
|
}
|
|
748
767
|
|
|
@@ -900,6 +919,21 @@ function resolveLocaleCodes(raw) {
|
|
|
900
919
|
return out;
|
|
901
920
|
}
|
|
902
921
|
|
|
922
|
+
// ../../packages/shared/src/constants.ts
|
|
923
|
+
var ORG_PLANS = ["FREE", "PRO", "ENTERPRISE"];
|
|
924
|
+
function isPaidPlan(plan) {
|
|
925
|
+
switch (plan) {
|
|
926
|
+
case "PRO":
|
|
927
|
+
case "ENTERPRISE":
|
|
928
|
+
return true;
|
|
929
|
+
case "FREE":
|
|
930
|
+
return false;
|
|
931
|
+
default:
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
var PAID_PLANS = ORG_PLANS.filter(isPaidPlan);
|
|
936
|
+
|
|
903
937
|
// src/tools/locales.ts
|
|
904
938
|
function registerLocaleTools(server, gql) {
|
|
905
939
|
server.registerTool(
|
|
@@ -1081,8 +1115,27 @@ function registerProjectTools(server, gql) {
|
|
|
1081
1115
|
}
|
|
1082
1116
|
|
|
1083
1117
|
// src/tools/providers.ts
|
|
1084
|
-
import { z as
|
|
1118
|
+
import { z as z10 } from "zod";
|
|
1085
1119
|
import { CONFIGURE_LLM_PROVIDER, LIST_LLM_PROVIDERS } from "@syntholoc/sdk";
|
|
1120
|
+
|
|
1121
|
+
// src/generated/input-schemas.ts
|
|
1122
|
+
import { z as z9 } from "zod";
|
|
1123
|
+
var isDefinedNonNullAny = (v) => v !== void 0 && v !== null;
|
|
1124
|
+
var definedNonNullAnySchema = z9.any().refine((v) => isDefinedNonNullAny(v));
|
|
1125
|
+
var CoverageCategorySchema = z9.enum(["DYNAMIC", "NOT_CAPTURED", "NOT_IN_CODE", "SCREENSHOT", "SERVER", "TEST", "UNKNOWN"]);
|
|
1126
|
+
var QaCriticScopeSchema = z9.enum(["ALL", "UNREVIEWED"]);
|
|
1127
|
+
var TranslateScopeSchema = z9.enum(["ALL", "DRAFT", "UNAPPROVED", "UNTRANSLATED"]);
|
|
1128
|
+
function ConfigureProviderInputSchema() {
|
|
1129
|
+
return z9.object({
|
|
1130
|
+
apiKey: z9.string(),
|
|
1131
|
+
baseUrl: z9.string().nullish(),
|
|
1132
|
+
defaultModel: z9.string().nullish(),
|
|
1133
|
+
label: z9.string(),
|
|
1134
|
+
type: z9.string()
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/tools/providers.ts
|
|
1086
1139
|
function registerProviderTools(server, gql) {
|
|
1087
1140
|
server.registerTool(
|
|
1088
1141
|
"list_llm_providers",
|
|
@@ -1099,12 +1152,12 @@ function registerProviderTools(server, gql) {
|
|
|
1099
1152
|
{
|
|
1100
1153
|
title: "Configure LLM provider",
|
|
1101
1154
|
description: "Adds a BYOK LLM provider (OPENROUTER | OPENAI | ANTHROPIC). The key is stored encrypted at rest.",
|
|
1155
|
+
// Base field shapes (apiKey/label/baseUrl/defaultModel) are generated from
|
|
1156
|
+
// the GraphQL ConfigureProviderInput; we curate `type` from the API's
|
|
1157
|
+
// free-form String down to the concrete provider set.
|
|
1102
1158
|
inputSchema: {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
apiKey: z9.string(),
|
|
1106
|
-
baseUrl: z9.string().optional(),
|
|
1107
|
-
defaultModel: z9.string().optional()
|
|
1159
|
+
...ConfigureProviderInputSchema().shape,
|
|
1160
|
+
type: z10.enum(["OPENROUTER", "OPENAI", "ANTHROPIC"])
|
|
1108
1161
|
},
|
|
1109
1162
|
annotations: { openWorldHint: true }
|
|
1110
1163
|
},
|
|
@@ -1113,15 +1166,19 @@ function registerProviderTools(server, gql) {
|
|
|
1113
1166
|
}
|
|
1114
1167
|
|
|
1115
1168
|
// src/tools/qa.ts
|
|
1116
|
-
import { z as
|
|
1169
|
+
import { z as z11 } from "zod";
|
|
1117
1170
|
import { LIST_QA_ISSUES as LIST_QA_ISSUES2, RUN_LOCALE_QA, RUN_QA } from "@syntholoc/sdk";
|
|
1118
1171
|
function registerQaTools(server, gql) {
|
|
1119
1172
|
server.registerTool(
|
|
1120
1173
|
"run_locale_qa",
|
|
1121
1174
|
{
|
|
1122
1175
|
title: "Run locale QA",
|
|
1123
|
-
description: "Queues an LLM QA-critic pass over a locale's non-approved translations. Returns tracking `jobIds`.",
|
|
1124
|
-
inputSchema: {
|
|
1176
|
+
description: "Queues an LLM QA-critic pass over a locale's non-approved translations. `scope` UNREVIEWED (default) critiques only translations not yet reviewed at their current text; ALL re-critiques every one. Returns tracking `jobIds`.",
|
|
1177
|
+
inputSchema: {
|
|
1178
|
+
projectId: z11.string(),
|
|
1179
|
+
localeId: z11.string(),
|
|
1180
|
+
scope: z11.enum(["UNREVIEWED", "ALL"]).optional()
|
|
1181
|
+
},
|
|
1125
1182
|
annotations: { openWorldHint: true }
|
|
1126
1183
|
},
|
|
1127
1184
|
async (vars) => jsonResult((await gql.request(RUN_LOCALE_QA, vars)).runLocaleQa)
|
|
@@ -1131,7 +1188,7 @@ function registerQaTools(server, gql) {
|
|
|
1131
1188
|
{
|
|
1132
1189
|
title: "Run QA on a translation",
|
|
1133
1190
|
description: "Runs QA on one translation and returns its score and issues (synchronous).",
|
|
1134
|
-
inputSchema: { projectId:
|
|
1191
|
+
inputSchema: { projectId: z11.string(), translationId: z11.string() },
|
|
1135
1192
|
annotations: { openWorldHint: true }
|
|
1136
1193
|
},
|
|
1137
1194
|
async (vars) => jsonResult((await gql.request(RUN_QA, vars)).runQa)
|
|
@@ -1142,11 +1199,11 @@ function registerQaTools(server, gql) {
|
|
|
1142
1199
|
title: "List QA issues",
|
|
1143
1200
|
description: "Open QA issues for a locale, plus the issue-type facets (filter tabs).",
|
|
1144
1201
|
inputSchema: {
|
|
1145
|
-
projectId:
|
|
1146
|
-
localeId:
|
|
1147
|
-
type:
|
|
1148
|
-
skip:
|
|
1149
|
-
take:
|
|
1202
|
+
projectId: z11.string(),
|
|
1203
|
+
localeId: z11.string(),
|
|
1204
|
+
type: z11.string().optional(),
|
|
1205
|
+
skip: z11.number().int().min(0).default(0),
|
|
1206
|
+
take: z11.number().int().min(1).max(100).default(25)
|
|
1150
1207
|
},
|
|
1151
1208
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1152
1209
|
},
|
|
@@ -1155,7 +1212,7 @@ function registerQaTools(server, gql) {
|
|
|
1155
1212
|
}
|
|
1156
1213
|
|
|
1157
1214
|
// src/tools/styleguides.ts
|
|
1158
|
-
import { z as
|
|
1215
|
+
import { z as z12 } from "zod";
|
|
1159
1216
|
import { CREATE_STYLE_GUIDE, GENERATE_STYLE_GUIDE, LIST_STYLE_GUIDES as LIST_STYLE_GUIDES2 } from "@syntholoc/sdk";
|
|
1160
1217
|
function registerStyleGuideTools(server, gql) {
|
|
1161
1218
|
server.registerTool(
|
|
@@ -1163,7 +1220,7 @@ function registerStyleGuideTools(server, gql) {
|
|
|
1163
1220
|
{
|
|
1164
1221
|
title: "List style guides",
|
|
1165
1222
|
description: "Lists style guides (project-scoped if projectId given, else org-wide).",
|
|
1166
|
-
inputSchema: { projectId:
|
|
1223
|
+
inputSchema: { projectId: z12.string().optional() },
|
|
1167
1224
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1168
1225
|
},
|
|
1169
1226
|
async ({ projectId }) => jsonResult((await gql.request(LIST_STYLE_GUIDES2, { projectId })).styleGuides)
|
|
@@ -1173,7 +1230,7 @@ function registerStyleGuideTools(server, gql) {
|
|
|
1173
1230
|
{
|
|
1174
1231
|
title: "Generate style guide (draft)",
|
|
1175
1232
|
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.",
|
|
1176
|
-
inputSchema: { projectId:
|
|
1233
|
+
inputSchema: { projectId: z12.string(), localeCode: z12.string().optional() },
|
|
1177
1234
|
annotations: { openWorldHint: true }
|
|
1178
1235
|
},
|
|
1179
1236
|
async ({ projectId, localeCode }) => jsonResult((await gql.request(GENERATE_STYLE_GUIDE, { input: { projectId, localeCode } })).generateStyleGuide)
|
|
@@ -1184,12 +1241,12 @@ function registerStyleGuideTools(server, gql) {
|
|
|
1184
1241
|
title: "Create style guide",
|
|
1185
1242
|
description: "Persists a style guide (tone, rules JSON, prompt fragment), project- or locale-scoped.",
|
|
1186
1243
|
inputSchema: {
|
|
1187
|
-
name:
|
|
1188
|
-
projectId:
|
|
1189
|
-
localeCode:
|
|
1190
|
-
tone:
|
|
1191
|
-
promptFragment:
|
|
1192
|
-
rules:
|
|
1244
|
+
name: z12.string(),
|
|
1245
|
+
projectId: z12.string().optional(),
|
|
1246
|
+
localeCode: z12.string().optional(),
|
|
1247
|
+
tone: z12.string().optional(),
|
|
1248
|
+
promptFragment: z12.string().optional(),
|
|
1249
|
+
rules: z12.array(z12.string()).optional()
|
|
1193
1250
|
},
|
|
1194
1251
|
annotations: { openWorldHint: true }
|
|
1195
1252
|
},
|
|
@@ -1198,7 +1255,7 @@ function registerStyleGuideTools(server, gql) {
|
|
|
1198
1255
|
}
|
|
1199
1256
|
|
|
1200
1257
|
// src/tools/tm.ts
|
|
1201
|
-
import { z as
|
|
1258
|
+
import { z as z13 } from "zod";
|
|
1202
1259
|
import { TM_ENTRIES, TM_SEARCH, TM_STATS as TM_STATS2 } from "@syntholoc/sdk";
|
|
1203
1260
|
function registerTmTools(server, gql) {
|
|
1204
1261
|
server.registerTool(
|
|
@@ -1207,10 +1264,10 @@ function registerTmTools(server, gql) {
|
|
|
1207
1264
|
title: "Search translation memory",
|
|
1208
1265
|
description: "Concordance search \u2014 the nearest prior translations for a source string in a target locale, with cosine distance (0 = identical). Scope to a project with `projectId` (includes its group siblings); omit for the whole organization. Requires an embedding-capable provider.",
|
|
1209
1266
|
inputSchema: {
|
|
1210
|
-
sourceText:
|
|
1211
|
-
targetLocale:
|
|
1212
|
-
projectId:
|
|
1213
|
-
limit:
|
|
1267
|
+
sourceText: z13.string(),
|
|
1268
|
+
targetLocale: z13.string(),
|
|
1269
|
+
projectId: z13.string().optional(),
|
|
1270
|
+
limit: z13.number().int().min(1).max(20).default(5)
|
|
1214
1271
|
},
|
|
1215
1272
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1216
1273
|
},
|
|
@@ -1221,7 +1278,7 @@ function registerTmTools(server, gql) {
|
|
|
1221
1278
|
{
|
|
1222
1279
|
title: "Translation memory stats",
|
|
1223
1280
|
description: "Entry counts per target locale (org-wide, or for a project with `projectId`).",
|
|
1224
|
-
inputSchema: { projectId:
|
|
1281
|
+
inputSchema: { projectId: z13.string().optional() },
|
|
1225
1282
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1226
1283
|
},
|
|
1227
1284
|
async ({ projectId }) => jsonResult((await gql.request(TM_STATS2, { projectId })).tmStats)
|
|
@@ -1232,10 +1289,10 @@ function registerTmTools(server, gql) {
|
|
|
1232
1289
|
title: "Browse translation memory",
|
|
1233
1290
|
description: "Lists stored TM entries (paginated, newest first), optionally filtered by project and locale.",
|
|
1234
1291
|
inputSchema: {
|
|
1235
|
-
projectId:
|
|
1236
|
-
targetLocale:
|
|
1237
|
-
skip:
|
|
1238
|
-
take:
|
|
1292
|
+
projectId: z13.string().optional(),
|
|
1293
|
+
targetLocale: z13.string().optional(),
|
|
1294
|
+
skip: z13.number().int().min(0).default(0),
|
|
1295
|
+
take: z13.number().int().min(1).max(100).default(25)
|
|
1239
1296
|
},
|
|
1240
1297
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1241
1298
|
},
|
|
@@ -1244,9 +1301,10 @@ function registerTmTools(server, gql) {
|
|
|
1244
1301
|
}
|
|
1245
1302
|
|
|
1246
1303
|
// src/tools/translations.ts
|
|
1247
|
-
import { z as
|
|
1304
|
+
import { z as z14 } from "zod";
|
|
1248
1305
|
import {
|
|
1249
1306
|
APPROVE_TRANSLATION,
|
|
1307
|
+
EXPORT_DOCUMENT_LOCALE,
|
|
1250
1308
|
EXPORT_LOCALE,
|
|
1251
1309
|
IMPORT_TRANSLATIONS,
|
|
1252
1310
|
LIST_TRANSLATIONS,
|
|
@@ -1255,7 +1313,8 @@ import {
|
|
|
1255
1313
|
TRANSLATE_KEY,
|
|
1256
1314
|
TRANSLATE_LOCALE
|
|
1257
1315
|
} from "@syntholoc/sdk";
|
|
1258
|
-
var FORMATS2 = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS", "NJK"];
|
|
1316
|
+
var FORMATS2 = ["JSON", "YAML", "MARKDOWN", "MDX", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS", "NJK"];
|
|
1317
|
+
var DOCUMENT_FORMATS2 = /* @__PURE__ */ new Set(["MARKDOWN", "MDX"]);
|
|
1259
1318
|
function registerTranslationTools(server, gql) {
|
|
1260
1319
|
server.registerTool(
|
|
1261
1320
|
"translate_locale",
|
|
@@ -1263,9 +1322,9 @@ function registerTranslationTools(server, gql) {
|
|
|
1263
1322
|
title: "Translate locale",
|
|
1264
1323
|
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.",
|
|
1265
1324
|
inputSchema: {
|
|
1266
|
-
projectId:
|
|
1267
|
-
localeId:
|
|
1268
|
-
scope:
|
|
1325
|
+
projectId: z14.string(),
|
|
1326
|
+
localeId: z14.string(),
|
|
1327
|
+
scope: z14.enum(["ALL", "UNAPPROVED", "UNTRANSLATED"]).default("UNTRANSLATED")
|
|
1269
1328
|
},
|
|
1270
1329
|
annotations: { openWorldHint: true }
|
|
1271
1330
|
},
|
|
@@ -1276,7 +1335,7 @@ function registerTranslationTools(server, gql) {
|
|
|
1276
1335
|
{
|
|
1277
1336
|
title: "Translate key",
|
|
1278
1337
|
description: "Queues AI translation of a single key into a locale. Returns tracking `jobIds`.",
|
|
1279
|
-
inputSchema: { projectId:
|
|
1338
|
+
inputSchema: { projectId: z14.string(), keyId: z14.string(), localeId: z14.string() },
|
|
1280
1339
|
annotations: { openWorldHint: true }
|
|
1281
1340
|
},
|
|
1282
1341
|
async (vars) => jsonResult((await gql.request(TRANSLATE_KEY, vars)).translateKey)
|
|
@@ -1287,11 +1346,11 @@ function registerTranslationTools(server, gql) {
|
|
|
1287
1346
|
title: "Set translation",
|
|
1288
1347
|
description: "Manually sets the translation text for a key in a locale.",
|
|
1289
1348
|
inputSchema: {
|
|
1290
|
-
projectId:
|
|
1291
|
-
keyId:
|
|
1292
|
-
localeId:
|
|
1293
|
-
text:
|
|
1294
|
-
origin:
|
|
1349
|
+
projectId: z14.string(),
|
|
1350
|
+
keyId: z14.string(),
|
|
1351
|
+
localeId: z14.string(),
|
|
1352
|
+
text: z14.string(),
|
|
1353
|
+
origin: z14.string().optional()
|
|
1295
1354
|
},
|
|
1296
1355
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
1297
1356
|
},
|
|
@@ -1305,10 +1364,10 @@ function registerTranslationTools(server, gql) {
|
|
|
1305
1364
|
title: "List translations",
|
|
1306
1365
|
description: "Lists translations for a locale with status, origin, model and QA score.",
|
|
1307
1366
|
inputSchema: {
|
|
1308
|
-
projectId:
|
|
1309
|
-
localeId:
|
|
1310
|
-
skip:
|
|
1311
|
-
take:
|
|
1367
|
+
projectId: z14.string(),
|
|
1368
|
+
localeId: z14.string(),
|
|
1369
|
+
skip: z14.number().int().min(0).default(0),
|
|
1370
|
+
take: z14.number().int().min(1).max(200).default(25)
|
|
1312
1371
|
},
|
|
1313
1372
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1314
1373
|
},
|
|
@@ -1319,7 +1378,7 @@ function registerTranslationTools(server, gql) {
|
|
|
1319
1378
|
{
|
|
1320
1379
|
title: "Approve translation",
|
|
1321
1380
|
description: "Marks a translation APPROVED.",
|
|
1322
|
-
inputSchema: { projectId:
|
|
1381
|
+
inputSchema: { projectId: z14.string(), translationId: z14.string() },
|
|
1323
1382
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
1324
1383
|
},
|
|
1325
1384
|
async ({ projectId, translationId }) => jsonResult((await gql.request(APPROVE_TRANSLATION, { projectId, id: translationId })).approveTranslation)
|
|
@@ -1329,7 +1388,7 @@ function registerTranslationTools(server, gql) {
|
|
|
1329
1388
|
{
|
|
1330
1389
|
title: "Reject translation",
|
|
1331
1390
|
description: "Marks a translation REJECTED (queued for re-translation).",
|
|
1332
|
-
inputSchema: { projectId:
|
|
1391
|
+
inputSchema: { projectId: z14.string(), translationId: z14.string() },
|
|
1333
1392
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
1334
1393
|
},
|
|
1335
1394
|
async ({ projectId, translationId }) => jsonResult((await gql.request(REJECT_TRANSLATION, { projectId, id: translationId })).rejectTranslation)
|
|
@@ -1338,43 +1397,72 @@ function registerTranslationTools(server, gql) {
|
|
|
1338
1397
|
"export_locale",
|
|
1339
1398
|
{
|
|
1340
1399
|
title: "Export locale",
|
|
1341
|
-
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. For a multi-file project prefer `sourceFile` (the real path, e.g. "messages/en/home.json") to export exactly that file with file-local key names \u2014 reliable. `keyPrefix` is the legacy name-prefix heuristic.',
|
|
1400
|
+
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. For a multi-file project prefer `sourceFile` (the real path, e.g. "messages/en/home.json") to export exactly that file with file-local key names \u2014 reliable. `keyPrefix` is the legacy name-prefix heuristic. For MARKDOWN and MDX use `export_document` instead: a document is spliced into its original file, which the server reads from the connected repository.',
|
|
1342
1401
|
inputSchema: {
|
|
1343
|
-
projectId:
|
|
1344
|
-
localeId:
|
|
1345
|
-
format:
|
|
1346
|
-
onlyApproved:
|
|
1347
|
-
sourceFile:
|
|
1348
|
-
keyPrefix:
|
|
1402
|
+
projectId: z14.string(),
|
|
1403
|
+
localeId: z14.string(),
|
|
1404
|
+
format: z14.enum(FORMATS2).default("JSON"),
|
|
1405
|
+
onlyApproved: z14.boolean().optional(),
|
|
1406
|
+
sourceFile: z14.string().optional().describe('Real source file path to export, e.g. "messages/en/home.json" (reliable, preferred).'),
|
|
1407
|
+
keyPrefix: z14.string().optional().describe('Legacy namespace prefix, e.g. "home".')
|
|
1349
1408
|
},
|
|
1350
1409
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1351
1410
|
},
|
|
1352
1411
|
async (vars) => jsonResult({ content: (await gql.request(EXPORT_LOCALE, vars)).exportLocale })
|
|
1353
1412
|
);
|
|
1413
|
+
server.registerTool(
|
|
1414
|
+
"export_document",
|
|
1415
|
+
{
|
|
1416
|
+
title: "Export document locale",
|
|
1417
|
+
description: "Exports one locale of a Markdown or MDX source document. Unlike `export_locale` this returns the whole translated document: the server reads the original file from the connected repository and splices the translations into it, so headings, list markers, code fences and indentation are preserved byte for byte. Segments with no translation keep their source text.",
|
|
1418
|
+
inputSchema: {
|
|
1419
|
+
projectId: z14.string(),
|
|
1420
|
+
localeId: z14.string(),
|
|
1421
|
+
sourceFile: z14.string().describe('Path of the source document in the repository, e.g. "docs/guide.md".'),
|
|
1422
|
+
repositoryId: z14.string().optional().describe("Only needed when the project has several repositories and the path is ambiguous."),
|
|
1423
|
+
onlyApproved: z14.boolean().optional()
|
|
1424
|
+
},
|
|
1425
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
1426
|
+
},
|
|
1427
|
+
async (vars) => {
|
|
1428
|
+
if (!vars.sourceFile.trim()) {
|
|
1429
|
+
return errorResult("export_document needs a sourceFile \u2014 the path of the document in the repository.");
|
|
1430
|
+
}
|
|
1431
|
+
return jsonResult({ content: (await gql.request(EXPORT_DOCUMENT_LOCALE, vars)).exportDocumentLocale });
|
|
1432
|
+
}
|
|
1433
|
+
);
|
|
1354
1434
|
server.registerTool(
|
|
1355
1435
|
"import_translations",
|
|
1356
1436
|
{
|
|
1357
1437
|
title: "Import translations",
|
|
1358
|
-
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. For a multi-file / namespaced project pass `keyPrefix` (the namespace) to match prefixed source keys.",
|
|
1438
|
+
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. For a multi-file / namespaced project pass `keyPrefix` (the namespace) to match prefixed source keys. For MARKDOWN and MDX `sourceFile` is REQUIRED: a translated document has translated headings, so it is matched to the source by segment position and the two must have the same structure.",
|
|
1359
1439
|
inputSchema: {
|
|
1360
|
-
projectId:
|
|
1361
|
-
localeId:
|
|
1362
|
-
content:
|
|
1363
|
-
format:
|
|
1364
|
-
keyPrefix:
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1440
|
+
projectId: z14.string(),
|
|
1441
|
+
localeId: z14.string(),
|
|
1442
|
+
content: z14.string(),
|
|
1443
|
+
format: z14.enum(FORMATS2),
|
|
1444
|
+
keyPrefix: z14.string().optional().describe('Namespace prefix, e.g. "home" (per-file, multi-file projects).'),
|
|
1445
|
+
sourceFile: z14.string().optional().describe('Source file these translations belong to; REQUIRED for MARKDOWN/MDX, e.g. "docs/guide.md".'),
|
|
1446
|
+
autoApprove: z14.boolean().default(false),
|
|
1447
|
+
conflict: z14.enum(["OVERWRITE", "SKIP_EXISTING", "KEEP_APPROVED"]).default("OVERWRITE"),
|
|
1448
|
+
dryRun: z14.boolean().default(false)
|
|
1368
1449
|
},
|
|
1369
1450
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
1370
1451
|
},
|
|
1371
|
-
async ({ projectId, localeId, content, format, keyPrefix, autoApprove, conflict, dryRun }) =>
|
|
1372
|
-
(
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1452
|
+
async ({ projectId, localeId, content, format, keyPrefix, sourceFile, autoApprove, conflict, dryRun }) => {
|
|
1453
|
+
if (DOCUMENT_FORMATS2.has(format) && !sourceFile?.trim()) {
|
|
1454
|
+
return errorResult(
|
|
1455
|
+
`${format} needs a sourceFile \u2014 a translated document is aligned to its source by position.`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
return jsonResult(
|
|
1459
|
+
(await gql.request(IMPORT_TRANSLATIONS, {
|
|
1460
|
+
projectId,
|
|
1461
|
+
localeId,
|
|
1462
|
+
input: { content, format, keyPrefix, sourceFile, autoApprove, conflict, dryRun }
|
|
1463
|
+
})).importTranslations
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1378
1466
|
);
|
|
1379
1467
|
}
|
|
1380
1468
|
|
|
@@ -1407,7 +1495,7 @@ function registerAllTools(server, gql, opts = {}) {
|
|
|
1407
1495
|
}
|
|
1408
1496
|
|
|
1409
1497
|
// package.json
|
|
1410
|
-
var version = "0.
|
|
1498
|
+
var version = "0.5.0";
|
|
1411
1499
|
|
|
1412
1500
|
// src/build-server.ts
|
|
1413
1501
|
function buildMcpServer(gql, opts = {}) {
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syntholoc/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Syntholoc MCP server — manage AI localization and translation from Codex, Claude Code, and other MCP clients.",
|
|
5
5
|
"homepage": "https://syntholoc.com",
|
|
6
6
|
"keywords": [
|
|
@@ -47,14 +47,17 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@modelcontextprotocol/sdk": "1.22.0",
|
|
50
|
-
"@syntholoc/sdk": "0.3.
|
|
50
|
+
"@syntholoc/sdk": "0.3.1",
|
|
51
51
|
"zod": "3.25.76"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@graphql-codegen/cli": "7.1.2",
|
|
55
|
+
"@graphql-codegen/typescript": "5.0.2",
|
|
54
56
|
"@syntholoc/shared": "0.1.0",
|
|
55
57
|
"@types/bun": "1.3.14",
|
|
56
58
|
"@types/node": "22.19.21",
|
|
57
59
|
"graphql": "16.14.2",
|
|
60
|
+
"graphql-codegen-typescript-validation-schema": "0.19.0",
|
|
58
61
|
"tsup": "8.5.1",
|
|
59
62
|
"typescript": "6.0.3"
|
|
60
63
|
}
|