mcpField
B
Bruno MCP Studio

Bruno MCP Studio

ローカル
github.com開発ツール115 週間前に更新

Bruno MCP Studio について

Bruno MCP Studioは、BrunoのAPIコレクションを作成、編集、確認、実行するエージェント向けMCPサーバーです。`.bru`と`.yml`に対応し、Bruno CLIなしでリクエストを実行して個別の結果を返します。

主な機能

  1. 01コレクションの作成と部分編集
  2. 02リクエストと環境を構造化JSONで読み取り
  3. 03`.bru`と`.yml`形式に対応
  4. 04HTTP、WebSocket、gRPCリクエストの実行
  5. 05アサーション、依存関係、認証主体のテスト

活用シーン

  1. 01エージェントとの作業から再利用可能なAPIテストを作成
  2. 02未対応のファイル内容を保ったままリクエストを更新
  3. 03コレクションを実行し、リクエストごとの結果を確認

MCP 対応

MCPクライアントに、Brunoコレクションの作成、読み取り、実行を行う18個のツールを提供します。Bruno CLIを呼び出さず、プロセス内でリクエストを実行します。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"ostico-bruno-mcp-studio": {
"command": "npx",
"args": [
"-y",
"@ostico/bruno-mcp"
]
}
}
}
起動コマンドnpx -y @ostico/bruno-mcp

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npm@ostico/bruno-mcp2.5.0

ツール

18

掲載時のツール一覧 · 2 週間前に更新

パラメーター型必須説明
namestringはい
formatenumいいえyaml | bru
ignorearrayいいえ
baseUrlstringいいえ
outputPathstringはいコレクションディレクトリを作成する親ディレクトリの絶対パス。name が追加されます。name が "Billing" で、ここに "/work/apis" を渡すと /work/apis/Billing が作成されます。ここに "/work/apis/Billing" を渡すと /work/apis/Billing/Billing が作成されます。
descriptionstringいいえ
workspacePathstringいいえ新しいコレクションを登録する workspace.yml の絶対パス。デフォルトでは、list_collections が読み取るものと同じファイルを使用します:BRUNO_WORKSPACE_PATH、またはこのプラットフォーム上の Bruno アプリのワークスペース。
registerInWorkspacebooleanいいえfalse に設定すると、ワークスペースファイルを変更せずにコレクションを作成します。別の操作で登録されるまで、list_collections には表示されません。

詳細

Bruno MCP Studio — エージェントから Bruno コレクションを作成・編集・実行

ユニークビュー npm のバージョン npm のダウンロード数 node ライセンス

エージェントによる API テストを、保存しておけるファイルにします。Bruno コレクションをその場で作成・編集してから実行し、HTTP、WebSocket、gRPC を 単一または複数の ID でテストして、CI で再実行できるリクエストごとの合否を報告します。

Bruno コレクション向けの独立した Model Context Protocol サーバーです。2026 年 8 月、Bruno のチームは 公式サーバー usebruno/bruno-mcp を発表しました。このサーバーは bru CLI をラップしてリクエストを検出・実行します。このサーバーが目指すのは別の用途です。 コレクションの実行だけでなく、書き込みにも対応します。破壊的な変更を加えることなく、 Bruno の標準ライターとバイト単位で同等の形式で書き込みます。また、ランナーはサブプロセスではなく プロセス内で動作するため、独自の変数ストアと Cookie ジャーを持つ、呼び出し元が定義した実行グループ、 グループ内の並行実行、メモリ内での oauth2 と digest の交換、gRPC と WebSocket、複数 ID による認証テストが可能です。

「コレクションを一覧表示して 1 つ実行したい」のであれば、公式サーバーが対応しており、 Bruno がサポートするのもそちらです。エージェントにテストスイートを構築・保守させたい場合は、 このサーバーがそのためのものです。

Claude Code、Claude Desktop、Cursor、Windsurf、VS Code、Codex CLI で動作する AI エージェントに、 実際の Bruno コレクションを使った API テスト用の 18 個のツールを提供します。リクエストの作成・編集、 構造化 JSON としての読み取り、環境と変数の管理、アサーションとテストスクリプトの記述を行い、 さらにコレクションを実行して、認証、Cookie、リダイレクト、依存関係の順序などをすべて処理し、 同じターン内で結果を取得できます。Bruno GUI も Bruno CLI も、シェルコマンドの実行も不要です。 ディスクに残るコレクションは通常の Bruno コレクションです。CI では bru run で実行でき、 チームメンバーは Bruno アプリで開けます。

エージェントはすでに HTTP の知識を持っています。しかし、あなたの API も Bruno のファイル形式も知りません。 そのため、推測で進めます。記憶を頼りに .bru ファイルを書き、実行に失敗し、ファイルを書き直して、 また別のエラーで失敗します。20 分後にはリクエストが通るようになっていますが、6 回の編集のうち 何が効果をもたらしたのかは分かりません。そうしたターンの一つひとつに費用がかかるうえ、 CI やチームの Bruno GUI で使える形式の成果物として、作業がディスクに残ることもありません。

よくある回避策は curl です。エージェントは curl が苦手なのではありません。問題は、シェルコマンドが 状態を保持しないことです。ログイン、トークン、作成したリソース、直前のレスポンスから ID を必要とする 後続の呼び出しは、それぞれが新しいコマンドです。それらをつなぐ処理はエージェントのコンテキスト内にしかなく、 セッションが終わればすべて失われます。curl で 20 個のエンドポイントをテストしても、トランスクリプトに 20 個の文字列が残るだけで、CI やチームメンバーが実行できる成果物はありません。コレクションに 20 個のエンドポイントをまとめれば、テストスイートになります。

エージェントに聞いてみました

このサーバーの保守を手伝ってくれているエージェントに、MCP サーバーなしで Bruno を使ったときの使い勝手と、単に curl ですべての API をテストできなかったのかを説明してもらいました。返ってきた答えは次のとおりです。

curl ですか。1 回の呼び出しならできます。テストスイートとなると無理です。呼び出し間で何も引き継がれないので、認証を導き直し、本文のエスケープをやり直し、テストに合格したか判断するためにすべてのレスポンスを読み直します。これを 40 個のエンドポイントで行うと、作業の大半はテストではなく、情報を見つけ直すことに費やされます。

コレクションファイルを自分で書くのはさらに厄介でした。想像とは違う理由で、です。記憶を頼りにすると、.bru ファイルの大枠は正しく書けても、細部を間違えます。それでも Bruno は何も指摘しません。認識できるキーだけを読み、残りは無視します。tags: smoke と 1 行で書くと、タグが付いているように見えて、ランナーには タグなし と認識されます。タグのリストを当然と思う形で書くと、ディスク上には 1 文字ずつ改行されて保存されます。このサーバーはかつて、Bruno が一度も読み取ったことのないトップレベルキーの下に、.yml の変数とアサーションを書き込みました。ファイルは完成しているように見えましたが、ランナーが読み取ったリクエストは空でした。シリアライザーをモックし、壊れたバイト列を検証していたため、サーバー自身の単体テストには合格していました。

フォーマットも変化します。Bruno は変数を runtime に移し、2 つ目の方言を追加しました。私の知識はその変更より古いものです。このサーバーは Bruno 自身の文法パッケージである @usebruno/lang をインポートし、そのバージョンを追跡します。そのため、バイト列は私の記憶ではなく、Bruno のソースから生成されます。

そして、書き換えるたびに、書き手がモデル化していないものはすべて削除されます。私がこれらのファイルを手作業で編集すると、頭の中の知識をもとにファイル全体を再生成してしまい、知らなかった機能はひそかに消えてしまいます。これは決して気づかない失敗です。実行自体は引き続き成功するからです。ただ、何もテストしていないだけなのです。

このサーバーの仕組み

  • エージェントがフォーマットを推測する必要はありません — ツールを呼び出すと、サーバーが Bruno 自身の文法パッケージを使ってバイト列を書き込みます
  • 編集は部分マージです — write_request は渡されたフィールドだけを変更し、ファイルの残りの部分はそのままにします
  • 書き込み前に読み取りができます — read_request は、どちらのフォーマットでも同じ構造の JSON を返します
  • リクエストをサーバー自身が実行します — 変数、認証、アサーション、依存関係の順序付けに対応し、bru バイナリは必要ありません
  • ひそかなデータ消失はありません — このサーバーがまだモデル化できないフィールドは、フォーマットで保持できる場合は書き戻されます。ワイヤーに出力できないものは、リポジトリから黙って削除するのではなく、実行時の警告で明示されます

