@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.
- package/README.md +261 -19
- package/bin/human-mcp.js +2 -0
- package/dist/index.js +65180 -1698
- package/package.json +19 -2
- package/.claude/agents/code-reviewer.md +0 -140
- package/.claude/agents/database-admin.md +0 -86
- package/.claude/agents/debugger.md +0 -119
- package/.claude/agents/docs-manager.md +0 -113
- package/.claude/agents/git-manager.md +0 -59
- package/.claude/agents/planner-researcher.md +0 -97
- package/.claude/agents/project-manager.md +0 -113
- package/.claude/agents/tester.md +0 -95
- package/.claude/commands/cook.md +0 -7
- package/.claude/commands/debug.md +0 -10
- package/.claude/commands/docs/init.md +0 -11
- package/.claude/commands/docs/update.md +0 -11
- package/.claude/commands/fix/ci.md +0 -8
- package/.claude/commands/fix/fast.md +0 -5
- package/.claude/commands/fix/hard.md +0 -7
- package/.claude/commands/fix/test.md +0 -16
- package/.claude/commands/git/cm.md +0 -5
- package/.claude/commands/git/cp.md +0 -4
- package/.claude/commands/plan/ci.md +0 -12
- package/.claude/commands/plan/two.md +0 -13
- package/.claude/commands/plan.md +0 -10
- package/.claude/commands/test.md +0 -7
- package/.claude/commands/watzup.md +0 -8
- package/.claude/hooks/telegram_notify.sh +0 -136
- package/.claude/send-discord.sh +0 -64
- package/.claude/settings.json +0 -7
- package/.claude/statusline.sh +0 -143
- package/.dockerignore +0 -81
- package/.env.example +0 -44
- package/.github/workflows/publish.yml +0 -88
- package/.opencode/agent/code-reviewer.md +0 -142
- package/.opencode/agent/debugger.md +0 -74
- package/.opencode/agent/docs-manager.md +0 -119
- package/.opencode/agent/git-manager.md +0 -60
- package/.opencode/agent/planner-researcher.md +0 -100
- package/.opencode/agent/project-manager.md +0 -113
- package/.opencode/agent/system-architecture.md +0 -200
- package/.opencode/agent/tester.md +0 -96
- package/.opencode/agent/ui-ux-developer.md +0 -97
- package/.opencode/command/cook.md +0 -7
- package/.opencode/command/debug.md +0 -10
- package/.opencode/command/fix/ci.md +0 -8
- package/.opencode/command/fix/fast.md +0 -5
- package/.opencode/command/fix/hard.md +0 -7
- package/.opencode/command/fix/test.md +0 -16
- package/.opencode/command/git/cm.md +0 -5
- package/.opencode/command/git/cp.md +0 -4
- package/.opencode/command/plan/ci.md +0 -12
- package/.opencode/command/plan/two.md +0 -13
- package/.opencode/command/plan.md +0 -10
- package/.opencode/command/test.md +0 -7
- package/.opencode/command/watzup.md +0 -8
- package/.releaserc.json +0 -26
- package/.serena/project.yml +0 -68
- package/CHANGELOG.md +0 -62
- package/CLAUDE.md +0 -141
- package/DEPLOYMENT.md +0 -329
- package/Dockerfile +0 -52
- package/QUICKSTART.md +0 -97
- package/bun.lock +0 -1872
- package/bunfig.toml +0 -15
- package/docker-compose.yaml +0 -128
- package/docs/README.md +0 -51
- package/docs/codebase-structure-architecture-code-standards.md +0 -428
- package/docs/codebase-summary.md +0 -321
- package/docs/project-overview-pdr.md +0 -286
- package/docs/project-roadmap.md +0 -494
- package/examples/debugging-session.ts +0 -96
- package/human-mcp.png +0 -0
- package/inspector-wrapper.mjs +0 -33
- package/plans/001-streamable-http-transport-plan.md +0 -905
- package/plans/002-sse-fallback-http-transport-plan.md +0 -161
- package/plans/003-fix-test-infrastructure-and-ci-plan.md +0 -699
- package/plans/003-http-transport-local-file-access-plan.md +0 -880
- package/plans/004-fix-typescript-compilation-errors-plan.md +0 -388
- package/plans/005-comprehensive-test-infrastructure-fix-plan.md +0 -854
- package/plans/templates/bug-fix-template.md +0 -69
- package/plans/templates/feature-implementation-template.md +0 -84
- package/plans/templates/refactor-template.md +0 -82
- package/plans/templates/template-usage-guide.md +0 -58
- package/src/index.ts +0 -49
- package/src/prompts/debugging-prompts.ts +0 -149
- package/src/prompts/index.ts +0 -55
- package/src/resources/documentation.ts +0 -316
- package/src/resources/index.ts +0 -49
- package/src/server.ts +0 -36
- package/src/tools/eyes/index.ts +0 -225
- package/src/tools/eyes/processors/gif.ts +0 -137
- package/src/tools/eyes/processors/image.ts +0 -213
- package/src/tools/eyes/processors/video.ts +0 -135
- package/src/tools/eyes/schemas.ts +0 -51
- package/src/tools/eyes/utils/formatters.ts +0 -126
- package/src/tools/eyes/utils/gemini-client.ts +0 -73
- package/src/transports/http/file-interceptor.ts +0 -134
- package/src/transports/http/middleware.ts +0 -46
- package/src/transports/http/routes.ts +0 -297
- package/src/transports/http/server.ts +0 -116
- package/src/transports/http/session.ts +0 -93
- package/src/transports/http/sse-routes.ts +0 -210
- package/src/transports/index.ts +0 -36
- package/src/transports/stdio.ts +0 -7
- package/src/transports/types.ts +0 -50
- package/src/types/index.ts +0 -41
- package/src/utils/cloudflare-r2.ts +0 -107
- package/src/utils/config.ts +0 -123
- package/src/utils/errors.ts +0 -40
- package/src/utils/logger.ts +0 -49
- package/tests/integration/http-transport-files.test.ts +0 -190
- package/tests/integration/server.test.ts +0 -27
- package/tests/integration/sse-transport.test.ts +0 -142
- package/tests/setup.ts +0 -55
- package/tests/types/api-responses.ts +0 -35
- package/tests/types/test-types.ts +0 -105
- package/tests/unit/cloudflare-r2.test.ts +0 -118
- package/tests/unit/config.test.ts +0 -40
- package/tests/unit/eyes-analyze.test.ts +0 -150
- package/tests/unit/formatters.test.ts +0 -85
- package/tests/unit/sse-routes.test.ts +0 -92
- package/tests/utils/error-scenarios.ts +0 -198
- package/tests/utils/index.ts +0 -3
- package/tests/utils/mock-helpers.ts +0 -99
- package/tests/utils/test-data-generators.ts +0 -217
- package/tests/utils/test-server-manager.ts +0 -172
- 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
|
-
}
|