Skip to main content
Glama
network-tools.ts7.63 kB
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod/v4"; import type { TailscaleCLIStatus } from "@/types.js"; import { logger } from "../logger.js"; import { returnToolError, returnToolSuccess } from "../utils.js"; import type { ToolContext, ToolModule } from "./index.js"; // Schemas const NetworkStatusSchema = z.object({ format: z .enum(["json", "summary"]) .optional() .default("json") .describe("Output format (json or summary)"), }); const ConnectNetworkSchema = z.object({ acceptRoutes: z .boolean() .optional() .default(false) .describe("Accept subnet routes from other devices"), acceptDNS: z .boolean() .optional() .default(false) .describe("Accept DNS configuration from the network"), hostname: z .string() .optional() .describe("Set a custom hostname for this device"), advertiseRoutes: z .array(z.string()) .optional() .describe("CIDR routes to advertise to other devices"), authKey: z .string() .optional() .describe("Authentication key for unattended setup"), loginServer: z.string().optional().describe("Custom coordination server URL"), }); const PingPeerSchema = z.object({ target: z.string().describe("Hostname or IP address of the target device"), count: z .number() .int() .min(1) .max(100) .optional() .default(4) .describe("Number of ping packets to send"), }); // Tool handlers async function getNetworkStatus( args: z.infer<typeof NetworkStatusSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Getting network status with format:", args.format); // Use unified client which will automatically choose between API and CLI const result = await context.client.getStatus(); if (!result.success) { return returnToolError(result.error); } const status = result.data as TailscaleCLIStatus; if (args.format === "summary") { let output = "**Tailscale Network Status**\n\n"; output += `Version: ${status.Version}\n`; output += `Backend state: ${status.BackendState}\n`; output += `TUN interface: ${status.TUN ? "Active" : "Inactive"}\n`; output += `Tailscale IPs: ${(status.TailscaleIPs ?? []).join(", ")}\n\n`; output += "**This device:**\n"; output += ` - Hostname: ${status.Self.HostName}\n`; output += ` - DNS name: ${status.Self.DNSName}\n`; output += ` - OS: ${status.Self.OS}\n`; output += ` - IPs: ${status.Self.TailscaleIPs.join(", ")}\n`; output += ` - Online: ${status.Self.Online ? "🟢" : "🔴"}\n`; if (status.Self.ExitNode) { output += " - Exit node: Yes\n"; } output += "\n"; if (status.Peer && Object.keys(status.Peer).length > 0) { const peers = Object.values(status.Peer); output += `**Connected peers (${peers.length}):**\n`; for (const peer of peers) { const onlineStatus = peer.Online ? "🟢" : "🔴"; output += ` ${onlineStatus} ${peer.HostName} (${peer.DNSName})\n`; output += ` - OS: ${peer.OS}\n`; output += ` - IPs: ${peer.TailscaleIPs.join(", ")}\n`; if (peer.LastSeen && peer.LastSeen !== "0001-01-01T00:00:00Z") { output += ` - Last seen: ${peer.LastSeen}\n`; } if (peer.ExitNode) { output += " - Exit node: Yes\n"; } if (peer.Active) { output += " - Active connection\n"; } } } return returnToolSuccess(output); } // JSON format return returnToolSuccess(JSON.stringify(status, null, 2)); } catch (error: unknown) { logger.error("Error getting network status:", error); const err = error instanceof Error ? error : new Error(String(error ?? "Unknown error")); return returnToolError(err); } } async function connectNetwork( args: z.infer<typeof ConnectNetworkSchema>, context: ToolContext, ): Promise<CallToolResult> { try { const options = { acceptRoutes: args.acceptRoutes || false, acceptDns: args.acceptDNS ?? false, // Note: CLI uses acceptDns, not acceptDNS hostname: args.hostname, advertiseRoutes: args.advertiseRoutes || [], authKey: args.authKey, loginServer: args.loginServer, }; logger.debug("Connecting to Tailscale network with options:", options); // Use unified client - this operation is CLI-only const result = await context.client.connect(options); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Successfully connected to Tailscale network\n\n${result.data}`, ); } catch (error: unknown) { logger.error("Error connecting to network:", error); return returnToolError(error); } } async function disconnectNetwork( _args: Record<string, unknown>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Disconnecting from Tailscale network"); // Use unified client - this operation is CLI-only const result = await context.client.disconnect(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Successfully disconnected from Tailscale network\n\n${result.data}`, ); } catch (error: unknown) { logger.error("Error disconnecting from network:", error); return returnToolError(error); } } async function pingPeer( args: z.infer<typeof PingPeerSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug(`Pinging ${args.target} (${args.count} packets)`); // Use unified client - this operation is CLI-only const result = await context.client.ping(args.target, args.count); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Ping results for ${args.target}:\n\n${result.data}`, ); } catch (error: unknown) { logger.error("Error pinging peer:", error); return returnToolError(error); } } async function getVersion( _args: Record<string, unknown>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Getting Tailscale version"); // Use unified client - this operation is CLI-only const result = await context.client.getVersion(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Tailscale version information:\n\n${result.data}`, ); } catch (error: unknown) { logger.error("Error getting version:", error); return returnToolError(error); } } // Export the tool module export const networkTools: ToolModule = { tools: [ { name: "get_network_status", description: "Get current network status from Tailscale CLI", inputSchema: NetworkStatusSchema, handler: getNetworkStatus, }, { name: "connect_network", description: "Connect to the Tailscale network", inputSchema: ConnectNetworkSchema, handler: connectNetwork, }, { name: "disconnect_network", description: "Disconnect from the Tailscale network", inputSchema: z.object({}), handler: disconnectNetwork, }, { name: "ping_peer", description: "Ping a peer device", inputSchema: PingPeerSchema, handler: pingPeer, }, { name: "get_version", description: "Get Tailscale version information", inputSchema: z.object({}), handler: getVersion, }, ], };

Implementation Reference

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/HexSleeves/tailscale-mcp'

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