Skip to main content
Model Context Protocol (MCP) is an open protocol that standardizes how applications provide tools and context to LLMs. LangChain agents can use tools defined on MCP servers using the langchain-mcp-adapters library.

Quickstart

Install the langchain-mcp-adapters library:
pip install langchain-mcp-adapters
langchain-mcp-adapters enables agents to use tools defined across one or more MCP servers.
MultiServerMCPClient is stateless by default. Each tool invocation creates a fresh MCP ClientSession, executes the tool, and then cleans up. See the stateful sessions section for more details.
Accessing multiple MCP servers
from langchain_mcp_adapters.client import MultiServerMCPClient  
from langchain.agents import create_agent


client = MultiServerMCPClient(  
    {
        "math": {
            "transport": "stdio",  # Local subprocess communication
            "command": "python",
            # Absolute path to your math_server.py file
            "args": ["/path/to/math_server.py"],
        },
        "weather": {
            "transport": "http",  # HTTP-based remote server
            # Ensure you start your weather server on port 8000
            "url": "http://localhost:8000/mcp",
        }
    }
)

tools = await client.get_tools()  
agent = create_agent(
    "claude-sonnet-4-5-20250929",
    tools  
)
math_response = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]}
)
weather_response = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "what is the weather in nyc?"}]}
)

Custom servers

To create a custom MCP server, use the FastMCP library:
pip install fastmcp
To test your agent with MCP tool servers, use the following examples:
from fastmcp import FastMCP

mcp = FastMCP("Math")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool()
def multiply(a: int, b: int) -> int:
    """Multiply two numbers"""
    return a * b

if __name__ == "__main__":
    mcp.run(transport="stdio")

Transports

MCP supports different transport mechanisms for client-server communication.

HTTP

The http transport (also referred to as streamable-http) uses HTTP requests for client-server communication. See the MCP HTTP transport specification for more details.
client = MultiServerMCPClient(
    {
        "weather": {
            "transport": "http",
            "url": "http://localhost:8000/mcp",
        }
    }
)

Passing headers

When connecting to MCP servers over HTTP, you can include custom headers (e.g., for authentication or tracing) using the headers field in the connection configuration. This is supported for sse (deprecated by MCP spec) and streamable_http transports.
Passing headers with MultiServerMCPClient
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient(
    {
        "weather": {
            "transport": "http",
            "url": "http://localhost:8000/mcp",
            "headers": {  
                "Authorization": "Bearer YOUR_TOKEN",  
                "X-Custom-Header": "custom-value"
            },  
        }
    }
)
tools = await client.get_tools()
agent = create_agent("openai:gpt-4.1", tools)
response = await agent.ainvoke({"messages": "what is the weather in nyc?"})

Authentication

The langchain-mcp-adapters library uses the official MCP SDK under the hood, which allows you to provide a custom authentication mechanism by implementing the httpx.Auth interface.
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient(
    {
        "weather": {
            "transport": "http",
            "url": "http://localhost:8000/mcp",
            "auth": auth, 
        }
    }
)

stdio

Client launches server as a subprocess and communicates via standard input/output. Best for local tools and simple setups.
Unlike HTTP transports, stdio connections are inherently stateful—the subprocess persists for the lifetime of the client connection. However, when using MultiServerMCPClient without explicit session management, each tool call still creates a new session. See stateful sessions for managing persistent connections.
client = MultiServerMCPClient(
    {
        "math": {
            "transport": "stdio",
            "command": "python",
            "args": ["/path/to/math_server.py"],
        }
    }
)

Stateful sessions

By default, MultiServerMCPClient is stateless—each tool invocation creates a fresh MCP session, executes the tool, and then cleans up. If you need to control the lifecycle of an MCP session (for example, when working with a stateful server that maintains context across tool calls), you can create a persistent ClientSession using client.session().
Using MCP ClientSession for stateful tool usage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain.agents import create_agent

client = MultiServerMCPClient({...})

# Create a session explicitly
async with client.session("server_name") as session:  
    # Pass the session to load tools, resources, or prompts
    tools = await load_mcp_tools(session)  
    agent = create_agent(
        "anthropic:claude-3-7-sonnet-latest",
        tools
    )

Core features

Tools

Tools allow MCP servers to expose executable functions that LLMs can invoke to perform actions—such as querying databases, calling APIs, or interacting with external systems. LangChain converts MCP tools into LangChain tools, making them directly usable in any LangChain agent or workflow.

Loading tools

Use client.get_tools() to retrieve tools from MCP servers and pass them to your agent:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient({...})
tools = await client.get_tools()  
agent = create_agent("claude-sonnet-4-5-20250929", tools)

Structured content

MCP tools can return structured content alongside the human-readable text response. This is useful when a tool needs to return machine-parseable data (like JSON) in addition to text that gets shown to the model. When an MCP tool returns structuredContent, the adapter wraps it in an MCPToolArtifact and returns it as the tool’s artifact. You can access this using the artifact field on the ToolMessage. Extracting structured content from artifact After invoking your agent, you can access the structured content from tool messages in the response:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from langchain.messages import ToolMessage

