Make the alternate-result seam method-agnostic - #1780
Open
onatozmenn wants to merge 1 commit into
Open
Conversation
The alternate-result augmentation seam is tools/call-shaped today, so enabling a second method to return an alternate result would mean another <Method>WithAlternateHandler/Filters pair plus new Core wiring each time. Add a method-keyed store (AlternateResultFilterCollection) and McpServerOptions.AddAlternateResultFilter<TParams, TResult>(method, filter). CallToolWithAlternateFilters becomes a typed view over the tools/call entry of that store, so filters registered through either API share one ordered list and nothing existing changes shape. Only adaptation and filter composition are generalized. Primitive matching and tool-call lifecycle logging stay per-method, so tools/call keeps its own composition in BuildComposedCallToolHandler. Registering filters for a method the server does not dispatch throws at construction rather than silently dropping them. Signed-off-by: onatozmenn <onatozmen44@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes part of #1704, specifically @jeffhandley's method-agnostic point. @PranavSenthilnathan asked to hold this until after 2.0.0 shipped, and v2.0.0 went out on 2026-07-28, so here it is.
Problem
CallToolWithAlternateFiltersistools/call-shaped. Adding alternate-result support for a second method means a new<Method>WithAlternateHandler/<Method>WithAlternateFilterspair plus new Core wiring every time, which is the thing the original review feedback wanted to avoid.What this does
Adds a method-keyed store,
AlternateResultFilterCollection, plus:CallToolWithAlternateFiltersbecomes a typed view over thetools/callentry of that same store. Filters added through either API land in one ordered list, so the Tasks extension andAuthorizationFilterSetupkeep working untouched. Nothing in the existing shape changes.Built-in typed handlers now go through
SetHandlerWithAlternateSupport, which is a no-op unless filters were registered for that method. When they were, the ordinary handler is adapted toResultOrAlternate<TResult>and the filters compose around it.The design call I had to make
In the issue I flagged that the lifecycle logging and primitive matching inside
BuildComposedCallToolHandlerare tool-specific, so generalizing means deciding how much of that becomes per-method policy.I went with the narrow answer: the generic seam only does adaptation and filter composition. Primitive matching,
ToolCallCompleted/ToolCallErrorlogging, andComposedCallToolInvocationStateall stay in thetools/callpath. A future task-enabled method brings its own policy rather than inheriting the tool one.Happy to go the other way if you'd rather Core owned a shared lifecycle abstraction, but that felt like it would bake tool semantics into methods that don't want them.
One behavior addition worth calling out: registering filters for a method the server doesn't dispatch now throws at construction. Without that, a typo or a missing capability silently drops the filters, which is a rough thing to debug. Empty registrations don't trip it, so just reading
CallToolWithAlternateFiltersis still free.Tests
tests/ModelContextProtocol.Tests/Server/AlternateResultFilterTests.cs, driven throughprompts/getandprompts/listrather than tools, to show it isn't tool-specific:prompts/getreturns a differentResultsubtype and the client sees that shape on the wirenextstill returns the ordinaryGetPromptResultprompts/listrun in registration orderCallToolWithAlternateFiltersandAddAlternateResultFilter(RequestMethods.ToolsCall, ...)resolve to the same list instanceMcpServer.Create, and an empty registration doesn'tI also broke it twice to check the tests actually hold: forcing
SetHandlerWithAlternateSupportto always take the ordinary path, and disabling the registration validation. Both were caught.Local run: 2243 passed, 0 failed on
ModelContextProtocol.Tests(net10.0), skipping the integration classes that need external processes. TheServernamespace alone is 579 passed.Implemented with AI assistance.