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
descriptioncarefully — 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.Serverfor 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-adaptersfor LangChain/LangGraph integration - [ ] MCP tools are async: use
await agent.ainvoke(), notagent.invoke() - [ ] Multiple servers:
MultiServerMCPClientmanages all connections centrally
Summary
Five core takeaways:
- MCP solves tool management, not just tool calling: Function Calling is per-agent tool binding; MCP is cross-agent tool services
-
Dynamic discovery is the key capability:
list_tools()lets an Agent find and use tools without knowing their names in advance - Independent process enables cross-language tools: a JavaScript MCP Server is callable by a Python Agent — JSON-RPC is language-agnostic
- 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
-
Async is the MCP tool contract: MCP tools execute via async calls; LangChain integration requires
ainvoke(), notinvoke()
Up next: A2A Protocol and Agent Networks — how do Agents collaborate with each other? MCP handles Agent ↔ Tool. A2A handles Agent ↔ Agent.
References
- Model Context Protocol Official Site
- MCP Python SDK
- langchain-mcp-adapters
- Full demo code for this series: agent-09-mcp
Find more useful knowledge and interesting products on my Homepage
Top comments (0)