Skip to content

Remote MCP, OAuth, and Enterprise Auth

Protected remote MCP: authorization, then tool traffic

  1. protected-resource metadata → authorization-server metadata

    Find the resource's authorization server, then its endpoints and supported features.

  2. pre-registration · client metadata document · optional registration

    Use a client identification method the authorization server supports; dynamic registration is optional.

  3. authorization code request + PKCE challenge + resource + state

    The user signs in with the authorization server and consents to the requested access.

  4. POST token endpoint: code + verifier + resource

    The authorization server validates the code and PKCE verifier before issuing an access token.

  5. initialize request → initialize response → initialized notification → tools/list

    MCP requests carry the bearer token. Tool visibility may vary; permission checks still run on calls.

  6. POST /mcp: JSON-RPC tools/call request

    The HTTP response can be JSON or an SSE stream. SSE is not required for every call.

The client talks to an authorization server for consent and tokens, and to the MCP resource server for tools. These may be separate services. Authorization is optional in MCP; this example uses it.

The previous MCP post introduced tools, resources, prompts, and a JSON-RPC exchange with a local subprocess. A remote server keeps that protocol while changing who operates the service and how clients connect to it.

This post follows a protected MCP server over HTTP: transport, authorization, token handling, and permission checks. HTTP authorization is optional in MCP; the OAuth flow applies when the server requires delegated access.


Hosting MCP over HTTP

Local stdio connects a host to a child process. Installing a binary does not make it trustworthy, and a subprocess is not automatically sandboxed: absent isolation, it commonly inherits the user’s permissions and configured credentials.

A stdio server can call cloud APIs, including GitHub or a company service. Hosting the MCP server remotely is a different deployment choice: it centralizes operation, updates, and access control rather than requiring every client to run an adapter.

A hosted MCP server can support:

  • Multi-tenancy. One server, many users, each seeing only their own data.
  • Hosted SaaS. The tool is owned by a vendor (GitHub, Atlassian, Linear). You don't run their backend; you call it.
  • Serverless tools. The operator wants scale-to-zero. Spinning up a local subprocess per request isn't the right shape.
  • Enterprise control. Security teams want to revoke access centrally, audit every call, and enforce scopes. A hosted server makes these controls easier to centralize, though local adapters can also use scoped and revocable cloud credentials.

The same JSON-RPC exchange over two transports

Both examples start after initialization. An empty tool list is a valid response.

Local subprocess · stdio

Host launches: python calc_server.py

stdin → {"jsonrpc":"2.0","id":1,"method":"tools/list"}
stdout ← {"jsonrpc":"2.0","id":1,"result":{"tools":[]}}

One JSON object per line.

The host manages a process and its pipes. Credentials and sandbox permissions are deployment choices; the process can itself call remote APIs.

Remote endpoint · Streamable HTTP

POST /mcp
Authorization: Bearer <access token>
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-11-25

{"jsonrpc":"2.0","id":1,"method":"tools/list"}

HTTP/1.1 200 OK
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}

This protected endpoint requires a token. If initialization issued a session ID, include MCP-Session-Id too. The server can return JSON or an SSE stream; authorization is optional in MCP.

Same JSON-RPC envelopes, different pipes. stdio wraps them in OS pipes. Streamable HTTP wraps them in POST bodies, with SSE for streaming responses. The MCP layer above the transport doesn't change.

Prereqs

MCP: A Cross-System Standard for Tool Integration introduces the JSON-RPC methods used here, including tools/list, tools/call, and resources/read.

In function calling as structured generation, the model emits a tool_use block, the host's runtime catches it, and something runs the tool. In the local-stdio case, that "something" is a subprocess. In the remote case, it's an HTTP request to a server we've authenticated against.


Two transports, same protocol

The MCP spec defines two transports. stdio is what we covered last post: a server launched as a subprocess, JSON-RPC over its stdin/stdout. Streamable HTTP carries those messages over HTTP:

  • Client-to-server is an ordinary HTTP POST with a JSON-RPC envelope in the body. Single request, single response, for most calls.
  • Server-to-client streaming uses Server-Sent Events. When a tool call will take a while, or when the server wants to send progress notifications, the server can respond with Content-Type: text/event-stream and the server emits a sequence of event: message frames, each containing a JSON-RPC object.

Streamable HTTP supports JSON or SSE responses to POST requests and an optional GET stream for server messages. SSE uses ordinary HTTP, but proxy buffering and idle timeouts still need configuration. Clients must send the required Accept headers and handle session and protocol-version headers when applicable.

The MCP methods, schemas, and tool definitions remain the same over either transport. Moving a server to HTTP also requires handling network sessions and, for protected resources, authorization.


The auth model: scoped bearer tokens

Once MCP is on HTTP, the authentication question is the same one every other HTTP API faces: how do we know which user is making this call, and what are they allowed to do?

