Back to AI App Dev Series

LangChain SDK Track Part 7: MCP — Model Context Protocol Integration

May 22, 2026 Wasil Zafar 45 min read

Build and connect MCP tool servers with LangChain agents using langchain-mcp-adapters. Master FastMCP, MultiServerMCPClient, transport mechanisms (stdio, HTTP), stateful sessions, structured content, interceptors, and production deployment patterns.

Table of Contents

  1. MCP Overview
  2. Building MCP Servers
  3. Connecting Agents to MCP
  4. Advanced Patterns
  5. LangGraph + MCP
  6. Production Patterns
What You’ll Learn: Advanced LangGraph features unlock production-grade capabilities: sub-graphs for modular workflows, streaming for real-time UIs, persistent checkpointing for fault tolerance, and human-in-the-loop for supervised automation. This article builds on LangGraph basics with patterns you’ll need at scale: long-running workflows that survive server restarts, parallel branches that merge results, and breakpoints where humans approve before the graph continues.

1. MCP Overview

The Model Context Protocol (MCP) is an open standard that defines how AI applications connect to external tools, data sources, and services. Think of it as a USB for AI — a universal protocol that lets any AI model interact with any tool through a standardized interface, regardless of the model provider or the tool implementation.

LangChain integrates with MCP via the langchain-mcp-adapters library, which converts MCP tools into LangChain tools — making them directly usable in any LangChain agent, LangGraph workflow, or Deep Agent.

1.1 MCP Architecture

MCP Architecture: Host → Client → Server
flowchart LR
    A["MCP Host
(Your App / Agent)"] --> B["MCP Client
(langchain-mcp-adapters)"] B -->|"stdio"| C["MCP Server A
(Local Process)"] B -->|"HTTP"| D["MCP Server B
(Remote Service)"] C --> E["Tools: add, multiply"] D --> F["Tools: get_weather, search"]
Component Role Example
MCP Host The AI application that initiates connections Your LangChain agent, IDE extension, Claude Desktop
MCP Client Protocol client that manages server connections MultiServerMCPClient from langchain-mcp-adapters
MCP Server Exposes tools, resources, and prompts to AI models Custom FastMCP server, GitHub MCP server, Slack MCP server

1.2 Why MCP Matters

Key Insight: Before MCP, every AI application had to implement custom integrations for each tool — N models × M tools = N×M integrations. MCP reduces this to N + M: each model implements the MCP client once, each tool implements the MCP server once. Write tools once, use them with any AI model that supports MCP.

2. Building MCP Servers

MCP servers expose tools (callable functions), resources (readable data), and prompts (reusable templates) that any MCP-compatible client can discover and use. The FastMCP library is the recommended way to build custom MCP servers.

2.1 FastMCP Setup

# Install FastMCP
pip install fastmcp

# Or with uv (faster)
uv add fastmcp

2.2 Defining Tools with FastMCP

A minimal MCP server with two tools — one for math, one for weather. Each @mcp.tool() decorated function becomes a tool discoverable by any MCP client:

from fastmcp import FastMCP

# Create a named MCP server
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__":
    # Run with stdio transport (for local subprocess communication)
    mcp.run(transport="stdio")

A separate weather server using HTTP transport for remote access:

from fastmcp import FastMCP

mcp = FastMCP("Weather")

@mcp.tool()
async def get_weather(location: str) -> str:
    """Get weather for a given location.

    Args:
        location: City name or coordinates
    """
    # In production, call a real weather API here
    return f"Weather in {location}: 22°C, Partly Cloudy"

@mcp.tool()
async def get_forecast(location: str, days: int = 5) -> str:
    """Get multi-day weather forecast.

    Args:
        location: City name or coordinates
        days: Number of forecast days (1-10)
    """
    return f"{days}-day forecast for {location}: Sunny, 20-25°C"

if __name__ == "__main__":
    # Run with HTTP transport (Streamable HTTP for remote access)
    mcp.run(transport="streamable-http", port=8000)

2.3 Resources & Prompts

MCP servers can also expose resources (readable data) and prompts (reusable templates) alongside tools:

from fastmcp import FastMCP

mcp = FastMCP("KnowledgeBase")

@mcp.tool()
def search_docs(query: str, max_results: int = 5) -> str:
    """Search the documentation knowledge base.

    Args:
        query: Search query string
        max_results: Maximum number of results to return
    """
    # Replace with actual search logic
    return f"Found {max_results} results for '{query}'"

@mcp.resource("docs://api-reference")
def get_api_reference() -> str:
    """Return the current API reference documentation."""
    return "# API Reference\n\n## Endpoints\n- GET /users\n- POST /orders"

@mcp.prompt("summarize")
def summarize_prompt(text: str) -> str:
    """A reusable prompt for summarizing text."""
    return f"Please summarize the following text concisely:\n\n{text}"

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8001)
Architecture Pattern

