SimProxy
SimProxy is a proxy simulation manager. Point it at a real upstream — an HTTP/RPC endpoint or a WebSocket server — and it hands you back an endpoint that behaves exactly like the real one until you decide it shouldn't. Break it, throttle it, make it return any error code you like, then put it back up. Your application keeps pointing at the same URL the whole time.
What is SimProxy
Testing what your app does when a dependency fails is hard. You can't take down a public RPC node, and mocks only cover the code paths you remembered to mock. SimProxy sits between your app and the real dependency, forwards traffic transparently, and lets you inject failures on demand — from the UI while you watch, or from a terminal inside an automated test.
What you can simulate
- A total outage — every request answered with your chosen status code and message; WebSockets closed and refused.
- Rate limiting — a hard requests-per-second ceiling that starts returning
429once exceeded. - Custom error contracts — any status code (
500,503,429,418…) with any JSON error message. - Recovery — flip the same endpoint back to healthy and watch your retry and backoff logic recover.
Typical uses
- Building and verifying error states, retries, fallbacks and reconnect logic.
- Reproducing "the API was down" bug reports deterministically.
- End-to-end tests (Playwright, Cypress, Jest, Vitest) that assert what the UI does during an outage.
- Demoing resilience live: break the dependency mid-demo from a second tab.
How it works
You create a simulation: a port, the real upstream URL, and how it should fail. SimProxy starts a proxy on that port and gives you back an endpoint. Everything your app sends there is forwarded to the real upstream — until you break it.
- Break makes the endpoint fail the way you configured it. Fix makes it healthy again. Nothing restarts and the port stays bound, so your app never sees a connection refused unless you want it to.
- Live logs from every simulation stream into the UI, and to any terminal or script that wants them.
- Everything over HTTPS — HTTP and WebSocket simulations are served from the SimProxy domain under
/proxy/<port>, so your endpoints are secure by default with nothing to configure.
Quick start
SimProxy is a hosted service — there is nothing to install or run. The web UI and the API both live at:
https://proxy.leafapi.com
In 60 seconds
- Open proxy.leafapi.com.
- Paste your real upstream into Live URL, pick a Protocol, hit ⚡ Suggest Port, then Deploy Simulation.
- Copy the Simulated Endpoint from the table and point your app at it instead of the real URL.
- Click Break. Your app now sees the failure. Click Fix to bring it back.
That is the whole loop. Everything below covers the same screens in more detail — and if
you would rather drive it from a script or a test, the terminal
guide does exactly the same things with curl.
In 60 seconds
The x-user-code header is what ties a simulation to you — copy yours from the
profile icon in the top-right corner:
Ask AI to Configure Terminal Quick Start Automatically
Copy this prompt into your AI coding assistant (Claude, Cursor, Antigravity, Devin, VS Code Copilot, etc.) to execute terminal simulation checks and API calls automatically:
Read the terminal documentation at https://proxy.leafapi.com/docs.html?doc=terminal to understand SimProxy CLI operations. Please write shell commands using curl to check port availability, create an HTTP proxy for "https://api.example.com" with header "x-user-code: <$SIM_CODE>", simulate an outage with /down, restore with /up, and delete when done.
# 1 · is the port you want free? (leave "port" out and SimProxy picks one)
curl -s "https://proxy.leafapi.com/api/ports/check?port=5001"
# → {"port":5001,"available":true}
# 2 · create the simulation
curl -X POST https://proxy.leafapi.com/api/simulations \
-H "Content-Type: application/json" \
-H "x-user-code: <$SIM_CODE>" \
-d '{"url_type":"http","port":5001,"simulate_url":"https://api.example.com"}'
# → {"port":5001,"state":"up"}
# 3 · break it, then fix it — a simulation is addressed by its port
curl -X POST https://proxy.leafapi.com/api/simulations/5001/down -H "x-user-code: <$SIM_CODE>" # break
curl -X POST https://proxy.leafapi.com/api/simulations/5001/up -H "x-user-code: <$SIM_CODE>" # fix
# 4 · delete the simulation when done
curl -X DELETE https://proxy.leafapi.com/api/simulations/5001 -H "x-user-code: <$SIM_CODE>"Core concepts
| Term | Meaning |
|---|---|
| Simulation | One proxy with its own port, upstream URL and failure settings. The port is how you address it in every call. |
Live URLsimulate_url | The real upstream traffic is forwarded to. |
Simulated endpointproxy_url | The URL you give your application in place of the real one. |
| State | up (forwarding normally), down (failing on purpose), starting, or stopped. |
| Break / Fix | Switching between down and up. Instant, and the port stays bound either way. |
Rate limitmax_requests_per_second | Traffic ceiling per second. Anything above it is rejected while the simulation is otherwise healthy. |
| Sim-Code | Your workspace, shown in the profile icon in the top-right corner. Everything you create belongs to it, and nobody else can see or control it. In the browser there is nothing to do — the page uses it for you. |
Sim-Codex-user-code | Issued by SimProxy and tied to everything you create. Only the code that created a simulation can see, break, fix, rename or delete it — see Your user code. |
Create a simulation
-
Open the Create Simulation panel
It's the first panel on the home page. Every panel can be collapsed with Minimize if you want more room.
-
Paste the Live URL
The real upstream you want to stand in front of:
https://api.example.com— an HTTP API or RPC nodewss://node.example.com/ws— a WebSocket server
-
Pick the Protocol
httporws. It must match the Live URL — see HTTP & WebSocket for how each one fails. -
Choose a Port
Anything from
1000to65535that's free. Click ⚡ Suggest Port and SimProxy fills in the next available one for you. -
Set the failure shape
Error Code (default
500) and Error Message are what callers receive while the simulation is broken. Enable Custom JSON response to paste a multi-field JSON error payload (with realtime syntax validation highlighting errors in red and disabling deployment until fixed). Click ⚡ Suggest Error to cycle through 10 curated error presets (500-level errors first: 500, 502, 503, 504, 507, followed by 400-level: 400, 401, 403, 404, 429), automatically updating both the Error Code and error payload. -
Name it (optional)
Leave Name blank and SimProxy names it after the upstream hostname. Create a second simulation for the same host and it becomes
example.com-v2, so nothing collides. -
Deploy Simulation
The row appears in Running Simulations with state
up. Click its Simulated Endpoint to copy it, and point your app there instead of the real URL.
API_URL, RPC_URL…) so you can move between the real upstream and a simulation without touching code.
Break, fix & manage
The Running Simulations table is the control panel. Each row shows the id, name, type, port, simulated endpoint, live URL, state and process id, with the actions on the right.
| Action | What happens |
|---|---|
| Break | State becomes down. Callers immediately get your error code and message (or a closed socket, for WebSocket simulations). Traffic stops reaching the real upstream. |
| Fix | State returns to up and forwarding resumes instantly. No restart, no dropped port. |
| Delete | Stops the proxy and removes the row. The port is released. |
| Click the name | Rename it inline — handy when several simulations share an upstream. |
| Click the endpoint | Copies it to your clipboard. |
Reading the state column
| State | Meaning |
|---|---|
starting | The proxy is coming up. It becomes up within a moment. |
up | Healthy — forwarding to the real upstream. |
down | Broken on purpose. This is your doing, not a fault. |
stopped | The proxy exited. Check its terminal for the reason, then delete and recreate it. |
The table scrolls internally after ten rows, so a long list never pushes the terminals off screen.
Watch live logs
-
Tick the Terminal checkbox on a row
A terminal card appears in the Terminal Views panel and starts streaming that simulation's output live. The checkbox in the table header selects or clears them all at once.
-
Arrange the grid
Use the 1 / 2 / 3 / 4 buttons in the panel header to set how many terminals sit side by side, and the height controls to make the cards taller or shorter. Both settings are remembered for next time.
-
Read the traffic
Each line is output straight from the proxy — forwarded requests, rate-limit rejections, connection open and close events, and a note whenever you break or fix it. Watch a terminal while you click Break to see the exact moment behaviour changes.
Setup & your user code
<$SIM_CODE> below is
yours: loading…. Copy it from the profile icon, top
right. In the browser you never need it — the UI sends it for you.
Everything the UI does is a plain HTTP call against https://proxy.leafapi.com, so
the whole lifecycle is scriptable with curl, fetch, or any HTTP
client. Responses are JSON. CORS is open, so browser-based test runners work too.
Your user code
Every request carries a user code in the x-user-code header.
It is what makes a simulation yours: only the code that created one can list, break, fix,
rename or delete it. Anyone else — including someone who guesses your port — gets a
404, as though it were not there.
How to get your Sim-Code:
- Click the profile icon in the top-right corner of the dashboard to copy your active Sim-Code. Use this value in your MCP client config (
x-user-codeheader orSIM_CODEenvironment variable), terminal scripts, or CI environment.
Codes are signed by the server, so an invented one is rejected with 401 — it
cannot create anything or reach anything. Keep yours somewhere your scripts can read it
(an environment variable or a CI secret) and reuse it; a new code means a new, empty
workspace.
Check it's working
curl -s https://proxy.leafapi.com/api/version
# → {"version":"1.3.18"}
curl -s https://proxy.leafapi.com/api/simulations \
-H "x-user-code: <$SIM_CODE>"
# → [] (a fresh code starts with nothing)Full lifecycle
1 · Pick a port (optional)
Leave the port out and SimProxy assigns a free one — only ask for a specific port when something is hard-coded to it. When the port is taken the reply names a free one, so one call is enough either way. No Sim-Code needed.
curl -s "https://proxy.leafapi.com/api/ports/check?port=5000"
# → {"port":5000,"available":true}
# → {"port":5000,"available":false,"suggested_port":5001} when taken2 · Create a simulation
# HTTP / RPC - no port given, SimProxy assigns one
curl -s -X POST https://proxy.leafapi.com/api/simulations \
-H "Content-Type: application/json" \
-H "x-user-code: <$SIM_CODE>" \
-d '{
"name": "flaky-api",
"url_type": "http",
"simulate_url": "https://api.example.com",
"error_status_code": 503,
"error_response_message": "Upstream unavailable (simulated)",
"max_requests_per_second": 15
}'
# WebSocket
curl -s -X POST https://proxy.leafapi.com/api/simulations \
-H "Content-Type: application/json" \
-H "x-user-code: <$SIM_CODE>" \
-d '{
"url_type": "ws",
"port": 5002,
"simulate_url": "wss://node.example.com/ws",
"max_requests_per_second": 5
}'The response is the full simulation object, so one call gives you everything you need — the assigned port, the id, and the URL to point your app at:
CREATED=$(curl -s -X POST https://proxy.leafapi.com/api/simulations \
-H "Content-Type: application/json" \
-H "x-user-code: <$SIM_CODE>" \
-d '{"url_type":"http","simulate_url":"https://api.example.com"}')
SIM_PORT=$(echo "$CREATED"| jq -r .port) # 5001, chosen for you
API_URL=$(echo "$CREATED" | jq -r .proxy_url) # https://proxy.leafapi.com/proxy/5001
echo "port $SIM_PORT → $API_URL"If you do ask for a specific port and it's taken, the error tells you what to use instead:
curl -s -X POST https://proxy.leafapi.com/api/simulations \
-H "Content-Type: application/json" \
-H "x-user-code: <$SIM_CODE>" \
-d '{"url_type":"http","port":5001,"simulate_url":"https://api.example.com"}'
# → 409 {"error":"port 5001 is already in use",
# "suggested_port":5002,"retry_with":{"port":5002}}3 · List what you have
curl -s https://proxy.leafapi.com/api/simulations \ -H "x-user-code: <$SIM_CODE>" \ | jq -r '.[] | "\(.port)\t\(.name)\t\(.state)\t\(.endpoint)"'
4 · Break and fix
Address a simulation by its id or by its port — both work, and both are scoped to your code.
# break → state: down curl -s -X POST https://proxy.leafapi.com/api/simulations/5001/down \ -H "x-user-code: <$SIM_CODE>" # fix → state: up curl -s -X POST https://proxy.leafapi.com/api/simulations/5001/up \ -H "x-user-code: <$SIM_CODE>"
5 · Rename
curl -s -X POST https://proxy.leafapi.com/api/simulations/5001 \
-H "Content-Type: application/json" \
-H "x-user-code: <$SIM_CODE>" \
-d '{"name":"payments-api"}'6 · Read the logs
curl -s https://proxy.leafapi.com/api/simulations/5001/logs \ -H "x-user-code: <$SIM_CODE>" \ | jq -r '.logs[] | "\(.at) [\(.stream)] \(.line)"'
7 · Delete
Deleting stops the proxy and releases the port, by id or by port number:
curl -s -X DELETE https://proxy.leafapi.com/api/simulations/5001 \
-H "x-user-code: <$SIM_CODE>"
# → {"deleted":true,"process_termination_requested":true}
# or everything this workspace created
for port in $(curl -s https://proxy.leafapi.com/api/simulations \
-H "x-user-code: <$SIM_CODE>" | jq -r '.[].port'); do
curl -s -X DELETE "https://proxy.leafapi.com/api/simulations/$port" \
-H "x-user-code: <$SIM_CODE>" > /dev/null
done/api/session at the start of a pipeline and use that code for the run. Everything it creates is invisible to everyone else, and "delete everything I created" at the end is then completely safe.
API reference
Base URL https://proxy.leafapi.com. JSON in, JSON out. Send
x-user-code on every call except /api/session,
/api/version, /api/ports/check and
/api/ports/suggest, which need none.
A simulation is addressed by its port (5001) — that is
what :port means in every route below. Ports are resolved among your own
simulations only, so a port you do not own reads as though it were not there.
| Method | Path | Description |
|---|---|---|
| GET | /api/session | Issues a user code. Send an existing code to have it echoed back instead of a new one. |
| GET | /api/version | Version of the running service. Handy as a health check. |
| GET | /api/simulations | Your simulations. |
| POST | /api/simulations | Create and start a simulation. Returns 201 with the object. |
| POST | /api/simulations/:port | Rename. Body { "name": "…" }. PATCH works too, for scripts already written against it. |
| POST | /api/simulations/:port/down | Break it. |
| POST | /api/simulations/:port/up | Fix it. |
| DELETE | /api/simulations/:port | Stop the proxy, free the port and remove it. |
| GET | /api/simulations/:port/logs | Recent log lines for one simulation. |
| GET | /api/ports/suggest?from= | First free port at or above from. No Sim-Code needed. |
| GET | /api/ports/check?port= | Whether that one port is free. Adds suggested_port when it is not. No Sim-Code needed. |
| GET | /api/logs/stream | Live log stream (Server-Sent Events). |
Create body
| Field | Required | Default | Notes |
|---|---|---|---|
url_type | yes | — | http | ws | db |
port | no | next free port | Integer 1000–65535. Leave it out and SimProxy picks a free one; the response tells you which. |
simulate_url | yes | — | The real upstream to forward to. |
name | no | upstream hostname | Duplicates get a -v2 suffix. |
error_status_code | no | 500 | Status returned while broken. |
error_response_message | no | Service unavailable (simulated) | Plain string or valid JSON object string returned while broken. |
is_custom_json | no | false | Set to true when error_response_message is formatted as a custom JSON object. Checked on server. |
max_requests_per_second | no | 15 | Traffic ceiling. Above it, callers get 429. |
Simulation object
{
"id": "sim-1",
"name": "api.example.com",
"urlType": "http",
"port": 5001,
"simulateUrl": "https://api.example.com",
"errorStatusCode": 503,
"errorResponseMessage": "Upstream unavailable (simulated)",
"maxRequestsPerSecond": 15,
"state": "up",
"createdAt": "2026-09-17T09:12:44.881Z",
"original_url": "https://api.example.com", // what it forwards to
"proxy_url": "https://proxy.leafapi.com/proxy/5001", // what your app should call
"last_activity_at": "2026-09-17T11:02:10.004Z", // last traffic it carried
"idle_expires_at": "2026-09-17T14:02:10.004Z", // 3h after that
"expires_at": "2026-09-18T09:12:44.881Z" // 24h after creation
}Error responses
| Status | When |
|---|---|
400 | Missing or invalid field — unknown url_type, port out of range, or no simulate_url. |
401 | No user code, or one that SimProxy did not issue. Get one from /api/session. |
404 | No simulation of yours matches that id or port. Simulations belonging to other codes are invisible, so this is also what a stranger's simulation looks like. |
409 | The port you asked for is taken — the response carries suggested_port and retry_with so a script can resubmit straight away. Also returned if you break or fix a simulation that has stopped. |
503 | No free port was available at all. |
409 carries suggested_port, so a script can resubmit with that port instead of failing the test run.
Streaming logs
GET /api/logs/stream is a Server-Sent Events endpoint that replays a short
backlog and then pushes new lines as they happen — useful for attaching proxy traffic to a
CI artefact, or asserting that a request actually reached the proxy.
| Query | Meaning |
|---|---|
ids | Comma-separated ports. Omit for everything in your workspace. |
backlog | Lines replayed per simulation on connect. Default 50, max 300, 0 for live-only. |
Follow from a terminal
curl -N -H "x-user-code: <$SIM_CODE>" \ "https://proxy.leafapi.com/api/logs/stream?ids=5001&backlog=0" \ | grep --line-buffered '^data: ' | sed -u 's/^data: //' | jq -r 'select(.line) | .line'
Capture to a file during a test run
curl -N -H "x-user-code: <$SIM_CODE>" \ "https://proxy.leafapi.com/api/logs/stream?backlog=0" > proxy.log & LOG_PID=$! npm test kill $LOG_PID
Each entry looks like this — stream is stdout, stderr or system:
{ "id": "sim-1", "name": "api.example.com",
"stream": "stdout", "line": "Forwarding request to upstream",
"at": "2026-09-17T09:13:02.104Z" }A heartbeat event arrives every few seconds to keep the connection alive; skip entries without a line field.
Playwright
Create the simulation once per suite, point the app at it, and break it inside the test that
needs an outage. No SDK required — fetch is enough.
Ask AI to Configure Playwright Automatically
Copy this prompt into your AI coding assistant to automatically generate a Playwright test suite with SimProxy outage testing helper:
Read the Playwright guide at https://proxy.leafapi.com/docs.html?doc=terminal#playwright to understand SimProxy test automation. Please create a Playwright helper utility (tests/simproxy.ts) using header "x-user-code: <$SIM_CODE>" and an E2E test file that provisions a proxy simulation in beforeAll, tests happy paths, breaks/fixes the proxy to test UI resilience, and cleans up in afterAll.
A small helper
// tests/simproxy.ts
const BASE = process.env.SIMPROXY_URL ?? "https://proxy.leafapi.com";
let CODE = process.env.SIMPROXY_CODE ?? ""; // set one in CI, or fetch below
async function call(path: string, init: RequestInit = {}) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { "content-type": "application/json", "x-user-code": CODE, ...init.headers },
});
if (!res.ok) throw new Error(`SimProxy ${path} → ${res.status} ${await res.text()}`);
return res.json();
}
export const simproxy = {
/* Call once before the first create if SIMPROXY_CODE is not set. */
signIn: async () => {
if (!CODE) CODE = (await (await fetch(`${BASE}/api/session`)).json()).user_code;
return CODE;
},
create: (body: Record<string, unknown>) =>
call("/api/simulations", { method: "POST", body: JSON.stringify(body) }),
break: (port: number) => call(`/api/simulations/${port}/down`, { method: "POST" }),
fix: (port: number) => call(`/api/simulations/${port}/up`, { method: "POST" }),
remove:(port: number) => call(`/api/simulations/${port}`, { method: "DELETE" }),
/* The port itself when it is free, otherwise the free one it suggests. */
freePort: async (port = 5000) => {
const res = await call(`/api/ports/check?port=${port}`);
return (res.available ? port : res.suggested_port) as number;
},
};The test
import { test, expect } from "@playwright/test";
import { simproxy } from "./simproxy";
let sim: { port: number; proxy_url: string };
test.beforeAll(async () => {
await simproxy.signIn();
sim = await simproxy.create({
name: "checkout-api",
url_type: "http",
simulate_url: process.env.REAL_API_URL, // no port: one is assigned
error_status_code: 503,
error_response_message: "Payments API is down (simulated)",
});
// start / configure your app with API_URL = sim.proxy_url
});
test.afterAll(async () => {
await simproxy.remove(sim.port);
});
test("checkout works while the API is healthy", async ({ page }) => {
await page.goto("/checkout");
await expect(page.getByRole("button", { name: "Pay" })).toBeEnabled();
});
test("shows a retry banner when the API goes down", async ({ page }) => {
await page.goto("/checkout");
await simproxy.break(sim.port);
await page.getByRole("button", { name: "Pay" }).click();
await expect(page.getByText("We couldn't reach payments")).toBeVisible();
await simproxy.fix(sim.port);
await page.getByRole("button", { name: "Try again" }).click();
await expect(page.getByText("Payment confirmed")).toBeVisible();
});Testing throttling
Create the simulation with a low ceiling and the proxy starts returning 429 under load — enough to assert your backoff or your "slow down" message.
sim = await simproxy.create({
url_type: "http", simulate_url: process.env.REAL_API_URL,
max_requests_per_second: 2,
});Testing a dropped WebSocket
Breaking a ws simulation closes every live socket and refuses new ones — exactly what an upstream going away looks like. Fix it and your reconnect logic should recover on its own.
test("reconnects after the socket drops", async ({ page }) => {
await page.goto("/live");
await expect(page.getByTestId("status")).toHaveText("connected");
await simproxy.break(sim.port);
await expect(page.getByTestId("status")).toHaveText("reconnecting");
await simproxy.fix(sim.port);
await expect(page.getByTestId("status")).toHaveText("connected", { timeout: 15_000 });
});Jest / Vitest
The same pattern works in any runner — it's just HTTP calls in setup and teardown.
Ask AI to Configure Jest / Vitest Automatically
Copy this prompt into your AI coding assistant to configure Jest or Vitest integration for API outage and retry testing:
Read the Jest & Vitest testing guide at https://proxy.leafapi.com/docs.html?doc=terminal#test-frameworks. Please write a Vitest/Jest test file that provisions a SimProxy simulation in beforeAll using header "x-user-code: <$SIM_CODE>", asserts client retry & fallback behavior during simulated outages, and removes the proxy in afterAll.
import { beforeAll, afterAll, expect, test } from "vitest";
import { simproxy } from "./simproxy";
let sim;
beforeAll(async () => {
await simproxy.signIn();
sim = await simproxy.create({
url_type: "http",
simulate_url: "https://api.example.com",
error_status_code: 503,
});
});
afterAll(() => simproxy.remove(sim.port));
test("client retries and then surfaces the outage", async () => {
const client = makeClient({ baseUrl: sim.proxy_url, retries: 2 });
await simproxy.break(sim.port);
await expect(client.getOrders()).rejects.toThrow(/unavailable/i);
await simproxy.fix(sim.port);
await expect(client.getOrders()).resolves.toHaveLength(3);
});CI & shell scripts
Ask AI to Configure CI & Shell Scripts Automatically
Copy this prompt into your AI coding assistant to generate CI pipeline shell scripts with isolated session codes and chaos loops:
Read the CI & shell scripts guide at https://proxy.leafapi.com/docs.html?doc=terminal#ci. Please write a CI pipeline bash script that requests an isolated Sim-Code from /api/session, creates a proxy simulation, sets EXIT trap cleanup, triggers dependency outages during E2E runs, and logs results.
Wrap a test run in an outage
#!/usr/bin/env bash
set -euo pipefail
# A fresh Sim-Code for this run: nothing it creates is visible to anyone else.
# (A pipeline issues its own rather than reusing the one from your browser.)
SIM_CODE=$(curl -s https://proxy.leafapi.com/api/session | jq -r .user_code)
CREATED=$(curl -s -X POST https://proxy.leafapi.com/api/simulations \
-H "Content-Type: application/json" \
-H "x-user-code: $SIM_CODE" \
-d "{\"url_type\":\"http\",\"simulate_url\":\"$REAL_API_URL\",\"error_status_code\":503}")
SIM_PORT=$(echo "$CREATED" | jq -r .port)
export API_URL=$(echo "$CREATED" | jq -r .proxy_url)
trap 'curl -s -X DELETE "https://proxy.leafapi.com/api/simulations/$SIM_PORT" \
-H "x-user-code: $SIM_CODE" > /dev/null' EXIT
npm run test:e2e:happy-path
curl -s -X POST "https://proxy.leafapi.com/api/simulations/$SIM_PORT/down" \
-H "x-user-code: $SIM_CODE" > /dev/null
npm run test:e2e:outageBreak the dependency part-way through a run
( sleep 5; curl -s -X POST "https://proxy.leafapi.com/api/simulations/$SIM_PORT/down" \
-H "x-user-code: $SIM_CODE" > /dev/null ) &
npm run test:integrationChaos loop — up 30s, down 10s, repeat
while true; do
curl -s -X POST "https://proxy.leafapi.com/api/simulations/$SIM_PORT/up" \
-H "x-user-code: $SIM_CODE" > /dev/null; echo "UP"; sleep 30
curl -s -X POST "https://proxy.leafapi.com/api/simulations/$SIM_PORT/down" \
-H "x-user-code: $SIM_CODE" > /dev/null; echo "DOWN"; sleep 10
doneAssert your retry budget
curl -s -X POST "https://proxy.leafapi.com/api/simulations/$SIM_PORT/down" \
-H "x-user-code: $SIM_CODE" > /dev/null
for i in $(seq 1 10); do
curl -s -o /dev/null -w "%{http_code} " "$API_URL/health"
done
echo # → 503 503 503 503 ...Wait until a simulation is ready
until [ "$(curl -s https://proxy.leafapi.com/api/simulations \
-H "x-user-code: $SIM_CODE" \
| jq -r --arg port "$SIM_PORT" \
'.[] | select(.port==($port|tonumber)) | .state')" = "up" ]; do
sleep 0.2
doneWhat is MCP
The Model Context Protocol (MCP) is an open standard by Anthropic that allows AI assistants (such as Claude Desktop, Cursor IDE, or VS Code Copilot) to connect directly to SimProxy. Instead of copying curl commands or manually clicking in the browser UI, your AI assistant can create, break, fix, rename, duplicate, and monitor simulations using simple natural language.
What your AI assistant can do
- "Create an HTTP proxy for https://api.example.com with rate limit 10 req/s"
- "Show me all my running simulations"
- "Simulate an outage on my proxy on port 5001"
- "Fetch recent logs from my RPC proxy"
- "Clean up all stopped simulations"
Client Configuration
SimProxy supports both Streamable HTTP (remote/hosted deployments) and stdio (local CLI mode). Choose your AI client tab below to view its specific configuration snippet or copy the AI Prompt to let your AI assistant configure MCP automatically.
Ask AI to Configure MCP Automatically
Copy this prompt into your AI coding assistant (Claude, Cursor, Antigravity, Devin, VS Code Copilot, PhpStorm AI, etc.). The AI will fetch the documentation URL to understand the MCP tools and write your configuration automatically:
Read the MCP documentation at https://proxy.leafapi.com/docs.html?doc=mcp to understand the SimProxy Model Context Protocol integration and available executable tools. Please configure the MCP server "proxy-url" for this workspace using endpoint "https://proxy.leafapi.com/mcp", transport "streamable-http", and header "x-user-code: <$SIM_CODE>".
Ask AI to Create a SimProxy Testing Skill
Copy this prompt into your AI coding assistant to give it a reusable skill. From then on, whenever a task involves downtime, retries, failover or error handling, the AI spins up a SimProxy simulation, breaks the dependency, checks how your code reacts and cleans up, without you asking:
Read the MCP documentation at https://proxy.leafapi.com/docs.html?doc=mcp to learn the SimProxy MCP tools. If the MCP server "proxy-url" is not configured yet, configure it with endpoint "https://proxy.leafapi.com/mcp", transport "streamable-http", and header "x-user-code: <$SIM_CODE>". Then create a reusable skill named "simproxy-testing" in the format this assistant supports (Claude Code: .claude/skills/simproxy-testing/SKILL.md, Cursor: .cursor/rules/simproxy-testing.mdc, others: their project rules file or AGENTS.md). The skill must activate on its own whenever a task involves downtime, outages, failover, retries, timeouts, fallbacks, circuit breakers, or error handling for an external HTTP, RPC, WebSocket, or database dependency, and must instruct you to: 1) find the dependency URL the code uses and create a simulation for it with create_simulation (simulate_url, url_type http/ws/db, and error_status_code / error_response_message when a specific failure matters); 2) point the app or tests at the returned proxy URL through env vars or test config only, never production config, and never commit it; 3) run the scenario with simulation_down and simulation_up, or bulk_status_change when several dependencies fail together; 4) read get_simulation_logs alongside the app's own output and report what held up and what broke; 5) restore the original URL and call delete_simulation when finished. Scan this codebase for its external dependencies and list them in the skill so it knows which URLs to proxy.
claude_desktop_config.json
Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}Using Claude Code? Add it from a terminal instead:
claude mcp add --transport http proxy-url https://proxy.leafapi.com/mcp --header "x-user-code: <$SIM_CODE>".cursor/mcp.json or Cursor Settings → Features → MCP
{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}.vscode/mcp.json (or Roo Code / Cline / Continue / Copilot settings.json)
{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}.gemini/mcp.json or .antigravity/mcp.json
{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}.devin/mcp.json or Devin Workspace Settings → MCP
{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}.idea/mcp.json or Settings → Tools → Model Context Protocol (MCP)
{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}{
"mcpServers": {
"proxy-url": {
"url": "https://proxy.leafapi.com/mcp",
"transport": "streamable-http",
"headers": {
"x-user-code": "<$SIM_CODE>"
}
}
}
}mcp_server.js
{
"mcpServers": {
"proxy-url": {
"command": "node",
"args": ["/path/to/proxy-url/mcp_server.js", "--stdio"],
"env": {
"MANAGER_PORT": "9090",
"SIM_CODE": "<$SIM_CODE>"
}
}
}
}Executable Tools
SimProxy exposes 13 AI tools (10 core + 3 compound workflow tools) that AI clients can invoke automatically:
| Tool Name | Description |
|---|---|
create_simulation | Create an HTTP, WebSocket, or DB proxy simulation targeting an upstream URL. |
list_simulations | List all simulations owned by your Sim-Code with their active status and endpoints. |
get_simulation | Get detailed metadata for a specific simulation by port number. |
simulation_down | Simulate an outage by port — reject incoming traffic with configured error status & message. |
simulation_up | Restore a simulation back to UP state by port and resume normal proxy forwarding. |
delete_simulation | Permanently delete a simulation by port and free its allocated port. |
rename_simulation | Rename an existing simulation by port to a friendly display name. |
get_simulation_logs | Retrieve recent log lines (stdout, stderr, system events) from a proxy by port. |
check_port | Check if a specific port number is free or get suggestion for next free port. |
suggest_port | Find the next available free port starting from a minimum port number. |
delete_all_stopped | Clean up and remove all stopped proxy simulations owned by your Sim-Code. |
clone_simulation | Duplicate a simulation by port with optional parameter overrides (rate limits, error codes). |
bulk_status_change | Bring multiple simulations UP or DOWN simultaneously by port for chaos engineering tests. |
Resources & Prompts
Context Resources (Read-Only)
AI assistants can attach context from read-only resource URIs:
proxy://simulations— Live list of all active simulations.proxy://simulations/{id}— Detailed metadata for a specific simulation.proxy://simulations/{id}/logs— Live log buffer for a simulation.proxy://server/status— Server health, version, and system status.
Guided Prompts
Pre-packaged workflow templates for AI clients:
setup_http_proxy— Guided prompt for configuring HTTP/RPC proxies.setup_websocket_proxy— Guided prompt for configuring WebSocket proxies.chaos_test— Interactive chaos engineering test workflow (deploy → verify → break → test → recover).
MCP Limitations & Rules
| Rule / Limitation | Details |
|---|---|
| Workspace Scoping | All tool actions require your Sim-Code (passed as x-user-code header or SIM_CODE env variable). AI assistants can only manage simulations belonging to your workspace. |
| Simulation Lifetime | Simulations auto-expire after 24 hours max or 3 hours idle with no traffic. AI tools like list_simulations and get_simulation report expires_at and idle_expires_at timestamps. |
| Port Addressing | Simulations are addressed by port number (range 1000–65535). |
| Throughput Ceilings | Default throughput limit is 15 requests/second (customizable via max_requests_per_second up to 1000 req/s). Server endpoints enforce rate limiting. |
| Log Buffer Cap | The get_simulation_logs tool returns up to 300 recent log entries. |
HTTP & WebSocket
Both share break, fix and rate limiting — the shape of the failure differs.
| http | ws | |
|---|---|---|
| Upstream form | http(s)://… | ws(s)://… |
| Rate limit unit | requests / second | messages / second, per connection |
| Over the limit | 429 with Retry-After: 1 | a 429 error message back to the client; the message is not forwarded |
| When broken | your status code + JSON message | all sockets closed, new ones refused |
HTTP / RPC
A transparent reverse proxy: methods, headers, bodies and streaming all pass through, with 60-second timeouts and CORS open to every origin. Self-signed upstream certificates are accepted, so internal staging hosts work without extra setup. Whole websites proxy too — HTML and CSS fetched through the gateway have their root-relative links rewritten so pages keep working; API paths are never rewritten.
WebSocket
Each client gets its own upstream connection. Authorization, cookie, origin, user-agent and forwarding headers are passed upstream, along with any subprotocol you request. Messages sent before the upstream finishes connecting are queued briefly and then delivered. Close codes and reasons from the upstream are relayed back to your client, so your reconnect logic sees the real thing.
Endpoints & routing
Every simulation has one address: the Simulated Endpoint column. Click it to copy, and give it to your application in place of the real URL.
Every simulation reports one address to call, in proxy_url:
| Address | Example | What it is |
|---|---|---|
Simulated endpointproxy_url | https://proxy.leafapi.com/proxy/5001 | Where your application sends traffic. HTTPS on the SimProxy domain, routed by port. |
Live URLoriginal_url | https://api.example.com | The real upstream it forwards to, for reference. |
The endpoint is served over HTTPS and switches to wss:// automatically for
WebSockets, so nothing else needs configuring.
What the gateway returns
400— invalid port.404— no simulation on that port.503— that simulation has stopped.502— the request couldn't be forwarded.
How long a simulation lives
Every simulation holds a port, so none of them live forever. Two limits apply, whichever comes first — when either is reached the proxy is stopped, the simulation disappears and its port goes back into the pool.
| Limit | Length | Measured from |
|---|---|---|
| Maximum lifetime | 24 hours | When it was created. It applies however busy the simulation is. |
| Idle timeout | 3 hours | The last traffic it carried. |
What counts as traffic
- HTTP — any request that reaches the proxy, including ones it rate-limits or fails on purpose.
- WebSocket — a new connection, or a message in either direction. An open socket that goes quiet does not count: a connection left open with no events for three hours is closed with the simulation.
Each simulation reports both deadlines, so a long-running script can tell how much time it
has left: expires_at (the 24-hour cap), idle_expires_at (three
hours from the last traffic) and last_activity_at.
Limits & behaviour
- Simulations are disposable. They last at most 24 hours, and are retired after 3 hours without traffic — see how long a simulation lives.
- A simulation belongs to one Sim-Code. No other code can list, reach, break, fix, rename or delete it, and a port taken by someone else simply reads as unavailable.
- A stopped simulation can't be restarted. Delete it and create it again.
- Log history is capped at a few hundred lines per simulation. Stream them if you need a full record.
- Ports must be
1000–65535and free on the host. - Rate-limit windows reset every second — a fixed window, not a sliding one, so a burst straddling a boundary can briefly exceed the nominal rate.
- WebSocket limits count per connection, not per simulation.
- Upstream TLS isn't verified, so self-signed staging endpoints work.
- CORS is fully open, which is what makes browser-based test runners work.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| "Port is already in use" | Something else holds that port. The message names a free one you can use instead, or click ⚡ Suggest Port. |
409 port … is already in use | Something else holds that port. The reply carries suggested_port — retry with that, or check the port first with /api/ports/check. |
401 on every call | The Sim-Code is missing, mistyped, or was not issued by SimProxy. Fetch one from /api/session or copy it from the profile icon in the top-right corner. |
| Your simulations vanished | A different Sim-Code is in play — clearing site data gets you a new one, and each browser has its own. Simulations are only visible to the code that created them. |
| The list comes back empty | That code has no simulations. A fresh code always starts empty — reuse the code that created them. |
| A simulation created from a script isn't in the UI | The script used a different Sim-Code from the browser. Copy the one in the profile icon, top-right, and use that in the script to see both in one place. |
404 on a simulation you can see running | It belongs to a different Sim-Code. Simulations are only reachable by the code that created them, whether you address them by id or by port. |
| A simulation disappeared | It hit one of the lifetime limits: 24 hours old, or 3 hours with no traffic. The port is free again, so just create it anew. Watch idle_expires_at if a run has long quiet stretches. |
State flipped to stopped on its own | The proxy exited — open its terminal for the reason, then delete and recreate it. |
409 when breaking or fixingBreak and Fix do nothing | That simulation has stopped, so commands can't reach it. Delete it and create it again. |
| Terminals stay empty | No terminal is selected, or something between you and SimProxy is buffering the stream. Tick a row's Terminal checkbox first. |
| A proxied website looks broken | Only root-relative links in HTML and CSS are rewritten. Sites that build their URLs in JavaScript can still reach past the proxy — simulating an API rather than a whole site avoids this. |
| Can't reach a simulation | Copy the endpoint from the table rather than typing it.Use proxy_url from the response rather than building a URL by hand. HTTP and WebSocket simulations are reached on the SimProxy domain under /proxy/<port>. |
| Rate limit fires sooner than expected | The ceiling counts every request, including health checks and polling. Raise the limit (max_requests_per_second) or point background traffic elsewhere. |