---
title: "MCP サーバーのツールを使う"
description: "MCP サーバーの登録、ツール制限、チャット・agent・workflow からの利用。"
---

`lait` は [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) サーバーのクライアント
として動作し、`lait.config.yml` に登録した MCP サーバーのツールを、チャット・`lait agent run`・
`lait run`（workflow）のどの経路からもモデルに渡せます。モデルがツール呼び出しを返すと lait が
MCP サーバーへ転送して結果を受け取り、モデルに返す、というやり取りを最終回答が出るまで自動で
繰り返します（tool loop）。

```yaml
# lait.config.yml
default:
  model: local
  mcp: [filesystem]        # 全経路（チャット / agent / workflow）の最終フォールバック
  max_tool_rounds: 8

mcp_servers:
  filesystem:
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
```

```sh
# チャット
cargo run -- "/tmp のファイルを一覧して" --mcp filesystem

# agent
cargo run -- agent run agents/fs-agent.md '{"task":"/tmp のファイルを一覧して"}'

# workflow
cargo run -- run workflow.yml "調べたいこと"
```

## MCP サーバーの登録（`lait.config.yml`）

`mcp_servers:` に、`command:`（子プロセスとして起動する stdio サーバー）または `url:`
（streamable HTTP でリモート接続するサーバー）のどちらか一方を指定します。詳しい書式は
[設定ファイル](/config#mcp-サーバー) を参照してください。

```yaml
mcp_servers:
  filesystem:
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    env:
      SOME_TOKEN: ${SOME_TOKEN}
    cwd: ./work

  remote-search:
    url: https://mcp.example.com/mcp
    headers:
      Authorization: "Bearer ${SEARCH_TOKEN}"
```

実際に `mcp:` で参照されたサーバーだけがその場で接続されます。未登録の名前を `mcp:` に書くと、
`lait.config.yml` の `mcp_servers:` を案内するエラーになります。

## 呼び出せるツールを制限する（`allowed_tools`）

`mcp_servers:` の各サーバーは `mcp_servers.<name>.allowed_tools` の設定を持てます。省略時は
無制限（そのサーバーが持つ全ツールをモデルが呼び出せる、この設定が存在する前と同じ挙動）です。
一方、空リスト `[]` を明示すると「そのサーバーの全ツールを禁止」という意味になり、`null`/省略とは
区別されます。

```yaml
mcp_servers:
  filesystem:
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    allowed_tools: [read_file, list_directory]   # write_file 等はモデルから呼び出せない
```

この制限はツール一覧（`tools/list`）ではなく実行前（`tools/call` の直前）で強制されます。つまり
`allowed_tools` に含まれないツールもモデルには見えたままですが、実際に呼び出そうとするとサーバー
へ接続する前にエラーになり、そのモデルからの呼び出しは拒否されます。`lait.config.yml` の
`mcp_servers` は AGENTS.md が言うとおり「信頼されたコード」として扱われますが、実際にどのツール
を実行するかはモデルが選ぶため、`allowed_tools` はその選択に対するユーザー側のガードレールです。

`lait lint` は、あるステップ/エージェント定義が `mcp:` で参照しているサーバーの
`allowed_tools` が空リストの場合、警告を出します（そのサーバーへのツール呼び出しは実行前に
必ず拒否されるため、参照していること自体がほぼ設定ミスだからです）。`allowed_tools` が空でない
リストの場合は、モデルが実際にどのツールを呼び出すかを事前には知りようがないため、警告は出ません
（実行時にそのつど拒否されるだけです）。

## サーバーからの対話的な質問に答える（`allow_elicitation`）

MCP サーバーは `tools/call` の途中で `elicitation/create` を送り、ユーザーへの追加の質問を
lait 経由で挟むことができます（MCP の Multi Round-Trip Requests 機構の一部）。lait は既定では
これを常に declined（拒否）で即答します。`mcp_servers.<name>.allow_elicitation: true` を
指定すると、実際に標準エラー出力へ質問を表示し、標準入力から回答を読み取るようになります。

```yaml
mcp_servers:
  filesystem:
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    allow_elicitation: true
```

- サーバーがユーザーへ対話的に質問できることは、`allowed_tools` と同様の信頼のエスカレーション
  です。そのため既定は無効（`false`）で、サーバーごとに opt-in する必要があります。
- 標準入力が対話端末でない場合（CI・パイプ・`lait serve --mcp` の被呼び出し側など）は、
  `allow_elicitation` の値に関わらず、プロンプトを一切表示せずに即座に declined を返します
  （ワークフローの `ask:` ステップの「非対話的な標準入力には答えようがない」という判断と同じです）。
- 質問（`message`）と、回答に必要なフィールド名・型・説明が標準エラー出力に表示されます。
  入力に応じた扱いは次のとおりです。

  | 入力 | 扱い |
  | --- | --- |
  | 1行の JSON オブジェクト（例: `{"name": "Tokyo"}`） | その内容を回答として返します |
  | `decline` / `cancel` | それぞれ拒否／キャンセルとして扱われます |
  | JSON オブジェクトとして解釈できない入力 | 拒否として扱われます（再入力のプロンプトはありません） |
- URL 型の elicitation（ブラウザで URL を開いて続行する形式）は、URL を標準エラー出力に表示する
  だけで、常に declined を返します — ブラウザを開く・完了を待つといった動作には対応していません。
- 複数の MCP サーバーが同時に elicitation を送ってきた場合、標準入力への出力が1つのプロセス
  全体で直列化されます（同時に2つの質問が混ざって表示されることはありません）。ただし
  `--approve-tools` の対話的承認プロンプトとは別の排他制御なので、両方が同時に発生した場合は
  出力が混ざる可能性があります。

**sampling（`sampling/createMessage`）・roots（`roots/list`）には対応していません。**
どちらも MCP の SEP-2577 で非推奨（deprecated）になったプリミティブです。「2026-07-28 仕様への
追従」を掲げる本項目としては、非推奨になった仕組みへ新規に対応するのではなく、現行仕様で現役の
elicitation にのみ対応する、というのが妥当な判断だと考えています。両者を必要とするサーバーへ
接続した場合、lait はそれらのリクエストを常にエラー（method not found）で返します（rmcp の既定
挙動のままです）。

## `tool_policy`（allow/deny）と `--approve-tools`（対話的承認）

`allowed_tools` がサーバーごとの設定なのに対し、`lait.config.yml` トップレベルの `tool_policy:`
は MCP サーバー・サブエージェント・[カスタムシェルツール](/tools)を横断して、修飾済みツール名
（`<サーバー名>__<ツール名>` / `agent__<エージェント名>` / `tool__<ツール名>`）単位でツール呼び出しを
許可/拒否します。

```yaml
# lait.config.yml
tool_policy:
  allow: ["fetch_*", "filesystem__read_file"]   # 非空なら、マッチしないツールは全て拒否
  deny: ["*__delete_*"]                          # allow より優先。マッチしたら常に拒否
```

| フィールド | 説明 |
| --- | --- |
| `deny` | 先に評価されます。マッチすれば `allow` の内容に関わらず拒否されます。 |
| `allow` | 空（既定）ならすべてのツールが許可対象です。非空の場合、マッチしないツールは拒否されます。 |

- パターンは前方一致（`fetch_*`）・後方一致（`*_delete`）・完全一致のみの簡易グロブです。
- `allowed_tools`（サーバー単位）とは **AND** で併用されます。どちらか一方でも拒否すれば呼び出せません。
- `allowed_tools` と異なり、**拒否してもツール呼び出し全体は失敗しません**。拒否した旨を
  ツールの実行結果としてモデルに返し、tool loop は継続します（モデルは「使えなかった」ことを
  踏まえて別の方法を試したり、素直に諦めて回答したりできます）。
- グローバル設定とプロジェクト設定がある場合、`allow`/`deny` はそれぞれ両方の内容を合わせたもの
  （和集合）になります。プロジェクト側で `allow`/`deny` を書いても、グローバル側の `deny` は
  無効になりません。

`--approve-tools`（グローバルフラグ）を指定すると、モデルがツールを呼び出す直前に名前と引数を
stderr に表示し、stdin で確認します。

| 入力 | 意味 |
| --- | --- |
| `y` | 今回だけ許可 |
| `n` | 今回だけ拒否 |
| `a` | このツール名を以後このプロセス内では常に許可 |

1 ラウンド内の複数のツール呼び出しは、実行前に
1 件ずつ順番に確認されます（並行実行中にプロンプトを出すと表示が混ざるため）。`tool_policy`/
`allowed_tools` で既に拒否されたツールは確認すら行われません。非対話的な標準入力
（CI・パイプなど)で `--approve-tools` を指定した場合は、確認しようとした時点でエラーになります
（応答できる相手がおらず、パイプが閉じているのか人間の応答待ちなのか区別できないため）。

