SuperAPI AI Directory

adding-mcp-oauth

Security & auditing

View repository

Updated 10-16 04:18

About this skill

Adding OAuth 2.1 Authorization to MCP Servers

Guardrails

  • MUST use Streamable HTTP transport — stdio does not support OAuth.
  • MUST validate the bearer token's audience (and required scopes, if enforcing scope-based authorization) before executing any tool logic.
  • MUST NOT protect the /.well-known/oauth-protected-resource discovery endpoint with auth middleware — it must remain publicly accessible.
  • MUST return 401 with a WWW-Authenticate header on missing or invalid tokens; a bare 401 causes AI hosts to fail silently.

Prerequisite: HTTP transport

MCP OAuth requires Streamable HTTP transport. Stdio does not support OAuth.

Node.js: Use StreamableHTTPServerTransport from @modelcontextprotocol/sdk/server/streamableHttp.js

Python: Use mcp.streamable_http_app(path="/mcp") and run with uvicorn module:app

If currently using stdio, migrate to HTTP first. See MCP Transport Docs.

Setup workflow

Copy this checklist and track progress:

MCP OAuth Setup:
- [ ] Step 1: Install Scalekit SDK
- [ ] Step 2: Register MCP server in Scalekit dashboard
- [ ] Step 3: Implement discovery endpoint
- [ ] Step 4: Add token validation middleware
- [ ] Step 5: (Optional) Add scope-based authorization
- [ ] Step 6: Test with AI hosts

Step 1: Install Scalekit SDK

Node.js:

npm install @scalekit-sdk/node

Python:

pip install scalekit-sdk-python

Get credentials from Scalekit dashboard after creating an account.

Step 2: Register MCP server

User action (dashboard) — tell the user to complete this, then wait for confirmation. Do not attempt to automate the Scalekit UI.

  1. Open app.scalekit.com → MCP servers → Add MCP server
  2. Provide a descriptive name (appears on consent page)
  3. Enable dynamic client registration (automatic MCP host registration)
  4. Enable Client ID Metadata Document (CIMD)
  5. Save, then restart the MCP server process after DCR/CIMD toggles

Advanced settings (optional):

  • Server URL: public MCP identifier — use one consistent base URL everywhere (example throughout this skill: https://mcp.yourapp.com)
  • Access token lifetime: 300-3600 seconds recommended
  • Scopes: e.g. todo:read, todo:write

After the user confirms registration, continue with code in Steps 3–5.

Step 3: Implement discovery endpoint

Create /.well-known/oauth-protected-resource endpoint. Copy metadata JSON from Dashboard > MCP Servers > Your server > Metadata JSON.

Node.js (Express):

app.get('/.well-known/oauth-protected-resource', (req, res) => {
  res.json({
    "authorization_servers": [
      "https://<SCALEKIT_ENVIRONMENT_URL>/resources/<YOUR_RESOURCE_ID>"
    ],
    "bearer_methods_supported": ["header"],
    "resource": "https://mcp.yourapp.com",
    "resource_documentation": "https://mcp.yourapp.com/docs",
    "scopes_supported": ["todo:read", "todo:write"]
  });
});

Python (FastAPI):

@app.get("/.well-known/oauth-protected-resource")
async def get_oauth_protected_resource():
    return {
        "authorization_servers": [
            "https://<SCALEKIT_ENVIRONMENT_URL>/resources/<YOUR_RESOURCE_ID>"
        ],
        "bearer_methods_supported": ["header"],
        "resource": "https://mcp.yourapp.com",
        "resource_documentation": "https://mcp.yourapp.com/docs",
        "scopes_supported": ["todo:read", "todo:write"]
    }

Replace placeholders with actual values from Scalekit dashboard.

Step 4: Add token validation middleware

Initialize Scalekit client

Node.js:

import { ScalekitClient } from '@scalekit-sdk/node';

const scalekit = new ScalekitClient(
  process.env.SCALEKIT_ENVIRONMENT_URL,
  process.env.SCALEKIT_CLIENT_ID,
  process.env.SCALEKIT_CLIENT_SECRET
);

const RESOURCE_ID = 'https://mcp.yourapp.com';  // Or autogenerated ID from dashboard
const METADATA_ENDPOINT = 'https://mcp.yourapp.com/.well-known/oauth-protected-resource';

export const WWWHeader = {
  HeaderKey: 'WWW-Authenticate',
  HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"`
};

Python:

from scalekit import ScalekitClient
import os

scalekit_client = ScalekitClient(
    env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
    client_id=os.getenv("SCALEKIT_CLIENT_ID"),
    client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
)

RESOURCE_ID = "https://mcp.yourapp.com"
METADATA_ENDPOINT = "https://mcp.yourapp.com/.well-known/oauth-protected-resource"

WWW_HEADER = {
    "WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"'
}

Implement authentication middleware

Node.js:

export async function authMiddleware(req, res, next) {
  try {
    // Allow public access to well-known endpoints
    if (req.path.includes('.well-known')) {
      return next();
    }

    // Extract Bearer token
    const authHeader = req.headers['authorization'];
    const token = authHeader?.startsWith('Bearer ')
      ? authHeader.split('Bearer ')[1]?.trim()
      : null;

    if (!token) {
      throw new Error('Missing or invalid Bearer token');
    }

    // Validate token against resource audience
    await scalekit.validateToken(token, {
      audience: [RESOURCE_ID]
    });

    next();
  } catch (err) {
    return res
      .status(401)
      .set(WWWHeader.HeaderKey, WWWHeader.HeaderValue)
      .end();
  }
}

// Apply to all MCP endpoints
app.use('/', authMiddleware);

Python:

from scalekit.common.scalekit import TokenValidationOptions
from fastapi import Request, HTTPException, status

async def auth_middleware(request: Request, call_next):
    # Allow public access to well-known endpoints
    if request.url.path.startswith("/.well-known"):
        return await call_next(request)

    # Extract Bearer token
    auth_header = request.headers.get("Authorization", "")
    token = None
    if auth_header.startswith("Bearer "):
        token = auth_header.split("Bearer ")[1].strip()

    if not token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            headers=WWW_HEADER
        )

    # Validate token
    try:
        options = TokenValidationOptions(
            issuer=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
            audience=[RESOURCE_ID]
        )
        scalekit_client.validate_access_token_and_get_claims(token, options=options)
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            headers=WWW_HEADER
        )

    return await call_next(request)

