Voice LiveでMCPサーバのツールを使ってみる話
音声会話のエージェントに、外部のツールで調べものをさせたいと思い、Voice Live APIで試してみました。
Voice Live APIは音声で会話するエージェントを作るためのAPIで、モデルにツールを使わせる手段として次の2つがあります。
- function calling:クライアントがツールを定義し、モデルからの呼び出しを受けてクライアントが実行する
- MCPサーバ:MCP(Model Context Protocol)のサーバをセッションに登録し、Voice Liveのサービスがツールを呼び出す
本記事では、MCPサーバのツールがfunction callingとどう違うのか、設定と実際に動かしたときに起きたことと対処について紹介します。あわせて、1つの質問に複数のツールを使う並列ツール呼び出しについても紹介します。Voice Live機能解説シリーズの第3回です。
シリーズの記事一覧(SDK 1.2.0 / API 2026-07-15)
-
Voice Live SDK 1.2.0の変更点とAPI 2026-07-15 GA
SDKに型が無い機能の分類 -
Voice Live APIで会話の「間」を扱う
先に話す・言いよどみを待つ・割り込む・待ち時間に話す -
Voice Live の音声入力の品質を上げる
ノイズ抑制・エコー除去・Live-Reference AEC の選び方 - Voice LiveでMCPサーバのツールを使う(本記事)
function callingとの違いと並列ツール呼び出し -
Voice LiveでFoundryのエージェントをツールとして呼ぶ
chat-supervisorパターンの使い方と前段モデルの選び方 -
30秒の音声で「自分の声」を作って Microsoft Foundry エージェントを喋らせる
Personal Voice の申請から実装まで -
Voice Live APIのアバターを使い分ける
標準・カスタム写真・カスタムビデオの設定と実装 -
Voice Liveのアバター映像をWebSocketで受ける
WebRTCとの違いと映像の形式
番外編:Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ
SDPを1行ずつ突き合わせた記録
検証に使ったコードはこちらです。
本記事は2026年10月時点の情報です。対象はVoice Live API 2026-07-15(GA)と.NET SDK Azure.AI.VoiceLive 1.2.0です。
準備
リソースの作成から、C#で最初の音声会話を動かすところまでは、公式のクイックスタートに沿って進めると準備できます。対応リージョンは、Voice Liveの概要ページで確認できます。
クイックスタートの環境で本記事の内容を試すときは、次の点に注意してください。
- MCPを使う場合は、APIバージョンを
2026-04-10以降にします - 並列ツール呼び出しの設定(
parallel_tool_calls)は、APIバージョン2026-06-01-preview以降で使えます - .NET SDKはAzure.AI.VoiceLive 1.2.0を使います
Voice Liveのツールの呼ばれ方
function callingとMCPでは、ツールを実行する場所が違います。
| function calling | MCPサーバ | |
|---|---|---|
| ツールの定義 | クライアントがsession.updateで渡す |
クライアントがMCPサーバを登録し、ツールの一覧はサービスが取得する |
| ツールの実行 | クライアント | Voice Liveのサービス |
| クライアントに届くもの |
function_call(ツール名と引数) |
実行の通知(response.mcp_call.completedなど) |
| ツールの結果 | クライアントがfunction_call_outputで送る |
サービスがそのまま使う |
| 結果を使った応答の要求 | クライアントがresponse.createを送る |
クライアントがresponse.createを送る |
最後の行は、どちらの方式でも同じです。ツールの結果を使って応答させるには、クライアントがresponse.createを送ります。Voice Liveは応答が重なることを拒否するので、この要求は1回だけ送る必要があります。後で紹介するMCPの課題1と並列ツール呼び出しは、どちらもこの規則の話です。
クライアントがツールを宣言する形(function calling、MCP)はModelのセッションの話です。Agentのセッションでは、ツールはエージェント(クラウド側)で管理・実行されます。
MCPサーバの登録
{
"type": "session.update",
"session": {
"tools": [
{
"type": "mcp",
"server_label": "deepwiki",
"server_url": "https://mcp.deepwiki.com/mcp",
"require_approval": "never"
}
]
}
}
SDK 1.2.0では型で書けます。
options.Tools.Add(new VoiceLiveMcpServerDefinition("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))
{
RequireApproval = new RequireApprovalOption(McpApprovalKind.Never)
});
MCPを使うにはAPIバージョン2026-04-10以降が必要です。
require_approvalの既定値はalwaysです。 省略すると、ツールを呼ぶたびにクライアントへmcp_approval_requestが届き、クライアントがmcp_approval_response(approve=true)を返すまでツールは実行されません。承認処理を書いていないと、そこで会話が止まります。自動で実行させるならneverを明示します。ツールごとに{"never": [...], "always": [...]}と分けることもできます。
MCPを動かしたときに起きた困ったこと
サンプルでは公開MCPサーバのdeepwiki(GitHubリポジトリのwikiを答えてくれる)を使い、microsoft/vscodeの構成を尋ねて、サーバ側でツールが実行されることを確認しました。その過程で、次のことが起きました。
課題1: ツールの実行後に応答が生成されない
MCPのツールはサーバで実行されますが、その結果を使って応答を作らせるresponse.createを送るのはクライアントの役目です。Voice Liveでは、MCPはツールの1つの形として扱われていて、function callingと同じく、ツールの結果がそろっても自動では応答を始めません(前述の表の最後の行)。Foundryのエージェントでも、MCPはfunctionやWeb検索などと並んで、ツールの1つとして設定します。OpenAIのRealtime APIも同じ仕様で、公式のガイドに「自動では続かない」と書かれています。
response.mcp_call.completedを受けたら、ツールを呼んだ応答(応答1)が閉じたのを確かめてから、クライアントが次の応答を要求します(公式how-toのサンプルも同じ作りになっています)。
MCPの呼び出しは、それを呼んだ応答がまだ開いている間に完了することがあります。応答は重ねられないので、サンプルでは応答が進行中かどうかを自分で持っておき、進行中ならresponse.doneを待ってから要求しています。
解決策: 応答の要求を1回だけ送る
実測ではresponse.done → response.mcp_call.completedの順で届くこともあり、順序は当てにできません。サンプルでは「要求すべき応答がある」ことを数えておき、どちらが後に来ても1回だけ要求するようにしています。
case SessionUpdateResponseCreated:
responseActive = true;
break;
case SessionUpdateResponseDone:
responseActive = false;
await RequestToolFollowUpAsync();
break;
case SessionUpdateResponseMcpCallCompleted:
// ツールはサービスが実行した。応答を要求するのはクライアントの仕事
Interlocked.Increment(ref pendingToolCalls);
await RequestToolFollowUpAsync();
break;
// ...
private async Task RequestToolFollowUpAsync()
{
// 応答の重なりは拒否されるので、進行中なら response.done 側に任せる
if (session == null || responseActive || Volatile.Read(ref toolsInFlight) > 0) return;
// response.done とツール完了が同時に来ても、要求するのは1回だけ
if (Interlocked.Exchange(ref pendingToolCalls, 0) == 0) return;
await session.StartResponseAsync();
}
課題2: ツールが呼ばれずに自分の知識で答える
これはエージェントの世界ではよくある話かもしれません。MCPサーバを登録しても、モデルがツールを呼ばずに自分の知識で答えてしまうことがよくありました。見た目はMCPの設定が壊れているのと区別がつきにくいです。特に今回は動作確認も兼ねていたため使用したMCPがお題に対してよくなかったかもしれないです。
解決策: instructionsでツールに誘導する
サンプルでは、セッションのinstructionsで「リポジトリの質問には必ずツールを使う」と明示しています。
For any question about a GitHub repository, you must call the deepwiki tools
instead of answering from memory, even if you believe you know the answer.
ツールが実際に呼ばれたかは、response.output_item.doneに入るMCP呼び出しの項目で確かめられます。これが来ていなければ、モデルが自分で答えています。
課題3: ツールの結果が大きい
deepwikiにはwikiの全文を返すread_wiki_contentsというツールがあり、これが呼ばれると約620KBの結果が返ってきました。これがそのまま音声で答えるための入力になるので、音声で答える用途には向かないと思います。
解決策: 使うツールを絞る
サンプルではinstructionsで、短く答えるask_questionを使うように誘導しています。確実に絞るなら、サーバ定義のallowed_tools(SDKではAllowedTools)で使ってよいツールを限定します。
課題4: 作成時点の項目が空
MCPのツール一覧や呼び出しの項目は、conversation.item.createdの時点では中身が空です(ツール一覧、呼び出しの引数・結果とも)。中身が入るのはmcp_list_tools.completedやresponse.output_item.doneのあとです。
解決策: 中身が入ったあとのイベントで表示する
conversation.item.createdで表示すると「ツール0個」「結果なし」と誤った表示になるので、表示や記録はmcp_list_tools.completedやresponse.output_item.doneで行います。
並列ツール呼び出し
1つの質問に複数のツールが必要なとき(「東京の天気と時刻は?」)、モデルは1ターンの中で複数のツールを呼びます。これが並列ツール呼び出しです。function callingでは、1つの応答の中に複数のfunction_callが届きます。
{
"type": "session.update",
"session": {
"parallel_tool_calls": true,
"tools": [
{ "type": "function", "name": "get_weather", "parameters": { "...": "..." } },
{ "type": "function", "name": "get_time", "parameters": { "...": "..." } }
]
}
}
parallel_tool_callsの既定値はtrueです。falseにすると、モデルに1つずつ順番にツールを呼ばせられます。SDK 1.2.0ではVoiceLiveSessionOptions.AllowParallelToolCallsで設定できます。
注意点は、すべてのツールの結果を送ってからresponse.createを1回だけ送ることです。結果を1つ送るたびにresponse.createを送ると、応答が二重に発火してエラーになります。
| 送り方 | 結果 |
|---|---|
結果ごとにresponse.create
|
二重に応答しようとしてエラー |
| 全部の結果を送ってから1回 |
get_weather / get_timeが1回ずつ呼ばれ、エラーなし |
MCPの課題1と同じく、「要求すべき応答の数」を数えておき、最後に1回だけ要求する形にすると、function callingとMCPの両方に使えます。なお、サンプルで並列に呼ばれることを確かめたのはfunctionツールの場合です。
まとめ
- function callingはクライアントが、MCPはVoice Liveのサービスがツールを実行します
- どちらの方式でも、ツールの結果を使った応答の要求はクライアントが送ります
- 応答は重ねられないので、応答の要求は1回だけ送ります
-
require_approvalの既定値はalwaysなので、自動で実行させるならneverを指定します - モデルがツールを呼ばないときは、
instructionsでツールに誘導します - 結果が大きいツールは、
allowed_toolsで使わせないようにします - 並列ツール呼び出しでは、全部の結果を送ってから
response.createを1回送ります