Describe the bug
The SDK sets resultType on a CallToolResult only inside Server.callTool. A receiving middleware can reject a call by returning a result instead of calling next. That result never reaches Server.callTool, so the tools/call response carries no resultType. The 2026-07-28 schema requires the field, and strict clients reject the response as malformed.
This gap is the tools/call half of #1032. PR #1060 fixed the other half. It embedded completeResultWithType into CompleteResult, DiscoverResult, and the four list results, then called setCompleteResultType from ServerSession.handle.
PR #1060 left out the three multi-round-trip results: CallToolResult, GetPromptResult, and ReadResourceResult. Each one also carries input_required, so each holds its own unexported resultType field. They implement setResultType but not isCompleteResult(), so setCompleteResultType skips them.
A middleware that rejects a call is a normal use of a public extension point. Servers gate authentication, rate limits, and entitlements this way. But the SDK offers no way to build a spec-compliant CallToolResult outside the package:
resultType is unexported.
- The SDK exposes no setter for it.
isResult() seals Result, so no external type can implement it.
A server that uses AddReceivingMiddleware this way cannot return a compliant response.
Environment:
To Reproduce
The program below starts two servers. They differ only in whether a middleware short-circuits tools/call. Each one prints whether the response carries resultType.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const meta = `"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",` +
`"io.modelcontextprotocol/clientInfo":{"name":"repro","version":"1.0"},` +
`"io.modelcontextprotocol/clientCapabilities":{}}`
// gate rejects tools/call the way an auth, rate-limit, or entitlement
// middleware does: by returning a result instead of calling next.
func gate() mcp.Middleware {
return func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
if method != "tools/call" {
return next(ctx, method, req)
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "rate limit exceeded"}},
IsError: true,
}, nil
}
}
}
func newServer(withGate bool) *mcp.Server {
s := mcp.NewServer(&mcp.Implementation{Name: "repro", Version: "1.0.0"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "greet"},
func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "hi"}}}, nil, nil
})
if withGate {
s.AddReceivingMiddleware(gate())
}
return s
}
func callTool(s *mcp.Server) map[string]json.RawMessage {
ts := httptest.NewServer(mcp.NewStreamableHTTPHandler(
func(*http.Request) *mcp.Server { return s },
&mcp.StreamableHTTPOptions{Stateless: true}))
defer ts.Close()
body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"tools/call",`+
`"params":{"name":"greet","arguments":{},%s}}`, meta)
req, _ := http.NewRequest("POST", ts.URL, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("MCP-Protocol-Version", "2026-07-28")
req.Header.Set("Mcp-Method", "tools/call")
req.Header.Set("Mcp-Name", "greet")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
payload := raw
if strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") {
for _, line := range strings.Split(string(raw), "\n") {
if d, ok := strings.CutPrefix(strings.TrimRight(line, "\r"), "data: "); ok {
payload = []byte(d)
break
}
}
}
var msg struct {
Result map[string]json.RawMessage `json:"result"`
}
if err := json.Unmarshal(payload, &msg); err != nil {
panic(fmt.Sprintf("decode %v: %s", err, raw))
}
return msg.Result
}
func main() {
for _, tc := range []struct {
label string
withGate bool
}{
{"handler reaches dispatcher", false},
{"middleware short-circuits tools/call", true},
} {
rt, ok := callTool(newServer(tc.withGate))["resultType"]
fmt.Printf("%-38s resultType present=%-5v value=%s\n", tc.label, ok, rt)
}
}
Expected behavior
- Both responses carry
resultType, because both are tools/call results on a 2026-07-28 request.
Observed behavior
handler reaches dispatcher resultType present=true value="complete"
middleware short-circuits tools/call resultType present=false value=
Describe the bug
The SDK sets
resultTypeon aCallToolResultonly insideServer.callTool. A receiving middleware can reject a call by returning a result instead of callingnext. That result never reachesServer.callTool, so thetools/callresponse carries noresultType. The 2026-07-28 schema requires the field, and strict clients reject the response as malformed.This gap is the
tools/callhalf of #1032. PR #1060 fixed the other half. It embeddedcompleteResultWithTypeintoCompleteResult,DiscoverResult, and the four list results, then calledsetCompleteResultTypefromServerSession.handle.PR #1060 left out the three multi-round-trip results:
CallToolResult,GetPromptResult, andReadResourceResult. Each one also carriesinput_required, so each holds its own unexportedresultTypefield. They implementsetResultTypebut notisCompleteResult(), sosetCompleteResultTypeskips them.A middleware that rejects a call is a normal use of a public extension point. Servers gate authentication, rate limits, and entitlements this way. But the SDK offers no way to build a spec-compliant
CallToolResultoutside the package:resultTypeis unexported.isResult()sealsResult, so no external type can implement it.A server that uses
AddReceivingMiddlewarethis way cannot return a compliant response.Environment:
v1.7.0, which contains mcp: include resultType on new-protocol responses #1060.mainat0d3036f785b6.CallToolResultdoes not embedcompleteResultWithType, andsetCompleteResultTypeis unchanged.Stateless: true.To Reproduce
The program below starts two servers. They differ only in whether a middleware short-circuits
tools/call. Each one prints whether the response carriesresultType.Expected behavior
resultType, because both aretools/callresults on a 2026-07-28 request.Observed behavior