Satchel MCP docs

Architecture

The monorepo layout, the DTO contract, and the invariants that hold sandbox and live together.

Monorepo layout

Satchel MCP is four independent npm projects — there is no root package.json; each subdirectory installs and runs on its own:

app/    Next.js 15 (App Router, React 19, Tailwind v4).
        Landing page + the sandbox API (app/api/sandbox/*) + waitlist +
        the remote MCP streamable-HTTP route (app/[transport]/route.ts) +
        an OAuth 2.1 authorization server + 2FA login + a /health endpoint.
mcp/    TypeScript stdio MCP server, three source files by design:
        src/client.ts  — HTTP + auth + typed DTOs
        src/tools.ts   — the 6 shared read tools + formatting (also consumed
                         by the remote route in app/)
        src/index.ts   — stdio server + satchel_move_money + widget wiring
        plus bin/connect.mjs (the connect wizard) and src/connect.mjs (the
        shared per-client config generator both the wizard and the landing
        page import from).
spec/   The real API swagger specs + CONTRACT.md, the shared build contract.
        Swagger files are never edited by hand.
video/  Remotion video projects.

The DTO shapes are load-bearing

The sandbox (app/api/sandbox/_data.ts), the MCP client's TypeScript types (mcp/src/client.ts), and the real swagger all agree field-for-field — deliberately, because flipping from sandbox to live is a URL-and-credentials change with no code change (see Overview). DTO field names are never "improved" or renamed for cosmetic reasons; a rename would break that guarantee.

When the build contract document and the real swagger disagree, the swagger wins. A few corrections were already applied after verifying against the real UAT API, for example: payment_state is EXECUTED | REJECTED | PROCESSING (not COMPLETED); payment_type uses the real API's full enum, not an invented shorthand; and MoneyDTO.amount is unsigned with direction carried by payment_role, not by a DEBIT/CREDIT enum (see below).

Money is unsigned; direction comes from payment_role

MoneyDTO.amount is always positive. Direction is a separate signal: payment_role: "SENDER" means money out, "RECIPIENT" means money in. This is never inferred from the sign of the amount — the sign is derived from the role, for display only, never read from the number. This single rule drives every balance, flow, and summary computation across both the sandbox and the MCP tools; see Tools overview for how it surfaces in structuredContent.

Auth flow

In live mode, the plain Client Office password is SHA-512 encoded client-side before being sent. Authentication is POST /token with X-Username / X-Password headers; the API returns the usable API token in the X-Auth-Token response header (a similarly-named field in the response body, session, is not accepted by read endpoints against the live API — the client keeps that as a sandbox-only fallback). Subsequent calls resend the token as X-Auth-Token. On a 401, the client re-authenticates once and retries automatically.

Session header

The header name is X-Auth-Token by default (also SATCHEL_SECURITY_HEADER's default), matching what the live UAT API expects. It remains configurable, for other Client Office gateways that might expect a differently-named header. The sandbox accepts this header name plus a couple of legacy aliases, case-insensitively.

Sandbox state is in-memory

The sandbox keeps all account, card, and transaction state in a process-wide in-memory store; it reseeds on every server restart. There is no database. Waitlist-issued sandbox API keys are the one exception — they persist to a local JSON file so a key survives a restart.

MCP Apps widgets are additive, never load-bearing

Widget HTML (see Widgets) is advertised via _meta on top of tools that always also return text and structuredContent. A text-only client gets full functionality with no widget rendering at all — no tool's core behaviour depends on whether a client happens to render its optional widget.

Read tools: one implementation, two hosts

The 6 read tools' handlers live in a single shared TypeScript module, consumed by both the local stdio server and the hosted remote server (which registers the exact same handlers against a per-session client resolved from the caller's bearer token). There is no second, divergent implementation of "what does satchel_get_balance do" sitting behind the remote endpoint — one set of pure functions, two transports.

On this page