ms365-mcp-server 1.1.19 → 1.1.21

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.
@@ -0,0 +1,62 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ /**
5
+ * Get configuration directory with simple fallback logic
6
+ * 1. First try: os.homedir()/.ms365-mcp
7
+ * 2. Fallback: /home/siya/.ms365-mcp
8
+ */
9
+ export function getConfigDir() {
10
+ const primaryPath = path.join(os.homedir(), '.ms365-mcp');
11
+ const fallbackPath = '/home/siya/.ms365-mcp';
12
+ // Check if primary path exists or can be created
13
+ try {
14
+ if (fs.existsSync(primaryPath)) {
15
+ return primaryPath;
16
+ }
17
+ // Try to create primary path
18
+ fs.mkdirSync(primaryPath, { recursive: true });
19
+ return primaryPath;
20
+ }
21
+ catch (error) {
22
+ // If primary path fails, use fallback
23
+ try {
24
+ if (!fs.existsSync(fallbackPath)) {
25
+ fs.mkdirSync(fallbackPath, { recursive: true });
26
+ }
27
+ return fallbackPath;
28
+ }
29
+ catch (fallbackError) {
30
+ // If everything fails, return primary path anyway
31
+ return primaryPath;
32
+ }
33
+ }
34
+ }
35
+ /**
36
+ * Check if config files exist in a directory
37
+ */
38
+ export function hasConfigFiles(dirPath) {
39
+ const credentialsFile = path.join(dirPath, 'credentials.json');
40
+ const tokenFile = path.join(dirPath, 'token.json');
41
+ const msalCacheFile = path.join(dirPath, 'msal-cache.json');
42
+ return fs.existsSync(credentialsFile) ||
43
+ fs.existsSync(tokenFile) ||
44
+ fs.existsSync(msalCacheFile);
45
+ }
46
+ /**
47
+ * Get configuration directory, checking fallback if no config files found in primary
48
+ */
49
+ export function getConfigDirWithFallback() {
50
+ const primaryPath = path.join(os.homedir(), '.ms365-mcp');
51
+ const fallbackPath = '/home/siya/.ms365-mcp';
52
+ // If primary path has config files, use it
53
+ if (fs.existsSync(primaryPath) && hasConfigFiles(primaryPath)) {
54
+ return primaryPath;
55
+ }
56
+ // If fallback path has config files, use it
57
+ if (fs.existsSync(fallbackPath) && hasConfigFiles(fallbackPath)) {
58
+ return fallbackPath;
59
+ }
60
+ // Default to primary path for new installations
61
+ return getConfigDir();
62
+ }
@@ -1,57 +1,65 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import * as os from 'os';
4
3
  import { logger } from './api.js';
4
+ import { getConfigDirWithFallback } from './config-dir.js';
5
5
  // Service name for keychain storage
6
6
  const SERVICE_NAME = 'ms365-mcp-server';
7
- // Fallback directory for file-based storage
8
- const FALLBACK_DIR = path.join(os.homedir(), '.ms365-mcp');
7
+ // Directory for file-based storage (now primary storage method)
8
+ const STORAGE_DIR = getConfigDirWithFallback();
9
9
  /**
10
- * Secure credential store that uses OS keychain when available,
11
- * with encrypted file fallback for cross-platform compatibility.
10
+ * Secure credential store that uses file-based storage as default,
11
+ * with optional OS keychain support if specifically enabled.
12
12
  */
