☰
自己动手写一个联网MCP工具:用TaoToken统一Key打通fastmcp与duckduckgo-search
2026/10/3 6:20:29 网站建设 项目流程

1. 从零构建联网 MCP 工具:为什么选 stdio + fastmcp + duckduckgo-search

MCP(Model Context Protocol)是让大模型调用外部工具的开放协议,而联网搜索是它最实用的落地场景之一。你可能会问:模型本身不是能联网吗?问题在于,大多数本地模型或第三方 API 通道默认没有实时检索能力,回答里全是训练截止日期之前的旧信息。自己写一个联网 MCP 工具,等于给模型装上一双能实时看网页的眼睛。

这个工具适合谁?适合正在用 Claude Desktop、Cline、Cursor 这类支持 MCP 客户端的开发者,也适合想把本地 Python 脚本升级成"模型可调用工具"的进阶玩家。整个方案的技术栈很轻:用 fastmcp 定义工具函数,用 duckduckgo-search 做免费检索后端,用 stdio 作为通信方式,再通过 TaoToken 统一 Key 打通模型调用通道。不需要服务器、不需要域名、不需要备案,本地跑通即可。

为什么是 stdio 而不是 SSE 或 HTTP?stdio 是本地开发最省事的方式——客户端直接以子进程方式启动你的 Python 脚本,通过标准输入输出交换 JSON-RPC 消息。没有端口占用、没有跨域、没有鉴权,调试时甚至能直接看进程日志。对于个人工具来说,stdio 的启动延迟几乎为零,稳定性也最好。

为什么用 duckduckgo-search?因为它不需要 API Key,安装即用,对入门者极其友好。虽然它的结果质量和稳定性不如商业搜索 API,但作为"跑通一次真实联网问答"的目标,它完全够用。等你验证完整个链路,再换成 SerpAPI 或自建检索服务,只需要改web_search函数内部几行代码。

这里有个容易踩的坑:很多人以为 MCP 工具写完就能被模型调用,其实中间还差一层"模型通道"。MCP 客户端负责把工具描述发给模型,模型决定调用哪个工具,但模型本身得先能连上。如果你用的是第三方 API 通道,就需要一个统一的 Key 和 Base URL 来承接模型请求。TaoToken 在这里扮演的就是这个角色——一个 Key 同时覆盖模型对话和工具调用链路,省去在多个平台之间来回切换配置的麻烦。

我试过把 MCP 工具和模型通道分开配置,结果调试时经常分不清是工具报错还是模型没连上。统一通道之后,排障路径清晰很多:先确认模型能正常对话,再确认工具能被列出,最后确认工具能被调用。这个顺序能帮你省下大量时间。

接下来的内容会按"环境准备 → 写 server.py → 配置模型通道 → 本地联调验证 → 常见报错排查"的顺序展开,每一步都给出可复制的命令和配置。目标很明确:让你在本地跑通一次"模型自主决定搜索 → 拿到实时结果 → 生成带来源的回答"的完整流程。

2. TaoToken 前置准备:统一 Key 与 API 通道配置

在写 MCP 工具之前,先把模型通道准备好。这一步经常被跳过,导致后面工具写完了却不知道怎么让模型用上。TaoToken 的作用是提供一个统一的 API 入口,你只需要一个 Key,就能同时完成模型对话和工具调用链路的对接。

先注册并拿到 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,完成账号注册后进入控制台。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在"API Keys"页面创建一个新的 Key。创建时建议给它起个能认出来的名字,比如mcp-search-dev,方便后续区分不同用途的 Key。

拿到 Key 之后,你需要记住两个核心信息:Base URL 和 Key 本身。Base URL 是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 API 根路径使用。Key 的格式通常是一串以sk-开头的字符串,复制后先存到本地环境变量里,不要硬编码进代码。

在终端里设置环境变量,macOS 和 Linux 用:

export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

Windows PowerShell 用:

$env:TAOTOKEN_API_KEY="sk-你的实际Key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"

设置完可以用echo $TAOTOKEN_API_KEY(Windows 用echo $env:TAOTOKEN_API_KEY)确认一下是否生效。如果输出为空,说明环境变量没设置成功,检查一下是否在正确的终端会话里执行。

