Skip to content

mcp.AddTool crashes the server with SIGSEGV when input/output struct tags trigger a nil schema from jsonschema-go — missing nil-guard in setSchema #916

Closed

Description

@fstienhans-lg

Summary

When registering a tool with mcp.AddTool[In, Out], the SDK calls jsonschema.ForType and then unconditionally invokes internalSchema.Resolve(...) on the result. If ForType returns a partially-nil schema tree (which happens with certain jsonschema:"..." struct-tag descriptions — see linked upstream issue), the call panics with a nil-pointer dereference at server startup, taking down the whole process.

The SDK should defend against a nil or malformed schema returned by jsonschema-go and surface a clear error (or panic with an actionable message that names the offending Go type and field), not segfault inside a third-party library.

Versions

  • github.com/modelcontextprotocol/go-sdk v1.4.0 and v1.6.0 (same code path)
  • github.com/google/jsonschema-go v0.4.2 / v0.4.3
  • Go 1.24, macOS arm64

Stack trace

[signal SIGSEGV: segmentation violation code=0x2 addr=0x10 pc=0x102b9c36c]

goroutine 1 [running]:
github.com/google/jsonschema-go/jsonschema.detectDraft(...)
    .../jsonschema-go@v0.4.2/jsonschema/resolve.go:52
github.com/google/jsonschema-go/jsonschema.newResolved(...)
    .../jsonschema-go@v0.4.2/jsonschema/resolve.go:43
github.com/google/jsonschema-go/jsonschema.(*resolver).resolve(0x..., 0x0, 0x...)
    .../jsonschema-go@v0.4.2/jsonschema/resolve.go:211 +0x2c
github.com/google/jsonschema-go/jsonschema.(*Schema).Resolve(0x0, 0x...)
    .../jsonschema-go@v0.4.2/jsonschema/resolve.go:185 +0x110
github.com/modelcontextprotocol/go-sdk/mcp.setSchema[...](...)
    .../go-sdk@v1.4.0/mcp/server.go:466 +0x1a0
github.com/modelcontextprotocol/go-sdk/mcp.toolForErr[...](...)
    .../go-sdk@v1.4.0/mcp/server.go:309 +0x230
github.com/modelcontextprotocol/go-sdk/mcp.AddTool[...](...)
    .../go-sdk@v1.4.0/mcp/server.go:501 +0x38

The receiver of Resolve is 0x0 — a nil *jsonschema.Schema.

Where the SDK should guard

mcp/server.go setSchema (current code, v1.6.0 line ~436):

internalSchema, err = jsonschema.ForType(rt, &jsonschema.ForOptions{})
if err != nil {
    return zero, err
}
*sfield = internalSchema

resolved, err := internalSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})

internalSchema is checked for err, never for nil. If ForType returns (nil, nil) (or a tree containing a nil sub-schema), the next call panics inside jsonschema-go.

Suggested fix

In setSchema (and the analogous *sfield/providedSchema branches further down):

internalSchema, err = jsonschema.ForType(rt, &jsonschema.ForOptions{})
if err != nil {
    return zero, err
}
if internalSchema == nil {
    return zero, fmt.Errorf("mcp: jsonschema.ForType returned nil schema for %s", rt)
}
*sfield = internalSchema

For added safety in toolForErr / AddTool, wrap the schema-related calls in a defer recover() that converts a panic to an error mentioning the tool name and the Go type, so a single bad tag in one tool does not take the whole server down at startup. Something like:

func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCache) (rt *Tool, rh ToolHandler, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("mcp: schema generation panicked for tool %q (In=%T): %v",
                t.Name, *new(In), r)
        }
    }()
    // ... existing code
}

That alone would have transformed our outage into a clear actionable error pointing at the tool name and input type.

Trigger

Reproducible by adding a tool whose input struct uses jsonschema:"..." descriptions containing combinations of commas, =, >, <, em/en dashes, parens, brackets, single quotes, or non-ASCII characters. The underlying jsonschema-go bug is reported separately at:

https://github.com/google/jsonschema-go/issues/NEW (link once filed)

The SDK fix is independent: regardless of whether jsonschema-go ever produces a nil schema, the SDK should not segfault as a result.

Workaround

Restrict every jsonschema:"..." tag description in tool input/output structs to ASCII letters and spaces only. No commas, no ,required directive, no parens, brackets, quotes, equals, angle brackets, em/en dashes, or non-ASCII characters.

Impact

This is a denial-of-service-grade bug for any server that registers tools dynamically based on user-authored Go types. A single malformed tag anywhere in the registered toolset prevents the server from starting and produces no actionable error message — just a stack trace pointing into a third-party library. Adding a nil-check and a panic-to-error conversion turns an outage into a clear, fixable error.

Activity

  1. jasondillingham commented on May 3, 2026

    @jasondillingham

    Reproducer (no malformed jsonschema tag needed):

    import "github.com/google/jsonschema-go/jsonschema"
    
    var nilSchema *jsonschema.Schema = nil
    mcp.AddTool(s, &mcp.Tool{
        Name:        "broken",
        InputSchema: nilSchema, // typed-nil into any
    }, handler)

    Panics with runtime error: invalid memory address or nil pointer dereference — no tool name, no field, no actionable info. Mechanism: typed nil in any is not equal to untyped nil, so t.InputSchema == nil is false. The (*jsonschema.Schema) type assertion succeeds with a nil pointer, then Resolve panics. Same shape on the auto-generation path if ForType ever returned (nil, nil) — defensive only since today it doesn't.

    Related: Server.AddTool (the method, line 250) hits the same class of bug — s.Type deref on a typed-nil *jsonschema.Schema before the schema even reaches setSchema. Same fix shape.

    Local fix branch ready:

    • Four nil-guards: Server.AddTool Input/Output schemas, setSchema post-ForType (defensive), setSchema post-type-assertion (the reproducible path). Each panics/errors with a message naming the tool and field.
    • New test TestAddToolNilSchemaPanicMessages with 3 sub-cases. Asserts the panic message contains the tool name — not just that it panics. Fails on main with the generic runtime panic; passes with the guards.
    • go test ./... green, go vet ./... clean.

    Deliberately scoped out: the defer recover() wrapper in toolForErr. The nil-guards cover the reproducible cases; the recover would only catch deeper panics from inside jsonschema-go that we can't predict at the SDK boundary. Happy to send as a separate PR if useful — figured a smaller defensive-fix PR was easier to review.

    I'll open the PR if you confirm — happy to refine the scope based on your read.

  2. guglielmo-san commented on May 4, 2026

    @guglielmo-san
    Contributor

    @jasondillingham thank you for the contribution! I added a comment on the PR that was already open on this issue

  3. added theissue type on May 5, 2026
  4. added
    P2Moderate issues, valuable feature requests
    on May 5, 2026
  5. added a commit that references this issue on May 6, 2026
    06c5f29
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

    P2Moderate issues, valuable feature requests

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions