Skip to main content
Glama

msgraph-mcp

A Model Context Protocol (MCP) server that gives AI assistants full access to Microsoft Outlook email and calendar through the Microsoft Graph API. Built on FastMCP, it supports delegated authentication via device-code flow and can run locally or in a framework-managed cloud environment.

Features

26 tools across mail, calendar, contacts, and scheduling:

Mail

  • Read — list folders, messages (with since time filter and conversation_id threading), search (OData $search), attachments (inline base64 for files under 1.5 MB)

  • Compose — send, reply, reply-all, forward with dry-run preview by default

  • Drafts — create, update, attach files, then send when ready

  • Organize — mark read/unread, flag, categorize, move to folder, soft- or hard-delete

  • Bulk — filtered operations (delete, mark read/unread, move) with dry-run preview; scans the entire folder by default, or pass limit to cap how many messages are scanned

  • Folders & aliases — create mail folders, list send-from addresses

Calendar

  • Read — list calendars, events (default window: yesterday through 14 days out), full event details

  • Write — create, update, delete/cancel events with attendees, body, location, all-day support

  • Shared calendars — full read/write access to other users' calendars via user_id parameter

  • Scheduling — check free/busy status for multiple users, or let Graph suggest optimal meeting times

  • Responses — accept, decline, or tentatively accept meeting invitations

Contacts

  • People search — resolve display names to email addresses using the People API

Authentication

  • Device-code flow — interactive three-step auth (start_auth → user approves → finish_auth)

  • Multi-account — cache and switch between multiple Microsoft accounts

  • Framework mode — accept pre-authenticated tokens via environment variables for serverless deployments

Related MCP server: m365-mcp-server

Tool reference

Area

Tool

Description

Auth

auth_status

Show configuration and cached accounts

Auth

start_auth

Begin device-code flow (returns URL + code)

Auth

finish_auth

Complete device-code flow after user approval

Mail

list_folders

List mail folders with item/unread counts

Mail

list_messages

List messages in a folder (limit 1000; since/until window, flag_status and category filters, fields override; results carry conversation_id, recipients, flag_status, categories)

Mail

get_message

Full message details including body

Mail

sync_messages

Delta sync: what changed in a folder since a token, including deletions

Mail

search_messages

Search via OData $search (limit 1000; conversation_id in results)

Mail

get_attachments

List attachment metadata, or download one by attachment_id

Mail

send_message

Send a new email (dry-run by default)

Mail

reply_to_message

Reply or reply-all (dry-run by default)

Mail

forward_message

Forward a message (dry-run by default)

Mail

create_draft

Create a draft without sending

Mail

manage_draft

Update or send an existing draft

Mail

add_attachment_to_draft

Attach a file to a draft

Mail

update_message

Mark read/unread, flag, or categorize

Mail

move_message

Move to a folder (supports well-known names)

Mail

delete_message

Soft-delete or permanently delete

Mail

bulk_manage_messages

Bulk filtered actions with dry-run (whole folder by default; received_after/received_before bound it server-side; recipient_contains, flag_status, category filters; delete/move need a confirm_token)

Mail

create_folder

Create a new mail folder

Mail

list_aliases

List email aliases / send-from addresses

Calendar

list_calendars

List calendars (own or shared via user_id)

Calendar

list_events

List events in a time range (limit 100; needs a timezone for offsetless times)

Calendar

get_event

Full event details with attendees

Calendar

create_event

Create an event or recurring series (dry-run by default; repeat/repeat_days/repeat_count; needs a timezone for offsetless times)

Calendar

update_event

Update an existing event (dry-run by default; needs a timezone for offsetless times)

Calendar

delete_event

Delete or cancel an event (dry-run by default)

Calendar

respond_to_event

Accept, decline, or tentatively accept

Calendar

check_availability

Free/busy lookup or meeting time suggestions (needs a timezone for offsetless times)

Contacts

search_people

Search contacts by name (limit 50; returns job_title)

Prerequisites

  • Python 3.11+

  • An Azure app registration with delegated Microsoft Graph permissions (see below)

  • uv (recommended) or pip

Azure app registration

Create an app registration in Microsoft Entra admin center (Azure AD).

1. Supported account types

Choose one:

  • Accounts in this organizational directory only — single tenant

  • Accounts in any organizational directory — multi-tenant work/school accounts

2. Authentication

  • Enable Allow public client flows (required for device-code flow)

3. API permissions

Add delegated Microsoft Graph permissions:

Permission

Purpose

User.Read

Read signed-in user profile

Mail.ReadWrite

Read, move, flag, categorize, delete mail

Mail.Send

Send mail, reply, forward

Calendars.ReadWrite

Read and write calendar events

Calendars.ReadWrite.Shared

Access shared / delegated calendars

People.Read

Search contacts by name

Calendars.ReadWrite.Shared is the widest permission here. With it, every calendar tool accepts a user_id and can create, modify, delete, or cancel events on any calendar the signed-in user has been granted access to — cancelling someone else's meeting emails all of its attendees. There is no allowlist of targetable users; authorization rests entirely with Graph. Drop this scope if you don't need it. See SECURITY_REVIEW.md.

For read-only use, replace Mail.ReadWrite and Mail.Send with Mail.Read, and Calendars.ReadWrite / Calendars.ReadWrite.Shared with Calendars.Read, and set MICROSOFT_SCOPES to match.

Scope-based tool registration: tools are exposed to the client only when a scope that satisfies them is present in MICROSOFT_SCOPES. A read-only scope set never advertises delete_message, send_message, create_event, etc. — the model can't attempt actions the token could not perform. With the default (full) scope set, all tools are available. The auth tools (auth_status, start_auth, finish_auth) are always registered so you can authenticate before any scope is granted.

Grant admin consent for the tenant if required by your organization's policies.

Configuration

Copy .env.example to .env and fill in your values:

cp .env.example .env

Variable

Default

Description

MICROSOFT_CLIENT_ID

(required)

Azure app registration client ID

MICROSOFT_TENANT_ID

common

organizations (work/school only), common (any), or a specific tenant GUID

MICROSOFT_SCOPES

User.Read Mail.ReadWrite Mail.Send Calendars.ReadWrite Calendars.ReadWrite.Shared People.Read

Space-separated delegated permissions

MICROSOFT_TOKEN_CACHE_PATH

.data/msal_token_cache.json

Path to the local MSAL token cache

MAX_ATTACHMENT_INLINE_SIZE

1572864

Max attachment size (bytes) for inline base64 (default 1.5 MB)

MAX_LIST_LIMIT

1000

Max items returned by list_messages / search_messages

GRAPH_IMMUTABLE_IDS

(unset)

1 to request Graph's immutable message ids, which survive folder moves. All-or-nothing; flipping it invalidates stored ids

MSGRAPH_DEFAULT_TIMEZONE

(unset)

IANA or Windows zone applied to calendar times written without a UTC offset. Unset → such times are refused, not guessed

Recommended tenant values:

  • organizations — work/school accounts only (most common for enterprise)

  • A specific tenant GUID — locks authentication to a single organization

  • common — any Microsoft account (work, school, or personal)

Deployment

Local (stdio)

The default transport is stdio, suitable for desktop MCP clients like Claude Code, Claude Desktop, Cursor, and VS Code.

# Install dependencies
uv sync

# Run the server
uv run msgraph-mcp

Or with pip:

pip install -e .
msgraph-mcp

MCP client configuration

Add to your MCP client's configuration (e.g. Claude Desktop claude_desktop_config.json, .mcp.json for Claude Code, etc.):

{
  "mcpServers": {
    "msgraph-mcp": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "msgraph-mcp"],
      "env": {
        "MICROSOFT_CLIENT_ID": "your-client-id",
        "MICROSOFT_TENANT_ID": "your-tenant-id"
      }
    }
  }
}

