---
title: "Expose Internal APIs as Governed MCP Tools"
description: "Give AI agents your internal API as MCP tools without pasting credentials into editors. Expose chosen operations as an MCP server in Zuplo, then front it with the MCP Gateway for SSO, tool curation, and a per-call audit trail."
canonicalUrl: "https://zuplo.com/blog/2026/06/17/expose-internal-apis-as-mcp-tools"
pageType: "blog"
date: "2026-06-17"
authors: "martyn"
tags: "Model Context Protocol, API Security, ai-agents"
image: "https://zuplo.com/og?text=Expose%20Internal%20APIs%20as%20Governed%20MCP%20Tools"
---
Your team wants Claude and Cursor to hit the internal billing API, the inventory
service, the admin tooling. The fast way is to mint an API key, paste it into
`mcp.json`, and point an agent at the service directly.

Now a long-lived key with write access to an internal system lives in a dotfile
on a laptop, and the agent can call every operation the API exposes, including
the ones that delete things.

You don't have to choose between giving agents the API and keeping it safe. Two
Zuplo capabilities chain together: the MCP Server handler turns your API's
routes into MCP tools, the discrete actions an agent like Claude can call, and
the [MCP Gateway](/blog/introducing-zuplo-mcp-gateway) puts those tools behind
SSO, strips the dangerous ones, and logs every call. This walkthrough wires them
together in the portal.

<CalloutAudience
  variant="bestFor"
  items={[
    "Platform and security teams giving agents internal APIs without handing out raw keys",
    "API owners ready to expose a safe subset of their service as agent tools",
    "Anyone who wants the whole flow in the portal, no config files required",
  ]}
/>

## Start with your internal API

This walkthrough assumes your internal API is managed in a Zuplo project, or is
about to be: its routes are described by an OpenAPI spec in the project, the way
Zuplo manages every gateway. If the API lives elsewhere today, put a Zuplo
gateway in front of it and import its OpenAPI spec. Once the routes are in the
project, exposing a slice of them as MCP tools takes just a few additional
clicks, as you'll see.

Keep this in its own project. It's how we split it internally: the API team owns
the gateway and the MCP server that sits on it, while platform or security owns
the separate gateway project we build later. One account, two projects, and
neither team waits on the other to ship.

## Turn routes into MCP tools

Open the **Code** tab, add a route, and pick the **Dynamic OpenAPI to MCP
Server** handler from the dropdown. The portal scaffolds it on `POST /mcp` and
serves it over HTTP, the transport MCP clients connect with. You choose the
handler rather than writing it by hand, so the route is wired up without
touching JSON.

![The Zuplo Route Designer with the Add menu open and the Dynamic OpenAPI to MCP Server handler highlighted as the route type.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/mcp-server-handler.png)

You do not surface the whole API. On the route, click **Select Tools** and tick
only the operations agents should reach. A read-only reporting endpoint and a
"create draft invoice" call are reasonable; "delete customer" is not.

Each checked operation becomes a tool, named from its `operationId` and
described from its OpenAPI summary, with the input schema derived from the
operation so the server validates arguments before your handler runs.

![The MCP Tools dialog with listInvoices, getRevenueReport, and createDraftInvoice ticked while sendInvoice and deleteCustomer are left unchecked.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/select-tools.png)

<CalloutDoc
  title="Dynamic MCP Server Quickstart"
  description="The full handler flow in text, including local development and every option the handler accepts."
  href="https://zuplo.com/docs/articles/mcp-quickstart"
  icon="book"
/>

## Lock the route with an API key

The MCP server is a normal Zuplo route, so it takes the same inbound policies as
any other. For an internal service the simplest control is an API key: open the
route's policy editor and add Zuplo's API key authentication policy, backed by a
key you create in the project. The server then rejects anyone without a valid
key. Skip the policy and the route still serves tools, but so does anyone who
finds the URL.

