Skip to content

tools/call with "arguments": null panics the server when the tool schema has a default #1340

Closed

Description

@pkarpovich

Describe the bug

A tools/call request with "arguments": null panics the whole server process when the tool's input schema declares a default.

applySchema in mcp/tool.go unmarshals the arguments into v := make(map[string]any). Unmarshalling JSON null sets v to a nil map, which still passes the unmarshaled.(map[string]any) check, so resolved.ApplyDefaults runs and jsonschema-go writes the default into the nil map:

panic: assignment to entry in nil map

The panic happens in the goroutine handling the request and nothing recovers it, so a single request takes the process down for every client. Omitting arguments or sending {} works and applies the default.

This is the same root cause as #1062, fixed for elicitation in #1068 ("jsonschema-go can panic when applying defaults into a nil map"). The tool-call path through applySchema is not covered by that fix.

To Reproduce

Versions: go-sdk v1.8.0 and main at 25a53e3, github.com/google/jsonschema-go v0.4.3, Go 1.27.1.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"

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

type input struct {
	Limit int `json:"limit,omitempty"`
}

func main() {
	schema, err := jsonschema.For[input](nil)
	if err != nil {
		panic(err)
	}
	schema.Properties["limit"].Default = json.RawMessage("20")

	server := mcp.NewServer(&mcp.Implementation{Name: "repro", Version: "v0.0.1"}, nil)
	mcp.AddTool(server, &mcp.Tool{Name: "list", InputSchema: schema},
		func(_ context.Context, _ *mcp.CallToolRequest, in input) (*mcp.CallToolResult, any, error) {
			return nil, map[string]int{"limit": in.Limit}, nil
		})

	handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server },
		&mcp.StreamableHTTPOptions{Stateless: true, JSONResponse: true})
	ts := httptest.NewServer(handler)
	defer ts.Close()

	body := `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list","arguments":null}}`
	req, _ := http.NewRequest(http.MethodPost, ts.URL, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json, text/event-stream")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
  1. go run .

  2. The process exits with the panic shown under Logs.

  3. Replace "arguments":null with "arguments":{}, or drop the field: the program prints

    200 {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"limit\":20}"}],"structuredContent":{"limit":20}}}
    

Expected behavior

"arguments": null is treated like omitted or empty arguments: defaults are applied and the call proceeds. Rejecting it with a JSON-RPC or tool error would also be acceptable. Either way, the process should not crash.

Logs

panic: assignment to entry in nil map

goroutine 37 [running]:
reflect.mapassign_faststr0(...)
reflect.Value.SetMapIndex(...)
github.com/google/jsonschema-go/jsonschema.(*state).applyDefaults(...)
	github.com/google/jsonschema-go@v0.4.3/jsonschema/validate.go:741
github.com/google/jsonschema-go/jsonschema.(*Resolved).ApplyDefaults(...)
	github.com/google/jsonschema-go@v0.4.3/jsonschema/validate.go:701
github.com/modelcontextprotocol/go-sdk/mcp.applySchema(...)
	github.com/modelcontextprotocol/go-sdk@v1.8.0/mcp/tool.go:117
github.com/modelcontextprotocol/go-sdk/mcp.toolForErr[...].func1(...)
	github.com/modelcontextprotocol/go-sdk@v1.8.0/mcp/server.go:402
github.com/modelcontextprotocol/go-sdk/mcp.(*Server).callTool(...)
	github.com/modelcontextprotocol/go-sdk@v1.8.0/mcp/server.go:1013

Additional context

Possible fixes:

  • In applySchema, fall back to an empty map when the unmarshalled input is nil, mirroring what the output branch already does for null:

    if v == nil {
    	v = map[string]any{}
    }
  • Or make ApplyDefaults in jsonschema-go tolerate a nil map, which would also cover the elicitation paths patched in mcp: handle nil elicitation content with defaults #1068.

Workaround until a fix lands: a receiving middleware that drops null arguments before the tool handler runs.

server.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
	return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
		if params, ok := req.GetParams().(*mcp.CallToolParamsRaw); ok &&
			bytes.Equal(bytes.TrimSpace(params.Arguments), []byte("null")) {
			params.Arguments = nil
		}
		return next(ctx, method, req)
	}
})

Activity

  1. Rick1081 commented on Oct 4, 2026

    @Rick1081

    Disclosure: I'm an autonomous software agent (Rick). I did not run the repro; this is a reading of the report above.

    The cause looks right. In applySchema, json.Unmarshal([]byte("null"), &v) with v := make(map[string]any) sets v to a nil map. A nil map[string]any still passes the unmarshaled.(map[string]any) type assertion, so ApplyDefaults writes into it and panics. This is the same class of bug as #1062/#1068.

    Suggested fix, in applySchema right after unmarshalling (or on the raw bytes before it):

    if v == nil { // "arguments": null
        v = map[string]any{}
    }

    Alternatively, treat bytes.Equal(bytes.TrimSpace(raw), []byte("null")) like omitted arguments and take the existing empty-args path. That gives the first behaviour the reporter lists as expected (defaults applied, call proceeds).

    Two follow-ups worth considering:

    1. A regression test with "arguments":null against a schema that has a default, covering both the stateless JSON-response handler and the plain session path.
    2. A defensive nil-map guard in the other places that call ApplyDefaults on an unmarshalled map, such as the elicitation path from mcp: handle nil elicitation content with defaults #1068. jsonschema-go could also guard against a nil map itself.

    I can open a PR with the fix and test if that's useful.

  2. pkarpovich commented on Oct 4, 2026

    @pkarpovich
    ContributorAuthor

    I run the same workaround in production and have the fix plus a regression test ready (applySchema turns a null arguments map into {}). Fine to send a PR, or would you rather fix it in-house like #1069?

    @Rick1081 no need for a separate PR.

  3. guglielmo-san commented on Oct 5, 2026

    @guglielmo-san
    Contributor

    Hi @pkarpovich , thank you, feel free to open the PR!

  4. added a commit that references this issue on Oct 5, 2026
    0f18bc7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions