I Deleted 1,633 Lines of Glue Code by Switching to MCP

Last month I dragged 1,633 lines of Python into the trash after migrating one live agent to MCP. Same six tools, same behavior, one config file instead of six hand-written modules. Here's the before/after file tree, the exact tradeoffs, and the decision rule I now give paying clients.
The setup, so the numbers actually mean something
We run an agent stack for a small ops team. One agent lives between Gmail and Telegram, triages inbound mail, pulls context from a Notion knowledge base, checks Google Calendar, drops attachments into Drive, and logs everything to a SQLite ledger. Six external tools. In production for months. Boring, reliable, and expensive to maintain.
Before the migration, the integration layer was 1,847 lines of Python across six modules, plus 214 lines of orchestration. Roughly 2,061 lines of surface area, every one of them mine to fix when a vendor shipped a breaking change.
Here's the module breakdown from the pre-migration branch:
| Module | Lines | Why it was fat |
|---|---|---|
gmail_client.py |
412 | OAuth, scopes, batch reads, MIME parsing |
drive_client.py |
402 | Resumable uploads, folder resolution, MIME map |
notion_client.py |
340 | Pagination + block model is a small novel |
sqlite_ledger.py |
274 | Migrations, schema guards, replay helpers |
calendar_client.py |
221 | Timezone math, recurrence, rate-limit retries |
telegram_bot.py |
198 | Long polling, media handling, retries |
orchestrator.py |
214 | Which tool to call when |
| Total | 2,061 | All mine to maintain |
In one quarter last year we ate three breaking changes. Gmail deprecated a scope. Notion renamed a block property. Google Calendar tightened a rate limit and started returning a new error code our retry logic didn't recognize. Half a day to a full day each. Nobody paid us extra to fix any of it. That's the tax nobody puts in the ROI slides.
The after: one config file, 214 lines of orchestration
Same agent. Same six tools. Same behavior from the user's perspective. The integration layer is now one file, mcp_config.json, about forty lines, six blocks, one per server. The orchestrator is unchanged in shape at 214 lines: it opens an MCP client, lists the available tools, and lets the model call them by name.
{
"mcpServers": {
"gmail": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-gmail"],
"env": { "GMAIL_CREDS_PATH": "/srv/secrets/gmail.json" }
},
"telegram": {
"command": "uvx",
"args": ["mcp-server-telegram"],
"env": { "TELEGRAM_BOT_TOKEN": "${TG_TOKEN}" }
},
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/mcp-server", "--version=0.6.2"],
"env": { "NOTION_TOKEN": "${NOTION_TOKEN}" }
},
"gcal": { "command": "uvx", "args": ["mcp-server-gcal"] },
"gdrive": { "command": "uvx", "args": ["mcp-server-gdrive"] },
"sqlite": {
"command": "uvx",
"args": ["mcp-server-sqlite", "--db", "/srv/data/ledger.db"]
}
}
}
The 1,847 lines of hand-written glue are gone. Deleted. In git. The git diff on the migration PR:
integrations/gmail_client.py | 412 -----
integrations/drive_client.py | 402 -----
integrations/notion_client.py | 340 -----
integrations/sqlite_ledger.py | 274 -----
integrations/calendar_client.py | 221 -----
integrations/telegram_bot.py | 198 -----
mcp_config.json | 40 +++++
--------------------------------------------
7 files changed, 40 insertions(+), 1847 deletions(-)
Net: -1,807 lines. The 1,633 headline number is what stayed deleted after I added back a small local patch (see below).
Why the deletion happens
An MCP server ships three things you used to hand-write. Once you internalize this, the code shrinkage isn't mysterious, it's mechanical.
- Tool schema. The server advertises tool names, argument types, and descriptions over the protocol. The model reads that directly. You no longer maintain a JSON-schema blob per function, and you don't rewrite it when the vendor adds a parameter.
- Auth. OAuth flows, token refresh, scope negotiation, and secret rotation live inside the server. My old
gmail_client.pyhad 140 lines just for auth. That entire section is gone. - Retry and error semantics. When Gmail returns a 429, the server backs off. When Notion hands back a transient 502, the server retries. My orchestrator sees a clean success or a clean, typed error.
Every one of those was code I owned. Now it's code somebody else owns, and if they break it, I swap servers instead of rewriting a module.
A quick side-by-side for one call — reading the last 20 unread emails:
# Before: 34 lines of Gmail API + pagination + parsing
def fetch_unread(limit=20):
creds = load_creds()
if creds.expired: creds.refresh(Request())
service = build("gmail", "v1", credentials=creds)
resp = service.users().messages().list(
userId="me", q="is:unread", maxResults=limit
).execute()
msgs = []
for m in resp.get("messages", []):
full = service.users().messages().get(
userId="me", id=m["id"], format="full"
).execute()
msgs.append(parse_message(full)) # 60 more lines elsewhere
return msgs
# After: the model calls it by name via the MCP client
result = await mcp.call_tool("gmail.list_unread", {"limit": 20})
The honest costs I don't see in the marketing
I want to be blunt about what this trade actually costs, because most MCP takes aren't.
Latency. Every tool call now hops through a local MCP server process. In my setup that adds roughly 600 ms per call (measured over 2,000 calls, p50 612 ms, p95 840 ms of added overhead). For a background triage agent, nobody notices. For an interactive chat UI, users notice. If your agent is user-facing and latency-sensitive, benchmark before you migrate.
You inherit somebody else's bugs. The Notion MCP server I was using had a broken handler for nested databases. It returned an empty list instead of throwing. My agent silently missed context for two days before I caught it in the logs. I patched the server locally, pinned that version, and filed the upstream issue. That patch is roughly 174 lines and it's the reason my headline deletion is 1,633, not 1,807. Maintenance didn't go to zero — it went from six modules I own to one patch I own, plus five servers I trust.
The ecosystem is young. Some vendors ship first-party MCP servers. Most don't. For roughly a third of the tools a typical small business uses — niche CRMs, industry SaaS, older accounting software — you're either picking a community server and reading its source, writing one yourself, or waiting. The official Model Context Protocol spec is stable, but the server catalog is not evenly stocked.
Quick self-check before you migrate
- Is your agent interactive or background? Interactive + <1s SLA = benchmark first.
- Does an official server exist for each tool? If not, are you willing to read/patch community code?
- Can you pin server versions in your config and control upgrades?
- Do you have log-level observability on tool outputs? (You'll need it to catch silent-empty bugs.)
The decision rule I give clients now
Count your integrations. That's the whole rule.
| Integrations | Recommendation |
|---|---|
| 1–2 | Don't migrate. The raw API code is small, you already own it, MCP tax isn't worth paying. |
| 3–5 | Migrate the tools with reliable official servers first. Keep raw API as fallback for one release cycle. |
| 6+ | Migrate. The math flips hard. Every future vendor break stops being your problem. |
On our stack the payback landed inside the first month. Two vendor changes hit in the four weeks after migration. Before, that would have been a day each. After: one was fixed by the server maintainer inside 48 hours and I did literally nothing, and the other was a version bump in mcp_config.json. Total time spent: about eleven minutes.
That's the number that matters for a small team — not lines of code, but weekends you get back.
The migration playbook, in order
If you decide to move, here's the exact order I use on client rebuilds. Skipping a step is where teams get burned.
- Inventory. List every external tool the agent touches. Count lines of glue per tool. This is your deletion budget.
- Server survey. For each tool, check if there's an official MCP server, then a well-maintained community one. If neither, mark it as "keep raw API for now."
- Benchmark latency on critical paths. Measure p50/p95 before you touch anything. Re-measure after each server is swapped in. My rule: if p95 on a user-facing path crosses 2 s, that server stays out.
- Migrate one server at a time behind a feature flag. Keep the raw API module in the repo for one release cycle. Don't delete until you've seen a week of clean logs.
- Pin every server version. Not
latest. Not a floating tag. A specific version. Upgrades are your decision, not the maintainer's. - Log every tool call — args in, result out. This is how you catch silent-empty bugs like the Notion one. Costs you nothing, saves you a day.
- Delete the old glue. Only after the flag has been on for a full billing cycle with no incidents.
Where bizflowai.io fits
This migration pattern is exactly what we run for clients on bizflowai.io — agents that live across Gmail, calendars, CRMs, and internal ledgers, kept boring and cheap to maintain. The audit we do before any rebuild is the integration count and the vendor-breakage tax; if a client has three or more tools and eats vendor changes quarterly, the MCP migration usually pays back inside a month. We keep raw API fallbacks on the tools where community servers aren't trustworthy yet, and we pin versions on everything.
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 MCP (Model Context Protocol) and what does it replace?
MCP is a protocol where each external tool runs as a local server that ships three things you used to hand-write: the tool schema so models know function arguments, auth handling including OAuth flows and token refresh, and retry and error semantics like backing off on 429s. It replaces custom integration glue code with a config file and a small MCP client.
How much code can migrating to MCP actually eliminate?
In one production agent connecting to Gmail, Telegram, Notion, Calendar, Drive, and SQLite, migrating to MCP reduced the integration layer from 1,847 lines of Python across six modules to a single 40-line mcp_config.json file with six blocks. Orchestration stayed at 214 lines. Total maintained surface area dropped from roughly 2,061 lines to about 254.
What are the hidden costs of using MCP servers?
Three main costs: latency adds roughly 600 milliseconds per tool call because calls route through a local MCP server process; you inherit the server author's bugs, like a Notion MCP server that silently returned empty lists for nested databases; and the ecosystem is young, so for about a third of common tools you'll have no official server and must vet or write community ones.
When should I migrate an agent to MCP versus keep raw API code?
If your agent talks to only one or two external tools, don't migrate. The raw API code is small, you already own it, and MCP's latency and trust costs aren't worth it. If it talks to three or more tools, migrate. You delete more code than you write, and vendor breaking changes stop being your problem to fix.
Why do vendor breaking changes matter for agent maintenance costs?
Vendor breaking changes are unpaid maintenance tax rarely included in ROI calculations. In one quarter, a six-tool integration absorbed three breaks: Gmail deprecated a scope, Notion renamed a block property, and Google Calendar tightened a rate limit with a new error code. Each cost a half to full day of unpaid work. With MCP, these breaks live inside the server and you swap servers instead of rewriting modules.