mcp-sunsama 0.14.0 → 0.14.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,13 @@
1
1
  # mcp-sunsama
2
2
 
3
+ ## 0.14.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: resolve stdio authentication race condition
8
+
9
+ Fixes critical race condition in stdio authentication that caused "Global Sunsama client not initialized" errors and -32800 request cancelled errors in Raycast and other MCP clients. Implements lazy authentication with promise caching to prevent concurrent auth attempts and adds graceful startup error handling.
10
+
3
11
  ## 0.14.0
4
12
 
5
13
  ### Minor Changes
@@ -3,11 +3,11 @@ import { SunsamaClient } from "sunsama-api";
3
3
  * Initialize stdio authentication using environment variables
4
4
  * @throws {Error} If credentials are missing or authentication fails
5
5
  */
6
- export declare function initializeStdioAuth(): Promise<void>;
6
+ export declare function initializeStdioAuth(): Promise<SunsamaClient>;
7
7
  /**
8
8
  * Get the global Sunsama client instance for stdio transport
9
- * @returns {SunsamaClient} The authenticated global client
10
- * @throws {Error} If global client is not initialized
9
+ * @returns {Promise<SunsamaClient>} The authenticated global client
10
+ * @throws {Error} If credentials are missing or authentication fails
11
11
  */
12
- export declare function getGlobalSunsamaClient(): SunsamaClient;
12
+ export declare function getGlobalSunsamaClient(): Promise<SunsamaClient>;
13
13
  //# sourceMappingURL=stdio.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/auth/stdio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAO5C;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CASzD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,aAAa,CAKtD"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/auth/stdio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAO5C;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,aAAa,CAAC,CAWlE;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,aAAa,CAAC,CAMrE"}
@@ -1,8 +1,8 @@
1
1
  import { SunsamaClient } from "sunsama-api";
2
2
  /**
3
- * Global Sunsama client instance for stdio transport
3
+ * Cached authentication promise to prevent concurrent auth attempts
4
4
  */
5
- let globalSunsamaClient = null;
5
+ let authenticationPromise = null;
6
6
  /**
7
7
  * Initialize stdio authentication using environment variables
8
8
  * @throws {Error} If credentials are missing or authentication fails
@@ -11,17 +11,18 @@ export async function initializeStdioAuth() {
11
11
  if (!process.env.SUNSAMA_EMAIL || !process.env.SUNSAMA_PASSWORD) {
12
12
  throw new Error("Sunsama credentials not configured. Please set SUNSAMA_EMAIL and SUNSAMA_PASSWORD environment variables.");
13
13
  }
14
- globalSunsamaClient = new SunsamaClient();
15
- await globalSunsamaClient.login(process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
14
+ const sunsamaClient = new SunsamaClient();
15
+ await sunsamaClient.login(process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
16
+ return sunsamaClient;
16
17
  }
17
18
  /**
18
19
  * Get the global Sunsama client instance for stdio transport
19
- * @returns {SunsamaClient} The authenticated global client
20
- * @throws {Error} If global client is not initialized
20
+ * @returns {Promise<SunsamaClient>} The authenticated global client
21
+ * @throws {Error} If credentials are missing or authentication fails
21
22
  */
