mcp-sunsama 0.2.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +29 -0
- package/.env.example +10 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +137 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/TODO_PREPUBLISH.md +182 -0
- package/bun.lock +515 -0
- package/dist/auth/http.d.ts +20 -0
- package/dist/auth/http.d.ts.map +1 -0
- package/dist/auth/http.js +52 -0
- package/dist/auth/stdio.d.ts +13 -0
- package/dist/auth/stdio.d.ts.map +1 -0
- package/dist/auth/stdio.js +27 -0
- package/dist/auth/types.d.ts +9 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +1 -0
- package/dist/config/transport.d.ts +32 -0
- package/dist/config/transport.d.ts.map +1 -0
- package/dist/config/transport.js +62 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +473 -0
- package/dist/schemas.d.ts +522 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +124 -0
- package/dist/utils/client-resolver.d.ts +10 -0
- package/dist/utils/client-resolver.d.ts.map +1 -0
- package/dist/utils/client-resolver.js +19 -0
- package/dist/utils/task-filters.d.ts +29 -0
- package/dist/utils/task-filters.d.ts.map +1 -0
- package/dist/utils/task-filters.js +42 -0
- package/dist/utils/task-trimmer.d.ts +47 -0
- package/dist/utils/task-trimmer.d.ts.map +1 -0
- package/dist/utils/task-trimmer.js +50 -0
- package/dist/utils/to-tsv.d.ts +8 -0
- package/dist/utils/to-tsv.d.ts.map +1 -0
- package/dist/utils/to-tsv.js +64 -0
- package/mcp-inspector.json +14 -0
- package/package.json +56 -0
- package/src/auth/http.ts +61 -0
- package/src/auth/stdio.ts +33 -0
- package/src/auth/types.ts +9 -0
- package/src/config/transport.ts +80 -0
- package/src/main.ts +542 -0
- package/src/schemas.ts +169 -0
- package/src/utils/client-resolver.ts +23 -0
- package/src/utils/task-filters.ts +49 -0
- package/src/utils/task-trimmer.ts +81 -0
- package/src/utils/to-tsv.ts +73 -0
- package/tsconfig.json +36 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { FastMCP } from "fastmcp";
|
|
2
|
+
import type { CreateTaskOptions } from "sunsama-api";
|
|
3
|
+
import { httpStreamAuthenticator } from "./auth/http.js";
|
|
4
|
+
import { initializeStdioAuth } from "./auth/stdio.js";
|
|
5
|
+
import type { SessionData } from "./auth/types.js";
|
|
6
|
+
import { getTransportConfig } from "./config/transport.js";
|
|
7
|
+
import {
|
|
8
|
+
createTaskSchema,
|
|
9
|
+
deleteTaskSchema,
|
|
10
|
+
getStreamsSchema,
|
|
11
|
+
getTasksBacklogSchema,
|
|
12
|
+
getTasksByDaySchema,
|
|
13
|
+
getUserSchema,
|
|
14
|
+
updateTaskCompleteSchema
|
|
15
|
+
} from "./schemas.js";
|
|
16
|
+
import { getSunsamaClient } from "./utils/client-resolver.js";
|
|
17
|
+
import { filterTasksByCompletion } from "./utils/task-filters.js";
|
|
18
|
+
import { trimTasksForResponse } from "./utils/task-trimmer.js";
|
|
19
|
+
import { toTsv } from "./utils/to-tsv.js";
|
|
20
|
+
|
|
21
|
+
// Get transport configuration with validation
|
|
22
|
+
const transportConfig = getTransportConfig();
|
|
23
|
+
|
|
24
|
+
// For stdio transport, authenticate at startup with environment variables
|
|
25
|
+
if (transportConfig.transportType === "stdio") {
|
|
26
|
+
await initializeStdioAuth();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const server = new FastMCP({
|
|
31
|
+
name: "Sunsama API Server",
|
|
32
|
+
version: "0.2.0",
|
|
33
|
+
instructions: `
|
|
34
|
+
This MCP server provides access to the Sunsama API for task and project management.
|
|
35
|
+
|
|
36
|
+
Available tools:
|
|
37
|
+
- Authentication: login, logout, check authentication status
|
|
38
|
+
- User operations: get current user information
|
|
39
|
+
- Task operations: get tasks by day, get backlog tasks
|
|
40
|
+
- Stream operations: get streams/channels for the user's group
|
|
41
|
+
|
|
42
|
+
Authentication is required for all operations. You can either:
|
|
43
|
+
1. Login with email/password using the 'login' tool
|
|
44
|
+
2. Use a session token if you have one
|
|
45
|
+
|
|
46
|
+
The server maintains session state per MCP connection, so you only need to authenticate once per session.
|
|
47
|
+
`.trim(),
|
|
48
|
+
// dynamically handle authentication
|
|
49
|
+
...(transportConfig.transportType === "httpStream" ? {
|
|
50
|
+
authenticate: httpStreamAuthenticator,
|
|
51
|
+
} : {})
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// User Operations
|
|
55
|
+
server.addTool({
|
|
56
|
+
name: "get-user",
|
|
57
|
+
description: "Get current user information including profile, timezone, and group details",
|
|
58
|
+
parameters: getUserSchema,
|
|
59
|
+
execute: async (_args, {session, log}) => {
|
|
60
|
+
try {
|
|
61
|
+
log.info("Getting user information");
|
|
62
|
+
|
|
63
|
+
// Get the appropriate client based on transport type
|
|
64
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
65
|
+
|
|
66
|
+
// Get user information
|
|
67
|
+
const user = await sunsamaClient.getUser();
|
|
68
|
+
|
|
69
|
+
log.info("Successfully retrieved user information", {userId: user._id});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify(user, null, 2)
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
log.error("Failed to get user information", {error: error instanceof Error ? error.message : 'Unknown error'});
|
|
82
|
+
|
|
83
|
+
throw new Error(`Failed to get user information: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Task Operations
|
|
89
|
+
server.addTool({
|
|
90
|
+
name: "get-tasks-backlog",
|
|
91
|
+
description: "Get tasks from the backlog",
|
|
92
|
+
parameters: getTasksBacklogSchema,
|
|
93
|
+
execute: async (_args, {session, log}) => {
|
|
94
|
+
try {
|
|
95
|
+
log.info("Getting backlog tasks");
|
|
96
|
+
|
|
97
|
+
// Get the appropriate client based on transport type
|
|
98
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
99
|
+
|
|
100
|
+
// Get backlog tasks
|
|
101
|
+
const tasks = await sunsamaClient.getTasksBacklog();
|
|
102
|
+
|
|
103
|
+
// Trim tasks to reduce response size while preserving essential data
|
|
104
|
+
const trimmedTasks = trimTasksForResponse(tasks);
|
|
105
|
+
|
|
106
|
+
log.info("Successfully retrieved backlog tasks", {count: tasks.length});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: toTsv(trimmedTasks)
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
} catch (error) {
|
|
118
|
+
log.error("Failed to get backlog tasks", {error: error instanceof Error ? error.message : 'Unknown error'});
|
|
119
|
+
|
|
120
|
+
throw new Error(`Failed to get backlog tasks: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.addTool({
|
|
126
|
+
name: "get-tasks-by-day",
|
|
127
|
+
description: "Get tasks for a specific day with optional filtering by completion status",
|
|
128
|
+
parameters: getTasksByDaySchema,
|
|
129
|
+
execute: async (args, {session, log}) => {
|
|
130
|
+
try {
|
|
131
|
+
// Extract and set defaults for parameters
|
|
132
|
+
const completionFilter = args.completionFilter || "all";
|
|
133
|
+
|
|
134
|
+
log.info("Getting tasks for day", {
|
|
135
|
+
day: args.day,
|
|
136
|
+
timezone: args.timezone,
|
|
137
|
+
completionFilter: completionFilter
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Get the appropriate client based on transport type
|
|
141
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
142
|
+
|
|
143
|
+
// If no timezone provided, we need to get the user's default timezone
|
|
144
|
+
let timezone = args.timezone;
|
|
145
|
+
if (!timezone) {
|
|
146
|
+
timezone = await sunsamaClient.getUserTimezone();
|
|
147
|
+
log.info("Using user's default timezone", {timezone});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Get tasks for the specified day with the determined timezone
|
|
151
|
+
const tasks = await sunsamaClient.getTasksByDay(args.day, timezone);
|
|
152
|
+
|
|
153
|
+
// Apply completion filter BEFORE trimming for efficiency
|
|
154
|
+
const filteredTasks = filterTasksByCompletion(tasks, completionFilter);
|
|
155
|
+
|
|
156
|
+
// Trim tasks to reduce response size while preserving essential data
|
|
157
|
+
const trimmedTasks = trimTasksForResponse(filteredTasks);
|
|
158
|
+
|
|
159
|
+
log.info("Successfully retrieved tasks for day", {
|
|
160
|
+
day: args.day,
|
|
161
|
+
totalCount: tasks.length,
|
|
162
|
+
filteredCount: filteredTasks.length,
|
|
163
|
+
filter: completionFilter,
|
|
164
|
+
timezone: timezone
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: toTsv(trimmedTasks)
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
} catch (error) {
|
|
177
|
+
log.error("Failed to get tasks by day", {
|
|
178
|
+
day: args.day,
|
|
179
|
+
timezone: args.timezone,
|
|
180
|
+
completionFilter: args.completionFilter,
|
|
181
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
throw new Error(`Failed to get tasks for ${args.day}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Task Mutation Operations
|
|
190
|
+
server.addTool({
|
|
191
|
+
name: "create-task",
|
|
192
|
+
description: "Create a new task with optional properties",
|
|
193
|
+
parameters: createTaskSchema,
|
|
194
|
+
execute: async (args, {session, log}) => {
|
|
195
|
+
try {
|
|
196
|
+
// Extract parameters from args
|
|
197
|
+
const {text, notes, streamIds, timeEstimate, dueDate, snoozeUntil, private: isPrivate, taskId} = args;
|
|
198
|
+
|
|
199
|
+
log.info("Creating new task", {
|
|
200
|
+
text: text,
|
|
201
|
+
hasNotes: !!notes,
|
|
202
|
+
streamCount: streamIds?.length || 0,
|
|
203
|
+
timeEstimate: timeEstimate,
|
|
204
|
+
hasDueDate: !!dueDate,
|
|
205
|
+
hasSnooze: !!snoozeUntil,
|
|
206
|
+
isPrivate: isPrivate,
|
|
207
|
+
customTaskId: !!taskId
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Get the appropriate client based on transport type
|
|
211
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
212
|
+
|
|
213
|
+
// Build options object for createTask
|
|
214
|
+
const options: CreateTaskOptions = {};
|
|
215
|
+
if (notes) options.notes = notes;
|
|
216
|
+
if (streamIds) options.streamIds = streamIds;
|
|
217
|
+
if (timeEstimate) options.timeEstimate = timeEstimate;
|
|
218
|
+
if (dueDate) options.dueDate = dueDate;
|
|
219
|
+
if (snoozeUntil) options.snoozeUntil = snoozeUntil;
|
|
220
|
+
if (isPrivate !== undefined) options.private = isPrivate;
|
|
221
|
+
if (taskId) options.taskId = taskId;
|
|
222
|
+
|
|
223
|
+
// Call sunsamaClient.createTask(text, options)
|
|
224
|
+
const result = await sunsamaClient.createTask(text, options);
|
|
225
|
+
|
|
226
|
+
log.info("Successfully created task", {
|
|
227
|
+
taskId: result.updatedFields?._id || 'unknown',
|
|
228
|
+
title: text,
|
|
229
|
+
success: result.success
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: JSON.stringify({
|
|
237
|
+
success: result.success,
|
|
238
|
+
taskId: result.updatedFields?._id,
|
|
239
|
+
title: text,
|
|
240
|
+
created: true,
|
|
241
|
+
updatedFields: result.updatedFields
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
} catch (error) {
|
|
248
|
+
log.error("Failed to create task", {
|
|
249
|
+
text: args.text,
|
|
250
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
throw new Error(`Failed to create task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
server.addTool({
|
|
259
|
+
name: "update-task-complete",
|
|
260
|
+
description: "Mark a task as complete with optional completion timestamp",
|
|
261
|
+
parameters: updateTaskCompleteSchema,
|
|
262
|
+
execute: async (args, {session, log}) => {
|
|
263
|
+
try {
|
|
264
|
+
// Extract taskId and optional parameters
|
|
265
|
+
const {taskId, completeOn, limitResponsePayload} = args;
|
|
266
|
+
|
|
267
|
+
log.info("Marking task as complete", {
|
|
268
|
+
taskId: taskId,
|
|
269
|
+
hasCustomCompleteOn: !!completeOn,
|
|
270
|
+
limitResponsePayload: limitResponsePayload
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Get the appropriate client based on transport type
|
|
274
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
275
|
+
|
|
276
|
+
// Call sunsamaClient.updateTaskComplete(taskId, completeOn, limitResponsePayload)
|
|
277
|
+
const result = await sunsamaClient.updateTaskComplete(
|
|
278
|
+
taskId,
|
|
279
|
+
completeOn,
|
|
280
|
+
limitResponsePayload
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
log.info("Successfully marked task as complete", {
|
|
284
|
+
taskId: taskId,
|
|
285
|
+
success: result.success,
|
|
286
|
+
updatedFields: !!result.updatedFields
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: JSON.stringify({
|
|
294
|
+
success: result.success,
|
|
295
|
+
taskId: taskId,
|
|
296
|
+
completed: true,
|
|
297
|
+
updatedFields: result.updatedFields
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
log.error("Failed to mark task as complete", {
|
|
305
|
+
taskId: args.taskId,
|
|
306
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
throw new Error(`Failed to mark task as complete: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
server.addTool({
|
|
315
|
+
name: "delete-task",
|
|
316
|
+
description: "Delete a task permanently",
|
|
317
|
+
parameters: deleteTaskSchema,
|
|
318
|
+
execute: async (args, {session, log}) => {
|
|
319
|
+
try {
|
|
320
|
+
// Extract taskId and optional parameters
|
|
321
|
+
const {taskId, limitResponsePayload, wasTaskMerged} = args;
|
|
322
|
+
|
|
323
|
+
log.info("Deleting task", {
|
|
324
|
+
taskId: taskId,
|
|
325
|
+
limitResponsePayload: limitResponsePayload,
|
|
326
|
+
wasTaskMerged: wasTaskMerged
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Get the appropriate client based on transport type
|
|
330
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
331
|
+
|
|
332
|
+
// Call sunsamaClient.deleteTask(taskId, limitResponsePayload, wasTaskMerged)
|
|
333
|
+
const result = await sunsamaClient.deleteTask(
|
|
334
|
+
taskId,
|
|
335
|
+
limitResponsePayload,
|
|
336
|
+
wasTaskMerged
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
log.info("Successfully deleted task", {
|
|
340
|
+
taskId: taskId,
|
|
341
|
+
success: result.success
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: JSON.stringify({
|
|
349
|
+
success: result.success,
|
|
350
|
+
taskId: taskId,
|
|
351
|
+
deleted: true,
|
|
352
|
+
updatedFields: result.updatedFields
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
} catch (error) {
|
|
359
|
+
log.error("Failed to delete task", {
|
|
360
|
+
taskId: args.taskId,
|
|
361
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
throw new Error(`Failed to delete task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Stream Operations
|
|
370
|
+
server.addTool({
|
|
371
|
+
name: "get-streams",
|
|
372
|
+
description: "Get streams for the user's group (streams are called 'channels' in the Sunsama UI)",
|
|
373
|
+
parameters: getStreamsSchema,
|
|
374
|
+
execute: async (_args, {session, log}) => {
|
|
375
|
+
try {
|
|
376
|
+
log.info("Getting streams for user's group");
|
|
377
|
+
|
|
378
|
+
// Get the appropriate client based on transport type
|
|
379
|
+
const sunsamaClient = getSunsamaClient(session as SessionData | null);
|
|
380
|
+
|
|
381
|
+
// Get streams for the user's group
|
|
382
|
+
const streams = await sunsamaClient.getStreamsByGroupId();
|
|
383
|
+
|
|
384
|
+
log.info("Successfully retrieved streams", {count: streams.length});
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: toTsv(streams)
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
} catch (error) {
|
|
396
|
+
log.error("Failed to get streams", {error: error instanceof Error ? error.message : 'Unknown error'});
|
|
397
|
+
|
|
398
|
+
throw new Error(`Failed to get streams: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
server.addResource({
|
|
404
|
+
uri: "sunsama://api/docs",
|
|
405
|
+
name: "Sunsama API Documentation",
|
|
406
|
+
description: "Documentation for the Sunsama API endpoints and data structures",
|
|
407
|
+
mimeType: "text/markdown",
|
|
408
|
+
load: async () => {
|
|
409
|
+
return {
|
|
410
|
+
text: `# Sunsama MCP Server Documentation
|
|
411
|
+
|
|
412
|
+
## Overview
|
|
413
|
+
This MCP server provides access to the Sunsama API for task and project management.
|
|
414
|
+
Authentication is handled server-side using environment variables.
|
|
415
|
+
|
|
416
|
+
## Authentication
|
|
417
|
+
The server authenticates to Sunsama using environment variables:
|
|
418
|
+
- \`SUNSAMA_EMAIL\`: Your Sunsama account email
|
|
419
|
+
- \`SUNSAMA_PASSWORD\`: Your Sunsama account password
|
|
420
|
+
|
|
421
|
+
Authentication happens automatically on server startup. No client-side authentication is required.
|
|
422
|
+
|
|
423
|
+
## Available Tools
|
|
424
|
+
|
|
425
|
+
### User Operations
|
|
426
|
+
- **get-user**: Get current user information
|
|
427
|
+
- Parameters: none
|
|
428
|
+
- Returns: User object with profile, timezone, and primary group details
|
|
429
|
+
|
|
430
|
+
### Task Operations
|
|
431
|
+
- **get-tasks-by-day**: Get tasks for a specific day with optional filtering
|
|
432
|
+
- Parameters:
|
|
433
|
+
- \`day\` (required): Date in YYYY-MM-DD format
|
|
434
|
+
- \`timezone\` (optional): Timezone string (e.g., "America/New_York")
|
|
435
|
+
- \`completionFilter\` (optional): Filter by completion status
|
|
436
|
+
- \`"all"\` (default): Return all tasks
|
|
437
|
+
- \`"incomplete"\`: Return only incomplete tasks
|
|
438
|
+
- \`"completed"\`: Return only completed tasks
|
|
439
|
+
- Returns: Array of filtered Task objects for the specified day
|
|
440
|
+
|
|
441
|
+
- **get-tasks-backlog**: Get tasks from the backlog
|
|
442
|
+
- Parameters: none
|
|
443
|
+
- Returns: Array of Task objects from the backlog
|
|
444
|
+
|
|
445
|
+
### Stream Operations
|
|
446
|
+
- **get-streams**: Get streams for the user's group
|
|
447
|
+
- Parameters: none
|
|
448
|
+
- Returns: Array of Stream objects
|
|
449
|
+
- Note: Streams are called "channels" in the Sunsama UI. If a user requests channels, use this tool.
|
|
450
|
+
|
|
451
|
+
## Data Types
|
|
452
|
+
|
|
453
|
+
### User Object
|
|
454
|
+
\`\`\`typescript
|
|
455
|
+
{
|
|
456
|
+
_id: string;
|
|
457
|
+
email: string;
|
|
458
|
+
profile: {
|
|
459
|
+
_id: string;
|
|
460
|
+
email: string;
|
|
461
|
+
firstName: string;
|
|
462
|
+
lastName: string;
|
|
463
|
+
timezone: string;
|
|
464
|
+
avatarUrl?: string;
|
|
465
|
+
};
|
|
466
|
+
primaryGroup?: {
|
|
467
|
+
groupId: string;
|
|
468
|
+
name: string;
|
|
469
|
+
role?: string;
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
\`\`\`
|
|
473
|
+
|
|
474
|
+
### Task Object
|
|
475
|
+
\`\`\`typescript
|
|
476
|
+
{
|
|
477
|
+
_id: string;
|
|
478
|
+
title: string;
|
|
479
|
+
description?: string;
|
|
480
|
+
status: string;
|
|
481
|
+
createdAt: string;
|
|
482
|
+
updatedAt: string;
|
|
483
|
+
scheduledDate?: string;
|
|
484
|
+
completedAt?: string;
|
|
485
|
+
streamId?: string;
|
|
486
|
+
userId: string;
|
|
487
|
+
groupId: string;
|
|
488
|
+
}
|
|
489
|
+
\`\`\`
|
|
490
|
+
|
|
491
|
+
### Stream Object
|
|
492
|
+
Note: Streams are called "channels" in the Sunsama UI.
|
|
493
|
+
\`\`\`typescript
|
|
494
|
+
{
|
|
495
|
+
_id: string;
|
|
496
|
+
name: string;
|
|
497
|
+
color?: string;
|
|
498
|
+
groupId: string;
|
|
499
|
+
isActive: boolean;
|
|
500
|
+
createdAt: string;
|
|
501
|
+
updatedAt: string;
|
|
502
|
+
}
|
|
503
|
+
\`\`\`
|
|
504
|
+
|
|
505
|
+
## Error Handling
|
|
506
|
+
- All operations require valid Sunsama authentication
|
|
507
|
+
- Invalid dates will return validation errors
|
|
508
|
+
- Network errors are handled gracefully with descriptive messages
|
|
509
|
+
- Server maintains session state across tool calls
|
|
510
|
+
|
|
511
|
+
## Environment Setup
|
|
512
|
+
Required environment variables:
|
|
513
|
+
- \`API_KEY\`: MCP server authentication key
|
|
514
|
+
- \`SUNSAMA_EMAIL\`: Sunsama account email
|
|
515
|
+
- \`SUNSAMA_PASSWORD\`: Sunsama account password
|
|
516
|
+
- \`PORT\`: Server port (default: 3000)
|
|
517
|
+
`.trim()
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
// Start server with dynamic transport configuration
|
|
524
|
+
if (transportConfig.transportType === "httpStream") {
|
|
525
|
+
// Log startup information
|
|
526
|
+
console.log(`HTTP Stream configuration: port=${transportConfig.httpStream?.port}, endpoint=${transportConfig.httpStream?.endpoint}`);
|
|
527
|
+
|
|
528
|
+
server.start({
|
|
529
|
+
transportType: "httpStream",
|
|
530
|
+
httpStream: {
|
|
531
|
+
port: transportConfig.httpStream!.port
|
|
532
|
+
}
|
|
533
|
+
}).then(() => {
|
|
534
|
+
console.log(`Sunsama MCP Server running on port ${transportConfig.httpStream!.port}`);
|
|
535
|
+
console.log(`HTTP endpoint: ${transportConfig.httpStream!.endpoint}`);
|
|
536
|
+
console.log("Authentication: HTTP Basic Auth with Sunsama credentials");
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
server.start({
|
|
540
|
+
transportType: "stdio"
|
|
541
|
+
});
|
|
542
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Task Operation Schemas
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Completion filter schema
|
|
8
|
+
export const completionFilterSchema = z.enum(["all", "incomplete", "completed"]);
|
|
9
|
+
|
|
10
|
+
// Get tasks by day parameters
|
|
11
|
+
export const getTasksByDaySchema = z.object({
|
|
12
|
+
day: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Day must be in YYYY-MM-DD format"),
|
|
13
|
+
timezone: z.string().optional().describe("Timezone string (e.g., 'America/New_York'). If not provided, uses user's default timezone"),
|
|
14
|
+
completionFilter: completionFilterSchema.optional().describe("Filter tasks by completion status. 'all' returns all tasks, 'incomplete' returns only incomplete tasks, 'completed' returns only completed tasks. Defaults to 'all'"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Get tasks backlog parameters (no parameters needed)
|
|
18
|
+
export const getTasksBacklogSchema = z.object({});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* User Operation Schemas
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Get user parameters (no parameters needed)
|
|
25
|
+
export const getUserSchema = z.object({});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stream Operation Schemas
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Get streams parameters (no parameters needed, uses cached group ID)
|
|
32
|
+
export const getStreamsSchema = z.object({});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Task Mutation Operation Schemas
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Create task parameters
|
|
39
|
+
export const createTaskSchema = z.object({
|
|
40
|
+
text: z.string().min(1, "Task text is required").describe("Task title/description"),
|
|
41
|
+
notes: z.string().optional().describe("Additional task notes"),
|
|
42
|
+
streamIds: z.array(z.string()).optional().describe("Array of stream IDs to associate with the task"),
|
|
43
|
+
timeEstimate: z.number().int().positive().optional().describe("Time estimate in minutes"),
|
|
44
|
+
dueDate: z.string().optional().describe("Due date string (ISO format)"),
|
|
45
|
+
snoozeUntil: z.string().optional().describe("Snooze until date string (ISO format)"),
|
|
46
|
+
private: z.boolean().optional().describe("Whether the task is private"),
|
|
47
|
+
taskId: z.string().optional().describe("Custom task ID (auto-generated if not provided)"),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Update task complete parameters
|
|
51
|
+
export const updateTaskCompleteSchema = z.object({
|
|
52
|
+
taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to mark as complete"),
|
|
53
|
+
completeOn: z.string().optional().describe("Completion timestamp (ISO format). Defaults to current time"),
|
|
54
|
+
limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Delete task parameters
|
|
58
|
+
export const deleteTaskSchema = z.object({
|
|
59
|
+
taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to delete"),
|
|
60
|
+
limitResponsePayload: z.boolean().optional().describe("Whether to limit response size"),
|
|
61
|
+
wasTaskMerged: z.boolean().optional().describe("Whether the task was merged before deletion"),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Response Type Schemas (for validation and documentation)
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
// Basic user profile schema
|
|
69
|
+
export const userProfileSchema = z.object({
|
|
70
|
+
_id: z.string(),
|
|
71
|
+
email: z.string().email(),
|
|
72
|
+
firstName: z.string(),
|
|
73
|
+
lastName: z.string(),
|
|
74
|
+
timezone: z.string(),
|
|
75
|
+
avatarUrl: z.string().url().optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Group schema
|
|
79
|
+
export const groupSchema = z.object({
|
|
80
|
+
groupId: z.string(),
|
|
81
|
+
name: z.string(),
|
|
82
|
+
role: z.string().optional(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// User schema with primary group
|
|
86
|
+
export const userSchema = z.object({
|
|
87
|
+
_id: z.string(),
|
|
88
|
+
email: z.string().email(),
|
|
89
|
+
profile: userProfileSchema,
|
|
90
|
+
primaryGroup: groupSchema.optional(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Task schema (simplified - based on common task properties)
|
|
94
|
+
export const taskSchema = z.object({
|
|
95
|
+
_id: z.string(),
|
|
96
|
+
title: z.string(),
|
|
97
|
+
description: z.string().optional(),
|
|
98
|
+
status: z.string(),
|
|
99
|
+
createdAt: z.string(),
|
|
100
|
+
updatedAt: z.string(),
|
|
101
|
+
scheduledDate: z.string().optional(),
|
|
102
|
+
completedAt: z.string().optional(),
|
|
103
|
+
streamId: z.string().optional(),
|
|
104
|
+
userId: z.string(),
|
|
105
|
+
groupId: z.string(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Stream schema
|
|
109
|
+
export const streamSchema = z.object({
|
|
110
|
+
_id: z.string(),
|
|
111
|
+
name: z.string(),
|
|
112
|
+
color: z.string().optional(),
|
|
113
|
+
groupId: z.string(),
|
|
114
|
+
isActive: z.boolean(),
|
|
115
|
+
createdAt: z.string(),
|
|
116
|
+
updatedAt: z.string(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* API Response Schemas
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
// User response
|
|
124
|
+
export const userResponseSchema = z.object({
|
|
125
|
+
user: userSchema,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Tasks response
|
|
129
|
+
export const tasksResponseSchema = z.object({
|
|
130
|
+
tasks: z.array(taskSchema),
|
|
131
|
+
count: z.number(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Streams response
|
|
135
|
+
export const streamsResponseSchema = z.object({
|
|
136
|
+
streams: z.array(streamSchema),
|
|
137
|
+
count: z.number(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Error Response Schema
|
|
142
|
+
*/
|
|
143
|
+
export const errorResponseSchema = z.object({
|
|
144
|
+
error: z.string(),
|
|
145
|
+
message: z.string(),
|
|
146
|
+
code: z.string().optional(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Type Exports (for use in tools)
|
|
151
|
+
*/
|
|
152
|
+
export type CompletionFilter = z.infer<typeof completionFilterSchema>;
|
|
153
|
+
|
|
154
|
+
export type GetTasksByDayInput = z.infer<typeof getTasksByDaySchema>;
|
|
155
|
+
export type GetTasksBacklogInput = z.infer<typeof getTasksBacklogSchema>;
|
|
156
|
+
export type GetUserInput = z.infer<typeof getUserSchema>;
|
|
157
|
+
export type GetStreamsInput = z.infer<typeof getStreamsSchema>;
|
|
158
|
+
|
|
159
|
+
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
|
|
160
|
+
export type UpdateTaskCompleteInput = z.infer<typeof updateTaskCompleteSchema>;
|
|
161
|
+
export type DeleteTaskInput = z.infer<typeof deleteTaskSchema>;
|
|
162
|
+
|
|
163
|
+
export type User = z.infer<typeof userSchema>;
|
|
164
|
+
export type Task = z.infer<typeof taskSchema>;
|
|
165
|
+
export type Stream = z.infer<typeof streamSchema>;
|
|
166
|
+
export type UserResponse = z.infer<typeof userResponseSchema>;
|
|
167
|
+
export type TasksResponse = z.infer<typeof tasksResponseSchema>;
|
|
168
|
+
export type StreamsResponse = z.infer<typeof streamsResponseSchema>;
|
|
169
|
+
export type ErrorResponse = z.infer<typeof errorResponseSchema>;
|