mcp: harden paginate against infinite loops and param mutation - #1110
mcp: harden paginate against infinite loops and param mutation#1110dongjiang1989 wants to merge 1 commit into
Conversation
6f43bc9 to
481cc4a
Compare
| // DefaultListMaxPages is the default value for [ClientOptions.ListMaxPages], | ||
| // matching the TypeScript SDK's DEFAULT_LIST_MAX_PAGES. | ||
| const DefaultListMaxPages = 64 | ||
|
|
There was a problem hiding this comment.
this is a behavioral change for existing users
There was a problem hiding this comment.
Thanks @guglielmo-san
You're right, this is a behavioral change. However, it's a security hardening fix — without a default limit, a misbehaving server can cause unbounded pagination loops (infinite CPU/memory consumption), which is a denial-of-service vector.
The default of 64 aligns with both the TypeScript SDK (DEFAULT_LIST_MAX_PAGES = 64) and the Python SDK. Users who legitimately need more pages can set ListMaxPages to a higher value or -1 for unlimited.
I've added a note to the PR description explaining this behavioral change.
| localParams := params | ||
| if v := reflect.ValueOf(params); v.Kind() == reflect.Pointer { | ||
| cp := reflect.New(v.Type().Elem()) | ||
| cp.Elem().Set(v.Elem()) | ||
| localParams = cp.Interface().(P) | ||
| } |
There was a problem hiding this comment.
it could be simplified to sth like
if params != nil {
cp := *params
localParams := &cp
// use localParams...
}
There was a problem hiding this comment.
Thanks for the suggestion! Unfortunately this doesn't work because P is a type parameter constrained by the listParams interface. Go doesn't allow comparing a type parameter with nil or dereferencing it directly. All concrete uses of P are pointer types, but the generic constraint is an interface, so we need reflect to create the shallow copy.
Add safety guards to the paginate() generic helper used by Tools(), Resources(), ResourceTemplates(), and Prompts() iterators: - Add ListMaxPages option to ClientOptions with a default of DefaultListMaxPages (64), matching the TypeScript SDK's DEFAULT_LIST_MAX_PAGES. A value of 0 uses the default; a negative value means unlimited. - Detect cursor cycles via a seen-set to prevent infinite pagination when a server returns a repeating cursor. - Copy the caller's params struct via reflect so pagination does not mutate the original value. Fixes a latent safety gap: without these guards, a buggy or malicious server could cause unbounded CPU/memory consumption via cursor cycles or infinite NextCursor chains. Signed-off-by: dongjiang <dongjiang1989@126.com>
e768e78 to
d5a21b8
Compare
The
paginate()generic helper used byTools(),Resources(),ResourceTemplates(), andPrompts()iterators has no safety guards against:NextCursoron every page causes an infinite loop and unbounded memory/CPU consumption.paginate()writes the next cursor back into the caller'sparamsstruct, which is surprising behavior and can cause subtle bugs if the caller reuses the params.The TypeScript SDK already protects against these cases in its
_listAllPageshelper with alistMaxPages=64limit and aseencursor set. This change brings the Go SDK to parity.ref: modelcontextprotocol/typescript-sdk#2336
ref: modelcontextprotocol/python-sdk#3156
Changes:
ClientOptions.ListMaxPages(default 64 when zero; negative means unlimited), passed through topaginate()by all four iterator methods.reflect.Newto shallow-copy the caller's params struct so the original is never mutated.Behavioral Change Note
This PR introduces a default page limit of 64, which is a behavioral change for existing users. Previously, pagination was unbounded. This is a security hardening fix — without a limit, a misbehaving or buggy server can cause the client to enter an infinite loop, consuming unbounded CPU and memory (CVE-worthy denial-of-service vector).
The default of 64 aligns with:
DEFAULT_LIST_MAX_PAGES = 64(ref)Users who need more pages can explicitly set
ListMaxPagesto a higher value or-1for unlimited.