@littlebearapps/outlook-assistant 3.4.1 → 3.5.1

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/.env.example CHANGED
@@ -20,3 +20,11 @@ USE_TEST_MODE=false
20
20
 
21
21
  # Restrict sending to specific domains/addresses (comma-separated)
22
22
  # OUTLOOK_ALLOWED_RECIPIENTS=mycompany.com,partner@example.com
23
+
24
+ # Optional: Enable immutable IDs (IDs persist through folder moves)
25
+ # OUTLOOK_IMMUTABLE_IDS=true
26
+
27
+ # Optional: Default authentication method (device-code or browser)
28
+ # device-code: No auth server needed, works remotely/headless
29
+ # browser: Traditional OAuth redirect via localhost:3333
30
+ # OUTLOOK_AUTH_METHOD=device-code
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="docs/assets/outlook-assistant-logo-full.svg" height="200" alt="Outlook Assistant" />
2
+ <img src="https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/assets/outlook-assistant-logo-full.png" height="200" alt="Outlook Assistant" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Outlook Assistant</h1>
@@ -11,13 +11,9 @@
11
11
  <p align="center">
12
12
  <a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/v/@littlebearapps/outlook-assistant" alt="npm version" /></a>
13
13
  <a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/dm/@littlebearapps/outlook-assistant" alt="npm downloads" /></a>
14
- <a href="https://github.com/littlebearapps/outlook-assistant/stargazers"><img src="https://img.shields.io/github/stars/littlebearapps/outlook-assistant" alt="GitHub stars" /></a>
15
- <a href="https://github.com/littlebearapps/outlook-assistant/commits/main"><img src="https://img.shields.io/github/last-commit/littlebearapps/outlook-assistant" alt="Last commit" /></a>
16
14
  <a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
17
15
  <a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml/badge.svg" alt="CodeQL" /></a>
18
- <a href="https://github.com/littlebearapps/outlook-assistant/issues"><img src="https://img.shields.io/github/issues/littlebearapps/outlook-assistant" alt="Open issues" /></a>
19
16
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
20
- <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="Node.js" /></a>
21
17
  </p>
22
18
 
23
19
  Outlook Assistant connects AI assistants to your Microsoft Outlook account through the [Model Context Protocol](https://modelcontextprotocol.io/). Ask your AI assistant to search your inbox, send emails, schedule meetings, manage contacts, and configure mailbox settings — without leaving the conversation. Works with Claude, Cursor, Windsurf, and any MCP-compatible client.
@@ -37,7 +33,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
37
33
  ### What you can do
38
34
 
39
35
  - 📨 **Search and read emails** — find messages by sender, subject, date, or keywords; read full threads with conversation grouping; batch flag, move, export, or categorise multiple emails at once
40
- - 🛡️ **Send emails with safety controls** — dry-run preview, session rate limiting, and recipient allowlist to prevent mistakes
36
+ - 🛡️ **Send emails with safety controls** — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
41
37
  - 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel invitations
42
38
  - 📦 **Export emails** — save to Markdown, EML, MBOX, JSON, or HTML for archiving, analysis, or migration; export search results or entire threads in one call
43
39
  - 🔍 **Investigate email headers** — check DKIM, SPF, and DMARC authentication; trace delivery chains; analyse spam scores — useful for phishing investigation and compliance
@@ -64,7 +60,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
64
60
 
65
61
  | Module | Tools | What You Can Do |
66
62
  |--------|------:|-----------------|
67
- | **Email** | 6 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run), `update-email` (read status, flags), `attachments`, `export` |
63
+ | **Email** | 7 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
68
64
  | **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (decline/cancel/delete) |
69
65
  | **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
70
66
  | **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
@@ -74,7 +70,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
74
70
  | **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
75
71
  | **Auth** | 1 | `auth` (status/authenticate/about) |
76
72
 
77
- **20 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
73
+ **21 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
78
74
 
79
75
  ### Export Formats
80
76
 
@@ -115,6 +111,7 @@ Outlook Assistant works with both personal and work/school Microsoft accounts, b
115
111
  - **Email forensics** — full header analysis (DKIM, SPF, DMARC, delivery chain, spam scores) built in as a first-class feature — useful for phishing investigation, compliance, and security review.
116
112
  - **Delta sync** — incremental inbox monitoring returns only what changed since your last check, with tokens for continuous polling. Designed for agent workflows that need to watch a mailbox.
