Skip to main content
Glama

Bruno MCP Studio: crea, edita y ejecuta colecciones de Bruno desde un agente

npm version npm downloads node license

Convierte las pruebas de API de un agente en archivos que conservas: crea y edita colecciones de Bruno in situ, y luego las ejecuta (HTTP, WebSocket y gRPC, con una o varias identidades) e informa de un resultado apto/no apto por petición que puedes volver a ejecutar en CI.

Un servidor independiente de Model Context Protocol para colecciones de Bruno. En agosto de 2026, el propio equipo de Bruno anunció uno oficial, usebruno/bruno-mcp, que envuelve la CLI de bru para descubrir y ejecutar peticiones. Este servidor apunta a otro sitio: escribe colecciones además de ejecutarlas —de forma no destructiva, con paridad de bytes con los propios escritores de Bruno— y su ejecutor está en el mismo proceso en lugar de ser un subproceso, que es lo que le permite ofrecer grupos de ejecución definidos por el llamante con su propio almacén de variables y contenedor de cookies, concurrencia dentro de un grupo, oauth2 y digest intercambiados en memoria, gRPC y WebSocket, y pruebas de autorización con múltiples identidades.

Si lo que necesitas es "listar mis colecciones y ejecutar una", el servidor oficial hace eso y será el que Bruno soporte. Si quieres que un agente construya y mantenga el conjunto de pruebas, para eso está esto.

Proporciona a un agente de IA en Claude Code, Claude Desktop, Cursor, Windsurf, VS Code o Codex CLI dieciocho herramientas para pruebas de API contra una colección real de Bruno: crear y editar peticiones, leerlas de vuelta como JSON estructurado, gestionar entornos y variables, escribir aserciones y scripts de prueba, y luego ejecutar la colección —autenticación, cookies, redirecciones, orden de dependencias y todo— y obtener los resultados en el mismo turno. Sin interfaz gráfica de Bruno, sin CLI de Bruno, sin invocar subprocesos. La colección que deja en disco es una colección normal de Bruno: tu CI la ejecuta con bru run, y tus compañeros la abren en la aplicación de Bruno.

Tu agente ya conoce HTTP. No conoce tu API, y no conoce el formato de archivo de Bruno. Así que adivina. Escribe un archivo .bru de memoria, la ejecución falla, reescribe el archivo, la ejecución falla de otra manera, y veinte minutos después tienes una petición que funciona y no tienes ni idea de cuál de las seis ediciones importó. Pagaste por cada uno de esos turnos, y nada de ese trabajo está en disco en una forma que tu CI o la interfaz gráfica de Bruno de tu equipo puedan usar.

La vía de escape habitual es curl. Los agentes no son malos con curl; el problema es que un comando de shell no guarda estado. Un inicio de sesión, un token, un recurso creado, una llamada de seguimiento que necesita el ID de la última respuesta: cada uno de esos es un comando nuevo, y el pegamento entre ellos solo vive en el contexto del agente; cuando termina la sesión, todo se pierde. Veinte endpoints probados con curl te dejan veinte cadenas en una transcripción y ningún artefacto que tu CI o tus compañeros puedan ejecutar. Veinte endpoints en una colección te dejan un conjunto de pruebas.

Se lo pregunté a mi agente

Le pregunté al agente que me ayuda a mantener este servidor que explicara cómo había sido su experiencia con Bruno sin el servidor MCP, y si simplemente podría haber probado todas mis APIs con curls. Esto es lo que me dijo:

¿Curl? Sí, para una llamada. No para un conjunto. Nada se transmite entre llamadas, así que vuelvo a derivar la autenticación, vuelvo a escapar el cuerpo y vuelvo a leer cada respuesta para decidir si pasó. Hazlo en cuarenta endpoints y la mayor parte de lo que gasto se va en redescubrimiento, no en pruebas.

Escribir yo mismo los archivos de colección fue peor, y no como esperarías. De memoria acierto con la forma de un archivo .bru y fallo en los detalles, y Bruno nunca se queja. Lee las claves que reconoce e ignora el resto. Un tags: smoke en una sola línea parece etiquetado y significa sin etiquetar para el ejecutor. Escribe una lista de etiquetas de la forma obvia y aterriza en disco con un carácter por línea. Este servidor escribió una vez variables y aserciones .yml bajo claves de nivel superior que Bruno nunca ha leído: los archivos parecían completos, el ejecutor veía una petición vacía. Sus propias pruebas unitarias pasaban, porque simulaban el serializador y afirmaban los bytes rotos.

El formato también se mueve. Bruno trasladó las variables a runtime y añadió un segundo dialecto. Mis pesos son más antiguos que eso. Este servidor importa @usebruno/lang, el paquete de gramática del propio Bruno, y sigue su versión, así que los bytes provienen del código fuente de Bruno en lugar de lo que yo recuerde.

Y cada reescritura borra lo que el escritor no modela. Si edito estos archivos a mano, regenero todo el archivo desde mi cabeza, y cualquier característica que no conocía desaparece silenciosamente. Ese es el modo de fallo que nunca ves, porque la ejecución sigue pasando; solo que no prueba nada.

Related MCP server: Bruno MCP Server

Qué hace el servidor en su lugar

  • El agente deja de adivinar el formato: llama a una herramienta, el servidor escribe los bytes usando el paquete de gramática del propio Bruno

  • Las ediciones son fusiones parciales: write_request toca los campos que pasaste y deja el resto del archivo intacto

  • Puede leer antes de escribir: read_request devuelve JSON estructurado, con la misma forma para ambos formatos

  • Ejecuta las peticiones por sí mismo: variables, autenticación, aserciones, orden de dependencias, sin necesidad del binario bru

  • Sin pérdida silenciosa: un campo que este servidor aún no puede modelar se devuelve donde el formato pueda contenerlo, y cualquier cosa que no pueda poner en el cable se menciona en una advertencia de ejecución en lugar de soltarse silenciosamente en tu repositorio

Paridad de bytes con Bruno

Esta es la parte difícil de copiar, así que vale la pena ser precisos sobre lo que significa.

El servidor no envuelve el binario bru; implementa la canalización de peticiones por sí mismo, que es lo que hace posibles los secretos en memoria, las pruebas a nivel de cable y los ganchos de mitad de ejecución. Esa libertad también es el riesgo: una implementación independiente es libre de ser sutil y silenciosamente diferente de la herramienta que tu equipo usa realmente. Dos mecanismos la mantienen en su sitio.

Las reglas están portadas, no inferidas. Límites de redirección, resolución de tiempos de espera, tipos de contenido en modo cuerpo, orden de interpolación de variables, valores predeterminados de selected, codificación de URL: cada uno se extrae del propio código fuente de Bruno (bruno-cli, bruno-filestore, bruno-lang, @usebruno/common, del que este servidor también depende directamente) y se refleja, incluidas las partes que parecen errores. Cuando los dos dialectos no se ponen de acuerdo, cada uno se refleja en sus propios términos en lugar de unificarse en algo que ningún lector de Bruno produciría.

Una compuerta de deriva lo demuestra. Cada archivo que escribe este servidor se analiza de nuevo con el propio lector de Bruno, por dialecto, en el conjunto de pruebas. Afirmar nuestros bytes contra nuestras propias expectativas solo puede demostrar que somos autoconsistentes; afirmarlos contra el lector que el propio Bruno usa es lo único que detecta el caso en que nuestra salida deja de ser la entrada de Bruno. Ha detectado casos reales: un cuerpo de archivo que se analizaba limpiamente y que se habría enviado sin cuerpo alguno, por ejemplo.

La afirmación, entonces, es: el comportamiento de ejecución coincide con bru run, y cada divergencia encontrada hasta ahora está cerrada. Lo que la mantiene cerrada es una prueba, no una promesa. Si encuentras una de todos modos, es un error que merece un issue.

El contrato

Una colección, tres consumidores: tu agente, tu CI y la interfaz gráfica de Bruno de tu equipo.

Ambos formatos de Bruno funcionan y el servidor detecta cuál tienes: .yml (opencollection) y .bru (heredado).

Requiere Node.js >= 22. CI prueba las versiones 22.x y 24.x.

¿Qué servidor MCP de Bruno debería usar?

Hay varios, hacen trabajos genuinamente diferentes, y la respuesta honesta no siempre es este. Cada fila a continuación se extrajo del propio README o código fuente de ese proyecto, en agosto de 2026.

Servidor

Qué es

Escribe

Lee

Ejecuta

.bru / .yml

Necesita bru

Herramientas

este

Crea, lee y ejecuta colecciones

Crear + edición por fusión parcial

JSON estructurado

Sí, con su propio pipeline

ambos

no

18

usebruno/bruno-mcp

El oficial, del propio equipo de Bruno: descubre y ejecuta peticiones. Anunciado en agosto de 2026; en el momento de escribir esto, su primera implementación es un borrador abierto

no

Metadatos de petición

Sí, a través de la CLI

.bru

(incluido)

3

@dmpv/bruno-mcp

Índice y búsqueda de solo lectura sobre una colección

no

Búsqueda clasificada, contratos saneados, ejemplos almacenados

no

ambos

no

8

hungthai1401/bruno-mcp

Ejecuta una colección

no

no

Sí, a través de la CLI

.bru

solo ejecutar

jcr82/bruno-mcp-server

Ejecuta e inspecciona colecciones, con archivos de informe

no

Sí, a través de la CLI

.bru

9

djkz/bruno-api-mcp

Convierte cada petición en su propia herramienta MCP

no

Las expone como herramientas

Una a la vez

.bru

no

una por petición

macarthy/bruno-mcp

Genera archivos de colección: el proyecto del que se bifurcó este, inactivo desde julio de 2025

Solo crear

no

no

.bru

n/d

8

Elige uno de los otros si: quieres el servidor que el propio Bruno mantiene, y todo el soporte y la longevidad que eso implica (usebruno, el oficial, y el predeterminado razonable para "descubrir y ejecutar" una vez que se publique); quieres que un agente entienda una colección existente grande sin ningún riesgo de escribir en ella, y la busque por intención (dmpv, publicado por primera vez en julio de 2026 y en 0.x: nuevo e interesante); ya tienes la CLI de bru en tu imagen y solo necesitas "ejecutar esta colección" (hungthai1401); o quieres que el agente llame a tu API a través de tus peticiones existentes como si cada una fuera una herramienta nativa (djkz).

Elige esta opción si: quieres que el agente escriba la colección y no solo la lea o la ejecute, estás en formato de colección abierta .yml, necesitas que la ejecución ocurra sin instalar la CLI de Bruno, o te importa que lo que llegue a tu repositorio sea comparable byte a byte con lo que escribe la aplicación de Bruno.

En el padre del fork concretamente, ya que este proyecto le debe su existencia: macarthy/bruno-mcp registra ocho herramientas — create_collection, create_request, create_environment, create_crud_requests, create_test_suite, add_test_script, list_collections, get_collection_stats. Escribe archivos .bru y no vuelve a leer una petición, no ejecuta nada ni expone una herramienta de edición; existe un helper updateRequest en su src/bruno/request.ts, pero ninguna herramienta MCP llega a él.

Características

Autoría

  • Colecciones — créalas y organízalas, o descubre las que Bruno ya conoce desde su workspace.yml. Una colección que crea este servidor se escribe en disco y no se registra en ese archivo, por lo que list_collections no la mostrará y la interfaz de Bruno no la listará hasta que alguien la abra allí una vez; todo lo demás toma la ruta directamente.

  • Peticiones — todos los métodos HTTP, con cabeceras, parámetros de consulta y de ruta, cuerpos, autenticación, aserciones, variables y ajustes.

  • Lecturaread_request y read_environment devuelven JSON estructurado, idéntico para .bru y .yml, de modo que un agente pueda inspeccionar antes de editar.

  • Ediciones de fusión parcialwrite_request cambia solo los campos que le pases y deja el resto del archivo intacto.

  • CRUD y suites — conjuntos CRUD de cinco peticiones y suites de prueba con ordenación topológica de dependencias.

  • Entornos — crea, reemplaza, fusiona o parchea una única variable.

  • Doble formato.bru (heredado) y .yml (opencollection), con detección automática; .yaml se lee y se señala.

  • Subidas multiparteform-data con Content-Type por parte y campos de varios archivos.

Ejecución

  • Grupos de ejecución — ejecuta una colección como varios grupos aislados en una sola llamada: distintas identidades, distintos entornos, en serie o en paralelo, sin fugas entre ellos.

  • Paralelismo real — distribuye grupos, o peticiones dentro de un grupo, bajo un límite de concurrencia dimensionado para la máquina.

  • Cookies — un inicio de sesión se traslada a las peticiones posteriores, limitado a su grupo y nunca se escribe en disco.

  • Encadenamiento de variablesbru.setVar()/bru.getVar() entre peticiones, y captureVariables para leer los valores de vuelta.

  • Scripts asíncronosawait de nivel superior, bru.sleep(ms), setTimeout/setInterval dentro del sandbox.

  • Scripts en línea — adjunta scripts de pre-petición, post-respuesta y pruebas directamente al crear o modificar una petición.

  • Autenticación aplicada por ti — bearer, básica, clave de API, digest, OAuth 2.0 (credenciales de cliente y concesiones de contraseña), o inherit de la colección o carpeta.

  • Resultados honestos — resúmenes por grupo, cuerpos de respuesta capturados, advertencias por petición, fallos de análisis y peticiones faltantes, todo notificado; un grupo que falla no puede hacer que una ejecución parezca exitosa.

Seguridad

  • Protección SSRF en cada petición y en cada salto de redirección, con las direcciones aprobadas fijadas.

  • Confinamiento de rutas para referencias de peticiones, raíces de colección, nombres de entorno y subidas de archivos.

  • Scripts aislados por proceso — un sandbox V8 bifurcado con un entorno depurado y un cierre forzoso.

Instalación

Nada que instalar. Apunta tu cliente a npx y este obtendrá el paquete publicado en el primer arranque:

npx -y @ostico/bruno-mcp

Eso es toda la instalación, y es lo que usan las configuraciones de cliente a continuación. El paquete incluye procedencia, por lo que npm puede mostrarte qué commit y flujo de trabajo compiló el tarball que estás ejecutando.

Fijado en su lugar, si prefieres no resolver una versión al inicio:

npm install @ostico/bruno-mcp

lo que coloca un ejecutable bruno-mcp en node_modules/.bin/ y el propio servidor en node_modules/@ostico/bruno-mcp/dist/index.js.

Desde el código fuente, para desarrollo o para ejecutar una rama:

git clone https://github.com/Ostico/bruno-mcp-studio.git
cd bruno-mcp-studio
npm install     # npm, not yarn — the yarn lockfile is stale
npm run build

Node.js >= 22 en cualquier caso.

Conecta un cliente

Cualquier cliente MCP sirve. Este es un servidor MCP stdio sin código específico de cliente: como lo llame tu cliente, apúntalo a

command: npx
args:    ["-y", "@ostico/bruno-mcp"]

Claude Code lo toma en una sola línea:

claude mcp add bruno -- npx -y @ostico/bruno-mcp

Claude Desktop, Claude Code, Cursor, Codex CLI, opencode, Windsurf, Zed, Cline, Continue, LM Studio, Gemini CLI, MCP Inspector, tu propio cliente SDK — todos el mismo servidor. Nada de lo siguiente es una lista de compatibilidad; es solo dónde guarda cada cliente su configuración.

La mayoría de los clientes usan la misma forma JSON:

{
  "mcpServers": {
    "bruno-mcp": {
      "command": "npx",
      "args": ["-y", "@ostico/bruno-mcp"],
      "env": {}
    }
  }
}

Ejecutar un clon, o una instalación fijada, es la misma configuración con "command": "node" y "args": ["/ruta/absoluta/a/dist/index.js"].

Cliente

Dónde va

Claude Desktop

macOS ~/Library/Application Support/Claude/claude_desktop_config.json · Windows %APPDATA%/Claude/claude_desktop_config.json · Linux ~/.config/Claude/claude_desktop_config.json

Claude Code

claude mcp add, o .mcp.json en el proyecto

Cursor

