@softeria/ms-365-mcp-server 0.11.0 → 0.11.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
@@ -60,6 +60,29 @@ get-sharepoint-site-drive-by-id, list-sharepoint-site-items, get-sharepoint-site
60
60
  get-sharepoint-site-list, list-sharepoint-site-list-items, get-sharepoint-site-list-item,
61
61
  get-sharepoint-sites-delta</sub>
62
62
 
63
+ ### Work Scopes Issues
64
+
65
+ If you're having issues accessing work/school features (Teams, SharePoint, etc.), you should pass the
66
+ `--force-work-scopes` flag!
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "ms365": {
72
+ "command": "npx",
73
+ "args": [
74
+ "-y",
75
+ "@softeria/ms-365-mcp-server",
76
+ "--force-work-scopes"
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ While the server should attempt to force a re-login when work scopes are needed, passing the flag explicitly is safer
84
+ and ensures proper scope permissions from the start.
85
+
63
86
  **User Profile**
64
87
  <sub>get-current-user</sub>
65
88
 
@@ -146,22 +169,23 @@ MCP clients will automatically handle the OAuth flow when they see the advertise
146
169
 
147
170
  ##### Setting up Azure AD for OAuth Testing
148
171
 
149
- To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app registration:
172
+ To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app
173
+ registration:
150
174
 
151
175
  1. **Create Azure AD App Registration**:
152
- - Go to [Azure Portal](https://portal.azure.com)
153
- - Navigate to Azure Active Directory → App registrations → New registration
154
- - Set name: "MS365 MCP Server"
176
+ - Go to [Azure Portal](https://portal.azure.com)
177
+ - Navigate to Azure Active Directory → App registrations → New registration
178
+ - Set name: "MS365 MCP Server"
155
179
 
156
180
  2. **Configure Redirect URIs**:
157
181
  Add these redirect URIs for testing with MCP Inspector:
158
- - `http://localhost:6274/oauth/callback`
159
- - `http://localhost:6274/oauth/callback/debug`
160
- - `http://localhost:3000/callback` (optional, for server callback)
182
+ - `http://localhost:6274/oauth/callback`
183
+ - `http://localhost:6274/oauth/callback/debug`
184
+ - `http://localhost:3000/callback` (optional, for server callback)
161
185
 
162
186
  3. **Get Credentials**:
163
- - Copy the **Application (client) ID** from Overview page
164
- - Go to Certificates & secrets → New client secret → Copy the secret value
187
+ - Copy the **Application (client) ID** from Overview page
188
+ - Go to Certificates & secrets → New client secret → Copy the secret value
165
189
 
166
190
  4. **Configure Environment Variables**:
167
191
  Create a `.env` file in your project root:
@@ -175,13 +199,15 @@ With these configured, the server will use your custom Azure app instead of the
175
199
 
176
200
  #### 3. Bring Your Own Token (BYOT)
177
201
 
178
- If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can provide an access token directly to this MCP server:
202
+ If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can
203
+ provide an access token directly to this MCP server:
179
204
 
180
205
  ```bash
181
206
  MS365_MCP_OAUTH_TOKEN=your_oauth_token npx @softeria/ms-365-mcp-server
182
207
  ```
183
208
 
184
209
  This method:
210
+
185
211
  - Bypasses the interactive authentication flows
186
212
  - Uses your pre-existing OAuth token for Microsoft Graph API requests
187
213
  - Does not handle token refresh (token lifecycle management is your responsibility)
@@ -77,4 +77,108 @@ export function registerAuthTools(server, authManager) {
77
77
  ],
78
78
  };
79
79
  });
80
+ server.tool('list-accounts', {}, async () => {
81
+ try {
82
+ const accounts = await authManager.listAccounts();
83
+ const selectedAccountId = authManager.getSelectedAccountId();
84
+ const result = accounts.map(account => ({
85
+ id: account.homeAccountId,
86
+ username: account.username,
87
+ name: account.name,
88
+ selected: account.homeAccountId === selectedAccountId
89
+ }));
90
+ return {
91
+ content: [
92
+ {
93
+ type: 'text',
94
+ text: JSON.stringify({ accounts: result }),
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ catch (error) {
100
+ return {
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: JSON.stringify({ error: `Failed to list accounts: ${error.message}` }),
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ });
110
+ server.tool('select-account', {
111
+ accountId: z.string().describe('The account ID to select'),
112
+ }, async ({ accountId }) => {
113
+ try {
114
+ const success = await authManager.selectAccount(accountId);
115
+ if (success) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: JSON.stringify({ message: `Selected account: ${accountId}` }),
121
+ },
122
+ ],
123
+ };
124
+ }
125
+ else {
126
+ return {
127
+ content: [
128
+ {
129
+ type: 'text',
130
+ text: JSON.stringify({ error: `Account not found: ${accountId}` }),
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ }
136
+ catch (error) {
137
+ return {
138
+ content: [
139
+ {
140
+ type: 'text',
141
+ text: JSON.stringify({ error: `Failed to select account: ${error.message}` }),
142
+ },
143
+ ],
144
+ };
145
+ }
146
+ });
147
+ server.tool('remove-account', {
148
+ accountId: z.string().describe('The account ID to remove'),
149
+ }, async ({ accountId }) => {
150
+ try {
151
+ const success = await authManager.removeAccount(accountId);
152
+ if (success) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: JSON.stringify({ message: `Removed account: ${accountId}` }),
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ else {
163
+ return {
164
+ content: [
165
+ {
166
+ type: 'text',
167
+ text: JSON.stringify({ error: `Account not found: ${accountId}` }),
168
+ },
169
+ ],
170
+ };
171
+ }
172
+ }
173
+ catch (error) {
174
+ return {
175
+ content: [
176
+ {
177
+ type: 'text',
178
+ text: JSON.stringify({ error: `Failed to remove account: ${error.message}` }),
179
+ },
180
+ ],
181
+ };
182
+ }
183
+ });
80
184
  }
package/dist/auth.js CHANGED
@@ -9,8 +9,10 @@ const endpoints = await import('./endpoints.json', {
9
9
  });
10
10
  const SERVICE_NAME = 'ms-365-mcp-server';
11
11
  const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
12
+ const SELECTED_ACCOUNT_KEY = 'selected-account';
12
13
  const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
13
14
  const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
15
+ const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, '..', '.selected-account.json');
14
16
  const DEFAULT_CONFIG = {
15
17
  auth: {
16
18
  clientId: process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
@@ -53,6 +55,7 @@ class AuthManager {
53
55
  this.msalApp = new PublicClientApplication(this.config);
54
56
  this.accessToken = null;
55
57
  this.tokenExpiry = null;
58
+ this.selectedAccountId = null;
56
59
  const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
57
60
  this.oauthToken = oauthTokenFromEnv ?? null;
58
61
  this.isOAuthMode = oauthTokenFromEnv != null;
@@ -75,11 +78,38 @@ class AuthManager {
75
78
  if (cacheData) {
76
79
  this.msalApp.getTokenCache().deserialize(cacheData);
77
80
  }
81
+ // Load selected account
82
+ await this.loadSelectedAccount();
78
83
  }
79
84
  catch (error) {
80
85
  logger.error(`Error loading token cache: ${error.message}`);
81
86
  }
82
87
  }
88
+ async loadSelectedAccount() {
89
+ try {
90
+ let selectedAccountData;
91
+ try {
92
+ const cachedData = await keytar.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
93
+ if (cachedData) {
94
+ selectedAccountData = cachedData;
95
+ }
96
+ }
97
+ catch (keytarError) {
98
+ logger.warn(`Keychain access failed for selected account, falling back to file storage: ${keytarError.message}`);
99
+ }
100
+ if (!selectedAccountData && fs.existsSync(SELECTED_ACCOUNT_PATH)) {
101
+ selectedAccountData = fs.readFileSync(SELECTED_ACCOUNT_PATH, 'utf8');
102
+ }
103
+ if (selectedAccountData) {
104
+ const parsed = JSON.parse(selectedAccountData);
105
+ this.selectedAccountId = parsed.accountId;
106
+ logger.info(`Loaded selected account: ${this.selectedAccountId}`);
107
+ }
108
+ }
109
+ catch (error) {
110
+ logger.error(`Error loading selected account: ${error.message}`);
111
+ }
112
+ }
83
113
  async saveTokenCache() {
84
114
  try {
85
115
  const cacheData = this.msalApp.getTokenCache().serialize();
@@ -95,6 +125,21 @@ class AuthManager {
95
125
  logger.error(`Error saving token cache: ${error.message}`);
96
126
  }
97
127
  }
128
+ async saveSelectedAccount() {
129
+ try {
130
+ const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
131
+ try {
132
+ await keytar.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
133
+ }
134
+ catch (keytarError) {
135
+ logger.warn(`Keychain save failed for selected account, falling back to file storage: ${keytarError.message}`);
136
+ fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData);
137
+ }
138
+ }
139
+ catch (error) {
140
+ logger.error(`Error saving selected account: ${error.message}`);
141
+ }
142
+ }
98
143
  async setOAuthToken(token) {
99
144
  this.oauthToken = token;
100
145
  this.isOAuthMode = true;
@@ -106,10 +151,10 @@ class AuthManager {
106
151
  if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
107
152
  return this.accessToken;
108
153
  }
109
- const accounts = await this.msalApp.getTokenCache().getAllAccounts();
110
- if (accounts.length > 0) {
154
+ const currentAccount = await this.getCurrentAccount();
155
+ if (currentAccount) {
111
156
  const silentRequest = {
112
- account: accounts[0],
157
+ account: currentAccount,
113
158
  scopes: this.scopes,
114
159
  };
115
160
  try {
@@ -125,6 +170,22 @@ class AuthManager {
125
170
  }
126
171
  throw new Error('No valid token found');
127
172
  }
173
+ async getCurrentAccount() {
174
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
175
+ if (accounts.length === 0) {
176
+ return null;
177
+ }
178
+ // If a specific account is selected, find it
179
+ if (this.selectedAccountId) {
180
+ const selectedAccount = accounts.find((account) => account.homeAccountId === this.selectedAccountId);
181
+ if (selectedAccount) {
182
+ return selectedAccount;
183
+ }
184
+ logger.warn(`Selected account ${this.selectedAccountId} not found, falling back to first account`);
185
+ }
186
+ // Fall back to first account (backward compatibility)
187
+ return accounts[0];
188
+ }
128
189
  async acquireTokenByDeviceCode(hack) {
129
190
  const deviceCodeRequest = {
130
191
  scopes: this.scopes,
@@ -147,6 +208,12 @@ class AuthManager {
147
208
  logger.info('Device code login successful');
148
209
  this.accessToken = response?.accessToken || null;
149
210
  this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
211
+ // Set the newly authenticated account as selected if no account is currently selected
212
+ if (!this.selectedAccountId && response?.account) {
213
+ this.selectedAccountId = response.account.homeAccountId;
214
+ await this.saveSelectedAccount();
215
+ logger.info(`Auto-selected new account: ${response.account.username}`);
216
+ }
150
217
  await this.saveTokenCache();
151
218
  return this.accessToken;
152
219
  }
@@ -218,8 +285,10 @@ class AuthManager {
218
285
  }
219
286
  this.accessToken = null;
220
287
  this.tokenExpiry = null;
288
+ this.selectedAccountId = null;
221
289
  try {
222
290
  await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
291
+ await keytar.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
223
292
  }
224
293
  catch (keytarError) {
225
294
  logger.warn(`Keychain deletion failed: ${keytarError.message}`);
@@ -227,6 +296,9 @@ class AuthManager {
227
296
  if (fs.existsSync(FALLBACK_PATH)) {
228
297
  fs.unlinkSync(FALLBACK_PATH);
229
298
  }
299
+ if (fs.existsSync(SELECTED_ACCOUNT_PATH)) {
300
+ fs.unlinkSync(SELECTED_ACCOUNT_PATH);
301
+ }
230
302
  return true;
231
303
  }
232
304
  catch (error) {
@@ -236,8 +308,8 @@ class AuthManager {
236
308
  }
237
309
  async hasWorkAccountPermissions() {
238
310
  try {
239
- const accounts = await this.msalApp.getTokenCache().getAllAccounts();
240
- if (accounts.length === 0) {
311
+ const currentAccount = await this.getCurrentAccount();
312
+ if (!currentAccount) {
241
313
  return false;
242
314
  }
243
315
  const workScopes = endpoints.default
@@ -246,7 +318,7 @@ class AuthManager {
246
318
  try {
247
319
  await this.msalApp.acquireTokenSilent({
248
320
  scopes: workScopes.slice(0, 1),
249
- account: accounts[0],
321
+ account: currentAccount,
250
322
  });
251
323
  return true;
252
324
  }
@@ -287,6 +359,12 @@ class AuthManager {
287
359
  this.accessToken = response?.accessToken || null;
288
360
  this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
289
361
  this.scopes = allScopes;
362
+ // Update selected account if this is a new account
363
+ if (response?.account) {
364
+ this.selectedAccountId = response.account.homeAccountId;
365
+ await this.saveSelectedAccount();
366
+ logger.info(`Updated selected account after scope expansion: ${response.account.username}`);
367
+ }
290
368
  await this.saveTokenCache();
291
369
  return true;
292
370
  }
@@ -295,6 +373,71 @@ class AuthManager {
295
373
  return false;
296
374
  }
297
375
  }
376
+ // Multi-account support methods
377
+ async listAccounts() {
378
+ return await this.msalApp.getTokenCache().getAllAccounts();
379
+ }
380
+ async selectAccount(accountId) {
381
+ const accounts = await this.listAccounts();
382
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
383
+ if (!account) {
384
+ logger.error(`Account with ID ${accountId} not found`);
385
+ return false;
386
+ }
387
+ this.selectedAccountId = accountId;
388
+ await this.saveSelectedAccount();
389
+ // Clear cached tokens to force refresh with new account
390
+ this.accessToken = null;
391
+ this.tokenExpiry = null;
392
+ logger.info(`Selected account: ${account.username} (${accountId})`);
393
+ return true;
394
+ }
395
+ async getTokenForAccount(accountId) {
396
+ const accounts = await this.listAccounts();
397
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
398
+ if (!account) {
399
+ throw new Error(`Account with ID ${accountId} not found`);
400
+ }
401
+ const silentRequest = {
402
+ account: account,
403
+ scopes: this.scopes,
404
+ };
405
+ try {
406
+ const response = await this.msalApp.acquireTokenSilent(silentRequest);
407
+ return response.accessToken;
408
+ }
409
+ catch (error) {
410
+ logger.error(`Failed to get token for account ${accountId}: ${error.message}`);
411
+ throw error;
412
+ }
413
+ }
414
+ async removeAccount(accountId) {
415
+ const accounts = await this.listAccounts();
416
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
417
+ if (!account) {
418
+ logger.error(`Account with ID ${accountId} not found`);
419
+ return false;
420
+ }
421
+ try {
422
+ await this.msalApp.getTokenCache().removeAccount(account);
423
+ // If this was the selected account, clear the selection
424
+ if (this.selectedAccountId === accountId) {
425
+ this.selectedAccountId = null;
426
+ await this.saveSelectedAccount();
427
+ this.accessToken = null;
428
+ this.tokenExpiry = null;
429
+ }
430
+ logger.info(`Removed account: ${account.username} (${accountId})`);
431
+ return true;
432
+ }
433
+ catch (error) {
434
+ logger.error(`Failed to remove account ${accountId}: ${error.message}`);
435
+ return false;
436
+ }
437
+ }
438
+ getSelectedAccountId() {
439
+ return this.selectedAccountId;
440
+ }
298
441
  requiresWorkAccountScope(toolName) {
299
442
  const endpoint = endpoints.default.find((e) => e.toolName === toolName);
300
443
  return endpoint?.requiresWorkAccount === true;
package/dist/cli.js CHANGED
@@ -15,6 +15,9 @@ program
15
15
  .option('--login', 'Login using device code flow')
16
16
  .option('--logout', 'Log out and clear saved credentials')
17
17
  .option('--verify-login', 'Verify login without starting the server')
18
+ .option('--list-accounts', 'List all cached accounts')
19
+ .option('--select-account <accountId>', 'Select a specific account by ID')
20
+ .option('--remove-account <accountId>', 'Remove a specific account by ID')
18
21
  .option('--read-only', 'Start server in read-only mode, disabling write operations')
19
22
  .option('--http [port]', 'Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)')
20
23
  .option('--enable-auth-tools', 'Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)')
@@ -11,14 +11,39 @@ class GraphClient {
11
11
  this.accessToken = accessToken;
12
12
  this.refreshToken = refreshToken || null;
13
13
  }
14
+ async getCurrentAccountId() {
15
+ const currentAccount = await this.authManager.getCurrentAccount();
16
+ return currentAccount?.homeAccountId || null;
17
+ }
18
+ getAccountSessions(accountId) {
19
+ if (!this.sessions.has(accountId)) {
20
+ this.sessions.set(accountId, new Map());
21
+ }
22
+ return this.sessions.get(accountId);
23
+ }
24
+ async getSessionForFile(filePath) {
25
+ const accountId = await this.getCurrentAccountId();
26
+ if (!accountId)
27
+ return null;
28
+ const accountSessions = this.getAccountSessions(accountId);
29
+ return accountSessions.get(filePath) || null;
30
+ }
31
+ async setSessionForFile(filePath, sessionId) {
32
+ const accountId = await this.getCurrentAccountId();
33
+ if (!accountId)
34
+ return;
35
+ const accountSessions = this.getAccountSessions(accountId);
36
+ accountSessions.set(filePath, sessionId);
37
+ }
14
38
  async createSession(filePath) {
15
39
  try {
16
40
  if (!filePath) {
17
41
  logger.error('No file path provided for Excel session');
18
42
  return null;
19
43
  }
20
- if (this.sessions.has(filePath)) {
21
- return this.sessions.get(filePath) || null;
44
+ const existingSession = await this.getSessionForFile(filePath);
45
+ if (existingSession) {
46
+ return existingSession;
22
47
  }
23
48
  logger.info(`Creating new Excel session for file: ${filePath}`);
24
49
  const accessToken = await this.authManager.getToken();
@@ -37,7 +62,7 @@ class GraphClient {
37
62
  }
38
63
  const result = await response.json();
39
64
  logger.info(`Session created successfully for file: ${filePath}`);
40
- this.sessions.set(filePath, result.id);
65
+ await this.setSessionForFile(filePath, result.id);
41
66
  return result.id;
42
67
  }
43
68
  catch (error) {
@@ -116,7 +141,7 @@ class GraphClient {
116
141
  !endpoint.startsWith('/teams') &&
117
142
  !endpoint.startsWith('/chats') &&
118
143
  !endpoint.startsWith('/planner')) {
119
- sessionId = this.sessions.get(options.excelFile) || null;
144
+ sessionId = await this.getSessionForFile(options.excelFile);
120
145
  if (!sessionId) {
121
146
  sessionId = await this.createSessionWithToken(options.excelFile, accessToken);
122
147
  }
@@ -166,8 +191,9 @@ class GraphClient {
166
191
  logger.error('No file path provided for Excel session');
167
192
  return null;
168
193
  }
169
- if (this.sessions.has(filePath)) {
170
- return this.sessions.get(filePath) || null;
194
+ const existingSession = await this.getSessionForFile(filePath);
195
+ if (existingSession) {
196
+ return existingSession;
171
197
  }
172
198
  logger.info(`Creating new Excel session for file: ${filePath}`);
173
199
  const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`, {
@@ -185,7 +211,7 @@ class GraphClient {
185
211
  }
186
212
  const result = await response.json();
187
213
  logger.info(`Session created successfully for file: ${filePath}`);
188
- this.sessions.set(filePath, result.id);
214
+ await this.setSessionForFile(filePath, result.id);
189
215
  return result.id;
190
216
  }
191
217
  catch (error) {
@@ -236,7 +262,7 @@ class GraphClient {
236
262
  !endpoint.startsWith('/chats') &&
237
263
  !endpoint.startsWith('/planner') &&
238
264
  !endpoint.startsWith('/sites')) {
239
- sessionId = this.sessions.get(options.excelFile) || null;
265
+ sessionId = await this.getSessionForFile(options.excelFile);
240
266
  if (!sessionId) {
241
267
  sessionId = await this.createSession(options.excelFile);
242
268
  }
@@ -407,7 +433,8 @@ class GraphClient {
407
433
  }
408
434
  }
409
435
  async closeSession(filePath) {
410
- if (!filePath || !this.sessions.has(filePath)) {
436
+ const sessionId = await this.getSessionForFile(filePath);
437
+ if (!filePath || !sessionId) {
411
438
  return {
412
439
  content: [
413
440
  {
@@ -417,7 +444,6 @@ class GraphClient {
417
444
  ],
418
445
  };
419
446
  }
420
- const sessionId = this.sessions.get(filePath);
421
447
  try {
422
448
  const accessToken = await this.authManager.getToken();
423
449
  const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`, {
@@ -429,7 +455,11 @@ class GraphClient {
429
455
  },
430
456
  });
431
457
  if (response.ok) {
432
- this.sessions.delete(filePath);
458
+ const accountId = await this.getCurrentAccountId();
459
+ if (accountId) {
460
+ const accountSessions = this.getAccountSessions(accountId);
461
+ accountSessions.delete(filePath);
462
+ }
433
463
  return {
434
464
  content: [
435
465
  {
@@ -458,9 +488,13 @@ class GraphClient {
458
488
  }
459
489
  async closeAllSessions() {
460
490
  const results = [];
461
- for (const [filePath] of this.sessions) {
462
- const result = await this.closeSession(filePath);
463
- results.push(result);
491
+ const accountId = await this.getCurrentAccountId();
492
+ if (accountId) {
493
+ const accountSessions = this.getAccountSessions(accountId);
494
+ for (const [filePath] of accountSessions) {
495
+ const result = await this.closeSession(filePath);
496
+ results.push(result);
497
+ }
464
498
  }
465
499
  return {
466
500
  content: [
package/dist/index.js CHANGED
@@ -40,6 +40,40 @@ async function main() {
40
40
  console.log(JSON.stringify({ message: 'Logged out successfully' }));
41
41
  process.exit(0);
42
42
  }
43
+ if (args.listAccounts) {
44
+ const accounts = await authManager.listAccounts();
45
+ const selectedAccountId = authManager.getSelectedAccountId();
46
+ const result = accounts.map(account => ({
47
+ id: account.homeAccountId,
48
+ username: account.username,
49
+ name: account.name,
50
+ selected: account.homeAccountId === selectedAccountId
51
+ }));
52
+ console.log(JSON.stringify({ accounts: result }));
53
+ process.exit(0);
54
+ }
55
+ if (args.selectAccount) {
56
+ const success = await authManager.selectAccount(args.selectAccount);
57
+ if (success) {
58
+ console.log(JSON.stringify({ message: `Selected account: ${args.selectAccount}` }));
59
+ }
60
+ else {
61
+ console.log(JSON.stringify({ error: `Account not found: ${args.selectAccount}` }));
62
+ process.exit(1);
63
+ }
64
+ process.exit(0);
65
+ }
66
+ if (args.removeAccount) {
67
+ const success = await authManager.removeAccount(args.removeAccount);
68
+ if (success) {
69
+ console.log(JSON.stringify({ message: `Removed account: ${args.removeAccount}` }));
70
+ }
71
+ else {
72
+ console.log(JSON.stringify({ error: `Account not found: ${args.removeAccount}` }));
73
+ process.exit(1);
74
+ }
75
+ process.exit(0);
76
+ }
43
77
  const server = new MicrosoftGraphServer(authManager, args);
44
78
  await server.initialize(version);
45
79
  await server.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Microsoft 365 MCP Server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",