教程2026-10-20 · 智涌AI

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

教程封面:用 MCP Python SDK 2.3 写 40 行笔记服务器,接进 OpenCode 并用本地 Qwen3.5-4B 驱动

想让 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 工具的说明文字和返回内容,写的是给模型看的接口文档:模型只能看到你给它的那一行。

开始前准备好这些

五步跑通:写服务器 → 内存测试 → 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 ×186 秒答错:说没找到负责人
同一问题(加了 read_note)search → read_note91 秒答对:负责人小王
新建名为「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 个坑

补充两条小经验:模型传参不一定守规矩,它调 read_note 时传了 weekly-1019.md(带后缀),所以代码里用 removesuffix 做了兼容;改动 MCP 配置后工具列表变了,提示词缓存失效,下一次请求又要从头读约 7400 token(我们测得 62 秒)。

怎么算做完了

本文未实测:Streamable HTTP 远程部署、OAuth 鉴权、同一服务器接 Claude Code / Cursor / Codex、OpenCode 是否会读取 MCP resource、显卡上的速度。我们这台机器的网络环境下 Ollama 拉模型失败,所以改用 llama.cpp;用 Ollama 接 OpenCode 的方案也未实测。

最后

挑一个你每天都要手动查的东西(周报目录、接口文档、数据库里的一张表),照第 2 步把它包成两三个工具,先用 Inspector 测通,再交给 Agent。

📬 订阅《智涌AI周报》

每周一封,精选 AI 大事和实用技巧。