☰
MCP Server Tool 开发学习文档:从零搭建可调试的本地工具服务
2026/10/7 14:25:00 网站建设 项目流程

1. 从一次“工具调不通”说起:MCP Server Tool 到底解决什么问题

如果你最近在折腾 AI 应用,大概率听过 MCP(Model Context Protocol)。简单说,它是一套让模型和外部工具“说同一种语言”的协议。而MCP Server Tool 开发,就是你自己写一个服务,把某个能力(抓网页、查数据库、跑脚本)注册成标准工具,让支持 MCP 的客户端能自动发现并调用它。

它适合谁?三类人最该上手:一是想把内部系统接进 AI 助手的后端同学;二是做智能硬件、需要本地工具链的嵌入式开发者;三是想理解“工具注册—参数校验—调用链路”这条完整链路的 AI 应用开发者。你不需要先精通协议细节,只要会写 Python 函数,就能跑通第一个工具。

我见过太多人卡在同一个地方:工具写好了,客户端却报Unknown tool或者参数校验失败,日志里只有一行reading choices让人摸不着头脑。问题往往不在业务逻辑,而在注册声明和实际调用对不上——list_tools里声明的inputSchema和call_tool里读取的arguments键名不一致,或者传输方式选错导致请求根本没到服务端。

这篇学习文档就按“能跟做”的标准来:先给可复制的项目初始化配置,再给工具定义模板,然后本地调试,最后通过统一 Key/API 通道做端到端验证。全程用 stdio 和 SSE 两种传输方式对照,把踩坑点摊开讲。你跟着敲一遍,基本就能独立开发自己的 MCP Server Tool 了。

2. 前置准备:项目初始化与 TaoToken 统一通道配置

动手之前,先把环境和“通道”理清楚。MCP Server Tool 本身是本地服务,但你要验证它能不能被模型正确调用,就需要一个能发起工具调用的客户端环境。这里我用 TaoToken 的统一 Key/API 通道来做验证,好处是 Base URL 和 Key 一套配置通吃,不用在多个平台之间来回切换。

先说项目初始化。推荐用uv管理依赖,速度快、隔离干净。新建目录后执行:

mkdir mcp-tool-demo && cd mcp-tool-demo uv init --python 3.11 uv add mcp uvicorn starlette anyio httpx click

如果你习惯 pip,等价命令是:

pip install mcp uvicorn starlette anyio httpx click

依赖说明一下:mcp是官方 Python SDK,提供Server、types、stdio_server、SseServerTransport等核心类;starlette+uvicorn用于 SSE 模式的 HTTP 服务;anyio负责异步主循环;httpx用来发外部请求;click解析命令行参数。

接下来配置 TaoToken 通道。访问控制台拿到 API Key,然后在项目根目录建一个.env文件(记得加进.gitignore):

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

这里有个关键点:Base URL 用https://taotoken.net/api,不要带任何多余路径。很多 401 报错就是因为把/v1之类的后缀手动拼上去了,导致鉴权路径不匹配。Key 的获取入口在控制台的 API Keys 页面,模型对话入口可以用来做交互式验证。

注意:.env只放本地,不要提交到仓库。团队协作时用环境变量注入,别把 Key 硬编码进源码。

环境就绪后,目录结构建议这样组织,后面调试会清晰很多:

mcp-tool-demo/ ├── .env ├── pyproject.toml ├── server.py # MCP Server 主文件 ├── client_test.py # 本地调用测试 └── tools/ └── fetch_tool.py # 工具业务逻辑

把工具逻辑单独拆文件,是为了后面工具变多时好维护。一个 Server 注册十几个工具很常见,全塞一个文件会失控。

3. 可复制配置:工具定义模板与 Server 注册完整代码

这一节是核心,直接给能跑的完整代码。先看工具业务逻辑tools/fetch_tool.py:

import httpx from mcp import types async def fetch_website(url: str) -> list[types.TextContent]: headers = {"User-Agent": "MCP-Tool-Demo/1.0"} async with httpx.AsyncClient(timeout=15.0) as client: response = await client.get(url, headers=headers) response.raise_for_status() return [types.TextContent(type="text", text=response.text[:2000])]