MCP Server Design Guidelines

Tools are for actions (queries, API calls, computations). Resources are for static or slowly-changing data (documentation, schemas, config). Prompts are reusable templates that clients can inject into conversations. Keep servers focused — one domain per server (e.g., a "database" server, a "weather" server, a "github" server).

Tools = Actions Resources = Data Prompts = Templates

3. Connecting LangChain Agents to MCP Servers

The langchain-mcp-adapters library converts MCP tools into LangChain tools, making them usable in any agent or workflow. The core class is MultiServerMCPClient, which manages connections to one or more MCP servers.

3.1 MultiServerMCPClient

# Install the adapter library
pip install langchain-mcp-adapters
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

async def main():
    # Connect to multiple MCP servers simultaneously
    client = MultiServerMCPClient(
        {
            "math": {
                "transport": "stdio",
                "command": "python",
                "args": ["/path/to/math_server.py"],
            },
            "weather": {
                "transport": "http",
                "url": "http://localhost:8000/mcp",
            }
        }
    )

    # Get all tools from all connected servers
    tools = await client.get_tools()
    print(f"Discovered {len(tools)} tools from MCP servers")

    # Create a LangChain agent with MCP tools
    agent = create_agent("openai:gpt-4o-mini", tools)

    # Invoke — the agent automatically selects the right MCP tool
    math_response = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "What's (3 + 5) x 12?"}]}
    )
    print(math_response["messages"][-1].content)

    weather_response = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "What's the weather in NYC?"}]}
    )
    print(weather_response["messages"][-1].content)

if __name__ == "__main__":
    asyncio.run(main())
Key Insight: MultiServerMCPClient is stateless by default. Each tool invocation creates a fresh MCP session, executes the tool, and cleans up. This is ideal for most use cases. For servers that maintain state across calls, use explicit stateful sessions.

3.2 Transport Mechanisms

MCP supports different transport types for client-server communication:

Transport Communication Best For Statefulness
stdio Standard input/output (subprocess) Local tools, simple setups, development Inherently stateful (subprocess persists)
http HTTP requests (Streamable HTTP) Remote services, production, shared servers Stateless by default
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient

async def demo_transports():
    client = MultiServerMCPClient(
        {
            # stdio: launches server as a subprocess
            "local_tools": {
                "transport": "stdio",
                "command": "python",
                "args": ["./servers/local_tools.py"],
            },
            # HTTP with custom headers (auth, tracing)
            "remote_api": {
                "transport": "http",
                "url": "http://api.example.com/mcp",
                "headers": {
                    "Authorization": "Bearer YOUR_TOKEN",
                    "X-Trace-Id": "session-001",
                },
            },
        }
    )

    tools = await client.get_tools()
    print(f"Total tools available: {len(tools)}")
    for tool in tools:
        print(f"  - {tool.name}: {tool.description}")

asyncio.run(demo_transports())

3.3 Stateful Sessions

When working with MCP servers that maintain state across tool calls (e.g., a database connection that keeps a transaction open), create a persistent session:

import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain.agents import create_agent

async def stateful_example():
    client = MultiServerMCPClient(
        {
            "database": {
                "transport": "http",
                "url": "http://localhost:9000/mcp",
            }
        }
    )

    # Create an explicit session — persists across tool calls
    async with client.session("database") as session:
        # Load tools within the session context
        tools = await load_mcp_tools(session)

        agent = create_agent("openai:gpt-4o-mini", tools)

        # Both calls share the same MCP session
        # (e.g., same DB transaction, same auth context)
        result1 = await agent.ainvoke(
            {"messages": [{"role": "user", "content": "Begin a transaction and insert a user named Alice"}]}
        )
        result2 = await agent.ainvoke(
            {"messages": [{"role": "user", "content": "Now commit the transaction"}]}
        )
        print(result2["messages"][-1].content)

