Skip to content

Commit 06c5f29

Browse files
fix(mcp): prevent SIGSEGV in AddTool when schema is nil (#918)
## Bug `mcp.AddTool` crashes the server with SIGSEGV when input/output struct tags trigger a nil schema. ## Root Cause In `mcp/server.go`, the function `setSchema` calls `internalSchema.Resolve(...)` without checking if `internalSchema` is nil. If the schema inference (e.g., via `jsonschema.ForType`) or a provided schema results in a nil value, this dereference causes a panic. ## Fix Added nil checks for `internalSchema` before calling `Resolve` in two locations within `setSchema`: 1. After schema generation via `jsonschema.ForType`. 2. After processing a provided schema (which might be nil or fail to unmarshal). If `internalSchema` is nil, the function now returns a descriptive error instead of crashing. ## Verification - `go test ./mcp/...` passes. - The fix prevents the crash by handling the nil case gracefully. Closes #916. --------- Signed-off-by: wucm667 <stevenwucongmin@gmail.com> Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
1 parent 2b7c993 commit 06c5f29

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

mcp/server.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
247247
panic(fmt.Errorf("AddTool %q: missing input schema", t.Name))
248248
}
249249
if s, ok := t.InputSchema.(*jsonschema.Schema); ok {
250+
if s == nil {
251+
panic(fmt.Errorf("AddTool %q: input schema is nil", t.Name))
252+
}
250253
if s.Type != "object" {
251254
panic(fmt.Errorf(`AddTool %q: input schema must have type "object"`, t.Name))
252255
}
@@ -261,6 +264,9 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
261264
}
262265
if t.OutputSchema != nil {
263266
if s, ok := t.OutputSchema.(*jsonschema.Schema); ok {
267+
if s == nil {
268+
panic(fmt.Errorf("AddTool %q: output schema is nil", t.Name))
269+
}
264270
if s.Type != "object" {
265271
panic(fmt.Errorf(`AddTool %q: output schema must have type "object"`, t.Name))
266272
}
@@ -437,6 +443,9 @@ func setSchema[T any](sfield *any, rfield **jsonschema.Resolved, cache *SchemaCa
437443
if err != nil {
438444
return zero, err
439445
}
446+
if internalSchema == nil {
447+
return zero, fmt.Errorf("schema is nil for type %v", rt)
448+
}
440449
*sfield = internalSchema
441450

442451
resolved, err := internalSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
@@ -466,6 +475,10 @@ func setSchema[T any](sfield *any, rfield **jsonschema.Resolved, cache *SchemaCa
466475
}
467476
}
468477

478+
if internalSchema == nil {
479+
return zero, fmt.Errorf("schema is nil for type %v", rt)
480+
}
481+
469482
resolved, err := internalSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
470483
if err != nil {
471484
return zero, err

mcp/server_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"context"
1010
"encoding/json"
11+
"fmt"
1112
"log"
1213
"log/slog"
1314
"slices"
@@ -604,6 +605,52 @@ func TestAddToolNameValidation(t *testing.T) {
604605
}
605606
}
606607

608+
func TestAddToolNilSchema(t *testing.T) {
609+
var nilSchema *jsonschema.Schema
610+
611+
panicMsg := func(f func()) (msg string) {
612+
defer func() {
613+
if r := recover(); r != nil {
614+
msg = fmt.Sprintf("%v", r)
615+
}
616+
}()
617+
f()
618+
return msg
619+
}
620+
621+
// Call s.AddTool directly to exercise the typed-nil checks added to that method.
622+
tests := []struct {
623+
name string
624+
tool *Tool
625+
wantContain string
626+
}{
627+
{
628+
name: "typed nil InputSchema",
629+
tool: &Tool{Name: "T", InputSchema: nilSchema},
630+
wantContain: "input schema is nil",
631+
},
632+
{
633+
name: "typed nil OutputSchema",
634+
tool: &Tool{Name: "T", InputSchema: &jsonschema.Schema{Type: "object"}, OutputSchema: nilSchema},
635+
wantContain: "output schema is nil",
636+
},
637+
}
638+
for _, tc := range tests {
639+
t.Run(tc.name, func(t *testing.T) {
640+
s := NewServer(testImpl, nil)
641+
642+
msg := panicMsg(func() {
643+
s.AddTool(tc.tool, nil)
644+
})
645+
if msg == "" {
646+
t.Error("expected panic")
647+
} else if !strings.Contains(msg, tc.wantContain) {
648+
t.Errorf("panic message %q does not contain %q", msg, tc.wantContain)
649+
}
650+
})
651+
}
652+
}
653+
607654
type schema = jsonschema.Schema
608655

609656
func testToolForSchema[In, Out any](t *testing.T, tool *Tool, in string, out Out, wantIn, wantOut any, wantErrContaining string) {

0 commit comments

Comments
 (0)