1.24.0 · signed, notarized, and sold direct
Bastion is a menu-bar app that supervises your MCP servers instead of letting each editor spawn its own. One process per profile, credentials in the Keychain, and every tool call recorded.
macOS 26 or later · loopback only · 30-minute trial, then $14.99
The shopify entry in a client's mcp.json, before and after wiring: a command, its arguments and three environment variables — a store domain, a client id and a Shopify client secret in plaintext — all removed, and one url pointing at the Bastion gateway added in their place. The same file in all 4 · the credential moves out of it
prod/shopify · one process, all 4 running
Keychain · every call recorded
The window
Two profiles, one installed server.
The panel above is drawn, and the line in it addresses a profile. This is what a profile is, photographed: one installed server, two named credential sets, serving two accounts without either knowing about the other.
Every client spawns its own 4 processes
Claude Code
own process · own secret
Cursor
own process · own secret
VS Code
own process · own secret
Terminal
own process · own secret
One supervisor holds them 1 process
Claude Code Cursor VS Code Terminal
Bastion.app
one child
Keychain-held
What leaks if a .mcp.json leaks is a
revocable loopback token, not a Shopify secret.
The runtime
HTTP to the clients. stdio to the child.
Each client points at a URL instead of a command. Bastion answers it, and keeps exactly one child per profile alive behind it.
http://127.0.0.1:8720/s/<profile>/<server>
Profiles
A named credential and configuration set — prod/shopify, staging/shopify, acme/keycloak. One global instance would be one identity, and one identity is unusable: every repo here already carries different credentials for the same server.
One handshake
The MCP handshake happens once, at spawn. Every client's initialize is answered from that one result, which the 2026-07-28 stateless-first revision makes correct rather than a hack.
Id remapping
Each client's request ids are rewritten into the supervisor's own numbering and back again — two clients using id 1 at the same time cannot receive each other's answers.
One process, N clients
This inverts the one-connection-one-process design on purpose. Three reasons are given for that choice, and each has to be answered rather than waved at.
What is genuinely given up is isolation between two clients of the same profile — the same identity with the same permissions, which is exactly the case where a second process bought nothing but memory.
- state
- The handshake happens once, at spawn, and is replayed to every client.
- crashes
- Backoff, a circuit breaker, and the blast radius said out loud: when a child dies every client waiting on it is told, with a count.
- write permissions
- Per profile, not per process — which a process boundary could never express.
The context cost
85 tool definitions, on every connect.
Every client wired to a profile is sent every tool definition that profile's server exposes
before it can call one, and holds them for the whole conversation. That is the largest fixed
charge a shared gateway imposes: prod/appstore-connect is 85 tools and about 26.2k tokens, in every editor that opens it, every
time one starts.
Load tools on demand serves 3 tools in their place — a search, a describe and a call — and the same profile connects at about 0.4k. The full searchable index costs 3.2k, and only if something asks for it. Nothing becomes unreachable, and Bastion measures both numbers itself: the profile row carries the figure rather than leaving it to be inferred.
What it costs is on the client's side, and the switch says so: every call reaches the editor
as bastion_call_tool, so one approval rule there
now covers every tool on that server. Bastion's own write gate is unaffected — a write tool
is still absent from the index and still refused by the dispatcher. The editor's gate is the
coarser one, which is why this ships off and why a profile can disagree with the app-wide
answer.
Claude Code and Claude Desktop already load a tool's schema on demand, so Bastion serves them the real list whatever a profile says — fronting a client that defers by itself would buy back the names and take its own tool search with it. The figures above are what every other client pays.
MCP has no method for fetching a schema later — inputSchema is required in a tools/list entry — so an index and
a dispatcher is the only shape lazy discovery can take. Doing it in the gateway is what keeps
it honest: the call is opened back up into the ordinary tools/call it stands for before the audit chain, the Activity window or the write gate sees the frame,
so all three go on naming the real tool. A facade bought as a server cannot do that.
On connect
prod/appstore-connect
Every tool, as any gateway serves it 85 tools · ~26.2k
Tools on demand 3 tools · ~0.4k
67× fewer tokens to hand a client the same server. The index it can ask for costs 3.2k.
Counted from the listing as it arrived on the wire, at four bytes to the token, with the profile's write gate already applied.
What the client is served instead
- bastion_search_tools
- Names and one-line summaries, filtered by a query. An empty query lists everything.
- bastion_describe_tool
- One tool's full input schema, by exact name.
- bastion_call_tool
- Runs one, by name and arguments.
One switch in Settings moves every profile at once; a profile that disagrees — one wired to a client that already defers tool schemas by itself, and so gains nothing here — overrides it in its own sheet.
The record
A record of requests, not a sandbox.
Bastion sees the JSON-RPC frames crossing the gateway, down to what each tool was called with. Credentials are never recorded, and no argument and no result is written to disk unless you ask for an audit log — which is chained, so an edited or missing record shows. It does not see what a server then does over the network or on the filesystem — and the page says so rather than implying otherwise.
It sees
- profile
- method
- tool name
- arguments
- results
- outcome
It does not
- credentials
- egress
- file reads
A server that reads a file it was never asked about does so out of Bastion's sight.
The app
A window, not a background process.
Holding the servers in one signed process rather than spawning them inside whichever
editor asked first is what makes any of this visible: there is somewhere to look, and it
says what happened. These are captures of that window, taken from a real build by
make screenshots and checked
against committed goldens, so they cannot quietly fall behind the app.
What is running, and who is attached
A row per profile that has started — four for five, because staging/shopify is still short a credential and has never had a process at all. Each row names its clients twice over: the identity its bearer token was issued to, and whatever the client calls itself, because the interesting case is when those disagree.
What the tool lists cost you
Each server pane quotes its own cost, and each one looks survivable on its own. This adds them up: a tool list is sent to your editor before it can call anything and held for the whole conversation, so it is a charge you pay on every connect whether or not you use the server. The bar that fades out is a listing read one page at a time, where the figure is a floor and the sentence under it says «at least» rather than «about». Everything above the range picker is what a connect costs; everything below it is what actually happened.
The log, with the arguments
Not just which tool — what it was called with, and what came back for a profile that opts in. Credentials are redacted before the row is written rather than filtered out of it afterwards, which is why one entry here reads «redacted» where a client secret would have been. No argument and no result is written to disk unless you ask for an audit log.
The file it is about to write
One entry per profile, each carrying a bearer token issued to that client and never a credential — that split is what makes writing into somebody else's config defensible at all. The servers you configured by hand are listed underneath, untouched, with the note that nothing they do reaches the log.
Try a tool before wiring it up
One profile's tools loaded into the on-device model, with the arguments it invents and the results it gets shown inline. The header counts what is loaded against the context it costs, because a server with forty tools does not fit and the pane would rather say so than quietly drop some.
A trial that is the whole app
Thirty minutes, every server relaying and every write gate obeying its own switch. It answers whether this works on your Mac against your servers, which is a different question from whether it was worth the money — and that one is what the refund is for.
The five rules
A loopback daemon holding every credential you own is a known shape of hole.
CVE-2025-49596 was a listener plus no CSRF protection: a page you visited could reach it and execute code. The rust-sdk and FastMCP rebinding advisories had the same root cause — no protection by default, because it is only localhost. All five rules landed in the first commit that opened a socket, not a hardening pass afterwards.
Servers
Thirty-five to start with. Add your own.
Bastion ships with nothing installed. The thirty-five below are a catalog you install from; anything else on npm you add by package name. The twenty-three packages are fetched on demand into Bastion's own directory and run with the Node runtime in the app. The eleven endpoints are servers their own vendors operate: nothing is installed and no process is started, and what Bastion adds is the credential in the Keychain, one identity per profile, and the audit line. Xcode's own server is the exception to both: a command that ships inside Xcode, which Bastion runs and supervises without installing anything, and which Xcode will not answer until you approve Bastion.
What stays closed is how a request picks one. A client names a profile and a server id — never a path, never a command line. The child inherits your credentials and runs unsandboxed, so "run whatever the config names" is the same shape of hole as CVE-2025-49596, and it is the half worth keeping. The reference has the argument in full.
30 behind a write gate
Off until a profile turns it on. lab/unifi-network with writes on beside home/unifi-network with
them off is expressible; a process boundary never made it so.
5 read-only
No mutating tool registered at all, so a host cannot call what it was never told about.
- ✓ appstore-connect write gate
- ✓ cloudkit write gate
- ✓ reddit write gate
- ✓ x write gate
- ✓ unifi-protect write gate
- ✓ unifi-network write gate
- stripe write gate
- ✓ shopify read-only
- ovh write gate
- ✓ keycloak write gate
- ✓ npm write gate
- github write gate
- notion write gate
- linear write gate
- sentry write gate
- atlassian write gate
- figma write gate
- vercel write gate
- cloudflare write gate
- cloudflare-docs write gate
- cloudflare-observability write gate
- ✓ ios-device write gate
- ✓ mongodb write gate
- ✓ dbhub write gate
- ✓ kubernetes write gate
- context7 read-only
- firecrawl read-only
- ✓ exa read-only
- tavily read-only
- playwright write gate
- ✓ supabase write gate
- netlify write gate
- ✓ apify write gate
- ✓ ios-simulator write gate
- xcode write gate
✓ marks the seventeen packages npm can tie to the repository they link to: a signed attestation says GitHub Actions built that exact version from that exact source. It is a smaller claim than a review — nobody here read the code — but it is the one claim a typosquat cannot copy, and it is checked against the registry rather than typed. The rest are not suspect; they publish the older way.
None of the thirty-five are modern. The twelve children written here run an SDK whose newest protocol is 2025-11-25, which is what they negotiate; the catalog said 2025-06-18 until a live handshake was run against one, and that was Bastion's own pin masquerading as a fact about the servers. The eleven published by somebody else carry the same revision as a seed rather than a measurement — their SDK is a dependency of a package this repo does not own — and so do the remote entries; the first real handshake through a profile is what measures each one. A server you add yourself is fronted the same way. The catalog is one file, and the reference table has every variable each entry reads.
Known gaps
What it does not do yet.
Everything above this line is built and runs today — the README lists it row by row. This is the other column, and it is here rather than somewhere you find out after paying.
A different kind of limit — what the checks themselves do not prove — is on the checked page.
- Server-initiated
- Sampling, elicitation and roots are refused with an explanatory error, not routed — a shared instance has no single client to ask.
- Write gate
- A remote server's write gate is a filter, not a boundary: Bastion refuses to forward the named tools, but anyone holding the credential can call the same API directly. Its own scopes are the real limit.
- Rate limits
- A remote server's rate limit is shared. Behind one profile every client spends one budget upstream, so a client in a loop can exhaust it for the rest.
- list_changed
- A call that asks for progress is answered with an SSE stream, so notifications/progress reaches the client that asked. list_changed and subscriptions/listen still cannot: they name no request, and a shared instance has no one client to send them to.
- Mcp-Param-*
- Headers are forwarded but not validated; no server in the manifest annotates a parameter yet.
Price
One price, one major version, every Mac you own.
There is no subscription and no per-seat tier. Bastion is a tool you install once and forget about; charging rent for a background process that holds your own credentials is not a business this wants to be in.
$14.99 once, not a subscription
€14.99 in the EU, VAT included.
- Every 1.x release, on every Mac you own The key is issued to you, not to a machine, and nothing counts your installs. It is checked against the major version, so 2.0 will be a new purchase.
- Verified offline, on your Mac A key is an Ed25519 signature the app checks locally. No activation call, no licence server, and nothing that can refuse to start because a host is down.
- Thirty days, full refund, no reason needed The trial answers whether this works against your servers. The refund answers whether it was worth the money, which is a different question and is asked second.
- The source stays public What is sold is the signed, notarized build. The code that holds your credentials is readable and compilable by the people trusting it.
- Nothing else to install, and no account No container runtime, no Docker Desktop, no sign-up. The bundle carries its own Node, binds loopback, and ships with no entitlements file at all — spawning children and binding loopback need none.
A 30-minute trial runs the whole app, not a crippled version of it. No account, nothing to cancel, and the window lives in memory — quitting and reopening starts another one. See the terms and the reasoning behind them .
Also from Magenta Creations
Cupertino
Your Apple apps, as MCP servers.
Bastion supervises the servers you bring it. Cupertino is a set of them — Mail, Notes, Calendar, Messages and the rest of the Apple apps already on your Mac — behind a single Full Disk Access grant, held by a signed menu-bar app you can watch.
Armada
Every coding agent, on one screen.
A menu bar app for the agents on the other end of those servers: every Claude Code and Codex session on your Mac, what each one is doing, how full its context window is, and how much of each plan is left, read locally from what the agents already write to disk.
One process. One grant of trust. Read the source.
A program that holds every credential you own should be readable and compilable by the people trusting it. The source is public and stays public; what is sold is the signed build.
$14.99 — every 1.x release, on every Mac you own. Thirty days, full refund, no reason needed. Buy a licence · terms