Remote access — sign-in & identity modes¶
When more than one person uses your server, you need real sign-in. MCP clients authenticate to a remote server with OAuth 2.1 + PKCE and discover the sign-in endpoint via RFC 9728, so you need an OIDC authorization server in front of Précis Finance MCP.
Your ingress must serve /.well-known/oauth-protected-resource and forward
the authorization-server discovery and endpoint URLs it advertises. In bundled
Keycloak mode, Précis also serves origin-relative compatibility routes:
/.well-known/oauth-authorization-server, /authorize, /register, /token.
Their metadata names the same canonical Keycloak realm issuer as protected-resource
discovery and advertises authorization-response issuer identification (iss).
The aliases forward to Keycloak and do not create a second token issuer.
The bundled Caddy and reference nginx configurations expose these routes;
a custom proxy must route them explicitly if its fallback serves another app.
External-OIDC mode uses the external issuer's endpoints and disables these
Keycloak-specific aliases.
For remote OAuth, publish the issuer under a resolvable hostname with a trusted TLS certificate. Some clients reject private-IP authorization-server identifiers even when the MCP resource itself is reachable on the LAN: Codex 0.161.0's pinned SDK policy does this during protected-resource discovery. Its subsequent origin fallback can report an issuer mismatch for a realm-based issuer. Origin aliases do not remove that client restriction; retain the actual issuer consistently in Keycloak, Précis metadata and token validation. The issuer hostname need not match the MCP resource hostname, but clients must resolve and trust it.
You pick the sign-in posture at install with one variable, PRECIS_AUTH_MODE:
| Mode | PRECIS_AUTH_MODE |
Sign-in | When |
|---|---|---|---|
| A | devkey |
One static key, localhost only | Trial / demo / your own machine — see the Quickstart. Single-user. |
| B | keycloak |
The bundled Keycloak (optionally federated to your IdP) | You want a ready-to-run sign-in provider. The default multi-user path. |
| C | oidc |
Your own OIDC IdP (Auth0, Okta, Entra, Ping, …), trusted directly | You already run an IdP and want no extra moving parts. |
The modes differ only in which issuer signs the token. The token check, the per-user permission model, and provisioning are identical across all three.
The dev-key server is a separate entrypoint — not part of B/C
Mode A is a distinct, localhost-only process (python -m precis_mcp.server),
enabled by ENABLE_MCP_DEV_SERVER=1 and off by default. The multi-user
modes B/C run a different entrypoint (precis_mcp.app_open, the OAuth-gated
/mcp server) and the production bundle doesn't include the dev server.
PRECIS_AUTH_MODE is enforced on both entrypoints: if you set
PRECIS_AUTH_MODE=keycloak or oidc, the dev server refuses to start even
if ENABLE_MCP_DEV_SERVER=1 — so a B/C-configured host can't accidentally
run a no-auth server. (And app_open likewise refuses devkey.)
Mode B — bundled Keycloak (PRECIS_AUTH_MODE=keycloak)¶
Précis Finance MCP ships a ready-to-run Keycloak in the bundled docker-compose, with a
committed realm (sign-in client, the precis_user_id user mapper, and the
audience mapper for /mcp) plus a per-deploy reconcile. You don't configure OIDC
by hand.
The bundled realm uses the Précis login theme in keycloak/themes/precis/login,
mounted separately from the server image. It includes a light appearance,
self-hosted fonts and the connected-P brand mark and favicons. Theme updates
must be installed with the deployment files; production Keycloak theme caches
require a restart to load them. External OIDC providers in mode C own their
sign-in appearance.
Release 0.2.7 bundles Keycloak 26.7.5, which removes the three server-runtime findings temporarily accepted in 0.2.6 (CVE-2026-13506, CVE-2026-75595, and CVE-2026-8763). The bundled PostgreSQL configuration does not load Keycloak's MSSQL JDBC driver; its separate scan exception remains. See the 0.2.7 changelog for the full scope of this update.
| Variable | Purpose | Default |
|---|---|---|
PRECIS_AUTH_MODE |
keycloak |
— |
PRECIS_BASE_URL |
Public address of your server; the other URLs derive from it | — |
KC_BASE_URL_INTERNAL |
Backend → Keycloak (container topology) | http://localhost:8080/auth |
KC_BOOTSTRAP_ADMIN_PASSWORD |
Keycloak realm-admin password | — |
KC_REALM / KC_CLIENT_ID |
Realm + client | precis / precis-spa |
KC_MCP_AUDIENCE |
Expected token audience on /mcp |
derived from PRECIS_BASE_URL |
KC_ENABLE_EXCEL_ADDIN |
Provision and advertise the hosted Excel add-in public client | unset |
KC_ADDIN_REDIRECT_URIS |
Optional override for the add-in callback URI | derived from PRECIS_BASE_URL |
Bring it up:
docker compose -f deploy/docker-compose.yml up -d
This pulls the pinned published image (ghcr.io/precis-finance/precis-finance-mcp,
selected by PRECIS_MCP_TAG) and starts your data store, the bundled Keycloak
with its seeded realm, and the server. Pulling a pinned release is the default;
docker compose … up -d --build builds from source instead (rolling main /
forks). From a workstation, scripts/deploy-mcp.sh does the same, pull-first —
see the production checklist. To let your users sign in with your corporate IdP, point Keycloak's
identity-brokering at it (OIDC or SAML) — Keycloak stays the issuer and the
above doesn't change; the walkthrough is
Sign in with your corporate IdP. This is also the
path for claude.ai / ChatGPT: their connectors self-register (DCR), which
the bundled Keycloak supports.
The precis-mcp client scope supplies the MCP audience. Although it is a
realm default, dynamic registration can make it optional on a client. The
realm-reconcile script enforces include.in.token.scope=true for new and
existing scopes: the token response then reports the granted scope, allowing
clients that replay reported scopes on refresh to retain their MCP audience.
For an existing installation with this scope hidden, apply the updated realm
reconcile and sign the connector in again; an already downscoped refresh grant
cannot restore the omitted scope. Verify a tool call after the access token
expires, as initial login alone does not accept refresh continuity.
To enable the hosted Excel add-in in mode B, set KC_ENABLE_EXCEL_ADDIN=true
before the Keycloak reconcile runs. The default callback is
https://<your-host>/excel/auth-callback.html; set KC_ADDIN_REDIRECT_URIS
only when your ingress needs a non-standard public origin. The add-in bundle is
baked into the published image and served at /excel; see
Précis for Excel.
Mode C — your own OIDC IdP (PRECIS_AUTH_MODE=oidc)¶
No Keycloak runs. The verifier points straight at your IdP. Register a client in your IdP and tell Précis Finance MCP about the issuer:
| Variable | Purpose |
|---|---|
PRECIS_AUTH_MODE |
oidc |
OIDC_ISSUER |
Your IdP's issuer URL (used verbatim — keep any trailing slash) |
OIDC_JWKS_URL |
JWKS endpoint for the issuer. Required in mode C. |
OIDC_AUDIENCE |
The audience your IdP stamps for the /mcp resource |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET |
Your pre-registered client (secret only if confidential) |
PRECIS_IDENTITY_CLAIM |
Which token claim carries identity (default precis_user_id) |
PRECIS_IDENTITY_COLUMN |
id (the claim value is the user id) or external_id (look it up) |
EXCEL_ADDIN_CLIENT_ID |
Optional public PKCE client_id for the hosted Excel add-in |
EXCEL_ADDIN_SCOPE / EXCEL_ADDIN_RESOURCE_INDICATOR |
Optional token-request shape for IdPs such as Entra/Okta |
Two things to know:
- Pre-registered client, not DCR. Most enterprise IdPs disallow dynamic
client registration, so you register the client yourself and set
OIDC_CLIENT_ID. Modern MCP clients (Claude Code, ChatGPT, a first-party frontend) then sign in via discovery. claude.ai/ChatGPT's public connectors need DCR — if your IdP doesn't offer it, use mode B (Keycloak brokering). - Stable identity claim. Map
PRECIS_IDENTITY_CLAIMto a stable, unique claim (e.g. an immutable subject /oid), not a mutable email. If it differs from your Précis Finance MCP user ids, setPRECIS_IDENTITY_COLUMN=external_idand store the IdP value withcreate-user --external-id(below). - Excel add-in uses a separate public client. If you enable the hosted Excel
add-in in mode C, register a public PKCE client in your IdP with callback
https://<your-host>/excel/auth-callback.html, then setEXCEL_ADDIN_CLIENT_IDon the/mcphost. Entra/Okta usually also needEXCEL_ADDIN_SCOPEandEXCEL_ADDIN_RESOURCE_INDICATOR=false; see External IdP recipes.
Create the first admin and provision users¶
Being signed in by the IdP grants nothing — each user must also exist in
Précis Finance MCP with a profile, and the UI can't bootstrap itself. The simplest path
is to let the deploy create it — pass --admin-id on your deploy-mcp.sh run
(or set PRECIS_BOOTSTRAP_ADMIN_ID in deploy/.env):
bash scripts/deploy-mcp.sh --admin-id alice@example.com
The deploy's first-admin step creates that admin — idempotently, so it's safe on
every run — and prints a one-time temporary password (change it on first login)
in the deploy output. If you already deployed with --admin-id, it's done; the
manual steps below are only for adding admins later, or if you didn't set it.
To do it by hand, run the admin CLI in the precis-mcp container. One
wrinkle in bundled-Keycloak (mode B): create-admin, create-user, and
reset-password provision a Keycloak account, so they need the Keycloak
bootstrap-admin password — which the long-running server deliberately does not
carry. Inject it for that one call (it lives in deploy/.env as
KEYCLOAK_ADMIN_PASSWORD) by using run rather than exec:
docker compose -f deploy/docker-compose.yml --env-file deploy/.env run --rm \
-e KC_BOOTSTRAP_ADMIN_PASSWORD="$(grep '^KEYCLOAK_ADMIN_PASSWORD=' deploy/.env | cut -d= -f2-)" \
precis-mcp python -m precis_mcp.admin_cli create-admin --id alice@example.com
# mode C (external IdP — no Keycloak account, the IdP owns the credential):
docker compose -f deploy/docker-compose.yml exec precis-mcp \
python -m precis_mcp.admin_cli create-admin --id alice --no-keycloak --external-id <idp-subject>
Then provision and grant access. create-user provisions Keycloak too, so it
takes the same injected credential as above; profile and assignment commands
touch only the platform DB, so a plain exec into the running server works:
docker compose -f deploy/docker-compose.yml exec precis-mcp \
python -m precis_mcp.admin_cli profile create --file analyst.yml
docker compose -f deploy/docker-compose.yml exec precis-mcp \
python -m precis_mcp.admin_cli assign --user bob --profile analyst
A user with no profile authenticates but can read nothing — assign a profile to grant access. What a profile contains — scenario patterns, roles, domain and dimension scopes, with worked examples — is documented in User profiles & permissions.
Verify¶
Check the issuer/JWKS/audience config before go-live:
python -m precis_mcp.admin_cli check-auth # static + reachability
python -m precis_mcp.admin_cli check-auth --no-fetch # static only
You can also have the server fail fast at startup on a misconfig by setting
PRECIS_AUTH_PREFLIGHT=1. Then confirm end-to-end: a client completes sign-in,
a query succeeds, and a token with the wrong /mcp audience is rejected.
Connect a client¶
A modern MCP client discovers the sign-in endpoint from your server and runs the
OAuth flow itself — you give it the server URL (and, for mode C, the
pre-registered client_id if the client needs it configured):
{
"mcpServers": {
"precis": { "url": "https://<your-host>/mcp" }
}
}