Home / AI / Claude AI / Outlook MCP Server: Connect to Claude Code CLI

Outlook MCP Server: Connect to Claude Code CLI

Diagram showing terminal connected to Outlook via glowing data streams

I wanted to ask Claude what emails were waiting for me without leaving the terminal. Not forward them to an API endpoint, not grant OAuth tokens to a cloud service, and definitely not open a browser. Just type a question into Claude Code and get my inbox back. That meant building an Outlook MCP server — a small Node.js process running on my Windows machine that exposes Outlook as a set of tools Claude can call. This guide covers exactly how I built it, every error that tripped me up along the way, and how to connect it to Claude Code CLI on Linux or Mac over a local network. No Azure. No Graph API. No cloud dependency of any kind.

What is MCP and why Outlook?

MCP stands for Model Context Protocol, an open standard from Anthropic that defines how AI models connect to external tools and data sources. Instead of hardcoding integrations into the model itself, MCP lets you run a server that exposes a set of named tools — functions the AI can call with structured inputs and get structured outputs back. I had already built a local MCP server for Ollama, so the pattern was familiar. Outlook was the natural next target.

The standard alternative for Outlook access is the Microsoft Graph API. That requires registering an Azure application, setting up OAuth, supplying tenant IDs, configuring delegated permissions, getting admin consent in many corporate setups, and writing ongoing token refresh logic. For a personal inbox on a local machine where Outlook is already open and running, all of that is unnecessary overhead. Windows COM automation has been part of the operating system for decades. Outlook exposes a complete COM object model that covers almost everything the desktop app can do. Node.js can shell out to PowerShell. That is the entire stack — Outlook on Windows, PowerShell talking to it via COM, Node.js calling PowerShell, and Claude calling Node.js via MCP over SSE.

The appeal over a cloud solution is not just simplicity. It is also privacy. Your emails never leave your machine. Claude reads what you give it via the tool results, but the raw inbox data stays entirely local. For anyone handling sensitive client correspondence, that distinction matters.

How the setup works

The architecture involves two machines on the same local network. On the Windows machine, Node.js runs an Express server on port 3000. That server implements the MCP protocol using Server-Sent Events transport, which means it opens an HTTP endpoint that any machine on the LAN can connect to and stream results from. When Claude calls a tool, Node.js spawns a PowerShell process that uses COM automation to talk to the running Outlook application, collects the output, and returns it as an MCP tool response.

On the Linux or Mac machine, Claude Code CLI is installed and configured with an MCP server entry pointing at the Windows machine’s local IP address. The connection uses a package called mcp-remote, which acts as a bridge between the local Claude Code process and the remote SSE endpoint. From Claude’s perspective it is just another MCP server with four available tools: get_unread_emails, read_email, search_emails, and save_draft.

The reason for SSE rather than stdio transport is that stdio only works for processes running on the same machine — it communicates over standard input and output streams. SSE opens an HTTP endpoint, which means any machine that can reach the Windows IP on port 3000 can use the server. The same server could equally be connected from a Mac, another Windows machine, or any device on the network running Claude Code CLI.

What you need

Before starting, confirm you have the following in place:

  • Windows 11 with Microsoft Outlook installed and actively running (the desktop app, not Outlook on the web or the new Outlook app — COM automation targets the classic desktop client)
  • Node.js on Windows, version 18 or later
  • Claude Code CLI installed on your Linux or Mac machine
  • Both machines on the same local network with the Windows machine reachable by IP
  • The Windows machine’s local IP address — run ipconfig in a Windows terminal and look for the IPv4 address under your active network adapter, typically something like 192.168.1.x

You do not need an Azure subscription, a Microsoft 365 developer account, or any cloud credentials. The Node.js dependencies are minimal: the MCP SDK and Express. PowerShell is already present on Windows 11. Outlook’s COM server is registered automatically when Outlook is installed.

Building the Node.js MCP server

Create a new directory on Windows and initialise the project:

mkdir outlook-mcp-server
cd outlook-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk express zod

The MCP SDK provides the server and transport classes. Express handles the HTTP routing. Zod handles tool input schema validation, which the MCP SDK requires for tool definitions.

