Graph Mail MCP
# Graph Mail MCP
A local stdio [Model Context Protocol](https://modelcontextprotocol.io/) server for Microsoft 365 mail. It provides controlled Outlook/Exchange Online search, read, thread, attachment, and draft workflows through Microsoft Graph.
It never sends, deletes, moves, copies, archives, or marks messages as read. Raw Graph URLs, raw OData, and unrestricted Graph requests are not exposed.
**Deployment order:** clone and build -> create your own configuration -> complete
first sign-in -> add the server to a supported MCP client configuration -> verify
it in the intended context.
Choose the configuration scope that suits your use: workspace/project, user
profile, or another location explicitly supported by your client. **User-global
installation is optional, not a requirement of this server.** The client must
load the configuration and resolve the correct executable, arguments, and
environment; an arbitrary JSON file is not discovered automatically.
This repository is self-contained. No maintainer-specific workspace, setup script,
Azure CLI login, separate authentication proxy, or other repository is required.
All `C:\to\path\...` paths below are placeholders: replace them with locations on
the machine being configured. Do not copy another person's account, browser
profile, or token cache.
- [Requirements](#requirements)
- [Deploy on a new Windows machine](#deploy-on-a-new-windows-machine)
- [Authentication lifecycle](#authentication-lifecycle)
- [Tools and efficient results](#tools)
- [Local data and updates](#local-data-and-updates)
- [Troubleshooting](#troubleshooting)
- [Security](#security)
## Requirements
- Windows with Microsoft Edge and an interactive desktop for initial sign-in/MFA.
- [Node.js 22+](https://nodejs.org/en/download), npm, and [Git](https://git-scm.com/downloads).
- Windows PowerShell (`powershell.exe`), used internally for Windows DPAPI.
You can run the setup commands in Windows PowerShell 5.1 or PowerShell 7.
- A Microsoft 365 work or school account with Exchange Online in the public cloud.
- Tenant policy permitting Graph Explorer and delegated `Mail.ReadWrite` access.
Existing grants in one tenant do not guarantee consent in another tenant.
- A supported, signed-in MCP client: VS Code with Copilot chat, Copilot CLI, or
another local stdio client. Client/organization policy must permit this server.
The included provider uses the installed Edge browser through Playwright.
You do not need a separate Playwright MCP server, app registration, client secret,
or manually supplied access token. Initial consent can still require your tenant
administrator; installation does not bypass that requirement.
Personal Microsoft accounts are disabled by default. Shared mailboxes additionally
require Exchange delegation and `Mail.ReadWrite.Shared`; leave shared access
disabled for the initial own-mailbox setup. The documented deployment is local
Windows, not an unattended service, WSL, a Linux container, or a remote SSH host.
## Deploy on a new Windows machine
Run these steps in one PowerShell window. Use a local, writable installation
directory, preferably outside synchronized folders. If you reopen PowerShell,
redefine the path variables before using later commands.
The commands use `npm.cmd` so PowerShell does not select an `npm.ps1` wrapper
that may be blocked by script policy; do not weaken execution policy for setup.
### 1. Clone and build
Replace `$ProjectRoot` with your installation directory. On an existing checkout,
use [the update procedure](#updating-an-existing-installation) instead of cloning
over it.
```powershell
$ProjectRoot = 'C:\to\path\graph-mail-mcp'
New-Item -ItemType Directory -Path (Split-Path -Parent $ProjectRoot) -Force | Out-Null
git clone https://github.com/hanhandly/graph-mail-mcp.git "$ProjectRoot"
if ($LASTEXITCODE -ne 0) { throw 'Clone failed. Stop and resolve repository access or the destination path.' }
Set-Location -LiteralPath $ProjectRoot
npm.cmd ci
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed.' }
npm.cmd run build
if ($LASTEXITCODE -ne 0) { throw 'Build failed.' }
$NodeExe = (Get-Command node -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
$EntryPoint = Join-Path $ProjectRoot 'dist\cli.js'
$ConfigPath = Join-Path $ProjectRoot 'config.local.json'
$NodeExe
```
Expected result: `dist\cli.js` exists, and `$NodeExe` is the absolute path to this
machine's Node executable. `node_modules` and `dist` are not checked into Git; each
machine must install dependencies and build.
### 2. Create local configuration and choose the account
**First installation must ask the person deploying the server which Microsoft 365
account to use.** Do not infer it from a GitHub login, a Windows username, this
README, or a different machine's cached session. This is separate from the GitHub
account used to sign in to Copilot.
The following commands prompt for that choice and refuse to overwrite an existing
configuration. Enter an exact sign-in UPN, such as `person@example.com`, or an
`@domain` selector, such as `@example.com`. An exact UPN is recommended when more
than one cached account belongs to the same domain.
```powershell
if (Test-Path -LiteralPath $ConfigPath) {
throw 'config.local.json already exists. Review or edit it instead of overwriting it.'
}
$AccountSelector = (Read-Host 'Microsoft 365 sign-in UPN or @domain selector').Trim()
if ([string]::IsNullOrWhiteSpace($AccountSelector)) {
throw 'An explicit account choice is required for first installation.'
}
$LocalConfig = Get-Content -LiteralPath (Join-Path $ProjectRoot 'config.example.json') -Raw | ConvertFrom-Json
$LocalConfig.graphExplorer.accountHint = $AccountSelector
$ConfigJson = $LocalConfig | ConvertTo-Json -Depth 20
[System.IO.File]::WriteAllText($ConfigPath, $ConfigJson, [System.Text.UTF8Encoding]::new($false))
& $NodeExe $EntryPoint -c $ConfigPath doctor
if ($LASTEXITCODE -ne 0) { throw 'Configuration validation failed. Correct the local JSON before signing in.' }
```
Save JSON as UTF-8 **without a byte-order mark (BOM)**. The write above works in
both supported PowerShell versions; Windows PowerShell 5.1's
`Set-Content -Encoding UTF8` adds a BOM and should not be substituted.
Review these settings in the ignored `config.local.json`:
| Setting | Initial choice and meaning |
| --- | --- |
| `graphExplorer.accountHint` | The UPN or domain you just chose. A domain selects the first matching cached account; it does not identify one particular person. |
| `tenant` | `"organizations"` accepts organizational tenants. To pin a tenant, use its actual tenant GUID, not a display name or tenant domain. |
| `defaultScopes` | Keep `["Mail.ReadWrite"]` for this read-and-managed-draft workflow. There is no read-first `Mail.Read` enrollment step. |
| `allowSharedMailboxes`, `allowedMailboxScopes` | Keep `false` and `["me"]` initially. These are mailbox access restrictions, not Inbox/Sent Items search filters. |
| `tokenProtection` | Keep `"dpapi"`. Never use plaintext token caching for a real mailbox. |
| `graphExplorer.edgeUserDataDir` | Keep the template's `%LOCALAPPDATA%\graph-mail-mcp\edge-profile`, not your ordinary Edge profile or a synchronized directory. |
| `graphExplorer.refreshBrowserMode`, `interactionMode` | The template explicitly enables `"background"` and `"auto"` for minimized renewal with a visible prompt only when needed. |
| `accountAliases` | Leave `[]` unless aliases are explicitly verified. Nonempty aliases require an exact UPN selector. |
For compatibility, a missing/null `accountHint` currently falls back to
`@microsoft.com`. **That fallback is not a substitute for first-install account
configuration**, particularly for users in other organizations.
`doctor` checks configuration and Node compatibility and reports cached auth
metadata. `ok: true` does not prove Edge can launch, consent exists, or Graph will
authorize a mailbox request. An unauthenticated cache before first sign-in is
expected.
### 3. Complete first sign-in
```powershell
& $NodeExe $EntryPoint -c $ConfigPath auth login
if ($LASTEXITCODE -ne 0) { throw 'Sign-in did not complete. Resolve the reported authentication requirement before continuing.' }
& $NodeExe $EntryPoint -c $ConfigPath auth status
```
The server opens its isolated Edge profile and uses Graph Explorer for delegated
authentication. Complete any password, MFA, or required approval in the Microsoft
window. Do not paste passwords, tokens, cookies, or browser storage into a terminal,
AI conversation, or configuration file.
Expected result: status identifies the chosen account and reports `state: "ready"`
and `authenticated: true`. Scope lists are cached-token observations, not a live
tenant-consent inventory. If consent requires an administrator or policy blocks
access, stop and obtain authorized assistance rather than changing OAuth clients,
loosening scope policy, or repeatedly attempting sign-in.
`--account-hint` on `auth login` is a temporary override; edit
`graphExplorer.accountHint` in the local JSON for a persistent account change.
For intentionally enabled shared-mailbox access, configure the mailbox allowlist
and Exchange delegation first, then use `auth login --shared` for the additional
`Mail.ReadWrite.Shared` requirement.
### 4. Add the server to your MCP client
Configure whichever client you want to use, in its supported configuration scope:
| Intended scope | Example configuration locations |
| --- | --- |
| One workspace/project | VS Code: `.vscode\mcp.json` in that workspace. Copilot CLI: `.mcp.json` or `.github\mcp.json` in the applicable repository context. |
| Across workspaces for one user/client profile | VS Code: the active profile's user `mcp.json`. Copilot CLI: its user `mcp-config.json`. This is an optional choice. |
| Another client or custom configuration source | Use the location or explicit loading mechanism documented by that client, with its required JSON schema. |
The client's MCP configuration describes how to launch the server. It is separate
from the application's `config.local.json`, which selects the mailbox account and
is passed through `-c`. If you move that application file, update the `-c` argument.
The examples below use absolute paths to avoid working-directory dependencies.
If you want multiple clients to reuse one account/session, point their launch
definitions to the same installation/configuration under the same Windows user
and local-data environment. You do not need to configure both clients.
Prepare the following values, even if you reopened PowerShell after signing in:
```powershell
$ProjectRoot = 'C:\to\path\graph-mail-mcp'
$NodeExe = (Get-Command node -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
$EntryPoint = Join-Path $ProjectRoot 'dist\cli.js'
$ConfigPath = Join-Path $ProjectRoot 'config.local.json'
foreach ($RequiredFile in @($NodeExe, $EntryPoint, $ConfigPath)) {
if (-not (Test-Path -LiteralPath $RequiredFile -PathType Leaf)) {
throw "Required file is missing: $RequiredFile"
}
}
if (-not $env:LOCALAPPDATA -or -not $env:USERPROFILE -or -not $env:SystemRoot) {
throw 'Run setup in the intended Windows user session with its standard environment.'
}
$McpServer = [ordered]@{
type = 'stdio'
command = $NodeExe
args = @($EntryPoint, '-c', $ConfigPath, 'serve')
env = [ordered]@{
LOCALAPPDATA = $env:LOCALAPPDATA
USERPROFILE = $env:USERPROFILE
SystemRoot = $env:SystemRoot
}
}
```
The environment values are resolved from the current Windows session, not from
the maintainer's machine. Passing them explicitly identifies the local-data root
even when a client filters inherited environment variables.
`ConvertTo-Json` below supplies the required JSON backslash escaping automatically.
**VS Code Copilot**
For workspace scope, create or edit `.vscode\mcp.json` in the intended workspace.
For user-profile scope instead, run **MCP: Open User Configuration** from the
Command Palette. The default Windows profile normally uses
`%APPDATA%\Code\User\mcp.json`; other profiles, Insiders, and remote settings can
use different locations. The guided **MCP: Add Server** flow also lets you choose
Workspace or Global.
Generate the `servers`-rooted JSON for the location you chose:
```powershell
[ordered]@{ servers = [ordered]@{ 'graph-mail' = $McpServer } } | ConvertTo-Json -Depth 10
```
If the file is empty, paste the whole output. Otherwise, merge **only** the
`graph-mail` entry into the existing `servers` object. Preserve all other servers
and settings. Save, then use **MCP: List Servers** to start/restart `graph-mail`
and review the client trust prompt.
**GitHub Copilot CLI**
For project scope, use `.mcp.json` or `.github\mcp.json` in the applicable
repository context. Project configuration is subject to the client's folder
trust and discovery rules.
For user scope instead, the default file is
`%USERPROFILE%\.copilot\mcp-config.json`; if `COPILOT_HOME` is set, use
`mcp-config.json` under that directory. Generate the CLI's `mcpServers`-rooted JSON
for your chosen location:
```powershell
$CliServer = [ordered]@{
type = 'local'
command = $McpServer.command
args = $McpServer.args
env = $McpServer.env
tools = @('*')
}
[ordered]@{ mcpServers = [ordered]@{ 'graph-mail' = $CliServer } } | ConvertTo-Json -Depth 10
```
Create the parent configuration directory if it does not exist. As above, paste
the whole output only into a new/empty file; otherwise merge just `graph-mail`
into the existing `mcpServers` object. Save JSON without a BOM. Start a fresh
Copilot CLI session in the intended context and use `/mcp` to inspect the server.
`tools: ["*"]` exposes the available tools; it does not mean you should globally
bypass tool approval.
The generated definitions contain resolved, machine-specific paths. Before
committing a project-level MCP file, decide whether it is a private local
configuration or a shared template. Keep private deployment files out of source
control; shared configurations should use client-supported variables or clearly
documented placeholders, never credentials or another user's local paths.
**Important differences and boundaries**
| Item | Rule |
| --- | --- |
| JSON root | VS Code uses `servers`; Copilot CLI uses `mcpServers`. Do not paste one whole file into the other. |
| Working directory | These examples use absolute paths and need no `cwd` setting. If you adapt them to supported workspace variables or relative paths, follow that client's path-resolution rules. |
| Process | The client launches `node ... -c ... serve` over stdio. There is no URL, listening port, or background service to start separately. Keep `-c` before `serve`. |
| Scope | Scope is selected by the client configuration, not imposed by Graph Mail MCP. Project scope is valid; user scope is optional and does not automatically cover other machines, profiles, WSL, or containers. |
| Duplicate definitions | If `graph-mail` exists in more than one configuration source, check client precedence rules and keep the intended entry active. A valid project-local setup does not need to be replaced with a global one. |
| VS Code Agent Host | VS Code can forward its configuration to Agent Host; some sessions also discover workspace `.mcp.json` or the Copilot CLI user file natively. Reuse the intended registration rather than creating competing copies. |
An AI assisting deployment must follow the requested configuration scope rather
than assume user-global installation. It must ask for the account choice, preserve
existing client entries and local configuration, leave token/profile files local,
and report any blocked step. It must not substitute another user's paths, invent
credentials, disable organizational controls, or claim success solely from a
configuration file being written.
### 5. Verify in the configured scope
For a workspace/project registration, open that workspace or repository in the
chosen client. For another explicitly loaded configuration, launch the client
with that source. Run the following checks where the configuration is intended
to apply:
1. Confirm that `graph-mail` exposes the ten tools listed below.
2. Invoke `mail_auth_status`. Check the intended account and the returned state;
do not treat `refresh_possible` as an already authenticated result.
3. Optionally invoke `mail_authenticate` to complete/reuse authentication without
reading a mailbox. It is not a mandatory preflight before every mail tool.
4. If mailbox access verification is authorized, call `mail_list_folders` with
`{"mailboxScope":"me","pageSize":1}`. This is read-only. Do not create a draft
merely to verify installation.
If you specifically chose user-global scope, you can additionally repeat the
checks from an unrelated workspace to confirm that coverage. This is not a
requirement for project-scoped deployments.
Tool discovery proves process startup; cached `ready` status proves usable local
token metadata; a successful mailbox read proves access to that particular Graph
operation. These are different checks. Do not attach raw mail or auth output to
public issues.
Running `serve` manually in PowerShell waits for an MCP client on stdin; it is not
an HTTP server or a human-facing mail command. Let VS Code/Copilot CLI own that
process. After rebuilding, existing server processes need a restart to load the
new code and tool definitions.
## Authentication lifecycle
Explicit `auth login` drives Graph Explorer's **Sign in**, configured account
selection, permissions panel, exact supported-scope **Consent**, and Microsoft
confirmation. Automation is limited to `Mail.ReadWrite` and intentionally enabled
`Mail.ReadWrite.Shared`; it never selects tenant-wide admin consent or runs the
preloaded POST query. Password, MFA, Conditional Access, and administrator approval
remain user-controlled. Cached tokens are checked against the configured account
or domain. `auth status` returns metadata and next actions, never the token.
Graph Explorer can return permissions previously granted to its shared application.
The default `unexpectedTokenScopePolicy: "warn"` reports scopes beyond the mail
and sign-in allowlist. Strict deployments can choose `"reject"`, but must handle
the resulting rejection rather than assuming it trims a token's permissions.
A genuinely least-privilege OAuth client would require a separately implemented,
authorized provider; the shipped server has no app-ID/client-secret configuration
switch to replace Graph Explorer.
### Automatic renewal after initial sign-in
Normal mail requests reuse a validated token without opening Edge. Within five minutes of expiry, or when the cache is missing, the provider tries the existing Graph Explorer session. The recommended example configuration uses a **real Edge window minimized in the background**:
```json
{
"graphExplorer": {
"automaticRefresh": true,
"refreshBrowserMode": "background",
"interactionMode": "auto",
"silentRefreshTimeoutMs": 45000,
"loginTimeoutMs": 300000
}
}
```
The browser and its authentication popups are minimized using Edge's own window API, with the resulting state checked. If Microsoft actually requests password, MFA, or permission confirmation, the **same existing prompt** is restored for your action; the browser is not restarted. Only you complete that action. Renewal does not click permission Consent or approve the Microsoft consent prompt. Explicit `auth login` remains the initial exact-scope enrollment path.
This is Playwright, not WAM or an invisible Windows service. A minimized window can appear in the taskbar, startup can briefly show a window, and system-owned authentication dialogs are not guaranteed to be hidden. No unrelated Edge profile or window is controlled.
The initial attempt is bounded by `silentRefreshTimeoutMs` (45 seconds by default). When a human step is detected, the same attempt gets up to `loginTimeoutMs` measured from its start; repeated polling does not extend that limit. Completion or failure closes the owned browser. Closing a required prompt does not trigger repeated new login windows. An explicit policy or administrator-approval block returns `AUTH_POLICY_BLOCKED` and stops repeated automatic attempts in that process until explicit login or a new validated cache resolves it; a generic access-denied page is not enough to identify Conditional Access.
Compatibility is opt-in: configurations **omitting** the new fields retain `refreshBrowserMode: "headless"` and `interactionMode: "manual"`. `manual` never allows mail requests to restore a login window. Set `automaticRefresh` to `false` for explicit-login-only mail requests. The example configuration recommends background/auto; do not overwrite an existing user's explicit choices when updating.
Concurrent requests share one renewal. A profile-scoped, heartbeat-maintained filesystem lease also coordinates separate VS Code and CLI processes, from browser acquisition through validated cache persistence. Waiting clients reuse the resulting token instead of opening another browser. A recognized profile-busy error still handles older clients that do not participate in the lease. Lease files, profile data, and the DPAPI cache remain under user-local app data; no HTTP proxy, listening port, detached service, or global Azure account change is introduced.
Tokens are cached in memory, with disk revision checks so another client's login or cache clearing is noticed without decrypting on every request. The encrypted disk cache holds one active account/scope context; older wildcard entries remain readable only after account validation.
On HTTP 401, the transport requests a **different** token and replays the approved request at most once. Returning the same token is not a successful refresh. Failed renewal preserves the disk cache, but expired or rejected tokens are not used, and a failed silent attempt has a 30-second process-local cooldown.
To exercise the same noninteractive path without reading mail:
```powershell
& $NodeExe $EntryPoint -c $ConfigPath auth refresh
```
`auth refresh` remains noninteractive even when mail requests use auto mode. `auth refresh --force` requires a replacement; it can fail if Graph Explorer still supplies the same token. To allow recovery to display a required Microsoft prompt without querying mail, use `auth refresh --interactive` (optionally with `--force`). These refresh commands never automate new permission consent or bypass Microsoft controls. `auth status` remains cache-only and reports `authenticationInProgress` when a local request or another participating client owns authentication.
MCP client timeouts are not under the server's control. If the client cancels or disconnects during MFA, no later Graph operation is started for that request, including deferred draft writes. Cancellation also stops transport retries. A cancelled request can leave shared authentication running to populate the cache for another request; disconnecting the MCP server closes its owned authentication resources. Complete the Microsoft prompt and retry a **read** if its client timed out. Do not blindly repeat a draft creation whose HTTP request was already sent: cancellation cannot undo an operation already accepted by Graph.
When the cache is missing or expired and silent renewal is available, status reports `authenticated: false`, `interactionRequired: false`, and a next action to try a mail tool or `auth refresh`. This is not a claim that renewal has succeeded; it avoids incorrectly demanding manual login before the automatic path has been tried. An observed interaction-required failure, disabled automatic renewal, or invalid account/scope policy directs the caller to the corresponding corrective action.
An MSAL/WAM broker is not enabled. An existing Graph Explorer grant does not automatically authorize Microsoft Graph PowerShell or another OAuth client. The selected implementation does not register an app, extract WorkIQ/Office refresh tokens, change Azure CLI accounts, or start an authentication proxy listener. See the [authentication design](docs/architecture.md#authentication-lifecycle-and-native-broker-boundary).
## Tools
| Area | Tools |
| --- | --- |
| Authentication and folders | `mail_auth_status`, `mail_authenticate`, `mail_list_folders` |
| Search and read | `mail_search_messages`, `mail_get_message`, `mail_get_thread` |
| Attachments | `mail_list_attachments`, `mail_get_attachment` |
| Managed drafts | `mail_create_draft`, `mail_update_draft` |
Draft updates are limited to drafts created and registered by this MCP. There is no send tool.
`mail_authenticate` is an optional, explicit recovery entry that reuses the same configured session. It makes no mailbox request, does not enroll `Mail.Read`, and does not automate new consent. It is not a required extra step before every mail tool. Normal read and draft workflows continue to use the already configured `Mail.ReadWrite` permission.
`mail_auth_status` remains cache-only. Its `state` distinguishes `ready`,
`refresh_possible`, `in_progress`, `login_required`, `interaction_required`,
`consent_required`, and `blocked`. A missing/expired cache may be
`refresh_possible` without promising that renewal will succeed. Consent is
reported only after direct observation or a recorded consent-specific failure;
another process's lease or generic denial does not establish consent requirements.
`nextAction` directs callers to an existing prompt rather than duplicate logins.
`requiredScopes` is the configured requirement. `grantedScopes` contains scopes
observed on the cached token, including an expired token; it is not a live
tenant-consent inventory. `extraGrantedScopes` is the difference from the
required scopes, so it can include normal ancillary sign-in permissions.
The existing `unexpectedScopes` warning instead uses the configured allowlist.
`mcpCapabilities` reports local policy and explicitly sets
`mailboxAccessChecked: false`; it is not proof that Graph accepted a mailbox call.
### Correctness and efficient results
Searches and threads span mailbox folders by default, including incoming replies and sent messages. `sentitems` is never inferred as a default from sender identity. An explicit single `folderIds` value can be an ID or a Graph well-known name such as `inbox` or `sentitems`; multiple folders are not silently expanded into extra requests.
Message collection `$search` now quotes the complete KQL expression as required by Graph, including nested subject phrases and literal `#` characters. Subject and participant roles are preserved; there is no automatic subject-to-full-text fallback on 400. Search/relevance keeps supplied text clauses conjunctive. Filter mode retains its existing substring alternatives for multiple subjects. Date ranges and `hasAttachments: false` are honored; incompatible `conversationId` plus indexed-search criteria fail explicitly rather than being ignored. Indexed date/attachment candidates are checked against returned fields before inclusion.
Thread deduplication uses explicit message IDs and Internet Message IDs only. Similar sender, subject, minute, or preview text never discards a different message. The seed is retained, and a capped thread reports partial coverage when additional pages remain.
The existing output stays available, with optional smaller views:
| Option | Effect and boundary |
| --- | --- |
| `bodyMode: "none"` / `"preview"` | No content, or the Graph preview; no full-body download is requested. |
| `bodyMode: "text"` / `"html"` | Full original message body in the requested format. |
| `bodyMode: "unique_text"` | Graph's native plain-text `uniqueBody`, selected in the existing request. It is a distinct field, not a locally summarized or stripped full body. Unsupported or missing native content fails explicitly; relevance mode does not support it. |
| `selectFields: []` | Core IDs, subject, from/sender, dates and message state only. Specify additional allowlisted metadata fields when needed; omit this option for the standard envelope. Unselected recipients are omitted, not represented as empty recipients. |
| `maxBodyChars` | Tightens the per-message returned-content budget without exceeding server limits. This is post-retrieval truncation, not a promise to reduce Graph's full-body download. |
| Thread `headerMode: "thread"` / `"none"` | Return only reference/Auto-Submitted headers, or omit headers from output. Reconstruction uses the same already-fetched header data. Omit for the existing full-header output. |
MCP JSON is compact; human-facing CLI output remains formatted. Content is also bounded by `maxMessageBodyCharacters` and `maxTotalBodyCharacters`; `bodyTruncated: true` and provenance disclose truncation. There is no default regular-expression removal of signatures, quotations, or forwarded content.
`includeSenderIdentity: true` adds address-match-based `isFromMe` and `isSentByMe` using the account on the token already used for that Graph response. Optional `accountAliases` may contain explicitly verified aliases, but requires an exact configured account selector. There is no directory lookup or additional scope request. Unrecognized addresses remain `null`, not a guessed `false`/other; these fields do not prove human authorship, message authenticity, or who used a send-as permission.
`excludeAutomaticMessages: true` removes only messages explicitly marked `Auto-Submitted: auto-replied` or `auto-generated`. Missing, conflicting, custom, or absent markers do not justify discarding a reply. The option selects headers in the existing collection request, never adds per-message GETs, and reports exclusions/unknown classification in provenance. It is opt-in, not available for relevance search, and may increase upstream header payload. An explicitly requested thread seed is retained even if marked automatic.
Native property references: [Graph message / uniqueBody](https://learn.microsoft.com/graph/api/resources/message?view=graph-rest-1.0), [message search syntax](https://learn.microsoft.com/graph/search-query-parameter), and [well-known folder names](https://learn.microsoft.com/graph/api/resources/mailfolder?view=graph-rest-1.0).
## Local data and updates
| Location | Contents and handling |
| --- | --- |
| `C:\to\path\graph-mail-mcp` | Your checkout: source, dependency lockfile, documentation, and locally built `dist`. |
| `config.local.json` in that checkout | Your account/configuration choices; Git-ignored, but not a secret store. Keep credentials out of it. |
| `%LOCALAPPDATA%\graph-mail-mcp\auth\token-cache.json` | DPAPI-encrypted Graph access-token material plus account/scope metadata. Never commit or share it. |
| `%LOCALAPPDATA%\graph-mail-mcp\edge-profile` | Isolated Edge sign-in state from the example configuration. Do not copy it from or to someone else's machine. |
| `%LOCALAPPDATA%\graph-mail-mcp` | Default location for other runtime state, including draft ownership, cursor/coordination data, and diagnostics. Keep it out of Git and synchronized folders. |
| Your chosen MCP configuration file(s) | Launch settings at the selected workspace, project, user, or other supported scope. Review machine-specific paths before sharing or synchronizing; keep mailbox credentials out of these files. |
DPAPI protects the **Microsoft Graph access token**, not a Windows logon token.
The current Windows user is the encryption context. Treat the cache/profile as
per-user, per-machine state and sign in separately on a new machine rather than
shipping a portable login bundle. Encryption is not protection from every process
running as that same Windows user.
### Updating an existing installation
Preserve `config.local.json` and your chosen MCP registrations. Do not repeat the
first-install copy/configuration step.
```powershell
Set-Location -LiteralPath 'C:\to\path\graph-mail-mcp'
git status --short
```
If tracked changes or untracked work are present, preserve and resolve them before
pulling; do not reset or clean the checkout as an update shortcut. With a clean
worktree:
```powershell
git pull --ff-only
if ($LASTEXITCODE -ne 0) { throw 'Update was not a clean fast-forward. Resolve it without discarding local work.' }
npm.cmd ci
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed.' }
npm.cmd run build
if ($LASTEXITCODE -ne 0) { throw 'Build failed.' }
```
Restart only this MCP in the affected clients, then check tool discovery and
authentication status again. Rebuilding does not hot-reload a running Node
process. Do not reinstall the browser, clear the token cache, or grant new scopes
as a routine code update.
## Troubleshooting
| Symptom | Check and action |
| --- | --- |
| `node` not found, missing `dist\cli.js`, or server fails before discovery | Install the prerequisites/build, re-resolve `$NodeExe`, and verify every absolute path. Read the server's output log; do not add a shell/npm wrapper that contaminates stdio. |
| `CONFIG_INVALID` or JSON parse error | Confirm the `-c` file exists, uses UTF-8 without BOM and valid JSON, and contains only supported fields. Check the account selector with `doctor`. |
| Works only in the configured workspace/project | This is expected for project scope. Choose user scope only if you want broader availability; otherwise verify loading and path resolution within the intended context. |
| One client unexpectedly sees a different account/cache | If the clients should share a session, compare their config arguments, Windows user, and `LOCALAPPDATA`/profile settings. Use the intended configuration, not another user's cache. |
| `refresh_possible` with `authenticated: false` | Renewal has not yet succeeded. Try the normal tool or `auth refresh`; do not assume a manual login is already required. |
| `in_progress` or `authenticationInProgress` | A local or participating peer process owns authentication. Follow `nextAction`; do not start competing logins or delete its lease. |
| `AUTH_INTERACTION_REQUIRED` or an observed consent prompt | Complete the existing Microsoft prompt when appropriate. `auth refresh --interactive` permits interaction but does not automatically grant consent; initial enrollment remains explicit `auth login`. |
| `AUTH_POLICY_BLOCKED`, tenant approval, or persistent missing scope | Follow the reported requirement with the tenant administrator. Do not bypass Conditional Access, add `Mail.Send`, switch to plaintext caching, or assume another OAuth client has the same grant. |
| `TOKEN_INVALID` or unexpected account/tenant | Correct the configured UPN/domain/tenant and sign in intentionally. A command-line account override does not persist to the MCP config. |
| DPAPI/cache cannot be read | Confirm the intended Windows user/session and `powershell.exe` availability. Do not copy a cache or weaken encryption to repair it. |
| Tool list remains at an older version | Rebuild, verify which entry point the active registration uses, and restart that MCP/client. |
| Edge is visible during renewal | Background mode minimizes owned windows; real MFA/policy prompts can still require visibility. It is not a guarantee of completely invisible authentication. |
For VS Code logs, use **MCP: List Servers**, select `graph-mail`, then **Show
Output**. In Copilot CLI use `/mcp` to inspect the registration and state. Redact
account identifiers and paths from shared diagnostics; never share raw tokens,
cookies, screenshots of credentials, or message bodies.
## Security
The Graph transport uses a deny-by-default policy firewall and typed request compiler. Tokens are stored outside the repository and protected with Windows DPAPI by default. Logs and tool responses must not expose access tokens, cookies, or authorization headers.
The server accepts delegated tokens only, always rejects `Mail.Send`, reports unexpected scopes by default, and can reject them in strict mode. Its request firewall limits runtime operations, but deployments requiring a strictly least-privilege bearer token should use a dedicated app registration or constrained authentication provider.
See [Security](SECURITY.md), [Tool reference](docs/tool-reference.md), [Configuration example](config.example.json), and [Architecture](docs/architecture.md).
Client configuration references: [VS Code MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers) and [GitHub Copilot CLI MCP servers](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers).
TDQS
Scored across 9 tools
Most tools are clearly distinct by resource and action: folders, auth, messages, threads, attachments, drafts. There is slight potential confusion between mail_get_message and mail_get_thread (both retrieve messages), but descriptions clarify the difference (single message vs conversation). mail_list_attachments and mail_get_attachment are distinct (list vs get). Overall, boundaries are clear.
All tools follow the exact pattern mail_<verb>_<noun>, using snake_case throughout. Verbs are consistent: list, auth, search, get, create, update. This is a highly predictable and consistent naming convention.
Nine tools is well within the ideal range (3-15). Each tool serves a distinct purpose in the email domain: folder navigation, auth, search, retrieval, thread handling, attachments, and draft management. No redundant or trivial tools.
The tool set covers core email operations: listing folders, searching/reading messages, handling threads, attachments, and creating/updating drafts. A notable missing operation is sending or deleting messages/drafts, which would be expected for a full lifecycle. However, the focus on drafts (without send) may be intentional. Thus, a minor gap.