asyncio.run(stateful_example())
Real-World Application

Regulatory Compliance Workflow

A bank built their compliance review as an advanced LangGraph: sub-graphs for different regulation types (AML, KYC, sanctions), streaming updates to a dashboard showing progress, and mandatory human approval breakpoints before any compliance decisions are finalized. Checkpointing ensures no review is lost during system maintenance. Result: compliance review time reduced 40% while maintaining 100% audit trail integrity.

Sub-GraphsHuman-in-the-LoopCheckpointing

4. Advanced Patterns

4.1 Structured Content

MCP tools can return structured content (JSON) alongside human-readable text. The adapter wraps structured content in an MCPToolArtifact accessible via the artifact field on the ToolMessage:

import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from langchain_core.messages import ToolMessage

async def structured_content_example():
    client = MultiServerMCPClient(
        {"data_api": {"transport": "http", "url": "http://localhost:8000/mcp"}}
    )
    tools = await client.get_tools()
    agent = create_agent("openai:gpt-4o-mini", tools)

    result = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "Get the sales data for Q1 2026"}]}
    )

    # Extract structured content from tool messages
    for message in result["messages"]:
        if isinstance(message, ToolMessage) and message.artifact:
            structured_data = message.artifact["structured_content"]
            print(f"Structured response: {structured_data}")

asyncio.run(structured_content_example())

4.2 Authentication

For production MCP servers requiring authentication, pass headers or implement the httpx.Auth interface:

import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

async def authenticated_mcp():
    client = MultiServerMCPClient(
        {
            "secure_api": {
                "transport": "http",
                "url": "https://api.company.com/mcp",
                "headers": {
                    "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs...",
                    "X-API-Version": "2026-05",
                },
            }
        }
    )

    tools = await client.get_tools()
    agent = create_agent("anthropic:claude-sonnet-4-6", tools)

    result = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "List my recent deployments"}]}
    )
    print(result["messages"][-1].content)

asyncio.run(authenticated_mcp())

4.3 Interceptors

Interceptors let you transform tool inputs/outputs before they reach the agent. Use them for logging, validation, or appending structured content to the conversation:

import json
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.agents import create_agent
from mcp.types import TextContent

async def log_and_transform(request: MCPToolCallRequest, handler):
    """Interceptor that logs tool calls and appends structured content."""
    print(f"[MCP] Calling tool: {request.tool_name} with args: {request.arguments}")

    # Execute the tool
    result = await handler(request)

    # If the tool returned structured content, append it as visible text
    if result.structuredContent:
        result.content += [
            TextContent(type="text", text=json.dumps(result.structuredContent, indent=2))
        ]

    print(f"[MCP] Tool {request.tool_name} completed")
    return result

async def interceptor_example():
    # Pass interceptors at client creation
    client = MultiServerMCPClient(
        {"api": {"transport": "http", "url": "http://localhost:8000/mcp"}},
        tool_interceptors=[log_and_transform]
    )

    tools = await client.get_tools()
    agent = create_agent("openai:gpt-4o-mini", tools)

    result = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "Search for 'rate limiting best practices'"}]}
    )
    print(result["messages"][-1].content)

asyncio.run(interceptor_example())

5. LangGraph + MCP

5.1 MCP Tools in LangGraph Workflows

MCP tools integrate seamlessly with LangGraph StateGraph workflows. Load tools from MCP servers and pass them to nodes that need external capabilities:

import asyncio
from typing import Annotated, TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode

class State(TypedDict):
    messages: Annotated[list, add_messages]

