Cloudflare Workerclaude.ai + Claude CodeMIT

Several accounts of the same MCP service, in claude.ai and Claude Code.

claude.ai takes a custom connector once per URL, so a second Resend account or a third Linear workspace has no way in. This is a Cloudflare Worker you deploy in your own account. It gives every account its own URL, like /m/resend/work/mcp and /m/resend/personal/mcp, and forwards each request to the real MCP server with that account's API key.

01.The problem

Why claude.ai takes only one account per service

I built this because I use more than one account of the same services, and claude.ai let me add only the first. Three things get in the way.

  1. The same URL is refused. claude.ai won't add a custom connector whose URL it already has (anthropics/claude-ai-mcp#178, open as I write this). Two Resend accounts both live at https://mcp.resend.com/mcp.
  2. The connector form can't send headers. It has a URL field and OAuth client fields, nothing else. So there's no way to tell one URL which API key to use.
  3. The query string trick is unreliable. Adding ?account=work makes the URL unique. It works with some OAuth servers and fails with others (Google and Zoho, for example), and one report says claude.ai drops the query when it calls the server.

The gateway gets around all three. Every account gets a path of its own, so claude.ai sees different URLs. The gateway speaks OAuth to claude.ai itself, so no header is needed. And the account is in the path, not in the query.

02.How it works

One URL per account, one API key behind each

Every account you add becomes a connector at https://<your-gateway>/m/<service>/<account>/mcp. The account comes from the name of the secret that holds its key:

Worker secret → connector URL

ACCOUNT_RESEND_WORK
/m/resend/work/mcp
ACCOUNT_RESEND_PERSONAL
/m/resend/personal/mcp
ACCOUNT_LINEAR_ACME
/m/linear/acme/mcp

Your client

claude.ai: OAuth token from the gatewayClaude Code: static client token
your tokenresponse

Your gateway

/m/resend/work/mcpChecks the token was issued for this URL.Drops it and adds the key in ACCOUNT_RESEND_WORK.
API keystream

The real MCP server

mcp.resend.com/mcpSees the account's API key and the MCP request.
One request to the work Resend account. The personal one goes through /m/resend/personal/mcp and its own secret, to the same server.
  1. claude.ai or Claude Code calls the account's URL with a token: one the gateway issued through OAuth, or a static client token you set.
  2. The gateway checks the token belongs to this connector. A token issued for /m/resend/work/mcp doesn't open /m/linear/acme/mcp, nor the personal Resend account.
  3. It removes your token, adds the account's API key and forwards the request. Of your client's headers only the MCP ones (Mcp-*), Accept, Content-Type, Last-Event-ID, trace context and User-Agent go through.
  4. The response streams back as it arrives, SSE included, without the upstream's WWW-Authenticate challenge or cookies. Passed through, the challenge would send claude.ai off to sign in with the upstream instead.

Two cases are rewritten. If the upstream refuses the key with a 401 or 403, the gateway answers 502 and names the service and the account: a 401 would make claude.ai redo OAuth with the gateway, which can't fix a key the upstream rejects. And upstream redirects aren't followed, because a redirect would carry the API key to another host.

03.Setup

From the Deploy button to your first connector

You need a Cloudflare account (the free plan is enough), a GitHub or GitLab account for the fork, and an API key for each account you want to connect.

  1. Step 1: Deploy the Worker

    Deploy to Cloudflare

    The button forks apps/gateway into your GitHub or GitLab account, creates the KV namespace that holds the OAuth grants, asks for the OWNER_PASSWORD secret, then builds and deploys. It deploys the gateway, not this site.

    Use a long random password, for example the output of openssl rand -base64 24, and keep it in your password manager. It is all that stands between the internet and every account behind the gateway.

  2. Step 2: Add one secret per account

    In the Cloudflare dashboard open the Worker, then Settings → Variables and Secrets. For each account add a secret named ACCOUNT_<SERVICE>_<ACCOUNT> whose value is that account's API key.

    Settings → Variables and Secrets

    ACCOUNT_RESEND_WORK
    re_...
    ACCOUNT_RESEND_PERSONAL
    re_...
    ACCOUNT_LINEAR_ACME
    lin_api_...

    Write the service and the account in uppercase letters and digits, starting with a letter, because the name is split on the underscores: ACCOUNT_RESEND_WORK works, ACCOUNT_RESEND_MY_WORK doesn't. The connector URL uses them in lowercase. A new secret takes effect without a redeploy.

  3. Step 3: Copy the connector URLs

    Open the Worker's URL, for example https://multiaccount-mcp-gateway.<your-subdomain>.workers.dev, and sign in with the password. The page lists one connector URL per account, plus any configuration problem it found and which variable causes it.

    Signed out, the same page shows only how many connectors and problems there are. It never names a service, an account or a client.

  4. Step 4: Add each URL to claude.ai

    In claude.ai open Settings → Connectors, click Add custom connector, paste one URL and leave Advanced settings empty. claude.ai opens the gateway's consent page: type the password and click Allow.

    A cookie remembers that browser for 30 days, so the next accounts take one click each. Repeat for every URL. The connectors then work in claude.ai on the web, on desktop and on mobile.

