The 2-Agent Rule: When MCP Beats Raw APIs (and When It

I run 12 tools across 4 AI agents on a home server. Four are wrapped in MCP. The other eight are raw Python calls behind Pydantic models, and they're staying that way. If you're rewriting your agent stack on MCP because a keynote told you to, you're probably about to make every agent slower for zero architectural benefit.
Most tutorials sell MCP as the new default. Nobody publishes the threshold where it actually pays off. Here's the exact rule I use before I wrap anything.
The real problem the MCP hype is hiding
A small ops team wires up an agent that reads Gmail, writes to a database, pings Telegram, checks a calendar. It works. Then they watch a Google or Anthropic keynote, hear MCP is the future, and burn two weekends rewriting every tool as an MCP server. The agent still works. It's just slower, harder to debug, and there are now three config files per tool that nobody remembers writing.
That's the pain I see on client calls week after week. People are paying a tax to sound modern. The Model Context Protocol is a good piece of engineering — Anthropic's MCP spec is worth reading — but it was designed to solve a distribution problem, not to be the default transport for every function your agent calls. Once you separate "protocol" from "packaging format," the decision gets simple.
The 2-agent rule, stated plainly
If exactly one agent will ever call a tool, keep it as a typed Python function. If two or more agents will call it, wrap it in MCP. That's the whole rule. It works because MCP's real benefits — shared auth, one integration surface, provider portability — only compound when multiple consumers exist. With a single consumer, you get the cost and none of the payoff.
Here's what "typed Python function" means in practice — no framework, no server, just an async function with a schema:
from pydantic import BaseModel
import httpx
class WeeklyReportInput(BaseModel):
channel_id: str
week_start: str # ISO date
class WeeklyReportOutput(BaseModel):
posted: bool
message_ts: str
async def post_weekly_report(inp: WeeklyReportInput) -> WeeklyReportOutput:
payload = build_report(inp.week_start)
async with httpx.AsyncClient(timeout=10) as client:
r = await client.post(SLACK_WEBHOOK, json={
"channel": inp.channel_id,
"blocks": payload,
})
r.raise_for_status()
return WeeklyReportOutput(posted=True, message_ts=r.headers["x-slack-ts"])
That's 15 lines. Wrapping it as an MCP server would add a server definition, a manifest, an auth config, and roughly 500–800 ms of transport overhead per call. For a tool one agent uses on a cron trigger, that math never pays back.
The actual deployment: 12 tools, 4 agents, 4 MCP servers
Here's the shape of the system running on the home box right now:
| Agent | Purpose | Tools it calls |
|---|---|---|
| triage-01 | Inbox triage | gmail_read (MCP), label_mutate (raw), db_upsert (MCP) |
| kb-01 | Notion-style KB | db_upsert (MCP), db_query (raw), embed_upsert (raw) |
| outbound-01 | Telegram sender | telegram_send (MCP), db_upsert (MCP), gmail_read (MCP), quiet_hours (raw) |
| sched-01 | Calendar + booking | calendar_query (MCP), calendar_write (raw), telegram_send (MCP), gmail_read (MCP), report_slack (raw) |
Four tools have 2+ consumers: gmail_read (3), db_upsert (3), telegram_send (2), calendar_query (2). Those earned MCP. The other eight — label_mutate, db_query, embed_upsert, quiet_hours, calendar_write, report_slack, and two more agent-specific webhook posters — each have exactly one caller. Raw Python, Pydantic in and out, done.
If I had shipped all 12 as MCP servers the way the keynotes suggest:
- Latency: ~600 ms average added per single-consumer call. On an agent averaging 4 tool calls per user turn, that's ~2.4 seconds of user-perceived lag added for nothing.
- Files: Each MCP tool ships a server file, a manifest, and an auth config. 8 unnecessary tools × 3 files = 24 files no one has a reason to open again.
- Debug surface: Every raw function is a stack trace away. Every MCP call is a stack trace, a transport log, a schema handshake, and a server process to check.
The four-question checklist I run before writing any code
Before I touch a keyboard on a new tool, I answer these in order. First "stop" wins.
The checklist
- Reuse count. How many agents in this system need this exact capability? If 1 → stop, use a typed Python function. If 2+ → keep going.
- Latency budget. Is this tool in a user-facing loop where 500 ms is felt, or a background job? User-facing + low reuse → lean raw. Background → MCP overhead is invisible.
- Auth complexity. Does it need shared credential handling across processes (OAuth token refresh, rotating keys, per-tenant scopes)? MCP handles that cleanly. Trivial auth → raw is fine.
- Schema stability. Is the upstream API stable, or does the vendor churn fields every quarter? Unstable → the MCP layer means you patch one server, not five call sites. Stable → the abstraction earns nothing.
Applied to gmail_read: three agents need it, OAuth is annoying to duplicate, Gmail API is stable. Reuse yes, auth yes, stability yes. Clean MCP win.
Applied to report_slack: one agent, plain webhook URL, Slack Block Kit is stable. Reuse no. Stop at question one. Raw Python.
Where MCP genuinely earns its keep
I want to be fair to the protocol, because on the right problems it's the correct answer. Three real wins I've seen on client work:
- Cross-agent tool sharing. Three agents that all need Gmail? One MCP server, one OAuth flow, one place to fix a bug. This is where the 2-agent rule was born — the moment consumer #2 shows up, MCP starts paying back.
- Runtime tool discovery. If you want agents to pick up new tools from a registry without a redeploy, MCP is designed for that. Rolling your own dynamic registration on top of raw Python is where you actually build a bad version of MCP.
- Provider portability. Because tools sit behind a standard interface, swapping Claude for GPT for Gemini doesn't force you to rewrite every integration. If you rotate model providers even once a year, that's real leverage.
None of those benefits apply to a tool with one consumer, a stable API, and trivial auth. Below the 2-agent threshold, MCP is a distributed-systems abstraction applied to a monolithic problem.
The measurable payoff of doing this correctly
Numbers from the current stack, measured over a week of real traffic (not synthetic benchmarks):
| Metric | All-MCP (projected) | 2-agent rule (actual) | Delta |
|---|---|---|---|
| Avg tool-call latency (raw path) | ~750 ms | ~140 ms | −610 ms |
| Config files across repo | 36 | 12 | −24 |
| Long-running server processes | 12 | 4 | −8 |
| RAM at idle (home server) | ~1.9 GB | ~640 MB | −1.3 GB |
| p95 agent turn (user-facing) | 5.8 s | 3.1 s | −2.7 s |
The RAM number matters if you're running this on a home box or a $12/month VPS instead of a Kubernetes cluster. Eight fewer Python processes sitting idle at 150 MB each is the difference between "runs fine on my mini PC" and "needs a bigger server."
And the debugging story is the one nobody talks about. When a raw Python tool breaks, I get a stack trace pointing at the exact line. When an MCP tool breaks, I get a JSON-RPC error, then I check the server logs, then I check the transport, then I find the line. That's fine when 3 agents depend on the tool. It's absurd overhead when 1 agent does.
When to break the rule (deliberately)
Two cases where I wrap a single-consumer tool in MCP anyway:
- I know a second consumer is coming within the sprint. Not "someday" — actually scheduled. Wrapping late costs more than wrapping once.
- The tool is going public. If a client wants to expose a tool for their own downstream automation or a partner integration, MCP is the right packaging format regardless of internal consumer count. That's the protocol doing what it was designed for.
Everything else stays raw until a second agent asks for it. Promotion from raw Python to MCP takes about 45 minutes when the function already has Pydantic models — because it does, by default, from day one.
Where bizflowai.io fits
Most of the client systems shipped through bizflowai.io look like the 12-tool, 4-agent shape above: Gmail triage, CRM upserts, calendar coordination, outbound messaging, custom reporting. The 2-agent rule is what keeps those systems responsive on modest infrastructure and cheap to maintain a year in. When a client's second use case for the same capability appears, that tool gets promoted to MCP with a scheduled migration — not before.
Want more like this?
I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.
Subscribe to bizflowai.io on YouTube — never miss a new tutorial.
Planning an AI automation project or need a second opinion on your architecture?
Connect with me on LinkedIn — Lazar Milicevic, GenAI Engineer & bizflowai.io Founder.
Visit bizflowai.io for our services, case studies, and AI consulting.
Frequently asked questions
What is the 2-agent rule for MCP?
The 2-agent rule is a decision heuristic: only wrap a tool as an MCP server if two or more agents will call it. If just one agent uses the tool, keep it as a raw async Python function with Pydantic input and output models. This prevents paying MCP's latency and config overhead for tools that gain no reuse benefit.
How do I decide whether to wrap a tool in MCP or keep it as a raw Python function?
Run a four-question checklist: (1) Reuse count—do two or more agents need it? (2) Latency budget—is it in a user-facing loop where 500ms matters? (3) Auth complexity—does it need shared credentials across processes? (4) Schema stability—does the vendor change fields often? High reuse, shared auth, or unstable schemas favor MCP. Otherwise, use a raw typed Python function.
Why does wrapping every tool in MCP hurt performance?
Each MCP call adds roughly 500–800 milliseconds per turn from the transport, schema handshake, and server round trip. On an agent averaging 4 tool calls per interaction, that's 2–3 seconds of extra latency users feel. MCP also ships three extra files per tool (server definition, manifest, auth config), creating maintenance overhead with no architectural benefit when only one agent uses the tool.
When should I use MCP versus a raw API call?
Use MCP when a tool is shared by two or more agents, requires shared credential handling like OAuth, or wraps an unstable API where updating one server beats fixing five call sites. Use a raw API call with a typed Python wrapper when only one agent needs the tool, auth is trivial, the schema is stable, or latency matters in a user-facing loop.
What does a real MCP deployment look like in practice?
In one 4-agent, 12-tool deployment, only 4 tools were wrapped in MCP: Gmail read, database upsert, Telegram send, and calendar query—each used by three or more agents. The remaining 8 tools, like a specific label mutation or single-channel Slack report, were used by exactly one agent and stayed as raw async Python functions with Pydantic models.