@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 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
+ }
@@ -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.
@@ -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,3 @@
1
+ """Verbumia MCP server (MIT)."""
2
+
3
+ __version__ = "0.16.0"
@@ -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()