One MCP gateway for every agent
A single MCP server with per-user permissions and auditing that became the front door to the company's agent tools.
Results
560+
automated tests in the gateway
measuredcount of test functions in the repository (Nov 2026)
0
lines of code to integrate a new service: just a record in the backoffice
before: one adapter per service
measuredsince Jul 2026architecture pivot documented in the spec
30 s
maximum time per tool call, with a structured error
before: a call hanging until the 300 s timeout
measuredNov 2026transport fix PR
2
critical vulnerability classes fixed in production: SSRF and identity spoofing
self-reportedsecurity audit I led
Summary
Context
Every AI tool at the company (editors, internal agents and the support agent on the omnichannel platform) had to configure, one by one, the MCP connections it depended on, each with its own credential. There was no central place to say who can access what, and no record of what had been queried. The request came from the tech lead.
Action
I designed and built a unified MCP server: the client authenticates once and sees, in a single list, the tools of every connection it is allowed to use. Early on I replaced the per-service adapters with a generic MCP client, and integrating something new became a record in the backoffice. Then came OAuth with Google login, REST connections imported from OpenAPI, GCP identity for private services and a Jira triage server that structurally refuses a duplicate card or one without evidence.
Result
In production, with more than 560 tests, the Unificador is the front door to the support agent's tools, including the Jira triage server and the internal RAG. I fixed critical vulnerabilities of two classes (SSRF and identity spoofing) found in my own audit. After an incident in which every call failed, I moved to a stateless transport with a 30 s limit per call.
Lesson
When one LLM's output flows back into another agent, it is user input: validate the format and never pass the model's free text along.
Architecture
Long version
The problem
With the Model Context Protocol, an agent can use external tools: Jira, knowledge bases, internal APIs. But every client had to register every MCP server, store every credential and hope nobody accessed what they shouldn't. Without a central point, there was no auditing either.
The Unificador
One authentication, every permitted tool. The client authenticates once (JWT or OAuth) and gets, aggregated, the tools of all the connections released to that user. On each call, the backend decides which connection to invoke.
Two-stage authorization and auditing. Permission is checked before the tool is exposed and again before the response is returned, against a blacklist. Every query becomes an audit record. The backoffice (React, behind Google Cloud IAP) manages connections, users, the prompt and logs.
The early pivot. The first version had one adapter per service. I realized that every connection could be treated as an MCP server (native, or a REST API exposed as MCP from its OpenAPI spec) and replaced the adapters with a single generic MCP client. Since then, integrating a new service takes no code.
OAuth, machine-to-machine and private Cloud Run. These came later: MCP OAuth with Google login and
dynamic client registration, client_credentials for services, and an authentication mode that issues
a Google ID token to call private services. I measured the difference against the real service: a
static secret got a 401, and the identity token got a 200.
The Jira triage server
Creating a card is easy. The hard part is refusing the wrong card: the duplicate, and the one without enough information to reproduce. Both refusals are structural, not prompt instructions that an agent under pressure or under prompt injection could ignore.
- No search, no card. The creation tools require a signed
busca_token, which only exists after a duplicate search. - Evidence gate. A bug report is only accepted with description, steps, expected, actual, evidence, where, when and frequency. If anything is missing, nothing is created, and the agent gets the list of what is missing along with a ready-made message for the customer.
- Hybrid dedup. Three queries (recent cards, the exact error-message phrase and a keyword search), plus a deterministic rule ("same error message on the same screen is a duplicate") and an LLM judge for the rest.
- The judge is not trusted. The customer writes the text the judge reads. So the verdict can only
be
duplicataornovo, and anything else becomes "ask the customer". The model's justification never reaches the agent, only the log, because it would be a second-order prompt injection channel speaking in the service's own voice.
A real bug shaped the dedup: a second report of the same error was opened 2 min 40 s after the first, because the keyword search required every term to match. The cause was in the search, and the fix was the hybrid strategy.
The incident
One day, every tool call started failing. The SSE transport kept the session in the memory of the instance that opened the stream; under autoscaling, the next request landed on another instance and got a 404. The fix had three parts: a stateless Streamable HTTP transport, a 30 s total limit per call, and reconnecting only when the request had not yet been sent, because the tools are not idempotent. A health endpoint that actually tests Jira and one structured log line per call were added too.
What is still missing
I don't yet have publishable usage metrics, such as cards avoided or time saved. They are the next step, and the structured logs are already in place for it.
Lessons learned
- Generic beats specific. A generic MCP client removed per-integration code early in the project.
- LLM output is untrusted input. The duplicate judge can only answer duplicate or new, and its text never reaches the agent.
- In-memory state does not scale. A session pinned to one instance brought down every call under autoscaling; the stateless transport fixed it.