Add Python SDK for interceptors - #30
Conversation
Implements the Python SDK on mcp v2 (>=2.0.0b1), covering both the 2025-11-25 handshake era and the 2026-07-28 modern era: - types.py: SEP wire models (camelCase), open result union so a future third interceptor type is non-breaking - server.py: Interceptors extension hosting interceptors/list + interceptor/invoke with decorator registration; capability advertised at capabilities.extensions[io.modelcontextprotocol/interceptors] (SEP-2133 format per PR #25) - client.py: list/invoke helpers and capability reader - chain.py: trust-boundary-aware chain orchestrator with InterceptorOverrides (per PR #24), parallel validators, sequential atomic mutators, audit/failOpen semantics, timeouts - 80 tests incl. both protocol eras over in-memory transport; examples; CONFORMANCE.md mapping SEP clauses to code and flagging WG questions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fix license-header grammar ("a Apache-2.0" -> "an Apache-2.0") across
the Python SDK sources, tests, and examples.
- Remove the vestigial `test_interceptors.py` placeholder (assert True);
real coverage lives in the other suites. Suite is now 79 tests.
- CONFORMANCE.md: update the test count and add WG flags for the -32602
overload (routing errors reuse the validation-failed code) and the
SEP's own method-name and error-data-key inconsistencies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a draft Python SDK for MCP Interceptors (SEP-2624) on top of the MCP Python SDK v2 beta, including server-side hosting as an extension, invoker-side helpers, and a chain orchestrator, with accompanying tests, docs, and examples.
Changes:
- Introduces interceptor wire types/constants (camelCase-on-wire Pydantic models) and helper functions (hook matching, priority resolution, tolerant invoke-result parsing).
- Adds server-side
Interceptorsextension plus invoker-sidelist_interceptors/invoke_interceptorhelpers and a multi-serverChainorchestrator. - Replaces placeholder tests with real unit/integration coverage, plus README/docs and runnable examples.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| python/sdk/src/mcp_ext_interceptors/types.py | Defines SEP-2624 wire models, constants, and helper functions (priority/hook matching, invoke parsing). |
| python/sdk/src/mcp_ext_interceptors/interceptor.py | Author-facing interceptor API (Invocation, Validator, Mutator) and hook construction helper. |
| python/sdk/src/mcp_ext_interceptors/server.py | Hosts interceptors as an MCP v2 Extension implementing list/invoke and capability settings. |
| python/sdk/src/mcp_ext_interceptors/client.py | Invoker helpers for list/invoke plus capability reading, tolerant of unknown result types. |
| python/sdk/src/mcp_ext_interceptors/chain.py | Implements multi-server discovery, ordering, and trust-boundary-aware execution model with overrides. |
| python/sdk/src/mcp_ext_interceptors/init.py | Exposes public SDK surface via re-exports and __all__. |
| python/sdk/tests/test_types.py | Unit tests for wire serialization, parsing, hook matching/subset checks, and author API enforcement. |
| python/sdk/tests/test_server_extension.py | Integration tests for hosting on in-memory server, wire shape, errors/timeouts, and capability behavior across modes. |
| python/sdk/tests/test_client.py | Tests client helper ergonomics and capability parsing. |
| python/sdk/tests/test_chain.py | Tests chain ordering, parallel validation, blocking/audit semantics, fail-open, atomicity, timeouts, and overrides. |
| python/sdk/tests/conftest.py | Configures anyio backend for async tests. |
| python/sdk/tests/test_interceptors.py | Removes the placeholder test file. |
| python/sdk/README.md | Adds usage/docs for hosting, invoking, chaining, capability notes, and dev workflow. |
| python/sdk/docs/CONFORMANCE.md | Documents SEP clause-to-implementation mapping and known divergences. |
| python/sdk/examples/validator_server.py | Example validator server + client driving via Chain. |
| python/sdk/examples/mutator_server.py | Example mutator server demonstrating phase-specific priority and chain execution. |
| python/sdk/pyproject.toml | Updates dependencies for MCP v2 beta + adds dev tooling group and uv prerelease config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mcp v2 is out of beta, so the >=2.0.0b1 bound, the uv pre-release workaround, and the beta caveats in the README are no longer needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mutator exception on the fail-open path skipped the in_flight reset, so a chain-level timeout in a later stage named the already-failed mutator in abortedAt. Clear in_flight before continuing, with a regression test. Also drop the hard-coded test count from CONFORMANCE.md (review feedback on #30). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (1)
python/sdk/src/mcp_ext_interceptors/server.py:233
anyio.fail_after()raisesanyio.exceptions.TimeoutError(AnyIO-specific), which is not guaranteed to be caught byexcept TimeoutError:. If it isn’t caught, interceptor timeouts will be reported asINTERCEPTOR_MUTATION_FAILEDinstead ofINTERCEPTOR_TIMEOUT, breaking the intended error code behavior (and thetest_timeoutexpectation).
if params.timeout_ms is not None:
with anyio.fail_after(params.timeout_ms / 1000):
result = await interceptor.handler(invocation)
else:
result = await interceptor.handler(invocation)
except TimeoutError:
raise MCPError(
code=INTERCEPTOR_TIMEOUT,
message="Interceptor execution timeout",
data={"interceptor": params.name, "timeoutMs": params.timeout_ms, "phase": params.phase},
) from None
CI installs latest ruff, which now formats markdown code blocks; align the README example spacing and collapse a call that fits the line width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
python/sdk/src/mcp_ext_interceptors/types.py:53
INTERCEPTOR_MUTATION_FAILEDis used as the general "interceptor execution failed" code (-32603) for both validators and mutators (see server_handle_invoke). The name is misleading for API consumers. Consider introducing a clearer constant (e.g.INTERCEPTOR_EXECUTION_FAILED) and keepingINTERCEPTOR_MUTATION_FAILEDas a backward-compatible alias.
# JSON-RPC error codes assigned by the SEP.
INTERCEPTOR_TIMEOUT: Final = -32000
INTERCEPTOR_VALIDATION_FAILED: Final = mcp_types.INVALID_PARAMS # -32602
INTERCEPTOR_MUTATION_FAILED: Final = mcp_types.INTERNAL_ERROR # -32603
python/sdk/src/mcp_ext_interceptors/server.py:241
anyio.fail_after(...)raisesTimeoutError, but thisexcept TimeoutError:will also catch a user handler that raisesTimeoutErrorfor its own reasons, and incorrectly translate it into anINTERCEPTOR_TIMEOUT. Preferanyio.move_on_after(...)and checkscope.cancelled_caughtso only actual deadline timeouts are mapped toINTERCEPTOR_TIMEOUT, while handler-raisedTimeoutErrorcontinues to surface as an execution failure.
try:
if params.timeout_ms is not None:
with anyio.fail_after(params.timeout_ms / 1000):
result = await interceptor.handler(invocation)
else:
result = await interceptor.handler(invocation)
except TimeoutError:
raise MCPError(
code=INTERCEPTOR_TIMEOUT,
message="Interceptor execution timeout",
data={"interceptor": params.name, "timeoutMs": params.timeout_ms, "phase": params.phase},
) from None
except TimeoutError around anyio.fail_after also caught a TimeoutError raised by the handler itself, misreporting it as an interceptor timeout (with timeoutMs: null when no deadline was set). Switch to move_on_after + cancelled_caught so handler-raised TimeoutError falls through to the execution-failure path; regression test covers both the no-deadline and unexpired-deadline cases. Also note the SEP's mutation-specific naming of the generic -32603 execution-failure code as a WG discussion flag in CONFORMANCE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
python/sdk/pyproject.toml:22
- This package imports
mcp_types(e.g.,src/mcp_ext_interceptors/types.py,src/mcp_ext_interceptors/client.py) butmcp-typesis not listed as a direct runtime dependency. Ifmcpever stops re-exporting/depending on it transitively, installs of this package can fail withImportError: mcp_types.
Add an explicit mcp-types requirement to dependencies.
dependencies = [
"mcp>=2.0.0,<3",
"anyio>=4.9",
"pydantic>=2.12.0",
]
python/sdk/src/mcp_ext_interceptors/chain.py:351
ChainExecutionParams.timeout_msenforces a chain-level deadline, but the per-invocationtimeout_mssent to servers is currently only sourced fromInterceptorOverrides. That means a chain timeout does not get communicated to interceptor servers via the SEPtimeoutMsfield, so remote servers may continue executing after the invoker has timed out.
Consider defaulting each invoke's timeout_ms to params.timeout_ms when no per-entry override is set (and still keeping the chain-level guard).
config=(params.config or {}).get(entry.interceptor.name),
timeout_ms=entry.effective_timeout_ms(),
context=params.context,
The package imported mcp_types without declaring mcp-types as a dependency; mcp 2.0.0 re-exports the same objects as mcp.types, so use that and stay within the declared mcp dependency. Also flag chain-level deadline propagation (remaining-budget timeoutMs per invoke) as a WG discussion item in CONFORMANCE.md rather than diverging unilaterally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| INTERCEPTOR_VALIDATION_FAILED: Final = mcp_types.INVALID_PARAMS # -32602 | ||
| INTERCEPTOR_MUTATION_FAILED: Final = mcp_types.INTERNAL_ERROR # -32603 | ||
|
|
||
| Phase = Literal["request", "response"] |
There was a problem hiding this comment.
i think we need to discuss about "both" phase. it's missing in sep, but go and c# have that phase.
There was a problem hiding this comment.
Well it doesn't appear on the wire, so I am not sure the SEP needs to mention it for it to exist.
Adds the Python SDK for authoring interceptor servers and clients, built on the released MCP Python SDK v2 (
mcp>=2.0.0,<3).🤖 Generated with Claude Code