mcp-inflight 0.2.3 → 0.2.4

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 (3) hide show
  1. package/README.md +58 -1
  2. package/dist/index.js +96 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,39 @@ The MCP server provides these tools:
49
49
 
50
50
  ## Supported Projects
51
51
 
52
- Next.js, Vite, Create React App, Node.js, and static HTML.
52
+ - **Next.js** (App Router and Pages Router)
53
+ - **Vite** (React, Vue, Svelte, etc.)
54
+ - **Create React App**
55
+ - **Node.js**
56
+ - **Static HTML**
57
+
58
+ ## Features
59
+
60
+ ### Binary File Support
61
+
62
+ The MCP server automatically handles binary files including:
63
+
64
+ - **Images**: PNG, JPG, JPEG, GIF, WebP, ICO, SVG, BMP, TIFF, AVIF
65
+ - **Fonts**: WOFF, WOFF2, TTF, EOT, OTF
66
+ - **Media**: MP3, MP4, WebM, OGG, WAV
67
+ - **Other**: PDF, ZIP
68
+
69
+ Binary files are base64 encoded during upload and properly decoded in the sandbox, ensuring your `/public` folder assets work correctly.
70
+
71
+ ### Monorepo Support
72
+
73
+ Automatically detects and bundles workspace dependencies for monorepo projects using pnpm, npm, or yarn workspaces.
74
+
75
+ ### Automatic Patching
76
+
77
+ The server automatically patches configuration files for CodeSandbox compatibility:
78
+
79
+ - **Vite**: Adds `allowedHosts` for CodeSandbox domains
80
+ - **Next.js**: Configures CSP headers to allow iframe embedding
81
+
82
+ ### Large Project Support
83
+
84
+ Projects over 3MB are automatically chunked into smaller uploads to handle Vercel's serverless function body size limits.
53
85
 
54
86
  ## Slash Commands (Optional)
55
87
 
