graph-cli 3.1.0

dotnet tool install --global graph-cli --version 3.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local graph-cli --version 3.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=graph-cli&version=3.1.0
                    
nuke :add-package graph-cli --version 3.1.0
                    

graph-cli

A .NET global tool for interacting with Microsoft Graph — manage emails, calendar events, Teams chats, To Do tasks, presence, user directory, and OneDrive/SharePoint files from the command line or via MCP for AI assistants like Claude. Output is JSON by default (--format table for human-readable output).

Installation

Requires .NET 10 SDK or later.

# Install from NuGet
dotnet tool install -g graph-cli

Or install from source:

git clone https://github.com/afroze9/graph-cli.git
cd graph-cli
dotnet pack -o ./nupkg
dotnet tool install -g graph-cli --add-source ./nupkg

Upgrading from 2.x to 3.x

Version 3.0 replaced the stateful "active account" model with stateless per-invocation profiles. If you only ever used a single account, the upgrade is transparent — your existing login keeps working and everything resolves to the default profile. If you used multiple accounts (or run graph-cli as an MCP server), read this section: the way you select an account has changed, and there are breaking removals.

What changed and why

In 2.x there was one mutable "active account" pointer. You changed accounts with auth switch, which rewrote shared state on disk — so two terminals (or two MCP servers) could never use two different accounts at once, and a background switch could yank the account out from under a running command.

In 3.x there is no active-account state to switch. Each invocation picks its account independently, with this precedence:

--profile <name>   >   GRAPH_CLI_PROFILE env var   >   "default"

Two shells, or two MCP servers, can now use two different accounts simultaneously with zero interference.

Breaking changes

2.x 3.x Action needed
graph-cli auth switch <account> (removed) Select per command with --profile <name>, or set GRAPH_CLI_PROFILE for the shell.
Named accounts (e.g. auth login <name>) Named profiles (auth login --profile <name>) Your accounts carry over as profiles of the same name; no re-login needed unless prompted to re-pin (below).
auth status returned the active account auth status reports the resolved profile (new profile field; add --profile to inspect a specific one) Update any scripts that parse auth status output.
config.json had an ActiveAccount field Field dropped Ignored automatically — nothing to do.

Config migration (automatic)

Your ~/.graph-cli/config.json and token cache (token-cache.bin) migrate in place on first run of 3.x — no manual edits:

  • A legacy single-account config ({ tenantId, clientId, scopes }) is rewritten into a multi-profile store under the default profile.
  • Existing named accounts become profiles of the same name.

One caveat: if several of your accounts shared a single app registration, the token cache holds multiple identities that a migrated profile can't tell apart, and you'll see:

Profile '<name>' isn't pinned to an identity and the token cache holds N accounts.

Fix it once per affected profile — this re-pins it to the right identity (no app re-registration):

graph-cli auth login --profile <name>

Confirm everything landed with graph-cli auth list (shows every profile and which one the current invocation resolves to).

Migrating your MCP config (Claude Desktop / Claude Code)

This is the biggest change for AI-assistant users. In 2.x a single MCP server followed the active account, and you changed accounts by running auth switch out-of-band. In 3.x you bind one MCP server per account and pin it with mcp --profile <name>; the assistant chooses the account by choosing the tool (tools are namespaced by server label). See Using as an MCP Server for the full walkthrough — the short version:

Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json on Windows, ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    // 2.x — single server, account chosen by out-of-band `auth switch`:
    // "graph-cli": { "command": "graph-cli", "args": ["mcp"] }

    // 3.x — one entry per account, each pinned to a profile:
    "graph-cli": {
      "command": "graph-cli",
      "args": ["mcp", "--profile", "default"]
    },
    "graph-cli-work": {
      "command": "graph-cli",
      "args": ["mcp", "--profile", "work"]
    }
  }
}

Claude Code — re-add each account as its own server:

claude mcp add graph-cli      -- graph-cli mcp --profile default
claude mcp add graph-cli-work -- graph-cli mcp --profile work    # add -s user to make it global

