用 MCP 和 Keycard 构建日程规划 Agent 的实践指南
分享如何用 MCP 集成日历、任务、邮件等工具,构建自动化工作流 Agent,具有强实践参考价值。
完整中文译文
我的工作日分散在许多彼此割裂的工具中:Google Calendar、Linear、Gmail、Google Docs、GitHub、Slack,以及 Granola 会议笔记。没有一个统一视图能够回答:“我现在应该做什么?”
AI 正在迅速改变我们的工作方式。MCP 服务器、AI 智能体以及 Claude Cowork 之类的功能,可以连接多个数据源,并通过对话、现成可用的 MCP 服务器和工具集成来帮助你规划一天。但我一直习惯亲自确定工作优先级和组织工作,主要依靠各种列表、不同的项目管理工具、Slack 提醒、Gmail 标签和日历时间块拼凑出一套勉强可用的方案。
这始终是一团乱麻,但它是属于我的一团乱麻。现成的 AI 智能体可以帮助我汇总数据,但我仍然需要在脑中对所有事情进行优先级排序和分析。这并不是工具可用性的问题,而是推理的问题。而且对我来说,这套推理必须属于我自己,这一点一直非常重要。我想要一个不只是展示所有事项,还能替我思考的仪表盘,并且它的思考方式要和我一样。
于是,我构建了一个 AI 智能体,让它读取我的所有工作工具并帮助我规划一天。不过很多人都做过类似的东西,所以更重要的是:我在没有为身份验证部分抓狂,也没有为此苦苦折腾数周的情况下完成了它。
我构建的产品:“Plan My Today”
Plan My Today 是一个每日规划仪表盘,其中嵌入了一个我称为“Todaygent”的聊天机器人。Plan My Today MCP 服务器会调用多个数据源、查找待办事项,并将它们汇总到一个统一且按优先级排列的视图中。随后,Todaygent 会基于真实数据,分析我的工作优先级、阻塞事项和会议准备工作。
在花时间定义 Todaygent 能做什么的同时,明确说明 Todaygent 不能做什么也同样重要。对于自己那份乱糟糟的任务清单,我一直多少有点自虐倾向……而且我很享受亲手勾掉一项任务时带来的多巴胺冲击。我不希望 AI 智能体随意移动我的任务,或者将任务标记为“Done”。
我想要的,只是一个足够聪明的东西,能够像拥有大量空闲时间的我一样进行规划和组织(不过没人真的有那么多空闲时间,对吧?)。它甚至不必规划今天以外的事情。如果说我从自己的规划习惯中学到了一件事,那就是我的一天每个小时都在变化。任何提前超过一天制订的计划,最终都会被完全打乱。我喜欢在源应用中亲自管理任务。我真正需要帮助的,只是在工具极度分散的情况下进行每日规划。
Todaygent 对每个数据源都只有只读访问权限,而且它清楚地知道自身的局限。它使用本地存储,让我可以忽略那些无法在源头忽略的事项,例如会议笔记或 Slack 中提及我的消息。
经过严格使用后,我还意识到,有些任务需要根据只有身为人类的我才掌握、而冰冷生硬的数据中并未记录的上下文来提高或降低优先级。MCP 服务器为这些操作分别提供了工具,并将我的更新写入本地 JSON 存储进行跟踪。至于其他所有内容——Linear、GitHub、Gmail、Calendar,以及我明确标记的 Slack 消息——我会亲自在源应用中更改其状态。Todaygent 不需要拥有外部工具的写入权限,也能为我规划一天。
应用仪表盘提供两种视图:按优先级分组(紧急/高/中/低)和按来源分组。此外,它还支持浅色/深色/系统主题,以及搜索、筛选和排序。
架构概览
Plan My Today 的架构如下:
所有 MCP 工具都通过一个中央注册表进行管理。需要访问外部 API 的工具遵循统一模式:Zod schema → async handler → extract authInfo → Keycard token exchange → MCP-formatted response。每个工具对应一个文件,并通过 TOOL_REGISTRY 数组进行注册。
聚合工具:list-all-tasks-today
就是它:这个工具会从每个第三方数据源提取内容、计算优先级,并通过交叉引用来丰富任务信息。
所有数据源都通过 Promise.allSettled() 并发获取。每次获取都会分别执行自己的 Keycard 令牌交换(身份验证部分会进一步介绍)。每项任务都会获得一个优先级分数。每个数据源都有自己的评分算法,使用不同的基础分数和信号权重。为了调好这些参数,我付出了大量精力;如果发现有什么不对,我就会根据需要调整权重和评分方式。Todaygent 会像我一样思考;如果它做不到……我就强迫它做到。MCP 服务器还会对相关任务进行交叉引用,以便帮助 Todaygent,也让我在查看仪表盘时不至于崩溃。
开销较高的多数据源获取操作(list-all-tasks-today)会写入本地 JSON 文件。另一个独立工具会读取本地任务列表,按需向 Todaygent 和仪表盘提供经过信息增强的任务列表。如果我添加或完成了任务,可以再次触发 list-all-tasks-today。不会浪费 token。
会议笔记是最混乱的数据源,因为行动事项经常在对话中被随口提及,而且表达得非常微妙,正则表达式无法识别。在加入 LLM 增强功能之前,我调试了大量遗漏项和误报。后端 LLM 会进行第二轮分析,以捕捉对话式的隐含内容。如果我在自然对话中主动承担或同意做某件事,LLM 能够识别出来,而正则表达式做不到。
身份验证挑战,以及 Keycard 如何解决它
就连 AI 都知道,身份验证才是最难的部分。这 8 个数据源涉及 7 种不同资源,每一种都需要执行令牌交换。其中包括 Google Calendar API、Gmail API、Drive API、Drive Activity API、Linear、GitHub 和 Slack 的作用域授权。浏览器无法安全地存储 API 密钥。为每个提供商定制身份验证,意味着需要分别实现 7 套 OAuth。我在身份与访问管理领域拥有丰富经验,因此我很清楚,这是一项工作量巨大、而且很多地方都可能出错的任务。
解决方案:使用 Keycard 作为 Todaygent 的控制平面
所有操作都通过一次 Keycard OAuth 登录完成,并使用 Google 作为身份提供商。浏览器运行 PKCE 流程,客户端密钥则保留在服务器端的机密后端中。完成身份验证后,我会从 Keycard 获得一个 JSON Web Token(JWT)。
令牌中介后端架构
我做出了一项架构决策:使用令牌中介后端(Token-Mediating Backend,TMB)。React 前端将 Keycard JWT 保存在 sessionStorage 中,并在每次向 Express 后端发送请求时,将其作为 bearer token 一并发送。后端负责处理所有令牌交换工作:它使用 JWT 请求特定提供商的凭证、调用 API,然后返回结果。源服务的令牌永远不会接触浏览器。
RFC 8693 令牌交换:关键机制
当某次工具调用需要访问第三方 API 时,我的 Keycard JWT 会被交换成特定提供商的访问令牌。请求中会包含我的 Keycard JWT,以及用于指明我希望访问哪个 API 的资源 URL。随后,Keycard 会为我提供一个短期访问令牌,而且该令牌仅适用于我需要的提供商。
但等等……Google 的所有服务不都应该归属于同一个 OAuth 提供商吗?确实如此,但这里调用的是多个 API。Keycard 会签发作用域限定到具体任务的临时凭证。我不希望从 Google 获得一个权限过高、能够访问所有这些服务的访问令牌。当 Todaygent 想查看 Calendar 时,我要的是一个只能以只读方式访问 Calendar API 的令牌;当我想查看分配给我的评论时,我要的是一个只能以只读方式访问 Drive API 的令牌……你懂我的意思。这样做很安全,会留下明确的审计记录,也能确保 Todaygent 针对每项具体任务都只能获得它所需的权限。不存在持续有效的访问权限,也不存在权限过高的凭证。
注意:Slack 的“OAuth 2.0”实现并不遵循 OAuth 2.0 规范。Keycard 支持非正统或自定义的“OAuth”,同时也提供预先配置好的热门提供商目录。如果某个提供商不遵循标准,Keycard 会直接替你“搞定它”。
临时令牌与性能
每次工具调用都会从 Keycard 交换一个全新的令牌,将其用于 API 调用,然后丢弃。每个操作使用一个令牌。提供商的访问令牌永远不会被存储。
AI 智能体授权就应该这样工作:AI 智能体不应该以超过实际需要的时间持有凭证。静态密钥和长期有效的令牌会带来无法监控的风险,而这正是 Keycard 旨在解决的问题。
这也是 Keycard 工作方式的核心:策略是在签发凭证时进行评估,而不是在使用凭证时进行评估。如果 Keycard 的策略规定 Todaygent 不应在这次工具调用中访问我的 Gmail,那么 Gmail API 访问令牌从一开始就不会存在。没有什么可以被拦截,因为根本就没有签发任何内容。
“但这么多令牌交换不会很慢吗?”
其实不会,原因如下。
并行交换消化了延迟。列表工具通过 Promise.allSettled() 并发运行所有令牌交换。延迟成本取决于最慢的一次交换(约 150~250 毫秒),而不是所有交换时间的总和。反正我本来就要等待响应最慢的 API,而在所有 API 数据返回之前,全部令牌交换早已完成。
这种模式可以概括为“一次获取,多次推理”。AI 智能体在每次对话中调用一次 list-all-tasks-today,然后在多个聊天轮次中基于结果进行推理,无需重新获取数据。每次聚合调用只使用一组凭据,之后便不再需要凭据。Todaygent 也提供了针对各数据源的 MCP 工具;单独调用这些工具时,它会获得作用域恰当的访问令牌。
在首次调用多提供商工具时,临时凭据带来的性能开销约为每次加载仪表盘增加 150~250 毫秒。用这点开销换取凭据零复用,实在太划算了。
可见性:Todaygent 到底做了什么
Keycard 最具吸引力的功能之一,是它所提供的信息。每次令牌交换、每次用户授权、每份签发的凭据以及每项被访问的资源,都会记录在 Keycard 审计日志中。加载仪表盘后打开 Keycard 控制台,我可以准确看到 Todaygent 访问了哪些内容。
Keycard 的审计日志展示了完整的事件时间序列。一次 list-all-tasks-today 调用后,日志中会记录多次令牌交换事件。由于通过并行的 Promise.allSettled() 发起数据获取,这些事件会接连快速触发。
但最有意思的部分是身份链,其中展示了两个身份:应用(“Plan My Today”)和用户(我的电子邮箱)。这是一条完整的轨迹:哪个应用代表哪个人访问了哪项资源。
授权类型(令牌交换,而非直接 OAuth 流程)、凭据提供商(Google)、实际授予的作用域、签发时间戳以及资源,全都可以追溯到这条身份链。我能准确知道 Todaygent 做了什么。
当我登录并授权 Plan My Today 访问我的 Calendar、Gmail 等服务时,Keycard 会记录用户授权事件。授权事件由人触发,而不是由 Todaygent 触发,因为是我在授予它访问本人资源的权限。
这个区别很重要:用户授权代表同意(“我允许这个应用访问我的 Gmail”),凭据签发代表执行(“Todaygent 刚刚通过令牌交换获得了读取我的 Gmail 的权限”)。这是两个不同的事件、两个不同的行为主体,而且都可以完整追踪。
Keycard 的审计日志既提供事件详情,也提供更高层级的信息。我还可以查看 Todaygent 在某个特定工作会话中做了什么:访问过哪些 API、分别访问了多少次,以及访问时间。如果发现异常,我可以检查审计日志,查看该会话完整的事件轨迹。
授权许可就是终止开关。我可以撤销单项资源的授权许可,而不必摧毁整个会话:Todaygent 下一次针对该资源的令牌交换会失败,但其他所有功能仍会继续工作。对于 Plan My Today,授权许可被特意设计为临时有效:它们会在各个提供商令牌的有效期结束后过期。在授权许可有效期更长的生产环境中,精准撤销意味着你可以“切断 Gmail 访问权限,但让 Calendar 和 Linear 继续工作”,而不是只能“关闭所有功能”。
这对 AI 智能体原生应用为何重要
传统身份验证只有两种用户状态:用户已登录,或者未登录。当系统中有能够思考的 AI 智能体代为执行操作时,这显然远远不够。你自己的服务器上或许有访问日志,但你无法看到签发了哪些令牌、签发给了哪个 AI 智能体、用于访问哪项资源,以及代表哪个用户。访问日志告诉你发生了什么,而 Keycard 的审计日志会在事件发生之前告诉你,什么操作获得了授权。
我构建 Plan My Today 有两个目标:规划自己的一天(显而易见),以及通过使用 Keycard,避开 AI 智能体访问多种工具所带来的惊人复杂性。这是我亲自使用自家产品的项目。(不过在 Keycard 内部,我们更喜欢说自己是在喝自家的香槟;这个比喻没那么恶心。)Plan My Today 展示了 Keycard 的核心模型:
-
任务作用域凭据:每次 API 调用使用一个令牌
-
复合身份:每个请求中都包含用户、AI 智能体和资源
-
零常驻访问权限:不缓存资源令牌,也没有长期有效的密钥
-
委托链:每项操作都能追溯到我
Keycard 的审计日志为我提供了完整链路:用户 → 应用 → 资源,其中每次令牌交换和授权事件都有记录,并且可以查询。对于 Plan My Today 这种代表我访问许多不同 API 的应用来说,这不是可选功能。在安全地开发和部署 AI 智能体时,只有这样,我才能用实际证据回答“AI 智能体对我的数据做了什么”,而不是盲目信任它。
AI 智能体:Todaygent
个性与理念
Todaygent 是“规划者,而不是执行者”。它什么都读,但几乎什么都不操作。我希望 Todaygent 带有冷幽默,并且清楚认识到自身的局限。因此,当我要求它做超出其职责范围的事情时,它会这样回答:“我的创造者不信任我,所以没有给我做那件事的权限。”
权限模型(有意限制)
-
自由读取(所有读取工具)——获取任意数据源的数据
-
操作前询问——
dismiss-task:忽略会议记录、文档、Slack 提及和临时任务(使用本地存储,不需要 API 凭据);restore-dismissed-task:撤销忽略操作;set-task-priority:当我要求重新调整优先级时,提高或降低任意任务的优先级(使用本地存储) -
有意不提供——更改 Linear 状态、执行 GitHub 操作、添加电子邮件标签、移除 Slack 表情符号、发送电子邮件、修改日历
这是一个关于信任的设计决策。能够阅读我的工作内容并帮助我思考的规划 AI 智能体很有用,而能够在没有护栏的情况下修改我工作内容的 AI 智能体则很危险。采用最小权限原则意味着,我可以让 Todaygent 运行,而不必担心它把我的 Linear 议题标记为“Done”或者自动回复电子邮件,即使我不小心要求它做了不该做的事。或者即使我是故意这么要求的……有时我确实会这样做,只是为了看看它如何回应。(得了吧,难道不是每个人都会这么干吗?)这就是我对 Keycard 实现方案的信心。
聊天功能实现在一个 React Hook 中,它负责管理完整的 AI 智能体循环:
User sends message
→ Stream Todaygent response (with all tools available)
→ If Todaygent calls tools, execute via MCP
→ Feed results back to Todaygent
→ Loop (max 10 rounds)
Todaygent 有很多可能失控的地方,因此我做了明确限制。工具调用次数上限可以防止无限循环。截断工具结果可以避免单个 API 响应占据整个对话。提示词缓存能在每一轮对话中节省 token。Claude Opus 4.6 的自适应思考功能让 Todaygent 可以针对复杂的规划任务进行扩展思考。最后,上下文管理使用 Anthropic 的 API,而不是手动划分对话窗口。
如果我让 Todaygent 规划自己的一天,它会给出一份简报,其中包含按时间段安排的日程、依赖关系分析以及跨数据源的上下文。我还可以询问它有哪些阻塞项、如何提高效率,甚至让它为我的任务列表写俳句。
系统提示词的重要性
Todaygent 的系统提示词并不是一句“乐于助人”。它非常详尽,定义了权限模型、个性、格式规则、工具调用策略以及各种边缘情况。它会告诉 Todaygent 何时应该获取最新数据,何时应该基于已有上下文推理,以及无法完成某件事时应该如何表现。早期的 Todaygent 非常热心,却也十分混乱:它会在对话中途重新获取数据、忘记添加链接,或者信心十足地虚构自己已成功完成某些它既没有工具也没有权限执行的操作。
我每天都会使用 Todaygent,而且一天要用好几次(因为我早上制订的计划,到了下午总会变得面目全非)。它是我在工作电脑上使用的个人工作工具,专门针对我的工作方式进行了定制。我没有理由把它部署到公共互联网,也没有必要支持多租户。我的 SaaS 蔓延问题已经够严重了。
不过,我确实实现了一项改善使用体验的功能:我把 Plan My Today 变成了渐进式 Web 应用(PWA),这样就可以把它安装成桌面应用。
我从 Plan My Today 的 AI 智能体编程实践中获得的经验永远学不完。我发现,为了安排好自己的一天,我在潜意识中进行的心理处理复杂得不可思议。试图让一个不是我的东西可靠地模拟这一过程,是一项艰巨且永无止境的任务。
这充分说明了 AI 目前所处的阶段,也说明了我们仍然多么需要让人类推理参与其中。有时我会怀疑,自己到底有没有真正节省时间。至少,对它进行折腾和改造还是非常令人满足的。
如果你正在构建需要代表用户访问多个 API 的 AI 智能体原生应用,Keycard 可以省去你为每个提供商分别实现 OAuth 的麻烦。我从事身份领域的工作(而且我很热爱它!),即便如此,只要想到使用 Keycard 让我避开了 OAuth 委托和访问控制的噩梦,我还是会暗自微笑。用户只需登录一次,就能授权所有提供商,并针对 AI 智能体所需的每个数据源签发感知策略、临时有效且限定任务作用域的令牌。你可以追踪 AI 智能体与人类之间完整的委托链,并在必要时精准撤销授权许可。
Keycard 目前已开放抢先体验,你可以注册并与我们的团队紧密合作,构建自己的多服务 AI 智能体应用(真的很棒)。
我正在持续收集积压在 Linear 中的任务,其中包含许多改进 Plan My Today 的想法:增加更多信息源、开发新工具、优化 UI,以及完善文档。我已经每天都在使用 Plan My Today。现在的重点是扩展 Todaygent 能够查看的信息,以及它能够(谨慎地)执行的操作。更重要的是,我还要将 Keycard 的新功能融入这个供自己实际体验的“香槟”项目中。
当然,Todaygent 也在不断提醒我,还有一些待办任务需要完成,以持续改进它。