@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 +37 -0
- package/dist/auth-tools.js +68 -60
- package/dist/auth.js +97 -19
- package/dist/endpoints.json +6 -0
- package/dist/generated/client.js +51 -1
- package/dist/graph-tools.js +39 -6
- package/dist/server.js +21 -2
- package/logs/mcp-server.log +10 -10
- package/package.json +1 -1
- package/src/endpoints.json +6 -0
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:
|
package/dist/auth-tools.js
CHANGED
|
@@ -83,63 +83,68 @@ function registerAuthTools(server, authManager) {
|
|
|
83
83
|
]
|
|
84
84
|
};
|
|
85
85
|
});
|
|
86
|
-
server.tool(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
135
|
+
account: z.string().describe("Email address or account ID of the account to select")
|
|
120
136
|
},
|
|
121
|
-
async ({
|
|
137
|
+
async ({ account }) => {
|
|
122
138
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
167
|
+
account: z.string().describe("Email address or account ID of the account to remove")
|
|
162
168
|
},
|
|
163
|
-
async ({
|
|
169
|
+
async ({ account }) => {
|
|
164
170
|
try {
|
|
165
|
-
const success = await authManager.removeAccount(
|
|
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: ${
|
|
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: `
|
|
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(
|
|
403
|
-
const
|
|
404
|
-
|
|
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} (${
|
|
408
|
+
logger.info(`Selected account: ${account.username} (${account.homeAccountId})`);
|
|
414
409
|
return true;
|
|
415
410
|
}
|
|
416
|
-
async removeAccount(
|
|
417
|
-
const
|
|
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 ===
|
|
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} (${
|
|
421
|
+
logger.info(`Removed account: ${account.username} (${account.homeAccountId})`);
|
|
432
422
|
return true;
|
|
433
423
|
} catch (error) {
|
|
434
|
-
logger.error(`Failed to remove account ${
|
|
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 {
|
package/dist/endpoints.json
CHANGED
|
@@ -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",
|
package/dist/generated/client.js
CHANGED
|
@@ -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
|
|
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",
|
package/dist/graph-tools.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/logs/mcp-server.log
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
2026-02-
|
|
2
|
-
2026-02-
|
|
3
|
-
2026-02-
|
|
4
|
-
2026-02-
|
|
5
|
-
2026-02-
|
|
6
|
-
2026-02-
|
|
7
|
-
2026-02-
|
|
8
|
-
2026-02-
|
|
9
|
-
2026-02-
|
|
10
|
-
2026-02-
|
|
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.
|
|
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",
|
package/src/endpoints.json
CHANGED
|
@@ -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",
|