@@ -80,6 +112,31 @@ cp packages/mcp-inflight/commands/*.md ~/.claude/commands/
80
112
  | `/share` | Share a project to InFlight (shortcut for the share tool) |
81
113
  | `/inflight` | Menu for managing prototypes: list, sync, delete, login, logout |
82
114
 
115
+ ## Troubleshooting
116
+
117
+ ### Server Startup Timeout
118
+
119
+ If you see "Timeout: Waiting for server to start took longer than 120s", this usually means:
120
+
121
+ 1. The project is large and takes a while to start
122
+ 2. There's an error in the dev server startup
123
+
124
+ The deployment logs will show the exact command being used. Try running it locally to verify it works.
125
+
126
+ ### Missing Images or Assets
127
+
128
+ If images from your `/public` folder aren't loading, ensure:
129
+
130
+ 1. The file extension is in the supported binary file list
131
+ 2. The file path is correct (paths are relative to project root)
132
+
133
+ ### Authentication Issues
134
+
135
+ If you're getting authentication errors:
136
+
137
+ 1. Run the `login` tool to re-authenticate
138
+ 2. Check that your InFlight account is active
139
+
83
140
  ## License
84
141
 
85
142
  MIT
package/dist/index.js CHANGED
@@ -496,6 +496,52 @@ function detectProjectType(projectPath) {
496
496
  // src/utils/file-utils.ts
497
497
  import * as fs2 from "fs";
498
498
  import * as path2 from "path";
499
+ var BINARY_EXTENSIONS = [
500
+ // Images
501
+ ".png",
502
+ ".jpg",
503
+ ".jpeg",
504
+ ".gif",
505
+ ".webp",
506
+ ".ico",
507
+ ".svg",
508
+ ".bmp",
509
+ ".tiff",
510
+ ".avif",
511
+ // Fonts
512
+ ".woff",
513
+ ".woff2",
514
+ ".ttf",
515
+ ".eot",
516
+ ".otf",
517
+ // Other binary
518
+ ".pdf",
519
+ ".zip",
520
+ ".mp3",
521
+ ".mp4",
522
+ ".webm",
523
+ ".ogg",
524
+ ".wav"
525
+ ];
526
+ function isBinaryFile(filename) {
527
+ const ext = path2.extname(filename).toLowerCase();
528
+ return BINARY_EXTENSIONS.includes(ext);
529
+ }
530
+ function getFileContent(file) {
531
+ return typeof file === "string" ? file : file.content;
532
+ }
533
+ function getFileSize(file) {
534
+ return getFileContent(file).length;
535
+ }
536
+ function getTextContent(file) {
537
+ if (typeof file === "string") {
538
+ return file;
539
+ }
540
+ if (file.encoding === "utf-8") {
541
+ return file.content;
542
+ }
543
+ return void 0;
544
+ }
499
545
  var ALWAYS_IGNORE = [
500
546
  "node_modules",
501
547
  ".git",
@@ -536,8 +582,15 @@ function readProjectFiles(projectPath, relativePath = "", includeEnvFiles = fals
536
582
  } else if (entry.isFile()) {
537
583
  const filePath = path2.join(projectPath, entryRelativePath);
538
584
  try {
539
- const content = fs2.readFileSync(filePath, "utf-8");
540
- files[entryRelativePath] = content;
585
+ if (isBinaryFile(entry.name)) {
586
+ const buffer = fs2.readFileSync(filePath);
587
+ const content = buffer.toString("base64");
588
+ console.log(`[MCP] Read binary file: ${entryRelativePath}, original size=${buffer.length}, base64 length=${content.length}`);
589
+ files[entryRelativePath] = { content, encoding: "base64" };
590
+ } else {
591
+ const content = fs2.readFileSync(filePath, "utf-8");
592
+ files[entryRelativePath] = content;
593
+ }
541
594
  } catch {
542
595
  }
543
596
  }
@@ -547,7 +600,7 @@ function readProjectFiles(projectPath, relativePath = "", includeEnvFiles = fals
547
600
  var MAX_CHUNK_SIZE = 2 * 1024 * 1024;
548
601
  var CHUNK_THRESHOLD = 3 * 1024 * 1024;
549
602
  function calculateTotalSize(files) {
550
- return Object.values(files).reduce((sum, content) => sum + content.length, 0);
603
+ return Object.values(files).reduce((sum, file) => sum + getFileSize(file), 0);
551
604
  }
552
605
  function needsChunkedUpload(files) {
553
606
  return calculateTotalSize(files) > CHUNK_THRESHOLD;
@@ -556,16 +609,16 @@ function chunkFiles(files, maxChunkSize = MAX_CHUNK_SIZE) {
556
609
  const chunks = [];
557
610
  let currentChunk = {};
558
611
  let currentSize = 0;
559
- const entries = Object.entries(files).sort((a, b) => a[1].length - b[1].length);
560
- for (const [filePath, content] of entries) {
561
- const fileSize = content.length;
612
+ const entries = Object.entries(files).sort((a, b) => getFileSize(a[1]) - getFileSize(b[1]));
613
+ for (const [filePath, file] of entries) {
614
+ const fileSize = getFileSize(file);
562
615
  if (fileSize > maxChunkSize) {
563
616
  if (Object.keys(currentChunk).length > 0) {
564
617
  chunks.push(currentChunk);
565
618
  currentChunk = {};
566
619
  currentSize = 0;
567
620
  }
568
- chunks.push({ [filePath]: content });
621
+ chunks.push({ [filePath]: file });
569
622
  continue;
570
623
  }
571
624
  if (currentSize + fileSize > maxChunkSize && Object.keys(currentChunk).length > 0) {
@@ -573,7 +626,7 @@ function chunkFiles(files, maxChunkSize = MAX_CHUNK_SIZE) {
573
626
  currentChunk = {};
574
627
  currentSize = 0;
575
628
  }
576
- currentChunk[filePath] = content;
629
+ currentChunk[filePath] = file;
577
630
  currentSize += fileSize;
578
631
  }
579
632
  if (Object.keys(currentChunk).length > 0) {
@@ -1125,7 +1178,7 @@ async function uploadChunk(sandboxId, files, chunkIndex, totalChunks, log) {
1125
1178
  throw new NotAuthenticatedError();
1126
1179
  }
1127
1180
  const fileCount = Object.keys(files).length;
1128
- const chunkSize = Object.values(files).reduce((sum, content) => sum + content.length, 0);
1181
+ const chunkSize = calculateTotalSize(files);
1129
1182
  await log(`Uploading chunk ${chunkIndex + 1}/${totalChunks} (${fileCount} files, ${(chunkSize / 1024).toFixed(1)} KB)...`);
1130
1183
  const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/${sandboxId}/upload`, {
1131
1184
  method: "POST",
@@ -1384,8 +1437,30 @@ function checkMiddleware(content, filePath) {
1384
1437
  configFile: filePath
1385
1438
  };
1386
1439
  }
1440
+ function configAllowsCodeSandbox(content) {
1441
+ const literalRegex = /['"`](https?:\/\/[^\s'"`]+|[^'"`\s]+)['"`]/g;
1442
+ let match;
1443
+ while ((match = literalRegex.exec(content)) !== null) {
1444
+ const literal = match[1];
1445
+ let hostname = "";
1446
+ try {
1447
+ if (/^https?:\/\//i.test(literal)) {
1448
+ const url = new URL(literal);
1449
+ hostname = url.hostname.toLowerCase();
1450
+ } else {
1451
+ hostname = literal.toLowerCase();
1452
+ }
1453
+ } catch {
1454
+ hostname = literal.toLowerCase();
1455
+ }
1456
+ if (hostname === "csb.app" || hostname === "codesandbox.io" || hostname.endsWith(".csb.app") || hostname.endsWith(".codesandbox.io")) {
1457
+ return true;
1458
+ }
1459
+ }
1460
+ return false;
1461
+ }
1387
1462
  function checkViteConfig(content, filePath) {
1388
- if (content.includes(".csb.app") || content.includes(".codesandbox.io")) {
1463
+ if (configAllowsCodeSandbox(content)) {
1389
1464
  return { canEmbed: true, issues: [], configFile: filePath };
1390
1465
  }
1391
1466
  const issues = [];
@@ -1579,7 +1654,11 @@ function patchFilesForEmbed(files) {
1579
1654
  const patchedFiles = [];
1580
1655
  const remainingIssues = [];
1581
1656
  const result = { ...files };
1582
- for (const [filePath, content] of Object.entries(files)) {
1657
+ for (const [filePath, file] of Object.entries(files)) {
1658
+ const content = getTextContent(file);
1659
+ if (content === void 0) {
1660
+ continue;
1661
+ }
1583
1662
  const fileName = filePath.split("/").pop() || "";
1584
1663
  let patched = null;
1585
1664
  if (isMiddlewareFile(filePath)) {
@@ -1641,7 +1720,11 @@ function patchFilesForEmbed(files) {
1641
1720
  }
1642
1721
  function checkEmbedCompatibility(files) {
1643
1722
  const allIssues = [];
1644
- for (const [filePath, content] of Object.entries(files)) {
1723
+ for (const [filePath, file] of Object.entries(files)) {
1724
+ const content = getTextContent(file);
1725
+ if (content === void 0) {
1726
+ continue;
1727
+ }
1645
1728
  const fileName = filePath.split("/").pop() || "";
1646
1729
  if (fileName === "next.config.js" || fileName === "next.config.mjs" || fileName === "next.config.ts") {
1647
1730
  const result = checkNextConfig(content, filePath);
@@ -1912,7 +1995,7 @@ async function deployPrototype(args, log) {
1912
1995
  }
1913
1996
  }
1914
1997
  }
1915
- const totalSize = Object.values(files).reduce((sum, content) => sum + content.length, 0);
1998
+ const totalSize = calculateTotalSize(files);
1916
1999
  await log(`Found ${fileCount} files (${(totalSize / 1024 / 1024).toFixed(1)} MB)`);
1917
2000
  let existingSandbox;
1918
2001
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-inflight",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "MCP server for sharing prototypes via InFlight",
5
5
  "author": "InFlight <hello@inflight.co>",
6
6
  "homepage": "https://github.com/inflight/mcp-inflight",