# Plugging in third-party MCP servers safely

By [Anubhav Choubey](https://anubhavchoubey.com/) · AI & Agents · 2026-11-04 · 4 min read

> Letting a team connect any remote MCP server once and use its tools everywhere, with an SSRF guard, bound secrets, pinned tools and a proxy that fails closed.

Part 6 of the MCP series. Start at [part 1](/blog/mcp-auth-for-people-and-agents).

```stats
SSRF guard in two languages · secrets bound to their place · tools pinned at approval · a proxy that refuses to deploy unsafely
```

## The problem

A store's team uses other tools: a CRM, an email platform, sometimes their own MCP server. I wanted them to connect those remote MCP servers once in the dashboard, and see the tools in every MCP client they already use.

Each part of that is attack surface. We dial URLs that customers type in. We store their vendor credentials. We show the model tool definitions written by someone else. And real vendors don't always follow the specs.

## Dialling URLs we don't control

Both the dashboard (Node) and the proxy (Python) contact customer-supplied URLs, so both got the same outbound SSRF guard, tested against one shared file of attack vectors that both suites must pass.

- https only, on allowlisted ports.
- Private, loopback, link-local and metadata, carrier-grade NAT, multicast and unique-local addresses are blocked.
- IPv4-mapped and translated IPv6 forms, and integer or hex IP literals, are refused.
- Every DNS answer is checked, and the caller dials the pinned IP it checked, so a second lookup can't swap the target.
- Redirects are never followed.

## Secrets that only decrypt in place

Refresh tokens, client secrets and API keys are encrypted with AES-GCM, with associated data built from a version, the store, the connection and the field name. A ciphertext copied to another store, connection or field fails to decrypt. I also removed an old plaintext checksum. The GCM tag already authenticates, and a checksum of the secret only gives an attacker a way to test guesses.

## A connections proxy

A small stateless service serves the connected tools behind the gateway.

- The store comes from the live request, never a cached session. Credentials are fetched on every request and never cached, so a disconnect takes effect on the next call.
- An admin approves each tool. The proxy serves it in its pinned form, and drops upstream extras the model would see. If a tool's definition hash changes after approval, the tool is hidden until someone re-approves it.
- Outbound headers are built from scratch. Errors are fixed codes, never vendor text, and logged URLs are stripped because a custom URL can carry a secret.
- Deadlines keep a list plus a call under the edge's cutoff, and a list-only circuit breaker stops one dead vendor from slowing every listing.
- Lists and results have separate size budgets, plus a parse-cost budget. A connection is admitted or refused whole, never silently truncated.

```callout warn
A tool description is a prompt someone else wrote. Pin what was approved, and treat any change as new code that needs review.
```

## Wiring it into the gateway

The proxy sits behind the gateway and relies on it for caller identity. So the deploy chart refuses to render in any state where that would be unsafe: network policy off, the gateway's permission layer not enforcing and failing closed, debug logging on, fewer than two replicas, or the gateway admitting any caller without an identity.

Before enabling the proxy in a region, I wanted proof that the gateway there really authenticates its callers. A config value saying "enforce" isn't proof, because an older image might ignore it. So a check reads the source the running gateway image was built from and confirms the enforcement is really there. Anything it can't prove counts as "cannot tell", never "authenticated".

## Gateway-hidden tools

Some vendors list only part of their tools and hide the rest behind a search tool and a generic "call any tool" dispatcher. Approving the dispatcher would bypass per-tool approval entirely.

So the proxy expands the hidden tools into the list before the normal filter runs, which means approval, drift checks and size budgets apply to them unchanged. On a call, it re-checks the pinned dispatcher and the approved hash, then calls the dispatcher with a freshly built name and arguments. Nothing in the model's arguments can redirect it.

## OAuth as vendors really do it

Real vendors broke the textbook flow in several ways:

- An issuer and authorize endpoint on one host and the token endpoint on another, with no `iss` in the response and no dynamic client registration. I now accept endpoints on the same registrable domain (using the public suffix list, so two unrelated subdomains of a shared host still fail), and fall back to the admin's own OAuth client.
- A token endpoint that advertises Basic client auth but ignores the header. When it is advertised, `client_secret_post` now wins.
- Long-lived access tokens with no refresh token. Those grants are kept with a capped expiry, then the connection moves to "needs re-auth".

## What I'd do differently

- **Decide who acts as whom first.** Whether a member's call uses their own vendor credential or a shared admin one should be a day-one decision, not a follow-up.
- **Capture real vendor responses before writing profiles.** Some gateway detection was written from documentation, and a wrong guess simply never matches.

## Takeaway

Connecting someone else's MCP server means trusting their URLs, their tokens and their words. Check every address you dial, bind every secret to where it lives, pin every tool at approval, and make the deploy itself refuse to run in an unsafe shape.

**Next:** [Agents on MCP](/blog/agents-on-mcp)
