mcp-sunsama 0.16.1 → 0.18.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 (63) hide show
  1. package/.claude/commands/upgrade-sunsama-api.md +70 -0
  2. package/CHANGELOG.md +37 -0
  3. package/CLAUDE.md +4 -2
  4. package/README.md +25 -0
  5. package/__tests__/unit/tools/api-coverage.test.ts +128 -0
  6. package/bun.lock +2 -2
  7. package/dist/auth/http.d.ts +1 -0
  8. package/dist/auth/http.d.ts.map +1 -1
  9. package/dist/auth/http.js +89 -12
  10. package/dist/auth/retry.d.ts +8 -0
  11. package/dist/auth/retry.d.ts.map +1 -0
  12. package/dist/auth/retry.js +25 -0
  13. package/dist/auth/stdio.d.ts +1 -0
  14. package/dist/auth/stdio.d.ts.map +1 -1
  15. package/dist/auth/stdio.js +12 -2
  16. package/dist/auth/types.d.ts +8 -1
  17. package/dist/auth/types.d.ts.map +1 -1
  18. package/dist/auth/types.js +13 -0
  19. package/dist/constants.d.ts +1 -1
  20. package/dist/constants.js +1 -1
  21. package/dist/main.js +12 -18
  22. package/dist/schemas.d.ts +165 -6
  23. package/dist/schemas.d.ts.map +1 -1
  24. package/dist/schemas.js +69 -0
  25. package/dist/schemas.test.js +8 -6
  26. package/dist/tools/calendar-tools.d.ts +4 -0
  27. package/dist/tools/calendar-tools.d.ts.map +1 -0
  28. package/dist/tools/calendar-tools.js +55 -0
  29. package/dist/tools/index.d.ts +2 -6
  30. package/dist/tools/index.d.ts.map +1 -1
  31. package/dist/tools/index.js +3 -0
  32. package/dist/tools/shared.d.ts +46 -36
  33. package/dist/tools/shared.d.ts.map +1 -1
  34. package/dist/tools/shared.js +16 -22
  35. package/dist/tools/stream-tools.d.ts +2 -12
  36. package/dist/tools/stream-tools.d.ts.map +1 -1
  37. package/dist/tools/task-tools.d.ts +22 -90
  38. package/dist/tools/task-tools.d.ts.map +1 -1
  39. package/dist/tools/task-tools.js +125 -1
  40. package/dist/tools/user-tools.d.ts +2 -12
  41. package/dist/tools/user-tools.d.ts.map +1 -1
  42. package/dist/transports/http.d.ts.map +1 -1
  43. package/dist/transports/http.js +21 -8
  44. package/dist/utils/client-resolver.d.ts +1 -1
  45. package/dist/utils/client-resolver.d.ts.map +1 -1
  46. package/dist/utils/client-resolver.js +15 -4
  47. package/dist/utils/task-trimmer.js +1 -1
  48. package/package.json +3 -3
  49. package/src/auth/http.ts +97 -12
  50. package/src/auth/retry.ts +35 -0
  51. package/src/auth/stdio.ts +13 -2
  52. package/src/auth/types.ts +15 -1
  53. package/src/constants.ts +1 -1
  54. package/src/main.ts +12 -18
  55. package/src/schemas.test.ts +8 -6
  56. package/src/schemas.ts +160 -0
  57. package/src/tools/calendar-tools.ts +91 -0
  58. package/src/tools/index.ts +3 -0
  59. package/src/tools/shared.ts +63 -37
  60. package/src/tools/task-tools.ts +197 -0
  61. package/src/transports/http.ts +20 -8
  62. package/src/utils/client-resolver.ts +22 -6
  63. package/src/utils/task-trimmer.ts +1 -1
