Skip to main content
Glama

office365-mcp

一个用于 Microsoft 365 的多用户远程 MCP 服务器 —— 通过 Microsoft Graph 接入 Outlook 邮件、Teams 和 SharePoint/OneDrive。每位用户都通过普通的浏览器登录,使用自己的 Microsoft 账户进行连接;服务器会加密保存该授权,并在之后的每一次工具调用中都以该用户的身份执行。因此,只需连接一次就能持续使用,而客户端全程不接触任何 Microsoft 令牌。共享邮箱和支持公用事业邮箱(support@、billing@、info@)在这里是约定身份,而不是作为参数附属在少数工具上。

一套 TypeScript 代码库,现有三种部署目标:

平台

入口

构建 / 部署

AWS Lambda (Function URL)

src/entries/lambda.ts

npm run build:lambda && npm run deploy:lambda

先验证配置,而无需触碰 AWS:

DRY_RUN=1 npm run deploy:lambda

| Azure Functions (v4 Node) | src/entries/azure.ts | npm run build:azure && func azure functionapp publish … | | 普通 Node(开发 / 自我托管) | src/entries/node.ts | npm run dev |

与其他 Microsoft 365 MCP 服务器的差异

开源领域非常庞大,其中也不乏优秀的项目。但本项目围绕一种不同的部署和身份模型构建,目标不是更多 Graph 覆盖。

  • 服务器负责代理刷新令牌;客户端永远看不到 Microsoft 令牌。 多数现有服务器会把一个用户、一台机器上的一个令牌放在 ~/.outlook-mcp-tokens.json、~/.microsoft_mcp_token_cache.json、~/.office-mcp-tokens.json 之类的路径下,并以明文保存。唯一成熟的远程服务器(Softeria 的 ms-365-mcp-server,HTTP 模式)直接表明令牌刷新是客户端的职责,因此 Graph 访问令牌约一小时过期后,会话就随之失效。而本服务器的做法是:每个用户的 Entra 刷新令牌用 AES-256-GCM 封存并保存在服务端,之后由服务器替用户在后台静默续期访问令牌。

  • 它是无状态、应 serverless 形态的。 没有 SSE 会话粘性,没有常驻进程,全部会话和凭据状态都放到 DynamoDB。支持 HTTP 的同类方案则假设总是存在一个常驻容器(Express、Azure Container Apps,或本地 stdio shim 后的 App Service 后端)。

  • 共享邮箱被成为建模的一部分,包括仅应用(app-only)路径。 如果调用方在 Exchange 中持有权限,服务器会借助其自身的委派令牌访问 /users/{mailbox};如果没有该种委派、也没有人登录邮箱,则可以使用作用域受限的应用凭据。没有任何开源服务器提供第二经治理的路径,而 Exchange 端的作用域限制通过一段脚本(deploy/entra/scope-app-only.ps1)随附在项目中,而不是留给用户自己处理。

  • 用户来决定助手能使用哪些邮箱。 Entra 没有针对委派后的 Mail.*.Shared 范围去支持单一的邮箱级同意:授权这些 range 范围会返回一种令牌,该令牌可以打开 Exchange 允许该用户打开的所有邮箱,且 Microsoft 没有办法对其进行收窄。因此,登录之后会进入一个由本服务器处理的 邮箱审批页面;用户在页面中勾选的内容会在服务端每一次调用时强制生效。可参阅 邮箱审批。

  • 有治理规则可以提供规划。 用户级工具允许列表开启(app tool allowlist),新工具默认拒绝(default-deny);有用户级速率限制;在用户自行批准之外,管理员还有一个邮箱上限;删除不可逆操作有部署级锁定;每次调用都会生成结构化审计记录,写明用户、工具、参数以及被实际请求或触碰到的是哪一个邮箱。

  • 工具覆盖的是刻意克制的。 共 27 个、按任务编写的工具,不是 300 个按端点映射的工具。广度并不是这个服务器的竞争点:Softeria 覆盖 Excel 区域和 OneNote 页面;Microsoft 自己的 Work IQ 则提供语义搜索和 Defender 级别的跟踪。而无论哪个产品,都不提供一个可让你在自己的区域内自托管并且无需 Microsoft 365 Copilot 授权的服务器。

还有两个第一个方的选项值得知道。Microsoft 的 MCP Server for Enterprise 是免费的,但只读,并且只覆盖 Entra 目录数据——这是互补,而不是对手。Agent 365 / Work IQ 确实覆盖邮件、日历、Teams 和 SharePoint,但当前只是预览、只能在 Microsoft 托管队,并且必须有 Microsoft 365 Copilot 许可证。

Related MCP server: m365-mcp

快速开始

1. 注册 Entra 应用。 这是最容易出错的步骤。按照 deploy/entra/SETUP.md 的说明——特别是,把平台注册为 Web 而不是 SPA(SPA 会通过重定向在后台把刷新令牌的生存期 24 小时,且所有由出的后衍生令牌都会继承这一时间窗口,从而使“只连接一次”的前提不复存在)。

2. 生成两个密钥。 这两个密钥用途不同,互相无法替代。

npm install
npm run gen:oauth-key   # RS256 keypair — signs the tokens Claude presents to US
npm run gen:enc-key     # AES-256-GCM key — seals the tokens WE present to Microsoft

