package mcp import ( "bufio" "context" "encoding/json" "fmt" "os" "os/exec" "strings" "testing" ) // The stdio transport is tested against a real subprocess — this test binary, // re-executed with MAVEN_MCP_FAKE set, acting as a minimal MCP server. No // python, no fixture file, no network. func TestMain(m *testing.M) { if os.Getenv("MAVEN_MCP_FAKE") != "" { fakeStdioServer() return } os.Exit(m.Run()) } func fakeStdioServer() { h := echoServer() sc := bufio.NewScanner(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" { continue } var req struct { ID *int64 `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` } if json.Unmarshal([]byte(line), &req) != nil { continue } if req.ID == nil { // A notification gets no reply, but we emit an unrelated // notification so the client's frame-skipping is exercised. _, _ = out.WriteString("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\"}\n") _ = out.Flush() continue } result, rerr := h(req.Method, req.Params) resp := map[string]any{"jsonrpc": "2.0", "id": *req.ID} if rerr != nil { resp["error"] = map[string]any{"code": rerr.Code, "message": rerr.Message} } else { resp["result"] = result } raw, _ := json.Marshal(resp) _, _ = out.Write(append(raw, '\n')) _ = out.Flush() if os.Getenv("MAVEN_MCP_FAKE") == "die" && req.Method == "tools/list" { return // hang up, so the reconnect path has something to see } } } func stdioManager(t *testing.T, mode string) *Manager { t.Helper() self, err := os.Executable() if err != nil { t.Skipf("no executable path: %v", err) } if _, err := exec.LookPath(self); err != nil && !strings.Contains(self, "/") { t.Skip("test binary not executable") } m, err := NewManager(nil, []ServerConfig{{ Name: "fake", Command: self, Env: []string{"MAVEN_MCP_FAKE=" + mode}, Enabled: true, }}) if err != nil { t.Fatal(err) } m.Connect(context.Background()) return m } func TestStdioTransportEndToEnd(t *testing.T) { m := stdioManager(t, "1") defer m.Close() st := m.Status() if len(st) != 1 || !st[0].Connected { t.Fatalf("status = %+v", st) } if st[0].Transport != "stdio" { t.Fatalf("transport = %q", st[0].Transport) } if got := len(m.Tools()); got != 2 { t.Fatalf("tools = %d", got) } out, err := m.Call(context.Background(), "fake", "read_thing", map[string]any{"q": "стдио"}) if err != nil { t.Fatalf("call: %v", err) } if out != "read_thing:стдио" { t.Fatalf("out = %q", out) } res := m.Resources(context.Background()) if len(res) != 1 || res[0].URI != "note://one" { t.Fatalf("resources = %+v", res) } body, err := m.ReadResource(context.Background(), "fake", "note://one") if err != nil { t.Fatal(err) } if body != "тело ресурса" { t.Fatalf("body = %q", body) } } func TestStdioServerThatDiesIsNotUsable(t *testing.T) { m := stdioManager(t, "die") defer m.Close() // The server hung up after tools/list; the next call must fail cleanly // rather than hang or panic. if _, err := m.Call(context.Background(), "fake", "read_thing", nil); err == nil { t.Fatal("a call into a dead server must error") } } func TestStdioMissingCommand(t *testing.T) { m, err := NewManager(nil, []ServerConfig{{ Name: "nope", Command: "/nonexistent/mcp-server-that-is-not-there", Enabled: true, }}) if err != nil { t.Fatal(err) } m.Connect(context.Background()) st := m.Status() if st[0].Connected || st[0].Err == "" { t.Fatalf("a missing binary must be recorded, not fatal: %+v", st) } if got := len(m.Tools()); got != 0 { t.Fatalf("tools = %d", got) } if !strings.Contains(fmt.Sprint(st[0].Err), "start") { t.Logf("err = %q", st[0].Err) } }