工良出品 | 長文講解 MCP 和案例實戰
作者:癡者工良
示例項目地址:https://github.com/whuanle/mcpdemo
近期 MCP 協議越來越爆火,很多開發者都投身參與 MCP Server/Client 的開發,各個大廠也紛紛推出自己的 MCP 集成平臺或開放 MCP 接口。也有一些朋友讀者在技術群討論 MCP 技術,很多人對 MCP 的機制不清楚,也有一些文章講解 MCP 時不夠清晰甚至誤導了讀者,所以筆者在這個周末在學習 MCP 時,寫下該筆記,盡可能提供更多的示例和講解,幫助讀者理清楚 MCP 和 LLM 之間的關系,已經如何實際落地使用 MCP。
MCP 協議
MCP 協議文檔地址:https://modelcontextprotocol.io/introduction
中文版文檔地址:https://mcp-docs.cn/introduction
根據 MCP 協議的規定,在 MCP 協議中有以下對象:
- MCP Hosts: 如 Claude Desktop、IDE 或 AI 工具,希望通過 MCP 訪問數據的程序;
- MCP Clients: 維護與服務器一對一連接的協議客戶端;
- MCP Servers: 輕量級程序,通過標準的 Model Context Protocol 提供特定能力;
- 本地數據源: MCP 服務器可安全訪問的計算機文件、數據庫和服務;
- 遠程服務: MCP 服務器可連接的互聯網上的外部系統(如通過 APIs);

MCP Host 就是一個 AI 應用,跟用戶交互的應用程序,一般是桌面程序,而 MCP Host 跟 MCP Client 可能是放在一起做的,自身即與用戶交互,也具有直接調用 MCP Server 的能力。
MCP Server 就是提供 Tool 、資源內容、提示詞、對話補全等功能的服務端,MCP Server 的功能或職責是多種多樣的,比如高德地圖 MCP Server 只提供了 Tool,即接口調用。
本地數據源、遠程服務者兩個跟 MCP 本身沒有關聯,而是 MCP Server 自身實現功能的一部分,或者說是支撐 MCP Server 的基礎設施和外部依賴。
由于 MCP 概念和功能比較多,因此筆者將一步步使用案例和項目的方式講解其中的細節,建議讀者將示例項目倉庫拉下來,根據本文教程嘗試自行編寫代碼以及跑通案例。
核心概念
MCP 協議定義了以下功能模塊:
- Resources
- Prompts
- Tools
- Sampling
- Roots
- Transports
由于 Roots 沒有多少案例,并且 C# 的 SDK 還沒有完善,因此本文只介紹其它功能模塊。
本文知識并不是線性講解以上 MCP 功能。
Transport
Transport 指傳輸處理消息發送和接收的底層機制,MCP 主要包含兩個標準傳輸實現:
- 標準輸入輸出 (stdio):主要對象是本地集成和命令行工具,使用 stdio 傳輸通過標準輸入和輸出流進行通信;
- 服務器發送事件 (SSE):SSE 傳輸通過 HTTP POST 請求(長連接)實現服務器到客戶端的流式通信;
當然,還有一個 Streamable ,但是由于社區支持還不算完善,并且本文也不講解。
以下是 MCP(Model Context Protocol)協議中 stdio、sse、streamable 三者的優缺點和差異的簡要說明:
stdio
-
優點:
- 平臺兼容性高:
stdio(標準輸入輸出)是操作系統底層的功能,幾乎所有操作系統和編程語言都支持。 - 簡單直接:用于進程間通信,通常是腳本和命令行工具的通信方式,易于實現。
- 平臺兼容性高:
-
缺點:
- 缺乏高級功能:
stdio只能處理簡單的文本和二進制數據流,沒有內建的消息結構或格式。 - 不適合在網絡環境中的實時交互:
stdio對于網絡通信來說不夠靈活和可靠,通常用于本地通信。
- 缺乏高級功能:
sse
-
優點:
-
實時更新**:允許服務器通過HTTP連接主動向客戶端發送更新消息,適合實時推送的應用場景。
-
簡單實現:基于HTTP協議,不需要復雜的傳輸層協議,客戶端通過 EventSource API 可以很容易地接收。
-
輕量級:相比WebSocket,SSE更輕量級,適合簡單的消息推送場景。
-
-
缺點:
-
單向通信:只能服務器向客戶端發送消息,客戶端如果需要發送消息,必須通過標準的HTTP請求回服務器。
-
連接限制:瀏覽器對同時建立的SSE連接數限制較嚴格,不適合大量連接的應用場景。
-
streamable
-
優點:
- 效率高:可以處理大數據或連續的數據流,不需要等待整個數據集傳輸完畢。
- 實時性好:可以在數據生成時逐步傳輸,在數據消費時逐步處理,提高實時響應能力。
- 靈活性高**:支持長時間的連接和傳輸,適合視頻、音頻、實時數據庫同步等應用。
-
缺點:
- 復雜性高:實現和管理流式傳輸協議、處理數據流的邏輯復雜度較高,需要確保數據的順序和完整性。
- 資源消耗:長時間的連接和持續的數據傳輸可能會消耗較多的服務器和網絡資源,需要優化處理。
ModelContextProtocol CSharp 中提供了三種 Transport ,其核心代碼在三個類中:
- StdioClientTransport
- SseClientTransport
- StreamClientTransport

