In this post we list the steps required to setup MCP server with Google oAuth on a EC2 server.
We use the standard method for authentication, which means that MCP clients like Claude and VSCode can used this.
Notice this implemention does not use Dynamic Client Registration, so we will manually get the client ID from the server.
I've spent a very long time on the python code due to some bugs and a very poor documentation on FastMCP.
It make the implementation not easy, be prepared!
1. Prepare The Server
Create an EC2 Instance
- instance type: t3.medium
- Open ingress ports: 80, 443, 22
- Assign elastic IP to the EC2 instance
Add DNS Entry
2. Simple MCP Server Update
Install Docker
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker ec2-user
newgrp docker
Install Python
python3.12 -m pip install --upgrade pip
python3.12 -m venv venv
source ~/venv/bin/activate
pip3.12 install mcp fastapi uvicorn pyjwt cryptography httpx authlib python-dotenv itsdangerous pyjwt
Basic Server Code
Create app.py
app = FastAPI(
title="Example MCP Server"
)
@app.get("/health")
def health():
return {
"status": "ok"
}
@app.get("/")
def root():
return {
"service": "mcp-server"
}
Uvicorn as Service
sudo vi /etc/systemd/system/mcp-server.service
Description=MCP FastAPI Server
After=network.target
[Service]
User=ec2-user
Group=ec2-user
WorkingDirectory=/home/ec2-user
Environment="PATH=/home/ec2-user/venv/bin"
RestartSec=5
[Install]
WantedBy=multi-user.target
Then run as service
sudo systemctl enable mcp-server
sudo systemctl start mcp-server
Check status and logs
Nginx
sudo systemctl enable nginx
sudo systemctl start nginx
sudo vi /etc/nginx/conf.d/mcp.conf
server {
server_name mcp-auth.alonana.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Apply to NGINX
sudo systemctl reload nginx
TLS with Let's Encrypt
sudo dnf install certbot python3-certbot-nginx -y
Request certificate
sudo certbot --nginx -d mcp-auth.alonana.com
check TLS is working and ports are allowed, run from your laptop
curl https://mcp-auth.alonana.com/health
3. OAuth MCP Server Update
Google OAuth Creation
GOOGLE_CLIENT_SECRET=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
Update the service
sudo vi /etc/systemd/system/mcp-server.service
add after the WorkingDirectoty
EnvironmentFile=/home/ec2-user/.env
Reload the service
sudo systemctl restart mcp-server
OAuth implementation
Wow, I mean that was very difficult.
We have FixIssuerTrailingSlashMiddleware as a bypass to FastMCP bug.
We have GoogleAuthServerProvider as a our own wrapping over Google OAuth.
We have TransportSecuritySettings to overcome blocking hosts redirection.
We have "app.mount" with empty argument to enable mcp with oAuth.
Each of this was hard to find and documentation is very bad.
vi app.py
import os
import json
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from authlib.integrations.starlette_client import OAuth
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
import secrets
import time
import httpx
from mcp.server.auth.provider import (
OAuthAuthorizationServerProvider,
AuthorizationParams,
AuthorizationCode,
AccessToken,
RefreshToken,
)
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
WELL_KNOWN_PATHS = {
"/.well-known/oauth-authorization-server",
"/.well-known/oauth-protected-resource",
}
class FixIssuerTrailingSlashMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
if request.url.path not in WELL_KNOWN_PATHS:
return response
body = b""
async for chunk in response.body_iterator:
body += chunk
try:
data = json.loads(body)
if isinstance(data.get("issuer"), str):
data["issuer"] = data["issuer"].rstrip("/")
if isinstance(data.get("resource"), str):
data["resource"] = data["resource"].rstrip("/")
if isinstance(data.get("authorization_servers"), list):
data["authorization_servers"] = [u.rstrip("/") for u in data["authorization_servers"]]
body = json.dumps(data).encode()
except (json.JSONDecodeError, AttributeError):
pass
headers = {k: v for k, v in response.headers.items() if k.lower() != "content-length"}
return Response(content=body, status_code=response.status_code, headers=headers, media_type="application/json")
class GoogleAuthServerProvider(OAuthAuthorizationServerProvider):
def __init__(self, client_id: str, client_secret: str, base_url: str):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url # e.g. https://mcp-auth.alonana.com
self.clients: dict[str, OAuthClientInformationFull] = {}
self.mcp_auth_codes: dict[str, AuthorizationCode] = {}
self.mcp_tokens: dict[str, AccessToken] = {}
# correlates our internal "state" with the original MCP request
self.pending: dict[str, dict] = {}
# --- Dynamic Client Registration: MCP clients self-register here ---
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
return self.clients.get(client_id)
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
self.clients[client_info.client_id] = client_info
# --- Step 1: MCP client hits /authorize, we bounce to Google ---
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
state = secrets.token_urlsafe(32)
self.pending[state] = {
"client_id": client.client_id,
"redirect_uri": str(params.redirect_uri),
"code_challenge": params.code_challenge,
"scopes": params.scopes or ["mcp:tools"],
"mcp_state": params.state,
}
google_params = {
"client_id": self.client_id,
"redirect_uri": f"{self.base_url}/oauth/google/callback",
"response_type": "code",
"scope": "openid email profile",
"state": state,
"access_type": "online",
"prompt": "select_account",
}
return f"{GOOGLE_AUTH_URL}?{httpx.QueryParams(google_params)}"
# --- Step 3: MCP client exchanges our code for an MCP token ---
async def load_authorization_code(self, client, authorization_code: str) -> AuthorizationCode | None:
return self.mcp_auth_codes.get(authorization_code)
async def exchange_authorization_code(self, client, authorization_code: AuthorizationCode) -> OAuthToken:
token = secrets.token_urlsafe(32)
self.mcp_tokens[token] = AccessToken(
token=token,
client_id=client.client_id,
scopes=authorization_code.scopes,
expires_at=int(time.time()) + 3600,
)
del self.mcp_auth_codes[authorization_code.code]
return OAuthToken(access_token=token, token_type="bearer", expires_in=3600,
scope=" ".join(authorization_code.scopes))
async def load_access_token(self, token: str) -> AccessToken | None:
access = self.mcp_tokens.get(token)
if access and access.expires_at and access.expires_at < time.time():
del self.mcp_tokens[token]
return None
return access
async def load_refresh_token(self, client, refresh_token: str) -> RefreshToken | None:
return None # keep it simple: no refresh tokens for now
async def exchange_refresh_token(self, client, refresh_token, scopes):
raise NotImplementedError
async def revoke_token(self, token) -> None:
self.mcp_tokens.pop(token, None)
load_dotenv()
auth_provider = GoogleAuthServerProvider(
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
base_url="https://mcp-auth.alonana.com",
)
# =====================================================
# MCP SERVER
# =====================================================
mcp = FastMCP(
"example-server",
auth_server_provider=auth_provider,
auth=AuthSettings(
issuer_url="https://mcp-auth.alonana.com",
resource_server_url="https://mcp-auth.alonana.com",
client_registration_options=ClientRegistrationOptions(
enabled=True,
valid_scopes=["mcp:tools"],
default_scopes=["mcp:tools"],
),
),
transport_security=TransportSecuritySettings(
allowed_hosts=["mcp-auth.alonana.com", "localhost:*", "127.0.0.1:*"],
allowed_origins=["https://mcp-auth.alonana.com"],
),
)
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
@mcp.tool()
def hello(name: str) -> str:
"""
Simple greeting tool.
"""
return f"Hello {name}"
@mcp.tool()
def get_status() -> str:
"""
Health check tool.
"""
return "MCP server is running"
# =====================================================
# FASTAPI APPLICATION
# =====================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
async with mcp.session_manager.run():
yield
app = FastAPI(
title="Example MCP Server",
lifespan=lifespan,
)
# =====================================================
# TRUSTED HOSTS
# =====================================================
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=[
"mcp-auth.alonana.com",
"localhost",
"127.0.0.1",
],
)
# =====================================================
# SESSION
# =====================================================
app.add_middleware(
SessionMiddleware,
secret_key=os.environ["SESSION_SECRET"],
)
# =====================================================
# GOOGLE OAUTH
# =====================================================
oauth = OAuth()
oauth.register(
name="google",
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
server_metadata_url=(
"https://accounts.google.com/.well-known/openid-configuration"
),
client_kwargs={
"scope": "openid email profile",
},
)
# =====================================================
# NORMAL ENDPOINTS
# =====================================================
@app.get("/")
async def root(request: Request):
user = request.session.get("user")
return {
"service": "mcp-server",
"logged_in": user is not None,
"user": user,
"mcp_endpoint": "/mcp",
}
@app.get("/health")
async def health():
return {
"status": "ok"
}
# =====================================================
# HUMAN AUTH
# =====================================================
@app.get("/login")
async def login(request: Request):
redirect_uri = request.url_for(
"auth_callback"
)
return await oauth.google.authorize_redirect(
request,
redirect_uri,
)
@app.get("/auth/callback")
async def auth_callback(request: Request):
token = await oauth.google.authorize_access_token(
request
)
user = token["userinfo"]
request.session["user"] = {
"id": user.get("sub"),
"name": user.get("name"),
"email": user.get("email"),
"picture": user.get("picture"),
}
return RedirectResponse(
url="/"
)
@app.get("/me")
async def me(request: Request):
user = request.session.get("user")
if not user:
return {
"authenticated": False
}
return {
"authenticated": True,
"user": user,
}
@app.get("/logout")
async def logout(request: Request):
request.session.clear()
return {
"message": "Logged out"
}
# =====================================================
# MCP CLIENT AUTH
# =====================================================
@app.get("/oauth/google/callback")
async def google_oauth_callback(request: Request):
state = request.query_params["state"]
code = request.query_params["code"]
pending = auth_provider.pending.pop(state, None)
if not pending:
return {"error": "invalid or expired state"}
async with httpx.AsyncClient() as client:
resp = await client.post(GOOGLE_TOKEN_URL, data={
"code": code,
"client_id": auth_provider.client_id,
"client_secret": auth_provider.client_secret,
"redirect_uri": f"{auth_provider.base_url}/oauth/google/callback",
"grant_type": "authorization_code",
})
resp.raise_for_status()
mcp_code = secrets.token_urlsafe(32)
auth_provider.mcp_auth_codes[mcp_code] = AuthorizationCode(
code=mcp_code,
client_id=pending["client_id"],
scopes=pending["scopes"],
expires_at=int(time.time()) + 300,
code_challenge=pending["code_challenge"],
redirect_uri=pending["redirect_uri"],
redirect_uri_provided_explicitly=True,
)
redirect_url = f"{pending['redirect_uri']}?code={mcp_code}&state={pending['mcp_state']}"
return RedirectResponse(url=redirect_url)
# =====================================================
# MCP HTTP ENDPOINT
# =====================================================
app.add_middleware(FixIssuerTrailingSlashMiddleware)
# MCP SDK 1.28.1 already exposes /mcp internally.
# Therefore mount at root.
app.mount(
"",
mcp.streamable_http_app()
)
# =====================================================
# LOCAL RUN
# =====================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app:app",
host="0.0.0.0",
port=8080,
reload=True,
)
Reload the service
sudo systemctl restart mcp-server
Using the MCP
Get a client ID from the server manually:
-H 'Content-Type: application/json' \
-d '{
"redirect_uris": ["http://portal/oauth-callback.html"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'
Use this client ID for new connection creation
No comments:
Post a Comment