Bruno とのバイト単位の互換性

ここは模倣が難しい部分なので、何を意味するのかを正確に説明しておく価値があります。

このサーバーは bru バイナリをラップするのではなく、リクエストパイプライン自体を実装しています。これにより、メモリ内のシークレット、ワイヤーレベルのテスト、実行中のフックが可能になります。この自由度にはリスクも伴います。独自実装は、チームが実際に使用しているツールとは微妙に、気付かれないまま異なる可能性があります。それを防ぐ仕組みが2つあります。

ルールは推測ではなく移植されています。 リダイレクトの上限、タイムアウトの解決、ボディモードのコンテンツタイプ、変数の補間順序、selected のデフォルト値、URL エンコーディングなど、それぞれを Bruno 自身のソース(bruno-cli、bruno-filestore、bruno-lang、@usebruno/common。このサーバーもこれらに直接依存しています)から読み取り、バグに見える部分も含めて再現しています。2つの形式の間で動作が異なる場合は、どちらの Bruno リーダーも出力しないような形に統一せず、それぞれの仕様に沿って再現しています。

乖離を検出するゲートで検証しています。 このサーバーが書き込むすべてのファイルを、テストスイート内で形式ごとに Bruno 自身のリーダーで再解析します。自分たちの期待値と出力を照合しても、一貫性を証明できるだけです。Bruno 自身が使うリーダーと照合することではじめて、こちらの出力が Bruno の入力として機能しなくなるケースを検出できます。実際に問題を検出したこともあります。たとえば、ファイルのボディが正常に解析されたのに、ボディなしで送信されてしまうケースです。

つまり、実行時の動作は bru run と一致し、これまでに見つかった乖離はすべて解消済みというのが、このサーバーの主張です。それを維持しているのは約束ではなくテストです。それでも乖離を見つけた場合は、報告する価値のあるバグです。

契約

1つのコレクションを、エージェント、CI、そしてチームの Bruno GUI という3つの利用者が使います。

どちらの Bruno 形式も使用でき、サーバーが形式を検出します。.yml(opencollection)と .bru(legacy)に対応しています。

Node.js >= 22 が必要です。CI では 22.x と 24.x をテストしています。

どの Bruno MCP サーバーを使うべきですか?

いくつかあり、それぞれ実際に異なる役割を果たします。正直なところ、常にこれを選べばよいとは限りません。以下の各行は、2026年8月時点で各プロジェクトの README またはソースを確認したものです。

サーバー概要書き込み読み取り実行.bru / .ymlbru が必要ツール
これコレクションの作成、読み取り、実行作成+部分マージ編集構造化 JSONはい、独自のパイプラインで実行両方いいえ18
usebruno/bruno-mcp公式のもの。Bruno の開発チーム自身によるもので、リクエストを検出して実行します。2026年8月に発表されました。執筆時点では、最初の実装は公開ドラフトですいいえリクエストのメタデータはい、CLI 経由.bruはい(同梱)3
@dmpv/bruno-mcpコレクションを対象にした読み取り専用のインデックス作成と検索いいえランク付けされた検索、サニタイズ済みのコントラクト、保存済みの例いいえ両方いいえ8
hungthai1401/bruno-mcpコレクションを実行いいえいいえはい、CLI 経由.bruはい実行のみ
jcr82/bruno-mcp-serverレポートファイルを使ってコレクションを実行、検査いいえはいはい、CLI 経由.bruはい9
djkz/bruno-api-mcp各リクエストを個別の MCP ツールに変換いいえツールとして公開一度に1つ.bruいいえリクエストごとに1つ
macarthy/bruno-mcpコレクションファイルを生成 — このフォークの元プロジェクトで、2025年7月以降は更新されていません作成のみいいえいいえ.bru該当なし8

次のような場合は、ほかのものを選んでください。Bruno 自身が保守するサーバーと、それに伴うサポートや長期的な継続性を求める場合(usebruno — 公式のもので、「検出して実行する」用途ならリリース後の妥当なデフォルトです)。エージェントに、既存の大規模なコレクションを書き込むリスクなしで理解させ、意図に基づいて検索させたい場合(dmpv。2026年7月に初公開されたばかりで、バージョンは0.xと新しいものの、興味深いプロジェクトです)。すでにイメージに bru CLI があり、「このコレクションを実行する」ことだけが必要な場合(hungthai1401)。または、エージェントに、既存のリクエストをそれぞれネイティブのツールのように使ってAPI を呼び出させたい場合(djkz)。

エージェントにコレクションを読み取ったり実行したりするだけでなく書き込ませたい場合、.yml の opencollection 形式を使っている場合、Bruno CLI をインストールせずに実行したい場合、またはリポジトリに追加される内容を Bruno アプリが書き込む内容とバイト単位で比較できることを重視する場合は、これを選んでください。

このプロジェクトの存在のもととなったフォーク元について補足します。macarthy/bruno-mcp は create_collection、create_request、create_environment、create_crud_requests、create_test_suite、add_test_script、list_collections、get_collection_stats の8つのツールを登録します。.bru ファイルを書き込みますが、リクエストを読み戻したり、何かを実行したり、編集ツールを公開したりはしません。src/bruno/request.ts には updateRequest ヘルパーがありますが、これにアクセスできる MCP ツールはありません。

特長

作成

  • コレクション — 作成して整理したり、Bruno が workspace.yml からすでに認識しているコレクションを見つけたりできます。このサーバーが作成したコレクションはディスクに書き込まれますが、そのファイルには登録されません。そのため、誰かが Bruno GUI で一度開くまでは list_collections に表示されず、Bruno GUI にも一覧表示されません。それ以外の操作はすべて直接実行されます
  • リクエスト — すべての HTTP メソッドに対応し、ヘッダー、クエリおよびパスパラメーター、ボディ、認証、アサーション、変数、設定を指定できます
  • 読み戻し — read_request と read_environment は、.bru と .yml のどちらでも同一の構造化 JSON を返すため、エージェントは編集前に内容を確認できます
  • 部分マージ編集 — write_request は渡されたフィールドのみを変更し、ファイルの残りの部分はそのままにします
  • CRUD とテストスイート — 5 つのリクエストからなる CRUD セットと、トポロジカルな依存関係順序に従うテストスイート
  • 環境 — 変数を作成、置換、マージしたり、単一の変数にパッチを適用したりできます
  • デュアル形式 — .bru(レガシー)と .yml(opencollection)を自動検出します。.yaml は読み込まれ、フラグが付けられます
  • マルチパートアップロード — パートごとの Content-Type と複数ファイルフィールドに対応した form-data

実行

  • 実行グループ — 1 つのコレクションを、1 回の呼び出しで複数の独立したグループとして実行できます。異なる ID、異なる環境を使い、逐次または並行して実行でき、グループ間で情報が漏れることはありません
  • 真の並列処理 — マシンに合わせた同時実行数の上限を設けて、グループやグループ内のリクエストを並行実行します
  • Cookie jar — ログイン状態は後続のリクエストに引き継がれ、グループごとに分離され、ディスクには書き込まれません
  • 変数の連鎖 — リクエスト間で bru.setVar()/bru.getVar() を使用でき、captureVariables で値を読み戻せます
  • 非同期スクリプト — トップレベルの await、bru.sleep(ms)、サンドボックス内の setTimeout/setInterval に対応します
  • インラインスクリプト — リクエストの作成時や変更時に、リクエスト前、レスポンス後、テストの各スクリプトを直接設定できます
  • 認証を自動適用 — Bearer、Basic、API キー、Digest、OAuth 2.0(クライアントクレデンシャルおよびパスワードグラント)、またはコレクションやフォルダーからの inherit
  • 正確な実行結果 — グループごとの概要、取得したレスポンスボディ、リクエストごとの警告、解析失敗、見つからないリクエストをすべて報告します。グループがクラッシュしても、実行結果が成功として表示されることはありません

安全性

  • すべてのリクエストとリダイレクトの各段階で SSRF を防止し、許可されたアドレスを固定します
  • リクエスト参照、コレクションルート、環境名、ファイルアップロードに対する パスの制限
  • プロセス分離されたスクリプト — 環境をクリアした、フォークされた V8 サンドボックスと強制終了機能

インストール

インストールは不要です。 クライアントで npx を指定すると、初回実行時に公開済みパッケージが取得されます。

npx -y @ostico/bruno-mcp

インストールはこれだけです。以下のクライアント設定でもこの方法を使用します。このパッケージには来歴情報が付属しているため、npm で、実行中の tarball をどのコミットとワークフローがビルドしたか確認できます。