下面筆者將會詳細講解 stdio、sse 兩種 Transport。
stdio
通過本地進程間通信實現,客戶端以子進程形式啟動 MCP Server 程序,雙方通過 stdin/stdout 交換 JSON-RPC 消息,傳輸每條消息時以換行符分隔。
本節示例項目參考 TransportStdioServer、TransportStdioClient。

當使用 stdio 時,McpServer 只需要實現靜態方法并配置特性注解即可,然后需要將該程序編譯為 .exe。

TransportStdioServer 添加 Tool :
后面講解 Tool ,這里先跳過。
[McpServerToolType]
public class EchoTool
{
[McpServerTool, Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"hello {message}";
}
然后創建 MCP Server 服務,并使用 WithStdioServerTransport() 暴露接口能力。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TransportStdioServer;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithTools<EchoTool>();
builder.Logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
await builder.Build().RunAsync();
編譯 TransportStdioServer 項目,在 Windows 下會生成 .exe 文件,復制 .exe 文件的絕對路徑,在編寫 Client 時要用。

C# 編寫 Client 時,需要通過命令行參數導入 .exe 文件,示例如下:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddEnvironmentVariables()
.AddUserSecrets<Program>();
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
// 要使用絕對路徑,這里筆者省略了
Command = "E:/../../TransportStdioServer.exe"
});
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
var tools = await mcpClient.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
}
啟動 TransportStdioClient,控制臺會打印 TransportStdioServer 中的所有 Mcp tool。

StdioClientTransport 原理是基于命令行參數啟動 TransportStdioServer,StdioClient 會將命令行參數拼接起來,然后以子進程方式啟動 MCP Server,命令行示例:
cmd.exe/c E:/../TransportStdioServer.exe
StdioClientTransport 核心代碼啟動子進程:

SSE
本節參考示例項目:TransportSseServer、TransportSseClient。
SSE 是通過 HTTP 長連接實現遠程通信的,在使用各種 AI 對話應用時,AI 會像打字機一樣逐個輸出字符,這種通過 HTTP 長連接、由 HTTP Server 服務器持續推送內容的方式就叫 sse。
SSE Server 需提供兩個端點:
- /sse(GET請求):建立長連接,接收服務器推送的事件流。
- /messages(POST請求):客戶端發送請求至該端點。

