创建使用LLM的MCP客户端

到目前为止,你已经了解了如何创建服务端和客户端。客户端能够显式地调用服务端来列出其工具、资源和提示词。然而,这种方法并不太实用。你的用户生活在智能代理时代,他们期望通过提示词与大语言模型(LLM)进行交互来完成任务。对用户而言,你是否使用 MCP 来存储功能并不重要,但他们确实期望能够使用自然语言进行交互。那么,我们该如何解决这个问题呢?解决方案就是在客户端中使用大语言模型(LLM)。

概述

在本课中,我们将重点讲解如何为客户端引入 LLM,并展示这如何为用户提供更好的体验。

学习目标

通过本课学习,您将能够:

  • 创建一个使用 LLM 的客户端。
  • 使用 LLM 无缝与 MCP 服务端交互。
  • 在客户端提供更好的终端用户体验。

方法

让我们尝试理解需要采取的方法。引入 LLM 听起来很简单,但我们实际上会怎么做呢?

以下是客户端与服务端交互的方式:

  1. 建立与服务端的连接。

  2. 列出功能、提示、资源和工具,并保存它们的模式。

  3. 引入 LLM,并以 LLM 能够理解的格式传递保存的功能及其模式。

  4. 通过将用户提示与客户端列出的工具一起传递给 LLM 来处理用户请求。

很好,现在我们已经从高层次上理解了如何实现这一点,让我们在下面的练习中尝试一下。

练习:创建一个使用 LLM 的客户端

在本练习中,我们将学习如何为客户端引入 LLM。

使用 GitHub 个人访问令牌进行身份验证

创建 GitHub 令牌是一个简单的过程。以下是操作步骤:

  • 前往 GitHub 设置 – 点击右上角的个人头像并选择“设置”。
  • 导航到开发者设置 – 向下滚动并点击“开发者设置”。
  • 选择个人访问令牌 – 点击“个人访问令牌”,然后选择“生成新令牌”。
  • 配置您的令牌 – 添加备注以供参考,设置过期日期,并选择必要的范围(权限)。
  • 生成并复制令牌 – 点击“生成令牌”,并确保立即复制,因为之后无法再次查看。

-1- 连接到服务端

让我们先创建客户端:

.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。

很好,接下来我们将列出服务端上的功能。

-2- 列出服务端功能

现在我们将连接到服务端并请求其功能:

.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 服务端上可用的工具。
  • 对于每个工具,列出了名称、描述及其模式。后者是我们稍后调用工具时会用到的内容

-3- 将服务端功能转换为 LLM 工具

列出服务端功能后,下一步是将其转换为 LLM 能够理解的格式。一旦完成,我们就可以将这些功能作为工具提供给 LLM。

.NET

  1. 添加代码将 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)能够理解的格式。
  2. 更新现有代码以利用上述函数:
    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 工具。以下是我们添加的代码:

    JsonElement propertiesElement;
    tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
    
    var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
    Console.WriteLine($"Tool definition: {def}");
    toolDefinitions.Add(def);
    输入模式是工具响应的一部分,但位于 "properties" 属性中,因此我们需要提取它。此外,我们现在使用工具详细信息调用 ConvertFrom。完成了这些准备工作后,让我们看看如何处理用户提示。

-4- 处理用户提示请求

在这部分代码中,我们将处理用户请求。

.NET

  1. 显示一些用于 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 发出了请求。
  1. 最后一步,查看 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 可以理解的内容。
posted @ 2025-09-15 10:10  菜鸟吊思  阅读(9)  评论(0)    收藏  举报