@@ -0,0 +1,70 @@
1
+ ---
2
+ description: Upgrade sunsama-api and add missing MCP tools
3
+ model: claude-opus-4-6
4
+ allowed-tools: Bash(bun:*), Bash(npm:*), Bash(cat:*), Bash(ls:*), Read, Write, Edit, Glob, Grep
5
+ ---
6
+
7
+ Upgrade the sunsama-api dependency to the latest version and ensure all new endpoints are wrapped as MCP tools.
8
+
9
+ ## Steps
10
+
11
+ 1. **Check current vs latest version**:
12
+ ```bash
13
+ cat package.json | grep sunsama-api
14
+ npm view sunsama-api version
15
+ ```
16
+ - If already on latest, inform user and stop
17
+
18
+ 2. **Upgrade the dependency**:
19
+ ```bash
20
+ bun add sunsama-api@latest
21
+ ```
22
+
23
+ 3. **Verify build passes**:
24
+ ```bash
25
+ bun run typecheck
26
+ bun test
27
+ ```
28
+ - If either fails, stop and report the issue
29
+
30
+ 4. **Analyze new API methods**:
31
+ - Read `node_modules/sunsama-api/dist/types/client/index.d.ts` to find the method class hierarchy
32
+ - Read each method class file to enumerate all public methods on `SunsamaClient`
33
+ - Compare against existing tools in `src/tools/` to find unwrapped methods
34
+ - Report: "Found X new methods not wrapped as MCP tools: [list]"
35
+
36
+ 5. **For each unwrapped method**:
37
+ - Read the method signature and JSDoc from the sunsama-api type definitions
38
+ - Read the corresponding types from `node_modules/sunsama-api/dist/types/types/api.d.ts`
39
+ - Add the appropriate Zod schema to `src/schemas.ts` following existing patterns
40
+ - Add the tool implementation to the appropriate file in `src/tools/`:
41
+ - User operations → `user-tools.ts`
42
+ - Task operations → `task-tools.ts`
43
+ - Stream operations → `stream-tools.ts`
44
+ - Calendar operations → `calendar-tools.ts`
45
+ - Update `src/tools/index.ts` if a new tools file was created
46
+
47
+ 6. **Verify new tools work**:
48
+ ```bash
49
+ bun run typecheck
50
+ bun test
51
+ ```
52
+
53
+ 7. **Create changeset**:
54
+ - If only dependency bump (no new tools): patch changeset
55
+ - If new tools added: minor changeset
56
+ - Write changeset to `.changeset/` with descriptive summary
57
+
58
+ 8. **Report summary**:
59
+ - Previous version → new version
60
+ - New methods found
61
+ - Tools added (if any)
62
+ - Changeset created
63
+
64
+ ## Notes
65
+
66
+ - Follow existing patterns in `src/tools/` for tool structure
67
+ - Use `withTransportClient` wrapper for all tools
68
+ - Prefer `formatJsonResponse` for single objects, `formatTsvResponse` for arrays
69
+ - Don't use `as` type assertions — if needed, request schema exports from sunsama-api
70
+ - Run typecheck after each file modification to catch errors early
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # mcp-sunsama
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f806778: Add three new tools for sunsama-api 0.14.0 endpoints:
8
+
9
+ - `reorder-task` — move a task to a specific 0-based position within a day
10
+ - `create-calendar-event` — create a new calendar event (with optional stream links, visibility, and task seed)
11
+ - `update-calendar-event` — update an existing calendar event (requires full CalendarEventUpdateData)
12
+
13
+ - f806778: Add `update-task-uncomplete` tool to mark a completed task as incomplete, wrapping the previously missing `SunsamaClient.updateTaskUncomplete()` method.
14
+
15
+ ### Patch Changes
16
+
17
+ - 846299b: Fix HTTP transport returning 401 for all errors instead of only authentication failures. Non-auth errors (transport failures, unexpected exceptions) now correctly return 500.
18
+ - c1badc0: Fix HTTP transport tools always falling through to stdio client. The MCP SDK provides `sessionId` as a string on the `extra` object, not a nested `session` object. `client-resolver` now checks `extra.sessionId` to look up the per-request authenticated client in `SessionManager`.
19
+ - d3f81d7: Add retry with exponential backoff for 429 rate limit errors during login. Both stdio startup auth and HTTP per-request auth now retry up to 3 times (delays: 1s, 5s, 15s) when `SunsamaAuthError` contains "429". Non-429 errors are rethrown immediately.
20
+ - 3f0070a: Eliminate any types in shared tool infrastructure. ToolConfig is now generic over the Zod schema so tool execute functions receive properly typed args. ToolContext.client is typed as SunsamaClient and session as unknown, removing the catch-all index signature.
21
+ - af74a9d: Update sunsama-api dependency to 0.14.0 (bug fix release — no new endpoints).
22
+
23
+ ## 0.17.0
24
+
25
+ ### Minor Changes
26
+
27
+ - 26000bd: Add subtask management support
28
+
29
+ - Update sunsama-api dependency to 0.13.1
30
+ - Add 5 new subtask management tools:
31
+ - `add-subtask` - Create a subtask with a title in one call (recommended)
32
+ - `create-subtasks` - Create multiple subtasks for a task (bulk operations)
33
+ - `update-subtask-title` - Update the title of a subtask
34
+ - `complete-subtask` - Mark a subtask as complete
35
+ - `uncomplete-subtask` - Mark a subtask as incomplete
36
+ - Update README and CLAUDE.md documentation
37
+
38
+ - e3336cf: Add session token authentication support for users with SSO/OAuth login. Users can now authenticate using `SUNSAMA_SESSION_TOKEN` environment variable (stdio) or Bearer token authentication (HTTP) as an alternative to email/password credentials.
39
+
3
40
  ## 0.16.1
