<p align="center">
  <img src="src/ui/favicon.svg" alt="MCP Chaos Rig" width="80" height="80">
</p>

<h1 align="center">MCP Chaos Rig</h1>

<p align="center">A local MCP server that breaks on demand. Test your client against auth failures, disappearing tools, flaky responses, and token expiry, all from a web UI.</p>

<p align="center">
  <a href="https://www.npmjs.com/package/mcp-chaos-rig"><img src="https://img.shields.io/npm/v/mcp-chaos-rig.svg" alt="npm version"></a>
  <a href="https://github.com/Typewise/mcp-chaos-rig/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/mcp-chaos-rig.svg" alt="license"></a>
  <a href="https://www.npmjs.com/package/mcp-chaos-rig"><img src="https://img.shields.io/npm/dm/mcp-chaos-rig.svg" alt="downloads"></a>
</p>

---

## The problem

You're building an MCP client. You need to test OAuth flows, token refresh, tool discovery, error handling, and session lifecycle. Production servers don't fail on command. You need a server that does.

## What Chaos Rig does

Run a local MCP server where you control everything:

- **Break authentication**: force 401s and 500s mid-session, expire tokens on demand, reject refresh tokens
- **Break tools**: disable tools to trigger `tools/changed`, switch schema versions live
- **Break reliability**: add random latency, make tool calls fail at configurable rates
- **See everything**: live request log shows inbound JSON-RPC calls and outbound SSE responses, with click-to-expand bodies

![Server tab](docs/server-tab.png)

### Test scenarios

| Scenario                    | How to test it                                                                                   |
| --------------------------- | ------------------------------------------------------------------------------------------------ |
| OAuth 2.1 consent flow      | Use the interactive consent page: approve, decline, invalid code, tampered state                 |
| Fixed header auth           | Switch to Headers mode, configure key-value pairs, verify client sends them                      |
| Missing/wrong headers       | Send requests with missing or mismatched headers — 401 with details                              |
| Token rejection mid-session | Toggle "Reject OAuth" to 401 or 500 while client is connected                                    |
| Token expiry and refresh    | Set access token TTL to a short value, watch the client refresh                                  |
| Reject refresh tokens       | Toggle "Reject refresh tokens" to force re-authentication                                        |
| Wrong client refreshing     | Enable "Enforce refresh token ownership" — catches clients that lose credentials and re-register |
| No dynamic registration     | Switch to "Pre-registered client only" — `/register` 404s, only your `client_id` works           |
| Rotated client credentials  | Change the pre-registered `client_id` mid-session — the old one now fails with `invalid_client`  |
| Scope discovery conflict    | Set different scopes in metadata vs WWW-Authenticate header, test which the client trusts        |
| Tool disappearing           | Disable a tool in the Tools tab. Clients receive `tools/changed`                                 |
| Tool schema changing        | Switch echo or add between v1 and v2 schemas                                                     |
| Flaky tool calls            | Set failure rate 0-100%. Failed calls return `isError: true`                                     |
| Slow responses              | Enable slow mode with configurable latency range                                                 |
| PKCE code exchange          | OAuth consent page offers "Wrong Code" and "Wrong State" options                                 |
| Database-backed tools       | CRUD operations on a real SQLite contact database                                                |

---

## Quick start

```bash
npx mcp-chaos-rig
```

