bruno-headless
Allows management and execution of Bruno API workspaces, providing CRUD operations on collections, folders, requests, environments, and dotenv files, as well as running requests with Bruno's scripting and testing capabilities over HTTP, GraphQL, gRPC, and WebSocket.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@bruno-headlessRun the login request in the auth collection"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 startHow 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 testThe 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 |
| — | Required. Workspace directory to serve. |
|
| Bind address. |
|
| Bind port. |
| generated | Bearer token. Generated and printed at startup if unset. |
| — | Workspace global environment to treat as active, by name. |
| from | Display name override. |
|
|
|
|
| Per-request execution timeout. |
|
| Cap on files scanned when listing requests. |
|
| TLS certificate verification. |
| — | Path to an additional CA certificate. |
|
| Ignore the system truststore when a custom CA is set. |
|
| Keep a cookie jar for the process lifetime. |
|
| Honour proxy configuration. |
|
|
|
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, requiresAuthorization: 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 |
|
Collections |
|
Folders |
|
Requests |
|
Environments |
|
Dotenv |
|
Execution |
|
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 |
|
| no frame has arrived for this long |
|
| this many frames have arrived |
|
| 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:
Workspace global environment (
<workspace>/environments/*, selected byBRUNO_GLOBAL_ENVIRONMENT)Collection variables (
collection.bru)Folder variables (
folder.bru)Request variables
Selected collection environment (
environment_name/environment_uidper call).envfiles — collection and workspace level — reached as{{process.env.NAME}}Process environment
runtime_variablespassed 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_TOKENis 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,locationandtarget_locationare resolved against the workspace root and rejected if they escape it, whether written relative or absolute.Scripts run in QuickJS by default.
BRUNO_SANDBOX=developergives collection scripts a Node VM with full access to the host — only set it for collections you trust.Binding to
0.0.0.0logs 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
secretinto 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.envfiles 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-headlessRuns 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.targetTests
npm testtest/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 checkoutLicence
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.
This server cannot be installed
Maintenance
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
- AlicenseBqualityDmaintenanceMCP Server for running Bruno CollectionsLast updated17544MIT
- AlicenseBqualityFmaintenanceA Model Context Protocol (MCP) server that enables programmatic creation and management of Bruno API testing collections, environments, and requests through standardized MCP tools.Last updated17530MIT
- AlicenseAqualityDmaintenanceA 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 updated422024MIT
- Alicense-qualityCmaintenanceMCP server that executes requests from Bruno API collections via the Bruno CLI tool, enabling API request execution and collection management.Last updated4MIT
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
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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