@sonenta/mcp 0.16.1 → 0.17.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.16.1",
3
+ "version": "0.17.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.16.0"
3
+ version = "0.17.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.16.0"
3
+ __version__ = "0.17.0"
@@ -28,6 +28,22 @@ Tools (V1 MCP surface; glossary_* added 2026-05-24; reports + trash added 2026-0
28
28
  - delete_keys_bulk — soft-delete (trash) keys by key_uuid; restorable
29
29
  - restore_keys_bulk — restore trashed keys by key_uuid
30
30
  - list_trash — list trashed (soft-deleted) keys (cursor paginated)
31
+ - a11y_estimate — preview the credit cost of an a11y generate/translate run (#a11y V1)
32
+ - generate_a11y_variant — AI-generate source-lang a11y variants (aria/alt/screen_reader/plain_language)
33
+ - translate_a11y_variants — translate existing source a11y overrides into target locales
34
+ - a11y_report — read-only WCAG a11y gap report (per key/surface/locale)
35
+ - list_a11y_gaps — actionable a11y gaps (filtered items[] of a11y_report)
36
+ - set_a11y_variant — upsert one a11y variant override (key, language, surface)
37
+ - delete_a11y_variant — delete one a11y variant override (cell re-inherits base value)
38
+
39
+ A11y (accessibility) tools (a11y V1) extend the variant engine with four SEMANTIC
40
+ surfaces — aria_label / alt_text / screen_reader / plain_language — orthogonal to
41
+ the visible text. The report hits `/v1/mcp/projects/{id}/a11y-report`; set/delete
42
+ use the mcp:* code-addressed route
43
+ `/v1/mcp/projects/{id}/keys/{key_uuid}/a11y-variants/{language_code}/{surface}`
44
+ (locale reinjected straight from an a11y_report item). estimate/generate/translate
45
+ are billed in credits. All are thin HTTP wrappers surfaced verbatim. Every a11y
46
+ tool is reachable with the same `mcp:*` key as the rest of the MCP surface.
31
47
 
32
48
  list_keys and list_untranslated_keys accept `include_glossary_hints=true` to
33
49
  attach backend-matched `glossary_hints[]` per key. All glossary matching is
@@ -45,6 +61,7 @@ The tool surface mirrors the canonical contract published in
45
61
  from __future__ import annotations
46
62
 
47
63
  import json
64
+ import uuid
48
65
  from typing import Any
49
66
 
50
67
  from mcp.server import Server
@@ -52,6 +69,12 @@ from mcp.types import TextContent, Tool
52
69
 
53
70
  from .client import VerbumiaApiError, VerbumiaClient
54
71
 
72
+ # The four accessibility (a11y) variant surfaces (task 989). Distinct from the
73
+ # device surfaces (desktop/mobile/tablet); these map to semantic attributes
74
+ # (aria-label, <img alt>, sr-only text, clear-language rendering), not the
75
+ # visible text. `surfaceKinds` in the variants matrix tags each as a11y.
76
+ A11Y_SURFACES = ("aria_label", "alt_text", "screen_reader", "plain_language")
77
+
55
78
 
56
79
  def _project_uuid(arguments: dict | None, client: VerbumiaClient) -> str:
57
80
  """Resolve project_uuid from per-call args or the configured env scope.
@@ -939,6 +962,331 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
939
962
  "additionalProperties": False,
940
963
  },
941
964
  ),
965
+ Tool(
966
+ name="a11y_estimate",
967
+ description=(
968
+ "Preview the CREDIT cost of an a11y generate/translate run BEFORE "
969
+ "spending anything (task 990). Returns {units, credit_cost_per_unit, "
970
+ "credits_required, balance, sufficient}. ALWAYS call this before "
971
+ "generate_a11y_variant / translate_a11y_variants so you know the cost "
972
+ "and whether the balance covers it. mode=generate counts missing "
973
+ "(key, surface) source variants; mode=translate counts "
974
+ "(key, surface, target-language) cells lacking a translation."
975
+ ),
976
+ inputSchema={
977
+ "type": "object",
978
+ "properties": {
979
+ "project_uuid": {
980
+ "type": "string",
981
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
982
+ },
983
+ "mode": {
984
+ "type": "string",
985
+ "enum": ["generate", "translate"],
986
+ "description": "generate = source-language a11y variants; translate = propagate existing a11y overrides into target locales.",
987
+ },
988
+ "tier": {
989
+ "type": "string",
990
+ "enum": ["standard", "premium"],
991
+ "default": "standard",
992
+ "description": "Quality tier (credit_cost per unit: standard=1, premium=4).",
993
+ },
994
+ "surfaces": {
995
+ "type": "array",
996
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
997
+ "description": "a11y surfaces to scope to. Omit/empty = all four (aria_label, alt_text, screen_reader, plain_language).",
998
+ },
999
+ "language_codes": {
1000
+ "type": "array",
1001
+ "items": {"type": "string"},
1002
+ "description": "Target locales (translate mode only), e.g. ['fr','es'].",
1003
+ },
1004
+ "key_uuids": {
1005
+ "type": "array",
1006
+ "items": {"type": "string"},
1007
+ "description": "Restrict to these keys (get key_uuid from list_keys). Omit to scope the whole project (or a namespace).",
1008
+ },
1009
+ "namespace_uuid": {
1010
+ "type": "string",
1011
+ "description": "Restrict to one namespace by its UUID.",
1012
+ },
1013
+ },
1014
+ "required": ["mode"],
1015
+ "additionalProperties": False,
1016
+ },
1017
+ ),
1018
+ Tool(
1019
+ name="generate_a11y_variant",
1020
+ description=(
1021
+ "AI-generate SOURCE-language a11y variants for keys that lack them "
1022
+ "(task 990). aria_label / screen_reader / plain_language are derived "
1023
+ "from the key's visible text; alt_text from the key's context/"
1024
+ "description. Only fills (key, surface) cells with NO existing override "
1025
+ "and an available source. BILLS CREDITS (standard=1, premium=4 per "
1026
+ "generated item, debit upfront + refund undelivered) — call a11y_estimate "
1027
+ "first. Results are draft (bot-authored) overrides that flow to the CDN "
1028
+ "a11y overlays. Returns {generated, requested, credits_charged, "
1029
+ "credits_refunded}."
1030
+ ),
1031
+ inputSchema={
1032
+ "type": "object",
1033
+ "properties": {
1034
+ "project_uuid": {
1035
+ "type": "string",
1036
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1037
+ },
1038
+ "tier": {
1039
+ "type": "string",
1040
+ "enum": ["standard", "premium"],
1041
+ "default": "standard",
1042
+ "description": "Quality tier (credit_cost per unit: standard=1, premium=4).",
1043
+ },
1044
+ "surfaces": {
1045
+ "type": "array",
1046
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1047
+ "description": "a11y surfaces to generate. Omit/empty = all four.",
1048
+ },
1049
+ "key_uuids": {
1050
+ "type": "array",
1051
+ "items": {"type": "string"},
1052
+ "description": "Restrict to these keys (get key_uuid from list_keys). Omit to scope the whole project (or a namespace).",
1053
+ },
1054
+ "namespace_uuid": {
1055
+ "type": "string",
1056
+ "description": "Restrict to one namespace by its UUID.",
1057
+ },
1058
+ "idempotency_key": {
1059
+ "type": "string",
1060
+ "description": "Stable token making this billable call safe to retry: reuse the SAME value to avoid a double-charge on retry. A random one is generated if omitted (so a blind retry would re-bill).",
1061
+ },
1062
+ },
1063
+ "additionalProperties": False,
1064
+ },
1065
+ ),
1066
+ Tool(
1067
+ name="translate_a11y_variants",
1068
+ description=(
1069
+ "Translate EXISTING source-language a11y overrides into target locales "
1070
+ "(task 990). Only translates (key, surface) that HAVE a source override, "
1071
+ "into the requested languages that lack one. BILLS CREDITS "
1072
+ "(standard=1, premium=4 per item, debit upfront + refund undelivered) — "
1073
+ "call a11y_estimate (mode=translate) first. Results are draft "
1074
+ "(bot-authored) overrides that flow to the CDN a11y overlays. Returns "
1075
+ "{translated, requested, credits_charged, credits_refunded}."
1076
+ ),
1077
+ inputSchema={
1078
+ "type": "object",
1079
+ "properties": {
1080
+ "project_uuid": {
1081
+ "type": "string",
1082
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1083
+ },
1084
+ "tier": {
1085
+ "type": "string",
1086
+ "enum": ["standard", "premium"],
1087
+ "default": "standard",
1088
+ "description": "Quality tier (credit_cost per unit: standard=1, premium=4).",
1089
+ },
1090
+ "language_codes": {
1091
+ "type": "array",
1092
+ "items": {"type": "string"},
1093
+ "minItems": 1,
1094
+ "description": "Target locales to translate into, e.g. ['fr','es'].",
1095
+ },
1096
+ "surfaces": {
1097
+ "type": "array",
1098
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1099
+ "description": "a11y surfaces to translate. Omit/empty = all four.",
1100
+ },
1101
+ "key_uuids": {
1102
+ "type": "array",
1103
+ "items": {"type": "string"},
1104
+ "description": "Restrict to these keys (get key_uuid from list_keys). Omit to scope the whole project (or a namespace).",
1105
+ },
1106
+ "namespace_uuid": {
1107
+ "type": "string",
1108
+ "description": "Restrict to one namespace by its UUID.",
1109
+ },
1110
+ "idempotency_key": {
1111
+ "type": "string",
1112
+ "description": "Stable token making this billable call safe to retry: reuse the SAME value to avoid a double-charge on retry. A random one is generated if omitted (so a blind retry would re-bill).",
1113
+ },
1114
+ },
1115
+ "required": ["language_codes"],
1116
+ "additionalProperties": False,
1117
+ },
1118
+ ),
1119
+ Tool(
1120
+ name="a11y_report",
1121
+ description=(
1122
+ "Read-only WCAG ACCESSIBILITY quality report for the project "
1123
+ "(task 991): per key/surface/locale a11y gaps with by_gap / "
1124
+ "by_severity / by_surface rollups + per-item details. Gap types: "
1125
+ "a11y_untranslated (a source a11y override exists but this target "
1126
+ "locale lacks it), alt_missing (image key with no source alt_text), "
1127
+ "reading_level_high (hard source text + no plain_language), "
1128
+ "a11y_variant_absent (a require_surface is missing in the source). "
1129
+ "Defaults to the production version. Surfaced verbatim — use "
1130
+ "list_a11y_gaps for just the actionable item list."
1131
+ ),
1132
+ inputSchema={
1133
+ "type": "object",
1134
+ "properties": {
1135
+ "project_uuid": {
1136
+ "type": "string",
1137
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1138
+ },
1139
+ "version_id": {
1140
+ "type": "string",
1141
+ "description": "Optional version UUID; defaults to the production version.",
1142
+ },
1143
+ "require_surface": {
1144
+ "type": "array",
1145
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1146
+ "description": "a11y surfaces to additionally flag as ABSENT in the source (produces a11y_variant_absent gaps), e.g. ['aria_label','alt_text'].",
1147
+ },
1148
+ "reading_level_max_words": {
1149
+ "type": "integer",
1150
+ "minimum": 5,
1151
+ "maximum": 60,
1152
+ "description": "Readability heuristic threshold (sentence length) for reading_level_high gaps. Default 15 server-side.",
1153
+ },
1154
+ },
1155
+ "additionalProperties": False,
1156
+ },
1157
+ ),
1158
+ Tool(
1159
+ name="list_a11y_gaps",
1160
+ description=(
1161
+ "List the actionable a11y gaps (the items[] of a11y_report, task "
1162
+ "991), optionally filtered client-side. Each gap = {key_uuid, "
1163
+ "key_name, namespace_slug, surface, locale, gap, severity, detail}. "
1164
+ "To find keys/surfaces with NO a11y variant in the source, pass "
1165
+ "require_surface=<surface> and filter gap='a11y_variant_absent'. "
1166
+ "Feed key_uuid + surface (+ locale) into set_a11y_variant to fix one, "
1167
+ "or generate_a11y_variant / translate_a11y_variants to fill them in "
1168
+ "bulk."
1169
+ ),
1170
+ inputSchema={
1171
+ "type": "object",
1172
+ "properties": {
1173
+ "project_uuid": {
1174
+ "type": "string",
1175
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1176
+ },
1177
+ "gap": {
1178
+ "type": "string",
1179
+ "enum": [
1180
+ "a11y_untranslated",
1181
+ "alt_missing",
1182
+ "reading_level_high",
1183
+ "a11y_variant_absent",
1184
+ ],
1185
+ "description": "Client-side filter: keep only gaps of this type.",
1186
+ },
1187
+ "surface": {
1188
+ "type": "string",
1189
+ "enum": list(A11Y_SURFACES),
1190
+ "description": "Client-side filter: keep only gaps on this a11y surface.",
1191
+ },
1192
+ "locale": {
1193
+ "type": "string",
1194
+ "description": "Client-side filter: keep only gaps for this target locale code (e.g. 'fr'). Source-level gaps have locale=null.",
1195
+ },
1196
+ "require_surface": {
1197
+ "type": "array",
1198
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1199
+ "description": "a11y surfaces to flag as ABSENT in the source (produces a11y_variant_absent gaps). Pair with gap='a11y_variant_absent' to list keys missing a surface.",
1200
+ },
1201
+ "version_id": {
1202
+ "type": "string",
1203
+ "description": "Optional version UUID; defaults to the production version.",
1204
+ },
1205
+ "reading_level_max_words": {
1206
+ "type": "integer",
1207
+ "minimum": 5,
1208
+ "maximum": 60,
1209
+ "description": "Readability heuristic threshold for reading_level_high gaps. Default 15 server-side.",
1210
+ },
1211
+ },
1212
+ "additionalProperties": False,
1213
+ },
1214
+ ),
1215
+ Tool(
1216
+ name="set_a11y_variant",
1217
+ description=(
1218
+ "Upsert ONE a11y variant override for (key, language, surface) — the "
1219
+ "manual fix for a gap (a11y V1). surface ∈ aria_label | alt_text | "
1220
+ "screen_reader | plain_language. The value is a TEXT string (the "
1221
+ "semantic a11y text, e.g. the accessible name or the simplified "
1222
+ "wording), translatable later via translate_a11y_variants. Address "
1223
+ "the key by key_uuid and the language by its locale CODE — exactly "
1224
+ "the (key_uuid, surface, locale) a gap carries in a11y_report, so you "
1225
+ "can fix it without resolving any UUID. Stored as a draft (bot) "
1226
+ "override. Returns {key_uuid, surface, language_code, value, status}."
1227
+ ),
1228
+ inputSchema={
1229
+ "type": "object",
1230
+ "properties": {
1231
+ "project_uuid": {
1232
+ "type": "string",
1233
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1234
+ },
1235
+ "key_uuid": {
1236
+ "type": "string",
1237
+ "description": "Key UUID (from list_keys or a11y_report / list_a11y_gaps items).",
1238
+ },
1239
+ "language_code": {
1240
+ "type": "string",
1241
+ "description": "Target locale CODE, e.g. 'fr' (the `locale` field of an a11y_report item). The backend resolves it to the language; 404 if unknown.",
1242
+ },
1243
+ "surface": {
1244
+ "type": "string",
1245
+ "enum": list(A11Y_SURFACES),
1246
+ "description": "Which a11y surface to set.",
1247
+ },
1248
+ "value": {
1249
+ "type": "string",
1250
+ "description": "The a11y text value for this (key, language, surface).",
1251
+ },
1252
+ },
1253
+ "required": ["key_uuid", "language_code", "surface", "value"],
1254
+ "additionalProperties": False,
1255
+ },
1256
+ ),
1257
+ Tool(
1258
+ name="delete_a11y_variant",
1259
+ description=(
1260
+ "Delete ONE a11y variant override for (key, language, surface) "
1261
+ "(a11y V1). The cell re-inherits the base value per the locale/surface "
1262
+ "fallback chain; a 404 means there was no override to remove. Address "
1263
+ "by key_uuid + language_code + surface (same as set_a11y_variant)."
1264
+ ),
1265
+ inputSchema={
1266
+ "type": "object",
1267
+ "properties": {
1268
+ "project_uuid": {
1269
+ "type": "string",
1270
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1271
+ },
1272
+ "key_uuid": {
1273
+ "type": "string",
1274
+ "description": "Key UUID (from list_keys or a11y_report / list_a11y_gaps items).",
1275
+ },
1276
+ "language_code": {
1277
+ "type": "string",
1278
+ "description": "Target locale CODE, e.g. 'fr' (the `locale` field of an a11y_report item).",
1279
+ },
1280
+ "surface": {
1281
+ "type": "string",
1282
+ "enum": list(A11Y_SURFACES),
1283
+ "description": "Which a11y surface to clear.",
1284
+ },
1285
+ },
1286
+ "required": ["key_uuid", "language_code", "surface"],
1287
+ "additionalProperties": False,
1288
+ },
1289
+ ),
942
1290
  ]
943
1291
 
944
1292
  @server.call_tool()
@@ -1131,7 +1479,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1131
1479
  project_uuid = _project_uuid(args, client)
1132
1480
  body: dict[str, Any] = {}
1133
1481
  for opt in ("language_code", "namespace", "version_slug"):
1134
- if opt in args and args[opt]:
1482
+ if args.get(opt):
1135
1483
  body[opt] = args[opt]
1136
1484
  data = await client.post(
1137
1485
  f"/v1/mcp/projects/{project_uuid}/cdn/releases", json=body
@@ -1273,6 +1621,131 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1273
1621
  f"/v1/mcp/projects/{project_uuid}/keys/trashed", params=params
1274
1622
  )
1275
1623
  return _text(data)
1624
+ if name == "a11y_estimate":
1625
+ project_uuid = _project_uuid(args, client)
1626
+ mode = args.get("mode")
1627
+ if mode not in ("generate", "translate"):
1628
+ return _err("mode must be 'generate' or 'translate'")
1629
+ body: dict[str, Any] = {"mode": mode, "tier": args.get("tier", "standard")}
1630
+ for opt in ("surfaces", "language_codes", "key_uuids", "namespace_uuid"):
1631
+ if args.get(opt):
1632
+ body[opt] = args[opt]
1633
+ data = await client.post(
1634
+ f"/v1/mcp/projects/{project_uuid}/a11y/estimate", json=body
1635
+ )
1636
+ return _text(data)
1637
+ if name == "generate_a11y_variant":
1638
+ project_uuid = _project_uuid(args, client)
1639
+ body: dict[str, Any] = {
1640
+ "tier": args.get("tier", "standard"),
1641
+ "idempotency_key": args.get("idempotency_key") or str(uuid.uuid4()),
1642
+ }
1643
+ for opt in ("surfaces", "key_uuids", "namespace_uuid"):
1644
+ if args.get(opt):
1645
+ body[opt] = args[opt]
1646
+ data = await client.post(
1647
+ f"/v1/mcp/projects/{project_uuid}/a11y/generate", json=body
1648
+ )
1649
+ return _text(data)
1650
+ if name == "translate_a11y_variants":
1651
+ project_uuid = _project_uuid(args, client)
1652
+ language_codes = args.get("language_codes")
1653
+ if not isinstance(language_codes, list) or not language_codes:
1654
+ return _err("language_codes must be a non-empty list")
1655
+ body: dict[str, Any] = {
1656
+ "tier": args.get("tier", "standard"),
1657
+ "language_codes": language_codes,
1658
+ "idempotency_key": args.get("idempotency_key") or str(uuid.uuid4()),
1659
+ }
1660
+ for opt in ("surfaces", "key_uuids", "namespace_uuid"):
1661
+ if args.get(opt):
1662
+ body[opt] = args[opt]
1663
+ data = await client.post(
1664
+ f"/v1/mcp/projects/{project_uuid}/a11y/translate", json=body
1665
+ )
1666
+ return _text(data)
1667
+ if name in ("a11y_report", "list_a11y_gaps"):
1668
+ project_uuid = _project_uuid(args, client)
1669
+ params: dict[str, Any] = {}
1670
+ if args.get("version_id"):
1671
+ params["version_id"] = args["version_id"]
1672
+ if args.get("require_surface"):
1673
+ # repeatable query param: httpx expands a list into repeats
1674
+ params["require_surface"] = args["require_surface"]
1675
+ if args.get("reading_level_max_words") is not None:
1676
+ params["reading_level_max_words"] = args["reading_level_max_words"]
1677
+ report = await client.get(
1678
+ f"/v1/mcp/projects/{project_uuid}/a11y-report", params=params
1679
+ )
1680
+ if name == "a11y_report":
1681
+ return _text(report)
1682
+ # list_a11y_gaps: flatten + client-side filter the items[] list.
1683
+ items = report.get("items", []) if isinstance(report, dict) else []
1684
+ gap_f = args.get("gap")
1685
+ surface_f = args.get("surface")
1686
+ locale_f = args.get("locale")
1687
+ gaps = [
1688
+ it
1689
+ for it in items
1690
+ if (gap_f is None or it.get("gap") == gap_f)
1691
+ and (surface_f is None or it.get("surface") == surface_f)
1692
+ and (locale_f is None or it.get("locale") == locale_f)
1693
+ ]
1694
+ applied = {
1695
+ k: v
1696
+ for k, v in {
1697
+ "gap": gap_f,
1698
+ "surface": surface_f,
1699
+ "locale": locale_f,
1700
+ "require_surface": args.get("require_surface"),
1701
+ }.items()
1702
+ if v
1703
+ }
1704
+ return _text(
1705
+ {
1706
+ "project_uuid": report.get("project_uuid", project_uuid)
1707
+ if isinstance(report, dict)
1708
+ else project_uuid,
1709
+ "version_uuid": report.get("version_uuid")
1710
+ if isinstance(report, dict)
1711
+ else None,
1712
+ "keys_scanned": report.get("keys_scanned")
1713
+ if isinstance(report, dict)
1714
+ else None,
1715
+ "filters": applied,
1716
+ "count": len(gaps),
1717
+ "gaps": gaps,
1718
+ }
1719
+ )
1720
+ if name in ("set_a11y_variant", "delete_a11y_variant"):
1721
+ project_uuid = _project_uuid(args, client)
1722
+ key_uuid = args.get("key_uuid")
1723
+ language_code = args.get("language_code")
1724
+ surface = args.get("surface")
1725
+ if not key_uuid or not language_code or not surface:
1726
+ return _err("key_uuid, language_code and surface are required")
1727
+ if surface not in A11Y_SURFACES:
1728
+ return _err(f"surface must be one of {', '.join(A11Y_SURFACES)}")
1729
+ # mcp:* code-addressed route: backend resolves the locale code -> uuid
1730
+ # and validates the a11y surface server-side.
1731
+ path = (
1732
+ f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}"
1733
+ f"/a11y-variants/{language_code}/{surface}"
1734
+ )
1735
+ if name == "delete_a11y_variant":
1736
+ await client.delete(path)
1737
+ return _text(
1738
+ {
1739
+ "deleted": True,
1740
+ "key_uuid": key_uuid,
1741
+ "language_code": language_code,
1742
+ "surface": surface,
1743
+ }
1744
+ )
1745
+ if "value" not in args:
1746
+ return _err("value is required")
1747
+ data = await client.put(path, json={"value": args["value"]})
1748
+ return _text(data)
1276
1749
  return _err(f"unknown tool: {name}")
1277
1750
  except VerbumiaApiError as exc:
1278
1751
  return _err(f"API {exc.status}: {exc.body}")