Authentication & security
OAuth 2.1, 2FA login, the encrypted store, the closed pilot model, writes-off-by-default, the kill switch, and the audit stream.
Satchel MCP's hosted remote server is deliberately more conservative than "connect and go" — real account access is gated by several independent controls, described below. None of these apply to the local sandbox, which needs no real credentials at all.
OAuth 2.1 authorization server
The hosted remote server is a full OAuth 2.1 authorization server: authorize, token, dynamic client registration, and revoke endpoints, with:
- PKCE (Proof Key for Code Exchange) on every authorization code flow — the standard mitigation against authorization-code interception, and mandatory under OAuth 2.1;
- protected-resource and authorization-server metadata, published per RFC 9728 and RFC 8414, so compliant MCP clients can discover the flow automatically instead of needing hardcoded configuration;
- dynamic client registration, so a new MCP client can register itself before the first authorization request, rather than requiring a manually-provisioned client id.
The remote MCP endpoint itself is an OAuth resource server: every request must carry a valid bearer token, checked per call.
Client Office authentication and conditional 2FA
Authorizing a connection uses Satchel's own Client Office login. When Client Office presents a two-factor challenge, the user completes the same verification step a customer would go through logging into the Satchel app directly, not a Satchel-MCP-specific shortcut.
The credential is exchanged, never retained
Consenting to a connection exchanges a submitted Client Office credential for a short-lived session, once. The plain credential is not persisted anywhere — not in the OAuth store, not in logs, not in chat history on the client side. When a live session has expired, the user must reauthorize; the product does not attempt to silently extend access using a stored password.
Encrypted OAuth store
The persistent OAuth store (registered clients, grants, token hashes) is encrypted with AES-256-GCM at rest, with restrictive file permissions. A 32-byte key is required to run in live mode — there is deliberately no way to start a live deployment with encryption silently disabled.
The closed pilot model
Live access is not self-serve. Two independent gates apply together:
- who may authorize — an explicit allowlist of permitted subjects
(
SATCHEL_LIVE_ALLOWLIST) gates the closed cohort; - where results may land and under which legal classification —
SATCHEL_LIVE_CLIENT_POLICIESbinds an exact origin (or an explicitly approvedloopbacklocal policy) to a verified entity, client mode, external notices and evidence references. Hosted external clients require written recipient/AISP/transfer dispositions. A Satchel-selected or managed provider additionally requires contract ownership and DORA-register coverage. The consent screen uses this verified identity, never the name supplied by dynamic client registration.
Both must be satisfied; neither alone is sufficient. This is what keeps live access a closed, invite-only pilot rather than an open registration flow.
A customer-selected local/customer-hosted AI is not automatically Satchel's ICT vendor. That boundary does not waive the DPIA, minimisation, SCA, audit, notice or corporate- authority controls on Satchel's own interface. Synthetic UAT without confirmed provider evidence is restricted to isolated local/in-process mocks; external AI, FintechLab, Telegram and external monitoring must remain unused.
Writes are off by default — everywhere
satchel_move_money refuses to run in every mode, sandbox and live alike, unless
SATCHEL_ALLOW_WRITES=true is explicitly set in the server's own environment. On the
hosted remote server this question doesn't even arise: the tool is never registered there
at all, so SATCHEL_ALLOW_WRITES has no effect on that surface. See
satchel_move_money for the full detail.
The read-only boundary on the hosted pilot
The hosted remote server exposes exactly the 6 read tools and nothing else — there is no configuration flag that adds write access back to that surface. Read access itself is scoped per authenticated subject: an authorization grant only lets its holder read their own accounts.
Kill switch
Live access can be disabled by an authorized operator without a deploy or restart. The runtime refreshes kill state on guarded requests with a cache window of up to one second, and the system fails closed: while the kill switch is engaged, every request to the live remote MCP endpoint, and consent/token issuance, is refused. Revocation of an existing connection stays reachable even while the switch is engaged, so a user can disconnect during an incident regardless.
Rate limiting
The hosted remote server enforces two request budgets per minute, refused with HTTP 429
and a Retry-After header once spent: one per subject (default 60/min — an interactive
assistant makes a handful of calls per user turn, so sustained one-request-per-second is a
loop, not a person) and one per OAuth client across all its subjects (default 180/min). The
check runs before any upstream call to the Client Office API, so a runaway or compromised
assistant burns local 429s rather than relaying its loop to a real account. The refusal is
deliberately not a 401: a 401 would push a well-behaved MCP client back through the consent
flow. Each refusal is submitted to the audit stream; it remains refused if the audit sink is
unavailable. Operators are alerted once per window rather than once per refused request.
Retention and erasure
OAuth records live only as long as the tokens they belong to can still be accepted: expired and revoked records are swept automatically on every token issuance and readiness check. On request, all authorization records for one subject — including the bound Client Office session — can be erased in one operator action (the GDPR art. 17 path). The pseudonymized audit stream is not automatically rewritten. Its subject reference remains personal data and follows the approved retention, rights and legal-exception assessment. What a tool already returned to your assistant is retained by your AI provider under their policy — deleting data on the Satchel side does not remove it from your assistant's conversation history.
Audit stream
Successful live connections and allowed read-tool outcomes are returned only after their persisted audit events succeed. Resource-access denials are submitted to the same append-only sink on a best-effort basis and remain denied if the sink is unavailable. OAuth form-validation failures outside the resource server are not claimed as complete audit evidence.
What's recorded, per event: a schema version, a unique event id, a UTC timestamp, the
action (for example, a connection being authorized, a connection being denied, a read tool
call completing, a token being revoked, or the kill switch engaging), and its outcome
(allowed / denied / revoked / blocked). A denial additionally carries a reason
drawn from a closed, fixed vocabulary (for example: no bearer token presented; the token was
invalid, expired, or revoked; the subject isn't on the live allowlist; or the underlying
tool call itself failed upstream) — closed specifically so that "why was this denied"
questions can be answered by querying a fixed set of values, never by parsing free text.
What's pseudonymized: the record ties activity to a subject via an HMAC of their identity, stable per subject for a given deployment key, so repeat activity can be correlated without storing who it was.
What's never present, by construction: email addresses, account numbers, IBANs, transaction narratives, session tokens, credentials, or access/refresh tokens. This is enforced by an automated test, not just a convention.
Retention is enforced by log rotation outside the application, over a documented retention window, on an append-only file — a deliberately simple, migration-free design: any future downstream consumer of the audit stream reads the stream and keys off its schema version, rather than sharing a live database schema with this service.