117
113
  - **Batch operations** — flag, move, export, or categorise multiple emails in a single call. Search-driven export lets you batch-export results without collecting IDs manually.
114
+ - **Pre-send intelligence** — check recipients for out-of-office, full mailbox, delivery restrictions, and moderation status before sending — no other Outlook MCP server offers this.
118
115
  - **Compound automation** — rules, categories, folders, and Focused Inbox work together. Set up complete inbox management through your AI assistant in one conversation.
119
116
 
120
117
  ## Safety & Token Efficiency
@@ -124,11 +121,12 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
124
121
  **Destructive action safeguards** — Every tool carries [MCP annotations](https://modelcontextprotocol.io/docs/concepts/tools#annotations) (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so AI clients can auto-approve safe reads and prompt for confirmation on destructive operations like sending email or deleting events.
125
122
 
126
123
  **Send-email protections** — The `send-email` tool includes:
124
+ - **Pre-send mail tips** (`checkRecipients: true`) — check recipients for out-of-office, mailbox full, delivery restrictions before sending
127
125
  - **Dry-run mode** (`dryRun: true`) — preview composed emails without sending
128
126
  - **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: unlimited)
129
127
  - **Recipient allowlist** — restrict sending to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`
130
128
 
131
- **Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 20 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
129
+ **Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 21 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
132
130
 
133
131
  > **Important**: These safeguards are defence-in-depth measures that reduce risk, but they are not a guarantee against unintended actions. AI-driven access to your email is inherently sensitive — always review tool calls before approving, particularly for sends and deletes. No automated guardrail is foolproof, and you remain responsible for actions taken through your mailbox.
134
132
 
@@ -154,6 +152,7 @@ You need a Microsoft Azure app registration to authenticate. See the **[Azure Se
154
152
  2. Set redirect URI to `http://localhost:3333/auth/callback`
155
153
  3. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
156
154
  4. Create a client secret and copy the **Value** (not the Secret ID)
155
+ 5. Enable **"Allow public client flows"** in Authentication > Advanced settings (for device code flow)
157
156
 
158
157
  ### 3. Configure Your MCP Client
159
158
 
@@ -191,6 +190,10 @@ Then set environment variables in your `.env` or shell.
191
190
  <details>
192
191
  <summary><strong>Cursor</strong> (<code>.cursor/mcp.json</code>)</summary>
193
192
 
193
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Outlook%20Assistant&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaXR0bGViZWFyYXBwcy9vdXRsb29rLWFzc2lzdGFudCJdLCJlbnYiOnsiT1VUTE9PS19DTElFTlRfSUQiOiIiLCJPVVRMT09LX0NMSUVOVF9TRUNSRVQiOiIifX0=)
194
+
195
+ Or add manually to `.cursor/mcp.json`:
196
+
194
197
  ```json
195
198
  {
196
199
  "mcpServers": {
@@ -338,7 +341,21 @@ If installed from source, use `node` instead of `npx`:
338
341
 
339
342
  ## Authentication Flow
340
343
 
341
- ### Step 1: Start the Auth Server
344
+ ### Device Code Flow (Default — Recommended)
345
+
346
+ No auth server needed. Works everywhere, including remote/headless environments.
347
+
348
+ 1. Ask your AI assistant to authenticate (calls `auth` tool with `action=authenticate`)
349
+ 2. Visit the URL shown (`microsoft.com/devicelogin`) on **any** browser, **any** device
350
+ 3. Enter the code, sign in with your Microsoft account, and grant permissions
351
+ 4. Tell your AI assistant to complete authentication (calls `auth` with `action=device-code-complete`)
352
+ 5. Tokens are saved to `~/.outlook-assistant-tokens.json` and **refresh automatically**
353
+
354
+ > **Prerequisite**: Enable "Allow public client flows" in Azure Portal > your app > Authentication > Advanced settings.
355
+
356
+ ### Browser Redirect Flow (Alternative)
357
+
358
+ For localhost development or if you prefer the traditional OAuth flow:
342
359
 
343
360
  ```bash
344
361
  npm run auth-server
@@ -346,24 +363,22 @@ npm run auth-server
346
363
 
347
364
  This starts a local server on port 3333 to handle the OAuth callback.
348
365
 
