Outlook Semantic MCP - Tools
26 min read
The Outlook Semantic MCP Server exposes tools whose availability depends on the deployment mode (MCP_BACKEND) and debug settings.
Mode A (
verify_inbox_connection, reconnect_inbox, delete_inbox_data, and sync_progress are only available when MCP_BACKEND=microsoft_graph_and_unique_api. They are not registered in microsoft_graph mode.
Debug-Mode Tools
run_full_sync, pause_full_sync, resume_full_sync, and restart_full_sync are only available when MCP_BACKEND=microsoft_graph_and_unique_api AND MCP_DEBUG_MODE=enabled. They do not appear for standard deployments or in microsoft_graph mode. Note: Debug mode exposes these tools to all connected MCP users, not just operators. Do not leave enabled in production.
Tool Overview
Tool | Category | Mutating | Mode |
|---|---|---|---|
Email Search | No | Both | |
Email Search | No | Both | |
Email Search | No | Both | |
Draft Creation | Yes | Both | |
Contact Lookup | No | Both | |
Mailbox Utilities | No | Both | |
Mailbox Utilities | Yes | Both | |
Subscription Management | No | Mode A only | |
Subscription Management | Yes | Mode A only | |
Subscription Management | Yes | Mode A only | |
Sync Monitoring | No | Mode A only | |
Full Sync Control (debug only) | Yes | Mode A only | |
Full Sync Control (debug only) | Yes | Mode A only | |
Full Sync Control (debug only) | Yes | Mode A only | |
Full Sync Control (debug only) | Yes | Mode A only | |
Calendar | No | Both, | |
Calendar | No | Both, | |
Calendar | No | Both, | |
Calendar | No | Both, | |
Calendar | Yes | Both, | |
Calendar | Yes | Both, | |
Calendar | Yes | Both, | |
Calendar | Yes | Both, |
Calendar tools
The eight calendar tools are registered only when CALENDAR_INTEGRATION=enabled. They query Microsoft Graph live (no calendar ingest). respond_to_invite notifies immediately; create, update, and cancel wait for in-chat confirmation. See Calendar.
Mutating means the tool writes data to at least one of the following:
Outlook mailbox — creates or modifies data in Microsoft Graph (e.g. a draft email or a webhook subscription)
Internal database — persists or removes state managed by this server (e.g. subscription records, sync state, folder cache)
Unique knowledge base — indexes or removes email content from the knowledge base used for search
Tool | What it mutates |
|---|---|
| Creates a draft message in the user's Outlook mailbox via Microsoft Graph |
| Refreshes the folder cache in the internal database by re-fetching the folder tree from Microsoft Graph |
| Creates or renews the Microsoft Graph webhook subscription and writes the subscription record to the internal database |
| Cancels the Microsoft Graph webhook subscription and deletes the subscription record, folder cache, root scope, and all ingested email content from the Unique knowledge base |
| Triggers ingestion of all mailbox emails into the Unique knowledge base and updates sync state in the internal database |
| Updates the sync state to |
| Updates the sync state to resume ingestion in the internal database |
| Resets sync state in the internal database and re-triggers full ingestion into the Unique knowledge base |
| Notifies the organizer of accept / tentative / decline via Microsoft Graph |
| Creates an event on the chosen mailbox calendar; invitations are sent immediately |
| Patches an existing event; attendees are notified immediately |
| Cancels an event and notifies attendees (not a silent delete) |
Email Search
search_emails
Search emails and return pages of results with partial content. The tool behaviour and input schema differ by deployment mode.
Available in: Both modes
Results never contain full email bodies. Each result's text holds the semantically matched passages and/or the first ~255 characters of the body (bodyPreview). Call open_email with a result's openEmailParams to read an email in full.
Results are paged. Each backend request — one per semantic search, and one per KQL query × mailbox (× folder in directories) — returns one entry in pages. The server sets every page size, at most 50 per page and 150 results per response; a semantic page holds matched passages ranked by relevance, for up to 4 pages. Further pages are fetched with fetch_next_search_pages.
Mode A: microsoft_graph_and_unique_api
Runs two searches in parallel — semantic search against the Unique knowledge base and a KQL keyword search against Microsoft Graph — then merges and deduplicates the results. Both query arrays are required and must address the same user question.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| array (1–3) | Yes | Semantic searches. Up to 3 entries from different angles for a targeted question; exactly one for an overview or listing question. |
| array (1–3) | Yes | KQL keyword searches addressing the same question. |
Each entry in uniqueSemanticSearchQueries:
Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Natural-language search query |
| No | Scope to one mailbox. When omitted all accessible mailboxes are searched. | |
| array | No | Structured filters. Multiple condition objects are OR-combined; fields within one object are AND-combined. |
Each entry in msGraphKeywordSearchQueries:
Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | KQL query string. See KQL reference below. |
| No | Scope to one mailbox. When omitted, the query runs once per accessible mailbox. | |
| string[] | No | Folder names or IDs. The query runs once per resolved folder. |
Each object in conditions (applies to uniqueSemanticSearchQueries entries only) may include:
Field | Type | Description |
|---|---|---|
|
| ISO 8601 UTC — emails received on or after this date |
|
| ISO 8601 UTC — emails received on or before this date |
|
| Filter by sender. Use |
|
| Filter by To recipient |
|
| Filter by CC recipient |
|
| Folder IDs from |
|
| Filter by attachment presence. Value is a string, not a boolean. |
|
| Filter by read state ( |
|
| Filter by follow-up flag. Emails without a flag can carry an empty value, so express "not flagged" as |
|
| Category labels from |
Available operators:
Singular:
equals,notEquals,greaterThan,greaterThanOrEqual,lessThan,lessThanOrEqual,contains,notContains,isNull,isNotNull,isEmpty,isNotEmptyArray:
in,notIn,containsAny(email fields only — expands to OR ofcontainsfilters)
Example (Mode A):
{
"uniqueSemanticSearchQueries": [
{
"search": "quarterly report from Alice",
"conditions": [
{
"fromSenders": { "value": "alice@example.com", "operator": "equals" },
"dateFrom": { "value": "2024-01-01T00:00:00Z", "operator": "greaterThanOrEqual" }
}
]
},
{
"search": "Q1 budget summary",
"conditions": [
{
"fromSenders": { "value": "alice@example.com", "operator": "equals" }
}
]
}
],
"msGraphKeywordSearchQueries": [
{
"kqlQuery": "from:alice@example.com subject:\"quarterly report\" received>=2024-01-01"
}
]
}Mode B: microsoft_graph
Searches the mailboxes directly through Microsoft Graph $search with KQL queries. No semantic search is performed. Only msGraphKeywordSearchQueries is accepted.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| array (1–3) | Yes | KQL keyword searches. |
Each entry uses the same kqlQuery, mailbox, and directories fields as described in Mode A above.
Example (Mode B):
{
"msGraphKeywordSearchQueries": [
{
"kqlQuery": "from:alice@example.com subject:\"quarterly report\" received>=2024-01-01"
}
]
}KQL reference
Microsoft Graph sorts $search results by sent date, newest first — not by relevance — and returns at most 1,000 results per query and mailbox across all pages.
Supported KQL property filters for kqlQuery:
Filter | Example | Notes |
|---|---|---|
|
| Sender SMTP, display name, or domain |
|
| To recipient |
|
| CC recipient |
|
| Any of from/to/cc/bcc |
|
| Words in subject |
|
| Words in body |
|
| Received on or after |
|
| Received on or before |
|
| Has attachments |
|
| Read state ( |
|
| Outlook category |
|
| Message type |
Syntax rules: - No space between property and value: from:alice@example.com not from: alice@example.com - Boolean operators must be uppercase: AND, OR, NOT - Suffix wildcards only: report*, not *report - Phrases in double quotes: subject:"quarterly report" - Do NOT use folder: — it is not supported; use directories instead - There is no KQL property for follow-up flags (flag:, flagStatus: are stripped)
Read state and flags by mode: read-state filtering works in both modes — in Mode A, put it in both backends (the isRead condition on semantic entries and isRead: in the KQL query) so the merged results do not mix read and unread mail. Flag filtering (flagStatus) works only in Mode A, on the semantic side; Mode B cannot filter by flag.
Semantic isRead and flagStatus are ingested metadata, refreshed by live catch-up only when the message's lastModifiedDateTime changes, so they can lag the mailbox. KQL isRead: is evaluated live by Graph.
Return shape (search_emails and fetch_next_search_pages)
{
success: boolean;
message?: string; // error description when success is false
status?: string; // informational subscription/backend status
syncWarning?: string; // search_emails in Mode A only — present when ingestion is incomplete or in error state. Always display to the user before showing results.
searchNotes?: string; // informational notes (e.g. unrecognised folders excluded, throttled mailboxes, unknown cursors). Display to the user after results.
hasMore?: boolean; // true when at least one page has status "hasMore" or "deferred"; throttled and failed pages do not count
pages?: Array<{
backend: "Unique" | "MsGraph";
query: string; // KQL query or semantic search text
mailbox?: string;
folder?: string;
status: "hasMore" | "deferred" | "complete" | "ceilingReached" | "throttled" | "failed" | "expired" | "accessRevoked";
cursorId?: string; // pass to fetch_next_search_pages: next page (hasMore), page not run yet (deferred) or retry (throttled, failed)
retryAfterSeconds?: number; // throttled only
}>;
results?: Array<{
uniqueContentId?: string; // Unique KB content ID. Present for semantic-backend results only.
msGraphMessageId?: string; // Microsoft Graph message ID. Present for Graph-backend results; also present for semantic results when both backends matched the same email.
backend: "Unique" | "MsGraph"; // which backend returned this result
folderId: string; // internal folder ID — do not display to users
title: string; // email subject
from: string; // sender email address
receivedDateTime?: string | null; // ISO 8601
text: string; // "## Semantically Matched Content" and/or "## Preview" — never the full body
outlookWebLink: string; // direct URL to open in Outlook Web — use as link target when non-empty
sourceMailbox?: string | null; // mailbox this email belongs to
openEmailParams: { // pass directly to open_email without modification
id: string;
idType: "Unique" | "MsGraph";
mailbox?: string;
parentFolderId?: string;
idIsImmutable?: boolean;
};
}>;
}Page statuses:
Status | Meaning | Agent action |
|---|---|---|
| Page delivered, more results exist | Call |
| The request did not run because the response was full (150 results) | Call |
| The request returned everything it matched. A semantic search is also | — |
| Stopped at 1,000 results (the Microsoft Graph | Tell the user results are capped; narrow the search |
| Microsoft returned HTTP 429 after one automatic retry | Retry once with the same |
| Temporary error (5xx, network), or a nextLink that cannot be followed. Without | Retry once with the same |
| Microsoft Graph no longer accepts the stored continuation, or served the first page again instead of the next one | Run the search again |
| Access to the mailbox was lost since the search started | Do not retry |
The results are complete only when every page is complete. hasMore: false alone does not mean complete.
Page sizes and fan-out:
Each semantic entry is one request. Each KQL query runs once per full-access mailbox, and once per folder when directories is set (at most 10 folders per query), so one call can start dozens of Graph requests. Every one of them gets its own entry in pages; none is dropped.
One response holds at most 150 results (maxResultsPerResponse). Page sizes are shrunk to fit before anything is fetched, so nothing fetched is thrown away:
Every query of a call gets an equal share:
floor(150 / (semantic queries + KQL queries)). A semantic search asks formin(50, share)passages. The KQL queries pool what the semantic searches leave.The Graph requests split that pool equally, at least 5 each:
min(50, floor(pool / running)), whererunning = min(requests, floor(pool / 5)). Requests that do not fit aredeferred: they come back as cursors with that same page size and run when the agent follows them. Requests are taken round-robin across the queries, so deferral holds back the last mailboxes and folders of every query rather than whole queries.Examples: one query over six folders runs six requests of 25 (150 results). Three semantic searches and one KQL query over three mailboxes give 37 passages per semantic search and 13 emails per mailbox. Two queries over 30 mailboxes run 30 requests of 5 and defer 30.
fetch_next_search_pagestakes the cursors in the order passed while their page sizes add up to at most 150. The rest come back unchanged asdeferred.The agent cannot choose page sizes; there is no
limitparameter. A page holds at most 50 emails or passages.Semantic search ranks every passage in scope, so page 1 holds the most relevant passages and each next page less relevant ones; it never runs out on a real mailbox. A semantic request is therefore
completeafter 4 pages, or earlier when a page comes back empty.A semantic page often holds fewer passages while more exist: Unique splits the page size between a vector and a full-text search, pages each on its own and merges them. The same passage can come back on a later page from the other search; it is dropped there. A later page can still add new passages of an email returned earlier.
Graph copies
$topinto the@odata.nextLink, and the nextLink is stored and followed exactly as Graph returned it. A shrunk first page therefore keeps its smaller size on later pages; the agent follows the cursor more times to get the rest.The agent picks the cursors to follow from
pages(bymailboxandfolder), or follows all of them when it cannot tell where the answer is.
Usage notes:
Pass the
openEmailParamsobject from a result directly toopen_emailto retrieve the full email body.Targeted questions: up to 3 queries per backend, follow cursors only when the answer is not found yet. Overview or listing questions: one query per backend, then follow every cursor until
hasMoreis false.If
syncWarningis present (Mode A only), display it to the user and callsync_progressto check ingestion status — results may be incomplete.If
searchNotesis present, display it to the user after showing results.Well-known system folder names (
"Inbox","Sent Items","Drafts") can be used directly indirectories— no need to calllist_mailboxes_and_directoriesfor those.For custom folders, call
list_mailboxes_and_directoriesfirst to obtain folder IDs.
fetch_next_search_pages
Fetch the next page of results, or retry a throttled or failed page, for searches started with search_emails.
Available in: Both modes
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| string[] (1–20) | Yes |
|
Return shape: same as search_emails.
Usage notes:
One response holds at most 150 results. Cursors are fetched in the order passed until their page sizes would exceed that; the rest come back unchanged with status
deferred.Cursors are stored server-side (
search_cursorstable) and scoped to the user. The agent only sees the id. Unknown ids, ids of another user, and ids older than 30 days are reported insearchNotes.Cursor rows are immutable: each page gets a new id, and following an id again returns the same page. A throttled or failed page keeps its id so it can be retried without skipping results.
The same email can appear on pages of different requests;
msGraphMessageIdidentifies it.
open_email
Retrieve the full content of an email by its ID returned from search_emails or fetch_next_search_pages. Search results only carry passages and previews, so this is how the agent reads an email's body.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| string | Yes | Email identifier. Use |
|
| Yes | Backend type. Use |
| No | Use | |
| string | No | Use |
| boolean | No | Use |
Return shape:
{
success: boolean;
status?: string;
message?: string;
emailData?: {
id: string;
title: string | null;
metadata: unknown | null;
text: string; // full email body or the matched chunks depending on which search returned the results
};
}Usage notes:
Always pass the
openEmailParamsobject from asearch_emailsresult directly as the tool input — do not construct these parameters manually.The
textfield inemailDatacontains the full email body. This is distinct from thetextfield in search results, which contains only matched passages and/or a short preview.The response is not capped: an email ingested with large attachments (Mode A) returns all of its text.
Draft Creation
create_draft_email
Create a draft email in the connected Outlook mailbox. The draft is saved to the Drafts folder but not sent — the user must open it in Outlook and send manually.
Supports two modes selected by recipientsData.type:
"draft"— fresh draft with explicit recipients (personal or shared mailbox)"reply"— reply-all draft for an existing email; Graph pre-fills all original recipients (personal or shared mailbox)
Common input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| string | Yes | Email body in Markdown. Paragraphs, bold, italic, lists, links, blockquotes, and inline code are supported. Raw HTML is escaped, not rendered. |
| string (UPN) | No | Shared mailbox to create the draft in (e.g. |
| array | No | Files to attach |
| string | Yes | File name including extension (e.g. |
| string | Yes | File content URI. Two schemes supported (see below) |
| object | Yes | Discriminated union — see below |
recipientsData — fresh draft (type: "draft"):
Field | Type | Required | Description |
|---|---|---|---|
|
| Yes | |
| string | Yes | Subject line |
| array | Yes | Primary recipients |
| string (email) | Yes | Recipient email address |
| string | No | Recipient display name |
| array | No | CC recipients (same shape as |
recipientsData — reply-all draft (type: "reply"):
Field | Type | Required | Description |
|---|---|---|---|
|
| Yes | |
| string | Yes |
|
Attachment data URI schemes:
Scheme | Format | Description |
|---|---|---|
Unique KB |
| File from the Unique knowledge base |
Inline base64 |
| Base64-encoded content with explicit MIME type |
Attachment scheme availability
The unique://content/{contentId} scheme only works when UNIQUE_SERVICE_AUTH_MODE=cluster_local. In external auth mode the Unique ingestion service cannot resolve internal content URIs — use the data:[mediatype];base64,<data> scheme instead.
Return shape:
{
success: boolean;
message: string;
draftId?: string; // Microsoft message ID (present when success is true)
webLink?: string; // link to open draft in Outlook (present when success is true and Graph returned one)
attachmentsFailed?: Array<{
fileName: string;
reason: string;
}>;
}Usage notes:
Use
lookup_contactsto resolve recipient email addresses before calling this tool.If attachments partially fail, the draft is still created and
draftIdis returned alongside theattachmentsFailedlist.The
webLinkin the response opens the draft directly in Outlook Web.For shared mailbox reply drafts: in Outlook Web the reply appears in the Drafts folder rather than inline in the thread — this is expected Outlook Web behaviour. The draft sends correctly regardless. In Outlook desktop the reply appears and sends normally.
Contact Lookup
lookup_contacts
Search for contacts by name across the Microsoft People API and the connected inbox. Use this to resolve recipient addresses before creating a draft.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| string (min 2 chars) | Yes | Name or partial name to search for |
Return shape:
{
contacts: Array<{
name: string;
email: string;
source: "people_api" | "inbox";
similarityScore: number;
}>;
message?: string;
}Usage notes:
Results come from two sources: the Microsoft People API (colleagues, frequent contacts) and emails found in the synced inbox.
similarityScoreranks results by name similarity — use this to surface the best match.
Mailbox Utilities
These tools return metadata needed to build filters for search_emails. They do not manage or modify mailbox data.
list_categories
List all Outlook mail categories available for the user.
Input parameters: None
Return shape:
{
success: boolean;
message: string;
status?: string;
categories?: string[]; // category display names
count?: number;
}Usage notes:
Category names returned here can be passed to the
categoriesfilter insearch_emails.
list_mailboxes_and_directories
List all Outlook mailboxes and their folder trees available to the user.
Available in: Both modes
Input parameters: None
Return shape:
{
success: boolean;
message: string;
status?: string;
mailboxes?: Array<{
email: string | null;
displayName: string | null;
isOwn: boolean; // true for the user's own primary mailbox
folders: Array<{
id: string; // pass to directories filter in search_emails
displayName: string;
children: Array<...>; // recursive — same shape
}>;
}>;
}Usage notes:
Folder
idvalues can be passed to thedirectoriesfilter insearch_emailsto narrow results to a specific folder.In Mode A, the folder tree is synced from Microsoft Graph and reflects the user's current mailbox structure. Calling this tool triggers a fresh sync of the folder tree.
Folder filtering via
directoriesis only effective in Mode A (microsoft_graph_and_unique_api). In Mode B, the Microsoft Graph Search API does not support folder-scoped filtering — thedirectoriescondition is ignored.Well-known system folder names (
"Inbox","Sent Items","Drafts", etc.) can be used directly insearch_emailswithout calling this tool first.When
DELEGATED_ACCESS_SCANis enabled, themailboxesarray includes delegated mailboxes alongside the user's own mailbox. TheisOwnfield istruefor the user's primary mailbox andfalsefor delegated ones. Folder IDs from delegated mailboxes can be passed to thedirectoriescondition insearch_emailsto narrow results to a specific folder in a delegated mailbox (folder filtering viadirectoriesis only effective in Mode A).
Subscription Management
Mode A (
verify_inbox_connection, reconnect_inbox, and delete_inbox_data are only available when MCP_BACKEND=microsoft_graph_and_unique_api. These tools are not registered in microsoft_graph mode because no webhook subscriptions are created.
verify_inbox_connection
Check the status of the inbox connection and Microsoft Graph webhook subscription.
Input parameters: None
Return shape:
{
status: "active" | "expiring_soon" | "expired" | "not_configured";
message: string;
subscription: {
id: string;
expiresAt: string;
minutesUntilExpiration: number;
createdAt: string;
updatedAt: string;
} | null;
}Status | Meaning | Action |
|---|---|---|
| Subscription is valid | None required |
| Expires within 15 minutes | Renewal is automatic; no action needed |
| Subscription has lapsed | Call |
| No subscription exists | Call |
reconnect_inbox
Re-establish the Microsoft Outlook inbox subscription when expired or not configured.
Input parameters: None
Return shape:
{
success: boolean;
message: string;
subscription: {
id: string;
expiresAt: string;
minutesUntilExpiration: number;
status: "created" | "already_active" | "expiring_soon";
} | null;
}Usage notes:
Safe to call even if a subscription already exists — it will return
already_activewithout creating a duplicate.When a new subscription is created (status:
created), a full sync is triggered automatically. If the subscription isalready_activeorexpiring_soon, no full sync is triggered.
delete_inbox_data
Permanently delete all synced email data from Unique and cancel the Microsoft Graph subscription. This stops future email ingestion and removes all previously ingested email content for your inbox from the Unique knowledge base.
Input parameters: None
Return shape:
{
success: boolean;
message: string;
subscription: {
id: string;
status: "removed" | "not_found";
} | null;
}Usage notes:
This is a destructive operation: all previously ingested email content for the user is permanently removed from the Unique knowledge base, and no new emails will be ingested.
To resume ingestion after deletion, call
reconnect_inbox.
Sync Monitoring
Mode A (
sync_progress is only available when MCP_BACKEND=microsoft_graph_and_unique_api. There is no sync pipeline in microsoft_graph mode.
sync_progress
Check the current state of the full email sync and live catch-up pipeline. Call this after a syncWarning is returned by search_emails, or to monitor initial sync progress after connecting.
Input parameters: None
Return shape:
{
state: "error" | "running" | "finished";
message: string;
userEmail: string;
syncStats?: {
fullSyncState: "ready" | "running" | "waiting-for-ingestion" | "paused" | "failed";
liveCatchUpState: "ready" | "running" | "failed";
runAt: string | null; // last completion time
startedAt: string | null; // last start time
expectedTotal: number | null; // total emails at sync start
skippedMessages: number; // filtered out by inbox filters
scheduledForIngestion: number; // successfully uploaded
failedToUploadForIngestion: number;
filters: {
retentionWindowInDays: number;
ignoredBefore: string; // ISO 8601 UTC cutoff — emails before this date are excluded
ignoredSenders: string[];
ignoredContents: string[];
};
dateWindow: {
newestReceivedEmailDateTime: string | null;
oldestReceivedEmailDateTime: string | null;
newestLastModifiedDateTime: string | null;
};
} | null;
ingestionStats?: {
failed: number;
finished: number;
inProgress: number;
} | { state: "error" } | null;
debugData?: { // only present when MCP_DEBUG_MODE=enabled
providerUserId: string;
userProfileId: string;
subscriptionId: string;
} | null;
}Usage notes:
state: "running"means the full sync is actively fetching and uploading. Search results will be partial untilstate: "finished".scheduledForIngestioncounts emails uploaded to Unique;ingestionStats.finishedcounts those confirmed processed by the knowledge base.failedToUploadForIngestionemails were skipped after retries — check operator logs for details.syncStats.liveCatchUpState: "failed"indicates the live catch-up pipeline stalled. Recovery is automatic — theINGESTION_LIVE_CATCHUP_RECOVERY_CRONscheduler resets it within 5 minutes. No user-callable tool exists for this outsideMCP_DEBUG_MODE; users should wait and operators can monitor pod logs.
Debug-Mode Tools
The following four tools are only available when MCP_DEBUG_MODE=enabled is set in the server configuration. They are intended for operators diagnosing sync issues, not for end users.
Mode A (
These tools are only available when MCP_BACKEND=microsoft_graph_and_unique_api AND MCP_DEBUG_MODE=enabled.
run_full_sync
Trigger a full re-sync of the Outlook mailbox into the knowledge base. Skips if a sync was run recently.
Input parameters: None
Return shape:{ success: boolean; message: string }
Usage notes: Use sync_progress to monitor progress after triggering.
pause_full_sync
Pause an in-progress full sync. The current batch finishes before the sync stops.
Input parameters: None
Return shape:{ success: boolean; message: string }
Usage notes: Use resume_full_sync to continue from where the sync paused.
resume_full_sync
Resume a paused full sync from the point it was paused.
Input parameters: None
Return shape:{ success: boolean; message: string }
restart_full_sync
Restart the full sync from scratch, discarding all previous progress.
Input parameters: None
Return shape:{ success: boolean; message: string; version?: string }
Usage notes: This is destructive — all sync progress counters and the next-page cursor are reset. Use only when recovering from a corrupted sync state.
Calendar
Available in: Both modes, only when CALENDAR_INTEGRATION=enabled. Live Graph query-through — no calendar ingest, webhooks, or calendar tables. Shared-mailbox profiles never call these tools.
respond_to_invite notifies the organizer immediately — there is no confirmation prompt. create_event, update_event, and cancel_event notify other people after in-chat confirmation. There is no draft state. Confirmation is Accept / Decline on the prompt — there is no extra checkbox. For a recurring update_event or cancel_event, the prompt also asks this occurrence or the entire series. cancel_event notifies attendees; it is not a silent delete.
If a calendar tool returns consentRequired: true, Graph denied calendar permission on the signed-in user's own mailbox (usually missing Calendars.ReadWrite.Shared). Ask the user to reconnect Outlook so they run Microsoft OAuth again. Do not call reconnect_inbox — that only renews the mail webhook. Do not send them to /auth/authorize — that is the MCP OAuth start URL for clients, not a user reconnect. See Configuration — CALENDAR_INTEGRATION and Permissions.
Do not display calendarRef, eventRef, calendarId, or eventId. Pass calendarRef (from list_calendars) and eventRef (from search_calendar_events) through unchanged — never assemble one from parts.
ID namespaces
Graph calendar and event IDs belong to one mailbox. Every calendar path is rooted at /me, which the delegated token resolves to the signed-in user, so all ids these tools hand out come from /me/calendars and resolve back under /me/calendars.
That includes calendars somebody shared with the user: Graph stores an accepted share as a local copy in the recipient's own mailbox, so it lists and resolves under /me like any other calendar. There is no mailbox to carry alongside the id — the token already names it, which is why calendarRef and eventRef hold ids only.
ownerEmail / ownerName are display and filtering — who the calendar belongs to. They are not routing, and a calendar with isOwn: false is still read under /me.
list_calendars
List Outlook calendars the signed-in user can access: their own, and calendars shared with them that they accepted.
Input parameters: None
Return shape:
{
success: boolean;
message: string;
calendars?: Array<{
calendarRef: { calendarId: string }; // pass through unchanged; never display
name: string;
ownerEmail: string | null; // SMTP of the owner; on isOwn true this is the signed-in user
ownerName: string | null;
isOwn: boolean; // false when the calendar is shared with the user
isDefaultCalendar: boolean; // true for the mailbox's primary calendar
canEdit: boolean;
canViewPrivateItems: boolean;
}>;
consentRequired?: true;
}Usage notes:
Primary calendars are listed first. Holiday and birthday calendars appear too — skip those by name.
Shared calendars have
isOwn: falseandisDefaultCalendar: falseand still hold meetings. Do not skip everyisDefaultCalendar: falsecalendar.For meetings between people, pass every
isDefaultCalendar: truecalendarRefand everyisOwn: falsecalendarReftosearch_calendar_events.ownerEmailon a calendar withisOwn: trueis the signed-in user SMTP — use it incheck_availabilityandsuggest_meeting_timesattendeeswhen they want to attend.
search_calendar_events
Search events in a time window across calendars returned by list_calendars. Each result includes the full plain-text body; there is no second tool to open an event.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| array of | Yes | From |
| object | Yes | Prefer |
| email[] (max 10) | No | Every listed SMTP must be on the event (organizer or attendee). Exact whole-address match — resolve a name with |
|
| No | Exactly one form. Prefer |
| string[] (max 10) | No | Every listed Outlook category must be on the event. Omit rather than guess. |
Relative dateRange.range values: today, tomorrow, yesterday, thisWeek, nextWeek, lastWeek, thisMonth, nextMonth, lastMonth, thisYear, nextYear, lastYear, next7Days, next30Days, next90Days, past7Days, past30Days. Weeks start Monday. today is the whole mailbox-local day (including meetings that already happened).
Which filters Graph evaluates
Results are capped. Where a filter runs relative to that cap decides what an empty result proves.
Filter | Where | Why |
|---|---|---|
| Graph | Narrows before the cap. Everything returned is a real match; |
| in-process | Graph cannot filter into attendee email addresses. Applied after the cap, on what Graph already returned. |
| in-process | Graph cannot express the AND of several categories. Applied after the cap. |
window | Graph | Required |
attendees and categories are AND filters: every named person or category has to be on the event. An empty result from those filters does not prove the meeting does not exist — matching events may sit outside the fetched set. When searchNotes reports a cap, say the answer may be incomplete and offer a narrower window.
Return shape:
{
success: boolean;
message: string;
events?: Array<{
subject: string | null;
body: string; // plain-text agenda; may be truncated — see bodyTruncated
bodyTruncated: boolean;
start: { dateTime: string; timeZone: string };
end: { dateTime: string; timeZone: string };
location: string | null;
joinUrl: string | null; // Teams URL when present; never invent one
attendees: Array<{ name: string | null; email: string | null; response: string | null; type: string | null }>;
organizerName: string | null;
organizerEmail: string | null;
isCancelled: boolean;
isAllDay: boolean;
isPrivate: boolean;
sensitivity: string | null; // normal, personal, private, confidential
categories: string[];
recurrence: string | null;
seriesMasterId: string | null; // never display
type: string | null; // singleInstance, occurrence, exception, seriesMaster
showAs: string | null; // free, tentative, busy, oof, workingElsewhere, unknown
webLink: string | null; // the only user-facing URL besides joinUrl
calendarName: string;
eventRef: { eventId: string; calendarId: string }; // never display
}>;
searchNotes?: string[]; // display after the results
resolvedWindow?: {
startDateTime: string;
endDateTime: string;
timeZone: string;
serverCurrentDateTime: string;
interpretation: string;
};
consentRequired?: true;
}Usage notes:
Call
list_calendarsfirst. Skip holiday and birthday calendars by name.If a relative range was used, state
resolvedWindow.interpretationin the answer.Use
webLinkwhen present; do not invent Outlook URLs.Do not use
todayfor "when is my next meeting" — that window includes the whole day, so it can return a meeting that already happened. Usenext7Days(starts now).
check_availability
Free/busy for people, distribution lists, or rooms via Graph getSchedule. Only attendees are checked (at most 20). Include the signed-in user when they want to attend; get their SMTP from list_calendars ownerEmail on a calendar with isOwn: true (prefer isDefaultCalendar: true).
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| email[] (1–20) | Yes | SMTP addresses to check. |
| object | Yes | Prefer relative. Must be shorter than 62 days. |
| integer (5–1440) | No | Length of each availability slot. Default 30. |
Usage notes:
busyBlocksare decoded fromavailabilityView(free slots omitted).Subject and location on
itemsappear only with detail-level permission; private items are redacted.Graph error 5006 is returned as a narrow-the-range message (more than 1000 calendar entries in a slot).
If a relative range was used, state
resolvedWindow.interpretation.
suggest_meeting_times
Ranked free slots via Graph findMeetingTimes. Always runs as the signed-in user (the organizer). Include them in attendees when they want to attend; omit attendees for organizer-only.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| email[] | No | People who must be free. |
| object | Yes | Future window. Prefer relative ( |
| integer (5–1440) | No | Default 30. |
| integer (1–20) | No | Default 5. |
|
| No | Default |
| boolean | No | Default false. |
| number (0–100) | No | Default 50. |
Usage notes:
If
emptySuggestionsReasonis present, explain it and suggest widening the window. Do not invent slots.
respond_to_invite
Accept, tentatively accept, or decline an invitation. Pass eventRef from search_calendar_events unchanged. The organizer is notified immediately; there is no confirmation prompt.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| object | Yes | From |
|
| Yes | Sent immediately. |
| string | No | Optional note included with the response. |
create_event
Create an event. There is no draft — if attendees are included, invitations are sent immediately after the user confirms.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| string | Yes | Event title. |
| string | Yes | Inclusive start with timezone offset, e.g. |
| string | Yes | Exclusive end with timezone offset. Must be after |
| email[] (max 20) | No | Required attendees. Omit for an appointment with no invitations. |
| string | No | Location display name. |
| string | No | Agenda as HTML, sent to Outlook unchanged. Use |
| boolean | No | If true, create a Teams meeting and return a join URL. |
| object | No | From |
| string (max 32) | No | Idempotency key. Reuse the same value if this create is retried. |
Usage notes:
All-day events are not supported yet.
The confirmation names the destination calendar by owner, not mailbox. The user confirms with Accept / Decline on the prompt.
canEditmust be true on the chosen calendar.bodyis HTML sent to Graph unchanged (contentType: HTML). Do not write Markdown. Graph appends the Teams join HTML whenisOnlineMeetingis true.
update_event
Change an existing event. Pass eventRef from search_calendar_events unchanged. Attendees are notified immediately after confirmation. For a recurring meeting the user chooses this occurrence or the whole series.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| object | Yes | From |
| string | No | Replacement title. |
| string | No | Replacement times with timezone offset. |
| email[] (max 20) | No | Replaces the entire attendee list. An empty list removes every attendee. |
| string | No | Replacement location. |
| string | No | Replacement agenda as HTML, sent to Outlook unchanged. Fragment only. Do not include the Teams join section. Omit to leave the body unchanged. |
|
| No | Add a Teams meeting. Omit to leave unchanged. |
At least one field besides eventRef must be set.
A Teams meeting body is an HTML document Graph already owns. Inside <body> the user agenda comes first, then Microsoft's insert: a gray underscore rule, a div.me-email-text block (Join Microsoft Teams Meeting link, dial-in, conference ID, Local numbers / Reset PIN / Learn more / Meeting options), and a closing underscore rule. Updating body writes the agent's HTML into the user-agenda region only; the Microsoft block is not rewritten, and the agent's HTML is not converted or escaped.
cancel_event
Cancel an event and notify attendees. This is not a silent delete. Only the organizer can cancel. Pass eventRef unchanged. For a recurring meeting the user chooses this occurrence or the whole series.
Input parameters:
Parameter | Type | Required | Description |
|---|---|---|---|
| object | Yes | From |
| string | No | Optional note included on the cancellation. |
Example prompts
These are user prompts. The assistant should use the named tools and never show internal IDs.
What meetings do I have next week?
What meetings do I have next week?
Use list_calendars, then search_calendar_events with dateRange.rangeType: relative and range: nextWeek. Weeks start Monday. State resolvedWindow.interpretation in the answer. Show subject, start/end (with timezone), attendees and their response, location and/or Teams joinUrl, and the agenda from body. Use webLink when present; do not invent Outlook URLs.
When is my next meeting with XY?
When is my next meeting with Alex Rivera?
Resolve the name with lookup_contacts first. Then search_calendar_events with dateRange.rangeType: relative, range: next7Days (starts now), and attendees: ['alex.rivera@…']. Answer with the soonest hit: when, where / join URL, and who else is on it. Widen the range only if that window is empty. Do not use today for this question.
Create a meeting invite for XY
Create a 30-minute invite for Alex Rivera tomorrow at 10:00, subject Sync, Teams meeting.
Optional:
suggest_meeting_timesorcheck_availabilityif the time is not already agreed.create_eventwith offset-bearingstartDateTime/endDateTime,attendees: ["alex@example.com"],isOnlineMeeting: trueif they asked for Teams.The user must confirm. Invitations are sent immediately; there is no draft. If the create is retried, reuse
transactionId.
Shared-calendar create: list_calendars, then pass that calendarRef unchanged. The confirmation names the destination by owner, not mailbox.
Related Documentation
Full Sync - Full sync mechanics and states
Live Catch-Up - Webhook-driven real-time ingestion
Flows - Sequence diagrams for OAuth, sync, and draft creation flows
Permissions - Microsoft Graph permissions required by these tools
Features — Calendar - What is supported, what is not
Configuration — CALENDAR_INTEGRATION - Operator enablement and re-consent