mcp-sunsama 0.6.0 → 0.8.0

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,36 @@
1
1
  # mcp-sunsama
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6585510: feat: add update-task-planned-time tool for updating task time estimates
8
+
9
+ This adds a new MCP tool that allows users to update the planned time (time estimate) for existing tasks in Sunsama. The tool accepts a task ID and time estimate in minutes, with optional response payload limiting.
10
+
11
+ Features:
12
+
13
+ - Update task time estimates in minutes (converted to seconds for API)
14
+ - Support for clearing time estimates by setting to 0
15
+ - Comprehensive input validation and error handling
16
+ - Full test coverage including edge cases
17
+ - Documentation updates for README and CLAUDE.md
18
+
19
+ The implementation follows established patterns in the codebase and leverages the existing sunsama-api updateTaskPlannedTime method.
20
+
21
+ ## 0.7.0
22
+
23
+ ### Minor Changes
24
+
25
+ - 3394176: Add get-task-by-id tool for retrieving specific tasks by their unique identifier
26
+
27
+ - Add `get-task-by-id` MCP tool that retrieves a specific task by its ID
28
+ - Add `getTaskByIdSchema` with taskId parameter validation
29
+ - Return complete task object if found, null if not found
30
+ - Follow standard tool patterns for authentication, error handling, and logging
31
+ - Update documentation in README.md and CLAUDE.md
32
+ - Maintain consistent JSON response format for single object retrieval
33
+
3
34
  ## 0.6.0
4
35
 
5
36
  ### Minor Changes
package/CLAUDE.md CHANGED
@@ -126,8 +126,8 @@ Optional:
126
126
 
127
127
  ### Task Operations
128
128
  Full CRUD support:
129
- - **Read**: `get-tasks-by-day`, `get-tasks-backlog`, `get-archived-tasks`, `get-streams`
130
- - **Write**: `create-task`, `update-task-complete`, `update-task-snooze-date`, `update-task-backlog`, `delete-task`
129
+ - **Read**: `get-tasks-by-day`, `get-tasks-backlog`, `get-archived-tasks`, `get-task-by-id`, `get-streams`
130
+ - **Write**: `create-task`, `update-task-complete`, `update-task-planned-time`, `update-task-snooze-date`, `update-task-backlog`, `delete-task`
131
131
 
132
132
  Task read operations support response trimming. `get-tasks-by-day` includes completion filtering. `get-archived-tasks` includes enhanced pagination with hasMore flag for LLM decision-making.
133
133
 
package/README.md CHANGED
@@ -92,7 +92,9 @@ Add this configuration to your Claude Desktop MCP settings:
92
92
  - `get-tasks-by-day` - Get tasks for a specific day with completion filtering
93
93
  - `get-tasks-backlog` - Get backlog tasks
94
94
  - `get-archived-tasks` - Get archived tasks with pagination (includes hasMore flag for LLM context)
95
+ - `get-task-by-id` - Get a specific task by its ID
95
96
  - `update-task-complete` - Mark tasks as complete
97
+ - `update-task-planned-time` - Update the planned time (time estimate) for tasks
96
98
  - `update-task-snooze-date` - Reschedule tasks to different dates
97
99
  - `update-task-backlog` - Move tasks to the backlog
98
100
  - `delete-task` - Delete tasks permanently
@@ -140,7 +142,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
140
142
 
141
143
  ## Support
142
144
 
