@sonenta/mcp 0.23.0 → 0.25.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 +3 -3
- package/package.json +1 -1
- package/python/README.md +3 -3
- package/python/dist/sonenta_mcp-0.25.0-py3-none-any.whl +0 -0
- package/python/pyproject.toml +1 -1
- package/python/verbumia_mcp/__init__.py +1 -1
- package/python/verbumia_mcp/config.py +69 -17
- package/python/verbumia_mcp/tools.py +54 -0
- package/python/dist/sonenta_mcp-0.23.0-py3-none-any.whl +0 -0
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.
|
|
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
|
|
67
|
-
| `SONENTA_BASE_URL` | no | `https://api.sonenta.
|
|
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.
|
|
3
|
+
"version": "0.25.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.
|
|
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
|
|
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.
|
|
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
|
|
Binary file
|
package/python/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sonenta-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.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,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.
|
|
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
|
-
|
|
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
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
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")
|
|
@@ -1971,6 +1971,48 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
1971
1971
|
"additionalProperties": False,
|
|
1972
1972
|
},
|
|
1973
1973
|
),
|
|
1974
|
+
# --- Placeholder mismatches (PM2 #173): translations whose interpolation
|
|
1975
|
+
# variables drift from the source (i18next {{}}, ICU {}, Ruby %{}). ---
|
|
1976
|
+
Tool(
|
|
1977
|
+
name="list_placeholder_mismatches",
|
|
1978
|
+
description=(
|
|
1979
|
+
"List TRANSLATIONS whose interpolation VARIABLES drift from their "
|
|
1980
|
+
"source value (Placeholder mismatch, PM2) — a broken `{{name}}` / "
|
|
1981
|
+
"`{count}` is a runtime bug (missing data, or a crash). Detection is "
|
|
1982
|
+
"by variable NAME and brace-agnostic (i18next `{{}}`, ICU `{}`, Ruby "
|
|
1983
|
+
"`%{}` all compared by name), evaluated PER LANGUAGE. Each item = "
|
|
1984
|
+
"{key_uuid, key ('namespace::name'), namespace_slug, language (BCP-47), "
|
|
1985
|
+
"source_value, source_vars[], target_value, target_vars[], missing[] "
|
|
1986
|
+
"(vars in the source absent from this translation), extra[] (vars in "
|
|
1987
|
+
"this translation unknown to the source)}. This is the worklist the "
|
|
1988
|
+
"sonenta-source-health agent repairs: rewrite the target value so its "
|
|
1989
|
+
"variables match the source, write it back (propose_translation), then "
|
|
1990
|
+
"RE-CALL this tool — `total == 0` confirms the fix (no server auto-fix). "
|
|
1991
|
+
"READ-ONLY. 0 credits."
|
|
1992
|
+
),
|
|
1993
|
+
inputSchema={
|
|
1994
|
+
"type": "object",
|
|
1995
|
+
"properties": {
|
|
1996
|
+
"project_uuid": {
|
|
1997
|
+
"type": "string",
|
|
1998
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
1999
|
+
},
|
|
2000
|
+
"version_id": {
|
|
2001
|
+
"type": "string",
|
|
2002
|
+
"description": "Optional version UUID; defaults to the production version.",
|
|
2003
|
+
},
|
|
2004
|
+
"key_id": {
|
|
2005
|
+
"type": "string",
|
|
2006
|
+
"description": "Restrict to a single key by its UUID.",
|
|
2007
|
+
},
|
|
2008
|
+
"language_code": {
|
|
2009
|
+
"type": "string",
|
|
2010
|
+
"description": "Restrict to a single target language by its BCP-47 code (e.g. 'fr').",
|
|
2011
|
+
},
|
|
2012
|
+
},
|
|
2013
|
+
"additionalProperties": False,
|
|
2014
|
+
},
|
|
2015
|
+
),
|
|
1974
2016
|
# --- Source Health: duplicate source strings (#1116; backend #1114, PR #147) ---
|
|
1975
2017
|
Tool(
|
|
1976
2018
|
name="list_source_duplicates",
|
|
@@ -2705,6 +2747,18 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
2705
2747
|
f"/v1/mcp/projects/{project_uuid}/cognitive/score-local", json=body
|
|
2706
2748
|
)
|
|
2707
2749
|
return _text(data)
|
|
2750
|
+
# --- Placeholder mismatches (PM2 #173) ---
|
|
2751
|
+
if name == "list_placeholder_mismatches":
|
|
2752
|
+
project_uuid = _project_uuid(args, client)
|
|
2753
|
+
params: dict[str, Any] = {}
|
|
2754
|
+
for opt in ("version_id", "key_id", "language_code"):
|
|
2755
|
+
if args.get(opt):
|
|
2756
|
+
params[opt] = args[opt]
|
|
2757
|
+
data = await client.get(
|
|
2758
|
+
f"/v1/mcp/projects/{project_uuid}/placeholder-mismatches",
|
|
2759
|
+
params=params,
|
|
2760
|
+
)
|
|
2761
|
+
return _text(data)
|
|
2708
2762
|
# --- Source Health: duplicates (#1116; backend #1114, PR #147) ---
|
|
2709
2763
|
if name == "list_source_duplicates":
|
|
2710
2764
|
project_uuid = _project_uuid(args, client)
|
|
Binary file
|