@syntholoc/mcp 0.3.0 → 0.4.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-GNXMB53B.js";
5
+ } from "./chunk-P7LZNIW2.js";
6
6
  export {
7
7
  TOOL_SET_NAMES,
8
8
  buildMcpServer,
@@ -105,30 +105,41 @@ function registerPrompts(server) {
105
105
  " default to the likeliest) and record the file\u2192locale mapping in the config `translation` pattern.",
106
106
  " (Target locales must exist before a Git pull so their translation files import.)",
107
107
  "3. RECOMMENDED \u2014 connect the repo and sync, instead of pushing strings one by one:",
108
- " a. `list_repositories`. If one is already connected, skip to (d).",
109
- " b. Otherwise connect it yourself with `connect_repository`. Ask the user which method they prefer:",
108
+ " a. `list_repositories`. If one is already connected, skip to (e).",
109
+ " b. Write `.syntholoc/config.json` FIRST and commit it to the source branch \u2014 the server reads it",
110
+ " from the repo as the SOURCE OF TRUTH for mappings on every sync/pull/write-back (Crowdin-style),",
111
+ " so it must land in the branch before the first sync. Schema (https://syntholoc.com/schemas/config.json):",
112
+ " the `projectId`, `sourceLocale`, and a Crowdin-style `files` array of `{ source, translation }`",
113
+ ' patterns, e.g. { "source": "messages/en/*.json", "translation": "messages/{locale}/%original_file_name%" }.',
114
+ " - Placeholders: `{locale}` (or %locale% / %two_letters_code% / %original_file_name% / %original_path%).",
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
+ ' add `languagesMapping`, e.g. { "two_letters_code": { "zh-CN": "cn", "pt-BR": "pt" } }.',
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.",
124
+ " c. Connect it with `connect_repository`. Ask the user which method they prefer:",
110
125
  " - GitHub App (no token, recommended): `get_github_app_install_url` \u2192 give the URL to the user",
111
126
  " to open and authorize (you cannot click it for them; you may offer to open it via the shell).",
112
127
  " Once they confirm, `list_github_installations` \u2192 `list_github_installation_repos` to pick the",
113
128
  " repo, then `connect_repository` with that `installationId`.",
114
129
  " - Token (any host): ask the user to paste a personal access token (repo read + PR scope), then",
115
130
  " `connect_repository` with `token`. Treat the token as a secret \u2014 do not echo it back.",
116
- " c. Pass `files` = Crowdin-style mappings matching the layout from step 1. NOTE the placeholder",
117
- " syntax differs from the config file: here use %two_letters_code% / %locale% / %original_file_name%",
118
- ' (e.g. source "messages/en/home.json", translation "messages/%two_letters_code%/%original_file_name%").',
119
- " d. `sync_repository` \u2014 imports ALL source strings from the repo at once and opens the translation PR.",
131
+ " d. `files`/`paths` on `connect_repository` are an OPTIONAL fallback used only when the repo has no",
132
+ " committed `.syntholoc/config.json`. If you committed the config in (b) you may pass the same `files`",
133
+ " for redundancy, but the in-repo config wins. Same Crowdin placeholders as the config file.",
134
+ " e. `sync_repository` \u2014 imports ALL source strings from the repo at once and opens the translation PR.",
120
135
  " Then `pull_translations` to import any translation files already in the repo (choose `conflict`",
121
136
  " KEEP_APPROVED to preserve in-app edits).",
122
- " e. `wait_for_job` on the returned job id, then report keys imported and the PR url.",
137
+ " f. `wait_for_job` on the returned job id, then report keys imported and the PR url.",
123
138
  "4. FALLBACK \u2014 file-based import when the user does NOT want Git integration:",
124
139
  " - Single file \u2192 `import_source_strings` once.",
125
140
  " - Namespaced \u2192 `import_source_strings` once PER FILE with `keyPrefix` set to the namespace",
126
141
  " (filename without extension). Keys become e.g. `home.title`, `common.title`.",
127
- "5. Write `.syntholoc/config.json` (schema https://syntholoc.com/schemas/config.json): the `projectId` and",
128
- " a Crowdin-style `files` array of `{ source, translation }` patterns with a `{locale}` placeholder,",
129
- ' e.g. { "source": "messages/en/home.json", "translation": "messages/{locale}/home.json", "namespace": "home" }.',
130
- ' Do NOT invent fields like `i18nDir`, `format: "nested-dirs"`, or `localeMapping` \u2014 they are not read.',
131
- "6. Offer to run the localize_changed flow next."
142
+ "5. Offer to run the localize_changed flow next."
132
143
  ].join("\n")
133
144
  )
