ローカルAI環境でMCPサーバーを動かす
MCPサーバー開発ガイドでは、Claude DesktopのようなクラウドAIをホストとする前提でMCPサーバーの仕組みと実装を整理しました。MCPというプロトコル自体は、ホスト側のAIモデルがクラウドかローカルかを区別しません。ではMicrosoft Foundry Localのような、端末上で動くAIランタイムをホストとして使う場合、何が変わるのでしょうか。
この記事では別記事で紹介したFoundry Localを中心に、ローカルAIランタイムでMCPサーバー・クライアントを動かす方法を整理します。コード例はC#/.NETで統一し、MCP公式のC# SDK(NuGetパッケージModelContextProtocol)を使います。掲載するサーバー・クライアントのコードは、実際に手元で動かして疎通確認まで行った学習用の実装をもとにしています。LM Studio・Ollamaについては、筆者が実際に検証できていないため、記事の終盤で簡単に触れる程度にとどめます。
Foundry LocalとMCPの関係を整理する
まず前提を整理しておきます。Foundry Localの記事で書いたとおり、Foundry Localは「アプリケーションに組み込んで使う、エンドツーエンドのローカルAIソリューション」であり、モデルのダウンロード・ハードウェアアクセラレーション・推論実行を担うランタイムです。ここには「MCPサーバーに接続する」という機能自体は含まれていません。MCPとの接続は、Foundry Localより上位の層——具体的にはMicrosoft.Extensions.AIのIChatClient抽象とMCP公式C# SDK、あるいはMicrosoft Agent Framework——が担う構造になっています。
ここで1つ重要な注意点があります。Microsoft Agent FrameworkのFoundry Localプロバイダのドキュメントを確認したところ、C#タブには次のように明記されていました。
Note: Foundry Local is not currently supported in .NET.
つまりAgent FrameworkがPython向けに提供しているFoundryLocalClient(agent_framework.foundry名前空間)は、2026年7月時点でC#/.NET版には存在しません(同ドキュメントによると、Go版も「近日対応予定」の段階です)。Agent FrameworkのMCPツール対応自体はC#でも提供されており、後述するとおりMCPサーバーのツールをAIToolに変換する部分はそのまま使えるのですが、「Foundry Localをバックエンドにしたエージェントを、Agent Frameworkの流儀でそのまま組み立てる」という道はC#では現状塞がれています。
そのためC#/.NETでFoundry LocalとMCPを組み合わせる場合、実務的なルートはAgent Frameworkを経由せず、Microsoft.Extensions.AIのIChatClient抽象とMCP C# SDKを直接組み合わせる方法になります。この記事でもそちらを扱います。
MCP公式C# SDK
MCPの公式SDK一覧ページでは、C# SDKはmodelcontextprotocol/csharp-sdkとしてTier 1(フル機能・継続的なメンテナンス対象)に分類されています。SDKは複数のNuGetパッケージに分かれており、用途に応じて使い分けます。
| パッケージ | 用途 |
|---|---|
ModelContextProtocol.Core |
クライアント・低レベルサーバーAPIのみの最小構成 |
ModelContextProtocol |
ホスティング・DI込みのメインパッケージ |
ModelContextProtocol.AspNetCore |
HTTPベースのMCPサーバーをASP.NET Coreでホストする場合 |
ModelContextProtocol.Extensions.Apps |
インタラクティブなUIアプリ向け |
サーバーを実装する場合はModelContextProtocol、HTTP転送でホストする場合はそれに加えてModelContextProtocol.AspNetCoreを追加する、という組み合わせが基本です。
トランスポート:stdioとStreamable HTTP、どちらを選ぶか
MCP公式仕様のTransportsページでは、標準のトランスポートとしてstdioとStreamable HTTPの2つが定義されています。ローカルAI環境ではこの2つの特性の違いが選択に直結しやすいので、後述の実装例に入る前に整理しておきます。
stdioは、クライアントがMCPサーバーをサブプロセスとして起動し、標準入出力経由でJSON-RPCメッセージをやり取りする方式です。ホストとサーバーが同一マシン上にある前提の、1対1の接続になります。Foundry Localが動く端末と同じマシン上でファイル操作・ローカルDB参照のようなツールを動かす場合、stdioが最も単純な選択です。MCPサーバー開発ガイドでも触れたとおり、stdioはOSのプロセス分離が実質的な境界になるため、追加の認証機構なしで運用しやすいという特性があります。ホストもモデルもツールも同じ端末の中に閉じているなら、ネットワーク越しの認証を持ち出す必要性自体が薄れます。この記事のサーバー・クライアントの実装例も、このstdio版です。
Streamable HTTPは、サーバーを独立したプロセスとして動かし、単一のHTTPエンドポイントがPOST/GET両方を受け付ける方式です。同じLAN内の別マシンで動くMCPサーバーを複数のクライアントから共有したい場合や、サーバー再起動をまたいでセッションを維持したまま再接続したい場合(stdioにはこのセッション・再接続という概念自体がありません)に向いています。公式仕様は、実装時のセキュリティ要件として「Originヘッダーを検証してDNSリバインディング攻撃を防ぐこと」「ローカルで動かす場合は0.0.0.0ではなく127.0.0.1にバインドすること」「接続には適切な認証を実装すること」を挙げています。ローカル環境だから安全、と判断してこれらを省略するのではなく、同一マシン内で完結させる設計ならstdioを選ぶ、ネットワーク越しにする以上はローカルであってもこれらの対策を講じる、という切り分けが実務的です。
サーバーを実装する: HelloToolsという最小構成
MCP C# SDKの特徴は、TypeScript SDKのような「tools/list・tools/callのハンドラを自分で書く」スタイルではなく、`[McpServerToolType]`・`[McpServerTool]`という属性でツールを宣言するスタイルが標準になっている点です。パラメータの説明はSystem.ComponentModelの[Description]属性で付与し、この情報がそのままモデルに渡される入力スキーマの説明文になります。
以下は、実際に手元で動かして疎通確認まで行った最小構成です。あいさつを返すSayHelloと、足し算をするAddという2つのツールだけを持つ、学習用のシンプルなサーバーにしています。
// HelloTools.cs
using System.ComponentModel;
using ModelContextProtocol.Server;
// [McpServerToolType] を付けたクラスの中で、[McpServerTool] を付けたメソッドが
// そのまま「MCPツール」として公開される。引数/戻り値とDescriptionから
// 入出力スキーマが自動生成され、クライアント(やAI)はこれを見て呼び出せる。
[McpServerToolType]
public static class HelloTools
{
[McpServerTool, Description("あいさつを返す。ローカルMCPの疎通確認用。")]
public static string SayHello() => "Hello Local AI World";
[McpServerTool, Description("2つの整数を足し算して返す。")]
public static int Add(
[Description("1つ目の数")] int a,
[Description("2つ目の数")] int b) => a + b;
}サーバーの起動コードです。stdioでは標準出力(stdout)がMCPプロトコル専用の通信路になるため、通常のログをConsole.WriteLineでうっかり混ぜるとプロトコルが壊れます。ログは必ず標準エラー出力(stderr)に向けておく必要があります。
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
// ローカルstdio MCPサーバー。
// 重要: stdioでは標準出力(stdout)がMCPプロトコル専用の通信路になる。
// ここでConsole.WriteLineすると通信が壊れるので、ログはすべてstderrに出す。
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Services
.AddMcpServer() // MCPサーバーを登録
.WithStdioServerTransport() // 通信は標準入出力(stdio)で
.WithToolsFromAssembly(); // このアセンブリ内の[McpServerTool]を自動公開
await builder.Build().RunAsync();.WithToolsFromAssembly()を呼ぶだけで、アセンブリ内の[McpServerToolType]が付いたクラスを自動的に探索し、[McpServerTool]が付いたメソッドを登録してくれます。MCPサーバー開発ガイドのTypeScript版のように、自分でtools/list・tools/callのハンドラを書く必要はありません。動作確認に使ったパッケージのバージョンは次のとおりです(2026年7月時点)。
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
</ItemGroup>Streamable HTTP版のサーバーを実装する
Streamable HTTPへの切り替えは、ツール定義側(HelloTools.cs)のコードを一切変更せず、Program.csと.csprojだけを差し替えれば済みます。実際に動かして確認したコードは次のとおりです。
// Program.cs (HTTP版)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// HTTP(Streamable HTTP)トランスポートのMCPサーバー。
// stdioと違いstdoutを予約する必要がないので、通常のASP.NET Coreログ設定でよい。
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer() // MCPサーバーを登録
.WithHttpTransport() // 通信はHTTPで
.WithToolsFromAssembly(); // このアセンブリ内の[McpServerTool]を自動公開
var app = builder.Build();
app.MapMcp(); // MCPエンドポイントをマッピング(既定: /)
await app.RunAsync();stdio版との違いは、ホストをHost.CreateApplicationBuilderからWebApplication.CreateBuilderに変え、.WithStdioServerTransport()を.WithHttpTransport()に差し替え、最後にapp.MapMcp()でMCPのエンドポイントをルーティングにマッピングしている点だけです。stdio版で必須だった「stdoutをログで汚さない」という制約もHTTP版にはないため、ログ出力まわりの特別な設定も不要になります。
プロジェクトファイル側の変更点は、SDKがMicrosoft.NET.SdkからMicrosoft.NET.Sdk.Webに変わり、HTTPトランスポート用のModelContextProtocol.AspNetCoreパッケージが追加されている点です。
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.4.1" />
</ItemGroup>
</Project>app.MapMcp()は引数なしで呼ぶと、ルート(/)にMCPエンドポイントをマッピングします。上記のコードは明示的なURLバインドを指定しておらず、Kestrelの既定設定に従う形になっていますが、ネットワーク越しに公開する場合は前節で触れた「Originヘッダーの検証」「ローカル利用なら127.0.0.1へのバインド」「適切な認証の実装」を別途検討してください。
クライアントを実装する: ツール一覧の取得とツール呼び出し
MCPの仕組み自体を理解する段階では、いきなりAIモデルと組み合わせるより、まずMCPクライアントとして「ツール一覧を取得する」「ツールを呼び出す」という基本の疎通を確認しておくと見通しが良くなります。ここで示すのも、実際に動かして確認したstdio接続のクライアント実装です。
トランスポートの生成部分だけを差し替えられるよう、共通処理は抽象基底クラスにまとめています。
// McpConnectorBase.cs
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
namespace McpClientApp;
// MCPへの接続方式(stdio/HTTP)をポリモーフィズムで切り替える抽象基底クラス。
// 派生クラスに任せるのは「トランスポートの作り方(CreateTransport)」だけ。
// 接続後の操作(ツール一覧・呼び出し)は方式に依存しないので共通化する。
public abstract class McpConnectorBase
{
// ── 派生クラスごとに異なる唯一の部分 ──
protected abstract IClientTransport CreateTransport();
public abstract string DisplayName { get; }
// ── 共通処理 ──
// トランスポートを作ってからは stdio でも HTTP でも全く同じ流れ。
public async Task RunDemoAsync()
{
var transport = CreateTransport(); // ← ここだけ多態
await using var client = await McpClient.CreateAsync(transport);
var tools = await client.ListToolsAsync();
foreach (var t in tools)
Console.WriteLine($" - {t.Name}: {t.Description}");
var hello = await client.CallToolAsync("say_hello");
Console.WriteLine($"say_hello() => {ReadText(hello)}");
var sum = await client.CallToolAsync(
"add",
new Dictionary<string, object?> { ["a"] = 2, ["b"] = 3 });
Console.WriteLine($"add(2, 3) => {ReadText(sum)}");
}
protected static string ReadText(CallToolResult result) =>
string.Join("", result.Content.OfType<TextContentBlock>().Select(b => b.Text));
}ポリモーフィズムで差し替わるのはCreateTransport()だけで、接続後のツール一覧取得・呼び出しのロジックはRunDemoAsync()に共通化しています。stdio・HTTPどちらのトランスポートでもIClientTransportという共通の型に収まるため、派生クラス側は「どうトランスポートを組み立てるか」だけを実装すればよく、接続後の処理を書き直す必要がありません。
stdio版のトランスポート生成部分です。サーバーはビルド済みのMcpServer.dllを子プロセスとして起動します。
// StdioMcpConnector.cs
using ModelContextProtocol.Client;
namespace McpClientApp;
public sealed class StdioMcpConnector : McpConnectorBase
{
public override string DisplayName => "stdio版";
protected override IClientTransport CreateTransport()
{
var serverDll = Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..",
"McpServer", "bin", "Debug", "net10.0", "McpServer.dll"));
return new StdioClientTransport(new StdioClientTransportOptions
{
Name = "hello-local-mcp-stdio",
Command = "dotnet", // dotnet runはビルドログがstdoutに混ざるため、ビルド済みdllを直接指定する
Arguments = [serverDll],
});
}
}dotnet runではなくdotnet <dll>という起動コマンドにしているのは、サーバー側と同じ理由です。dotnet runは再ビルドの判定やログをstdoutに出すことがあり、これがMCPのJSON-RPCメッセージに混ざるとクライアント側がパースに失敗します。ビルド済みのDLLを直接dotnetコマンドで起動すれば、stdoutに余計な出力が混ざりません。
HTTP版のトランスポート生成部分です。あらかじめ起動しておいたStreamable HTTPサーバーに接続します。
// HttpMcpConnector.cs
using ModelContextProtocol.Client;
namespace McpClientApp;
// HTTP(Streamable HTTP)版:あらかじめ起動しておいたサーバーに接続する。
// 事前に `dotnet run --project McpServer` でサーバーを立ち上げておくこと。
// 接続先は既定で http://localhost:9999 だが、コマンドライン引数で上書きできる。
// dotnet run --project McpClient -- http http://localhost:5000
public sealed class HttpMcpConnector(Uri? endpoint = null) : McpConnectorBase
{
private readonly Uri _endpoint = endpoint ?? new Uri("http://localhost:9999");
public override string DisplayName => $"HTTP版 ({_endpoint})";
protected override IClientTransport CreateTransport() =>
new HttpClientTransport(new HttpClientTransportOptions
{
Name = "hello-local-mcp-http",
Endpoint = _endpoint, // 繋ぎに行く先
});
}stdio版と同じく、HTTP版でもポリモーフィズムが効いているのはCreateTransport()だけです。McpConnectorBase.RunDemoAsync()はトランスポートの種類を意識せず、say_hello・addの呼び出しロジックをそのまま使い回せます。
どちらのトランスポートを使うかは、クライアントのProgram.csでコマンドライン引数を見て切り替えます。
// Program.cs
using McpClientApp;
// 実行時引数でトランスポートを切り替える(既定: stdio)。
// dotnet run → stdio版
// dotnet run http → HTTP版(既定: http://localhost:9999)
// dotnet run http http://localhost:5000 → HTTP版(接続先を明示指定)
var mode = args.Length > 0 ? args[0].ToLowerInvariant() : "stdio";
var httpEndpoint = args.Length > 1 ? new Uri(args[1]) : null;
// ポリモーフィズム:基底型の変数で受け、どの実装かをProgramは意識しない。
McpConnectorBase connector = mode switch
{
"stdio" => new StdioMcpConnector(),
"http" => new HttpMcpConnector(httpEndpoint),
_ => throw new ArgumentException($"未知のモード: '{mode}'(stdioかhttpを指定)"),
};
await connector.RunDemoAsync();HTTP版を試す場合は、前節のStreamable HTTPサーバーをあらかじめ起動しておく必要があります。既定の接続先http://localhost:9999は、サーバー側の起動ポートに合わせて調整してください。
tools/listで得られるツール名は、C#側のメソッド名(SayHello・Add)ではなくsay_hello・addというsnake_caseに変換されている点にも注意してください。MCP C# SDKは既定でメソッド名をsnake_caseの公開名に変換します(`[McpServerTool(Name = "...")]`で明示的に上書きすることもできます)。CallToolAsyncを呼ぶ際は、この変換後の名前を指定する必要があります。動作確認に使ったパッケージは次のとおりです。
<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
</ItemGroup>Foundry LocalのIChatClientと組み合わせるには
ここまではMCPクライアントが直接ツールを呼び出すだけで、AIモデルは登場していません。実際にFoundry LocalのようなローカルAIにツール選択を任せる場合は、Microsoft.Extensions.AIのIChatClientに、ListToolsAsync()で取得したツールをAIToolとして渡す構成になります。Agent Frameworkのローカル MCP ツールガイドには、ListToolsAsync()が返すツールが自動的にAIToolオブジェクトに変換される旨と、mcpTools.Cast<AITool>()という形でキャストしてChatOptions.Toolsに渡す例が示されています。
Foundry Local自体をIChatClientにする方法については、Foundry Localの公式ドキュメントにあるとおり、FoundryLocalManager.StartWebServiceAsync()でOpenAI互換のローカルWebサービスを起動し、OpenAIパッケージのChatClientを取得したうえで、Microsoft.Extensions.AI.OpenAIの.AsIChatClient()拡張メソッドでIChatClientに変換する、という流れが公式ドキュメントから追えます。ただし「Foundry Localのローカルエンドポイント+.AsIChatClient()」という組み合わせそのものを一つのサンプルとして示した公式ドキュメントは見当たらず、この記事の学習用実装にも含まれていません。この部分を実装する際は、上記2つの公式ドキュメントを直接参照しながら、手元で動作を確認することをおすすめします。
なお前述のとおり、Microsoft Agent FrameworkのFoundry Local統合(FoundryLocalClient)は2026年7月時点でC#/.NET未サポートです。そのためC#では、Agent Frameworkの流儀ではなくMicrosoft.Extensions.AIのIChatClientを直接使うルートになります。
クラウド版との違いが出やすいポイント
認証: 「不要になる」のではなく「境界の取り方が変わる」
MCPサーバー開発ガイドの本番運用の節では、リモート公開する場合はOAuth 2.1をベースにした認可の仕組みに沿うべきだと書きました。Foundry Local・stdio接続の場合、この認証要件はプロセス分離という別の境界に置き換わるだけで、「セキュリティを考えなくてよくなる」わけではありません。端末そのものが複数ユーザーで共有されている、あるいはツールが機密データにアクセスするなら、OS側のユーザー権限・ファイルパーミッションといった、クラウド版とは別の観点での境界設計が必要になります。
モデルの性能が低いと、ツール呼び出しの精度がより顕著に落ちる
MCPサーバー開発ガイドでも、モデルがツールを選ぶ根拠はname・description・入力スキーマというテキスト情報だけであり、プロトコルレベルで正しい選択が保証されるわけではないと書きました。前述のとおりFoundry Localでは、関数呼び出し自体に対応していないモデルも珍しくなく、対応しているモデルでもツール呼び出しガイドによれば「required(必ず1つ以上のツールを呼ぶ)」「特定の関数を指定して呼ばせる」といった強めの指定はベストエフォート扱いとされています。ローカルAIで動かす量子化された小〜中規模モデルでは、クラウドAIとローカルAIの比較記事で整理したモデル品質の差が、ツール呼び出しの成功率という形でも表面化しやすいと考えられます。採用するモデルがツール呼び出しに対応しているかを事前に確認し、descriptionの書き方やツール数の絞り込みといった対策を、クラウド版以上に丁寧に行う必要がありそうです。
オフライン運用のメリットは「モデルとツールの両方がローカルにある場合」に限られる
クラウドAIとローカルAIの比較記事で書いたとおり、ローカルAIの利点の一つはオフラインでの動作です。ただしMCPサーバー自体が外部APIを呼び出すツールを提供している場合(天気情報の取得など)、モデルがローカルでもツールの実行自体はネットワークに依存するため、構成全体としてはオフラインになりません。真にオフラインで完結させたいなら、Foundry Local・MCPサーバー・ツールが扱うデータソースのすべてを、端末内またはネットワークが不要な範囲に収める設計が必要です。
他にもこういうのがあります
Foundry Local以外にも、ローカルAIランタイムがMCPホストの役割を担う選択肢はいくつかあります。今回はいずれも実際に検証したわけではないため、概要の紹介にとどめます。
LM Studioは、公式ドキュメントに「Starting LM Studio 0.3.17, LM Studio acts as an Model Context Protocol (MCP) Host」と明記されているとおり、アプリ自体がMCPホストとして動作します。設定はmcp.jsonというファイルで行い、Cursorと同じ記法を採用しているとのことです。
Ollamaは、GitHub Issueを見る限りアプリ本体はMCP/ツール呼び出しに未対応ですが、APIレベルではツール呼び出しに対応しているため、mcp-client-for-ollamaのようなサードパーティ製のブリッジツールを挟むことで、MCPサーバーと接続する構成が実務上の選択肢になっています。
どちらも対応状況が変化しやすい領域なので、実際に採用する際は必ず各製品の最新の公式ドキュメントを確認してください。
まとめ
Foundry LocalとMCPを組み合わせる場合、Foundry Localというランタイム自体にMCP接続の機能があるわけではなく、Microsoft.Extensions.AIのIChatClient抽象とMCP公式C# SDKを組み合わせる形で実現します。Microsoft Agent FrameworkのFoundry Local向け統合(FoundryLocalClient)は、2026年7月時点でC#/.NETではまだサポートされていない点には注意してください。C#でのMCPサーバー実装は[McpServerToolType]・[McpServerTool]という属性ベースの書き方が中心で、TypeScript SDKに比べてもかなり簡潔に書けます。この記事のサーバー・クライアントのコードは、HelloToolsという最小限のツール(あいさつ・足し算)を使い、stdio接続で実際に動かして疎通確認まで行った上で掲載しています。AIモデルにツール選択を委ねるIChatClientとの組み合わせは、まず素のMCPクライアントで疎通を確認してから、公式ドキュメントを参照しつつ段階的に足していくのが安全な進め方です。トランスポートについては、同一マシン内で完結するならstdio、別プロセス・別マシンから使う可能性やセッションの再接続性を重視するならStreamable HTTPという基準はクラウド版と同じですが、ローカルAIでは「同一マシン内で完結する」構成が現実的な選択肢に入りやすい分、stdioで十分なケースがクラウド版より多いはずです。一方で、モデルの性能に起因するツール呼び出しの精度低下や、認証境界の置き換え、オフライン運用が成り立つ条件など、ローカルAIならではの論点も踏まえた設計が求められます。