在 TransportSseServer 實現簡單的 EchoTool。
[McpServerToolType]
public sealed class EchoTool
{
[McpServerTool, Description("Echoes the input back to the client.")]
public static string Echo(string message)
{
return "hello " + message;
}
}
配置 MCP Server 支持 SSE:
using TransportSseServer.Tools;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<EchoTool>()
.WithTools<SampleLlmTool>();
var app = builder.Build();
app.MapMcp();
app.Run("http://0.0.0.0:5000");
TransportSseClient 實現客戶端連接 Mcp Server,其代碼非常簡單,連接到 MCP Server 后將對方支持的 Tool 列出來。
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
var defaultOptions = new McpClientOptions
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
};
var defaultConfig = new SseClientTransportOptions
{
Endpoint = new Uri($"http://localhost:5000/sse"),
Name = "Everything",
};
// Create client and run tests
await using var client = await McpClientFactory.CreateAsync(
new SseClientTransport(defaultConfig),
defaultOptions,
loggerFactory: NullLoggerFactory.Instance);
var tools = await client.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
}
Streamable
- Streamable HTTP 是 SSE 的升級方案,完全基于標準 HTTP 協議,移除了專用 SSE 端點,所有消息通過 /message 端點傳輸。
本節不講解 Streamable 。
MCP Tool 說明
目前社區有兩大主流 LLM 開發框架,分別是 Microsoft.SemanticKernel、LangChain,它們都支持 Plugin ,能夠將本地函數、Swagger 等轉換為函數,將 Function 提交給 LLM,AI 返回要調用的 Function 后,由框架引擎實現動態調用,這樣功能叫 Function call。
注意,MCP 有很多功能,其中一個叫 MCP Tool,可以視為跟 Plugin 實現類似功能的東西。
MCP Tool 對標 Plugin ,MCP 不止包含 Tool 這一功能。
但是每個 LLM 框架的 Plugin 實現方式不一樣,其使用和實現機制跟語言特性深度綁定,不能實現跨服務跨平臺使用,所以出現了 MCP Tool, MCP Tool 是對標 Plugin 的一類功能,主要目的跟 Plugin 一樣提供 Function,但是 MCP 有統一協議標準,跟語言無關、跟平臺無關,但是 MCP 也不是完全替換 Plugin ,Plugin 依然具有很大的用武之地。
MCP Tool、Plugin 最后都是轉換為 Function call 的,有很多人會把 MCP 、MCP Tool 和 Function call 搞混,認為 MCP 是替換 Function call 的,所以要注意,對標 Plugin 的是 MCP Tool,而兩者都是轉換為 Function 給 AI 使用的。
MCP Tool
以 TransportSseClient 為例,如果要在 Client 調用 TransportSseServer 的 Tool,需要指定 Tool 名字和參數。
后續將會講解如何通過 SK 將 mcp tool 提供給 AI 模型。
var echoTool = tools.First(x => x.Name == "Echo");
var result = await client.CallToolAsync("Echo", new Dictionary<string, object?>
{
{ "message","癡者工良"}
});
foreach (var item in result.Content)
{
Console.WriteLine($"type: {item.Type},text: {item.Text}");
}
讓我們再回顧 MCP Server 是怎么提供 Tool 的。
首先服務端需要定義 Tool 類和函數。
[McpServerToolType]
public sealed class EchoTool
{
[McpServerTool, Description("Echoes the input back to the client.")]
public static string Echo(string message)
{
return "hello " + message;
}
}
Mcp server 可以通過以下兩種方式暴露 tool。
// 直接指定 Tool 類
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithTools<EchoTool>()
.WithTools<SampleLlmTool>();
// 掃描程序集
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithStdioServerTransport()
.WithToolsFromAssembly();
Client 識別服務端的 Tool 列表時,可以使用 McpClientTool.ProtocolTool.InputSchema 獲取 tool 的輸入參數格式:

其內容格式示例如下:
Annotations: null
Description: "Echoes the input back to the client."
Name: "Echo"
InputSchema: "{"title":"Echo","description":"Echoes the input back to the client.","type":"object","properties":{"message":{"type":"string"}},"required":["message"]}"
[McpServerToolType] 用于將包含應該作為ModelContextProtocol.Server.McpServerTools公開的方法的類型屬性化。
[McpServerTool]用于指示應該將方法視為 ModelContextProtocol.Server.McpServerTool。
[Description] 則用于添加注釋。
依賴注入
在實現 Tool 函數時,服務端是可以通過函數實現依賴注入的。
參考示例項目 InjectServer、InjectClient。

