You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: astro/src/content/docs/extend/examples/protecting-mcp-servers.mdx
+53-53Lines changed: 53 additions & 53 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,20 +15,20 @@ Building a production Model Context Protocol (MCP) server requires you to consid
15
15
16
16
Starting with a simple, unprotected MCP server and adding OAuth protection step by step, this guide demonstrates how to:
17
17
18
-
* Examine a basic MCP server with a simple `get_name` tool
19
-
* Add FusionAuth OAuth configuration and token validation
20
-
* Register an MCP client (such as Claude Desktop) and test the protected tool
18
+
* Examine a basic MCP server with a simple `get_name` tool.
19
+
* Add FusionAuth OAuth configuration and token validation.
20
+
* Register an MCP client (such as Claude Desktop) and test the protected tool.
21
21
22
22
The following tutorial builds a working example of an OAuth-protected MCP server that you can extend with additional tools and security policies.
23
23
24
24
## Prerequisites
25
25
26
26
To follow this guide, you need the following installed:
27
27
28
-
* Docker Desktop (ensure it's running)
29
-
* An MCP client such as Claude Desktop or Cursor
30
-
* Python 3.12 or later (and `pip`)
31
-
* Node.js version 20.18.1 or later (ensure it's available in your PATH)
28
+
* Docker Desktop (ensure it's running).
29
+
* An MCP client such as Claude Desktop or Cursor.
30
+
* Python 3.12 or later (and `pip`).
31
+
* Node.js version 20.18.1 or later (ensure it's available in your PATH).
32
32
33
33
<Asidetype="tip"nodark="true">
34
34
Verify your Node.js installation:
@@ -133,9 +133,9 @@ if __name__ == "__main__":
133
133
134
134
This is a minimal MCP server with:
135
135
136
-
* A single `get_name` tool that returns a hardcoded greeting
137
-
* HTTP transport on port 8000
138
-
* No authentication
136
+
* A single `get_name` tool that returns a hardcoded greeting.
137
+
* HTTP transport on port 8000.
138
+
* No authentication.
139
139
140
140
### Test The Unprotected Server
141
141
@@ -147,9 +147,9 @@ docker compose up -d
147
147
148
148
Wait about 20 seconds for all services to start, including:
149
149
150
-
***FusionAuth:** On port 9011, preconfigured via Kickstart
151
-
***MCP Server:** The unprotected starter version on port 8000
152
-
***PostgreSQL:** The FusionAuth database
150
+
***FusionAuth:** On port 9011, preconfigured via Kickstart.
151
+
***MCP Server:** The unprotected starter version on port 8000.
152
+
***PostgreSQL:** The FusionAuth database.
153
153
154
154
Check that all services are healthy:
155
155
@@ -252,11 +252,11 @@ In this code:
252
252
253
253
The values of the configuration variables match the Kickstart configuration in `kickstart/kickstart.json`. When you ran `docker compose up`, FusionAuth detected a fresh installation and automatically ran the Kickstart configuration, which created:
254
254
255
-
* An OAuth application called "MCP Server"
256
-
* A test user (`test@example.com`/`password`)
257
-
* An admin user (`admin@example.com`/`password`) with access to the FusionAuth admin UI
258
-
* An API key
259
-
* A custom `get_name` scope
255
+
* An OAuth application called "MCP Server".
256
+
* A test user (`test@example.com`/`password`).
257
+
* An admin user (`admin@example.com`/`password`) with access to the FusionAuth admin UI.
258
+
* An API key.
259
+
* A custom `get_name` scope.
260
260
261
261
The Ids and keys are hardcoded in the Kickstart file. In production, you'd use environment variables or secrets management instead of hardcoded values.
262
262
@@ -324,10 +324,10 @@ class FusionAuthTokenVerifier(TokenVerifier):
324
324
325
325
This token verifier validates access tokens by:
326
326
327
-
1. Calling FusionAuth's UserInfo endpoint with the token. A `200 OK` response confirms the token is valid
328
-
2. Decoding the JWT to extract scopes, since the UserInfo endpoint does not return them. FastMCP needs scopes to enforce per-tool access control
329
-
3. Merging the UserInfo response into the token claims, so tools can read profile data (name, email) without making a separate API call
330
-
4. Returning an `AccessToken` object that FastMCP can use
327
+
1. Calling FusionAuth's UserInfo endpoint with the token. A `200 OK` response confirms the token is valid.
328
+
2. Decoding the JWT to extract scopes, since the UserInfo endpoint does not return them. FastMCP needs scopes to enforce per-tool access control.
329
+
3. Merging the UserInfo response into the token claims, so tools can read profile data (name, email) without making a separate API call.
330
+
4. Returning an `AccessToken` object that FastMCP can use.
331
331
332
332
### Step 3: Enable OAuth On The MCP Server
333
333
@@ -393,10 +393,10 @@ def get_name() -> str:
393
393
394
394
This code updates the `get_name` tool so that it:
395
395
396
-
1. Uses the FastMCP `get_access_token()` to get the validated access token from the request context
397
-
2. Reads `given_name` and `family_name` directly from the token claims, which were populated from the UserInfo response in `verify_token`, so no additional API call is needed
398
-
3. Falls back to `preferred_username`, then `email`, then the user Id if no name is available
399
-
4. Returns a personalized greeting
396
+
1. Uses the FastMCP `get_access_token()` to get the validated access token from the request context.
397
+
2. Reads `given_name` and `family_name` directly from the token claims, which were populated from the UserInfo response in `verify_token`, so no additional API call is needed.
398
+
3. Falls back to `preferred_username`, then `email`, then the user Id if no name is available.
399
+
4. Returns a personalized greeting.
400
400
401
401
### Step 5: Rebuild And Restart
402
402
@@ -459,9 +459,9 @@ Each MCP client needs its own dedicated OAuth application in FusionAuth, separat
459
459
460
460
For the OAuth grant to work, FusionAuth needs to know:
461
461
462
-
* Which redirect URLs are allowed for each MCP client
463
-
* Whether PKCE is required (some MCP clients use PKCE for security)
464
-
* Which OAuth grants are enabled
462
+
* Which redirect URLs are allowed for each MCP client.
463
+
* Whether PKCE is required (some MCP clients use PKCE for security).
This includes the following key configuration values:
514
514
515
-
*`authorizedURLValidationPolicy: "AllowWildcards"` enables wildcard matching for redirect URLs, so the OAuth callback works regardless of which port the MCP client picks
516
-
*`proofKeyForCodeExchangePolicy: "Required"` enforces PKCE for security
517
-
*`clientAuthenticationPolicy: "NotRequiredWhenUsingPKCE"` means that no client secret is needed when PKCE is used
518
-
*`scopeHandlingPolicy: "Compatibility"` allows custom scopes like `get_name`
519
-
*`unknownScopePolicy: "Allow"` permits scopes that aren't pre-defined in FusionAuth
515
+
*`authorizedURLValidationPolicy: "AllowWildcards"` enables wildcard matching for redirect URLs, so the OAuth callback works regardless of which port the MCP client picks.
516
+
*`proofKeyForCodeExchangePolicy: "Required"` enforces PKCE for security.
517
+
*`clientAuthenticationPolicy: "NotRequiredWhenUsingPKCE"` means that no client secret is needed when PKCE is used.
518
+
*`scopeHandlingPolicy: "Compatibility"` allows custom scopes like `get_name`.
519
+
*`unknownScopePolicy: "Allow"` permits scopes that aren't pre-defined in FusionAuth.
520
520
521
521
The script returns the generated client Id, which you need for configuring your MCP client.
522
522
@@ -604,9 +604,9 @@ Restart your MCP client to load the new configuration.
604
604
605
605
When your MCP client starts, it does the following:
606
606
607
-
1. Discovers the OAuth configuration from the MCP server
608
-
2. Opens your browser to FusionAuth's login page
609
-
3. Asks you to authenticate and grant consent
607
+
1. Discovers the OAuth configuration from the MCP server.
608
+
2. Opens your browser to FusionAuth's login page.
609
+
3. Asks you to authenticate and grant consent.
610
610
611
611
<Asidetype="note"nodark="true">
612
612
If the OAuth flow doesn't start or you encounter errors, consult the [Troubleshooting section](#troubleshooting) below.
@@ -666,28 +666,28 @@ When an MCP client connects to the MCP server, here's what happens:
666
666
667
667
The MCP server validates tokens using the FusionAuth UserInfo endpoint. This is a standard OpenID Connect endpoint that:
668
668
669
-
* Accepts `Bearer` tokens in the `Authorization` header
670
-
* Returns user information if the token is valid
671
-
* Does not require client authentication (because the token authenticates the request)
669
+
* Accepts `Bearer` tokens in the `Authorization` header.
670
+
* Returns user information if the token is valid.
671
+
* Does not require client authentication (because the token authenticates the request).
672
672
673
673
If the UserInfo endpoint returns `200 OK`, the token is valid and active.
674
674
675
675
### Scope Enforcement
676
676
677
677
The `required_scopes=["openid", "profile", "email", "get_name"]` parameter ensures that:
678
678
679
-
* Only tokens containing all four scopes can execute tools
680
-
* FastMCP automatically validates scopes before tool execution
681
-
* Users must explicitly grant these scopes during the consent flow
682
-
* The `openid`, `profile`, and `email` scopes cause FusionAuth to include user profile data in the UserInfo response, so the MCP server can read the user's name without a separate API call
679
+
* Only tokens containing all four scopes can execute tools.
680
+
* FastMCP automatically validates scopes before tool execution.
681
+
* Users must explicitly grant these scopes during the consent flow.
682
+
* The `openid`, `profile`, and `email` scopes cause FusionAuth to include user profile data in the UserInfo response, so the MCP server can read the user's name without a separate API call.
683
683
684
684
### Custom Scopes In FusionAuth
685
685
686
686
FusionAuth requires an Essentials license (or higher) to use custom scopes. The Kickstart configuration:
687
687
688
-
1. Activates the license
689
-
2. Creates the `get_name` scope
690
-
3. Configures the application with the scope
688
+
1. Activates the license.
689
+
2. Creates the `get_name` scope.
690
+
3. Configures the application with the scope.
691
691
692
692
When users authorize the application, FusionAuth redirects them back to the MCP client with an "Authorization successful!" message.
693
693
@@ -712,9 +712,9 @@ For a production deployment, you would host the MCP server on a remote machine a
712
712
713
713
For a remote deployment you need:
714
714
715
-
* A server hosting your MCP server, accessible over the internet
716
-
* A reverse proxy such as [Caddy](https://caddyserver.com/) (which handles TLS certificates automatically) or TLS configured directly on your load balancer to serve HTTPS on the MCP server endpoint
717
-
* A publicly accessible FusionAuth instance, since the OAuth browser redirect requires the user's browser to reach FusionAuth
715
+
* A server hosting your MCP server, accessible over the internet.
716
+
* A reverse proxy such as [Caddy](https://caddyserver.com/) (which handles TLS certificates automatically) or TLS configured directly on your load balancer to serve HTTPS on the MCP server endpoint.
717
+
* A publicly accessible FusionAuth instance, since the OAuth browser redirect requires the user's browser to reach FusionAuth.
718
718
719
719
### Server Configuration
720
720
@@ -866,8 +866,8 @@ When you see this error, look at the error message to find which URL was attempt
866
866
867
867
You now have a working OAuth-protected MCP server, which you can extend by:
868
868
869
-
* Adding more tools with different scope requirements
870
-
* Implementing role-based access control using FusionAuth roles
871
-
* Integrating additional FusionAuth features like multi-factor authentication
869
+
* Adding more tools with different scope requirements.
870
+
* Implementing role-based access control using FusionAuth roles.
871
+
* Integrating additional FusionAuth features like multi-factor authentication.
872
872
873
873
The token validation and scope enforcement patterns demonstrated here also apply to OAuth-protected REST APIs, GraphQL servers, and other authenticated services.
0 commit comments