Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

mcp: typed tool handler adds zero structuredContent to an IsError result

Closed Beginner friendly
#1,387 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #1397 by @Ilyuha888 — merged

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go

Research direction

Start in mcp/server.go at toolForErr, where typed handler output is converted and validated; compare its handling of input requests and error results. The report includes a reproducer and says a test already passes with the proposed change. Confirm that IsError results omit generated structured content and skip output validation, while explicitly supplied StructuredContent remains available.

Written by the indexing model from the issue text.

Description

What did you do?

A typed tool handler reports a tool execution error the way the spec describes: it returns a result with IsError: true and text content, the zero Out, and a nil error. (Returning a Go error avoids the problem below, but then the SDK discards the handler's own result.)

type Out struct {
	Items []string `json:"items"`
}

func search(ctx context.Context, _ *mcp.CallToolRequest, in In) (*mcp.CallToolResult, Out, error) {
	return &mcp.CallToolResult{
		IsError: true,
		Content: []mcp.Content{&mcp.TextContent{Text: "no shop with this id"}},
	}, Out{}, nil
}

What did you see?

The result carries a zero structuredContent next to the error:

{"content":[{"type":"text","text":"no shop with this id"}],"structuredContent":{"items":null},"isError":true}

A client that reads structuredContent first gets {"items":null}, which looks like an empty successful result.

When the output schema rejects the zero value, the call fails with a protocol error instead of returning the tool error. With an OutputSchema whose required status property has "enum": ["ok", "partial"]:

calling "tools/call": validating tool output: validating root: validating /properties/status: enum:  does not equal any of: [ok partial]

What did you expect to see?

The error result as the handler built it: no structuredContent and no output validation, as in the spec's tool execution error example. The err != nil path already behaves this way.

Versions

go-sdk v1.8.0; same code on main at 452ac5a. Go 1.27.1, darwin/arm64.

Possible fix

In toolForErr (mcp/server.go), skip the output step for an error result, the same way it is skipped when the handler asks for input:

if asksForInput(res) || res.IsError {
	outval = nil
}

A handler that wants structured data on an error result can still set res.StructuredContent itself. I have this change with a test (go test ./... passes) and can send a PR if the approach works for you.

Full reproduction
package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/google/jsonschema-go/jsonschema"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type In struct {
	Query string `json:"query"`
}

type Out struct {
	Items []string `json:"items"`
}

type Report struct {
	Status string `json:"status"`
}

func failing[O any](ctx context.Context, _ *mcp.CallToolRequest, _ In) (*mcp.CallToolResult, O, error) {
	var zero O
	return &mcp.CallToolResult{
		IsError: true,
		Content: []mcp.Content{&mcp.TextContent{Text: "no shop with this id"}},
	}, zero, nil
}

func main() {
	ctx := context.Background()
	server := mcp.NewServer(&mcp.Implementation{Name: "repro", Version: "v0"}, nil)
	mcp.AddTool(server, &mcp.Tool{Name: "search"}, failing[Out])
	mcp.AddTool(server, &mcp.Tool{
		Name: "report",
		OutputSchema: &jsonschema.Schema{
			Type:       "object",
			Required:   []string{"status"},
			Properties: map[string]*jsonschema.Schema{"status": {Type: "string", Enum: []any{"ok", "partial"}}},
		},
	}, failing[Report])

	ct, st := mcp.NewInMemoryTransports()
	if _, err := server.Connect(ctx, st, nil); err != nil {
		panic(err)
	}
	cs, err := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0"}, nil).Connect(ctx, ct, nil)
	if err != nil {
		panic(err)
	}
	defer cs.Close()

	for _, name := range []string{"search", "report"} {
		res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: name, Arguments: map[string]any{"query": "x"}})
		if err != nil {
			fmt.Printf("%s: error: %v\n", name, err)
			continue
		}
		b, _ := json.Marshal(res)
		fmt.Printf("%s: %s\n", name, b)
	}
}

Output on v1.8.0 (_meta and resultType trimmed from the first line):

search: {"content":[{"type":"text","text":"no shop with this id"}],"structuredContent":{"items":null},"isError":true}
report: error: calling "tools/call": validating tool output: validating root: validating /properties/status: enum:  does not equal any of: [ok partial]
Dominant language
Go
Stars
5.2k
Forks
583
Avg merge
1d 13h
Merged PRs (30d)
53

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/go-sdk

All issues in modelcontextprotocol/go-sdk

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.