@sonenta/mcp 0.32.0 → 0.34.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.32.0",
3
+ "version": "0.34.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 = "sonenta-mcp"
3
- version = "0.32.0"
3
+ version = "0.34.0"
4
4
  description = "Model Context Protocol server for Sonenta — 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
  """Sonenta MCP server (MIT)."""
2
2
 
3
- __version__ = "0.32.0"
3
+ __version__ = "0.34.0"
@@ -1383,6 +1383,44 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1383
1383
  "additionalProperties": False,
1384
1384
  },
1385
1385
  ),
1386
+ Tool(
1387
+ name="list_untranslated_surfaces",
1388
+ description=(
1389
+ "The TRANSLATION MAP: every a11y surface cell that has an APPROVED "
1390
+ "SOURCE value but is missing the target-language translation, WITH that "
1391
+ "source value to translate from. Returns {cells: [{key_uuid, key_name, "
1392
+ "namespace_slug, key_type, surface, language_code, source_value}]}. Use "
1393
+ "this to fill a11y surfaces (aria_label / alt_text / screen_reader / "
1394
+ "plain_language) by FAITHFULLY translating each source_value into the "
1395
+ "missing language — do NOT invent new content — then write each with "
1396
+ "set_a11y_variant(key_uuid, language_code, surface, <translation>). "
1397
+ "Optionally filter by surface and/or language_code. (Local-first: you "
1398
+ "translate; 0 AI credits. The billed server path is translate_a11y_variants.)"
1399
+ ),
1400
+ inputSchema={
1401
+ "type": "object",
1402
+ "properties": {
1403
+ "project_uuid": {
1404
+ "type": "string",
1405
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1406
+ },
1407
+ "surface": {
1408
+ "type": "string",
1409
+ "enum": list(A11Y_SURFACES),
1410
+ "description": "Optional: only this a11y surface.",
1411
+ },
1412
+ "language_code": {
1413
+ "type": "string",
1414
+ "description": "Optional: only this target language (BCP-47).",
1415
+ },
1416
+ "version_id": {
1417
+ "type": "string",
1418
+ "description": "Optional version UUID; defaults to the production version.",
1419
+ },
1420
+ },
1421
+ "additionalProperties": False,
1422
+ },
1423
+ ),
1386
1424
  Tool(
1387
1425
  name="set_a11y_variant",
1388
1426
  description=(
@@ -2350,6 +2388,79 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
2350
2388
  "additionalProperties": False,
2351
2389
  },
2352
2390
  ),
2391
+ Tool(
2392
+ name="set_duplicate_status_bulk",
2393
+ description=(
2394
+ "Set ONE triage status across MANY duplicate groups at once (coarse "
2395
+ "triage). Body {source_values[], status, note?}. Intentionally NO "
2396
+ "files_modified (a single status over many groups). Use it to bulk-mark "
2397
+ "a batch as allowed (sanctioned) or to_fix; use the per-group "
2398
+ "set_duplicate_status for anything that records code changes."
2399
+ ),
2400
+ inputSchema={
2401
+ "type": "object",
2402
+ "properties": {
2403
+ "project_uuid": {
2404
+ "type": "string",
2405
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
2406
+ },
2407
+ "source_values": {
2408
+ "type": "array",
2409
+ "items": {"type": "string"},
2410
+ "description": "The shared source values identifying the duplicate groups.",
2411
+ },
2412
+ "status": {
2413
+ "type": "string",
2414
+ "enum": ["neutral", "allowed", "to_fix", "resolved"],
2415
+ "description": "New triage status applied to every listed group (neutral clears the override).",
2416
+ },
2417
+ "note": {"type": "string", "description": "Optional human/agent note (same on all)."},
2418
+ },
2419
+ "required": ["source_values", "status"],
2420
+ "additionalProperties": False,
2421
+ },
2422
+ ),
2423
+ Tool(
2424
+ name="propose_duplicate_merge_plan",
2425
+ description=(
2426
+ "OPT-IN, METERED second opinion: a server-side LLM reads a duplicate "
2427
+ "group (keys + translations + glossary + context) and PROPOSES a plan — "
2428
+ "recommendation merge | split | keep_separate + a well-formed merge_plan "
2429
+ "(null when keep_separate) + a one-line reason. PROPOSE-ONLY: it does NOT "
2430
+ "change status; apply via set_source_duplicate_merge_plan + "
2431
+ "apply_new_key_cluster / mark_cluster_applied / set_duplicate_status. "
2432
+ "You are already an LLM and decide locally at 0 credit — use this only "
2433
+ "as a tie-breaker or when the dev wants the AI's take. Costs 1 credit per "
2434
+ "generation UNLESS a valid cached proposal exists (cached=true → free); "
2435
+ "regenerate=true forces a fresh one. 402 when credits are insufficient. "
2436
+ "The persisted proposal also surfaces on list_source_duplicates "
2437
+ "(ai_proposed_plan / ai_diagnosis, free to read)."
2438
+ ),
2439
+ inputSchema={
2440
+ "type": "object",
2441
+ "properties": {
2442
+ "project_uuid": {
2443
+ "type": "string",
2444
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
2445
+ },
2446
+ "source_value": {
2447
+ "type": "string",
2448
+ "description": "The shared source value identifying the duplicate group.",
2449
+ },
2450
+ "namespace_slug": {
2451
+ "type": "string",
2452
+ "description": "Required only if the value spans namespaces (groups are intra-namespace).",
2453
+ },
2454
+ "regenerate": {
2455
+ "type": "boolean",
2456
+ "description": "Force a fresh diagnosis (1 credit) even if a valid cached one exists. Default false.",
2457
+ },
2458
+ "version_id": {"type": "string", "description": "Optional version UUID; defaults to the live version."},
2459
+ },
2460
+ "required": ["source_value"],
2461
+ "additionalProperties": False,
2462
+ },
2463
+ ),
2353
2464
  ]
