工良出品 | 长文讲解 MCP 和案例实战

打印 上一主题 下一主题

主题 1984|帖子 1984|积分 5952

作者:痴者工良
博客地点:https://www.whuanle.cn/
示例项目地点: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 ,这里先跳过。
  1. [McpServerToolType]
  2. public class EchoTool
  3. {
  4.     [McpServerTool, Description("Echoes the message back to the client.")]
  5.     public static string Echo(string message) => $"hello {message}";
  6. }
复制代码
然后创建 MCP Server 服务,并利用 WithStdioServerTransport() 暴露接口能力。
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. using Microsoft.Extensions.Logging;
  4. using TransportStdioServer;
  5. var builder = Host.CreateApplicationBuilder(args);
  6. builder.Services.AddMcpServer()
  7.     .WithStdioServerTransport()
  8.     .WithTools<EchoTool>();
  9. builder.Logging.AddConsole(options =>
  10. {
  11.     options.LogToStandardErrorThreshold = LogLevel.Trace;
  12. });
  13. await builder.Build().RunAsync();
复制代码
编译 TransportStdioServer  项目,在 Windows 下会天生 .exe 文件,复制 .exe 文件的绝对路径,在编写 Client 时要用。


C# 编写 Client 时,需要通过下令行参数导入 .exe 文件,示比方下:
  1. using Microsoft.Extensions.Configuration;
  2. using Microsoft.Extensions.Hosting;
  3. using ModelContextProtocol.Client;
  4. using ModelContextProtocol.Protocol.Transport;
  5. var builder = Host.CreateApplicationBuilder(args);
  6. builder.Configuration
  7.     .AddEnvironmentVariables()
  8.     .AddUserSecrets<Program>();
  9. var clientTransport = new StdioClientTransport(new()
  10. {
  11.     Name = "Demo Server",
  12.     // 要使用绝对路径,这里笔者省略了
  13.     Command = "E:/../../TransportStdioServer.exe"
  14. });
  15. await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
  16. var tools = await mcpClient.ListToolsAsync();
  17. foreach (var tool in tools)
  18. {
  19.     Console.WriteLine($"Connected to server with tools: {tool.Name}");
  20. }
复制代码
启动 TransportStdioClient,控制台会打印 TransportStdioServer 中的所有 Mcp tool。


StdioClientTransport 原理是基于下令行参数启动 TransportStdioServer,StdioClient 会将下令行参数拼接起来,然后以子进程方式启动 MCP Server,下令行示例:
  1. 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。
  1. [McpServerToolType]
  2. public sealed class EchoTool
  3. {
  4.     [McpServerTool, Description("Echoes the input back to the client.")]
  5.     public static string Echo(string message)
  6.     {
  7.         return "hello " + message;
  8.     }
  9. }
复制代码
配置 MCP Server 支持 SSE:
  1. using TransportSseServer.Tools;
  2. var builder = WebApplication.CreateBuilder(args);
  3. builder.Services.AddMcpServer()
  4.     .WithHttpTransport()
  5.     .WithTools<EchoTool>()
  6.     .WithTools<SampleLlmTool>();
  7. var app = builder.Build();
  8. app.MapMcp();
  9. app.Run("http://0.0.0.0:5000");
复制代码
TransportSseClient 实现客户端毗连 Mcp Server,其代码非常简单,毗连到 MCP Server 后将对方支持的 Tool 列出来。
  1. using Microsoft.Extensions.Logging;
  2. using Microsoft.Extensions.Logging.Abstractions;
  3. using ModelContextProtocol.Client;
  4. using ModelContextProtocol.Protocol.Transport;
  5. var defaultOptions = new McpClientOptions
  6. {
  7.     ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
  8. };
  9. var defaultConfig = new SseClientTransportOptions
  10. {
  11.     Endpoint = new Uri($"http://localhost:5000/sse"),
  12.     Name = "Everything",
  13. };
  14. // Create client and run tests
  15. await using var client = await McpClientFactory.CreateAsync(
  16.     new SseClientTransport(defaultConfig),
  17.     defaultOptions,
  18.     loggerFactory: NullLoggerFactory.Instance);
  19. var tools = await client.ListToolsAsync();
  20. foreach (var tool in tools)
  21. {
  22.     Console.WriteLine($"Connected to server with tools: {tool.Name}");
  23. }
