mcp-sunsama 0.14.1 → 0.15.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/.env.example +12 -7
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +73 -5
- package/Dockerfile +1 -1
- package/README.md +65 -9
- package/__tests__/integration/http-transport.test.ts +381 -0
- package/__tests__/unit/auth/http.test.ts +283 -0
- package/__tests__/unit/config/session-config.test.ts +176 -0
- package/__tests__/unit/session/session-manager.test.ts +258 -0
- package/bun.lock +43 -117
- package/dist/__tests__/integration/http-transport.test.d.ts +2 -0
- package/dist/__tests__/integration/http-transport.test.d.ts.map +1 -0
- package/dist/__tests__/integration/http-transport.test.js +265 -0
- package/dist/__tests__/unit/auth/http.test.d.ts +2 -0
- package/dist/__tests__/unit/auth/http.test.d.ts.map +1 -0
- package/dist/__tests__/unit/auth/http.test.js +211 -0
- package/dist/__tests__/unit/config/session-config.test.d.ts +2 -0
- package/dist/__tests__/unit/config/session-config.test.d.ts.map +1 -0
- package/dist/__tests__/unit/config/session-config.test.js +134 -0
- package/dist/__tests__/unit/session/session-manager.test.d.ts +2 -0
- package/dist/__tests__/unit/session/session-manager.test.d.ts.map +1 -0
- package/dist/__tests__/unit/session/session-manager.test.js +192 -0
- package/dist/auth/http.d.ts +12 -9
- package/dist/auth/http.d.ts.map +1 -1
- package/dist/auth/http.js +141 -33
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/config/session-config.d.ts +49 -0
- package/dist/config/session-config.d.ts.map +1 -0
- package/dist/config/session-config.js +54 -0
- package/dist/config/transport.d.ts +2 -24
- package/dist/config/transport.d.ts.map +1 -1
- package/dist/config/transport.js +15 -52
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +5 -0
- package/dist/main.js +45 -48
- package/dist/resources/index.d.ts +5 -2
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +7 -4
- package/dist/schemas.d.ts +14 -18
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +6 -14
- package/dist/session/session-manager.d.ts +53 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +141 -0
- package/dist/src/auth/http.d.ts +23 -0
- package/dist/src/auth/http.d.ts.map +1 -0
- package/dist/src/auth/http.js +165 -0
- package/dist/src/auth/stdio.d.ts +13 -0
- package/dist/src/auth/stdio.d.ts.map +1 -0
- package/dist/src/auth/stdio.js +28 -0
- package/dist/src/auth/types.d.ts +11 -0
- package/dist/src/auth/types.d.ts.map +1 -0
- package/dist/src/auth/types.js +1 -0
- package/dist/src/config/session-config.d.ts +49 -0
- package/dist/src/config/session-config.d.ts.map +1 -0
- package/dist/src/config/session-config.js +54 -0
- package/dist/src/config/transport.d.ts +12 -0
- package/dist/src/config/transport.d.ts.map +1 -0
- package/dist/src/config/transport.js +27 -0
- package/dist/src/main.d.ts +3 -0
- package/dist/src/main.d.ts.map +1 -0
- package/dist/src/main.js +58 -0
- package/dist/src/resources/index.d.ts +13 -0
- package/dist/src/resources/index.d.ts.map +1 -0
- package/dist/src/resources/index.js +166 -0
- package/dist/src/schemas.d.ts +658 -0
- package/dist/src/schemas.d.ts.map +1 -0
- package/dist/src/schemas.js +193 -0
- package/dist/src/schemas.test.d.ts +2 -0
- package/dist/src/schemas.test.d.ts.map +1 -0
- package/dist/src/schemas.test.js +857 -0
- package/dist/src/session/session-manager.d.ts +53 -0
- package/dist/src/session/session-manager.d.ts.map +1 -0
- package/dist/src/session/session-manager.js +141 -0
- package/dist/src/tools/index.d.ts +11 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +12 -0
- package/dist/src/tools/shared.d.ts +63 -0
- package/dist/src/tools/shared.d.ts.map +1 -0
- package/dist/src/tools/shared.js +84 -0
- package/dist/src/tools/stream-tools.d.ts +13 -0
- package/dist/src/tools/stream-tools.d.ts.map +1 -0
- package/dist/src/tools/stream-tools.js +12 -0
- package/dist/src/tools/task-tools.d.ts +91 -0
- package/dist/src/tools/task-tools.d.ts.map +1 -0
- package/dist/src/tools/task-tools.js +271 -0
- package/dist/src/tools/user-tools.d.ts +13 -0
- package/dist/src/tools/user-tools.d.ts.map +1 -0
- package/dist/src/tools/user-tools.js +13 -0
- package/dist/src/transports/http.d.ts +8 -0
- package/dist/src/transports/http.d.ts.map +1 -0
- package/dist/src/transports/http.js +245 -0
- package/dist/src/transports/stdio.d.ts +3 -0
- package/dist/src/transports/stdio.d.ts.map +1 -0
- package/dist/src/transports/stdio.js +11 -0
- package/dist/src/utils/client-resolver.d.ts +8 -0
- package/dist/src/utils/client-resolver.d.ts.map +1 -0
- package/dist/src/utils/client-resolver.js +23 -0
- package/dist/src/utils/task-filters.d.ts +29 -0
- package/dist/src/utils/task-filters.d.ts.map +1 -0
- package/dist/src/utils/task-filters.js +42 -0
- package/dist/src/utils/task-trimmer.d.ts +50 -0
- package/dist/src/utils/task-trimmer.d.ts.map +1 -0
- package/dist/src/utils/task-trimmer.js +59 -0
- package/dist/src/utils/to-tsv.d.ts +8 -0
- package/dist/src/utils/to-tsv.d.ts.map +1 -0
- package/dist/src/utils/to-tsv.js +64 -0
- package/dist/tools/index.d.ts +3 -104
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/shared.d.ts +37 -16
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +42 -24
- package/dist/tools/stream-tools.d.ts +2 -3
- package/dist/tools/stream-tools.d.ts.map +1 -1
- package/dist/tools/stream-tools.js +4 -8
- package/dist/tools/task-tools.d.ts +17 -176
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +72 -223
- package/dist/tools/user-tools.d.ts +2 -3
- package/dist/tools/user-tools.d.ts.map +1 -1
- package/dist/tools/user-tools.js +5 -8
- package/dist/transports/http.d.ts +8 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +245 -0
- package/dist/transports/stdio.d.ts +3 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +11 -0
- package/dist/utils/client-resolver.d.ts +4 -6
- package/dist/utils/client-resolver.d.ts.map +1 -1
- package/dist/utils/client-resolver.js +15 -11
- package/package.json +13 -6
- package/scripts/kill-inspector.sh +22 -0
- package/src/auth/http.ts +170 -37
- package/src/auth/types.ts +2 -0
- package/src/config/session-config.ts +63 -0
- package/src/config/transport.ts +24 -67
- package/src/main.ts +53 -51
- package/src/resources/index.ts +11 -7
- package/src/schemas.ts +138 -60
- package/src/session/session-manager.ts +177 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/shared.ts +67 -42
- package/src/tools/stream-tools.ts +8 -13
- package/src/tools/task-tools.ts +183 -290
- package/src/tools/user-tools.ts +9 -13
- package/src/transports/http.ts +289 -0
- package/src/transports/stdio.ts +14 -0
- package/src/utils/client-resolver.ts +16 -12
- package/dev/prd-update-task-stream.md +0 -112
package/.env.example
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# MCP Server Configuration
|
|
2
|
-
PORT=
|
|
2
|
+
PORT=8080
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
4
|
+
# Transport Selection
|
|
5
|
+
# Options: stdio (default) | http
|
|
6
|
+
TRANSPORT_MODE=stdio
|
|
7
|
+
|
|
8
|
+
# HTTP Transport Configuration (only used when TRANSPORT_MODE=http)
|
|
9
|
+
HTTP_ENDPOINT=/mcp
|
|
7
10
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
+
# Sunsama Authentication
|
|
12
|
+
# For stdio transport: Required
|
|
13
|
+
# For HTTP transport: Provided via HTTP Basic Auth per request
|
|
14
|
+
SUNSAMA_EMAIL=your-email@example.com
|
|
15
|
+
SUNSAMA_PASSWORD=your-password
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# mcp-sunsama
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f2a39f2: Add HTTP transport support with session management and comprehensive test suite
|
|
8
|
+
|
|
9
|
+
- Implement HTTP Stream transport alongside existing stdio transport
|
|
10
|
+
- Add dual-layer session caching (client cache + session manager)
|
|
11
|
+
- Implement TTL-based session management with configurable timeouts
|
|
12
|
+
- Add comprehensive unit test suite (251 tests)
|
|
13
|
+
- Add integration test suite for authenticated flows
|
|
14
|
+
- Reorganize tests into standard **tests** directory structure
|
|
15
|
+
- Add TypeScript types for better type safety in tests
|
|
16
|
+
|
|
3
17
|
## 0.14.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/CLAUDE.md
CHANGED
|
@@ -11,9 +11,16 @@ bun run typecheck # TypeScript type checking
|
|
|
11
11
|
bun run typecheck:watch # Watch mode type checking
|
|
12
12
|
bun run inspect # MCP Inspector for debugging
|
|
13
13
|
|
|
14
|
+
# Testing
|
|
15
|
+
bun test # Run unit tests only
|
|
16
|
+
bun test:unit # Run unit tests only (alias)
|
|
17
|
+
bun test:integration # Run integration tests (requires credentials)
|
|
18
|
+
bun test:all # Run all tests
|
|
19
|
+
bun test:watch # Watch mode for unit tests
|
|
20
|
+
|
|
14
21
|
# Build and Distribution
|
|
15
22
|
bun run build # Compile TypeScript to dist/
|
|
16
|
-
bun
|
|
23
|
+
bun run prepublishOnly # Run build before publish
|
|
17
24
|
|
|
18
25
|
# Version Management (Changeset)
|
|
19
26
|
bun run changeset # Create new changeset
|
|
@@ -36,7 +43,21 @@ This server supports two transport modes with different authentication strategie
|
|
|
36
43
|
- Session-isolated SunsamaClient instances
|
|
37
44
|
- Credentials provided in Authorization header
|
|
38
45
|
|
|
39
|
-
Transport selection via `
|
|
46
|
+
Transport selection via `TRANSPORT_MODE` environment variable ("stdio" | "http").
|
|
47
|
+
|
|
48
|
+
### Session Management Architecture
|
|
49
|
+
For HTTP transport, the server implements dual-layer session caching:
|
|
50
|
+
|
|
51
|
+
**Client Cache Layer** (`utils/client-resolver.ts`):
|
|
52
|
+
- In-memory Map caching authenticated SunsamaClient instances
|
|
53
|
+
- SHA-256 hashed credential keys for security
|
|
54
|
+
- Automatic cache invalidation on authentication failure
|
|
55
|
+
|
|
56
|
+
**Session Manager Layer** (`session/session-manager.ts`):
|
|
57
|
+
- Manages session lifecycle with configurable TTL
|
|
58
|
+
- Tracks session metadata (createdAt, lastAccessedAt)
|
|
59
|
+
- Automatic cleanup of expired sessions
|
|
60
|
+
- Transport reference management for proper cleanup
|
|
40
61
|
|
|
41
62
|
### Client Resolution Pattern
|
|
42
63
|
`utils/client-resolver.ts` abstracts transport differences:
|
|
@@ -134,11 +155,32 @@ src/
|
|
|
134
155
|
├── resources/
|
|
135
156
|
│ └── index.ts # API documentation resource
|
|
136
157
|
├── auth/ # Authentication strategies per transport type
|
|
158
|
+
│ ├── stdio.ts # Stdio transport authentication
|
|
159
|
+
│ ├── http.ts # HTTP Basic Auth parsing
|
|
160
|
+
│ └── types.ts # Shared auth types
|
|
161
|
+
├── transports/
|
|
162
|
+
│ ├── stdio.ts # Stdio transport implementation
|
|
163
|
+
│ └── http.ts # HTTP Stream transport with session management
|
|
164
|
+
├── session/
|
|
165
|
+
│ └── session-manager.ts # Session lifecycle management
|
|
137
166
|
├── config/ # Environment configuration and validation
|
|
138
|
-
├──
|
|
167
|
+
│ ├── transport.ts # Transport mode configuration
|
|
168
|
+
│ └── session-config.ts # Session TTL configuration
|
|
169
|
+
├── utils/ # Reusable utilities
|
|
170
|
+
│ ├── client-resolver.ts # Transport-agnostic client resolution
|
|
171
|
+
│ ├── task-filters.ts # Task completion filtering
|
|
172
|
+
│ ├── task-trimmer.ts # Response size optimization
|
|
173
|
+
│ └── to-tsv.ts # TSV formatting utilities
|
|
139
174
|
├── schemas.ts # Zod validation schemas for all tools
|
|
140
|
-
├── schemas.test.ts # Comprehensive test suite for all Zod schemas
|
|
141
175
|
└── main.ts # Streamlined server setup (47 lines vs 1162 before)
|
|
176
|
+
|
|
177
|
+
__tests__/
|
|
178
|
+
├── unit/ # Unit tests (251+ tests, no auth required)
|
|
179
|
+
│ ├── auth/ # Auth utility tests
|
|
180
|
+
│ ├── config/ # Configuration tests
|
|
181
|
+
│ └── session/ # Session management tests
|
|
182
|
+
└── integration/ # Integration tests (requires credentials)
|
|
183
|
+
└── http-transport.test.ts
|
|
142
184
|
```
|
|
143
185
|
|
|
144
186
|
### Tool Architecture Improvements
|
|
@@ -159,6 +201,29 @@ src/
|
|
|
159
201
|
- **Zod Schema Integration**: Full TypeScript inference from Zod schemas
|
|
160
202
|
- **Eliminated `any` Types**: All parameters properly typed with generated types
|
|
161
203
|
|
|
204
|
+
## Testing Architecture
|
|
205
|
+
|
|
206
|
+
### Test Organization
|
|
207
|
+
Tests are organized in the `__tests__/` directory following standard conventions:
|
|
208
|
+
|
|
209
|
+
**Unit Tests** (`__tests__/unit/`):
|
|
210
|
+
- Run without authentication requirements
|
|
211
|
+
- Test individual components in isolation
|
|
212
|
+
- Fast execution for TDD workflow
|
|
213
|
+
- Run with `bun test` or `bun test:unit`
|
|
214
|
+
|
|
215
|
+
**Integration Tests** (`__tests__/integration/`):
|
|
216
|
+
- Require real Sunsama credentials
|
|
217
|
+
- Test full HTTP transport flow
|
|
218
|
+
- Validate session management
|
|
219
|
+
- Run with `bun test:integration`
|
|
220
|
+
|
|
221
|
+
### Test Patterns
|
|
222
|
+
- Use Bun's built-in test runner with pattern matching
|
|
223
|
+
- TypeScript types from MCP SDK for type-safe testing
|
|
224
|
+
- Generic helper functions for request/response handling
|
|
225
|
+
- Mock transports for unit testing session management
|
|
226
|
+
|
|
162
227
|
## Important Notes
|
|
163
228
|
|
|
164
229
|
### Version Synchronization
|
|
@@ -170,8 +235,11 @@ Required for stdio transport:
|
|
|
170
235
|
- `SUNSAMA_PASSWORD`: Sunsama account password
|
|
171
236
|
|
|
172
237
|
Optional:
|
|
173
|
-
- `
|
|
238
|
+
- `TRANSPORT_MODE`: "stdio" (default) | "http"
|
|
174
239
|
- `PORT`: Server port (default: 3002, HTTP transport only)
|
|
240
|
+
- `SESSION_TTL`: Session timeout in milliseconds (default: 3600000 / 1 hour)
|
|
241
|
+
- `CLIENT_IDLE_TIMEOUT`: Client idle timeout in milliseconds (default: 900000 / 15 minutes)
|
|
242
|
+
- `MAX_SESSIONS`: Maximum concurrent sessions for HTTP transport (default: 100)
|
|
175
243
|
|
|
176
244
|
### Task Operations
|
|
177
245
|
Full CRUD support:
|
package/Dockerfile
CHANGED
package/README.md
CHANGED
|
@@ -48,22 +48,44 @@ cp .env.example .env
|
|
|
48
48
|
Environment variables:
|
|
49
49
|
- `SUNSAMA_EMAIL` - Your Sunsama account email (required for stdio transport)
|
|
50
50
|
- `SUNSAMA_PASSWORD` - Your Sunsama account password (required for stdio transport)
|
|
51
|
-
- `
|
|
52
|
-
- `PORT` - Server port for HTTP transport (default:
|
|
53
|
-
- `
|
|
51
|
+
- `TRANSPORT_MODE` - Transport type: `stdio` (default) or `http`
|
|
52
|
+
- `PORT` - Server port for HTTP transport (default: 8080)
|
|
53
|
+
- `HTTP_ENDPOINT` - MCP endpoint path (default: `/mcp`)
|
|
54
|
+
- `SESSION_TTL` - Session timeout in milliseconds (default: 3600000 / 1 hour)
|
|
55
|
+
- `CLIENT_IDLE_TIMEOUT` - Client idle timeout in milliseconds (default: 900000 / 15 minutes)
|
|
56
|
+
- `MAX_SESSIONS` - Maximum concurrent sessions for HTTP transport (default: 100)
|
|
54
57
|
|
|
55
58
|
## Usage
|
|
56
59
|
|
|
57
|
-
###
|
|
60
|
+
### Transport Modes
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
This server supports two transport modes:
|
|
63
|
+
|
|
64
|
+
#### Stdio Transport (Default)
|
|
65
|
+
For local AI assistants (Claude Desktop, Cursor, etc.):
|
|
66
|
+
```bash
|
|
67
|
+
bun run dev
|
|
68
|
+
# or
|
|
69
|
+
TRANSPORT_MODE=stdio bun run src/main.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### HTTP Stream Transport
|
|
73
|
+
For remote access and web-based integrations:
|
|
60
74
|
```bash
|
|
61
|
-
bun run src/main.ts
|
|
75
|
+
TRANSPORT_MODE=http PORT=8080 bun run src/main.ts
|
|
62
76
|
```
|
|
63
77
|
|
|
64
|
-
**HTTP
|
|
78
|
+
**HTTP Endpoints:**
|
|
79
|
+
- MCP Endpoint: `POST http://localhost:8080/mcp`
|
|
80
|
+
- Health Check: `GET http://localhost:8080/`
|
|
81
|
+
|
|
82
|
+
**Authentication:**
|
|
83
|
+
HTTP requests require HTTP Basic Auth with your Sunsama credentials:
|
|
65
84
|
```bash
|
|
66
|
-
|
|
85
|
+
curl -X POST http://localhost:8080/mcp \
|
|
86
|
+
-H "Authorization: Basic $(echo -n 'your-email:your-password' | base64)" \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
67
89
|
```
|
|
68
90
|
|
|
69
91
|
### Claude Desktop Configuration
|
|
@@ -121,6 +143,15 @@ bun run inspect
|
|
|
121
143
|
|
|
122
144
|
Then connect the MCP Inspector to test the tools interactively.
|
|
123
145
|
|
|
146
|
+
### Testing
|
|
147
|
+
```bash
|
|
148
|
+
bun test # Run unit tests only
|
|
149
|
+
bun test:unit # Run unit tests only (alias)
|
|
150
|
+
bun test:integration # Run integration tests (requires credentials)
|
|
151
|
+
bun test:all # Run all tests
|
|
152
|
+
bun test:watch # Watch mode for unit tests
|
|
153
|
+
```
|
|
154
|
+
|
|
124
155
|
### Build and Type Checking
|
|
125
156
|
```bash
|
|
126
157
|
bun run build # Compile TypeScript to dist/
|
|
@@ -146,9 +177,32 @@ src/
|
|
|
146
177
|
├── resources/
|
|
147
178
|
│ └── index.ts # API documentation resource
|
|
148
179
|
├── auth/ # Authentication strategies
|
|
180
|
+
│ ├── stdio.ts # Stdio transport authentication
|
|
181
|
+
│ ├── http.ts # HTTP Basic Auth parsing
|
|
182
|
+
│ └── types.ts # Shared auth types
|
|
183
|
+
├── transports/
|
|
184
|
+
│ ├── stdio.ts # Stdio transport implementation
|
|
185
|
+
│ └── http.ts # HTTP Stream transport with session management
|
|
186
|
+
├── session/
|
|
187
|
+
│ └── session-manager.ts # Session lifecycle management
|
|
149
188
|
├── config/ # Environment configuration
|
|
189
|
+
│ ├── transport.ts # Transport mode configuration
|
|
190
|
+
│ └── session-config.ts # Session TTL configuration
|
|
150
191
|
├── utils/ # Utilities (filtering, trimming, etc.)
|
|
192
|
+
│ ├── client-resolver.ts # Transport-agnostic client resolution
|
|
193
|
+
│ ├── task-filters.ts # Task completion filtering
|
|
194
|
+
│ ├── task-trimmer.ts # Response size optimization
|
|
195
|
+
│ └── to-tsv.ts # TSV formatting utilities
|
|
196
|
+
├── schemas.ts # Zod validation schemas
|
|
151
197
|
└── main.ts # Server setup (47 lines vs 1162 before refactoring)
|
|
198
|
+
|
|
199
|
+
__tests__/
|
|
200
|
+
├── unit/ # Unit tests (no auth required)
|
|
201
|
+
│ ├── auth/ # Auth utility tests
|
|
202
|
+
│ ├── config/ # Configuration tests
|
|
203
|
+
│ └── session/ # Session management tests
|
|
204
|
+
└── integration/ # Integration tests (requires credentials)
|
|
205
|
+
└── http-transport.test.ts
|
|
152
206
|
```
|
|
153
207
|
|
|
154
208
|
**Key Features:**
|
|
@@ -157,12 +211,14 @@ src/
|
|
|
157
211
|
- **Shared Utilities**: Common patterns extracted to reduce duplication
|
|
158
212
|
- **Error Handling**: Standardized error handling across all tools
|
|
159
213
|
- **Response Optimization**: Task filtering and trimming for large datasets
|
|
214
|
+
- **Session Management**: Dual-layer caching with TTL-based lifecycle management
|
|
215
|
+
- **Test Coverage**: 251+ unit tests and comprehensive integration tests
|
|
160
216
|
|
|
161
217
|
## Authentication
|
|
162
218
|
|
|
163
219
|
**Stdio Transport:** Requires `SUNSAMA_EMAIL` and `SUNSAMA_PASSWORD` environment variables.
|
|
164
220
|
|
|
165
|
-
**HTTP Transport:**
|
|
221
|
+
**HTTP Transport:** Credentials provided via HTTP Basic Auth per request. No environment variables needed for credentials.
|
|
166
222
|
|
|
167
223
|
## Contributing
|
|
168
224
|
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import type {
|
|
3
|
+
JSONRPCResponse,
|
|
4
|
+
JSONRPCError,
|
|
5
|
+
ListToolsResult,
|
|
6
|
+
CallToolResult
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Integration Tests for HTTP Transport
|
|
11
|
+
*
|
|
12
|
+
* These tests require real Sunsama credentials and will be skipped if
|
|
13
|
+
* SUNSAMA_EMAIL and SUNSAMA_PASSWORD are not set in environment.
|
|
14
|
+
*
|
|
15
|
+
* Run these tests locally with: bun test:integration
|
|
16
|
+
* They should NOT run in CI/CD pipelines.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Server info type (not provided by MCP SDK)
|
|
20
|
+
interface ServerInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
transport: string;
|
|
23
|
+
version: string;
|
|
24
|
+
activeSessions: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SUNSAMA_EMAIL = process.env.SUNSAMA_EMAIL;
|
|
28
|
+
const SUNSAMA_PASSWORD = process.env.SUNSAMA_PASSWORD;
|
|
29
|
+
const TEST_PORT = process.env.TEST_PORT || "3099";
|
|
30
|
+
const BASE_URL = `http://localhost:${TEST_PORT}`;
|
|
31
|
+
|
|
32
|
+
const shouldRunIntegrationTests = Boolean(SUNSAMA_EMAIL && SUNSAMA_PASSWORD);
|
|
33
|
+
|
|
34
|
+
// Helper to create Basic Auth header
|
|
35
|
+
function createAuthHeader(email: string, password: string): string {
|
|
36
|
+
return `Basic ${Buffer.from(`${email}:${password}`).toString("base64")}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Type-safe response interface for tests
|
|
40
|
+
interface McpError {
|
|
41
|
+
code: number;
|
|
42
|
+
message: string;
|
|
43
|
+
data?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface McpResponse<T = any> {
|
|
47
|
+
jsonrpc: "2.0";
|
|
48
|
+
id: number | string;
|
|
49
|
+
result?: T;
|
|
50
|
+
error?: McpError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Helper to make MCP requests with properly typed responses
|
|
54
|
+
async function mcpRequest<T = any>(method: string, params: Record<string, unknown> = {}, auth?: string): Promise<McpResponse<T>> {
|
|
55
|
+
const headers: Record<string, string> = {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Accept": "application/json, text/event-stream",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (auth) {
|
|
61
|
+
headers["Authorization"] = auth;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const response = await fetch(`${BASE_URL}/mcp`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers,
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
method,
|
|
70
|
+
params,
|
|
71
|
+
id: Math.floor(Math.random() * 10000),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return response.json() as Promise<McpResponse<T>>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe.skipIf(!shouldRunIntegrationTests)("HTTP Transport Integration Tests", () => {
|
|
79
|
+
beforeAll(() => {
|
|
80
|
+
console.log("\n🔧 Integration Test Setup");
|
|
81
|
+
console.log("═".repeat(50));
|
|
82
|
+
console.log(`Test server: ${BASE_URL}`);
|
|
83
|
+
console.log(`Using credentials: ${SUNSAMA_EMAIL}`);
|
|
84
|
+
console.log("Note: Server must be running separately on port", TEST_PORT);
|
|
85
|
+
console.log("═".repeat(50) + "\n");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterAll(() => {
|
|
89
|
+
console.log("\n✅ Integration tests completed\n");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Server Health", () => {
|
|
93
|
+
test("should return server info", async () => {
|
|
94
|
+
const response = await fetch(`${BASE_URL}/`);
|
|
95
|
+
const data = await response.json() as ServerInfo;
|
|
96
|
+
|
|
97
|
+
expect(response.status).toBe(200);
|
|
98
|
+
expect(data.name).toBe("mcp-sunsama");
|
|
99
|
+
expect(data.transport).toBe("http");
|
|
100
|
+
expect(data).toHaveProperty("activeSessions");
|
|
101
|
+
expect(data).toHaveProperty("version");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("Authentication", () => {
|
|
106
|
+
test("should authenticate with valid credentials", async () => {
|
|
107
|
+
const auth = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
108
|
+
const result = await mcpRequest("tools/list", {}, auth);
|
|
109
|
+
|
|
110
|
+
expect(result.error).toBeUndefined();
|
|
111
|
+
expect(result.result).toBeDefined();
|
|
112
|
+
expect(result.result.tools).toBeArray();
|
|
113
|
+
expect(result.result.tools.length).toBeGreaterThan(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("should reject invalid credentials", async () => {
|
|
117
|
+
const auth = createAuthHeader("wrong@example.com", "wrongpassword");
|
|
118
|
+
const result = await mcpRequest("tools/list", {}, auth);
|
|
119
|
+
|
|
120
|
+
expect(result.error).toBeDefined();
|
|
121
|
+
expect(result.error!.code).toBe(-32000);
|
|
122
|
+
expect(result.error!.message).toContain("Authentication failed");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("should reject missing authorization", async () => {
|
|
126
|
+
const result = await mcpRequest("tools/list", {});
|
|
127
|
+
|
|
128
|
+
expect(result.error).toBeDefined();
|
|
129
|
+
expect(result.error!.code).toBe(-32000);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Session Caching", () => {
|
|
134
|
+
test("should cache authenticated sessions", async () => {
|
|
135
|
+
const auth = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
136
|
+
|
|
137
|
+
// First request - creates session
|
|
138
|
+
const result1 = await mcpRequest("tools/call", {
|
|
139
|
+
name: "get-user",
|
|
140
|
+
arguments: {},
|
|
141
|
+
}, auth);
|
|
142
|
+
|
|
143
|
+
expect(result1.error).toBeUndefined();
|
|
144
|
+
expect(result1.result).toBeDefined();
|
|
145
|
+
|
|
146
|
+
// Second request - should use cached session
|
|
147
|
+
const result2 = await mcpRequest("tools/call", {
|
|
148
|
+
name: "get-user",
|
|
149
|
+
arguments: {},
|
|
150
|
+
}, auth);
|
|
151
|
+
|
|
152
|
+
expect(result2.error).toBeUndefined();
|
|
153
|
+
expect(result2.result).toBeDefined();
|
|
154
|
+
|
|
155
|
+
// Both should return the same user
|
|
156
|
+
const user1 = JSON.parse(result1.result.content[0].text);
|
|
157
|
+
const user2 = JSON.parse(result2.result.content[0].text);
|
|
158
|
+
|
|
159
|
+
expect(user1._id).toBe(user2._id);
|
|
160
|
+
expect(user1.email).toBe(user2.email);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("should create separate sessions for different credentials", async () => {
|
|
164
|
+
if (!process.env.SUNSAMA_EMAIL_2 || !process.env.SUNSAMA_PASSWORD_2) {
|
|
165
|
+
console.log("⏭️ Skipping multiple user test (SUNSAMA_EMAIL_2 not set)");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const auth1 = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
170
|
+
const auth2 = createAuthHeader(
|
|
171
|
+
process.env.SUNSAMA_EMAIL_2,
|
|
172
|
+
process.env.SUNSAMA_PASSWORD_2
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result1 = await mcpRequest("tools/call", {
|
|
176
|
+
name: "get-user",
|
|
177
|
+
arguments: {},
|
|
178
|
+
}, auth1);
|
|
179
|
+
|
|
180
|
+
const result2 = await mcpRequest("tools/call", {
|
|
181
|
+
name: "get-user",
|
|
182
|
+
arguments: {},
|
|
183
|
+
}, auth2);
|
|
184
|
+
|
|
185
|
+
expect(result1.error).toBeUndefined();
|
|
186
|
+
expect(result2.error).toBeUndefined();
|
|
187
|
+
|
|
188
|
+
const user1 = JSON.parse(result1.result.content[0].text);
|
|
189
|
+
const user2 = JSON.parse(result2.result.content[0].text);
|
|
190
|
+
|
|
191
|
+
// Should be different users
|
|
192
|
+
expect(user1.email).not.toBe(user2.email);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("Tool Execution", () => {
|
|
197
|
+
const auth = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
198
|
+
|
|
199
|
+
test("should execute get-user tool", async () => {
|
|
200
|
+
const result = await mcpRequest("tools/call", {
|
|
201
|
+
name: "get-user",
|
|
202
|
+
arguments: {},
|
|
203
|
+
}, auth);
|
|
204
|
+
|
|
205
|
+
expect(result.error).toBeUndefined();
|
|
206
|
+
expect(result.result.content).toBeArray();
|
|
207
|
+
expect(result.result.content[0].type).toBe("text");
|
|
208
|
+
|
|
209
|
+
const user = JSON.parse(result.result.content[0].text);
|
|
210
|
+
expect(user).toHaveProperty("_id");
|
|
211
|
+
expect(user).toHaveProperty("email");
|
|
212
|
+
expect(user).toHaveProperty("profile");
|
|
213
|
+
expect(user.email).toBe(SUNSAMA_EMAIL);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("should execute get-streams tool", async () => {
|
|
217
|
+
const result = await mcpRequest("tools/call", {
|
|
218
|
+
name: "get-streams",
|
|
219
|
+
arguments: {},
|
|
220
|
+
}, auth);
|
|
221
|
+
|
|
222
|
+
expect(result.error).toBeUndefined();
|
|
223
|
+
expect(result.result.content).toBeArray();
|
|
224
|
+
|
|
225
|
+
const response = result.result.content[0].text;
|
|
226
|
+
// Response should be TSV format
|
|
227
|
+
expect(response).toContain("\t"); // TSV uses tabs
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("should execute get-tasks-backlog tool", async () => {
|
|
231
|
+
const result = await mcpRequest("tools/call", {
|
|
232
|
+
name: "get-tasks-backlog",
|
|
233
|
+
arguments: {},
|
|
234
|
+
}, auth);
|
|
235
|
+
|
|
236
|
+
expect(result.error).toBeUndefined();
|
|
237
|
+
expect(result.result.content).toBeArray();
|
|
238
|
+
|
|
239
|
+
const response = result.result.content[0].text;
|
|
240
|
+
// Response should be TSV format
|
|
241
|
+
expect(typeof response).toBe("string");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("should execute get-tasks-by-day tool", async () => {
|
|
245
|
+
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
246
|
+
|
|
247
|
+
const result = await mcpRequest("tools/call", {
|
|
248
|
+
name: "get-tasks-by-day",
|
|
249
|
+
arguments: {
|
|
250
|
+
day: today,
|
|
251
|
+
completionFilter: "all",
|
|
252
|
+
},
|
|
253
|
+
}, auth);
|
|
254
|
+
|
|
255
|
+
expect(result.error).toBeUndefined();
|
|
256
|
+
expect(result.result.content).toBeArray();
|
|
257
|
+
|
|
258
|
+
const response = result.result.content[0].text;
|
|
259
|
+
expect(typeof response).toBe("string");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should handle tool errors gracefully", async () => {
|
|
263
|
+
const result = await mcpRequest("tools/call", {
|
|
264
|
+
name: "get-task-by-id",
|
|
265
|
+
arguments: {
|
|
266
|
+
taskId: "non-existent-task-id-12345",
|
|
267
|
+
},
|
|
268
|
+
}, auth);
|
|
269
|
+
|
|
270
|
+
// Should return an error or empty result, not crash
|
|
271
|
+
expect(result).toBeDefined();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("Concurrent Requests", () => {
|
|
276
|
+
test("should handle multiple concurrent requests", async () => {
|
|
277
|
+
const auth = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
278
|
+
|
|
279
|
+
// Make 10 concurrent requests
|
|
280
|
+
const promises = Array.from({ length: 10 }, () =>
|
|
281
|
+
mcpRequest("tools/call", {
|
|
282
|
+
name: "get-user",
|
|
283
|
+
arguments: {},
|
|
284
|
+
}, auth)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const results = await Promise.all(promises);
|
|
288
|
+
|
|
289
|
+
// All should succeed
|
|
290
|
+
results.forEach((result) => {
|
|
291
|
+
expect(result.error).toBeUndefined();
|
|
292
|
+
expect(result.result).toBeDefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// All should return the same user
|
|
296
|
+
const users = results.map((r) => JSON.parse(r.result.content[0].text));
|
|
297
|
+
const firstUserId = users[0]._id;
|
|
298
|
+
|
|
299
|
+
users.forEach((user) => {
|
|
300
|
+
expect(user._id).toBe(firstUserId);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("should handle concurrent requests with different credentials", async () => {
|
|
305
|
+
if (!process.env.SUNSAMA_EMAIL_2 || !process.env.SUNSAMA_PASSWORD_2) {
|
|
306
|
+
console.log("⏭️ Skipping concurrent multi-user test");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const auth1 = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
311
|
+
const auth2 = createAuthHeader(
|
|
312
|
+
process.env.SUNSAMA_EMAIL_2,
|
|
313
|
+
process.env.SUNSAMA_PASSWORD_2
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// 5 requests for each user, interleaved
|
|
317
|
+
const promises = Array.from({ length: 10 }, (_, i) =>
|
|
318
|
+
mcpRequest("tools/call", {
|
|
319
|
+
name: "get-user",
|
|
320
|
+
arguments: {},
|
|
321
|
+
}, i % 2 === 0 ? auth1 : auth2)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const results = await Promise.all(promises);
|
|
325
|
+
|
|
326
|
+
// All should succeed
|
|
327
|
+
results.forEach((result) => {
|
|
328
|
+
expect(result.error).toBeUndefined();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Check that we got two different users
|
|
332
|
+
const users = results.map((r) => JSON.parse(r.result.content[0].text));
|
|
333
|
+
const uniqueEmails = new Set(users.map((u) => u.email));
|
|
334
|
+
|
|
335
|
+
expect(uniqueEmails.size).toBe(2);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("Error Recovery", () => {
|
|
340
|
+
test("should recover from transient failures", async () => {
|
|
341
|
+
const auth = createAuthHeader(SUNSAMA_EMAIL!, SUNSAMA_PASSWORD!);
|
|
342
|
+
|
|
343
|
+
// Make a valid request
|
|
344
|
+
const result1 = await mcpRequest("tools/call", {
|
|
345
|
+
name: "get-user",
|
|
346
|
+
arguments: {},
|
|
347
|
+
}, auth);
|
|
348
|
+
|
|
349
|
+
expect(result1.error).toBeUndefined();
|
|
350
|
+
|
|
351
|
+
// Make an invalid request
|
|
352
|
+
const result2 = await mcpRequest("tools/call", {
|
|
353
|
+
name: "invalid-tool-name",
|
|
354
|
+
arguments: {},
|
|
355
|
+
}, auth);
|
|
356
|
+
|
|
357
|
+
expect(result2.error).toBeDefined();
|
|
358
|
+
|
|
359
|
+
// Make another valid request - should still work
|
|
360
|
+
const result3 = await mcpRequest("tools/call", {
|
|
361
|
+
name: "get-user",
|
|
362
|
+
arguments: {},
|
|
363
|
+
}, auth);
|
|
364
|
+
|
|
365
|
+
expect(result3.error).toBeUndefined();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Print helpful message if tests are skipped
|
|
371
|
+
if (!shouldRunIntegrationTests) {
|
|
372
|
+
console.log("\n" + "⏭️".repeat(25));
|
|
373
|
+
console.log("⏭️ Integration tests SKIPPED");
|
|
374
|
+
console.log("⏭️");
|
|
375
|
+
console.log("⏭️ To run integration tests:");
|
|
376
|
+
console.log("⏭️ 1. Set SUNSAMA_EMAIL and SUNSAMA_PASSWORD in .env");
|
|
377
|
+
console.log("⏭️ 2. Start server: TRANSPORT_MODE=http PORT=3099 bun dev");
|
|
378
|
+
console.log("⏭️ 3. Run tests: bun test:integration");
|
|
379
|
+
console.log("⏭️");
|
|
380
|
+
console.log("⏭️".repeat(25) + "\n");
|
|
381
|
+
}
|