349
- > **Note**: The auth server reads `OUTLOOK_CLIENT_ID` and `OUTLOOK_CLIENT_SECRET` from environment variables (or `MS_CLIENT_ID`/`MS_CLIENT_SECRET`). When running the auth server separately, ensure your `.env` file is in the project root or export the variables in your shell. Your MCP client's `"env"` config only applies to the MCP server process, not a separately-started auth server.
350
-
351
- ### Step 2: Authenticate
352
-
353
- 1. In your AI assistant, use the `auth` tool with `action=authenticate`
366
+ 1. In your AI assistant, use the `auth` tool with `action=authenticate, method=browser`
354
367
  2. Open the provided URL in your browser
355
- 3. Sign in with your Microsoft account and grant permissions
356
- 4. Tokens are saved to `~/.outlook-assistant-tokens.json` and refresh automatically
368
+ 3. Sign in and grant permissions — tokens are saved automatically
369
+
370
+ > **Note**: The auth server reads `OUTLOOK_CLIENT_ID` and `OUTLOOK_CLIENT_SECRET` from environment variables. Your MCP client's `"env"` config only applies to the MCP server process, not a separately-started auth server.
357
371
 
358
372
  ## Directory Structure
359
373
 
360
374
  ```
361
375
  outlook-assistant/
362
- ├── index.js # Main entry point (20 tools)
376
+ ├── index.js # Main entry point (21 tools)
363
377
  ├── config.js # Configuration settings
364
378
  ├── outlook-auth-server.js # OAuth server (port 3333)
365
379
  ├── auth/ # Authentication module (1 tool)
366
- ├── email/ # Email module (6 tools)
380
+ ├── email/ # Email module (7 tools)
381
+ │ ├── mail-tips.js # Pre-send recipient validation
367
382
  │ ├── headers.js # Email header retrieval
368
383
  │ ├── mime.js # Raw MIME/EML content
369
384
  │ ├── conversations.js # Thread listing/export
@@ -377,7 +392,7 @@ outlook-assistant/
377
392
  ├── rules/ # Rules module (1 tool)
378
393
  ├── advanced/ # Advanced module (2 tools)
379
394
  └── utils/
380
- ├── graph-api.js # Microsoft Graph API client
395
+ ├── graph-api.js # Microsoft Graph API client (includes $batch)
381
396
  ├── safety.js # Rate limiting, recipient allowlist, dry-run
382
397
  ├── odata-helpers.js # OData query building
383
398
  ├── field-presets.js # Token-efficient field selections
@@ -406,7 +421,11 @@ You're using the Secret **ID** instead of the Secret **Value**. Go to Azure Port
406
421
 
407
422
  ### Authentication URL doesn't work
408
423
 
409
- Start the auth server first: `npm run auth-server`
424
+ If using browser flow: start the auth server first with `npm run auth-server`. If using device code flow: visit `microsoft.com/devicelogin` instead.
425
+
426
+ ### Device code "invalid_client"
427
+
428
+ Enable "Allow public client flows" in Azure Portal > App registrations > Authentication > Advanced settings.
410
429
 
411
430
  ### Empty API responses
412
431
 
@@ -444,9 +463,9 @@ USE_TEST_MODE=true npm start
444
463
  |-------|-------------|
445
464
  | [Getting Started](docs/how-to/getting-started/connect-outlook-to-claude.md) | Install, configure, and authenticate — start here |
446
465
  | [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
447
- | [How-To Guides](docs/how-to/index.md) | 27 practical guides for email, calendar, contacts, and settings |
466
+ | [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
448
467
  | [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
449
- | [Tools Reference](docs/quickrefs/tools-reference.md) | All 20 tools with parameters |
468
+ | [Tools Reference](docs/quickrefs/tools-reference.md) | All 21 tools with parameters |
450
469
  | [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
451
470
 
452
471
  Full documentation: [docs/](docs/README.md)
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Device Code Flow for Microsoft OAuth2
3
+ *
4
+ * Enables authentication without browser redirect — ideal for
5
+ * headless/remote environments (SSH, VPS, containers).
6
+ *
7
+ * The user gets a short code, visits https://microsoft.com/devicelogin
8
+ * on any device, and enters it. No auth server or port forwarding needed.
9
+ */
10
+ const https = require('https');
11
+ const querystring = require('querystring');
12
+ const config = require('../config');
13
+
14
+ /**
15
+ * POST helper for OAuth2 endpoints
16
+ * @param {string} url - Full URL to POST to
17
+ * @param {string} postData - URL-encoded form data
18
+ * @returns {Promise<{statusCode: number, body: object}>}
19
+ */
20
+ function postRequest(url, postData) {
21
+ return new Promise((resolve, reject) => {
22
+ const req = https.request(
23
+ url,
24
+ {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/x-www-form-urlencoded',
28
+ 'Content-Length': Buffer.byteLength(postData),
29
+ },
30
+ },
31
+ (res) => {
32
+ let data = '';
33
+ res.on('data', (chunk) => (data += chunk));
34
+ res.on('end', () => {
35
+ try {
36
+ resolve({ statusCode: res.statusCode, body: JSON.parse(data) });
37
+ } catch (_e) {
38
+ reject(new Error(`Failed to parse response: ${data}`));
39
+ }
40
+ });
41
+ }
42
+ );
43
+ req.on('error', reject);
44
+ req.write(postData);
45
+ req.end();
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Initiates the device code flow by requesting a device code from Azure.
51
+ * @param {string} clientId - Azure app client ID
52
+ * @param {string[]} scopes - OAuth2 scopes to request
53
+ * @returns {Promise<{userCode: string, verificationUri: string, deviceCode: string, expiresIn: number, interval: number, message: string}>}
54
+ */
55
+ async function initiateDeviceCodeFlow(clientId, scopes) {
56
+ const postData = querystring.stringify({
57
+ client_id: clientId,
58
+ scope: scopes.join(' '),
59
+ });
60
+
61
+ const endpoint = config.AUTH_CONFIG.deviceCodeEndpoint;
62
+ const { statusCode, body } = await postRequest(endpoint, postData);
63
+
64
+ if (statusCode < 200 || statusCode >= 300) {
65
+ throw new Error(
66
+ body.error_description ||
67
+ `Device code request failed with status ${statusCode}`
68
+ );
69
+ }
70
+
71
+ return {
72
+ userCode: body.user_code,
73
+ verificationUri: body.verification_uri,
74
+ deviceCode: body.device_code,
75
+ expiresIn: body.expires_in,
76
+ interval: body.interval || 5,
77
+ message: body.message,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Polls the token endpoint until the user completes authentication.
83
+ * @param {string} clientId - Azure app client ID
84
+ * @param {string} deviceCode - Device code from initiateDeviceCodeFlow
85
+ * @param {number} interval - Polling interval in seconds
86
+ * @param {number} expiresIn - Seconds until the device code expires
87
+ * @returns {Promise<{access_token: string, refresh_token: string, expires_in: number, scope: string, token_type: string}>}
88
+ */
89
+ async function pollForToken(clientId, deviceCode, interval, expiresIn) {
90
+ const endpoint = config.AUTH_CONFIG.tokenEndpoint;
91
+ const deadline = Date.now() + expiresIn * 1000;
92
+ let pollInterval = interval;
93
+
94
+ while (Date.now() < deadline) {
95
+ await new Promise((resolve) => setTimeout(resolve, pollInterval * 1000));
96
+
97
+ const postData = querystring.stringify({
98
+ client_id: clientId,
99
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
100
+ device_code: deviceCode,
101
+ });
102
+
103
+ const { statusCode, body } = await postRequest(endpoint, postData);
104
+
105
+ if (statusCode >= 200 && statusCode < 300) {
106
+ return body;
107
+ }
108
+
109
+ switch (body.error) {
110
+ case 'authorization_pending':
111
+ // User hasn't completed auth yet — keep polling
112
+ break;
113
+ case 'slow_down':
114
+ // Server asked us to slow down — increase interval by 5s
115
+ pollInterval += 5;
116
+ break;
117
+ case 'authorization_declined':
118
+ throw new Error('Authentication was declined by the user.');
119
+ case 'expired_token':
120
+ throw new Error(
121
+ 'Device code expired. Please restart the authentication process.'
122
+ );
123
+ default:
124
+ throw new Error(
125
+ body.error_description ||
126
+ `Token polling failed: ${body.error || `status ${statusCode}`}`
127
+ );
128
+ }
129
+ }
130
+
131
+ throw new Error(
132
+ 'Device code expired. Please restart the authentication process.'
133
+ );
134
+ }
135
+
136
+ module.exports = {
137
+ initiateDeviceCodeFlow,
138
+ pollForToken,
139
+ };
package/auth/index.js CHANGED
@@ -2,22 +2,32 @@
2
2
  * Authentication module for Outlook Assistant server
3
3
  */
4
4
  const tokenManager = require('./token-manager');
5
+ const TokenStorage = require('./token-storage');
6
+ const config = require('../config');
5
7
  const { authTools } = require('./tools');
6
8
 
9
+ // Singleton TokenStorage instance with auto-refresh support
10
+ const tokenStorage = new TokenStorage({
11
+ clientId: config.AUTH_CONFIG.clientId,
12
+ clientSecret: config.AUTH_CONFIG.clientSecret,
13
+ tokenStorePath: config.AUTH_CONFIG.tokenStorePath,
14
+ scopes: config.AUTH_CONFIG.scopes,
15
+ tokenEndpoint: config.AUTH_CONFIG.tokenEndpoint,
16
+ });
17
+
7
18
  /**
8
- * Ensures the user is authenticated and returns an access token
19
+ * Ensures the user is authenticated and returns an access token.
20
+ * Automatically refreshes expired tokens via tokenStorage.
9
21
  * @param {boolean} forceNew - Whether to force a new authentication
10
22
  * @returns {Promise<string>} - Access token
11
23
  * @throws {Error} - If authentication fails
12
24
  */
13
25
  async function ensureAuthenticated(forceNew = false) {
14
26
  if (forceNew) {
15
- // Force re-authentication
16
27
  throw new Error('Authentication required');
17
28
  }
18
29
 
19
- // Check for existing token
20
- const accessToken = tokenManager.getAccessToken();
30
+ const accessToken = await tokenStorage.getValidAccessToken();
21
31
  if (!accessToken) {
22
32
  throw new Error('Authentication required');
23
33
  }
@@ -26,7 +36,8 @@ async function ensureAuthenticated(forceNew = false) {
26
36
  }
27
37
 
28
38
  module.exports = {
29
- tokenManager,
39
+ tokenManager, // deprecated: use tokenStorage
40
+ tokenStorage,
30
41
  authTools,
31
42
  ensureAuthenticated,
32
43
  };
package/auth/tools.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  const config = require('../config');
5
5
  const tokenManager = require('./token-manager');
6
+ const { initiateDeviceCodeFlow, pollForToken } = require('./device-code');
6
7
 
7
8
  /**
8
9
  * About tool handler
@@ -46,18 +47,14 @@ async function handleAbout() {
46
47
  }
47
48
 
48
49
  /**
49
- * Authentication tool handler
50
+ * Authentication tool handler — supports browser redirect and device code flow.
50
51
  * @param {object} args - Tool arguments
51
52
  * @returns {object} - MCP response
52
53
  */
53
54
  async function handleAuthenticate(args) {
54
- const _force = args && args.force === true;
55
-
56
55
  // For test mode, create a test token
57
56
  if (config.USE_TEST_MODE) {
58
- // Create a test token with a 1-hour expiry
59
57
  tokenManager.createTestTokens();
60
-
61
58
  return {
62
59
  content: [
63
60
  {
@@ -68,43 +65,205 @@ async function handleAuthenticate(args) {
68
65
  };
69
66
  }
70
67
 
71
- // For real authentication, generate an auth URL and instruct the user to visit it
68
+ const method = args?.method || config.AUTH_CONFIG.defaultAuthMethod;
69
+
70
+ if (method === 'device-code') {
71
+ return handleDeviceCodeAuth();
72
+ }
73
+
74
+ // Browser redirect flow (existing behaviour)
72
75
  const authUrl = `${config.AUTH_CONFIG.authServerUrl}/auth?client_id=${config.AUTH_CONFIG.clientId}`;
76
+ return {
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: `Authentication required. Please visit the following URL to authenticate with Microsoft: ${authUrl}\n\nAfter authentication, you will be redirected back to this application.\n\nNote: The auth server must be running on port 3333. If working remotely, consider using method=device-code instead.`,
81
+ },
82
+ ],
83
+ };
84
+ }
85
+
86
+ // Module-level state for pending device code flow
87
+ let pendingDeviceCode = null;
88
+
89
+ /**
90
+ * Device code flow step 1 — request a code for the user to enter.
91
+ * Returns the code + URL immediately. Call device-code-complete to finish.
92
+ * @returns {object} - MCP response
93
+ */
94
+ async function handleDeviceCodeAuth() {
95
+ const clientId = config.AUTH_CONFIG.clientId;
96
+ if (!clientId) {
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: 'Error: OUTLOOK_CLIENT_ID is not configured.',
102
+ },
103
+ ],
104
+ };
105
+ }
106
+
107
+ console.error('[AUTH] Starting device code flow...');
108
+ const response = await initiateDeviceCodeFlow(
109
+ clientId,
110
+ config.AUTH_CONFIG.scopes
111
+ );
112
+
113
+ // Store for the completion step
114
+ pendingDeviceCode = {
115
+ deviceCode: response.deviceCode,
116
+ interval: response.interval,
117
+ expiresIn: response.expiresIn,
118
+ expiresAt: Date.now() + response.expiresIn * 1000,
119
+ };
120
+
121
+ console.error(
122
+ `[AUTH] Device code: ${response.userCode}, expires in ${response.expiresIn}s`
123
+ );
73
124
 
74
125
  return {
75
126
  content: [
76
127
  {
77
128
  type: 'text',
78
- text: `Authentication required. Please visit the following URL to authenticate with Microsoft: ${authUrl}\n\nAfter authentication, you will be redirected back to this application.`,
129
+ text: [
130
+ `## Device Code Authentication\n`,
131
+ `Visit: **${response.verificationUri}**`,
132
+ `Enter code: **${response.userCode}**\n`,
133
+ `The code expires in ${Math.floor(response.expiresIn / 60)} minutes.\n`,
134
+ `After entering the code and signing in, call this tool again with \`action=device-code-complete\` to finish authentication.`,
135
+ ].join('\n'),
79
136
  },
