mcp: fix ListTools panic and null normalization on malformed tools (#1120)

Fixes #1119.

`ClientSession.ListTools` post-processes the server response through
`filterValidTools`, which has two problems on malformed input:

1. **Nil-pointer panic on `"tools":[null]`.** A `nil` `*Tool` element
reaches `validateParamHeaderAnnotations(tool)`, which dereferences
`tool.InputSchema`, crashing the client on untrusted server input.
2. **`"tools":null` is normalized into a non-nil empty slice.**
`make([]*Tool, 0, len(tools))` is always non-nil, so a nil `Tools` slice
from a malformed response is silently laundered into valid-looking data.

Reproduction:

```go
filterValidTools(nil, []*Tool{nil}) // panics: nil pointer dereference
filterValidTools(nil, nil)          // returns non-nil slice (len 0), want nil
```

Fix (`mcp/streamable_headers.go`):

- Skip (log and exclude) nil tool elements instead of dereferencing
them, so a malformed `"tools":[null]` can no longer crash the client.
This is consistent with the function’s existing contract of excluding
invalid tools.
- Return early with `nil` for a nil input slice so a `"tools":null`
response is preserved as nil rather than normalized into an empty slice.

Added `TestFilterValidToolsNilHandling` covering both defects plus the
mixed valid/nil case. `go test ./mcp/` passes; `gofmt` and `go vet` are
clean.

Note on scope: this keeps the change minimal and within
`filterValidTools`’s existing signature. If maintainers would rather
surface a hard protocol error for a malformed `"tools":null` /
`"tools":[null]` response at the `ListTools` level (rather than
tolerating and filtering it), I’m happy to follow up with that larger
change.
This commit is contained in:
n0liu 2026-07-30 16:06:14 +08:00 • committed by GitHub
commit 7743b71b1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -262,6 +262,12 @@ func filterValidTools(logger *slog.Logger, tools []*Tool) []*Tool {
logger = ensureLogger(logger)
result := make([]*Tool, 0, len(tools))
for _, tool := range tools {
// A nil tool (e.g. from a malformed "tools":[null] response) is
// invalid; exclude it instead of dereferencing it.
if tool == nil {
logger.Error("excluding nil tool from tools/list")
continue
}
if err := validateParamHeaderAnnotations(tool); err != nil {
logger.Error("excluding tool from tools/list", "tool", tool.Name, "error", err)
continue