Skip to main content
Glama
README.md
# Bruno MCP

A local MCP v2 stdio server for enumerating and safely editing Bruno
workspaces, legacy `.bru` collections, and OpenCollection YAML collections.
It stores MCP-managed secret values in the operating-system keychain and
delegates execution to the official `bru` CLI.

## Prerequisites

- Node.js 20 or newer
- npm
- Bruno CLI for `run_*` tools (`bru` 4.0.0 is the validated version)
- A native OS credential store for secret tools:
  - macOS Keychain
  - Windows Credential Manager
  - a supported Linux Secret Service

Bruno Desktop is optional. Generated legacy `.bru` files use the conservative
format associated with Desktop 3.4.2, while execution is mechanically
validated with CLI 4.0.0. Desktop exposes no supported headless parser, so its
compatibility cannot be fully automated without opening the GUI or modifying a
real workspace. These versions do not share a major version: keep backups, use
revisions, and do not assume a CLI-4-only feature is understood by Desktop
3.4.2. OpenCollection YAML is validated against CLI 4.0.0; Desktop 3.4.2
compatibility is not guaranteed. Desktop refresh behavior is controlled by
Bruno, not this server.

## Install, build, test, and start

From a source checkout:

```sh
npm install
npm run typecheck
npm run build
npm test
npm start
```

Run the disposable compatibility test when Bruno CLI 4.0.0 is installed:

```sh
BRUNO_CLI_PATH=/opt/homebrew/bin/bru npm run test:compat
```

The smoke test uses only a generated collection under `.test-work`, an
in-memory secret store, and a loopback HTTP server. It never reads or changes
the user's Bruno workspace or collections.

After packaging/installing, the executable is:

```sh
bruno-mcp
```

The server uses stdio. Standard output is reserved for MCP protocol messages;
diagnostics go to standard error.

## Configuration

| Variable | Meaning | Default |
| --- | --- | --- |
| `BRUNO_MCP_WORKSPACE_PATHS` | Workspace YAML files to inspect/update, separated by the platform path-list delimiter | Bruno's platform default workspace |
| `BRUNO_MCP_ALLOWED_ROOTS` | Roots beneath which collections may be read or changed | Parents of registered collections |
| `BRUNO_CLI_PATH` | `bru` executable name or path | `bru` |
| `BRUNO_MCP_RUN_TIMEOUT_MS` | Default CLI timeout in milliseconds | `30000` |
| `BRUNO_MCP_RUN_OUTPUT_LIMIT_BYTES` | Maximum returned stdout/stderr bytes | `1048576` |
| `BRUNO_MCP_RUN_REPORT_LIMIT_BYTES` | Maximum JSON report bytes read | `10485760` |
| `BRUNO_MCP_SOURCE_FILE_LIMIT_BYTES` | Maximum bytes read from one Bruno or workspace source file | `8388608` |
| `BRUNO_MCP_COLLECTION_SNAPSHOT_LIMIT_BYTES` | Maximum aggregate bytes hashed for one collection revision | `67108864` |
| `BRUNO_MCP_COLLECTION_SNAPSHOT_FILE_LIMIT` | Maximum Bruno-managed files hashed for one collection revision | `10000` |

The legacy aliases `BRUNO_MCP_MAX_OUTPUT_BYTES` and
`BRUNO_MCP_MAX_REPORT_BYTES` are also accepted. Path lists use `:` on
macOS/Linux and `;` on Windows.

## MCP client configuration

Build first, then point the client at the built entry point:

```json
{
  "mcpServers": {
    "bruno": {
      "command": "node",
      "args": ["/absolute/path/to/bruno-mcp/dist/index.js"],
      "env": {
        "BRUNO_MCP_WORKSPACE_PATHS": "/Users/me/Library/Application Support/Bruno/default-workspace/workspace.yml",
        "BRUNO_MCP_ALLOWED_ROOTS": "/Users/me/projects/apis",
        "BRUNO_CLI_PATH": "/opt/homebrew/bin/bru"
      }
    }
  }
}
```

## Tool catalog

### Discovery

- `list_workspaces`
- `list_collections`
- `get_collection`
- `list_items`
- `get_request`
- `list_environments`
- `get_environment`

`list_workspaces`, `list_collections`, `list_items`, and
`list_environments` accept `limit` and an opaque `cursor`. Their paged
responses include `total`, `returned`, `truncated`, and `nextCursor`.
`list_workspaces` returns summaries (`path`, `name`, `revision`, `status`,
and `collectionCount`) rather than embedding registration arrays.
`list_collections` pages one flattened record per workspace registration;
each record includes workspace identity plus collection name, path, status,
and format when available. Its `collections.snapshotToken` identifies the
listing snapshot. Pass `nextCursor` unchanged, and restart traversal if a
workspace registration changes and a later page reports a revision conflict.
Collection item and environment pages similarly include
`collectionRevision`.

### Collections and workspaces

- `create_collection`
- `update_collection`
- `move_collection`
- `register_collection`
- `unregister_collection`
- `delete_collection`

### Folders, requests, and environments

