Wiring AI agents into my own infrastructure with MCP
One MCP server in the fleet won't boot without a reachable Redis instance. No retry loop, no backoff — the process starts, checks for Redis, and exits the moment it doesn't find one. That turned out to be one of the milder surprises in wiring up 8+ MCP servers, because nearly every one of them shipped assuming a vendor's cloud product sat on the other end of the connection. My project tracker, deploy platform, analytics, observability, and billing all run on my own infrastructure instead, so almost none of these servers worked as installed — each one needed a different URL, a different auth header, sometimes a completely different way of authenticating, before an agent could actually use it.
#Built for someone's cloud
The pattern repeated almost everywhere: an MCP server ships wired for an OAuth exchange against the vendor's cloud endpoint, with no path to a self-hosted instance built in anywhere. The project tracker was the clearest example. In place of the OAuth flow, it needed exactly three things: my own instance's API URL, a workspace identifier, and a plain API key on the request header, the same key already in use for every other integration against that instance. Getting to that exact list took longer than using it: the documentation covers the cloud flow by default and stays silent on self-hosted setups entirely.
#Standing up my own MCP server
Analytics turned into the longest chase of the fleet. The first attempt was the vendor's own official MCP, and it was a dead end almost immediately — it only speaks to their cloud product, with no configuration surface for pointing it at anything self-hosted. The second attempt looked more promising: a separate npm package advertising self-hosted support directly in its description. It installed cleanly. What it didn't have anywhere inside it was a runnable binary — a library with no server behind it, meant to be wrapped in something else rather than run on its own.
The third attempt was the one that worked: the same vendor also publishes a prebuilt Docker image of their MCP server, the same image they run themselves. Standing it up meant treating it like any other small service on the infrastructure, one more container next to everything else already running. The first run failed immediately — the container came up and exited again within seconds, no retry attempted, and the one consistent detail in its output pointed straight at Redis. The dependency turned out to be absolute: nothing about the server starts without Redis reachable on the other end, not even in some reduced mode. Pointing it at the Redis instance already running elsewhere in the infrastructure fixed it outright. Auth ended up being the easy part by comparison: a personal API key sent as a bearer token on every request.
#Small things that cost time
A couple of smaller stories, each eating more time than its size warranted. The deploy platform's MCP server authenticates with a plain API-key header, about as simple as setup gets. Its CLI can report a failed connection on the very first run, and the instinct is to start second-guessing the key, the URL, the permissions. The real cause is almost always duller: the platform's cold start doesn't finish inside the health-check timeout the CLI uses on its first few attempts. Waiting and trying again usually clears it, before there's any real reason to start rewriting the config.
The observability platform's MCP shipped a different kind of surprise. Nearly every other server in this fleet arrives as an npm package — install, run, done. This one shipped as a Go binary instead, the only one in the fleet built that way: a binary matched to the right architecture, an execute permission, a path entry in the agent's config, a different install shape entirely from the rest. Its auth header is easy to get subtly wrong, too — the wrong case or the wrong exact format on the header name is enough for the server to reject the request silently, with nothing in the response pointing at what was actually wrong.
#Two hosts behind one login
Billing was the case where the mistake nearly made itself. The obvious move was pasting the same host used to log into the billing dashboard in a browser straight into the MCP config. That host turned out to be the wrong one. The real API lives on a separate domain, close enough in name to the dashboard's to invite the mix-up, and that's the host the MCP server actually needs along with its own API key on the header. The dashboard's login-gated host has no role in that exchange, since the MCP talks to the API directly and never goes through a browser session at all.
The same shape shows up in more than just billing. A lot of the self-hosted UIs in this fleet sit behind a dedicated SSO gateway that protects the frontend with a login and a session, without necessarily protecting the API sitting behind that same product on its own separate host. None of that is visible from a single browser tab — the dashboard loads, everything looks fine, and the gap only surfaces once an MCP server tries to reach the API directly and finds out the address bar was never pointing at it.
#Wiring the databases
The rest of the databases took direct connections with no MCP-side detours: a columnar analytics database, a cache, and a vector database, each with its own authentication shape and its own client. A handful of connection variables per database, and every one of them worked on the first try.
Postgres, the primary relational database, was the exception — the only place in the whole project where the fix belonged to the infrastructure underneath rather than to any MCP server's configuration. The MCP client connected over direct SSL, and the handshake was failing during protocol negotiation, a stage that completes or fails before Postgres itself ever gets a chance to see the connection, let alone authenticate it. A failure at that layer says nothing about credentials or the database itself — it only means the two ends of the TLS connection couldn't agree on what protocol to speak next. The actual cause sat in the TLS proxy in front of the database: its configuration was missing exactly one protocol name from the list of what it was willing to negotiate on incoming connections. Adding that one name and restarting the proxy was the entire fix — the handshake completed on the very next attempt.
#One gateway in
One more layer sits underneath everything above: a self-hosted, cluster-wide LLM gateway, built on an open-source LLM-routing project and deployed on the same platform as everything else described here, just another service in the same infrastructure with no separate cloud account behind it. The point is that every internal service and every agent reaches language models through that single entry point, rather than each one holding its own set of provider keys.
MCP servers and the gateway sit at different points in the same chain. MCP servers are how an agent reaches my systems — the tracker, the deploy platform, the databases. The gateway is how that same agent reaches the models themselves.
#Locking the agents down
The last piece of this was restricting the coding agents themselves. Two of the open-source AI coding CLI tools I run are forked builds, each compiled to reach exactly one endpoint and one credential, baked in at build time — that internal gateway, nothing else reachable from the binary at all. Every other outbound path is blocked at the network level, by firewall rules that live outside the process entirely, somewhere the tool has no visibility into and therefore no way to touch.
The practical difference is significant. An agent that can physically reach only infrastructure under my own control is a fundamentally smaller attack surface than one with open outbound access to anywhere at all, waiting for a prompt injection or an ordinary bug to point it somewhere it shouldn't go.