Concepts

Core concepts for outlook-local-mcp. Use these as background when the Quick Start or Troubleshooting guide is not enough.

Output tiers

All read tools accept an output parameter with three modes. Write tools return concise text confirmations unconditionally.

text (default) returns pre-formatted plain text optimised for LLM context consumption. Collections render as numbered lists with human-readable fields and a total count. A 12,000-token raw Graph API event becomes roughly 150-800 tokens in text mode — a 60-70% reduction. The LLM can pass this through without additional formatting.

summary returns compact JSON with a deliberately curated field set per tool. Useful when the LLM needs structured data for programmatic reasoning. Nested objects are flattened: start/end become plain dateTime strings, organizer becomes a name string. Summary mode includes a displayTime field with a pre-formatted human-readable time string.

raw returns the full, unmodified Graph API serialisation including empty values. Use this when you need HTML body content, recurrence patterns, attendee email addresses, or other detailed fields. raw is never the default; it must be requested explicitly.

Invalid values return an error: output must be 'summary', 'raw', or 'text'.

Multi-account model and UPN identity

The server supports managing multiple Microsoft accounts simultaneously. Each account is identified by a human-chosen label (e.g., work, personal). The User Principal Name (UPN, e.g. alice@contoso.com) resolved from Microsoft Graph /me is the canonical identity and is persisted to accounts.json in the upn field.

UPN is available immediately at startup without a Graph API call and is shown in all account surfaces (account.list, system.status, elicitation prompts, write-tool confirmations). Any tool that accepts an account parameter resolves it by label first, then by case-insensitive UPN fallback — so account=alice@contoso.com and account=work both target the same entry.

Account selection logic for calendar and mail verbs:

Each account has its own token cache partition, auth record file, and Graph client instance. The accounts.json file stores only non-secret identity metadata. Tokens and credentials are managed separately by the OS-native token cache. The file is written atomically to prevent corruption on sudden exit.

Auto-default account semantics

A default account is registered automatically at startup using the server's configured credentials (CLIENT_ID, TENANT_ID, AUTH_METHOD). Additional accounts added via account.add are persisted to accounts.json and restored on subsequent startups with silent token acquisition from the per-account cache (see CR-0064).

At startup the server performs a silent token probe (5-second timeout) to pre-authenticate persisted accounts. Accounts with expired tokens are registered as disconnected — they remain visible in account.list and system.status and can be reconnected explicitly via account.login, or will be re-authenticated automatically by the auth middleware on the first tool call that targets them.

The default account cannot be removed via account.remove.

MCP elicitation requirement

Multi-account features (account selection prompts, inline authentication during account.add) use the MCP Elicitation API. The server declares the elicitation capability at startup. MCP clients that support elicitation receive interactive prompts; clients that do not fall back to the default account for account selection and receive authentication feedback as tool result text.

For device_code auth without elicitation, account.add uses a two-call pattern: the first call returns the device code and keeps the authentication goroutine alive in the background; the second call with the same label picks up the completed authentication and registers the account.

Read-only mode

Set OUTLOOK_MCP_READ_ONLY=true to disable all write operations. All write verbs (calendar.create_event, calendar.create_meeting, calendar.update_event, calendar.update_meeting, calendar.delete_event, calendar.cancel_meeting, calendar.respond_event, calendar.reschedule_event, calendar.reschedule_meeting, mail.create_draft, mail.create_reply_draft, mail.create_forward_draft, mail.update_draft, mail.delete_draft) return an error when invoked. Read and search verbs remain fully functional.

OUTLOOK_MCP_READ_ONLY=true ./outlook-local-mcp

Mail gating

Mail access is disabled by default and enabled in two tiers via environment variables:

Variable Value Effect
MAIL_ENABLED false (default) Mail verbs unavailable; no mail OAuth scope requested
MAIL_ENABLED true Enables read-only mail verbs (mail.list_folders, mail.list_messages, mail.search_messages, mail.get_message, mail.get_attachment); requests Mail.Read scope
MAIL_MANAGE_ENABLED true Enables all mail verbs including draft management (implies MAIL_ENABLED); requests Mail.ReadWrite scope

