MCP Authentication with FastMCP | PropelAuth
Announcing MCP Authentication: secure your MCP servers with PropelAuth
MCP servers are just a clean way to give an AI client (like Claude Desktop, Claude Code, ChatGPT, etc) a set of functions it can call. This can really be anything, like:
- A function that fetches the weather for a date/zip code
- A function that loads your calendar data from Google Calendar
- A function that manages your weekly goals in a file on your computer
- A function that manages weekly goals in a database for all employees of a company
We’re going to show how we can build an example MCP server and then secure it by adding authentication & authorization. We’ll do this in two steps:
- We’ll build a tiny, unauthenticated MCP server with a
whoamitool that always returns:{"user": "unknown"}. - We’ll update that tool to return
{"user": "{logged_in_users_email}"}(and only if the user has consented to theread:whoamiscope)
We’ll use FastMCP (Python) and PropelAuth’s MCP support.
Step 0: Set up our project
We’ll use uv to create a virtual environment and install FastMCP:
uv init whoami
cd whoami
uv venv # create a virtual env
source .venv/bin/activate # activate our virtual env
uv add "fastmcp>=2,<3" # 3 is currently still in beta
Step 1: Build the unauthenticated MCP server
Create a file called server.py:
from fastmcp import FastMCP
mcp = FastMCP("Who Am I")
@mcp.tool()
def whoami() -> dict[str, str]:
"""Returns the currently authenticated user"""
return {"username": "unknown"}
if __name__ == "__main__":
mcp.run(transport="http", host="127.0.0.1", port=8000)
We can then run this, which will create an MCP endpoint at http://localhost:8000/mcp
uv run server.py
Step 2: Use ngrok so external clients can reach your server
If your MCP server is only available on localhost, Claude Desktop won’t be able to connect to it. Claude needs to reach a URL over the internet.
In a second terminal, run:
ngrok http 8000
ngrok will print a public HTTPS URL like https://abc123.ngrok-free.app which tunnels the connection to your locally running MCP Server.
Your MCP URL is then https://abc123.ngrok-free.app/mcp
Step 3: Test our MCP Server
We’ll continue using Claude Desktop for testing. Go to Settings ⇒ Connectors ⇒ Add custom connector. Enter a name for your MCP Server as well as your MCP URL:
Aside: How does the user experience change for an authenticated MCP Server?
When we added the MCP Server to Claude Desktop before, we just pasted in a URL and Claude connected to it.
Let’s say now that we’re building a product called Example Project and we want users of our MCP Server to allow Claude to access their personal information within Example Project. How does that work?
At a high level, when we go to connect our MCP Server, the server will return an Unauthorized error and tell Claude Desktop how to authenticate. Claude Desktop will then open a browser and do a few things:
- Require the user to log in (if they aren't already)
- Ask the user for permission to perform actions on their account (also called a consent flow)
- Redirect the user back from the browser into Claude Desktop
After the user goes through that flow, Claude Desktop will have credentials to make requests on behalf of the user to our Example Project application. Let’s see how we can enable this easily with PropelAuth and FastMCP.
Step 4: Enable MCP Authentication in PropelAuth
First, we need to enable MCP support, which you can do in the PropelAuth dashboard:
While testing, we recommend only enabling it in the Test and/or Staging environments.
Next, you’ll want to choose which AI clients you allow your users to use. We provide configurations for popular clients like Cursor, Claude Desktop, ChatGPT, Gemini, etc or you can add your own custom clients.
Step 5: Add authentication to your FastMCP server
Now we’ll update server.py to require authentication and require the read:whoami scope. FastMCP takes care of most of this for you, so all we need to do is hook up some boilerplate at the top of our server:
from pydantic import AnyHttpUrl
from fastmcp import FastMCP
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
from fastmcp.server.dependencies import get_access_token
# From PropelAuth dashboard
PROPELAUTH_AUTH_URL = "https://auth.yourapp.com" # your PropelAuth Auth URL
REQUEST_VALIDATION_CLIENT_ID = "TODO"
REQUEST_VALIDATION_CLIENT_SECRET = "TODO"
# From ngrok
MCP_SERVER_BASE_URL = "https://abc123.ngrok-free.app"
# General config
REQUIRED_SCOPES = ["read:whoami"]
# This tells the MCP Server how to verify credentials that Claude Desktop sends
token_verifier = IntrospectionTokenVerifier(
introspection_url=f"{PROPELAUTH_AUTH_URL}/oauth/2.1/introspect",
client_id=REQUEST_VALIDATION_CLIENT_ID,
client_secret=REQUEST_VALIDATION_CLIENT_SECRET,
required_scopes=REQUIRED_SCOPES,
)
auth = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(PROPELAUTH_AUTH_URL + "/oauth/2.1")],
base_url=MCP_SERVER_BASE_URL,
)
# Make sure to hook up auth
mcp = FastMCP("Who Am I", auth=auth)
The IntrospectionTokenVerifier is responsible for verifying requests coming from Claude Desktop (or any other AI Client). For it to work, you must generate what we call “Request Validation Credentials.”
These are credentials that your MCP Server sends to PropelAuth so we can be sure that you are the one validating the request. You can generate these on the Request Validation tab of the dashboard.
And now, everything is in place for us to update our whoami tool call to return the user’s information:
@mcp.tool()
def whoami() -> dict[str, str]:
token = get_access_token()
# With required_scopes set, this isn't necessary, but it's good to be defensive
if not token or "read:whoami" not in token.scopes:
return {"username": "unknown"}
# claims has the user's information, like their ID or email address
username = token.claims.get("username", "unknown")
return {"username": username}
get_access_token returns all the information we have about the user, what scopes they consented to, etc.
And now we are all set up to test our authenticated MCP Server!
Step 6: Testing our Authenticated MCP Server
If it isn’t already running, we can run our server with:
uv run server.py
Let’s go back to Claude Desktop to add our server. We once again go to the Add custom connector modal, only this time we expand the Advanced Settings section:
Here you’ll see two fields: OAuth Client ID and OAuth Client Secret but both of them are optional. We actually have a choice here for how we want to proceed, and you can see the options in the PropelAuth Dashboard under “How Do Users Create OAuth Clients?”
One option is “Manually via Hosted Pages.” If you select this, PropelAuth provides a UI where your users can create their own OAuth Client ID and OAuth Client Secret to enter into this modal. This is the most secure option and what we typically recommend.
The other option is “Dynamic Client Registration.” If you select this, PropelAuth supports a new API which allows Claude Desktop to create the OAuth Client programmatically. The user doesn’t have to enter anything in for the OAuth Client ID or OAuth Client Secret fields, but there are some tradeoffs to be aware of with this approach.
After you add your connector, you’ll be prompted to authenticate and consent to the read:whoami scope. After you accept, you’ll be able to get Claude to query the tool again:
and now you can see that it correctly returns the username for the Example Project account that we are correctly logged in to.
PropelAuth also provides UIs for your users to revoke consent. If you revoke consent, you’ll be prompted to re-consent before you can call the tool again.
Summary
In this post, we built an MCP server in two passes:
- Unauthenticated: We created a tiny FastMCP server with a single
whoamitool that returns{"username": "unknown"}. We ran it locally withuv, exposed it to external AI clients usingngrok, and verified in Claude Desktop that the tool is discoverable and callable. - Authenticated + scoped: We enabled MCP support in PropelAuth, configured allowed AI clients, and created a
read:whoamiscope. Then we updated our FastMCP server to require authentication by validating OAuth 2.1 access tokens via introspection, enforcing theread:whoamiscope, and returning the logged-in user’susernamefrom token claims.
What’s cool about this approach is that you get the best of both worlds: MCP makes your tools feel like “native capabilities” inside AI clients, while OAuth-style auth keeps it safe and user-friendly. Users authenticate once, explicitly consent to scopes, and you can confidently expose real product data (not just demos) without handing the AI client broad access. That pattern scales from “hello world” all the way up to enterprise MCP servers where every tool call is tied to a real identity, explicit permissions, and revocable consent.