When we first wired billing into the agent, the config looked like this:

{
  "mcpServers": {
    "billing": {
      "command": "npx",
      "args": ["-y", "@powerduck/openapi-mcp-server", "serve", "--transport", "stdio"],
      "env": {
        "BILLING_UPSTREAM_TOKEN": "sk_live_9f2c..."
      }
    }
  }
}

We thought we were being careful. The token lived in a local JSON file. It was not in a git repo. It was just... on every engineer's laptop that wanted to use the agent.

Six of us had that file. One of us had a laptop that ran a backup sync to an external drive. Another ran a dotfiles repo that she sometimes pushed to a personal GitHub. The token was one grep -r sk_live away from being anywhere.

Worse: the token was a live billing token. It could call POST /refunds, not just GET /invoices. The agent's tool list was read-only, but the credential it was using had write scope. We had layered a safety net on top of an unsafe identity.

The two-token model

There are two credentials in an MCP setup, and they want opposite homes:

The upstream credential talks to your real API. It knows how to authenticate to billing, the CRM, whatever. It has real scope. It should live somewhere you control, can rotate, and can revoke centrally. It should not live on an engineer's laptop.

The access credential talks to the MCP endpoint itself. It says "this client is allowed to ask the gateway to call billing on its behalf." It has narrow scope: it can call the exposed tools, it cannot call billing directly, and you can issue a separate one per teammate.

When you paste the upstream token into ~/.cursor/mcp.json, you have collapsed those two into one. The agent client now holds a credential that reaches past the gateway and straight into your production service.

What changes when the gateway holds the upstream token

The config on the engineer's laptop shrinks to:

{
  "mcpServers": {
    "billing": {
      "type": "http",
      "url": "https://mcp.powerduck.com/d/abc123",
      "headers": {
        "Authorization": "Bearer pk_eng_maria_..."
      }
    }
  }
}

pk_eng_maria_... is a per-teammate access key scoped to the exposed read-only tools. If Maria's laptop is lost or she leaves, you revoke that one key. The live billing token never touched her disk. If it leaks, you have a real incident; if the access key leaks, the blast radius is "the attacker can call the 9 read-only tools on a demo org."

Rotation stops being a company-wide announcement. The upstream token rotates in the gateway's config on whatever schedule billing already uses. No engineer gets an email, no mcp.json edit, no "did you update yours?" thread. The next request after rotation just works.

Logs get cleaner too. When the gateway holds the token, the request log can redact the Authorization header before it hits the console. When the token is on the laptop, the same header shows up in the agent's transcript, in your terminal scrollback, and in any error report the agent decides to paste back to you.

The migration that took an afternoon

We did this in one sitting:

  1. Generated a new read-scoped token in billing (the old write-scoped one was already rotated out).
  2. Pasted it into the gateway's upstream-auth config for the billing document.
  3. Issued one per-teammate access key, sent it in a 1Password shared item.
  4. Asked everyone to replace their mcp.json block with the URL + access key version.
  5. Deleted the old BILLING_UPSTREAM_TOKEN env var from every local config we could find.

The agent did not notice. The tools returned the same shapes. The difference was that a year later, when Maria's laptop was stolen on a train, the incident response was "revoke pk_eng_maria_" and nothing else.

The rule of thumb

If a credential in your MCP config can reach a production service without going through the gateway, it is in the wrong place. The client config should describe where to call, not how to authenticate to the upstream service. The gateway is the only thing that ever needs to know the upstream token. Everything else gets a narrow, revocable, per-user key.

It feels like a small distinction. It is the difference between "rotate a secret for 6 laptops" and "rotate a secret in one config screen."

This is the model Powerduck uses: the upstream Bearer/Basic/APIKey is stored in the gateway, not in Cursor's MCP config, so rotating a token does not mean re-editing every teammate's client.