Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Python SDK: support `phase="both"` as decorator sugar

Open
#37 0 comments 0 reactions 1 assignee View on GitHub

@olaservo is already working on this.

Since Aug 13, 2026.

Assessment

This issue has not been assessed yet.

Description

enhancement

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 hooks array 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 the phase= parameter on Interceptors.validator(), .mutator(), and .sink().
  • Expand it inside build_hooks() in interceptor.py into two Hook entries, one per phase.
  • Leave the wire type Phase = Literal["request", "response"] unchanged, so Hook, InterceptorResult, and the invoke/execute params keep rejecting both.
  • Add a test asserting that phase="both" produces exactly two hook entries and that Hook(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

Dominant language
C#
Stars
23
Forks
15
PR merge metrics
No merged PRs in 30d

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/experimental-ext-interceptors

All issues in modelcontextprotocol/experimental-ext-interceptors

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.