13
13
  export class CredentialStore {
14
14
  constructor() {
15
15
  this.useKeychain = false;
16
16
  this.keytar = null;
17
- this.initialized = false;
18
- this.initPromise = null;
19
- this.ensureFallbackDir();
20
- this.initPromise = this.initializeKeychain();
17
+ this.initialized = true; // File storage is always ready
18
+ this.forceFileStorage = true; // Force file storage as default
19
+ this.ensureStorageDir();
20
+ // Don't initialize keychain by default - file storage is primary
21
+ logger.log('Using file-based credential storage as default');
21
22
  }
22
23
  /**
23
24
  * Ensure the credential store is initialized
24
25
  */
25
26
  async ensureInitialized() {
26
- if (!this.initialized && this.initPromise) {
27
- await this.initPromise;
28
- this.initialized = true;
29
- }
27
+ // File storage is always ready, no async initialization needed
28
+ return;
30
29
  }
31
30
  /**
32
- * Initialize keychain support if available
31
+ * Enable keychain support (optional)
33
32
  */
34
- async initializeKeychain() {
33
+ async enableKeychain() {
35
34
  try {
36
35
  // Try to load keytar for secure OS keychain storage
37
36
  // Use eval to prevent TypeScript from checking the import at compile time
38
37
  const keytarModule = await eval('import("keytar")');
39
38
  this.keytar = keytarModule.default || keytarModule;
40
39
  this.useKeychain = true;
40
+ this.forceFileStorage = false;
41
41
  logger.log('OS keychain support enabled');
42
+ return true;
42
43
  }
43
44
  catch (error) {
44
- // Keytar might not be available on all systems
45
- logger.log('OS keychain not available, using file fallback');
46
- this.useKeychain = false;
45
+ logger.log('OS keychain not available, continuing with file storage');
46
+ return false;
47
47
  }
48
48
  }
49
49
  /**
50
- * Ensure fallback directory exists
50
+ * Force file storage (disable keychain)
51
+ */
52
+ forceFileStorageMode() {
53
+ this.useKeychain = false;
54
+ this.forceFileStorage = true;
55
+ logger.log('Forced file storage mode enabled');
56
+ }
57
+ /**
58
+ * Ensure storage directory exists
51
59
  */
52
- ensureFallbackDir() {
53
- if (!fs.existsSync(FALLBACK_DIR)) {
54
- fs.mkdirSync(FALLBACK_DIR, { recursive: true });
60
+ ensureStorageDir() {
61
+ if (!fs.existsSync(STORAGE_DIR)) {
62
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
55
63
  }
56
64
  }
57
65
  /**
@@ -59,27 +67,35 @@ export class CredentialStore {
59
67
  */
60
68
  async setCredentials(account, credentials) {
61
69
  await this.ensureInitialized();
70
+ // Always use file storage as primary method
71
+ if (this.forceFileStorage || !this.useKeychain || !this.keytar) {
72
+ await this.setCredentialsFile(account, credentials);
73
+ return;
74
+ }
75
+ // Only use keychain if explicitly enabled and file storage is disabled
62
76
  const credentialString = JSON.stringify(credentials);
63
- if (this.useKeychain && this.keytar) {
64
- try {
65
- await this.keytar.setPassword(SERVICE_NAME, account, credentialString);
66
- logger.log(`Stored credentials for ${account} in OS keychain`);
67
- return;
68
- }
69
- catch (error) {
70
- logger.error(`Failed to store in keychain for ${account}:`, error);
71
- // Fall through to file storage
72
- }
77
+ try {
78
+ await this.keytar.setPassword(SERVICE_NAME, account, credentialString);
79
+ logger.log(`Stored credentials for ${account} in OS keychain`);
80
+ }
81
+ catch (error) {
82
+ logger.error(`Failed to store in keychain for ${account}:`, error);
83
+ // Fall back to file storage
84
+ await this.setCredentialsFile(account, credentials);
73
85
  }
74
- // Fallback to file storage
75
- await this.setCredentialsFile(account, credentials);
76
86
  }
77
87
  /**
78
88
  * Retrieve credentials securely
79
89
  */
80
90
  async getCredentials(account) {
81
91
  await this.ensureInitialized();
82
- if (this.useKeychain && this.keytar) {
92
+ // Always try file storage first
93
+ const fileCredentials = await this.getCredentialsFile(account);
94
+ if (fileCredentials) {
95
+ return fileCredentials;
96
+ }
97
+ // Only check keychain if file storage failed and keychain is enabled
98
+ if (!this.forceFileStorage && this.useKeychain && this.keytar) {
83
99
  try {
84
100
  const credentialString = await this.keytar.getPassword(SERVICE_NAME, account);
85
101
  if (credentialString) {
@@ -89,11 +105,9 @@ export class CredentialStore {
89
105
  }
90
106
  catch (error) {
91
107
  logger.error(`Failed to retrieve from keychain for ${account}:`, error);
92
- // Fall through to file storage
93
108
  }
94
109
  }
95
- // Fallback to file storage
96
- return await this.getCredentialsFile(account);
110
+ return null;
97
111
  }
98
112
  /**
99
113
  * Delete credentials securely
@@ -138,11 +152,11 @@ export class CredentialStore {
138
152
  return Array.from(accounts);
139
153
  }
140
154
  /**
141
- * File-based credential storage (fallback)
155
+ * File-based credential storage (primary method)
142
156
  */
143
157
  async setCredentialsFile(account, credentials) {
144
158
  try {
145
- const filePath = path.join(FALLBACK_DIR, `token_${this.sanitizeFilename(account)}.json`);
159
+ const filePath = path.join(STORAGE_DIR, `token_${this.sanitizeFilename(account)}.json`);
146
160
  const encryptedData = this.simpleEncrypt(JSON.stringify(credentials));
147
161
  fs.writeFileSync(filePath, encryptedData);
148
162
  logger.log(`Stored credentials for ${account} in file`);
@@ -153,11 +167,11 @@ export class CredentialStore {
153
167
  }
154
168
  }
155
169
  /**
156
- * File-based credential retrieval (fallback)
170
+ * File-based credential retrieval (primary method)
157
171
  */
158
172
  async getCredentialsFile(account) {
159
173
  try {
160
- const filePath = path.join(FALLBACK_DIR, `token_${this.sanitizeFilename(account)}.json`);
174
+ const filePath = path.join(STORAGE_DIR, `token_${this.sanitizeFilename(account)}.json`);
161
175
  if (!fs.existsSync(filePath)) {
162
176
  return null;
163
177
  }
@@ -172,11 +186,11 @@ export class CredentialStore {
172
186
  }
173
187
  }
174
188
  /**
175
- * File-based credential deletion (fallback)
189
+ * File-based credential deletion (primary method)
176
190
  */
177
191
  async deleteCredentialsFile(account) {
178
192
  try {
179
- const filePath = path.join(FALLBACK_DIR, `token_${this.sanitizeFilename(account)}.json`);
193
+ const filePath = path.join(STORAGE_DIR, `token_${this.sanitizeFilename(account)}.json`);
180
194
  if (fs.existsSync(filePath)) {
181
195
  fs.unlinkSync(filePath);
182
196
  logger.log(`Deleted credentials for ${account} from file`);
@@ -194,7 +208,7 @@ export class CredentialStore {
194
208
  */
195
209
  async listFileAccounts() {
196
210
  try {
197
- const files = fs.readdirSync(FALLBACK_DIR);
211
+ const files = fs.readdirSync(STORAGE_DIR);
198
212
  return files
199
213
  .filter(file => file.startsWith('token_') && file.endsWith('.json'))
200
214
  .map(file => file.replace('token_', '').replace('.json', ''))
@@ -246,13 +260,22 @@ export class CredentialStore {
246
260
  * Check if keychain is available
247
261
  */
248
262
  isKeychainAvailable() {
249
- return this.useKeychain && this.keytar !== null;
263
+ return this.useKeychain && this.keytar !== null && !this.forceFileStorage;
250
264
  }
251
265
  /**
252
266
  * Get storage method being used
253
267
  */
254
268
  getStorageMethod() {
255
- return this.useKeychain ? 'OS Keychain' : 'Encrypted File';
269
+ if (this.forceFileStorage || !this.useKeychain || !this.keytar) {
270
+ return 'Encrypted File';
271
+ }
272
+ return 'OS Keychain';
273
+ }
274
+ /**
275
+ * Get storage location
276
+ */
277
+ getStorageLocation() {
278
+ return STORAGE_DIR;
256
279
  }
257
280
  }
258
281
  export const credentialStore = new CredentialStore();
@@ -2,12 +2,12 @@ import { ConfidentialClientApplication, PublicClientApplication } from '@azure/m
2
2
  import { Client } from '@microsoft/microsoft-graph-client';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import * as os from 'os';
6
5
  import open from 'open';
7
6
  import { createServer } from 'http';
8
7
  import { URL } from 'url';
9
8
  import { logger } from './api.js';
10
9
  import { credentialStore } from './credential-store.js';
10
+ import { getConfigDirWithFallback } from './config-dir.js';
11
11
  // Scopes required for Microsoft 365 operations
12
12
  const SCOPES = [
13
13
  'https://graph.microsoft.com/Mail.ReadWrite',
@@ -21,7 +21,7 @@ const SCOPES = [
21
21
  const BUILTIN_CLIENT_ID = "14d82eec-204b-4c2f-b7e8-296a70dab67e"; // Microsoft Graph Command Line Tools
22
22
  const DEFAULT_TENANT_ID = "common";
23
23
  // Configuration directory and file paths
24
- const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
24
+ const CONFIG_DIR = getConfigDirWithFallback();
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
27
  const TOKEN_CACHE_FILE = path.join(CONFIG_DIR, 'msal-cache.json');
@@ -835,7 +835,7 @@ export class EnhancedMS365Auth {
835
835
  getStorageInfo() {
836
836
  return {
837
837
  method: credentialStore.getStorageMethod(),
838
- location: credentialStore.isKeychainAvailable() ? 'OS Keychain' : CONFIG_DIR
838
+ location: credentialStore.getStorageLocation()
839
839
  };
840
840
  }
841
841
  /**
@@ -2,11 +2,11 @@ import { ConfidentialClientApplication } from '@azure/msal-node';
2
2
  import { Client } from '@microsoft/microsoft-graph-client';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import * as os from 'os';
6
5
  import open from 'open';
7
6
  import { createServer } from 'http';
8
7
  import { URL } from 'url';
9
8
  import { logger } from './api.js';
9
+ import { getConfigDirWithFallback } from './config-dir.js';
10
10
  // Scopes required for Microsoft 365 operations
11
11
  const SCOPES = [
12
12
  'https://graph.microsoft.com/Mail.ReadWrite',
@@ -17,7 +17,7 @@ const SCOPES = [
17
17
  'offline_access'
18
18
  ];
19
19
  // Configuration directory and file paths
20
- const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
20
+ const CONFIG_DIR = getConfigDirWithFallback();
21
21
  const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
22
22
  const TOKEN_FILE = path.join(CONFIG_DIR, 'token.json');
23
23
  /**
@@ -2,11 +2,11 @@ import { ConfidentialClientApplication } from '@azure/msal-node';
2
2
  import { Client } from '@microsoft/microsoft-graph-client';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import * as os from 'os';
6
5
  import { createServer } from 'http';
7
6
  import { URL } from 'url';
8
7
  import { logger } from './api.js';
9
8
  import { createHash, randomBytes } from 'crypto';
9
+ import { getConfigDirWithFallback } from './config-dir.js';
10
10
  // Scopes required for Microsoft 365 operations
11
11
  const SCOPES = [
12
12
  'https://graph.microsoft.com/Mail.ReadWrite',
@@ -17,7 +17,7 @@ const SCOPES = [
17
17
  'offline_access'
18
18
  ];
19
19
  // Configuration directory
20
- const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
20
+ const CONFIG_DIR = getConfigDirWithFallback();
21
21
  /**
22
22
  * Multi-user Microsoft 365 authentication manager
23
23
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
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",