前言
自己想要搭建一个MCP服务器帮助我管理日程。所以想自己编写一个。网上看到了一位B站UP主的视频按照他的想法编写了框架代码,然后又自己找资料、问AI来完成Caldav日历系统的操作。目前已经发布到服务器了挺好用的。(但就是有点费Token)。
我把所有踩过的坑和实际开发时的过程总结成了这篇文章希望对其他人有些帮助。
一、准备工作
-
安装UV
Windows
shellpowershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
(如果下载比较慢可以尝试开代理)
Linux
bashcurl -LsSf https://astral.sh/uv/install.sh | sh # 如果无法下载开代理 pip install uv #服务器上用这个更好
-
初始化项目
bashuv init [项目名]
-
配置虚拟环境
Windows
shelluv venv .venv\Scripts\activate
Linux
bashuv venv source .venv/bin/activate
-
安装依赖
shelluv add "mcp[cli]"
-
在IDE中打开项目
项目中的main.py用来编写MCP代码,项目中的pyproject.toml是项目描述文件,README.md文件顾名思义是读我文件。
-
安装mcp包
bash# 先进入虚拟环境 pip install mcp
-
安装CalDav包
bash# 先进入虚拟环境 pip install caldav
二、了解Caldav文件结构
vbnet
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:20200516T060000Z-123401@example.com
DTSTAMP:20200516T060000Z
DTSTART:20200517T060000Z
DTEND:20200517T230000Z
RRULE:FREQ=YEARLY
SUMMARY:Do the needful
END:VEVENT
END:VCALENDAR
以下内容根据Radicale所使用的icalendar标准总结
VEVENT
表示该文件是一个日程(事件)
VTODO
表示该文件是一个待办
DTSTART
表示开始时间
DTEND
表示结束时间(注意,待办的结束时间是DUE
不是DTEND
)
SUMMARY
标题(不是备注信息)
LOCATION
日程地理位置(比如:人民公园。注意:如果日程没有指定地理位置,这个属性就不存在,因此在写程序时要判断一下)
STATUS
待办事项的完成状态,完成了是COMPLETED
PRIORITY
待办事项的优先级,是一个整数
更多字段信息可以通过event.data
来获取。
三、开始编写代码
我们可以在项目中创建一些其他模块文件。
先创建日程的封装类 和任务的封装类
entities/calendar_info.py
python
class CalendarEventInfo:
"""
日历日程信息类
用于存储和管理日历事件的相关信息,包括事件的基本属性和操作方法。 该类提供了事件信息的封装、访问和修改功能。 属性:
name(str):日程名
start_time(datetime):开始时间
end_time(datetime):结束时间
calendar_name(str):所属日历
返回值:
CalendarEventInfo实例对象
""" def __init__(self,calendar_name:str,name:str,start_time:datetime,end_time:datetime):
self.name=name
self.start_time=start_time
self.end_time=end_time
self.calendar_name=calendar_name
def to_dict(self):
return {
"calendar":self.calendar_name,
"name":self.name,
"start_time":self.start_time,
"end_time":self.end_time
}
@staticmethod
def from_dic(dic):
return CalendarEventInfo(dic["calendar_name"],dic["name"],dic["start_time"],dic["end_time"])
def to_LLM(self)->str:
"""
用于转化为大模型能理解的文字。
"""
return f"日历:{self.calendar_name},日程:{self.name}\n时间:{self.start_time}~{self.end_time}\n"
class CalendarTodoInfo:
"""
日历待办信息类
属性:
name(str): 待办名
start_time(str):开始时间
end_time(str):结束时间
calendar_name(str):所属日历的名字
status(str):任务状态
priority(int)=0:优先级
""" def __init__(self,calendar_name:str,name:str,start_time:datetime,end_time:datetime,priority:int=0):
self.name=name
self.start_time=start_time
self.end_time=end_time
self.calendar_name=calendar_name
self.status=""
self.priority=priority
def to_dict(self):
return {
"calendar":self.calendar_name,
"name":self.name,
"start_time":self.start_time,
"end_time":self.end_time,
"status":self.status,
"priority":self.priority
}
@staticmethod
def from_dic(dic):
return CalendarTodoInfo(dic["calendar_name"],dic["name"],dic["start_time"],dic["end_time"],dic["priority"])
def to_LLM(self)->str:
return f"日历:{self.calendar_name},待办:{self.name}\n时间:{self.start_time}~{self.end_time}\n状态:{self.status}\n优先级:{self.priority}\n"
编写MCP核心代码-获取全部日历和程序入口
python
from mcp.server.fastmcp import FastMCP
from caldav.davclient import get_davclient
from datetime import datetime
# 创建MCP客户端
fastMCP=FastMCP("Calendar")
client=None
# 处理时间的函数
def to_datetime(strDatetime:str,format="%Y-%m-%dT%H:%M:%S"):
return datetime.strptime(strDatetime,format)
@mcp.tool("list_calendars")
async def list_calendars():
# 必须写多行注释,方便大模型理解工具的用途。
"""
获取所有日历
:return: 日历名
"""
# 创建caldav客户端主体,日历所有操作都通过主体实现。
principal=client.principal()
calendars=principal.caldendars()
calendar_str=""
# 组织返回文本,大模型以return的表达式作为工具的结果输出。同时,不建议输出JSON字符串。
for calendar in calendars:
calendar_str+=calendar.get_display_name()+"\n"
return f"找到了以下日历:\n{calendar_str}"
if __name__=="__main__":
config_JSON=json.loads(open("config.json","r",encoding="utf-8").read())
client=get_davclient(username=config_JSON["calendar_username"],
password=config_JSON["calendar_password"],
url=config_JSON["calendar_url"])
mcp.run(transport="stdio")
编写MCP核心代码-获取日程
先用calendar.date_search()
搜索出指定日期范围内的日程。得到event
列表。再用for遍历列表得到event对象,再通过event对象的icalendar_component
属性来获取事件字段的值(DTSTART、SUMMARY这些。)
python
# 其他部分省略
@fastMCP.tool("get_event")
async def get_events(start_time:str,end_time:str):
"""
获取指定日期的日程(日期的格式是:2025-09-29T10:00:00)
:param start_time: 指定的开始日期
:end_time: 指定的结束日期
:return: 日程信息
"""
principal=client.principal()
calendars=principal.calendars()
events_result=""""""
new_start_time=to_datetime(start_time)
new_end_time=to_datetime(end_time)
for calendar in calendars:
events = calendar.date_search(start=new_start_time, end=new_end_time)
print(events)
for event in events:
eventInfo=CalendarEventInfo(calendar.get_display_name(),event.icalendar_component["SUMMARY"],event.icalendar_component["DTSTART"].dt,event.icalendar_component["DTEND"].dt)
events_result+=eventInfo.to_LLM()
return events_result
编写MCP核心代码-添加日程
python
# 其他部分省略
@fastMCP.tool("create_event")
async def creat_event(calendar_name:str,name:str,start_time:str,end_time:str,location:str=""):
"""
创建日程(日期的格式是:2025-09-29T10:00:00)
:param calendar_name: 指定的日历名称(模糊查询),用户未指定时询问用户保存到哪个日历里
:param name: 日程名
:param start_time: 日程开始时间
:param end_time: 日程结束时间
:param location: 日程地理位置
:return: 完成状态
""" principal = client.principal()
calendars = principal.calendars()
new_start_time=to_datetime(start_time)
new_end_time=to_datetime(end_time)
calendar=None
for c in calendars:
if re.match(f"(.*){calendar_name}(.*)",c.get_display_name()):
calendar=c
break
if calendar is None:
return "未找到日历"
else:
event=calendar.save_event(summary=name,location=location,
dtstart=new_start_time,dtend=new_end_time)
if event!=None:
return f"将日程{name}添加到日历{calendar.get_display_name()}成功"
else:
return "添加日程失败"
(待办事项的操作和日程操作的过程是一样的,只不过icalendar_component属性中的键有一些不一样。)
四、使用MCP服务器
-
打开CherryStudio
-
找到设置中的MCP
-
安装CherryStudio内部UV
(我这里已经安装完了,所以是绿色的,未安装的话不是绿色的。)
-
配置服务器(点击添加下面的快速创建)
命令那一栏写uv,其他的自己填写就行。
-
启动调试
四、部署
-
将
"__main__"
中的fastMCP.run(transport="stdio")
中的stdio改成sse -
在
fastMCP.run()
前添加fastMCP.settings.host="0.0.0.0"
方便外网访问。 -
上传文件到服务器上
- 按照之前安装UV的步骤为服务器安装UV
- 将项目文件中的main.py、calendar_info.py(要保证目录结构)、pyproject.toml、config.json一起上传到服务器的项目目录中。
- 进入到项目中先创建一个虚拟环境
uv venv
- 执行
uv pip install .
来安装整个项目的依赖环境 - 执行
uv run main.py
- 如果提示缺少某个包,利用uv安装以一个就行`uv pip install xxx
-
修改Cherry Studio中的MCP服务器配置。将标准输出改成sse然后URL改成
http://[ip地址]:[MCP端口,默认8000]/sse
就可以了。
五、已知问题
- 所有工具的注释内容过多,实际并不需要这么多的描述。
- 消耗Token有点多。