From 78a6007afdeeb4b777648e6e4d657a2cfdded7d1 Mon Sep 17 00:00:00 2001 From: Simon Franken Date: Fri, 20 Mar 2026 16:12:36 +0100 Subject: [PATCH] fix calendar discovery parsing and load env config --- .gitignore | 2 + package-lock.json | 13 +++++++ package.json | 1 + src/caldav/client.ts | 60 ++++++++++++++++++++++++++--- src/index.ts | 1 + tests/caldav-client-parsing.test.ts | 32 +++++++++++++++ 6 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 tests/caldav-client-parsing.test.ts diff --git a/.gitignore b/.gitignore index a013ea1..b92834c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ Thumbs.db .idea/ .vscode/ + +opencode.json diff --git a/package-lock.json b/package-lock.json index 26ac777..372c285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.13.3", + "dotenv": "^16.4.7", "fast-xml-parser": "^4.5.0", "zod": "^4.1.11" }, @@ -1265,6 +1266,18 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 0b9bd49..8149073 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.13.3", + "dotenv": "^16.4.7", "fast-xml-parser": "^4.5.0", "zod": "^4.1.11" }, diff --git a/src/caldav/client.ts b/src/caldav/client.ts index a389b8d..afc78a9 100644 --- a/src/caldav/client.ts +++ b/src/caldav/client.ts @@ -110,16 +110,14 @@ export class CaldavClient { if (!ok) { return undefined; } - const resourceType = String(ok.props.resourcetype ?? ""); - if (!resourceType.toLowerCase().includes("calendar")) { + if (!hasDavNode(ok.props.resourcetype, "calendar")) { return undefined; } - const displayName = String(ok.props.displayname ?? normalizeIdFromHref(href)); + const displayName = toDavText(ok.props.displayname) ?? normalizeIdFromHref(href); const etag = normalizeEtag(ok.props.getetag); - const writable = String(ok.props["current-user-privilege-set"] ?? "").toLowerCase().includes("write"); - const componentsRaw = String(ok.props["supported-calendar-component-set"] ?? ""); - const components = ["VEVENT", "VTODO", "VJOURNAL"].filter((component) => componentsRaw.includes(component)); + const writable = hasDavNode(ok.props["current-user-privilege-set"], "write"); + const components = ["VEVENT", "VTODO", "VJOURNAL"].filter((component) => hasDavNode(ok.props["supported-calendar-component-set"], component)); const calendar: CalendarInfo = { id: normalizeIdFromHref(href), @@ -391,3 +389,53 @@ function normalizeEtag(input: unknown): string | undefined { } return String(input).trim(); } + +export function hasDavNode(value: unknown, nodeName: string): boolean { + const target = nodeName.toLowerCase(); + if (value === null || value === undefined) { + return false; + } + if (typeof value === "string") { + return value.toLowerCase().includes(target); + } + if (Array.isArray(value)) { + return value.some((item) => hasDavNode(item, nodeName)); + } + if (typeof value === "object") { + return Object.entries(value).some(([key, child]) => { + if (key.toLowerCase() === target) { + return true; + } + return hasDavNode(child, nodeName); + }); + } + return false; +} + +export function toDavText(value: unknown): string | undefined { + if (value === null || value === undefined) { + return undefined; + } + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + } + if (Array.isArray(value)) { + for (const item of value) { + const text = toDavText(item); + if (text) { + return text; + } + } + return undefined; + } + if (typeof value === "object") { + for (const child of Object.values(value)) { + const text = toDavText(child); + if (text) { + return text; + } + } + } + return undefined; +} diff --git a/src/index.ts b/src/index.ts index b1859a0..ea9e75a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { createServer } from "node:http"; import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; diff --git a/tests/caldav-client-parsing.test.ts b/tests/caldav-client-parsing.test.ts new file mode 100644 index 0000000..3c145ea --- /dev/null +++ b/tests/caldav-client-parsing.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { hasDavNode, toDavText } from "../src/caldav/client.js"; + +describe("DAV parsing helpers", () => { + it("detects nodes in nested DAV object trees", () => { + const resourcetype = { + collection: "", + calendar: "", + }; + + expect(hasDavNode(resourcetype, "calendar")).toBe(true); + expect(hasDavNode(resourcetype, "write")).toBe(false); + }); + + it("detects privileges and components in nested values", () => { + const privileges = { + privilege: [{ read: "" }, { write: "" }], + }; + const components = { + comp: [{ "@_name": "VEVENT" }, { "@_name": "VTODO" }], + }; + + expect(hasDavNode(privileges, "write")).toBe(true); + expect(hasDavNode(components, "VEVENT")).toBe(true); + expect(hasDavNode(components, "VJOURNAL")).toBe(false); + }); + + it("extracts text from nested DAV displayname values", () => { + expect(toDavText(" Personal Calendar ")).toBe("Personal Calendar"); + expect(toDavText({ "#text": "Work" })).toBe("Work"); + }); +});