Proposals · SEP-2624 · Draft

Interceptors for Model Context Protocol

Standards Track · Created 2025-11-04 · Source

Abstract

This SEP proposes an interceptor framework for the Model Context Protocol (MCP) that allows context operations to be intercepted, validated, and transformed at key points in the agentic lifecycle. It introduces a new MCP primitive for operations that shape agent context, including MCP-defined operations such as tool invocations, resource access, and prompt handling, as well as other well defined context operations with a standardized interface. Like other MCP primitives, interceptors are deployed as MCP servers and can be invoked by both clients and servers. The framework defines two types of interceptors, validators, which check context and return a pass/fail decision, and mutators, which transform context and return a modified payload. Interceptors follow a deterministic, trust-boundary-aware execution model and are discoverable and invocable through MCP's existing JSON-RPC patterns.

Motivation

The Problem: A Sprawling Ecosystem Without Reusability

The MCP ecosystem is rapidly developing a sprawling landscape of sidecars, proxies, and gateways to address cross-cutting concerns like validation, security, auditing, and rate limiting. However, these implementations are largely:

Imported from upstream. Proposed as modelcontextprotocol#2624 by @Degiorgio on 2026-04-22, where it was still open on 2026-09-25. Accepted into omcp on its technical merit. The text is the author's; only the status, the sponsor field, and documentation links were changed on import.

Reviewed 2026-09-25 — not yet accepted. Held as a draft because no prototype a reviewer can run is cited. Acceptance needs a prototype a reviewer can run, with its setup and the data to run it; Final also needs conformance tests and the reference documentation chapter.

This leads to an M × N problem: Each of M clients must integrate with each of N middleware solutions, resulting in M × N integration points and configurations.

The Solution: Standardized Plug-and-Play Interceptors

This SEP proposes standardizing interceptors in the same plug-and-play fashion that made MCP tools successful. The core goal is to transform the M × N problem into an M + N problem:

Key Advantages

1. Plug-and-Play Simplicity

Just as tools made MCP powerful through discoverability and standardization, interceptors bring the same benefits to cross-cutting concerns:

Add gateway-level interceptor → Plug in configuration → All tools and servers gain new capabilities

2. Language-Agnostic Deployment

Unlike framework-specific middleware (e.g., FastAPI middleware, Express.js middleware), interceptors enable platform teams to:

3. Sidecar and Service-Based Architecture

While traditional library-based middleware requires code integration, interceptors support:

This flexibility is critical for:

4. Well-Defined Invocation Lifecycle

Interceptors provide a standardized execution model with clear semantics for:

Technical Gaps Addressed

The current MCP specification lacks a standardized mechanism for:

  1. Input Validation: No way to validate tool parameters, prompt inputs, resource requests, sampling requests, or elicitation requests before processing
  2. Output Transformation: No capability to transform or sanitize responses before they reach clients or servers
  3. Cross-cutting Concerns: No support for logging, rate limiting, or content filtering that applies across multiple protocol operations (both server and client features)
  4. Policy Enforcement: No mechanism to enforce organizational policies on what prompts can be sent to LLMs, what tool results are returned, what sampling models can be used, or what data can be elicited from users
  5. Security Boundaries: When MCP servers and clients cross trust boundaries, there's no way to inspect or validate messages in either direction
  6. LLM Interaction Control: While MCP handles tool/resource/prompt orchestration and sampling requests, there's no hook into these interactions for content filtering or prompt injection detection
  7. Client Feature Protection: No way to prevent servers from requesting excessive sampling tokens, asking for sensitive information through elicitation, or accessing unauthorized filesystem roots

These limitations become critical in enterprise deployments where:

Ecosystem Benefits

For Platform Teams

For Server Developers

For Client Developers

For the Community

An interceptor framework addresses these needs by providing standardized extension points that maintain MCP's design principles while enabling powerful cross-cutting functionality and solving the ecosystem's reusability challenge.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Interceptor

Definition: a Context Operation is any operation that shapes, accesses, or modifies agentic context, such as tool invocations (which add results to context), resource access (which add content to context), prompt handling (which provide context templates), and skills (which orchestrate context).

Definition: a Lifecycle Event is an occurrence during a Context Operation, a specific moment when a context operation is initiated (request phase) or completed (response phase).

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.

Interceptors come in two types: Validators (see Validator) and Mutators (see Mutator).

Important: How do Interceptors Differ from Tools?

Tools and interceptors have fundamentally different invocation models:

Tools Interceptors
Invoked by LLM (non-deterministic) Invoked on specific Lifecycle Events (deterministic)
Result handling Automatically added to LLM context Returned to invoker (client, server, or agent harness) who then decides what to do with it.
Purpose Extend agent capabilities Govern context operations

Interface

interface Interceptor {
  /**
   * Unique identifier for the interceptor
   */
  name: string;