80
137
  ],
81
138
  };
82
139
  }
83
140
 
84
141
  /**
85
- * Check authentication status tool handler
142
+ * Device code flow step 2 — poll until the user completes authentication.
143
+ * @returns {object} - MCP response
144
+ */
145
+ async function handleDeviceCodeComplete() {
146
+ if (!pendingDeviceCode) {
147
+ return {
148
+ content: [
149
+ {
150
+ type: 'text',
151
+ text: 'No pending device code flow. Call authenticate with method=device-code first.',
152
+ },
153
+ ],
154
+ };
155
+ }
156
+
157
+ if (Date.now() > pendingDeviceCode.expiresAt) {
158
+ pendingDeviceCode = null;
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text',
163
+ text: 'Device code has expired. Please start a new authentication with action=authenticate.',
164
+ },
165
+ ],
166
+ };
167
+ }
168
+
169
+ const clientId = config.AUTH_CONFIG.clientId;
170
+
171
+ try {
172
+ console.error('[AUTH] Polling for device code completion...');
173
+ const tokenResponse = await pollForToken(
174
+ clientId,
175
+ pendingDeviceCode.deviceCode,
176
+ pendingDeviceCode.interval,
177
+ Math.ceil((pendingDeviceCode.expiresAt - Date.now()) / 1000)
178
+ );
179
+
180
+ pendingDeviceCode = null;
181
+
182
+ // Save tokens using TokenStorage
183
+ const TokenStorage = require('./token-storage');
184
+ const tokenStorage = new TokenStorage({
185
+ clientId: config.AUTH_CONFIG.clientId,
186
+ clientSecret: config.AUTH_CONFIG.clientSecret,
187
+ tokenStorePath: config.AUTH_CONFIG.tokenStorePath,
188
+ scopes: config.AUTH_CONFIG.scopes,
189
+ tokenEndpoint: config.AUTH_CONFIG.tokenEndpoint,
190
+ });
191
+
192
+ tokenStorage.tokens = {
193
+ access_token: tokenResponse.access_token,
194
+ refresh_token: tokenResponse.refresh_token,
195
+ expires_in: tokenResponse.expires_in,
196
+ expires_at: Date.now() + tokenResponse.expires_in * 1000,
197
+ scope: tokenResponse.scope,
198
+ token_type: tokenResponse.token_type,
199
+ };
200
+ await tokenStorage._saveTokensToFile();
201
+
202
+ console.error('[AUTH] Device code flow completed successfully.');
203
+
204
+ return {
205
+ content: [
206
+ {
207
+ type: 'text',
208
+ text: 'Authentication successful! Tokens saved. You can now use Outlook tools.',
209
+ },
210
+ ],
211
+ };
212
+ } catch (error) {
213
+ pendingDeviceCode = null;
214
+ return {
215
+ content: [
216
+ {
217
+ type: 'text',
218
+ text: `Authentication failed: ${error.message}`,
219
+ },
220
+ ],
221
+ };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Check authentication status — attempts token refresh if expired.
86
227
  * @returns {object} - MCP response
87
228
  */
88
229
  async function handleCheckAuthStatus() {
89
230
  console.error('[CHECK-AUTH-STATUS] Starting authentication status check');
90
231
 
91
- const tokens = tokenManager.loadTokenCache();
232
+ // Use TokenStorage for accurate status (includes refresh attempt)
233
+ const TokenStorage = require('./token-storage');
234
+ const tokenStorage = new TokenStorage({
235
+ clientId: config.AUTH_CONFIG.clientId,
236
+ clientSecret: config.AUTH_CONFIG.clientSecret,
237
+ tokenStorePath: config.AUTH_CONFIG.tokenStorePath,
238
+ scopes: config.AUTH_CONFIG.scopes,
239
+ tokenEndpoint: config.AUTH_CONFIG.tokenEndpoint,
240
+ });
92
241
 
93
- console.error(`[CHECK-AUTH-STATUS] Tokens loaded: ${tokens ? 'YES' : 'NO'}`);
242
+ const accessToken = await tokenStorage.getValidAccessToken();
94
243
 
95
- if (!tokens || !tokens.access_token) {
96
- console.error('[CHECK-AUTH-STATUS] No valid access token found');
244
+ if (!accessToken) {
245
+ console.error('[CHECK-AUTH-STATUS] No valid access token');
97
246
  return {
98
247
  content: [{ type: 'text', text: 'Not authenticated' }],
99
248
  };
100
249
  }
101
250
 
102
- console.error('[CHECK-AUTH-STATUS] Access token present');
103
- console.error(`[CHECK-AUTH-STATUS] Token expires at: ${tokens.expires_at}`);
104
- console.error(`[CHECK-AUTH-STATUS] Current time: ${Date.now()}`);
251
+ const expiresAt = tokenStorage.getExpiryTime();
252
+ const expiresIn = expiresAt
253
+ ? Math.round((expiresAt - Date.now()) / 60000)
254
+ : 'unknown';
255
+
256
+ console.error(
257
+ `[CHECK-AUTH-STATUS] Authenticated, token expires in ~${expiresIn} min`
258
+ );
105
259
 
106
260
  return {
107
- content: [{ type: 'text', text: 'Authenticated and ready' }],
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ text: `Authenticated and ready (token expires in ~${expiresIn} minutes)`,
265
+ },
266
+ ],
108
267
  };
109
268
  }
