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 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 delete emails
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.0"
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('Saved MS365 access token securely');
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
- const storedToken = await this.loadStoredToken();
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: storedToken.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.0",
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
+ }