同じ `lait` 実行中に `parallel`、並行 `for_each`、`compare` など複数の tool loop が動いても、承認の
質問は一度に1件だけ表示されます。Ctrl-C で読み取りを中断した場合も、読み取り中の処理が
標準入力を解放するまで次の承認が始まらないため、後続の質問が先の回答を受け取ることはありません。
`a` を選んだ承認は、この排他区間の中でキャッシュされるので、同じツール名について再度質問されません。

## 各経路での指定方法

`mcp:`（使うサーバー名のリスト）と `max_tool_rounds:`（そのツール周回の上限回数、既定 8）は、
経路ごとに次のようにフォールバックします。各フィールドは独立してフォールバックします
（`retry` のようにブロック単位でフォールバックするものではありません）。

| 経路 | 優先順位 |
|---|---|
| チャット（`lait "prompt"`） | `--mcp`（CLI フラグ、複数指定可）→ `lait.config.yml` の `default.mcp` |
| `lait agent run` | agent ファイルの frontmatter `mcp:` → `lait.config.yml` の `default.mcp` |
| `lait run`（workflow） | ステップの `mcp:` → （`agent:` ステップなら）エージェント定義の `mcp:` → ワークフローの `default.mcp` → `lait.config.yml` の `default.mcp` |

```sh
lait "prompt" --mcp filesystem --mcp remote-search
```