04.Claude Code

Claude Code with a static token

Claude Code can send a header, so it can skip OAuth. Add another secret to the Worker named CLIENT_TOKEN_<NAME>, for example CLIENT_TOKEN_LAPTOP, holding at least 32 random characters. openssl rand -base64 32 gives you 44. A shorter token is ignored and shows up as a problem on the status page.

.mcp.json
{
  "mcpServers": {
    "resend-work": {
      "type": "http",
      "url": "https://<your-gateway>/m/resend/work/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_GATEWAY_TOKEN}"
      }
    }
  }
}

Claude Code expands ${MCP_GATEWAY_TOKEN} from your environment, cloud sessions included, so the token stays out of the file. Or add the server from the terminal:

terminal
claude mcp add --transport http resend-work \
  https://<your-gateway>/m/resend/work/mcp \
  --header "Authorization: Bearer <token>"

A token opens every connector unless you limit it. To limit it, add a variable with the same name after CLIENT_PATHS_, holding a comma-separated list of connector paths:

Settings → Variables and Secrets

CLIENT_PATHS_LAPTOP
/m/resend/work/mcp,/m/linear/acme/mcp

Without a header, Claude Code can still connect through OAuth: its client metadata document is on the default allowlist, so it opens the same consent page in your browser.

05.Services

Linear, Resend, or any MCP server that takes an API key

Two services are built in. I only list servers I checked accept an API key as a bearer token.

  • Linear

    https://mcp.linear.app/mcp

    Takes a personal API key, sent as Authorization: Bearer. Secret: ACCOUNT_LINEAR_<ACCOUNT>.

  • Resend

    https://mcp.resend.com/mcp

    Takes an API key, sent as Authorization: Bearer. Secret: ACCOUNT_RESEND_<ACCOUNT>.

Any other server

For an MCP server that isn't built in, tell the gateway where it is with plain text variables (not secrets), then add accounts as usual. The name you pick in SERVICE_<NAME>_* is the one you use in ACCOUNT_<NAME>_*.

Settings → Variables and Secrets

SERVICE_EXAMPLE_URL
https://mcp.example.com/mcp
SERVICE_EXAMPLE_HEADER
X-Api-Key
SERVICE_EXAMPLE_NAME
Example
ACCOUNT_EXAMPLE_TEAM
the API key, as a secret
  • SERVICE_<NAME>_URL is the server's MCP endpoint. It must be https. Setting it for LINEAR or RESEND overrides the built-in one.
  • SERVICE_<NAME>_HEADER is optional: the key goes in that header instead of Authorization: Bearer. Letters, digits and hyphens; Host, Content-Length and Cookie are refused.
  • SERVICE_<NAME>_NAME is optional too: the display name on the status and consent pages.

Servers that only accept OAuth, like Vercel's MCP, don't work yet. See Limits.

06.Configuration

Every setting is a variable or a secret on the Worker

Nothing is configured in code, so the fork the Deploy button makes doesn't need editing. Credentials go in as secrets, the rest as plain text variables, both under Settings → Variables and Secrets. The Worker keeps what you set in the dashboard when it is redeployed from your fork (keep_vars is on).

OWNER_PASSWORDSecret · required
The password for the status page and the consent page. Without it nobody can approve a connector.
ACCOUNT_<SERVICE>_<ACCOUNT>Secret
One per account: its API key. Becomes the connector /m/<service>/<account>/mcp.
SERVICE_<NAME>_URLVariable
The MCP endpoint of a service that isn't built in, or an override for a built-in one. https only.
SERVICE_<NAME>_HEADERVariable
The header that carries the API key.
Default: Authorization: Bearer
SERVICE_<NAME>_NAMEVariable
Display name on the status and consent pages.
Default: the built-in name, or the service slug
CLIENT_TOKEN_<NAME>Secret
A static bearer token for a client that can send headers, like Claude Code. At least 32 characters.
CLIENT_PATHS_<NAME>Variable
Comma-separated connector paths the token with the same name may open.
Default: every connector
PUBLIC_URLVariable
Pins the gateway to one origin, such as your own domain. Requests on any other hostname get a plain 404 that doesn't name the right one, so a hard-to-guess hostname stays hard to guess.
Default: the origin of each request
ACCESS_TOKEN_DAYSVariable
How long an OAuth access token lasts.
Default: 30
REFRESH_TOKEN_DAYSVariable
How long a refresh token lasts. It slides: the grant stays alive while the connector is in use.
Default: 180
ALLOWED_CLIENTSVariable
Comma-separated Client ID Metadata Document URLs that may start an authorization.
Default: claude.ai and Claude Code
DYNAMIC_REGISTRATIONVariable
true turns on dynamic client registration (RFC 7591) at /oauth/register. Registered clients may only redirect to claude.ai's callback, or over http to localhost, 127.0.0.1 or [::1].
Default: off

