只读MCP Server:AI安全边界的工程实践与设计解析
最近在 Hacker News 上有一个项目很值得开发者注意Show HN: All my mail accounts in one read-only MCP server, usable from my phone。作者把自己的所有邮箱账号汇聚到一个只读的 MCP Server 中然后用手机上的 AI 客户端随时跨账号查邮件。这个项目表面看是一次个人工具分享但仔细拆开它触及了 MCP 实践里最容易被忽略的问题AI 工具的安全边界应该怎么划。如果你写过 Java 后端大概率见过一个经典报错write operations are not allowed in read-only mode。这个错误的意思很直白底层连接已经被声明为只读事务管理器会在更深层拦截写操作。今天聊的这个 MCP 项目与它背后是同一个思想只不过把“只读”从数据库连接层搬到了 AI 工具的 API 层。我对这个项目的判断是它有明确的实用价值也有很好的示范意义。真正值得学习的不是“怎么连邮箱”而是它如何通过只读接口设计让 AI 能读邮件、会搜邮件、能总结邮件却没有任何能力去发送、删除或修改邮件。这个很克制的设计同时解决了检索效率和安全隐患两个问题。这篇文章会从四个层面展开先讲 MCP 在当前 AI 应用中的定位再拆解只读 MCP Server 的设计边界然后给出一个可复用的 Python 实现最后讨论手机端访问时的工程落地和常见坑。全文以可操作为目标代码可以直接改成自己的邮箱配置跑起来。1. 为什么要关注这个项目邮箱接入 AI 的两种姿势先问一个问题你的邮箱里有多少账号很多人不止一个。工作邮箱、私人邮箱、项目通知邮箱、各种订阅列表。搜索一封几个月前的邮件时你需要分别登录不同客户端重复输入关键词来回切换。这是邮件管理最原始、最烦躁的痛点。MCP 出现后这个问题有了新解法。MCP也就是 Model Context Protocol模型上下文协议是 Anthropic 在 2024 年底开源的一套协议。它定义了 AI 助手Host如何通过 MCP Client 去调用 MCP Server 暴露的能力。放在邮箱场景里AI 助手可以调用“搜索邮件”“读取邮件”这两个工具然后告诉你“上周三的报销单在 work 账号的收件箱里”甚至帮你把三封相关邮件的关键信息汇总成一段摘要。这个场景听起来很诱人但有一个现实隐患如果 AI 同时具备“发送邮件”的能力一次幻觉、一次误触就可能把草稿发给错误的人或者把内部邮件转发出去。这是邮箱接入 AI 时最恐怖的失败模式。于是就有了两种设计姿势。第一种是让 AI 拥有完整读写权限优点是能力完整缺点是出事的代价可能非常严重。第二种就是本文主角的做法只暴露读取和搜索类工具发送、删除、移动、改状态这些操作一律不提供让 AI 在安全边界内把“读”这件事做到极致。从工程角度看第二种姿势显然更适合邮件这类高隐私、高影响数据。它看起来像是对 AI 能力的一种限制实际上是对确定性的一种保护。这个项目的标题特意强调了 read-only说明作者把这个边界当成核心特性而不是简单省略。2. MCP 与只读模式先搞清楚协议本身要理解这个项目得先理解 MCP 的基本结构。MCP 的整体架构是三层Host宿主应用也就是 AI 聊天客户端、Client宿主应用内部与 Server 通信的组件、Server提供工具和资源的服务。以邮箱场景为例你在手机或电脑上打开一个支持 MCP 的 AI 客户端这个客户端就是 Host。客户端内置了 MCP Client 模块负责与 MCP Server 建立连接。MCP Server 是你的邮箱聚合服务负责与 IMAP 邮件服务器通信把搜索结果、邮件正文等数据返回给客户端。最终AI 模型只看到工具名称、入参出参和执行结果它不关心底层 IMAP 协议怎么工作。MCP Server 对外暴露三种能力Tools、Resources、Prompts。Tools 是可被 AI 调用的函数类似函数调用的概念Resources 是可以暴露给模型的文件和结构化数据Prompts 是预设的提示词模板。在本文项目里重点是 Tools。Tools 的定义需要声明名称、描述和参数结构。当 AI 判断用户需要搜索某封邮件时它会根据工具描述来构造调用参数。这里的关键是AI 只能调用 Server 注册过的工具如果 Server 根本不注册“发送邮件”这个工具AI 再聪明也没有通道去做这件事。下面用一个表对比只读和全权限设计的差异维度只读 MCP Server全权限 MCP Server暴露工具搜邮件、读邮件、列账号搜邮件、读邮件、发邮件、删邮件数据风险AI 只能读取无法修改幻觉可能导致误发、误删用户信任容易接受需要较强信任背书实现复杂度低IMAP 只读会话即可高需要事务、确认、审计适合阶段个人使用、初期产品企业流程完善后对个人开发者来说先做只读版本是最理性的选择代码量小、风险低、能快速验证 AI 邮箱助手的产品价值。等真的需要“让 AI 帮你写邮件”时再单独设计发送工具、加入确认机制也不会太迟。3. 只读边界不止是一个标签而是三层约束这个项目最值得研究的是“只读”到底怎么落地。只看工具命名不叫只读真正可靠的是在多个层面同时实施限制。第一层是工具注册层。MCP Server 里根本不注册发送、删除、移动类的工具AI 就不会产生调用它们的意图。这是最基本的约束相当于让这些可选项从 API 上消失。第二层是 IMAP 会话层。IMAP 协议提供了两种打开文件夹的方式SELECT 以读写模式打开EXAMINE 以只读模式打开。本项目应该使用 EXAMINE这样即使 MCP 工具代码里误调用了设置标志位的方法服务器端也会拒绝或忽略。第三层是数据读取方式。即使只读取邮件正文也有读写差异使用 BODY[] 抓取会隐式设置 \Seen 标志也就是把邮件标记为已读使用 BODY.PEEK[] 抓取则不会改变任何状态。在只读设计里所有读取都应该使用 BODY.PEEK。这三层约束的关系很像数据库事务的只读模式。你在 Spring 里配置了 readOnlytrue 的事务MyBatis 执行写操作时会抛出 write operations are not allowed in read-only mode 之类的错误。工具注册层相当于 SQL 层面的权限控制IMAP 会话层相当于事务管理器而 BODY.PEEK 则相当于查询语句本身避免了副作用。每多一层约束意外写操作的概率就低一截。有一个容易被忽视的细节标记已读算不算写操作从 IMAP 协议角度看它当然算因为修改 \Seen 标志位就是状态变更。因此严格只读的邮件 MCP Server 不应该暴露“标记已读”工具。如果一个 AI 客户端尝试执行类似操作接口应该直接返回“不支持”而不是悄悄降级后执行。4. 环境准备与前置条件在写代码之前先确认环境。以下以 Python 为例这套逻辑同样可以用 TypeScript、Go 或 Java 实现语言不是关键。Python 3.10 或更高版本具体版本以实际环境为准。MCP Python SDK使用 pip 安装。一个支持 IMAP 的邮箱账号并确认服务商已开启 IMAP 服务。邮箱服务商提供的应用专用密码不要使用主登录密码。一台可以运行 Python 进程的设备本地电脑或小服务器均可。一个支持连接 MCP Server 的 AI 客户端手机或桌面端均可。安装 MCP SDK 的命令pip install mcp如果使用 uv 等包管理器也可以按对应方式安装。安装后可以通过以下命令确认 SDK 可用但不同版本 API 存在差异后续代码以通用模式为准python -c import mcp; print(mcp.__version__)这里要特别提醒邮箱密码不要直接写进代码。大部分邮箱服务商都支持开启 IMAP 后生成应用专用密码这类密码一般只对特定协议有效即使泄露也相对容易撤销。后面的示例代码会使用环境变量来读取密码。5. 核心流程拆解把整个项目拆开核心流程有五步。第一步是配置邮箱服务。你需要确定每个邮箱的 IMAP 服务器地址和端口一般是 imap.服务商域名 和 993 端口使用 SSL 加密。同时创建一个配置文件记录邮箱名称、IMAP 主机、用户名、对应环境变量名但不记录真实密码。第二步是初始化 MCP Server。用 FastMCP 创建一个单例实例给服务命名比如 mail-reader。这个实例负责工具注册和通信协议处理。第三步是实现只读工具。这一步是核心。至少需要三个基础工具list_accounts 列出账号、search_mails 按条件搜索邮件、read_mail 读取某封邮件内容。每个工具只做读取操作内部全部使用 EXAMINE 和 BODY.PEEK。第四步是启动服务。本地开发可以用 stdio 模式MCP Client 通过标准输入输出与 Server 通信适合调试。如果要从手机访问则需要使用 streamable-http 模式监听网络端口让手机上的客户端通过网络连接。第五步是客户端注册。在支持 MCP 的 AI 客户端里配置 Server 地址。本地 stdio 模式配置为启动命令远程 HTTP 模式配置为 URL。配置完成后AI 客户端就能发现并调用邮箱工具了。这五步里最容易踩坑的是第二步和第四步的传输模式。FastMCP 默认的传输方式是 stdio适合本地脚本。手机访问场景必须显式切换到 HTTP 传输