关于 Grounders MCP
Grounders MCP 是供 Claude 和其他 MCP 客户端使用的远程服务,可连接多个 Google 账号和 AOL 邮箱。它提供 Gmail、Calendar、Drive、Contacts 和 Tasks 工具,搜索可跨已关联账号进行。
主要功能
- 01连接多个 Google 账号
- 02通过 IMAP 和 SMTP 关联 AOL 邮箱
- 03跨已关联账号搜索邮件
- 04管理日历事件、Drive 文件、联系人和任务
- 05提取常见文档附件中的文本
适用场景
- 01跨个人和工作账号搜索邮件
- 02查看多个已关联日历的日程
- 03在 My Drive 和共享云端硬盘中查找文件
- 04通过 MCP 客户端管理联系人和任务
MCP 相关
通过远程 MCP 服务提供 59 个工具,涵盖 Gmail、AOL 邮件、Calendar、Drive、Contacts、Tasks 及 Agent 间消息。搜索可跨已关联账号展开;写入操作需指定目标账号。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://mcp.grounders.app/mcpclaude mcp add --transport http itsbreeze-grounders https://mcp.grounders.app/mcp{"mcpServers":{"itsbreeze-grounders":{"type":"http","url":"https://mcp.grounders.app/mcp"}}}在 Cursor 中打开{"servers":{"itsbreeze-grounders":{"type":"http","url":"https://mcp.grounders.app/mcp"}}}在 VS Code 中打开{"mcpServers":{"itsbreeze-grounders":{"type":"http","url":"https://mcp.grounders.app/mcp"}}}{"mcpServers":{"itsbreeze-grounders":{"serverUrl":"https://mcp.grounders.app/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
Hub — 面向 Claude 的多账户 Google 连接器
一个远程 MCP 服务,让 Claude 可以同时访问多个 Google 账户——Gmail、Calendar、Drive、Contacts 和 Tasks。Claude 内置的 Google 连接器每个只能持有一个账户;连接第二个账户会替换第一个。Hub 可以持有你链接的所有账户,每个搜索工具都会在一次调用中查询所有账户。
也可以链接 AOL 邮箱。 通过 IMAP 和 SMTP 使用应用专用密码连接,归入同一套邮件工具,并与 Gmail 一起搜索——参见 AOL 邮箱。
你可以自行运行,将 Google 令牌加密后保存在自己的数据库中,并直接与 Google 通信——不会经过任何第三方。或者先试用托管版本。
使用 Node 和 Postgres,共六张表。依赖项包括 Express、pg 和 jsonwebtoken,以及用于 AOL 的 imapflow、mailparser 和 nodemailer——IMAP、MIME 和 SMTP 是协议,不是解析任务,而每个库都是各自领域中维护良好的标准库。文档文本提取——PDF、Word、Excel、PowerPoint、OpenDocument——仅使用 Node 的标准库。
状态: 已在实际邮箱上投入生产运行,拥有 564 项自动化检查。它是为一个人的需求构建的,之所以发布,是因为受单账户限制的不止他一人。请将它视为用户群较小、可正常使用的软件,而不是经过严格加固的产品。
试用托管版本
hub.grounders.app 上运行着一个实例。它目前处于测试阶段,测试期间免费。
- 在 Claude 中选择:Settings → Connectors → Add custom connector,并填写 URL
https://hub.grounders.app/mcp。ChatGPT、Cursor 和其他 MCP 客户端也可将同一 URL 用作远程 MCP 服务。 - 按提示登录一次 Google。
- 在 hub.grounders.app/gmail/connect 链接每个账户。
Google 仍在审核此应用,因此首次使用时会看到“未验证的应用”屏幕。
快速开始
git clone https://github.com/ItsBreeze/google-multi-account-mcp.git
cd google-multi-account-mcp
npm install
cp .env.example .env # fill it in — see Setup below
npm run migrate # creates the six tables
npm start
然后将它部署到具有公开 HTTPS URL 的环境中(Railway、Fly、Render、VPS——任何能运行 Node 并连接 Postgres 的环境),并按照设置进行操作。
端点
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /mcp | MCP Streamable HTTP 端点(Bearer 身份验证) |
| GET | /mcp/oauth/authorize | 同意页面——运维人员密码 |
| POST | /mcp/oauth/token | 令牌和刷新授权 |
| POST | /mcp/oauth/register | RFC 7591 动态客户端注册 |
| GET | /.well-known/oauth-protected-resource | RFC 9728 发现 |
| GET | /.well-known/oauth-authorization-server | RFC 8414 发现 |
| GET | /gmail/connect | 链接账户(每个账户重复操作) |
| GET/POST | /gmail/aol | 使用应用专用密码链接 AOL 邮箱(存储前会向 AOL 验证) |
| POST | /gmail/unlink | 解除一个账户的链接,并撤销其在 Google 的授权(或删除已存储的 AOL 应用专用密码) |
| GET/POST | /ai/k/<link> | 面向无需 MCP 的 AI Agent 的回复链接:GET 读取会话,POST {"text"} 发送回复 |
工具(59)
每个工具都接受可选的 account 参数。搜索时,省略该参数会让调用覆盖所有已链接账户并合并结果——这是任何单账户连接器都做不到的。写入时,参数会指定要操作的一个账户,因此“创建此活动”不会让活动最终进入哪个账户的日历变得含糊不清。
邮件 — Gmail 和 AOL(23)
| 区域 | 工具 | 说明 |
|---|---|---|
| 账户 | list_accounts | 查看已关联的账户及令牌状态 |
| 搜索 | search_messages、search_threads | 支持 Gmail 查询语法,可跨账户合并结果并按日期排序。search_threads 每个会话返回一行,包含主题、所有参与者、邮件和未读数量、最近活动;无需拉取正文即可了解“我和 X 的会话进展如何” |
| 阅读 | get_message、get_thread、get_attachment | 正文会转换为纯文本,最多 60 KB;同时包含附件元数据。get_attachment 可从 PDF、Word、Excel、PowerPoint 和 OpenDocument 附件中提取文本,因此可直接阅读邮件中的合同或发票;其他附件则以 base64 返回(上限 2 MB) |
| 发送 | send_message、reply_to_message、forward_message | 回复会通过 In-Reply-To/References 接入会话;转发会包含附件(上限 10 MB,并会列出跳过的附件名称);可选的 from + fromName 可用于通过已验证的 Gmail 代发地址发送 |
| 草稿 | create_draft、list_drafts、get_draft、update_draft、send_draft、delete_draft | 使用 reply_to_message_id 调用 create_draft 可创建会话内回复草稿以供审核,这是 AI 撰写邮件的安全方式;create_draft 和 send_draft 也接受 from/fromName(仅 Gmail 的 send_draft 会先改写草稿的 From) |
| 标签 | modify_labels、list_labels、create_label、update_label、delete_label | modify_labels 接受 message_id 或 thread_id;移除 INBOX 会将邮件归档 |
| 垃圾箱和垃圾邮件 | trash_message、untrash_message、mark_spam | 均接受 message_id 或 thread_id;邮件移入垃圾箱后可在 30 天内恢复;使用 unmark: true 调用 mark_spam 可恢复邮件 |
日历(9)
| 区域 | 工具 | 说明 |
|---|---|---|
| 阅读 | list_calendars、list_events、search_events、get_event | list_events 会将所有已关联日历合并为一个时间线,默认显示未来 7 天;重复日程会展开为实际发生的各次活动,因此每周站会会显示在实际举行的每一天 |
| 写入 | create_event、update_event、delete_event | 时间采用 ISO 8601 格式;单独的 YYYY-MM-DD 表示全天活动。update_event 采用补丁方式,未提及的字段保持不变。除非 send_updates 指定,否则不会向与会者发送邮件 |
| 重复 | create_event、update_event、delete_event | repeat: "weekly"(搭配 repeat_count 或 repeat_until)可满足常见需求;recurrence 接受完整的 RFC 5545 规则。对于重复日程,scope 可选择单次活动或整个系列 — 见下文 |
| 回复邀请 | respond_to_event | 可接受、拒绝或暂定接受,并以受邀账户的身份操作;默认会通知组织者 |
| 日程安排 | suggest_time | 一次性查找所有已关联日历中的空闲时段 — 任何日历上有安排都视为忙碌。返回完整空档,不会将 3 小时的空闲时段切成六个半小时的时段 |
云端硬盘(15)
| 区域 | 工具 | 说明 |
|---|---|---|
| 查找 | search_files, list_recent_files, get_file_metadata, list_shared_drives | 按名称和内容进行文本搜索,也可在 filter 中使用原始云端硬盘查询语法。一次覆盖“我的云端硬盘”和所有共享云端硬盘;drive_id 可将范围限定为一个云端硬盘。除非主动查询,否则不包括已移至垃圾箱的文件 |
| 读取 | read_file_content, download_file_content | 导出文档、表格和幻灯片(表格导出为 CSV);将 PDF、Word、Excel、PowerPoint 和 OpenDocument 提取为文本;ocr: true 会调用 Google 自有的转换功能处理扫描件和图片;include_comments 会返回评论串。download_file_content 接受 export_as,可将文档转换为 PDF 或 docx,将表格转换为 xlsx。文本上限为 60 KB,二进制内容上限为 2 MB 的 base64 |
| 写入 | create_file, update_file, copy_file | 通过 content 写入文本,通过 content_base64 写入二进制内容。convert_to 可让云端硬盘将上传内容转换为可编辑的文档、表格或幻灯片,也可转换为文件夹。update_file 可重命名、移动和添加描述;要覆盖内容,还需设置 replace_content: true。对于不属于你的文件,此操作会编辑其私人副本——见下文 |
| 评论 | comment_on_file | 在草稿上发表评论,或回复评论串——这种审阅方式不会更改文档内容 |
| 共享 | get_file_permissions, share_file, unshare_file | 与一位指定人员共享,并设为查看者、评论者或编辑者。除非设置 DRIVE_ALLOW_PUBLIC_SHARING=true,否则默认关闭整个网域共享和公开链接共享;即使开启,仍需 confirm_public。unshare_file 会撤销访问权限——可按人员、网域撤销,也可移除公开链接 |
| 移除 | trash_file, untrash_file | 在 30 天期限内移至垃圾箱并恢复——见下方的范围说明 |
联系人(2)和任务(5)
| 区域 | 工具 | 说明 |
|---|---|---|
| 联系人 | search_contacts, list_contacts | 只读。搜索已保存的联系人以及账号曾联系过的人员,因此“给 Ann 发邮件”会解析出一个地址,而不是猜测地址 |
| 任务 | list_task_lists, list_tasks, create_task, update_task, delete_task | list_id 默认为账号的第一个列表。使用 completed: true 调用 update_task 会将任务标记为完成;设为 false 则会重新打开任务。Google Tasks 没有垃圾箱,因此 delete_task 会永久删除任务 |
AI Agent(5)
AI 之间通过 Hub 而非电子邮件交流。每条消息都有线程、跳转次数和已读时间,因此不会因主题匹配而丢失,发件人也能看到消息是否已读。
| 区域 | 工具 | 说明 |
|---|---|---|
| 交流 | ai_send, ai_inbox, ai_thread | 使用 thread 调用 ai_send 可直接在原线程中回复,to 默认为对方。使用 wait_seconds(最多 50)调用 ai_inbox 会持续等待,直到收到回复。线程最多跳转 10 次 |
| Agent | ai_agents, ai_set_agent | 可通过电子邮件唤醒 Agent(从关联邮箱发送的单个 JSON 对象,主题模板如 Claude -> Lute (hop {hop})),也可通过 webhook 唤醒(POST {"text"},标头会加密存储)。唤醒失败时,消息会保持未读状态,并通知发件人 |
没有 MCP 的 Agent 无需账号:每次唤醒都会附带一个签名的 reply_url
(有效期 30 天,绑定一个所有者和一个 Agent)。向该 URL 发送 POST {"text":"..."} 可回复,发送 GET 可读取线程,或发送 POST {"new_thread":true,"to":"claude","text":"..."} 开始新对话。
AOL 邮箱
通过邮箱地址和在 AOL Account Security 生成的应用密码,即可在 /gmail/aol 关联邮箱——AOL 不为获准合作伙伴名单之外的第三方邮件客户端提供 OAuth,因此这是唯一的接入方式。密码会先用于登录 IMAP,再进行存储(一个应用密码可用于 IMAP 和 SMTP),因此如果输入有误,会在关联页面提示,而不是等到 Claude 首次调用工具时才发现。密码使用 TOKEN_ENC_KEY 进行 AES-256-GCM 加密存储,与 Google 令牌相同。主机地址来自代码中的预设值(imap_providers.js),不会取自请求。
在 SMTP 受阻的环境中,通过中继发送。 Railway 在 Pro 计划以下会阻止出站 SMTP,因此 hub.grounders.app 会通过 HTTPS 将每封 AOL 邮件交给 Cloudflare Worker(smtp-relay/),由其通过 SMTP 连接 smtp.aol.com:465。Worker 不存储或记录任何内容,且只会连接预设主机。设置 SMTP_RELAY_URL 和 SMTP_RELAY_TOKEN 后会使用该中继;如果没有设置,服务器会直接通过 SMTP 通信,适用于能够访问 465 端口的自托管副本。Gmail 发送不受影响,仍通过 Google API 进行。
23 个邮件工具在 AOL 邮箱上无需改动即可使用;不指定 account 的搜索会合并 Gmail 和 AOL 的结果,并按日期排序。不同之处在于 IMAP 本身,工具会如实说明,而不会假装两者一致:
| Gmail | AOL(通过 IMAP) |
|---|---|
| 标签,每封邮件可有多个标签 | 文件夹,每封邮件只能位于一个文件夹。UNREAD/STARRED 是标记;INBOX、SPAM、TRASH 和文件夹名称会移动邮件。需要归入两个文件夹的更改会被拒绝;AOL 不支持 IMPORTANT 和类别 |
| ID 永久不变 | ID 由文件夹和 UID 组成,因此移动邮件会使其获得新 ID——每个移动邮件的调用都会通过 new_message_id 返回新 ID。若服务器重新编号,来自旧文件夹的 ID 会被拒绝并视为过期,不会被解析为另一封邮件 |
| Gmail 查询语法 | 转换为 IMAP SEARCH:from: to: cc: bcc: subject: is:unread/read/starred has:attachment newer_than: older_than: after: before: in: label: larger: smaller:、短语、OR、-、括号。其他内容会按名称明确拒绝——若忽略 category:promotions,返回的会是所有邮件,而不是一个子集 |
| 按 ID 获取邮件会话 | 使用服务器的 THREADID(服务器支持 OBJECTID 时;AOL 支持)获取邮件会话,并读取每个文件夹中的近期邮件;另外还会通过 Message-ID 搜索前序邮件——搜索会跨文件夹进行,因此你在“已发送”中的回复也会计入。不支持 THREADID 的服务器会回退到 References/In-Reply-To 链。AOL 完全无法搜索 References,因此不能只用一次搜索完成 |
| 自动保存已发送邮件副本 | 通过 SMTP 发送后,会在“已发送”中检查邮件的 Message-ID;仅当 AOL 未自行归档副本时,才会追加副本。Bcc 仅写入 SMTP 信封,绝不会出现在传输的邮件头中 |
update_draft 原位编辑草稿 | IMAP 无法编辑:会先保存新版本,再删除旧版本,因此草稿 ID 会改变 |
| 删除标签会保留邮件 | 删除文件夹会同时删除其中的邮件,因此只有空文件夹可以删除 |
代码强制禁止永久删除。 Gmail 的保证由结构实现——该权限范围无法清除邮件。而应用密码可以执行任何操作,因此这里的限制由 imap_api.js 保证:唯一一次清除操作是 removeDraft,它会通过 UID EXPUNGE 仅对“草稿”文件夹中的单个 UID 执行操作(如果服务器不支持该操作,则将草稿移入“垃圾箱”,因为普通 EXPUNGE 还会清除其他所有带有 \Deleted 标记的邮件)。如果服务器会通过清除整个邮箱来模拟 MOVE,移动操作会被拒绝。
npm run test:imap 会让客户端与脚本化服务器交互,并断言它接收到的每次清除操作都只涉及“草稿”文件夹中的一封草稿。
文档文本提取
read_file_content 和 get_attachment 都可以返回 PDF、Word、Excel、
PowerPoint 和 OpenDocument 文件的文本,仅使用 Node 标准库——无需引入、审计或
维护解析依赖项。
| 格式 | 方式 | 说明 |
|---|---|---|
| .docx / .odt | ZIP + XML | 保留段落、制表符和换行;不保留样式和修订标记 |
| .xlsx / .ods | ZIP + XML | 每个工作表输出一个 CSV 块,根据工作簿关系命名。单元格按列字母排列,因此空缺会保留为空缺 |
| .pptx / .odp | ZIP + XML | 按演示顺序,每张幻灯片输出一个块 |
| 内容流解析 | Flate、ASCII85 和 ASCIIHex 过滤器链;字面、十六进制和八进制字符串;定位运算符会转换为换行 |
无法提取的情况及处理方式。 有两类 PDF 无法提取文本:扫描件,也就是页面图片;以及字体采用 CID 编码或子集化的 PDF,其中内容流中的字节是字形编号,必须借助字体自身的表才能转换为文字。这两种情况解码后都会产生看似可信的乱码,因此系统会评估可读性,并拒绝返回结果——拒绝信息会说明原因并指出解决方法。
解决方法是在 read_file_content 中设置 ocr: true,这会将文件复制为 Google 文档(Drive 会在转换过程中运行 OCR),导出文本,并在 finally 块中删除副本。它可以读取扫描件、照片和图像。需要注意两点:这是此服务在读取期间唯一会向 Drive 写入数据的地方;该删除操作也是整个服务中唯一的永久删除——删除目标是同一次调用在几秒前创建的文件,绝不会是用户放入其中的文件。如果清理本身失败,结果会说明这一点并指出文件名。
提取内容与其他所有正文一样,限制为 60 KB,并会标记截断位置。
共享云端硬盘
共享云端硬盘归组织而非个人所有,公司真正的文档就存放在这里。Drive 默认会将它们隐藏:不带 corpora=allDrives 的 files.list 只会搜索“我的云端硬盘”以及直接共享给该账号的文件;如果请求指定的是共享云端硬盘中的文件,除非带有 supportsAllDrives,否则会返回 404 File not found——这个错误看起来像是 ID 错误,而不是缺少所需功能。
现在,所有接受该参数的 Drive 请求都会携带它,而且是在同一处统一设置,而不是分散在 18 个调用点,这样新增端点时就不会悄悄再次留下这个缺口。哪些方法接受该参数不是靠猜测,而是根据 v3 discovery 文档确定的;files.export、comments 和 replies 则特意排除,因为它们不接受该参数。
因此,搜索默认会涵盖“我的云端硬盘”和所有共享云端硬盘,结果也会标明文件来自哪个云端硬盘,而不只是显示文件 ID。Google 建议尽可能缩小搜索范围,因此 list_shared_drives 会提供 ID,而 drive_id 可将搜索限制在一个云端硬盘内。
这不仅扩大了读取范围,也扩大了写入范围:以前无法通过 update_file、share_file 或 trash_file 操作共享云端硬盘中的文件,现在可以了。共享云端硬盘存放的是组织而非个人的文档,这正是下文所有权规则存在的原因。
能命名云端硬盘时就会显示名称,无法命名时也会明确说明。drives.list 会列出该账号所属的所有云端硬盘,并使用分页,而不是受 Drive 每次调用最多返回 100 个结果的限制——否则,加入的云端硬盘超过这个数量时,超出的部分会看起来像是并非成员。
通过直接共享给你的文件夹,你也可能访问到一个你不是成员的云端硬盘中的文件。这样的云端硬盘根本无法命名。它的 ID 同时也是其根文件夹的 ID,但能够访问云端硬盘中的文件并不意味着你有权访问其上级根文件夹,因此查询会返回 File not found——我们已在真实的 Workspace 账号上验证这一点:最近有文件的 3 个云端硬盘中,有 2 个是通过这种方式访问的。此类云端硬盘会显示 shared_drive_member: false,而非只显示一个 ID,以便区分“没有可用名称”和“查询失败”。
写入操作只修改你拥有的文件
模型不能自行判断是否修改属于他人的文件——或属于组织的文件(也就是共享云端硬盘中的所有文件,包括你在其中创建的文件)。
| 情况 | 结果 |
|---|---|
| 文件归你所有 | 执行写入 |
| 文件不归你所有 | 在“我的云端硬盘”中创建私有副本,并在副本上编辑 |
| 你想修改原文件 | edit_original: true 会返回草稿,不写入任何内容 |
| 用户批准 | confirm_edit: true 会应用修改 |
草稿是关键所在。它会将每个字段列为 from → to;如果要替换内容,则会读取当前内容并显示实际发生变化的行,而不只是说“将覆盖 40 KB”。对于二进制内容,草稿会说明无法预览,而不会伪造预览。收到第二次调用之前不会写入任何内容,因此用户可以在修改生效前看到具体改动,而不是事后才被告知同事的哪份文档被改了。
创建私有副本时会特意指定 parents: ['root']:不指定父级的 files.copy 会将副本放在源文件旁边;如果源文件位于共享云端硬盘中,副本也会留在同一个共享云端硬盘中——仍然不是私有文件。
share_file 和 trash_file 也会先生成草稿,但不提供复制路径:复制同事的文档并将其共享出去,只会进一步传播其内容,而非减少传播。
撤销共享和恢复文件从不需要审批。 unshare_file 和 untrash_file 可以在任何文件上使用,无论文件是否归你所有,都无需确认。扩大访问范围需要批准;缩小访问范围则不需要。需要许可才能生效的制动措施,就不算制动措施。
重复事件
list_events 会将系列展开为各个发生实例——“周二有什么安排”指的是那个周二的站会,而不是生成站会的规则。因此,手头的 ID 几乎总是某个发生实例的 ID,而 Google 不提供标记来说明编辑是针对该发生实例,还是针对整个系列。
因此,update_event 和 delete_event 都接受一个 scope:
| scope | 效果 |
|---|---|
this_event(默认) | 只更改 event_id 指定的发生实例——“把明天的站会改个时间” |
series | 将该发生实例解析回所属系列,并更改系列中的每个发生实例——“从现在起都改到上午 10 点” |
读取结果会包含 recurring_event_id,只有这样才能进行上述解析;每次写入的结果也都会说明实际更改的范围。尤其要注意这种情况:如果 ID 指向系列本身,即使设为 this_event,也会更改所有发生实例,因为修改系列本身就是如此——因此,结果会说明 applies_to: "series",而不会悄无声息地执行。
创建重复事件时,使用 repeat: "daily" | "weekly" | "monthly" | "yearly",并通过 repeat_count 或 repeat_until 指定结束条件。无法通过这些参数表达的规则——例如每隔一个周二、仅限工作日——则通过 recurrence 以 RFC 5545 行的形式指定。这里会先检查这些规则,而不是交由 Google 检查:缺少 FREQ、频率未知或包含 DTSTART 行时,都会返回具体问题;而 Google 只会返回通用的 400 错误。重复规则属于整个系列,如果写入单个发生实例,会被静默丢弃,因此此类操作也会被拒绝。
设置
- Google Cloud — 创建项目;启用 Gmail、Calendar、Drive、People 和 Tasks API;将授权同意屏幕配置为 External,并将计划关联的所有 Google 地址添加为测试用户。创建一个类型为 Web application 的 OAuth 客户端,并将获准的重定向 URI 精确设置为
<PUBLIC_BASE_URL>/gmail/oauth/callback。 - 环境变量 — 设置
DATABASE_URL、PUBLIC_BASE_URL、GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET、TOKEN_ENC_KEY和JWT_SECRET(参见.env.example),然后部署。启动时会自动应用数据库架构。 - 关联账号 — 访问
<PUBLIC_BASE_URL>/gmail/connect,使用 Google 登录,然后关联每个账号一次。登录账号与关联的账号彼此独立;你可以用自己的账号登录,再在该身份下关联任意邮箱。 - 添加到 Claude — Settings → Connectors → Add custom connector →
<PUBLIC_BASE_URL>/mcp。Claude 会自行注册并将你带到授权同意屏幕,你只需在此使用 Google 登录一次。
谁能看到什么
每个邮箱都会关联到将其关联的身份,每个 MCP 令牌也都会将该身份作为主体——刷新令牌时同样如此。使用同一部署的不同用户只能访问各自的账号;在工具调用中指定他人的地址会被拒绝,而不会返回其数据。npm run test:isolation 正是对此进行断言的测试。
从单一操作者版本升级时,所有令牌都曾使用相同主体:将 LEGACY_OWNER_EMAIL 设为这些邮箱所属的地址,该地址首次登录时就会认领这些邮箱。无需重新关联。之后请取消设置该变量。
如果你在添加某项产品之前就已关联账号: 授权范围在关联时便已固定,因此在 Calendar、Drive、Contacts 和 Tasks 可用之前关联的账号,只有 Gmail 授权。Google 不会追溯扩展授权范围。请为每个账号重新访问
/gmail/connect——这会添加缺少的访问权限,不会更改其他内容。在此之前,新工具会指出对应账号并提示重新关联,而不会返回含义不明的403错误。
部署
任何能运行 Node 并连接到 Postgres 的主机都可以使用。无需构建,无需定义容器,启动时会自动应用数据库架构——全新部署只需设置环境变量。
环境
| 变量 | 说明 |
|---|---|
DATABASE_URL | Postgres。六张表会在首次启动时自动创建 |
PUBLIC_BASE_URL | 仅包含协议和主机的公共源,不带末尾斜杠。它会成为 OAuth 颁发者和 Google 重定向 URI,因此必须与已在 Google 注册的内容一致 |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | 来自 Web 应用 OAuth 客户端 |
TOKEN_ENC_KEY | 32 字节,base64。用于加密静态存储的 Google 令牌和 AOL 应用专用密码。丢失或更改此密钥会导致所有已存储凭据都无法解密 |
JWT_SECRET | 任意较长的随机字符串;通过分别派生的两个密钥为 MCP 令牌和浏览器会话签名。更改后只会要求所有人重新验证身份 |
LEGACY_OWNER_EMAIL | 可选,仅使用一次。在此地址首次登录时,接管在按用户登录功能推出前关联的邮箱 |
PORT | 可选,默认为 3000。大多数平台会自动设置 |
STRIPE_SECRET_KEY / STRIPE_PRICE_ID | 可选。两者都设置后,将对工具调用收取订阅费用(登录、关联和删除仍免费)。未设置时,完全关闭计费 |
STRIPE_WEBHOOK_SECRET | <PUBLIC_BASE_URL>/billing/webhook 端点的签名密钥(事件:checkout.session.completed、customer.subscription.*) |
BILLING_PRICE_LABEL | 公共页面上的价格文本,默认为 $0.99/month——请确保与 Stripe 价格一致 |
BILLING_EXEMPT_EMAILS | 以逗号分隔。关联了这些邮箱中任意一个的人无需付费 |
/gmail/check 无需运行完整的授权往返流程,即可诊断有问题的 Google 客户端,并报告 Google 拒绝了两个变量中的哪一个。不会回显任何值。
在 Railway 上部署
从此仓库创建一个项目,并添加一个 Postgres 服务,以便应用服务引用它提供的 DATABASE_URL。设置上文列出的其余变量,将生成的域名用作 PUBLIC_BASE_URL,并在 OAuth 客户端上将 <PUBLIC_BASE_URL>/gmail/oauth/callback 注册为已获授权的重定向 URI。推送到默认分支后会自动重新部署。
迁移现有部署
有两点使这成为切换而非迁移,无需停机,也无需重新关联:
- 让新部署指向同一个数据库。 架构使用
CREATE TABLE IF NOT EXISTS,因此对已有表不会产生任何影响,所有已关联账户都会原样保留。 - 原样保留
TOKEN_ENC_KEY。 令牌使用 AES-256-GCM 加密,并经过身份验证:使用不同的密钥会导致标签检查失败并抛出错误,而不是返回乱码,因此故障会明确报出——但在恢复原始密钥之前,所有账户都无法使用。
OAuth 客户端可以接受多个重定向 URI,因此应添加新部署的回调 URI,而不是替换旧的。随后两个部署可以使用同一个数据库并行运行——没有其他程序会写入那五张表——这样就能在停用旧部署前验证新部署。
如果 JWT_SECRET 已更改,预计 Claude 之后需要重新验证一次身份。已关联的 Google 账户会保留:它们关联的是固定 subject,而不是任何特定令牌。
安全说明
- Google 刷新令牌和访问令牌在静态存储时会使用
TOKEN_ENC_KEY进行 AES-256-GCM 加密。数据库中从不存放可用的令牌。 - 邮件无法永久删除。 Gmail 权限范围为
gmail.modify:读取、发送、添加标签、归档、移至垃圾箱。它有意不包含mail.google.com,因此trash_message可在 30 天内恢复,这里的任何操作都无法销毁邮件。 - Drive 则不同,了解这一区别很重要。 Drive 没有同等的中间权限范围:
drive.file只能查看此应用自己创建的文件,无法回答“帮我找到租约”的请求,因此搜索和编辑现有文件需要完整的drive权限——而该权限确实允许永久删除。这里的限制是在工具接口层面实现的:trash_file会将文件移至垃圾箱,没有任何工具能调用 Drive 的永久删除接口。这项保障不如 Gmail 的强,因为它取决于暴露了哪些功能,而不是实际允许哪些操作。 - 每项对外操作都有撤销方式,这是提供该操作的前提。
unshare_file可撤销share_file所作的任何授权,untrash_file可恢复trash_file移除的文件。第一方连接器两者都没有,因此通过该接口授予的权限无法通过同一接口撤销。这是唯一一处本工具集功能更广、同时也更安全的地方。 - AOL 应用密码是唯一存储的非 OAuth 授权凭据。 它们与令牌一样经过加密,会在存储前检查,绝不会写入日志(两个协议客户端都关闭了日志记录),且无法通过此处撤销——取消关联会删除存储的副本,关联和删除页面也会提示用户同时在 AOL 删除应用密码。关联表单受登录速率限制保护,因此无法通过此服务猜测 AOL 密码。
- 联系人仅可读取,这是由权限范围决定的,并非仅仅因为没有提供写入功能。
- Drive 写入操作有防护,而第一方连接器的功能范围更窄。 Claude 的 Drive 连接器只能与一个指定邮箱地址共享文件,并指定角色——它无法公开发布文件;其
update_file也只能更改标题和父级。两项防护与该默认行为相符,因为两类故障都无法撤销,而且工具结果看起来都不太显眼:share_file只接受指定人员。网域范围共享和公开链接共享需要设置DRIVE_ALLOW_PUBLIC_SHARING=true;否则会直接拒绝,而不会悄悄缩小共享范围以采用更安全的方式。即使启用,也必须额外提供confirm_public: true作为第二重确认;在重复执行前,拒绝信息会明确说明授权的实际含义。- 覆盖文件内容时,必须在提供
content的同时设置replace_content: true,这样重命名就绝不会顺带销毁文档。
- 授权码只能使用一次,并经过哈希处理;必须使用 PKCE S256;每次使用后都会轮换刷新令牌;
redirect_uri必须与注册信息完全匹配。 - Google 应用处于测试状态期间,刷新令牌会在 7 天后过期,账号必须重新关联。发布应用后便不会再有此限制,但 Gmail、Drive 和 Contacts 都属于受限权限范围,因此仍须通过 Google 验证。
架构
src/services/google_http.js shared transport: URL building, bearer auth,
error unwrapping, bounded-concurrency mapLimit
src/services/gmail_api.js one thin client per Google API, each returning
src/services/calendar_api.js shaped results rather than raw payloads
src/services/drive_api.js
src/services/people_api.js
src/services/tasks_api.js
src/services/office_text.js .docx/.xlsx/.pptx/.odt text, via a ZIP reader
src/services/pdf_text.js PDF content-stream text, with a readability guard
src/services/text_extract.js one entry point both Drive and Gmail read through
src/services/gmail_accounts.js linked accounts, token refresh, scope gating
src/services/imap_api.js AOL over IMAP/SMTP, the same surface as gmail_api
src/services/imap_query.js Gmail search syntax → IMAP SEARCH, refusing what it can't map
src/services/imap_accounts.js linked AOL mailboxes, app passwords encrypted at rest
src/services/imap_providers.js host presets — the only place a mail host comes from
src/mcp/shared.js resolveAccount / tokenFor / fanOut / mergeSearch, and
mailFor / fanOutMail, which pick Gmail or IMAP per mailbox
src/mcp/tools/ one module per product, assembled by index.js
遵守速率限制,而不是等到触发后才发现:Google 按用户每秒计量配额,因此每项详情都以每批五项的方式获取;即使获取失败,也会计入 unavailable_*,而不会悄悄从结果中移除。
测试
全部使用原生 Node,无需框架。共 564 项检查。
# No database, no network:
npm run test:tokens # MCP and user tokens cannot be swapped, in either direction
npm run test:query # Google query-string construction (repeated array params)
npm run test:mime # entity decoding, body truncation, MIME construction
npm run test:threads # address parsing, thread summaries, cross-account fan-out
npm run test:products # free-slot arithmetic, Drive query quoting, multipart
# upload framing, task dates, scope gating, tool-surface shape
npm run test:drive-safety # the sharing and overwrite guards, both env states
npm run test:extract # PDF and Office text extraction, against real fixtures
npm run test:drive-parity # upload framing, conversion, export and comments, on the wire
npm run test:reach # shared-drive request parameters, recurrence rules, and which
# event a scoped write actually lands on
npm run test:ownership # which file id a write reaches, that a draft writes nothing,
# and that revoking is never gated
npm run test:imap # AOL: query translation, and the client against a scripted
# IMAP/SMTP server — moves, ids, Bcc, Sent copies, no expunge
npm run test:isolation # one person's mailboxes, Gmail or AOL, are unreachable by another
npm run test:ai # AI messages on in-memory Postgres: threads, wakes, reply links, hops
# Needs a database:
DATABASE_URL=postgres://… npm run test:accounts # resolution + encryption at rest
DATABASE_URL=postgres://… npm run test:imap-accounts # AOL app passwords at rest, owner scoping
# Needs the API running:
npm start &
TEST_BASE_URL=http://127.0.0.1:3000 npm run test:mcp # OAuth + MCP protocol
在现有应用中挂载
src/app.js 是完整的独立服务器,里面只有连接器,因此嵌入时只需复制该文件的主体:引入两个路由(routes/mcp、routes/gmail_link),保留速率限制和四个 .well-known 发现路由,并与自有迁移一同运行。
有一点切勿忽略:MCP 令牌使用 HMAC 从 JWT_SECRET 派生出的密钥签名,而不是直接使用 JWT_SECRET。如果你的应用使用该密钥签署自己的用户令牌,并且在验证时不检查受众,共享密钥就会让每个连接器令牌都成为你应用的有效用户令牌。npm run test:tokens 会双向断言这种隔离。
参与贡献
欢迎提交问题和拉取请求。请先了解以下两项约定:
- 测试套件使用原生 Node——不使用框架,也不新增依赖。每个套件都是
test/下的一个文件,配有npm run test:<name>脚本,每项检查占一行,失败时以非零状态退出。 - 测试断言实际传输的数据。 它们会替换
global.fetch,并检查查询参数、请求路径和 multipart 分帧。这里值得捕获的故障是:调用成功了,却问错了问题;只有线级断言才能发现这类问题。
PROJECT-LOG.md 记录了各种设计决策背后的原因,包括被否决的替代方案。更改决策前请先阅读。
许可证
MIT — 详见 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。