Skip to main content
Glama
deploy103

AfterHours Outlook MCP

by deploy103

AfterHours Outlook MCP

Single-owner, authenticated Streamable HTTP MCP for read-only inbox previews and basic calendar data through Microsoft Graph. Use your own Entra application and delegated consent, independently of browser logins or other MCP servers. There is no scheduler/background monitoring, message sending/replies/deletion, read-state change, calendar mutation, full mail body or attachment download.

Fresh clone and disconnected startup

Linux/POSIX, Python 3.11+ and a private local account are required. This public repository can be cloned without GitHub authentication; Outlook still requires your own delegated consent.

git clone https://github.com/deploy103/afterhours-outlook-mcp-public.git
cd afterhours-outlook-mcp-public
python3.11 -m venv .venv
.venv/bin/python -m pip install --require-hashes --only-binary=:all: -r requirements.lock.txt
.venv/bin/python -m pip check
mkdir -p "$HOME/.config"
.venv/bin/python configure.py --directory "$HOME/.config/afterhours-outlook-mcp" \
  --public-origin https://mcp.example.com
"$HOME/.config/afterhours-outlook-mcp/run.sh"

Replace the example origin with your own HTTPS hostname before exposing the service. The option defaults to https://mcp.example.com; only HTTPS origins on 443 (implicit or explicit) are accepted, with no trailing slash/path, userinfo, query or fragment. The destination must be new, outside the checkout, with an existing parent and no symlink components. ~ and relative paths become absolute paths. There is no overwrite option. Partial failures leave the new private directory for inspection, never recursively delete it. Choose a new directory to regenerate; retire the old configuration deliberately.

The stdlib bootstrap creates secrets without printing them:

~/.config/afterhours-outlook-mcp/            0700
  config.env                              0600 (exported paths, origin, zero UUIDs)
  run.sh                                  0700
  credentials/                            0700
    mcp-token                             0600 (random MCP bearer)
    cache-key                             0600 (random Fernet encryption key)
  state/                                  0700 (encrypted OAuth cache after consent)

Keep this directory out of Git, logs and chat. The three all-zero UUID settings permit startup with no configured owner or consent. outlook_status then returns not_connected without Microsoft I/O. Health GET /healthz proves only process liveness. run.sh sources config.env, changes to the absolute checkout path, and executes its .venv/bin/python -m uvicorn server:app --factory on 127.0.0.1:8004, one worker, no access logs and no proxy headers. All paths are shell-quoted. Moving the checkout/venv requires a new configuration or a deliberate launcher update.

Related MCP server: outlook-mac-mcp

Client and HTTPS proxy

Internal MCP path is /mcp. Your own TLS reverse proxy must pass Authorization and preserve the configured external Host, stripping any prefix (for example /outlook/mcp → /mcp). Set OUTLOOK_PUBLIC_ORIGIN in the private config.env to match your HTTPS hostname. Host/Origin are exact allowlists, not wildcard CORS. For local MCP checks use Host: mcp.example.com (or your configured host), even though the socket address is 127.0.0.1:8004. No proxy/firewall/service is installed. Keep the proxy on this host or design a separate authenticated TLS/VPN transport; never expose a bearer over untrusted HTTP. Suppress sensitive proxy access/debug logs.

Register https://YOUR_HOST/outlook/mcp (or your chosen proxy path) as a Streamable HTTP server in your client. The header is Authorization: Bearer <MCP token>, not a Microsoft token. The token file is $HOME/.config/afterhours-outlook-mcp/credentials/mcp-token; use the client's protected file/secret input if available. The file path is not the bearer value. Do not put the value in command arguments/history or chat. A bearer holder can read this owner's data; this is not multi-user OAuth. Replace the private token file and restart to rotate it.

