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();
Install Server

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