@goonnguyen/human-mcp 1.0.2 → 1.2.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/.dockerignore +81 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -1
- package/DEPLOYMENT.md +329 -0
- package/Dockerfile +36 -12
- package/README.md +388 -14
- package/bun.lock +49 -4
- package/dist/index.js +27887 -1473
- package/docker-compose.yaml +128 -0
- package/package.json +11 -4
- package/plans/001-streamable-http-transport-plan.md +905 -0
- package/src/index.ts +44 -2
- package/src/transports/http/middleware.ts +46 -0
- package/src/transports/http/routes.ts +136 -0
- package/src/transports/http/server.ts +66 -0
- package/src/transports/http/session.ts +85 -0
- package/src/transports/index.ts +31 -0
- package/src/transports/stdio.ts +7 -0
- package/src/transports/types.ts +37 -0
- package/src/utils/config.ts +46 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
# Implementation Plan: Adding Streamable HTTP Transport to Human MCP Server
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document outlines the comprehensive plan for adding Streamable HTTP transport support to the Human MCP server while maintaining backward compatibility with the existing stdio transport. The implementation will follow MCP specification version 2025-03-26 for Streamable HTTP transport.
|
|
6
|
+
|
|
7
|
+
## Current State Analysis
|
|
8
|
+
|
|
9
|
+
### Existing Architecture
|
|
10
|
+
- **Transport**: Currently only supports stdio transport via `StdioServerTransport`
|
|
11
|
+
- **Entry Point**: `src/index.ts` directly calls `startStdioServer()`
|
|
12
|
+
- **Server Creation**: `src/server.ts` contains `createServer()` and `startStdioServer()` functions
|
|
13
|
+
- **Configuration**: Environment-based config via `src/utils/config.ts`
|
|
14
|
+
- **Tools**: Two vision analysis tools (`eyes.analyze` and `eyes.compare`)
|
|
15
|
+
- **Dependencies**: Uses `@modelcontextprotocol/sdk` version 1.4.0
|
|
16
|
+
|
|
17
|
+
### Key Findings
|
|
18
|
+
- No Express or HTTP server infrastructure exists
|
|
19
|
+
- Configuration already supports server settings (port, timeouts, security)
|
|
20
|
+
- Clean separation between server creation and transport initialization
|
|
21
|
+
- TypeScript with ESNext modules and Bun runtime
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
### Functional Requirements
|
|
26
|
+
1. **Dual Transport Support**: Support both stdio and Streamable HTTP transports
|
|
27
|
+
2. **Session Management**: Implement stateful session handling with resumability
|
|
28
|
+
3. **SSE Support**: Enable Server-Sent Events for notifications
|
|
29
|
+
4. **Backward Compatibility**: Maintain existing stdio functionality
|
|
30
|
+
5. **Stateless Mode**: Support stateless operation for serverless deployments
|
|
31
|
+
6. **Security**: Implement CORS, DNS rebinding protection, and optional authentication
|
|
32
|
+
|
|
33
|
+
### Non-Functional Requirements
|
|
34
|
+
1. **Performance**: Handle concurrent sessions efficiently
|
|
35
|
+
2. **Scalability**: Support horizontal scaling with external session storage
|
|
36
|
+
3. **Maintainability**: Clean code architecture with separation of concerns
|
|
37
|
+
4. **Testing**: Comprehensive test coverage for all transport modes
|
|
38
|
+
5. **Documentation**: Clear documentation for configuration and usage
|
|
39
|
+
|
|
40
|
+
## Architecture Design
|
|
41
|
+
|
|
42
|
+
### High-Level Architecture
|
|
43
|
+
|
|
44
|
+
```mermaid
|
|
45
|
+
graph TB
|
|
46
|
+
subgraph "Client Layer"
|
|
47
|
+
C1[Stdio Client]
|
|
48
|
+
C2[HTTP Client]
|
|
49
|
+
C3[Legacy SSE Client]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph "Transport Layer"
|
|
53
|
+
T1[Transport Manager]
|
|
54
|
+
T2[Stdio Transport]
|
|
55
|
+
T3[Streamable HTTP Transport]
|
|
56
|
+
T4[SSE Fallback]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
subgraph "Server Layer"
|
|
60
|
+
S1[MCP Server Core]
|
|
61
|
+
S2[Session Manager]
|
|
62
|
+
S3[Event Store]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
subgraph "Application Layer"
|
|
66
|
+
A1[Eyes Tools]
|
|
67
|
+
A2[Prompts]
|
|
68
|
+
A3[Resources]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
C1 --> T2
|
|
72
|
+
C2 --> T3
|
|
73
|
+
C3 --> T4
|
|
74
|
+
T1 --> S1
|
|
75
|
+
T2 --> T1
|
|
76
|
+
T3 --> T1
|
|
77
|
+
T4 --> T1
|
|
78
|
+
S1 --> S2
|
|
79
|
+
S2 --> S3
|
|
80
|
+
S1 --> A1
|
|
81
|
+
S1 --> A2
|
|
82
|
+
S1 --> A3
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Component Design
|
|
86
|
+
|
|
87
|
+
#### 1. Transport Manager
|
|
88
|
+
- Handles transport selection based on startup mode
|
|
89
|
+
- Manages transport lifecycle
|
|
90
|
+
- Provides unified interface for different transports
|
|
91
|
+
|
|
92
|
+
#### 2. HTTP Server Module
|
|
93
|
+
- Express-based HTTP server
|
|
94
|
+
- Route handlers for MCP endpoints
|
|
95
|
+
- Middleware for security and logging
|
|
96
|
+
|
|
97
|
+
#### 3. Session Manager
|
|
98
|
+
- In-memory session storage (default)
|
|
99
|
+
- Interface for external storage adapters
|
|
100
|
+
- Session lifecycle management
|
|
101
|
+
|
|
102
|
+
#### 4. Security Module
|
|
103
|
+
- CORS configuration
|
|
104
|
+
- DNS rebinding protection
|
|
105
|
+
- Rate limiting
|
|
106
|
+
- Optional authentication
|
|
107
|
+
|
|
108
|
+
## Implementation Approaches
|
|
109
|
+
|
|
110
|
+
### Approach 1: Modular Transport System (Recommended)
|
|
111
|
+
|
|
112
|
+
**Description**: Create a modular transport system with pluggable transports and a unified startup mechanism.
|
|
113
|
+
|
|
114
|
+
**Pros**:
|
|
115
|
+
- Clean separation of concerns
|
|
116
|
+
- Easy to add new transports in the future
|
|
117
|
+
- Testable components
|
|
118
|
+
- Supports dynamic transport selection
|
|
119
|
+
- Better code organization
|
|
120
|
+
|
|
121
|
+
**Cons**:
|
|
122
|
+
- More initial setup complexity
|
|
123
|
+
- Requires refactoring existing code structure
|
|
124
|
+
- More files to manage
|
|
125
|
+
|
|
126
|
+
**Implementation Structure**:
|
|
127
|
+
```
|
|
128
|
+
src/
|
|
129
|
+
├── transports/
|
|
130
|
+
│ ├── index.ts # Transport manager
|
|
131
|
+
│ ├── stdio.ts # Stdio transport wrapper
|
|
132
|
+
│ ├── http/
|
|
133
|
+
│ │ ├── server.ts # Express server setup
|
|
134
|
+
│ │ ├── routes.ts # Route handlers
|
|
135
|
+
│ │ ├── middleware.ts # Security & logging
|
|
136
|
+
│ │ └── session.ts # Session management
|
|
137
|
+
│ └── types.ts # Transport interfaces
|
|
138
|
+
├── server.ts # Refactored server creation
|
|
139
|
+
└── index.ts # Unified entry point
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Approach 2: Minimal Integration
|
|
143
|
+
|
|
144
|
+
**Description**: Add HTTP support directly in existing files with minimal structural changes.
|
|
145
|
+
|
|
146
|
+
**Pros**:
|
|
147
|
+
- Minimal changes to existing code
|
|
148
|
+
- Faster initial implementation
|
|
149
|
+
- Less file reorganization
|
|
150
|
+
|
|
151
|
+
**Cons**:
|
|
152
|
+
- Less maintainable long-term
|
|
153
|
+
- Harder to test components independently
|
|
154
|
+
- Mixed concerns in single files
|
|
155
|
+
- Limited extensibility
|
|
156
|
+
|
|
157
|
+
**Implementation Structure**:
|
|
158
|
+
```
|
|
159
|
+
src/
|
|
160
|
+
├── server.ts # Add HTTP functions here
|
|
161
|
+
├── http.ts # All HTTP-related code
|
|
162
|
+
└── index.ts # Modified entry point
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Detailed Implementation Plan (Approach 1 - Recommended)
|
|
166
|
+
|
|
167
|
+
### Phase 1: Foundation (Week 1)
|
|
168
|
+
|
|
169
|
+
#### 1.1 Install Dependencies
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"dependencies": {
|
|
173
|
+
"express": "^4.21.0",
|
|
174
|
+
"cors": "^2.8.5",
|
|
175
|
+
"compression": "^1.7.4",
|
|
176
|
+
"helmet": "^7.1.0"
|
|
177
|
+
},
|
|
178
|
+
"devDependencies": {
|
|
179
|
+
"@types/express": "^4.17.21",
|
|
180
|
+
"@types/cors": "^2.8.17",
|
|
181
|
+
"@types/compression": "^1.7.5"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### 1.2 Create Transport Interfaces
|
|
187
|
+
**File**: `src/transports/types.ts`
|
|
188
|
+
```typescript
|
|
189
|
+
export interface TransportConfig {
|
|
190
|
+
type: 'stdio' | 'http' | 'both';
|
|
191
|
+
http?: HttpTransportConfig;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface HttpTransportConfig {
|
|
195
|
+
port: number;
|
|
196
|
+
host?: string;
|
|
197
|
+
sessionMode: 'stateful' | 'stateless';
|
|
198
|
+
enableSse?: boolean;
|
|
199
|
+
enableJsonResponse?: boolean;
|
|
200
|
+
security?: SecurityConfig;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface SecurityConfig {
|
|
204
|
+
enableCors?: boolean;
|
|
205
|
+
corsOrigins?: string[];
|
|
206
|
+
enableDnsRebindingProtection?: boolean;
|
|
207
|
+
allowedHosts?: string[];
|
|
208
|
+
enableRateLimiting?: boolean;
|
|
209
|
+
secret?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface SessionStore {
|
|
213
|
+
get(sessionId: string): Promise<TransportSession | null>;
|
|
214
|
+
set(sessionId: string, session: TransportSession): Promise<void>;
|
|
215
|
+
delete(sessionId: string): Promise<void>;
|
|
216
|
+
cleanup(): Promise<void>;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### 1.3 Create Transport Manager
|
|
221
|
+
**File**: `src/transports/index.ts`
|
|
222
|
+
```typescript
|
|
223
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
224
|
+
import { startStdioTransport } from "./stdio.js";
|
|
225
|
+
import { startHttpTransport } from "./http/server.js";
|
|
226
|
+
import type { TransportConfig } from "./types.js";
|
|
227
|
+
|
|
228
|
+
export class TransportManager {
|
|
229
|
+
private server: McpServer;
|
|
230
|
+
private config: TransportConfig;
|
|
231
|
+
|
|
232
|
+
constructor(server: McpServer, config: TransportConfig) {
|
|
233
|
+
this.server = server;
|
|
234
|
+
this.config = config;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async start(): Promise<void> {
|
|
238
|
+
switch (this.config.type) {
|
|
239
|
+
case 'stdio':
|
|
240
|
+
await startStdioTransport(this.server);
|
|
241
|
+
break;
|
|
242
|
+
case 'http':
|
|
243
|
+
await startHttpTransport(this.server, this.config.http!);
|
|
244
|
+
break;
|
|
245
|
+
case 'both':
|
|
246
|
+
await Promise.all([
|
|
247
|
+
startStdioTransport(this.server),
|
|
248
|
+
startHttpTransport(this.server, this.config.http!)
|
|
249
|
+
]);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Phase 2: HTTP Server Implementation (Week 1-2)
|
|
257
|
+
|
|
258
|
+
#### 2.1 Express Server Setup
|
|
259
|
+
**File**: `src/transports/http/server.ts`
|
|
260
|
+
```typescript
|
|
261
|
+
import express from "express";
|
|
262
|
+
import cors from "cors";
|
|
263
|
+
import compression from "compression";
|
|
264
|
+
import helmet from "helmet";
|
|
265
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
266
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
267
|
+
import { createRoutes } from "./routes.js";
|
|
268
|
+
import { SessionManager } from "./session.js";
|
|
269
|
+
import { createSecurityMiddleware } from "./middleware.js";
|
|
270
|
+
import type { HttpTransportConfig } from "../types.js";
|
|
271
|
+
|
|
272
|
+
export async function startHttpTransport(
|
|
273
|
+
mcpServer: McpServer,
|
|
274
|
+
config: HttpTransportConfig
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
const app = express();
|
|
277
|
+
const sessionManager = new SessionManager(config.sessionMode);
|
|
278
|
+
|
|
279
|
+
// Apply middleware
|
|
280
|
+
app.use(express.json({ limit: '50mb' }));
|
|
281
|
+
app.use(compression());
|
|
282
|
+
app.use(helmet());
|
|
283
|
+
|
|
284
|
+
if (config.security?.enableCors) {
|
|
285
|
+
app.use(cors({
|
|
286
|
+
origin: config.security.corsOrigins || '*',
|
|
287
|
+
exposedHeaders: ['Mcp-Session-Id'],
|
|
288
|
+
allowedHeaders: ['Content-Type', 'mcp-session-id'],
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
app.use(createSecurityMiddleware(config.security));
|
|
293
|
+
|
|
294
|
+
// Create routes
|
|
295
|
+
const routes = createRoutes(mcpServer, sessionManager, config);
|
|
296
|
+
app.use('/mcp', routes);
|
|
297
|
+
|
|
298
|
+
// Health check endpoint
|
|
299
|
+
app.get('/health', (req, res) => {
|
|
300
|
+
res.json({ status: 'healthy', transport: 'streamable-http' });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Start server
|
|
304
|
+
const port = config.port || 3000;
|
|
305
|
+
const host = config.host || '0.0.0.0';
|
|
306
|
+
|
|
307
|
+
app.listen(port, host, () => {
|
|
308
|
+
console.log(`MCP HTTP Server listening on http://${host}:${port}`);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### 2.2 Route Handlers
|
|
314
|
+
**File**: `src/transports/http/routes.ts`
|
|
315
|
+
```typescript
|
|
316
|
+
import { Router } from "express";
|
|
317
|
+
import { randomUUID } from "node:crypto";
|
|
318
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
319
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
320
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
321
|
+
import { SessionManager } from "./session.js";
|
|
322
|
+
import type { HttpTransportConfig } from "../types.js";
|
|
323
|
+
|
|
324
|
+
export function createRoutes(
|
|
325
|
+
mcpServer: McpServer,
|
|
326
|
+
sessionManager: SessionManager,
|
|
327
|
+
config: HttpTransportConfig
|
|
328
|
+
): Router {
|
|
329
|
+
const router = Router();
|
|
330
|
+
|
|
331
|
+
// POST /mcp - Handle client requests
|
|
332
|
+
router.post('/', async (req, res) => {
|
|
333
|
+
try {
|
|
334
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
335
|
+
|
|
336
|
+
if (config.sessionMode === 'stateless') {
|
|
337
|
+
await handleStatelessRequest(mcpServer, req, res);
|
|
338
|
+
} else {
|
|
339
|
+
await handleStatefulRequest(mcpServer, sessionManager, sessionId, req, res);
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
handleError(res, error);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// GET /mcp - SSE endpoint for notifications
|
|
347
|
+
router.get('/', async (req, res) => {
|
|
348
|
+
if (config.sessionMode === 'stateless') {
|
|
349
|
+
res.status(405).json({
|
|
350
|
+
jsonrpc: "2.0",
|
|
351
|
+
error: {
|
|
352
|
+
code: -32000,
|
|
353
|
+
message: "SSE not supported in stateless mode"
|
|
354
|
+
},
|
|
355
|
+
id: null
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const sessionId = req.headers['mcp-session-id'] as string;
|
|
361
|
+
const transport = await sessionManager.getTransport(sessionId);
|
|
362
|
+
|
|
363
|
+
if (!transport) {
|
|
364
|
+
res.status(400).send('Invalid or missing session ID');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await transport.handleRequest(req, res);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// DELETE /mcp - Session termination
|
|
372
|
+
router.delete('/', async (req, res) => {
|
|
373
|
+
if (config.sessionMode === 'stateless') {
|
|
374
|
+
res.status(405).json({
|
|
375
|
+
jsonrpc: "2.0",
|
|
376
|
+
error: {
|
|
377
|
+
code: -32000,
|
|
378
|
+
message: "Session termination not applicable in stateless mode"
|
|
379
|
+
},
|
|
380
|
+
id: null
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const sessionId = req.headers['mcp-session-id'] as string;
|
|
386
|
+
await sessionManager.terminateSession(sessionId);
|
|
387
|
+
res.status(204).send();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return router;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function handleStatelessRequest(
|
|
394
|
+
mcpServer: McpServer,
|
|
395
|
+
req: any,
|
|
396
|
+
res: any
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
const transport = new StreamableHTTPServerTransport({
|
|
399
|
+
sessionIdGenerator: undefined,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
res.on('close', () => {
|
|
403
|
+
transport.close();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await mcpServer.connect(transport);
|
|
407
|
+
await transport.handleRequest(req, res, req.body);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function handleStatefulRequest(
|
|
411
|
+
mcpServer: McpServer,
|
|
412
|
+
sessionManager: SessionManager,
|
|
413
|
+
sessionId: string | undefined,
|
|
414
|
+
req: any,
|
|
415
|
+
res: any
|
|
416
|
+
): Promise<void> {
|
|
417
|
+
let transport = sessionId ?
|
|
418
|
+
await sessionManager.getTransport(sessionId) : null;
|
|
419
|
+
|
|
420
|
+
if (!transport && isInitializeRequest(req.body)) {
|
|
421
|
+
transport = await sessionManager.createSession(mcpServer);
|
|
422
|
+
res.setHeader('Mcp-Session-Id', transport.sessionId);
|
|
423
|
+
} else if (!transport) {
|
|
424
|
+
res.status(400).json({
|
|
425
|
+
jsonrpc: '2.0',
|
|
426
|
+
error: {
|
|
427
|
+
code: -32000,
|
|
428
|
+
message: 'Bad Request: No valid session ID provided',
|
|
429
|
+
},
|
|
430
|
+
id: null,
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await transport.handleRequest(req, res, req.body);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function handleError(res: any, error: any): void {
|
|
439
|
+
console.error('MCP request error:', error);
|
|
440
|
+
if (!res.headersSent) {
|
|
441
|
+
res.status(500).json({
|
|
442
|
+
jsonrpc: '2.0',
|
|
443
|
+
error: {
|
|
444
|
+
code: -32603,
|
|
445
|
+
message: 'Internal server error',
|
|
446
|
+
},
|
|
447
|
+
id: null,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
#### 2.3 Session Management
|
|
454
|
+
**File**: `src/transports/http/session.ts`
|
|
455
|
+
```typescript
|
|
456
|
+
import { randomUUID } from "node:crypto";
|
|
457
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
458
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
459
|
+
import type { SessionStore } from "../types.js";
|
|
460
|
+
|
|
461
|
+
export class SessionManager {
|
|
462
|
+
private transports: Map<string, StreamableHTTPServerTransport>;
|
|
463
|
+
private sessionMode: 'stateful' | 'stateless';
|
|
464
|
+
private store?: SessionStore;
|
|
465
|
+
|
|
466
|
+
constructor(sessionMode: 'stateful' | 'stateless', store?: SessionStore) {
|
|
467
|
+
this.transports = new Map();
|
|
468
|
+
this.sessionMode = sessionMode;
|
|
469
|
+
this.store = store;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async createSession(mcpServer: McpServer): Promise<StreamableHTTPServerTransport> {
|
|
473
|
+
const sessionId = randomUUID();
|
|
474
|
+
|
|
475
|
+
const transport = new StreamableHTTPServerTransport({
|
|
476
|
+
sessionIdGenerator: () => sessionId,
|
|
477
|
+
enableJsonResponse: true,
|
|
478
|
+
enableDnsRebindingProtection: true,
|
|
479
|
+
allowedHosts: ['127.0.0.1', 'localhost'],
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
transport.onclose = () => {
|
|
483
|
+
this.terminateSession(sessionId);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
this.transports.set(sessionId, transport);
|
|
487
|
+
|
|
488
|
+
if (this.store) {
|
|
489
|
+
await this.store.set(sessionId, {
|
|
490
|
+
id: sessionId,
|
|
491
|
+
createdAt: Date.now(),
|
|
492
|
+
transport: transport
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await mcpServer.connect(transport);
|
|
497
|
+
return transport;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async getTransport(sessionId: string): Promise<StreamableHTTPServerTransport | null> {
|
|
501
|
+
let transport = this.transports.get(sessionId);
|
|
502
|
+
|
|
503
|
+
if (!transport && this.store) {
|
|
504
|
+
const session = await this.store.get(sessionId);
|
|
505
|
+
if (session) {
|
|
506
|
+
transport = session.transport;
|
|
507
|
+
this.transports.set(sessionId, transport);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return transport || null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async terminateSession(sessionId: string): Promise<void> {
|
|
515
|
+
const transport = this.transports.get(sessionId);
|
|
516
|
+
if (transport) {
|
|
517
|
+
transport.close();
|
|
518
|
+
this.transports.delete(sessionId);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (this.store) {
|
|
522
|
+
await this.store.delete(sessionId);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async cleanup(): Promise<void> {
|
|
527
|
+
for (const [sessionId, transport] of this.transports) {
|
|
528
|
+
transport.close();
|
|
529
|
+
}
|
|
530
|
+
this.transports.clear();
|
|
531
|
+
|
|
532
|
+
if (this.store) {
|
|
533
|
+
await this.store.cleanup();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Phase 3: Configuration & Integration (Week 2)
|
|
540
|
+
|
|
541
|
+
#### 3.1 Update Configuration
|
|
542
|
+
**File**: `src/utils/config.ts` (additions)
|
|
543
|
+
```typescript
|
|
544
|
+
transport: z.object({
|
|
545
|
+
type: z.enum(["stdio", "http", "both"]).default("stdio"),
|
|
546
|
+
http: z.object({
|
|
547
|
+
enabled: z.boolean().default(false),
|
|
548
|
+
port: z.number().default(3000),
|
|
549
|
+
host: z.string().default("0.0.0.0"),
|
|
550
|
+
sessionMode: z.enum(["stateful", "stateless"]).default("stateful"),
|
|
551
|
+
enableSse: z.boolean().default(true),
|
|
552
|
+
enableJsonResponse: z.boolean().default(true),
|
|
553
|
+
cors: z.object({
|
|
554
|
+
enabled: z.boolean().default(true),
|
|
555
|
+
origins: z.array(z.string()).optional(),
|
|
556
|
+
}).optional(),
|
|
557
|
+
dnsRebinding: z.object({
|
|
558
|
+
enabled: z.boolean().default(true),
|
|
559
|
+
allowedHosts: z.array(z.string()).default(["127.0.0.1", "localhost"]),
|
|
560
|
+
}).optional(),
|
|
561
|
+
}).optional(),
|
|
562
|
+
}),
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
#### 3.2 Update Entry Point
|
|
566
|
+
**File**: `src/index.ts`
|
|
567
|
+
```typescript
|
|
568
|
+
#!/usr/bin/env bun
|
|
569
|
+
|
|
570
|
+
import { createServer } from "./server.js";
|
|
571
|
+
import { TransportManager } from "./transports/index.js";
|
|
572
|
+
import { loadConfig } from "./utils/config.js";
|
|
573
|
+
import { logger } from "./utils/logger.js";
|
|
574
|
+
|
|
575
|
+
async function main() {
|
|
576
|
+
try {
|
|
577
|
+
const config = loadConfig();
|
|
578
|
+
const server = await createServer();
|
|
579
|
+
|
|
580
|
+
const transportConfig = {
|
|
581
|
+
type: config.transport.type,
|
|
582
|
+
http: config.transport.http
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const transportManager = new TransportManager(server, transportConfig);
|
|
586
|
+
await transportManager.start();
|
|
587
|
+
|
|
588
|
+
logger.info(`Human MCP Server started with ${config.transport.type} transport`);
|
|
589
|
+
|
|
590
|
+
// Graceful shutdown
|
|
591
|
+
process.on('SIGINT', async () => {
|
|
592
|
+
logger.info('Shutting down server...');
|
|
593
|
+
process.exit(0);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
} catch (error) {
|
|
597
|
+
logger.error('Failed to start server:', error);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
main();
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
#### 3.3 Update Environment Variables
|
|
606
|
+
**File**: `.env.example` (additions)
|
|
607
|
+
```bash
|
|
608
|
+
# Transport Configuration
|
|
609
|
+
TRANSPORT_TYPE=http # stdio, http, or both
|
|
610
|
+
HTTP_PORT=3000
|
|
611
|
+
HTTP_HOST=0.0.0.0
|
|
612
|
+
HTTP_SESSION_MODE=stateful # stateful or stateless
|
|
613
|
+
HTTP_ENABLE_SSE=true
|
|
614
|
+
HTTP_ENABLE_JSON_RESPONSE=true
|
|
615
|
+
|
|
616
|
+
# CORS Configuration
|
|
617
|
+
HTTP_CORS_ENABLED=true
|
|
618
|
+
HTTP_CORS_ORIGINS=http://localhost:3000,https://app.example.com
|
|
619
|
+
|
|
620
|
+
# DNS Rebinding Protection
|
|
621
|
+
HTTP_DNS_REBINDING_ENABLED=true
|
|
622
|
+
HTTP_ALLOWED_HOSTS=127.0.0.1,localhost
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Phase 4: Security & Middleware (Week 2-3)
|
|
626
|
+
|
|
627
|
+
#### 4.1 Security Middleware
|
|
628
|
+
**File**: `src/transports/http/middleware.ts`
|
|
629
|
+
```typescript
|
|
630
|
+
import { Request, Response, NextFunction } from "express";
|
|
631
|
+
import type { SecurityConfig } from "../types.js";
|
|
632
|
+
|
|
633
|
+
export function createSecurityMiddleware(config?: SecurityConfig) {
|
|
634
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
635
|
+
// DNS Rebinding Protection
|
|
636
|
+
if (config?.enableDnsRebindingProtection) {
|
|
637
|
+
const host = req.headers.host?.split(':')[0];
|
|
638
|
+
const allowedHosts = config.allowedHosts || ['127.0.0.1', 'localhost'];
|
|
639
|
+
|
|
640
|
+
if (host && !allowedHosts.includes(host)) {
|
|
641
|
+
res.status(403).json({
|
|
642
|
+
error: 'Forbidden: Invalid host'
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Rate Limiting (basic implementation)
|
|
649
|
+
if (config?.enableRateLimiting) {
|
|
650
|
+
// Implement rate limiting logic here
|
|
651
|
+
// Could use express-rate-limit package
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Secret-based authentication (optional)
|
|
655
|
+
if (config?.secret) {
|
|
656
|
+
const authHeader = req.headers.authorization;
|
|
657
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
658
|
+
res.status(401).json({
|
|
659
|
+
error: 'Unauthorized: Missing authentication'
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const token = authHeader.substring(7);
|
|
665
|
+
if (token !== config.secret) {
|
|
666
|
+
res.status(401).json({
|
|
667
|
+
error: 'Unauthorized: Invalid token'
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
next();
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Phase 5: Testing Strategy (Week 3)
|
|
679
|
+
|
|
680
|
+
#### 5.1 Unit Tests
|
|
681
|
+
```typescript
|
|
682
|
+
// tests/transports/session.test.ts
|
|
683
|
+
import { describe, it, expect } from "bun:test";
|
|
684
|
+
import { SessionManager } from "@/transports/http/session";
|
|
685
|
+
|
|
686
|
+
describe("SessionManager", () => {
|
|
687
|
+
it("should create and retrieve sessions", async () => {
|
|
688
|
+
const manager = new SessionManager('stateful');
|
|
689
|
+
const transport = await manager.createSession(mockServer);
|
|
690
|
+
expect(transport.sessionId).toBeDefined();
|
|
691
|
+
|
|
692
|
+
const retrieved = await manager.getTransport(transport.sessionId);
|
|
693
|
+
expect(retrieved).toBe(transport);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("should terminate sessions", async () => {
|
|
697
|
+
const manager = new SessionManager('stateful');
|
|
698
|
+
const transport = await manager.createSession(mockServer);
|
|
699
|
+
await manager.terminateSession(transport.sessionId);
|
|
700
|
+
|
|
701
|
+
const retrieved = await manager.getTransport(transport.sessionId);
|
|
702
|
+
expect(retrieved).toBeNull();
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
#### 5.2 Integration Tests
|
|
708
|
+
```typescript
|
|
709
|
+
// tests/integration/http-transport.test.ts
|
|
710
|
+
import { describe, it, expect } from "bun:test";
|
|
711
|
+
import request from "supertest";
|
|
712
|
+
|
|
713
|
+
describe("HTTP Transport", () => {
|
|
714
|
+
it("should handle initialize request", async () => {
|
|
715
|
+
const response = await request(app)
|
|
716
|
+
.post('/mcp')
|
|
717
|
+
.send({
|
|
718
|
+
jsonrpc: "2.0",
|
|
719
|
+
method: "initialize",
|
|
720
|
+
params: {
|
|
721
|
+
protocolVersion: "2025-03-26",
|
|
722
|
+
capabilities: {}
|
|
723
|
+
},
|
|
724
|
+
id: 1
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
expect(response.status).toBe(200);
|
|
728
|
+
expect(response.headers['mcp-session-id']).toBeDefined();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("should handle tool calls", async () => {
|
|
732
|
+
// Initialize session first
|
|
733
|
+
const initResponse = await request(app)
|
|
734
|
+
.post('/mcp')
|
|
735
|
+
.send(initializeRequest);
|
|
736
|
+
|
|
737
|
+
const sessionId = initResponse.headers['mcp-session-id'];
|
|
738
|
+
|
|
739
|
+
// Call tool
|
|
740
|
+
const toolResponse = await request(app)
|
|
741
|
+
.post('/mcp')
|
|
742
|
+
.set('mcp-session-id', sessionId)
|
|
743
|
+
.send({
|
|
744
|
+
jsonrpc: "2.0",
|
|
745
|
+
method: "tools/call",
|
|
746
|
+
params: {
|
|
747
|
+
name: "eyes.analyze",
|
|
748
|
+
arguments: {
|
|
749
|
+
source: "test.jpg",
|
|
750
|
+
type: "image"
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
id: 2
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
expect(toolResponse.status).toBe(200);
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Phase 6: Documentation & Deployment (Week 3-4)
|
|
762
|
+
|
|
763
|
+
#### 6.1 Update README.md
|
|
764
|
+
```markdown
|
|
765
|
+
## Transport Options
|
|
766
|
+
|
|
767
|
+
Human MCP supports multiple transport mechanisms:
|
|
768
|
+
|
|
769
|
+
### Stdio Transport (Default)
|
|
770
|
+
```bash
|
|
771
|
+
bun run start
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### HTTP Transport
|
|
775
|
+
```bash
|
|
776
|
+
TRANSPORT_TYPE=http bun run start
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Both Transports
|
|
780
|
+
```bash
|
|
781
|
+
TRANSPORT_TYPE=both bun run start
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
## HTTP API Endpoints
|
|
785
|
+
|
|
786
|
+
- `POST /mcp` - Handle client requests
|
|
787
|
+
- `GET /mcp` - SSE endpoint for notifications (stateful mode only)
|
|
788
|
+
- `DELETE /mcp` - Terminate session (stateful mode only)
|
|
789
|
+
- `GET /health` - Health check endpoint
|
|
790
|
+
|
|
791
|
+
## Session Modes
|
|
792
|
+
|
|
793
|
+
### Stateful Mode (Default)
|
|
794
|
+
- Maintains session state between requests
|
|
795
|
+
- Supports SSE notifications
|
|
796
|
+
- Enables session resumability
|
|
797
|
+
- Requires session ID management
|
|
798
|
+
|
|
799
|
+
### Stateless Mode
|
|
800
|
+
- No session persistence
|
|
801
|
+
- Each request is independent
|
|
802
|
+
- Suitable for serverless deployments
|
|
803
|
+
- No SSE support
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### 6.2 Docker Support
|
|
807
|
+
**File**: `Dockerfile.http`
|
|
808
|
+
```dockerfile
|
|
809
|
+
FROM oven/bun:1-alpine
|
|
810
|
+
|
|
811
|
+
WORKDIR /app
|
|
812
|
+
|
|
813
|
+
COPY package.json bun.lockb ./
|
|
814
|
+
RUN bun install --frozen-lockfile
|
|
815
|
+
|
|
816
|
+
COPY . .
|
|
817
|
+
RUN bun run build
|
|
818
|
+
|
|
819
|
+
ENV TRANSPORT_TYPE=http
|
|
820
|
+
ENV HTTP_PORT=3000
|
|
821
|
+
ENV HTTP_HOST=0.0.0.0
|
|
822
|
+
|
|
823
|
+
EXPOSE 3000
|
|
824
|
+
|
|
825
|
+
CMD ["bun", "run", "start"]
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
## Testing & Validation Strategy
|
|
829
|
+
|
|
830
|
+
### 1. Unit Testing
|
|
831
|
+
- Transport manager logic
|
|
832
|
+
- Session management
|
|
833
|
+
- Security middleware
|
|
834
|
+
- Route handlers
|
|
835
|
+
|
|
836
|
+
### 2. Integration Testing
|
|
837
|
+
- End-to-end HTTP requests
|
|
838
|
+
- Session lifecycle
|
|
839
|
+
- SSE notifications
|
|
840
|
+
- Error handling
|
|
841
|
+
|
|
842
|
+
### 3. Compatibility Testing
|
|
843
|
+
- Stdio transport regression
|
|
844
|
+
- HTTP client compatibility
|
|
845
|
+
- SSE fallback scenarios
|
|
846
|
+
|
|
847
|
+
### 4. Performance Testing
|
|
848
|
+
- Concurrent session handling
|
|
849
|
+
- Memory usage under load
|
|
850
|
+
- Response time metrics
|
|
851
|
+
|
|
852
|
+
### 5. Security Testing
|
|
853
|
+
- CORS validation
|
|
854
|
+
- DNS rebinding protection
|
|
855
|
+
- Rate limiting effectiveness
|
|
856
|
+
- Authentication mechanisms
|
|
857
|
+
|
|
858
|
+
## Risk Mitigation
|
|
859
|
+
|
|
860
|
+
### Technical Risks
|
|
861
|
+
1. **Breaking Changes**: Mitigated by maintaining backward compatibility and phased rollout
|
|
862
|
+
2. **Performance Impact**: Addressed through proper session management and optional stateless mode
|
|
863
|
+
3. **Security Vulnerabilities**: Mitigated with comprehensive security middleware and testing
|
|
864
|
+
|
|
865
|
+
### Implementation Risks
|
|
866
|
+
1. **Complexity**: Managed through modular architecture and clear separation of concerns
|
|
867
|
+
2. **Testing Coverage**: Ensured through comprehensive test suite at multiple levels
|
|
868
|
+
3. **Documentation**: Maintained through inline comments and updated README
|
|
869
|
+
|
|
870
|
+
## Success Metrics
|
|
871
|
+
|
|
872
|
+
1. **Functionality**: All existing stdio functionality preserved
|
|
873
|
+
2. **Performance**: HTTP response time < 100ms for tool calls
|
|
874
|
+
3. **Reliability**: 99.9% uptime for HTTP server
|
|
875
|
+
4. **Security**: Zero security vulnerabilities in OWASP top 10
|
|
876
|
+
5. **Adoption**: Successful integration with at least 3 different MCP clients
|
|
877
|
+
|
|
878
|
+
## Implementation Timeline
|
|
879
|
+
|
|
880
|
+
### Week 1: Foundation
|
|
881
|
+
- [ ] Install dependencies
|
|
882
|
+
- [ ] Create transport interfaces and manager
|
|
883
|
+
- [ ] Basic HTTP server setup
|
|
884
|
+
|
|
885
|
+
### Week 2: Core Implementation
|
|
886
|
+
- [ ] Route handlers implementation
|
|
887
|
+
- [ ] Session management
|
|
888
|
+
- [ ] Configuration updates
|
|
889
|
+
|
|
890
|
+
### Week 3: Security & Testing
|
|
891
|
+
- [ ] Security middleware
|
|
892
|
+
- [ ] Unit tests
|
|
893
|
+
- [ ] Integration tests
|
|
894
|
+
|
|
895
|
+
### Week 4: Documentation & Polish
|
|
896
|
+
- [ ] Documentation updates
|
|
897
|
+
- [ ] Docker support
|
|
898
|
+
- [ ] Performance optimization
|
|
899
|
+
- [ ] Final testing and validation
|
|
900
|
+
|
|
901
|
+
## Conclusion
|
|
902
|
+
|
|
903
|
+
This implementation plan provides a comprehensive approach to adding Streamable HTTP transport to the Human MCP server. The modular architecture ensures maintainability and extensibility while preserving backward compatibility. The phased approach allows for iterative development and testing, reducing implementation risks.
|
|
904
|
+
|
|
905
|
+
The recommended Approach 1 (Modular Transport System) provides the best balance of functionality, maintainability, and extensibility, setting up the project for future enhancements and transport options.
|