ARTICLE DETAIL

资讯详情

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

动手学agent应用开发笔记_task04_MCP的实现:用FastMCP搭一个可调试的本地工具服务

动手学agent应用开发笔记_task04_MCP的实现:用FastMCP搭一个可调试的本地工具服务 1. 从 Function Calling 到 MCP为什么我要把本地函数封装成工具服务如果你已经跟着前面的笔记跑通过 Function Calling大概会有一种“原来大模型真的能调我的函数”的兴奋感。但兴奋过后问题很快就来了每加一个工具就要在tools数组里手写一遍 JSON Schema每换一个模型供应商base_url和api_key就要改一遍工具一多messages里塞的tool_calls和role: tool消息能把上下文撑爆。我试过在一个项目里塞了七八个工具光是维护那段tools定义就花掉大半天更别提调试时根本不知道模型到底有没有选中我期望的那个函数。MCPModel Context Protocol要解决的就是这个“工具定义与 Agent 逻辑耦合”的问题。你可以把它理解成给大模型装了一个 USB 接口工具服务端只管把能力暴露出来Agent 端只管按协议去发现和调用两边通过一套标准协议通信谁也不用关心对方内部怎么实现。而 FastMCP 就是 Python 生态里把这个协议封装得最顺手的框架几行装饰器就能把一个普通函数变成 MCP 工具。这篇笔记的目标很明确用 FastMCP 搭一个可调试的本地工具服务然后把它接到 TaoToken 的统一 Key/API 通道上跑通“定义工具 → 启动服务 → Agent 调用 → 拿到结果”的最小闭环。适合已经写过 Function Calling、想往 Agent 工程化方向走一步的开发者。全程只需要 Python 3.10 和一个能用的 API Key不需要任何额外硬件。2. 前置准备TaoToken 统一通道与 FastMCP 环境在动手写服务端之前先把两件事搞定一个是模型调用的通道一个是 FastMCP 的运行环境。2.1 为什么用 TaoToken 做统一 Key/API 通道做 Agent 开发最烦的事情之一就是不同模型供应商的接口格式、鉴权方式、计费口径都不一样。今天用 A 家的模型调工具明天想换成 B 家对比效果代码里base_url、api_key、甚至tools字段的写法都可能要动。TaoToken 的思路是提供一个统一的 API 入口你只需要维护一份 Key就能在多个模型之间切换Agent 侧的工具调用逻辑不用跟着改。对 MCP 场景来说这点尤其重要MCP 服务端本身不关心模型是谁它只负责暴露工具真正决定“调哪个工具、怎么调”的是 Agent 背后的模型。把模型通道统一到 TaoToken 之后你可以很方便地换模型来测试同一个 MCP 服务的表现而不用每次重写接入层。你需要先去控制台创建一个 API Key地址是 https://taotoken.net/api-keys 创建完先复制保存好后面配置里要用。如果你还没注册可以从官网入口进https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。2.2 安装 FastMCP 与依赖FastMCP 现在推荐用uv来管理环境比裸 pip 干净很多尤其是你要同时跑多个 MCP 服务的时候。先装 uv再初始化项目# 安装 uv如果还没装 pip install uv # 新建项目目录 mkdir fastmcp-demo cd fastmcp-demo # 初始化虚拟环境并添加依赖 uv init uv add mcp mcp[cli] openpyxl httpx这里mcp[cli]带上了命令行工具方便你用mcp run直接启动服务openpyxl是后面示例工具要用的httpx用来在验证环节发请求。装完之后可以用uv run python -c import mcp; print(mcp.__version__)确认一下版本避免后面因为版本差异出现奇怪的报错。3. 可复制的 FastMCP 服务端骨架下面这个server.py是我实际调试通过的最小骨架包含一个工具、一个 prompt 模板以及启动入口。你可以直接复制到项目根目录。# server.py from mcp.server.fastmcp import FastMCP # 创建 MCP Server名字会出现在客户端工具列表里 mcp FastMCP(LocalToolDemo) # 定义一个 prompt 模板给模型提供系统级提示 mcp.prompt() def tool_helper() - str: 告诉模型当前有哪些工具可用 return ( 你是一个本地工具助手已连接 LocalToolDemo 服务。\n 可用工具\n - word_count: 统计一段文本的字符数和词数\n - read_file_head: 读取本地文本文件的前 N 行\n 当用户提到统计、字数、读取文件等关键词时直接调用对应工具。 ) # 工具一统计文本 mcp.tool() def word_count(text: str) - str: 统计输入文本的字符数与词数 :param text: 待统计的文本内容 :return: 统计结果字符串 char_count len(text) word_count len(text.split()) return f字符数{char_count}词数{word_count} # 工具二读取文件前 N 行 mcp.tool() def read_file_head(file_path: str, lines: int 5) - str: 读取本地文本文件的前 N 行 :param file_path: 文件路径支持相对与绝对路径 :param lines: 读取行数默认 5 :return: 文件前 N 行内容 try: with open(file_path, r, encodingutf-8) as f: head [next(f) for _ in range(lines)] return .join(head) except FileNotFoundError: return f文件未找到{file_path} except StopIteration: return 文件行数不足指定行数 if __name__ __main__: # 直接运行即可启动 stdio 模式的 MCP 服务 mcp.run()几个关键点说明一下。FastMCP(LocalToolDemo)里的名字会作为服务标识出现在客户端建议起得有意义一点。mcp.tool()装饰的函数类型注解和 docstring 会被自动转成 JSON Schema所以参数类型一定要写清楚docstring 里的:param描述也会被带进去模型就是靠这些描述来判断该不该调这个工具的。mcp.prompt()定义的是提示模板不是工具它用来给模型提供上下文比如告诉它有哪些工具可用。启动服务uv run mcp run server.py如果终端没有报错、光标停住不动说明服务已经以 stdio 模式跑起来了正在等待客户端连接。这是正常现象不是卡死。4. 接入 TaoToken 通道并验证一次完整工具调用服务端跑起来之后下一步是让 Agent 通过 TaoToken 的通道去调用它。这里我用一个最小的 Python 客户端来模拟 Agent 的行为先拉取 MCP 服务暴露的工具列表再把工具定义转成 OpenAI 兼容的tools格式发给 TaoToken 的 API最后根据返回的tool_calls去执行本地工具。4.1 配置 config.toml在项目根目录建一个config.toml把通道信息集中管理避免硬编码[llm] base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 model gpt-4o-mini [mcp] server_script server.py transport stdiobase_url用https://taotoken.net/api即可不要加多余的路径。api_key换成你在控制台创建的那串。模型名按你实际想用的填TaoToken 支持多个模型换模型只需要改这一行。4.2 验证请求从工具发现到调用返回下面这段客户端代码演示完整链路。为了聚焦逻辑我用subprocess直接和 MCP 服务通信实际项目里你可以换成官方 SDK。# client_demo.py import json import subprocess import tomllib import httpx # 读取配置 with open(config.toml, rb) as f: config tomllib.load(f) llm_cfg config[llm] # 1. 启动 MCP 服务并拉取工具列表简化演示实际用 SDK 更稳 proc subprocess.Popen( [uv, run, mcp, run, config[mcp][server_script]], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, textTrue, ) # 这里仅示意工具定义真实场景由 MCP 协议返回 tools [ { type: function, function: { name: word_count, description: 统计输入文本的字符数与词数, parameters: { type: object, properties: { text: {type: string, description: 待统计的文本内容} }, required: [text], }, }, } ] # 2. 构造请求发给 TaoToken 统一通道 messages [{role: user, content: 帮我统计一下这句话的字数今天天气不错适合写代码。}] resp httpx.post( f{llm_cfg[base_url]}/v1/chat/completions, headers{Authorization: fBearer {llm_cfg[api_key]}}, json{ model: llm_cfg[model], messages: messages, tools: tools, }, timeout30, ) data resp.json() message data[choices][0][message] print(模型返回, json.dumps(message, ensure_asciiFalse, indent2)) # 3. 如果模型决定调用工具执行本地函数并回传结果 if message.get(tool_calls): tool_call message[tool_calls][0] args json.loads(tool_call[function][arguments]) # 这里直接调用本地函数模拟 MCP 执行 result f字符数{len(args[text])}词数{len(args[text].split())} messages.append(message) messages.append({ role: tool, tool_call_id: tool_call[id], content: result, }) # 4. 把工具结果回传让模型生成最终回答 final httpx.post( f{llm_cfg[base_url]}/v1/chat/completions, headers{Authorization: fBearer {llm_cfg[api_key]}}, json{model: llm_cfg[model], messages: messages, tools: tools}, timeout30, ).json() print(最终回答, final[choices][0][message][content]) proc.terminate()跑通之后你会看到类似这样的输出模型先返回一个tool_calls里面function.name是word_countarguments里带着从用户话里抽出来的text然后你把执行结果以role: tool回传模型再生成一句自然语言总结。这就是 MCP 工具调用的最小闭环。如果你更想直接在对话界面里验证模型对工具的理解可以打开模型对话页面 https://taotoken.net/model-chat 把工具描述贴进去手动问一句“统计这句话字数”看模型会不会主动提出调用工具。这个方式适合快速验证 prompt 和工具描述写得清不清楚。5. 本篇常见错排查5.1 启动时报 ModuleNotFoundError: No module named mcp九成是环境没对上。用uv run mcp run server.py启动时uv 会自动用项目虚拟环境但如果你之前用系统 Python 装过旧版 mcp可能串了。先uv pip list | grep mcp确认版本再uv sync重新同步依赖。另外注意mcp[cli]和mcp是两个不同的安装目标只装mcp是没有mcp run命令的。5.2 工具调用返回 401 或 invalid api key先检查config.toml里的api_key有没有多余空格TOML 里字符串不会自动 trim。再确认base_url写的是https://taotoken.net/api不要手滑写成带/v1的完整路径客户端代码里已经拼了/v1/chat/completions。如果还是 401去控制台重新生成一个 Key 试试有时候是复制时漏了字符。5.3 模型不调用工具直接自己编答案这是工具描述没写清楚。模型判断要不要调工具主要看description和参数说明。把word_count的描述从“统计文本”改成“统计输入文本的字符数与词数当用户询问字数、长度、统计时使用”命中率会明显提升。另外 prompt 模板里明确列出工具名和触发关键词也能帮模型做决策。5.4 文件路径类工具报“文件未找到”MCP 服务的工作目录取决于你从哪里启动它。如果你在项目根目录启动相对路径就是相对项目根目录如果客户端用 subprocess 启动工作目录可能变成别的地方。稳妥做法是工具内部用Path(file_path).resolve()打印一下实际解析路径或者干脆要求传绝对路径。我踩过的坑就是调试时路径看着对实际跑起来差了两级目录。5.5 stdio 模式下 print 调试导致协议错乱MCP 的 stdio 传输是靠标准输入输出传 JSON-RPC 消息的你在工具函数里随手print(debug)会污染协议流客户端直接解析失败。调试信息一律用sys.stderr.write或者写日志文件别用 print。6. 下一步把 MCP 服务接到长期编码与 Agent 工作流跑通这个最小闭环之后你可以往两个方向走。一个是把工具数量扩起来比如加数据库查询、HTTP 请求、文件批量处理这时候建议按功能拆成多个 MCP 服务每个服务职责单一调试和复用都方便。另一个方向是把它接进真实的编码或 Agent 工作流让模型在写代码、改配置的时候自动调用你封装好的本地能力。如果你打算长期跑编码类 Agent可以了解一下 Coding Plan https://taotoken.net/coding-plan 它更适合需要持续调用、上下文较长的场景。接入文档在 https://taotoken.net/doc 里面有各语言 SDK 的完整示例遇到协议细节问题可以先翻这里。API Key 管理还是回到 https://taotoken.net/api-keys 建议给不同项目建不同的 Key方便排查用量。最后留一个实用习惯每次改完工具定义先用模型对话页面手动问一句触发词确认模型能正确选中工具再去跑完整客户端。这样能把“工具描述问题”和“客户端代码问题”分开定位省掉大量来回折腾的时间。
返回列表