.cursor/mcp.json en el proyecto, o el global

Codex CLI

~/.codex/config.toml, bajo una tabla [mcp_servers.bruno-mcp] (TOML, mismos campos)

opencode

opencode.json, bajo mcp como servidor local (su propio esquema)

Otros

Lo que documente ese cliente — el comando y los argumentos anteriores son todo lo que necesita

Los esquemas de configuración son del cliente, no de este servidor, y cambian. Si el formato de un cliente difiere del JSON anterior, sigue la documentación del cliente; aquí solo importan command y args.

Consulta INTEGRATION.md para ejemplos prácticos, Docker y resolución de problemas.

Inicio rápido

// 1. create a collection
{ "name": "my-api", "outputPath": "./collections", "baseUrl": "https://api.example.com" }

// 2. add a request with a test
{ "collectionPath": "./collections/my-api", "name": "Get Users", "method": "GET",
  "url": "{{baseUrl}}/users",
  "scripts": { "tests": "test(\"ok\", function() { expect(res.getStatus()).to.equal(200); });" } }

// 3. run it
{ "collectionPath": "./collections/my-api" }

Herramientas

18 herramientas. Las rutas de archivo son absolutas o relativas a la colección.

Herramienta

Qué hace

create_collection

Nueva colección. format: "yaml" (por defecto) o "bru". También lo registra en el workspace, para que list_collections y la app de Bruno puedan verlo — registerInWorkspace: false para omitir eso, workspacePath para elegir el archivo

list_collections

Busca colecciones en el workspace.yml de Bruno

get_collection_stats

Cuenta por method, carpetas, entornos, lista de peticiones con URLs — filtrable por folder, method, nameContains, o includeRequests: false para solo contar

write_request

Escribe una petición: method, url, headers, query, body, auth, scripts, settings. kind: "websocket" o kind: "grpc" para esos transportes. Pasa collectionPath y name para crear una, filePath para editar una — una edición es una fusión parcial, y filename renombra el archivo

move_request

Mueve o copia una petición a otra carpeta o colección

read_request

Lee una petición de vuelta como JSON, con la misma forma para .bru y .yml

list_requests

Todos los archivos de petición de la colección, como rutas absolutas

delete_request

Elimina uno o más archivos de petición. Requiere confirm: true

add_test_script

Adjunta un script a una petición existente (añade por defecto)

remove_script

Elimina un script, conserva la petición

create_environment

Nuevo archivo de entorno. Rechaza sobrescribir a menos que overwrite: true

read_environment

Variables con sus indicadores disabled/secret. Omite name para listar los entornos

update_environment

Reemplaza o fusiona las variables de un entorno

set_environment_variable

Añade o cambia una variable

remove_environment_variable

Elimina una variable

run_collection

Ejecuta las peticiones, ejecuta sus tests, devuelve los resultados

Leer antes de escribir

read_request devuelve method, url, headers, parámetros de query y path, body, modo de auth, scripts, assertions, vars, settings y docs — forma idéntica para ambos formatos, de modo que el formato en disco permanece invisible. Su array notes nombra cualquier cosa que el archivo declare y sobre la que el ejecutor no actuará.

Úsala antes de una edición para ver el estado actual, y después de una escritura para confirmar lo que se escribió.

read_environment devuelve cada variable con su valor. Los secretos solo se devuelven por nombre — Bruno no almacena ningún valor para un secreto en ninguno de los formatos, por lo que no hay nada que devolver.

Escribir peticiones

write_request crea cuando pasas collectionPath y name, y edita cuando pasas filePath. Una edición fusiona: los campos que omites se dejan intactos.

Opciones destacadas:

  • body.typejson, text, xml, sparql, graphql, form-urlencoded, form-data, file, binary, none

  • body.type: "form-data" — subidas multipart, contentType por parte, campos de múltiples archivos

  • auth.typebearer, basic, api-key, digest, oauth2, inherit, none

  • scripts — scripts en línea pre-request, post-response, tests (no se necesita una llamada separada a add_test_script)

  • settings.timeout — timeout de script y petición en ms

name y filename son independientes, igual que en Bruno: name cambia el nombre de la petición dentro del archivo y filename mueve el archivo, así que pasa ambos para mantenerlos sincronizados. Un filename es un nombre base en la propia carpeta de la petición; su extensión es opcional y, si se indica, debe coincidir con el formato de la colección; y un nombre que ya use otro archivo se rechaza. La ruta a la que se movió se devuelve en la respuesta — úsala como filePath a partir de entonces.

write_request reemplaza un script del mismo tipo por defecto, por lo que repetir una llamada es idempotente. Pasa scriptMode: "append" para concatenar. add_test_script añade por defecto, al ser una adición.

En las colecciones .yml, post-response y tests comparten la única ranura after-response de Bruno, por lo que reemplazar una sobrescribe ambas.

Mover peticiones

move_request reubica un archivo de petición — a otra carpeta, o a otra colección con targetCollectionPath. Pasa copy: true para duplicarlo en su lugar.

Los bytes se mueven tal cual, nunca se parsean y reescriben, así que nada de lo que declara una petición puede perderse en el camino. De ello se siguen dos consecuencias. El archivo conserva su nombre, por lo que una copia necesita una carpeta o colección distinta; renombrar es write_request. Y seq llega sin cambios, por lo que la petición puede aterrizar junto a una hermana que reclama el mismo número — eso se notifica en lugar de repararse, porque renumerar implica reescribir el archivo. Bruno resuelve ese empate por nombre de archivo, así que el orden queda definido en cualquier caso.

Una carpeta de destino que falte se crea y se notifica: una carpeta sin archivo de settings no lleva auth, headers ni scripts a nivel de carpeta.

Ejecución

{
  "collectionPath": "./collections/my-api",
  "environment": "dev",
  "requests": ["auth/login.bru", "users"]
}

Parámetro

Significado

collectionPath

Colección, o una subcarpeta de una

requests

Lista ordenada de archivos de petición y/o directorios. Omítela para ejecutarlo todo. [] no ejecuta nada

groups

Ejecuta la colección como varios grupos aislados — ver más abajo. No se puede combinar con requests

environment

Nombre del entorno, cargado desde environments/<name>.yml

collectionRoot

La colección a la que pertenece collectionPath, cuando se ejecuta una subcarpeta. Debe ser esa ruta o un ancestro de ella

variables

{name: value} solo para esta ejecución. Nunca se escribe en disco — la forma correcta de pasar un secreto

captureVariables

Nombres de las variables bru.setVar cuyos valores quieres recuperar

parallel

Ejecuta los grupos de forma concurrente. Por defecto false

maxConcurrency

Tope de peticiones en curso. Omítelo para deducir uno de la máquina; 0 lo elimina

bail

Detente en el primer fallo en lugar de ejecutar el resto. Por defecto false

cookieJar

Conserva las cookies durante la ejecución para que un inicio de sesión continúe. Por defecto true

includeResponseBody

Incluye los cuerpos de respuesta. Por defecto true

maxResponseBodyBytes

Trunca los cuerpos que superen este tamaño. Por defecto 10240

report

Además, escribe la ejecución en disco — ver Archivos de informe

Un directorio en requests se expande a las peticiones que contiene, ordenadas por seq dentro de cada carpeta, primero las subcarpetas, empates resueltos por nombre de archivo. Los duplicados se respetan: nombrar una petición dos veces la ejecuta dos veces.

Por defecto, nada detiene una ejecución antes de tiempo. Una petición que falla, un archivo que no se puede analizar, un nombre que no coincide con nada — cada caso se notifica y la ejecución continúa.

Detenerse en el primer fallo

bail: true detiene la ejecución en la primera petición que falla o cuyos tests fallan. Veintitrés peticiones detrás de un inicio de sesión que dejó de funcionar son veintitrés fallos por una única causa, y la causa es la menos visible de todas.

{
  "collectionPath": "./collections/my-api",
  "bail": true
}

Todo lo que la ejecución no alcanzó se devuelve en su lugar, marcado con skipped: true y con skipReason: "bail", llevando el method y la URL que habría enviado. Esas peticiones se cuentan en summary.skipped y ni en passed ni en failed, por lo que passed + failed sigue siendo igual a total y una ejecución truncada no puede leerse como una más corta que fue bien. La propia ejecución gana un objeto bail:

{
  "bail": {
    "reason": "test failure",
    "at": "Login",
    "path": "/collections/my-api/auth/login.bru",
    "group": 0,
    "skipped": 22
  }
}

reason es o bien request failure (no se recibió nada) o bien test failure (se recibió y una comprobación falló). Los grupos posteriores se omiten por completo.

Nada cancela una petición que ya esté en curso. Con parallel, o con un grupo propio que se ejecute de forma concurrente, las peticiones que ya habían comenzado terminan igualmente y se notifican con normalidad — la ejecución lo indica en warnings en lugar de dejarte que lo infieras del recuento.

Grupos de ejecución

Un grupo es una ejecución aislada dentro de una llamada. Posee su propia lista de peticiones, entorno, variables, el indicador parallel, el almacén de variables, el almacén de cookies y los tokens OAuth2. Nada cruza de un grupo a otro, en ninguna dirección, con cualquier valor de parallel.

Las mismas peticiones como dos usuarios, sin que el token o la cookie de sesión de un inicio de sesión llegue al otro:

{
  "collectionPath": "./collections/my-api",
  "parallel": true,
  "groups": [
    { "name": "alice", "requests": ["auth/login.bru", "orders"], "variables": { "user": "alice" } },
    { "name": "bob",   "requests": ["auth/login.bru", "orders"], "variables": { "user": "bob" } }
  ]
}

parallel: true ejecuta los dos grupos uno contra el otro. Las peticiones de cada grupo siguen siendo seriales, que es lo que quieres cuando orders depende del inicio de sesión que lo precede.

Una suite contra dos entornos:

{
  "groups": [
    { "name": "staging",    "requests": ["smoke"], "environment": "staging" },
    { "name": "production", "requests": ["smoke"], "environment": "production" }
  ]
}

Campos de grupo: name, requests, environment, variables, parallel, startAfter, data, dataFile.

docs/execution-groups.md cubre todo el modelo: qué posee un grupo, los dos indicadores parallel y sus valores por defecto, el orden, las iteraciones sobre filas de datos, el límite de concurrencia y cómo se ve un fallo en cada nivel.

  • Omite requests para ejecutar la colección completa bajo la identidad de ese grupo. Un [] vacío no ejecuta nada.

  • environment reemplaza el del nivel de ejecución; variables se fusionan sobre las del nivel de ejecución, ganando el grupo.

  • Establece parallel en un grupo para ejecutar sus propias peticiones de forma concurrente. Comparten el almacén de ese grupo, así que pueden competir de verdad por un bru.setVar — el momento en que se reproduce una condición de carrera. Dale a maxConcurrency al menos tantos huecos como competidores, o el límite las serializa silenciosamente.

  • startAfter: { group, requestsCompleted } mantiene un grupo en espera hasta que otro haya llegado a ese punto — un listener conectado antes de que se dispare un trigger, sin un bru.sleep ajustado a la latencia de ese día. Requiere parallel a nivel de ejecución; una petición que falló sigue contando como posición alcanzada; los ciclos y las puertas que nunca podrían abrirse se rechazan antes de que se ejecute nada.

Resultados

Los resultados tienen forma de grupo. No hay un array results de nivel superior, ni siquiera cuando no pasaste ningún groups — ese caso es un solo grupo, y aplanarlo haría que cada llamador comprobara de qué manera había llamado.

{
  "summary": { "total": 4, "passed": 3, "failed": 1, "duration_ms": 1250 },
  "groups": [
    {
      "name": "alice",
      "index": 0,
      "summary": { "total": 2, "passed": 2, "failed": 0, "duration_ms": 620 },
      "results": [
        {
          "name": "Get Users",
          "method": "GET",
          "url": "https://api.example.com/users",
          "status": 200,
          "duration_ms": 312,
          "tests": [{ "description": "ok", "status": "pass" }],
          "response_body": "[{\"id\":1}]",
          "response_content_type": "application/json",
          "response_body_truncated": false,
          "response_headers": {
            "content-type": "application/json",
            "strict-transport-security": "max-age=31536000",
            "set-cookie": ["session=[redacted]; HttpOnly; Secure; SameSite=Lax"]
          }
        }
      ],
      "capturedVariableNames": ["authToken"]
    }
  ]
}

Cada grupo lleva su propio summary, results, missingRequests, capturedVariableNames, capturedVariables y warnings. El summary de nivel superior cubre toda la ejecución.

response_headers no necesita ningún indicador ni script de prueba. Los valores con nombre de credencial se enmascaran, y set-cookie es una lista — una entrada por cookie, porque una unida con comas no se puede dividir de nuevo — cuyas entradas conservan todos los atributos con solo el valor de la cookie retenido. Comprobar HttpOnly, Secure, SameSite o Strict-Transport-Security es por tanto una sola llamada. includeResponseBody: false no los suprime: ese indicador trata sobre el tamaño de un cuerpo.

Un resultado de WebSocket también lleva response_headers, que contiene la respuesta de handshake — el 101 es el único lugar donde aparece una cookie de sesión o un sec-websocket-protocol acordado para ese transporte, ya que los frames no tienen cabeceras. Un resultado de gRPC informa de sus metadatos en su propio detalle grpc en su lugar.

Un grupo que no pudo iniciarse en absoluto informa de error en lugar de resultados y cuenta como un fallo — de lo contrario, una ejecución con un grupo muerto se leería como verde.

Campos de nivel de ejecución: parseErrors y parseFailures nombran archivos que no se pudieron analizar, warnings recoge cualquier otra cosa que valga la pena ver.

Scripts

Los scripts se ejecutan en un contexto V8 dentro de un proceso bifurcado (ver Seguridad). Ambos tipos son funciones asíncronas, así que await de nivel superior funciona.

Pruebas y post-respuesta reciben test(), expect(), res y bru:

API

Notas

test(name, fn)

Envuelve aserciones. Necesario para que una se informe

expect(v)

Estilo Chai: .to.equal, .include/.contain, .match, .have.property/.lengthOf/.keys, .be.above/.below/.least/.most/.oneOf, .throw, y .to.not.* para cualquiera de ellos

res.getStatus() res.getStatusText()

res.getHeader(name) res.getHeaders()

La búsqueda de cabeceras no distingue entre mayúsculas y minúsculas

res.getSetCookies()

Cookies que la respuesta estableció

res.getBody()

Ya analizado cuando el subtipo del tipo de medio es json o termina en +json

res.getResponseTime()

ms

res(path, ...fns)

El lenguaje de consulta de Bruno sobre el cuerpo: res("data.pets..name") desciende a cada name, [0] indexa, [?] filtra o mapea con un callback. También es válido como lado izquierdo de una aserción, donde la sintaxis no podría aparecer desnuda

bru.setVar(name, v) bru.getVar(name)

Pasa valores a peticiones posteriores como {{name}}

bru.sleep(ms)

También setTimeout/setInterval y sus clear*

atob(s) btoa(s)

base64, en ambos tipos de script. Suficiente para leer un payload de JWT sin una segunda petición

Scripts de pre-petición reciben req y bru en su lugar — aún no hay respuesta. Mutar req cambia lo que se envía: req.getUrl(), req.setUrl(), req.getMethod(), req.getHeader(), req.setHeader(), req.getHeaders(), req.getBody(), req.setBody().

Dos cosas que pillan a la gente

Envuelve las aserciones en test(). Una expect() que pasa desnuda nunca se registra, así que la ejecución informa de "tests": [] mientras la petición cuenta como superada — verde sin nada asertado. El runner lo detecta y lo dice en los warnings de ese resultado. Una aserción fallida desnuda no es silenciosa: lanza y se informa como error de script.

test("status is 200", function() {          // ✅ recorded
  expect(res.getStatus()).to.equal(200);
});

expect(res.getStatus()).to.equal(200);      // ❌ runs, passes, reported nowhere

No hagas JSON.parse(res.getBody()). Ya es un objeto siempre que el subtipo del tipo de medio sea json o lleve el sufijo +jsonapplication/json, text/json, application/vnd.api+json — así que analizarlo de nuevo lanza SyntaxError: "[object Object]" is not valid JSON. Lee los campos directamente. Si un endpoint puede devolver cualquiera de los dos, ramifica: typeof b === "string" ? JSON.parse(b) : b.

Leer una reclamación de un token no necesita una segunda petición. atob y btoa están ambos presentes, bajo los nombres que usa el propio sandbox de Bruno, así que el baile base64url habitual funciona:

test("the token is for the user we logged in as", function() {
  const payload = res.getBody().token.split(".")[1];
  const claims = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
  expect(claims.uid).to.equal(bru.getVar("expectedUid"));
});

Buffer no está disponible. Es una clase de host con capacidades que un sandbox no debería entregar, y un sustituto fiel sería un fake cuyas lagunas encontrarías de una en una; una referencia desnuda lanza Buffer is not defined, que se informa como error de script en lugar de comportarse mal silenciosamente.

Dormir cuenta contra el tiempo de espera del script — settings.timeout, 5000 ms cuando no se establece. await bru.sleep(10000) bajo el valor por defecto informa de un tiempo de espera agotado en lugar de esperar.

Entornos y variables

Un entorno es environments/<name>.yml en la colección:

name: dev
variables:
  - name: baseUrl
    value: https://api-dev.example.com
  - name: apiKey
    value: dev-key-123
  - name: skipped
    value: whatever
    disabled: true

Las herramientas toman las variables como un objeto plano ({"baseUrl": "..."}) y escriben ese array por ti.

{{name}} se sustituye en urls, cabeceras, cuerpos y autenticación. Las variables deshabilitadas se omiten; las no resueltas se dejan como están escritas y se nombran en los warnings de la ejecución.

Precedencia, de menor a mayor: archivo de entorno → variables de ejecución → vars de la propia petición → bru.setVar durante la ejecución. Esto coincide con el comportamiento de --env-var de Bruno.

Una variable puede construirse a partir de otras: base_url: "https://{{host}}/{{stage}}" se resuelve como lo hace bajo bru run, usando el propio interpolate de Bruno. Una excepción, deliberada: un valor capturado de una respuesta — por bru.setVar o un bloque vars de post-respuesta — se inserta como texto y nunca se vuelve a escanear, así que una respuesta que haga eco de key={{api_key}} no puede hacer que la siguiente petición envíe tu clave.

Generadores. {{$guid}}, {{$timestamp}}, {{$randomEmail}} y el resto de las ~120 variables dinámicas de Bruno funcionan en urls, cabeceras, parámetros de consulta, cuerpos y autenticación. No son variables: nada las declara, cada aparición produce su propio valor, y ninguna se informa como no resuelta. Una palabra clave a la que ningún generador responde — {{$gid}} — se deja como está escrita y se nombra en los warnings, así que un error tipográfico sigue saliendo a la superficie. En un cuerpo JSON o un bloque de variables de GraphQL, el valor generado se escapa, así que un generador que emite un salto de línea ({{$randomLoremParagraphs}}) deja el documento analizable.

Secretos: ninguno de los formatos de Bruno almacena el valor de un secreto — solo su nombre. Así que pasa los secretos como variables de ejecución, que permanecen en memoria y nunca se escriben en un archivo.

Un nombre de entorno es un nombre, no una ruta. Cualquier cosa que contenga un separador se rechaza.

Archivos de informe

run_collection devuelve sus resultados como JSON, que es lo que lee un agente. Los otros dos consumidores de una ejecución de prueba leen archivos, así que report los escribe:

{ "collectionPath": "/path/to/collection",
  "report": { "junit": "reports/junit.xml", "html": "reports/run.html" } }

Nombra cualquiera de los dos formatos o ambos. El resultado lleva entonces reports, una entrada por archivo escrito, con su ruta absoluta y su tamaño en bytes.

Las rutas están confinadas a la colección. Una ruta que se resuelva fuera de ella se rechaza y la razón se convierte en un warning de la ejecución; la ejecución en sí misma sigue teniendo éxito, porque los resultados son lo que se pidió y el archivo es un subproducto. Copia el archivo después si tu pipeline recoge informes de otro lugar — escribir donde un llamador apunte es una autorización mucho mayor que ejecutar sus peticiones. Los directorios padre que falten dentro de la colección se crean, y un informe existente se sobrescribe.

El XML de JUnit sigue a bru run --reporter-junit: un <testsuite> por petición, un <testcase> por aserción o prueba, y una petición que falló se informa como error de suite. Cuatro cosas que hace de manera diferente, cada una porque la alternativa es un informe que se lee más verde que la ejecución:

  • Una solicitud que se ejecutó y no verificó nada obtiene un testcase omitido que lo indica, en lugar de una suite vacía. Una suite vacía es invisible en cualquier resumen de CI, que es exactamente la lectura de "pasó en verde sin comprobar nada" que el contador requestsWithoutTests existe para exponer.

  • Un archivo de solicitud que no se pudiera analizar, una solicitud nombrada que no resolviera a nada y un grupo que fallara obtienen cada uno una suite propia. Un informe que solo enumera lo que se ejecutó dice que se ejecutó un subconjunto sin decir que era un subconjunto.

  • La etiqueta de un grupo nombrado se integra en el nombre de la suite, ya que JUnit no tiene concepto de etiqueta de grupo, y dos identidades que ejecutan la misma solicitud serían indistinguibles de otro modo.

  • Sin atributo hostname. Upstream escribe el nombre de la máquina en el archivo; estos informes están pensados para confirmarse.

El informe HTML es propio de Bruno, renderizado por @usebruno/common, con los grupos de ejecución como sus iteraciones: una ejecución con dos identidades se lee como dos secciones. Dos cosas que conviene saber: la página incrusta los datos de la ejecución pero carga Vue y naive-ui desde unpkg.com, por lo que necesita acceso a red al abrirse y no muestra nada sin conexión; y su panel de solicitud está vacío, porque un resultado no conserva la solicitud tal como se envió. Las aserciones y las pruebas de script comparten una sola lista por la misma razón: un resultado no las distingue.

Un informe contiene lo que contienen los resultados, en disco: cuerpos de respuesta incluidos, cabeceras de respuesta enmascaradas exactamente como están en el JSON.

Formatos

Archivo marcador en la colección

Formato

opencollection.yml

YAML — se comprueba primero

bruno.json

BRU (heredado)

ninguno

YAML

Las colecciones nuevas son YAML a menos que pases format: "bru".

Los archivos de solicitud .yaml se leen como YAML, exactamente igual que .yml, porque otras herramientas adyacentes a Bruno los escriben. Pero la propia aplicación de Bruno y bru run no reconocen la extensión, por lo que cada archivo .yaml leído se menciona en las advertencias de la ejecución: un paso silencioso sería una ejecución en verde de una solicitud que Bruno no puede ver. Cambia el nombre a .yml para eliminarlo. Nada de lo que escribe este servidor usa .yaml.

Solicitudes gRPC y WebSocket

Una colección puede contener solicitudes gRPC y WebSocket junto a las HTTP. Este servidor las lee, conserva e informa: read_request devuelve el tipo, el destino, el método y la ruta proto para gRPC, su bloque de metadatos y cuántos mensajes están almacenados; list_requests las enumera; y editar cualquier solicitud de la colección ya no las destruye. Antes, ambos formatos descartaban el bloque de destino, las credenciales y todos los mensajes almacenados, por lo que un solo write_request sobre una solicitud no relacionada reescribía el archivo sin ellos.

run_collection ejecuta ambas. Una solicitud gRPC realiza una llamada unaria contra el servicio que declara su .proto; una solicitud WebSocket abre el socket, envía las tramas que almacena el archivo y registra lo que vuelve hasta alcanzar un límite. Cada una informa de su propio detalle: un resultado gRPC lleva el código de estado gRPC, la cadena de detalles y los metadatos finales censurados, y un resultado WebSocket lleva la transcripción, el stop_reason que la terminó y si fue truncado. El código gRPC vive en su propio campo y nunca se asigna al status del resultado, porque el OK de gRPC es 0 y 0 es el centinela de rechazo de esta API: una llamada exitosa y un rechazo de seguridad serían indistinguibles en el campo que se lee primero.

Una sesión WebSocket no tiene fin natural, por lo que está acotada, y cada límite es configurable por ejecución mediante el argumento websocket de run_collection:

Límite

Predeterminado

Qué hace

maxMessages

50

Tramas entrantes registradas antes de detenerse

maxDurationMs

5000

Techo de tiempo real para una sesión

idleTimeoutMs

1500

Silencio que termina una sesión; 0 espera al techo

sendIntervalMs

0

Intervalo entre los mensajes que envía una solicitud; 0 los envía en un solo tick

includePayloads

false

Registrar el contenido de las tramas, no solo tamaño y temporización

maxFrameBytes

65536

Techo por trama para la carga útil registrada

maxTranscriptBytes

1048576

Techo acumulativo, contado desde el tamaño en la red

engineIoKeepalive

false

Responder a un 2 de engine.io con un 3

El techo de tiempo real es un límite de seguridad más que una programación, por lo que idleTimeoutMs es lo que normalmente termina una sesión: una vez que no ha llegado nada durante 1500 ms se detiene e informa stop_reason: "idle", que no se cuenta como truncamiento porque no se activó ningún bit de límite y el techo quedó sin gastar. El reloj se arma con la primera trama, no al conectar, por lo que una solicitud solo de escucha que no crea mensajes aún espera maxDurationMs a un interlocutor que quizá aún hable. Ponlo a 0 para un protocolo cuyos intervalos son más largos que sus respuestas.

sendIntervalMs es lo que hace alcanzable un protocolo de enviar-esperar-enviar. Con el valor predeterminado de 0, los mensajes de una solicitud salen todos en un solo tick, por lo que cada respuesta llega después del último y el intercambio no tiene orden sobre el que asertar; pon un intervalo y la transcripción lleva cada respuesta entre los envíos a los que pertenece, en el desplazamiento en el que realmente llegó. Dos consecuencias que conviene conocer. maxDurationMs tiene que cubrir toda la secuencia espaciada: una sesión detenida a mitad nombra los mensajes que nunca salieron, por su nombre creado, en lugar de dejar una transcripción con un envío de menos que se lea como un interlocutor que dejó de responder. Y el límite de inactividad no se arma mientras la secuencia aún está saliendo, por lo que un sendIntervalMs mayor que idleTimeoutMs es seguro: el intervalo que una solicitud deja deliberadamente entre sus propios mensajes no es el silencio del interlocutor.

Un subprotocolo se crea como una cabecera Sec-WebSocket-Protocol en la solicitud, separada por comas si hay más de uno, y se negocia en el handshake; el que el servidor aceptó vuelve en los response_headers de ese resultado. No hay un campo separado para ello, aquí ni en Bruno. Escribir la cabecera solía ser peor que omitirla: la librería valida la respuesta del servidor contra la lista que se le dio en la conexión, por lo que un servidor que hizo exactamente lo que la cabecera pedía veía rechazado su handshake por ofrecer un subprotocolo que nadie solicitó. Una Sec-WebSocket-Version creada se respeta de la misma manera, por la misma razón.

Cada entrada de la transcripción dice qué tipo de trama era — text, binary, ping, pong o close —, lleva el title creado de un mensaje que la sesión envió y, en una trama de cierre, el close_code que dio el interlocutor, con su motivo como carga útil de esa entrada: 1000 es una despedida ordinaria, 1006 un interlocutor que desapareció sin una, 1008 un rechazo, 1011 un error del servidor. Las tramas de control no cuentan para maxMessages, o un interlocutor que hace ping una vez por segundo terminaría una sesión por sí mismo e informaría count para una que no recibió respuesta. La carga útil de una trama binaria es base64 y bytes es el tamaño real en la red para cada tipo. Un script posterior a la respuesta ve los mismos campos, porque la transcripción es lo que es res.body en este transporte.

Aserción sobre un resultado gRPC o WebSocket

Ambos transportes ejecutan scripts posteriores a la respuesta y de prueba, y res está formado de modo que hay una sola cosa que aprender en lugar de dos. Lo que difiere de HTTP merece exponerse sin rodeos, porque adivinarlo mal hace una prueba que no puede fallar.

En una solicitud WebSocket:

  • res.getBody() es la transcripción: el mismo array que lleva el resultado, entregado al script como una estructura en lugar de como texto JSON. res.rawBody conserva la forma serializada.

  • res.getStatus() es siempre 0. Una sesión no tiene estado, e inventar uno sería peor que no tener ninguno. El resultado está en res.statusText, que lleva el motivo de detención (count, timeout, bytes, closed o error).

  • Por lo tanto, una aserción WebSocket lee tramas y statusText. Una prueba escrita contra res.getStatus() aserta sobre una constante.

test("the server answered our subscribe", function() {
  const inbound = res.getBody().filter(f => f.direction === "in" && f.type === "text");
  expect(inbound.length).to.be.at.least(1);
  expect(inbound[0].payload).to.contain('"subscribed"');
  expect(res.statusText).to.equal("count");
});

Las cargas útiles que ve un script son siempre las reales, diga lo que diga includePayloads. Ese indicador condiciona la transcripción en el resultado, no la de res, porque las tramas salientes se registran después de la interpolación de {{var}} y un resultado devuelto por defecto no debe llevar cada secreto que pasaste. Esta es la división que HTTP ya tiene: res.body siempre contiene el cuerpo completo mientras que response_body está condicionado por includeResponseBody. Significa que includePayloads: false junto con aserciones de contenido es la forma prevista para CI, no una solución alternativa: las aserciones comprueban las cargas útiles, y lo que vuelve contiene solo dirección, temporización y tamaños.

En una solicitud gRPC, res está más cerca de HTTP: res.getStatus() es el código de estado gRPC (0 es OK), res.statusText son los details propios del servidor cuando los proporcionó y el nombre canónico del código en caso contrario, res.getBody() es el mensaje de respuesta analizado, y los trailers de respuesta llegan como las cabeceras.

includePayloads está desactivado por defecto como propiedad de seguridad, no como preferencia: las tramas salientes se registran después de la sustitución de {{var}}, por lo que registrarlas por defecto escribiría cada secreto pasado en variables en un resultado que se devuelve por defecto. engineIoKeepalive está desactivado por una razón relacionada: pone en la red una trama que la solicitud no creó — y, incluso cuando está activado, solo responde después de que realmente se haya visto una trama OPEN.

Una solicitud WebSocket ahora se puede crear en lugar de copiar. write_request acepta kind: "websocket" con una url y websocket.messages, y rechaza los campos para los que ese transporte no tiene lugar: un método HTTP, un cuerpo, parámetros de consulta, parámetros de ruta. Cada mensaje lleva content y, opcionalmente, un title y un type de text o binary; un mensaje sin título se nombra message 1, message 2 por posición, exactamente como Bruno nombra uno. Las cabeceras, la autenticación, assert, vars, settings y los scripts funcionan como lo hacen para una solicitud HTTP, y el archivo escrito es byte a byte idéntico a lo que Bruno escribe para la misma solicitud en ambos formatos — demostrado contra el propio escritor de upstream, no contra una ida y vuelta a través de nuestro analizador.

Un campo se registra de forma diferente en los dos formatos. selected: false marca un mensaje como creado pero no enviado. .yml escribe el false. .bru expresa solo la mitad verdadera: el escritor de upstream emite el indicador cuando está establecido y nada cuando no lo está, y su lector resuelve un indicador ausente a false — por lo que en ese dialecto un mensaje deseleccionado y uno sin marcar son el mismo mensaje, y ninguno se envía. Una ejecución sigue esa lectura, lo que significa que un mensaje .bru escrito a mano se envía solo si dice selected: true; cada mensaje omitido por no tenerlo se menciona en las advertencias del resultado, por lo que una solicitud que ahora no envía nada dice por qué en lugar de informar una sesión vacía. Crear un mensaje deseleccionado en una colección .bru lo escribe sin indicador, exactamente como hace Bruno, por lo que el archivo se comporta como se pidió; lo que pierde el dialecto es solo el informe, ya que al leer la solicitud de vuelta encuentra el indicador ausente en lugar de false.

Una solicitud gRPC se crea de la misma manera, con kind: "grpc": una url y, bajo grpc, el method totalmente cualificado, el protoPath, el methodType y los messages. También rechaza un método HTTP, un cuerpo, parámetros de consulta y parámetros de ruta. Tres cosas sobre ella merecen conocerse antes de escribir una.

Las cabeceras pasan a ser metadata, la única superficie de cabecera de ese transporte: un bloque headers en una petición gRPC es algo que el lector gRPC de Bruno nunca mira, así que el argumento headers se escribe como metadata. protoPath debe existir ya dentro de la colección y se guarda relativo a ella sea cual sea la grafía que se dé, porque una ruta absoluta es la disposición de directorios del operador comprometida en un archivo compartido; se rechaza una ruta que resuelva fuera de la colección, incluidos los enlaces simbólicos, así como una cuyos imports salgan de ella por muchos saltos que dé (un import conocido de google/protobuf/ no es un archivo y no se rechaza). Y los cuatro valores de methodType se aceptan porque Bruno escribe los cuatro, pero solo unary se ejecuta aquí, así que los otros tres redactan un archivo que Bruno puede abrir y run_collection rechazará por nombre. Igual que en WebSocket, los bytes son idénticos a los del propio escritor de Bruno en ambos formatos, incluido el desacuerdo de los dos dialectos sobre la ortografía: .bru escribe protoPath dentro del bloque grpc, .yml escribe protoFilePath.

write_request edita ambos transportes. Se aplican url, headers, auth, assert, vars, settings, name y sequence, igual que el objeto anidado websocket o grpc — sus mensajes, y para gRPC el method, protoPath y methodType. Cada campo se escribe donde ese transporte lo guarda, así que una edición de cabeceras gRPC aterriza en metadata y nunca escribe un bloque headers. Todo lo que la edición no nombre vuelve byte a byte idéntico, lo que aquí importa más que en HTTP: una edición regenera todo el archivo desde un modelo analizado, así que cualquier cosa que el modelo no lleve desaparece sin mensaje.

Lo que sigue rechazado es lo que el transporte no tiene dónde guardar — un método HTTP, un body, parámetros de consulta, parámetros de ruta y el objeto del otro transporte —, por su nombre, dejando el archivo sin tocar ni un byte. Rechazar también url, headers y auth solía ser el comportamiento, lo que significaba que el destino de una petición WebSocket no podía cambiarse durante toda la vida del archivo.

Un script de pre-petición corre en ambos transportes y alcanza lo que cada uno tiene. bru.setVar se respeta y el valor llega a los {{placeholders}} de esa misma petición, así que un script puede calcular un nombre de sala, un tema o un destino y usarlo. req.setUrl sustituye el destino. req.setHeader escribe en la superficie de cabeceras de ese transporte: las cabeceras de handshake de un WebSocket, o las de un gRPC — que es la misma superficie, porque grpc-js las pone en el cable como metadatos. Un script que lanza una excepción detiene la petición antes de que se contacte nada, y el error se informa tal cual. req.getUrl() y req.getHeaders() leen el destino sustituido y las cabeceras; las credenciales que el transporte calcula no están entre ellas, porque el script corre antes de aplicar auth. Lo que sigue rechazado es req.setBody(), que en su lugar avisa: una sesión WebSocket envía una lista de mensajes y una llamada gRPC unaria envía uno solo, así que no hay nada para que un único valor sustituya, y el archivo no autorizó esos bytes.

Ambos transportes se cargan perezosamente, y eso se verifica, no se asume: un test registra todos los módulos que el proceso real carga, y una ejecución solo HTTP no debe nombrar ni @grpc/grpc-js ni ws. Medido, solo carga undici.

Se rechazan dos cosas. Una petición cuyo tipo declarado y bloque no coinciden — grpc: con un bloque http: — porque el tipo decide lo que un lector informa y lo que otro escribe: el archivo parecería tener dos métodos a la vez. Y un .bru cuya methodType no es unary, o un .yml con stream con más de un mensaje, se rechazan por nombre; en Bruno hay un único escritor para ambos transportes, así que en la práctica no hay dialécticas que generar. Lo que aún está rechazado es lo que el transporte genuinamente no tiene donde guardar: un método HTTP, un cuerpo, parámetros de consulta, parámetros de ruta y el objeto del otro transporte —por su nombre—, dejando el archivo byte a byte sin cambios. Rechazar también url, headers y auth solía ser el comportamiento, lo que significaba que el destino de una petición WebSocket no podía cambiarse en toda la vida del archivo.

Un script de pre-petición corre en ambos transportes. bru.setVar se respeta y el valor llega a los {{placeholders}} de esa misma petición, así que un script puede calcular un nombre de sala, un tema o un destino y luego usarlo. req.setUrl reemplaza el destino. req.setHeader escribe en la superficie de cabeceras de ese transporte: las cabeceras de handshake de un WebSocket o los metadatos de una llamada gRPC — que es la misma superficie, porque grpc-js pone los metadatos en el cable como cabeceras HTTP/2. Un script que lanza un error detiene la petición antes de conectarse, y el fallo se reporta como es. req.getUrl() y req.getHeaders() leen el destino ya sustituido y las cabeceras propias de la petición; las credenciales que el transporte calcula no están entre ellas, porque la autenticación se aplica después del script. Lo único que no se honra es req.setBody(), que avisa en su lugar: ninguno de los dos transportes envía un único cuerpo — una sesión WebSocket envía una lista de mensajes y una llamada gRPC unaria envía un mensaje tipado —, así que no hay nada que un cuerpo único pueda reemplazar, y adivinar escribiría en el cable algo que el archivo nunca autorizó.

Ambos transportes se cargan de forma perezosa, y esto se verifica, no se asume: el servidor real registra cada módulo que resuelve y falla si una ejecución solo-HTTP carga @grpc/grpc-js o ws. Medido, una ejecución solo-HTTP nombra undici y ninguno de los otros dos.

Dos cosas se rechazan en lugar de adivinarse. Un archivo cuyo tipo declarado y bloque no coinciden (type: grpc con un bloque http:) es un error que nombra ambos, porque el tipo decide lo que un lector informa mientras que el bloque decide a qué se conecta el ejecutor. Y una petición cuya url es vacía se rechaza al escribir: algunos lectores la informan como tal, otros la omiten, y ninguno la ejecuta.

Cinco cosas no se construyen. Llamadas gRPC en streaming y sesiones WebSocket sostenidas harían que el resultado de una petición dependiera de cuándo se leyera, y cada petición devuelve un valor contrastable. Reflexión gRPC buscaría el esquema en el cable; estas peticiones son autónomas. Autenticación por proxy no llega a estos transportes: undici no expone una API de proxy y ws no usa una, así que no hay superficie para ello. Y un bloque socket.io o un bloque mqtt inventaría un formato de archivo que Bruno no ha elegido, y eso es una migración en el momento en que Bruno lo elija.

socket.io no necesita bloque, porque es una convención de empaquetado sobre WebSocket, no un protocolo aparte. Medido contra socket.io 4.8.3, una petición ws normal lo consigue:

  1. Conectar a ws://host:port/socket.io/?EIO=4&transport=websocket. Ambos parámetros de consulta son obligatorios: EIO=4 selecciona la versión 4 del protocolo Engine.IO, y transport=websocket evita que el servidor espere un handshake de polling HTTP.

  2. El servidor envía 0{...}, el paquete OPEN de Engine.IO. Su payload trae sid, pingInterval y pingTimeout en milisegundos.

  3. Enviar 40 para unirse al namespace por defecto — nada funciona antes de esto. Un namespace con nombre es 40/namespace,.

  4. El servidor responde 40{"sid":"…"}.

  5. Enviar un evento como 42["nombre-evento",payload]: 4 es MESSAGE, 2 es EVENT, y luego un array JSON cuyo primer elemento es el nombre del evento.

  6. El servidor envía 2 (PING) cada pingInterval y corta a un cliente que no responda 3 (PONG) dentro de pingTimeout. Pon websocket.engineIoKeepalive en run_collection si una grabación dura más que esa ventana; está apagado por defecto y solo responde después de haber visto un frame OPEN real.

Los pasos 1 a 5 son frames que el archivo de petición ya guarda, así que solo el paso 6 necesita algo del runner. Esto está fijado a EIO=4 — Engine.IO v2 y v3 enmarcan distinto. Los acks (42<id>[…] respondidos con 43<id>[…]) y los adjuntos binarios (un placeholder 45 seguido de frames binarios separados) se pueden escribir a mano y en la práctica son desagradables.

Seguridad

SSRF. Cada URL saliente, incluido cada salto de redirección, se resuelve y se comprueba. Se rechazan las direcciones privadas, de bucle local, link-local y demás reservadas, y las aprobadas quedan fijadas para la petición de modo que el nombre no pueda resolverse a otra cosa entre medias. Un rechazo se informa por petición como error SSRF blocked con estado 0.

Los scripts corren en un contexto V8 dentro de un proceso hijo con el entorno higienizado: su stdio no está conectado al del servidor MCP, de modo que no puede escribir en el flujo JSON-RPC. Si un script se cuelga, el hijo recibe SIGKILL; la promesa no se puede cancelar desde fuera. Es defensa en profundidad, no un límite de lo que el código puede hacer dentro del hijo.

Rutas. Las referencias a archivos deben permanecer dentro de la colección. collectionRoot debe contener la ruta de la colección. Los nombres de entorno no pueden contener separadores.

Subidas de archivos. Una parte form-data nombra una ruta en el disco del servidor, así que está confinada: solo puede leerse bajo la raíz de la colección, el directorio personal del operador, el directorio temporal del sistema o un directorio que el operador añada. Además, cualquier componente de la ruta que empiece por . se rechaza — así ~/.ssh/id_rsa, .env y .aws quedan ilegibles aunque el directorio personal esté permitido. Las rutas relativas se resuelven contra la raíz de la colección.

Vías de escape del operador, todas desactivadas por defecto:

Variable

Descripción

BRUNO_SSRF_ALLOWLIST

Nombres de host exactos y separados por comas y/o rangos CIDR permitidos a pesar de ser privados. Una entrada de nombre se compara contra la grafía en la URL; una entrada de dirección se compara contra lo que la URL resuelve, y también permite un nombre que de otro modo estaría bloqueado cuando todas sus direcciones resueltas están en la allowlist. Se lee una vez al arrancar y nunca la afectan los argumentos de la herramienta; los comodines se rechazan

BRUNO_UPLOAD_DIRS

Directorios adicionales de los que las subidas pueden leer

BRUNO_PROXY_ALLOWLIST

Hosts permitidos a usar el proxy de una colección

BRUNO_INSECURE_TLS_HOSTS

Hosts permitidos a saltarse la verificación de certificados

BRUNO_ENV_FILE

Ruta a un archivo JSON de variables de entorno; si no existe, MCP_* se leen del entorno del proceso.

BRUNO_DNS_TIMEOUT_MS

Tiempo de espera para la resolución de nombres

Todo el tráfico de salida es solo HTTPS, y los certificados se verifican contra la lista de confianza del sistema — no hay manera de desactivarlo por petición. El proxy es una función de la colección, no del runner, así que un proxy en la colección no se aplica a menos que el operador lo permita por servidor, y es la única manera de desviar el tráfico. Esto está pensado como una capa, no como una frontera: la política se aplica en el proceso, no en la red. El objetivo es que una petición que un operador no querría no ocurra por accidente, no que no pueda ocurrir con intención.

Lo que un agente puede hacer está delimitado por lo que run_collection puede hacer, que a su vez es lo que Bruno puede hacer. La intención del servidor está en esos cinco campos: https, tu propio host, tu propio script de corredor, tu propia higiene.

- `BRUNO_TLS_CERT`/`BRUNO_TLS_KEY`: par opcional para que el servidor hable TLS presentando ese certificado.
- `BRUNO_COLLECTION_ROOT`: root para las referencias a colecciones.

El proxy se lee de la petición: proxyUrl y proxyUrl en el objeto request de una petición HTTP, o el ajuste de colección equivalente. Los proxies no autenticados son la norma; ninguno de los transportes soporta credenciales en la URL, así que el runner no intenta adivinarlo.

Los archivos .bru son YAML de Bruno y pueden contener secretos. El runner los lee del disco para ejecutar, así que el operador ya debe haberlos puesto ahí; el MCP no añade una vía de lectura aparte.

No. El pipeline de solicitudes está implementado aquí — variables, autenticación, cookies, redirecciones, aserciones, scripts, orden de dependencias — así que nada invoca a bru fuera del proceso y nada necesita el binario en el PATH. Esa es también la razón por la que existe el trabajo de paridad anterior: una implementación independiente tiene que medirse deliberadamente contra la original.

¿Admite archivos opencollection .yml, o solo .bru?

Ambos, y detecta cuál usa una colección en lugar de preguntarte. Las colecciones nuevas usan .yml por defecto; create_collection acepta format: "bru" si quieres el dialecto heredado. Donde los dos formatos discrepan de verdad — y discrepan —, cada uno se escribe igual que el propio escritor de Bruno lo escribe para ese dialecto.

Con todo, una colección es un dialecto u otro, no una mezcla: el manifiesto raíz lo elige, y Bruno lee solo esa extensión, así que una solicitud .yml dentro de una colección bruno.json es un archivo en un directorio en lo que a Bruno respecta. Este servidor sigue leyendo, escribiendo y ejecutando ese archivo — negarse te dejaría sin poder hacer la corrección, que es renombrar ese mismo archivo — pero toda herramienta que toca o lista uno te dice que Bruno no puede verlo, y lo nombra.

¿Puedo ejecutar esto en CI?

La colección que produce es una colección normal de Bruno, así que CI la ejecuta con bru run exactamente como si un humano la hubiera creado en la app. El servidor MCP en sí es para el bucle de autenticación y depuración, en el que hay un agente presente. También escribe archivos de informe JUnit XML y HTML — consulta Archivos de informe — de modo que una ejecución impulsada por un agente sigue dejando el artefacto que espera un panel de CI.

¿Reescribirá archivos que escribió la aplicación de Bruno?

Solo los campos que pediste cambiar. Una edición medines write_request es una fusión parcial: una clave que este servidor no modela se conserva en la salida donde el formato puede conservarla — .yml en su totalidad, y .bru siempre que su gramática tenga un bloque de diccionario para contenerla — y cada escritura se verifica contra el propio lector de Bruno en la suite de pruebas. Los borrorrados requieren un confirm: true explícito.

¿Qué clientes MCP funcionan?

Cualquiera de ellos — es un servidor stdio sencillo, sin código específico de cliente. Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Codex CLI, opencode, Zed, Cline, Continue, LM Studio, Gemini CLI, el MCP Inspector o tu propio cliente SDK. Ver Conectar un cliente para ver dónde guarda su configuración cada uno.

¿Qué pasa con mis secretos?

Las variables de entorno secretas permanecen en memoria durante la ejecución y nunca se escriben en un archivo — ningún dialecto guarda el valor de un secreto en el disco, que es el diseño de Bruno, no una limitación añadida aquí. Las credenciales se redactan de los resultados devueltos al agente, incluida una colocada en un parámetro de consulta. Los scripts se ejecutan en un sandbox de V8 separado con un entorno limpio. Ver Seguridad.

¿Es esto lo mismo que el bruno-mcp original?

Empezó como un fork de macarthy/bruno-mcp, que ha está inactivo desde julio de 2025 y generaba archivos de colección sin ejecutarlos. Todo lo anterior — el ejecutor, los lectores, ambos dialectos, el control de paridad, el sandbox — se construyó después del fork. El repositorio ahora es Ostico/bruno-mcp-studio (anuncio); el nombre del paquete npm no cambia, @ostico/bruno-mcp.

¿Está afiliado con Bruno?

No. Bruno es un producto de usebruno y el nombre y las marcas comerciales pertenecen a ellos. Este es un proyecto de la comunidad que le cosas de Bruno, y no está respaldado ni afiliado con usebruno.

Los los propios de Bruno anunciaron un servidor MCP oficial, usebruno/bruno-mcp, en agosto de 2026. No es esto, y no compite por ese papel — ver ¿Qué servidor MCP de Bruno debería usar? para saber cuál es bueno para cada cosa.

Actualizando a la 2.5.0 — se fusionaron cuatro herramientas en write_request

Cuatro herramientas ya no están. Lo que hacían, lo hace write_request, con la misma semántica: una edición sigue actualizando solo los campos que pasas y respeta todos los demás campos, byte a byte.

Eliminado en 2.5.0

En su lugar, llama a

create_request, modify_request

write_request

create_test_suite, create_crud_requests

write_request con requests, y dependencies para the order

delete_request no ha cambiado y sigue aquí; ahora también acepta filePaths para borrar un conjunto de una sola llamada.

Falla de forma segura. Un cliente pide la lista de herramientas al conectar y se actualiza con los nombres actuales, así que un agente que lee lista no se ve afectado. Llamar a un nombre eliminado es un error de herramienta desconocida o un aviso de permiso — nunca un resultado incorrecto, nunca una llamada silenciosa.

Lo que de verdad hay que editar es la conf. que fija un nombre: un argumento --allowedTools mcp__bruno-mcp__create_request, una entrada de permisos de settings.json, un matcher de hooks, y cualquier texto en un CLAUDE.md o un skill que indique a un agent que llame a una o a las cuatro por su nombre.

Versiones

Los nombres de las herramientas se descubren desde tools/list al conectar, no se enlazan, así que renombrar o quitar una herramienta se publica en una versión menor. Una llamada se comporta exactamente igual o no no existe, y el segundo caso es ruidos.

Cambiar lo que hace una llamada que no cambia se publica en versión mayor. Ese es el tipo peligroso: 2.0.0 convirtió las carpetas en grupos de ejecución, así que quien no cambió nada obtuvo un resultado distinto. Nada más silencioso que eso merece una major, porque las majors que no avisan de peligro enseñan a ignorarlas.

Actualización desde 1.x

  • requestPath y folder han desaparecido. Ambas se convierten en requests, un array ordenado de archivos y/o directorios.

  • Ya no hay un array results de nivel superior. Ley cada result.groups[0].results.

  • parallel: true antes aislaba cada carpeta. Ya no: sin groups, toda la selección es una solo grupo que comparte un solo almacén y una sola cookie jar. Nombra las carpetas como grupos separados para mantener el comportamiento anterior.

  • seq ya no restringe la ejecución. Solo es el orden por defecto y el orden de informes.

  • Un requests: [] vacío no ejecuta nada. Antes se ejecutaba toda la colección.

  • Exámenes eliminados: BruGenerator, generateBruFile, createBasicBruFile.

Detalle completo en CHANGELOG.md.

Desarrollo

npm run build       # compile to dist/
npm test            # full suite
npm run test:unit   # unit only
npm run typecheck
npm run lint

Usa npm, no rilos — el lockfile es de npm y CI ejecuta npm ci.

Ver CONTRIBUTING.md para lo que CI comprueba, las de convenciones de commit y la signación con (DCO) que cada commit necesita.

Licencia

MIT — ver LICENSE. Los contribuciones quedan bajo los mismos términos, con una confirmación (DCO).

Enlaces

Available Tools

21 tools
add_test_scriptAdd Test ScriptA

Add pre-request, post-response, or tests scripts to a Bruno request. Canonical scriptType values are pre-request/post-response/tests; the aliases before-request (→ pre-request) and after-response (→ post-response) are also accepted. Appends to any existing script of that type by default — pass scriptMode:"replace" to overwrite it, or use remove_script to clear it. Assertions must be wrapped in test("name", function() { ... }) to be reported. Scripts run as async functions: top-level await works, and bru.sleep(ms), setTimeout and setInterval are available. Time spent waiting counts against the script timeout (settings.timeout, default 5000ms); raise it with modify_request's settings argument.

ParametersJSON Schema
NameRequiredDescriptionDefault
scriptYesScript body. For post-response/tests, wrap every assertion in a test() block — test("status is 200", function() { expect(res.getStatus()).to.equal(200); }); — because only test() blocks are recorded in run_collection results. A bare passing expect() at the top level records nothing and the run reports "tests": []. res.getBody() returns the response already parsed into a JS object/array for application/json and +json content-types, so read fields directly (res.getBody().field) and do NOT JSON.parse() it.
scriptModeNoHow to write the script. "append" (default) concatenates onto any existing script of this type; "replace" overwrites it. Each of the three script types has its own slot in both .bru and .yml, so replacing one leaves the other two untouched.append
scriptTypeYesScript type. Canonical: pre-request, post-response, tests. Aliases: before-request (→ pre-request), after-response (→ post-response).
bruFilePathYesAbsolute path to the .yml or .bru request file. Get from list_requests or get_collection_stats.

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden and delivers richly: discloses appending behavior, alias mapping, async execution, top-level await support, timeout counting, and the critical requirement that assertions must be wrapped in test() to be recorded. This goes far beyond generic 'adds a script'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise yet information-dense, with the primary purpose front-loaded. Every sentence adds value—aliases, modes, async details, timeout. No fluff or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite no output schema, the description covers behavior, modes, aliases, runtime environment, timeout, and alternatives. It fully equips an agent to select and invoke the tool correctly for typical use cases.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds significant meaning beyond schema descriptions: explains script body expectations (test() wrappers, res.getBody() behavior), append/replace semantics, and scriptType aliases. It compensates and enhances parameter understanding.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb+resource: 'Add pre-request, post-response, or tests scripts to a Bruno request.' It clearly names the tool's action and object, and the mention of scriptType values distinguishes it from sibling tools like remove_script or modify_request.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly covers when to use append vs. replace, and points to remove_script as the alternative for clearing scripts. It also directs users to modify_request's settings argument for timeout adjustments, showing awareness of alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_collectionCreate Bruno CollectionA

Create a new Bruno API testing collection with configuration. outputPath is the PARENT directory and name is appended to it: outputPath "/work/apis" with name "Billing" creates the collection at /work/apis/Billing. Do not put the collection name in outputPath as well. The new collection is also added to the workspace registry, because list_collections reads that registry and not the disk: a collection that is not listed there is invisible to it and to the Bruno app. The result says whether it was registered and, if not, why.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYes
formatNoyaml
ignoreNo
baseUrlNo
outputPathYesAbsolute path of the PARENT directory to create the collection directory in. name is appended to it, so outputPath "/work/apis" with name "Billing" creates /work/apis/Billing. Passing "/work/apis/Billing" here would create /work/apis/Billing/Billing.
descriptionNo
workspacePathNoAbsolute path of the workspace.yml to register the new collection in. Defaults to the same one list_collections reads: BRUNO_WORKSPACE_PATH, or the Bruno app's workspace for this platform.
registerInWorkspaceNoSet false to create the collection without touching any workspace file. It will not appear in list_collections until something else registers it.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It discloses key side effects: the collection is added to the workspace registry, and list_collections reads that registry rather than the disk. It also explains the result indicates registration status and failure reasons. This is unusually transparent for a creation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise yet packed with necessary information. Every sentence earns its place: definition, path behavior, registry warning, and result explanation. It is well-structured and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with side effects and a registry integration, the description covers all essential aspects: creation location, naming, registration behavior, and result content. It also explains the default workspace path. Without an output schema, the description still gives a sufficient picture of what to expect.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 38%, so the description must compensate. It does so by explaining outputPath's parent-directory semantics with a concrete example and clarifying how registerInWorkspace and workspacePath interact with the registry. Other parameters like format, ignore, baseUrl, and description remain simple, but the critical, error-prone ones are well covered.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it creates a new Bruno API testing collection, with a specific verb and resource. It distinguishes itself from sibling tools like delete_collection and list_collections by focusing on the creation side-effect and registry integration.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear context on usage, including the exact path semantics and the registry behavior that affects visibility. It does not explicitly mention alternatives like create_crud_requests, but it provides enough guidance on how to operate correctly, including a 'do not' warning about duplicating the collection name.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_crud_requestsCreate CRUD RequestsA

Generate a complete set of CRUD operations for an entity: list, get by id, create, update, delete. All five inherit the collection or folder auth block unless you pass auth.

ParametersJSON Schema
NameRequiredDescriptionDefault
authNoDefaults to inherit, matching what Bruno itself gives a new request. Passing "none" is an opt-OUT that stops the collection auth block from applying to these five files, not an absence of opinion.
folderNo
baseUrlYes
entityNameYes
collectionPathYesAbsolute path to existing collection directory.

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description carries the full burden. It discloses an important behavior: 'All five inherit the collection or folder auth block unless you pass auth.' However, it omits other behavioral details such as file system effects, overwrite behavior, or error conditions, leaving some ambiguity for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two concise sentences, front-loaded with the primary action and a key behavioral note. Every word earns its place with no filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is moderately complex (5 parameters, nested auth object, no output schema), and the description is too sparse to be complete. It does not explain how entityName is used, what baseUrl does, where the requests are written, or what the tool returns. For a batch generation tool, this is a significant gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 40%, with descriptions for auth and collectionPath. The tool-level description adds minimal parameter clarification; it mentions 'auth' but does not explain entityName, baseUrl, or folder. With low schema coverage, the description should compensate, but it does not.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'Generate a complete set of CRUD operations for an entity: list, get by id, create, update, delete.' It names the specific resource (an entity) and distinguishes itself from sibling tools like create_request (single request) by producing five operations at once.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the primary use case: scaffolding full CRUD for an entity. It is clear when to use this tool, but it does not explicitly mention alternatives or exclusions (e.g., 'use create_request for a single request'). This qualifies as clear context without explicit alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_environmentCreate Bruno EnvironmentA

