@sonenta/mcp 0.17.0 → 0.19.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.17.0",
3
+ "version": "0.19.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.17.0"
3
+ version = "0.19.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.17.0"
3
+ __version__ = "0.19.0"
@@ -9,9 +9,9 @@ Tools (V1 MCP surface; glossary_* added 2026-05-24; reports + trash added 2026-0
9
9
  - missing_keys_stats — count-only summary of missing events (drives pagination)
10
10
  - acknowledge_missing_keys— batch-resolve missing events (cleanup the dashboard)
11
11
  - create_namespace — idempotent namespace creation (slug validated, slug_invalid on bad input)
12
- - create_key — idempotent key creation, optionally seeds source value
12
+ - create_key — idempotent key creation; optionally seeds source value + sets semantic `type`
13
13
  - create_keys_bulk — batch create_key (up to 500/call, per-item results)
14
- - update_key — edit a key's SOURCE value in place (PATCH); demotes reviewed/approved targets to needs-review
14
+ - update_key — edit a key's SOURCE value / `type` / description in place (PATCH); demotes reviewed/approved targets to needs-review
15
15
  - update_keys_bulk — batch update_key (up to 500/call, per-item results)
16
16
  - propose_translation — submit a translation value (auto-creates the key if missing)
17
17
  - propose_translations_bulk— batch propose_translation (up to 500/call, per-item results)
@@ -35,15 +35,24 @@ Tools (V1 MCP surface; glossary_* added 2026-05-24; reports + trash added 2026-0
35
35
  - list_a11y_gaps — actionable a11y gaps (filtered items[] of a11y_report)
36
36
  - set_a11y_variant — upsert one a11y variant override (key, language, surface)
37
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)
38
45
 
39
46
  A11y (accessibility) tools (a11y V1) extend the variant engine with four SEMANTIC
40
47
  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.
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.
47
56
 
48
57
  list_keys and list_untranslated_keys accept `include_glossary_hints=true` to
49
58
  attach backend-matched `glossary_hints[]` per key. All glossary matching is
@@ -75,6 +84,30 @@ from .client import VerbumiaApiError, VerbumiaClient
75
84
  # visible text. `surfaceKinds` in the variants matrix tags each as a11y.
76
85
  A11Y_SURFACES = ("aria_label", "alt_text", "screen_reader", "plain_language")
77
86
 
87
+ # Key semantic types (a11y key.type pivot). Default "text". The type decides
88
+ # which a11y treatments a key offers (e.g. an `image` key's base value IS its
89
+ # alt text; a `button` offers aria_label) — the engine gates variant/a11y tools
90
+ # by type server-side, so listing these here is just for create/update_key.
91
+ KEY_TYPES = (
92
+ "text",
93
+ "heading",
94
+ "label",
95
+ "caption",
96
+ "badge",
97
+ "tooltip",
98
+ "error_message",
99
+ "toast",
100
+ "button",
101
+ "link",
102
+ "menu_item",
103
+ "input_placeholder",
104
+ "input_label",
105
+ "image",
106
+ "icon",
107
+ "meta_title",
108
+ "meta_description",
109
+ )
110
+
78
111
 
79
112
  def _project_uuid(arguments: dict | None, client: VerbumiaClient) -> str:
80
113
  """Resolve project_uuid from per-call args or the configured env scope.
@@ -362,7 +395,8 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
362
395
  "Returns created=false if the key already exists. When "
363
396
  "source_value is supplied AND the project has a source "
364
397
  "language, the value is also written as a source-language "
365
- "translation in the same call."
398
+ "translation in the same call. Pass `type` (default 'text') to "
399
+ "set the key's semantic type, which decides its a11y treatments."
366
400
  ),
367
401
  inputSchema={
368
402
  "type": "object",
@@ -386,6 +420,12 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
386
420
  "description": {"type": "string"},
387
421
  "plurals": {"type": "boolean", "default": False},
388
422
  "max_length": {"type": "integer", "minimum": 1},
423
+ "type": {
424
+ "type": "string",
425
+ "enum": list(KEY_TYPES),
426
+ "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).",
428
+ },
389
429
  },
390
430
  "required": ["namespace", "name"],
391
431
  "additionalProperties": False,
@@ -456,6 +496,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
456
496
  "description": {"type": "string"},
457
497
  "plurals": {"type": "boolean", "default": False},
458
498
  "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."},
459
500
  },
460
501
  "required": ["namespace", "name"],
461
502
  "additionalProperties": False,
@@ -469,7 +510,8 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
469
510
  Tool(
470
511
  name="update_key",
471
512
  description=(
472
- "Edit a key's SOURCE-language value in place. Preserves the "
513
+ "Edit a key in place — its SOURCE-language value, its `type`, "
514
+ "and/or its description (pass at least one). Preserves the "
473
515
  "key and its history (version bump + history revision + audit). "
474
516
  "Address the key by EITHER key_uuid (preferred; from list_keys) "
475
517
  "OR namespace + name — exactly one of the two. When the source "
@@ -507,8 +549,13 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
507
549
  "type": "string",
508
550
  "description": "Optional: also update the key's description.",
509
551
  },
552
+ "type": {
553
+ "type": "string",
554
+ "enum": list(KEY_TYPES),
555
+ "description": "Optional: reclassify the key's semantic type (see create_key). Changing the type re-gates which a11y treatments the key offers.",
556
+ },
510
557
  },
511
- "required": ["source_value"],
558
+ "required": [],
512
559
  "additionalProperties": False,
513
560
  },
514
561
  ),
@@ -543,8 +590,9 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
543
590
  "name": {"type": "string", "description": "Dot-notation key name (use with namespace)."},
544
591
  "source_value": {"type": "string", "description": "New source-language value to write."},
545
592
  "description": {"type": "string", "description": "Optional: also update the key's description."},
593
+ "type": {"type": "string", "enum": list(KEY_TYPES), "description": "Optional: reclassify the key's semantic type (see create_key)."},
546
594
  },
547
- "required": ["source_value"],
595
+ "required": [],
548
596
  "additionalProperties": False,
549
597
  },
550
598
  },
@@ -1126,6 +1174,9 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1126
1174
  "locale lacks it), alt_missing (image key with no source alt_text), "
1127
1175
  "reading_level_high (hard source text + no plain_language), "
1128
1176
  "a11y_variant_absent (a require_surface is missing in the source). "
1177
+ "Gaps are TYPE-AWARE: only treatments relevant to each key's type are "
1178
+ "reported (e.g. alt_missing only on image keys, aria gaps only on "
1179
+ "buttons/links/inputs), so a text key is never flagged for aria/alt. "
1129
1180
  "Defaults to the production version. Surfaced verbatim — use "
1130
1181
  "list_a11y_gaps for just the actionable item list."
1131
1182
  ),
@@ -1287,6 +1338,221 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1287
1338
  "additionalProperties": False,
1288
1339
  },
1289
1340
  ),
1341
+ Tool(
1342
+ name="list_surfaces",
1343
+ description=(
1344
+ "List the project's configurable SURFACES (task 928): the device "
1345
+ "and a11y surfaces variants can be authored for. Returns "
1346
+ "{surfaces:[{slug, label, kind, builtin, active}]} where kind is "
1347
+ "'device' or 'a11y'. An un-customized project lazily defaults to the "
1348
+ "builtins — device: desktop/mobile/tablet; a11y: aria_label/alt_text/"
1349
+ "screen_reader/plain_language — all active. Only ACTIVE surfaces accept "
1350
+ "variant writes and get published to the CDN."
1351
+ ),
1352
+ inputSchema={
1353
+ "type": "object",
1354
+ "properties": {
1355
+ "project_uuid": {
1356
+ "type": "string",
1357
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1358
+ },
1359
+ },
1360
+ "additionalProperties": False,
1361
+ },
1362
+ ),
1363
+ Tool(
1364
+ name="create_surface",
1365
+ description=(
1366
+ "Create a CUSTOM DEVICE surface for the project (task 928), e.g. "
1367
+ "'watch', 'tv', 'kiosk'. Device surfaces are extensible; a11y surfaces "
1368
+ "are a FIXED semantic set — creating an a11y slug (e.g. aria_label) is "
1369
+ "rejected with 422. Returns the updated surface catalog. Builtins need "
1370
+ "no creation (they exist lazily)."
1371
+ ),
1372
+ inputSchema={
1373
+ "type": "object",
1374
+ "properties": {
1375
+ "project_uuid": {
1376
+ "type": "string",
1377
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1378
+ },
1379
+ "slug": {
1380
+ "type": "string",
1381
+ "pattern": "^[a-z0-9_-]{1,40}$",
1382
+ "description": "Surface slug: lowercase letters, digits, '_' or '-', 1-40 chars (e.g. 'watch').",
1383
+ },
1384
+ "label": {
1385
+ "type": "string",
1386
+ "description": "Optional human-readable label (defaults from the slug).",
1387
+ },
1388
+ },
1389
+ "required": ["slug"],
1390
+ "additionalProperties": False,
1391
+ },
1392
+ ),
1393
+ Tool(
1394
+ name="update_surface",
1395
+ description=(
1396
+ "Rename and/or toggle a surface (task 928): set its `label` and/or "
1397
+ "`active`. Deactivating is SOFT — existing variants are RETAINED but "
1398
+ "hidden and dropped from the CDN until reactivated (which republishes "
1399
+ "them); the response `affected_variants` reports how many are impacted. "
1400
+ "Builtins can be toggled/renamed but not deleted. Returns "
1401
+ "{surfaces, affected_variants}."
1402
+ ),
1403
+ inputSchema={
1404
+ "type": "object",
1405
+ "properties": {
1406
+ "project_uuid": {
1407
+ "type": "string",
1408
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1409
+ },
1410
+ "slug": {
1411
+ "type": "string",
1412
+ "description": "Slug of the surface to update.",
1413
+ },
1414
+ "label": {
1415
+ "type": "string",
1416
+ "description": "New human-readable label (rename).",
1417
+ },
1418
+ "active": {
1419
+ "type": "boolean",
1420
+ "description": "Activate (true) or soft-deactivate (false) the surface.",
1421
+ },
1422
+ },
1423
+ "required": ["slug"],
1424
+ "additionalProperties": False,
1425
+ },
1426
+ ),
1427
+ Tool(
1428
+ name="delete_surface",
1429
+ description=(
1430
+ "Delete a CUSTOM surface (task 928). Builtins cannot be deleted — "
1431
+ "deactivate them with update_surface(active=false) instead (a 422/409 "
1432
+ "is returned if you try to delete a builtin). Deleting a custom surface "
1433
+ "removes it from the catalog."
1434
+ ),
1435
+ inputSchema={
1436
+ "type": "object",
1437
+ "properties": {
1438
+ "project_uuid": {
1439
+ "type": "string",
1440
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1441
+ },
1442
+ "slug": {
1443
+ "type": "string",
1444
+ "description": "Slug of the custom surface to delete.",
1445
+ },
1446
+ },
1447
+ "required": ["slug"],
1448
+ "additionalProperties": False,
1449
+ },
1450
+ ),
1451
+ Tool(
1452
+ name="get_variants",
1453
+ description=(
1454
+ "Read the full VARIANT MATRIX for one key (task 928): every surface "
1455
+ "OFFERED for the key x every project language, with each cell's "
1456
+ "{override, value, resolvedValue, status, drift, ...}. The response "
1457
+ "lists `surfaces` and `surfaceKinds` ({surface: 'device'|'a11y'}) — "
1458
+ "these are the key's offered surfaces only: the project's active "
1459
+ "device surfaces plus the a11y treatments relevant to the key's TYPE "
1460
+ "(e.g. a text key shows no aria/alt). This is the source of truth for "
1461
+ "which surfaces set_variant will accept. Address by key_uuid (from "
1462
+ "list_keys). Surfaced verbatim."
1463
+ ),
1464
+ inputSchema={
1465
+ "type": "object",
1466
+ "properties": {
1467
+ "project_uuid": {
1468
+ "type": "string",
1469
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1470
+ },
1471
+ "key_uuid": {
1472
+ "type": "string",
1473
+ "description": "Key UUID (from list_keys).",
1474
+ },
1475
+ },
1476
+ "required": ["key_uuid"],
1477
+ "additionalProperties": False,
1478
+ },
1479
+ ),
1480
+ Tool(
1481
+ name="set_variant",
1482
+ description=(
1483
+ "Upsert ONE variant override for (key, language, surface) for ANY "
1484
+ "surface active on the project (task 928) — device (desktop / mobile / "
1485
+ "tablet / custom like watch / tv) OR a11y (aria_label / alt_text / "
1486
+ "screen_reader / plain_language). Address by key_uuid + locale CODE + "
1487
+ "surface slug (the fields get_variants / list_keys already give you). "
1488
+ "Stored as a draft. 422 unless the surface is OFFERED for this key — "
1489
+ "an active device surface (see list_surfaces) OR an a11y treatment "
1490
+ "relevant to the key's TYPE (e.g. no aria_label on a text key; see "
1491
+ "get_variants for the key's offered surfaces); 409 if the cell is "
1492
+ "already reviewed/approved (never overwritten). Returns {key_uuid, "
1493
+ "surface, language_code, value, status}. (For a11y specifically you "
1494
+ "can also use set_a11y_variant.)"
1495
+ ),
1496
+ inputSchema={
1497
+ "type": "object",
1498
+ "properties": {
1499
+ "project_uuid": {
1500
+ "type": "string",
1501
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1502
+ },
1503
+ "key_uuid": {
1504
+ "type": "string",
1505
+ "description": "Key UUID (from list_keys / get_variants).",
1506
+ },
1507
+ "language_code": {
1508
+ "type": "string",
1509
+ "description": "Target locale CODE, e.g. 'fr'. Backend resolves it; 404 if unknown.",
1510
+ },
1511
+ "surface": {
1512
+ "type": "string",
1513
+ "description": "Surface slug — any surface ACTIVE for the project (device or a11y). See list_surfaces.",
1514
+ },
1515
+ "value": {
1516
+ "type": "string",
1517
+ "description": "The variant text value for this (key, language, surface).",
1518
+ },
1519
+ },
1520
+ "required": ["key_uuid", "language_code", "surface", "value"],
1521
+ "additionalProperties": False,
1522
+ },
1523
+ ),
1524
+ Tool(
1525
+ name="delete_variant",
1526
+ description=(
1527
+ "Delete ONE variant override for (key, language, surface), for ANY "
1528
+ "surface (device or a11y) — task 928. The cell re-inherits the base "
1529
+ "value; a 404 means there was no override. Address by key_uuid + "
1530
+ "language_code + surface (same as set_variant)."
1531
+ ),
1532
+ inputSchema={
1533
+ "type": "object",
1534
+ "properties": {
1535
+ "project_uuid": {
1536
+ "type": "string",
1537
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1538
+ },
1539
+ "key_uuid": {
1540
+ "type": "string",
1541
+ "description": "Key UUID (from list_keys / get_variants).",
1542
+ },
1543
+ "language_code": {
1544
+ "type": "string",
1545
+ "description": "Target locale CODE, e.g. 'fr'.",
1546
+ },
1547
+ "surface": {
1548
+ "type": "string",
1549
+ "description": "Surface slug (device or a11y) to clear.",
1550
+ },
1551
+ },
1552
+ "required": ["key_uuid", "language_code", "surface"],
1553
+ "additionalProperties": False,
1554
+ },
1555
+ ),
1290
1556
  ]
1291
1557
 
1292
1558
  @server.call_tool()
@@ -1408,7 +1674,7 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1408
1674
  "namespace": args["namespace"],
1409
1675
  "name": args["name"],
1410
1676
  }
1411
- for opt in ("source_value", "description", "plurals", "max_length"):
1677
+ for opt in ("source_value", "description", "plurals", "max_length", "type"):
1412
1678
  if opt in args:
1413
1679
  body[opt] = args[opt]
1414
1680
  data = await client.post(
@@ -1440,16 +1706,18 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1440
1706
  return _text(data)
1441
1707
  if name == "update_key":
1442
1708
  project_uuid = _project_uuid(args, client)
1443
- if "source_value" not in args:
1444
- return _err("source_value is required")
1709
+ if not any(k in args for k in ("source_value", "type", "description")):
1710
+ return _err(
1711
+ "nothing to update: pass at least one of source_value, type, description"
1712
+ )
1445
1713
  has_uuid = bool(args.get("key_uuid"))
1446
1714
  has_name = bool(args.get("namespace")) and bool(args.get("name"))
1447
1715
  if has_uuid == has_name:
1448
1716
  return _err(
1449
1717
  "address the key by exactly one of key_uuid OR (namespace + name)"
1450
1718
  )
1451
- body: dict[str, Any] = {"source_value": args["source_value"]}
1452
- for opt in ("key_uuid", "namespace", "name", "description"):
1719
+ body: dict[str, Any] = {}
1720
+ for opt in ("source_value", "key_uuid", "namespace", "name", "description", "type"):
1453
1721
  if opt in args:
1454
1722
  body[opt] = args[opt]
1455
1723
  data = await client.patch(
@@ -1726,11 +1994,13 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1726
1994
  return _err("key_uuid, language_code and surface are required")
1727
1995
  if surface not in A11Y_SURFACES:
1728
1996
  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.
1997
+ # mcp:* code-addressed GENERIC variant route (covers a11y + device;
1998
+ # task 928). Backend resolves the locale code -> uuid and validates the
1999
+ # surface is active server-side. (The legacy /a11y-variants/ path still
2000
+ # works for back-compat, but /variants/ is canonical.)
1731
2001
  path = (
1732
2002
  f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}"
1733
- f"/a11y-variants/{language_code}/{surface}"
2003
+ f"/variants/{language_code}/{surface}"
1734
2004
  )
1735
2005
  if name == "delete_a11y_variant":
1736
2006
  await client.delete(path)
@@ -1746,6 +2016,79 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1746
2016
  return _err("value is required")
1747
2017
  data = await client.put(path, json={"value": args["value"]})
1748
2018
  return _text(data)
2019
+ if name == "list_surfaces":
2020
+ project_uuid = _project_uuid(args, client)
2021
+ data = await client.get(f"/v1/mcp/projects/{project_uuid}/surfaces")
2022
+ return _text(data)
2023
+ if name == "create_surface":
2024
+ project_uuid = _project_uuid(args, client)
2025
+ if not args.get("slug"):
2026
+ return _err("slug is required")
2027
+ body: dict[str, Any] = {"slug": args["slug"]}
2028
+ if "label" in args:
2029
+ body["label"] = args["label"]
2030
+ data = await client.post(
2031
+ f"/v1/mcp/projects/{project_uuid}/surfaces", json=body
2032
+ )
2033
+ return _text(data)
2034
+ if name == "update_surface":
2035
+ project_uuid = _project_uuid(args, client)
2036
+ slug = args.get("slug")
2037
+ if not slug:
2038
+ return _err("slug is required")
2039
+ body: dict[str, Any] = {}
2040
+ for opt in ("label", "active"):
2041
+ if opt in args:
2042
+ body[opt] = args[opt]
2043
+ if not body:
2044
+ return _err("nothing to update: pass label and/or active")
2045
+ data = await client.put(
2046
+ f"/v1/mcp/projects/{project_uuid}/surfaces/{slug}", json=body
2047
+ )
2048
+ return _text(data)
2049
+ if name == "delete_surface":
2050
+ project_uuid = _project_uuid(args, client)
2051
+ slug = args.get("slug")
2052
+ if not slug:
2053
+ return _err("slug is required")
2054
+ await client.delete(f"/v1/mcp/projects/{project_uuid}/surfaces/{slug}")
2055
+ return _text({"deleted": True, "slug": slug})
2056
+ if name == "get_variants":
2057
+ project_uuid = _project_uuid(args, client)
2058
+ key_uuid = args.get("key_uuid")
2059
+ if not key_uuid:
2060
+ return _err("key_uuid is required")
2061
+ data = await client.get(
2062
+ f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}/variants"
2063
+ )
2064
+ return _text(data)
2065
+ if name in ("set_variant", "delete_variant"):
2066
+ project_uuid = _project_uuid(args, client)
2067
+ key_uuid = args.get("key_uuid")
2068
+ language_code = args.get("language_code")
2069
+ surface = args.get("surface")
2070
+ if not key_uuid or not language_code or not surface:
2071
+ return _err("key_uuid, language_code and surface are required")
2072
+ # generic variant route: backend validates the surface is active (422)
2073
+ # and guards reviewed/approved cells (409).
2074
+ path = (
2075
+ f"/v1/mcp/projects/{project_uuid}/keys/{key_uuid}"
2076
+ f"/variants/{language_code}/{surface}"
2077
+ )
2078
+ if name == "delete_variant":
2079
+ await client.delete(path)
2080
+ return _text(
2081
+ {
2082
+ "deleted": True,
2083
+ "key_uuid": key_uuid,
2084
+ "language_code": language_code,
2085
+ "surface": surface,
2086
+ }
2087
+ )
2088
+ if "value" not in args:
2089
+ return _err("value is required")
2090
+ data = await client.put(path, json={"value": args["value"]})
2091
+ return _text(data)
1749
2092
  return _err(f"unknown tool: {name}")
1750
2093
  except VerbumiaApiError as exc:
1751
2094
  return _err(f"API {exc.status}: {exc.body}")