start_tunnel
Creates a secure tunnel to expose a local server to the public internet. Provides a public URL for access, allows custom naming, and supports status checks via 'list_tunnels'.
Instructions
Creates a secure tunnel from a public internet address to your local server.
This tool will:
Start an untun tunnel process connecting to your specified local URL
Return a public URL that can be used to access your local server
Allow you to name your tunnel for easier management
After starting a tunnel, wait a few seconds and use 'list_tunnels' to check its status and get the public URL.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | Optional custom name for the tunnel. If not provided, 'default' will be used. | |
| url | Yes | The local URL to expose (e.g., http://localhost:3000) |
Implementation Reference
- src/index.ts:48-238 (registration)Registers the 'start_tunnel' MCP tool with description, Zod input schema (url, optional name), and inline async handler that starts untun tunnel process and manages it.server.tool( "start_tunnel", `Creates a secure tunnel from a public internet address to your local server. This tool will: - Start an untun tunnel process connecting to your specified local URL - Return a public URL that can be used to access your local server - Allow you to name your tunnel for easier management After starting a tunnel, wait a few seconds and use 'list_tunnels' to check its status and get the public URL.`, { url: z .string() .url() .describe("The local URL to expose (e.g., http://localhost:3000)"), name: z .string() .optional() .describe( "Optional custom name for the tunnel. If not provided, 'default' will be used.", ), }, async ({ url, name = "default" }) => { // Check if tunnel with this name already exists if (activeTunnels.has(name)) { return { content: [ { type: "text", text: `Tunnel with name "${name}" is already running. Please stop it first or use a different name.`, }, ], }; } try { log(`Starting tunnel to ${url} with name "${name}"`); debugLog(`Starting tunnel to ${url} with name "${name}"`); // Start the tunnel process immediately (non-blocking) const command = `npx untun tunnel ${url}`; log(`Executing command: ${command}`); const tunnelProcess = exec(command); let output = ""; let tunnelUrl: string | null = null; // URL detection regex const urlRegex = /Tunnel ready at (https?:\/\/[^\s]+)|https?:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/; // Track when we set the URL let urlResolved = false; // Create the tunnel object const tunnel = { url: null, process: tunnelProcess, close: async () => { log(`Closing tunnel to ${url}`); debugLog(`Closing tunnel to ${url}`); // Try normal kill first try { tunnelProcess.kill(); } catch (e) { const error = e as Error; log(`Error killing process: ${error.message}`); } // Get information about this specific tunnel const tunnelInfo = activeTunnels.get(name); // Close the specific tunnel using its URL and PID if (tunnelInfo?.url) { await closeSpecificTunnel(tunnelInfo.url, tunnelInfo.pid); } return true; }, }; // Store tunnel information immediately with hostname activeTunnels.set(name, { tunnel, url, publicUrl: null, created: new Date(), output, pid: tunnelProcess.pid, hostId: os.hostname(), // Add hostname to identify the origin }); // Save to file immediately saveTunnels(); // Setup stdout for URL detection (will continue after we respond) tunnelProcess.stdout?.on("data", (data) => { const text = data.toString(); output += text; // Look for tunnel URL in output if (!tunnelUrl) { const match = text.match(urlRegex); if (match) { tunnelUrl = match[1] || match[0]; log(`Detected tunnel URL: ${tunnelUrl}`); debugLog(`Detected tunnel URL: ${tunnelUrl}`); // Update the stored tunnel information const tunnelInfo = activeTunnels.get(name); if (tunnelInfo) { tunnelInfo.publicUrl = tunnelUrl; tunnelInfo.tunnel.url = tunnelUrl; tunnelInfo.output = output; urlResolved = true; // Save to file when URL is resolved saveTunnels(); } } } // Log to stderr and debug file text .split("\n") .filter(Boolean) .forEach((line: string) => { log(`[untun stdout]: ${line}`); debugLog(`[untun stdout]: ${line}`); }); }); // Setup stderr handler tunnelProcess.stderr?.on("data", (data) => { const text = data.toString(); output += text; // Log to stderr and debug file text .split("\n") .filter(Boolean) .forEach((line: string) => { log(`[untun stderr]: ${line}`); debugLog(`[untun stderr]: ${line}`); }); // Update the stored output const tunnelInfo = activeTunnels.get(name); if (tunnelInfo) { tunnelInfo.output = output; } }); // Handle process exit tunnelProcess.on("exit", (code) => { log(`Tunnel process exited with code ${code}`); debugLog(`Tunnel process exited with code ${code}`); // If we never got a URL and the process exited, remove the tunnel if (!urlResolved) { activeTunnels.delete(name); saveTunnels(); } }); // Return success immediately - don't wait for the URL return { content: [ { type: "text", text: `✅ Tunnel process started successfully!\n\nName: ${name}\nLocal URL: ${url}\nPublic URL: will be available shortly...\n\nUse 'list_tunnels' in a few seconds to check the status and get the public URL.`, }, ], }; } catch (error) { const err = error as Error; const errorMsg = `Error starting tunnel: ${err.message || error}`; log(errorMsg); debugLog(errorMsg); return { content: [ { type: "text", text: errorMsg, }, ], }; } }, );
- src/index.ts:70-237 (handler)The core handler function for start_tunnel: spawns 'npx untun tunnel' process, monitors output to extract public URL, sets up close function, stores in activeTunnels map, handles process events, returns immediate success response instructing to use list_tunnels for URL.async ({ url, name = "default" }) => { // Check if tunnel with this name already exists if (activeTunnels.has(name)) { return { content: [ { type: "text", text: `Tunnel with name "${name}" is already running. Please stop it first or use a different name.`, }, ], }; } try { log(`Starting tunnel to ${url} with name "${name}"`); debugLog(`Starting tunnel to ${url} with name "${name}"`); // Start the tunnel process immediately (non-blocking) const command = `npx untun tunnel ${url}`; log(`Executing command: ${command}`); const tunnelProcess = exec(command); let output = ""; let tunnelUrl: string | null = null; // URL detection regex const urlRegex = /Tunnel ready at (https?:\/\/[^\s]+)|https?:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/; // Track when we set the URL let urlResolved = false; // Create the tunnel object const tunnel = { url: null, process: tunnelProcess, close: async () => { log(`Closing tunnel to ${url}`); debugLog(`Closing tunnel to ${url}`); // Try normal kill first try { tunnelProcess.kill(); } catch (e) { const error = e as Error; log(`Error killing process: ${error.message}`); } // Get information about this specific tunnel const tunnelInfo = activeTunnels.get(name); // Close the specific tunnel using its URL and PID if (tunnelInfo?.url) { await closeSpecificTunnel(tunnelInfo.url, tunnelInfo.pid); } return true; }, }; // Store tunnel information immediately with hostname activeTunnels.set(name, { tunnel, url, publicUrl: null, created: new Date(), output, pid: tunnelProcess.pid, hostId: os.hostname(), // Add hostname to identify the origin }); // Save to file immediately saveTunnels(); // Setup stdout for URL detection (will continue after we respond) tunnelProcess.stdout?.on("data", (data) => { const text = data.toString(); output += text; // Look for tunnel URL in output if (!tunnelUrl) { const match = text.match(urlRegex); if (match) { tunnelUrl = match[1] || match[0]; log(`Detected tunnel URL: ${tunnelUrl}`); debugLog(`Detected tunnel URL: ${tunnelUrl}`); // Update the stored tunnel information const tunnelInfo = activeTunnels.get(name); if (tunnelInfo) { tunnelInfo.publicUrl = tunnelUrl; tunnelInfo.tunnel.url = tunnelUrl; tunnelInfo.output = output; urlResolved = true; // Save to file when URL is resolved saveTunnels(); } } } // Log to stderr and debug file text .split("\n") .filter(Boolean) .forEach((line: string) => { log(`[untun stdout]: ${line}`); debugLog(`[untun stdout]: ${line}`); }); }); // Setup stderr handler tunnelProcess.stderr?.on("data", (data) => { const text = data.toString(); output += text; // Log to stderr and debug file text .split("\n") .filter(Boolean) .forEach((line: string) => { log(`[untun stderr]: ${line}`); debugLog(`[untun stderr]: ${line}`); }); // Update the stored output const tunnelInfo = activeTunnels.get(name); if (tunnelInfo) { tunnelInfo.output = output; } }); // Handle process exit tunnelProcess.on("exit", (code) => { log(`Tunnel process exited with code ${code}`); debugLog(`Tunnel process exited with code ${code}`); // If we never got a URL and the process exited, remove the tunnel if (!urlResolved) { activeTunnels.delete(name); saveTunnels(); } }); // Return success immediately - don't wait for the URL return { content: [ { type: "text", text: `✅ Tunnel process started successfully!\n\nName: ${name}\nLocal URL: ${url}\nPublic URL: will be available shortly...\n\nUse 'list_tunnels' in a few seconds to check the status and get the public URL.`, }, ], }; } catch (error) { const err = error as Error; const errorMsg = `Error starting tunnel: ${err.message || error}`; log(errorMsg); debugLog(errorMsg); return { content: [ { type: "text", text: errorMsg, }, ], }; } },
- src/index.ts:58-69 (schema)Zod schema defining input parameters for start_tunnel tool: required 'url' (validated URL string), optional 'name' (string).{ url: z .string() .url() .describe("The local URL to expose (e.g., http://localhost:3000)"), name: z .string() .optional() .describe( "Optional custom name for the tunnel. If not provided, 'default' will be used.", ), },
- src/index.ts:27-28 (helper)Initial loadTunnels() call ensures activeTunnels is populated before tool handlers run, supporting tunnel state management used in start_tunnel.loadTunnels();