@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/src/index.ts CHANGED
@@ -1,5 +1,47 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { startStdioServer } from "./server.js";
3
+ import { createServer } from "./server.js";
4
+ import { TransportManager } from "./transports/index.js";
5
+ import { loadConfig } from "./utils/config.js";
6
+ import { logger } from "./utils/logger.js";
4
7
 
5
- await startStdioServer();
8
+ async function main() {
9
+ try {
10
+ const config = loadConfig();
11
+ const server = await createServer();
12
+
13
+ const transportConfig = {
14
+ type: config.transport.type,
15
+ http: config.transport.http?.enabled ? {
16
+ port: config.transport.http.port,
17
+ host: config.transport.http.host,
18
+ sessionMode: config.transport.http.sessionMode,
19
+ enableSse: config.transport.http.enableSse,
20
+ enableJsonResponse: config.transport.http.enableJsonResponse,
21
+ security: config.transport.http.security
22
+ } : undefined
23
+ };
24
+
25
+ const transportManager = new TransportManager(server, transportConfig);
26
+ await transportManager.start();
27
+
28
+ logger.info(`Human MCP Server started with ${config.transport.type} transport`);
29
+
30
+ // Graceful shutdown
31
+ process.on('SIGINT', async () => {
32
+ logger.info('Shutting down server...');
33
+ process.exit(0);
34
+ });
35
+
36
+ process.on('SIGTERM', async () => {
37
+ logger.info('Shutting down server...');
38
+ process.exit(0);
39
+ });
40
+
41
+ } catch (error) {
42
+ logger.error('Failed to start server:', error);
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ main();
@@ -0,0 +1,46 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ import type { SecurityConfig } from "../types.js";
3
+
4
+ export function createSecurityMiddleware(config?: SecurityConfig) {
5
+ return async (req: Request, res: Response, next: NextFunction) => {
6
+ // DNS Rebinding Protection
7
+ if (config?.enableDnsRebindingProtection) {
8
+ const host = req.headers.host?.split(':')[0];
9
+ const allowedHosts = config.allowedHosts || ['127.0.0.1', 'localhost'];
10
+
11
+ if (host && !allowedHosts.includes(host)) {
12
+ res.status(403).json({
13
+ error: 'Forbidden: Invalid host'
14
+ });
15
+ return;
16
+ }
17
+ }
18
+
19
+ // Rate Limiting (basic implementation)
20
+ if (config?.enableRateLimiting) {
21
+ // Implement rate limiting logic here
22
+ // Could use express-rate-limit package
23
+ }
24
+
25
+ // Secret-based authentication (optional)
26
+ if (config?.secret) {
27
+ const authHeader = req.headers.authorization;
28
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
29
+ res.status(401).json({
30
+ error: 'Unauthorized: Missing authentication'
31
+ });
32
+ return;
33
+ }
34
+
35
+ const token = authHeader.substring(7);
36
+ if (token !== config.secret) {
37
+ res.status(401).json({
38
+ error: 'Unauthorized: Invalid token'
39
+ });
40
+ return;
41
+ }
42
+ }
43
+
44
+ next();
45
+ };
46
+ }
@@ -0,0 +1,136 @@
1
+ import { Router } from "express";
2
+ import { randomUUID } from "node:crypto";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
+ import { SessionManager } from "./session.js";
7
+ import type { HttpTransportConfig } from "../types.js";
8
+
9
+ export function createRoutes(
10
+ mcpServer: McpServer,
11
+ sessionManager: SessionManager,
12
+ config: HttpTransportConfig
13
+ ): Router {
14
+ const router = Router();
15
+
16
+ // POST /mcp - Handle client requests
17
+ router.post('/', async (req, res) => {
18
+ try {
19
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
20
+
21
+ if (config.sessionMode === 'stateless') {
22
+ await handleStatelessRequest(mcpServer, req, res);
23
+ } else {
24
+ await handleStatefulRequest(mcpServer, sessionManager, sessionId, req, res);
25
+ }
26
+ } catch (error) {
27
+ handleError(res, error);
28
+ }
29
+ });
30
+
31
+ // GET /mcp - SSE endpoint for notifications
32
+ router.get('/', async (req, res) => {
33
+ if (config.sessionMode === 'stateless') {
34
+ res.status(405).json({
35
+ jsonrpc: "2.0",
36
+ error: {
37
+ code: -32000,
38
+ message: "SSE not supported in stateless mode"
39
+ },
40
+ id: null
41
+ });
42
+ return;
43
+ }
44
+
45
+ const sessionId = req.headers['mcp-session-id'] as string;
46
+ const transport = await sessionManager.getTransport(sessionId);
47
+
48
+ if (!transport) {
49
+ res.status(400).send('Invalid or missing session ID');
50
+ return;
51
+ }
52
+
53
+ await transport.handleRequest(req, res);
54
+ });
55
+
56
+ // DELETE /mcp - Session termination
57
+ router.delete('/', async (req, res) => {
58
+ if (config.sessionMode === 'stateless') {
59
+ res.status(405).json({
60
+ jsonrpc: "2.0",
61
+ error: {
62
+ code: -32000,
63
+ message: "Session termination not applicable in stateless mode"
64
+ },
65
+ id: null
66
+ });
67
+ return;
68
+ }
69
+
70
+ const sessionId = req.headers['mcp-session-id'] as string;
71
+ await sessionManager.terminateSession(sessionId);
72
+ res.status(204).send();
73
+ });
74
+
75
+ return router;
76
+ }
77
+
78
+ async function handleStatelessRequest(
79
+ mcpServer: McpServer,
80
+ req: any,
81
+ res: any
82
+ ): Promise<void> {
83
+ const transport = new StreamableHTTPServerTransport({
84
+ sessionIdGenerator: undefined,
85
+ });
86
+
87
+ res.on('close', () => {
88
+ transport.close();
89
+ });
90
+
91
+ await mcpServer.connect(transport);
92
+ await transport.handleRequest(req, res, req.body);
93
+ }
94
+
95
+ async function handleStatefulRequest(
96
+ mcpServer: McpServer,
97
+ sessionManager: SessionManager,
98
+ sessionId: string | undefined,
99
+ req: any,
100
+ res: any
101
+ ): Promise<void> {
102
+ let transport = sessionId ?
103
+ await sessionManager.getTransport(sessionId) : null;
104
+
105
+ if (!transport && isInitializeRequest(req.body)) {
106
+ const session = await sessionManager.createSession(mcpServer);
107
+ transport = session.transport;
108
+ res.setHeader('Mcp-Session-Id', session.sessionId);
109
+ } else if (!transport) {
110
+ res.status(400).json({
111
+ jsonrpc: '2.0',
112
+ error: {
113
+ code: -32000,
114
+ message: 'Bad Request: No valid session ID provided',
115
+ },
116
+ id: null,
117
+ });
118
+ return;
119
+ }
120
+
121
+ await transport.handleRequest(req, res, req.body);
122
+ }
123
+
124
+ function handleError(res: any, error: any): void {
125
+ console.error('MCP request error:', error);
126
+ if (!res.headersSent) {
127
+ res.status(500).json({
128
+ jsonrpc: '2.0',
129
+ error: {
130
+ code: -32603,
131
+ message: 'Internal server error',
132
+ },
133
+ id: null,
134
+ });
135
+ }
136
+ }
@@ -0,0 +1,66 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import compression from "compression";
4
+ import helmet from "helmet";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { createRoutes } from "./routes.js";
7
+ import { SessionManager } from "./session.js";
8
+ import { createSecurityMiddleware } from "./middleware.js";
9
+ import type { HttpTransportConfig } from "../types.js";
10
+
11
+ export async function startHttpTransport(
12
+ mcpServer: McpServer,
13
+ config: HttpTransportConfig
14
+ ): Promise<void> {
15
+ const app = express();
16
+ const sessionManager = new SessionManager(config.sessionMode, config);
17
+
18
+ // Apply middleware
19
+ app.use(express.json({ limit: '50mb' }));
20
+ app.use(compression());
21
+ app.use(helmet({
22
+ contentSecurityPolicy: false, // Disable CSP for API server
23
+ crossOriginEmbedderPolicy: false
24
+ }));
25
+
26
+ if (config.security?.enableCors !== false) {
27
+ app.use(cors({
28
+ origin: config.security?.corsOrigins || '*',
29
+ exposedHeaders: ['Mcp-Session-Id'],
30
+ allowedHeaders: ['Content-Type', 'mcp-session-id'],
31
+ credentials: true
32
+ }));
33
+ }
34
+
35
+ app.use(createSecurityMiddleware(config.security));
36
+
37
+ // Create routes
38
+ const routes = createRoutes(mcpServer, sessionManager, config);
39
+ app.use('/mcp', routes);
40
+
41
+ // Health check endpoint
42
+ app.get('/health', (req, res) => {
43
+ res.json({ status: 'healthy', transport: 'streamable-http' });
44
+ });
45
+
46
+ // Start server
47
+ const port = config.port || 3000;
48
+ const host = config.host || '0.0.0.0';
49
+
50
+ app.listen(port, host, () => {
51
+ console.log(`MCP HTTP Server listening on http://${host}:${port}`);
52
+ console.log(`Health check: http://${host}:${port}/health`);
53
+ console.log(`MCP endpoint: http://${host}:${port}/mcp`);
54
+ });
55
+
56
+ // Graceful shutdown handling
57
+ process.on('SIGTERM', async () => {
58
+ console.log('Shutting down HTTP server...');
59
+ await sessionManager.cleanup();
60
+ });
61
+
62
+ process.on('SIGINT', async () => {
63
+ console.log('Shutting down HTTP server...');
64
+ await sessionManager.cleanup();
65
+ });
66
+ }
@@ -0,0 +1,85 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import type { SessionStore, TransportSession, HttpTransportConfig } from "../types.js";
5
+
6
+ export class SessionManager {
7
+ private transports: Map<string, StreamableHTTPServerTransport>;
8
+ private sessionMode: 'stateful' | 'stateless';
9
+ private store?: SessionStore;
10
+ private config: HttpTransportConfig;
11
+
12
+ constructor(sessionMode: 'stateful' | 'stateless', config: HttpTransportConfig, store?: SessionStore) {
13
+ this.transports = new Map();
14
+ this.sessionMode = sessionMode;
15
+ this.config = config;
16
+ this.store = store;
17
+ }
18
+
19
+ async createSession(mcpServer: McpServer): Promise<{ transport: StreamableHTTPServerTransport, sessionId: string }> {
20
+ const sessionId = randomUUID();
21
+
22
+ const transport = new StreamableHTTPServerTransport({
23
+ sessionIdGenerator: () => sessionId,
24
+ enableJsonResponse: this.config.enableJsonResponse,
25
+ enableDnsRebindingProtection: this.config.security?.enableDnsRebindingProtection ?? true,
26
+ allowedHosts: this.config.security?.allowedHosts ?? ['127.0.0.1', 'localhost'],
27
+ });
28
+
29
+ // Store the transport by the generated session ID
30
+ this.transports.set(sessionId, transport);
31
+
32
+ transport.onclose = () => {
33
+ this.terminateSession(sessionId);
34
+ };
35
+
36
+ if (this.store) {
37
+ await this.store.set(sessionId, {
38
+ id: sessionId,
39
+ createdAt: Date.now(),
40
+ transport: transport
41
+ });
42
+ }
43
+
44
+ await mcpServer.connect(transport);
45
+
46
+ return { transport, sessionId };
47
+ }
48
+
49
+ async getTransport(sessionId: string): Promise<StreamableHTTPServerTransport | null> {
50
+ let transport = this.transports.get(sessionId);
51
+
52
+ if (!transport && this.store) {
53
+ const session = await this.store.get(sessionId);
54
+ if (session && session.transport) {
55
+ transport = session.transport;
56
+ this.transports.set(sessionId, transport);
57
+ }
58
+ }
59
+
60
+ return transport || null;
61
+ }
62
+
63
+ async terminateSession(sessionId: string): Promise<void> {
64
+ const transport = this.transports.get(sessionId);
65
+ if (transport) {
66
+ transport.close();
67
+ this.transports.delete(sessionId);
68
+ }
69
+
70
+ if (this.store) {
71
+ await this.store.delete(sessionId);
72
+ }
73
+ }
74
+
75
+ async cleanup(): Promise<void> {
76
+ for (const [sessionId, transport] of this.transports) {
77
+ transport.close();
78
+ }
79
+ this.transports.clear();
80
+
81
+ if (this.store) {
82
+ await this.store.cleanup();
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,31 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { startStdioTransport } from "./stdio.js";
3
+ import { startHttpTransport } from "./http/server.js";
4
+ import type { TransportConfig } from "./types.js";
5
+
6
+ export class TransportManager {
7
+ private server: McpServer;
8
+ private config: TransportConfig;
9
+
10
+ constructor(server: McpServer, config: TransportConfig) {
11
+ this.server = server;
12
+ this.config = config;
13
+ }
14
+
15
+ async start(): Promise<void> {
16
+ switch (this.config.type) {
17
+ case 'stdio':
18
+ await startStdioTransport(this.server);
19
+ break;
20
+ case 'http':
21
+ await startHttpTransport(this.server, this.config.http!);
22
+ break;
23
+ case 'both':
24
+ await Promise.all([
25
+ startStdioTransport(this.server),
26
+ startHttpTransport(this.server, this.config.http!)
27
+ ]);
28
+ break;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+
4
+ export async function startStdioTransport(server: McpServer): Promise<void> {
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ }
@@ -0,0 +1,37 @@
1
+ import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
+
3
+ export interface TransportConfig {
4
+ type: 'stdio' | 'http' | 'both';
5
+ http?: HttpTransportConfig;
6
+ }
7
+
8
+ export interface HttpTransportConfig {
9
+ port: number;
10
+ host?: string;
11
+ sessionMode: 'stateful' | 'stateless';
12
+ enableSse?: boolean;
13
+ enableJsonResponse?: boolean;
14
+ security?: SecurityConfig;
15
+ }
16
+
17
+ export interface SecurityConfig {
18
+ enableCors?: boolean;
19
+ corsOrigins?: string[];
20
+ enableDnsRebindingProtection?: boolean;
21
+ allowedHosts?: string[];
22
+ enableRateLimiting?: boolean;
23
+ secret?: string;
24
+ }
25
+
26
+ export interface TransportSession {
27
+ id: string;
28
+ createdAt: number;
29
+ transport: StreamableHTTPServerTransport;
30
+ }
31
+
32
+ export interface SessionStore {
33
+ get(sessionId: string): Promise<TransportSession | null>;
34
+ set(sessionId: string, session: TransportSession): Promise<void>;
35
+ delete(sessionId: string): Promise<void>;
36
+ cleanup(): Promise<void>;
37
+ }
@@ -5,6 +5,25 @@ const ConfigSchema = z.object({
5
5
  apiKey: z.string().min(1, "Google Gemini API key is required"),
6
6
  model: z.string().default("gemini-2.5-flash"),
7
7
  }),
8
+ transport: z.object({
9
+ type: z.enum(["stdio", "http", "both"]).default("stdio"),
10
+ http: z.object({
11
+ enabled: z.boolean().default(false),
12
+ port: z.number().default(3000),
13
+ host: z.string().default("0.0.0.0"),
14
+ sessionMode: z.enum(["stateful", "stateless"]).default("stateful"),
15
+ enableSse: z.boolean().default(true),
16
+ enableJsonResponse: z.boolean().default(true),
17
+ security: z.object({
18
+ enableCors: z.boolean().default(true),
19
+ corsOrigins: z.array(z.string()).optional(),
20
+ enableDnsRebindingProtection: z.boolean().default(true),
21
+ allowedHosts: z.array(z.string()).default(["127.0.0.1", "localhost"]),
22
+ enableRateLimiting: z.boolean().default(false),
23
+ secret: z.string().optional(),
24
+ }).optional(),
25
+ }).optional(),
26
+ }),
8
27
  server: z.object({
9
28
  port: z.number().default(3000),
10
29
  maxRequestSize: z.string().default("50MB"),
@@ -26,11 +45,38 @@ const ConfigSchema = z.object({
26
45
  export type Config = z.infer<typeof ConfigSchema>;
27
46
 
28
47
  export function loadConfig(): Config {
48
+ const corsOrigins = process.env.HTTP_CORS_ORIGINS ?
49
+ process.env.HTTP_CORS_ORIGINS.split(',').map(origin => origin.trim()) :
50
+ undefined;
51
+
52
+ const allowedHosts = process.env.HTTP_ALLOWED_HOSTS ?
53
+ process.env.HTTP_ALLOWED_HOSTS.split(',').map(host => host.trim()) :
54
+ ["127.0.0.1", "localhost"];
55
+
29
56
  return ConfigSchema.parse({
30
57
  gemini: {
31
58
  apiKey: process.env.GOOGLE_GEMINI_API_KEY || "",
32
59
  model: process.env.GOOGLE_GEMINI_MODEL || "gemini-2.5-flash",
33
60
  },
61
+ transport: {
62
+ type: (process.env.TRANSPORT_TYPE as any) || "stdio",
63
+ http: {
64
+ enabled: process.env.TRANSPORT_TYPE === "http" || process.env.TRANSPORT_TYPE === "both",
65
+ port: parseInt(process.env.HTTP_PORT || "3000"),
66
+ host: process.env.HTTP_HOST || "0.0.0.0",
67
+ sessionMode: (process.env.HTTP_SESSION_MODE as any) || "stateful",
68
+ enableSse: process.env.HTTP_ENABLE_SSE !== "false",
69
+ enableJsonResponse: process.env.HTTP_ENABLE_JSON_RESPONSE !== "false",
70
+ security: {
71
+ enableCors: process.env.HTTP_CORS_ENABLED !== "false",
72
+ corsOrigins,
73
+ enableDnsRebindingProtection: process.env.HTTP_DNS_REBINDING_ENABLED !== "false",
74
+ allowedHosts,
75
+ enableRateLimiting: process.env.HTTP_ENABLE_RATE_LIMITING === "true",
76
+ secret: process.env.HTTP_SECRET,
77
+ },
78
+ },
79
+ },
34
80
  server: {
35
81
  port: parseInt(process.env.PORT || "3000"),
36
82
  maxRequestSize: process.env.MAX_REQUEST_SIZE || "50MB",