AIFunction(参考上篇)通过绑定一个方法实现了在Agent本地执行工具,当时更多的情况下,我们会利用一个MCP服务来为一个或者多个Agent提供工具。这种情况下,我们就需要利用McpClientTool来远程调用MCP服务提供的工具。McpClientTool自身是一个AIFunction,它通过MCP协议来调用远程的MCP服务提供的工具。在正式介绍McpClientTool之前,我们先通过一个简单的例子来演示一下如何使用McpClientTool来调用远程的MCP服务提供的工具。

1. 搭建一个MCP服务

下面的程序利用FastMCP框架搭建了一个MCP服务,它提供了两个工具:

  • look_up_location:用于查询指定城市的地理位置;
  • get_weather:用于获取指定位置的实时天气信息。

我们调用的是和风天气提供的API,相应的URL和API Key等信息我们都放在了.env文件中

from fastmcp import FastMCP
from typing import Callable,Any
from requests import Response
from dotenv import load_dotenv
import requests,os

load_dotenv()
mcp = FastMCP("weather-forecast")

def invoke(url:str, params:dict, extract_result:Callable[[Response], Any]) -> Any:
    response = requests.get(
        headers={"X-QW-Api-Key" : os.getenv("QW_API_KEY")},
        url = url, 
        params=params)
    if response.status_code == 200:
        return extract_result(response)
    else:
        raise Exception(f"请求失败,状态码:{response.status_code}")

@mcp.tool()
def look_up_location(city: str) -> str:
    """查询指定城市的地理位置
    Args:
        city (str): 城市名称,例如 "北京" 或 "beijing"
    """

    return invoke(
        url=os.getenv("QW_LOCATION_LOOKUP_URL",""), 
        params={"location": city},
        extract_result=lambda response: response.json()["location"][0]["id"]
    )     

@mcp.tool()
def get_weather(location: str) -> dict:
    """获取指定位置的实时天气信息
    
    Args:
        location (str): 工具look_up_location返回的指定城市的地理位置
    """

    return invoke(
        url=os.getenv("QW_WEATHER_URL",""), 
        params={"location": location},
        extract_result=lambda response: response.json()["now"]
    )

mcp.run(transport="http", host="0.0.0.0", port=3721, stateless_http=True)

2. 使用McpClientTool调用MCP服务提供的工具