```markdown
---
model: local
mcp: [filesystem]
max_tool_rounds: 8
---
{{ input.task }} を実行してください。
```

```yaml
# workflow.yml
steps:
  - prompt: "{{ input }} について調べてください。"
    mcp: [filesystem, remote-search]
```

それぞれの詳細は [エージェント Markdown ファイル（agent.md）](/agent#mcp-ツールの利用) と
[ワークフロー（workflow.yml）](/workflow#ツール連携mcp--skills--subagents--tools) にもあります。

## ツール名の扱い

モデルに渡すツール名は `<サーバー名>__<ツール名>`（例: `filesystem__read_file`）の形に修飾されます。
これは複数のサーバーが同名のツール（例: 両方に `search`）を持っていても衝突しないようにするため
です。OpenAI の function 名の制約（`^[a-zA-Z0-9_-]{1,64}$`）に合わせて、それ以外の文字は `_` に
置き換えられ、修飾後の名前が64文字を超える場合や、2つのツールが同じ修飾名になる場合はエラーに
なります。

## `output_schema` との併用

`output_schema`（Structured Outputs）と `mcp` は併用できます。ただし、多くの OpenAI
互換サーバーは厳密な `json_schema` の `response_format` を渡されると、スキーマ準拠の出力を強制し
`tool_calls` を一切返さなくなります。そのため lait は、ツールを呼び出している間は
`response_format` を送らず、モデルがツール呼び出しを止めた最後のラウンドだけ `response_format`
を付けて再送します（リクエストが1回増えます）。

## `--stream` との併用

`--stream` と `mcp` は併用できます。ストリームに含まれる `tool_calls` は index 付きの断片として
届くため、lait 側でラウンドごとに再組み立てしてから実行します。テキストは各ラウンドとも届いた分
から順に表示され、ツール呼び出しが尽きて最終応答が確定したラウンドまでこれを繰り返します。
一度確立したストリームの途中で `models:` の[フォールバック](/config#複数プロバイダーによるフォールバック)先へ
切り替わることはありません（ラウンドの最初、ストリームが確立する前の失敗にのみ切り替わります）。

## 上限とエラー

- `max_tool_rounds`（既定 8）に達してもモデルがツール呼び出しを止めない場合はエラーになり、
  ワークフロー/agent の実行がそこで止まります。無限ループを避けるための上限です。
  `max_tool_rounds` に達する前に接続先モデルのコンテキストウィンドウ/トークン上限に当たって
  しまう場合は、[ツール周回の要約による圧縮（default.compaction）](/compaction)で
  ラウンドあたりのメッセージ量を抑えられます。
- ワークフローの `retry` はステップの「ツール周回全体」を1つの単位として包みます。リトライが
  発生すると、副作用のあるツール呼び出し（ファイル書き込みなど）も含めてそのステップのツール
  周回全体がやり直されるため、副作用のあるツールをリトライ対象のステップで使う場合は注意して
  ください。

## 動作確認について

lait は LM Studio のローカルモデルを主な想定用途としていますが、ローカルモデルはツール呼び出し
（tool calling）への対応度が様々です。ツール呼び出しに対応していないモデルでは `tool_calls` が
一切返らず、`mcp:` を指定していてもツールは呼ばれません。ツール対応モデル（`qwen3` 系など）を
使って手元で確認してください。