![The Zuplo route's policy editor with the API key authentication policy added to the inbound pipeline of the MCP server route.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/api-key-policy.png)

That single key is the only credential that reaches your internal MCP server.
What matters is who holds it: not your developers, and not their editors. It
goes into the gateway in the next step, which keeps it server-side and never
hands it to a client. Your internal API is now reachable as MCP tools, but only
by a caller holding a key your developers never see.

## Front it with the MCP Gateway

Create a second project in the same account for the gateway. Open its **Code**
tab, click **Add Route**, and pick **MCP Gateway Virtual Server**. A virtual
server fronts exactly one upstream MCP server, putting it behind your own auth
and tool policy, so this one points at the internal MCP server you just built.

The wizard opens on a library of known servers like Linear and Stripe. Yours
isn't in it, so choose the custom MCP server option and paste your internal MCP
server's URL as the upstream. The same wizard, pointed at a third-party server,
is the one we walk through in
[fronting a third-party server](/blog/set-up-virtual-mcp-server-portal); here
the upstream is just yours.

![The New MCP Gateway Virtual Server wizard on the Upstream step, the Custom MCP server option chosen and the internal MCP server's URL pasted as the upstream.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/gateway-custom-upstream.png)

## Gate access with SSO

The wizard's next step is inbound auth: who is allowed to connect to the
gateway. This is where your internal API stops being reachable by anyone with a
pasted key and starts being reachable only by your team. Pick the identity
provider your organization already runs, Auth0, Okta, Entra, Google, or another
OIDC provider, and the same SSO login that gates everything else now gates the
MCP server.

The wizard names the environment variables that provider needs, the domain,
client ID, and secret, which you add under **Settings** before anyone connects.

The gateway runs full OAuth on the inbound side. Clients authenticate through
your IdP, and the gateway issues its own short-lived token scoped to that one
virtual server. The developer's editor only holds that token, never a credential
for the internal API.

## Curate tools per team

The wizard's **Tools** step decides which of the MCP server's tools this gateway
hands to your team. **Passthrough** forwards all of them; **Curate** lists them
and lets you tick only the ones you want.

You already trimmed the list once, back on the MCP server. That trim is the
ceiling: the most any caller could ever see. Curate narrows it again, just for
this gateway, and never touches the API project. So even though the server
offers `createDraftInvoice`, you can stand up a read-only gateway here that
exposes only `listInvoices` and `getRevenueReport`.

<CalloutTip variant="tip">
  Leaving a tool off the curated list doesn't just hide it, it removes it. The
  gateway rejects any call to a tool you didn't expose before that call reaches
  your API, so no cleverly worded prompt can talk an agent into using one.
</CalloutTip>

## Broker the key, don't share it

The wizard's last step is outbound auth: how the gateway authenticates to your
internal MCP server. Your server uses an API key, not OAuth, so make two choices
here:

- **None** for upstream credentials, so the gateway doesn't attempt an OAuth
  exchange with your server.
- **Remove auth token**, so the inbound client's `Authorization` header is
  stripped before forwarding and the developer's token never reaches your
  internal API.

![The wizard's Outbound Auth step with None selected for upstream credentials and Remove auth token selected for the inbound Authorization header.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/outbound-auth.png)

That clears the inbound token but doesn't yet present your internal key. Store
the key as a secret environment variable under **Settings**, then add a
set-headers policy to the end of the route's inbound pipeline that injects it as
the upstream `Authorization` header.

![The gateway route's policy editor with a set-headers policy at the end of the inbound pipeline, injecting the upstream Authorization header.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/outbound-set-headers.png)

The policy reads the key with `$env(...)`, so the value lives only in your
secret environment variable and never appears in source control:

```json
{
  "export": "SetHeadersInboundPolicy",
  "module": "$import(@zuplo/runtime)",
  "options": {
    "headers": [
      {
        "name": "Authorization",
        "value": "Bearer $env(INTERNAL_BILLING_API_KEY)"
      }
    ]
  }
}
```

<CalloutDoc
  title="Connect to an API-key upstream"
  description="The full outbound-auth flow, including the set-headers policy config and the secret environment variable."
  href="https://zuplo.com/docs/mcp-gateway/how-to/connect-upstream-api-key"
  icon="book"
/>

This is the move that makes the whole thing safe. The internal API key lives in
one place, the gateway, and is never copied to a laptop or an editor config. The
token a developer's agent presents and the key that reaches your internal API
are two different things, exactly the boundary the
[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
is built to enforce: an MCP server must not pass through the token it received.

It's the same boundary we argued for in
[governing shadow MCP](/blog/shadow-mcp-governance). Rotate the key in one place
and every connected agent keeps working.

![An AI agent connects over an OAuth token to the Zuplo MCP Gateway in a platform/security project, which curates tools and brokers an API key to the internal MCP server in a separate API-team project, which exposes selected routes of the internal API. The OAuth token stops at the gateway and the API key never leaves it.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/diagram-1.png)

## Connect your team and audit

Hand your team the gateway URL: the project's gateway URL plus the route path,
something like `/mcp/internal-billing`. In Claude, that's **Settings**,
**Connectors**, **Add custom connector**, paste the URL, and run the connect
flow. The developer authenticates through your IdP once and their agent lists
exactly the curated tools, with no key to paste and nothing to leak.

![Claude's Connectors view listing only the curated read-only tools from the gateway, each with a per-tool permission control, and no API key in sight.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/curated-tools-in-claude.png)

Alternatively, skip the chat-thread URL and publish the instructions. Every
gateway project ships a developer portal, and its MCP setup page generates
copy-paste config for Claude, ChatGPT, Codex, Cursor, VS Code, or a generic
client, all pointed at your gateway route. Hand your team that page instead of a
raw URL.

![The gateway project's developer portal showing per-app MCP setup instructions, with tabs for Claude, ChatGPT, Codex, Cursor, VS Code, and Generic, plus a copyable Claude Code CLI command.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/dev-portal-mcp-setup.png)

Lock that portal down to match the gateway:

- **Whole portal behind login**, so only your organization can see it.
- **Individual pages behind protected routes**, so the internal-billing
  instructions reach only the teams that should have them.

<CalloutDoc
  title="Protected routes in the developer portal"
  description="Gate portal pages behind authentication so setup instructions reach only the right teams."
  href="https://zuplo.com/docs/dev-portal/zudoku/configuration/protected-routes"
  icon="book"
/>

Then open **Observability**, select **Analytics**, and choose **MCP** from the
sidebar. Every tool call shows up: which operation ran, who invoked it, success
rates, and upstream errors. That per-call record is the inventory you never had
when developers wired up their own connections, and it's the same governance
you'd want on
[any MCP server you ship](/blog/never-ship-mcp-server-without-rate-limit).

![The gateway's MCP analytics view: each tool broken out by call volume, error rate, and p95 latency, plus the consumers who invoked them.](/blog-images/2026-06-17-expose-internal-apis-as-mcp-tools/per-call-audit.png)

Step back and the walkthrough stacked five independent controls between an agent
and your internal API. Each one was a single step above, each closes a different
gap, and none depends on the others holding:

| Control             | Where you set it                      | The gap it closes                                   |
| ------------------- | ------------------------------------- | --------------------------------------------------- |
| Operation selection | MCP server handler, **Select Tools**  | Limits which operations can ever become tools       |
| SSO inbound auth    | Gateway wizard, your IdP              | Keeps anyone outside your team off the gateway      |
| Tool curation       | Gateway wizard, **Curate**            | Blocks curated-out tools before they reach upstream |
| Key brokering       | Gateway, set-headers + secret env var | Stops the internal key landing in an editor config  |
| Per-call analytics  | Observability, **Analytics → MCP**    | Removes blind spots about who called which tool     |

## Going to production

Everything you just built lives in your Working Copy, Zuplo's development
environment, which deploys on save and is ideal for testing the connect flow.
For production, two changes apply to both projects:

- **Connect source control**, so deploys come from a git branch with review and
  rollback.
- **Put the gateway on a custom domain**, so the URL you hand your team is one
  of yours.

## Get started today

The [MCP Gateway](/blog/introducing-zuplo-mcp-gateway) is in public beta, free
to try on a new project.
[Spin up a free Zuplo project](https://portal.zuplo.com/signup?utm_source=zuplo-blog&utm_medium=web&utm_campaign=mcp-gateway)
and put your internal API in front of your team's agents without handing over a
single key.