What did you do?
Called ClientSession.CallTool against a tool whose handler gates on an elicitation InputRequest (SEP-2322), reusing one *CallToolParams struct for a second call:
params := &mcp.CallToolParams{Name: "delete", Arguments: args}
res1, _ := cs.CallTool(ctx, params) // elicitation fires, user accepts, tool runs
res2, _ := cs.CallTool(ctx, params) // same struct, second call
The server handler uses the gating idiom from the SDK's own tests and examples:
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"confirm": &mcp.ElicitParams{Message: "Sure?"}},
RequestState: state,
}, nil, nil
}
What did you see?
After the first call returns, the caller's params struct has been mutated: params.InputResponses holds the first call's ElicitResult and params.RequestState holds the first call's state. setMultiRoundTripRetryParams (mcp/mrtr.go) writes both fields through the caller's pointer during the middleware's retry loop and never clears them.
The second call therefore goes out with the first call's answer pre-attached. The server sees len(InputResponses) > 0, treats the call as already answered, and the client's ElicitationHandler never fires. For a confirmation gate on a destructive tool, the second action runs with no user confirmation at all.
What did you expect to see?
CallTool / GetPrompt / ReadResource should not mutate caller-owned params. Each fresh call should round-trip its own input requests; retry bookkeeping belongs to the middleware, not the caller's struct.
What version of the Go MCP SDK are you using?
v1.7.0; the behavior is unchanged at head (7256941).
What version of Go are you using?
go1.26.5 darwin/arm64
I have a fix ready (carry the retry responses/state on a shallow copy of the params instead of mutating the original) and will send a PR.
What did you do?
Called
ClientSession.CallToolagainst a tool whose handler gates on an elicitationInputRequest(SEP-2322), reusing one*CallToolParamsstruct for a second call:The server handler uses the gating idiom from the SDK's own tests and examples:
What did you see?
After the first call returns, the caller's params struct has been mutated:
params.InputResponsesholds the first call'sElicitResultandparams.RequestStateholds the first call's state.setMultiRoundTripRetryParams(mcp/mrtr.go) writes both fields through the caller's pointer during the middleware's retry loop and never clears them.The second call therefore goes out with the first call's answer pre-attached. The server sees
len(InputResponses) > 0, treats the call as already answered, and the client'sElicitationHandlernever fires. For a confirmation gate on a destructive tool, the second action runs with no user confirmation at all.What did you expect to see?
CallTool/GetPrompt/ReadResourceshould not mutate caller-owned params. Each fresh call should round-trip its own input requests; retry bookkeeping belongs to the middleware, not the caller's struct.What version of the Go MCP SDK are you using?
v1.7.0; the behavior is unchanged at head (7256941).
What version of Go are you using?
go1.26.5 darwin/arm64
I have a fix ready (carry the retry responses/state on a shallow copy of the params instead of mutating the original) and will send a PR.