4
41
 
5
42
  ### Patch Changes
package/CLAUDE.md CHANGED
@@ -182,7 +182,7 @@ src/
182
182
  ├── tools/
183
183
  │ ├── shared.ts # Common utilities and tool wrapper patterns
184
184
  │ ├── user-tools.ts # User operations (get-user)
185
- │ ├── task-tools.ts # Task operations (15 tools)
185
+ │ ├── task-tools.ts # Task operations (20 tools)
186
186
  │ ├── stream-tools.ts # Stream operations (get-streams)
187
187
  │ └── index.ts # Export all tools
188
188
  ├── resources/
@@ -226,13 +226,14 @@ __tests__/
226
226
 
227
227
  **Resource-Based Organization**:
228
228
  - **User Tools**: Single tool for user operations
229
- - **Task Tools**: 15 tools organized by function (query, lifecycle, update)
229
+ - **Task Tools**: 20 tools organized by function (query, lifecycle, update, subtasks)
230
230
  - **Stream Tools**: Single tool for stream operations
231
231
 
232
232
  **Type Safety Improvements**:
233
233
  - **Parameter Destructuring**: Function signatures directly destructure typed parameters
234
234
  - **Zod Schema Integration**: Full TypeScript inference from Zod schemas
235
235
  - **Eliminated `any` Types**: All parameters properly typed with generated types
236
+ - **Avoid Type Assertions**: Don't use `as` casts if possible. Never use `as unknown as X` — this bypasses all type checking. If you reach for these, stop and find a type-safe alternative (generics, type guards, schema validation, etc.)
236
237
 
237
238
  ## Testing Architecture
238
239
 
@@ -278,6 +279,7 @@ Optional:
278
279
  Full CRUD support:
279
280
  - **Read**: `get-tasks-by-day`, `get-tasks-backlog`, `get-archived-tasks`, `get-task-by-id`, `get-streams`
280
281
  - **Write**: `create-task` (with GitHub/Gmail integration support), `update-task-complete`, `update-task-planned-time`, `update-task-notes`, `update-task-snooze-date`, `update-task-backlog`, `update-task-stream`, `update-task-text`, `update-task-due-date`, `delete-task`
282
+ - **Subtasks**: `add-subtask` (recommended), `create-subtasks` (bulk), `update-subtask-title`, `complete-subtask`, `uncomplete-subtask`
281
283
 
282
284
  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.
283
285
 
package/README.md CHANGED
@@ -12,6 +12,7 @@ A Model Context Protocol (MCP) server that provides comprehensive task managemen
12
12
  - **Create Tasks** - Create new tasks with notes, time estimates, due dates, stream assignments, and GitHub/Gmail integrations
13
13
  - **Read Tasks** - Get tasks by day with completion filtering, access backlog tasks, retrieve archived task history
14
14
  - **Update Tasks** - Mark tasks as complete with custom timestamps, reschedule tasks or move to backlog
15
+ - **Subtasks** - Add, update, complete, and manage subtasks within tasks
15
16
  - **Delete Tasks** - Permanently remove tasks from your workspace
16
17
 
17
18
  ### User & Stream Operations
@@ -111,6 +112,23 @@ Add this configuration to your Claude Desktop MCP settings:
111
112
  }
