mcp-sunsama 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # mcp-sunsama
2
2
 
3
+ ## 0.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 002cd07: Fix MCP Inspector compatibility for update-task-notes schema
8
+
9
+ - Replace ZodIntersection (.and()) with single z.object() using .refine() for XOR validation
10
+ - Ensure schema generates proper JSON Schema with "type": "object" for MCP Inspector
11
+ - Add clear parameter descriptions indicating mutual exclusivity
12
+ - Maintain same validation behavior while fixing compatibility issues
13
+ - Update documentation to reflect schema architecture improvements
14
+
15
+ ## 0.10.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 52e464c: Refactor update-task-notes schema to use XOR pattern for content parameters
20
+
21
+ - Replace nested content object with separate html and markdown parameters
22
+ - Implement XOR validation ensuring exactly one content type is provided
23
+ - Add comprehensive test coverage for the new schema pattern
24
+ - Update tool implementation to handle simplified parameter structure
25
+ - Update documentation across README.md, CLAUDE.md, and API docs
26
+
3
27
  ## 0.9.0
4
28
 
5
29
  ### Minor Changes
package/CLAUDE.md CHANGED
@@ -66,6 +66,8 @@ All tools use Zod schemas from `schemas.ts`:
66
66
  - Automatic TypeScript inference
67
67
  - Comprehensive parameter documentation
68
68
  - Union types for completion filters
69
+ - XOR schema patterns for mutually exclusive parameters using `.refine()` for MCP Inspector compatibility
70
+ - Example: `update-task-notes` requires either `html` OR `markdown`, but not both
69
71
 
70
72
  ## Key Patterns
71
73
 
