Skip to content

#337: Add a MCP Server to LangGraph

Last week we saw how FastMCP can help us to create a MCP server. In this post we see how we can connect our LangGraph application to an MCP server.

Installation

We need the package langchain-mcp-adapters to connect LangGraph with a MCP server. Let us use this chance to also update all the other packages we need:

uv pip install -U langgraph langchain-openai langchain-mcp-adapters fastmcp

Improved MCP Server

While a simple placeholder was enough for last week’s post, this time we want to create something more useful. We need a method that detects the local IP by opening a socket to 8.8.8.8. That way we can find the real IP our device uses and not just the loopback address 127.0.0.1.

With a bit of netsh magic and output parsing we can detect the name of the Wi-Fi network we currently use. While this will not work on Linux or Mac, it is a nice way for everyone who uses Windows.

import socket
import subprocess
import re
from fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP("Network Support Service")

def get_local_ip() -> str:
    """Retrieves the active local IP address."""
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        # Does not actually establish a connection, but triggers routing
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
    except Exception:
        ip = "127.0.0.1"
    finally:
        s.close()
    return ip

def get_wifi_ssid() -> str:
    """Retrieves the current Wi-Fi SSID on Windows using netsh."""
    try:
        # Run Windows netsh command to get interface state
        result = subprocess.run(
            ["netsh", "wlan", "show", "interfaces"],
            capture_output=True,
            text=True,
            check=True,
            encoding="utf-8" # or "cp1252" if standard windows encoding causes issues
        )

        # Look for the SSID line in the output
        match = re.search(r"^\s*SSID\s*:\s*(.*)$", result.stdout, re.MULTILINE)
        if match:
            return match.group(1).strip()
        return "Not connected to Wi-Fi (or Ethernet/VPN active)"
    except Exception as e:
        return f"Unknown (Error fetching SSID: {str(e)})"

@mcp.tool()
def get_network_info() -> str:
    """
    Retrieves the current local IP address and the connected Wi-Fi SSID.

    Returns:
        A formatted string with the network details.
    """
    ip = get_local_ip()
    ssid = get_wifi_ssid()

    return f"Current Network Info:\n- Local IP: {ip}\n- Wi-Fi SSID: {ssid}"


if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)

With this code in place, we can run it as we did before and see in LM Studio if it found our new tool:

In LM Studio we now have the tool get_network_info

Connect the LangGraph application

The langchain-mcp-adapters module works asynchronously, that is why we need to switch to the async/await pattern for this example. We build our LangGraph application as we did so often before, but this time we need a client that connects to the MCP server and reads the tool-list from there. We then hand the tool list to our LLM, build our nodes and connect them. Our agent node talks to the LLM and collects all tool calls, that then are handled in the call_tools node.

import asyncio
import sys

from langchain_core.messages import ToolMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI
from langgraph.graph import START, END, MessagesState, StateGraph

sys.stdout.reconfigure(encoding="utf-8")

llm = ChatOpenAI(
    base_url="http://localhost:1234/v1",
    api_key="lm-studio",
    model="openai/gpt-oss-20b",
    temperature=0.1,
)

MCP_URL = "http://127.0.0.1:8000/mcp"

client = MultiServerMCPClient(
    {"support": {"url": MCP_URL, "transport": "streamable_http"}}
)

# get_tools() opens a session, lists the server's tools, and closes it. Each
# returned tool reconnects on its own when invoked, so we fetch once here and
# reuse the tool objects for the lifetime of the process.
tools = asyncio.run(client.get_tools())
tools_by_name = {t.name: t for t in tools}
llm_with_tools = llm.bind_tools(tools)


async def agent(state: MessagesState) -> dict:
    print(f"[agent] calling LLM (messages={len(state['messages'])})")
    response = await llm_with_tools.ainvoke(state["messages"])
    if response.tool_calls:
        names = ", ".join(tc["name"] for tc in response.tool_calls)
        print(f"[agent] LLM requested tool(s): {names}")
    else:
        print("[agent] LLM produced a final answer (no tool calls)")
    return {"messages": [response]}


async def call_tools(state: MessagesState) -> dict:
    results = []
    for tc in state["messages"][-1].tool_calls:
        print(f"[tools] executing MCP tool '{tc['name']}' args={tc['args']}")
        output = await tools_by_name[tc["name"]].ainvoke(tc["args"])
        results.append(ToolMessage(content=str(output), tool_call_id=tc["id"], name=tc["name"]))
    return {"messages": results}


def route(state: MessagesState) -> str:
    return "tools" if state["messages"][-1].tool_calls else END


workflow = StateGraph(MessagesState)

workflow.add_node("agent", agent)
workflow.add_node("tools", call_tools)

workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", route, {"tools": "tools", END: END})
workflow.add_edge("tools", "agent")

graph = workflow.compile()
png_bytes = graph.get_graph(xray=1).draw_mermaid_png()
with open("mcp_tools.png", "wb") as f:
    f.write(png_bytes)


async def main() -> None:
    question = sys.argv[1] if len(sys.argv) > 1 else "What is my current IP address and Wi-Fi network?"

    print("--- TOOLS DISCOVERED ON MCP SERVER ---")
    for t in tools:
        print(f"  {t.name}: {(t.description or '').splitlines()[0]}")
    print("--- /TOOLS ---\n")

    result = await graph.ainvoke({"messages": [{"role": "user", "content": question}]})

    print("\n--- ANSWER ---")
    print(result["messages"][-1].content)
    print("--- /ANSWER ---")


if __name__ == "__main__":
    asyncio.run(main())

This gives us this graph: We have the agent node that loops through the tools node.

Run the application

If we now run our LangGraph application, it will fetch the tools from the MCP server, initialises our graph and answers the question with the help of the tools from the MCP server:

--- TOOLS DISCOVERED ON MCP SERVER ---
  get_network_info: Retrieves the current local IP address and the connected Wi-Fi SSID.
--- /TOOLS ---

[agent] calling LLM (messages=1)
[agent] LLM requested tool(s): get_network_info
[tools] executing MCP tool 'get_network_info' args={}
[agent] calling LLM (messages=3)
[agent] LLM produced a final answer (no tool calls)

--- ANSWER ---
**Current Network Info**

- **Local IP address:** `192.***.***.***`
- **Wi‑Fi SSID:** *Not connected to Wi‑Fi* (you’re likely on Ethernet or a VPN).
--- /ANSWER ---

Next

The biggest change to add an MCP server to a LangGraph application is the switch to the async/await pattern. Without that change, most would stay the same as we know from local tool calling.

Next week we explore the topic of multi-agents and see how LangGraph can help us implementing them.