Tools are namespaced by server name (graph-climcp__graph-cli__*, graph-cli-workmcp__graph-cli-work__*). If you only have one account, keep the single graph-cli entry and drop --profile (it defaults to default). Restart the client after editing.

Tip: log each profile in from a terminal (graph-cli auth login --profile <name> ...) before pointing an MCP server at it — the server reuses cached profiles and never prompts for login itself.

Setup (First-Time Users)

1. Register an Azure AD App

  1. Go to Azure Portal → App registrations and click New registration
  2. Name it something like graph-cli, set Supported account types to "Single tenant", and set Redirect URI to http://localhost (type: Public client/native)
  3. After creation, copy the Application (client) ID and Directory (tenant) ID from the Overview page
  4. Go to API permissions → Add a permission → Microsoft Graph → Delegated permissions and add:
    • User.Read, User.ReadBasic.All
    • Mail.ReadWrite, Mail.Send
    • Calendars.Read.Shared, Calendars.ReadWrite
    • Chat.Create, Chat.ReadWrite, ChatMessage.Read, ChatMessage.Send
    • Presence.Read.All
    • Tasks.ReadWrite
    • Files.Read.All, Sites.ReadWrite.All (or Sites.ReadWrite.All for page create/update/publish)
  5. Click Grant admin consent (or ask your tenant admin to do this)

2. Configure graph-cli

Create ~/.graph-cli/config.json:

{
  "tenantId": "<your-tenant-id>",
  "clientId": "<your-client-id>"
}

Or set environment variables GRAPH_CLI_TENANT_ID and GRAPH_CLI_CLIENT_ID instead.

3. Authenticate

graph-cli auth login    # Opens browser for interactive auth (only needed once)
graph-cli auth status   # Check if authenticated
graph-cli auth list     # List all configured profiles
graph-cli auth logout   # Clear cached tokens

Tokens are cached at ~/.graph-cli/token-cache.bin and auto-refresh silently — you won't need to log in again unless you explicitly log out.

Profiles (multiple accounts)

Every account is a named profile. If you never name one, everything uses the profile default, so single-account users can ignore this entirely.

There is no stateful "active account" to switch — you select a profile per invocation, and it never mutates shared state. This means two shells (or two MCP servers) can use two different accounts at the same time without interfering. Selection precedence:

--profile <name>   >   GRAPH_CLI_PROFILE env var   >   "default"
# Log in a second account under a named profile
graph-cli auth login --profile work --tenant <tenant-id> --client <client-id>

# Use a specific profile for one command
graph-cli mail list --profile work

# Or set it for a whole shell session (no persisted state, no switch-back)
export GRAPH_CLI_PROFILE=work
graph-cli mail list

# See every profile and which one the current invocation resolves to
graph-cli auth list

Each profile is pinned to its Microsoft identity at login, so profiles that share one app registration still resolve deterministically.

Using as an MCP Server (Claude Desktop, Cursor, etc.)

graph-cli includes a built-in MCP (Model Context Protocol) server, so AI assistants like Claude can interact with your Microsoft 365 data directly.

1. Register an Azure AD App

Follow step 1 from Setup above if you haven't already.

2. Log in first

Authenticate each account once from a terminal so its profile and tokens are cached (see Profiles):

graph-cli auth login                                                   # → "default" profile
graph-cli auth login --profile work --tenant <id> --client <id>       # → a second account

The MCP server reuses these cached profiles; it never prompts for login itself.

3. Add to Claude Desktop

Edit your Claude Desktop config file:

  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Bind one server per account and select the profile with --profile:

{
  "mcpServers": {
    "graph-cli": {
      "command": "graph-cli",
      "args": ["mcp", "--profile", "default"]
    },
    "graph-cli-work": {
      "command": "graph-cli",
      "args": ["mcp", "--profile", "work"]
    }
  }
}

