主题
项目 5 · 自己的 MCP Server
把个人笔记能力包装成 MCP Server。先用自己的 Python Client 验证协议,不用大模型 Key;之后再连接支持 MCP 的 Host。
准备与目录
bash
uv init notes-mcp --python 3.12
cd notes-mcp
uv add "mcp==1.12.4" "pydantic==2.11.7" "pydantic-settings==2.10.1"text
notes-mcp/
├── server.py
├── client.py
└── notes/
└── python.md这是固定 SDK 1.x 与兼容依赖的课程;旧 MCP SDK 直接搭配最新 Pydantic 可能出现内部接口 ImportError,按上面的版本安装并保存 uv.lock。升级 2.x 时需要迁移 API。创建 notes 和 python.md,写一段不敏感的学习笔记。
服务端:列出与读取笔记
python
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("我的学习笔记")
ROOT = Path(__file__).resolve().parent / "notes"
if ROOT.is_symlink():
raise ValueError("笔记根目录不允许符号链接")
ROOT.mkdir(exist_ok=True)
ROOT = ROOT.resolve()
def resolve_note(name: str) -> Path:
if not 1 <= len(name) <= 100 or name.startswith("."):
raise ValueError("非法笔记名")
relative = Path(name)
if relative.is_absolute() or len(relative.parts) != 1 or relative.suffix != ".md":
raise ValueError("只允许 notes 下单个 Markdown 文件")
file = ROOT / relative
if file.is_symlink():
raise ValueError("不允许符号链接")
file = file.resolve()
if not file.is_relative_to(ROOT) or not file.is_file() or file.stat().st_size > 20_000:
raise ValueError("笔记不存在、越界或超过 20 KB")
return file
@mcp.tool()
def list_notes() -> list[str]:
"""列出可以读取的学习笔记名称。"""
result = []
for path in sorted(ROOT.glob("*.md")):
try:
resolve_note(path.name)
except (ValueError, OSError):
continue
result.append(path.name)
if len(result) >= 100:
break
return result
@mcp.tool()
def read_note(name: str) -> str:
"""读取一篇学习笔记,name 必须来自 list_notes 返回的名称。"""
return resolve_note(name).read_text(encoding="utf-8")
if __name__ == "__main__":
mcp.run(transport="stdio")客户端:握手、列工具、调工具
python
import asyncio
import sys
from pathlib import Path
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server = Path(__file__).resolve().parent / "server.py"
params = StdioServerParameters(command=sys.executable, args=[str(server)])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize() # 协议握手
response = await session.list_tools()
print("工具:", [tool.name for tool in response.tools])
notes = await session.call_tool("list_notes", {})
print("笔记列表:", notes.content)
result = await session.call_tool("read_note", {"name": "python.md"})
if result.isError:
raise RuntimeError("读取笔记失败,请检查 notes/python.md")
for part in result.content:
if part.type == "text":
print(part.text)
if __name__ == "__main__":
asyncio.run(main())bash
uv run client.py客户端自动启动 server 子进程并关闭。使用 sys.executable 确保 Server 也使用当前项目的虚拟环境,而不是系统 Python。
连接 Host
不同产品 MCP 配置字段可能不同,以其设置界面为准。常见 JSON 形式如下,替换绝对路径:
json
{
"mcpServers": {
"learning-notes": {
"command": "uv",
"args": ["--directory", "/absolute/path/notes-mcp", "run", "server.py"]
}
}
}GUI 启动的应用可能找不到终端的 PATH;必要时把 command 换成 uv 的绝对路径。不要在配置中填模型 Key,这个只读 Server 不需要它。
问 Host:「列出我的学习笔记,然后总结 python.md」。Host 应展示 MCP 工具调用。确认过哪些工具读取了内容,再验证总结。
验收与升级
- 协议客户端能列出两个工具并读到内容。
read_note("../.env")和绝对路径被拒绝。- 不存在文件返回工具错误,而不是让整个 Server 崩溃。
- Server 不向 stdout 打调试日志,否则会污染协议。
本版只有只读能力。增加写笔记前,先设计明确确认、文件名规则、冲突处理和备份。远程 HTTP 版本需要认证、访问控制与资源限制,不要把本地无认证 Server 直接暴露公网。