Skip to main content
Glama

stop_tunnel

Stop specific or all local tunnels running on the current machine. Use this to terminate tunnels identified by name or halt all active ones without affecting other systems.

Instructions

Stops a running tunnel or all local tunnels.

This tool will:

  • Stop a specific tunnel identified by name (if provided)

  • Stop all local tunnels (if no name is provided)

  • Only affects tunnels running on the current machine

  • Will not affect tunnels running on other machines

After stopping tunnels, you can use 'list_tunnels' to confirm they've been terminated.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nameNoOptional name of a specific tunnel to stop. If not provided, all local tunnels will be stopped.

Implementation Reference

  • Primary implementation of the stop_tunnel tool: registers the tool, defines input schema, and provides the handler logic that either stops a specific tunnel using closeSpecificTunnel or all tunnels using killTunnelProcesses, while managing the activeTunnels state.
    // Define 'stop_tunnel' tool
    server.tool(
      "stop_tunnel",
      `Stops a running tunnel or all local tunnels.
      
      This tool will:
      - Stop a specific tunnel identified by name (if provided)
      - Stop all local tunnels (if no name is provided)
      - Only affects tunnels running on the current machine
      - Will not affect tunnels running on other machines
      
      After stopping tunnels, you can use 'list_tunnels' to confirm they've been terminated.`,
      {
        name: z
          .string()
          .optional()
          .describe(
            "Optional name of a specific tunnel to stop. If not provided, all local tunnels will be stopped.",
          ),
      },
      async ({ name }) => {
        try {
          // If name is provided, stop specific tunnel
          if (name) {
            if (!activeTunnels.has(name)) {
              return {
                content: [
                  {
                    type: "text",
                    text: `No active tunnel found with name "${name}".`,
                  },
                ],
              };
            }
    
            const tunnelInfo = activeTunnels.get(name);
    
            // Check if this is a remote tunnel
            if (tunnelInfo?.isRemote) {
              return {
                content: [
                  {
                    type: "text",
                    text: `Tunnel "${name}" is running on a different host (${tunnelInfo.hostId}) and cannot be stopped from this instance. Please stop it from the originating host.`,
                  },
                ],
              };
            }
    
            log(`==== STOPPING SPECIFIC TUNNEL: ${name} ====`);
            log(
              `Tunnel details: URL=${tunnelInfo?.url}, PID=${tunnelInfo?.pid || "unknown"}, Public URL=${tunnelInfo?.publicUrl || "unknown"}`,
            );
            debugLog(
              `Stopping specific tunnel: ${name} (PID: ${tunnelInfo?.pid || "unknown"})`,
            );
    
            let killResults: string[] = [];
    
            if (tunnelInfo?.url) {
              const result = await closeSpecificTunnel(
                tunnelInfo.url,
                tunnelInfo.pid,
              );
              killResults = result.split("\n");
            }
    
            // Remove from active tunnels
            log(`Removing tunnel ${name} from registry`);
            activeTunnels.delete(name);
    
            // Save updated tunnels to file
            saveTunnels();
    
            const killResultsText = killResults.join("\n");
            log(`Kill results:\n${killResultsText}`);
            log(`==== FINISHED STOPPING TUNNEL: ${name} ====`);
    
            return {
              content: [
                {
                  type: "text",
                  text: `Tunnel "${name}" stopped.\n\nDetails:\n${killResultsText}`,
                },
              ],
            };
          }
          // No name provided, stop all local tunnels
          else {
            log(`==== STOPPING ALL LOCAL TUNNELS ====`);
    
            // Use killTunnelProcesses to forcefully kill all untun processes
            const results = killTunnelProcesses();
    
            // Get the count of local tunnels before cleaning
            const entries = Array.from(activeTunnels.entries());
            const localTunnels = entries.filter((entry) => {
              // Type assertion for entry
              const [_, info] = entry as [string, TunnelInfo];
              return !info.isRemote;
            });
            const localCount = localTunnels.length;
    
            if (localCount === 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: "No local tunnels to stop. Remote tunnels are not affected.",
                  },
                ],
              };
            }
    
            // Remove all local tunnels from the registry
            for (const [tunnelName, _] of localTunnels) {
              activeTunnels.delete(tunnelName);
            }
    
            // Save updated tunnels to file
            saveTunnels();
    
            log(`==== FINISHED STOPPING ALL TUNNELS ====`);
    
            return {
              content: [
                {
                  type: "text",
                  text: `Stopped ${localCount} local tunnels. Remote tunnels were not affected.\n\nDetails:\n${results}`,
                },
              ],
            };
          }
        } catch (error) {
          const err = error as Error;
          const errorMsg = `Error stopping tunnel(s): ${err.message || error}`;
          log(errorMsg);
          debugLog(errorMsg);
    
          return {
            content: [
              {
                type: "text",
                text: errorMsg,
              },
            ],
          };
        }
      },
    );
  • Helper function killTunnelProcesses: Force-kills all tunnel-related processes (cloudflared launched by untun and untun itself), used when no specific name is provided to stop all local tunnels.
    export const killTunnelProcesses = (): string => {
      log("Forcefully killing all tunnel processes");
      debugLog("Forcefully killing all tunnel processes");
    
      const results: string[] = [];
    
      // Find all cloudflared processes
      const cloudflaredDetails = getCloudflaredDetails();
    
      // Filter to get only cloudflared processes that were launched by untun
      // They typically have 'node-untun' in the path and/or '--url http://' in the command
      const untunCloudflaredProcesses = cloudflaredDetails.filter((proc) => {
        return (
          (proc.command.includes("node-untun") ||
            proc.command.includes("--url http") ||
            proc.command.match(/tunnel --url/i)) &&
          !proc.command.includes("--token")
        ); // Exclude processes using an authentication token
      });
    
      if (untunCloudflaredProcesses.length > 0) {
        results.push(
          `Found ${untunCloudflaredProcesses.length} untun-managed cloudflared processes`,
        );
    
        // Log the processes we found for debugging
        untunCloudflaredProcesses.forEach((proc) => {
          results.push(
            `Will terminate: PID ${proc.pid} (${proc.command.slice(0, 50)}...)`,
          );
        });
    
        // Try to kill each PID with SIGTERM first
        untunCloudflaredProcesses.forEach((proc) => {
          const killed = killPid(proc.pid, "TERM");
          results.push(
            `Kill TERM PID ${proc.pid}: ${killed ? "Success" : "Failed"}`,
          );
        });
    
        // Wait a moment for processes to terminate
        try {
          execSync("sleep 0.5");
        } catch (e) {}
    
        // Find which ones remain and use SIGKILL
        const remainingDetails = getCloudflaredDetails();
        const remainingPids = untunCloudflaredProcesses
          .map((proc) => proc.pid)
          .filter((pid) => remainingDetails.some((p) => p.pid === pid));
    
        if (remainingPids.length > 0) {
          results.push(
            `${remainingPids.length} cloudflared processes still running after SIGTERM`,
          );
    
          // Force kill each remaining process
          remainingPids.forEach((pid) => {
            const killed = killPid(pid, "KILL");
            results.push(`Kill KILL PID ${pid}: ${killed ? "Success" : "Failed"}`);
          });
        }
      } else {
        results.push("No untun-managed cloudflared processes found to kill");
      }
    
      // Also try to kill untun processes directly
      try {
        const untunPids = findPidsByPattern("untun tunnel");
        if (untunPids.length > 0) {
          results.push(`Found ${untunPids.length} untun tunnel processes`);
          untunPids.forEach((pid) => {
            const killed = killPid(pid, "TERM");
            results.push(
              `Kill TERM untun PID ${pid}: ${killed ? "Success" : "Failed"}`,
            );
          });
    
          // Force kill if TERM didn't work
          try {
            execSync("sleep 0.3");
            const remainingUntunPids = findPidsByPattern("untun tunnel");
            remainingUntunPids.forEach((pid) => {
              const killed = killPid(pid, "KILL");
              results.push(
                `Kill KILL untun PID ${pid}: ${killed ? "Success" : "Failed"}`,
              );
            });
          } catch (e) {}
        } else {
          results.push("No untun processes found");
        }
      } catch (e) {
        const error = e as Error;
        results.push(`Error killing untun processes: ${error.message}`);
      }
    
      // Final check to see if our cloudflared processes are gone
      try {
        const finalUntunCloudflared = getCloudflaredDetails().filter((proc) => {
          return (
            (proc.command.includes("node-untun") ||
              proc.command.includes("--url http") ||
              proc.command.match(/tunnel --url/i)) &&
            !proc.command.includes("--token")
          );
        });
    
        if (finalUntunCloudflared.length > 0) {
          const details = finalUntunCloudflared
            .map((p) => `PID ${p.pid}: ${p.command.slice(0, 30)}...`)
            .join("\n");
          results.push(
            `WARNING: ${finalUntunCloudflared.length} untun cloudflared processes still running:\n${details}`,
          );
        } else {
          results.push("All untun cloudflared processes successfully terminated");
        }
      } catch (e) {
        // No processes found is good
        const error = e as Error;
        results.push(`Error in final check: ${error.message}`);
      }
    
      return results.join("\n");
    };
  • Helper function closeSpecificTunnel: Closes a specific tunnel by killing its untun process (by PID) and associated cloudflared child process(es), used for named tunnel stops.
    export const closeSpecificTunnel = async (
      url: string,
      pid?: number,
    ): Promise<string> => {
      const results: string[] = [];
    
      log(`Attempting to close specific tunnel for URL: ${url}, PID: ${pid}`);
    
      // Function to kill a specific process
      const killProcess = (targetPid: string, signal = "TERM"): boolean => {
        try {
          log(`Killing specific PID ${targetPid} with signal ${signal}`);
          execSync(`kill -${signal} ${targetPid}`, { stdio: "ignore" });
          results.push(`✓ Killed process ${targetPid} with ${signal}`);
          return true;
        } catch (e) {
          const error = e as Error;
          results.push(`✗ Failed to kill process ${targetPid}: ${error.message}`);
          return false;
        }
      };
    
      // Kill the main untun process if PID is provided
      let mainProcessKilled = false;
      if (pid) {
        try {
          // Check if process exists first
          try {
            execSync(`ps -p ${pid}`);
            mainProcessKilled = killProcess(pid.toString());
          } catch (e) {
            results.push(`Process ${pid} not found (might have exited already)`);
          }
        } catch (error) {
          const err = error as Error;
          results.push(`Error checking/killing main process: ${err.message}`);
        }
    
        // Only look for cloudflared processes with the specific parent PID
        // This ensures we only kill the exact cloudflared process associated with this tunnel
        try {
          // Find the child processes of the main process
          const pgrep = `pgrep -P ${pid}`;
          let childPids: string[] = [];
    
          try {
            const childPidsOutput = execSync(pgrep).toString().trim();
            if (childPidsOutput) {
              childPids = childPidsOutput.split("\n").filter(Boolean);
            }
          } catch (e) {
            // No child processes found, which might be normal
            results.push(`No child processes found for PID ${pid}`);
          }
    
          // If we found child processes, check which ones are cloudflared
          if (childPids.length > 0) {
            results.push(
              `Found ${childPids.length} child processes for PID ${pid}`,
            );
    
            for (const childPid of childPids) {
              try {
                // Check if this process is a cloudflared process
                const psCmd = `ps -p ${childPid} -o command=`;
                const processCmd = execSync(psCmd).toString().trim();
    
                if (processCmd.includes("cloudflared")) {
                  results.push(`Found cloudflared child process: ${childPid}`);
                  killProcess(childPid);
                }
              } catch (e) {
                // Process might have exited already
              }
            }
          }
    
          return results.join("\n");
        } catch (error) {
          const err = error as Error;
          results.push(`Error finding/killing child processes: ${err.message}`);
        }
      } else {
        // If no PID is provided, fall back to URL-based matching but with a warning
        results.push(
          "Warning: No PID provided, trying to match by URL which may affect other tunnels",
        );
    
        try {
          // Clean the URL for matching (remove protocol, only use host:port)
          const cleanUrl = url.replace(/^https?:\/\//, "");
    
          // Find cloudflared processes that match this URL
          const cmd = `ps aux | grep cloudflared | grep "${cleanUrl}" | grep -v grep`;
          let matchingProcesses: Array<{ pid: string; cmd: string }> = [];
    
          try {
            const output = execSync(cmd).toString().trim();
            if (output) {
              matchingProcesses = output
                .split("\n")
                .filter(Boolean)
                .map((line) => {
                  const parts = line.trim().split(/\s+/);
                  return { pid: parts[1], cmd: parts.slice(10).join(" ") };
                });
            }
          } catch (e) {
            // If grep returns non-zero (no matches), that's fine
            results.push("No matching cloudflared processes found");
          }
    
          // Kill each matching cloudflared process
          if (matchingProcesses.length > 0) {
            results.push(
              `Found ${matchingProcesses.length} cloudflared processes for URL ${cleanUrl}`,
            );
    
            for (const proc of matchingProcesses) {
              results.push(
                `Attempting to kill cloudflared process ${proc.pid} (${proc.cmd.slice(0, 30)}...)`,
              );
              killProcess(proc.pid);
            }
          }
        } catch (error) {
          const err = error as Error;
          results.push(
            `Error finding/killing cloudflared processes: ${err.message}`,
          );
        }
      }
    
      return results.join("\n");
    };
  • Helper saveTunnels: Persists the activeTunnels state to file after modifications during tunnel stops.
    export const saveTunnels = (): void => {
      try {
        // Convert Map to a serializable object
        const tunnelsData = Array.from(activeTunnels.entries()).map(
          ([name, info]) => {
            // Create a serializable version of the tunnel info
            return {
              name,
              url: info.url,
              publicUrl: info.publicUrl,
              created: info.created.toISOString(),
              pid: info.pid,
              hostId: os.hostname(), // Add hostname to identify the host
            };
          },
        );
    
        fs.writeFileSync(tunnelStoragePath, JSON.stringify(tunnelsData, null, 2));
        log(`Saved ${tunnelsData.length} tunnel(s) to ${tunnelStoragePath}`);
        debugLog(`Saved tunnels to storage file`);
      } catch (error) {
        const err = error as Error;
        log(`Error saving tunnels to file: ${err.message}`);
        debugLog(`Error saving tunnels: ${err.message}`);
      }
    };
  • Helper loadTunnels: Restores activeTunnels state from file, called on server start.
    export const loadTunnels = (): void => {
      try {
        if (!fs.existsSync(tunnelStoragePath)) {
          log(`No tunnel storage file found at ${tunnelStoragePath}`);
          return;
        }
    
        const data = fs.readFileSync(tunnelStoragePath, "utf8");
        const tunnelsData = JSON.parse(data);
    
        // Process each tunnel
        tunnelsData.forEach((tunnelData: any) => {
          // Skip if tunnel is already loaded
          if (activeTunnels.has(tunnelData.name)) return;
    
          // Check if this is a remote tunnel or a local one
          const isRemoteTunnel = tunnelData.hostId !== os.hostname();
    
          // Create a tunnel object with appropriate properties based on type
          activeTunnels.set(tunnelData.name, {
            url: tunnelData.url,
            publicUrl: tunnelData.publicUrl,
            created: new Date(tunnelData.created),
            pid: tunnelData.pid,
            hostId: tunnelData.hostId,
            isRemote: isRemoteTunnel,
            tunnel: {
              // Remote tunnels cannot be closed from this instance
              // Local tunnels can be closed via processes
              close: async () => {
                if (isRemoteTunnel) {
                  log(
                    `Cannot close remote tunnel "${tunnelData.name}" from this host`,
                  );
                  return false;
                } else {
                  log(`Closing local tunnel "${tunnelData.name}"`);
                  return closeSpecificTunnel(tunnelData.url, tunnelData.pid);
                }
              },
            },
          });
        });
    
        log(`Loaded ${tunnelsData.length} tunnel(s) from storage file`);
        debugLog(`Loaded tunnels from storage file`);
      } catch (error) {
        const err = error as Error;
        log(`Error loading tunnels from file: ${err.message}`);
        debugLog(`Error loading tunnels: ${err.message}`);
      }
    };
Behavior4/5

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

With no annotations, the description carries full burden and discloses key behavioral traits: it only affects tunnels on the current machine, not others, and the outcome depends on name parameter presence. It doesn't mention error handling or permissions, but covers scope and conditional behavior adequately.

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?

Front-loaded with purpose, followed by bullet points for key behaviors, and ends with a usage tip. Every sentence earns its place, with no redundancy or fluff, making it efficient and well-structured.

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 no annotations, no output schema, and a simple parameter, the description is nearly complete: it explains purpose, usage, behavioral scope, and references a sibling tool. It lacks details on errors or return values, but for a stop action with one optional parameter, this is sufficient.

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

Parameters4/5

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

Schema coverage is 100%, so baseline is 3. The description adds value by explaining the semantic effect of the parameter: if provided, stops a specific tunnel; if not, stops all local tunnels. This clarifies the conditional logic beyond the schema's description.

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 verb ('stops') and resource ('a running tunnel or all local tunnels'), distinguishing it from sibling tools list_tunnels (for listing) and start_tunnel (for starting). It specifies the scope ('local tunnels') and dual functionality (specific vs. all).

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

Usage Guidelines5/5

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

Explicit guidance is provided: use with a name to stop a specific tunnel, or without to stop all local tunnels. It distinguishes when to use this tool vs. list_tunnels for confirmation, and implicitly contrasts with start_tunnel for opposite actions.

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