添加一個服務類并注冊到容器中。
public class MyService
{
public string Echo(string message)
{
return "hello " + message;
}
}
builder.Services.AddScoped<MyService>();
在 Tool 函數中注入該服務:
[McpServerToolType]
public sealed class MyTool
{
[McpServerTool, Description("Echoes the input back to the client.")]
public static string Echo(MyService myService, string message)
{
return myService.Echo(message);
}
}
將 MCP Tool 提交到 AI 對話中
前面提到,MCP Tool 和 Plugin 都是實現 Function call 的一種方式,當在 AI 對話中使用 Tool 時,其主要過程如下:
當你提出問題時:
- client 將你的問題發送給 LLM ;
- LLM 分析可用的 tools 并決定使用哪些 tool;
- client 通過 MCP server 執行選擇的 tool
- 結果被發回給 LLM;
- LLM 制定自然語言響應;
- 響應顯示給你;
這個過程并不是只有一兩次,可能發生多次,具體細節將會在 高德地圖 MCP 實戰 中講解,這里只是簡單提及。
將 Tool 提交到對話上下文的偽代碼:
// Get available functions.
IList<McpClientTool> tools = await client.ListToolsAsync();
// Call the chat client using the tools.
IChatClient chatClient = ...;
var response = await chatClient.GetResponseAsync(
"your prompt here",
new() { Tools = [.. tools] },
高德地圖 MCP 實戰
聊了這么久,終于到了實戰對接環節,本節將會通過高德地圖案例講解 MCP Tool 的邏輯細節和對接使用方式。
代碼參考示例項目 amap。
高德地圖 MCP Server 目前主要提供的功能:
- 地理編碼
- 逆地理編碼
- IP 定位
- 天氣查詢
- 騎行路徑規劃
- 步行路徑規劃
- 駕車路徑規劃
- 公交路徑規劃
- 距離測量
- 關鍵詞搜索
- 周邊搜索
- 詳情搜索
其 Tool 名稱如下:
maps_direction_bicycling
maps_direction_driving
maps_direction_transit_integrated
maps_direction_walking
maps_distance
maps_geo
maps_regeocode
maps_ip_location
maps_around_search
maps_search_detail
maps_text_search
maps_weather
高德地圖每天都給開發者提供了免費額度,所以做該實驗時,不需要擔心需要付費。
打開 https://console.amap.com/dev/key/app 創建一個新的應用,然后復制應用 key。
高德 mcp 服務器地址:
https://mcp.amap.com/sse?key={在高德官網上申請的key}
在 amap 項目的 appsettings.json 添加以下 json,替換里面的部分參數。
筆者注,除了 gpt-4o 模型,其它注冊 Function call 的模型也可以使用。
"McpServers": {
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key={在高德官網上申請的key}"
}
},
"AIModel": {
"ModelId": "gpt-4o",
"DeploymentName": "gpt-4o",
"Endpoint": "https://openai.com/",
"Key": "aaaaaaaa"
}

導入配置并創建日志:
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json")
.Build();
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
第一步:創建 mcp 客戶端
連接高德 MCP Server,并獲取 Tool 列表。
var defaultOptions = new McpClientOptions
{
ClientInfo = new() { Name = "地圖規劃", Version = "1.0.0" }
};
var defaultConfig = new SseClientTransportOptions
{
Endpoint = new Uri(configuration["McpServers:amap-amap-sse:url"]!),
Name = "amap-amap-sse",
};
await using var client = await McpClientFactory.CreateAsync(
new SseClientTransport(defaultConfig),
defaultOptions,
loggerFactory: factory);
var tools = await client.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
}

第二步:連接 AI 模型和配置 MCP
使用 SemanticKernel 框架對接 LLM,將 MCP Tool 轉換為 Function 添加到對話上下文中。
var aiModel = configuration.GetSection("AIModel");
var builder = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: aiModel["ModelId"],
endpoint: aiModel["Endpoint"],
apiKey: aiModel["Key"]);
builder.Services.AddLogging(s =>
{
s.AddConsole();
});
Kernel kernel = builder.Build();
// 這里將 mcp 轉換為 functaion call
kernel.Plugins.AddFromFunctions("amap", tools.Select(aiFunction => aiFunction.AsKernelFunction()));
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
Temperature = 0,
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
};