Create server.js with the following content. Read the comments carefully — the key architectural decisions are explained inline:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { exec } from "child_process";
import { z } from "zod";

const app = express();

// Spawn a PowerShell process and return its stdout
function runPowerShell(script) {
  return new Promise((resolve, reject) => {
    exec(
      `powershell -NonInteractive -NoProfile -Command "${script.replace(/"/g, '"')}"`,
      { timeout: 15000 },
      (err, stdout, stderr) => {
        if (err) reject(stderr || err.message);
        else resolve(stdout.trim());
      }
    );
  });
}

// Create a fresh McpServer for each SSE connection.
// Do NOT share one server instance — the second connection will fail with a 500.
function createServer() {
  const server = new McpServer({ name: "outlook-mcp", version: "1.0.0" });

  server.tool(
    "get_unread_emails",
    "Get unread emails from Outlook inbox",
    { count: z.number().optional().default(10) },
    async ({ count }) => {
      const script = `
        $outlook = New-Object -ComObject Outlook.Application
        $ns = $outlook.GetNamespace('MAPI')
        $inbox = $ns.GetDefaultFolder(6)
        $unread = $inbox.Items | Where-Object { $_.UnRead -eq $true } | Select-Object -First ${count}
        $unread | ForEach-Object {
          Write-Output "ID: $($_.EntryID)"
          Write-Output "From: $($_.SenderName) <$($_.SenderEmailAddress)>"
          Write-Output "Subject: $($_.Subject)"
          Write-Output "Received: $($_.ReceivedTime)"
          Write-Output "---"
        }
      `;
      const result = await runPowerShell(script);
      return { content: [{ type: "text", text: result || "No unread emails." }] };
    }
  );

  server.tool(
    "read_email",
    "Read a specific email by its EntryID",
    { entry_id: z.string() },
    async ({ entry_id }) => {
      const script = `
        $outlook = New-Object -ComObject Outlook.Application
        $ns = $outlook.GetNamespace('MAPI')
        $mail = $ns.GetItemFromID('${entry_id}')
        Write-Output "From: $($mail.SenderName) <$($mail.SenderEmailAddress)>"
        Write-Output "Subject: $($mail.Subject)"
        Write-Output "Received: $($mail.ReceivedTime)"
        Write-Output ""
        Write-Output $mail.Body
      `;
      const result = await runPowerShell(script);
      return { content: [{ type: "text", text: result }] };
    }
  );

  server.tool(
    "search_emails",
    "Search Outlook inbox by keyword",
    { query: z.string() },
    async ({ query }) => {
      const script = `
        $outlook = New-Object -ComObject Outlook.Application
        $ns = $outlook.GetNamespace('MAPI')
        $inbox = $ns.GetDefaultFolder(6)
        $results = $inbox.Items | Where-Object {
          $_.Subject -like '*${query}*' -or $_.Body -like '*${query}*'
        } | Select-Object -First 5
        $results | ForEach-Object {
          Write-Output "ID: $($_.EntryID)"
          Write-Output "From: $($_.SenderName)"
          Write-Output "Subject: $($_.Subject)"
          Write-Output "---"
        }
      `;
      const result = await runPowerShell(script);
      return { content: [{ type: "text", text: result || "No matching emails." }] };
    }
  );

  server.tool(
    "save_draft",
    "Save an email draft in Outlook (does not send)",
    { to: z.string(), subject: z.string(), body: z.string() },
    async ({ to, subject, body }) => {
      const safeBody = body.replace(/'/g, "''");
      const script = `
        $outlook = New-Object -ComObject Outlook.Application
        $mail = $outlook.CreateItem(0)
        $mail.To = '${to}'
        $mail.Subject = '${subject}'
        $mail.Body = '${safeBody}'
        $mail.Save()
        Write-Output "Draft saved to Outlook Drafts folder."
      `;
      const result = await runPowerShell(script);
      return { content: [{ type: "text", text: result }] };
    }
  );

  return server;
}

// Track active transports by session ID
const transports = {};

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/message", res);
  transports[transport.sessionId] = transport;
  const server = createServer();
  await server.connect(transport);
  res.on("close", () => delete transports[transport.sessionId]);
});

