From afd7eebf91299549ae09a310f1763464d89928a9 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 4 Apr 2025 09:17:35 -0700 Subject: [PATCH] cmd/tsmcp: add a MCP implementation Change-Id: I6e2a391dfe0929e6c44a537456fb2ea4c06b603b Signed-off-by: Brad Fitzpatrick --- cmd/tsmcp/tsmcp.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 4 ++ 3 files changed, 100 insertions(+) create mode 100644 cmd/tsmcp/tsmcp.go 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 +} diff --git a/go.mod b/go.mod index 8ca56a4b9..11d837134 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.17.11 github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a + github.com/mark3labs/mcp-go v0.18.0 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/mdlayher/genetlink v1.3.2 @@ -156,6 +157,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go-simpler.org/musttag v0.9.0 // indirect go-simpler.org/sloglint v0.5.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index ca1b5e30c..0970bce6d 100644 --- a/go.sum +++ b/go.sum @@ -656,6 +656,8 @@ github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao= +github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -989,6 +991,8 @@ github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=