@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
package/src/index.ts
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -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",
|