本教程带你从零构建一个 MCP(Model Context Protocol)通知服务器,让 Claude Code 在执行任务过程中或任务完成后,能主动把消息推送到钉钉、飞书、企业微信等团队协作渠道。学完你将掌握:MCP 服务器的标准骨架、Python FastMCP 工具开发、主流 IM 的 webhook 加签算法、以及如何把 MCP 工具与 Claude Code 的 Stop 钩子组合成"完活即报"的自动化闭环。适合已经会用 Claude Code 基础命令、想给 AI 装上"对外喊话"能力的开发者与运维同学。
/mcp 命令查看已注册服务器pip 安装依赖很多人第一反应是:发个 HTTP 请求不就行了,干嘛要包成 MCP?这里的关键区别在于 谁来决定发、发什么。
notify.py,再让 cron 或 CI 去跑。脚本是死的。举个真实场景:你让 Claude 跑一遍测试并修复失败的用例。它修完后,自己判断 "修好了,该通知主人",然后调用 send_notify 工具,把修复摘要推到钉钉。你不必盯着终端,去喝杯咖啡回来手机上已经收到结果。
MCP 在这里的角色,就是 把"发消息"这个能力,以标准协议暴露给 Claude,让它像调用内置工具一样调用它。理解了这一点,后面的代码就顺理成章了。
在动手前,先在脑子里把数据流走一遍。Claude Code(MCP 客户端)通过 stdio 与我们的 MCP 服务器进程通信,服务器进程再通过 HTTP webhook 与钉钉/飞书的服务器通信。
flowchart LR
U["用户对 Claude 说:\n'跑测试,修完通知我'"]
C["Claude Code\n(MCP 客户端)"]
S["notify_mcp.py\n(MCP 服务器)"]
subgraph IM["团队协作平台"]
D["钉钉群机器人"]
F["飞书自定义机器人"]
end
P["手机收到推送"]
U --> C
C -->|"stdio / JSON-RPC\n调用 send_notify 工具"| S
S -->|"HTTPS + 签名\nwebhook"| D
S -->|"HTTPS + 签名\nwebhook"| F
D --> P
F --> P
注意中间那段 stdio / JSON-RPC:Claude Code 和 MCP 服务器之间走的是标准协议,不是你自定义的接口。也就是说,只要你按 MCP 规范暴露工具,任何支持 MCP 的客户端(Claude Code、Claude Desktop、其他 IDE 插件)都能用你的工具。这正是 MCP 的价值——写一次,到处接。
我们用官方 Python SDK 的 FastMCP 风格开发,它用装饰器把普通函数变成 MCP 工具,写法极简。
先建工作目录并安装 SDK:
mkdir ~/notify-mcp && cd ~/notify-mcp
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]" httpx
说明:
mcp[cli]是官方 Python SDK(包名就叫mcp),带 CLI 扩展;httpx用来发 HTTP 请求。官方 stable v2 计划随规范一起发布,当前 v1.x 已稳定可用。
创建 notify_mcp.py,先写一个最小骨架验证 SDK 跑得通:
# notify_mcp.py —— 最小骨架
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("notify-mcp") # 服务器名,会显示在 Claude Code 的 /mcp 列表里
@mcp.tool()
def ping(message: str) -> str:
"""原样回显消息,用于验证连通性。"""
return f"pong: {message}"
if __name__ == "__main__":
mcp.run(transport="stdio") # Claude Code 默认用 stdio 传输
预期结果:这只是一个连通性测试。下一节接入后,在 Claude Code 里输入"调用 ping 工具,消息写 hi",它应返回 pong: hi,证明你的 MCP 服务器已被识别。
常见问题:
ModuleNotFoundError: No module named 'mcp':你大概率没激活虚拟环境,或装到了别的解释器里。Claude Code 启动 MCP 时用的是你配置里指定的那个 python,要确保那个解释器能 import mcp。from mcp.server.fastmcp import FastMCP 报错:旧版 SDK 路径不同。请用 mcp>=1.x。钉钉自定义机器人发消息的核心是 POST 一个 JSON 到 webhook URL。难点在于 安全设置里的"加签"模式:钉钉要求每个请求带上 timestamp 和 sign 两个参数,sign 由 时间戳\n密钥 经 HMAC-SHA256 后再 base64、再 URL 编码得到。
在 notify_mcp.py 里追加钉钉发送逻辑:
import os, time, hmac, hashlib, base64, urllib.parse, json, urllib.request
# ---------- 钉钉 ----------
def _dingtalk_sign(secret: str, ts_ms: int) -> str:
"""钉钉加签:HMAC-SHA256(timestamp + '\\n' + secret) -> base64 -> URL 编码"""
string_to_sign = f"{ts_ms}\n{secret}"
digest = hmac.new(
secret.encode("utf-8"),
string_to_sign.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
return urllib.parse.quote_plus(base64.b64encode(digest))
def _dingtalk_send(webhook: str, secret: str, title: str, text: str) -> dict:
ts_ms = int(round(time.time() * 1000)) # 钉钉要毫秒级时间戳
if secret:
sign = _dingtalk_sign(secret, ts_ms)
webhook = f"{webhook}×tamp={ts_ms}&sign={sign}"
payload = {"msgtype": "markdown", "markdown": {"title": title, "text": text}}
req = urllib.request.Request(
webhook, data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode("utf-8"))
这里有几个容易踩的坑,务必记住:
* 1000)。飞书用的是秒,两者别搞混。\n,不是字面量。f"{ts}\n{secret}" 这里的 \n 是转义,正确。如何拿到 webhook 和 secret:在钉钉群里「群设置 → 智能群助手 → 添加机器人 → 自定义」,安全设置勾选"加签",创建后会给你一个 webhook URL(含 access_token)和一个 secret。两者都妥善保管。
飞书自定义机器人的加签算法与钉钉 形似而神不同:同样是 HMAC-SHA256,但它的密钥和消息串拼法相反——飞书是 "{timestamp}\n{secret}" 作为 HMAC 的 key,消息体为空,digest 后直接 base64,不做 URL 编码。这是个反直觉的细节,写错就只会收到"签名不匹配"。
继续在文件里追加:
# ---------- 飞书 ----------
def _feishu_sign(secret: str, ts_sec: int) -> str:
"""飞书加签:HMAC-SHA256(key=timestamp+'\\n'+secret, msg='') -> base64"""
string_to_sign = f"{ts_sec}\n{secret}"
digest = hmac.new(
string_to_sign.encode("utf-8"),
digestmod=hashlib.sha256, # 注意:msg 为空,只传 key
).digest()
return base64.b64encode(digest).decode("utf-8")
def _feishu_send(webhook: str, secret: str, title: str, text: str) -> dict:
payload = {"msg_type": "text", "content": {"text": f"{title}\n{text}"}}
if secret:
ts_sec = int(time.time()) # 飞书要秒级时间戳
payload["timestamp"] = str(ts_sec)
payload["sign"] = _feishu_sign(secret, ts_sec)
req = urllib.request.Request(
webhook, data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode("utf-8"))
对比两个加签函数,体会差异:钉钉 hmac.new(key=secret, msg=串),飞书 hmac.new(key=串, msg=None)。这种"长得像但参数位置相反"的设计,是踩坑重灾区,建议把两个函数的注释写得足够显眼。
现在把两个渠道整合成一个对外工具。设计要点:渠道用参数区分,密钥走环境变量——绝不能把 secret 硬编码进代码或写进会被提交的配置文件。
@mcp.tool()
def send_notify(message: str, channel: str = "dingtalk", title: str = "Claude 通知") -> str:
"""向团队通知渠道推送一条消息。
Args:
message: 消息正文(钉钉支持 markdown,飞书按纯文本处理)。
channel: 渠道名,dingtalk 或 feishu,默认 dingtalk。
title: 消息标题,仅对支持标题的渠道生效。
Returns:
发送结果摘要,便于 Claude 判断是否成功。
"""
channel = (channel or "dingtalk").lower()
if channel == "dingtalk":
webhook = os.environ.get("DINGTALK_WEBHOOK", "")
secret = os.environ.get("DINGTALK_SECRET", "")
if not webhook:
return "❌ 未配置环境变量 DINGTALK_WEBHOOK"
res = _dingtalk_send(webhook, secret, title, message)
elif channel == "feishu":
webhook = os.environ.get("FEISHU_WEBHOOK", "")
secret = os.environ.get("FEISHU_SECRET", "")
if not webhook:
return "❌ 未配置环境变量 FEISHU_WEBHOOK"
res = _feishu_send(webhook, secret, title, message)
else:
return f"❌ 未知渠道:{channel}(支持 dingtalk / feishu)"
# 钉钉成功返回 errcode=0,飞书返回 code=0
ok = res.get("errcode", res.get("code", -1)) == 0
return f"{'✅' if ok else '⚠️'} {channel} 推送结果:{res}"
为什么把 docstring 写这么细:MCP 工具的 docstring 会被原样喂给 Claude,作为它判断"什么时候该调用这个工具、参数怎么填"的依据。docstring 写得越清楚(含参数含义、默认值、返回值含义),Claude 调用得越准。这是 MCP 工具开发的一条黄金法则:docstring 就是给 AI 看的 API 文档。
FastMCP 的 mcp.run(transport="stdio") 已经让服务器通过标准输入输出与客户端对话。现在把它注册进 Claude Code。推荐用命令行方式(会自动写入项目级 .mcp.json):
# 注意 venv 的 python 绝对路径,避免 Claude Code 找不到依赖
claude mcp add notify-mcp \
--env DINGTALK_WEBHOOK='https://oapi.dingtalk.com/robot/send?access_token=你的token' \
--env DINGTALK_SECRET='SEC你的密钥' \
--env FEISHU_WEBHOOK='https://open.feishu.cn/open-apis/bot/v2/hook/你的hook' \
--env FEISHU_SECRET='你的飞书密钥' \
-- ~/notify-mcp/.venv/bin/python ~/notify-mcp/notify_mcp.py
也可以直接手写 .mcp.json(效果等价,便于版本管理与团队共享):
{
"mcpServers": {
"notify-mcp": {
"command": "/home/you/notify-mcp/.venv/bin/python",
"args": ["/home/you/notify-mcp/notify_mcp.py"],
"env": {
"DINGTALK_WEBHOOK": "https://oapi.dingtalk.com/robot/send?access_token=xxx",
"DINGTALK_SECRET": "SECxxx",
"FEISHU_WEBHOOK": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
"FEISHU_SECRET": "xxx"
}
}
}
}
安全提醒:
.mcp.json里含 secret,务必加入.gitignore,或把env中的密钥改为从系统环境变量读取(os.environ仍可用,只要 Claude Code 进程能继承到这些变量)。绝对不要把带密钥的.mcp.json提交到公开仓库。
重启 Claude Code 让配置生效,然后在对话里直接说需求,不要教它怎么调工具:
> 跑一下 pytest,把通过/失败的用例数发到钉钉群通知我。
Claude 会自动:读测试 → 跑 pytest → 解析结果 → 调用 send_notify(message="pytest 结果:12 通过,0 失败", channel="dingtalk") → 你的手机"叮"地收到一条群消息。
用 /mcp 命令可以查看 notify-mcp 是否在线、工具是否注册成功(应显示 send_notify)。如果显示连接失败,常见原因是 command 路径写错、venv 未激活、或端口/stdio 被占用——回到第七步逐项核对。
手动让 Claude 发消息还不够爽。真正的自动化是:每次 Claude 跑完一轮任务,自动静默地通知你。这要用到 Claude Code 的 Stop 钩子——在 Claude 结束响应时触发的脚本。
Stop 钩子与 MCP 的分工很清晰:
两者可以并存:复杂结论走 MCP 工具(Claude 组织内容),简单"我干完了"走 Stop 钩子(脚本固定文案)。
先写一个独立的通知脚本 notify_on_stop.py(复用钉钉加签,但不走 MCP——因为它是脚本,不需要协议):
import sys, json, os, time, hmac, hashlib, base64, urllib.parse, urllib.request
def dingtalk_send(text: str) -> str:
webhook = os.environ["DINGTALK_WEBHOOK"]
secret = os.environ.get("DINGTALK_SECRET", "")
ts = int(round(time.time() * 1000))
if secret:
sign = urllib.parse.quote_plus(base64.b64encode(
hmac.new(secret.encode(), f"{ts}\n{secret}".encode(), hashlib.sha256).digest()))
webhook = f"{webhook}×tamp={ts}&sign={sign}"
data = json.dumps({"msgtype": "markdown",
"markdown": {"title": "Claude 完工", "text": text}}).encode()
req = urllib.request.Request(webhook, data=data,
headers={"Content-Type": "application/json"})
return urllib.request.urlopen(req, timeout=5).read().decode()
def main():
payload = json.load(sys.stdin) # Claude Code 把会话信息以 JSON 传入 stdin
if payload.get("stop_hook_active"): # 关键:避免与 MCP 通知互相触发导致循环
return
dingtalk_send("✅ Claude Code 已结束本轮任务,回终端看看吧。")
if __name__ == "__main__":
main()
然后在 Claude Code 的 settings.json 里注册这个钩子(项目级 .claude/settings.json 或用户级 ~/.claude/settings.json):
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command",
"command": "/home/you/notify-mcp/.venv/bin/python /home/you/notify-mcp/notify_on_stop.py" }
]
}
]
}
}
注意钩子脚本的运行环境要能读到 DINGTALK_WEBHOOK/DINGTALK_SECRET 环境变量。如果它们只写在 .mcp.json 的 env 里,钩子进程是读不到的——需要把这些变量也 export 到 shell 环境里(例如写进 ~/.bashrc 或一个专用的 envrc),或在脚本里直接 os.environ 读。
stop_hook_active 那一行检查很重要:如果 Claude 自己调了 send_notify 发消息,发完它停下来又触发 Stop 钩子发一遍,就可能陷入提醒风暴。加这层防护可以避免重复通知。
把前面学到的组合成一个真实可用的场景:在 Claude Code 帮你排查 CI 失败、改完代码后,自动把"改了什么、CI 现在状态"推送到飞书。完整工作流如下:
sequenceDiagram
participant U as 你
participant C as Claude Code
participant M as notify-mcp(MCP)
participant CI as GitHub Actions
participant F as 飞书群
U->>C: "CI 挂了,看下日志改一下,改完通知飞书"
C->>CI: gh run view / 读日志
CI-->>C: 失败堆栈
C->>C: 定位 bug 并修复代码
C->>CI: 推送修复 / 触发重跑
CI-->>C: 通过 ✅
C->>M: send_notify(message="修复 Lint 报错,CI 已绿", channel="feishu")
M->>F: POST webhook + 签名
F-->>U: 群消息: ✅ CI 已修复通过
Note over C: 同时 Stop 钩子兜底: "本轮任务结束"
这个案例的价值在于:它把 AI 从"被动回答"变成了"主动闭环交付"。你不盯着屏幕,CI 的修复结论会自己飞到群里。把它套到任何"耗时任务 + 需要告知他人"的场景上(数据清洗跑完通知、批量重构完通知、长时间 Agent 任务完通知)都成立。
markdown 类型支持标题、加粗、链接、图片;飞书用 msg_type=interactive 可发交互卡片。想要更好看的报告,把 msgtype 换掉、payload 结构按官方文档改即可。errcode=130101(频率限制)做识别与降级(合并多条消息为一条摘要)。channel 参数从枚举扩展为"群别名→webhook 映射表"配置文件,就能用一个工具推到不同项目群,由 Claude 根据任务上下文选群。| 维度 | 说明 |
|---|---|
| 适合训练营类型 | 后端/MCP 开发营、AI 工程化营、运维自动化营 |
| 学员基础 | 会 Python 函数、懂 HTTP,能看懂 HMAC 概念即可 |
| 建议教学时长 | 1.5~2 小时(含动手实现钉钉+飞书+钩子) |
| 可裁剪版本 | 只做钉钉单一渠道 + 骨架验证,45 分钟可完成 |
| 拓展作业 | 加企业微信/Slack/Discord 渠道;用 Channels 方向做反向推送;加失败重试与限流合并 |
本文核心是一条可复用的 MCP 交付路径:把任何"对外动作"封装成标准工具 → 注册进 Claude Code → 用钩子补齐自动化闭环。掌握这套范式,下一个你想让 AI 控制的东西(发邮件、建工单、调 K8s)都能照此办理。
py.sdk.modelcontextprotocol.io/server/code.claude.com/docs/en/channels-referenceopen.dingtalk.com/document/orgapp/enterprise-internal-robots-send-markdown-messagesopen.feishu.cn/document/client-docs/bot-v3/add-custom-bot@mcp.tool() 用法来源[!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 等场景全覆盖。
