altalandi-mcp 0.1.1 → 0.3.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 CHANGED
@@ -1,77 +1,71 @@
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.
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.
@@ -1,23 +1,55 @@
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
- }
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,51 @@
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
- }
1
+ {
2
+ "name": "altalandi-mcp",
3
+ "version": "0.3.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
+ "report.mjs",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "test": "node --test test/*.test.mjs"
20
+ },
21
+ "keywords": [
22
+ "icelandic",
23
+ "íslenska",
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "claude",
27
+ "claude-code",
28
+ "codex",
29
+ "altalandi",
30
+ "nlp",
31
+ "translation",
32
+ "agent"
33
+ ],
34
+ "homepage": "https://altalandi.is",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/pbidhjorvar/ritvit.git",
38
+ "directory": "tools/mcp"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/pbidhjorvar/ritvit/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.0.0",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "license": "MIT"
51
+ }
package/report.mjs ADDED
@@ -0,0 +1,114 @@
1
+ // Before/after for people: a terminal view and an optional Markdown report.
2
+ //
3
+ // The website shows each change inline; in a terminal or an editor the
4
+ // before/after got lost. This module renders it the same way everywhere:
5
+ // beforeAfterText() plain (or coloured) block for a terminal or a tool result
6
+ // reportMarkdown() the report file: input, output, every change with its
7
+ // reason, and the verdict
8
+ // writeReport() writes it to ./altalandi-reports/ (or ALTALANDI_REPORT_DIR)
9
+ //
10
+ // Reports are opt-in (--report, or ALTALANDI_REPORT=1): they contain the text.
11
+
12
+ import { mkdir, writeFile } from "node:fs/promises";
13
+ import { join, resolve } from "node:path";
14
+
15
+ const VERDICT = {
16
+ rewritten: "Endurskrifað og merking staðfest",
17
+ refused: "Endurskrift hafnað: upprunalega textanum skilað óbreyttum",
18
+ unchanged: "Engin breyting þurfti",
19
+ unverified: "Merking ekki staðfest: farðu yfir áður en þú notar textann",
20
+ };
21
+
22
+ export function outcomeOf(data) {
23
+ if (data?.rejectionReason) return "refused";
24
+ if (data?.meaningPreserved === false) return "refused";
25
+ if (data?.rewriteAttempted === false) return "unchanged";
26
+ if (data?.meaningPreserved !== true) return "unverified";
27
+ return "rewritten";
28
+ }
29
+
30
+ export function reportEnabled(flag) {
31
+ if (flag !== undefined && flag !== null) return Boolean(flag);
32
+ return ["1", "true", "yes"].includes(String(process.env.ALTALANDI_REPORT || "").toLowerCase());
33
+ }
34
+
35
+ const paint = (on) => ({
36
+ red: (s) => (on ? `\x1b[31m${s}\x1b[0m` : s),
37
+ green: (s) => (on ? `\x1b[32m${s}\x1b[0m` : s),
38
+ dim: (s) => (on ? `\x1b[2m${s}\x1b[0m` : s),
39
+ bold: (s) => (on ? `\x1b[1m${s}\x1b[0m` : s),
40
+ });
41
+
42
+ /** A before/after block with the list of changes. `color` adds ANSI colours. */
43
+ export function beforeAfterText(input, data, { color = false } = {}) {
44
+ const c = paint(color);
45
+ const diffs = Array.isArray(data?.diffs) ? data.diffs : [];
46
+ const rule = (label) => c.dim(`── ${label} ${"─".repeat(Math.max(4, 40 - label.length))}`);
47
+ const lines = [
48
+ rule("Fyrir"), String(input ?? ""), "",
49
+ rule("Eftir"), String(data?.output ?? ""), "",
50
+ rule(`Breytingar (${diffs.length})`),
51
+ ];
52
+ if (!diffs.length) lines.push(c.dim("Engar breytingar."));
53
+ diffs.forEach((d, i) => {
54
+ lines.push(`${i + 1}. ${c.red("− " + String(d.before ?? "").trim())}`);
55
+ lines.push(` ${c.green("+ " + String(d.after ?? "").trim())}`);
56
+ if (d.reason) lines.push(` ${c.dim("↳ " + String(d.reason))}`);
57
+ });
58
+ const q = data?.quality || {};
59
+ lines.push("", c.bold(VERDICT[outcomeOf(data)]) + (q.nativenessScore != null ? c.dim(` · innfæddni ${q.nativenessScore}/100`) : ""));
60
+ return lines.join("\n");
61
+ }
62
+
63
+ const fence = (s) => {
64
+ const text = String(s ?? "");
65
+ const ticks = text.includes("```") ? "````" : "```";
66
+ return `${ticks}text\n${text}\n${ticks}`;
67
+ };
68
+ const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
69
+
70
+ export function reportMarkdown(input, data, options = {}) {
71
+ const q = data?.quality || {};
72
+ const diffs = Array.isArray(data?.diffs) ? data.diffs : [];
73
+ const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
74
+ const tier = data?.billedTier || data?.tier || {};
75
+ const when = new Date().toISOString().replace("T", " ").slice(0, 19);
76
+ const out = [
77
+ `# Altalandi: ${data?.id || "yfirlestur"}`,
78
+ "",
79
+ `| | |`, `|---|---|`,
80
+ `| Tími | ${when} UTC |`,
81
+ `| Niðurstaða | ${VERDICT[outcomeOf(data)]} |`,
82
+ `| Frummál | ${data?.sourceLang || "?"} |`,
83
+ `| Tegund texta | ${options.domain || "general"} |`,
84
+ `| Málsnið | ${options.register || "neutral"} |`,
85
+ `| Innfæddni | ${q.nativenessScore ?? "n/a"}/100 |`,
86
+ `| Ritill | ${tier.writer || "standard"} / ${tier.scoring || "?"} |`,
87
+ `| Auðkenni | ${data?.id || "?"} |`,
88
+ "",
89
+ "## Fyrir", "", fence(input), "",
90
+ "## Eftir", "", fence(data?.output), "",
91
+ `## Breytingar (${diffs.length})`, "",
92
+ ];
93
+ if (diffs.length) {
94
+ out.push("| # | Fyrir | Eftir | Ástæða | Lag |", "|---|---|---|---|---|");
95
+ diffs.forEach((d, i) => out.push(`| ${i + 1} | ${cell(d.before)} | ${cell(d.after)} | ${cell(d.reason)} | ${cell(d.layer)} |`));
96
+ } else {
97
+ out.push("Engar breytingar.");
98
+ }
99
+ if (data?.rejectionReason) out.push("", "## Af hverju var endurskriftinni hafnað", "", cell(data.rejectionReason));
100
+ if (warnings.length) out.push("", "## Viðvaranir", "", ...warnings.map((w) => `- ${cell(w)}`));
101
+ if (data?.meaningNotes) out.push("", "## Athugun á merkingu", "", cell(data.meaningNotes));
102
+ return out.join("\n") + "\n";
103
+ }
104
+
105
+ /** Write the report; returns its path. */
106
+ export async function writeReport(input, data, options = {}, dir) {
107
+ const base = resolve(dir || process.env.ALTALANDI_REPORT_DIR || join(process.cwd(), "altalandi-reports"));
108
+ await mkdir(base, { recursive: true });
109
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
110
+ const id = String(data?.id || "yfirlestur").replace(/[^A-Za-z0-9_-]/g, "");
111
+ const path = join(base, `altalandi-${stamp}-${id}.md`);
112
+ await writeFile(path, reportMarkdown(input, data, options), "utf8");
113
+ return path;
114
+ }
package/server.mjs CHANGED
@@ -1,63 +1,75 @@
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());
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
+ import { beforeAfterText, reportEnabled, writeReport } from "./report.mjs";
18
+
19
+ const BASE = (process.env.ALTALANDI_BASE_URL || "https://altalandi.is/api").replace(/\/$/, "");
20
+ const server = new McpServer({ name: "altalandi", version: "0.3.0" });
21
+
22
+ server.tool(
23
+ "refine_icelandic",
24
+ "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. After every call, show the user the before/after block from the result (Fyrir / Eftir) and the numbered list of changes with their reasons, then mention the report path if one was written. Never replace the user's text silently.",
25
+ {
26
+ text: z.string().describe("Text to refine (English or Icelandic)."),
27
+ domain: z.enum(["general", "marketing", "data-analysis", "childrens-fiction"]).optional()
28
+ .describe("Tunes register/idiom. Default general."),
29
+ register: z.enum(["formal", "neutral", "warm"]).optional().describe("Tone. Default neutral."),
30
+ sourceLang: z.enum(["auto", "en", "is"]).optional()
31
+ .describe("Source-language hint. Default auto."),
32
+ report: z.boolean().optional()
33
+ .describe("Also write a Markdown report (input, output, every change with its reason, verdict) to ./altalandi-reports/. Default: on when ALTALANDI_REPORT=1."),
34
+ },
35
+ async ({ text, domain, register, sourceLang, report }) => {
36
+ // Resolved per call, so a login made after the server started is picked up.
37
+ const { key: KEY } = resolveKey();
38
+ if (!KEY) {
39
+ return { content: [{ type: "text", text:
40
+ "No Altalandi key found. Run `npx altalandi login` once on this computer, or set ALTALANDI_API_KEY." }],
41
+ isError: true };
42
+ }
43
+ try {
44
+ const { response, data } = await requestRefine(BASE, KEY, {
45
+ text,
46
+ domain,
47
+ register,
48
+ sourceLang,
49
+ });
50
+ if (!response.ok) {
51
+ return { content: [{ type: "text", text: `Altalandi error ${response.status}: ${data.detail || response.statusText}` }], isError: true };
52
+ }
53
+ let reportLine = "";
54
+ if (reportEnabled(report)) {
55
+ try {
56
+ reportLine = `\n\nReport: ${await writeReport(text, data, { domain, register })}`;
57
+ } catch (e) {
58
+ reportLine = `\n\nReport could not be written: ${e.message}`;
59
+ }
60
+ }
61
+ return {
62
+ content: [{ type: "text", text: `${formatRefineResponse(data)}\n\n${beforeAfterText(text, data)}${reportLine}` }],
63
+ // MCP callers should not treat a refused rewrite (drift, a lost
64
+ // invariant, or a candidate veto) or an unverified response as a
65
+ // successfully refined artifact. "No rewrite needed" is a success. The
66
+ // full result remains visible so an agent can explain it.
67
+ isError: ["refused", "unverified"].includes(outcomeOf(data)),
68
+ };
69
+ } catch (e) {
70
+ return { content: [{ type: "text", text: `Request failed: ${e.message}` }], isError: true };
71
+ }
72
+ },
73
+ );
74
+
75
+ await server.connect(new StdioServerTransport());