office365-mcp
office365-mcp
Многопользовательский удалённый MCP-сервер для Microsoft 365 — почта Outlook, Teams и SharePoint/OneDrive через Microsoft Graph. Каждый пользователь подключается со своей учётной записью Microsoft через обычный вход в браузере; сервер сохраняет эту авторизацию в зашифрованном виде и действует от имени этого пользователя при каждом последующем вызове инструмента, так что подключение, установленное один раз, продолжает работать без хранения токена Microsoft у клиента. Общие и служебные почтовые ящики (support@, billing@, info@) являются полноценной идентичностью, а не параметром, прикрученным к нескольким инструментам.
Одна кодовая база на TypeScript, три цели развёртывания:
Платформа | Точка входа | Сборка / развёртывание |
AWS Lambda (Function URL) |
|
|
Проверьте конфигурацию, не трогая 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 (dev / self-host) | src/entries/node.ts | npm run dev |
Чем это отличается от других MCP-серверов для Microsoft 365
Поле open-source велико, и несколько проектов хороши. Этот построен вокруг другой модели развёртывания и идентичности, а не вокруг более широкого покрытия Graph.
Сервер выступает посредником для refresh-токенов; клиент никогда не видит токен Microsoft. Большинство существующих серверов хранят один токен для одного пользователя на одной машине —
~/.outlook-mcp-tokens.json,~/.microsoft_mcp_token_cache.json,~/.office-mcp-tokens.json— в открытом виде. Единственный зрелый удалённый сервер (Softeria'sms-365-mcp-server, в HTTP-режиме) прямо заявляет, что обновление токена — обязанность клиента, поэтому сессия умирает, когда access-токен Graph истекает примерно через час. Здесь per-user refresh-токен Entra запечатывается с помощью AES-256-GCM и хранится на стороне сервера, а сервер незаметно обновляет access-токены от имени пользователя.Он не имеет состояния и имеет форму serverless. Никакой привязки к SSE-сессии, никакого долгоживущего процесса, всё состояние сессии и учётных данных — в DynamoDB. Альтернативы с поддержкой HTTP предполагают постоянно работающий контейнер (Express, Azure Container Apps, бэкенд App Service за локальным stdio-шимом).
Общие почтовые ящики смоделированы, включая путь app-only. Если у вызывающего есть права Exchange, сервер использует его собственный делегированный токен против
/users/{mailbox}; если в почтовый ящик вообще никто не входит, он может использовать ограниченные учётные данные приложения. Ни один другой сервер с открытым исходным кодом не предлагает этот второй, управляемый путь, а ограничение на стороне Exchange поставляется здесь в виде скрипта (deploy/entra/scope-app-only.ps1), а не оставлено как упражнение.Пользователь сам выбирает, какими почтовыми ящиками может пользоваться ассистент. В Entra нет согласия на уровне почтового ящика для делегированных
Mail.*.Shared: предоставление этих областей даёт токен, который может открыть любой почтовый ящик, который Exchange позволяет открыть этому человеку, и Microsoft не предлагает способа это сузить. Поэтому после входа следует страница одобрения, которую обслуживает этот сервер, и то, что пользователь отмечает там, принудительно применяется на стороне сервера при каждом запросе. См. Одобрение почтовых ящиков.Есть история управления. Списки разрешённых инструментов для каждого пользователя, которые по умолчанию запрещают новые инструменты, ограничения скорости для каждого пользователя, потолок почтовых ящиков администратора поверх собственного одобрения пользователя, блокировка необратимого удаления на уровне развёртывания и структурированная запись аудита для каждого вызова с указанием пользователя, инструмента, аргументов и фактически затронутого почтового ящика.
Поверхность инструментов намеренно мала. 27 инструментов в форме задач, а не 300 в форме конечных точек. Широта — не то, с чем этот проект конкурирует: Softeria покрывает диапазоны Excel и страницы OneNote, а собственные серверы Work IQ от Microsoft имеют семантический поиск и трассировку уровня Defender. Чего не предлагает ни один из них — это сервер, который вы размещаете сами, в своём регионе, без лицензии Microsoft 365 Copilot.
Существуют два собственных варианта, о которых стоит знать. MCP Server for Enterprise от Microsoft бесплатен, но только для чтения и ограничен данными каталога Entra — это дополнение, а не конкурент. Agent 365 / Work IQ действительно покрывает почту, календарь, Teams и SharePoint, но находится в предварительной версии, размещается только у Microsoft и требует лицензии Microsoft 365 Copilot.
Быстрый старт
1. Зарегистрируйте приложение Entra. Это шаг, который чаще всего идёт не так. Следуйте deploy/entra/SETUP.md — в частности, зарегистрируйте платформу как Web, а не SPA (redirect URI SPA незаметно ограничивает refresh-токены 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://<your-deployment>/mcp в качестве пользовательского коннектора. Клиент обнаруживает конечные точки OAuth из /.well-known/oauth-protected-resource, регистрируется и отправляет пользователя через вход Microsoft. За экраном согласия Microsoft следует собственная страница одобрения почтовых ящиков этого сервера, где пользователь отмечает, какими общими почтовыми ящиками может пользоваться ассистент; клиент получает свой токен только после этого. Затем вызовите o365_whoami — он сообщает состояние подключения, предоставленные разрешения, почтовые ящики, одобренные пользователем, и какие инструменты в данный момент доступны вызывающему.
Для локальной разработки поместите переменные из Конфигурации в файл .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 в качестве второго redirect 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Транспорт. 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=…, что заставляет клиент запустить поток OAuth коннектора. Документ защищённого ресурса обслуживается как по адресу /.well-known/oauth-protected-resource, так и по /.well-known/oauth-protected-resource/mcp и сообщает resource как {origin}/mcp — конечную точку, которую фактически ввёл пользователь и с которой клиент сравнивает.
Модель идентичности
Существуют два отдельных отношения OAuth, и их различие — вся суть дизайна:
MCP-клиент ↔ этот сервер. Мы — сервер авторизации. Клиент регистрируется динамически (RFC 7591), выполняет поток authorization-code + PKCE против
/oauth/authorizeи/oauth/tokenи получает подписанный нами JWT RS256. Entra не поддерживает динамическую регистрацию клиентов и никогда не видит этот обмен.Этот сервер ↔ Entra. Мы — конфиденциальный клиент с одним статическим Web redirect URI. Во время входа пользователя мы запускаем собственную, независимую цепочку PKCE к Entra, обмениваем код на наш client secret или сертификат и получаем id_token, access-токен Graph и — поскольку мы запрашиваем
offline_access— refresh-токен.
Этот refresh-токен и есть продукт. Он запечатывается с помощью AES-256-GCM под дополнительными аутентифицированными данными, привязанными к строке ({tid}:{oid}:refresh), так что blob, извлечённый из записи одного пользователя, нельзя воспроизвести в записи другого, и записывается в таблицу, которая существует только для учётных данных. Каждый последующий вызов инструмента идёт: JWT → (tid, oid) → кэшированный access-токен или обмен refresh-токена → Graph. Ключ пользователя — неизменяемая пара (tid, oid) из id_token, никогда не email, preferred_username или upn — все они изменяемы и управляются администратором.
Между ними /oauth/callback делает паузу. После сохранения авторизации Microsoft он не передаёт MCP-клиенту свой authorization code; он перенаправляет браузер на /oauth/consent с одноразовым билетом, и пользователь выбирает, какими почтовыми ящиками этот ассистент может адресовать. Только когда они отправляют эту страницу, код создаётся и клиент перенаправляется домой. См. Одобрение почтовых ящиков.
Список разрешённых тенантов (O365_ALLOWED_TENANTS) повторно проверяется при каждом запросе, а не только при входе, поэтому удаление тенанта вступает в силу немедленно, а не при следующем входе.
Делегированный и app-only
Каждый вызов Graph сначала детерминированно определяет актора из конфигурации — модель никогда не может заставить сервер повысить свои права:
Актор | Адрес | Когда | Видит |
|
| Нет аргумента | Ровно то, что видит вошедший пользователь |
|
| Другой почтовый ящик, который пользователь одобрил при подключении, политика администратора разрешает, и у вызывающего есть права Exchange | То, что Exchange предоставил этому пользователю на этом почтовом ящике |
|
| Почтовый ящик находится в | Всё, к чему Exchange RBAC ограничивает приложение |
Каждый актор, кроме delegated-self, сначала проходит policyAllowsMailbox — собственное одобрение пользователя, затем потолок администратора — прежде чем Graph будет задан какой-либо вопрос. Делегированный — это по умолчанию и обычный путь: после этих двух ворот существующие разрешения тенанта остаются реальными воротами, и отклонённый вызов даёт честный 403, который сервер переводит в «попросите администратора предоставить Full Access на этом почтовом ящике». App-only существует только для почтовых ящиков, в которые никто не входит, по умолчанию выключен и полностью описан ниже. /me никогда не означает общий почтовый ящик — нет пути /me к нему — и app-only токен вообще не может использовать /me, потому что нет вошедшего пользователя.
Teams — только делегированный по дизайну, навсегда. См. Ограничения.
Инструменты, доступные модели
27 инструментов. W отмечает инструмент, который изменяет состояние тенанта; они отключены по умолчанию для только что подготовленного пользователя и дополнительно требуют allowWrites в его политике. Каждый инструмент Outlook принимает необязательный аргумент mailbox (UPN или SMTP-адрес), выбирающий почтовый ящик для действия; опустите его для своего собственного. Все возвращаемые идентификаторы — непрозрачные идентификаторы Microsoft Graph — передавайте их обратно дословно и никогда не конструируйте свои.
Outlook — чтение
Инструмент | Что делает |
| Полнотекстовый поиск по почтовому ящику с использованием синтаксиса поиска Outlook ( |
| Список сообщений в папке со структурными фильтрами и сортировкой — только непрочитанные, отправитель, диапазон дат, порядок. Аналог |
| Одно сообщение целиком, тело в виде обычного текста, опционально с интернет-заголовками сообщения и метаданными вложений. Длинные тела обрезаются с указанием исходной длины. |
| Список почтовых папок — верхнего уровня или всего дерева — с количеством сообщений и непрочитанных. Используйте его для определения идентификатора папки перед перемещением сообщений. |
| Имена вложений, типы, размеры и inline-флаги для одного сообщения. Только метаданные — никогда содержимое файлов. |
| Скачивает вложение и возвращает кратковременный предварительно подписанный URL. Никогда не возвращает байты inline. |
Outlook — написание
Инструмент | Что делает |
W | Отправляет сообщение немедленно, составленное inline или из черновика. Graph принимает к доставке и не возвращает идентификатор, поэтому отчёт — «принято», а не «доставлено». |
W | Ответ, ответ всем или пересылка существующего сообщения одним шагом. Ваш текст помещается над цитируемым оригиналом. |
W | Создаёт неотправленный черновик — с нуля или как ответ/пересылку, уже цитирующую оригинал. Возвращает идентификатор черновика. |
W | Редактирует тему, тело или получателей неотправленного черновика. Работает только с черновиками. |
W | Перемещает или копирует сообщение в другую папку. Перемещение изменяет идентификатор сообщения — возвращается новый, а старый перестаёт работать. |
W | Удаляет сообщения: |
W | Отмечает прочитанным/непрочитанным, ставит флажок, категоризирует и задаёт важность до 20 сообщений за раз. |
W | Создаёт, переименовывает, перемещает или удаляет почтовую папку. |
W | Прикрепляет файл к черновику. До 3 МБ inline, до 150 МБ через сеанс фрагментированной загрузки. |
Teams
Инструмент | Что делает |
| Ваши команды или каналы в одной команде. Способ сопоставить имя команды или канала с идентификаторами, которые нужны другим инструментам Teams. |
| Ваши чаты — один на один, групповые и собрания — сначала самые недавно активные. Чаты один на один не имеют собственного имени, поэтому оно формируется из участников. |
| Читает сообщения из канала или чата. Чаты поддерживают диапазон дат; каналы — нет, потому что API каналов Graph не принимает фильтр по дате. |
W | Публикует от вашего имени в канал, в ветку канала или в чат — в том числе людям по электронной почте, что находит или создаёт чат один на один. Не принимает аргумент |
| Поиск по ключевым словам во всех чатах и каналах, которые вам видны. Единственный способ поиска в Teams; API списков вообще не имеют поиска. Также не принимает аргумент |
SharePoint и OneDrive
Инструмент | Что делает |
| Поиск файлов в SharePoint и OneDrive или в пределах одного сайта или библиотеки. Поддерживает термины KQL ( |
| Находит сайты или перечисляет библиотеки документов сайта и их идентификаторы дисков. Отправная точка для работы с SharePoint. |
| Перечисляет папку в OneDrive или библиотеке документов — по drive + item id, по пути или корень вашего OneDrive. |
| Сведения об одном файле или папке — в том числе из вставленного URL общего доступа, который он распознаёт. Опционально сообщает, у кого есть доступ. |
| Скачивает файл как предварительно подписанный URL, опционально конвертируя в PDF на выходе. Никогда не inline. |
W | Предоставляет доступ по ссылке или приглашая людей. Сообщает о фактически предоставленном доступе, поскольку политика общего доступа клиента может незаметно понизить запрошенный уровень. |
Основное
Инструмент | Что делает |
| Кто вы под учётной записью, активна ли связь с Microsoft и когда она обновлялась в последний раз, какие разрешения предоставлены, какие инструменты у вас есть, какие общие почтовые ящики вы одобрили и можете использовать прямо сейчас, и для каждого из них — будет ли вызов выполняться от вашего имени или от имени сервисной учётной записи. Вызывайте это первым при сбоях. |
Конфигурация
Всё настраивается только через переменные окружения. Авторитетный список — тип Env в src/config.ts.
Регистрация приложения Entra
Var | Required | Description |
| да | GUID клиента или проверенный домен. Каждый вызов направляется в этот конкретный клиент — |
| да | Идентификатор приложения (клиента). Регистрация должна использовать тип платформы Web. |
| один из | Секрет клиента. Самый простой вариант, но Entra ограничивает его срок действия 24 месяцами. |
| один из | Закрытый ключ PEM в формате PKCS#8 (буквально или в base64) для аутентификации клиента по сертификату. Предпочтительно в производственной среде. |
| с сертификатом | SHA-1 отпечаток в шестнадцатеричном виде, как показано на портале. Entra сопоставляет утверждение с сертификатом по отпечатку, поэтому требуются обе части. |
| Идентификаторы клиентов через запятую, принимаемые при работе в мультитенантном режиме. Проверяется при входе и повторно при каждом запросе, поэтому удаление клиента отсюда немедленно блокирует его существующие подключения, а не при следующем входе. Пустое значение с |
Ключи
Var | Required | Description |
| да | Закрытый ключ PKCS#8 PEM в base64 для подписи RS256 наших токенов доступа MCP. |
| да | Открытый ключ SPKI PEM в base64. Публикуется по адресу |
| Идентификатор ключа в JWKS и заголовках токенов. Меняйте вместе с ротацией пары ключей. По умолчанию | |
| да |
|
| Выведенные из эксплуатации ключи через запятую в том же формате, принимаются только для расшифровки. Именно это превращает ротацию в плавную операцию, а не в «день переключения». |
Хранилище
Var | Required | Description |
| да¹ | Таблица DynamoDB для запечатанных refresh-токенов (без TTL) и кэшированных токенов доступа (с TTL). Намеренно отделена от таблицы OAuth, чтобы учётные данные имели собственную границу IAM и политику резервного копирования. |
| Запасной вариант на JSON-файле для самостоятельного хостинга и разработки. Игнорируется, если задан | |
| да¹ | Таблица DynamoDB для собственного состояния OAuth — зарегистрированных клиентов, состояний входа, кодов авторизации, refresh-токенов. TTL по |
| Запасной вариант на JSON-файле для того же состояния, чтобы | |
| Таблица DynamoDB для пользователей и политик, с GSI | |
| JSON-хранилище пользователей для самостоятельного хостинга и разработки. Игнорируется, если задан | |
| Устаревший единый bearer-токен администратора, обходящий хранилище пользователей. Полезен для смоук-тестов. У него нет собственного подключения к Graph, поэтому инструменты Graph возвращают ошибку переподключения, если он не сопоставлен с пользователем, выполнившим вход. |
¹ или соответствующий вариант _FILE (MCP_GRAPH_FILE / MCP_OAUTH_FILE) для локальной разработки.
Поведение Graph
Var | По умолчанию | Description |
|
|
|
| производное | Полная замена списка областей через пробел. Проверяется при загрузке: |
|
| Меняйте только для суверенных облаков, где некоторые используемые здесь возможности отсутствуют. |
|
| Отправляет |
|
|
|
|
| Таймаут на запрос, выдерживается значительно ниже 300-секундного таймаута инструмента клиента. |
|
| Ограничение одновременных запросов на (приложение, почтовый ящик). Exchange допускает ровно четыре; значение ограничивается этим числом, потому что его повышение лишь превращает пропускную способность в ошибки 429. |
|
| Microsoft понижает приоритет трафика без оформления. Сохраняйте документированную форму и вставляйте название своей компании в среднее поле. |
| auto | География SharePoint ( |
Ограничения и вывод
Var | По умолчанию | Описание |
|
| Главный переключатель режима «только приложение». Пока он равен |
| пусто | Разделённые запятыми адреса почтовых ящиков, доступные с учётными данными приложения. Подстановочный знак отклоняется сразу. |
| пусто | Разделённые запятыми идентификаторы сайтов или URL-адреса, доступные с учётными данными приложения; применяется к каждому вызову, адресованному сайту или диску, совершённому субъектом в режиме «только приложение». Пустой список означает, что режим «только приложение» вообще не достигает SharePoint, а вызов в режиме «только приложение», не указывающий сайт, отклоняется, а не разрешается. Сопоставление нечувствительно к регистру и является либо точным, либо префиксом, заканчивающимся на границе пути |
|
| Блокировка на уровне развёртывания для режима |
|
|
|
| S3-корзина для загрузок. Требуется каждым инструментом загрузки — запасного варианта с base64 нет по замыслу. | |
|
| Время жизни предварительно подписанных URL-адресов. Любой, кто владеет таким URL, может получить файл без аутентификации, поэтому держите его коротким. |
|
| Переопределяет регион для корзины артефактов. |
| Приёмник JSONL для аудита и записей безопасности, в дополнение к stderr. Для самостоятельных развёртываний; на Lambda stderr уже попадает в CloudWatch. | |
| не задано |
|
|
| Порт прослушивания для обычной точки входа Node. Не используется на Lambda и Azure Functions. |
Доступ к общему почтовому ящику
Это функция, вокруг которой построена архитектура, и сама возможность зависит от клиента, а не от этого сервера.
Чтобы Graph вообще это сделал, должны выполняться оба условия. Соединению нужны делегированные области Graph .Shared (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 в общий почтовый ящик нет. Адрес также должен быть одобрен пользователем при подключении; см. Одобрение почтового ящика ниже.
Два ограничения, о которых стоит знать заранее. Не существует API Graph, который перечисляет почтовые ящики, на которые у пользователя есть права — именно поэтому страница одобрения проверяет адрес-кандидат, а не перечисляет их для вас, и почему вызывающий по-прежнему называет почтовый ящик в вызове инструмента. И вошедшему пользователю обычно нужен собственный лицензированный почтовый ящик, хотя сам общий почтовый ящик в лицензии не нуждается.
Дальнейшее сужение. policy.allowedMailboxes — это потолок администратора, поверх того, что одобрил сам пользователь; фактический доступ — это пересечение двух. По умолчанию он равен "*", что передаёт оставшееся решение Exchange, где ему и место. Установите его в явный список, чтобы ограничить пользователя ниже его прав Exchange, или в null, чтобы отклонять аргумент mailbox outright:
npm run user -- add alice --mailboxes=support@contoso.com,billing@contoso.comОдобрение почтового ящика
Адреса вводятся вручную. Microsoft не предоставляет API, который перечисляет почтовые ящики, которые может открыть человек, а вывод этого из того, с кем они переписываются, давал список, который был в основном неверен — поэтому страница не угадывает. Что она делает, так это проверяет: каждый введённый адрес проверяется в Exchange до того, как его можно одобрить, и показывается как доступный или недоступный с указанием причины.
Собственный почтовый ящик вошедшего пользователя — обычная запись в этом списке, и его можно удалить. Ассистент, построенный вокруг общего почтового ящика поддержки, не должен читать личный входящий ящик оператора, поэтому это должно быть выразимо. Если его не указать, каждый инструмент Outlook откажет в вызове, который не называет другой одобренный почтовый ящик; Teams и SharePoint не затрагиваются, потому что ни один из них не проходит через шлюз почтового ящика.
Microsoft не может ограничить это разрешение — поэтому это делает сервер. В Entra нет согласия в разрезе отдельных почтовых ящиков для делегированных Mail.*.Shared. Как только пользователь предоставляет эти области действия (scopes), полученный токен может открыть каждый почтовый ящик, который Exchange позволит открыть этому человеку, и на стороне Microsoft нет способа это сузить — SharePoint в 2024 году получил делегированное Sites.Selected, а у Exchange нет ни аналога, ни записи в плане разработки. Страница одобрения ниже — поэтому не прихоть, а единственное, что ограничивает данное разрешение, и применяется оно на стороне сервера, при каждом запросе, до любого вызова Graph (policyAllowsMailbox в src/users.ts, вызываемый из resolveActor).
Как устроен процесс. /oauth/authorize → вход в Entra → /oauth/callback сохраняет запечатанный refresh-токен и, вместо того чтобы отдать MCP-клиенту его код авторизации, перенаправляет на /oauth/consent с одноразовым билетом (15 минут). Страница показывает собственный почтовый ящик пользователя — он всегда включён, его нельзя убрать — а также потенциальные общие ящики, каждый уже проверен: адрес, который нельзя открыть, показывается серым с причиной, а не принимается заранее и не отваливается потом. Пользователь отмечает, что этот ассистент может использовать, и только после этого выпускается код авторизации и клиент перенаправляется домой. При повторном подключении страница открывается снова с уже отмеченным выбором; так же пользователь позже убирает почтовый ящик.
Чего проверка увидеть не может. У ошибки 403 на «Входящих» есть три причины, и лишь одна из них — «доступа нет вовсе». Пользователь, у которого есть только разрешение Send As (только отправка от имени) или доступ к одной папке, а не к всего ящику, не проходит проверку «Входящих», хотя этого более узкого доступа достаточно для нужной ему операции. Страница говорит об этом там, где отображается почтовый ящик. Честный итог: проверка выполняет скорее занижение, чем завышение — она никогда не даёт ложных срабатываний: адрес, ответивший на проверку 200, действительно открывается сервером.
Два ограничивающих уровня, оба на стороне сервера. grantedMailboxes — что одобрил пользователь; allowedMailboxes — потолок, заданный администратором. Почтовый ящик достижим только тогда, когда он присутствует в обоих списках, и o365_whoami возвращает это пересечение как usableSharedMailboxes, чтобы модель видела только те ящики, которые она действительно может использовать. Собственный почтовый ящик пользователя всегда разрешён и не фигурирует ни в одном из этих списков.
Сервисный аккаунт с API-ключом никогда не увидит этой страницы — у него нет ни браузера, ни человека, у которого можно спросить, — поэтому ограничение согласия на него не распространяется. Это осознанный выбор: стороной, дающей согласие, выступает администратор, создавший ключ, а allowedMailboxes остаётся единственным текущим органом. Граница проводится по тому, есть ли у учётной записи Entra oid, то есть проходила ли она через вход в браузере.
Режим только для приложения
Режим только для приложения (app-only) существует для одного случая: почтовый ящик, в который никто не входит, на который никто не имеет делегированных прав, но который агент всё равно должен разбирать. Используются собственные учётные данные приложения, а не пользователя, поэтому нет вошедшего пользователя, а /me недействительн.
По умолчанию этот режим выключon and должен оставаться выключенным, если в нём реальной необходимости: разрешение приложения Mail.ReadWrite с согласием администратора даёт доступ к каждому почтовому ящику в организации. Для включения нужно три независимых вещи: O365_APP_ONLY_ENABLED=true, почтовый ящик в списке O365_APP_ONLY_MAILBOXES и allowAppOnly в политике вызывающего пользователя. Ящик, который есть в белом списке, но у вызывающего нет allowAppOnly, просто откатывается на делегированный сценарий и позволяет отвечать Exchange. Переход на app-only не обходит два ограничителя почтовых ящиков: resolveActor применяет их до того, как вообще переходит к ветке app-only, поэтому вошедший пользователь всё равно должен одобрить адрес на странице одобрения. Типичный вызывающий за app-only — сервисный аккаунт с API-ключом, его никогда не спрашивают, и для него allowedMailboxes и является полным контролем.
O365_APP_ONLY_MAILBOXES — это только половина контроля, и менее сильная. Он ограничивает то, что этот код попросит у Graph. Сам кредыал? this value он программно не меняет: любой, кто получит доступ к учётным данным, открывает reaches всех ящиков в клиенте. Настоящий контроль — это Exchange RBAC for Applications на стороне клиента. deploy/entra/scope-app-only.ps1 автоматизирует этот процесс: регистрирует простой service principal in Exchange, creates an operational scope decreases group for security, assigns a role Application Mail.Checkout, соз sq restricted, and verifies everything finishing with Test-ServicePrincipalAuthorization.
Ловушка, которая ломает всё: RBAC-гранты действуют аддитивно с грантами Entra. Если на регистрации приложения осталось соглас соответствует неприобластное разрешение для приложения, то действует объединение обоих, и ограничение обесценивается. Разнение приложения в Entra нужно обязательно удалить. Так учтите право на кэш: изменения вступают в силу через 30 минут — 2 hours (Test-ServicePrincipalAuthorization обходит кэш — именно поэтому скрипт заканчивается им).
Teams nowode has no app-only case at all — see below.
Разрешения на инструменты для каждого пользователя
Когда настроено хранилище пользователей (MCP_USERS_TABLE or MCP_USERS_FILE for development), в каждой записи пользователя есть поле policy:
Field | Meaning |
|
|
| Map of |
| Large amount to be for every tool in the mutating category. |
| Which user on approval page at the moment connected to proxy properties, absent — didn't ask; own mailbox only. Written by |
| The administrator's maximum line: |
| Per-user gate for the app-only subject. Default |
| Per-user call limit. Default 60. |
| Disables identity without limits. It doesn't delete the record. |
Tools the user doesn't have are also hidden from any tools/list, so the model can't see them. On every OAuth login, map is aligned with the actual registry: newly included tools appear as false — potentially damaging, so you won't be allowed silently; removed tools are cut out. Storage is written only when there is some change.
A newly provisioned user has all read-only tools enabled and all modifying disarmed.
Admin CLIs
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 alicenpm 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 keyconnection revoke means this server no longer uses the credential. The standard tenant-side switch is Revocation button "Sessions" in the user's object in Entra — remember: changing the password alone doesn't not reset the refresh token of a confidential client (matrix revocation see in SECURITY.md).
Constraints and known issues
На AWS header
WWW-Authenticateis renamed. The Lambda Function URL rewrites it tox-amzn-Remapped-WWW-Authenticate— nothing in a function can prevent it. Discovery doesn't suffer: the MCP spec requires clients to load directly/.well-known/oauth-protected-resource/mcp(and then root) as fallback anyway, and reference SD KON on 401 does it silently — that's why the server returns both andresourceis byte-identical to its MCP endpoint. If you encounter a client that actually needs a header, place CloudFront and add a Lambda@Edge function at level origin-response that copies the remapped name back; level viewer-response won't work, because CloudFront doesn't cause those when origin reserves to 400 or more.
State directly: otherwise most of this will be mistaken for bugs.
Refresh tokens expire in opaque ways. The 90-day window is inactive, not a fixed term: a user who interacts weekly not in practice, while one who has been silent for 91 days returns without a lifespan (AADSTS70008/700082). Independent of this, a Conditional Access frequency sign-in forces reauthentication on its own schedule — any server code can't cancel it. Password reset through the Entra/ Microsoft 365 admin center returns: admin info, not is diff; but a change by the user does not. Every one of these returns as one explicit error with reconnection link.
Secret expiry of the application client is a cliff, not a scale. Entra caps the lifetime of a secret to 24 months and if it's expiry, then now everyone in the deployment will fail with with AADSTS7000222 — not gradual, not one at. Certificate subject can save from this degradation; try to renew key long prior.
Loss cryptographic data unrecoverable. Without a key, no stored connections; each user must sign in again, simultaneously. Backup is separate rotation via O365_TOKEN_ENC_KEYS_PREVIOUS + connection rewrap, the surface cannot replace.
Teams sending is possible only in delegated mode, and it's permanently so. All Graph send endpoints list Teamwork.Migrate.All as the only application scope, and Microsoft notes this for migration scenarios. There's a compliant way to post a service account banner for missing one in this orientation: only in a Bot Framework bot or a team app with resource-specific consent (install for each), and none of those suit an HTML-based standalone MCP server. Unattended texting in Teams is not supposed. Teams payments aren't even included in use-case: model A / model B are stopped from August 25, 2025, despite what most existing RFC says.
(Note: I had "deprecated" examples; let me not mention that.)
As a final form, I need to produce translation with these technical terms. Вот итоговый ответ.
Scopes для каналов Teams требуют согласия администратора тенанта. ChannelMessage.Read.All, в частности, нельзя согласовать самостоятельно. Тот, кто поднял сервер у себя (self-hoster) без прав администратора и запускает O365_SCOPE_PROFILE=personal, получает рабочую почту, файлы и чат; инструменты каналов при этом дают сбой, но с объяснением, а не с загадочным 403. Профиль personal также не включает User.ReadBasic.All, поэтому @-упоминание человека, которого нет в беседе, не может быть сопоставлено с пользователем: сообщение всё равно отправляется, имя остаётся в тексте письма как обычный текст, а инструмент возвращает предупреждение, что адресат не был уведомлён.
Страница подтверждения почтовых ящиков занижает список, но никогда не завышает. Она определяет, можно ли использовать ящик, пытаясь открыть его папку «Входящие», и у 403 здесь три причины. Если у вас только право Send As (отправка от имени) или доступ к одной папке, а не ко всему ящику, адрес показывается как недоступный и отметить его нельзя, хотя этого более узкого доступа было бы достаточно для вашей задачи. Обратная ошибка невозможна: адрес, который можно отметить, сервер действительно может открыть.
У поиска есть жёсткие потолки, которые выглядят как потеря данных. $search в Outlook возвращает не более 1 000 результатов и не сочетается с фильтрами или собственной сортировкой. Поиск Teams возвращает количество страниц, а не общий итог, поэтому его никогда нельзя выдать за количество совпадений. Глубокая страничная выдача SharePoint обрывается после 1 000-го результата, а поиск в режиме только приложения по умолчанию исключает личный контент OneDrive — включение такой возможности разворачивает новый индекс, который может строиться от пары дней до недели, и всё это время результаты молча неполны, без какой-либо ошибки.
Политика общего доступа тенанта молча переписывает результат o365_files_share. Настройки на уровне организации и отдельного сайта могут понизить анонимную ссылку до уровня «только для организации», принудительно задать срок действия или сделать ссылки только на просмотр. Хуже того, createLink идемпотентен для каждой пары (приложение, тип ссылки): запрос свежей семидневной ссылки может вернуть выданную годы назад бессрочную ссылку с другим уровнем доступа. Инструмент всегда считывает результат обратно и возвращает фактически выданный доступ — это единственная защита.
Идентификаторы сообщений меняются при перемещении сообщений, а в Teams они не глобально уникальны. O365_IMMUTABLE_IDS включён по умолчанию именно ради этого, но это по сути односторонняя дверь: идентификаторы, выданные в одном формате, не работают в другом, а переключение флага на живом стенде вызывает ErrorInvalidIdMalformed. Отдельно отметим: идентификатор сообщения Teams уникален только в пределах своего чата или канала, поэтому идентификаторы сообщений всегда возвращаются вместе с координатами беседы.
Троттлинг — самый частый повседневный сбой. Outlook допускает четыре одновременных запроса на каждую пару (приложение, почтовый ящик) и 10 000 запросов за десять минут; Teams — примерно один запрос в секунду на канал, на чат и на пользователя; SharePoint списывает пять единиц ресурсов за каждый вызов для работы с разрешениями и троттлит поиск заметно жёстче, чем остальной Graph. Батчинг не помогает: Graph передаёт в Outlook одновременно не более четырёх подзапросов из пачки. Сервер сам ограничивает собственный веер запросов (fan-out) и в точности соблюдает Retry-After, но настойчивый агент всё равно рано или поздно встретит 429.
Вложения крупнее 3 МБ не работают в общем почтовом ящике. Microsoft документирует: делегированный вызывающий получает 403 при вложении большого файла в сообщение в общем или делегируемом ящике. До 3 МБ всё работает. Инструмент сам объясняет это, а не показывает голый 403.
В суверенных облаках функции незаметно отсутствуют. Безвозвратное удаление, дельта-запросы чатов и экспортные API Teams недоступны в округах US Government L4/L5 и China 21Vianet; к росс-гео достпупа не работает по причинам на права приложений. Мультигео-тенантам нужен отдельный поисковый запрос по каждому региону, иначе контент в других регионах молча пропадает из результатов.
Гонки при ротации refresh-токенов безопасны, но реальны. Entra при каждом обмене выдаёт новый refresh-токен и не отзывает старый, поэтому оба параллельных вызова для одного пользователя получают по валидному новому токену. Условная запись (conditional-write) leads том, что один вызов выигрывает, а проигравший отбрасывает свою копию; потерянная гонка никогда не пагубается вызову инструмента. Именно кэш access-токенов делает такие гонки редкими.
Дорожная карта
Это намеренно урезаная функциональность v1, а не упущения:
Календарь. Второе по популярности после почты. Для
Calendars.ReadWriteвозможно согласие пользователя, акторская модель, уже построенная для общих почтовых ящиков, без изменений переносится на общие календари. Следующий пункт в очереди.Загрузка файлов в OneDrive и SharePoint. В v1 уже покрыты поиск, список, чтение, скачивание и раздача доступов.
Контакты / поиск людей, чтобы находить адрес по имени. Сейчас каждый инструмент передачи считает, что у вызывающего адрес уже есть.
Правила входящих (
messageRules) — нуженMailboxSettings.ReadWrite— для серверной автоматизации сортировки поступающих.
Также рассматриваются: подключаемый приёмник аудита (Firehose → S3 → Athena) помимо stderr; федерация identity рабочей нагрузки (workload identity federation) как третья разновидность клиентских учётных данных в AWS, чтобы больше не существовало долгоживущих секретов, и непрозрачные страница-дескрипторы, выпускаемые сервером, вместо сырых строк @odata.nextLink.
Администрирование каталога сознательно оставлено за кадром: бесплатный Microsoft MCP Server for Enterprise уже закрывает запросы только для чтения к Entra.
Участие в разработке
Смотрите CONTRIBUTING.md. Проблемы безопасности: SECURITY.md — пожалуйста, не открывайте публичный issue.
Лицензия
MIT. Смотрите LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Copilot connector permission audits with owner signoff receipts.
*Updated June 17th 2025** Manage your Microsoft 365 services effortlessly. Create and manage distr…
Governed email for AI agents (Mailbuttons / mbag.ai): sandbox inboxes, policy gate, audit log.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/LotzerDigital/aws-office365mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server