# Authentication, authorization and approval For anyone who deploys these servers or reviews their security: who may call a server, whose grant it uses at the service behind it, and who approves a call that spends money or files a document. Facts about other products date from 2026-10-07 unless marked; UNVERIFIED means unchecked. ## What these servers do ```text MCP client MCP server (this repo) upstream API (Claude Code, --A--> serve() -> guard -> tool --B--> (Google, Notion, Codex, ...) | ANAF, a bank) C: a person approves a consequential call ``` **A, client to server**: who may call, and as whom; only over HTTP, since over stdio the client spawned the process. **B, server to upstream**: whose grant the server uses at Google or a bank, and how it is obtained, kept and renewed. **C, approval**: whether a person says yes to this exact call before a tool that moves money or makes a legal document runs. The layers are independent. The MCP specification covers A in detail, says little about B, and leaves C to clients and servers. ### Layer A today Over stdio, the default, there is no authentication, as the specification asks; every caller is named `stdio`. Over HTTP (`MCP_HTTP_PORT` set, [`serve.ts`](../packages/mcp-kit/src/serve.ts)), a server serves one machine: - **Loopback, one route**: `127.0.0.1` only, `POST /mcp`, stateless (no session id, a fresh server per request), for both protocol generations ([architecture.md](architecture.md#protocol-version)). - **A static bearer token per client**: `token-` files in `MCP_HTTP_TOKENS_DIR`, or one `MCP_HTTP_TOKEN_FILE` for the client `MCP_HTTP_TOKEN_CLIENT` names. Every process and browser on the machine reaches loopback, so the server will not start without a token, or with one under 16 characters. Every token is compared in constant time, so timing does not reveal a near miss; a missing or wrong one gets a plain 401. - **Browsers kept out**: a `Host` other than `127.0.0.1:` or `localhost:` gets 421 (DNS rebinding); the CORS preflight a page needs to send `Authorization` gets a 401 without CORS headers. - **The token's client name is the identity** for the audit line, the rate limit (`MCP_HTTP_RATE`), the per-client ceiling (`MCP_CLIENT_MAX_ACCESS`), the binding of a question asked mid-call, and the tenant in `servers/devices` (never an argument). - **No OAuth**, no Protected Resource Metadata, no `WWW-Authenticate` challenge: nothing off the machine reaches the listener, by design. - **Or no sign-in at all, on purpose**: `MCP_HTTP_PUBLIC_HOSTS` opens a server whose tools all read public data to anyone a reverse proxy forwards, as the client `public`, with a rate limit per caller's address. It refuses to start if anything but a read could run ([security.md](security.md#a-public-server)). ### Layer B today Each server reads its credentials through the kit ([architecture.md](architecture.md#the-networking-stack)): `readSecret` takes a `*_FILE` variable first and the plain one for a local run, and fails on a named file that is missing or empty; `makeOAuthClient` runs the OAuth grants, and `makeTokenSource` keeps access tokens in memory and in a 0600 file. Under systemd, grants live in a `StateDirectory=` only the service's user reads ([configuration.md](configuration.md#conventions)). | Server | Upstream credential | How it is obtained | Renewal | |---|---|---|---| | `google` | an OAuth refresh token per account and product (Gmail, Calendar, Drive, Docs/Sheets/Slides), under the operator's own OAuth client | `begin_consent` returns a Google URL (PKCE, `access_type=offline`, `prompt=consent`, `login_hint`); the account holder approves, and the redirect to a loopback address fails to load; `complete_consent` takes that address, checks the account and every scope, and saves the grant | the refresh token; consent again when Google revokes it | | `onrc` | the signed-in person's Keycloak session (the portal's public client `frontoffice-app`) | `begin_login` / `complete_login`: an authorization code with PKCE, through the browser where the person is signed in | as long as the SSO session lasts; then `needs_login` | | `anaf-efactura` | ANAF OAuth tokens for an application registered in `app.json` | `begin_authorization` / `complete_authorization`; the person signs in at ANAF with a qualified certificate | long-lived tokens ([provider notes](#provider-notes)); a dead refresh token needs the certificate again | | `anaf-spv` | the taxpayer's SPV user name and password (a file, or a vault MCP server) and the code SPV e-mails (through a mail MCP server) | configured once; the server signs in by itself | a session lasts until SPV ends it; then it signs in again | | `enable-banking` | an application id and an RS256 private key; a PSD2 session per bank consent | every request carries a JWT signed with the key; a consent is `start_auth` (`money`), then `complete_auth` with the redirect | a consent lasts up to 180 days, then the account holder repeats it | | `notion` | one token per workspace (a personal access token or an internal integration token) | made by hand in Notion, saved as `notion-