Each server is pinned to a single account for its whole lifetime — no state switching. Their tools are namespaced by the server label (graph-climcp__graph-cli__*, graph-cli-workmcp__graph-cli-work__*), so the assistant picks the account by choosing the tool. If you only have one account, keep the single graph-cli entry and drop --profile (it defaults to default).

Zero-config bootstrap: if you'd rather not run auth login first, you can instead supply GRAPH_CLI_TENANT_ID and GRAPH_CLI_CLIENT_ID in the server's env block. A browser window opens for login on the first tool call. This path is single-account only; use profiles (above) for multiple accounts.

4. Add to Claude Code

Register the server with the claude mcp add command — same binary, same --profile flag:

# Default account
claude mcp add graph-cli -- graph-cli mcp --profile default

# A second account, as its own server
claude mcp add graph-cli-work -- graph-cli mcp --profile work

This writes to your Claude Code MCP config (use -s user to make it global across projects). Verify with claude mcp list. As in Desktop, each entry is one account and tools are namespaced by the server name.

5. Restart the client

Restart Claude Desktop (or reload Claude Code) and you should see graph-cli tools available. Ask something like "What's my email address?" or "Show my calendar for today" to verify.

Troubleshooting

  • "Authentication required" / "No cached token" — Run graph-cli auth login (add --profile <name> for a named account) in a terminal, then retry.
  • "Profile '<name>' not found" — The server's --profile doesn't match a logged-in profile. Run graph-cli auth list to see configured profiles, and graph-cli auth login --profile <name> --tenant <id> --client <id> to add it.
  • "isn't pinned to an identity" — A profile migrated from an older single-account config has multiple identities in the token cache. Run graph-cli auth login --profile <name> once to pin the correct one.
  • Tools not appearing — Run graph-cli auth status --profile <name> in a terminal to confirm that profile authenticates.

Quick Start

# Check your profile
graph-cli user me --format table

# See latest emails
graph-cli mail list --top 5 --format table

# Check today's calendar
graph-cli calendar events --format table

# Allow a contact before sending
graph-cli contacts allow jane@company.com --actions email,chat
graph-cli mail send --to jane@company.com --subject "Hello" --body "Hi Jane"

Commands

Command Availability

All commands are available via both the CLI and the MCP server unless noted otherwise.

Command CLI MCP MCP Tool Name
Auth
auth login
auth status auth_status
auth list
auth logout
User
user me user_me
user get user_get
user search user_search
user manager user_manager
user reports user_reports
Mail
mail list mail_list
mail get mail_get
mail search mail_search
mail send mail_send
mail draft mail_draft
mail send-draft mail_send_draft
mail reply mail_reply
mail reply-all mail_reply (with replyAll=true)
mail forward mail_forward
mail move mail_move
mail delete mail_delete
mail mark-read mail_mark_read
mail folders mail_folders
mail attachments mail_attachments
mail download-attachment
Calendar
calendar list calendar_list
calendar events calendar_events
calendar get-event calendar_get_event
calendar create-event calendar_create_event
calendar update-event calendar_update_event
calendar delete-event calendar_delete_event
calendar respond calendar_respond
calendar find-times calendar_find_times
calendar schedule calendar_schedule
Chat (Teams)
chat list chat_list
chat search chat_search
chat find-with chat_find_with
chat get chat_get
chat create chat_create
chat members chat_members
chat messages chat_messages
chat since chat_since
chat send chat_send
chat send-image chat_send_image
chat reply chat_reply
Presence
presence me presence_me
presence get presence_get
presence batch presence_batch
Tasks (To Do)
task lists task_lists
task list task_list
task create task_create
task update task_update
task delete task_delete
task complete task_complete
Files (OneDrive/SharePoint)
files list files_list
files get files_get
files search files_search
files share files_share
files download files_download
Contacts (Allow-List)
contacts list contacts_list
contacts allow contacts_allow
contacts remove contacts_remove
Sites (SharePoint)
sites search sites_search
Pages (SharePoint)
pages list pages_list
pages get pages_get
pages create pages_create
pages update pages_update
pages publish pages_publish
pages sections list pages_sections_list
pages sections get pages_sections_get
pages sections create pages_sections_create
pages sections update pages_sections_update
pages sections delete pages_sections_delete
pages columns list pages_columns_list
pages webparts list pages_webparts_list
pages webparts get pages_webparts_get
pages webparts create pages_webparts_create
pages webparts update pages_webparts_update
pages webparts delete pages_webparts_delete
Lists (SharePoint)
lists list lists_list
lists items lists_items
lists columns lists_columns

