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.
Files changed (152) hide show
  1. package/.env.example +12 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +73 -5
  4. package/Dockerfile +1 -1
  5. package/README.md +65 -9
  6. package/__tests__/integration/http-transport.test.ts +381 -0
  7. package/__tests__/unit/auth/http.test.ts +283 -0
  8. package/__tests__/unit/config/session-config.test.ts +176 -0
  9. package/__tests__/unit/session/session-manager.test.ts +258 -0
  10. package/bun.lock +43 -117
  11. package/dist/__tests__/integration/http-transport.test.d.ts +2 -0
  12. package/dist/__tests__/integration/http-transport.test.d.ts.map +1 -0
  13. package/dist/__tests__/integration/http-transport.test.js +265 -0
  14. package/dist/__tests__/unit/auth/http.test.d.ts +2 -0
  15. package/dist/__tests__/unit/auth/http.test.d.ts.map +1 -0
  16. package/dist/__tests__/unit/auth/http.test.js +211 -0
  17. package/dist/__tests__/unit/config/session-config.test.d.ts +2 -0
  18. package/dist/__tests__/unit/config/session-config.test.d.ts.map +1 -0
  19. package/dist/__tests__/unit/config/session-config.test.js +134 -0
  20. package/dist/__tests__/unit/session/session-manager.test.d.ts +2 -0
  21. package/dist/__tests__/unit/session/session-manager.test.d.ts.map +1 -0
  22. package/dist/__tests__/unit/session/session-manager.test.js +192 -0
  23. package/dist/auth/http.d.ts +12 -9
  24. package/dist/auth/http.d.ts.map +1 -1
  25. package/dist/auth/http.js +141 -33
  26. package/dist/auth/types.d.ts +2 -0
  27. package/dist/auth/types.d.ts.map +1 -1
  28. package/dist/config/session-config.d.ts +49 -0
  29. package/dist/config/session-config.d.ts.map +1 -0
  30. package/dist/config/session-config.js +54 -0
  31. package/dist/config/transport.d.ts +2 -24
  32. package/dist/config/transport.d.ts.map +1 -1
  33. package/dist/config/transport.js +15 -52
  34. package/dist/context/index.d.ts +9 -0
  35. package/dist/context/index.d.ts.map +1 -0
  36. package/dist/context/index.js +5 -0
  37. package/dist/main.js +45 -48
  38. package/dist/resources/index.d.ts +5 -2
  39. package/dist/resources/index.d.ts.map +1 -1
  40. package/dist/resources/index.js +7 -4
  41. package/dist/schemas.d.ts +14 -18
  42. package/dist/schemas.d.ts.map +1 -1
  43. package/dist/schemas.js +6 -14
  44. package/dist/session/session-manager.d.ts +53 -0
  45. package/dist/session/session-manager.d.ts.map +1 -0
  46. package/dist/session/session-manager.js +141 -0
  47. package/dist/src/auth/http.d.ts +23 -0
  48. package/dist/src/auth/http.d.ts.map +1 -0
  49. package/dist/src/auth/http.js +165 -0
  50. package/dist/src/auth/stdio.d.ts +13 -0
  51. package/dist/src/auth/stdio.d.ts.map +1 -0
  52. package/dist/src/auth/stdio.js +28 -0
  53. package/dist/src/auth/types.d.ts +11 -0
  54. package/dist/src/auth/types.d.ts.map +1 -0
  55. package/dist/src/auth/types.js +1 -0
  56. package/dist/src/config/session-config.d.ts +49 -0
  57. package/dist/src/config/session-config.d.ts.map +1 -0
  58. package/dist/src/config/session-config.js +54 -0
  59. package/dist/src/config/transport.d.ts +12 -0
  60. package/dist/src/config/transport.d.ts.map +1 -0
  61. package/dist/src/config/transport.js +27 -0
  62. package/dist/src/main.d.ts +3 -0
  63. package/dist/src/main.d.ts.map +1 -0
  64. package/dist/src/main.js +58 -0
  65. package/dist/src/resources/index.d.ts +13 -0
  66. package/dist/src/resources/index.d.ts.map +1 -0
  67. package/dist/src/resources/index.js +166 -0
  68. package/dist/src/schemas.d.ts +658 -0
  69. package/dist/src/schemas.d.ts.map +1 -0
  70. package/dist/src/schemas.js +193 -0
  71. package/dist/src/schemas.test.d.ts +2 -0
  72. package/dist/src/schemas.test.d.ts.map +1 -0
  73. package/dist/src/schemas.test.js +857 -0
  74. package/dist/src/session/session-manager.d.ts +53 -0
  75. package/dist/src/session/session-manager.d.ts.map +1 -0
  76. package/dist/src/session/session-manager.js +141 -0
  77. package/dist/src/tools/index.d.ts +11 -0
  78. package/dist/src/tools/index.d.ts.map +1 -0
  79. package/dist/src/tools/index.js +12 -0
  80. package/dist/src/tools/shared.d.ts +63 -0
  81. package/dist/src/tools/shared.d.ts.map +1 -0
  82. package/dist/src/tools/shared.js +84 -0
  83. package/dist/src/tools/stream-tools.d.ts +13 -0
  84. package/dist/src/tools/stream-tools.d.ts.map +1 -0
  85. package/dist/src/tools/stream-tools.js +12 -0
  86. package/dist/src/tools/task-tools.d.ts +91 -0
  87. package/dist/src/tools/task-tools.d.ts.map +1 -0
  88. package/dist/src/tools/task-tools.js +271 -0
  89. package/dist/src/tools/user-tools.d.ts +13 -0
  90. package/dist/src/tools/user-tools.d.ts.map +1 -0
  91. package/dist/src/tools/user-tools.js +13 -0
  92. package/dist/src/transports/http.d.ts +8 -0
  93. package/dist/src/transports/http.d.ts.map +1 -0
  94. package/dist/src/transports/http.js +245 -0
  95. package/dist/src/transports/stdio.d.ts +3 -0
  96. package/dist/src/transports/stdio.d.ts.map +1 -0
  97. package/dist/src/transports/stdio.js +11 -0
  98. package/dist/src/utils/client-resolver.d.ts +8 -0
  99. package/dist/src/utils/client-resolver.d.ts.map +1 -0
  100. package/dist/src/utils/client-resolver.js +23 -0
  101. package/dist/src/utils/task-filters.d.ts +29 -0
  102. package/dist/src/utils/task-filters.d.ts.map +1 -0
  103. package/dist/src/utils/task-filters.js +42 -0
  104. package/dist/src/utils/task-trimmer.d.ts +50 -0
  105. package/dist/src/utils/task-trimmer.d.ts.map +1 -0
  106. package/dist/src/utils/task-trimmer.js +59 -0
  107. package/dist/src/utils/to-tsv.d.ts +8 -0
  108. package/dist/src/utils/to-tsv.d.ts.map +1 -0
  109. package/dist/src/utils/to-tsv.js +64 -0
  110. package/dist/tools/index.d.ts +3 -104
  111. package/dist/tools/index.d.ts.map +1 -1
  112. package/dist/tools/index.js +1 -1
  113. package/dist/tools/shared.d.ts +37 -16
  114. package/dist/tools/shared.d.ts.map +1 -1
  115. package/dist/tools/shared.js +42 -24
  116. package/dist/tools/stream-tools.d.ts +2 -3
  117. package/dist/tools/stream-tools.d.ts.map +1 -1
  118. package/dist/tools/stream-tools.js +4 -8
  119. package/dist/tools/task-tools.d.ts +17 -176
  120. package/dist/tools/task-tools.d.ts.map +1 -1
  121. package/dist/tools/task-tools.js +72 -223
  122. package/dist/tools/user-tools.d.ts +2 -3
  123. package/dist/tools/user-tools.d.ts.map +1 -1
  124. package/dist/tools/user-tools.js +5 -8
  125. package/dist/transports/http.d.ts +8 -0
  126. package/dist/transports/http.d.ts.map +1 -0
  127. package/dist/transports/http.js +245 -0
  128. package/dist/transports/stdio.d.ts +3 -0
  129. package/dist/transports/stdio.d.ts.map +1 -0
  130. package/dist/transports/stdio.js +11 -0
  131. package/dist/utils/client-resolver.d.ts +4 -6
  132. package/dist/utils/client-resolver.d.ts.map +1 -1
  133. package/dist/utils/client-resolver.js +15 -11
  134. package/package.json +13 -6
  135. package/scripts/kill-inspector.sh +22 -0
  136. package/src/auth/http.ts +170 -37
  137. package/src/auth/types.ts +2 -0
  138. package/src/config/session-config.ts +63 -0
  139. package/src/config/transport.ts +24 -67
  140. package/src/main.ts +53 -51
  141. package/src/resources/index.ts +11 -7
  142. package/src/schemas.ts +138 -60
  143. package/src/session/session-manager.ts +177 -0
  144. package/src/tools/index.ts +2 -2
  145. package/src/tools/shared.ts +67 -42
  146. package/src/tools/stream-tools.ts +8 -13
  147. package/src/tools/task-tools.ts +183 -290
  148. package/src/tools/user-tools.ts +9 -13
  149. package/src/transports/http.ts +289 -0
  150. package/src/transports/stdio.ts +14 -0
  151. package/src/utils/client-resolver.ts +16 -12
  152. package/dev/prd-update-task-stream.md +0 -112
