Per-User Authenticated MCP
Host an MCP server on MINEO whose tools know which user invoked them — and prove it, so no user can ever pretend to be another. MINEO attaches a short-lived, signed token to every tool call; your server checks the signature with a public key and trusts the identity inside. No popups, no shared secrets, no way for the assistant to lie about who it is.
The problem
You write a tool. When it runs, you want to know the real user behind the request — to scope data to them, log who did what, or call another API on their behalf.
Two things make this hard:
- A tool call is generated by the LLM. If the user's id were just an argument the model fills in, the model (or a crafted prompt) could put any id there. You can't trust it.
- You don't want to manage passwords or API keys per user, and you don't want the user clicking through a consent screen every time.
You need an identity that comes from MINEO, not from the model, and that your server can verify on its own.
The idea: a signature only MINEO can make
This works with asymmetric keys — the same maths behind HTTPS and SSH. There are two matching keys:
- Private key — a stamp that only MINEO holds. It signs each token. It never leaves the MINEO backend.
- Public key — anyone can use it to check that a stamp is genuine, but nobody can reproduce the stamp with it.
So MINEO publishes the public key for the world to read, and that's safe: holding it lets your server verify identities but never forge them. Verifying ≠ signing.
The signed token is a JWT (a small JSON blob with a signature) using the ES256 algorithm (ECDSA on the P-256 curve). Inside it, MINEO writes the real caller's uuid — read straight from its database, never from the model. Your server downloads the public key once (as a standard JWK Set) and from then on can validate every token offline.
How it works
End to end:
- You pick the auth mode. On the assistant's MCP server you set auth mode = Per-user identity. That's the only switch — you never set headers by hand.
- Sync authenticates too. Tool discovery (
list_tools) also needs a valid token, so the Sync action signs a token for whoever clicks Sync. Set the auth mode before syncing. - Each message mints a fresh token. When a thread message triggers the tool, MINEO mints a JWT whose
subclaim is the calling user's uuid — taken from the database, so the model can't influence it — and short-lived (it expires soon after being minted). - The token rides the standard header. It travels as
Authorization: Bearer <token>on every tool call. Because per-user calls always run from MINEO's own backend, the token never passes through the LLM provider's infrastructure. - The pod verifies before any tool runs. FastMCP's
JWTVerifierchecks the signature, expiry, issuer and audience at the server level. A request with no token or a bad token is rejected with 401 before your tool code is ever reached.
The public verification key is served at:
https://<your-mineo-host>/.well-known/mineo-jwks.json
Why it can't be spoofed
| Attack | Where it dies |
|---|---|
| The model invents a user id | The id is the token's sub, written by MINEO from its database — it is never a tool argument the model controls. |
Someone edits sub in the token | Editing the payload breaks the ES256 signature → JWTVerifier returns 401. |
| A pod operator tries to forge a token | The pod only holds the public key; you cannot sign with it. |
| A token is captured and replayed | It is short-lived — it expires soon after being minted (exp), bounding the window. |
Step 1: Write the server
A minimal example with a whoami tool. Authentication is enforced for the whole server by FastMCP's JWTVerifier — individual tools never parse headers, so a new tool cannot forget to verify:
# app.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.server.dependencies import get_access_token
auth = JWTVerifier(
jwks_uri=os.environ["MINEO_JWKS_URL"],
issuer="mineo",
audience="mineo-mcp",
algorithm="ES256",
)
mcp = FastMCP("mineo-identity-demo", auth=auth)
@mcp.tool
def whoami() -> dict:
token = get_access_token() # already verified by JWTVerifier
return {"user_uuid": token.claims["sub"], "thread_uuid": token.claims.get("thr")}
app = mcp.http_app()
With server-level auth, tool discovery (list_tools) requires a valid token too. MINEO's Sync action handles this by identifying the user who clicks Sync — make sure the server's auth mode is set to Per-user identity before syncing.
Step 2: Set the entrypoint
pip install fastmcp && uvicorn app:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*' --root-path "${MINEO_LIVE_APP_URL_PATH%/}"
| Setting | Value |
|---|---|
MINEO_JWKS_URL | Injected automatically into every Live App pod, pointing at /.well-known/mineo-jwks.json on your MINEO host. Not sensitive. A project .env variable of the same name is ignored — the platform value always wins. To target a different host (e.g. local Docker), read a differently-named variable or hardcode jwks_uri. |
--port 8000 | The port MINEO routes to. |
--root-path "${MINEO_LIVE_APP_URL_PATH%/}" | Re-adds the public prefix behind MINEO's proxy. |
Step 3: Configure wake-up behavior
Set the Availability mode to On-demand (or Scheduled outside its window) so the app wakes up automatically when an MCP client connects. Then go to the Connection section and configure:
| Field | Value |
|---|---|
| App type | MCP |
| App main path | The path your MCP server listens on (e.g. /mcp or /). |
When set to MCP, MINEO sends a JSON-RPC initialize request to the app after the pod starts, and waits for a valid JSON-RPC result before forwarding the original request. This ensures the MCP server is fully ready before the first tool call arrives.
If the original request carries an Authorization header, it is forwarded to the readiness probe so your server can validate it during initialization.
The Live App must be public: the per-user token is the authentication. Enabling login required on the Live App would redirect MINEO's MCP client to the login page and break every call.
Step 4: Connect it to an assistant
- Open your assistant's MCP Tools tab and add a server pointing at the Live App URL.
- Set its auth mode to Per-user identity.
- Click Sync to index the tools. If the app is hibernated, MINEO wakes it up automatically and waits for the
initializehandshake to succeed before proceeding. - From a thread, ask the assistant who you are —
whoamireturns your uuid. Another user gets their uuid, and neither can obtain the other's.
Next steps
- Retrieving user info from Streamlit — the browser-session equivalent for full Live Apps.
- Entrypoints and Hot Reload