From 8dd314819c203764f7d43cf242369a57b0d4d959 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 31 Jul 2025 21:15:44 +0800 Subject: [PATCH 1/6] fix CallToolResult json marshaling and unmarshaling: need structuredContent . --- mcp/tools.go | 10 + mcp/tools_test.go | 562 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 571 insertions(+), 1 deletion(-) diff --git a/mcp/tools.go b/mcp/tools.go index 997bdc912..5999330a9 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -486,6 +486,11 @@ func (r CallToolResult) MarshalJSON() ([]byte, error) { } m["content"] = content + // Marshal StructuredContent if present + if r.StructuredContent != nil { + m["structuredContent"] = r.StructuredContent + } + // Marshal IsError if true if r.IsError { m["isError"] = r.IsError @@ -526,6 +531,11 @@ func (r *CallToolResult) UnmarshalJSON(data []byte) error { } } + // Unmarshal StructuredContent if present + if structured, ok := raw["structuredContent"]; ok { + r.StructuredContent = structured + } + // Unmarshal IsError if isError, ok := raw["isError"]; ok { if isErrorBool, ok := isError.(bool); ok { diff --git a/mcp/tools_test.go b/mcp/tools_test.go index 7beec31dd..458c9f705 100644 --- a/mcp/tools_test.go +++ b/mcp/tools_test.go @@ -306,7 +306,6 @@ func TestParseToolCallToolRequest(t *testing.T) { param15 := ParseInt64(request, "string_value", 1) assert.Equal(t, fmt.Sprintf("%T", param15), "int64") t.Logf("param15 type: %T,value:%v", param15, param15) - } func TestCallToolRequestBindArguments(t *testing.T) { @@ -580,6 +579,567 @@ func TestNewToolResultStructured(t *testing.T) { assert.NotNil(t, result.StructuredContent) } +// TestCallToolResultMarshalJSON tests the custom JSON marshaling of CallToolResult +func TestCallToolResultMarshalJSON(t *testing.T) { + tests := []struct { + name string + result CallToolResult + expected map[string]any + }{ + { + name: "basic result with text content", + result: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Hello, world!"}, + }, + IsError: false, + }, + expected: map[string]any{ + "_meta": map[string]any{"key": "value"}, + "content": []any{ + map[string]any{ + "type": "text", + "text": "Hello, world!", + }, + }, + }, + }, + { + name: "result with structured content", + result: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Operation completed"}, + }, + StructuredContent: map[string]any{ + "status": "success", + "count": 42, + "message": "Data processed successfully", + }, + IsError: false, + }, + expected: map[string]any{ + "_meta": map[string]any{"key": "value"}, + "content": []any{ + map[string]any{ + "type": "text", + "text": "Operation completed", + }, + }, + "structuredContent": map[string]any{ + "status": "success", + "count": float64(42), // JSON numbers are unmarshaled as float64 + "message": "Data processed successfully", + }, + }, + }, + { + name: "error result", + result: CallToolResult{ + Result: Result{ + Meta: map[string]any{"error_code": "E001"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "An error occurred"}, + }, + IsError: true, + }, + expected: map[string]any{ + "_meta": map[string]any{"error_code": "E001"}, + "content": []any{ + map[string]any{ + "type": "text", + "text": "An error occurred", + }, + }, + "isError": true, + }, + }, + { + name: "result with multiple content types", + result: CallToolResult{ + Result: Result{ + Meta: map[string]any{"session_id": "12345"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Processing complete"}, + ImageContent{Type: "image", Data: "base64-encoded-image-data", MIMEType: "image/jpeg"}, + }, + StructuredContent: map[string]any{ + "processed_items": 100, + "errors": 0, + }, + IsError: false, + }, + expected: map[string]any{ + "_meta": map[string]any{"session_id": "12345"}, + "content": []any{ + map[string]any{ + "type": "text", + "text": "Processing complete", + }, + map[string]any{ + "type": "image", + "data": "base64-encoded-image-data", + "mimeType": "image/jpeg", + }, + }, + "structuredContent": map[string]any{ + "processed_items": float64(100), + "errors": float64(0), + }, + }, + }, + { + name: "result with nil structured content", + result: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Simple result"}, + }, + StructuredContent: nil, + IsError: false, + }, + expected: map[string]any{ + "_meta": map[string]any{"key": "value"}, + "content": []any{ + map[string]any{ + "type": "text", + "text": "Simple result", + }, + }, + }, + }, + { + name: "result with empty content array", + result: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{}, + StructuredContent: map[string]any{ + "data": "structured only", + }, + IsError: false, + }, + expected: map[string]any{ + "_meta": map[string]any{"key": "value"}, + "content": []any{}, + "structuredContent": map[string]any{ + "data": "structured only", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Marshal the result + data, err := json.Marshal(tt.result) + assert.NoError(t, err) + + // Unmarshal to map for comparison + var result map[string]any + err = json.Unmarshal(data, &result) + assert.NoError(t, err) + + // Compare expected fields + for key, expectedValue := range tt.expected { + assert.Contains(t, result, key, "Result should contain key: %s", key) + assert.Equal(t, expectedValue, result[key], "Value for key %s should match", key) + } + + // Verify that unexpected fields are not present + for key := range result { + if key != "_meta" && key != "content" && key != "structuredContent" && key != "isError" { + t.Errorf("Unexpected field in result: %s", key) + } + } + }) + } +} + +// TestCallToolResultUnmarshalJSON tests the custom JSON unmarshaling of CallToolResult +func TestCallToolResultUnmarshalJSON(t *testing.T) { + tests := []struct { + name string + jsonData string + expected CallToolResult + wantErr bool + }{ + { + name: "basic result with text content", + jsonData: `{ + "_meta": {"key": "value"}, + "content": [ + {"type": "text", "text": "Hello, world!"} + ] + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Hello, world!"}, + }, + IsError: false, + }, + wantErr: false, + }, + { + name: "result with structured content", + jsonData: `{ + "_meta": {"key": "value"}, + "content": [ + {"type": "text", "text": "Operation completed"} + ], + "structuredContent": { + "status": "success", + "count": 42, + "message": "Data processed successfully" + } + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Operation completed"}, + }, + StructuredContent: map[string]any{ + "status": "success", + "count": float64(42), + "message": "Data processed successfully", + }, + IsError: false, + }, + wantErr: false, + }, + { + name: "error result", + jsonData: `{ + "_meta": {"error_code": "E001"}, + "content": [ + {"type": "text", "text": "An error occurred"} + ], + "isError": true + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"error_code": "E001"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "An error occurred"}, + }, + IsError: true, + }, + wantErr: false, + }, + { + name: "result with multiple content types", + jsonData: `{ + "_meta": {"session_id": "12345"}, + "content": [ + {"type": "text", "text": "Processing complete"}, + {"type": "image", "data": "base64-encoded-image-data", "mimeType": "image/jpeg"} + ], + "structuredContent": { + "processed_items": 100, + "errors": 0 + } + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"session_id": "12345"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Processing complete"}, + ImageContent{Type: "image", Data: "base64-encoded-image-data", MIMEType: "image/jpeg"}, + }, + StructuredContent: map[string]any{ + "processed_items": float64(100), + "errors": float64(0), + }, + IsError: false, + }, + wantErr: false, + }, + { + name: "result with nil structured content", + jsonData: `{ + "_meta": {"key": "value"}, + "content": [ + {"type": "text", "text": "Simple result"} + ] + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Simple result"}, + }, + StructuredContent: nil, + IsError: false, + }, + wantErr: false, + }, + { + name: "result with empty content array", + jsonData: `{ + "_meta": {"key": "value"}, + "content": [], + "structuredContent": { + "data": "structured only" + } + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: []Content{}, + StructuredContent: map[string]any{ + "data": "structured only", + }, + IsError: false, + }, + wantErr: false, + }, + { + name: "invalid JSON", + jsonData: `{invalid json}`, + wantErr: true, + }, + { + name: "result with missing content field", + jsonData: `{ + "_meta": {"key": "value"}, + "structuredContent": {"data": "no content"} + }`, + expected: CallToolResult{ + Result: Result{ + Meta: map[string]any{"key": "value"}, + }, + Content: nil, + StructuredContent: map[string]any{ + "data": "no content", + }, + IsError: false, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result CallToolResult + err := json.Unmarshal([]byte(tt.jsonData), &result) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + + // Compare Meta + if tt.expected.Meta != nil { + assert.Equal(t, tt.expected.Meta, result.Meta) + } + + // Compare Content + assert.Len(t, result.Content, len(tt.expected.Content)) + for i, expectedContent := range tt.expected.Content { + if i < len(result.Content) { + // Compare content types and values + switch expected := expectedContent.(type) { + case TextContent: + if actual, ok := result.Content[i].(TextContent); ok { + assert.Equal(t, expected.Text, actual.Text) + } else { + t.Errorf("Expected TextContent at index %d, got %T", i, result.Content[i]) + } + case ImageContent: + if actual, ok := result.Content[i].(ImageContent); ok { + assert.Equal(t, expected.Data, actual.Data) + assert.Equal(t, expected.MIMEType, actual.MIMEType) + } else { + t.Errorf("Expected ImageContent at index %d, got %T", i, result.Content[i]) + } + } + } + } + + // Compare StructuredContent + assert.Equal(t, tt.expected.StructuredContent, result.StructuredContent) + + // Compare IsError + assert.Equal(t, tt.expected.IsError, result.IsError) + }) + } +} + +// TestCallToolResultRoundTrip tests that marshaling and unmarshaling preserves all data +func TestCallToolResultRoundTrip(t *testing.T) { + original := CallToolResult{ + Result: Result{ + Meta: map[string]any{ + "session_id": "12345", + "user_id": "user123", + "timestamp": "2024-01-01T00:00:00Z", + }, + }, + Content: []Content{ + TextContent{Type: "text", Text: "Operation started"}, + ImageContent{Type: "image", Data: "base64-encoded-chart-data", MIMEType: "image/png"}, + TextContent{Type: "text", Text: "Operation completed successfully"}, + }, + StructuredContent: map[string]any{ + "status": "success", + "processed_count": 150, + "error_count": 0, + "warnings": []any{"Minor issue detected"}, + "metadata": map[string]any{ + "version": "1.0.0", + "build": "2024-01-01", + }, + }, + IsError: false, + } + + // Marshal to JSON + data, err := json.Marshal(original) + assert.NoError(t, err) + + // Unmarshal back + var unmarshaled CallToolResult + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err) + + // Verify all fields are preserved + assert.Equal(t, original.Meta, unmarshaled.Meta) + assert.Equal(t, original.IsError, unmarshaled.IsError) + assert.Equal(t, original.StructuredContent, unmarshaled.StructuredContent) + + // Verify content array + assert.Len(t, unmarshaled.Content, len(original.Content)) + for i, expectedContent := range original.Content { + if i < len(unmarshaled.Content) { + switch expected := expectedContent.(type) { + case TextContent: + if actual, ok := unmarshaled.Content[i].(TextContent); ok { + assert.Equal(t, expected.Text, actual.Text) + } else { + t.Errorf("Expected TextContent at index %d, got %T", i, unmarshaled.Content[i]) + } + case ImageContent: + if actual, ok := unmarshaled.Content[i].(ImageContent); ok { + assert.Equal(t, expected.Data, actual.Data) + assert.Equal(t, expected.MIMEType, actual.MIMEType) + } else { + t.Errorf("Expected ImageContent at index %d, got %T", i, unmarshaled.Content[i]) + } + } + } + } +} + +// TestCallToolResultEdgeCases tests edge cases for CallToolResult marshaling/unmarshaling +func TestCallToolResultEdgeCases(t *testing.T) { + tests := []struct { + name string + result CallToolResult + jsonData string + }{ + { + name: "result with complex structured content", + result: CallToolResult{ + Content: []Content{ + TextContent{Type: "text", Text: "Complex data returned"}, + }, + StructuredContent: map[string]any{ + "nested": map[string]any{ + "array": []any{1, 2, 3, "string", true, nil}, + "object": map[string]any{ + "deep": map[string]any{ + "value": "very deep", + }, + }, + }, + "mixed_types": []any{ + map[string]any{"type": "object"}, + "string", + 42.5, + true, + nil, + }, + }, + }, + }, + { + name: "result with empty structured content object", + result: CallToolResult{ + Content: []Content{ + TextContent{Type: "text", Text: "Empty structured content"}, + }, + StructuredContent: map[string]any{}, + }, + }, + { + name: "result with null structured content in JSON", + jsonData: `{ + "content": [{"type": "text", "text": "Null structured content"}], + "structuredContent": null + }`, + }, + { + name: "result with missing isError field", + jsonData: `{ + "content": [{"type": "text", "text": "No error field"}] + }`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var data []byte + var err error + + if tt.jsonData != "" { + // Test unmarshaling from JSON + var result CallToolResult + err = json.Unmarshal([]byte(tt.jsonData), &result) + assert.NoError(t, err) + + // Verify the result can be marshaled back + data, err = json.Marshal(result) + assert.NoError(t, err) + } else { + // Test marshaling the result + data, err = json.Marshal(tt.result) + assert.NoError(t, err) + + // Verify it can be unmarshaled back + var result CallToolResult + err = json.Unmarshal(data, &result) + assert.NoError(t, err) + } + + // Verify the JSON is valid + var jsonMap map[string]any + err = json.Unmarshal(data, &jsonMap) + assert.NoError(t, err) + }) + } +} + // TestNewItemsAPICompatibility tests that the new Items API functions // generate the same schema as the original Items() function with manual schema objects func TestNewItemsAPICompatibility(t *testing.T) { From 1314f4fd51d228a97c2ac088da67dcbe20dc541d Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 7 Aug 2025 09:55:29 +0800 Subject: [PATCH 2/6] golang json use float to store number type --- mcp/tools_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcp/tools_test.go b/mcp/tools_test.go index 458c9f705..cbb501520 100644 --- a/mcp/tools_test.go +++ b/mcp/tools_test.go @@ -1003,8 +1003,8 @@ func TestCallToolResultRoundTrip(t *testing.T) { }, StructuredContent: map[string]any{ "status": "success", - "processed_count": 150, - "error_count": 0, + "processed_count": float64(150.0), + "error_count": float64(0.0), "warnings": []any{"Minor issue detected"}, "metadata": map[string]any{ "version": "1.0.0", From 0ba984a0e45bea3d6def6022ce12f5054eb81ba3 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 8 Aug 2025 14:30:24 +0800 Subject: [PATCH 3/6] fix unittest: use NewMetaFromMap to create Result.Meta --- mcp/tools_test.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mcp/tools_test.go b/mcp/tools_test.go index cbb501520..25876a62d 100644 --- a/mcp/tools_test.go +++ b/mcp/tools_test.go @@ -590,7 +590,7 @@ func TestCallToolResultMarshalJSON(t *testing.T) { name: "basic result with text content", result: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{ TextContent{Type: "text", Text: "Hello, world!"}, @@ -611,7 +611,7 @@ func TestCallToolResultMarshalJSON(t *testing.T) { name: "result with structured content", result: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{ TextContent{Type: "text", Text: "Operation completed"}, @@ -642,7 +642,7 @@ func TestCallToolResultMarshalJSON(t *testing.T) { name: "error result", result: CallToolResult{ Result: Result{ - Meta: map[string]any{"error_code": "E001"}, + Meta: NewMetaFromMap(map[string]any{"error_code": "E001"}), }, Content: []Content{ TextContent{Type: "text", Text: "An error occurred"}, @@ -664,7 +664,7 @@ func TestCallToolResultMarshalJSON(t *testing.T) { name: "result with multiple content types", result: CallToolResult{ Result: Result{ - Meta: map[string]any{"session_id": "12345"}, + Meta: NewMetaFromMap(map[string]any{"session_id": "12345"}), }, Content: []Content{ TextContent{Type: "text", Text: "Processing complete"}, @@ -699,7 +699,7 @@ func TestCallToolResultMarshalJSON(t *testing.T) { name: "result with nil structured content", result: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{ TextContent{Type: "text", Text: "Simple result"}, @@ -721,7 +721,7 @@ func TestCallToolResultMarshalJSON(t *testing.T) { name: "result with empty content array", result: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{}, StructuredContent: map[string]any{ @@ -784,7 +784,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{ TextContent{Type: "text", Text: "Hello, world!"}, @@ -808,7 +808,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{ TextContent{Type: "text", Text: "Operation completed"}, @@ -833,7 +833,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"error_code": "E001"}, + Meta: NewMetaFromMap(map[string]any{"error_code": "E001"}), }, Content: []Content{ TextContent{Type: "text", Text: "An error occurred"}, @@ -857,7 +857,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"session_id": "12345"}, + Meta: NewMetaFromMap(map[string]any{"session_id": "12345"}), }, Content: []Content{ TextContent{Type: "text", Text: "Processing complete"}, @@ -881,7 +881,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{ TextContent{Type: "text", Text: "Simple result"}, @@ -902,7 +902,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: []Content{}, StructuredContent: map[string]any{ @@ -925,7 +925,7 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { }`, expected: CallToolResult{ Result: Result{ - Meta: map[string]any{"key": "value"}, + Meta: NewMetaFromMap(map[string]any{"key": "value"}), }, Content: nil, StructuredContent: map[string]any{ @@ -990,11 +990,11 @@ func TestCallToolResultUnmarshalJSON(t *testing.T) { func TestCallToolResultRoundTrip(t *testing.T) { original := CallToolResult{ Result: Result{ - Meta: map[string]any{ + Meta: NewMetaFromMap(map[string]any{ "session_id": "12345", "user_id": "user123", "timestamp": "2024-01-01T00:00:00Z", - }, + }), }, Content: []Content{ TextContent{Type: "text", Text: "Operation started"}, From 36d32f94a20082097b25a12aaa7acac37cdc3f89 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 8 Aug 2025 15:05:43 +0800 Subject: [PATCH 4/6] mcptest: add a test case to response stuctured content --- mcp/utils.go | 6 +++ mcptest/mcptest_test.go | 85 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/mcp/utils.go b/mcp/utils.go index 4d2b170b4..fd2ba0b0d 100644 --- a/mcp/utils.go +++ b/mcp/utils.go @@ -670,6 +670,12 @@ func ParseCallToolResult(rawMessage *json.RawMessage) (*CallToolResult, error) { result.Content = append(result.Content, content) } + // Handle structured content + structuredContent, ok := jsonContent["structuredContent"] + if ok { + result.StructuredContent = structuredContent + } + return &result, nil } diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index 18922cb84..00a1b606a 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -188,6 +188,91 @@ func TestServerWithResource(t *testing.T) { } } +func TestServerWithToolStructuredContent(t *testing.T) { + ctx := context.Background() + + srv, err := mcptest.NewServer(t, server.ServerTool{ + Tool: mcp.NewTool("get_user", + mcp.WithDescription("Gets user information with structured data."), + mcp.WithString("user_id", mcp.Description("The user ID to look up.")), + ), + Handler: structuredContentHandler, + }) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + client := srv.Client() + + var req mcp.CallToolRequest + req.Params.Name = "get_user" + req.Params.Arguments = map[string]any{ + "user_id": "123", + } + + result, err := client.CallTool(ctx, req) + if err != nil { + t.Fatal("CallTool:", err) + } + + if len(result.Content) != 1 { + t.Fatalf("Expected 1 content item, got %d", len(result.Content)) + } + + // Check text content (fallback) + textContent, ok := result.Content[0].(mcp.TextContent) + if !ok { + t.Fatalf("Expected content to be TextContent, got %T", result.Content[0]) + } + expectedText := "User found" + if textContent.Text != expectedText { + t.Errorf("Expected text %q, got %q", expectedText, textContent.Text) + } + + // Check structured content + if result.StructuredContent == nil { + t.Fatal("Expected StructuredContent to be present") + } + + structuredData, ok := result.StructuredContent.(map[string]any) + if !ok { + t.Fatalf("Expected StructuredContent to be map[string]any, got %T", result.StructuredContent) + } + + // Verify structured data + if structuredData["id"] != "123" { + t.Errorf("Expected id '123', got %v", structuredData["id"]) + } + if structuredData["name"] != "John Doe" { + t.Errorf("Expected name 'John Doe', got %v", structuredData["name"]) + } + if structuredData["email"] != "john@example.com" { + t.Errorf("Expected email 'john@example.com', got %v", structuredData["email"]) + } + if structuredData["active"] != true { + t.Errorf("Expected active true, got %v", structuredData["active"]) + } +} + +func structuredContentHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID, ok := request.GetArguments()["user_id"].(string) + if !ok { + return mcp.NewToolResultError("user_id parameter is required"), nil + } + + // Create structured data + userData := map[string]any{ + "id": userID, + "name": "John Doe", + "email": "john@example.com", + "active": true, + } + + // Use NewToolResultStructured to create result with both text fallback and structured content + return mcp.NewToolResultStructured(userData, "User found"), nil +} + func TestServerWithResourceTemplate(t *testing.T) { ctx := context.Background() From 6bdac1ee98fa6e137b18c761ec757c12e2ba21cc Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 8 Aug 2025 15:42:20 +0800 Subject: [PATCH 5/6] move after TestServerWithTool --- mcptest/mcptest_test.go | 178 +++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 85 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index 00a1b606a..d9807ffa9 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -78,6 +78,99 @@ func resultToString(result *mcp.CallToolResult) (string, error) { return b.String(), nil } +func TestServerWithToolStructuredContent(t *testing.T) { + ctx := context.Background() + + srv, err := mcptest.NewServer(t, server.ServerTool{ + Tool: mcp.NewTool("get_user", + mcp.WithDescription("Gets user information with structured data."), + mcp.WithString("user_id", mcp.Description("The user ID to look up.")), + ), + Handler: structuredContentHandler, + }) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + client := srv.Client() + + var req mcp.CallToolRequest + req.Params.Name = "get_user" + req.Params.Arguments = map[string]any{ + "user_id": "123", + } + + result, err := client.CallTool(ctx, req) + if err != nil { + t.Fatal("CallTool:", err) + } + + if result.IsError { + t.Fatalf("unexpected error result: %+v", result) + } + + if result.Meta.AdditionalFields["key"] != "value" { + t.Errorf("Expected Meta.AdditionalFields['key'] to be 'value', got %v", result.Meta.AdditionalFields["key"]) + } + + if len(result.Content) != 1 { + t.Fatalf("Expected 1 content item, got %d", len(result.Content)) + } + + // Check text content (fallback) + textContent, ok := result.Content[0].(mcp.TextContent) + if !ok { + t.Fatalf("Expected content to be TextContent, got %T", result.Content[0]) + } + expectedText := "User found" + if textContent.Text != expectedText { + t.Errorf("Expected text %q, got %q", expectedText, textContent.Text) + } + + // Check structured content + if result.StructuredContent == nil { + t.Fatal("Expected StructuredContent to be present") + } + + structuredData, ok := result.StructuredContent.(map[string]any) + if !ok { + t.Fatalf("Expected StructuredContent to be map[string]any, got %T", result.StructuredContent) + } + + // Verify structured data + if structuredData["id"] != "123" { + t.Errorf("Expected id '123', got %v", structuredData["id"]) + } + if structuredData["name"] != "John Doe" { + t.Errorf("Expected name 'John Doe', got %v", structuredData["name"]) + } + if structuredData["email"] != "john@example.com" { + t.Errorf("Expected email 'john@example.com', got %v", structuredData["email"]) + } + if structuredData["active"] != true { + t.Errorf("Expected active true, got %v", structuredData["active"]) + } +} + +func structuredContentHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID, ok := request.GetArguments()["user_id"].(string) + if !ok { + return mcp.NewToolResultError("user_id parameter is required"), nil + } + + // Create structured data + userData := map[string]any{ + "id": userID, + "name": "John Doe", + "email": "john@example.com", + "active": true, + } + + // Use NewToolResultStructured to create result with both text fallback and structured content + return mcp.NewToolResultStructured(userData, "User found"), nil +} + func TestServerWithPrompt(t *testing.T) { ctx := context.Background() @@ -188,91 +281,6 @@ func TestServerWithResource(t *testing.T) { } } -func TestServerWithToolStructuredContent(t *testing.T) { - ctx := context.Background() - - srv, err := mcptest.NewServer(t, server.ServerTool{ - Tool: mcp.NewTool("get_user", - mcp.WithDescription("Gets user information with structured data."), - mcp.WithString("user_id", mcp.Description("The user ID to look up.")), - ), - Handler: structuredContentHandler, - }) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - client := srv.Client() - - var req mcp.CallToolRequest - req.Params.Name = "get_user" - req.Params.Arguments = map[string]any{ - "user_id": "123", - } - - result, err := client.CallTool(ctx, req) - if err != nil { - t.Fatal("CallTool:", err) - } - - if len(result.Content) != 1 { - t.Fatalf("Expected 1 content item, got %d", len(result.Content)) - } - - // Check text content (fallback) - textContent, ok := result.Content[0].(mcp.TextContent) - if !ok { - t.Fatalf("Expected content to be TextContent, got %T", result.Content[0]) - } - expectedText := "User found" - if textContent.Text != expectedText { - t.Errorf("Expected text %q, got %q", expectedText, textContent.Text) - } - - // Check structured content - if result.StructuredContent == nil { - t.Fatal("Expected StructuredContent to be present") - } - - structuredData, ok := result.StructuredContent.(map[string]any) - if !ok { - t.Fatalf("Expected StructuredContent to be map[string]any, got %T", result.StructuredContent) - } - - // Verify structured data - if structuredData["id"] != "123" { - t.Errorf("Expected id '123', got %v", structuredData["id"]) - } - if structuredData["name"] != "John Doe" { - t.Errorf("Expected name 'John Doe', got %v", structuredData["name"]) - } - if structuredData["email"] != "john@example.com" { - t.Errorf("Expected email 'john@example.com', got %v", structuredData["email"]) - } - if structuredData["active"] != true { - t.Errorf("Expected active true, got %v", structuredData["active"]) - } -} - -func structuredContentHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - userID, ok := request.GetArguments()["user_id"].(string) - if !ok { - return mcp.NewToolResultError("user_id parameter is required"), nil - } - - // Create structured data - userData := map[string]any{ - "id": userID, - "name": "John Doe", - "email": "john@example.com", - "active": true, - } - - // Use NewToolResultStructured to create result with both text fallback and structured content - return mcp.NewToolResultStructured(userData, "User found"), nil -} - func TestServerWithResourceTemplate(t *testing.T) { ctx := context.Background() From d821d49ff7b5564da4fca7362971cb891053f288 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 11 Aug 2025 17:23:24 +0800 Subject: [PATCH 6/6] fix unittest: no need check meta --- mcptest/mcptest_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index d9807ffa9..3e4be38e3 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -110,10 +110,6 @@ func TestServerWithToolStructuredContent(t *testing.T) { t.Fatalf("unexpected error result: %+v", result) } - if result.Meta.AdditionalFields["key"] != "value" { - t.Errorf("Expected Meta.AdditionalFields['key'] to be 'value', got %v", result.Meta.AdditionalFields["key"]) - } - if len(result.Content) != 1 { t.Fatalf("Expected 1 content item, got %d", len(result.Content)) }