☰
fastmcp client list_resources 加载 skills 技能:ResourcesAsTools 把技能转成 MCP 工具
2026/10/10 15:22:02 网站建设 项目流程

1. 从一次本地 MCP 调试说起:skills 技能为什么加载不出来

最近在本地调试一个 MCP 服务时,遇到一个挺典型的问题:技能目录里明明放了好几个SKILL.md,客户端连上去之后却什么都看不到。我一开始以为是路径写错了,反复检查roots参数,确认目录存在、文件也在,但list_resources返回的就是空列表。后来才发现,问题不在路径,而在于我根本没搞清楚 fastmcp 里「资源(Resource)」和「工具(Tool)」这两套东西是怎么衔接的。

这个场景其实很常见。你手头有一批用 Markdown 写的技能文件,比如travel-guide/SKILL.md、code-review/SKILL.md,它们本质上是「资源」——静态的、可读取的内容。但很多 MCP 客户端(比如 CherryStudio、Cline 这类)在界面上更习惯展示「工具」,因为工具是可以被模型直接调用的。于是就有了一个需求:能不能让客户端通过list_resources发现这些技能,同时又把它们暴露成可调用的 MCP 工具?答案就是 fastmcp 的ResourcesAsTools转换器。

先把几个核心概念说清楚,不然后面配置容易懵。

fastmcp 是什么:它是 MCP(Model Context Protocol)的一个 Python 实现库,让你用几行代码就能起一个 MCP 服务端,或者写一个 MCP 客户端。3.0 版本之后对 provider、transform 这些机制做了比较大的增强,SkillsDirectoryProvider和ResourcesAsTools都是这个版本体系下的能力。

list_resources 能做什么:客户端调用它,服务端返回当前注册的所有资源列表,每条资源有一个 URI,比如skill://travel-guide/SKILL.md。这是「发现」阶段。

ResourcesAsTools 适合谁:适合那些希望把资源「工具化」的开发者。加上这个 transform 之后,服务端会自动多出两个工具——list_resources和read_resource。客户端在工具列表里就能看到它们,模型可以主动调用read_resource去读取某个技能的完整内容,而不是只能被动等客户端去拉资源。

所以整条链路是:技能文件放在目录里 →SkillsDirectoryProvider把它们注册成资源 → 客户端list_resources发现 →ResourcesAsTools把资源读取能力包装成工具 → 模型或客户端通过工具调用拿到技能内容。下面我按这个顺序,把每一步的可复制配置和验证方法都写出来。

2. 前置准备:装对 fastmcp 版本并理解 provider 与 transform

这一步看着简单,但版本装错会直接导致SkillsDirectoryProvider或ResourcesAsTools导入失败。我踩过的坑就是先用pip install fastmcp装了稳定版,结果from fastmcp.server.transforms import ResourcesAsTools直接报ModuleNotFoundError。原因是这些能力在 3.0 的 beta 阶段才引入,稳定版还没有。

正确的安装命令是这样:

pip install "fastmcp>=3.0.0b2"

装完之后建议确认一下版本,避免环境里有多份 fastmcp 互相覆盖:

python -c "import fastmcp; print(fastmcp.__version__)"

如果输出是3.0.0b2或更高,就说明没问题。低于这个版本的话,ResourcesAsTools和SkillsDirectoryProvider都不存在,后面所有代码都会失败。

接下来理解两个关键角色。

Provider(提供者):负责往服务端「注册」内容。SkillsDirectoryProvider就是一个 provider,你给它一个根目录,它会扫描目录下的技能结构,把每个技能注册成资源。它的roots参数可以传字符串路径,也可以传Path对象。

Transform(转换器):负责在服务端已有内容的基础上「加工」。ResourcesAsTools是一个 transform,它接收服务端实例,然后基于现有的资源生成对应的工具。注意它的用法是mcp.add_transform(ResourcesAsTools(mcp)),要把mcp自己传进去,因为它需要读取当前服务端注册了哪些资源。

这里有个容易忽略的点:provider 和 transform 的添加顺序会影响结果。如果你先加 transform 再加 provider,transform 在生成工具时可能还没看到那些资源。所以稳妥的写法是先 add_provider,再 add_transform。我在调试时就因为顺序反了,导致工具列表里只有list_resources没有read_resource,排查了好一会儿。

技能目录的结构也要注意。SkillsDirectoryProvider期望的是一种约定式布局,每个技能一个子目录,目录里放SKILL.md。比如:

C:\Users\loong\.opencode\skill\ ├── travel-guide\ │ └── SKILL.md ├── code-review\ │ └── SKILL.md └──>pip install fastapi uvicorn

这样后面可以用mcp.run(transport="streamable-http")把服务跑起来,客户端通过 URL 接入。本地调试阶段其实用 stdio 传输就够了,但既然场景里提到 CherryStudio 访问,HTTP 方式更贴近实际。

3. 可复制配置:server 端把 skills 转成 MCP 工具

这一节是核心,我把完整的 server 代码写出来,你可以直接复制改路径就能跑。先看最小可运行版本,它同时做了三件事:注册技能目录、把资源转成工具、启动服务。

# server.py from fastmcp import FastMCP from fastmcp.server.providers.skills import SkillsDirectoryProvider from fastmcp.server.transforms import ResourcesAsTools # 1. 创建服务端实例 mcp = FastMCP("Skills Server") # 2. 注册技能目录为资源 provider mcp.add_provider( SkillsDirectoryProvider(roots=r"C:\Users\loong\.opencode\skill") ) # 3. 把资源转成工具(注意传入 mcp 自身) mcp.add_transform(ResourcesAsTools(mcp)) if __name__ == "__main__": # 本地调试用 stdio;要远程访问改成 streamable-http mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)

这段代码里,roots指向你的技能根目录。Windows 路径用原始字符串r"..."避免反斜杠转义问题;Linux 或 macOS 直接写/home/user/.opencode/skill这种即可。

如果你还想额外注册一些普通资源(不只是技能),可以像下面这样混着写。ResourcesAsTools会把所有资源统一转成工具,不管来源是 provider 还是@mcp.resource装饰器。

from fastmcp import FastMCP from fastmcp.server.providers.skills import SkillsDirectoryProvider from fastmcp.server.transforms import ResourcesAsTools mcp = FastMCP("My Server") # 普通资源:应用配置 @mcp.resource("config://app") def app_config() -> str: """Application configuration.""" return '{"app_name": "My App", "version": "1.0.0"}' # 带路径参数的资源:用户档案 @mcp.resource("user://{user_id}/profile") def user_profile(user_id: str) -> str: """Get a user's profile by ID.""" return f'{{"user_id": "{user_id}", "name": "User {user_id}"}}' # 技能目录 mcp.add_provider( SkillsDirectoryProvider(roots=r"C:\Users\loong\.opencode\skill") ) # 统一转成工具 mcp.add_transform(ResourcesAsTools(mcp)) if __name__ == "__main__": mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)

跑起来之后,服务端会暴露这些工具:list_resources、read_resource,以及每个资源对应的读取入口。客户端在工具列表里就能看到它们。

如果你用的是配置文件方式管理 MCP 服务(比如某些客户端支持 JSON 配置),可以写成这样。注意这里只是客户端侧的接入配置,服务端还是上面那段 Python 代码。

{ "mcpServers": { "skills-server": { "url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http" } } }

有些客户端用 TOML 风格配置,等价写法:

[mcp_servers.skills-server] url = "http://127.0.0.1:8000/mcp" transport = "streamable-http"

这里要提醒一句:ResourcesAsTools生成的两个工具名是固定的list_resources和read_resource。如果你在服务端已经手动定义了同名工具,可能会冲突。我建议先跑最小版本,确认工具列表正常,再往里加自定义工具。

