modelcontextprotocol / modelcontextprotocol/experimental-ext-interceptors
Python SDK: support `phase="both"` as decorator sugar
@olaservo is already working on this.
Since Aug 13, 2026.
- Dominant language
- C#
- Stars
- 23
- Forks
- 14
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 1
Description
Follow-up to #30, from @jeongukjae's review thread on types.py about the both phase.
Context
The SEP is explicit that both is not a wire value (docs/sep.md, Hooks section):
Hooks define which Lifecycle Events trigger an interceptor's invocation. The
hooksarray in the interceptor definition contains one or more entries, each declaring a set of Lifecycle Events (events) and a phase (phase:"request"or"response") where this interceptor will be invoked. Interceptors that run on both phases use two entries — one per phase.
The C# SDK follows this: InterceptorPhase.Both is documented as valid only at the attribute/SDK convenience layer, and the reflection layer expands it into two InterceptorHook entries before anything reaches the wire.
The Python SDK currently has no equivalent convenience — Phase = Literal["request", "response"] is used both as the wire type and as the decorator parameter, so an interceptor that runs on both phases must pass an explicit two-entry hooks=[...].
Proposal
Add both as decorator-level sugar only, matching the C# model:
- Introduce a separate literal for the convenience layer (e.g.
HookPhase = Literal["request", "response", "both"]) and accept it as thephase=parameter onInterceptors.validator(),.mutator(), and.sink(). - Expand it inside
build_hooks()ininterceptor.pyinto twoHookentries, one per phase. - Leave the wire type
Phase = Literal["request", "response"]unchanged, soHook,InterceptorResult, and the invoke/execute params keep rejectingboth. - Add a test asserting that
phase="both"produces exactly two hook entries and thatHook(phase="both")still fails validation.
This is purely additive and has no wire impact.
Non-goal
Accepting both on the wire. Hook matching in chain.py should keep comparing against request/response only — the expansion happens at registration time, not at dispatch time.
Related: cross-SDK divergence
Worth tracking separately, but it's the reason this came up. The Go SDK takes the dispatch-time approach rather than the expansion approach: PhaseBoth is a value of InterceptionPhase, which is the type of the JSON-serialized Hook.Phase field, and matchesHooks in chain/chain.go treats PhaseBoth as matching any target phase. That means a Go interceptor declaring both serializes "phase": "both" in its interceptors/list response, which the SEP disallows and which the Python and C# SDKs would reject when parsing. Filing a separate issue for that.
cc @PederHP @Degiorgio @jeongukjae
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.