接下来验证模型通道是否可用。用 curl 发一个最简单的对话请求:

curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "claude-3-5-sonnet-20241022", "messages": [{"role": "user", "content": "回复两个字:收到"}], "max_tokens": 20 }'

如果返回的 JSON 里有choices字段且内容包含"收到",说明模型通道已经打通。如果返回 401,说明 Key 不对或没带上;如果返回 404,检查 Base URL 是否写成了https://taotoken.net/api而不是带/v1的完整路径——不同客户端对路径拼接的处理方式不同,这一点后面配置 MCP 客户端时还会遇到。

模型 ID 的选择上,建议先用一个你熟悉的模型跑通链路,比如claude-3-5-sonnet-20241022或gpt-4o。等工具联调成功后再换成你日常用的模型。模型 ID 的完整列表可以在接入文档里查到: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

这里有个细节值得注意:MCP 工具本身不直接调用模型,它只负责"被模型调用"。模型通道的配置是在 MCP 客户端那一侧完成的。所以你现在配置的 TaoToken Key 和 Base URL,后面要填到 Claude Desktop 或 Cline 的配置文件里,而不是填到 server.py 里。server.py 只关心搜索逻辑,不关心模型是谁。

如果你打算长期做编码类 Agent 开发,可以考虑 Coding Plan 方案,它针对高频工具调用场景做了通道优化: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。不过对于本篇的入门目标,按量付费的 API Key 就足够了。

3. 可复制配置:server.py 与依赖清单完整实现

现在进入核心部分:写 MCP 服务器。先建项目目录并安装依赖。

mkdir my_mcp_search && cd my_mcp_search python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install "mcp[cli]" fastmcp duckduckgo-search

依赖说明:mcp[cli]是官方 Python SDK,带 CLI 工具方便调试;fastmcp提供更简洁的工具注册装饰器;duckduckgo-search是免费检索后端。三个包都不大,安装通常在一分钟内完成。

创建server.py,完整代码如下:

from mcp.server.fastmcp import FastMCP from duckduckgo_search import DDGS mcp = FastMCP("My Web Search") @mcp.tool() async def web_search(query: str, max_results: int = 3) -> str: """在互联网上执行实时搜索,并返回摘要结果。 Args: query (str): 用户的搜索关键词。 max_results (int): 返回的最大结果数量(默认3)。 Returns: str: 格式化的搜索结果摘要。 """ try: with DDGS() as ddgs: results = ddgs.text(query, max_results=max_results) formatted_results = [] for i, result in enumerate(results, 1): title = result.get("title", "N/A") body = result.get("body", "N/A") href = result.get("href", "#") formatted_results.append( f"{i}. **{title}**\n {body}\n [来源]({href})" ) if not formatted_results: return "未找到相关搜索结果。" return "\n\n".join(formatted_results) except Exception as e: return f"搜索时发生错误: {str(e)}" if __name__ == "__main__": mcp.run(transport="stdio")

这段代码有几个关键点。@mcp.tool()装饰器把web_search注册为 MCP 工具,客户端能自动读取函数签名和文档字符串,生成工具描述发给模型。query: str和max_results: int = 3的类型注解很重要——MCP 会据此做参数序列化和反序列化,类型写错会导致调用失败。文档字符串里的 Args 和 Returns 部分会被模型读到,帮助它判断何时调用这个工具。

transport="stdio"指定通信方式为标准输入输出。注意mcp.run()会阻塞进程,这是正常的——它进入事件循环等待客户端消息。不要在它后面写任何代码,否则不会执行。

如果你用的是较新版本的 fastmcp,导入路径可能是from fastmcp import FastMCP。两个包都装了的话,优先用mcp.server.fastmcp,它是官方 SDK 自带的,兼容性更稳。

接下来配置 MCP 客户端。以 Claude Desktop 为例,配置文件路径:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json

在mcpServers字段下加入你的服务器配置:

