@sonenta/mcp 0.19.0 → 0.20.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonenta/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "MCP server for Sonenta translation management \u2014 wires Claude Desktop and other MCP clients into your project's keys, missing-key feed, and translation drafts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://sonenta.com",
|
|
Binary file
|
package/python/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "verbumia-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.20.1"
|
|
4
4
|
description = "Model Context Protocol server for Verbumia — list projects, missing keys, propose translations from Claude Desktop and other MCP clients."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<3.14"
|
|
@@ -42,6 +42,9 @@ Tools (V1 MCP surface; glossary_* added 2026-05-24; reports + trash added 2026-0
|
|
|
42
42
|
- get_variants — full variant matrix for one key (every active surface x language; #928)
|
|
43
43
|
- set_variant — upsert one variant override for ANY active surface (device or a11y)
|
|
44
44
|
- delete_variant — delete one variant override for any surface (cell re-inherits base)
|
|
45
|
+
- list_cognitive_candidates — text keys eligible for plain-language (cognitive) scoring
|
|
46
|
+
- set_cognitive_score — record a key's cognitive score (0-100) + plain-language suggestion (0 credit)
|
|
47
|
+
- analyze_cognitive — server AI cognitive analysis (billed; opt-in fallback to local scoring)
|
|
45
48
|
|
|
46
49
|
A11y (accessibility) tools (a11y V1) extend the variant engine with four SEMANTIC
|
|
47
50
|
surfaces — aria_label / alt_text / screen_reader / plain_language — orthogonal to
|
|
@@ -108,6 +111,38 @@ KEY_TYPES = (
|
|
|
108
111
|
"meta_description",
|
|
109
112
|
)
|
|
110
113
|
|
|
114
|
+
# Human/LLM guidance for choosing a key's `type`. Embedded in the create_key /
|
|
115
|
+
# update_key tool descriptions so an agent types keys correctly instead of
|
|
116
|
+
# leaving everything as the "text" default. The type DRIVES which a11y
|
|
117
|
+
# treatments + cognitive (plain-language) scoring a key gets, so it is the input
|
|
118
|
+
# that makes the a11y tooling work.
|
|
119
|
+
KEY_TYPE_GUIDANCE = (
|
|
120
|
+
"Set this at CREATION — do NOT leave everything as 'text'. The type drives "
|
|
121
|
+
"which a11y treatments the key offers (and whether it gets cognitive / "
|
|
122
|
+
"plain-language scoring), so correct typing is what lets the a11y tooling "
|
|
123
|
+
"work. Pick by the string's UI role:\n"
|
|
124
|
+
"- text — body / paragraph copy (offers plain_language, screen_reader)\n"
|
|
125
|
+
"- heading — a title or section heading (plain_language, screen_reader)\n"
|
|
126
|
+
"- label — a standalone short label (plain_language, screen_reader)\n"
|
|
127
|
+
"- caption — image caption / helper / secondary text (plain_language, screen_reader)\n"
|
|
128
|
+
"- button — a button's label (offers aria_label, screen_reader)\n"
|
|
129
|
+
"- link — a hyperlink / anchor label (aria_label, screen_reader)\n"
|
|
130
|
+
"- menu_item — a navigation / menu entry (aria_label, screen_reader)\n"
|
|
131
|
+
"- input_placeholder — a form input placeholder (aria_label)\n"
|
|
132
|
+
"- input_label — a form field's label (aria_label)\n"
|
|
133
|
+
"- tooltip — tooltip text (screen_reader)\n"
|
|
134
|
+
"- badge — a small status / count badge (screen_reader)\n"
|
|
135
|
+
"- error_message — a validation / error message (screen_reader)\n"
|
|
136
|
+
"- toast — a transient toast / snackbar notification (screen_reader)\n"
|
|
137
|
+
"- image — an image; THIS key's value IS the alt text\n"
|
|
138
|
+
"- icon — an icon; THIS key's value IS the accessible name (aria)\n"
|
|
139
|
+
"- meta_title — an SEO page <title> (no a11y treatment)\n"
|
|
140
|
+
"- meta_description — an SEO meta description (no a11y treatment)\n"
|
|
141
|
+
"Default 'text'. Reclassify existing keys later with update_key / "
|
|
142
|
+
"update_keys_bulk (type-only, no source_value needed). The current type is "
|
|
143
|
+
"returned by list_keys so you can audit and fix mis-typed keys."
|
|
144
|
+
)
|
|
145
|
+
|
|
111
146
|
|
|
112
147
|
def _project_uuid(arguments: dict | None, client: VerbumiaClient) -> str:
|
|
113
148
|
"""Resolve project_uuid from per-call args or the configured env scope.
|
|
@@ -205,13 +240,16 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
205
240
|
Tool(
|
|
206
241
|
name="list_keys",
|
|
207
242
|
description=(
|
|
208
|
-
"List translation keys with their source-language value
|
|
209
|
-
"per-language translations. Designed for
|
|
210
|
-
"Claude can scan the result and propose
|
|
211
|
-
"every key that's still missing in a target
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
243
|
+
"List translation keys with their source-language value, their "
|
|
244
|
+
"semantic `type`, and per-language translations. Designed for "
|
|
245
|
+
"bulk translation: Claude can scan the result and propose "
|
|
246
|
+
"translations for every key that's still missing in a target "
|
|
247
|
+
"language. Each key's `type` lets you AUDIT typing and reclassify "
|
|
248
|
+
"mis-typed keys (e.g. a button left as 'text') via update_key / "
|
|
249
|
+
"update_keys_bulk so the a11y treatments apply. Filters: "
|
|
250
|
+
"namespace slug, status_filter (only keys with at least one "
|
|
251
|
+
"translation in that status), language_code (restrict "
|
|
252
|
+
"translations[] to a single language)."
|
|
215
253
|
),
|
|
216
254
|
inputSchema={
|
|
217
255
|
"type": "object",
|
|
@@ -395,8 +433,11 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
395
433
|
"Returns created=false if the key already exists. When "
|
|
396
434
|
"source_value is supplied AND the project has a source "
|
|
397
435
|
"language, the value is also written as a source-language "
|
|
398
|
-
"translation in the same call.
|
|
399
|
-
"
|
|
436
|
+
"translation in the same call. ALWAYS set `type` to the key's "
|
|
437
|
+
"semantic role (button / link / heading / image / icon / "
|
|
438
|
+
"input_label / …) — do NOT leave it as the default 'text'; the "
|
|
439
|
+
"type drives the key's a11y treatments. See the `type` field for "
|
|
440
|
+
"the full 17-type taxonomy and when to use each."
|
|
400
441
|
),
|
|
401
442
|
inputSchema={
|
|
402
443
|
"type": "object",
|
|
@@ -424,7 +465,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
424
465
|
"type": "string",
|
|
425
466
|
"enum": list(KEY_TYPES),
|
|
426
467
|
"default": "text",
|
|
427
|
-
"description": "Semantic key type
|
|
468
|
+
"description": "Semantic key type. " + KEY_TYPE_GUIDANCE,
|
|
428
469
|
},
|
|
429
470
|
},
|
|
430
471
|
"required": ["namespace", "name"],
|
|
@@ -438,7 +479,11 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
438
479
|
"doesn't exist (write-through). Status is set to draft (or "
|
|
439
480
|
"translated if explicitly requested); reviewer/approved "
|
|
440
481
|
"promotion requires a human reviewer. Identifies the key "
|
|
441
|
-
"by namespace + key name + language_code — no uuids needed."
|
|
482
|
+
"by namespace + key name + language_code — no uuids needed. "
|
|
483
|
+
"The response carries `variable_warnings: {missing, extra}` "
|
|
484
|
+
"(null when interpolation variables match the source) — a "
|
|
485
|
+
"non-blocking flag for placeholder drift; check it and fix the "
|
|
486
|
+
"value if set."
|
|
442
487
|
),
|
|
443
488
|
inputSchema={
|
|
444
489
|
"type": "object",
|
|
@@ -496,7 +541,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
496
541
|
"description": {"type": "string"},
|
|
497
542
|
"plurals": {"type": "boolean", "default": False},
|
|
498
543
|
"max_length": {"type": "integer", "minimum": 1},
|
|
499
|
-
"type": {"type": "string", "enum": list(KEY_TYPES), "default": "text", "description": "Semantic key type (default 'text'
|
|
544
|
+
"type": {"type": "string", "enum": list(KEY_TYPES), "default": "text", "description": "Semantic key type — set per item by the string's UI role (button / image / heading / …); do NOT default everything to 'text'. See create_key's `type` field for the full taxonomy and why it matters for a11y."},
|
|
500
545
|
},
|
|
501
546
|
"required": ["namespace", "name"],
|
|
502
547
|
"additionalProperties": False,
|
|
@@ -612,7 +657,10 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
612
657
|
"(same namespace+key+language twice) error per item; status is "
|
|
613
658
|
"draft (or translated if requested), reviewed/approved require "
|
|
614
659
|
"a human reviewer. NO server-side translation — values come "
|
|
615
|
-
"from you. Use this instead of N propose_translation calls."
|
|
660
|
+
"from you. Use this instead of N propose_translation calls. "
|
|
661
|
+
"Each result carries `variable_warnings: {missing, extra}` "
|
|
662
|
+
"(null when interpolation variables match the source) flagging "
|
|
663
|
+
"placeholder drift, non-blocking."
|
|
616
664
|
),
|
|
617
665
|
inputSchema={
|
|
618
666
|
"type": "object",
|
|
@@ -682,7 +730,10 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
682
730
|
description=(
|
|
683
731
|
"Lint a JSON i18next blob against the project: checks for "
|
|
684
732
|
"missing keys, extra keys, and placeholder parity ({var}, "
|
|
685
|
-
"{{var}}, %{var}) vs the source language."
|
|
733
|
+
"{{var}}, %{var}) vs the source language. Each "
|
|
734
|
+
"`placeholder_mismatches[]` item carries `expected` / `got` plus "
|
|
735
|
+
"`missing` (source variables absent from the translation) and "
|
|
736
|
+
"`extra` (variables in the translation unknown to the source)."
|
|
686
737
|
),
|
|
687
738
|
inputSchema={
|
|
688
739
|
"type": "object",
|
|
@@ -1553,6 +1604,116 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
1553
1604
|
"additionalProperties": False,
|
|
1554
1605
|
},
|
|
1555
1606
|
),
|
|
1607
|
+
Tool(
|
|
1608
|
+
name="list_cognitive_candidates",
|
|
1609
|
+
description=(
|
|
1610
|
+
"List keys that are CANDIDATES for plain-language (cognitive) scoring: "
|
|
1611
|
+
"text-relevant keys (a type that offers plain_language — text / heading "
|
|
1612
|
+
"/ label / caption) whose source value clears a small word floor. This "
|
|
1613
|
+
"is a scope filter, NOT a difficulty judgement — score them to find the "
|
|
1614
|
+
"hard ones. Pass only_unanalyzed=true to skip keys already scored. Each "
|
|
1615
|
+
"item carries key_uuid + source_value so you can score it locally with "
|
|
1616
|
+
"set_cognitive_score."
|
|
1617
|
+
),
|
|
1618
|
+
inputSchema={
|
|
1619
|
+
"type": "object",
|
|
1620
|
+
"properties": {
|
|
1621
|
+
"project_uuid": {
|
|
1622
|
+
"type": "string",
|
|
1623
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
1624
|
+
},
|
|
1625
|
+
"only_unanalyzed": {
|
|
1626
|
+
"type": "boolean",
|
|
1627
|
+
"description": "Only return keys with no cognitive_score yet (not analyzed).",
|
|
1628
|
+
},
|
|
1629
|
+
"namespace_uuid": {
|
|
1630
|
+
"type": "string",
|
|
1631
|
+
"description": "Restrict to one namespace by its UUID.",
|
|
1632
|
+
},
|
|
1633
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 100},
|
|
1634
|
+
"offset": {"type": "integer", "minimum": 0, "default": 0},
|
|
1635
|
+
},
|
|
1636
|
+
"additionalProperties": False,
|
|
1637
|
+
},
|
|
1638
|
+
),
|
|
1639
|
+
Tool(
|
|
1640
|
+
name="set_cognitive_score",
|
|
1641
|
+
description=(
|
|
1642
|
+
"Record a key's cognitive-difficulty SCORE (0-100, higher = harder to "
|
|
1643
|
+
"read) and an optional plain-language SUGGESTION — the local-first, "
|
|
1644
|
+
"**0-credit** path. Use this after scoring a candidate YOURSELF: you "
|
|
1645
|
+
"(the agent) judge the difficulty and write a clearer rewrite, then "
|
|
1646
|
+
"persist both here (marked by_bot). A key only enters the review queue "
|
|
1647
|
+
"(reading_level_high) once its score is >= the project threshold "
|
|
1648
|
+
"(default 60). The suggestion is a draft for a human to apply/approve."
|
|
1649
|
+
),
|
|
1650
|
+
inputSchema={
|
|
1651
|
+
"type": "object",
|
|
1652
|
+
"properties": {
|
|
1653
|
+
"project_uuid": {
|
|
1654
|
+
"type": "string",
|
|
1655
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
1656
|
+
},
|
|
1657
|
+
"key_uuid": {
|
|
1658
|
+
"type": "string",
|
|
1659
|
+
"description": "Key UUID (from list_cognitive_candidates / list_keys).",
|
|
1660
|
+
},
|
|
1661
|
+
"score": {
|
|
1662
|
+
"type": "number",
|
|
1663
|
+
"minimum": 0,
|
|
1664
|
+
"maximum": 100,
|
|
1665
|
+
"description": "Cognitive difficulty score, 0 (easy) to 100 (hard).",
|
|
1666
|
+
},
|
|
1667
|
+
"suggestion": {
|
|
1668
|
+
"type": "string",
|
|
1669
|
+
"description": "Optional plain-language rewrite of the source value (draft).",
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
"required": ["key_uuid", "score"],
|
|
1673
|
+
"additionalProperties": False,
|
|
1674
|
+
},
|
|
1675
|
+
),
|
|
1676
|
+
Tool(
|
|
1677
|
+
name="analyze_cognitive",
|
|
1678
|
+
description=(
|
|
1679
|
+
"Server-side AI cognitive analysis — the BILLED, opt-in FALLBACK to "
|
|
1680
|
+
"local scoring. Rates the cognitive difficulty of candidate keys and "
|
|
1681
|
+
"suggests plain-language rewrites server-side. Prefer scoring locally "
|
|
1682
|
+
"with set_cognitive_score (0 credits); use this only for very large "
|
|
1683
|
+
"volumes or on explicit request. Bills credits (standard=1, premium=4 "
|
|
1684
|
+
"per key; 402 if insufficient, 503 if AI unavailable). Returns "
|
|
1685
|
+
"{requested, analyzed, credits_charged, credits_refunded}."
|
|
1686
|
+
),
|
|
1687
|
+
inputSchema={
|
|
1688
|
+
"type": "object",
|
|
1689
|
+
"properties": {
|
|
1690
|
+
"project_uuid": {
|
|
1691
|
+
"type": "string",
|
|
1692
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
1693
|
+
},
|
|
1694
|
+
"tier": {
|
|
1695
|
+
"type": "string",
|
|
1696
|
+
"enum": ["standard", "premium"],
|
|
1697
|
+
"default": "standard",
|
|
1698
|
+
"description": "Quality tier (credit_cost per key: standard=1, premium=4).",
|
|
1699
|
+
},
|
|
1700
|
+
"key_uuids": {
|
|
1701
|
+
"type": "array",
|
|
1702
|
+
"items": {"type": "string"},
|
|
1703
|
+
"description": "Restrict to these keys (from list_cognitive_candidates). Omit to scope the whole project (or a namespace).",
|
|
1704
|
+
},
|
|
1705
|
+
"namespace_uuid": {
|
|
1706
|
+
"type": "string",
|
|
1707
|
+
"description": "Restrict to one namespace by its UUID.",
|
|
1708
|
+
},
|
|
1709
|
+
"idempotency_key": {
|
|
1710
|
+
"type": "string",
|
|
1711
|
+
"description": "Stable token making this billable call safe to retry: reuse the SAME value to avoid a double-charge. A random one is generated if omitted.",
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
"additionalProperties": False,
|
|
1715
|
+
},
|
|
1716
|
+
),
|
|
1556
1717
|
]
|
|
1557
1718
|
|
|
1558
1719
|
@server.call_tool()
|
|
@@ -2089,6 +2250,50 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
2089
2250
|
return _err("value is required")
|
|
2090
2251
|
data = await client.put(path, json={"value": args["value"]})
|
|
2091
2252
|
return _text(data)
|
|
2253
|
+
if name == "list_cognitive_candidates":
|
|
2254
|
+
project_uuid = _project_uuid(args, client)
|
|
2255
|
+
params = {
|
|
2256
|
+
k: v
|
|
2257
|
+
for k, v in {
|
|
2258
|
+
"only_unanalyzed": ("true" if args.get("only_unanalyzed") else None),
|
|
2259
|
+
"namespace_uuid": args.get("namespace_uuid"),
|
|
2260
|
+
"limit": args.get("limit", 100),
|
|
2261
|
+
"offset": args.get("offset", 0),
|
|
2262
|
+
}.items()
|
|
2263
|
+
if v is not None
|
|
2264
|
+
}
|
|
2265
|
+
data = await client.get(
|
|
2266
|
+
f"/v1/mcp/projects/{project_uuid}/cognitive/candidates", params=params
|
|
2267
|
+
)
|
|
2268
|
+
return _text(data)
|
|
2269
|
+
if name == "set_cognitive_score":
|
|
2270
|
+
project_uuid = _project_uuid(args, client)
|
|
2271
|
+
key_uuid = args.get("key_uuid")
|
|
2272
|
+
if not key_uuid:
|
|
2273
|
+
return _err("key_uuid is required")
|
|
2274
|
+
if "score" not in args:
|
|
2275
|
+
return _err("score is required")
|
|
2276
|
+
body: dict[str, Any] = {"score": args["score"]}
|
|
2277
|
+
if "suggestion" in args:
|
|
2278
|
+
body["suggestion"] = args["suggestion"]
|
|
2279
|
+
data = await client.put(
|
|
2280
|
+
f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}/cognitive-score",
|
|
2281
|
+
json=body,
|
|
2282
|
+
)
|
|
2283
|
+
return _text(data)
|
|
2284
|
+
if name == "analyze_cognitive":
|
|
2285
|
+
project_uuid = _project_uuid(args, client)
|
|
2286
|
+
body: dict[str, Any] = {
|
|
2287
|
+
"tier": args.get("tier", "standard"),
|
|
2288
|
+
"idempotency_key": args.get("idempotency_key") or str(uuid.uuid4()),
|
|
2289
|
+
}
|
|
2290
|
+
for opt in ("key_uuids", "namespace_uuid"):
|
|
2291
|
+
if args.get(opt):
|
|
2292
|
+
body[opt] = args[opt]
|
|
2293
|
+
data = await client.post(
|
|
2294
|
+
f"/v1/mcp/projects/{project_uuid}/cognitive/analyze", json=body
|
|
2295
|
+
)
|
|
2296
|
+
return _text(data)
|
|
2092
2297
|
return _err(f"unknown tool: {name}")
|
|
2093
2298
|
except VerbumiaApiError as exc:
|
|
2094
2299
|
return _err(f"API {exc.status}: {exc.body}")
|
|
Binary file
|