[MAF的工具-02]McpClientTool:MCP工具化桥接器
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)。我们调用McpClient的ListToolsAsync方法来获取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工具
我们说了,McpClient的ListToolsAsync方法返回的是一个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
更多推荐



所有评论(0)