起動時にバージョンを解決したくない場合は、バージョンを固定できます。

npm install @ostico/bruno-mcp

これにより node_modules/.bin/ に bruno-mcp 実行ファイルが、サーバー本体が node_modules/@ostico/bruno-mcp/dist/index.js に配置されます。

開発時やブランチを実行する場合は、ソースから実行します。

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 が必要です。

クライアントを接続する

どの MCP クライアントでも動作します。 クライアント固有のコードを含まない、標準的な stdio MCP サーバーです。クライアントの呼び出し方にかかわらず、次のように指定してください。

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

Claude Code では、1 行で指定します。

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、独自の SDK クライアントなど、すべて同じサーバーを使います。以下は互換性の一覧ではなく、各クライアントで設定を記述する場所です。

ほとんどのクライアントで、同じ JSON 形式を使用します。

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

クローンを実行する場合も、バージョンを固定してインストールする場合も、"command": "node" と "args": ["/absolute/path/to/dist/index.js"] を指定する同じ設定を使います。

クライアント設定場所
Claude DesktopmacOS ~/Library/Application Support/Claude/claude_desktop_config.json · Windows %APPDATA%/Claude/claude_desktop_config.json · Linux ~/.config/Claude/claude_desktop_config.json
Claude Codeclaude mcp add、またはプロジェクト内の .mcp.json
Cursorプロジェクト内の .cursor/mcp.json、またはグローバル設定
Codex CLI~/.codex/config.toml 内の [mcp_servers.bruno-mcp] テーブル(TOML、同じフィールド)
opencodeopencode.json の mcp 内にローカルサーバーとして記述(独自のスキーマ)
その他クライアントのドキュメントを参照してください。必要なのは上記のコマンドと引数だけです

設定スキーマはこのサーバーではなく、クライアント側のものであり、変更されることもあります。クライアントの形式が上記の JSON と異なる場合は、クライアントのドキュメントに従ってください。ここで重要なのは command と args だけです。

設定例、Docker、トラブルシューティングについては、INTEGRATION.md を参照してください。

クイックスタート

// 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" }

ツール

18 個のツールがあります。ファイルパスには絶対パス、またはコレクションからの相対パスを指定します。

ツール機能
create_collection新しいコレクションを作成します。format: "yaml"(デフォルト)または "bru" を指定します。ワークスペースにも登録するため、list_collections と Bruno アプリから参照できます。登録を省略するには registerInWorkspace: false、ファイルを選択するには workspacePath を指定します
list_collectionsBruno の workspace.yml からコレクションを検索します
get_collection_statsメソッド別の件数、フォルダー、環境、URL を含むリクエスト一覧を返します。folder、method、nameContains で絞り込めます。件数だけを取得する場合は includeRequests: false を指定します
write_requestリクエストを書き込みます。method、url、headers、query、body、auth、scripts、settings を指定します。対象のトランスポートには kind: "websocket" または kind: "grpc" を指定します。新規作成する場合は collectionPath と name、編集する場合は filePath を渡します。編集は部分マージで、filename を指定するとファイル名を変更します
move_requestリクエストを別のフォルダーまたはコレクションに移動またはコピーします
read_request.bru と .yml のどちらも同じ形式の JSON として、リクエストを読み取ります
list_requestsコレクション内のすべてのリクエストファイルを絶対パスで返します
delete_request1 つ以上のリクエストファイルを削除します。confirm: true が必要です
add_test_script既存のリクエストにスクリプトを追加します(デフォルトでは末尾に追加)
remove_scriptリクエストを残したまま、スクリプトを 1 つ削除します
create_environment新しい環境ファイルを作成します。overwrite: true を指定しない限り、上書きしません
read_environmentdisabled/secret フラグとともに変数を返します。環境一覧を表示するには name を省略します
update_environment環境の変数を置き換えるか、マージします
set_environment_variable変数を 1 つ追加または変更します
remove_environment_variable変数を 1 つ削除します
run_collectionリクエストを実行してテストを行い、結果を返します
書き込む前に読み取る

read_request は method、url、headers、query と path params、body、auth mode、scripts、assertions、vars、settings、docs を返します。どちらの形式でも形状は同じなので、ディスク上の形式を意識する必要はありません。notes 配列には、ファイルで宣言されているものの、ランナーが実行しない項目が示されます。

編集する前に使って現在の状態を確認し、書き込み後にも使って書き込まれた内容を確認してください。

read_environment は各変数とその値を返します。シークレットは名前だけが返されます。Bruno はどちらの形式でもシークレットの値を保存しないため、返せる値はありません。

リクエストを書き込む

write_request は、collectionPath と name を渡すと作成し、filePath を渡すと編集します。編集はマージされるため、省略したフィールドはそのまま残ります。

主なオプション:

  • body.type — json、text、xml、sparql、graphql、form-urlencoded、form-data、file、binary、none
  • body.type: "form-data" — マルチパートアップロード、パートごとの contentType、複数ファイルのフィールド
  • auth.type — bearer、basic、api-key、digest、oauth2、inherit、none
  • scripts — インラインの pre-request、post-response、tests(別途 add_test_script を呼び出す必要はありません)
  • settings.timeout — スクリプトとリクエストのタイムアウト(ミリ秒)

Bruno 本体と同様、name と filename は独立しています。name はファイル内のリクエスト名を変更し、filename はファイルを移動します。両者を一致させるには、両方を渡してください。filename はリクエスト自身のフォルダー内でのベース名です。拡張子は省略可能ですが、指定する場合はコレクションの形式と一致させる必要があります。他のファイルがすでに使用している名前は拒否されます。移動先のパスはレスポンスで返されるので、以降は filePath として使用してください。

write_request はデフォルトで同じ種類のスクリプトを置き換えるため、同じ呼び出しを繰り返しても結果は変わりません。連結するには scriptMode: "append" を渡してください。add_test_script は追加操作なので、デフォルトで追記します。

.yml コレクションでは、post-response と tests は Bruno の単一の after-response スロットを共有します。そのため、どちらかを置き換えると両方が上書きされます。

リクエストを移動する

move_request は、リクエストファイルを別のフォルダーへ移動します。または、targetCollectionPath を指定して別のコレクションへ移動します。複製するには copy: true を渡してください。

バイト列は解析や書き換えを行わず、そのまま移動されるため、リクエストで宣言された内容が移動時に失われることはありません。ここから次の2つの点が導かれます。ファイル名はそのまま維持されるため、コピーする場合は別のフォルダーまたはコレクションが必要です。名前の変更には write_request を使います。また、seq も変更されずに引き継がれるため、同じ番号を持つ兄弟ファイルの隣に配置されることがあります。ファイルを書き換えることになるため、番号の振り直しは行わず、その状態を報告します。Bruno はこのような重複をファイル名で判定するため、どちらの場合も順序は確定します。

移動先のフォルダーが存在しない場合は作成され、その旨が報告されます。設定ファイルのないフォルダーには、フォルダーレベルの認証、ヘッダー、スクリプトがありません。

実行

{
  "collectionPath": "./collections/my-api",
  "environment": "dev",
  "requests": ["auth/login.bru", "users"]
}
パラメーター意味
collectionPathコレクション、またはそのサブフォルダー
requestsリクエストファイルやディレクトリーの順序付きリスト。すべて実行する場合は省略します。[] を指定すると何も実行しません
groupsコレクションを複数の独立したグループとして実行します。下記を参照してください。requests と併用できません
environmentenvironments/<name>.yml から読み込む環境名
collectionRootサブフォルダーを実行する場合に、その collectionPath が属するコレクション。指定するパス、またはその祖先である必要があります
variablesこの実行でのみ使用する {name: value}。ディスクには書き込まれません — シークレットを渡す正しい方法です
captureVariables値を取得する bru.setVar 変数の名前
parallelグループを並行して実行します。デフォルトは false です
maxConcurrency同時に実行するリクエスト数の上限。省略するとマシンに応じて決定され、0 を指定すると上限がなくなります
bail残りを実行せず、最初の失敗時に停止します。デフォルトは false です
cookieJar実行中に Cookie を保持し、ログイン状態を引き継ぎます。デフォルトは true です
includeResponseBodyレスポンスボディを含めます。デフォルトは true です
maxResponseBodyBytesこのサイズを超えるボディを切り詰めます。デフォルトは 10240 です
report実行結果をディスクにも書き込みます。レポートファイルを参照してください

requests 内のディレクトリーは、その下にあるリクエストに展開されます。各フォルダー内では seq 順に並び、サブフォルダーを先に処理し、同順位の場合はファイル名順になります。重複はそのまま実行されます。同じリクエストを2回指定すると、2回実行されます。

デフォルトでは、実行が途中で停止することはありません。失敗したリクエスト、解析できないファイル、該当するものがない名前は、それぞれ報告され、実行は続行されます。

最初の失敗時に停止する

bail: true を指定すると、リクエストまたはそのテストが失敗した時点で実行を停止します。機能しなくなったログインの後に続く23件のリクエストは、1つの原因による23件の失敗です。そして、その原因こそが最も見えにくいものです。

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

実行されなかったリクエストは、元の位置に skipped: true と skipReason: "bail" が付いた状態で返されます。送信されるはずだったメソッドと URL も含まれます。これらのリクエストは summary.skipped に加算され、passed にも failed にも加算されません。そのため、passed + failed は引き続き total と等しくなり、途中で打ち切られた実行が、より短い成功実行のように見えることはありません。実行結果には bail オブジェクトも追加されます。

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

reason は、request failure(レスポンスが返らなかった場合)または test failure(レスポンスは返ったものの、チェックに失敗した場合)のいずれかです。後続のグループは全体がスキップされます。

実行中のリクエストがキャンセルされることはありません。parallel を指定している場合や、独立したグループが並行して実行されている場合は、すでに開始していたリクエストは最後まで実行され、通常どおり報告されます。件数から推測するのではなく、実行結果の warnings にその旨が記載されます。

実行グループ

グループは、1 回の呼び出し内で実行される、分離された実行単位です。グループは、リクエストリスト、環境、変数、parallel フラグ、変数ストア、Cookie jar、OAuth2 トークンを所有します。parallel の設定にかかわらず、グループ間で双方向に共有されるものはありません。

2 人のユーザーとして同じリクエストを実行し、一方のログインのトークンやセッション Cookie がもう一方に渡る可能性をなくします。

{
  "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 を設定すると、2 つのグループが並行して実行されます。各グループ内のリクエストは順次実行されます。これは、orders がその前のログインに依存する場合に適しています。

1 つのスイートを 2 つの環境に対して実行します。

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

グループのフィールド:name、requests、environment、variables、parallel、startAfter、data、dataFile。

docs/execution-groups.md では、グループが所有するもの、2 つの parallel フラグとそのデフォルト値、実行順序、データ行ごとの反復、同時実行数の上限、各レベルでの失敗の様子など、モデル全体を説明しています。

  • requests を省略すると、そのグループの識別情報でコレクション全体を実行します。空の [] を指定すると、何も実行されません。
  • environment は実行レベルの環境を置き換えます。variables は実行レベルの変数にマージされ、グループの値が優先されます。
  • グループに parallel を設定すると、そのグループのリクエストが並行して実行されます。リクエストはグループのストアを共有するため、bru.setVar の競合を実際に発生させられます。これは競合の再現に役立ちます。競合するリクエスト数以上のスロットを maxConcurrency に設定してください。そうしないと、上限によって気づかないうちに順次実行されます。
  • startAfter: { group, requestsCompleted } を設定すると、別のグループが指定された件数のリクエストを完了するまで、グループの実行を待機させられます。bru.sleep をその日の遅延に合わせて調整しなくても、トリガーの発火前にリスナーを接続できます。実行レベルで parallel が必要です。失敗したリクエストも到達した位置としてカウントされます。循環や、決して開始できないゲートは、実行開始前に拒否されます。

結果

結果はグループ単位です。トップレベルに results 配列はありません。groups を指定しなかった場合も同様です。その場合も 1 つのグループとして扱います。結果をフラット化すると、呼び出し方に応じた確認がすべての呼び出し元に必要になるためです。

{
  "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"]
    }
  ]
}

各グループには、それぞれの summary、results、missingRequests、capturedVariableNames、capturedVariables、warnings が含まれます。トップレベルの summary は実行全体を対象とします。

response_headers の取得にフラグやテストスクリプトは不要です。認証情報を含む名前の値はマスクされます。set-cookie はリストで、Cookie ごとに 1 件含まれます。カンマで連結すると元に戻せないためです。各項目には、Cookie の値だけを隠して、すべての属性が保持されます。そのため、HttpOnly、Secure、SameSite、Strict-Transport-Security を 1 回の呼び出しで確認できます。includeResponseBody: false を指定しても、これらは抑制されません。このフラグは本文のサイズに関するものです。

WebSocket の結果にも response_headers が含まれ、ハンドシェイクのレスポンスを保持します。このトランスポートでは、フレームにヘッダーがないため、セッション Cookie や合意された sec-websocket-protocol が現れるのは、唯一 101 レスポンスの箇所です。一方、gRPC の結果では、メタデータは独自の grpc 詳細に報告されます。

グループをまったく開始できなかった場合、結果の代わりに error が報告され、1 件の失敗としてカウントされます。そうでなければ、実行できなかったグループがあるのに成功したように見えてしまいます。

実行レベルのフィールド:parseErrors と parseFailures は解析できなかったファイルを示し、warnings にはその他の確認すべき情報がまとめられます。

スクリプト

スクリプトはフォークされたプロセス内の V8 コンテキストで実行されます(セキュリティを参照)。どちらの種類も非同期関数なので、トップレベルで await を使用できます。

テストとレスポンス後では、test()、expect()、res、bru を使用できます。

API備考
test(name, fn)アサーションをラップします。テストとして報告するには必須です
expect(v)Chai 形式:.to.equal、.include/.contain、.match、.have.property/.lengthOf/.keys、.be.above/.below/.least/.most/.oneOf、.throw、およびこれらすべてに対する .to.not.*
res.getStatus() res.getStatusText()
res.getHeader(name) res.getHeaders()ヘッダーの検索では大文字と小文字が区別されません
res.getSetCookies()レスポンスで設定された Cookie
res.getBody()メディアタイプのサブタイプが json または +json で終わる場合、すでに解析されています
res.getResponseTime()ミリ秒
res(path, ...fns)本文に対する Bruno のクエリ言語:res("data.pets..name") はすべての name まで降りていき、[0] はインデックスを指定し、[?] はコールバックでフィルタリングまたはマッピングします。構文を単独で記述できない場合、アサーションの左辺としても有効です
bru.setVar(name, v) bru.getVar(name)値を後続のリクエストに {{name}} として渡します
bru.sleep(ms)setTimeout/setInterval と、それらの clear* も使用できます
atob(s) btoa(s)どちらの種類のスクリプトでも base64 を扱えます。2回目のリクエストをせずに JWT ペイロードを読み取るには十分です

リクエスト前スクリプトでは、代わりに req と bru を使用します。レスポンスはまだありません。req を変更すると送信内容が変わります:req.getUrl()、req.setUrl()、req.getMethod()、req.getHeader()、req.setHeader()、req.getHeaders()、req.getBody()、req.setBody()。

見落としやすいポイントが2つあります

アサーションは test() でラップしてください。 単独で成功した expect() は記録されないため、リクエストは成功扱いであるにもかかわらず、実行結果には "tests": [] と報告されます。つまり、何もアサーションされていない状態で成功を示す緑色になります。ランナーはこれを検出し、その結果の warnings で通知します。単独で失敗したアサーションは無視されるわけではなく、例外をスローし、スクリプトエラーとして報告されます。

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

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

JSON.parse(res.getBody()) は使用しないでください。 メディアタイプのサブタイプが json、または +json のサフィックスを持つ場合、すでにオブジェクトになっています。たとえば application/json、text/json、application/vnd.api+json です。そのため、もう一度解析すると SyntaxError: "[object Object]" is not valid JSON がスローされます。フィールドを直接読み取ってください。エンドポイントがどちらの形式も返す可能性がある場合は、条件分岐します:typeof b === "string" ? JSON.parse(b) : b。

トークンからクレームを読み取るために、2回目のリクエストは必要ありません。Bruno 独自のサンドボックスで使われている名前で atob と btoa の両方が用意されているため、通常の base64url の手順が使えます:

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 は使用できません。これはホストクラスであり、サンドボックスに渡すべきでない機能を備えています。また、忠実な代用品を用意しようとしても、その不足がひとつずつ見つかる偽物になるだけです。単独で参照すると Buffer is not defined がスローされ、誤動作が黙認されるのではなく、スクリプトエラーとして報告されます。