  /**
   * Semantic version of this interceptor
   */
  version?: string;

  /**
   * Human-readable description
   */
  description?: string;

  /**
   * Interceptor operation type
   */
  type: "validation" | "mutation";

  /**
   * Hooks: defines which Lifecycle Events and phases trigger this interceptor.
   * Each entry specifies a set of events and a single phase.
   */
  hooks: Array<{
    /**
     * List of Lifecycle Events this interceptor hooks into
     */
    events: InterceptionEvent[];

    /**
     * Execution phase
     */
    phase: "request" | "response";
  }>;

  /**
   * Execution mode (default: "active")
   * - "active": Normal blocking / transforming behavior
   * - "audit": Non-blocking operation
   *   - For validators: logs violations without blocking execution
   *   - For mutators: computes transformations without applying them (shadow mutations)
   */
  mode?: "active" | "audit";

  /**
   * Failure routing policy (default: false - fail-closed)
   *
   * Enforce 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
   *
   * Note: In audit mode, the message itself always proceeds (audit never blocks execution).
   * failOpen controls whether audit failures are treated as errors or warnings.
   */
  failOpen?: boolean;

  /**
   * Priority hint for mutation interceptor ordering (lower executes first).
   *
   * Can be specified as:
   * - A single number: applies to both request and response phases
   * - An object: specify different priorities per phase
   *
   * Range: 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
   * Default: 0 if omitted
   *
   * Tie-breaker: Interceptors with equal priorityHint are ordered alphabetically by name.
   * For validation interceptors, this field is ignored.
   *
   * Examples:
   *   priorityHint: -1000                           // Same priority for both phases
   *   priorityHint: { request: -1000 }              // Request: -1000, Response: 0 (default)
   *   priorityHint: { response: 1000 }              // Request: 0 (default), Response: 1000
   *   priorityHint: { request: -1000, response: 1000 }  // Different per phase
   */
  priorityHint?:
    | number
    | {
        request?: number;
        response?: number;
      };

  /**
   * Protocol version compatibility
   */
  compat?: {
    /**
     * Minimum MCP protocol version required
     */
    minProtocol: string;
    /**
     * Maximum MCP protocol version supported (optional)
     */
    maxProtocol?: string;
  };

  /**
   * Optional JSON Schema for interceptor configuration
   * Documents the expected configuration format
   */
  configSchema?: {
    type: "object";
    properties?: Record<string, unknown>;
    required?: string[];
    additionalProperties?: boolean;
  };
}

Hooks

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, e.g., tools/call, resources/read) 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 following interception events are defined by this specification. This list is NOT exhaustive, implementations MAY define additional interception events for custom or non-MCP context operations.

type InterceptionEvent =
  // MCP Server Features
  | "tools/list"
  | "tools/call"
  | "prompts/list"
  | "prompts/get"
  | "resources/list"
  | "resources/read"
  | "resources/subscribe"

  // MCP Client Features
  | "sampling/createMessage"
  | "elicitation/create"
  | "roots/list"

  // LLM Interaction Events (using common format)
  | "llm/completion"

  // Wildcards
  | "*" // Matches all events

  // Implementations MAY define additional events
  | string;

Wildcards:

Implementations MAY support additional wildcard patterns, such as namespace wildcards (e.g., "tools/*" to match all tool events, "resources/*" to match all resource events). Custom wildcard patterns SHOULD follow glob-style conventions.

Implementations MAY extend the set of interceptable Lifecycle Events beyond those defined in this specification. Custom events SHOULD follow the namespace/operation naming convention (e.g., "custom/myOperation").

Validator

A Validator is defined as strictly non-mutating interceptor that inspects a context operation and returns a structured decision. Validators MUST NOT modify the payload.

Common use cases of Validators include:

When invoked a validator receives the event payload and MUST return a ValidationResult.

ValidationResult:

interface ValidationResult {
  // Common Interceptor fields
  interceptor: string; // Name of the interceptor
  type: "validation"; // Interceptor type
  phase: "request" | "response"; // Execution phase
  durationMs?: number; // Execution time in ms
  info?: Record<string, unknown>; // Additional interceptor-specific data

  // Validation-specific fields
  valid: boolean; // Overall validation outcome
  severity?: "info" | "warn" | "error";
  messages?: Array<{
    path?: string; // JSON path to the offending field
    message: string; // Human-readable explanation
    severity: "info" | "warn" | "error";
  }>;
  suggestions?: Array<{
    // Optional corrections
    path: string;
    value: unknown;
  }>;
  signature?: {
    // Future: cryptographic proof of validation
    algorithm: "ed25519";
    publicKey: string;
    value: string;
  };
}

See Execution Model for validator execution semantics.

Mutator

A Mutator is a transformation interceptor that modifies context as it flows through the system. Mutators MAY change payloads to enforce policies, sanitize data, or enrich context.

