@sonenta/mcp 0.22.0 → 0.24.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/README.md CHANGED
@@ -18,7 +18,7 @@ runs the bundled `sonenta-mcp` directly.
18
18
  "env": {
19
19
  "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
20
20
  "SONENTA_PROJECTS": "<project_uuid>",
21
- "SONENTA_BASE_URL": "https://api.sonenta.com"
21
+ "SONENTA_BASE_URL": "https://api.sonenta.dev"
22
22
  }
23
23
  }
24
24
  }
@@ -63,8 +63,8 @@ Stdio is fully passthrough so the MCP JSON-RPC framing is preserved.
63
63
 
64
64
  | Var | Required | Default | Notes |
65
65
  |----------------------|----------|-----------------------------|--------------------------------------------------------|
66
- | `SONENTA_API_KEY` | yes | | Bearer ApiKey token. `SONENTA_TOKEN` accepted (back-compat) |
67
- | `SONENTA_BASE_URL` | no | `https://api.sonenta.com` | Self-host / staging override. `SONENTA_API_BASE` accepted (back-compat) |
66
+ | `SONENTA_API_KEY` | yes* | | Bearer ApiKey token. `SONENTA_TOKEN` accepted (back-compat). *Optional if you ran `sonenta login` — the server reads `~/.sonenta/credentials`. |
67
+ | `SONENTA_BASE_URL` | no | `https://api.sonenta.dev` | Self-host / staging override. `SONENTA_API_BASE` accepted (back-compat) |
68
68
  | `SONENTA_PROJECTS` | no | (LLM picks per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` per call. |
69
69
  | `SONENTA_PROJECT` | no | (legacy) | Singular fallback for v0.10.x users. Ignored when `SONENTA_PROJECTS` is set (warns). |
70
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonenta/mcp",
3
- "version": "0.22.0",
3
+ "version": "0.24.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",
package/python/README.md CHANGED
@@ -57,7 +57,7 @@ Single-project setup:
57
57
  "env": {
58
58
  "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
59
59
  "SONENTA_PROJECTS": "<project_uuid>",
60
- "SONENTA_BASE_URL": "https://api.sonenta.com"
60
+ "SONENTA_BASE_URL": "https://api.sonenta.dev"
61
61
  }
62
62
  }
63
63
  }
@@ -80,10 +80,10 @@ Restart Claude Desktop. Tools show up in the prompt UI.
80
80
 
81
81
  | Variable | Required | Default | Description |
82
82
  |----------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------|
83
- | `SONENTA_API_KEY` | yes | | API key from Org Settings → API Keys (`SONENTA_TOKEN` also accepted, back-compat) |
83
+ | `SONENTA_API_KEY` | yes* | | API key from Org Settings → API Keys (`SONENTA_TOKEN` also accepted). *Optional if you ran `sonenta login` — the server then reads `~/.sonenta/credentials`. |
84
84
  | `SONENTA_PROJECTS` | no | (LLM passes per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` on each tool call. |
85
85
  | `SONENTA_PROJECT` | no | (legacy) | Singular fallback for v0.10.x users. Ignored when `SONENTA_PROJECTS` is set (warns). |
86
- | `SONENTA_BASE_URL` | no | `https://api.sonenta.com` | Override for self-host or staging (`SONENTA_API_BASE` also accepted, back-compat) |
86
+ | `SONENTA_BASE_URL` | no | `https://api.sonenta.dev` | Override for self-host or staging (`SONENTA_API_BASE` also accepted, back-compat) |
87
87
 
88
88
  > Migrating from `verbumia-mcp`? The legacy `VERBUMIA_API_KEY` / `VERBUMIA_TOKEN`
89
89
  > / `VERBUMIA_PROJECTS` / `VERBUMIA_BASE_URL` env vars are still accepted, so
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sonenta-mcp"
3
- version = "0.22.0"
3
+ version = "0.24.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.22.0"
3
+ __version__ = "0.24.0"
@@ -1,21 +1,69 @@
1
1
  """Runtime configuration loaded from environment variables.
2
2
 
3
3
  The MCP server is invoked by clients like Claude Desktop, which pass `env`
4
- entries through to the spawned process. We never read a config file — env
5
- vars are the only contract.
4
+ entries through to the spawned process. Env vars are the primary contract.
6
5
 
7
6
  Canonical env vars are SONENTA_*. The legacy VERBUMIA_* names are still
8
7
  accepted (deprecate-not-remove) so existing MCP client configs keep working;
9
8
  the SONENTA_* name wins when both are set.
9
+
10
+ One exception to "env only": when no API key is in the env, we fall back to
11
+ the CLI credentials store (`~/.sonenta/credentials`, written by `sonenta
12
+ login`). That lets the auto-wired `.mcp.json` carry NO secret — just the host
13
+ and project — and the server resolves the key from the developer's login at
14
+ startup. CI / headless still pass SONENTA_API_KEY explicitly.
10
15
  """
11
16
 
12
17
  from __future__ import annotations
13
18
 
19
+ import json
14
20
  import os
15
21
  import sys
16
22
  from dataclasses import dataclass
17
-
18
- DEFAULT_API_BASE = "https://api.sonenta.com"
23
+ from pathlib import Path
24
+
25
+ DEFAULT_API_BASE = "https://api.sonenta.dev"
26
+
27
+ # CLI credentials store (written by `sonenta login`). Canonical ~/.sonenta,
28
+ # legacy ~/.verbumia read as a fallback. Shape:
29
+ # {"default": "<host>", "hosts": {"<host>": {"api_key": "...", ...}}}
30
+ _CREDENTIALS_PATHS = (
31
+ Path.home() / ".sonenta" / "credentials",
32
+ Path.home() / ".verbumia" / "credentials",
33
+ )
34
+
35
+
36
+ def _credentials_token(api_base: str) -> str:
37
+ """Resolve an API key from the CLI credentials store for `api_base`.
38
+
39
+ Lets the MCP server work with NO secret in its env / `.mcp.json` when the
40
+ user has logged in via `sonenta login`: we read the same creds file the CLI
41
+ writes and pick the key for the configured host (falling back to the file's
42
+ `default` host, then a sole entry). Returns "" when nothing resolves.
43
+ """
44
+ target = api_base.rstrip("/")
45
+ for path in _CREDENTIALS_PATHS:
46
+ try:
47
+ data = json.loads(path.read_text(encoding="utf-8"))
48
+ except (OSError, ValueError):
49
+ continue
50
+ hosts = data.get("hosts") if isinstance(data, dict) else None
51
+ if not isinstance(hosts, dict):
52
+ continue
53
+ for key in (target, api_base, data.get("default")):
54
+ entry = hosts.get(key) if isinstance(key, str) else None
55
+ if isinstance(entry, dict):
56
+ tok = str(entry.get("api_key", "")).strip()
57
+ if tok:
58
+ return tok
59
+ # A single-account creds file: use it regardless of the host key.
60
+ if len(hosts) == 1:
61
+ entry = next(iter(hosts.values()))
62
+ if isinstance(entry, dict):
63
+ tok = str(entry.get("api_key", "")).strip()
64
+ if tok:
65
+ return tok
66
+ return ""
19
67
 
20
68
 
21
69
  @dataclass(frozen=True)
@@ -59,24 +107,28 @@ def _env(*names: str) -> str:
59
107
 
60
108
 
61
109
  def load_config() -> Config:
62
- # Auth key. Canonical: SONENTA_API_KEY. Accepted fallbacks (in order):
63
- # SONENTA_TOKEN, then the legacy VERBUMIA_API_KEY / VERBUMIA_TOKEN.
64
- token = _env("SONENTA_API_KEY", "SONENTA_TOKEN", "VERBUMIA_API_KEY", "VERBUMIA_TOKEN")
65
- if not token:
66
- raise RuntimeError(
67
- "SONENTA_API_KEY is required. Generate one in Sonenta → Org Settings → "
68
- "API Keys and add it to your MCP client config (e.g. Claude Desktop's "
69
- "mcp.json `env`). SONENTA_TOKEN — and the legacy VERBUMIA_API_KEY / "
70
- "VERBUMIA_TOKEN — are also accepted for back-compat."
71
- )
72
-
73
- # API base. Canonical: SONENTA_BASE_URL. Fallbacks: SONENTA_API_BASE, then
74
- # legacy VERBUMIA_BASE_URL / VERBUMIA_API_BASE, then the default host.
110
+ # API base FIRST the creds fallback below keys the API key on this host.
111
+ # Canonical: SONENTA_BASE_URL. Fallbacks: SONENTA_API_BASE, then legacy
112
+ # VERBUMIA_BASE_URL / VERBUMIA_API_BASE, then the default host.
75
113
  api_base = (
76
114
  _env("SONENTA_BASE_URL", "SONENTA_API_BASE", "VERBUMIA_BASE_URL", "VERBUMIA_API_BASE")
77
115
  or DEFAULT_API_BASE
78
116
  ).rstrip("/")
79
117
 
118
+ # Auth key. Canonical: SONENTA_API_KEY. Env fallbacks (in order): SONENTA_TOKEN,
119
+ # then legacy VERBUMIA_API_KEY / VERBUMIA_TOKEN. When NONE are set, fall back to
120
+ # the CLI credentials store (`sonenta login`) so .mcp.json needs no secret.
121
+ token = _env("SONENTA_API_KEY", "SONENTA_TOKEN", "VERBUMIA_API_KEY", "VERBUMIA_TOKEN")
122
+ if not token:
123
+ token = _credentials_token(api_base)
124
+ if not token:
125
+ raise RuntimeError(
126
+ "No Sonenta API key. Either run `sonenta login` (the server then reads "
127
+ "your key from ~/.sonenta/credentials) or set SONENTA_API_KEY in your MCP "
128
+ "client config (e.g. Claude Desktop's mcp.json `env`). SONENTA_TOKEN — and "
129
+ "the legacy VERBUMIA_API_KEY / VERBUMIA_TOKEN — are also accepted."
130
+ )
131
+
80
132
  # Multi-project scoping. Canonical: SONENTA_PROJECTS (CSV) / SONENTA_PROJECT
81
133
  # (singular). Legacy VERBUMIA_PROJECTS / VERBUMIA_PROJECT still accepted.
82
134
  projects_csv = _env("SONENTA_PROJECTS", "VERBUMIA_PROJECTS")
@@ -1823,6 +1823,154 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
1823
1823
  "additionalProperties": False,
1824
1824
  },
1825
1825
  ),
1826
+ # --- a11y conformance reports + remediation plan (sprint 111c4658, B5 MCP twins) ---
1827
+ Tool(
1828
+ name="wcag_report",
1829
+ description=(
1830
+ "Read-only WCAG 2.2 CONFORMANCE report for the content layer Sonenta "
1831
+ "governs (strings, alt text, accessible names, a11y variants, reading "
1832
+ "level), evaluated PER LOCALE with a level-AA score. Reports the "
1833
+ "success criteria decidable from content (1.1.1, 1.3.1, 2.4.4, 2.4.9, "
1834
+ "3.1.1, 3.1.2, 3.1.5, 4.1.2); SC requiring the rendered DOM (contrast, "
1835
+ "keyboard, focus) appear ONLY under scope.out_of_scope_sc and are NEVER "
1836
+ "counted as pass. AAA criteria (2.4.9, 3.1.5) are shown but EXCLUDED "
1837
+ "from the AA score. conformance.score_pct is the headline AA number "
1838
+ "(with per-locale breakdown). Read scope.content_layer_sc dynamically — "
1839
+ "do not hardcode the SC list. Defaults to the production version. "
1840
+ "0-credit, read-only."
1841
+ ),
1842
+ inputSchema={
1843
+ "type": "object",
1844
+ "properties": {
1845
+ "project_uuid": {
1846
+ "type": "string",
1847
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1848
+ },
1849
+ "version_id": {
1850
+ "type": "string",
1851
+ "description": "Optional version UUID; defaults to the production version.",
1852
+ },
1853
+ },
1854
+ "additionalProperties": False,
1855
+ },
1856
+ ),
1857
+ Tool(
1858
+ name="eaa_statement",
1859
+ description=(
1860
+ "Read-only EAA / EN 301 549 accessibility conformance STATEMENT for the "
1861
+ "content layer, per project x locale set (JSON). Maps each covered WCAG "
1862
+ "2.2 SC to its EN 301 549 clause (e.g. 1.1.1 -> 9.1.1.1) and reports "
1863
+ "{org, product, version, level:'AA', score_aa_percent, generated_at, "
1864
+ "standard, criteria_summary[], scope_statement:{attested[], "
1865
+ "out_of_scope[]}, contact}. Honest scope: attests alt-text / labels / "
1866
+ "language / plain-language per locale; the rendered-DOM audit is out of "
1867
+ "scope. Defaults to the production version. 0-credit, read-only."
1868
+ ),
1869
+ inputSchema={
1870
+ "type": "object",
1871
+ "properties": {
1872
+ "project_uuid": {
1873
+ "type": "string",
1874
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1875
+ },
1876
+ "version_id": {
1877
+ "type": "string",
1878
+ "description": "Optional version UUID; defaults to the production version.",
1879
+ },
1880
+ },
1881
+ "additionalProperties": False,
1882
+ },
1883
+ ),
1884
+ Tool(
1885
+ name="a11y_remediation_plan_get",
1886
+ description=(
1887
+ "Read the a11y REMEDIATION PLAN for the project (or null if none "
1888
+ "exists). The plan is authored/approved in the dashboard; this tool "
1889
+ "OBSERVES it. Shape: {version_uuid, status (draft|approved), "
1890
+ "approved_at, plan:{items:[{key_uuid, locale, surface, decision "
1891
+ "(apply|ignore), reason?, value?}]}}. The backend mutates nothing on "
1892
+ "PUT/approve — once status=approved, call a11y_remediation_plan_apply "
1893
+ "to bulk-execute it. Defaults to the production version. Read-only."
1894
+ ),
1895
+ inputSchema={
1896
+ "type": "object",
1897
+ "properties": {
1898
+ "project_uuid": {
1899
+ "type": "string",
1900
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1901
+ },
1902
+ "version_id": {
1903
+ "type": "string",
1904
+ "description": "Optional version UUID; defaults to the production version.",
1905
+ },
1906
+ },
1907
+ "additionalProperties": False,
1908
+ },
1909
+ ),
1910
+ Tool(
1911
+ name="a11y_remediation_plan_apply",
1912
+ description=(
1913
+ "EXECUTE the APPROVED a11y remediation plan — the agent's bulk-execute "
1914
+ "entrypoint. Only acts on a plan whose status=approved (poll "
1915
+ "a11y_remediation_plan_get first); a draft/absent plan is rejected. For "
1916
+ "each 'apply' item it writes the a11y variant (the item's value, else "
1917
+ "the current draft override) via the normal variant overlay — "
1918
+ "non-destructive and reversible (trashable/restorable). 'ignore' items "
1919
+ "persistently suppress that (key, locale, surface) cell. Returns the "
1920
+ "execution summary. Defaults to the production version."
1921
+ ),
1922
+ inputSchema={
1923
+ "type": "object",
1924
+ "properties": {
1925
+ "project_uuid": {
1926
+ "type": "string",
1927
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1928
+ },
1929
+ "version_id": {
1930
+ "type": "string",
1931
+ "description": "Optional version UUID; defaults to the production version.",
1932
+ },
1933
+ },
1934
+ "additionalProperties": False,
1935
+ },
1936
+ ),
1937
+ Tool(
1938
+ name="score_cognitive_local",
1939
+ description=(
1940
+ "Compute and persist cognitive-difficulty scores from a VALIDATED, "
1941
+ "deterministic READABILITY metric — the local-first, **0-credit**, "
1942
+ "no-AI path (SC 3.1.5). English uses Flesch-Kincaid; every other "
1943
+ "language uses LIX, both normalised to difficulty 0-100 (higher = "
1944
+ "harder) with a per-language threshold. Writes cognitive_score for the "
1945
+ "scoped keys; a key enters the reading_level_high queue once its score "
1946
+ ">= the project threshold. Prefer this over analyze_cognitive (billed "
1947
+ "AI) for difficulty scoring. Scope with key_uuids and/or namespace_uuid "
1948
+ "(omit both = whole project). Returns the scoring summary."
1949
+ ),
1950
+ inputSchema={
1951
+ "type": "object",
1952
+ "properties": {
1953
+ "project_uuid": {
1954
+ "type": "string",
1955
+ "description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
1956
+ },
1957
+ "key_uuids": {
1958
+ "type": "array",
1959
+ "items": {"type": "string"},
1960
+ "description": "Restrict scoring to these keys. Omit to scope the whole project (or a namespace).",
1961
+ },
1962
+ "namespace_uuid": {
1963
+ "type": "string",
1964
+ "description": "Restrict scoring to one namespace by its UUID.",
1965
+ },
1966
+ "overwrite": {
1967
+ "type": "boolean",
1968
+ "description": "Re-score keys that already have a cognitive_score (default false = only score unscored keys).",
1969
+ },
1970
+ },
1971
+ "additionalProperties": False,
1972
+ },
1973
+ ),
1826
1974
  # --- Source Health: duplicate source strings (#1116; backend #1114, PR #147) ---
