bruno-mcp Server

Server Model Context Protocol (MCP) untuk membuat, mengelola, dan menjalankan koleksi pengujian API Bruno. Mendukung format .bru dan .yml (opencollection) dengan pengamanan keamanan bawaan.

Dokumentasi

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

  1. 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
  2. Add Bruno MCP Server:

    {
      "mcpServers": {
        "bruno-mcp": {
          "command": "node",
          "args": ["/absolute/path/to/bruno-mcp/dist/index.js"],
          "env": {}
        }
      }
    }
    
  3. 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 fileFormatPriority
opencollection.ymlYAML (opencollection)Checked first
bruno.jsonBRU (legacy)Fallback
NeitherYAML (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 name
  • description (string, optional): Collection description
  • baseUrl (string, optional): Default base URL
  • outputPath (string): Directory to create collection
  • ignore (array, optional): Files to ignore
  • format (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 collection
  • name (string): Environment name
  • variables (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 collection
  • name (string): Request name
  • method (string): HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
  • url (string): Request URL (supports {{variable}} syntax)
  • headers (object, optional): HTTP headers
  • body (object, optional): Request body — see Body Types
  • auth (object, optional): Authentication — see Auth Types
  • query (object, optional): Query parameters as Record<string, string | number | boolean>
  • folder (string, optional): Subfolder within collection
  • sequence (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.typeRequired auth.config keys
bearertoken
basicusername, password
api-keykey, value, in ("header" or "query")
digestusername, password
oauth2See 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.typeFieldsDescription
jsoncontent: JSON stringJSON body
textcontent: plain textPlain text body
xmlcontent: XML stringXML body
form-dataformData: [{name, value, type?}]Multipart form (type: "text" or "file")
form-urlencodedcontent: URL-encoded stringURL-encoded form
binarycontent: file pathBinary 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_suite supports a subset of auth types (bearer, basic, oauth2, api-key) and body types (json, text, xml, form-data, form-urlencoded). digest auth and binary body are only available in create_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 .bru or .yml request file
  • name (string, optional): New request name
  • method (string, optional): New HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
  • url (string, optional): New URL
  • headers (object, optional): Headers to merge (Record<string, string>)
  • body (object, optional): Request body -- same shape as create_request body
  • auth (object, optional): Authentication -- same shape as create_request auth
  • query (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 collection
  • entityName (string): Entity name (e.g., "Users")
  • baseUrl (string): API base URL
  • folder (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 collection
  • suiteName (string): Suite/folder name
  • requests (array): Array of request definitions (same shape as create_request)
  • dependencies (array, optional): Execution ordering constraints as [{from: string, to: string}] — enforces topological order via seq numbers. 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 .bru or .yml request file
  • scriptType (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 to workspace.yml

Workspace Resolution Cascade (highest priority first):

  1. Explicit workspacePath argument
  2. BRUNO_WORKSPACE_PATH environment variable (set this when running the server in CI or on a machine where Bruno is not installed at the default location)
  3. 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

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 subfolder
  • environment (string, optional): Environment name (loads from environments/<name>.yml)
  • collectionRoot (string, optional): Collection root for environment resolution
  • requestPath (string, optional): Run a single request file instead of the full collection

Execution Flow:

  1. Find all .yml request files, sort by seq field
  2. Load environment variables (if specified)
  3. For each request: substitute {{variables}} (env + runtime) in URL, headers, and body → execute via fetch() → run test scripts → extract bru.setVar() variables for next request
  4. Requests execute serially in sequence order; variables accumulate across the run
  5. On failure: network errors or HTTP errors are recorded in the result — execution continues to the next request (never stops early)
  6. 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:

GlobalDescription
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.
resResponse object (see methods below)
bruVariable store for cross-request chaining (see methods below)

bru methods (variable chaining):

MethodDescription
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:

MethodReturnsDescription
res.getStatus()numberHTTP status code
res.getStatusText()stringStatus text (e.g. "OK")
res.getHeaders()objectAll response headers
res.getHeader(name)string | nullSingle header (case-insensitive)
res.getBody()anyParsed JSON if content-type is application/json, otherwise raw text
res.getResponseTime()numberResponse 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: and https: 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() and new Function() blocked via codeGeneration option
  • Script size limit: 50KB maximum
  • Execution timeout: Default 5 seconds
  • No filesystem/network access: Only test(), expect(), res, and bru are 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

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. Submit a pull request

License

MIT License - see LICENSE file for details.

Links