Skip to main content
Glama
bachden

bruno-headless

by bachden

bruno-headless

Serves a single Bruno workspace over MCP, on machines with no UI.

Full CRUD over collections, folders, requests, environments and dotenv files, plus request execution with Bruno's scripting, tests and assertions — over HTTP, GraphQL, gRPC and WebSocket. No Electron, no display, no keychain.

BRUNO_WORKSPACE=/srv/api-workspace \
BRUNO_MCP_TOKEN=$(openssl rand -hex 32) \
npm start

How it is put together

Three layers, and only the middle one is vendored:

The request engine is bruno-cli's, vendored verbatim into src/bruno/runtime/ (18 files). It is already headless — nothing in it imports electron, an Electron store, or the OS keychain — and it brings pre-request and post-response scripts, tests, assertions, variable updates and the whole interpolation pipeline with it. Nothing under src/bruno is ever hand-edited.

gRPC and WebSocket are driven directly from @usebruno/requests, which exports the same GrpcClient and WsClient the desktop app uses. The desktop's ~1000 lines around them are IPC plumbing that streams events to a renderer; src/protocols/ replaces that with adapters that collect the event stream into a single result, which is what a tool call needs. Nothing extra is vendored for this.

CRUD is this project's own code (src/collections.js), written against @usebruno/filestore's parsers and serializers — the same ones Bruno uses. This module decides which file to touch and how to merge a change; it never formats .bru or .yml by hand, so files stay byte-compatible with the desktop app and the CLI.

To re-sync the engine after upstream changes:

node scripts/sync-bruno.js /path/to/bruno
npm install
npm test

The script recomputes the require closure itself, so new upstream dependencies are picked up automatically. src/bruno/VENDOR.json records the source commit and file list.

vendor/@usebruno/ holds the workspace packages, which must be vendored because their monorepo versions do not match the registry (@usebruno/filestore is 0.1.0 here and 0.11.0 on npm). Source maps and esm builds are stripped — bruno-headless is CommonJS throughout — which takes that tree from 39 MB to under 7 MB.

Related MCP server: Bruno MCP Server

Configuration

All configuration is environment variables. See .env.example.

Variable

Default

Meaning

BRUNO_WORKSPACE

Required. Workspace directory to serve.

BRUNO_MCP_HOST

127.0.0.1

Bind address.

BRUNO_MCP_PORT

3847

Bind port.

BRUNO_MCP_TOKEN

generated

Bearer token. Generated and printed at startup if unset.

BRUNO_GLOBAL_ENVIRONMENT

Workspace global environment to treat as active, by name.

BRUNO_WORKSPACE_NAME

from workspace.yml

Display name override.

BRUNO_SANDBOX

safe

safe runs scripts in QuickJS, developer in a Node VM with full access.

BRUNO_REQUEST_TIMEOUT_MS

120000

Per-request execution timeout.

BRUNO_MAX_REQUEST_FILES

20000

Cap on files scanned when listing requests.

BRUNO_SSL_VERIFY

true

TLS certificate verification.

BRUNO_CA_CERT

Path to an additional CA certificate.

BRUNO_IGNORE_TRUSTSTORE

false

Ignore the system truststore when a custom CA is set.

BRUNO_STORE_COOKIES

true

Keep a cookie jar for the process lifetime.

BRUNO_USE_PROXY

true

Honour proxy configuration.

BRUNO_LOG_LEVEL

info

error, warn, info, debug.

Nothing is persisted outside the workspace: OAuth2 tokens and cookies live in memory for the life of the process, so there is no data directory to provision.

A workspace.yml is optional — collections are discovered by scanning for bruno.json and opencollection.yml, so a plain directory of collections works and is named after its folder.

Endpoints

  • POST /mcp — streamable-HTTP MCP, requires Authorization: Bearer <token>

  • GET /healthz — unauthenticated liveness, reports the served workspace

Requests are handled statelessly (no session id), so clients can reconnect freely and the service can sit behind a load balancer without sticky sessions.

