altalandi-mcp 0.1.0 → 0.1.1
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 -21
- package/README.md +77 -53
- package/api-client.mjs +54 -0
- package/format-response.mjs +23 -0
- package/package.json +48 -21
- package/server.mjs +63 -56
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Altalandi
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Altalandi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,53 +1,77 @@
|
|
|
1
|
-
# altalandi-mcp
|
|
2
|
-
|
|
3
|
-
An [MCP](https://modelcontextprotocol.io) server that exposes
|
|
4
|
-
[Altalandi](https://altalandi.is) — native-Icelandic refinement with a
|
|
5
|
-
meaning-preservation check — as a tool any MCP client (Claude Code, etc.) can
|
|
6
|
-
call. stdio transport.
|
|
7
|
-
|
|
8
|
-
## Use it in Claude Code
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
# altalandi-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that exposes
|
|
4
|
+
[Altalandi](https://altalandi.is) — native-Icelandic refinement with a
|
|
5
|
+
meaning-preservation check — as a tool any MCP client (Claude Code, etc.) can
|
|
6
|
+
call. stdio transport.
|
|
7
|
+
|
|
8
|
+
## Use it in Claude Code
|
|
9
|
+
|
|
10
|
+
Mint a key at <https://altalandi.is/portal>, store it in your password manager,
|
|
11
|
+
then load it without putting the plaintext in shell history:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
read -rsp "Altalandi API key: " ALTALANDI_API_KEY; echo
|
|
15
|
+
export ALTALANDI_API_KEY
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
PowerShell 7: `$env:ALTALANDI_API_KEY = Read-Host "Altalandi API key" -MaskInput`.
|
|
19
|
+
|
|
20
|
+
Launch Claude Code from that shell, then add this secret-free MCP config
|
|
21
|
+
(`.mcp.json` in the project, or your Claude Code config). The server inherits
|
|
22
|
+
the variable from its parent process:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"altalandi": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "altalandi-mcp"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For CI, services, or GUI-launched clients, inject the variable with the host's
|
|
36
|
+
secret manager. Never commit the plaintext key in MCP JSON. Then ask Claude to
|
|
37
|
+
use the **`refine_icelandic`** tool.
|
|
38
|
+
|
|
39
|
+
## The tool
|
|
40
|
+
|
|
41
|
+
`refine_icelandic({ text, domain?, register?, sourceLang? })` → the native
|
|
42
|
+
Icelandic text, a 0–100 nativeness score, an explicit meaning verdict, and every
|
|
43
|
+
warning returned by the API.
|
|
44
|
+
|
|
45
|
+
- `domain`: `general` \| `marketing` \| `data-analysis` \| `childrens-fiction`
|
|
46
|
+
- `register`: `formal` \| `neutral` \| `warm`
|
|
47
|
+
- `sourceLang`: `auto` \| `en` \| `is`
|
|
48
|
+
|
|
49
|
+
**Meaning guarantee:** on Icelandic input where the rewrite can't be verified to
|
|
50
|
+
preserve meaning, the tool returns the **original unchanged** and flags it —
|
|
51
|
+
never a silent change.
|
|
52
|
+
|
|
53
|
+
Only **meaning preserved** is a verified result. **MEANING UNVERIFIED** means the
|
|
54
|
+
check did not run or could not complete; review that output before use. API
|
|
55
|
+
warnings are included under a clear `Warnings:` section in the tool's text result.
|
|
56
|
+
|
|
57
|
+
## Config
|
|
58
|
+
|
|
59
|
+
| env | default | purpose |
|
|
60
|
+
|-----|---------|---------|
|
|
61
|
+
| `ALTALANDI_API_KEY` | — | your key (Bearer). Mint at https://altalandi.is/portal |
|
|
62
|
+
| `ALTALANDI_BASE_URL` | `https://altalandi.is/api` | API base (override for self-hosting) |
|
|
63
|
+
|
|
64
|
+
Each API request has a fixed 300-second deadline, including response-body
|
|
65
|
+
download, so a stalled provider cannot leave the MCP tool hanging forever.
|
|
66
|
+
|
|
67
|
+
## Run standalone
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx -y altalandi-mcp # inherits ALTALANDI_API_KEY; speaks MCP over stdio
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
MIT licensed. Requires Node ≥ 18. Portal keys share a daily account-level
|
|
74
|
+
billable-word budget; operator-issued keys remain per-key. Text and glossary
|
|
75
|
+
fields count; each
|
|
76
|
+
whitespace-delimited token is at least one unit, and an unbroken token uses one
|
|
77
|
+
unit per 20 characters, rounded up.
|
package/api-client.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const REFINE_TIMEOUT_MS = 300_000;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Send one MCP refinement request under a single deadline. The timer remains
|
|
5
|
+
* active while the response body is consumed, since a server can send headers
|
|
6
|
+
* and then stall indefinitely.
|
|
7
|
+
*
|
|
8
|
+
* Timer/fetch injection is intentionally private test plumbing; production
|
|
9
|
+
* callers use the stable 300-second timeout and platform implementations.
|
|
10
|
+
*/
|
|
11
|
+
export async function requestRefine(
|
|
12
|
+
baseUrl,
|
|
13
|
+
apiKey,
|
|
14
|
+
body,
|
|
15
|
+
{
|
|
16
|
+
fetchImpl = globalThis.fetch,
|
|
17
|
+
setTimeoutImpl = globalThis.setTimeout,
|
|
18
|
+
clearTimeoutImpl = globalThis.clearTimeout,
|
|
19
|
+
} = {},
|
|
20
|
+
) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeout = setTimeoutImpl(() => controller.abort(), REFINE_TIMEOUT_MS);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetchImpl(`${baseUrl}/v1/refine`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"content-type": "application/json",
|
|
31
|
+
authorization: `Bearer ${apiKey}`,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
signal: controller.signal,
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (controller.signal.aborted) {
|
|
38
|
+
throw new Error(`Altalandi request timed out after ${REFINE_TIMEOUT_MS} ms.`);
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return { response, data: await response.json() };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (controller.signal.aborted) {
|
|
47
|
+
throw new Error(`Altalandi request timed out after ${REFINE_TIMEOUT_MS} ms.`);
|
|
48
|
+
}
|
|
49
|
+
return { response, data: {} };
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeoutImpl(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function formatRefineResponse(data) {
|
|
2
|
+
const quality = data?.quality || {};
|
|
3
|
+
const lines = [
|
|
4
|
+
String(data?.output ?? ""),
|
|
5
|
+
"",
|
|
6
|
+
`— nativeness ${quality.nativenessScore ?? "n/a"}/100`,
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
if (data?.meaningPreserved === true) {
|
|
10
|
+
lines.push("— meaning preserved");
|
|
11
|
+
} else if (data?.meaningPreserved === false) {
|
|
12
|
+
lines.push("— ⚠ MEANING DRIFT — original returned unchanged");
|
|
13
|
+
} else {
|
|
14
|
+
lines.push("— ⚠ MEANING UNVERIFIED — check did not run or could not complete; review before use");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
|
|
18
|
+
if (warnings.length) {
|
|
19
|
+
lines.push("", "Warnings:", ...warnings.map((warning) => `- ${String(warning)}`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "altalandi-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "MCP server exposing Altalandi (native-Icelandic refinement) as a tool for Claude Code and any MCP client.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "altalandi-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server exposing Altalandi (native-Icelandic refinement) as a tool for Claude Code and any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"altalandi-mcp": "server.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.mjs",
|
|
11
|
+
"api-client.mjs",
|
|
12
|
+
"format-response.mjs",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.test.mjs"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"icelandic",
|
|
21
|
+
"íslenska",
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"claude",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"altalandi",
|
|
27
|
+
"nlp",
|
|
28
|
+
"translation",
|
|
29
|
+
"agent"
|
|
30
|
+
],
|
|
31
|
+
"homepage": "https://altalandi.is",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/pbidhjorvar/ritvit.git",
|
|
35
|
+
"directory": "tools/mcp"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/pbidhjorvar/ritvit/issues"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|
package/server.mjs
CHANGED
|
@@ -1,56 +1,63 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Altalandi MCP server — exposes native-Icelandic refinement as a tool that
|
|
3
|
-
// Claude Code (and any MCP client) can call. stdio transport.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const data = await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
content: [{ type: "text", text:
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Altalandi MCP server — exposes native-Icelandic refinement as a tool that
|
|
3
|
+
// Claude Code (and any MCP client) can call. stdio transport.
|
|
4
|
+
//
|
|
5
|
+
// Load ALTALANDI_API_KEY through the parent environment or a secret manager,
|
|
6
|
+
// then run: npx -y altalandi-mcp
|
|
7
|
+
//
|
|
8
|
+
// Claude Code config (.mcp.json or your Claude Code config) — no plaintext
|
|
9
|
+
// secret required; the child process inherits ALTALANDI_API_KEY:
|
|
10
|
+
// { "mcpServers": { "altalandi": {
|
|
11
|
+
// "command": "npx",
|
|
12
|
+
// "args": ["-y", "altalandi-mcp"] } } }
|
|
13
|
+
|
|
14
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { requestRefine } from "./api-client.mjs";
|
|
18
|
+
import { formatRefineResponse } from "./format-response.mjs";
|
|
19
|
+
|
|
20
|
+
const BASE = (process.env.ALTALANDI_BASE_URL || "https://altalandi.is/api").replace(/\/$/, "");
|
|
21
|
+
const KEY = process.env.ALTALANDI_API_KEY || process.env.ALTALANDI_KEY;
|
|
22
|
+
|
|
23
|
+
const server = new McpServer({ name: "altalandi", version: "0.1.1" });
|
|
24
|
+
|
|
25
|
+
server.tool(
|
|
26
|
+
"refine_icelandic",
|
|
27
|
+
"Rewrite English or stiff/AI-translated Icelandic into native-sounding Icelandic, with a meaning-preservation check. Use whenever you need Icelandic that must read like a native wrote it (marketing copy, UI strings, prose). Returns the refined text, a 0-100 nativeness score, an explicit meaning verdict, and every API warning. Only 'meaning preserved' is verified; 'MEANING UNVERIFIED' requires review. On drift it returns the original unchanged.",
|
|
28
|
+
{
|
|
29
|
+
text: z.string().describe("Text to refine (English or Icelandic)."),
|
|
30
|
+
domain: z.enum(["general", "marketing", "data-analysis", "childrens-fiction"]).optional()
|
|
31
|
+
.describe("Tunes register/idiom. Default general."),
|
|
32
|
+
register: z.enum(["formal", "neutral", "warm"]).optional().describe("Tone. Default neutral."),
|
|
33
|
+
sourceLang: z.enum(["auto", "en", "is"]).optional()
|
|
34
|
+
.describe("Source-language hint. Default auto."),
|
|
35
|
+
},
|
|
36
|
+
async ({ text, domain, register, sourceLang }) => {
|
|
37
|
+
if (!KEY) {
|
|
38
|
+
return { content: [{ type: "text", text: "ALTALANDI_API_KEY is not set for the MCP server." }], isError: true };
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const { response, data } = await requestRefine(BASE, KEY, {
|
|
42
|
+
text,
|
|
43
|
+
domain,
|
|
44
|
+
register,
|
|
45
|
+
sourceLang,
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
return { content: [{ type: "text", text: `Altalandi error ${response.status}: ${data.detail || response.statusText}` }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: formatRefineResponse(data) }],
|
|
52
|
+
// MCP callers should not treat an original-on-drift or unverified
|
|
53
|
+
// response as a successfully refined artifact. The full result remains
|
|
54
|
+
// visible so an agent can explain/review it instead of losing context.
|
|
55
|
+
isError: data.meaningPreserved !== true,
|
|
56
|
+
};
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { content: [{ type: "text", text: `Request failed: ${e.message}` }], isError: true };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await server.connect(new StdioServerTransport());
|