Sample implementation of a Model Context Protocol (MCP) server protected by OAuth 2.1, written in Go.
This project demonstrates how to implement an MCP server with OAuth protection using the MCP Go SDK and Keycloak as the authorization server.
- OAuth 2.0 Protected Resource: Implements RFC 9728 (OAuth 2.0 Protected Resource Metadata)
- JWT Access Token Validation: Local validation using JWKS
- Streamable HTTP Transport: Remote-accessible MCP server
- Simple Echo Tool: Basic MCP tool for demonstration
- Keycloak Integration: Uses Keycloak 26.4 as authorization server with Dynamic Client Registration (DCR)
OAuth 2.1 Flow (DCR, Authorization Code)
┌───────────────────────────────────────────────────────────┐
│ │
│ ▼
┌───┴─────────┐ HTTP + Bearer Token ┌─────────────┐ ┌───────────────┐
│ MCP Client │────────────────────────►│ MCP Server │ │ Keycloak │
│ (Inspector) │ │ (This repo) │ │ (AuthZ Server)│
└─────────────┘ └─────────────┘ └───────────────┘
│ │
│◄──────────────────┘
│ JWKS (RS256 Public Key)
│
▼
JWT Access Token Validation:
• Signature verification
• iss, exp, aud claims
• scope claim
- Go 1.25 or later
- Docker & Docker Compose (for running Keycloak)
cd authz-server
docker-compose up -dKeycloak will be available at http://localhost (admin/admin).
- Create a new realm named
demo - Create a client scope named
mcp:tools:- Include in token scope:
On - Add Audience mapper:
- Name:
audience-config - Included Custom Audience:
http://localhost:8000
- Name:
- Include in token scope:
- Configure Client Policies:
- Delete the default "Trusted Hosts" policy
- Update "Allowed Client Scopes" policy to include
mcp:tools
- Create a test user
go run . \
-authz-server-url="http://localhost/realms/demo" \
-jwks-url="http://localhost/realms/demo/protocol/openid-connect/certs" \
-resource-url="http://localhost:8000"Run MCP Inspector and connect to http://localhost:8000.
The MCP Inspector will:
- Fetch Protected Resource Metadata from
http://localhost:8000/.well-known/oauth-protected-resource - Discover authorization server metadata
- Register as a client using Dynamic Client Registration (DCR)
- Initiate OAuth authorization code flow
- Redirect you to Keycloak login page
- Obtain access token and connect to MCP server
.
├── authz-server/ # Keycloak setup
│ ├── docker-compose.yml
│ └── nginx.conf
├── main.go # MCP server implementation
├── oauth_middleware.go # OAuth middleware & JWT Access Token validation
└── README.md
The server exposes metadata at /.well-known/oauth-protected-resource:
{
"resource": "http://localhost:8000",
"authorization_servers": ["http://localhost/realms/demo"],
"scopes_supported": ["mcp:tools"]
}The middleware validates:
- Signature: Using JWKS from authorization server (RS256)
- Standard Claims:
iss(issuer): Must match authorization server URLexp(expiration): Token must not be expiredaud(audience): Must include this server's URL
- Custom Claims:
scope: Must includemcp:tools
Provides a simple echo tool that returns the input message.
| Flag | Description | Default |
|---|---|---|
-authz-server-url |
Authorization server URL | http://localhost/realms/demo |
-jwks-url |
JWKS endpoint URL | http://localhost/realms/demo/protocol/openid-connect/certs |
-resource-url |
This server's URL | http://localhost:8000 |
Keycloak 26.4 does not yet support RFC 8707 (Resource Indicators for OAuth 2.0). As a workaround, this implementation uses an Audience Mapper in the mcp:tools scope to set the aud claim.
Due to a known issue in Keycloak 26.4, nginx is used as a reverse proxy to add CORS headers for the DCR endpoint.
- MCP Specification 2025-06-18 - Authorization
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 9068: OAuth 2.0 Access Token in JWT Format
- RFC 8707: Resource Indicators for OAuth 2.0
MIT