async def build_mcp_workflow():
    # Load MCP tools
    client = MultiServerMCPClient(
        {
            "search": {"transport": "http", "url": "http://localhost:8000/mcp"},
            "database": {"transport": "http", "url": "http://localhost:8001/mcp"},
        }
    )
    tools = await client.get_tools()

    # Bind tools to model
    model = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)

    def agent_node(state: State) -> dict:
        response = model.invoke(state["messages"])
        return {"messages": [response]}

    def should_use_tools(state: State) -> str:
        last_msg = state["messages"][-1]
        if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
            return "tools"
        return END

    # Build the graph
    graph = StateGraph(State)
    graph.add_node("agent", agent_node)
    graph.add_node("tools", ToolNode(tools))

    graph.add_edge(START, "agent")
    graph.add_conditional_edges("agent", should_use_tools, {"tools": "tools", END: END})
    graph.add_edge("tools", "agent")

    app = graph.compile()

    # Run the workflow
    result = await app.ainvoke(
        {"messages": [HumanMessage(content="Search for Python best practices and save to database")]}
    )
    print(result["messages"][-1].content)

asyncio.run(build_mcp_workflow())

5.2 Exposing LangGraph Agents as MCP Servers

Agent Server implements MCP using the Streamable HTTP transport, allowing your LangGraph agents to be exposed as MCP tools themselves — making them usable by any MCP-compliant client:

Bidirectional MCP: Agents consume AND expose MCP tools
flowchart TB
    A["External MCP Client
(Claude Desktop, IDE)"] -->|"/mcp endpoint"| B["Agent Server
(Your LangGraph Agent)"] B -->|"MCP Client"| C["MCP Server A
(Database)"] B -->|"MCP Client"| D["MCP Server B
(Search)"] B -->|"MCP Client"| E["MCP Server C
(GitHub)"]
Key Insight: This creates a powerful composition pattern: your LangGraph agent consumes tools from multiple MCP servers, performs complex reasoning, and then exposes its own capabilities as MCP tools at /mcp. Other agents or clients can use your agent as a single unified tool — abstracting away all the internal complexity.

6. Production Patterns

Pattern When to Use Implementation
Server per domain Clear separation of concerns One FastMCP server per API/service
Stateless tools Scalable, cacheable operations Default MultiServerMCPClient mode
Stateful sessions Transactions, multi-step workflows async with client.session()
Auth via headers Token-based API access headers: {"Authorization": "Bearer ..."}
Interceptors for logging Observability, auditing tool_interceptors=[log_fn]
Agent-as-MCP-server Composing agents into larger systems Agent Server /mcp endpoint
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from deepagents import create_deep_agent

async def production_mcp_agent():
    """Production pattern: Deep Agent with MCP tools from multiple servers."""

    # Connect to production MCP servers
    client = MultiServerMCPClient(
        {
            "github": {
                "transport": "http",
                "url": "https://mcp.internal.company.com/github",
                "headers": {"Authorization": "Bearer ghp_..."},
            },
            "slack": {
                "transport": "http",
                "url": "https://mcp.internal.company.com/slack",
                "headers": {"Authorization": "Bearer xoxb-..."},
            },
            "database": {
                "transport": "http",
                "url": "https://mcp.internal.company.com/postgres",
                "headers": {"Authorization": "Bearer db_token_..."},
            },
        }
    )

    # Load all MCP tools
    mcp_tools = await client.get_tools()
    print(f"Loaded {len(mcp_tools)} tools from 3 MCP servers")

    # Create a Deep Agent with MCP tools
    agent = create_deep_agent(
        model="anthropic:claude-sonnet-4-6",
        tools=mcp_tools,
        system_prompt="""You are a DevOps assistant with access to GitHub, Slack, and the database.
Use the appropriate tools to help the user manage their infrastructure.""",
    )

    result = agent.invoke(
        {"messages": [{"role": "user", "content": "Check for open PRs with failing CI, then post a summary to #engineering"}]}
    )
    print(result["messages"][-1].content)

asyncio.run(production_mcp_agent())
Security Note: Never hardcode API tokens in your MCP client configuration. Use environment variables or a secrets manager. For production, implement the httpx.Auth interface for automatic token refresh and rotation.
Try It Yourself: Build an ‘investment analysis’ graph with: (1) parallel branches (one researches fundamentals, another researches technicals), (2) a merge node that combines findings, (3) a human-in-the-loop breakpoint before the ‘buy/sell/hold’ recommendation, (4) persistent checkpointing with SQLite. Test: run the graph, stop it mid-way, restart, and verify it resumes from the checkpoint.

Next in the SDK Track

In LC Part 8: Deep Agents & Harness Framework, we’ll explore the batteries-included agent harness — create_deep_agent(), virtual filesystems, context engineering, subagent delegation, skills, and production deployment.