-
Notifications
You must be signed in to change notification settings - Fork 127
fix(mcp): name the non-text blocks a tool result drops #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Vasanthdev2004
wants to merge
2
commits into
main
Choose a base branch
from
fix/823-mcp-non-text-blocks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+229
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package mcp | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/Gitlawb/zero/internal/tools" | ||
| ) | ||
|
|
||
| // A server that returns only an image currently reports "(empty MCP tool | ||
| // result)": TextContent keeps text blocks and drops the rest, so a successful | ||
| // call looks like it produced nothing. The model then usually retries, which is | ||
| // the worst outcome, and the user is never told an image existed (#823). | ||
| // | ||
| // Carrying the payload is a separate change. Naming what was dropped is what | ||
| // stops the retry loop, and it has to be true of the DELIVERED result, so this | ||
| // drives registryTool.Run rather than the helper alone. | ||
| func TestAnImageOnlyResultSaysWhatItReturned(t *testing.T) { | ||
| tool := registryTool{ | ||
| client: &nonTextClient{content: []Content{ | ||
| {Type: "image", MimeType: "image/png"}, | ||
| }}, | ||
| server: Server{Name: "shots"}, | ||
| remote: RemoteTool{Name: "screenshot"}, | ||
| } | ||
|
|
||
| result := tool.Run(context.Background(), map[string]any{}) | ||
|
|
||
| if strings.Contains(result.Output, "(empty MCP tool result)") { | ||
| t.Fatalf("an image-only result still reports empty, so the model will retry:\n%s", result.Output) | ||
| } | ||
| if !strings.Contains(result.Output, "image/png") { | ||
| t.Errorf("the output does not name what the server returned:\n%s", result.Output) | ||
| } | ||
| // Naming the block is only half of it. Without the guidance the model still | ||
| // retries, which is the expensive symptom, so the wording is pinned too. | ||
| // | ||
| // "cannot recover this payload" and not "will return the same thing": every | ||
| // retry is a fresh call and the server may answer differently. What cannot | ||
| // change is that Zero has nowhere to put a non-text block. | ||
| if !strings.Contains(result.Output, "Retrying cannot recover this payload.") { | ||
| t.Errorf("the output does not tell the model retrying is pointless:\n%s", result.Output) | ||
| } | ||
| if strings.Contains(result.Output, "will return the same thing") { | ||
| t.Errorf("the output promises an identical response, which a fresh call cannot guarantee:\n%s", result.Output) | ||
| } | ||
| if result.Status != tools.StatusOK { | ||
| t.Errorf("status = %v, want OK: the call succeeded, we just cannot forward the payload", result.Status) | ||
| } | ||
| } | ||
|
|
||
| // The quieter half of the same bug: when a result carries text AND an image, | ||
| // the text arrives and the image vanishes with no mention at all. | ||
| func TestTextAlongsideAnImageStillReportsTheImage(t *testing.T) { | ||
| tool := registryTool{ | ||
| client: &nonTextClient{content: []Content{ | ||
| {Type: "text", Text: "captured the page"}, | ||
| {Type: "image", MimeType: "image/png"}, | ||
| }}, | ||
| server: Server{Name: "shots"}, | ||
| remote: RemoteTool{Name: "screenshot"}, | ||
| } | ||
|
|
||
| result := tool.Run(context.Background(), map[string]any{}) | ||
|
|
||
| if !strings.Contains(result.Output, "captured the page") { | ||
| t.Errorf("the text block was lost:\n%s", result.Output) | ||
| } | ||
| if !strings.Contains(result.Output, "image/png") { | ||
| t.Errorf("the dropped image was not mentioned:\n%s", result.Output) | ||
| } | ||
| } | ||
|
|
||
| // A text-only result must be byte-for-byte what it was before: this change adds | ||
| // a line only when something was actually dropped. | ||
| func TestATextOnlyResultIsUnchanged(t *testing.T) { | ||
| tool := registryTool{ | ||
| client: &nonTextClient{content: []Content{{Type: "text", Text: "plain answer"}}}, | ||
| server: Server{Name: "shots"}, | ||
| remote: RemoteTool{Name: "lookup"}, | ||
| } | ||
|
|
||
| if got := tool.Run(context.Background(), map[string]any{}).Output; got != "plain answer" { | ||
| t.Fatalf("output = %q, want exactly %q", got, "plain answer") | ||
| } | ||
| } | ||
|
|
||
| func TestDroppedContentSummaryNamesTheBlocks(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| content []Content | ||
| want string | ||
| }{ | ||
| { | ||
| name: "nothing dropped", | ||
| content: []Content{{Type: "text", Text: "hi"}}, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "no content at all", | ||
| content: nil, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "one image with a mime type", | ||
| content: []Content{{Type: "image", MimeType: "image/png"}}, | ||
| want: "1 image/png block", | ||
| }, | ||
| { | ||
| // A server may omit mimeType. Fall back to the block type rather than | ||
| // inventing one or printing an empty pair of slashes. | ||
| name: "one image without a mime type", | ||
| content: []Content{{Type: "image"}}, | ||
| want: "1 image block", | ||
| }, | ||
| { | ||
| name: "several of the same kind are counted, not repeated", | ||
| content: []Content{ | ||
| {Type: "image", MimeType: "image/png"}, | ||
| {Type: "image", MimeType: "image/png"}, | ||
| }, | ||
| want: "2 image/png blocks", | ||
| }, | ||
| { | ||
| // Order follows first appearance so the message is stable to read and | ||
| // to assert on. | ||
| name: "mixed kinds", | ||
| content: []Content{ | ||
| {Type: "text", Text: "ignored here"}, | ||
| {Type: "resource"}, | ||
| {Type: "audio", MimeType: "audio/wav"}, | ||
| {Type: "resource"}, | ||
| }, | ||
| want: "2 resource blocks, 1 audio/wav block", | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| if got := DroppedContentSummary(test.content); got != test.want { | ||
| t.Fatalf("DroppedContentSummary() = %q, want %q", got, test.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| type nonTextClient struct { | ||
| content []Content | ||
| } | ||
|
|
||
| func (client *nonTextClient) ListTools(context.Context) ([]RemoteTool, error) { return nil, nil } | ||
|
|
||
| func (client *nonTextClient) CallTool(context.Context, string, map[string]any) (CallToolResult, error) { | ||
| return CallToolResult{Content: client.content}, nil | ||
| } | ||
|
|
||
| func (client *nonTextClient) Close() error { return nil } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.