// Do NOT add express.json() here — handlePostMessage reads the raw stream.
// If Express has already parsed the body, the stream is consumed and you get
// "stream is not readable" on every tool call.
app.post("/message", async (req, res) => {
  const transport = transports[req.query.sessionId];
  if (!transport) return res.status(404).send("Session not found");
  await transport.handlePostMessage(req, res);
});

app.listen(3000, "0.0.0.0", () => {
  console.log("Outlook MCP server listening on port 3000");
});

Add "type": "module" to your package.json so Node treats the file as an ES module, then run it with node server.js. You should see the listening message. Leave this terminal open — it needs to stay running for Claude to connect.

PowerShell COM automation explained

Every tool call spawns a new PowerShell process that opens a COM connection to the running Outlook instance. The entry point is always the same:

$outlook = New-Object -ComObject Outlook.Application

This line does not launch a new Outlook process — it connects to the one that is already running. If Outlook is closed, this will throw. Make sure Outlook is open before starting the Node.js server, and leave it open while you are using the MCP tools.

From the application object you get a namespace, and from the namespace you get folders by a numeric constant called a MAPI folder type:

$ns = $outlook.GetNamespace('MAPI')
$inbox = $ns.GetDefaultFolder(6)   # 6 = Inbox

The commonly used MAPI folder constants are:

  • 3 — Deleted Items
  • 4 — Outbox
  • 5 — Sent Items
  • 6 — Inbox
  • 9 — Calendar
  • 10 — Contacts
  • 16 — Drafts

These constants come from the OlDefaultFolders enumeration in the Outlook object model. They are not arbitrary — they are stable across Outlook versions, so code written against Outlook 2016 using these constants will work on Microsoft 365 desktop.

Searching uses PowerShell’s Where-Object to filter items client-side. This is simple and reliable but will be slow on large inboxes because it loads every item. For production use, switch to Outlook’s native Restrict method which applies an SQL-like filter on the server side:

$filter = "@SQL=urn:schemas:httpmail:subject LIKE '%invoice%'"
$results = $inbox.Items.Restrict($filter)