@@ -106,6 +108,7 @@ server.addTool({
106
108
 
107
109
  - `src/main.ts`: FastMCP server setup and tool definitions
108
110
  - `src/schemas.ts`: Zod validation schemas for all tools
111
+ - `src/schemas.test.ts`: Comprehensive test suite for all Zod schemas including parameter validation, response schemas, and XOR patterns
109
112
  - `src/auth/`: Authentication strategies per transport type
110
113
  - `src/config/`: Environment configuration and validation
111
114
  - `src/utils/`: Reusable utilities (client resolution, filtering, formatting)
package/README.md CHANGED
@@ -95,7 +95,7 @@ Add this configuration to your Claude Desktop MCP settings:
95
95
  - `get-task-by-id` - Get a specific task by its ID
96
96
  - `update-task-complete` - Mark tasks as complete
97
97
  - `update-task-planned-time` - Update the planned time (time estimate) for tasks
98
- - `update-task-notes` - Update task notes content with HTML or Markdown
98
+ - `update-task-notes` - Update task notes content (requires either `html` or `markdown` parameter, mutually exclusive)
99
99
  - `update-task-snooze-date` - Reschedule tasks to different dates
100
100
  - `update-task-backlog` - Move tasks to the backlog
101
101
  - `delete-task` - Delete tasks permanently
package/dist/main.js CHANGED
@@ -16,7 +16,7 @@ if (transportConfig.transportType === "stdio") {
16
16
  }
17
17
  const server = new FastMCP({
18
18
  name: "Sunsama API Server",
19
- version: "0.9.0",
19
+ version: "0.10.1",
20
20
  instructions: `
21
21
  This MCP server provides access to the Sunsama API for task and project management.
22
22
 
@@ -575,11 +575,15 @@ server.addTool({
575
575
  execute: async (args, { session, log }) => {
576
576
  try {
577
577
  // Extract parameters
578
- const { taskId, content, limitResponsePayload } = args;
578
+ const { taskId, html, markdown, limitResponsePayload } = args;
579
+ // Create simplified content object with type and value
580
+ const content = html
581
+ ? { type: "html", value: html }
582
+ : { type: "markdown", value: markdown };
579
583
  log.info("Updating task notes", {
580
584
  taskId: taskId,
581
- contentType: 'html' in content ? 'html' : 'markdown',
582
- contentLength: ('html' in content ? content.html : content.markdown).length,
585
+ contentType: content.type,
586
+ contentLength: content.value.length,
583
587
  limitResponsePayload: limitResponsePayload
584
588
  });
585
589
  // Get the appropriate client based on transport type
@@ -588,12 +592,14 @@ server.addTool({
588
592
  const options = {};
589
593
  if (limitResponsePayload !== undefined)
590
594
  options.limitResponsePayload = limitResponsePayload;
591
- // Call sunsamaClient.updateTaskNotes(taskId, content, options)
592
- const result = await sunsamaClient.updateTaskNotes(taskId, content, options);
595
+ // Build content object for API call
596
+ const apiContent = content.type === "html" ? { html: content.value } : { markdown: content.value };
597
+ // Call sunsamaClient.updateTaskNotes(taskId, apiContent, options)
598
+ const result = await sunsamaClient.updateTaskNotes(taskId, apiContent, options);
593
599
  log.info("Successfully updated task notes", {
594
600
  taskId: taskId,
595
601
  success: result.success,
596
- contentType: 'html' in content ? 'html' : 'markdown'
602
+ contentType: content.type
597
603
  });
598
604
  return {
599
605
  content: [
@@ -612,7 +618,7 @@ server.addTool({
612
618
  catch (error) {
613
619
  log.error("Failed to update task notes", {
614
620
  taskId: args.taskId,
615
- contentType: 'html' in args.content ? 'html' : 'markdown',
621
+ contentType: args.html ? 'html' : 'markdown',
616
622
  error: error instanceof Error ? error.message : 'Unknown error'
617
623
  });
618
624
  throw new Error(`Failed to update task notes: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -752,11 +758,11 @@ Uses HTTP Basic Auth headers (per-request authentication):
752
758
  - **update-task-notes**: Update task notes content
753
759
  - Parameters:
754
760
  - \`taskId\` (required): The ID of the task to update notes for
755
- - \`content\` (required): Task notes content in either HTML or Markdown format
756
- - \`{html: string}\` OR \`{markdown: string}\` (mutually exclusive)
761
+ - \`html\` (required XOR): HTML content for the task notes
762
+ - \`markdown\` (required XOR): Markdown content for the task notes
757
763
  - \`limitResponsePayload\` (optional): Whether to limit response size (defaults to true)
758
764
  - Returns: JSON with update result
759
- - Note: Supports both HTML and Markdown content with automatic format conversion
765
+ - Note: Exactly one of \`html\` or \`markdown\` must be provided (mutually exclusive)
760
766
 
761
767
  ### Stream Operations
762
768
  - **get-streams**: Get streams for the user's group
package/dist/schemas.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Helper Schemas
4
+ */
5
+ export declare const jsonStringSchema: z.ZodEffects<z.ZodString, any, string>;
2
6
  /**
3
7
  * Task Operation Schemas
4
8
  */
@@ -141,38 +145,31 @@ export declare const updateTaskPlannedTimeSchema: z.ZodObject<{
141
145
  timeEstimateMinutes: number;
142
146
  limitResponsePayload?: boolean | undefined;
143
147
  }>;
144
- export declare const updateTaskNotesSchema: z.ZodObject<{
148
+ export declare const updateTaskNotesSchema: z.ZodEffects<z.ZodObject<{
145
149
  taskId: z.ZodString;
146
- content: z.ZodUnion<[z.ZodObject<{
147
- html: z.ZodString;
148
- }, "strip", z.ZodTypeAny, {
149
- html: string;
150
- }, {
151
- html: string;
152
- }>, z.ZodObject<{
153
- markdown: z.ZodString;
154
- }, "strip", z.ZodTypeAny, {
155
- markdown: string;
156
- }, {
157
- markdown: string;
158
- }>]>;
150
+ html: z.ZodOptional<z.ZodString>;
151
+ markdown: z.ZodOptional<z.ZodString>;
159
152
  limitResponsePayload: z.ZodOptional<z.ZodBoolean>;
160
153
  }, "strip", z.ZodTypeAny, {
161
154
  taskId: string;
162
- content: {
163
- html: string;
164
- } | {
165
- markdown: string;
166
- };
167
155
  limitResponsePayload?: boolean | undefined;
156
+ html?: string | undefined;
157
+ markdown?: string | undefined;
158
+ }, {
159
+ taskId: string;
160
+ limitResponsePayload?: boolean | undefined;
161
+ html?: string | undefined;
162
+ markdown?: string | undefined;
163
+ }>, {
164
+ taskId: string;
165
+ limitResponsePayload?: boolean | undefined;
166
+ html?: string | undefined;
167
+ markdown?: string | undefined;
168
168
  }, {
169
169
  taskId: string;
170
- content: {
171
- html: string;
172
- } | {
173
- markdown: string;
174
- };
175
170
  limitResponsePayload?: boolean | undefined;
171
+ html?: string | undefined;
172
+ markdown?: string | undefined;
176
173
  }>;
177
174
  /**
178
175
  * Response Type Schemas (for validation and documentation)
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
package/dist/schemas.js CHANGED
@@ -1,4 +1,16 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Helper Schemas
4
+ */
5
+ export const jsonStringSchema = z.string().transform((str, ctx) => {
6
+ try {
7
+ return JSON.parse(str);
8
+ }
9
+ catch (error) {
10
+ ctx.addIssue({ code: 'custom', message: 'Invalid JSON' });
11
+ return z.NEVER;
12
+ }
13
+ });
2
14
  /**
3
15
  * Task Operation Schemas
4
16
  */
@@ -76,18 +88,20 @@ export const updateTaskPlannedTimeSchema = z.object({
76
88
  timeEstimateMinutes: z.number().int().min(0).describe("Time estimate in minutes (use 0 to clear the time estimate)"),
77
89
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
78
90
  });
79
- // Update task notes parameters
91
+ // Update task notes parameters with XOR content validation
80
92
  export const updateTaskNotesSchema = z.object({
81
93
  taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to update notes for"),
82
- content: z.union([
83
- z.object({
84
- html: z.string().describe("HTML content for the task notes")
85
- }),
86
- z.object({
87
- markdown: z.string().describe("Markdown content for the task notes")
88
- })
89
- ]).describe("Task notes content in either HTML or Markdown format"),
94
+ html: z.string().optional().describe("HTML content for the task notes (mutually exclusive with markdown)"),
95
+ markdown: z.string().optional().describe("Markdown content for the task notes (mutually exclusive with html)"),
90
96
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size (defaults to true)"),
97
+ }).refine((data) => {
98
+ // Exactly one of html or markdown must be provided
99
+ const hasHtml = data.html !== undefined;
100
+ const hasMarkdown = data.markdown !== undefined;
101
+ return hasHtml !== hasMarkdown; // XOR: exactly one must be true
102
+ }, {
103
+ message: "Exactly one of 'html' or 'markdown' must be provided",
104
+ path: [], // This will show the error at the root level
91
105
  });
92
106
  /**
93
107
  * Response Type Schemas (for validation and documentation)
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { completionFilterSchema, getTasksByDaySchema, getTasksBacklogSchema, getArchivedTasksSchema, getUserSchema, getStreamsSchema, createTaskSchema, updateTaskCompleteSchema, deleteTaskSchema, updateTaskSnoozeDateSchema, updateTaskBacklogSchema, updateTaskPlannedTimeSchema, userProfileSchema, groupSchema, userSchema, taskSchema, streamSchema, userResponseSchema, tasksResponseSchema, streamsResponseSchema, errorResponseSchema, } from "./schemas.js";
2
+ import { completionFilterSchema, getTasksByDaySchema, getTasksBacklogSchema, getArchivedTasksSchema, getUserSchema, getStreamsSchema, createTaskSchema, updateTaskCompleteSchema, deleteTaskSchema, updateTaskSnoozeDateSchema, updateTaskBacklogSchema, updateTaskPlannedTimeSchema, updateTaskNotesSchema, userProfileSchema, groupSchema, userSchema, taskSchema, streamSchema, userResponseSchema, tasksResponseSchema, streamsResponseSchema, errorResponseSchema, } from "./schemas.js";
3
3
  describe("Tool Parameter Schemas", () => {
4
4
  describe("completionFilterSchema", () => {
5
5
  test("should accept valid completion filter values", () => {
@@ -252,6 +252,74 @@ describe("Tool Parameter Schemas", () => {
252
252
  })).toThrow();
253
253
  });
254
254
  });
255
+ describe("updateTaskNotesSchema", () => {
256
+ test("should accept valid HTML content", () => {
257
+ const htmlInput = {
258
+ taskId: "task-123",
259
+ html: "<p>This is HTML content</p>",
260
+ limitResponsePayload: true,
261
+ };
262
+ expect(() => updateTaskNotesSchema.parse(htmlInput)).not.toThrow();
263
+ });
264
+ test("should accept valid Markdown content", () => {
265
+ const markdownInput = {
266
+ taskId: "task-123",
267
+ markdown: "# This is Markdown content",
268
+ limitResponsePayload: true,
269
+ };
270
+ expect(() => updateTaskNotesSchema.parse(markdownInput)).not.toThrow();
271
+ });
272
+ test("should accept minimal HTML input", () => {
273
+ const minimalHtmlInput = {
274
+ taskId: "task-123",
275
+ html: "<p>Simple HTML</p>",
276
+ };
277
+ expect(() => updateTaskNotesSchema.parse(minimalHtmlInput)).not.toThrow();
278
+ });
279
+ test("should accept minimal Markdown input", () => {
280
+ const minimalMarkdownInput = {
281
+ taskId: "task-123",
282
+ markdown: "Simple markdown",
283
+ };
284
+ expect(() => updateTaskNotesSchema.parse(minimalMarkdownInput)).not.toThrow();
285
+ });
286
+ test("should reject both HTML and Markdown provided", () => {
287
+ const invalidInput = {
288
+ taskId: "task-123",
289
+ html: "<p>HTML content</p>",
290
+ markdown: "Markdown content",
291
+ };
292
+ expect(() => updateTaskNotesSchema.parse(invalidInput)).toThrow();
293
+ });
294
+ test("should reject neither HTML nor Markdown provided", () => {
295
+ const invalidInput = {
296
+ taskId: "task-123",
297
+ limitResponsePayload: true,
298
+ };
299
+ expect(() => updateTaskNotesSchema.parse(invalidInput)).toThrow();
300
+ });
301
+ test("should reject empty task ID", () => {
302
+ expect(() => updateTaskNotesSchema.parse({
303
+ taskId: "",
304
+ html: "<p>Content</p>",
305
+ })).toThrow();
306
+ expect(() => updateTaskNotesSchema.parse({
307
+ markdown: "Content",
308
+ })).toThrow();
309
+ });
310
+ test("should accept empty HTML content", () => {
311
+ expect(() => updateTaskNotesSchema.parse({
312
+ taskId: "task-123",
313
+ html: "",
314
+ })).not.toThrow();
315
+ });
316
+ test("should accept empty Markdown content", () => {
317
+ expect(() => updateTaskNotesSchema.parse({
318
+ taskId: "task-123",
319
+ markdown: "",
320
+ })).not.toThrow();
321
+ });
322
+ });
255
323
  });
256
324
  describe("Response Schemas", () => {
257
325
  describe("userProfileSchema", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-sunsama",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "MCP server for Sunsama API integration",
5
5
  "type": "module",
6
6
  "private": false,
package/src/main.ts CHANGED
@@ -35,7 +35,7 @@ if (transportConfig.transportType === "stdio") {
35
35
 
36
36
  const server = new FastMCP({
37
37
  name: "Sunsama API Server",
38
- version: "0.9.0",
38
+ version: "0.10.1",
39
39
  instructions: `
40
40
  This MCP server provides access to the Sunsama API for task and project management.
41
41
 
@@ -697,12 +697,17 @@ server.addTool({
697
697
  execute: async (args, {session, log}) => {
698
698
  try {
699
699
  // Extract parameters
700
- const {taskId, content, limitResponsePayload} = args;
700
+ const {taskId, html, markdown, limitResponsePayload} = args;
701
+
702
+ // Create simplified content object with type and value
703
+ const content = html
704
+ ? {type: "html" as const, value: html}
705
+ : {type: "markdown" as const, value: markdown!};
701
706
 
702
707
  log.info("Updating task notes", {
703
708
  taskId: taskId,
704
- contentType: 'html' in content ? 'html' : 'markdown',
705
- contentLength: ('html' in content ? content.html : content.markdown).length,
709
+ contentType: content.type,
710
+ contentLength: content.value.length,
706
711
  limitResponsePayload: limitResponsePayload
707
712
  });
708
713
 
@@ -715,17 +720,20 @@ server.addTool({
715
720
  } = {};
716
721
  if (limitResponsePayload !== undefined) options.limitResponsePayload = limitResponsePayload;
717
722
 
718
- // Call sunsamaClient.updateTaskNotes(taskId, content, options)
723
+ // Build content object for API call
724
+ const apiContent = content.type === "html" ? {html: content.value} : {markdown: content.value};
725
+
726
+ // Call sunsamaClient.updateTaskNotes(taskId, apiContent, options)
719
727
  const result = await sunsamaClient.updateTaskNotes(
720
728
  taskId,
721
- content,
729
+ apiContent,
722
730
  options
723
731
  );
724
732
 
725
733
  log.info("Successfully updated task notes", {
726
734
  taskId: taskId,
727
735
  success: result.success,
728
- contentType: 'html' in content ? 'html' : 'markdown'
736
+ contentType: content.type
729
737
  });
730
738
 
731
739
  return {
@@ -745,7 +753,7 @@ server.addTool({
745
753
  } catch (error) {
746
754
  log.error("Failed to update task notes", {
747
755
  taskId: args.taskId,
748
- contentType: 'html' in args.content ? 'html' : 'markdown',
756
+ contentType: args.html ? 'html' : 'markdown',
749
757
  error: error instanceof Error ? error.message : 'Unknown error'
750
758
  });
751
759
 
@@ -893,11 +901,11 @@ Uses HTTP Basic Auth headers (per-request authentication):
893
901
  - **update-task-notes**: Update task notes content
894
902
  - Parameters:
895
903
  - \`taskId\` (required): The ID of the task to update notes for
896
- - \`content\` (required): Task notes content in either HTML or Markdown format
897
- - \`{html: string}\` OR \`{markdown: string}\` (mutually exclusive)
904
+ - \`html\` (required XOR): HTML content for the task notes
905
+ - \`markdown\` (required XOR): Markdown content for the task notes
898
906
  - \`limitResponsePayload\` (optional): Whether to limit response size (defaults to true)
899
907
  - Returns: JSON with update result
900
- - Note: Supports both HTML and Markdown content with automatic format conversion
908
+ - Note: Exactly one of \`html\` or \`markdown\` must be provided (mutually exclusive)
901
909
 
902
910
  ### Stream Operations
903
911
  - **get-streams**: Get streams for the user's group
@@ -12,6 +12,7 @@ import {
12
12
  updateTaskSnoozeDateSchema,
13
13
  updateTaskBacklogSchema,
14
14
  updateTaskPlannedTimeSchema,
15
+ updateTaskNotesSchema,
15
16
  userProfileSchema,
16
17
  groupSchema,
17
18
  userSchema,
@@ -351,6 +352,91 @@ describe("Tool Parameter Schemas", () => {
351
352
  ).toThrow();
352
353
  });
353
354
  });
355
+
356
+ describe("updateTaskNotesSchema", () => {
357
+ test("should accept valid HTML content", () => {
358
+ const htmlInput = {
359
+ taskId: "task-123",
360
+ html: "<p>This is HTML content</p>",
361
+ limitResponsePayload: true,
362
+ };
363
+ expect(() => updateTaskNotesSchema.parse(htmlInput)).not.toThrow();
364
+ });
365
+
366
+ test("should accept valid Markdown content", () => {
367
+ const markdownInput = {
368
+ taskId: "task-123",
369
+ markdown: "# This is Markdown content",
370
+ limitResponsePayload: true,
371
+ };
372
+ expect(() => updateTaskNotesSchema.parse(markdownInput)).not.toThrow();
373
+ });
374
+
375
+ test("should accept minimal HTML input", () => {
376
+ const minimalHtmlInput = {
377
+ taskId: "task-123",
378
+ html: "<p>Simple HTML</p>",
379
+ };
380
+ expect(() => updateTaskNotesSchema.parse(minimalHtmlInput)).not.toThrow();
381
+ });
382
+
383
+ test("should accept minimal Markdown input", () => {
384
+ const minimalMarkdownInput = {
385
+ taskId: "task-123",
386
+ markdown: "Simple markdown",
387
+ };
388
+ expect(() => updateTaskNotesSchema.parse(minimalMarkdownInput)).not.toThrow();
389
+ });
390
+
391
+ test("should reject both HTML and Markdown provided", () => {
392
+ const invalidInput = {
393
+ taskId: "task-123",
394
+ html: "<p>HTML content</p>",
395
+ markdown: "Markdown content",
396
+ };
397
+ expect(() => updateTaskNotesSchema.parse(invalidInput)).toThrow();
398
+ });
399
+
400
+ test("should reject neither HTML nor Markdown provided", () => {
401
+ const invalidInput = {
402
+ taskId: "task-123",
403
+ limitResponsePayload: true,
404
+ };
405
+ expect(() => updateTaskNotesSchema.parse(invalidInput)).toThrow();
406
+ });
407
+
408
+ test("should reject empty task ID", () => {
409
+ expect(() =>
410
+ updateTaskNotesSchema.parse({
411
+ taskId: "",
412
+ html: "<p>Content</p>",
413
+ })
414
+ ).toThrow();
415
+ expect(() =>
416
+ updateTaskNotesSchema.parse({
417
+ markdown: "Content",
418
+ })
419
+ ).toThrow();
420
+ });
421
+
422
+ test("should accept empty HTML content", () => {
423
+ expect(() =>
424
+ updateTaskNotesSchema.parse({
425
+ taskId: "task-123",
426
+ html: "",
427
+ })
428
+ ).not.toThrow();
429
+ });
430
+
431
+ test("should accept empty Markdown content", () => {
432
+ expect(() =>
433
+ updateTaskNotesSchema.parse({
434
+ taskId: "task-123",
435
+ markdown: "",
436
+ })
437
+ ).not.toThrow();
438
+ });
439
+ });
354
440
  });
355
441
 
356
442
  describe("Response Schemas", () => {
package/src/schemas.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  import { z } from "zod";
2
2
 
3
+ /**
4
+ * Helper Schemas
5
+ */
6
+
7
+ export const jsonStringSchema = z.string().transform((str, ctx) => {
8
+ try {
9
+ return JSON.parse(str);
10
+ } catch (error) {
11
+ ctx.addIssue({ code: 'custom', message: 'Invalid JSON' });
12
+ return z.NEVER;
13
+ }
14
+ });
15
+
3
16
  /**
4
17
  * Task Operation Schemas
5
18
  */
@@ -94,19 +107,24 @@ export const updateTaskPlannedTimeSchema = z.object({
94
107
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
95
108
  });
96
109
 
97
- // Update task notes parameters
110
+ // Update task notes parameters with XOR content validation
98
111
  export const updateTaskNotesSchema = z.object({
99
112
  taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to update notes for"),
100
- content: z.union([
101
- z.object({
102
- html: z.string().describe("HTML content for the task notes")
103
- }),
104
- z.object({
105
- markdown: z.string().describe("Markdown content for the task notes")
106
- })
107
- ]).describe("Task notes content in either HTML or Markdown format"),
113
+ html: z.string().optional().describe("HTML content for the task notes (mutually exclusive with markdown)"),
114
+ markdown: z.string().optional().describe("Markdown content for the task notes (mutually exclusive with html)"),
108
115
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size (defaults to true)"),
109
- });
116
+ }).refine(
117
+ (data) => {
118
+ // Exactly one of html or markdown must be provided
119
+ const hasHtml = data.html !== undefined;
120
+ const hasMarkdown = data.markdown !== undefined;
121
+ return hasHtml !== hasMarkdown; // XOR: exactly one must be true
122
+ },
123
+ {
124
+ message: "Exactly one of 'html' or 'markdown' must be provided",
125
+ path: [], // This will show the error at the root level
126
+ }
127
+ );
110
128
 
111
129
  /**
112
130
  * Response Type Schemas (for validation and documentation)