diff options
| author | Brad Fitzpatrick <bradfitz@tailscale.com> | 2025-04-04 09:17:35 -0700 |
|---|---|---|
| committer | Brad Fitzpatrick <bradfitz@tailscale.com> | 2025-04-04 09:29:54 -0700 |
| commit | afd7eebf91299549ae09a310f1763464d89928a9 (patch) | |
| tree | 6ef0428ff60a6e1c45ab077a04c8a535c02cc5fc /cmd/tsmcp | |
| parent | 7b29d39f45f2908178dd07120153e57ac3a914e6 (diff) | |
| download | tailscale-bradfitz/mcp.tar.xz tailscale-bradfitz/mcp.zip | |
cmd/tsmcp: add a MCP implementationbradfitz/mcp
Change-Id: I6e2a391dfe0929e6c44a537456fb2ea4c06b603b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Diffstat (limited to 'cmd/tsmcp')
| -rw-r--r-- | cmd/tsmcp/tsmcp.go | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/cmd/tsmcp/tsmcp.go b/cmd/tsmcp/tsmcp.go new file mode 100644 index 000000000..3016f8088 --- /dev/null +++ b/cmd/tsmcp/tsmcp.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "tailscale.com/client/local" + "tailscale.com/ipn" +) + +func main() { + var s Server + s.lc = new(local.Client) + + // Create MCP server + s.ms = server.NewMCPServer( + "Tailscale", + "1.0.0", + ) + + // Add tool + toolStatus := mcp.NewTool("get_connection_status", + mcp.WithDescription("Check Tailscale's connection status"), + // mcp.WithString("name", + // mcp.Required(), + // mcp.Description("Name of the person to greet"), + // ), + ) + + s.ms.AddTool(toolStatus, s.statusHandler) + + toolUp := mcp.NewTool("up", + mcp.WithDescription("Turn Tailscale on (run 'tailscale up')"), + ) + s.ms.AddTool(toolUp, s.upHandler) + + toolDown := mcp.NewTool("down", + mcp.WithDescription("Turn Tailscale off (run 'tailscale down')"), + ) + s.ms.AddTool(toolDown, s.downHandler) + + // Start the stdio server + if err := server.ServeStdio(s.ms); err != nil { + fmt.Printf("Server error: %v\n", err) + } +} + +type Server struct { + lc *local.Client + ms *server.MCPServer +} + +func (s *Server) statusHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + status, err := s.lc.StatusWithoutPeers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get connection status: %w", err) + } + switch status.BackendState { + case "NoState": + return mcp.NewToolResultText("In 'NoState', meaning it's broken or wedged or hung or maybe very early in its startup life cycle. But probably broken."), nil + case "Starting": + return mcp.NewToolResultText("In 'Starting', meaning it's starting up, but not yet fully connected. In particular, the control plane connection might be up, but no DERP yet."), nil + default: + return mcp.NewToolResultText(status.BackendState), nil + } +} + +func (s *Server) upHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{ + WantRunningSet: true, + Prefs: ipn.Prefs{ + WantRunning: true, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to turn on Tailscale: %w", err) + } + return mcp.NewToolResultText("done"), nil +} + +func (s *Server) downHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{ + WantRunningSet: true, + Prefs: ipn.Prefs{ + WantRunning: false, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to turn off Tailscale: %w", err) + } + return mcp.NewToolResultText("done"), nil +} |