112
113
  ```
113
114
 
115
+ ### Claude Code Configuration
116
+
117
+ Add the Sunsama MCP server using the Claude Code CLI:
118
+
119
+ ```bash
120
+ claude mcp add sunsama --scope user \
121
+ -e SUNSAMA_EMAIL=your-email@example.com \
122
+ -e SUNSAMA_PASSWORD=your-password \
123
+ -- npx mcp-sunsama
124
+ ```
125
+
126
+ **Scope Options:**
127
+ - `--scope user` - Available across all projects (recommended)
128
+ - `--scope project` - Only available in the current project
129
+
130
+ After adding the server, restart Claude Code to connect to the Sunsama MCP server.
131
+
114
132
  ## API Tools
115
133
 
116
134
  ### Task Management
@@ -129,6 +147,13 @@ Add this configuration to your Claude Desktop MCP settings:
129
147
  - `update-task-backlog` - Move tasks to the backlog
130
148
  - `delete-task` - Delete tasks permanently
131
149
 
150
+ #### Subtask Management
151
+ - `add-subtask` - Create a subtask with a title in one call (recommended for single subtask creation)
152
+ - `create-subtasks` - Create multiple subtasks for a task (low-level API for bulk operations)
153
+ - `update-subtask-title` - Update the title of a subtask
154
+ - `complete-subtask` - Mark a subtask as complete with optional completion timestamp
155
+ - `uncomplete-subtask` - Mark a subtask as incomplete
156
+
132
157
  ### User & Stream Operations
133
158
  - `get-user` - Get current user information
134
159
  - `get-streams` - Get streams/channels for project organization
@@ -0,0 +1,128 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { readdir, readFile } from "fs/promises";
3
+ import { join } from "path";
4
+
5
+ /**
6
+ * Extracts all public instance method names from sunsama-api client .d.ts files.
7
+ * Scans both base.d.ts and the methods/ subdirectory.
8
+ * Skips private, protected, static, and abstract members.
9
+ */
10
+ async function getSunsamaClientMethods(): Promise<Set<string>> {
11
+ const clientDir = join(
12
+ import.meta.dir,
13
+ "../../../node_modules/sunsama-api/dist/types/client",
14
+ );
15
+
16
+ const methodsDir = join(clientDir, "methods");
17
+
18
+ const methodFiles = (await readdir(methodsDir))
19
+ .filter((f) => f.endsWith(".d.ts"))
20
+ .map((f) => join(methodsDir, f));
21
+
22
+ const allFiles = [join(clientDir, "base.d.ts"), ...methodFiles];
23
+
24
+ const methods = new Set<string>();
25
+
26
+ for (const file of allFiles) {
27
+ const content = await readFile(file, "utf-8");
28
+ const lines = content.split("\n");
29
+
30
+ for (const line of lines) {
31
+ // Skip comment lines (JSDoc, inline comments)
32
+ const trimmed = line.trim();
33
+ if (trimmed.startsWith("*") || trimmed.startsWith("//")) continue;
34
+
35
+ // Skip private, protected, static, abstract, and constructor declarations
36
+ if (
37
+ trimmed.startsWith("private ") ||
38
+ trimmed.startsWith("protected ") ||
39
+ trimmed.startsWith("static ") ||
40
+ trimmed.startsWith("abstract ") ||
41
+ trimmed.startsWith("constructor")
42
+ ) continue;
43
+
44
+ // Match a public instance method: 4 spaces indent + camelCase name + (
45
+ const match = line.match(/^ ([a-z][a-zA-Z]+)\(/);
46
+ if (match) {
47
+ methods.add(match[1]);
48
+ }
49
+ }
50
+ }
51
+
52
+ return methods;
53
+ }
54
+
55
+ /**
56
+ * Extracts all SunsamaClient methods called in the MCP tool implementations
57
+ * by scanning for `context.client.<methodName>` patterns.
58
+ */
59
+ async function getWrappedMethods(): Promise<Set<string>> {
60
+ const toolsDir = join(import.meta.dir, "../../../src/tools");
61
+
62
+ const files = (await readdir(toolsDir)).filter(
63
+ (f) => f.endsWith(".ts") && f !== "shared.ts" && f !== "index.ts",
64
+ );
65
+
66
+ const methods = new Set<string>();
67
+ const callPattern = /context\.client\.([a-zA-Z]+)\(/g;
68
+
69
+ for (const file of files) {
70
+ const content = await readFile(join(toolsDir, file), "utf-8");
71
+ let match;
72
+ while ((match = callPattern.exec(content)) !== null) {
73
+ methods.add(match[1]);
74
+ }
75
+ }
76
+
77
+ return methods;
78
+ }
79
+
80
+ /**
81
+ * Methods intentionally not exposed as MCP tools.
82
+ * Add to this list with a comment explaining why.
83
+ */
84
+ const INTENTIONALLY_EXCLUDED = new Set([
85
+ "getConfig", // Client configuration — not useful as a standalone tool
86
+ "getSessionToken", // Internal auth plumbing — session managed by the transport layer
87
+ "isAuthenticated", // Auth checked at the transport layer, not exposed to LLMs
88
+ "login", // Auth handled at startup (stdio) or per-request (HTTP)
89
+ "logout", // Lifecycle managed by the server transport
90
+ "getUserTimezone", // Used internally by tools (e.g. get-tasks-by-day) — not useful standalone
91
+ ]);
92
+
93
+ describe("sunsama-api coverage", () => {
94
+ test("all public SunsamaClient methods are wrapped as MCP tools or explicitly excluded", async () => {
95
+ const clientMethods = await getSunsamaClientMethods();
96
+ const wrappedMethods = await getWrappedMethods();
97
+
98
+ const unwrapped = [...clientMethods].filter(
99
+ (m) => !wrappedMethods.has(m) && !INTENTIONALLY_EXCLUDED.has(m),
100
+ );
101
+
102
+ if (unwrapped.length > 0) {
103
+ console.error(
104
+ "\nUnwrapped SunsamaClient methods (add a tool or add to INTENTIONALLY_EXCLUDED):\n" +
105
+ unwrapped.map((m) => ` - ${m}`).join("\n"),
106
+ );
107
+ }
108
+
109
+ expect(unwrapped).toEqual([]);
110
+ });
111
+
112
+ test("INTENTIONALLY_EXCLUDED list contains only real SunsamaClient methods", async () => {
113
+ const clientMethods = await getSunsamaClientMethods();
114
+
115
+ const stale = [...INTENTIONALLY_EXCLUDED].filter(
116
+ (m) => !clientMethods.has(m),
117
+ );
118
+
119
+ if (stale.length > 0) {
120
+ console.error(
121
+ "\nStale entries in INTENTIONALLY_EXCLUDED (method no longer exists in sunsama-api):\n" +
122
+ stale.map((m) => ` - ${m}`).join("\n"),
123
+ );
124
+ }
125
+
126
+ expect(stale).toEqual([]);
127
+ });
128
+ });
package/bun.lock CHANGED
@@ -10,7 +10,7 @@
10
10
  "cors": "^2.8.5",
11
11
  "express": "^5.1.0",
12
12
  "papaparse": "^5.5.3",
13
- "sunsama-api": "0.12.1",
13
+ "sunsama-api": "0.14.0",
14
14
  "zod": "3.24.4",
15
15
  },
16
16
  "devDependencies": {
@@ -397,7 +397,7 @@
397
397
 
398
398
  "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
399
399
 
400
- "sunsama-api": ["sunsama-api@0.12.1", "", { "dependencies": { "graphql": "^16.11.0", "graphql-tag": "^2.12.6", "marked": "^14.1.3", "tough-cookie": "^5.1.2", "tslib": "^2.8.1", "turndown": "^7.2.0", "yjs": "^13.6.27", "zod": "^3.25.64" } }, "sha512-F2GLJjanAeboP4vYg3VIKP8xxQ/8LLqHhNFx3lm9oONOSvLzVAXEJlwonESuUI/iIf93b2i/jYCJ/2guoUhRYQ=="],
400
+ "sunsama-api": ["sunsama-api@0.14.0", "", { "dependencies": { "graphql": "^16.11.0", "graphql-tag": "^2.12.6", "marked": "^14.1.3", "tough-cookie": "^5.1.2", "tslib": "^2.8.1", "turndown": "^7.2.0", "yjs": "^13.6.27", "zod": "^3.25.64" } }, "sha512-pXRy7naTES4/tjVHP/8H/kOgd4WaUTgNnPBT+aS5WC9Yqqiz+ajo3PT9dix8Yso0fw05lBjnC/hVPq19C00TMw=="],
401
401
 
402
402
  "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
403
403
 
@@ -17,6 +17,7 @@ export declare function stopClientCacheCleanup(): void;
17
17
  export declare function cleanupAllClients(): void;
18
18
  /**
19
19
  * Authenticate HTTP request and get or create cached client
20
+ * Supports both Basic Auth (email/password) and Bearer token authentication
20
21
  * Uses secure cache key (password hash) and race condition protection
21
22
  */
22
23
  export declare function authenticateHttpRequest(authHeader?: string): Promise<SessionData>;
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/auth/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAe9C;;GAEG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAiBtF;AA+CD,wBAAgB,uBAAuB,IAAI,IAAI,CAQ9C;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAM7C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAYxC;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CAiEtB"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/auth/http.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgB9C;;GAEG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAiBtF;AAwDD,wBAAgB,uBAAuB,IAAI,IAAI,CAQ9C;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAM7C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAYxC;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CA0ItB"}
package/dist/auth/http.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "crypto";
2
2
  import { SunsamaClient } from "sunsama-api/client";
3
+ import { AuthenticationError } from "./types.js";
4
+ import { loginWithRetry } from "./retry.js";
3
5
  import { getSessionConfig } from "../config/session-config.js";
4
6
  // Client cache with TTL management (keyed by credential hash for security)
5
7
  const clientCache = new Map();
@@ -18,12 +20,12 @@ export function parseBasicAuth(authHeader) {
18
20
  const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
19
21
  const colonIndex = credentials.indexOf(':');
20
22
  if (colonIndex === -1) {
21
- throw new Error("Invalid Basic Auth format");
23
+ throw new AuthenticationError("Invalid Basic Auth format");
22
24
  }
23
25
  const email = credentials.substring(0, colonIndex);
24
26
  const password = credentials.substring(colonIndex + 1);
25
27
  if (!email || password === undefined) {
26
- throw new Error("Invalid Basic Auth format");
28
+ throw new AuthenticationError("Invalid Basic Auth format");
27
29
  }
28
30
  return { email, password };
29
31
  }
@@ -36,6 +38,14 @@ function getCacheKey(email, password) {
36
38
  .update(`${email}:${password}`)
37
39
  .digest('hex');
38
40
  }
41
+ /**
42
+ * Generate secure cache key from session token
43
+ */
44
+ function getTokenCacheKey(token) {
45
+ return createHash('sha256')
46
+ .update(`token:${token}`)
47
+ .digest('hex');
48
+ }
39
49
  /**
40
50
  * Check if a cached client is still valid based on TTL
41
51
  */
@@ -102,18 +112,80 @@ export function cleanupAllClients() {
102
112
  }
103
113
  /**
104
114
  * Authenticate HTTP request and get or create cached client
115
+ * Supports both Basic Auth (email/password) and Bearer token authentication
105
116
  * Uses secure cache key (password hash) and race condition protection
106
117
  */
107
118
  export async function authenticateHttpRequest(authHeader) {
108
- if (!authHeader || !authHeader.startsWith('Basic ')) {
109
- throw new Error("Basic Auth required");
119
+ if (!authHeader) {
120
+ throw new AuthenticationError("Authorization header required (Basic or Bearer)");
121
+ }
122
+ const isBearer = authHeader.startsWith('Bearer ');
123
+ const isBasic = authHeader.startsWith('Basic ');
124
+ if (!isBearer && !isBasic) {
125
+ throw new AuthenticationError("Authorization header must be Basic or Bearer");
110
126
  }
111
- const { email, password } = parseBasicAuth(authHeader);
112
- const cacheKey = getCacheKey(email, password);
113
127
  const now = Date.now();
128
+ let cacheKey;
129
+ let identifier;
130
+ if (isBearer) {
131
+ const token = authHeader.replace('Bearer ', '').trim();
132
+ if (!token) {
133
+ throw new AuthenticationError("Invalid Bearer token");
134
+ }
135
+ cacheKey = getTokenCacheKey(token);
136
+ identifier = 'token-user';
137
+ // Check for pending authentication (race condition protection)
138
+ if (authPromises.has(cacheKey)) {
139
+ console.error(`[Client Cache] Waiting for pending authentication for ${identifier}`);
140
+ return await authPromises.get(cacheKey);
141
+ }
142
+ // Check cache first
143
+ if (clientCache.has(cacheKey)) {
144
+ const cached = clientCache.get(cacheKey);
145
+ if (isClientValid(cached)) {
146
+ console.error(`[Client Cache] Reusing cached client for ${identifier}`);
147
+ cached.lastAccessedAt = now;
148
+ return cached;
149
+ }
150
+ else {
151
+ console.error(`[Client Cache] Cached client expired for ${identifier}, re-authenticating`);
152
+ try {
153
+ cached.sunsamaClient.logout();
154
+ }
155
+ catch (err) {
156
+ console.error(`[Client Cache] Error logging out expired client:`, err);
157
+ }
158
+ clientCache.delete(cacheKey);
159
+ }
160
+ }
161
+ // Create authentication promise for Bearer token
162
+ console.error(`[Client Cache] Creating new client for ${identifier}`);
163
+ const authPromise = (async () => {
164
+ try {
165
+ const sunsamaClient = new SunsamaClient({ sessionToken: token });
166
+ const sessionData = {
167
+ sunsamaClient,
168
+ createdAt: now,
169
+ lastAccessedAt: now
170
+ };
171
+ clientCache.set(cacheKey, sessionData);
172
+ console.error(`[Client Cache] Cached new client for ${identifier} (total: ${clientCache.size})`);
173
+ return sessionData;
174
+ }
175
+ finally {
176
+ authPromises.delete(cacheKey);
177
+ }
178
+ })();
179
+ authPromises.set(cacheKey, authPromise);
180
+ return authPromise;
181
+ }
182
+ // Basic Auth flow
183
+ const { email, password } = parseBasicAuth(authHeader);
184
+ cacheKey = getCacheKey(email, password);
185
+ identifier = email;
114
186
  // Check for pending authentication (race condition protection)
115
187
  if (authPromises.has(cacheKey)) {
116
- console.error(`[Client Cache] Waiting for pending authentication for ${email}`);
188
+ console.error(`[Client Cache] Waiting for pending authentication for ${identifier}`);
117
189
  return await authPromises.get(cacheKey);
118
190
  }
119
191
  // Check cache first
@@ -121,13 +193,13 @@ export async function authenticateHttpRequest(authHeader) {
121
193
  const cached = clientCache.get(cacheKey);
122
194
  // Check if still valid (lazy expiration)
123
195
  if (isClientValid(cached)) {
124
- console.error(`[Client Cache] Reusing cached client for ${email}`);
196
+ console.error(`[Client Cache] Reusing cached client for ${identifier}`);
125
197
  // Update last accessed time (sliding window)
126
198
  cached.lastAccessedAt = now;
127
199
  return cached;
128
200
  }
129
201
  else {
130
- console.error(`[Client Cache] Cached client expired for ${email}, re-authenticating`);
202
+ console.error(`[Client Cache] Cached client expired for ${identifier}, re-authenticating`);
131
203
  // Cleanup expired client
132
204
  try {
133
205
  cached.sunsamaClient.logout();
@@ -139,11 +211,16 @@ export async function authenticateHttpRequest(authHeader) {
139
211
  }
140
212
  }
141
213
  // Create authentication promise to prevent concurrent authentications
142
- console.error(`[Client Cache] Creating new client for ${email}`);
214
+ console.error(`[Client Cache] Creating new client for ${identifier}`);
143
215
  const authPromise = (async () => {
144
216
  try {
145
217
  const sunsamaClient = new SunsamaClient();
146
- await sunsamaClient.login(email, password);
218
+ try {
219
+ await loginWithRetry(sunsamaClient, email, password);
220
+ }
221
+ catch (err) {
222
+ throw new AuthenticationError("Login failed: invalid credentials", err);
223
+ }
147
224
  const sessionData = {
148
225
  sunsamaClient,
149
226
  email,
@@ -151,7 +228,7 @@ export async function authenticateHttpRequest(authHeader) {
151
228
  lastAccessedAt: now
152
229
  };
153
230
  clientCache.set(cacheKey, sessionData);
154
- console.error(`[Client Cache] Cached new client for ${email} (total: ${clientCache.size})`);
231
+ console.error(`[Client Cache] Cached new client for ${identifier} (total: ${clientCache.size})`);
155
232
  return sessionData;
156
233
  }
157
234
  finally {
@@ -0,0 +1,8 @@
1
+ import { SunsamaClient } from "sunsama-api/client";
2
+ /**
3
+ * Calls sunsamaClient.login() with exponential backoff on 429 rate limit errors.
4
+ * Makes up to 4 attempts total (initial + 3 retries) with delays of 5s, 15s, 45s.
5
+ * Non-429 errors are rethrown immediately without retrying.
6
+ */
7
+ export declare function loginWithRetry(client: SunsamaClient, email: string, password: string): Promise<void>;
8
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/auth/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAKnD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -0,0 +1,25 @@
1
+ import { SunsamaClient } from "sunsama-api/client";
2
+ import { SunsamaAuthError } from "sunsama-api/errors";
3
+ const RETRY_DELAYS = [1_000, 5_000, 15_000]; // ms
4
+ /**
5
+ * Calls sunsamaClient.login() with exponential backoff on 429 rate limit errors.
6
+ * Makes up to 4 attempts total (initial + 3 retries) with delays of 5s, 15s, 45s.
7
+ * Non-429 errors are rethrown immediately without retrying.
8
+ */
9
+ export async function loginWithRetry(client, email, password) {
10
+ for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
11
+ try {
12
+ await client.login(email, password);
13
+ return;
14
+ }
15
+ catch (err) {
16
+ const isRateLimit = err instanceof SunsamaAuthError && err.message.includes("429");
17
+ if (!isRateLimit || attempt === RETRY_DELAYS.length) {
18
+ throw err;
19
+ }
20
+ const delay = RETRY_DELAYS[attempt];
21
+ console.error(`[Auth] Rate limited (429). Retrying in ${delay / 1000}s (attempt ${attempt + 1}/${RETRY_DELAYS.length})`);
22
+ await new Promise((resolve) => setTimeout(resolve, delay));
23
+ }
24
+ }
25
+ }
@@ -1,6 +1,7 @@
1
1
  import { SunsamaClient } from "sunsama-api/client";
2
2
  /**
3
3
  * Initialize stdio authentication using environment variables
4
+ * Supports session token (SUNSAMA_SESSION_TOKEN) or email/password (SUNSAMA_EMAIL, SUNSAMA_PASSWORD)
4
5
  * @throws {Error} If credentials are missing or authentication fails
5
6
  */
6
7
  export declare function initializeStdioAuth(): Promise<SunsamaClient>;
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/auth/stdio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAOnD;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,aAAa,CAAC,CAWlE;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,aAAa,CAAC,CAMrE"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/auth/stdio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQnD;;;;GAIG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,aAAa,CAAC,CAoBlE;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,aAAa,CAAC,CAMrE"}
@@ -1,18 +1,28 @@
1
1
  import { SunsamaClient } from "sunsama-api/client";
2
+ import { loginWithRetry } from "./retry.js";
2
3
  /**
3
4
  * Cached authentication promise to prevent concurrent auth attempts
4
5
  */
5
6
  let authenticationPromise = null;
6
7
  /**
7
8
  * Initialize stdio authentication using environment variables
9
+ * Supports session token (SUNSAMA_SESSION_TOKEN) or email/password (SUNSAMA_EMAIL, SUNSAMA_PASSWORD)
8
10
  * @throws {Error} If credentials are missing or authentication fails
9
11
  */
10
12
  export async function initializeStdioAuth() {
13
+ // Prefer session token if available (useful for Google SSO users)
14
+ if (process.env.SUNSAMA_SESSION_TOKEN) {
15
+ const sunsamaClient = new SunsamaClient({
16
+ sessionToken: process.env.SUNSAMA_SESSION_TOKEN
17
+ });
18
+ return sunsamaClient;
19
+ }
20
+ // Fall back to email/password authentication
11
21
  if (!process.env.SUNSAMA_EMAIL || !process.env.SUNSAMA_PASSWORD) {
12
- throw new Error("Sunsama credentials not configured. Please set SUNSAMA_EMAIL and SUNSAMA_PASSWORD environment variables.");
22
+ throw new Error("Sunsama credentials not configured. Please set SUNSAMA_SESSION_TOKEN or both SUNSAMA_EMAIL and SUNSAMA_PASSWORD environment variables.");
13
23
  }
14
24
  const sunsamaClient = new SunsamaClient();
15
- await sunsamaClient.login(process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
25
+ await loginWithRetry(sunsamaClient, process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
16
26
  return sunsamaClient;
17
27
  }
18
28
  /**
@@ -4,8 +4,15 @@ import { SunsamaClient } from "sunsama-api/client";
4
4
  */
5
5
  export interface SessionData extends Record<string, unknown> {
6
6
  sunsamaClient: SunsamaClient;
7
- email: string;
7
+ email?: string;
8
8
  createdAt: number;
9
9
  lastAccessedAt: number;
10
10
  }
11
+ /**
12
+ * Error thrown when authentication fails (missing/invalid credentials or login failure).
13
+ * Used to distinguish auth failures (401) from other server errors (500) in HTTP transport.
14
+ */
15
+ export declare class AuthenticationError extends Error {
16
+ constructor(message: string, cause?: unknown);
17
+ }
11
18
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAO7C"}
@@ -1 +1,14 @@
1
1
  import { SunsamaClient } from "sunsama-api/client";
2
+ /**
3
+ * Error thrown when authentication fails (missing/invalid credentials or login failure).
4
+ * Used to distinguish auth failures (401) from other server errors (500) in HTTP transport.
5
+ */
6
+ export class AuthenticationError extends Error {
7
+ constructor(message, cause) {
8
+ super(message);
9
+ this.name = "AuthenticationError";
10
+ if (cause !== undefined) {
11
+ this.cause = cause;
12
+ }
13
+ }
14
+ }
@@ -4,7 +4,7 @@
4
4
  * VERSION is automatically synced from package.json by scripts/sync-version.ts
5
5
  * when running `bun run version`
6
6
  */
7
- export declare const VERSION = "0.16.1";
7
+ export declare const VERSION = "0.18.0";
8
8
  export declare const SERVER_NAME = "Sunsama API Server";
9
9
  export declare const PACKAGE_NAME = "mcp-sunsama";
10
10
  //# sourceMappingURL=constants.d.ts.map