Register your own Entra app and authorize with PKCE

  1. In your own organizational tenant, create an Entra app registration: Single tenant, Mobile and desktop applications / public client. Register redirect URI http://localhost:8400 (Microsoft also supports the localhost port exception when http://localhost is registered). Do not use a Web/SPA platform, client secret, or another application's client ID. Check the tenant's public client flow setting/policy.

  2. Add Microsoft Graph delegated Mail.Read and Calendars.ReadBasic only. No Mail.Send, ReadWrite or Application permissions. MSAL additionally requests openid profile offline_access for identity and refresh tokens.

  3. Stop the launcher. Edit the generated private config.env, replacing:

    • OUTLOOK_TENANT_ID: your Directory (tenant) UUID.

    • OUTLOOK_CLIENT_ID: your Application (client) UUID.

    • OUTLOOK_OWNER_OBJECT_ID: your user Object ID (oid) in that tenant. This is not an email address. A wrong tenant/owner fails closed.

    • Optional export OUTLOOK_LOGIN_HINT=... is only a login hint, not an authorization check; quote shell values properly. Keep CREDENTIALS_DIRECTORY pointing to the generated credentials/ and OUTLOOK_STATE_DIRECTORY to its state/. Do not generate another cache-key for an existing encrypted cache. Keep file mode 0600.

  4. If the browser is on another computer, first open an authenticated tunnel on the browser's computer:

    ssh -N -L 8400:127.0.0.1:8400 YOUR_SSH_USER@YOUR_OUTLOOK_HOST

    Then, in the checkout on the server, run as the same UID that owns the generated configuration and will run the launcher:

    umask 077
    . "$HOME/.config/afterhours-outlook-mcp/config.env"
    .venv/bin/python oauth.py connect

    config.env exports all needed environment variables including CREDENTIALS_DIRECTORY (where cache-key is read) and OUTLOOK_STATE_DIRECTORY (where tokens.cache/cache.lock are written). No systemd credential mount is needed for this fresh-clone CLI workflow.

  5. Open the printed Microsoft authorize URL in your own browser, sign in as the exact configured owner and consent. This URL is transient authorization material: do not share/log it. PKCE S256, state and nonce are checked by MSAL; the CLI additionally checks tenant, owner, issuer, audience and time claims. The loopback callback accepts form POST only on 127.0.0.1:8400 and times out after five minutes. No public callback route is exposed. A received callback is not proof of success: check the terminal's final result. Failed/wrong-owner consent does not replace a working cache.

  6. Restart run.sh, then call outlook_status from your authenticated client. inbox_access_verified means a real inbox ID read succeeded; it does not verify calendar permission. Call outlook_calendar separately to check that.

Tenant app-registration rules, user-consent restrictions, admin consent, MFA and Conditional Access may block this flow. Obtain the tenant administrator's approval; do not bypass policy or extract browser sessions. Personal Microsoft accounts/multi-tenant login are not supported by this owner/tenant contract.

The OAuth cache is Fernet-encrypted in state/tokens.cache (0600), locked across processes and atomically replaced; the separate credentials/cache-key must be preserved. No mail/calendar data is stored. MSAL refreshes on demand and persists refresh-token rotation. Expiry/revocation/policy can require interactive CLI reconnection; no permanent-login guarantee or automatic Graph retry is provided. Fernet is not protection against compromise of the same OS account holding both cache and key.

To disconnect locally, stop the launcher, source the same environment and run:

. "$HOME/.config/afterhours-outlook-mcp/config.env"
.venv/bin/python oauth.py disconnect

This removes local tokens only; revoke the app grant in Microsoft/Entra to revoke remote access. An in-flight read may already hold a token. Restart disconnected if desired.

Tools and deployment boundaries

  • outlook_status: disconnected on an empty cache; otherwise verifies inbox access only. Credential/provider errors are tool errors, not connected status.

  • outlook_inbox(limit=20): latest 1–50 INBOX previews, newest first, original message IDs/source/links/received timestamps. No full bodies or attachments.

  • outlook_calendar(start,end,limit=20): 1–50 occurrences from the default calendar, explicit timezone-aware timestamps with seconds, max 31 elapsed days. Preserves UTC and original timezone facts.

Reads use fixed Graph /me HTTPS GET endpoints, no arbitrary URL/OData inputs, TLS validation, no ambient proxies/redirects, 15-second timeout, 1 MiB response limit. Only one page is fetched; has_more is not a complete scan. Returned mail text and links are untrusted source data, never instructions.

deploy/ contains optional systemd/nginx/env templates, not an installation. Review identity/paths/private state/Host and credential mounts before using them; do not overwrite existing services. General credential/cache files are 0400/0600. The code also recognizes its tightly bounded root-owned read-only systemd /run/credentials/<unit> contract (0440 files / 0550 directory), not arbitrary group-readable files. No shared systemd, production or Vooy configuration is changed by the bootstrap. Dependencies retain their upstream licenses/notices; this project's license is MIT.

Offline checks and limits

.venv/bin/python -B -m unittest -v
.venv/bin/python -m pip check

Tests use disposable keys/cache and fake provider I/O; bootstrap tests exercise the CLI, modes, no-overwrite, origin validation, and the generated launcher's real loopback MCP not_connected status. They do not contact Microsoft. Actual Entra registration/consent, tenant policy, account access and public TLS/proxy deployment require owner validation. Hash-locked dependencies are in requirements.lock.txt.

Official references:

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables Claude.ai and other MCP clients to access a personal Outlook.com account's mail and calendar through Microsoft Graph after a standard OAuth 2.0 sign-in, with no stored tokens or pre-generated credentials. Supports searching, reading, sending, drafting, organizing, and moving messages plus read-only calendar browsing, with optional read-only aliases that restrict an account to view-only scopes.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables headless reading of Microsoft 365 mail, calendar, Teams chats and channels, OneDrive, SharePoint and OneNote over Microsoft Graph using a single password-plus-TOTP sign-in. No app registration or admin consent is needed, since it brokers pre-consented first-party clients from one FOCI refresh token.
    1
    MIT