client = MultiServerMCPClient({...})
tools = await client.get_tools()
agent = create_agent("claude-sonnet-4-5-20250929", tools)

result = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "Get data from the server"}]}
)

# Extract structured content from tool messages
for message in result["messages"]:
    if isinstance(message, ToolMessage) and message.artifact:
        structured_content = message.artifact["structured_content"]

Multimodal tool content

MCP tools can return multimodal content (images, text, etc.) in their responses. When an MCP server returns content with multiple parts (e.g., text and images), the adapter converts them to LangChain’s standard content blocks. You can access the standardized representation via the content_blocks property on the ToolMessage:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient({...})
tools = await client.get_tools()
agent = create_agent("claude-sonnet-4-5-20250929", tools)

result = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "Take a screenshot of the current page"}]}
)

# Access multimodal content from tool messages
for message in result["messages"]:
    if message.type == "tool":
        # Raw content in provider-native format
        print(f"Raw content: {message.content}")

        # Standardized content blocks  #
        for block in message.content_blocks:  
            if block["type"] == "text":  
                print(f"Text: {block['text']}")  
            elif block["type"] == "image":  
                print(f"Image URL: {block.get('url')}")  
                print(f"Image base64: {block.get('base64', '')[:50]}...")  
This allows you to handle multimodal tool responses in a provider-agnostic way, regardless of how the underlying MCP server formats its content.

Resources

Resources allow MCP servers to expose data—such as files, database records, or API responses—that can be read by clients. LangChain converts MCP resources into Blob objects, which provide a unified interface for handling both text and binary content.

Loading resources

Use client.get_resources() to load resources from an MCP server:
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({...})

# Load all resources from a server
blobs = await client.get_resources("server_name")  

# Or load specific resources by URI
blobs = await client.get_resources("server_name", uris=["file:///path/to/file.txt"])  

for blob in blobs:
    print(f"URI: {blob.metadata['uri']}, MIME type: {blob.mimetype}")
    print(blob.as_string())  # For text content
You can also use load_mcp_resources directly with a session for more control:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.resources import load_mcp_resources

client = MultiServerMCPClient({...})

async with client.session("server_name") as session:
    # Load all resources
    blobs = await load_mcp_resources(session)

    # Or load specific resources by URI
    blobs = await load_mcp_resources(session, uris=["file:///path/to/file.txt"])

Prompts

Prompts allow MCP servers to expose reusable prompt templates that can be retrieved and used by clients. LangChain converts MCP prompts into messages, making them easy to integrate into chat-based workflows.

Loading prompts

Use client.get_prompt() to load a prompt from an MCP server:
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({...})

# Load a prompt by name
messages = await client.get_prompt("server_name", "summarize")  

# Load a prompt with arguments
messages = await client.get_prompt(  
    "server_name",  
    "code_review",  
    arguments={"language": "python", "focus": "security"}  
)  

# Use the messages in your workflow
for message in messages:
    print(f"{message.type}: {message.content}")
You can also use load_mcp_prompt directly with a session for more control:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.prompts import load_mcp_prompt

client = MultiServerMCPClient({...})

async with client.session("server_name") as session:
    # Load a prompt by name
    messages = await load_mcp_prompt(session, "summarize")

    # Load a prompt with arguments
    messages = await load_mcp_prompt(
        session,
        "code_review",
        arguments={"language": "python", "focus": "security"}
    )

Advanced features

Tool Interceptors

MCP servers run as separate processes—they can’t access LangGraph runtime information like the store, context, or agent state. Interceptors bridge this gap by giving you access to this runtime context during MCP tool execution. Interceptors also provide middleware-like control over tool calls: you can modify requests, implement retries, add headers dynamically, or short-circuit execution entirely.
SectionDescription
Accessing runtime contextRead user IDs, API keys, store data, and agent state
State updates and commandsUpdate agent state or control graph flow with Command
Writing interceptorsPatterns for modifying requests, composing interceptors, and error handling

Accessing runtime context

When MCP tools are used within a LangChain agent (via create_agent), interceptors receive access to the ToolRuntime context. This provides access to the tool call ID, state, config, and store—enabling powerful patterns for accessing user data, persisting information, and controlling agent behavior.
  • Runtime context
  • Store
  • State
  • Tool call ID
Access user-specific configuration like user IDs, API keys, or permissions that are passed at invocation time:
Inject user context into MCP tool calls
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-..."}
)
For more context engineering patterns, see Context engineering and Tools.

State updates and commands

Interceptors can return Command objects to update agent state or control graph execution flow. This is useful for tracking task progress, switching between agents, or ending execution early.
Mark task complete and switch agents
from langchain.agents import AgentState, create_agent
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.messages import ToolMessage
from langgraph.types import Command

async def handle_task_completion(
    request: MCPToolCallRequest,
    handler,
):
    """Mark task complete and hand off to summary agent."""
    result = await handler(request)

    if request.name == "submit_order":
        return Command(
            update={
                "messages": [result] if isinstance(result, ToolMessage) else [],
                "task_status": "completed",  
            },
            goto="summary_agent",  
        )

    return result
