There is a point in almost every agent project where the excitement starts to fade.
At first, it feels magical. You wire an LLM to a few Python functions, wrap them as tools, and suddenly your assistant can calculate, search, transform, and automate. But then the project grows. A few tools become ten. Ten become thirty. Business logic starts mixing with agent logic. Logging becomes messy. Reuse becomes painful. One agent needs the same tools as another, so you copy code. Then you copy it again. And somewhere in that process, your âsmart systemâ quietly turns into a pile of tightly coupled Python.
That is exactly where MCP starts to make sense.
Model Context Protocol (MCP) is an open standard for exposing tools, resources, and prompts to LLM applications in a structured way. The official docs describe it as a standardized way for AI apps to connect to external systems, and even compare it to a âUSB-C port for AI applications.â (Model Context Protocol)
And once that clicks, a very important design shift becomes obvious:
Your tools do not have to live inside your agent code anymore.
You can build them once, run them as an MCP server, and let agents consume them cleanly from the outside. (LangChain Docs)
That is the idea this post is about.
Iâll show you how I built a tiny MCP server with math and dice tools, added logging to observe tool calls, and then plugged that server into a LangChain agent exposed via FastAPI. Along the way, the architecture changed from âmy agent has toolsâ to something much cleaner:
my tools live in their own server, and my agent just uses them.
Why this matters more than it looks
Imagine a normal backend team.
One person owns business logic. Another owns APIs. Another owns observability. Now imagine if every API consumer copied that business logic into their own codebase. That would quickly turn into chaos.
Thatâs essentially what happens when we embed tools directly inside agent code.
The issue isnât that it breaks, itâs that it doesnât scale.
With MCP, things get cleaner:
- the agent focuses on reasoning
- the tool server handles execution
- logging and latency stay observable
- tools can be reused across agents
- you can evolve tools without touching the agent
That separation is exactly what MCP brings to the table, and libraries like langchain-mcp-adapters make this integration seamless(LangChain Docs).
So what is MCP, in plain English?
Letâs strip away the buzzwords.
When an LLM needs to do something real, like querying a database, calling an API, or running a workflow, we usually define those as tools inside the agent code.
MCP changes that idea:
What if tools were exposed through a standard protocol instead?
Now, the same tool server can be used by multiple agents, clients, or even frameworks(FASTMCP).
A simple way to think about it:
- MCP server â where tools live
- MCP client â connects to the server
- Agent â decides when to use tools
It sounds like a small shift, but itâs the difference between a quick demo and a system you can actually scale.
Building a tiny MCP server (and making it observable)
To understand MCP, I didnât start with anything complex.
I built a small server with just two kinds of tools:
- basic math operations
- a dice roll
Instead of pasting the full code here, you can check the clean version directly:
Simple MCP server: server_simple.py
At its core, it looks like this:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("math-server")
@mcp.tool()
def add(a: float, b: float) -> float:
return a + b
Thatâs all it takes to expose a function as a tool.
Now hereâs the important part:
- These tools are not inside your agent
- They are running in a separate server
Running the MCP server
You can start the server using:
fastmcp run server.py --transport http --port 9090
This exposes your tools over HTTP at:
http://127.0.0.1:9090/mcp
Now any agent (or client) can connect to it.
The moment things start to feel real
At this point, everything worked.
But something was missing.
I could call toolsâŚ
but I couldnât see what was happening inside them.
And thatâs when logging becomes non-negotiable.
Adding logging (turning it into a real service)
Instead of rewriting everything, I added a simple decorator to log:
- when a tool starts
- what inputs it received
- what it returned
- how long it took
You can check the full version here:
Logged MCP server: server.py
The core idea looks like this:
def log_tool(func):
def wrapper(*args, **kwargs):
print(f"START {func.__name__} {kwargs}")
result = func(*args, **kwargs)
print(f"END {func.__name__} -> {result}")
return result
return wrapper
And then:
@mcp.tool()
@log_tool
def roll_dice(faces: int = 6) -> int:
...
What this gives you
Now when your agent calls a tool, you donât just get a result,
you get visibility:
TOOL START | roll_dice | faces=20
TOOL END | roll_dice | result=15
And this is where MCP starts to click.
Your âtool layerâ is no longer hidden inside your agent.
Itâs running as a separate, observable service.
Why this step matters
This small setup already gives you:
- tools defined independently
- a server that can be reused
- logging and latency visibility
- a clean boundary between reasoning and execution
And we havenât even touched the agent yet.
Thatâs where things get interesting next.
Now! Can I plug this server into an agent?
And the answer is yes.
LangChain now provides an MCP adapter library, langchain-mcp-adapters, which lets agents consume tools directly from MCP servers. Its MultiServerMCPClient can connect to one or more MCP servers, and by default it is stateless: each tool invocation opens a fresh MCP session, executes, and cleans up. (LangChain Docs)
That stateless behavior turned out to match my use case nicely.
The agent side: FastAPI + LangChain + MCP
Now comes the satisfying part.
Instead of embedding tools inside the agent, I made the agent connect to the MCP server over HTTP.
You can check the full working code here:
Agent (FastAPI + MCP): agent.py
What the agent really does
At a high level, the agent is surprisingly simple.
It:
- connects to the MCP server
- fetches available tools
- initializes an LLM
- lets the agent use those tools when needed
The key piece looks like this:
client = MultiServerMCPClient({
"math": {
"transport": "http",
"url": "http://127.0.0.1:9090/mcp",
}
})
tools = await client.get_tools()
Thatâs it.
MCP tools â automatically become usable by the agent
Adding the agent on top
Then we plug those tools into a LangChain agent:
agent = create_agent(
model=llm,
tools=tools,
system_prompt="You are a helpful assistant that can use tools."
)
And expose it via FastAPI:
@app.post("/chat")
async def chat(request: ChatRequest):
result = await agent.ainvoke({
"messages": [
{"role": "user", "content": request.query}
]
})
return {"response": result["messages"][-1].content}
And hereâs the shift
Notice whatâs missing.
There is no math logic here.
No add, no divide, no roll_dice.
The FastAPI app simply says:
- here is my LLM
- here is my MCP client
- give me the tools
- let the agent use them
What the full flow looks like
Letâs say the user sends:
Roll a 20 sided dice and then add 5 to it
The request travels like this:
- user hits the FastAPI
/chatendpoint - the agent receives the query
- the agent decides it needs a tool
- LangChain calls the MCP adapter
- the MCP adapter calls the MCP server over HTTP
-
roll_dice(faces=20)executes - result comes back
- the agent uses that numeric output in the next tool call
-
add(a=15, b=5)executes - final answer is returned to the user
And because the server has logging, you can actually watch this happen.
Here is the kind of trace I saw:
2026-04-08 12:34:34,822 | INFO | [7cb8650b] TOOL START | roll_dice | args=() kwargs={'faces': 20}
2026-04-08 12:34:34,822 | INFO | [7cb8650b] TOOL END | roll_dice | result=15 | 0.03ms
2026-04-08 12:34:36,231 | INFO | [149d5f59] TOOL START | add | args=() kwargs={'a': 15.0, 'b': 5.0}
2026-04-08 12:34:36,231 | INFO | [149d5f59] TOOL END | add | result=20.0 | 0.03ms
This is one of those moments where the architecture suddenly feels real.
You are not just âcalling functions from an LLM.â
You are watching an agent orchestrate a tool server.
The part that stayed with me
What stood out wasnât the dice roll or the API.
It was the separation.
- the MCP server owns the tools
- the agent handles reasoning
- the API manages interaction
Each piece has a clear role and can evolve independently.
MCP challenges the habit of packing everything into one place and instead gives you a cleaner way to separate intelligence from execution.
And once you see that, itâs hard not to think:
Why was all of this in one file to begin with?
Whatâs next
This is just the starting point.
In upcoming blogs, Iâll go deeper into:
- making MCP tools more robust
- improving reliability and performance
- adding security and access control
- and turning this into something closer to production-ready
Because building tools is one thing â
building safe, scalable, and reliable tool systems is where things get really interesting.

Top comments (0)