简单介绍
在进入教程之前我们可以先对MCP进行一个大概的了解,MCP是什么?为什么要用MCP?MCP和Function Calling有什么区别?
MCP是什么?
Model Context Protocol (MCP) 是一个开放协议,它使 LLM 应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。
- MCP Hosts: 想要通过MCP访问数据的程序,如Claude Desktop、ide或AI工具
- MCP Clients: 与服务器保持1:1连接的协议客户端
- MCP Servers: 轻量级程序,每个程序都通过标准化的模型上下文协议公开特定的功能
- Local Data Sources: MCP服务器可以安全访问的计算机文件、数据库和服务
- Remote Services: MCP服务器可以连接的internet上可用的外部系统(例如通过api)
为什么要用MCP?
- 扩展LLM的能力:让模型能够访问最新数据、专有信息和本地资源
- 数据私密性和安全性:数据可以保留在本地或受控环境中,减少敏感信息共享的风险
- 工具集成:使LLM能够调用和控制外部工具,扩展其功能范围
- 减少幻觉:通过提供准确的上下文信息,降低模型生成虚假内容的可能性
- 标准化:为开发者提供一致的接口,简化集成过程
- 可扩展性:支持从简单用例到复杂企业级应用的多种场景
- 跨语言的能力:通过标准的MCP协议我们可以使用不同语言提供的MCPServer的能力
MCP和Function Calling有什么区别?
我听过最多人问的就是MCP和Function Calling有什么区别?
特性MCPFunction Calling基础关系基于Function Calling构建并扩展作为MCP的基础技术设计范围更广泛的协议,处理上下文获取和工具使用主要聚焦于调用特定函数架构客户端-服务器架构,支持分布式系统通常是API参数的直接定义和调用数据处理可以处理大型数据集和复杂上下文主要处理结构化的函数参数和返回值上下文管理专门设计用于管理和提供上下文上下文管理不是核心功能标准化程度开放协议,旨在跨不同系统和模型标准化实现方式因平台而异,标准化程度较低应用场景适用于需要复杂上下文和工具集成的场景适用于调用预定义函数执行特定任务简而言之,MCP(Model Context Protocol)是在Function Calling基础上发展而来的更全面的协议,它不仅保留了Function Calling的核心功能,还扩展了上下文获取和管理能力,为LLM提供更丰富、更标准化的环境交互能力。
开始MCPClient基础教程
前提条件,您需要先将上一个教程的MCPServer跑起来一会才能进行当前步骤,因为MCPClient是需要MCPServer提供Tools,下面我默认您已经运行了MCPServer,
然后可以访问 https://account.coreshub.cn/signup?invite=ZmpMQlZxYVU= 获取免费的DeepSeek-V3模型和白嫖GPU服务器,通过上面连接注册以后访问
https://console.coreshub.cn/xb3/maas/global-keys/ 地址获取到APIKey
然后记住我们的APIKey,开始下面的旅途
创建一个McpClient的控制台项目
给项目添加依赖包- <ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
- <PackageReference Include="Microsoft.SemanticKernel" Version="1.44.0" />
- <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.4" />
- </ItemGroup>
复制代码 下面的教程我们将使用Microsoft.SemanticKernel开发,然后依赖ModelContextProtocol官方的包,然后我们需要用到Microsoft.Extensions.Hosting
开始我们的代码开发
- 扩展SemanticKernel的MCPClient
由于SemnaticKernel默认是不能直接使用MCP的功能,那么我们需要先对它进行扩展
创建几个基础类
JsonSchema.cs- /// <summary>
- /// Represents a JSON schema for a tool's input arguments.
- /// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
- /// </summary>
- internal class JsonSchema
- {
- /// <summary>
- /// The type of the schema, should be "object".
- /// </summary>
- [JsonPropertyName("type")]
- public string Type { get; set; } = "object";
- /// <summary>
- /// Map of property names to property definitions.
- /// </summary>
- [JsonPropertyName("properties")]
- public Dictionary<string, JsonSchemaProperty>? Properties { get; set; }
- /// <summary>
- /// List of required property names.
- /// </summary>
- [JsonPropertyName("required")]
- public List<string>? Required { get; set; }
- }
- JsonSchemaProperty.cs
- /// <summary>
- /// Represents a property in a JSON schema.
- /// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
- /// </summary>
- internal class JsonSchemaProperty
- {
- /// <summary>
- /// The type of the property. Should be a JSON Schema type and is required.
- /// </summary>
- [JsonPropertyName("type")]
- public string Type { get; set; } = string.Empty;
- /// <summary>
- /// A human-readable description of the property.
- /// </summary>
- [JsonPropertyName("description")]
- public string? Description { get; set; } = string.Empty;
- }
复制代码 接下来创建MCP的扩展类,用于将MCPFunction进行转换成SemanticKernel Function
ModelContextProtocolExtensions.cs- /// <summary>
- /// Extension methods for ModelContextProtocol
- /// </summary>
- internal static class ModelContextProtocolExtensions
- {
- /// <summary>
- /// Map the tools exposed on this <see cref="IMcpClient"/> to a collection of <see cref="KernelFunction"/> instances for use with the Semantic Kernel.
- /// <param name="mcpClient">The <see cref="IMcpClient"/>.</param>
- /// <param name="cancellationToken">The optional <see cref="CancellationToken"/>.</param>
- /// </summary>
- internal static async Task<IReadOnlyList<KernelFunction>> MapToFunctionsAsync(this IMcpClient mcpClient, CancellationToken cancellationToken = default)
- {
- var functions = new List<KernelFunction>();
- foreach (var tool in await mcpClient.ListToolsAsync(cancellationToken).ConfigureAwait(false))
- {
- functions.Add(tool.ToKernelFunction(mcpClient, cancellationToken));
- }
- return functions;
- }
- private static KernelFunction ToKernelFunction(this McpClientTool tool, IMcpClient mcpClient, CancellationToken cancellationToken)
- {
- async Task<string> InvokeToolAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken ct)
- {
- try
- {
- // Convert arguments to dictionary format expected by ModelContextProtocol
- Dictionary<string, object?> mcpArguments = [];
- foreach (var arg in arguments)
- {
- if (arg.Value is not null)
- {
- mcpArguments[arg.Key] = function.ToArgumentValue(arg.Key, arg.Value);
- }
- }
- // Call the tool through ModelContextProtocol
- var result = await mcpClient.CallToolAsync(
- tool.Name,
- mcpArguments.AsReadOnly(),
- cancellationToken: ct
- ).ConfigureAwait(false);
- // Extract the text content from the result
- return string.Join("\n", result.Content
- .Where(c => c.Type == "text")
- .Select(c => c.Text));
- }
- catch (Exception ex)
- {
- await Console.Error.WriteLineAsync($"Error invoking tool '{tool.Name}': {ex.Message}");
- // Rethrowing to allow the kernel to handle the exception
- throw;
- }
- }
- return KernelFunctionFactory.CreateFromMethod(
- method: InvokeToolAsync,
- functionName: tool.Name,
- description: tool.Description,
- parameters: tool.ToParameters(),
- returnParameter: ToReturnParameter()
- );
- }
- private static object ToArgumentValue(this KernelFunction function, string name, object value)
- {
- var parameterType = function.Metadata.Parameters.FirstOrDefault(p => p.Name == name)?.ParameterType;
- if (parameterType == null)
- {
- return value;
- }
- if (Nullable.GetUnderlyingType(parameterType) == typeof(int))
- {
- return Convert.ToInt32(value);
- }
- if (Nullable.GetUnderlyingType(parameterType) == typeof(double))
- {
- return Convert.ToDouble(value);
- }
- if (Nullable.GetUnderlyingType(parameterType) == typeof(bool))
- {
- return Convert.ToBoolean(value);
- }
- if (parameterType == typeof(List<string>))
- {
- return (value as IEnumerable<object>)?.ToList() ?? value;
- }
- if (parameterType == typeof(Dictionary<string, object>))
- {
- return (value as Dictionary<string, object>)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? value;
- }
- return value;
- }
- private static List<KernelParameterMetadata>? ToParameters(this McpClientTool tool)
- {
- var inputSchema = JsonSerializer.Deserialize<JsonSchema>(tool.JsonSchema.GetRawText());
- var properties = inputSchema?.Properties;
- if (properties == null)
- {
- return null;
- }
- HashSet<string> requiredProperties = [.. inputSchema!.Required ?? []];
- return properties.Select(kvp =>
- new KernelParameterMetadata(kvp.Key)
- {
- Description = kvp.Value.Description,
- ParameterType = ConvertParameterDataType(kvp.Value, requiredProperties.Contains(kvp.Key)),
- IsRequired = requiredProperties.Contains(kvp.Key)
- }).ToList();
- }
- private static KernelReturnParameterMetadata ToReturnParameter()
- {
- return new KernelReturnParameterMetadata
- {
- ParameterType = typeof(string),
- };
- }
- private static Type ConvertParameterDataType(JsonSchemaProperty property, bool required)
- {
- var type = property.Type switch
- {
- "string" => typeof(string),
- "integer" => typeof(int),
- "number" => typeof(double),
- "boolean" => typeof(bool),
- "array" => typeof(List<string>),
- "object" => typeof(Dictionary<string, object>),
- _ => typeof(object)
- };
- return !required && type.IsValueType ? typeof(Nullable<>).MakeGenericType(type) : type;
- }
- }
复制代码 然后创建SemanticKernel的扩展,对SKFunction进行扩展
KernelExtensions.cs- /// <summary>
- /// Extension methods for KernelPlugin
- /// </summary>
- public static class KernelExtensions
- {
- private static readonly ConcurrentDictionary<string, IKernelBuilderPlugins> SseMap = new();
- /// <summary>
- /// Creates a Model Content Protocol plugin from an SSE server that contains the specified MCP functions and adds it into the plugin collection.
- /// </summary>
- /// <param name="endpoint"></param>
- /// <param name="serverName"></param>
- /// <param name="cancellationToken">The optional <see cref="CancellationToken"/>.</param>
- /// <param name="plugins"></param>
- /// <returns>A <see cref="KernelPlugin"/> containing the functions.</returns>
- public static async Task<IKernelBuilderPlugins> AddMcpFunctionsFromSseServerAsync(this IKernelBuilderPlugins plugins,
- string endpoint, string serverName, CancellationToken cancellationToken = default)
- {
- var key = ToSafePluginName(serverName);
- if (SseMap.TryGetValue(key, out var sseKernelPlugin))
- {
- return sseKernelPlugin;
- }
- var mcpClient = await GetClientAsync(serverName, endpoint, null, null, cancellationToken).ConfigureAwait(false);
- var functions = await mcpClient.MapToFunctionsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
- cancellationToken.Register(() => mcpClient.DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult());
- sseKernelPlugin = plugins.AddFromFunctions(key, functions);
- return SseMap[key] = sseKernelPlugin;
- }
- private static async Task<IMcpClient> GetClientAsync(string serverName, string? endpoint,
- Dictionary<string, string>? transportOptions, ILoggerFactory? loggerFactory,
- CancellationToken cancellationToken)
- {
- var transportType = !string.IsNullOrEmpty(endpoint) ? TransportTypes.Sse : TransportTypes.StdIo;
- McpClientOptions options = new()
- {
- ClientInfo = new()
- {
- Name = $"{serverName} {transportType}Client",
- Version = "1.0.0"
- }
- };
- var config = new McpServerConfig
- {
- Id = serverName.ToLowerInvariant(),
- Name = serverName,
- Location = endpoint,
- TransportType = transportType,
- TransportOptions = transportOptions
- };
- return await McpClientFactory.CreateAsync(config, options,
- loggerFactory: loggerFactory ?? NullLoggerFactory.Instance, cancellationToken: cancellationToken);
- }
- // A plugin name can contain only ASCII letters, digits, and underscores.
- private static string ToSafePluginName(string serverName)
- {
- return Regex.Replace(serverName, @"[^\w]", "_");
- }
- }
复制代码 现在我们就可以实现核心的代码了,打开我们的Program.cs- using McpClient;
- using Microsoft.SemanticKernel;
- using Microsoft.SemanticKernel.ChatCompletion;
- using Microsoft.SemanticKernel.Connectors.OpenAI;
- using ChatMessageContent = Microsoft.SemanticKernel.ChatMessageContent;
- #pragma warning disable SKEXP0010
- var builder = Host.CreateEmptyApplicationBuilder(settings: null);
- builder.Configuration
- .AddEnvironmentVariables()
- .AddUserSecrets<Program>();
- var kernelBuilder = builder.Services.AddKernel()
- .AddOpenAIChatCompletion("DeepSeek-V3", new Uri("https://openapi.coreshub.cn/v1"), "您使用的https://openapi.coreshub.cn/v1平台的APIKey");
- await kernelBuilder.Plugins.AddMcpFunctionsFromSseServerAsync("http://您的MCPServerip:端口/sse", "token");
- Console.ForegroundColor = ConsoleColor.Green;
- Console.WriteLine("MCP Client Started!");
- Console.ResetColor();
- var app = builder.Build();
- var kernel = app.Services.GetService<Kernel>();
- var chatCompletion = app.Services.GetService<IChatCompletionService>();
- PromptForInput();
- while (Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
- {
- if (string.IsNullOrWhiteSpace(query))
- {
- PromptForInput();
- continue;
- }
- var history = new ChatHistory
- {
- new ChatMessageContent(AuthorRole.System, "下面如果需要计算俩个数的和,请使用我提供的工具。"),
- new ChatMessageContent(AuthorRole.User, query)
- };
- await foreach (var message in chatCompletion?.GetStreamingChatMessageContentsAsync(history,
- new OpenAIPromptExecutionSettings()
- {
- ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
- }, kernel))
- {
- Console.Write(message.Content);
- }
- Console.WriteLine();
- PromptForInput();
- }
- static void PromptForInput()
- {
- Console.WriteLine("Enter a command (or 'exit' to quit):");
- Console.ForegroundColor = ConsoleColor.Cyan;
- Console.Write("> ");
- Console.ResetColor();
- }
复制代码 然后开始启动我们的MCPClient,在此之前需要先修改代码中的参数改成自己的,然后先启动MCPServer以后,在启动我们的MCPClient
执行以后提问1+1=?然后会得到一下的结果
在执行之前我们可以先在MCPServer中的算法函数Debug,这样就可以清晰的看到进入的流程了
通过上面的案例,恭喜您您已经熟练的掌握了MCPServer+MCPClient+SemanticKernel的入门级教程
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |