ms-graph-mcp
An MCP server that signs in as you with delegated Microsoft Graph permissions, giving an AI assistant tools to read and manage your Microsoft 365 data.
Mail: list/search/read messages and threads, inspect folders and attachments, send/reply/forward, draft, move, mark read/unread, delete.
Calendar: list calendars/events, get event details, check free/busy, find meeting times, create/update/delete events, respond to invitations.
OneDrive/files: browse/search/recent/shared items, read metadata and text, parse .docx/.xlsx/.pptx/.pdf, create folders, upload text, delete, create/resolve sharing links.
Tasks and contacts: manage Microsoft To Do lists/tasks and Outlook personal contacts.
Microsoft Search: search across mail, files, sites, events and chats, subject to enabled scopes.
Optional groups (per README): Planner, Teams chat/channels, SharePoint, Entra directory lookups and Intune device inventory.
Auth/session: browser PKCE or device sign-in, login/session/consent status, incremental scopes.
Direct Graph: arbitrary Graph requests (GET always; writes opt-in), inspect resource shapes, list enabled groups/scopes/permissions.
Guardrails: read-only mode, host pinning, encrypted token cache, paging/truncation and model-shaped projections.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ms-graph-mcp what meetings do I have on my calendar today?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ms-graph-mcp
A Model Context Protocol server that gives an AI assistant access to your own Microsoft 365 data — Outlook mail and calendar, OneDrive, SharePoint, To Do, Planner, Teams chat, Entra ID and Intune — through the Microsoft Graph API. It runs locally over stdio, signs in as you with delegated permissions, and stores its token cache encrypted on your machine. (Why an MCP server)
Published on npm as @devyhan/ms-graph-mcp.
Not affiliated with Microsoft. This is an independent community project. Microsoft, Microsoft 365, Microsoft Graph, Outlook, OneDrive, SharePoint, Teams, Entra and Intune are trademarks of the Microsoft group of companies, used here only to describe what this software connects to.
Features
112 tools across 14 groups — mail, calendar, files, chat, teams, tasks, contacts, search, directory and device inventory, each enabled independently
Delegated sign-in only — acts as you, with your permissions; the token never leaves the server process or enters the conversation
Incremental consent — a bare login asks for the minimal set, and each tool call authenticates with just the scopes it needs
Model-shaped responses — Graph results are projected down to sender, timestamp and plain text with paging cursors, not raw markup
Document parsing —
.docx,.xlsx,.pptxand.pdfextracted as unit rows from OneDrive, SharePoint, Teams or mail attachmentsGuardrails — host-pinned generic requests,
--read-onlymode, opt-in generic writes, retry withRetry-AfterDiscovery mode —
--discoverycollapses the catalogue todiscover_tools+call_toolso clients pay for schemas only on use
Related MCP server: Microsoft Graph MCP
Install with one prompt
Paste this to your AI assistant, replacing the two IDs with your Entra app registration's values:
https://github.com/devyhan/ms-graph-mcp
Install the MCP above.
• CLIENT_ID: <APPLICATION_ID>
• TENANT_ID: <DIRECTORY_ID>Find both IDs in the Microsoft Entra admin center under App registrations → your app → Overview: Application (client) ID and Directory (tenant) ID. The app must allow public client flows with an http://localhost redirect URI (under Authentication → Mobile and desktop applications), and an administrator must grant consent for the Graph permissions it requests.
Quick start
You need Node.js 22 or later — 22.23.4+, 24.22.1+, or 26.12.1+ (the October 2026 security release; 20.x is end-of-life and unpatched). Sign in once, in a terminal:
npx @devyhan/ms-graph-mcp loginThe server is meant to sign in through a shared multi-tenant Entra application published by this project — except that the shared registration is not published yet (SHIPPED_CLIENT_ID in src/config.ts is still empty), so until it ships, register an application of your own and name it on each command:
npx @devyhan/ms-graph-mcp login --client-id <your-client-id>Register your own Entra application has the steps.
Your system browser opens on the Microsoft sign-in page; approve the permissions it asks for and the terminal finishes on its own. On a machine with no browser to open (SSH, container, headless server), use login --auth-flow device and sign in from another device. Then check it worked:
npx @devyhan/ms-graph-mcp statusstatus prints the application in use and where it came from, the resolved account, and the scopes this configuration may request. How the flows, consent, and AADSTS errors work is in Authentication and consent.
Configuration
The fastest path is the Claude Code plugin — no config file to edit:
/plugin marketplace add devyhan/ms-graph-mcp
/plugin install ms-graph-mcp@devyhanFor other clients, add an MCP server entry (here with the work preset):
{
"mcpServers": {
"microsoft-graph": {
"command": "npx",
"args": ["-y", "@devyhan/ms-graph-mcp", "--preset", "work"]
}
}
}Every setting is also an MS365_MCP_* environment variable or a command-line flag. See Configuration for the plugin settings, per-client files, and every command and flag.
Tools
112 tools across 14 groups. --read-only removes every write tool, leaving 81. Enable only the groups you need.
Group | Product | Tools |
| Entra ID profile, Outlook mailbox settings | 2 |
| Outlook mail | 15 |
| Outlook calendar | 9 |
| OneDrive | 13 |
| Microsoft To Do | 8 |
| Microsoft Planner | 9 |
| Outlook contacts | 6 |
| Teams one-to-one and group chats | 7 |
| SharePoint sites, libraries, lists | 11 |
| Microsoft Search across mail, files, sites | 1 |
| Teams teams, channels, channel messages | 10 |
| Entra ID user and group lookups | 9 |
| Intune managed device inventory | 6 |
| Signing in, any Graph endpoint, introspection | 6 |
* Needs --org-mode: these groups request scopes only a tenant administrator can consent to.
The full reference — scopes, admin-consent flags, and every tool with its arguments — is in Tools. Under --discovery the client sees six tools instead: discover_tools and call_tool plus the session, sign-in and permission probes.
Usage examples
"Summarize the reply chain about the Q3 invoice."
Search first, then read the whole thread in one call instead of looping mail_get_message:
{ "query": "Q3 invoice", "after": "2026-09-01" }{ "fromMessageId": "<id from the search>" }"Find a 30-minute slot with Ana and Ben next week."
{
"attendees": ["ana@contoso.com", "ben@contoso.com"],
"durationMinutes": 30,
"start": "2026-10-19T09:00:00",
"end": "2026-10-23T18:00:00",
"timeZone": "UTC"
}More per-group examples — files, chat, search, Planner, SharePoint, Teams, directory, Intune, and the generic escape hatch — are in Usage examples.
Instructions for AI agents
If you are an AI assistant working with this server, follow Instructions for AI agents: call session_status once first, sign in with auth_begin_login plus auth_login_status when needed, orient via discover_tools, and read the Hint and Blocked-by lines on errors.
Troubleshooting
Something failing? Start with Troubleshooting — the common failures and their fixes — then Authentication and consent for AADSTS error codes.
Security
Delegated permissions only. The server acts as you, holds no client secret, and the token never enters the conversation.
Host pinning.
graph_requestpaths and every@odata.nextLinkare checked against the Graph hostname before the request goes out.Read-only mode.
--read-onlyremoves write tools from the catalogue; generic writes need--allow-generic-writeon top.Encrypted token cache. The refresh token lives AES-256-GCM-encrypted in your config directory (
--cache-diroverrides).
Details: Security. To report a vulnerability privately, see SECURITY.md.
What this is not
This is a local, delegated-permission MCP server. It acts as you, with the permissions you have, over a stdio transport on your own machine. It is not a replacement for Microsoft's own Agent 365 MCP servers, which are remote, run inside Microsoft's tenant boundary, use application permissions and admin-managed agent identities, and require a Microsoft 365 Copilot licence. If you need agent identity, tenant-wide governance, or auditing that a tenant administrator controls, that is the product to look at.
It is also not a general Microsoft Graph client library. The tools cover the common read and write paths for each product area and project their results down to what a model can usefully read; graph_request exists for everything else, deliberately behind a flag.
Development
Requires Node.js 22 or later — 22.23.4+, 24.22.1+, or 26.12.1+ (20.x is end-of-life and unpatched).
git clone https://github.com/devyhan/ms-graph-mcp.git
cd ms-graph-mcp
npm install
npm testCommand | What it does |
| Builds, then runs the suite. This is the one to run. |
| Types only, no emit. |
| Compile to |
| Checks scopes and paths against live Microsoft data. Needs the network. |
The test suite needs no Microsoft account, no tenant and no network — every HTTP call is stubbed. npm run contract is separate on purpose and runs weekly in CI instead. See CONTRIBUTING.md for the code layout, the rules for adding a tool, and how to report a bug. Changes are tracked in CHANGELOG.md.
Licence
MIT — see LICENSE.
Available Tools
59 toolsauth_begin_loginStart signing in to Microsoft 365ARead-onlyIdempotent
Starts OAuth 2.0 browser sign-in to Microsoft 365 using authorization code + PKCE. The system browser opens on this machine and the tool returns immediately while the user signs in there; call auth_login_status afterward to see whether they finished. Calling it again while a sign-in is pending reuses the same attempt instead of opening another browser window. Use it when a tool reports that there is no session, and again with scopes when a tool reports missing consent: sign-in is incremental, so a bare call asks only for the minimal set and each repeat consents to just what was added. Pass mode: "full" to consent up front to the rest instead, minus what an ordinary user cannot grant. Never ask the user to paste credentials or an authorization code into the conversation. No token is ever returned.
Inputs: mode, scopes.
auth_login_statusCheck the sign-inARead-onlyIdempotent
Reports where the browser sign-in started by auth_begin_login has got to: "pending" (the user has not finished), "signedIn" (with the account), "failed" (with the reason), or "none" (no sign-in was started). When no browser sign-in was started in this session it falls back to the cached account, so an existing CLI session reports "signedIn" rather than "none". Never returns a token. Poll it rather than waiting: this returns immediately either way.
Inputs: no named parameters.
calendar_create_eventCreate a calendar eventA
Creates an event on the primary calendar and returns the created event. Listing attendees sends them a meeting invitation immediately, so confirm the details with the user first. A start or end without an explicit UTC offset is treated as local time in timeZone (UTC when that is omitted).
Inputs: end (required), start (required), subject (required), body, +6 more.
calendar_delete_eventDelete a calendar eventADestructive
Deletes an event, moving it to Deleted Items. If the signed-in user organized a meeting, every attendee is sent a cancellation; if they were only invited, this removes it from their calendar without telling the organizer — use calendar_respond_event with decline for that. Deleting a series master deletes every occurrence.
Inputs: id (required).
calendar_find_meeting_timesFind meeting timesARead-onlyIdempotent
Suggests meeting slots that work for the signed-in user and the given attendees, ranked by confidence, with the attendees who are unavailable in each slot. Returns 10 suggestions by default. Read-only despite being an HTTP POST. All attendees are treated as required, and an empty result comes back with emptySuggestionsReason explaining why (commonly "AttendeesUnavailable" or a window that is too narrow).
Inputs: attendees (required), durationMinutes (required), end, start, +2 more.
calendar_get_eventGet a calendar eventARead-onlyIdempotent
Returns one event in full: subject, start, end, location, organizer, the attendee list with each response, the description body (HTML converted to text and capped), recurrence summary, and the Outlook web link. Use calendar_list_events first to find the id.
Inputs: id (required), timeZone.
calendar_get_scheduleGet free/busy scheduleARead-onlyIdempotent
Returns free/busy availability for up to 20 people or rooms over a time window, as an availabilityView string where each character covers one interval (0 free, 1 tentative, 2 busy, 3 out of office, 4 working elsewhere), plus working hours and any visible busy blocks. Read-only despite being an HTTP POST. Subject and location are only returned for people who share those details with you; everything else comes back as a bare busy block.
Inputs: end (required), start (required), schedules (required), timeZone, +1 more.
calendar_list_calendarsList calendarsARead-onlyIdempotent
Lists the signed-in user's calendars with id, name, colour, owner and whether they can be edited or shared. Returns 50 by default. Use the returned id as calendarId in calendar_list_events.
Inputs: top.
calendar_list_eventsList calendar eventsARead-onlyIdempotent
Lists events with subject, start, end, location, organizer and attendee count, plus a nextLink when more pages exist. Returns 25 events by default. Passing both start and end switches to calendarView, which expands recurring meetings into their individual occurrences; without them the raw event list is returned, where a recurring series appears once as its series master. Times without an explicit offset in start/end are read as UTC — pass a timeZone to control how returned times are rendered.
Inputs: end, top, start, select, +3 more.
calendar_respond_eventRespond to a meeting invitationADestructive
Accepts, declines, or tentatively accepts a meeting invitation, optionally with a comment to the organizer. Graph returns no content on success. Only works on events where the signed-in user is an attendee — responding to an event they organized fails. Proposing a new time is not supported.
Inputs: id (required), response (required), comment, sendResponse.
calendar_update_eventUpdate a calendar eventAIdempotent
Patches the given fields on an event and returns the updated event; omitted fields are left alone. Two traps: sending attendees replaces the whole list, so anyone left out is uninvited, and changing the time or attendees of a meeting sends an update to everyone invited. Updating a recurring series master changes every occurrence.
Inputs: id (required), end, body, start, +10 more.
contacts_createCreate a contactA
Creates a personal contact in the signed-in user's default Contacts folder and returns the stored contact, including the new id. Outlook builds displayName from givenName and surname. Outlook holds at most 3 email addresses per contact. Graph does not de-duplicate: calling this twice creates two contacts, so search first when the contact may already exist.
Inputs: givenName (required), surname, jobTitle, companyName, +2 more.
contacts_deleteDelete a contactADestructive
Deletes one personal contact. The contact moves to the Deleted Items folder, so it is recoverable from Outlook but not through these tools, and Graph returns no body — only a confirmation of the id. Deleting an id that is already gone fails with 404 ErrorItemNotFound.
Inputs: id (required).
contacts_getGet a contactARead-onlyIdempotent
Returns one personal contact by id, with names, company, job title, department, every email address, phone numbers, postal addresses, birthday, categories and timestamps. Personal notes are truncated to 1000 characters. Properties Graph reports as empty are omitted, so a missing key means the field is not set. An id from a different mailbox or a deleted contact fails with 404 ErrorItemNotFound.
Inputs: id (required).
contacts_listList contactsARead-onlyIdempotent
Lists personal contacts from the signed-in user's default Outlook Contacts folder, 25 per page by default (max 100). Each row carries id, displayName, given/surname, company, job title, email addresses and phone numbers; pass select for different properties. A nextLink in the result means more pages exist — call again with skip set to the number already seen. Contacts stored in a custom contact folder are not included.
Inputs: top, skip, filter, select, +1 more.
contacts_searchSearch contactsARead-onlyIdempotent
Full-text search over the default Contacts folder using Outlook $search, returning the same compact rows as contacts_list, 25 per page by default. Results come back ranked by relevance, which is why $filter and $orderby cannot be combined with a search and are not offered here — use contacts_list for filtered or sorted results. Search matches whole words and prefixes only: it will not do date ranges, wildcards in the middle of a term, or "not equal" style conditions.
Inputs: query (required), top, skip.
contacts_updateUpdate a contactAIdempotent
Updates the given fields of one personal contact and returns the stored result. Omitted fields are left alone, but emailAddresses is a collection: supplying it REPLACES every address on the contact, so read the contact first and resend the ones to keep. There is no optimistic-concurrency check here — a concurrent edit from Outlook is silently overwritten. At least one field besides id is required.
Inputs: id (required), surname, jobTitle, givenName, +4 more.
files_create_folderCreate a OneDrive folderA
Creates a folder in the signed-in user's OneDrive and returns the created item. Give the parent as parentItemId or parentPath, or omit both for the drive root. A name collision does not fail: Graph renames the new folder with a numeric suffix, so read the returned name rather than assuming it.
Inputs: name (required), parentPath, parentItemId.
files_create_linkCreate a OneDrive sharing linkA
Creates a sharing link for a OneDrive file or folder and returns its URL, permission type and scope. Defaults to a view link scoped to the organization. Anonymous links are blocked by policy in many tenants, which surfaces as a 403: retry with scope organization. Calling this twice with the same type and scope returns the existing link rather than creating a second one.
Inputs: itemId (required), type, scope.
files_deleteDelete a OneDrive itemADestructive
Deletes a file or folder from the signed-in user's OneDrive. Deleting a folder deletes everything inside it. The item goes to the OneDrive recycle bin rather than being destroyed, but this server has no tool to restore it, so confirm the id with files_get_item first.
Inputs: itemId (required).
files_get_drive_itemGet an item from any driveARead-onlyIdempotent
Reads the metadata of one file or folder in ANY drive the signed-in user can reach — not just their own OneDrive — returning the same detail as files_get_item. This is what makes the remoteDriveId/remoteItemId stubs from files_list_shared and files_list_recent readable, and the driveId/itemId from teams_get_channel_files_folder addressable. Needs the Files.Read.All scope, which a user can consent to without an administrator.
Inputs: itemId (required), driveId (required).
files_get_itemGet a OneDrive itemARead-onlyIdempotent
Reads the metadata of one OneDrive file or folder: name, kind, size, mime type, created and last-modified times and authors, parent folder path and ids, and webUrl. Address it by itemId or by path, not both. The pre-authenticated download URL is deliberately not returned; use files_read_text for text contents or files_create_link to share the item.
Inputs: path, itemId.
files_list_childrenList OneDrive folder contentsARead-onlyIdempotent
Lists the direct children of a folder in the signed-in user's OneDrive, returning id, name, kind (file or folder), size, mime type, child count, last-modified time and webUrl per entry, plus count and nextLink when more pages exist. Address the folder by itemId or by path; omit both for the drive root. Page size defaults to 50. This is not recursive: use files_search to look through the whole drive.
Inputs: top, path, itemId, orderby.
files_list_recentList recent OneDrive filesARead-onlyIdempotent
Lists files the signed-in user recently opened or edited, most recent first, with count and nextLink. Defaults to 25 items. Entries can come from other people's drives or from SharePoint; those carry remoteDriveId and remoteItemId, and their plain id will not resolve against your own drive.
Inputs: top.
files_parse_documentParse a document to textARead-onlyIdempotent
Extracts the text of a .docx, .xlsx, .pptx or .pdf as unit ROWS — sections, sheets, slides or pages, never one giant string — plus a manifest accounting for every unit: unitCount, per-unit truncated flags, returnedRange, a reason ("complete" means the document ran out, "maxItems" means units remain), and a cursor to continue with. Give exactly one source: shareUrl (a Teams attachment contentUrl or mail reference-attachment link), driveId+itemId (including remote ids from files_list_shared and teams_get_channel_files_folder), or messageId+attachmentId (a mail file attachment). Spreadsheet formulas come back as their saved values, never evaluated. Friendly errors, never attempts: password/IRM-protected files, scanned PDFs (no OCR here), legacy .doc/.ppt/.xls, files over 16 MiB. Long documents cap at 50 PDF pages and 100k spreadsheet rows per parse.
Inputs: cursor, itemId, driveId, shareUrl, +5 more.
files_read_textRead a text file from OneDriveARead-onlyIdempotent
Returns the contents of a text-like OneDrive file (plain text, Markdown, CSV, JSON, XML, source code, config) as a string, truncated to maxChars, which defaults to 20000. Reads the file metadata first and refuses binary formats — PDFs, images, Office documents, archives, media — with an explanation, because their bytes are not readable as text. For .docx, .xlsx, .pptx and .pdf use files_parse_document, which extracts their text as pages, slides or sheets.
Inputs: itemId (required), maxChars.
files_searchSearch OneDriveARead-onlyIdempotent
Searches the whole of the signed-in user's OneDrive for free text, matching file names and indexed file contents, and returns the same compact fields as files_list_children plus count and nextLink. Page size defaults to 25. This endpoint accepts only a search term: it supports no date range, no file-type filter and no sort order, so narrow the results yourself or list a folder with files_list_children instead. Recently uploaded files are missing until the service has indexed them.
Inputs: query (required), top.
files_upload_textUpload a text file to OneDriveA
Writes a text file to the given path in the signed-in user's OneDrive and returns the stored item. Missing parent folders are created automatically. An existing file at that path is REPLACED, not merged or appended, so read it with files_read_text first if you mean to edit it. Text only, up to 4 MB; larger or binary uploads need a resumable upload session, which this server does not expose.
Inputs: path (required), content (required).
graph_list_permissionsList granted permissionsARead-onlyIdempotent
Reports how this server is configured: which tool groups are enabled, the delegated Microsoft Graph scopes each one uses, which of them need a tenant administrator to consent, whether write tools are suppressed, and the signed-in account. Call it when a request fails with 403 or "insufficient privileges" to see whether the needed area is switched on at all. Reads only the signed-in account from Graph.
Inputs: no named parameters.
graph_requestCall Microsoft Graph directlyARead-onlyIdempotent
Sends an arbitrary request to Microsoft Graph and returns the raw JSON response. Use this only when no purpose-built tool covers the endpoint: the dedicated tools project and trim their results, while this one returns everything Graph sends, which is often far more text. The host is fixed to this tenant's Graph endpoint and path must be relative, so an absolute URL is rejected. GET is always available; the write methods require --allow-generic-write. Pass maxPages to follow @odata.nextLink, and prefer $select and $top in query to keep the response small. The call authenticates with the minimal scope guessed from path, so a path whose scope is granted succeeds even when an unrelated scope is missing; pass scopes to override the guess.
Inputs: path (required), body, query, method, +3 more.
graph_schemaInspect a Graph resource shapeARead-onlyIdempotent
Fetches one item from a Graph path ($top=1) and reports the property names it carries with their JSON types, plus the @odata.context that names the resource type. Use it before graph_request to learn what $select and $filter can reference. It reports the shape of one real item, so properties that happen to be null on that item are absent from the list, and an empty collection yields no properties at all. Read-only.
Inputs: entityPath (required), version.
mail_create_draftCreate a mail draftA
Creates an unsent draft in the Drafts folder and returns its id and webLink. Nothing is transmitted — this tool cannot send, and there is no companion tool that sends an existing draft, so the user must send it from Outlook (or you can use mail_send instead). Recipients are optional on a draft.
Inputs: body (required), subject (required), cc, to, +2 more.
mail_delete_messageDelete a messageADestructive
Deletes a message. This is a soft delete: the message moves to Deleted Items and the user can restore it from Outlook, but this server has no tool to undo it, so confirm with the user first. Deleting a message already in Deleted Items removes it permanently.
Inputs: id (required).
mail_fetch_threadFetch a mail threadARead-onlyIdempotent
Reads a whole reply chain in one call and folds away the text the messages repeat at each other. Use it instead of looping mail_get_message whenever you want two or more messages of one conversation — summarising a thread, finding when something was decided, reconstructing who asked what. Pass fromMessageId (any message id from the thread, and the server finds the rest) or ids (an explicit list, which also works across conversations). Messages come back oldest-first as messages, each with an n you can cite. Where a message quotes text an earlier one already carried, the quote is replaced by a marker like [quoted from #3] or [quoted from #2-#5]; every index named is a message in this same response and always lower than the one citing it, so nothing points outside what you were given. On a measured 24-message Outlook thread this returned 92,000 characters where 24 separate mail_get_message calls returned 381,000, with no unique sentence lost. Read meta.fold to see how much was folded and meta.budget to see whether bodies were cut. reason is "complete" when you have the whole conversation, "maxMessages" when only the newest maxMessages of a longer thread came back, and "maxPages" when the conversation is longer than this tool will page — there is no cursor, because a pointer on page 2 could not name a message on page 1, so narrow with mail_search_messages and pass ids instead. A message tagged bodyRendering:"stripped-html" reached us as HTML and lost every link on the way through the tag stripper; linksLost counts them. Nothing here is marked read, and attachment contents are never included.
Inputs: ids, fold, foldLinks, maxMessages, +3 more.
mail_forwardForward a messageADestructive
Forwards an existing message immediately, attachments included — it does not create a draft and there is no undo, so confirm the recipients with the user first. Your comment, if any, is placed above the forwarded content. Sending needs the Mail.Send permission, which is separate from read/write access.
Inputs: id (required), to (required), comment.
mail_get_attachment_contentGet attachment contentARead-onlyIdempotent
Returns the content of one message attachment. A file attachment under 1 MB comes back as text when it is text-like, or as base64 otherwise; larger files are refused with a pointer at files_parse_document, which reads them through $value instead. A reference attachment (a link, not a file) comes back as its sourceUrl — pass that as shareUrl to files_resolve_share or files_parse_document. Check kind on mail_list_attachments first.
Inputs: id (required), attachmentId (required), maxChars.
mail_get_messageGet one mail messageARead-onlyIdempotent
Returns one message with its body plus sender, recipients, timestamps, importance, conversationId and webLink. The body defaults to plain text and is capped at 8000 characters; raise maxBodyChars for a long thread, or pass format:"preview" to skip the body when you only need to identify the message. Attachment contents are never included — use mail_list_attachments. Reading a message here does not mark it read; use mail_mark_read for that. DO NOT loop this over a reply chain: each message quotes the ones before it, so reading a thread one call at a time returns the same text again and again — on a measured 24-message thread that was 381,000 characters, four times what the same content costs through mail_fetch_thread, which folds the repeats. Use this tool for a single message, and mail_fetch_thread for two or more of one conversation. bodyRendering says where the text came from: "exchange-text" is Exchange's own conversion, "stripped-html" means the tags were removed locally and every link went with them (linksLost counts how many), "html" is raw markup.
Inputs: id (required), format, foldLinks, maxBodyChars.
mail_list_attachmentsList message attachmentsARead-onlyIdempotent
Lists attachment metadata for one message — id, name, contentType, size in bytes, whether it is inline, and its kind (file, item, or reference) — 20 per call by default. File contents are deliberately never returned: a single attachment would exceed the output budget many times over. Check hasAttachments on the message first; inline images count as attachments, so a message with no visible attachment can still list several.
Inputs: id (required), top.
mail_list_foldersList mail foldersARead-onlyIdempotent
Lists the top-level Outlook mail folders with their ids, unread counts and total counts, 50 per call by default. Pass includeChildren to also get one level of subfolders. Only top-level folders are returned otherwise, so a nested folder needs a second call. Use an id from here as folderId in mail_list_messages, though the well-known names (inbox, archive, sentitems) work there without looking anything up.
Inputs: top, includeChildren.
mail_list_messagesList mail messagesARead-onlyIdempotent
Lists Outlook messages newest-first, 10 per call by default (50 max), as a compact projection: id, subject, from, toRecipients, receivedDateTime, isRead, hasAttachments and bodyPreview. The Outlook webLink is NOT included — it is the id again, URL-encoded, and costs more than everything else on the row; pass includeWebLink when you need a clickable link, and use the id for any further mail tool call. Bodies are never included — bodyPreview is Graph's first 255 characters, and mail_get_message returns the rest. Page with skip. Note that Exchange rejects some filter/orderby combinations with "The restriction or sort order is too complex"; when that happens, drop orderby or sort on the same property you filtered.
Inputs: top, skip, filter, select, +4 more.
mail_mark_readMark a message read or unreadAIdempotent
Sets the read flag on one message. Defaults to marking it read; pass isRead:false to mark it unread again. Reading a message with mail_get_message does not change this flag, so unread counts stay accurate until you call this.
Inputs: id (required), isRead.
mail_move_messageMove a message to a folderAIdempotent
Moves a message to another mail folder. The move assigns the message a NEW id — the old one stops resolving, so use the id returned here for any follow-up call. destinationId accepts a folder id from mail_list_folders or a well-known name such as archive or deleteditems; moving to deleteditems is how you soft-delete without mail_delete_message.
Inputs: id (required), destinationId (required).
mail_replyReply to a messageADestructive
Sends a reply to an existing message immediately — it does not create a draft, and there is no undo, so confirm the text with the user first. Your comment is placed above the quoted original as plain text; Graph builds the subject and recipients itself. replyAll includes every original recipient. Sending needs the Mail.Send permission, which is separate from read/write access.
Inputs: id (required), comment (required), replyAll.
mail_search_messagesSearch mail messagesARead-onlyIdempotent
Finds messages by text, returning the same compact projection as mail_list_messages, 10 per call by default (50 max). Two modes, because Graph will not combine them: with no date range it uses $search, a relevance-ranked KQL search over subject, body and participants (results are NOT in date order, and $search supports no date syntax). Passing after or before switches to $filter on receivedDateTime, which sorts newest-first but can only match query as a substring of the subject — body text is not searched in that mode. The response reports which mode ran. To search body text within a period, search first and filter the dates yourself.
Inputs: query (required), to, top, from, +4 more.
mail_sendSend mailADestructive
Sends a message immediately from the signed-in mailbox — there is no undo and no confirmation step, so confirm the recipients and text with the user first. Graph returns no message id for a send: if you need one, use mail_create_draft instead. A copy lands in Sent Items unless saveToSentItems is false. Sending needs the Mail.Send permission, which is separate from read/write access.
Inputs: to (required), body (required), subject (required), cc, +3 more.
me_get_mailbox_settingsGet my mailbox settingsARead-onlyIdempotent
Returns the signed-in user's Outlook mailbox configuration: time zone, date and time format, locale, working hours, and the automatic-reply (out-of-office) setting. Automatic-reply bodies are HTML in Graph and come back stripped to plain text and truncated to replyBodyChars. The scheduled start and end times are only meaningful when status is "scheduled" — when status is "alwaysEnabled" or "disabled" they are stale leftovers from the last schedule. Accounts with no Exchange Online mailbox fail with 404 MailboxNotEnabledForRESTAPI.
Inputs: replyBodyChars.
me_get_profileGet my profileARead-onlyIdempotent
Returns the signed-in user's own Entra ID profile from /me: by default id, displayName, mail, userPrincipalName, jobTitle, officeLocation and preferredLanguage. Pass select to ask for different properties instead. Properties Graph returns as null or empty are omitted, so a missing key means "not set" rather than "not requested". mail is null for accounts without an Exchange mailbox and personal Microsoft accounts leave the work fields empty — userPrincipalName is the identifier that is always present.
Inputs: select.
search_querySearch Microsoft 365ARead-onlyIdempotent
Runs a relevance-ranked Microsoft Search query across Microsoft 365 and returns a flattened list of hits, each with its rank, a text summary and the resource's id, name or subject, web URL and last-modified time. Defaults to 10 hits per entity-type group over ['message','driveItem']; page with from. Each entity type is searched in its own request and the hits merged, because Graph rejects some combinations outright (observed: message+driveItem 400s) and ranks stay per-request; chatMessage hits ignore fields. Each split asks only for the scopes its own index needs — Sites.Read.All for driveItem, listItem and site, Mail.Read for message, Calendars.Read for event, and BOTH Chat.Read and ChannelMessage.Read.All for chatMessage. That last pair is the one to watch: ChannelMessage.Read.All needs tenant admin consent and is carried by the teams group, so a chat search needs teams enabled as well as chat, and without it Graph refuses the chatMessage split with a 403 naming both permissions while every other split still returns; results are ranked, not sorted by date, and freshly changed items may not be indexed yet. This is read-only despite using POST.
Inputs: query (required), from, size, fields, +1 more.
session_statusCheck the session and consentARead-onlyIdempotent
Unified session and consent probe. Returns cached account (Entra ID, tenant, consented scopes) plus whether the stored session still mints tokens and live GET /me reachability. Call once first; on consent errors sign in again with just the missing scopes.
Inputs: no named parameters.
todo_complete_taskComplete a To Do taskAIdempotent
Marks a To Do task completed by patching its status to 'completed', and returns the updated task including the completedDateTime Graph stamps on it. Recurring tasks are rolled forward by the service rather than closed outright.
Inputs: listId (required), taskId (required).
todo_create_listCreate a To Do listA
Creates a new To Do list and returns its id and display name. Display names are not required to be unique, so check todo_list_lists first if the user expects one list per name.
Inputs: displayName (required).
todo_create_taskCreate a To Do taskA
Creates a task in a To Do list and returns the created task. Only title is required. Due and reminder values are sent as Graph dateTimeTimeZone objects using the timeZone argument (default UTC), and a date-only dueDateTime becomes midnight, matching how the To Do apps store all-day due dates. Supplying reminderDateTime also switches the reminder on, since Graph leaves isReminderOn false otherwise.
Inputs: title (required), listId (required), body, timeZone, +3 more.
todo_delete_taskDelete a To Do taskADestructive
Deletes a To Do task permanently and returns a confirmation. There is no recycle bin for To Do tasks, so prefer todo_complete_task when the user only wants it out of the way.
Inputs: listId (required), taskId (required).
todo_get_taskGet a To Do taskARead-onlyIdempotent
Reads one To Do task in full: title, status, importance, reminder state, start/due/completed times, categories and the notes body (truncated). Needs both the list id and the task id, which come from todo_list_lists and todo_list_tasks.
Inputs: listId (required), taskId (required).
todo_list_listsList To Do listsARead-onlyIdempotent
Lists the signed-in user's Microsoft To Do lists with their id, display name and well-known name (for example defaultList for the built-in Tasks list). Follows up to three pages, which covers any realistic number of lists. Use the returned id as listId for every other todo tool; To Do is delegated-only, so these are always the caller's own lists.
Inputs: no named parameters.
todo_list_tasksList To Do tasksARead-onlyIdempotent
Lists tasks in one To Do list, returning id, title, status, importance, due and reminder times and a short body preview per task, plus count and nextLink when more pages exist. Page size defaults to 50. Completed tasks are excluded unless includeCompleted is true. To Do tasks do not support $search: narrow with filter instead, and note that dueDateTime is a complex type, so a date filter reads dueDateTime/dateTime ge '2026-01-01T00:00:00'.
Inputs: listId (required), top, filter, includeCompleted.
todo_update_taskUpdate a To Do taskAIdempotent
Patches an existing To Do task and returns the updated task. Only the supplied fields change; omitting dueDateTime or reminderDateTime leaves the existing value alone, so use clearDueDateTime or clearReminderDateTime to remove one. Due and reminder values are sent as Graph dateTimeTimeZone objects using the timeZone argument (default UTC).
Inputs: listId (required), taskId (required), body, title, +7 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 59 tools
Tools are grouped by resource (mail, calendar, files, contacts, todo) and explicit cross-references distinguish near-pairs like mail_get_message vs mail_fetch_thread and files_get_item vs files_get_drive_item. A few clusters (multiple search tools, auth/session probes, generic graph_request vs purpose-built tools) could still invite misselection, but the boundaries are well documented.
Names are almost all snake_case and follow a domain_verb_noun pattern (mail_list_messages, calendar_create_event, todo_update_task). Minor deviations exist: object-less verbs (mail_send, files_search), domain-only helpers (session_status, graph_schema), and search_query's reversed ordering.
59 tools is far beyond the 3-15 sweet spot and fits the rubric's 50+ extreme-mismatch range for a single MCP server. Although Microsoft Graph is broad and the tools are organized by domain, the set risks overwhelming agents and includes many thin endpoint wrappers plus a generic graph_request fallback.
Core lifecycle coverage is strong: mail supports list/search/read/send/reply/forward/draft/delete/move; contacts, calendar events, and To Do tasks have near-full CRUD. Gaps remain, including no Teams/chat tools despite references to them, no file move/copy/rename, no way to send an existing draft, and limited binary upload support.
Maintenance
Related MCP Connectors
Permissioned access to Outlook, OneDrive and Teams via the user's own Microsoft account
Governed memory and workspace for any AI: tasks, calendar, mail and pages, with per-action consent.
Connect Apple iCloud and Microsoft 365 mail, calendars, contacts and tasks to AI.
Mac & Windows: let ChatGPT, Claude & Cursor use your email, calendar, iMessage, Teams, files.
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceEnables AI assistants to interact with Microsoft 365 services (users, mail, calendar, files) via Microsoft Graph API.55 npm1MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with Microsoft 365 through the Microsoft Graph API, including searching Teams messages, managing chats, and sending messages.77 npmMIT
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to seamlessly interact with Microsoft 365 services through the Graph API, featuring super tools, unified search, and intelligent learning.-
- AlicenseNot gradedqualityCmaintenanceEnables multiple users to securely connect their own Microsoft 365 account and use natural language to manage Outlook mail, Teams, and SharePoint/OneDrive files through Microsoft Graph, with support for shared mailboxes and admin governance.MIT