@softeria/ms-365-mcp-server 0.41.0 → 0.43.0

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
@@ -431,6 +431,43 @@ This method:
431
431
  > **Authentication Tools**: In HTTP mode, login/logout tools are disabled by default since OAuth handles authentication.
432
432
  > Use `--enable-auth-tools` if you need them available.
433
433
 
434
+ ## Multi-Account Support
435
+
436
+ Use a single server instance to serve multiple Microsoft accounts. When more than one account is logged in, an `account` parameter is automatically injected into every tool, allowing you to specify which account to use per tool call.
437
+
438
+ **Login multiple accounts** (one-time per account):
439
+
440
+ ```bash
441
+ # Login first account (device code flow)
442
+ npx @softeria/ms-365-mcp-server --login
443
+ # Follow the device code prompt, sign in as personal@outlook.com
444
+
445
+ # Login second account
446
+ npx @softeria/ms-365-mcp-server --login
447
+ # Follow the device code prompt, sign in as work@company.com
448
+ ```
449
+
450
+ **List configured accounts:**
451
+
452
+ ```bash
453
+ npx @softeria/ms-365-mcp-server --list-accounts
454
+ ```
455
+
456
+ **Use in tool calls:** Pass `"account": "work@company.com"` in any tool request:
457
+
458
+ ```json
459
+ { "tool": "list-mail-messages", "arguments": { "account": "work@company.com" } }
460
+ ```
461
+
462
+ **Behavior:**
463
+
464
+ - With a **single account** configured, it auto-selects (no `account` parameter needed).
465
+ - With **multiple accounts** and no `account` parameter, the server uses the selected default or returns a helpful error listing available accounts.
466
+ - **100% backward compatible**: existing single-account setups work unchanged.
467
+ - The `account` parameter accepts email address (e.g. `user@outlook.com`) or MSAL `homeAccountId`.
468
+
469
+ > **For MCP multiplexers (Legate, Governor):** Multi-account mode replaces the N-process pattern. Instead of spawning one server per account, a single instance handles all accounts via the `account` parameter, reducing tool duplication from N×110 to 110.
470
+
434
471
  ## Tool Presets
435
472
 
436
473
  To reduce initial connection overhead, use preset tool categories instead of loading all 90+ tools:
@@ -83,63 +83,68 @@ function registerAuthTools(server, authManager) {
83
83
  ]
84
84
  };
85
85
  });
86
- server.tool("list-accounts", "List all available Microsoft accounts", {}, async () => {
87
- try {
88
- const accounts = await authManager.listAccounts();
89
- const selectedAccountId = authManager.getSelectedAccountId();
90
- const result = accounts.map((account) => ({
91
- id: account.homeAccountId,
92
- username: account.username,
93
- name: account.name,
94
- selected: account.homeAccountId === selectedAccountId
95
- }));
96
- return {
97
- content: [
98
- {
99
- type: "text",
100
- text: JSON.stringify({ accounts: result })
101
- }
102
- ]
103
- };
104
- } catch (error) {
105
- return {
106
- content: [
107
- {
108
- type: "text",
109
- text: JSON.stringify({ error: `Failed to list accounts: ${error.message}` })
110
- }
111
- ]
112
- };
86
+ server.tool(
87
+ "list-accounts",
88
+ "List all Microsoft accounts configured in this server. Use this to discover available account emails before making tool calls. Reflects accounts added mid-session via --login.",
89
+ {},
90
+ {
91
+ title: "list-accounts",
92
+ readOnlyHint: true,
93
+ openWorldHint: false
94
+ },
95
+ async () => {
96
+ try {
97
+ const accounts = await authManager.listAccounts();
98
+ const selectedAccountId = authManager.getSelectedAccountId();
99
+ const result = accounts.map((account) => ({
100
+ email: account.username || "unknown",
101
+ name: account.name,
102
+ isDefault: account.homeAccountId === selectedAccountId
103
+ }));
104
+ return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: JSON.stringify({
109
+ accounts: result,
110
+ count: result.length,
111
+ tip: "Pass the 'email' value as the 'account' parameter in any tool call to target a specific account."
112
+ })
113
+ }
114
+ ]
115
+ };
116
+ } catch (error) {
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: JSON.stringify({
122
+ error: `Failed to list accounts: ${error.message}`
123
+ })
124
+ }
125
+ ],
126
+ isError: true
127
+ };
128
+ }
113
129
  }
