Distribute · Upstream credentials
Upstream credentials
Two ways an MCP server authenticates to the API behind it: a token per end user through a remote identity provider, or one shared credential used for every caller.
Once a server knows who is calling it, a second question remains: which credential it presents to the API behind it. There are two answers, and a server can use both at once for different upstreams.
- A credential per end user. The Control Plane acts as an OAuth client of the upstream service and holds an access token and refresh token for each person. A Linear tool call runs as that person in Linear.
- One credential for the whole server. An API key held once and used for every caller, supplied as an environment variable on a built server or as an upstream header on a remote or tunneled one.
Per-user credentials require user sessions, since a token has to be stored against a known person. Shared credentials work on any server, including an anonymous public one.
Credentials per end user
Section titled “Credentials per end user”Per-user upstream credentials come from a remote identity provider: an OAuth authorization server such as Linear, GitHub, or Slack that the Control Plane delegates to on the end user’s behalf. Tokens are encrypted at rest and refreshed silently.
Attaching a provider
Section titled “Attaching a provider”Open the server’s Authentication tab, find Remote Identity Providers, and click Attach Provider. The sheet asks for two things:
- Identity Provider, described as the upstream OAuth authorization server Speakeasy delegates to. Either select an existing provider or add a new one by issuer URL, which is validated by fetching its metadata.
- Session Client, described as the OAuth client Speakeasy registers and uses with this provider.
A session client can be created three ways:
| Client type | Use when |
|---|---|
| Dynamic Client Registration | The provider supports DCR and can mint a client automatically |
| Client ID Metadata Document | The provider accepts a hosted metadata URL as the client identity, with no registration step |
| Manual | A client ID and secret already exist, created by hand in the provider’s console |
Providers and clients live at three tiers: project, organization, and platform. A lookup prefers a project-scoped record, then an organization-scoped one, then the platform default, taking the oldest match within a tier. Platform-tier providers are what makes a common service work with no setup at all.
A server can attach one client per identity provider. Two clients for the same issuer on the same server is not a supported configuration.
Attaching a new remote identity provider invalidates every existing consent for that server. Consent is recorded against the exact set of providers in force when it was given, so every connected user is prompted again on their next connection. Plan provider changes the same way as a breaking change.
What the end user sees
Section titled “What the end user sees”Attached providers appear on the consent screen under Connect required services, one card per upstream. Each card shows one of three states:
- Connected, with a check mark.
- Expired, prompting a reconnect.
- Not connected, which is simply the absence of any stored credential rather than a state of its own.
Clicking Connect or Reconnect sends the user through the upstream’s own OAuth flow and returns them to the consent screen. Access cannot be granted until at least one service is connected, and an expired card does not satisfy that requirement.
Expiry is all or nothing. One expired upstream credential fails every tool served through that issuer, not only the tools that need it. Reconnecting restores all of them at once.
Refresh and revocation
Section titled “Refresh and revocation”Refresh happens on demand, just before a token is needed, and is single-flight so that concurrent tool calls do not each mint a new token. A credential that the upstream returns with no expiry is treated as non-expiring, except when a refresh token came with it, in which case it is refreshed roughly hourly.
Stored credentials carry only two statuses, active and expired. Revoking a user’s connection deletes the stored credential, which puts the card back to not connected and forces a reconnect on the next consent screen.
Provider and client records are managed at the organization level in Remote identity providers. Setup walkthroughs for individual services live in the guides.
One credential for the whole server
Section titled “One credential for the whole server”Shared credentials reach the upstream two different ways depending on the backend.
Built servers use an environment attached to the MCP server. Remote and tunneled servers use upstream headers configured on the server itself, encrypted at rest.
Environment variables on built servers
Section titled “Environment variables on built servers”Each variable declared by a source appears in the server’s configuration with one of three modes:
| Mode | Meaning |
|---|---|
| System | Value is stored securely and injected by the system |
| User | User must provide this value when connecting |
| Omit | Variable is not included in the configuration |
Only variables set to System are pulled from the attached environment. A variable set to User becomes part of the install snippet the connecting client has to fill in, which is how a server can be shared without sharing its credentials.
Values are resolved in layers, each overriding the last: the source’s own environment, then the toolset environment, then the environment attached to the MCP server for system variables, then the authenticated caller’s selected environment, then MCP- prefixed request headers, then any OAuth token injection, and finally environment variables passed in the tool call arguments.
A public server does serve its System environment variables to anonymous callers. That is the design, not an oversight. System variables are the server’s own credentials and are used for every caller, so a credential that should not be spent by strangers does not belong on a public server.
Two behaviors catch people out:
- Every system environment variable that is not consumed as a security scheme is still sent on HTTP tool calls, as a header named after the variable in title case with dashes.
MY_CUSTOM_THINGarrives at the upstream asMy-Custom-Thing. - A missing credential is not an error. The request is made without the header and the upstream answers, usually with a 401 that surfaces to the model as a normal tool result.
A related guard: a user-supplied server URL is refused whenever the server has any system environment variable set, which stops a caller from redirecting a credentialed request to a host of their choosing.
Upstream headers on remote and tunneled servers
Section titled “Upstream headers on remote and tunneled servers”Remote and tunneled servers have no toolset and therefore no environment. Shared credentials are configured as headers on the server and applied to every proxied request, last in the chain, so an upstream header named Authorization overrides whatever the proxy resolved.
A header whose configured value resolves to empty removes the header instead of sending a blank one, which is the way to strip a header the client sent rather than replace it.