另外,如果你希望把服务端的模型调用通道统一走一个 Key 管理,而不是每个服务各自配一套凭证,可以在环境变量里指定统一的 API 通道。比如把 endpoint 指向 TaoToken 的 API 地址https://taotoken.net/api,Key 用统一申请的那把。这样本地多个 MCP 服务联调时,不用来回切换凭证。具体做法是在启动服务前设置环境变量:

export TAOTOKEN_API_KEY="你的统一Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

Windows PowerShell 用$env:TAOTOKEN_API_KEY="..."。服务端代码里读取这两个变量即可。这样做的价值在于:技能资源本身是本地文件,但技能执行过程中如果需要调用模型,走的是统一通道,便于排查和计费。

4. 验证请求:client 端 list_resources 与 read_resource 实测

服务端跑起来之后,先别急着上客户端软件,用 fastmcp 自带的 Client 写个脚本验证一遍,能快速定位是服务端问题还是客户端问题。下面这段代码我实测过,可以直接用。

# client_check.py import asyncio from fastmcp import FastMCP, Client from fastmcp.server.providers.skills import SkillsDirectoryProvider async def main(): # 为了本地自测,这里直接内嵌一个服务端实例 mcp = FastMCP("Skills Server") mcp.add_provider( SkillsDirectoryProvider(roots=r"C:\Users\loong\.opencode\skill") ) async with Client(mcp) as client: # 1. 列出所有资源 resources = await client.list_resources() print("=== 资源列表 ===") for r in resources: print(r.uri) # 2. 读取某个技能 result = await client.read_resource("skill://travel-guide/SKILL.md") print("=== 技能内容 ===") print(result[0].text) asyncio.run(main())

运行后,正常输出应该类似:

=== 资源列表 === skill://travel-guide/SKILL.md skill://code-review/SKILL.md skill://data-clean/SKILL.md === 技能内容 === # Travel Guide Skill ...

如果资源列表是空的,先检查三件事:roots路径是否存在、每个技能子目录里是否有SKILL.md、fastmcp 版本是否够。如果read_resource报 URI 找不到,多半是 URI 拼写和实际注册的不一致,把list_resources的输出复制出来对照。

验证完资源读取,再验证工具转换是否生效。因为ResourcesAsTools是在服务端加的,客户端通过 HTTP 连接时才能看到工具。用下面的脚本连远程服务:

# client_tools_check.py import asyncio from fastmcp import Client async def main(): async with Client("http://127.0.0.1:8000/mcp") as client: tools = await client.list_tools() print("=== 工具列表 ===") for t in tools: print(t.name) # 调用 read_resource 工具读取技能 result = await client.call_tool( "read_resource", {"uri": "skill://travel-guide/SKILL.md"} ) print("=== 工具返回 ===") print(result) asyncio.run(main())

预期能看到list_resources和read_resource两个工具。调用read_resource时传入技能 URI,返回的就是SKILL.md的内容。这一步通了,说明整条链路打通。

在 CherryStudio 里验证的方式类似:添加 MCP 服务,填 URLhttp://127.0.0.1:8000/mcp,连接成功后进「资源」面板能看到技能列表,进「工具」面板能看到那两个工具。如果资源有、工具没有,八成是add_transform没加或者加在了 provider 之前。

联调阶段如果技能执行需要模型能力,把 endpoint 改到统一通道再测一遍。比如在客户端侧配置模型时,Base URL 填https://taotoken.net/api,Key 填统一 Key,Model ID 填你用的模型。这样技能读取和模型调用分开验证,出问题好定位。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

调试这条链路时,我遇到过几类报错,这里按现象、原因、解决方式列出来,方便你对照。