Create an environment file for a Bruno collection. Writes the WHOLE file, so it REFUSES a name that already exists rather than overwriting it — the error names the existing variables and says which ones a replace would delete, so you can choose between merging with update_environment, picking another name, and retrying with overwrite: true.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYes
overwriteNoReplace an environment that already exists. Without this an existing name is refused. Keys in the file that this tool does not model are preserved either way.
variablesYesEither a flat name-to-value map, or a list of variable objects when you need flags. Use the list form to declare a secret at create time: a secret variable is stored as a name only (no format persists a secret's value), so `value` is dropped for it.
collectionPathYesAbsolute path to existing collection directory.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does so thoroughly. It discloses that the tool writes the whole file, refuses existing names rather than overwriting, reports which existing variables would be deleted, and explains the remedy. This gives the agent a strong sense of the mutation risk and failure modes.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, focused sentence that front-loads the primary purpose, then packs the critical caveat and resolution options into a well-structured clause. There is no filler or repetition; every phrase earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the lack of annotations and output schema, the description covers the essential behavioral context: creation, overwrite refusal, error details, and interaction with update_environment. Combined with the rich parameter schemas, the agent has everything needed to invoke the tool safely and choose the right alternative.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 75%, so the schema already explains most parameters in detail, including the two forms of variables and the overwrite flag. The description adds practical meaning by tying overwrite to the retry path after a refusal, and by mentioning 'which ones a replace would delete,' which helps the agent interpret the consequences of setting overwrite.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Create an environment file for a Bruno collection.' It also distinguishes the tool from sibling update_environment by highlighting that existing names are refused rather than overwritten, clarifying its unique scope.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly provides guidance for the main conflict scenario: when a name already exists, the agent is directed to choose between merging via update_environment, picking another name, or retrying with overwrite: true. This names a concrete alternative and gives clear when-not-to-use context, going beyond a generic 'use for creation.'

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_requestCreate Bruno RequestA

Generate request files for API testing (supports .bru and .yml formats). Authors HTTP requests by default, WebSocket requests with kind "websocket" (url plus websocket.messages) and gRPC requests with kind "grpc" (url plus grpc.method, grpc.protoPath and grpc.messages); neither takes an HTTP method or a body. Supports multipart/form-data with file uploads and per-part contentType (body.type "form-data" with formData entries of type "file"), and inline scripts (pre-request/post-response/tests) so no separate add_test_script call is needed. Scripts run as async functions: top-level await works, and bru.sleep(ms)/setTimeout/setInterval are available, spending the script timeout (settings.timeout, default 5000ms) — raise it via the settings argument.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYes
authNo
bodyNo
grpcNogRPC-only fields. Applies to kind "grpc" and is refused otherwise. Headers given for a gRPC request are written as metadata, which is that transport's only header surface.
kindNoTransport. Defaults to "http". "websocket" and "grpc" take no method and no body; their payloads are websocket.messages and grpc.messages.
nameYes
varsNo
queryNo
assertNoDeclared assertions, evaluated on every run without needing a test() block.
folderNo
methodNoRequired for kind "http", refused for "websocket" and "grpc", which have no HTTP method. A gRPC request names its RPC method in grpc.method.
headersNo
scriptsNoInline scripts to persist with the request. Keys: pre-request, post-response, tests (aliases before-request/after-response accepted). Avoids a separate add_test_script call. IMPORTANT for tests/post-response: only assertions inside a test() block are reported. Write test("status is 200", function() { expect(res.getStatus()).to.equal(200); }); — a bare expect() at the top level still runs, but a passing one records nothing, so run_collection reports "tests": [] and the request looks green with no assertions. Available in scripts: res.getStatus()/getStatusText()/getHeader(name)/getHeaders()/getBody()/getResponseTime(), res.getStopReason()/getCloseCode()/getSessionTruncated() on a websocket request, which report the same session outcome the result does (all null or false on an HTTP response, which has no session), bru.setVar(name, value)/getVar(name)/getEnvVar(name)/hasEnvVar(name), and expect(actual) with .to.equal/.contain/.include, .to.have.property/.lengthOf, .to.be.a/.an, .to.be.above/.below/.at.least/.at.most (aliases .gt/.lt/.gte/.lte/.greaterThan/.lessThan), .to.be.within(min, max), .to.be.oneOf([...]), .to.match(/re/), .to.startWith/.endWith, .to.be.true/.false/.null/.undefined/.empty/.json,and .to.not.* negations. VARIABLES: bru.getVar(name) resolves environment and collection variables as well as anything a script set, so an environment variable needs no shadow copy to be readable. bru.getEnvVar(name) is narrower on purpose: it reads the environment layer only, so a runtime variable of the same name does not shadow it. There is no setEnvVar — nothing here writes an environment file. RETURN TYPE: res.getBody() returns the response already parsed into a JS object/array when the Content-Type is application/json or a +json type (raw text otherwise). Access fields directly — res.getBody().field — and do NOT JSON.parse() it, which throws SyntaxError: "[object Object]" is not valid JSON.
sequenceNo
settingsNoRequest-level settings: transport behaviour (timeouts, redirects, URL encoding), not payload. What reaches the file depends on the dialect, because Bruno's own two writers differ: a .yml request always carries a fully resolved settings block whether or not you pass one, while a .bru request carries only what you supply. On modify_request the fields are merged individually over the existing block, so setting one does not clear the rest. Note the encodeUrl field: in .bru, creating a block at all changes the URL-encoding default.
websocketNoWebSocket-only fields. Applies to kind "websocket" and is refused otherwise.
pathParamsNoValues for :name segments in the URL, e.g. { id: "42" } for /users/:id.
collectionPathYesAbsolute path to existing collection directory.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the transparency burden and does so well: it discloses that scripts run as async functions, that top-level await and bru.sleep/setTimeout/setInterval work, that the script timeout defaults to 5000ms and can be raised via settings, and that websocket/grpc requests reject HTTP method/body semantics. It does not mention overwrite behavior or error cases, but the disclosed behavioral traits go well beyond a minimal statement.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense and information-rich, front-loaded with the core purpose and format support, then branching into kind-specific and script-specific details. It earns its length for an 18-parameter tool, though a single long paragraph could be better structured with bullets or section breaks for skimmability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's high complexity (18 params, nested objects, no output schema), the description covers the essential orientation: file formats, three transport kinds, key body modes, script execution semantics, and timeout handling. It does not explain return values or overwrite behavior, but the combination of description and rich input schema gives an agent sufficient context to invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 50%, so the description should add semantic meaning, and it does: it explains how kind interacts with method/body, what form-data file uploads require, and how settings.timeout relates to script execution. The schema itself carries rich descriptions for many properties, and the main description ties key parameters together without simply repeating the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Generate request files for API testing' and immediately names the supported formats (.bru and .yml). It distinguishes itself from siblings by stating that inline scripts mean 'no separate add_test_script call is needed' and by clarifying the kind-specific behavior (http, websocket, grpc), making the tool's purpose unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear context on when to use create_request: to author request files, with explicit guidance on kind selection and the script inclusion alternative. It does not provide exhaustive when-not guidance against all siblings (e.g., create_crud_requests or modify_request), but the inline-script note and kind distinctions offer solid usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_test_suiteCreate Test SuiteC

Generate comprehensive test collections with multiple related requests

ParametersJSON Schema
NameRequiredDescriptionDefault
requestsYes
suiteNameYes
dependenciesNo
collectionPathYesAbsolute path to existing collection directory.

TDQS

C2.4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full responsibility for behavioral transparency. It fails to disclose what 'generate' means in terms of side effects (e.g., file creation), whether it overwrites existing suites, or any dependencies like collectionPath needing to exist. Only weak inference from param names suggests creation behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, concise sentence that front-loads the core purpose. It is efficient with words, though it lacks critical detail, which is more a completeness issue than conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of the schema (arrays of requests with auth, body, folders, dependencies), the description is woefully incomplete. It doesn't mention how requests are structured, how dependencies (from/to) work, or the requirement for an existing collectionPath. A richer description is needed to guide the agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With only 25% schema coverage (only collectionPath has a description), the description must clarify parameter usage. It only mentions 'related requests' without explaining how requests, suiteName, dependencies, or collectionPath function. The description adds minimal value beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Generate comprehensive test collections with multiple related requests' clearly states the verb ('Generate') and resource ('test collections'), and hints at the multi-request nature. However, it doesn't distinguish itself from sibling tools like create_collection or create_crud_requests, making it less effective at differentiating.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like create_collection or run_collection. It lacks any mention of context, prerequisites, or exclusions, leaving the agent without direction on tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

delete_collectionDelete CollectionA

Permanently delete a Bruno collection: the collection directory and everything inside it is removed from disk, and its workspace registry entry is removed with it. Nothing here can be recovered through this server. Only a directory that is itself a collection root — one holding opencollection.yml or bruno.json — can be deleted, so this cannot be pointed at an arbitrary directory. To keep the files and only stop the collection being listed, use unregister_collection instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
confirmYesMust be true. Explicit acknowledgement that the directory and every request in it are deleted permanently.
workspacePathNoAbsolute path of the workspace.yml holding its entry. Defaults to the same one list_collections reads: BRUNO_WORKSPACE_PATH, or the Bruno app's workspace for this platform.
collectionPathYesAbsolute path of the collection directory to delete. Use the path from list_collections.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully discloses the destructive nature: permanent, irreversible, deletes entire directory and registry entry, and restricts to collection roots. It also mentions no recovery through the server. This covers all critical behavioral traits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three concise sentences: first states the core action and scope, second adds irreversibility, third provides a safety condition and an alternative. No redundant words, well-structured and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive tool with no output schema and no annotations, this description is fully complete: it explains the exact effect, the validation constraint, and the alternative tool. Sibling context is addressed by naming unregister_collection. Nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema descriptions cover 100% of parameters, so the description does not need to repeat them. It adds no new parameter-specific semantics beyond what the schema already provides (e.g., the confirm flag requirement and path guidance are already in the schema). Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool permanently deletes a Bruno collection including its directory and registry entry, and distinguishes it from unregister_collection which only stops listing. The verb 'delete' plus resource 'collection' and specific scope (directory + registry) makes the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use (permanent deletion) and when not to (if you want to keep files, use unregister_collection). Also provides a safety condition: only collection roots are valid, preventing misuse. This is exemplary guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

delete_requestDelete RequestA

Permanently delete a Bruno request file from a collection. Use this to remove a request created by mistake; the file is unlinked from disk and cannot be recovered through this server. Only .yml/.bru files inside a detected Bruno collection can be deleted. To clear just a script and keep the request, use remove_script instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
confirmYesMust be true. Explicit acknowledgement that the file is deleted permanently.
filePathYesAbsolute path to the .yml or .bru request file to delete. Get from list_requests or get_collection_stats.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full responsibility for behavioral disclosure. It explicitly states the action is permanent ('cannot be recovered through this server') and describes the mechanism ('the file is unlinked from disk'). It also discloses the file type and collection scope limitation, providing essential transparency for a destructive operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and well-structured: the first sentence states the action, the second adds context and consequence, and the third clarifies constraints and an alternative. Every sentence adds value, with no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's destructive nature, the description covers all essential context: permanence, scope, file type restrictions, and a clear alternative. The schema fully documents parameters, and no output schema is needed for a simple delete operation. The description is sufficiently complete for an agent to select and invoke the tool safely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for both parameters, and the schema descriptions are already detailed (e.g., filePath explains it is an absolute path and suggests sources like list_requests). The tool description adds no additional parameter semantics beyond what the schema provides, so the baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Permanently delete a Bruno request file from a collection.' It uses a specific verb (delete), identifies the resource (request file), and explicitly distinguishes from sibling remove_script by saying 'To clear just a script and keep the request, use remove_script instead.'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear usage context: 'Use this to remove a request created by mistake' and names the alternative tool for a related but different task. It also states constraints (only .yml/.bru files inside a detected Bruno collection), giving the agent explicit guidance on when this tool is appropriate.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_collection_statsGet Collection StatisticsA

Get statistics about a Bruno collection — request counts by method, folders, environments, and per-request details including each request's file path and URL. environmentDetails lists each environment with the NAMES of the variables it declares (values are withheld), so you can see what an environment already defines before merging into it with set_environment_variable. Use filePath values as entries in the requests list of run_collection.

SIZE: the per-request array is the bulk of the response and grows with the collection — a few hundred requests run to tens of kilobytes. Narrow it with folder, method or nameContains, or pass includeRequests: false for counts only. The counts always describe the WHOLE collection; a filtered call adds matchedRequests so the two cannot be confused.

ParametersJSON Schema
NameRequiredDescriptionDefault
folderNoReport only requests in this folder, path relative to the collection root. Nested folders are included, so "auth" also matches "auth/oauth2".
methodNoReport only requests with this method, case-insensitive. A request that has no method is bucketed under its kind, so "GRPC" and "WS" work here too.
nameContainsNoReport only requests whose name contains this text, case-insensitive.
collectionPathYesAbsolute path to collection directory. Use the path returned by list_collections.
includeRequestsNoSet false to drop the per-request array and return only the counts, folders and environments.

TDQS

A4.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explains the behavior thoroughly: it returns counts and details, mentions that environment variable values are withheld, and discusses response size and filtering side effects. Although no read-only annotation exists, the nature of 'Get statistics' implies no modifications, and the description makes the operational behavior clear.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured in two logical paragraphs: first describing the output components, then addressing response size and filtering. It is concise, with no redundant phrasing, and every sentence adds valuable information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity—multiple filtering options, response size considerations, and the relationship between counts and per-request data—the description covers all necessary aspects, including the use of filePath in run_collection, making it self-sufficient for an agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

All five parameters (collectionPath, folder, method, nameContains, includeRequests) are mentioned and explained in the description. It details the semantics of each, such as folder nesting, case-insensitivity, and the meaning of includeRequests, exceeding the bare schema documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'Get statistics about a Bruno collection' with specifics on counts by method, folders, environments, and per-request details. It distinguishes itself by providing filePath for use in run_collection, making its unique purpose evident.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit guidance on when to use the tool is provided: to obtain statistics and to use filePath values in run_collection. It also explains how to narrow results via folder, method, nameContains, and includeRequests, and clarifies the distinction between total counts and matchedRequests.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_collectionsList CollectionsA

List the Bruno collections REGISTERED IN workspace.yml, with their names and paths. This is a registry listing, not a filesystem scan: a collection that exists on disk but is not registered will NOT appear, and registered entries that no longer exist are returned with "exists": false. If you already know a collection's absolute path, pass it directly to the other tools — it does not need to appear here. Use the returned path as collectionPath in other tools (get_collection_stats, list_requests, run_collection).

ParametersJSON Schema
NameRequiredDescriptionDefault
workspacePathNoOptional explicit path to workspace.yml

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description fully discloses key behavioral traits: it is a registry listing, not a filesystem scan; unregistered disk collections won't appear; and non-existent registered entries return 'exists': false. This goes beyond the schema and provides crucial context for correct interpretation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, each serving a distinct purpose: stating the core function, explaining the registry behavior, and giving usage guidance. It is concise, front-loaded, and free of redundant information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema, the description explains the key outputs (names, paths, exists) and how to use them with other tools. Given the simple nature of the tool (one optional parameter) and the rich behavioral detail provided, the description is complete enough for an agent to use the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has one parameter (workspacePath) with complete description coverage (100%), so the description does not need to add parameter details. The description does not mention workspacePath, but the schema already documents it adequately, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists Bruno collections registered in workspace.yml with their names and paths. It distinguishes itself from a filesystem scan and explicitly contrasts with other tools that use the path, making its purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit guidance: if you already know the absolute path, pass it directly to other tools instead of using this. It also instructs to use the returned path as collectionPath in get_collection_stats, list_requests, and run_collection, clarifying when this tool is appropriate.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_requestsList RequestsA

List all request files (.yml/.bru) in a Bruno collection. Returns absolute file paths, each usable as an entry in the requests list of run_collection, or of one of its groups.

ParametersJSON Schema
NameRequiredDescriptionDefault
collectionPathYesAbsolute path to collection directory. Use the path returned by list_collections.

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It does add useful context: the tool returns absolute file paths and specifies the file formats (.yml/.bru). However, it does not state whether the tool is read-only (though 'List' implies it), nor does it disclose error behavior or recursion depth. It is adequate but not rich in transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, consisting of two sentences that are front-loaded with the core purpose. Every sentence adds value: the first states what the tool does, the second explains the output format and its integration with run_collection. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's low complexity (one parameter, no output schema), the description is quite complete. It explains the return value (absolute file paths) and their usability in run_collection. A minor gap is that it does not clarify whether subdirectories are traversed, but this is not a critical omission for a listing tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already provides a clear description for collectionPath: 'Absolute path to collection directory. Use the path returned by list_collections.' Since schema coverage is 100% and the description adds no additional parameter-level guidance, the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action: 'List all request files (.yml/.bru) in a Bruno collection.' It clearly identifies the resource (request files) and scope (collection), and distinguishes from sibling tools like list_collections (which lists collections) and read_request (which likely reads a request's content).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context on how to use the output: 'absolute file paths, each usable as an entry in the requests list of run_collection, or of one of its groups.' This effectively tells the agent when to use this tool (to get request paths for running a collection) and how the returned values integrate with run_collection. It does not explicitly mention alternatives or exclusions, so it stops short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

modify_requestModify RequestA

Update an existing Bruno request file with partial-merge semantics. Only provided fields are updated; all other fields are preserved. Supports multipart/form-data with file uploads and per-part contentType. Inline scripts REPLACE the existing script of the same type by default (idempotent — repeated calls do not accumulate duplicate blocks); pass scriptMode:"append" to concatenate instead. Use remove_script to clear a script entirely. RENAMING: name and filename are independent, as they are in Bruno itself — name changes the request's name inside the file and filename moves the file. Pass both to keep them in step, and read the new path back from the response.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNo
authNo
bodyNo
grpcNogRPC-only fields. Applies to kind "grpc" and is refused otherwise. Headers given for a gRPC request are written as metadata, which is that transport's only header surface.
nameNoThe request's name inside the file. Does NOT rename the file — pass filename for that.
varsNo
queryNo
assertNoReplaces the whole assert block. Omit to leave existing assertions untouched.
methodNo
headersNo
scriptsNoInline scripts to persist with the request. Keys: pre-request, post-response, tests (aliases before-request/after-response accepted). Avoids a separate add_test_script call. IMPORTANT for tests/post-response: only assertions inside a test() block are reported. Write test("status is 200", function() { expect(res.getStatus()).to.equal(200); }); — a bare expect() at the top level still runs, but a passing one records nothing, so run_collection reports "tests": [] and the request looks green with no assertions. Available in scripts: res.getStatus()/getStatusText()/getHeader(name)/getHeaders()/getBody()/getResponseTime(), res.getStopReason()/getCloseCode()/getSessionTruncated() on a websocket request, which report the same session outcome the result does (all null or false on an HTTP response, which has no session), bru.setVar(name, value)/getVar(name)/getEnvVar(name)/hasEnvVar(name), and expect(actual) with .to.equal/.contain/.include, .to.have.property/.lengthOf, .to.be.a/.an, .to.be.above/.below/.at.least/.at.most (aliases .gt/.lt/.gte/.lte/.greaterThan/.lessThan), .to.be.within(min, max), .to.be.oneOf([...]), .to.match(/re/), .to.startWith/.endWith, .to.be.true/.false/.null/.undefined/.empty/.json,and .to.not.* negations. VARIABLES: bru.getVar(name) resolves environment and collection variables as well as anything a script set, so an environment variable needs no shadow copy to be readable. bru.getEnvVar(name) is narrower on purpose: it reads the environment layer only, so a runtime variable of the same name does not shadow it. There is no setEnvVar — nothing here writes an environment file. RETURN TYPE: res.getBody() returns the response already parsed into a JS object/array when the Content-Type is application/json or a +json type (raw text otherwise). Access fields directly — res.getBody().field — and do NOT JSON.parse() it, which throws SyntaxError: "[object Object]" is not valid JSON.
filePathYesAbsolute path to the .yml or .bru request file to modify. Get from list_requests or get_collection_stats.
filenameNoRenames the file, keeping it in its own folder. Basename only, no path separators. The extension is optional and must match the collection's format if given, since a collection carries one format only. Refused if another file of that name already exists. The new path comes back in the response; use it as filePath from then on, because the old one is gone.
settingsNoRequest-level settings: transport behaviour (timeouts, redirects, URL encoding), not payload. What reaches the file depends on the dialect, because Bruno's own two writers differ: a .yml request always carries a fully resolved settings block whether or not you pass one, while a .bru request carries only what you supply. On modify_request the fields are merged individually over the existing block, so setting one does not clear the rest. Note the encodeUrl field: in .bru, creating a block at all changes the URL-encoding default.
websocketNoWebSocket-only fields. Applies to kind "websocket" and is refused otherwise.
pathParamsNoReplaces the declared path parameters; query parameters are left alone.
scriptModeNoHow to write the scripts field. "replace" (default) overwrites the existing script of each provided type, so calling modify_request repeatedly is idempotent. "append" concatenates onto the existing script, which accumulates blocks across calls. Each script type has its own slot in both .bru and .yml, so replacing one leaves the others untouched. One exception on .yml: supplying post-response and tests together in a single call still merges both into the after-response slot, so write the tests script in its own call to keep it in the tests slot.replace

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full disclosure burden, and it delivers: partial-merge preservation of unprovided fields, idempotent script replacement (repeated calls do not accumulate), the append escape hatch, and the subtle name/filename independence mirroring Bruno's own behavior. It also points the caller to read the new path from the response after a rename. It doesn't cover reversibility or auth requirements, but for a file-editing tool the disclosed traits go well beyond the minimal mutation hint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three dense sentences, front-loaded with the core merge semantics before moving to script behavior and rename rules. Every sentence carries non-redundant information; the append-mode, remove_script reference, and filename/name distinction each earn their place. Slightly long for a one-liner, but the density justifies the length given the tool's complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's high complexity (17 parameters, nested objects for auth/body/grpc/settings/websocket/vars) and no output schema, the description covers the cross-cutting behaviors — merge semantics, script modes, file uploads, rename handling — while the schema exhaustively documents individual fields. It even hints at the response contract ('read the new path back from the response'). The only gap is that it delegates most parameter behavior to the schema, which is acceptable since the schema is thorough.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 59%, so the description partially compensates. It adds value by explaining the global partial-merge semantics that apply to every provided field, clarifying scriptMode's append/replace distinction, and unpacking the name-vs-filename independence beyond what the schema's bare descriptions state. The heavy per-parameter detail (auth, grpc, settings, websocket) is already richly documented in the schema itself, so the description need not repeat it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb+resource: 'Update an existing Bruno request file with partial-merge semantics.' This clearly distinguishes it from siblings: create_request (new files), read_request, delete_request, and move_request are all named different actions. The partial-merge framing immediately separates it from create_request's full-write behavior.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear usage context: it targets existing files ('Update an existing...') versus create_request, and explicitly names an alternative tool — 'Use remove_script to clear a script entirely' — when clearing is needed rather than replacing. It also explains when to choose append vs. replace via scriptMode. It doesn't enumerate exclusions against every sibling, but covers the key decision points for a modify tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