Mail.Send is never requested under any configuration. The model prepares drafts that land in Outlook Drafts for manual review; email is never sent automatically. Enabling mail read for the first time triggers an incremental consent prompt; upgrading to mail manage triggers re-consent.

Tool annotation semantics

The four aggregate tools (calendar, mail, account, system) each publish the five MCP annotations (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint) at tool granularity. Because a single tool hosts many verbs whose individual classifications differ, each aggregate annotation is computed as a conservative fold over the verbs actually registered in the running configuration, not hard-coded:

Every verb declares its own read-only, destructive, idempotent, and open-world classification, and a verb cannot be registered without one. The help verb, present in every domain, is read-only, non-destructive, idempotent, and local.

Because gating changes the registered verb set, the published annotations are configuration-dependent. The same tool reports different hints under different settings:

Tool Configuration readOnlyHint destructiveHint openWorldHint
mail neither MAIL_ENABLED nor MAIL_MANAGE_ENABLED true false true
mail MAIL_MANAGE_ENABLED=true false true true
system auth method is not auth_code true false false
system auth method is auth_code (registers complete_auth) false false true

In the default gated configuration mail therefore advertises readOnlyHint: true because only read verbs are registered, and system advertises openWorldHint: false because every registered verb is local. A client that honours destructiveHint will not prompt for confirmation on read-only mail operations, and a client that honours openWorldHint will correctly treat system as local when complete_auth is absent. readOnlyHint: true is a hint, not the enforcement point; server-side write blocking is governed by read-only mode.

Headless and non-interactive authentication

Authentication is lazy — deferred until the first tool call rather than blocking at startup. Three flows are available, controlled by OUTLOOK_MCP_AUTH_METHOD:

device_code (default for well-known client IDs) — the server obtains a device code from Entra ID and delivers it to the user. If MCP Elicitation is supported, the user sees the code in a prompt; otherwise it appears as tool result text. The tool returns immediately; calling any tool after the user completes sign-in in their browser picks up the cached token automatically. Works in all environments including headless and Docker.

browser (default for custom app registrations) — the system browser opens to the Microsoft login page and the server listens on a localhost port for the OAuth callback. Requires an app registration with http://localhost redirect URI.

auth_code — the system browser opens for OAuth login. The user pastes the redirect URL back via MCP Elicitation or the system.complete_auth verb. Uses PKCE for security. Suitable for headless or remote environments where a localhost port cannot be opened.

On subsequent runs the server acquires tokens silently using the cached refresh token. No browser interaction is needed unless the refresh token expires (typically after 90 days of inactivity) or the token cache is cleared. When a token expires mid-session, the auth middleware detects the failure and re-initiates the configured flow with client-visible prompts.

OAuth scopes used per feature

The server requests scopes incrementally. Expanding mail access after initial consent triggers a re-consent prompt.

Feature OAuth scope
Calendar (always active) Calendars.ReadWrite
MAIL_ENABLED=false (default) (none)
MAIL_ENABLED=true Mail.Read
MAIL_MANAGE_ENABLED=true (implies MAIL_ENABLED) Mail.ReadWrite
Refresh tokens (always) offline_access (added automatically by the identity library)

Mail.Send is never requested under any configuration.

Well-known client IDs

OUTLOOK_MCP_CLIENT_ID accepts friendly names in addition to raw UUIDs. Resolution is case-insensitive.

Friendly name Client ID Application
outlook-desktop (default) d3590ed6-52b3-4102-aeff-aad2292ab01c Outlook desktop
outlook-local-mcp dd5fc5c5-eb9a-4f6f-97bd-1a9fecb277d3 Outlook Local MCP (project app registration)
teams-desktop 1fec8e78-bce4-4aaf-ab1b-5451cc387264 Teams desktop and mobile
teams-web 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 Teams web
m365-web 4765445b-32c6-49b0-83e6-1d93765276ca Microsoft 365 web
m365-desktop 0ec893e0-5785-4de6-99da-4ed124e5296c Microsoft 365 desktop
m365-mobile d3590ed6-52b3-4102-aeff-aad2292ab01c Microsoft 365 mobile
outlook-web bc59ab01-8403-45c6-8796-ac3ef710b3e3 Outlook web
outlook-mobile 27922004-5251-4030-b22d-91ecd9a37ea4 Outlook mobile

