Skip to main content

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:

The intuition
  • 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:

  1. 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.
  2. 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.
  3. Each message mints a fresh token. When a thread message triggers the tool, MINEO mints a JWT whose sub claim 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).
  4. 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.
  5. The pod verifies before any tool runs. FastMCP's JWTVerifier checks 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

AttackWhere it dies
The model invents a user idThe 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 tokenEditing the payload breaks the ES256 signature → JWTVerifier returns 401.
A pod operator tries to forge a tokenThe pod only holds the public key; you cannot sign with it.
A token is captured and replayedIt 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()
info

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%/}"
SettingValue
MINEO_JWKS_URLInjected 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 8000The 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:

FieldValue
App typeMCP
App main pathThe 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.

warning

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

  1. Open your assistant's MCP Tools tab and add a server pointing at the Live App URL.
  2. Set its auth mode to Per-user identity.
  3. Click Sync to index the tools. If the app is hibernated, MINEO wakes it up automatically and waits for the initialize handshake to succeed before proceeding.
  4. From a thread, ask the assistant who you are — whoami returns your uuid. Another user gets their uuid, and neither can obtain the other's.

Next steps