@sonenta/mcp 0.34.1 → 0.35.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/package.json CHANGED
@@ -1,40 +1,60 @@
1
1
  {
2
2
  "name": "@sonenta/mcp",
3
- "version": "0.34.1",
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.",
3
+ "version": "0.35.0",
4
+ "description": "MCP server for Sonenta translation management pure TypeScript, no Python. Wires Claude Desktop and other MCP clients into your project's keys, translations, accessibility, and source-health tools.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sonenta.com",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/sonenta/tools.git",
10
- "directory": "mcp/npm"
9
+ "url": "https://github.com/verbumia/verbumia-tools.git",
10
+ "directory": "mcp-ts"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/sonenta/tools/issues"
13
+ "url": "https://github.com/verbumia/verbumia-tools/issues"
14
14
  },
15
15
  "keywords": [
16
- "mcp",
17
16
  "sonenta",
17
+ "verbumia",
18
+ "mcp",
18
19
  "i18n",
19
20
  "translations",
20
- "claude",
21
- "anthropic"
21
+ "claude"
22
22
  ],
23
23
  "author": "Sonenta",
24
- "engines": {
25
- "node": ">=18.0.0"
26
- },
24
+ "type": "module",
27
25
  "bin": {
28
26
  "sonenta-mcp": "bin/sonenta-mcp.js"
29
27
  },
30
28
  "files": [
31
29
  "bin/",
32
- "python/",
30
+ "dist/",
33
31
  "README.md",
34
32
  "LICENSE"
35
33
  ],
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
36
37
  "scripts": {
37
- "test": "node bin/sonenta-mcp.js --self-check",
38
- "prepublishOnly": "cd python && uv build && rm -f dist/*.tar.gz && printf '' > dist/.npmignore"
38
+ "build": "tsup",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit",
41
+ "dev": "tsx src/index.ts",
42
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build",
43
+ "gen:schemas": "node scripts/gen-schemas.mjs"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.0.0",
47
+ "lossless-json": "^4.3.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "tsup": "^8.3.0",
52
+ "tsx": "^4.19.0",
53
+ "typescript": "^5.5.0",
54
+ "vitest": "^2.1.0"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "registry": "https://registry.npmjs.org/"
39
59
  }
40
60
  }
package/python/README.md DELETED
@@ -1,114 +0,0 @@
1
- # Sonenta MCP server
2
-
3
- Model Context Protocol server for [Sonenta](https://sonenta.com) — 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 @sonenta/mcp
37
-
38
- # Or from PyPI:
39
- pipx install sonenta-mcp
40
- ```
41
-
42
- A Homebrew tap (`brew install sonenta/tap/sonenta-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
- "sonenta": {
55
- "command": "npx",
56
- "args": ["-y", "@sonenta/mcp"],
57
- "env": {
58
- "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
59
- "SONENTA_PROJECTS": "<project_uuid>",
60
- "SONENTA_BASE_URL": "https://api.sonenta.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
- "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
73
- "SONENTA_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
- | `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
- | `SONENTA_PROJECTS` | no | (LLM passes per call) | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` on each tool call. |
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.dev` | Override for self-host or staging (`SONENTA_API_BASE` also accepted, back-compat) |
87
-
88
- > Migrating from `verbumia-mcp`? The legacy `VERBUMIA_API_KEY` / `VERBUMIA_TOKEN`
89
- > / `VERBUMIA_PROJECTS` / `VERBUMIA_BASE_URL` env vars are still accepted, so
90
- > existing MCP client configs keep working — switch to the `SONENTA_*` names
91
- > when convenient.
92
-
93
- The token format is `vrb_live_<prefix>.<secret>` and must carry the `mcp:*`
94
- scope. For the `project_context_*` tools, also grant `project:read` and/or
95
- `project:settings:write` — see the **Project context document** section
96
- above. Treat the token like any other secret — keychain or `.env`, never
97
- commit.
98
-
99
- If your API key is pinned to a single project (its `project_uuid` is set
100
- server-side), listing additional UUIDs in `SONENTA_PROJECTS` will surface
101
- as a `403 API key is scoped to a different project` from the backend on
102
- any call targeting one of the others.
103
-
104
- ## Local development
105
-
106
- ```bash
107
- cd mcp
108
- uv sync
109
- uv run sonenta-mcp # stdio transport — pipe MCP frames over stdin/stdout
110
- ```
111
-
112
- ## License
113
-
114
- MIT.
@@ -1,59 +0,0 @@
1
- [project]
2
- name = "sonenta-mcp"
3
- version = "0.34.1"
4
- description = "Model Context Protocol server for Sonenta — list projects, missing keys, propose translations from Claude Desktop and other MCP clients."
5
- readme = "README.md"
6
- requires-python = ">=3.11,<3.14"
7
- license = { text = "MIT" }
8
- authors = [{ name = "Sonenta" }]
9
- keywords = ["mcp", "sonenta", "i18n", "translations", "claude"]
10
- classifiers = [
11
- "License :: OSI Approved :: MIT License",
12
- "Programming Language :: Python :: 3.11",
13
- "Programming Language :: Python :: 3.12",
14
- "Topic :: Software Development :: Internationalization",
15
- ]
16
- dependencies = [
17
- "mcp>=1.0",
18
- "httpx>=0.28",
19
- "pydantic>=2.9",
20
- ]
21
-
22
- [project.scripts]
23
- sonenta-mcp = "verbumia_mcp.__main__:main"
24
-
25
- [project.urls]
26
- Homepage = "https://sonenta.com"
27
- Documentation = "https://sonenta.com/docs/mcp/setup"
28
- Source = "https://github.com/sonenta/tools"
29
- Issues = "https://github.com/sonenta/tools/issues"
30
-
31
- [dependency-groups]
32
- dev = [
33
- "pytest>=8.3",
34
- "pytest-asyncio>=0.24",
35
- "ruff>=0.8",
36
- ]
37
-
38
- [tool.uv]
39
- package = true
40
-
41
- [tool.ruff]
42
- line-length = 100
43
- target-version = "py312"
44
-
45
- [tool.ruff.lint]
46
- select = ["E", "F", "W", "I", "B", "UP", "SIM", "ASYNC", "RUF"]
47
- ignore = ["E501"]
48
-
49
- [build-system]
50
- requires = ["hatchling"]
51
- build-backend = "hatchling.build"
52
-
53
- [tool.hatch.build.targets.wheel]
54
- packages = ["verbumia_mcp"]
55
-
56
- [tool.pytest.ini_options]
57
- asyncio_mode = "auto"
58
- testpaths = ["tests"]
59
- addopts = "-q --strict-markers"
@@ -1,3 +0,0 @@
1
- """Sonenta MCP server (MIT)."""
2
-
3
- __version__ = "0.34.1"
@@ -1,23 +0,0 @@
1
- """Entrypoint for `sonenta-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"sonenta-mcp: {exc}", file=sys.stderr)
19
- sys.exit(2)
20
-
21
-
22
- if __name__ == "__main__":
23
- main()
@@ -1,85 +0,0 @@
1
- """Async HTTP client wrapping the Sonenta 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"Sonenta 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()
@@ -1,157 +0,0 @@
1
- """Runtime configuration loaded from environment variables.
2
-
3
- The MCP server is invoked by clients like Claude Desktop, which pass `env`
4
- entries through to the spawned process. Env vars are the primary contract.
5
-
6
- Canonical env vars are SONENTA_*. The legacy VERBUMIA_* names are still
7
- accepted (deprecate-not-remove) so existing MCP client configs keep working;
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.
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import json
20
- import os
21
- import sys
22
- from dataclasses import dataclass
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 ""
67
-
68
-
69
- @dataclass(frozen=True)
70
- class Config:
71
- api_base: str
72
- token: str
73
- # Tuple of project UUIDs the operator scoped this MCP server to. Empty
74
- # means "unrestricted — fall back to whatever the API key can see".
75
- allowed_project_uuids: tuple[str, ...]
76
- user_agent: str
77
-
78
- @property
79
- def default_project_uuid(self) -> str | None:
80
- """The single project to use when none is passed explicitly.
81
-
82
- Only defined when exactly one project is configured — otherwise the
83
- caller MUST pass `project_uuid` per call (the resolver in tools.py
84
- enforces this).
85
- """
86
- return self.allowed_project_uuids[0] if len(self.allowed_project_uuids) == 1 else None
87
-
88
-
89
- def _parse_projects_csv(raw: str) -> tuple[str, ...]:
90
- """Split, strip, and de-duplicate a CSV of project UUIDs while preserving order."""
91
- seen: list[str] = []
92
- for token in raw.split(","):
93
- v = token.strip()
94
- if v and v not in seen:
95
- seen.append(v)
96
- return tuple(seen)
97
-
98
-
99
- def _env(*names: str) -> str:
100
- """First non-empty env var among `names` (canonical SONENTA_* first,
101
- legacy VERBUMIA_* fallback)."""
102
- for n in names:
103
- v = os.environ.get(n, "").strip()
104
- if v:
105
- return v
106
- return ""
107
-
108
-
109
- def load_config() -> Config:
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.
113
- api_base = (
114
- _env("SONENTA_BASE_URL", "SONENTA_API_BASE", "VERBUMIA_BASE_URL", "VERBUMIA_API_BASE")
115
- or DEFAULT_API_BASE
116
- ).rstrip("/")
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
-
132
- # Multi-project scoping. Canonical: SONENTA_PROJECTS (CSV) / SONENTA_PROJECT
133
- # (singular). Legacy VERBUMIA_PROJECTS / VERBUMIA_PROJECT still accepted.
134
- projects_csv = _env("SONENTA_PROJECTS", "VERBUMIA_PROJECTS")
135
- project_single = _env("SONENTA_PROJECT", "VERBUMIA_PROJECT")
136
- if projects_csv and project_single:
137
- print(
138
- "sonenta-mcp: both *_PROJECTS and *_PROJECT are set; the CSV "
139
- "(*_PROJECTS) wins. Remove the singular var to silence this warning.",
140
- file=sys.stderr,
141
- )
142
- if projects_csv:
143
- allowed = _parse_projects_csv(projects_csv)
144
- elif project_single:
145
- allowed = (project_single,)
146
- else:
147
- allowed = ()
148
-
149
- from . import __version__
150
-
151
- user_agent = f"sonenta-mcp/{__version__}"
152
- return Config(
153
- api_base=api_base,
154
- token=token,
155
- allowed_project_uuids=allowed,
156
- user_agent=user_agent,
157
- )
@@ -1,88 +0,0 @@
1
- """MCP stdio server bootstrapping.
2
-
3
- `run()` is the async entrypoint: load config, open the HTTP client, register
4
- tools, then hand control to mcp's stdio transport. The MCP protocol uses
5
- JSON-RPC over stdin/stdout, so we never print to stdout for logging — all
6
- diagnostics go to stderr.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import sys
12
- from collections.abc import AsyncIterator
13
- from contextlib import asynccontextmanager
14
-
15
- from mcp.server import Server
16
- from mcp.server.models import InitializationOptions
17
- from mcp.server.stdio import stdio_server
18
- from mcp.types import ServerCapabilities, ToolsCapability
19
-
20
- from . import __version__
21
- from .client import VerbumiaApiError, VerbumiaClient
22
- from .config import Config, load_config
23
- from .tools import register
24
-
25
-
26
- def _info(msg: str) -> None:
27
- """Stderr-only logging — stdout is reserved for the MCP framing."""
28
- print(msg, file=sys.stderr, flush=True)
29
-
30
-
31
- @asynccontextmanager
32
- async def _client_for(config: Config) -> AsyncIterator[VerbumiaClient]:
33
- client = VerbumiaClient(config)
34
- try:
35
- yield client
36
- finally:
37
- await client.aclose()
38
-
39
-
40
- async def _preflight(client: VerbumiaClient) -> None:
41
- """Verify reachability + auth before starting the JSON-RPC loop.
42
-
43
- Hits `/v1/mcp/projects?limit=1` so we exercise both the API key (auth)
44
- and the mcp:* scope. Failures are turned into stderr-friendly messages
45
- so the MCP host (Claude Desktop / etc.) surfaces a real cause.
46
- """
47
- try:
48
- await client.get("/v1/mcp/projects", params={"limit": 1})
49
- _info(f"sonenta-mcp: connected to {client.config.api_base}")
50
- except VerbumiaApiError as exc:
51
- if exc.status == 401:
52
- raise RuntimeError(
53
- "SONENTA_API_KEY was rejected (401). Verify the key in Sonenta → "
54
- "Org Settings → API Keys; new keys are shown once on creation."
55
- ) from exc
56
- if exc.status == 403:
57
- raise RuntimeError(
58
- "API key lacks the `mcp:*` scope (403). Issue a key with mcp:* in "
59
- "Sonenta → Org Settings → API Keys."
60
- ) from exc
61
- raise RuntimeError(
62
- f"Sonenta API rejected the connectivity probe ({exc.status}): {exc.body}"
63
- ) from exc
64
- except Exception as exc:
65
- raise RuntimeError(
66
- f"Could not reach {client.config.api_base}: {exc}. "
67
- f"Set SONENTA_BASE_URL (or legacy VERBUMIA_*) for self-host or staging."
68
- ) from exc
69
-
70
-
71
- async def run() -> None:
72
- config = load_config()
73
- _info(f"sonenta-mcp {__version__} starting (api={config.api_base})")
74
- server: Server = Server("sonenta")
75
-
76
- async with _client_for(config) as client:
77
- await _preflight(client)
78
- register(server, client)
79
- async with stdio_server() as (read_stream, write_stream):
80
- await server.run(
81
- read_stream,
82
- write_stream,
83
- InitializationOptions(
84
- server_name="sonenta",
85
- server_version=__version__,
86
- capabilities=ServerCapabilities(tools=ToolsCapability(listChanged=False)),
87
- ),
88
- )