# ๐Ÿ›ก๏ธ mcp-bastion **A reliability & security proxy for the Model Context Protocol (MCP).** _Self-healing connections, runtime tool-security, and a compliance-mapped audit trail for your MCP servers._ [![npm version](https://img.shields.io/npm/v/mcp-bastion.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/mcp-bastion) [![npm downloads](https://img.shields.io/npm/dm/mcp-bastion.svg?color=cb3837)](https://www.npmjs.com/package/mcp-bastion) [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6f42c1)](https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.Gowthaman90/mcp-bastion) [![Measured coverage]()](https://github.com/Gowthaman90/mcp-defense-bench) [![mcp-bastion MCP server โ€“ quality and maintenance score on Glama](https://glama.ai/mcp/servers/Gowthaman90/mcp-bastion/badges/score.svg)](https://glama.ai/mcp/servers/Gowthaman90/mcp-bastion) [![CI](https://github.com/Gowthaman90/mcp-bastion/actions/workflows/ci.yml/badge.svg)](https://github.com/Gowthaman90/mcp-bastion/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg)](https://www.typescriptlang.org/) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing)
mcp-bastion: an MCP server crashes mid-session and the agent recovers it automatically
--- `mcp-bastion` sits between your MCP client (Claude Code, Cursor, Cline, Windsurf, Zed, Claude Desktop, or any MCP-compliant agent) and your MCP servers. It is **client-agnostic** โ€” it works with any compliant client through configuration alone, with zero client-specific code โ€” and **non-invasive**: your servers run unchanged, and removing Bastion is a one-line config revert. ๐Ÿ“ฆ **Package:** [`mcp-bastion` on npm](https://www.npmjs.com/package/mcp-bastion) ยท ๐Ÿ—‚๏ธ **Official MCP Registry:** `io.github.Gowthaman90/mcp-bastion` ๐Ÿ”’ **Security, measured:** on the open, vendor-neutral [mcp-defense-bench](https://github.com/Gowthaman90/mcp-defense-bench), Bastion covers **63% of the MCP attack surface (15.0/24 vectors; 11 enforced)** at zero false positives โ€” the broadest of the proxies measured. ๐Ÿ“– **Launch story:** [Medium](https://medium.com/p/e74f638e9e15) ยท [dev.to](https://dev.to/gowthaman90/the-mcp-reliability-security-gap-and-an-open-source-proxy-that-fills-it-3ppo) ๐Ÿ‘ค **Created & maintained by [Gowthaman Arumugam](https://github.com/Gowthaman90)** โ€” Independent Researcher. Companion benchmark: [mcp-defense-bench](https://github.com/Gowthaman90/mcp-defense-bench). ## Contents - [Why](#why) - [How it works](#how-it-works) - [Features](#features) - [Quick start](#quick-start) - [Demo](#demo) - [Control tools](#control-tools) - [Configuration](#configuration) - [Transports](#transports) - [Runtime security](#runtime-security) - [Audit & compliance](#audit--compliance) - [Client setup](#client-setup) - [Architecture](#architecture) - [Development](#development) - [Roadmap](#roadmap) - [Contributing](#contributing) - [Security](#security) - [License](#license) ## Why When an MCP server disconnects mid-session, the agent only sees a generic _"No such tool available"_ error โ€” **indistinguishable from a tool that never existed** โ€” and it cannot reconnect; only a human can. Long agent sessions silently lose capabilities and fail in confusing ways. Bastion closes that gap. It health-checks every server, auto-reconnects with backoff, and โ€” crucially โ€” exposes control tools so the **agent itself** can inspect connection health and recover a dropped server without human intervention. > Bastion now spans three layers: **reliability** (v0.1), **runtime security** (v0.2 โ€” tool pinning / > rug-pull & poisoning detection), and **audit & compliance** (v0.3 โ€” pluggable sinks mapped to NIST > AI RMF / OWASP LLM Top 10). See the [roadmap](#roadmap). ## How it works Today your client connects **directly** to each server. With Bastion, your client connects to **Bastion**, which connects to those same servers on your behalf โ€” so it sits in the tool-call path and can add reliability (and, later, security) transparently. ``` Before: Client โ”€โ–ถ server A / server B / server C After: Client โ”€โ–ถ mcp-bastion โ”€โ–ถ server A โ”€โ–ถ server B โ”€โ–ถ server C ``` Bastion is a standard MCP **server** to your client and a standard MCP **client** to each upstream. Because it speaks the protocol faithfully, it works with every compliant client automatically โ€” the only per-client difference is where you put a few lines of config. ## Features - ๐Ÿ”Œ **Client-agnostic** โ€” one binary, config-only integration; no per-client plugins. - โ™ป๏ธ **Self-healing** โ€” health checks + capped exponential-backoff auto-reconnect for stdio servers. - ๐Ÿงญ **Agent-recoverable** โ€” `bastion__status` and `bastion__reconnect` let the agent detect and fix drops itself, instead of hitting an opaque "no such tool" wall. - ๐Ÿงฉ **Transparent aggregation** โ€” merges many servers into one, with per-server tool namespacing to prevent collisions and tool-shadowing. - ๐Ÿ’ฌ **Legible failures** โ€” a dropped server yields an actionable message, not a crash. - ๐Ÿ›ก๏ธ **Runtime security** _(new in v0.2)_ โ€” pins each tool's definition and blocks "rug pulls" (a server changing a tool after approval); heuristically inspects descriptions for poisoning; detects cross-server shadowing. See [Runtime security](#runtime-security). - ๐Ÿ“ **Audit & compliance** _(new in v0.3, opt-in)_ โ€” structured, integrity-hash-chained audit events to pluggable sinks (console / file / webhook), mapped to NIST AI RMF & OWASP LLM Top 10. See [Audit & compliance](#audit--compliance). - ๐Ÿชถ **Non-invasive & reversible** โ€” your servers run unchanged; uninstall is a config revert. - ๐Ÿงฑ **Enterprise-grade codebase** โ€” strict TypeScript, layered architecture, ESLint + Prettier, and unit + end-to-end tests. ## Quick start Bastion is published on npm as [`mcp-bastion`](https://www.npmjs.com/package/mcp-bastion) โ€” the `npx` command below fetches it automatically, so there's nothing to install first. **1. Add Bastion to your client**, pointing it at a config file: ```jsonc // your client's mcpServers config { "mcpServers": { "bastion": { "command": "npx", "args": ["-y", "mcp-bastion", "--config", "bastion.config.json"], }, }, } ``` **2. List your real servers in `bastion.config.json`** (moved verbatim from the client): ```jsonc { "servers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] }, "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], }, }, "reconnect": { "auto": true }, "healthCheck": { "enabled": true }, } ``` **3. Restart your client.** Your tools now appear namespaced (e.g. `github__create_issue`) alongside Bastion's control tools. See [`bastion.config.example.json`](./bastion.config.example.json) for the full set of options. > ๐Ÿ”’ **Security is on by default.** Out of the box, Bastion runs the `balanced` enforcement profile: > it **blocks** high-confidence attacks (rug-pulls, argument/command injection, cross-server > exfiltration) and **warns** on heuristic ones (description/response > poisoning), while redacting leaked secrets from tool results. Set `security.enforcementProfile` to > `observe` (warn-only) or `strict` (block-all), or tune any individual control โ€” see > [Runtime security](#runtime-security). ## Demo See the whole thing in action โ€” a server crashing mid-session and healing itself: ```bash npm run demo ``` It boots Bastion in front of a server that crashes on command, shows the agent getting an actionable "reconnect" message instead of a cryptic error, and then the connection auto-recovering with no human involved. To record it as a GIF: `asciinema rec demo.cast -c "npm run demo" && agg demo.cast assets/demo.gif`. ## Control tools Bastion injects control tools so the agent can manage connections and review security itself, using only standard MCP calls: | Tool | Purpose | | --------------------- | ---------------------------------------------------------------------------------------------------------- | | `bastion__status` | Health of every proxied server: connected / disconnected / reconnecting / failed, tool counts, last error. | | `bastion__reconnect` | Reconnect a named server (argument: `{ "server": "" }`) without human intervention. | | `bastion__security` | Per-tool security report: pin status (approved vs changed), poisoning findings, and shadowing. | | `bastion__compliance` | Audit summary of recent activity mapped to NIST AI RMF / OWASP LLM Top 10 (requires `audit.enabled`). | > **Re-approval is operator-only.** Clearing a rug-pull block is a security authority, so it is **not** an > agent-callable tool โ€” a prompt-injected agent must not be able to re-approve the very tool it was blocked > from. A changed tool stays blocked until an operator clears it out-of-band; `bastion__approve` is not > advertised and a client call to it is refused. ## Configuration | Key | Type | Default | Description | | --------------------------------- | ------------------------------ | -------------------- | -------------------------------------------------------------------------------------- | | `servers` | map | โ€” | Upstream servers to proxy (required, at least one). | | `servers..command` | string | โ€” | Executable to launch (e.g. `npx`, `node`). | | `servers..args` | string[] | `[]` | Arguments to `command`. | | `servers..env` | map | โ€” | Env overrides merged over the process env. | | `servers..cwd` | string | โ€” | Working directory for the spawned process. | | `reconnect.auto` | boolean | `true` | Auto-reconnect after an unexpected disconnect. | | `reconnect.maxRetries` | number | `10` | Max attempts before giving up (`-1` = unlimited). | | `reconnect.initialBackoffMs` | number | `500` | Initial backoff, doubled each attempt. | | `reconnect.maxBackoffMs` | number | `30000` | Backoff ceiling. | | `healthCheck.enabled` | boolean | `true` | Enable periodic liveness probing. | | `healthCheck.intervalMs` | number | `30000` | Interval between probes. | | `healthCheck.timeoutMs` | number | `5000` | Per-probe timeout. | | `namespace.strategy` | `prefix` \| `passthrough` | `prefix` | How upstream tool names are exposed. | | `namespace.separator` | string | `__` | Separator used by the `prefix` strategy. | | `security.pinTools` | boolean | `true` | Pin tool definitions and detect later changes. | | `security.onRugPull` | `block` \| `warn` | `block` | Action when a pinned tool's definition changed. | | `security.inspectDescriptions` | boolean | `true` | Run poisoning heuristics on tool descriptions. | | `security.onPoisoning` | `block` \| `warn` | `warn` | Action on a high-severity poisoning finding. | | `audit.enabled` | boolean | `false` | Record an audit event for every tool call. | | `audit.includeArgs` | `none`\|`redacted`\|`full` | `none` | How tool arguments are recorded. | | `audit.tamperEvident` | boolean | `false` | Hash-chain events so tampering is detectable. | | `audit.sinks` | array | console | Destinations: `console`, `file`, `webhook`, `otlp`. | | `servers..transport` | `stdio` \| `http` | `stdio` | Local subprocess or remote endpoint. | | `servers..url` | string | โ€” | Remote MCP URL (required for `http`). | | `servers..headers` | map | โ€” | Headers for `http` upstreams (e.g. `Authorization`). | | `listen.mode` | `stdio` \| `http` | `stdio` | Serve Bastion over stdio or Streamable HTTP. | | `listen.host` / `listen.port` | string / number | `127.0.0.1` / `3000` | Bind address for `http` mode. | | `listen.validateRoutingHeaders` | boolean | `true` | Reject `Mcp-*` routing headers that disagree with the body (`-32020`, MCP 2026-07-28). | | `security.maxCacheTtlMs` | number (ms) | `3600000` | Ceiling on upstream `ttlMs` cache hints forwarded downstream (MCP 2026-07-28). | | `servers..protocol` | `auto`\|`legacy`\|`2026-07-28` | `auto` | Era negotiation per upstream (probe 2026-07-28, fall back; or pin). | | `listen.legacy` | `stateless` \| `reject` | `stateless` | Serve pre-2026-07-28 clients statelessly, or refuse them (`-32022`). | | `security.inspectInputRequests` | boolean | `true` | Gate `input_required` rounds (credential phishing, model steering). | | `security.onInputRequired` | `block` \| `warn` | `block` | Action on a high-severity MRTR finding (`warn` strips the request). | | `security.requestStateKey` | string (โ‰ฅ16) | random per process | HMAC key sealing `requestState` envelopes; or `MCP_BASTION_REQUEST_STATE_KEY`. | | `security.requestStateTtlSeconds` | number | `300` | Lifetime of a sealed `requestState` envelope. | ## Transports Bastion runs on the MCP TypeScript SDK **2.0** and speaks **both protocol eras** โ€” the stateless 2026-07-28 revision and the pre-2026 `initialize` handshake โ€” on both faces, per upstream and per client. When the protocol is stateless, the gateway is the only component that can still hold security state, which is why v1.0 adds `requestState` custody and an `input_required` consent gate (see `CHANGELOG.md`). Bastion speaks two transports on **both** faces: - **stdio** (default) โ€” the client spawns Bastion, and Bastion spawns local servers. - **Streamable HTTP** โ€” connect to **remote** MCP servers (`servers.` with `transport: "http"`, a `url`, and optional auth `headers`), and/or **serve** Bastion over HTTP to multiple/remote clients (`listen.mode: "http"`, or `--http `). HTTP upstreams configured without an authentication header are flagged (`authenticated: false`) in `bastion__status` and warned at connect time. ## Runtime security _New in v0.2._ Bastion adds a security layer in the tool-call path (an interceptor pipeline), enabled by default: - **Rug-pull detection (tool pinning).** Each tool's definition is pinned on first use. If a server later changes that definition, the tool is blocked (`onRugPull: "block"`) until an operator reviews it and re-approves it out-of-band (operator-only โ€” not an agent-callable tool). This catches a server that looks benign at install time and turns malicious afterward. - **Poisoning inspection.** Tool names and descriptions are scanned for manipulation heuristics (instruction override, secret access, data exfiltration, covert instructions, embedded directives, hidden/zero-width characters). Because heuristics can false-positive, the default is `warn` (logged and reported, not blocked); set `onPoisoning: "block"` to enforce. - **Shadowing.** When two servers expose a tool with the same name, it's surfaced in the report. Review everything with the `bastion__security` tool. These checks apply to local stdio servers today; authentication checks for remote servers arrive with HTTP transport support. ## Audit & compliance _New in v0.3, opt-in._ Enable `audit` to record a structured, versioned event for every tool call โ€” including calls blocked by the security layer: ```jsonc "audit": { "enabled": true, "includeArgs": "redacted", // none | redacted | full "tamperEvident": true, // integrity hash-chain (detects naive edits; unkeyed, not signed) "sinks": [ { "type": "file", "path": "./bastion-audit.jsonl" }, { "type": "webhook", "url": "https://collector.example/v1/audit" } ] } ``` - **Pluggable sinks.** `console` (stderr JSONL), `file` (JSONL append), `webhook` (batched POST), and `otlp` (native OpenTelemetry logs export โ€” point it at an OTel Collector to fan out to any SIEM/cloud backend). The sink interface makes new destinations additive. - **Compliance mapping.** Each event is mapped to **NIST AI RMF** functions and **OWASP LLM Top 10** categories; `bastion__compliance` returns an aggregate report of recent activity. - **Integrity hash chain.** With `tamperEvident`, events are linked by a SHA-256 hash chain, and the exported `verifyChain` helper detects naive or partial edits within an intact log. The chain is **unkeyed**: a party who can rewrite the log file can recompute a consistent chain, and truncating the most-recent events links cleanly โ€” so treat this as corruption-detection, **not** cryptographic tamper-proofing. For stronger guarantees, ship events to an append-only external sink (webhook / OTLP โ†’ a WORM store or SIEM). Keyed/signed attestation is on the roadmap. - **Redaction (best-effort).** Arguments are omitted by default; `includeArgs: "redacted"` keeps structure while masking values under known credential key-names and common secret patterns. It is a heuristic, not a guarantee โ€” a secret under an unrecognized key can still be recorded, so prefer `none` for high-sensitivity deployments. ## Standards alignment Every security check maps to recognized frameworks โ€” the **NIST AI Risk Management Framework** (a U.S. federal standard), the **OWASP Top 10 for LLM (2025)** and **Agentic (2026)** Applications, and **STRIDE**. The full per-check mapping (with reference links to the MCP-security literature the checks are drawn from) is in **[docs/CHECKS-MAPPING.md](docs/CHECKS-MAPPING.md)**. Coverage is measured independently โ€” bastion is scored against a 22-vector attack surface by the vendor-neutral [mcp-defense-bench](https://github.com/Gowthaman90/mcp-defense-bench) benchmark, with a public [leaderboard and framework mapping](https://gowthaman90.github.io/mcp-defense-bench/). ## Client setup The steps are identical for every client โ€” only the **config file location** differs: | Client | Where to add the `bastion` entry | | -------------- | -------------------------------------------------- | | Claude Code | project `.mcp.json` (or `claude mcp add`) | | Cursor | `~/.cursor/mcp.json` or project `.cursor/mcp.json` | | Claude Desktop | `claude_desktop_config.json` | | Cline | `cline_mcp_settings.json` | | Windsurf | `~/.codeium/windsurf/mcp_config.json` | > **Gradual adoption:** you don't have to route every server through Bastion โ€” put only your flaky or > untrusted servers behind it and leave the rest connected directly. ## Architecture Bastion is organized into clear layers with a one-directional dependency flow, so each concern is independently testable and easy to evolve: ``` src/ โ”œโ”€โ”€ cli.ts # thin CLI entrypoint (parse โ†’ wire โ†’ serve) โ”œโ”€โ”€ index.ts # public library API โ”œโ”€โ”€ errors.ts # error hierarchy (BastionError, โ€ฆ) โ”œโ”€โ”€ config/ # schema (Zod) + loader โ”œโ”€โ”€ core/ # domain: upstream connection lifecycle, aggregation & routing โ”œโ”€โ”€ proxy/ # client-facing MCP server + control tools โ”œโ”€โ”€ observability/ # logging (audit sinks in v0.3) โ””โ”€โ”€ internal/ # small cross-cutting utilities ``` Design details โ€” including the client-agnostic rationale, the interceptor pipeline, and the audit-sink strategy โ€” live in the project's design docs. ## Development ```bash npm install npm run check # format:check + lint + typecheck + test (the full gate) npm test # unit + end-to-end (in-memory transport) tests npm run build # bundle to dist/ (CLI + library) npm run dev -- --config bastion.config.json ``` | Script | Does | | ------------------------- | ----------------------------------- | | `build` | Bundle CLI + library with `tsup`. | | `dev` | Run the CLI from source with `tsx`. | | `typecheck` | `tsc --noEmit` (strict). | | `lint` / `lint:fix` | ESLint (flat config). | | `format` / `format:check` | Prettier. | | `test` / `test:watch` | Vitest. | | `check` | Everything above, as one gate. | ## Roadmap | Version | Theme | Highlights | | ----------- | ---------------------- | ----------------------------------------------------------------------------------------- | | **v0.1** โœ… | **Reliability** | Aggregating proxy, auto-reconnect, `bastion__status` / `__reconnect`. | | **v0.2** โœ… | **Runtime security** | Tool-definition pinning (rug-pull detection), poisoning inspection, shadowing detection. | | **v0.3** โœ… | **Audit & compliance** | Pluggable audit sinks (console / file / webhook), NIST AI RMF / OWASP LLM Top 10 mapping. | Both **stdio** and **Streamable HTTP** transports are supported (see [Transports](#transports)). ## Contributing Contributions are very welcome โ€” this project is built to be community-owned. Please read **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the dev setup, project layout, and PR workflow, and our **[Code of Conduct](./CODE_OF_CONDUCT.md)**. In short: open an issue for non-trivial changes, keep PRs focused with tests, and make sure `npm run check` passes (CI runs it on Node 18/20/22). Good first areas: additional client setup recipes, more upstream test fixtures, and Streamable HTTP transport support. ## Security `mcp-bastion` is security-adjacent software, so we hold it to a high bar. Please report vulnerabilities privately โ€” **do not open a public issue**. See **[SECURITY.md](./SECURITY.md)** for the disclosure process. ## License [Apache-2.0](./LICENSE) ยฉ Gowthaman Arumugam and mcp-bastion contributors