Commands marked in MCP are either interactive-only (auth login, auth logout) or involve file system write operations (download-attachment) that require explicit user confirmation outside the MCP context.

Mail

graph-cli mail list [--top <n>] [--folder <name>]
graph-cli mail get <message-id>
graph-cli mail search --query <text> [--top <n>]
graph-cli mail send --to <emails> --subject <text> --body <text> [--cc <emails>] [--content-type text|html] [--attachment <file> ...]
graph-cli mail draft --to <emails> --subject <text> --body <text>
graph-cli mail send-draft <message-id>
graph-cli mail reply <message-id> --body <text> [--cc <emails>] [--bcc <emails>] [--content-type text|html] [--attachment <file> ...] [--draft]
graph-cli mail reply-all <message-id> --body <text> [--cc <emails>] [--bcc <emails>] [--content-type text|html] [--attachment <file> ...] [--draft]
graph-cli mail forward <message-id> --to <emails> --body <text> [--cc <emails>] [--bcc <emails>] [--content-type text|html] [--attachment <file> ...] [--draft]
graph-cli mail mark-read <message-id> [<id2> ...] [--unread]
graph-cli mail move <message-id> [<id2> ...] --folder <folder-id-or-name>
graph-cli mail delete <message-id> [<id2> ...]
graph-cli mail folders [--parent <folder-id-or-name>]
graph-cli mail attachments <message-id>
graph-cli mail download-attachment <message-id> <attachment-id> [--out <path>]

Calendar

graph-cli calendar list
graph-cli calendar events [--start <iso-date>] [--end <iso-date>] [--calendar-id <id>] [--top <n>]
graph-cli calendar get-event <event-id>
graph-cli calendar create-event --subject <text> --start <iso-datetime> --end <iso-datetime> \
    [--attendees <emails>] [--body <text>] [--content-type text|html] \
    [--categories <names>] [--location <text>] [--online-meeting] [--calendar-id <id>]
graph-cli calendar update-event <event-id> [--subject <text>] [--start <datetime>] [--end <datetime>] \
    [--body <text>] [--content-type text|html] [--categories <names>]
graph-cli calendar delete-event <event-id>
graph-cli calendar respond <event-id> --action accept|decline|tentative [--comment <text>]
graph-cli calendar find-times --attendees <emails> --duration <minutes> [--start <iso-datetime>] [--end <iso-datetime>]
graph-cli calendar schedule --users <emails> --start <iso-datetime> --end <iso-datetime>

Chat (Teams)

graph-cli chat list [--top <n>]
graph-cli chat search --query <text> [--top <n>] [--refresh] [--max-depth <n>]
graph-cli chat find-with --user <email-or-id> [--type oneOnOne|group|all] [--top <n>]
graph-cli chat get <chat-id>
graph-cli chat create --members <emails> [--topic <text>] [--type oneOnOne|group]
graph-cli chat members <chat-id>
graph-cli chat messages <chat-id> [--top <n>]
graph-cli chat since [--since <when>] [--continue] [--max-chats <n>] [--include-system] [--exclude-own] [--no-save-watermark]
graph-cli chat send <chat-id> --message <text> [--content-type text|html] [--mentions <emails>]
graph-cli chat send-image <chat-id> --image <path> [--caption <text>]
graph-cli chat reply <chat-id> <message-id> --message <text> [--content-type text|html] [--mentions <emails>]