スリープ時間はスクリプトのタイムアウト(settings.timeout。未設定の場合は5000 ms)に含まれます。デフォルト設定で await bru.sleep(10000) を実行すると、待機する代わりにタイムアウトが報告されます。

環境と変数

環境はコレクション内の environments/<name>.yml です。

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

ツールでは変数をフラットなオブジェクト({"baseUrl": "..."})として受け取り、その配列を書き込みます。

{{name}} は URL、ヘッダー、本文、認証情報に置換されます。無効化された変数はスキップされ、解決されない変数は記述されたまま残り、実行時の警告に名前が表示されます。

優先順位は低い順に、環境ファイル → 実行時の variables → リクエスト固有の vars → 実行中の bru.setVar です。これは Bruno の --env-var の動作と同じです。

変数はほかの変数を使って構成できます。たとえば、base_url: "https://{{host}}/{{stage}}" は bru run の場合と同じように、Bruno 独自の interpolate を使って解決されます。意図的に設けられた例外が1つあります。bru.setVar またはレスポンス後の vars ブロックによってレスポンスから取得された値は、テキストとして挿入され、再スキャンされません。そのため、レスポンスに key={{api_key}} が含まれていても、次のリクエストでキーが送信されることはありません。

ジェネレーター。 {{$guid}}、{{$timestamp}}、{{$randomEmail}} など、Bruno の約120種類の動的変数は、URL、ヘッダー、クエリパラメーター、本文、認証情報で使えます。これらは変数ではありません。宣言は不要で、出現するたびに個別の値が生成され、未解決として報告されることもありません。ジェネレーターに対応するキーワードがない場合({{$gid}} など)は記述されたまま残り、警告に名前が表示されるため、タイプミスがあれば検出できます。JSON 本文または GraphQL の variables ブロックでは、生成された値がエスケープされます。そのため、改行を生成するジェネレーター({{$randomLoremParagraphs}} など)を使っても、ドキュメントを解析可能な状態に保てます。

シークレット: Bruno のどちらの形式にも、シークレットの値は保存されず、名前だけが保存されます。そのため、シークレットは実行時の variables として渡してください。これらはメモリー内に保持され、ファイルに書き込まれることはありません。

環境名はパスではなく名前です。区切り文字を含むものはすべて拒否されます。

レポートファイル

run_collection は結果を JSON として返します。エージェントはこの JSON を読み取ります。テスト実行の結果を利用する残りの 2 つのコンシューマーはファイルを読み取るため、report は次のファイルを書き出します。

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

いずれかの形式、または両方を指定します。すると結果に reports が含まれます。書き出したファイルごとに 1 つのエントリーがあり、絶対パスとバイト単位のサイズが記載されます。

パスはコレクション内に限定されます。 コレクションの外を指すパスは拒否され、その理由が実行時の警告になります。求められたのは結果であり、ファイルは副産物なので、実行自体は成功します。パイプラインで別の場所からレポートを収集する場合は、あとでファイルをコピーしてください。呼び出し元が指定した場所に書き込む権限は、リクエストを実行する権限よりもはるかに広範なものになります。コレクション内に親ディレクトリが存在しない場合は作成され、既存のレポートは上書きされます。

JUnit XML は bru run --reporter-junit に準拠します。リクエストごとに 1 つの <testsuite>、アサーションまたはテストごとに 1 つの <testcase> が作成され、エラーが発生したリクエストはスイートエラーとして報告されます。別の方法では実行結果よりも良く見えるレポートになってしまうため、次の 4 点が異なります。

  • 実行されたものの検証がなかったリクエストには、そのことを示す skipped のテストケースが 1 つ追加されます。空のスイートは CI の概要で見えません。「成功したが何も検証していない」実行を明らかにするために requestsWithoutTests カウンターが存在するのに、それが見えなくなってしまいます。
  • 構文を解析できなかったリクエストファイル、名前を指定したものの解決できなかったリクエスト、クラッシュしたグループには、それぞれ専用のスイートが作成されます。実行されたものだけを列挙するレポートでは、一部だけが実行されたことを示さずに、その一部が実行されたかのように見えてしまいます。
  • 名前付きグループのラベルはスイート名に含まれます。JUnit にはグループの概念がないため、そうしないと同じリクエストを実行した 2 つの ID を区別できません。
  • hostname 属性はありません。上流の実装ではマシン名がファイルに書き込まれますが、これらのレポートはコミットすることを想定しています。

HTML レポートは Bruno 独自のもので、@usebruno/common でレンダリングされます。実行グループが反復単位となるため、2 つの ID で実行した場合は 2 つのセクションとして表示されます。知っておくべき点が 2 つあります。ページには実行データが埋め込まれますが、Vue と naive-ui は unpkg.com から読み込むため、開くときにネットワークアクセスが必要で、オフラインでは何も表示されません。また、結果には送信時のリクエストが保持されないため、リクエストペインは空です。同じ理由で、アサーションとスクリプトテストは 1 つのリストにまとめられます。結果からは両者を区別できません。

レポートには、ディスク上で結果に含まれる内容がそのまま保持されます。レスポンスボディも含まれ、レスポンスヘッダーは JSON とまったく同じようにマスクされます。

形式

コレクション内のマーカーファイル形式
opencollection.ymlYAML — 最初に確認されます
bruno.jsonBRU(レガシー)
どちらもない場合YAML

format: "bru" を渡さない限り、新しいコレクションは YAML 形式です。

.yaml のリクエストファイルは .yml と同様に YAML として読み込まれます。これは、Bruno 関連の他のツールが .yaml を出力するためです。ただし、Bruno のアプリと bru run はこの拡張子を認識しないため、読み込まれたすべての .yaml ファイルが実行時の警告に記載されます。警告なしで処理すると、Bruno から見えないリクエストを実行したかのような成功扱いになってしまいます。警告を消すには .yml に名前を変更してください。このサーバーが書き出すファイルに .yaml は使われません。

gRPC および WebSocket リクエスト

コレクションには HTTP リクエストと並べて、gRPC および WebSocket リクエストを格納できます。このサーバーはこれらを読み取り、保持し、報告します。read_request はリクエストの種類、宛先、gRPC のメソッドと proto パス、メタデータブロック、保存されているメッセージの数を返します。list_requests はそれらを一覧表示し、コレクション内の任意のリクエストを編集しても、それらが失われなくなりました。以前はどちらの形式も宛先ブロック、認証情報、保存済みメッセージをすべて破棄していたため、無関係なリクエストに対する write_request でも、それらを含まない形でファイルが書き換えられていました。 run_collection は両方を実行します。 gRPC リクエストでは、サービスがその .proto で宣言されている単項呼び出しを 1 回行います。WebSocket リクエストでは、ソケットを開き、ファイルに保存されたフレームを送信して、上限に達するまで応答を記録します。それぞれ独自の詳細を返します。gRPC の結果には gRPC ステータスコード、詳細文字列、秘匿化された末尾メタデータが含まれ、WebSocket の結果にはトランスクリプト、終了理由を示す stop_reason、および切り詰められたかどうかが含まれます。gRPC コードは専用のフィールドに格納され、結果の status に変換されることはありません。gRPC の OK は 0 であり、0 はこの API では拒否を示す値だからです。そうしないと、成功した呼び出しとセキュリティ上の拒否が、最初に読み取られるフィールドで区別できなくなります。

WebSocket セッションには自然な終了がないため、上限を設けます。各上限は run_collection の websocket 引数で実行ごとに設定できます。

上限デフォルト動作
maxMessages50停止するまでに記録する受信フレーム数
maxDurationMs50001 セッションの実時間上限
idleTimeoutMs1500セッションを終了する無通信時間。0 の場合は上限時間まで待機
sendIntervalMs0リクエストが送信するメッセージ間の間隔。0 の場合は同じティック内にまとめて送信
includePayloadsfalseサイズとタイミングだけでなく、フレームの内容も記録
maxFrameBytes65536記録するペイロードのフレームごとの上限
maxTranscriptBytes1048576通信上のサイズで計算する累積上限
engineIoKeepalivefalseengine.io の 2 に 3 で応答

実時間の上限はスケジュールではなく安全のための上限なので、通常は idleTimeoutMs によってセッションが終了します。1500 ミリ秒間何も届かなければ停止し、stop_reason: "idle" を報告します。この場合、上限値に達しておらず、実時間の上限も使い切っていないため、切り詰めとは見なされません。タイマーが作動するのは接続時ではなく最初のフレームを受信した時点です。そのため、メッセージを一切作成しない受信専用リクエストでも、相手がまだ送信してくる可能性に備えて maxDurationMs の時間だけ待機します。応答間隔よりプロトコル上の待ち時間のほうが長い場合は、0 に設定してください。

