@sonenta/mcp 0.33.0 → 0.34.1
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/bin/sonenta-mcp.js +89 -22
- package/package.json +1 -1
- package/python/dist/sonenta_mcp-0.34.1-py3-none-any.whl +0 -0
- package/python/pyproject.toml +3 -2
- package/python/verbumia_mcp/__init__.py +1 -1
- package/python/verbumia_mcp/tools.py +102 -0
- package/python/dist/sonenta_mcp-0.33.0-py3-none-any.whl +0 -0
package/bin/sonenta-mcp.js
CHANGED
|
@@ -32,32 +32,81 @@ function which(cmd) {
|
|
|
32
32
|
return !probe.error && probe.status === 0;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// A runner that exits non-zero almost immediately never actually started the
|
|
36
|
+
// server — the usual cause is a Python-interpreter mismatch (e.g. the wheel's
|
|
37
|
+
// requires-python floor is unmet, so uv reports the resolution "unsatisfiable").
|
|
38
|
+
// Below this many ms, a non-zero exit is treated as "this launcher can't run
|
|
39
|
+
// it" and we hand off to the next one instead of dying. A real server runs far
|
|
40
|
+
// longer (it blocks on stdin until the MCP client disconnects).
|
|
41
|
+
const FAST_FAIL_MS = 4000;
|
|
42
|
+
|
|
43
|
+
// Give uv the freedom to pick/provision a compatible interpreter rather than
|
|
44
|
+
// hard-failing on the ambient python3, and never silence its errors.
|
|
45
|
+
const UV_ENV = {
|
|
46
|
+
...process.env,
|
|
47
|
+
// Prefer a uv-managed interpreter, but still accept a compatible system one.
|
|
48
|
+
UV_PYTHON_PREFERENCE: process.env.UV_PYTHON_PREFERENCE || "managed",
|
|
49
|
+
// Allow uv to download a matching interpreter when none on PATH satisfies the
|
|
50
|
+
// wheel's requires-python (no-op in offline/locked-down envs — the lowered
|
|
51
|
+
// >=3.11 floor is what makes the common ambient interpreter satisfy).
|
|
52
|
+
UV_PYTHON_DOWNLOADS: process.env.UV_PYTHON_DOWNLOADS || "automatic",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Spawn a runner with stdio passthrough. Calls onFastFail(code) if the child
|
|
57
|
+
* dies non-zero within FAST_FAIL_MS (so the caller can try the next launcher);
|
|
58
|
+
* otherwise propagates the child's exit code. Runners fail fast BEFORE reading
|
|
59
|
+
* stdin, so stdin is untouched and handing off is safe.
|
|
60
|
+
*/
|
|
61
|
+
function runOrHandoff(cmd, args, env, onFastFail) {
|
|
62
|
+
const started = Date.now();
|
|
63
|
+
const child = spawn(cmd, args, { stdio: "inherit", env });
|
|
64
|
+
let settled = false;
|
|
65
|
+
child.on("error", (err) => {
|
|
66
|
+
if (settled) return;
|
|
67
|
+
settled = true;
|
|
68
|
+
process.stderr.write("@sonenta/mcp: " + cmd + " failed to start: " + err.message + "\n");
|
|
69
|
+
onFastFail(1);
|
|
70
|
+
});
|
|
71
|
+
child.on("exit", (code, signal) => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
settled = true;
|
|
74
|
+
const elapsed = Date.now() - started;
|
|
75
|
+
if (code === 0 || elapsed >= FAST_FAIL_MS || signal) {
|
|
76
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
77
|
+
}
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
"@sonenta/mcp: " + cmd + " exited " + code + " after " + elapsed +
|
|
80
|
+
"ms (interpreter/launch problem) — trying the next launcher...\n",
|
|
81
|
+
);
|
|
82
|
+
onFastFail(code);
|
|
83
|
+
});
|
|
39
84
|
}
|
|
40
85
|
|
|
41
|
-
function tryUvx(passthroughArgs) {
|
|
86
|
+
function tryUvx(passthroughArgs, next) {
|
|
42
87
|
if (!which("uvx") || !BUNDLED_WHEEL) return false;
|
|
43
|
-
|
|
88
|
+
runOrHandoff("uvx", ["--from", BUNDLED_WHEEL, "sonenta-mcp", ...passthroughArgs], UV_ENV, next);
|
|
44
89
|
return true;
|
|
45
90
|
}
|
|
46
91
|
|
|
47
|
-
function tryUvRun(passthroughArgs) {
|
|
92
|
+
function tryUvRun(passthroughArgs, next) {
|
|
48
93
|
if (!which("uv")) return false;
|
|
49
|
-
|
|
94
|
+
runOrHandoff(
|
|
50
95
|
"uv",
|
|
51
96
|
["run", "--project", BUNDLED_PY_DIR, "sonenta-mcp", ...passthroughArgs],
|
|
97
|
+
UV_ENV,
|
|
98
|
+
next,
|
|
52
99
|
);
|
|
53
100
|
return true;
|
|
54
101
|
}
|
|
55
102
|
|
|
56
|
-
function tryPipxRun(passthroughArgs) {
|
|
103
|
+
function tryPipxRun(passthroughArgs, next) {
|
|
57
104
|
if (!which("pipx") || !BUNDLED_WHEEL) return false;
|
|
58
|
-
|
|
105
|
+
runOrHandoff(
|
|
59
106
|
"pipx",
|
|
60
107
|
["run", "--spec", BUNDLED_WHEEL, "sonenta-mcp", ...passthroughArgs],
|
|
108
|
+
process.env,
|
|
109
|
+
next,
|
|
61
110
|
);
|
|
62
111
|
return true;
|
|
63
112
|
}
|
|
@@ -68,7 +117,7 @@ function tryPipxRun(passthroughArgs) {
|
|
|
68
117
|
* first run; subsequent runs reuse the venv (keyed by wheel sha so a
|
|
69
118
|
* future sonenta-mcp version triggers a fresh venv automatically).
|
|
70
119
|
*/
|
|
71
|
-
function tryAdhocVenv(passthroughArgs) {
|
|
120
|
+
function tryAdhocVenv(passthroughArgs, next) {
|
|
72
121
|
if (!which("python3") || !BUNDLED_WHEEL) return false;
|
|
73
122
|
const wheelHash = crypto
|
|
74
123
|
.createHash("sha256")
|
|
@@ -93,14 +142,24 @@ function tryAdhocVenv(passthroughArgs) {
|
|
|
93
142
|
{ stdio: "inherit" },
|
|
94
143
|
);
|
|
95
144
|
} catch (err) {
|
|
96
|
-
process.stderr.write(
|
|
145
|
+
process.stderr.write(
|
|
146
|
+
"@sonenta/mcp: venv bootstrap failed (python3 " + pythonVersion() +
|
|
147
|
+
" may be older than the required >=3.11): " + err.message + "\n",
|
|
148
|
+
);
|
|
149
|
+
if (typeof next === "function") return next(1), true;
|
|
97
150
|
return false;
|
|
98
151
|
}
|
|
99
152
|
}
|
|
100
|
-
|
|
153
|
+
runOrHandoff(venvEntry, passthroughArgs, process.env, next || (() => process.exit(1)));
|
|
101
154
|
return true;
|
|
102
155
|
}
|
|
103
156
|
|
|
157
|
+
// Best-effort ambient python3 version string for diagnostics.
|
|
158
|
+
function pythonVersion() {
|
|
159
|
+
const probe = spawnSync("python3", ["--version"], { encoding: "utf8" });
|
|
160
|
+
return (probe.stdout || probe.stderr || "unknown").trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
104
163
|
function selfCheck() {
|
|
105
164
|
const hits = [];
|
|
106
165
|
if (which("uvx")) hits.push("uvx");
|
|
@@ -119,11 +178,13 @@ function selfCheck() {
|
|
|
119
178
|
|
|
120
179
|
function fail() {
|
|
121
180
|
process.stderr.write(
|
|
122
|
-
"@sonenta/mcp:
|
|
123
|
-
"
|
|
124
|
-
"
|
|
181
|
+
"@sonenta/mcp: could not start the MCP server with any Python launcher.\n" +
|
|
182
|
+
"Every available launcher failed — the most common cause is an ambient\n" +
|
|
183
|
+
"python3 older than the required >=3.11 (detected: " + pythonVersion() + ").\n" +
|
|
184
|
+
"Install or expose one of these on PATH and retry:\n" +
|
|
185
|
+
" - uv (recommended, auto-provisions Python): https://docs.astral.sh/uv/\n" +
|
|
125
186
|
" - pipx https://pipx.pypa.io/\n" +
|
|
126
|
-
" - python3 (>=3.
|
|
187
|
+
" - python3 (>=3.11)\n",
|
|
127
188
|
);
|
|
128
189
|
process.exit(127);
|
|
129
190
|
}
|
|
@@ -132,11 +193,17 @@ function main() {
|
|
|
132
193
|
const argv = process.argv.slice(2);
|
|
133
194
|
if (argv.includes("--self-check")) return selfCheck();
|
|
134
195
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
196
|
+
// Chain the launchers: each hands off to the next when it can't actually run
|
|
197
|
+
// the server (e.g. Python-floor mismatch), so one unusable launcher on PATH
|
|
198
|
+
// never causes a silent total failure.
|
|
199
|
+
const chain = [tryUvx, tryUvRun, tryPipxRun, tryAdhocVenv];
|
|
200
|
+
const step = (i) => {
|
|
201
|
+
for (let j = i; j < chain.length; j++) {
|
|
202
|
+
if (chain[j](argv, () => step(j + 1))) return;
|
|
203
|
+
}
|
|
204
|
+
fail();
|
|
205
|
+
};
|
|
206
|
+
step(0);
|
|
140
207
|
}
|
|
141
208
|
|
|
142
209
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonenta/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
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",
|
|
Binary file
|
package/python/pyproject.toml
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sonenta-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.34.1"
|
|
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
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.11,<3.14"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
8
|
authors = [{ name = "Sonenta" }]
|
|
9
9
|
keywords = ["mcp", "sonenta", "i18n", "translations", "claude"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
12
13
|
"Programming Language :: Python :: 3.12",
|
|
13
14
|
"Topic :: Software Development :: Internationalization",
|
|
14
15
|
]
|
|
@@ -2388,6 +2388,79 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
2388
2388
|
"additionalProperties": False,
|
|
2389
2389
|
},
|
|
2390
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
|
+
),
|
|
2391
2464
|
]
|
|
2392
2465
|
|
|
2393
2466
|
@server.call_tool()
|
|
@@ -3187,6 +3260,35 @@ def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
|
3187
3260
|
json=body,
|
|
3188
3261
|
)
|
|
3189
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)
|
|
3190
3292
|
return _err(f"unknown tool: {name}")
|
|
3191
3293
|
except VerbumiaApiError as exc:
|
|
3192
3294
|
return _err(f"API {exc.status}: {exc.body}")
|
|
Binary file
|