一个 Python 函数,加个 @mcp.tool(),大模型就能识别参数、调用代码、拿到结果。
我第一次看到这段代码时还有点不信。以前给大模型接内部系统,要写接口、整理参数说明、处理调用结果,还得想办法让模型知道接口怎么用。换成 MCP 后,协议层那堆活基本被 SDK 接走了。
这次不写计算器,那种 Demo 看完也不知道能干什么。直接做一个日志排查服务:让大模型读取指定目录下的日志,统计错误类型,再把最近几条异常捞出来。
MCP 可以把服务能力分成 Tool、Resource 和 Prompt。Tool 适合执行查询、计算、写入这类动作;Resource 更像只读数据源。Python 官方 SDK 支持 stdio、SSE 和 Streamable HTTP 等传输方式,当前稳定版要求 Python 3.10 及以上。
先建项目:
mkdir log-mcp
cdlog-mcp
python -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"
mkdir runtime_logs
Windows 激活虚拟环境用:
.venv\Scripts\activate
在 runtime_logs/app.log 里放几行测试日志:
2026-07-10 09:12:03 INFO order created, order_id=7312
2026-07-10 09:12:07 ERROR payment timeout, order_id=7312
2026-07-10 09:13:21 WARN inventory response slow, cost=1840ms
2026-07-10 09:14:02 ERROR database connection refused
然后新建 server.py:
from collections import Counter
from pathlib import Path
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.exceptions import ToolError
mcp = FastMCP("log-inspector")
LOG_ROOT = Path(__file__).parent.joinpath("runtime_logs").resolve()
deflocate_log(file_name: str) -> Path:
candidate = LOG_ROOT.joinpath(file_name).resolve()
if LOG_ROOT notin candidate.parents:
raise ToolError("不允许读取日志目录之外的文件")
ifnot candidate.is_file():
raise ToolError(f"日志文件不存在:{file_name}")
return candidate
@mcp.tool()
definspect_errors(file_name: str, limit: int = 10) -> dict:
"""读取日志并返回错误统计以及最近的异常记录。
Args:
file_name: runtime_logs 目录下的日志文件名
limit: 最多返回多少条异常,范围为 1 到 50
"""
ifnot1 <= limit <= 50:
raise ToolError("limit 必须在 1 到 50 之间")
log_file = locate_log(file_name)
lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()
error_lines = [
line for line in lines
if" ERROR "in line or"Exception"in line
]
categories = Counter()
for line in error_lines:
message = line.split(" ERROR ", maxsplit=1)[-1]
category = message.split(",", maxsplit=1)[0].strip()
categories[category] += 1
return {
"file": file_name,
"total_lines": len(lines),
"error_count": len(error_lines),
"error_categories": dict(categories.most_common()),
"recent_errors": error_lines[-limit:],
}
@mcp.tool()
deffind_slow_calls(file_name: str, threshold_ms: int = 1000) -> list[str]:
"""查找日志中超过指定耗时的调用记录。"""
if threshold_ms < 0:
raise ToolError("threshold_ms 不能小于 0")
log_file = locate_log(file_name)
matched: list[str] = []
for line in log_file.read_text(
encoding="utf-8",
errors="replace",
).splitlines():
if"cost="notin line or"ms"notin line:
continue
try:
cost_text = line.split("cost=", maxsplit=1)[1].split("ms", maxsplit=1)[0]
if int(cost_text) >= threshold_ms:
matched.append(line)
except ValueError:
continue
return matched
@mcp.resource("ops://log-rules")
deflog_rules() -> str:
"""返回日志排查约定。"""
return"""
排查顺序:
1. 先看 ERROR 数量和错误类型,不要直接全文搜索堆栈。
2. 再检查同一时间段内的慢调用。
3. 涉及订单时,按 order_id 串联上下游日志。
4. 禁止读取 runtime_logs 目录之外的文件。
""".strip()
if __name__ == "__main__":
mcp.run(transport="streamable-http")
这里真正有用的不是装饰器,而是函数签名。
file_name: str、limit: int 会被 SDK 转成工具参数定义,函数注释会成为工具说明。模型看到的不只是一个函数名,它还能知道每个参数应该传什么。官方文档也明确说明,FastMCP 会利用 Python 类型标注和 docstring 生成工具定义。
我在代码里额外卡了两道口子。
第一道是路径校验。日志工具最忌讳让模型随便传一个绝对路径,不然它理论上可以继续尝试读取配置文件、密钥或者系统文件。
第二道是 ToolError。参数不对、文件不存在,这些都属于预期内的业务错误,不该返回一大段 Python 堆栈。官方 SDK 会把 ToolError 转成 MCP 的错误结果,客户端能明确区分成功和失败。
启动服务:
python server.py
默认可以通过下面的 MCP 地址连接:
http://localhost:8000/mcp
调试时我更习惯直接开 Inspector:
npx -y @modelcontextprotocol/inspector
填入 MCP 地址,连接后就能看到 inspect_errors 和 find_slow_calls 两个工具。官方文档同样推荐使用 MCP Inspector 检查工具参数和调用结果。
调用 inspect_errors:
{
"file_name": "app.log",
"limit": 5
}
返回的数据大概是这样:
{
"file": "app.log",
"total_lines": 4,
"error_count": 2,
"error_categories": {
"payment timeout": 1,
"database connection refused": 1
},
"recent_errors": [
"2026-07-10 09:12:07 ERROR payment timeout, order_id=7312",
"2026-07-10 09:14:02 ERROR database connection refused"
]
}
到这里,这个 MCP 服务已经能用了。
后面接数据库、工单系统或者内部 HTTP 接口,改的主要是工具函数里的业务代码。协议解析、参数描述和工具注册不需要再手搓一遍。
不过也别把 MCP 想成给函数加几个装饰器就能直接上生产。真正费时间的地方还是权限、超时、审计和返回数据裁剪。尤其是查询数据库的工具,能只读就别给写权限,能限制行数就别让模型自己决定查多少。
服务搭起来确实简单,边界不收紧,出问题也一样简单。