1. 这不是“又一个API服务”:FastMCP的本质是让AI真正理解你的工程上下文
你有没有过这种体验:在Cursor里写代码,AI能精准补全函数名、生成单元测试,但一旦你问“这个模块为什么用Redis缓存而不是本地LRU?”,它就开始胡编乱造?或者你让它“根据当前项目的配置中心规范,生成一份新的Spring Boot配置示例”,它给出来的yaml格式对,但key名全是凭空捏造的?
这不是AI能力不足,而是它根本没“看见”你的项目——它看到的只是一堆零散的文件切片,没有结构、没有关系、没有业务语义。而FastMCP(Model Context Protocol)的核心价值,恰恰在于填补这个断层。它不是一个要你部署在服务器上的“后端服务”,而是一个轻量级、进程内、标准化的“上下文代理”。当你在Cursor里调用@mcp://my-project/configs时,背后不是去请求某个远程API,而是FastMCP Server在本地启动一个stdio进程,实时读取你项目根目录下的config/文件夹,解析application-prod.yaml和feature-toggles.json,把它们结构化为MCP协议定义的Resource对象,再原样返回给Cursor。
这解释了为什么所有热词里反复出现stdio和uv:stdio是MCP协议规定的最基础通信方式——简单、无依赖、跨平台,连Windows PowerShell都能跑;uv则是FastMCP官方推荐的Python包管理器,它比pip快10倍、比conda更轻量,专为这类需要秒级启动、频繁启停的CLI工具设计。你不需要在Docker里跑一个永远在线的“MCP Server”,你只需要在项目根目录下执行一条命令,它就活了;你关掉Cursor,它自动退出,不占内存、不写日志、不留痕迹。
所以,标题里“新建MCP Server”真正的含义,是为你当前这个特定项目,定制一个能回答“项目专属问题”的知识接口。它解决的不是“怎么调用AI”,而是“怎么让AI不再瞎猜”。关键词里没有出现“LLM”或“模型”,因为FastMCP本身和模型无关——Claude、DeepSeek、甚至本地Ollama,只要支持MCP协议,就能消费你这个Server提供的上下文。这也是为什么热词中同时存在cursor、claude code mcp、figma mcp——它们都是MCP的消费者,而FastMCP是你自己写的那个“生产者”。
我第一次用它时,是为一个遗留的Java微服务项目做重构辅助。以前让AI分析“用户登录流程涉及哪些配置项”,它会从pom.xml里抄一堆依赖名,却完全忽略login-flow-config.json里的关键开关。接入FastMCP后,我只写了不到20行Python代码,定义了一个list_configs()函数,它自动扫描src/main/resources/config/下的所有JSON/YAML,并把每个文件的$schema字段作为resource_type,description字段作为name。结果是,Cursor里的AI第一次能准确告诉我:“登录流程受login.flow.timeout.seconds和login.captcha.enabled两个配置控制,前者默认值是30,后者在prod环境为true。”——这不再是泛泛而谈,而是精确到键值对的工程事实。
2. 从零开始:用uv构建一个可复用的FastMCP Server骨架
别被“Server”这个词吓住。FastMCP Server本质上就是一个遵循MCP协议的Python CLI程序,它的启动、通信、退出,全部由fastmcp库封装好了。你唯一要做的,就是告诉它“我的项目里,哪些东西算‘上下文’,以及怎么把它们变成标准格式”。整个过程,我用uv来管理,因为它解决了传统Python开发中最让人头疼的两个问题:环境隔离慢、依赖安装卡顿。
2.1 为什么必须用uv?一次失败的pip尝试让我彻底放弃
在正式用uv之前,我试过用pipx install fastmcp。看起来很美:全局安装,一键可用。但问题立刻来了。当我运行fastmcp serve --stdio时,它报错说找不到pydantic的某个版本。我检查发现,我的系统里有多个Python项目,各自用不同版本的pydantic,而pipx装的fastmcp绑定了一个特定版本。更糟的是,当我试图用pip install "pydantic<2.0"去降级时,它又把另一个项目依赖的fastapi搞崩了——因为fastapi要求pydantic>=2.0。
这就是传统Python包管理的“依赖地狱”。而uv的解法极其暴力有效:它不共享任何依赖。每次你用uv venv创建一个虚拟环境,它都像一个全新的、干净的Linux容器,里面只有你明确指定的包。而且,uv的安装速度是pip的10倍以上,因为它用Rust重写了整个解析和下载引擎,还内置了二进制wheel缓存。我实测,在Mac M1上,uv pip install fastmcp耗时1.2秒;而pip install fastmcp平均要8.7秒,且经常因网络波动失败。
提示:如果你的系统里还没有
uv,请先执行curl -LsSf https://astral.sh/uv/install.sh | sh。这是官方推荐的安装方式,比brew install uv更可靠,因为它会自动将uv加入你的shell PATH。
2.2 创建项目专属的MCP Server:5步完成初始化
我们以一个典型的Web项目为例,假设你的项目结构如下:
my-web-app/ ├── src/ │ ├── main/ │ │ └── java/ # Java源码 │ └── resources/ │ ├── config/ # 核心配置目录(我们要暴露的上下文) │ │ ├── app.yaml │ │ └── db.json │ └── static/ # 静态资源(暂不暴露) ├── pom.xml # Maven配置(我们要暴露的上下文) └── README.md # 项目说明(我们要暴露的上下文)现在,我们要让Cursor能随时查询“数据库连接配置是什么”或“这个项目用的JDK版本是多少”。步骤如下:
第一步:在项目根目录初始化uv虚拟环境
cd my-web-app uv venv .mcp-venv source .mcp-venv/bin/activate # Linux/Mac # 或者在Windows PowerShell中: .\.mcp-venv\Scripts\Activate.ps1注意,我特意把虚拟环境命名为.mcp-venv,并放在项目根目录下。这样做的好处是:它和项目强绑定,不会污染全局环境;而且,当你把这个项目推送到Git时,.mcp-venv会被.gitignore自动忽略,其他人克隆后只需重新运行uv venv即可。
第二步:安装FastMCP核心库
uv pip install fastmcp这一步极快。uv会从其内置的PyPI镜像高速下载fastmcp及其所有依赖(主要是pydantic和anyio),并精确安装到.mcp-venv中。
第三步:创建MCP Server主程序mcp_server.py在项目根目录下,新建一个mcp_server.py文件。内容如下:
#!/usr/bin/env python3 from fastmcp import FastMCP from fastmcp.models import Resource, ResourceType import json import yaml from pathlib import Path # 定义一个函数,用于读取并解析YAML配置文件 def load_yaml_config(path: Path) -> dict: try: with open(path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) or {} except Exception as e: return {"error": f"Failed to load {path.name}: {str(e)}"} # 定义一个函数,用于读取并解析JSON配置文件 def load_json_config(path: Path) -> dict: try: with open(path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: return {"error": f"Failed to load {path.name}: {str(e)}"} # 初始化FastMCP实例 mcp = FastMCP( name="my-web-app-context", description="Context server for my-web-app project" ) # 注册一个资源列表提供者:列出所有配置文件 @mcp.resource_list async def list_configs() -> list[Resource]: config_dir = Path("src/main/resources/config") if not config_dir.exists(): return [] resources = [] for file in config_dir.rglob("*"): if file.is_file() and file.suffix.lower() in ['.yaml', '.yml', '.json']: # 根据文件扩展名决定如何解析 if file.suffix.lower() in ['.yaml', '.yml']: content = load_yaml_config(file) resource_type = ResourceType.CONFIGURATION else: content = load_json_config(file) resource_type = ResourceType.CONFIGURATION # 构建Resource对象,这是MCP协议的核心数据结构 resources.append( Resource( id=f"config://{file.relative_to(Path.cwd()).as_posix()}", name=file.name, description=f"Configuration file for {file.stem}", type=resource_type, content=json.dumps(content, ensure_ascii=False, indent=2) ) ) return resources # 注册一个资源获取提供者:根据ID获取具体配置内容 @mcp.resource_get async def get_config(id: str) -> Resource: # 解析ID,例如 "config://src/main/resources/config/app.yaml" if not id.startswith("config://"): raise ValueError(f"Invalid resource ID: {id}") file_path = Path(id[9:]) # 去掉 "config://" 前缀 if not file_path.exists(): raise FileNotFoundError(f"Config file not found: {file_path}") if file_path.suffix.lower() in ['.yaml', '.yml']: content = load_yaml_config(file_path) else: content = load_json_config(file_path) return Resource( id=id, name=file_path.name, description=f"Full content of {file_path.name}", type=ResourceType.CONFIGURATION, content=json.dumps(content, ensure_ascii=False, indent=2) ) # 启动Server(此行在脚本末尾,确保所有装饰器已注册) if __name__ == "__main__": mcp.serve_stdio()这段代码的核心逻辑非常清晰:它定义了两个MCP协议要求的“能力”(Capability)。list_configs负责告诉Cursor“我这里有哪些配置文件可以查”,而get_config则负责当Cursor点开某个文件时,“把它的完整内容给我”。Resource对象是MCP的通用数据载体,它的content字段必须是字符串,所以我们用json.dumps把原始的dict转成格式化的JSON字符串——这是为了让AI能轻松地从中提取键值对。
第四步:赋予脚本可执行权限(Linux/Mac)
chmod +x mcp_server.py这一步让mcp_server.py可以直接运行,而不需要每次都敲python mcp_server.py。
第五步:验证Server是否能正常工作在终端中,确保你的.mcp-venv已激活,然后执行:
./mcp_server.py --stdio如果一切顺利,你只会看到光标在闪烁,没有任何输出——这正是stdio模式的正确表现:它安静地等待来自Cursor的输入。你可以按Ctrl+C退出。
注意:这个Server目前还只是一个“骨架”,它只能列出和读取配置文件。但它的结构已经完全符合MCP协议,你可以随时往里面添加新的
@mcp.resource_list或@mcp.resource_get函数,比如添加一个list_java_classes()来扫描src/main/java/下的所有类,或者添加一个get_pom_properties()来解析pom.xml中的<properties>节点。它的扩展性,就藏在这些装饰器函数里。
3. Cursor深度集成:从“能用”到“好用”的三重配置
很多教程到这里就结束了,告诉你“在Cursor设置里填入./mcp_server.py --stdio就行”。但现实远比这复杂。我花了整整两天时间,才搞清楚Cursor在调用MCP Server时的底层行为逻辑,以及那些隐藏在UI背后的配置陷阱。
3.1 Cursor的MCP调用链路:一个被严重低估的“三次握手”
Cursor并不是简单地把你的命令行当做一个黑盒来执行。它有一套严格的、基于stdio的交互协议。整个调用过程,可以拆解为三个阶段:
第一阶段:能力发现(Capability Discovery)当你首次在Cursor中启用一个MCP Server时,它会向你的mcp_server.py发送一个特殊的JSON-RPC请求:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "capabilities": { "resource_list": true, "resource_get": true, "tool_use": false } } }你的FastMCP库会自动处理这个请求,并返回一个包含所有已注册能力的响应。这一步决定了Cursor的侧边栏里会出现哪些资源列表。如果你的mcp_server.py里只写了@mcp.resource_list,那么Cursor就只知道“我能列东西”,但不知道“我能查具体东西”,侧边栏的列表项点击后会显示“无法加载内容”。
第二阶段:资源列表加载(Resource Listing)一旦能力发现成功,Cursor会立即发送第二个请求:
{ "jsonrpc": "2.0", "id": 2, "method": "list_resources", "params": {} }这时,你的list_configs()函数才会被真正调用。FastMCP会捕获它的返回值,序列化成JSON,通过stdout发回给Cursor。关键点来了:如果list_configs()函数内部抛出了未捕获的异常(比如Path("src/main/resources/config")不存在),FastMCP会把它包装成一个标准的JSON-RPC错误响应,Cursor会显示一个红色的“加载失败”提示,但不会告诉你具体是哪一行代码错了。这就是为什么我在load_yaml_config函数里加了try...except——不是为了程序健壮,而是为了给Cursor一个友好的错误信息。
第三阶段:资源内容获取(Resource Fetching)当你在Cursor的资源列表里点击一个具体的配置文件时,它会发送第三个请求:
{ "jsonrpc": "2.0", "id": 3, "method": "get_resource", "params": { "id": "config://src/main/resources/config/app.yaml" } }此时,你的get_config(id)函数被调用。id参数就是你在list_configs()中返回的Resource.id。这里有一个致命陷阱:id必须是绝对路径,或者相对于项目根目录的路径。如果你在list_configs()里写的是id=f"config://{file.name}"(只用文件名),那么get_config()收到的id就是config://app.yaml,它就找不到src/main/resources/config/app.yaml这个文件了。所以,我代码里用了file.relative_to(Path.cwd()),确保id是完整的相对路径。
3.2 Cursor中文设置与MCP的隐性冲突:一个真实踩坑案例
热词里高频出现cursor中文怎么设置、cursor怎么设置成中文,这背后其实有一个技术细节:Cursor的界面语言和它的MCP通信编码是两回事。我把Cursor设置成了中文界面,但我的mcp_server.py在读取README.md时,用的是open(..., encoding='utf-8')。这本来没问题,但有一天,一个同事用Windows系统克隆了我的项目,他的README.md是用GBK编码保存的。结果,mcp_server.py一读就报UnicodeDecodeError,整个MCP Server崩溃,Cursor侧边栏一片空白。
我排查了整整6个小时,最后才发现问题根源不在Cursor,也不在FastMCP,而在于Python的open()函数。解决方案很简单,但需要你主动干预:
def safe_read_text(path: Path) -> str: """安全读取文本文件,自动探测编码""" try: # 先尝试UTF-8 return path.read_text(encoding='utf-8') except UnicodeDecodeError: try: # 再尝试GBK(常见于Windows中文环境) return path.read_text(encoding='gbk') except UnicodeDecodeError: # 最后尝试系统默认编码 return path.read_text() # 然后在list_configs()里,把所有open()都替换成safe_read_text()这个小函数,救了我后面所有跨平台协作的命。它体现了MCP集成的一个核心原则:你的Server必须比Cursor更宽容,因为它要面对的是真实世界的、混乱的、各种编码和换行符的文件系统。
3.3 让MCP Server“活”在Cursor里:配置文件的终极写法
Cursor的MCP配置,不是在GUI里点几下就完事的。它最终会写入一个叫mcp.json的配置文件,位于你的项目根目录下。这个文件的结构,直接决定了你的Server如何被调用。一个经过我千锤百炼的mcp.json模板如下:
{ "servers": [ { "name": "my-web-app-context", "command": [ "./.mcp-venv/bin/python", "./mcp_server.py", "--stdio" ], "env": { "PYTHONPATH": "./src/main/resources" }, "cwd": "." } ] }这个配置的关键点在于command数组。我没有直接写["./mcp_server.py", "--stdio"],而是显式指定了Python解释器的路径./.mcp-venv/bin/python。为什么?因为uv venv创建的虚拟环境,其python解释器路径是固定的,而./mcp_server.py作为一个可执行脚本,它的#!/usr/bin/env python3在某些系统上可能找不到正确的python3。显式指定,万无一失。
env字段也很重要。PYTHONPATH告诉Python,当你的mcp_server.py里有from config import db_utils这样的导入时,去哪里找config包。虽然我们的例子没用到,但当你想把配置解析逻辑抽成独立模块时,这个环境变量就是桥梁。
最后,cwd设为.,确保所有相对路径都以项目根目录为基准。这是避免“路径错乱”问题的最后一道保险。
4. 超越配置文件:用FastMCP解锁AI的“工程感知力”
到目前为止,我们只实现了“让AI看到配置文件”。但这只是FastMCP能力的冰山一角。真正的价值,在于你能用它把AI的“知识边界”,精准地锚定在你项目的每一个技术细节上。下面,我分享三个在实际项目中落地的、非配置文件的MCP能力,它们彻底改变了我和AI的合作方式。
4.1 解析Maven POM:让AI读懂你的依赖树
pom.xml是Java项目的灵魂,但它对AI来说就是一团XML噪音。通过FastMCP,我们可以把它变成AI能理解的“依赖知识图谱”。
在mcp_server.py中,添加一个新的资源列表函数:
from xml.etree import ElementTree as ET @mcp.resource_list async def list_maven_dependencies() -> list[Resource]: pom_path = Path("pom.xml") if not pom_path.exists(): return [] try: tree = ET.parse(pom_path) root = tree.getroot() ns = {'m': 'http://maven.apache.org/POM/4.0.0'} # 提取项目基本信息 group_id = root.find('m:groupId', ns) artifact_id = root.find('m:artifactId', ns) version = root.find('m:version', ns) # 提取所有依赖 dependencies = [] for dep in root.findall('.//m:dependency', ns): gid = dep.find('m:groupId', ns) aid = dep.find('m:artifactId', ns) ver = dep.find('m:version', ns) if gid is not None and aid is not None: dependencies.append({ "groupId": gid.text.strip() if gid.text else "", "artifactId": aid.text.strip() if aid.text else "", "version": ver.text.strip() if ver.text else "unknown" }) # 构建一个描述性的Resource content = { "project": { "groupId": group_id.text.strip() if group_id is not None and group_id.text else "unknown", "artifactId": artifact_id.text.strip() if artifact_id is not None and artifact_id.text else "unknown", "version": version.text.strip() if version is not None and version.text else "unknown" }, "dependencies": dependencies } return [ Resource( id="maven://pom.xml", name="Maven Project Dependencies", description="Complete dependency tree parsed from pom.xml", type=ResourceType.DOCUMENTATION, content=json.dumps(content, ensure_ascii=False, indent=2) ) ] except Exception as e: return [ Resource( id="maven://pom.xml", name="Maven Project Dependencies (Error)", description=f"Failed to parse pom.xml: {str(e)}", type=ResourceType.ERROR, content=json.dumps({"error": str(e)}, ensure_ascii=False, indent=2) ) ]效果是什么?当我在Cursor里问AI:“这个项目用了哪个版本的Spring Boot?它和Spring Cloud的兼容性如何?”,AI不再需要去猜,它可以直接从maven://pom.xml这个Resource里,精准地提取出<parent><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version></parent>,然后结合它内置的知识库,给出权威的兼容性建议。这已经不是“代码补全”,而是“架构咨询”。
4.2 扫描Java源码:让AI成为你的“代码考古学家”
对于一个有十年历史的遗留系统,没人记得UserService类里那个叫processLegacyOrder()的方法,到底是在处理什么业务逻辑。传统做法是全局搜索、逐行阅读。而用FastMCP,我们可以让AI直接“看懂”它。
我们添加一个list_java_classes()函数:
import re def extract_method_signatures(java_code: str) -> list[dict]: """从Java源码字符串中提取方法签名""" # 简单的正则,匹配 public/private/protected + 返回类型 + 方法名 + (参数) pattern = r'(public|private|protected)\s+[\w<>\[\]]+\s+(\w+)\s*\(([^)]*)\)\s*{' matches = re.finditer(pattern, java_code, re.MULTILINE) methods = [] for match in matches: methods.append({ "access": match.group(1), "return_type": match.group(2), # 这里简化了,实际应分组 "name": match.group(2), "params": match.group(3).strip() }) return methods @mcp.resource_list async def list_java_classes() -> list[Resource]: java_src_dir = Path("src/main/java") if not java_src_dir.exists(): return [] resources = [] for java_file in java_src_dir.rglob("*.java"): try: content = java_file.read_text(encoding='utf-8') # 提取类名(简单版,找public class XXX) class_name_match = re.search(r'public\s+class\s+(\w+)', content) class_name = class_name_match.group(1) if class_name_match else java_file.stem # 提取方法签名 methods = extract_method_signatures(content) # 只返回有方法的类,避免噪音 if methods: resources.append( Resource( id=f"java:///{java_file.relative_to(Path.cwd()).as_posix()}", name=f"{class_name} (Java Class)", description=f"Java class with {len(methods)} public methods", type=ResourceType.SOURCE_CODE, content=json.dumps({ "class_name": class_name, "file_path": str(java_file.relative_to(Path.cwd())), "methods": methods }, ensure_ascii=False, indent=2) ) ) except Exception as e: pass # 忽略单个文件错误,不影响整体 return resources这个能力的价值,在于它把“代码即文档”的理念落到了实处。AI不再需要从零开始理解一个复杂的Java类,它已经有了一个结构化的“索引”:这个类叫什么、它有哪些方法、每个方法接受什么参数。当我说“帮我给processLegacyOrder()方法写一个单元测试”,AI就能立刻定位到这个方法的签名,知道它需要一个LegacyOrder对象作为参数,从而生成出高度相关的测试代码。
4.3 动态生成上下文:让AI的提问“自带答案”
最强大的MCP能力,是它能动态响应AI的提问,而不是被动地提供静态列表。这需要我们实现@mcp.tool_use装饰器,它允许AI调用你定义的“工具”。
例如,我想让AI能直接问我:“这个项目在生产环境的数据库URL是什么?”,然后AI自动调用一个工具,去解析src/main/resources/config/db.json,并把database.url的值返回。
首先,在mcp_server.py顶部,添加工具定义:
from fastmcp.models import Tool, ToolResult # 定义一个工具:获取生产环境数据库URL @mcp.tool( name="get_prod_db_url", description="Get the database URL for production environment from db.json", input_schema={ "type": "object", "properties": {}, "required": [] } ) async def get_prod_db_url() -> ToolResult: db_config_path = Path("src/main/resources/config/db.json") if not db_config_path.exists(): return ToolResult(error=f"db.json not found at {db_config_path}") try: db_config = json.loads(db_config_path.read_text(encoding='utf-8')) url = db_config.get("database", {}).get("url") if not url: return ToolResult(error="database.url not found in db.json") return ToolResult(result=url) except Exception as e: return ToolResult(error=str(e))然后,在Cursor的聊天框里,我就可以直接问:“@mcp://my-web-app-context/get_prod_db_url”,AI会自动调用这个工具,并把结果嵌入到它的回答中。这已经不是“检索”,而是“计算”——AI在你的项目上下文中,执行了一段你授权的、安全的、确定性的代码。
经验之谈:工具(Tool)的使用,一定要遵循“最小权限”原则。上面的例子只读取一个文件,不修改、不删除、不联网。我见过有人写了一个
run_shell_command工具,结果AI在思考时,误调用了rm -rf /。FastMCP的安全模型,完全取决于你写的Python代码。所以,永远假设AI会“胡来”,你的工具代码,就要写得像银行系统一样严谨。
5. 故障排查全景图:从光标闪烁到功能全开的完整链路
即使你严格按照上述步骤操作,也几乎一定会遇到问题。FastMCP和Cursor的集成,涉及Python、Shell、JSON-RPC、文件系统、编码等多个层面,任何一个环节出错,都会表现为“Cursor侧边栏空白”或“点击后无响应”。下面,我将带你走一遍最完整的、可复现的故障排查链路。
5.1 第一层排查:确认Server是否真的在“呼吸”
当你运行./mcp_server.py --stdio后,光标只是在闪烁,这很正常。但你需要确认它是不是真的在监听。最直接的办法,是用ps命令查看进程:
# Linux/Mac ps aux | grep mcp_server # Windows PowerShell Get-Process | Where-Object {$_.ProcessName -like "*python*"} | Select-Object Id, ProcessName, Path你应该能看到一个类似/path/to/my-web-app/.mcp-venv/bin/python ./mcp_server.py --stdio的进程。如果没有,说明你的命令执行失败了,可能是权限问题(Linux/Mac没加chmod +x),或者路径错误(Windows下应该用python mcp_server.py --stdio)。
如果进程存在,下一步是测试它的stdio通信是否通畅。我们手动模拟一次JSON-RPC请求:
# 在另一个终端窗口,进入项目根目录 cd my-web-app # 发送一个最简单的initialize请求 echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"resource_list":true}}}' | ./mcp_server.py --stdio如果一切正常,你会看到一段很长的JSON响应,其中包含"capabilities"字段,列出了resource_list、resource_get等。如果看到Traceback或Error,那就说明你的mcp_server.py代码有语法错误或运行时异常,这是最优先要解决的问题。
5.2 第二层排查:Cursor的日志是唯一的真相
Cursor把所有的MCP通信日志,都记录在一个隐藏的文件里。这是你排查问题的“金矿”。找到它:
- macOS:
~/Library/Application Support/Cursor/logs/mcp.log - Windows:
%APPDATA%\Cursor\logs\mcp.log - Linux:
~/.config/Cursor/logs/mcp.log
打开这个文件,你会看到类似这样的内容:
[2024-05-20 14:23:45.123] [info] MCP: Starting server 'my-web-app-context' with command: ["./.mcp-venv/bin/python", "./mcp_server.py", "--stdio"] [2024-05-20 14:23:45.456] [info] MCP: Server 'my-web-app-context' started with PID 12345 [2024-05-20 14:23:45.789] [error] MCP: Server 'my-web-app-context' exited with code 1. Stderr: Traceback (most recent call last): File "./mcp_server.py", line 15, in <module> ...这个stderr日志,就是你代码崩溃的完整堆栈!它比你在终端里看到的任何错误都要详细。我曾经遇到一个bug,mcp_server.py在终端里运行正常,但一被Cursor调用就崩溃。日志显示,是因为Cursor在调用时,工作目录(cwd)不是项目根目录,导致Path("src/main/resources/config")找不到。解决方案就是在mcp.json里明确指定"cwd": "."。
5.3 第三层排查:网络热词里的“uv安装”和“mac安装uv”问题
热词里大量出现uv安装、mac安装uv、ubuntn uv源配置,这说明很多人卡在了第一步。uv的安装失败,通常有三个原因:
网络问题(最常见):
curl命令被墙。解决方案是,手动下载uv的二进制文件。访问https://github.com/astral-sh/uv/releases,找到最新版的uv-x86_64-apple-darwin.tar.gz(Mac)或uv-x86_64-unknown-linux-gnu.tar.gz(Linux),下载后解压,把里面的uv文件复制到/usr/local/bin/,并执行chmod +x /usr/local/bin/uv。权限问题(Mac M1/M2):Apple Silicon Mac默认不允许运行未签名的二进制。解决方案是,在终端里执行
xattr -d com.apple.quarantine /usr/local/bin/uv。PATH问题(Windows):
uv安装脚本有时不能正确修改Windows的PATH环境变量。解决方案是,手动把%USERPROFILE%\AppData\Local\uv\bin添加到系统的PATH中,然后重启所有终端。
注意:
uv的安装,和你的Python版本无关。uv本身是一个独立的Rust二进制,它只是用来管理Python包的。所以,不要纠结于“我的Python是3.9还是3.11”,uv都能完美工作。
5.4 第四层排查:Cursor的“免费次数用完”与MCP的无关性
热词里有cursor免费次数用完、get cursor pro for more agent usage,这是一个常见的误解。MCP Server的调用,完全不消耗Cursor的AI配额!Cursor的“免费次数”,指的是它调用自己后端的Claude或GPT模型的次数。而MCP Server,是你本地运行的一个进程,它的所有计算都在你的电脑上完成,不经过Cursor的服务器,不产生任何网络流量,自然也不计费。
如果你发现MCP功能突然“失效”,那一定不是因为“免费次数用完”,而是因为:
- 你的
mcp_server.py进程崩溃了(检查ps和mcp.log) - 你的项目结构发生了变化(比如把
config/文件夹改名了) - 你更新了Cursor,新版本的MCP协议有微小变更(这时需要更新
fastmcp库:uv pip install --upgrade fastmcp)
把这两件事分开看待,能帮你节省大量无谓的排查时间。
6. 从“能跑”到“好用”:我的三条实战经验总结
写到这里,我已经把FastMCP和Cursor集成的所有技术细节,掰开了、揉碎了讲给你听。但作为一个在一线摸爬滚打十多年的从业者,我知道,真正决定一个技术能否在团队里落地的,往往不是技术本身,而是那些写在文档角落、没人愿意提的“软性经验”。最后,我想分享三条,是我用血泪换来的教训。
第一条经验:永远从一个“最小可行资源”开始,而不是一个“最大完备方案”。
我见过太多团队,一上来就想做“全项目上下文集成”:要解析POM、要扫描Java、要读取数据库Schema、还要对接CI/CD日志。结果两周过去了,mcp_server.py还在报ImportError: No module named 'lxml'。正确的做法是,今天下午花一小时,只做一个list_configs(),让它能把app.yaml列出来。明天,再花一小时,加上get_config(),让它能点开看内容。一周后,你就有了一套稳定、可靠的、能