请将 gen:enc-key 的输出备份到一个秘密管理器中,并且不要与部署文件放在一起。否则每次已经保存的连接都会无法解密,并且需要所有用户都重新登录一次。

3. 部署。

npm run build:lambda
npm run deploy:lambda        # wraps `sam deploy` against deploy/aws/template.yaml

该堆栈会打印出一个 EntraRedirectUri 的最终输出。请在应用注册中注册一个准确匹配该 URI:这是唯一不能自动化的步。

4. 连接客户端。 把 https://<你的部署>/mcp 添加为自定义客户端。客户端可以从 /.well-known/oauth-protected-resource 发现 OAuth 的中间端点,进行注册,然后引导用户走 Microsoft 登录流程。Microsoft 同意之后,会接着出现本服务器的 邮箱审批页面。用户确认、页面上勾选哪些共享邮箱可供助手访问;只有在这个页面完成后,客户端才会收到令牌。然后调用 o365_whoami —— 它会返回连接状态、已授予的权限、用户批准的邮箱范围,以及当前调用方可以使用的工具。

如需在本地开发,请把 Configuration 中定义的变量写入 .env 文件——最小是 Entra 注册信息、两个密钥以及 MCP_USERS_FILE + MCP_GRAPH_FILE + MCP_OAUTH_FILE(这些变量用于一批文件型存储,正因如此 npm run dev 才可以在没有 AWS 的情况下跑起真实的浏览器登录和邮箱审批流程)。.env.example 是带注释的版本。随后:

npm run dev                              # http://localhost:3000/mcp

在应用注册中把 http://localhost:3000/oauth/callback 添加为第二段重定向 URI。

架构

  Claude / MCP client
        │  1. POST /mcp  (Bearer: our RS256 JWT)
        ▼
