bruno-mcp Server
A Model Context Protocol (MCP) server for creating, managing, and executing Bruno API testing collections. Supports both .bru and .yml (opencollection) formats with built-in security hardening.
Documentation
Bruno MCP Server
Active fork of macarthy/bruno-mcp (original inactive since Jul 2025). Maintained at Ostico/bruno-mcp — see announcement.
A Model Context Protocol (MCP) server for creating, managing, and executing Bruno API testing collections. Supports both .bru and .yml (opencollection) formats with built-in security hardening.
Why This MCP Server?
Use this when you want an AI agent (Claude, Copilot, etc.) to create, inspect, or execute Bruno API test collections programmatically — without opening the Bruno GUI or installing the Bruno CLI. Typical use cases: AI-assisted test generation, CI pipeline integration, automated API exploration.
Requires Node.js >= 18.0.0.
Features
- Collection Management: Create and organize Bruno collections
- Environment Configuration: Manage multiple environments (dev, staging, prod)
- Request Generation: Generate request files for all HTTP methods
- Authentication Support: Bearer, Basic, OAuth 2.0, API key, Digest
- Test Scripts: Add pre/post request scripts and assertions
- CRUD Operations: Generate complete CRUD request sets
- Collection Statistics: Analyze existing collections
- Dual Format Support:
.bru(legacy) and.yml(opencollection YAML) with auto-detection - Collection Discovery: Discover Bruno collections from workspace with zero config
- Request Modification: Partial-merge updates to existing request files
- Variable Chaining:
bru.setVar()/bru.getVar()for cross-request variable flow - Dependency Ordering: Topological sort for test suite execution order
- Request Execution: Execute requests and run tests with structured results
- Security Hardening: SSRF protection, path traversal prevention, VM sandbox for test scripts
Installation
git clone https://github.com/macarthy/bruno-mcp.git
cd bruno-mcp
npm install
npm run build
Client Integration
Quick Setup for Claude Desktop
-
Edit Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
- macOS:
-
Add Bruno MCP Server:
{ "mcpServers": { "bruno-mcp": { "command": "node", "args": ["/absolute/path/to/bruno-mcp/dist/index.js"], "env": {} } } } -
Restart Claude Desktop
Supported Clients
- Claude Desktop App - Full support
- Claude Code (VS Code) - Full support
- Continue - Tools and resources
- Cline - Tools and resources
- LM Studio - Tools support
- MCP Inspector - Development/testing
- Custom MCP Clients - via SDK
For detailed integration instructions with all clients, see INTEGRATION.md
Format Detection
The server auto-detects collection format by checking for marker files:
| Marker file | Format | Priority |
|---|---|---|
opencollection.yml | YAML (opencollection) | Checked first |
bruno.json | BRU (legacy) | Fallback |
| Neither | YAML (default) | — |
New collections default to YAML format. Pass format: "bru" to create_collection for legacy format.
Available MCP Tools
create_collection
Create a new Bruno collection with configuration.
Parameters:
name(string): Collection namedescription(string, optional): Collection descriptionbaseUrl(string, optional): Default base URLoutputPath(string): Directory to create collectionignore(array, optional): Files to ignoreformat(string, optional):"yaml"(default) or"bru"
Example:
{
"name": "my-api-tests",
"description": "API tests for my application",
"baseUrl": "https://api.example.com",
"outputPath": "./collections"
}
create_environment
Create environment configuration files.
Parameters:
collectionPath(string): Path to Bruno collectionname(string): Environment namevariables(object): Environment variables
Example:
{
"collectionPath": "./collections/my-api-tests",
"name": "production",
"variables": {
"baseUrl": "https://api.example.com",
"apiKey": "prod-key-123",
"timeout": 30000
}
}
create_request
Generate request files (.bru or .yml based on collection format).
Parameters:
collectionPath(string): Path to collectionname(string): Request namemethod(string): HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)url(string): Request URL (supports{{variable}}syntax)headers(object, optional): HTTP headersbody(object, optional): Request body — see Body Typesauth(object, optional): Authentication — see Auth Typesquery(object, optional): Query parameters asRecord<string, string | number | boolean>folder(string, optional): Subfolder within collectionsequence(number, optional): Execution order
Example:
{
"collectionPath": "./collections/my-api-tests",
"name": "Get User Profile",
"method": "GET",
"url": "{{baseUrl}}/users/{{userId}}",
"headers": {
"Authorization": "Bearer {{token}}"
},
"folder": "users"
}
Auth Types
auth.type | Required auth.config keys |
|---|---|
bearer | token |
basic | username, password |
api-key | key, value, in ("header" or "query") |
digest | username, password |
oauth2 | See Bruno OAuth2 docs |
none | (omit auth entirely) |
Example (bearer):
{ "auth": { "type": "bearer", "config": { "token": "{{token}}" } } }
Example (api-key):
{ "auth": { "type": "api-key", "config": { "key": "X-API-Key", "value": "{{apiKey}}", "in": "header" } } }
Body Types
body.type | Fields | Description |
|---|---|---|
json | content: JSON string | JSON body |
text | content: plain text | Plain text body |
xml | content: XML string | XML body |
form-data | formData: [{name, value, type?}] | Multipart form (type: "text" or "file") |
form-urlencoded | content: URL-encoded string | URL-encoded form |
binary | content: file path | Binary body |
none | (omit body entirely) | No body |
Example (JSON body):
{ "body": { "type": "json", "content": "{\"name\": \"test\"}" } }
Example (form-data):
{ "body": { "type": "form-data", "formData": [{"name": "file", "value": "photo.jpg", "type": "file"}] } }
Note:
create_test_suitesupports a subset of auth types (bearer,basic,oauth2,api-key) and body types (json,text,xml,form-data,form-urlencoded).digestauth andbinarybody are only available increate_request.
modify_request
Update an existing Bruno request file with partial-merge semantics. Only provided fields are updated; all other fields are preserved.
Parameters:
filePath(string, required): Absolute path to.bruor.ymlrequest filename(string, optional): New request namemethod(string, optional): New HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)url(string, optional): New URLheaders(object, optional): Headers to merge (Record<string, string>)body(object, optional): Request body -- same shape ascreate_requestbodyauth(object, optional): Authentication -- same shape ascreate_requestauthquery(object, optional): Query parameters to merge (Record<string, string | number | boolean>)
Example:
{
"filePath": "/path/to/collections/my-api-tests/users/get-users.yml",
"url": "{{baseUrl}}/v2/users",
"headers": {
"X-Api-Version": "2"
},
"query": {
"limit": 50
}
}
create_crud_requests
Generate a complete set of CRUD operations (5 requests: List, Get, Create, Update, Delete).
Parameters:
collectionPath(string): Path to collectionentityName(string): Entity name (e.g., "Users")baseUrl(string): API base URLfolder(string, optional): Folder name
Example:
{
"collectionPath": "./collections/my-api-tests",
"entityName": "Products",
"baseUrl": "{{baseUrl}}/api/v1",
"folder": "products"
}
create_test_suite
Generate a test suite with multiple related requests and optional dependencies.
Parameters:
collectionPath(string): Path to collectionsuiteName(string): Suite/folder namerequests(array): Array of request definitions (same shape ascreate_request)dependencies(array, optional): Execution ordering constraints as[{from: string, to: string}]— enforces topological order viaseqnumbers. Circular dependencies return an error.
Example:
{
"collectionPath": "./collections/my-api-tests",
"suiteName": "Auth Flow",
"requests": [
{ "name": "Login", "method": "POST", "url": "{{baseUrl}}/auth/login" },
{ "name": "Get Profile", "method": "GET", "url": "{{baseUrl}}/auth/profile" }
],
"dependencies": [
{ "from": "Login", "to": "Get Profile" }
]
}
add_test_script
Add test scripts to existing request files. Format-aware: injects into .bru or .yml automatically.
Parameters:
bruFilePath(string): Path to.bruor.ymlrequest filescriptType(string):"pre-request","post-response", or"tests"script(string): JavaScript code (max 50KB)
list_collections
Discover Bruno collections from the Bruno app's workspace.yml.
Parameters:
workspacePath(string, optional): Explicit path toworkspace.yml
Workspace Resolution Cascade (highest priority first):
- Explicit
workspacePathargument BRUNO_WORKSPACE_PATHenvironment variable (set this when running the server in CI or on a machine where Bruno is not installed at the default location)- Platform default:
- macOS:
~/Library/Application Support/bruno/default-workspace/workspace.yml - Linux:
~/.config/bruno/default-workspace/workspace.yml - Windows:
%APPDATA%/bruno/default-workspace/workspace.yml
- macOS:
Returns:
{
"collections": [
{ "name": "My API", "path": "/path/to/collection", "exists": true },
{ "name": "Old API", "path": "/missing/path", "exists": false }
]
}
get_collection_stats
Get detailed statistics about a collection.
Parameters:
collectionPath(string): Path to collection
Returns:
{
"totalRequests": 12,
"requestsByMethod": { "GET": 5, "POST": 4, "PUT": 2, "DELETE": 1 },
"folders": ["auth", "users", "products"],
"environments": ["dev", "staging", "prod"],
"requests": [
{ "name": "Get Users", "method": "GET", "seq": 1, "folder": "users", "hasTests": true }
]
}
run_collection
Execute all requests in a collection (or a single request) and run test scripts.
Parameters:
collectionPath(string): Path to collection or subfolderenvironment(string, optional): Environment name (loads fromenvironments/<name>.yml)collectionRoot(string, optional): Collection root for environment resolutionrequestPath(string, optional): Run a single request file instead of the full collection
Execution Flow:
- Find all
.ymlrequest files, sort byseqfield - Load environment variables (if specified)
- For each request: substitute
{{variables}}(env + runtime) in URL, headers, and body → execute viafetch()→ run test scripts → extractbru.setVar()variables for next request - Requests execute serially in sequence order; variables accumulate across the run
- On failure: network errors or HTTP errors are recorded in the result — execution continues to the next request (never stops early)
- Requests with no test scripts report zero tests (still counted in
summary.total)
Returns:
{
"summary": { "total": 4, "passed": 3, "failed": 1, "duration_ms": 1250 },
"results": [
{
"name": "Get Schema",
"method": "GET",
"url": "https://api.example.com/schema",
"status": 200,
"duration_ms": 312,
"tests": [
{ "description": "Status is 200", "status": "pass" },
{ "description": "Body is JSON", "status": "pass" }
]
}
]
}
Environment Variables
Environment files define variables that are substituted into requests at execution time.
YAML format (environments/dev.yml):
name: dev
variables:
- name: baseUrl
value: https://dev.api.example.com
- name: apiKey
value: dev-key-123
- name: disabled_var
value: skip-me
disabled: true
Input vs file format: The create_environment tool accepts variables as a flat object ({"baseUrl": "..."}) and converts them to the YAML array format shown above. You never need to construct the array format yourself when calling the tool.
Substitution: Any {{variableName}} in request URLs, headers, or body content is replaced with the corresponding environment variable value. Variables with disabled: true are skipped. Unresolved references (e.g. {{missing}}) are left as-is.
Test Script API
Test scripts run in a sandboxed VM with these globals:
| Global | Description |
|---|---|
test(description, fn) | Define a test case. fn is called synchronously; exceptions mark the test as failed. |
expect(value) | Chai expect — supports .to.equal(), .to.have.property(), .to.be.above(), etc. |
res | Response object (see methods below) |
bru | Variable store for cross-request chaining (see methods below) |
bru methods (variable chaining):
| Method | Description |
|---|---|
bru.setVar(name, value) | Store a variable for use by subsequent requests. Run-scoped — lost when execution ends. |
bru.getVar(name) | Retrieve a previously set variable. Returns undefined if not set. |
Variables set via bru.setVar() are merged with environment variables for {{substitution}} in subsequent requests. Runtime variables take precedence over environment variables with the same name.
res methods:
| Method | Returns | Description |
|---|---|---|
res.getStatus() | number | HTTP status code |
res.getStatusText() | string | Status text (e.g. "OK") |
res.getHeaders() | object | All response headers |
res.getHeader(name) | string | null | Single header (case-insensitive) |
res.getBody() | any | Parsed JSON if content-type is application/json, otherwise raw text |
res.getResponseTime() | number | Response time in milliseconds |
Example test script:
test("Status is 200", function() {
expect(res.getStatus()).to.equal(200);
});
test("Response is JSON array", function() {
const body = res.getBody();
expect(body).to.be.an("array");
expect(body.length).to.be.above(0);
});
test("Response time under 2s", function() {
expect(res.getResponseTime()).to.be.below(2000);
});
// Chain a variable to subsequent requests
bru.setVar("userId", res.getBody()[0].id);
Variable chaining example (Login → Profile):
// In Login's after-response script:
test("Login returns token", function() {
expect(res.getBody().access_token).to.be.a("string");
});
bru.setVar("token", res.getBody().access_token);
// In Get Profile's request, {{token}} is now substituted automatically
File Formats
YAML Request (.yml)
info:
name: Get Users
type: http
seq: 1
http:
method: GET
url: "{{baseUrl}}/users"
headers:
- name: Authorization
value: "Bearer {{token}}"
body:
type: json
data: '{"limit": 10}'
auth:
type: bearer
token: "{{token}}"
runtime:
scripts:
- type: after-response
code: |
test("Status is 200", function() {
expect(res.getStatus()).to.equal(200);
});
settings:
timeout: 5000
BRU Request (.bru)
meta {
name: Get Users
type: http
seq: 1
}
get {
url: {{baseUrl}}/users
body: none
auth: none
}
headers {
Content-Type: application/json
Authorization: Bearer {{token}}
}
tests {
test("Status should be 200", function() {
expect(res.status).to.equal(200);
});
}
Generated Collection Structure
my-collection/
├── opencollection.yml # YAML format collection config
├── bruno.json # BRU format collection config
├── .gitignore
├── README.md
├── environments/
│ ├── dev.yml
│ └── prod.yml
├── auth/
│ ├── login.yml
│ └── get-profile.yml
└── users/
├── get-users.yml
└── create-user.yml
Security
SSRF Protection
All outbound requests from run_collection are validated:
- Private IP blocking: Requests to
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16, and IPv6 equivalents (including IPv4-mapped::ffff:x.x.x.x) are blocked - Scheme validation: Only
http:andhttps:schemes allowed - Redirect TOCTOU protection: Each redirect hop is re-validated against SSRF rules (prevents DNS rebinding via redirects to internal IPs)
Path Traversal Prevention
All tool inputs that accept file paths are validated:
..segments rejected- Null byte (
\0) injection blocked - Paths resolved and checked against expected base directory
VM Sandbox (Test Scripts)
Test scripts execute in a hardened node:vm context:
- Prototype chain isolation: Context created with
Object.create(null) - Code generation disabled:
eval()andnew Function()blocked viacodeGenerationoption - Script size limit: 50KB maximum
- Execution timeout: Default 5 seconds
- No filesystem/network access: Only
test(),expect(),res, andbruare available
Testing
npm test # Run 591 unit tests (95%+ coverage)
Development
Project Structure
src/
├── index.ts # Main entry point & exports
├── server.ts # MCP server (10 tools)
└── bruno/
├── types.ts # TypeScript interfaces
├── collection.ts # Collection management
├── environment.ts # Environment management
├── request.ts # Request builder (dual format)
├── bru-parser.ts # .bru file parser/generator
├── yaml-parser.ts # YAML request parser
├── yaml-generator.ts # YAML file generator
├── format-detector.ts # Auto-detect .bru vs .yml
├── format-factory.ts # Format-aware read/write
├── collection-stats.ts # Collection analysis
├── request-executor.ts # HTTP execution engine
├── test-runner.ts # Sandboxed test runner (node:vm)
├── env-loader.ts # Environment variable loader
├── workspace.ts # Workspace resolver
├── variable-store.ts # Run-scoped variable store for cross-request chaining
├── list-collections-handler.ts
├── url-validator.ts # SSRF protection
├── path-validator.ts # Path traversal prevention
└── response-wrapper.ts # Response object for test scripts
Building
npm run build # Build with tsup
npm run typecheck # TypeScript type checking
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
License
MIT License - see LICENSE file for details.