Note: chat reply works for both 1:1 and group chats. It sends a quoted reply using a messageReference attachment, which Teams renders as a native reply with the original message quoted above.

Note: chat messages includes a reactions array on each message, listing any emoji reactions (e.g. a 👍 thumbs-up). Each reaction reports reactionType (the emoji), displayName (a friendly label such as Like), createdDateTime, and userId (the AAD id of who reacted). This lets you detect when someone acknowledged a message with a reaction rather than a text reply.

Note: chat since fetches every new message across all your Teams chats since a cutoff — built for automation (dump to SQLite/JSON, extract tasks, etc.). --since accepts ISO 8601 (2026-07-15T13:00), a bare time (1pm = today), today/yesterday, or a relative offset (-3h, -2d, -1w). It enumerates chats by most-recent activity and short-circuits once past the cutoff, so it stays cheap to poll. Each run saves a watermark (the newest message timestamp); pass --continue (or omit --since) next time to fetch only what has arrived since — no overlap, no duplicates. The result is { since, chatsScanned, count, watermark, messages[] } with messages oldest-first; each carries chatId, chatTopic (topic or participant names), from, createdDateTime, raw body plus an HTML-stripped text, and attachments. System join/leave/rename events are excluded unless you pass --include-system; --exclude-own drops your own messages; --no-save-watermark leaves the --continue cursor untouched (useful for ad-hoc queries).

Presence

graph-cli presence me
graph-cli presence get <user-id>
graph-cli presence batch --user-ids <comma-separated-ids>

Tasks (Microsoft To Do)

graph-cli task lists
graph-cli task list <list-id> [--status notStarted|inProgress|completed]
graph-cli task create <list-id> --title <text> [--due <iso-date>] [--importance low|normal|high] [--body <text>]
graph-cli task update <list-id> <task-id> [--title <text>] [--status notStarted|inProgress|completed] [--due <date>] [--importance low|normal|high]
graph-cli task complete <list-id> <task-id>
graph-cli task delete <list-id> <task-id>

Files (OneDrive & SharePoint)

graph-cli files list [--folder <item-id>] [--drive-id <id>] [--site <site-id-or-hostname>] [--top <n>]
graph-cli files get <item-id-or-sharing-url> [--drive-id <id>] [--site <site>]
graph-cli files download <item-id-or-sharing-url> [--out <path>] [--drive-id <id>] [--site <site>]
graph-cli files search <query> [--drive-id <id>] [--site <site>] [--top <n>] [--refresh]
graph-cli files share <item-id-or-sharing-url> --recipients <emails> [--role read|write|owner] [--message <text>] [--drive-id <id>] [--site <site>]

User Directory

graph-cli user me
graph-cli user get <user-id-or-email>
graph-cli user search --query <text>
graph-cli user manager
graph-cli user reports

Contacts Allow-List

Outbound actions (mail send, chat send, file share, etc.) are gated by an allowed contacts list for safety.

graph-cli contacts allow <email-or-group> --actions email,chat,share [--name "Display Name"] [--type user|group]
graph-cli contacts list [--type user|group]
graph-cli contacts remove <email-or-group>

SharePoint Sites, Pages & Lists

# Sites
graph-cli sites search <query> [--top <n>] [--refresh]

# Pages — CRUD and publish
graph-cli pages list --site <site> [--top <n>] [--search <text>]
graph-cli pages get <page-id> --site <site> [--expand-content]
graph-cli pages create --site <site> --title <text> [--name <filename.aspx>] [--content <html>] [--publish]
graph-cli pages update <page-id> --site <site> [--title <text>] [--content <html>] [--publish]
graph-cli pages publish <page-id> --site <site>

