@goonnguyen/human-mcp 1.3.0 → 2.0.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 (128) hide show
  1. package/README.md +261 -19
  2. package/bin/human-mcp.js +2 -0
  3. package/dist/index.js +65180 -1698
  4. package/package.json +19 -2
  5. package/.claude/agents/code-reviewer.md +0 -140
  6. package/.claude/agents/database-admin.md +0 -86
  7. package/.claude/agents/debugger.md +0 -119
  8. package/.claude/agents/docs-manager.md +0 -113
  9. package/.claude/agents/git-manager.md +0 -59
  10. package/.claude/agents/planner-researcher.md +0 -97
  11. package/.claude/agents/project-manager.md +0 -113
  12. package/.claude/agents/tester.md +0 -95
  13. package/.claude/commands/cook.md +0 -7
  14. package/.claude/commands/debug.md +0 -10
  15. package/.claude/commands/docs/init.md +0 -11
  16. package/.claude/commands/docs/update.md +0 -11
  17. package/.claude/commands/fix/ci.md +0 -8
  18. package/.claude/commands/fix/fast.md +0 -5
  19. package/.claude/commands/fix/hard.md +0 -7
  20. package/.claude/commands/fix/test.md +0 -16
  21. package/.claude/commands/git/cm.md +0 -5
  22. package/.claude/commands/git/cp.md +0 -4
  23. package/.claude/commands/plan/ci.md +0 -12
  24. package/.claude/commands/plan/two.md +0 -13
  25. package/.claude/commands/plan.md +0 -10
  26. package/.claude/commands/test.md +0 -7
  27. package/.claude/commands/watzup.md +0 -8
  28. package/.claude/hooks/telegram_notify.sh +0 -136
  29. package/.claude/send-discord.sh +0 -64
  30. package/.claude/settings.json +0 -7
  31. package/.claude/statusline.sh +0 -143
  32. package/.dockerignore +0 -81
  33. package/.env.example +0 -44
  34. package/.github/workflows/publish.yml +0 -88
  35. package/.opencode/agent/code-reviewer.md +0 -142
  36. package/.opencode/agent/debugger.md +0 -74
  37. package/.opencode/agent/docs-manager.md +0 -119
  38. package/.opencode/agent/git-manager.md +0 -60
  39. package/.opencode/agent/planner-researcher.md +0 -100
  40. package/.opencode/agent/project-manager.md +0 -113
  41. package/.opencode/agent/system-architecture.md +0 -200
  42. package/.opencode/agent/tester.md +0 -96
  43. package/.opencode/agent/ui-ux-developer.md +0 -97
  44. package/.opencode/command/cook.md +0 -7
  45. package/.opencode/command/debug.md +0 -10
  46. package/.opencode/command/fix/ci.md +0 -8
  47. package/.opencode/command/fix/fast.md +0 -5
  48. package/.opencode/command/fix/hard.md +0 -7
  49. package/.opencode/command/fix/test.md +0 -16
  50. package/.opencode/command/git/cm.md +0 -5
  51. package/.opencode/command/git/cp.md +0 -4
  52. package/.opencode/command/plan/ci.md +0 -12
  53. package/.opencode/command/plan/two.md +0 -13
  54. package/.opencode/command/plan.md +0 -10
  55. package/.opencode/command/test.md +0 -7
  56. package/.opencode/command/watzup.md +0 -8
  57. package/.releaserc.json +0 -26
  58. package/.serena/project.yml +0 -68
  59. package/CHANGELOG.md +0 -62
  60. package/CLAUDE.md +0 -141
  61. package/DEPLOYMENT.md +0 -329
  62. package/Dockerfile +0 -52
  63. package/QUICKSTART.md +0 -97
  64. package/bun.lock +0 -1872
  65. package/bunfig.toml +0 -15
  66. package/docker-compose.yaml +0 -128
  67. package/docs/README.md +0 -51
  68. package/docs/codebase-structure-architecture-code-standards.md +0 -428
  69. package/docs/codebase-summary.md +0 -321
  70. package/docs/project-overview-pdr.md +0 -286
  71. package/docs/project-roadmap.md +0 -494
  72. package/examples/debugging-session.ts +0 -96
  73. package/human-mcp.png +0 -0
  74. package/inspector-wrapper.mjs +0 -33
  75. package/plans/001-streamable-http-transport-plan.md +0 -905
  76. package/plans/002-sse-fallback-http-transport-plan.md +0 -161
  77. package/plans/003-fix-test-infrastructure-and-ci-plan.md +0 -699
  78. package/plans/003-http-transport-local-file-access-plan.md +0 -880
  79. package/plans/004-fix-typescript-compilation-errors-plan.md +0 -388
  80. package/plans/005-comprehensive-test-infrastructure-fix-plan.md +0 -854
  81. package/plans/templates/bug-fix-template.md +0 -69
  82. package/plans/templates/feature-implementation-template.md +0 -84
  83. package/plans/templates/refactor-template.md +0 -82
  84. package/plans/templates/template-usage-guide.md +0 -58
  85. package/src/index.ts +0 -49
  86. package/src/prompts/debugging-prompts.ts +0 -149
  87. package/src/prompts/index.ts +0 -55
  88. package/src/resources/documentation.ts +0 -316
  89. package/src/resources/index.ts +0 -49
  90. package/src/server.ts +0 -36
  91. package/src/tools/eyes/index.ts +0 -225
  92. package/src/tools/eyes/processors/gif.ts +0 -137
  93. package/src/tools/eyes/processors/image.ts +0 -213
  94. package/src/tools/eyes/processors/video.ts +0 -135
  95. package/src/tools/eyes/schemas.ts +0 -51
  96. package/src/tools/eyes/utils/formatters.ts +0 -126
  97. package/src/tools/eyes/utils/gemini-client.ts +0 -73
  98. package/src/transports/http/file-interceptor.ts +0 -134
  99. package/src/transports/http/middleware.ts +0 -46
  100. package/src/transports/http/routes.ts +0 -297
  101. package/src/transports/http/server.ts +0 -116
  102. package/src/transports/http/session.ts +0 -93
  103. package/src/transports/http/sse-routes.ts +0 -210
  104. package/src/transports/index.ts +0 -36
  105. package/src/transports/stdio.ts +0 -7
  106. package/src/transports/types.ts +0 -50
  107. package/src/types/index.ts +0 -41
  108. package/src/utils/cloudflare-r2.ts +0 -107
  109. package/src/utils/config.ts +0 -123
  110. package/src/utils/errors.ts +0 -40
  111. package/src/utils/logger.ts +0 -49
  112. package/tests/integration/http-transport-files.test.ts +0 -190
  113. package/tests/integration/server.test.ts +0 -27
  114. package/tests/integration/sse-transport.test.ts +0 -142
  115. package/tests/setup.ts +0 -55
  116. package/tests/types/api-responses.ts +0 -35
  117. package/tests/types/test-types.ts +0 -105
  118. package/tests/unit/cloudflare-r2.test.ts +0 -118
  119. package/tests/unit/config.test.ts +0 -40
  120. package/tests/unit/eyes-analyze.test.ts +0 -150
  121. package/tests/unit/formatters.test.ts +0 -85
  122. package/tests/unit/sse-routes.test.ts +0 -92
  123. package/tests/utils/error-scenarios.ts +0 -198
  124. package/tests/utils/index.ts +0 -3
  125. package/tests/utils/mock-helpers.ts +0 -99
  126. package/tests/utils/test-data-generators.ts +0 -217
  127. package/tests/utils/test-server-manager.ts +0 -172
  128. package/tsconfig.json +0 -26
