Bastion is a macOS menu-bar app that supervises your MCP servers instead of letting each editor spawn its own. A Model Context Protocol client normally launches a stdio server as a child process, handing it a command line and an environment full of credentials; Bastion inverts that. Each client is pointed at a loopback URL instead, and Bastion answers it, keeping exactly one supervised child per profile alive behind the gateway. It is aimed at developers who run several MCP clients — Claude Code, Claude Desktop, Cursor, VS Code, Codex, Terminal — against several accounts and several servers on a single Mac, and who want one place where the running processes, the credentials and the calls are visible.
The shape of the problem is ordinary. Every client spawns its own copy of every stdio server, each with its own environment, so four servers kept a .mcp.json holding real secrets in plaintext — readable by anything running as the same user, one careless git add away from being published. Nothing was auditable and nothing was shared. The site cites four public trackers, none of them Bastion's own: six processes for three configured servers in Claude Desktop on every restart; about 3.5 GB of MCP processes at idle with 82 children from one OpenCode launch; 5.7 GiB across 60 subprocesses, with the request to share one closed as not planned; and a request to read credentials from the Keychain instead of a config file, also closed not planned. Bastion exists because that duplication is not something upstream was on its way to fixing.
Profiles are the central unit. A profile is a named credential and configuration set — prod/shopify, staging/shopify, acme/keycloak, lab/unifi-network, home/unifi-network — bound to one installed server. One global instance would be one identity, and one identity is unusable, because every repo already carries different credentials for the same server. Two profiles of the same installed server serve two accounts without either knowing about the other. Write permissions are set per profile rather than per process, which a process boundary could never express: lab/unifi-network with writes on can sit beside home/unifi-network with them off. Of the forty-three servers in the catalog, thirty-seven sit behind a write gate that is off until a profile turns it on, and six are read-only, registering no mutating tool at all so a host cannot call what it was never told about. Credentials themselves live in the macOS Keychain: the server pane can set a secret and cannot show you one.
Underneath, the transport is split: HTTP to the clients, stdio to the child. Each client points at http://127.0.0.1:8720/s/
/ instead of a command, and Bastion keeps exactly one child per profile alive behind it. The MCP handshake happens once, at spawn, and every client's initialize is answered from that one result — which the 2026-07-28 stateless-first revision makes correct rather than a hack. Request ids are remapped: each client's ids are rewritten into the supervisor's own numbering and back again, so two clients using id 1 at the same time cannot receive each other's answers. Reliability is handled explicitly, with 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. What is genuinely given up is isolation between two clients of the same profile — the same identity with the same permissions, exactly the case where a second process bought nothing but memory.
The largest fixed cost a shared gateway imposes is the tool list. 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; 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 three tools in their place — a search, a describe and a call — and the same profile connects at about 0.4k tokens, 67 times fewer; the full searchable index costs 3.2k and only if something asks for it. Nothing becomes unreachable, and Bastion measures both numbers itself rather than leaving the figure to be inferred. The switch is honest about what it costs on the client's side: every call reaches the editor as bastion_call_tool, or bastion_call_write_tool for a write, so an editor's per-tool approval rules no longer apply tool by tool, while Bastion's own write gate is unaffected. Because MCP has no method for fetching a schema later, an index plus a dispatcher is the only shape lazy discovery can take, and doing it in the gateway keeps it honest: the call is opened back up into the ordinary tools/call before the audit chain, the Activity window or the write gate sees the frame.
Bastion keeps a record of requests, not a sandbox. It sees the JSON-RPC frames crossing the gateway, down to what each tool was called with — profile, method, tool name, arguments, results and outcome — and it does not see credentials, egress or file reads; a server that reads a file it was never asked about does so out of Bastion's sight, and the page says so rather than implying otherwise. 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. The window is where this becomes legible: a Running pane with a row per started profile, each naming its clients twice over — the identity its bearer token was issued to and whatever the client calls itself; a Stats pane ranking what each server's tool list costs on every connect, above a bar chart of calls a day; a Log pane of timestamped calls with their JSON arguments and results, one entry reading "redacted" where a client secret would have been; a client pane showing the file it is about to write; and a Chat pane that loads one profile's tools into the on-device model so you can try a tool before wiring it up.
A loopback daemon holding every credential you own is a known shape of hole, so five rules are load-bearing, and all five landed in the first commit that opened a socket rather than in a hardening pass afterwards. Bastion binds 127.0.0.1 explicitly, never 0.0.0.0, and that is not configurable. It validates Origin — the page-you-visited rule that CVE-2025-49596 lacked — and then Host, the anti-rebinding rule, before it reads a token at all. It requires a per-client bearer token minted at wiring time and kept in the Keychain, and it never puts a secret in a config file: the token goes in the client config, the credential stays in the Keychain. The ordering matters, because a rebinding attempt must be refused whether or not it also guessed a token. A make audit script checks all of it — loopback-only via lsof, foreign Origin refused, rebound Host refused, refusal order enforced, profiles.json holding no secret value — and the app holds exactly four entitlements: its identity, its team, its keychain and iCloud sync, with the audit asserting the set is exact in the file and in the signed app.
Bastion ships with nothing installed. A catalog of forty-three servers is there to install from: thirty npm packages fetched on demand into Bastion's own directory and run with the Node runtime in the app, twelve remote endpoints operated by their own vendors where nothing is installed and no process is started — Bastion adds the credential in the Keychain, one identity per profile and the audit line — and Xcode's own server, a command that ships inside Xcode which Bastion runs and supervises without installing anything. Anything else on npm you add by package name. A client names a profile and a server id, never a path and never a command line, because the child inherits your credentials and runs unsandboxed. Twenty-three of the packages carry a signed attestation tying the published version to the repository it links to, checked against the registry rather than typed.
What comes out of this for a user is a set of properties that are hard to get otherwise. What leaks if a .mcp.json leaks becomes a revocable loopback token rather than a Shopify secret. One click wires Claude Code, Claude Desktop, VS Code, Cursor, Codex and more, and the entry written into each client's config carries a bearer token issued to that client and never a credential — the split that makes writing into somebody else's config defensible. Servers you configured by hand are listed underneath, untouched, with the note that nothing they do reaches the log. The same file in all four clients, with the credential moved out of it, is the whole idea in one image.
Concrete scenarios follow from the same primitives. Four clients — Claude Code, Cursor, VS Code and Terminal — can share one prod/shopify process instead of spawning four children with four secrets. A developer can keep prod/shopify and staging/shopify wired at once, each profile holding its own client id and secret, so an agent working in one account cannot reach the other. Writes can be enabled for lab/unifi-network while home/unifi-network stays read-only. A large tool list such as appstore-connect's 85 tools can be reduced to three with load tools on demand, so connecting costs about 0.4k tokens instead of 26.2k in every editor. And a tool can be tried in the Chat pane against the on-device model before it is wired into anything.
Bastion is a macOS 26 or later app, distributed as a signed, notarized build sold direct, with the source public. There is no subscription and no per-seat tier: $14.99 once, €14.99 in the EU with VAT included, covering every 1.x release on every Mac you own. The key is an Ed25519 signature checked locally, so there is no activation call and no licence server, and nothing counts installs; it is checked against the major version, so 2.0 will be a new purchase. The trial is the whole app for thirty minutes — every server relaying and every write gate obeying its own switch — with no account and nothing to cancel, and there is a thirty-day full refund that needs no reason. Known gaps are stated plainly: server-initiated sampling, elicitation and roots are refused with an explanatory error, a remote server's write gate is a filter rather than a boundary, a remote server's rate limit is shared behind one profile, and list_changed and subscriptions/listen cannot be routed because a shared instance has no single client to send them to.
The takeaway is that Bastion turns a pile of per-editor MCP subprocesses into one supervised process per profile: credentials out of plaintext configs and into the Keychain, write permissions expressed per identity rather than per process, tool calls recorded rather than invisible, and a window that shows what is running instead of leaving it to be guessed.