Help with MultiServerMCPClient tool interceptor / runtime

Hi,

I am testing the exact code from the documentation:

https://docs.langchain.com/oss/python/langchain/mcp#accessing-runtime-context

I want to add auth headers to the call to an external mcp server

from dataclasses import dataclass
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.agents import create_agent

@dataclass
class Context:
user_id: str
api_key: str

async def inject_user_context(
request: MCPToolCallRequest,
handler,
):
“”“Inject user credentials into MCP tool calls.”“”
runtime = request.runtime
user_id = runtime.context.user_id
api_key = runtime.context.api_key

# Add user context to tool arguments
modified_request = request.override(
    args={**request.args, "user_id": user_id}
)
return await handler(modified_request)

client = MultiServerMCPClient(
{…},
tool_interceptors=[inject_user_context],
)
tools = await client.get_tools()
agent = create_agent(“gpt-4o”, tools, context_schema=Context)

Invoke with user context

result = await agent.ainvoke(
{“messages”: [{“role”: “user”, “content”: “Search my orders”}]},
context={“user_id”: “user_123”, “api_key”: “sk-…”}
)

the interceptor is called properly but request.runtime is None , I provided one in aiinvoke.

Any clue ?

Thanks

Hello, if you pass context to ainvoke, then request.runtime will not be None. For example:

response_with_context = await agent.ainvoke(
    {"messages": [HumanMessage(content="What is my user ID?")]},
    config=config,
    context={"user_id": "user_456", "name": "Jane Smith"}
)

Please see my sample repo with code to show how to pass context using tool_interceptors:

Hello,

Thanks to the sample project, very helpful, I was able to make it work, the code was fine, the problem was with my langchain version, I had to to update from 1.2.1 to 1.2.3 to make it work.

The runtime in interceptor works for me when using a prebuilt agent (create_agent), or in a custom StateGraph when using the ToolNode.

However I can get the runtime in the interceptor when using a custom tool node in a state graph:

Here is the tool node, I am adding the context but it do not pass to the interceptor call.

    async def _tool_call(self, state: GraphState, config: RunnableConfig, runtime: Runtime) -> Command:
        """Process tool calls from the last message.

        Args:
            state: The current agent state containing messages and tool calls.
            config: The runnable config containing context and metadata.
            context:The runtime context
        Returns:
            Command: Command object with updated messages and routing back to chat.
        """
        outputs = []
        for tool_call in state.messages[-1].tool_calls:

            try:
                # Invoke tool with config to pass context for ToolRuntime and InjectedState
                tool_result = await self.tools_by_name[tool_call["name"]].ainvoke(
                    tool_call["args"],
                    config=config,
                    context=runtime.context,
                )
                logger.info(
                    "tool_executed_successfully",
                    tool_name=tool_call["name"],
                    session_id=config["configurable"]["thread_id"],
                )
            except Exception as e:
                # Log the error and return generic message for LLM to handle
                logger.exception(
                    "tool_execution_failed",
                    tool_name=tool_call["name"],
                    error=str(e),
                    session_id=config["configurable"]["thread_id"],
                    tool_args=tool_call["args"],
                )
                tool_result = f"An error occurred while executing this tool. Please inform the user that the operation failed and they should try again."

            outputs.append(
                ToolMessage(
                    content=tool_result,
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        return Command(update={"messages": outputs}, goto="chat")

Thanks!

Hi @PierreC
Great the issue is solved! :heart:
Could you mark the post as Solved picking the @conrad.corbett’s answer? It’s for the others to follow this solution if they face it too.

sure!

1 Like

Thanks @PierreC :flexed_biceps: