Skip to main content

MCP OAuth 2.0 authentication

How to protect MCP servers with OAuth 2.0 using Catalyst's middleware pipeline.

Overview

The MCP specification does not mandate authentication between an MCP client and server. Without authentication, any caller that can reach your MCP server can invoke its tools.

Catalyst addresses this through two distinct middleware components that you compose on the HTTP pipeline of an App ID. They do different things:

MiddlewareWhat it doesDirectionUse it on
middleware.http.oauth2Acquires an OAuth 2.0 access token from a provider and attaches it to the outbound request as a bearer header.Outbound (client-side)httpPipeline on the MCP client's App ID, when calling a server that requires a token.
middleware.http.bearerValidates an inbound JWT — signature, iss, aud — against a JWKS endpoint and rejects requests with missing or invalid tokens.Inbound (server-side)appHttpPipeline on the MCP server's App ID, when only authenticated callers should reach it.

The two are complementary. The client side ensures your MCP client carries a token; the server side ensures incoming requests carry a valid one. Either can be applied independently, and both layer cleanly with App ID access policies for defense in depth.

Client-side authentication

In client-side authentication, Dapr's OAuth2 middleware intercepts outbound requests from the MCP client's sidecar, acquires a token from the OAuth2 provider, and attaches it to the request before it reaches the MCP server.

Step 1 — Register the MCP server as an HTTPEndpoint

Create an HTTPEndpoint resource that tells Catalyst about the remote MCP server. This allows Catalyst's service invocation layer to govern and route calls to it.

# components/mcp-server-endpoint.yaml
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: mcp-server
spec:
baseUrl: https://my-mcp-server.example.com
headers:
- name: Accept
value: text/event-stream

Step 2 — Define the OAuth2 middleware component

# components/oauth2.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: oauth2
spec:
type: middleware.http.oauth2
version: v1
metadata:
- name: clientId
value: "<your-client-id>"
- name: clientSecret
secretKeyRef:
name: oauth-secret
key: clientSecret
- name: authURL
value: "https://auth.example.com/authorize"
- name: tokenURL
value: "https://auth.example.com/token"
- name: scopes
value: "mcp:read,mcp:write"

Note: Store sensitive values like clientSecret using Catalyst secret references rather than inline plaintext.

Step 3 — Create the Configuration resource

The Configuration resource tells Catalyst to apply the OAuth2 middleware to outbound HTTP requests from this app (via httpPipeline):

# config/oauth2-client.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: oauth2-client-config
spec:
httpPipeline:
handlers:
- name: oauth2
type: middleware.http.oauth2

Apply the configuration to your project:

diagrid apply --project my-project \
-f components/mcp-server-endpoint.yaml \
-f components/oauth2.yaml \
-f config/oauth2-client.yaml

Step 4 — Connect the MCP client through Catalyst

In your MCP client code, connect through the Catalyst sidecar's service-invocation API. Catalyst handles the OAuth2 token injection automatically:

import os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

DAPR_HTTP_ENDPOINT = os.getenv("DAPR_HTTP_ENDPOINT", "http://localhost:3500")
MCP_URL = f"{DAPR_HTTP_ENDPOINT}/v1.0/invoke/mcp-server/method/mcp"

async def main():
async with streamablehttp_client(url=MCP_URL) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Available tools:", tools)

Step 5 — Run the MCP client app

diagrid dev run \
--project my-project \
--app-id mcp-client \
--config ./config/oauth2-client.yaml \
--resources-path ./components \
-- python mcpclient.py

Catalyst starts the OAuth2 pipeline before the first request to the MCP server. Subsequent requests reuse the cached token until it expires.


Server-side validation (bearer middleware)

To require that every inbound request to the MCP server carries a valid OAuth 2.0 token, attach middleware.http.bearer to the appHttpPipeline of the MCP server's App ID. The middleware validates the token's signature, issuer, and audience against a JWKS endpoint. Requests with a missing or invalid token receive 401 Unauthorized before reaching server code.

This is the pattern walked through end-to-end in the Authentication tutorial.

# components/bearer.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: bearer-validator
spec:
type: middleware.http.bearer
version: v1
metadata:
- name: jwksURL
value: "https://auth.example.com/.well-known/jwks.json"
- name: audience
value: "mcp-server"
- name: issuer
value: "https://auth.example.com"
# config/bearer-server.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: bearer-server-config
spec:
appHttpPipeline:
handlers:
- name: bearer-validator
type: middleware.http.bearer

Apply and attach to the MCP server's App ID:

diagrid apply -f components/bearer.yaml -f config/bearer-server.yaml
diagrid appid update mcp-server --app-config bearer-server-config

Combine with App ID access policies for defense in depth: access policies decide which callers may reach the server; bearer validation insists they present a live, signed token.


Choosing the right approach

RequirementPattern
MCP client must present a token when calling a remote MCP servermiddleware.http.oauth2 on httpPipeline of the client App ID
MCP server must reject unauthenticated callersmiddleware.http.bearer on appHttpPipeline of the server App ID
Layered security (caller identity + access policy + valid token)Pair middleware.http.bearer with a Configuration accessControl rule on the server App ID

See also