Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ versions adhere to [Semantic Versioning](https://semver.org).

## [Unreleased]

### Added

- `create_project` and `update_project` now expose `startOn` (project
start date). It was always present on project responses but never
settable through the connector. Wire-verified: POST stores it, PUT
changes it, and `startOn: null` on update clears it. `update_project`
follows the connector's usual nullable-date semantics (`undefined` =
leave untouched, `null` = clear).

## [2.0.1] — 2026-06-11

Patch on v2.0.0, fixing the one v2 vocabulary translation that was
Expand Down
2 changes: 1 addition & 1 deletion HOWTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ npm install
npm test
```

576 tests, all mocked — no Capsule API calls happen, no token needed. The suite has three layers:
579 tests, all mocked — no Capsule API calls happen, no token needed. The suite has three layers:

- **Per-tool unit tests** (e.g. `tests/parties.test.ts`): import the tool function, mock `undici.fetch`, assert on the URL, method, body, and response handling. Most tests live here.
- **MCP-protocol integration tests** (`tests/mcp-integration.test.ts`): drive a real `McpServer` through the wire protocol via the SDK's in-memory transport pair, with `undici.fetch` still mocked. Catches the layer between "tool function works" and "MCP correctly registers and dispatches the tool". Includes the `get_attachment` content-type routing logic (which lives in `server.ts`, not the tool function).
Expand Down
15 changes: 15 additions & 0 deletions src/tools/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ export const createProjectSchema = z.object({
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional()
.describe("YYYY-MM-DD"),
startOn: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional()
.describe(
"Project start date, YYYY-MM-DD. Verified empirically (v2.0.1 wire probe): Capsule's POST /kases accepts and stores it; reads back as `startOn` on the project.",
),
fields: z
.array(CustomFieldWriteSchema)
.optional()
Expand Down Expand Up @@ -187,6 +194,14 @@ export const updateProjectSchema = z.object({
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional()
.describe("YYYY-MM-DD"),
startOn: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.nullable()
.optional()
.describe(
"Set the project start date (YYYY-MM-DD), or `null` to clear it. Verified empirically (v2.0.1 wire probe): PUT accepts both the set and the null-clear. `undefined` leaves the field untouched.",
),
fields: z
.array(CustomFieldWriteSchema)
.optional()
Expand Down
32 changes: 32 additions & 0 deletions tests/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,35 @@ describe("getProjects (batch)", () => {
expect(result.projects.map((p) => p.id)).toEqual(ids);
});
});

describe("startOn", () => {
// Wire-verified (v2.0.1 probe): POST stores startOn (201, echoed),
// PUT changes it (200), PUT null clears it (200).
it("create_project forwards startOn in the body", async () => {
mockFetch(201, { kase: { id: 10 } });
const { createProject } = await import("../src/tools/projects.js");
await createProject({ name: "X", partyId: 5, startOn: "2026-07-01" });
const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string);
expect(body.kase.startOn).toBe("2026-07-01");
});

it("update_project sets and null-clears startOn", async () => {
mockFetch(200, { kase: { id: 10 } });
const { updateProject } = await import("../src/tools/projects.js");
await updateProject({ id: 10, startOn: "2026-08-15" });
let body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string);
expect(body.kase.startOn).toBe("2026-08-15");

mockFetch(200, { kase: { id: 10 } });
await updateProject({ id: 10, startOn: null });
body = JSON.parse((vi.mocked(fetch).mock.calls[1]![1] as RequestInit).body as string);
expect(body.kase.startOn).toBeNull();
});

it("rejects non-date startOn at the schema layer", async () => {
const { createProjectSchema } = await import("../src/tools/projects.js");
expect(
createProjectSchema.safeParse({ name: "X", partyId: 5, startOn: "July 1st" }).success,
).toBe(false);
});
});