The default outlook-desktop client ID is pre-authorised for Graph Calendar scopes in all tenants — no admin consent required. If the value is not a recognised friendly name and does not look like a UUID, a warning is logged and the value is used as-is.

In-server documentation surface

The server embeds its own documentation and exposes it through three verbs on the system domain:

{tool: "system", args: {operation: "list_docs"}}
{tool: "system", args: {operation: "search_docs", query: "token refresh"}}
{tool: "system", args: {operation: "get_docs", slug: "troubleshooting", section: "keychain-locked"}}

Each document is also exposed as an MCP resource at doc://outlook-local-mcp/{slug} for clients that support resources/list and resources/read. The server status response (system.status) includes a docs section with the base URI and the troubleshooting slug so an LLM client can locate the documentation surface without prior knowledge.

The embedded bundle contains exactly four slugs: readme, quickstart, concepts (this file), and troubleshooting.

When troubleshooting or filing an issue, call system.about first to capture the build identity and host environment in a single snapshot (see Before you file an issue):

{tool: "system", args: {operation: "about", output: "summary"}}

Container runtime

The server ships as an OCI container image at ghcr.io/desek/outlook-local-mcp. The image uses stdio transport so the container integrates with MCP clients the same way the native binary does.

Supported

Running inside a container supports the full feature set over stdio:

Image variants

Tag Base Size User Use case
:latest / :vX.Y.Z scratch ~15 MB root (UID 0) Default, smallest attack surface, ca-certs only
:distroless / :vX.Y.Z-distroless gcr.io/distroless/static-debian12:nonroot ~17 MB nonroot (UID 65532) Non-root enforcement (Kubernetes PSA restricted, OpenShift, hardened CI)
:debug / :vX.Y.Z-debug distroless :debug ~20 MB nonroot (UID 65532) Incident response only, includes busybox shell; not for production

The default :latest tag points at the scratch image. The scratch variant runs as root because there is no alternative inside a FROM scratch image. Users who require a non-root UID should use :distroless.

Limitations

The OS keychain (Apple Keychain, Windows Credential Manager, libsecret) is not reachable from a Linux container. When the server starts inside a container, it automatically falls back to file-backed token storage and logs a warning at the warn level. Token files land at /data/auth/ inside the container.

Tokens at rest are protected only by filesystem permissions on the host volume, not by the OS keychain. This is a weaker security posture than the native binary on a desktop OS. Users who require keychain-grade protection at rest should use the native binary (go install or Homebrew) rather than the container. See Container has no keychain access in the troubleshooting guide.

Deferred

HTTP transport (SSE / streamable-HTTP) is not implemented in this release. The container ships stdio only. HTTP transport requires transport selection, port exposure, multi-client token isolation, and TLS/auth at the transport layer, all deferred to a future CR.

Observability at a glance

Structured logging — written to stderr. Configure with:

Audit logging — when OUTLOOK_MCP_AUDIT_LOG_ENABLED=true (default), every tool invocation emits a structured JSON audit entry with the tool name, operation, and outcome. Set OUTLOOK_MCP_AUDIT_LOG_PATH to write entries to a file instead of stderr.

OpenTelemetry — optional OTLP gRPC export for metrics and traces:

OUTLOOK_MCP_OTEL_ENABLED=true \
OUTLOOK_MCP_OTEL_ENDPOINT=localhost:4317 \
./outlook-local-mcp

Metrics include per-tool invocation counts and durations. Traces create a span per tool invocation with tool name, parameters, and outcome attributes. Deep-dive OTel attribute lists and the full middleware chain are documented in docs/reference/observability.md.