114
- });
130
+ );
115
131
  server.tool(
116
132
  "select-account",
117
- "Select a specific Microsoft account to use",
133
+ "Select a Microsoft account as the default. Accepts email address (e.g. user@outlook.com) or account ID. Use list-accounts to discover available accounts.",
118
134
  {
119
- accountId: z.string().describe("The account ID to select")
135
+ account: z.string().describe("Email address or account ID of the account to select")
120
136
  },
121
- async ({ accountId }) => {
137
+ async ({ account }) => {
122
138
  try {
123
- const success = await authManager.selectAccount(accountId);
124
- if (success) {
125
- return {
126
- content: [
127
- {
128
- type: "text",
129
- text: JSON.stringify({ message: `Selected account: ${accountId}` })
130
- }
131
- ]
132
- };
133
- } else {
134
- return {
135
- content: [
136
- {
137
- type: "text",
138
- text: JSON.stringify({ error: `Account not found: ${accountId}` })
139
- }
140
- ]
141
- };
142
- }
139
+ await authManager.selectAccount(account);
140
+ return {
141
+ content: [
142
+ {
143
+ type: "text",
144
+ text: JSON.stringify({ message: `Selected account: ${account}` })
145
+ }
146
+ ]
147
+ };
143
148
  } catch (error) {
144
149
  return {
145
150
  content: [
@@ -149,26 +154,27 @@ function registerAuthTools(server, authManager) {
149
154
  error: `Failed to select account: ${error.message}`
150
155
  })
151
156
  }
152
- ]
157
+ ],
158
+ isError: true
153
159
  };
154
160
  }
155
161
  }
156
162
  );
157
163
  server.tool(
158
164
  "remove-account",
159
- "Remove a Microsoft account from the cache",
165
+ "Remove a Microsoft account from the cache. Accepts email address (e.g. user@outlook.com) or account ID. Use list-accounts to discover available accounts.",
160
166
  {
161
- accountId: z.string().describe("The account ID to remove")
167
+ account: z.string().describe("Email address or account ID of the account to remove")
162
168
  },
163
- async ({ accountId }) => {
169
+ async ({ account }) => {
164
170
  try {
165
- const success = await authManager.removeAccount(accountId);
171
+ const success = await authManager.removeAccount(account);
166
172
  if (success) {
167
173
  return {
168
174
  content: [
169
175
  {
170
176
  type: "text",
171
- text: JSON.stringify({ message: `Removed account: ${accountId}` })
177
+ text: JSON.stringify({ message: `Removed account: ${account}` })
172
178
  }
173
179
  ]
174
180
  };
@@ -177,9 +183,10 @@ function registerAuthTools(server, authManager) {
177
183
  content: [
178
184
  {
179
185
  type: "text",
180
- text: JSON.stringify({ error: `Account not found: ${accountId}` })
186
+ text: JSON.stringify({ error: `Failed to remove account from cache: ${account}` })
181
187
  }
182
- ]
188
+ ],
189
+ isError: true
183
190
  };
184
191
  }
185
192
  } catch (error) {
@@ -191,7 +198,8 @@ function registerAuthTools(server, authManager) {
191
198
  error: `Failed to remove account: ${error.message}`
192
199
  })
193
200
  }
194
- ]
201
+ ],
202
+ isError: true
195
203
  };
196
204
  }
197
205
  }