401 Unauthorized:这个最常见,出现在客户端调用模型或访问受保护 endpoint 时。原因通常是 Key 没配、配错,或者环境变量没生效。排查步骤:先确认TAOTOKEN_API_KEY在当前 shell 里能echo出来;再确认客户端配置里的 Key 和服务端读取的是同一个。如果服务端代码里硬编码了旧 Key,记得改掉。还有一种情况是 Key 有额度但被限流,返回也可能是 401 或 429,看具体响应体。

local proxy failed:这个报错一般出现在客户端配置了本地转发但转发进程没起来,或者端口被占用。先检查127.0.0.1:8000是否真的在监听,用netstat -ano | findstr 8000(Windows)或lsof -i:8000(macOS/Linux)。如果端口被占,换个端口重启服务端,客户端 URL 同步改。另外确认服务端mcp.run的 host 是127.0.0.1而不是0.0.0.0之外的其他地址,避免绑定失败。

reading choices 相关报错:这类通常出现在模型返回结构解析阶段,比如客户端期望choices字段但拿到的是别的结构。原因可能是 Base URL 指向的通道返回格式和客户端预期不一致。解决方式是确认 endpoint 是标准的 OpenAI 兼容格式,https://taotoken.net/api这种。如果客户端有「兼容模式」开关,打开试试。还有一种可能是模型名写错,服务端返回了错误对象而不是正常响应,客户端解析时就报 choices 相关错误。

OAuth 报错:如果客户端配置里开了 OAuth 认证,但服务端没配对应的认证流程,就会报 OAuth 相关错误。本地调试阶段建议先关掉 OAuth,用简单 Key 认证跑通链路,再按需加认证。如果确实需要 OAuth,确认回调地址、client id、secret 都填对,且服务端有对应的 token 端点。

除了这几类,还有一个隐蔽的坑:ResourcesAsTools生成的工具在部分客户端里显示为「只读工具」,如果客户端过滤了只读工具,就看不到。检查客户端的工具过滤设置,确保没有把list_resources、read_resource排除掉。

排查时我习惯按这个顺序:先本地 Client 脚本验证服务端 → 再 HTTP 脚本验证工具 → 最后上图形客户端。每层都通了,问题范围就缩小到具体那一层,不用瞎猜。

6. 把链路接到统一通道:Key、Base URL、Model ID 三件套

前面验证用的是本地直连,实际项目里往往需要把模型调用统一管理。这时候三件套要写全:Base URL、Key、Model ID。缺任何一个,客户端都可能报错或者静默失败。

以 Claude Code 这类编码工具为例,如果它支持自定义 endpoint,配置大致是这样:

{ "base_url": "https://taotoken.net/api", "api_key": "你的统一Key", "model": "claude-sonnet-4-20250514" }

如果是 Codex 风格的auth.json,写法类似:

{ "base_url": "https://taotoken.net/api", "api_key": "你的统一Key", "model": "gpt-4o" }

Cline 或 CC Switch 这类工具在设置界面里分别填 Base URL、API Key、Model ID 三个字段,对应填上即可。Model ID 要和你实际调用的模型一致,写错了会返回模型不存在。

把 endpoint 改到统一通道之后,再跑一遍第 4 节的验证脚本。这次客户端调用模型时走的是统一 Key,技能读取还是本地文件,两者互不影响。如果技能执行过程中需要模型总结、改写,就能直接复用这套配置。

需要统一 Key 的话,可以在https://taotoken.net/api-keys申请,接入文档在https://taotoken.net/doc。模型对话调试用https://taotoken.net/chat,长期编码或 Agent 场景可以看https://taotoken.net/coding-plan。控制台在https://taotoken.net/console。

最后说一个实用技巧:技能目录里的SKILL.md建议加上明确的触发条件描述,比如「当用户询问旅行规划时使用」。虽然ResourcesAsTools只是把内容转成工具,但模型在决定调用哪个工具时,会参考工具描述。你可以在SKILL.md开头写一段简短的用途说明,读取后模型更容易判断该不该用这个技能。这个细节不影响链路通不通,但影响实际使用效果,值得花几分钟优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询