Control panel at [localhost:4100/ui](http://localhost:4100/ui), MCP endpoint at `http://localhost:4100/mcp`. Requires Node 20+.

If you prefer a global install:

```bash
npm install -g mcp-chaos-rig
mcp-chaos-rig
```

Or run from source:

```bash
git clone https://github.com/Typewise/mcp-chaos-rig.git
cd mcp-chaos-rig
npm install
npm run dev
```

### Remote access

If your production environment needs to reach Chaos Rig, expose it via a tunnel (ngrok, Cloudflare Tunnel, etc.) and set `BASE_URL` so OAuth redirects resolve correctly:

```bash
BASE_URL=https://your-tunnel.example.dev npx mcp-chaos-rig
```

Auth mode starts at Bearer, so a tunnel-facing rig usually wants `AUTH_MODE` too (`none`, `bearer`, `headers`, `oauth`). For OAuth with pre-registered credentials, see [Client registration](#client-registration).

### Auth state

All state is in-memory and resets on restart, back to whatever the environment seeds (`AUTH_MODE`, `OAUTH_CLIENT_MODE`, `STATIC_*`) or to the built-in defaults. Bearer starts with token `test-token-123` (valid until changed). OAuth tokens expire per TTL. Refresh tokens track ownership per client when enabled. After restart, do one refresh with ownership off to re-seed, then turn it on.

---

## Control panel tabs

### Server

Configure auth mode, slow mode (random latency), and flaky tools (% failure rate).

| Auth mode     | Behavior                                              |
| ------------- | ----------------------------------------------------- |
| None          | All requests pass through                             |
| Bearer        | Requires `Authorization: Bearer test-token-123`       |
| Fixed Headers | Requires configured key-value header pairs on every request |
| OAuth 2.1     | Full authorization flow with interactive consent page |

Bearer, Fixed Headers and OAuth modes support fault injection: force 401 or 500 responses to test error handling. The mode starts at Bearer unless `AUTH_MODE` says otherwise.

OAuth mode adds controls for client registration, access token TTL, refresh token rejection, and refresh token ownership enforcement. OAuth endpoints are listed in a collapsible section.

#### Client registration

| Mode                       | Behavior                                                                                  |
| -------------------------- | ----------------------------------------------------------------------------------------- |
| Dynamic registration       | Clients register themselves at `/oauth/register` and get fresh credentials (RFC 7591)     |
| Pre-registered client only | Only the configured `client_id` / `client_secret` is accepted; registration is turned off |

Static mode reproduces authorization servers that issue credentials out of band (Google, Atlassian, most enterprise IdPs):

- `registration_endpoint` disappears from the well-known metadata
- `POST /oauth/register` and `POST /register` return 404 `registration_not_supported`
- any other `client_id` gets `invalid_client` at `/authorize` and `/token`
- an empty `client_secret` makes it a public client, so the token endpoint accepts auth method `none`
- redirect URIs must match a configured one exactly, except the port on loopback hosts (RFC 8252)

The shipped default redirect URI points at a local client. Testing against a deployed client means registering that client's callback instead, or `/authorize` returns 400 `invalid_request` — the response lists the URIs that are registered, since the port relaxation only applies to loopback hosts and `https://` callbacks must match exactly.

Set the client at boot so a tunnel-facing rig starts ready:

```bash
AUTH_MODE=oauth \
OAUTH_CLIENT_MODE=static \
STATIC_CLIENT_ID=acme-client \
STATIC_CLIENT_SECRET=acme-secret \
STATIC_REDIRECT_URIS=https://platform-api.example.app/api/mcp/oauth/callback \
BASE_URL=https://your-tunnel.example.dev npx mcp-chaos-rig
```

`AUTH_MODE` is required here: it defaults to `bearer`, and the OAuth endpoints 404 until it is `oauth` (`none`, `bearer`, `headers`, `oauth`; anything else fails at startup). `STATIC_REDIRECT_URIS` is comma-separated. An empty `STATIC_CLIENT_SECRET=` boots a public client. Everything stays editable from the Server tab afterwards.

Changing the `client_id` drops the previous one, so you can test credential rotation against a live client. Set it from the API too:

```bash
curl -X POST localhost:4100/api/oauth-client -H 'Content-Type: application/json' \
  -d '{"mode":"static","clientId":"acme-client","clientSecret":"acme-secret","redirectUris":["http://localhost:3000/api/mcp/oauth/callback"]}'
```

### Tools

![Tools tab](docs/tools-tab.png)

Toggle tools on/off. Disabling sends `tools/changed` to connected clients. Some tools (echo, add) support version switching.

**Available tools:**

- `echo`: returns your message (v2 adds format options)
- `add`: sums two numbers (v2 accepts an array)
- `get-time`: current server time as ISO 8601
- `random-number`: random integer in a range
- `reverse`: reverses a string
- `typeEcho`: echoes one optional parameter per JSON Schema primitive, to check a client round-trips every type
- `dispute-charge`: files a billing dispute, returns a JSON receipt
- `list-contacts`, `get-contact-by-id`, `get-contact-by-email`, `search-contacts`, `create-contact`, `update-contact`, `delete-contact`: SQLite CRUD

Three large-schema tools start disabled, for testing how a client handles wide inputs: `submit-customs-declaration` (all fields required), `create-product-listing` (25 required, 25 optional), `search-properties` (50 optional filters).

### Contacts

![Contacts tab](docs/contacts-tab.png)

View and reset the SQLite database backing the contact tools. Starts with three seed records.

### Log

![Log tab](docs/log-tab.png)

Live request log showing inbound requests and outbound SSE responses. Displays timestamp, source (mcp/auth/sse), method, status, JSON-RPC method, tool name, and arguments. Click any truncated body or args line to expand it. Keeps last 200 entries.

---

## OAuth consent page

![OAuth consent page](docs/oauth-consent.png)

When auth mode is OAuth, the authorization endpoint shows an interactive consent page:

| Button      | Result                                             |
| ----------- | -------------------------------------------------- |
| Approve     | Redirects with valid authorization code            |
| Decline     | Redirects with `error=access_denied`               |
| Wrong Code  | Redirects with invalid code (token exchange fails) |
| Wrong State | Redirects with tampered state parameter            |

---

## Links

- [npm package](https://www.npmjs.com/package/mcp-chaos-rig)
- [GitHub repository](https://github.com/Typewise/mcp-chaos-rig)
- [MIT License](LICENSE)
