fix calendar discovery parsing and load env config

This commit is contained in:
2026-03-20 16:12:36 +01:00
parent 4735b9ccf2
commit 78a6007afd
6 changed files with 103 additions and 6 deletions

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ Thumbs.db
.idea/
.vscode/
opencode.json

13
package-lock.json generated
View File

@@ -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",

View File

@@ -15,6 +15,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.3",
"dotenv": "^16.4.7",
"fast-xml-parser": "^4.5.0",
"zod": "^4.1.11"
},

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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");
});
});