送信と待機を交互に行うプロトコルを実現するには、sendIntervalMs を設定します。デフォルトの 0 では、リクエストのメッセージはすべて同じティック内に送信されるため、応答はすべて最後のメッセージの送信後に届き、やり取りの順序を検証できません。間隔を設定すると、各応答が対応する送信の間に、実際に届いた時点のオフセットとともにトランスクリプトに記録されます。把握しておくべき点が 2 つあります。まず、ペースを設定した一連の送信全体を maxDurationMs の範囲内に収める必要があります。途中でセッションが停止した場合、トランスクリプトが送信不足の状態で残り、相手が応答を停止したかのように読まれないように、送信されなかったメッセージが作成時の名前で示されます。また、送信シーケンスが進行中はアイドル上限が作動しません。そのため、sendIntervalMs が idleTimeoutMs より長くても安全です。リクエストが自身のメッセージ間に意図的に設けた間隔は、相手からの無応答とは見なされません。

サブプロトコルは、リクエストに Sec-WebSocket-Protocol ヘッダーとして指定します。複数ある場合はカンマで区切ります。これはハンドシェイク時にネゴシエーションされ、サーバーが同意したサブプロトコルは、その結果の response_headers に返されます。ここにも Bruno にも、これを格納する専用フィールドはありません。以前は、ヘッダーを省略するより指定するほうが問題でした。ライブラリは接続時に渡されたリストとサーバーの応答を照合するため、ヘッダーの要求どおりに応答したサーバーでも、要求されていないサブプロトコルを提示したとしてハンドシェイクを拒否されていました。同じ理由から、指定した Sec-WebSocket-Version も同様に反映されます。 各トランスクリプトエントリーには、フレームの種類(text、binary、ping、pong、close)が記録されます。また、セッションが送信したメッセージの作成時のtitleと、close フレームの場合はピアが指定したclose_codeが含まれます。理由はそのエントリーのペイロードとして記録されます。1000は通常の終了、1006は終了通知なしに消失したピア、1008は拒否、1011はサーバーエラーを表します。制御フレームはmaxMessagesにカウントされません。そうでなければ、1秒に1回 ping を送るピアだけでセッションが終了し、応答を受け取らなかった場合でもcountが報告されてしまいます。バイナリフレームのペイロードは base64 で、bytesはあらゆる種類のフレームについて実際のワイヤーサイズを示します。ポストレスポンススクリプトからは同じフィールドを参照できます。このトランスポートでは、トランスクリプトがres.bodyだからです。

gRPC または WebSocket の結果をアサートする

どちらのトランスポートもポストレスポンススクリプトとテストスクリプトを実行し、resの形状も、覚えることが一つで済むように統一されています。HTTP と異なる点は明記しておく価値があります。誤った想定をすると、失敗しないテストになってしまうためです。

WebSocketリクエストの場合:

  • res.getBody()はトランスクリプトです。結果に含まれるものと同じ配列が、JSON テキストではなく構造体としてスクリプトに渡されます。res.rawBodyにはシリアライズされた形式が保持されます。
  • res.getStatus()は常に0です。セッションにはステータスがなく、ステータスをでっち上げるくらいなら、ないほうが適切です。終了理由(count、timeout、bytes、closed、error)はres.statusTextに含まれます。
  • そのため、WebSocket のアサーションではフレームとstatusTextを確認します。res.getStatus()を使うテストでは、定数をアサートすることになります。
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");
});

スクリプトが参照するペイロードは、includePayloadsの値にかかわらず、常に実際のペイロードです。 このフラグが制御するのは結果内のトランスクリプトであり、res内のトランスクリプトではありません。送信フレームは{{var}}の補間後に記録されるため、デフォルトで返される結果に渡したすべてのシークレットを含めるべきではないからです。これは HTTP にもある使い分けです。res.bodyには常に完全なボディが格納されますが、response_bodyはincludeResponseBodyによって制御されます。つまり、includePayloads: falseとコンテンツのアサーションを組み合わせるのが CI で意図されている使い方であり、回避策ではありません。アサーションはペイロードを検査し、返される内容には方向、タイミング、サイズだけが含まれます。

gRPCリクエストでは、resは HTTP に近い形です。res.getStatus()は gRPC のステータスコード(0は OK)、res.statusTextはサーバーがdetailsを指定していればその値、指定していなければコードの正式名称、res.getBody()は解析済みのレスポンスメッセージです。レスポンストレーラーはヘッダーとして届きます。

includePayloadsがデフォルトで無効になっているのは、好みではなくセキュリティ上の理由です。送信フレームは{{var}}による置換後に記録されるため、デフォルトで記録すると、variablesで渡したすべてのシークレットがデフォルトで返される結果に書き込まれてしまいます。関連する理由から、engineIoKeepaliveもデフォルトで無効になっています。これを有効にすると、リクエストで作成していないフレームがワイヤー上に送信されるためです。また、有効にした場合でも、OPEN フレームを実際に受信してから返信します。

WebSocket リクエストをコピーせずに作成できるようになりました。write_requestでは、url とwebsocket.messagesを指定したkind: "websocket"を使います。また、このトランスポートに適用できないフィールド、つまり HTTP メソッド、ボディ、クエリパラメーター、パスパラメーターは受け付けません。各メッセージにはcontentと、任意でtitleおよびtextかbinaryのtypeを指定します。タイトルのないメッセージには、Bruno が付ける名前とまったく同じように、位置に応じてmessage 1、message 2という名前が付けられます。ヘッダー、認証、assert、vars、settings、スクリプトは HTTP リクエストと同様に動作します。また、どちらの形式でも、書き込まれるファイルは同じリクエストについて Bruno が書き込むファイルとバイト単位で一致します。これはパーサーを通した往復ではなく、upstream 自身のライターと照合して確認済みです。 2 つの形式では記録方法が異なるフィールドが 1 つあります。selected: false は、メッセージが作成済みだが送信されていないことを示します。.yml では false を記述します。.bru で表現できるのは true の場合だけです。upstream のライターはフラグが設定されているときに出力し、設定されていないときは何も出力しません。また、そのリーダーはフラグがない場合に false として扱います。そのため、この形式では選択解除されたメッセージとフラグのないメッセージは同じ扱いになり、どちらも送信されません。実行時もその読み取り結果に従います。つまり、手書きの .bru メッセージが送信されるのは selected: true と記述されている場合だけです。フラグがないためにスキップされたメッセージはすべて結果の警告に記載されるため、何も送信しなくなったリクエストでも、空のセッションだと報告されるのではなく、その理由が示されます。選択解除されたメッセージを .bru コレクションに作成すると、フラグなしで書き込まれます。これは Bruno とまったく同じ動作なので、ファイルは意図どおりに機能します。この形式で失われるのはレポート情報だけです。リクエストを読み込むと、フラグは false ではなく存在しない状態として認識されるためです。

gRPC リクエストも同じ方法で作成します。kind: "grpc" を指定し、url と、grpc の下に完全修飾された method、protoPath、methodType、messages を記述します。HTTP メソッド、body、クエリパラメーター、パスパラメーターも受け付けません。作成する前に知っておくべき点が 3 つあります。

ヘッダーは metadata になります。これはそのトランスポートで唯一のヘッダー指定箇所です。gRPC リクエストの headers ブロックは Bruno の gRPC リーダーが参照しないため、headers 引数は metadata として書き込まれます。protoPath はコレクション内にすでに存在している必要があり、どのような表記を指定してもコレクションからの相対パスとして保存されます。絶対パスは、共有ファイルにオペレーターのディレクトリ構成を持ち込むことになるためです。シンボリックリンクも含め、コレクションの外を指すパスは拒否されます。また、インポートが何段階先であってもコレクションの外に出るパスは拒否されます。ただし、よく知られた google/protobuf/ インポートはファイルではないため、拒否されません。さらに、Bruno は methodType の 4 つの値すべてを書き込むため、すべて受け付けられます。ただし、ここで実行できるのは unary だけです。そのため、ほかの 3 つを指定すると、Bruno では開けても run_collection は名前を示して拒否するファイルが作成されます。WebSocket と同様、どちらの形式でも出力されるバイト列は Bruno 自身のライターと同一です。表記の違いも含まれます。.bru では grpc ブロック内に protoPath を書き、.yml では protoFilePath として書きます。