# Apply to all MCP endpoints
app.middleware("http")(auth_middleware)

Step 5: Scope-based tool authorization (Optional)

Add fine-grained access control at the tool execution level:

Node.js:

try {
    await scalekit.validateToken(token, {
      audience: [RESOURCE_ID],
      requiredScopes: [scope]  // e.g., 'todo:write'
    });
} catch(error) {
    return res.status(403).json({
        error: 'insufficient_scope',
        error_description: `Required scope: ${scope}`,
        scope: scope
    });
}

Python:

try:
    scalekit_client.validate_access_token(
        token,
        options=TokenValidationOptions(
            audience=[RESOURCE_ID],
            required_scopes=[scope]
        )
    )
except Exception:
    return {
        "error": "insufficient_scope",
        "error_description": f"Required scope: {scope}",
        "scope": scope
    }

Step 6: Verify and deploy

Verify your integration

Before testing with AI hosts, the coding agent will scan your project to determine the right URL to verify against. It will look for:

  • RESOURCE_ID or resource values in your code or .env
  • The host/domain used in /.well-known/oauth-protected-resource
  • Any deployed base URL in environment config (SERVER_URL, PUBLIC_URL, etc.)

If no URL is found, you'll be asked:

"What is your MCP server base URL? (e.g., https://mcp.yourapp.com or https://mcp.yourapp.com/mcp)"

Once the URL is known, run these three checks:

Check 1 – Confirm 401 without token:

curl -i <your-mcp-url>

Expected: HTTP/1.1 401 Unauthorized

Check 2 – Confirm WWW-Authenticate header: The response must include:

WWW-Authenticate: Bearer realm="OAuth", resource_metadata="https://<your-domain>/.well-known/oauth-protected-resource"

This is what triggers the MCP client's OAuth flow. A plain 401 without this header will cause AI hosts (Claude Desktop, Cursor, VS Code) to fail silently.

Check 3 – Confirm metadata endpoint is reachable:

curl https://<your-domain>/.well-known/oauth-protected-resource

Expected: JSON with resource, authorization_servers, and scopes_supported.

After verification passes, test with Claude Desktop, Cursor, and VS Code. Ensure invalid tokens get 401, and scope-based authorization (if implemented) rejects insufficient scopes.

Framework-specific references

Common issues

Token validation fails:

  • Verify RESOURCE_ID matches Server URL in dashboard
  • Check environment variables are set correctly
  • Ensure token hasn't expired

Discovery endpoint not found:

  • Verify endpoint path is exactly /.well-known/oauth-protected-resource
  • Check endpoint is publicly accessible (not protected by auth middleware)

Scope validation errors:

  • Verify scopes in dashboard match those in code
  • Check token includes required scopes
  • Ensure scope strings match exactly (case-sensitive)