Agent Series (10): MCP Protocol — Standardizing the Tool Ecosystem

A deep dive into how Model Context Protocol (MCP) solves the tool ecosystem management problem. Three runnable demos cover traditional Function Calling limitations, MCP Server dynamic tool discovery, and LLM agents using MCP tools via langchain-mcp-adapters. Includes Host/Client/Server architecture breakdown and a complete MCP vs Function Calling comparison.

·8 min read·AI Engineering

More Tools, More Chaos

After you build an Agent, the first thing you usually do is give it tools: search, code execution, database queries, API calls…

With traditional Function Calling, tool definitions look like this:

@tool
def search_jira(query: str) -> str:
    """Search Jira tickets"""
    ...
 
@tool
def query_database(sql: str) -> str:
    """Execute a SQL query"""
    ...
 
agent = create_react_agent(model=llm, tools=[search_jira, query_database, ...])

This works great — until you start building a second Agent.

The problem: Agent B also needs search_jira. You import the function or copy it over. Then Agent C, Agent D. Tool definitions start drifting across the codebase. One day you update search_jira's logic and need to find every file that references it — scattered across four different files.

This is the problem MCP (Model Context Protocol) solves: turning tools from "each Agent defines its own" into "a shared service any Agent connects to via protocol".


MCP's Three-Layer Architecture

MCP breaks tool invocation into three roles:

┌────────────────────────────────────────────────────────────────┐
│                      MCP Architecture                           │
├──────────────────┬──────────────────────────────────────────────┤
│ Host             │ The environment hosting the Agent:           │
│                  │ Claude Desktop, Claude Code, custom apps     │
│                  │ Contains one or more MCP Clients             │
├──────────────────┼──────────────────────────────────────────────┤
│ Client           │ Protocol client embedded in the Host         │
│                  │ Manages connections and communication        │
│                  │ with MCP Servers                             │
├──────────────────┼──────────────────────────────────────────────┤
│ Server           │ Independent process exposing Tools           │
│                  │ Can also expose Resources and Prompts        │
└──────────────────┴──────────────────────────────────────────────┘
 
Transport options:
  Local:  stdio (subprocess stdin/stdout)
  Remote: HTTP + SSE (Server-Sent Events)

The core architectural difference from Function Calling is that the Server is an independent process:

  • Function Calling: tools are Python functions, written in Agent code, called in-process
  • MCP: tools are independent services, called cross-process via JSON-RPC

An independent process means: tools can be implemented in any language, shared by any number of Agents, and updated without touching Agent code.


Demo 1: Traditional Function Calling's Limitations

@lc_tool
def calculator(expression: str) -> str:
    """Evaluate a simple arithmetic expression."""
    ...
 
@lc_tool
def text_stats(text: str) -> str:
    """Return word count, sentence count, and character count."""
    ...
 
@lc_tool
def weather_mock(city: str) -> str:
    """Return mock weather data for a city."""
    ...
 
traditional_tools = [calculator, text_stats, weather_mock]
agent = create_react_agent(model=llm, tools=traditional_tools)

Three test questions, real execution:

Q: What is 2 ** 10 + 100 / 4?
A: The result of 2 ** 10 + 100 / 4 is 1049.0.
 
Q: Analyze this text: 'Python is elegant. It is readable. Everyone loves it!'
A: 9 words, 3 sentences, 53 characters.
 
Q: What's the weather in Beijing?
A: sunny, 25°C, humidity 40%.

Works perfectly. The problem is architectural:

Tools defined in THIS file: ['calculator', 'text_stats', 'weather_mock']
 
If Agent B also needs these tools:  copy-paste or re-import
If you update calculator's logic:   find every Agent file that uses it
If the tools are written in TypeScript:  this approach doesn't work at all

Demo 2: MCP Server — Dynamic Tool Discovery

The same three tools implemented as a standalone MCP Server using FastMCP:

# tools_server.py
from mcp.server.fastmcp import FastMCP
 
mcp = FastMCP("demo-tools")
 
@mcp.tool()
def calculator(expression: str) -> str:
    """Evaluate a simple arithmetic expression (e.g. '2 ** 10', '100 / 7')."""
    ...
 
@mcp.tool()
def text_stats(text: str) -> str:
    """Return word count, sentence count, and character count for the given text."""
    ...
 
@mcp.tool()
def weather_mock(city: str) -> str:
    """Return mock weather data for a city (demo only — not real data)."""
    ...
 
if __name__ == "__main__":
    mcp.run(transport="stdio")

Client side — connect and discover:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
 
server_params = StdioServerParameters(command="python", args=["tools_server.py"])
 
async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
 
        # Dynamic discovery — no tool names hardcoded in client code
        tools_result = await session.list_tools()

Measured list_tools() response:

Server: demo-tools
Discovered 3 tools:
  ● calculator       — Evaluate a simple arithmetic expression (e.g. '2 ** 10', '100 / 7').
  ● text_stats       — Return word count, sentence count, and character count for the given text.
  ● weather_mock     — Return mock weather data for a city (demo only — not real data).

Key point: the client code contains no mention of calculator, text_stats, or weather_mock. It only called list_tools() — the tool catalog was obtained dynamically from the Server.

Direct tool calls (no LLM):

calculator('2 ** 10 + 100 / 4')  → 2 ** 10 + 100 / 4 = 1049.0
weather_mock('Shanghai')          → {"city": "Shanghai", "temp": 22, "condition": "cloudy", "humidity": 75}
text_stats(...)                   → {"words": 9, "sentences": 3, "chars": 53}

Demo 3: LLM Agent Using MCP Tools

langchain-mcp-adapters automatically converts MCP tool schemas into LangChain Tool objects. The Agent code looks almost identical to Demo 1 — the difference is where the tools come from:

from langchain_mcp_adapters.client import MultiServerMCPClient
 
client = MultiServerMCPClient(
    {
        "demo-tools": {
            "command": "python",
            "args": ["tools_server.py"],
            "transport": "stdio",
        }
    }
)
mcp_tools = await client.get_tools()
# → ['calculator', 'text_stats', 'weather_mock']
# Tool definitions live in the Server, not in this file
 
agent = create_react_agent(model=llm, tools=mcp_tools)
result = await agent.ainvoke({"messages": [HumanMessage(multi_q)]})

Note: MCP tools are async. You must use await agent.ainvoke() — synchronous agent.invoke() throws NotImplementedError.

Three-question multi-tool test, real results:

Questions:
  1. What is sqrt(144) + 2 ** 8?
  2. What's the weather in Shenzhen?
  3. Count the stats for: 'MCP is a protocol. It standardizes tools. Agents love it!'
 
Answers:
  1. The result of sqrt(144) + 2 ** 8 is 260.
  2. (LLM called weather_mock, but added a "not real-time data" disclaimer
       because the tool description says "demo only")
  3. word count: 15, sentence count: 1, character count: 52

All three CallToolRequest events appeared in the Server log — the Agent routed all three questions through MCP.

The weather disclaimer is a useful teaching moment: the LLM reads description carefully. When the tool says "demo only — not real data", the LLM correctly qualifies its answer. Good tool descriptions make LLMs better decision-makers.


MCP vs Function Calling: Comparison Matrix

Dimension           Function Calling               MCP
──────────────────────────────────────────────────────────────────────
Tool definition     In Agent code                  In independent MCP Server
Tool discovery      Hardcoded import               list_tools() (dynamic)
Multi-agent reuse   Copy-paste or re-import        All Agents connect to same Server
Update tool logic   Edit every Agent file          Edit Server only
Cross-language      Same language only             Any language (JSON-RPC)
Invocation          In-process function call       Cross-process JSON-RPC
Startup overhead    None                           subprocess launch
Best fit            Single Agent / small projects  Multi-Agent / team collaboration

The MCP Ecosystem

Much of MCP's value comes from the growing catalog of existing servers:

Official MCP Servers (@modelcontextprotocol/):
  server-filesystem   — local file read/write
  server-github       — GitHub repo, issues, PR operations
  server-postgres     — PostgreSQL queries
  server-brave-search — web search (requires Brave API key)
 
Community maintained:
  Search "awesome-mcp-servers" for the full list
  Includes: Jira, Slack, Notion, Linear, Figma, and dozens more
 
Claude Code's tools are MCP:
  → file read/write, terminal commands, code search…
  → all run through this same protocol

MCP Server Development Checklist

Tool Design

  • Each tool does one thing with a clear input/output schema
  • Write description carefully — the LLM uses it to decide whether to call the tool
  • Dangerous operations (file write, command execution) need allowlists or confirmation

Server Implementation

  • FastMCP for rapid development; use low-level mcp.server.Server for fine-grained control
  • Tool functions return strings (MCP protocol's TextContent type)
  • Error handling: return descriptive error strings, don't let exceptions propagate to the protocol layer

Transport Selection

  • Local tools → stdio (subprocess, zero config)
  • Remote/shared tools → HTTP + SSE (needs auth and network config)
  • Production → consider connection pooling and timeout settings

Client Integration

  • Use langchain-mcp-adapters for LangChain/LangGraph integration
  • MCP tools are async: use await agent.ainvoke(), not agent.invoke()
  • Multiple servers: MultiServerMCPClient manages all connections centrally

Summary

Five core takeaways:

  1. MCP solves tool management, not just tool calling: Function Calling is per-agent tool binding; MCP is cross-agent tool services
  2. Dynamic discovery is the key capability: list_tools() lets an Agent find and use tools without knowing their names in advance
  3. Independent process enables cross-language tools: a JavaScript MCP Server is callable by a Python Agent — JSON-RPC is language-agnostic
  4. Claude Code's tools are MCP: every time you use Claude Code to read a file or run a command, that's this exact protocol in action
  5. Async is the MCP tool contract: MCP tools execute via async calls; LangChain integration requires ainvoke(), not invoke()

Up next: A2A Protocol and Agent Networks — how do Agents collaborate with each other? MCP handles Agent ↔ Tool. A2A handles Agent ↔ Agent.


References


Find more useful knowledge and interesting products on my Homepage