@sonenta/mcp 0.20.0 → 0.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonenta/mcp",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
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.20.0"
3
+ version = "0.21.0"
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.20.0"
3
+ __version__ = "0.21.0"
@@ -25,6 +25,8 @@ Tools (V1 MCP surface; glossary_* added 2026-05-24; reports + trash added 2026-0
25
25
  - glossary_delete — delete an entry by uuid
26
26
  - health_report — read-only project source-health report (#752)
27
27
  - coverage_report — read-only project translation-coverage report (#752)
28
+ - list_source_duplicates — read-only list of duplicate-source-value groups (#1116)
29
+ - set_duplicate_status — triage a duplicate group: to_fix | allowed | resolved (#1116)
28
30
  - delete_keys_bulk — soft-delete (trash) keys by key_uuid; restorable
29
31
  - restore_keys_bulk — restore trashed keys by key_uuid
30
32
  - list_trash — list trashed (soft-deleted) keys (cursor paginated)
@@ -111,6 +113,38 @@ KEY_TYPES = (
111
113
  "meta_description",
112
114
  )
113
115
 
116
+ # Human/LLM guidance for choosing a key's `type`. Embedded in the create_key /
117
+ # update_key tool descriptions so an agent types keys correctly instead of
118
+ # leaving everything as the "text" default. The type DRIVES which a11y
119
+ # treatments + cognitive (plain-language) scoring a key gets, so it is the input
120
+ # that makes the a11y tooling work.
121
+ KEY_TYPE_GUIDANCE = (
122
+ "Set this at CREATION — do NOT leave everything as 'text'. The type drives "
123
+ "which a11y treatments the key offers (and whether it gets cognitive / "
124
+ "plain-language scoring), so correct typing is what lets the a11y tooling "
125
+ "work. Pick by the string's UI role:\n"
126
+ "- text — body / paragraph copy (offers plain_language, screen_reader)\n"
127
+ "- heading — a title or section heading (plain_language, screen_reader)\n"
128
+ "- label — a standalone short label (plain_language, screen_reader)\n"
129
+ "- caption — image caption / helper / secondary text (plain_language, screen_reader)\n"
130
+ "- button — a button's label (offers aria_label, screen_reader)\n"
131
+ "- link — a hyperlink / anchor label (aria_label, screen_reader)\n"
132
+ "- menu_item — a navigation / menu entry (aria_label, screen_reader)\n"
133
+ "- input_placeholder — a form input placeholder (aria_label)\n"
134
+ "- input_label — a form field's label (aria_label)\n"
135
+ "- tooltip — tooltip text (screen_reader)\n"
136
+ "- badge — a small status / count badge (screen_reader)\n"
137
+ "- error_message — a validation / error message (screen_reader)\n"
138
+ "- toast — a transient toast / snackbar notification (screen_reader)\n"
139
+ "- image — an image; THIS key's value IS the alt text\n"
140
+ "- icon — an icon; THIS key's value IS the accessible name (aria)\n"
141
+ "- meta_title — an SEO page <title> (no a11y treatment)\n"
142
+ "- meta_description — an SEO meta description (no a11y treatment)\n"
143
+ "Default 'text'. Reclassify existing keys later with update_key / "
144
+ "update_keys_bulk (type-only, no source_value needed). The current type is "
145
+ "returned by list_keys so you can audit and fix mis-typed keys."
146
+ )
147
+
114
148
 
115
149
  def _project_uuid(arguments: dict | None, client: VerbumiaClient) -> str:
116
150
  """Resolve project_uuid from per-call args or the configured env scope.
@@ -208,13 +242,16 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
208
242
  Tool(
209
243
  name="list_keys",
210
244
  description=(
211
- "List translation keys with their source-language value and "
212
- "per-language translations. Designed for bulk translation: "
213
- "Claude can scan the result and propose translations for "
214
- "every key that's still missing in a target language. "
215
- "Filters: namespace slug, status_filter (only keys with at "
216
- "least one translation in that status), language_code "
217
- "(restrict translations[] to a single language)."
245
+ "List translation keys with their source-language value, their "
246
+ "semantic `type`, and per-language translations. Designed for "
247
+ "bulk translation: Claude can scan the result and propose "
248
+ "translations for every key that's still missing in a target "
249
+ "language. Each key's `type` lets you AUDIT typing and reclassify "
250
+ "mis-typed keys (e.g. a button left as 'text') via update_key / "
251
+ "update_keys_bulk so the a11y treatments apply. Filters: "
252
+ "namespace slug, status_filter (only keys with at least one "
253
+ "translation in that status), language_code (restrict "
254
+ "translations[] to a single language)."
218
255
  ),
219
256
  inputSchema={
220
257
  "type": "object",
@@ -398,8 +435,11 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
398
435
  "Returns created=false if the key already exists. When "
399
436
  "source_value is supplied AND the project has a source "
400
437
  "language, the value is also written as a source-language "
401
- "translation in the same call. Pass `type` (default 'text') to "
402
- "set the key's semantic type, which decides its a11y treatments."
438
+ "translation in the same call. ALWAYS set `type` to the key's "
439
+ "semantic role (button / link / heading / image / icon / "
440
+ "input_label / …) — do NOT leave it as the default 'text'; the "
441
+ "type drives the key's a11y treatments. See the `type` field for "
442
+ "the full 17-type taxonomy and when to use each."
403
443
  ),
404
444
  inputSchema={
405
445
  "type": "object",
@@ -427,7 +467,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
427
467
  "type": "string",
428
468
  "enum": list(KEY_TYPES),
429
469
  "default": "text",
430
- "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).",
470
+ "description": "Semantic key type. " + KEY_TYPE_GUIDANCE,
431
471
  },
432
472
  },
433
473
  "required": ["namespace", "name"],
@@ -441,7 +481,11 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
441
481
  "doesn't exist (write-through). Status is set to draft (or "
442
482
  "translated if explicitly requested); reviewer/approved "
443
483
  "promotion requires a human reviewer. Identifies the key "
444
- "by namespace + key name + language_code — no uuids needed."
484
+ "by namespace + key name + language_code — no uuids needed. "
485
+ "The response carries `variable_warnings: {missing, extra}` "
486
+ "(null when interpolation variables match the source) — a "
487
+ "non-blocking flag for placeholder drift; check it and fix the "
488
+ "value if set."
445
489
  ),
446
490
  inputSchema={
447
491
  "type": "object",
@@ -499,7 +543,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
499
543
  "description": {"type": "string"},
500
544
  "plurals": {"type": "boolean", "default": False},
501
545
  "max_length": {"type": "integer", "minimum": 1},
502
- "type": {"type": "string", "enum": list(KEY_TYPES), "default": "text", "description": "Semantic key type (default 'text'); see create_key."},
546
+ "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."},
503
547
  },
504
548
  "required": ["namespace", "name"],
505
549
  "additionalProperties": False,
@@ -615,7 +659,10 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
615
659
  "(same namespace+key+language twice) error per item; status is "
616
660
  "draft (or translated if requested), reviewed/approved require "
617
661
  "a human reviewer. NO server-side translation — values come "
618
- "from you. Use this instead of N propose_translation calls."
662
+ "from you. Use this instead of N propose_translation calls. "
663
+ "Each result carries `variable_warnings: {missing, extra}` "
664
+ "(null when interpolation variables match the source) flagging "
665
+ "placeholder drift, non-blocking."
619
666
  ),
620
667
  inputSchema={
621
668
  "type": "object",
@@ -685,7 +732,10 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
685
732
  description=(
686
733
  "Lint a JSON i18next blob against the project: checks for "
687
734
  "missing keys, extra keys, and placeholder parity ({var}, "
688
- "{{var}}, %{var}) vs the source language."
735
+ "{{var}}, %{var}) vs the source language. Each "
736
+ "`placeholder_mismatches[]` item carries `expected` / `got` plus "
737
+ "`missing` (source variables absent from the translation) and "
738
+ "`extra` (variables in the translation unknown to the source)."
689
739
  ),
690
740
  inputSchema={
691
741
  "type": "object",
@@ -1666,6 +1716,81 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1666
1716
  "additionalProperties": False,
1667
1717
  },
1668
1718
  ),
1719
+ # --- Source Health: duplicate source strings (#1116; backend #1114, PR #147) ---
1720
+ Tool(
1721
+ name="list_source_duplicates",
1722
+ description=(
1723
+ "List groups of keys that SHARE an identical source-language "
1724
+ "value (Source Health duplicates, #1116). Each group = "
1725
+ "{source_value, key_count, key_names[], key_uuids[], status, note}. "
1726
+ "Duplicates inflate translation cost and drift (the same string "
1727
+ "translated N ways), so this is the worklist the "
1728
+ "sonenta-source-health agent repairs. Status is tracked PROJECT-WIDE "
1729
+ "per source_value: to_fix (needs attention; the default), allowed "
1730
+ "(intentional duplicate — listed+flagged but excluded from "
1731
+ "total_issues), resolved (already fixed — re-scan confirms). Filter "
1732
+ "by status to focus. READ-ONLY."
1733
+ ),
1734
+ inputSchema={
1735
+ "type": "object",
1736
+ "properties": {
1737
+ "project_uuid": {
1738
+ "type": "string",
1739
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1740
+ },
1741
+ "status": {
1742
+ "type": "string",
1743
+ "enum": ["to_fix", "allowed", "resolved"],
1744
+ "description": "Filter to duplicate groups in this triage status. Omit for all.",
1745
+ },
1746
+ "version_id": {
1747
+ "type": "string",
1748
+ "description": "Optional version UUID; defaults to the production version.",
1749
+ },
1750
+ },
1751
+ "additionalProperties": False,
1752
+ },
1753
+ ),
1754
+ Tool(
1755
+ name="set_duplicate_status",
1756
+ description=(
1757
+ "Set the triage STATUS of one duplicate group, addressed by its "
1758
+ "shared source_value (Source Health duplicates, #1116). status ∈ "
1759
+ "to_fix (this duplication should be repaired — the default; "
1760
+ "clears any override), allowed (an intentional, sanctioned "
1761
+ "duplicate — kept but EXCLUDED from total_issues, stop flagging "
1762
+ "it), resolved (the duplication has been fixed — removed from the "
1763
+ "list, a re-scan confirms). An invalid status returns 422. Status "
1764
+ "persists PROJECT-WIDE per source_value. Use this after the "
1765
+ "sonenta-source-health agent has de-duplicated the keys, to mark "
1766
+ "the group resolved, or to whitelist a legitimate duplicate as "
1767
+ "allowed. An optional note records WHY (audit trail)."
1768
+ ),
1769
+ inputSchema={
1770
+ "type": "object",
1771
+ "properties": {
1772
+ "project_uuid": {
1773
+ "type": "string",
1774
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1775
+ },
1776
+ "source_value": {
1777
+ "type": "string",
1778
+ "description": "The shared source-language value identifying the duplicate group (from list_source_duplicates).",
1779
+ },
1780
+ "status": {
1781
+ "type": "string",
1782
+ "enum": ["to_fix", "allowed", "resolved"],
1783
+ "description": "New triage status for the group.",
1784
+ },
1785
+ "note": {
1786
+ "type": "string",
1787
+ "description": "Optional human/agent note recording why the status was set (audit trail).",
1788
+ },
1789
+ },
1790
+ "required": ["source_value", "status"],
1791
+ "additionalProperties": False,
1792
+ },
1793
+ ),
1669
1794
  ]
1670
1795
 
1671
1796
  @server.call_tool()
@@ -2246,6 +2371,39 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
2246
2371
  f"/v1/mcp/projects/{project_uuid}/cognitive/analyze", json=body
2247
2372
  )
2248
2373
  return _text(data)
2374
+ # --- Source Health: duplicates (#1116; backend #1114, PR #147) ---
2375
+ if name == "list_source_duplicates":
2376
+ project_uuid = _project_uuid(args, client)
2377
+ params = {
2378
+ k: v
2379
+ for k, v in {
2380
+ "status": args.get("status"),
2381
+ "version_id": args.get("version_id"),
2382
+ }.items()
2383
+ if v is not None
2384
+ }
2385
+ data = await client.get(
2386
+ f"/v1/mcp/projects/{project_uuid}/source-duplicates",
2387
+ params=params,
2388
+ )
2389
+ return _text(data)
2390
+ if name == "set_duplicate_status":
2391
+ project_uuid = _project_uuid(args, client)
2392
+ if not args.get("source_value"):
2393
+ return _err("source_value is required")
2394
+ if not args.get("status"):
2395
+ return _err("status is required")
2396
+ body: dict[str, Any] = {
2397
+ "source_value": args["source_value"],
2398
+ "status": args["status"],
2399
+ }
2400
+ if args.get("note"):
2401
+ body["note"] = args["note"]
2402
+ data = await client.put(
2403
+ f"/v1/mcp/projects/{project_uuid}/source-duplicates/status",
2404
+ json=body,
2405
+ )
2406
+ return _text(data)
2249
2407
  return _err(f"unknown tool: {name}")
2250
2408
  except VerbumiaApiError as exc:
2251
2409
  return _err(f"API {exc.status}: {exc.body}")