Skip to content

Compose alternate-result filters with typed server handlers#1718

Open
onatozmenn wants to merge 1 commit into
modelcontextprotocol:mainfrom
onatozmenn:feat/composable-server-extensions
Open

Compose alternate-result filters with typed server handlers#1718
onatozmenn wants to merge 1 commit into
modelcontextprotocol:mainfrom
onatozmenn:feat/composable-server-extensions

Conversation

@onatozmenn

@onatozmenn onatozmenn commented Jul 20, 2026

Copy link
Copy Markdown

Compose alternate-result behavior around the ordinary typed server pipeline instead of replacing it.

  • Add an experimental, method-keyed McpServerOptions.AddAlternateResultFilter<TParams, TResult> API.
  • Compose ordinary tools/call filters, primitive matching, error handling, and normalization inside alternate-result filters.
  • Keep an explicit CallToolWithAlternateHandler as the low-level replacement path and reject incompatible combinations at startup.
  • Move the Tasks extension to the method-keyed seam and give background execution its own async DI scope.
  • Document ordering, replacement semantics, task timing, and scope lifetime.

Motivation and Context

CallToolFilters and alternate-result filters were previously mutually exclusive. As a result, enabling Tasks could displace validation, telemetry, and ASP.NET authorization filters.

The tool-specific alternate APIs also meant that extending another built-in method would require adding another Core <Method>WithAlternateHandler/filter pair. The new registration is keyed by method and typed by the existing request/result contract, so future extension-owned augmentation does not require a new Core API for each method.

For task-backed calls, the task record is created first. The ordinary filters then run exactly once in background execution with MatchedPrimitive populated and before the tool body. An authorization denial therefore fails the task without invoking the tool. A dedicated async DI scope keeps scoped filters and tool dependencies alive after the initiating HTTP request completes.

This addresses the alternate-result composition and method-agnostic augmentation slice of #1704. Typed custom RPC registration and subscription grant processing remain coordinated follow-up work.

How Has This Been Tested?

  • Full solution build in the .NET 10 SDK container across netstandard2.0, net8.0, net9.0, and net10.0: 0 warnings, 0 errors.
  • Full net10 Core suite with Node/npm installed for external stdio coverage: 2,296 passed, 5 skipped, 0 failed.
  • Net10 ASP.NET suite excluding external conformance/OAuth fixtures: 284 passed, 30 skipped, 0 failed.
  • Full AuthorizeAttributeTests class: 30 passed, 0 failed.
  • Final focused alternate/filter/task regressions: 73 passed on net10; 13 passed on both net8 and net9.
  • Tasks + authorization integration: 2 passed on each of net8, net9, and net10.
  • Scoped dotnet format whitespace --verify-no-changes and git diff --check.

Breaking Changes

No stable API break. The new API is experimental. An explicit CallToolWithAlternateHandler combined with a method-keyed alternate-result filter now fails with an actionable startup error because the explicit handler is a full replacement.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The first method-keyed consumer is Tasks wrapping tools/call, but coverage also augments prompts/get and prompts/list to prove the seam is method-agnostic. Tests cover filter ordering, immediate and alternate serialization, cache/result normalization, unknown methods, type mismatches, explicit replacement collisions, exactly-once ordinary filters, scoped service lifetime/disposal, and ASP.NET authorization.

Signed-off-by: onatozmenn <onatozmen44@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant