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.
- 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. - 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.
- The query string trick is unreliable. Adding
?account=workmakes 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
Your gateway
The real MCP server
- 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.
- The gateway checks the token belongs to this connector. A token issued for
/m/resend/work/mcpdoesn't open/m/linear/acme/mcp, nor the personal Resend account. - 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 andUser-Agentgo through. - The response streams back as it arrives, SSE included, without the upstream's
WWW-Authenticatechallenge 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.
Step 1: Deploy the Worker
The button forks
apps/gatewayinto your GitHub or GitLab account, creates the KV namespace that holds the OAuth grants, asks for theOWNER_PASSWORDsecret, 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.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_WORKworks,ACCOUNT_RESEND_MY_WORKdoesn't. The connector URL uses them in lowercase. A new secret takes effect without a redeploy.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.
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.
{
"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:
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>_URLis the server's MCP endpoint. It must be https. Setting it forLINEARorRESENDoverrides the built-in one.SERVICE_<NAME>_HEADERis optional: the key goes in that header instead ofAuthorization: Bearer. Letters, digits and hyphens;Host,Content-LengthandCookieare refused.SERVICE_<NAME>_NAMEis 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
trueturns on dynamic client registration (RFC 7591) at/oauth/register. Registered clients may only redirect to claude.ai's callback, or over http tolocalhost,127.0.0.1or[::1].- Default: off
The default ALLOWED_CLIENTS are claude.ai's and Claude Code's metadata documents:
https://claude.ai/oauth/mcp-oauth-client-metadata,https://claude.ai/oauth/claude-code-client-metadataOAuth 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
accountargument 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.