FastMCP Integration
Overview
Section titled “Overview”FastMCP is the most popular Python framework for building MCP servers. Its auth model separates concerns cleanly:
- Authgent is the Authorization Server — it handles OAuth 2.1 flows, issues JWTs, manages client registration, and serves metadata endpoints.
- FastMCP is the Resource Server — it only validates JWTs. No OAuth logic runs in your MCP server.
When an MCP client connects, FastMCP’s RemoteAuthProvider fetches Authgent’s OAuth metadata, redirects the client through the authorization flow, and validates the resulting JWT using Authgent’s JWKS endpoint. Your MCP server never implements any OAuth endpoints.
Installation
Section titled “Installation”pip install fastmcpFastMCP includes all necessary JWT verification dependencies. No additional packages required.
RemoteAuthProvider setup
Section titled “RemoteAuthProvider setup”# server.py — FastMCP with Authgentimport osfrom fastmcp import FastMCPfrom fastmcp.server.auth import RemoteAuthProvider
# Authgent's base URL — FastMCP fetches all OAuth metadata from hereAUTHGENT_ISSUER = os.environ.get("AUTHGENT_ISSUER", "https://auth.yourcompany.com")MCP_AUDIENCE = os.environ.get("MCP_AUDIENCE", "https://mcp.yourcompany.com")
auth = RemoteAuthProvider( # Authgent's discovery endpoint # FastMCP fetches /.well-known/oauth-authorization-server automatically authorization_server_url=AUTHGENT_ISSUER,
# The audience claim your tokens must contain # Must match the MCP server URL you registered in Authgent audience=MCP_AUDIENCE,
# Scopes this MCP server requires for all requests required_scopes=["mcp:read"],)
mcp = FastMCP( name="My MCP Server", auth=auth,)Tool-level scope enforcement
Section titled “Tool-level scope enforcement”You can require specific scopes on individual tools. This is enforced at the middleware level — if the token lacks the required scope, the request is rejected before your tool code runs.
@mcp.tool()async def list_projects() -> list[dict]: """List all projects. Requires the default mcp:read scope.""" ctx = mcp.get_context() user_id = ctx.auth.claims.get("sub") return await db.get_projects_for_user(user_id)
@mcp.tool(scopes=["mcp:write"])async def create_project(name: str, description: str) -> dict: """Create a project. Requires mcp:write scope in addition to mcp:read.""" ctx = mcp.get_context() return await db.create_project( owner=ctx.auth.claims["sub"], name=name, description=description, )
@mcp.tool(scopes=["admin"])async def delete_project(project_id: str) -> dict: """Delete a project. Requires admin scope.""" ctx = mcp.get_context() return await db.delete_project(project_id, deleted_by=ctx.auth.claims["sub"])Accessing token claims
Section titled “Accessing token claims”Every authenticated request has claims available via the context:
@mcp.tool()async def whoami() -> dict: """Return the authenticated user's identity.""" ctx = mcp.get_context() claims = ctx.auth.claims
return { "user_id": claims.get("sub"), "email": claims.get("email"), "name": claims.get("name"), "scopes": claims.get("scope", "").split(" "), "issuer": claims.get("iss"), "audience": claims.get("aud"), "expires_at": claims.get("exp"), }
@mcp.tool()async def get_user_data() -> dict: """Fetch data scoped to the authenticated user.""" ctx = mcp.get_context() user_id = ctx.auth.claims["sub"]
# Use the user ID to scope database queries # Only return data the authenticated user owns return await db.get_user_data(user_id)Running the server
Section titled “Running the server”if __name__ == "__main__": # HTTP transport for remote MCP clients (Claude Desktop, Cursor, etc.) mcp.run(transport="http", host="0.0.0.0", port=8000, path="/mcp")Transport options:
| Transport | Use case | Auth |
|---|---|---|
http | Remote clients (Claude Desktop, Cursor) | OAuth via Authgent |
sse | Server-sent events (legacy) | OAuth via Authgent |
stdio | Local dev, subprocess | No auth needed |
For production, always use http transport with Authgent.
Testing with MCP Inspector
Section titled “Testing with MCP Inspector”npx @modelcontextprotocol/inspector http://localhost:8000/mcpThe inspector automatically:
- Discovers
/.well-known/oauth-protected-resourceon your MCP server - Finds Authgent’s authorization endpoint from the metadata
- Runs the OAuth flow (Authorization Code + PKCE)
- Sends authenticated requests to your tools
Docker deployment
Section titled “Docker deployment”Dockerfile
Section titled “Dockerfile”FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 8000
CMD ["python", "server.py"]requirements.txt
Section titled “requirements.txt”fastmcp>=2.0.0docker-compose.yml
Section titled “docker-compose.yml”version: "3.9"services: authgent: image: authgent/authgent:latest ports: - "8080:8080" environment: AUTHGENT_ISSUER: http://authgent:8080 AUTHGENT_SIGNING_KEY: /keys/ec-private.pem AUTHGENT_SIGNING_ALG: ES256 AUTHGENT_DB_DSN: sqlite:///data/authgent.db volumes: - ./keys/ec-private.pem:/keys/ec-private.pem:ro - authgent-data:/data
mcp-server: build: . ports: - "8000:8000" environment: AUTHGENT_ISSUER: http://authgent:8080 MCP_AUDIENCE: http://mcp-server:8000 depends_on: - authgent
volumes: authgent-data:Start everything:
# Generate signing keys firstopenssl ecparam -genkey -name prime256v1 -noout -out keys/ec-private.pemchmod 600 keys/ec-private.pem
# Start both servicesdocker compose up -d
# Verify Authgent is runningcurl http://localhost:8080/.well-known/oauth-authorization-server | jq .
# Test with MCP Inspectornpx @modelcontextprotocol/inspector http://localhost:8000/mcpTroubleshooting
Section titled “Troubleshooting”Token expired
Section titled “Token expired”Error: token_expired — JWT exp claim is in the pastAuthgent issues tokens with a 5-minute TTL by default. If you see this error:
- Check that your server’s clock is synchronized (NTP)
- The MCP client should automatically refresh tokens — if it doesn’t, it may be using an older SDK version
Wrong audience
Section titled “Wrong audience”Error: invalid_audience — expected "https://mcp.yourcompany.com"The audience in your RemoteAuthProvider must exactly match the resource URL registered in Authgent. Check:
- No trailing slash differences (
https://mcp.yourcompany.comvshttps://mcp.yourcompany.com/) - The URL matches what you configured in Authgent’s resource registration
Missing scope
Section titled “Missing scope”Error: insufficient_scope — required scope "mcp:write" not presentThe token was issued without the required scope. This happens when:
- The MCP client didn’t request the scope during authorization
- The user didn’t consent to the scope
- The scope isn’t configured in Authgent’s allowed scopes for this resource
Connection refused to Authgent
Section titled “Connection refused to Authgent”Error: ConnectionError — Cannot connect to authorization_server_urlIn Docker Compose, use the service name (http://authgent:8080) not localhost. From outside Docker, use http://localhost:8080.