@sonenta/mcp 0.16.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/LICENSE +21 -0
- package/README.md +89 -0
- package/bin/sonenta-mcp.js +142 -0
- package/bin/verbumia-mcp.js +6 -0
- package/package.json +42 -0
- package/python/README.md +109 -0
- package/python/dist/verbumia_mcp-0.16.0-py3-none-any.whl +0 -0
- package/python/pyproject.toml +58 -0
- package/python/verbumia_mcp/__init__.py +3 -0
- package/python/verbumia_mcp/__main__.py +23 -0
- package/python/verbumia_mcp/client.py +85 -0
- package/python/verbumia_mcp/config.py +105 -0
- package/python/verbumia_mcp/server.py +88 -0
- package/python/verbumia_mcp/tools.py +1284 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Verbumia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @verbumia/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Verbumia](https://verbumia.ca) — wires Claude Desktop and
|
|
4
|
+
other Model Context Protocol clients into your translation project.
|
|
5
|
+
|
|
6
|
+
This npm package **bundles the Python MCP server source** (no PyPI
|
|
7
|
+
download). The launcher picks the best Python runtime on your host and
|
|
8
|
+
runs the bundled `verbumia_mcp` directly.
|
|
9
|
+
|
|
10
|
+
## Use it from Claude Desktop
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"verbumia": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "@verbumia/mcp"],
|
|
18
|
+
"env": {
|
|
19
|
+
"VERBUMIA_API_KEY": "vrb_live_<prefix>.<secret>",
|
|
20
|
+
"VERBUMIA_PROJECTS": "<project_uuid>",
|
|
21
|
+
"VERBUMIA_BASE_URL": "https://api.verbumia.dev"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For **multiple projects** (v0.11+), comma-separate the UUIDs in
|
|
29
|
+
`VERBUMIA_PROJECTS`. The LLM then has to pass `project_uuid` on every tool
|
|
30
|
+
call:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
"env": {
|
|
34
|
+
"VERBUMIA_API_KEY": "vrb_live_<prefix>.<secret>",
|
|
35
|
+
"VERBUMIA_PROJECTS": "01993a..,01993b..,01993c.."
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Restart Claude Desktop. All thirteen Verbumia tools (`list_projects`,
|
|
40
|
+
`get_project_info`, `list_keys`, `list_untranslated_keys`,
|
|
41
|
+
`list_missing_keys`, `missing_keys_stats`, `acknowledge_missing_keys`,
|
|
42
|
+
`create_key`, `propose_translation`, `publish_cdn`,
|
|
43
|
+
`validate_translations`, `project_context_get`, `project_context_set`)
|
|
44
|
+
become callable from your prompt.
|
|
45
|
+
|
|
46
|
+
## How the launcher works
|
|
47
|
+
|
|
48
|
+
The npm tarball ships the Python sources under `python/` plus a pre-built
|
|
49
|
+
wheel under `python/dist/`. The Node bin tries these in order:
|
|
50
|
+
|
|
51
|
+
1. **`uvx --from <bundled_wheel> verbumia-mcp`** — fastest, ephemeral
|
|
52
|
+
venv. Recommended.
|
|
53
|
+
2. **`uv run --project <bundled_dir> verbumia-mcp`** — uv-managed,
|
|
54
|
+
resolves deps from `pyproject.toml`.
|
|
55
|
+
3. **`pipx run --spec <bundled_wheel> verbumia-mcp`** — pipx-managed.
|
|
56
|
+
4. **Ad-hoc venv at `~/.verbumia/mcp-venv-<wheelhash>/`** — last-resort
|
|
57
|
+
fallback that requires only `python3 >= 3.12`. Slow first run, fast
|
|
58
|
+
thereafter.
|
|
59
|
+
|
|
60
|
+
Stdio is fully passthrough so the MCP JSON-RPC framing is preserved.
|
|
61
|
+
|
|
62
|
+
## Environment variables (canonical)
|
|
63
|
+
|
|
64
|
+
| Var | Required | Default | Notes |
|
|
65
|
+
|----------------------|----------|-----------------------------|--------------------------------------------------------|
|
|
66
|
+
| `VERBUMIA_API_KEY` | yes | | Bearer ApiKey token. `VERBUMIA_TOKEN` accepted (back-compat) |
|
|
67
|
+
| `VERBUMIA_BASE_URL` | no | `https://api.verbumia.dev` | Self-host / staging override. `VERBUMIA_API_BASE` accepted (back-compat) |
|
|
68
|
+
| `VERBUMIA_PROJECTS` | no | (LLM picks per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` per call. |
|
|
69
|
+
| `VERBUMIA_PROJECT` | no | (legacy) | Singular fallback for v0.10.x users. Ignored when `VERBUMIA_PROJECTS` is set (warns). |
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
One of:
|
|
74
|
+
|
|
75
|
+
- **uv** (recommended, https://docs.astral.sh/uv/)
|
|
76
|
+
- **pipx** (https://pipx.pypa.io/)
|
|
77
|
+
- **python3 >= 3.12** (for the venv fallback)
|
|
78
|
+
|
|
79
|
+
## Self-check
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx -y @verbumia/mcp --self-check
|
|
83
|
+
# -> @verbumia/mcp launchers detected: uvx, uv, python3
|
|
84
|
+
# -> @verbumia/mcp bundled wheel: <node_modules>/.../python/dist/verbumia_mcp-…whl
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @sonenta/mcp launcher
|
|
4
|
+
*
|
|
5
|
+
* Ships the Python MCP server source bundled inside the npm tarball
|
|
6
|
+
* (no PyPI dependency). Spawns it via the first available Python launcher:
|
|
7
|
+
*
|
|
8
|
+
* 1. uvx --from <bundled_wheel> — fastest, ephemeral venv
|
|
9
|
+
* 2. uv run --project <bundled_dir> verbumia-mcp — uv-managed
|
|
10
|
+
* 3. pipx run --spec <bundled_wheel> verbumia-mcp — pipx-managed
|
|
11
|
+
* 4. python3 + ad-hoc venv — last-resort, slowest first run
|
|
12
|
+
*
|
|
13
|
+
* Stdio is fully passthrough so the MCP JSON-RPC framing is preserved.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { spawn, spawnSync, execSync } = require("node:child_process");
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const os = require("node:os");
|
|
20
|
+
const crypto = require("node:crypto");
|
|
21
|
+
|
|
22
|
+
const BUNDLED_PY_DIR = path.resolve(__dirname, "..", "python");
|
|
23
|
+
const BUNDLED_WHEEL = (() => {
|
|
24
|
+
const distDir = path.join(BUNDLED_PY_DIR, "dist");
|
|
25
|
+
if (!fs.existsSync(distDir)) return null;
|
|
26
|
+
const wheels = fs.readdirSync(distDir).filter((f) => f.endsWith(".whl"));
|
|
27
|
+
return wheels.length ? path.join(distDir, wheels[0]) : null;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
function which(cmd) {
|
|
31
|
+
const probe = spawnSync(cmd, ["--version"], { stdio: "ignore" });
|
|
32
|
+
return !probe.error && probe.status === 0;
|
|
33
|
+
}
|
|
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;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tryUvx(passthroughArgs) {
|
|
42
|
+
if (!which("uvx") || !BUNDLED_WHEEL) return false;
|
|
43
|
+
runInherit("uvx", ["--from", BUNDLED_WHEEL, "verbumia-mcp", ...passthroughArgs]);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tryUvRun(passthroughArgs) {
|
|
48
|
+
if (!which("uv")) return false;
|
|
49
|
+
runInherit(
|
|
50
|
+
"uv",
|
|
51
|
+
["run", "--project", BUNDLED_PY_DIR, "verbumia-mcp", ...passthroughArgs],
|
|
52
|
+
);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryPipxRun(passthroughArgs) {
|
|
57
|
+
if (!which("pipx") || !BUNDLED_WHEEL) return false;
|
|
58
|
+
runInherit(
|
|
59
|
+
"pipx",
|
|
60
|
+
["run", "--spec", BUNDLED_WHEEL, "verbumia-mcp", ...passthroughArgs],
|
|
61
|
+
);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Last-resort fallback: bootstrap an ad-hoc venv under
|
|
67
|
+
* ~/.verbumia/mcp-venv-<wheelhash> and reuse it on subsequent runs. Slow
|
|
68
|
+
* first run; subsequent runs reuse the venv (keyed by wheel sha so a
|
|
69
|
+
* future verbumia-mcp version triggers a fresh venv automatically).
|
|
70
|
+
*/
|
|
71
|
+
function tryAdhocVenv(passthroughArgs) {
|
|
72
|
+
if (!which("python3") || !BUNDLED_WHEEL) return false;
|
|
73
|
+
const wheelHash = crypto
|
|
74
|
+
.createHash("sha256")
|
|
75
|
+
.update(fs.readFileSync(BUNDLED_WHEEL))
|
|
76
|
+
.digest("hex")
|
|
77
|
+
.slice(0, 12);
|
|
78
|
+
const venvRoot = path.join(os.homedir(), ".verbumia", "mcp-venv-" + wheelHash);
|
|
79
|
+
const venvBin = path.join(venvRoot, "bin");
|
|
80
|
+
const venvPy = path.join(venvBin, "python");
|
|
81
|
+
const venvEntry = path.join(venvBin, "verbumia-mcp");
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(venvEntry)) {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
"@sonenta/mcp: bootstrapping a venv at " + venvRoot + " (one-time install)\n",
|
|
86
|
+
);
|
|
87
|
+
try {
|
|
88
|
+
execSync("python3 -m venv " + JSON.stringify(venvRoot), { stdio: "ignore" });
|
|
89
|
+
execSync(
|
|
90
|
+
JSON.stringify(venvPy) +
|
|
91
|
+
" -m pip install --quiet --disable-pip-version-check " +
|
|
92
|
+
JSON.stringify(BUNDLED_WHEEL),
|
|
93
|
+
{ stdio: "inherit" },
|
|
94
|
+
);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
process.stderr.write("@sonenta/mcp: venv bootstrap failed: " + err.message + "\n");
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
runInherit(venvEntry, passthroughArgs);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function selfCheck() {
|
|
105
|
+
const hits = [];
|
|
106
|
+
if (which("uvx")) hits.push("uvx");
|
|
107
|
+
if (which("uv")) hits.push("uv");
|
|
108
|
+
if (which("pipx")) hits.push("pipx");
|
|
109
|
+
if (which("python3")) hits.push("python3");
|
|
110
|
+
process.stdout.write(
|
|
111
|
+
"@sonenta/mcp launchers detected: " + (hits.join(", ") || "none") + "\n",
|
|
112
|
+
);
|
|
113
|
+
process.stdout.write(
|
|
114
|
+
"@sonenta/mcp bundled wheel: " + (BUNDLED_WHEEL ?? "missing") + "\n",
|
|
115
|
+
);
|
|
116
|
+
process.stdout.write("@sonenta/mcp bundled source: " + BUNDLED_PY_DIR + "\n");
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fail() {
|
|
121
|
+
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" +
|
|
125
|
+
" - pipx https://pipx.pypa.io/\n" +
|
|
126
|
+
" - python3 (>=3.12)\n",
|
|
127
|
+
);
|
|
128
|
+
process.exit(127);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function main() {
|
|
132
|
+
const argv = process.argv.slice(2);
|
|
133
|
+
if (argv.includes("--self-check")) return selfCheck();
|
|
134
|
+
|
|
135
|
+
if (tryUvx(argv)) return;
|
|
136
|
+
if (tryUvRun(argv)) return;
|
|
137
|
+
if (tryPipxRun(argv)) return;
|
|
138
|
+
if (tryAdhocVenv(argv)) return;
|
|
139
|
+
fail();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deprecated alias for `sonenta-mcp` (@sonenta/mcp) — kept functional.
|
|
3
|
+
console.error(
|
|
4
|
+
"[deprecated] `verbumia-mcp` is now `sonenta-mcp` (@sonenta/mcp). This alias still works but will be removed in a future major.",
|
|
5
|
+
);
|
|
6
|
+
require("./sonenta-mcp.js");
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sonenta/mcp",
|
|
3
|
+
"version": "0.16.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://sonenta.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/verbumia/verbumia-tools.git",
|
|
10
|
+
"directory": "mcp/npm"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/verbumia/verbumia-tools/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"sonenta",
|
|
18
|
+
"verbumia",
|
|
19
|
+
"i18n",
|
|
20
|
+
"translations",
|
|
21
|
+
"claude",
|
|
22
|
+
"anthropic"
|
|
23
|
+
],
|
|
24
|
+
"author": "Sonenta",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"sonenta-mcp": "bin/sonenta-mcp.js",
|
|
30
|
+
"verbumia-mcp": "bin/verbumia-mcp.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"python/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node bin/sonenta-mcp.js --self-check",
|
|
40
|
+
"prepublishOnly": "cd python && uv build && rm -f dist/verbumia_mcp-*.tar.gz && printf '' > dist/.npmignore"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/python/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Verbumia MCP server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for [Verbumia](https://verbumia.ca) — exposes
|
|
4
|
+
your translation project to Claude Desktop and other MCP clients.
|
|
5
|
+
|
|
6
|
+
## Status
|
|
7
|
+
|
|
8
|
+
Thirteen tools wired: `list_projects`, `get_project_info`, `list_keys`,
|
|
9
|
+
`list_untranslated_keys`, `list_missing_keys`, `missing_keys_stats`,
|
|
10
|
+
`acknowledge_missing_keys`, `create_key`, `propose_translation`,
|
|
11
|
+
`publish_cdn`, `validate_translations`, `project_context_get`,
|
|
12
|
+
`project_context_set`.
|
|
13
|
+
|
|
14
|
+
### Project context document (V1.2)
|
|
15
|
+
|
|
16
|
+
`project_context_get` / `project_context_set` read and write a free-form
|
|
17
|
+
markdown blob attached to the project — terminology, brand voice, domain
|
|
18
|
+
notes (religious, legal, medical, gaming, etc.). The content is intended
|
|
19
|
+
as ambient context for human translators **and** for AI agents producing
|
|
20
|
+
translations: prepend it to your translation prompts so every output stays
|
|
21
|
+
consistent with the project's vocabulary and tone. Hard cap 100 KiB.
|
|
22
|
+
|
|
23
|
+
Scopes:
|
|
24
|
+
- `project_context_get` requires `project:read` (existing scope).
|
|
25
|
+
- `project_context_set` requires `project:settings:write` (new scope, narrower
|
|
26
|
+
than `project:write` since settings changes propagate to every translator's
|
|
27
|
+
prompt context).
|
|
28
|
+
|
|
29
|
+
Note: the blanket `mcp:*` scope is **not** sufficient for these tools — grant
|
|
30
|
+
the precise scope on the key.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Recommended (zero-install, used by Claude Desktop's mcp.json):
|
|
36
|
+
npx -y @verbumia/mcp
|
|
37
|
+
|
|
38
|
+
# Or from PyPI:
|
|
39
|
+
pipx install verbumia-mcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A Homebrew tap (`brew install verbumia/tap/verbumia-mcp`) is coming once we
|
|
43
|
+
publish to npm.
|
|
44
|
+
|
|
45
|
+
## Configure (Claude Desktop)
|
|
46
|
+
|
|
47
|
+
Open **Settings → Developer → Edit Config**, then:
|
|
48
|
+
|
|
49
|
+
Single-project setup:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"verbumia": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "@verbumia/mcp"],
|
|
57
|
+
"env": {
|
|
58
|
+
"VERBUMIA_API_KEY": "vrb_live_<prefix>.<secret>",
|
|
59
|
+
"VERBUMIA_PROJECTS": "<project_uuid>",
|
|
60
|
+
"VERBUMIA_BASE_URL": "https://api.verbumia.dev"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Multi-project setup (v0.11+) — comma-separate the UUIDs and Claude will pass
|
|
68
|
+
`project_uuid` on each tool call:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
"env": {
|
|
72
|
+
"VERBUMIA_API_KEY": "vrb_live_<prefix>.<secret>",
|
|
73
|
+
"VERBUMIA_PROJECTS": "01993a..,01993b..,01993c.."
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Restart Claude Desktop. Tools show up in the prompt UI.
|
|
78
|
+
|
|
79
|
+
## Environment
|
|
80
|
+
|
|
81
|
+
| Variable | Required | Default | Description |
|
|
82
|
+
|----------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------|
|
|
83
|
+
| `VERBUMIA_API_KEY` | yes | | API key from Org Settings → API Keys (`VERBUMIA_TOKEN` also accepted, back-compat) |
|
|
84
|
+
| `VERBUMIA_PROJECTS` | no | (LLM passes per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` on each tool call. |
|
|
85
|
+
| `VERBUMIA_PROJECT` | no | (legacy) | Singular fallback for v0.10.x users. Ignored when `VERBUMIA_PROJECTS` is set (warns). |
|
|
86
|
+
| `VERBUMIA_BASE_URL` | no | `https://api.verbumia.dev` | Override for self-host or staging (`VERBUMIA_API_BASE` also accepted, back-compat) |
|
|
87
|
+
|
|
88
|
+
The token format is `vrb_live_<prefix>.<secret>` and must carry the `mcp:*`
|
|
89
|
+
scope. For the `project_context_*` tools, also grant `project:read` and/or
|
|
90
|
+
`project:settings:write` — see the **Project context document** section
|
|
91
|
+
above. Treat the token like any other secret — keychain or `.env`, never
|
|
92
|
+
commit.
|
|
93
|
+
|
|
94
|
+
If your API key is pinned to a single project (its `project_uuid` is set
|
|
95
|
+
server-side), listing additional UUIDs in `VERBUMIA_PROJECTS` will surface
|
|
96
|
+
as a `403 API key is scoped to a different project` from the backend on
|
|
97
|
+
any call targeting one of the others.
|
|
98
|
+
|
|
99
|
+
## Local development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd mcp
|
|
103
|
+
uv sync
|
|
104
|
+
uv run verbumia-mcp # stdio transport — pipe MCP frames over stdin/stdout
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT.
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "verbumia-mcp"
|
|
3
|
+
version = "0.16.0"
|
|
4
|
+
description = "Model Context Protocol server for Verbumia — list projects, missing keys, propose translations from Claude Desktop and other MCP clients."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.14"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Verbumia" }]
|
|
9
|
+
keywords = ["mcp", "verbumia", "i18n", "translations", "claude"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Topic :: Software Development :: Internationalization",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"mcp>=1.0",
|
|
17
|
+
"httpx>=0.28",
|
|
18
|
+
"pydantic>=2.9",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
verbumia-mcp = "verbumia_mcp.__main__:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://verbumia.ca"
|
|
26
|
+
Documentation = "https://verbumia.ca/docs/mcp/setup"
|
|
27
|
+
Source = "https://github.com/verbumia/verbumia-tools"
|
|
28
|
+
Issues = "https://github.com/verbumia/verbumia-tools/issues"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.3",
|
|
33
|
+
"pytest-asyncio>=0.24",
|
|
34
|
+
"ruff>=0.8",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.uv]
|
|
38
|
+
package = true
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py312"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "ASYNC", "RUF"]
|
|
46
|
+
ignore = ["E501"]
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["hatchling"]
|
|
50
|
+
build-backend = "hatchling.build"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["verbumia_mcp"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
addopts = "-q --strict-markers"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Entrypoint for `verbumia-mcp` (and `python -m verbumia_mcp`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
from .server import run
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
asyncio.run(run())
|
|
14
|
+
except KeyboardInterrupt:
|
|
15
|
+
sys.exit(130)
|
|
16
|
+
except RuntimeError as exc:
|
|
17
|
+
# Configuration errors (missing token, etc.) — print a friendly hint.
|
|
18
|
+
print(f"verbumia-mcp: {exc}", file=sys.stderr)
|
|
19
|
+
sys.exit(2)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Async HTTP client wrapping the Verbumia REST API.
|
|
2
|
+
|
|
3
|
+
The MCP tools are thin adapters over this client — they translate MCP
|
|
4
|
+
arguments to API calls and shape the response. Authentication is the
|
|
5
|
+
`Authorization: ApiKey vrb_live_<prefix>.<secret>` header read from
|
|
6
|
+
`SONENTA_API_KEY` (or `SONENTA_TOKEN` for back-compat).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .config import Config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VerbumiaApiError(Exception):
|
|
19
|
+
"""Raised when the API returns a non-2xx response."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, status: int, body: str | dict[str, Any]) -> None:
|
|
22
|
+
self.status = status
|
|
23
|
+
self.body = body
|
|
24
|
+
super().__init__(f"Verbumia API error {status}: {body}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VerbumiaClient:
|
|
28
|
+
"""Tiny async REST client. Reuse across tool calls — open once at startup."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self, config: Config, *, transport: httpx.AsyncBaseTransport | None = None
|
|
32
|
+
) -> None:
|
|
33
|
+
self._config = config
|
|
34
|
+
self._http = httpx.AsyncClient(
|
|
35
|
+
base_url=config.api_base,
|
|
36
|
+
headers={
|
|
37
|
+
"Authorization": f"ApiKey {config.token}",
|
|
38
|
+
"User-Agent": config.user_agent,
|
|
39
|
+
"Accept": "application/json",
|
|
40
|
+
},
|
|
41
|
+
timeout=httpx.Timeout(15.0, connect=5.0),
|
|
42
|
+
transport=transport,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
async def aclose(self) -> None:
|
|
46
|
+
await self._http.aclose()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def config(self) -> Config:
|
|
50
|
+
return self._config
|
|
51
|
+
|
|
52
|
+
async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
53
|
+
r = await self._http.get(path, params=params)
|
|
54
|
+
return _unpack(r)
|
|
55
|
+
|
|
56
|
+
async def post(self, path: str, json: dict[str, Any]) -> Any:
|
|
57
|
+
r = await self._http.post(path, json=json)
|
|
58
|
+
return _unpack(r)
|
|
59
|
+
|
|
60
|
+
async def put(self, path: str, json: dict[str, Any]) -> Any:
|
|
61
|
+
r = await self._http.put(path, json=json)
|
|
62
|
+
return _unpack(r)
|
|
63
|
+
|
|
64
|
+
async def patch(self, path: str, json: dict[str, Any]) -> Any:
|
|
65
|
+
r = await self._http.patch(path, json=json)
|
|
66
|
+
return _unpack(r)
|
|
67
|
+
|
|
68
|
+
async def delete(self, path: str) -> Any:
|
|
69
|
+
r = await self._http.delete(path)
|
|
70
|
+
return _unpack(r)
|
|
71
|
+
|
|
72
|
+
async def health(self) -> dict[str, Any]:
|
|
73
|
+
return await self.get("/health")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _unpack(r: httpx.Response) -> Any:
|
|
77
|
+
if r.status_code >= 400:
|
|
78
|
+
try:
|
|
79
|
+
body: str | dict[str, Any] = r.json()
|
|
80
|
+
except ValueError:
|
|
81
|
+
body = r.text
|
|
82
|
+
raise VerbumiaApiError(r.status_code, body)
|
|
83
|
+
if r.status_code == 204 or not r.content:
|
|
84
|
+
return None
|
|
85
|
+
return r.json()
|