)
1. 为什么 TextContent.text 是 MCP 工具链的“通用插头”如果你正在用 Cline、CC Switch 或者自己写的 Agent 去接 MCP Server大概率遇到过这种场景工具明明执行成功了日志里也看到返回了内容但前端拿到的却是一串CallToolResult(...)的字符串或者干脆报AttributeError: ToolResult object has no attribute data。问题往往不在网络也不在模型而是出在结果提取环节依赖了非标准字段。MCP 协议对工具返回内容有明确定义ToolResult.content是一个List[Content]其中文本内容的标准载体是TextContent它的核心字段就是text。换句话说只要一个 Server 声称自己符合 MCP 规范你就一定能从content列表里找到type text的项并读取它的text字段。这是跨平台兼容的“最大公约数”。但现实里FastMCP 2.0 在某些版本会额外挂一个.text快捷属性部分第三方 Server 会返回.data或.result还有些实验性的structured_content字段。如果你把这些扩展字段当成主路径应用就会被绑死在特定框架上。我试过在一个同时接三个 MCP Server 的项目里只因为其中一个 Server 升级后改了返回结构整个工具调用链就断了。后来把提取逻辑收敛到TextContent.text再配合多级兜底才真正稳定下来。这篇内容面向的是已经在用或准备用 Cline、CC Switch 接入 MCP 服务的开发者。我会先给出 TaoToken 的统一 Key 配置骨架再交付可复制的settings.json/config.toml然后重点讲 FastMCP 服务端返回TextContent的合规校验动作以及客户端侧的标准提取函数怎么写、怎么验证、怎么排错。目标只有一个让你的工具调用结果提取不再挑 Server。2. TaoToken 前置统一 Key 与 MCP 接入配置在讲提取逻辑之前先把“钥匙”配好。TaoToken 在这里的角色是统一模型接入层你可以在一个 Key 下调用不同模型省去为每个模型单独维护 base_url 和 api_key 的麻烦。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置时直接写这个。对于 MCP 场景你通常需要两样东西一是模型对话能力用于工具决策和结果整合二是 Coding Plan 或按量 Key用于长期编码/Agent 任务。如果你只是验证 MCP 工具调用链路用模型对话的 Key 就够了如果是长期跑 Agent建议走 Coding Plan额度更稳。配置时最容易踩的坑是把 base_url 写成带路径的完整地址。TaoToken 的 OpenAI 兼容端点就是https://taotoken.net/api后面由 SDK 自己拼/v1/chat/completions。如果你在 Cline 或 CC Switch 里填了多余的/v1会出现 404 或路径重复。另一个坑是 Key 权限有些 Key 只开了对话权限没开工具调用权限表现就是模型能回话但永远不触发 tool_calls。遇到这种情况去 console 里检查 Key 的 scope或者直接换一个全权限 Key 测试。提示MCP Server 本身不依赖 TaoTokenTaoToken 只负责模型侧。但如果你用 TaoToken 的模型来做工具决策Key 配置错会导致“模型不调用工具”而不是“工具提取失败”排错时要先区分这两类问题。3. 可复制配置settings.json 与 config.toml 骨架下面这份settings.json是给 Cline 类客户端用的核心是把 TaoToken 作为 OpenAI 兼容 provider同时把 MCP Server 以 stdio 方式挂上去。注意mcpServers里的command和args要换成你本地实际的可执行路径。{ llm: { provider: openai, baseUrl: https://taotoken.net/api, apiKey: sk-你的TaoTokenKey, model: claude-sonnet-4-20250514 }, mcpServers: { fastmcp-demo: { command: python, args: [-m, my_mcp_server], env: { MCP_LOG_LEVEL: DEBUG } } }, toolCalling: { extractMode: textcontent-first, fallbackToRawString: true, stripWhitespace: true } }如果你用的是 CC Switch 或类似支持 TOML 的工具等价配置如下。base_url同样只写到/api不要加/v1。[llm] provider openai base_url https://taotoken.net/api api_key sk-你的TaoTokenKey model claude-sonnet-4-20250514 [mcp_servers.fastmcp_demo] command python args [-m, my_mcp_server] [tool_calling] extract_mode textcontent_first fallback_to_raw_string true strip_whitespace true这两个骨架的共同点是把“提取策略”显式写进配置而不是散落在代码里。这样当你要切换 Server 或调试兼容性时改配置就能切换行为不用重新打包。extractMode建议默认textcontent-first只有在确认某个 Server 完全不返回标准content时才临时切到raw-string做兜底。4. 标准提取函数严格遵循 TextContent.text客户端侧的核心是一个提取函数。它的职责很单一从ToolResult里按 MCP 规范找到TextContent.text找不到再降级。下面这个版本我用了很久兼容性最好。from typing import Any def extract_text_from_mcp_result(result: Any) - str: 严格遵循 MCP Spec 提取文本。 标准路径result.content - List[Content] - TextContent(textstr) 兼容路径result.textFastMCP 扩展 最终兜底安全字符串表示 # 标准路径MCP 规范定义的 content 列表 try: if hasattr(result, content) and isinstance(result.content, list): for item in result.content: if getattr(item, type, None) text and hasattr(item, text): text item.text if isinstance(text, str): return text.strip() if text is not None: return str(text).strip() except Exception: pass # 兼容路径FastMCP 部分版本直接挂 .text try: if hasattr(result, text) and isinstance(result.text, str): return result.text.strip() except Exception: pass # 最终兜底返回可读字符串避免调用链崩溃 try: s str(result) for prefix in (CallToolResult(, Result(, ToolResult(): if s.startswith(prefix): s s[len(prefix):].rstrip()) break return s.strip() except Exception: return Tool execution succeeded, but no text result available.设计上有三个关键点。第一用getattr(item, type, None)而不是item.type避免非标准对象直接抛AttributeError。第二先检查type text再读text确保类型安全不会把图片或资源对象的字段误读成文本。第三返回前统一.strip()因为很多 Server 会在文本前后带换行下游做 JSON 解析时容易因此失败。调用侧这样写raw_result await mcp.call_tool(func_name, func_args) clean_text extract_text_from_mcp_result(raw_result) print(f[MCP] 提取文本前 100 字符: {clean_text[:100]})如果你在run()主流程里做智能短路可以加一个有效性判断单工具有效结果直接返回跳过 LLM 二次包装def is_valid_tool_result(text: str) - bool: stripped text.strip() return ( len(stripped) 0 and not stripped.startswith((Error, [, Exception, Failure)) and CallToolResult not in stripped )这样做的收益是减少一次 LLM 推理省 50 到 200 毫秒也避免模型把工具返回的 JSON 改写掉。但要注意这个短路只适合单工具且结果明确有效的场景多工具或错误结果还是要交给模型整合。5. FastMCP 服务端合规校验返回 TextContent 的正确姿势客户端提取逻辑再稳如果服务端返回的不是标准TextContent也只能走兜底。所以服务端侧要做一次合规校验。FastMCP 2.0 的mcp.tool()装饰器默认会把返回的字符串包装成TextContent但如果你手动构造ToolResult就容易写偏。正确的服务端返回写法from mcp.server.fastmcp import FastMCP from mcp.types import TextContent mcp FastMCP(demo-server) mcp.tool() def get_weather(city: str) - str: 返回指定城市的天气摘要 return f{city} 今天晴气温 22 到 28 摄氏度。 mcp.tool() def get_weather_structured(city: str) - list[TextContent]: 显式返回 TextContent 列表便于校验 return [TextContent(typetext, textf{city} 今天晴气温 22 到 28 摄氏度。)]上面两个工具在合规 Server 上都能被客户端用标准路径提取到。区别在于第二个显式构造了TextContent适合用来做协议校验。你可以写一个校验脚本直接调用工具并检查返回结构import asyncio from mcp.client.session import ClientSession from mcp.client.stdio import stdio_client async def verify_textcontent(server_cmd: list[str]): async with stdio_client(server_cmd) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result await session.call_tool(get_weather_structured, {city: 杭州}) assert hasattr(result, content), 缺少 content 字段 assert isinstance(result.content, list), content 不是列表 for item in result.content: assert getattr(item, type, None) text, 存在非 text 类型 assert isinstance(item.text, str), text 字段不是字符串 print([校验通过] 返回结构符合 TextContent 规范) asyncio.run(verify_textcontent([python, -m, my_mcp_server]))这个校验动作建议加进 CI每次改服务端返回逻辑就跑一遍。踩过的坑是有些 Server 在异常分支里直接return {error: ...}FastMCP 会把它序列化成非标准结构客户端标准路径就提取不到。解决办法是异常也走TextContent把错误信息放进text字段由客户端判断内容而不是靠字段名区分。6. 本篇常见错排查报错一AttributeError: TextContent object has no attribute data这是典型的依赖了非标准字段。检查你的提取函数是不是在找.data或.result。改回TextContent.text标准路径即可。如果某个 Server 确实只返回.data把它放到兼容路径里不要放在主路径。报错二提取结果为空字符串但工具日志显示有输出先看result.content是不是空列表。FastMCP 某些版本在工具返回None时会生成空content。服务端侧要保证任何分支都返回非空TextContent。客户端侧可以在提取后加一个空值告警方便定位。报错三TypeError: object of type TextContent is not JSON serializable说明你在把TextContent对象直接塞进 JSON 响应。提取函数返回的应该是str不是对象。检查调用侧有没有漏掉extract_text_from_mcp_result。报错四Cline 里工具调用成功但对话卡住多半是第二阶段 LLM 整合时消息格式不对。tool消息必须带tool_call_id且要和 assistant 消息里的tool_calls[].id对应。如果 ID 对不上模型会认为工具结果缺失一直等。建议在追加tool消息前打印一次 ID 做核对。报错五TaoToken 返回 401 或 404401 先查 Key 是否复制完整、有没有多余空格。404 查base_url是不是写成了https://taotoken.net/api/v1。正确写法就是https://taotoken.net/api路径由 SDK 拼接。如果用的是自写 HTTP 客户端确认请求路径是/api/v1/chat/completions。报错六FastMCP 服务端启动后客户端连不上stdio 模式下command和args必须能在客户端环境里直接执行。常见问题是用了虚拟环境里的python但客户端启动时没激活该环境。把command写成虚拟环境的绝对路径比如/path/to/venv/bin/python能省很多排查时间。7. 接入与验证把 Key、文档和模型对话串起来配置和提取逻辑都就位后建议按这个顺序验证一遍。先去 API Keys 页面创建一个专用 Key权限勾选对话和工具调用地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建后不要直接写进代码先放到环境变量里避免提交到仓库。然后打开接入文档对照一遍参数确认base_url、model名称和工具调用格式没有写错文档入口是 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。文档里对 OpenAI 兼容端点和工具调用消息结构有完整示例比对着改最快。如果你只是想先确认模型能不能正常触发工具调用用模型对话页面发一条带工具定义的请求就行地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。观察返回里有没有tool_calls字段有就说明模型侧通了剩下的是提取逻辑问题。长期跑编码或 Agent 任务的话建议切到 Coding Plan额度更稳入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。配置时把 Key 换成 Coding Plan 的 Key其他不变。最后如果你用 Claude Code 或 Anthropic 风格的工具链可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里的接入说明把 MCP Server 和模型侧分开配置排错时更容易定位是协议层还是模型层的问题。