- `create_folder`, `update_folder`, `move_folder`, `delete_folder`
- `create_request`, `update_request`, `move_request`, `delete_request`
- `create_environment`, `update_environment`, `move_environment`,
  `delete_environment`

### Variables and secrets

- `set_variables`
- `delete_variables`
- `set_secret`
- `rename_secret`
- `delete_secret`

### Execution

- `run_request`
- `run_folder`
- `run_collection`

Execution supports environments, non-secret runtime overrides, recursion,
include/exclude tags, bail, tests-only mode, safe/developer sandboxes, CA and
client certificates, proxy disabling, CSV/JSON data files, iterations, delay,
and timeout. Developer sandbox and insecure TLS require their explicit
matching opt-in flags.

## Security model

- Collection access is restricted to canonical allowed roots.
- Traversal, absolute item paths, reserved metadata names, mixed formats, and
  symlink escapes are rejected.
- Paths are re-resolved immediately before reads, writes, moves, runs, and
  trash operations.
- Mutations are serialized per collection and use atomic writes.
- Folder and collection directory moves require an atomic same-filesystem
  rename. Cross-filesystem moves are rejected with
  `CROSS_DEVICE_MOVE_UNSUPPORTED`; use an explicit copy/import workflow
  instead.
- Read results contain SHA-256 revisions. Mutation tools require the matching
  `expectedRevision`, preventing silent overwrites after Desktop edits.
- All `delete_*` tools require `confirm: true` and use the OS trash. There is
  no permanent-delete fallback.
- `bru` is spawned directly without a shell. Output and reports are bounded.
- Safe sandboxing and TLS verification are defaults.

## Secret behavior

`set_secret` writes the value to the native OS credential store. Bruno files
contain only the variable name and secret marker. `get_environment` returns
the secret name and `present` state, never its value.

For a run, the server builds an owner-only temporary environment file containing
the selected environment's regular variables and managed secrets, passes its
path to `bru`, and removes it in a `finally` path. Secret values are not placed
in argv, logs, or MCP responses. Exact values and common sensitive headers are
redacted from CLI output and parsed reports. Secret namespaces migrate with
environment/collection moves and are removed with confirmed environment or
collection deletion.

## Examples

Examples below show `tools/call` arguments; paths must be allowed.

List registered and discovered collections (pass the returned `nextCursor`
to continue):

```json
{ "name": "list_collections", "arguments": { "discover": true, "limit": 100 } }
```

Create a legacy collection:

```json
{
  "name": "create_collection",
  "arguments": {
    "root": "/Users/me/projects/apis/example",
    "name": "Example API",
    "format": "bru",
    "register": true
  }
}
```

Create a request:

```json
{
  "name": "create_request",
  "arguments": {
    "collectionRoot": "/Users/me/projects/apis/example",
    "parentPath": "Users",
    "name": "List Users",
    "item": {
      "type": "http-request",
      "request": {
        "method": "GET",
        "url": "{{baseUrl}}/users",
        "headers": [],
        "params": [],
        "body": { "mode": "none" }
      }
    }
  }
}
```

Update it using the revision returned by `get_request`:

```json
{
  "name": "update_request",
  "arguments": {
    "collectionRoot": "/Users/me/projects/apis/example",
    "relativePath": "Users/List Users.bru",
    "expectedRevision": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "patch": { "description": "Returns all users" }
  }
}
```

Run a request:

```json
{
  "name": "run_request",
  "arguments": {
    "collectionRoot": "/Users/me/projects/apis/example",
    "relativePath": "Users/List Users.bru",
    "environment": "local",
    "runtimeOverrides": { "page": 1 },
    "timeoutMs": 30000
  }
}
```

Delete it explicitly:

```json
{
  "name": "delete_request",
  "arguments": {
    "collectionRoot": "/Users/me/projects/apis/example",
    "relativePath": "Users/List Users.bru",
    "expectedRevision": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "confirm": true
  }
}
```

## Troubleshooting

- **Workspace missing/unreadable:** set `BRUNO_MCP_WORKSPACE_PATHS` to the
  exact `workspace.yml` path and verify filesystem permissions.
- **Path outside allowed roots:** add the collection's parent to
  `BRUNO_MCP_ALLOWED_ROOTS`; restart the MCP server after configuration changes.
- **Revision conflict:** re-read the collection/item/environment and retry with
  the new revision. Do not reuse a stale revision.
- **Cross-filesystem directory move rejected:** `move_folder` and
  `move_collection` never fall back to copy-and-delete. Copy or import into the
  destination through an explicit separate workflow.
- **`bru` spawn failure:** run `$BRUNO_CLI_PATH --version` in the same
  environment as the MCP client.
- **Secret store unavailable:** unlock/configure the platform's native
  credential service. The server intentionally does not fall back to plaintext.
- **Run report missing or malformed:** run the same target with `bru run`
  directly and verify CLI 4.0.0 compatibility.
- **Desktop does not immediately refresh:** reopen/reload the collection in
  Bruno Desktop; the server guarantees durable files, not UI focus/refresh.