2354
2465
 
2355
2466
  @server.call_tool()
@@ -2791,6 +2902,33 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
2791
2902
  "gaps": gaps,
2792
2903
  }
2793
2904
  )
2905
+ if name == "list_untranslated_surfaces":
2906
+ project_uuid = _project_uuid(args, client)
2907
+ params: dict[str, Any] = {"include_untranslated": True}
2908
+ if args.get("version_id"):
2909
+ params["version_id"] = args["version_id"]
2910
+ report = await client.get(
2911
+ f"/v1/mcp/projects/{project_uuid}/a11y-report", params=params
2912
+ )
2913
+ items = report.get("items", []) if isinstance(report, dict) else []
2914
+ surf_f = args.get("surface")
2915
+ lang_f = args.get("language_code")
2916
+ cells = [
2917
+ {
2918
+ "key_uuid": it.get("key_uuid"),
2919
+ "key_name": it.get("key_name"),
2920
+ "namespace_slug": it.get("namespace_slug"),
2921
+ "key_type": it.get("key_type"),
2922
+ "surface": it.get("surface"),
2923
+ "language_code": it.get("locale"),
2924
+ "source_value": it.get("source_value"),
2925
+ }
2926
+ for it in items
2927
+ if it.get("gap") == "a11y_untranslated"
2928
+ and (surf_f is None or it.get("surface") == surf_f)
2929
+ and (lang_f is None or it.get("locale") == lang_f)
2930
+ ]
2931
+ return _text({"project_uuid": project_uuid, "count": len(cells), "cells": cells})
2794
2932
  if name in ("set_a11y_variant", "delete_a11y_variant"):
2795
2933
  project_uuid = _project_uuid(args, client)
2796
2934
  key_uuid = args.get("key_uuid")
@@ -3122,6 +3260,35 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
3122
3260
  json=body,
3123
3261
  )
3124
3262
  return _text(data)
3263
+ if name == "set_duplicate_status_bulk":
3264
+ project_uuid = _project_uuid(args, client)
3265
+ source_values = args.get("source_values")
3266
+ if not isinstance(source_values, list) or not source_values:
3267
+ return _err("source_values must be a non-empty list")
3268
+ if not args.get("status"):
3269
+ return _err("status is required")
3270
+ body = {"source_values": source_values, "status": args["status"]}
3271
+ if args.get("note") is not None:
3272
+ body["note"] = args["note"]
3273
+ data = await client.put(
3274
+ f"/v1/mcp/projects/{project_uuid}/source-duplicates/status/bulk",
3275
+ json=body,
3276
+ )
3277
+ return _text(data)
3278
+ if name == "propose_duplicate_merge_plan":
3279
+ project_uuid = _project_uuid(args, client)
3280
+ if not args.get("source_value"):
3281
+ return _err("source_value is required")
3282
+ body = {"source_value": args["source_value"]}
3283
+ if args.get("namespace_slug"):
3284
+ body["namespace_slug"] = args["namespace_slug"]
3285
+ if args.get("regenerate"):
3286
+ body["regenerate"] = True
3287
+ url = f"/v1/mcp/projects/{project_uuid}/source-duplicates/propose-plan"
3288
+ if args.get("version_id"):
3289
+ url += f"?version_id={args['version_id']}"
3290
+ data = await client.post(url, json=body)
3291
+ return _text(data)
3125
3292
  return _err(f"unknown tool: {name}")
3126
3293
  except VerbumiaApiError as exc:
3127
3294
  return _err(f"API {exc.status}: {exc.body}")