# Sections — manage page layout sections
graph-cli pages sections list --site <site> --page-id <id>
graph-cli pages sections get <section-id> --site <site> --page-id <id>
graph-cli pages sections create --site <site> --page-id <id> --layout fullWidth|twoColumn|threeColumn|oneThirdLeftColumn|oneThirdRightColumn [--emphasis none|neutral|soft|strong]
graph-cli pages sections update <section-id> --site <site> --page-id <id> [--layout <layout>] [--emphasis <emphasis>]
graph-cli pages sections delete <section-id> --site <site> --page-id <id>

# Columns — inspect columns within a section
graph-cli pages columns list --site <site> --page-id <id> --section-id <id>

# WebParts — manage content within columns
graph-cli pages webparts list --site <site> --page-id <id> [--section-id <id> --column-id <id>]
graph-cli pages webparts get <webpart-id> --site <site> --page-id <id> --section-id <id> --column-id <id>
graph-cli pages webparts create --site <site> --page-id <id> --section-id <id> --column-id <id> --inner-html <html>
graph-cli pages webparts create --site <site> --page-id <id> --section-id <id> --column-id <id> --webpart-type <guid> [--data-json <json>]
graph-cli pages webparts update <webpart-id> --site <site> --page-id <id> --section-id <id> --column-id <id> [--inner-html <html>] [--data-json <json>]
graph-cli pages webparts delete <webpart-id> --site <site> --page-id <id> --section-id <id> --column-id <id>

# Lists
graph-cli lists list --site <site>
graph-cli lists items <list-id> --site <site> [--top <n>] [--fields <field1,field2>] [--filter <odata-filter>] [--expand-lookups <col1,col2>]
graph-cli lists columns <list-id> --site <site>   # column definitions: displayName + internal name

Architecture

graph-cli uses a layered architecture where core business logic lives in shared services, exposed via both the CLI and MCP interfaces:

Services/           ← Core logic (Graph SDK calls)
  ├── UserService.cs
  ├── MailService.cs
  ├── CalendarService.cs
  ├── ChatService.cs
  ├── FileService.cs
  ├── ...
  │
Commands/           ← CLI layer (arg parsing, output formatting)
  ├── UserCommands.cs
  ├── MailCommands.cs
  ├── ChatCommands.cs
  ├── ...
  │
McpTools/           ← MCP layer (JSON serialization, tool registration)
  ├── UserTools.cs
  ├── MailTools.cs
  ├── ChatTools.cs
  ├── FilesTools.cs
  ├── ...

Global Options

Option Description
--format json\|table Output format (default: json)
--timezone <tz> Timezone for datetime I/O — accepts IANA (e.g. Asia/Karachi) or Windows IDs (e.g. Pakistan Standard Time). Defaults to system local timezone.
--profile <name> Account profile to use for this invocation. Overrides GRAPH_CLI_PROFILE; defaults to default.

Environment Variables

Variable Description
GRAPH_CLI_PROFILE Account profile to use when --profile is not passed (default: default)
GRAPH_CLI_TENANT_ID Azure AD tenant ID (alternative to config.json)
GRAPH_CLI_CLIENT_ID Azure AD client/application ID (alternative to config.json)
GRAPH_CLI_SCOPES Comma-separated Microsoft Graph scopes (default: all required scopes)
GRAPH_CLI_SKIP_ALLOWLIST Set to true to bypass the allowed contacts check for outbound actions (send email, chat, share)

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
3.1.0 0 7/15/2026
3.0.0 119 7/1/2026
2.10.2 100 7/1/2026
2.10.1 112 6/9/2026
2.10.0 107 6/3/2026
2.7.0 118 5/7/2026
2.6.1 94 5/7/2026
2.6.0 96 5/7/2026
2.5.1 126 4/29/2026
2.5.0 129 4/16/2026
2.4.0 128 4/16/2026
2.3.3 111 4/13/2026
2.3.0 127 4/12/2026
2.2.2 108 4/10/2026
2.2.1 123 4/10/2026
2.2.0 133 4/10/2026
2.1.1 108 4/10/2026
2.1.0 107 4/10/2026
2.0.1 121 4/10/2026
2.0.0 124 4/9/2026
Loading failed