DEV Community

Cover image for Agent Series (10): MCP Protocol — Standardizing the Tool Ecosystem
WonderLab
WonderLab

Posted on

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

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, ...])
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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%.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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).
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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)]})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)