{ "mcpServers": { "my-web-search": { "command": "/absolute/path/to/.venv/bin/python", "args": ["/absolute/path/to/server.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的实际Key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } } }

三个关键字段必须写全:command指向虚拟环境里的 Python 解释器绝对路径,args指向 server.py 的绝对路径,env里放 TaoToken 的 Key 和 Base URL。路径必须用绝对路径,相对路径在客户端启动子进程时会解析失败。

如果你用的是 Cline 或 Cursor,配置结构类似,但字段名可能不同。Cline 的 MCP 配置在设置面板里,格式是:

{ "mcpServers": { "my-web-search": { "command": "python", "args": ["/absolute/path/to/server.py"], "disabled": false, "autoApprove": ["web_search"] } } }

autoApprove字段可以让web_search免确认直接执行,调试时很方便,但生产环境建议关掉,避免模型频繁调用消耗额度。

模型通道的配置在客户端另一处。以 Cline 为例,在 API Provider 设置里选 "OpenAI Compatible",Base URL 填https://taotoken.net/api,API Key 填你的 TaoToken Key,Model ID 填claude-3-5-sonnet-20241022。这三件套(Base URL + Key + Model ID)必须同时正确,缺一个都会导致模型无法调用工具。

4. 验证请求:本地 stdio 联调与成功结果确认

配置写完后,先别急着开客户端,用官方 CLI 工具单独验证 server.py 能否正常工作。这是排障的关键一步——如果 CLI 都跑不通,客户端里更不可能跑通。

mcp dev server.py

第一次运行会提示是否安装@modelcontextprotocol/inspector,输入y确认。安装完成后会自动打开浏览器,地址是http://127.0.0.1:6274。这是 MCP Inspector 界面。

在 Inspector 里点击 "Tools" → "List Tools",你应该能看到web_search工具及其参数描述。点击web_search,在表单里填入:

{ "query": "Python 3.13 新特性", "max_results": 2 }

点击 "Execute",右侧会返回搜索结果。如果看到带标题、摘要和来源链接的格式化文本,说明 server.py 本身没问题。

接下来写一个 Python 客户端做端到端验证。创建test_client.py:

import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params = StdioServerParameters( command="python", args=["server.py"], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() print("可用工具:", [t.name for t in tools.tools]) result = await session.call_tool( "web_search", arguments={"query": "MCP 协议是什么", "max_results": 2}, ) print("搜索结果:\n", result.content[0].text) if __name__ == "__main__": asyncio.run(main())

运行python test_client.py。预期输出分两部分:先打印可用工具: ['web_search'],再打印两条搜索结果。如果工具列表为空,说明@mcp.tool()装饰器没生效或导入路径有问题;如果调用时报错,看错误信息里是搜索库的问题还是参数序列化的问题。

端到端验证的最后一步是在真实客户端里测试。重启 Claude Desktop 或 Cline,在对话里输入:

帮我搜索一下最近 Python 生态有什么值得关注的新库,并给出信息来源。

如果配置正确,模型会先输出一段"我来搜索一下"之类的过渡语,然后触发web_search工具调用,拿到结果后生成带来源的回答。整个过程你能在客户端的工具调用面板里看到web_search的执行记录。

成功的关键标志有三个:工具出现在客户端的工具列表里、模型主动决定调用工具而不是直接回答、返回结果里包含实时信息。三个都满足,说明整条链路——从模型通道到 MCP 工具到搜索后端——全部打通。

如果模型没有调用工具而是直接回答,通常是工具描述不够清晰。检查web_search的文档字符串是否说明了"实时搜索"和"互联网"这两个关键词,模型靠这些判断何时该用工具。另外确认客户端的模型确实支持 function calling,部分小模型不支持工具调用。

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

排障时按"模型通道 → 工具注册 → 搜索后端"的顺序排查,能最快定位问题。

401 Unauthorized:模型通道鉴权失败。检查三处:TaoToken Key 是否复制完整(有没有漏掉尾部字符)、Base URL 是否写成https://taotoken.net/api(不要带/v1,客户端会自动拼)、请求头是否是Authorization: Bearer sk-xxx格式。如果 Key 是在环境变量里设置的,确认客户端启动时能读到——Claude Desktop 的env字段就是干这个的,别只在本机 shell 里 export 而忘了写进配置。

local proxy failed / connection refused:客户端连不上模型通道。先确认网络能访问https://taotoken.net/api,用 curl 测一下。如果 curl 通但客户端不通,检查客户端是否配置了额外的网络设置导致请求被拦截。另一个常见原因是 Base URL 末尾多了斜杠,比如https://taotoken.net/api/,某些客户端拼接路径时会变成//v1/chat/completions,导致 404 而非 401,报错信息可能被包装成连接失败。

Error reading choices / choices field missing:模型返回了非预期格式。通常是 Model ID 写错了,客户端请求了一个不存在的模型,通道返回错误 JSON,客户端解析choices时失败。对照接入文档确认 Model ID 拼写,注意大小写和日期后缀。另一个可能是max_tokens设得太小,模型还没输出完整 JSON 就被截断。

OAuth / authentication flow required:某些客户端默认走 OAuth 流程,但 TaoToken 用的是 API Key 鉴权。在客户端设置里把认证方式从 OAuth 改成 API Key,或者选择 "OpenAI Compatible" 这类通用接口模式。Claude Code 的配置在~/.claude/settings.json,需要显式指定apiKey和baseURL字段。

工具列表为空:server.py 启动了但工具没注册。检查@mcp.tool()装饰器是否在mcp = FastMCP(...)之后、mcp.run()之前。如果用了if __name__ == "__main__"保护,确认客户端启动的确实是这个文件。还有一种情况是虚拟环境路径写错,客户端用系统 Python 启动,找不到安装的包,进程直接退出,工具列表自然为空。

搜索返回空结果:duckduckgo-search 被限流或查询词太生僻。换一个常见查询词试试,比如"Python tutorial"。如果持续为空,可能是网络环境导致 DuckDuckGo 不可达,这种情况需要换检索后端,但那是另一个话题了。

stdio 进程启动后立即退出:看客户端日志里子进程的 stderr 输出。常见原因是mcp.run()之前有语法错误,或者依赖没装全。在终端里直接python server.py跑一下,如果报ModuleNotFoundError,说明虚拟环境没激活或包装错了地方。

排查时养成看日志的习惯。Claude Desktop 的日志在~/Library/Logs/Claude/(macOS),Cline 在 VS Code 的输出面板里选 "Cline" 频道。日志里会打印子进程的启动命令和 stderr,大部分问题看一眼日志就能定位。

6. 继续深入:从跑通到好用,下一步可以做什么

跑通一次联网问答只是起点。接下来你可以从三个方向继续打磨这个工具。

第一,换检索后端。duckduckgo-search 适合入门,但结果质量和稳定性有限。把web_search函数内部的DDGS()换成 SerpAPI、Bing Search API 或自建检索服务,接口签名保持不变,模型侧完全无感知。这就是 MCP 协议的好处——工具实现和模型调用解耦。

第二,加更多工具。除了搜索,你还可以加fetch_url(抓取指定网页正文)、get_news(按关键词拉新闻摘要)、search_github(搜代码仓库)。每个工具就是一个带@mcp.tool()的异步函数,注册完重启客户端就能用。工具多了之后,模型会自动根据用户意图选择调用哪个。

第三,优化工具描述。模型决定调不调用工具,全靠文档字符串。把"在互联网上执行实时搜索"改成"当用户询问最新事件、实时数据或需要外部信息时,调用此工具搜索互联网",能显著提升触发准确率。参数描述也可以写得更具体,比如max_results注明"建议 3-5,过多会稀释关键信息"。

如果你打算把这个工具用在长期编码或 Agent 场景,建议把模型通道切到 Coding Plan,它在高频工具调用下的稳定性和成本控制更好: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。日常调试和验证模型行为时,用模型对话页面快速测试更顺手: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。

最后提醒一个实用技巧:把 server.py 里的搜索逻辑和格式化逻辑拆成两个函数。搜索函数只负责拿原始结果,格式化函数负责转成模型友好的文本。这样换后端时只改搜索函数,格式化逻辑复用,测试也更容易写。工具函数本身保持薄薄一层,只做参数校验和调用编排,复杂逻辑下沉到独立模块。这个结构在工具数量增长后优势会非常明显。

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

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

立即咨询