@joshluedeman/m365-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Josh Luedeman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # 📬 m365-mcp
2
+
3
+ **Microsoft 365 tools for any MCP-compatible AI client — Mail, Calendar, Tasks, and Contacts via the Microsoft Graph API.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@joshluedeman/m365-mcp)](https://www.npmjs.com/package/@joshluedeman/m365-mcp)
6
+ [![license](https://img.shields.io/npm/l/@joshluedeman/m365-mcp)](./LICENSE)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/joshluedeman/m365-mcp/ci.yml?label=CI)](https://github.com/joshluedeman/m365-mcp/actions)
8
+ [![node](https://img.shields.io/node/v/@joshluedeman/m365-mcp)](https://www.npmjs.com/package/@joshluedeman/m365-mcp)
9
+
10
+ `m365-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server that connects your Microsoft 365 account to any MCP-compatible AI client. It exposes 22 tools across Mail, Calendar, Tasks (Microsoft To Do), and Contacts — all authenticated via a single device code sign-in with no Azure portal setup required. It runs over stdio and works with Claude Desktop, Claude Code, Cursor, Zed, Windsurf, and any other MCP-compatible client.
11
+
12
+ ---
13
+
14
+ ## ✨ Features
15
+
16
+ - 📧 **Mail** — search, read, send, flag, and organize email across folders
17
+ - 📅 **Calendar** — search events, create/update meetings, find free/busy availability
18
+ - ✅ **Tasks** — full Microsoft To Do integration: task lists, tasks, create/complete/delete
19
+ - 👤 **Contacts** — search, read, create, and update personal contacts
20
+ - 🔐 **Zero Azure setup** — ships with a pre-registered multi-tenant app; no portal configuration required
21
+ - 📦 **npx distribution** — run with `npx @joshluedeman/m365-mcp`, no global install needed
22
+ - 🖥 **Any MCP client** — Claude Desktop, Claude Code, Cursor, Zed, Windsurf, and more
23
+
24
+ ---
25
+
26
+ ## 🏗 How it works
27
+
28
+ ```mermaid
29
+ graph LR
30
+ User["User"] --> Client["MCP Client"]
31
+ Client -->|"MCP stdio"| Server["m365-mcp"]
32
+ Server -->|"HTTPS"| Graph["Microsoft Graph API"]
33
+ Graph --> Mail["Mail"]
34
+ Graph --> Calendar["Calendar"]
35
+ Graph --> Tasks["Tasks"]
36
+ Graph --> Contacts["Contacts"]
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 🚀 Quick Start
42
+
43
+ **1. (Optional) Install globally**
44
+
45
+ ```bash
46
+ npm install -g @joshluedeman/m365-mcp
47
+ ```
48
+
49
+ Or skip this and use `npx` directly — it works either way.
50
+
51
+ **2. Run setup**
52
+
53
+ ```bash
54
+ npx @joshluedeman/m365-mcp setup
55
+ ```
56
+
57
+ This opens a browser for device code sign-in. Sign in with your Microsoft 365 account and the token is cached to `~/.config/m365-mcp/token-cache.json`. You will not be prompted again unless the refresh token expires (~90 days).
58
+
59
+ **3. Add to your MCP client**
60
+
61
+ Most MCP clients accept a JSON server definition in this form:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "m365": {
67
+ "command": "npx",
68
+ "args": ["m365-mcp"]
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ **Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
75
+
76
+ **Claude Code** — add via CLI:
77
+
78
+ ```bash
79
+ claude mcp add m365 -- npx @joshluedeman/m365-mcp
80
+ ```
81
+
82
+ **Cursor** — add to `.cursor/mcp.json` in your project or `~/.cursor/mcp.json` globally.
83
+
84
+ **Zed** — add to your Zed settings under `"context_servers"`.
85
+
86
+ **Other clients** — consult your client's MCP server configuration docs; the command is `npx @joshluedeman/m365-mcp`.
87
+
88
+ ---
89
+
90
+ ## 🔐 Authentication
91
+
92
+ Authentication uses the [MSAL device code flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code). No Azure CLI, no service principal, no secrets to manage.
93
+
94
+ ```mermaid
95
+ sequenceDiagram
96
+ participant U as User
97
+ participant S as m365-mcp setup
98
+ participant A as Azure AD
99
+
100
+ U->>S: npx @joshluedeman/m365-mcp setup
101
+ S->>A: Request device code
102
+ A-->>S: Device code + verification URL
103
+ S-->>U: Display URL and code
104
+ U->>A: Open browser, enter code, sign in
105
+ A-->>S: Access token + refresh token
106
+ S->>S: Cache tokens to ~/.config/m365-mcp/token-cache.json
107
+ S-->>U: Done — setup complete
108
+ ```
109
+
110
+ On subsequent runs the server loads the cached token silently. The refresh token is valid for approximately 90 days; re-run `npx @joshluedeman/m365-mcp setup` if it expires.
111
+
112
+ ---
113
+
114
+ ## 🛠 Available Tools
115
+
116
+ ### Mail
117
+
118
+ | Tool | Description |
119
+ |------|-------------|
120
+ | `search_emails` | Search emails by keyword, sender, date range, or folder |
121
+ | `read_email` | Read the full content of an email by ID |
122
+ | `send_email` | Send a new email |
123
+ | `flag_email` | Flag or unflag an email for follow-up |
124
+ | `list_mail_folders` | List all mail folders in the mailbox |
125
+ | `move_email` | Move an email to a different folder |
126
+
127
+ ### Calendar
128
+
129
+ | Tool | Description |
130
+ |------|-------------|
131
+ | `search_events` | Search calendar events by keyword or time range |
132
+ | `get_event` | Get full details of a calendar event by ID |
133
+ | `create_event` | Create a new calendar event |
134
+ | `update_event` | Update an existing calendar event |
135
+ | `find_availability` | Find free/busy availability across attendees |
136
+
137
+ ### Tasks (Microsoft To Do)
138
+
139
+ | Tool | Description |
140
+ |------|-------------|
141
+ | `list_task_lists` | List all task lists |
142
+ | `list_tasks` | List tasks in a task list |
143
+ | `get_task` | Get details of a specific task |
144
+ | `create_task` | Create a new task in a task list |
145
+ | `update_task` | Update an existing task |
146
+ | `complete_task` | Mark a task as completed |
147
+ | `delete_task` | Delete a task |
148
+
149
+ ### Contacts
150
+
151
+ | Tool | Description |
152
+ |------|-------------|
153
+ | `search_contacts` | Search personal contacts by name or email |
154
+ | `get_contact` | Get full details of a contact by ID |
155
+ | `create_contact` | Create a new personal contact |
156
+ | `update_contact` | Update an existing contact |
157
+
158
+ ---
159
+
160
+ ## ⚙️ Enterprise / Advanced Configuration
161
+
162
+ ### Environment variables
163
+
164
+ | Variable | Description |
165
+ |----------|-------------|
166
+ | `M365_MCP_CLIENT_ID` | Override the built-in app registration with your own Azure AD client ID |
167
+ | `M365_MCP_TENANT_ID` | Restrict authentication to a specific tenant (defaults to `common`) |
168
+
169
+ ### Config file override
170
+
171
+ Create `~/.config/m365-mcp/config.json` to set options persistently:
172
+
173
+ ```json
174
+ {
175
+ "clientId": "your-azure-ad-app-client-id",
176
+ "tenantId": "your-tenant-id"
177
+ }
178
+ ```
179
+
180
+ Environment variables take precedence over the config file.
181
+
182
+ ### Enterprise consent
183
+
184
+ The built-in app registration is multi-tenant and supports personal Microsoft accounts. First-time sign-in for work/school accounts will show an **"unverified publisher"** consent screen — this is expected for community-distributed apps. An Azure AD admin can pre-consent on behalf of the organization to suppress this prompt for all users. If your organization requires a verified app registration, use the `M365_MCP_CLIENT_ID` override with your own registered application.
185
+
186
+ ---
187
+
188
+ ## 🤝 Contributing
189
+
190
+ Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, branching conventions, and the pull request process.
191
+
192
+ ---
193
+
194
+ ## 📄 License
195
+
196
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,207 @@
1
+ // src/auth/msal-client.ts
2
+ import fs2 from "fs";
3
+ import path2 from "path";
4
+ import os2 from "os";
5
+ import { PublicClientApplication } from "@azure/msal-node";
6
+
7
+ // src/auth/token-cache.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ var CACHE_DIR = path.join(os.homedir(), ".config", "m365-mcp");
12
+ var CACHE_PATH = path.join(CACHE_DIR, "token-cache.json");
13
+ function ensureCacheDir() {
14
+ if (!fs.existsSync(CACHE_DIR)) {
15
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
16
+ }
17
+ }
18
+ var tokenCachePlugin = {
19
+ async beforeCacheAccess(tokenCacheContext) {
20
+ try {
21
+ if (fs.existsSync(CACHE_PATH)) {
22
+ const data = fs.readFileSync(CACHE_PATH, "utf-8");
23
+ tokenCacheContext.tokenCache.deserialize(data);
24
+ }
25
+ } catch (err) {
26
+ process.stderr.write(`[m365-mcp] token cache read error (starting fresh): ${String(err)}
27
+ `);
28
+ }
29
+ },
30
+ async afterCacheAccess(tokenCacheContext) {
31
+ if (!tokenCacheContext.cacheHasChanged) {
32
+ return;
33
+ }
34
+ try {
35
+ ensureCacheDir();
36
+ const data = tokenCacheContext.tokenCache.serialize();
37
+ fs.writeFileSync(CACHE_PATH, data, { encoding: "utf-8", mode: 384 });
38
+ fs.chmodSync(CACHE_PATH, 384);
39
+ } catch (err) {
40
+ process.stderr.write(`[m365-mcp] token cache write error: ${String(err)}
41
+ `);
42
+ }
43
+ }
44
+ };
45
+
46
+ // src/auth/app-config.ts
47
+ var DEFAULT_CLIENT_ID = "dc9f42bd-30a5-4c43-a498-305ef0c3ad87";
48
+
49
+ // src/auth/msal-client.ts
50
+ var USER_CONFIG_PATH = path2.join(os2.homedir(), ".config", "m365-mcp", "config.json");
51
+ var LOCAL_CONFIG_PATH = path2.join(process.cwd(), ".m365-mcp.json");
52
+ function readConfigFile(filePath) {
53
+ try {
54
+ if (fs2.existsSync(filePath)) {
55
+ const raw = fs2.readFileSync(filePath, "utf-8");
56
+ return JSON.parse(raw);
57
+ }
58
+ } catch {
59
+ }
60
+ return null;
61
+ }
62
+ function loadClientId() {
63
+ const envClientId = process.env["M365_MCP_CLIENT_ID"];
64
+ if (envClientId) {
65
+ return envClientId;
66
+ }
67
+ const userConfig = readConfigFile(USER_CONFIG_PATH);
68
+ if (userConfig?.clientId) {
69
+ return userConfig.clientId;
70
+ }
71
+ const localConfig = readConfigFile(LOCAL_CONFIG_PATH);
72
+ if (localConfig?.clientId) {
73
+ return localConfig.clientId;
74
+ }
75
+ if (DEFAULT_CLIENT_ID) {
76
+ return DEFAULT_CLIENT_ID;
77
+ }
78
+ throw new Error('No client ID configured. Run "npx m365-mcp setup" to get started.');
79
+ }
80
+ function loadTenantId() {
81
+ const envTenantId = process.env["M365_MCP_TENANT_ID"];
82
+ if (envTenantId) {
83
+ return envTenantId;
84
+ }
85
+ const userConfig = readConfigFile(USER_CONFIG_PATH);
86
+ if (userConfig?.tenantId) {
87
+ return userConfig.tenantId;
88
+ }
89
+ const localConfig = readConfigFile(LOCAL_CONFIG_PATH);
90
+ if (localConfig?.tenantId) {
91
+ return localConfig.tenantId;
92
+ }
93
+ return "common";
94
+ }
95
+ var _msalClient;
96
+ function getMsalClient() {
97
+ if (_msalClient) {
98
+ return _msalClient;
99
+ }
100
+ const clientId = loadClientId();
101
+ const tenantId = loadTenantId();
102
+ const msalConfig = {
103
+ auth: {
104
+ clientId,
105
+ authority: `https://login.microsoftonline.com/${tenantId}`
106
+ },
107
+ cache: {
108
+ cachePlugin: tokenCachePlugin
109
+ },
110
+ system: {
111
+ loggerOptions: {
112
+ loggerCallback: (_level, message, containsPii) => {
113
+ if (!containsPii) {
114
+ process.stderr.write(`[msal] ${message}
115
+ `);
116
+ }
117
+ },
118
+ piiLoggingEnabled: false
119
+ }
120
+ }
121
+ };
122
+ _msalClient = new PublicClientApplication(msalConfig);
123
+ return _msalClient;
124
+ }
125
+
126
+ // src/auth/scopes.ts
127
+ var GRAPH_SCOPES = [
128
+ "Mail.ReadWrite",
129
+ "Mail.Send",
130
+ "Calendars.ReadWrite",
131
+ "Tasks.ReadWrite",
132
+ "Contacts.ReadWrite",
133
+ "offline_access",
134
+ "User.Read"
135
+ ];
136
+
137
+ // src/auth/device-code-flow.ts
138
+ async function trySilentAcquire() {
139
+ const client = getMsalClient();
140
+ const accounts = await client.getAllAccounts();
141
+ if (accounts.length === 0) {
142
+ return null;
143
+ }
144
+ for (const account of accounts) {
145
+ try {
146
+ const result = await client.acquireTokenSilent({
147
+ account,
148
+ scopes: GRAPH_SCOPES
149
+ });
150
+ if (result?.accessToken) {
151
+ return result.accessToken;
152
+ }
153
+ } catch {
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ async function acquireToken() {
159
+ const silentToken = await trySilentAcquire();
160
+ if (silentToken) {
161
+ return silentToken;
162
+ }
163
+ const client = getMsalClient();
164
+ const deviceCodeRequest = {
165
+ scopes: GRAPH_SCOPES,
166
+ deviceCodeCallback: (response) => {
167
+ process.stderr.write("\n[m365-mcp] Authentication required\n");
168
+ process.stderr.write(`[m365-mcp] ${response.message}
169
+
170
+ `);
171
+ }
172
+ };
173
+ const result = await client.acquireTokenByDeviceCode(deviceCodeRequest);
174
+ if (!result?.accessToken) {
175
+ throw new Error("Device code authentication did not return an access token.");
176
+ }
177
+ return result.accessToken;
178
+ }
179
+ async function runAuthCommand() {
180
+ process.stderr.write("[m365-mcp] Starting authentication...\n");
181
+ try {
182
+ const token = await acquireToken();
183
+ if (!token) {
184
+ process.stderr.write("[m365-mcp] Authentication failed \u2014 no token returned.\n");
185
+ process.exit(1);
186
+ }
187
+ const client = getMsalClient();
188
+ const accounts = await client.getAllAccounts();
189
+ const account = accounts[0];
190
+ if (account) {
191
+ process.stderr.write(`[m365-mcp] Authenticated as: ${account.username}
192
+ `);
193
+ } else {
194
+ process.stderr.write("[m365-mcp] Authentication successful.\n");
195
+ }
196
+ } catch (err) {
197
+ process.stderr.write(`[m365-mcp] Authentication error: ${String(err)}
198
+ `);
199
+ process.exit(1);
200
+ }
201
+ }
202
+
203
+ export {
204
+ acquireToken,
205
+ runAuthCommand
206
+ };
207
+ //# sourceMappingURL=chunk-JEMHJMEL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth/msal-client.ts","../src/auth/token-cache.ts","../src/auth/app-config.ts","../src/auth/scopes.ts","../src/auth/device-code-flow.ts"],"sourcesContent":["import fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport { PublicClientApplication } from '@azure/msal-node';\nimport type { Configuration } from '@azure/msal-node';\nimport { tokenCachePlugin } from './token-cache.js';\nimport { DEFAULT_CLIENT_ID } from './app-config.js';\n\n// Priority 2: ~/.config/m365-mcp/config.json (written by `setup` command)\nconst USER_CONFIG_PATH = path.join(os.homedir(), '.config', 'm365-mcp', 'config.json');\n// Priority 3: legacy per-directory config\nconst LOCAL_CONFIG_PATH = path.join(process.cwd(), '.m365-mcp.json');\n\ninterface M365Config {\n clientId: string;\n tenantId?: string;\n}\n\nfunction readConfigFile(filePath: string): M365Config | null {\n try {\n if (fs.existsSync(filePath)) {\n const raw = fs.readFileSync(filePath, 'utf-8');\n return JSON.parse(raw) as M365Config;\n }\n } catch {\n // unreadable or malformed — treat as missing\n }\n return null;\n}\n\nfunction loadClientId(): string {\n // 1. Environment variable\n const envClientId = process.env['M365_MCP_CLIENT_ID'];\n if (envClientId) {\n return envClientId;\n }\n\n // 2. ~/.config/m365-mcp/config.json (written by `setup` command)\n const userConfig = readConfigFile(USER_CONFIG_PATH);\n if (userConfig?.clientId) {\n return userConfig.clientId;\n }\n\n // 3. .m365-mcp.json in cwd (legacy fallback)\n const localConfig = readConfigFile(LOCAL_CONFIG_PATH);\n if (localConfig?.clientId) {\n return localConfig.clientId;\n }\n\n // 4. Shared publisher-registered app ID baked into the package\n if (DEFAULT_CLIENT_ID) {\n return DEFAULT_CLIENT_ID;\n }\n\n throw new Error('No client ID configured. Run \"npx m365-mcp setup\" to get started.');\n}\n\nfunction loadTenantId(): string {\n const envTenantId = process.env['M365_MCP_TENANT_ID'];\n if (envTenantId) {\n return envTenantId;\n }\n\n // 2. ~/.config/m365-mcp/config.json\n const userConfig = readConfigFile(USER_CONFIG_PATH);\n if (userConfig?.tenantId) {\n return userConfig.tenantId;\n }\n\n // 3. .m365-mcp.json in cwd (legacy fallback)\n const localConfig = readConfigFile(LOCAL_CONFIG_PATH);\n if (localConfig?.tenantId) {\n return localConfig.tenantId;\n }\n\n // Use common endpoint for multi-tenant / personal accounts\n return 'common';\n}\n\nlet _msalClient: PublicClientApplication | undefined;\n\nexport function getMsalClient(): PublicClientApplication {\n if (_msalClient) {\n return _msalClient;\n }\n\n const clientId = loadClientId();\n const tenantId = loadTenantId();\n\n const msalConfig: Configuration = {\n auth: {\n clientId,\n authority: `https://login.microsoftonline.com/${tenantId}`,\n },\n cache: {\n cachePlugin: tokenCachePlugin,\n },\n system: {\n loggerOptions: {\n loggerCallback: (_level, message, containsPii) => {\n if (!containsPii) {\n process.stderr.write(`[msal] ${message}\\n`);\n }\n },\n piiLoggingEnabled: false,\n },\n },\n };\n\n _msalClient = new PublicClientApplication(msalConfig);\n return _msalClient;\n}\n","import fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport type { ICachePlugin, TokenCacheContext } from '@azure/msal-node';\n\nconst CACHE_DIR = path.join(os.homedir(), '.config', 'm365-mcp');\nconst CACHE_PATH = path.join(CACHE_DIR, 'token-cache.json');\n\nfunction ensureCacheDir(): void {\n if (!fs.existsSync(CACHE_DIR)) {\n fs.mkdirSync(CACHE_DIR, { recursive: true });\n }\n}\n\nexport const tokenCachePlugin: ICachePlugin = {\n async beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void> {\n try {\n if (fs.existsSync(CACHE_PATH)) {\n const data = fs.readFileSync(CACHE_PATH, 'utf-8');\n tokenCacheContext.tokenCache.deserialize(data);\n }\n } catch (err) {\n // Cache file unreadable or corrupt — start fresh\n process.stderr.write(`[m365-mcp] token cache read error (starting fresh): ${String(err)}\\n`);\n }\n },\n\n async afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void> {\n if (!tokenCacheContext.cacheHasChanged) {\n return;\n }\n try {\n ensureCacheDir();\n const data = tokenCacheContext.tokenCache.serialize();\n fs.writeFileSync(CACHE_PATH, data, { encoding: 'utf-8', mode: 0o600 });\n // Enforce 600 even if file already existed with different permissions\n fs.chmodSync(CACHE_PATH, 0o600);\n } catch (err) {\n process.stderr.write(`[m365-mcp] token cache write error: ${String(err)}\\n`);\n }\n },\n};\n\nexport { CACHE_PATH };\n","// Shared public client app registration for m365-mcp.\n// Registered once by the publisher; all users share this client ID.\n// Override via M365_MCP_CLIENT_ID env var or ~/.config/m365-mcp/config.json for enterprise use.\nexport const DEFAULT_CLIENT_ID = 'dc9f42bd-30a5-4c43-a498-305ef0c3ad87';\n","/**\n * Microsoft Graph API scopes required by this server.\n * offline_access is required for refresh token acquisition.\n */\nexport const GRAPH_SCOPES: string[] = [\n 'Mail.ReadWrite',\n 'Mail.Send',\n 'Calendars.ReadWrite',\n 'Tasks.ReadWrite',\n 'Contacts.ReadWrite',\n 'offline_access',\n 'User.Read',\n];\n","import type { AccountInfo, AuthenticationResult, DeviceCodeRequest } from '@azure/msal-node';\nimport { getMsalClient } from './msal-client.js';\nimport { GRAPH_SCOPES } from './scopes.js';\n\n/**\n * Attempts silent token acquisition using cached accounts.\n * Returns null if no cached accounts exist or silent acquisition fails.\n */\nasync function trySilentAcquire(): Promise<string | null> {\n const client = getMsalClient();\n const accounts = await client.getAllAccounts();\n\n if (accounts.length === 0) {\n return null;\n }\n\n // Try each cached account — use the first one that succeeds\n for (const account of accounts) {\n try {\n const result: AuthenticationResult | null = await client.acquireTokenSilent({\n account,\n scopes: GRAPH_SCOPES,\n });\n if (result?.accessToken) {\n return result.accessToken;\n }\n } catch {\n // Silent acquisition failed for this account — try next or fall through to device code\n }\n }\n\n return null;\n}\n\n/**\n * Acquires an access token. Tries silent auth first; falls back to device code flow.\n * Prints device code instructions to stderr (stdout is reserved for MCP JSON-RPC).\n */\nexport async function acquireToken(): Promise<string> {\n const silentToken = await trySilentAcquire();\n if (silentToken) {\n return silentToken;\n }\n\n const client = getMsalClient();\n\n const deviceCodeRequest: DeviceCodeRequest = {\n scopes: GRAPH_SCOPES,\n deviceCodeCallback: (response) => {\n process.stderr.write('\\n[m365-mcp] Authentication required\\n');\n process.stderr.write(`[m365-mcp] ${response.message}\\n\\n`);\n },\n };\n\n const result: AuthenticationResult | null = await client.acquireTokenByDeviceCode(deviceCodeRequest);\n\n if (!result?.accessToken) {\n throw new Error('Device code authentication did not return an access token.');\n }\n\n return result.accessToken;\n}\n\n/**\n * CLI `auth` subcommand — runs device code flow interactively and reports the signed-in account.\n */\nexport async function runAuthCommand(): Promise<void> {\n process.stderr.write('[m365-mcp] Starting authentication...\\n');\n\n try {\n const token = await acquireToken();\n\n if (!token) {\n process.stderr.write('[m365-mcp] Authentication failed — no token returned.\\n');\n process.exit(1);\n }\n\n // Retrieve account info for confirmation message\n const client = getMsalClient();\n const accounts: AccountInfo[] = await client.getAllAccounts();\n const account = accounts[0];\n\n if (account) {\n process.stderr.write(`[m365-mcp] Authenticated as: ${account.username}\\n`);\n } else {\n process.stderr.write('[m365-mcp] Authentication successful.\\n');\n }\n } catch (err) {\n process.stderr.write(`[m365-mcp] Authentication error: ${String(err)}\\n`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":["fs","path","os","path","os","fs"]}