Lessons from adding OAuth IMAP access and read-only Outlook tools to the misc MCP server on 2026-10-04. Operational documentation remains at misc-server/outlook-mail.
Microsoft's device-flow consent screen did not visibly present a separate ‘Allow IMAP’ choice. That UI was not reliable evidence of the effective grant. The dependable checks were: request https://outlook.office.com/IMAP.AccessAsUser.All explicitly, inspect the token endpoint's returned scope metadata, and successfully authenticate to outlook.office365.com:993 with XOAUTH2.
An existing POP refresh token could reach Microsoft's token endpoint but could not be exchanged for an IMAP token; Microsoft returned AADSTS70000 unauthorized-or-expired scope. A fresh interactive grant requesting POP and IMAP together was required.
Successful authorization on pomelo updated only pomelo's ~/.netrc. The misc MCP service runs as john on gravlax and retained its older POP-only refresh token, so locally successful IMAP code still failed after deployment. Deployment verification must include the service host's identity, home directory, credential source, and live token exchange.
The corrected token was transferred over SSH stdin and only the outlook.office365.com password field was replaced atomically. Tokens should not appear in command arguments, logs, notes, or tool output. Preserve mode 0600.
The development host could not reach Microsoft directly from the managed execution environment. Its system stunnel exposes SOCKS5 on 127.0.0.1:1080 via kelp, and both OAuth HTTPS and IMAP TLS had to use that path. gravlax has direct connectivity and did not have PySocks installed.
Importing PySocks unconditionally therefore broke an otherwise valid direct deployment. Optional network paths need optional imports: require PySocks only when a socks5/socks5h URL is actually configured, and give a specific error then.
Raw IMAP tags and changing sequence numbers are poor tool interfaces. The useful abstraction is search → {folder, UID} → read. Folder UIDs are stable within one folder and UIDVALIDITY generation; sequence numbers are not. Always return the folder with the UID.
Read-only behavior requires both EXAMINE/read-only selection and BODY.PEEK fetches. The MCP surface intentionally omits move, delete, append, flag, and send operations. Folder names containing spaces must be quoted before imaplib sends them; an all-folder live test exposed this edge case.
Mock tests caught search quoting, MIME decoding, attachment metadata, date criteria, folder quoting, and logout behavior. Live CLI tests caught Outlook-specific behavior. Direct plugin invocation on gravlax caught missing dependencies and stale credentials. The final Streamable HTTP MCP test caught the complete integration path: bearer authentication, initialization, tool listing, description text, status, and search.
A restart being active is necessary but insufficient. Verify at least one real tool call through the running MCP transport after restart.
The plugin loader intentionally discovers files ending in _tools.py, but test_mcp_tools.py also matched that suffix. On gravlax it attempted to import pytest, which was absent, creating noisy warnings. Discovery now excludes test_*.py, with regression coverage. Filename conventions need negative filters as well as positive suffix filters.
Each MCP description is deliberately short but names the canonical note key misc-server/outlook-mail. Detailed parameters, examples, safety semantics, renewal, and troubleshooting live in notes. A test asserts that all four descriptions retain the notes key, preventing documentation discoverability from silently regressing.