134
145
  );
@@ -376,6 +387,9 @@ var fileMapping = z3.object({
376
387
  source: z3.string().describe('Source file path or glob, e.g. "messages/en.json" or "locales/en/*.json".'),
377
388
  translation: z3.string().describe(
378
389
  'Translation pattern with Crowdin placeholders, e.g. "messages/%two_letters_code%/%original_file_name%" or "locales/%locale%/%original_file_name%".'
390
+ ),
391
+ languagesMapping: z3.record(z3.string(), z3.record(z3.string(), z3.string())).optional().describe(
392
+ 'Crowdin `languages_mapping`: per-placeholder, per-locale path overrides. Keyed by placeholder name without % (e.g. "two_letters_code", "locale"), then by locale code, to the on-disk replacement. E.g. { "two_letters_code": { "zh-CN": "cn" } } writes Chinese under "cn" instead of "zh".'
379
393
  )
380
394
  });
381
395
  function registerGitTools(server, gql) {
@@ -426,14 +440,18 @@ function registerGitTools(server, gql) {
426
440
  "connect_repository",
427
441
  {
428
442
  title: "Connect repository",
429
- description: "Connects a git repository to the project so syncs can pull source strings and push translation PRs. Provide EITHER `installationId` (GitHub App mode \u2014 after the browser authorize flow: `get_github_app_install_url` \u2192 `list_github_installations` \u2192 `list_github_installation_repos`) OR `token` (PAT mode \u2014 a personal access token with repo read + pull-request scope; works for GitHub, GitLab and Bitbucket, no browser needed). Set `files` to Crowdin-style mappings that match the project layout. After connecting, call `sync_repository` to import all source strings at once.",
443
+ description: "Connects a git repository to the project so syncs can pull source strings and push translation PRs. Provide EITHER `installationId` (GitHub App mode \u2014 after the browser authorize flow: `get_github_app_install_url` \u2192 `list_github_installations` \u2192 `list_github_installation_repos`) OR `token` (PAT mode \u2014 a personal access token with repo read + pull-request scope; works for GitHub, GitLab and Bitbucket, no browser needed). Mappings come from the repo's `.syntholoc/config.json` when present (the source of truth); `files`/`paths` here are an optional fallback used only when that file is absent. After connecting, call `sync_repository` to import all source strings at once.",
430
444
  inputSchema: {
431
445
  projectId: z3.string(),
432
446
  provider: z3.enum(["GITHUB", "GITLAB", "BITBUCKET"]).default("GITHUB"),
433
447
  fullName: z3.string().describe('owner/repo, e.g. "acme/webapp".'),
434
448
  branch: z3.string().optional().describe('Source branch (default "main").'),
435
- files: z3.array(fileMapping).optional().describe("Crowdin-style source\u2192translation mappings."),
436
- paths: z3.array(z3.string()).optional().describe("Simple mode: source file paths/globs to track (use instead of `files`)."),
449
+ files: z3.array(fileMapping).optional().describe(
450
+ "Crowdin-style source\u2192translation mappings. Fallback only \u2014 an in-repo `.syntholoc/config.json` wins."
451
+ ),
452
+ paths: z3.array(z3.string()).optional().describe(
453
+ "Simple mode: source file paths/globs to track (use instead of `files`). Fallback for in-repo config."
454
+ ),
437
455
  installationId: z3.number().int().optional().describe("GitHub App installation id. Mutually exclusive with `token`."),
438
456
  token: z3.string().optional().describe("Access token (PAT mode), stored encrypted. Mutually exclusive with `installationId`."),
439
457
  translationBranch: z3.string().optional().describe("Branch translations are committed to (default l10n_<branch>)."),
@@ -448,7 +466,7 @@ function registerGitTools(server, gql) {
448
466
  "update_repository",
449
467
  {
450
468
  title: "Update repository",
451
- description: "Updates the configuration of an already-connected repository in place \u2014 no disconnect/reconnect, stored credentials and webhooks are preserved. Use this to fix or change file mappings (`files` or `paths`), the source `branch`, the translation/PR branches, or auto-sync settings. Only the fields you pass are changed. After changing mappings, call `sync_repository` to re-import source strings.",
469
+ description: "Updates an already-connected repository in place \u2014 no disconnect/reconnect, stored credentials and webhooks are preserved. Use it for integration settings (source `branch`, translation/PR branches, `pushSources`, auto-sync), and for `files`/`paths` only as the DB fallback. IMPORTANT: to change file MAPPINGS, prefer editing the repo's `.syntholoc/config.json` and opening a PR \u2014 that file is the source of truth and, when present, overrides `files`/`paths` here (they are ignored). So do NOT use this tool to change mappings of a config-managed repo; edit the file, open a PR, and after it merges call `sync_repository`. Only the fields you pass are changed.",
452
470
  inputSchema: {
453
471
  projectId: z3.string(),
454
472
  repositoryId: z3.string(),
@@ -499,7 +517,7 @@ function registerGitTools(server, gql) {
499
517
  "pull_translations",
500
518
  {
501
519
  title: "Pull translations",
502
- 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.",
520
+ 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.",
503
521
  inputSchema: {
504
522
  projectId: z3.string(),
505
523
  repositoryId: z3.string(),
@@ -892,6 +910,21 @@ function resolveLocaleCodes(raw) {
892
910
  return out;
893
911
  }
894
912
 
913
+ // ../../packages/shared/src/constants.ts
914
+ var ORG_PLANS = ["FREE", "PRO", "ENTERPRISE"];
915
+ function isPaidPlan(plan) {
916
+ switch (plan) {
917
+ case "PRO":
918
+ case "ENTERPRISE":
919
+ return true;
920
+ case "FREE":
921
+ return false;
922
+ default:
923
+ return false;
924
+ }
925
+ }
926
+ var PAID_PLANS = ORG_PLANS.filter(isPaidPlan);
927
+
895
928
  // src/tools/locales.ts
896
929
  function registerLocaleTools(server, gql) {
897
930
  server.registerTool(
@@ -1073,8 +1106,27 @@ function registerProjectTools(server, gql) {
1073
1106
  }
1074
1107
 
1075
1108
  // src/tools/providers.ts
1076
- import { z as z9 } from "zod";
1109
+ import { z as z10 } from "zod";
1077
1110
  import { CONFIGURE_LLM_PROVIDER, LIST_LLM_PROVIDERS } from "@syntholoc/sdk";
1111
+
1112
+ // src/generated/input-schemas.ts
1113
+ import { z as z9 } from "zod";
1114
+ var isDefinedNonNullAny = (v) => v !== void 0 && v !== null;
1115
+ var definedNonNullAnySchema = z9.any().refine((v) => isDefinedNonNullAny(v));
1116
+ var CoverageCategorySchema = z9.enum(["DYNAMIC", "NOT_CAPTURED", "NOT_IN_CODE", "SCREENSHOT", "SERVER", "TEST", "UNKNOWN"]);
1117
+ var QaCriticScopeSchema = z9.enum(["ALL", "UNREVIEWED"]);
1118
+ var TranslateScopeSchema = z9.enum(["ALL", "DRAFT", "UNAPPROVED", "UNTRANSLATED"]);
1119
+ function ConfigureProviderInputSchema() {
1120
+ return z9.object({
1121
+ apiKey: z9.string(),
1122
+ baseUrl: z9.string().nullish(),
1123
+ defaultModel: z9.string().nullish(),
1124
+ label: z9.string(),
1125
+ type: z9.string()
1126
+ });
1127
+ }
1128
+
1129
+ // src/tools/providers.ts
1078
1130
  function registerProviderTools(server, gql) {
1079
1131
  server.registerTool(
1080
1132
  "list_llm_providers",
@@ -1091,12 +1143,12 @@ function registerProviderTools(server, gql) {
1091
1143
  {
1092
1144
  title: "Configure LLM provider",
1093
1145
  description: "Adds a BYOK LLM provider (OPENROUTER | OPENAI | ANTHROPIC). The key is stored encrypted at rest.",
1146
+ // Base field shapes (apiKey/label/baseUrl/defaultModel) are generated from
1147
+ // the GraphQL ConfigureProviderInput; we curate `type` from the API's
1148
+ // free-form String down to the concrete provider set.
1094
1149
  inputSchema: {
1095
- type: z9.enum(["OPENROUTER", "OPENAI", "ANTHROPIC"]),
1096
- label: z9.string(),
1097
- apiKey: z9.string(),
1098
- baseUrl: z9.string().optional(),
1099
- defaultModel: z9.string().optional()
1150
+ ...ConfigureProviderInputSchema().shape,
1151
+ type: z10.enum(["OPENROUTER", "OPENAI", "ANTHROPIC"])
1100
1152
  },
1101
1153
  annotations: { openWorldHint: true }
1102
1154
  },
@@ -1105,15 +1157,19 @@ function registerProviderTools(server, gql) {
1105
1157
  }
1106
1158
 
1107
1159
  // src/tools/qa.ts
1108
- import { z as z10 } from "zod";
1160
+ import { z as z11 } from "zod";
1109
1161
  import { LIST_QA_ISSUES as LIST_QA_ISSUES2, RUN_LOCALE_QA, RUN_QA } from "@syntholoc/sdk";
1110
1162
  function registerQaTools(server, gql) {
1111
1163
  server.registerTool(
1112
1164
  "run_locale_qa",
1113
1165
  {
1114
1166
  title: "Run locale QA",
1115
- description: "Queues an LLM QA-critic pass over a locale's non-approved translations. Returns tracking `jobIds`.",
1116
- inputSchema: { projectId: z10.string(), localeId: z10.string() },
1167
+ 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`.",
1168
+ inputSchema: {
1169
+ projectId: z11.string(),
1170
+ localeId: z11.string(),
1171
+ scope: z11.enum(["UNREVIEWED", "ALL"]).optional()
1172
+ },
1117
1173
  annotations: { openWorldHint: true }
1118
1174
  },
1119
1175
  async (vars) => jsonResult((await gql.request(RUN_LOCALE_QA, vars)).runLocaleQa)
@@ -1123,7 +1179,7 @@ function registerQaTools(server, gql) {
1123
1179
  {
1124
1180
  title: "Run QA on a translation",
1125
1181
  description: "Runs QA on one translation and returns its score and issues (synchronous).",
1126
- inputSchema: { projectId: z10.string(), translationId: z10.string() },
1182
+ inputSchema: { projectId: z11.string(), translationId: z11.string() },
1127
1183
  annotations: { openWorldHint: true }
1128
1184
  },
1129
1185
  async (vars) => jsonResult((await gql.request(RUN_QA, vars)).runQa)
@@ -1134,11 +1190,11 @@ function registerQaTools(server, gql) {
1134
1190
  title: "List QA issues",
1135
1191
  description: "Open QA issues for a locale, plus the issue-type facets (filter tabs).",
1136
1192
  inputSchema: {
1137
- projectId: z10.string(),
1138
- localeId: z10.string(),
1139
- type: z10.string().optional(),
1140
- skip: z10.number().int().min(0).default(0),
1141
- take: z10.number().int().min(1).max(100).default(25)
1193
+ projectId: z11.string(),
1194
+ localeId: z11.string(),
1195
+ type: z11.string().optional(),
1196
+ skip: z11.number().int().min(0).default(0),
1197
+ take: z11.number().int().min(1).max(100).default(25)
1142
1198
  },
1143
1199
  annotations: { readOnlyHint: true, openWorldHint: true }
1144
1200
  },
@@ -1147,7 +1203,7 @@ function registerQaTools(server, gql) {
1147
1203
  }
1148
1204
 
1149
1205
  // src/tools/styleguides.ts
1150
- import { z as z11 } from "zod";
1206
+ import { z as z12 } from "zod";
1151
1207
  import { CREATE_STYLE_GUIDE, GENERATE_STYLE_GUIDE, LIST_STYLE_GUIDES as LIST_STYLE_GUIDES2 } from "@syntholoc/sdk";
1152
1208
  function registerStyleGuideTools(server, gql) {
1153
1209
  server.registerTool(
@@ -1155,7 +1211,7 @@ function registerStyleGuideTools(server, gql) {
1155
1211
  {
1156
1212
  title: "List style guides",
1157
1213
  description: "Lists style guides (project-scoped if projectId given, else org-wide).",
1158
- inputSchema: { projectId: z11.string().optional() },
1214
+ inputSchema: { projectId: z12.string().optional() },
1159
1215
  annotations: { readOnlyHint: true, openWorldHint: true }
1160
1216
  },
1161
1217
  async ({ projectId }) => jsonResult((await gql.request(LIST_STYLE_GUIDES2, { projectId })).styleGuides)
@@ -1165,7 +1221,7 @@ function registerStyleGuideTools(server, gql) {
1165
1221
  {
1166
1222
  title: "Generate style guide (draft)",
1167
1223
  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.",
1168
- inputSchema: { projectId: z11.string(), localeCode: z11.string().optional() },
1224
+ inputSchema: { projectId: z12.string(), localeCode: z12.string().optional() },
1169
1225
  annotations: { openWorldHint: true }
1170
1226
  },
1171
1227
  async ({ projectId, localeCode }) => jsonResult((await gql.request(GENERATE_STYLE_GUIDE, { input: { projectId, localeCode } })).generateStyleGuide)
@@ -1176,12 +1232,12 @@ function registerStyleGuideTools(server, gql) {
1176
1232
  title: "Create style guide",
1177
1233
  description: "Persists a style guide (tone, rules JSON, prompt fragment), project- or locale-scoped.",
1178
1234
  inputSchema: {
1179
- name: z11.string(),
1180
- projectId: z11.string().optional(),
1181
- localeCode: z11.string().optional(),
1182
- tone: z11.string().optional(),
1183
- promptFragment: z11.string().optional(),
1184
- rules: z11.array(z11.string()).optional()
1235
+ name: z12.string(),
1236
+ projectId: z12.string().optional(),
1237
+ localeCode: z12.string().optional(),
1238
+ tone: z12.string().optional(),
1239
+ promptFragment: z12.string().optional(),
1240
+ rules: z12.array(z12.string()).optional()
1185
1241
  },
1186
1242
  annotations: { openWorldHint: true }
1187
1243
  },
@@ -1190,7 +1246,7 @@ function registerStyleGuideTools(server, gql) {
1190
1246
  }
1191
1247
 
1192
1248
  // src/tools/tm.ts
1193
- import { z as z12 } from "zod";
1249
+ import { z as z13 } from "zod";
1194
1250
  import { TM_ENTRIES, TM_SEARCH, TM_STATS as TM_STATS2 } from "@syntholoc/sdk";
1195
1251
  function registerTmTools(server, gql) {
1196
1252
  server.registerTool(
@@ -1199,10 +1255,10 @@ function registerTmTools(server, gql) {
1199
1255
  title: "Search translation memory",
1200
1256
  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.",
1201
1257
  inputSchema: {
1202
- sourceText: z12.string(),
1203
- targetLocale: z12.string(),
1204
- projectId: z12.string().optional(),
1205
- limit: z12.number().int().min(1).max(20).default(5)
1258
+ sourceText: z13.string(),
1259
+ targetLocale: z13.string(),
1260
+ projectId: z13.string().optional(),
1261
+ limit: z13.number().int().min(1).max(20).default(5)
1206
1262
  },
1207
1263
  annotations: { readOnlyHint: true, openWorldHint: true }
1208
1264
  },
@@ -1213,7 +1269,7 @@ function registerTmTools(server, gql) {
1213
1269
  {
1214
1270
  title: "Translation memory stats",
1215
1271
  description: "Entry counts per target locale (org-wide, or for a project with `projectId`).",
1216
- inputSchema: { projectId: z12.string().optional() },
1272
+ inputSchema: { projectId: z13.string().optional() },
1217
1273
  annotations: { readOnlyHint: true, openWorldHint: true }
1218
1274
  },
1219
1275
  async ({ projectId }) => jsonResult((await gql.request(TM_STATS2, { projectId })).tmStats)
@@ -1224,10 +1280,10 @@ function registerTmTools(server, gql) {
1224
1280
  title: "Browse translation memory",
1225
1281
  description: "Lists stored TM entries (paginated, newest first), optionally filtered by project and locale.",
1226
1282
  inputSchema: {
1227
- projectId: z12.string().optional(),
1228
- targetLocale: z12.string().optional(),
1229
- skip: z12.number().int().min(0).default(0),
1230
- take: z12.number().int().min(1).max(100).default(25)
1283
+ projectId: z13.string().optional(),
1284
+ targetLocale: z13.string().optional(),
1285
+ skip: z13.number().int().min(0).default(0),
1286
+ take: z13.number().int().min(1).max(100).default(25)
1231
1287
  },
1232
1288
  annotations: { readOnlyHint: true, openWorldHint: true }
1233
1289
  },
@@ -1236,7 +1292,7 @@ function registerTmTools(server, gql) {
1236
1292
  }
1237
1293
 
1238
1294
  // src/tools/translations.ts
1239
- import { z as z13 } from "zod";
1295
+ import { z as z14 } from "zod";
1240
1296
  import {
1241
1297
  APPROVE_TRANSLATION,
1242
1298
  EXPORT_LOCALE,
@@ -1255,9 +1311,9 @@ function registerTranslationTools(server, gql) {
1255
1311
  title: "Translate locale",
1256
1312
  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.",
1257
1313
  inputSchema: {
1258
- projectId: z13.string(),
1259
- localeId: z13.string(),
1260
- scope: z13.enum(["ALL", "UNAPPROVED", "UNTRANSLATED"]).default("UNTRANSLATED")
1314
+ projectId: z14.string(),
1315
+ localeId: z14.string(),
1316
+ scope: z14.enum(["ALL", "UNAPPROVED", "UNTRANSLATED"]).default("UNTRANSLATED")
1261
1317
  },
1262
1318
  annotations: { openWorldHint: true }
1263
1319
  },
@@ -1268,7 +1324,7 @@ function registerTranslationTools(server, gql) {
1268
1324
  {
1269
1325
  title: "Translate key",
1270
1326
  description: "Queues AI translation of a single key into a locale. Returns tracking `jobIds`.",
1271
- inputSchema: { projectId: z13.string(), keyId: z13.string(), localeId: z13.string() },
1327
+ inputSchema: { projectId: z14.string(), keyId: z14.string(), localeId: z14.string() },
1272
1328
  annotations: { openWorldHint: true }
1273
1329
  },
1274
1330
  async (vars) => jsonResult((await gql.request(TRANSLATE_KEY, vars)).translateKey)
@@ -1279,11 +1335,11 @@ function registerTranslationTools(server, gql) {
1279
1335
  title: "Set translation",
1280
1336
  description: "Manually sets the translation text for a key in a locale.",
1281
1337
  inputSchema: {
1282
- projectId: z13.string(),
1283
- keyId: z13.string(),
1284
- localeId: z13.string(),
1285
- text: z13.string(),
1286
- origin: z13.string().optional()
1338
+ projectId: z14.string(),
1339
+ keyId: z14.string(),
1340
+ localeId: z14.string(),
1341
+ text: z14.string(),
1342
+ origin: z14.string().optional()
1287
1343
  },
1288
1344
  annotations: { idempotentHint: true, openWorldHint: true }
1289
1345
  },
@@ -1297,10 +1353,10 @@ function registerTranslationTools(server, gql) {
1297
1353
  title: "List translations",
1298
1354
  description: "Lists translations for a locale with status, origin, model and QA score.",
1299
1355
  inputSchema: {
1300
- projectId: z13.string(),
1301
- localeId: z13.string(),
1302
- skip: z13.number().int().min(0).default(0),
1303
- take: z13.number().int().min(1).max(200).default(25)
1356
+ projectId: z14.string(),
1357
+ localeId: z14.string(),
1358
+ skip: z14.number().int().min(0).default(0),
1359
+ take: z14.number().int().min(1).max(200).default(25)
1304
1360
  },
1305
1361
  annotations: { readOnlyHint: true, openWorldHint: true }
1306
1362
  },
@@ -1311,7 +1367,7 @@ function registerTranslationTools(server, gql) {
1311
1367
  {
1312
1368
  title: "Approve translation",
1313
1369
  description: "Marks a translation APPROVED.",
1314
- inputSchema: { projectId: z13.string(), translationId: z13.string() },
1370
+ inputSchema: { projectId: z14.string(), translationId: z14.string() },
1315
1371
  annotations: { idempotentHint: true, openWorldHint: true }
1316
1372
  },
1317
1373
  async ({ projectId, translationId }) => jsonResult((await gql.request(APPROVE_TRANSLATION, { projectId, id: translationId })).approveTranslation)
@@ -1321,7 +1377,7 @@ function registerTranslationTools(server, gql) {
1321
1377
  {
1322
1378
  title: "Reject translation",
1323
1379
  description: "Marks a translation REJECTED (queued for re-translation).",
1324
- inputSchema: { projectId: z13.string(), translationId: z13.string() },
1380
+ inputSchema: { projectId: z14.string(), translationId: z14.string() },
1325
1381
  annotations: { idempotentHint: true, openWorldHint: true }
1326
1382
  },
1327
1383
  async ({ projectId, translationId }) => jsonResult((await gql.request(REJECT_TRANSLATION, { projectId, id: translationId })).rejectTranslation)
@@ -1332,12 +1388,12 @@ function registerTranslationTools(server, gql) {
1332
1388
  title: "Export locale",
1333
1389
  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.',
1334
1390
  inputSchema: {
1335
- projectId: z13.string(),
1336
- localeId: z13.string(),
1337
- format: z13.enum(FORMATS2).default("JSON"),
1338
- onlyApproved: z13.boolean().optional(),
1339
- sourceFile: z13.string().optional().describe('Real source file path to export, e.g. "messages/en/home.json" (reliable, preferred).'),
1340
- keyPrefix: z13.string().optional().describe('Legacy namespace prefix, e.g. "home".')
1391
+ projectId: z14.string(),
1392
+ localeId: z14.string(),
1393
+ format: z14.enum(FORMATS2).default("JSON"),
1394
+ onlyApproved: z14.boolean().optional(),
1395
+ sourceFile: z14.string().optional().describe('Real source file path to export, e.g. "messages/en/home.json" (reliable, preferred).'),
1396
+ keyPrefix: z14.string().optional().describe('Legacy namespace prefix, e.g. "home".')
1341
1397
  },
1342
1398
  annotations: { readOnlyHint: true, openWorldHint: true }
1343
1399
  },
@@ -1349,14 +1405,14 @@ function registerTranslationTools(server, gql) {
1349
1405
  title: "Import translations",
1350
1406
  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.",
1351
1407
  inputSchema: {
1352
- projectId: z13.string(),
1353
- localeId: z13.string(),
1354
- content: z13.string(),
1355
- format: z13.enum(FORMATS2),
1356
- keyPrefix: z13.string().optional().describe('Namespace prefix, e.g. "home" (per-file, multi-file projects).'),
1357
- autoApprove: z13.boolean().default(false),
1358
- conflict: z13.enum(["OVERWRITE", "SKIP_EXISTING", "KEEP_APPROVED"]).default("OVERWRITE"),
1359
- dryRun: z13.boolean().default(false)
1408
+ projectId: z14.string(),
1409
+ localeId: z14.string(),
1410
+ content: z14.string(),
1411
+ format: z14.enum(FORMATS2),
1412
+ keyPrefix: z14.string().optional().describe('Namespace prefix, e.g. "home" (per-file, multi-file projects).'),
1413
+ autoApprove: z14.boolean().default(false),
1414
+ conflict: z14.enum(["OVERWRITE", "SKIP_EXISTING", "KEEP_APPROVED"]).default("OVERWRITE"),
1415
+ dryRun: z14.boolean().default(false)
1360
1416
  },
1361
1417
  annotations: { idempotentHint: true, openWorldHint: true }
1362
1418
  },
@@ -1399,7 +1455,7 @@ function registerAllTools(server, gql, opts = {}) {
1399
1455
  }
1400
1456
 
1401
1457
  // package.json
1402
- var version = "0.3.0";
1458
+ var version = "0.4.0";
1403
1459
 
1404
1460
  // src/build-server.ts
1405
1461
  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-GNXMB53B.js";
5
+ } from "./chunk-P7LZNIW2.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.0",
3
+ "version": "0.4.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.2.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
  }