package/.env.example CHANGED
@@ -1,10 +1,15 @@
1
1
  # MCP Server Configuration
2
- PORT=3002
2
+ PORT=8080
3
3
 
4
- # Sunsama Authentication
5
- # Option 1: Use session token (recommended for server environments)
6
- SUNSAMA_SESSION_TOKEN=your-session-token-here
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
- # Option 2: Use email/password (for development/testing)
9
- # SUNSAMA_EMAIL=your-email@example.com
10
- # SUNSAMA_PASSWORD=your-password
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 test # Run test suite
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 `TRANSPORT_TYPE` environment variable ("stdio" | "httpStream").
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
- ├── utils/ # Reusable utilities (client resolution, filtering, formatting)
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
- - `TRANSPORT_TYPE`: "stdio" (default) | "httpStream"
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
@@ -25,7 +25,7 @@ COPY package.json ./
25
25
  RUN bun install --production --frozen-lockfile
26
26
 
27
27
  # Set environment defaults for containerized deployment
28
- ENV TRANSPORT_TYPE=httpStream
28
+ ENV TRANSPORT_MODE=http
29
29
  ENV PORT=3000
30
30
 
31
31
  # Expose HTTP port
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
- - `SUNSAMA_SESSION_TOKEN` - Alternative session token authentication (optional)
52
- - `PORT` - Server port for HTTP transport (default: 3002)
53
- - `MCP_TRANSPORT` - Transport type: `stdio` or `httpStream` (default: stdio)
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
- ### Running the Server
60
+ ### Transport Modes
58
61
 
59
- **Stdio Transport (default):**
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 Stream Transport:**
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
- MCP_TRANSPORT=httpStream PORT=3002 bun run src/main.ts
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:** The Sunsama credentials are passed in the HTTP request. No environment variables needed.
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
+ }