注意返回类型必须是list[types.TextContent | types.ImageContent | types.EmbeddedResource],这是协议规定的。截断到 2000 字符是防止超大页面把上下文撑爆,实际项目里你可以按需调整。

然后是server.py,包含工具注册、参数校验和双传输模式:

import anyio import click import uvicorn from mcp import types from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.responses import Response from starlette.routing import Mount, Route from tools.fetch_tool import fetch_website app = Server("mcp-tool-demo") @app.list_tools() async def list_tools() -> list[types.Tool]: return [ types.Tool( name="fetch", description="抓取指定网页并返回文本内容", inputSchema={ "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "description": "要抓取的网页 URL", } }, }, ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name != "fetch": raise ValueError(f"Unknown tool: {name}") if "url" not in arguments: raise ValueError("Missing required argument 'url'") return await fetch_website(arguments["url"])

这里有两个必须对齐的地方:list_tools里name="fetch",call_tool里判断的也是"fetch";inputSchema里required: ["url"],call_tool里读的也是arguments["url"]。任何一处不一致,客户端就会报工具不存在或参数缺失,这是最高频的坑。

接着是入口函数,用 click 控制传输方式:

@click.command() @click.option("--port", default=8000, help="SSE 监听端口") @click.option("--transport", type=click.Choice(["stdio", "sse"]), default="stdio") def main(port: int, transport: str) -> int: if transport == "stdio": anyio.run(run_stdio) else: run_sse(port) return 0 async def run_stdio(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) def run_sse(port: int): sse = SseServerTransport("/messages/") async def handle_sse(request): async with sse.connect_sse( request.scope, request.receive, request._send ) as streams: await app.run(streams[0], streams[1], app.create_initialization_options()) return Response() starlette_app = Starlette( debug=True, routes=[ Route("/sse", endpoint=handle_sse, methods=["GET"]), Mount("/messages/", app=sse.handle_post_message), ], ) uvicorn.run(starlette_app, host="0.0.0.0", port=port) if __name__ == "__main__": main()

如果你用的是 Claude Code 或 Cline 这类客户端,它们的 MCP 配置通常是一个 JSON 片段,三件套必须写全——Base URL、Key、Model ID。以 Cline 的 MCP 配置为例:

{ "mcpServers": { "tool-demo": { "command": "uv", "args": ["run", "python", "server.py", "--transport", "stdio"], "env": { "TAOTOKEN_API_KEY": "sk-你的实际Key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } } }

stdio 模式下客户端会自己拉起进程,所以command和args要指向你的启动命令。SSE 模式则改成填 URL:http://localhost:8000/sse。两种模式别混用,stdio 配置里填 URL、SSE 配置里写 command,都会导致连接失败。

4. 本地调试与端到端验证:从 list_tools 到 call_tool 跑通

代码写完,先别急着接客户端,用本地测试脚本把链路跑通最稳妥。新建client_test.py:

import asyncio from mcp.client.session import ClientSession from mcp.client.sse import sse_client, SseServerParameters async def main(): params = SseServerParameters(url="http://localhost:8000/sse") async with sse_client(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("fetch", {"url": "https://example.com"}) print("调用结果:", result.content[0].text[:200]) asyncio.run(main())

先启动 SSE 服务:

uv run python server.py --transport sse --port 8000

看到 uvicorn 输出Uvicorn running on http://0.0.0.0:8000就说明服务起来了。另开一个终端跑测试脚本:

uv run python client_test.py

预期输出类似:

可用工具: ['fetch'] 调用结果: <!doctype html><html>...

list_tools返回工具名列表,说明注册声明被正确读取;call_tool返回网页内容,说明参数校验和业务逻辑都通了。这两步都过,本地链路就没问题。

接下来做端到端验证,把 TaoToken 通道接进来。如果你用的是支持 MCP 的编码客户端,在配置里填好三件套后,直接问模型“帮我抓取 example.com 的内容”。模型会先调用list_tools发现fetch工具,再发起call_tool。你可以在服务端日志里看到完整的调用记录。

验证模型对话能力时,可以用模型对话入口做一次交互式确认,确保 Key 和 Base URL 生效。长期做编码和 Agent 开发的,建议走 Coding Plan,配额和稳定性更适合持续调试。

实测下来,端到端最容易出问题的不是代码,而是客户端配置里的路径和传输方式。stdio 模式下客户端拉起的进程工作目录可能不是你的项目根目录,导致tools.fetch_tool导入失败。解决办法是在配置里显式指定cwd,或者把工具逻辑内联进server.py。SSE 模式则要确认端口没被占用,/sse和/messages/两个路由都要能访问。

5. 常见报错排查:401、Unknown tool、reading choices 逐个击破

调试阶段报错是常态,关键是看懂错误在说什么。下面这几个是我踩过最多的,对照着排查能省不少时间。

401 Unauthorized:九成是 Key 或 Base URL 的问题。先确认.env里的TAOTOKEN_API_KEY没有多余空格或引号;再确认 Base URL 是https://taotoken.net/api,没有手动拼/v1。如果客户端配置里同时写了环境变量和硬编码,以硬编码为准,容易覆盖出错。排查方法:用 curl 直接打一次接口,看返回是不是 401。

Unknown tool: xxx:call_tool里判断的工具名和list_tools里声明的对不上。检查两处name字段是否完全一致,大小写敏感。还有一种情况是客户端缓存了旧的工具列表,重启客户端即可。

Missing required argument 'url':inputSchema里声明了required: ["url"],但客户端传参时键名写成了URL或link。JSON Schema 的键名是大小写敏感的。另外确认call_tool里读的是arguments["url"]而不是arguments.get("url")后没做空判断。

local proxy failed / reading choices:这类错误通常出现在 SSE 模式下,客户端连不上/sse端点。先确认服务真的在监听,curl http://localhost:8000/sse应该返回一个持续的事件流而不是 404。如果返回 404,检查 Starlette 路由注册顺序,Route("/sse")要在Mount("/messages/")之前。reading choices有时是客户端解析 SSE 事件格式失败,确认SseServerTransport的路径参数和客户端 URL 完全匹配。

OAuth 相关报错:如果你在客户端里配了 OAuth 流程但服务端没实现,会卡在授权环节。本地调试阶段建议先用 API Key 直连,别引入 OAuth。等工具稳定了再考虑加鉴权层。

导入错误 ModuleNotFoundError:stdio 模式下客户端拉起进程时工作目录不对。在 MCP 配置里加"cwd": "/你的项目绝对路径",或者用uv run --directory /你的项目路径 python server.py。

排查有个通用思路:先确认服务端单独能跑,再确认客户端能连上,最后确认工具能被调用。三层分开验证,比一上来就端到端调试高效得多。服务端日志一定要开,app.run的调用记录会告诉你请求到底有没有到。

6. 把工具接进真实工作流:下一步怎么走

跑通第一个工具后,你会发现 MCP Server Tool 的开发模式很统一:写业务函数、声明 schema、注册、选传输方式。真正拉开差距的是工具设计的颗粒度和错误处理。

给你几个实用建议。第一,工具粒度别太细也别太粗。一个工具只做一件事,但要把这件事做完整。比如“抓网页”就专注抓取和返回,别在里面顺便做摘要,摘要交给模型。第二,inputSchema的description写清楚,模型靠它决定什么时候调用你的工具。描述模糊的工具,模型要么不用,要么乱用。第三,错误信息要具体。raise ValueError("Missing required argument 'url'")比raise ValueError("bad input")有用得多,客户端能把具体原因反馈给模型,模型可以自我修正后重试。

传输方式的选择也有讲究。本地开发和嵌入式场景用 stdio,简单、无端口冲突;需要多客户端共享或 Web 场景用 SSE。生产环境如果工具调用量大,SSE 模式要加连接池和超时控制,别让一个慢请求拖垮整个服务。

验证环节,建议把模型对话、Coding Plan、API Keys 这几个入口都走一遍,确认你的 Key 在不同场景下都生效。接入文档里有各客户端的详细配置示例,遇到配置问题先查文档再动手改。

最后说个心态问题:MCP 生态还在快速演进,SDK 的 API 偶尔会变。遇到方法签名对不上,先看官方 SDK 的 examples 目录,比搜博客靠谱。把第一个工具跑通,后面加工具就是复制模板改业务逻辑的事。真正的门槛不在写代码,而在想清楚“这个能力该不该做成工具、怎么描述才能让模型用对”。想明白这点,你的 MCP Server Tool 才算开发到位。

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

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

立即咨询