altalandi-mcp 0.2.0 → 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.
Files changed (3) hide show
  1. package/package.json +2 -1
  2. package/report.mjs +114 -0
  3. package/server.mjs +15 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altalandi-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server exposing Altalandi (native-Icelandic refinement) as a tool for Claude Code and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "key.mjs",
12
12
  "api-client.mjs",
13
13
  "format-response.mjs",
14
+ "report.mjs",
14
15
  "README.md",
15
16
  "LICENSE"
16
17
  ],
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
@@ -14,13 +14,14 @@ import { z } from "zod";
14
14
  import { requestRefine } from "./api-client.mjs";
15
15
  import { formatRefineResponse, outcomeOf } from "./format-response.mjs";
16
16
  import { resolveKey } from "./key.mjs";
17
+ import { beforeAfterText, reportEnabled, writeReport } from "./report.mjs";
17
18
 
18
19
  const BASE = (process.env.ALTALANDI_BASE_URL || "https://altalandi.is/api").replace(/\/$/, "");
19
- const server = new McpServer({ name: "altalandi", version: "0.2.0" });
20
+ const server = new McpServer({ name: "altalandi", version: "0.3.0" });
20
21
 
21
22
  server.tool(
22
23
  "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
+ "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.",
24
25
  {
25
26
  text: z.string().describe("Text to refine (English or Icelandic)."),
26
27
  domain: z.enum(["general", "marketing", "data-analysis", "childrens-fiction"]).optional()
@@ -28,8 +29,10 @@ server.tool(
28
29
  register: z.enum(["formal", "neutral", "warm"]).optional().describe("Tone. Default neutral."),
29
30
  sourceLang: z.enum(["auto", "en", "is"]).optional()
30
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."),
31
34
  },
32
- async ({ text, domain, register, sourceLang }) => {
35
+ async ({ text, domain, register, sourceLang, report }) => {
33
36
  // Resolved per call, so a login made after the server started is picked up.
34
37
  const { key: KEY } = resolveKey();
35
38
  if (!KEY) {
@@ -47,8 +50,16 @@ server.tool(
47
50
  if (!response.ok) {
48
51
  return { content: [{ type: "text", text: `Altalandi error ${response.status}: ${data.detail || response.statusText}` }], isError: true };
49
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
+ }
50
61
  return {
51
- content: [{ type: "text", text: formatRefineResponse(data) }],
62
+ content: [{ type: "text", text: `${formatRefineResponse(data)}\n\n${beforeAfterText(text, data)}${reportLine}` }],
52
63
  // MCP callers should not treat a refused rewrite (drift, a lost
53
64
  // invariant, or a candidate veto) or an unverified response as a
54
65
  // successfully refined artifact. "No rewrite needed" is a success. The