ms365-mcp-server 1.0.0 → 1.0.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.
@@ -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
- const accountKey = token.account?.username || 'default-user';
274
- await credentialStore.setCredentials(accountKey, tokenData);
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(accountKey = 'default-user') {
285
+ async loadStoredToken() {
285
286
  try {
286
- return await credentialStore.getCredentials(accountKey);
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(accountKey) {
333
- // If no specific account key provided, use the first available account
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(accountKey);
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(accountKey = 'default-user') {
360
- const storedToken = await this.loadStoredToken(accountKey);
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(accountKey) {
388
- // If no specific account key provided, check all available accounts
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(accountKey);
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(accountKey) {
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 (accountKey) {
431
- // Delete specific account
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
- else {
436
- // Delete all authenticated accounts
437
- const authenticatedAccounts = await this.listAuthenticatedAccounts();
438
- if (authenticatedAccounts.length === 0) {
439
- // If no accounts found by listing, try deleting the default-user key as fallback
440
- await credentialStore.deleteCredentials('default-user');
441
- logger.log('Cleared stored authentication tokens (fallback to default-user)');
442
- }
443
- else {
444
- // Delete all found accounts
445
- for (const account of authenticatedAccounts) {
446
- await credentialStore.deleteCredentials(account);
447
- logger.log(`Cleared stored authentication tokens for account: ${account}`);
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 clearing authentication data:', 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
- // Store the pending auth promise
497
- const authPromise = msalClient.acquireTokenByDeviceCode(deviceCodeRequest)
498
- .then(async (tokenResponse) => {
499
- if (!tokenResponse) {
500
- throw new Error('Failed to acquire token via device code');
501
- }
502
- await this.saveToken(tokenResponse, 'device');
503
- logger.log('MS365 device code authentication successful');
504
- this.pendingAuth = null; // Clear pending auth
505
- return tokenResponse;
506
- })
507
- .catch((error) => {
508
- this.pendingAuth = null; // Clear pending auth on error
509
- throw error;
510
- });
511
- this.pendingAuth = { authPromise, deviceCodeInfo };
512
- logger.log(`Device code authentication: ${response.verificationUri} - ${response.userCode}`);
513
- // Return device code info immediately
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
- // This will never complete, but will call the callback immediately
518
- msalClient.acquireTokenByDeviceCode(deviceCodeRequest).catch(() => {
519
- // Ignore the error from this call since we're handling it in the stored promise
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
- * List all authenticated accounts
726
+ * Get current authenticated user (secure - only your own info)
616
727
  */
617
- async listAuthenticatedAccounts() {
618
- return await credentialStore.listAccounts();
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)
@@ -389,16 +389,14 @@ export class MS365Operations {
389
389
  return false;
390
390
  }
391
391
  if (criteria.to) {
392
- const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase().includes(criteria.to.toLowerCase()) ||
393
- recipient.name.toLowerCase().includes(criteria.to.toLowerCase()));
392
+ const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
394
393
  if (!toMatch)
395
394
  return false;
396
395
  }
397
396
  if (criteria.cc) {
398
397
  // Handle case where ccRecipients might be undefined
399
398
  const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
400
- message.ccRecipients.some(recipient => recipient.address.toLowerCase().includes(criteria.cc.toLowerCase()) ||
401
- recipient.name.toLowerCase().includes(criteria.cc.toLowerCase()));
399
+ message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
402
400
  if (!ccMatch)
403
401
  return false;
404
402
  }
@@ -640,5 +638,61 @@ export class MS365Operations {
640
638
  throw error;
641
639
  }
642
640
  }
641
+ /**
642
+ * Get current user's email address
643
+ */
644
+ async getCurrentUserEmail() {
645
+ try {
646
+ const graphClient = await this.getGraphClient();
647
+ const user = await graphClient
648
+ .api('/me')
649
+ .select('mail,userPrincipalName')
650
+ .get();
651
+ return user.mail || user.userPrincipalName || '';
652
+ }
653
+ catch (error) {
654
+ logger.error('Error getting current user email:', error);
655
+ throw error;
656
+ }
657
+ }
658
+ /**
659
+ * Search for emails addressed to the current user (both TO and CC recipients)
660
+ */
661
+ async searchEmailsToMe(additionalCriteria = {}) {
662
+ try {
663
+ const userEmail = await this.getCurrentUserEmail();
664
+ // First search for emails where user is in TO field
665
+ const toCriteria = {
666
+ ...additionalCriteria,
667
+ to: userEmail
668
+ };
669
+ // Then search for emails where user is in CC field
670
+ const ccCriteria = {
671
+ ...additionalCriteria,
672
+ cc: userEmail
673
+ };
674
+ // Execute both searches in parallel
675
+ const [toResults, ccResults] = await Promise.all([
676
+ this.searchEmails(toCriteria),
677
+ this.searchEmails(ccCriteria)
678
+ ]);
679
+ // Combine results and remove duplicates based on email ID
680
+ const allMessages = [...toResults.messages, ...ccResults.messages];
681
+ const uniqueMessages = allMessages.filter((message, index, array) => array.findIndex(m => m.id === message.id) === index);
682
+ // Sort by received date (newest first)
683
+ uniqueMessages.sort((a, b) => new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime());
684
+ // Apply maxResults limit if specified
685
+ const maxResults = additionalCriteria.maxResults || 50;
686
+ const limitedMessages = uniqueMessages.slice(0, maxResults);
687
+ return {
688
+ messages: limitedMessages,
689
+ hasMore: uniqueMessages.length > maxResults || toResults.hasMore || ccResults.hasMore
690
+ };
691
+ }
692
+ catch (error) {
693
+ logger.error('Error searching emails addressed to me:', error);
694
+ throw error;
695
+ }
696
+ }
643
697
  }
644
698
  export const ms365Operations = new MS365Operations();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",