@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
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
"""V1 MCP tool registrations.
|
|
2
|
+
|
|
3
|
+
Tools (V1 MCP surface; glossary_* added 2026-05-24; reports + trash added 2026-05-25):
|
|
4
|
+
- list_projects — list projects accessible to the API key
|
|
5
|
+
- get_project_info — project metadata: source_language, languages, namespaces
|
|
6
|
+
- list_keys — keys + source value + per-language translations (bulk-translate)
|
|
7
|
+
- list_untranslated_keys — keys WITHOUT a valid translation in a target language
|
|
8
|
+
- list_missing_keys — pending missing-key events (cursor paginated)
|
|
9
|
+
- missing_keys_stats — count-only summary of missing events (drives pagination)
|
|
10
|
+
- acknowledge_missing_keys— batch-resolve missing events (cleanup the dashboard)
|
|
11
|
+
- create_namespace — idempotent namespace creation (slug validated, slug_invalid on bad input)
|
|
12
|
+
- create_key — idempotent key creation, optionally seeds source value
|
|
13
|
+
- create_keys_bulk — batch create_key (up to 500/call, per-item results)
|
|
14
|
+
- update_key — edit a key's SOURCE value in place (PATCH); demotes reviewed/approved targets to needs-review
|
|
15
|
+
- update_keys_bulk — batch update_key (up to 500/call, per-item results)
|
|
16
|
+
- propose_translation — submit a translation value (auto-creates the key if missing)
|
|
17
|
+
- propose_translations_bulk— batch propose_translation (up to 500/call, per-item results)
|
|
18
|
+
- publish_cdn — trigger a CDN release (closes the dogfood loop)
|
|
19
|
+
- validate_translations — lint a JSON i18next blob server-side
|
|
20
|
+
- project_context_get — read the project's markdown context document
|
|
21
|
+
- project_context_set — upsert the project's markdown context document
|
|
22
|
+
- glossary_list — list glossary entries (filters rule_type/q/limit)
|
|
23
|
+
- glossary_create — create a glossary entry (409 on duplicate rule_type+term)
|
|
24
|
+
- glossary_update — update an entry by uuid (term/translations/case_sensitive/note)
|
|
25
|
+
- glossary_delete — delete an entry by uuid
|
|
26
|
+
- health_report — read-only project source-health report (#752)
|
|
27
|
+
- coverage_report — read-only project translation-coverage report (#752)
|
|
28
|
+
- delete_keys_bulk — soft-delete (trash) keys by key_uuid; restorable
|
|
29
|
+
- restore_keys_bulk — restore trashed keys by key_uuid
|
|
30
|
+
- list_trash — list trashed (soft-deleted) keys (cursor paginated)
|
|
31
|
+
|
|
32
|
+
list_keys and list_untranslated_keys accept `include_glossary_hints=true` to
|
|
33
|
+
attach backend-matched `glossary_hints[]` per key. All glossary matching is
|
|
34
|
+
backend-side; these tools carry ZERO glossary logic client-side.
|
|
35
|
+
|
|
36
|
+
Trash is UUID-addressed (key_uuids[]) — get key_uuid from list_keys (active) or
|
|
37
|
+
list_trash (trashed). No hard-delete/purge over MCP (human-only JWT). reports +
|
|
38
|
+
trash tools are thin HTTP wrappers, surfaced verbatim.
|
|
39
|
+
|
|
40
|
+
The tool surface mirrors the canonical contract published in
|
|
41
|
+
`docs/mcp/setup`. Argument names use the LLM-friendly `key` / `namespace` /
|
|
42
|
+
`language_code` rather than uuids — the backend resolves uuids server-side.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import json
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from mcp.server import Server
|
|
51
|
+
from mcp.types import TextContent, Tool
|
|
52
|
+
|
|
53
|
+
from .client import VerbumiaApiError, VerbumiaClient
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _project_uuid(arguments: dict | None, client: VerbumiaClient) -> str:
|
|
57
|
+
"""Resolve project_uuid from per-call args or the configured env scope.
|
|
58
|
+
|
|
59
|
+
Rules (multi-project aware, 0.11+):
|
|
60
|
+
1. Explicit `project_uuid` in args wins. If `SONENTA_PROJECTS` is set
|
|
61
|
+
and the value isn't in that allowlist, raise early — better UX than
|
|
62
|
+
a backend 404/403 after a round-trip.
|
|
63
|
+
2. No explicit arg + exactly one configured project → use that one.
|
|
64
|
+
3. No explicit arg + multiple configured projects → raise asking the
|
|
65
|
+
caller to pass `project_uuid` and listing the allowed UUIDs.
|
|
66
|
+
4. No explicit arg + nothing configured → raise asking the caller to
|
|
67
|
+
pass `project_uuid` or set SONENTA_PROJECTS / SONENTA_PROJECT.
|
|
68
|
+
"""
|
|
69
|
+
allowed = client.config.allowed_project_uuids
|
|
70
|
+
if arguments and arguments.get("project_uuid"):
|
|
71
|
+
requested = str(arguments["project_uuid"])
|
|
72
|
+
if allowed and requested not in allowed:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"project_uuid {requested!r} is not in SONENTA_PROJECTS allowlist "
|
|
75
|
+
f"({', '.join(allowed)})"
|
|
76
|
+
)
|
|
77
|
+
return requested
|
|
78
|
+
if client.config.default_project_uuid:
|
|
79
|
+
return client.config.default_project_uuid
|
|
80
|
+
if allowed:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"project_uuid is required when multiple projects are configured "
|
|
83
|
+
"via SONENTA_PROJECTS (or the legacy SONENTA_PROJECT singular). "
|
|
84
|
+
f"Pass one of: {', '.join(allowed)}"
|
|
85
|
+
)
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"project_uuid is required (pass it explicitly or set SONENTA_PROJECTS / SONENTA_PROJECT)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _filter_projects_by_allowlist(data: Any, allowed: tuple[str, ...]) -> Any:
|
|
92
|
+
"""Restrict a list_projects response to the SONENTA_PROJECTS allowlist so
|
|
93
|
+
`see == touch`: the backend returns everything the API key can reach, but
|
|
94
|
+
when the human has set an allowlist we only surface those projects — the
|
|
95
|
+
same scope `_project_uuid` enforces for get_project_info / writes. No
|
|
96
|
+
allowlist set → return `data` unchanged.
|
|
97
|
+
"""
|
|
98
|
+
if allowed and isinstance(data, dict) and isinstance(data.get("items"), list):
|
|
99
|
+
return {**data, "items": [p for p in data["items"] if p.get("uuid") in allowed]}
|
|
100
|
+
return data
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _text(payload: Any) -> list[TextContent]:
|
|
104
|
+
return [TextContent(type="text", text=json.dumps(payload, indent=2, default=str))]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _err(msg: str) -> list[TextContent]:
|
|
108
|
+
return [TextContent(type="text", text=f"error: {msg}")]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def register(server: Server, client: VerbumiaClient) -> tuple[Any, Any]:
|
|
112
|
+
@server.list_tools()
|
|
113
|
+
async def _list_tools() -> list[Tool]:
|
|
114
|
+
return [
|
|
115
|
+
Tool(
|
|
116
|
+
name="list_projects",
|
|
117
|
+
description=(
|
|
118
|
+
"List projects accessible to the configured Verbumia API key. "
|
|
119
|
+
"Project-scoped keys return a single project; org-scoped keys "
|
|
120
|
+
"return all projects in the org."
|
|
121
|
+
),
|
|
122
|
+
inputSchema={
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 50}
|
|
126
|
+
},
|
|
127
|
+
"additionalProperties": False,
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
Tool(
|
|
131
|
+
name="get_project_info",
|
|
132
|
+
description=(
|
|
133
|
+
"Fetch a project's metadata: source language, all configured "
|
|
134
|
+
"languages, namespaces, key count. Use before list_missing_keys "
|
|
135
|
+
"or propose_translation to learn what namespace + language codes "
|
|
136
|
+
"are valid."
|
|
137
|
+
),
|
|
138
|
+
inputSchema={
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"project_uuid": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"additionalProperties": False,
|
|
147
|
+
},
|
|
148
|
+
),
|
|
149
|
+
Tool(
|
|
150
|
+
name="list_keys",
|
|
151
|
+
description=(
|
|
152
|
+
"List translation keys with their source-language value and "
|
|
153
|
+
"per-language translations. Designed for bulk translation: "
|
|
154
|
+
"Claude can scan the result and propose translations for "
|
|
155
|
+
"every key that's still missing in a target language. "
|
|
156
|
+
"Filters: namespace slug, status_filter (only keys with at "
|
|
157
|
+
"least one translation in that status), language_code "
|
|
158
|
+
"(restrict translations[] to a single language)."
|
|
159
|
+
),
|
|
160
|
+
inputSchema={
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"project_uuid": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
166
|
+
},
|
|
167
|
+
"namespace": {"type": "string", "description": "Namespace slug filter"},
|
|
168
|
+
"language_code": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"description": "Restrict translations[] to one BCP-47 code (e.g. fr).",
|
|
171
|
+
},
|
|
172
|
+
"status_filter": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"enum": ["missing", "draft", "translated", "reviewed", "approved"],
|
|
175
|
+
"description": "Include only keys with at least one translation in this status.",
|
|
176
|
+
},
|
|
177
|
+
"include_glossary_hints": {
|
|
178
|
+
"type": "boolean",
|
|
179
|
+
"default": False,
|
|
180
|
+
"description": "When true, attach matched glossary_hints[] to each key (backend-matched against the source string). Default false.",
|
|
181
|
+
},
|
|
182
|
+
"cursor": {"type": "string"},
|
|
183
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 50},
|
|
184
|
+
},
|
|
185
|
+
"additionalProperties": False,
|
|
186
|
+
},
|
|
187
|
+
),
|
|
188
|
+
Tool(
|
|
189
|
+
name="list_untranslated_keys",
|
|
190
|
+
description=(
|
|
191
|
+
"List keys that DON'T yet have a valid translation in the "
|
|
192
|
+
"target language. A key is untranslated when ANY of: no "
|
|
193
|
+
"translation row exists, value is empty, or status is "
|
|
194
|
+
"missing. Compact response (no translations[]) so Claude "
|
|
195
|
+
"can iterate a long list and propose translations from "
|
|
196
|
+
"source_value."
|
|
197
|
+
),
|
|
198
|
+
inputSchema={
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {
|
|
201
|
+
"project_uuid": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
204
|
+
},
|
|
205
|
+
"language_code": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"description": "Required: target BCP-47 language code (e.g. fr).",
|
|
208
|
+
},
|
|
209
|
+
"namespace": {"type": "string", "description": "Namespace slug filter"},
|
|
210
|
+
"include_glossary_hints": {
|
|
211
|
+
"type": "boolean",
|
|
212
|
+
"default": False,
|
|
213
|
+
"description": "When true, attach matched glossary_hints[] to each key (term, rule_type, target_translation, matched_text) — backend-matched against the source string. Default false.",
|
|
214
|
+
},
|
|
215
|
+
"cursor": {"type": "string"},
|
|
216
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 50},
|
|
217
|
+
},
|
|
218
|
+
"required": ["language_code"],
|
|
219
|
+
"additionalProperties": False,
|
|
220
|
+
},
|
|
221
|
+
),
|
|
222
|
+
Tool(
|
|
223
|
+
name="list_missing_keys",
|
|
224
|
+
description=(
|
|
225
|
+
"List pending missing-key events captured from the SDK runtime. "
|
|
226
|
+
"Cursor paginated (default page = 50). Filters: namespace slug, "
|
|
227
|
+
"language_code, status. CALL missing_keys_stats FIRST to learn how "
|
|
228
|
+
"many events exist so you know whether to keep paginating."
|
|
229
|
+
),
|
|
230
|
+
inputSchema={
|
|
231
|
+
"type": "object",
|
|
232
|
+
"properties": {
|
|
233
|
+
"project_uuid": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
236
|
+
},
|
|
237
|
+
"namespace": {"type": "string"},
|
|
238
|
+
"language_code": {"type": "string"},
|
|
239
|
+
"status": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"enum": ["open", "acknowledged", "resolved"],
|
|
242
|
+
},
|
|
243
|
+
"cursor": {"type": "string"},
|
|
244
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 50},
|
|
245
|
+
},
|
|
246
|
+
"additionalProperties": False,
|
|
247
|
+
},
|
|
248
|
+
),
|
|
249
|
+
Tool(
|
|
250
|
+
name="missing_keys_stats",
|
|
251
|
+
description=(
|
|
252
|
+
"Count-only summary of missing-key events for a project. Returns "
|
|
253
|
+
"{total, by_status, by_language, by_namespace}. Lightweight (no row "
|
|
254
|
+
"payloads). Use this BEFORE list_missing_keys so you know how many "
|
|
255
|
+
"pages remain and whether to keep paginating. Filters mirror "
|
|
256
|
+
"list_missing_keys; status defaults to `open` (pass `all` to count "
|
|
257
|
+
"every status)."
|
|
258
|
+
),
|
|
259
|
+
inputSchema={
|
|
260
|
+
"type": "object",
|
|
261
|
+
"properties": {
|
|
262
|
+
"project_uuid": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
265
|
+
},
|
|
266
|
+
"namespace": {"type": "string"},
|
|
267
|
+
"language_code": {"type": "string"},
|
|
268
|
+
"status": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"description": "Defaults to `open`. Pass `all` to count every status.",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
"additionalProperties": False,
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
Tool(
|
|
277
|
+
name="acknowledge_missing_keys",
|
|
278
|
+
description=(
|
|
279
|
+
"Batch-resolve a set of missing-event uuids. Use this to clean up "
|
|
280
|
+
"the dashboard after deciding events are noise (typo, dead code, "
|
|
281
|
+
"locale mismatch) — the project's `missing_keys_stats` total drops "
|
|
282
|
+
"by the count returned. Idempotent: rows already resolved are "
|
|
283
|
+
"counted under `already_resolved`; unknown uuids under `not_found`. "
|
|
284
|
+
"Auto-resolution on key creation already covers the happy path; "
|
|
285
|
+
"this is for the events you want to dismiss without creating a key."
|
|
286
|
+
),
|
|
287
|
+
inputSchema={
|
|
288
|
+
"type": "object",
|
|
289
|
+
"properties": {
|
|
290
|
+
"project_uuid": {
|
|
291
|
+
"type": "string",
|
|
292
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
293
|
+
},
|
|
294
|
+
"uuids": {
|
|
295
|
+
"type": "array",
|
|
296
|
+
"items": {"type": "string"},
|
|
297
|
+
"minItems": 1,
|
|
298
|
+
"maxItems": 500,
|
|
299
|
+
"description": "Missing-event uuids to mark resolved.",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
"required": ["uuids"],
|
|
303
|
+
"additionalProperties": False,
|
|
304
|
+
},
|
|
305
|
+
),
|
|
306
|
+
Tool(
|
|
307
|
+
name="create_namespace",
|
|
308
|
+
description=(
|
|
309
|
+
"Create a namespace (the container for keys). Idempotent on "
|
|
310
|
+
"(project, slug): returns created=false if it already exists. "
|
|
311
|
+
"slug must be lowercase letters/digits with internal hyphens "
|
|
312
|
+
"(no leading/trailing hyphen, no underscore); an invalid slug "
|
|
313
|
+
"returns a slug_invalid error rather than 'namespace not found'. "
|
|
314
|
+
"Use this before create_key/propose_translation when the target "
|
|
315
|
+
"namespace does not exist yet."
|
|
316
|
+
),
|
|
317
|
+
inputSchema={
|
|
318
|
+
"type": "object",
|
|
319
|
+
"properties": {
|
|
320
|
+
"project_uuid": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
323
|
+
},
|
|
324
|
+
"slug": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"description": "Namespace slug: lowercase letters/digits + internal hyphens (e.g. common, marketing-site).",
|
|
327
|
+
},
|
|
328
|
+
"name": {"type": "string", "description": "Human-readable namespace name."},
|
|
329
|
+
"description": {"type": "string"},
|
|
330
|
+
},
|
|
331
|
+
"required": ["slug", "name"],
|
|
332
|
+
"additionalProperties": False,
|
|
333
|
+
},
|
|
334
|
+
),
|
|
335
|
+
Tool(
|
|
336
|
+
name="create_key",
|
|
337
|
+
description=(
|
|
338
|
+
"Create a translation key, idempotent on (namespace, name). "
|
|
339
|
+
"Returns created=false if the key already exists. When "
|
|
340
|
+
"source_value is supplied AND the project has a source "
|
|
341
|
+
"language, the value is also written as a source-language "
|
|
342
|
+
"translation in the same call."
|
|
343
|
+
),
|
|
344
|
+
inputSchema={
|
|
345
|
+
"type": "object",
|
|
346
|
+
"properties": {
|
|
347
|
+
"project_uuid": {
|
|
348
|
+
"type": "string",
|
|
349
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
350
|
+
},
|
|
351
|
+
"namespace": {
|
|
352
|
+
"type": "string",
|
|
353
|
+
"description": "Namespace slug (LLM-friendly).",
|
|
354
|
+
},
|
|
355
|
+
"name": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"description": "Dot-notation key name (e.g. hero.title).",
|
|
358
|
+
},
|
|
359
|
+
"source_value": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"description": "Optional source-language value to seed.",
|
|
362
|
+
},
|
|
363
|
+
"description": {"type": "string"},
|
|
364
|
+
"plurals": {"type": "boolean", "default": False},
|
|
365
|
+
"max_length": {"type": "integer", "minimum": 1},
|
|
366
|
+
},
|
|
367
|
+
"required": ["namespace", "name"],
|
|
368
|
+
"additionalProperties": False,
|
|
369
|
+
},
|
|
370
|
+
),
|
|
371
|
+
Tool(
|
|
372
|
+
name="propose_translation",
|
|
373
|
+
description=(
|
|
374
|
+
"Submit a translation value. Auto-creates the key if it "
|
|
375
|
+
"doesn't exist (write-through). Status is set to draft (or "
|
|
376
|
+
"translated if explicitly requested); reviewer/approved "
|
|
377
|
+
"promotion requires a human reviewer. Identifies the key "
|
|
378
|
+
"by namespace + key name + language_code — no uuids needed."
|
|
379
|
+
),
|
|
380
|
+
inputSchema={
|
|
381
|
+
"type": "object",
|
|
382
|
+
"properties": {
|
|
383
|
+
"project_uuid": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
386
|
+
},
|
|
387
|
+
"namespace": {"type": "string"},
|
|
388
|
+
"key": {"type": "string"},
|
|
389
|
+
"language_code": {
|
|
390
|
+
"type": "string",
|
|
391
|
+
"description": "BCP-47 code (e.g. fr, fr-CA, es).",
|
|
392
|
+
},
|
|
393
|
+
"value": {"type": "string"},
|
|
394
|
+
"status": {
|
|
395
|
+
"type": "string",
|
|
396
|
+
"enum": ["draft", "translated"],
|
|
397
|
+
"default": "draft",
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
"required": ["namespace", "key", "language_code", "value"],
|
|
401
|
+
"additionalProperties": False,
|
|
402
|
+
},
|
|
403
|
+
),
|
|
404
|
+
Tool(
|
|
405
|
+
name="create_keys_bulk",
|
|
406
|
+
description=(
|
|
407
|
+
"Batch version of create_key: create up to 500 keys in one "
|
|
408
|
+
"call, idempotent per item (existing keys return created=false). "
|
|
409
|
+
"Returns a per-item results array + a summary roll-up; valid "
|
|
410
|
+
"items commit even if siblings fail. Intra-batch duplicates "
|
|
411
|
+
"(same namespace+name twice) error per item; the whole call is "
|
|
412
|
+
"rejected if the new keys would exceed the org plan cap. NO "
|
|
413
|
+
"server-side translation — this only batches the writes. Use "
|
|
414
|
+
"this instead of N create_key calls for a backlog."
|
|
415
|
+
),
|
|
416
|
+
inputSchema={
|
|
417
|
+
"type": "object",
|
|
418
|
+
"properties": {
|
|
419
|
+
"project_uuid": {
|
|
420
|
+
"type": "string",
|
|
421
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
422
|
+
},
|
|
423
|
+
"items": {
|
|
424
|
+
"type": "array",
|
|
425
|
+
"minItems": 1,
|
|
426
|
+
"maxItems": 500,
|
|
427
|
+
"items": {
|
|
428
|
+
"type": "object",
|
|
429
|
+
"properties": {
|
|
430
|
+
"namespace": {"type": "string", "description": "Namespace slug."},
|
|
431
|
+
"name": {"type": "string", "description": "Dot-notation key name (e.g. hero.title)."},
|
|
432
|
+
"source_value": {"type": "string", "description": "Optional source-language value to seed."},
|
|
433
|
+
"description": {"type": "string"},
|
|
434
|
+
"plurals": {"type": "boolean", "default": False},
|
|
435
|
+
"max_length": {"type": "integer", "minimum": 1},
|
|
436
|
+
},
|
|
437
|
+
"required": ["namespace", "name"],
|
|
438
|
+
"additionalProperties": False,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
"required": ["items"],
|
|
443
|
+
"additionalProperties": False,
|
|
444
|
+
},
|
|
445
|
+
),
|
|
446
|
+
Tool(
|
|
447
|
+
name="update_key",
|
|
448
|
+
description=(
|
|
449
|
+
"Edit a key's SOURCE-language value in place. Preserves the "
|
|
450
|
+
"key and its history (version bump + history revision + audit). "
|
|
451
|
+
"Address the key by EITHER key_uuid (preferred; from list_keys) "
|
|
452
|
+
"OR namespace + name — exactly one of the two. When the source "
|
|
453
|
+
"value ACTUALLY changes, the key's reviewed/approved target "
|
|
454
|
+
"translations are demoted to needs-review (\"translated\"); "
|
|
455
|
+
"missing/draft/translated targets are untouched, and "
|
|
456
|
+
"stale_flagged reports how many were demoted. Submitting the "
|
|
457
|
+
"current value is a no-op (result=unchanged, not billed). A "
|
|
458
|
+
"real source change (or a description-only change) bills 1 unit."
|
|
459
|
+
),
|
|
460
|
+
inputSchema={
|
|
461
|
+
"type": "object",
|
|
462
|
+
"properties": {
|
|
463
|
+
"project_uuid": {
|
|
464
|
+
"type": "string",
|
|
465
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
466
|
+
},
|
|
467
|
+
"key_uuid": {
|
|
468
|
+
"type": "string",
|
|
469
|
+
"description": "Key uuid to update (preferred addressing; get it from list_keys). Mutually exclusive with namespace+name.",
|
|
470
|
+
},
|
|
471
|
+
"namespace": {
|
|
472
|
+
"type": "string",
|
|
473
|
+
"description": "Namespace slug (use with name as an alternative to key_uuid).",
|
|
474
|
+
},
|
|
475
|
+
"name": {
|
|
476
|
+
"type": "string",
|
|
477
|
+
"description": "Dot-notation key name (use with namespace as an alternative to key_uuid).",
|
|
478
|
+
},
|
|
479
|
+
"source_value": {
|
|
480
|
+
"type": "string",
|
|
481
|
+
"description": "New source-language value to write.",
|
|
482
|
+
},
|
|
483
|
+
"description": {
|
|
484
|
+
"type": "string",
|
|
485
|
+
"description": "Optional: also update the key's description.",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
"required": ["source_value"],
|
|
489
|
+
"additionalProperties": False,
|
|
490
|
+
},
|
|
491
|
+
),
|
|
492
|
+
Tool(
|
|
493
|
+
name="update_keys_bulk",
|
|
494
|
+
description=(
|
|
495
|
+
"Batch version of update_key: edit up to 500 keys' SOURCE "
|
|
496
|
+
"values in one call. Returns a per-item results array + a "
|
|
497
|
+
"summary roll-up; valid items commit even if siblings fail "
|
|
498
|
+
"(status ok | unchanged | error per item). Each item addresses "
|
|
499
|
+
"its key by EITHER key_uuid OR namespace + name (exactly one). "
|
|
500
|
+
"Same staleness, no-op and billing semantics as update_key "
|
|
501
|
+
"(1 unit per key actually changed; no-ops and errors are free). "
|
|
502
|
+
"Use this instead of N update_key calls for a backlog."
|
|
503
|
+
),
|
|
504
|
+
inputSchema={
|
|
505
|
+
"type": "object",
|
|
506
|
+
"properties": {
|
|
507
|
+
"project_uuid": {
|
|
508
|
+
"type": "string",
|
|
509
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
510
|
+
},
|
|
511
|
+
"items": {
|
|
512
|
+
"type": "array",
|
|
513
|
+
"minItems": 1,
|
|
514
|
+
"maxItems": 500,
|
|
515
|
+
"items": {
|
|
516
|
+
"type": "object",
|
|
517
|
+
"properties": {
|
|
518
|
+
"key_uuid": {"type": "string", "description": "Key uuid (preferred). Mutually exclusive with namespace+name."},
|
|
519
|
+
"namespace": {"type": "string", "description": "Namespace slug (use with name)."},
|
|
520
|
+
"name": {"type": "string", "description": "Dot-notation key name (use with namespace)."},
|
|
521
|
+
"source_value": {"type": "string", "description": "New source-language value to write."},
|
|
522
|
+
"description": {"type": "string", "description": "Optional: also update the key's description."},
|
|
523
|
+
},
|
|
524
|
+
"required": ["source_value"],
|
|
525
|
+
"additionalProperties": False,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
"required": ["items"],
|
|
530
|
+
"additionalProperties": False,
|
|
531
|
+
},
|
|
532
|
+
),
|
|
533
|
+
Tool(
|
|
534
|
+
name="propose_translations_bulk",
|
|
535
|
+
description=(
|
|
536
|
+
"Batch version of propose_translation: submit up to 500 "
|
|
537
|
+
"translation values in one call. Auto-creates missing keys "
|
|
538
|
+
"(write-through). Returns a per-item results array + summary; "
|
|
539
|
+
"an item whose value AND status already match returns "
|
|
540
|
+
"status=unchanged (no-op). Intra-batch duplicates "
|
|
541
|
+
"(same namespace+key+language twice) error per item; status is "
|
|
542
|
+
"draft (or translated if requested), reviewed/approved require "
|
|
543
|
+
"a human reviewer. NO server-side translation — values come "
|
|
544
|
+
"from you. Use this instead of N propose_translation calls."
|
|
545
|
+
),
|
|
546
|
+
inputSchema={
|
|
547
|
+
"type": "object",
|
|
548
|
+
"properties": {
|
|
549
|
+
"project_uuid": {
|
|
550
|
+
"type": "string",
|
|
551
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
552
|
+
},
|
|
553
|
+
"items": {
|
|
554
|
+
"type": "array",
|
|
555
|
+
"minItems": 1,
|
|
556
|
+
"maxItems": 500,
|
|
557
|
+
"items": {
|
|
558
|
+
"type": "object",
|
|
559
|
+
"properties": {
|
|
560
|
+
"namespace": {"type": "string"},
|
|
561
|
+
"key": {"type": "string"},
|
|
562
|
+
"language_code": {"type": "string", "description": "BCP-47 code (e.g. fr, fr-CA, es)."},
|
|
563
|
+
"value": {"type": "string"},
|
|
564
|
+
"status": {"type": "string", "enum": ["draft", "translated"], "default": "draft"},
|
|
565
|
+
},
|
|
566
|
+
"required": ["namespace", "key", "language_code", "value"],
|
|
567
|
+
"additionalProperties": False,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
"required": ["items"],
|
|
572
|
+
"additionalProperties": False,
|
|
573
|
+
},
|
|
574
|
+
),
|
|
575
|
+
Tool(
|
|
576
|
+
name="publish_cdn",
|
|
577
|
+
description=(
|
|
578
|
+
"Trigger a CDN release for the project. Builds bundles "
|
|
579
|
+
"for every (language, namespace) combo (or restricted to "
|
|
580
|
+
"the ones you pass) and pushes them to the public CDN. "
|
|
581
|
+
"Idempotent: re-running with no content changes returns "
|
|
582
|
+
"the same bundles as `reused`. Subscribed SDKs receive a "
|
|
583
|
+
"Centrifugo `translations_published` event for every "
|
|
584
|
+
"newly created bundle and can refresh in-place."
|
|
585
|
+
),
|
|
586
|
+
inputSchema={
|
|
587
|
+
"type": "object",
|
|
588
|
+
"properties": {
|
|
589
|
+
"project_uuid": {
|
|
590
|
+
"type": "string",
|
|
591
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
592
|
+
},
|
|
593
|
+
"language_code": {
|
|
594
|
+
"type": "string",
|
|
595
|
+
"description": "Restrict to one BCP-47 language. Omit for ALL.",
|
|
596
|
+
},
|
|
597
|
+
"namespace": {
|
|
598
|
+
"type": "string",
|
|
599
|
+
"description": "Restrict to one namespace slug. Omit for ALL.",
|
|
600
|
+
},
|
|
601
|
+
"version_slug": {
|
|
602
|
+
"type": "string",
|
|
603
|
+
"description": "Project version slug. Defaults to the production version.",
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
"additionalProperties": False,
|
|
607
|
+
},
|
|
608
|
+
),
|
|
609
|
+
Tool(
|
|
610
|
+
name="validate_translations",
|
|
611
|
+
description=(
|
|
612
|
+
"Lint a JSON i18next blob against the project: checks for "
|
|
613
|
+
"missing keys, extra keys, and placeholder parity ({var}, "
|
|
614
|
+
"{{var}}, %{var}) vs the source language."
|
|
615
|
+
),
|
|
616
|
+
inputSchema={
|
|
617
|
+
"type": "object",
|
|
618
|
+
"properties": {
|
|
619
|
+
"project_uuid": {
|
|
620
|
+
"type": "string",
|
|
621
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
622
|
+
},
|
|
623
|
+
"language_code": {"type": "string"},
|
|
624
|
+
"namespace": {
|
|
625
|
+
"type": "string",
|
|
626
|
+
"description": "Restrict to one namespace (default: all).",
|
|
627
|
+
},
|
|
628
|
+
"payload": {
|
|
629
|
+
"type": "object",
|
|
630
|
+
"description": "The translation tree to validate.",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
"required": ["language_code", "payload"],
|
|
634
|
+
"additionalProperties": False,
|
|
635
|
+
},
|
|
636
|
+
),
|
|
637
|
+
Tool(
|
|
638
|
+
name="project_context_get",
|
|
639
|
+
description=(
|
|
640
|
+
"Read the project's markdown context document. The document is "
|
|
641
|
+
"free-form prose attached to the project (terminology, brand "
|
|
642
|
+
"voice, constraints, domain — e.g. religious, technical, "
|
|
643
|
+
"medical, gaming, legal) and is intended as ambient context for "
|
|
644
|
+
"human translators AND for AI agents producing translations. "
|
|
645
|
+
"Call this BEFORE batch-translating: prepend the content to your "
|
|
646
|
+
"translation prompts so every output stays consistent with the "
|
|
647
|
+
"project's terminology and tone. Empty content means the project "
|
|
648
|
+
"owner hasn't authored a context yet — that's not an error. "
|
|
649
|
+
"Requires API-key scope: project:read."
|
|
650
|
+
),
|
|
651
|
+
inputSchema={
|
|
652
|
+
"type": "object",
|
|
653
|
+
"properties": {
|
|
654
|
+
"project_uuid": {
|
|
655
|
+
"type": "string",
|
|
656
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
"additionalProperties": False,
|
|
660
|
+
},
|
|
661
|
+
),
|
|
662
|
+
Tool(
|
|
663
|
+
name="project_context_set",
|
|
664
|
+
description=(
|
|
665
|
+
"Upsert the project's markdown context document. Replaces the "
|
|
666
|
+
"entire document — fetch first with project_context_get if you "
|
|
667
|
+
"want to append rather than overwrite. Use this to memorise "
|
|
668
|
+
"terminology choices, glossary entries, domain framing (e.g. "
|
|
669
|
+
"'this is a Catholic-liturgy app — use traditional vocabulary, "
|
|
670
|
+
"avoid neologisms'), tone guidance, and translation constraints "
|
|
671
|
+
"you discover while working the project. Hard cap: 100 KiB. "
|
|
672
|
+
"Requires API-key scope: project:settings:write (narrower than "
|
|
673
|
+
"project:write — settings changes propagate to every translator's "
|
|
674
|
+
"prompt context)."
|
|
675
|
+
),
|
|
676
|
+
inputSchema={
|
|
677
|
+
"type": "object",
|
|
678
|
+
"properties": {
|
|
679
|
+
"project_uuid": {
|
|
680
|
+
"type": "string",
|
|
681
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
682
|
+
},
|
|
683
|
+
"content": {
|
|
684
|
+
"type": "string",
|
|
685
|
+
"description": "New markdown content (UTF-8, ≤ 100 KiB).",
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
"required": ["content"],
|
|
689
|
+
"additionalProperties": False,
|
|
690
|
+
},
|
|
691
|
+
),
|
|
692
|
+
Tool(
|
|
693
|
+
name="glossary_list",
|
|
694
|
+
description=(
|
|
695
|
+
"List a project's glossary entries — the terminology rules the "
|
|
696
|
+
"backend applies when matching source strings. Filters: rule_type "
|
|
697
|
+
"(translation | do_not_translate | forbidden), q (case-insensitive "
|
|
698
|
+
"substring on the term), limit (1..500, default 200). Returns "
|
|
699
|
+
"{items: [GlossaryEntry], total}. Matching itself is backend-side; "
|
|
700
|
+
"this only lists the rules."
|
|
701
|
+
),
|
|
702
|
+
inputSchema={
|
|
703
|
+
"type": "object",
|
|
704
|
+
"properties": {
|
|
705
|
+
"project_uuid": {
|
|
706
|
+
"type": "string",
|
|
707
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
708
|
+
},
|
|
709
|
+
"rule_type": {
|
|
710
|
+
"type": "string",
|
|
711
|
+
"enum": ["translation", "do_not_translate", "forbidden"],
|
|
712
|
+
"description": "Filter to one rule type.",
|
|
713
|
+
},
|
|
714
|
+
"q": {
|
|
715
|
+
"type": "string",
|
|
716
|
+
"description": "Case-insensitive substring filter on the term.",
|
|
717
|
+
},
|
|
718
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 200},
|
|
719
|
+
},
|
|
720
|
+
"additionalProperties": False,
|
|
721
|
+
},
|
|
722
|
+
),
|
|
723
|
+
Tool(
|
|
724
|
+
name="glossary_create",
|
|
725
|
+
description=(
|
|
726
|
+
"Create a glossary entry. rule_type is one of translation | "
|
|
727
|
+
"do_not_translate | forbidden and defaults to 'translation'. "
|
|
728
|
+
"`translations` ({<locale>: value}) is honored only for "
|
|
729
|
+
"rule_type='translation' (cleared otherwise by the backend). "
|
|
730
|
+
"Returns the created GlossaryEntry. Errors 409 if an entry with "
|
|
731
|
+
"the same (rule_type, term) already exists — use glossary_update "
|
|
732
|
+
"to change an existing one."
|
|
733
|
+
),
|
|
734
|
+
inputSchema={
|
|
735
|
+
"type": "object",
|
|
736
|
+
"properties": {
|
|
737
|
+
"project_uuid": {
|
|
738
|
+
"type": "string",
|
|
739
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
740
|
+
},
|
|
741
|
+
"rule_type": {
|
|
742
|
+
"type": "string",
|
|
743
|
+
"enum": ["translation", "do_not_translate", "forbidden"],
|
|
744
|
+
"default": "translation",
|
|
745
|
+
"description": "Rule type. Defaults to 'translation'.",
|
|
746
|
+
},
|
|
747
|
+
"term": {
|
|
748
|
+
"type": "string",
|
|
749
|
+
"description": "The source term/phrase the rule applies to.",
|
|
750
|
+
},
|
|
751
|
+
"translations": {
|
|
752
|
+
"type": "object",
|
|
753
|
+
"description": "Per-locale target translations, e.g. {\"fr\": \"tableau de bord\"}. Only used for rule_type='translation'.",
|
|
754
|
+
},
|
|
755
|
+
"case_sensitive": {
|
|
756
|
+
"type": "boolean",
|
|
757
|
+
"default": False,
|
|
758
|
+
"description": "Match the term case-sensitively.",
|
|
759
|
+
},
|
|
760
|
+
"note": {"type": "string", "description": "Optional human note."},
|
|
761
|
+
},
|
|
762
|
+
"required": ["term"],
|
|
763
|
+
"additionalProperties": False,
|
|
764
|
+
},
|
|
765
|
+
),
|
|
766
|
+
Tool(
|
|
767
|
+
name="glossary_update",
|
|
768
|
+
description=(
|
|
769
|
+
"Update a glossary entry by its uuid (entry_id). Send only the "
|
|
770
|
+
"fields to change: term, translations, case_sensitive, note. "
|
|
771
|
+
"rule_type is immutable — delete and recreate to change it. "
|
|
772
|
+
"Returns the updated GlossaryEntry; 404 if entry_id is unknown, "
|
|
773
|
+
"409 on a term clash with another entry of the same rule_type."
|
|
774
|
+
),
|
|
775
|
+
inputSchema={
|
|
776
|
+
"type": "object",
|
|
777
|
+
"properties": {
|
|
778
|
+
"project_uuid": {
|
|
779
|
+
"type": "string",
|
|
780
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
781
|
+
},
|
|
782
|
+
"entry_id": {
|
|
783
|
+
"type": "string",
|
|
784
|
+
"description": "The GlossaryEntry uuid to update.",
|
|
785
|
+
},
|
|
786
|
+
"term": {"type": "string"},
|
|
787
|
+
"translations": {
|
|
788
|
+
"type": "object",
|
|
789
|
+
"description": "Per-locale target translations (rule_type='translation' only).",
|
|
790
|
+
},
|
|
791
|
+
"case_sensitive": {"type": "boolean"},
|
|
792
|
+
"note": {"type": "string"},
|
|
793
|
+
},
|
|
794
|
+
"required": ["entry_id"],
|
|
795
|
+
"additionalProperties": False,
|
|
796
|
+
},
|
|
797
|
+
),
|
|
798
|
+
Tool(
|
|
799
|
+
name="glossary_delete",
|
|
800
|
+
description=(
|
|
801
|
+
"Delete a glossary entry by its uuid (entry_id). Returns "
|
|
802
|
+
"{deleted: true, entry_id}. A missing entry_id returns 404."
|
|
803
|
+
),
|
|
804
|
+
inputSchema={
|
|
805
|
+
"type": "object",
|
|
806
|
+
"properties": {
|
|
807
|
+
"project_uuid": {
|
|
808
|
+
"type": "string",
|
|
809
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
810
|
+
},
|
|
811
|
+
"entry_id": {
|
|
812
|
+
"type": "string",
|
|
813
|
+
"description": "The GlossaryEntry uuid to delete.",
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
"required": ["entry_id"],
|
|
817
|
+
"additionalProperties": False,
|
|
818
|
+
},
|
|
819
|
+
),
|
|
820
|
+
Tool(
|
|
821
|
+
name="health_report",
|
|
822
|
+
description=(
|
|
823
|
+
"Read-only SOURCE-HEALTH report for the project (#752): issues in the "
|
|
824
|
+
"source strings with by_issue / by_severity rollups + per-key items. "
|
|
825
|
+
"Defaults to the production version; pass version_id for a specific "
|
|
826
|
+
"version. Surfaced verbatim (read-only)."
|
|
827
|
+
),
|
|
828
|
+
inputSchema={
|
|
829
|
+
"type": "object",
|
|
830
|
+
"properties": {
|
|
831
|
+
"project_uuid": {
|
|
832
|
+
"type": "string",
|
|
833
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
834
|
+
},
|
|
835
|
+
"version_id": {
|
|
836
|
+
"type": "string",
|
|
837
|
+
"description": "Optional version UUID; defaults to the production version.",
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
"additionalProperties": False,
|
|
841
|
+
},
|
|
842
|
+
),
|
|
843
|
+
Tool(
|
|
844
|
+
name="coverage_report",
|
|
845
|
+
description=(
|
|
846
|
+
"Read-only translation-COVERAGE report for the project (#752): global "
|
|
847
|
+
"completeness, total key count, and per-language coverage. Defaults to "
|
|
848
|
+
"the production version; pass version_id for a specific version. "
|
|
849
|
+
"Surfaced verbatim (read-only)."
|
|
850
|
+
),
|
|
851
|
+
inputSchema={
|
|
852
|
+
"type": "object",
|
|
853
|
+
"properties": {
|
|
854
|
+
"project_uuid": {
|
|
855
|
+
"type": "string",
|
|
856
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
857
|
+
},
|
|
858
|
+
"version_id": {
|
|
859
|
+
"type": "string",
|
|
860
|
+
"description": "Optional version UUID; defaults to the production version.",
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
"additionalProperties": False,
|
|
864
|
+
},
|
|
865
|
+
),
|
|
866
|
+
Tool(
|
|
867
|
+
name="delete_keys_bulk",
|
|
868
|
+
description=(
|
|
869
|
+
"Soft-delete (move to TRASH) up to 500 keys by their key_uuid. "
|
|
870
|
+
"Restorable via restore_keys_bulk. Idempotent: already-trashed keys "
|
|
871
|
+
"are reported unchanged, unknown uuids not_found. Trashed keys "
|
|
872
|
+
"disappear from list_keys, list_untranslated_keys, the CDN, and the "
|
|
873
|
+
"reports. Get key_uuid from list_keys. NO hard-delete/purge over MCP "
|
|
874
|
+
"(purge is human-only via the dashboard)."
|
|
875
|
+
),
|
|
876
|
+
inputSchema={
|
|
877
|
+
"type": "object",
|
|
878
|
+
"properties": {
|
|
879
|
+
"project_uuid": {
|
|
880
|
+
"type": "string",
|
|
881
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
882
|
+
},
|
|
883
|
+
"key_uuids": {
|
|
884
|
+
"type": "array",
|
|
885
|
+
"items": {"type": "string"},
|
|
886
|
+
"minItems": 1,
|
|
887
|
+
"maxItems": 500,
|
|
888
|
+
"description": "Key uuids to trash (get them from list_keys).",
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
"required": ["key_uuids"],
|
|
892
|
+
"additionalProperties": False,
|
|
893
|
+
},
|
|
894
|
+
),
|
|
895
|
+
Tool(
|
|
896
|
+
name="restore_keys_bulk",
|
|
897
|
+
description=(
|
|
898
|
+
"Restore up to 500 trashed keys by their key_uuid (from list_trash). "
|
|
899
|
+
"Idempotent: already-active keys are reported unchanged, unknown uuids "
|
|
900
|
+
"not_found; a key whose name was re-created while it was trashed is "
|
|
901
|
+
"reported as a conflict. Get key_uuid from list_trash."
|
|
902
|
+
),
|
|
903
|
+
inputSchema={
|
|
904
|
+
"type": "object",
|
|
905
|
+
"properties": {
|
|
906
|
+
"project_uuid": {
|
|
907
|
+
"type": "string",
|
|
908
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
909
|
+
},
|
|
910
|
+
"key_uuids": {
|
|
911
|
+
"type": "array",
|
|
912
|
+
"items": {"type": "string"},
|
|
913
|
+
"minItems": 1,
|
|
914
|
+
"maxItems": 500,
|
|
915
|
+
"description": "Key uuids to restore (get them from list_trash).",
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
"required": ["key_uuids"],
|
|
919
|
+
"additionalProperties": False,
|
|
920
|
+
},
|
|
921
|
+
),
|
|
922
|
+
Tool(
|
|
923
|
+
name="list_trash",
|
|
924
|
+
description=(
|
|
925
|
+
"List trashed (soft-deleted) keys: items of {key_uuid, namespace, "
|
|
926
|
+
"name, deleted_at}, cursor-paginated. Feed key_uuid to "
|
|
927
|
+
"restore_keys_bulk to bring them back."
|
|
928
|
+
),
|
|
929
|
+
inputSchema={
|
|
930
|
+
"type": "object",
|
|
931
|
+
"properties": {
|
|
932
|
+
"project_uuid": {
|
|
933
|
+
"type": "string",
|
|
934
|
+
"description": "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise.",
|
|
935
|
+
},
|
|
936
|
+
"cursor": {"type": "string"},
|
|
937
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 50},
|
|
938
|
+
},
|
|
939
|
+
"additionalProperties": False,
|
|
940
|
+
},
|
|
941
|
+
),
|
|
942
|
+
]
|
|
943
|
+
|
|
944
|
+
@server.call_tool()
|
|
945
|
+
async def _call(name: str, arguments: dict | None) -> list[TextContent]:
|
|
946
|
+
try:
|
|
947
|
+
args = arguments or {}
|
|
948
|
+
if name == "list_projects":
|
|
949
|
+
data = await client.get(
|
|
950
|
+
"/v1/mcp/projects", params={"limit": args.get("limit", 50)}
|
|
951
|
+
)
|
|
952
|
+
data = _filter_projects_by_allowlist(
|
|
953
|
+
data, client.config.allowed_project_uuids
|
|
954
|
+
)
|
|
955
|
+
return _text(data)
|
|
956
|
+
if name == "get_project_info":
|
|
957
|
+
project_uuid = _project_uuid(args, client)
|
|
958
|
+
data = await client.get(f"/v1/mcp/projects/{project_uuid}")
|
|
959
|
+
return _text(data)
|
|
960
|
+
if name == "list_keys":
|
|
961
|
+
project_uuid = _project_uuid(args, client)
|
|
962
|
+
params = {
|
|
963
|
+
k: v
|
|
964
|
+
for k, v in {
|
|
965
|
+
"namespace": args.get("namespace"),
|
|
966
|
+
"language_code": args.get("language_code"),
|
|
967
|
+
"status_filter": args.get("status_filter"),
|
|
968
|
+
"include_glossary_hints": (
|
|
969
|
+
"true" if args.get("include_glossary_hints") else None
|
|
970
|
+
),
|
|
971
|
+
"cursor": args.get("cursor"),
|
|
972
|
+
"limit": args.get("limit", 50),
|
|
973
|
+
}.items()
|
|
974
|
+
if v is not None
|
|
975
|
+
}
|
|
976
|
+
data = await client.get(
|
|
977
|
+
f"/v1/mcp/projects/{project_uuid}/keys", params=params
|
|
978
|
+
)
|
|
979
|
+
return _text(data)
|
|
980
|
+
if name == "list_untranslated_keys":
|
|
981
|
+
project_uuid = _project_uuid(args, client)
|
|
982
|
+
if "language_code" not in args:
|
|
983
|
+
return _err("language_code is required")
|
|
984
|
+
params = {
|
|
985
|
+
k: v
|
|
986
|
+
for k, v in {
|
|
987
|
+
"language_code": args["language_code"],
|
|
988
|
+
"namespace": args.get("namespace"),
|
|
989
|
+
"include_glossary_hints": (
|
|
990
|
+
"true" if args.get("include_glossary_hints") else None
|
|
991
|
+
),
|
|
992
|
+
"cursor": args.get("cursor"),
|
|
993
|
+
"limit": args.get("limit", 50),
|
|
994
|
+
}.items()
|
|
995
|
+
if v is not None
|
|
996
|
+
}
|
|
997
|
+
data = await client.get(
|
|
998
|
+
f"/v1/mcp/projects/{project_uuid}/untranslated-keys", params=params
|
|
999
|
+
)
|
|
1000
|
+
return _text(data)
|
|
1001
|
+
if name == "list_missing_keys":
|
|
1002
|
+
project_uuid = _project_uuid(args, client)
|
|
1003
|
+
params = {
|
|
1004
|
+
k: v
|
|
1005
|
+
for k, v in {
|
|
1006
|
+
"namespace": args.get("namespace"),
|
|
1007
|
+
"language_code": args.get("language_code"),
|
|
1008
|
+
"status": args.get("status"),
|
|
1009
|
+
"cursor": args.get("cursor"),
|
|
1010
|
+
"limit": args.get("limit", 50),
|
|
1011
|
+
}.items()
|
|
1012
|
+
if v is not None
|
|
1013
|
+
}
|
|
1014
|
+
data = await client.get(
|
|
1015
|
+
f"/v1/mcp/projects/{project_uuid}/missing-keys", params=params
|
|
1016
|
+
)
|
|
1017
|
+
return _text(data)
|
|
1018
|
+
if name == "missing_keys_stats":
|
|
1019
|
+
project_uuid = _project_uuid(args, client)
|
|
1020
|
+
params = {
|
|
1021
|
+
k: v
|
|
1022
|
+
for k, v in {
|
|
1023
|
+
"namespace": args.get("namespace"),
|
|
1024
|
+
"language_code": args.get("language_code"),
|
|
1025
|
+
"status": args.get("status"),
|
|
1026
|
+
}.items()
|
|
1027
|
+
if v is not None
|
|
1028
|
+
}
|
|
1029
|
+
data = await client.get(
|
|
1030
|
+
f"/v1/mcp/projects/{project_uuid}/missing-keys/stats",
|
|
1031
|
+
params=params,
|
|
1032
|
+
)
|
|
1033
|
+
return _text(data)
|
|
1034
|
+
if name == "acknowledge_missing_keys":
|
|
1035
|
+
project_uuid = _project_uuid(args, client)
|
|
1036
|
+
uuids = args.get("uuids") or []
|
|
1037
|
+
if not uuids:
|
|
1038
|
+
return _err("uuids must be a non-empty list")
|
|
1039
|
+
data = await client.post(
|
|
1040
|
+
f"/v1/mcp/projects/{project_uuid}/missing-keys/acknowledge",
|
|
1041
|
+
json={"uuids": uuids},
|
|
1042
|
+
)
|
|
1043
|
+
return _text(data)
|
|
1044
|
+
if name == "create_namespace":
|
|
1045
|
+
project_uuid = _project_uuid(args, client)
|
|
1046
|
+
if "slug" not in args or "name" not in args:
|
|
1047
|
+
return _err("slug and name are required")
|
|
1048
|
+
body: dict[str, Any] = {"slug": args["slug"], "name": args["name"]}
|
|
1049
|
+
if "description" in args:
|
|
1050
|
+
body["description"] = args["description"]
|
|
1051
|
+
data = await client.post(
|
|
1052
|
+
f"/v1/mcp/projects/{project_uuid}/namespaces", json=body
|
|
1053
|
+
)
|
|
1054
|
+
return _text(data)
|
|
1055
|
+
if name == "create_key":
|
|
1056
|
+
project_uuid = _project_uuid(args, client)
|
|
1057
|
+
if "namespace" not in args or "name" not in args:
|
|
1058
|
+
return _err("namespace and name are required")
|
|
1059
|
+
body: dict[str, Any] = {
|
|
1060
|
+
"namespace": args["namespace"],
|
|
1061
|
+
"name": args["name"],
|
|
1062
|
+
}
|
|
1063
|
+
for opt in ("source_value", "description", "plurals", "max_length"):
|
|
1064
|
+
if opt in args:
|
|
1065
|
+
body[opt] = args[opt]
|
|
1066
|
+
data = await client.post(
|
|
1067
|
+
f"/v1/mcp/projects/{project_uuid}/keys", json=body
|
|
1068
|
+
)
|
|
1069
|
+
return _text(data)
|
|
1070
|
+
if name == "propose_translation":
|
|
1071
|
+
project_uuid = _project_uuid(args, client)
|
|
1072
|
+
body = {
|
|
1073
|
+
"namespace": args["namespace"],
|
|
1074
|
+
"key": args["key"],
|
|
1075
|
+
"language_code": args["language_code"],
|
|
1076
|
+
"value": args["value"],
|
|
1077
|
+
}
|
|
1078
|
+
if "status" in args:
|
|
1079
|
+
body["status"] = args["status"]
|
|
1080
|
+
data = await client.put(
|
|
1081
|
+
f"/v1/mcp/projects/{project_uuid}/translations", json=body
|
|
1082
|
+
)
|
|
1083
|
+
return _text(data)
|
|
1084
|
+
if name == "create_keys_bulk":
|
|
1085
|
+
project_uuid = _project_uuid(args, client)
|
|
1086
|
+
items = args.get("items")
|
|
1087
|
+
if not isinstance(items, list) or not items:
|
|
1088
|
+
return _err("items must be a non-empty list")
|
|
1089
|
+
data = await client.post(
|
|
1090
|
+
f"/v1/mcp/projects/{project_uuid}/keys/bulk", json={"items": items}
|
|
1091
|
+
)
|
|
1092
|
+
return _text(data)
|
|
1093
|
+
if name == "update_key":
|
|
1094
|
+
project_uuid = _project_uuid(args, client)
|
|
1095
|
+
if "source_value" not in args:
|
|
1096
|
+
return _err("source_value is required")
|
|
1097
|
+
has_uuid = bool(args.get("key_uuid"))
|
|
1098
|
+
has_name = bool(args.get("namespace")) and bool(args.get("name"))
|
|
1099
|
+
if has_uuid == has_name:
|
|
1100
|
+
return _err(
|
|
1101
|
+
"address the key by exactly one of key_uuid OR (namespace + name)"
|
|
1102
|
+
)
|
|
1103
|
+
body: dict[str, Any] = {"source_value": args["source_value"]}
|
|
1104
|
+
for opt in ("key_uuid", "namespace", "name", "description"):
|
|
1105
|
+
if opt in args:
|
|
1106
|
+
body[opt] = args[opt]
|
|
1107
|
+
data = await client.patch(
|
|
1108
|
+
f"/v1/mcp/projects/{project_uuid}/keys", json=body
|
|
1109
|
+
)
|
|
1110
|
+
return _text(data)
|
|
1111
|
+
if name == "update_keys_bulk":
|
|
1112
|
+
project_uuid = _project_uuid(args, client)
|
|
1113
|
+
items = args.get("items")
|
|
1114
|
+
if not isinstance(items, list) or not items:
|
|
1115
|
+
return _err("items must be a non-empty list")
|
|
1116
|
+
data = await client.patch(
|
|
1117
|
+
f"/v1/mcp/projects/{project_uuid}/keys/bulk", json={"items": items}
|
|
1118
|
+
)
|
|
1119
|
+
return _text(data)
|
|
1120
|
+
if name == "propose_translations_bulk":
|
|
1121
|
+
project_uuid = _project_uuid(args, client)
|
|
1122
|
+
items = args.get("items")
|
|
1123
|
+
if not isinstance(items, list) or not items:
|
|
1124
|
+
return _err("items must be a non-empty list")
|
|
1125
|
+
data = await client.post(
|
|
1126
|
+
f"/v1/mcp/projects/{project_uuid}/translations/bulk",
|
|
1127
|
+
json={"items": items},
|
|
1128
|
+
)
|
|
1129
|
+
return _text(data)
|
|
1130
|
+
if name == "publish_cdn":
|
|
1131
|
+
project_uuid = _project_uuid(args, client)
|
|
1132
|
+
body: dict[str, Any] = {}
|
|
1133
|
+
for opt in ("language_code", "namespace", "version_slug"):
|
|
1134
|
+
if opt in args and args[opt]:
|
|
1135
|
+
body[opt] = args[opt]
|
|
1136
|
+
data = await client.post(
|
|
1137
|
+
f"/v1/mcp/projects/{project_uuid}/cdn/releases", json=body
|
|
1138
|
+
)
|
|
1139
|
+
return _text(data)
|
|
1140
|
+
if name == "validate_translations":
|
|
1141
|
+
project_uuid = _project_uuid(args, client)
|
|
1142
|
+
body = {
|
|
1143
|
+
"language_code": args["language_code"],
|
|
1144
|
+
"payload": args.get("payload", {}),
|
|
1145
|
+
}
|
|
1146
|
+
if "namespace" in args:
|
|
1147
|
+
body["namespace"] = args["namespace"]
|
|
1148
|
+
data = await client.post(
|
|
1149
|
+
f"/v1/mcp/projects/{project_uuid}/validate", json=body
|
|
1150
|
+
)
|
|
1151
|
+
return _text(data)
|
|
1152
|
+
if name == "project_context_get":
|
|
1153
|
+
project_uuid = _project_uuid(args, client)
|
|
1154
|
+
data = await client.get(
|
|
1155
|
+
f"/v1/mcp/projects/{project_uuid}/context"
|
|
1156
|
+
)
|
|
1157
|
+
return _text(data)
|
|
1158
|
+
if name == "project_context_set":
|
|
1159
|
+
project_uuid = _project_uuid(args, client)
|
|
1160
|
+
if "content" not in args:
|
|
1161
|
+
return _err("content is required")
|
|
1162
|
+
data = await client.put(
|
|
1163
|
+
f"/v1/mcp/projects/{project_uuid}/context",
|
|
1164
|
+
json={"content": args["content"]},
|
|
1165
|
+
)
|
|
1166
|
+
return _text(data)
|
|
1167
|
+
if name == "glossary_list":
|
|
1168
|
+
project_uuid = _project_uuid(args, client)
|
|
1169
|
+
params = {
|
|
1170
|
+
k: v
|
|
1171
|
+
for k, v in {
|
|
1172
|
+
"rule_type": args.get("rule_type"),
|
|
1173
|
+
"q": args.get("q"),
|
|
1174
|
+
"limit": args.get("limit", 200),
|
|
1175
|
+
}.items()
|
|
1176
|
+
if v is not None
|
|
1177
|
+
}
|
|
1178
|
+
data = await client.get(
|
|
1179
|
+
f"/v1/mcp/projects/{project_uuid}/glossary", params=params
|
|
1180
|
+
)
|
|
1181
|
+
return _text(data)
|
|
1182
|
+
if name == "glossary_create":
|
|
1183
|
+
project_uuid = _project_uuid(args, client)
|
|
1184
|
+
if "term" not in args:
|
|
1185
|
+
return _err("term is required")
|
|
1186
|
+
body: dict[str, Any] = {"term": args["term"]}
|
|
1187
|
+
for opt in ("rule_type", "translations", "case_sensitive", "note"):
|
|
1188
|
+
if opt in args:
|
|
1189
|
+
body[opt] = args[opt]
|
|
1190
|
+
data = await client.post(
|
|
1191
|
+
f"/v1/mcp/projects/{project_uuid}/glossary", json=body
|
|
1192
|
+
)
|
|
1193
|
+
return _text(data)
|
|
1194
|
+
if name == "glossary_update":
|
|
1195
|
+
project_uuid = _project_uuid(args, client)
|
|
1196
|
+
if "entry_id" not in args:
|
|
1197
|
+
return _err("entry_id is required")
|
|
1198
|
+
body: dict[str, Any] = {}
|
|
1199
|
+
for opt in ("term", "translations", "case_sensitive", "note"):
|
|
1200
|
+
if opt in args:
|
|
1201
|
+
body[opt] = args[opt]
|
|
1202
|
+
if not body:
|
|
1203
|
+
return _err(
|
|
1204
|
+
"nothing to update: pass at least one of term, translations, "
|
|
1205
|
+
"case_sensitive, note"
|
|
1206
|
+
)
|
|
1207
|
+
data = await client.patch(
|
|
1208
|
+
f"/v1/mcp/projects/{project_uuid}/glossary/{args['entry_id']}",
|
|
1209
|
+
json=body,
|
|
1210
|
+
)
|
|
1211
|
+
return _text(data)
|
|
1212
|
+
if name == "glossary_delete":
|
|
1213
|
+
project_uuid = _project_uuid(args, client)
|
|
1214
|
+
if "entry_id" not in args:
|
|
1215
|
+
return _err("entry_id is required")
|
|
1216
|
+
await client.delete(
|
|
1217
|
+
f"/v1/mcp/projects/{project_uuid}/glossary/{args['entry_id']}"
|
|
1218
|
+
)
|
|
1219
|
+
return _text({"deleted": True, "entry_id": args["entry_id"]})
|
|
1220
|
+
if name == "health_report":
|
|
1221
|
+
project_uuid = _project_uuid(args, client)
|
|
1222
|
+
params = {
|
|
1223
|
+
k: v
|
|
1224
|
+
for k, v in {"version_id": args.get("version_id")}.items()
|
|
1225
|
+
if v is not None
|
|
1226
|
+
}
|
|
1227
|
+
data = await client.get(
|
|
1228
|
+
f"/v1/mcp/projects/{project_uuid}/source-health", params=params
|
|
1229
|
+
)
|
|
1230
|
+
return _text(data)
|
|
1231
|
+
if name == "coverage_report":
|
|
1232
|
+
project_uuid = _project_uuid(args, client)
|
|
1233
|
+
params = {
|
|
1234
|
+
k: v
|
|
1235
|
+
for k, v in {"version_id": args.get("version_id")}.items()
|
|
1236
|
+
if v is not None
|
|
1237
|
+
}
|
|
1238
|
+
data = await client.get(
|
|
1239
|
+
f"/v1/mcp/projects/{project_uuid}/coverage", params=params
|
|
1240
|
+
)
|
|
1241
|
+
return _text(data)
|
|
1242
|
+
if name == "delete_keys_bulk":
|
|
1243
|
+
project_uuid = _project_uuid(args, client)
|
|
1244
|
+
key_uuids = args.get("key_uuids")
|
|
1245
|
+
if not isinstance(key_uuids, list) or not key_uuids:
|
|
1246
|
+
return _err("key_uuids must be a non-empty list")
|
|
1247
|
+
data = await client.post(
|
|
1248
|
+
f"/v1/mcp/projects/{project_uuid}/keys/bulk/delete",
|
|
1249
|
+
json={"key_uuids": key_uuids},
|
|
1250
|
+
)
|
|
1251
|
+
return _text(data)
|
|
1252
|
+
if name == "restore_keys_bulk":
|
|
1253
|
+
project_uuid = _project_uuid(args, client)
|
|
1254
|
+
key_uuids = args.get("key_uuids")
|
|
1255
|
+
if not isinstance(key_uuids, list) or not key_uuids:
|
|
1256
|
+
return _err("key_uuids must be a non-empty list")
|
|
1257
|
+
data = await client.post(
|
|
1258
|
+
f"/v1/mcp/projects/{project_uuid}/keys/bulk/restore",
|
|
1259
|
+
json={"key_uuids": key_uuids},
|
|
1260
|
+
)
|
|
1261
|
+
return _text(data)
|
|
1262
|
+
if name == "list_trash":
|
|
1263
|
+
project_uuid = _project_uuid(args, client)
|
|
1264
|
+
params = {
|
|
1265
|
+
k: v
|
|
1266
|
+
for k, v in {
|
|
1267
|
+
"cursor": args.get("cursor"),
|
|
1268
|
+
"limit": args.get("limit", 50),
|
|
1269
|
+
}.items()
|
|
1270
|
+
if v is not None
|
|
1271
|
+
}
|
|
1272
|
+
data = await client.get(
|
|
1273
|
+
f"/v1/mcp/projects/{project_uuid}/keys/trashed", params=params
|
|
1274
|
+
)
|
|
1275
|
+
return _text(data)
|
|
1276
|
+
return _err(f"unknown tool: {name}")
|
|
1277
|
+
except VerbumiaApiError as exc:
|
|
1278
|
+
return _err(f"API {exc.status}: {exc.body}")
|
|
1279
|
+
except KeyError as exc:
|
|
1280
|
+
return _err(f"missing required argument: {exc}")
|
|
1281
|
+
except Exception as exc: # pragma: no cover
|
|
1282
|
+
return _err(f"{type(exc).__name__}: {exc}")
|
|
1283
|
+
|
|
1284
|
+
return _list_tools, _call
|