40 行写个 MCP 服务器,接进 OpenCode 跑本地模型:6 个坑实测

想让 AI 编程助手查你自己的笔记、内部文档或数据库,最通用的接法是写一个 MCP 服务器:写一次,Claude Code、Cursor、OpenCode 这类支持 MCP 的客户端都能用。今年 7 月 MCP 官方 Python SDK 发布 2.0(目前是 2.3.0),入门写法变了,网上大部分教程的第一行代码已经跑不通。
先弄清楚:MCP 服务器就是给 Agent 用的「插件接口」
MCP(Model Context Protocol)规定了 AI 应用和外部工具之间怎么对话。服务器一侧声明三类东西:tool(模型可以主动调用的函数)、resource(可读取的数据)、prompt(预设提示词模板)。客户端(OpenCode、Claude Code 等)启动时把工具列表告诉模型,模型决定什么时候调用、传什么参数。
Python SDK 2.x 的入口叫 MCPServer。官方「What's new in v2」页面写得很直接:高层服务器类 FastMCP 改名为 MCPServer,旧的导入路径「是直接删掉,而不是标记弃用」。函数的类型标注就是参数的 JSON Schema,docstring 就是给模型看的工具说明,不用手写协议代码。
MCP 工具的说明文字和返回内容,写的是给模型看的接口文档:模型只能看到你给它的那一行。
开始前准备好这些
- 适合谁:会一点 Python,想让编程 Agent 访问自己数据的开发者
- 软件:Python 3.11+(本文 3.13.5)、uv 0.13.1、Node.js(本文 20.19.2);mcp 2.3.0(PyPI,10 月 2 日发布)、@modelcontextprotocol/inspector 2.11.0、opencode-ai 1.19.3、llama.cpp b11748
- 模型:unsloth/Qwen3.5-4B-GGUF 的 Q4_K_M 量化,文件约 2.7 GB
- 机器:本文实测为 8 核 CPU、15 GB 内存、无显卡的 Linux 云主机
- 花费:0 元,不需要注册任何账号
五步跑通:写服务器 → 内存测试 → Inspector → 接 OpenCode → 让 Agent 干活
第 1 步:建项目,装 SDK。
uv init notes-mcp && cd notes-mcp
uv add "mcp[cli]==2.3.0"
mkdir notes # 往里放几篇 .md 笔记
第 2 步:写 server.py。三个工具:按关键词搜、读全文、新建笔记。
from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
NOTES = Path(__file__).parent / "notes"
mcp = MCPServer("notes")
@mcp.tool()
def search_notes(keyword: str) -> list[str]:
"""按关键词搜索笔记,返回命中的文件名和那一行内容。"""
hits = []
for f in sorted(NOTES.glob("*.md")):
for line in f.read_text(encoding="utf-8").splitlines():
if keyword.lower() in line.lower():
hits.append(f"{f.name}: {line.strip()}")
return hits
@mcp.tool()
def read_note(name: str) -> str:
"""读取一条笔记全文。name 是不带 .md 的文件名,比如 weekly-1019。"""
path = NOTES / f"{name.removesuffix('.md')}.md"
if not path.exists():
raise ToolError(f"没有 {path.name},先用 search_notes 找文件名")
return path.read_text(encoding="utf-8")
@mcp.tool()
def add_note(name: str, content: str) -> str:
"""新建一条笔记。name 只能用字母、数字和短横线。"""
if not name.replace("-", "").isalnum():
raise ToolError("name 只能包含字母、数字和短横线")
path = NOTES / f"{name}.md"
if path.exists():
raise ToolError(f"{path.name} 已存在,换个名字")
path.write_text(content, encoding="utf-8")
return f"已保存 {path.name}({len(content)} 字)"
if __name__ == "__main__":
mcp.run() # 默认走 stdio
第 3 步:不接 AI,先用内存客户端测。v2 的 Client 可以直接传服务器对象,不起进程、不占端口:
import anyio
from mcp import Client
from server import mcp
async def main():
async with Client(mcp) as c:
print(c.protocol_version)
r = await c.call_tool("search_notes", {"keyword": "MCP"})
print(r.structured_content)
anyio.run(main)
我们这里输出协议版本 2026-07-28,搜索结果 {'result': ['weekly-1019.md: - 下周上线 MCP 笔记服务']},整个脚本 0.76 秒跑完。
第 4 步:用 MCP Inspector 命令行模式走一遍真实的 stdio 连接。
npx -y @modelcontextprotocol/inspector@2.11.0 --cli \
uv run --directory $PWD server.py --method tools/list
npx -y @modelcontextprotocol/inspector@2.11.0 --cli \
uv run --directory $PWD server.py \
--method tools/call --tool-name search_notes --tool-arg keyword=rsync
第一条列出三个工具和自动生成的 inputSchema、outputSchema;第二条返回 "isError": false 和命中的那行。去掉 --cli 会打开网页版 Inspector(本文未实测网页版)。
第 5 步:接进 OpenCode,用本地模型驱动。先起 llama.cpp 服务(注意后两个参数,原因见下面的坑):
./llama-server -m Qwen3.5-4B-Q4_K_M.gguf --jinja --reasoning off \
-c 65536 -np 1 -t 8 --port 8080 --alias qwen3.5-4b
在项目目录建 opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"llama.cpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llama-server (local)",
"options": { "baseURL": "http://127.0.0.1:8080/v1" },
"models": { "qwen3.5-4b": { "limit": { "context": 65536, "output": 8192 } } }
}
},
"model": "llama.cpp/qwen3.5-4b",
"small_model": "llama.cpp/qwen3.5-4b",
"mcp": {
"notes": {
"type": "local",
"command": ["uv", "run", "--directory", "/绝对路径/notes-mcp", "server.py"]
}
}
}
small_model 是 OpenCode 生成会话标题等小任务用的模型,也指向本地,避免它去调用默认的云端模型。opencode mcp list 显示 ✓ notes connected 就算接上了。MCP 工具在 OpenCode 里会带上服务器名前缀,比如 notes_search_notes。
实测:4B 本地模型能正确调工具,慢在「读题」
10 月 20 日,我们用 opencode run "…" < /dev/null 跑了下面几组任务:
| 任务 | 工具调用 | 耗时 | 结果 |
|---|---|---|---|
| 搜 MCP 相关笔记,问负责人(只有搜索工具) | search_notes ×1 | 86 秒 | 答错:说没找到负责人 |
| 同一问题(加了 read_note) | search → read_note | 91 秒 | 答对:负责人小王 |
| 新建名为「mcp test 1020」的笔记 | add_note 失败 1 次,改名后成功 | 29 秒 | 自动改成 mcp-test-1020.md |
| 上下文只开 4096 | 无 | 5 分钟后手动终止 | 反复报错、压缩,没有结果 |
第一组答错,问题出在工具设计上:搜索只返回命中的那一行「下周上线 MCP 笔记服务」,负责人写在下一行,模型看不到。加一个读全文的工具后,模型自己先搜、再读,答对了。第三组说明 ToolError 的报错文字会原样交给模型,它读到「只能包含字母、数字和短横线」后,自己把空格换成了短横线。
速度方面:OpenCode 每次请求的系统提示词和内置工具说明就有约 7400 token,我们 4 个 MCP 工具再加约 600 token。这台 CPU 读提示词约每秒 123 token,所以第一轮光「读题」就要 64 秒左右;后续请求命中 llama.cpp 的提示词缓存,第三组只用了 29 秒。
踩过的 6 个坑
- 照旧教程写
from mcp.server.fastmcp import FastMCP直接报错:2.3.0 下是ModuleNotFoundError,报错里会提示改用MCPServer或固定mcp<2。旧项目不想迁移就在依赖里写上mcp>=1.28,<2,官方说 1.x 进入维护期,仍有安全修复。 - 普通异常的报错信息模型看不到:工具里
raise ValueError("…"),模型只收到「Error executing tool boom」,原因被藏起来了;换成ToolError才会把文字带过去。我们实测两种写法,结果和官方文档描述一致。想让模型自己纠错,就用ToolError写清楚怎么改。 - 在脚本里跑
opencode run一直卡住:我们第一次在非交互终端里运行,13 分钟没有任何输出,llama.cpp 也没收到请求;末尾加上< /dev/null后立刻正常。看起来它在等标准输入,写进定时任务或 CI 时记得加。 - 上下文开小了会死循环,报错还误导人:llama-server 开
-c 4096时,第一个请求就有 8024 token,服务端返回「exceeds the available context size」;OpenCode 随后反复自动压缩对话,还提示是「媒体附件太大」,5 分钟没有结果。Ollama 官方文档要求给 OpenCode 配 64k 以上上下文,我们用 65536 正常。 - 思考模式在 CPU 上等不起:没加
--reasoning off时,Qwen3.5-4B 先输出大段思考,两个请求各生成了 1000 多个 token、每秒约 4 个,5 分钟还没开始回答。纯 CPU 跑 Agent,建议关掉思考,并用-np 1只开一个槽位。 - Inspector 2.x 要求 Node 22.19+:README 写明了这一点。我们在 Node 20.19.2 上运行时出现 EBADENGINE 警告,但
--cli的列工具和调工具都能用;网页版没测,遇到问题先升级 Node。
补充两条小经验:模型传参不一定守规矩,它调 read_note 时传了 weekly-1019.md(带后缀),所以代码里用 removesuffix 做了兼容;改动 MCP 配置后工具列表变了,提示词缓存失效,下一次请求又要从头读约 7400 token(我们测得 62 秒)。
怎么算做完了
- 内存客户端能调用每个工具,返回值符合预期,故意传错参数时
is_error为 True - Inspector
--cli的tools/list能看到全部工具和中文说明 opencode mcp list显示 connected,用一句自然语言让 Agent 完成「先搜、再读、再写」
本文未实测:Streamable HTTP 远程部署、OAuth 鉴权、同一服务器接 Claude Code / Cursor / Codex、OpenCode 是否会读取 MCP resource、显卡上的速度。我们这台机器的网络环境下 Ollama 拉模型失败,所以改用 llama.cpp;用 Ollama 接 OpenCode 的方案也未实测。
最后
挑一个你每天都要手动查的东西(周报目录、接口文档、数据库里的一张表),照第 2 步把它包成两三个工具,先用 Inspector 测通,再交给 Agent。
- MCP Python SDK 文档:What's new in v2
- GitHub:modelcontextprotocol/python-sdk(README)
- PyPI:mcp 2.3.0
- GitHub:modelcontextprotocol/inspector(npm 2.11.0)
- OpenCode 文档:MCP servers
- OpenCode 文档:Providers(llama.cpp 本地模型配置)
- Ollama 文档:OpenCode 集成(64k 上下文要求)
- llama.cpp Release b11748
- Hugging Face:unsloth/Qwen3.5-4B-GGUF
- 本文实测:2026-10-20,mcp 2.3.0、Inspector 2.11.0、OpenCode 1.19.3、llama.cpp b11748、Qwen3.5-4B Q4_K_M,8 核 CPU、无 GPU