feat(mcptoolset): surface elicitation and tool result _meta#1168
feat(mcptoolset): surface elicitation and tool result _meta#1168QuentinBisson wants to merge 2 commits into
Conversation
Add ElicitationHandler and ElicitationCompleteHandler to mcptoolset.Config, applied to the default MCP client so servers can use elicitation/create (including URL-mode elicitation per SEP-1036) during tool calls. The client declares both form and URL elicitation capabilities when a handler is set; combining the handlers with a custom Client is rejected since handlers must be configured on the client itself. Preserve the tool result _meta field in the function response map under the "_meta" key (mirroring raw MCP serialization) instead of dropping it, so server-attached metadata such as auth challenges reaches callbacks and the embedding application.
karolpiotrowicz
left a comment
There was a problem hiding this comment.
Thanks for this, @QuentinBisson! I know this is still a draft, but since it's linked to #1165 I took an early look — and it's already in really good shape, so I wanted to share some feedback now rather than make you wait. Appreciate the work you've put in here, especially the clean split into the two independent pieces (_meta passthrough + elicitation plumbing) and the in-memory-transport tests running real protocol traffic against a live server.
I pulled the branch and it holds up well: the full test suite passes, -race is clean on the package, and it's backward compatible (_meta is only added when present, and the two new Config fields are purely additive).
None of this is urgent given the draft status — just flagging it early so it's easy to fold in. One thing I'd want to resolve before it comes out of draft, plus a couple of doc clarifications and an optional design question:
1. ElicitationCompleteHandler set on its own advertises a capability the client can't service. New enters the capability branch on ElicitationHandler != nil || ElicitationCompleteHandler != nil, so setting only ElicitationCompleteHandler advertises form+URL elicitation while ElicitationHandler stays nil — and the SDK then rejects any incoming elicitation/create with "client does not support elicitation". Since a completion notification can't arrive unless an elicitation was created first, this combination is effectively unusable. I'd suggest failing fast in New (details inline).
2. Doc accuracy on _meta. The functionResponse comment says the metadata reaches "callbacks and the embedding application" — worth adding that it's also included in the function response returned to the model (and therefore persisted in session/traces). That's the intended behavior per #1165, but it's the most consequential consumer, so it's good to spell out.
3. (Optional / your call) Opt-in vs. always-on for _meta. Because the metadata now flows to the model for every server that emits it, users of meta-emitting servers will start seeing it surface without opting in. The reserved-key shape you chose is faithful to #1165 — just flagging the always-on aspect in case you'd prefer a flag or the OnToolResult hook that was also floated.
A few optional test/robustness follow-ups can wait until it's marked ready. Thanks again for driving this — happy to review again whenever you flip it out of draft.
| if cfg.ElicitationHandler != nil || cfg.ElicitationCompleteHandler != nil { | ||
| if cfg.Client != nil { | ||
| return nil, fmt.Errorf("mcptoolset: ElicitationHandler and ElicitationCompleteHandler cannot be combined with a custom Client; set them in the client's mcp.ClientOptions instead") | ||
| } |
There was a problem hiding this comment.
The capability branch is gated on either handler being set, but only ElicitationHandler can actually service an incoming elicitation/create. If a caller sets only ElicitationCompleteHandler, the client advertises form+URL elicitation yet the SDK rejects the request at runtime with "client does not support elicitation" — and a completion notification can't arrive without a prior elicitation anyway, so this config can't work.
Simplest fix is to fail fast in New, e.g. right after the custom-client check:
| } | |
| } | |
| if cfg.ElicitationHandler == nil { | |
| return nil, fmt.Errorf("mcptoolset: ElicitationCompleteHandler requires ElicitationHandler to be set") | |
| } |
(Alternatively, gate just the Capabilities.Elicitation advertisement on cfg.ElicitationHandler != nil — but since the complete-handler-only case is inert, erroring is clearer.)
| // functionResponse builds the function response map for a tool result. | ||
| // The result's _meta field is preserved under the "_meta" key, mirroring the | ||
| // raw MCP serialization, so that metadata attached by the server (e.g. auth | ||
| // challenges from MCP gateways) reaches callbacks and the embedding | ||
| // application instead of being silently dropped. |
There was a problem hiding this comment.
Small doc clarification: this map is the function response returned to the model, so _meta reaches the LLM (and is persisted to session/traces), not only "callbacks and the embedding application." Could you extend the comment to say so? It's the intended behavior per #1165, but the model being a consumer is the important part for anyone reading this later.
…rify _meta doc Fail fast in New when ElicitationCompleteHandler is set without ElicitationHandler: the client cannot service an elicitation/create and a completion notification cannot arrive without a prior elicitation, so the config is inert. Clarify that _meta in the function response reaches the model and is persisted to session and traces.
|
Thanks for the early look, @karolpiotrowicz, and for pulling the branch and running 1. 2. 3. Always-on vs opt-in for Both fixes are pushed. Marking ready for review. |
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
_metais discarded, leaving no path for MCP server auth challenges (e.g. SEP-1036 URL elicitation) #1165Problem:
mcptoolsetflattens everyCallToolResultto{"output": ...}and drops the result's_metafield, and there is no way to handle elicitation requests: servers using URL-mode elicitation (SEP-1036, spec 2025-11-25) for out-of-band flows such as per-user OAuth challenges fail opaquely inside the toolset (client does not support "url" elicitation). go-sdk already ships the full client surface (ClientOptions.ElicitationHandler,ElicitationCompleteHandler,URLElicitationCapabilities), so this is purely mcptoolset plumbing.Solution:
Two independent pieces, as proposed in #1165:
Config.ElicitationHandlerandConfig.ElicitationCompleteHandler, applied to the default MCP client. Setting a handler declares both form and URL elicitation capabilities (the SDK's inferred capability covers form mode only;RootsV2preserves the default roots capability that settingCapabilitieswould otherwise disable). Combining the handlers with a customConfig.Clientreturns an error fromNew, since handlers must be configured in that client's ownmcp.ClientOptions._metapassthrough: when a tool result carriesMeta, it is included in the returned function response map under the"_meta"key, mirroring the raw MCP serialization. Results withoutMetaare unchanged.I went with the reserved-key shape for
_metarather than anOnToolResulthook (the other option floated in #1165) because it is data-only and involves no new callback surface; happy to rework to the hook shape if preferred.Testing Plan
Unit Tests:
New tests in
tool/mcptoolset/set_test.go, all running real MCP protocol traffic over in-memory transports against a realmcp.Server:TestCallToolMetaPassthrough: text and structured results with_metapreserved; no_metakey when the result has none.TestElicitationHandler: server raises a URL-mode elicitation mid tool call; the configured handler receives the URL and the call completes.TestNewRejectsElicitationHandlerWithCustomClient: error when combining handlers with a custom client.Manual End-to-End (E2E) Tests:
The elicitation test drives the full path end-to-end (real
mcp.Server→elicitation/createwithmode: "url"→ client handler → tool result), which is the same wire sequence an MCP gateway produces. We (Giant Swarm) also plan to run this against our MCP gateway (per-user OAuth to backends, challenges mapped to the A2Aauth-requiredtask state) and can report back on the PR.Checklist
Additional context
Python twin (elicitation callback only;
metaalready survives there): google/adk-python#6422 / google/adk-python#6423.