implement CalDAV MCP server v1 with streamable HTTP tools
This commit is contained in:
16
tests/errors.test.ts
Normal file
16
tests/errors.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { CaldavHttpError, mapErrorToMcp } from "../src/errors.js";
|
||||
|
||||
describe("mapErrorToMcp", () => {
|
||||
it("maps 412 to invalid request conflict", () => {
|
||||
const error = mapErrorToMcp(new CaldavHttpError(412, "PUT", "https://example.com", ""));
|
||||
expect(error.code).toBe(ErrorCode.InvalidRequest);
|
||||
expect(error.message.toLowerCase()).toContain("conflict");
|
||||
});
|
||||
|
||||
it("maps 5xx to internal error", () => {
|
||||
const error = mapErrorToMcp(new CaldavHttpError(503, "PROPFIND", "https://example.com", ""));
|
||||
expect(error.code).toBe(ErrorCode.InternalError);
|
||||
});
|
||||
});
|
||||
17
tests/ics.test.ts
Normal file
17
tests/ics.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ensureUid, validateIcsEvent } from "../src/ics.js";
|
||||
import { ValidationError } from "../src/errors.js";
|
||||
|
||||
describe("ics helpers", () => {
|
||||
it("adds UID when missing", () => {
|
||||
const ics = `BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260101T100000Z\nDTEND:20260101T110000Z\nEND:VEVENT\nEND:VCALENDAR`;
|
||||
const output = ensureUid(ics);
|
||||
expect(output.uid).toBeTruthy();
|
||||
expect(output.ics).toContain(`UID:${output.uid}`);
|
||||
});
|
||||
|
||||
it("rejects DTEND before DTSTART", () => {
|
||||
const ics = `BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:abc\nDTSTART:20260102T100000Z\nDTEND:20260101T100000Z\nEND:VEVENT\nEND:VCALENDAR`;
|
||||
expect(() => validateIcsEvent(ics)).toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
14
tests/util.test.ts
Normal file
14
tests/util.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeHref } from "../src/util.js";
|
||||
|
||||
describe("normalizeHref", () => {
|
||||
it("resolves relative href against base", () => {
|
||||
const href = normalizeHref("/caldav/user/Calendar/personal/", "https://caldav.example.com/caldav");
|
||||
expect(href).toBe("https://caldav.example.com/caldav/user/Calendar/personal/");
|
||||
});
|
||||
|
||||
it("strips URL fragment", () => {
|
||||
const href = normalizeHref("https://caldav.example.com/caldav/user/Calendar/personal/#fragment", "https://caldav.example.com/caldav");
|
||||
expect(href).toBe("https://caldav.example.com/caldav/user/Calendar/personal/");
|
||||
});
|
||||
});
|
||||
25
tests/xml.test.ts
Normal file
25
tests/xml.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseMultiStatus } from "../src/caldav/xml.js";
|
||||
|
||||
describe("parseMultiStatus", () => {
|
||||
it("parses href and props from multistatus response", () => {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:response>
|
||||
<d:href>/SOGo/dav/user/Calendar/personal/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:displayname>Personal</d:displayname>
|
||||
</d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const parsed = parseMultiStatus(xml);
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0].href).toBe("/SOGo/dav/user/Calendar/personal/");
|
||||
expect(parsed[0].statuses[0].status).toBe(200);
|
||||
expect(parsed[0].statuses[0].props.displayname).toBe("Personal");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user