|
| 1 | +# Example FastMCP MCP Server with Auth0 for On-Behalf-Of Token Exchange (Python) |
| 2 | + |
| 3 | +## Available Tools |
| 4 | + |
| 5 | +The server exposes the following tools: |
| 6 | + |
| 7 | +- `whoami` - Returns authenticated user information and granted scopes |
| 8 | +- `greet` - Personalized greeting demonstrating On-Behalf-Of Token Exchange with upstream API |
| 9 | +- `get_datetime` - Returns the current UTC date and time (no scope required) |
| 10 | + |
| 11 | +## Install dependencies |
| 12 | + |
| 13 | +Install the dependencies using Poetry: |
| 14 | + |
| 15 | +```bash |
| 16 | +poetry install |
| 17 | +``` |
| 18 | + |
| 19 | +## Configuration |
| 20 | + |
| 21 | +Create a `.env` file in the project root and configure the following environment variables: |
| 22 | + |
| 23 | +``` |
| 24 | +# Auth0 tenant domain |
| 25 | +AUTH0_DOMAIN=example-tenant.us.auth0.com |
| 26 | +
|
| 27 | +# Auth0 API Identifier |
| 28 | +AUTH0_AUDIENCE=http://localhost:3001/ |
| 29 | +
|
| 30 | +# Server port |
| 31 | +PORT=3001 |
| 32 | +
|
| 33 | +# MCP server URL |
| 34 | +MCP_SERVER_URL=http://localhost:3001 |
| 35 | +
|
| 36 | +# MCP Auth0 client credentials and configuration |
| 37 | +MCP_AUTH0_CLIENT_ID=your-client-id |
| 38 | +MCP_AUTH0_CLIENT_SECRET=your-client-secret |
| 39 | +
|
| 40 | +# Scopes to request |
| 41 | +MCP_AUTH0_EXCHANGE_SCOPE=read:private |
| 42 | +
|
| 43 | +# Upstream API configuration |
| 44 | +API_AUTH0_AUDIENCE=your-api-audience |
| 45 | +API_BASE_URL=http://localhost:8787 |
| 46 | +``` |
| 47 | + |
| 48 | +## Services |
| 49 | + |
| 50 | +This example consists of two services that work together: |
| 51 | + |
| 52 | +### 1. Upstream API Service (`poetry run python -m src.api.server`) |
| 53 | + |
| 54 | +The upstream API is a Starlette-based service that demonstrates a protected resource requiring specific scopes: |
| 55 | + |
| 56 | +- **Port**: 8787 (configurable via `API_BASE_URL`) |
| 57 | +- **Authentication**: Auth0 JWT tokens with `API_AUTH0_AUDIENCE` |
| 58 | +- **Endpoints**: |
| 59 | + - `GET /api/private-scope` - Protected endpoint requiring `read:private` scope |
| 60 | + |
| 61 | +The API service validates incoming tokens and returns authenticated user information including the subject (`sub`) and granted scopes. |
| 62 | + |
| 63 | +### 2. MCP Server (`poetry run python -m src.server`) |
| 64 | + |
| 65 | +The MCP (Model Context Protocol) server implements on-behalf-of token exchange with Auth0: |
| 66 | + |
| 67 | +- **Port**: 3001 (configurable via `PORT`) |
| 68 | +- **Transport**: HTTP streaming at `/` endpoint |
| 69 | +- **Authentication**: Auth0 JWT tokens with `AUTH0_AUDIENCE` |
| 70 | + |
| 71 | +## Running the Services |
| 72 | + |
| 73 | +With the configuration in place, start both services: |
| 74 | + |
| 75 | +**1. Start the upstream API:** |
| 76 | +```bash |
| 77 | +cd /path/to/fastmcp-mcp-on-behalf-of-tokenexchange-python |
| 78 | +poetry run python -m src.api.server |
| 79 | +``` |
| 80 | + |
| 81 | +**2. Start the MCP server (in a new terminal):** |
| 82 | +```bash |
| 83 | +cd /path/to/fastmcp-mcp-on-behalf-of-tokenexchange-python |
| 84 | +poetry run python -m src.server |
| 85 | +``` |
| 86 | + |
| 87 | +The MCP server will use On-Behalf-Of Token Exchange (OBO) to obtain tokens for calling the upstream API on behalf of authenticated users. |
| 88 | + |
| 89 | +## Testing |
| 90 | + |
| 91 | +Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively: |
| 92 | + |
| 93 | +```bash |
| 94 | +npx @modelcontextprotocol/inspector |
| 95 | +``` |
| 96 | + |
| 97 | +The server will start up and the UI will be accessible at http://localhost:6274. |
| 98 | + |
| 99 | +In the MCP Inspector, select `Streamable HTTP` as the `Transport Type`, enter `http://localhost:3001/mcp` as the URL, and select `Via Proxy` for `Connection Type`. |
| 100 | + |
| 101 | +### Using cURL |
| 102 | +You can use cURL to verify that the server is running: |
| 103 | + |
| 104 | +```sh |
| 105 | +# Test that the server is running and accessible - check OAuth resource metadata |
| 106 | +curl -v http://localhost:3001/.well-known/oauth-protected-resource |
| 107 | + |
| 108 | +# Test MCP initialization (requires valid Auth0 access token) |
| 109 | +curl -X POST http://localhost:3001/mcp \ |
| 110 | + -H "Content-Type: application/json" \ |
| 111 | + -H "Accept: application/json, text/event-stream" \ |
| 112 | + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ |
| 113 | + -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "curl-test", "version": "1.0.0"}}}' |
| 114 | + |
| 115 | +# Test get_datetime tool (no scope required) - outputs ISO string like 2025-10-31T14:12:03.123Z |
| 116 | +curl -X POST http://localhost:3001/mcp \ |
| 117 | + -H "Content-Type: application/json" \ |
| 118 | + -H "Accept: application/json, text/event-stream" \ |
| 119 | + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ |
| 120 | + -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "get_datetime", "arguments": {}}}' |
| 121 | + |
| 122 | +# Test greet tool (tool:greet permission required) - outputs Upstream API response when OBO flow succeeds |
| 123 | +curl -X POST http://localhost:3001/mcp \ |
| 124 | + -H "Content-Type: application/json" \ |
| 125 | + -H "Accept: application/json, text/event-stream" \ |
| 126 | + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ |
| 127 | + -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "greet", "arguments": {"name": "World"}}}' |
| 128 | + |
| 129 | +# sample response from greet tool: |
| 130 | +event: message |
| 131 | +id: b79cc975-0b9a-4ad7-9f0b-60709f33dcaa_1777299634174_47cmz49v |
| 132 | +data: |
| 133 | +{ |
| 134 | + "result": { |
| 135 | + "content": [ |
| 136 | + { |
| 137 | + "type": "text", |
| 138 | + "text": "Hello, World (auth0|69d9483f2d2c4494f9de0d8c)!\n\ |
| 139 | + \n\ |
| 140 | + Upstream API Response:\n\ |
| 141 | + {\n\ |
| 142 | + \"msg\": \"Hello from upstream API\",\n\ |
| 143 | + \"sub\": \"auth0|69d9483f2d2c4494f9de0d8c\",\n\ |
| 144 | + \"scopes\": \"read:private\"\n\ |
| 145 | + }" |
| 146 | + } |
| 147 | + ] |
| 148 | + }, |
| 149 | + "jsonrpc": "2.0", |
| 150 | + "id": 2 |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +**Note:** Use the MCP Inspector or other MCP-compatible clients for comprehensive testing. |
| 155 | + |
| 156 | +## On-Behalf-Of Token Exchange Flow |
| 157 | + |
| 158 | +This example demonstrates the following On-Behalf-Of (OBO) Token Exchange flow: |
| 159 | + |
| 160 | +1. **Client authenticates** to the MCP server with an Auth0 access token |
| 161 | +2. **MCP server validates** the token and creates a session |
| 162 | +3. **Client calls `greet` tool** on the MCP server |
| 163 | +4. **MCP server exchanges** the original token for a new token with different audience using OBO: |
| 164 | + - Subject token: Original MCP access token |
| 165 | + - Subject token type: `urn:ietf:params:oauth:token-type:access_token` |
| 166 | + - Target audience: Upstream API (`API_AUTH0_AUDIENCE`) |
| 167 | + - Requested scope: `read:private` |
| 168 | +5. **MCP server calls upstream API** using the exchanged token |
| 169 | +6. **Upstream API validates** the token and returns user information |
| 170 | +7. **MCP server returns** the result to the client |
| 171 | + |
| 172 | +This pattern is useful for: |
| 173 | +- MCP servers that need to call downstream APIs on behalf of the user |
| 174 | +- Microservices architectures where each service has its own audience |
| 175 | +- Token scoping where different services require different permissions |
| 176 | +- Maintaining user identity across service boundaries while respecting audience constraints |
0 commit comments