A reference implementation of a cloud-ready MCP server with full OAuth 2.1 authorization, using the Streamable HTTP transport. The server exposes simple weather tools behind authentication and works with multiple identity providers out of the box.
- MCP spec-compliant auth — serves Protected Resource Metadata (RFC 9728), returns proper
WWW-Authenticatechallenges, validates bearer tokens. - Multi-provider — preconfigured for Keycloak, Google Identity, and Atlassian (or bring any OIDC-compliant provider via
genericmode). - Streamable HTTP transport — the recommended remote MCP transport with session management.
- Cloud-ready — includes Dockerfile + docker-compose for one-command local testing.
- Simple demo tools —
get_weather,get_forecast,get_alerts(swap with real APIs).
┌─────────────┐ OAuth 2.1 ┌───────────────────┐
│ MCP Client │ ◄──── flow ────► │ Identity Provider│
│ (VS Code, │ │ (Keycloak/Google/ │
│ Claude, │ │ Atlassian/etc.) │
│ custom) │ └───────────────────┘
└──────┬──────┘ │
│ Bearer token │ Token
▼ │ introspection
┌──────────────────────────────────────────────────────┐
│ MCP Weather Server │
│ │
│ /.well-known/oauth-protected-resource (public) │
│ POST / — Streamable HTTP MCP endpoint (auth) │
│ GET / — SSE notifications (auth) │
│ DELETE / — Session teardown (auth) │
│ GET /health (public) │
└──────────────────────────────────────────────────────┘
This is the fastest way to see everything working end-to-end.
- Node.js ≥ 20
- Docker Desktop (for Keycloak)
docker run -p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak start-devOpen http://localhost:8080 and log in with admin / admin.
Create the mcp:tools scope:
- Go to Client scopes → Create client scope.
- Name:
mcp:tools, Type: Default, toggle Include in token scope ON. - Inside the scope, go to Mappers → Configure a new mapper → Audience.
- Name:
audience-config, Included Custom Audience:http://localhost:3000.
Allow dynamic client registration:
- Go to Clients → Client registration → Trusted Hosts.
- Disable Client URIs Must Match, add your host IP (check Keycloak logs for the IP).
Create a server client (for token introspection):
- Go to Clients → Create client.
- Client ID:
mcp-server, click Next. - Enable Client authentication, click Next → Save.
- Go to Credentials tab, copy the Client Secret.
# Clone / extract the project
cd mcp-weather-oauth
# Install dependencies
npm install
# Create .env from the Keycloak template
cp .env.keycloak .env
# Edit .env — paste your OAUTH_CLIENT_SECRET
# Build and start
npm run build
npm startYou should see:
┌──────────────────────────────────────────────────┐
│ MCP Weather Server with OAuth 2.1 │
├──────────────────────────────────────────────────┤
│ URL: http://localhost:3000
│ Provider: keycloak
│ Issuer: http://localhost:8080/realms/master/
│ Metadata: http://localhost:3000/.well-known/oauth-protected-resource
└──────────────────────────────────────────────────┘
- Press
Cmd+Shift+P→ MCP: Add server… → HTTP. - Enter
http://localhost:3000. - Give it a name (e.g.
weather). - VS Code will open Keycloak login in your browser — log in and grant access.
- You should see the weather tools listed in the MCP panel.
- Go to Google Cloud Console → Credentials.
- Create OAuth Client ID → type Web application.
- Add
http://localhost:3000to Authorized redirect URIs. - Copy the Client ID and Secret.
cp .env.google .env
# Edit .env with your Google Client ID and Secretnpm run build && npm startThe server will advertise Google's authorization endpoints in its Protected Resource Metadata. When an MCP client connects, it will be redirected to Google's consent screen.
- Go to Atlassian Developer Console.
- Create a new app → OAuth 2.0 (3LO).
- Set callback URL:
http://localhost:3000/callback. - Copy Client ID and Secret.
cp .env.atlassian .env
# Edit .env with your Atlassian credentialsnpm run build && npm startAtlassian doesn't provide a token introspection endpoint, so the server falls back to local JWT validation (decoding + expiry + audience check).
Set OAUTH_PROVIDER=generic and supply all endpoints explicitly:
OAUTH_PROVIDER=generic
OAUTH_ISSUER=https://your-idp.example.com
OAUTH_AUTHORIZATION_ENDPOINT=https://your-idp.example.com/authorize
OAUTH_TOKEN_ENDPOINT=https://your-idp.example.com/token
OAUTH_INTROSPECTION_ENDPOINT=https://your-idp.example.com/introspect
OAUTH_JWKS_URI=https://your-idp.example.com/.well-known/jwks.json
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secretdocker build -t mcp-weather-oauth .
docker run -p 3000:3000 --env-file .env mcp-weather-oauth# Make sure .env exists with your config
docker compose upThe Dockerfile is a standard multi-stage Node.js build. Deploy to any container service:
- AWS ECS / Fargate — push the image to ECR, create a task definition.
- Google Cloud Run —
gcloud run deploy --source . - Azure Container Apps —
az containerapp up --source . - Fly.io —
fly launch
Remember to:
- Set
SERVER_URLto your public URL (e.g.https://mcp-weather.fly.dev). - Set
HOST=0.0.0.0so the server binds to all interfaces. - Update the Keycloak audience / redirect URIs to match the public URL.
- Use HTTPS in production — never accept tokens over plain HTTP.
mcp-weather-oauth/
├── src/
│ ├── index.ts # Express app, MCP server, route wiring
│ ├── config.ts # Env-based config with provider presets
│ └── token-verifier.ts # Token validation (introspection + JWT)
├── .env.example # Generic env template
├── .env.keycloak # Keycloak-specific template
├── .env.google # Google Identity template
├── .env.atlassian # Atlassian template
├── Dockerfile # Multi-stage production build
├── docker-compose.yml # Local dev: Keycloak + server
├── tsconfig.json
├── package.json
└── README.md
The MCP authorization flow follows OAuth 2.1 (authorization code + PKCE):
- Client connects → Server returns
401 Unauthorizedwith aWWW-Authenticateheader pointing to/.well-known/oauth-protected-resource. - Client fetches PRM (Protected Resource Metadata) → learns which authorization server to use and what scopes are available.
- Client discovers auth server → fetches the OIDC/OAuth metadata from the authorization server.
- Client registers (if DCR is supported) or uses pre-registered credentials.
- User authenticates → browser-based login + consent at the identity provider.
- Client gets tokens → authorization code exchanged for access + refresh tokens.
- Client calls MCP → sends the access token in
Authorization: Bearer <token>. - Server validates → introspection or JWT verification, audience check.
| Tool | Description |
|---|---|
get_weather |
Current weather for a city (temp, condition, humidity, wind) |
get_forecast |
3-day forecast |
get_alerts |
Active weather alerts |
The weather data is simulated (deterministic based on city name). Replace simulateWeather() with calls to a real API like OpenWeatherMap for production use.
| Variable | Default | Description |
|---|---|---|
OAUTH_PROVIDER |
keycloak |
Provider preset: keycloak, google, atlassian, generic |
HOST |
localhost |
Server bind address |
PORT |
3000 |
Server port |
SERVER_URL |
http://{HOST}:{PORT} |
Public-facing URL (set for cloud deploys) |
AUTH_HOST |
localhost |
Keycloak host |
AUTH_PORT |
8080 |
Keycloak port |
AUTH_REALM |
master |
Keycloak realm |
OAUTH_CLIENT_ID |
mcp-server |
OAuth client ID for introspection |
OAUTH_CLIENT_SECRET |
— | OAuth client secret |
OAUTH_SCOPES |
mcp:tools |
Comma-separated scopes |
OAUTH_ISSUER |
— | (generic) Token issuer URL |
OAUTH_AUTHORIZATION_ENDPOINT |
— | (generic) Authorization URL |
OAUTH_TOKEN_ENDPOINT |
— | (generic) Token URL |
OAUTH_INTROSPECTION_ENDPOINT |
— | (generic) Introspection URL |
OAUTH_JWKS_URI |
— | (generic) JWKS endpoint |
MIT