@onereason/svenska-dagar-mcp 0.1.5 → 0.2.1
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 +7 -8
- package/dist/sanitize.js +50 -5
- package/dist/server.js +13 -89
- package/dist/tools.js +46 -0
- package/dist/upstream.js +75 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @onereason/svenska-dagar-mcp
|
|
2
2
|
|
|
3
|
+
[](https://mcptrustchecker.com/registry/onereason-svenska-dagar-mcp)
|
|
4
|
+
|
|
3
5
|
MCP server exposing the Swedish public holiday and calendar API ([sholiday.faboul.se](https://sholiday.faboul.se)) to Claude and other MCP clients.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
@@ -38,7 +40,6 @@ Returns calendar data for a single date.
|
|
|
38
40
|
| `year` | integer | Year (1753–2100) |
|
|
39
41
|
| `month` | integer | Month (1–12) |
|
|
40
42
|
| `day` | integer | Day of month (1–31) |
|
|
41
|
-
| `sanitize` | boolean | See [Security](#security) — default `true` |
|
|
42
43
|
|
|
43
44
|
### `get_swedish_month`
|
|
44
45
|
|
|
@@ -48,7 +49,6 @@ Returns calendar data for every day in a month.
|
|
|
48
49
|
|-----------|------|-------------|
|
|
49
50
|
| `year` | integer | Year (1753–2100) |
|
|
50
51
|
| `month` | integer | Month (1–12) |
|
|
51
|
-
| `sanitize` | boolean | Default `true` |
|
|
52
52
|
|
|
53
53
|
### `get_swedish_year`
|
|
54
54
|
|
|
@@ -57,7 +57,6 @@ Returns calendar data for every day in a year (~365 records). Prefer `get_swedis
|
|
|
57
57
|
| Parameter | Type | Description |
|
|
58
58
|
|-----------|------|-------------|
|
|
59
59
|
| `year` | integer | Year (1753–2100) |
|
|
60
|
-
| `sanitize` | boolean | Default `true` |
|
|
61
60
|
|
|
62
61
|
## Response fields
|
|
63
62
|
|
|
@@ -80,7 +79,8 @@ Each day record contains:
|
|
|
80
79
|
|
|
81
80
|
## Security
|
|
82
81
|
|
|
83
|
-
All responses are sanitized before reaching the LLM
|
|
82
|
+
All responses are sanitized before reaching the LLM. Sanitization cannot be
|
|
83
|
+
disabled through tool arguments:
|
|
84
84
|
|
|
85
85
|
- **Field allowlist** — unknown fields are dropped
|
|
86
86
|
- **Injection scan** — string values are checked against known prompt-injection patterns (ChatML tokens, role prefixes, markdown headers, etc.) and replaced with `[redacted: suspicious content]` if matched
|
|
@@ -88,8 +88,7 @@ All responses are sanitized before reaching the LLM by default:
|
|
|
88
88
|
- **Enum coercion** — `"röd dag"` and `"arbetsfri dag"` are coerced to exactly `"Ja"` or `"Nej"`
|
|
89
89
|
- **Hardcoded flags** — presence-only fields (`klämdagen`, `dag före arbetsfri helgdag`) are always written as `"Ja"` regardless of what the API sends
|
|
90
90
|
- **Non-object responses** — if the upstream returns a string, array, or any non-object, an empty safe structure is returned instead of passing through raw content
|
|
91
|
-
|
|
92
|
-
Pass `sanitize: false` to receive the raw upstream response (useful for debugging).
|
|
91
|
+
- **Bounded upstream data** — non-JSON responses, redirects, payloads over 256 KiB, oversized record lists, invalid day records, and sanitized output over 128 KiB are rejected
|
|
93
92
|
|
|
94
93
|
## Development
|
|
95
94
|
|
|
@@ -99,8 +98,8 @@ npm run build # compile TypeScript → dist/
|
|
|
99
98
|
npm test # run unit + integration tests (requires network for API tests)
|
|
100
99
|
```
|
|
101
100
|
|
|
102
|
-
**Running the server** requires Node.js ≥
|
|
103
|
-
**Running the tests** requires Node.js ≥ 22.6 (`--experimental-strip-types`).
|
|
101
|
+
- **Running the server** requires a supported Node.js release ≥ 22.
|
|
102
|
+
- **Running the tests** requires Node.js ≥ 22.6 (`--experimental-strip-types`).
|
|
104
103
|
|
|
105
104
|
## License
|
|
106
105
|
|
package/dist/sanitize.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
// Legitimate Swedish calendar strings (day names, holiday names) are never
|
|
3
3
|
// longer than this. Anything longer is almost certainly injected content.
|
|
4
4
|
const MAX_STRING_LEN = 100;
|
|
5
|
+
export const MAX_DAY_RECORDS = 366;
|
|
6
|
+
export const MAX_NAME_DAYS_PER_DAY = 10;
|
|
7
|
+
export const MAX_SERIALIZED_BYTES = 128 * 1024;
|
|
8
|
+
export const RESPONSE_DAY_LIMITS = {
|
|
9
|
+
day: { min: 1, max: 1 },
|
|
10
|
+
month: { min: 0, max: 31 },
|
|
11
|
+
year: { min: 0, max: MAX_DAY_RECORDS },
|
|
12
|
+
};
|
|
5
13
|
// Patterns that appear in prompt injection payloads but never in calendar data.
|
|
6
14
|
const INJECTION_PATTERNS = [
|
|
7
15
|
/ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|context|text)/i,
|
|
@@ -34,6 +42,11 @@ function jaOrNej(raw) {
|
|
|
34
42
|
return raw === "Ja" ? "Ja" : "Nej";
|
|
35
43
|
}
|
|
36
44
|
function sanitizeDay(raw) {
|
|
45
|
+
const rawNameDays = raw["namnsdag"];
|
|
46
|
+
if (Array.isArray(rawNameDays)
|
|
47
|
+
&& rawNameDays.length > MAX_NAME_DAYS_PER_DAY) {
|
|
48
|
+
throw new Error("upstream response contains too many name days");
|
|
49
|
+
}
|
|
37
50
|
const day = {
|
|
38
51
|
datum: safeStr(raw["datum"]),
|
|
39
52
|
veckodag: safeStr(raw["veckodag"]),
|
|
@@ -41,8 +54,8 @@ function sanitizeDay(raw) {
|
|
|
41
54
|
"röd dag": jaOrNej(raw["röd dag"]),
|
|
42
55
|
vecka: safeStr(raw["vecka"]),
|
|
43
56
|
"dag i vecka": safeStr(raw["dag i vecka"]),
|
|
44
|
-
namnsdag: Array.isArray(
|
|
45
|
-
?
|
|
57
|
+
namnsdag: Array.isArray(rawNameDays)
|
|
58
|
+
? rawNameDays
|
|
46
59
|
.map((n) => safeStr(n))
|
|
47
60
|
.filter((n) => !n.startsWith("[redacted"))
|
|
48
61
|
: [],
|
|
@@ -61,15 +74,33 @@ function sanitizeDay(raw) {
|
|
|
61
74
|
day["dag före arbetsfri helgdag"] = "Ja";
|
|
62
75
|
return day;
|
|
63
76
|
}
|
|
64
|
-
export function sanitizeCalendarResponse(raw) {
|
|
77
|
+
export function sanitizeCalendarResponse(raw, limits = RESPONSE_DAY_LIMITS.year) {
|
|
78
|
+
if (!Number.isSafeInteger(limits.min)
|
|
79
|
+
|| !Number.isSafeInteger(limits.max)
|
|
80
|
+
|| limits.min < 0
|
|
81
|
+
|| limits.max < limits.min
|
|
82
|
+
|| limits.max > MAX_DAY_RECORDS) {
|
|
83
|
+
throw new Error("invalid response day limits");
|
|
84
|
+
}
|
|
65
85
|
// If the upstream returns a non-object (string, number, array at root level),
|
|
66
86
|
// do NOT pass it through — return an empty safe structure instead.
|
|
67
87
|
// A hijacked API could otherwise inject content via a raw string response
|
|
68
88
|
// that would bypass all field-level sanitization.
|
|
69
89
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
90
|
+
if (limits.min > 0) {
|
|
91
|
+
throw new Error("upstream response contains an unexpected day count");
|
|
92
|
+
}
|
|
70
93
|
return { version: "", startdatum: "", slutdatum: "", dagar: [] };
|
|
71
94
|
}
|
|
72
95
|
const obj = raw;
|
|
96
|
+
const rawDays = obj["dagar"];
|
|
97
|
+
const dayCount = Array.isArray(rawDays) ? rawDays.length : 0;
|
|
98
|
+
if (dayCount < limits.min) {
|
|
99
|
+
throw new Error("upstream response contains an unexpected day count");
|
|
100
|
+
}
|
|
101
|
+
if (dayCount > limits.max) {
|
|
102
|
+
throw new Error("upstream response contains too many day records");
|
|
103
|
+
}
|
|
73
104
|
return {
|
|
74
105
|
// Metadata: keep only the fields an LLM consumer actually needs.
|
|
75
106
|
// Drop "cachetid" (unstable timestamp) and "uri" (mirrors our own request
|
|
@@ -77,8 +108,22 @@ export function sanitizeCalendarResponse(raw) {
|
|
|
77
108
|
version: safeStr(obj["version"]),
|
|
78
109
|
startdatum: safeStr(obj["startdatum"]),
|
|
79
110
|
slutdatum: safeStr(obj["slutdatum"]),
|
|
80
|
-
dagar: Array.isArray(
|
|
81
|
-
?
|
|
111
|
+
dagar: Array.isArray(rawDays)
|
|
112
|
+
? rawDays.map((day) => {
|
|
113
|
+
if (typeof day !== "object"
|
|
114
|
+
|| day === null
|
|
115
|
+
|| Array.isArray(day)) {
|
|
116
|
+
throw new Error("upstream response contains an invalid day record");
|
|
117
|
+
}
|
|
118
|
+
return sanitizeDay(day);
|
|
119
|
+
})
|
|
82
120
|
: [],
|
|
83
121
|
};
|
|
84
122
|
}
|
|
123
|
+
export function serializeCalendarResponse(raw, limits = RESPONSE_DAY_LIMITS.year) {
|
|
124
|
+
const serialized = JSON.stringify(sanitizeCalendarResponse(raw, limits), null, 0);
|
|
125
|
+
if (new TextEncoder().encode(serialized).byteLength > MAX_SERIALIZED_BYTES) {
|
|
126
|
+
throw new Error("sanitized response is too large");
|
|
127
|
+
}
|
|
128
|
+
return serialized;
|
|
129
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -3,105 +3,23 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { validateYear, validateMonth, validateDay } from "./validate.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const TIMEOUT_MS = 10_000;
|
|
10
|
-
// Shared input schema fragment added to every tool.
|
|
11
|
-
const SANITIZE_SCHEMA = {
|
|
12
|
-
sanitize: {
|
|
13
|
-
type: "boolean",
|
|
14
|
-
description: "When true (default), API response values are validated against known " +
|
|
15
|
-
"formats, unknown fields are dropped, and strings are scanned for prompt " +
|
|
16
|
-
"injection patterns before being returned. Set to false only if you need " +
|
|
17
|
-
"the raw upstream response.",
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// HTTP helper — path is always built from validated integers only.
|
|
22
|
-
// No JSONP callback parameter is ever used.
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
async function fetchCalendar(path) {
|
|
25
|
-
const controller = new AbortController();
|
|
26
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
27
|
-
try {
|
|
28
|
-
const response = await fetch(`${BASE_URL}/${path}`, {
|
|
29
|
-
signal: controller.signal,
|
|
30
|
-
});
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw new Error(`upstream returned HTTP ${response.status}`);
|
|
33
|
-
}
|
|
34
|
-
return await response.json();
|
|
35
|
-
}
|
|
36
|
-
finally {
|
|
37
|
-
clearTimeout(timer);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function applyPolicy(data, sanitize) {
|
|
41
|
-
return JSON.stringify(sanitize ? sanitizeCalendarResponse(data) : data, null, 0);
|
|
42
|
-
}
|
|
6
|
+
import { RESPONSE_DAY_LIMITS, serializeCalendarResponse, } from "./sanitize.js";
|
|
7
|
+
import { fetchCalendar } from "./upstream.js";
|
|
8
|
+
import { TOOL_DEFINITIONS } from "./tools.js";
|
|
43
9
|
// ---------------------------------------------------------------------------
|
|
44
10
|
// MCP server
|
|
45
11
|
// ---------------------------------------------------------------------------
|
|
46
|
-
const server = new Server({ name: "svenska-dagar", version: "0.1
|
|
12
|
+
const server = new Server({ name: "svenska-dagar", version: "0.2.1" }, { capabilities: { tools: {} } });
|
|
47
13
|
// Tool descriptions are static strings — no API data is embedded in them.
|
|
48
14
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
49
|
-
tools: [
|
|
50
|
-
{
|
|
51
|
-
name: "get_swedish_day",
|
|
52
|
-
description: "Return Swedish calendar data for a single date: weekday name, week number, " +
|
|
53
|
-
"whether it is a work-free day (arbetsfri dag), a red day (röd dag / public holiday), " +
|
|
54
|
-
"a flag day (flaggdag), name days (namnsdag), holiday name (helgdag), " +
|
|
55
|
-
"holiday eve (helgdagsafton), and squeeze day (klämdagen).",
|
|
56
|
-
inputSchema: {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: {
|
|
59
|
-
year: { type: "integer", description: "Year (1753–2100)" },
|
|
60
|
-
month: { type: "integer", description: "Month (1–12)" },
|
|
61
|
-
day: { type: "integer", description: "Day of month (1–31)" },
|
|
62
|
-
...SANITIZE_SCHEMA,
|
|
63
|
-
},
|
|
64
|
-
required: ["year", "month", "day"],
|
|
65
|
-
additionalProperties: false,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "get_swedish_month",
|
|
70
|
-
description: "Return Swedish calendar data for every day in a calendar month. " +
|
|
71
|
-
"Each day entry contains the same fields as get_swedish_day.",
|
|
72
|
-
inputSchema: {
|
|
73
|
-
type: "object",
|
|
74
|
-
properties: {
|
|
75
|
-
year: { type: "integer", description: "Year (1753–2100)" },
|
|
76
|
-
month: { type: "integer", description: "Month (1–12)" },
|
|
77
|
-
...SANITIZE_SCHEMA,
|
|
78
|
-
},
|
|
79
|
-
required: ["year", "month"],
|
|
80
|
-
additionalProperties: false,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: "get_swedish_year",
|
|
85
|
-
description: "Return Swedish calendar data for every day in a calendar year (~365 records). " +
|
|
86
|
-
"Prefer get_swedish_month when only a subset of the year is needed.",
|
|
87
|
-
inputSchema: {
|
|
88
|
-
type: "object",
|
|
89
|
-
properties: {
|
|
90
|
-
year: { type: "integer", description: "Year (1753–2100)" },
|
|
91
|
-
...SANITIZE_SCHEMA,
|
|
92
|
-
},
|
|
93
|
-
required: ["year"],
|
|
94
|
-
additionalProperties: false,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
],
|
|
15
|
+
tools: [...TOOL_DEFINITIONS],
|
|
98
16
|
}));
|
|
99
17
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
100
18
|
const { name, arguments: args = {} } = request.params;
|
|
101
19
|
const { year, month, day } = args;
|
|
102
|
-
const sanitize = args["sanitize"] !== false;
|
|
103
20
|
try {
|
|
104
21
|
let data;
|
|
22
|
+
let dayLimits;
|
|
105
23
|
if (name === "get_swedish_day") {
|
|
106
24
|
validateYear(year);
|
|
107
25
|
validateMonth(month);
|
|
@@ -110,6 +28,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
110
28
|
const m = month;
|
|
111
29
|
const d = day;
|
|
112
30
|
data = await fetchCalendar(`${y}/${String(m).padStart(2, "0")}/${String(d).padStart(2, "0")}`);
|
|
31
|
+
dayLimits = RESPONSE_DAY_LIMITS.day;
|
|
113
32
|
}
|
|
114
33
|
else if (name === "get_swedish_month") {
|
|
115
34
|
validateYear(year);
|
|
@@ -117,16 +36,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
117
36
|
const y = year;
|
|
118
37
|
const m = month;
|
|
119
38
|
data = await fetchCalendar(`${y}/${String(m).padStart(2, "0")}`);
|
|
39
|
+
dayLimits = RESPONSE_DAY_LIMITS.month;
|
|
120
40
|
}
|
|
121
41
|
else if (name === "get_swedish_year") {
|
|
122
42
|
validateYear(year);
|
|
123
43
|
data = await fetchCalendar(String(year));
|
|
44
|
+
dayLimits = RESPONSE_DAY_LIMITS.year;
|
|
124
45
|
}
|
|
125
46
|
else {
|
|
126
47
|
throw new Error(`unknown tool: ${name}`);
|
|
127
48
|
}
|
|
128
49
|
return {
|
|
129
|
-
content: [{
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: serializeCalendarResponse(data, dayLimits),
|
|
53
|
+
}],
|
|
130
54
|
};
|
|
131
55
|
}
|
|
132
56
|
catch (err) {
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const TOOL_DEFINITIONS = [
|
|
2
|
+
{
|
|
3
|
+
name: "get_swedish_day",
|
|
4
|
+
description: "Return Swedish calendar data for a single date: weekday name, week number, " +
|
|
5
|
+
"whether it is a work-free day (arbetsfri dag), a red day (röd dag / public holiday), " +
|
|
6
|
+
"a flag day (flaggdag), name days (namnsdag), holiday name (helgdag), " +
|
|
7
|
+
"holiday eve (helgdagsafton), and squeeze day (klämdagen).",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
year: { type: "integer", description: "Year (1753–2100)" },
|
|
12
|
+
month: { type: "integer", description: "Month (1–12)" },
|
|
13
|
+
day: { type: "integer", description: "Day of month (1–31)" },
|
|
14
|
+
},
|
|
15
|
+
required: ["year", "month", "day"],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "get_swedish_month",
|
|
21
|
+
description: "Return Swedish calendar data for every day in a calendar month. " +
|
|
22
|
+
"Each day entry contains the same fields as get_swedish_day.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
year: { type: "integer", description: "Year (1753–2100)" },
|
|
27
|
+
month: { type: "integer", description: "Month (1–12)" },
|
|
28
|
+
},
|
|
29
|
+
required: ["year", "month"],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "get_swedish_year",
|
|
35
|
+
description: "Return Swedish calendar data for every day in a calendar year (~365 records). " +
|
|
36
|
+
"Prefer get_swedish_month when only a subset of the year is needed.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
year: { type: "integer", description: "Year (1753–2100)" },
|
|
41
|
+
},
|
|
42
|
+
required: ["year"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
];
|
package/dist/upstream.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const BASE_URL = "https://sholiday.faboul.se/dagar/v2.1";
|
|
2
|
+
const TIMEOUT_MS = 10_000;
|
|
3
|
+
export const MAX_RESPONSE_BYTES = 256 * 1024;
|
|
4
|
+
export async function readJsonResponse(response) {
|
|
5
|
+
if (!response.ok) {
|
|
6
|
+
throw new Error(`upstream returned HTTP ${response.status}`);
|
|
7
|
+
}
|
|
8
|
+
const mediaType = response.headers
|
|
9
|
+
.get("content-type")
|
|
10
|
+
?.split(";", 1)[0]
|
|
11
|
+
?.trim()
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
if (mediaType !== "application/json") {
|
|
14
|
+
throw new Error("upstream returned an unexpected content type");
|
|
15
|
+
}
|
|
16
|
+
const contentLength = response.headers.get("content-length");
|
|
17
|
+
if (contentLength !== null) {
|
|
18
|
+
const declaredLength = Number(contentLength);
|
|
19
|
+
if (!Number.isSafeInteger(declaredLength) || declaredLength < 0) {
|
|
20
|
+
throw new Error("upstream returned an invalid content length");
|
|
21
|
+
}
|
|
22
|
+
if (declaredLength > MAX_RESPONSE_BYTES) {
|
|
23
|
+
throw new Error("upstream response is too large");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (response.body === null) {
|
|
27
|
+
throw new Error("upstream returned an empty response");
|
|
28
|
+
}
|
|
29
|
+
const reader = response.body.getReader();
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let totalBytes = 0;
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
const { done, value } = await reader.read();
|
|
35
|
+
if (done)
|
|
36
|
+
break;
|
|
37
|
+
totalBytes += value.byteLength;
|
|
38
|
+
if (totalBytes > MAX_RESPONSE_BYTES) {
|
|
39
|
+
await reader.cancel();
|
|
40
|
+
throw new Error("upstream response is too large");
|
|
41
|
+
}
|
|
42
|
+
chunks.push(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
reader.releaseLock();
|
|
47
|
+
}
|
|
48
|
+
const body = new Uint8Array(totalBytes);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
body.set(chunk, offset);
|
|
52
|
+
offset += chunk.byteLength;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(new TextDecoder().decode(body));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error("upstream returned invalid JSON");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function fetchCalendar(path) {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${BASE_URL}/${path}`, {
|
|
66
|
+
headers: { accept: "application/json" },
|
|
67
|
+
redirect: "error",
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
return await readJsonResponse(response);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onereason/svenska-dagar-mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server for the Swedish public holiday and calendar API (sholiday.faboul.se)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"svenska-dagar-mcp": "dist/server.js"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=22"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"build": "tsc && chmod 755 dist/server.js",
|
|
33
33
|
"prepublishOnly": "npm run build",
|
|
34
34
|
"start": "node dist/server.js",
|
|
35
|
-
"test": "node --experimental-strip-types --test test/unit.test.ts test/sanitize.test.ts test/api.test.ts"
|
|
35
|
+
"test": "node --experimental-strip-types --test test/unit.test.ts test/sanitize.test.ts test/security-boundary.test.ts test/api.test.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "1.30.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/node": "
|
|
42
|
-
"typescript": "
|
|
41
|
+
"@types/node": "22.20.3",
|
|
42
|
+
"typescript": "7.0.2"
|
|
43
43
|
}
|
|
44
44
|
}
|