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/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
opencode.json
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.13.3",
|
"@modelcontextprotocol/sdk": "^1.13.3",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"fast-xml-parser": "^4.5.0",
|
"fast-xml-parser": "^4.5.0",
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
@@ -1265,6 +1266,18 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.13.3",
|
"@modelcontextprotocol/sdk": "^1.13.3",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"fast-xml-parser": "^4.5.0",
|
"fast-xml-parser": "^4.5.0",
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -110,16 +110,14 @@ export class CaldavClient {
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const resourceType = String(ok.props.resourcetype ?? "");
|
if (!hasDavNode(ok.props.resourcetype, "calendar")) {
|
||||||
if (!resourceType.toLowerCase().includes("calendar")) {
|
|
||||||
return undefined;
|
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 etag = normalizeEtag(ok.props.getetag);
|
||||||
const writable = String(ok.props["current-user-privilege-set"] ?? "").toLowerCase().includes("write");
|
const writable = hasDavNode(ok.props["current-user-privilege-set"], "write");
|
||||||
const componentsRaw = String(ok.props["supported-calendar-component-set"] ?? "");
|
const components = ["VEVENT", "VTODO", "VJOURNAL"].filter((component) => hasDavNode(ok.props["supported-calendar-component-set"], component));
|
||||||
const components = ["VEVENT", "VTODO", "VJOURNAL"].filter((component) => componentsRaw.includes(component));
|
|
||||||
|
|
||||||
const calendar: CalendarInfo = {
|
const calendar: CalendarInfo = {
|
||||||
id: normalizeIdFromHref(href),
|
id: normalizeIdFromHref(href),
|
||||||
@@ -391,3 +389,53 @@ function normalizeEtag(input: unknown): string | undefined {
|
|||||||
}
|
}
|
||||||
return String(input).trim();
|
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 { createServer } from "node:http";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
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