Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
nameNoOptional custom name for the tunnel. If not provided, 'default' will be used.
urlYesThe 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, }, ], }; } }, );
  • 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, }, ], }; } },
  • 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.", ), },
  • Initial loadTunnels() call ensures activeTunnels is populated before tool handlers run, supporting tunnel state management used in start_tunnel.
    loadTunnels();

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/minte-app/untun-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server