-
Notifications
You must be signed in to change notification settings - Fork 16
feat(example): Add FastMCP MCP server example #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cafc41f
3f17c9a
649a3b9
7fa9b48
1ba65c6
fd1524d
a2ea6e4
f995225
adf514b
5643498
6c1c74e
420a2d0
1ba67a8
3914a34
91df18e
83f0063
beab942
6bbb80d
5986213
34d3592
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| AUTH0_DOMAIN= | ||
| AUTH0_AUDIENCE= | ||
| PORT=3001 | ||
| MCP_SERVER_URL=http://localhost:3001 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| registry=https://registry.npmjs.org |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| # Example FastMCP MCP Server with Auth0 Integration | ||
|
|
||
| This is a practical example of securing a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs) server | ||
| with Auth0 using the [FastMCP](https://github.com/punkpeye/fastmcp) TypeScript framework. It demonstrates | ||
| real-world OAuth 2.0 and OIDC integration with JWT token verification and scope enforcement. | ||
|
|
||
| ## Install dependencies | ||
|
|
||
| Install the dependencies using npm: | ||
|
|
||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| ## Auth0 Tenant Setup | ||
|
|
||
| ### Pre-requisites: | ||
|
|
||
| This guide uses [Auth0 CLI](https://auth0.github.io/auth0-cli/) to configure an Auth0 tenant for secure MCP tool access. If you don't have it, you can follow the [Auth0 CLI installation instructions](https://auth0.github.io/auth0-cli/) to set it up. Alternatively, all the following configuration steps can be done through the [Auth0 Management Dashboard](https://manage.auth0.com/). | ||
|
|
||
| ### Step 1: Authenticate with Auth0 CLI | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add installing the Auth0 CLI as a dependency? Or is this example usable without installing it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can configure your tenant from the dashboard or with the Management API. The Auth0 CLI isn't required, but it simplifies the setup process.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But the entire next steps rely on the auth0 CLI, which makes it a required prerequisite to follow any of the following steps.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 That makes sense. I updated it to explicitly say that auth0 cli is a prerequisite to follow the rest of steps
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps, we can add a small sentence that explains everything can be configured through the manage dashboard as well if prefered. |
||
|
|
||
| First, you need to log in to the Auth0 CLI with the correct scopes to manage all the necessary resources. | ||
|
|
||
| 1. Run the login command: This command will open a browser window for you to authenticate. We are requesting a set of | ||
| scopes to configure APIs, roles, and clients. | ||
|
|
||
| ``` | ||
| auth0 login --scopes "read:client_grants,create:client_grants,delete:client_grants,read:clients,create:clients,update:clients,read:resource_servers,create:resource_servers,update:resource_servers,read:roles,create:roles,update:roles,update:tenant_settings,read:connections,update:connections" | ||
| ``` | ||
|
|
||
| 2. Verify your tenant: After logging in, confirm you are operating on the tenant you want to configure. | ||
|
|
||
| ``` | ||
| auth0 tenants list | ||
| ``` | ||
|
|
||
| ### Step 2: Configure Tenant Settings | ||
|
|
||
| Next, enable tenant-level flags required for Dynamic Client Registration (DCR) and an improved user consent experience. | ||
|
|
||
| - `enable_dynamic_client_registration`: Allows MCP tools to register themselves as applications automatically. | ||
| [Learn more](https://auth0.com/docs/get-started/applications/dynamic-client-registration#enable-dynamic-client-registration) | ||
| - `use_scope_descriptions_for_consent`: Shows user-friendly descriptions for scopes on the consent screen. | ||
| [Learn more](https://auth0.com/docs/customize/login-pages/customize-consent-prompts). | ||
|
|
||
| Execute the following command to enable the above mentioned flags through the tenant settings: | ||
|
|
||
| ``` | ||
| auth0 tenant-settings update set flags.enable_dynamic_client_registration flags.use_scope_descriptions_for_consent | ||
| ``` | ||
|
|
||
| ### Step 3: Promote Connections to Domain Level | ||
|
|
||
| [Learn more](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level) about promoting | ||
| connections to domain level. | ||
|
|
||
| 1. List your connections to get their IDs: `auth0 api get connections` | ||
| 2. From the list, identify only the connections that should be available to be used with third party applications. For each of those specific connection IDs, run the following command to mark it as a domain-level connection. Replace `YOUR_CONNECTION_ID` with the actual ID (e.g., `con_XXXXXXXXXXXXXXXX`) | ||
|
|
||
| ``` | ||
| auth0 api patch connections/YOUR_CONNECTION_ID --data '{"is_domain_connection": true}' | ||
| ``` | ||
|
|
||
| ### Step 4: Configure the API and Default Audience | ||
|
|
||
| This step creates the API (also known as a Resource Server) that represents your protected MCP Server and sets it as the | ||
| default for your tenant. | ||
|
|
||
| 1. Create the API: This command registers the API with Auth0, defines its signing algorithm, enables Role-Based Access | ||
| Control (RBAC), and specifies the available scopes. Replace `http://localhost:3001` and `MCP Tools API` | ||
| with your desired identifier and name. Add your tool-specific scopes to the scopes array. | ||
|
|
||
| Note that `rfc9068_profile_authz` is used instead of `rfc9068_profile` as the token dialect to enable RBAC. [Learn more](https://auth0.com/docs/get-started/apis/enable-role-based-access-control-for-apis#token-dialect-options) | ||
|
|
||
| ``` | ||
| auth0 api post resource-servers --data '{ | ||
| "identifier": "http://localhost:3001", | ||
| "name": "MCP Tools API", | ||
| "signing_alg": "RS256", | ||
| "token_dialect": "rfc9068_profile_authz", | ||
| "enforce_policies": true, | ||
| "scopes": [ | ||
| {"value": "tool:whoami", "description": "Access the WhoAmI tool"}, | ||
| {"value": "tool:greet", "description": "Access the Greeting tool"} | ||
| ] | ||
| }' | ||
|
|
||
| ``` | ||
|
|
||
| 2. Set the Default Audience: This ensures that users logging in interactively get access tokens that are valid for your | ||
| newly created MCP Server. Replace `http://localhost:3001` with the same API identifier you used above. | ||
|
|
||
| **Note:** This step is currently required but temporary. Without setting a default audience, the issued access tokens will not be scoped specifically to your MCP resource server. Support for RFC 8707 (Resource Indicators for OAuth 2.0) is coming soon, which will provide proper resource targeting. Once available, these instructions will be updated to explain how to enable support for RFC 8707 instead of the default audience approach. | ||
|
|
||
| ``` | ||
| auth0 api patch "tenants/settings" --data '{"default_audience": "http://localhost:3001"}' | ||
| ``` | ||
|
|
||
| ### Step 5: Configure RBAC Roles and Permissions | ||
|
|
||
| Now, set up roles and assign permissions to them. This allows you to control which users can access which tools. | ||
|
|
||
| 1. Create Roles: For each role you need (e.g., "Tool Administrator", "Tool User"), run the create command. | ||
|
|
||
| ``` | ||
| # Example for an admin role | ||
| auth0 roles create --name "Tool Administrator" --description "Grants access to all MCP tools" | ||
|
|
||
| # Example for a basic user role | ||
| auth0 roles create --name "Tool User" --description "Grants access to basic MCP tools" | ||
| ``` | ||
|
|
||
| 2. Assign Permissions to Roles: After creating roles, note the ID from the output (e.g. `rol_`) and and assign the API | ||
| permissions to it. Replace `YOUR_ROLE_ID`, `http://localhost:3001`, and the list of scopes. | ||
|
|
||
| ``` | ||
| # Example for admin role (all scopes) | ||
| auth0 roles permissions add YOUR_ADMIN_ROLE_ID --api-id "http://localhost:3001" --permissions "tool:whoami,tool:greet" | ||
|
|
||
| # Example for user role (one scope) | ||
| auth0 roles permissions add YOUR_USER_ROLE_ID --api-id "http://localhost:3001" --permissions "tool:whoami" | ||
| ``` | ||
|
|
||
| 3. Assign Roles to Users: Find users and assign them to the roles. | ||
|
|
||
| ``` | ||
| # Find a user's ID | ||
| auth0 users search --query "email:\"example@google.com\"" | ||
|
|
||
| # Assign the role using the user's ID and the role's ID | ||
| auth0 users roles assign "auth0|USER_ID_HERE" --roles "YOUR_ROLE_ID_HERE" | ||
| ``` | ||
|
|
||
| **Note:** Further customization not supported out of the box by RBAC can be done via a custom Post-Login action trigger. | ||
|
|
||
| ## Configuration | ||
|
|
||
| Rename `.env.example` to `.env` and configure the domain and audience: | ||
|
|
||
| ``` | ||
| # Auth0 tenant domain | ||
| AUTH0_DOMAIN=example-tenant.us.auth0.com | ||
|
|
||
| # Auth0 API Identifier | ||
| AUTH0_AUDIENCE=http://localhost:3001 | ||
| ``` | ||
|
|
||
| With the configuration in place, the example can be started by running: | ||
|
|
||
| ```bash | ||
| npm run start | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively: | ||
|
|
||
| ```bash | ||
| npx @modelcontextprotocol/inspector | ||
| ``` | ||
|
|
||
| The server will start up and the UI will be accessible at http://localhost:6274. | ||
|
|
||
| In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "name": "example-fastmcp-mcp", | ||
| "version": "0.1.0-beta.1", | ||
| "description": "FastMCP Server Example (Beta) — Example implementation of an MCP server using FastMCP framework with Auth0 authentication. This example is in beta and demonstrates secure MCP server patterns with OAuth 2.0 and JWT validation.", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the approach to move this out of beta post merging this example?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call out. We are starting with beta and might not support every framework. The idea is to first validate those mcp examples and get some feedback. Framework popularity with mcp server integration is also a factor. Curious if there's an existing path we usually follow to move examples here out of beta
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No we have never realy used a beta concept with examples, as they are not released like versioned artifacts. |
||
| "type": "module", | ||
| "scripts": { | ||
| "start": "tsx src/index.ts --project tsconfig.json" | ||
| }, | ||
| "dependencies": { | ||
| "@auth0/auth0-api-js": "*", | ||
| "@modelcontextprotocol/sdk": "^1.17.2", | ||
| "dotenv": "^17.2.1", | ||
| "fastmcp": "^3.12.0", | ||
| "zod": "^4.0.17" | ||
| }, | ||
| "devDependencies": { | ||
| "tsx": "^4.20.3", | ||
| "typescript": "^5.8.3" | ||
| }, | ||
| "keywords": [ | ||
| "fastmcp", | ||
| "example" | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import type { IncomingMessage } from "http"; | ||
| import { ApiClient, VerifyAccessTokenError } from "@auth0/auth0-api-js"; | ||
| import { | ||
| InsufficientScopeError, | ||
| InvalidTokenError, | ||
| } from "@modelcontextprotocol/sdk/server/auth/errors.js"; | ||
| import { getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js"; | ||
| import { FastMCPAuthSession } from "./types.js"; | ||
|
|
||
| const PORT = parseInt(process.env.PORT ?? "3001", 10); | ||
| const MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}`; | ||
| const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN as string; | ||
| const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE as string; | ||
|
|
||
| const apiClient = new ApiClient({ | ||
| domain: AUTH0_DOMAIN, | ||
| audience: AUTH0_AUDIENCE, | ||
| }); | ||
|
|
||
| function isNonEmptyString(value: unknown): value is string { | ||
| return typeof value === "string" && value.length > 0; | ||
| } | ||
|
|
||
| export const authenticate = async ( | ||
| request: IncomingMessage | ||
| ): Promise<FastMCPAuthSession> => { | ||
| try { | ||
| const authHeader = request.headers.authorization; | ||
| if (!authHeader) { | ||
| throw new InvalidTokenError("Missing authorization header"); | ||
| } | ||
|
|
||
| const [type, accessToken] = authHeader.split(" "); | ||
| if (type?.toLocaleLowerCase() !== "bearer" || !accessToken) { | ||
| throw new InvalidTokenError("Invalid authorization header"); | ||
| } | ||
| const decoded = await apiClient.verifyAccessToken({ | ||
| accessToken, | ||
| }); | ||
|
|
||
| if (!isNonEmptyString(decoded.sub)) { | ||
| throw new InvalidTokenError( | ||
| "Token is missing required subject (sub) claim" | ||
| ); | ||
| } | ||
|
|
||
| let clientId: string | null = null; | ||
| if (isNonEmptyString(decoded.client_id)) { | ||
| clientId = decoded.client_id; | ||
| } else if (isNonEmptyString(decoded.azp)) { | ||
| clientId = decoded.azp; | ||
| } | ||
|
|
||
| if (!clientId) { | ||
| throw new InvalidTokenError( | ||
| "Token is missing required client identification (client_id or azp claim)." | ||
| ); | ||
| } | ||
|
|
||
| const token = { | ||
| token: accessToken, | ||
| clientId, | ||
| scopes: | ||
| typeof decoded.scope === "string" | ||
| ? decoded.scope.split(" ").filter(Boolean) | ||
| : [], | ||
| ...(decoded.exp && { expiresAt: decoded.exp }), | ||
| extra: { | ||
| sub: decoded.sub, | ||
| ...(isNonEmptyString(decoded.client_id) && { | ||
| client_id: decoded.client_id, | ||
| }), | ||
| ...(isNonEmptyString(decoded.azp) && { azp: decoded.azp }), | ||
| ...(isNonEmptyString(decoded.name) && { name: decoded.name }), | ||
| ...(isNonEmptyString(decoded.email) && { email: decoded.email }), | ||
| }, | ||
| } satisfies FastMCPAuthSession; | ||
|
|
||
| return token; | ||
| } catch (error) { | ||
| console.error(error); | ||
| if ( | ||
| error instanceof VerifyAccessTokenError || | ||
| error instanceof InvalidTokenError | ||
| ) { | ||
| /** | ||
| * WWW-Authenticate header is used for 401 responses as per spec. | ||
| */ | ||
| const wwwAuthValue = `Bearer error="invalid_token", error_description="${ | ||
| error.message | ||
| }", resource_metadata="${getOAuthProtectedResourceMetadataUrl( | ||
| new URL(MCP_SERVER_URL) | ||
| )}"`; | ||
| throw new Response(null, { | ||
| status: 401, | ||
| statusText: "Unauthorized", | ||
| headers: { | ||
| "WWW-Authenticate": wwwAuthValue, | ||
| }, | ||
| }); | ||
| } else if (error instanceof InsufficientScopeError) { | ||
| throw new Response(null, { | ||
| status: 403, | ||
| statusText: "Forbidden", | ||
| }); | ||
| } else { | ||
| throw new Response(null, { | ||
| status: 500, | ||
| statusText: "Internal Server Error", | ||
| }); | ||
| } | ||
| } | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.