Use Command with goto="__end__" to end execution early:
End agent run on completion
async def end_on_success(
    request: MCPToolCallRequest,
    handler,
):
    """End agent run when task is marked complete."""
    result = await handler(request)

    if request.name == "mark_complete":
        return Command(
            update={"messages": [result], "status": "done"},
            goto="__end__",  
        )

    return result

Custom interceptors

Interceptors are async functions that wrap tool execution, enabling request/response modification, retry logic, and other cross-cutting concerns. They follow an “onion” pattern where the first interceptor in the list is the outermost layer. Basic pattern An interceptor is an async function that receives a request and a handler. You can modify the request before calling the handler, modify the response after, or skip the handler entirely.
Basic interceptor pattern
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest

async def logging_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """Log tool calls before and after execution."""
    print(f"Calling tool: {request.name} with args: {request.args}")
    result = await handler(request)
    print(f"Tool {request.name} returned: {result}")
    return result

client = MultiServerMCPClient(
    {"math": {"transport": "stdio", "command": "python", "args": ["/path/to/server.py"]}},
    tool_interceptors=[logging_interceptor],  
)
Modifying requests Use request.override() to create a modified request. This follows an immutable pattern, leaving the original request unchanged.
Modifying tool arguments
async def double_args_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """Double all numeric arguments before execution."""
    modified_args = {k: v * 2 for k, v in request.args.items()}
    modified_request = request.override(args=modified_args)  
    return await handler(modified_request)

# Original call: add(a=2, b=3) becomes add(a=4, b=6)
Modifying headers at runtime Interceptors can modify HTTP headers dynamically based on the request context:
Dynamic header modification
async def auth_header_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """Add authentication headers based on the tool being called."""
    token = get_token_for_tool(request.name)
    modified_request = request.override(
        headers={"Authorization": f"Bearer {token}"}  
    )
    return await handler(modified_request)
Composing interceptors Multiple interceptors compose in “onion” order — the first interceptor in the list is the outermost layer:
Composing multiple interceptors
async def outer_interceptor(request, handler):
    print("outer: before")
    result = await handler(request)
    print("outer: after")
    return result

async def inner_interceptor(request, handler):
    print("inner: before")
    result = await handler(request)
    print("inner: after")
    return result

client = MultiServerMCPClient(
    {...},
    tool_interceptors=[outer_interceptor, inner_interceptor],  
)

# Execution order:
# outer: before -> inner: before -> tool execution -> inner: after -> outer: after
Error handling Use interceptors to catch tool execution errors and implement retry logic:
Retry on error
import asyncio

async def retry_interceptor(
    request: MCPToolCallRequest,
    handler,
    max_retries: int = 3,
    delay: float = 1.0,
):
    """Retry failed tool calls with exponential backoff."""
    last_error = None
    for attempt in range(max_retries):
        try:
            return await handler(request)
        except Exception as e:
            last_error = e
            if attempt < max_retries - 1:
                wait_time = delay * (2 ** attempt)  # Exponential backoff
                print(f"Tool {request.name} failed (attempt {attempt + 1}), retrying in {wait_time}s...")
                await asyncio.sleep(wait_time)
    raise last_error

client = MultiServerMCPClient(
    {...},
    tool_interceptors=[retry_interceptor],  
)
You can also catch specific error types and return fallback values:
Error handling with fallback
async def fallback_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """Return a fallback value if tool execution fails."""
    try:
        return await handler(request)
    except TimeoutError:
        return f"Tool {request.name} timed out. Please try again later."
    except ConnectionError:
        return f"Could not connect to {request.name} service. Using cached data."

Progress notifications

Subscribe to progress updates for long-running tool executions:
Progress callback
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext

async def on_progress(
    progress: float,
    total: float | None,
    message: str | None,
    context: CallbackContext,
):
    """Handle progress updates from MCP servers."""
    percent = (progress / total * 100) if total else progress
    tool_info = f" ({context.tool_name})" if context.tool_name else ""
    print(f"[{context.server_name}{tool_info}] Progress: {percent:.1f}% - {message}")

client = MultiServerMCPClient(
    {...},
    callbacks=Callbacks(on_progress=on_progress),  
)
The CallbackContext provides:
  • server_name: Name of the MCP server
  • tool_name: Name of the tool being executed (available during tool calls)

Logging

The MCP protocol supports logging notifications from servers. Use the Callbacks class to subscribe to these events.
Logging callback
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
from mcp.types import LoggingMessageNotificationParams

async def on_logging_message(
    params: LoggingMessageNotificationParams,
    context: CallbackContext,
):
    """Handle log messages from MCP servers."""
    print(f"[{context.server_name}] {params.level}: {params.data}")

client = MultiServerMCPClient(
    {...},
    callbacks=Callbacks(on_logging_message=on_logging_message),  
)

Additional resources


Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.