nativ-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,28 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ *.egg
20
+ .env
21
+ .venv
22
+ venv/
23
+ ENV/
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nativ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: nativ-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Nativ — AI-powered localization platform. Translate text, manage translation memory, and access style guides from any MCP-compatible AI tool.
5
+ Project-URL: Homepage, https://usenativ.com
6
+ Project-URL: Repository, https://github.com/nativ-ai/nativ-mcp
7
+ Project-URL: Documentation, https://github.com/nativ-ai/nativ-mcp#readme
8
+ Project-URL: Issues, https://github.com/nativ-ai/nativ-mcp/issues
9
+ Author-email: Nativ <hello@usenativ.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,i18n,l10n,localization,mcp,nativ,translation,translation-memory
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Internationalization
22
+ Classifier: Topic :: Software Development :: Localization
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: mcp[cli]>=1.2.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Nativ MCP Server
29
+
30
+ AI-powered localization for any MCP-compatible tool — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.sh), [Windsurf](https://codeium.com/windsurf), and more.
31
+
32
+ [Nativ](https://usenativ.com) is a localization platform that uses AI to translate content while respecting your brand voice, translation memory, glossaries, and style guides. This MCP server brings Nativ's full localization engine into your AI coding workflow.
33
+
34
+ <a href="https://smithery.ai/server/@nativ-ai/nativ-mcp"><img alt="Smithery" src="https://smithery.ai/badge/@nativ-ai/nativ-mcp"></a>
35
+
36
+ ---
37
+
38
+ ## Why use Nativ via MCP?
39
+
40
+ - **Translate in-context** — localize strings, copy, and content directly from your editor without switching to a browser
41
+ - **Translation Memory aware** — every translation checks your TM first, ensuring consistency across your project
42
+ - **Brand voice built-in** — your team's tone, formality, and style guides are applied automatically
43
+ - **Review and approve** — add approved translations to TM from your editor, building quality over time
44
+ - **Multi-format** — JSON, CSV, Markdown, or freeform text — Nativ handles it all
45
+
46
+ ## Quick Start
47
+
48
+ ### 1. Get a Nativ API Key
49
+
50
+ Sign up at [dashboard.usenativ.com](https://dashboard.usenativ.com), go to **Settings → API Keys**, and create a key. It looks like `nativ_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
51
+
52
+ ### 2. Install
53
+
54
+ #### Claude Code / Claude Desktop
55
+
56
+ Add to your MCP configuration (`~/.claude/claude_desktop_config.json`):
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "nativ": {
62
+ "command": "uvx",
63
+ "args": ["nativ-mcp"],
64
+ "env": {
65
+ "NATIV_API_KEY": "nativ_your_api_key_here"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ #### Cursor
73
+
74
+ Add to your Cursor MCP settings (`.cursor/mcp.json` in your project or global config):
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "nativ": {
80
+ "command": "uvx",
81
+ "args": ["nativ-mcp"],
82
+ "env": {
83
+ "NATIV_API_KEY": "nativ_your_api_key_here"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ #### Windsurf
91
+
92
+ Add to your Windsurf MCP configuration:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "nativ": {
98
+ "command": "uvx",
99
+ "args": ["nativ-mcp"],
100
+ "env": {
101
+ "NATIV_API_KEY": "nativ_your_api_key_here"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ > **Note:** `uvx` runs the package directly from PyPI without needing a manual install. If you prefer, install it first with `pip install nativ-mcp` and use `"command": "nativ-mcp"` instead.
109
+
110
+ ### 3. Use it
111
+
112
+ Ask your AI assistant things like:
113
+
114
+ - *"Translate 'Welcome back!' to French and German"*
115
+ - *"Check our translation memory for existing translations of 'Sign up'"*
116
+ - *"What are our style guides for localization?"*
117
+ - *"Localize these i18n strings to all configured languages"*
118
+ - *"Review this German translation against our TM and brand voice"*
119
+
120
+ ## Tools
121
+
122
+ | Tool | Description |
123
+ |------|-------------|
124
+ | `translate` | Translate text using the full localization engine (TM, style guides, brand voice, glossary) |
125
+ | `translate_batch` | Translate multiple texts to a target language in one call |
126
+ | `search_translation_memory` | Fuzzy-search the translation memory for existing translations |
127
+ | `add_translation_memory_entry` | Add an approved translation to TM for future reuse |
128
+ | `get_languages` | List all configured languages with formality and style settings |
129
+ | `get_translation_memory_stats` | Get TM statistics — total entries, sources, and breakdown |
130
+ | `get_style_guides` | List all style guides with their content and status |
131
+ | `get_brand_voice` | Get the brand voice prompt that shapes all translations |
132
+
133
+ ## Resources
134
+
135
+ | URI | Description |
136
+ |-----|-------------|
137
+ | `nativ://languages` | Configured languages (JSON) |
138
+ | `nativ://style-guides` | All style guides (JSON) |
139
+ | `nativ://brand-prompt` | Brand voice prompt (JSON) |
140
+ | `nativ://tm/stats` | Translation memory statistics (JSON) |
141
+
142
+ ## Prompts
143
+
144
+ | Prompt | Description |
145
+ |--------|-------------|
146
+ | `localize-content` | Guided workflow to localize content into target languages |
147
+ | `review-translation` | Review a translation against TM, style guides, and brand voice |
148
+ | `batch-localize-strings` | Batch-localize i18n strings with structured output |
149
+
150
+ ## Examples
151
+
152
+ ### Translate a marketing headline
153
+
154
+ ```
155
+ You: Translate "The future of luxury, delivered" to French and Japanese
156
+
157
+ AI: [calls translate tool for each language]
158
+
159
+ Translation (French): "L'avenir du luxe, livré chez vous"
160
+ TM Match: 0% — new translation, no prior TM entries
161
+ Rationale: "Livré chez vous" adds a personal touch absent from the literal
162
+ "livré", aligning with the brand's premium yet approachable voice.
163
+
164
+ Translation (Japanese): "ラグジュアリーの未来を、あなたの元へ"
165
+ TM Match: 45% partial — similar pattern found in TM from brand_voice source
166
+ ```
167
+
168
+ ### Check existing translations
169
+
170
+ ```
171
+ You: Do we have translations for "Add to cart" in our TM?
172
+
173
+ AI: [calls search_translation_memory]
174
+
175
+ TM Search Results for "Add to cart" (3 matches):
176
+ - 95% [strong] "Add to cart" → "Ajouter au panier" (source: approved)
177
+ - 95% [strong] "Add to cart" → "In den Warenkorb" (source: brand_voice)
178
+ - 72% [partial] "Add items to cart" → "Ajouter des articles" (source: phrase_tm)
179
+ ```
180
+
181
+ ### Batch localize i18n strings
182
+
183
+ ```
184
+ You: Localize these to French:
185
+ - "Sign up"
186
+ - "Log in"
187
+ - "Forgot password?"
188
+ - "Continue with Google"
189
+
190
+ AI: [calls translate_batch]
191
+
192
+ Batch translation to French (4 items):
193
+ 1. "Sign up" → "S'inscrire" (TM 100%)
194
+ 2. "Log in" → "Se connecter" (TM 100%)
195
+ 3. "Forgot password?" → "Mot de passe oublié ?" (TM 92%)
196
+ 4. "Continue with Google" → "Continuer avec Google" (TM 85%)
197
+ ```
198
+
199
+ ## Configuration
200
+
201
+ | Environment Variable | Required | Description |
202
+ |---------------------|----------|-------------|
203
+ | `NATIV_API_KEY` | Yes | Your Nativ API key (`nativ_xxx...`) |
204
+ | `NATIV_API_URL` | No | API base URL (defaults to `https://api.usenativ.com`) |
205
+
206
+ ## How It Works
207
+
208
+ This MCP server acts as a bridge between your AI coding assistant and the Nativ API:
209
+
210
+ ```
211
+ ┌─────────────────────┐ ┌──────────────┐ ┌─────────────────┐
212
+ │ Claude / Cursor / │────▶│ Nativ MCP │────▶│ Nativ API │
213
+ │ Windsurf / etc. │◀────│ Server │◀────│ (Translation, │
214
+ │ │ │ (stdio) │ │ TM, Styles) │
215
+ └─────────────────────┘ └──────────────┘ └─────────────────┘
216
+ ```
217
+
218
+ The MCP server runs locally via stdio. It authenticates with your API key and calls the Nativ REST API on your behalf. Your AI assistant sees Nativ's tools, resources, and prompts as native capabilities.
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ # Clone the repo
224
+ git clone https://github.com/nativ-ai/nativ-mcp.git
225
+ cd nativ-mcp
226
+
227
+ # Set up environment
228
+ uv venv
229
+ source .venv/bin/activate
230
+ uv pip install -e ".[dev]"
231
+
232
+ # Run the server (for testing)
233
+ NATIV_API_KEY=nativ_xxx nativ-mcp
234
+
235
+ # Run with MCP Inspector
236
+ NATIV_API_KEY=nativ_xxx npx @modelcontextprotocol/inspector uv run nativ-mcp
237
+ ```
238
+
239
+ ## License
240
+
241
+ MIT — see [LICENSE](LICENSE).
242
+
243
+ ## Links
244
+
245
+ - [Nativ Platform](https://usenativ.com)
246
+ - [Nativ Dashboard](https://dashboard.usenativ.com)
247
+ - [MCP Protocol](https://modelcontextprotocol.io)
248
+ - [Report Issues](https://github.com/nativ-ai/nativ-mcp/issues)
@@ -0,0 +1,221 @@
1
+ # Nativ MCP Server
2
+
3
+ AI-powered localization for any MCP-compatible tool — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.sh), [Windsurf](https://codeium.com/windsurf), and more.
4
+
5
+ [Nativ](https://usenativ.com) is a localization platform that uses AI to translate content while respecting your brand voice, translation memory, glossaries, and style guides. This MCP server brings Nativ's full localization engine into your AI coding workflow.
6
+
7
+ <a href="https://smithery.ai/server/@nativ-ai/nativ-mcp"><img alt="Smithery" src="https://smithery.ai/badge/@nativ-ai/nativ-mcp"></a>
8
+
9
+ ---
10
+
11
+ ## Why use Nativ via MCP?
12
+
13
+ - **Translate in-context** — localize strings, copy, and content directly from your editor without switching to a browser
14
+ - **Translation Memory aware** — every translation checks your TM first, ensuring consistency across your project
15
+ - **Brand voice built-in** — your team's tone, formality, and style guides are applied automatically
16
+ - **Review and approve** — add approved translations to TM from your editor, building quality over time
17
+ - **Multi-format** — JSON, CSV, Markdown, or freeform text — Nativ handles it all
18
+
19
+ ## Quick Start
20
+
21
+ ### 1. Get a Nativ API Key
22
+
23
+ Sign up at [dashboard.usenativ.com](https://dashboard.usenativ.com), go to **Settings → API Keys**, and create a key. It looks like `nativ_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
24
+
25
+ ### 2. Install
26
+
27
+ #### Claude Code / Claude Desktop
28
+
29
+ Add to your MCP configuration (`~/.claude/claude_desktop_config.json`):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "nativ": {
35
+ "command": "uvx",
36
+ "args": ["nativ-mcp"],
37
+ "env": {
38
+ "NATIV_API_KEY": "nativ_your_api_key_here"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ #### Cursor
46
+
47
+ Add to your Cursor MCP settings (`.cursor/mcp.json` in your project or global config):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "nativ": {
53
+ "command": "uvx",
54
+ "args": ["nativ-mcp"],
55
+ "env": {
56
+ "NATIV_API_KEY": "nativ_your_api_key_here"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ #### Windsurf
64
+
65
+ Add to your Windsurf MCP configuration:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "nativ": {
71
+ "command": "uvx",
72
+ "args": ["nativ-mcp"],
73
+ "env": {
74
+ "NATIV_API_KEY": "nativ_your_api_key_here"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ > **Note:** `uvx` runs the package directly from PyPI without needing a manual install. If you prefer, install it first with `pip install nativ-mcp` and use `"command": "nativ-mcp"` instead.
82
+
83
+ ### 3. Use it
84
+
85
+ Ask your AI assistant things like:
86
+
87
+ - *"Translate 'Welcome back!' to French and German"*
88
+ - *"Check our translation memory for existing translations of 'Sign up'"*
89
+ - *"What are our style guides for localization?"*
90
+ - *"Localize these i18n strings to all configured languages"*
91
+ - *"Review this German translation against our TM and brand voice"*
92
+
93
+ ## Tools
94
+
95
+ | Tool | Description |
96
+ |------|-------------|
97
+ | `translate` | Translate text using the full localization engine (TM, style guides, brand voice, glossary) |
98
+ | `translate_batch` | Translate multiple texts to a target language in one call |
99
+ | `search_translation_memory` | Fuzzy-search the translation memory for existing translations |
100
+ | `add_translation_memory_entry` | Add an approved translation to TM for future reuse |
101
+ | `get_languages` | List all configured languages with formality and style settings |
102
+ | `get_translation_memory_stats` | Get TM statistics — total entries, sources, and breakdown |
103
+ | `get_style_guides` | List all style guides with their content and status |
104
+ | `get_brand_voice` | Get the brand voice prompt that shapes all translations |
105
+
106
+ ## Resources
107
+
108
+ | URI | Description |
109
+ |-----|-------------|
110
+ | `nativ://languages` | Configured languages (JSON) |
111
+ | `nativ://style-guides` | All style guides (JSON) |
112
+ | `nativ://brand-prompt` | Brand voice prompt (JSON) |
113
+ | `nativ://tm/stats` | Translation memory statistics (JSON) |
114
+
115
+ ## Prompts
116
+
117
+ | Prompt | Description |
118
+ |--------|-------------|
119
+ | `localize-content` | Guided workflow to localize content into target languages |
120
+ | `review-translation` | Review a translation against TM, style guides, and brand voice |
121
+ | `batch-localize-strings` | Batch-localize i18n strings with structured output |
122
+
123
+ ## Examples
124
+
125
+ ### Translate a marketing headline
126
+
127
+ ```
128
+ You: Translate "The future of luxury, delivered" to French and Japanese
129
+
130
+ AI: [calls translate tool for each language]
131
+
132
+ Translation (French): "L'avenir du luxe, livré chez vous"
133
+ TM Match: 0% — new translation, no prior TM entries
134
+ Rationale: "Livré chez vous" adds a personal touch absent from the literal
135
+ "livré", aligning with the brand's premium yet approachable voice.
136
+
137
+ Translation (Japanese): "ラグジュアリーの未来を、あなたの元へ"
138
+ TM Match: 45% partial — similar pattern found in TM from brand_voice source
139
+ ```
140
+
141
+ ### Check existing translations
142
+
143
+ ```
144
+ You: Do we have translations for "Add to cart" in our TM?
145
+
146
+ AI: [calls search_translation_memory]
147
+
148
+ TM Search Results for "Add to cart" (3 matches):
149
+ - 95% [strong] "Add to cart" → "Ajouter au panier" (source: approved)
150
+ - 95% [strong] "Add to cart" → "In den Warenkorb" (source: brand_voice)
151
+ - 72% [partial] "Add items to cart" → "Ajouter des articles" (source: phrase_tm)
152
+ ```
153
+
154
+ ### Batch localize i18n strings
155
+
156
+ ```
157
+ You: Localize these to French:
158
+ - "Sign up"
159
+ - "Log in"
160
+ - "Forgot password?"
161
+ - "Continue with Google"
162
+
163
+ AI: [calls translate_batch]
164
+
165
+ Batch translation to French (4 items):
166
+ 1. "Sign up" → "S'inscrire" (TM 100%)
167
+ 2. "Log in" → "Se connecter" (TM 100%)
168
+ 3. "Forgot password?" → "Mot de passe oublié ?" (TM 92%)
169
+ 4. "Continue with Google" → "Continuer avec Google" (TM 85%)
170
+ ```
171
+
172
+ ## Configuration
173
+
174
+ | Environment Variable | Required | Description |
175
+ |---------------------|----------|-------------|
176
+ | `NATIV_API_KEY` | Yes | Your Nativ API key (`nativ_xxx...`) |
177
+ | `NATIV_API_URL` | No | API base URL (defaults to `https://api.usenativ.com`) |
178
+
179
+ ## How It Works
180
+
181
+ This MCP server acts as a bridge between your AI coding assistant and the Nativ API:
182
+
183
+ ```
184
+ ┌─────────────────────┐ ┌──────────────┐ ┌─────────────────┐
185
+ │ Claude / Cursor / │────▶│ Nativ MCP │────▶│ Nativ API │
186
+ │ Windsurf / etc. │◀────│ Server │◀────│ (Translation, │
187
+ │ │ │ (stdio) │ │ TM, Styles) │
188
+ └─────────────────────┘ └──────────────┘ └─────────────────┘
189
+ ```
190
+
191
+ The MCP server runs locally via stdio. It authenticates with your API key and calls the Nativ REST API on your behalf. Your AI assistant sees Nativ's tools, resources, and prompts as native capabilities.
192
+
193
+ ## Development
194
+
195
+ ```bash
196
+ # Clone the repo
197
+ git clone https://github.com/nativ-ai/nativ-mcp.git
198
+ cd nativ-mcp
199
+
200
+ # Set up environment
201
+ uv venv
202
+ source .venv/bin/activate
203
+ uv pip install -e ".[dev]"
204
+
205
+ # Run the server (for testing)
206
+ NATIV_API_KEY=nativ_xxx nativ-mcp
207
+
208
+ # Run with MCP Inspector
209
+ NATIV_API_KEY=nativ_xxx npx @modelcontextprotocol/inspector uv run nativ-mcp
210
+ ```
211
+
212
+ ## License
213
+
214
+ MIT — see [LICENSE](LICENSE).
215
+
216
+ ## Links
217
+
218
+ - [Nativ Platform](https://usenativ.com)
219
+ - [Nativ Dashboard](https://dashboard.usenativ.com)
220
+ - [MCP Protocol](https://modelcontextprotocol.io)
221
+ - [Report Issues](https://github.com/nativ-ai/nativ-mcp/issues)
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "nativ-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for Nativ — AI-powered localization platform. Translate text, manage translation memory, and access style guides from any MCP-compatible AI tool."
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Nativ", email = "hello@usenativ.com" },
10
+ ]
11
+ keywords = [
12
+ "mcp",
13
+ "localization",
14
+ "translation",
15
+ "i18n",
16
+ "l10n",
17
+ "nativ",
18
+ "ai",
19
+ "translation-memory",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Software Development :: Internationalization",
31
+ "Topic :: Software Development :: Localization",
32
+ ]
33
+ dependencies = [
34
+ "mcp[cli]>=1.2.0",
35
+ "httpx>=0.27.0",
36
+ ]
37
+
38
+ [project.scripts]
39
+ nativ-mcp = "nativ_mcp.server:main"
40
+
41
+ [project.urls]
42
+ Homepage = "https://usenativ.com"
43
+ Repository = "https://github.com/nativ-ai/nativ-mcp"
44
+ Documentation = "https://github.com/nativ-ai/nativ-mcp#readme"
45
+ Issues = "https://github.com/nativ-ai/nativ-mcp/issues"
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/nativ_mcp"]
@@ -0,0 +1,3 @@
1
+ """Nativ MCP Server — AI-powered localization from any MCP-compatible tool."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,660 @@
1
+ """
2
+ Nativ MCP Server
3
+
4
+ Exposes Nativ's AI localization platform as an MCP server, enabling
5
+ translation, translation memory, and style guide access from any
6
+ MCP-compatible AI tool (Claude Code, Cursor, Windsurf, etc.).
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ import logging
13
+ from typing import Any, Optional
14
+
15
+ import httpx
16
+ from mcp.server.fastmcp import FastMCP
17
+
18
+ logging.basicConfig(level=logging.INFO, stream=sys.stderr)
19
+ logger = logging.getLogger("nativ-mcp")
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Nativ API client
23
+ # ---------------------------------------------------------------------------
24
+
25
+ DEFAULT_API_URL = "https://api.usenativ.com"
26
+
27
+
28
+ class NativClient:
29
+ """Thin async wrapper around the Nativ REST API."""
30
+
31
+ def __init__(self, api_key: str, base_url: str | None = None):
32
+ self.base_url = (base_url or DEFAULT_API_URL).rstrip("/")
33
+ self.headers = {
34
+ "X-API-Key": api_key,
35
+ "Content-Type": "application/json",
36
+ "User-Agent": "nativ-mcp/0.1.0",
37
+ }
38
+
39
+ async def _request(
40
+ self,
41
+ method: str,
42
+ path: str,
43
+ *,
44
+ json_body: dict | None = None,
45
+ params: dict | None = None,
46
+ ) -> dict[str, Any]:
47
+ async with httpx.AsyncClient(timeout=120.0) as client:
48
+ resp = await client.request(
49
+ method,
50
+ f"{self.base_url}{path}",
51
+ headers=self.headers,
52
+ json=json_body,
53
+ params=params,
54
+ )
55
+ if resp.status_code == 402:
56
+ raise RuntimeError("Insufficient Nativ credits. Top up at https://dashboard.usenativ.com")
57
+ resp.raise_for_status()
58
+ return resp.json()
59
+
60
+ # -- Translation --------------------------------------------------------
61
+
62
+ async def translate(
63
+ self,
64
+ text: str,
65
+ language: str,
66
+ *,
67
+ language_code: str | None = None,
68
+ source_language: str = "English",
69
+ source_language_code: str = "en",
70
+ context: str | None = None,
71
+ glossary: str | None = None,
72
+ formality: str | None = None,
73
+ max_characters: int | None = None,
74
+ include_tm_info: bool = True,
75
+ backtranslate: bool = False,
76
+ include_rationale: bool = True,
77
+ ) -> dict[str, Any]:
78
+ body: dict[str, Any] = {
79
+ "text": text,
80
+ "language": language,
81
+ "source_language": source_language,
82
+ "source_language_code": source_language_code,
83
+ "tool": "api",
84
+ "include_tm_info": include_tm_info,
85
+ "backtranslate": backtranslate,
86
+ "include_rationale": include_rationale,
87
+ }
88
+ if language_code:
89
+ body["language_code"] = language_code
90
+ if context:
91
+ body["context"] = context
92
+ if glossary:
93
+ body["glossary"] = glossary
94
+ if formality:
95
+ body["formality"] = formality
96
+ if max_characters is not None:
97
+ body["max_characters"] = max_characters
98
+ return await self._request("POST", "/text/culturalize", json_body=body)
99
+
100
+ # -- Languages ----------------------------------------------------------
101
+
102
+ async def get_languages(self) -> dict[str, Any]:
103
+ return await self._request("GET", "/user/languages")
104
+
105
+ # -- Translation Memory -------------------------------------------------
106
+
107
+ async def search_tm(
108
+ self,
109
+ query: str,
110
+ *,
111
+ source_lang: str = "en",
112
+ target_lang: str | None = None,
113
+ score_cutoff: float = 0.0,
114
+ limit: int = 20,
115
+ ) -> dict[str, Any]:
116
+ params: dict[str, Any] = {
117
+ "query": query,
118
+ "source_lang": source_lang,
119
+ "score_cutoff": score_cutoff,
120
+ "limit": limit,
121
+ }
122
+ if target_lang:
123
+ params["target_lang"] = target_lang
124
+ return await self._request("GET", "/master-tm/fuzzy-search", params=params)
125
+
126
+ async def list_tm_entries(
127
+ self,
128
+ *,
129
+ source_lang: str | None = None,
130
+ target_lang: str | None = None,
131
+ search: str | None = None,
132
+ limit: int = 50,
133
+ offset: int = 0,
134
+ ) -> dict[str, Any]:
135
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
136
+ if source_lang:
137
+ params["source_lang"] = source_lang
138
+ if target_lang:
139
+ params["target_lang"] = target_lang
140
+ if search:
141
+ params["search"] = search
142
+ return await self._request("GET", "/master-tm/entries", params=params)
143
+
144
+ async def add_tm_entry(
145
+ self,
146
+ source_text: str,
147
+ target_text: str,
148
+ source_language_code: str,
149
+ target_language_code: str,
150
+ *,
151
+ source_name: str | None = None,
152
+ ) -> dict[str, Any]:
153
+ body = {
154
+ "source_text": source_text,
155
+ "target_text": target_text,
156
+ "source_language_code": source_language_code,
157
+ "target_language_code": target_language_code,
158
+ "information_source": "manual",
159
+ }
160
+ if source_name:
161
+ body["source_name"] = source_name
162
+ return await self._request("POST", "/master-tm/entries", json_body=body)
163
+
164
+ async def get_tm_stats(self) -> dict[str, Any]:
165
+ return await self._request("GET", "/master-tm/stats")
166
+
167
+ # -- Style Guides -------------------------------------------------------
168
+
169
+ async def list_style_guides(self) -> dict[str, Any]:
170
+ return await self._request("GET", "/style-guide")
171
+
172
+ async def get_brand_prompt(self) -> dict[str, Any]:
173
+ return await self._request("GET", "/style-guide/prompt")
174
+
175
+ async def get_combined_prompt(self) -> dict[str, Any]:
176
+ return await self._request("GET", "/style-guide/combined")
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Helpers
181
+ # ---------------------------------------------------------------------------
182
+
183
+ def _get_client() -> NativClient:
184
+ """Build a NativClient from env vars. Raises on missing key."""
185
+ api_key = os.environ.get("NATIV_API_KEY")
186
+ if not api_key:
187
+ raise RuntimeError(
188
+ "NATIV_API_KEY environment variable is required. "
189
+ "Create one at https://dashboard.usenativ.com → Settings → API Keys"
190
+ )
191
+ base_url = os.environ.get("NATIV_API_URL")
192
+ return NativClient(api_key, base_url)
193
+
194
+
195
+ def _fmt_json(data: Any) -> str:
196
+ """Pretty-print JSON for LLM consumption."""
197
+ return json.dumps(data, indent=2, ensure_ascii=False, default=str)
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # MCP Server
202
+ # ---------------------------------------------------------------------------
203
+
204
+ mcp = FastMCP(
205
+ "nativ",
206
+ instructions=(
207
+ "Nativ is an AI-powered localization platform. Use the available tools to "
208
+ "translate text, search translation memory, manage TM entries, and access "
209
+ "style guides. Always use the user's configured languages and respect their "
210
+ "brand voice and style settings."
211
+ ),
212
+ )
213
+
214
+
215
+ # ===================== TOOLS =====================
216
+
217
+
218
+ @mcp.tool()
219
+ async def translate(
220
+ text: str,
221
+ target_language: str,
222
+ target_language_code: str = "",
223
+ source_language: str = "English",
224
+ source_language_code: str = "en",
225
+ context: str = "",
226
+ glossary: str = "",
227
+ formality: str = "",
228
+ max_characters: int = 0,
229
+ backtranslate: bool = False,
230
+ ) -> str:
231
+ """Translate text using Nativ's AI localization engine.
232
+
233
+ Uses the team's translation memory, style guides, and brand voice
234
+ automatically. Returns the translation along with TM match info and
235
+ rationale.
236
+
237
+ Args:
238
+ text: The text to translate.
239
+ target_language: Full target language name (e.g. "French", "German", "Japanese").
240
+ target_language_code: Optional ISO language code (e.g. "fr", "de", "ja").
241
+ source_language: Source language name. Defaults to English.
242
+ source_language_code: Source language code. Defaults to "en".
243
+ context: Optional context to guide the translation (e.g. "marketing headline for Gen Z audience").
244
+ glossary: Optional inline glossary as CSV (e.g. "term,translation\\nbrand,marque").
245
+ formality: Tone override — one of: very_informal, informal, neutral, formal, very_formal.
246
+ max_characters: Optional strict character limit for the translation output.
247
+ backtranslate: If true, also returns a back-translation to verify intent.
248
+ """
249
+ client = _get_client()
250
+ result = await client.translate(
251
+ text=text,
252
+ language=target_language,
253
+ language_code=target_language_code or None,
254
+ source_language=source_language,
255
+ source_language_code=source_language_code,
256
+ context=context or None,
257
+ glossary=glossary or None,
258
+ formality=formality or None,
259
+ max_characters=max_characters if max_characters > 0 else None,
260
+ backtranslate=backtranslate,
261
+ )
262
+
263
+ parts = [f"**Translation ({target_language}):** {result.get('translated_text', '')}"]
264
+
265
+ tm = result.get("tm_match")
266
+ if tm and tm.get("score", 0) > 0:
267
+ parts.append(f"\n**TM Match:** {tm['score']:.0f}% ({tm.get('match_type', 'unknown')}) — source: {tm.get('tm_source', 'N/A')}")
268
+ if tm.get("source_text"):
269
+ parts.append(f" TM reference: \"{tm['source_text']}\" → \"{tm.get('target_text', '')}\"")
270
+
271
+ if result.get("backtranslation"):
272
+ parts.append(f"\n**Back-translation:** {result['backtranslation']}")
273
+
274
+ if result.get("rationale"):
275
+ parts.append(f"\n**Rationale:** {result['rationale']}")
276
+
277
+ return "\n".join(parts)
278
+
279
+
280
+ @mcp.tool()
281
+ async def translate_batch(
282
+ texts: list[str],
283
+ target_language: str,
284
+ target_language_code: str = "",
285
+ source_language: str = "English",
286
+ source_language_code: str = "en",
287
+ context: str = "",
288
+ formality: str = "",
289
+ ) -> str:
290
+ """Translate multiple texts to a single target language.
291
+
292
+ Useful for localizing lists of strings, UI labels, or i18n files.
293
+ Each text is translated individually using the team's TM and style guides.
294
+
295
+ Args:
296
+ texts: List of texts to translate.
297
+ target_language: Full target language name (e.g. "French").
298
+ target_language_code: Optional ISO language code.
299
+ source_language: Source language name. Defaults to English.
300
+ source_language_code: Source language code. Defaults to "en".
301
+ context: Optional context to guide all translations.
302
+ formality: Tone override for all translations.
303
+ """
304
+ client = _get_client()
305
+ results = []
306
+ for i, text in enumerate(texts):
307
+ try:
308
+ result = await client.translate(
309
+ text=text,
310
+ language=target_language,
311
+ language_code=target_language_code or None,
312
+ source_language=source_language,
313
+ source_language_code=source_language_code,
314
+ context=context or None,
315
+ formality=formality or None,
316
+ include_tm_info=True,
317
+ backtranslate=False,
318
+ include_rationale=False,
319
+ )
320
+ translated = result.get("translated_text", "")
321
+ tm = result.get("tm_match")
322
+ tm_note = ""
323
+ if tm and tm.get("score", 0) > 0:
324
+ tm_note = f" (TM {tm['score']:.0f}%)"
325
+ results.append(f"{i+1}. \"{text}\" → \"{translated}\"{tm_note}")
326
+ except Exception as e:
327
+ results.append(f"{i+1}. \"{text}\" → ERROR: {e}")
328
+
329
+ header = f"**Batch translation to {target_language}** ({len(texts)} items):\n"
330
+ return header + "\n".join(results)
331
+
332
+
333
+ @mcp.tool()
334
+ async def search_translation_memory(
335
+ query: str,
336
+ source_language_code: str = "en",
337
+ target_language_code: str = "",
338
+ min_score: float = 0.0,
339
+ limit: int = 10,
340
+ ) -> str:
341
+ """Search the translation memory for existing translations.
342
+
343
+ Use this to check if translations already exist before creating new ones,
344
+ or to find reference translations for consistency.
345
+
346
+ Args:
347
+ query: Text to search for in the translation memory.
348
+ source_language_code: Source language code (default: "en").
349
+ target_language_code: Optional target language code to filter results.
350
+ min_score: Minimum fuzzy match score (0-100). Default 0 returns all.
351
+ limit: Maximum number of results (default 10).
352
+ """
353
+ client = _get_client()
354
+ result = await client.search_tm(
355
+ query=query,
356
+ source_lang=source_language_code,
357
+ target_lang=target_language_code or None,
358
+ score_cutoff=min_score,
359
+ limit=limit,
360
+ )
361
+
362
+ matches = result.get("matches", [])
363
+ if not matches:
364
+ return f"No translation memory matches found for \"{query}\"."
365
+
366
+ lines = [f"**TM Search Results** for \"{query}\" ({len(matches)} matches):\n"]
367
+ for m in matches:
368
+ lines.append(
369
+ f"- **{m['score']:.0f}%** [{m.get('match_type', '')}] "
370
+ f"\"{m['source_text']}\" → \"{m['target_text']}\" "
371
+ f"(source: {m.get('information_source', 'N/A')})"
372
+ )
373
+ return "\n".join(lines)
374
+
375
+
376
+ @mcp.tool()
377
+ async def add_translation_memory_entry(
378
+ source_text: str,
379
+ target_text: str,
380
+ source_language_code: str,
381
+ target_language_code: str,
382
+ name: str = "",
383
+ ) -> str:
384
+ """Add a new entry to the translation memory.
385
+
386
+ Use this to store approved translations so they are reused in future
387
+ localizations.
388
+
389
+ Args:
390
+ source_text: The original text.
391
+ target_text: The approved translation.
392
+ source_language_code: Source language code (e.g. "en").
393
+ target_language_code: Target language code (e.g. "fr-FR").
394
+ name: Optional label for this entry (e.g. "homepage hero copy").
395
+ """
396
+ client = _get_client()
397
+ result = await client.add_tm_entry(
398
+ source_text=source_text,
399
+ target_text=target_text,
400
+ source_language_code=source_language_code,
401
+ target_language_code=target_language_code,
402
+ source_name=name or None,
403
+ )
404
+ return (
405
+ f"Added TM entry: \"{source_text}\" ({source_language_code}) "
406
+ f"→ \"{target_text}\" ({target_language_code})"
407
+ )
408
+
409
+
410
+ @mcp.tool()
411
+ async def get_languages() -> str:
412
+ """Get all languages configured for the Nativ workspace.
413
+
414
+ Returns language names, codes, formality settings, and custom style
415
+ directives for each language.
416
+ """
417
+ client = _get_client()
418
+ result = await client.get_languages()
419
+ languages = result.get("languages", [])
420
+
421
+ if not languages:
422
+ return "No languages configured. Set up languages at https://dashboard.usenativ.com"
423
+
424
+ lines = [f"**Configured Languages** ({len(languages)}):\n"]
425
+ for lang in languages:
426
+ formality = lang.get("formality") or "neutral"
427
+ parts = [f"- **{lang['language']}** (`{lang['language_code']}`) — formality: {formality}"]
428
+ if lang.get("custom_style"):
429
+ parts.append(f" Custom style: {lang['custom_style']}")
430
+ lines.append("\n".join(parts))
431
+ return "\n".join(lines)
432
+
433
+
434
+ @mcp.tool()
435
+ async def get_translation_memory_stats() -> str:
436
+ """Get statistics about the translation memory.
437
+
438
+ Shows total entries, enabled/disabled counts, and breakdown by source type.
439
+ """
440
+ client = _get_client()
441
+ stats = await client.get_tm_stats()
442
+
443
+ lines = [
444
+ "**Translation Memory Statistics:**\n",
445
+ f"- Total entries: {stats.get('total', 0)}",
446
+ f"- Enabled: {stats.get('enabled', 0)}",
447
+ f"- Disabled: {stats.get('disabled', 0)}",
448
+ ]
449
+
450
+ by_source = stats.get("by_source", {})
451
+ if by_source:
452
+ lines.append("\n**By Source:**")
453
+ source_labels = {
454
+ "brand_voice": "Brand Voice",
455
+ "phrase_tm": "Phrase TM",
456
+ "phrase_tb": "Phrase Term Base",
457
+ "user_glossary": "User Glossary",
458
+ "approved": "Approved Translations",
459
+ "manual": "Manual Entries",
460
+ }
461
+ for src, counts in by_source.items():
462
+ label = source_labels.get(src, src)
463
+ lines.append(f"- {label}: {counts.get('total', 0)} entries ({counts.get('enabled', 0)} enabled)")
464
+
465
+ return "\n".join(lines)
466
+
467
+
468
+ @mcp.tool()
469
+ async def get_style_guides() -> str:
470
+ """Get all style guides configured for the workspace.
471
+
472
+ Returns the titles, content, and enabled status of each style guide.
473
+ """
474
+ client = _get_client()
475
+ result = await client.list_style_guides()
476
+ guides = result.get("guides", [])
477
+
478
+ if not guides:
479
+ return "No custom style guides configured. Create them at https://dashboard.usenativ.com → Settings → Style Guides"
480
+
481
+ lines = [f"**Style Guides** ({len(guides)}):\n"]
482
+ for g in guides:
483
+ status = "enabled" if g.get("is_enabled") else "disabled"
484
+ lines.append(f"### {g.get('title', 'Untitled')} ({status})")
485
+ content = g.get("content", "")
486
+ if len(content) > 500:
487
+ content = content[:500] + "..."
488
+ lines.append(content)
489
+ lines.append("")
490
+ return "\n".join(lines)
491
+
492
+
493
+ @mcp.tool()
494
+ async def get_brand_voice() -> str:
495
+ """Get the brand voice prompt — the core localization personality.
496
+
497
+ This is the master prompt that shapes all translations. It captures
498
+ the brand's tone, personality, terminology, and localization guidelines.
499
+ """
500
+ client = _get_client()
501
+ result = await client.get_brand_prompt()
502
+
503
+ if not result.get("exists"):
504
+ return "No brand voice prompt configured. Set one up at https://dashboard.usenativ.com → Settings → Style Guides"
505
+
506
+ prompt = result.get("prompt", "")
507
+ if len(prompt) > 2000:
508
+ return f"**Brand Voice Prompt:**\n\n{prompt[:2000]}...\n\n(Truncated — full prompt has {len(prompt)} characters)"
509
+ return f"**Brand Voice Prompt:**\n\n{prompt}"
510
+
511
+
512
+ # ===================== RESOURCES =====================
513
+
514
+
515
+ @mcp.resource("nativ://languages")
516
+ async def resource_languages() -> str:
517
+ """Configured languages with formality and style settings."""
518
+ client = _get_client()
519
+ result = await client.get_languages()
520
+ return _fmt_json(result)
521
+
522
+
523
+ @mcp.resource("nativ://style-guides")
524
+ async def resource_style_guides() -> str:
525
+ """All style guides including enabled/disabled status."""
526
+ client = _get_client()
527
+ result = await client.list_style_guides()
528
+ return _fmt_json(result)
529
+
530
+
531
+ @mcp.resource("nativ://brand-prompt")
532
+ async def resource_brand_prompt() -> str:
533
+ """The master brand voice prompt used for all translations."""
534
+ client = _get_client()
535
+ result = await client.get_brand_prompt()
536
+ return _fmt_json(result)
537
+
538
+
539
+ @mcp.resource("nativ://tm/stats")
540
+ async def resource_tm_stats() -> str:
541
+ """Translation memory statistics and source breakdown."""
542
+ client = _get_client()
543
+ result = await client.get_tm_stats()
544
+ return _fmt_json(result)
545
+
546
+
547
+ # ===================== PROMPTS =====================
548
+
549
+
550
+ @mcp.prompt()
551
+ def localize_content(
552
+ content: str,
553
+ target_languages: str = "",
554
+ context: str = "",
555
+ ) -> str:
556
+ """Localize content into target languages using Nativ.
557
+
558
+ Args:
559
+ content: The content to localize.
560
+ target_languages: Comma-separated list of target languages (e.g. "French, German, Japanese"). Leave empty to use all configured languages.
561
+ context: Optional context about the content (e.g. "marketing email subject line").
562
+ """
563
+ parts = [
564
+ "You are helping localize content using Nativ's AI localization platform.\n",
565
+ "## Instructions\n",
566
+ "1. First, call `get_languages` to see which languages are configured and their formality/style settings.",
567
+ "2. Call `get_brand_voice` to understand the brand's localization personality.",
568
+ ]
569
+
570
+ if target_languages:
571
+ parts.append(f"3. Translate the content below into: **{target_languages}**")
572
+ else:
573
+ parts.append("3. Translate the content below into **all configured languages**.")
574
+
575
+ parts.extend([
576
+ "4. For each language, use the `translate` tool with the appropriate formality and context.",
577
+ "5. Present results in a clear table: Source | Language | Translation | TM Match %",
578
+ "",
579
+ "## Content to Localize\n",
580
+ content,
581
+ ])
582
+
583
+ if context:
584
+ parts.extend(["", f"## Context\n", context])
585
+
586
+ return "\n".join(parts)
587
+
588
+
589
+ @mcp.prompt()
590
+ def review_translation(
591
+ source_text: str,
592
+ translated_text: str,
593
+ target_language: str,
594
+ ) -> str:
595
+ """Review a translation for quality and consistency with TM and style guides.
596
+
597
+ Args:
598
+ source_text: The original text.
599
+ translated_text: The translation to review.
600
+ target_language: The target language name.
601
+ """
602
+ return "\n".join([
603
+ "You are reviewing a translation for quality using Nativ.\n",
604
+ "## Instructions\n",
605
+ "1. Call `get_brand_voice` to understand the brand's localization guidelines.",
606
+ "2. Call `get_style_guides` to check for any relevant style rules.",
607
+ f"3. Call `search_translation_memory` with the source text to find existing TM matches for {target_language}.",
608
+ "4. Compare the translation against TM matches and style guides.",
609
+ "5. Provide feedback on:",
610
+ " - Accuracy: Does it preserve the original meaning?",
611
+ " - Consistency: Does it match existing TM entries?",
612
+ " - Brand voice: Does it follow style guide rules?",
613
+ " - Tone: Is the formality level appropriate?",
614
+ "6. If the translation is good, suggest adding it to TM with `add_translation_memory_entry`.",
615
+ "",
616
+ f"## Source Text\n{source_text}",
617
+ "",
618
+ f"## Translation ({target_language})\n{translated_text}",
619
+ ])
620
+
621
+
622
+ @mcp.prompt()
623
+ def batch_localize_strings(
624
+ strings: str,
625
+ target_languages: str = "",
626
+ format_hint: str = "json",
627
+ ) -> str:
628
+ """Batch-localize i18n strings for a software project.
629
+
630
+ Args:
631
+ strings: The strings to localize — can be JSON, CSV, or one-per-line.
632
+ target_languages: Comma-separated target languages. Leave empty for all configured.
633
+ format_hint: Expected output format: "json", "csv", or "plain".
634
+ """
635
+ return "\n".join([
636
+ "You are batch-localizing i18n strings using Nativ.\n",
637
+ "## Instructions\n",
638
+ "1. Call `get_languages` to see configured languages.",
639
+ f"2. Parse the strings below (format hint: {format_hint}).",
640
+ "3. Use `translate_batch` for each target language to translate all strings at once.",
641
+ f"4. Output results in **{format_hint}** format, preserving the original keys/structure.",
642
+ "5. If any TM matches are >= 90%, note them — these are well-established translations.",
643
+ "",
644
+ f"## Target Languages\n{target_languages if target_languages else '(all configured languages)'}",
645
+ "",
646
+ f"## Strings\n```\n{strings}\n```",
647
+ ])
648
+
649
+
650
+ # ---------------------------------------------------------------------------
651
+ # Entry point
652
+ # ---------------------------------------------------------------------------
653
+
654
+
655
+ def main():
656
+ mcp.run(transport="stdio")
657
+
658
+
659
+ if __name__ == "__main__":
660
+ main()