ms365-mcp-server 1.0.0 → 1.0.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/README.md +115 -130
- package/dist/index.js +64 -209
- package/dist/utils/ms365-auth-enhanced.js +202 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ✉️ Microsoft 365 MCP Server
|
|
2
2
|
|
|
3
|
-
A powerful **Model Context Protocol (MCP) server** that enables seamless Microsoft 365 email integration through natural language interactions. Built for **Claude Desktop**, **LLMs**, and any MCP-compatible application
|
|
3
|
+
A powerful **Model Context Protocol (MCP) server** that enables seamless Microsoft 365 email integration through natural language interactions. Built for **Claude Desktop**, **LLMs**, and any MCP-compatible application with **enhanced security** and **simplified authentication**.
|
|
4
4
|
|
|
5
5
|
This project provides an **MCP (Model Context Protocol) server** that enables **Claude Desktop** to access your **Microsoft 365 email** seamlessly. It acts as a bridge between AI models and email services by exposing mailbox capabilities through a **secure, standardized interface**.
|
|
6
6
|
|
|
@@ -11,9 +11,9 @@ This project provides an **MCP (Model Context Protocol) server** that enables **
|
|
|
11
11
|
- **Smart email search** with advanced filtering and criteria support
|
|
12
12
|
- **Email organization** with read/unread marking and folder management
|
|
13
13
|
- **File attachment support** - send and receive attachments seamlessly
|
|
14
|
-
- **
|
|
14
|
+
- **Enhanced security** with simplified single-user authentication model
|
|
15
15
|
- **Contact management** - retrieve and search your contact directory
|
|
16
|
-
- **Secure authentication** with
|
|
16
|
+
- **Secure authentication** with OS keychain storage and automatic refresh
|
|
17
17
|
- **Cross-platform compatibility** (macOS, Linux, Windows)
|
|
18
18
|
|
|
19
19
|
## 🛠️ Tools & Capabilities
|
|
@@ -31,19 +31,16 @@ This project provides an **MCP (Model Context Protocol) server** that enables **
|
|
|
31
31
|
- **`get_contacts`** - Retrieve contact list from Microsoft 365
|
|
32
32
|
- **`search_contacts`** - Search contacts by name or email address
|
|
33
33
|
|
|
34
|
-
### 🔐 Authentication Tools (
|
|
35
|
-
- **`
|
|
36
|
-
- **`
|
|
37
|
-
- **`
|
|
34
|
+
### 🔐 Secure Authentication Tools (7 tools)
|
|
35
|
+
- **`authenticate_with_device_code`** - Complete authentication in one step (RECOMMENDED)
|
|
36
|
+
- **`device_code_login`** - Device code authentication flow (manual code entry)
|
|
37
|
+
- **`check_pending_auth`** - Check for pending device code authentication
|
|
38
38
|
- **`get_device_code`** - Get device code info for manual authentication
|
|
39
|
-
- **`
|
|
40
|
-
- **`
|
|
41
|
-
- **`
|
|
42
|
-
- **`logout`** - Clear authentication tokens for current user
|
|
43
|
-
- **`authenticate_user`** - Start authentication flow for new users (multi-user mode)
|
|
44
|
-
- **`get_my_session_info`** - View your own session information (multi-user mode)
|
|
39
|
+
- **`get_auth_link`** - Get authentication link without auto browser launch
|
|
40
|
+
- **`verify_authentication`** - Check current authentication status (secure)
|
|
41
|
+
- **`logout`** - Clear authentication tokens and reset session
|
|
45
42
|
|
|
46
|
-
**Total:
|
|
43
|
+
**Total: 18 tools** (11 email operations + 7 secure authentication tools)
|
|
47
44
|
|
|
48
45
|
## 🌟 Key Feature Highlights
|
|
49
46
|
|
|
@@ -76,6 +73,23 @@ This project provides an **MCP (Model Context Protocol) server** that enables **
|
|
|
76
73
|
* Perform smart contact searches
|
|
77
74
|
* Integration with Microsoft 365 address book
|
|
78
75
|
|
|
76
|
+
### 🛡️ Enhanced Security Features
|
|
77
|
+
|
|
78
|
+
* **Simplified Authentication Model**
|
|
79
|
+
* Single-user authentication for better security
|
|
80
|
+
* No account enumeration or information disclosure
|
|
81
|
+
* Secure credential storage with OS keychain integration
|
|
82
|
+
|
|
83
|
+
* **Privacy Protection**
|
|
84
|
+
* No sensitive system information exposed
|
|
85
|
+
* User can only see their own authentication status
|
|
86
|
+
* Secure token management with automatic refresh
|
|
87
|
+
|
|
88
|
+
* **Device Code Authentication**
|
|
89
|
+
* No browser auto-open for better security control
|
|
90
|
+
* Manual code entry prevents unauthorized access
|
|
91
|
+
* Built-in Microsoft app requires no Azure registration
|
|
92
|
+
|
|
79
93
|
### 🌟 Advanced Features
|
|
80
94
|
|
|
81
95
|
* **Advanced Search Filters**
|
|
@@ -130,69 +144,69 @@ Add to your `claude_desktop_config.json`:
|
|
|
130
144
|
```
|
|
131
145
|
|
|
132
146
|
### First Time Authentication
|
|
133
|
-
**
|
|
147
|
+
**Device Code Flow (Recommended & Secure)**
|
|
134
148
|
```bash
|
|
135
149
|
ms365-mcp-server --login
|
|
136
150
|
# Follow the on-screen instructions - no Azure app registration needed!
|
|
137
151
|
```
|
|
138
152
|
|
|
139
|
-
**
|
|
153
|
+
**Interactive Setup (Advanced)**
|
|
140
154
|
```bash
|
|
141
155
|
ms365-mcp-server --setup-auth
|
|
142
156
|
# Choose device code flow (recommended) or custom Azure app
|
|
143
157
|
```
|
|
144
158
|
|
|
145
|
-
## 🔐 **Enhanced Authentication
|
|
159
|
+
## 🔐 **Enhanced Security & Authentication**
|
|
146
160
|
|
|
147
|
-
Our MS365 MCP Server features a **
|
|
161
|
+
Our MS365 MCP Server features a **security-first authentication system** with simplified single-user model:
|
|
148
162
|
|
|
149
|
-
###
|
|
163
|
+
### **🔒 Security Improvements**
|
|
164
|
+
|
|
165
|
+
- **🚫 No Account Enumeration**: Removed tools that could expose user lists
|
|
166
|
+
- **🚫 No System Information Disclosure**: Hidden sensitive storage details
|
|
167
|
+
- **🚫 No Multi-User Complexity**: Simplified to single-user for better security
|
|
168
|
+
- **✅ Secure Credential Storage**: OS Keychain with encrypted file fallback
|
|
169
|
+
- **✅ Privacy Protection**: Users can only see their own information
|
|
170
|
+
- **✅ Token Security**: Automatic refresh with secure MSAL library
|
|
171
|
+
|
|
172
|
+
### **🎯 Secure Authentication Flow**
|
|
150
173
|
|
|
151
174
|
1. **🔥 Device Code Flow (Recommended)**
|
|
152
175
|
- **No Azure app registration required**
|
|
153
176
|
- **Built-in Microsoft app** for instant setup
|
|
154
|
-
- **
|
|
155
|
-
-
|
|
177
|
+
- **Manual code entry** prevents unauthorized access
|
|
178
|
+
- **No local server** required (more secure)
|
|
156
179
|
|
|
157
180
|
2. **⚙️ Custom Azure App (Advanced)**
|
|
158
181
|
- **Full control** over permissions and settings
|
|
159
182
|
- **Redirect-based OAuth2** authentication
|
|
160
183
|
- **Custom tenant configuration**
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
### **🔒 Secure Credential Storage**
|
|
184
|
+
- Requires Azure app registration
|
|
164
185
|
|
|
165
|
-
|
|
166
|
-
- **Encrypted File Fallback** for cross-platform compatibility
|
|
167
|
-
- **Automatic token refresh** with MSAL library
|
|
168
|
-
- **Multi-account support** with isolated storage
|
|
169
|
-
|
|
170
|
-
### **⚡ New Authentication Commands**
|
|
186
|
+
### **⚡ Secure Authentication Commands**
|
|
171
187
|
|
|
172
188
|
```bash
|
|
173
|
-
# Device code authentication (
|
|
189
|
+
# Device code authentication (most secure)
|
|
174
190
|
ms365-mcp-server --login
|
|
175
191
|
|
|
176
|
-
# Verify authentication status
|
|
192
|
+
# Verify your authentication status
|
|
177
193
|
ms365-mcp-server --verify-login
|
|
178
194
|
|
|
179
|
-
#
|
|
195
|
+
# Secure logout and clear tokens
|
|
180
196
|
ms365-mcp-server --logout
|
|
181
197
|
|
|
182
198
|
# Interactive credential setup
|
|
183
199
|
ms365-mcp-server --setup-auth
|
|
184
200
|
```
|
|
185
201
|
|
|
186
|
-
### **🔄 Authentication Flow
|
|
202
|
+
### **🔄 Authentication Flow**
|
|
187
203
|
|
|
188
204
|
| Feature | Device Code Flow *(Recommended)* | Redirect Flow *(Advanced)* |
|
|
189
205
|
|---------|----------------------------------|----------------------------|
|
|
190
206
|
| **Setup Required** | ❌ None (uses built-in app) | ✅ Azure app registration |
|
|
191
|
-
| **Callback Endpoint** | ❌ Not needed | ✅ `http://localhost:44001/oauth2callback` |
|
|
192
|
-
| **Port Requirements** | ❌ No ports needed | ✅ Port 44001 must be free |
|
|
193
|
-
| **Browser Auto-open** | ❌ Manual code entry | ✅ Automatic browser redirect |
|
|
194
207
|
| **Security** | 🔒 High (no local server) | 🔒 High (local callback) |
|
|
195
|
-
| **
|
|
208
|
+
| **Privacy** | 🔒 Manual code entry | 🔒 Browser auto-open |
|
|
209
|
+
| **Ease of Use** | ✅ Simple setup | ⚙️ Advanced configuration |
|
|
196
210
|
|
|
197
211
|
**Most users should use Device Code Flow** - it's simpler, more secure, and requires no setup!
|
|
198
212
|
|
|
@@ -222,11 +236,20 @@ node dist/index.js
|
|
|
222
236
|
|
|
223
237
|
## 🔑 Authentication Setup
|
|
224
238
|
|
|
225
|
-
### Step 1:
|
|
239
|
+
### Step 1: Recommended Setup (No Azure Registration)
|
|
240
|
+
|
|
241
|
+
**Most users should use this method:**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# One-command authentication
|
|
245
|
+
ms365-mcp-server --login
|
|
246
|
+
```
|
|
226
247
|
|
|
227
|
-
|
|
248
|
+
This uses a built-in Microsoft app with device code flow - **no Azure registration needed!**
|
|
228
249
|
|
|
229
|
-
|
|
250
|
+
### Step 2: Advanced Setup (Custom Azure App)
|
|
251
|
+
|
|
252
|
+
> **💡 Note**: Only needed for advanced users who want custom Azure app configuration.
|
|
230
253
|
|
|
231
254
|
1. Go to [Azure Portal](https://portal.azure.com)
|
|
232
255
|
2. Navigate to **Azure Active Directory** > **App registrations**
|
|
@@ -234,7 +257,7 @@ node dist/index.js
|
|
|
234
257
|
4. Configure your application:
|
|
235
258
|
- **Name**: MS365 MCP Server (or your preferred name)
|
|
236
259
|
- **Supported account types**: Choose appropriate option
|
|
237
|
-
- **Redirect URI**: `http://localhost:44001/oauth2callback` *(only
|
|
260
|
+
- **Redirect URI**: `http://localhost:44001/oauth2callback` *(only for redirect flow)*
|
|
238
261
|
5. **Grant API permissions** for Microsoft Graph:
|
|
239
262
|
- `Mail.ReadWrite` - Read and write access to user mail
|
|
240
263
|
- `Mail.Send` - Send mail as user
|
|
@@ -247,9 +270,9 @@ node dist/index.js
|
|
|
247
270
|
- **Directory (tenant) ID**
|
|
248
271
|
- **Client secret value**
|
|
249
272
|
|
|
250
|
-
### Step
|
|
273
|
+
### Step 3: Configure Credentials (Advanced Only)
|
|
251
274
|
|
|
252
|
-
**Method 1: Environment Variables
|
|
275
|
+
**Method 1: Environment Variables**
|
|
253
276
|
```bash
|
|
254
277
|
export MS365_CLIENT_ID="your_client_id_here"
|
|
255
278
|
export MS365_CLIENT_SECRET="your_client_secret_here"
|
|
@@ -262,31 +285,6 @@ ms365-mcp-server --setup-auth
|
|
|
262
285
|
# Follow the prompts to enter your credentials
|
|
263
286
|
```
|
|
264
287
|
|
|
265
|
-
**Method 3: Credentials File**
|
|
266
|
-
```bash
|
|
267
|
-
# Save credentials to ~/.ms365-mcp/credentials.json
|
|
268
|
-
{
|
|
269
|
-
"clientId": "your_client_id",
|
|
270
|
-
"clientSecret": "your_client_secret",
|
|
271
|
-
"tenantId": "your_tenant_id",
|
|
272
|
-
"redirectUri": "http://localhost:44001/oauth2callback"
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Step 3: Authentication Modes
|
|
277
|
-
|
|
278
|
-
**Single-User Mode (Personal Use)**
|
|
279
|
-
```bash
|
|
280
|
-
ms365-mcp-server
|
|
281
|
-
# Use quick_authenticate or get_auth_link tools
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
**Multi-User Mode (Teams/Shared)**
|
|
285
|
-
```bash
|
|
286
|
-
ms365-mcp-server --multi-user
|
|
287
|
-
# Use authenticate_user tool for each user
|
|
288
|
-
```
|
|
289
|
-
|
|
290
288
|
## 📖 Usage Examples
|
|
291
289
|
|
|
292
290
|
### Sending Emails
|
|
@@ -322,6 +320,14 @@ ms365-mcp-server --multi-user
|
|
|
322
320
|
}
|
|
323
321
|
```
|
|
324
322
|
|
|
323
|
+
### Secure Authentication Check
|
|
324
|
+
```javascript
|
|
325
|
+
{
|
|
326
|
+
"tool": "verify_authentication",
|
|
327
|
+
"arguments": {}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
325
331
|
### Contact Search
|
|
326
332
|
```javascript
|
|
327
333
|
{
|
|
@@ -340,17 +346,20 @@ ms365-mcp-server --multi-user
|
|
|
340
346
|
- `--logout` - Logout and clear stored authentication tokens
|
|
341
347
|
- `--verify-login` - Verify current authentication status
|
|
342
348
|
- `--reset-auth` - Clear stored authentication tokens
|
|
343
|
-
- `--multi-user` - Enable multi-user authentication mode
|
|
344
349
|
- `--debug` - Enable detailed debug logging
|
|
345
350
|
- `--help` - Show help information
|
|
346
351
|
|
|
352
|
+
🔒 **Security Note**: Removed `--multi-user` mode for enhanced security and simplicity.
|
|
353
|
+
|
|
347
354
|
## 🔒 Security & Privacy
|
|
348
355
|
|
|
349
|
-
- **
|
|
350
|
-
- **
|
|
356
|
+
- **Secure token storage** with OS Keychain integration
|
|
357
|
+
- **Encrypted file fallback** for cross-platform compatibility
|
|
351
358
|
- **OAuth2 compliance** with Microsoft MSAL library
|
|
352
359
|
- **No external data transmission** - all data stays local
|
|
353
360
|
- **Automatic token refresh** with secure credential management
|
|
361
|
+
- **Privacy protection** - users can only access their own data
|
|
362
|
+
- **No information disclosure** - system details are protected
|
|
354
363
|
|
|
355
364
|
## 📊 Logging & Debugging
|
|
356
365
|
|
|
@@ -366,20 +375,20 @@ ms365-mcp-server --debug
|
|
|
366
375
|
### Common Issues
|
|
367
376
|
|
|
368
377
|
**"MS365 credentials not configured"**
|
|
369
|
-
-
|
|
370
|
-
-
|
|
378
|
+
- Run `ms365-mcp-server --login` for quick setup
|
|
379
|
+
- Use `--setup-auth` for advanced configuration
|
|
371
380
|
|
|
372
|
-
**
|
|
373
|
-
-
|
|
374
|
-
-
|
|
381
|
+
**Device code authentication issues**
|
|
382
|
+
- Use `ms365-mcp-server --verify-login` to check status
|
|
383
|
+
- Try `ms365-mcp-server --logout` then `--login` to reset
|
|
375
384
|
|
|
376
385
|
**Authentication timeout**
|
|
377
|
-
-
|
|
378
|
-
-
|
|
386
|
+
- Device codes expire in 15 minutes
|
|
387
|
+
- Use `--reset-auth` to clear old tokens and start fresh
|
|
379
388
|
|
|
380
389
|
**Permission errors**
|
|
381
|
-
- Verify Microsoft Graph API permissions
|
|
382
|
-
-
|
|
390
|
+
- Verify Microsoft Graph API permissions if using custom Azure app
|
|
391
|
+
- Device code flow handles permissions automatically
|
|
383
392
|
|
|
384
393
|
### Reset Authentication
|
|
385
394
|
```bash
|
|
@@ -420,62 +429,38 @@ npm run version-bump major
|
|
|
420
429
|
- **Research assistance** - Query and analyze email communications
|
|
421
430
|
- **Email productivity** - Automate routine email tasks and responses
|
|
422
431
|
|
|
423
|
-
## 🆚 **Enhanced
|
|
424
|
-
|
|
425
|
-
While inspired by [Softeria's excellent MS365 MCP server](https://github.com/Softeria/ms-365-mcp-server), our implementation focuses on **email specialization** with significant enhancements:
|
|
426
|
-
|
|
427
|
-
### **🎯 Our Email-First Advantages**
|
|
428
|
-
|
|
429
|
-
| Feature | Our Implementation | Softeria's |
|
|
430
|
-
|---------|-------------------|------------|
|
|
431
|
-
| **Email Tools** | 11 comprehensive email tools | Basic mail operations |
|
|
432
|
-
| **Authentication** | Device code + Redirect flows | Device code only |
|
|
433
|
-
| **Storage Security** | OS Keychain + encrypted file | OS keychain fallback |
|
|
434
|
-
| **Multi-user Support** | ✅ Full user isolation | ❌ Single user only |
|
|
435
|
-
| **Email Search** | Advanced filtering & criteria | Basic search |
|
|
436
|
-
| **Attachment Handling** | Full send/receive support | Basic attachment support |
|
|
437
|
-
| **Documentation** | Comprehensive setup guides | Basic documentation |
|
|
438
|
-
| **CLI Features** | Rich authentication commands | Basic login/logout |
|
|
439
|
-
|
|
440
|
-
### **🚀 Unique Email Features**
|
|
432
|
+
## 🆚 **Enhanced Security vs Other Implementations**
|
|
441
433
|
|
|
442
|
-
|
|
443
|
-
- **Multi-folder Search**: Search across all mailbox folders simultaneously
|
|
444
|
-
- **Email Organization**: Move emails between folders, mark read/unread status
|
|
445
|
-
- **Contact Integration**: Full contact search and management
|
|
446
|
-
- **Professional Email Composition**: HTML/text emails with attachments and CC/BCC
|
|
447
|
-
- **Seamless Claude Integration**: Purpose-built for AI email assistance
|
|
434
|
+
Our implementation prioritizes **security and simplicity** over feature complexity:
|
|
448
435
|
|
|
449
|
-
###
|
|
436
|
+
### **🛡️ Our Security-First Advantages**
|
|
450
437
|
|
|
451
|
-
|
|
438
|
+
| Feature | Our Implementation | Other Implementations |
|
|
439
|
+
|---------|-------------------|----------------------|
|
|
440
|
+
| **Account Enumeration** | 🚫 Blocked (security risk) | ✅ Often exposed |
|
|
441
|
+
| **System Information** | 🚫 Hidden (privacy protection) | ✅ Often exposed |
|
|
442
|
+
| **Multi-User Complexity** | 🚫 Simplified to single-user | ✅ Complex multi-user |
|
|
443
|
+
| **Authentication Model** | ✅ Secure device code focus | ⚠️ Various approaches |
|
|
444
|
+
| **Information Disclosure** | 🚫 Minimized | ⚠️ Often excessive |
|
|
445
|
+
| **Privacy Protection** | ✅ User sees only own data | ⚠️ Variable |
|
|
452
446
|
|
|
453
|
-
|
|
454
|
-
# Install our server
|
|
455
|
-
npm install -g ms365-mcp-server
|
|
447
|
+
### **🚀 Security Features**
|
|
456
448
|
|
|
457
|
-
|
|
458
|
-
|
|
449
|
+
- **Privacy by Design**: Users can only access their own information
|
|
450
|
+
- **Minimal Information Disclosure**: No sensitive system details exposed
|
|
451
|
+
- **Simplified Authentication**: Single-user model reduces attack surface
|
|
452
|
+
- **Secure Token Management**: OS keychain with automatic refresh
|
|
453
|
+
- **Device Code Focus**: More secure than redirect-based flows
|
|
459
454
|
|
|
460
|
-
|
|
461
|
-
{
|
|
462
|
-
"mcpServers": {
|
|
463
|
-
"ms365": {
|
|
464
|
-
"command": "ms365-mcp-server",
|
|
465
|
-
"args": []
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
```
|
|
455
|
+
### **🔄 Migration Considerations**
|
|
470
456
|
|
|
471
|
-
**
|
|
472
|
-
- **
|
|
473
|
-
- **
|
|
474
|
-
- **
|
|
475
|
-
- **
|
|
476
|
-
- **Active development** focused on email productivity
|
|
457
|
+
**Enhanced Security Benefits:**
|
|
458
|
+
- **Reduced Attack Surface**: Fewer tools mean less potential vulnerabilities
|
|
459
|
+
- **Privacy Protection**: No account enumeration or system information disclosure
|
|
460
|
+
- **Simplified Model**: Single-user approach is more secure and easier to audit
|
|
461
|
+
- **Security Transparency**: Clear notices about what information is protected
|
|
477
462
|
|
|
478
|
-
|
|
463
|
+
Choose our implementation for **maximum security and privacy protection** in your Microsoft 365 integrations.
|
|
479
464
|
|
|
480
465
|
## 📝 License
|
|
481
466
|
|
|
@@ -494,4 +479,4 @@ For issues and questions:
|
|
|
494
479
|
|
|
495
480
|
---
|
|
496
481
|
|
|
497
|
-
**Built with ❤️ for the AI and productivity community**
|
|
482
|
+
**Built with 🔒 Security and ❤️ for the AI and productivity community**
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ function parseArgs() {
|
|
|
55
55
|
}
|
|
56
56
|
const server = new Server({
|
|
57
57
|
name: "ms365-mcp-server",
|
|
58
|
-
version: "1.0.
|
|
58
|
+
version: "1.0.1"
|
|
59
59
|
}, {
|
|
60
60
|
capabilities: {
|
|
61
61
|
resources: {
|
|
@@ -453,21 +453,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
453
453
|
additionalProperties: false
|
|
454
454
|
}
|
|
455
455
|
},
|
|
456
|
-
{
|
|
457
|
-
name: "get_my_session_info",
|
|
458
|
-
description: "Get information about your own authentication session (user-specific, secure).",
|
|
459
|
-
inputSchema: {
|
|
460
|
-
type: "object",
|
|
461
|
-
properties: {
|
|
462
|
-
userId: {
|
|
463
|
-
type: "string",
|
|
464
|
-
description: "Your User ID to get information for"
|
|
465
|
-
}
|
|
466
|
-
},
|
|
467
|
-
required: ["userId"],
|
|
468
|
-
additionalProperties: false
|
|
469
|
-
}
|
|
470
|
-
},
|
|
471
456
|
{
|
|
472
457
|
name: "remove_my_session",
|
|
473
458
|
description: "Remove your own authentication session (user-specific, secure).",
|
|
@@ -487,47 +472,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
487
472
|
// Add one-time authentication tool for single-user mode
|
|
488
473
|
const oneTimeAuthTools = [
|
|
489
474
|
{
|
|
490
|
-
name: "
|
|
491
|
-
description: "
|
|
475
|
+
name: "get_auth_link",
|
|
476
|
+
description: "Get a clickable Microsoft 365 authentication link without auto-opening browser. Perfect for manual authentication control.",
|
|
492
477
|
inputSchema: {
|
|
493
478
|
type: "object",
|
|
494
479
|
properties: {
|
|
495
480
|
force: {
|
|
496
481
|
type: "boolean",
|
|
497
|
-
description: "Force
|
|
482
|
+
description: "Force new authentication link even if already authenticated (default: false)",
|
|
498
483
|
default: false
|
|
499
484
|
}
|
|
500
485
|
},
|
|
501
486
|
additionalProperties: false
|
|
502
487
|
}
|
|
503
|
-
}
|
|
488
|
+
}
|
|
489
|
+
];
|
|
490
|
+
// Enhanced authentication tools with device code flow
|
|
491
|
+
const enhancedAuthTools = [
|
|
504
492
|
{
|
|
505
|
-
name: "
|
|
506
|
-
description: "
|
|
493
|
+
name: "authenticate_with_device_code",
|
|
494
|
+
description: "Complete device code authentication in MCP. First call shows device code, second call (after browser auth) completes the process. RECOMMENDED method.",
|
|
507
495
|
inputSchema: {
|
|
508
496
|
type: "object",
|
|
509
497
|
properties: {
|
|
510
498
|
force: {
|
|
511
499
|
type: "boolean",
|
|
512
|
-
description: "Force new authentication
|
|
500
|
+
description: "Force new authentication even if already authenticated (default: false)",
|
|
513
501
|
default: false
|
|
514
502
|
}
|
|
515
503
|
},
|
|
516
504
|
additionalProperties: false
|
|
517
505
|
}
|
|
518
506
|
},
|
|
519
|
-
{
|
|
520
|
-
name: "check_quick_auth_status",
|
|
521
|
-
description: "Check if quick authentication has completed successfully. Use this after using quick_authenticate to see if you're now authenticated.",
|
|
522
|
-
inputSchema: {
|
|
523
|
-
type: "object",
|
|
524
|
-
properties: {},
|
|
525
|
-
additionalProperties: false
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
];
|
|
529
|
-
// Enhanced authentication tools with device code flow
|
|
530
|
-
const enhancedAuthTools = [
|
|
531
507
|
{
|
|
532
508
|
name: "device_code_login",
|
|
533
509
|
description: "Start device code authentication flow and get the URL/code to enter. Shows authentication details immediately in the UI.",
|
|
@@ -543,15 +519,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
543
519
|
additionalProperties: false
|
|
544
520
|
}
|
|
545
521
|
},
|
|
546
|
-
{
|
|
547
|
-
name: "complete_device_code_auth",
|
|
548
|
-
description: "Check if device code authentication has completed after visiting the URL and entering the code. This is a quick status check - authentication happens automatically in the background.",
|
|
549
|
-
inputSchema: {
|
|
550
|
-
type: "object",
|
|
551
|
-
properties: {},
|
|
552
|
-
additionalProperties: false
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
522
|
{
|
|
556
523
|
name: "check_pending_auth",
|
|
557
524
|
description: "Check if there's a pending device code authentication and get the URL/code again if needed.",
|
|
@@ -579,24 +546,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
579
546
|
additionalProperties: false
|
|
580
547
|
}
|
|
581
548
|
},
|
|
582
|
-
{
|
|
583
|
-
name: "list_authenticated_accounts",
|
|
584
|
-
description: "List all authenticated Microsoft 365 accounts stored securely. Shows which accounts are available for use.",
|
|
585
|
-
inputSchema: {
|
|
586
|
-
type: "object",
|
|
587
|
-
properties: {},
|
|
588
|
-
additionalProperties: false
|
|
589
|
-
}
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
name: "get_storage_info",
|
|
593
|
-
description: "Get information about how credentials are stored (OS Keychain vs encrypted file). Useful for security auditing.",
|
|
594
|
-
inputSchema: {
|
|
595
|
-
type: "object",
|
|
596
|
-
properties: {},
|
|
597
|
-
additionalProperties: false
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
549
|
{
|
|
601
550
|
name: "logout",
|
|
602
551
|
description: "Log out and clear stored authentication tokens for the current user. This will require re-authentication.",
|
|
@@ -633,26 +582,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
633
582
|
try {
|
|
634
583
|
switch (name) {
|
|
635
584
|
// ============ ENHANCED AUTHENTICATION TOOLS ============
|
|
636
|
-
case "
|
|
637
|
-
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
585
|
+
case "authenticate_with_device_code":
|
|
586
|
+
try {
|
|
587
|
+
// Check if already authenticated
|
|
588
|
+
if (await enhancedMS365Auth.isAuthenticated() && !args?.force) {
|
|
589
|
+
return {
|
|
590
|
+
content: [
|
|
591
|
+
{
|
|
592
|
+
type: "text",
|
|
593
|
+
text: `✅ Already authenticated with Microsoft 365! Use force: true to re-authenticate.`
|
|
594
|
+
}
|
|
595
|
+
]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
// First, try to complete any existing pending authentication
|
|
599
|
+
const existingResult = await enhancedMS365Auth.completeDeviceCodeAuth();
|
|
600
|
+
if (existingResult) {
|
|
601
|
+
const currentUser = await enhancedMS365Auth.getCurrentUser();
|
|
602
|
+
return {
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: `✅ Device code authentication completed successfully!\n\n👤 User: ${currentUser || 'authenticated-user'}\n🔐 Status: Valid\n\n🚀 You can now use all Microsoft 365 email features!`
|
|
607
|
+
}
|
|
608
|
+
]
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
// Check if there's already a pending device code
|
|
612
|
+
let deviceCodeInfo = await enhancedMS365Auth.getPendingDeviceCodeInfo();
|
|
613
|
+
if (!deviceCodeInfo || args?.force) {
|
|
614
|
+
// No pending code or force new one, start fresh
|
|
615
|
+
deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth();
|
|
616
|
+
}
|
|
617
|
+
// Return device code information immediately (MCP pattern)
|
|
641
618
|
return {
|
|
642
619
|
content: [
|
|
643
620
|
{
|
|
644
621
|
type: "text",
|
|
645
|
-
text: `🔐 Microsoft 365 Authentication
|
|
622
|
+
text: `🔐 Microsoft 365 Device Code Authentication\n\n📱 Visit: ${deviceCodeInfo.verificationUri}\n🔑 Enter this code: ${deviceCodeInfo.userCode}\n\n⏳ After completing authentication in your browser, call this tool again to finish the process.\n\n💡 This code will expire in 15 minutes.`
|
|
646
623
|
}
|
|
647
624
|
]
|
|
648
625
|
};
|
|
649
626
|
}
|
|
650
|
-
|
|
627
|
+
catch (error) {
|
|
651
628
|
return {
|
|
652
629
|
content: [
|
|
653
630
|
{
|
|
654
631
|
type: "text",
|
|
655
|
-
text:
|
|
632
|
+
text: `❌ Authentication failed: ${error.message}\n\n💡 Try again or use CLI: node dist/index.js --login`
|
|
656
633
|
}
|
|
657
634
|
]
|
|
658
635
|
};
|
|
@@ -679,40 +656,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
679
656
|
]
|
|
680
657
|
};
|
|
681
658
|
}
|
|
682
|
-
case "check_quick_auth_status":
|
|
683
|
-
const isCurrentlyAuthenticated = await enhancedMS365Auth.isAuthenticated();
|
|
684
|
-
const hasPending = enhancedMS365Auth.hasPendingAuth();
|
|
685
|
-
if (isCurrentlyAuthenticated) {
|
|
686
|
-
return {
|
|
687
|
-
content: [
|
|
688
|
-
{
|
|
689
|
-
type: "text",
|
|
690
|
-
text: "✅ Quick authentication completed successfully! You can now use all Microsoft 365 email features."
|
|
691
|
-
}
|
|
692
|
-
]
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
else if (hasPending) {
|
|
696
|
-
const pendingInfo = enhancedMS365Auth.getPendingDeviceCodeInfo();
|
|
697
|
-
return {
|
|
698
|
-
content: [
|
|
699
|
-
{
|
|
700
|
-
type: "text",
|
|
701
|
-
text: `⏳ Authentication still pending.\n\n📱 Please visit: ${pendingInfo?.verificationUri}\n🔑 Enter code: ${pendingInfo?.userCode}\n\n💡 Check again after entering the code in your browser. Authentication completes automatically - no need to wait or use additional tools.`
|
|
702
|
-
}
|
|
703
|
-
]
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
return {
|
|
708
|
-
content: [
|
|
709
|
-
{
|
|
710
|
-
type: "text",
|
|
711
|
-
text: "❌ No authentication found. Please use quick_authenticate to start the authentication process."
|
|
712
|
-
}
|
|
713
|
-
]
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
659
|
case "device_code_login":
|
|
717
660
|
if (!await enhancedMS365Auth.isAuthenticated() || args?.force) {
|
|
718
661
|
const deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth();
|
|
@@ -720,7 +663,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
720
663
|
content: [
|
|
721
664
|
{
|
|
722
665
|
type: "text",
|
|
723
|
-
text: `🔐 Microsoft 365 Device Code Authentication Started!\n\n📱 Visit: ${deviceCodeInfo.verificationUri}\n🔑 Enter this code: ${deviceCodeInfo.userCode}\n\n⏳ After entering the code, use the "
|
|
666
|
+
text: `🔐 Microsoft 365 Device Code Authentication Started!\n\n📱 Visit: ${deviceCodeInfo.verificationUri}\n🔑 Enter this code: ${deviceCodeInfo.userCode}\n\n⏳ After entering the code, use the "authenticate_with_device_code" tool to complete authentication.\n\n💡 This code will expire in 15 minutes.`
|
|
724
667
|
}
|
|
725
668
|
]
|
|
726
669
|
};
|
|
@@ -735,53 +678,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
735
678
|
]
|
|
736
679
|
};
|
|
737
680
|
}
|
|
738
|
-
case "
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
content: [
|
|
742
|
-
{
|
|
743
|
-
type: "text",
|
|
744
|
-
text: "❌ No pending device code authentication found. Use 'device_code_login' first to start the authentication process."
|
|
745
|
-
}
|
|
746
|
-
]
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
// Check if authentication has already completed (non-blocking)
|
|
750
|
-
const isNowAuthenticated = await enhancedMS365Auth.isAuthenticated();
|
|
751
|
-
if (isNowAuthenticated) {
|
|
681
|
+
case "check_pending_auth":
|
|
682
|
+
const pendingDeviceCodeState = await enhancedMS365Auth.getPendingDeviceCodeInfo();
|
|
683
|
+
if (pendingDeviceCodeState) {
|
|
752
684
|
return {
|
|
753
685
|
content: [
|
|
754
686
|
{
|
|
755
687
|
type: "text",
|
|
756
|
-
text:
|
|
688
|
+
text: `⏳ Pending Device Code Authentication\n\n📱 Visit: ${pendingDeviceCodeState.verificationUri}\n🔑 Enter this code: ${pendingDeviceCodeState.userCode}\n\n💡 Use 'authenticate_with_device_code' to finish authentication after entering the code.`
|
|
757
689
|
}
|
|
758
690
|
]
|
|
759
691
|
};
|
|
760
692
|
}
|
|
761
|
-
// If still pending, return status with instructions
|
|
762
|
-
const pendingInfo = enhancedMS365Auth.getPendingDeviceCodeInfo();
|
|
763
|
-
return {
|
|
764
|
-
content: [
|
|
765
|
-
{
|
|
766
|
-
type: "text",
|
|
767
|
-
text: `⏳ Device code authentication still in progress.\n\n📱 Please visit: ${pendingInfo?.verificationUri}\n🔑 Enter code: ${pendingInfo?.userCode}\n\n💡 Try this tool again after completing the authentication in your browser.\n\nNote: Authentication completes automatically once you enter the code - no need to wait.`
|
|
768
|
-
}
|
|
769
|
-
]
|
|
770
|
-
};
|
|
771
|
-
case "check_pending_auth":
|
|
772
|
-
if (enhancedMS365Auth.hasPendingAuth()) {
|
|
773
|
-
const pendingInfo = enhancedMS365Auth.getPendingDeviceCodeInfo();
|
|
774
|
-
if (pendingInfo) {
|
|
775
|
-
return {
|
|
776
|
-
content: [
|
|
777
|
-
{
|
|
778
|
-
type: "text",
|
|
779
|
-
text: `⏳ Pending Device Code Authentication\n\n📱 Visit: ${pendingInfo.verificationUri}\n🔑 Enter this code: ${pendingInfo.userCode}\n\n💡 Use 'complete_device_code_auth' to finish authentication after entering the code.`
|
|
780
|
-
}
|
|
781
|
-
]
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
693
|
return {
|
|
786
694
|
content: [
|
|
787
695
|
{
|
|
@@ -802,64 +710,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
802
710
|
};
|
|
803
711
|
case "verify_authentication":
|
|
804
712
|
const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
|
|
805
|
-
const
|
|
713
|
+
const currentUser = await enhancedMS365Auth.getCurrentUser();
|
|
806
714
|
const storageInfo = enhancedMS365Auth.getStorageInfo();
|
|
807
715
|
return {
|
|
808
716
|
content: [
|
|
809
717
|
{
|
|
810
718
|
type: "text",
|
|
811
|
-
text: `📊 Microsoft 365 Authentication Status\n\n🔐 Authentication: ${isAuthenticated ? '✅ Valid' : '❌ Not authenticated'}\n
|
|
719
|
+
text: `📊 Microsoft 365 Authentication Status\n\n🔐 Authentication: ${isAuthenticated ? '✅ Valid' : '❌ Not authenticated'}\n👤 Current User: ${currentUser || 'None'}\n💾 Storage method: ${storageInfo.method}\n📁 Storage location: ${storageInfo.location}`
|
|
812
720
|
}
|
|
813
721
|
]
|
|
814
722
|
};
|
|
815
|
-
case "
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
{
|
|
820
|
-
type: "text",
|
|
821
|
-
text: `👥 Authenticated Microsoft 365 Accounts:\n\n${authenticatedAccounts.length > 0 ? authenticatedAccounts.map(account => `• ${account}`).join('\n') : 'No authenticated accounts found'}`
|
|
822
|
-
}
|
|
823
|
-
]
|
|
824
|
-
};
|
|
825
|
-
case "get_storage_info":
|
|
826
|
-
const storage = enhancedMS365Auth.getStorageInfo();
|
|
723
|
+
case "logout":
|
|
724
|
+
const accountKey = args?.accountKey;
|
|
725
|
+
const wasAuthenticated = await enhancedMS365Auth.isAuthenticated();
|
|
726
|
+
await enhancedMS365Auth.resetAuth();
|
|
827
727
|
return {
|
|
828
728
|
content: [
|
|
829
729
|
{
|
|
830
730
|
type: "text",
|
|
831
|
-
text:
|
|
731
|
+
text: wasAuthenticated ?
|
|
732
|
+
`✅ Successfully logged out from Microsoft 365.` :
|
|
733
|
+
`ℹ️ No active authentication found.`
|
|
832
734
|
}
|
|
833
735
|
]
|
|
834
736
|
};
|
|
835
|
-
case "logout":
|
|
836
|
-
const accountKey = args?.accountKey;
|
|
837
|
-
// Get list of authenticated accounts before logout for better feedback
|
|
838
|
-
const authenticatedAccountsBefore = await enhancedMS365Auth.listAuthenticatedAccounts();
|
|
839
|
-
await enhancedMS365Auth.resetAuth(accountKey);
|
|
840
|
-
// Check what was actually logged out
|
|
841
|
-
const authenticatedAccountsAfter = await enhancedMS365Auth.listAuthenticatedAccounts();
|
|
842
|
-
const loggedOutAccounts = authenticatedAccountsBefore.filter(account => !authenticatedAccountsAfter.includes(account));
|
|
843
|
-
if (accountKey) {
|
|
844
|
-
return {
|
|
845
|
-
content: [
|
|
846
|
-
{
|
|
847
|
-
type: "text",
|
|
848
|
-
text: `✅ Successfully logged out from Microsoft 365. Account: ${accountKey}`
|
|
849
|
-
}
|
|
850
|
-
]
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
else {
|
|
854
|
-
return {
|
|
855
|
-
content: [
|
|
856
|
-
{
|
|
857
|
-
type: "text",
|
|
858
|
-
text: `✅ Successfully logged out from Microsoft 365.\n${loggedOutAccounts.length > 0 ? `Logged out accounts: ${loggedOutAccounts.join(', ')}` : 'No accounts found to logout'}`
|
|
859
|
-
}
|
|
860
|
-
]
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
737
|
// ============ MULTI-USER AUTHENTICATION TOOLS ============
|
|
864
738
|
case "authenticate_user":
|
|
865
739
|
if (!ms365Config.multiUser) {
|
|
@@ -874,25 +748,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
874
748
|
}
|
|
875
749
|
]
|
|
876
750
|
};
|
|
877
|
-
case "get_my_session_info":
|
|
878
|
-
if (!ms365Config.multiUser) {
|
|
879
|
-
throw new Error("Multi-user mode not enabled. Start server with --multi-user flag.");
|
|
880
|
-
}
|
|
881
|
-
if (!args?.userId) {
|
|
882
|
-
throw new Error("User ID is required");
|
|
883
|
-
}
|
|
884
|
-
const session = multiUserMS365Auth.getUserSession(args.userId);
|
|
885
|
-
if (!session) {
|
|
886
|
-
throw new Error(`User session not found: ${args.userId}`);
|
|
887
|
-
}
|
|
888
|
-
return {
|
|
889
|
-
content: [
|
|
890
|
-
{
|
|
891
|
-
type: "text",
|
|
892
|
-
text: `👤 Session Information\n\n🆔 User ID: ${session.userId}\n📧 Email: ${session.userEmail || 'Unknown'}\n🔐 Authenticated: ${session.authenticated ? 'Yes' : 'No'}\n⏰ Expires: ${new Date(session.expiresOn).toLocaleString()}`
|
|
893
|
-
}
|
|
894
|
-
]
|
|
895
|
-
};
|
|
896
751
|
case "remove_my_session":
|
|
897
752
|
if (!ms365Config.multiUser) {
|
|
898
753
|
throw new Error("Multi-user mode not enabled. Start server with --multi-user flag.");
|
|
@@ -1218,13 +1073,13 @@ async function main() {
|
|
|
1218
1073
|
if (ms365Config.verifyLogin) {
|
|
1219
1074
|
try {
|
|
1220
1075
|
const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
|
|
1221
|
-
const
|
|
1076
|
+
const currentUser = await enhancedMS365Auth.getCurrentUser();
|
|
1222
1077
|
const storageInfo = enhancedMS365Auth.getStorageInfo();
|
|
1223
1078
|
console.log('\n📊 MS365 Authentication Status\n');
|
|
1224
1079
|
console.log(`Authentication: ${isAuthenticated ? '✅ Valid' : '❌ Not authenticated'}`);
|
|
1080
|
+
console.log(`Current User: ${currentUser || 'None'}`);
|
|
1225
1081
|
console.log(`Storage method: ${storageInfo.method}`);
|
|
1226
|
-
console.log(`Storage location: ${storageInfo.location}`);
|
|
1227
|
-
console.log(`Authenticated accounts: ${accounts.length > 0 ? accounts.join(', ') : 'None'}\n`);
|
|
1082
|
+
console.log(`Storage location: ${storageInfo.location}\n`);
|
|
1228
1083
|
process.exit(isAuthenticated ? 0 : 1);
|
|
1229
1084
|
}
|
|
1230
1085
|
catch (error) {
|
|
@@ -23,6 +23,7 @@ const DEFAULT_TENANT_ID = "common";
|
|
|
23
23
|
// Configuration directory and file paths
|
|
24
24
|
const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
|
|
25
25
|
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
26
|
+
const DEVICE_CODE_FILE = path.join(CONFIG_DIR, 'device-code.json');
|
|
26
27
|
/**
|
|
27
28
|
* Enhanced Microsoft 365 authentication manager with device code flow support
|
|
28
29
|
*/
|
|
@@ -259,7 +260,7 @@ export class EnhancedMS365Auth {
|
|
|
259
260
|
return 'device';
|
|
260
261
|
}
|
|
261
262
|
/**
|
|
262
|
-
* Save token using secure credential store
|
|
263
|
+
* Save token using secure credential store (simplified single account)
|
|
263
264
|
*/
|
|
264
265
|
async saveToken(token, authType) {
|
|
265
266
|
try {
|
|
@@ -270,8 +271,8 @@ export class EnhancedMS365Auth {
|
|
|
270
271
|
account: token.account,
|
|
271
272
|
authType: authType
|
|
272
273
|
};
|
|
273
|
-
|
|
274
|
-
await credentialStore.setCredentials(
|
|
274
|
+
// Always use a single account key for simplicity
|
|
275
|
+
await credentialStore.setCredentials('ms365-user', tokenData);
|
|
275
276
|
logger.log('Saved MS365 access token securely');
|
|
276
277
|
}
|
|
277
278
|
catch (error) {
|
|
@@ -279,11 +280,11 @@ export class EnhancedMS365Auth {
|
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
/**
|
|
282
|
-
* Load stored token using secure credential store
|
|
283
|
+
* Load stored token using secure credential store (simplified single account)
|
|
283
284
|
*/
|
|
284
|
-
async loadStoredToken(
|
|
285
|
+
async loadStoredToken() {
|
|
285
286
|
try {
|
|
286
|
-
return await credentialStore.getCredentials(
|
|
287
|
+
return await credentialStore.getCredentials('ms365-user');
|
|
287
288
|
}
|
|
288
289
|
catch (error) {
|
|
289
290
|
logger.error('Error loading stored token:', error);
|
|
@@ -329,22 +330,14 @@ export class EnhancedMS365Auth {
|
|
|
329
330
|
/**
|
|
330
331
|
* Get authenticated Microsoft Graph client
|
|
331
332
|
*/
|
|
332
|
-
async getGraphClient(
|
|
333
|
-
|
|
334
|
-
if (!accountKey) {
|
|
335
|
-
const accounts = await this.listAuthenticatedAccounts();
|
|
336
|
-
if (accounts.length === 0) {
|
|
337
|
-
throw new Error('No authenticated accounts found. Please authenticate first.');
|
|
338
|
-
}
|
|
339
|
-
accountKey = accounts[0];
|
|
340
|
-
}
|
|
341
|
-
const storedToken = await this.loadStoredToken(accountKey);
|
|
333
|
+
async getGraphClient() {
|
|
334
|
+
const storedToken = await this.loadStoredToken();
|
|
342
335
|
if (!storedToken) {
|
|
343
336
|
throw new Error('No stored token found. Please authenticate first.');
|
|
344
337
|
}
|
|
345
338
|
// Check if token is expired
|
|
346
339
|
if (storedToken.expiresOn < Date.now()) {
|
|
347
|
-
await this.refreshToken(
|
|
340
|
+
await this.refreshToken();
|
|
348
341
|
}
|
|
349
342
|
const client = Client.init({
|
|
350
343
|
authProvider: (done) => {
|
|
@@ -356,8 +349,8 @@ export class EnhancedMS365Auth {
|
|
|
356
349
|
/**
|
|
357
350
|
* Refresh access token
|
|
358
351
|
*/
|
|
359
|
-
async refreshToken(
|
|
360
|
-
const storedToken = await this.loadStoredToken(
|
|
352
|
+
async refreshToken() {
|
|
353
|
+
const storedToken = await this.loadStoredToken();
|
|
361
354
|
if (!storedToken?.account) {
|
|
362
355
|
throw new Error('No account information available. Please re-authenticate.');
|
|
363
356
|
}
|
|
@@ -384,29 +377,15 @@ export class EnhancedMS365Auth {
|
|
|
384
377
|
/**
|
|
385
378
|
* Check if user is authenticated
|
|
386
379
|
*/
|
|
387
|
-
async isAuthenticated(
|
|
388
|
-
|
|
389
|
-
if (!accountKey) {
|
|
390
|
-
const accounts = await this.listAuthenticatedAccounts();
|
|
391
|
-
if (accounts.length === 0) {
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
// Check if any account has valid authentication
|
|
395
|
-
for (const account of accounts) {
|
|
396
|
-
if (await this.isAuthenticated(account)) {
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
const storedToken = await this.loadStoredToken(accountKey);
|
|
380
|
+
async isAuthenticated() {
|
|
381
|
+
const storedToken = await this.loadStoredToken();
|
|
403
382
|
if (!storedToken) {
|
|
404
383
|
return false;
|
|
405
384
|
}
|
|
406
385
|
// If token is expired, try to refresh
|
|
407
386
|
if (storedToken.expiresOn < Date.now()) {
|
|
408
387
|
try {
|
|
409
|
-
await this.refreshToken(
|
|
388
|
+
await this.refreshToken();
|
|
410
389
|
return true;
|
|
411
390
|
}
|
|
412
391
|
catch (error) {
|
|
@@ -425,33 +404,160 @@ export class EnhancedMS365Auth {
|
|
|
425
404
|
/**
|
|
426
405
|
* Clear stored authentication data
|
|
427
406
|
*/
|
|
428
|
-
async resetAuth(
|
|
407
|
+
async resetAuth() {
|
|
408
|
+
try {
|
|
409
|
+
await credentialStore.deleteCredentials('ms365-user');
|
|
410
|
+
await this.clearDeviceCodeState();
|
|
411
|
+
logger.log('Cleared stored authentication tokens');
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logger.error('Error clearing authentication data:', error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Save device code state to file
|
|
419
|
+
*/
|
|
420
|
+
async saveDeviceCodeState(state) {
|
|
421
|
+
try {
|
|
422
|
+
fs.writeFileSync(DEVICE_CODE_FILE, JSON.stringify(state, null, 2));
|
|
423
|
+
logger.log('Saved device code state to file');
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
logger.error('Error saving device code state:', error);
|
|
427
|
+
throw new Error('Failed to save device code state');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Load device code state from file
|
|
432
|
+
*/
|
|
433
|
+
async loadDeviceCodeState() {
|
|
429
434
|
try {
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
await credentialStore.deleteCredentials(accountKey);
|
|
433
|
-
logger.log(`Cleared stored authentication tokens for account: ${accountKey}`);
|
|
435
|
+
if (!fs.existsSync(DEVICE_CODE_FILE)) {
|
|
436
|
+
return null;
|
|
434
437
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
438
|
+
const stateData = fs.readFileSync(DEVICE_CODE_FILE, 'utf8');
|
|
439
|
+
const state = JSON.parse(stateData);
|
|
440
|
+
// Check if device code has expired
|
|
441
|
+
const now = Date.now();
|
|
442
|
+
const elapsed = (now - state.startTime) / 1000;
|
|
443
|
+
if (elapsed > state.expiresIn) {
|
|
444
|
+
// Device code has expired, clean up
|
|
445
|
+
await this.clearDeviceCodeState();
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return state;
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
logger.error('Error loading device code state:', error);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Clear device code state file
|
|
457
|
+
*/
|
|
458
|
+
async clearDeviceCodeState() {
|
|
459
|
+
try {
|
|
460
|
+
if (fs.existsSync(DEVICE_CODE_FILE)) {
|
|
461
|
+
fs.unlinkSync(DEVICE_CODE_FILE);
|
|
462
|
+
logger.log('Cleared device code state file');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
logger.error('Error clearing device code state:', error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Complete device code authentication using saved state
|
|
471
|
+
*/
|
|
472
|
+
async completeDeviceCodeAuth() {
|
|
473
|
+
const deviceCodeState = await this.loadDeviceCodeState();
|
|
474
|
+
if (!deviceCodeState) {
|
|
475
|
+
return false; // No pending device code authentication
|
|
476
|
+
}
|
|
477
|
+
if (!await this.loadCredentials()) {
|
|
478
|
+
throw new Error('MS365 credentials not configured');
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
// Use the raw MSAL token endpoint to check if authentication completed
|
|
482
|
+
const tokenUrl = `https://login.microsoftonline.com/${this.credentials.tenantId}/oauth2/v2.0/token`;
|
|
483
|
+
const response = await fetch(tokenUrl, {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers: {
|
|
486
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
487
|
+
},
|
|
488
|
+
body: new URLSearchParams({
|
|
489
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
490
|
+
device_code: deviceCodeState.deviceCode,
|
|
491
|
+
client_id: this.credentials.clientId,
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
const result = await response.json();
|
|
495
|
+
if (response.ok && result.access_token) {
|
|
496
|
+
// Authentication completed successfully
|
|
497
|
+
// Microsoft's token response doesn't include account info, so we need to get it from the token
|
|
498
|
+
let username = 'authenticated-user';
|
|
499
|
+
try {
|
|
500
|
+
// Try to decode the access token to get user info
|
|
501
|
+
const tokenParts = result.access_token.split('.');
|
|
502
|
+
if (tokenParts.length >= 2) {
|
|
503
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
|
|
504
|
+
username = payload.upn || payload.unique_name || payload.preferred_username || 'authenticated-user';
|
|
448
505
|
}
|
|
449
506
|
}
|
|
507
|
+
catch (decodeError) {
|
|
508
|
+
logger.log('Could not decode token for username, using default');
|
|
509
|
+
}
|
|
510
|
+
const tokenResponse = {
|
|
511
|
+
accessToken: result.access_token,
|
|
512
|
+
refreshToken: result.refresh_token || '',
|
|
513
|
+
expiresOn: new Date(Date.now() + (result.expires_in * 1000)),
|
|
514
|
+
account: {
|
|
515
|
+
username: username,
|
|
516
|
+
homeAccountId: `${username}.${this.credentials.tenantId}`,
|
|
517
|
+
environment: 'login.microsoftonline.com',
|
|
518
|
+
tenantId: this.credentials.tenantId,
|
|
519
|
+
localAccountId: username
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
await this.saveToken(tokenResponse, 'device');
|
|
523
|
+
await this.clearDeviceCodeState();
|
|
524
|
+
logger.log('MS365 device code authentication completed successfully');
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
else if (result.error === 'authorization_pending') {
|
|
528
|
+
// Still waiting for user to complete authentication
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
else if (result.error === 'expired_token') {
|
|
532
|
+
// Device code has expired
|
|
533
|
+
await this.clearDeviceCodeState();
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Other error - don't clear device code state, just return false
|
|
538
|
+
logger.error('Device code authentication error:', result);
|
|
539
|
+
return false;
|
|
450
540
|
}
|
|
451
541
|
}
|
|
452
542
|
catch (error) {
|
|
453
|
-
logger.error('Error
|
|
543
|
+
logger.error('Error completing device code authentication:', error);
|
|
544
|
+
// Don't clear device code state on network/other errors
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get pending device code info from saved state
|
|
550
|
+
*/
|
|
551
|
+
async getPendingDeviceCodeInfo() {
|
|
552
|
+
const deviceCodeState = await this.loadDeviceCodeState();
|
|
553
|
+
if (!deviceCodeState) {
|
|
554
|
+
return null;
|
|
454
555
|
}
|
|
556
|
+
return {
|
|
557
|
+
verificationUri: deviceCodeState.verificationUri,
|
|
558
|
+
userCode: deviceCodeState.userCode,
|
|
559
|
+
message: deviceCodeState.message
|
|
560
|
+
};
|
|
455
561
|
}
|
|
456
562
|
/**
|
|
457
563
|
* Get authentication URL for device code flow
|
|
@@ -487,36 +593,47 @@ export class EnhancedMS365Auth {
|
|
|
487
593
|
return new Promise((resolve, reject) => {
|
|
488
594
|
const deviceCodeRequest = {
|
|
489
595
|
scopes: SCOPES,
|
|
490
|
-
deviceCodeCallback: (response) => {
|
|
596
|
+
deviceCodeCallback: async (response) => {
|
|
491
597
|
const deviceCodeInfo = {
|
|
492
598
|
verificationUri: response.verificationUri,
|
|
493
599
|
userCode: response.userCode,
|
|
494
600
|
message: response.message
|
|
495
601
|
};
|
|
496
|
-
//
|
|
497
|
-
const
|
|
498
|
-
.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
resolve(deviceCodeInfo);
|
|
602
|
+
// Save device code state for later completion
|
|
603
|
+
const deviceCodeState = {
|
|
604
|
+
deviceCode: response.deviceCode,
|
|
605
|
+
userCode: response.userCode,
|
|
606
|
+
verificationUri: response.verificationUri,
|
|
607
|
+
expiresIn: response.expiresIn,
|
|
608
|
+
startTime: Date.now(),
|
|
609
|
+
message: response.message
|
|
610
|
+
};
|
|
611
|
+
try {
|
|
612
|
+
await this.saveDeviceCodeState(deviceCodeState);
|
|
613
|
+
logger.log(`Device code authentication started: ${response.verificationUri} - ${response.userCode}`);
|
|
614
|
+
// Return device code info immediately
|
|
615
|
+
resolve(deviceCodeInfo);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
reject(error);
|
|
619
|
+
}
|
|
515
620
|
}
|
|
516
621
|
};
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
622
|
+
// Start the device code flow - we need this to run to get the device code
|
|
623
|
+
// The callback will resolve our promise, and we'll handle the auth later
|
|
624
|
+
msalClient.acquireTokenByDeviceCode(deviceCodeRequest).then((result) => {
|
|
625
|
+
// If this completes immediately (unlikely but possible), save the token
|
|
626
|
+
if (result) {
|
|
627
|
+
this.saveToken(result, 'device').catch(() => {
|
|
628
|
+
// Ignore save errors here since we primarily care about getting the device code
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}).catch((error) => {
|
|
632
|
+
// Only reject if we haven't already resolved with device code info
|
|
633
|
+
// The most common case is that the user hasn't completed auth yet
|
|
634
|
+
if (error.errorCode !== 'user_cancelled' && error.errorCode !== 'authorization_pending') {
|
|
635
|
+
logger.error('Device code flow error:', error);
|
|
636
|
+
}
|
|
520
637
|
});
|
|
521
638
|
});
|
|
522
639
|
}
|
|
@@ -535,12 +652,6 @@ export class EnhancedMS365Auth {
|
|
|
535
652
|
hasPendingAuth() {
|
|
536
653
|
return this.pendingAuth !== null;
|
|
537
654
|
}
|
|
538
|
-
/**
|
|
539
|
-
* Get pending device code info
|
|
540
|
-
*/
|
|
541
|
-
getPendingDeviceCodeInfo() {
|
|
542
|
-
return this.pendingAuth?.deviceCodeInfo || null;
|
|
543
|
-
}
|
|
544
655
|
/**
|
|
545
656
|
* Setup credentials interactively
|
|
546
657
|
*/
|
|
@@ -612,10 +723,14 @@ export class EnhancedMS365Auth {
|
|
|
612
723
|
};
|
|
613
724
|
}
|
|
614
725
|
/**
|
|
615
|
-
*
|
|
726
|
+
* Get current authenticated user (secure - only your own info)
|
|
616
727
|
*/
|
|
617
|
-
async
|
|
618
|
-
|
|
728
|
+
async getCurrentUser() {
|
|
729
|
+
const storedToken = await this.loadStoredToken();
|
|
730
|
+
if (storedToken && storedToken.expiresOn > Date.now()) {
|
|
731
|
+
return storedToken.account?.username || 'authenticated-user';
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
619
734
|
}
|
|
620
735
|
/**
|
|
621
736
|
* Get authentication URL without opening browser (redirect flow)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|