@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 +8 -0
- package/README.md +42 -23
- package/auth/device-code.js +139 -0
- package/auth/index.js +16 -5
- package/auth/tools.js +187 -18
- package/config.js +7 -0
- package/email/export.js +2 -4
- package/email/index.js +43 -0
- package/email/mail-tips.js +211 -0
- package/email/send.js +44 -0
- package/llms-install.md +90 -0
- package/llms.txt +8 -7
- package/package.json +4 -3
- package/utils/graph-api.js +128 -5
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.
|
|
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** |
|
|
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
|
-
**
|
|
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.
|
|
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
|
+
[](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
|
-
###
|
|
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
|
-
|
|
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
|
|
356
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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) |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
const accessToken = await tokenStorage.getValidAccessToken();
|
|
94
243
|
|
|
95
|
-
if (!
|
|
96
|
-
console.error('[CHECK-AUTH-STATUS] No valid access token
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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: [
|
|
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
|
};
|