Microsoft Agent Framework をローカルLLMで試してみる その14(MCPクライアント)
シリーズ一覧
一覧
Microsoft Agent Framework をローカルLLMで試してみる その1
Microsoft Agent Framework をローカルLLMで試してみる その2
Microsoft Agent Framework をローカルLLMで試してみる その3(LoggingFactory)
Microsoft Agent Framework をローカルLLMで試してみる その4(Tool)
Microsoft Agent Framework をローカルLLMで試してみる その5(AIFunction)
Microsoft Agent Framework をローカルLLMで試してみる その6(ChatHistoryProvider)
Microsoft Agent Framework をローカルLLMで試してみる その7(ChatHistoryProvider)
Microsoft Agent Framework をローカルLLMで試してみる その8(ChatHistoryProvider)
Microsoft Agent Framework をローカルLLMで試してみる その9(AIContextProvider)
Microsoft Agent Framework をローカルLLMで試してみる その10(AIContextProvider)
Microsoft Agent Framework をローカルLLMで試してみる その11(AIContextProviderで簡易RAG)
Microsoft Agent Framework をローカルLLMで試してみる その12(Structured Output)
Microsoft Agent Framework をローカルLLMで試してみる その13(ミドルウェア)
Microsoft Agent Framework をローカルLLMで試してみる その14(MCPクライアント)
はじめに
Microsoft Agent FrameworkをローカルLLMで試してみるその14では、外部のMCPサーバーをAgentから呼び出す方法を確認します。
その13ではミドルウェアの動作を確認しました。
その14では、AgentにMCPクライアント機能を追加し、Microsoft Learn Docs MCP Serverのツールを呼ぶ流れを見ていきます。
今回は次の点を確認します。
-
Streamable HTTPの MCP サーバーへ .NET の MCP クライアントで接続する流れ - 固定チャット → MCP ツール → ツール実行結果 → 最終レスポンス のシーケンス
環境情報
今回確認した主な環境情報は次のとおりです。
| 項目 | バージョン |
|---|---|
| Target Framework |
.NET 10 (net10.0) |
| Microsoft Agent Framework |
Microsoft.Agents.AI 1.6.1 / Microsoft.Agents.AI.OpenAI 1.6.1
|
| MCP C# SDK | ModelContextProtocol 1.3.0 |
| LM Studio のモデル | openai/gpt-oss-20b |
確認する動作
下記のシナリオでMCPの動作を確認します。
- チャットは固定の1回のみ
- MCP サーバーは Microsoft Learn Docs MCP Server
- 使用するツールは
microsoft_docs_search - ツールの呼び出し結果をコンソールで確認
- 最終レスポンスもコンソールで確認
MCP クライアントの接続について
最初に Microsoft Learn Docs MCP Server へ接続します。(https://learn.microsoft.com/api/mcp)
HttpClientTransport を直接渡し、Streamable HTTP を明示して接続します。
await using var mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri(settings.McpServerUrl),
Name = "MicrosoftLearnDocs",
TransportMode = HttpTransportMode.StreamableHttp
}));
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
McpClientTool selectedTool = mcpTools
.SingleOrDefault(tool => string.Equals(tool.Name, settings.ToolName, StringComparison.Ordinal))
?? throw new InvalidOperationException("対象の MCP ツールが見つかりませんでした。");
今回は接続方式を固定したいので HttpTransportMode.StreamableHttp を明示しています。(既定値は AutoDetect )
McpClient で使える主な機能
McpClient は ListToolsAsync() だけのための型ではなく、MCP サーバーの公開機能をまとめて扱うクラスです。主なメソッドを用途別にまとめると下記になります。
| 分類 | 主な メソッド / プロパティ | できること | 今回のサンプル |
|---|---|---|---|
| 接続と再開 |
CreateAsync(...), ResumeSessionAsync(...)
|
新しい接続を開始し、必要なら既存セッションを再利用する |
CreateAsync(...) を使用 |
| サーバー情報 |
ServerInfo, ServerCapabilities, ServerInstructions, Completion
|
サーバー名、対応機能、利用上の補足、接続終了理由を取得する |
ServerInstructions を Instructions に加えている |
| 疎通確認 | PingAsync(...) |
サーバーに応答できるかを確認する | 未使用 |
| ツール |
ListToolsAsync(...), CallToolAsync(...), CallToolAsTaskAsync(...)
|
ツール一覧の取得、ツール実行、長時間ツールのタスク実行を行う |
ListToolsAsync() で microsoft_docs_search を選択 |
| プロンプト |
ListPromptsAsync(...), GetPromptAsync(...), CompleteAsync(...)
|
サーバー公開プロンプトの列挙、取得、引数補完を行う | 未使用 |
| リソース |
ListResourcesAsync(...), ListResourceTemplatesAsync(...), ReadResourceAsync(...)
|
公開リソースとリソーステンプレートを列挙し、中身を読む | 未使用 |
| 更新通知 |
SubscribeToResourceAsync(...), UnsubscribeFromResourceAsync(...)
|
リソースの更新通知を購読する | 未使用 |
| タスク管理 |
ListTasksAsync(...), GetTaskAsync(...), GetTaskResultAsync(...), PollTaskUntilCompleteAsync(...), CancelTaskAsync(...)
|
長時間実行の状態確認、結果取得、キャンセルを行う | 未使用 |
| サーバーログ | SetLoggingLevelAsync(...) |
サーバーから受けるログの粒度を調整する | 未使用 |
どの API が実際に使えるかは、接続先サーバーが返す ServerCapabilities に依存します。
MCP の接続方式をどう選ぶか
MCP で標準として押さえる接続方式は、標準入出力接続 (stdio) と HTTP ストリーム接続 (Streamable HTTP) です。C# SDK の HTTP transport では、互換用途として HTTP + SSE 接続も扱えます。
| 接続方式 | 概要 | サーバー起動 | 向いている用途 | C# SDK での主な型 |
|---|---|---|---|---|
標準入出力接続 (stdio) |
クライアントが子プロセスとして MCP サーバーを起動し、標準入力と標準出力で JSON-RPC をやり取りする | クライアント側が Command と Arguments を指定して起動する |
ローカル CLI ツール、ローカル検証、同一端末内の連携 |
StdioClientTransport, StdioClientTransportOptions
|
HTTP ストリーム接続 (Streamable HTTP) |
常駐中の HTTP エンドポイントへ接続し、サーバー側からのストリーム通知も扱う | 別プロセス、別コンテナー、別ホストとしてサーバーを起動しておく | リモート MCP サーバー、認証付きサービス、セッション再開を使いたい場合 |
HttpClientTransport, HttpClientTransportOptions, HttpTransportMode.StreamableHttp
|
| HTTP + SSE 接続 | 旧来の HTTP transport。C# SDK では互換目的で利用でき、AutoDetect ではこの方式へフォールバックできる |
HTTP サーバーを別途起動しておく | 既存サーバー互換、段階的移行 |
HttpClientTransport, HttpTransportMode.Sse, HttpTransportMode.AutoDetect
|
| 独自 transport | アプリ側で IClientTransport を実装して独自の接続層を差し込む |
実装に依存する | 既存ミドルウェアや特殊なランタイム制約がある場合 |
IClientTransport 実装 |
今回は、Microsoft Learn Docs MCP Server が HTTP エンドポイントを公開しているため、HttpClientTransport を使った Streamable HTTP 接続を選択しています。
Agent の構成
Agent は使うツールを1個に固定し、チャットも1回だけで確認します。
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "LmStudioMcpAgent",
Description = "LM Studio から MCP ツールを呼び出す最小サンプルです。",
ChatOptions = new ChatOptions
{
Instructions =
"あなたは Microsoft Learn の内容だけを根拠に回答するエージェントです。Microsoft または Azure に関する質問では、回答前に必ず microsoft_docs_search を使って確認してください。回答は日本語で簡潔にまとめ、最後に参照したドキュメントのタイトルと URL を含めてください。",
Tools = [selectedTool],
MaxOutputTokens = settings.MaxOutputTokens
}
});
今回のポイントは次の 2 つです。
-
Tools = [selectedTool]で MCP ツールをそのまま渡している -
Instructionsで Microsoft / Azure 系の質問ではツールを必ず使うように寄せている
この設定でLM Studio 側のモデルがツール呼び出しを返した時に、Agent がそのまま MCP サーバーへ中継してくれるように促します。
固定チャットと実行結果の確認
実行に使うチャットは固定です。
FixedPrompt:
"Azure AI Agent Framework で MCP Tool Calling を使う方法を 3 点で要約してください。回答する前に必ず Microsoft Learn の検索ツールで確認し、最後に参照したドキュメントのタイトルと URL を付けてください。"
実行後は、AgentResponse のメッセージから FunctionCallContent と FunctionResultContent を抜き出して表示しています。
List<FunctionCallContent> calls = response.Messages
.SelectMany(message => message.Contents)
.OfType<FunctionCallContent>()
.ToList();
List<FunctionResultContent> results = response.Messages
.SelectMany(message => message.Contents)
.OfType<FunctionResultContent>()
.ToList();
これにより、コンソールでは下記の順で確認できます。
-
Fixed Chat: LM Studio に渡した固定チャット -
MCP Tool: 実際に Agent に渡した MCP ツール -
Tool Call Summary: モデルが要求したツール名と引数、実行結果 -
Agent Response: ツール結果を踏まえた最終レスポンス
もしモデルがツールを呼ばなかった場合は例外で止めています。
if (calls.Count == 0)
{
throw new InvalidOperationException("モデルが MCP ツールを呼び出しませんでした。tool calling に対応したモデルを LM Studio で読み込み、固定チャットを確認して再実行してください。");
}
実際のコンソール出力
今回の環境では下記の出力になりました。
[Tool Call Summary]
call[0] microsoft_docs_search: query=Azure AI Agent Framework MCP Tool Calling
result[0] {"results":[{"title":"Integrate MCP Tools with Azure AI Agents - Training","content":"# Integrate MCP Tools with Azure AI Agents\r\n\r\n- Module\r\n- 6 Units\r\n\r\nIntermediate\r\n\r\nAI Engineer\r\n\r\nDeveloper\r\n..."}]}
[Agent Response]
**Azure AI Agent FrameworkでMCP Tool Callingを利用する手順(3点)**
1. **MCPサーバーへの接続**
2. **ツール取得と変換**
3. **エージェントの作成と実行**
> **参照ドキュメント**
> 「Using MCP tools with Agents (programming-language-csharp)」
ポイントは次の 3 つです。
-
call[0]が出ているので、LM Studio 側のモデルが実際にmicrosoft_docs_searchを呼んでいる -
result[0]が出ているので、MCP サーバーの検索結果が Agent へ戻り、その結果を使って最終レスポンスを作れている - 検索結果は JSON 文字列として返るため、本文中の改行は
\r\nの形で残る
まとめ
今回はAgentにMCPクライアント機能を実装し、外部のMCPサーバーを呼び出す方法を確認しました。
確認できたポイントは下記の通りです。
- MCP サーバーには
HttpClientTransport+McpClient.CreateAsync(...)で接続できる -
ListToolsAsync()で取ったMcpClientToolをそのまま Agent のツールへ渡せる - 固定チャットからのツール呼び出しとツール結果を使った最終レスポンスをコンソールで確認できる
実際の運用ではMCPサーバーからのToolの取得とAgentへのチャットは別のタイミングで行うことが多いと思います。(Toolの選択をユーザーに任せたり、AIに絞り込ませたり等)
ソース
Program.cs
using System.ClientModel;
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using OpenAI;
Console.OutputEncoding = Encoding.UTF8;
SampleSettings settings = SampleSettings.Load();
try
{
Console.WriteLine("=== MCP Client Sample with LM Studio ===");
Console.WriteLine($"lm_studio={settings.LmStudioUrl}");
Console.WriteLine($"model={settings.ModelName}");
Console.WriteLine($"mcp_server={settings.McpServerUrl}");
Console.WriteLine($"tool={settings.ToolName}");
Console.WriteLine();
var clientOptions = new OpenAIClientOptions
{
Endpoint = new Uri(settings.LmStudioUrl)
};
var openAIClient = new OpenAIClient(
new ApiKeyCredential(settings.ApiKey),
clientOptions);
IChatClient chatClient = openAIClient
.GetChatClient(settings.ModelName)
.AsIChatClient();
await using var mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri(settings.McpServerUrl),
Name = "MicrosoftLearnDocs",
TransportMode = HttpTransportMode.StreamableHttp
}));
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
McpClientTool selectedTool = mcpTools
.SingleOrDefault(tool => string.Equals(tool.Name, settings.ToolName, StringComparison.Ordinal))
?? throw new InvalidOperationException($"MCP ツール '{settings.ToolName}' が見つかりませんでした。利用可能: {string.Join(", ", mcpTools.Select(tool => tool.Name))}");
selectedTool = selectedTool.WithDescription(
"Search official Microsoft Learn and Azure documentation. Use this tool before answering Microsoft or Azure questions.");
Console.WriteLine("[Fixed Chat]");
Console.WriteLine(settings.FixedPrompt);
Console.WriteLine();
Console.WriteLine("[MCP Tool]");
Console.WriteLine($"name={selectedTool.Name}");
Console.WriteLine($"description={selectedTool.Description}");
Console.WriteLine();
string serverInstructions = string.IsNullOrWhiteSpace(mcpClient.ServerInstructions)
? string.Empty
: $"\nサーバーから渡された補足: {mcpClient.ServerInstructions}";
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "LmStudioMcpAgent",
Description = "LM Studio から MCP ツールを呼び出す最小サンプルです。",
ChatOptions = new ChatOptions
{
Instructions =
"あなたは Microsoft Learn の内容だけを根拠に回答するエージェントです。Microsoft または Azure に関する質問では、回答前に必ず microsoft_docs_search を使って確認してください。回答は日本語で簡潔にまとめ、最後に参照したドキュメントのタイトルと URL を含めてください。"
+ serverInstructions,
Tools = [selectedTool],
MaxOutputTokens = settings.MaxOutputTokens
}
});
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync(settings.FixedPrompt, session);
PrintToolSummary(response);
Console.WriteLine();
Console.WriteLine("[Agent Response]");
Console.WriteLine(response);
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("LM Studio または MCP サーバーへの接続に失敗しました。LM Studio を既定の endpoint で起動し、再実行してください。");
Console.WriteLine(ex.Message);
}
static void PrintToolSummary(AgentResponse response)
{
List<FunctionCallContent> calls = response.Messages
.SelectMany(message => message.Contents)
.OfType<FunctionCallContent>()
.ToList();
List<FunctionResultContent> results = response.Messages
.SelectMany(message => message.Contents)
.OfType<FunctionResultContent>()
.ToList();
Console.WriteLine("[Tool Call Summary]");
if (calls.Count == 0)
{
throw new InvalidOperationException("モデルが MCP ツールを呼び出しませんでした。tool calling に対応したモデルを LM Studio で読み込み、固定チャットを確認して再実行してください。");
}
for (int index = 0; index < calls.Count; index++)
{
FunctionCallContent call = calls[index];
Console.WriteLine($"call[{index}] {call.Name}{FormatFunctionCallArguments(call.Arguments)}");
}
for (int index = 0; index < results.Count; index++)
{
FunctionResultContent result = results[index];
Console.WriteLine($"result[{index}] {Preview(result.Result)}");
}
}
static string FormatFunctionCallArguments(IDictionary<string, object?>? arguments)
{
if (arguments is null || arguments.Count == 0) return string.Empty;
string formatted = string.Join(", ", arguments.Select(argument => $"{argument.Key}={Preview(argument.Value)}"));
return $": {formatted}";
}
static string Preview(object? value, int maxLength = 240)
{
if (value is null) return "<null>";
string text = value.ToString()?.Replace("\r", " ").Replace("\n", " ").Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(text)) return "<empty>";
return text.Length <= maxLength
? text
: text[..maxLength] + "...";
}
sealed record SampleSettings(
string LmStudioUrl,
string ApiKey,
string ModelName,
string McpServerUrl,
string ToolName,
string FixedPrompt,
int MaxOutputTokens)
{
public static SampleSettings Load()
{
return new SampleSettings(
LmStudioUrl: Environment.GetEnvironmentVariable("OPENAI_BASE_URL") ?? "http://localhost:1234/v1",
ApiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? "sk-dummy",
ModelName: Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "openai/gpt-oss-20b",
McpServerUrl: Environment.GetEnvironmentVariable("MCP_SERVER_URL") ?? "https://learn.microsoft.com/api/mcp",
ToolName: Environment.GetEnvironmentVariable("MCP_TOOL_NAME") ?? "microsoft_docs_search",
FixedPrompt: Environment.GetEnvironmentVariable("MCP_FIXED_PROMPT")
?? "Azure AI Agent Framework で MCP Tool Calling を使う方法を 3 点で要約してください。回答する前に必ず Microsoft Learn の検索ツールで確認し、最後に参照したドキュメントのタイトルと URL を付けてください。",
MaxOutputTokens: ReadInt32EnvironmentVariable("MAX_OUTPUT_TOKENS", 2000));
}
private static int ReadInt32EnvironmentVariable(string variableName, int defaultValue)
{
string? value = Environment.GetEnvironmentVariable(variableName);
return int.TryParse(value, out int parsed)
? parsed
: defaultValue;
}
}
Discussion