复制代码
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 模型。
  1. var echoTool = tools.First(x => x.Name == "Echo");
  2. var result = await client.CallToolAsync("Echo", new Dictionary<string, object?>
  3. {
  4.     { "message","痴者工良"}
  5. });
  6. foreach (var item in result.Content)
  7. {
  8.     Console.WriteLine($"type: {item.Type},text: {item.Text}");
  9. }
复制代码
让我们再回首 MCP Server 是怎么提供 Tool 的。
首先服务端需要定义 Tool 类和函数。
  1. [McpServerToolType]
  2. public sealed class EchoTool
  3. {
  4.     [McpServerTool, Description("Echoes the input back to the client.")]
  5.     public static string Echo(string message)
  6.     {
  7.         return "hello " + message;
  8.     }
  9. }
复制代码
Mcp server 可以通过以下两种方式暴露 tool。
  1. // 直接指定 Tool 类
  2. builder.Services
  3.     .AddMcpServer()
  4.     .WithHttpTransport()
  5.     .WithTools<EchoTool>()
  6.     .WithTools<SampleLlmTool>();
  7. // 扫描程序集
  8. builder.Services
  9.     .AddMcpServer()
  10.     .WithHttpTransport()
  11.     .WithStdioServerTransport()
  12.     .WithToolsFromAssembly();
复制代码
Client 识别服务端的 Tool 列表时,可以利用 McpClientTool.ProtocolTool.InputSchema 获取 tool 的输入参数格式:


其内容格式示比方下:
  1. Annotations: null
  2. Description: "Echoes the input back to the client."
  3. Name: "Echo"
  4. 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。


添加一个服务类并注册到容器中。
  1. public class MyService
  2. {
  3.     public string Echo(string message)
  4.     {
  5.         return "hello " + message;
  6.     }
  7. }
复制代码
  1. builder.Services.AddScoped<MyService>();
复制代码
在 Tool 函数中注入该服务:
  1. [McpServerToolType]
  2. public sealed class MyTool
  3. {
  4.     [McpServerTool, Description("Echoes the input back to the client.")]
  5.     public static string Echo(MyService myService, string message)
  6.     {
  7.         return myService.Echo(message);
  8.     }
  9. }
复制代码
将 MCP Tool 提交到 AI 对话中

前面提到,MCP Tool 和 Plugin 都是实现 Function call 的一种方式,当在 AI 对话中利用 Tool 时,其重要过程如下:
当你提出题目时:

  • client 将你的题目发送给 LLM ;
  • LLM 分析可用的 tools 并决定利用哪些 tool;
  • client 通过 MCP server 实行选择的 tool
  • 结果被发回给 LLM;
  • LLM 制定自然语言相应;
  • 相应显示给你;