package/dist/auth.js CHANGED
@@ -399,45 +399,123 @@ class AuthManager {
399
399
  async listAccounts() {
400
400
  return await this.msalApp.getTokenCache().getAllAccounts();
401
401
  }
402
- async selectAccount(accountId) {
403
- const accounts = await this.listAccounts();
404
- const account = accounts.find((acc) => acc.homeAccountId === accountId);
405
- if (!account) {
406
- logger.error(`Account with ID ${accountId} not found`);
407
- return false;
408
- }
409
- this.selectedAccountId = accountId;
402
+ async selectAccount(identifier) {
403
+ const account = await this.resolveAccount(identifier);
404
+ this.selectedAccountId = account.homeAccountId;
410
405
  await this.saveSelectedAccount();
411
406
  this.accessToken = null;
412
407
  this.tokenExpiry = null;
413
- logger.info(`Selected account: ${account.username} (${accountId})`);
408
+ logger.info(`Selected account: ${account.username} (${account.homeAccountId})`);
414
409
  return true;
415
410
  }
416
- async removeAccount(accountId) {
417
- const accounts = await this.listAccounts();
418
- const account = accounts.find((acc) => acc.homeAccountId === accountId);
419
- if (!account) {
420
- logger.error(`Account with ID ${accountId} not found`);
421
- return false;
422
- }
411
+ async removeAccount(identifier) {
412
+ const account = await this.resolveAccount(identifier);
423
413
  try {
424
414
  await this.msalApp.getTokenCache().removeAccount(account);
425
- if (this.selectedAccountId === accountId) {
415
+ if (this.selectedAccountId === account.homeAccountId) {
426
416
  this.selectedAccountId = null;
427
417
  await this.saveSelectedAccount();
428
418
  this.accessToken = null;
429
419
  this.tokenExpiry = null;
430
420
  }
431
- logger.info(`Removed account: ${account.username} (${accountId})`);
421
+ logger.info(`Removed account: ${account.username} (${account.homeAccountId})`);
432
422
  return true;
433
423
  } catch (error) {
434
- logger.error(`Failed to remove account ${accountId}: ${error.message}`);
424
+ logger.error(`Failed to remove account ${identifier}: ${error.message}`);
435
425
  return false;
436
426
  }
437
427
  }
438
428
  getSelectedAccountId() {
439
429
  return this.selectedAccountId;
440
430
  }
431
+ /**
432
+ * Returns true if auth is in OAuth/HTTP mode (token supplied via env or setOAuthToken).
433
+ * In this mode, account resolution should be skipped — the request context drives token selection.
434
+ */
435
+ isOAuthModeEnabled() {
436
+ return this.isOAuthMode;
437
+ }
438
+ /**
439
+ * Resolves an account by identifier (email or homeAccountId).
440
+ * Resolution: username match (case-insensitive) → homeAccountId match → throw.
441
+ */
442
+ async resolveAccount(identifier) {
443
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
444
+ if (accounts.length === 0) {
445
+ throw new Error("No accounts found. Please login first.");
446
+ }
447
+ const lowerIdentifier = identifier.toLowerCase();
448
+ let account = accounts.find((a) => a.username?.toLowerCase() === lowerIdentifier) ?? null;
449
+ if (!account) {
450
+ account = accounts.find((a) => a.homeAccountId === identifier) ?? null;
451
+ }
452
+ if (!account) {
453
+ const availableAccounts = accounts.map((a) => a.username || a.name || "unknown").join(", ");
454
+ throw new Error(
455
+ `Account '${identifier}' not found. Available accounts: ${availableAccounts}`
456
+ );
457
+ }
458
+ return account;
459
+ }
460
+ /**
461
+ * Returns true if the MSAL cache contains more than one account.
462
+ * Used to decide whether to inject the `account` parameter into tool schemas.
463
+ */
464
+ async isMultiAccount() {
465
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
466
+ return accounts.length > 1;
467
+ }
468
+ /**
469
+ * Acquires a token for a specific account identified by username (email) or homeAccountId,
470
+ * WITHOUT changing the persisted selectedAccountId.
471
+ *
472
+ * Resolution order:
473
+ * 1. Exact match on username (case-insensitive)
474
+ * 2. Exact match on homeAccountId
475
+ * 3. If identifier is empty/undefined AND only 1 account exists → auto-select
476
+ * 4. If identifier is empty/undefined AND multiple accounts → use selectedAccountId or throw
477
+ *
478
+ * @returns The access token string.
479
+ */
480
+ async getTokenForAccount(identifier) {
481
+ if (this.isOAuthMode && this.oauthToken) {
482
+ return this.oauthToken;
483
+ }
484
+ let targetAccount = null;
485
+ if (identifier) {
486
+ targetAccount = await this.resolveAccount(identifier);
487
+ } else {
488
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
489
+ if (accounts.length === 0) {
490
+ throw new Error("No accounts found. Please login first.");
491
+ }
492
+ if (accounts.length === 1) {
493
+ targetAccount = accounts[0];
494
+ } else {
495
+ if (this.selectedAccountId) {
496
+ targetAccount = accounts.find((a) => a.homeAccountId === this.selectedAccountId) ?? null;
497
+ }
498
+ if (!targetAccount) {
499
+ const availableAccounts = accounts.map((a) => a.username || a.name || "unknown").join(", ");
500
+ throw new Error(
501
+ `Multiple accounts configured but no 'account' parameter provided and no default selected. Available accounts: ${availableAccounts}. Pass account="<email>" in your tool call or use select-account to set a default.`
502
+ );
503
+ }
504
+ }
505
+ }
506
+ const silentRequest = {
507
+ account: targetAccount,
508
+ scopes: this.scopes
509
+ };
510
+ try {
511
+ const response = await this.msalApp.acquireTokenSilent(silentRequest);
512
+ return response.accessToken;
513
+ } catch {
514
+ throw new Error(
515
+ `Failed to acquire token for account '${targetAccount.username || targetAccount.name || "unknown"}'. The token may have expired. Please re-login with: --login`
516
+ );
517
+ }
518
+ }
441
519
  }
442
520
  var auth_default = AuthManager;
443
521
  export {
@@ -586,6 +586,12 @@
586
586
  "toolName": "reply-to-channel-message",
587
587
  "workScopes": ["ChannelMessage.Send"]
588
588
  },
589
+ {
590
+ "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
591
+ "method": "get",
592
+ "toolName": "list-channel-message-replies",
593
+ "workScopes": ["ChannelMessage.Read.All"]
594
+ },
589
595
  {
590
596
  "pathPattern": "/teams/{team-id}/members",
591
597
  "method": "get",
@@ -2268,7 +2268,7 @@ const microsoft_graph_team = z.lazy(
2268
2268
  ).nullish(),
2269
2269
  allChannels: z.array(microsoft_graph_channel).describe("List of channels either hosted in or shared with the team (incoming channels).").optional(),
2270
2270
  channels: z.array(microsoft_graph_channel).describe("The collection of channels and messages associated with the team.").optional(),
2271
- group: microsoft_graph_group.describe("[Note: Simplified from 71 properties to 25 most common ones]").optional(),
2271
+ group: microsoft_graph_group.describe("[Note: Simplified from 73 properties to 25 most common ones]").optional(),
2272
2272
  incomingChannels: z.array(microsoft_graph_channel).describe("List of channels shared with the team.").optional(),
2273
2273
  installedApps: z.array(microsoft_graph_teamsAppInstallation).describe("The apps installed in this team.").optional(),
2274
2274
  members: z.array(microsoft_graph_conversationMember).describe("Members and owners of the team.").optional(),
@@ -7239,6 +7239,56 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
7239
7239
  ],
7240
7240
  response: z.void()
7241
7241
  },
7242
+ {
7243
+ method: "get",
7244
+ path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/replies",
7245
+ alias: "list-channel-message-replies",
7246
+ description: `List all the replies to a message in a channel of a team. This method lists only the replies of the specified message, if any. To get the message itself, call get channel message.`,
7247
+ requestFormat: "json",
7248
+ parameters: [
7249
+ {
7250
+ name: "$top",
7251
+ type: "Query",
7252
+ schema: z.number().int().gte(0).describe("Show only the first n items").optional()
7253
+ },
7254
+ {
7255
+ name: "$skip",
7256
+ type: "Query",
7257
+ schema: z.number().int().gte(0).describe("Skip the first n items").optional()
7258
+ },
7259
+ {
7260
+ name: "$search",
7261
+ type: "Query",
7262
+ schema: z.string().describe("Search items by search phrases").optional()
7263
+ },
7264
+ {
7265
+ name: "$filter",
7266
+ type: "Query",
7267
+ schema: z.string().describe("Filter items by property values").optional()
7268
+ },
7269
+ {
7270
+ name: "$count",
7271
+ type: "Query",
7272
+ schema: z.boolean().describe("Include count of items").optional()
7273
+ },
7274
+ {
7275
+ name: "$orderby",
7276
+ type: "Query",
7277
+ schema: z.array(z.string()).describe("Order items by property values").optional()
7278
+ },
7279
+ {
7280
+ name: "$select",
7281
+ type: "Query",
7282
+ schema: z.array(z.string()).describe("Select properties to be returned").optional()
7283
+ },
7284
+ {
7285
+ name: "$expand",
7286
+ type: "Query",
7287
+ schema: z.array(z.string()).describe("Expand related entities").optional()
7288
+ }
7289
+ ],
7290
+ response: z.void()
7291
+ },
7242
7292
  {
7243
7293
  method: "post",
7244
7294
  path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/replies",
@@ -10,9 +10,26 @@ const __dirname = path.dirname(__filename);
10
10
  const endpointsData = JSON.parse(
11
11
  readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
12
12
  );
13
- async function executeGraphTool(tool, config, graphClient, params) {
13
+ async function executeGraphTool(tool, config, graphClient, params, authManager) {
14
14
  logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
15
15
  try {
16
+ let accountAccessToken;
17
+ if (authManager && !authManager.isOAuthModeEnabled()) {
18
+ const accountParam = params.account;
19
+ try {
20
+ accountAccessToken = await authManager.getTokenForAccount(accountParam);
21
+ } catch (err) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify({ error: err.message })
27
+ }
28
+ ],
29
+ isError: true
30
+ };
31
+ }
32
+ }
16
33
  const parameterDefinitions = tool.parameters || [];
17
34
  let path2 = tool.path;
18
35
  const queryParams = {};
@@ -20,6 +37,7 @@ async function executeGraphTool(tool, config, graphClient, params) {
20
37
  let body = null;
21
38
  for (const [paramName, paramValue] of Object.entries(params)) {
22
39
  if ([
40
+ "account",
23
41
  "fetchAllPages",
24
42
  "includeHeaders",
25
43
  "excludeResponse",
@@ -141,7 +159,13 @@ async function executeGraphTool(tool, config, graphClient, params) {
141
159
  if (params.excludeResponse === true) {
142
160
  options.excludeResponse = true;
143
161
  }
144
- logger.info(`Making graph request to ${path2} with options: ${JSON.stringify(options)}`);
162
+ if (accountAccessToken) {
163
+ options.accessToken = accountAccessToken;
164
+ }
165
+ const { accessToken: _redacted, ...safeOptions } = options;
166
+ logger.info(
167
+ `Making graph request to ${path2} with options: ${JSON.stringify(safeOptions)}${_redacted ? " [accessToken=REDACTED]" : ""}`
168
+ );
145
169
  let response = await graphClient.graphRequest(path2, options);
146
170
  const fetchAllPages = params.fetchAllPages === true;
147
171
  if (fetchAllPages && response?.content?.[0]?.text) {
@@ -226,7 +250,7 @@ async function executeGraphTool(tool, config, graphClient, params) {
226
250
  };
227
251
  }
228
252
  }
229
- function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false) {
253
+ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false, authManager, multiAccount = false, accountNames = []) {
230
254
  let enabledToolsRegex;
231
255
  if (enabledToolsPattern) {
232
256
  try {
@@ -265,6 +289,12 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
265
289
  if (tool.method.toUpperCase() === "GET" && tool.path.includes("/")) {
266
290
  paramSchema["fetchAllPages"] = z.boolean().describe("Automatically fetch all pages of results").optional();
267
291
  }
292
+ if (multiAccount) {
293
+ const accountHint = accountNames.length > 0 ? `Known accounts: ${accountNames.join(", ")}. ` : "";
294
+ paramSchema["account"] = z.string().describe(
295
+ `${accountHint}Microsoft account email to use for this request. Required when multiple accounts are configured. Use the list-accounts tool to discover all currently available accounts.`
296
+ ).optional();
297
+ }
268
298
  paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
269
299
  paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
270
300
  if (endpointConfig?.supportsTimezone) {
@@ -295,7 +325,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
295
325
  openWorldHint: true
296
326
  // All tools call Microsoft Graph API
297
327
  },
298
- async (params) => executeGraphTool(tool, endpointConfig, graphClient, params)
328
+ async (params) => executeGraphTool(tool, endpointConfig, graphClient, params, authManager)
299
329
  );
300
330
  registeredCount++;
301
331
  } catch (error) {
@@ -303,6 +333,9 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
303
333
  failedCount++;
304
334
  }
305
335
  }
336
+ if (multiAccount) {
337
+ logger.info('Multi-account mode: "account" parameter injected into all tool schemas');
338
+ }
306
339
  logger.info(
307
340
  `Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped, ${failedCount} failed`
308
341
  );
@@ -322,7 +355,7 @@ function buildToolsRegistry(readOnly, orgMode) {
322
355
  }
323
356
  return toolsMap;
324
357
  }
325
- function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false) {
358
+ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager, _multiAccount = false) {
326
359
  const toolsRegistry = buildToolsRegistry(readOnly, orgMode);
327
360
  logger.info(`Discovery mode: ${toolsRegistry.size} tools available in registry`);
328
361
  server.tool(
@@ -414,7 +447,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
414
447
  isError: true
415
448
  };
416
449
  }
417
- return executeGraphTool(toolData.tool, toolData.config, graphClient, parameters);
450
+ return executeGraphTool(toolData.tool, toolData.config, graphClient, parameters, authManager);
418
451
  }
419
452
  );
420
453
  }
