sonenta-mcp 0.21.2__tar.gz

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.
Files changed (44) hide show
  1. sonenta_mcp-0.21.2/.gitignore +28 -0
  2. sonenta_mcp-0.21.2/LICENSE +21 -0
  3. sonenta_mcp-0.21.2/PKG-INFO +325 -0
  4. sonenta_mcp-0.21.2/README.md +304 -0
  5. sonenta_mcp-0.21.2/alias-pypi/README.md +17 -0
  6. sonenta_mcp-0.21.2/alias-pypi/pyproject.toml +37 -0
  7. sonenta_mcp-0.21.2/alias-pypi/verbumia_mcp_alias.py +30 -0
  8. sonenta_mcp-0.21.2/brew/sonenta-mcp.rb +47 -0
  9. sonenta_mcp-0.21.2/npm/.npmignore +12 -0
  10. sonenta_mcp-0.21.2/npm/.npmrc +5 -0
  11. sonenta_mcp-0.21.2/npm/LICENSE +21 -0
  12. sonenta_mcp-0.21.2/npm/README.md +89 -0
  13. sonenta_mcp-0.21.2/npm/bin/sonenta-mcp.js +142 -0
  14. sonenta_mcp-0.21.2/npm/bin/verbumia-mcp.js +6 -0
  15. sonenta_mcp-0.21.2/npm/package.json +41 -0
  16. sonenta_mcp-0.21.2/npm/python/README.md +114 -0
  17. sonenta_mcp-0.21.2/npm/python/dist/.npmignore +0 -0
  18. sonenta_mcp-0.21.2/npm/python/pyproject.toml +58 -0
  19. sonenta_mcp-0.21.2/npm/python/verbumia_mcp/__init__.py +3 -0
  20. sonenta_mcp-0.21.2/npm/python/verbumia_mcp/__main__.py +23 -0
  21. sonenta_mcp-0.21.2/npm/python/verbumia_mcp/client.py +85 -0
  22. sonenta_mcp-0.21.2/npm/python/verbumia_mcp/config.py +105 -0
  23. sonenta_mcp-0.21.2/npm/python/verbumia_mcp/server.py +88 -0
  24. sonenta_mcp-0.21.2/npm/python/verbumia_mcp/tools.py +2420 -0
  25. sonenta_mcp-0.21.2/pyproject.toml +61 -0
  26. sonenta_mcp-0.21.2/tests/__init__.py +0 -0
  27. sonenta_mcp-0.21.2/tests/test_a11y_mcp.py +377 -0
  28. sonenta_mcp-0.21.2/tests/test_client_health.py +24 -0
  29. sonenta_mcp-0.21.2/tests/test_cognitive_mcp.py +152 -0
  30. sonenta_mcp-0.21.2/tests/test_config.py +123 -0
  31. sonenta_mcp-0.21.2/tests/test_glossary_mcp.py +253 -0
  32. sonenta_mcp-0.21.2/tests/test_preflight.py +70 -0
  33. sonenta_mcp-0.21.2/tests/test_source_duplicates_mcp.py +135 -0
  34. sonenta_mcp-0.21.2/tests/test_surfaces_mcp.py +239 -0
  35. sonenta_mcp-0.21.2/tests/test_tools.py +100 -0
  36. sonenta_mcp-0.21.2/tests/test_trash_reports_mcp.py +149 -0
  37. sonenta_mcp-0.21.2/tests/test_update_key_mcp.py +239 -0
  38. sonenta_mcp-0.21.2/uv.lock +620 -0
  39. sonenta_mcp-0.21.2/verbumia_mcp/__init__.py +3 -0
  40. sonenta_mcp-0.21.2/verbumia_mcp/__main__.py +23 -0
  41. sonenta_mcp-0.21.2/verbumia_mcp/client.py +85 -0
  42. sonenta_mcp-0.21.2/verbumia_mcp/config.py +105 -0
  43. sonenta_mcp-0.21.2/verbumia_mcp/server.py +88 -0
  44. sonenta_mcp-0.21.2/verbumia_mcp/tools.py +2420 -0
