enrico-mcp 1.0.1 → 1.0.3
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 +2 -0
- package/dist/api.js +86 -0
- package/dist/server.js +30 -4
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/api.js
CHANGED
|
@@ -29,3 +29,89 @@ export async function holidays(args) {
|
|
|
29
29
|
return `* ${date}: ${h.name?.[0]?.text ?? '?'}`;
|
|
30
30
|
}).join('\n');
|
|
31
31
|
}
|
|
32
|
+
// ---- muxE tools:
|
|
33
|
+
// The two existing tools print a country's code and name, or one year of its
|
|
34
|
+
// public holidays. The new ones use the parts of the payload neither reads: the
|
|
35
|
+
// coverage window and holiday type list every country record carries, and the
|
|
36
|
+
// non public holiday types the service also serves per country.
|
|
37
|
+
const ENRICO = "https://kayaposoft.com/enrico/json/v2.0";
|
|
38
|
+
const ENRICO_UA = "mrfentmen-enrico-mcp/1.0";
|
|
39
|
+
async function enricoJson(params) {
|
|
40
|
+
const qs = new URLSearchParams(params).toString();
|
|
41
|
+
const res = await fetch(`${ENRICO}/?${qs}`, {
|
|
42
|
+
headers: { "User-Agent": ENRICO_UA, Accept: "application/json" },
|
|
43
|
+
signal: AbortSignal.timeout(30000),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error(`Enrico returned ${res.status}`);
|
|
47
|
+
const raw = await res.text();
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("Enrico returned a body that is not JSON.");
|
|
54
|
+
}
|
|
55
|
+
if (parsed && typeof parsed.error === "string")
|
|
56
|
+
throw new Error(`Enrico said: ${parsed.error}`);
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
const isoDate = (d) => {
|
|
60
|
+
if (!d || typeof d.year !== "number")
|
|
61
|
+
return "not reported";
|
|
62
|
+
// 52 of the 60 country records carry 32767 as a placeholder end year, verified
|
|
63
|
+
// live, so a real date is not printed for it
|
|
64
|
+
if (d.year >= 30000)
|
|
65
|
+
return "open ended, the record carries a placeholder year";
|
|
66
|
+
return `${d.year}-${String(d.month ?? 1).padStart(2, "0")}-${String(d.day ?? 1).padStart(2, "0")}`;
|
|
67
|
+
};
|
|
68
|
+
/** the name array is localised, so pick English when the service offers it */
|
|
69
|
+
const localised = (names) => {
|
|
70
|
+
if (!Array.isArray(names) || !names.length)
|
|
71
|
+
return "name not reported";
|
|
72
|
+
const english = names.find((n) => String(n?.lang ?? "").toLowerCase().startsWith("en"));
|
|
73
|
+
if (english?.text)
|
|
74
|
+
return english.text;
|
|
75
|
+
const first = names.find((n) => n?.text);
|
|
76
|
+
const others = names.length - 1;
|
|
77
|
+
return `${first?.text ?? "name not reported"}${others > 0 ? ` (only ${names.length} translation${names.length === 1 ? "" : "s"}; ${names.map((n) => n?.lang ?? "?").join(", ")})` : ""}`;
|
|
78
|
+
};
|
|
79
|
+
const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
80
|
+
export async function countryCoverage(_args = {}) {
|
|
81
|
+
const list = await enricoJson({ action: "getSupportedCountries" });
|
|
82
|
+
const countries = Array.isArray(list) ? list : [];
|
|
83
|
+
if (!countries.length)
|
|
84
|
+
throw new Error("Enrico returned no countries.");
|
|
85
|
+
const typeCount = new Map();
|
|
86
|
+
for (const c of countries) {
|
|
87
|
+
for (const t of c.holidayTypes ?? [])
|
|
88
|
+
typeCount.set(t, (typeCount.get(t) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
const fromYears = countries
|
|
91
|
+
.map((c) => c.fromDate?.year)
|
|
92
|
+
.filter((y) => typeof y === "number");
|
|
93
|
+
const bounded = countries.filter((c) => typeof c.toDate?.year === "number" && c.toDate.year < 30000);
|
|
94
|
+
const withRegions = countries.filter((c) => Array.isArray(c.regions) && c.regions.length > 0);
|
|
95
|
+
const lines = [
|
|
96
|
+
`What the Enrico holiday service actually covers, across its ${countries.length} countries:`,
|
|
97
|
+
` every country offers: ${[...typeCount.entries()].filter(([, n]) => n === countries.length).map(([t]) => t).join(", ") || "none of them"}`,
|
|
98
|
+
` distinct holiday types in the catalogue: ${typeCount.size}`,
|
|
99
|
+
];
|
|
100
|
+
for (const [type, n] of [...typeCount.entries()].sort((a, b) => b[1] - a[1])) {
|
|
101
|
+
const names = countries.filter((c) => (c.holidayTypes ?? []).includes(type)).map((c) => (c.countryCode ?? "?").toUpperCase());
|
|
102
|
+
const shown = names.slice(0, 12).join(", ");
|
|
103
|
+
lines.push(` ${type}: ${n} of ${countries.length} countries${names.length ? ` (${shown}${names.length > 12 ? `, and ${names.length - 12} more` : ""})` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push(` countries that publish a start year: ${fromYears.length}, earliest ${fromYears.length ? Math.min(...fromYears) : "not reported"}, latest ${fromYears.length ? Math.max(...fromYears) : "not reported"}`);
|
|
106
|
+
lines.push(` countries whose records stop: ${bounded.length ? `${bounded.length} name a real end year (${[...new Set(bounded.map((c) => c.toDate?.year))].sort((a, b) => (a ?? 0) - (b ?? 0)).join(", ")}), the other ${countries.length - bounded.length} carry a placeholder end year` : `none name a real end year, all ${countries.length} carry a placeholder`}`);
|
|
107
|
+
lines.push(` countries split into regions: ${withRegions.length}`);
|
|
108
|
+
for (const c of withRegions.slice(0, 8)) {
|
|
109
|
+
lines.push(` ${(c.countryCode ?? "?").toUpperCase()} ${c.fullName ?? ""}: ${(c.regions ?? []).length} region code(s) - ${(c.regions ?? []).join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
if (withRegions.length > 8)
|
|
112
|
+
lines.push(` and ${withRegions.length - 8} more region-split countries`);
|
|
113
|
+
const longest = [...countries].sort((a, b) => (b.holidayTypes ?? []).length - (a.holidayTypes ?? []).length)[0];
|
|
114
|
+
lines.push(` richest country record: ${(longest.countryCode ?? "?").toUpperCase()} ${longest.fullName ?? ""} with ${(longest.holidayTypes ?? []).length} holiday types, covering ${isoDate(longest.fromDate)} to ${isoDate(longest.toDate)}`);
|
|
115
|
+
lines.push(" the countries tool prints only the code and the name and stops at 30 of them, so none of this is visible there.");
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { countries } from "./api.js";
|
|
4
|
-
import { holidays } from "./api.js";
|
|
3
|
+
import { countries, holidays, countryCoverage } from "./api.js";
|
|
5
4
|
const text = (value) => ({ content: [{ type: "text", text: value }] });
|
|
6
5
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
7
6
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -18,7 +17,7 @@ export function createServer() {
|
|
|
18
17
|
return text(await countries());
|
|
19
18
|
}
|
|
20
19
|
catch (e) {
|
|
21
|
-
return textError(
|
|
20
|
+
return textError(String(e));
|
|
22
21
|
}
|
|
23
22
|
});
|
|
24
23
|
server.registerTool("holidays", {
|
|
@@ -31,7 +30,34 @@ export function createServer() {
|
|
|
31
30
|
return text(await holidays(args));
|
|
32
31
|
}
|
|
33
32
|
catch (e) {
|
|
34
|
-
return textError(
|
|
33
|
+
return textError(String(e));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
server.registerTool("country_coverage", {
|
|
37
|
+
title: "Country coverage",
|
|
38
|
+
description: "What the Enrico holiday service actually covers across all its countries: every holiday type in the catalogue and how many countries offer each, the earliest year any country starts at, which countries name a real end year rather than a placeholder, and which countries are split into regions. The countries tool prints only a code and a name and stops at 30 of them.",
|
|
39
|
+
inputSchema: z.object({}),
|
|
40
|
+
annotations: READ_ONLY,
|
|
41
|
+
}, async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
return text(await countryCoverage());
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return textError(String(e));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
server.registerTool("country_calendar", {
|
|
50
|
+
title: "Country calendar",
|
|
51
|
+
description: "Every holiday type Enrico serves for one country in one year, not just public holidays: observances, other days, extra working days, school holidays and postal holidays, with the English name where the record carries one, the weekday, the country's real coverage window and its region codes. The holidays tool reads a single type and takes only the first name in each record.",
|
|
52
|
+
inputSchema: z.object({ country: z.string().describe("Three letter country code like USA or DEU."), year: z.number().describe("Year, 1940 to 2100.") }),
|
|
53
|
+
annotations: READ_ONLY,
|
|
54
|
+
}, async (args) => {
|
|
55
|
+
// TODO: implement calendar locally (paywall removed)
|
|
56
|
+
try {
|
|
57
|
+
return textError("Not yet implemented: calendar");
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return textError(String(e));
|
|
35
61
|
}
|
|
36
62
|
});
|
|
37
63
|
return server;
|