Skip to content

Commit 21f6b5f

Browse files
copeusclaude
andcommitted
feat: wait for tunnel readiness before opening playground
- Add waitForTunnelUrl() to poll tunnel endpoint before browser open - Prevents 'Connection Failed' error on first load - Shows progress feedback to user - Gracefully handles timeout with manual URL Fixes cloudflare tunnel initialization timing issue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 72ec9fb commit 21f6b5f

3 files changed

Lines changed: 67 additions & 5 deletions

File tree

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcpize",
3-
"version": "1.0.30",
3+
"version": "1.0.31",
44
"description": "Deploy and monetize MCP servers in the cloud",
55
"type": "module",
66
"bin": {

‎src/commands/dev.ts‎

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,57 @@ async function getPortProcess(port: number): Promise<string | null> {
304304
});
305305
}
306306

307+
/**
308+
* Wait for tunnel URL to be accessible
309+
*/
310+
async function waitForTunnelUrl(tunnelUrl: string, maxAttempts = 10): Promise<boolean> {
311+
const mcpUrl = `${tunnelUrl}/mcp`;
312+
313+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
314+
try {
315+
const controller = new AbortController();
316+
const timeout = setTimeout(() => controller.abort(), 3000);
317+
318+
const response = await fetch(mcpUrl, {
319+
method: "POST",
320+
headers: {
321+
"Content-Type": "application/json",
322+
"Accept": "application/json, text/event-stream",
323+
},
324+
body: JSON.stringify({
325+
jsonrpc: "2.0",
326+
method: "initialize",
327+
params: {
328+
protocolVersion: "2025-06-18",
329+
capabilities: {},
330+
clientInfo: { name: "mcpize-cli", version: "1.0.0" },
331+
},
332+
id: 1,
333+
}),
334+
signal: controller.signal,
335+
});
336+
337+
clearTimeout(timeout);
338+
339+
if (response.ok) {
340+
return true;
341+
}
342+
} catch {
343+
// Tunnel not ready yet, continue polling
344+
}
345+
346+
// Wait 1 second before next attempt
347+
await new Promise(resolve => setTimeout(resolve, 1000));
348+
349+
// Show progress dots every 2 attempts
350+
if (attempt % 2 === 0) {
351+
process.stdout.write(".");
352+
}
353+
}
354+
355+
return false;
356+
}
357+
307358
/**
308359
* Wait for server to be healthy by polling the MCP endpoint
309360
*/
@@ -548,8 +599,19 @@ export const devCommand = new Command("dev")
548599

549600
// Open playground if requested
550601
if (options.playground) {
551-
console.log(chalk.gray("\n Opening playground in browser..."));
552-
openBrowser(playgroundUrl);
602+
console.log(chalk.gray("\n Waiting for tunnel to be ready..."));
603+
604+
// Wait for tunnel URL to be accessible
605+
const tunnelReady = await waitForTunnelUrl(tunnel.url);
606+
607+
if (tunnelReady) {
608+
console.log(chalk.green(" ✓ Tunnel ready"));
609+
console.log(chalk.gray(" Opening playground in browser..."));
610+
openBrowser(playgroundUrl);
611+
} else {
612+
console.log(chalk.yellow(" ⚠ Tunnel may take a moment to fully connect"));
613+
console.log(chalk.gray(` Open manually: ${playgroundUrl}`));
614+
}
553615
} else {
554616
console.log(
555617
chalk.gray("\n Use --playground flag to open in MCPize Playground"),

0 commit comments

Comments
 (0)