move_requestMove RequestA

Relocate a request file to another folder, or to another collection entirely. Pass copy:true to duplicate it instead of moving it. The bytes are moved verbatim and nothing is parsed and rewritten, so no part of the request can be lost on the way — which also means seq arrives unchanged and may tie with a request already there; that is reported, and Bruno breaks such a tie by filename. The file keeps its name: use modify_request with filename to rename it. The new path comes back in the response.

ParametersJSON Schema
NameRequiredDescriptionDefault
copyNoLeave the original in place and write a duplicate at the destination. Since the file keeps its name, a copy needs a different folder or collection.
filePathYesAbsolute path to the .yml or .bru request file to move. Get from list_requests or get_collection_stats.
targetFolderNoFolder inside the target collection, relative to its root, e.g. "auth/login". Omit for the collection root. Created if it does not exist.
targetCollectionPathNoAbsolute path to the collection it should land in. Omit to move within the request's own collection.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden of behavioral disclosure. It goes above and beyond by explaining that the move is byte-verbatim, that seq may tie and are resolved by filename, and that the new path is returned. This level of detail is outstanding and far exceeds the minimum.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise: three sentences that carry substantial information without fluff. It leads with the primary purpose, then covers the key nuance (copy mode), and concludes with behavior and an alternative. Every sentence adds value, and the structure is efficient.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity, the description is remarkably complete. It explains the return value (new path), an edge case (seq ties and resolution), and the difference between move and copy. There is no output schema, so the description appropriately covers the response. It also provides guidance on selecting destinations via parameters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already covers 100% of parameters with descriptions, so the baseline is 3. The description reinforces the copy semantics and notes that the file keeps its name, but these are more behavioral than parameter-specific. It does not add significant new meaning beyond the schema, but it does not need to.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Relocate a request file to another folder, or to another collection entirely.' It specifies the verb (relocate), the resource (request file), and the two primary operation modes (move and copy). It also distinguishes from sibling tools like modify_request by noting that renaming is handled there.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use copy:true versus moving, and explicitly points to modify_request for renaming, which differentiates it from that sibling. It does not explicitly list all alternatives or exclusion criteria, but the context is clear enough for typical usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_environmentRead Bruno EnvironmentA

Read an environment back as structured JSON: every variable with its value, plus its disabled and secret flags. Omit "name" to list the collection's environments instead. Secret variables are returned by name only — Bruno stores no value for a secret in either file format, so there is none to return.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNoEnvironment name, without extension. Omit to list the available environment names.
collectionPathYesAbsolute path to the collection directory.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description takes on full responsibility for behavioral disclosure. It reveals that secret variables are returned by name only because Bruno stores no value for them, and it clarifies that the return is structured JSON with specific flags. This is non-obvious context that prepares the agent for actual output.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, using three sentences that explicitly state the purpose, the alternate usage, and an important edge case. Every sentence contributes information without redundancy, making it easy to parse and act on.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity and the lack of an output schema, the description adequately explains what is returned and the special handling of secrets. It could potentially mention error cases or the exact JSON shape, but it is complete enough for the intended use, especially considering the schema's 100% coverage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already provides thorough descriptions for both parameters, including the meaning of 'name' and the effect of omitting it. The description adds only a note about secrets, which is more about return behavior than parameter semantics. Thus, it meets the baseline without adding significant parameter-specific value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool reads an environment and returns structured JSON with all variables, disabled and secret flags. It additionally notes that omitting 'name' lists environments, which helps distinguish it from sibling tools like read_request or list_collections.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear guidance on the optional name parameter, explaining that omitting it lists environments instead. It also explains the secret variable behavior, which is useful for avoiding confusion. While it doesn't explicitly name alternative tools, the usage context is sufficiently clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_requestRead Bruno RequestA

Read a single request file back as structured JSON: method, url, headers, query and path params, body, auth mode, scripts, assertions, vars, settings and docs. Works on both .bru and .yml and returns the same shape for each, so the on-disk format stays invisible. Use this before modify_request to see current state, and after create_request to confirm what was written. A websocket or grpc request also carries its stored messages in full — title, content, and for a websocket the type and whether the runner will send it — under websocket.messages or grpc.messages, keyed as create_request accepts them. A "notes" array reports anything the file declares that the runner will not act on.

ParametersJSON Schema
NameRequiredDescriptionDefault
filePathYesAbsolute path to the .bru or .yml request file. Use the path returned by create_request or list_requests rather than rebuilding it: request filenames are lowercased on write.

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Although no annotations are provided, the description compensates by explaining that the tool returns the same shape for .bru and .yml, making the on-disk format invisible, and that a 'notes' array lists declarations the runner ignores. It also details websocket/grpc message payloads. It does not explicitly state it has no side effects, but 'read' implies read-only, and the content is sufficiently transparent for a read operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average but each sentence adds value: it defines the output shape, gives usage guidance, explains websocket/grpc handling, and notes the 'notes' array. It is front-loaded with the core purpose and structured logically, though it could be slightly tightened without losing information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given there is no output schema, the description fully covers the return content: method, url, headers, params, body, auth, scripts, assertions, vars, settings, docs, and messages for websocket/grpc. It also explains the 'notes' array. For a single-parameter tool, this provides comprehensive context, leaving no major gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single parameter filePath is fully described in the schema with details about absolute path and lowercasing. Since schema description coverage is 100%, the description does not need to add more, and indeed it only reiterates that both .bru and .yml are supported. No additional meaning beyond the schema is provided, so baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: reading a single request file as structured JSON. It specifies the resource (request file) and the action (read), and differentiates from sibling tools like create_request and modify_request by focusing on reading the current state. The mention of .bru and .yml formats and the output shape adds specificity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly advises when to use this tool: before modify_request to see current state, and after create_request to confirm what was written. It also mentions how websocket/grpc messages are handled, guiding the agent on what to expect. This provides clear context and distinguishes it from alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

remove_environment_variableRemove Environment VariableA

Remove a single variable from an existing Bruno environment. MERGES into the environment — all other variables are preserved.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesVariable key to remove.
environmentYesName of the existing environment.
collectionPathYesAbsolute path to existing collection directory.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the transparency burden. It discloses a critical behavioral trait: 'MERGES into the environment — all other variables are preserved.' This goes beyond the schema by clarifying that removal does not replace the entire environment. It also notes 'single variable,' setting expectations for scope. However, it does not cover error handling or prerequisites (e.g., environment must exist), which could be relevant.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, directly front-loaded with the action, and the second sentence provides essential behavioral context. Every word earns its place without unnecessary elaboration.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple removal tool with 3 parameters and no output schema, the description covers the key aspects: what it does, the scope (single variable), and the critical side effect (preserving other variables). It does not explain return values or error behavior, but the absence of an output schema lowers the expectation. Overall, it is sufficiently complete for selecting and invoking the tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema covers 100% of parameters with descriptions, so the baseline is 3. The description adds no additional parameter-level detail beyond what the schema already provides. It does relate to parameters indirectly (e.g., 'single variable' maps to 'name'), but that is behavioral context, not parameter semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'Remove a single variable from an existing Bruno environment.' It uses a specific verb ('remove'), identifies the resource ('variable'), and specifies the scope ('single' and 'existing environment'). This distinguishes it from sibling tools like set_environment_variable (which adds/updates) and update_environment (which modifies environment settings).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies when to use the tool (to remove a variable) and includes a limitation ('single variable'), but it does not explicitly name alternatives or provide 'when not to use' guidance. The context is clear enough for an agent to select this tool over siblings, though explicit exclusions would improve it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

remove_scriptRemove ScriptA

Delete a pre-request, post-response, or tests script from a Bruno request, leaving the rest of the request intact. Use this to undo or clean up a script written by create_request/modify_request/add_test_script — including duplicate blocks accumulated by appending. Canonical scriptType values are pre-request/post-response/tests; the aliases before-request and after-response are accepted. Removing all scripts also drops the now-empty script container.

ParametersJSON Schema
NameRequiredDescriptionDefault
scriptTypeYesWhich script to remove. Both .bru and .yml keep the three script types in separate slots, so removal is precise: clearing tests leaves a post-response script in place, and vice versa.
bruFilePathYesAbsolute path to the .yml or .bru request file. Get from list_requests or get_collection_stats.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It reveals that the operation is non-destructive to the rest of the request, explains alias handling, and discloses the side effect of dropping the now-empty script container when all scripts are removed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences with purpose front-loaded, followed by usage guidance and a side-effect note. It is efficient but slightly dense, combining parameter semantics with behavior in one sentence.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a delete tool with no annotations and no output schema, the description covers the core behavior, use cases, side effects, and parameter values adequately. Minor gaps exist around error conditions or return values, but these are not critical for a removal operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with both parameters already having detailed descriptions. The description adds some context about canonical vs. alias scriptType values, but this is largely redundant with the schema's enum and field descriptions, so little new meaning is added.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action (Delete) and the object (a script from a Bruno request), with specific script types listed. It also distinguishes itself from sibling tools by noting it leaves the rest of the request intact and referencing the tools that create scripts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says 'Use this to undo or clean up a script written by create_request/modify_request/add_test_script', giving a clear when-to-use directive. It also clarifies that it removes scripts rather than the whole request, implicitly excluding delete_request.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_collectionRun CollectionA

Execute requests in a Bruno collection and run test scripts. HOW TO RUN A SUBSET: requests takes an ORDERED list of entries, each a .yml/.bru request file or a directory (which expands to every request under it, recursively). Absolute, or relative to collectionPath. Order is yours and duplicates are allowed — naming the same request twice runs it twice. Omit requests to run the whole collection. GROUPS: pass groups instead when one call needs more than one identity or configuration. Each group owns its OWN variable store and cookie jar, so nothing a group sets — a bru.setVar, a session cookie — is visible to any other group. That is what makes running the same five requests as alice and as bob in one call safe. Passing both requests and groups is REJECTED rather than resolved for you. ITERATIONS: data or dataFile runs a group once per row of a table, with the row bound as variables — one call to check the same requests against 50 accounts, or one login against 50 credential pairs. Each row is reported as its own group, with the same name, its own index and an iterationIndex, because each row IS a separate group: it gets that isolation, so two rows differing only in a password authenticate separately instead of one silently reusing the other's token. Rows are independent, so a failing row does not stop the rows after it. Ceiling of 1000 rows per scope. PARALLELISM: parallel on the RUN fans the groups out against each other; parallel on a GROUP fans that group's own requests out. A group is serial unless it says otherwise, whatever the run does. seq no longer constrains execution — it is the default order and the reporting order only, so two requests in one parallel group genuinely run at the same time and may contend on the store they share. A group can also WAIT for another: startAfter holds it until the named group has completed a given number of its requests, which is how you get a listener connected before a trigger fires without a bru.sleep tuned to whatever the latency was the day it was written. It needs parallel on the run, chains are allowed, and cycles or gates that could never open are refused before anything runs; if the group being waited on ends early, the waiting group reports that as its error instead of starting. RESULTS are group-shaped: groups[] each carry their own summary, results, capturedVariableNames and capturedVariables, and the top-level summary is the run. Warnings about the run as a whole are top-level, not per group — a captureVariables name is reported as unset only when NO group set it, since a group's store is isolated and lacking a name there says nothing on its own. There is no top-level results array, in the no-groups case either. A group that crashed outright reports error and counts as one failure in the run summary. Each result includes the response body (response_body, response_content_type, response_body_truncated) by default — disable with includeResponseBody=false or cap the size with maxResponseBodyBytes. Each result also carries response_headers by default, no test script needed (set includeResponseHeaders=false to leave them out, and maxResponseHeaderBytes to bound one value): credential-named values are masked, and set-cookie is a LIST — one entry per cookie — whose entries keep every attribute (HttpOnly, Secure, SameSite, Path, Max-Age) and withhold only the cookie value, so a cookie-flag or HSTS check is one call. includeResponseBody=false does not suppress headers; it bounds the body only. A websocket result carries response_headers too, holding the handshake response (the 101) — the only place a session cookie or an agreed sec-websocket-protocol is visible for that transport; a gRPC result reports its own metadata under the grpc detail instead. Within a group, requests run in the order given; a directory expands by seq, scoped to its own folder, with subfolders before that directory's own loose requests, ties broken by filename. A request file that cannot be parsed is skipped when it was DISCOVERED — by running the whole collection, or by expanding a directory: the count is parseErrors and each skipped file is named with its reason in parseFailures. A file you NAMED yourself is different: you asked for that specific request and there is no partial answer to it, so it fails the group that named it, reported as that group's error, and the other groups still run. A named request that does not exist is reported in that group's missingRequests rather than failing anything, so you can see which subset ran. TRANSPORTS: http, graphql, grpc and websocket requests all run; any other kind is refused per-request with status 0 and a named reason, leaving the rest of the group alone. A gRPC result carries a grpc detail — the gRPC status code (0 is OK, and is NOT the refusal sentinel: status 0 with an error and no grpc detail is a refusal), the method, and redacted metadata. A WebSocket result carries a websocket detail — the transcript, stop_reason naming what ended the session (count, timeout, bytes, closed or error), and truncated, which is true for the three that cut a session short. Frame contents are recorded only if you ask for them, via websocket.includePayloads. ASSERTING ON A TRANSPORT: on a websocket result res.getBody() IS the transcript array — the frames, handed over as a structure — and res.getStatus() is ALWAYS 0, because a session has no status. The outcome is res.getStopReason() (equally res.stopReason, and res.statusText, which carries the same string), res.getCloseCode() for the code the peer closed with (null when it sent no close frame — the ordinary case for a session stopped by its own bound), and res.getSessionTruncated() for whether the recording stops short of the session. Each returns exactly what the websocket detail of the same result reports, so an assertion cannot pass against an outcome the result denies. A test written against res.getStatus() on a websocket asserts on a constant and cannot fail. On a gRPC result res.getStatus() is the gRPC code, res.statusText the server details or the code name, and res.getBody() the parsed message. SIMULTANEITY IS PER CALL: groups plus parallel are the only way to make two things happen at the same moment. Two separate run_collection calls are not serialised by this server, but nothing here decides when the second one starts — that is the caller's scheduling, and a client that issues tool calls one at a time produces runs seconds or tens of seconds apart, which no single result shows. If a test needs one identity listening while another triggers, put both in ONE call as parallel groups; the wall-clock gap between separate calls is not something this server can close. Each result carries the path of the request file it came from, so a failure can be read back or re-run by name. STOPPING EARLY: bail stops the run at the first request that fails or whose tests fail, so a chain of dependent requests reports one cause instead of one failure plus every consequence of it. Everything after it comes back with skipped: true and skipReason: "bail", counted in summary.skipped and in neither passed nor failed, and the run carries a bail object naming the reason, the request it stopped at and how many were skipped. Nothing cancels a request already in flight, so with parallel the requests that had already started still finish and are reported normally. REPORT FILES: pass report to also write the run to disk — JUnit XML for CI, HTML for a person — at a path inside the collection; the files written come back under reports. Outbound requests are SSRF-filtered: targets resolving to private, loopback, link-local or otherwise reserved addresses are refused unless the server operator has allowlisted them, and a refusal is reported per-request as an "SSRF blocked" error with status 0.

