@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 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
@@ -2,7 +2,7 @@ import {
2
2
  TOOL_SET_NAMES,
3
3
  buildMcpServer,
4
4
  parseList
5
- } from "./chunk-MUY33R6O.js";
5
+ } from "./chunk-V5AAF2RJ.js";
6
6
  export {
7
7
  TOOL_SET_NAMES,
8
8
  buildMcpServer,
@@ -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("Crowdin-style source\u2192translation mappings. Fallback only \u2014 an in-repo `.syntholoc/config.json` wins."),
444
- paths: z3.array(z3.string()).optional().describe("Simple mode: source file paths/globs to track (use instead of `files`). Fallback for in-repo config."),
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: "Pulls translation files from the connected repo and imports them per locale. `autoApprove` lands them as APPROVED; `conflict` is OVERWRITE | SKIP_EXISTING | KEEP_APPROVED.",
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 }) => jsonResult(
741
- (await gql.request(IMPORT_SOURCE_STRINGS, {
742
- projectId,
743
- input: { content, format, keyPrefix, sourceFile, dryRun }
744
- })).importSourceStrings
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 z9 } from "zod";
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
- type: z9.enum(["OPENROUTER", "OPENAI", "ANTHROPIC"]),
1104
- label: z9.string(),
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 z10 } from "zod";
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: { projectId: z10.string(), localeId: z10.string() },
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: z10.string(), translationId: z10.string() },
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: z10.string(),
1146
- localeId: z10.string(),
1147
- type: z10.string().optional(),
1148
- skip: z10.number().int().min(0).default(0),
1149
- take: z10.number().int().min(1).max(100).default(25)
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 z11 } from "zod";
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: z11.string().optional() },
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: z11.string(), localeCode: z11.string().optional() },
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: z11.string(),
1188
- projectId: z11.string().optional(),
1189
- localeCode: z11.string().optional(),
1190
- tone: z11.string().optional(),
1191
- promptFragment: z11.string().optional(),
1192
- rules: z11.array(z11.string()).optional()
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 z12 } from "zod";
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: z12.string(),
1211
- targetLocale: z12.string(),
1212
- projectId: z12.string().optional(),
1213
- limit: z12.number().int().min(1).max(20).default(5)
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: z12.string().optional() },
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: z12.string().optional(),
1236
- targetLocale: z12.string().optional(),
1237
- skip: z12.number().int().min(0).default(0),
1238
- take: z12.number().int().min(1).max(100).default(25)
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 z13 } from "zod";
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: z13.string(),
1267
- localeId: z13.string(),
1268
- scope: z13.enum(["ALL", "UNAPPROVED", "UNTRANSLATED"]).default("UNTRANSLATED")
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: z13.string(), keyId: z13.string(), localeId: z13.string() },
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: z13.string(),
1291
- keyId: z13.string(),
1292
- localeId: z13.string(),
1293
- text: z13.string(),
1294
- origin: z13.string().optional()
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: z13.string(),
1309
- localeId: z13.string(),
1310
- skip: z13.number().int().min(0).default(0),
1311
- take: z13.number().int().min(1).max(200).default(25)
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: z13.string(), translationId: z13.string() },
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: z13.string(), translationId: z13.string() },
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: z13.string(),
1344
- localeId: z13.string(),
1345
- format: z13.enum(FORMATS2).default("JSON"),
1346
- onlyApproved: z13.boolean().optional(),
1347
- sourceFile: z13.string().optional().describe('Real source file path to export, e.g. "messages/en/home.json" (reliable, preferred).'),
1348
- keyPrefix: z13.string().optional().describe('Legacy namespace prefix, e.g. "home".')
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: z13.string(),
1361
- localeId: z13.string(),
1362
- content: z13.string(),
1363
- format: z13.enum(FORMATS2),
1364
- keyPrefix: z13.string().optional().describe('Namespace prefix, e.g. "home" (per-file, multi-file projects).'),
1365
- autoApprove: z13.boolean().default(false),
1366
- conflict: z13.enum(["OVERWRITE", "SKIP_EXISTING", "KEEP_APPROVED"]).default("OVERWRITE"),
1367
- dryRun: z13.boolean().default(false)
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 }) => jsonResult(
1372
- (await gql.request(IMPORT_TRANSLATIONS, {
1373
- projectId,
1374
- localeId,
1375
- input: { content, format, keyPrefix, autoApprove, conflict, dryRun }
1376
- })).importTranslations
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.3.1";
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
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  buildMcpServer,
4
4
  parseList
5
- } from "./chunk-MUY33R6O.js";
5
+ } from "./chunk-V5AAF2RJ.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syntholoc/mcp",
3
- "version": "0.3.1",
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.0",
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
  }