2026:レンタル ProxyMac Mac mini 上の OpenClaw stdio バッファリング、MCP JSON-RPC の停滞、パイプのバックプレッシャー
香港、日本、韓国、シンガポール、米国の ProxyMac Mac mini 上では、OpenClaw がしばしば stdio で MCP サーバーを登録します:ゲートウェイが子プロセスを起動し、stdin/stdout で JSON-RPC を話し、各メッセージがすぐ届くことを期待します。失敗の兆候は静かで苛立たしいものです:最初のツール呼び出しは成功し、その後子が出力を止めるのに top では CPU が遊んでいるように見えます。十中八九、犯人は「悪いモデル」ではなく stdio バッファリングとパイプのバックプレッシャーです。子は TTY が見えなくなると完全ブロックバッファに切り替わり、部分的な応答はバッファが満ちるまで libc に留まります。一方親は read() で永遠に来ない区切りを待ってブロックするか、親がパイプを空にしていないため子が write() でブロックします。本稿ではアーキテクチャを説明し、症状マトリクスでラインバッファとブロックバッファを対比し、launchd 下でPTY ラッパと生パイプを比較し、Python(PYTHONUNBUFFERED=1、python -u)、POSIX フィルタ(stdbuf -oL)、Node ストリーム(stdout を flowing で消費)の緩和を列挙し、MCP セットアップ、ゲートウェイ再起動、デプロイのトラブルシュート、ulimits に接続する5 ステップのトリアージを示します——実際はディスクリプタ枯渇が「ハング」に見えている場合向けです。
macOS 上の stdio MCP アーキテクチャ(常に真でなければならないこと)
三つの協調プロセスを想定します:(A) OpenClaw ゲートウェイ、(B) MCP サーバーのバイナリまたはスクリプト、(C) 任意のヘルパーフィルタ(jq、言語ランタイム)。書き込みでブロックしている参加者がいる一方で、別の参加者が同じ循環依存の読み取りで待っているとデッドロックになります。stdio 転送は stderr の規律も継承します:stderr に進捗バーを吐くおしゃべりなライブラリは、誰も消費しなければカーネルパイプバッファを満たします。
- フラッシュごとに 1 JSON メッセージという期待は非現実的——libc は JSON の境界を知りません。
- 大きな応答にはストリーミング読み取りが必要です。ゲートウェイがペイロード全体を RAM にバッファすると、MCP の意味論とは無関係な「停滞」を感じます。
- launchd は対話シェルの rc を読み込みません——環境の一致は手作業です。環境変更後の順序付き再起動は アップグレード/ロールバック パターンを参照してください。
ラインバッファとブロックバッファ(ターミナルで「動く」理由)
多くの CLI は isatty(stdout) が真のとき ラインバッファ、stdout がパイプのとき ブロックバッファ(多くの場合 4〜8 KiB の倍数)を使います。ヘッドレス LaunchAgent の下では、MCP サーバーは突然パイプ書き手になります——ターミナルでは「ライブ」に見えたログはバッファが満ちるまでバッチ化されます。その遅延は「モデルが止まった」ように見えますが、LLM は数秒前に終わっていることもあります。
stdbuf -oL -eL で包む——本番 plist に焼き込む前に遅延を測定してください。
MCP 停滞の症状マトリクス
| シグナル | MCP のバグより可能性が高いもの | 立証/反証 | 次のリンク |
|---|---|---|---|
| 1 回目の RPC は OK、2 回目が永遠に止まる | ブロックバッファされた stdout | 同一バイナリを script -q /dev/null 下で PTY スモークテスト | 本稿 |
| CPU が張り付き、RAM は平坦 | 空の fd を読むタイトスピン | sample pid 5 -file /tmp/st.txt でサンプル | トラブルシュート |
Too many open files | Ulimit、MCP のファンアウト | launchctl limit maxfiles とプロセスのソフト上限 | Ulimits |
| ログローテーション後に断続的 | SIGHUP 処理/再オープンされた fd | タイムスタンプを newsyslog と突き合わせる | ログ |
LaunchAgent 下の PTY ラッパと生パイプ
チームによっては script、unbuffer、またはカスタム PTY 親で MCP サーバーを包み、子が対話的だと思わせます。トレードオフ:PTY はCPU とコピー負荷を増やしますが、バッファリングの驚きのクラスを取り除きます。生パイプは安価ですが、子での規律ある flush か stdbuf 下敷きを要求します。意識的に選んでください——サーバー間でスタイルを混在させるとオンコールが混乱します。
ツールチェーンの緩和策(LaunchAgent の EnvironmentVariables にコピー)
Python: PYTHONUNBUFFERED=1 をエクスポートするか python3 -u を使う。パッケージ化された CLI では、サポート版で sys.stdout.reconfigure(line_buffering=True) を呼ぶエントリポイントを推奨。Node: stdout を flowing モードで消費する——子プロセス間のパイプでストリームを一時停止するのは典型的な落とし穴。Go / Rust: ソースを制御できるなら各 JSON-RPC フレームの後に明示的に flush。シェルフィルタ: 自動化でパイプ越しに tail するときは grep --line-buffered を忘れずに。
<key>EnvironmentVariables</key>
<dict>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
<key>NODE_OPTIONS</key>
<string>--max-old-space-size=4096</string>
</dict>
エージェントを書き直す前の stdio 5 ステップ・トリアージ
- 再現:
ssh下で plist のProgramArgumentsを対話シェルにそのまま貼り付ける——突然動けば環境/TTY の差があります。 - strace 相当: macOS では短時間
sudo fs_usage -w -f filesys | grep mcpで書き込み停滞を見る(本番では慎重に)。 - stderr を分離 してローテーション付きファイルへ。デバッグのノイズが stdout の JSON-RPC と競合しないようにする。
- ソークテスト: 緩和を有効にした後、合成の大きなペイロードで負荷をかける。
- 転送を決める: stdio が依然として脆いなら、セットアップガイドに従い HTTP MCP を計画する。
FAQ
パイプバッファの sysctl を上げれば直るか? 最後の手段として扱う——根本原因は多くの場合、読み手と書き手のペースの不整合であり、バッファサイズ単独ではありません。
MCP サーバーは stdout にログすべきか? stdout は JSON-RPC のみに。人間向けログは stderr か構造化ファイルへ。
Apple Silicon はバッファを変えるか? いいえ——M4 はバグに当たる速度を上げるだけで、libc の既定からは逃れません。
stdio MCP を固める場所として ProxyMac Mac mini が適している理由
Apple Silicon M4 の mini は HK / JP / KR / SG / US にあり、常時稼働のベアメタルで毎晩同じ stdio グラフを再生でき、呼び出す SaaS API の横に リージョン容量 をぶら下げ、LaunchAgent リポのそばに ヘルプ リンクを置けます。人間が TCC プロンプトを承認する必要があるときは VNC にフォールバック。ネットワークがためらいを注入するときは同じリリーストレインの IPv6 Happy Eyeballs SSH を読んでください。