One Decorator vs. the Plumbing Tax: FastMCP, and What It Taught Me About Agent Tooling
Every few weeks a thread goes viral claiming you can turn a Python function into an AI tool with “one line of code.” Usually it’s hype. This time the hype is mostly earned — but not for the reason the thread says. Let me show you the real before/after, then something the thread misses entirely: how this compares to the way I already run agents.
The plumbing tax
Building an MCP server by hand means writing all of this before your actual logic:
- Transport (stdio, HTTP, SSE)
- Authentication
- JSON schemas for every tool
- Input validation
- Protocol lifecycle (initialize, list, call, shutdown)
- Error handling in the shape the protocol expects
None of that is your business logic. It’s the tax you pay to make a function callable by an LLM over a standard protocol.
The before
Here’s a lead-lookup function — the kind of thing I have all over projects like LotPay (a BHPH dealer platform). Raw, this is what “expose it as a tool” looks like conceptually:
# Hand-rolled: you own the schema, validation, dispatch, errors
TOOLS = {
"lookup_lead": {
"name": "lookup_lead",
"description": "Look up a lead by phone number",
"inputSchema": {
"type": "object",
"properties": {"phone": {"type": "string"}},
"required": ["phone"],
},
}
}
async def handle_call(name, arguments):
if name != "lookup_lead":
raise ProtocolError(f"unknown tool: {name}")
phone = arguments.get("phone")
if not isinstance(phone, str):
raise ValidationError("phone must be a string")
# ...and you still haven't written the query
return await db.leads.find_by_phone(phone)
Multiply that by every tool. Then wire up transport and auth.
The after
from fastmcp import FastMCP
mcp = FastMCP("Dealer Tools 🚗")
@mcp.tool
def lookup_lead(phone: str) -> dict:
"""Look up a lead by phone number."""
return db.leads.find_by_phone(phone)
if __name__ == "__main__":
mcp.run()
The type hints become the schema. The docstring becomes the description. Validation and the protocol lifecycle are handled. That’s the honest version of “one decorator” — and it’s genuinely a big deal for anyone shipping tools.
A correction worth making: the viral thread says FastMCP “powers a huge share of MCP servers.” The real, verifiable claim from the project is sharper — FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024, and some version of FastMCP powers ~70% of MCP servers across all languages. (The actively maintained project now lives under PrefectHQ, not the original author’s namespace.) Cite the number, not the vibe.
The part the thread skips: is the protocol always worth it?
Here’s what nobody selling you MCP will say out loud: if your tool only ever runs inside one process, MCP is overhead. A decorator is cheap, but the protocol — transport, handshake, serialization — isn’t free.
MCP earns its keep when you cross a boundary:
- Remote tools — the tool runs somewhere your agent isn’t.
- Cross-vendor composition — one agent, tools from five different teams/languages.
- Reuse across agents — write once, any MCP client can call it.
If none of those apply, calling the function directly is faster and simpler. MCP is connective tissue; you only need tissue where two things connect.
How this compares to how I actually run agents
I run agents on OpenClaw, which has its own tools-and-skills model — and putting it next to MCP is the most useful thing I can add to the conversation, because they solve overlapping problems differently.
| FastMCP tool | OpenClaw skill | |
|---|---|---|
| Unit | A typed Python function | A SKILL.md + scripts, loaded on demand |
| Interface | JSON-RPC over a transport | Instructions the agent reads, then acts |
| Discovery | Client lists tools at runtime | Agent scans skill descriptions, opens the relevant one |
| Best for | Deterministic calls across a boundary | Procedural know-how (“how to do X here”) |
| Overhead | Protocol handshake per connection | A file read |
They’re not competitors — they’re different altitudes. An MCP tool is a capability: call lookup_lead, get data. A skill is a procedure: “when the user wants a blog post, here’s the workflow, the repo, the frontmatter format.” One is a verb the agent invokes; the other is a playbook the agent follows — often to decide which verbs to invoke.
The interesting design question — the one I’m actually chewing on — is where the line sits. A skill can wrap MCP tools. An MCP server can expose something a skill would otherwise describe by hand. The best agent stacks will probably use both: MCP for capabilities that cross boundaries, skills for the judgment about when and how to use them.
Bottom line
FastMCP is worth the hype for the boring reason: it deletes the plumbing tax so you can ship the logic. Use it when you’re crossing a process, vendor, or agent boundary. Skip the protocol when you aren’t. And if you’re building agents seriously, don’t think “tools or skills” — think capabilities and the procedures that orchestrate them.
Python function → @mcp.tool → AI-ready capability. Just remember the capability is the easy part. Knowing when to reach for it is the real work.