Skip to main content
Troubleshooting Last updated: 5 October 2026

Troubleshooting

Nathan Schram
By Founder, Little Bear Apps

Common issues and their fixes. For getting-started guidance, see docs/how-to/getting-started/. For architecture and module layout, see docs/architecture.md.

Common Issues

IssueSolution
AADSTS7000215 (invalid secret)Use the secret VALUE, not the Secret ID, from Azure > Certificates & secrets. The Value is shown only once at creation — if you navigated away, create a new secret. An expired secret gives the same error. Since v3.11.0 the server keeps Microsoft’s original error text and appends this remediation to it, so you no longer have to look the code up (#69)
AADSTS9002331 (“configured for Microsoft Account users only … use /consumers”)Your Azure app is registered as “Personal Microsoft accounts only”. Set OUTLOOK_AUTH_AUDIENCE=consumers in your MCP client env block (v3.8.0+). Single-tenant apps need their tenant GUID; multi-tenant apps can use the default (common).
A tool says “Shared-mailbox support is turned off”Shared mailboxes are opt-in (work/school only). Set OUTLOOK_SHARED_MAILBOX=read (read) or =true (read and organise) in the server’s env block, restart, then run auth action=authenticate force=true. auth action=about shows whether the shared scopes were granted.
OUTLOOK_SHARED_MAILBOX is on and a personal account’s sign-in fails, then hands you a second device codeDevice-code flow only: personal accounts can’t be granted the .Shared scopes, so Outlook Assistant retries once with the standard scopes — enter the second code. Simpler: leave OUTLOOK_SHARED_MAILBOX unset on personal accounts. The browser flow (npm run auth-server) requests the configured scopes with no fallback, so don’t enable the setting if you sign in that way.
OUTLOOK_SHARED_MAILBOX is on and work/school sign-in shows “Need admin approval” or fails with AADSTS65001Your tenant doesn’t let users consent to Mail.Read.Shared/Mail.ReadWrite.Shared. Ask an admin to grant consent for the app, then re-run auth action=authenticate force=true. If that isn’t possible, unset OUTLOOK_SHARED_MAILBOX and restart — sign-in goes back to the standard scopes. Scopes are never silently downgraded on a consent error.
EADDRINUSE :3333npx kill-port 3333 then restart auth server
Module not foundRun npm install
Auth URL doesn’t workBrowser flow only: start the auth server first (npm run auth-server). The default device code flow uses microsoft.com/devicelogin and needs no auth server
A tool returns Authentication required.You’re signed out, or the token expired and couldn’t be refreshed. Sign in with auth action=authenticate (add force=true to replace an existing session), then retry the call. auth action=status shows the current state
search-emails returns no resultsOn personal accounts, query auto-falls back to subject search (v3.5.2). Use subject, from, to, receivedAfter filters for best results. A search scoped to one folder (the inbox by default) misses mail elsewhere: mail from a new sender often lands in Junk Email, so try searchAllFolders: true or folder: "junkemail"
create-event wrong timezoneOmit the Z suffix on times for local timezone. Z suffix = UTC, which may be hours off
Auth server says “Microsoft Graph API credentials are not set”Ensure OUTLOOK_CLIENT_ID/OUTLOOK_CLIENT_SECRET are set as env vars for the auth server process. The browser flow needs both; it doesn’t use a client ID saved with auth action=authenticate clientId=…
auth action=authenticate says “OUTLOOK_CLIENT_ID is not configured”Device code sign-in needs only the client ID, not the secret. Either set OUTLOOK_CLIENT_ID in the MCP client’s env block, or (for clients with no env settings, such as plugin marketplaces) call auth action=authenticate clientId=<Application (client) ID>. The ID is saved to ~/.outlook-assistant-config.json and applies without a restart. auth action=about shows where the client ID comes from
clientId refused: “environment variable is set to a different client ID”OUTLOOK_CLIENT_ID (or the legacy MS_CLIENT_ID) takes precedence over a saved ID, so the new one would be ignored. Change or remove the env var and restart the server, then authenticate again
clientId refused: “not a valid Azure Application (client) ID”Use the GUID labelled Application (client) ID on the app registration’s Overview page, not the Directory (tenant) ID, Object ID or a secret
Device code authenticate returns nothing / empty outputFixed in v3.8.2 — earlier versions rendered a failed initiation as empty output (the underlying error was swallowed). Update to v3.8.2+, which shows the real error plus a hint. Common causes: AADSTS9002331 (set OUTLOOK_AUTH_AUDIENCE=consumers), invalid_client (enable public client flows), or blocked outbound network access to login.microsoftonline.com (e.g. a sandboxed connector). (#213)
Device code “invalid_client”Enable “Allow public client flows” in Azure Portal > App registrations > Authentication
Device code sign-in shows “wrongplace”Normal — sign-in completed. Close the browser, call device-code-complete
Device code sign-in redirects to localhostUse incognito/private browser for microsoft.com/devicelogin
device-code-complete hangsTool is polling (not a permission prompt). Wait 10-15s. If still hanging, sign-in didn’t complete — get new code, use incognito browser
device-code-complete “no pending flow”Fixed in v3.7.2 — device code state now persists to disk, surviving MCP server restarts. Update to v3.7.2+
Token refresh fails after ~60 min (device code)Fixed in v3.7.2 — earlier versions sent client_secret for public client refresh. Update to v3.7.2+
search-emails returns 503 errorFixed in v3.5.2 — query now falls back to contains(subject) on personal accounts. For body search, use searchExpression (#98)
send-email returns Graph 400 ErrorInvalidRecipients with literal-bracket addressFixed in v3.7.4 — pass recipients as a comma-separated string (to: "a@x.com,b@x.com"), not an array literal. Earlier versions silently stringified array shapes; v3.7.4 rejects both live arrays and JSON-encoded array strings at the MCP boundary with a friendly hint. (#168) Separate addresses with commas only: a semicolon (a@x.com;b@x.com) isn’t split, and with OUTLOOK_ALLOWED_RECIPIENTS set it’s refused as not a single plain address (v3.14.0+).
search-emails kqlQuery=... returns unrelated recent emailsFixed in v3.7.4 — earlier versions auto-wrapped the kqlQuery in extra quotes (breaking phrases like subject:"foo bar") and silently fell through to combined-search when Graph returned 0, dropping the filter. v3.7.4 trusts your expression syntax and never falls through. v3.10.0 adds one deliberate, disclosed exception — see the field-scoped translation row below. (#169 V37-F-1)
Can’t move or address a nested folder (e.g. a subfolder)Fixed in v3.9.0 — address folders by path, e.g. targetFolder="Parent/Child", or by targetFolderId / folderId. Run folders action=list to see each folder’s full path and ID. Earlier versions resolved only top-level folder names. (#216)
searchAllFolders: true returns fewer results than an inbox searchFixed in v3.9.0 — cross-folder search now returns a superset of an inbox-scoped search; broadening the scope never shrinks the result set. (#169)
searchExpression="from:…" / subject:"…" returns 0 on a personal accountFixed in v3.10.0 — field-scoped $search expressions return nothing on personal Outlook.com accounts even when matching mail exists. Expressions built purely from from:, to: and subject: terms are now translated into the closest equivalent OData filters and retried (strategy raw-kql-translated, rewrite recorded in searchMetadata.kqlTranslatedTo) — note a subject: term becomes a substring match, so the translation is close rather than identical. Boolean operators, grouping, wildcards and other field prefixes are deliberately not translated and still terminate. Unscoped expressions were never affected. (#217)
Search with two filters returns results matching only one of themFixed in v3.10.0 — when Graph rejected the combined filter, the ladder returned the first single-term result set and dropped the rest, while still reporting filterApplied: true. Remaining terms are now applied locally, and searchMetadata.droppedFilters reports anything that could not be honoured. (#229)
search-emails with an apostrophe in from/to finds nothingFixed in v3.10.0 — values such as O'Brien were interpolated unescaped into the OData filter, producing a Graph 400 that the fallback ladder swallowed into “No emails found”. (#230)
Empty search suggests “use from filter instead of to”Fixed in v3.10.0 — that suggestion had been false since v3.7.1 added the client-side to fallback, and it printed on every empty search regardless of what you actually supplied. No-results guidance is now derived from the parameters you passed and the strategies actually attempted, and reports any client-side fallback including how many messages it examined. (#231)
to combined with receivedAfter/receivedBefore returns mail addressed to someone elseFixed in v3.11.1 — the single-term rung built the recipient filter and then had it overwritten by the date/boolean filter, so the request went out carrying only the date window and the whole window came back labelled single-term-to with filterApplied: true. from, subject and query were affected identically. Filters now compose. (defect report 2026-09-09)
export target=messages writes fewer files than the IDs you passedFixed in v3.11.1 — per-message filenames were <date>_<subject>, so every message in a same-day reply chain overwrote the previous one while the summary still reported Failed 0. Filenames now carry the message time, collisions get a numbered suffix instead of clobbering (-1, -2 since v3.12.1; _2, _3 before), and _meta.manifest maps each requested ID to the path actually written so you can reconcile. Attachment names had the same defect (emailId.substring(0, 8) is a shared prefix across a mailbox, not a disambiguator). (extends #82, which covered only the aggregated CSV)
searchExpression returns irrelevant top hits where query finds the right messageWorking as designed, now documented. They are different requests, not the same one ranked differently: an untranslated searchExpression goes to Graph $search, which matches the whole message including the body and ranks by relevance with no date sort — so a term buried in a body can outrank the obvious subject-line match. query on a personal account is a subject substring match, which is why it looks more accurate for subject terms and misses body text entirely. Pick query for subject terms, searchExpression to reach bodies.
A to search misses mail you know exists further backThe server-side recipient filter is rejected on personal Outlook.com, so to is matched locally over the 500 most recent messages. On a large archive that silently excludes anything older. Since v3.11.1 the response says so whenever the scan was truncated (previously only on empty results). Narrow with receivedAfter/receivedBefore, or raise OUTLOOK_SEARCH_SCAN_LIMIT (max 5000).
list-events only shows upcoming eventsThat’s the default. Pass startAfter, startBefore and/or subject (v3.12.0+) to look back or search by name — supplying any of them replaces the implicit “from now” bound. startBefore alone, or subject alone, returns the most recent matches first.
list-events returns Invalid startAfter / Invalid startBeforeDates must be ISO 8601 with Z or a ±hh:mm offset, e.g. 2026-01-01T00:00:00Z or 2026-01-01T09:00:00+10:00. Zone-less (2026-01-01T09:00:00), date-only (2026-01-01), impossible (2026-02-30…) and pre-1900 dates are rejected rather than guessed, and startAfter must be earlier than startBefore. Nothing is sent to Graph when an argument is invalid.
Invalid resource path: IDs must not contain "." or ".." path segmentsSince v3.12.0 a message, folder or attachment ID containing a . or .. path segment (including percent-encoded forms) is refused before any request is made, so it can’t be resolved against a different Graph resource. Real Graph IDs never contain these segments — copy the ID again from search-emails or folders action=list.
A tool hangs, or fails with Request timed out after 60000 msSince v3.12.1 a Graph request attempt that receives no data for OUTLOOK_REQUEST_TIMEOUT_MS (default 60000 ms) is abandoned instead of waiting forever. This is an inactivity timeout, not an overall deadline: a slow response that keeps arriving isn’t cut off. Read requests are retried once after a timeout or dropped connection; writes are not, so check whether a timed-out change (a move or flag, say) actually landed before repeating it. A sent email is never re-sent after a timeout. On a slow link, raise OUTLOOK_REQUEST_TIMEOUT_MS.
API call failed with status 429 (throttling), or 503/504Microsoft Graph throttles bursts of requests. Since v3.12.1 these are retried automatically up to 3 times, waiting as long as Graph’s Retry-After header asks (or backing off up to 30 seconds), with at most 4 requests in flight at once. POST requests (send, reply, move, $batch) are never re-sent after a timeout or network error; they are only retried on 429, and only when Graph asks for a wait of 10 seconds or less each (20 seconds in total), to stay well inside a typical MCP client timeout. If you still see the error, Graph asked for a longer wait (over 60 seconds, or over 10 seconds for a send) or kept throttling — wait a minute, check whether a send actually went out, then repeat with a smaller batch.
Refusing to call non-Graph URLA deltaToken or continuation link pointed somewhere other than https://graph.microsoft.com (v3.11.2+). Pass the token exactly as a previous search-emails deltaMode=true call returned it, or start a fresh delta sync.
Refusing to write file outside outputDirAn internal backstop: export and attachments action=download keep every file name they choose inside the folder they’re writing to (outputDir, or the folder savePath names), v3.12.0+; before v3.12.1 the message read Refusing to write export file outside outputDir. You shouldn’t see this in normal use; if you do, report it with the arguments you used. A path outside the allowed folders gets the Refusing to write to <path> errors below instead.
Refusing to write to <path>: exports and attachment downloads can only write inside …export and attachments action=download only write inside the system temp directory, ~/Downloads, ~/Documents or the folder in OUTLOOK_EXPORT_DIR. The path is resolved first, so .. or a symlinked folder that leads elsewhere is refused too. Pick a path inside one of the listed folders, or set OUTLOOK_EXPORT_DIR to an absolute path in the server’s env block and restart. See Where exports can be written.
Refusing to write to <path>: … never write to a dotfile or into a dot-directoryNothing is written to a name starting with . below the allowed folders (for example ~/Documents/project/.git/). Choose a path without a dot-prefixed folder or file name.
File already exists: <path>. Nothing was written.Since v3.14.0, export with a savePath file path doesn’t replace an existing file unless you pass overwrite: true (earlier versions replaced it silently). Alternatively pass a directory as savePath (or use outputDir) and a new, unique name is chosen. A symlink, a hard-linked file, a dotfile or a file in a dot-directory below the allowed folder is never replaced.
Refusing to write to "<path>": output paths must be absolutesavePath and outputDir must be absolute paths, or start with ~/ for your home directory (for example ~/Downloads/report.md). A relative path such as report.md is refused because it would land wherever the server happens to run. Pass a full path, or leave the path out to use the system temp directory.
Refusing to replace <path>: it is a symbolic linkThe savePath you gave is a symlink. It isn’t followed, even when it points to a file inside an allowed folder, and it’s never replaced with overwrite: true. Pass the real file’s path, or a directory so a new, unique name is used.
Recipient not allowed: "<text>" (not a single plain email address) (or the same wording in a draft, rule or event refusal)With OUTLOOK_ALLOWED_RECIPIENTS set, each recipient must be one plain address: exactly one @, and no ;, <, >, quotes, brackets, spaces or control characters. Split multiple addresses with commas (a@company.com, b@company.com) and leave out display names (Alice <alice@company.com>).
Event refused: OUTLOOK_ALLOWED_RECIPIENTS does not allow attendee … (or Event update refused: …)With a recipient allowlist set, every create-event attendee, and every address in the attendee list you pass to manage-event action=update, must be allowed. Nothing was created or changed and nobody was invited. Remove those attendees, or ask whoever runs the server to add them to OUTLOOK_ALLOWED_RECIPIENTS and restart it.
<tool> is blocked: OUTLOOK_MAX_…_PER_SESSION=0 turns off … (or … is not a whole number)Since v3.14.1 a session limit of 0 blocks that tool instead of meaning no limit, and a value that isn’t a whole number blocks it too. Nothing was sent or changed. For no limit, remove the setting (or leave it empty) and restart the server; for a cap, use a whole number above 0. auth action=about lists each tool’s limit. In the Claude Code plugin, clear the Send limit per session setting for no limit.
Rate limit reached: N create-event operations per sessioncreate-event counts towards OUTLOOK_MAX_CREATE_EVENT_PER_SESSION, or OUTLOOK_MAX_EMAILS_PER_SESSION if that’s set instead. dryRun previews don’t count. Restart the server to reset the count, or raise the limit.
Draft not sent: it is addressed to …, which OUTLOOK_ALLOWED_RECIPIENTS does not allowSince v3.14.0 draft action=send re-checks the draft’s current to/cc/bcc against the allowlist, so recipients added later (here or in Outlook) are caught. The draft is unchanged and still in Drafts. Remove those recipients (in Outlook or with draft action=update), or ask whoever runs the server to allow them.
The reply-all draft would be addressed to … (or The reply draft …)With a recipient allowlist set, draft action=reply/reply-all checks the recipients Graph copies from the original message. If any isn’t allowed, the new draft is deleted again and nothing is kept. Use reply instead of reply-all, or draft action=create addressed only to allowed recipients.
dryRun is not supported for <tool> action=<action>; nothing was changed.Since v3.14.0 dryRun: true is refused on any call that can’t preview, instead of being ignored and running for real. Only send-email, draft create, create-event, manage-event, manage-rules create/update, mailbox-settings set-auto-replies, and folders and manage-contact delete preview. For anything else, describe the change, confirm it, and call it without dryRun.
OUTLOOK_SHARED_MAILBOX is set but auth action=about shows the .Shared scopes as not grantedA token refresh never adds scopes: since v3.12.0 it requests only the scopes granted at sign-in plus offline_access (#241), so an existing token keeps its old scope set. Restart the server and run auth action=authenticate force=true once. If they’re still not granted, the account is personal (it can’t hold them) or the device-code flow fell back to the standard scopes — see the rows above.
kqlQuery shown as deprecatedkqlQuery was renamed to searchExpression in v3.9.0 (it was always a Microsoft Graph $search expression, never full KQL). The kqlQuery alias still works for back-compat — prefer searchExpression.
Shared-mailbox read works but move/categorise/flag/create-folder fails with 404 ErrorInvalidMailboxItemId, “folder not found”, or the change lands in your own mailboxThe write tool must target the shared mailbox, and the token must carry Mail.ReadWrite.Shared. (1) Pass sharedMailbox (alias email) on the write tool — folders action=move, folders action=create, apply-category, update-email. (2) Set OUTLOOK_SHARED_MAILBOX=true (read requests only Mail.Read.Shared), add the delegated Mail.ReadWrite.Shared permission to your Azure app and grant consent. (3) Restart and re-authenticate with auth action=authenticate force=true so the token includes the new scope. Omitting sharedMailbox targets your own mailbox, where the shared message ID doesn’t exist (the 404); a missing scope makes the shared-scoped request fail with 403 — it never falls back to your own mailbox.
Invalid mailbox "…" — expected a shared mailbox email addresssharedMailbox (or email) must be a plain email address such as team@contoso.com. Since v3.12.0 only printable ASCII is accepted, so look-alike characters (full-width /, zero-width spaces), #, %, / and spaces are refused. User GUIDs aren’t accepted either.
Mail sent/replied/forwarded “from” a shared mailbox arrives from your own addressWorking as designed — shared-mailbox support covers reading and organising only. send-email and draft (create/update/send/delete, reply, reply-all, forward) always act on the signed-in user’s mailbox and accept no sharedMailbox parameter; Mail.Send.Shared is not requested. Use the Outlook UI for send-as / send-on-behalf.
A change is refused with “Outlook Assistant is in read-only mode (OUTLOOK_READ_ONLY)”Read-only mode is on, so every call that isn’t a read is refused before it runs, including dry runs, exports and attachment downloads. Nothing was changed. To allow changes, remove OUTLOOK_READ_ONLY from the server’s env block (or set it to false; in the Claude Code plugin, turn off Read-only mode) and restart the server. A value the server doesn’t recognise, such as a typo, turns the mode on and logs a warning on stderr. auth action=about shows whether it’s on.
send-email says “Email not sent: the recipient check flagged …”You passed checkRecipients: true and the mail tips showed an out-of-office reply, a full mailbox, a delivery restriction, an external recipient or a group with external members. Nothing was sent and the send limit wasn’t used. Check the listed warnings; to send anyway, repeat the call with acknowledgeWarnings: true. “Email not sent: the recipient check failed” means the mail tips lookup itself failed: retry, or send without checkRecipients. See When send-email Refuses to Send.
manage-rules says “Rule refused: OUTLOOK_ALLOWED_RECIPIENTS does not allow …”A forwardTo or redirectTo address isn’t on the recipient allowlist, so the whole rule (or update) was refused and nothing was created or changed. Remove the blocked address, or add it to OUTLOOK_ALLOWED_RECIPIENTS and restart the server. Earlier versions saved such a rule with the forwarding silently dropped.
manage-rules create or update fails with 400 UnableToDeserializePostBody on a rule using hasAttachments or exceptHasAttachmentsFixed in v3.14.1 (#300). Earlier versions sent hasAttachment, but Graph’s rule property is hasAttachments, so any such rule was rejected, and list didn’t show the condition on rules made in Outlook. Update the server and try again.
export with a savePath ending in / wrote an extensionless file instead of creating a folderFixed in v3.14.1 (#301). A savePath ending in / now always names a folder: it is created (0700) if it doesn’t exist yet, and the export gets a new, unique file name inside it.
Automatic-reply start and end times look hours out, in US date orderFixed in v3.14.1 (#304). Earlier versions printed Graph’s UTC time with no zone. mailbox-settings now shows the UTC instant followed by a labelled local time in the display timezone (OUTLOOK_DEFAULT_TIMEZONE).

Client-Specific Issues

These apply to the plugin’s safety hook and to particular clients. What each client supports is set out in Supported Clients and Their Limits.

IssueSolution
Cursor sign-in fails with AADSTS900023, or an error shows literal ${user_config.…} textThe v3.13.0 plugin had no Cursor manifest, so Cursor loaded the Claude Code manifest and passed its ${user_config.*} placeholders to the server as text instead of your client ID. Update to the v3.14.0 plugin, which includes .cursor-plugin/plugin.json, or remove the plugin and use a manual .cursor/mcp.json with OUTLOOK_CLIENT_ID set (see the README).
Cursor asks “Run this MCP tool?” without saying whyA known Cursor limitation (checked in Cursor CLI 2026.10.01; the desktop app hasn’t been checked). When the hook asks, Cursor shows its own generic prompt and drops the hook’s reason. The prompt still means the hook flagged the call as reaching other people, deleting something or keeping acting, so check the tool name and arguments before you approve.
The hook doesn’t ask before a send, delete or rule changeReads and dryRun: true previews never ask, and with a manual MCP configuration there’s no hook at all. Otherwise: Claude Code: in bypass permissions mode Claude Code may auto-approve the hook’s prompts; add the permissions.ask rules from the plugin README. Allow rules don’t skip the hook. Cursor: an Mcp(...) allow rule or --force / Run Everything mode runs the call without asking, so remove any allow rule covering Outlook’s send, rule or delete tools. Any client: check the confirmation level isn’t off.
GitHub Copilot or VS Code ignores the confirmation levelThey have no plugin settings, so the hook reads OUTLOOK_CONFIRM_LEVEL from the environment. Set it in the shell you start Copilot CLI from (export OUTLOOK_CONFIRM_LEVEL=all-writes), or in the environment VS Code starts with, then restart the client. Putting it in the server’s env block does nothing: the server doesn’t read it. An unrecognised value counts as outward. Cursor reads it the same way.
The hook times outThe hook allows 30 seconds and normally finishes far sooner. What happens on a timeout depends on the client: GitHub Copilot CLI and VS Code let the call through, while Cursor blocks it. The hook runs with node, so check that node is on the PATH your client starts from.
Writes are denied in a headless run (claude -p, copilot -p)Nobody is there to answer the hook’s prompt, so it becomes a denial. Copilot reports “Denied by preToolUse hook (unable to ask user …)”, and the Copilot cloud agent treats every prompt as a denial. The hook doesn’t block calls it wouldn’t ask about. Make sends, deletes and rule changes in an interactive session.
A call is refused although you approved the promptThe server’s own checks run after the hook and the client’s prompt, and approving a prompt doesn’t override them. A “read-only mode” refusal means OUTLOOK_READ_ONLY is on: see the read-only row above. Allowlist, send-limit and mail-tips refusals are final too; change the setting, or repeat a mail-tips refusal with acknowledgeWarnings: true.

Checking Authentication State

The simplest check is from your AI assistant: auth action=status reports whether the token is valid and when it expires, and auth action=about shows the connected mailbox, the configured and granted scopes, shared-mailbox status and the safety-belt settings, without ever printing a token.

From a shell:

# Token state (redacted)
cat ~/.outlook-assistant-tokens.json | python3 -c "import json,sys; t=json.load(sys.stdin); print('auth_method:', t.get('auth_method')); print('expires_at:', t.get('expires_at')); print('has access_token:', bool(t.get('access_token')))"

# Pending device-code state (only present between authenticate and device-code-complete)
ls -la ~/.outlook-assistant-pending-auth.json 2>/dev/null || echo "No pending flow"

Forcing a Fresh Auth

Usually auth action=authenticate force=true is enough, followed by auth action=device-code-complete. If tokens are corrupted or stuck:

rm ~/.outlook-assistant-tokens.json ~/.outlook-assistant-pending-auth.json
# Then call the auth tool again with action=authenticate

Server Logs and Debug Logging

The server writes its logs to stderr (stdout carries the MCP protocol). Where you read them depends on the client: Claude Desktop writes them to mcp-server-<name>.log in its logs folder (<name> is the key in your MCP config), and the MCP Inspector (npm run inspect) shows them live.

By default, after the startup lines, each tool call logs exactly one line and never its arguments:

tool=search-emails outcome=ok ms=412
tool=folders action=create outcome=isError ms=230 graph="409 POST me/mailFolders/{id}/childFolders"
tool=read-email outcome=isError ms=95 auth=refresh-failed:AADSTS70008

outcome is ok, isError (the tool returned an error to the assistant), thrown (with error=<class>) or unknown-tool. Optional fields: graph= is the last failed Graph request in the call (status or network error code, method, and the path with IDs shown as {id} and mailboxes as <mailbox>), graphRetries= counts throttling retries, and auth= is a sign-in or token problem with its AADSTS code. Search terms, filters, addresses, folder names, subjects and Message-IDs don’t appear.

To see what a tool actually did, set OUTLOOK_DEBUG=true (also 1, yes, on) in the server’s env block and restart it. Lines starting [debug] then show the search strategy and filters tried, the Graph request and error bodies, folder resolution and the auth steps. Even then, email addresses (including Message-IDs) become <redacted-email> and long IDs <id>; tokens, device codes and the client secret are never logged in either mode. Debug output can still include search terms, subjects and folder names, so check it before pasting it into an issue, and turn debug off when you’re done.

Reporting Issues

Report issues at https://github.com/littlebearapps/outlook-assistant/issues with:

  • Error message (full text)
  • The server’s stderr lines for the failing call (the default one-line log is safe to share; check OUTLOOK_DEBUG output for search terms or subjects first)
  • The output of auth action=about (version, configured and granted scopes; it never includes tokens). Don’t paste the token file.
  • Auth method: device code or browser
  • Client and version (for example Claude Code, Copilot CLI or Cursor), and whether you installed the plugin or a manual MCP configuration
  • Account type: personal (Microsoft/Outlook.com) or work/school
Was this helpful?

Related Articles