MCP Deep Dive, Part 1: Why Model Context Protocol Kills Integration Glue Code for Good
Every AI agent you build needs every backend you own — that is N×M glue code. MCP turns it into N+M. Here is why, with real Mattrx production numbers.
- Author
- Randhir Jassal
- Published
- Reading time
- 15 min read
- Views
- 7 views
Your AI roadmap does not die from a bad model. It dies from integration glue code — the hand-written adapter that wires agent number four to backend number nine, times every agent and every backend you will ever build. Model Context Protocol is the thing that stops that multiplication. This series is how we adopted it on a real production system.
This is Part 1 of a 15-part deep dive on Model Context Protocol (MCP). We will go from "why" all the way to running MCP servers in production — architecture, servers, clients, custom tools, auth, authorization, security, streaming, debugging, enterprise rollout, and the .NET / Azure / OpenAI integrations. Every part uses the same running example: Mattrx, our multi-tenant marketing-analytics SaaS, and every metric in here is from that real system.
Part 1 answers the only question that matters before you write a line of MCP code: why bother? The honest answer is not "because it is new." It is because the alternative — bespoke integrations — scales quadratically, and quadratic is the enemy.
TL;DR
| Dimension | Before (bespoke glue) | After (MCP) |
|---|---|---|
| Integration model | N agents × M backends | N agents + M servers |
| Mattrx integrations | 14 point-to-point clients | 3 MCP servers |
| Adding a capability | New adapter on both sides | Declare one MCP tool |
| Tool discovery | Hardcoded per agent | Discovered at runtime |
| Auth & audit | Reinvented per integration | One OAuth/Entra boundary |
| External AI access | Unsafe / not possible | Scoped, governed, audited |
- 14 bespoke integrations collapsed to 3 MCP servers (
mattrx-analytics,mattrx-reports,mattrx-admin). - ~9,000 lines of glue code deleted — roughly a 40% cut in integration code.
- New-capability onboarding dropped from ~3 days to ~2 hours.
- Agent tool-call error rate fell from 6% to 0.8% once every tool spoke one protocol.
- ~85,000 MCP tool calls/day across tenants, all governed by the same boundary.
- Read-tool p95 120 ms; report-enqueue p95 90 ms.
- ~40 tool-abuse / injection attempts per week blocked at the MCP boundary.
- Approved external AI assistants now have a scoped, audited path in — previously impossible.
- Every tool call is tenant-bound in code and append-only audited (eval gate stays at 0.90).
- One identity boundary (OAuth 2.1 via Entra ID) replaced N bespoke auth flows.
The one mental shift: stop building integrations and start publishing capabilities. An integration teaches one agent how to call one backend. A capability is a tool any agent can discover and call from its schema alone. MCP is the protocol that makes capabilities additive instead of multiplicative.
The running example: Mattrx, and the glue-code tax
Mattrx is a real system. Angular 19 front end, .NET 9 / ASP.NET Core back end (Clean Architecture + CQRS with MediatR), Azure SQL, Azure App Service. Campaigns ~4M rows, Events ~180M, CampaignEvents ~1.2B. Ingestion on Confluent Kafka; report commands on Azure Service Bus; Event Grid for reactive glue. Two AI products: Mattrx Help (RAG support) and Mattrx Insights (an agentic analyst).
A year ago, every one of those AI features reached its data the same way: a hand-written client per backend. Insights had a campaigns client, an events client, a KPI client, and a reporting client. Help had its own campaigns client and its own KPI client — subtly different from Insights'. When we prototyped a third agent, we started copying clients again.
That is the N×M problem. With N agents and M backends, you write up to N×M integrations, and each one re-implements auth, retries, error mapping, and logging in its own slightly-wrong way. Mattrx had 14 of these. The fourteenth is the one that finally made us stop and adopt MCP.
BEFORE — N agents x M backends = up to N*M bespoke integrations
Insights ---+--> Campaigns API (custom client)
+--> Events API (custom client)
+--> KPI API (custom client)
+--> Reporting API (custom client)
Help -------+--> Campaigns API (a DIFFERENT custom client)
+--> KPI API (a DIFFERENT custom client)
External AI ....> (no safe path at all — would need even more clients)
AFTER — N agents + M servers = N+M, one protocol
Insights ---+
Help -------+ +--> mattrx-analytics (campaigns, events, kpis)
External AI +------ MCP -------+--> mattrx-reports (create_report, status)
(approved) -+ +--> mattrx-admin (flags, exports; locked)
MCP flips the multiplication into addition: every agent learns one protocol, every backend speaks it once, and a new agent gets all existing tools for free. Let's walk the four places that tax showed up, with the before and the after.
1. The integration explosion
Before
Every agent embedded a bespoke client for every backend it touched. The agent had to know each client's method names, auth scheme, and error shapes.
// BEFORE: the agent is welded to four hand-written clients.
public sealed class InsightsAgent(
CampaignsApiClient campaigns, // bespoke HTTP client #1
EventsApiClient events, // bespoke HTTP client #2
KpiApiClient kpis, // bespoke HTTP client #3
ReportingApiClient reporting) // bespoke HTTP client #4
{
public async Task<AgentAnswer> AnswerAsync(string goal, CancellationToken ct)
{
// Each client has its own auth handling, retry policy, and error model.
var campaign = await campaigns.GetAsync(campaignId: "4821", ct);
var series = await events.QueryAsync(campaignId: "4821", window: "7d", ct);
var kpi = await kpis.GetAsync(campaignId: "4821", metric: "ctr", ct);
var reportId = await reporting.CreateAsync(new ReportSpec(/* ... */), ct);
// ...and the next agent we build re-implements a slice of all four.
}
}
Diagnostic: the agent's constructor is the problem. Four dependencies, four auth flows, four error models — and the next agent duplicates whichever subset it needs. Multiply across the team and you get fourteen integrations that drift apart over time.
After
Each capability is declared once as an MCP tool on a server. The agent never sees a bespoke client again.
// AFTER: a capability declared once on the mattrx-analytics MCP server.
[McpServerToolType]
public sealed class AnalyticsTools(ICampaignQueries campaigns, AiPrincipal principal)
{
[McpServerTool(Name = "get_campaign_kpis")]
[Description("Return the KPI time-series for a campaign in the caller's tenant.")]
public async Task<CampaignKpis> GetCampaignKpis(
[Description("Campaign id within the caller's tenant")] string campaignId,
[Description("ISO-8601 range, e.g. 2026-06-01/2026-06-28")] string range,
CancellationToken ct)
{
// Tenant comes from the authenticated principal — never from the arguments.
return await campaigns.GetKpisAsync(principal.TenantId, campaignId, range, ct);
}
}
The agent side collapses to one client that speaks MCP to every server:
// AFTER: the agent calls any tool through one MCP client. No bespoke adapters.
var result = await mcp.CallToolAsync(
"get_campaign_kpis",
new { campaignId = "4821", range = "2026-06-01/2026-06-28" },
ct);
Mattrx metric: the 14 bespoke integrations collapsed into 3 MCP servers. We deleted roughly 9,000 lines of client/glue code — about a 40% cut in integration code — and the diff was almost all deletions.
2. Capability discovery
Before
Each agent shipped with a hardcoded list of what it could do. Add a backend method, and you redeployed every agent that wanted it.
// BEFORE: the toolset is a constant the agent is compiled with.
private static readonly ToolDescriptor[] Tools =
{
new("getCampaign", typeof(CampaignsApiClient)),
new("queryEvents", typeof(EventsApiClient)),
new("getKpi", typeof(KpiApiClient)),
new("createReport", typeof(ReportingApiClient)),
};
Diagnostic: the list and the reality drift. A backend ships a new method; agents don't know it exists until someone updates a constant and redeploys. Capability is static, but your product is not.
After
The server advertises its tools — names, descriptions, and JSON schemas — and the client discovers them at runtime via tools/list.
// AFTER: the agent asks the server what it can do, every session.
var tools = await mcp.ListToolsAsync(ct);
foreach (var tool in tools)
{
// name, description, and a JSON Schema for arguments — enough for an
// LLM to decide when and how to call it, with zero hardcoding.
logger.ToolDiscovered(tool.Name, tool.Description);
}
Diagnostic: discovery is the quiet superpower. Because tool definitions are data, an LLM can read them and choose a tool it was never explicitly told about. Ship a new tool on the server, and every agent can use it on its next session — no redeploy.
Mattrx metric: shipping a new capability went from ~3 days (write the backend method, write a client on both sides, redeploy agents) to ~2 hours (declare one MCP tool; agents discover it automatically).
3. One auth and audit boundary
Before
Every bespoke client reinvented authentication. One used a static API key from config, another fetched a token with subtly different scopes, a third forgot to propagate the tenant at all. Audit was wherever each developer remembered to log.
// BEFORE: three integrations, three different ideas of "secure".
campaigns.UseApiKey(config["Campaigns:ApiKey"]); // static key
events.UseBearer(await tokenService.GetAsync("events.read")); // OAuth, one scope
kpis.UseBearer(await tokenService.GetAsync("kpi")); // OAuth, different scope name
// reporting client: tenant passed as a string argument (trusted!) — the bug we shipped.
Diagnostic: N integrations means N chances to get auth wrong, and we took several of them. The reporting client trusted a tenant id passed in an argument — exactly the class of bug that lets a hijacked agent act across tenants.
After
Every tool call enters through one MCP boundary: OAuth 2.1 bearer tokens from Microsoft Entra ID, per-tenant and per-scope, with the tenant bound in code and an append-only audit entry written for every call.
// AFTER: one boundary enforces auth, scope, tenant binding, and audit for ALL tools.
public sealed class GovernedToolFilter(AiPrincipal principal, IAuthorizationService authz, IAiAuditLog audit)
{
public async Task<ToolResult> InvokeAsync(McpToolCall call, Func<Task<ToolResult>> next, CancellationToken ct)
{
var decision = await authz.AuthorizeAsync(principal, call.RequiredScope, ct);
if (!decision.Allowed)
return ToolResult.Denied(call.RequiredScope);
var result = await next(); // tenant already bound from the token
await audit.RecordAsync(principal, call, result, ct);
return result;
}
}
Diagnostic: this is the same governance from our AI-Native Architecture and Enterprise AI Security posts — except now it lives at one boundary instead of being re-implemented per integration. Auth stops being a per-client afterthought and becomes a property of the protocol.
Mattrx metric: one OAuth/Entra boundary replaced N bespoke auth flows, and we now block ~40 tool-abuse / injection attempts per week at this single chokepoint. Agent tool-call error rate fell from 6% to 0.8% — most of those errors were auth and contract mismatches that simply stopped existing.
4. A safe door for external AI
Before
A partner asked to let their AI assistant pull campaign KPIs from Mattrx. With bespoke integrations, the only answer was "build them yet another client, with yet another auth path" — so the real answer was "no." There was no safe, generic way to expose a capability to an outside agent.
Diagnostic: bespoke integration is inherently internal. Every new consumer is a new bespoke build, so external consumers never happen. You leave product surface — and revenue — on the table because the integration model can't scale to strangers.
After
mattrx-analytics is an MCP server. An approved external assistant authenticates via Entra ID, receives a token scoped to its tenant and to campaigns:read / events:read, and calls the exact same tools our internal agents do — discovered, scoped, and audited identically.
External assistant --- OAuth (Entra, scope: campaigns:read) ---> mattrx-analytics (MCP)
| |
| tools/call get_campaign_kpis { campaignId, range } |--> scope check
| |--> tenant from token
| result: CampaignKpis (audited) |--> audit appended
|<-------------------------------------------------------------|
Mattrx metric: approved external AI assistants now run scoped, audited queries against Mattrx — a capability that simply did not exist under bespoke integration. Same tools, same boundary, zero new glue.
What an MCP call actually looks like
The protocol is small. A client initializes, discovers tools, then calls them — all JSON-RPC over the transport (Streamable HTTP + SSE in our production, stdio in local dev).
Agent (MCP client) mattrx-analytics (MCP server)
| initialize (protocol version, caps) |
|-------------------------------------------->|
| result (serverInfo, capabilities) |
|<--------------------------------------------|
| tools/list |
|-------------------------------------------->|
| [get_campaign_kpis, query_events, ...] |
|<--------------------------------------------|
| tools/call get_campaign_kpis { args } |
|-------------------------------------------->| -> authorize scope
| | -> bind tenant from token
| | -> append audit entry
| result (CampaignKpis) | or JSON-RPC error |
|<--------------------------------------------|
Three message types do almost all the work: initialize, tools/list, tools/call. That small surface is the point — it is small enough that any client and any server can implement it, which is exactly what makes capabilities additive. We pull this apart message by message in Part 2.
The numbers, in one place
| Metric | Before (glue) | After (MCP) |
|---|---|---|
| Integrations | 14 point-to-point | 3 MCP servers |
| Integration code | ~9,000 LOC of glue | removed (−40%) |
| New-capability onboarding | ~3 days | ~2 hours |
| Tool-call error rate | 6% | 0.8% |
| Tool calls / day | siloed, unmeasured | ~85,000 |
| Read-tool p95 | varied per client | 120 ms |
| Report-enqueue p95 | varied per client | 90 ms |
| Auth boundaries | N (one per integration) | 1 (OAuth/Entra) |
| Tool-abuse blocked / week | not measured | ~40 |
| External AI access | impossible | scoped + audited |
Should you adopt MCP? A checklist
- You have more than one agent or AI consumer (N > 1).
- You have more than one backend capability worth calling (M > 1).
- You expect N or M to grow — new agents, new tools, maybe external consumers.
- You already have (or want) a single identity provider for tool auth.
- You want tool discovery and governance to be uniform, not per-integration.
- You can express your capabilities as typed tools with clear schemas.
If most of these are checked, the N+M math pays for itself fast. If not, read the honest section before you adopt anything.
The honest stuff: when NOT to adopt MCP
MCP is a protocol, and protocols have overhead. Skip it when the math doesn't favor it:
- One agent, one backend. N×M is 1×1. A direct method call is simpler and faster — don't add a protocol to a problem you don't have.
- A stable internal toolset with no external consumers. If nothing is growing and no stranger will ever call in, the additive win is theoretical.
- Ultra-low-latency hot paths. MCP adds a hop and JSON-RPC framing. Sub-millisecond inner loops should not route through it.
- Auth is still a mess. MCP's value compounds with one identity provider. Bolt it onto five inconsistent auth schemes and you get five inconsistent auth schemes with a protocol on top.
- The "tool" is really an in-process function. Don't promote a local method to a network protocol just to say you use MCP.
- You haven't shipped a v1. Build the naive integration first. Adopt MCP when N or M actually grows — we did it at integration fourteen, not integration two.
- Governance isn't ready to expose capabilities. If compliance must review anything that crosses a generic protocol boundary, get that process in place first (we cover it in Parts 6–8 and 11).
We did not adopt MCP because it was fashionable. We adopted it because the fourteenth bespoke client was the moment N×M stopped being survivable. If you are below that line, stay simple.
The model to carry forward
Integrations scale as N×M. Protocols scale as N+M. Every bespoke client you write is a multiplication you will pay for again with the next agent. Every capability you publish as an MCP tool is an addition that every future agent gets for free.
Three habits that make the switch pay off:
- Publish capabilities, not endpoints. Design each tool as a contract an unfamiliar agent can call from its schema alone — name, description, typed arguments.
- Put one identity boundary in front of every tool. Never let an integration invent its own auth. One OAuth/Entra door, scoped per tool, tenant bound in code.
- Treat tool schemas as your public API. Version them, document them, and break them as carefully as you would break any contract — because external agents now depend on them.
In Part 2 we open the box: the MCP architecture — hosts, clients, servers, transports, and the message lifecycle — and how Mattrx maps each piece onto real Azure infrastructure.
Continue the series — MCP Deep Dive
- Why Model Context Protocol Kills Integration Glue Code for Good (you are here)
- Inside the MCP Architecture: Hosts, Clients, and Servers
- Build a Production-Grade MCP Server From Scratch
- Build an MCP Client That Connects to Any AI Tool
- Custom MCP Tools Your AI Agents Can Actually Trust
- MCP Authentication With OAuth and Entra ID, Done Right
- Scoped Authorization for MCP Tools (Least Privilege for Agents)
- Securing MCP Against Prompt Injection and Tool Abuse
- Streaming and Long-Running Tools Over MCP
- Debugging and Observability for MCP in Production
- Rolling MCP Out Across the Enterprise
- Building MCP Servers in C# and .NET 9
- Hosting MCP on Azure at Real Scale
- Wiring MCP Into OpenAI and Agent Frameworks
- Running MCP in Production — Lessons From Mattrx
Further reading
- AI-Native Architecture: The 9-Layer Blueprint Every Enterprise Will Adopt by 2027
- Enterprise AI Security: 7 Attacks on Your LLM App, and the Layer That Stops Them
Adopting MCP and want a second pair of eyes on where to draw your server boundaries? I'm always happy to compare notes — reach me at randhir.jassal@gmail.com.
Get the next issue
A short, curated email with the newest posts and questions.