Docs / Start

Authentication

Bearer tokens, per-tenant isolation enforced in the database, and what the server does not do.

Bearer tokens#

Every request carries a bearer token:

Authorization: Bearer qmcp_...

Keys are stored as SHA-256 digests. The server never holds a key in a form it could show you, so a lost key is replaced, not recovered.

An unauthenticated call to /mcp returns 401. There is no anonymous read path to tenant data.

Isolation is enforced by the database, not by queries#

Every tenant-scoped table carries an owner_id and is protected by Postgres row-level security, ENABLED and FORCED. The application sets app.principal_id on the connection; the policy does the rest.

This is a deliberate choice about failure modes. If isolation lived in query filters, a tool that forgot its WHERE owner_id = ... would return another tenant's rows. Under row-level security the same bug returns nothing. One of those is a data breach and the other is a visible malfunction.

Some consequences worth knowing:

  • holdout_one_shot is unique on (owner_id, strategy_id). Scoped to strategy_id alone, one tenant could have spent another tenant's only holdout attempt.
  • Family names, spec hashes and client_request_ids are per-tenant. Your family btc-ma and someone else's are unrelated.
  • Your trial count N is yours. It is never aggregated across tenants, and no document — including this one — can tell you what it is. Call experiment_ledger for your own number.

Market data is deliberately shared#

Bars are the expensive resource, and OHLCV for BTC-USD is the same fact for everyone. The catalog is shared across tenants on purpose.

What is not shared is what you did with it. A split freezes the gap policy in force when it was registered, so shared data cannot retroactively change what your registered backtest saw.

Scopes and plan limits#

A key carries its principal's plan. Quotas — trials per month, universe size, holdout evaluations — are enforced server-side on every call, not in the client. See Limits.

Check yours with:

account_status()

What the server does not implement#

The MCP authorization specification (revision 2026-07-28) makes OAuth optional: implementations using an HTTP transport SHOULD conform, not MUST. This server serves a static bearer token and has deliberately opted out of MCP authorization. That is permitted, and it is a considered choice rather than an unfinished one.

Two things follow if that ever changes:

  • RFC 9728 Protected Resource Metadata becomes a MUST on the server, and clients MUST use it for authorization-server discovery.
  • The target would be Client ID Metadata Documents, not Dynamic Client Registration. RFC 7591 DCR is deprecated in the current revision and retained only for backwards compatibility.

Adopting OAuth later would not invalidate any key already issued.

Rotating a key#

account_status()          # shows key metadata, never the key

Contact support to rotate. A rotated key takes effect immediately — there is no positive cache, so a revoked key stops working on the next request rather than at the end of a TTL.

Read this page as Markdown: /docs/authentication.md