@goonnguyen/human-mcp 1.2.0 → 1.3.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 (71) hide show
  1. package/.claude/agents/project-manager.md +2 -2
  2. package/.env.example +28 -1
  3. package/.github/workflows/publish.yml +43 -6
  4. package/.opencode/agent/code-reviewer.md +142 -0
  5. package/.opencode/agent/debugger.md +74 -0
  6. package/.opencode/agent/docs-manager.md +119 -0
  7. package/.opencode/agent/git-manager.md +60 -0
  8. package/.opencode/agent/planner-researcher.md +100 -0
  9. package/.opencode/agent/project-manager.md +113 -0
  10. package/.opencode/agent/system-architecture.md +200 -0
  11. package/.opencode/agent/tester.md +96 -0
  12. package/.opencode/agent/ui-ux-developer.md +97 -0
  13. package/.opencode/command/cook.md +7 -0
  14. package/.opencode/command/debug.md +10 -0
  15. package/.opencode/command/fix/ci.md +8 -0
  16. package/.opencode/command/fix/fast.md +5 -0
  17. package/.opencode/command/fix/hard.md +7 -0
  18. package/.opencode/command/fix/test.md +16 -0
  19. package/.opencode/command/git/cm.md +5 -0
  20. package/.opencode/command/git/cp.md +4 -0
  21. package/.opencode/command/plan/ci.md +12 -0
  22. package/.opencode/command/plan/two.md +13 -0
  23. package/.opencode/command/plan.md +10 -0
  24. package/.opencode/command/test.md +7 -0
  25. package/.opencode/command/watzup.md +8 -0
  26. package/CHANGELOG.md +21 -0
  27. package/CLAUDE.md +5 -3
  28. package/QUICKSTART.md +3 -3
  29. package/README.md +551 -20
  30. package/bun.lock +275 -3
  31. package/dist/index.js +71091 -17256
  32. package/docs/README.md +51 -0
  33. package/docs/codebase-structure-architecture-code-standards.md +17 -5
  34. package/docs/project-overview-pdr.md +37 -21
  35. package/docs/project-roadmap.md +494 -0
  36. package/human-mcp.png +0 -0
  37. package/package.json +9 -1
  38. package/plans/002-sse-fallback-http-transport-plan.md +161 -0
  39. package/plans/003-fix-test-infrastructure-and-ci-plan.md +699 -0
  40. package/plans/003-http-transport-local-file-access-plan.md +880 -0
  41. package/plans/004-fix-typescript-compilation-errors-plan.md +388 -0
  42. package/plans/005-comprehensive-test-infrastructure-fix-plan.md +854 -0
  43. package/src/index.ts +2 -0
  44. package/src/tools/eyes/index.ts +7 -7
  45. package/src/tools/eyes/processors/image.ts +90 -0
  46. package/src/transports/http/file-interceptor.ts +134 -0
  47. package/src/transports/http/routes.ts +165 -4
  48. package/src/transports/http/server.ts +64 -14
  49. package/src/transports/http/session.ts +11 -3
  50. package/src/transports/http/sse-routes.ts +210 -0
  51. package/src/transports/index.ts +11 -6
  52. package/src/transports/types.ts +13 -0
  53. package/src/utils/cloudflare-r2.ts +107 -0
  54. package/src/utils/config.ts +26 -0
  55. package/tests/integration/http-transport-files.test.ts +190 -0
  56. package/tests/integration/server.test.ts +4 -1
  57. package/tests/integration/sse-transport.test.ts +142 -0
  58. package/tests/setup.ts +45 -1
  59. package/tests/types/api-responses.ts +35 -0
  60. package/tests/types/test-types.ts +105 -0
  61. package/tests/unit/cloudflare-r2.test.ts +118 -0
  62. package/tests/unit/eyes-analyze.test.ts +150 -0
  63. package/tests/unit/formatters.test.ts +1 -1
  64. package/tests/unit/sse-routes.test.ts +92 -0
  65. package/tests/utils/error-scenarios.ts +198 -0
  66. package/tests/utils/index.ts +3 -0
  67. package/tests/utils/mock-helpers.ts +99 -0
  68. package/tests/utils/test-data-generators.ts +217 -0
  69. package/tests/utils/test-server-manager.ts +172 -0
  70. package/tsconfig.json +1 -1
  71. package/plans/reports/001-from-qa-engineer-to-development-team-test-suite-report.md +0 -188
