Skip to main content
Glama

Apple Reminders MCP

by chenningling
reminder.py22.9 kB
# reminder.py from mcp.server.fastmcp import FastMCP import logging import subprocess import json from datetime import datetime, timedelta import re # 配置日志 logging.basicConfig( level=logging.DEBUG, # 从INFO改为DEBUG以获取更详细信息 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('reminder_mcp') # 创建 MCP 服务器 mcp = FastMCP("AppleReminder") # 确保工具函数正确注册 logger.info("开始注册MCP工具函数...") # 使用超时控制函数 def run_with_timeout(func, args=(), kwargs={}, timeout_seconds=10): """ 使用超时控制运行函数,默认超时时间减少到10秒 """ import threading import time result = [None] exception = [None] completed = [False] def worker(): try: result[0] = func(*args, **kwargs) completed[0] = True except Exception as e: exception[0] = e thread = threading.Thread(target=worker) thread.daemon = True # 设置为守护线程,主线程结束时会自动结束 thread.start() # 等待指定时间 thread.join(timeout_seconds) if completed[0]: return result[0] elif exception[0]: raise exception[0] else: raise TimeoutError(f"函数执行超过 {timeout_seconds} 秒超时") # 定义创建提醒事项函数 def create_reminder(title: str, notes: str = "", date: str = "", time: str = "") -> dict: """ 创建苹果提醒事项 参数: title: 提醒事项标题(必填) notes: 提醒事项备注(选填) date: 提醒日期,格式为 YYYY-MM-DD(选填) time: 提醒时间,格式为 HH:MM:SS(选填,需要提供 date 才有效) 返回: 包含创建结果的字典 """ try: logger.info(f"创建提醒事项: {title}, 备注: {notes}, 日期: {date}, 时间: {time}") # 构建 AppleScript 命令 script = create_reminder_applescript(title, notes, date, time) # 输出完整脚本以便调试 logger.info("生成的 AppleScript:") logger.info("-----------------------------") logger.info(script) logger.info("-----------------------------") # 使用超时控制执行 AppleScript (最多等待15秒) logger.info("正在执行 AppleScript (带超时控制)...") try: result = run_with_timeout(run_applescript, args=(script,), timeout_seconds=15) logger.info(f"AppleScript 执行结果: '{result.strip()}'") except TimeoutError: logger.warning("AppleScript 执行超时,返回错误") return { "success": False, "message": "创建提醒事项超时,请稍后再试" } # 更宽松的成功判断条件 if result.strip() and not result.lower().startswith("error"): # 如果有返回值且不是错误消息,则认为成功 reminder_id = result.strip() logger.info(f"提醒事项创建成功,ID: {reminder_id}") return { "success": True, "message": "提醒事项创建成功", "reminder_id": reminder_id, "details": { "title": title, "notes": notes, "date": date, "time": time } } else: logger.error(f"创建提醒事项失败: {result}") return { "success": False, "message": f"创建提醒事项失败: {result}" } except Exception as e: import traceback logger.error(f"创建提醒事项时出错: {str(e)}") logger.error(f"错误详情: {traceback.format_exc()}") return { "success": False, "message": f"创建提醒事项时出错: {str(e)}" } def create_reminder_applescript(title, notes, date, time): """ 创建用于添加提醒事项的 AppleScript 脚本 优化版本:直接使用ISO日期格式,减少日期时间处理步骤 """ # 转义标题和备注中的引号 title = escape_quotes(title) notes = escape_quotes(notes) # 基本脚本 - 创建提醒事项 script = f''' tell application "Reminders" set newReminder to make new reminder with properties {{name:"{ title }"}} set body of newReminder to "{ notes }"''' # 如果提供了日期,则添加日期设置 if date: try: # 解析日期字符串 (格式: YYYY-MM-DD) year, month, day = map(int, date.split('-')) # 构建日期时间字符串 date_time_str = f"{year}-{month:02d}-{day:02d}" # 如果提供了时间,则添加时间 if time: try: # 解析时间字符串 (格式: HH:MM:SS 或 HH:MM) time_parts = time.split(':') hour = int(time_parts[0]) minute = int(time_parts[1]) second = int(time_parts[2]) if len(time_parts) > 2 else 0 date_time_str += f" {hour:02d}:{minute:02d}:{second:02d}" except Exception as e: logger.warning(f"解析时间时出错: {str(e)},将使用默认时间 00:00:00") date_time_str += " 00:00:00" else: # 没有时间,默认为当天开始 date_time_str += " 00:00:00" # 使用更简洁的方式设置日期时间 script += f''' -- 设置提醒日期时间 set dueDate to date "{date_time_str}" set due date of newReminder to dueDate''' except Exception as e: logger.warning(f"解析日期时出错: {str(e)},将不设置提醒日期") # 完成脚本 script += ''' save return id of newReminder as string end tell ''' return script def escape_quotes(text): """ 转义字符串中的引号,以便在 AppleScript 中使用 """ return text.replace('"', '\\"') def run_applescript(script): """ 执行 AppleScript 并返回结果 """ process = subprocess.Popen( ['osascript', '-e', script], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) stdout, stderr = process.communicate() if process.returncode != 0: raise Exception(f"AppleScript 执行失败: {stderr}") return stdout # 日期范围解析函数 def parse_date_range(date_range: str): """ 解析日期范围字符串,返回开始和结束日期 参数: date_range: 日期范围字符串,如"今天"、"明天"、"本周"等 返回: (start_date, end_date): 开始和结束日期的元组,格式为"YYYY-MM-DD" """ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) # 默认为今天 start_date = today end_date = today + timedelta(days=1) # 今天结束(明天开始) # 处理特定日期范围 if "今天" in date_range or "今日" in date_range: # 保持默认值 pass elif "明天" in date_range or "明日" in date_range: start_date = today + timedelta(days=1) end_date = today + timedelta(days=2) elif "后天" in date_range: start_date = today + timedelta(days=2) end_date = today + timedelta(days=3) elif "本周" in date_range or "这周" in date_range: # 计算本周的开始(周一)和结束(下周一) weekday = today.weekday() # 0=周一, 6=周日 start_date = today - timedelta(days=weekday) # 本周一 end_date = start_date + timedelta(days=7) # 下周一 elif "下周" in date_range or "下星期" in date_range: # 计算下周的开始(下周一)和结束(下下周一) weekday = today.weekday() start_date = today - timedelta(days=weekday) + timedelta(days=7) # 下周一 end_date = start_date + timedelta(days=7) # 下下周一 elif "本月" in date_range or "这个月" in date_range: # 本月第一天到下个月第一天 start_date = today.replace(day=1) if today.month == 12: end_date = today.replace(year=today.year+1, month=1, day=1) else: end_date = today.replace(month=today.month+1, day=1) elif "下个月" in date_range: # 下个月第一天到下下个月第一天 if today.month == 12: start_date = today.replace(year=today.year+1, month=1, day=1) end_date = today.replace(year=today.year+1, month=2, day=1) elif today.month == 11: start_date = today.replace(month=12, day=1) end_date = today.replace(year=today.year+1, month=1, day=1) else: start_date = today.replace(month=today.month+1, day=1) end_date = today.replace(month=today.month+2, day=1) else: # 尝试解析具体日期 try: # 尝试匹配年月日格式,如"2025年5月20日" pattern = r'(\d{4})\s*\u5e74\s*(\d{1,2})\s*\u6708\s*(\d{1,2})\s*\u65e5' match = re.search(pattern, date_range) if match: year, month, day = map(int, match.groups()) start_date = datetime(year, month, day) end_date = start_date + timedelta(days=1) else: # 尝试匹配月日格式,如"5月20日" pattern = r'(\d{1,2})\s*\u6708\s*(\d{1,2})\s*\u65e5' match = re.search(pattern, date_range) if match: month, day = map(int, match.groups()) year = today.year # 如果指定的月日已经过去,则假设是明年 if (month < today.month) or (month == today.month and day < today.day): year += 1 start_date = datetime(year, month, day) end_date = start_date + timedelta(days=1) else: # 尝试匹配星期格式,如"周五" weekday_map = { '一': 0, '二': 1, '三': 2, '四': 3, '五': 4, '六': 5, '日': 6, '天': 6 } pattern = r'\u5468(\u4e00|\u4e8c|\u4e09|\u56db|\u4e94|\u516d|\u65e5|\u5929)' match = re.search(pattern, date_range) if match: target_weekday = weekday_map[match.group(1)] days_ahead = (target_weekday - today.weekday()) % 7 if days_ahead == 0: days_ahead = 7 # 如果是今天的星期,则往前推一周 start_date = today + timedelta(days=days_ahead) end_date = start_date + timedelta(days=1) except (ValueError, AttributeError): # 如果解析失败,使用默认值(今天) logger.warning(f"无法解析日期范围: {date_range},使用默认值(今天)") # 返回格式化的日期字符串 return ( start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d") ) # 创建查询提醒事项的 AppleScript def create_get_reminders_applescript(start_date, end_date): """ 创建用于查询提醒事项的 AppleScript 脚本 参数: start_date: 开始日期,格式为"YYYY-MM-DD" end_date: 结束日期,格式为"YYYY-MM-DD" 返回: AppleScript 脚本字符串 """ # 添加时间信息到日期字符串 start_date_time = f"{start_date} 00:00:00" end_date_time = f"{end_date} 23:59:59" # 优化版本,简化查询逻辑,减少循环嵌套层数 script = f''' tell application "Reminders" -- 设置日期范围 set theStartDate to date "{start_date_time}" set theEndDate to date "{end_date_time}" -- 初始化结果列表 set reminderList to {{}} -- 直接获取所有提醒事项,减少嵌套循环 set allReminders to {{}} -- 尝试使用更高效的查询方式 try -- 直接获取所有提醒事项 set allReminders to (reminders whose due date is greater than or equal to theStartDate and due date is less than or equal to theEndDate) on error -- 如果上面的方法失败,则使用备用方法 tell default account set allLists to every list repeat with aList in allLists set theReminders to (reminders in aList whose due date is greater than or equal to theStartDate and due date is less than or equal to theEndDate) set allReminders to allReminders & theReminders end repeat end tell end try -- 处理所有找到的提醒事项 repeat with aReminder in allReminders set reminderName to name of aReminder -- 获取备注 set reminderBody to "" if body of aReminder is not missing value then set reminderBody to body of aReminder end if -- 获取日期时间,使用更简洁的方式 set reminderDueDate to due date of aReminder set reminderDateStr to (year of reminderDueDate as text) & "-" & (month of reminderDueDate as text) & "-" & (day of reminderDueDate as text) set reminderTimeStr to (hours of reminderDueDate as text) & ":" & (minutes of reminderDueDate as text) -- 组装提醒信息 set reminderInfo to reminderName & "|||" & reminderBody & "|||" & reminderDateStr & "|||" & reminderTimeStr set end of reminderList to reminderInfo end repeat return reminderList end tell ''' return script # 解析提醒事项结果 def parse_reminders_result(result): """ 解析 AppleScript 返回的提醒事项结果 参数: result: AppleScript 返回的结果字符串 返回: 提醒事项列表 """ reminders = [] # 如果结果为空或只有空格,返回空列表 if not result or result.strip() == "{}": return reminders # 处理 AppleScript 返回的列表格式 # 去除大括号和空格 result = result.strip().strip('{}') # 如果结果为空,返回空列表 if not result: return reminders # 分割每个提醒事项 items = result.split(", ") for item in items: # 去除引号 item = item.strip('"') # 按分隔符分割各个字段 parts = item.split("|||") if len(parts) >= 4: title = parts[0] notes = parts[1] date_str = parts[2] time_str = parts[3] # 添加到提醒事项列表 reminders.append({ "title": title, "notes": notes, "date": date_str, "time": time_str }) return reminders def get_reminders(date_range: str = "今天") -> dict: """ 查询指定日期范围内的提醒事项 参数: date_range: 日期范围,如"今天"、"明天"、"本周"等 返回: 包含提醒事项列表的字典 """ try: logger.info(f"查询提醒事项,日期范围: {date_range}") # 解析日期范围 start_date, end_date = parse_date_range(date_range) logger.info(f"解析后的日期范围: {start_date} 到 {end_date}") # 构建 AppleScript 脚本 script = create_get_reminders_applescript(start_date, end_date) # 输出完整脚本以便调试 logger.info("生成的 AppleScript:") logger.info("-----------------------------") logger.info(script) logger.info("-----------------------------") # 使用超时控制执行 AppleScript (减少超时时间到 8 秒) logger.info("正在执行 AppleScript (带超时控制)...") try: result = run_with_timeout(run_applescript, args=(script,), timeout_seconds=8) # 解析结果 reminders = parse_reminders_result(result) logger.info(f"找到 {len(reminders)} 个提醒事项") if len(reminders) == 0: logger.info("没有找到提醒事项") success = True message = f"成功获取{date_range}的提醒事项" except TimeoutError: logger.warning("AppleScript 执行超时,返回空结果") # 超时时返回部分成功的信息 reminders = [] success = False message = f"查询{date_range}提醒事项超时,请稍后再试或缩小日期范围" # 格式化日期范围描述 date_description = date_range return { "success": success, "message": message, "date_range": { "start": start_date, "end": end_date, "description": date_description }, "reminders": reminders, "count": len(reminders) } except Exception as e: import traceback logger.error(f"查询提醒事项时出错: {str(e)}") logger.error(f"错误详情: {traceback.format_exc()}") return { "success": False, "message": f"查询提醒事项时出错: {str(e)}" } # 手动注册工具函数 def register_tools(): """手动注册工具函数到MCP服务器""" logger.info("手动注册工具函数...") # 确保工具函数被正确注册 try: # 检查FastMCP实例的工具管理器 if hasattr(mcp, '_tool_manager'): tool_manager = mcp._tool_manager logger.info(f"工具管理器方法: {dir(tool_manager)}") # 尝试多种注册方式 # 方式1: 使用tool装饰器函数直接调用 logger.info("方式1: 使用tool装饰器函数直接调用") create_reminder_tool = mcp.tool(name="create_reminder")(create_reminder) get_reminders_tool = mcp.tool(name="get_reminders")(get_reminders) logger.info(f"create_reminder_tool: {create_reminder_tool}") logger.info(f"get_reminders_tool: {get_reminders_tool}") # 方式2: 如果有register_tool方法,尝试使用它 if hasattr(mcp, 'register_tool'): logger.info("方式2: 使用register_tool方法") mcp.register_tool("create_reminder", create_reminder) mcp.register_tool("get_reminders", get_reminders) # 方式3: 如果工具管理器有register_tool方法,直接使用 if hasattr(mcp, '_tool_manager') and hasattr(mcp._tool_manager, 'register_tool'): logger.info("方式3: 使用工具管理器的register_tool方法") mcp._tool_manager.register_tool( name="create_reminder", fn=create_reminder, description="创建苹果提醒事项" ) mcp._tool_manager.register_tool( name="get_reminders", fn=get_reminders, description="查询指定日期范围内的提醒事项" ) # 方式4: 尝试直接设置工具字典 if hasattr(mcp._tool_manager, 'tools') and isinstance(mcp._tool_manager.tools, dict): logger.info("方式4: 直接设置工具字典") mcp._tool_manager.tools['create_reminder'] = create_reminder mcp._tool_manager.tools['get_reminders'] = get_reminders logger.info("工具注册完成") except Exception as e: import traceback logger.error(f"注册工具函数时出错: {str(e)}") logger.error(f"错误详情: {traceback.format_exc()}") # 启动服务器 if __name__ == "__main__": # 打印注册的工具列表 logger.info("已注册的MCP工具:") # 检查工具注册情况 tools = [attr for attr in dir(mcp) if not attr.startswith("_")] for tool_name in tools: logger.info(f" - {tool_name}") # 手动注册工具函数 register_tools() # 检查工具函数注册情况: logger.info("检查工具函数注册情况:") # 检查工具管理器 if hasattr(mcp, '_tool_manager'): tool_manager = mcp._tool_manager logger.info(f"找到工具管理器: {tool_manager}") logger.info(f"工具管理器属性: {dir(tool_manager)}") # 尝试获取已注册的工具 if hasattr(tool_manager, 'tools'): logger.info(f"工具容器类型: {type(tool_manager.tools)}") if isinstance(tool_manager.tools, dict) and tool_manager.tools: logger.info("已注册的工具:") for tool_name, tool_func in tool_manager.tools.items(): logger.info(f" - 工具: {tool_name}, 函数: {tool_func}") elif isinstance(tool_manager.tools, list) and tool_manager.tools: logger.info("已注册的工具(列表):") for tool in tool_manager.tools: logger.info(f" - 工具: {tool}") else: logger.info(f"工具容器为空或不是预期类型: {tool_manager.tools}") else: logger.info("工具管理器没有tools属性") # 检查其他可能的工具容器 for attr_name in dir(tool_manager): if 'tool' in attr_name.lower() and not attr_name.startswith('_'): attr_value = getattr(tool_manager, attr_name) logger.info(f"工具管理器属性: {attr_name}, 类型: {type(attr_value)}") if isinstance(attr_value, (dict, list)) and attr_value: logger.info(f"内容: {attr_value}") else: logger.info("没有找到工具管理器") # 检查FastMCP实例的__dict__属性 logger.info("检查FastMCP实例的所有属性:") for attr_name, attr_value in mcp.__dict__.items(): if 'tool' in attr_name.lower(): logger.info(f" - 属性: {attr_name}, 类型: {type(attr_value)}") mcp.run(transport="stdio")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chenningling/MCP-AppleReminders'

If you have feedback or need assistance with the MCP directory API, please join our Discord server