For a server that supports MCP authorization, the client sends an access token in the Authorization: Bearer header. The server validates the token’s issuer, audience, expiry, and permissions. Authentication of a token and authorization of a particular operation are separate checks.

For interactive access, the client obtains that token through the OAuth authorization-code flow with PKCE.


OAuth 2.1 + PKCE: obtaining authorization

OAuth is about delegated access. You have an account on GitHub. You want Claude Code to act on your GitHub account without handing it your password. OAuth is the protocol that lets you grant it a scoped, revocable, time-limited capability to do that.

The MCP authorization framework builds on OAuth 2.1 and related standards. The interactive flow binds the authorization request to token exchange with PKCE:

Discovery. The client finds the MCP server’s protected-resource metadata, often through a resource_metadata pointer in a 401 challenge. That metadata identifies authorization servers. The client then retrieves the selected issuer’s OAuth or OpenID Connect metadata. The authorization server may be on a different origin from the MCP server.

Client identity. Depending on the deployment, the client uses preregistration, a supported client ID metadata document, or dynamic client registration. Dynamic registration is optional, so a client cannot assume that every issuer exposes a registration endpoint.

PKCE setup. The client generates a random code_verifier (a secret it keeps), computes code_challenge = BASE64URL(SHA256(code_verifier)), and remembers both. At token exchange, this verifier binds the code to the client that initiated authorization, including public clients that cannot keep a client secret.

PKCE · binding the code to the verifier · 3. token exchange
client memory (private)
code_verifier (secret)
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
challenge = BASE64URL(SHA256(verifier))
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
abbreviated wire fields (other OAuth parameters omitted)
// legit client POSTs verifier + code
POST /token code=auth_9k1z code_verifier=dBjftJeZ4CVP...
// attacker has the code but not the verifier
POST /token code=auth_9k1z code_verifier=?????
server compares BASE64URL(SHA256(verifier)) to challenge
legit client
BASE64URL(SHA256(dBjftJeZ4C...)) = E9Melhoa2OwvFrEM...
match ✓ token issued
attacker
BASE64URL(SHA256(GUESS-I-DO...)) = mDEqbGIJ7kNpfjCXN9Ly...
mismatch ✗ 400 invalid_grant

At token exchange, the server hashes the supplied verifier and compares it with the challenge from authorization. The intercepted code cannot be redeemed with the attacker's different verifier.

Authorize. The client opens the user's browser to the authorize endpoint with the challenge, requested scopes, redirect URI, a fresh state, and the MCP resource URI. The client verifies the returned state and redirect before accepting a code. The user signs in with the authorization server, sees the scopes spelled out, and clicks allow.

Redirect with code. The authorization server redirects the browser to the client's registered redirect URI with a short-lived code. Redeeming it requires the matching verifier.

Token exchange. The client POSTs the code, the MCP resource URI, and the original code_verifier to the token endpoint. The server checks that BASE64URL(SHA256(verifier)), without padding, matches the challenge it saw at authorize time, and if it does, issues an access_token (and possibly a refresh_token).

Authenticated calls. From here on, the client just sticks Authorization: Bearer <token> on every MCP request.

OAuth 2.1 · authorization code + PKCE
CLIENT · 0. generate PKCE pair
code_verifier  = random_urlsafe(64)
code_challenge = BASE64URL(SHA256(code_verifier))
Client invents a one-time secret (verifier) and a hash of it (challenge). Only the hash goes out first. The verifier proves the client is the same one that started the flow.

PKCE binds this authorization code to the client’s verifier. A stolen code alone is insufficient to redeem the token; the authorization server also checks the verifier. Redirect validation and the other OAuth checks still apply.

Follow which messages go to the MCP resource server and which go to its authorization server.