If you use a .env file in the project directory, the env block can be omitted.

Cloud — AWS Lambda with mcp-lambda-wrappers (ChatGPT, Claude.ai)

For use with remote MCP clients like ChatGPT and Claude.ai, this server can be deployed as a serverless AWS Lambda function using mcp-cloud-wrappers. That framework wraps any stdio-based MCP server behind Amazon Bedrock AgentCore Gateway with full OAuth 2.0 and Dynamic Client Registration (RFC 7591) support — no code changes required in this project.

What the framework provides:

  • Serverless deployment — runs this MCP server as a Lambda subprocess behind AgentCore Gateway

  • Per-user OAuth — each user authenticates with their own Microsoft account; tokens are stored in AWS Secrets Manager with automatic refresh

  • Caller authentication — Cognito JWT validation for all inbound requests

  • Dynamic Client Registration — MCP clients (ChatGPT, Claude.ai) self-register via a standard /register endpoint

  • Zero idle cost — Lambda functions spin up on demand

How it works:

  1. An MCP client sends a tool call to the AgentCore Gateway endpoint

  2. The framework validates the caller's JWT, extracts their identity, and loads their Microsoft Graph OAuth token from Secrets Manager

  3. The token is injected as GRAPH_ACCESS_TOKEN into this server's environment

  4. This server runs as a subprocess, reads the token, and executes the tool against Microsoft Graph

  5. If the user hasn't authenticated yet, start_auth returns the framework's OAuth URL instead of a device code

This project is used as the reference example service in mcp-lambda-wrappers — see infra/lambda/services/msgraph/ in that repo for the full configuration.

Quick deploy (from the mcp-lambda-wrappers repo):

# One-time: deploy shared infrastructure (Cognito, DCR, OAuth callback)
make deploy-shared

# Create the Azure app secret
aws secretsmanager create-secret \
  --name mcp-wrappers-msgraph-service-secrets \
  --secret-string '{"MICROSOFT_CLIENT_ID": "your-client-id"}'

# Generate tool definitions and deploy
make gen-tools SERVICE=msgraph
make deploy-service SERVICE=msgraph

Framework environment variables

When running inside the framework, this server auto-detects Lambda mode via these injected environment variables:

Variable

Description

GRAPH_ACCESS_TOKEN

Pre-authenticated Microsoft Graph access token (per-user)

OAUTH_AUTHENTICATED

Set to true when auth is complete

OAUTH_USER_ID

Authenticated user identifier

OAUTH_AUTH_URL

OAuth authorization URL (shown when user needs to authenticate)

SERVICE_NAME

Service identifier for the framework

