MCP サーバーの timeout を 10 分に伸ばしたのに、処理が 1 分で打ち切られてしまうという報告を読み返していた夜、私は自分の設定ファイルを開いて、手が止まりました。timeout を書ける場所が、思っていたより多かったからです。
設定は変えたのに、効いていないようです。こういうとき、私はまず「Gemini CLI の不具合だろうか」と疑います。ただ、その疑いを持ったまま調べ始めると、たいてい遠回りになります。先に確かめるべきなのは、値を決めている場所が一か所ではないという事実のほうでした。
同じような症状は、gemini-cli の Issue でも「設定した時間より短く切れる」という趣旨で報告されています。原因は環境ごとに違うため、この記事は「犯人を一つに決める」ことはいたしません。代わりに、手元で順に潰していける切り分けの手順と、そのためのスクリプトをお伝えします。
まず、timeout は何の単位で、どこに書けるのか
MCP サーバーの設定には timeout というキーがあり、単位はミリ秒です。指定しなければ 600,000(10 分)が既定値として使われます。
一見して分かりにくい点が二つあります。
- 単位がミリ秒なので、
"timeout": 600と書くと 0.6 秒です。10 分のつもりで 600 と書いてしまう取り違えは、起こりやすいはずです - 設定を書ける場所が複数あります。ユーザー全体の
~/.gemini/settings.json、プロジェクト直下の.gemini/settings.json、そして拡張機能が持つgemini-extension.jsonです
同じ名前の MCP サーバーが二か所に書かれていると、どちらの値が採用されるかは層の優先順位に左右されます。優先順位は版によって変わりうるので、私は「どちらが勝つか」を覚えるより、全部の層の値を並べて目で見るほうを選んでいます。
全部の層を一度に並べる小さなスクリプト
次のスクリプトは、ユーザー設定・プロジェクト設定・インストール済み拡張の 3 種類を読み、サーバー名ごとに timeout を一覧します。勝者は断定せず、食い違いと単位の疑いだけを知らせる作りです。
#!/usr/bin/env python3
"""MCP サーバーの timeout が、どの設定ファイルで決まっているかを一覧する。"""
import json
import sys
from pathlib import Path
DEFAULT_MS = 600_000 # 設定が無いときの既定値(公式ドキュメント記載の値)
def humanize(ms):
if ms < 1000:
return f"{ms} ms"
if ms < 60_000:
return f"{ms / 1000:g} 秒"
return f"{ms / 60_000:g} 分"
def layers(home: Path, project: Path):
yield "user", home / ".gemini" / "settings.json"
yield "project", project / ".gemini" / "settings.json"
ext_dir = home / ".gemini" / "extensions"
if ext_dir.is_dir():
for p in sorted(ext_dir.glob("*/gemini-extension.json")):
yield f"extension:{p.parent.name}", p
def collect(home: Path, project: Path):
found = {} # サーバー名 -> [(層, ファイル, timeout or None)]
for label, path in layers(home, project):
if not path.is_file():
continue
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
print(f"! {path}: JSON が壊れています ({e})", file=sys.stderr)
continue
for name, conf in (data.get("mcpServers") or {}).items():
found.setdefault(name, []).append((label, path, conf.get("timeout")))
return found
def report(found):
suspicious = 0
for name, rows in sorted(found.items()):
print(f"[{name}]")
for label, path, t in rows:
shown = "未指定(既定値)" if t is None else f"{t} → {humanize(t)}"
flag = ""
if isinstance(t, (int, float)) and t < 1000:
flag = " ← 秒で書いていませんか(単位はミリ秒)"
suspicious += 1
print(f" {label:<24} {shown}{flag}")
values = {t for _, _, t in rows if t is not None}
if len(values) > 1:
print(f" ※ {len(values)} 通りの値が定義されています。優先順位を確かめてください")
suspicious += 1
if not values:
print(f" (どの層にも無し → 既定の {humanize(DEFAULT_MS)})")
return suspicious
if __name__ == "__main__":
home = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home()
project = Path(sys.argv[2]) if len(sys.argv) > 2 else Path.cwd()
sys.exit(1 if report(collect(home, project)) else 0)なぜ勝者を決めないのか、と思われるかもしれません。優先順位の仕様は更新で変わることがあり、スクリプトに埋め込むと、古い知識が静かに誤った結論を出してしまうからです。事実だけを並べて、判断は人に残しました。
わざと食い違わせた設定で動かしてみました
仕組みの確認のために、検証用の設定を手で作りました。実際の私の環境ではなく、動作確認のためのサンプルです。
- ユーザー設定に
docsサーバー(timeout 600000)と、timeout 無指定のfilesサーバー - プロジェクト設定に
filesサーバー(timeout 600) - 拡張機能
docs-helperに、同じ名前のdocsサーバー(timeout 60000)
実行すると、次のように出力されました。
[docs]
user 600000 → 10 分
extension:docs-helper 60000 → 1 分
※ 2 通りの値が定義されています。優先順位を確かめてください
[files]
user 未指定(既定値)
project 600 → 600 ms ← 秒で書いていませんか(単位はミリ秒)docs では、10 分と 1 分が同居しています。冒頭で私が見た「きっかり 1 分」は、こういう拡張側の値が採用されていた場合と、症状としてはよく似ています。files では 600 と書いたために 0.6 秒になる設定が見つかりました。どちらも、CLI を疑う前に目で見えるはずの食い違いです。
なお、この検証が示せるのは「スクリプトが食い違いを拾えること」までです。あなたの環境で 1 分の切断が拡張に由来するかどうかは、実行して初めて分かります。
食い違いが見つかったあとの直し方
docs のように層をまたいで値が割れていたら、私は次の三つのうち一つを選びます。
- 拡張側の値を正とするなら、ユーザー設定の
docsを消して、定義を一か所に寄せます - 自分の値を正とするなら、同じサーバーを拡張に頼らず自前の設定へ移し、拡張から該当サーバーを外します
- どちらとも決められないなら、サーバー名を変えて二つに分け、それぞれに目的の timeout を書きます
単位の取り違えのほうは、もっと簡単です。600 を 600000 に直すだけですが、数字のゼロを数え間違えやすいので、私は 10 * 60 * 1000 のように式で書いた値を一度別のメモに計算しておき、そのまま写すようにしています。JSON に式は書けないため、数字だけを丁寧に写す作業になります。
直したあとは、もう一度スクリプトを走らせて、終了コードが 0 になるのを確かめます。共有のリポジトリで使うなら、コミット前のフックに入れておくと、チームの誰かが同じ食い違いを持ち込む前に止められます。終了コードが 1 のときは何かしら疑わしい点が残っている、という約束にしてあるのは、そのためです。
一覧で見つからなかったときの切り分け
スクリプトが何も見つけなければ、設定ファイルの外に原因があります。私は次の順で確かめています。
- サーバー側の制限。MCP サーバー自身や、その先の API が、独自の制限時間を持っていないか
- 間に挟まるもの。プロキシやリバースプロキシが、長く無応答の接続を切っていないか
- 進捗の通知。処理中に何も返さない設計だと、途中の層が「無通信」とみなす場合があります
- CLI の版。既知の不具合の一覧に当たっていないかを、更新履歴と Issue で確かめます
3 を試すときは、処理を小さく刻んで途中経過を返す形に直すのが手堅い方法です。時間を伸ばすより、長く黙らない設計のほうが、どの層でも壊れにくくなります。
私が引いた線
今回の件で、はっきりしたことがあります。**設定を変えたら、効いた値を読み戻します。**書いた値ではなく、実際に採用されている値を確かめるところまでを一つの作業にしました。
次に timeout を触るときは、まず find_mcp_timeouts.py を一度走らせてから、値を書き換えてみてください。MCP の組み方そのものは Gemini CLI で MCP サーバーを使う に、増えたツールの整理は つないだ MCP を初めて外した日 に、既知の不具合を先に引く方法は 既知バグ早見表を先に grep する話 に書き残しております。