@syntholoc/mcp 0.2.0 → 0.3.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 +59 -14
- package/dist/build-server.js +1 -1
- package/dist/{chunk-DZKWHV2O.js → chunk-GNXMB53B.js} +326 -18
- package/dist/index.js +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
# @syntholoc/mcp
|
|
2
2
|
|
|
3
3
|
The [Syntholoc](https://syntholoc.com) MCP server — manage AI localization and
|
|
4
|
-
translation straight from
|
|
5
|
-
|
|
4
|
+
translation straight from Codex, Claude Code, or any MCP client without leaving
|
|
5
|
+
your editor.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Hosted OAuth
|
|
8
|
+
|
|
9
|
+
Use the hosted Streamable HTTP endpoint when your MCP client supports OAuth.
|
|
10
|
+
|
|
11
|
+
### Codex
|
|
12
|
+
|
|
13
|
+
Add this to `~/.codex/config.toml` (or project `.codex/config.toml` in a trusted
|
|
14
|
+
project):
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[mcp_servers.syntholoc]
|
|
18
|
+
url = "https://api.syntholoc.com/mcp/v1"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then authorize:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
codex mcp login syntholoc
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Claude Code
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude mcp add --transport http syntholoc https://api.syntholoc.com/mcp/v1
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Local stdio
|
|
34
|
+
|
|
35
|
+
Use local stdio when you want to run the MCP server as a local process with a
|
|
36
|
+
machine API key.
|
|
37
|
+
|
|
38
|
+
### Codex
|
|
39
|
+
|
|
40
|
+
Add this to `~/.codex/config.toml` (or project `.codex/config.toml`):
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[mcp_servers.syntholoc]
|
|
44
|
+
command = "bunx"
|
|
45
|
+
args = ["@syntholoc/mcp"]
|
|
46
|
+
|
|
47
|
+
[mcp_servers.syntholoc.env]
|
|
48
|
+
SYNTHOLOC_API_KEY = "<your-api-key>"
|
|
49
|
+
SYNTHOLOC_API_URL = "https://api.syntholoc.com/graphql"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Claude Code
|
|
8
53
|
|
|
9
54
|
```bash
|
|
10
55
|
claude mcp add --transport stdio syntholoc \
|
|
@@ -18,13 +63,13 @@ never passed as a tool argument.
|
|
|
18
63
|
|
|
19
64
|
### Environment variables
|
|
20
65
|
|
|
21
|
-
| Variable
|
|
22
|
-
|
|
23
|
-
| `SYNTHOLOC_API_KEY`
|
|
24
|
-
| `SYNTHOLOC_API_URL`
|
|
25
|
-
| `SYNTHOLOC_TOOL_SETS`
|
|
26
|
-
| `SYNTHOLOC_READ_ONLY`
|
|
27
|
-
| `SYNTHOLOC_TOOL_FILTER` |
|
|
66
|
+
| Variable | Default | Description |
|
|
67
|
+
| ----------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `SYNTHOLOC_API_KEY` | – | Machine API key (`x-api-key`). Required for most tools. |
|
|
69
|
+
| `SYNTHOLOC_API_URL` | `https://api.syntholoc.com/graphql` | GraphQL endpoint. Override to target a self-hosted or local instance. |
|
|
70
|
+
| `SYNTHOLOC_TOOL_SETS` | _(all)_ | Comma-separated tool sets to load (`projects,keys,translations,qa,glossary,styleguides,tm,git,providers,jobs,analytics,locales,config`). Fewer tools = a more focused agent. |
|
|
71
|
+
| `SYNTHOLOC_READ_ONLY` | `false` | Set `true` to expose only read-only tools (list/get) — safe exploration and onboarding. |
|
|
72
|
+
| `SYNTHOLOC_TOOL_FILTER` | _(none)_ | Comma-separated wildcard patterns matched against tool names (e.g. `list_*,get_*`). |
|
|
28
73
|
|
|
29
74
|
> The remote (HTTP) transport accepts the same controls as request headers:
|
|
30
75
|
> `syntholoc-tool-sets`, `syntholoc-read-only`, `syntholoc-tool-filter`. The hosted
|
|
@@ -59,10 +104,10 @@ and analytics — plus MCP resources and prompts for common workflows.
|
|
|
59
104
|
|
|
60
105
|
Typical asks, one sentence each:
|
|
61
106
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
107
|
+
- _"Translate the untranslated German strings and wait for it to finish."_
|
|
108
|
+
- _"Show project progress and the QA score."_
|
|
109
|
+
- _"Add these glossary terms and re-run QA on French."_
|
|
110
|
+
- _"Export the Spanish locale."_
|
|
66
111
|
|
|
67
112
|
Translation runs asynchronously and returns a job id; a `wait_for_job` tool
|
|
68
113
|
blocks until completion, so you can chain "translate → wait → export" in a
|
package/dist/build-server.js
CHANGED
|
@@ -88,23 +88,47 @@ function registerPrompts(server) {
|
|
|
88
88
|
},
|
|
89
89
|
() => userMessage(
|
|
90
90
|
[
|
|
91
|
-
"Set up Syntholoc for this repository
|
|
91
|
+
"Set up Syntholoc for this repository. PREFER the Git-sync path \u2014 it pulls every",
|
|
92
|
+
"source string (and any existing translations) in one server-side sync and keeps",
|
|
93
|
+
"the repo in sync going forward. Only fall back to manual imports if the user",
|
|
94
|
+
"declines Git or the code lives outside a supported host.",
|
|
95
|
+
"",
|
|
92
96
|
"1. Find the i18n source files and infer the source locale. Note the LAYOUT:",
|
|
93
97
|
" - Single file per locale (e.g. messages/en.json) \u2192 one mapping, no namespace.",
|
|
94
98
|
" - A directory per locale with many namespace files (e.g. messages/en/home.json, common.json) \u2192",
|
|
95
|
-
" each file is a NAMESPACE. Syntholoc stores a flat key space, so
|
|
99
|
+
" each file is a NAMESPACE. Syntholoc stores a flat key space, so each file is imported under a",
|
|
96
100
|
" `keyPrefix` = its namespace to avoid collisions between files.",
|
|
97
101
|
"2. `create_project`, then `seed_starter_locales` (or `add_locale`) for the target languages. Use valid",
|
|
98
|
-
' BCP-47 codes (e.g. "zh-CN", not "cn"); `add_locale` accepts `code` or `locale`.',
|
|
99
|
-
"
|
|
102
|
+
' BCP-47 codes (e.g. "zh-CN", not "cn"); `add_locale` accepts `code` or `locale`. Region-ambiguous',
|
|
103
|
+
' languages MUST be qualified \u2014 "pt" \u2192 "pt-BR" or "pt-PT", "zh" \u2192 "zh-CN" or "zh-TW". The tools reject',
|
|
104
|
+
' ambiguous codes and return the candidates; if a source filename is just "pt", ask which region (or',
|
|
105
|
+
" default to the likeliest) and record the file\u2192locale mapping in the config `translation` pattern.",
|
|
106
|
+
" (Target locales must exist before a Git pull so their translation files import.)",
|
|
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:",
|
|
110
|
+
" - GitHub App (no token, recommended): `get_github_app_install_url` \u2192 give the URL to the user",
|
|
111
|
+
" to open and authorize (you cannot click it for them; you may offer to open it via the shell).",
|
|
112
|
+
" Once they confirm, `list_github_installations` \u2192 `list_github_installation_repos` to pick the",
|
|
113
|
+
" repo, then `connect_repository` with that `installationId`.",
|
|
114
|
+
" - Token (any host): ask the user to paste a personal access token (repo read + PR scope), then",
|
|
115
|
+
" `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.",
|
|
120
|
+
" Then `pull_translations` to import any translation files already in the repo (choose `conflict`",
|
|
121
|
+
" 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.",
|
|
123
|
+
"4. FALLBACK \u2014 file-based import when the user does NOT want Git integration:",
|
|
100
124
|
" - Single file \u2192 `import_source_strings` once.",
|
|
101
125
|
" - Namespaced \u2192 `import_source_strings` once PER FILE with `keyPrefix` set to the namespace",
|
|
102
126
|
" (filename without extension). Keys become e.g. `home.title`, `common.title`.",
|
|
103
|
-
"
|
|
127
|
+
"5. Write `.syntholoc/config.json` (schema https://syntholoc.com/schemas/config.json): the `projectId` and",
|
|
104
128
|
" a Crowdin-style `files` array of `{ source, translation }` patterns with a `{locale}` placeholder,",
|
|
105
129
|
' e.g. { "source": "messages/en/home.json", "translation": "messages/{locale}/home.json", "namespace": "home" }.',
|
|
106
130
|
' Do NOT invent fields like `i18nDir`, `format: "nested-dirs"`, or `localeMapping` \u2014 they are not read.',
|
|
107
|
-
"
|
|
131
|
+
"6. Offer to run the localize_changed flow next."
|
|
108
132
|
].join("\n")
|
|
109
133
|
)
|
|
110
134
|
);
|
|
@@ -148,7 +172,10 @@ function registerResources(server, gql) {
|
|
|
148
172
|
description: "Organizations you belong to (id, name, slug, your role). Pin one for a request with the `syntholoc-org-id` header; otherwise the earliest org is used.",
|
|
149
173
|
mimeType: "application/json"
|
|
150
174
|
},
|
|
151
|
-
async (uri) => json(
|
|
175
|
+
async (uri) => json(
|
|
176
|
+
uri,
|
|
177
|
+
(await gql.request(MY_ORGANIZATIONS)).myProjects.map((g) => ({ ...g.organization, viewerRole: g.viewerRole }))
|
|
178
|
+
)
|
|
152
179
|
);
|
|
153
180
|
server.registerResource(
|
|
154
181
|
"projects",
|
|
@@ -334,11 +361,23 @@ function registerConfigTools(server, gql) {
|
|
|
334
361
|
// src/tools/git.ts
|
|
335
362
|
import { z as z3 } from "zod";
|
|
336
363
|
import {
|
|
364
|
+
CONNECT_REPOSITORY,
|
|
365
|
+
DISCONNECT_REPOSITORY,
|
|
366
|
+
GITHUB_APP,
|
|
367
|
+
GITHUB_INSTALLATIONS,
|
|
368
|
+
GITHUB_INSTALLATION_REPOS,
|
|
337
369
|
LIST_REPOSITORIES,
|
|
338
370
|
PULL_TRANSLATIONS,
|
|
339
371
|
PUSH_TRANSLATIONS,
|
|
340
|
-
SYNC_REPOSITORY
|
|
372
|
+
SYNC_REPOSITORY,
|
|
373
|
+
UPDATE_REPOSITORY
|
|
341
374
|
} from "@syntholoc/sdk";
|
|
375
|
+
var fileMapping = z3.object({
|
|
376
|
+
source: z3.string().describe('Source file path or glob, e.g. "messages/en.json" or "locales/en/*.json".'),
|
|
377
|
+
translation: z3.string().describe(
|
|
378
|
+
'Translation pattern with Crowdin placeholders, e.g. "messages/%two_letters_code%/%original_file_name%" or "locales/%locale%/%original_file_name%".'
|
|
379
|
+
)
|
|
380
|
+
});
|
|
342
381
|
function registerGitTools(server, gql) {
|
|
343
382
|
server.registerTool(
|
|
344
383
|
"list_repositories",
|
|
@@ -350,6 +389,92 @@ function registerGitTools(server, gql) {
|
|
|
350
389
|
},
|
|
351
390
|
async ({ projectId }) => jsonResult((await gql.request(LIST_REPOSITORIES, { projectId })).repositories)
|
|
352
391
|
);
|
|
392
|
+
server.registerTool(
|
|
393
|
+
"get_github_app_install_url",
|
|
394
|
+
{
|
|
395
|
+
title: "GitHub App install URL",
|
|
396
|
+
description: "Returns whether the Syntholoc GitHub App is available and a signed install URL for this project. GitHub App connect flow, step 1: give this URL to the user to open in a browser and authorize Syntholoc on the repositories they want (this authorize click cannot be automated). After they finish, call `list_github_installations` to see the new installation. If `enabled` is false, the server has no GitHub App configured \u2014 fall back to the token (PAT) path via `connect_repository`.",
|
|
397
|
+
inputSchema: { projectId: z3.string() },
|
|
398
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
399
|
+
},
|
|
400
|
+
async ({ projectId }) => jsonResult((await gql.request(GITHUB_APP, { projectId })).githubApp)
|
|
401
|
+
);
|
|
402
|
+
server.registerTool(
|
|
403
|
+
"list_github_installations",
|
|
404
|
+
{
|
|
405
|
+
title: "List GitHub installations",
|
|
406
|
+
description: "Lists GitHub App installations available to the active organization. GitHub App connect flow, step 2: run this after the user authorized the App (see `get_github_app_install_url`) to get the `installationId` to pass to `list_github_installation_repos` and `connect_repository`.",
|
|
407
|
+
inputSchema: { projectId: z3.string() },
|
|
408
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
409
|
+
},
|
|
410
|
+
async ({ projectId }) => jsonResult((await gql.request(GITHUB_INSTALLATIONS, { projectId })).githubInstallations)
|
|
411
|
+
);
|
|
412
|
+
server.registerTool(
|
|
413
|
+
"list_github_installation_repos",
|
|
414
|
+
{
|
|
415
|
+
title: "List installation repositories",
|
|
416
|
+
description: "Lists the repositories Syntholoc can access through a GitHub App installation. GitHub App connect flow, step 3: pick the target repo (its `fullName` and `defaultBranch`) before calling `connect_repository` with the same `installationId`.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
projectId: z3.string(),
|
|
419
|
+
installationId: z3.number().int().describe("From `list_github_installations`.")
|
|
420
|
+
},
|
|
421
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
422
|
+
},
|
|
423
|
+
async ({ projectId, installationId }) => jsonResult((await gql.request(GITHUB_INSTALLATION_REPOS, { projectId, installationId })).githubInstallationRepos)
|
|
424
|
+
);
|
|
425
|
+
server.registerTool(
|
|
426
|
+
"connect_repository",
|
|
427
|
+
{
|
|
428
|
+
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.",
|
|
430
|
+
inputSchema: {
|
|
431
|
+
projectId: z3.string(),
|
|
432
|
+
provider: z3.enum(["GITHUB", "GITLAB", "BITBUCKET"]).default("GITHUB"),
|
|
433
|
+
fullName: z3.string().describe('owner/repo, e.g. "acme/webapp".'),
|
|
434
|
+
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`)."),
|
|
437
|
+
installationId: z3.number().int().optional().describe("GitHub App installation id. Mutually exclusive with `token`."),
|
|
438
|
+
token: z3.string().optional().describe("Access token (PAT mode), stored encrypted. Mutually exclusive with `installationId`."),
|
|
439
|
+
translationBranch: z3.string().optional().describe("Branch translations are committed to (default l10n_<branch>)."),
|
|
440
|
+
prTargetBranch: z3.string().optional().describe("Branch translation PRs target (default = branch)."),
|
|
441
|
+
autoSyncEnabled: z3.boolean().optional().describe("Enable scheduled auto-sync.")
|
|
442
|
+
},
|
|
443
|
+
annotations: { openWorldHint: true }
|
|
444
|
+
},
|
|
445
|
+
async ({ projectId, ...input }) => jsonResult((await gql.request(CONNECT_REPOSITORY, { projectId, input })).connectRepository)
|
|
446
|
+
);
|
|
447
|
+
server.registerTool(
|
|
448
|
+
"update_repository",
|
|
449
|
+
{
|
|
450
|
+
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.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
projectId: z3.string(),
|
|
454
|
+
repositoryId: z3.string(),
|
|
455
|
+
branch: z3.string().optional().describe("Source branch."),
|
|
456
|
+
files: z3.array(fileMapping).optional().describe("Crowdin-style source\u2192translation mappings (replaces existing)."),
|
|
457
|
+
paths: z3.array(z3.string()).optional().describe("Simple mode: source file paths/globs to track (replaces existing; use instead of `files`)."),
|
|
458
|
+
translationBranch: z3.string().optional().describe("Branch translations are committed to."),
|
|
459
|
+
prTargetBranch: z3.string().optional().describe("Branch translation PRs target."),
|
|
460
|
+
pushSources: z3.boolean().optional().describe("Rewrite the source file(s) in the repo from in-app keys."),
|
|
461
|
+
autoSyncEnabled: z3.boolean().optional().describe("Enable scheduled auto-sync."),
|
|
462
|
+
syncIntervalHours: z3.number().int().optional().describe("Auto-sync interval in hours.")
|
|
463
|
+
},
|
|
464
|
+
annotations: { openWorldHint: true }
|
|
465
|
+
},
|
|
466
|
+
async ({ projectId, repositoryId, ...input }) => jsonResult((await gql.request(UPDATE_REPOSITORY, { projectId, id: repositoryId, input })).updateRepository)
|
|
467
|
+
);
|
|
468
|
+
server.registerTool(
|
|
469
|
+
"disconnect_repository",
|
|
470
|
+
{
|
|
471
|
+
title: "Disconnect repository",
|
|
472
|
+
description: "Removes the link to a connected repository and its stored credentials. The repository itself is not affected. Destructive \u2014 confirm with the user first.",
|
|
473
|
+
inputSchema: { projectId: z3.string(), repositoryId: z3.string() },
|
|
474
|
+
annotations: { destructiveHint: true, openWorldHint: true }
|
|
475
|
+
},
|
|
476
|
+
async ({ projectId, repositoryId }) => jsonResult((await gql.request(DISCONNECT_REPOSITORY, { projectId, id: repositoryId })).disconnectRepository)
|
|
477
|
+
);
|
|
353
478
|
server.registerTool(
|
|
354
479
|
"sync_repository",
|
|
355
480
|
{
|
|
@@ -521,7 +646,7 @@ import {
|
|
|
521
646
|
UPDATE_KEY,
|
|
522
647
|
UPSERT_KEYS
|
|
523
648
|
} from "@syntholoc/sdk";
|
|
524
|
-
var FORMATS = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS"];
|
|
649
|
+
var FORMATS = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS", "NJK"];
|
|
525
650
|
function registerKeyTools(server, gql) {
|
|
526
651
|
server.registerTool(
|
|
527
652
|
"list_keys",
|
|
@@ -591,13 +716,15 @@ function registerKeyTools(server, gql) {
|
|
|
591
716
|
"import_source_strings",
|
|
592
717
|
{
|
|
593
718
|
title: "Import source strings",
|
|
594
|
-
description: 'Imports source strings from ONE file (paste the raw content) and returns the diff summary. 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.',
|
|
719
|
+
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.',
|
|
595
720
|
inputSchema: {
|
|
596
721
|
projectId: z6.string(),
|
|
597
722
|
content: z6.string(),
|
|
598
723
|
format: z6.enum(FORMATS),
|
|
599
724
|
keyPrefix: z6.string().optional().describe('Namespace prefix, e.g. "home" (per-file, multi-file projects).'),
|
|
600
|
-
sourceFile: z6.string().optional().describe(
|
|
725
|
+
sourceFile: z6.string().optional().describe(
|
|
726
|
+
'Real source file path, e.g. "messages/en/home.json" \u2014 stored for the file tree + reliable export.'
|
|
727
|
+
),
|
|
601
728
|
dryRun: z6.boolean().default(false)
|
|
602
729
|
},
|
|
603
730
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
@@ -614,6 +741,158 @@ function registerKeyTools(server, gql) {
|
|
|
614
741
|
// src/tools/locales.ts
|
|
615
742
|
import { z as z7 } from "zod";
|
|
616
743
|
import { ADD_LOCALE, LIST_LOCALES as LIST_LOCALES2, REMOVE_LOCALE, SEED_STARTER_LOCALES } from "@syntholoc/sdk";
|
|
744
|
+
|
|
745
|
+
// ../../packages/shared/src/locales.ts
|
|
746
|
+
var STARTER_LOCALES = [
|
|
747
|
+
{ code: "ar", name: "Arabic", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", rtl: true },
|
|
748
|
+
{ code: "az", name: "Azerbaijani", nativeName: "Az\u0259rbaycan" },
|
|
749
|
+
{ code: "be", name: "Belarusian", nativeName: "\u0411\u0435\u043B\u0430\u0440\u0443\u0441\u043A\u0430\u044F" },
|
|
750
|
+
{ code: "bn", name: "Bengali", nativeName: "\u09AC\u09BE\u0982\u09B2\u09BE" },
|
|
751
|
+
{ code: "cs", name: "Czech", nativeName: "\u010Ce\u0161tina" },
|
|
752
|
+
{ code: "de", name: "German", nativeName: "Deutsch" },
|
|
753
|
+
{ code: "el", name: "Greek", nativeName: "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC" },
|
|
754
|
+
{ code: "en", name: "English", nativeName: "English" },
|
|
755
|
+
{ code: "es", name: "Spanish", nativeName: "Espa\xF1ol" },
|
|
756
|
+
{ code: "es-419", name: "Spanish (Latin America)", nativeName: "Espa\xF1ol (Latinoam\xE9rica)" },
|
|
757
|
+
{ code: "fa", name: "Persian", nativeName: "\u0641\u0627\u0631\u0633\u06CC", rtl: true },
|
|
758
|
+
{ code: "fr", name: "French", nativeName: "Fran\xE7ais" },
|
|
759
|
+
{ code: "hi", name: "Hindi", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940" },
|
|
760
|
+
{ code: "hy", name: "Armenian", nativeName: "\u0540\u0561\u0575\u0565\u0580\u0565\u0576" },
|
|
761
|
+
{ code: "id", name: "Indonesian", nativeName: "Bahasa Indonesia" },
|
|
762
|
+
{ code: "it", name: "Italian", nativeName: "Italiano" },
|
|
763
|
+
{ code: "ja", name: "Japanese", nativeName: "\u65E5\u672C\u8A9E" },
|
|
764
|
+
{ code: "ka", name: "Georgian", nativeName: "\u10E5\u10D0\u10E0\u10D7\u10E3\u10DA\u10D8" },
|
|
765
|
+
{ code: "kk", name: "Kazakh", nativeName: "\u049A\u0430\u0437\u0430\u049B \u0442\u0456\u043B\u0456" },
|
|
766
|
+
{ code: "ko", name: "Korean", nativeName: "\uD55C\uAD6D\uC5B4" },
|
|
767
|
+
{ code: "lv", name: "Latvian", nativeName: "Latvie\u0161u" },
|
|
768
|
+
{ code: "ms", name: "Malay", nativeName: "Bahasa Melayu" },
|
|
769
|
+
{ code: "nl", name: "Dutch", nativeName: "Nederlands" },
|
|
770
|
+
{ code: "pl", name: "Polish", nativeName: "Polski" },
|
|
771
|
+
{ code: "pt-BR", name: "Portuguese (Brazil)", nativeName: "Portugu\xEAs (Brasil)" },
|
|
772
|
+
{ code: "pt-PT", name: "Portuguese (Portugal)", nativeName: "Portugu\xEAs (Portugal)" },
|
|
773
|
+
{ code: "ro", name: "Romanian", nativeName: "Rom\xE2n\u0103" },
|
|
774
|
+
{ code: "ru", name: "Russian", nativeName: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
|
|
775
|
+
{ code: "sk", name: "Slovak", nativeName: "Sloven\u010Dina" },
|
|
776
|
+
{ code: "sl", name: "Slovenian", nativeName: "Sloven\u0161\u010Dina" },
|
|
777
|
+
{ code: "sw", name: "Swahili", nativeName: "Kiswahili" },
|
|
778
|
+
{ code: "th", name: "Thai", nativeName: "\u0E44\u0E17\u0E22" },
|
|
779
|
+
{ code: "tl", name: "Filipino", nativeName: "Filipino" },
|
|
780
|
+
{ code: "tr", name: "Turkish", nativeName: "T\xFCrk\xE7e" },
|
|
781
|
+
{ code: "uk", name: "Ukrainian", nativeName: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
|
|
782
|
+
{ code: "ur", name: "Urdu", nativeName: "\u0627\u0631\u062F\u0648", rtl: true },
|
|
783
|
+
{ code: "uz", name: "Uzbek", nativeName: "O\u02BBzbek" },
|
|
784
|
+
{ code: "vi", name: "Vietnamese", nativeName: "Ti\u1EBFng Vi\u1EC7t" },
|
|
785
|
+
{ code: "zh-CN", name: "Chinese (Simplified)", nativeName: "\u7B80\u4F53\u4E2D\u6587" },
|
|
786
|
+
{ code: "zh-TW", name: "Chinese (Traditional)", nativeName: "\u7E41\u9AD4\u4E2D\u6587" }
|
|
787
|
+
];
|
|
788
|
+
var STARTER_LOCALE_CODES = STARTER_LOCALES.map((l) => l.code);
|
|
789
|
+
var STARTER_BY_CODE = new Map(STARTER_LOCALES.map((l) => [l.code, l]));
|
|
790
|
+
function findStarterLocale(code) {
|
|
791
|
+
return STARTER_BY_CODE.get(code);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ../../packages/shared/src/locale-resolve.ts
|
|
795
|
+
var ALIASES = {
|
|
796
|
+
cn: "zh-CN",
|
|
797
|
+
"zh-hans": "zh-CN",
|
|
798
|
+
"zh-hant": "zh-TW",
|
|
799
|
+
jp: "ja",
|
|
800
|
+
kr: "ko",
|
|
801
|
+
ua: "uk",
|
|
802
|
+
cz: "cs",
|
|
803
|
+
gr: "el",
|
|
804
|
+
iw: "he",
|
|
805
|
+
// legacy Hebrew
|
|
806
|
+
in: "id"
|
|
807
|
+
// legacy Indonesian
|
|
808
|
+
};
|
|
809
|
+
var VARIANTS_BY_LANG = (() => {
|
|
810
|
+
const map = /* @__PURE__ */ new Map();
|
|
811
|
+
for (const { code } of STARTER_LOCALES) {
|
|
812
|
+
const lang = code.split("-")[0].toLowerCase();
|
|
813
|
+
(map.get(lang) ?? map.set(lang, []).get(lang)).push(code);
|
|
814
|
+
}
|
|
815
|
+
return map;
|
|
816
|
+
})();
|
|
817
|
+
function canonicalize(input) {
|
|
818
|
+
try {
|
|
819
|
+
return Intl.getCanonicalLocales(input)[0] ?? null;
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function isRealLanguage(code) {
|
|
825
|
+
try {
|
|
826
|
+
const name = new Intl.DisplayNames(["en"], { type: "language" }).of(code);
|
|
827
|
+
return !!name && name.toLowerCase() !== code.toLowerCase();
|
|
828
|
+
} catch {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function resolveLocaleCode(raw) {
|
|
833
|
+
const input = raw.trim();
|
|
834
|
+
if (!input) {
|
|
835
|
+
return { status: "unknown", input: raw, message: "Empty locale code." };
|
|
836
|
+
}
|
|
837
|
+
const alias = ALIASES[input.toLowerCase()];
|
|
838
|
+
if (alias) {
|
|
839
|
+
return { status: "normalized", code: alias, from: input, reason: `mapped "${input}" to catalog code "${alias}"` };
|
|
840
|
+
}
|
|
841
|
+
const canonical = canonicalize(input);
|
|
842
|
+
if (!canonical) {
|
|
843
|
+
return { status: "unknown", input, message: `"${input}" is not a valid BCP-47 locale code.` };
|
|
844
|
+
}
|
|
845
|
+
if (findStarterLocale(canonical)) {
|
|
846
|
+
return canonical === input ? { status: "ok", code: canonical } : { status: "normalized", code: canonical, from: input, reason: `casing normalized to "${canonical}"` };
|
|
847
|
+
}
|
|
848
|
+
const lang = canonical.split("-")[0].toLowerCase();
|
|
849
|
+
const hasRegionOrScript = canonical.includes("-");
|
|
850
|
+
const variants = VARIANTS_BY_LANG.get(lang) ?? [];
|
|
851
|
+
if (!hasRegionOrScript) {
|
|
852
|
+
if (variants.length > 1) {
|
|
853
|
+
return {
|
|
854
|
+
status: "ambiguous",
|
|
855
|
+
input,
|
|
856
|
+
candidates: variants,
|
|
857
|
+
message: `"${input}" is ambiguous \u2014 the catalog has ${variants.join(", ")}. Pick one.`
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (variants.length === 1) {
|
|
861
|
+
return {
|
|
862
|
+
status: "normalized",
|
|
863
|
+
code: variants[0],
|
|
864
|
+
from: input,
|
|
865
|
+
reason: `expanded "${input}" to "${variants[0]}"`
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
return isRealLanguage(canonical) ? { status: "ok", code: canonical } : { status: "unknown", input, message: `"${input}" is not a recognized language.` };
|
|
869
|
+
}
|
|
870
|
+
if (!isRealLanguage(canonical)) {
|
|
871
|
+
return { status: "unknown", input, message: `"${input}" is not a valid locale.` };
|
|
872
|
+
}
|
|
873
|
+
return canonical === input ? { status: "ok", code: canonical } : { status: "normalized", code: canonical, from: input, reason: `canonicalized to "${canonical}"` };
|
|
874
|
+
}
|
|
875
|
+
function resolveLocaleCodes(raw) {
|
|
876
|
+
const out = { codes: [], normalized: [], rejected: [] };
|
|
877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
878
|
+
for (const code of raw) {
|
|
879
|
+
const res = resolveLocaleCode(code);
|
|
880
|
+
if (res.status === "ambiguous") {
|
|
881
|
+
out.rejected.push({ input: res.input, message: res.message, candidates: res.candidates });
|
|
882
|
+
} else if (res.status === "unknown") {
|
|
883
|
+
out.rejected.push({ input: res.input, message: res.message });
|
|
884
|
+
} else {
|
|
885
|
+
if (res.status === "normalized") out.normalized.push({ from: res.from, code: res.code, reason: res.reason });
|
|
886
|
+
if (!seen.has(res.code)) {
|
|
887
|
+
seen.add(res.code);
|
|
888
|
+
out.codes.push(res.code);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return out;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/tools/locales.ts
|
|
617
896
|
function registerLocaleTools(server, gql) {
|
|
618
897
|
server.registerTool(
|
|
619
898
|
"list_locales",
|
|
@@ -629,7 +908,7 @@ function registerLocaleTools(server, gql) {
|
|
|
629
908
|
"add_locale",
|
|
630
909
|
{
|
|
631
910
|
title: "Add locale",
|
|
632
|
-
description:
|
|
911
|
+
description: 'Adds ONE target locale to a project. To add several at once, prefer seed_starter_locales (one call for the whole list). Codes are validated: aliases/casing are normalized (e.g. "cn" \u2192 "zh-CN"), and ambiguous macro-languages (e.g. "pt", "zh") are rejected with the concrete variants to choose from (pt-BR / pt-PT, zh-CN / zh-TW).',
|
|
633
912
|
inputSchema: {
|
|
634
913
|
projectId: z7.string(),
|
|
635
914
|
// `code` and `locale` are aliases — accept either so the natural
|
|
@@ -650,22 +929,48 @@ function registerLocaleTools(server, gql) {
|
|
|
650
929
|
error: 'Missing locale code. Pass `code` (BCP-47, e.g. "de-DE" or "ru"). To add several locales at once, use seed_starter_locales with a `codes` array.'
|
|
651
930
|
});
|
|
652
931
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
932
|
+
const res = resolveLocaleCode(resolvedCode);
|
|
933
|
+
if (res.status === "ambiguous") {
|
|
934
|
+
return jsonResult({
|
|
935
|
+
error: res.message,
|
|
936
|
+
hint: `Retry add_locale with one of: ${res.candidates.join(", ")}.`,
|
|
937
|
+
candidates: res.candidates
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
if (res.status === "unknown") {
|
|
941
|
+
return jsonResult({ error: res.message, hint: 'Use a valid BCP-47 code, e.g. "de-DE" or "zh-CN".' });
|
|
942
|
+
}
|
|
943
|
+
const input = { code: res.code, name, formality, gender, variant };
|
|
944
|
+
const added = (await gql.request(ADD_LOCALE, { projectId, input })).addLocale;
|
|
945
|
+
return res.status === "normalized" ? jsonResult({ ...added, note: `Interpreted "${res.from}" as "${res.code}" (${res.reason}).` }) : jsonResult(added);
|
|
655
946
|
}
|
|
656
947
|
);
|
|
657
948
|
server.registerTool(
|
|
658
949
|
"seed_starter_locales",
|
|
659
950
|
{
|
|
660
951
|
title: "Seed starter locales",
|
|
661
|
-
description: 'Adds MULTIPLE target locales in one call \u2014 pass `codes` (BCP-47, e.g. ["ru","de-DE","fr"]), or omit for a common starter set. Prefer this over repeated add_locale calls.',
|
|
952
|
+
description: 'Adds MULTIPLE target locales in one call \u2014 pass `codes` (BCP-47, e.g. ["ru","de-DE","fr"]), or omit for a common starter set. Prefer this over repeated add_locale calls. Codes are validated as a batch: if any are ambiguous (e.g. "pt", "zh") or invalid, NONE are added and the response lists what to fix; aliases like "cn" are normalized to "zh-CN".',
|
|
662
953
|
inputSchema: {
|
|
663
954
|
projectId: z7.string(),
|
|
664
955
|
codes: z7.array(z7.string()).optional().describe('BCP-47 codes to add, e.g. ["ru","de-DE","fr"].')
|
|
665
956
|
},
|
|
666
957
|
annotations: { openWorldHint: true }
|
|
667
958
|
},
|
|
668
|
-
async ({ projectId, codes }) =>
|
|
959
|
+
async ({ projectId, codes }) => {
|
|
960
|
+
if (codes && codes.length > 0) {
|
|
961
|
+
const resolved = resolveLocaleCodes(codes);
|
|
962
|
+
if (resolved.rejected.length > 0) {
|
|
963
|
+
return jsonResult({
|
|
964
|
+
error: "Some locale codes are ambiguous or invalid; nothing was added.",
|
|
965
|
+
hint: "Fix the codes below and retry. For ambiguous ones, pick a listed candidate.",
|
|
966
|
+
rejected: resolved.rejected
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
const seeded = (await gql.request(SEED_STARTER_LOCALES, { projectId, codes: resolved.codes })).seedStarterLocales;
|
|
970
|
+
return resolved.normalized.length > 0 ? jsonResult({ locales: seeded, normalized: resolved.normalized }) : jsonResult(seeded);
|
|
971
|
+
}
|
|
972
|
+
return jsonResult((await gql.request(SEED_STARTER_LOCALES, { projectId, codes })).seedStarterLocales);
|
|
973
|
+
}
|
|
669
974
|
);
|
|
670
975
|
server.registerTool(
|
|
671
976
|
"remove_locale",
|
|
@@ -942,7 +1247,7 @@ import {
|
|
|
942
1247
|
TRANSLATE_KEY,
|
|
943
1248
|
TRANSLATE_LOCALE
|
|
944
1249
|
} from "@syntholoc/sdk";
|
|
945
|
-
var FORMATS2 = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS"];
|
|
1250
|
+
var FORMATS2 = ["JSON", "YAML", "CSV", "PO", "XLIFF", "ANDROID_XML", "IOS_STRINGS", "NJK"];
|
|
946
1251
|
function registerTranslationTools(server, gql) {
|
|
947
1252
|
server.registerTool(
|
|
948
1253
|
"translate_locale",
|
|
@@ -1093,9 +1398,12 @@ function registerAllTools(server, gql, opts = {}) {
|
|
|
1093
1398
|
}
|
|
1094
1399
|
}
|
|
1095
1400
|
|
|
1401
|
+
// package.json
|
|
1402
|
+
var version = "0.3.0";
|
|
1403
|
+
|
|
1096
1404
|
// src/build-server.ts
|
|
1097
1405
|
function buildMcpServer(gql, opts = {}) {
|
|
1098
|
-
const server = new McpServer({ name: "syntholoc", version
|
|
1406
|
+
const server = new McpServer({ name: "syntholoc", version });
|
|
1099
1407
|
registerAllTools(server, gql, opts);
|
|
1100
1408
|
registerResources(server, gql);
|
|
1101
1409
|
registerPrompts(server);
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syntholoc/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Syntholoc MCP server — manage AI localization and translation from Claude Code.",
|
|
3
|
+
"version": "0.3.0",
|
|
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": [
|
|
7
7
|
"syntholoc",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"translation",
|
|
11
11
|
"mcp",
|
|
12
12
|
"model-context-protocol",
|
|
13
|
+
"codex",
|
|
13
14
|
"claude-code"
|
|
14
15
|
],
|
|
15
16
|
"type": "module",
|
|
@@ -46,12 +47,14 @@
|
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@modelcontextprotocol/sdk": "1.22.0",
|
|
49
|
-
"@syntholoc/sdk": "0.
|
|
50
|
+
"@syntholoc/sdk": "0.2.0",
|
|
50
51
|
"zod": "3.25.76"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
54
|
+
"@syntholoc/shared": "0.1.0",
|
|
53
55
|
"@types/bun": "1.3.14",
|
|
54
56
|
"@types/node": "22.19.21",
|
|
57
|
+
"graphql": "16.14.2",
|
|
55
58
|
"tsup": "8.5.1",
|
|
56
59
|
"typescript": "6.0.3"
|
|
57
60
|
}
|