@@ -1,73 +0,0 @@
1
- import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai";
2
- import type { Config } from "@/utils/config";
3
- import { logger } from "@/utils/logger";
4
- import { APIError } from "@/utils/errors";
5
-
6
- export class GeminiClient {
7
- private genAI: GoogleGenerativeAI;
8
-
9
- constructor(private config: Config) {
10
- if (!config.gemini.apiKey) {
11
- throw new APIError("Google Gemini API key is required");
12
- }
13
- this.genAI = new GoogleGenerativeAI(config.gemini.apiKey);
14
- }
15
-
16
- getModel(detailLevel: "quick" | "detailed"): GenerativeModel {
17
- const modelName = detailLevel === "detailed"
18
- ? this.config.gemini.model
19
- : "gemini-2.5-flash";
20
-
21
- return this.genAI.getGenerativeModel({
22
- model: modelName,
23
- generationConfig: {
24
- temperature: 0.1,
25
- topK: 1,
26
- topP: 0.95,
27
- maxOutputTokens: 8192,
28
- }
29
- });
30
- }
31
-
32
- async analyzeContent(
33
- model: GenerativeModel,
34
- prompt: string,
35
- mediaData: Array<{ mimeType: string; data: string }>
36
- ): Promise<string> {
37
- try {
38
- logger.debug(`Analyzing content with ${mediaData.length} media files`);
39
-
40
- const parts = [
41
- { text: prompt },
42
- ...mediaData.map(media => ({
43
- inlineData: {
44
- mimeType: media.mimeType,
45
- data: media.data
46
- }
47
- }))
48
- ];
49
-
50
- // Add timeout wrapper
51
- const analysisPromise = model.generateContent(parts);
52
- const timeoutPromise = new Promise<never>((_, reject) => {
53
- setTimeout(() => reject(new APIError("Gemini API request timed out")), this.config.server.requestTimeout);
54
- });
55
-
56
- const result = await Promise.race([analysisPromise, timeoutPromise]);
57
- const response = await result.response;
58
- const text = response.text();
59
-
60
- if (!text) {
61
- throw new APIError("No response from Gemini API");
62
- }
63
-
64
- return text;
65
- } catch (error) {
66
- logger.error("Gemini API error:", error);
67
- if (error instanceof Error) {
68
- throw new APIError(`Gemini API error: ${error.message}`);
69
- }
70
- throw new APIError("Unknown Gemini API error");
71
- }
72
- }
73
- }
@@ -1,134 +0,0 @@
1
- import type { Request, Response, NextFunction } from 'express';
2
- import { getCloudflareR2 } from '@/utils/cloudflare-r2.js';
3
- import { logger } from '@/utils/logger.js';
4
- import fs from 'fs/promises';
5
- import path from 'path';
6
-
7
- export async function fileInterceptorMiddleware(
8
- req: Request,
9
- res: Response,
10
- next: NextFunction
11
- ) {
12
- // Only intercept tool calls with file paths
13
- if (req.body?.method === 'tools/call' && req.body?.params?.arguments) {
14
- const args = req.body.params.arguments;
15
-
16
- // Check for source fields that might contain file paths
17
- const fileFields = ['source', 'source1', 'source2', 'path', 'filePath'];
18
-
19
- for (const field of fileFields) {
20
- if (args[field] && typeof args[field] === 'string') {
21
- const filePath = args[field];
22
-
23
- // Detect Claude Desktop virtual paths
24
- if (filePath.startsWith('/mnt/user-data/') || filePath.startsWith('/mnt/')) {
25
- logger.info(`Intercepting Claude Desktop virtual path: ${filePath}`);
26
-
27
- try {
28
- // Extract filename
29
- const filename = path.basename(filePath);
30
-
31
- // Check if we have a temporary file saved by Claude Desktop
32
- const tempPath = path.join('/tmp/claude-uploads', filename);
33
-
34
- if (await fs.access(tempPath).then(() => true).catch(() => false)) {
35
- const cloudflare = getCloudflareR2();
36
- if (cloudflare) {
37
- // File exists in temp, upload to Cloudflare
38
- const buffer = await fs.readFile(tempPath);
39
- const publicUrl = await cloudflare.uploadFile(buffer, filename);
40
-
41
- // Replace the virtual path with CDN URL
42
- args[field] = publicUrl;
43
-
44
- // Clean up temp file
45
- await fs.unlink(tempPath).catch(() => {});
46
-
47
- logger.info(`Replaced virtual path with CDN URL: ${publicUrl}`);
48
- }
49
- } else {
50
- // No temp file, try to extract from request if it's base64
51
- // This handles cases where Claude Desktop might send base64 inline
52
- if ((req.body.params as any).fileData && (req.body.params as any).fileData[field]) {
53
- const base64Data = (req.body.params as any).fileData[field];
54
- const mimeType = (req.body.params as any).fileMimeTypes?.[field] || 'image/jpeg';
55
-
56
- const cloudflare = getCloudflareR2();
57
- if (cloudflare) {
58
- const publicUrl = await cloudflare.uploadBase64(
59
- base64Data,
60
- mimeType,
61
- filename
62
- );
63
-
64
- args[field] = publicUrl;
65
- logger.info(`Uploaded inline base64 to CDN: ${publicUrl}`);
66
- }
67
- } else {
68
- // Provide helpful error response
69
- logger.warn(`Cannot access virtual path: ${filePath}`);
70
- return res.status(400).json({
71
- jsonrpc: '2.0',
72
- error: {
73
- code: -32602,
74
- message: 'File not accessible via HTTP transport',
75
- data: {
76
- path: filePath,
77
- suggestions: [
78
- 'Upload the file using the /mcp/upload endpoint first',
79
- 'Use a public URL instead of a local file path',
80
- 'Convert the image to a base64 data URI',
81
- 'Switch to stdio transport for local file access'
82
- ]
83
- }
84
- },
85
- id: req.body.id
86
- });
87
- }
88
- }
89
- } catch (error) {
90
- logger.error(`Error processing virtual path: ${error instanceof Error ? error.message : 'Unknown error'}`);
91
- return res.status(500).json({
92
- jsonrpc: '2.0',
93
- error: {
94
- code: -32603,
95
- message: `Failed to process file: ${error instanceof Error ? error.message : 'Unknown error'}`
96
- },
97
- id: req.body.id
98
- });
99
- }
100
- }
101
-
102
- // Handle regular local paths when in HTTP mode
103
- else if (!filePath.startsWith('http') && !filePath.startsWith('data:')) {
104
- if (process.env.TRANSPORT_TYPE === 'http') {
105
- const cloudflare = getCloudflareR2();
106
- if (cloudflare) {
107
- try {
108
- // Check if file exists locally
109
- await fs.access(filePath);
110
-
111
- // Upload to Cloudflare R2
112
- const buffer = await fs.readFile(filePath);
113
- const filename = path.basename(filePath);
114
- const publicUrl = await cloudflare.uploadFile(buffer, filename);
115
-
116
- // Replace local path with CDN URL
117
- args[field] = publicUrl;
118
-
119
- logger.info(`Auto-uploaded local file to CDN: ${publicUrl}`);
120
- } catch (error) {
121
- if (error instanceof Error && error.message.includes('ENOENT')) {
122
- logger.warn(`Local file not found: ${filePath}`);
123
- }
124
- // Continue without modification if file doesn't exist or cloudflare not configured
125
- }
126
- }
127
- }
128
- }
129
- }
130
- }
131
- }
132
-
133
- next();
134
- }
@@ -1,46 +0,0 @@
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
- }
@@ -1,297 +0,0 @@
1
- import { Router } from "express";
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
5
- import { SessionManager } from "./session.js";
6
- import type { HttpTransportConfig } from "../types.js";
7
- import multer from 'multer';
8
- import { getCloudflareR2 } from '@/utils/cloudflare-r2.js';
9
- import { logger } from '@/utils/logger.js';
10
-
11
- // Interface for SSE session checking (to avoid circular dependency)
12
- interface SSESessionChecker {
13
- hasSession(sessionId: string): boolean;
14
- }
15
-
16
- export function createRoutes(
17
- mcpServer: McpServer,
18
- sessionManager: SessionManager,
19
- config: HttpTransportConfig,
20
- sseSessionChecker?: SSESessionChecker
21
- ): Router {
22
- const router = Router();
23
-
24
- // POST /mcp - Handle client requests
25
- router.post('/', async (req, res) => {
26
- try {
27
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
28
-
29
- if (config.sessionMode === 'stateless') {
30
- await handleStatelessRequest(mcpServer, req, res);
31
- } else {
32
- await handleStatefulRequest(mcpServer, sessionManager, sessionId, req, res, sseSessionChecker);
33
- }
34
- } catch (error) {
35
- handleError(res, error);
36
- }
37
- });
38
-
39
- // GET /mcp - SSE endpoint for notifications
40
- router.get('/', async (req, res) => {
41
- if (config.sessionMode === 'stateless') {
42
- res.status(405).json({
43
- jsonrpc: "2.0",
44
- error: {
45
- code: -32000,
46
- message: "SSE not supported in stateless mode"
47
- },
48
- id: null
49
- });
50
- return;
51
- }
52
-
53
- const sessionId = req.headers['mcp-session-id'] as string;
54
- const transport = await sessionManager.getTransport(sessionId);
55
-
56
- if (!transport) {
57
- res.status(400).send('Invalid or missing session ID');
58
- return;
59
- }
60
-
61
- await transport.handleRequest(req, res);
62
- });
63
-
64
- // DELETE /mcp - Session termination
65
- router.delete('/', async (req, res) => {
66
- if (config.sessionMode === 'stateless') {
67
- res.status(405).json({
68
- jsonrpc: "2.0",
69
- error: {
70
- code: -32000,
71
- message: "Session termination not applicable in stateless mode"
72
- },
73
- id: null
74
- });
75
- return;
76
- }
77
-
78
- const sessionId = req.headers['mcp-session-id'] as string;
79
- await sessionManager.terminateSession(sessionId);
80
- res.status(204).send();
81
- });
82
-
83
- // Configure multer for memory storage
84
- const upload = multer({
85
- storage: multer.memoryStorage(),
86
- limits: {
87
- fileSize: 100 * 1024 * 1024, // 100MB limit
88
- },
89
- fileFilter: (req, file, cb) => {
90
- // Accept images, videos, and GIFs
91
- if (file.mimetype.startsWith('image/') ||
92
- file.mimetype.startsWith('video/') ||
93
- file.mimetype === 'image/gif') {
94
- cb(null, true);
95
- } else {
96
- cb(new Error('Invalid file type. Only images and videos are allowed.'));
97
- }
98
- }
99
- });
100
-
101
- // POST /upload - Handle file uploads to Cloudflare R2
102
- router.post('/upload', upload.single('file'), async (req, res) => {
103
- try {
104
- if (!req.file) {
105
- res.status(400).json({
106
- jsonrpc: '2.0',
107
- error: {
108
- code: -32600,
109
- message: 'No file uploaded'
110
- },
111
- id: null
112
- });
113
- return;
114
- }
115
-
116
- const cloudflare = getCloudflareR2();
117
- if (!cloudflare) {
118
- res.status(500).json({
119
- jsonrpc: '2.0',
120
- error: {
121
- code: -32603,
122
- message: 'Cloudflare R2 not configured. Please set up environment variables.'
123
- },
124
- id: null
125
- });
126
- return;
127
- }
128
-
129
- // Upload to Cloudflare R2
130
- const publicUrl = await cloudflare.uploadFile(
131
- req.file.buffer,
132
- req.file.originalname
133
- );
134
-
135
- res.json({
136
- jsonrpc: '2.0',
137
- result: {
138
- success: true,
139
- url: publicUrl,
140
- originalName: req.file.originalname,
141
- size: req.file.size,
142
- mimeType: req.file.mimetype,
143
- message: 'File uploaded successfully to Cloudflare R2'
144
- },
145
- id: (req.body as any)?.id || null
146
- });
147
- } catch (error) {
148
- logger.error('Upload error:', error);
149
- res.status(500).json({
150
- jsonrpc: '2.0',
151
- error: {
152
- code: -32603,
153
- message: `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`
154
- },
155
- id: (req.body as any)?.id || null
156
- });
157
- }
158
- });
159
-
160
- // POST /upload-base64 - Handle base64 uploads
161
- router.post('/upload-base64', async (req, res) => {
162
- try {
163
- const { data, mimeType, filename } = req.body;
164
-
165
- if (!data || !mimeType) {
166
- res.status(400).json({
167
- jsonrpc: '2.0',
168
- error: {
169
- code: -32600,
170
- message: 'Missing required fields: data and mimeType'
171
- },
172
- id: null
173
- });
174
- return;
175
- }
176
-
177
- const cloudflare = getCloudflareR2();
178
- if (!cloudflare) {
179
- res.status(500).json({
180
- jsonrpc: '2.0',
181
- error: {
182
- code: -32603,
183
- message: 'Cloudflare R2 not configured. Please set up environment variables.'
184
- },
185
- id: null
186
- });
187
- return;
188
- }
189
-
190
- // Remove data URI prefix if present
191
- const base64Data = data.replace(/^data:.*?;base64,/, '');
192
-
193
- // Upload to Cloudflare R2
194
- const publicUrl = await cloudflare.uploadBase64(
195
- base64Data,
196
- mimeType,
197
- filename
198
- );
199
-
200
- res.json({
201
- jsonrpc: '2.0',
202
- result: {
203
- success: true,
204
- url: publicUrl,
205
- message: 'Base64 data uploaded successfully to Cloudflare R2'
206
- },
207
- id: req.body?.id || null
208
- });
209
- } catch (error) {
210
- logger.error('Base64 upload error:', error);
211
- res.status(500).json({
212
- jsonrpc: '2.0',
213
- error: {
214
- code: -32603,
215
- message: `Failed to upload base64 data: ${error instanceof Error ? error.message : 'Unknown error'}`
216
- },
217
- id: req.body?.id || null
218
- });
219
- }
220
- });
221
-
222
- return router;
223
- }
224
-
225
- async function handleStatelessRequest(
226
- mcpServer: McpServer,
227
- req: any,
228
- res: any
229
- ): Promise<void> {
230
- const transport = new StreamableHTTPServerTransport({
231
- sessionIdGenerator: undefined,
232
- });
233
-
234
- res.on('close', () => {
235
- transport.close();
236
- });
237
-
238
- await mcpServer.connect(transport);
239
- await transport.handleRequest(req, res, req.body);
240
- }
241
-
242
- async function handleStatefulRequest(
243
- mcpServer: McpServer,
244
- sessionManager: SessionManager,
245
- sessionId: string | undefined,
246
- req: any,
247
- res: any,
248
- sseSessionChecker?: SSESessionChecker
249
- ): Promise<void> {
250
- // Check if sessionId is being used by SSE transport
251
- if (sessionId && sseSessionChecker?.hasSession(sessionId)) {
252
- res.status(400).json({
253
- jsonrpc: '2.0',
254
- error: {
255
- code: -32600,
256
- message: 'Session ID is already in use by SSE transport',
257
- },
258
- id: null,
259
- });
260
- return;
261
- }
262
-
263
- let transport = sessionId ?
264
- await sessionManager.getTransport(sessionId) : null;
265
-
266
- if (!transport && isInitializeRequest(req.body)) {
267
- const session = await sessionManager.createSession(mcpServer);
268
- transport = session.transport;
269
- res.setHeader('Mcp-Session-Id', session.sessionId);
270
- } else if (!transport) {
271
- res.status(400).json({
272
- jsonrpc: '2.0',
273
- error: {
274
- code: -32000,
275
- message: 'Bad Request: No valid session ID provided',
276
- },
277
- id: null,
278
- });
279
- return;
280
- }
281
-
282
- await transport.handleRequest(req, res, req.body);
283
- }
284
-
285
- function handleError(res: any, error: any): void {
286
- console.error('MCP request error:', error);
287
- if (!res.headersSent) {
288
- res.status(500).json({
289
- jsonrpc: '2.0',
290
- error: {
291
- code: -32603,
292
- message: 'Internal server error',
293
- },
294
- id: null,
295
- });
296
- }
297
- }
@@ -1,116 +0,0 @@
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 { createSSERoutes } from "./sse-routes.js";
8
- import { SessionManager } from "./session.js";
9
- import { createSecurityMiddleware } from "./middleware.js";
10
- import { fileInterceptorMiddleware } from "./file-interceptor.js";
11
- import type { HttpTransportConfig, HttpServerHandle } from "../types.js";
12
-
13
- export async function startHttpTransport(
14
- mcpServer: McpServer,
15
- config: HttpTransportConfig
16
- ): Promise<HttpServerHandle> {
17
- const app = express();
18
- const sessionManager = new SessionManager(config.sessionMode, config);
19
-
20
- // Apply middleware
21
- app.use(express.json({ limit: '50mb' }));
22
- app.use(compression());
23
- app.use(helmet({
24
- contentSecurityPolicy: false, // Disable CSP for API server
25
- crossOriginEmbedderPolicy: false
26
- }));
27
-
28
- if (config.security?.enableCors !== false) {
29
- app.use(cors({
30
- origin: config.security?.corsOrigins || '*',
31
- exposedHeaders: ['Mcp-Session-Id'],
32
- allowedHeaders: ['Content-Type', 'mcp-session-id', 'Authorization'],
33
- credentials: true
34
- }));
35
- }
36
-
37
- app.use(createSecurityMiddleware(config.security));
38
-
39
- // Add file interceptor middleware before routes
40
- app.use(fileInterceptorMiddleware);
41
-
42
- // Create SSE routes first if enabled to get SSE manager reference
43
- let sseManager: any = undefined;
44
- if (config.enableSseFallback) {
45
- console.log('Enabling SSE fallback transport');
46
- const sseRoutes = createSSERoutes(mcpServer, config, sessionManager);
47
- app.use(sseRoutes);
48
-
49
- // Store SSE manager reference for cleanup and cross-validation
50
- sseManager = (sseRoutes as any).sseManager;
51
- (app as any).sseManager = sseManager;
52
- }
53
-
54
- // Create routes with SSE session checker
55
- const routes = createRoutes(mcpServer, sessionManager, config, sseManager);
56
- app.use('/mcp', routes);
57
-
58
- // Health check endpoint
59
- app.get('/health', (req, res) => {
60
- const healthStatus: any = {
61
- status: 'healthy',
62
- transport: 'streamable-http'
63
- };
64
-
65
- if (config.enableSseFallback) {
66
- healthStatus.sseFallback = 'enabled';
67
- healthStatus.ssePaths = config.ssePaths;
68
- }
69
-
70
- res.json(healthStatus);
71
- });
72
-
73
- // Start server
74
- const port = config.port || 3000;
75
- const host = config.host || '0.0.0.0';
76
-
77
- const server = app.listen(port, host, () => {
78
- console.log(`MCP HTTP Server listening on http://${host}:${port}`);
79
- console.log(`Health check: http://${host}:${port}/health`);
80
- console.log(`MCP endpoint: http://${host}:${port}/mcp`);
81
- });
82
-
83
- // Create cleanup function
84
- const cleanup = async () => {
85
- console.log('Shutting down HTTP server...');
86
- await sessionManager.cleanup();
87
-
88
- // Cleanup SSE sessions if enabled
89
- if (config.enableSseFallback && (app as any).sseManager) {
90
- await (app as any).sseManager.cleanup();
91
- }
92
- };
93
-
94
- // Graceful shutdown handling
95
- process.on('SIGTERM', cleanup);
96
- process.on('SIGINT', cleanup);
97
-
98
- // Return server handle
99
- const handle: HttpServerHandle = {
100
- app,
101
- server,
102
- sessionManager,
103
- sseManager,
104
- async close() {
105
- await cleanup();
106
- return new Promise<void>((resolve, reject) => {
107
- server.close((err) => {
108
- if (err) reject(err);
109
- else resolve();
110
- });
111
- });
112
- }
113
- };
114
-
115
- return handle;
116
- }