Python技术迷

用 Python 开发 MCP 服务很简单,完整案例!

一个 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 想成给函数加几个装饰器就能直接上生产。真正费时间的地方还是权限、超时、审计和返回数据裁剪。尤其是查询数据库的工具,能只读就别给写权限,能限制行数就别让模型自己决定查多少。

服务搭起来确实简单,边界不收紧,出问题也一样简单。