143
- - [Sunsama API Documentation](https://help.sunsama.com)
144
145
  - [sunsama-api Library](https://github.com/robertn702/sunsama-api) - The underlying API client
145
146
  - [Model Context Protocol Documentation](https://modelcontextprotocol.io)
146
147
  - [Issue Tracker](https://github.com/robertn702/mcp-sunsama/issues)
package/bun.lock CHANGED
@@ -7,7 +7,7 @@
7
7
  "@types/papaparse": "^5.3.16",
8
8
  "fastmcp": "3.3.1",
9
9
  "papaparse": "^5.5.3",
10
- "sunsama-api": "0.6.1",
10
+ "sunsama-api": "0.7.0",
11
11
  "zod": "3.24.4",
12
12
  },
13
13
  "devDependencies": {
@@ -426,7 +426,7 @@
426
426
 
427
427
  "strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="],
428
428
 
429
- "sunsama-api": ["sunsama-api@0.6.1", "", { "dependencies": { "graphql": "^16.11.0", "graphql-tag": "^2.12.6", "tough-cookie": "^5.1.2", "tslib": "^2.8.1", "yjs": "^13.6.27", "zod": "^3.25.64" } }, "sha512-4GhOdrAMo99JIXv9GZg7+NQJu1uPRgGbmnKFWTCNzXYiSVicNPNv27vgqHa3kJtPo19x8sBf4gOYZYlw6b1Wgw=="],
429
+ "sunsama-api": ["sunsama-api@0.7.0", "", { "dependencies": { "graphql": "^16.11.0", "graphql-tag": "^2.12.6", "tough-cookie": "^5.1.2", "tslib": "^2.8.1", "yjs": "^13.6.27", "zod": "^3.25.64" } }, "sha512-/wPvJrtE0rUftl7c+OuQdu27OYkTvd23jzcQoAUP2SilQ6QYnFeG/Fjdf6nfl/MFuz5B8mpviGJdSgqYjAkd0g=="],
430
430
 
431
431
  "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
432
432
 
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import { FastMCP } from "fastmcp";
3
3
  import { httpStreamAuthenticator } from "./auth/http.js";
4
4
  import { initializeStdioAuth } from "./auth/stdio.js";
5
5
  import { getTransportConfig } from "./config/transport.js";
6
- import { createTaskSchema, deleteTaskSchema, getArchivedTasksSchema, getStreamsSchema, getTasksBacklogSchema, getTasksByDaySchema, getUserSchema, updateTaskBacklogSchema, updateTaskCompleteSchema, updateTaskSnoozeDateSchema } from "./schemas.js";
6
+ import { createTaskSchema, deleteTaskSchema, getArchivedTasksSchema, getStreamsSchema, getTaskByIdSchema, getTasksBacklogSchema, getTasksByDaySchema, getUserSchema, updateTaskBacklogSchema, updateTaskCompleteSchema, updateTaskPlannedTimeSchema, updateTaskSnoozeDateSchema } from "./schemas.js";
7
7
  import { getSunsamaClient } from "./utils/client-resolver.js";
8
8
  import { filterTasksByCompletion } from "./utils/task-filters.js";
9
9
  import { trimTasksForResponse } from "./utils/task-trimmer.js";
@@ -16,14 +16,15 @@ if (transportConfig.transportType === "stdio") {
16
16
  }
17
17
  const server = new FastMCP({
18
18
  name: "Sunsama API Server",
19
- version: "0.6.0",
19
+ version: "0.8.0",
20
20
  instructions: `
21
21
  This MCP server provides access to the Sunsama API for task and project management.
22
22
 
23
23
  Available tools:
24
24
  - Authentication: login, logout, check authentication status
25
25
  - User operations: get current user information
26
- - Task operations: get tasks by day, get backlog tasks, get archived tasks
26
+ - Task operations: get tasks by day, get backlog tasks, get archived tasks, get task by ID
27
+ - Task mutations: create tasks, mark complete, delete tasks, reschedule tasks, update planned time
27
28
  - Stream operations: get streams/channels for the user's group
28
29
 
29
30
  Authentication is required for all operations. You can either:
@@ -210,6 +211,57 @@ ${toTsv(trimmedTasks)}`;
210
211
  }
211
212
  }
212
213
  });
214
+ server.addTool({
215
+ name: "get-task-by-id",
216
+ description: "Get a specific task by its ID",
217
+ parameters: getTaskByIdSchema,
218
+ execute: async (args, { session, log }) => {
219
+ try {
220
+ const { taskId } = args;
221
+ log.info("Getting task by ID", {
222
+ taskId: taskId
223
+ });
224
+ // Get the appropriate client based on transport type
225
+ const sunsamaClient = getSunsamaClient(session);
226
+ // Get the specific task by ID
227
+ const task = await sunsamaClient.getTaskById(taskId);
228
+ if (task) {
229
+ log.info("Successfully retrieved task by ID", {
230
+ taskId: taskId,
231
+ taskText: task.text
232
+ });
233
+ return {
234
+ content: [
235
+ {
236
+ type: "text",
237
+ text: JSON.stringify(task, null, 2)
238
+ }
239
+ ]
240
+ };
241
+ }
242
+ else {
243
+ log.info("Task not found", {
244
+ taskId: taskId
245
+ });
246
+ return {
247
+ content: [
248
+ {
249
+ type: "text",
250
+ text: "null"
251
+ }
252
+ ]
253
+ };
254
+ }
255
+ }
256
+ catch (error) {
257
+ log.error("Failed to get task by ID", {
258
+ taskId: args.taskId,
259
+ error: error instanceof Error ? error.message : 'Unknown error'
260
+ });
261
+ throw new Error(`Failed to get task by ID: ${error instanceof Error ? error.message : 'Unknown error'}`);
262
+ }
263
+ }
264
+ });
213
265
  // Task Mutation Operations
214
266
  server.addTool({
215
267
  name: "create-task",
@@ -470,6 +522,52 @@ server.addTool({
470
522
  }
471
523
  }
472
524
  });
525
+ server.addTool({
526
+ name: "update-task-planned-time",
527
+ description: "Update the planned time (time estimate) for a task",
528
+ parameters: updateTaskPlannedTimeSchema,
529
+ execute: async (args, { session, log }) => {
530
+ try {
531
+ // Extract parameters
532
+ const { taskId, timeEstimateMinutes, limitResponsePayload } = args;
533
+ log.info("Updating task planned time", {
534
+ taskId: taskId,
535
+ timeEstimateMinutes: timeEstimateMinutes,
536
+ limitResponsePayload: limitResponsePayload
537
+ });
538
+ // Get the appropriate client based on transport type
539
+ const sunsamaClient = getSunsamaClient(session);
540
+ // Call sunsamaClient.updateTaskPlannedTime(taskId, timeEstimateMinutes, limitResponsePayload)
541
+ const result = await sunsamaClient.updateTaskPlannedTime(taskId, timeEstimateMinutes, limitResponsePayload);
542
+ log.info("Successfully updated task planned time", {
543
+ taskId: taskId,
544
+ timeEstimateMinutes: timeEstimateMinutes,
545
+ success: result.success
546
+ });
547
+ return {
548
+ content: [
549
+ {
550
+ type: "text",
551
+ text: JSON.stringify({
552
+ success: result.success,
553
+ taskId: taskId,
554
+ timeEstimateMinutes: timeEstimateMinutes,
555
+ updatedFields: result.updatedFields
556
+ })
557
+ }
558
+ ]
559
+ };
560
+ }
561
+ catch (error) {
562
+ log.error("Failed to update task planned time", {
563
+ taskId: args.taskId,
564
+ timeEstimateMinutes: args.timeEstimateMinutes,
565
+ error: error instanceof Error ? error.message : 'Unknown error'
566
+ });
567
+ throw new Error(`Failed to update task planned time: ${error instanceof Error ? error.message : 'Unknown error'}`);
568
+ }
569
+ }
570
+ });
473
571
  // Stream Operations
474
572
  server.addTool({
475
573
  name: "get-streams",
@@ -561,6 +659,11 @@ Uses HTTP Basic Auth headers (per-request authentication):
561
659
  - Returns: TSV of trimmed archived Task objects with pagination metadata header
562
660
  - Pagination: Uses limit+1 pattern to determine if more results are available
563
661
 
662
+ - **get-task-by-id**: Get a specific task by its ID
663
+ - Parameters:
664
+ - \`taskId\` (required): The ID of the task to retrieve
665
+ - Returns: JSON with complete Task object if found, or null if not found
666
+
564
667
  - **create-task**: Create a new task with optional properties
565
668
  - Parameters:
566
669
  - \`text\` (required): Task title/description
package/dist/schemas.d.ts CHANGED
@@ -27,6 +27,13 @@ export declare const getArchivedTasksSchema: z.ZodObject<{
27
27
  offset?: number | undefined;
28
28
  limit?: number | undefined;
29
29
  }>;
30
+ export declare const getTaskByIdSchema: z.ZodObject<{
31
+ taskId: z.ZodString;
32
+ }, "strip", z.ZodTypeAny, {
33
+ taskId: string;
34
+ }, {
35
+ taskId: string;
36
+ }>;
30
37
  /**
31
38
  * User Operation Schemas
32
39
  */
@@ -49,22 +56,22 @@ export declare const createTaskSchema: z.ZodObject<{
49
56
  taskId: z.ZodOptional<z.ZodString>;
50
57
  }, "strip", z.ZodTypeAny, {
51
58
  text: string;
59
+ taskId?: string | undefined;
52
60
  notes?: string | undefined;
53
61
  streamIds?: string[] | undefined;
54
62
  timeEstimate?: number | undefined;
55
63
  dueDate?: string | undefined;
56
64
  snoozeUntil?: string | undefined;
57
65
  private?: boolean | undefined;
58
- taskId?: string | undefined;
59
66
  }, {
60
67
  text: string;
68
+ taskId?: string | undefined;
61
69
  notes?: string | undefined;
62
70
  streamIds?: string[] | undefined;
63
71
  timeEstimate?: number | undefined;
64
72
  dueDate?: string | undefined;
65
73
  snoozeUntil?: string | undefined;
66
74
  private?: boolean | undefined;
67
- taskId?: string | undefined;
68
75
  }>;
69
76
  export declare const updateTaskCompleteSchema: z.ZodObject<{
70
77
  taskId: z.ZodString;
@@ -121,6 +128,19 @@ export declare const updateTaskBacklogSchema: z.ZodObject<{
121
128
  timezone?: string | undefined;
122
129
  limitResponsePayload?: boolean | undefined;
123
130
  }>;
131
+ export declare const updateTaskPlannedTimeSchema: z.ZodObject<{
132
+ taskId: z.ZodString;
133
+ timeEstimateMinutes: z.ZodNumber;
134
+ limitResponsePayload: z.ZodOptional<z.ZodBoolean>;
135
+ }, "strip", z.ZodTypeAny, {
136
+ taskId: string;
137
+ timeEstimateMinutes: number;
138
+ limitResponsePayload?: boolean | undefined;
139
+ }, {
140
+ taskId: string;
141
+ timeEstimateMinutes: number;
142
+ limitResponsePayload?: boolean | undefined;
143
+ }>;
124
144
  /**
125
145
  * Response Type Schemas (for validation and documentation)
126
146
  */
@@ -547,12 +567,14 @@ export type CompletionFilter = z.infer<typeof completionFilterSchema>;
547
567
  export type GetTasksByDayInput = z.infer<typeof getTasksByDaySchema>;
548
568
  export type GetTasksBacklogInput = z.infer<typeof getTasksBacklogSchema>;
549
569
  export type GetArchivedTasksInput = z.infer<typeof getArchivedTasksSchema>;
570
+ export type GetTaskByIdInput = z.infer<typeof getTaskByIdSchema>;
550
571
  export type GetUserInput = z.infer<typeof getUserSchema>;
551
572
  export type GetStreamsInput = z.infer<typeof getStreamsSchema>;
552
573
  export type CreateTaskInput = z.infer<typeof createTaskSchema>;
553
574
  export type UpdateTaskCompleteInput = z.infer<typeof updateTaskCompleteSchema>;
554
575
  export type DeleteTaskInput = z.infer<typeof deleteTaskSchema>;
555
576
  export type UpdateTaskSnoozeDateInput = z.infer<typeof updateTaskSnoozeDateSchema>;
577
+ export type UpdateTaskPlannedTimeInput = z.infer<typeof updateTaskPlannedTimeSchema>;
556
578
  export type User = z.infer<typeof userSchema>;
557
579
  export type Task = z.infer<typeof taskSchema>;
558
580
  export type Stream = z.infer<typeof streamSchema>;
@@ -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
@@ -17,6 +17,10 @@ export const getArchivedTasksSchema = z.object({
17
17
  offset: z.number().int().min(0).optional().describe("Pagination offset (defaults to 0)"),
18
18
  limit: z.number().int().min(1).max(1000).optional().describe("Maximum number of tasks to return (defaults to 100)"),
19
19
  });
20
+ // Get task by ID parameters
21
+ export const getTaskByIdSchema = z.object({
22
+ taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to retrieve"),
23
+ });
20
24
  /**
21
25
  * User Operation Schemas
22
26
  */
@@ -66,6 +70,12 @@ export const updateTaskBacklogSchema = z.object({
66
70
  timezone: z.string().optional().describe("Timezone string (e.g., 'America/New_York'). If not provided, uses user's default timezone"),
67
71
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
68
72
  });
73
+ // Update task planned time parameters
74
+ export const updateTaskPlannedTimeSchema = z.object({
75
+ taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to update planned time for"),
76
+ timeEstimateMinutes: z.number().int().min(0).describe("Time estimate in minutes (use 0 to clear the time estimate)"),
77
+ limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
78
+ });
69
79
  /**
70
80
  * Response Type Schemas (for validation and documentation)
71
81
  */
@@ -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, 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, 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", () => {
@@ -202,6 +202,56 @@ describe("Tool Parameter Schemas", () => {
202
202
  expect(() => updateTaskBacklogSchema.parse({})).toThrow();
203
203
  });
204
204
  });
205
+ describe("updateTaskPlannedTimeSchema", () => {
206
+ test("should accept valid task planned time input", () => {
207
+ const validInput = {
208
+ taskId: "task-123",
209
+ timeEstimateMinutes: 45,
210
+ limitResponsePayload: true,
211
+ };
212
+ expect(() => updateTaskPlannedTimeSchema.parse(validInput)).not.toThrow();
213
+ });
214
+ test("should accept minimal required input", () => {
215
+ const minimalInput = {
216
+ taskId: "task-123",
217
+ timeEstimateMinutes: 30
218
+ };
219
+ expect(() => updateTaskPlannedTimeSchema.parse(minimalInput)).not.toThrow();
220
+ });
221
+ test("should accept zero time estimate", () => {
222
+ const zeroInput = {
223
+ taskId: "task-123",
224
+ timeEstimateMinutes: 0
225
+ };
226
+ expect(() => updateTaskPlannedTimeSchema.parse(zeroInput)).not.toThrow();
227
+ });
228
+ test("should reject empty task ID", () => {
229
+ expect(() => updateTaskPlannedTimeSchema.parse({
230
+ taskId: "",
231
+ timeEstimateMinutes: 30
232
+ })).toThrow();
233
+ expect(() => updateTaskPlannedTimeSchema.parse({
234
+ timeEstimateMinutes: 30
235
+ })).toThrow();
236
+ });
237
+ test("should reject negative time estimate", () => {
238
+ expect(() => updateTaskPlannedTimeSchema.parse({
239
+ taskId: "task-123",
240
+ timeEstimateMinutes: -1
241
+ })).toThrow();
242
+ });
243
+ test("should reject non-integer time estimate", () => {
244
+ expect(() => updateTaskPlannedTimeSchema.parse({
245
+ taskId: "task-123",
246
+ timeEstimateMinutes: 30.5
247
+ })).toThrow();
248
+ });
249
+ test("should reject missing time estimate", () => {
250
+ expect(() => updateTaskPlannedTimeSchema.parse({
251
+ taskId: "task-123"
252
+ })).toThrow();
253
+ });
254
+ });
205
255
  });
206
256
  describe("Response Schemas", () => {
207
257
  describe("userProfileSchema", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-sunsama",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server for Sunsama API integration",
5
5
  "type": "module",
6
6
  "private": false,
@@ -26,7 +26,7 @@
26
26
  "@types/papaparse": "^5.3.16",
27
27
  "fastmcp": "3.3.1",
28
28
  "papaparse": "^5.5.3",
29
- "sunsama-api": "0.6.1",
29
+ "sunsama-api": "0.7.0",
30
30
  "zod": "3.24.4"
31
31
  },
32
32
  "devDependencies": {
package/src/main.ts CHANGED
@@ -10,11 +10,13 @@ import {
10
10
  deleteTaskSchema,
11
11
  getArchivedTasksSchema,
12
12
  getStreamsSchema,
13
+ getTaskByIdSchema,
13
14
  getTasksBacklogSchema,
14
15
  getTasksByDaySchema,
15
16
  getUserSchema,
16
17
  updateTaskBacklogSchema,
17
18
  updateTaskCompleteSchema,
19
+ updateTaskPlannedTimeSchema,
18
20
  updateTaskSnoozeDateSchema
19
21
  } from "./schemas.js";
20
22
  import { getSunsamaClient } from "./utils/client-resolver.js";
@@ -32,14 +34,15 @@ if (transportConfig.transportType === "stdio") {
32
34
 
33
35
  const server = new FastMCP({
34
36
  name: "Sunsama API Server",
35
- version: "0.6.0",
37
+ version: "0.8.0",
36
38
  instructions: `
37
39
  This MCP server provides access to the Sunsama API for task and project management.
38
40
 
39
41
  Available tools:
40
42
  - Authentication: login, logout, check authentication status
41
43
  - User operations: get current user information
42
- - Task operations: get tasks by day, get backlog tasks, get archived tasks
44
+ - Task operations: get tasks by day, get backlog tasks, get archived tasks, get task by ID
45
+ - Task mutations: create tasks, mark complete, delete tasks, reschedule tasks, update planned time
43
46
  - Stream operations: get streams/channels for the user's group
44
47
 
45
48
  Authentication is required for all operations. You can either:
@@ -262,6 +265,64 @@ ${toTsv(trimmedTasks)}`;
262
265
  }
263
266
  });
264
267
 
268
+ server.addTool({
269
+ name: "get-task-by-id",
270
+ description: "Get a specific task by its ID",
271
+ parameters: getTaskByIdSchema,
272
+ execute: async (args, {session, log}) => {
273
+ try {
274
+ const {taskId} = args;
275
+
276
+ log.info("Getting task by ID", {
277
+ taskId: taskId
278
+ });
279
+
280
+ // Get the appropriate client based on transport type
281
+ const sunsamaClient = getSunsamaClient(session as SessionData | null);
282
+
283
+ // Get the specific task by ID
284
+ const task = await sunsamaClient.getTaskById(taskId);
285
+
286
+ if (task) {
287
+ log.info("Successfully retrieved task by ID", {
288
+ taskId: taskId,
289
+ taskText: task.text
290
+ });
291
+
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: JSON.stringify(task, null, 2)
297
+ }
298
+ ]
299
+ };
300
+ } else {
301
+ log.info("Task not found", {
302
+ taskId: taskId
303
+ });
304
+
305
+ return {
306
+ content: [
307
+ {
308
+ type: "text",
309
+ text: "null"
310
+ }
311
+ ]
312
+ };
313
+ }
314
+
315
+ } catch (error) {
316
+ log.error("Failed to get task by ID", {
317
+ taskId: args.taskId,
318
+ error: error instanceof Error ? error.message : 'Unknown error'
319
+ });
320
+
321
+ throw new Error(`Failed to get task by ID: ${error instanceof Error ? error.message : 'Unknown error'}`);
322
+ }
323
+ }
324
+ });
325
+
265
326
  // Task Mutation Operations
266
327
  server.addTool({
267
328
  name: "create-task",
@@ -571,6 +632,63 @@ server.addTool({
571
632
  }
572
633
  });
573
634
 
635
+ server.addTool({
636
+ name: "update-task-planned-time",
637
+ description: "Update the planned time (time estimate) for a task",
638
+ parameters: updateTaskPlannedTimeSchema,
639
+ execute: async (args, {session, log}) => {
640
+ try {
641
+ // Extract parameters
642
+ const {taskId, timeEstimateMinutes, limitResponsePayload} = args;
643
+
644
+ log.info("Updating task planned time", {
645
+ taskId: taskId,
646
+ timeEstimateMinutes: timeEstimateMinutes,
647
+ limitResponsePayload: limitResponsePayload
648
+ });
649
+
650
+ // Get the appropriate client based on transport type
651
+ const sunsamaClient = getSunsamaClient(session as SessionData | null);
652
+
653
+ // Call sunsamaClient.updateTaskPlannedTime(taskId, timeEstimateMinutes, limitResponsePayload)
654
+ const result = await sunsamaClient.updateTaskPlannedTime(
655
+ taskId,
656
+ timeEstimateMinutes,
657
+ limitResponsePayload
658
+ );
659
+
660
+ log.info("Successfully updated task planned time", {
661
+ taskId: taskId,
662
+ timeEstimateMinutes: timeEstimateMinutes,
663
+ success: result.success
664
+ });
665
+
666
+ return {
667
+ content: [
668
+ {
669
+ type: "text",
670
+ text: JSON.stringify({
671
+ success: result.success,
672
+ taskId: taskId,
673
+ timeEstimateMinutes: timeEstimateMinutes,
674
+ updatedFields: result.updatedFields
675
+ })
676
+ }
677
+ ]
678
+ };
679
+
680
+ } catch (error) {
681
+ log.error("Failed to update task planned time", {
682
+ taskId: args.taskId,
683
+ timeEstimateMinutes: args.timeEstimateMinutes,
684
+ error: error instanceof Error ? error.message : 'Unknown error'
685
+ });
686
+
687
+ throw new Error(`Failed to update task planned time: ${error instanceof Error ? error.message : 'Unknown error'}`);
688
+ }
689
+ }
690
+ });
691
+
574
692
  // Stream Operations
575
693
  server.addTool({
576
694
  name: "get-streams",
@@ -668,6 +786,11 @@ Uses HTTP Basic Auth headers (per-request authentication):
668
786
  - Returns: TSV of trimmed archived Task objects with pagination metadata header
669
787
  - Pagination: Uses limit+1 pattern to determine if more results are available
670
788
 
789
+ - **get-task-by-id**: Get a specific task by its ID
790
+ - Parameters:
791
+ - \`taskId\` (required): The ID of the task to retrieve
792
+ - Returns: JSON with complete Task object if found, or null if not found
793
+
671
794
  - **create-task**: Create a new task with optional properties
672
795
  - Parameters:
673
796
  - \`text\` (required): Task title/description
@@ -11,6 +11,7 @@ import {
11
11
  deleteTaskSchema,
12
12
  updateTaskSnoozeDateSchema,
13
13
  updateTaskBacklogSchema,
14
+ updateTaskPlannedTimeSchema,
14
15
  userProfileSchema,
15
16
  groupSchema,
16
17
  userSchema,
@@ -283,6 +284,73 @@ describe("Tool Parameter Schemas", () => {
283
284
  ).toThrow();
284
285
  });
285
286
  });
287
+
288
+ describe("updateTaskPlannedTimeSchema", () => {
289
+ test("should accept valid task planned time input", () => {
290
+ const validInput = {
291
+ taskId: "task-123",
292
+ timeEstimateMinutes: 45,
293
+ limitResponsePayload: true,
294
+ };
295
+ expect(() => updateTaskPlannedTimeSchema.parse(validInput)).not.toThrow();
296
+ });
297
+
298
+ test("should accept minimal required input", () => {
299
+ const minimalInput = {
300
+ taskId: "task-123",
301
+ timeEstimateMinutes: 30
302
+ };
303
+ expect(() => updateTaskPlannedTimeSchema.parse(minimalInput)).not.toThrow();
304
+ });
305
+
306
+ test("should accept zero time estimate", () => {
307
+ const zeroInput = {
308
+ taskId: "task-123",
309
+ timeEstimateMinutes: 0
310
+ };
311
+ expect(() => updateTaskPlannedTimeSchema.parse(zeroInput)).not.toThrow();
312
+ });
313
+
314
+ test("should reject empty task ID", () => {
315
+ expect(() =>
316
+ updateTaskPlannedTimeSchema.parse({
317
+ taskId: "",
318
+ timeEstimateMinutes: 30
319
+ })
320
+ ).toThrow();
321
+ expect(() =>
322
+ updateTaskPlannedTimeSchema.parse({
323
+ timeEstimateMinutes: 30
324
+ })
325
+ ).toThrow();
326
+ });
327
+
328
+ test("should reject negative time estimate", () => {
329
+ expect(() =>
330
+ updateTaskPlannedTimeSchema.parse({
331
+ taskId: "task-123",
332
+ timeEstimateMinutes: -1
333
+ })
334
+ ).toThrow();
335
+ });
336
+
337
+ test("should reject non-integer time estimate", () => {
338
+ expect(() =>
339
+ updateTaskPlannedTimeSchema.parse({
340
+ taskId: "task-123",
341
+ timeEstimateMinutes: 30.5
342
+ })
343
+ ).toThrow();
344
+ });
345
+
346
+ test("should reject missing time estimate", () => {
347
+ expect(() =>
348
+ updateTaskPlannedTimeSchema.parse({
349
+ taskId: "task-123"
350
+ })
351
+ ).toThrow();
352
+ });
353
+ });
286
354
  });
287
355
 
288
356
  describe("Response Schemas", () => {
package/src/schemas.ts CHANGED
@@ -23,6 +23,11 @@ export const getArchivedTasksSchema = z.object({
23
23
  limit: z.number().int().min(1).max(1000).optional().describe("Maximum number of tasks to return (defaults to 100)"),
24
24
  });
25
25
 
26
+ // Get task by ID parameters
27
+ export const getTaskByIdSchema = z.object({
28
+ taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to retrieve"),
29
+ });
30
+
26
31
  /**
27
32
  * User Operation Schemas
28
33
  */
@@ -82,6 +87,13 @@ export const updateTaskBacklogSchema = z.object({
82
87
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
83
88
  });
84
89
 
90
+ // Update task planned time parameters
91
+ export const updateTaskPlannedTimeSchema = z.object({
92
+ taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to update planned time for"),
93
+ timeEstimateMinutes: z.number().int().min(0).describe("Time estimate in minutes (use 0 to clear the time estimate)"),
94
+ limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
95
+ });
96
+
85
97
  /**
86
98
  * Response Type Schemas (for validation and documentation)
87
99
  */
@@ -175,6 +187,7 @@ export type CompletionFilter = z.infer<typeof completionFilterSchema>;
175
187
  export type GetTasksByDayInput = z.infer<typeof getTasksByDaySchema>;
176
188
  export type GetTasksBacklogInput = z.infer<typeof getTasksBacklogSchema>;
177
189
  export type GetArchivedTasksInput = z.infer<typeof getArchivedTasksSchema>;
190
+ export type GetTaskByIdInput = z.infer<typeof getTaskByIdSchema>;
178
191
  export type GetUserInput = z.infer<typeof getUserSchema>;
179
192
  export type GetStreamsInput = z.infer<typeof getStreamsSchema>;
180
193
 
@@ -182,6 +195,7 @@ export type CreateTaskInput = z.infer<typeof createTaskSchema>;
182
195
  export type UpdateTaskCompleteInput = z.infer<typeof updateTaskCompleteSchema>;
183
196
  export type DeleteTaskInput = z.infer<typeof deleteTaskSchema>;
184
197
  export type UpdateTaskSnoozeDateInput = z.infer<typeof updateTaskSnoozeDateSchema>;
198
+ export type UpdateTaskPlannedTimeInput = z.infer<typeof updateTaskPlannedTimeSchema>;
185
199
 
186
200
  export type User = z.infer<typeof userSchema>;
187
201
  export type Task = z.infer<typeof taskSchema>;