modelcontextprotocol / modelcontextprotocol/csharp-sdk
Make server extension handlers composable with filters and subscriptions
@onatozmenn is already working on this.
Since Jul 25, 2026.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
Problem
#1693 moves Tasks out of Core. That split is useful only if extension packages can layer behavior onto the normal server pipeline without relying on private implementation details.
The current extension seams have three gaps:
CallToolWithAlternateFiltersis mutually exclusive with normalCallToolFilters. A task wrapper can therefore displace filters used for validation, telemetry, and ASP.NET authorization.- Custom extension RPCs use raw
JsonRpcRequesthandlers. They do not get typed parameters/results or method-specific typed filters through the same pipeline as built-in methods. subscriptions/listengrants are closed over the built-in tools, prompts, and resources fields. Extensions cannot implement their own subscription fields cleanly.
Subscription delivery has an additional transport constraint. The current Core fan-out model tracks active listeners on one long-lived McpServerImpl, which works for stdio and legacy stateful Streamable HTTP. It does not work for stateless Streamable HTTP, where each request has an independent server instance. Stateless delivery is tracked by #1662.
Proposed changes
Compose alternate tools/call behavior with the normal pipeline
- Build the ordinary
tools/callpipeline first, including primitive matching,CallToolFilters, request scoping, logging, and exception handling. - Adapt its normal result to
ResultOrAlternate<CallToolResult>, then apply alternate-result filters around that adapter. - Keep an explicit
CallToolWithAlternateHandleras the low-level full-replacement escape hatch. Reject combinations whose semantics cannot be preserved with an actionable configuration error. - Have the Tasks extension wrap the ordinary pipeline rather than replace it. Ordinary filters must run exactly once before a task-backed primitive executes.
- Document the timing for task-backed calls, including whether an authorization denial is surfaced before or after the task record is created.
Add filterable typed custom RPCs
- Keep raw custom handlers as an escape hatch and add a global raw custom-handler filter pipeline for coarse authorization, logging, and telemetry.
- Add a typed custom-handler registration shape carrying the method name,
JsonTypeInfo<TParams>,JsonTypeInfo<TResult>, a typedRequestContext<TParams>handler, and optional method-specific typed filters. - Preserve per-request DI scopes and source-generated serialization through the typed path.
- Define and document filter ordering. Global raw filters should wrap typed filters, which should wrap the typed handler before result serialization.
- Validate collisions with built-in, raw custom, and typed custom methods. Fail clearly when a configured typed filter uses parameter or result types that do not match its handler.
- Move
tasks/get,tasks/update, andtasks/cancelto the typed path, retaining protocol-version and Tasks capability checks.
Make subscriptions/listen extensible without coupling it to one delivery model
- Allow extension-owned fields in
SubscriptionsListenNotifications. - Add subscription grant processors that can inspect a request, validate capabilities or authorization, and populate the granted acknowledgement.
- Do not invoke extension grant processors when the transport cannot deliver any granted subscriptions. Until #1662 lands, stateless HTTP should acknowledge no grants without task-store lookups, capability errors, or other extension side effects.
- Use the grant hook for Tasks
taskIds. Grant only recognized task IDs and require the Tasks capability. - Keep Core responsible for subscription ID metadata and routing, but do not make the current per-
McpServerImpl_activeSubscriptionsregistry part of the public contract. - Coordinate the public notification delivery abstraction with #1662. It must be able to support a held-open stateless listen request or another transport-neutral backend rather than assuming the publisher owns every active listener.
SendTaskStatusNotificationAsynccan remain the stable Tasks-facing API and delegate to the resulting Core delivery abstraction.- If this work lands before #1662, scope notification fan-out explicitly to listeners owned by the current long-lived server instance, keep the Core seam experimental, and document that stateless Streamable HTTP delivery is not yet supported.
The filtering and typed-handler portions should not be blocked on #1662. The DTO and grant-processing seams can also land independently. Only the final public delivery contract needs coordination.
Tests and documentation
Add coverage for:
- Normal and alternate tool filter composition and ordering.
- Task-backed tools running ordinary
CallToolFiltersexactly once with the matched primitive available. - An ASP.NET integration using
AddAuthorizationFilters()plusWithTasks(). An unauthorized caller must not execute the tool, and an authorized task-backed call must complete. - Global raw and method-specific typed filters around custom extension methods.
- All three task lifecycle methods participating in typed filters.
- Handler collisions, mismatched typed filters, request scopes, and serialization behavior.
- Task subscription grants, capability failures, subscription metadata, and suppression of notifications without a matching grant on long-lived transports.
- Stateless HTTP acknowledging no extension grants and not invoking extension grant processors until #1662 provides delivery.
Mark the new Core extensibility APIs experimental and document their ordering, replacement semantics, and transport limitations.
Why
This preserves existing authorization and policy behavior while allowing Tasks and future protocol extensions to remain separate packages. It also gives extensions a reusable, typed composition model instead of requiring each package to duplicate Core's private dispatch, filtering, serialization, and subscription machinery.
Related work
- #1662 owns server-to-client
subscriptions/listendelivery over stateless Streamable HTTP. This issue must not introduce a competing shared-session or cross-request registry design. - #1635 tracks task-body input request composition with MRTR.
Out of scope
- Implementing stateless Streamable HTTP subscription delivery, tracked by #1662.
- Ambient task context or a public task ID/status helper for tool implementations.
- Broader changes to the
ResultOrAlternaterepresentation.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.