1827
1975
  Tool(
1828
1976
  name="list_source_duplicates",
@@ -2515,6 +2663,48 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
2515
2663
  f"/v1/mcp/projects/{project_uuid}/cognitive/analyze", json=body
2516
2664
  )
2517
2665
  return _text(data)
2666
+ # --- a11y conformance reports + remediation plan (sprint 111c4658, B5) ---
2667
+ if name in ("wcag_report", "eaa_statement"):
2668
+ project_uuid = _project_uuid(args, client)
2669
+ params: dict[str, Any] = {}
2670
+ if args.get("version_id"):
2671
+ params["version_id"] = args["version_id"]
2672
+ segment = "wcag-report" if name == "wcag_report" else "eaa-statement"
2673
+ data = await client.get(
2674
+ f"/v1/mcp/projects/{project_uuid}/{segment}", params=params
2675
+ )
2676
+ return _text(data)
2677
+ if name == "a11y_remediation_plan_get":
2678
+ project_uuid = _project_uuid(args, client)
2679
+ params: dict[str, Any] = {}
2680
+ if args.get("version_id"):
2681
+ params["version_id"] = args["version_id"]
2682
+ data = await client.get(
2683
+ f"/v1/mcp/projects/{project_uuid}/a11y/remediation-plan", params=params
2684
+ )
2685
+ return _text(data)
2686
+ if name == "a11y_remediation_plan_apply":
2687
+ project_uuid = _project_uuid(args, client)
2688
+ body: dict[str, Any] = {}
2689
+ if args.get("version_id"):
2690
+ body["version_id"] = args["version_id"]
2691
+ data = await client.post(
2692
+ f"/v1/mcp/projects/{project_uuid}/a11y/remediation-plan/apply",
2693
+ json=body,
2694
+ )
2695
+ return _text(data)
2696
+ if name == "score_cognitive_local":
2697
+ project_uuid = _project_uuid(args, client)
2698
+ body: dict[str, Any] = {}
2699
+ for opt in ("key_uuids", "namespace_uuid"):
2700
+ if args.get(opt):
2701
+ body[opt] = args[opt]
2702
+ if args.get("overwrite"):
2703
+ body["overwrite"] = True
2704
+ data = await client.post(
2705
+ f"/v1/mcp/projects/{project_uuid}/cognitive/score-local", json=body
2706
+ )
2707
+ return _text(data)
2518
2708
  # --- Source Health: duplicates (#1116; backend #1114, PR #147) ---
2519
2709
  if name == "list_source_duplicates":
2520
2710
  project_uuid = _project_uuid(args, client)