@sonenta/mcp 0.16.1 → 0.18.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.18.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.18.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.18.0"
@@ -28,6 +28,31 @@ 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
+ - list_surfaces — list the project's configurable surfaces (device + a11y; #928)
39
+ - create_surface — create a custom DEVICE surface (a11y is a fixed set)
40
+ - update_surface — rename / toggle-active a surface (deactivate is soft)
41
+ - delete_surface — delete a custom surface (builtins: deactivate only)
42
+ - get_variants — full variant matrix for one key (every active surface x language; #928)
43
+ - set_variant — upsert one variant override for ANY active surface (device or a11y)
44
+ - delete_variant — delete one variant override for any surface (cell re-inherits base)
45
+
46
+ A11y (accessibility) tools (a11y V1) extend the variant engine with four SEMANTIC
47
+ surfaces — aria_label / alt_text / screen_reader / plain_language — orthogonal to
48
+ the visible text. The report hits `/v1/mcp/projects/{id}/a11y-report`. Variant
49
+ writes (set_variant / delete_variant, and the a11y-named set_a11y_variant /
50
+ delete_a11y_variant) use the mcp:* code-addressed GENERIC route
51
+ `/v1/mcp/projects/{id}/keys/{key_uuid}/variants/{language_code}/{surface}`, which
52
+ accepts any surface ACTIVE for the project (device or a11y; #928) — locale
53
+ reinjected straight from a report/matrix item. estimate/generate/translate are
54
+ billed in credits. All are thin HTTP wrappers surfaced verbatim. Every tool is
55
+ reachable with the same `mcp:*` key as the rest of the MCP surface.
31
56
 
32
57
  list_keys and list_untranslated_keys accept `include_glossary_hints=true` to
33
58
  attach backend-matched `glossary_hints[]` per key. All glossary matching is
@@ -45,6 +70,7 @@ The tool surface mirrors the canonical contract published in
45
70
  from __future__ import annotations
46
71
 
47
72
  import json
73
+ import uuid
48
74
  from typing import Any
49
75
 
50
76
  from mcp.server import Server
@@ -52,6 +78,12 @@ from mcp.types import TextContent, Tool
52
78
 
53
79
  from .client import VerbumiaApiError, VerbumiaClient
54
80
 
81
+ # The four accessibility (a11y) variant surfaces (task 989). Distinct from the
82
+ # device surfaces (desktop/mobile/tablet); these map to semantic attributes
83
+ # (aria-label, <img alt>, sr-only text, clear-language rendering), not the
84
+ # visible text. `surfaceKinds` in the variants matrix tags each as a11y.
85
+ A11Y_SURFACES = ("aria_label", "alt_text", "screen_reader", "plain_language")
86
+
55
87
 
56
88
  def _project_uuid(arguments: dict | None, client: VerbumiaClient) -> str:
57
89
  """Resolve project_uuid from per-call args or the configured env scope.
@@ -939,6 +971,541 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
939
971
  "additionalProperties": False,
940
972
  },
941
973
  ),
974
+ Tool(
975
+ name="a11y_estimate",
976
+ description=(
977
+ "Preview the CREDIT cost of an a11y generate/translate run BEFORE "
978
+ "spending anything (task 990). Returns {units, credit_cost_per_unit, "
979
+ "credits_required, balance, sufficient}. ALWAYS call this before "
980
+ "generate_a11y_variant / translate_a11y_variants so you know the cost "
981
+ "and whether the balance covers it. mode=generate counts missing "
982
+ "(key, surface) source variants; mode=translate counts "
983
+ "(key, surface, target-language) cells lacking a translation."
984
+ ),
985
+ inputSchema={
986
+ "type": "object",
987
+ "properties": {
988
+ "project_uuid": {
989
+ "type": "string",
990
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
991
+ },
992
+ "mode": {
993
+ "type": "string",
994
+ "enum": ["generate", "translate"],
995
+ "description": "generate = source-language a11y variants; translate = propagate existing a11y overrides into target locales.",
996
+ },
997
+ "tier": {
998
+ "type": "string",
999
+ "enum": ["standard", "premium"],
1000
+ "default": "standard",
1001
+ "description": "Quality tier (credit_cost per unit: standard=1, premium=4).",
1002
+ },
1003
+ "surfaces": {
1004
+ "type": "array",
1005
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1006
+ "description": "a11y surfaces to scope to. Omit/empty = all four (aria_label, alt_text, screen_reader, plain_language).",
1007
+ },
1008
+ "language_codes": {
1009
+ "type": "array",
1010
+ "items": {"type": "string"},
1011
+ "description": "Target locales (translate mode only), e.g. ['fr','es'].",
1012
+ },
1013
+ "key_uuids": {
1014
+ "type": "array",
1015
+ "items": {"type": "string"},
1016
+ "description": "Restrict to these keys (get key_uuid from list_keys). Omit to scope the whole project (or a namespace).",
1017
+ },
1018
+ "namespace_uuid": {
1019
+ "type": "string",
1020
+ "description": "Restrict to one namespace by its UUID.",
1021
+ },
1022
+ },
1023
+ "required": ["mode"],
1024
+ "additionalProperties": False,
1025
+ },
1026
+ ),
1027
+ Tool(
1028
+ name="generate_a11y_variant",
1029
+ description=(
1030
+ "AI-generate SOURCE-language a11y variants for keys that lack them "
1031
+ "(task 990). aria_label / screen_reader / plain_language are derived "
1032
+ "from the key's visible text; alt_text from the key's context/"
1033
+ "description. Only fills (key, surface) cells with NO existing override "
1034
+ "and an available source. BILLS CREDITS (standard=1, premium=4 per "
1035
+ "generated item, debit upfront + refund undelivered) — call a11y_estimate "
1036
+ "first. Results are draft (bot-authored) overrides that flow to the CDN "
1037
+ "a11y overlays. Returns {generated, requested, credits_charged, "
1038
+ "credits_refunded}."
1039
+ ),
1040
+ inputSchema={
1041
+ "type": "object",
1042
+ "properties": {
1043
+ "project_uuid": {
1044
+ "type": "string",
1045
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1046
+ },
1047
+ "tier": {
1048
+ "type": "string",
1049
+ "enum": ["standard", "premium"],
1050
+ "default": "standard",
1051
+ "description": "Quality tier (credit_cost per unit: standard=1, premium=4).",
1052
+ },
1053
+ "surfaces": {
1054
+ "type": "array",
1055
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1056
+ "description": "a11y surfaces to generate. Omit/empty = all four.",
1057
+ },
1058
+ "key_uuids": {
1059
+ "type": "array",
1060
+ "items": {"type": "string"},
1061
+ "description": "Restrict to these keys (get key_uuid from list_keys). Omit to scope the whole project (or a namespace).",
1062
+ },
1063
+ "namespace_uuid": {
1064
+ "type": "string",
1065
+ "description": "Restrict to one namespace by its UUID.",
1066
+ },
1067
+ "idempotency_key": {
1068
+ "type": "string",
1069
+ "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).",
1070
+ },
1071
+ },
1072
+ "additionalProperties": False,
1073
+ },
1074
+ ),
1075
+ Tool(
1076
+ name="translate_a11y_variants",
1077
+ description=(
1078
+ "Translate EXISTING source-language a11y overrides into target locales "
1079
+ "(task 990). Only translates (key, surface) that HAVE a source override, "
1080
+ "into the requested languages that lack one. BILLS CREDITS "
1081
+ "(standard=1, premium=4 per item, debit upfront + refund undelivered) — "
1082
+ "call a11y_estimate (mode=translate) first. Results are draft "
1083
+ "(bot-authored) overrides that flow to the CDN a11y overlays. Returns "
1084
+ "{translated, requested, credits_charged, credits_refunded}."
1085
+ ),
1086
+ inputSchema={
1087
+ "type": "object",
1088
+ "properties": {
1089
+ "project_uuid": {
1090
+ "type": "string",
1091
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1092
+ },
1093
+ "tier": {
1094
+ "type": "string",
1095
+ "enum": ["standard", "premium"],
1096
+ "default": "standard",
1097
+ "description": "Quality tier (credit_cost per unit: standard=1, premium=4).",
1098
+ },
1099
+ "language_codes": {
1100
+ "type": "array",
1101
+ "items": {"type": "string"},
1102
+ "minItems": 1,
1103
+ "description": "Target locales to translate into, e.g. ['fr','es'].",
1104
+ },
1105
+ "surfaces": {
1106
+ "type": "array",
1107
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1108
+ "description": "a11y surfaces to translate. Omit/empty = all four.",
1109
+ },
1110
+ "key_uuids": {
1111
+ "type": "array",
1112
+ "items": {"type": "string"},
1113
+ "description": "Restrict to these keys (get key_uuid from list_keys). Omit to scope the whole project (or a namespace).",
1114
+ },
1115
+ "namespace_uuid": {
1116
+ "type": "string",
1117
+ "description": "Restrict to one namespace by its UUID.",
1118
+ },
1119
+ "idempotency_key": {
1120
+ "type": "string",
1121
+ "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).",
1122
+ },
1123
+ },
1124
+ "required": ["language_codes"],
1125
+ "additionalProperties": False,
1126
+ },
1127
+ ),
1128
+ Tool(
1129
+ name="a11y_report",
1130
+ description=(
1131
+ "Read-only WCAG ACCESSIBILITY quality report for the project "
1132
+ "(task 991): per key/surface/locale a11y gaps with by_gap / "
1133
+ "by_severity / by_surface rollups + per-item details. Gap types: "
1134
+ "a11y_untranslated (a source a11y override exists but this target "
1135
+ "locale lacks it), alt_missing (image key with no source alt_text), "
1136
+ "reading_level_high (hard source text + no plain_language), "
1137
+ "a11y_variant_absent (a require_surface is missing in the source). "
1138
+ "Defaults to the production version. Surfaced verbatim — use "
1139
+ "list_a11y_gaps for just the actionable item list."
1140
+ ),
1141
+ inputSchema={
1142
+ "type": "object",
1143
+ "properties": {
1144
+ "project_uuid": {
1145
+ "type": "string",
1146
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1147
+ },
1148
+ "version_id": {
1149
+ "type": "string",
1150
+ "description": "Optional version UUID; defaults to the production version.",
1151
+ },
1152
+ "require_surface": {
1153
+ "type": "array",
1154
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1155
+ "description": "a11y surfaces to additionally flag as ABSENT in the source (produces a11y_variant_absent gaps), e.g. ['aria_label','alt_text'].",
1156
+ },
1157
+ "reading_level_max_words": {
1158
+ "type": "integer",
1159
+ "minimum": 5,
1160
+ "maximum": 60,
1161
+ "description": "Readability heuristic threshold (sentence length) for reading_level_high gaps. Default 15 server-side.",
1162
+ },
1163
+ },
1164
+ "additionalProperties": False,
1165
+ },
1166
+ ),
1167
+ Tool(
1168
+ name="list_a11y_gaps",
1169
+ description=(
1170
+ "List the actionable a11y gaps (the items[] of a11y_report, task "
1171
+ "991), optionally filtered client-side. Each gap = {key_uuid, "
1172
+ "key_name, namespace_slug, surface, locale, gap, severity, detail}. "
1173
+ "To find keys/surfaces with NO a11y variant in the source, pass "
1174
+ "require_surface=<surface> and filter gap='a11y_variant_absent'. "
1175
+ "Feed key_uuid + surface (+ locale) into set_a11y_variant to fix one, "
1176
+ "or generate_a11y_variant / translate_a11y_variants to fill them in "
1177
+ "bulk."
1178
+ ),
1179
+ inputSchema={
1180
+ "type": "object",
1181
+ "properties": {
1182
+ "project_uuid": {
1183
+ "type": "string",
1184
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1185
+ },
1186
+ "gap": {
1187
+ "type": "string",
1188
+ "enum": [
1189
+ "a11y_untranslated",
1190
+ "alt_missing",
1191
+ "reading_level_high",
1192
+ "a11y_variant_absent",
1193
+ ],
1194
+ "description": "Client-side filter: keep only gaps of this type.",
1195
+ },
1196
+ "surface": {
1197
+ "type": "string",
1198
+ "enum": list(A11Y_SURFACES),
1199
+ "description": "Client-side filter: keep only gaps on this a11y surface.",
1200
+ },
1201
+ "locale": {
1202
+ "type": "string",
1203
+ "description": "Client-side filter: keep only gaps for this target locale code (e.g. 'fr'). Source-level gaps have locale=null.",
1204
+ },
1205
+ "require_surface": {
1206
+ "type": "array",
1207
+ "items": {"type": "string", "enum": list(A11Y_SURFACES)},
1208
+ "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.",
1209
+ },
1210
+ "version_id": {
1211
+ "type": "string",
1212
+ "description": "Optional version UUID; defaults to the production version.",
1213
+ },
1214
+ "reading_level_max_words": {
1215
+ "type": "integer",
1216
+ "minimum": 5,
1217
+ "maximum": 60,
1218
+ "description": "Readability heuristic threshold for reading_level_high gaps. Default 15 server-side.",
1219
+ },
1220
+ },
1221
+ "additionalProperties": False,
1222
+ },
1223
+ ),
1224
+ Tool(
1225
+ name="set_a11y_variant",
1226
+ description=(
1227
+ "Upsert ONE a11y variant override for (key, language, surface) — the "
1228
+ "manual fix for a gap (a11y V1). surface ∈ aria_label | alt_text | "
1229
+ "screen_reader | plain_language. The value is a TEXT string (the "
1230
+ "semantic a11y text, e.g. the accessible name or the simplified "
1231
+ "wording), translatable later via translate_a11y_variants. Address "
1232
+ "the key by key_uuid and the language by its locale CODE — exactly "
1233
+ "the (key_uuid, surface, locale) a gap carries in a11y_report, so you "
1234
+ "can fix it without resolving any UUID. Stored as a draft (bot) "
1235
+ "override. Returns {key_uuid, surface, language_code, value, status}."
1236
+ ),
1237
+ inputSchema={
1238
+ "type": "object",
1239
+ "properties": {
1240
+ "project_uuid": {
1241
+ "type": "string",
1242
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1243
+ },
1244
+ "key_uuid": {
1245
+ "type": "string",
1246
+ "description": "Key UUID (from list_keys or a11y_report / list_a11y_gaps items).",
1247
+ },
1248
+ "language_code": {
1249
+ "type": "string",
1250
+ "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.",
1251
+ },
1252
+ "surface": {
1253
+ "type": "string",
1254
+ "enum": list(A11Y_SURFACES),
1255
+ "description": "Which a11y surface to set.",
1256
+ },
1257
+ "value": {
1258
+ "type": "string",
1259
+ "description": "The a11y text value for this (key, language, surface).",
1260
+ },
1261
+ },
1262
+ "required": ["key_uuid", "language_code", "surface", "value"],
1263
+ "additionalProperties": False,
1264
+ },
1265
+ ),
1266
+ Tool(
1267
+ name="delete_a11y_variant",
1268
+ description=(
1269
+ "Delete ONE a11y variant override for (key, language, surface) "
1270
+ "(a11y V1). The cell re-inherits the base value per the locale/surface "
1271
+ "fallback chain; a 404 means there was no override to remove. Address "
1272
+ "by key_uuid + language_code + surface (same as set_a11y_variant)."
1273
+ ),
1274
+ inputSchema={
1275
+ "type": "object",
1276
+ "properties": {
1277
+ "project_uuid": {
1278
+ "type": "string",
1279
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1280
+ },
1281
+ "key_uuid": {
1282
+ "type": "string",
1283
+ "description": "Key UUID (from list_keys or a11y_report / list_a11y_gaps items).",
1284
+ },
1285
+ "language_code": {
1286
+ "type": "string",
1287
+ "description": "Target locale CODE, e.g. 'fr' (the `locale` field of an a11y_report item).",
1288
+ },
1289
+ "surface": {
1290
+ "type": "string",
1291
+ "enum": list(A11Y_SURFACES),
1292
+ "description": "Which a11y surface to clear.",
1293
+ },
1294
+ },
1295
+ "required": ["key_uuid", "language_code", "surface"],
1296
+ "additionalProperties": False,
1297
+ },
1298
+ ),
1299
+ Tool(
1300
+ name="list_surfaces",
1301
+ description=(
1302
+ "List the project's configurable SURFACES (task 928): the device "
1303
+ "and a11y surfaces variants can be authored for. Returns "
1304
+ "{surfaces:[{slug, label, kind, builtin, active}]} where kind is "
1305
+ "'device' or 'a11y'. An un-customized project lazily defaults to the "
1306
+ "builtins — device: desktop/mobile/tablet; a11y: aria_label/alt_text/"
1307
+ "screen_reader/plain_language — all active. Only ACTIVE surfaces accept "
1308
+ "variant writes and get published to the CDN."
1309
+ ),
1310
+ inputSchema={
1311
+ "type": "object",
1312
+ "properties": {
1313
+ "project_uuid": {
1314
+ "type": "string",
1315
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1316
+ },
1317
+ },
1318
+ "additionalProperties": False,
1319
+ },
1320
+ ),
1321
+ Tool(
1322
+ name="create_surface",
1323
+ description=(
1324
+ "Create a CUSTOM DEVICE surface for the project (task 928), e.g. "
1325
+ "'watch', 'tv', 'kiosk'. Device surfaces are extensible; a11y surfaces "
1326
+ "are a FIXED semantic set — creating an a11y slug (e.g. aria_label) is "
1327
+ "rejected with 422. Returns the updated surface catalog. Builtins need "
1328
+ "no creation (they exist lazily)."
1329
+ ),
1330
+ inputSchema={
1331
+ "type": "object",
1332
+ "properties": {
1333
+ "project_uuid": {
1334
+ "type": "string",
1335
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1336
+ },
1337
+ "slug": {
1338
+ "type": "string",
1339
+ "pattern": "^[a-z0-9_-]{1,40}$",
1340
+ "description": "Surface slug: lowercase letters, digits, '_' or '-', 1-40 chars (e.g. 'watch').",
1341
+ },
1342
+ "label": {
1343
+ "type": "string",
1344
+ "description": "Optional human-readable label (defaults from the slug).",
1345
+ },
1346
+ },
1347
+ "required": ["slug"],
1348
+ "additionalProperties": False,
1349
+ },
1350
+ ),
1351
+ Tool(
1352
+ name="update_surface",
1353
+ description=(
1354
+ "Rename and/or toggle a surface (task 928): set its `label` and/or "
1355
+ "`active`. Deactivating is SOFT — existing variants are RETAINED but "
1356
+ "hidden and dropped from the CDN until reactivated (which republishes "
1357
+ "them); the response `affected_variants` reports how many are impacted. "
1358
+ "Builtins can be toggled/renamed but not deleted. Returns "
1359
+ "{surfaces, affected_variants}."
1360
+ ),
1361
+ inputSchema={
1362
+ "type": "object",
1363
+ "properties": {
1364
+ "project_uuid": {
1365
+ "type": "string",
1366
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1367
+ },
1368
+ "slug": {
1369
+ "type": "string",
1370
+ "description": "Slug of the surface to update.",
1371
+ },
1372
+ "label": {
1373
+ "type": "string",
1374
+ "description": "New human-readable label (rename).",
1375
+ },
1376
+ "active": {
1377
+ "type": "boolean",
1378
+ "description": "Activate (true) or soft-deactivate (false) the surface.",
1379
+ },
1380
+ },
1381
+ "required": ["slug"],
1382
+ "additionalProperties": False,
1383
+ },
1384
+ ),
1385
+ Tool(
1386
+ name="delete_surface",
1387
+ description=(
1388
+ "Delete a CUSTOM surface (task 928). Builtins cannot be deleted — "
1389
+ "deactivate them with update_surface(active=false) instead (a 422/409 "
1390
+ "is returned if you try to delete a builtin). Deleting a custom surface "
1391
+ "removes it from the catalog."
1392
+ ),
1393
+ inputSchema={
1394
+ "type": "object",
1395
+ "properties": {
1396
+ "project_uuid": {
1397
+ "type": "string",
1398
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1399
+ },
1400
+ "slug": {
1401
+ "type": "string",
1402
+ "description": "Slug of the custom surface to delete.",
1403
+ },
1404
+ },
1405
+ "required": ["slug"],
1406
+ "additionalProperties": False,
1407
+ },
1408
+ ),
1409
+ Tool(
1410
+ name="get_variants",
1411
+ description=(
1412
+ "Read the full VARIANT MATRIX for one key (task 928): every ACTIVE "
1413
+ "surface (device + a11y) x every project language, with each cell's "
1414
+ "{override, value, resolvedValue, status, drift, ...}. The response "
1415
+ "also lists `surfaces` (active) and `surfaceKinds` ({surface: "
1416
+ "'device'|'a11y'}). Address the key by key_uuid (from list_keys). "
1417
+ "Surfaced verbatim."
1418
+ ),
1419
+ inputSchema={
1420
+ "type": "object",
1421
+ "properties": {
1422
+ "project_uuid": {
1423
+ "type": "string",
1424
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1425
+ },
1426
+ "key_uuid": {
1427
+ "type": "string",
1428
+ "description": "Key UUID (from list_keys).",
1429
+ },
1430
+ },
1431
+ "required": ["key_uuid"],
1432
+ "additionalProperties": False,
1433
+ },
1434
+ ),
1435
+ Tool(
1436
+ name="set_variant",
1437
+ description=(
1438
+ "Upsert ONE variant override for (key, language, surface) for ANY "
1439
+ "surface active on the project (task 928) — device (desktop / mobile / "
1440
+ "tablet / custom like watch / tv) OR a11y (aria_label / alt_text / "
1441
+ "screen_reader / plain_language). Address by key_uuid + locale CODE + "
1442
+ "surface slug (the fields get_variants / list_keys already give you). "
1443
+ "Stored as a draft. 422 if the surface is not active for the project "
1444
+ "(see list_surfaces); 409 if the cell is already reviewed/approved "
1445
+ "(never overwritten). Returns {key_uuid, surface, language_code, "
1446
+ "value, status}. (For a11y specifically you can also use "
1447
+ "set_a11y_variant.)"
1448
+ ),
1449
+ inputSchema={
1450
+ "type": "object",
1451
+ "properties": {
1452
+ "project_uuid": {
1453
+ "type": "string",
1454
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1455
+ },
1456
+ "key_uuid": {
1457
+ "type": "string",
1458
+ "description": "Key UUID (from list_keys / get_variants).",
1459
+ },
1460
+ "language_code": {
1461
+ "type": "string",
1462
+ "description": "Target locale CODE, e.g. 'fr'. Backend resolves it; 404 if unknown.",
1463
+ },
1464
+ "surface": {
1465
+ "type": "string",
1466
+ "description": "Surface slug — any surface ACTIVE for the project (device or a11y). See list_surfaces.",
1467
+ },
1468
+ "value": {
1469
+ "type": "string",
1470
+ "description": "The variant text value for this (key, language, surface).",
1471
+ },
1472
+ },
1473
+ "required": ["key_uuid", "language_code", "surface", "value"],
1474
+ "additionalProperties": False,
1475
+ },
1476
+ ),
1477
+ Tool(
1478
+ name="delete_variant",
1479
+ description=(
1480
+ "Delete ONE variant override for (key, language, surface), for ANY "
1481
+ "surface (device or a11y) — task 928. The cell re-inherits the base "
1482
+ "value; a 404 means there was no override. Address by key_uuid + "
1483
+ "language_code + surface (same as set_variant)."
1484
+ ),
1485
+ inputSchema={
1486
+ "type": "object",
1487
+ "properties": {
1488
+ "project_uuid": {
1489
+ "type": "string",
1490
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1491
+ },
1492
+ "key_uuid": {
1493
+ "type": "string",
1494
+ "description": "Key UUID (from list_keys / get_variants).",
1495
+ },
1496
+ "language_code": {
1497
+ "type": "string",
1498
+ "description": "Target locale CODE, e.g. 'fr'.",
1499
+ },
1500
+ "surface": {
1501
+ "type": "string",
1502
+ "description": "Surface slug (device or a11y) to clear.",
1503
+ },
1504
+ },
1505
+ "required": ["key_uuid", "language_code", "surface"],
1506
+ "additionalProperties": False,
1507
+ },
1508
+ ),
942
1509
  ]
943
1510
 
944
1511
  @server.call_tool()
@@ -1131,7 +1698,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1131
1698
  project_uuid = _project_uuid(args, client)
1132
1699
  body: dict[str, Any] = {}
1133
1700
  for opt in ("language_code", "namespace", "version_slug"):
1134
- if opt in args and args[opt]:
1701
+ if args.get(opt):
1135
1702
  body[opt] = args[opt]
1136
1703
  data = await client.post(
1137
1704
  f"/v1/mcp/projects/{project_uuid}/cdn/releases", json=body
@@ -1273,6 +1840,206 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1273
1840
  f"/v1/mcp/projects/{project_uuid}/keys/trashed", params=params
1274
1841
  )
1275
1842
  return _text(data)
1843
+ if name == "a11y_estimate":
1844
+ project_uuid = _project_uuid(args, client)
1845
+ mode = args.get("mode")
1846
+ if mode not in ("generate", "translate"):
1847
+ return _err("mode must be 'generate' or 'translate'")
1848
+ body: dict[str, Any] = {"mode": mode, "tier": args.get("tier", "standard")}
1849
+ for opt in ("surfaces", "language_codes", "key_uuids", "namespace_uuid"):
1850
+ if args.get(opt):
1851
+ body[opt] = args[opt]
1852
+ data = await client.post(
1853
+ f"/v1/mcp/projects/{project_uuid}/a11y/estimate", json=body
1854
+ )
1855
+ return _text(data)
1856
+ if name == "generate_a11y_variant":
1857
+ project_uuid = _project_uuid(args, client)
1858
+ body: dict[str, Any] = {
1859
+ "tier": args.get("tier", "standard"),
1860
+ "idempotency_key": args.get("idempotency_key") or str(uuid.uuid4()),
1861
+ }
1862
+ for opt in ("surfaces", "key_uuids", "namespace_uuid"):
1863
+ if args.get(opt):
1864
+ body[opt] = args[opt]
1865
+ data = await client.post(
1866
+ f"/v1/mcp/projects/{project_uuid}/a11y/generate", json=body
1867
+ )
1868
+ return _text(data)
1869
+ if name == "translate_a11y_variants":
1870
+ project_uuid = _project_uuid(args, client)
1871
+ language_codes = args.get("language_codes")
1872
+ if not isinstance(language_codes, list) or not language_codes:
1873
+ return _err("language_codes must be a non-empty list")
1874
+ body: dict[str, Any] = {
1875
+ "tier": args.get("tier", "standard"),
1876
+ "language_codes": language_codes,
1877
+ "idempotency_key": args.get("idempotency_key") or str(uuid.uuid4()),
1878
+ }
1879
+ for opt in ("surfaces", "key_uuids", "namespace_uuid"):
1880
+ if args.get(opt):
1881
+ body[opt] = args[opt]
1882
+ data = await client.post(
1883
+ f"/v1/mcp/projects/{project_uuid}/a11y/translate", json=body
1884
+ )
1885
+ return _text(data)
1886
+ if name in ("a11y_report", "list_a11y_gaps"):
1887
+ project_uuid = _project_uuid(args, client)
1888
+ params: dict[str, Any] = {}
1889
+ if args.get("version_id"):
1890
+ params["version_id"] = args["version_id"]
1891
+ if args.get("require_surface"):
1892
+ # repeatable query param: httpx expands a list into repeats
1893
+ params["require_surface"] = args["require_surface"]
1894
+ if args.get("reading_level_max_words") is not None:
1895
+ params["reading_level_max_words"] = args["reading_level_max_words"]
1896
+ report = await client.get(
1897
+ f"/v1/mcp/projects/{project_uuid}/a11y-report", params=params
1898
+ )
1899
+ if name == "a11y_report":
1900
+ return _text(report)
1901
+ # list_a11y_gaps: flatten + client-side filter the items[] list.
1902
+ items = report.get("items", []) if isinstance(report, dict) else []
1903
+ gap_f = args.get("gap")
1904
+ surface_f = args.get("surface")
1905
+ locale_f = args.get("locale")
1906
+ gaps = [
1907
+ it
1908
+ for it in items
1909
+ if (gap_f is None or it.get("gap") == gap_f)
1910
+ and (surface_f is None or it.get("surface") == surface_f)
1911
+ and (locale_f is None or it.get("locale") == locale_f)
1912
+ ]
1913
+ applied = {
1914
+ k: v
1915
+ for k, v in {
1916
+ "gap": gap_f,
1917
+ "surface": surface_f,
1918
+ "locale": locale_f,
1919
+ "require_surface": args.get("require_surface"),
1920
+ }.items()
1921
+ if v
1922
+ }
1923
+ return _text(
1924
+ {
1925
+ "project_uuid": report.get("project_uuid", project_uuid)
1926
+ if isinstance(report, dict)
1927
+ else project_uuid,
1928
+ "version_uuid": report.get("version_uuid")
1929
+ if isinstance(report, dict)
1930
+ else None,
1931
+ "keys_scanned": report.get("keys_scanned")
1932
+ if isinstance(report, dict)
1933
+ else None,
1934
+ "filters": applied,
1935
+ "count": len(gaps),
1936
+ "gaps": gaps,
1937
+ }
1938
+ )
1939
+ if name in ("set_a11y_variant", "delete_a11y_variant"):
1940
+ project_uuid = _project_uuid(args, client)
1941
+ key_uuid = args.get("key_uuid")
1942
+ language_code = args.get("language_code")
1943
+ surface = args.get("surface")
1944
+ if not key_uuid or not language_code or not surface:
1945
+ return _err("key_uuid, language_code and surface are required")
1946
+ if surface not in A11Y_SURFACES:
1947
+ return _err(f"surface must be one of {', '.join(A11Y_SURFACES)}")
1948
+ # mcp:* code-addressed GENERIC variant route (covers a11y + device;
1949
+ # task 928). Backend resolves the locale code -> uuid and validates the
1950
+ # surface is active server-side. (The legacy /a11y-variants/ path still
1951
+ # works for back-compat, but /variants/ is canonical.)
1952
+ path = (
1953
+ f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}"
1954
+ f"/variants/{language_code}/{surface}"
1955
+ )
1956
+ if name == "delete_a11y_variant":
1957
+ await client.delete(path)
1958
+ return _text(
1959
+ {
1960
+ "deleted": True,
1961
+ "key_uuid": key_uuid,
1962
+ "language_code": language_code,
1963
+ "surface": surface,
1964
+ }
1965
+ )
1966
+ if "value" not in args:
1967
+ return _err("value is required")
1968
+ data = await client.put(path, json={"value": args["value"]})
1969
+ return _text(data)
1970
+ if name == "list_surfaces":
1971
+ project_uuid = _project_uuid(args, client)
1972
+ data = await client.get(f"/v1/mcp/projects/{project_uuid}/surfaces")
1973
+ return _text(data)
1974
+ if name == "create_surface":
1975
+ project_uuid = _project_uuid(args, client)
1976
+ if not args.get("slug"):
1977
+ return _err("slug is required")
1978
+ body: dict[str, Any] = {"slug": args["slug"]}
1979
+ if "label" in args:
1980
+ body["label"] = args["label"]
1981
+ data = await client.post(
1982
+ f"/v1/mcp/projects/{project_uuid}/surfaces", json=body
1983
+ )
1984
+ return _text(data)
1985
+ if name == "update_surface":
1986
+ project_uuid = _project_uuid(args, client)
1987
+ slug = args.get("slug")
1988
+ if not slug:
1989
+ return _err("slug is required")
1990
+ body: dict[str, Any] = {}
1991
+ for opt in ("label", "active"):
1992
+ if opt in args:
1993
+ body[opt] = args[opt]
1994
+ if not body:
1995
+ return _err("nothing to update: pass label and/or active")
1996
+ data = await client.put(
1997
+ f"/v1/mcp/projects/{project_uuid}/surfaces/{slug}", json=body
1998
+ )
1999
+ return _text(data)
2000
+ if name == "delete_surface":
2001
+ project_uuid = _project_uuid(args, client)
2002
+ slug = args.get("slug")
2003
+ if not slug:
2004
+ return _err("slug is required")
2005
+ await client.delete(f"/v1/mcp/projects/{project_uuid}/surfaces/{slug}")
2006
+ return _text({"deleted": True, "slug": slug})
2007
+ if name == "get_variants":
2008
+ project_uuid = _project_uuid(args, client)
2009
+ key_uuid = args.get("key_uuid")
2010
+ if not key_uuid:
2011
+ return _err("key_uuid is required")
2012
+ data = await client.get(
2013
+ f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}/variants"
2014
+ )
2015
+ return _text(data)
2016
+ if name in ("set_variant", "delete_variant"):
2017
+ project_uuid = _project_uuid(args, client)
2018
+ key_uuid = args.get("key_uuid")
2019
+ language_code = args.get("language_code")
2020
+ surface = args.get("surface")
2021
+ if not key_uuid or not language_code or not surface:
2022
+ return _err("key_uuid, language_code and surface are required")
2023
+ # generic variant route: backend validates the surface is active (422)
2024
+ # and guards reviewed/approved cells (409).
2025
+ path = (
2026
+ f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}"
2027
+ f"/variants/{language_code}/{surface}"
2028
+ )
2029
+ if name == "delete_variant":
2030
+ await client.delete(path)
2031
+ return _text(
2032
+ {
2033
+ "deleted": True,
2034
+ "key_uuid": key_uuid,
2035
+ "language_code": language_code,
2036
+ "surface": surface,
2037
+ }
2038
+ )
2039
+ if "value" not in args:
2040
+ return _err("value is required")
2041
+ data = await client.put(path, json={"value": args["value"]})
2042
+ return _text(data)
1276
2043
  return _err(f"unknown tool: {name}")
1277
2044
  except VerbumiaApiError as exc:
1278
2045
  return _err(f"API {exc.status}: {exc.body}")