Integrations/Knowledge
Advanced15 min

MCP Interceptors

MCP interceptors are async middleware for tool calls — wrapping every MCP invocation with auth, retry, logging, and access control. This article covers the real API (MCPToolCallRequest, handler, override), when to use interceptors vs alternatives, core patterns with correct imports, multi-server routing via server_name, failure modes when interceptors break, and testing strategies.

Quick Reference

  • from langchain_mcp_adapters.interceptors import MCPToolCallRequest — the typed request object
  • Interceptor signature: async def my_interceptor(request: MCPToolCallRequest, handler) — NOT call_next
  • request.name for tool name, request.args for arguments, request.server_name for routing — NOT request.tool_name
  • request.override(headers={...}, args={...}) for immutable updates — never mutate request directly
  • tool_interceptors=[...] on MultiServerMCPClient — NOT interceptors=
  • First interceptor in list = outermost layer (runs first on request, last on response)
  • request.runtime can be None outside LangGraph — always guard access with if request.runtime:
  • Return ToolMessage to short-circuit without calling the tool; return Command to update agent state

Should I Use Interceptors?

Interceptors add indirection. Before reaching for one, ask whether the concern applies to all or most MCP tool calls uniformly. If it does — auth injection, logging, retry, rate limiting — an interceptor is the right abstraction. If the logic is specific to one tool, put it in the tool itself or in a graph node. The distinction matters because interceptors run on every tool call from every server, and a bug in any interceptor takes down all your MCP tools simultaneously.

Should I Use an Interceptor?Applies to all/mosttool calls?NO@tool decorator orgraph node insteadYESModify request orshort-circuit before tool?YESUse Interceptorauth · gating · headersNOLog, retry, orrate-limit all calls?YESUse Interceptorlogging · retry · CommandNOLogic is tool-specificuse a graph node or @tool instead

Interceptors are for cross-cutting concerns — if the logic targets one tool, keep it in the tool

ConcernUse Interceptor?AlternativeWhy
Inject user auth token into all MCP callsYesCross-cutting, applies uniformly to all servers
Log every tool call with timingYesCross-cutting observability — one interceptor covers all tools
Retry transient failures (ConnectionError, TimeoutError)YesCross-cutting resilience; retry logic doesn't belong per-tool
Block premium tools for free usersYesGraph conditional edgeInterceptor is simpler when the gate applies to all servers
Transform output format of one specific toolNo@tool wrapper or graph nodeTool-specific — an interceptor checking request.name == '...' for all logic is wrong
Route to different graph nodes based on resultMaybeGraph conditional edgeUse Command return only if routing needs tool call context
Rate-limit calls to one specific serverYes, with server_name guardPer-server configInterceptor can dispatch by request.server_name
One rule

If your interceptor starts with `if request.name == 'specific_tool':` for most of its logic, it belongs in the tool or a graph node. Interceptors shine when the logic is uniform across all tool calls.