{
  "mcpServers": {
    "bruno": {
      "type": "http",
      "url": "http://127.0.0.1:3847/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

Tools

38 tools. The workspace is fixed at startup, so no tool takes a workspace_path or workspace_uid — every call targets BRUNO_WORKSPACE implicitly.

Area

Tools

Service

bruno_status, bruno_list_workspaces

Collections

list, get, create, update, update_tab, clone, move, delete, list_collection_items, resequence_items

Folders

get_folder, create_folder, update_folder, update_folder_tab, delete_folder, move_item

Requests

list_requests, search_requests, get_request, create_request, update_request, update_request_tab, duplicate_request, delete_request

Environments

list_environments, get_environment, create_environment, update_environment, delete_environment

Dotenv

get_dotenv, set_dotenv, delete_dotenv

Execution

prepare_request, run_request, get_request_run, list_request_runs

bruno_prepare_request resolves a request fully and returns it without sending it, along with any variables that stayed unresolved — the cheapest way for an agent to check its work before firing a request at a real API.

WebSocket runs

A WebSocket stays open indefinitely, but a tool call has to return, so bruno_run_request takes a stopping rule for ws requests. It returns as soon as any of these is true:

Parameter

Default

Stops when

the server closes the connection, or errors

idle_timeout_ms

2000

no frame has arrived for this long

max_messages

100

this many frames have arrived

duration_ms

30000

this much total time has elapsed

The result reports terminationReason so the caller knows which rule fired. On connect, every message in the request that has content is sent — matching Bruno's own behaviour.

Variable resolution

The engine is Bruno's, so precedence is Bruno's. Layers, from lowest:

  1. Workspace global environment (<workspace>/environments/*, selected by BRUNO_GLOBAL_ENVIRONMENT)

  2. Collection variables (collection.bru)

  3. Folder variables (folder.bru)

  4. Request variables

  5. Selected collection environment (environment_name / environment_uid per call)

  6. .env files — collection and workspace level — reached as {{process.env.NAME}}

  7. Process environment

  8. runtime_variables passed to the call

One deliberate addition over bruno-cli: it reads only a collection's .env, because it runs against a bare collection path. Here a workspace can also hold a shared .env one level up, and collection values win on conflict.

Security

  • Bearer token on every MCP call, compared in constant time. Generated at startup if BRUNO_MCP_TOKEN is unset — convenient for a first run, but it changes on restart, so set it explicitly for anything long-lived.

  • The workspace is a hard boundary. collection_path, location and target_location are resolved against the workspace root and rejected if they escape it, whether written relative or absolute.

  • Scripts run in QuickJS by default. BRUNO_SANDBOX=developer gives collection scripts a Node VM with full access to the host — only set it for collections you trust.

  • Binding to 0.0.0.0 logs a warning. The default is loopback. Requests execute with the service's network access, so treat the port as privileged and firewall it or front it with a proxy that terminates TLS.

  • Tool arguments are redacted by key name before reaching debug logs, since dotenv contents and environment values flow through them.

Logging

One JSON object per line on stdout, errors on stderr — what container collectors and journald expect.

The vendored engine is a CLI underneath and prints a coloured progress line per request straight to stdout, which would corrupt that stream. Rather than patch a vendored file, start() redirects the global console into the logger: engine chatter becomes debug (tagged "source":"engine"), its warnings and errors keep their level. Embedders who need the console back get a restoreConsole() from start().

Known limitations

  • Interactive OAuth2 does not work. Authorization-code grants need a browser. The non-interactive grants (client_credentials, password) work, as does supplying a token directly. Everything else — basic, bearer, API key, AWS SigV4, digest, NTLM — is unaffected.

  • Environment secrets are not encrypted. Desktop Bruno pushes variables marked secret into the OS keychain, which has no headless equivalent; here they stay in the environment file as written. Keep anything that must not sit in the collection in .env files or the process environment. A workspace carrying secrets written by desktop Bruno will not resolve them — re-enter those values.

  • OAuth2 tokens and cookies do not survive a restart. They are in-memory only.

  • Single workspace per process. By design. Run one instance per workspace.

Docker

docker build -t bruno-headless .

docker run -d --name bruno-headless \
  -p 127.0.0.1:3847:3847 \
  -v /srv/api-workspace:/workspace \
  -e BRUNO_MCP_TOKEN=$(openssl rand -hex 32) \
  bruno-headless

Runs as a non-root user, uses tini so SIGTERM reaches the graceful shutdown, and ships a healthcheck against /healthz. Mount the workspace read-only to expose a strictly read-and-execute service.

systemd

[Unit]
Description=bruno-headless
After=network-online.target

[Service]
Type=simple
User=bruno
WorkingDirectory=/opt/bruno-headless
EnvironmentFile=/etc/bruno-headless.env
ExecStart=/usr/bin/node bin/bruno-headless.js
Restart=on-failure
RestartSec=5

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/api-workspace

[Install]
WantedBy=multi-user.target

Tests

npm test

test/smoke.js starts the real service on a real socket and drives it with the real MCP client, against live HTTP, WebSocket and gRPC servers: transport and auth, the tool surface, CRUD on the filesystem, every layer of variable precedence, scripts/assertions/ tests, all four protocols, and the workspace boundary.

Layout

bin/bruno-headless.js     entry point
src/index.js              startup sequence
src/config.js             environment configuration
src/collections.js        CRUD over collections, folders, requests, environments, dotenv
src/execute.js            resolution and execution, dispatched by protocol
src/environment.js        the variable layers a request resolves against
src/protocols/grpc.js     gRPC adapter over @usebruno/requests
src/protocols/ws.js       WebSocket adapter, with the run termination policy
src/workspace.js          the single workspace
src/mcp/server.js         streamable-HTTP MCP server
src/mcp/tools.js          tool definitions and the workspace boundary
src/bruno/runtime/        bruno-cli's engine, vendored, never edited
vendor/@usebruno/         vendored workspace packages
scripts/sync-bruno.js     re-vendor from a Bruno checkout

Licence

MIT — see LICENSE, and NOTICE for third-party attribution.

This project redistributes code from Bruno, also MIT licensed, Copyright (c) 2022 Anoop M D, Anusree P S and Contributors. The vendored trees (src/bruno/runtime/ and vendor/@usebruno/) each carry a copy of Bruno's licence, and scripts/sync-bruno.js refuses to vendor without one. bruno-headless is an independent project, not affiliated with or endorsed by Bruno.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    F
    maintenance
    A Model Context Protocol (MCP) server that enables programmatic creation and management of Bruno API testing collections, environments, and requests through standardized MCP tools.
    Last updated
    1
    75
    30
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A lightweight, local-first MCP server for executing HTTP requests and managing API collections and environments without cloud dependencies. It enables testing APIs, handling authentication, and importing OpenAPI specifications directly within MCP-compatible workflows.
    Last updated
    42
    202
    4
    MIT

View all related MCP servers

Related MCP Connectors

  • MCP (Model Context Protocol) server for Appwrite

  • MCP server for interacting with the Supabase platform

  • The official MCP Server from Mia-Platform to interact with Mia-Platform Console

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bachden/bruno-headless'

If you have feedback or need assistance with the MCP directory API, please join our Discord server