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):

Updates:

  1. 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.
  2. 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.
  3. 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.
  4. 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

Variables

{{variable}} syntax in URL, params, headers, auth and body. Precedence (highest wins):

runtime (scripts) β†’ data file row β†’ collection β†’ environment β†’ globals

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, and JSON.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:

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:

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:

  1. 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.
  2. Response β€” ✨ Explain button: analyzes the request + response + failed tests and explains what happened, the likely cause of the error and how to fix it.
  3. 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:

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.

How to set it up

  1. Open MCP Server β†’ check Enable local server ("● running" appears).
  2. Copy the MCP endpoint (http://127.0.0.1:<port>/mcp) and the Bearer token.
  3. 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:

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