22
- export function getGlobalSunsamaClient() {
23
- if (!globalSunsamaClient) {
24
- throw new Error("Global Sunsama client not initialized.");
23
+ export async function getGlobalSunsamaClient() {
24
+ if (!authenticationPromise) {
25
+ authenticationPromise = initializeStdioAuth();
25
26
  }
26
- return globalSunsamaClient;
27
+ return authenticationPromise;
27
28
  }
package/dist/main.js CHANGED
@@ -7,13 +7,20 @@ import { allTools } from "./tools/index.js";
7
7
  import { apiDocumentationResource } from "./resources/index.js";
8
8
  // Get transport configuration with validation
9
9
  const transportConfig = getTransportConfig();
10
- // For stdio transport, authenticate at startup with environment variables
10
+ // For stdio transport, attempt authentication at startup with environment variables
11
11
  if (transportConfig.transportType === "stdio") {
12
- await initializeStdioAuth();
12
+ try {
13
+ await initializeStdioAuth();
14
+ console.log("Sunsama authentication successful");
15
+ }
16
+ catch (error) {
17
+ console.error("Failed to initialize Sunsama authentication:", error instanceof Error ? error.message : 'Unknown error');
18
+ console.error("Server will start but tools will retry authentication on first use");
19
+ }
13
20
  }
14
21
  const server = new FastMCP({
15
22
  name: "Sunsama API Server",
16
- version: "0.14.0",
23
+ version: "0.14.1",
17
24
  instructions: `
18
25
  This MCP server provides access to the Sunsama API for task and project management.
19
26
 
@@ -21,10 +21,6 @@ export declare function createToolWrapper<T>(config: {
21
21
  parameters: any;
22
22
  execute: (args: T, context: ToolContext) => Promise<any>;
23
23
  };
24
- /**
25
- * Gets the Sunsama client for the current session
26
- */
27
- export declare function getClient(session: SessionData | undefined | null): import("sunsama-api").SunsamaClient;
28
24
  /**
29
25
  * Formats data as JSON response
30
26
  */
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/tools/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;CACJ;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,GAAG,CAAC;IAChB,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAC1D;;;;oBAKyB,CAAC,WAAW,WAAW;EAchD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,uCAEhE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,GAAG,YAAY,CAS1D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,YAAY,CAS3D;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,GAAG,EAAE,EACX,UAAU,EAAE;IACV,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GACA,YAAY,CAYd"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/tools/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;CACJ;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,GAAG,CAAC;IAChB,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAC1D;;;;oBAKyB,CAAC,WAAW,WAAW;EAchD;AAGD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,GAAG,YAAY,CAS1D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,YAAY,CAS3D;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,GAAG,EAAE,EACX,UAAU,EAAE;IACV,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GACA,YAAY,CAYd"}
@@ -1,4 +1,3 @@
1
- import { getSunsamaClient } from "../utils/client-resolver.js";
2
1
  import { toTsv } from "../utils/to-tsv.js";
3
2
  /**
4
3
  * Creates a standardized tool execution wrapper with error handling and logging
@@ -24,12 +23,6 @@ export function createToolWrapper(config) {
24
23
  }
25
24
  };
26
25
  }
27
- /**
28
- * Gets the Sunsama client for the current session
29
- */
30
- export function getClient(session) {
31
- return getSunsamaClient(session);
32
- }
33
26
  /**
34
27
  * Formats data as JSON response
35
28
  */
@@ -1 +1 @@
1
- {"version":3,"file":"stream-tools.d.ts","sourceRoot":"","sources":["../../src/tools/stream-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEhG,eAAO,MAAM,cAAc;;;;;CAczB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;GAAmB,CAAC"}
1
+ {"version":3,"file":"stream-tools.d.ts","sourceRoot":"","sources":["../../src/tools/stream-tools.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAErF,eAAO,MAAM,cAAc;;;;;CAczB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;GAAmB,CAAC"}
@@ -1,12 +1,13 @@
1
1
  import { getStreamsSchema } from "../schemas.js";
2
- import { createToolWrapper, getClient, formatTsvResponse } from "./shared.js";
2
+ import { getSunsamaClient } from "../utils/client-resolver.js";
3
+ import { createToolWrapper, formatTsvResponse } from "./shared.js";
3
4
  export const getStreamsTool = createToolWrapper({
4
5
  name: "get-streams",
5
6
  description: "Get streams for the user's group (streams are called 'channels' in the Sunsama UI)",
6
7
  parameters: getStreamsSchema,
7
8
  execute: async (_args, context) => {
8
9
  context.log.info("Getting streams for user's group");
9
- const sunsamaClient = getClient(context.session);
10
+ const sunsamaClient = await getSunsamaClient(context.session);
10
11
  const streams = await sunsamaClient.getStreamsByGroupId();
11
12
  context.log.info("Successfully retrieved streams", { count: streams.length });
12
13
  return formatTsvResponse(streams);
@@ -1 +1 @@
1
- {"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAiCA,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAGrB,eAAO,MAAM,mBAAmB;;;;;CAe9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;CAkC5B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;CAsC/B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;CAqB1B,CAAC;AAGH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;CA4CzB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;CA2BzB,CAAC;AAGH,eAAO,MAAM,sBAAsB;;;;;;;;;CA4BjC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;CAkCnC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;CAgChC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;CA4BpC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAsC9B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;CA6BhC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;CAmC7B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;CAiC/B,CAAC;AAGH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAoBrB,CAAC"}
1
+ {"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAkCA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAGrB,eAAO,MAAM,mBAAmB;;;;;CAe9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;CAkC5B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;CAsC/B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;CAqB1B,CAAC;AAGH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;CA4CzB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;CA2BzB,CAAC;AAGH,eAAO,MAAM,sBAAsB;;;;;;;;;CA4BjC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;CAkCnC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;CAgChC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;CA4BpC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAsC9B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;CA6BhC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;CAmC7B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;CAiC/B,CAAC;AAGH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAoBrB,CAAC"}
@@ -1,7 +1,8 @@
1
1
  import { createTaskSchema, deleteTaskSchema, getArchivedTasksSchema, getTaskByIdSchema, getTasksBacklogSchema, getTasksByDaySchema, updateTaskBacklogSchema, updateTaskCompleteSchema, updateTaskDueDateSchema, updateTaskNotesSchema, updateTaskPlannedTimeSchema, updateTaskSnoozeDateSchema, updateTaskStreamSchema, updateTaskTextSchema } from "../schemas.js";
2
2
  import { filterTasksByCompletion } from "../utils/task-filters.js";
3
3
  import { trimTasksForResponse } from "../utils/task-trimmer.js";
4
- import { createToolWrapper, getClient, formatJsonResponse, formatTsvResponse, formatPaginatedTsvResponse } from "./shared.js";
4
+ import { getSunsamaClient } from "../utils/client-resolver.js";
5
+ import { createToolWrapper, formatJsonResponse, formatTsvResponse, formatPaginatedTsvResponse } from "./shared.js";
5
6
  // Task Query Tools
6
7
  export const getTasksBacklogTool = createToolWrapper({
7
8
  name: "get-tasks-backlog",
@@ -9,7 +10,7 @@ export const getTasksBacklogTool = createToolWrapper({
9
10
  parameters: getTasksBacklogSchema,
10
11
  execute: async (_args, context) => {
11
12
  context.log.info("Getting backlog tasks");
12
- const sunsamaClient = getClient(context.session);
13
+ const sunsamaClient = await getSunsamaClient(context.session);
13
14
  const tasks = await sunsamaClient.getTasksBacklog();
14
15
  const trimmedTasks = trimTasksForResponse(tasks);
15
16
  context.log.info("Successfully retrieved backlog tasks", { count: tasks.length });
@@ -26,7 +27,7 @@ export const getTasksByDayTool = createToolWrapper({
26
27
  timezone,
27
28
  completionFilter
28
29
  });
29
- const sunsamaClient = getClient(context.session);
30
+ const sunsamaClient = await getSunsamaClient(context.session);
30
31
  // If no timezone provided, get the user's default timezone
31
32
  let resolvedTimezone = timezone;
32
33
  if (!resolvedTimezone) {
@@ -58,7 +59,7 @@ export const getArchivedTasksTool = createToolWrapper({
58
59
  requestedLimit,
59
60
  fetchLimit
60
61
  });
61
- const sunsamaClient = getClient(context.session);
62
+ const sunsamaClient = await getSunsamaClient(context.session);
62
63
  const allTasks = await sunsamaClient.getArchivedTasks(offset, fetchLimit);
63
64
  const hasMore = allTasks.length > requestedLimit;
64
65
  const tasks = hasMore ? allTasks.slice(0, requestedLimit) : allTasks;
@@ -85,7 +86,7 @@ export const getTaskByIdTool = createToolWrapper({
85
86
  parameters: getTaskByIdSchema,
86
87
  execute: async ({ taskId }, context) => {
87
88
  context.log.info("Getting task by ID", { taskId });
88
- const sunsamaClient = getClient(context.session);
89
+ const sunsamaClient = await getSunsamaClient(context.session);
89
90
  const task = await sunsamaClient.getTaskById(taskId);
90
91
  if (task) {
91
92
  context.log.info("Successfully retrieved task by ID", {
@@ -116,7 +117,7 @@ export const createTaskTool = createToolWrapper({
116
117
  isPrivate: isPrivate,
117
118
  customTaskId: !!taskId
118
119
  });
119
- const sunsamaClient = getClient(context.session);
120
+ const sunsamaClient = await getSunsamaClient(context.session);
120
121
  const options = {};
121
122
  if (notes)
122
123
  options.notes = notes;
@@ -157,7 +158,7 @@ export const deleteTaskTool = createToolWrapper({
157
158
  limitResponsePayload,
158
159
  wasTaskMerged
159
160
  });
160
- const sunsamaClient = getClient(context.session);
161
+ const sunsamaClient = await getSunsamaClient(context.session);
161
162
  const result = await sunsamaClient.deleteTask(taskId, limitResponsePayload, wasTaskMerged);
162
163
  context.log.info("Successfully deleted task", {
163
164
  taskId,
@@ -182,7 +183,7 @@ export const updateTaskCompleteTool = createToolWrapper({
182
183
  hasCustomCompleteOn: !!completeOn,
183
184
  limitResponsePayload
184
185
  });
185
- const sunsamaClient = getClient(context.session);
186
+ const sunsamaClient = await getSunsamaClient(context.session);
186
187
  const result = await sunsamaClient.updateTaskComplete(taskId, completeOn, limitResponsePayload);
187
188
  context.log.info("Successfully marked task as complete", {
188
189
  taskId,
@@ -208,7 +209,7 @@ export const updateTaskSnoozeDateTool = createToolWrapper({
208
209
  timezone,
209
210
  limitResponsePayload
210
211
  });
211
- const sunsamaClient = getClient(context.session);
212
+ const sunsamaClient = await getSunsamaClient(context.session);
212
213
  const options = {};
213
214
  if (timezone)
214
215
  options.timezone = timezone;
@@ -238,7 +239,7 @@ export const updateTaskBacklogTool = createToolWrapper({
238
239
  timezone,
239
240
  limitResponsePayload
240
241
  });
241
- const sunsamaClient = getClient(context.session);
242
+ const sunsamaClient = await getSunsamaClient(context.session);
242
243
  const options = {};
243
244
  if (timezone)
244
245
  options.timezone = timezone;
@@ -267,7 +268,7 @@ export const updateTaskPlannedTimeTool = createToolWrapper({
267
268
  timeEstimateMinutes,
268
269
  limitResponsePayload
269
270
  });
270
- const sunsamaClient = getClient(context.session);
271
+ const sunsamaClient = await getSunsamaClient(context.session);
271
272
  const result = await sunsamaClient.updateTaskPlannedTime(taskId, timeEstimateMinutes, limitResponsePayload);
272
273
  context.log.info("Successfully updated task planned time", {
273
274
  taskId,
@@ -296,7 +297,7 @@ export const updateTaskNotesTool = createToolWrapper({
296
297
  contentLength: content.value.length,
297
298
  limitResponsePayload
298
299
  });
299
- const sunsamaClient = getClient(context.session);
300
+ const sunsamaClient = await getSunsamaClient(context.session);
300
301
  const options = {};
301
302
  if (limitResponsePayload !== undefined)
302
303
  options.limitResponsePayload = limitResponsePayload;
@@ -325,7 +326,7 @@ export const updateTaskDueDateTool = createToolWrapper({
325
326
  dueDate,
326
327
  limitResponsePayload
327
328
  });
328
- const sunsamaClient = getClient(context.session);
329
+ const sunsamaClient = await getSunsamaClient(context.session);
329
330
  const result = await sunsamaClient.updateTaskDueDate(taskId, dueDate, limitResponsePayload);
330
331
  context.log.info("Successfully updated task due date", {
331
332
  taskId,
@@ -352,7 +353,7 @@ export const updateTaskTextTool = createToolWrapper({
352
353
  recommendedStreamId,
353
354
  limitResponsePayload
354
355
  });
355
- const sunsamaClient = getClient(context.session);
356
+ const sunsamaClient = await getSunsamaClient(context.session);
356
357
  const options = {};
357
358
  if (recommendedStreamId !== undefined)
358
359
  options.recommendedStreamId = recommendedStreamId;
@@ -383,7 +384,7 @@ export const updateTaskStreamTool = createToolWrapper({
383
384
  streamId,
384
385
  limitResponsePayload
385
386
  });
386
- const sunsamaClient = getClient(context.session);
387
+ const sunsamaClient = await getSunsamaClient(context.session);
387
388
  const result = await sunsamaClient.updateTaskStream(taskId, streamId, limitResponsePayload !== undefined ? limitResponsePayload : true);
388
389
  context.log.info("Successfully updated task stream assignment", {
389
390
  taskId,
@@ -1 +1 @@
1
- {"version":3,"file":"user-tools.d.ts","sourceRoot":"","sources":["../../src/tools/user-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjG,eAAO,MAAM,WAAW;;;;;CActB,CAAC;AAEH,eAAO,MAAM,SAAS;;;;;GAAgB,CAAC"}
1
+ {"version":3,"file":"user-tools.d.ts","sourceRoot":"","sources":["../../src/tools/user-tools.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtF,eAAO,MAAM,WAAW;;;;;CActB,CAAC;AAEH,eAAO,MAAM,SAAS;;;;;GAAgB,CAAC"}
@@ -1,12 +1,13 @@
1
1
  import { getUserSchema } from "../schemas.js";
2
- import { createToolWrapper, getClient, formatJsonResponse } from "./shared.js";
2
+ import { getSunsamaClient } from "../utils/client-resolver.js";
3
+ import { createToolWrapper, formatJsonResponse } from "./shared.js";
3
4
  export const getUserTool = createToolWrapper({
4
5
  name: "get-user",
5
6
  description: "Get current user information including profile, timezone, and group details",
6
7
  parameters: getUserSchema,
7
8
  execute: async (_args, context) => {
8
9
  context.log.info("Getting user information");
9
- const sunsamaClient = getClient(context.session);
10
+ const sunsamaClient = await getSunsamaClient(context.session);
10
11
  const user = await sunsamaClient.getUser();
11
12
  context.log.info("Successfully retrieved user information", { userId: user._id });
12
13
  return formatJsonResponse(user);
@@ -2,9 +2,9 @@ import { SunsamaClient } from "sunsama-api";
2
2
  import type { SessionData } from "../auth/types.js";
3
3
  /**
4
4
  * Gets the appropriate SunsamaClient instance based on transport type
5
- * @param session - Session data for HTTP transport (null for stdio)
6
- * @returns Authenticated SunsamaClient instance
5
+ * @param session - Session data for HTTP transport (undefined/null for stdio)
6
+ * @returns Promise<SunsamaClient> Authenticated SunsamaClient instance
7
7
  * @throws {Error} If session is not available for HTTP transport or global client not initialized for stdio
8
8
  */
9
- export declare function getSunsamaClient(session: SessionData | null): SunsamaClient;
9
+ export declare function getSunsamaClient(session?: SessionData | null): Promise<SunsamaClient>;
10
10
  //# sourceMappingURL=client-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-resolver.d.ts","sourceRoot":"","sources":["../../src/utils/client-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,aAAa,CAW3E"}
1
+ {"version":3,"file":"client-resolver.d.ts","sourceRoot":"","sources":["../../src/utils/client-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,CAW3F"}
@@ -3,11 +3,11 @@ import { getGlobalSunsamaClient } from "../auth/stdio.js";
3
3
  import { getTransportConfig } from "../config/transport.js";
4
4
  /**
5
5
  * Gets the appropriate SunsamaClient instance based on transport type
6
- * @param session - Session data for HTTP transport (null for stdio)
7
- * @returns Authenticated SunsamaClient instance
6
+ * @param session - Session data for HTTP transport (undefined/null for stdio)
7
+ * @returns Promise<SunsamaClient> Authenticated SunsamaClient instance
8
8
  * @throws {Error} If session is not available for HTTP transport or global client not initialized for stdio
9
9
  */
10
- export function getSunsamaClient(session) {
10
+ export async function getSunsamaClient(session) {
11
11
  const transportConfig = getTransportConfig();
12
12
  if (transportConfig.transportType === "httpStream") {
13
13
  if (!session?.sunsamaClient) {
@@ -15,5 +15,5 @@ export function getSunsamaClient(session) {
15
15
  }
16
16
  return session.sunsamaClient;
17
17
  }
18
- return getGlobalSunsamaClient();
18
+ return await getGlobalSunsamaClient();
19
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-sunsama",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "MCP server for Sunsama API integration",
5
5
  "type": "module",
6
6
  "private": false,
package/src/auth/stdio.ts CHANGED
@@ -1,33 +1,36 @@
1
1
  import { SunsamaClient } from "sunsama-api";
2
2
 
3
3
  /**
4
- * Global Sunsama client instance for stdio transport
4
+ * Cached authentication promise to prevent concurrent auth attempts
5
5
  */
6
- let globalSunsamaClient: SunsamaClient | null = null;
6
+ let authenticationPromise: Promise<SunsamaClient> | null = null;
7
7
 
8
8
  /**
9
9
  * Initialize stdio authentication using environment variables
10
10
  * @throws {Error} If credentials are missing or authentication fails
11
11
  */
12
- export async function initializeStdioAuth(): Promise<void> {
12
+ export async function initializeStdioAuth(): Promise<SunsamaClient> {
13
13
  if (!process.env.SUNSAMA_EMAIL || !process.env.SUNSAMA_PASSWORD) {
14
14
  throw new Error(
15
15
  "Sunsama credentials not configured. Please set SUNSAMA_EMAIL and SUNSAMA_PASSWORD environment variables."
16
16
  );
17
17
  }
18
18
 
19
- globalSunsamaClient = new SunsamaClient();
20
- await globalSunsamaClient.login(process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
19
+ const sunsamaClient = new SunsamaClient();
20
+ await sunsamaClient.login(process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
21
+
22
+ return sunsamaClient;
21
23
  }
22
24
 
23
25
  /**
24
26
  * Get the global Sunsama client instance for stdio transport
25
- * @returns {SunsamaClient} The authenticated global client
26
- * @throws {Error} If global client is not initialized
27
+ * @returns {Promise<SunsamaClient>} The authenticated global client
28
+ * @throws {Error} If credentials are missing or authentication fails
27
29
  */
28
- export function getGlobalSunsamaClient(): SunsamaClient {
29
- if (!globalSunsamaClient) {
30
- throw new Error("Global Sunsama client not initialized.");
30
+ export async function getGlobalSunsamaClient(): Promise<SunsamaClient> {
31
+ if (!authenticationPromise) {
32
+ authenticationPromise = initializeStdioAuth();
31
33
  }
32
- return globalSunsamaClient;
34
+
35
+ return authenticationPromise;
33
36
  }
package/src/main.ts CHANGED
@@ -9,14 +9,20 @@ import { apiDocumentationResource } from "./resources/index.js";
9
9
  // Get transport configuration with validation
10
10
  const transportConfig = getTransportConfig();
11
11
 
12
- // For stdio transport, authenticate at startup with environment variables
12
+ // For stdio transport, attempt authentication at startup with environment variables
13
13
  if (transportConfig.transportType === "stdio") {
14
- await initializeStdioAuth();
14
+ try {
15
+ await initializeStdioAuth();
16
+ console.log("Sunsama authentication successful");
17
+ } catch (error) {
18
+ console.error("Failed to initialize Sunsama authentication:", error instanceof Error ? error.message : 'Unknown error');
19
+ console.error("Server will start but tools will retry authentication on first use");
20
+ }
15
21
  }
16
22
 
17
23
  const server = new FastMCP({
18
24
  name: "Sunsama API Server",
19
- version: "0.14.0",
25
+ version: "0.14.1",
20
26
  instructions: `
21
27
  This MCP server provides access to the Sunsama API for task and project management.
22
28
 
@@ -1,6 +1,5 @@
1
1
  import type { Context } from "fastmcp";
2
2
  import type { SessionData } from "../auth/types.js";
3
- import { getSunsamaClient } from "../utils/client-resolver.js";
4
3
  import { toTsv } from "../utils/to-tsv.js";
5
4
 
6
5
  export type ToolContext = Context<SessionData>;
@@ -41,12 +40,6 @@ export function createToolWrapper<T>(config: {
41
40
  };
42
41
  }
43
42
 
44
- /**
45
- * Gets the Sunsama client for the current session
46
- */
47
- export function getClient(session: SessionData | undefined | null) {
48
- return getSunsamaClient(session as SessionData | null);
49
- }
50
43
 
51
44
  /**
52
45
  * Formats data as JSON response
@@ -1,5 +1,6 @@
1
1
  import { getStreamsSchema, type GetStreamsInput } from "../schemas.js";
2
- import { createToolWrapper, getClient, formatTsvResponse, type ToolContext } from "./shared.js";
2
+ import { getSunsamaClient } from "../utils/client-resolver.js";
3
+ import { createToolWrapper, formatTsvResponse, type ToolContext } from "./shared.js";
3
4
 
4
5
  export const getStreamsTool = createToolWrapper({
5
6
  name: "get-streams",
@@ -8,7 +9,7 @@ export const getStreamsTool = createToolWrapper({
8
9
  execute: async (_args: GetStreamsInput, context: ToolContext) => {
9
10
  context.log.info("Getting streams for user's group");
10
11
 
11
- const sunsamaClient = getClient(context.session);
12
+ const sunsamaClient = await getSunsamaClient(context.session);
12
13
  const streams = await sunsamaClient.getStreamsByGroupId();
13
14
 
14
15
  context.log.info("Successfully retrieved streams", { count: streams.length });
@@ -31,9 +31,9 @@ import {
31
31
  } from "../schemas.js";
32
32
  import { filterTasksByCompletion } from "../utils/task-filters.js";
33
33
  import { trimTasksForResponse } from "../utils/task-trimmer.js";
34
+ import { getSunsamaClient } from "../utils/client-resolver.js";
34
35
  import {
35
36
  createToolWrapper,
36
- getClient,
37
37
  formatJsonResponse,
38
38
  formatTsvResponse,
39
39
  formatPaginatedTsvResponse,
@@ -48,7 +48,7 @@ export const getTasksBacklogTool = createToolWrapper({
48
48
  execute: async (_args: GetTasksBacklogInput, context: ToolContext) => {
49
49
  context.log.info("Getting backlog tasks");
50
50
 
51
- const sunsamaClient = getClient(context.session);
51
+ const sunsamaClient = await getSunsamaClient(context.session);
52
52
  const tasks = await sunsamaClient.getTasksBacklog();
53
53
  const trimmedTasks = trimTasksForResponse(tasks);
54
54
 
@@ -69,7 +69,7 @@ export const getTasksByDayTool = createToolWrapper({
69
69
  completionFilter
70
70
  });
71
71
 
72
- const sunsamaClient = getClient(context.session);
72
+ const sunsamaClient = await getSunsamaClient(context.session);
73
73
 
74
74
  // If no timezone provided, get the user's default timezone
75
75
  let resolvedTimezone = timezone;
@@ -108,7 +108,7 @@ export const getArchivedTasksTool = createToolWrapper({
108
108
  fetchLimit
109
109
  });
110
110
 
111
- const sunsamaClient = getClient(context.session);
111
+ const sunsamaClient = await getSunsamaClient(context.session);
112
112
  const allTasks = await sunsamaClient.getArchivedTasks(offset, fetchLimit);
113
113
 
114
114
  const hasMore = allTasks.length > requestedLimit;
@@ -141,7 +141,7 @@ export const getTaskByIdTool = createToolWrapper({
141
141
  execute: async ({ taskId }: GetTaskByIdInput, context: ToolContext) => {
142
142
  context.log.info("Getting task by ID", { taskId });
143
143
 
144
- const sunsamaClient = getClient(context.session);
144
+ const sunsamaClient = await getSunsamaClient(context.session);
145
145
  const task = await sunsamaClient.getTaskById(taskId);
146
146
 
147
147
  if (task) {
@@ -175,7 +175,7 @@ export const createTaskTool = createToolWrapper({
175
175
  customTaskId: !!taskId
176
176
  });
177
177
 
178
- const sunsamaClient = getClient(context.session);
178
+ const sunsamaClient = await getSunsamaClient(context.session);
179
179
 
180
180
  const options: CreateTaskOptions = {};
181
181
  if (notes) options.notes = notes;
@@ -216,7 +216,7 @@ export const deleteTaskTool = createToolWrapper({
216
216
  wasTaskMerged
217
217
  });
218
218
 
219
- const sunsamaClient = getClient(context.session);
219
+ const sunsamaClient = await getSunsamaClient(context.session);
220
220
  const result = await sunsamaClient.deleteTask(taskId, limitResponsePayload, wasTaskMerged);
221
221
 
222
222
  context.log.info("Successfully deleted task", {
@@ -246,7 +246,7 @@ export const updateTaskCompleteTool = createToolWrapper({
246
246
  limitResponsePayload
247
247
  });
248
248
 
249
- const sunsamaClient = getClient(context.session);
249
+ const sunsamaClient = await getSunsamaClient(context.session);
250
250
  const result = await sunsamaClient.updateTaskComplete(taskId, completeOn, limitResponsePayload);
251
251
 
252
252
  context.log.info("Successfully marked task as complete", {
@@ -277,7 +277,7 @@ export const updateTaskSnoozeDateTool = createToolWrapper({
277
277
  limitResponsePayload
278
278
  });
279
279
 
280
- const sunsamaClient = getClient(context.session);
280
+ const sunsamaClient = await getSunsamaClient(context.session);
281
281
 
282
282
  const options: { timezone?: string; limitResponsePayload?: boolean } = {};
283
283
  if (timezone) options.timezone = timezone;
@@ -312,7 +312,7 @@ export const updateTaskBacklogTool = createToolWrapper({
312
312
  limitResponsePayload
313
313
  });
314
314
 
315
- const sunsamaClient = getClient(context.session);
315
+ const sunsamaClient = await getSunsamaClient(context.session);
316
316
 
317
317
  const options: { timezone?: string; limitResponsePayload?: boolean } = {};
318
318
  if (timezone) options.timezone = timezone;
@@ -346,7 +346,7 @@ export const updateTaskPlannedTimeTool = createToolWrapper({
346
346
  limitResponsePayload
347
347
  });
348
348
 
349
- const sunsamaClient = getClient(context.session);
349
+ const sunsamaClient = await getSunsamaClient(context.session);
350
350
  const result = await sunsamaClient.updateTaskPlannedTime(taskId, timeEstimateMinutes, limitResponsePayload);
351
351
 
352
352
  context.log.info("Successfully updated task planned time", {
@@ -381,7 +381,7 @@ export const updateTaskNotesTool = createToolWrapper({
381
381
  limitResponsePayload
382
382
  });
383
383
 
384
- const sunsamaClient = getClient(context.session);
384
+ const sunsamaClient = await getSunsamaClient(context.session);
385
385
 
386
386
  const options: { limitResponsePayload?: boolean } = {};
387
387
  if (limitResponsePayload !== undefined) options.limitResponsePayload = limitResponsePayload;
@@ -416,7 +416,7 @@ export const updateTaskDueDateTool = createToolWrapper({
416
416
  limitResponsePayload
417
417
  });
418
418
 
419
- const sunsamaClient = getClient(context.session);
419
+ const sunsamaClient = await getSunsamaClient(context.session);
420
420
  const result = await sunsamaClient.updateTaskDueDate(taskId, dueDate, limitResponsePayload);
421
421
 
422
422
  context.log.info("Successfully updated task due date", {
@@ -448,7 +448,7 @@ export const updateTaskTextTool = createToolWrapper({
448
448
  limitResponsePayload
449
449
  });
450
450
 
451
- const sunsamaClient = getClient(context.session);
451
+ const sunsamaClient = await getSunsamaClient(context.session);
452
452
 
453
453
  const options: { recommendedStreamId?: string | null; limitResponsePayload?: boolean } = {};
454
454
  if (recommendedStreamId !== undefined) options.recommendedStreamId = recommendedStreamId;
@@ -484,7 +484,7 @@ export const updateTaskStreamTool = createToolWrapper({
484
484
  limitResponsePayload
485
485
  });
486
486
 
487
- const sunsamaClient = getClient(context.session);
487
+ const sunsamaClient = await getSunsamaClient(context.session);
488
488
  const result = await sunsamaClient.updateTaskStream(
489
489
  taskId,
490
490
  streamId,
@@ -1,5 +1,6 @@
1
1
  import { getUserSchema, type GetUserInput } from "../schemas.js";
2
- import { createToolWrapper, getClient, formatJsonResponse, type ToolContext } from "./shared.js";
2
+ import { getSunsamaClient } from "../utils/client-resolver.js";
3
+ import { createToolWrapper, formatJsonResponse, type ToolContext } from "./shared.js";
3
4
 
4
5
  export const getUserTool = createToolWrapper({
5
6
  name: "get-user",
@@ -8,7 +9,7 @@ export const getUserTool = createToolWrapper({
8
9
  execute: async (_args: GetUserInput, context: ToolContext) => {
9
10
  context.log.info("Getting user information");
10
11
 
11
- const sunsamaClient = getClient(context.session);
12
+ const sunsamaClient = await getSunsamaClient(context.session);
12
13
  const user = await sunsamaClient.getUser();
13
14
 
14
15
  context.log.info("Successfully retrieved user information", { userId: user._id });
@@ -5,19 +5,19 @@ import { getTransportConfig } from "../config/transport.js";
5
5
 
6
6
  /**
7
7
  * Gets the appropriate SunsamaClient instance based on transport type
8
- * @param session - Session data for HTTP transport (null for stdio)
9
- * @returns Authenticated SunsamaClient instance
8
+ * @param session - Session data for HTTP transport (undefined/null for stdio)
9
+ * @returns Promise<SunsamaClient> Authenticated SunsamaClient instance
10
10
  * @throws {Error} If session is not available for HTTP transport or global client not initialized for stdio
11
11
  */
12
- export function getSunsamaClient(session: SessionData | null): SunsamaClient {
12
+ export async function getSunsamaClient(session?: SessionData | null): Promise<SunsamaClient> {
13
13
  const transportConfig = getTransportConfig();
14
-
14
+
15
15
  if (transportConfig.transportType === "httpStream") {
16
16
  if (!session?.sunsamaClient) {
17
17
  throw new Error("Session not available. Authentication may have failed.");
18
18
  }
19
19
  return session.sunsamaClient;
20
20
  }
21
-
22
- return getGlobalSunsamaClient();
21
+
22
+ return await getGlobalSunsamaClient();
23
23
  }