第三步:對話交互
編寫控制臺與用戶對話交互。
var history = new ChatHistory();
string? userInput;
do
{
Console.Write("用戶提問 > ");
userInput = Console.ReadLine();
history.AddUserMessage(userInput!);
var result = await chatCompletionService.GetChatMessageContentAsync(
history,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel);
Console.WriteLine("AI 回答 > " + result);
history.AddMessage(result.Role, result.Content ?? string.Empty);
} while (userInput is not null);

演示地圖規劃
注意,由于高德地圖免費額度限流,而 AI 對話時可能有多次對 MCP Server 請求,因此有時候效果并不是那么好。
1. 智能旅游路線規劃
最多支持16個途經點的旅游路線規劃,自動計算最優順序,并提供可視化地圖鏈接。
使用示例:
請幫我規劃一條上海三日游路線,包括外灘、東方明珠、迪士尼、豫園、南京路,并提供可視化地圖

2. 景點搜索與詳情查詢
查詢景點的詳細信息,包括評分、開放時間、門票價格等。
使用示例:
請查詢黃山風景區的開放時間、門票價格和旅游季節推薦

AI 是怎么識別調用 MCP
在編寫高德地圖規劃時,有一段代碼是將 MCP 服務器的接口轉換為 Function 的,代碼如下:
kernel.Plugins
.AddFromFunctions("amap", tools.Select(aiFunction => aiFunction.AsKernelFunction()))
其實在這里就可以下結論,并不是 AI 模型直接調用 MCP Server 的,依然 Client 進行是 Function call 。
通過攔截 http 請求可以發現,當用戶輸入 請幫我規劃一條上海三日游路線,包括外灘、東方明珠、迪士尼、豫園、南京路,并提供可視化地圖 時,客戶端首先將用戶提問和 mcp 服務所提供的 function call 一起發送到 AI 模型服務器。
對話時,Client 提供給 LLM 的 Function (MCP Tool)列表。


然后 AI 回答要調用的 Function call 步驟和參數,接著由客戶端實現將 Function 定位 MCP Server,并順序調用每個 Tool。
LLM 返回要順序調用的 Function 列表以及參數:

客戶端將每個 Function 的執行結果和用戶的提問等信息,一起再次提交給 AI 模型服務器。

由于高德接口并發限制,有部分接口調用失敗,那么客戶端可能會來回請求多次,最后輸出 AI 的回答。

到這里,讀者應該明白 MCP Tool、Plugin、Function Call 的關系了吧!
實現 Mcp Server
前面筆者介紹了 MCP Tool,但是 MCP Server 還可以提供很多很有用的功能,MCP 協議定義了以下核心模塊:
- Core architecture
- Resources
- Prompts
- Tools
- Sampling
- Roots
- Transports
作為當前社區中最關注的 Tools,本文已經單獨介紹,接下來將會以繼續講解其它功能模塊。
實現 Resources
示例項目參考:ResourceServer、ResourceClient。
Resources 定義:Resources 是 Model Context Protocol (MCP) 中的一個核心原語,它允許服務器暴露可以被 clients 讀取并用作 LLM 交互上下文的數據和內容。
Resources 代表 MCP server 想要提供給 clients 的任何類型的數據,在使用上,MCP Server 可以給每種資源定義一個 Uri,這個 Uri 的協議格式可以是虛擬的,這不重要,只要是能夠定位資源的一段 Uri 字符串即可。
只看定義,讀者可能不理解什么意思,沒關系,等后面動手做的時候就知道了。
Resources 可以包括:
- 文件內容
- 數據庫記錄
- API 響應
- 實時系統數據
- 屏幕截圖和圖像
- 日志文件
- 等等
每個 resource 都由一個唯一的 URI 標識,并且可以包含文本或二進制數據。
Resources 使用以下格式的 URIs 進行標識:
[protocol]://[host]/[path]
例如:
file:///home/user/documents/report.pdfpostgres://database/customers/schemascreen://localhost/display1
Resources 的文件類型,主要是文本資源和二進制資源。
文本資源
文本資源包含 UTF-8 編碼的文本數據。這些適用于:
- 源代碼
- 配置文件
- 日志文件
- JSON/XML 數據
- 純文本
二進制資源
二進制資源包含以 base64 編碼的原始二進制數據。這些適用于:
- 圖像
- PDFs
- 音頻文件
- 視頻文件
- 其他非文本格式
Resources Server、Client 實現
客戶端使用 Resources 服務時,有以下 Api,那么在本節的學習中,將會圍繞這這些接口講解如何在服務段實現對應的功能。