110
269
 
@@ -113,7 +272,7 @@ const authTools = [
113
272
  {
114
273
  name: 'auth',
115
274
  description:
116
- 'Manage authentication with Microsoft Graph API. action=status (default) checks auth state, action=authenticate starts OAuth flow, action=about shows server info.',
275
+ 'Manage authentication with Microsoft Graph API. action=status (default) checks auth state and refreshes tokens if needed, action=authenticate starts OAuth flow (device-code by default — no auth server needed), action=device-code-complete finishes device code auth after user enters code, action=about shows server info.',
117
276
  annotations: {
118
277
  title: 'Authentication',
119
278
  readOnlyHint: false,
@@ -125,9 +284,15 @@ const authTools = [
125
284
  properties: {
126
285
  action: {
127
286
  type: 'string',
128
- enum: ['status', 'authenticate', 'about'],
287
+ enum: ['status', 'authenticate', 'device-code-complete', 'about'],
129
288
  description: 'Action to perform (default: status)',
130
289
  },
290
+ method: {
291
+ type: 'string',
292
+ enum: ['device-code', 'browser'],
293
+ description:
294
+ 'Auth method for action=authenticate. device-code (default): no auth server needed, works remotely. browser: traditional OAuth redirect via port 3333.',
295
+ },
131
296
  force: {
132
297
  type: 'boolean',
133
298
  description:
@@ -141,6 +306,8 @@ const authTools = [
141
306
  switch (action) {
142
307
  case 'authenticate':
143
308
  return handleAuthenticate(args);
309
+ case 'device-code-complete':
310
+ return handleDeviceCodeComplete();
144
311
  case 'about':
145
312
  return handleAbout();
146
313
  case 'status':
@@ -155,5 +322,7 @@ module.exports = {
155
322
  authTools,
156
323
  handleAbout,
157
324
  handleAuthenticate,
325
+ handleDeviceCodeAuth,
326
+ handleDeviceCodeComplete,
158
327
  handleCheckAuthStatus,
159
328
  };