Common use cases include:

When invoked a mutator receives the event payload and MUST return a MutationResult

MutationResult:

interface MutationResult {
  // Common Interceptor fields
  interceptor: string; // Name of the interceptor
  type: "mutation"; // Interceptor type
  phase: "request" | "response"; // Execution phase
  durationMs?: number; // Execution time in ms
  info?: Record<string, unknown>; // Additional interceptor-specific data

  // Mutation-specific fields
  modified: boolean; // Whether the payload was changed
  payload: unknown; // Transformed payload (or original if not modified)
}

See Execution Model for mutator execution semantics.

JSON-RPC Methods

Interceptor Discovery

Request:

{
  jsonrpc: "2.0",
  id: 1,
  method: "interceptors/list",
  params?: {
    // Optional filter by event
    event?: InterceptionEvent;
  }
}

Response:

{
  jsonrpc: "2.0",
  id: 1,
  result: {
    interceptors: [
      {
        name: "content-filter",
        version: "1.2.0",
        description: "Filters sensitive content from prompts and responses",
        type: "mutation",
        hooks: [
          { events: ["llm/completion", "prompts/get"], phase: "request" },
          { events: ["llm/completion", "prompts/get"], phase: "response" }
        ],
        priorityHint: -500,  // Same priority for both request and response
        compat: {
          minProtocol: "2024-11-05"
        },
        configSchema: {
          type: "object",
          properties: {
            sensitivityLevel: {
              type: "string",
              enum: ["low", "medium", "high"]
            },
            redactPatterns: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "parameter-validator",
        version: "2.0.1",
        description: "Validates tool call parameters",
        type: "validation",
        hooks: [
          { events: ["tools/call"], phase: "request" }
        ],
        compat: {
          minProtocol: "2024-11-05",
          maxProtocol: "2025-12-31"
        },
        configSchema: {
          type: "object",
          properties: {
            strictMode: { type: "boolean" }
          }
        }
      },
      {
        name: "pii-redactor",
        version: "2.1.0",
        description: "Redacts PII from requests and responses",
        type: "mutation",
        hooks: [
          { events: ["tools/call", "llm/completion"], phase: "request" },
          { events: ["tools/call", "llm/completion"], phase: "response" }
        ],
        failOpen: false,  // Fail-closed: block if interceptor fails (security-critical)
        // Different priorities for request vs response
        priorityHint: {
          request: -1000,  // Redact early when sending
          response: 1000   // Redact late when receiving (after decryption)
        },
        compat: {
          minProtocol: "2024-11-05"
        },
        configSchema: {
          type: "object",
          properties: {
            patterns: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "sampling-guard",
        version: "1.0.0",
        description: "Enforces token limits and model policies for sampling requests",
        type: "validation",
        hooks: [
          { events: ["sampling/createMessage"], phase: "request" }
        ],
        compat: {
          minProtocol: "2025-06-18"
        },
        configSchema: {
          type: "object",
          properties: {
            maxTokens: { type: "number" },
            allowedModels: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "elicitation-pii-blocker",
        version: "1.0.0",
        description: "Prevents elicitation requests from asking for sensitive information",
        type: "validation",
        hooks: [
          { events: ["elicitation/create"], phase: "request" }
        ],
        compat: {
          minProtocol: "2025-06-18"
        },
        configSchema: {
          type: "object",
          properties: {
            blockedPatterns: {
              type: "array",
              items: { type: "string" }
            },
            sensitiveFields: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "audit-logger",
        version: "1.0.0",
        description: "Logs all MCP operations for compliance",
        type: "validation",
        hooks: [
          { events: ["*"], phase: "request" },
          { events: ["*"], phase: "response" }
        ],
        mode: "audit",
        failOpen: true,
        configSchema: {
          type: "object",
          properties: {
            destination: {
              type: "string",
              enum: ["local", "remote", "both"]
            },
            includePayloads: { type: "boolean" }
          },
          required: ["destination"]
        }
      }
    ]
  }
}

Interceptor Invocation

Request for Validation:

{
  jsonrpc: "2.0",
  id: 2,
  method: "interceptor/invoke",
  params: {
    name: "parameter-validator",
    event: "tools/call",
    phase: "request",
    payload: {
      // Original request content
      method: "tools/call",
      params: {
        name: "get_weather",
        arguments: {
          location: "malicious_sql_injection"
        }
      }
    },
    config?: {
      // Optional interceptor-specific configuration
    }
  }
}

Response for Validation:

{
  jsonrpc: "2.0",
  id: 2,
  result: {
    valid: false,
    severity: "error",
    messages: [
      {
        path: "params.arguments.location",
        message: "Input contains potentially malicious content",
        severity: "error"
      }
    ],
    // Optional: suggested corrections
    suggestions?: [
      {
        path: "params.arguments.location",
        value: "[REDACTED]"
      }
    ],
    // Future: Cryptographic signature for validation result
    // This enables verification that validation occurred at trust boundary
    signature?: {
      algorithm: "ed25519",
      publicKey: "...",
      value: "..."
    }
  }
}

Note: The signature field is reserved for future use to enable cryptographic verification of validation results at trust boundaries. This will allow recipients to verify that validations were performed by authorized interceptor before accepting data.

Request for Mutation:

{
  jsonrpc: "2.0",
  id: 3,
  method: "interceptor/invoke",
  params: {
    name: "content-filter",
    event: "llm/completion",
    phase: "request",
    payload: {
      // LLM completion request (following common format)
      messages: [
        {
          role: "user",
          content: "What is the password for admin@example.com?"
        }
      ],
      model: "gpt-4"
    }
  }
}

Response for Mutation:

{
  jsonrpc: "2.0",
  id: 3,
  result: {
    modified: true,
    payload: {
      messages: [
        {
          role: "user",
          content: "What is the password for [REDACTED_EMAIL]?"
        }
      ],
      model: "gpt-4"
    },
    // Additional information about the mutation
    info: {
      redactions: 1,
      reason: "PII detected and redacted"
    }
  }
}
Invocation Configuration and Context

Interceptors MAY receive configuration and context:

interface InterceptorInvocationParams {
  name: string;
  event: InterceptionEvent;
  phase: "request" | "response";
  payload: unknown;

  // Optional configuration for this invocation
  config?: Record<string, unknown>;

  // Optional timeout in milliseconds
  // If exceeded, interceptor execution is cancelled and returns timeout error
  timeoutMs?: number;

  // Optional context information
  context?: {
    // Identity information
    principal?: {
      type: "user" | "service" | "anonymous";
      id?: string;
      claims?: Record<string, unknown>;
    };

    // Tracing information
    traceId?: string;
    spanId?: string;

    // Timing information
    timestamp: string; // ISO 8601

    // Session information
    sessionId?: string;

    // FUTURE: Interceptor state propagation
    // Allows interceptor to share state through the chain
    // This field is reserved for future enhancement
    interceptorState?: Record<string, unknown>;
  };
}

Timeout and Cancellation:

When an interceptor is invoked with timeoutMs specified:

// Timeout error response
{
  jsonrpc: "2.0",
  id: 1,
  error: {
    code: -32000,
    message: "Interceptor execution timeout",
    data: {
      interceptor: "slow-validator",
      timeoutMs: 5000,
      phase: "request"
    }
  }
}

Error Handling

Interceptor invocation errors follow standard JSON-RPC error format:

{
  jsonrpc: "2.0",
  id: 4,
  error: {
    code: -32603,
    message: "Interceptor execution failed",
    data: {
      interceptor: "content-filter",
      reason: "Configuration invalid"
    }
  }
}

Execution Model

Execution Model Summary:

  • Sending: Mutate → Validate → Send. A mutation error MUST halt the chain 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.
  • 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.
  • failOpen: false (default): If an interceptor fails, the message MUST be blocked.

The execution model follows a Trust-Boundary-Aware Pattern where validation acts as a security gate.

Trust Boundary Execution Pattern

Execution order depends on data flow direction:

Sending Data (Client → Server or Server → Client):

Mutate (sequential) → Validate (parallel) → Send

Receiving Data (Server ← Client or Client ← Server):

Receive → Validate (parallel) → Mutate (sequential)

Implementations MUST follow this trust-boundary-aware ordering when executing interceptors to ensure validation guards every boundary crossing.

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

Key semantics:

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:

function resolvePriority(
  entry: ChainEntry,
  phase: "request" | "response",
): number {
  const hint = entry.overrides?.priorityHint ?? entry.interceptor.priorityHint;
  if (hint === undefined) return 0;
  if (typeof hint === "number") return hint;
  return hint[phase] ?? 0;
}

Recommended Priority Ranges:

To promote consistency across interceptor implementations, the following priority ranges are RECOMMENDED:

Priority Range Purpose Example Use Cases
-2,000,000,000 to -1,000,000 System-critical security Anti-malware scanning, SQL injection prevention
-999,999 to -10,000 Security sanitization PII redaction, credential scrubbing, XSS filtering
-9,999 to -1,000 Input/output normalization Character encoding, format standardization
-999 to -1 Content transformation Language translation, markdown rendering
0 Default (no priority specified) General-purpose interceptor
1 to 999 Enrichment Metadata injection, tagging, formatting
1,000 to 9,999 Optimization Compression, caching, batching
10,000 to 999,999 Low-priority transformations Response enrichment, metadata injection
1,000,000 to 2,000,000,000 Low-priority finalization Audit stamps, response wrapping

Phase-Specific Priorities:

Different interceptors MAY need different priorities for request vs response phases:

// PII Redactor: Redact early when sending, late when receiving
{
  name: "pii-redactor",
  priorityHint: {
    request: -50000,   // Before other transforms
    response: 50000    // After decompression, decryption
  }
}

// Compression: Compress late when sending, decompress early when receiving
{
  name: "compressor",
  priorityHint: {
    request: 5000,     // After all transforms
    response: -5000    // Before other processing
  }
}

Sequence Diagrams

Client-Side Interceptor Invocation:

MCP ServerValidation(Validator)Mutation 2(Content Filter)Mutation 1(PII Redactor)MCP ClientApplicationMCP ServerValidation(Validator)Mutation 2(Content Filter)Mutation 1(PII Redactor)MCP ClientApplicationCLIENT REQUEST (Sending)STEP 1: Mutations (Sequential)STEP 2: Validation (Parallel)Trust Boundary Check═══ TRUST BOUNDARY ═══CLIENT RESPONSE (Receiving)STEP 1: Validation (Parallel)Trust Boundary CheckSTEP 2: Mutations (Sequential)tools/call requestDiscover interceptor via interceptor/listOrder mutations by priorityinterceptor/invoke(pii-redactor, priority=10){ modified: true, payload: {...} }interceptor/invoke(content-filter, priority=20){ modified: true, payload: {...} }interceptor/invoke(validator){ valid: true }Modified & validated requesttools/call responseinterceptor/invoke(response-validator){ valid: true }interceptor/invoke(response-enhancer, priority=10){ modified: false, payload: {...} }Final validated & processed response

Server-Side Interceptor Invocation:

Tool ImplementationMutation(Sanitizer)Validation(PII Checker)Validation(Schema Validator)MCP ServerMCP ClientTool ImplementationMutation(Sanitizer)Validation(PII Checker)Validation(Schema Validator)MCP ServerMCP ClientSERVER REQUEST (Receiving)═══ TRUST BOUNDARY ═══STEP 1: Validation (Parallel)Trust Boundary Checkpar[Validation after receiving][Validation]STEP 2: Mutations (Sequential)SERVER RESPONSE (Sending)STEP 1: Mutations (Sequential)STEP 2: Validation (Parallel)Trust Boundary Checkpar[Validation before sending][Validation]═══ TRUST BOUNDARY ═══alt[Any validation failed][All validations passed]tools/call requestDiscover interceptor via interceptor/listOrder mutations by priorityinterceptor/invoke(schema-validator){ valid: true }interceptor/invoke(pii-checker){ valid: true }Error: Validation failedinterceptor/invoke(sanitizer, priority=10){ modified: true, payload: {...} }Execute toolTool resultinterceptor/invoke(response-sanitizer, priority=10){ modified: true, payload: {...} }interceptor/invoke(schema-validator){ valid: true }interceptor/invoke(pii-checker){ valid: true }Mutated & validated response

Cross-Boundary Interceptor (Client + Server):

ToolServer Mutation(Sanitizer)Server Validation(Schema/PII Validator)MCP ServerClient Validation(Validator)Client Mutation(PII Redactor)MCP ClientApplicationToolServer Mutation(Sanitizer)Server Validation(Schema/PII Validator)MCP ServerClient Validation(Validator)Client Mutation(PII Redactor)MCP ClientApplicationCLIENT REQUEST (Sending - Trust Boundary)Client STEP 1: Mutations (Sequential)Client STEP 2: Validation (Parallel)Trust Boundary Check═══════════════════════║ TRUST BOUNDARY ║═══════════════════════Server STEP 1: Validation (Parallel)Trust Boundary CheckServer STEP 2: Mutations (Sequential)SERVER RESPONSE (Sending - Trust Boundary)Server STEP 1: Mutations (Sequential)Server STEP 2: Validation (Parallel)Trust Boundary Check═══════════════════════║ TRUST BOUNDARY ║═══════════════════════Client STEP 1: Validation (Parallel)Trust Boundary CheckClient STEP 2: Mutations (Sequential)Validation guards both sides of trust boundarytools/call with PIIinterceptor/invoke(pii-redactor){ modified: true, payload: "[REDACTED]" }interceptor/invoke(validator){ valid: true }Validated requestinterceptor/invoke(schema/pii-validator){ valid: true }interceptor/invoke(sanitizer){ modified: true, payload: {...} }Execute tool safelyResultinterceptor/invoke(response-sanitizer){ modified: true, payload: {...} }interceptor/invoke(schema-validator){ valid: true }Validated responseinterceptor/invoke(response-validator){ valid: true }interceptor/invoke(response-enhancer){ modified: false, payload: {...} }Final validated result

Chain Execution

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.

Important: The execution model defined above MUST be followed by all implementations, whether invoking interceptors individually via interceptor/invoke or using this convenience utility.

Capability vs Policy

The chain distinguishes between two layers of interceptor configuration:

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.

This separation enables:

Orchestration

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.
  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.
  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

The chain is constructed from interceptor entries, each describing an interceptor and the server that hosts it:

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.
   * Takes precedence over the interceptor's declared failOpen default.
   */
  failOpen?: boolean;

  /**
   * Override mutation ordering priority.
   * Takes precedence over the interceptor's declared priorityHint default.
   * Ignored for validation interceptors.
   */
  priorityHint?: number | { request?: number; response?: number };

  /**
   * Override execution mode.
   * Takes precedence over the interceptor's declared mode default.
   * - "active": Normal blocking / transforming behavior
   * - "audit": Non-blocking operation (log-only for validators, shadow for mutators)
   */
  mode?: "active" | "audit";

  /**
   * Per-interceptor timeout in milliseconds.
   * If exceeded, the interceptor is cancelled and the resolved failOpen
   * determines whether the message proceeds or is blocked.
   */
  timeoutMs?: number;

  /**
   * Narrow the interceptor's declared hooks.
   * Each entry MUST be a subset of the interceptor's declared hooks.
   * If provided, the interceptor is only invoked for matching events/phases
   * from this narrowed set rather than its full declared hooks.
   * Implementations MUST reject overrides that widen beyond declared hooks.
   */
  hooks?: Array<{
    events: InterceptionEvent[];
    phase: "request" | "response";
  }>;
}
ChainExecutionParams
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<string, Record<string, unknown>>;

  /**
   * 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
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<string, unknown>;
    // 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

For a tools/call event, assume the following interceptor are available:

// From interceptor/list response:
[
  {
    name: "pii-redactor",
    type: "mutation",
    hooks: [
      { events: ["tools/call", "llm/completion"], phase: "request" },
      { events: ["tools/call", "llm/completion"], phase: "response" },
    ],
    priorityHint: {
      request: -1000, // Run first when sending
      response: 1000, // Run last when receiving
    },
  },
  {
    name: "content-filter",
    type: "mutation",
    hooks: [
      { events: ["llm/completion"], phase: "request" },
      { events: ["llm/completion"], phase: "response" },
    ],
    priorityHint: -500, // Same priority for both phases
  },
  {
    name: "format-normalizer",
    type: "mutation",
    hooks: [
      { events: ["tools/call"], phase: "request" },
      { events: ["tools/call"], phase: "response" },
    ],
    priorityHint: { request: 100 }, // Only applies to requests, response uses default (0)
  },
  {
    name: "schema-validator",
    type: "validation",
    hooks: [{ events: ["tools/call"], phase: "request" }],
    // No priorityHint - runs in parallel with other validators
  },
  {
    name: "parameter-validator",
    type: "validation",
    hooks: [{ events: ["tools/call"], phase: "request" }],
    // No priorityHint - runs in parallel
  },
  {
    name: "audit-logger",
    type: "validation",
    hooks: [
      { events: ["*"], phase: "request" },
      { events: ["*"], phase: "response" },
    ],
    mode: "audit",
    // No priorityHint - runs in parallel with other validators
    // Audit mode means violations logged but don't block
  },
];
Example: Chain Entries with Overrides

The following example shows how invoker-side overrides adjust execution policy without modifying interceptor servers:

// Chain entries with invoker overrides applied:
[
  {
    interceptor: {
      name: "pii-redactor",
      type: "mutation",
      hooks: [
        { events: ["tools/call", "llm/completion"], phase: "request" },
        { events: ["tools/call", "llm/completion"], phase: "response" },
      ],
      priorityHint: { request: -1000, response: 1000 },
      failOpen: false,
    },
    server: serverA,
    overrides: {
      // Override priority for this deployment
      priorityHint: { request: -2000, response: 2000 },
      // Set a per-interceptor timeout
      timeoutMs: 3000,
      // Narrow hooks: only intercept tools/call, not llm/completion
      hooks: [
        { events: ["tools/call"], phase: "request" },
        { events: ["tools/call"], phase: "response" },
      ],
    },
  },
  {
    interceptor: {
      name: "audit-logger",
      type: "validation",
      hooks: [
        { events: ["*"], phase: "request" },
        { events: ["*"], phase: "response" },
      ],
      mode: "audit",
      failOpen: true,
    },
    server: serverB,
    overrides: {
      // Promote from audit to active in production
      mode: "active",
    },
  },
  {
    interceptor: {
      name: "schema-validator",
      type: "validation",
      hooks: [{ events: ["tools/call"], phase: "request" }],
      failOpen: false,
    },
    server: serverC,
    overrides: {
      // Known fragile interceptor: fail-open in this deployment
      failOpen: true,
      timeoutMs: 2000,
    },
  },
];

In this example:

Full Request/Response Cycle
CLIENT REQUEST (Sending):
  Original: { email: "john@example.com", ssn: "123-45-6789" }
  → Mutate: pii-redactor (-1000), content-filter (-500), format-normalizer (100)
  → Result: { email: "[EMAIL]", ssn: "[SSN]" }
  → Validate & Observe: client-validator ✓, audit-logger ✓
  → Send across TRUST BOUNDARY

SERVER REQUEST (Receiving):
  Received: { email: "[EMAIL]", ssn: "[SSN]" }
  → Validate & Observe: schema-validator ✓, parameter-validator ✓, audit-logger ✓
  → Mutate: sanitizer (10)
  → Execute Tool → { result: { name: "John", ... } }

SERVER RESPONSE (Sending):
  Original: { name: "John", ... }
  → Mutate: pii-redactor (1000), content-filter (-500)
  → Validate & Observe: response-validator ✓, audit-logger ✓
  → Send across TRUST BOUNDARY

CLIENT RESPONSE (Receiving):
  Received: { name: "John", ... }
  → Validate & Observe: response-validator ✓, audit-logger ✓
  → Mutate: response-enhancer (10)
  → Deliver to Application
Error Handling

Interceptor chains MUST handle errors based on type:

// Mutation failure: Chain MUST halt immediately
{
  error: {
    code: -32603,
    message: "Interceptor mutation failed",
    data: { failedInterceptor: "content-filter", lastValidPayload: {...} }
  }
}
// Validation failure: All validations MUST complete, then reject if any has severity: "error"
{
  error: {
    code: -32602,
    message: "Interceptor validation failed",
    data: {
      validationErrors: [
        { interceptor: "schema-validator", severity: "error", message: "Required field missing" }
      ]
    }
  }
}

Interceptor Server Capabilities

During initialization, servers declare interceptor support:

{
  jsonrpc: "2.0",
  id: 1,
  result: {
    protocolVersion: "2025-06-18",
    capabilities: {
      extensions: {
        "io.modelcontextprotocol/interceptors": {
          // Events this server's interceptors can handle
          supportedEvents: InterceptionEvent[];
        }
      }
      // ...existing capabilities
    },
    serverInfo: {
      name: "example-server",
      version: "1.0.0"
    }
  }
}

Future Enhancements

Context Propagation and Interceptor State (Future)

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:

Proposed Model:

// FUTURE: Enhanced context with state propagation
interface InterceptorChainContext {
  // Immutable request context (set by initiator, read-only)
  readonly request: {
    principal?: {
      type: "user" | "service" | "anonymous";
      id?: string;
      claims?: Record<string, unknown>;
    };
    traceId: string;
    spanId?: string;
    timestamp: string;
    sessionId?: string;
  };

  // Mutable interceptor state (enriched by interceptor during chain execution)
  // Each interceptor can read and write to this shared state
  interceptorState?: Record<string, unknown>;

  // Deadline for entire operation (inherited and propagated)
  deadline?: string; // ISO 8601 timestamp

  // Distributed tracing baggage (key-value pairs for trace correlation)
  // Follows W3C Baggage specification
  baggage?: Record<string, string>;
}

Interceptor State Enrichment:

Mutation and validation interceptors could optionally return context updates:

// FUTURE: MutationResult extended with context enrichment
interface MutationResult {
  interceptor: string;
  type: "mutation";
  phase: "request" | "response";
  durationMs?: number;
  info?: Record<string, unknown>;
  modified: boolean;
  payload: unknown;

  // Optional: Updates to propagate to downstream interceptors
  contextUpdates?: {
    interceptorState?: Record<string, unknown>;
    baggage?: Record<string, string>;
  };
}

Example Flow:

// Interceptor 1: Authentication enriches context
{
  interceptor: "authenticator",
  type: "mutation",
  modified: false,
  payload: { /* unchanged */ },
  contextUpdates: {
    interceptorState: {
      principal: {
        id: "user-123",
        roles: ["admin"],
        authMethod: "oauth2"
      }
    }
  }
}

// Interceptor 2: Rate limiter reads enriched context
// Has access to context.interceptorState.principal
{
  interceptor: "rate-limiter",
  type: "validation",
  valid: true,
  info: {
    rateLimitUsed: 45,
    rateLimitMax: 100,
    // Used principal.id from context for rate limiting
  },
  contextUpdates: {
    interceptorState: {
      rateLimit: { remaining: 55, resetAt: "2025-11-04T15:30:00Z" }
    }
  }
}

// Interceptor 3: Audit logger has full enriched context
// Logs with principal + rate limit info + original request
// Runs in audit mode (non-blocking)
{
  interceptor: "audit-logger",
  type: "validation",
  mode: "audit",
  valid: true,
  severity: "info",
  info: {
    logEntry: {
      user: "user-123",
      roles: ["admin"],
      rateLimitRemaining: 55,
      action: "tools/call",
      timestamp: "2025-11-04T15:29:00Z"
    }
  }
}

Benefits of State Propagation:

  1. Loose Coupling: Interceptor don't need direct dependencies
  2. Composability: New interceptor can leverage existing enriched state
  3. Audit and Logging: Full context available for audit interceptors to log and trace
  4. Performance: Avoid redundant operations (e.g., auth check once, reuse result)

Design Considerations:

This feature would be opt-in and backward compatible, with interceptor that don't support context enrichment working unchanged.

Rationale

Design Decisions

1. Separation of Validation and Mutation

We explicitly separate validation and mutation interceptor types rather than combining them because:

Audit Mode for Both Types

Instead of a separate observability type, both validators and mutators support audit mode:

This approach is simpler and more flexible than a separate observability type

2. Validation as Trust Boundary Guard

Execution order is trust-boundary-aware:

This ensures validation guards all boundary crossings, enabling zero-trust architecture and future cryptographic verification. Alternative (same order for all phases) rejected because validation must be first when receiving untrusted data.

3. Phase-Aware Priority Hints

Mutations declare priorityHint (default: 0) which can differ by phase. Benefits:

Alternative (global fixed ordering) rejected as too rigid for phase-aware operations like compression/decompression.

4. Separation of Capability and Policy

The chain separates interceptor capability (what the server advertises) from execution policy (what the invoker configures via overrides):

Alternative (policy only at server side) rejected because enterprise deployments need invoker-side control over execution behavior without coupling to interceptor authors' choices. Alternative (no server defaults) rejected because it forces every invoker to fully configure every interceptor.

5. Other Design Choices

Inspired By:

Complements Existing MCP:

Backward Compatibility

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.

  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.

  4. Graceful Degradation: If a client calls interceptors/list on a server that does not support interceptors, the server MUST return a standard JSON-RPC error.

  5. Version Negotiation: The existing protocol version negotiation during initialization SHOULD be used to discover interceptor support.

Security Implications

Threat Model

1. Malicious Interceptor

A compromised interceptor could:

Mitigations:

2. Interceptor Bypass

Attackers might try to bypass interceptor validation:

Mitigations:

3. Information Disclosure

Interceptor error messages might leak sensitive information:

Mitigations:

4. Denial of Service

Interceptor could be exploited for DoS:

Mitigations:

5. Privilege Escalation

Interceptor mutation could be used to inject unauthorized operations:

Mitigations:

Security Best Practices

  1. Principle of Least Privilege: Interceptors SHOULD only have access to the data they need to process

  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)

  4. Fail Secure: Interceptors MUST default to failOpen: false (fail-closed). Implementations MUST reject requests when interceptors fail unless explicitly configured otherwise.

  5. Input Validation: Interceptor configuration and payloads MUST be validated before processing

  6. Cryptographic Integrity: Implementations SHOULD consider signing interceptor responses to prevent tampering

  7. Isolation: Untrusted interceptors SHOULD be run in sandboxed environments

Compliance Considerations

Interceptors enable compliance with:

However, organizations MUST ensure:

Reference Implementation

The reference implementation will provide:

Multi-Language SDKs

SDK libraries for writing and running interceptors in various languages, including Python, TypeScript/JavaScript, Go and other community-contributed languages. These SDKs will provide base classes, helpers, and patterns for building validation and mutation interceptors, with built-in support for audit mode operation.

Common Interceptor Sidecar

A universal interceptor runtime that enables teams to deploy and manage interceptors without modifying individual MCP servers. The sidecar acts as an MCP proxy that loads custom interceptors (both local and remote) via configuration and injects interceptor invocations transparently.

Example Configuration:

# interceptor-config.yaml
interceptors:
  # Local interceptor: runs in-process
  - name: pii-redactor
    type: mutation
    transport: local
    command: ./interceptors/pii-redactor.py
    args: []
    config:
      patterns: ["email", "ssn", "phone"]

  # Remote interceptor: calls external service
  - name: security-scanner
    type: validation
    transport: remote
    endpoint: https://security.example.com/scan
    timeout: 5s
    headers:
      Authorization: "Bearer ${SECURITY_TOKEN}"

  # Local TypeScript interceptor
  - name: rate-limiter
    type: validation
    transport: local
    command: ./interceptors/rate-limiter.js
    args: []
    config:
      requests_per_minute: 100

routing:
  # Apply interceptors to specific lifecycle event types
  tools.call:
    request: [rate-limiter, security-scanner]
    response: [pii-redactor]

  llm.completion:
    request: [security-scanner]
    response: [pii-redactor]

This approach enables platform teams to deploy guardrails across all services from a central control plane without code changes to existing MCP servers.

Acknowledgments

This proposal draws inspiration from: