Metadata-Version: 2.4
Name: verbumia-mcp
Version: 0.20.0
Summary: Model Context Protocol server for Verbumia — list projects, missing keys, propose translations from Claude Desktop and other MCP clients.
Project-URL: Homepage, https://verbumia.ca
Project-URL: Documentation, https://verbumia.ca/docs/mcp/setup
Project-URL: Source, https://github.com/verbumia/verbumia-tools
Project-URL: Issues, https://github.com/verbumia/verbumia-tools/issues
Author: Verbumia
License: MIT
License-File: LICENSE
Keywords: claude,i18n,mcp,translations,verbumia
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Internationalization
Requires-Python: <3.14,>=3.12
Requires-Dist: httpx>=0.28
Requires-Dist: mcp>=1.0
Requires-Dist: pydantic>=2.9
Description-Content-Type: text/markdown

# Sonenta MCP server

Model Context Protocol server for [Sonenta](https://sonenta.com) — exposes
your translation project to Claude Desktop and other MCP clients.

## Status

Tools wired: `list_projects`, `get_project_info`, `list_keys`,
`list_untranslated_keys`, `list_missing_keys`, `missing_keys_stats`,
`acknowledge_missing_keys`, `create_namespace`, `create_key`,
`create_keys_bulk`, `update_key`, `update_keys_bulk`, `propose_translation`,
`propose_translations_bulk`, `publish_cdn`, `validate_translations`,
`project_context_get`, `project_context_set`, `glossary_list`, `glossary_create`,
`glossary_update`, `glossary_delete`, `health_report`, `coverage_report`,
`delete_keys_bulk`, `restore_keys_bulk`, `list_trash`, `a11y_estimate`,
`generate_a11y_variant`, `translate_a11y_variants`, `a11y_report`,
`list_a11y_gaps`, `set_a11y_variant`, `delete_a11y_variant`, `list_surfaces`,
`create_surface`, `update_surface`, `delete_surface`, `get_variants`,
`set_variant`, `delete_variant`, `list_cognitive_candidates`,
`set_cognitive_score`, `analyze_cognitive`.

### Project context document (V1.2)

`project_context_get` / `project_context_set` read and write a free-form
markdown blob attached to the project — terminology, brand voice, domain
notes (religious, legal, medical, gaming, etc.). The content is intended
as ambient context for human translators **and** for AI agents producing
translations: prepend it to your translation prompts so every output stays
consistent with the project's vocabulary and tone. Hard cap 100 KiB.

Scopes:
- `project_context_get` requires `project:read` (existing scope).
- `project_context_set` requires `project:settings:write` (new scope, narrower
  than `project:write` since settings changes propagate to every translator's
  prompt context).

Note: the blanket `mcp:*` scope is **not** sufficient for these tools — grant
the precise scope on the key.

### Glossary (V1.3)

CRUD over the project glossary — the terminology rules the backend applies when
translating. The single public `rule_type` vocabulary is `translation` |
`do_not_translate` | `forbidden` (the internal storage type is never exposed).

- `glossary_list` — filters `rule_type`, `q` (substring on term), `limit`
  (1..500, default 200) → `{ items, total }`.
- `glossary_create` — `{ rule_type?, term, translations?, case_sensitive?, note? }`;
  `rule_type` defaults to `translation`; `translations` is honored only for
  `translation`. 409 on a duplicate `(rule_type, term)`.
- `glossary_update` — by `entry_id`; send only the fields to change (`term`,
  `translations`, `case_sensitive`, `note`). `rule_type` is immutable.
- `glossary_delete` — by `entry_id`.

These use the dedicated MCP surface `/v1/mcp/projects/{id}/glossary` and need the
`mcp:*` scope (the dashboard glossary endpoints are project-role only).

**Per-string glossary hints.** `list_untranslated_keys` and `list_keys` accept
`include_glossary_hints: true` (default false). When set, each returned key
carries a `glossary_hints[]` array — `{ term, rule_type, target_translation,
matched_text }` — for the rules the backend matched against the key's source
string. All matching is backend-side; the MCP forwards the flag and surfaces the
hints verbatim.

### Quality reports + trash (V1.4)

Read-only inspection (surfaced verbatim):
- `health_report` — project source-health report (source-string issues, with
  `by_issue` / `by_severity` rollups). Optional `version_id`.
- `coverage_report` — translation-coverage report (global completeness +
  per-language). Optional `version_id`.

Trash — soft-delete, restorable; **no purge over MCP** (hard-delete is
human-only via the dashboard):
- `delete_keys_bulk` — move up to 500 keys to the trash by `key_uuid` (idempotent).
- `restore_keys_bulk` — restore trashed keys by `key_uuid`.
- `list_trash` — list trashed keys (`{key_uuid, namespace, name, deleted_at}`,
  cursor-paginated).

The trash tools are **uuid-addressed**: get `key_uuid` from `list_keys` (active)
or `list_trash` (trashed), then pass it to delete/restore. A trashed key
disappears from `list_keys`, `list_untranslated_keys`, the CDN, and the reports
until it is restored.

### Edit a key's source value (V1.5)

`update_key` / `update_keys_bulk` edit a key's **source-language value in place**
(`PATCH`), preserving the key and its history (version bump + history revision +
audit). Address the key by **exactly one** of `key_uuid` (preferred — from
`list_keys`) **or** `namespace` + `name`; an optional `description` can be updated
in the same call.

- When the `source_value` **actually changes**, the key's `reviewed` / `approved`
  target translations are demoted to needs-review (`translated`, the "à revoir"
  state); `missing` / `draft` / `translated` targets are left untouched.
  `stale_flagged` reports how many were demoted.
- Submitting the current value is a **no-op** (`result: "unchanged"`, not billed);
  a real source change — or a `description`-only change — bills 1 unit per key.
- `update_keys_bulk` takes up to 500 items and returns a per-item `results` array
  (`status`: `ok` | `unchanged` | `error`) plus a `summary` roll-up; valid items
  commit even if siblings fail.

### Key semantic type (a11y key.type)

`create_key` / `update_key` (and their bulk variants) accept a `type` — one of 17
semantic types (`text` default, plus `heading`, `label`, `caption`, `badge`,
`tooltip`, `error_message`, `toast`, `button`, `link`, `menu_item`,
`input_placeholder`, `input_label`, `image`, `icon`, `meta_title`,
`meta_description`). The type decides which a11y treatments a key offers — e.g. an
`image` key's base value **is** its alt text, an `icon`'s base **is** its
accessible name, `button`/`link` offer `aria_label`, and `heading`/`label` offer
`plain_language` / `screen_reader`. `update_key` can change the type alone (no
`source_value` needed) to reclassify a key.

The engine gates everything by type server-side, so the a11y and variant tools are
**type-aware** automatically: `a11y_report` only flags treatments relevant to each
key's type (no aria/alt on a text key), `get_variants` lists only a key's offered
surfaces, and `set_variant` returns `422` for a surface the key's type doesn't
offer.

### Accessibility (a11y) variants (a11y V1)

Semantic accessibility variants that extend the variant engine with four
surfaces — `aria_label`, `alt_text`, `screen_reader`, `plain_language` —
orthogonal to the visible text. All are thin HTTP wrappers surfaced verbatim.

Discover gaps (read-only WCAG report):
- `a11y_report` — per key/surface/locale a11y gaps with `by_gap` / `by_severity` /
  `by_surface` rollups + per-item details. Gap types: `a11y_untranslated`,
  `alt_missing`, `reading_level_high`, `a11y_variant_absent`. Optional
  `version_id`, `require_surface[]` (flag surfaces absent in the source), and
  `reading_level_max_words` (5..60, default 15).
- `list_a11y_gaps` — the actionable `items[]` of `a11y_report`, filtered
  client-side by `gap` / `surface` / `locale`. To find keys with no a11y variant,
  pass `require_surface=<surface>` and filter `gap=a11y_variant_absent`.

AI fill (billed in credits — call `a11y_estimate` first):
- `a11y_estimate` — preview `{units, credit_cost_per_unit, credits_required,
  balance, sufficient}` for a `generate` or `translate` run (tier `standard`=1 /
  `premium`=4 per unit).
- `generate_a11y_variant` — AI-generate **source-language** a11y variants for keys
  that lack them (aria/screen_reader/plain_language from the visible text,
  alt_text from the key context). Draft, bot-authored.
- `translate_a11y_variants` — translate existing source a11y overrides into the
  requested `language_codes`.

Manual CRUD (one cell at a time):
- `set_a11y_variant` — upsert one override for `(key_uuid, language_code,
  surface)` with a text `value`. Address by the locale **code** an `a11y_report`
  gap already carries — no UUID resolution. Stored as a draft (bot) override.
- `delete_a11y_variant` — clear one override; the cell re-inherits the base value.

All a11y tools are reachable with the same `mcp:*` key as the rest of the MCP
surface — no extra scope. `estimate` / `generate` / `translate` hit
`/v1/mcp/projects/{id}/a11y/*`; the report hits
`/v1/mcp/projects/{id}/a11y-report`; set/delete use the generic code-addressed
variant route below.

### Configurable surfaces + generic variants (task 928)

Surfaces are **per-project**: device surfaces (`desktop`/`mobile`/`tablet` +
custom like `watch`/`tv`) are extensible; the four a11y surfaces are a fixed
semantic set (toggle-active only). An un-customized project behaves exactly as
before (all builtins active). Manage them with:

- `list_surfaces` — `{surfaces:[{slug, label, kind, builtin, active}]}` (`kind` ∈
  `device`/`a11y`).
- `create_surface` — add a **custom device** surface (`{slug, label?}`); creating
  an a11y slug is rejected.
- `update_surface` — rename and/or toggle `active`. Deactivating is **soft**
  (variants retained but hidden; the response `affected_variants` reports the
  count; reactivating republishes them).
- `delete_surface` — delete a custom surface (builtins can only be deactivated).

Authoring variants works for **any active surface** (device or a11y), addressed
by the locale **code** + surface slug a report/matrix already gives you:

- `get_variants` — the full matrix for one key: every active surface × language,
  with each cell's `{override, value, resolvedValue, status, drift}` plus
  `surfaces` + `surfaceKinds`. (`GET .../keys/{key_uuid}/variants`)
- `set_variant` — upsert one override `(key_uuid, language_code, surface, value)`;
  `422` if the surface is not active, `409` if the cell is already
  reviewed/approved (never overwritten). Stored as a draft.
- `delete_variant` — clear one override (cell re-inherits the base value).

Variant writes (`set_variant`/`delete_variant`, and the a11y-named
`set_a11y_variant`/`delete_a11y_variant`) all use the mcp:* code-addressed route
`/v1/mcp/projects/{id}/keys/{key_uuid}/variants/{language_code}/{surface}`.

### Plain-language by cognitive score

Replaces the a-priori reading-level heuristic with an actual cognitive-difficulty
score (0–100, higher = harder). A key only enters the "review" queue
(`reading_level_high`) once it's been scored at or above the project threshold —
so long-but-simple strings no longer clutter the queue. Same local-first model as
the rest of the a11y surface:

- `list_cognitive_candidates` — text-relevant keys (a type that offers
  `plain_language`: text/heading/label/caption) whose source clears a word floor;
  a scope filter, not a difficulty judgement. `only_unanalyzed`, `namespace_uuid`,
  `limit`, `offset`.
- `set_cognitive_score` — the **0-credit** local path: record a `score` (0–100)
  you judged yourself plus an optional plain-language `suggestion` (draft,
  bot-authored).
- `analyze_cognitive` — the **billed** server-side AI fallback (estimate/opt-in;
  `tier`, `key_uuids?`, `namespace_uuid?`, `idempotency_key`) for bulk runs.

## Install

```bash
# Recommended (zero-install, used by Claude Desktop's mcp.json):
npx -y @sonenta/mcp

# Or from PyPI:
pipx install verbumia-mcp
```

A Homebrew tap (`brew install sonenta/tap/verbumia-mcp`) is coming once we
publish to npm.

## Configure (Claude Desktop)

Open **Settings → Developer → Edit Config**, then:

Single-project setup:

```json
{
  "mcpServers": {
    "sonenta": {
      "command": "npx",
      "args": ["-y", "@sonenta/mcp"],
      "env": {
        "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
        "SONENTA_PROJECTS": "<project_uuid>",
        "SONENTA_BASE_URL": "https://api.sonenta.com"
      }
    }
  }
}
```

Multi-project setup (v0.11+) — comma-separate the UUIDs and Claude will pass
`project_uuid` on each tool call:

```json
"env": {
  "SONENTA_API_KEY": "vrb_live_<prefix>.<secret>",
  "SONENTA_PROJECTS": "01993a..,01993b..,01993c.."
}
```

Restart Claude Desktop. Tools show up in the prompt UI.

## Environment

| Variable             | Required | Default                        | Description                                                                                |
|----------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------|
| `SONENTA_API_KEY`   | yes      |                                | API key from Org Settings → API Keys (`SONENTA_TOKEN` also accepted, back-compat)         |
| `SONENTA_PROJECTS`  | no       | (LLM passes per call)          | CSV of project UUIDs. When >1, the LLM MUST pass `project_uuid` on each tool call.         |
| `SONENTA_PROJECT`   | no       | (legacy)                       | Singular fallback for v0.10.x users. Ignored when `SONENTA_PROJECTS` is set (warns).      |
| `SONENTA_BASE_URL`  | no       | `https://api.sonenta.com`      | Override for self-host or staging (`SONENTA_API_BASE` also accepted, back-compat)         |

The token format is `vrb_live_<prefix>.<secret>` and must carry the `mcp:*`
scope. For the `project_context_*` tools, also grant `project:read` and/or
`project:settings:write` — see the **Project context document** section
above. Treat the token like any other secret — keychain or `.env`, never
commit.

If your API key is pinned to a single project (its `project_uuid` is set
server-side), listing additional UUIDs in `SONENTA_PROJECTS` will surface
as a `403 API key is scoped to a different project` from the backend on
any call targeting one of the others.

## Local development

```bash
cd mcp
uv sync
uv run verbumia-mcp        # stdio transport — pipe MCP frames over stdin/stdout
```

## License

MIT.
