1. 从零跑通 FastMCP:本地工具服务接入 Cline MCP 的完整链路
FastMCP 是 Python 生态里把普通函数变成 MCP 工具最省事的 SDK,它把 stdio、SSE、Streamable HTTP 三种传输方式都封装好了,你只要写几个带类型注解的函数,加个@mcp.tool()装饰器,就能被 Cline、Cursor 这类支持 MCP 的客户端直接调用。这篇面向的是已经写过一点 Python、想把本地业务逻辑(比如内存里的用户表、学校表)暴露给 AI 编程助手的开发者,尤其是那些在 Cline 里配 MCP 时卡在command路径、cwd工作目录、ModuleNotFoundError这几个坑上的人。我会用一个tools_demo项目做例子,从service/store.py的内存数据开始,到mcp/server.py注册工具,再到 Cline 的cline_mcp_settings.json配置,最后用tools-agent命令行验证整条链路。整个过程不需要联网调远程服务,纯本地 stdio 就能跑通,适合当作 python AI 工程里 MCP SDK 的第一块垫脚石。
我试过把 MCP 服务直接塞进 Cline 的全局配置里,结果因为 Python 解释器指向了系统全局而不是虚拟环境,报了一晚上的No module named 'mcp'。后来把command改成.venv/Scripts/python.exe的绝对路径,问题才消失。所以下面每个路径我都会写清楚,你照着改成本机目录就行。
先明确一下这个 demo 的定位:它不是一个生产级的用户管理系统,而是一个“最小可验证”的 MCP 服务。service/store.py里用线程锁保护了一个内存字典,预置了两个学校和一条用户记录;mcp/server.py通过 FastMCP 把user_list、user_create、school_list三个函数注册成工具。Cline 作为 MCP 客户端,通过 stdio 启动这个 Python 进程,读取工具列表,然后在对话里按需调用。整条链路的关键在于:Cline 启动的 Python 进程必须能 import 到service.store,而service目录在项目根目录下,所以cwd必须指向项目根,且mcp/目录下不能有__init__.py,否则会和 PyPI 的mcp包冲突。
2. TaoToken 前置:给 MCP 工具调用准备一个稳定的模型入口
MCP 本身只负责“工具怎么被调用”,但真正决定 AI 助手能不能理解你的自然语言、决定调哪个工具、传什么参数的,是背后的模型。Cline 默认会让你填一个 OpenAI 兼容的 API 地址和 Key,如果你直接用官方地址,在国内网络环境下经常遇到超时或者 401。TaoToken 在这里的角色就是一个 OpenAI 兼容的模型网关,它提供/v1/chat/completions接口,Cline 里填上 Base URL 和 API Key 就能用,模型 ID 可以选gpt-5.1这类支持 function calling 的模型。
你需要先拿到两样东西:API Key 和 Base URL。API Key 在控制台的 API Keys 页面创建,Base URL 固定是https://taotoken.net/api,注意这个地址不带/v1后缀,Cline 的 OpenAI 配置里通常会自动补/v1,如果它没补,你就在 Base URL 后面手动加上/v1。模型 ID 填gpt-5.1或者你在模型对话页面看到的其他可用模型名。
这里有个容易混淆的点:TaoToken 的 API 地址和官网地址是两个不同的域名。官网是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,用来注册和看文档;API 是https://taotoken.net/api,用来给 Cline 发请求。不要把官网地址填进 Cline 的 Base URL,否则会返回 HTML 而不是 JSON。
如果你只是想让 Cline 能调用本地 MCP 工具,其实模型用哪个都行,只要支持 function calling。但如果你后面要跑tools-agent那个命令行客户端,它内部也是用 OpenAI 兼容接口发请求的,所以同样需要配好LLM_BASE_URL和LLM_API_KEY。我建议你先把 TaoToken 的 Key 拿到手,后面 Cline 和命令行客户端共用同一个 Key,省得来回切换。
另外,Cline 的 MCP 配置和模型配置是分开的。MCP 配置在cline_mcp_settings.json里,模型配置在 Cline 的设置面板里。很多人第一次配的时候只配了 MCP,结果 Cline 能列出工具但对话时模型不响应,就是因为模型那栏没填。所以这一节先把模型入口准备好,下一节再写 MCP 的服务端和客户端配置。
3. 可复制配置:FastMCP 服务端 + Cline MCP 客户端
这一节是全文的核心,我会把service/store.py、mcp/server.py、requirements.txt和 Cline 的cline_mcp_settings.json四份配置都写出来,你直接复制改路径就能用。
先看项目结构,这是所有路径的基准:
tools_demo/ ├── README.md ├── pyproject.toml ├── requirements.txt ├── .gitignore ├── service/ │ ├── __init__.py │ └── store.py └── mcp/ └── server.py注意mcp/目录下没有__init__.py,这是故意的。因为 PyPI 上有个同名的mcp包,如果你在mcp/里放了__init__.py,Python 会把这个目录当成一个包,import 的时候可能优先加载你的目录而不是 PyPI 的包,导致from mcp.server.fastmcp import FastMCP失败。
requirements.txt内容如下:
mcp[cli]>=1.2.0 pydantic>=2.0.0 email-validator>=2.0.0service/store.py是内存数据层,用 Pydantic 模型定义 User 和 School,用线程锁保护字典操作。完整代码如下:
"""Shared in-memory store for API and MCP (single process each; same module = one store per process).""" from __future__ import annotations import uuid from threading import Lock from typing import Any from pydantic import BaseModel, EmailStr, Field class User(BaseModel): id: str name: str email: EmailStr | None = None school_id: str | None = Field(default=None) class School(BaseModel): id: str name: str region: str | None = None class InMemoryStore: def __init__(self) -> None: self._lock = Lock() self._users: dict[str, dict[str, Any]] = {} self._schools: dict[str, dict[str, Any]] = {} self._seed() def _seed(self) -> None: s1 = str(uuid.uuid4()) s2 = str(uuid.uuid4()) self._schools[s1] = {"id": s1, "name": "North Institute", "region": "US-West"} self._schools[s2] = {"id": s2, "name": "East Academy", "region": "EU-Central"} u1 = str(uuid.uuid4()) self._users[u1] = { "id": u1, "name": "Ada Lovelace", "email": "ada@example.com", "school_id": s1, } def list_users(self) -> list[User]: with self._lock: return [User.model_validate(u) for u in self._users.values()] def get_user(self, user_id: str) -> User | None: with self._lock: raw = self._users.get(user_id) return User.model_validate(raw) if raw else None def create_user(self, name: str, email: str | None, school_id: str | None) -> User: if school_id is not None and school_id not in self._schools: raise ValueError("school_id does not exist") uid = str(uuid.uuid4()) row = {"id": uid, "name": name, "email": email, "school_id": school_id} with self._lock: self._users[uid] = row return User.model_validate(row) def update_user( self, user_id: str, name: str | None, email: str | None, school_id: str | None, ) -> User | None: with self._lock: row = self._users.get(user_id) if not row: return None if school_id is not None and school_id not in self._schools: raise ValueError("school_id does not exist") if name is not None: row["name"] = name if email is not None: row["email"] = email if school_id is not None: row["school_id"] = school_id return User.model_validate(dict(row)) def delete_user(self, user_id: str) -> bool: with self._lock: return self._users.pop(user_id, None) is not None def list_schools(self) -> list[School]: with self._lock: return [School.model_validate(s) for s in self._schools.values()] def get_school(self, school_id: str) -> School | None: with self._lock: raw = self._schools.get(school_id) return School.model_validate(raw) if raw else None def create_school(self, name: str, region: str | None) -> School: sid = str(uuid.uuid4()) row = {"id": sid, "name": name, "region": region} with self._lock: self._schools[sid] = row return School.model_validate(row) def update_school( self, school_id: str, name: str | None, region: str | None, ) -> School | None: with self._lock: row = self._schools.get(school_id) if not row: return None if name is not None: row["name"] = name if region is not None: row["region"] = region return School.model_validate(dict(row)) def delete_school(self, school_id: str) -> bool: with self._lock: if school_id not in self._schools: return False for u in self._users.values(): if u.get("school_id") == school_id: u["school_id"] = None del self._schools[school_id] return True store = InMemoryStore()mcp/server.py是 FastMCP 服务端,注册三个工具,默认 stdio 传输:
""" MCP server: tools call service/store.py in-process. Run from project root: python mcp/server.py Do not add mcp/__init__.py so the PyPI `mcp` package imports correctly. """ from __future__ import annotations import argparse import os import sys from pathlib import Path from typing import Any from mcp.server.fastmcp import FastMCP _root = Path(__file__).resolve().parent.parent if str(_root) not in sys.path: sys.path.insert(0, str(_root)) from service.store import store mcp = FastMCP( "tools-demo-bridge", json_response=True, ) @mcp.tool() def user_list() -> list[dict[str, Any]]: """List all users.""" return [u.model_dump(mode="json") for u in store.list_users()] @mcp.tool() def user_create(name: str, email: str | None = None, school_id: str | None = None) -> dict[str, Any]: """Create a user. Optional email and school_id (must reference an existing school).""" try: u = store.create_user(name, email, school_id) except ValueError as e: return {"ok": False, "detail": str(e)} return u.model_dump(mode="json") @mcp.tool() def school_list() -> list[dict[str, Any]]: """List all schools.""" return [s.model_dump(mode="json") for s in store.list_schools()] def main() -> None: parser = argparse.ArgumentParser(description="MCP server for tools_demo") parser.add_argument( "--transport", default=os.environ.get("MCP_TRANSPORT", "stdio"), choices=("stdio", "streamable-http", "sse"), help="MCP transport (default: stdio)", ) args = parser.parse_args() mcp.run(transport=args.transport) if __name__ == "__main__": main()Cline 的 MCP 配置文件是cline_mcp_settings.json,在 Cline 面板里点 MCP Servers 再点 Configure 就能打开。Windows 路径用双反斜杠或者正斜杠都行,我习惯用正斜杠避免转义问题:
{ "mcpServers": { "tools-demo": { "command": "C:/python-project/tools_demo/.venv/Scripts/python.exe", "args": [ "C:/python-project/tools_demo/mcp/server.py" ], "cwd": "C:/python-project/tools_demo", "env": {} } } }Linux 或 macOS 下command改成/path/to/tools_demo/.venv/bin/python,args和cwd同理。三个字段缺一不可:command指向虚拟环境里的 Python,args指向 server.py 的绝对路径,cwd指向项目根目录。cwd决定了 Python 进程的工作目录,server.py里用Path(__file__).resolve().parent.parent把项目根插进sys.path,所以即使cwd不对,只要args路径对,import 也能成功。但为了保险,cwd还是填项目根。
如果你用的是 Cline 的 MCP 市场安装方式,它可能会把配置写到全局的cline_mcp_settings.json里,路径在 VS Code 的settings.json同目录。不管哪种方式,核心就是这三个字段。
4. 验证请求:从手动启动到 Cline 对话调用
配置写完之后,先别急着在 Cline 里点重载,先用命令行手动启动一次,确认服务端本身没问题。这一步能帮你把“服务端错误”和“客户端配置错误”分开。
进入项目目录,激活虚拟环境,安装依赖:
cd /path/to/tools_demo python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txtWindows 下激活命令是.venv\Scripts\activate。安装完成后,直接运行服务端:
python mcp/server.py如果 stdio 传输正常,你会看到进程挂起,没有报错输出,光标停在下一行等待输入。这说明 FastMCP 已经启动,正在通过标准输入输出等待 MCP 协议消息。按 Ctrl+C 退出。
如果你想验证工具注册是否成功,可以加--transport streamable-http启动一个 HTTP 服务:
python mcp/server.py --transport streamable-http然后用 curl 请求工具列表:
curl -X POST http://127.0.0.1:8000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'返回的 JSON 里应该能看到user_list、user_create、school_list三个工具,每个工具的inputSchema里包含参数类型和必填项。这一步验证的是 FastMCP 服务端本身,和 Cline 无关。
接下来回到 Cline,打开 MCP Servers 面板,点 Restart 或者 Reload。如果配置正确,tools-demo旁边会显示一个绿色的圆点,展开后能看到三个工具。如果显示红色或者一直转圈,看 Cline 的输出面板,里面会有 stderr 日志。
在 Cline 的对话里输入:
列出 tools-demo 里的所有用户,然后创建一个新用户,名字叫 Grace Hopper,邮箱 grace@example.com。Cline 会先调用user_list,返回 Ada Lovelace 那条记录,然后调用user_create,传入 name 和 email。如果school_id不传,创建出来的用户school_id是 null。你可以在对话里继续问“现在有哪些学校”,它会调用school_list返回两个预置学校。
这里有个细节:Cline 调用工具时,模型需要支持 function calling。如果你在 Cline 里配的模型不支持,它会直接把工具描述当普通文本处理,不会真正发起调用。所以第 2 节里让你准备 TaoToken 的 Key,就是为了确保模型这一侧没问题。
如果你还想验证更复杂的链路,可以用tools-agent那个命令行客户端。它同时连接本地的tools_demoMCP 和远程的 Playwright MCP,然后让模型自己决定调哪个工具。配置在.env里:
LLM_BASE_URL=https://taotoken.net/api/v1 LLM_API_KEY=你的Key LLM_MODEL=gpt-5.1 TOOLS_DEMO_ROOT=C:/python-project/tools_demo PLAYWRIGHT_MCP_ENABLED=true然后运行:
tools-agent "List users from the demo using tools, then summarize available tools."它会先连接tools_demo的 stdio 服务,再尝试连接 Playwright 的远程 MCP,最后把工具列表转成 OpenAI function schema 发给模型。模型返回 tool_calls 后,客户端通过ClientSessionGroup.call_tool执行,把结果塞回 messages 继续下一轮。这个循环最多跑max_tool_rounds次,默认 8 次。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节列的都是真实会遇到的报错,每个都给出定位方法和修复动作。
报错一:ModuleNotFoundError: No module named 'mcp'
这是最高频的。原因通常是 Cline 启动的 Python 不是虚拟环境里的那个,或者虚拟环境里没装mcp[cli]。先确认command字段指向的是.venv/Scripts/python.exe(Windows)或.venv/bin/python(Linux/macOS),而不是系统全局的python。然后在项目根目录下用同一个解释器执行:
.venv/Scripts/python.exe -c "import mcp; print(mcp.__file__)"如果打印出.venv/Lib/site-packages/mcp/__init__.py,说明解释器和包都对。如果报错,重新pip install -r requirements.txt。还有一个隐蔽原因:mcp/目录下不小心放了__init__.py,导致 Python 把项目里的mcp目录当成包,而不是 PyPI 的mcp。删掉那个文件即可。
报错二:local proxy failed或Connection refused
这个通常出现在 Cline 尝试连接远程 MCP 的时候。如果你只配了本地 stdio 的tools-demo,不应该出现这个错。检查cline_mcp_settings.json里有没有多余的url字段,本地 stdio 服务不需要url,只需要command、args、cwd。如果你确实配了远程 MCP,确认那个远程地址在浏览器里能访问,且返回的是 MCP 协议响应而不是 HTML 登录页。
报错三:reading 'choices'或Cannot read properties of undefined (reading 'choices')
这是模型接口返回的 JSON 结构不对。Cline 期望的是 OpenAI 格式的{"choices": [{"message": {...}}]},但如果 Base URL 填错,比如填成了官网地址而不是 API 地址,返回的可能是 HTML 或者{"error": "..."}。检查 Cline 的模型配置里 Base URL 是不是https://taotoken.net/api或者https://taotoken.net/api/v1,Key 是不是以sk-开头。如果用的是tools-agent,检查.env里的LLM_BASE_URL有没有带/v1。
报错四:OAuth相关错误
有些 MCP 客户端在连接远程服务时会尝试 OAuth 流程,如果你的服务端没有实现 OAuth,就会卡住。本地 stdio 服务不涉及 OAuth,所以如果你看到 OAuth 报错,说明 Cline 在尝试连接一个远程 MCP。检查cline_mcp_settings.json里是不是混入了远程服务的配置,或者 Cline 的 MCP 市场自动添加了某个需要 OAuth 的服务。把不需要的条目删掉,只保留tools-demo。
报错五:工具调用返回{"ok": false, "detail": "school_id does not exist"}
这是业务逻辑错误,不是配置错误。user_create里传了一个不存在的school_id,store.create_user会抛ValueError,被捕获后返回这个 JSON。先调school_list拿到真实的学校 ID,再传进去。这个设计是故意的,让模型能根据错误信息自我纠正。
报错六:Cline 里工具列表为空
如果tools-demo显示绿色但展开没有工具,检查server.py里@mcp.tool()装饰器有没有漏掉,或者函数有没有被if __name__ == "__main__"之外的代码覆盖。另外,FastMCP 的工具注册是在模块加载时完成的,如果server.py在 import 阶段就抛异常,工具列表也会空。手动运行python mcp/server.py看有没有 traceback。
6. 语义一致 CTA:把 MCP 工具接进你的日常编码流
跑通这个 demo 之后,你手里就有了一套可复制的模板:service/store.py换成你自己的业务逻辑,mcp/server.py里加几个@mcp.tool()函数,Cline 配置里改一下路径,就能让 AI 助手直接操作你的本地数据。下一步可以试试把user_create换成调用真实数据库,或者把school_list换成查询内部 API。
如果你在配 Cline 的模型时还没填 Key,可以去 TaoToken 控制台创建一个:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,然后在 API Keys 页面生成 Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Base URL 用https://taotoken.net/api,模型 ID 填gpt-5.1。如果你更习惯用 Claude Code 那套工具链,可以看接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有针对不同客户端的配置示例。
想先验证模型对话是否正常,可以直接在模型对话页面发一条消息:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你打算长期用 MCP 做 Agent 开发,Coding Plan 里包含了更稳定的调用额度:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后提醒一句:mcp/目录下千万别放__init__.py,这个坑我踩过两次,每次都要花十分钟才想起来。