// Throwaway spike to validate that mark3labs/mcp-go's stdio client can drive // mcp-garmin (spawn, initialize handshake, call tools, parse results). // Not part of the production build — delete once internal/garmin is built. package main import ( "context" "encoding/json" "flag" "fmt" "log" "os" "strconv" "time" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" ) func main() { pythonPath := flag.String("python", "", "path to mcp-garmin .venv python executable") serverPath := flag.String("server", "", "path to mcp-garmin server.py") limit := flag.Int("limit", 5, "activity limit for get_activities") startDate := flag.String("start", "", "start date YYYY-MM-DD for get_activities (default: 365 days ago)") endDate := flag.String("end", "", "end date YYYY-MM-DD for get_activities (default: today)") flag.Parse() if *pythonPath == "" || *serverPath == "" { log.Fatal("usage: mcpspike -python -server ") } email := os.Getenv("GARMIN_EMAIL") password := os.Getenv("GARMIN_PASSWORD") if email == "" || password == "" { log.Fatal("GARMIN_EMAIL and GARMIN_PASSWORD must be set in the environment") } env := []string{ "GARMIN_EMAIL=" + email, "GARMIN_PASSWORD=" + password, "PYTHONUNBUFFERED=1", } c, err := client.NewStdioMCPClient(*pythonPath, env, *serverPath) if err != nil { log.Fatalf("spawn subprocess: %v", err) } defer c.Close() if stdio, ok := c.GetTransport().(*transport.Stdio); ok { go func() { buf := make([]byte, 4096) for { n, err := stdio.Stderr().Read(buf) if n > 0 { fmt.Fprint(os.Stderr, string(buf[:n])) } if err != nil { return } } }() } else { log.Println("warning: could not get stdio transport to forward subprocess stderr") } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() initReq := mcp.InitializeRequest{} initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION initReq.Params.ClientInfo = mcp.Implementation{Name: "mcpspike", Version: "0.0.1"} initRes, err := c.Initialize(ctx, initReq) if err != nil { log.Fatalf("initialize handshake failed: %v", err) } fmt.Printf("initialized OK: server=%s version=%s protocol=%s\n", initRes.ServerInfo.Name, initRes.ServerInfo.Version, initRes.ProtocolVersion) tools, err := c.ListTools(ctx, mcp.ListToolsRequest{}) if err != nil { log.Fatalf("list tools failed: %v", err) } fmt.Printf("server exposes %d tools:\n", len(tools.Tools)) for _, t := range tools.Tools { fmt.Printf(" - %s: %s\n", t.Name, t.Description) } fmt.Println("\ncalling authenticate()...") authRes, err := callTool(ctx, c, "authenticate", nil) if err != nil { log.Fatalf("authenticate call failed: %v", err) } fmt.Printf("authenticate() -> %s\n", authRes) if containsMFAPrompt(authRes) { fmt.Print("MFA required. Enter code: ") var code string fmt.Scanln(&code) mfaRes, err := callTool(ctx, c, "complete_mfa", map[string]any{"code": code}) if err != nil { log.Fatalf("complete_mfa call failed: %v", err) } fmt.Printf("complete_mfa() -> %s\n", mfaRes) } start := *startDate if start == "" { start = time.Now().AddDate(0, 0, -365).Format("2006-01-02") } end := *endDate if end == "" { end = time.Now().Format("2006-01-02") } fmt.Printf("\ncalling get_activities(start_date=%s, end_date=%s, limit=%d)...\n", start, end, *limit) actRes, err := callTool(ctx, c, "get_activities", map[string]any{ "start_date": start, "end_date": end, "limit": *limit, }) if err != nil { log.Fatalf("get_activities call failed: %v", err) } var activities []map[string]any if err := json.Unmarshal([]byte(actRes), &activities); err != nil { fmt.Printf("get_activities() returned non-JSON (likely an error string): %s\n", actRes) return } pretty, _ := json.MarshalIndent(activities, "", " ") fmt.Printf("get_activities() parsed OK, %d activities, %d bytes:\n%s\n", len(activities), len(actRes), truncate(string(pretty), 4000)) if len(activities) == 0 { fmt.Println("\nno activities in range, skipping get_activity_details") return } // Prefer a running activity if one is present, for the most relevant lap/split shape. chosen := activities[0] for _, a := range activities { if at, ok := a["activityType"].(map[string]any); ok { if tk, _ := at["typeKey"].(string); tk == "running" || tk == "trail_running" || tk == "treadmill_running" { chosen = a break } } } var activityID string switch v := chosen["activityId"].(type) { case float64: activityID = strconv.FormatFloat(v, 'f', -1, 64) default: activityID = fmt.Sprint(v) } fmt.Printf("\ncalling get_activity_details(activity_id=%s) [activityName=%v]...\n", activityID, chosen["activityName"]) splitsRes, err := callTool(ctx, c, "get_activity_splits", map[string]any{"activity_id": activityID}) if err != nil { log.Fatalf("get_activity_splits call failed: %v", err) } var splits map[string]any if err := json.Unmarshal([]byte(splitsRes), &splits); err != nil { fmt.Printf("get_activity_splits() returned non-JSON: %s\n", splitsRes) return } keys := make([]string, 0, len(splits)) for k := range splits { keys = append(keys, k) } fmt.Printf("get_activity_splits() top-level keys: %v\n", keys) prettySplits, _ := json.MarshalIndent(splits, "", " ") fmt.Printf("get_activity_splits() parsed OK, %d bytes:\n%s\n", len(splitsRes), truncate(string(prettySplits), 8000)) fmt.Println("\ncalling get_activity_details() again to inspect metricDescriptors + a small metrics sample...") detailsRes, err := callTool(ctx, c, "get_activity_details", map[string]any{"activity_id": activityID}) if err != nil { log.Fatalf("get_activity_details call failed: %v", err) } var details map[string]any if err := json.Unmarshal([]byte(detailsRes), &details); err != nil { fmt.Printf("get_activity_details() returned non-JSON: %s\n", detailsRes) return } if descriptors, ok := details["metricDescriptors"]; ok { pretty, _ := json.MarshalIndent(descriptors, "", " ") fmt.Printf("metricDescriptors:\n%s\n", pretty) } else { fmt.Println("no metricDescriptors key found") } if rows, ok := details["activityDetailMetrics"].([]any); ok && len(rows) > 0 { sampleN := 5 if len(rows) < sampleN { sampleN = len(rows) } pretty, _ := json.MarshalIndent(rows[:sampleN], "", " ") fmt.Printf("activityDetailMetrics sample (first %d of %d rows):\n%s\n", sampleN, len(rows), pretty) } } func callTool(ctx context.Context, c *client.Client, name string, args map[string]any) (string, error) { req := mcp.CallToolRequest{} req.Params.Name = name req.Params.Arguments = args res, err := c.CallTool(ctx, req) if err != nil { return "", err } if res.IsError { return "", fmt.Errorf("tool %s returned an error result", name) } var out string for _, content := range res.Content { if tc, ok := content.(mcp.TextContent); ok { out += tc.Text } } return out, nil } func containsMFAPrompt(s string) bool { for _, needle := range []string{"MFA required", "MFA code", "complete_mfa"} { if len(s) >= len(needle) { for i := 0; i+len(needle) <= len(s); i++ { if s[i:i+len(needle)] == needle { return true } } } } return false } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "...(truncated)" }