💡 MCP 解决的不是“让模型更聪明”,而是让模型以统一协议调用真实工具。本文从一个只读的本地笔记搜索器开始,把一个最小可用的 MCP Server 跑起来。
先看结果
完成本文后,你会得到一个本地 MCP Server:
AI Agent / MCP Inspector
│ tools/list、tools/call
▼
本地 MCP Server
│
▼
只读搜索 ./notes 下的 Markdown 文件
它可以回答类似的问题:
“在我的笔记里搜索 retry,返回相关段落。”
工具只读 notes 目录,不执行 Shell,不访问目录之外的文件。这种边界很重要:工具一旦能写文件、发请求或执行命令,就已经进入需要权限控制和人工确认的范围。
MCP 到底解决什么问题
传统做法通常是给每个模型客户端写一套插件接口:
客户端 A → A 的插件格式
客户端 B → B 的插件格式
客户端 C → C 的插件格式
MCP 把工具、资源和提示词抽象成统一的 Server/Client 协议:
模型客户端 → MCP Client → MCP Server → 本地文件、数据库或 API
MCP Server 不负责“理解用户意图”,它只负责声明能力并执行调用。模型先看到工具名称、描述和 JSON Schema,再决定是否调用;客户端负责把调用结果交回模型。
工具调用的核心消息是两个 JSON-RPC 方法:
| 方法 | 作用 |
|---|---|
tools/list | 列出工具名称、描述和 inputSchema |
tools/call | 按工具名和参数执行一次调用 |
因此,一个好工具的关键不是代码量,而是三件事:名称清晰、参数可校验、失败结果可理解。
准备环境
本文使用 Python 3.10+ 和 uv。没有 uv 时,也可以把命令中的 uv run 换成已激活虚拟环境里的 python。
mkdir mcp-local-tools
cd mcp-local-tools
uv init --python 3.12
uv add "mcp[cli]"
mkdir notes
Set-Content notes\python.md "Python 的 pathlib 可以安全处理文件路径。"
Set-Content notes\mcp.md "MCP Server 通过工具把能力暴露给客户端。"
项目目录应该是:
mcp-local-tools/
├── pyproject.toml
├── server.py
└── notes/
├── mcp.md
└── python.md
写第一个 MCP Server
新建 server.py,完整内容如下:
from __future__ import annotations
from pathlib import Path
from mcp.server.fastmcp import FastMCP
BASE_DIR = Path(__file__).resolve().parent
NOTES_DIR = (BASE_DIR / "notes").resolve()
MAX_RESULTS = 20
MAX_CHARS_PER_FILE = 8_000
mcp = FastMCP("local-notes")
def safe_note_path(relative_path: str) -> Path:
"""把用户输入限制在 notes 目录内,阻止 .. 越界读取。"""
candidate = (NOTES_DIR / relative_path).resolve()
if candidate == NOTES_DIR or NOTES_DIR not in candidate.parents:
raise ValueError("只能访问 notes 目录中的文件")
if candidate.suffix.lower() != ".md":
raise ValueError("只允许读取 Markdown 文件")
return candidate
@mcp.tool()
def search_notes(query: str, limit: int = 10) -> str:
"""在本地 Markdown 笔记中搜索关键词,返回文件名和匹配行。"""
query = query.strip()
if not query:
raise ValueError("query 不能为空")
if len(query) > 100:
raise ValueError("query 最多 100 个字符")
limit = max(1, min(limit, MAX_RESULTS))
results: list[str] = []
for path in sorted(NOTES_DIR.rglob("*.md")):
# 目录中可能存在损坏或非 UTF-8 文件,单个文件失败不应中断整次搜索。
try:
text = path.read_text(encoding="utf-8")[:MAX_CHARS_PER_FILE]
except OSError:
continue
for line_number, line in enumerate(text.splitlines(), start=1):
if query.casefold() in line.casefold():
relative = path.relative_to(NOTES_DIR)
results.append(f"{relative}:{line_number}: {line.strip()}")
if len(results) >= limit:
return "\n".join(results)
return "没有找到匹配内容。"
@mcp.tool()
def read_note(relative_path: str) -> str:
"""读取 notes 目录中的一个 Markdown 文件。"""
path = safe_note_path(relative_path)
if not path.is_file():
raise ValueError("文件不存在")
return path.read_text(encoding="utf-8")[:MAX_CHARS_PER_FILE]
if __name__ == "__main__":
# 默认使用 stdio,适合桌面客户端启动子进程。
mcp.run()
启动代码只有一行,但工具定义里的限制不能省略:
resolve()+parents检查阻止../../secret.txt这类路径穿越。- 后缀限制避免把任意文件当笔记读取。
- 查询长度、结果数量和单文件大小都有上限,避免一次调用拖垮进程。
- 读取单个文件失败时跳过,而不是把整个工具调用变成无意义的堆栈信息。
用 Inspector 调试
先用官方 CLI 启动 MCP Inspector:
uv run mcp dev server.py
浏览器打开 CLI 输出的地址,在工具列表里应该能看到:
search_notes(query, limit)
read_note(relative_path)
调用 search_notes 时传入:
{
"query": "MCP",
"limit": 5
}
预期结果类似:
mcp.md:1: MCP Server 通过工具把能力暴露给客户端。
Inspector 适合验证三件事:工具是否被发现、输入 Schema 是否生效、错误是否能返回给客户端。不要一上来就接入真实生产数据,先用示例目录验证边界。
连接桌面客户端
支持 MCP 的客户端通常需要一个 Server 配置。以使用 stdio 子进程的配置为例:
{
"mcpServers": {
"local-notes": {
"command": "uv",
"args": [
"run",
"--directory",
"C:/path/to/mcp-local-tools",
"mcp",
"run",
"server.py"
]
}
}
}
Windows 路径建议使用 /,或者把反斜杠写成 \\。配置中的路径必须替换成你自己的绝对路径,不能照抄示例路径。
启动客户端后,模型会先通过 tools/list 发现两个工具。当用户提出“搜索笔记”或“读取某篇笔记”时,客户端再发送 tools/call。模型并不会直接获得你的文件系统权限,它只能使用 Server 主动暴露的能力。
改成 Streamable HTTP
stdio 适合本机单用户;需要多个客户端或容器部署时,可以使用 Streamable HTTP:
uv run mcp run server.py --transport streamable-http
默认监听地址和端口以 SDK/CLI 输出为准。部署到网络环境时,至少补上:
- 身份认证和授权,不能把本地文件工具直接暴露到公网。
- HTTPS,避免笔记内容和工具参数明文传输。
- 请求超时、并发限制和审计日志。
- 每次调用前检查用户是否仍有权访问对应资源。
MCP 的传输层解决“消息怎么传”,不替你解决“谁能调用”。权限必须在 Server 和部署层分别实现。
让它成为真正的 Agent
MCP Server 只是工具提供方。一个 Agent 的最小循环是:
sequenceDiagram
participant U as 用户
participant M as 模型
participant C as MCP Client
participant S as MCP Server
U->>M: 搜索笔记中的 MCP
M->>C: 选择 search_notes
C->>S: tools/call(query=MCP)
S-->>C: 返回匹配行
C-->>M: 工具结果
M-->>U: 总结并引用文件位置
循环可以概括为:
- 客户端获取工具清单和 Schema。
- 模型根据用户问题选择工具并生成参数。
- 客户端展示或审核调用,再发送
tools/call。 - Server 校验参数并执行操作。
- 工具结果回到模型,模型生成最终回答。
涉及写文件、发邮件、执行命令、修改数据库时,应在第 3 步增加明确的人工确认;不要因为“模型说要做”就自动执行不可逆操作。
常见坑
把工具描述写成营销文案
模型靠工具描述决定是否调用。描述应该说明输入、输出、限制和失败条件,例如“只读取 notes 下的 Markdown 文件”,而不是“智能管理你的所有文件”。
让 Server 自己解析自然语言
工具参数应保持结构化:query、limit、relative_path。自然语言理解交给模型,Server 负责验证已经结构化的参数。
忽略错误结果
错误消息要能帮助模型修正参数,例如返回“只允许读取 Markdown 文件”,不要只返回 ValueError 或一大段堆栈。
把 token 放进工具参数
密钥、Cookie、PAT 不应成为模型可见的普通参数。放到 Server 的环境变量或密钥管理系统中,并避免把敏感值写入日志和工具结果。
把本地工具当成可信代码
MCP 只是协议,不会自动给代码增加安全性。第三方 Server 仍要审查源码、锁定依赖版本、限制文件范围,并在沙箱或低权限账户中运行。
最小检查清单
- 工具输入是否有 JSON Schema 和长度/数量上限?
- 是否限制了文件、网络和命令的访问范围?
- 是否对写操作、外发请求和敏感数据增加人工确认?
- 错误消息是否能帮助模型修正参数?
- 是否记录调用者、工具名、参数摘要、耗时和结果状态?
- Server 依赖是否锁版本并定期更新?
总结
MCP 的价值在于把“模型能调用什么”变成一个清晰、可发现、可审计的协议层。一个可靠的落地顺序是:
只读工具 → 参数校验 → Inspector 调试 → 客户端接入 → 权限与审计 → 谨慎开放写操作
先从一个只读工具开始,比一次性暴露“执行任意命令”的万能 Agent 更容易验证,也更容易控制风险。
参考资料
作者:牛马便利店一号店员
文档信息
- 本文作者:牛马
- 本文链接:https://geekhappy.com/2026/08/25/mcp-local-tool-agent/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)