實現 Resources 時,主要有兩種提供 Resources 的方式,一種是通過模板動態提供 Resource Uri 的格式,一種是直接提供具體的 Resource Uri。
Resource Uri 格式示例:
"test://static/resource/{README.txt}"
MCP Server 提供的 Resource Uri 格式是可以隨意自定義的,這些 Uri 并不是直接給 Client 讀取的,Client 在需要讀取 Resource 是,把 Uri 發送給 MCP Server,MCP Server 自行解析 Uri 并定位對應的資源,然后把資源內容返回給 Client。
也就是說,該 Uri 的協議其實就是字符串,只要在當前 MCP Server 和 Client 之間能用即可。
MCP Server 可以通過模板提供某類資源,這類資源的的地址是動態的,要根據 id 實時獲取。
builder.Services.AddMcpServer()
.WithListResourceTemplatesHandler(async (ctx, ct) =>
{
return new ListResourceTemplatesResult
{
ResourceTemplates =
[
new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" }
]
};
});
對于地址固定的 Resource,可以通過這種方式暴露出去,比如有個使用必讀的文件,只需要固定暴露地址。
builder.Services.AddMcpServer()
.WithListResourcesHandler(async (ctx, ct) =>
{
await Task.CompletedTask;
var readmeResource = new Resource
{
Uri = "test://static/resource/README.txt",
Name = "Resource README.txt",
MimeType = "application/octet-stream",
Description = Convert.ToBase64String(Encoding.UTF8.GetBytes("這是一個必讀文件"))
};
return new ListResourcesResult
{
Resources = new List<Resource>
{
readmeResource
}
};
})
Client 讀取資源模板和靜態資源列表:
var defaultOptions = new McpClientOptions
{
ClientInfo = new() { Name = "ResourceClient", Version = "1.0.0" }
};
var defaultConfig = new SseClientTransportOptions
{
Endpoint = new Uri($"http://localhost:5000/sse"),
Name = "Everything",
};
// Create client and run tests
await using var client = await McpClientFactory.CreateAsync(
new SseClientTransport(defaultConfig),
defaultOptions,
loggerFactory: NullLoggerFactory.Instance);
var resourceTemplates = await client.ListResourceTemplatesAsync();
var resources = await client.ListResourcesAsync();
foreach (var template in resourceTemplates)
{
Console.WriteLine($"Connected to server with resource templates: {template.Name}");
}
foreach (var resource in resources)
{
Console.WriteLine($"Connected to server with resources: {resource.Name}");
}
那么,客戶端如果從 MCP 服務器讀取資源只需要將 Resource Uri 傳遞即可。
var readmeResource = await client.ReadResourceAsync(resources.First().Uri);
這里只介紹了 MCP Server 提供 Resource Uri,那么當 Client 要獲取某個 Resource Uri 的內容時,MCP Server 要怎么處理呢?
ModelContextProtocol CSharp 目前提供了兩種實現:
-
TextResourceContents
-
BlobResourceContents
比如說,當 Client 訪問 test://static/resource/README.txt 時,可以將 README.txt 文件直接以文本的形式返回:
.WithReadResourceHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is null || !uri.StartsWith("test://static/resource/"))
{
throw new NotSupportedException($"Unknown resource: {uri}");
}
if(uri == "test://static/resource/README.txt")
{
var readmeResource = new Resource
{
Uri = "test://static/resource/README.txt",
Name = "Resource README.txt",
MimeType = "application/octet-stream",
Description = "這是一個必讀文件"
};
return new ReadResourceResult
{
Contents = [new TextResourceContents
{
Text = File.ReadAllText("README.txt"),
MimeType = readmeResource.MimeType,
Uri = readmeResource.Uri,
}]
};
}
})

