@sonenta/mcp 0.34.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.
@@ -32,32 +32,81 @@ function which(cmd) {
32
32
  return !probe.error && probe.status === 0;
33
33
  }
34
34
 
35
- function runInherit(cmd, args) {
36
- const child = spawn(cmd, args, { stdio: "inherit" });
37
- child.on("exit", (code) => process.exit(code ?? 1));
38
- return child;
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
- runInherit("uvx", ["--from", BUNDLED_WHEEL, "sonenta-mcp", ...passthroughArgs]);
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
- runInherit(
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
- runInherit(
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("@sonenta/mcp: venv bootstrap failed: " + err.message + "\n");
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
- runInherit(venvEntry, passthroughArgs);
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: no usable Python launcher found.\n" +
123
- "Install one of these on PATH and retry:\n" +
124
- " - uv (recommended): https://docs.astral.sh/uv/\n" +
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.12)\n",
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
- if (tryUvx(argv)) return;
136
- if (tryUvRun(argv)) return;
137
- if (tryPipxRun(argv)) return;
138
- if (tryAdhocVenv(argv)) return;
139
- fail();
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.34.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",
@@ -1,14 +1,15 @@
1
1
  [project]
2
2
  name = "sonenta-mcp"
3
- version = "0.34.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.12,<3.14"
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
  ]
@@ -1,3 +1,3 @@
1
1
  """Sonenta MCP server (MIT)."""
2
2
 
3
- __version__ = "0.34.0"
3
+ __version__ = "0.34.1"