ms365-mcp-server 1.1.0 → 1.1.2
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 +15 -1
- package/dist/index.js +83 -3
- package/dist/utils/ms365-auth-enhanced.js +72 -5
- package/dist/utils/ms365-operations.js +68 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ A powerful **Model Context Protocol (MCP) server** that enables seamless Microso
|
|
|
16
16
|
|
|
17
17
|
### **📧 Email Management**
|
|
18
18
|
- **`send_email`** - Send emails with attachments and rich formatting
|
|
19
|
-
- **`manage_email`** - **UNIFIED**: Read, search, list, mark, move, or
|
|
19
|
+
- **`manage_email`** - **UNIFIED**: Read, search, list, mark, move, delete, or draft emails
|
|
20
20
|
- **`get_attachment`** - Download email attachments with metadata
|
|
21
21
|
- **`list_folders`** - Browse mailbox folders with item counts
|
|
22
22
|
|
|
@@ -200,6 +200,20 @@ ms365-mcp-server --login
|
|
|
200
200
|
"messageId": "email_id"
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
// Create draft email
|
|
205
|
+
{
|
|
206
|
+
"tool": "manage_email",
|
|
207
|
+
"arguments": {
|
|
208
|
+
"action": "draft",
|
|
209
|
+
"draftTo": ["colleague@company.com"],
|
|
210
|
+
"draftSubject": "Draft: Project Update",
|
|
211
|
+
"draftBody": "<h2>Status Report</h2><p>Project is on track!</p>",
|
|
212
|
+
"draftBodyType": "html",
|
|
213
|
+
"draftCc": ["manager@company.com"],
|
|
214
|
+
"draftImportance": "normal"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
203
217
|
```
|
|
204
218
|
|
|
205
219
|
### Contact & Authentication
|
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.1.
|
|
58
|
+
version: "1.1.2"
|
|
59
59
|
}, {
|
|
60
60
|
capabilities: {
|
|
61
61
|
resources: {
|
|
@@ -183,8 +183,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
183
183
|
},
|
|
184
184
|
action: {
|
|
185
185
|
type: "string",
|
|
186
|
-
enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me"],
|
|
187
|
-
description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you)"
|
|
186
|
+
enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft"],
|
|
187
|
+
description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create/save draft)"
|
|
188
188
|
},
|
|
189
189
|
messageId: {
|
|
190
190
|
type: "string",
|
|
@@ -259,6 +259,64 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
259
259
|
minimum: 1,
|
|
260
260
|
maximum: 200,
|
|
261
261
|
default: 50
|
|
262
|
+
},
|
|
263
|
+
// Draft email parameters
|
|
264
|
+
draftTo: {
|
|
265
|
+
type: "array",
|
|
266
|
+
items: { type: "string" },
|
|
267
|
+
description: "List of recipient email addresses (required for draft action)"
|
|
268
|
+
},
|
|
269
|
+
draftCc: {
|
|
270
|
+
type: "array",
|
|
271
|
+
items: { type: "string" },
|
|
272
|
+
description: "List of CC recipient email addresses (optional for draft action)"
|
|
273
|
+
},
|
|
274
|
+
draftBcc: {
|
|
275
|
+
type: "array",
|
|
276
|
+
items: { type: "string" },
|
|
277
|
+
description: "List of BCC recipient email addresses (optional for draft action)"
|
|
278
|
+
},
|
|
279
|
+
draftSubject: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Email subject line (required for draft action)"
|
|
282
|
+
},
|
|
283
|
+
draftBody: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Email content (required for draft action)"
|
|
286
|
+
},
|
|
287
|
+
draftBodyType: {
|
|
288
|
+
type: "string",
|
|
289
|
+
enum: ["text", "html"],
|
|
290
|
+
description: "Content type of the email body for draft (default: text)",
|
|
291
|
+
default: "text"
|
|
292
|
+
},
|
|
293
|
+
draftImportance: {
|
|
294
|
+
type: "string",
|
|
295
|
+
enum: ["low", "normal", "high"],
|
|
296
|
+
description: "Email importance level for draft (default: normal)",
|
|
297
|
+
default: "normal"
|
|
298
|
+
},
|
|
299
|
+
draftAttachments: {
|
|
300
|
+
type: "array",
|
|
301
|
+
items: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
name: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Name of the attachment file"
|
|
307
|
+
},
|
|
308
|
+
contentBytes: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Base64 encoded content of the attachment"
|
|
311
|
+
},
|
|
312
|
+
contentType: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "MIME type of the attachment (optional, auto-detected if not provided)"
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
required: ["name", "contentBytes"]
|
|
318
|
+
},
|
|
319
|
+
description: "List of email attachments for draft (optional)"
|
|
262
320
|
}
|
|
263
321
|
},
|
|
264
322
|
required: ["action"],
|
|
@@ -653,6 +711,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
653
711
|
}
|
|
654
712
|
]
|
|
655
713
|
};
|
|
714
|
+
case "draft":
|
|
715
|
+
if (!args?.draftTo || !args?.draftSubject || !args?.draftBody) {
|
|
716
|
+
throw new Error("draftTo, draftSubject, and draftBody are required for draft action");
|
|
717
|
+
}
|
|
718
|
+
const draftResult = await ms365Ops.saveDraftEmail({
|
|
719
|
+
to: args.draftTo,
|
|
720
|
+
cc: args.draftCc,
|
|
721
|
+
bcc: args.draftBcc,
|
|
722
|
+
subject: args.draftSubject,
|
|
723
|
+
body: args.draftBody,
|
|
724
|
+
bodyType: args.draftBodyType || 'text',
|
|
725
|
+
importance: args.draftImportance || 'normal',
|
|
726
|
+
attachments: args.draftAttachments
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: `✅ Draft email saved successfully!\n📧 Subject: ${args.draftSubject}\n👥 To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\n🆔 Draft ID: ${draftResult.id}`
|
|
733
|
+
}
|
|
734
|
+
]
|
|
735
|
+
};
|
|
656
736
|
default:
|
|
657
737
|
throw new Error(`Unknown email action: ${emailAction}`);
|
|
658
738
|
}
|
|
@@ -24,6 +24,7 @@ const DEFAULT_TENANT_ID = "common";
|
|
|
24
24
|
const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
|
|
25
25
|
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
26
26
|
const DEVICE_CODE_FILE = path.join(CONFIG_DIR, 'device-code.json');
|
|
27
|
+
const TOKEN_CACHE_FILE = path.join(CONFIG_DIR, 'msal-cache.json');
|
|
27
28
|
/**
|
|
28
29
|
* Enhanced Microsoft 365 authentication manager with device code flow support
|
|
29
30
|
*/
|
|
@@ -97,13 +98,42 @@ export class EnhancedMS365Auth {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
|
-
* Initialize MSAL client based on auth type
|
|
101
|
+
* Initialize MSAL client based on auth type with persistent token cache
|
|
101
102
|
*/
|
|
102
103
|
initializeMsalClient() {
|
|
103
104
|
if (!this.credentials) {
|
|
104
105
|
throw new Error('Credentials not loaded');
|
|
105
106
|
}
|
|
107
|
+
// Return existing client if already initialized with same credentials
|
|
108
|
+
if (this.msalClient) {
|
|
109
|
+
return this.msalClient;
|
|
110
|
+
}
|
|
106
111
|
const isConfidential = this.credentials.clientSecret && this.credentials.authType === 'redirect';
|
|
112
|
+
// Create persistent token cache
|
|
113
|
+
const cachePlugin = {
|
|
114
|
+
beforeCacheAccess: async (cacheContext) => {
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(TOKEN_CACHE_FILE)) {
|
|
117
|
+
const cacheData = fs.readFileSync(TOKEN_CACHE_FILE, 'utf8');
|
|
118
|
+
cacheContext.tokenCache.deserialize(cacheData);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.error('Error loading MSAL token cache:', error);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
afterCacheAccess: async (cacheContext) => {
|
|
126
|
+
try {
|
|
127
|
+
if (cacheContext.cacheHasChanged) {
|
|
128
|
+
const cacheData = cacheContext.tokenCache.serialize();
|
|
129
|
+
fs.writeFileSync(TOKEN_CACHE_FILE, cacheData);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
logger.error('Error saving MSAL token cache:', error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
107
137
|
if (isConfidential) {
|
|
108
138
|
// Confidential client for redirect-based auth
|
|
109
139
|
const config = {
|
|
@@ -112,6 +142,9 @@ export class EnhancedMS365Auth {
|
|
|
112
142
|
clientSecret: this.credentials.clientSecret,
|
|
113
143
|
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
|
|
114
144
|
},
|
|
145
|
+
cache: {
|
|
146
|
+
cachePlugin
|
|
147
|
+
},
|
|
115
148
|
system: {
|
|
116
149
|
loggerOptions: {
|
|
117
150
|
loggerCallback: (level, message, containsPii) => {
|
|
@@ -133,6 +166,9 @@ export class EnhancedMS365Auth {
|
|
|
133
166
|
clientId: this.credentials.clientId,
|
|
134
167
|
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
|
|
135
168
|
},
|
|
169
|
+
cache: {
|
|
170
|
+
cachePlugin
|
|
171
|
+
},
|
|
136
172
|
system: {
|
|
137
173
|
loggerOptions: {
|
|
138
174
|
loggerCallback: (level, message, containsPii) => {
|
|
@@ -147,6 +183,7 @@ export class EnhancedMS365Auth {
|
|
|
147
183
|
};
|
|
148
184
|
this.msalClient = new PublicClientApplication(config);
|
|
149
185
|
}
|
|
186
|
+
logger.log('Initialized MSAL client with persistent token cache');
|
|
150
187
|
return this.msalClient;
|
|
151
188
|
}
|
|
152
189
|
/**
|
|
@@ -266,14 +303,14 @@ export class EnhancedMS365Auth {
|
|
|
266
303
|
try {
|
|
267
304
|
const tokenData = {
|
|
268
305
|
accessToken: token.accessToken,
|
|
269
|
-
refreshToken: '',
|
|
306
|
+
refreshToken: '', // MSAL manages refresh tokens internally
|
|
270
307
|
expiresOn: token.expiresOn?.getTime() || 0,
|
|
271
308
|
account: token.account,
|
|
272
309
|
authType: authType
|
|
273
310
|
};
|
|
274
311
|
// Always use a single account key for simplicity
|
|
275
312
|
await credentialStore.setCredentials('ms365-user', tokenData);
|
|
276
|
-
logger.log(
|
|
313
|
+
logger.log(`Saved MS365 access token securely (expires: ${new Date(tokenData.expiresOn).toLocaleString()})`);
|
|
277
314
|
}
|
|
278
315
|
catch (error) {
|
|
279
316
|
logger.error('Error saving token:', error);
|
|
@@ -331,13 +368,19 @@ export class EnhancedMS365Auth {
|
|
|
331
368
|
* Get authenticated Microsoft Graph client
|
|
332
369
|
*/
|
|
333
370
|
async getGraphClient() {
|
|
334
|
-
|
|
371
|
+
let storedToken = await this.loadStoredToken();
|
|
335
372
|
if (!storedToken) {
|
|
336
373
|
throw new Error('No stored token found. Please authenticate first.');
|
|
337
374
|
}
|
|
338
375
|
// Check if token is expired
|
|
339
376
|
if (storedToken.expiresOn < Date.now()) {
|
|
377
|
+
logger.log('Access token expired, refreshing...');
|
|
340
378
|
await this.refreshToken();
|
|
379
|
+
// Reload the token after refresh
|
|
380
|
+
storedToken = await this.loadStoredToken();
|
|
381
|
+
if (!storedToken) {
|
|
382
|
+
throw new Error('Failed to refresh token. Please re-authenticate.');
|
|
383
|
+
}
|
|
341
384
|
}
|
|
342
385
|
const client = Client.init({
|
|
343
386
|
authProvider: (done) => {
|
|
@@ -399,9 +442,23 @@ export class EnhancedMS365Auth {
|
|
|
399
442
|
}
|
|
400
443
|
const msalClient = this.initializeMsalClient();
|
|
401
444
|
try {
|
|
445
|
+
// Try to get all accounts from MSAL cache first (only available on PublicClientApplication)
|
|
446
|
+
let accountToUse = storedToken.account;
|
|
447
|
+
if (msalClient instanceof PublicClientApplication) {
|
|
448
|
+
const accounts = await msalClient.getAllAccounts();
|
|
449
|
+
// If we have accounts in MSAL cache, use the first one that matches
|
|
450
|
+
if (accounts.length > 0) {
|
|
451
|
+
const matchingAccount = accounts.find((acc) => acc.username === storedToken.account?.username ||
|
|
452
|
+
acc.homeAccountId === storedToken.account?.homeAccountId);
|
|
453
|
+
if (matchingAccount) {
|
|
454
|
+
accountToUse = matchingAccount;
|
|
455
|
+
logger.log('Using account from MSAL cache for token refresh');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
402
459
|
const tokenResponse = await msalClient.acquireTokenSilent({
|
|
403
460
|
scopes: SCOPES,
|
|
404
|
-
account:
|
|
461
|
+
account: accountToUse
|
|
405
462
|
});
|
|
406
463
|
if (!tokenResponse) {
|
|
407
464
|
throw new Error('Failed to refresh token - please re-authenticate using: authenticate_with_device_code');
|
|
@@ -417,6 +474,9 @@ export class EnhancedMS365Auth {
|
|
|
417
474
|
else if (error.errorCode === 'consent_required') {
|
|
418
475
|
throw new Error('Additional consent required. Please re-authenticate using the "authenticate_with_device_code" tool.');
|
|
419
476
|
}
|
|
477
|
+
else if (error.errorCode === 'no_account_in_silent_request') {
|
|
478
|
+
throw new Error('No account found in token cache. Please re-authenticate using the "authenticate_with_device_code" tool.');
|
|
479
|
+
}
|
|
420
480
|
else {
|
|
421
481
|
logger.error('Token refresh failed:', error);
|
|
422
482
|
throw new Error(`Token refresh failed: ${error.message}. Please re-authenticate using the "authenticate_with_device_code" tool.`);
|
|
@@ -457,6 +517,13 @@ export class EnhancedMS365Auth {
|
|
|
457
517
|
try {
|
|
458
518
|
await credentialStore.deleteCredentials('ms365-user');
|
|
459
519
|
await this.clearDeviceCodeState();
|
|
520
|
+
// Clear MSAL token cache
|
|
521
|
+
if (fs.existsSync(TOKEN_CACHE_FILE)) {
|
|
522
|
+
fs.unlinkSync(TOKEN_CACHE_FILE);
|
|
523
|
+
logger.log('Cleared MSAL token cache');
|
|
524
|
+
}
|
|
525
|
+
// Reset MSAL client instance to force re-initialization
|
|
526
|
+
this.msalClient = null;
|
|
460
527
|
logger.log('Cleared stored authentication tokens');
|
|
461
528
|
}
|
|
462
529
|
catch (error) {
|
|
@@ -244,6 +244,74 @@ export class MS365Operations {
|
|
|
244
244
|
throw error;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Save a draft email
|
|
249
|
+
*/
|
|
250
|
+
async saveDraftEmail(message) {
|
|
251
|
+
try {
|
|
252
|
+
const graphClient = await this.getGraphClient();
|
|
253
|
+
// Prepare recipients
|
|
254
|
+
const toRecipients = message.to.map(email => ({
|
|
255
|
+
emailAddress: {
|
|
256
|
+
address: email,
|
|
257
|
+
name: email.split('@')[0]
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
260
|
+
const ccRecipients = message.cc?.map(email => ({
|
|
261
|
+
emailAddress: {
|
|
262
|
+
address: email,
|
|
263
|
+
name: email.split('@')[0]
|
|
264
|
+
}
|
|
265
|
+
})) || [];
|
|
266
|
+
const bccRecipients = message.bcc?.map(email => ({
|
|
267
|
+
emailAddress: {
|
|
268
|
+
address: email,
|
|
269
|
+
name: email.split('@')[0]
|
|
270
|
+
}
|
|
271
|
+
})) || [];
|
|
272
|
+
// Prepare attachments
|
|
273
|
+
const attachments = message.attachments?.map(att => ({
|
|
274
|
+
'@odata.type': '#microsoft.graph.fileAttachment',
|
|
275
|
+
name: att.name,
|
|
276
|
+
contentBytes: att.contentBytes,
|
|
277
|
+
contentType: att.contentType || 'application/octet-stream'
|
|
278
|
+
})) || [];
|
|
279
|
+
// Prepare draft email body
|
|
280
|
+
const draftBody = {
|
|
281
|
+
subject: message.subject,
|
|
282
|
+
body: {
|
|
283
|
+
contentType: message.bodyType === 'html' ? 'html' : 'text',
|
|
284
|
+
content: message.body || ''
|
|
285
|
+
},
|
|
286
|
+
toRecipients,
|
|
287
|
+
ccRecipients,
|
|
288
|
+
bccRecipients,
|
|
289
|
+
importance: message.importance || 'normal',
|
|
290
|
+
attachments: attachments.length > 0 ? attachments : undefined
|
|
291
|
+
};
|
|
292
|
+
if (message.replyTo) {
|
|
293
|
+
draftBody.replyTo = [{
|
|
294
|
+
emailAddress: {
|
|
295
|
+
address: message.replyTo,
|
|
296
|
+
name: message.replyTo.split('@')[0]
|
|
297
|
+
}
|
|
298
|
+
}];
|
|
299
|
+
}
|
|
300
|
+
// Save draft email
|
|
301
|
+
const result = await graphClient
|
|
302
|
+
.api('/me/messages')
|
|
303
|
+
.post(draftBody);
|
|
304
|
+
logger.log('Draft email saved successfully');
|
|
305
|
+
return {
|
|
306
|
+
id: result.id,
|
|
307
|
+
status: 'draft'
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
logger.error('Error saving draft email:', error);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
247
315
|
/**
|
|
248
316
|
* Get email by ID
|
|
249
317
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=18.0.0"
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|