RestRuno
Desktop REST client (Windows & macOS) β Postman/Bruno style, with 100% local collections, variables, scripts, a data-driven runner and CSV generation.
Free and Pro
RestRuno is free for everyday work: collections, requests, environments, variables, scripts with tests, Postman/Bruno import/export, cookies and console. Two advanced features are part of RestRuno Pro (one-time payment, license for 2 devices):
| Free | Pro | |
|---|---|---|
| Collections, requests, environments, variables | β | β |
| Pre/post scripts with tests | β | β |
| Postman & Bruno import/export | β | β |
| β¨ AI assistant | β | β |
| βΆ Data-driven runner (CSV/JSON, reports, CSV output) | β | β |
| π Local MCP server + API (control from AI) | β | β |
Buy RestRuno Pro β $20 β one-time payment, updates included, 2 devices. The purchase is processed by Polar and you receive your license key by email; activate it in the app with the Upgrade button in the top bar. Terms of use in the license (EULA).
For your team? RestRuno Enterprise β $149: one-time payment, 25 seats (a single key for the whole team, 50 activations), simple rollout and priority email support.
Installation and updates
Install (the buttons on restruno.com detect your system, or download from the latest release):
- Windows:
restruno-X.Y.Z-setup.exeβ one-click installer (Windows will show "unknown publisher" since the app is unsigned: More info β Run anyway). - macOS:
restruno-X.Y.Z-arm64.dmgfor Apple Silicon (M-series chips) orrestruno-X.Y.Z-x64.dmgfor Intel Macs β open the dmg and drag RestRuno to Applications.
Updates:
- Automatic and silent: shortly after you open the app (once it has finished loading, so startup is never slowed down) it checks this repository; if there is a new version it is fully downloaded in the background.
- When the download is ready, an orange Update button appears in the bottom-right corner: click it β it installs and the app restarts. If you never click it, the update installs itself when the app closes.
- Check manually: click the version number (
vX.Y.Z) in the bottom-right corner. It tells you right away if there is a new version (and starts the background download), if you are up to date, or the error if there is no connection. - The Docs button (top bar) opens this documentation in your browser.
Collections
A collection is a folder on your disk that you choose. Every request is a readable *.rr.json file; subfolders are folders. You can version them with Git or back them up by copying the folder.
| Action | How |
|---|---|
| Create a collection | New button β name β choose the parent folder |
| Open an existing collection | Open button β select the collection folder |
| Import from Postman/Bruno | Import button β choose the exported JSON (Postman v2.x or Bruno) |
| Export | Right-click the collection β Export Collection (generates Postman v2.1 + one file per environment) |
| New request / folder | Right-click the collection or folder β New Request / New Folder |
| Duplicate | Right-click β Duplicate (requests, folders or the whole collection) |
| Rename / Delete | Right-click β Rename / Delete |
| Move | Drag & drop requests or folders (with everything inside) to another folder or another collection |
| Expand / collapse all | β/β button next to Import |
| Hide the sidebar | β§ button at the top left or Ctrl+B |
Requests
- URL and Params in sync: add params in the Params tab and they appear in the URL (and the other way around β type
?a=1&b=2in the URL and the table fills in). - Auth: None, Bearer Token or Basic (accepts
{{variables}}). - Body: JSON, XML (SOAP), GraphQL (query + JSON variables), plain text, form-url-encoded or multipart form (text fields and files). Visible Content-Type selector, Beautify button for JSON/XML, and the exported cURL uses
-Ffor multipart. - Send: Send button or Ctrl+Enter Β· Save: Save or Ctrl+S (the orange dot on the tab means unsaved changes).
- Copy as cURL: cURL button β copies the command with every variable resolved to its current value.
- Paste a cURL: paste any
curlcommand into the URL field and the method, URL, params, headers, auth and body are configured automatically. - Response: status, time, size, date/time and final URL (both hideable with the β± and URL buttons), formatted body, headers, test results and console. The panel can sit below or to the side (β¬/β¨ button in the top bar).
Variables
{{variable}} syntax in URL, params, headers, auth and body. Precedence (highest wins):
runtime (scripts) β data file row β collection β environment β globals
- Globals: Globals button (apply to every collection).
- Environment: selector at the top right; managed in Manage Environments (per collection). From there you can also copy variables to another collection (selected ones or all).
- Collection: right-click the collection β Collection Variables.
Scripts (Scripts tab of each request)
Two panels: Pre Request (before sending) and Post Response (validations). Each panel has a Snippets⦠menu with ready-made examples and a ⢠button to maximize it.
// Pre Request
rr.variables.set('token', 'abc'); // runtime: memory only, flows between requests
rr.variables.setGlobal('key', 'value'); // saved to globals (globals.json)
rr.variables.setEnvironment('token', 'abc'); // saved to the ACTIVE ENVIRONMENT (environments/<name>.json)
rr.request.headers['X-Trace'] = '1'; // modify the outgoing request
// Post Response (tests)
rr.test('status is 200', () => rr.expect(rr.response.status).toBe(200));
rr.test('has id', () => rr.expect(rr.response.body.id).toBeDefined());
rr.variables.set('token', rr.response.body.token); // chain login β next request
rr command reference
Scripts are synchronous JavaScript in a sandbox. There is no require/import/fetch/process/setTimeout, no async/await, and no network access. 5-second timeout.
Variables (work in pre and post):
| Command | What it does | Persistence |
|---|---|---|
rr.variables.get(key) |
Reads a variable resolving the precedence: runtime β data row β collection β environment β global | β |
rr.variables.set(key, value) |
Saves to the runtime scope (memory); flows to the next requests of the session/run, resets between iterations | Not saved to disk |
rr.variables.setEnvironment(key, value) |
Saves to the active environment | environments/<name>.json (visible in the editor) |
rr.variables.setGlobal(key, value) |
Saves a global variable | globals.json (Globals dialog) |
Values are stored as text β use
JSON.stringify(obj)if you need to store an object, andJSON.parse(...)when reading it.
Pre Request β modify the request before sending it (pre script only):
| Command | Description |
|---|---|
rr.request.method |
Method (string), assignable |
rr.request.url |
URL (string, accepts {{vars}}) |
rr.request.headers['Name'] = value |
Adds/replaces a header |
rr.request.params['key'] = value |
Adds/replaces a query param |
Post Response β read the response (post script only):
| Command | Description |
|---|---|
rr.response.status |
HTTP code (number), e.g. 200 |
rr.response.statusText |
Status text |
rr.response.headers['content-type'] |
Headers (keys in lowercase) |
rr.response.body |
Body as parsed JSON (or undefined if not JSON) |
rr.response.bodyText |
Body as raw text |
rr.response.timeMs |
Elapsed time (number) |
rr.response.sizeBytes |
Response size (number) |
Tests (post) β every rr.test is reported as pass/fail in the runner and the response panel:
rr.test('name', () => { ... })rr.expect(actual)with matchers:.toBe(x)(strict ===),.toEqual(x)(deep comparison),.toContain(x)(array item or substring),.toBeDefined(),.toBeLessThan(n),.toBeGreaterThan(n),.toMatch(regexOrStr), and the negation prefix.not(e.g.rr.expect(s).not.toBe(500)).
console.log/info/warn/error(...) is captured and shows up in the global console.
Full example (post-response of a login that stores the token persistently):
rr.test('login OK', () => rr.expect(rr.response.status).toBe(200));
rr.test('returns token', () => rr.expect(rr.response.body.access_token).toBeDefined());
rr.test('content-type json', () =>
rr.expect(rr.response.headers['content-type']).toMatch(/application\/json/));
// store the token in the active environment for the next requests:
rr.variables.setEnvironment('token', rr.response.body.access_token);
console.log('token stored:', rr.response.body.access_token);
The Snippets⦠menu of each panel inserts many of these patterns ready to use. If you have the AI assistant enabled, the ⨠button generates scripts from a description using this same reference.
Runner (Pro)
RestRuno Pro feature since version 3.0.
Right-click a collection or folder β Run Collection / Run Folder. It opens as a full-screen tab:
- Two views (runner's β¬/β¨ button): stacked, or split with the configuration on the left and the executions on the right. While the run is in progress, the configuration hides to give the results all the space.
- Request selection: checkboxes to run only a subset. The selection is preserved between runs of the same scope β run, adjust and repeat without re-checking.
- Data file (CSV / JSON / TXT): repeats the sequence once per row; every column becomes a
{{variable}}(TXT uses thevaluevariable). - Iterations: field next to Run β how many iterations to execute. The maximum is the number of rows in the data file (without a file, always 1).
- Results with full detail: expand any execution to see its tests, the console, and the Request (method, resolved URL, headers, sent body) and Response (status, headers, body) blocks. Very large bodies are trimmed on screen; with Persist you have the full copy.
- Persist executions: checkbox that saves the resolved request + full response of every execution under
runs/inside the collection. - Generate CSV (collapsible section βΈ): enable Generate CSV file from response values, choose folder and name, and define columns with expressions over the response:
body.data[0].id,status,headers.content-type,timeMs,request.url,iteration. Each column can betext(value as is) ornumber(strips leading zeros:00742β742). The Save always / Only passed / Only failed selector controls which executions write a row β and in every case, the row is only written if at least one expression matches. - Failures do not stop the run; you can cancel at any time.
AI assistant (Pro)
RestRuno Pro feature since version 3.0. Configuration is free; running AI actions requires a license.
β¨ AI button in the top bar. Connect any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, OpenRouter, Groq, etc. It is completely optional β if you don't configure it, RestRuno works exactly the same and no AI button appears.
π‘ Works with open source models β free and 100% on your machine. Install Ollama or LM Studio and use Llama, Mistral, DeepSeek, Qwen, Gemma, or any other open model running locally: no API fees, no subscriptions, and your requests never leave your computer. You can also use hosted open source models (OpenRouter, Groq) if you'd rather not run them yourself.
Configuration (stored only on your computer):
| Field | Examples |
|---|---|
| Base URL | https://api.openai.com/v1 Β· http://localhost:11434/v1 (Ollama) Β· https://openrouter.ai/api/v1 |
| API Key | your provider key (empty for local models) |
| Model | gpt-4o-mini Β· llama3.1 Β· claude-3-5-sonnet β¦ |
Check Enable AI assistant, use Test Connection to verify, and save. The β¨ AI button in the top bar shows a green dot when it is enabled and the last connection test succeeded (gray if disabled or unverified). When enabled, β¨ buttons appear in three places:
- Scripts (pre and post) β β¨ in each panel's header: describe what you need ("validate that the status is 201 and store the id in a variable") and it generates the script using the correct
rr.*API, with your request's context. The Insert into script button adds it to the editor. - Response β β¨ Explain button: analyzes the request + response + failed tests and explains what happened, the likely cause of the error and how to fix it.
- Runner CSV columns β β¨ AI expression: describe the value you want to extract ("the id of the first item") and it generates the expression (
body.items[0].id) using the last response of the run as context; Add as column adds it directly.
In every action you can edit the instruction, regenerate, copy or insert the result. Privacy: when you use these actions, the request/response involved is sent to your configured AI provider β with Ollama/LM Studio everything stays on your machine.
Usage examples:
- Scripts: "validate that the status is 201, that the body has a numeric
id, and storeaccess_tokenin the active environment" β generates the post-response withrr.test,rr.expectandrr.variables.setEnvironmentready to insert. - Explain: facing an unexpected 500, β¨ Explain tells you what went wrong (missing header, malformed body, server error) and what to fix.
- Runner CSV: "the email of the second user in the list" β generates
body.users[1].emailand adds it as a column.
Global console
Console bar at the bottom of the window: logs every sent request, every runner execution and the console.log calls from your scripts. All / Requests / Errors / Logs filters, hideable date and time, and a Clear button.
Local MCP server + API (Pro)
RestRuno Pro feature since version 3.0.
MCP Server button in the top bar. Lets an AI (Claude, GitHub Copilot, Cursor, Antigravity, OpenAI Codex, etc.) control RestRuno: create, edit, delete and send requests, and read the response, the console and the errors β all local, no cloud.
- 100% local: listens only on
127.0.0.1(never reachable from outside your machine). Disabled by default. - Status dot: the MCP Server button shows a green dot when the server is on and gray when it is off.
- Bearer token: every external request must present the token shown in the window (you can regenerate it). Copy it with one click.
- The server only acts on the collections you have open in the app; it cannot read or write files outside them.
How to set it up
- Open MCP Server β check Enable local server ("β running" appears).
- Copy the MCP endpoint (
http://127.0.0.1:<port>/mcp) and the Bearer token. - In Connect your AI β pick your client, choose your client and copy the ready-to-paste snippet:
| Client | Where the configuration goes |
|---|---|
| Claude Code | One terminal command: claude mcp add --transport http restruno <url> --header "Authorization: Bearer <token>" |
| Claude Desktop | Settings β Developer β Edit Config β claude_desktop_config.json (uses the mcp-remote bridge); restart Claude |
| VS Code / Copilot | The project's .vscode/mcp.json (Copilot agent mode) |
| OpenAI Codex | One terminal command: codex mcp add restruno -- npx -y mcp-remote <url> --header "Authorization: Bearer <token>" |
| Cursor / Antigravity / others | Generic mcpServers JSON β ~/.cursor/mcp.json in Cursor, MCP panel (mcp_config.json) in Antigravity; most MCP clients accept this same shape |
Tools exposed to the MCP client: list_collections, get_tree, read_request, create_request, update_request, delete_request, create_folder, send_request, get_console.
Usage examples (from your AI)
With the client connected, ask for things like:
- "List my RestRuno collections and show me the tree of the
paymentscollection." - "Create in
paymentsa POST request to{{baseUrl}}/refundswith this JSON body and send it; tell me the status and the response body." - "Send the
loginrequest, and from what you see in the console tell me why it is failing." - "Go through every request in the
smokefolder and flag the ones that don't return 200."
REST API (alternative without MCP)
Plain HTTP calls to http://127.0.0.1:<port>/api/* with Authorization: Bearer <token> β handy for scripts or local CI:
# list collections (returns name + rootPath for each one)
curl http://127.0.0.1:<port>/api/collections -H "Authorization: Bearer <token>"
# tree of a collection
curl "http://127.0.0.1:<port>/api/tree?rootPath=/path/to/payments" -H "Authorization: Bearer <token>"
# send an existing request (absolute path) with an environment, and read the response
curl -X POST http://127.0.0.1:<port>/api/send \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"requestPath": "/path/to/payments/login.rr.json", "environmentName": "dev"}'
# read the console (last 20 entries)
curl "http://127.0.0.1:<port>/api/console?limit=20" -H "Authorization: Bearer <token>"
Endpoints: GET /api/health Β· GET /api/collections Β· GET /api/tree?rootPath= Β· GET|POST|PATCH|DELETE /api/request Β· POST /api/folder Β· POST /api/send Β· GET /api/console?limit=.
Session cookies
Responses with Set-Cookie are remembered in memory only (never on disk) and replayed to requests to the same host β that's how login flows work. In Globals you can disable or clear them. No HTTP cache is stored.
Where is everything stored?
| What | Where |
|---|---|
| Requests, folders, collection variables | The collection folder (JSON files) |
| Environments | environments/ inside the collection |
| Persisted runner executions | runs/ inside the collection |
| Global variables and preferences | User profile (AppData) |
| Manual request responses | Memory only (lost on close) |
| Cookies | Memory only (lost on close) |
Shortcuts
Ctrl+Enter send Β· Ctrl+S save Β· Ctrl+W close tab Β· Ctrl+B show/hide sidebar