@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.19.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",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "verbumia-mcp"
3
- version = "0.19.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"
@@ -1,3 +1,3 @@
1
1
  """Verbumia MCP server (MIT)."""
2
2
 
3
- __version__ = "0.19.0"
3
+ __version__ = "0.20.1"
@@ -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 and "
209
- "per-language translations. Designed for bulk translation: "
210
- "Claude can scan the result and propose translations for "
211
- "every key that's still missing in a target language. "
212
- "Filters: namespace slug, status_filter (only keys with at "
213
- "least one translation in that status), language_code "
214
- "(restrict translations[] to a single language)."
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. Pass `type` (default 'text') to "
399
- "set the key's semantic type, which decides its a11y treatments."
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 (default 'text'). Decides which a11y treatments apply — e.g. 'image' (base value is the alt text), 'icon' (base is the accessible name), 'button'/'link' (offer aria_label), 'heading'/'label' (plain_language/screen_reader).",
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'); see create_key."},
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}")