altalandi-mcp 0.1.1 → 0.2.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/README.md +71 -77
- package/format-response.mjs +55 -23
- package/key.mjs +30 -0
- package/package.json +50 -48
- package/server.mjs +64 -63
package/README.md
CHANGED
|
@@ -1,77 +1,71 @@
|
|
|
1
|
-
# altalandi-mcp
|
|
2
|
-
|
|
3
|
-
An [MCP](https://modelcontextprotocol.io) server that exposes
|
|
4
|
-
[Altalandi](https://altalandi.is)
|
|
5
|
-
meaning-preservation check
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
|
60
|
-
|
|
61
|
-
| `
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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 can call. stdio transport.
|
|
6
|
+
|
|
7
|
+
## Set up (Claude Code or Codex)
|
|
8
|
+
|
|
9
|
+
Connect this computer once. This opens altalandi.is, you approve, and the key
|
|
10
|
+
is stored locally. You never copy it:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx altalandi login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then add the server. No key goes into any config file; the server reads the
|
|
17
|
+
one `altalandi login` stored:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
claude mcp add altalandi -s user -- npx -y altalandi-mcp # Claude Code
|
|
21
|
+
codex mcp add altalandi -- npx -y altalandi-mcp # Codex
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Ask for Icelandic and mention Altalandi, e.g. *"Write this in proper Icelandic
|
|
25
|
+
with Altalandi."* The agent calls the **`refine_icelandic`** tool.
|
|
26
|
+
|
|
27
|
+
### Without `altalandi login` (CI, services, other clients)
|
|
28
|
+
|
|
29
|
+
Set `ALTALANDI_API_KEY` in the server's environment, from your host's secret
|
|
30
|
+
manager. It takes precedence over a stored login. Mint a key at
|
|
31
|
+
<https://altalandi.is/portal>. Never commit a plaintext key in MCP JSON.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "mcpServers": { "altalandi": { "command": "npx", "args": ["-y", "altalandi-mcp"] } } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## The tool
|
|
38
|
+
|
|
39
|
+
`refine_icelandic({ text, domain?, register?, sourceLang? })` returns the native
|
|
40
|
+
Icelandic text, a 0–100 nativeness score, an explicit meaning verdict, and every
|
|
41
|
+
warning returned by the API.
|
|
42
|
+
|
|
43
|
+
- `domain`: `general` \| `marketing` \| `data-analysis` \| `childrens-fiction`
|
|
44
|
+
- `register`: `formal` \| `neutral` \| `warm`
|
|
45
|
+
- `sourceLang`: `auto` \| `en` \| `is`
|
|
46
|
+
|
|
47
|
+
**Meaning guarantee:** on Icelandic input where the rewrite can't be verified to
|
|
48
|
+
preserve meaning, the tool returns the **original unchanged** and flags it,
|
|
49
|
+
never a silent change.
|
|
50
|
+
|
|
51
|
+
Only **meaning preserved** is a verified result. **MEANING UNVERIFIED** means the
|
|
52
|
+
check did not run or could not complete; review that output before use. API
|
|
53
|
+
warnings are included under a clear `Warnings:` section in the tool's result.
|
|
54
|
+
|
|
55
|
+
## Config
|
|
56
|
+
|
|
57
|
+
| env | default | purpose |
|
|
58
|
+
|-----|---------|---------|
|
|
59
|
+
| `ALTALANDI_API_KEY` | (the key from `altalandi login`) | your key (Bearer) |
|
|
60
|
+
| `ALTALANDI_CONFIG_DIR` | `%APPDATA%\altalandi` / `~/.config/altalandi` | where `altalandi login` stored it |
|
|
61
|
+
| `ALTALANDI_BASE_URL` | `https://altalandi.is/api` | API base (override for self-hosting) |
|
|
62
|
+
|
|
63
|
+
Each API request has a fixed 300-second deadline, including the response body,
|
|
64
|
+
so a stalled provider cannot leave the tool hanging.
|
|
65
|
+
|
|
66
|
+
## Cost
|
|
67
|
+
|
|
68
|
+
Calls are paid from the account's prepaid credit in krónur (a new account
|
|
69
|
+
starts with trial credit). See <https://altalandi.is/verd>.
|
|
70
|
+
|
|
71
|
+
MIT licensed. Requires Node ≥ 18.
|
package/format-response.mjs
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return
|
|
23
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* What actually happened to the caller's text. `meaningPreserved` alone cannot
|
|
3
|
+
* say: when the candidate veto refuses a broken rewrite, the meaning check has
|
|
4
|
+
* already passed, so `meaningPreserved` is still true while the ORIGINAL comes
|
|
5
|
+
* back. An agent reading "meaning preserved" would then report a rewrite that
|
|
6
|
+
* never shipped.
|
|
7
|
+
*
|
|
8
|
+
* rewritten the output is a verified rewrite
|
|
9
|
+
* refused a rewrite was produced and rejected; the original is returned
|
|
10
|
+
* unchanged the text needed no rewrite
|
|
11
|
+
* unverified meaning could not be established; do not present as verified
|
|
12
|
+
*
|
|
13
|
+
* Responses from older API versions have no `rewriteAttempted` or
|
|
14
|
+
* `rejectionReason`; they fall through to the meaningPreserved verdict, exactly
|
|
15
|
+
* as before.
|
|
16
|
+
*/
|
|
17
|
+
export function outcomeOf(data) {
|
|
18
|
+
if (data?.rejectionReason) return "refused";
|
|
19
|
+
if (data?.meaningPreserved === false) return "refused";
|
|
20
|
+
if (data?.rewriteAttempted === false) return "unchanged";
|
|
21
|
+
if (data?.meaningPreserved !== true) return "unverified";
|
|
22
|
+
return "rewritten";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatRefineResponse(data) {
|
|
26
|
+
const quality = data?.quality || {};
|
|
27
|
+
const outcome = outcomeOf(data);
|
|
28
|
+
const lines = [
|
|
29
|
+
String(data?.output ?? ""),
|
|
30
|
+
"",
|
|
31
|
+
`— nativeness ${quality.nativenessScore ?? "n/a"}/100`,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
if (outcome === "refused") {
|
|
35
|
+
if (data?.meaningPreserved === false) {
|
|
36
|
+
lines.push("— ⚠ MEANING DRIFT — original returned unchanged");
|
|
37
|
+
} else {
|
|
38
|
+
lines.push("— ⚠ REWRITE REFUSED — the original is returned unchanged");
|
|
39
|
+
}
|
|
40
|
+
if (data?.rejectionReason) lines.push(`— reason: ${String(data.rejectionReason)}`);
|
|
41
|
+
} else if (outcome === "unchanged") {
|
|
42
|
+
lines.push("— no rewrite needed — the text is returned as it was");
|
|
43
|
+
} else if (outcome === "unverified") {
|
|
44
|
+
lines.push("— ⚠ MEANING UNVERIFIED — check did not run or could not complete; review before use");
|
|
45
|
+
} else {
|
|
46
|
+
lines.push("— meaning preserved");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
|
|
50
|
+
if (warnings.length) {
|
|
51
|
+
lines.push("", "Warnings:", ...warnings.map((warning) => `- ${String(warning)}`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
package/key.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Where the MCP server finds its API key, in order:
|
|
2
|
+
// 1. ALTALANDI_API_KEY (or ALTALANDI_KEY) in its environment;
|
|
3
|
+
// 2. the key `npx altalandi login` stored on this computer.
|
|
4
|
+
// The second makes the MCP server plug-and-play: after one login, adding it
|
|
5
|
+
// to Claude Code or Codex needs no secret in any config file. The location
|
|
6
|
+
// matches the CLI's (tools/cli/auth.mjs); ALTALANDI_CONFIG_DIR overrides it.
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export function configDir({ env = process.env, home = homedir(), platform = process.platform } = {}) {
|
|
13
|
+
if (env.ALTALANDI_CONFIG_DIR) return env.ALTALANDI_CONFIG_DIR;
|
|
14
|
+
if (platform === "win32") return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "altalandi");
|
|
15
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "altalandi");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** { key, source } where source is "env", "login" or "none". Never throws. */
|
|
19
|
+
export function resolveKey(opts = {}) {
|
|
20
|
+
const env = opts.env ?? process.env;
|
|
21
|
+
const fromEnv = env.ALTALANDI_API_KEY || env.ALTALANDI_KEY;
|
|
22
|
+
if (fromEnv) return { key: fromEnv, source: "env" };
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(readFileSync(path.join(configDir({ ...opts, env }), "credentials.json"), "utf8"));
|
|
25
|
+
if (data && typeof data.apiKey === "string" && data.apiKey) return { key: data.apiKey, source: "login" };
|
|
26
|
+
} catch {
|
|
27
|
+
/* no stored login */
|
|
28
|
+
}
|
|
29
|
+
return { key: "", source: "none" };
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,48 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "altalandi-mcp",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"claude
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "altalandi-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"key.mjs",
|
|
12
|
+
"api-client.mjs",
|
|
13
|
+
"format-response.mjs",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.mjs"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"icelandic",
|
|
22
|
+
"íslenska",
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"claude",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"codex",
|
|
28
|
+
"altalandi",
|
|
29
|
+
"nlp",
|
|
30
|
+
"translation",
|
|
31
|
+
"agent"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://altalandi.is",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/pbidhjorvar/ritvit.git",
|
|
37
|
+
"directory": "tools/mcp"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/pbidhjorvar/ritvit/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
47
|
+
"zod": "^3.23.8"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT"
|
|
50
|
+
}
|
package/server.mjs
CHANGED
|
@@ -1,63 +1,64 @@
|
|
|
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
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
// The key comes from ALTALANDI_API_KEY or, failing that, from the login that
|
|
6
|
+
// `npx altalandi login` stored on this computer, so no config file ever holds
|
|
7
|
+
// a secret:
|
|
8
|
+
// claude mcp add altalandi -s user -- npx -y altalandi-mcp
|
|
9
|
+
// codex mcp add altalandi -- npx -y altalandi-mcp
|
|
10
|
+
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { requestRefine } from "./api-client.mjs";
|
|
15
|
+
import { formatRefineResponse, outcomeOf } from "./format-response.mjs";
|
|
16
|
+
import { resolveKey } from "./key.mjs";
|
|
17
|
+
|
|
18
|
+
const BASE = (process.env.ALTALANDI_BASE_URL || "https://altalandi.is/api").replace(/\/$/, "");
|
|
19
|
+
const server = new McpServer({ name: "altalandi", version: "0.2.0" });
|
|
20
|
+
|
|
21
|
+
server.tool(
|
|
22
|
+
"refine_icelandic",
|
|
23
|
+
"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.",
|
|
24
|
+
{
|
|
25
|
+
text: z.string().describe("Text to refine (English or Icelandic)."),
|
|
26
|
+
domain: z.enum(["general", "marketing", "data-analysis", "childrens-fiction"]).optional()
|
|
27
|
+
.describe("Tunes register/idiom. Default general."),
|
|
28
|
+
register: z.enum(["formal", "neutral", "warm"]).optional().describe("Tone. Default neutral."),
|
|
29
|
+
sourceLang: z.enum(["auto", "en", "is"]).optional()
|
|
30
|
+
.describe("Source-language hint. Default auto."),
|
|
31
|
+
},
|
|
32
|
+
async ({ text, domain, register, sourceLang }) => {
|
|
33
|
+
// Resolved per call, so a login made after the server started is picked up.
|
|
34
|
+
const { key: KEY } = resolveKey();
|
|
35
|
+
if (!KEY) {
|
|
36
|
+
return { content: [{ type: "text", text:
|
|
37
|
+
"No Altalandi key found. Run `npx altalandi login` once on this computer, or set ALTALANDI_API_KEY." }],
|
|
38
|
+
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 a refused rewrite (drift, a lost
|
|
53
|
+
// invariant, or a candidate veto) or an unverified response as a
|
|
54
|
+
// successfully refined artifact. "No rewrite needed" is a success. The
|
|
55
|
+
// full result remains visible so an agent can explain it.
|
|
56
|
+
isError: ["refused", "unverified"].includes(outcomeOf(data)),
|
|
57
|
+
};
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return { content: [{ type: "text", text: `Request failed: ${e.message}` }], isError: true };
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await server.connect(new StdioServerTransport());
|