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();
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes key behaviors: it starts a tunnel process, returns a public URL, allows naming for management, and mentions the need to wait and check status with another tool. It covers the core operational flow but doesn't mention potential errors, timeouts, or security implications.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded with the core purpose, followed by bullet points for key behaviors and a clear follow-up instruction. Every sentence earns its place with no redundant information, making it efficient and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (creating a network tunnel), no annotations, and no output schema, the description does a good job covering the essential context: what it does, key behaviors, and follow-up steps. However, it doesn't describe the return value format or error conditions, leaving some gaps for a mutation tool with no structured output documentation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already fully documents both parameters (name and url). The description adds marginal value by mentioning naming 'for easier management' and giving an example URL format, but doesn't provide additional semantic context beyond what the schema descriptions already state.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Creates a secure tunnel'), the resource ('from a public internet address to your local server'), and distinguishes from siblings by mentioning 'list_tunnels' for status checking. It explicitly describes what the tool does beyond just the name.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context on when to use this tool (to expose a local server) and references 'list_tunnels' as a follow-up action. However, it doesn't explicitly state when NOT to use it or mention alternatives like 'stop_tunnel' for tunnel management, leaving some guidance implicit rather than explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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