feat: add missing MCP tools and fix undefined content response

- Add time_entries_update, time_entries_delete, time_entries_get_summary tools
- Fix textResult to use `data ?? null` so JSON.stringify always returns a
  string — procedures returning undefined (e.g. getRunning with no timer,
  businesses.getById not found) were dropping the text field from the MCP
  content item, causing client SDK validation failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 18:46:30 -04:00
parent c490556a02
commit 2b4608bfb4
+62 -1
View File
@@ -234,7 +234,7 @@ function parseInvoiceItems(items: z.infer<typeof invoiceItemSchema>[]) {
function textResult(data: unknown): ToolResult {
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
content: [{ type: "text", text: JSON.stringify(data ?? null, null, 2) }],
};
}
@@ -509,6 +509,67 @@ const tools = {
endedAt: input.endedAt ? parseDate(input.endedAt, "endedAt") : undefined,
}),
}),
time_entries_update: defineTool({
description: "Update an existing time entry by ID. All fields are optional except id.",
inputSchema: {
type: "object",
properties: {
id: { type: "string" },
description: { type: "string", maxLength: 500 },
clientId: { type: "string" },
startedAt: { type: "string", format: "date-time" },
endedAt: { type: "string", format: "date-time" },
hours: { type: "number", minimum: 0 },
rate: { type: "number", minimum: 0 },
notes: { type: "string", maxLength: 500 },
},
required: ["id"],
additionalProperties: false,
},
schema: z.object({
id: z.string(),
description: z.string().max(500).optional(),
clientId: z.string().optional().or(z.literal("")),
startedAt: dateString.optional(),
endedAt: dateString.optional(),
hours: z.number().min(0).optional(),
rate: z.number().min(0).optional(),
notes: z.string().max(500).optional().or(z.literal("")),
}),
handler: async (input, caller) =>
caller.timeEntries.update({
...input,
startedAt: input.startedAt ? parseDate(input.startedAt, "startedAt") : undefined,
endedAt: input.endedAt ? parseDate(input.endedAt, "endedAt") : undefined,
}),
}),
time_entries_delete: defineTool({
description: "Delete a time entry by ID.",
inputSchema: jsonSchemas.id,
schema: z.object({ id: z.string() }),
handler: async (input, caller) => caller.timeEntries.delete(input),
}),
time_entries_get_summary: defineTool({
description:
"Get total hours, total earnings, and entry count for the authenticated user, optionally filtered by date range.",
inputSchema: {
type: "object",
properties: {
from: { type: "string", format: "date-time" },
to: { type: "string", format: "date-time" },
},
additionalProperties: false,
},
schema: z.object({
from: dateString.optional(),
to: dateString.optional(),
}),
handler: async (input, caller) =>
caller.timeEntries.getSummary({
from: input.from ? parseDate(input.from, "from") : undefined,
to: input.to ? parseDate(input.to, "to") : undefined,
}),
}),
} satisfies Record<string, ToolDefinition>;
function rpcResult(id: JsonRpcId, result: unknown, init?: ResponseInit) {