创建使用LLM的MCP客户端
到目前为止,你已经了解了如何创建服务端和客户端。客户端能够显式地调用服务端来列出其工具、资源和提示词。然而,这种方法并不太实用。你的用户生活在智能代理时代,他们期望通过提示词与大语言模型(LLM)进行交互来完成任务。对用户而言,你是否使用 MCP 来存储功能并不重要,但他们确实期望能够使用自然语言进行交互。那么,我们该如何解决这个问题呢?解决方案就是在客户端中使用大语言模型(LLM)。
在本课中,我们将重点讲解如何为客户端引入 LLM,并展示这如何为用户提供更好的体验。
通过本课学习,您将能够:
- 创建一个使用 LLM 的客户端。
- 使用 LLM 无缝与 MCP 服务端交互。
- 在客户端提供更好的终端用户体验。
让我们尝试理解需要采取的方法。引入 LLM 听起来很简单,但我们实际上会怎么做呢?
以下是客户端与服务端交互的方式:
-
建立与服务端的连接。
-
列出功能、提示、资源和工具,并保存它们的模式。
-
引入 LLM,并以 LLM 能够理解的格式传递保存的功能及其模式。
-
通过将用户提示与客户端列出的工具一起传递给 LLM 来处理用户请求。
很好,现在我们已经从高层次上理解了如何实现这一点,让我们在下面的练习中尝试一下。
在本练习中,我们将学习如何为客户端引入 LLM。
创建 GitHub 令牌是一个简单的过程。以下是操作步骤:
- 前往 GitHub 设置 – 点击右上角的个人头像并选择“设置”。
- 导航到开发者设置 – 向下滚动并点击“开发者设置”。
- 选择个人访问令牌 – 点击“个人访问令牌”,然后选择“生成新令牌”。
- 配置您的令牌 – 添加备注以供参考,设置过期日期,并选择必要的范围(权限)。
- 生成并复制令牌 – 点击“生成令牌”,并确保立即复制,因为之后无法再次查看。
让我们先创建客户端:
.NET
using Azure;
using Azure.AI.Inference;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using System.Text.Json;
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "dotnet",
Arguments = ["run", "--project", @"F:\dht\SLM\Source\McpCalculatorServer\McpCalculatorServer.csproj"],
});
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
重要
在运行应用程序之前,请确保使用您的 GitHub 令牌设置
GITHUB_TOKEN
环境变量,并重启电脑,让环境变量设置生效。还要安装Nuget包AutoGen.AzureAIInference。
很好,接下来我们将列出服务端上的功能。
现在我们将连接到服务端并请求其功能:
.NET
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
// TODO: convert tool definition from MCP tool to LLm tool
}
return toolDefinitions;
}
在上述代码中,我们:
- 列出了 MCP 服务端上可用的工具。
- 对于每个工具,列出了名称、描述及其模式。后者是我们稍后调用工具时会用到的内容
列出服务端功能后,下一步是将其转换为 LLM 能够理解的格式。一旦完成,我们就可以将这些功能作为工具提供给 LLM。
- 添加代码将 MCP 工具响应转换为 LLM 能够理解的格式:
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement) { // convert the tool to a function definition FunctionDefinition functionDefinition = new FunctionDefinition(name) { Description = description, Parameters = BinaryData.FromObjectAsJson(new { Type = "object", Properties = jsonElement }, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) }; // create a tool definition ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsFunctionToolDefinition(functionDefinition); return toolDefinition; }
在上述代码中,我们:
- 创建了一个名为
ConvertFrom
的函数,它接收名称(name)、描述(description)和输入模式(input schema)作为参数。 - 定义了创建
FunctionDefinition
的功能,该定义将被传递给ChatCompletionsDefinition
。后者是大语言模型(LLM)能够理解的格式。
- 创建了一个名为
- 更新现有代码以利用上述函数:
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools() { Console.WriteLine("Listing tools"); var tools = await mcpClient.ListToolsAsync(); List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>(); foreach(var tool in tools) { Console.WriteLine($"Connected to server with tools:{tool.Name}"); Console.WriteLine($"Tool description:{tool.Description}"); Console.WriteLine($"Tool parameters:{tool.JsonSchema}"); JsonElement propertiesElement; tool.JsonSchema.TryGetProperty("properties", out propertiesElement); var def = ConvertFrom(tool.Name, tool.Description, propertiesElement); Console.WriteLine($"Tool definition:{def}"); toolDefinitions.Add(def); Console.WriteLine($"Properties:{propertiesElement}"); } return toolDefinitions; }
在上述代码中,我们:
-
更新了函数以将 MCP 工具响应转换为 LLM 工具。以下是我们添加的代码:
输入模式是工具响应的一部分,但位于 "properties" 属性中,因此我们需要提取它。此外,我们现在使用工具详细信息调用JsonElement propertiesElement; tool.JsonSchema.TryGetProperty("properties", out propertiesElement); var def = ConvertFrom(tool.Name, tool.Description, propertiesElement); Console.WriteLine($"Tool definition: {def}"); toolDefinitions.Add(def);
ConvertFrom
。完成了这些准备工作后,让我们看看如何处理用户提示。
在这部分代码中,我们将处理用户请求。
-
显示一些用于 LLM 提示请求的代码:
var endpoint = "https://models.inference.ai.azure.com"; var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); var client = new ChatCompletionsClient(new Uri(endpoint), new AzureKeyCredential(token)); var chatHistory = new List<ChatRequestMessage> { new ChatRequestSystemMessage("You are a helpful assistant that knows about AI") }; // 1. List tools on mcp server var tools = await GetMcpTools(); for(int i = 0; i < tools.Count; i++) { var tool = tools[i]; Console.WriteLine($"MCP Tools def: {i}: {tool}"); } // 2. Define the chat history and the user message var userMessage = "add 2 and 4"; chatHistory.Add(new ChatRequestUserMessage(userMessage)); // 3. Define options, including the tools var options = new ChatCompletionsOptions(chatHistory) { Model = "gpt-4o-mini", Tools = { tools[0] } }; // 4. Call the model var response = await client.CompleteAsync(options); var content = response.GetRawResponse().Content; Console.WriteLine(content);
在上述代码中,我们:
-
- 从 MCP 服务端获取工具,
var tools = await GetMcpTools()
。 - 定义了一个用户提示
userMessage
。 - 构造了一个指定模型和工具的选项对象。
- 向 LLM 发出了请求。
- 从 MCP 服务端获取工具,
-
最后一步,查看 LLM 是否认为应该调用某个函数:
// 5. Check if the response contains a function call int j = 0; foreach(var call in response.Value.Choices[0].Message.ToolCalls) { var toolCall = call as ChatCompletionsFunctionToolCall; Console.WriteLine($"Tool call {j++}: {toolCall.Name} with arguments {toolCall.Arguments}"); var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(toolCall.Arguments); var result = await mcpClient.CallToolAsync(toolCall.Name, dict!, cancellationToken: CancellationToken.None); var contentBlock = result.Content.First(c => c.Type == "text") as TextContentBlock; Console.WriteLine(contentBlock.Text); }
在上述代码中,我们:
- 遍历了一组函数调用列表。
- 对于每个工具调用,解析出函数名称和参数,然后通过 MCP 客户端在 MCP 服务端上调用相应的工具。最后,我们打印出结果。
以下是完整的代码:
using Azure;
using Azure.AI.Inference;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using System.Text.Json;
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "dotnet",
Arguments = ["run", "--project", @"F:\dht\SLM\Source\McpCalculatorServer\McpCalculatorServer.csproj"],
});
Console.WriteLine("Setting up stdio transport");
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach(var tool in tools)
{
Console.WriteLine($"Connected to server with tools:{tool.Name}");
Console.WriteLine($"Tool description:{tool.Description}");
Console.WriteLine($"Tool parameters:{tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition:{def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties:{propertiesElement}");
}
return toolDefinitions;
}
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsFunctionToolDefinition(functionDefinition);
return toolDefinition;
}
var endpoint = "https://models.inference.ai.azure.com";
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
var client = new ChatCompletionsClient(new Uri(endpoint), new AzureKeyCredential(token));
var chatHistory = new List<ChatRequestMessage> { new ChatRequestSystemMessage("You are a helpful assistant that knows about AI") };
// 1. List tools on mcp server
var tools = await GetMcpTools();
for(int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 2. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 3. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4o-mini",
Tools = { tools[0] }
};
// 4. Call the model
var response = await client.CompleteAsync(options);
var content = response.GetRawResponse().Content;
Console.WriteLine(content);
// 5. Check if the response contains a function call
int j = 0;
foreach(var call in response.Value.Choices[0].Message.ToolCalls)
{
var toolCall = call as ChatCompletionsFunctionToolCall;
Console.WriteLine($"Tool call {j++}: {toolCall.Name} with arguments {toolCall.Arguments}");
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(toolCall.Arguments);
var result = await mcpClient.CallToolAsync(toolCall.Name, dict!, cancellationToken: CancellationToken.None);
var contentBlock = result.Content.First(c => c.Type == "text") as TextContentBlock;
Console.WriteLine(contentBlock.Text);
}
- 在客户端中添加 LLM 为用户与 MCP 服务端的交互提供了更好的方式。
- 需要将 MCP 服务端的响应转换为 LLM 可以理解的内容。