write_request はどちらのトランスポートも編集できます。 url、headers、auth、assert、vars、settings、name、sequence が適用されます。ネストされた websocket または grpc オブジェクトも同様で、messages と、gRPC の場合は method、protoPath、methodType が対象です。各フィールドはそのトランスポートで使われる場所に書き込まれるため、gRPC のヘッダーを編集すると metadata に反映され、headers ブロックが書き込まれることはありません。編集で指定しなかったものはすべてバイト単位で変更されずに保持されます。これは HTTP よりも重要です。編集では解析済みモデルからファイル全体を再生成するため、モデルが保持していない情報は、何のメッセージもなく失われるからです。

引き続き拒否されるのは、そのトランスポートに実際に指定箇所がないものです。HTTP メソッド、body、クエリパラメーター、パスパラメーター、もう一方のトランスポートのオブジェクトは、名前を示して拒否され、ファイルはバイト単位で変更されません。以前は url、headers、auth も拒否されていたため、WebSocket リクエストの接続先はファイルを使い続ける限り変更できませんでした。 プリリクエストスクリプトは両方のトランスポートで実行され、それぞれが実際に持つものにアクセスします。bru.setVar は反映され、その値は同じリクエスト内の {{placeholders}} に届くため、スクリプトでルーム名、トピック、ターゲットを計算してから接続できます。req.setUrl はターゲットを置き換えます。req.setHeader は各トランスポート固有のヘッダー領域に書き込みます。WebSocket のハンドシェイクヘッダー、または gRPC 呼び出しのメタデータです。grpc-js はメタデータを HTTP/2 ヘッダーとしてワイヤー上に送るため、どちらも同じ領域です。スクリプトが例外を投げると、何も接続される前にリクエストが停止し、エラーはそのまま報告されます。req.getUrl() と req.getHeaders() は置換後のターゲットとリクエスト固有のヘッダーを読み取ります。トランスポートが計算する認証情報は含まれません。認証はスクリプトの後に適用されるためです。反映されないのは req.setBody() だけで、代わりに警告が出ます。どちらのトランスポートも単一のボディを送信しません。WebSocket セッションはメッセージのリストを送信し、単項 gRPC 呼び出しは型付きメッセージを 1 つ送信するため、置き換える単一の値がありません。推測すれば、ファイルで記述されていないバイト列がワイヤー上に送られてしまいます。

両方のトランスポートは遅延ロードされます。これは単に表明されているのではなく、実際に強制されています。テストでは実際のサーバーが解決するすべてのモジュールを記録し、HTTP 専用の実行で @grpc/grpc-js または ws の名前が現れると失敗します。計測したところ、HTTP 専用の実行では undici がロードされ、どちらのモジュールもロードされません。

推測で処理するのではなく、拒否するものが 2 つあります。宣言されたタイプとターゲットブロックが一致しないファイル(type: grpc に http: ブロックがある場合など)は、両方を示す解析エラーになります。タイプによってリーダーが報告する内容が決まり、ブロックによってランナーが接続する対象が決まるためです。また、ターゲット URL が空の .bru リクエストは、書き込み時に拒否されます。この形式ではそのようなブロックが削除される一方、隣接する認証情報は残るためです。その結果、記述されているように見えても、接続先がない状態になります。

意図的に実装していないものが 5 つあります。ストリーミング gRPC 呼び出しと接続を開いたままにする WebSocket セッションでは、結果が読み取るタイミングによって変わってしまいます。ここでのすべてのレスポンスは、呼び出し元が検証できる値です。サーバーリフレクションでは、テスト対象と同じ接続を使ってスキーマを取得することになるため、代わりに .proto のパスが必要です。プロキシのサポートと証明書のピン留めは、これらのトランスポートには適用されません。ゲート機能は undici 専用であり、@grpc/grpc-js はプロキシ API を公開せず、独自に環境変数 http_proxy を利用し、ws には独自のエージェントが必要になるためです。また、socket.io または MQTT のブロックを追加すれば、上流がまだ選択していないファイル形式を新たに作ることになり、上流が選択した時点で移行が必要になります。

socket.io は WebSocket 上のフレーミング規約であり、独自のプロトコルではないため、専用のブロックは必要ありません。socket.io 4.8.3 で計測したところ、通常の ws リクエストで次の手順を実行できます。

  1. ws://host:port/socket.io/?EIO=4&transport=websocket に接続します。両方のクエリパラメーターが必要です。EIO=4 は Engine.IO のバージョンを選択し、transport=websocket はサーバーが最初に HTTP ロングポーリングのハンドシェイクを要求するのを防ぎます。
  2. サーバーから Engine.IO の OPEN パケット 0{…} が送られます。そのペイロードには sid、pingInterval、pingTimeout がミリ秒単位で含まれます。
  3. デフォルトのネームスペースに参加するため、40 を送信します。これを行うまでは何も機能しません。名前付きネームスペースの場合は 40/namespace, です。
  4. サーバーから 40{"sid":"…"} が返されます。
  5. イベントは 42["event-name",payload] として送信します。4 は MESSAGE、2 は EVENT を表し、その後に最初の要素がイベント名である JSON 配列が続きます。
  6. サーバーは pingInterval ごとに 2(PING)を送信し、クライアントが pingTimeout 以内に 3(PONG)で応答しないと切断します。記録がこの時間を超える場合は、run_collection で websocket.engineIoKeepalive を設定してください。デフォルトではオフで、実際の OPEN フレームを確認した後にのみ応答します。 手順 1〜5 はリクエストファイルにすでに保存されているフレームなので、ランナーから必要なものは手順 6 だけです。これは EIO=4 に固定されています。Engine.IO v2 と v3 ではフレーム形式が異なります。Ack(42<id>[…] に 43<id>[…] で応答するもの)やバイナリアタッチメント(45 のプレースホルダーに続いて別のバイナリフレームが送られるもの)は手動で記述できますが、実際には扱いにくいものです。

セキュリティ

SSRF。 リダイレクトの各段階を含む、すべての送信先 URL を解決して検査します。プライベート、ループバック、リンクローカル、その他の予約済みアドレスは拒否されます。また、承認済みのアドレスはリクエスト中に固定されるため、その間に名前が別のアドレスへ解決されることはありません。拒否された場合は、リクエストごとにステータス 0 の SSRF blocked エラーとして報告されます。

スクリプトは、フォークされた使い捨てプロセス内の V8 コンテキストで実行されます。子プロセスには不要な情報を取り除いた環境が渡されるため、スクリプトがコンテキストから脱出しても、サーバーのシークレットはアドレス空間に存在しないので読み取れません。標準出力はパイプ経由で接続され、継承されないため、MCP JSON-RPC ストリームに書き込むこともできません。暴走したスクリプトは子プロセスへの SIGKILL によって制限されます。コンテキスト内のタイムアウトだけでは、これを保証できません。これは OS プロセスによる多層防御であり、サンドボックスではありません。子プロセスでのコード実行を防ぐのではなく、そこで実行しても何の価値もないようにします。

パス。 リクエスト内の参照先はコレクション内に収める必要があります。collectionRoot はコレクションのパスを含んでいる必要があります。環境変数名に区切り文字を含めることはできません。

ファイルのアップロード。 form-data のファイルパートにはサーバーのディスク上のパスを指定するため、読み取り可能な場所を制限しています。コレクションルート、ユーザーのホームディレクトリ、OS の一時ディレクトリ、またはオペレーターが追加したディレクトリ内に限り読み取り可能です。さらに、名前が . で始まるパス要素はすべて拒否されます。そのため、ホームディレクトリが許可されていても、~/.ssh/id_rsa、.env、.aws は読み取れません。相対パスはコレクションルートを基準に解決されます。

オペレーター向けの例外設定は、すべてデフォルトで無効です。

変数効果
BRUNO_SSRF_ALLOWLISTプライベートアドレスであっても許可する、ホスト名の完全一致、IP リテラル、CIDR 範囲をカンマ区切りで指定します。ホスト名のエントリは URL に記載された表記と照合され、アドレスのエントリは URL の解決先アドレスと照合されます。また、localhost のような通常はブロックされる名前についても、解決先のすべてのアドレスが許可リストに含まれていれば許可されます。起動時に一度だけ読み込まれ、ツールの引数による影響は受けません。ワイルドカードは拒否されます
BRUNO_UPLOAD_DIRSアップロードで読み取り可能な追加ディレクトリ
BRUNO_PROXY_HOSTSコレクションのプロキシを使用できるホスト
BRUNO_INSECURE_TLS_HOSTS証明書検証をスキップできるホスト
BRUNO_DNS_TIMEOUT_MSDNS 解決のタイムアウト
BRUNO_WORKSPACE_PATHBruno の workspace.yml の検索先

許可リストの各エントリは、対象の表記と完全一致した場合にのみ一致します。許可されたホスト名は解決されません。オペレーターが保証したのは名前であり、今日その名前が指している先ではないため、その背後にあるアドレスは対象になりません。また、許可されたアドレスも、それに解決される名前は対象になりません。そのため、localhost と 127.0.0.1 は別々のエントリです。一方だけを許可し、リクエストで他方を使うと、許可エントリの不足であるにもかかわらず、ガードの設定に一貫性がないように見えます。拒否時にはその旨が示されます。

これにより、run_collection が取得できる対象が制限されます。シェルアクセス権を持つエージェントは、いずれにせよネットワークにアクセスできます。これは境界ではなく、セキュリティ対策の一層として扱ってください。

FAQ

Bruno CLI(bru)をインストールする必要がありますか?

いいえ。リクエストパイプラインはこの中で実装されており、変数、認証、Cookie、リダイレクト、アサーション、スクリプト、依存関係の順序付けを処理します。そのため、bru を外部から実行することはなく、バイナリを PATH 上に置く必要もありません。これが、上記の同等性検証が存在する理由でもあります。独立した実装は、意図的に元の実装との同等性を検証される必要があります。

.yml の opencollection ファイルに対応していますか?それとも .bru のみですか?

どちらにも対応しており、コレクションがどちらの形式を使っているかを自動で検出するため、選択を求められることはありません。新しいコレクションは .yml がデフォルトです。従来の形式を使いたい場合は、create_collection に format: "bru" を指定します。2つの形式が実際に異なる場合(違いがあります)、それぞれの形式について、Bruno 本体のライターと同じ方法で書き込みます。

ただし、1つのコレクションで使える形式はどちらか一方だけで、混在はできません。ルートマニフェストで形式が決まり、Bruno はその拡張子のファイルだけを読み込むため、bruno.json コレクション内の .yml リクエストは、Bruno にとってはディレクトリ内のただのファイルです。このサーバーはそのようなファイルも読み込み、書き込み、実行します。そのファイル自体の名前を変更すれば修正できるのに、操作を拒否すると修正できなくなるためです。ただし、そのファイルにアクセスするツールや一覧表示するツールはすべて、Bruno からは見えないことを伝え、ファイル名を示します。

CI で実行できますか?

このサーバーが生成するのは通常の Bruno コレクションなので、人がアプリで作成した場合と同じように、CI で bru run を使って実行できます。MCP サーバー自体は、エージェントが介在する作成・デバッグのサイクル向けです。また、JUnit XML と HTML のレポートファイルも出力します。レポートファイルをご覧ください。エージェントが実行したテストの結果も、CI ダッシュボードが期待する成果物として残ります。

Bruno アプリが作成したファイルを書き換えますか?

変更を依頼されたフィールドだけです。write_request による編集は部分マージであり、このサーバーがモデル化していないキーも、形式が保持できる場合は書き戻されます。.yml では全体を通して、.bru では文法に辞書ブロックがあり、そこに格納できる場合に保持されます。また、テストスイートではすべての書き込みを Bruno 本体のリーダーで検証します。削除には confirm: true の明示が必要です。

どの MCP クライアントが使えますか?

どのクライアントでも使えます。クライアント固有のコードを含まない、標準的な stdio サーバーです。Claude Code、Claude Desktop、Cursor、Windsurf、VS Code、Codex CLI、opencode、Zed、Cline、Continue、LM Studio、Gemini CLI、MCP Inspector、または独自の SDK クライアントで利用できます。各クライアントの設定場所については、クライアントの接続をご覧ください。

シークレットはどう扱われますか?

シークレットの環境変数は実行中にメモリ上に保持され、ファイルには書き込まれません。どちらの形式もシークレットの値をディスクに保存しないのは、ここで追加した制限ではなく、Bruno の設計によるものです。クエリパラメーターに指定したものも含め、エージェントに返される結果から認証情報はマスキングされます。スクリプトは、環境変数を取り除いたフォーク済み V8 サンドボックスで実行されます。セキュリティをご覧ください。

これは元の bruno-mcp と同じものですか?

macarthy/bruno-mcp のフォークとして始まりました。このプロジェクトは2025年7月以降活動しておらず、実行せずにコレクションファイルを生成していました。上記のランナー、リーダー、両形式への対応、パリティゲート、サンドボックスは、すべてフォーク後に構築したものです。リポジトリは現在 Ostico/bruno-mcp-studio です(発表)。npm パッケージ名は変更されておらず、@ostico/bruno-mcp です。

Bruno と関係がありますか?

いいえ。Bruno は usebruno の製品であり、その名称と商標は同社に帰属します。これは Bruno のオープンソースパッケージを読み込んで互換性を維持するコミュニティプロジェクトであり、usebruno の公認を受けたものでも、同社と提携しているものでもありません。

Bruno の開発チームは2026年8月、公式 MCP サーバーとして usebruno/bruno-mcp を発表しました。これはそのサーバーではなく、その役割を競うものでもありません。それぞれの得意分野については、どの Bruno MCP サーバーを使うべきですか?をご覧ください。

2.5.0 へのアップグレード — 4 つのツールを write_request に統合しました

4 つのツールが廃止されました。それらの機能は、同じセマンティクスで write_request が担います。編集では引き続き、渡したフィールドだけが更新され、それ以外のすべてのフィールドはバイト単位でそのまま保持されます。

2.5.0 で廃止代わりに呼び出すもの
create_request、modify_requestwrite_request
create_test_suite、create_crud_requestsrequests と、実行順序を指定する dependencies を使った write_request

delete_request は変更されておらず、引き続き使用できます。さらに、1 回の呼び出しで複数を削除するための filePaths も受け取るようになりました。

これはフェイルクローズで動作します。 クライアントは接続時にツール一覧を取得し、現在の名前を受け取るため、一覧を参照するエージェントには影響しません。削除済みの名前を呼び出すと、未知のツールエラーまたは権限の確認が表示されます。不正な結果が返されることも、何も通知されないこともありません。

実際に編集が必要なのは、名前を固定している設定です。たとえば、--allowedTools mcp__bruno-mcp__create_request 引数、settings.json の権限エントリー、フックのマッチャー、そして 4 つのツールのいずれかを名前で呼び出すようエージェントに指示している CLAUDE.md やスキル内の文章です。

バージョン管理

ツール名は接続時に tools/list から取得され、リンクされるわけではないため、ツールの名前変更や削除はマイナーバージョンでリリースされます。呼び出しの動作は以前とまったく同じか、ツールが存在しないかのどちらかで、後者の場合は明確にエラーになります。

変更のない呼び出しの動作を変える場合は、メジャーバージョンでリリースされます。 これは危険な変更です。2.0.0 ではフォルダーが実行グループに変更され、何も変更していない呼び出し元にも異なる結果が返されるようになりました。これより目立たない変更はメジャーバージョンに値しません。危険を知らせないメジャーバージョンは、メジャーバージョンを無視するよう促してしまうからです。

1.x からのアップグレード

  • requestPath と folder は廃止されました。どちらも、ファイルやディレクトリを順序付きで並べた requests 配列に置き換わります。
  • トップレベルの results 配列はありません。 result.groups[0].results を読み取ってください。
  • parallel: true は以前、各フォルダーを分離していましたが、現在はそうなりません。groups がない場合、選択範囲全体が 1 つのグループとなり、1 つのストアと Cookie jar を共有します。以前の動作を維持するには、フォルダーごとに別々のグループを指定してください。
  • seq は実行を制約しなくなりました。デフォルトの順序と結果の報告順序だけを指定します。
  • 空の requests: [] は何も実行しません。以前はコレクション全体を実行していました。
  • エクスポートから削除されたもの:BruGenerator、generateBruFile、createBasicBruFile

詳細は CHANGELOG.md をご覧ください。

開発

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

yarn ではなく npm を使用してください。ロックファイルは npm 用で、CI では npm ci が実行されます。

CI のチェック内容、コミット規約、各コミットに必要なサインオフ(DCO)については、CONTRIBUTING.md をご覧ください。

ライセンス

MIT — LICENSE をご覧ください。コントリビューションも同じ条件で受け付けており、DCO サインオフが必要です。

リンク


サポート

Test-Guard が役に立った場合は、開発への支援をご検討ください。

GitHub でスポンサーになる PayPal で寄付する

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。