在如下的演示程序中,我们调用McpClient的静态方法CreateAsync来创建一个McpClient实例,它通过创建的HTTP传输协议连接上面我们搭建的MCP服务(http://localhost:3721/mcp)。我们调用McpClientListToolsAsync方法来获取MCP服务提供的工具列表,然后将这些工具注册到OpenAIClient中创建的Agent中。最后我们调用Agent的RunAsync方法来执行一个任务:根据目前北京天气提供一些着装建议

using DotNetEnv;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using OpenAI;
using System.ClientModel;

Env.Load();
var openaiUrl = Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
var model = Environment.GetEnvironmentVariable("MODEL")!;

var options = new HttpClientTransportOptions
{
    Endpoint = new Uri("http://localhost:3721/mcp"),
};
var mcpClient = await McpClient.CreateAsync(new HttpClientTransport(options));
var tools = await mcpClient.ListToolsAsync();

var openAIClient = new OpenAIClient(
    new ApiKeyCredential(apiKey), 
    new OpenAIClientOptions { Endpoint = new Uri(openaiUrl) });
var agent = openAIClient
    .GetChatClient(model)
    .AsIChatClient()
    .AsAIAgent(tools: [.. tools]);

var response = await agent.RunAsync("根据目前北京天气提供一些着装建议");
Console.WriteLine(response);

输出

北京目前天气为雾,气温24°C,体感约27°C,湿度很高(97%),能见度一般,北风较弱。

这种天气建议穿得轻薄、透气一些:

- 上衣适合短袖、薄衬衫、速干T恤等透气材质。
- 下装可以选择薄款长裤、休闲裤或短裤。
- 湿度高,体感会有些闷,尽量避免厚重或不透气的衣物。
- 早晚如果长时间待在空调房,可以备一件轻薄外套。
- 目前有雾、能见度较低,如果夜间外出或骑行,建议穿稍微亮色一点的衣服,更容易被看见。

另外,空气潮湿时鞋子容易闷,运动鞋或透气凉鞋会更舒服。

3. 查看MCP工具

我们说了,McpClientListToolsAsync方法返回的是一个McpClientTool列表,McpClientTool都是AIFunction的子类,下面的程序演示了如何查看MCP服务提供的工具的名称、描述和输入输出Schema。

using DotNetEnv;
using ModelContextProtocol.Client;
using System.Text.Json;

var options = new HttpClientTransportOptions
{
    Endpoint = new Uri("http://localhost:3721/mcp"),
};
var mcpClient = await McpClient.CreateAsync(new HttpClientTransport(options));
var tools = await mcpClient.ListToolsAsync();
var serializerOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

foreach (var tool in tools)
{
    Console.WriteLine($"""
        {new string('-',20)}{tool.Name}{new string('-', 20)}
        Description: 
        {tool.Description} 
        JsonSchema: 
        {JsonSerializer.Serialize(tool.JsonSchema, serializerOptions)}  
        ReturnJsonSchema: 
        {JsonSerializer.Serialize(tool.ReturnJsonSchema, serializerOptions)}  

        """);
}

输出

--------------------look_up_location--------------------
Description:
查询指定城市的地理位置
Args:
    city (str): 城市名称,例如 "北京" 或 "beijing"
JsonSchema:
{
  "additionalProperties": false,
  "properties": {
    "city": {
      "type": "string"
    }
  },
  "required": [
    "city"
  ],
  "type": "object"
}
ReturnJsonSchema:
{
  "properties": {
    "result": {
      "type": "string"
    }
  },
  "required": [
    "result"
  ],
  "type": "object",
  "x-fastmcp-wrap-result": true
}

--------------------get_weather--------------------
Description:
获取指定位置的实时天气信息
JsonSchema:
{
  "additionalProperties": false,
  "properties": {
    "location": {
      "type": "string",
      "description": "\u5DE5\u5177look_up_location\u8FD4\u56DE\u7684\u6307\u5B9A\u57CE\u5E02\u7684\u5730\u7406\u4F4D\u7F6E"
    }
  },
  "required": [
    "location"
  ],
  "type": "object"
}
ReturnJsonSchema:
{
  "additionalProperties": true,
  "type": "object"
}

从上述的输出可以看出,两个工具的名称、描述、JsonSchema和ReturnJsonSchema与我们在MCP服务端定义的完全一致。

4. McpClientTool

在我们的系列文章FastMCP设计、原理与应用-17虽然聚焦于FastMCP框架,同时也提供了针对MCP协议本身的详细介绍。MCP协议将工具、资源和提示词统一定义成原语(Primtive)。MCP协议在.NET平台通过ModelContextProtocol实现,工具原语通过如下这个Tool类型来表示:

public sealed class Tool : IBaseMetadata
{
	public required string Name { get; set; }
	public string? Title { get; set; }
	public string? Description { get; set; }
	public JsonElement InputSchema { get; set; }
	public JsonElement? OutputSchema { get; set; }
	public ToolAnnotations? Annotations { get; set; }
	public ToolExecution? Execution { get; set; }
	public IList<Icon>? Icons { get; set; }
	public JsonObject? Meta { get; set; }
}

用于描述工具的各个属性说明如下:

  • Name:工具的唯一标识符,必填,用于模型在调用时识别具体工具;
  • Title:可选的简短标题,通常用于 UI 展示或人类可读的工具名称;
  • Description:工具的文字说明,帮助模型和用户理解工具的用途;
  • InputSchema:工具输入参数的JSON Schema,定义调用时必须遵循的结构;
  • OutputSchema:描述工具返回结果的JSON Schema;
  • Annotations:工具的额外注解信息(如分类、标签),用于扩展元数据;
  • Execution:描述工具的执行方式,比如调用的端点或执行逻辑;
  • Icons:工具的图标列表;
  • Meta:任意扩展的元数据,开发者可自定义。

McpClientTool本质上是对上述这个Tool对象的封装,它重写的很多属性都来源于这个Tool对象。在CallAsync方法中,McpClientTool利用提供的McpClient实现了针对工具的远程调用,重写的InvokeCoreAsync方法则是调用CallAsync方法来实现的。

public sealed class McpClientTool : AIFunction
{
	public Tool ProtocolTool { get; }
	public override string Name { get; }
	public string? Title{ get; }
	public override string Description { get; }
	public override JsonElement JsonSchema { get; }
	public override JsonElement? ReturnJsonSchema { get; }
	public override JsonSerializerOptions JsonSerializerOptions { get; }

	public McpClientTool(McpClient client, Tool tool, JsonSerializerOptions? serializerOptions = null);

	protected override async ValueTask<object?> InvokeCoreAsync(
        AIFunctionArguments arguments, 
        CancellationToken cancellationToken);
	public ValueTask<CallToolResult> CallAsync(
        IReadOnlyDictionary<string, object?>? arguments = null, 
        IProgress<ProgressNotificationValue>? progress = null, 
        RequestOptions? options = null, 
        CancellationToken cancellationToken = default);    
    
	public McpClientTool WithName(string name);
	public McpClientTool WithDescription(string description);
	public McpClientTool WithProgress(IProgress<ProgressNotificationValue> progress);
	public McpClientTool WithMeta(JsonObject? meta);

McpClientTool还提供了四个With-方法来修改工具的名称、描述、进度通知和元数据,这些方法都会返回一个新的McpClientTool实例,原有的实例不会被修改。

5. 进度通知

CallAsync方法的定义可以看出,我们可以提供一个IProgress<ProgressNotificationValue>对象来接收工具调用的进度通知。IProgress<T>接口和ProgressNotificationValue类型的定义如下:

public interface IProgress<in T>
{
	void Report(T value);
}
public sealed class ProgressNotificationValue
{
	public required float Progress { get; init; }
	public float? Total { get; init; }
	public string? Message { get; init; }
}

为了演示如何使用IProgress<ProgressNotificationValue>来接收工具调用的进度通知,我们在搭建的MCP服务中增加了一个新的工具long_running_task,它模拟了一个耗时的任务,并在执行过程中通过MCP协议发送进度通知。下面是修改后的MCP服务代码:

import asyncio

from fastmcp import FastMCP
from typing import Callable,Any
from requests import Response
from dotenv import load_dotenv
from fastmcp.server import Context
import requests,os

load_dotenv()
mcp = FastMCP("weather-forecast")

@mcp.tool()
async def long_running_task(
    context: Context, 
) -> int:
    for i in range(1, 6):
        await context.report_progress(progress=i, total=5, message=f"Step {i} completed")
        await asyncio.sleep(1)
    return 5

在如下所示的客户端代码中,我们在获取MCP服务提供的工具列表后,找到名为long_running_task的工具,并调用它的CallAsync方法,同时传入一个Progress<ProgressNotificationValue>对象来接收进度通知。每当工具执行到一个步骤时,它会通过MCP协议发送进度通知,我们在客户端中打印出这些通知。

using ModelContextProtocol;
using ModelContextProtocol.Client;

var options = new HttpClientTransportOptions
{
    Endpoint = new Uri("http://localhost:3721/mcp"),
};
var mcpClient = await McpClient.CreateAsync(new HttpClientTransport(options));
var tools = await mcpClient.ListToolsAsync();
var longRunningTool = tools.Single(t => t.Name == "long_running_task");
var progress = new Progress<ProgressNotificationValue>(update =>
{
    Console.WriteLine($"{update.Message}: {update.Progress}/{update.Total}");
});
await longRunningTool.CallAsync(progress: progress);

输出:

Step 1 completed: 1/5
Step 2 completed: 2/5
Step 3 completed: 3/5
Step 4 completed: 4/5
Step 5 completed: 5/5
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