这个过程并不是只有一两次,大概发生多次,详细细节将会在 高德地图 MCP 实战 中讲解,这里只是简单提及。
将 Tool 提交到对话上下文的伪代码
  1. // Get available functions.
  2. IList<McpClientTool> tools = await client.ListToolsAsync();
  3. // Call the chat client using the tools.
  4. IChatClient chatClient = ...;
  5. var response = await chatClient.GetResponseAsync(
  6.     "your prompt here",
  7.     new() { Tools = [.. tools] },
复制代码
高德地图 MCP 实战

聊了这么久,终于到了实战对接环节,本节将会通过高德地图案例讲解 MCP Tool 的逻辑细节和对接利用方式。
代码参考示例项目 amap。

高德地图 MCP Server 目前重要提供的功能:

  • 地理编码
  • 逆地理编码
  • IP 定位
  • 天气查询
  • 骑行路径规划
  • 步行路径规划
  • 驾车路径规划
  • 公交路径规划
  • 距离测量
  • 关键词搜索
  • 周边搜索
  • 详情搜索

其 Tool 名称如下:
  1. maps_direction_bicycling
  2. maps_direction_driving
  3. maps_direction_transit_integrated
  4. maps_direction_walking
  5. maps_distance
  6. maps_geo
  7. maps_regeocode
  8. maps_ip_location
  9. maps_around_search
  10. maps_search_detail
  11. maps_text_search
  12. maps_weather
复制代码
高德地图每天都给开发者提供了免费额度,所以做该实验时,不需要担心需要付费。
打开 https://console.amap.com/dev/key/app 创建一个新的应用,然后复制应用 key。

高德 mcp 服务器地点:
  1. https://mcp.amap.com/sse?key={在高德官网上申请的key}
复制代码
在 amap 项目的 appsettings.json 添加以下 json,替换里面的部分参数。
笔者注,除了 gpt-4o 模型,其它注册 Function call 的模型也可以利用。
  1.   "McpServers": {
  2.     "amap-amap-sse": {
  3.       "url": "https://mcp.amap.com/sse?key={在高德官网上申请的key}"
  4.     }
  5.   },
  6.   "AIModel": {
  7.     "ModelId": "gpt-4o",
  8.     "DeploymentName": "gpt-4o",
  9.     "Endpoint": "https://openai.com/",
  10.     "Key": "aaaaaaaa"
  11.   }
复制代码


导入配置并创建日志:
  1. var configuration = new ConfigurationBuilder()
  2.     .AddJsonFile("appsettings.json")
  3.     .AddJsonFile("appsettings.Development.json")
  4.     .Build();
  5. using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
复制代码
第一步:创建 mcp 客户端

毗连高德 MCP Server,并获取 Tool 列表。
  1. var defaultOptions = new McpClientOptions
  2. {
  3.     ClientInfo = new() { Name = "地图规划", Version = "1.0.0" }
  4. };
  5. var defaultConfig = new SseClientTransportOptions
  6. {
  7.     Endpoint = new Uri(configuration["McpServers:amap-amap-sse:url"]!),
  8.     Name = "amap-amap-sse",
  9. };
  10. await using var client = await McpClientFactory.CreateAsync(
  11.     new SseClientTransport(defaultConfig),
  12.     defaultOptions,
  13.     loggerFactory: factory);
  14. var tools = await client.ListToolsAsync();
  15. foreach (var tool in tools)
  16. {
  17.     Console.WriteLine($"Connected to server with tools: {tool.Name}");
  18. }
复制代码

第二步:毗连 AI 模型和配置 MCP

利用 SemanticKernel 框架对接 LLM,将 MCP Tool 转换为 Function 添加到对话上下文中。
  1. var aiModel = configuration.GetSection("AIModel");
  2. var builder = Kernel.CreateBuilder()
  3.     .AddAzureOpenAIChatCompletion(
  4.     deploymentName: aiModel["ModelId"],
  5.     endpoint: aiModel["Endpoint"],
  6.     apiKey: aiModel["Key"]);
  7. builder.Services.AddLogging(s =>
  8. {
  9.     s.AddConsole();
  10. });
  11. Kernel kernel = builder.Build();
  12. // 这里将 mcp 转换为 functaion call
  13. kernel.Plugins
  14. .AddFromFunctions("amap", tools.Select(aiFunction => aiFunction.AsKernelFunction()));
  15. var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
  16. OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
  17. {
  18.     Temperature = 0,
  19.     FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
  20. };
复制代码

第三步:对话交互

编写控制台与用户对话交互。
  1. var history = new ChatHistory();
  2. string? userInput;
  3. do
  4. {
  5.     Console.Write("用户提问 > ");
  6.     userInput = Console.ReadLine();
  7.     history.AddUserMessage(userInput!);
  8.     var result = await chatCompletionService.GetChatMessageContentAsync(
  9.         history,
  10.         executionSettings: openAIPromptExecutionSettings,
  11.         kernel: kernel);
  12.     Console.WriteLine("AI 回答 > " + result);
  13.     history.AddMessage(result.Role, result.Content ?? string.Empty);
  14. } while (userInput is not null);
复制代码

演示地图规划

注意,由于高德地图免费额度限流,而 AI 对话时大概有多次对 MCP Server 请求,因此有时候结果并不是那么好。
1. 智能旅游路线规划
最多支持16个途经点的旅游路线规划,主动计算最优次序,并提供可视化地图链接。
利用示例
  1. 请帮我规划一条上海三日游路线,包括外滩、东方明珠、迪士尼、豫园、南京路,并提供可视化地图
复制代码


2. 景点搜索与详情查询
查询景点的详细信息,包括评分、开放时间、门票代价等。
利用示例:
  1. 请查询黄山风景区的开放时间、门票价格和旅游季节推荐
复制代码

AI 是怎么识别调用 MCP

在编写高德地图规划时,有一段代码是将 MCP 服务器的接口转换为 Function 的,代码如下:
  1. kernel.Plugins
  2. .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 举行标识:
  1. [protocol]://[host]/[path]
复制代码
比方:

  • file:///home/user/documents/report.pdf
  • postgres://database/customers/schema
  • screen://localhost/display1

Resources 的文件类型,重要是文本资源和二进制资源。
文本资源
文本资源包含 UTF-8 编码的文本数据。这些适用于:

  • 源代码
  • 配置文件
  • 日志文件
  • JSON/XML 数据
  • 纯文本

二进制资源
二进制资源包含以 base64 编码的原始二进制数据。这些适用于:

  • 图像
  • PDFs
  • 音频文件
  • 视频文件
  • 其他非文本格式

Resources Server、Client 实现

客户端利用 Resources 服务时,有以下 Api,那么在本节的学习中,将会围绕这这些接口讲解如安在服务段实现对应的功能。


实现 Resources 时,重要有两种提供 Resources 的方式,一种是通过模板动态提供 Resource Uri 的格式,一种是直接提供详细的 Resource Uri。
Resource Uri 格式示例:
  1. "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 实时获取。
  1. builder.Services.AddMcpServer()
  2.         .WithListResourceTemplatesHandler(async (ctx, ct) =>
  3.         {
  4.             return new ListResourceTemplatesResult
  5.             {
  6.                 ResourceTemplates =
  7.                 [
  8.                     new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" }
  9.                 ]
  10.             };
  11.         });
复制代码
对于地点固定的 Resource,可以通过这种方式暴露出去,好比有个利用必读的文件,只需要固定暴露地点。
  1. builder.Services.AddMcpServer()
  2.                 .WithListResourcesHandler(async (ctx, ct) =>
  3.         {
  4.             await Task.CompletedTask;
  5.             var readmeResource = new Resource
  6.             {
  7.                 Uri = "test://static/resource/README.txt",
  8.                 Name = "Resource README.txt",
  9.                 MimeType = "application/octet-stream",
  10.                 Description = Convert.ToBase64String(Encoding.UTF8.GetBytes("这是一个必读文件"))
  11.             };
  12.             return new ListResourcesResult
  13.             {
  14.                 Resources = new List<Resource>
  15.                 {
  16.                     readmeResource
  17.                 }
  18.             };
  19.         })
复制代码
Client 读取资源模板和静态资源列表:
  1. var defaultOptions = new McpClientOptions
  2. {
  3.     ClientInfo = new() { Name = "ResourceClient", Version = "1.0.0" }
  4. };
  5. var defaultConfig = new SseClientTransportOptions
  6. {
  7.     Endpoint = new Uri($"http://localhost:5000/sse"),
  8.     Name = "Everything",
  9. };
  10. // Create client and run tests
  11. await using var client = await McpClientFactory.CreateAsync(
  12.     new SseClientTransport(defaultConfig),
  13.     defaultOptions,
  14.     loggerFactory: NullLoggerFactory.Instance);
  15. var resourceTemplates = await client.ListResourceTemplatesAsync();
  16. var resources = await client.ListResourcesAsync();
  17. foreach (var template in resourceTemplates)
  18. {
  19.     Console.WriteLine($"Connected to server with resource templates: {template.Name}");
  20. }
  21. foreach (var resource in resources)
  22. {
  23.     Console.WriteLine($"Connected to server with resources: {resource.Name}");
  24. }
复制代码
那么,客户端如果从 MCP 服务器读取资源只需要将 Resource Uri 通报即可。
  1. 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 文件直接以文本的形式返回:
  1.     .WithReadResourceHandler(async (ctx, ct) =>
  2.     {
  3.         var uri = ctx.Params?.Uri;
  4.         if (uri is null || !uri.StartsWith("test://static/resource/"))
  5.         {
  6.             throw new NotSupportedException($"Unknown resource: {uri}");
  7.         }
  8.         if(uri == "test://static/resource/README.txt")
  9.         {
  10.             var readmeResource = new Resource
  11.             {
  12.                 Uri = "test://static/resource/README.txt",
  13.                 Name = "Resource README.txt",
  14.                 MimeType = "application/octet-stream",
  15.                 Description = "这是一个必读文件"
  16.             };
  17.             return new ReadResourceResult
  18.             {
  19.                 Contents = [new TextResourceContents
  20.                 {
  21.                     Text = File.ReadAllText("README.txt"),
  22.                     MimeType = readmeResource.MimeType,
  23.                     Uri = readmeResource.Uri,
  24.                 }]
  25.             };
  26.         }
  27.     })
复制代码


如果 Client 访问了其它 Resource,则以二进制的形式返回:
  1.     .WithReadResourceHandler(async (ctx, ct) =>
  2.     {
  3.         var uri = ctx.Params?.Uri;
  4.         if (uri is null || !uri.StartsWith("test://static/resource/"))
  5.         {
  6.             throw new NotSupportedException($"Unknown resource: {uri}");
  7.         }
  8.         int index = int.Parse(uri["test://static/resource/".Length..]) - 1;
  9.         if (index < 0 || index >= ResourceGenerator.Resources.Count)
  10.         {
  11.             throw new NotSupportedException($"Unknown resource: {uri}");
  12.         }
  13.         var resource = ResourceGenerator.Resources[index];
  14.         return new ReadResourceResult
  15.         {
  16.             Contents = [new TextResourceContents
  17.                 {
  18.                     Text = resource.Description!,
  19.                     MimeType = resource.MimeType,
  20.                     Uri = resource.Uri,
  21.                 }]
  22.         };
  23.     })
复制代码
客户端读取 "test://static/resource/README.txt" 示例:
  1. var readmeResource = await client.ReadResourceAsync(resources.First().Uri);
  2. var textContent = readmeResource.Contents.First() as TextResourceContents;
  3. 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() 实验失败。
  1.     .WithSubscribeToResourcesHandler(async (ctx, ct) =>
  2.     {
  3.         var uri = ctx.Params?.Uri;
  4.         if (uri is not null)
  5.         {
  6.             subscriptions.Add(uri);
  7.         }
  8.         return new EmptyResult();
  9.     })
  10.     .WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
  11.     {
  12.         var uri = ctx.Params?.Uri;
  13.         if (uri is not null)
  14.         {
  15.             subscriptions.Remove(uri);
  16.         }
  17.         return new EmptyResult();
  18.     });
