summaryrefslogtreecommitdiffhomepage
path: root/cmd/tsmcp
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2025-04-04 09:17:35 -0700
committerBrad Fitzpatrick <bradfitz@tailscale.com>2025-04-04 09:29:54 -0700
commitafd7eebf91299549ae09a310f1763464d89928a9 (patch)
tree6ef0428ff60a6e1c45ab077a04c8a535c02cc5fc /cmd/tsmcp
parent7b29d39f45f2908178dd07120153e57ac3a914e6 (diff)
downloadtailscale-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.go94
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
+}