如果 Client 訪問了其它 Resource,則以二進制的形式返回:
.WithReadResourceHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is null || !uri.StartsWith("test://static/resource/"))
{
throw new NotSupportedException($"Unknown resource: {uri}");
}
int index = int.Parse(uri["test://static/resource/".Length..]) - 1;
if (index < 0 || index >= ResourceGenerator.Resources.Count)
{
throw new NotSupportedException($"Unknown resource: {uri}");
}
var resource = ResourceGenerator.Resources[index];
return new ReadResourceResult
{
Contents = [new TextResourceContents
{
Text = resource.Description!,
MimeType = resource.MimeType,
Uri = resource.Uri,
}]
};
})
客戶端讀取 "test://static/resource/README.txt" 示例:
var readmeResource = await client.ReadResourceAsync(resources.First().Uri);
var textContent = readmeResource.Contents.First() as TextResourceContents;
Console.WriteLine(textContent.Text));

Resource 訂閱
Clients 可以訂閱特定 resources 的更新:
- Client 使用 resource URI 發送
resources/subscribe - 當 resource 更改時,服務器發送
notifications/resources/updated - Client 可以使用
resources/read獲取最新內容 - Client 可以使用
resources/unsubscribe取消訂閱
一般來說,MCP Server 要實現工廠模式,以便動態記錄有哪些 Resource Uri 是被訂閱的,那么當這些 Uri 的資源發生變化時,才需要推送,否則即使發送變化,也沒有推送更新的必要。
但是目前來說,只有 WithStdioServerTransport() 才能起效,筆者在 WithHttpTransport() 實驗失敗。
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is not null)
{
subscriptions.Add(uri);
}
return new EmptyResult();
})
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is not null)
{
subscriptions.Remove(uri);
}
return new EmptyResult();
});
例如,我們可以一個接口,手動觸發更新訂閱了 "test://static/resource/README.txt 的 Client。
await _mcpServer.SendNotificationAsync("notifications/resource/updated",
new
{
Uri = "test://static/resource/README.txt",
});
return "已通知";
客戶端只需要很簡單的代碼即可訂閱。
client.RegisterNotificationHandler("notifications/resource/updated", async (message, ctx) =>
{
await Task.CompletedTask;
// 回調
});
await client.SubscribeToResourceAsync("test://static/resource/README.txt");
最佳實踐
在實現 resource 支持時:
- 使用清晰、描述性的 resource 名稱和 URIs
- 包含有用的描述以指導 LLM 理解
- 在已知時設置適當的 MIME 類型
- 為動態內容實現 resource 模板
- 對頻繁更改的 resources 使用訂閱
- 使用清晰的錯誤消息優雅地處理錯誤
- 考慮對大型 resource 列表進行分頁
- 在適當的時候緩存 resource 內容
- 在處理之前驗證 URIs
- 記錄你的自定義 URI 方案
安全考慮
在暴露 resources 時:
- 驗證所有 resource URIs
- 實現適當的訪問控制
- 清理文件路徑以防止目錄遍歷
- 謹慎處理二進制數據
- 考慮對 resource 讀取進行速率限制
- 審核 resource 訪問
- 加密傳輸中的敏感數據
- 驗證 MIME 類型
- 為長時間運行的讀取操作實現超時
- 適當處理 resource 清理
實現 Prompts
Prompts 的目的是創建可復用的提示模板和工作流,MCP Server Prompts 允許 servers 定義可復用的提示模板和工作流,clients 可以輕松地將它們呈現給用戶和 LLMs。它們提供了一種強大的方式來標準化和共享常見的 LLM 交互。
示例項目參考 PromptsServer、PromptsClient。
MCP 中的 Prompts 是預定義的模板,可以:
- 接受動態參數
- 包含來自 resources 的上下文
- 鏈接多個交互
- 引導特定的工作流程
- 呈現為 UI 元素(如斜杠命令)
MCP Server 示例:
[McpServerPromptType]
public static class MyPrompts
{
[McpServerPrompt, Description("Creates a prompt to summarize the provided message.")]
public static ChatMessage Summarize([Description("The content to summarize")] string content) =>
new(ChatRole.User, $"Please summarize this content into a single sentence: {content}");
}
根據官方框架倉庫的示例,Prompts 主要有兩種使用方式。
第一種直接返回字符串。
[McpServerPromptType]
public class SimplePromptType
{
[McpServerPrompt(Name = "simple_prompt"), Description("A prompt without arguments")]
public static string SimplePrompt() => "This is a simple prompt without arguments";
}
第二種則是編排對話上下文再返回。
[McpServerPromptType]
public class ComplexPromptType
{
[McpServerPrompt(Name = "complex_prompt"), Description("A prompt with arguments")]
public static IEnumerable<ChatMessage> ComplexPrompt(
[Description("Temperature setting")] int temperature,
[Description("Output style")] string? style = null)
{
return [
new ChatMessage(ChatRole.User,$"This is a complex prompt with arguments: temperature={temperature}, style={style}"),
new ChatMessage(ChatRole.Assistant, "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"),
new ChatMessage(ChatRole.User, [new DataContent(Convert.ToBase64String(File.ReadAllBytes("img.png")))])
];
}
}
Client 可以獲取 MCP Server 提供的提示詞列表。
var prompts = await client.ListPromptsAsync();
foreach (var item in prompts)
{
Console.WriteLine($"prompt name :{item.Name}");
}
客戶端可以通過使用需要的提示詞,將其自動加載到當前 AI 對話上下文中。
var result = await prompts.First(x => x.Name == "test").GetAsync(new Dictionary<string, object?>() { ["message"] = "hello" });
IList<ChatMessage> chatMessages = result.ToChatMessages();
最佳實踐
在實現 prompts 時:
- 使用清晰、描述性的 prompt 名稱
- 為 prompts 和參數提供詳細的描述
- 驗證所有必需的參數
- 優雅地處理缺失的參數
- 考慮 prompt 模板的版本控制
- 在適當的時候緩存動態內容
- 實現錯誤處理
- 記錄預期的參數格式
- 考慮 prompt 的可組合性
- 使用各種輸入測試 prompts
UI 集成
Prompts 可以在 client UI 中呈現為:
- 斜杠命令
- 快速操作
- 上下文菜單項
- 命令面板條目
- 引導式工作流程
- 交互式表單
實現 Sampling
Sampling 是一個強大的 MCP 功能,它允許 servers 通過 client 請求 LLM 補全,從而實現復雜的 agentic 行為,同時保持安全性和隱私性。
Sampling 流程遵循以下步驟:
- Server 向 client 發送
sampling/createMessage請求 - Client 審查請求并可以修改它
- Client 從 LLM 中 sampling
- Client 審查補全結果
- Client 將結果返回給 server
這種人機交互設計確保用戶可以控制 LLM 看到和生成的內容。
按筆者理解來說, Sampling 適合 AI Agent 應用,服務器下方命令給 Client 后,Client 自身通過 LLM 等完成任務,并將結果返回給 Server。
但是目前來說, ModelContextProtocol Csharp 應該缺乏這種功能,因為 IMcpServer 只能出現在 Client 請求 Server 時的上下文中,而 Server 是不能隨意找到 Client 的,不能通過注入 IMcpServer 去給 Client 下發任務。
對于 stdio 方式的 MCP Server 來說,可以通過以下方式實現 Sampling。
await ctx.Server.RequestSamplingAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
],
對于 http 方式實現的 MCP Server ,由于不能實現 Server 調用 Client ,因此這里不再贅述。

浙公網安備 33010602011771號