This example demonstrates how to set up an OAuth2-protected MCP server using the NVIDIA NeMo Agent Toolkit FastMCP server runtime. This complements the unprotected Simple Calculator FastMCP example to demonstrate both authenticated and unauthenticated MCP servers using the FastMCP server runtime.
This example uses per-user mode, enabling complete per-user isolation while accessing the same protected calculator tools.
This example consists of three main components:
graph TB
subgraph Client["MCP Client (Calculator Client)"]
direction TB
CalculatorClient["Calculator Client Workflow"]
MCPClientPlugin["MCP Client Plugin<br/>• Discovers tools<br/>• Handles OAuth2 flow<br/>• Manages JWT tokens"]
CalculatorClient --> MCPClientPlugin
end
subgraph Server["MCP Server Using FastMCP Runtime (Calculator)"]
direction TB
Calculator["Calculator Workflow"]
OAuthMiddleware["FastMCP OAuth2 Resource Server Middleware<br/>• Introspects tokens<br/>• Checks scopes<br/>• Validates activity"]
Calculator --> OAuthMiddleware
end
subgraph Auth["Authorization Server (Keycloak)"]
direction LR
AuthCore["Keycloak OAuth2 Server<br/>• Authenticates users<br/>• Issues tokens<br/>• Provides introspection endpoint"]
end
MCPClientPlugin -->|"① MCP initialize<br/>(triggers authentication flow)"| Calculator
MCPClientPlugin -->|"② OAuth2 Authorization Flow<br/>(Browser-based)"| AuthCore
MCPClientPlugin -->|"③ MCP tool calls<br/>(Authorization: Bearer token)"| Calculator
OAuthMiddleware -.->|"Introspect token<br/>(RFC 7662)"| AuthCore
style Client fill:#e1f5ff
style Server fill:#ffe1e1
style Auth fill:#e1ffe1
- NVIDIA NeMo Agent Toolkit installed (see Installation Guide)
- Keycloak server running locally (see setup instructions below)
# Start Keycloak
docker run -d --name keycloak \
-p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-devWait for Keycloak to start (about 30-60 seconds). Check logs:
docker logs -f keycloakLook for: Listening on: http://0.0.0.0:8080
Access Keycloak: Open http://localhost:8080 in your browser
-
Log in to Keycloak Admin Console:
- Username:
admin - Password:
admin
- Username:
-
Verify you are in the
masterrealm (top-left dropdown) -
Create the
calculator_mcp_executescope (for the calculator):- Go to Client scopes (left sidebar)
- Click Create client scope
- Fill in:
- Name:
calculator_mcp_execute - Description:
Permission to execute calculator operations - Type:
Optional - Protocol:
openid-connect - Include in token scope:
On
- Name:
- Click Save
-
Verify OpenID discovery endpoint:
curl http://localhost:8080/realms/master/.well-known/openid-configuration | python3 -m json.toolYou should see the OAuth2 endpoints, including:
authorization_endpoint:http://localhost:8080/realms/master/protocol/openid-connect/authtoken_endpoint:http://localhost:8080/realms/master/protocol/openid-connect/tokenintrospection_endpoint:http://localhost:8080/realms/master/protocol/openid-connect/token/introspect
You can register the client manually or use the dynamic client registration feature. For testing, manual registration is used.
-
In Keycloak Admin Console, go to Clients (left sidebar)
-
Click Create client
-
General settings:
- Client ID:
nat-mcp-client - Client type:
OpenID Connect - Click Next
- Client ID:
-
Capability config:
- Client authentication:
On(confidential client) - Authorization:
Off - Authentication flow:
- Standard flow (authorization code)
- Direct access grants
- Click Next
- Client authentication:
-
Login settings:
- Valid redirect URIs:
http://localhost:8000/auth/redirect - Web origins:
http://localhost:8000 - Click Save
- Valid redirect URIs:
-
Add client scope if not already added:
- Go to Client scopes tab
- Click Add client scope
- Select
calculator_mcp_execute - Choose Optional
- Click Add
-
Set Consent required:
- Go to Settings tab
- Toggle Consent required to
On(scroll down to the bottom of the page to see the setting) - Click Save
-
Get client credentials:
- Go to Credentials tab
- Copy the Client secret
- Note the Client ID:
nat-mcp-client
The FastMCP server runtime validates OAuth2 bearer tokens for this example. Register a resource server client so the MCP server can authenticate to Keycloak when introspecting opaque tokens, and so JWT access tokens can be audience-bound to the same resource server.
-
In Keycloak Admin Console, go to Clients
-
Click Create client
-
General settings:
- Client ID:
nat-mcp-resource-server - Client type:
OpenID Connect - Click Next
- Client ID:
-
Capability config:
- Client authentication:
On(confidential client) - Authorization:
Off - Authentication flow:
- Direct access grants
- Click Next
- Click Save
- Client authentication:
-
Get resource server credentials:
- Go to Credentials tab
- Copy the Client secret
- Note the Client ID:
nat-mcp-resource-server
-
Add the resource server audience to calculator tokens:
- Go back to Client scopes (left sidebar)
- Open the
calculator_mcp_executescope - Go to the Mappers tab
- Click Configure a new mapper
- Select Audience
- Configure the mapper:
- Name:
mcp-resource-server-audience - Included Client Audience:
nat-mcp-resource-server - Included Custom Audience: Leave blank
- Add to ID token:
Off - Add to access token:
On - Add to token introspection:
On(if available in your Keycloak version)
- Name:
- Click Save
The server config expects
nat-mcp-resource-serverin the access token'saudclaim. Without this mapper, browser consent can succeed but the protected FastMCP server still rejects MCP initialization.
# Terminal 1
export NAT_CALCULATOR_RESOURCE_CLIENT_ID="nat-mcp-resource-server" # Resource server client ID
export NAT_CALCULATOR_RESOURCE_CLIENT_SECRET="<your-resource-client-secret>" # Resource server client secret from Step 4.5
nat fastmcp server run --config_file examples/MCP/simple_calculator_fastmcp_protected/configs/config-server.ymlThe client workflow also starts the mcp_time function group with python -m mcp_server_time. If you installed this example package, the dependency is included automatically. To verify it is available before running the workflow:
uv run --project examples/MCP/simple_calculator_fastmcp_protected --locked python -m mcp_server_time --helpSet the client ID and client secret from Step 3 in the environment variables:
# Terminal 2
export NAT_CALCULATOR_CLIENT_ID="nat-mcp-client" # OAuth client ID for the MCP client (auth code flow)
export NAT_CALCULATOR_CLIENT_SECRET="<your-client-secret>" # OAuth client secret for the MCP client from Step 3.8
nat run --config_file examples/MCP/simple_calculator_fastmcp_protected/configs/config-client.yml \
--input "Is the product of 2 and 3 greater than the current hour of the day?"What should happen:
- Browser opens with Keycloak login page
- Log in with any user (or create one)
- Consent page appears requesting
calculator_mcp_executescope - Browser redirects back to
localhost:8000/auth/redirect - Workflow continues and calls the calculator
- Response returned successfully
To stop and remove Keycloak:
docker stop keycloak
docker rm keycloakTo restart with clean state:
docker rm -f keycloak
# Then run the start command againThis configures the protected MCP server frontend with OAuth2 resource server authentication using the FastMCP server runtime:
general:
front_end:
_type: fastmcp
name: "Protected Calculator FastMCP"
port: 9902
host: localhost
server_auth:
issuer_url: http://localhost:8080/realms/master
discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration
introspection_endpoint: http://localhost:8080/realms/master/protocol/openid-connect/token/introspect
audience: nat-mcp-resource-server
client_id: ${NAT_CALCULATOR_RESOURCE_CLIENT_ID:-"nat-mcp-resource-server"}
client_secret: ${NAT_CALCULATOR_RESOURCE_CLIENT_SECRET}
scopes: [calculator_mcp_execute]This configures an MCP client to connect to the protected MCP server in per-user mode:
function_groups:
mcp_calculator_protected:
_type: per_user_mcp_client
server:
transport: streamable-http
url: http://localhost:9902/mcp
auth_provider: mcp_oauth2_calculator
authentication:
mcp_oauth2_calculator:
_type: mcp_oauth2
server_url: http://localhost:9902/mcp
redirect_uri: http://localhost:8000/auth/redirect
client_id: ${NAT_CALCULATOR_CLIENT_ID:-"nat-mcp-client"}
client_secret: ${NAT_CALCULATOR_CLIENT_SECRET}
scopes: [calculator_mcp_execute]
workflow:
_type: per_user_react_agent
tool_names: [mcp_calculator_protected]If the server logs JWT audience ['master-realm', 'account'] does not contain required audience 'nat-mcp-resource-server', the access token does not include the resource server audience. Re-check the calculator_mcp_execute client-scope mapper from Step 4.6:
- Included Client Audience must be
nat-mcp-resource-server - Add to access token must be
On - The
calculator_mcp_executescope must be assigned to thenat-mcp-clientclient and requested by the client workflow
examples/MCP/simple_calculator_fastmcp/: FastMCP calculator example without authentication
- FastMCP Server - Learn about running the FastMCP server runtime
- MCP Client - Learn about using the MCP client to interact with the MCP server