package mcp import ( "encoding/json" "testing" ) func TestInitializeReturnsCapabilities(t *testing.T) { s := &Server{} req := []byte(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude","version":"1.0"}}}`) resp := s.dispatch("", req) if resp == nil { t.Fatal("expected response for initialize") } var parsed struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Result map[string]interface{} `json:"result"` Error *struct { Code int `json:"code"` } `json:"error"` } if err := json.Unmarshal(resp, &parsed); err != nil { t.Fatalf("parse: %v\n%s", err, resp) } if parsed.Error != nil { t.Fatalf("initialize returned error: %+v", parsed.Error) } if parsed.Result["protocolVersion"] == nil { t.Fatalf("missing protocolVersion: %+v", parsed.Result) } caps, ok := parsed.Result["capabilities"].(map[string]interface{}) if !ok { t.Fatalf("capabilities not object: %+v", parsed.Result) } if caps["tools"] == nil { t.Fatalf("tools capability missing: %+v", caps) } // patterm-specific orientation: clients show this to the underlying // LLM, so it's our primary hook for steering vendor TUIs (codex in // particular) toward the MCP tool surface instead of shell-ing out. instructions, ok := parsed.Result["instructions"].(string) if !ok || instructions == "" { t.Fatalf("instructions missing or wrong type: %+v", parsed.Result) } } func TestInitializedNotificationSuppressesResponse(t *testing.T) { s := &Server{} req := []byte(`{"jsonrpc":"2.0","method":"notifications/initialized"}`) resp := s.dispatch("", req) if resp != nil { t.Fatalf("notification produced a response: %s", resp) } } func TestToolsListReturnsConcreteSchemas(t *testing.T) { s := &Server{} req := []byte(`{"jsonrpc":"2.0","id":2,"method":"tools/list"}`) resp := s.dispatch("", req) if resp == nil { t.Fatal("expected response for tools/list") } var parsed struct { Result map[string]interface{} `json:"result"` Error *struct { Code int `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(resp, &parsed); err != nil { t.Fatalf("parse: %v\n%s", err, resp) } if parsed.Error != nil { t.Fatalf("tools/list returned error: %+v", parsed.Error) } tools, ok := parsed.Result["tools"].([]interface{}) if !ok { t.Fatalf("tools not array: %+v", parsed.Result) } if len(tools) == 0 { t.Fatalf("expected at least one tool, got 0") } // Every tool must have name, description, and inputSchema with // `type=object` and a concrete `properties` object — `properties: // null` trips up strict MCP clients (claude in particular). for i, tool := range tools { entry, ok := tool.(map[string]interface{}) if !ok { t.Fatalf("tool %d not object: %#v", i, tool) } if entry["name"] == "" || entry["name"] == nil { t.Fatalf("tool %d missing name: %#v", i, entry) } if entry["description"] == "" || entry["description"] == nil { t.Fatalf("tool %d missing description: %#v", i, entry) } schema, ok := entry["inputSchema"].(map[string]interface{}) if !ok { t.Fatalf("tool %d inputSchema not object: %#v", i, entry) } if schema["type"] != "object" { t.Fatalf("tool %d schema type != object: %#v", i, schema) } props, ok := schema["properties"] if !ok { t.Fatalf("tool %s missing properties", entry["name"]) } if _, ok := props.(map[string]interface{}); !ok { t.Fatalf("tool %s properties not object (got %T): %#v", entry["name"], props, props) } } } func TestPingReturnsEmptyObject(t *testing.T) { s := &Server{} req := []byte(`{"jsonrpc":"2.0","id":3,"method":"ping"}`) resp := s.dispatch("", req) if resp == nil { t.Fatal("expected response for ping") } var parsed struct { Result map[string]interface{} `json:"result"` Error *struct{ Code int } `json:"error"` } if err := json.Unmarshal(resp, &parsed); err != nil { t.Fatalf("parse: %v\n%s", err, resp) } if parsed.Error != nil { t.Fatalf("ping returned error: %+v", parsed.Error) } if parsed.Result == nil { t.Fatal("ping result missing") } } func TestTypedInvalidArgsMapToInvalidParams(t *testing.T) { for _, errKind := range []string{ErrorKindInvalidArgs, ErrorKindInvalidKind} { _, code, msg, data := mapToolError(Errorf(errKind, "bad args")) if code != codeInvalidParams { t.Fatalf("%s code = %d, want %d", errKind, code, codeInvalidParams) } if msg != "bad args" { t.Fatalf("%s message = %q", errKind, msg) } kind, ok := data.(map[string]string) if !ok || kind["kind"] != errKind { t.Fatalf("%s data = %#v", errKind, data) } } }