ParametersJSON Schema
NameRequiredDescriptionDefault
bailNoStop at the first request that errors or fails a test, instead of running the rest. Bruno's CLI calls this --bail. Use it when the requests depend on each other: without it, a failed login is followed by every request that needed its token failing too, which is four failures to read for one cause. What it stops is bounded by what has already started. Requests not yet started are skipped — in the failing group and in every group after it — and each comes back as a result with skipped:true rather than being left out, so you can see which ones still need running. Requests already in flight are NOT cancelled, so with parallel:true or a parallel group this skips the tail rather than the remainder, and the run says so in its warnings. A skipped request counts in summary.skipped and in neither passed nor failed. Where the run stopped is reported in the result's bail field. Default: false.
dataNoRows every group iterates over, as the group-level data but applied to all of them. A group that gives its own data or dataFile REPLACES this rather than adding to it, the same rule environment follows. With no groups given, the run is one group and these rows are its iterations. Mutually exclusive with dataFile.
groupsNoRun the same collection under more than one identity or configuration in one call. Each group owns its OWN variable store and cookie jar: nothing a group sets is visible to any other group, in either direction, at any parallel setting. Groups are reported in the order given. The same request may appear in several groups. Cannot be combined with requests; omit both to run the whole collection as one group.
reportNoWrite the run to a file, in addition to returning it here. Name at least one format. Paths are CONFINED TO THE COLLECTION: a path resolving outside it is refused, with the reason as a run warning, because writing wherever a caller points is a far bigger authorization than running its requests — copy the file afterwards if your pipeline collects it elsewhere. Missing parent directories inside the collection are created, and an existing report is overwritten. The result reports each file written under reports, with its absolute path and size; a report that could not be written never fails the run, it adds a warning naming the format and the reason. A report holds what the results hold — response bodies included, response headers masked exactly as they are here — so it lands on disk with whatever the run saw.
dataFileNoA CSV inside the collection whose rows every group iterates over. Same rules as the group-level dataFile, and likewise replaced by a group that names its own rows. Ceiling of 1000 rows, because every row runs every request in the group and a spreadsheet passed by mistake is an outbound request storm; slice the file and run it in parts.
parallelNoFan out. At the run level this runs the GROUPS concurrently; a group's own requests are serial unless that group sets its own parallel. With no groups given the run is one group, so parallel here runs every selected request concurrently. Reporting order is the listed order regardless. Default: false.
requestsNoThe requests to run, IN THE ORDER GIVEN. Each entry is a .yml or .bru request file, or a directory, which expands to every request under it, recursively. Absolute, or relative to collectionPath. Get paths from list_requests or get_collection_stats. Duplicates are allowed: naming the same request twice runs it twice. Omit to run every request in the collection; an empty [] is a selection of nothing and runs nothing. Cannot be combined with groups. An entry naming nothing is reported in missingRequests rather than failing the run, so you can see which subset ran.
cookieJarNoKeep cookies from each response and send them on later requests in the same run, so a login carries into the requests after it. Scoped to the group — nothing crosses from one group to another, at any parallel setting — held in memory, never written to disk, and matched by host/path/expiry — a cookie set by one host is not sent to another. Precedence, per cookie name: a Cookie header the request writes itself WINS over the jar, and the jar only adds names the request did not set, so a request that pins a specific credential keeps it and a run that relies on the jar is unaffected. This diverges from Bruno's CLI, where the stored value wins the clash; a warning names any cookie whose stored value was dropped. Default: true, matching Bruno's CLI. Set false to send only the Cookie headers a request writes itself.
variablesNoVariables for this run only, as {name: value}. They override the environment file and work without one. Held in memory and never written to any file — this is the only correct way to supply a secret, because neither Bruno file format stores a secret value. Referenced as {{name}} in urls, headers, bodies and auth. A request-level vars:pre-request entry or a bru.setVar in a script still overrides these, matching Bruno's --env-var precedence.
websocketNoBounds for websocket requests in this run. Applies to every websocket request in the call; there is no per-request form. Omit and the defaults below apply. A session always ends on one of these bounds or on the peer closing, and nothing is held open past the call. Each transcript entry carries its frame type ("text", "binary", "ping", "pong" or "close"), the authored title of a message the session sent, and, on a close frame, the close_code the peer gave — 1000 is an ordinary goodbye, 1006 a peer that vanished, 1008 a refusal, 1011 a server error. Control frames do not count toward maxMessages. A binary frame's payload is base64; bytes is the true wire size for every kind. A subprotocol is not a bound and is not set here: author it as a Sec-WebSocket-Protocol header on the request, comma-separated for more than one, and it is negotiated at the handshake — the one the server agreed to comes back in that result's response_headers.
environmentNoEnvironment name to use (e.g. "dev", "staging"). Get available names from get_collection_stats.
collectionPathYesAbsolute path to collection root directory. Use the path returned by list_collections.
collectionRootNoThe collection that collectionPath belongs to, when running a subfolder of one: environments and the collection- and folder-level scripts are resolved from here. Must be collectionPath itself or an ancestor of it — a root that does not contain the collection is rejected, because its root scripts would then run against these requests.
maxConcurrencyNoCeiling on requests in flight across the whole run. Omit to derive one from this machine's cores and memory, held below capacity. 0 lifts it entirely, at your own risk. Applies to THIS run and is given back when it ends, so it neither re-caps another run already in flight nor outlives the call that asked for it. A ceiling below the number of requests you meant to run at once silently serialises them, so reproducing a race needs a value at least as large as the number of racers.
captureVariablesNoNames of variables set by bru.setVar during the run whose values you want back, e.g. ["token"]. A script that captures a value out of a response — bru.setVar("token", res.body.token) — makes it available to later requests as {{token}}, and this is the only way to see it yourself; without it the value exists only inside the run. Every name a script set is listed in capturedVariableNames on every run, so run once to see what is there and then ask for the one you need. Values are only returned for names you list here, and they are returned verbatim. Nothing is captured from the environment file or from variables you supplied — only what a script set.
includeResponseBodyNoInclude the response body of each request in the results. Default: true.
maxResponseBodyBytesNoMaximum response body size (bytes) to return per request; longer bodies are truncated and response_body_truncated is set. Default: 10240.
includeResponseHeadersNoInclude the response headers of each request in the results, credential values masked. Default: true, which is what every run did before this option existed. Turn it OFF for a run whose results you read in bulk: a header map is small for one response and not for thirty, and a run of a few dozen requests can spend more of its output on the same headers repeated per result than on anything you asked about. A test script is unaffected either way — res.getHeader() always reads the real headers, as res.body always carries the full body while response_body is gated by includeResponseBody.
maxResponseHeaderBytesNoMaximum size (bytes) of one header VALUE to return; a longer value is cut and response_headers_truncated is set on that result. Default: 2048. Per value, not per map, so this changes how much of a long value comes back and never which headers are reported — the names are the part worth reading, and dropping a header to save bytes would take its name with it. set-cookie is a list and each cookie is capped on its own.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden — and it exceeds it. It discloses defaults (includeResponseBody=true, cookieJar=true), mutation contexts (request files that cannot be parsed are skipped vs. named ones fail the group), security properties (SSRF filtering, credential-named header values masked, websocket payloads off by default to avoid leaking secrets), edge cases (parallel request contention, bail behavior, group isolation), and failure modes (refusals, missing requests, crashed groups). It even documents timing behavior (wall-clock gaps between separate calls). This is far beyond what a schema could convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tarifficiently dense — every section (SUBSET, GROUPS, ITERATIONS, PARALLELISM, RESULTS, TRANSPORTS, STOPPING EARLY, REPORT FILES) adds necessary behavior — but it is extremely long (approximately 1,000 words) with deeply nested explanations. The use of CAPITALIZED section labels helps scannability, but the level of detail on edge cases like websocket stop reasons, engine.io PING/PONG, and gRPC details exceeds typical needs and could overwhelm an agent. It earns a 3 for being well-structured and front-loaded but loses points for length; the content is justified but the sheer volume makes it hard to extract the core purpose quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's extreme complexity (19 parameters, nested objects, multiple transports, concurrency, reporting, security) and no output schema, the description is complete. It explains return shapes (groups[] with their own summary and results, top-level summary), result fields (response_body, response_headers, capturedVariables, bail, reports), error handling (refusals, missing requests, parse errors), and omnipresent edge cases. It answers 'what happens if...' questions that an agent would need before calling. Nothing critical is left uncovered within the tool's scope.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds substantial meaning beyond the schema. It explains semantics of the requests parameter with examples ('naming the same request twice runs it twice'), defines the groups parameter's isolation guarantees, clarifies iteration reporting, explains the distinction between data and dataFile behavior, details the startAfter gate semantics, and clarifies precedence rules for variables and cookieJar. This is value-added semantics that the schema's brief descriptions do not provide.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool executes requests in a Bruno collection and runs test scripts. It explicitly enumerates core capabilities (requests, groups, iterations, parallelism, transports, reporting) and distinguishes itself from sibling tools by being the only execution tool — siblings are CRUD for collections, environments, and requests. The verb 'execute' plus resource 'Bruno collection' is specific and unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides extensive when-to-use guidance: how to select a subset via requests list with ordered entries, when to use groups for multiple identities, when to use iterations for data-driven runs, and when to use parallel for concurrency. It explicitly states that simultaneous execution requires a single call with parallel groups, warning that separate calls are not serialized. It also names alternatives — 'get paths from list_requests or get_collection_stats' and 'Get available names from get_collection_stats' — and explicitly states when not to use certain features (e.g., 'Passing both requests and groups is REJECTED').

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

set_environment_variableSet Environment VariableA

Set (add or update) a single variable in an existing Bruno environment. MERGES into the environment — all other variables are preserved.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesVariable key to set.
valueYesVariable value.
secretNoWhether the variable is a secret. Persisted. IMPORTANT: marking a variable secret means its VALUE IS NOT SAVED — Bruno stores a secret variable as a name only (.bru lists it under vars:secret, .yml writes secret: true with no value) and keeps the value outside the collection, so `value` is discarded. Omit this to leave an existing variable's secret state untouched; pass false to convert a secret variable back to a plain one carrying `value`.
enabledNoWhether the variable is enabled. Persisted: enabled=false is written as a disabled variable.
environmentYesName of the existing environment.
collectionPathYesAbsolute path to existing collection directory.

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must disclose behavioral traits itself. It adds the key merge behavior (other variables preserved), but omits other traits like file persistence, environment existence requirements, or secret-handling caveats (though the schema covers the secret behavior in detail).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short clauses deliver the core purpose and the critical merge note without any filler. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple mutation tool with a rich schema, the description plus schema provide adequate context for selection and invocation. It lacks explicit error/return behavior, but the merge note covers the main surprise.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with detailed descriptions for all parameters, so the description adds little parameter-specific value beyond its 'add or update' and 'single variable' framing, which is minimal.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Set') and identifies the resource ('a single variable in an existing Bruno environment'), clearly distinguishing it from sibling tools like remove_environment_variable and broader update_environment.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It conveys clear context: add or update a single variable within an existing environment, with merging semantics. However, it does not explicitly state when to use this versus alternatives such as create_environment or remove_environment_variable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

unregister_collectionUnregister CollectionA

Remove a collection from the workspace registry, leaving every file on disk untouched. list_collections reads that registry, so this is how an entry stops being listed — including a stale entry whose directory is gone, which is what accumulates when collections are created in temporary directories. To delete the collection itself, use delete_collection.

ParametersJSON Schema
NameRequiredDescriptionDefault
workspacePathNoAbsolute path of the workspace.yml to remove the entry from. Defaults to the same one list_collections reads: BRUNO_WORKSPACE_PATH, or the Bruno app's workspace for this platform.
collectionPathYesAbsolute path of the collection as it is listed. Use the path from list_collections: matching is by path, not by name, because two entries are allowed to share a name.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully carries the burden. It states that files on disk are untouched, works even if the directory is gone, and clarifies that it only affects the registry. This is transparent about side effects and safety.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences, with the main action first and the alternative explicitly stated. No redundant wording; every clause adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given a simple operation with two well-documented parameters and no output schema, the description covers the purpose, the non-destructive effect, the edge case of stale entries, and the alternative tool. It is complete for an agent to understand when and how to use it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds valuable context: for collectionPath it emphasizes matching by path (not name) and advises using the path from list_collections; for workspacePath it explains the default behavior. This goes beyond the schema's basic definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Remove') and clearly identifies the resource (workspace registry), while explicitly distinguishing from delete_collection. It also explains the consequence (leaves files untouched) and the connection to list_collections, making it unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explains when to use this tool (to stop an entry from being listed, especially for stale entries) and explicitly points to delete_collection for deletion of the collection itself. This provides clear context for choosing between tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

update_environmentUpdate Bruno EnvironmentA

Partially update an existing Bruno environment by MERGING the provided variables into the existing ones. Pre-existing variables not listed are preserved (unlike create_environment, which replaces the whole file).

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesName of the existing environment to update.
variablesYesVariables to merge into the environment. Existing variables not listed here are kept.
collectionPathYesAbsolute path to existing collection directory.

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It clearly discloses the merge behavior and preservation of existing variables, which go beyond a simple 'update' label. However, it does not mention what happens if the environment does not exist, whether the operation is idempotent, or any error conditions—minor gaps given the tool's simplicity.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loaded with the core action and immediately followed by the key behavioral nuance. It is compact, easy to scan, and every word adds value. No redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (3 required params, no annotations, no output schema), the description covers the essential behavior, merge semantics, and a critical distinction from a sibling. It falls slightly short of a perfect score by not specifying return values or edge-case behavior (e.g., when variables are empty), but it is sufficiently complete for typical usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already provides 100% coverage for all three parameters, including a description for 'variables' that states existing variables not listed are kept. The description's merge explanation mirrors the schema, adding no new parameter-level semantics. Baseline 3 is appropriate as the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states the exact action: 'Partially update an existing Bruno environment by MERGING the provided variables into the existing ones.' It names the resource (Bruno environment) and the specific verb (update), making the purpose unambiguous. It also distinguishes itself from a sibling by explicitly contrasting with create_environment.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides a clear when-to-use guideline by explaining that it merges variables and preserves unlisted ones, and explicitly says 'unlike create_environment, which replaces the whole file.' This tells the agent to use this tool for partial updates while preserving existing variables, and to use create_environment when a full replacement is intended.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

TDQS

A3.6/5.0
Disambiguation4/5

Most tools pair a clear resource with a clear action—collection, environment, request, script—so an agent can usually tell them apart. The main boundary ambiguity is between create_collection, create_test_suite, and create_crud_requests (all generate collections/requests), and between add_test_script and the inline-script support in create_request/modify_request.

Naming Consistency4/5

The set mostly follows a consistent verb_noun pattern: create_collection, delete_request, list_requests, read_environment. Minor inconsistencies exist—add_test_script vs remove_script, and read_*/get_* verbs—but the overall convention is predictable.

Tool Count3/5

At 21 tools, the server sits in the heavy range. The breadth is partially justified by covering collections, environments, requests, scripts, generation, and execution, but several tools could be consolidated (e.g., set_environment_variable/remove_environment_variable vs update_environment), making the surface feel larger than necessary.

Completeness3/5

Collection, request, script, and execution workflows are well covered, and generation tools create test suites and CRUD operations. However, the environment lifecycle has an obvious gap: there is no delete_environment, and collection configuration/rename/move operations are also absent, leaving some dead ends.

Maintenance

ActivityActive
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Integrates Bruno CLI for API testing, enabling users to run API requests and collections, manage environments, generate test reports (JSON/JUnit/HTML), and validate collection structures through natural language commands.
    9
    40
    5
    MIT
  • A
    license
    B
    quality
    A
    maintenance
    Enables AI agents to create, manage, and execute API collections, requests, and environments in Insomnia-compatible formats. It supports direct synchronization with the local Insomnia app database and importing from OpenAPI, Postman, and cURL.
    29
    64
    24
    MIT

Latest Blog Posts

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/Ostico/bruno-mcp-studio'

If you have feedback or need assistance with the MCP directory API, please join our Discord server