The default ALLOWED_CLIENTS are claude.ai's and Claude Code's metadata documents:

ALLOWED_CLIENTS
https://claude.ai/oauth/mcp-oauth-client-metadata,https://claude.ai/oauth/claude-code-client-metadata

OAuth tokens are bound to the origin they were issued on. If you move the gateway to a new hostname, add the connectors again from there.

07.Security

What keeps your accounts yours

  • One owner, one password

    The status page and the consent page ask for OWNER_PASSWORD, at most 10 attempts a minute per IP. Changing the password signs every browser out.

  • An OAuth token opens one connector

    OAuth tokens carry their connector as audience (RFC 8707). A token for a Resend account doesn't open Linear, nor the other Resend account.

  • Only allowlisted clients

    By default only claude.ai and Claude Code may start an authorization. The check runs before the gateway fetches any client metadata, so nobody can make it fetch a URL of their choice.

  • No open registration

    Dynamic client registration is off by default, so nobody can register a client or use up your KV writes.

  • Long-lived tokens, on purpose

    30 days for access tokens and 180 sliding for refresh tokens, because claude.ai may never refresh a custom connector's token (anthropics/claude-ai-mcp#228). Both can be shortened.

  • Keys stay in secrets

    API keys live only in Worker secrets, never in KV. What the OAuth grants in KV carry is encrypted with a key only the token holder can unwrap.

  • The upstream sees only the request

    It gets the account's key, the MCP request and a short list of headers, never your token or cookies. Your client never sees the upstream's auth challenge or cookies.

  • The public page names nothing

    Signed out, the home page shows only counts. Problem details name the variable at fault, and only after you sign in.

The other side of it. Whoever has the password, or a client token, reaches every account that password or token opens. Treat them like the API keys themselves.

08.Limits

Limits and what isn't there yet

  • Cloudflare's free plan allows 100,000 requests a day and 10 ms of CPU per request. Forwarding uses almost none of that CPU: time spent waiting on the upstream doesn't count. KV allows 1,000 writes a day, and the gateway writes when it issues tokens.
  • Upstreams that only speak OAuth, like Vercel's MCP, aren't supported yet. The gateway needs an API key to send. It's planned.
  • A merged mode isn't there yet either: one URL per service, with an account argument on every tool. Also planned.
  • One owner. There is one password and no user accounts.

If you only need Linear, linear-mcp-gateway, the project this one grew out of, puts every workspace behind a single connection with a workspace argument on each tool.

09.FAQ

Questions

Why not add ?account=work to the URL instead?

Because it depends on the server. Some OAuth servers accept it, others (Google and Zoho, for example) don't, and one report says claude.ai drops the query when it calls the server. A path is part of the URL everywhere.

Do I have to redeploy when I add an account?

No. The gateway reads its configuration from the Worker's environment on every request, so a secret saved in the dashboard is picked up as soon as it is saved.

What happens when an API key is wrong or revoked?

It depends on the server. If it answers 401 or 403, the gateway turns that into a 502 whose message names the service and the account. Some servers, Resend among them, accept the connection and report the bad key in the result of the first tool call instead. Either way, fix the secret in the dashboard; the connector in claude.ai can stay as it is.

Can I put the gateway on my own domain?

Yes: add a custom domain to the Worker in the Cloudflare dashboard, and set PUBLIC_URL to it if you want every other hostname to stop answering. Tokens are bound to the origin, so connectors added under the workers.dev URL have to be added again under the new one.

Does it work with clients other than Claude?

Any client that can send an Authorization header can use a static client token. For OAuth, a client needs a Client ID Metadata Document listed in ALLOWED_CLIENTS, or you turn on DYNAMIC_REGISTRATION.

What does the gateway store?

OAuth clients and grants, in the Worker's KV namespace. API keys stay in secrets, and requests and responses pass through without being stored. Workers Logs is on in the configuration, so your Cloudflare dashboard keeps the usual invocation logs.

How do I cut off access?

Delete an ACCOUNT_ secret and its connector is gone. Change or delete a CLIENT_TOKEN_ to lock that client out. Changing OWNER_PASSWORD signs every browser out of the consent page, but tokens already issued keep working until they expire or their grant is removed from KV.