feat: official Microsoft 365 / Graph tool suite (MCP-independent, app-registration based) #862

Open
opened 2026-07-20 17:13:54 +00:00 by jason.woltje · 0 comments
Owner

Summary

Add an official Microsoft 365 / Graph tool suite (tools/m365/) plus
load_credentials microsoft365 support, so agents can do mail/calendar work via a
wrapper-based direct-Graph path instead of depending on the claude.ai M365 MCP connector.

Motivation

The claude.ai Microsoft 365 MCP connector is unreliable in practice: sessions expire
mid-task, the whole server can disconnect, and /mcp reconnect has been observed returning
Server not found. When that happens, every mcp__…Microsoft_365__* tool vanishes and any
in-flight inbox/calendar work is dead with no fallback.

A direct Microsoft Graph path using an app registration has been proven to survive these
outages and do real work (mail scan + staging a draft reply) with no MCP involved. Today that
requires hand-rolled curl/scripts. It should be a first-class, supported suite like
tools/glpi/, tools/authentik/, etc.

Proposal

1. Credential support

Extend load_credentials to support microsoft365, mirroring the existing per-instance
pattern (e.g. gitea-<instance>):

microsoft365.<instance>.tenant_id
microsoft365.<instance>.client_id
microsoft365.<instance>.client_secret

load_credentials microsoft365 [-a <instance>] exports the values; the loader's supported-service
list and TOOLS.md should be updated to include it. (Currently microsoft365 is absent from the
loader even when present in credentials.json.)

2. Auth helper

Client-credentials flow to an app-only token:

POST https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token
  grant_type=client_credentials
  client_id / client_secret
  scope=https://graph.microsoft.com/.default

Base URL: https://graph.microsoft.com/v1.0/users/{upn}.

3. Wrapper commands (initial)

Script Purpose
mail-list.sh list messages (date window, folder, sender/subject filters)
mail-read.sh fetch one message body
draft-reply.sh stage a threaded draft reply (createReply → PATCH body); does NOT send
calendar-list.sh list events in a window

Safety default: no send. Drafts are staged in the mailbox for a human to send. Any
mail-send capability must be behind an explicit flag AND require Mail.Send be granted to the
app registration (which may not be). Default posture is read + stage-draft only.

Implementation notes (Graph gotchas already hit)

  • $filter on from/emailAddress/address combined with $orderby returns
    InefficientFilter (400) — sort client-side, or pull a receivedDateTime ge … window and
    match sender/subject in code.
  • $filter on from can return empty even when the message exists; a date-window pull is more
    reliable.
  • URL-encode spaces in query params (receivedDateTime desc) — some HTTP clients reject the raw
    URL outright.
  • id is returned on every message even under $select.
  • Threaded draft pattern that works: POST /messages/{id}/createReply (no body) → PATCH /messages/{draftId} with {body:{contentType:HTML, content: <followup> + <existing quoted>}}
    to prepend the new text while preserving the quoted thread.

Security

  • Secrets come from credentials.json; never echo or commit values (same rule as all suites).
  • Read + stage-draft only by default; sending gated behind explicit opt-in + verified permission.

Acceptance

  • load_credentials microsoft365 works and is documented in TOOLS.md / the loader list.
  • tools/m365/ provides at least mail-list / mail-read / draft-reply, wrappers self-relaunch/
    auth consistently, and draft-reply stages (never sends) by default.
  • A smoke test against a test tenant covers token acquisition + one read + one draft.
## Summary Add an official **Microsoft 365 / Graph tool suite** (`tools/m365/`) plus `load_credentials microsoft365` support, so agents can do mail/calendar work via a **wrapper-based direct-Graph path** instead of depending on the claude.ai M365 MCP connector. ## Motivation The claude.ai Microsoft 365 MCP connector is unreliable in practice: sessions expire mid-task, the whole server can disconnect, and `/mcp` reconnect has been observed returning `Server not found`. When that happens, every `mcp__…Microsoft_365__*` tool vanishes and any in-flight inbox/calendar work is dead with no fallback. A direct **Microsoft Graph** path using an app registration has been proven to survive these outages and do real work (mail scan + staging a draft reply) with no MCP involved. Today that requires hand-rolled `curl`/scripts. It should be a first-class, supported suite like `tools/glpi/`, `tools/authentik/`, etc. ## Proposal ### 1. Credential support Extend `load_credentials` to support `microsoft365`, mirroring the existing per-instance pattern (e.g. `gitea-<instance>`): ``` microsoft365.<instance>.tenant_id microsoft365.<instance>.client_id microsoft365.<instance>.client_secret ``` `load_credentials microsoft365 [-a <instance>]` exports the values; the loader's supported-service list and `TOOLS.md` should be updated to include it. (Currently `microsoft365` is absent from the loader even when present in `credentials.json`.) ### 2. Auth helper Client-credentials flow to an app-only token: ``` POST https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token grant_type=client_credentials client_id / client_secret scope=https://graph.microsoft.com/.default ``` Base URL: `https://graph.microsoft.com/v1.0/users/{upn}`. ### 3. Wrapper commands (initial) | Script | Purpose | |---|---| | `mail-list.sh` | list messages (date window, folder, sender/subject filters) | | `mail-read.sh` | fetch one message body | | `draft-reply.sh` | **stage** a threaded draft reply (createReply → PATCH body); does NOT send | | `calendar-list.sh` | list events in a window | **Safety default: no send.** Drafts are staged in the mailbox for a human to send. Any `mail-send` capability must be behind an explicit flag AND require `Mail.Send` be granted to the app registration (which may not be). Default posture is read + stage-draft only. ## Implementation notes (Graph gotchas already hit) - `$filter` on `from/emailAddress/address` combined with `$orderby` returns `InefficientFilter` (400) — sort client-side, or pull a `receivedDateTime ge …` window and match sender/subject in code. - `$filter` on `from` can return empty even when the message exists; a date-window pull is more reliable. - URL-encode spaces in query params (`receivedDateTime desc`) — some HTTP clients reject the raw URL outright. - `id` is returned on every message even under `$select`. - Threaded draft pattern that works: `POST /messages/{id}/createReply` (no body) → `PATCH /messages/{draftId}` with `{body:{contentType:HTML, content: <followup> + <existing quoted>}}` to prepend the new text while preserving the quoted thread. ## Security - Secrets come from `credentials.json`; never echo or commit values (same rule as all suites). - Read + stage-draft only by default; sending gated behind explicit opt-in + verified permission. ## Acceptance - `load_credentials microsoft365` works and is documented in `TOOLS.md` / the loader list. - `tools/m365/` provides at least mail-list / mail-read / draft-reply, wrappers self-relaunch/ auth consistently, and draft-reply stages (never sends) by default. - A smoke test against a test tenant covers token acquisition + one read + one draft.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#862