MCP实战案例
前面的章节都是讲单个概念,本篇用一个完整的实战案例串起来:构建一个"博客助手 MCP Server",让 Claude 帮你管理博客文章。
1. 需求分析
我们要让 Claude 能完成以下任务:
- 列出所有文章 - 看博客都写过什么
- 读取某篇文章 - 把内容加载为上下文
- 创建新文章 - 给定标题和内容自动建立文件
- 搜索文章 - 关键词检索
- 生成文章模板 - 提供文章标题和大纲,让 LLM 写初稿
- 统计写作情况 - 看本月写了多少篇
涉及 MCP 的全部三个原语:
- Tools:创建、搜索、统计(有副作用或动态计算)
- Resources:列出和读取文章(只读数据)
- Prompts:文章模板(提示词工程)
2. 项目结构
blog-mcp/
├── pyproject.toml
├── server.py
├── README.md
└── posts/ # 文章目录(用户实际博客)
├── post1.md
└── post2.md
3. 完整代码
新建 server.py:
import os
import re
import json
import datetime
from pathlib import Path
from typing import Annotated
from pydantic import Field
from mcp.server.fastmcp import FastMCP, Context
# === 配置 ===
BLOG_DIR = Path(os.environ.get("BLOG_DIR", "./posts")).resolve()
BLOG_DIR.mkdir(parents=True, exist_ok=True)
mcp = FastMCP("blog-helper")
# ==================== Tools ====================
@mcp.tool()
async def list_posts(
limit: Annotated[int, Field(description="最多返回多少篇", ge=1, le=200)] = 20,
) -> list[dict]:
"""列出博客所有文章的元信息(标题、文件名、修改时间)。
适用场景:用户询问"我都写了哪些文章"、"最近写了什么"。
"""
posts = []
for md_file in BLOG_DIR.glob("*.md"):
title = extract_title(md_file)
posts.append({
"filename": md_file.name,
"title": title,
"modified": datetime.datetime.fromtimestamp(
md_file.stat().st_mtime
).strftime("%Y-%m-%d %H:%M"),
"size": md_file.stat().st_size,
})
# 按修改时间倒序
posts.sort(key=lambda x: x["modified"], reverse=True)
return posts[:limit]
@mcp.tool()
async def search_posts(
keyword: Annotated[str, Field(description="搜索关键词")],
case_sensitive: Annotated[bool, Field(description="是否区分大小写")] = False,
) -> list[dict]:
"""在所有文章中搜索关键词,返回匹配的文件和上下文片段。
适用场景:用户问"我之前写过关于 XXX 的文章吗"、"搜索关于 YYY 的内容"。
"""
results = []
pattern = keyword if case_sensitive else keyword.lower()
for md_file in BLOG_DIR.glob("*.md"):
content = md_file.read_text(encoding="utf-8")
text = content if case_sensitive else content.lower()
if pattern in text:
# 提取上下文片段
idx = text.find(pattern)
start = max(0, idx - 50)
end = min(len(content), idx + 100)
snippet = content[start:end].replace("\n", " ")
results.append({
"filename": md_file.name,
"title": extract_title(md_file),
"snippet": f"...{snippet}...",
})
return results
@mcp.tool()
async def create_post(
title: Annotated[str, Field(description="文章标题")],
content: Annotated[str, Field(description="Markdown 内容")],
filename: Annotated[
str | None,
Field(description="文件名(可选),不填则根据标题自动生成")
] = None,
) -> dict:
"""创建一篇新文章。返回创建后的文件信息。
适用场景:用户写了文章正文要保存、或者要求"帮我创建一篇关于 XXX 的文章"。
"""
if filename is None:
# 根据标题生成安全的文件名
safe_title = re.sub(r'[^\w一-龥-]', '_', title)
date_prefix = datetime.date.today().strftime("%Y-%m-%d")
filename = f"{date_prefix}-{safe_title}.md"
if not filename.endswith(".md"):
filename += ".md"
file_path = BLOG_DIR / filename
if file_path.exists():
raise ValueError(f"文件 {filename} 已存在,请换个文件名或先删除")
# 添加 frontmatter
full_content = f"""---
title: {title}
date: {datetime.date.today().isoformat()}
---
# {title}
{content}
"""
file_path.write_text(full_content, encoding="utf-8")
return {
"filename": filename,
"path": str(file_path),
"size": file_path.stat().st_size,
}
@mcp.tool()
async def get_writing_stats(
days: Annotated[int, Field(description="统计最近多少天", ge=1, le=365)] = 30,
) -> dict:
"""统计指定天数内的写作情况。
适用场景:用户问"我这个月写了几篇"、"最近的写作情况"。
"""
cutoff = datetime.datetime.now() - datetime.timedelta(days=days)
posts = list(BLOG_DIR.glob("*.md"))
recent = [p for p in posts if datetime.datetime.fromtimestamp(p.stat().st_mtime) > cutoff]
total_chars = sum(len(p.read_text(encoding="utf-8")) for p in recent)
return {
"period_days": days,
"total_posts": len(recent),
"total_chars": total_chars,
"avg_chars_per_post": total_chars // max(len(recent), 1),
"post_titles": [extract_title(p) for p in recent],
}
# ==================== Resources ====================
@mcp.list_resources()
def list_post_resources():
"""让客户端能在资源浏览器中看到所有文章"""
return [
{
"uri": f"post://{md_file.name}",
"name": extract_title(md_file),
"mimeType": "text/markdown",
}
for md_file in BLOG_DIR.glob("*.md")
]
@mcp.resource("post://{filename}")
def read_post(filename: str) -> str:
"""读取一篇文章的完整内容"""
file_path = BLOG_DIR / filename
# 路径安全检查
if not str(file_path.resolve()).startswith(str(BLOG_DIR.resolve())):
raise PermissionError("非法路径")
if not file_path.exists():
raise ValueError(f"文章 {filename} 不存在")
return file_path.read_text(encoding="utf-8")
@mcp.resource("blog://stats", mime_type="application/json")
def blog_overview() -> str:
"""博客整体统计信息"""
posts = list(BLOG_DIR.glob("*.md"))
return json.dumps({
"total_posts": len(posts),
"blog_dir": str(BLOG_DIR),
}, ensure_ascii=False, indent=2)
# ==================== Prompts ====================
@mcp.prompt()
def write_article_draft(
topic: Annotated[str, Field(description="文章主题")],
style: Annotated[str, Field(description="写作风格:技术/随笔/教程")] = "技术",
target_length: Annotated[str, Field(description="目标长度:短/中/长")] = "中",
) -> str:
"""生成一篇文章的初稿"""
length_map = {
"短": "800 字左右",
"中": "1500-2500 字",
"长": "3000 字以上",
}
return f"""请围绕主题"{topic}"写一篇{style}风格的博客文章,长度约 {length_map.get(target_length, "1500 字")}。
要求:
1. 标题吸引人、有信息量
2. 开头用 1-2 句话点明文章核心价值
3. 主体部分逻辑清晰,分段合理,必要时用代码示例
4. 结尾用 3-5 个要点总结
5. 使用 Markdown 格式
6. 适合在个人技术博客发布
只输出 Markdown 内容,第一行是 # 标题,后面是正文。
"""
@mcp.prompt()
def review_article(filename: str) -> str:
"""对一篇已有文章进行写作评审"""
return f"""请阅读文章(资源 URI: post://{filename})并给出写作评审。
评审维度:
1. **标题**:是否吸引人,是否准确
2. **结构**:开头、主体、结尾是否清晰
3. **内容**:技术深度是否合理,有无明显错误
4. **可读性**:行文是否流畅,是否有冗余
5. **代码示例**:是否有代码、代码是否正确
请给出:
- 总体打分(1-10)
- 三个具体的改进建议
- 如果有明显错误,指出在哪一段
"""
# ==================== 工具函数 ====================
def extract_title(md_file: Path) -> str:
"""从 Markdown 文件提取标题"""
try:
content = md_file.read_text(encoding="utf-8")
# 优先从 frontmatter 找
m = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
if m:
return m.group(1).strip()
# 否则从第一个 # 找
m = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
if m:
return m.group(1).strip()
return md_file.stem
except Exception:
return md_file.stem
# ==================== 启动 ====================
if __name__ == "__main__":
mcp.run()
4. 测试 Server
准备测试数据
mkdir -p posts
cat > posts/2026-04-01-hello.md << 'EOF'
---
title: 我的第一篇博客
date: 2026-04-01
---
# 我的第一篇博客
这是一篇测试文章,记录使用 MCP 的过程。
EOF
用 inspector 调试
在浏览器中:
- 切到 Tools 标签,调用
list_posts(),应该返回刚创建的文章 - 调用
search_posts(keyword="MCP"),应该匹配到 - 调用
create_post(title="新文章", content="正文"),看看 posts 目录有没有新文件
5. 接入 Claude Desktop
编辑 claude_desktop_config.json:
{
"mcpServers": {
"blog-helper": {
"command": "python",
"args": ["/Users/me/path/to/blog-mcp/server.py"],
"env": {
"BLOG_DIR": "/Users/me/Documents/blog/posts"
}
}
}
}
完全退出 Claude Desktop 后重启。
6. 实际使用场景
场景 1:查看历史文章
用户: 我最近写了什么?
Claude: [自动调用 list_posts]
你最近的 5 篇文章是:
1. 《MCP 实战案例》(2026-04-25)
2. 《Python 装饰器》(2026-04-20)
...
场景 2:搜索关键词
场景 3:写新文章
用户: /write_article_draft
topic: "Python 协程详解"
style: "技术"
target_length: "中"
Claude: # Python 协程详解
...(完整文章初稿)
用户: 不错,帮我保存为新文章
Claude: [调用 create_post]
已创建 2026-04-27-Python_协程详解.md
场景 4:评审已有文章
用户: /review_article
filename: "2026-04-20-python-decorator.md"
Claude: [自动读取 post://2026-04-20-python-decorator.md]
这篇文章总体评分 7/10。优点是...
三个改进建议:
1. ...
场景 5:写作统计
7. 进阶改进点
这个 Server 还可以扩展:
1. 文章发布
加一个 publish_post Tool,自动 git commit + push 到博客仓库。
2. 标签系统
加 add_tag 和 list_by_tag,按标签管理。
3. SEO 优化
加 seo_check Tool,分析文章的 SEO 友好度。
4. 草稿管理
把 posts/ 拆为 drafts/ 和 published/,加状态切换 Tool。
5. 对接外部服务
- 用
langchain-mcp-adapters把这个 Server 接入 LangChain Agent - 部署成 Streamable HTTP,多人共享
8. 经验总结
1. 三大原语各司其职
- Tool:用户问什么 LLM 自己用什么(
list_posts、search_posts) - Resource:让用户主动选择上下文(点击 📎 选择某篇文章)
- Prompt:高质量提示词工程(写文章、评审文章)
2. 安全很重要
文件路径必须做边界检查,否则 LLM 可能被 prompt injection 诱导读其他目录。
3. 描述要细
每个 Tool 加上"适用场景"说明,模型才知道什么时候调用。
4. 配置可外部化
用环境变量传 BLOG_DIR,让用户配置自己的目录,不要硬编码。
5. 错误信息要友好
raise ValueError("文章 xxx.md 不存在") 比 FileNotFoundError 信息量大得多,模型可以根据错误调整下一步动作。
总结
- 一个完整的 MCP Server 通常综合使用 Tools、Resources、Prompts 三大原语
- 设计原则:Tool 让模型自主操作,Resource 让用户加载数据,Prompt 提供高质量提示
- 真实场景下务必做安全检查(路径、权限、参数验证)
- 用环境变量管理用户特定的配置(数据目录、API key 等)
- 一个 Server 能让 Claude 完整掌管一个具体场景,远比单纯聊天有用