@@ -0,0 +1,28 @@
1
+ # Node
2
+ node_modules/
3
+ cli/dist/
4
+ *.tsbuildinfo
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ .venv/
10
+ venv/
11
+ *.egg-info/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+
15
+ # Built artifacts (rebuilt at publish time)
16
+ mcp/dist/
17
+ mcp/npm/python/dist/*.whl
18
+ mcp/npm/python/dist/*.tar.gz
19
+ mcp/alias-pypi/dist/
20
+
21
+ # Env / secrets
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+
26
+ # OS / editor
27
+ .DS_Store
28
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Verbumia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.4
2
+ Name: sonenta-mcp
3
+ Version: 0.21.2
4
+ Summary: Model Context Protocol server for Sonenta — list projects, missing keys, propose translations from Claude Desktop and other MCP clients.
5
+ Project-URL: Homepage, https://sonenta.com
6
+ Project-URL: Documentation, https://sonenta.com/docs/mcp/setup
7
+ Project-URL: Source, https://github.com/sonenta/tools
8
+ Project-URL: Issues, https://github.com/sonenta/tools/issues
9
+ Author: Sonenta
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: claude,i18n,mcp,sonenta,translations
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Internationalization
16
+ Requires-Python: <3.14,>=3.12
17
+ Requires-Dist: httpx>=0.28
18
+ Requires-Dist: mcp>=1.0
19
+ Requires-Dist: pydantic>=2.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Sonenta MCP server
23
+
24
+ Model Context Protocol server for [Sonenta](https://sonenta.com) — exposes
25
+ your translation project to Claude Desktop and other MCP clients.
26
+
27
+ ## Status
28
+
29
+ Tools wired: `list_projects`, `get_project_info`, `list_keys`,
30
+ `list_untranslated_keys`, `list_missing_keys`, `missing_keys_stats`,
31
+ `acknowledge_missing_keys`, `create_namespace`, `create_key`,
32
+ `create_keys_bulk`, `update_key`, `update_keys_bulk`, `propose_translation`,
33
+ `propose_translations_bulk`, `publish_cdn`, `validate_translations`,
34
+ `project_context_get`, `project_context_set`, `glossary_list`, `glossary_create`,
35
+ `glossary_update`, `glossary_delete`, `health_report`, `coverage_report`,
36
+ `delete_keys_bulk`, `restore_keys_bulk`, `list_trash`, `a11y_estimate`,
37
+ `generate_a11y_variant`, `translate_a11y_variants`, `a11y_report`,
38
+ `list_a11y_gaps`, `set_a11y_variant`, `delete_a11y_variant`, `list_surfaces`,
39
+ `create_surface`, `update_surface`, `delete_surface`, `get_variants`,
40
+ `set_variant`, `delete_variant`, `list_cognitive_candidates`,
41
+ `set_cognitive_score`, `analyze_cognitive`.
42
+
43
+ ### Project context document (V1.2)
44
+
45
+ `project_context_get` / `project_context_set` read and write a free-form
46
+ markdown blob attached to the project — terminology, brand voice, domain
47
+ notes (religious, legal, medical, gaming, etc.). The content is intended
48
+ as ambient context for human translators **and** for AI agents producing
49
+ translations: prepend it to your translation prompts so every output stays
50
+ consistent with the project's vocabulary and tone. Hard cap 100 KiB.
51
+
52
+ Scopes:
53
+ - `project_context_get` requires `project:read` (existing scope).
54
+ - `project_context_set` requires `project:settings:write` (new scope, narrower
55
+ than `project:write` since settings changes propagate to every translator's
56
+ prompt context).
57
+
58
+ Note: the blanket `mcp:*` scope is **not** sufficient for these tools — grant
59
+ the precise scope on the key.
60
+
61
+ ### Glossary (V1.3)
62
+
63
+ CRUD over the project glossary — the terminology rules the backend applies when
64
+ translating. The single public `rule_type` vocabulary is `translation` |
65
+ `do_not_translate` | `forbidden` (the internal storage type is never exposed).
66
+
67
+ - `glossary_list` — filters `rule_type`, `q` (substring on term), `limit`
68
+ (1..500, default 200) → `{ items, total }`.
69
+ - `glossary_create` — `{ rule_type?, term, translations?, case_sensitive?, note? }`;
70
+ `rule_type` defaults to `translation`; `translations` is honored only for
71
+ `translation`. 409 on a duplicate `(rule_type, term)`.
72
+ - `glossary_update` — by `entry_id`; send only the fields to change (`term`,
73
+ `translations`, `case_sensitive`, `note`). `rule_type` is immutable.
74
+ - `glossary_delete` — by `entry_id`.
75
+
76
+ These use the dedicated MCP surface `/v1/mcp/projects/{id}/glossary` and need the
77
+ `mcp:*` scope (the dashboard glossary endpoints are project-role only).
78
+
79
+ **Per-string glossary hints.** `list_untranslated_keys` and `list_keys` accept
80
+ `include_glossary_hints: true` (default false). When set, each returned key
81
+ carries a `glossary_hints[]` array — `{ term, rule_type, target_translation,
82
+ matched_text }` — for the rules the backend matched against the key's source
83
+ string. All matching is backend-side; the MCP forwards the flag and surfaces the
84
+ hints verbatim.
85
+
86
+ ### Quality reports + trash (V1.4)
87
+
88
+ Read-only inspection (surfaced verbatim):
89
+ - `health_report` — project source-health report (source-string issues, with
90
+ `by_issue` / `by_severity` rollups). Optional `version_id`.
91
+ - `coverage_report` — translation-coverage report (global completeness +
92
+ per-language). Optional `version_id`.
93
+
94
+ Trash — soft-delete, restorable; **no purge over MCP** (hard-delete is
95
+ human-only via the dashboard):
96
+ - `delete_keys_bulk` — move up to 500 keys to the trash by `key_uuid` (idempotent).
97
+ - `restore_keys_bulk` — restore trashed keys by `key_uuid`.
98
+ - `list_trash` — list trashed keys (`{key_uuid, namespace, name, deleted_at}`,
99
+ cursor-paginated).
100
+
101
+ The trash tools are **uuid-addressed**: get `key_uuid` from `list_keys` (active)
102
+ or `list_trash` (trashed), then pass it to delete/restore. A trashed key
103
+ disappears from `list_keys`, `list_untranslated_keys`, the CDN, and the reports
104
+ until it is restored.
105
+
106
+ ### Edit a key's source value (V1.5)
107
+
108
+ `update_key` / `update_keys_bulk` edit a key's **source-language value in place**
109
+ (`PATCH`), preserving the key and its history (version bump + history revision +
110
+ audit). Address the key by **exactly one** of `key_uuid` (preferred — from
111
+ `list_keys`) **or** `namespace` + `name`; an optional `description` can be updated
112
+ in the same call.
113
+
114
+ - When the `source_value` **actually changes**, the key's `reviewed` / `approved`
115
+ target translations are demoted to needs-review (`translated`, the "à revoir"
116
+ state); `missing` / `draft` / `translated` targets are left untouched.
117
+ `stale_flagged` reports how many were demoted.
118
+ - Submitting the current value is a **no-op** (`result: "unchanged"`, not billed);
119
+ a real source change — or a `description`-only change — bills 1 unit per key.
120
+ - `update_keys_bulk` takes up to 500 items and returns a per-item `results` array
121
+ (`status`: `ok` | `unchanged` | `error`) plus a `summary` roll-up; valid items
122
+ commit even if siblings fail.
123
+
124
+ ### Key semantic type (a11y key.type)
125
+
126
+ `create_key` / `update_key` (and their bulk variants) accept a `type`. **Always
127
+ set it at creation — don't leave everything as the `text` default** — because the
128
+ type DRIVES which a11y treatments (and cognitive / plain-language scoring) a key
129
+ gets. Pick by the string's UI role:
130
+
131
+ | type | use for | a11y treatments |
132
+ |---|---|---|
133
+ | `text` | body / paragraph copy | plain_language, screen_reader |
134
+ | `heading` | a title / section heading | plain_language, screen_reader |
135
+ | `label` | a standalone short label | plain_language, screen_reader |
136
+ | `caption` | image caption / helper text | plain_language, screen_reader |
137
+ | `button` | a button's label | aria_label, screen_reader |
138
+ | `link` | a hyperlink / anchor label | aria_label, screen_reader |
139
+ | `menu_item` | a nav / menu entry | aria_label, screen_reader |
140
+ | `input_placeholder` | a form input placeholder | aria_label |
141
+ | `input_label` | a form field's label | aria_label |
142
+ | `tooltip` | tooltip text | screen_reader |
143
+ | `badge` | a small status / count badge | screen_reader |
144
+ | `error_message` | a validation / error message | screen_reader |
145
+ | `toast` | a transient toast / snackbar | screen_reader |
146
+ | `image` | an image — value **is** the alt text | base = alt |
147
+ | `icon` | an icon — value **is** the accessible name | base = aria name |
148
+ | `meta_title` | an SEO page `<title>` | — |
149
+ | `meta_description` | an SEO meta description | — |
150
+
151
+ `update_key` / `update_keys_bulk` can change the type alone (no `source_value`
152
+ needed) to **reclassify** a mis-typed key; `list_keys` returns each key's current
153
+ `type` so you can audit and fix typing.
154
+
155
+ The engine gates everything by type server-side, so the a11y and variant tools are
156
+ **type-aware** automatically: `a11y_report` only flags treatments relevant to each
157
+ key's type (no aria/alt on a text key), `get_variants` lists only a key's offered
158
+ surfaces, and `set_variant` returns `422` for a surface the key's type doesn't
159
+ offer.
160
+
161
+ ### Accessibility (a11y) variants (a11y V1)
162
+
163
+ Semantic accessibility variants that extend the variant engine with four
164
+ surfaces — `aria_label`, `alt_text`, `screen_reader`, `plain_language` —
165
+ orthogonal to the visible text. All are thin HTTP wrappers surfaced verbatim.
166
+
167
+ Discover gaps (read-only WCAG report):
168
+ - `a11y_report` — per key/surface/locale a11y gaps with `by_gap` / `by_severity` /
169
+ `by_surface` rollups + per-item details. Gap types: `a11y_untranslated`,
170
+ `alt_missing`, `reading_level_high`, `a11y_variant_absent`. Optional
171
+ `version_id`, `require_surface[]` (flag surfaces absent in the source), and
172
+ `reading_level_max_words` (5..60, default 15).
173
+ - `list_a11y_gaps` — the actionable `items[]` of `a11y_report`, filtered
174
+ client-side by `gap` / `surface` / `locale`. To find keys with no a11y variant,
175
+ pass `require_surface=<surface>` and filter `gap=a11y_variant_absent`.
176
+
177
+ AI fill (billed in credits — call `a11y_estimate` first):
178
+ - `a11y_estimate` — preview `{units, credit_cost_per_unit, credits_required,
179
+ balance, sufficient}` for a `generate` or `translate` run (tier `standard`=1 /
180
+ `premium`=4 per unit).
181
+ - `generate_a11y_variant` — AI-generate **source-language** a11y variants for keys
182
+ that lack them (aria/screen_reader/plain_language from the visible text,
183
+ alt_text from the key context). Draft, bot-authored.
184
+ - `translate_a11y_variants` — translate existing source a11y overrides into the
185
+ requested `language_codes`.
186
+
187
+ Manual CRUD (one cell at a time):
188
+ - `set_a11y_variant` — upsert one override for `(key_uuid, language_code,
189
+ surface)` with a text `value`. Address by the locale **code** an `a11y_report`
190
+ gap already carries — no UUID resolution. Stored as a draft (bot) override.
191
+ - `delete_a11y_variant` — clear one override; the cell re-inherits the base value.
192
+
193
+ All a11y tools are reachable with the same `mcp:*` key as the rest of the MCP
194
+ surface — no extra scope. `estimate` / `generate` / `translate` hit
195
+ `/v1/mcp/projects/{id}/a11y/*`; the report hits
196
+ `/v1/mcp/projects/{id}/a11y-report`; set/delete use the generic code-addressed
197
+ variant route below.
198
+
199
+ ### Configurable surfaces + generic variants (task 928)
200
+
201
+ Surfaces are **per-project**: device surfaces (`desktop`/`mobile`/`tablet` +
202
+ custom like `watch`/`tv`) are extensible; the four a11y surfaces are a fixed
203
+ semantic set (toggle-active only). An un-customized project behaves exactly as
204
+ before (all builtins active). Manage them with:
205
+
206
+ - `list_surfaces` — `{surfaces:[{slug, label, kind, builtin, active}]}` (`kind` ∈
207
+ `device`/`a11y`).
208
+ - `create_surface` — add a **custom device** surface (`{slug, label?}`); creating
209
+ an a11y slug is rejected.
210
+ - `update_surface` — rename and/or toggle `active`. Deactivating is **soft**
211
+ (variants retained but hidden; the response `affected_variants` reports the
212
+ count; reactivating republishes them).
213
+ - `delete_surface` — delete a custom surface (builtins can only be deactivated).
214
+
215
+ Authoring variants works for **any active surface** (device or a11y), addressed
216
+ by the locale **code** + surface slug a report/matrix already gives you:
217
+
218
+ - `get_variants` — the full matrix for one key: every active surface × language,
219
+ with each cell's `{override, value, resolvedValue, status, drift}` plus
220
+ `surfaces` + `surfaceKinds`. (`GET .../keys/{key_uuid}/variants`)
221
+ - `set_variant` — upsert one override `(key_uuid, language_code, surface, value)`;
222
+ `422` if the surface is not active, `409` if the cell is already
223
+ reviewed/approved (never overwritten). Stored as a draft.
224
+ - `delete_variant` — clear one override (cell re-inherits the base value).
225
+
226
+ Variant writes (`set_variant`/`delete_variant`, and the a11y-named
227
+ `set_a11y_variant`/`delete_a11y_variant`) all use the mcp:* code-addressed route
228
+ `/v1/mcp/projects/{id}/keys/{key_uuid}/variants/{language_code}/{surface}`.
229
+
230
+ ### Plain-language by cognitive score
231
+
232
+ Replaces the a-priori reading-level heuristic with an actual cognitive-difficulty
233
+ score (0–100, higher = harder). A key only enters the "review" queue
234
+ (`reading_level_high`) once it's been scored at or above the project threshold —
235
+ so long-but-simple strings no longer clutter the queue. Same local-first model as
236
+ the rest of the a11y surface:
237
+
238
+ - `list_cognitive_candidates` — text-relevant keys (a type that offers
239
+ `plain_language`: text/heading/label/caption) whose source clears a word floor;
240
+ a scope filter, not a difficulty judgement. `only_unanalyzed`, `namespace_uuid`,
241
+ `limit`, `offset`.
242
+ - `set_cognitive_score` — the **0-credit** local path: record a `score` (0–100)
243
+ you judged yourself plus an optional plain-language `suggestion` (draft,
244
+ bot-authored).
245
+ - `analyze_cognitive` — the **billed** server-side AI fallback (estimate/opt-in;
246
+ `tier`, `key_uuids?`, `namespace_uuid?`, `idempotency_key`) for bulk runs.
247
+
248
+ ## Install
249
+
250
+ ```bash
251
+ # Recommended (zero-install, used by Claude Desktop's mcp.json):
252
+ npx -y @sonenta/mcp
253
+
254
+ # Or from PyPI:
255
+ pipx install sonenta-mcp
256
+ ```
257
+
258
+ A Homebrew tap (`brew install sonenta/tap/sonenta-mcp`) is coming once we
259
+ publish to npm.
260
+
261
+ ## Configure (Claude Desktop)
262
+
263
+ Open **Settings → Developer → Edit Config**, then:
264
+
265
+ Single-project setup:
266
+
267
+ ```json
268
+ {
269
+ "mcpServers": {
270
+ "sonenta": {
271
+ "command": "npx",
272
+ "args": ["-y", "@sonenta/mcp"],
273
+ "env": {
274
+ "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
275
+ "SONENTA_PROJECTS": "<project_uuid>",
276
+ "SONENTA_BASE_URL": "https://api.sonenta.dev"
277
+ }
278
+ }
279
+ }
280
+ }
281
+ ```
282
+
283
+ Multi-project setup (v0.11+) — comma-separate the UUIDs and Claude will pass
284
+ `project_uuid` on each tool call:
285
+
286
+ ```json
287
+ "env": {
288
+ "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
289
+ "SONENTA_PROJECTS": "01993a..,01993b..,01993c.."
290
+ }
291
+ ```
292
+
293
+ Restart Claude Desktop. Tools show up in the prompt UI.
294
+
295
+ ## Environment
296
+
297
+ | Variable | Required | Default | Description |
298
+ |----------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------|
299
+ | `SONENTA_API_KEY` | yes | | API key from Org Settings → API Keys (`SONENTA_TOKEN` also accepted, back-compat) |
300
+ | `SONENTA_PROJECTS` | no | (LLM passes per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` on each tool call. |
301
+ | `SONENTA_PROJECT` | no | (legacy) | Singular fallback for v0.10.x users. Ignored when `SONENTA_PROJECTS` is set (warns). |
302
+ | `SONENTA_BASE_URL` | no | `https://api.sonenta.dev` | Override for self-host or staging (`SONENTA_API_BASE` also accepted, back-compat) |
303
+
304
+ The token format is `vrb_live_<prefix>.<secret>` and must carry the `mcp:*`
305
+ scope. For the `project_context_*` tools, also grant `project:read` and/or
306
+ `project:settings:write` — see the **Project context document** section
307
+ above. Treat the token like any other secret — keychain or `.env`, never
308
+ commit.
309
+
310
+ If your API key is pinned to a single project (its `project_uuid` is set
311
+ server-side), listing additional UUIDs in `SONENTA_PROJECTS` will surface
312
+ as a `403 API key is scoped to a different project` from the backend on
313
+ any call targeting one of the others.
314
+
315
+ ## Local development
316
+
317
+ ```bash
318
+ cd mcp
319
+ uv sync
320
+ uv run sonenta-mcp # stdio transport — pipe MCP frames over stdin/stdout
321
+ ```
322
+
323
+ ## License
324
+
325
+ MIT.
@@ -0,0 +1,304 @@
1
+ # Sonenta MCP server
2
+
3
+ Model Context Protocol server for [Sonenta](https://sonenta.com) — exposes
4
+ your translation project to Claude Desktop and other MCP clients.
5
+
6
+ ## Status
7
+
8
+ Tools wired: `list_projects`, `get_project_info`, `list_keys`,
9
+ `list_untranslated_keys`, `list_missing_keys`, `missing_keys_stats`,
10
+ `acknowledge_missing_keys`, `create_namespace`, `create_key`,
11
+ `create_keys_bulk`, `update_key`, `update_keys_bulk`, `propose_translation`,
12
+ `propose_translations_bulk`, `publish_cdn`, `validate_translations`,
13
+ `project_context_get`, `project_context_set`, `glossary_list`, `glossary_create`,
14
+ `glossary_update`, `glossary_delete`, `health_report`, `coverage_report`,
15
+ `delete_keys_bulk`, `restore_keys_bulk`, `list_trash`, `a11y_estimate`,
16
+ `generate_a11y_variant`, `translate_a11y_variants`, `a11y_report`,
17
+ `list_a11y_gaps`, `set_a11y_variant`, `delete_a11y_variant`, `list_surfaces`,
18
+ `create_surface`, `update_surface`, `delete_surface`, `get_variants`,
19
+ `set_variant`, `delete_variant`, `list_cognitive_candidates`,
20
+ `set_cognitive_score`, `analyze_cognitive`.
21
+
22
+ ### Project context document (V1.2)
23
+
24
+ `project_context_get` / `project_context_set` read and write a free-form
25
+ markdown blob attached to the project — terminology, brand voice, domain
26
+ notes (religious, legal, medical, gaming, etc.). The content is intended
27
+ as ambient context for human translators **and** for AI agents producing
28
+ translations: prepend it to your translation prompts so every output stays
29
+ consistent with the project's vocabulary and tone. Hard cap 100 KiB.
30
+
31
+ Scopes:
32
+ - `project_context_get` requires `project:read` (existing scope).
33
+ - `project_context_set` requires `project:settings:write` (new scope, narrower
34
+ than `project:write` since settings changes propagate to every translator's
35
+ prompt context).
36
+
37
+ Note: the blanket `mcp:*` scope is **not** sufficient for these tools — grant
38
+ the precise scope on the key.
39
+
40
+ ### Glossary (V1.3)
41
+
42
+ CRUD over the project glossary — the terminology rules the backend applies when
43
+ translating. The single public `rule_type` vocabulary is `translation` |
44
+ `do_not_translate` | `forbidden` (the internal storage type is never exposed).
45
+
46
+ - `glossary_list` — filters `rule_type`, `q` (substring on term), `limit`
47
+ (1..500, default 200) → `{ items, total }`.
48
+ - `glossary_create` — `{ rule_type?, term, translations?, case_sensitive?, note? }`;
49
+ `rule_type` defaults to `translation`; `translations` is honored only for
50
+ `translation`. 409 on a duplicate `(rule_type, term)`.
51
+ - `glossary_update` — by `entry_id`; send only the fields to change (`term`,
52
+ `translations`, `case_sensitive`, `note`). `rule_type` is immutable.
53
+ - `glossary_delete` — by `entry_id`.
54
+
55
+ These use the dedicated MCP surface `/v1/mcp/projects/{id}/glossary` and need the
56
+ `mcp:*` scope (the dashboard glossary endpoints are project-role only).
57
+
58
+ **Per-string glossary hints.** `list_untranslated_keys` and `list_keys` accept
59
+ `include_glossary_hints: true` (default false). When set, each returned key
60
+ carries a `glossary_hints[]` array — `{ term, rule_type, target_translation,
61
+ matched_text }` — for the rules the backend matched against the key's source
62
+ string. All matching is backend-side; the MCP forwards the flag and surfaces the
63
+ hints verbatim.
64
+
65
+ ### Quality reports + trash (V1.4)
66
+
67
+ Read-only inspection (surfaced verbatim):
68
+ - `health_report` — project source-health report (source-string issues, with
69
+ `by_issue` / `by_severity` rollups). Optional `version_id`.
70
+ - `coverage_report` — translation-coverage report (global completeness +
71
+ per-language). Optional `version_id`.
72
+
73
+ Trash — soft-delete, restorable; **no purge over MCP** (hard-delete is
74
+ human-only via the dashboard):
75
+ - `delete_keys_bulk` — move up to 500 keys to the trash by `key_uuid` (idempotent).
76
+ - `restore_keys_bulk` — restore trashed keys by `key_uuid`.
77
+ - `list_trash` — list trashed keys (`{key_uuid, namespace, name, deleted_at}`,
78
+ cursor-paginated).
79
+
80
+ The trash tools are **uuid-addressed**: get `key_uuid` from `list_keys` (active)
81
+ or `list_trash` (trashed), then pass it to delete/restore. A trashed key
82
+ disappears from `list_keys`, `list_untranslated_keys`, the CDN, and the reports
83
+ until it is restored.
84
+
85
+ ### Edit a key's source value (V1.5)
86
+
87
+ `update_key` / `update_keys_bulk` edit a key's **source-language value in place**
88
+ (`PATCH`), preserving the key and its history (version bump + history revision +
89
+ audit). Address the key by **exactly one** of `key_uuid` (preferred — from
90
+ `list_keys`) **or** `namespace` + `name`; an optional `description` can be updated
91
+ in the same call.
92
+
93
+ - When the `source_value` **actually changes**, the key's `reviewed` / `approved`
94
+ target translations are demoted to needs-review (`translated`, the "à revoir"
95
+ state); `missing` / `draft` / `translated` targets are left untouched.
96
+ `stale_flagged` reports how many were demoted.
97
+ - Submitting the current value is a **no-op** (`result: "unchanged"`, not billed);
98
+ a real source change — or a `description`-only change — bills 1 unit per key.
99
+ - `update_keys_bulk` takes up to 500 items and returns a per-item `results` array
100
+ (`status`: `ok` | `unchanged` | `error`) plus a `summary` roll-up; valid items
101
+ commit even if siblings fail.
102
+
103
+ ### Key semantic type (a11y key.type)
104
+
105
+ `create_key` / `update_key` (and their bulk variants) accept a `type`. **Always
106
+ set it at creation — don't leave everything as the `text` default** — because the
107
+ type DRIVES which a11y treatments (and cognitive / plain-language scoring) a key
108
+ gets. Pick by the string's UI role:
109
+
110
+ | type | use for | a11y treatments |
111
+ |---|---|---|
112
+ | `text` | body / paragraph copy | plain_language, screen_reader |
113
+ | `heading` | a title / section heading | plain_language, screen_reader |
114
+ | `label` | a standalone short label | plain_language, screen_reader |
115
+ | `caption` | image caption / helper text | plain_language, screen_reader |
116
+ | `button` | a button's label | aria_label, screen_reader |
117
+ | `link` | a hyperlink / anchor label | aria_label, screen_reader |
118
+ | `menu_item` | a nav / menu entry | aria_label, screen_reader |
119
+ | `input_placeholder` | a form input placeholder | aria_label |
120
+ | `input_label` | a form field's label | aria_label |
121
+ | `tooltip` | tooltip text | screen_reader |
122
+ | `badge` | a small status / count badge | screen_reader |
123
+ | `error_message` | a validation / error message | screen_reader |
124
+ | `toast` | a transient toast / snackbar | screen_reader |
125
+ | `image` | an image — value **is** the alt text | base = alt |
126
+ | `icon` | an icon — value **is** the accessible name | base = aria name |
127
+ | `meta_title` | an SEO page `<title>` | — |
128
+ | `meta_description` | an SEO meta description | — |
129
+
130
+ `update_key` / `update_keys_bulk` can change the type alone (no `source_value`
131
+ needed) to **reclassify** a mis-typed key; `list_keys` returns each key's current
132
+ `type` so you can audit and fix typing.
133
+
134
+ The engine gates everything by type server-side, so the a11y and variant tools are
135
+ **type-aware** automatically: `a11y_report` only flags treatments relevant to each
136
+ key's type (no aria/alt on a text key), `get_variants` lists only a key's offered
137
+ surfaces, and `set_variant` returns `422` for a surface the key's type doesn't
138
+ offer.
139
+
140
+ ### Accessibility (a11y) variants (a11y V1)
141
+
142
+ Semantic accessibility variants that extend the variant engine with four
143
+ surfaces — `aria_label`, `alt_text`, `screen_reader`, `plain_language` —
144
+ orthogonal to the visible text. All are thin HTTP wrappers surfaced verbatim.
145
+
146
+ Discover gaps (read-only WCAG report):
147
+ - `a11y_report` — per key/surface/locale a11y gaps with `by_gap` / `by_severity` /
148
+ `by_surface` rollups + per-item details. Gap types: `a11y_untranslated`,
149
+ `alt_missing`, `reading_level_high`, `a11y_variant_absent`. Optional
150
+ `version_id`, `require_surface[]` (flag surfaces absent in the source), and
151
+ `reading_level_max_words` (5..60, default 15).
152
+ - `list_a11y_gaps` — the actionable `items[]` of `a11y_report`, filtered
153
+ client-side by `gap` / `surface` / `locale`. To find keys with no a11y variant,
154
+ pass `require_surface=<surface>` and filter `gap=a11y_variant_absent`.
155
+
156
+ AI fill (billed in credits — call `a11y_estimate` first):
157
+ - `a11y_estimate` — preview `{units, credit_cost_per_unit, credits_required,
158
+ balance, sufficient}` for a `generate` or `translate` run (tier `standard`=1 /
159
+ `premium`=4 per unit).
160
+ - `generate_a11y_variant` — AI-generate **source-language** a11y variants for keys
161
+ that lack them (aria/screen_reader/plain_language from the visible text,
162
+ alt_text from the key context). Draft, bot-authored.
163
+ - `translate_a11y_variants` — translate existing source a11y overrides into the
164
+ requested `language_codes`.
165
+
166
+ Manual CRUD (one cell at a time):
167
+ - `set_a11y_variant` — upsert one override for `(key_uuid, language_code,
168
+ surface)` with a text `value`. Address by the locale **code** an `a11y_report`
169
+ gap already carries — no UUID resolution. Stored as a draft (bot) override.
170
+ - `delete_a11y_variant` — clear one override; the cell re-inherits the base value.
171
+
172
+ All a11y tools are reachable with the same `mcp:*` key as the rest of the MCP
173
+ surface — no extra scope. `estimate` / `generate` / `translate` hit
174
+ `/v1/mcp/projects/{id}/a11y/*`; the report hits
175
+ `/v1/mcp/projects/{id}/a11y-report`; set/delete use the generic code-addressed
176
+ variant route below.
177
+
178
+ ### Configurable surfaces + generic variants (task 928)
179
+
180
+ Surfaces are **per-project**: device surfaces (`desktop`/`mobile`/`tablet` +
181
+ custom like `watch`/`tv`) are extensible; the four a11y surfaces are a fixed
182
+ semantic set (toggle-active only). An un-customized project behaves exactly as
183
+ before (all builtins active). Manage them with:
184
+
185
+ - `list_surfaces` — `{surfaces:[{slug, label, kind, builtin, active}]}` (`kind` ∈
186
+ `device`/`a11y`).
187
+ - `create_surface` — add a **custom device** surface (`{slug, label?}`); creating
188
+ an a11y slug is rejected.
189
+ - `update_surface` — rename and/or toggle `active`. Deactivating is **soft**
190
+ (variants retained but hidden; the response `affected_variants` reports the
191
+ count; reactivating republishes them).
192
+ - `delete_surface` — delete a custom surface (builtins can only be deactivated).
193
+
194
+ Authoring variants works for **any active surface** (device or a11y), addressed
195
+ by the locale **code** + surface slug a report/matrix already gives you:
196
+
197
+ - `get_variants` — the full matrix for one key: every active surface × language,
198
+ with each cell's `{override, value, resolvedValue, status, drift}` plus
199
+ `surfaces` + `surfaceKinds`. (`GET .../keys/{key_uuid}/variants`)
200
+ - `set_variant` — upsert one override `(key_uuid, language_code, surface, value)`;
201
+ `422` if the surface is not active, `409` if the cell is already
202
+ reviewed/approved (never overwritten). Stored as a draft.
203
+ - `delete_variant` — clear one override (cell re-inherits the base value).
204
+
205
+ Variant writes (`set_variant`/`delete_variant`, and the a11y-named
206
+ `set_a11y_variant`/`delete_a11y_variant`) all use the mcp:* code-addressed route
207
+ `/v1/mcp/projects/{id}/keys/{key_uuid}/variants/{language_code}/{surface}`.
208
+
209
+ ### Plain-language by cognitive score
210
+
211
+ Replaces the a-priori reading-level heuristic with an actual cognitive-difficulty
212
+ score (0–100, higher = harder). A key only enters the "review" queue
213
+ (`reading_level_high`) once it's been scored at or above the project threshold —
214
+ so long-but-simple strings no longer clutter the queue. Same local-first model as
215
+ the rest of the a11y surface:
216
+
217
+ - `list_cognitive_candidates` — text-relevant keys (a type that offers
218
+ `plain_language`: text/heading/label/caption) whose source clears a word floor;
219
+ a scope filter, not a difficulty judgement. `only_unanalyzed`, `namespace_uuid`,
220
+ `limit`, `offset`.
221
+ - `set_cognitive_score` — the **0-credit** local path: record a `score` (0–100)
222
+ you judged yourself plus an optional plain-language `suggestion` (draft,
223
+ bot-authored).
224
+ - `analyze_cognitive` — the **billed** server-side AI fallback (estimate/opt-in;
225
+ `tier`, `key_uuids?`, `namespace_uuid?`, `idempotency_key`) for bulk runs.
226
+
227
+ ## Install
228
+
229
+ ```bash
230
+ # Recommended (zero-install, used by Claude Desktop's mcp.json):
231
+ npx -y @sonenta/mcp
232
+
233
+ # Or from PyPI:
234
+ pipx install sonenta-mcp
235
+ ```
236
+
237
+ A Homebrew tap (`brew install sonenta/tap/sonenta-mcp`) is coming once we
238
+ publish to npm.
239
+
240
+ ## Configure (Claude Desktop)
241
+
242
+ Open **Settings → Developer → Edit Config**, then:
243
+
244
+ Single-project setup:
245
+
246
+ ```json
247
+ {
248
+ "mcpServers": {
249
+ "sonenta": {
250
+ "command": "npx",
251
+ "args": ["-y", "@sonenta/mcp"],
252
+ "env": {
253
+ "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
254
+ "SONENTA_PROJECTS": "<project_uuid>",
255
+ "SONENTA_BASE_URL": "https://api.sonenta.dev"
256
+ }
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ Multi-project setup (v0.11+) — comma-separate the UUIDs and Claude will pass
263
+ `project_uuid` on each tool call:
264
+
265
+ ```json
266
+ "env": {
267
+ "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
268
+ "SONENTA_PROJECTS": "01993a..,01993b..,01993c.."
269
+ }
270
+ ```
271
+
272
+ Restart Claude Desktop. Tools show up in the prompt UI.
273
+
274
+ ## Environment
275
+
276
+ | Variable | Required | Default | Description |
277
+ |----------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------|
278
+ | `SONENTA_API_KEY` | yes | | API key from Org Settings → API Keys (`SONENTA_TOKEN` also accepted, back-compat) |
279
+ | `SONENTA_PROJECTS` | no | (LLM passes per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` on each tool call. |
280
+ | `SONENTA_PROJECT` | no | (legacy) | Singular fallback for v0.10.x users. Ignored when `SONENTA_PROJECTS` is set (warns). |
281
+ | `SONENTA_BASE_URL` | no | `https://api.sonenta.dev` | Override for self-host or staging (`SONENTA_API_BASE` also accepted, back-compat) |
282
+
283
+ The token format is `vrb_live_<prefix>.<secret>` and must carry the `mcp:*`
284
+ scope. For the `project_context_*` tools, also grant `project:read` and/or
285
+ `project:settings:write` — see the **Project context document** section
286
+ above. Treat the token like any other secret — keychain or `.env`, never
287
+ commit.
288
+
289
+ If your API key is pinned to a single project (its `project_uuid` is set
290
+ server-side), listing additional UUIDs in `SONENTA_PROJECTS` will surface
291
+ as a `403 API key is scoped to a different project` from the backend on
292
+ any call targeting one of the others.
293
+
294
+ ## Local development
295
+
296
+ ```bash
297
+ cd mcp
298
+ uv sync
299
+ uv run sonenta-mcp # stdio transport — pipe MCP frames over stdin/stdout
300
+ ```
301
+
302
+ ## License
303
+
304
+ MIT.