Skip to content

ServerSession.Elicit: accept response without content panics the server ("assignment to entry in nil map") #1062

Closed

Description

@Yuncun

Summary

If an elicitation client replies {"action": "accept"} with the content field omitted (or null), and the requested schema has any property with a default, the server process panics with assignment to entry in nil map. The panic originates in jsonschema-go's ApplyDefaults but is reached through ServerSession.Elicit, so a peer's response shape can kill a Go MCP server outright.

Versions

  • github.com/modelcontextprotocol/go-sdk v1.6.1 (latest release)
  • github.com/google/jsonschema-go v0.4.3 (latest; indirect)
  • Go 1.26

Reproduction (public API, in-memory transports)

package main

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

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

func main() {
	ctx := context.Background()

	server := mcp.NewServer(&mcp.Implementation{Name: "s", Version: "0.0.1"}, nil)
	client := mcp.NewClient(&mcp.Implementation{Name: "c", Version: "0.0.1"}, &mcp.ClientOptions{
		// A spec-loose client: accepts, but omits content entirely.
		ElicitationHandler: func(ctx context.Context, req *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
			return &mcp.ElicitResult{Action: "accept"}, nil
		},
	})

	st, ct := mcp.NewInMemoryTransports()
	ss, err := server.Connect(ctx, st, nil)
	if err != nil {
		panic(err)
	}
	if _, err := client.Connect(ctx, ct, nil); err != nil {
		panic(err)
	}

	res, err := ss.Elicit(ctx, &mcp.ElicitParams{
		Message: "pick",
		RequestedSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"x": {Type: "boolean", Default: json.RawMessage(`true`)},
			},
		},
	})
	fmt.Println("Elicit returned:", res, err)
}

Output (Go 1.26, darwin/arm64 — same on a stdio transport):

panic: assignment to entry in nil map

goroutine 1 [running]:
reflect.mapassign_faststr0(...)
reflect.Value.SetMapIndex(...)
github.com/google/jsonschema-go/jsonschema.(*state).applyDefaults(...)
	.../jsonschema-go@v0.4.3/jsonschema/validate.go:741

Mechanism

In ServerSession.Elicit (mcp/server.go ~1298–1304, v1.6.1):

  1. The client's {"action":"accept"} unmarshals with res.Content as a nil map[string]any.
  2. resolved.Validate(res.Content) passes — a nil map still reflects as Kind() == Map, and with no required properties an empty object is valid.
  3. resolved.ApplyDefaults(&res.Content) walks the schema, finds a property with a default that is missing from the instance, and assigns via instance.SetMapIndex(...) (jsonschema-go validate.go:741) — assignment into the nil map, panic.

The panic propagates out of whatever server code called Elicit (typically a tool handler); the SDK does not recover panics there, so the process dies and every session it serves dies with it.

Minimal jsonschema-go-only repro of steps 2+3, in case you'd rather route part of the fix there (happy to split this into a jsonschema-go issue if that's preferred):

schema := &jsonschema.Schema{
	Type: "object",
	Properties: map[string]*jsonschema.Schema{
		"x": {Type: "string", Default: json.RawMessage(`"hi"`)},
	},
}
resolved, _ := schema.Resolve(nil)
var content map[string]any
_ = resolved.Validate(content)       // passes
_ = resolved.ApplyDefaults(&content) // panics: assignment to entry in nil map

Why this matters

Whether accept-without-content is spec-conformant is debatable (the schema here has no required fields, so "accept with all defaults" is a reasonable client reading) — but either way, a response crossing a trust boundary shouldn't be able to terminate the receiving process. Any long-running server that elicits with defaults is one loose client away from a crash.

Suggested direction

Either (or both):

  • In ServerSession.Elicit (and the client twin at mcp/client.go:643): normalize a nil res.Content to an empty map[string]any{} before Validate/ApplyDefaults.
  • In jsonschema-go ApplyDefaults: when handed a pointer to a nil map, allocate through the pointer before assigning (it already has the settable lvalue).

Workaround we use

Wrap the Elicit call in a defer/recover and fold the panic into the same fallback path as "client can't elicit", so the consent surface survives a client that gets this wrong.

Activity

  1. guglielmo-san commented on Jul 9, 2026

    @guglielmo-san
    Contributor

    Hi @Yuncun, thank you for flagging this issue. I think you should report it on the jsonschema-go repo too.

  2. guglielmo-san commented on Jul 10, 2026

    @guglielmo-san
    Contributor

    Fixed in #1069

  3. added a commit that references this issue on Jul 10, 2026
    ce393ac
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