In this mode:

  • The MSAL device-code flow is bypassed — tokens are injected by the framework

  • No local token cache is used (compatible with read-only filesystems like Lambda's /var/task)

  • auth_status reports the framework-managed token state

  • start_auth / finish_auth return guidance to authenticate through the framework's OAuth flow instead

Docker

While no Dockerfile is included, the server can be containerized:

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir .
ENV MICROSOFT_CLIENT_ID=""
ENV MICROSOFT_TENANT_ID="organizations"
EXPOSE 8000
CMD ["msgraph-mcp"]

For persistent authentication, mount a volume for the token cache:

docker run -v msgraph-data:/app/.data \
  -e MICROSOFT_CLIENT_ID=your-id \
  -e MICROSOFT_TENANT_ID=your-tenant \
  msgraph-mcp

Safety defaults

Write operations default to safe behavior:

Feature

Default

Notes

send_message

dry_run=True

Creates a temporary draft for preview, then deletes it

reply_to_message

dry_run=True

Preview before sending

forward_message

dry_run=True

Preview before sending

bulk_manage_messages

dry_run=True

Shows matches without executing; delete/move also need a confirm_token

delete_message

permanent=False

Moves to Deleted Items (recoverable)

create_event

dry_run=True

Graph mails invitations immediately, so preview first

update_event

dry_run=True

Preview shows current state next to the proposed changes

delete_event

dry_run=True

Preview names the event and says whether attendees are notified

respond_to_event is deliberately not gated — accepting or declining is reversible by responding again, so a confirmation step would be friction with no safety payoff.

Calendar times and timezones

Graph's dateTimeTimeZone pairs a naive wall-clock string with a separate zone name, so how a time is written matters:

Input

Sent to Graph

2026-08-01T14:00:00-04:00

{"dateTime": "2026-08-01T18:00:00", "timeZone": "UTC"} — the instant is unambiguous, so it is converted

2026-08-01T14:00:00 + timezone="America/New_York"

{"dateTime": "2026-08-01T14:00:00", "timeZone": "America/New_York"} — handed to Graph unconverted, which keeps recurring events correct across DST

2026-08-01T14:00:00, no zone anywhere

Refused

That last row is deliberate. Reading a bare local time as UTC is how a 2pm Eastern meeting silently becomes 10:00 EDT, with invitations already sent — and on an MCP server the caller is usually a model turning "book me 2pm Thursday" into exactly that string. Set MSGRAPH_DEFAULT_TIMEZONE if you want a server-side default instead of passing timezone per call.

The same rule applies to reads — list_events refuses an offsetless window rather than shifting it silently by your offset. One difference: a $filter compares instants, so an offsetless read time is resolved through its zone to UTC, whereas a write hands Graph the wall-clock time plus the zone name.

All-day events are a separate contract: Graph wants midnight in the stated zone, so the calendar date is preserved and the instant is not. 2026-04-01T23:00:00-04:00 with is_all_day=True books April 1, not April 2.

Confirming a bulk delete or move

Destructive bulk actions are two-step. The dry run returns a confirm_token derived from the ids it actually matched:

// 1. preview
{"action": "delete", "dry_run": true}
// -> {"matched": 42, "confirm_token": "42-b7e2d4a1c3f9", "matches": [...]}

// 2. act
{"action": "delete", "dry_run": false, "confirm_token": "42-b7e2d4a1c3f9"}

The live run rescans and re-derives the token from its own results. If the mailbox changed in between, it refuses rather than acting on a set you never saw:

confirm_token does not match the current scan: it described 42 message(s),
this scan matched 43. The mailbox is live, so re-check the preview before
acting. Confirm with the new token: 43-9f1c2ae5b7d0

Nothing is stored server-side — the token is recomputed each time — so this works unchanged across a Lambda cold start between the two calls.

Bulk scan semantics

bulk_manage_messages scans newest-first and applies its filters client-side. By default (limit=None) it scans the entire folder, so "find/act on all messages matching X" returns a true total. Pass limit=N to scan at most the newest N messages; the value is honored exactly (paged internally at up to 1000/request), never silently clamped. The response reports coverage explicitly:

Field

Meaning

scanned

Distinct messages inspected this run

matched

How many passed the filters

total_in_folder

Folder size, as a scale anchor

truncated

False only when the scan reached the folder end (count is a true total)

stop_reason

folder_exhausted | window_exhausted | scan_limit_reached | cursor_stalled

acted / already_gone / failed

Per-message outcomes of a live (non-dry-run) run

Follow-up flags and categories

flag_message and categorize_message could always write these; nothing read them back, so an agent could flag a message and then had no way to report which messages were flagged. Both are now on every list and detail result (flag_status, categories) and appear in the generated summary string.

list_messages(flag_status="flagged") filters server-side, so "show me my flagged mail" is one request. Two constraints worth knowing, both confirmed against the live API rather than inferred:

  • Exchange rejects a flag restriction combined with a sort — the pairing returns "The restriction or sort order is too complex for this operation." So $orderby is dropped for these queries and the rows are re-sorted client-side to keep the newest-first contract.

  • Because the server chose those rows unsorted, limit selects an arbitrary subset rather than the newest N. Raise limit above your expected flagged count, or narrow with since/until — a date clause does combine with the flag filter.

Categories behave differently, and better. Exchange accepts a category restriction with a sort, so category is pushed server-side on both list_messages and bulk_manage_messages, keeps newest-first ordering, and imposes none of the limit caveat above. Filtering a 50k-message folder by category reads only the matching rows — a live check scanned 1 message rather than the folder.

bulk_manage_messages filters flags client-side, since its cursor pagination depends on the receivedDateTime sort a flag restriction would force it to drop. Category names are escaped as OData string literals, so a name containing a quote (Bob's stuff) is handled rather than breaking the filter.

Date windows

received_after and received_before are applied by Graph, not after the fetch, so a date-scoped query reads only its window. This is what makes old mail cheap to reach: without a bound, "what did this sender send me last March" pages the entire folder to match a handful of rows. On a 50k-message mailbox that is the difference between ~50 round-trips and one.

The upper bound needs no extra machinery — paging already anchors on receivedDateTime le, so received_before is the starting cursor and the scan begins inside the window rather than at the newest message.

When either bound is given, stop_reason is window_exhausted rather than folder_exhausted. That distinction is deliberate: the scan covered all of what you asked for, but not all of the folder, and reporting the latter would overclaim.

The mailbox is live, so counts are a point-in-time snapshot: re-running may legitimately see a different set. Collection and action are separate phases — messages moved or deleted (e.g. by a rule) between the two are reported as already_gone, not errors. truncated=True means matches may exist deeper than this call reached; it is not safe to read a converged count of 0 as "the folder is clean."

Security

  • Path segment validation — all user-supplied IDs are validated against a safe-character pattern before URL interpolation, blocking path traversal

  • Next-link hardening — pagination only follows HTTPS URLs on the configured Graph host

  • Search sanitization — double-quotes stripped from OData $search queries

  • Retry with backoff — automatic retry for HTTP 429 and transient 5xx errors (3 attempts, respects Retry-After)

  • Error translation — raw Graph API payloads are never exposed to callers

  • Token cache permissions — cache file 0600, parent directory 0700, symlinks rejected

See SECURITY_REVIEW.md for the full threat model and remaining risks.

Development

# Install with dev dependencies
uv sync --dev

# Run tests
uv run pytest

# Or with pip
pip install -e '.[dev]'
pytest

CI runs the suite on Python 3.11–3.14 for every push and pull request to main (.github/workflows/ci.yml).

Two guardrails run as part of the ordinary test suite:

  • tests/conftest.py blocks real outbound HTTP. Tests that need it patch GraphClient; anything that reaches httpx fails loudly. This exists because a test once made a live Graph call against a developer's cached token.

  • tests/test_docs_parity.py asserts the tool tables in README.md and AGENTS.md match the registered tools exactly, in both directions. Adding a tool without documenting it — or documenting one that does not exist — fails the build.

Smoke test harness

A CLI harness for manual testing without a full MCP client:

# Auth
python3 scripts/smoke_test.py status
python3 scripts/smoke_test.py start-auth
python3 scripts/smoke_test.py finish-auth
python3 scripts/smoke_test.py list-accounts

# Mail
python3 scripts/smoke_test.py list-folders
python3 scripts/smoke_test.py list-messages --folder inbox --limit 5
python3 scripts/smoke_test.py get-message MESSAGE_ID
python3 scripts/smoke_test.py search-messages "search term"
python3 scripts/smoke_test.py mark-message-read MESSAGE_ID
python3 scripts/smoke_test.py move-message MESSAGE_ID archive
python3 scripts/smoke_test.py delete-message MESSAGE_ID
python3 scripts/smoke_test.py bulk-manage-messages --sender-contains "newsletters" --limit 50

# Calendar
python3 scripts/smoke_test.py list-calendars
python3 scripts/smoke_test.py list-events --limit 10
python3 scripts/smoke_test.py get-event EVENT_ID

License

See LICENSE for details.

Available Tools

29 tools
add_attachment_to_draftC

Attach a file (base64-encoded) to a draft message.

Inputs: name (required), message_id (required), content_base64 (required), account_id, +1 more. Returns: object

auth_statusA

Show current Microsoft auth configuration and cached accounts.

When running inside the MCP Lambda wrapper framework, reports framework-managed authentication status instead of local MSAL state.

Inputs: no named parameters. Returns: object

bulk_manage_messagesA

Bulk-manage messages with filtering. Supports dry-run, delete, move, and mark-read actions.

By default (limit=None) this scans the entire folder newest-first, so "find/act on all messages matching X" sees the whole folder, not a window. Pass limit only to cap how many messages are scanned (e.g. limit=200 scans at most the newest 200); the value is honored exactly, never silently clamped. Filtering is client-side.

The response reports coverage honestly: truncated is False only when the scan reached the end of the folder (stop_reason="folder_exhausted"), so a match count is a true total; if limit cut the scan short it is True. The mailbox is live, so counts are a point-in-time snapshot. Collection and action are separate phases; messages moved or deleted between them are reported as already_gone, not errors.

Inputs: limit, action, folder, dry_run, +6 more. Returns: object

check_availabilityB

Check calendar availability. mode='free_busy': get free/busy schedule. mode='suggest': suggest meeting times.

Inputs: emails (required), end_iso (required), start_iso (required), mode, +2 more. Returns: result

create_draftB

Create a draft email (saved to Drafts folder). Use this when you need to add attachments before sending, or want to build a message over multiple steps. Then use manage_draft to send.

Inputs: to (required), body (required), subject (required), cc, +3 more. Returns: object

create_eventB

Create a new calendar event. Pass user_id for shared calendars.

Inputs: end_iso (required), subject (required), start_iso (required), body, +6 more. Returns: object

create_folderB

Create a new mail folder, optionally under a parent folder.

Inputs: name (required), account_id, parent_folder_id. Returns: object

delete_eventB

Delete or cancel a calendar event. Pass user_id for shared calendars.

Inputs: event_id (required), user_id, account_id, cancel_message. Returns: object

delete_messageC

Delete a message or move it to Deleted Items. Requires Mail.ReadWrite permission.

Inputs: message_id (required), permanent, account_id. Returns: object

finish_authA

Complete Microsoft authentication after user approval.

In Lambda mode, this is not needed — call auth_status instead. In local mode, completes the device-code flow.

Inputs: no named parameters. Returns: object

forward_messageA

Forward a message. Defaults to dry-run (preview only). Set dry_run=False to actually send.

Inputs: to (required), message_id (required), body, dry_run, +2 more. Returns: object

get_attachmentsA

Get attachments for a message. Without attachment_id: list all metadata. With attachment_id: download that attachment (base64 if under 1.5 MB).

Inputs: message_id (required), account_id, attachment_id. Returns: result

get_eventB

Get full details for a specific calendar event. Pass user_id for shared calendars.

Inputs: event_id (required), user_id, account_id. Returns: object

get_messageB

Get a full read-only view of a specific mail message.

Inputs: message_id (required), account_id. Returns: object

list_aliasesC

List email aliases (send-from addresses) for the authenticated user.

Inputs: account_id. Returns: object

list_calendarsB

List readable calendars. Pass user_id for shared calendars (e.g. another user's email or object ID).

Inputs: user_id, account_id. Returns: result

list_eventsC

List events from a calendar in a time range. Pass user_id for shared calendars.

Inputs: limit, end_iso, user_id, start_iso, +2 more. Returns: result

list_foldersA

List mail folders. Pass parent_folder_id to list subfolders (check child_folder_count to know if subfolders exist).

Inputs: account_id, include_hidden, parent_folder_id. Returns: result

list_messagesA

List recent mail messages from a folder, newest first.

Pass since (ISO-8601) to filter server-side to messages received at or after that time. Pass fields to override the selected columns (id is always included) for a leaner or extended payload. Each summary includes conversation_id for threading without a follow-up fetch.

Inputs: limit, since, fields, folder, +1 more. Returns: result

manage_draftA

Update and/or send a previously created draft. Provide fields to update, set send=True to send. Use after create_draft and optionally add_attachment_to_draft.

Inputs: message_id (required), cc, to, bcc, +5 more. Returns: object

move_messageC

Move a message to another folder. Requires Mail.ReadWrite permission.

Inputs: message_id (required), destination (required), account_id. Returns: object

reply_to_messageA

Reply to a message. Defaults to dry-run (preview only). Set dry_run=False to actually send.

Inputs: body (required), message_id (required), dry_run, send_as, +2 more. Returns: object

respond_to_eventC

Respond to a meeting invite. Pass user_id for shared calendars.

Inputs: event_id (required), response (required), message, user_id, +1 more. Returns: object

search_messagesC

Search messages using Microsoft Graph message search.

Inputs: query (required), limit, account_id. Returns: result

search_peopleB

Search for people by name to find their email addresses.

Inputs: query (required), limit, account_id. Returns: result

send_messageA

Compose and send an email (no attachments). Defaults to dry-run (preview only). Set dry_run=False to actually send. To send with attachments, use create_draft + add_attachment_to_draft + manage_draft instead.

Inputs: to (required), body (required), subject (required), cc, +4 more. Returns: object

start_authA

Start Microsoft authentication.

In Lambda mode, returns an OAuth authorization URL. In local mode, starts the device-code flow.

Inputs: no named parameters. Returns: object

update_eventC

Update an existing calendar event. Pass user_id for shared calendars.

Inputs: event_id (required), body, end_iso, subject, +6 more. Returns: object

update_messageB

Update message properties: read/unread, follow-up flag (flagged/complete/notFlagged), or color categories.

Inputs: message_id (required), is_read, account_id, categories, +1 more. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 29 tools

    Disambiguation5/5

    Each tool targets a distinct operation (get vs list vs create vs delete vs reply vs forward, etc.) within clear subdomains (mail, calendar, auth, people). No two tools serve the same purpose, and descriptions clarify subtle differences like send_message vs reply_to_message.

    Naming Consistency5/5

    Virtually all tools follow a consistent verb_noun snake_case pattern (e.g., list_messages, create_event, delete_message). The only slight deviation is auth_status (no verb), but it's a common idiom for status checks and does not break the overall pattern.

    Tool Count4/5

    With 29 tools, the set is slightly large but well-justified by the breadth of the Microsoft Graph API (mail, calendar, people, auth). Each tool fills a specific need, and no tools feel redundant. It's just above the typical well-scoped range (3-15) but still cohesive.

    Completeness4/5

    The toolset covers the core CRUD operations for mail and calendar, plus search, bulk actions, and check availability. Minor gaps exist (e.g., sending attachments directly without draft flow, no contact management beyond search), but these are acceptable workarounds for the common use case.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      C
      quality
      D
      maintenance
      A powerful MCP server that enables AI assistants to interact with Microsoft Graph API for managing Outlook emails, Calendar events, OneDrive files, and Contacts through natural language commands.
      35
      56
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      A production-ready MCP server that provides secure, delegated access to Microsoft 365 services including Email, SharePoint, OneDrive, and Calendar. It enables AI models to search messages, browse files, manage calendar events, and parse document contents using OAuth 2.1 authentication.
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      MCP server for any Microsoft Exchange / OWA deployment. Gives LLM agents access to email, calendar, directory search, folders, availability, and meeting analytics via 30 tools.
      30
      43 PyPI
      12
      MIT