altalandi-mcp 0.7.10 → 0.8.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
@@ -62,6 +62,17 @@ what changed and what it cost in the terminal or the editor, not only on the web
62
62
  preserve meaning, the tool returns the **original unchanged** and flags it,
63
63
  never a silent change.
64
64
 
65
+ ## UI strings: the Hnappasafn
66
+
67
+ `lookup_icelandic_ui_strings({ strings, minConfidence? })` looks up English buttons,
68
+ menus and labels (up to 200, 80 characters each) in the
69
+ [Hnappasafn](https://altalandi.is/hnappasafn): the Icelandic that Icelandic websites
70
+ and open-source software (Firefox, Thunderbird, LibreOffice, Android) actually use,
71
+ served only where the sources agree. No model runs, so it is instant and the same
72
+ string gets the same Icelandic across a project. **1 kr per string found**, from the
73
+ free daily characters first; a string not found costs nothing. Translate the misses
74
+ with `refine_icelandic`.
75
+
65
76
  ## Proofreading
66
77
 
67
78
  `proofread_icelandic({ text, suggestions?, minPrecision?, noStore? })` fixes only
package/api-client.mjs CHANGED
@@ -83,3 +83,15 @@ export async function requestSuggestionDecisions(baseUrl, apiKey, body, { fetchI
83
83
  return { ok: false, error: error.message };
84
84
  }
85
85
  }
86
+
87
+ /** Look up English UI strings in the Hnappasafn (no model; 1 kr per string
88
+ * found, misses free). Returns {response, data}; a network failure throws. */
89
+ export async function requestHnappasafn(baseUrl, apiKey, body, { fetchImpl = globalThis.fetch } = {}) {
90
+ const response = await fetchImpl(`${baseUrl}/v1/hnappasafn`, {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
93
+ body: JSON.stringify(body),
94
+ signal: AbortSignal.timeout(30_000),
95
+ });
96
+ return { response, data: await response.json().catch(() => ({})) };
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altalandi-mcp",
3
- "version": "0.7.10",
3
+ "version": "0.8.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": {
package/report.mjs CHANGED
@@ -89,6 +89,7 @@ export function reportMarkdown(input, data, options = {}) {
89
89
  `| Innfæddni | ${q.nativenessScore ?? "n/a"}/100 |`,
90
90
  `| Ritill | ${tier.writer || "standard"} / ${tier.scoring || "?"} |`,
91
91
  `| Auðkenni | ${data?.runUrl ? `[${data.id}](${data.runUrl})` : data?.id || "?"} |`,
92
+ ...(data?.jobUrl ? [`| Verkið | ${data.jobUrl} |`] : []),
92
93
  "",
93
94
  "## Fyrir", "", fence(input), "",
94
95
  "## Eftir", "", fence(data?.output), "",
@@ -335,7 +336,9 @@ export function reviewMarkdown(input, data, { before = null, after = null, maxCh
335
336
 
336
337
  out.push("**Kostnaður**", ...costLines(data, before, after));
337
338
  // The run's own page: the text sent, the result, the checks and the review.
338
- if (data?.runUrl) out.push(`- Verkið: ${data.runUrl}`);
339
+ // A call sent as part of a job (--job) links the job first, then this part.
340
+ if (data?.jobUrl) out.push(`- Verkið: ${data.jobUrl}`);
341
+ if (data?.runUrl) out.push(`- ${data?.jobUrl ? "Hlutinn" : "Verkið"}: ${data.runUrl}`);
339
342
  else if (data?.id) out.push(`- Auðkenni: \`${data.id}\``);
340
343
  return out.join("\n") + "\n";
341
344
  }
@@ -362,6 +365,18 @@ const count = (n) => formatInt(n || 0);
362
365
  /** The one summary of a folder or site run: every file, every changed passage
363
366
  * (before and after, with the meaning verdict) and, under their own heading,
364
367
  * the passages that keep the original text because a check did not pass. */
368
+ /** A job's classification (POST /v1/jobs) as one line: "Vefsíða · Bókhald",
369
+ * the kind and the field names. null when it is not known: uncertain, a
370
+ * fallback, or nothing named. */
371
+ export function jobTypeText(classification) {
372
+ const c = classification;
373
+ if (!c || typeof c !== "object" || c.uncertain || c.source === "fallback") return null;
374
+ const name = (x) => String(x?.name || x?.label || x?.code || "").replace(/\s+/g, " ").trim();
375
+ const kind = c.kind ? name(c.kind) : "";
376
+ const fields = (Array.isArray(c.fields) ? c.fields : []).map(name).filter(Boolean).join(", ");
377
+ return [kind, fields].filter(Boolean).join(" · ") || null;
378
+ }
379
+
365
380
  export function batchReportMarkdown(summary) {
366
381
  const t = summary?.totals || {};
367
382
  const files = Array.isArray(summary?.files) ? summary.files : [];
@@ -386,8 +401,12 @@ export function batchReportMarkdown(summary) {
386
401
  `| Stafir sendir | ${count(t.characters)} |`,
387
402
  ...(Number.isFinite(t.billedCharacters)
388
403
  ? [`| Stafir rukkaðir | ${t.billedCharactersApprox ? "um " : ""}${count(t.billedCharacters)} |`] : []),
404
+ // The job's kind and fields, as the engine classified them (when it knows).
405
+ ...(jobTypeText(summary?.classification) ? [`| Tegund verks | ${cell(jobTypeText(summary.classification))} |`] : []),
389
406
  `| Tegund texta | ${cell(summary?.domain || "general")} |`,
390
407
  `| Málsnið | ${cell(summary?.register || "neutral")} |`,
408
+ // The run's one job page (an engine that knows jobs); never one link per call.
409
+ ...(summary?.jobUrl ? [`| Verkið | ${summary.jobUrl} |`] : []),
391
410
  ];
392
411
 
393
412
  if (summary?.stopped) {
package/server.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
16
  import { z } from "zod";
17
- import { requestMe, requestRefine, requestSuggestionDecisions } from "./api-client.mjs";
17
+ import { requestHnappasafn, requestMe, requestRefine, requestSuggestionDecisions } from "./api-client.mjs";
18
18
  import { CARD_HTML, RENDER_META, UI_META, UI_MIME, UI_URI, UI_URI_ALIASES, connectCard, failureCard, proofreadCard, refineCard } from "./card.mjs";
19
19
  import { beginLogin } from "./device.mjs";
20
20
  import { formatProofreadResponse, outcomeOf } from "./format-response.mjs";
@@ -22,7 +22,7 @@ import { resolveKey } from "./key.mjs";
22
22
  import { formatIsk, freeCharsText, reportEnabled, reviewMarkdown, writeReport } from "./report.mjs";
23
23
 
24
24
  const BASE = (process.env.ALTALANDI_BASE_URL || "https://altalandi.is/api").replace(/\/$/, "");
25
- const server = new McpServer({ name: "altalandi", version: "0.7.10" });
25
+ const server = new McpServer({ name: "altalandi", version: "0.8.0" });
26
26
 
27
27
  const CARD_NOTE = " When the host shows Altalandi's card, the card already shows this: do not repeat it, add at most a short sentence.";
28
28
 
@@ -185,6 +185,39 @@ server.registerTool(
185
185
  },
186
186
  );
187
187
 
188
+ server.registerTool(
189
+ "lookup_icelandic_ui_strings",
190
+ {
191
+ title: "Look up Icelandic UI strings",
192
+ description: "Look up English user-interface strings (buttons, menus, labels, short messages) in the Hnappasafn: the Icelandic that Icelandic websites and software (Firefox, LibreOffice, Android) actually use, e.g. 'Search' -> 'Leita', 'Save changes' -> 'Vista breytingar'. Only strings the sources agree on are returned, each with its confidence and alternatives. No model runs, so it is instant and consistent across a project. Costs 1 kr per string found, from the account's free allowance first; strings not found cost nothing: translate those with refine_icelandic. Use it first for app and website strings, up to 200 at a time. Never purchases credit.",
193
+ inputSchema: {
194
+ strings: z.array(z.string().min(1).max(80)).min(1).max(200).describe("English UI strings, exactly as they appear in the interface."),
195
+ minConfidence: z.enum(["high", "medium"]).optional().describe("'high' returns only strings the most sources agree on. Default medium."),
196
+ },
197
+ },
198
+ async ({ strings, minConfidence }) => {
199
+ const { key: KEY } = resolveKey();
200
+ if (!KEY) return connectFirst();
201
+ let r;
202
+ try {
203
+ r = await requestHnappasafn(BASE, KEY, { strings, ...(minConfidence ? { minConfidence } : {}) });
204
+ } catch (e) {
205
+ return requestFailed(e);
206
+ }
207
+ if (!r.response.ok) return httpError(r.response, r.data);
208
+ const results = r.data.results ?? [];
209
+ const found = results.filter((x) => x.found);
210
+ const missing = results.filter((x) => !x.found).map((x) => x.en);
211
+ const lines = found.map((x) => `- ${x.en} → **${x.is}**` + (x.alternatives?.length ? ` (eða: ${x.alternatives.join(", ")})` : "")
212
+ + (x.confidence === "medium" ? " · _medium_" : ""));
213
+ let text = lines.length ? `**Hnappasafn:** ${found.length} of ${results.length} found` +
214
+ (typeof r.data.chargedIsk === "number" ? ` · ${formatIsk(r.data.chargedIsk)}` : "") + "\n\n" + lines.join("\n")
215
+ : "None of these strings are in the Hnappasafn.";
216
+ if (missing.length) text += `\n\nNot in the library (translate these with refine_icelandic): ${missing.join("; ")}`;
217
+ return { content: [{ type: "text", text }], structuredContent: r.data };
218
+ },
219
+ );
220
+
188
221
  server.tool(
189
222
  "altalandi_status",
190
223
  "The connected Altalandi account: balance, today's free characters left, and where to add credit. Use when the person asks about their balance or credit, or before a large batch.",