One important note about newlines in PowerShell strings passed to COM properties: PowerShell backtick-n (`n) does not always produce a newline in COM string properties, depending on how the string is being passed. If you extend the server to include a send_email tool and find that body line breaks are not being preserved, use [char]13 + [char]10 (CRLF) explicitly rather than backtick-n. This was one of the more obscure issues I hit during development.

Connecting Claude Code CLI via mcp-remote

On your Linux or Mac machine, open Claude Code CLI and type /mcp to open the MCP configuration interface. Add a new server entry and use this as the command:

npx -y mcp-remote http://192.168.1.249:3000/sse --allow-http

Replace 192.168.1.249 with your actual Windows machine IP address. The --allow-http flag is required because mcp-remote defaults to requiring HTTPS. Since this is a local LAN connection you control, plain HTTP is acceptable — you would only need TLS if the server were exposed to the internet.

After saving the configuration, Claude Code CLI will open an SSE connection to the Windows server. Verify the connection by typing /mcp again — the server should appear in the list with status connected, and you should be able to see all four tools listed under it: get_unread_emails, read_email, search_emails, and save_draft.

If you restart the Node.js server on Windows, the SSE stream drops and Claude Code CLI loses the connection. To reconnect, open /mcp and use the reconnect option for the Outlook server entry. The underlying session ID changes on each server start, but mcp-remote handles the re-handshake automatically — you do not need to edit the configuration.

One thing to check if the connection times out: Windows Firewall may be blocking port 3000. On the Windows machine, open Windows Defender Firewall, go to Inbound Rules, and add a rule allowing TCP traffic on port 3000. Without this, the Linux machine’s connection attempts will silently time out.

The gotcha that trips everyone up

If the server starts without errors but every tool call returns a COM failure, you have almost certainly hit this issue: CO_E_SERVER_EXEC_FAILURE (0x80080005).

This error means COM could not connect to the target process. In the Outlook MCP context it nearly always means one thing: Node.js is running from an elevated (administrator) command prompt, and Outlook is running as a normal user.

COM automation has a strict security boundary between elevated and non-elevated processes. When a high-integrity process (admin) tries to connect to a COM server registered by a normal-integrity process (standard user), Windows refuses the connection. Outlook runs as your regular user account, so any process trying to automate it via COM must also run without elevation.

# This will fail if the terminal was opened with "Run as administrator"
node server.js

# Open a regular Command Prompt (no admin) and run:
node server.js

The fix is simply to close the admin terminal and open a normal Command Prompt or PowerShell window — right-click the Start menu and choose Terminal (not “Terminal (Admin)”). Start Node.js from there and the COM connection to Outlook will work immediately.

This trips up almost everyone who works in admin terminals out of habit. The server starts fine, the SSE connection from Claude Code CLI establishes correctly, and everything looks healthy until you actually call a tool. Then the PowerShell script fails with the 0x80080005 code and it is not immediately obvious why.

Controlling your inbox from the terminal

With the server running and Claude Code CLI connected, you can interact with Outlook through natural language prompts. Some examples of what this enables in practice:

Asking for a summary of unread emails: Claude calls get_unread_emails, reads through the list of subjects and senders, and produces a concise summary grouped by priority or topic. If you have fifteen unread emails, you can know in seconds which ones need a response today without opening Outlook at all.

Reading a specific thread: type something like “read the email from Sarah about the Q2 report” and Claude will call search_emails to find it, get the EntryID from the results, then call read_email to retrieve the full body. It chains two tool calls automatically without you specifying the steps.

Drafting a reply: the save_draft tool is intentionally limited to saving rather than sending. This is a deliberate safety decision — the same principle used throughout the Claude Code CLI workflow for any tool that has real-world side effects. Claude composes the reply, saves it to your Outlook Drafts folder, and you open it, review it, and click Send yourself. You get the speed benefit of AI-drafted email without the risk of Claude sending something you have not approved.

A typical session might look like this — you type into Claude Code CLI:

Check my unread emails and tell me which ones need a reply today

---

Draft a reply to the invoice email from Jones Ltd saying we'll process payment within 5 working days

Claude handles the tool calls behind the scenes and the draft appears in Outlook ready to send. The whole interaction takes thirty seconds and never requires switching to the email client.

What to build next

The four tools in this server are a foundation. The Outlook COM object model covers the full desktop application, so almost anything Outlook can do can be exposed as an MCP tool. The most useful additions:

send_email — change $mail.Save() to $mail.Send() in the draft tool. Consider keeping the save_draft tool as the default and only adding send when you are confident in how Claude uses it. A two-step workflow where Claude drafts and you confirm is safer than giving it direct send access.

Calendar access$ns.GetDefaultFolder(9) gives you the Calendar folder. List today’s appointments, check availability for a given time slot, or create new calendar items. Combined with the email tools, you could ask Claude to check your calendar and draft a meeting request to someone who just emailed you.

Contacts$ns.GetDefaultFolder(10) for the Contacts folder. Useful for resolving names to email addresses before drafting, or for looking up phone numbers and company names from within a Claude workflow.

Flag and category management — Outlook items have FlagStatus, Categories, and IsMarkedAsTask properties. A tool that lets Claude flag emails for follow-up or assign categories would allow genuinely useful inbox management workflows.

Attachment handling — each email item has an Attachments collection. You could save attachments to disk and return the file paths as tool output, letting Claude read them using its file access tools in the same workflow.

Multi-tool agent workflows — once you have email and calendar together, compound workflows become possible. A single prompt like “prepare my briefing for tomorrow’s calls” could pull calendar appointments, find related emails from each attendee, and produce a structured summary — all through chained MCP tool calls that Claude orchestrates automatically.

Each new capability follows the same pattern: a PowerShell script that talks to Outlook’s COM model, wrapped in a tool definition and added to the createServer() function. The MCP layer handles the rest.

The Outlook MCP server took an afternoon to build and removed an entire category of context-switching from my day. If you are already running Claude Code as your primary development environment, connecting your inbox to it means one less reason to leave the terminal. The full server.js is above — copy it, run it from a non-admin terminal, add the mcp-remote config to Claude Code CLI, and you have a working Outlook MCP server. If you hit CO_E_SERVER_EXEC_FAILURE, the cause is almost certainly the admin process issue covered above. Fix that and everything else should connect cleanly.