Outlook
An MCP server for personal Outlook — and a worked example of how to wrap any REST API as an AI tool, safely.
I ask Claude what I have on the calendar before lunch, and it answers. I ask it to draft a reply to the vendor email that came in this morning, and a draft is waiting in my Outlook folder when I check. I never give Claude my password. I never let it click “send.”
outlook-mcp is the small Node.js server that makes those two things possible — and a worked example of something more general: how to take a REST API you don’t own and turn it into a tool an AI can use, without handing the AI the keys to your life.
Wrapping an API as an MCP tool is a pattern that shows up everywhere — email, calendars, CRMs, project trackers. Building one end-to-end, including the OAuth flow and the safety boundaries, teaches you what to look for (and what shortcuts to distrust) when evaluating enterprise integration platforms that promise the same thing out of the box.
This walkthrough follows one request — “What’s on my calendar before 2pm?” — from the moment I type it, through the MCP handshake, through OAuth, through Microsoft’s Graph API, and back. Then it shows the design decisions that make the server safe enough to live in my actual inbox: drafts that never send, calendar events that always start tentative, and a webLink on every response so I can confirm what the AI did with my own eyes.
TL;DR
- How to wrap any REST API as an MCP tool — including the OAuth handshake that lets an AI call the API without ever seeing your password.
- Why the server exposes only seven tools and physically cannot send email or delete anything — the safety boundaries are in code, not prompts.
- How the same Node.js server runs locally over stdio (zero auth) and remotely over HTTP (JWT + Lambda authorizer) with no changes to business logic.
- The “webLink on every response” pattern — how to build verifiability into every AI write operation so a human can confirm what actually happened.
The problem: your email and calendar are locked behind an API
My email and calendar live in Outlook. Claude has no way to reach either by default. Microsoft exposes both through the Graph API — the door any application walks through to read or change anything in a Microsoft 365 account. But “Microsoft exposes an API” and “Claude can use it” are not the same sentence.
Bridging them requires three things:
- Identity — Microsoft needs to know which account is being read and that the caller is authorized.
- A translator — Graph speaks REST and JSON; Claude speaks MCP, the protocol AI tools use to call external services.
- Guardrails — handing an AI read-write access to your inbox without restrictions is a bad idea even when the AI is good.
outlook-mcp is all three at once. It holds the credentials, speaks both protocols, and ships with safety defaults that make destructive operations physically impossible to trigger.
The building blocks
The request crosses several layers before it comes back as an answer. Here’s the cast before tracing the flow.
| Component | Role | Why this one |
|---|---|---|
| Node.js 20 | Server runtime | Natural fit for REST-heavy code; same process in both stdio and HTTP modes |
| Microsoft Graph | API target | The single REST surface for all of Microsoft 365 — mail, calendar, contacts, Teams — under one base URL |
| OAuth 2.0 PKCE | Identity | Standard way to prove you’re allowed to read your own data without sharing your password |
| MCP (Model Context Protocol) | AI interface | Open protocol Claude uses to discover and call external tools by name |
stdio (src/index.js) | Local transport | Zero-ceremony pipe between Claude Desktop/Code and the server; no network, no auth |
HTTP Streamable (src/server.js) | Remote transport | Always-on Railway service reachable from any MCP client |
| AWS Lambda authorizer | Remote auth | Validates JWTs (issued via magic-link email) or falls back to x-api-key; borrowed from niu-library / niu-news |
How a request becomes an answer
Here’s what happens when I ask Claude “what’s on my calendar today?”.
Steps with a blue stripe are AI-powered. Claude picks the tool, fills in the parameters, and reads the response. Everything in between is plumbing — and the plumbing is what this server is.
The MCP handshake
The first time Claude connects to outlook-mcp, it asks two questions: what tools do you have? and what’s the schema for each? The server answers with a list — list_emails, read_email, search_emails, create_draft, list_calendar_events, get_calendar_event, create_calendar_event — plus a JSON schema for each one describing its arguments and return shape.
That’s the whole magic of MCP. Claude doesn’t need to know anything about Outlook. It reads the tool names and schemas, and from that point they’re part of its vocabulary. Add delete_email tomorrow and Claude discovers it on the next handshake.
The OAuth dance
Before any tool can run, the server needs a Microsoft access token. That’s what the OAuth dance produces.
OAuth solves one awkward problem: letting an app read your email without giving it your password. With a password the app could do anything — change credentials, read everything forever, lock you out. OAuth adds Microsoft as middleman: it holds the password and hands the app a narrower credential — a token scoped only to the permissions you approved (Mail.Read, Mail.ReadWrite, Calendars.Read, Calendars.ReadWrite) and revocable at any time.
The flavor Microsoft uses for personal accounts is PKCE (Proof Key for Code Exchange):
- The server opens a Microsoft login page in the browser.
- I sign in to Microsoft — not to
outlook-mcp. The server never sees my password. - Microsoft asks, in plain English, whether
outlook-mcpcan read my mail and calendar. - I click yes. Microsoft redirects back to a local server URL with a short-lived authorization code.
- The server exchanges that code — plus a secret it generated at step 1 (the “proof key”) — for a refresh token (long-lived) and an access token (short-lived, used to call Graph).
- The server stores the refresh token on disk (
tokens.json) or in an env var for the deployed instance. All future API calls use it to mint fresh access tokens silently.
PKCE is the right OAuth pattern for personal-scale tools. The “proof key” is a random secret the server generates and holds only for the seconds between steps 1 and 5. If someone intercepts the redirect in step 4, they get a useless authorization code — unredeemable without the proof key. This matters because outlook-mcp is a “public client” that can’t be trusted to hold a long-lived secret. PKCE is OAuth’s answer for that situation — the same flow mobile apps, desktop apps, and CLI tools use every day.
The seven tools, and the principle behind them
The server exposes exactly seven tools. Three read mail, one writes a draft, three handle the calendar. There is no send_email. There is no delete_event. That absence is the point.
AI tools should prepare; humans should confirm. Drafting is reversible — read it, edit it, discard it. Sending is not. The server is designed so every irreversible Outlook operation is unreachable through MCP regardless of what Claude is asked to do. A safety property that depends on the LLM remembering to behave is no safety property at all.
If Claude misunderstands an instruction or hallucinates a recipient, the worst-case outcome is a wrong draft in my drafts folder. That’s a cost I can pay.
Each tool earns the boundary it sits behind:
create_draftwrites to the Drafts folder viaPOST /me/messages. Graph’s send endpoint is never called. The draft appears in Outlook like any other — I open it, read it, edit it, decide whether to click send.create_calendar_eventwrites events withshowAs: tentativeby default — a soft hold visible to me, not blocking time others see as busy. An LLM that doesn’t pass the override field can’t accidentally mark time as busy. For v1, the tool also rejects anyattendeesarray: the system can put things on my calendar; it cannot send invitations to other people’s.- Every response — read or write — includes a
webLink: Outlook’s direct URL to the item. One click to verify exactly what the server did.
Include a verification link in every AI write operation. webLink on every response means I can confirm what was created without navigating Outlook manually. It’s a one-line addition to the response normalizer that closes the trust loop: the AI acts, the human verifies, no friction. Any tool that writes to an external system should return a URL to what it wrote.
These aren’t external policies enforced by a wrapper. They’re how the tools are coded.
The dual-transport pattern
The same Node.js code runs in two modes.
Stdio mode (src/index.js) is the local path. The server runs as a child process of Claude Desktop or Claude Code, talking over stdin/stdout. No network, no auth — the host process spawned the server, so trust is implicit. OAuth tokens live in tokens.json on disk. Right model when the user and the server are the same person on the same machine.
HTTP Streamable mode (src/server.js) is the deployed path. The server runs on Railway, reachable from anywhere — which flips the trust model. Any incoming request must prove its identity before the server touches Microsoft. A Lambda authorizer (a small AWS function deployed in its own stack) validates JWTs issued via magic-link email, falling back to a static x-api-key. The Microsoft refresh token lives in OUTLOOK_REFRESH_TOKEN rather than on disk, because Railway containers are ephemeral.
Tool definitions, Graph calls, and safety constraints are shared across both modes. The only difference is how trust is established.
Build local first, add the transport layer second. Stdio gives you a tight feedback loop with zero ceremony — no tokens, no ports, no deploy. Once the business logic is right, wrapping it in an HTTP transport + auth layer is a bounded problem. The alternative (designing for remote auth from day one) couples concern you want to separate: “does this Graph call work?” and “is this request authenticated?” Both matter; answering them together slows you down.
The stack, annotated
| Layer | Service | What it does | Notes |
|---|---|---|---|
| Runtime | Node.js 20 | Executes the server code | Same code in stdio and HTTP modes |
| API target | Microsoft Graph | One REST API for all of Microsoft 365 | graph.microsoft.com/v1.0/me/* |
| Identity | OAuth 2.0 PKCE | Proves the server is allowed to call Graph as me | No client secret — safe for public clients |
| Token storage | tokens.json or env var | Holds the long-lived refresh token | File for local, env for containers |
| Protocol | MCP (Model Context Protocol) | Standard interface AI tools call | 7 tools registered |
| Local transport | stdio | Pipe between Claude and the server process | Zero auth — implicit trust |
| Remote transport | HTTP Streamable on Railway | Reachable from any MCP client | Always-on web service |
| Auth (remote) | AWS Lambda authorizer | Validates JWTs (magic-link email) or x-api-key | Borrowed from niu-library / niu-news |
| Scopes | Mail.Read, Mail.ReadWrite, Calendars.Read, Calendars.ReadWrite | The four permissions the app asks Microsoft for | Adding one requires re-running the OAuth flow |
| Safety defaults | Draft-only mail, tentative-only calendar, no attendees, webLink on every response | Encoded in tool implementations, not enforced by prompt | Boundaries live in code |
One Node.js server, two transports, one external API. The omissions — send_email, delete_event, attendee invitations — do as much work as the inclusions.
Why build this yourself?
Microsoft has its own AI assistant. So does every major productivity vendor. You could plug into one without writing a line of code.
The reason outlook-mcp exists is that vendor assistants encode the vendor’s safety judgment, not mine. Their bar for “is this draft ready to send” is calibrated to the median user across millions of accounts. Their decision about what an AI may do to a calendar is one I never participated in.
Building the small thing yourself flips that. The seven tools are the seven I actually want. The four boundaries — drafts only, tentative only, no attendees, always a webLink — are mine, encoded in code I can read and change in an afternoon. Whether the AI can send mail on my behalf isn’t a setting I hope is configured correctly somewhere; it’s a function that doesn’t exist.
That’s the through-line for every project on this site. The interesting layer isn’t the framework or the SDK. It’s the small, specific judgment encoded in code, sitting between an AI that’s good at language and a system powerful enough to do real damage. outlook-mcp is what that judgment looks like when the system in question is my inbox.