fix calendar discovery parsing and load env config
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,3 +17,5 @@ Thumbs.db
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
opencode.json
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.13.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
32
tests/caldav-client-parsing.test.ts
Normal file
32
tests/caldav-client-parsing.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user