ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

[MAF的工具-02]McpClientTool:把 MCP 工具桥接进 AIFunction 的配置与验证

[MAF的工具-02]McpClientTool:把 MCP 工具桥接进 AIFunction 的配置与验证 1. 为什么你的 Agent 调不动远程工具McpClientTool 桥接场景拆解如果你已经用 AIFunction 在本地跑通过工具调用接下来大概率会撞上同一堵墙工具逻辑不在本机而是跑在另一个进程、另一台机器上的 MCP 服务里。这时候你手里只有一个 MCP 服务地址Agent 却完全不知道那边有哪些工具、参数长什么样、返回什么结构。McpClientTool 就是解决这个断层的东西——它是 MCP 工具化桥接器把远程 MCP 服务暴露的工具包装成 AIFunction让 Agent 的调用链完全不用改。先说清楚它是什么。McpClientTool 继承自 AIFunction也就是说对 Agent 而言它和你在本地写的那个绑定方法的 AIFunction 没有区别。区别在内部本地 AIFunction 的 InvokeCoreAsync 直接执行你的 C# 方法而 McpClientTool 的 InvokeCoreAsync 会走 MCP 协议把参数序列化后发给远程 MCP 服务等对方执行完再把结果反序列化回来。整个过程对 Agent 透明。它能做什么三件事最关键。第一自动发现工具。你不需要手写每个工具的 schemaMcpClient 的 ListToolsAsync 会把远程服务的工具列表拉回来每个工具就是一个 McpClientTool名称、描述、输入输出 JSON Schema 全部带回来。第二无缝接入调用链。这些 McpClientTool 直接塞进 Agent 的 tools 参数模型看到的就是标准 AIFunction 列表该调哪个调哪个。第三支持进度通知和元数据改写。长任务可以通过 IProgress 接收进度工具名和描述还能用 WithName、WithDescription 临时改掉适配不同 Agent 的语义。适合谁适合已经在用 MAFMicrosoft Agent Framework或 Microsoft.Extensions.AI 搭 Agent、并且工具逻辑需要独立部署的人。典型场景是天气查询、地理位置解析、数据库查询、内部 API 封装这类工具你希望它们作为独立 MCP 服务跑着多个 Agent 共享而不是每个 Agent 项目里复制一份 C# 代码。这种时候 McpClientTool 就是那座桥。我试过的坑是一开始以为 ListToolsAsync 返回的是普通对象直接当字典用结果发现每个元素都是 AIFunction 子类得按 AIFunction 的方式注册。这个认知差是后面所有配置的基础。2. TaoToken 前置给桥接链路准备一个稳定的模型出口McpClientTool 解决的是工具侧的问题但整条链路要跑通模型侧也得有个能用的出口。Agent 的 RunAsync 最终要调模型模型要能理解工具 schema 并决定调哪个工具。这一步如果模型接口不稳定你会误以为是 McpClientTool 桥接失败其实是模型根本没返回 tool_calls。TaoToken 在这里的角色是提供一个兼容 OpenAI 接口的模型访问入口。它的 Base URL 是https://taotoken.net/api你拿到的 API Key 直接填进 OpenAIClient 就行。为什么要在讲 McpClientTool 之前先提这个因为后面验证桥接是否生效时你需要一个确定的模型来产生工具调用决策。如果模型侧配置错了报错信息会混在一起排查成本翻倍。具体要准备三样东西。第一API Key。去https://taotoken.net/api-keys创建一个注意这个 Key 只在创建时完整显示一次复制好。第二Base URL。就是上面那个https://taotoken.net/api注意不要多加/v1之类的后缀OpenAIClient 的 Endpoint 配置方式不同后面代码里会写清楚。第三Model ID。这个取决于你想用哪个模型在模型对话页面能看到可用列表选一个支持 function calling 的。https://taotoken.net/models这个入口可以快速试模型对话确认模型能正常响应。这里有个容易踩的点OpenAIClient 的 Endpoint 和很多 SDK 的 base_url 语义不一样。在 .NET 的 OpenAI SDK 里OpenAIClientOptions.Endpoint要的是完整的基础地址SDK 会自己在后面拼/chat/completions这类路径。所以如果你填了https://taotoken.net/api/v1实际请求可能变成https://taotoken.net/api/v1/chat/completions而正确的应该是https://taotoken.net/api/chat/completions。这个差异在 401 或 404 报错时特别容易混淆。另外如果你打算长期跑编码类 Agent或者工具调用频率很高可以了解一下 Coding Plan它在持续调用场景下更省心。入口在https://taotoken.net/coding-plan。但如果你只是先验证 McpClientTool 桥接用按量计费的 API Key 就够了不用一上来就上套餐。把模型出口准备好之后整条链路就是Agent 收到任务 → 模型决定调哪个工具 → McpClientTool 把调用转发给 MCP 服务 → MCP 服务执行 → 结果回传 → 模型生成最终回答。McpClientTool 负责的是中间那段转发但两端的配置都得对。3. 可复制配置MCP 服务端连接参数与 AIFunction 注册片段这一节直接给能跑的配置。分三块MCP 服务端怎么起、客户端连接参数怎么写、McpClientTool 怎么注册进 Agent。先看 MCP 服务端。用 FastMCP 起一个带两个工具的 HTTP 服务端口 3721传输方式用 streamable HTTP。服务端代码里工具定义和普通 FastMCP 没区别关键是mcp.run的参数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)}, urlurl, paramsparams ) 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( urlos.getenv(QW_LOCATION_LOOKUP_URL, ), params{location: city}, extract_resultlambda response: response.json()[location][0][id] ) mcp.tool() def get_weather(location: str) - dict: 获取指定位置的实时天气信息 Args: location (str): 工具 look_up_location 返回的指定城市的地理位置 return invoke( urlos.getenv(QW_WEATHER_URL, ), params{location: location}, extract_resultlambda response: response.json()[now] ) mcp.run(transporthttp, host0.0.0.0, port3721, stateless_httpTrue)注意stateless_httpTrue这个参数。它让服务端不维护会话状态每次请求独立处理。对于工具调用场景这通常够用而且省去了会话管理的复杂度。如果你的工具需要跨调用保持状态才需要关掉它。客户端连接参数。核心是HttpClientTransportOptionsEndpoint 指向 MCP 服务的/mcp路径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();这里tools的类型是IListMcpClientTool每个元素都是 AIFunction 子类。你可以直接把它展开注册进 Agentusing 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);这段代码里三件套齐全Base URL 是openaiUrl从环境变量读值应该是https://taotoken.net/apiKey 是apiKeyModel ID 是model。MCP 服务端的连接参数是http://localhost:3721/mcp。两边都配好桥接才能通。如果你用settings.json或appsettings.json管理配置可以写成这样{ OpenAI: { BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key, Model: gpt-4o-mini }, McpServer: { Endpoint: http://localhost:3721/mcp } }注意 BaseUrl 不要带/v1原因前面说过。McpServer 的 Endpoint 要带/mcp这是 FastMCP 的默认路径。4. 验证请求一次工具调用往返的完整动作与预期输出配置写完不算完得验证桥接真的生效了。验证分两步先确认工具列表拉回来了再确认 Agent 能通过 McpClientTool 调通远程工具。第一步查看 MCP 工具。写个小程序把 ListToolsAsync 的结果打印出来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和get_weather两个工具每个都带 Description、JsonSchema 和 ReturnJsonSchema。JsonSchema 里city是 required 的 stringReturnJsonSchema 里result是 string。这些和你在 MCP 服务端定义的一致说明工具发现这步通了。第二步跑一次完整调用。用前面那段 Agent 代码任务写「根据目前北京天气提供一些着装建议」。预期输出类似北京目前天气为雾气温24°C体感约27°C湿度很高97%能见度一般北风较弱。 这种天气建议穿得轻薄、透气一些 - 上衣适合短袖、薄衬衫、速干T恤等透气材质。 - 下装可以选择薄款长裤、休闲裤或短裤。 - 湿度高体感会有些闷尽量避免厚重或不透气的衣物。 - 早晚如果长时间待在空调房可以备一件轻薄外套。 - 目前有雾、能见度较低如果夜间外出或骑行建议穿稍微亮色一点的衣服更容易被看见。 另外空气潮湿时鞋子容易闷运动鞋或透气凉鞋会更舒服。看到这个输出说明整条链路通了模型决定调look_up_location拿北京的位置 ID再调get_weather拿天气最后生成建议。McpClientTool 在中间完成了两次远程调用转发。如果你想更直观地确认工具调用发生了可以在 MCP 服务端加日志或者在客户端用WithProgress接收进度。对于长任务进度通知是验证桥接的另一个信号var longRunningTool tools.Single(t t.Name long_running_task); var progress new ProgressProgressNotificationValue(update { Console.WriteLine(${update.Message}: {update.Progress}/{update.Total}); }); await longRunningTool.CallAsync(progress: progress);预期输出是逐步打印Step 1 completed: 1/5到Step 5 completed: 5/5。这说明 McpClientTool 的 CallAsync 不仅转发了调用还正确接收了服务端通过 MCP 协议发回的进度通知。验证通过的标准很简单工具列表能拉到、Agent 能调通、结果符合预期。三个都满足桥接就是生效的。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错对照桥接不生效时报错信息往往指向不同层。这一节按真实报错对照排查。401 Unauthorized。这个最常见但来源可能有两个。如果报错信息里提到 OpenAI 或 chat completions那是模型侧的 Key 问题。检查OPENAI_API_KEY环境变量是否设置、是否有多余空格、Key 是否过期。如果报错提到 MCP 或 tool call那是 MCP 服务端的认证问题。FastMCP 默认不启用认证但如果你加了 auth 中间件客户端连接时就得带 token。HttpClientTransportOptions 里可以配 AdditionalHeaders 传认证头。local proxy failed。这个报错通常出现在客户端连不上 MCP 服务端时。先确认 MCP 服务真的在跑curl http://localhost:3721/mcp看有没有响应。如果服务在 Docker 里localhost 可能不通得用容器网络地址。另外检查端口有没有被占用3721 被占的话换个端口两边同步改。reading choices 相关报错。这个一般出现在模型返回结构不符合预期时。比如模型返回了 tool_calls 但格式不对或者模型根本不支持 function calling。排查方法先用模型对话页面单独测一下这个模型能不能正常返回 tool_calls。如果模型不支持换一个支持 function calling 的 Model ID。另外确认 Base URL 没写错https://taotoken.net/api后面不要加/v1加了会导致请求路径错误返回的可能是 HTML 而不是 JSON解析时就报 reading choices 的错。OAuth 报错。如果你在 MCP 服务端启用了 OAuth客户端连接时会走授权流程。报错可能是 token 过期、scope 不对、或者回调地址不匹配。排查时先看服务端的 OAuth 配置确认 client_id、client_secret、授权端点都对。客户端这边HttpClientTransportOptions 需要配置对应的认证方式。如果只是本地验证建议先关掉 OAuth用无认证模式跑通桥接再逐步加认证。工具列表为空。ListToolsAsync 返回空列表但服务端明明定义了工具。检查 MCP 服务端的 transport 配置stateless_httpTrue在某些 FastMCP 版本下需要配合正确的路径。另外确认客户端 Endpoint 是http://localhost:3721/mcp而不是http://localhost:3721少了/mcp路径会连到根路径拿不到工具列表。调用超时。McpClientTool 的 CallAsync 默认超时可能不够长任务用。可以在 RequestOptions 里设置更长的超时或者在 HttpClientTransportOptions 里配置 HttpClient 的超时。对于长任务配合进度通知使用避免误判为卡死。排查顺序建议先确认 MCP 服务端能独立响应用 curl 或 MCP Inspector再确认客户端能拉到工具列表最后确认 Agent 能调通。每一步单独验证比一上来就跑完整链路更容易定位问题。6. 从桥接到生产McpClientTool 的长期使用建议跑通验证之后下一步是怎么在真实项目里稳定用。几个实际经验。工具命名冲突。多个 MCP 服务可能提供同名工具注册进同一个 Agent 时会冲突。McpClientTool 提供了WithName方法可以在注册前改掉工具名加个前缀区分来源。比如weather_look_up_location和map_look_up_location。注意改完名字后模型看到的就是新名字描述也可以同步用WithDescription调整。进度通知的消费。长任务场景下IProgress 的回调是在调用线程上执行的如果回调里做耗时操作会阻塞。建议回调里只做轻量记录比如写日志或更新 UI 状态重活丢到队列里异步处理。连接复用。McpClient 创建一次可以复用不需要每次调用都 CreateAsync。在 Agent 生命周期内保持一个 McpClient 实例工具列表也缓存起来避免频繁拉取。如果 MCP 服务端的工具会动态变化再考虑定期刷新。错误处理。McpClientTool 的 CallAsync 在远程调用失败时会抛异常Agent 的 RunAsync 可能会把异常包装后返回。建议在 Agent 层面加一层错误处理把工具调用失败的信息透传给模型让模型决定是重试还是换工具。直接让异常冒泡到用户界面体验很差。配置管理。Base URL、API Key、Model ID、MCP Endpoint 这些不要硬编码在代码里。用环境变量或配置文件管理不同环境开发、测试、生产用不同配置。特别是 API Key不要提交到代码仓库。如果你打算把这条链路用到编码类 Agent 或长期运行的自动化任务上Coding Plan 在持续调用场景下比按量计费更可控。入口在https://taotoken.net/coding-plan。接入文档在https://taotoken.net/doc里面有各语言 SDK 的配置示例。API Key 管理在https://taotoken.net/api-keys可以创建多个 Key 分配给不同环境。最后一点McpClientTool 是桥不是替代品。它不负责工具逻辑的实现也不负责模型的决策。它的职责边界很清楚——把远程 MCP 工具翻译成 AIFunction让 Agent 的调用链无感接入。理解这个边界配置和排查时就不会跑偏。
返回列表