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