The native-client example below uses a loopback redirect: the client listens on a loopback address (http://127.0.0.1:<random_port>/cb) and the browser redirects there. RFC 8252 is the spec for this pattern, and it is one documented native-app redirect option. Some constrained clients can't run a local server; some can use the device code grant (RFC 8628) when the issuer and integration support it, where the client displays a user code and the user types it into a separate browser.


The runtime: what actually happens at call time

Walk through an illustrative session. The server and scope-to-tool policy here are examples, not GitHub’s published permission mapping. You open Claude Code in a repo and type "file an issue on the dependency upgrade branch."

  1. Claude Code needs a remote MCP server for GitHub. It checks its config, sees https://github.mcp.example.com, and discovers its protected-resource metadata and then the advertised authorization server.
  2. No access token yet, so Claude Code starts the authorization flow. Your browser pops open, you see "Claude Code wants repo, read:user access on GitHub," you click allow. A loopback redirect delivers the code, Claude Code exchanges it for a token. The client stores the resulting credentials securely and handles expiry or revocation.
  3. After initialization, Claude Code calls tools/list with Authorization: Bearer <token>. In this example, the server filters the response to tools available under the token’s scopes. create_issue is in there. delete_repo is not, because you didn't grant admin.
  4. The model emits a tool_use for create_issue. The runtime packs that into a JSON-RPC tools/call, POSTs it to /mcp. The server streams back progress events over SSE while it talks to the GitHub API, then sends a final result.
  5. The runtime appends the tool result to the transcript and calls the model again. The model writes a sentence to you. stop_reason = end_turn. Done.

The authenticated session runs the MCP lifecycle inside the agent loop from earlier in the arc. Tokens expire, can be revoked, and may need additional consent for broader scopes.


Scopes and the trust boundary

With a remote MCP server, the client presents a token for a particular protected resource. The server enforces permissions on every operation. Filtering tools/list by scope can improve usability, but it is a server policy, not the authorization boundary; an unlisted or previously listed tool must still reject unauthorized calls.

example server scope policy · scope: read:user
tools/list response (filtered by token scope)
get_user
scope: read:user
read
search_repos
scope: read:user
read
get_issue
scope: repo
read
list_pull_requests
scope: repo
read
create_issue
scope: repo
write
merge_pr
scope: repo
write
delete_repo
scope: admin
destructive
transfer_org
scope: admin
destructive
This example grant exposes only the two read-only tools.

This toy server filters tools/list by scope. Filtering is optional; every invocation still requires server-side authorization. The scope names and mappings here are illustrative, not GitHub’s permission specification.

Central token issuance, revocation, and audit logs can help an organization manage access. They still need implementation: self-contained tokens may remain usable until expiry unless the resource server checks revocation or another policy signal.


Enterprise auth, briefly

An enterprise deployment must connect MCP authorization to its identity and access policies. Possible integration points include:

  • SSO via OIDC. The authorization server integrates sign-in with the company's identity provider (Okta, Entra, Google Workspace). The user's corporate account is the one granting the scopes. Deprovisioning must be connected to session and token revocation; already-issued tokens do not necessarily stop working immediately.
  • SAML federation. Same idea, older protocol. Still common in big enterprises.
  • Service accounts. For server-to-server, non-interactive callers (a CI job calling an MCP-backed deploy tool), you want a client-credentials-style grant or signed JWTs, not an interactive OAuth flow.
  • API keys. A possible choice for a single-user server. Key systems can support scopes and rotation, but delegated user consent and identity-provider integration require additional design.

The core MCP authorization contract covers discovery, audience binding, token handling, and interactive delegation. Enterprise identity policies and optional authorization extensions supply additional behavior. Check the version and extensions supported by both peers.


Implementation sketch

A complete OAuth client needs discovery, secure callback handling, state validation, token storage, refresh, and audience binding. The official SDK’s OAuth examples cover those pieces. Here is the smaller transport half after a valid token has been obtained for this server; it lists tools without invoking a mutating operation.

# pip install 'mcp>=1,<2' httpx
import asyncio
import os
import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
 
async def main():
    endpoint = os.environ["MCP_URL"]  # HTTPS endpoint for the protected resource
    access_token = os.environ["MCP_ACCESS_TOKEN"]  # issued for this resource
    async with httpx.AsyncClient(
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=30.0,
    ) as http:
        async with streamable_http_client(endpoint, http_client=http) as streams:
            read, write, _ = streams
            async with ClientSession(read, write) as session:
                # Includes initialize + notifications/initialized.
                await session.initialize()
                result = await session.list_tools()
                print([tool.name for tool in result.tools])
 
asyncio.run(main())

The SDK handles JSON versus SSE responses, message correlation, and the MCP lifecycle. The caller must supply a valid token and handle refresh.


Misconceptions

"OAuth setup repeats on every tool call." Consent and token acquisition happen outside the model’s tool loop. The client then reuses a valid access token and refreshes or reauthorizes when necessary. Discovery helps interoperability, but client registration and organizational policy still vary.

"API keys and OAuth solve the same problem." Keys can be appropriate for a service or personal integration. OAuth standardizes delegated access and consent. Choose based on who grants access, which resource receives the token, and how credentials are scoped and revoked.

"The model handles auth." The model emits a tool_use block. The host's MCP client is the one that holds the token, attaches the bearer header, handles refresh, and hides the whole thing from the model. The model may see a filtered tool list, but the resource server must enforce permissions even if the model requests a forbidden call.

"Remote MCP means the tool runs in the cloud." Not necessarily. A remote MCP server is just an HTTP endpoint that speaks MCP. What happens behind that endpoint is up to the operator. An HTTP server could forward authenticated work to a local agent on the user’s laptop or to a dedicated VM. The transport says nothing about where the work runs.

What's next

The next post covers planning and reasoning in agent loops: how intermediate work, reasoning budgets, and explicit search affect the choice and ordering of tool calls.


Additional reading (and watching)