本教程带你用 Python 的 FastMCP 框架从零开发一个完整的 MCP 服务器——LogScope 日志分析工具,让 Claude Code 能直接读取、搜索、统计你的应用日志。适合已会用 Claude Code、想把自己的工具喂给 AI 的开发者。学完你将掌握 MCP 的三大原语(工具/资源/提示词)、FastMCP 的装饰器开发范式、用 MCP Inspector 调试、以及把自研服务器接入 Claude Code 的完整闭环。
python --versionpip install uv 或 curl -LsSf https://astral.sh/uv/install.sh | sh),FastMCP 官方推荐用它安装claude --version)MCP(Model Context Protocol,模型上下文协议) 是 Anthropic 于 2024 年开源的标准协议,专门解决一个问题:让 AI 用统一的方式调用外部工具和数据。没有 MCP 之前,每接一个外部系统(数据库、监控、Jira……)都得手写一套对接逻辑;有了 MCP,你只要按协议暴露「工具」「资源」「提示词」,任何支持 MCP 的客户端(Claude Code、Claude Desktop、Cursor 等)都能直接调用。
FastMCP 则是 MCP 生态里最主流的 Python 框架。它的核心卖点一句话概括:把一个普通 Python 函数变成 MCP 工具,只需一个装饰器。类型注解自动生成 JSON Schema,docstring 自动变成给 AI 看的工具说明,参数校验、协议握手、传输层细节全部自动处理。据官方数据,某种版本的 FastMCP 驱动着全网约 70% 的 MCP 服务器,每天下载量百万级——FastMCP 1.0 甚至在 2024 年被合并进了官方 MCP Python SDK。
MCP 服务器对外暴露三类「原语」,理解这三者你就抓住了 MCP 的全部:
| 原语 | 作用 | FastMCP 写法 | 给 AI 的语义 |
|---|---|---|---|
| 工具 Tool | 可执行的函数(有副作用/返回值) | @mcp.tool |
「我可以帮你做这件事」 |
| 资源 Resource | 可读取的数据(文件、DB 内容) | @mcp.resource("uri") |
「我可以帮你读这份资料」 |
| 提示词 Prompt | 可复用的对话模板 | @mcp.prompt |
「这里有套开场白模板」 |
关键区分:工具是动词,资源是名词。让 AI「搜索日志」用工具,让 AI「看一眼某个日志文件」用资源。本教程两者都会用到。
新建项目目录并安装依赖:
mkdir logscope-mcp && cd logscope-mcp
uv init --python 3.12 # 初始化项目,生成 pyproject.toml
uv add fastmcp # 安装最新 FastMCP(当前 v3.x)
uv add --dev ruff # 可选:代码风格检查
验证安装成功:
uv run python -c "import fastmcp; print(fastmcp.__version__)"
# 预期输出类似:3.x.x
常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
import fastmcp 失败,提示 No module |
pip 升级后残留旧版 | 跑 pip install --force-reinstall fastmcp(用 uv 则不受影响) |
uv init 报找不到命令 |
未安装 uv | pip install uv 后重试 |
| 版本号显示 1.x | 装到了旧官方 SDK | uv add fastmcp@latest 升到独立版 |
创建 logscope_mcp.py,先写最小可运行版本:
from fastmcp import FastMCP
mcp = FastMCP("LogScope") # 实例化服务器,名字会展示给客户端
@mcp.tool
def greet(name: str) -> str:
"""向指定用户打招呼。"""
return f"你好,{name}!我是 LogScope 日志助手。"
if __name__ == "__main__":
mcp.run() # 默认走 stdio 传输,适配 Claude Code
运行:uv run python logscope_mcp.py
预期结果:进程不会退出,它在 stdin/stdout 上监听 JSON-RPC 消息——这就是「服务器在运行」的样子(看起来像卡住了,其实正常)。
此时 FastMCP 已经自动帮你做了三件事:
greet 作为工具名name: str 类型注解生成参数 schema(AI 必须传一个字符串才能调用)这就是 FastMCP 的核心红利:你写的是普通 Python,得到的是协议完备的 MCP 工具。
现在构建真正的工具集。假设你有一堆应用日志(Nginx、应用服务日志等),想让 Claude 直接帮你查错。最终 AI 能说:「帮我看下 app.log 最近 50 行里所有 ERROR」并自动调用我们的工具。
先准备测试数据目录:
mkdir -p logs
# 造一个测试日志(也可换成你真实日志)
cat > logs/app.log <<'EOF'
2026-07-12 09:01:12 INFO 用户 u_8821 登录成功
2026-07-12 09:03:44 WARN 缓存命中率降至 41%
2026-07-12 09:10:08 ERROR 数据库连接超时 (host=db-2)
2026-07-12 09:10:09 ERROR 重试 1/3 失败
2026-07-12 09:11:30 ERROR 重试 2/3 失败
2026-07-12 09:12:50 INFO 数据库连接恢复
2026-07-12 09:45:00 WARN 磁盘使用率 88%
2026-07-12 10:02:17 ERROR 订单 ord_55231 支付回调签名校验失败
EOF
完整的 logscope_mcp.py:
from pathlib import Path
from collections import Counter
import re
from fastmcp import FastMCP, Context
LOG_DIR = Path("logs").resolve() # 日志根目录,按需改
mcp = FastMCP("LogScope")
LEVELS = ("DEBUG", "INFO", "WARN", "WARNING", "ERROR", "FATAL", "CRITICAL")
def _resolve(name: str) -> Path:
"""安全地解析日志路径,禁止目录穿越。"""
base = LOG_DIR
target = (base / name).resolve()
if not str(target).startswith(str(base)):
raise ValueError(f"非法路径:{name}") # 防止 ../../../etc/passwd
return target
# ---------- 工具 1:列出可用日志 ----------
@mcp.tool
def list_logs() -> list[dict]:
"""列出 logs 目录下所有 .log 文件,返回文件名、大小、行数。
当用户不确定有哪些日志可查时,先调用此工具获取清单。
"""
result = []
for f in sorted(LOG_DIR.glob("*.log")):
with f.open(encoding="utf-8", errors="replace") as fh:
lines = sum(1 for _ in fh)
result.append({
"name": f.name,
"size_bytes": f.stat().st_size,
"lines": lines,
})
return result
# ---------- 工具 2:按正则 + 级别搜索 ----------
@mcp.tool
def search_logs(
filename: str,
pattern: str = "",
level: str = "",
last_n: int = 0,
) -> str:
"""在指定日志文件中搜索匹配的行。
参数:
filename: 文件名,如 "app.log"(不含目录)
pattern: 正则表达式,为空则匹配所有行
level: 日志级别过滤,如 "ERROR" / "WARN"(不区分大小写)
last_n: 仅在最后 N 行中搜索,0 表示全文
返回匹配行(最多 200 条),每行带行号前缀。
"""
try:
path = _resolve(filename)
except ValueError as e:
return f"[错误] {e}" # 预期内的错误,返回友好文本给 AI
if not path.exists():
return f"[错误] 文件不存在:{filename}"
try:
regex = re.compile(pattern, re.IGNORECASE) if pattern else None
except re.error as e:
return f"[错误] 正则非法:{e}"
level = level.upper()
matches = []
with path.open(encoding="utf-8", errors="replace") as fh:
all_lines = fh.readlines()
scope = all_lines[-last_n:] if last_n > 0 else all_lines
for i, line in enumerate(scope, start=len(all_lines) - len(scope) + 1):
up = line.upper()
if level and level not in up:
continue
if regex and not regex.search(line):
continue
matches.append(f"{i}: {line.rstrip()}")
if len(matches) >= 200: # 截断,避免撑爆上下文
break
return "\n".join(matches) if matches else "[无匹配行]"
# ---------- 工具 3:按级别统计 ----------
@mcp.tool
def log_stats(filename: str) -> dict:
"""统计指定日志文件各级别的行数。
用于快速判断日志整体健康度,如 ERROR 是否激增。
"""
path = _resolve(filename)
if not path.exists():
return {"error": f"文件不存在:{filename}"}
counter: Counter = Counter()
with path.open(encoding="utf-8", errors="replace") as fh:
for line in fh:
for lv in LEVELS:
if lv in line.upper():
counter[lv] += 1
break
else:
counter["OTHER"] += 1
return {"filename": filename, "total": sum(counter.values()), "by_level": dict(counter)}
# ---------- 工具 4:尾随(异步 + Context 进度上报)----------
@mcp.tool
async def tail_logs(filename: str, n: int = 20, ctx: Context) -> str:
"""返回日志文件最后 N 行(默认 20)。
演示异步工具与 MCP Context:通过 ctx 上报进度日志。
"""
await ctx.info(f"开始读取 {filename} 的最后 {n} 行")
path = _resolve(filename)
if not path.exists():
return f"[错误] 文件不存在:{filename}"
with path.open(encoding="utf-8", errors="replace") as fh:
lines = fh.readlines()
tail = lines[-n:] if n < len(lines) else lines
await ctx.info(f"读取完成,共 {len(tail)} 行")
return "".join(tail).rstrip() or "[文件为空]"
# ---------- 资源:暴露整份日志给 AI 直读 ----------
@mcp.resource("logs://{filename}")
def read_log(filename: str) -> str:
"""以资源形式暴露日志全文,AI 可直接引用读取。"""
path = _resolve(filename)
if not path.exists():
return f"[错误] 文件不存在:{filename}"
return path.read_text(encoding="utf-8", errors="replace")
if __name__ == "__main__":
mcp.run()
这段代码的几个设计要点:
_resolve() 强制目标必须落在 LOG_DIR 内,挡住 ../../etc/passwd 这类目录穿越攻击——给 AI 的工具必须有边界。[错误] xxx 会自己换思路重试;真正的 bug 才让它崩。search_logs 最多 200 行,防止几万行日志撑爆模型上下文窗口。list[dict]、dict 等结构化类型,FastMCP 会生成 output schema,AI 更易解析。tail_logs 用 await ctx.info() 把进度发回客户端,长任务不会显得「没反应」。写完代码先别急着接 Claude Code,用官方调试器 MCP Inspector 验证每个工具。FastMCP 自带一条捷径——fastmcp dev 命令会自动拉起 Inspector:
uv run fastmcp dev logscope_mcp.py
执行后会自动打开浏览器(默认 http://localhost:6274),界面分几栏:
list_logs / search_logs / log_stats / tail_logs 四个工具,每个都能填参数点 Run 测试logs://{filename} 资源模板,填 app.log 试读手动测试 search_logs:在 Tools 栏选中它,参数填 filename=app.log、level=ERROR,点 Run,预期返回三行 ERROR 记录(含行号)。
也可以用 Inspector 通用启动器:
npx @modelcontextprotocol/inspector uv run python logscope_mcp.py,效果相同,适合非 FastMCP 服务器。
常见问题
| 现象 | 解决 |
|---|---|
| Inspector 打不开、端口占用 | fastmcp dev --port 6280 logscope_mcp.py 换端口 |
| 工具列表为空 | 检查 @mcp.tool 是否拼对(v3 可不带括号),重启 dev |
| Run 报「正则非法」 | 你在 Inspector 里手填的 pattern 可能含特殊字符,先空着试 |
| 中文乱码 | 文件读取已加 errors="replace",Inspector 控制台显示编码问题可忽略 |
调试通过后,把 LogScope 注册成 Claude Code 的本地 stdio 服务器。
方式 A:命令行注册(推荐,进 user 作用域)
# 注意 -- 之后是启动命令;用 uv run 保证用对环境
claude mcp add logscope -- uv run --project /home/you/logscope-mcp python logscope_mcp.py
方式 B:项目级 .mcp.json(团队共享,随仓库提交)
在项目根目录建 .mcp.json:
{
"mcpServers": {
"logscope": {
"command": "uv",
"args": ["run", "--project", "/home/you/logscope-mcp", "python", "logscope_mcp.py"]
}
}
}
团队成员
git pull后,首次在项目里启动 Claude Code 会提示信任该 MCP 配置,确认即生效。
验证连接:
claude mcp list # 应看到 logscope
claude mcp get logscope # 查看详细配置与当前状态
进入 Claude Code 交互界面,输入 /mcp 能看到 logscope 显示 connected,并列出四个工具。
⚠️ 最大坑:stdio 服务器卡在 connecting 永不就绪
这是 2026 年被频繁报告的问题(Claude Code 2.x 尤甚)。根因几乎都是 stdout 被污染:MCP stdio 协议要求 stdout 只能走 JSON-RPC,一旦你在代码里写了 print("启动成功") 这类调试输出,客户端就解析不了握手包,表现为永久 connecting。
解决清单:
print()——调试输出改用 ctx.info() 经 MCP 日志通道上报,或写文件/stderrmcp.run() 没传错 transport(接 Claude Code 必须是默认 stdio)claude --mcp-debug 启动查看详细握手日志定位--project 参数),避免工作目录不一致导致找不到文件接通后在 Claude Code 里直接用自然语言提问,它会自动选工具:
你:app.log 里最近有哪些 ERROR?给我总结下原因。
Claude(自动调用 search_logs filename=app.log level=ERROR):
找到 3 条 ERROR:
- 09:10 数据库连接超时(host=db-2),重试两次失败后 09:12 恢复
- 10:02 订单 ord_55231 支付回调签名校验失败
建议优先排查支付回调的密钥配置。
你:统计下 app.log 各级别分布。
Claude(自动调用 log_stats filename=app.log):
INFO 2 条,WARN 2 条,ERROR 4 条,整体 ERROR 占比较高,建议关注。
你:把 app.log 整份读出来给我看看格式。
Claude(自动通过 logs://app.log 资源读取):
[返回全文并说明日志格式]
注意第三条用的是资源而非工具——AI 自己判断「读全文」是名词性操作,选了资源原语。这就是双原语设计的妙处。
flowchart LR
U[用户自然语言] --> CC[Claude Code\nMCP 客户端]
CC <-->|stdio / JSON-RPC| FS[FastMCP 服务器\nLogScope]
FS -->|读| L1[(logs/app.log)]
FS -->|读| L2[(logs/nginx.log)]
FS -->|读| L3[(logs/...)]
FS -->|ctx.info 进度| CC
style FS fill:#fde68a,stroke:#b45309,stroke-width:2px
style CC fill:#bfdbfe,stroke:#1d4ed8
flowchart TD
A[1. uv add fastmcp] --> B[2. 写工具/资源\[email protected] / @mcp.resource]
B --> C[3. fastmcp dev 调试\nMCP Inspector 验证]
C --> D{工具行为正确?}
D -- 否 --> B
D -- 是 --> E[4. claude mcp add\n或 .mcp.json]
E --> F[5. /mcp 确认 connected]
F --> G[6. 自然语言对话\n驱动日志分析]
G --> H{要加新能力?}
H -- 是 --> B
H -- 否 --> DONE[完成]
掌握基础后,LogScope 还能这样扩展:
mcp.run(transport="http", port=8000),配合 claude mcp add --transport http url,让团队共享一台日志服务器(注意加鉴权)_resolve 换成调用 Elasticsearch / Loki / CloudWatch 的 API,工具签名不变,AI 无感升级ctx.report_progress(done, total) 分块上报,避免一次性返回超大结果@mcp.prompt 沉淀「标准故障排查话术」,团队成员一键复用pyproject.toml 的 console_scripts 入口,发布 PyPI,别人 uvx logscope-mcp 直接用gofastmcp.com)——工具/资源/Context/部署完整参考,附 llms.txt 可直接喂给 AImodelcontextprotocol.io)——协议规范、Inspector、调试指南、参考服务器合集code.claude.com/docs/en/mcp)——stdio/HTTP/SSE/WebSocket 四种传输、三大作用域、OAuth 鉴权、MCP 资源与提示词用法modelcontextprotocol/inspector)——可视化调试工具源码与导出客户端配置功能@mcp.tool 装饰器、stdio/HTTP 传输运行方式.mcp.json 配置与 MCP 资源用法[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