复制代码
比方,我们可以一个接口,手动触发更新订阅了 "test://static/resource/README.txt 的 Client。
  1.         await _mcpServer.SendNotificationAsync("notifications/resource/updated",
  2.             new
  3.             {
  4.                 Uri = "test://static/resource/README.txt",
  5.             });
  6.         return "已通知";
复制代码
客户端只需要很简单的代码即可订阅。
  1. client.RegisterNotificationHandler("notifications/resource/updated", async (message, ctx) =>
  2. {
  3.     await Task.CompletedTask;
  4.    
  5.     // 回调
  6. });
  7. 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 示例:
  1. [McpServerPromptType]
  2. public static class MyPrompts
  3. {
  4.     [McpServerPrompt, Description("Creates a prompt to summarize the provided message.")]
  5.     public static ChatMessage Summarize([Description("The content to summarize")] string content) =>
  6.         new(ChatRole.User, $"Please summarize this content into a single sentence: {content}");
  7. }
复制代码
根据官方框架仓库的示例,Prompts 重要有两种利用方式。
第一种直接返回字符串。
  1. [McpServerPromptType]
  2. public class SimplePromptType
  3. {
  4.     [McpServerPrompt(Name = "simple_prompt"), Description("A prompt without arguments")]
  5.     public static string SimplePrompt() => "This is a simple prompt without arguments";
  6. }
