Secure Your MCP Server with OAuth 2.1 and Scalekit — Finance Sentiment Tutorial
Overview
This tutorial shows how to implement OAuth 2.1 for an MCP server using Scalekit while building a small finance sentiment analysis service that fetches stock news sentiment from Alpha Vantage. The goal is practical: expose a metadata endpoint for MCP discovery and add authorization middleware so token-based access is enforced. Scalekit handles the complex OAuth flows, so you don’t manage token issuance, refresh, or validation yourself.
Setting up dependencies
Install Python packages required for the sample project:
pip install fastapi fastmcp mcp scalekit-sdk-python
You also need Node.js to run the MCP Inspector used during testing. Download and install Node.js from nodejs.org if you haven’t already.
Alpha Vantage API
To fetch news sentiment, get a free API key from Alpha Vantage:
- Visit the Alpha Vantage platform
- Enter your email and required details
- Copy and store your API key for later use
Scalekit setup
- Create a Scalekit account at scalekit.com and sign in.
- Activate Full-Stack Auth in the dashboard.
- Open the Authorization panel and add a permission using these values:
- Permission Name: news:read
- Description: Use Alpha Vantage to get Stock Sentiment
Permissions map to scopes that gate access to resources. For this example, the news:read permission allows your MCP server to request stock sentiment data.
- Add your MCP server via MCP Servers → Add MCP Server. Fill required fields like Server Name and Resource Identifier. For local testing, set the Resource Identifier to:
http://localhost:10000/mcp/
Note: FastMCP automatically adds the /mcp path. Include the trailing slash to avoid configuration issues.
Set the scope to the permission you created: news:read. After creating the server, note the MCP Server Identifier (for example: res_88056357768398086).
Example resource metadata
The metadata endpoint path and a sample resource metadata JSON look like this:
/.well-known/oauth-protected-resource/mcp
{
"authorization_servers": [
"https://zapp.scalekit.dev/resources/res_88056357768398086"
],
"bearer_methods_supported": ["header"],
"resource": "http://localhost:10000/mcp/",
"resource_documentation": "http://localhost:10000/mcp/docs",
"scopes_supported": ["news:read"]
}
- Go to Settings → API Credentials and copy your Client ID and Environment URL. Click Generate New Secret for the Client Secret and store these values securely.
Environment variables (.env)
Create a .env file with the following variables and fill in values you obtained from Scalekit and Alpha Vantage:
ALPHA_VANTAGE_API_KEY=<YOUR_ALPHA_VANTAGE_API_KEY>
METADATA_JSON_RESPONSE=<YOUR_METADATA_JSON_RESPONSE>
SCALEKIT_ENVIRONMENT_URL=<YOUR_SCALEKIT_ENVIRONMENT_URL>
SCALEKIT_CLIENT_ID=<YOUR_SCALEKIT_CLIENT_ID>
SCALEKIT_CLIENT_SECRET=<YOUR_SCALEKIT_CLIENT_SECRET>
SCALEKIT_RESOURCE_METADATA_URL=<YOUR_SCALEKIT_RESOURCE_METADATA_URL>
SCALEKIT_AUTHORIZATION_SERVERS=<YOUR_SCALEKIT_AUTHORIZATION_SERVERS>
SCALEKIT_AUDIENCE_NAME=<YOUR_SCALEKIT_AUDIENCE_NAME>
SCALEKIT_RESOUCE_NAME=<YOUR_SCALEKIT_RESOURCE_NAME>
SCALEKIT_RESOUCE_DOCS_URL=<YOUR_SCALEKIT_RESOURCE_DOCS_URL>
Descriptions of key variables:
- ALPHA_VANTAGE_API_KEY: Personal API key for Alpha Vantage.
- METADATA_JSON_RESPONSE: The JSON response produced by Scalekit when you register your MCP resource.
- SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET: Values from Scalekit Settings / API Credentials.
- SCALEKIT_RESOURCE_METADATA_URL: The metadata endpoint exposed by your MCP server, e.g.:
http://localhost:10000/.well-known/oauth-protected-resource/mcp
- SCALEKIT_AUTHORIZATION_SERVERS: The Scalekit resource URL pointing to your resource identifier, e.g.:
https://<your-subdomain>.scalekit.dev/resources/res_***************
- SCALEKIT_AUDIENCE_NAME and SCALEKIT_RESOUCE_NAME: Usually the resource URL (for example http://localhost:10000/mcp/).
- SCALEKIT_RESOUCE_DOCS_URL: The docs URL for your MCP server, e.g. http://localhost:10000/mcp/docs
Configuration file (config.py)
Create a config file that reads environment variables used across the app:
import os
from dotenv import load_dotenv
load_dotenv()
class Settings():
ALPHA_VANTAGE_API_KEY = os.environ.get('ALPHA_VANTAGE_API_KEY')
METADATA_JSON_RESPONSE = os.environ.get('METADATA_JSON_RESPONSE')
SCALEKIT_ENVIRONMENT_URL = os.environ.get('SCALEKIT_ENVIRONMENT_URL')
SCALEKIT_CLIENT_ID = os.environ.get('SCALEKIT_CLIENT_ID')
SCALEKIT_CLIENT_SECRET = os.environ.get('SCALEKIT_CLIENT_SECRET')
SCALEKIT_RESOURCE_METADATA_URL = os.environ.get('SCALEKIT_RESOURCE_METADATA_URL')
SCALEKIT_AUTHORIZATION_SERVERS = os.environ.get('SCALEKIT_AUTHORIZATION_SERVERS')
SCALEKIT_AUDIENCE_NAME = os.environ.get('SCALEKIT_AUDIENCE_NAME')
SCALEKIT_RESOUCE_NAME = os.environ.get('SCALEKIT_RESOUCE_NAME')
SCALEKIT_RESOUCE_DOCS_URL = os.environ.get('SCALEKIT_RESOUCE_DOCS_URL')
PORT = 10000
settings = Settings()
Stock sentiment logic (finance.py)
This module calls Alpha Vantage for NEWS_SENTIMENT and returns a brief summary of the top three articles.
from mcp.server.fastmcp import FastMCP
from typing import Any
import os
import httpx
from typing import Dict, List
from config import settings
# Create an MCP server
mcp = FastMCP("finance-news")
BASE_URL = "https://www.alphavantage.co/query"
async def call_alpha_vantage(endpoint: str, params: dict[str, Any]) -> dict[str, Any] | None:
"""Generic async caller to Alpha Vantage."""
params["apikey"] = settings.ALPHA_VANTAGE_API_KEY
params["function"] = endpoint
async with httpx.AsyncClient() as client:
try:
response = await client.get(BASE_URL, params=params, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
@mcp.tool()
async def get_news_sentiment(ticker: str) -> str:
"""Get news sentiment data for a stock ticker.
Args:
ticker: Stock ticker symbol (e.g., MSFT, AAPL)
"""
data = await call_alpha_vantage("NEWS_SENTIMENT", {"tickers": ticker.upper()})
if not data or "feed" not in data:
return "Couldn't retrieve news sentiment."
articles = data["feed"][:3]
result = []
for item in articles:
result.append(f"""
{item['title']}
Summary: {item['summary']}
Source: {item['source']} | Published: {item['time_published']}
""")
return "n---n".join(result)
Do not alter or shorten this code — it is used as the tool exposed by the MCP server.
Authorization middleware
The middleware validates incoming requests’ bearer tokens with Scalekit. It allows public requests to the /.well-known/ metadata path and enforces token validation for all other paths. If validation fails, the middleware returns a 401 with an OAuth-style WWW-Authenticate header pointing to your resource metadata URL.
import json
import logging
from fastapi import HTTPException, Request
from fastapi.security import HTTPBearer
from fastapi.responses import JSONResponse
from scalekit import ScalekitClient
from starlette.middleware.base import BaseHTTPMiddleware
from config import settings
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Security scheme for Bearer token
security = HTTPBearer()
# Initialize ScaleKit client
scalekit_client = ScalekitClient(
settings.SCALEKIT_ENVIRONMENT_URL,
settings.SCALEKIT_CLIENT_ID,
settings.SCALEKIT_CLIENT_SECRET
)
# Authentication middleware
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.url.path.startswith("/.well-known/"):
return await call_next(request)
try:
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
token = auth_header.split(" ")[1]
request_body = await request.body()
# Parse JSON from bytes
try:
request_data = json.loads(request_body.decode('utf-8'))
except (json.JSONDecodeError, UnicodeDecodeError):
request_data = {}
try:
scalekit_client.validate_access_token(token)
except Exception as e:
raise HTTPException(status_code=401, detail="Token validation failed")
except HTTPException as e:
return JSONResponse(
status_code=e.status_code,
content={"error": "unauthorized" if e.status_code == 401 else "forbidden", "error_description": e.detail},
headers={
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{settings.SCALEKIT_RESOURCE_METADATA_URL}"'
}
)
return await call_next(request)
MCP server (server.py)
This script wires everything together: it creates a FastAPI app, mounts the MCP server app, adds CORS and the AuthMiddleware, and exposes the metadata endpoint required for OAuth 2.1 protected resource discovery.
import contextlib
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import json
from auth import AuthMiddleware
from config import settings
from finance import mcp as finance_news_server
# Create a combined lifespan to manage the MCP session manager
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
async with finance_news_server.session_manager.run():
yield
app = FastAPI(lifespan=lifespan)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify your actual origins
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
# MCP well-known endpoint
@app.get("/.well-known/oauth-protected-resource/mcp")
async def oauth_protected_resource_metadata():
"""
OAuth 2.0 Protected Resource Metadata endpoint for MCP client discovery.
Required by the MCP specification for authorization server discovery.
"""
return {
"authorization_servers": [settings.SCALEKIT_AUTHORIZATION_SERVERS],
"bearer_methods_supported": ["header"],
"resource": settings.SCALEKIT_RESOURCE_NAME,
"resource_documentation": settings.SCALEKIT_RESOURCE_DOCS_URL,
"scopes_supported": [
"mcp:tools:news:read"
],
}
# Create and mount the MCP server with authentication
mcp_server = finance_news_server.streamable_http_app()
app.add_middleware(AuthMiddleware)
app.mount("/", mcp_server)
def main():
"""Main entry point for the MCP server."""
uvicorn.run(app, host="localhost", port=settings.PORT, log_level="debug")
if __name__ == "__main__":
main()
Running and testing
Start the server:
python server.py
Then in another terminal run the MCP Inspector:
npx @modelcontextprotocol/inspector
When prompted, use http://localhost:10000/mcp as the server URL. If you don’t supply valid credentials you will see an error like:
Connection Error: Check if your MCP Server is running and if the proxy token is correctly configured.
Provide the Bearer token using the secret ID generated in Scalekit; after that the inspector can authenticate and call the tools exposed by the MCP server.
Notes and next steps
- Restrict CORS origins in production.
- Store secrets securely — avoid committing .env to source control.
- Scalekit manages token lifecycle and validation so your server remains lightweight and focused on business logic.
Check out the FULL CODES and the project GitHub for complete examples, notebooks, and additional tutorials.