import os
import logging
import requests
from fastmcp import FastMCP
from pydantic import Field
_LOGGER = logging.getLogger(__name__)
NTFY_ACTIONS_RULE = """
The following actions are supported:
- view: Opens a website or app when the action button is tapped
- broadcast: Sends an Android broadcast intent when the action button is tapped (only supported on Android)
- http: Sends HTTP POST/GET/PUT request when the action button is tapped
```json
[
{
"action": "view",
"label": "Open portal",
"url": "https://home.nest.com/",
"clear": true
},
{
"action": "http",
"label": "Turn down",
"url": "https://api.nest.com/",
"body": "{\"temperature\": 65}"
},
{
"action": "broadcast",
"label": "Take picture",
"extras": {
"cmd": "pic",
"camera": "front"
}
}
]
```
"""
def add_tools(mcp: FastMCP):
@mcp.tool(
title="钉钉群机器人-发送文本消息",
description="钉钉群机器人发送文本或Markdown消息",
)
def ding_send_text(
text: str = Field(description="消息内容"),
title: str = Field("", description="消息标题"),
msgtype: str = Field("markdown", description="内容类型,仅支持: text/markdown"),
bot_key: str = Field("", description="钉钉群机器人access_token,默认从环境变量获取"),
):
"""
https://open.dingtalk.com/document/development/custom-robots-send-group-messages
"""
if msgtype == "markdown":
body = {"title": title, "text": text}
else:
body = {"content": f'{title}\n{text}'.strip()}
if not bot_key:
bot_key = os.getenv("DINGTALK_BOT_KEY", "")
base = os.getenv("DINGTALK_BASE_URL") or "https://oapi.dingtalk.com"
res = requests.post(
f"{base}/robot/send?access_token={bot_key}",
json={"msgtype": msgtype, msgtype: body},
)
return res.json()
@mcp.tool(
title="飞书/Lark机器人-发送文本消息",
description="飞书/Lark群机器人发送文本或Markdown消息",
)
def lark_send_text(
text: str = Field(description="消息内容"),
msgtype: str = Field("markdown", description="内容类型,仅支持: text/markdown"),
bot_key: str = Field("", description="飞书/Lark机器人key,uuid格式,默认从环境变量获取"),
is_lark: int = Field(0, description="根据用户描述识别 0:飞书 1:Lark"),
):
"""
https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN
https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot
"""
if msgtype == "markdown":
body = {
"msg_type": "interactive",
"card": {"elements": [{"tag": msgtype, "content": text}]},
}
else:
body = {"msg_type": msgtype, "content": {"text": text}}
if not bot_key:
bot_key = os.getenv("LARK_BOT_KEY" if is_lark else "FEISHU_BOT_KEY", "")
if is_lark:
base = os.getenv("LARK_BASE_URL") or "https://open.larksuite.com"
else:
base = os.getenv("FEISHU_BASE_URL") or "https://open.feishu.cn"
res = requests.post(
f"{base}/open-apis/bot/v2/hook/{bot_key}",
json=body,
)
return res.json()
@mcp.tool(
title="Bark推送通知",
description="通过Bark推送通知",
)
def bark_send_notify(
body: str = Field(description="推送内容"),
title: str = Field("", description="推送标题"),
subtitle: str = Field("", description="推送副标题"),
device_key: str = Field("", description="设备key,默认从环境变量获取"),
url: str = Field("", description="点击推送时,跳转的URL ,支持URL Scheme 和 Universal Link"),
icon: str = Field("", description="自定义图标URL"),
level: str = Field(
"active",
description="推送中断级别。"
"critical: 重要警告, 在静音模式下也会响铃"
"active:默认值,系统会立即亮屏显示通知"
"timeSensitive:时效性通知,可在专注状态下显示通知。"
"passive:仅将通知添加到通知列表,不会亮屏提醒。",
),
volume: int = Field(5, description="重要警告的通知音量(0-10),默认为5"),
):
"""
https://bark.day.app/#/tutorial
"""
if not device_key:
device_key = os.getenv("BARK_DEVICE_KEY", "")
base = os.getenv("BARK_BASE_URL") or "https://api.day.app"
res = requests.post(
f"{base}/{device_key}",
json={
"body": body,
"title": title,
"subtitle": subtitle,
"url": url,
"icon": icon,
"level": level,
"volume": volume,
},
)
return res.json()
@mcp.tool(
title="Ntfy Push Notification",
description="Push a notification via Ntfy",
)
def ntfy_send_notify(
message: str = Field(description="Notification message body; set to `triggered` if empty or not passed"),
title: str = Field("", description="Notification title"),
topic: str = Field("", description="Target topic name or URL"),
click: str = Field("", description="URL opened when notification is clicked"),
attach: str = Field("", description="URL of an attachment"),
icon: str = Field("", description="URL of notification icon"),
markdown: bool = Field(False, description="Set to `true` if the message is Markdown-formatted"),
filename: str = Field("", description="File name of the attachment"),
priority: int = Field(3, description="Message priority with 1=min, 3=default and 5=max"),
delay: str = Field("", description="Timestamp or duration for delayed delivery. Example: 30min, 9am"),
actions: list | None = Field(None, description=f"List of action buttons.{NTFY_ACTIONS_RULE}"),
):
"""
https://docs.ntfy.sh/publish/#publish-as-json
"""
base = os.getenv("NTFY_BASE_URL") or "https://ntfy.sh"
if topic and topic.startswith("http"):
base, topic = topic.rsplit("/", 1)
if not topic:
topic = os.getenv("NTFY_DEFAULT_TOPIC", "")
data = {
"topic": topic,
"title": title,
"message": message,
"click": click,
"icon": icon,
"markdown": markdown or False,
"priority": priority,
"delay": delay,
}
if attach:
data["attach"] = attach
data["filename"] = filename
if actions:
data["actions"] = actions
res = requests.post(f"{base}", json=data)
return res.json()
@mcp.tool(
title="PushPlus推送消息",
description="通过PushPlus(推送加)推送消息",
)
def pushplus_send_msg(
content: str = Field(description="消息内容"),
title: str = Field("", description="消息标题"),
token: str = Field("", description="用户token,默认从环境变量获取"),
template: str = Field("", description="消息内容格式: `html`(默认)/`txt`/`markdown`"),
channel: str = Field("", description="发送渠道: `wechat`(默认)/`webhook`/`mail`"),
):
"""
https://www.pushplus.plus/doc/guide/api.html
"""
if not token:
token = os.getenv("PUSH_PLUS_TOKEN", "")
base = os.getenv("PUSH_PLUS_BASE_URL") or "http://www.pushplus.plus"
res = requests.post(
f"{base}/{token}",
json={
"content": content,
"title": title,
"template": template,
"channel": channel,
},
)
return res.json()