@@ -0,0 +1,880 @@
1
+ # Plan: Fix Local File Access in HTTP Transport for Claude Desktop with Cloudflare R2 Integration
2
+
3
+ ## Problem Statement
4
+
5
+ The Human MCP server's Vision Analysis Tool cannot read local image files when used with HTTP transport in Claude Desktop, while URL images work correctly. The issue occurs because:
6
+
7
+ 1. **Path Translation Issue**: Claude Desktop transforms local file paths to container-style paths (`/mnt/user-data/uploads/`) when using HTTP transport
8
+ 2. **File Access Limitation**: The HTTP server cannot access files outside its working directory due to security restrictions
9
+ 3. **Missing File Upload Mechanism**: The current HTTP transport doesn't handle file uploads from the client
10
+
11
+ ## Root Cause Analysis
12
+
13
+ ### Current Behavior
14
+ When Claude Desktop uses the HTTP transport with a local file:
15
+ 1. Claude Desktop sends: `source: "/mnt/user-data/uploads/CleanShot_2025-09-13_at_13_07_56_2x.png"`
16
+ 2. The server tries to read this path using `fs.readFile()`
17
+ 3. The file doesn't exist at that path on the server's filesystem
18
+ 4. Error: `ENOENT: no such file or directory`
19
+
20
+ ### Why URLs Work
21
+ - URLs are fetched directly using `fetch()` API
22
+ - No filesystem access required
23
+ - Data is downloaded and processed in memory
24
+
25
+ ## Solution Design
26
+
27
+ ### Approach 1: Cloudflare R2 Storage Integration (Recommended)
28
+ Automatically upload local files to Cloudflare R2 and use CDN URLs.
29
+
30
+ **Pros:**
31
+ - Scalable and reliable cloud storage
32
+ - Fast CDN delivery worldwide
33
+ - No base64 overhead
34
+ - Files accessible via public URLs
35
+ - Automatic file management
36
+ - Works with all file sizes
37
+
38
+ **Cons:**
39
+ - Requires Cloudflare account setup
40
+ - Network dependency for uploads
41
+ - Storage costs for large volumes
42
+
43
+ ### Approach 2: File Upload via Base64
44
+ Transform local files to base64 data URIs before sending to the server.
45
+
46
+ **Pros:**
47
+ - Works with existing server code
48
+ - No external dependencies
49
+ - Secure - no filesystem access required
50
+
51
+ **Cons:**
52
+ - Increased payload size (~33% overhead)
53
+ - Memory usage for large files
54
+ - Size limitations
55
+
56
+ ### Approach 3: Hybrid Approach
57
+ Combine Cloudflare R2 for large files and base64 for small files.
58
+
59
+ **Pros:**
60
+ - Optimal for all file sizes
61
+ - Fallback mechanism
62
+ - Best performance
63
+
64
+ **Cons:**
65
+ - More complex implementation
66
+ - Requires both systems
67
+
68
+ ## Recommended Solution: Cloudflare R2 Integration with Automatic Upload
69
+
70
+ ### Implementation Plan
71
+
72
+ #### Phase 1: Cloudflare R2 Integration
73
+
74
+ 1. **Install Dependencies**
75
+ ```bash
76
+ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner mime-types uuid
77
+ ```
78
+
79
+ 2. **Create Cloudflare R2 Client** (`src/utils/cloudflare-r2.ts`)
80
+ ```typescript
81
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
82
+ import { v4 as uuidv4 } from 'uuid';
83
+ import mime from 'mime-types';
84
+ import { logger } from './logger.js';
85
+
86
+ export class CloudflareR2Client {
87
+ private s3Client: S3Client;
88
+ private bucketName: string;
89
+ private baseUrl: string;
90
+
91
+ constructor() {
92
+ const config = {
93
+ region: 'auto',
94
+ endpoint: process.env.CLOUDFLARE_CDN_ENDPOINT_URL,
95
+ credentials: {
96
+ accessKeyId: process.env.CLOUDFLARE_CDN_ACCESS_KEY!,
97
+ secretAccessKey: process.env.CLOUDFLARE_CDN_SECRET_KEY!,
98
+ },
99
+ };
100
+
101
+ this.s3Client = new S3Client(config);
102
+ this.bucketName = process.env.CLOUDFLARE_CDN_BUCKET_NAME!;
103
+ this.baseUrl = process.env.CLOUDFLARE_CDN_BASE_URL!;
104
+ }
105
+
106
+ async uploadFile(buffer: Buffer, originalName: string): Promise<string> {
107
+ try {
108
+ const fileExtension = originalName.split('.').pop() || 'bin';
109
+ const mimeType = mime.lookup(originalName) || 'application/octet-stream';
110
+ const key = `human-mcp/${uuidv4()}.${fileExtension}`;
111
+
112
+ const command = new PutObjectCommand({
113
+ Bucket: this.bucketName,
114
+ Key: key,
115
+ Body: buffer,
116
+ ContentType: mimeType,
117
+ Metadata: {
118
+ originalName: originalName,
119
+ uploadedAt: new Date().toISOString(),
120
+ source: 'human-mcp-http-transport'
121
+ }
122
+ });
123
+
124
+ await this.s3Client.send(command);
125
+
126
+ const publicUrl = `${this.baseUrl}/${key}`;
127
+ logger.info(`File uploaded to Cloudflare R2: ${publicUrl}`);
128
+
129
+ return publicUrl;
130
+ } catch (error) {
131
+ logger.error('Failed to upload to Cloudflare R2:', error);
132
+ throw new Error(`Failed to upload file: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ async uploadBase64(base64Data: string, mimeType: string, originalName?: string): Promise<string> {
137
+ const buffer = Buffer.from(base64Data, 'base64');
138
+ const extension = mimeType.split('/')[1] || 'bin';
139
+ const fileName = originalName || `upload-${Date.now()}.${extension}`;
140
+
141
+ return this.uploadFile(buffer, fileName);
142
+ }
143
+ }
144
+
145
+ // Singleton instance
146
+ export const cloudflareR2 = new CloudflareR2Client();
147
+ ```
148
+
149
+ 3. **Update File Path Detection with Auto-Upload** (`src/tools/eyes/processors/image.ts`)
150
+ ```typescript
151
+ import { cloudflareR2 } from '@/utils/cloudflare-r2.js';
152
+
153
+ async function loadImage(source: string, fetchTimeout?: number): Promise<{ imageData: string; mimeType: string }> {
154
+ // Detect Claude Desktop virtual paths and auto-upload to Cloudflare
155
+ if (source.startsWith('/mnt/user-data/') || source.startsWith('/mnt/')) {
156
+ logger.info(`Detected Claude Desktop virtual path: ${source}`);
157
+
158
+ // Extract filename from path
159
+ const filename = source.split('/').pop() || 'upload.jpg';
160
+
161
+ // Try to read from a temporary upload directory (if middleware saved it)
162
+ const tempPath = `/tmp/mcp-uploads/${filename}`;
163
+
164
+ try {
165
+ // Check if file was temporarily saved by middleware
166
+ if (await fs.access(tempPath).then(() => true).catch(() => false)) {
167
+ const buffer = await fs.readFile(tempPath);
168
+
169
+ // Upload to Cloudflare R2
170
+ const publicUrl = await cloudflareR2.uploadFile(buffer, filename);
171
+
172
+ // Clean up temp file
173
+ await fs.unlink(tempPath).catch(() => {});
174
+
175
+ // Now fetch from the CDN URL
176
+ return loadImage(publicUrl, fetchTimeout);
177
+ }
178
+ } catch (error) {
179
+ logger.warn(`Could not process temp file: ${error.message}`);
180
+ }
181
+
182
+ // If no temp file, provide helpful error with Cloudflare upload instructions
183
+ throw new ProcessingError(
184
+ `Local file access not supported via HTTP transport.\n` +
185
+ `The file path "${source}" is not accessible.\n\n` +
186
+ `Solutions:\n` +
187
+ `1. Upload your file to Cloudflare R2 first using the /mcp/upload endpoint\n` +
188
+ `2. Use a public URL instead of a local file path\n` +
189
+ `3. Convert the image to a base64 data URI\n` +
190
+ `4. Use the stdio transport for local file access`
191
+ );
192
+ }
193
+
194
+ // Existing base64 handling
195
+ if (source.startsWith('data:image/')) {
196
+ const [header, data] = source.split(',');
197
+ if (!header || !data) {
198
+ throw new ProcessingError("Invalid base64 image format");
199
+ }
200
+ const mimeMatch = header.match(/data:(image\/[^;]+)/);
201
+ if (!mimeMatch || !mimeMatch[1]) {
202
+ throw new ProcessingError("Invalid base64 image format");
203
+ }
204
+
205
+ // Optional: For large base64 images, upload to Cloudflare R2
206
+ if (data.length > 1024 * 1024) { // > 1MB base64
207
+ logger.info('Large base64 image detected, uploading to Cloudflare R2');
208
+ const publicUrl = await cloudflareR2.uploadBase64(data, mimeMatch[1]);
209
+ return loadImage(publicUrl, fetchTimeout);
210
+ }
211
+
212
+ return {
213
+ imageData: data,
214
+ mimeType: mimeMatch[1]
215
+ };
216
+ }
217
+
218
+ // Existing URL handling
219
+ if (source.startsWith('http://') || source.startsWith('https://')) {
220
+ // ... existing code
221
+ }
222
+
223
+ // Local file handling - auto-upload to Cloudflare for HTTP transport
224
+ try {
225
+ const stats = await fs.stat(source);
226
+ if (!stats.isFile()) {
227
+ throw new ProcessingError(`Path is not a file: ${source}`);
228
+ }
229
+
230
+ // If using HTTP transport, upload to Cloudflare
231
+ if (process.env.TRANSPORT_TYPE === 'http') {
232
+ logger.info(`HTTP transport detected, uploading local file to Cloudflare R2: ${source}`);
233
+ const buffer = await fs.readFile(source);
234
+ const filename = source.split('/').pop() || 'upload.jpg';
235
+ const publicUrl = await cloudflareR2.uploadFile(buffer, filename);
236
+
237
+ // Fetch from CDN
238
+ return loadImage(publicUrl, fetchTimeout);
239
+ }
240
+
241
+ // For stdio transport, process locally as before
242
+ const buffer = await fs.readFile(source);
243
+ const processedImage = await sharp(buffer)
244
+ .resize(1024, 1024, { fit: 'inside', withoutEnlargement: true })
245
+ .jpeg({ quality: 85 })
246
+ .toBuffer();
247
+
248
+ return {
249
+ imageData: processedImage.toString('base64'),
250
+ mimeType: 'image/jpeg'
251
+ };
252
+ } catch (error) {
253
+ if (error.code === 'ENOENT') {
254
+ throw new ProcessingError(
255
+ `File not found: ${source}\n` +
256
+ `When using HTTP transport, files are automatically uploaded to Cloudflare R2.`
257
+ );
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+ ```
263
+
264
+ 4. **Add File Upload Endpoint** (`src/transports/http/routes.ts`)
265
+ ```typescript
266
+ import multer from 'multer';
267
+ import { cloudflareR2 } from '@/utils/cloudflare-r2.js';
268
+
269
+ // Configure multer for memory storage
270
+ const upload = multer({
271
+ storage: multer.memoryStorage(),
272
+ limits: {
273
+ fileSize: 100 * 1024 * 1024, // 100MB limit
274
+ },
275
+ fileFilter: (req, file, cb) => {
276
+ // Accept images, videos, and GIFs
277
+ if (file.mimetype.startsWith('image/') ||
278
+ file.mimetype.startsWith('video/') ||
279
+ file.mimetype === 'image/gif') {
280
+ cb(null, true);
281
+ } else {
282
+ cb(new Error('Invalid file type. Only images and videos are allowed.'));
283
+ }
284
+ }
285
+ });
286
+
287
+ // POST /mcp/upload - Handle file uploads to Cloudflare R2
288
+ router.post('/upload', upload.single('file'), async (req, res) => {
289
+ try {
290
+ if (!req.file) {
291
+ res.status(400).json({
292
+ jsonrpc: '2.0',
293
+ error: {
294
+ code: -32600,
295
+ message: 'No file uploaded'
296
+ },
297
+ id: null
298
+ });
299
+ return;
300
+ }
301
+
302
+ // Upload to Cloudflare R2
303
+ const publicUrl = await cloudflareR2.uploadFile(
304
+ req.file.buffer,
305
+ req.file.originalname
306
+ );
307
+
308
+ res.json({
309
+ jsonrpc: '2.0',
310
+ result: {
311
+ success: true,
312
+ url: publicUrl,
313
+ originalName: req.file.originalname,
314
+ size: req.file.size,
315
+ mimeType: req.file.mimetype,
316
+ message: 'File uploaded successfully to Cloudflare R2'
317
+ },
318
+ id: req.body?.id || null
319
+ });
320
+ } catch (error) {
321
+ logger.error('Upload error:', error);
322
+ res.status(500).json({
323
+ jsonrpc: '2.0',
324
+ error: {
325
+ code: -32603,
326
+ message: `Failed to upload file: ${error.message}`
327
+ },
328
+ id: req.body?.id || null
329
+ });
330
+ }
331
+ });
332
+
333
+ // POST /mcp/upload-base64 - Handle base64 uploads
334
+ router.post('/upload-base64', express.json({ limit: '100mb' }), async (req, res) => {
335
+ try {
336
+ const { data, mimeType, filename } = req.body;
337
+
338
+ if (!data || !mimeType) {
339
+ res.status(400).json({
340
+ jsonrpc: '2.0',
341
+ error: {
342
+ code: -32600,
343
+ message: 'Missing required fields: data and mimeType'
344
+ },
345
+ id: null
346
+ });
347
+ return;
348
+ }
349
+
350
+ // Remove data URI prefix if present
351
+ const base64Data = data.replace(/^data:.*?;base64,/, '');
352
+
353
+ // Upload to Cloudflare R2
354
+ const publicUrl = await cloudflareR2.uploadBase64(
355
+ base64Data,
356
+ mimeType,
357
+ filename
358
+ );
359
+
360
+ res.json({
361
+ jsonrpc: '2.0',
362
+ result: {
363
+ success: true,
364
+ url: publicUrl,
365
+ message: 'Base64 data uploaded successfully to Cloudflare R2'
366
+ },
367
+ id: req.body?.id || null
368
+ });
369
+ } catch (error) {
370
+ logger.error('Base64 upload error:', error);
371
+ res.status(500).json({
372
+ jsonrpc: '2.0',
373
+ error: {
374
+ code: -32603,
375
+ message: `Failed to upload base64 data: ${error.message}`
376
+ },
377
+ id: req.body?.id || null
378
+ });
379
+ }
380
+ });
381
+ ```
382
+
383
+ 5. **Add Configuration Validation** (`src/utils/config.ts`)
384
+ ```typescript
385
+ // Add Cloudflare R2 configuration
386
+ cloudflare: z.object({
387
+ projectName: z.string().optional().default('human-mcp'),
388
+ bucketName: z.string(),
389
+ accessKey: z.string(),
390
+ secretKey: z.string(),
391
+ endpointUrl: z.string().url(),
392
+ baseUrl: z.string().url(),
393
+ }).optional(),
394
+ ```
395
+
396
+ #### Phase 2: Client Configuration Documentation
397
+
398
+ 1. **Update README.md** with Cloudflare R2 integration:
399
+ ```markdown
400
+ ### Using Local Files with HTTP Transport
401
+
402
+ When using HTTP transport (common with Claude Desktop), local files are automatically uploaded to Cloudflare R2:
403
+
404
+ #### Automatic Upload (Default Behavior)
405
+ When you provide a local file path, the server automatically:
406
+ 1. Detects the local file path
407
+ 2. Uploads it to Cloudflare R2
408
+ 3. Returns the CDN URL for processing
409
+ 4. Uses the fast Cloudflare CDN for delivery
410
+
411
+ #### Manual Upload Options
412
+
413
+ ##### Option 1: Upload File Directly
414
+ ```bash
415
+ # Upload file to Cloudflare R2 and get CDN URL
416
+ curl -X POST http://localhost:3000/mcp/upload \
417
+ -F "file=@/path/to/image.png" \
418
+ -H "Authorization: Bearer your_secret"
419
+
420
+ # Response:
421
+ {
422
+ "result": {
423
+ "success": true,
424
+ "url": "https://cdn.gotest.app/human-mcp/abc123.png",
425
+ "originalName": "image.png",
426
+ "size": 102400,
427
+ "mimeType": "image/png"
428
+ }
429
+ }
430
+ ```
431
+
432
+ ##### Option 2: Upload Base64 Data
433
+ ```bash
434
+ # Upload base64 data to Cloudflare R2
435
+ curl -X POST http://localhost:3000/mcp/upload-base64 \
436
+ -H "Content-Type: application/json" \
437
+ -H "Authorization: Bearer your_secret" \
438
+ -d '{
439
+ "data": "iVBORw0KGgoAAAANSUhEUgA...",
440
+ "mimeType": "image/png",
441
+ "filename": "screenshot.png"
442
+ }'
443
+ ```
444
+
445
+ ##### Option 3: Use Existing CDN URLs
446
+ If your files are already hosted, use the public URL directly:
447
+ - Cloudflare R2: `https://cdn.gotest.app/path/to/file.jpg`
448
+ - Other CDNs: Any publicly accessible URL
449
+
450
+ #### Configuration
451
+ Add these to your `.env` file:
452
+ ```env
453
+ # Cloudflare R2 Configuration
454
+ CLOUDFLARE_CDN_PROJECT_NAME=human-mcp
455
+ CLOUDFLARE_CDN_BUCKET_NAME=digitop
456
+ CLOUDFLARE_CDN_ACCESS_KEY=your_access_key
457
+ CLOUDFLARE_CDN_SECRET_KEY=your_secret_key
458
+ CLOUDFLARE_CDN_ENDPOINT_URL=https://your-account.r2.cloudflarestorage.com
459
+ CLOUDFLARE_CDN_BASE_URL=https://cdn.gotest.app
460
+ ```
461
+
462
+ #### Benefits of Cloudflare R2 Integration
463
+ - **Fast Global Delivery**: Files served from Cloudflare's global CDN
464
+ - **Automatic Handling**: No manual conversion needed
465
+ - **Large File Support**: Handle files up to 100MB
466
+ - **Persistent URLs**: Files remain accessible for future reference
467
+ - **Cost Effective**: Cloudflare R2 offers competitive pricing
468
+ ```
469
+
470
+ 2. **Add Claude Desktop Specific Configuration**:
471
+ ```json
472
+ {
473
+ "mcpServers": {
474
+ "human-mcp-http": {
475
+ "command": "node",
476
+ "args": ["path/to/http-wrapper.js"],
477
+ "env": {
478
+ "GOOGLE_GEMINI_API_KEY": "your_key",
479
+ "TRANSPORT_TYPE": "http",
480
+ "HTTP_PORT": "3000",
481
+ "AUTO_CONVERT_FILES": "true"
482
+ }
483
+ }
484
+ }
485
+ }
486
+ ```
487
+
488
+ #### Phase 3: Middleware for Automatic File Handling
489
+
490
+ Create `src/transports/http/file-interceptor.ts`:
491
+ ```typescript
492
+ import { Request, Response, NextFunction } from 'express';
493
+ import { cloudflareR2 } from '@/utils/cloudflare-r2.js';
494
+ import { logger } from '@/utils/logger.js';
495
+ import fs from 'fs/promises';
496
+ import path from 'path';
497
+
498
+ export async function fileInterceptorMiddleware(
499
+ req: Request,
500
+ res: Response,
501
+ next: NextFunction
502
+ ) {
503
+ // Only intercept tool calls with file paths
504
+ if (req.body?.method === 'tools/call' && req.body?.params?.arguments) {
505
+ const args = req.body.params.arguments;
506
+
507
+ // Check for source fields that might contain file paths
508
+ const fileFields = ['source', 'source1', 'source2', 'path', 'filePath'];
509
+
510
+ for (const field of fileFields) {
511
+ if (args[field] && typeof args[field] === 'string') {
512
+ const filePath = args[field];
513
+
514
+ // Detect Claude Desktop virtual paths
515
+ if (filePath.startsWith('/mnt/user-data/') || filePath.startsWith('/mnt/')) {
516
+ logger.info(`Intercepting Claude Desktop virtual path: ${filePath}`);
517
+
518
+ try {
519
+ // Extract filename
520
+ const filename = path.basename(filePath);
521
+
522
+ // Check if we have a temporary file saved by Claude Desktop
523
+ const tempPath = path.join('/tmp/claude-uploads', filename);
524
+
525
+ if (await fs.access(tempPath).then(() => true).catch(() => false)) {
526
+ // File exists in temp, upload to Cloudflare
527
+ const buffer = await fs.readFile(tempPath);
528
+ const publicUrl = await cloudflareR2.uploadFile(buffer, filename);
529
+
530
+ // Replace the virtual path with CDN URL
531
+ args[field] = publicUrl;
532
+
533
+ // Clean up temp file
534
+ await fs.unlink(tempPath).catch(() => {});
535
+
536
+ logger.info(`Replaced virtual path with CDN URL: ${publicUrl}`);
537
+ } else {
538
+ // No temp file, try to extract from request if it's base64
539
+ // This handles cases where Claude Desktop might send base64 inline
540
+ if (req.body.params.fileData && req.body.params.fileData[field]) {
541
+ const base64Data = req.body.params.fileData[field];
542
+ const mimeType = req.body.params.fileMimeTypes?.[field] || 'image/jpeg';
543
+
544
+ const publicUrl = await cloudflareR2.uploadBase64(
545
+ base64Data,
546
+ mimeType,
547
+ filename
548
+ );
549
+
550
+ args[field] = publicUrl;
551
+ logger.info(`Uploaded inline base64 to CDN: ${publicUrl}`);
552
+ } else {
553
+ // Provide helpful error response
554
+ logger.warn(`Cannot access virtual path: ${filePath}`);
555
+ return res.status(400).json({
556
+ jsonrpc: '2.0',
557
+ error: {
558
+ code: -32602,
559
+ message: 'File not accessible via HTTP transport',
560
+ data: {
561
+ path: filePath,
562
+ suggestions: [
563
+ 'Upload the file using the /mcp/upload endpoint first',
564
+ 'Use a public URL instead of a local file path',
565
+ 'Convert the image to a base64 data URI',
566
+ 'Switch to stdio transport for local file access'
567
+ ]
568
+ }
569
+ },
570
+ id: req.body.id
571
+ });
572
+ }
573
+ }
574
+ } catch (error) {
575
+ logger.error(`Error processing virtual path: ${error}`);
576
+ return res.status(500).json({
577
+ jsonrpc: '2.0',
578
+ error: {
579
+ code: -32603,
580
+ message: `Failed to process file: ${error.message}`
581
+ },
582
+ id: req.body.id
583
+ });
584
+ }
585
+ }
586
+
587
+ // Handle regular local paths when in HTTP mode
588
+ else if (!filePath.startsWith('http') && !filePath.startsWith('data:')) {
589
+ if (process.env.TRANSPORT_TYPE === 'http') {
590
+ try {
591
+ // Check if file exists locally
592
+ await fs.access(filePath);
593
+
594
+ // Upload to Cloudflare R2
595
+ const buffer = await fs.readFile(filePath);
596
+ const filename = path.basename(filePath);
597
+ const publicUrl = await cloudflareR2.uploadFile(buffer, filename);
598
+
599
+ // Replace local path with CDN URL
600
+ args[field] = publicUrl;
601
+
602
+ logger.info(`Auto-uploaded local file to CDN: ${publicUrl}`);
603
+ } catch (error) {
604
+ if (error.code === 'ENOENT') {
605
+ logger.warn(`Local file not found: ${filePath}`);
606
+ }
607
+ // Continue without modification if file doesn't exist
608
+ }
609
+ }
610
+ }
611
+ }
612
+ }
613
+ }
614
+
615
+ next();
616
+ }
617
+ ```
618
+
619
+ 4. **Update HTTP Server to Use Middleware** (`src/transports/http/server.ts`)
620
+ ```typescript
621
+ import { fileInterceptorMiddleware } from './file-interceptor.js';
622
+
623
+ // Add before route handlers
624
+ app.use(fileInterceptorMiddleware);
625
+
626
+ // Existing routes...
627
+ app.use('/mcp', routes);
628
+ ```
629
+
630
+ #### Phase 4: Testing Strategy
631
+
632
+ 1. **Unit Tests** (`tests/unit/cloudflare-r2.test.ts`):
633
+ ```typescript
634
+ import { CloudflareR2Client } from '@/utils/cloudflare-r2';
635
+
636
+ describe('Cloudflare R2 Integration', () => {
637
+ let client: CloudflareR2Client;
638
+
639
+ beforeAll(() => {
640
+ client = new CloudflareR2Client();
641
+ });
642
+
643
+ it('should upload buffer to Cloudflare R2', async () => {
644
+ const buffer = Buffer.from('test image data');
645
+ const url = await client.uploadFile(buffer, 'test.jpg');
646
+
647
+ expect(url).toMatch(/^https:\/\/cdn\.gotest\.app\/human-mcp\//);
648
+ });
649
+
650
+ it('should upload base64 to Cloudflare R2', async () => {
651
+ const base64 = Buffer.from('test').toString('base64');
652
+ const url = await client.uploadBase64(base64, 'image/png', 'test.png');
653
+
654
+ expect(url).toMatch(/^https:\/\/cdn\.gotest\.app\/human-mcp\//);
655
+ });
656
+
657
+ it('should handle upload errors gracefully', async () => {
658
+ const invalidBuffer = null as any;
659
+
660
+ await expect(client.uploadFile(invalidBuffer, 'test.jpg'))
661
+ .rejects.toThrow('Failed to upload file');
662
+ });
663
+ });
664
+ ```
665
+
666
+ 2. **Integration Tests** (`tests/integration/http-transport-files.test.ts`):
667
+ ```typescript
668
+ describe('HTTP Transport File Handling', () => {
669
+ it('should auto-upload Claude Desktop virtual paths to Cloudflare', async () => {
670
+ const request = {
671
+ jsonrpc: '2.0',
672
+ method: 'tools/call',
673
+ params: {
674
+ name: 'eyes_analyze',
675
+ arguments: {
676
+ source: '/mnt/user-data/uploads/test.png',
677
+ type: 'image'
678
+ }
679
+ }
680
+ };
681
+
682
+ const response = await sendRequest(request);
683
+
684
+ // Should either upload successfully or provide helpful error
685
+ if (response.result) {
686
+ expect(response.result).toBeDefined();
687
+ } else {
688
+ expect(response.error.data.suggestions).toContain(
689
+ 'Upload the file using the /mcp/upload endpoint first'
690
+ );
691
+ }
692
+ });
693
+
694
+ it('should handle file upload endpoint', async () => {
695
+ const response = await request(app)
696
+ .post('/mcp/upload')
697
+ .attach('file', 'test/fixtures/test.png');
698
+
699
+ expect(response.body.result.url).toMatch(/^https:\/\/cdn\.gotest\.app\//);
700
+ expect(response.body.result.success).toBe(true);
701
+ });
702
+
703
+ it('should handle base64 upload endpoint', async () => {
704
+ const base64Data = Buffer.from('test image').toString('base64');
705
+
706
+ const response = await request(app)
707
+ .post('/mcp/upload-base64')
708
+ .send({
709
+ data: base64Data,
710
+ mimeType: 'image/png',
711
+ filename: 'test.png'
712
+ });
713
+
714
+ expect(response.body.result.url).toMatch(/^https:\/\/cdn\.gotest\.app\//);
715
+ });
716
+
717
+ it('should auto-upload local files in HTTP mode', async () => {
718
+ process.env.TRANSPORT_TYPE = 'http';
719
+
720
+ const result = await processImage(
721
+ model,
722
+ './test/fixtures/local-image.png',
723
+ options
724
+ );
725
+
726
+ // Should have uploaded to Cloudflare and processed from CDN
727
+ expect(result.metadata).toBeDefined();
728
+ });
729
+ });
730
+ ```
731
+
732
+ ## Implementation Checklist
733
+
734
+ ### Immediate Actions (Phase 1)
735
+ - [ ] Install AWS SDK S3 client and dependencies
736
+ - [ ] Create Cloudflare R2 client utility class
737
+ - [ ] Add Cloudflare configuration to environment variables
738
+ - [ ] Update config validation schema
739
+
740
+ ### Short-term (Phase 2)
741
+ - [ ] Update `loadImage()` function to auto-upload to Cloudflare
742
+ - [ ] Implement file upload endpoint `/mcp/upload`
743
+ - [ ] Implement base64 upload endpoint `/mcp/upload-base64`
744
+ - [ ] Add file interceptor middleware for automatic handling
745
+ - [ ] Update error messages with Cloudflare upload instructions
746
+
747
+ ### Medium-term (Phase 3)
748
+ - [ ] Add file caching to avoid re-uploading same files
749
+ - [ ] Implement file cleanup/retention policies
750
+ - [ ] Add progress tracking for large uploads
751
+ - [ ] Create upload status endpoint
752
+
753
+ ### Long-term (Phase 4)
754
+ - [ ] Add support for video and GIF uploads
755
+ - [ ] Implement chunked upload for very large files
756
+ - [ ] Add file compression before upload
757
+ - [ ] Create dashboard for managing uploaded files
758
+
759
+ ## Security Considerations
760
+
761
+ 1. **Cloudflare R2 Security**:
762
+ - Use secure access keys and never expose them
763
+ - Implement proper CORS policies on the bucket
764
+ - Set appropriate ACLs for uploaded files
765
+
766
+ 2. **Upload Validation**:
767
+ - Enforce file size limits (100MB default)
768
+ - Validate MIME types strictly
769
+ - Scan for malicious content if needed
770
+
771
+ 3. **Access Control**:
772
+ - Require authentication for upload endpoints
773
+ - Implement rate limiting for uploads
774
+ - Log all upload activities
775
+
776
+ 4. **Data Privacy**:
777
+ - Consider file encryption for sensitive content
778
+ - Implement retention policies
779
+ - Provide deletion capabilities
780
+
781
+ ## Performance Optimizations
782
+
783
+ 1. **Cloudflare CDN Benefits**:
784
+ - Global edge caching for fast delivery
785
+ - Automatic image optimization
786
+ - WebP conversion for supported browsers
787
+ - Bandwidth savings through compression
788
+
789
+ 2. **Upload Optimizations**:
790
+ - Parallel uploads for multiple files
791
+ - Resume capability for interrupted uploads
792
+ - Deduplication based on file hash
793
+
794
+ 3. **Processing Optimizations**:
795
+ - Process images directly from CDN URLs
796
+ - Skip re-upload for already uploaded files
797
+ - Use Cloudflare Workers for on-the-fly transformations
798
+
799
+ ## Alternative Solutions
800
+
801
+ ### Using stdio Transport
802
+ For users who need direct local file access without cloud uploads:
803
+ ```json
804
+ {
805
+ "mcpServers": {
806
+ "human-mcp": {
807
+ "command": "npx",
808
+ "args": ["@goonnguyen/human-mcp"],
809
+ "env": {
810
+ "GOOGLE_GEMINI_API_KEY": "key",
811
+ "TRANSPORT_TYPE": "stdio"
812
+ }
813
+ }
814
+ }
815
+ }
816
+ ```
817
+
818
+ ### Pre-uploading to Cloudflare R2
819
+ Users can pre-upload files using the provided endpoints:
820
+ ```bash
821
+ # Upload script
822
+ #!/bin/bash
823
+ for file in *.png; do
824
+ curl -X POST http://localhost:3000/mcp/upload \
825
+ -F "file=@$file" \
826
+ -H "Authorization: Bearer $MCP_SECRET"
827
+ done
828
+ ```
829
+
830
+ ### Using Existing CDN URLs
831
+ If files are already hosted on Cloudflare or other CDNs, use the URLs directly without re-uploading.
832
+
833
+ ## Success Metrics
834
+
835
+ 1. **Error Resolution**: Eliminate "file not found" errors for Claude Desktop users
836
+ 2. **Upload Performance**: Files uploaded to Cloudflare R2 in under 3 seconds
837
+ 3. **CDN Performance**: Image delivery under 100ms from edge locations
838
+ 4. **User Experience**: Seamless file handling without manual intervention
839
+ 5. **Reliability**: 99.9% upload success rate
840
+ 6. **Cost Efficiency**: Under $0.015 per GB stored on Cloudflare R2
841
+
842
+ ## Rollout Plan
843
+
844
+ 1. **Day 1-2**:
845
+ - Set up Cloudflare R2 client
846
+ - Implement upload endpoints
847
+ - Add configuration validation
848
+
849
+ 2. **Day 3-4**:
850
+ - Update image processors with auto-upload
851
+ - Add file interceptor middleware
852
+ - Test with Claude Desktop
853
+
854
+ 3. **Day 5-6**:
855
+ - Add comprehensive error handling
856
+ - Update documentation
857
+ - Create usage examples
858
+
859
+ 4. **Day 7**:
860
+ - Deploy to production
861
+ - Monitor upload metrics
862
+ - Gather user feedback
863
+
864
+ ## Conclusion
865
+
866
+ The Cloudflare R2 integration provides a robust, scalable solution for handling local files in HTTP transport. By automatically uploading files to Cloudflare's global CDN, we eliminate file access issues while providing superior performance and reliability. This approach transforms a limitation into an advantage, giving users faster file processing through Cloudflare's edge network.
867
+
868
+ ### Key Benefits:
869
+ - **Zero Configuration for Users**: Automatic file handling without manual steps
870
+ - **Global Performance**: Files served from Cloudflare's 300+ edge locations
871
+ - **Cost Effective**: R2's competitive pricing with no egress fees
872
+ - **Future Proof**: Scalable solution that grows with usage
873
+ - **Enhanced Security**: Files isolated from server filesystem
874
+
875
+ ### Next Steps:
876
+ 1. Implement the Cloudflare R2 client
877
+ 2. Update file processors with auto-upload logic
878
+ 3. Add comprehensive testing
879
+ 4. Deploy and monitor performance
880
+ 5. Gather user feedback for improvements