{"id":"ff9bbe31-55ae-4b08-8a10-99cbe4e4c492","slug":"typewise-mcp-chaos-rig","kind":"mcp","name":"mcp-chaos-rig","summary":"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.","owner":"Typewise","repoUrl":"https://github.com/Typewise/mcp-chaos-rig","license":"MIT","licenseTier":"permissive","trust":"community","stars":10,"match":null,"fewStars":false,"format":"mcp","createdAt":"2026-02-04T10:27:01.000Z","addedAt":"2026-10-14T05:24:04.449Z","logoUrl":"https://avatars.githubusercontent.com/u/89851760?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","developer-tools","design-media"],"tags":["chaos","debugging","mock","server"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-14T23:27:19.778Z","sample":false,"url":"https://conduits.dev/tools/typewise-mcp-chaos-rig","language":"TypeScript","transports":["stdio"],"install":{"npm":"mcp-chaos-rig"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\">\n  <img src=\"src/ui/favicon.svg\" alt=\"MCP Chaos Rig\" width=\"80\" height=\"80\">\n</p>\n\n<h1 align=\"center\">MCP Chaos Rig</h1>\n\n<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>\n\n<p align=\"center\">\n  <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>\n  <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>\n  <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>\n</p>\n\n---\n\n## The problem\n\nYou'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.\n\n## What Chaos Rig does\n\nRun a local MCP server where you control everything:\n\n- **Break authentication**: force 401s and 500s mid-session, expire tokens on demand, reject refresh tokens\n- **Break tools**: disable tools to trigger `tools/changed`, switch schema versions live\n- **Break reliability**: add random latency, make tool calls fail at configurable rates\n- **See everything**: live request log shows inbound JSON-RPC calls and outbound SSE responses, with click-to-expand bodies\n\n![Server tab](docs/server-tab.png)\n\n### Test scenarios\n\n| Scenario                    | How to test it                                                                                   |\n| --------------------------- | ------------------------------------------------------------------------------------------------ |\n| OAuth 2.1 consent flow      | Use the interactive consent page: approve, decline, invalid code, tampered state                 |\n| Fixed header auth           | Switch to Headers mode, configure key-value pairs, verify client sends them                      |\n| Missing/wrong headers       | Send requests with missing or mismatched headers — 401 with details                              |\n| Token rejection mid-session | Toggle \"Reject OAuth\" to 401 or 500 while client is connected                                    |\n| Token expiry and refresh    | Set access token TTL to a short value, watch the client refresh                                  |\n| Reject refresh tokens       | Toggle \"Reject refresh tokens\" to force re-authentication                                        |\n| Wrong client refreshing     | Enable \"Enforce refresh token ownership\" — catches clients that lose credentials and re-register |\n| No dynamic registration     | Switch to \"Pre-registered client only\" — `/register` 404s, only your `client_id` works           |\n| Rotated client credentials  | Change the pre-registered `client_id` mid-session — the old one now fails with `invalid_client`  |\n| Scope discovery conflict    | Set different scopes in metadata vs WWW-Authenticate header, test which the client trusts        |\n| Tool disappearing           | Disable a tool in the Tools tab. Clients receive `tools/changed`                                 |\n| Tool schema changing        | Switch echo or add between v1 and v2 schemas                                                     |\n| Flaky tool calls            | Set failure rate 0-100%. Failed calls return `isError: true`                                     |\n| Slow responses              | Enable slow mode with configurable latency range                                                 |\n| PKCE code exchange          | OAuth consent page offers \"Wrong Code\" and \"Wrong State\" options                                 |\n| Database-backed tools       | CRUD operations on a real SQLite contact database                                                |\n\n---\n\n## Quick start\n\n```bash\nnpx mcp-chaos-rig\n```\n\nControl panel at [localhost:4100/ui](http://localhost:4100/ui), MCP endpoint at `http://localhost:4100/mcp`. Requires Node 20+.\n\nIf you prefer a global install:\n\n```bash\nnpm install -g mcp-chaos-rig\nmcp-chaos-rig\n```\n\nOr run from source:\n\n```bash\ngit clone https://github.com/Typewise/mcp-chaos-rig.git\ncd mcp-chaos-rig\nnpm install\nnpm run dev\n```\n\n### Remote access\n\nIf 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:\n\n```bash\nBASE_URL=https://your-tunnel.example.dev npx mcp-chaos-rig\n```\n\nAuth 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).\n\n### Auth state\n\nAll 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.\n\n---\n\n## Control panel tabs\n\n### Server\n\nConfigure auth mode, slow mode (random latency), and flaky tools (% failure rate).\n\n| Auth mode     | Behavior                                              |\n| ------------- | ----------------------------------------------------- |\n| None          | All requests pass through                             |\n| Bearer        | Requires `Authorization: Bearer test-token-123`       |\n| Fixed Headers | Requires configured key-value header pairs on every request |\n| OAuth 2.1     | Full authorization flow with interactive consent page |\n\nBearer, 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.\n\nOAuth 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.\n\n#### Client registration\n\n| Mode                       | Behavior                                                                                  |\n| -------------------------- | ----------------------------------------------------------------------------------------- |\n| Dynamic registration       | Clients register themselves at `/oauth/register` and get fresh credentials (RFC 7591)     |\n| Pre-registered client only | Only the configured `client_id` / `client_secret` is accepted; registration is turned off |\n\nStatic mode reproduces authorization servers that issue credentials out of band (Google, Atlassian, most enterprise IdPs):\n\n- `registration_endpoint` disappears from the well-known metadata\n- `POST /oauth/register` and `POST /register` return 404 `registration_not_supported`\n- any other `client_id` gets `invalid_client` at `/authorize` and `/token`\n- an empty `client_secret` makes it a public client, so the token endpoint accepts auth method `none`\n- redirect URIs must match a configured one exactly, except the port on loopback hosts (RFC 8252)\n\nThe 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.\n\nSet the client at boot so a tunnel-facing rig starts ready:\n\n```bash\nAUTH_MODE=oauth \\\nOAUTH_CLIENT_MODE=static \\\nSTATIC_CLIENT_ID=acme-client \\\nSTATIC_CLIENT_SECRET=acme-secret \\\nSTATIC_REDIRECT_URIS=https://platform-api.example.app/api/mcp/oauth/callback \\\nBASE_URL=https://your-tunnel.example.dev npx mcp-chaos-rig\n```\n\n`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.\n\nChanging the `client_id` drops the previous one, so you can test credential rotation against a live client. Set it from the API too:\n\n```bash\ncurl -X POST localhost:4100/api/oauth-client -H 'Content-Type: application/json' \\\n  -d '{\"mode\":\"static\",\"clientId\":\"acme-client\",\"clientSecret\":\"acme-secret\",\"redirectUris\":[\"http://localhost:3000/api/mcp/oauth/callback\"]}'\n```\n\n### Tools\n\n![Tools tab](docs/tools-tab.png)\n\nToggle tools on/off. Disabling sends `tools/changed` to connected clients. Some tools (echo, add) support version switching.\n\n**Available tools:**\n\n- `echo`: returns your message (v2 adds format options)\n- `add`: sums two numbers (v2 accepts an array)\n- `get-time`: current server time as ISO 8601\n- `random-number`: random integer in a range\n- `reverse`: reverses a string\n- `typeEcho`: echoes one optional parameter per JSON Schema primitive, to check a client round-trips every type\n- `dispute-charge`: files a billing dispute, returns a JSON receipt\n- `list-contacts`, `get-contact-by-id`, `get-contact-by-email`, `search-contacts`, `create-contact`, `update-contact`, `delete-contact`: SQLite CRUD\n\nThree 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).\n\n### Contacts\n\n![Contacts tab](docs/contacts-tab.png)\n\nView and reset the SQLite database backing the contact tools. Starts with three seed records.\n\n### Log\n\n![Log tab](docs/log-tab.png)\n\nLive 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.\n\n---\n\n## OAuth consent page\n\n![OAuth consent page](docs/oauth-consent.png)\n\nWhen auth mode is OAuth, the authorization endpoint shows an interactive consent page:\n\n| Button      | Result                                             |\n| ----------- | -------------------------------------------------- |\n| Approve     | Redirects with valid authorization code            |\n| Decline     | Redirects with `error=access_denied`               |\n| Wrong Code  | Redirects with invalid code (token exchange fails) |\n| Wrong State | Redirects with tampered state parameter            |\n\n---\n\n## Links\n\n- [npm package](https://www.npmjs.com/package/mcp-chaos-rig)\n- [GitHub repository](https://github.com/Typewise/mcp-chaos-rig)\n- [MIT License](LICENSE)\n"}