diff --git a/docs/seps/2624-interceptors-for-model-context-protocol.mdx b/docs/seps/2624-interceptors-for-model-context-protocol.mdx index d1fccfe23..6ffd01b86 100644 --- a/docs/seps/2624-interceptors-for-model-context-protocol.mdx +++ b/docs/seps/2624-interceptors-for-model-context-protocol.mdx @@ -71,17 +71,17 @@ Unlike framework-specific middleware (e.g., FastAPI middleware, Express.js middl **3. Sidecar and Service-Based Architecture** -While traditional library-based middleware requires code integration, interceptors support: +Traditional library-based middleware runs inside the client or server process and is tied to that SDK. Interceptors are MCP servers invoked over an MCP transport, and the expected deployment is out of process: -- **First-party interceptors**: Deployed as code running locally within the server process -- **Third-party interceptors**: Deployed as separate services that servers call out to +- **Local interceptors**: Deployed as stdio MCP servers on the same host as the invoking client, server, or proxy +- **Remote interceptors**: Deployed as Streamable HTTP MCP servers that clients, servers, or proxies call out to - **Hybrid approaches**: Mix local and remote interceptors based on security, performance, and organizational requirements This flexibility is critical for: - **Security**: Sensitive validation logic can run in isolated, hardened services - **Compliance**: Audit logging can be centralized and immutable -- **Performance**: High-throughput operations can use in-process interceptors while complex policies use dedicated services +- **Performance**: Latency-sensitive interceptors can run as local stdio servers while complex policies use dedicated services **4. Well-Defined Invocation Lifecycle** @@ -152,7 +152,9 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S > > Example of MCP Lifecycle Events include when `tools/call` is invoked, when `resources/read` returns data, or when `sampling/createMessage` is requested. -An **Interceptor** is an MCP primitive that provides governance for context operations through validation or mutation logic. Like tools, prompts, and resources, interceptors are discoverable, and hosted on MCP servers. +An **Interceptor** is an MCP primitive that provides governance for context operations through validation or mutation logic. Like tools, prompts, and resources, interceptors are discoverable, and hosted on MCP servers. Interceptors are always invoked over an MCP transport using `interceptors/list` and `interceptor/invoke`; this SEP defines no in-process interceptor API. The expected deployment is out of process, as a stdio or Streamable HTTP MCP server. An SDK MAY connect to an Interceptor Server over an in-memory transport, for example for testing or embedding, and the wire contract is unchanged. + +An MCP server that hosts interceptors is an **Interceptor Server**. An Interceptor Server SHOULD NOT also expose tools, prompts, or resources. Co-hosting is permitted; the recommendation exists because a server that does both is harder to place in a deployment and to keep out of the request path. An Interceptor Server sits beside the client, server, or proxy that invokes it and is not in the request path between a client and the server whose traffic is being intercepted. Interceptors come in two types: **Validators** (see [Validator](#validator)) and **Mutators** (see [Mutator](#mutator)). @@ -218,7 +220,7 @@ interface Interceptor { /** * Failure routing policy (default: false - fail-closed) * - * Enforce mode: + * Active mode: * - false (fail-closed): If the interceptor crashes or times out, block the message * - true (fail-open): If the interceptor fails, allow the message to proceed * @@ -701,7 +703,7 @@ See [Execution Model](#execution-model) for mutator execution semantics. ##### Invocation Configuration and Context -Interceptors MAY receive configuration and context: +Interceptors MAY receive configuration and context. Configuration is passed on each invocation so that Interceptor Servers can remain stateless. If `config` is omitted, the interceptor's own defaults apply. An interceptor MAY omit `configSchema` from its descriptor, or declare `{ type: "object", additionalProperties: false }` with no properties, to indicate that it exposes no invoker-controlled settings. ```typescript interface InterceptorInvocationParams { @@ -737,7 +739,7 @@ interface InterceptorInvocationParams { sessionId?: string; // FUTURE: Interceptor state propagation - // Allows interceptor to share state through the chain + // Allows interceptor to share state across interceptors // This field is reserved for future enhancement interceptorState?: Record; }; @@ -793,9 +795,9 @@ Interceptor invocation errors follow standard JSON-RPC error format: > **Execution Model Summary:** > -> - **Sending**: Mutate → Validate → Send. A mutation error MUST halt the chain before validation runs. +> - **Sending**: Mutate → Validate → Send. A mutation error MUST halt execution before validation runs. > - **Receiving**: Validate → Mutate → Process. A validation error MUST prevent mutations from running. -> - **Mutations**: Sequential by `priorityHint` (alphabetical tie-break). MUST be atomic — the entire chain succeeds or none apply. +> - **Mutations**: Sequential by `priorityHint` (alphabetical tie-break). MUST be atomic — all mutations succeed or none apply. > - **Validations**: Parallel. Only `severity: "error"` blocks; `"warn"` and `"info"` MUST NOT block. > - **Audit Mode**: Interceptors in audit mode MUST NOT block execution regardless of results. > - **failOpen: true**: If an interceptor fails (crash/timeout), the message MUST be allowed to proceed. @@ -829,28 +831,29 @@ Implementations MUST follow this trust-boundary-aware ordering when executing in **Interceptor Type Behaviors** -| Type | Execution | Failure Behavior | Ordering | Audit Mode | -| -------------- | ---------- | ------------------------------------- | ------------------------------------------------- | ----------------------------------------- | -| **Mutation** | Sequential | Chain halts, returns last valid state | By `priorityHint` (low→high), then alphabetically | Shadow mutations: compute but don't apply | -| **Validation** | Parallel | `severity: "error"` rejects request | None (parallel) | Log violations without blocking | +| Type | Execution | Failure Behavior | Ordering | Audit Mode | +| -------------- | ---------- | ----------------------------------------- | ------------------------------------------------- | ----------------------------------------- | +| **Mutation** | Sequential | Execution halts, returns last valid state | By `priorityHint` (low→high), then alphabetically | Shadow mutations: compute but don't apply | +| **Validation** | Parallel | `severity: "error"` rejects request | None (parallel) | Log violations without blocking | Key semantics: -- Mutations MUST be atomic: the entire chain succeeds or none of the mutations apply +- Mutations MUST be atomic: all mutations succeed or none of them apply - Validations with `severity: "warn"` or `"info"` MUST NOT block execution - Only validations with `severity: "error"` MUST block execution - Interceptors in audit mode MUST NOT block execution regardless of results #### Priority Resolution -When ordering mutations, the invoker resolves phase-specific priorities. If the chain entry has `overrides.priorityHint`, it takes precedence over the interceptor's declared default: +When ordering mutations, the invoker resolves phase-specific priorities. If the invoker's overrides for an interceptor include `priorityHint`, it takes precedence over the interceptor's declared default: ```typescript function resolvePriority( - entry: ChainEntry, + interceptor: Interceptor, + overrides: InterceptorOverrides | undefined, phase: "request" | "response", ): number { - const hint = entry.overrides?.priorityHint ?? entry.interceptor.priorityHint; + const hint = overrides?.priorityHint ?? interceptor.priorityHint; if (hint === undefined) return 0; if (typeof hint === "number") return hint; return hint[phase] ?? 0; @@ -912,7 +915,7 @@ sequenceDiagram Note over App,Server: CLIENT REQUEST (Sending) App->>Client: tools/call request - Client->>Client: Discover interceptor via interceptor/list
Order mutations by priority + Client->>Client: Discover interceptor via interceptors/list
Order mutations by priority Note over Client,MW2: STEP 1: Mutations (Sequential) Client->>MW1: interceptor/invoke
(pii-redactor, priority=10) @@ -956,7 +959,7 @@ sequenceDiagram Note over Client,Tool: SERVER REQUEST (Receiving) Client->>Server: tools/call request Note over Client,Server: ═══ TRUST BOUNDARY ═══ - Server->>Server: Discover interceptor via interceptor/list
Order mutations by priority + Server->>Server: Discover interceptor via interceptors/list
Order mutations by priority Note over Server,MW2: STEP 1: Validation (Parallel)
Trust Boundary Check par Validation after receiving @@ -1061,20 +1064,20 @@ sequenceDiagram Note over App,Tool: Validation guards both sides of trust boundary ``` -#### Chain Execution +#### Executing Multiple Interceptors -Chain execution is a **convenience utility**, provided by SDKs to enforce the execution model defined above. Its purpose is to execute interceptors — potentially hosted across N MCP servers — in a way that enforces the trust-boundary-aware ordering, type-specific execution semantics, and failure behaviors. +An invoker MAY apply zero or more interceptors, potentially hosted across N MCP servers, to a Lifecycle Event. How it applies them is local to the invoker: the set of interceptors, their order, and their overrides are invoker-owned state, not an MCP primitive. This SEP defines no chain resource, chain identifier, chain discovery method, or aggregate invocation method. Each interceptor MUST be invoked independently with `interceptor/invoke` on the server that advertised it via `interceptors/list`. -> Important: The execution model defined above MUST be followed by all implementations, whether invoking interceptors individually via `interceptor/invoke` or using this convenience utility. +> Important: The execution model defined above MUST be followed by all implementations. SDKs MAY provide orchestration helpers over these steps; such helpers are implementation conveniences, not wire protocol. ##### Capability vs Policy -The chain distinguishes between two layers of interceptor configuration: +The execution model distinguishes between two layers of interceptor configuration: - **Capability** (server-declared): The interceptor's type, hooks, and `configSchema` as advertised via `interceptors/list`. These describe what the interceptor _can_ do. -- **Policy** (invoker-declared): Execution parameters such as `failOpen`, `priorityHint`, `mode`, `timeoutMs`, and hook narrowing that the invoker provides via `overrides` on each chain entry. These describe how the interceptor _should_ behave in a specific deployment. +- **Policy** (invoker-declared): Execution parameters such as `failOpen`, `priorityHint`, `mode`, `timeoutMs`, and hook narrowing that the invoker provides via `overrides` for each interceptor. These describe how the interceptor _should_ behave in a specific deployment. -Interceptor servers declare **defaults** for policy fields (`failOpen`, `priorityHint`, `mode`) alongside their capabilities. Invokers MAY provide **overrides** per-interceptor in the chain to adjust execution policy. When an override is present, it takes precedence over the server-declared default. Hook overrides can only **narrow** the set of events (restrict to a subset of declared hooks), never **widen** (add events the interceptor does not declare). Implementations MUST reject override hooks that reference events not present in the interceptor's declared hooks. +Interceptor servers declare **defaults** for policy fields (`failOpen`, `priorityHint`, `mode`) alongside their capabilities. Invokers MAY provide **overrides** per interceptor to adjust execution policy. When an override is present, it takes precedence over the server-declared default. Hook overrides can only **narrow** the set of events (restrict to a subset of declared hooks), never **widen** (add events the interceptor does not declare). Implementations MUST reject override hooks that reference events not present in the interceptor's declared hooks. This separation enables: @@ -1087,64 +1090,19 @@ This separation enables: The orchestration pattern is as follows: 1. **Discover:** Call `interceptors/list` on one or more MCP servers to collect all registered interceptors. -2. **Resolve Policy:** For each chain entry, merge `overrides` with interceptor defaults (overrides take precedence). Filter entries whose override `hooks` exclude the current event/phase. +2. **Select and Resolve Policy:** From the discovered interceptors, take the set the invoker has chosen to apply. For each selected interceptor, merge `overrides` with interceptor defaults (overrides take precedence). Exclude any interceptor whose declared `hooks`, or narrowed override `hooks` if present, do not match the current event/phase. 3. **Sort:** Order mutations by resolved `priorityHint` (ascending, with alphabetical tie-breaking by interceptor name). 4. **Order by Trust Boundary:** Apply the trust-boundary-aware execution model — mutations before validations when sending, validations before mutations when receiving. -5. **Execute:** Call `interceptor/invoke` on the appropriate MCP server for each interceptor, applying resolved `failOpen`, `mode`, and `timeoutMs`. Mutations MUST be invoked sequentially (each receiving the output of the previous). Validations MAY be invoked in parallel. +5. **Execute:** Call `interceptor/invoke` on the appropriate MCP server for each interceptor, applying resolved `failOpen`, `mode`, and `timeoutMs`. Mutations MUST be invoked sequentially (each receiving the output of the previous). Validations MAY be invoked in parallel. The invoker MAY also impose an aggregate timeout covering all interceptors applied to one operation; if it is exceeded, the invoker MUST cancel the remaining invocations and treat the operation as timed out. 6. **Aggregate:** Collect all results, assembling the final payload and validation summary. SDK libraries are expected to provide reference implementations of this orchestration logic so that individual applications do not need to implement it from scratch. -##### Interceptor Chain +##### Invoker Overrides -The chain is constructed from interceptor entries, each describing an interceptor and the server that hosts it: +Overrides are invoker-local configuration associated with a discovered interceptor. The overrides object itself is not sent to the interceptor server; the invoker applies the resolved values, and the resolved `timeoutMs` is passed as `InterceptorInvocationParams.timeoutMs` on each invocation so that the server-side cancellation behavior in [Invocation Configuration and Context](#invocation-configuration-and-context) applies. ```typescript -interface InterceptorChain { - /** - * All interceptor entries in the chain, collected from one or more servers. - * Entries are merged and ordered by the chain - * according to the execution model. - */ - entries: ChainEntry[]; - - /** - * Execute the chain for a given event and phase. - */ - execute(params: ChainExecutionParams): ChainExecutionResult; -} - -interface ChainEntry { - /** - * The interceptor descriptor (as returned by interceptors/list). - * Contains the interceptor's capabilities and author-declared defaults. - */ - interceptor: { - name: string; - type: "mutation" | "validation"; - hooks: Array<{ - events: InterceptionEvent[]; - phase: "request" | "response"; - }>; - priorityHint?: number | { request?: number; response?: number }; - mode?: "active" | "audit"; - failOpen?: boolean; - }; - - /** - * The MCP server that hosts this interceptor. - * Used to route interceptor/invoke calls to the correct server. - * The concrete type is implementation-specific. - */ - server: MCPServerConnection; - - /** - * Optional invoker-side overrides for this interceptor's execution policy. - * Fields present here take precedence over the interceptor's declared defaults. - */ - overrides?: InterceptorOverrides; -} - interface InterceptorOverrides { /** * Override failure routing policy for this interceptor. @@ -1188,123 +1146,12 @@ interface InterceptorOverrides { } ``` -###### ChainExecutionParams - -```typescript -interface ChainExecutionParams { - /** - * Lifecycle event (hook) and phase for this chain - */ - event: InterceptionEvent; - phase: "request" | "response"; - - /** - * The payload to pass through the interceptor chain - */ - payload: unknown; - - /** - * Optional: restrict chain to specific interceptors by name - */ - interceptors?: string[]; - - /** - * Optional: per-interceptor configuration overrides - */ - config?: Record>; - - /** - * Optional: timeout for the entire chain in milliseconds - * implementations MUST cancel the entire chain if the aggregate timeout is exceeded. - */ - timeoutMs?: number; - - /** - * Optional: context passed to all interceptors - */ - context?: { - principal?: { - type: "user" | "service" | "anonymous"; - id?: string; - }; - traceId?: string; - timestamp: string; - }; -} -``` - -###### ChainExecutionResult - -```typescript -interface ChainExecutionResult { - /** - * Overall chain execution status - */ - status: "success" | "validation_failed" | "mutation_failed" | "timeout"; - - /** - * Lifecycle event (hook) and phase for this chain - */ - event: InterceptionEvent; - phase: "request" | "response"; - - /** - * Results from all executed interceptors - */ - results: Array<{ - interceptor: string; - type: "mutation" | "validation"; - phase: "request" | "response"; - // Mutation-specific fields - modified?: boolean; - payload?: unknown; - // Validation-specific fields - valid?: boolean; - severity?: "error" | "warn" | "info"; - messages?: Array<{ message: string; severity: "error" | "warn" | "info" }>; - // Audit mode - mode?: "audit"; - info?: Record; - // Timing - durationMs: number; - }>; - - /** - * Final payload after all mutations (if chain completed) - */ - finalPayload?: unknown; - - /** - * Validation summary - */ - validationSummary: { - errors: number; - warnings: number; - infos: number; - }; - - /** - * Total execution time - */ - totalDurationMs: number; - - /** - * If chain was aborted, details about where and why - */ - abortedAt?: { - interceptor: string; - reason: string; - type: "validation" | "mutation" | "timeout"; - }; -} -``` - -##### Example Interceptor Chain +##### Example: Interceptors for a `tools/call` Event For a `tools/call` event, assume the following interceptor are available: ```typescript -// From interceptor/list response: +// From interceptors/list response: [ { name: "pii-redactor", @@ -1362,12 +1209,12 @@ For a `tools/call` event, assume the following interceptor are available: ]; ``` -##### Example: Chain Entries with Overrides +##### Example: Invoker Overrides -The following example shows how invoker-side overrides adjust execution policy without modifying interceptor servers: +The following example shows how invoker-side overrides adjust execution policy without modifying interceptor servers. Each entry pairs a discovered interceptor descriptor, the server that advertised it, and the invoker's overrides: ```typescript -// Chain entries with invoker overrides applied: +// Discovered interceptors with invoker overrides applied: [ { interceptor: { @@ -1464,10 +1311,10 @@ CLIENT RESPONSE (Receiving): ##### Error Handling -Interceptor chains MUST handle errors based on type: +Invokers MUST handle errors based on interceptor type: ```typescript -// Mutation failure: Chain MUST halt immediately +// Mutation failure (active mode, failOpen: false): Execution MUST halt immediately { error: { code: -32603, @@ -1503,13 +1350,13 @@ During initialization, servers declare interceptor support: result: { protocolVersion: "2025-06-18", capabilities: { + // ...existing capabilities extensions: { "io.modelcontextprotocol/interceptors": { // Events this server's interceptors can handle supportedEvents: InterceptionEvent[]; } } - // ...existing capabilities }, serverInfo: { name: "example-server", @@ -1525,10 +1372,10 @@ During initialization, servers declare interceptor support: **Status**: Reserved for future specification -Inspired by gRPC interceptors and OpenTelemetry context propagation, future versions may support enrichable interceptor state that flows through the entire chain. This would enable: +Inspired by gRPC interceptors and OpenTelemetry context propagation, future versions may support enrichable interceptor state that flows across all interceptors applied to an operation. This would enable: - **State Sharing**: Early interceptor can enrich context for downstream interceptor -- **Cross-Cutting Data**: Authentication, rate limiting, and audit data flows through chain +- **Cross-Cutting Data**: Authentication, rate limiting, and audit data flows across interceptors - **Trace Correlation**: Distributed tracing baggage propagates automatically - **Deadline Propagation**: Timeout context flows through nested operations @@ -1536,7 +1383,7 @@ Inspired by gRPC interceptors and OpenTelemetry context propagation, future vers ```typescript // FUTURE: Enhanced context with state propagation -interface InterceptorChainContext { +interface InterceptorExecutionContext { // Immutable request context (set by initiator, read-only) readonly request: { principal?: { @@ -1550,7 +1397,7 @@ interface InterceptorChainContext { sessionId?: string; }; - // Mutable interceptor state (enriched by interceptor during chain execution) + // Mutable interceptor state (enriched by interceptors during execution) // Each interceptor can read and write to this shared state interceptorState?: Record; @@ -1706,7 +1553,7 @@ Alternative (global fixed ordering) rejected as too rigid for phase-aware operat **4. Separation of Capability and Policy** -The chain separates interceptor **capability** (what the server advertises) from execution **policy** (what the invoker configures via `overrides`): +The execution model separates interceptor **capability** (what the server advertises) from execution **policy** (what the invoker configures via `overrides`): - **Deployment flexibility**: The same interceptor can run in audit mode in staging and active mode in production without server changes - **SLO adaptation**: Platform teams adjust `failOpen` and `timeoutMs` per-deployment based on reliability requirements @@ -1720,7 +1567,8 @@ Alternative (policy only at server side) rejected because enterprise deployments - **Hook-Based Targeting**: Interceptors declare specific hooks (vs. all traffic) for performance and security - **Severity Levels** (info/warn/error): Graduated response vs. binary pass/fail enables audit logging without blocking - **Replace vs. Patch**: Mutations replace entire payloads (vs. JSON Patch) for simplicity and atomicity -- **Method Names**: `interceptor/list` and `interceptor/invoke` mirror MCP patterns (`tools/list`, etc.) +- **Method Names**: `interceptors/list` follows the plural namespace of `tools/list` and `resources/read`. `interceptor/invoke` is singular; aligning it to `interceptors/invoke` is under WG consideration +- **No Chain Primitive**: Multi-interceptor execution is invoker-local. Earlier drafts defined a chain first as a protocol method and then as an SDK utility; both were removed because the wire protocol needs only `interceptors/list` and `interceptor/invoke`, and an aggregate chain object invited implementations to diverge - **"info" Field**: Used instead of "metadata" to avoid confusion with MCP's `_meta` protocol metadata - **Cross-Boundary Support**: Client and server interceptors enable defense-in-depth and zero-trust architecture @@ -1744,7 +1592,7 @@ This SEP is fully backward compatible: 1. **Optional Capability**: Interceptors are an OPTIONAL server capability. Servers without interceptor support MUST continue to work unchanged. -2. **No Protocol Changes**: Existing JSON-RPC methods MUST NOT be modified. Interceptors add new methods (`interceptors/list`, `interceptors/invoke`) without changing existing ones. +2. **No Protocol Changes**: Existing JSON-RPC methods MUST NOT be modified. Interceptors add new methods (`interceptors/list`, `interceptor/invoke`) without changing existing ones. 3. **Client Compatibility**: Clients that do not support interceptors MUST be able to communicate with interceptor-enabled servers. Interceptors MAY run transparently on the server side. @@ -1767,7 +1615,7 @@ A compromised interceptor could: **Mitigations:** -- Interceptor runs in the same trust domain as the server/client +- Deploy interceptors out of process (stdio or Streamable HTTP), so they do not share the invoker's memory and see only what the invoker sends in each invocation (`payload`, `config`, `context`) and transport-level metadata. An Interceptor Server embedded over an in-memory transport shares the invoker's process and gets no such isolation. A local stdio interceptor still inherits the environment and filesystem access of the process that launches it, so it SHOULD be isolated accordingly - Organizations must vet interceptor before deployment - Interceptor should be sandboxed where possible - Audit logging of all interceptor invocations @@ -1778,13 +1626,13 @@ A compromised interceptor could: Attackers might try to bypass interceptor validation: - Send requests directly to underlying services -- Exploit interceptor chain ordering +- Exploit interceptor ordering **Mitigations:** - Interceptor enforcement must be at the transport layer - Server-side interceptor is mandatory for requests entering the trust boundary -- Interceptor chain order must be explicitly configured and immutable +- Interceptor order must be explicitly configured and immutable **3. Information Disclosure** @@ -1835,7 +1683,7 @@ Interceptor mutation could be used to inject unauthorized operations: 2. **Defense in Depth**: Interceptors SHOULD be deployed at both client and server boundaries -3. **Audit Everything**: Implementations SHOULD log all interceptor invocations, including payloads (with PII redaction) +3. **Audit Everything**: Implementations SHOULD log all interceptor invocations, including payloads (with PII redaction). For each invocation, the log SHOULD include the resolved `mode` and `failOpen`, and whether each value came from an invoker override or the interceptor's declared default 4. **Fail Secure**: Interceptors MUST default to `failOpen: false` (fail-closed). Implementations MUST reject requests when interceptors fail unless explicitly configured otherwise. @@ -1877,7 +1725,7 @@ A universal interceptor runtime that enables teams to deploy and manage intercep ```yaml # interceptor-config.yaml interceptors: - # Local interceptor: runs in-process + # Local interceptor: stdio MCP server on the same host - name: pii-redactor type: mutation transport: local @@ -1886,7 +1734,7 @@ interceptors: config: patterns: ["email", "ssn", "phone"] - # Remote interceptor: calls external service + # Remote interceptor: Streamable HTTP MCP server - name: security-scanner type: validation transport: remote @@ -1895,7 +1743,7 @@ interceptors: headers: Authorization: "Bearer ${SECURITY_TOKEN}" - # Local TypeScript interceptor + # Local TypeScript interceptor: stdio MCP server - name: rate-limiter type: validation transport: local diff --git a/seps/2624-interceptors-for-model-context-protocol.md b/seps/2624-interceptors-for-model-context-protocol.md index 8e3345b0e..165520c18 100644 --- a/seps/2624-interceptors-for-model-context-protocol.md +++ b/seps/2624-interceptors-for-model-context-protocol.md @@ -52,17 +52,17 @@ Unlike framework-specific middleware (e.g., FastAPI middleware, Express.js middl **3. Sidecar and Service-Based Architecture** -While traditional library-based middleware requires code integration, interceptors support: +Traditional library-based middleware runs inside the client or server process and is tied to that SDK. Interceptors are MCP servers invoked over an MCP transport, and the expected deployment is out of process: -- **First-party interceptors**: Deployed as code running locally within the server process -- **Third-party interceptors**: Deployed as separate services that servers call out to +- **Local interceptors**: Deployed as stdio MCP servers on the same host as the invoking client, server, or proxy +- **Remote interceptors**: Deployed as Streamable HTTP MCP servers that clients, servers, or proxies call out to - **Hybrid approaches**: Mix local and remote interceptors based on security, performance, and organizational requirements This flexibility is critical for: - **Security**: Sensitive validation logic can run in isolated, hardened services - **Compliance**: Audit logging can be centralized and immutable -- **Performance**: High-throughput operations can use in-process interceptors while complex policies use dedicated services +- **Performance**: Latency-sensitive interceptors can run as local stdio servers while complex policies use dedicated services **4. Well-Defined Invocation Lifecycle** @@ -133,7 +133,9 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S > > Example of MCP Lifecycle Events include when `tools/call` is invoked, when `resources/read` returns data, or when `sampling/createMessage` is requested. -An **Interceptor** is an MCP primitive that provides governance for context operations through validation or mutation logic. Like tools, prompts, and resources, interceptors are discoverable, and hosted on MCP servers. +An **Interceptor** is an MCP primitive that provides governance for context operations through validation or mutation logic. Like tools, prompts, and resources, interceptors are discoverable, and hosted on MCP servers. Interceptors are always invoked over an MCP transport using `interceptors/list` and `interceptor/invoke`; this SEP defines no in-process interceptor API. The expected deployment is out of process, as a stdio or Streamable HTTP MCP server. An SDK MAY connect to an Interceptor Server over an in-memory transport, for example for testing or embedding, and the wire contract is unchanged. + +An MCP server that hosts interceptors is an **Interceptor Server**. An Interceptor Server SHOULD NOT also expose tools, prompts, or resources. Co-hosting is permitted; the recommendation exists because a server that does both is harder to place in a deployment and to keep out of the request path. An Interceptor Server sits beside the client, server, or proxy that invokes it and is not in the request path between a client and the server whose traffic is being intercepted. Interceptors come in two types: **Validators** (see [Validator](#validator)) and **Mutators** (see [Mutator](#mutator)). @@ -199,7 +201,7 @@ interface Interceptor { /** * Failure routing policy (default: false - fail-closed) * - * Enforce mode: + * Active mode: * - false (fail-closed): If the interceptor crashes or times out, block the message * - true (fail-open): If the interceptor fails, allow the message to proceed * @@ -682,7 +684,7 @@ See [Execution Model](#execution-model) for mutator execution semantics. ##### Invocation Configuration and Context -Interceptors MAY receive configuration and context: +Interceptors MAY receive configuration and context. Configuration is passed on each invocation so that Interceptor Servers can remain stateless. If `config` is omitted, the interceptor's own defaults apply. An interceptor MAY omit `configSchema` from its descriptor, or declare `{ type: "object", additionalProperties: false }` with no properties, to indicate that it exposes no invoker-controlled settings. ```typescript interface InterceptorInvocationParams { @@ -718,7 +720,7 @@ interface InterceptorInvocationParams { sessionId?: string; // FUTURE: Interceptor state propagation - // Allows interceptor to share state through the chain + // Allows interceptor to share state across interceptors // This field is reserved for future enhancement interceptorState?: Record; }; @@ -774,9 +776,9 @@ Interceptor invocation errors follow standard JSON-RPC error format: > **Execution Model Summary:** > -> - **Sending**: Mutate → Validate → Send. A mutation error MUST halt the chain before validation runs. +> - **Sending**: Mutate → Validate → Send. A mutation error MUST halt execution before validation runs. > - **Receiving**: Validate → Mutate → Process. A validation error MUST prevent mutations from running. -> - **Mutations**: Sequential by `priorityHint` (alphabetical tie-break). MUST be atomic — the entire chain succeeds or none apply. +> - **Mutations**: Sequential by `priorityHint` (alphabetical tie-break). MUST be atomic — all mutations succeed or none apply. > - **Validations**: Parallel. Only `severity: "error"` blocks; `"warn"` and `"info"` MUST NOT block. > - **Audit Mode**: Interceptors in audit mode MUST NOT block execution regardless of results. > - **failOpen: true**: If an interceptor fails (crash/timeout), the message MUST be allowed to proceed. @@ -810,28 +812,29 @@ Implementations MUST follow this trust-boundary-aware ordering when executing in **Interceptor Type Behaviors** -| Type | Execution | Failure Behavior | Ordering | Audit Mode | -| -------------- | ---------- | ------------------------------------- | ------------------------------------------------- | ----------------------------------------- | -| **Mutation** | Sequential | Chain halts, returns last valid state | By `priorityHint` (low→high), then alphabetically | Shadow mutations: compute but don't apply | -| **Validation** | Parallel | `severity: "error"` rejects request | None (parallel) | Log violations without blocking | +| Type | Execution | Failure Behavior | Ordering | Audit Mode | +| -------------- | ---------- | ----------------------------------------- | ------------------------------------------------- | ----------------------------------------- | +| **Mutation** | Sequential | Execution halts, returns last valid state | By `priorityHint` (low→high), then alphabetically | Shadow mutations: compute but don't apply | +| **Validation** | Parallel | `severity: "error"` rejects request | None (parallel) | Log violations without blocking | Key semantics: -- Mutations MUST be atomic: the entire chain succeeds or none of the mutations apply +- Mutations MUST be atomic: all mutations succeed or none of them apply - Validations with `severity: "warn"` or `"info"` MUST NOT block execution - Only validations with `severity: "error"` MUST block execution - Interceptors in audit mode MUST NOT block execution regardless of results #### Priority Resolution -When ordering mutations, the invoker resolves phase-specific priorities. If the chain entry has `overrides.priorityHint`, it takes precedence over the interceptor's declared default: +When ordering mutations, the invoker resolves phase-specific priorities. If the invoker's overrides for an interceptor include `priorityHint`, it takes precedence over the interceptor's declared default: ```typescript function resolvePriority( - entry: ChainEntry, + interceptor: Interceptor, + overrides: InterceptorOverrides | undefined, phase: "request" | "response", ): number { - const hint = entry.overrides?.priorityHint ?? entry.interceptor.priorityHint; + const hint = overrides?.priorityHint ?? interceptor.priorityHint; if (hint === undefined) return 0; if (typeof hint === "number") return hint; return hint[phase] ?? 0; @@ -893,7 +896,7 @@ sequenceDiagram Note over App,Server: CLIENT REQUEST (Sending) App->>Client: tools/call request - Client->>Client: Discover interceptor via interceptor/list
Order mutations by priority + Client->>Client: Discover interceptor via interceptors/list
Order mutations by priority Note over Client,MW2: STEP 1: Mutations (Sequential) Client->>MW1: interceptor/invoke
(pii-redactor, priority=10) @@ -937,7 +940,7 @@ sequenceDiagram Note over Client,Tool: SERVER REQUEST (Receiving) Client->>Server: tools/call request Note over Client,Server: ═══ TRUST BOUNDARY ═══ - Server->>Server: Discover interceptor via interceptor/list
Order mutations by priority + Server->>Server: Discover interceptor via interceptors/list
Order mutations by priority Note over Server,MW2: STEP 1: Validation (Parallel)
Trust Boundary Check par Validation after receiving @@ -1042,20 +1045,20 @@ sequenceDiagram Note over App,Tool: Validation guards both sides of trust boundary ``` -#### Chain Execution +#### Executing Multiple Interceptors -Chain execution is a **convenience utility**, provided by SDKs to enforce the execution model defined above. Its purpose is to execute interceptors — potentially hosted across N MCP servers — in a way that enforces the trust-boundary-aware ordering, type-specific execution semantics, and failure behaviors. +An invoker MAY apply zero or more interceptors, potentially hosted across N MCP servers, to a Lifecycle Event. How it applies them is local to the invoker: the set of interceptors, their order, and their overrides are invoker-owned state, not an MCP primitive. This SEP defines no chain resource, chain identifier, chain discovery method, or aggregate invocation method. Each interceptor MUST be invoked independently with `interceptor/invoke` on the server that advertised it via `interceptors/list`. -> Important: The execution model defined above MUST be followed by all implementations, whether invoking interceptors individually via `interceptor/invoke` or using this convenience utility. +> Important: The execution model defined above MUST be followed by all implementations. SDKs MAY provide orchestration helpers over these steps; such helpers are implementation conveniences, not wire protocol. ##### Capability vs Policy -The chain distinguishes between two layers of interceptor configuration: +The execution model distinguishes between two layers of interceptor configuration: - **Capability** (server-declared): The interceptor's type, hooks, and `configSchema` as advertised via `interceptors/list`. These describe what the interceptor _can_ do. -- **Policy** (invoker-declared): Execution parameters such as `failOpen`, `priorityHint`, `mode`, `timeoutMs`, and hook narrowing that the invoker provides via `overrides` on each chain entry. These describe how the interceptor _should_ behave in a specific deployment. +- **Policy** (invoker-declared): Execution parameters such as `failOpen`, `priorityHint`, `mode`, `timeoutMs`, and hook narrowing that the invoker provides via `overrides` for each interceptor. These describe how the interceptor _should_ behave in a specific deployment. -Interceptor servers declare **defaults** for policy fields (`failOpen`, `priorityHint`, `mode`) alongside their capabilities. Invokers MAY provide **overrides** per-interceptor in the chain to adjust execution policy. When an override is present, it takes precedence over the server-declared default. Hook overrides can only **narrow** the set of events (restrict to a subset of declared hooks), never **widen** (add events the interceptor does not declare). Implementations MUST reject override hooks that reference events not present in the interceptor's declared hooks. +Interceptor servers declare **defaults** for policy fields (`failOpen`, `priorityHint`, `mode`) alongside their capabilities. Invokers MAY provide **overrides** per interceptor to adjust execution policy. When an override is present, it takes precedence over the server-declared default. Hook overrides can only **narrow** the set of events (restrict to a subset of declared hooks), never **widen** (add events the interceptor does not declare). Implementations MUST reject override hooks that reference events not present in the interceptor's declared hooks. This separation enables: @@ -1068,64 +1071,19 @@ This separation enables: The orchestration pattern is as follows: 1. **Discover:** Call `interceptors/list` on one or more MCP servers to collect all registered interceptors. -2. **Resolve Policy:** For each chain entry, merge `overrides` with interceptor defaults (overrides take precedence). Filter entries whose override `hooks` exclude the current event/phase. +2. **Select and Resolve Policy:** From the discovered interceptors, take the set the invoker has chosen to apply. For each selected interceptor, merge `overrides` with interceptor defaults (overrides take precedence). Exclude any interceptor whose declared `hooks`, or narrowed override `hooks` if present, do not match the current event/phase. 3. **Sort:** Order mutations by resolved `priorityHint` (ascending, with alphabetical tie-breaking by interceptor name). 4. **Order by Trust Boundary:** Apply the trust-boundary-aware execution model — mutations before validations when sending, validations before mutations when receiving. -5. **Execute:** Call `interceptor/invoke` on the appropriate MCP server for each interceptor, applying resolved `failOpen`, `mode`, and `timeoutMs`. Mutations MUST be invoked sequentially (each receiving the output of the previous). Validations MAY be invoked in parallel. +5. **Execute:** Call `interceptor/invoke` on the appropriate MCP server for each interceptor, applying resolved `failOpen`, `mode`, and `timeoutMs`. Mutations MUST be invoked sequentially (each receiving the output of the previous). Validations MAY be invoked in parallel. The invoker MAY also impose an aggregate timeout covering all interceptors applied to one operation; if it is exceeded, the invoker MUST cancel the remaining invocations and treat the operation as timed out. 6. **Aggregate:** Collect all results, assembling the final payload and validation summary. SDK libraries are expected to provide reference implementations of this orchestration logic so that individual applications do not need to implement it from scratch. -##### Interceptor Chain +##### Invoker Overrides -The chain is constructed from interceptor entries, each describing an interceptor and the server that hosts it: +Overrides are invoker-local configuration associated with a discovered interceptor. The overrides object itself is not sent to the interceptor server; the invoker applies the resolved values, and the resolved `timeoutMs` is passed as `InterceptorInvocationParams.timeoutMs` on each invocation so that the server-side cancellation behavior in [Invocation Configuration and Context](#invocation-configuration-and-context) applies. ```typescript -interface InterceptorChain { - /** - * All interceptor entries in the chain, collected from one or more servers. - * Entries are merged and ordered by the chain - * according to the execution model. - */ - entries: ChainEntry[]; - - /** - * Execute the chain for a given event and phase. - */ - execute(params: ChainExecutionParams): ChainExecutionResult; -} - -interface ChainEntry { - /** - * The interceptor descriptor (as returned by interceptors/list). - * Contains the interceptor's capabilities and author-declared defaults. - */ - interceptor: { - name: string; - type: "mutation" | "validation"; - hooks: Array<{ - events: InterceptionEvent[]; - phase: "request" | "response"; - }>; - priorityHint?: number | { request?: number; response?: number }; - mode?: "active" | "audit"; - failOpen?: boolean; - }; - - /** - * The MCP server that hosts this interceptor. - * Used to route interceptor/invoke calls to the correct server. - * The concrete type is implementation-specific. - */ - server: MCPServerConnection; - - /** - * Optional invoker-side overrides for this interceptor's execution policy. - * Fields present here take precedence over the interceptor's declared defaults. - */ - overrides?: InterceptorOverrides; -} - interface InterceptorOverrides { /** * Override failure routing policy for this interceptor. @@ -1169,123 +1127,12 @@ interface InterceptorOverrides { } ``` -###### ChainExecutionParams - -```typescript -interface ChainExecutionParams { - /** - * Lifecycle event (hook) and phase for this chain - */ - event: InterceptionEvent; - phase: "request" | "response"; - - /** - * The payload to pass through the interceptor chain - */ - payload: unknown; - - /** - * Optional: restrict chain to specific interceptors by name - */ - interceptors?: string[]; - - /** - * Optional: per-interceptor configuration overrides - */ - config?: Record>; - - /** - * Optional: timeout for the entire chain in milliseconds - * implementations MUST cancel the entire chain if the aggregate timeout is exceeded. - */ - timeoutMs?: number; - - /** - * Optional: context passed to all interceptors - */ - context?: { - principal?: { - type: "user" | "service" | "anonymous"; - id?: string; - }; - traceId?: string; - timestamp: string; - }; -} -``` - -###### ChainExecutionResult - -```typescript -interface ChainExecutionResult { - /** - * Overall chain execution status - */ - status: "success" | "validation_failed" | "mutation_failed" | "timeout"; - - /** - * Lifecycle event (hook) and phase for this chain - */ - event: InterceptionEvent; - phase: "request" | "response"; - - /** - * Results from all executed interceptors - */ - results: Array<{ - interceptor: string; - type: "mutation" | "validation"; - phase: "request" | "response"; - // Mutation-specific fields - modified?: boolean; - payload?: unknown; - // Validation-specific fields - valid?: boolean; - severity?: "error" | "warn" | "info"; - messages?: Array<{ message: string; severity: "error" | "warn" | "info" }>; - // Audit mode - mode?: "audit"; - info?: Record; - // Timing - durationMs: number; - }>; - - /** - * Final payload after all mutations (if chain completed) - */ - finalPayload?: unknown; - - /** - * Validation summary - */ - validationSummary: { - errors: number; - warnings: number; - infos: number; - }; - - /** - * Total execution time - */ - totalDurationMs: number; - - /** - * If chain was aborted, details about where and why - */ - abortedAt?: { - interceptor: string; - reason: string; - type: "validation" | "mutation" | "timeout"; - }; -} -``` - -##### Example Interceptor Chain +##### Example: Interceptors for a `tools/call` Event For a `tools/call` event, assume the following interceptor are available: ```typescript -// From interceptor/list response: +// From interceptors/list response: [ { name: "pii-redactor", @@ -1343,12 +1190,12 @@ For a `tools/call` event, assume the following interceptor are available: ]; ``` -##### Example: Chain Entries with Overrides +##### Example: Invoker Overrides -The following example shows how invoker-side overrides adjust execution policy without modifying interceptor servers: +The following example shows how invoker-side overrides adjust execution policy without modifying interceptor servers. Each entry pairs a discovered interceptor descriptor, the server that advertised it, and the invoker's overrides: ```typescript -// Chain entries with invoker overrides applied: +// Discovered interceptors with invoker overrides applied: [ { interceptor: { @@ -1445,10 +1292,10 @@ CLIENT RESPONSE (Receiving): ##### Error Handling -Interceptor chains MUST handle errors based on type: +Invokers MUST handle errors based on interceptor type: ```typescript -// Mutation failure: Chain MUST halt immediately +// Mutation failure (active mode, failOpen: false): Execution MUST halt immediately { error: { code: -32603, @@ -1484,13 +1331,13 @@ During initialization, servers declare interceptor support: result: { protocolVersion: "2025-06-18", capabilities: { + // ...existing capabilities extensions: { "io.modelcontextprotocol/interceptors": { // Events this server's interceptors can handle supportedEvents: InterceptionEvent[]; } } - // ...existing capabilities }, serverInfo: { name: "example-server", @@ -1506,10 +1353,10 @@ During initialization, servers declare interceptor support: **Status**: Reserved for future specification -Inspired by gRPC interceptors and OpenTelemetry context propagation, future versions may support enrichable interceptor state that flows through the entire chain. This would enable: +Inspired by gRPC interceptors and OpenTelemetry context propagation, future versions may support enrichable interceptor state that flows across all interceptors applied to an operation. This would enable: - **State Sharing**: Early interceptor can enrich context for downstream interceptor -- **Cross-Cutting Data**: Authentication, rate limiting, and audit data flows through chain +- **Cross-Cutting Data**: Authentication, rate limiting, and audit data flows across interceptors - **Trace Correlation**: Distributed tracing baggage propagates automatically - **Deadline Propagation**: Timeout context flows through nested operations @@ -1517,7 +1364,7 @@ Inspired by gRPC interceptors and OpenTelemetry context propagation, future vers ```typescript // FUTURE: Enhanced context with state propagation -interface InterceptorChainContext { +interface InterceptorExecutionContext { // Immutable request context (set by initiator, read-only) readonly request: { principal?: { @@ -1531,7 +1378,7 @@ interface InterceptorChainContext { sessionId?: string; }; - // Mutable interceptor state (enriched by interceptor during chain execution) + // Mutable interceptor state (enriched by interceptors during execution) // Each interceptor can read and write to this shared state interceptorState?: Record; @@ -1687,7 +1534,7 @@ Alternative (global fixed ordering) rejected as too rigid for phase-aware operat **4. Separation of Capability and Policy** -The chain separates interceptor **capability** (what the server advertises) from execution **policy** (what the invoker configures via `overrides`): +The execution model separates interceptor **capability** (what the server advertises) from execution **policy** (what the invoker configures via `overrides`): - **Deployment flexibility**: The same interceptor can run in audit mode in staging and active mode in production without server changes - **SLO adaptation**: Platform teams adjust `failOpen` and `timeoutMs` per-deployment based on reliability requirements @@ -1701,7 +1548,8 @@ Alternative (policy only at server side) rejected because enterprise deployments - **Hook-Based Targeting**: Interceptors declare specific hooks (vs. all traffic) for performance and security - **Severity Levels** (info/warn/error): Graduated response vs. binary pass/fail enables audit logging without blocking - **Replace vs. Patch**: Mutations replace entire payloads (vs. JSON Patch) for simplicity and atomicity -- **Method Names**: `interceptor/list` and `interceptor/invoke` mirror MCP patterns (`tools/list`, etc.) +- **Method Names**: `interceptors/list` follows the plural namespace of `tools/list` and `resources/read`. `interceptor/invoke` is singular; aligning it to `interceptors/invoke` is under WG consideration +- **No Chain Primitive**: Multi-interceptor execution is invoker-local. Earlier drafts defined a chain first as a protocol method and then as an SDK utility; both were removed because the wire protocol needs only `interceptors/list` and `interceptor/invoke`, and an aggregate chain object invited implementations to diverge - **"info" Field**: Used instead of "metadata" to avoid confusion with MCP's `_meta` protocol metadata - **Cross-Boundary Support**: Client and server interceptors enable defense-in-depth and zero-trust architecture @@ -1725,7 +1573,7 @@ This SEP is fully backward compatible: 1. **Optional Capability**: Interceptors are an OPTIONAL server capability. Servers without interceptor support MUST continue to work unchanged. -2. **No Protocol Changes**: Existing JSON-RPC methods MUST NOT be modified. Interceptors add new methods (`interceptors/list`, `interceptors/invoke`) without changing existing ones. +2. **No Protocol Changes**: Existing JSON-RPC methods MUST NOT be modified. Interceptors add new methods (`interceptors/list`, `interceptor/invoke`) without changing existing ones. 3. **Client Compatibility**: Clients that do not support interceptors MUST be able to communicate with interceptor-enabled servers. Interceptors MAY run transparently on the server side. @@ -1748,7 +1596,7 @@ A compromised interceptor could: **Mitigations:** -- Interceptor runs in the same trust domain as the server/client +- Deploy interceptors out of process (stdio or Streamable HTTP), so they do not share the invoker's memory and see only what the invoker sends in each invocation (`payload`, `config`, `context`) and transport-level metadata. An Interceptor Server embedded over an in-memory transport shares the invoker's process and gets no such isolation. A local stdio interceptor still inherits the environment and filesystem access of the process that launches it, so it SHOULD be isolated accordingly - Organizations must vet interceptor before deployment - Interceptor should be sandboxed where possible - Audit logging of all interceptor invocations @@ -1759,13 +1607,13 @@ A compromised interceptor could: Attackers might try to bypass interceptor validation: - Send requests directly to underlying services -- Exploit interceptor chain ordering +- Exploit interceptor ordering **Mitigations:** - Interceptor enforcement must be at the transport layer - Server-side interceptor is mandatory for requests entering the trust boundary -- Interceptor chain order must be explicitly configured and immutable +- Interceptor order must be explicitly configured and immutable **3. Information Disclosure** @@ -1816,7 +1664,7 @@ Interceptor mutation could be used to inject unauthorized operations: 2. **Defense in Depth**: Interceptors SHOULD be deployed at both client and server boundaries -3. **Audit Everything**: Implementations SHOULD log all interceptor invocations, including payloads (with PII redaction) +3. **Audit Everything**: Implementations SHOULD log all interceptor invocations, including payloads (with PII redaction). For each invocation, the log SHOULD include the resolved `mode` and `failOpen`, and whether each value came from an invoker override or the interceptor's declared default 4. **Fail Secure**: Interceptors MUST default to `failOpen: false` (fail-closed). Implementations MUST reject requests when interceptors fail unless explicitly configured otherwise. @@ -1858,7 +1706,7 @@ A universal interceptor runtime that enables teams to deploy and manage intercep ```yaml # interceptor-config.yaml interceptors: - # Local interceptor: runs in-process + # Local interceptor: stdio MCP server on the same host - name: pii-redactor type: mutation transport: local @@ -1867,7 +1715,7 @@ interceptors: config: patterns: ["email", "ssn", "phone"] - # Remote interceptor: calls external service + # Remote interceptor: Streamable HTTP MCP server - name: security-scanner type: validation transport: remote @@ -1876,7 +1724,7 @@ interceptors: headers: Authorization: "Bearer ${SECURITY_TOKEN}" - # Local TypeScript interceptor + # Local TypeScript interceptor: stdio MCP server - name: rate-limiter type: validation transport: local