Skip to content

Safety

Written against the MCP specification 2026-07-28 (tools, authorization and the security best practices page) and Microsoft’s guidance for Entra-protected MCP servers. Each control names the test that pins it.

A change reaches Exchange Online only as the replay of a preview:

  1. exonaut_preview_change runs the route with ?dryRun=true. The API injects -WhatIf, so the cmdlet changes nothing; cmdlets whose -WhatIf is a no-op answer 422 DryRunUnsupported before they run, and the tool reports that the change cannot go through MCP.
  2. On a successful preview the server mints a confirmation token: an opaque random handle (32 bytes), stored server-side with the SHA-256 of method, path, canonical body and the caller’s object id, valid for 20 minutes, usable once.
  3. exonaut_apply_change consumes the token and replays exactly the stored request. A token that is unknown, expired or already used answers PreviewRequired; a token of another caller, or a method, path or body that differs from what was previewed, answers TokenMismatch and discards the token. Nothing in the request can stand in for the token; a client-supplied “confirm” flag is never trusted.
  4. Every apply writes one log line naming the method, path, caller (oid), the first eight characters of the confirmation token and the route’s status; the same values are also log scopes. A completed write whose answer exceeds the result budget is still reported as a success (resultTruncated).

Pinned by McpWriteGateTests (apply without a token, with a used token, with another caller’s token, with a differing body, on a host without writes, and the Enterprise RestOnly case).

  • Plan gate: a host whose licence lacks the mcp feature (Community) answers PlanRequired for every tool except exonaut_status. McpPlanGateTests.
  • Write gate: exonaut_apply_change answers WritesDisabled unless the operator set Mcp:AllowWrites=true. McpWriteGateTests.
  • Tier gate: routes whose policy maps to Enterprise answer RestOnly; the MCP server applies Pro-tier writes only. McpWriteGateTests.
  • Route gate: a change is only sent to a route the documentation manifest knows; an unmatched path answers UnknownRoute instead of being tried. McpWriteGateTests.
  • The endpoint requires an authenticated principal of the API’s audience; the REST route a tool calls enforces its own scope or role, so a read-only token can never write. McpReadToolTests (a Mailbox.Read token reads mailboxes and gets 403 on groups).
  • Audience: the API accepts tokens issued for its client id, api://<client-id>, the configured AzureAd:Audience and the canonical MCP URL (RFC 8707). A token minted for another audience answers 401 with the metadata pointer. McpAuthTests.
  • The loopback is not token passthrough. The specification forbids forwarding a client’s token to a downstream API it was not issued for. The loopback calls the same resource on the same host with a token issued for that resource, which the host validated before the tool ran, and the REST route validates it again. No third-party API sees the token.
  • Scope minimization: the metadata advertises the sixteen read scopes by default, so a first consent is read-only and a stolen token cannot write; Mcp:ScopesSupported=All advertises the writes. A write that lacks its scope answers the API’s 403 naming the scope to add. McpAuthTests.
  • The challenge routing changes nothing for REST: only /mcp answers with the resource_metadata pointer. McpAuthTests.
  • Inputs: paths are validated against the OData key alphabet before any request is made (no query syntax, fragments, parent segments or percent-encoded separators such as %2e and %2f, which the server would decode before routing); top is clamped; methods are an allowlist of three. The loopback client refuses, before sending, any target that does not resolve under /exchange/v1.0/, /readyz or /explorer/examples/, so no tool can reach the diagnostics, explorer or MCP endpoints through it. McpReadToolTests, McpWriteGateTests, McpHardeningTests.
  • Results: every result stays under 20 KB. Null-valued fields are dropped, select trims, query pages and says truncated whenever either its own probe or the route’s page size cut the list (links are always relative, never the loopback origin), describe keeps the head of a wide route list, and a filter replaces anything still larger with ResultTooLarge. McpReadToolTests.
  • Rate limit: /mcp sits behind the same per-principal token bucket as the REST routes, and each loopback call counts against the same principal.
  • Tool surface: exactly eight tools in a fixed order, every description under 2,048 characters, server instructions under 1,500. McpToolCensusTests.

The specification places the human in the loop on the client: it shows the tool inputs, asks for confirmation on sensitive operations and may refuse. The server’s annotations make that possible: every tool but exonaut_apply_change is read-only and idempotent; exonaut_apply_change is destructive and not idempotent.

  • Step-up authorization (a WWW-Authenticate scope challenge from inside a tool result) is not implemented; a missing scope surfaces as the API’s 403 in the tool result with the scope named.
  • Observability is the host’s console logging; OpenTelemetry is on the product roadmap.
  • The SDK advertises the protocol’s logging capability in the handshake, but the server never sends log notifications; logging/setLevel is accepted and has no effect.