bruno-headless

bruno-headless

A headless MCP server for Bruno workspaces, enabling CRUD operations on collections, folders, requests, environments, and dotenv files, plus request execution with scripting, tests, and assertions over HTTP, GraphQL, gRPC, and WebSocket.

Category
访问服务器

README

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.

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选