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 (MCP) is the thing that stops that multiplication.
This is Part 1 of a 15-part deep dive. Every part uses the same running example: Mattrx, our multi-tenant marketing-analytics SaaS (.NET 9 / Azure), and every metric here is from that real system.
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.
- ~9,000 lines of glue code deleted — roughly a 40% cut.
- New-capability onboarding dropped from ~3 days to ~2 hours.
- Agent tool-call error rate fell from 6% to 0.8%.
- ~85,000 MCP tool calls/day, all governed by the same boundary.
- ~40 tool-abuse / injection attempts per week blocked at the MCP boundary.
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 makes capabilities additive instead of multiplicative.
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.
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)
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)
1. The integration explosion
Before, every agent embedded a bespoke client for every backend:
// 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
{
// Each client has its own auth, retry policy, and error model.
// The next agent we build re-implements a slice of all four.
}
After, each capability is declared once as an MCP tool:
// 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:
var result = await mcp.CallToolAsync(
"get_campaign_kpis",
new { campaignId = "4821", range = "2026-06-01/2026-06-28" },
ct);
Result: 14 integrations → 3 servers, ~9,000 lines of glue deleted, almost all deletions.
2. Capability discovery
Before, the toolset was a constant the agent was compiled with — the list and reality drift. After, the server advertises its tools and the client discovers them at runtime:
// AFTER: the agent asks the server what it can do, every session.
var tools = await mcp.ListToolsAsync(ct);
// each tool: name, description, JSON Schema for args — enough for an LLM to
// decide when and how to call it, with zero hardcoding.
Discovery is the quiet superpower: ship a new tool on the server, and every agent can use it next session. Onboarding a capability went from ~3 days to ~2 hours.
3. One auth and audit boundary
Before, every bespoke client reinvented auth (one static key, one OAuth scope, one that trusted a tenant id passed as an argument — the bug we shipped). After, every tool call enters through one MCP boundary:
// 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;
}
}
One OAuth 2.1 / Entra ID boundary replaced N bespoke auth flows. 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 wanting their AI assistant to pull your KPIs meant “build them yet another client” — so the answer was “no.” After, an approved external assistant authenticates via Entra ID, gets a token scoped to its tenant and to campaigns:read, and calls the exact same tools our internal agents do — discovered, scoped, and audited identically. A capability that simply did not exist under bespoke integration.
What an MCP call actually looks like
The protocol is small — three message types do almost all the work: initialize, tools/list, tools/call, all JSON-RPC over the transport (Streamable HTTP + SSE in production, stdio in local dev). That small surface is the point: it’s small enough that any client and any server can implement it, which is exactly what makes capabilities additive.
When NOT to adopt MCP
- One agent, one backend (1×1). A direct method call is simpler and faster.
- A stable internal toolset with no external consumers. The additive win is theoretical.
- Ultra-low-latency hot paths. MCP adds a hop and JSON-RPC framing.
- Auth is still a mess. MCP’s value compounds with one identity provider.
- 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 two.
The model to carry forward
Integrations scale as N×M. Protocols scale as N+M. Every bespoke client you write is a multiplication you’ll pay for again with the next agent. Every capability you publish as an MCP tool is an addition every future agent gets for free.
- Publish capabilities, not endpoints. Design each tool as a contract an unfamiliar agent can call from its schema alone.
- Put one identity boundary in front of every tool. One OAuth/Entra door, scoped per tool, tenant bound in code.
- Treat tool schemas as your public API. Version them, document them, break them carefully.
Originally published on PrepStack. Adopting MCP and want a second pair of eyes on where to draw your server boundaries? Reach me at randhir.jassal[at]gmail.com.