复制代码
第二种则是编排对话上下文再返回。
  1. [McpServerPromptType]
  2. public class ComplexPromptType
  3. {
  4.     [McpServerPrompt(Name = "complex_prompt"), Description("A prompt with arguments")]
  5.     public static IEnumerable<ChatMessage> ComplexPrompt(
  6.         [Description("Temperature setting")] int temperature,
  7.         [Description("Output style")] string? style = null)
  8.     {
  9.         return [
  10.             new ChatMessage(ChatRole.User,$"This is a complex prompt with arguments: temperature={temperature}, style={style}"),
  11.             new ChatMessage(ChatRole.Assistant, "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"),
  12.             new ChatMessage(ChatRole.User, [new DataContent(Convert.ToBase64String(File.ReadAllBytes("img.png")))])
  13.         ];
  14.     }
  15. }
复制代码
Client 可以获取 MCP Server 提供的提示词列表。
  1. var prompts = await client.ListPromptsAsync();
  2. foreach (var item in prompts)
  3. {
  4.     Console.WriteLine($"prompt name :{item.Name}");
  5. }
复制代码
客户端可以通过利用需要的提示词,将其主动加载到当前 AI 对话上下文中。
  1. var result = await prompts.First(x => x.Name == "test").GetAsync(new Dictionary<string, object?>() { ["message"] = "hello" });
  2. 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。
  1. await ctx.Server.RequestSamplingAsync([
  2.         new ChatMessage(ChatRole.System, "You are a helpful test server"),
  3.         new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
  4. ],
复制代码
对于 http 方式实现的 MCP Server ,由于不能实现 Server 调用 Client ,因此这里不再赘述。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

没腿的鸟

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表