package/dist/server.js CHANGED
@@ -34,6 +34,8 @@ function parseHttpOption(httpOption) {
34
34
  class MicrosoftGraphServer {
35
35
  constructor(authManager, options = {}) {
36
36
  this.version = "0.0.0";
37
+ this.multiAccount = false;
38
+ this.accountNames = [];
37
39
  this.authManager = authManager;
38
40
  this.options = options;
39
41
  this.graphClient = null;
@@ -54,7 +56,9 @@ class MicrosoftGraphServer {
54
56
  server,
55
57
  this.graphClient,
56
58
  this.options.readOnly,
57
- this.options.orgMode
59
+ this.options.orgMode,
60
+ this.authManager,
61
+ this.multiAccount
58
62
  );
59
63
  } else {
60
64
  registerGraphTools(
@@ -62,7 +66,10 @@ class MicrosoftGraphServer {
62
66
  this.graphClient,
63
67
  this.options.readOnly,
64
68
  this.options.enabledTools,
65
- this.options.orgMode
69
+ this.options.orgMode,
70
+ this.authManager,
71
+ this.multiAccount,
72
+ this.accountNames
66
73
  );
67
74
  }
68
75
  return server;
@@ -70,6 +77,18 @@ class MicrosoftGraphServer {
70
77
  async initialize(version) {
71
78
  this.secrets = await getSecrets();
72
79
  this.version = version;
80
+ try {
81
+ this.multiAccount = await this.authManager.isMultiAccount();
82
+ if (this.multiAccount) {
83
+ const accounts = await this.authManager.listAccounts();
84
+ this.accountNames = accounts.map((a) => a.username).filter((u) => !!u);
85
+ logger.info(
86
+ `Multi-account mode detected (${this.accountNames.length} accounts): "account" parameter will be injected into all tool schemas`
87
+ );
88
+ }
89
+ } catch (err) {
90
+ logger.warn(`Failed to detect multi-account mode: ${err.message}`);
91
+ }
73
92
  const outputFormat = this.options.toon ? "toon" : "json";
74
93
  this.graphClient = new GraphClient(this.authManager, this.secrets, outputFormat);
75
94
  if (!this.options.http) {
@@ -1,10 +1,10 @@
1
- 2026-02-15 08:15:07 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
2
- 2026-02-15 08:15:07 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
3
- 2026-02-15 08:15:07 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
4
- 2026-02-15 08:15:07 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
5
- 2026-02-15 08:15:07 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
6
- 2026-02-15 08:15:08 INFO: Using environment variables for secrets
7
- 2026-02-15 08:15:08 INFO: Using environment variables for secrets
8
- 2026-02-15 08:15:08 INFO: Using environment variables for secrets
9
- 2026-02-15 08:15:08 INFO: Using environment variables for secrets
10
- 2026-02-15 08:15:08 INFO: Using environment variables for secrets
1
+ 2026-02-27 13:54:04 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
2
+ 2026-02-27 13:54:04 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
3
+ 2026-02-27 13:54:04 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
4
+ 2026-02-27 13:54:04 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
5
+ 2026-02-27 13:54:04 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
6
+ 2026-02-27 13:54:05 INFO: Using environment variables for secrets
7
+ 2026-02-27 13:54:05 INFO: Using environment variables for secrets
8
+ 2026-02-27 13:54:05 INFO: Using environment variables for secrets
9
+ 2026-02-27 13:54:05 INFO: Using environment variables for secrets
10
+ 2026-02-27 13:54:05 INFO: Using environment variables for secrets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -586,6 +586,12 @@
586
586
  "toolName": "reply-to-channel-message",
587
587
  "workScopes": ["ChannelMessage.Send"]
588
588
  },
589
+ {
590
+ "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
591
+ "method": "get",
592
+ "toolName": "list-channel-message-replies",
593
+ "workScopes": ["ChannelMessage.Read.All"]
594
+ },
589
595
  {
590
596
  "pathPattern": "/teams/{team-id}/members",
591
597
  "method": "get",