┌──────────────────────────────────────────────────────────┐
│  office365-mcp   (Lambda Function URL / Azure Fn / Node)  │
│                                                           │
│  Hono ── /mcp ── JSON-RPC 2.0 ── tool registry            │
│    │                                                       │
│    ├─ OAuth 2.1 authorization server (for the MCP client)  │
│    │    /.well-known/*  /oauth/register  /authorize        │
│    │    /callback  /consent  /token  /jwks.json            │
│    │                                                       │
│    └─ Graph token broker ── actor resolution ── client     │
└───────┬──────────────────────────┬────────────────────────┘
        │                          │
        │ 2. browser sign-in       │ 5. Bearer: Graph access token
        ▼                          ▼
  Microsoft Entra ID        Microsoft Graph
  login.microsoftonline     graph.microsoft.com/v1.0
        │
        │ 3. refresh token ──► AES-256-GCM ──► DynamoDB (MCP_GRAPH_TABLE)
        │                       (row-bound AAD)
        ▼
  4. the browser lands back here, on /oauth/consent — the user ticks
     which mailboxes the assistant may use, and only then is the
     authorization code handed to the MCP client

传输层(Transport)。 使用无状态的 Streamable HTTP。每路请求用 JSON-RPC 2.0 调用 POST /mcp,每个请求只传一条消息。JSON-RPC 批处理会被拒绝,返回 -32600——一方面批次在 MCP 2025-06-18 修订版中已经移除,另一方面一批调用会被当成一次速率限制购买。而未认证请求会 401,并带有 RFC 9728 的 WWW-Authenticate: Bearer realm="mcp", resource_metadata=… 挑战,这会促使客户端加入 MCP OAuth 流程。受保护资源文档会同时放在 /.well-known/oauth-protected-resource 和 /.well-known/oauth-protected-resource/mcp 两个路径,其中 resourceiates改为 {origin}/mcp —— 即用户真实输入的那个端点,这正是客户端在比对时要使用的值。

身份模型

这里存在着两种完全独立的 OAuth 关系,并且它们分离是设计的整体基础。

  1. MCP 客户端 ↔ 本服务器。 本服务器相当于授权服务器。客户端动态注册(RFC 7591),通过 /oauth/authorize 和 /oauth/token 进行授权码 + PKCE 流程,并收到由我们签名的 RS256 JWT(JSON Web Token)。Entra Italiana 不支持动态客户端注册,也不会看到这段交换。

  2. 本服务器 ↔ Entra。 本服务器是声明明确的 的。理在用户登录过程中,我们向 Entra 发起一个独立的 PKCE 链路,用准备 .运行 or. 授权码. 由于我们请求了 offline_access,因此会收到获得的 id_token, 一个 Graph 访问令牌,以及——因为我们 — regardless — a refresh token。这里的一个 refresh token本身就是产品。该令牌以附加认证数据绑定到数据行({tid}:{oid}:refresh)后,用 AES-256-GCM 封装;因此,从一条用户记录中取出的 blob 无法换插到另一个用户的记录,而且写这些凭据的专专表与用户其他数据相互隔离。之后对于每次工具调用,它会:处理 JWT → (tid, oid) → 命中带有缓存的 access token,否则 renew via paid?。整个过程中,用户的身份标识不可变的 (tid, oid) 组合 (来自 id token), rather than email, preferred_username 或 upn。这三个字段都可能变,而且管理都可以改。

这两者之间,执行流程会在 /oauth/callback 处暂停。当 Microsoft 授权被保存后,它不会直接把 MCP 客户端的授权码返回,而是会带有一个只能使用一次的票据,将浏览器重定向到 /oauth/consent,由用户来决定这个助手使用哪些邮箱。只有在提交这个页面后,授权码才被正式接受,并让客户端来到后台浏览器跳走。可参见 邮箱审批。

租户允许列表(O365_ALLOWED_TENANTS)会被每一次请求就此检查,而不仅仅是在登录时;所以一旦从列表中删除一个租户,会立即生效,而不必等下一次登录后。 Azure AD 提供不了这类结果。

委派 (delegated) hard vs app-only

每次 Graph 调用都会先从配置中确定性地解析出一个 actor,它是不会因为模型生成的请求而提升权限:

Actor

访问的地址

满足条件

可以查看

delegated-self

/nme

没有 mailbox 参数,或该参数就是调用者自己的邮箱

与当前登录用户只见到的完全一致

delegated-shared

/users/{upn}

另一个邮箱;用户在连接时已同意的、被管理员策略允许,且调用者在该邮箱策略上有 Exchange 权限

Exchange 已授予该用户在此邮箱中的 explicit 权限

app-only

/users/{user}

邮箱在 O365_APP_ONLY_MAILBOXES 中、app-only 已启用,且调用者策略包含 allowAppOnly 可能授权

应用在 Exchange 层作用域 (scope) 中定义并可获得的那个范围

除 delegated-self 之外,先整体执行 policyAllowsMailbox 检查——先查用户本身的审批,再查管理员的配置上限——然后才会向 Graph 提出请求。委派 (delegated) 是默认且正常的路径:通过上述两层次门后,租户现有的权限是真正的门槛;当 Graph 对拒绝时,服务器会返回真实 403,但服务器会将其转成“请管理员在该邮箱上启用 Full Access”这样的提示。app-only 只为那些没有授权登录的邮箱存在,并且默认是关闭(关于详情见下文)。/me 永远不能表示一个共享邮箱——它不存在进入 shared 邮箱的路径——并且 app-only 令牌也永远不能用 /me,因为根本没有“已登录用户””。

在上述这些都上,Teams 的设计目标只是固有地只支持 delegated 权限,且永远这样。参见 限制与已知问题。

暴露给模型的工具

共 个? Tools. W 标记的是改变租户状态的工具——这些工具对新用户默认不启用,并且清单上还需包括 allowWrites 策略。每个 Outlook 工具都有一个可选的 mailbox 参数(UPN 或 SMTP 地址),用于指定便签要执行命令的目标;如果不填,则作用于发件人本人。所有返回的 id 都是不透白视的 Microsoft Graph id——请原封不动地传回,不要构造 id。

Outlook —— 读操作

工具

用途

o365_mail_search

使用 Outlook 搜索语法(from:、subject:、attachment:、hasAttachments:true 等)对邮箱进行全文搜索。结果始终按日期排序,上限为 1,000 条(Microsoft 限制),且不能与筛选条件组合使用。

o365_mail_list

使用结构化筛选和排序列出文件夹中的邮件 — 仅未读、特定发件人、日期范围、排序方式。与 o365_mail_search 互补:精确筛选,不支持关键词匹配。

o365_mail_get

获取一封邮件的完整内容,正文为纯文本,可选附带 Internet 邮件头和附件元数据。过长的正文会被截断,并报告原始长度。

o365_mail_folders

列出邮件文件夹,可只列出顶层,也可列出完整目录树,附带邮件数和未读数。在移动邮件前,用它将文件夹名称解析为文件夹 id。

o365_mail_attachments_list

列出一封邮件的附件名称、类型、大小和内联标志。仅元数据 — 绝不含文件内容。

o365_mail_attachment_download

下载附件并返回一个短期有效的预签名 URL。绝不内联返回字节内容。

Outlook — 写入

工具

用途

W o365_mail_send

立即发送邮件,可内联编写,也可从草稿发送。Graph 接受投递但不返回 id,因此此工具报告的是 “accepted”,而不是 “delivered”。

W o365_mail_reply

一步完成对现有邮件的回复、全部回复或转发。你的文本会放在所引用的原文上方。

W o365_mail_draft_create

创建草稿,可新建,也可作为已引用原邮件的回复/转发。返回草稿 id。

W o365_mail_draft_update

编辑草稿的主题、正文或收件人。仅适用于草稿。

W o365_mail_move

将邮件移动或复制到另一个文件夹。移动会改变邮件 id — 返回新 id,旧 id 立即失效。

W o365_mail_delete

删除邮件:trash(可恢复,默认)、soft 或 permanent — permanent 不可恢复,并额外受 O365_ALLOW_PERMANENT_DELETE 限制。

W o365_mail_flags

一次最多同时为 20 封邮件标记已读/未读、设置标志、分类和设置重要性。

W o365_mail_folder_manage

创建、重命名、移动或删除邮件文件夹。

W o365_mail_attachment_add

向草稿添加附件。小于 3 MB 以内联方式,最高 150 MB 通过分块上传会话方式添加。

Teams

工具

用途

o365_teams_list

列出你的团队,或某个团队中的频道。解析其他 Teams 工具所需的团队或频道 id 的方法。

o365_teams_chats_list

列出你的聊天 — 一对一、群聊和会议聊天 — 最近活动优先。一对一聊天的标题由显示参与者姓名拼接而成。

o365_teams_messages_list

读取频道或聊天中的消息。聊天支持日期范围;频道不支持,因为 Graph 的频道 API 没有日期筛选参数。

W o365_teams_message_send

以你自己的身份向频道、频道讨论或聊天发送消息 — 也可以直接向邮箱地址发送,它会自动查找或创建一对一聊天。不使用 user 参数:Teams 永远将消息归属于当前登录用户,因此如果提供 user 这样的参数,就等于暗示一种不可能发生的用户模拟。

o365_teams_search

在你能访问的所有聊天和频道中进行关键词搜索。这是唯一搜索 Teams 的途径;列表 API 完全没有搜索功能。同样不使用 user 参数 — /search/query 作用域限于令牌的所属者,而且没有任何“以他人身份搜索”参数。

SharePoint 和 OneDrive

工具

用途

o365_files_search

在 SharePoint 和 OneDrive 中,或在任意一个网站点或文档库内进行文件搜索。支持 KQL 关键词(filetype:、author:、path:)。

o365_files_sites

查找网站,或列出网站的文档库及对应的驱动器 id。SharePoint 工作的起始点.

o365_files_list

列出 OneDrive 或文档库中的文件夹 — 按驱动器 + 项目 id、按路径,或你的 OneDrive 根目录。

o365_files_get

获取某个文件或文件夹的信息 — 包括粘贴的共享链接,提供该链接可自动解析。可选提供哪些用户访问权限。

o365_files_download

下载文件为预签名 URL,可选转换 PDF 后交给您。绝不内联返回。

W o365_files_share

通过链接共享或邀请人员共享。报告的是实际可获取的权限,因为租户的共享策略可能静默降低你请求的权限。

核心

工具

用途

o365_whoami

查看你当前登录身份、Microsoft 连接是否生效及其有效活动时间、已授予哪些权限、你当前持有哪些工具、你有权使用且可立即使用的共享邮箱,以及每一项权限会以你本人身份还是以服务账户的身份运行。当出问题或出现异常时,首选先调用这个工具。

配置

所有配置都只通过环境变量完成。最权威的定义在 src/plconfig.ts 中的 Env 类型中。

Entra 应用注册

变量

必填

描述

OAUTH_ENTRA_TENANT_ID

是

租户 GUID 或已验证的域名。每个调用都指向这个具体的租户 —— /common 会导致令牌缓存未命中和不必要的重新认证,并且对客户端凭据无效。common / organizations 会把部署变成多租户方式。

OAUTH_ENTRA_CLIENT_ID

是

应用程序(客户端)ID。注册时必须使用平台类型 Web。

OAUTH_ENTRA_CLIENT_SECRET

二选一

客户端机密。最简单,但 Entra 将其有效期限制为 24 个月。

OAUTH_ENTRA_CLIENT_CERT_PEM

二选一

用于证书客户端身份验证的 PKCS#8 PEM 私钥(字面形式或 base64)。生产环境推荐此方式。

OAUTH_ENTRA_CLIENT_CERT_THUMBPRINT

配证书使用

与门户中显示的十六进制 SHA-1 指纹。Entra 通过指纹将断言与证书 相匹配,因此两个部分都必需提供。

O365_ALLOWED_TENANTS

多租户运行时允许的租户 ID 逗号分隔列表。登录时以及每个请求到来时都会检查,因此在此移除某个租户会立即让其现有连接失效,而不是等到下一次登录。与 common 搭配且留空时,表示任意已同意的租户都可以连接;服务器会在启动时发出明显警告。

密钥

变量

必填

描述

OAUTH_SIGNING_KEY_PRIVATE

是

用于签署我们的 MCP 访问令牌的 RS256 密钥的 Base64 PKCS#8 PEM。用 npm run gen:oauth-key 生成。同步更换这两个字段即可。

OAUTH_SIGNING_KEY_PUBLIC

是

匹配公钥的 Base64 SPKI PEM。发布在 /.well-known/jwks.json。

OAUTH_SIGNING_KEY_KID

JWKS 和令牌标头中的密钥 ID。在轮换密钥对时一并修改。默认值 primary。

O365_TOKEN_ENC_KEY

是

<kid>:<base64 32 字节> —— 用作存储在所有位置的充数字符,来封存每个已存储的 Entra 刷新令牌和缓存的访问令牌。用 npm run gen:enc-key 生成。如果丢失,则所有已保存的连接将永久失效。

O365_TOKEN_ENC_KEYS_PREVIOUS

逗号分隔的已弃用密钥(格式相同),仅接受用于解密。这就是轮换能够以滚动方式而非一次性切换完成的机制。

Graph 行为

变量

默认值

说明

O365_SCOPE_PROFILE

work

work 包含共享邮箱、SharePoint 网站和 Teams 频道 作用域,其中多项需要租户管理员一次性授权。personal 仅请求用户可同意的作用域。

O365_SCOPES

自动派生

以空格分隔的全量覆盖。加载时校验:/.default 不得与具名作用域混用(AADSTS70011),且 offline_access 为必填。

O365_GRAPH_BASE

https://graph.microsoft.com/v1.0

仅为某些独立云修改,因为许多本应依赖的功能在这些环境中不存在。

O365_IMMUTABLE_IDS

true

在 Outlook 调用中发送 Prefer: IdType="ImmutableId",使 id 在邮箱移动后依然有效。第一次部署时就确定,绝不要在运行中的堆栈上再更改位置。

O365_BODY_FORMAT

text

text 请求纯文本正文,这是面向模型的服务器需要 格式——HTML 正文基本都是跟踪标记。

O365_GRAPH_TIMEOUT_MS

30000

每请求超时,远小于客户端 300 秒的工具超时。

O365_MAX_CONCURRENCY_PER_MAILBOX

4

每个(应用程序,邮箱)的在途请求数上限。Exchange 只允许 4 个;该值被固定在这里,因为提升它只会把吞吐变成 429 响应。

O365_USER_AGENT

NONISV|SelfHosted|office365-mcp/0.1.0

Microsoft 会降低未注明来源的流量的优先级。请保持文档规定的格式,把你们自己的公司名填到中间字段。

O365_SEARCH_REGION

auto

SharePoint 地理位置(NAM、EUR、APC),用于 POST /search/query。仅应用搜索时必须设置;在多地理位置租户则需要显式设置。

门限与输出

变量

默认值

描述

O365_APP_ONLY_ENABLED

false

仅应用模式的主开关。当为 false 时,无论配置任何允许列表,代码路径均不可达。

O365_APP_ONLY_MAILBOXES

空

逗号分隔的邮箱地址列表,可使用应用凭据访问。通配符会被直接拒绝。

O365_APP_ONLY_SITES

空值

使用应用凭据可访问的站点 ID 或 URL 的逗号分隔列表,并强制应用于每次使用应用专用执行体进行的站点寻址或驱动器寻址调用。空列表意味着“应用专用模式”完全无法访问 SharePoint,且应用程序专用调用若未指明站点则被拒绝,而不是被允许。匹配不区分大小写,且可以是精确匹配,也可以是以 / 结尾的路径边界前缀,因此某个条目既可以覆盖一个站点及其下所有内容,又不会因较短的引用而扩大范围。与 Sites.Selected 注册搭配使用。

O365_ALLOW_PERMANENT_DELETE

false

在用户策略之上,为 o365_maintenance_delete 模式 permanent 设置部署级锁定。

MCP_OUTPUT_FORMAT

toon

toon 输出紧凑的表格化输出,可在列表操作中显著减少令牌使用量;json 为编程式调用方输出美观的 JSON。

MCP_ARTIFACT_BUCKET

S3 存储桶,用于下载。每个下载工具必需——设计上无 base64 回退。

MCP_ARTIFACT_URL_TTL_SECONDS

3600

预签名 URL 的存活时长。持有该 URL 的任何人都可以在不进行身份验证的情况下获取文件,因此请保持较短的 TTL。

MCP_ARTIFACT_REGION

AWS_REGION

覆盖产物存储桶的区域。

MCP_AUDIT_FILE

JSONL 接收器,用于审计与安全记录,并额外写入 stderr。对于自托管部署,在 Lambda 上,stderr 已经可以到达 CloudWatch。

MCP_AUDIT_READS

未设置

1 同时审计读取和写入工具。默认关闭,因为读取量占主导。独立于本设置之外,每个变更调用、每个失败调用,以及每次调用会命名另一个邮箱或用户的内容都会被记录——处理他人邮箱正是合规性审查所关注的。

PORT

3000

普通 Node 入口点的监听端口。在 Lambda 和 Azure Functions 上未使用。

共享邮箱访问

此功能是整个设计所围绕的核心,其能力本身取决于租户而非此服务器。

必须同时满足以下两个条件,Graph 才会执行操作。 被委派的 .Shared Graph 作用域(Mail.Read.Shared、Mail.ReadWrite.Shared、Mail.Send.Shared——全部存在于 work 作用域配置文件中),并且 Exchange Online 已授予已登录用户对目标邮箱的权限。作用域仅解锁能力;Exchange 才是真正的门槛。 若无 Exchange 授权,无论授予何种同意,Graph 均返回 403。

管理员在 Exchange 管理中心(收件人 → 邮箱 → 共享邮箱 → 委派)中授予以下一项或多项权限:

权限

启用能力

效果

完全访问权限

读取、列表、移动、删除、草拟邮箱中的内容

每个针对该邮箱的读/写工具均需此权限。如果发送时应将副本放入共享邮箱的“已发送邮件”中,也同样需要此权限。

以其他身份发送

以共享邮箱作为发件人进行发送

收件人只会看到共享邮箱。

代表发送

代表该邮箱发送

收件人会看到“用户 代表 共享邮箱”。用户可以自行在 Outlook 中授予;只有管理员可以授予“以其他身份发送”。

权限授权后,最长可能需要一小时生效。授权后立即收到 403 通常是这一原因,服务器也会在错误中加以说明。

在上述两项之上,此服务器还增加了自己的两项:用户必须在连接时批准邮箱,并且管理员的 allowedMailboxes 上限必须满足。这两项都要在 Graph 被调用之前且条件不符时才会生效——请参阅 邮箱批准 章节。

然后简单传入任何地址,例如 o365_mail_list({ mailbox: "support@contoso.com", unreadOnly: true })。服务器解析操作者身份,调用 /users/support@contoso.com/…,并且绝不使用 /me——不存在通向共享邮箱的 /me 路由。该地址还必须在连接时得到用户的批准;请参阅下面的 邮箱批准。

有两个限制值得提前了解。没有 Graph API 可以枚举用户拥有哪些邮箱的权限——因此上述“批准”页面是验证候选地址,而非为你列举地址;同时调用工具仍需指定邮箱。已登录用户通常需要有各自已授权的邮箱,不过共享邮箱本身并不需要授权。

进一步缩小范围。 policy.allowedMailboxes 是管理员级上限,配置在用户自行批准的基础上;有效访问权限是两者的交集。默认值为 "*",即把剩余决定权交给 Exchange 来授权——你还可以将其设置为一个显式有限列表,以限制用户的 Exchange 权限;或者设置为 null,以整体拒绝 mailbox 参数:

npm run user -- add alice --mailboxes=support@contoso.com,billing@contoso.com

微软无法限制此授权范围,因此本服务器将加以限制。Entra 不为委派的 Mail.*(所有邮箱)提供按邮箱的单独授权。用户一旦授予这些权限,所生成的令牌就能打开 Exchange 允许该用户访问的每个邮箱,而微软方面无法进一步缩小范围 —— SharePoint 在 2024 年获得了委派 Sites.Selected 权限,Exchange 没有任何等效权限,也没有路线图上的计划。因此,下面的审批页面并非锦上添花:它是唯一能够限制该授权范围的手段,并且它在服务器端强制执行,每次 Graph 请求之前都会经过(policyAllowsMailbox 位于 src/users.ts,从 resolveActor 流程中调用)。

流程。 /oauth/authorize → Entra 登录 → /oauth/callback 存储密封的刷新令牌,然后将 MCP 客户端的授权代码重定向到 /oauth/consent,并附带一次性票据(15 分钟有效期)。该页面显示用户自己的邮箱 —— 始终包含、不可移除 —— 以及候选的共享邮箱,每个都已经探测过,因此无法打开的地址会以灰色显示并注明原因,而不是先接收再在之后失败。用户勾选此助手可使用的邮箱,然后才铸造授权代码,并将客户端重定向回首页。重新连接会重新运行该页面,并按之前的选项预勾选。这也是用户移除某个邮箱的方法。

需要保留“Allow”? allowed?

好的继续:“两个门禁,均在服务器端。 grantedMailboxes 是 用户 所批准的;allowedMailboxes 是管理员的上限。只有当两个列表中同时包含某个邮箱时,该邮箱才可访问。o365_whoami 将该交集报告为 usableMailboxes,因此模型只会看到实际可用的邮箱。用户自己的邮箱始终被允许,并且不出现于任一列表中。

API 密钥的身份永远看不到页面 ——没有浏览器,也没有人类用户可询问——因此同意门禁不适用于它。这是刻意为之:创建该密钥的管理员即为同意方,allowedMailboxes 单独生效。区分标准是身份是否具有 Entra 的 oid,即是否曾经历过浏览器登录。

仅应用模式

仅应用模式仅适用于一种场景:一个邮箱没有人登录、也没有被委派给任何人,但代理仍需对其进行分类。它使用应用程序自己的凭据而非用户的凭据,因此没有登录用户,/currentUser 无效。

默认关闭,除非确有需要,否则应保持关闭,因为管理员同样授权的应用程序 Mail.ReadWrite 会授权访问组织内每个邮箱。开启它需要三个独立的前提:AllowAppOAuth 为 true、O365_APP_ONLY_MAILBOXES 中列出的邮箱,以及调用者策略上的 allowAppOnly。虽然邮箱被加入了允许列表,但调用者缺少 allowAppOnly,则仍然回到委派模式,由 Exchange 应答。升级到仅应用模式并不会跳过两个邮箱门控:resolveActor 会先于仅应用分支考虑它们;因此,已登录用户仍然需要在审批页面上批准该地址。典型的仅应用调用者是 API 密钥的服务账户,它永远不会被询问,而 allowedMailboxes (允许邮箱列表)就是它的全部控制。

O365_APP_ONLY_MAILBOXES(O365 仅应用邮箱列表)只是控制的一半,而且是较弱的一半。 它约束的是这个代码会请求哪些邮箱。它不会对该凭据本身做任何限制:任何获得该凭据的人都可以访问租户中的每个邮箱。真正的控制是 面向应用的 Exchange RBAC,在租户侧强制实施。deploy/entra/scope-app-only.ps1 对其编写脚本:在 Exchange 中注册服务主体,创建启用邮件的安全组上的管理范围,将 Application.Mail.* 角色限制到该范围,并使用 Test-ServicePrincipalAuthorization 验证。

让整个方案失败的陷阱: RBAC 角色是附加 Entra 应用授权的。如果应用上仍然保留了未限定的应用程序同意,则实际生效的是两者的并集,而范围设置将毫无效果。必须移除 Entra 应用程序授权部分。同时为授权缓存做好预算:更改需要 30 分钟到 2 小时才能生效(Test-ServicePrincipalAuthorization 绕过该缓存,因此脚本以它结束)。

Teams 在此完全没有仅应用模式的路径——请参见下文。

每个用户密钥工具权限

当用户仓库配置完成(MCP_USERS_TABLE,或开发环境使用 MCP_USERS_FILE),每个用户记录都会携带 policy 策略:

字段

含义

allowedTools

"*" 表示授予所有工具权限,包括本地数据库未来新增加的工具 —— 管理员密钥权限、服务账户的全访问权限。数组则是固定的允许列表。

toolPermissions

本地数据库 {name: boolean} 映射表。默认拒绝:当且仅当本地 name 条目为 true 时,该工具才可调用。覆盖了 allowedTools 数组中配置。"*" 仍然最高优先级生效。

allowWrites

必填。标记为 user 且导致变更或移动的工具的必经权限。

grantedMailboxes

用户 的,前提:用户在服务器连接邮箱时,在权限确认页面上已经同意的邮箱列表。如果该字段缺失,则表明:用户从未被询问,且只有自己的邮箱可访问。由公司外部接口 /oauth/consent 决定,而非由邮件服务器的命令行(CLI),决定。

allowedMailboxes

管理员;在工具箱之上:"*"(默认)为显式地址列表,null 为公共权限——仅管理员自己的邮箱列表。权限判断逻辑为:为 grantedMailboxes 中的并集。对于仅通过 API 密钥实现的身份,API 密钥的 授权页 永远不会展示,因此管理员的整个控制层就是这里的权限判断。

allowAppOnly

是否允许仅应用模式参与者的用户级别门禁。默认 false,这将 永远 不允许基本用户绕过其自身已有的 Exchange 权限进行静默升级。隐私保护

permissionsPerMinute

每次调用的最大请求数。默认 60。更多。

disabled

禁用该邮箱。仅需标记为已删除,并不真正删除。

用户无法调用的工具同样会从脚本返回列表中被忽略,因此 LLM(大语言模型层) 永远无法「看到」它们。每次 OAuth 登录会话时,该应用映射都会被公司的注册表重新同步:设置 中含有新功能,以 now 初始值;但对于一个全新且可能存在危害的工具,必须发出故障信号。当且仅当 list 配置发生变更时,才会执行写入操作。

新配置的用户会收到所有只读权限的工具以及所有变更类工具均被禁用。

管理员命令行工具

npm run user -- list
npm run user -- add alice --writes --mailboxes=support@contoso.com --app-only
npm run user -- tools alice                      # the effective 27-tool map
npm run user -- tools alice --enable=o365_mail_send
npm run user -- rotate alice                     # new API key, old one dead
npm run user -- disable alice
npm run connection -- list                       # who is connected, scopes, last refresh — never token material
npm run connection -- test alice@contoso.com     # one live Graph call, proves the stored credential still redeems
npm run connection -- revoke alice@contoso.com   # server-side kill switch: delete the row, purge cached tokens
npm run connection -- rewrap                     # re-seal every stored secret under the current encryption key

connection revoke 命令将停止 此服务器 使用该凭据。租户侧的最终止杀开关是 撤销会话,位于 Entra 中用户对象的 "Exchange Online" 属性 —— 注意:仅更改密码并不能使机密客户端的刷新失效 页面 stored in the KINTO 刷新页面 for the revocation matrix.

局限性与已知问题

  • 在 AWS 上,WWW-Authenticate 头会重命名。 Lambda Function URL 会将其改写为 x-amzn-Remapped-WWW-Authenticate,且函数内部任何代码都无法阻止该行为。这** 不会影响 MCP 发现机制**:mcp 规范要求客户端直接回退到获取 /.well-known/oauth-protected-resource/mcp(根目录变体),而参考实现中 遇到 401 时无条件使用该地址—— 这就是为什么本服务器同时提供两份文档,并在响应中返回 与 MCP 节点 URL 完全相同的 resource 值。如果您遇到真正需要该标头的客户端,请在 CloudFront 前加上 Lambda \ Edge 的 origin-response 函数, 将重新映射的名称复制回来;viewer-response 不会生效,因为当源站返回 400 或更高状态码时,CloudFront 不会调用该函数。

简言之,因为这些大部分都会被当作 bug 体验到。

我们设计刷新令牌的中断模式是:90 天,滑动过期,而非固定到期:一个连接者如果每周在线,实际上永远不会过期;而另一个中断 91 天后回来了,会立即失效 AADSTS700082。另外,条件访问的登录频率会强制重新认证,并且任何服务器端代码都无法阻止该认证。管理员在 Microsoft 365 中重置密码会立即使旧凭证失效;而用户更改自己的密码则也不会影响旧会话。所有这些都会体现为一个单一的、清晰的、带有 URL 的工具错误。

关于修订令牌的过期,是悬崖,不是陡坡。 Entra 将令牌的生命周期上限设为 24 个月;到期后,该部署的所有用户同时失败,错误码 AADSTS7000822 —— 不是逐步失败,也并非一个接一个地失败。证书凭据可避免此失效模式;务必在任一日期前轮换。

丢失加密密钥意味着不可恢复。 丢失密钥, 将丢失用户会话,所有用户需要同时重新登录。通过 O365_TOKEN_ENC_KEYS_PREVIOUS 旋转备份,并保持 connection rewrap,永远不要替换它。实际上,如果你想要 O365_TOKEN_ENC_KEYS_PREVIOUS 是 O365_TOKEN_ENC_KEYS_PREVIOUS。

Teams 消息仅适用于代理授权。 Teams 提供唯一的应用权限 Teamwork.Migrate.All 以及其端点 Teamwork Migration scenario;而微软将其限制为只能迁移应用及执行,其使用范围受限,不存在任何合规方式以服务的形式发送 Teams 消息。若要实现此需求, 仅能使用 robot framework 机器人,而此方式为一个 Bot Framework 机器人或 Teams 应用程序包:安装包需安装在每个租户/每个资源下面。 的架构(这并不适合远程访问 )远程调用。 无人值守的 Teams 消息不在考虑范围内。 (Teams metering 无关紧要:模式 A / 模式 B 计费制度于 2025 年 8 月 25 日截止,尽管当前大多数文档仍然这样认为。)

Teams 频道作用域需要租户管理员同意。 具体而言,ChannelMessage.Read.All 无法自行同意。没有管理员权限的自托管者运行 O365_SCOPE_PROFILE=personal 时,邮件、文件和聊天功能可以正常工作,而频道工具会失败但附带解释,而不是神秘的 403。personal 还会省略 User.ReadBasic.All,因此无法解析对话之外人员的 @ 提及:消息仍然会发送,名字以纯文本形式留在正文中,工具会返回警告,说明未通知该人员。

邮箱审批页面只会少报,绝不会多报。 它通过尝试打开邮箱的收件箱来判断你是否可以使用该邮箱,而这里的 403 有三种可能的原因。如果你仅拥有 Send As 权限,或者只能访问某个文件夹而非整个邮箱,该地址会显示为不可用且无法勾选,即使这种更窄的访问权限本来足以满足你的需求。相反的错误不会发生——你能勾选的地址一定是服务器确实能够打开的。

搜索存在看似数据丢失的硬性上限。 Outlook $search 最多返回 1,000 条结果,并且无法与筛选器或自定义排序结合使用。Teams 搜索报告的是页数而不是总数,因此永远不能作为匹配计数来呈现。SharePoint 深层分页在第 1,000 条结果之后停止,而应用专用搜索默认排除私有 OneDrive 内容——启用它会预配一个新索引,可能需要几天到一周的时间,在此期间结果会静默不完整,而且完全没有错误。

租户共享策略会静默改写 o365_files_share 生成的结果。 组织级和站点级设置可以将匿名链接降级为仅组织内部、强制设置过期时间,或将链接设为仅查看。更糟糕的是,createLink 按(应用程序,链接类型)幂等,因此请求一个新的七天链接可能返回一个多年前创建、永不过期且作用域不同的链接。该工具始终会回读并报告实际授予的权限,这是唯一的防御。

消息 ID 在消息移动时会改变,Teams ID 并非全局唯一。 正是出于这个原因,O365_IMMUTABLE_IDS 默认开启,但它实际上是一扇单向门:以某种格式发放的 ID 在另一种格式下无法使用,而在运行中的堆栈上开启它会产生 ErrorInvalidIdMalformed。另外,Teams 消息 ID 仅在其聊天或频道内唯一,因此消息 ID 总是随其对话坐标一起返回。

限流是日常故障中最可能出现的原因。 Outlook 允许每个(应用程序,邮箱)4 个并发请求,每十分钟 10,000 个请求;Teams 每个频道、每个聊天和每个用户每秒大约允许一个请求;SharePoint 每次权限调用消耗五个资源单位,并且对搜索的限流远严于 Graph 的其他部分。批处理无济于事——Graph 从一批中最多并发转发四个子请求到 Outlook。服务器会限制自身的扇出,并精确遵守 Retry-After,但一个急切的智能体最终仍会遇到 429。

超过 3 MB 的附件在共享邮箱中无法使用。 Microsoft 有文档说明,委派调用者在共享或委派邮箱中向邮件附加大文件时会收到 403。低于 3 MB 则没问题。工具会直接说明这一点,而不是只显示一个 403。

主权云会静默缺失功能。 永久删除、聊天增量(chat delta)和 Teams 导出 API 在美国政府 L4/L5 和中国 21Vianet 中不可用,并且跨地域站点访问可能因与应用权限无关的原因失败。多地域租户需要为每个区域发出一个搜索请求,否则其他区域的内容会静默缺失。

刷新令牌轮换的竞争是良性的,但确实存在。 Entra 在每次兑换时都会颁发一个新的刷新令牌,并且不会撤销旧令牌,因此同一用户的两个并发调用各自都会收到一个有效的后继令牌。条件写入意味着一个会胜出,落败方会丢弃其副本;竞争失败绝不会导致工具调用失败。访问令牌缓存正是让它很少发生的原因。

路线图

这些是刻意做出的 v1 范围裁剪,而非疏忽:

  • 日历(Calendar)。 继邮件之后,人们最先期待的第二个功能。Calendars.ReadWrite 可由用户同意,而已为共享邮箱构建的参与者模型可以原样应用于共享日历。已排在下一步。

  • 文件上传到 OneDrive 和 SharePoint。v1 涵盖搜索、列表、获取、下载和共享。

  • 联系人/人员查找,用于将姓名解析为电子邮件地址。目前每个发送工具都假定调用者已经有了一个。

  • 收件箱规则(messageRules,需要 MailboxSettings.ReadWrite),用于服务器端分拣自动化。

此外还在考虑:在 stderr 之外提供可插拔的审计接收器(Firehose → S3 → Athena);在 AWS 上将工作负载身份联合作为第三种客户端凭据类型,从而根本不存在长期有效的机密;以及由服务器签发的不透明分页句柄,而不是原始的 @odata.nextLink 字符串。

目录管理被刻意排除在范围之外——Microsoft 的免费 MCP Server for Enterprise 已经覆盖了只读 Entra 查询。

贡献

参见 CONTRIBUTING.md。安全问题:SECURITY.md — 请勿公开提交 issue。

许可证

MIT。参见 LICENSE。

Related MCP Connectors

Related MCP Servers