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.
- package/.claude/commands/upgrade-sunsama-api.md +70 -0
- package/CHANGELOG.md +37 -0
- package/CLAUDE.md +4 -2
- package/README.md +25 -0
- package/__tests__/unit/tools/api-coverage.test.ts +128 -0
- package/bun.lock +2 -2
- package/dist/auth/http.d.ts +1 -0
- package/dist/auth/http.d.ts.map +1 -1
- package/dist/auth/http.js +89 -12
- package/dist/auth/retry.d.ts +8 -0
- package/dist/auth/retry.d.ts.map +1 -0
- package/dist/auth/retry.js +25 -0
- package/dist/auth/stdio.d.ts +1 -0
- package/dist/auth/stdio.d.ts.map +1 -1
- package/dist/auth/stdio.js +12 -2
- package/dist/auth/types.d.ts +8 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js +13 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/main.js +12 -18
- package/dist/schemas.d.ts +165 -6
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +69 -0
- package/dist/schemas.test.js +8 -6
- package/dist/tools/calendar-tools.d.ts +4 -0
- package/dist/tools/calendar-tools.d.ts.map +1 -0
- package/dist/tools/calendar-tools.js +55 -0
- package/dist/tools/index.d.ts +2 -6
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/shared.d.ts +46 -36
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +16 -22
- package/dist/tools/stream-tools.d.ts +2 -12
- package/dist/tools/stream-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.d.ts +22 -90
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +125 -1
- package/dist/tools/user-tools.d.ts +2 -12
- package/dist/tools/user-tools.d.ts.map +1 -1
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +21 -8
- package/dist/utils/client-resolver.d.ts +1 -1
- package/dist/utils/client-resolver.d.ts.map +1 -1
- package/dist/utils/client-resolver.js +15 -4
- package/dist/utils/task-trimmer.js +1 -1
- package/package.json +3 -3
- package/src/auth/http.ts +97 -12
- package/src/auth/retry.ts +35 -0
- package/src/auth/stdio.ts +13 -2
- package/src/auth/types.ts +15 -1
- package/src/constants.ts +1 -1
- package/src/main.ts +12 -18
- package/src/schemas.test.ts +8 -6
- package/src/schemas.ts +160 -0
- package/src/tools/calendar-tools.ts +91 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/shared.ts +63 -37
- package/src/tools/task-tools.ts +197 -0
- package/src/transports/http.ts +20 -8
- package/src/utils/client-resolver.ts +22 -6
- 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 (
|
|
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**:
|
|
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.
|
|
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.
|
|
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
|
|
package/dist/auth/http.d.ts
CHANGED
|
@@ -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>;
|
package/dist/auth/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/auth/http.ts"],"names":[],"mappings":"
|
|
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
|
|
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
|
|
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
|
|
109
|
-
throw new
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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 ${
|
|
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
|
+
}
|
package/dist/auth/stdio.d.ts
CHANGED
|
@@ -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>;
|
package/dist/auth/stdio.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/auth/stdio.js
CHANGED
|
@@ -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
|
|
25
|
+
await loginWithRetry(sunsamaClient, process.env.SUNSAMA_EMAIL, process.env.SUNSAMA_PASSWORD);
|
|
16
26
|
return sunsamaClient;
|
|
17
27
|
}
|
|
18
28
|
/**
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/auth/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/auth/types.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -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.
|
|
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
|