前言

自己想要搭建一个MCP服务器帮助我管理日程。所以想自己编写一个。网上看到了一位B站UP主的视频按照他的想法编写了框架代码,然后又自己找资料、问AI来完成Caldav日历系统的操作。目前已经发布到服务器了挺好用的。(但就是有点费Token)。

我把所有踩过的坑和实际开发时的过程总结成了这篇文章希望对其他人有些帮助。

一、准备工作

  1. 安装UV

    Windows

    powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
    

    (如果下载比较慢可以尝试开代理)

    Linux

    curl -LsSf https://astral.sh/uv/install.sh | sh # 如果无法下载开代理
    pip install uv #服务器上用这个更好
    
  2. 初始化项目

    uv init [项目名]
    
  3. 配置虚拟环境

    Windows

    uv venv
    .venvScriptsactivate
    

    Linux

    uv venv
    source .venv/bin/activate
    
  4. 安装依赖

    uv add "mcp[cli]"
    
  5. 在IDE中打开项目

    项目中的main.py用来编写MCP代码,项目中的pyproject.toml是项目描述文件,README.md文件顾名思义是读我文件。

  6. 安装mcp包

    # 先进入虚拟环境
    pip install mcp
    
  7. 安装CalDav包

    # 先进入虚拟环境
    pip install caldav
    

二、了解Caldav文件结构

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

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核心代码-获取全部日历和程序入口

main.py

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这些。)

main.py

# 其他部分省略
@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核心代码-添加日程

main.py

# 其他部分省略
@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服务器

  1. 打开CherryStudio

  2. 找到设置中的MCP

  3. 安装CherryStudio内部UV

    image.png (我这里已经安装完了,所以是绿色的,未安装的话不是绿色的。)

  4. 配置服务器(点击添加下面的快速创建)

    image.png 命令那一栏写uv,其他的自己填写就行。

  5. 启动调试

四、部署

  1. "__main__"中的fastMCP.run(transport="stdio")中的stdio改成sse

  2. fastMCP.run()前添加fastMCP.settings.host="0.0.0.0"方便外网访问。

  3. 上传文件到服务器上

    1. 按照之前安装UV的步骤为服务器安装UV
    2. 将项目文件中的main.py、calendar_info.py(要保证目录结构)、pyproject.toml、config.json一起上传到服务器的项目目录中。
    3. 进入到项目中先创建一个虚拟环境uv venv
    4. 执行uv pip install .来安装整个项目的依赖环境
    5. 执行uv run main.py
    6. 如果提示缺少某个包,利用uv安装以一个就行`uv pip install xxx
  4. 修改Cherry Studio中的MCP服务器配置。将标准输出改成sse然后URL改成http://[ip地址]:[MCP端口,默认8000]/sse就可以了。

    image.png

五、已知问题

  1. 所有工具的注释内容过多,实际并不需要这么多的描述。
  2. 消耗Token有点多。
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]