@softeria/ms-365-mcp-server 0.109.0 → 0.110.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 +30 -0
- package/dist/auth-tools.js +11 -2
- package/dist/auth.js +144 -4
- package/dist/cli.js +33 -1
- package/dist/index.js +16 -1
- package/dist/startup-pinning.js +40 -0
- package/docs/deployment.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -467,6 +467,32 @@ npx @softeria/ms-365-mcp-server --list-accounts
|
|
|
467
467
|
- **100% backward compatible**: existing single-account setups work unchanged.
|
|
468
468
|
- The `account` parameter accepts email address (e.g. `user@outlook.com`) or MSAL `homeAccountId`.
|
|
469
469
|
|
|
470
|
+
### Strict Account Pinning
|
|
471
|
+
|
|
472
|
+
Headless stdio deployments can pin the local MSAL cache to one expected Microsoft account:
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
# Username matching is case-insensitive
|
|
476
|
+
MS365_MCP_EXPECTED_USERNAME=work@company.com npx @softeria/ms-365-mcp-server --login
|
|
477
|
+
|
|
478
|
+
# Or pin the exact MSAL homeAccountId shown by --list-accounts
|
|
479
|
+
npx @softeria/ms-365-mcp-server --expected-home-account-id <homeAccountId> --login
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Use `--list-accounts` to discover `homeAccountId` values. The MCP `list-accounts` tool intentionally hides account IDs, so use the CLI for exact ID pinning.
|
|
483
|
+
|
|
484
|
+
Pinning is opt-in and local-MSAL only:
|
|
485
|
+
|
|
486
|
+
- CLI values (`--expected-username`, `--expected-home-account-id`) take precedence over `MS365_MCP_EXPECTED_USERNAME` and `MS365_MCP_EXPECTED_HOME_ACCOUNT_ID`.
|
|
487
|
+
- Supplying an empty pin value fails at startup instead of being ignored.
|
|
488
|
+
- Username pins are compared case-insensitively; `homeAccountId` pins are exact.
|
|
489
|
+
- If both pins are set, they must resolve to the same cached account.
|
|
490
|
+
- Local stdio startup fails fast when the expected account is not in the token cache. Bootstrap by setting the pin, running `--login`, then starting the headless server.
|
|
491
|
+
- Device-code and browser logins reject a missing or mismatched account before persisting the selected account or token cache.
|
|
492
|
+
- Pinning collapses the effective MCP mode to single-account: the server does not advertise an `account` parameter and MCP instructions do not suggest account switching.
|
|
493
|
+
- `--http`, `--obo`, and `MS365_MCP_OAUTH_TOKEN` use request-provided tokens for Graph calls, so account pins are warning-only in those modes. If HTTP auth tools are enabled, the pin still applies to those local MSAL helper flows.
|
|
494
|
+
- `--logout` clears all cached accounts, including the pinned account. For surgical cleanup, prefer `--remove-account <id>`.
|
|
495
|
+
|
|
470
496
|
> **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.
|
|
471
497
|
|
|
472
498
|
## Tool Presets
|
|
@@ -504,6 +530,8 @@ The following options can be used when running ms-365-mcp-server directly from t
|
|
|
504
530
|
--force-work-scopes Backwards compatibility alias for --org-mode (deprecated)
|
|
505
531
|
--cloud <type> Microsoft cloud environment: global (default) or china (21Vianet)
|
|
506
532
|
--allowed-scopes <scopes> Limit exposed tools to Graph scopes covered by this allowlist
|
|
533
|
+
--expected-username <username> Require local MSAL auth to use this account username
|
|
534
|
+
--expected-home-account-id <id> Require local MSAL auth to use this exact homeAccountId
|
|
507
535
|
```
|
|
508
536
|
|
|
509
537
|
### Server Options
|
|
@@ -543,6 +571,8 @@ Environment variables:
|
|
|
543
571
|
- `MS365_MCP_KEYVAULT_URL`: Azure Key Vault URL for secrets management (see Azure Key Vault section)
|
|
544
572
|
- `MS365_MCP_TOKEN_CACHE_PATH`: Custom file path for MSAL token cache (see Token Storage below)
|
|
545
573
|
- `MS365_MCP_SELECTED_ACCOUNT_PATH`: Custom file path for selected account metadata (see Token Storage below)
|
|
574
|
+
- `MS365_MCP_EXPECTED_USERNAME`: Require local MSAL auth to use this Microsoft account username (case-insensitive; CLI flag takes precedence)
|
|
575
|
+
- `MS365_MCP_EXPECTED_HOME_ACCOUNT_ID`: Require local MSAL auth to use this exact MSAL homeAccountId (CLI flag takes precedence)
|
|
546
576
|
|
|
547
577
|
## Token Storage
|
|
548
578
|
|
package/dist/auth-tools.js
CHANGED
|
@@ -89,7 +89,15 @@ function registerAuthTools(server, authManager) {
|
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
91
|
server.tool("verify-login", "Check current Microsoft authentication status", {}, async () => {
|
|
92
|
-
|
|
92
|
+
let testResult;
|
|
93
|
+
try {
|
|
94
|
+
testResult = await authManager.testLogin();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
testResult = {
|
|
97
|
+
success: false,
|
|
98
|
+
message: `Login failed: ${error.message}`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
93
101
|
return {
|
|
94
102
|
content: [
|
|
95
103
|
{
|
|
@@ -112,6 +120,7 @@ function registerAuthTools(server, authManager) {
|
|
|
112
120
|
try {
|
|
113
121
|
const accounts = await authManager.listAccounts();
|
|
114
122
|
const selectedAccountId = authManager.getSelectedAccountId();
|
|
123
|
+
const pinnedMode = authManager.hasExpectedAccount();
|
|
115
124
|
const result = accounts.map((account) => ({
|
|
116
125
|
email: account.username || "unknown",
|
|
117
126
|
name: account.name,
|
|
@@ -124,7 +133,7 @@ function registerAuthTools(server, authManager) {
|
|
|
124
133
|
text: JSON.stringify({
|
|
125
134
|
accounts: result,
|
|
126
135
|
count: result.length,
|
|
127
|
-
tip: "Pass the 'email' value as the 'account' parameter in any tool call to target a specific account."
|
|
136
|
+
tip: pinnedMode ? "Expected account pinning is configured; account parameters are disabled." : "Pass the 'email' value as the 'account' parameter in any tool call to target a specific account."
|
|
128
137
|
})
|
|
129
138
|
}
|
|
130
139
|
]
|
package/dist/auth.js
CHANGED
|
@@ -288,7 +288,7 @@ function buildScopeDiagnostics(toolScopes, allowedScopesInput) {
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
class AuthManager {
|
|
291
|
-
constructor(config, scopes = []) {
|
|
291
|
+
constructor(config, scopes = [], expectedAccount) {
|
|
292
292
|
logger.info(`And scopes are ${scopes.join(", ")}`, scopes);
|
|
293
293
|
this.config = config;
|
|
294
294
|
this.scopes = scopes;
|
|
@@ -297,6 +297,10 @@ class AuthManager {
|
|
|
297
297
|
this.tokenExpiry = null;
|
|
298
298
|
this.selectedAccountId = null;
|
|
299
299
|
this.useInteractiveAuth = false;
|
|
300
|
+
this.expectedUsername = this.normalizeExpectedUsername(expectedAccount?.expectedUsername);
|
|
301
|
+
this.expectedHomeAccountId = this.normalizeExpectedHomeAccountId(
|
|
302
|
+
expectedAccount?.expectedHomeAccountId
|
|
303
|
+
);
|
|
300
304
|
const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
|
|
301
305
|
this.oauthToken = oauthTokenFromEnv ?? null;
|
|
302
306
|
this.isOAuthMode = oauthTokenFromEnv != null;
|
|
@@ -305,10 +309,10 @@ class AuthManager {
|
|
|
305
309
|
* Creates an AuthManager instance with secrets loaded from the configured provider.
|
|
306
310
|
* Uses Key Vault if MS365_MCP_KEYVAULT_URL is set, otherwise environment variables.
|
|
307
311
|
*/
|
|
308
|
-
static async create(scopes = []) {
|
|
312
|
+
static async create(scopes = [], expectedAccount) {
|
|
309
313
|
const secrets = await getSecrets();
|
|
310
314
|
const config = createMsalConfig(secrets);
|
|
311
|
-
return new AuthManager(config, scopes);
|
|
315
|
+
return new AuthManager(config, scopes, expectedAccount);
|
|
312
316
|
}
|
|
313
317
|
async loadTokenCache() {
|
|
314
318
|
try {
|
|
@@ -411,6 +415,118 @@ class AuthManager {
|
|
|
411
415
|
logger.error(`Error saving selected account: ${error.message}`);
|
|
412
416
|
}
|
|
413
417
|
}
|
|
418
|
+
normalizeExpectedUsername(value) {
|
|
419
|
+
if (value === void 0) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const trimmed = value.trim();
|
|
423
|
+
if (trimmed === "") {
|
|
424
|
+
throw new Error("Expected Microsoft account username was provided but is empty.");
|
|
425
|
+
}
|
|
426
|
+
return trimmed.toLowerCase();
|
|
427
|
+
}
|
|
428
|
+
normalizeExpectedHomeAccountId(value) {
|
|
429
|
+
if (value === void 0) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
const trimmed = value.trim();
|
|
433
|
+
if (trimmed === "") {
|
|
434
|
+
throw new Error("Expected Microsoft account homeAccountId was provided but is empty.");
|
|
435
|
+
}
|
|
436
|
+
return trimmed;
|
|
437
|
+
}
|
|
438
|
+
hasExpectedAccount() {
|
|
439
|
+
return this.expectedUsername !== null || this.expectedHomeAccountId !== null;
|
|
440
|
+
}
|
|
441
|
+
expectedAccountLabel() {
|
|
442
|
+
const parts = [];
|
|
443
|
+
if (this.expectedUsername) {
|
|
444
|
+
parts.push(`username ${this.expectedUsername}`);
|
|
445
|
+
}
|
|
446
|
+
if (this.expectedHomeAccountId) {
|
|
447
|
+
parts.push(`homeAccountId ${this.expectedHomeAccountId}`);
|
|
448
|
+
}
|
|
449
|
+
return parts.join(" and ");
|
|
450
|
+
}
|
|
451
|
+
describeAccount(account) {
|
|
452
|
+
return account?.username || account?.name || "unknown";
|
|
453
|
+
}
|
|
454
|
+
describeCachedAccounts(accounts) {
|
|
455
|
+
if (accounts.length === 0) {
|
|
456
|
+
return "none";
|
|
457
|
+
}
|
|
458
|
+
return accounts.map((account) => this.describeAccount(account)).join(", ");
|
|
459
|
+
}
|
|
460
|
+
accountMatchesExpected(account) {
|
|
461
|
+
if (!this.hasExpectedAccount() || !account) {
|
|
462
|
+
return !this.hasExpectedAccount();
|
|
463
|
+
}
|
|
464
|
+
if (this.expectedUsername && account.username?.toLowerCase() !== this.expectedUsername) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (this.expectedHomeAccountId && account.homeAccountId !== this.expectedHomeAccountId) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
buildExpectedAccountMissingError(accounts) {
|
|
473
|
+
return new Error(
|
|
474
|
+
`Expected Microsoft account '${this.expectedAccountLabel()}' not found in token cache. Cached accounts: ${this.describeCachedAccounts(accounts)}. Run --login after configuring the expected account, or use --select-account to recover.`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
resolveExpectedAccountFromAccounts(accounts) {
|
|
478
|
+
if (!this.hasExpectedAccount()) {
|
|
479
|
+
throw new Error("No expected Microsoft account is configured.");
|
|
480
|
+
}
|
|
481
|
+
const usernameMatch = this.expectedUsername ? accounts.find((account) => account.username?.toLowerCase() === this.expectedUsername) : void 0;
|
|
482
|
+
const homeAccountIdMatch = this.expectedHomeAccountId ? accounts.find((account) => account.homeAccountId === this.expectedHomeAccountId) : void 0;
|
|
483
|
+
if (this.expectedUsername && this.expectedHomeAccountId) {
|
|
484
|
+
if (!usernameMatch || !homeAccountIdMatch) {
|
|
485
|
+
throw this.buildExpectedAccountMissingError(accounts);
|
|
486
|
+
}
|
|
487
|
+
if (usernameMatch.homeAccountId !== homeAccountIdMatch.homeAccountId) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Expected Microsoft account pins conflict: username ${this.expectedUsername} matched ${this.describeAccount(usernameMatch)}, but homeAccountId ${this.expectedHomeAccountId} matched ${this.describeAccount(homeAccountIdMatch)}.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return usernameMatch;
|
|
493
|
+
}
|
|
494
|
+
const expectedAccount = usernameMatch ?? homeAccountIdMatch;
|
|
495
|
+
if (!expectedAccount) {
|
|
496
|
+
throw this.buildExpectedAccountMissingError(accounts);
|
|
497
|
+
}
|
|
498
|
+
return expectedAccount;
|
|
499
|
+
}
|
|
500
|
+
async assertExpectedAccountAvailable() {
|
|
501
|
+
if (!this.hasExpectedAccount()) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
505
|
+
this.resolveExpectedAccountFromAccounts(accounts);
|
|
506
|
+
}
|
|
507
|
+
async rejectUnexpectedLoginAccount(account) {
|
|
508
|
+
if (!this.hasExpectedAccount()) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (this.accountMatchesExpected(account)) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.accessToken = null;
|
|
515
|
+
this.tokenExpiry = null;
|
|
516
|
+
if (account) {
|
|
517
|
+
try {
|
|
518
|
+
await this.msalApp.getTokenCache().removeAccount(account);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
logger.warn(`Failed to remove unexpected account from cache: ${error.message}`);
|
|
521
|
+
}
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Authenticated Microsoft account '${this.describeAccount(account)}' does not match expected Microsoft account '${this.expectedAccountLabel()}'. Login was not persisted.`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Microsoft login did not return an account. Expected Microsoft account '${this.expectedAccountLabel()}'. Login was not persisted.`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
414
530
|
async setOAuthToken(token) {
|
|
415
531
|
this.oauthToken = token;
|
|
416
532
|
this.isOAuthMode = true;
|
|
@@ -443,6 +559,9 @@ class AuthManager {
|
|
|
443
559
|
}
|
|
444
560
|
async getCurrentAccount() {
|
|
445
561
|
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
562
|
+
if (this.hasExpectedAccount()) {
|
|
563
|
+
return this.resolveExpectedAccountFromAccounts(accounts);
|
|
564
|
+
}
|
|
446
565
|
if (accounts.length === 0) {
|
|
447
566
|
return null;
|
|
448
567
|
}
|
|
@@ -480,6 +599,7 @@ class AuthManager {
|
|
|
480
599
|
logger.info("Device code login successful");
|
|
481
600
|
this.accessToken = response?.accessToken || null;
|
|
482
601
|
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
602
|
+
await this.rejectUnexpectedLoginAccount(response?.account);
|
|
483
603
|
if (!this.selectedAccountId && response?.account) {
|
|
484
604
|
this.selectedAccountId = response.account.homeAccountId;
|
|
485
605
|
await this.saveSelectedAccount();
|
|
@@ -521,6 +641,7 @@ class AuthManager {
|
|
|
521
641
|
logger.info("Interactive browser login successful");
|
|
522
642
|
this.accessToken = response?.accessToken || null;
|
|
523
643
|
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
644
|
+
await this.rejectUnexpectedLoginAccount(response?.account);
|
|
524
645
|
if (!this.selectedAccountId && response?.account) {
|
|
525
646
|
this.selectedAccountId = response.account.homeAccountId;
|
|
526
647
|
await this.saveSelectedAccount();
|
|
@@ -625,6 +746,11 @@ class AuthManager {
|
|
|
625
746
|
}
|
|
626
747
|
async selectAccount(identifier) {
|
|
627
748
|
const account = await this.resolveAccount(identifier);
|
|
749
|
+
if (this.hasExpectedAccount() && !this.accountMatchesExpected(account)) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`Account '${identifier}' does not match expected Microsoft account '${this.expectedAccountLabel()}'.`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
628
754
|
this.selectedAccountId = account.homeAccountId;
|
|
629
755
|
await this.saveSelectedAccount();
|
|
630
756
|
this.accessToken = null;
|
|
@@ -686,6 +812,9 @@ class AuthManager {
|
|
|
686
812
|
* Used to decide whether to inject the `account` parameter into tool schemas.
|
|
687
813
|
*/
|
|
688
814
|
async isMultiAccount() {
|
|
815
|
+
if (this.hasExpectedAccount()) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
689
818
|
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
690
819
|
return accounts.length > 1;
|
|
691
820
|
}
|
|
@@ -706,7 +835,18 @@ class AuthManager {
|
|
|
706
835
|
return this.oauthToken;
|
|
707
836
|
}
|
|
708
837
|
let targetAccount = null;
|
|
709
|
-
if (
|
|
838
|
+
if (this.hasExpectedAccount()) {
|
|
839
|
+
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
840
|
+
targetAccount = this.resolveExpectedAccountFromAccounts(accounts);
|
|
841
|
+
if (identifier) {
|
|
842
|
+
const requestedAccount = await this.resolveAccount(identifier);
|
|
843
|
+
if (requestedAccount.homeAccountId !== targetAccount.homeAccountId) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`Account '${identifier}' does not match expected Microsoft account '${this.expectedAccountLabel()}'.`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} else if (identifier) {
|
|
710
850
|
targetAccount = await this.resolveAccount(identifier);
|
|
711
851
|
} else {
|
|
712
852
|
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,13 @@ const packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
|
8
8
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
9
9
|
const version = packageJson.version;
|
|
10
10
|
const program = new Command();
|
|
11
|
-
program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").version(version).option("-v", "Enable verbose logging").option("--login", "Login to Microsoft account").option("--logout", "Log out and clear saved credentials").option("--verify-login", "Verify login without starting the server").option("--list-accounts", "List all cached accounts").option("--select-account <accountId>", "Select a specific account by ID").option("--remove-account <accountId>", "Remove a specific account by ID").option(
|
|
11
|
+
program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").version(version).option("-v", "Enable verbose logging").option("--login", "Login to Microsoft account").option("--logout", "Log out and clear saved credentials").option("--verify-login", "Verify login without starting the server").option("--list-accounts", "List all cached accounts").option("--select-account <accountId>", "Select a specific account by ID").option("--remove-account <accountId>", "Remove a specific account by ID").option(
|
|
12
|
+
"--expected-username <username>",
|
|
13
|
+
"Require local MSAL authentication to use this Microsoft account username"
|
|
14
|
+
).option(
|
|
15
|
+
"--expected-home-account-id <id>",
|
|
16
|
+
"Require local MSAL authentication to use this exact MSAL homeAccountId"
|
|
17
|
+
).option("--read-only", "Start server in read-only mode, disabling write operations").option(
|
|
12
18
|
"--http [address]",
|
|
13
19
|
'Use Streamable HTTP transport instead of stdio. Format: [host:]port (e.g., "localhost:3000", ":3000", "3000"). Default: all interfaces on port 3000'
|
|
14
20
|
).option(
|
|
@@ -85,6 +91,32 @@ function parseArgs() {
|
|
|
85
91
|
);
|
|
86
92
|
process.exit(1);
|
|
87
93
|
}
|
|
94
|
+
if (options.expectedUsername === void 0 && process.env.MS365_MCP_EXPECTED_USERNAME !== void 0) {
|
|
95
|
+
options.expectedUsername = process.env.MS365_MCP_EXPECTED_USERNAME;
|
|
96
|
+
}
|
|
97
|
+
if (options.expectedHomeAccountId === void 0 && process.env.MS365_MCP_EXPECTED_HOME_ACCOUNT_ID !== void 0) {
|
|
98
|
+
options.expectedHomeAccountId = process.env.MS365_MCP_EXPECTED_HOME_ACCOUNT_ID;
|
|
99
|
+
}
|
|
100
|
+
if (options.expectedUsername !== void 0) {
|
|
101
|
+
const expectedUsername = String(options.expectedUsername).trim();
|
|
102
|
+
if (expectedUsername === "") {
|
|
103
|
+
console.error(
|
|
104
|
+
"Error: --expected-username / MS365_MCP_EXPECTED_USERNAME was provided but is empty. Provide a Microsoft account username, or omit it to allow any cached account."
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
options.expectedUsername = expectedUsername;
|
|
109
|
+
}
|
|
110
|
+
if (options.expectedHomeAccountId !== void 0) {
|
|
111
|
+
const expectedHomeAccountId = String(options.expectedHomeAccountId).trim();
|
|
112
|
+
if (expectedHomeAccountId === "") {
|
|
113
|
+
console.error(
|
|
114
|
+
"Error: --expected-home-account-id / MS365_MCP_EXPECTED_HOME_ACCOUNT_ID was provided but is empty. Provide an MSAL homeAccountId, or omit it to allow any cached account."
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
options.expectedHomeAccountId = expectedHomeAccountId;
|
|
119
|
+
}
|
|
88
120
|
if (options.enabledTools) {
|
|
89
121
|
try {
|
|
90
122
|
new RegExp(options.enabledTools, "i");
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,10 @@ import { parseArgs } from "./cli.js";
|
|
|
4
4
|
import logger from "./logger.js";
|
|
5
5
|
import AuthManager, { buildAllowedScopeDiagnostics, resolveAuthScopes } from "./auth.js";
|
|
6
6
|
import MicrosoftGraphServer from "./server.js";
|
|
7
|
+
import {
|
|
8
|
+
getExpectedAccountInertWarning,
|
|
9
|
+
shouldAssertExpectedAccountAtStartup
|
|
10
|
+
} from "./startup-pinning.js";
|
|
7
11
|
import { version } from "./version.js";
|
|
8
12
|
async function main() {
|
|
9
13
|
try {
|
|
@@ -37,12 +41,20 @@ async function main() {
|
|
|
37
41
|
);
|
|
38
42
|
process.exit(0);
|
|
39
43
|
}
|
|
40
|
-
const authManager = await AuthManager.create(effectiveScopes
|
|
44
|
+
const authManager = await AuthManager.create(effectiveScopes, {
|
|
45
|
+
expectedUsername: args.expectedUsername,
|
|
46
|
+
expectedHomeAccountId: args.expectedHomeAccountId
|
|
47
|
+
});
|
|
41
48
|
await authManager.loadTokenCache();
|
|
42
49
|
if (args.authBrowser) {
|
|
43
50
|
authManager.setUseInteractiveAuth(true);
|
|
44
51
|
logger.info("Browser-based interactive auth enabled");
|
|
45
52
|
}
|
|
53
|
+
const expectedAccountWarning = getExpectedAccountInertWarning(args, authManager);
|
|
54
|
+
if (expectedAccountWarning) {
|
|
55
|
+
logger.warn(expectedAccountWarning);
|
|
56
|
+
console.error(expectedAccountWarning);
|
|
57
|
+
}
|
|
46
58
|
if (args.login) {
|
|
47
59
|
if (args.authBrowser) {
|
|
48
60
|
await authManager.acquireTokenInteractive();
|
|
@@ -97,6 +109,9 @@ async function main() {
|
|
|
97
109
|
}
|
|
98
110
|
process.exit(0);
|
|
99
111
|
}
|
|
112
|
+
if (shouldAssertExpectedAccountAtStartup(args, authManager)) {
|
|
113
|
+
await authManager.assertExpectedAccountAvailable();
|
|
114
|
+
}
|
|
100
115
|
const server = new MicrosoftGraphServer(authManager, args);
|
|
101
116
|
await server.initialize(version);
|
|
102
117
|
await server.start();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const LOCAL_ACCOUNT_COMMANDS = [
|
|
2
|
+
"login",
|
|
3
|
+
"logout",
|
|
4
|
+
"listAccounts",
|
|
5
|
+
"selectAccount",
|
|
6
|
+
"removeAccount",
|
|
7
|
+
"verifyLogin"
|
|
8
|
+
];
|
|
9
|
+
function getExpectedAccountInertWarning(args, authManager) {
|
|
10
|
+
if (!authManager.hasExpectedAccount()) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const inertModes = [];
|
|
14
|
+
if (args.http) {
|
|
15
|
+
inertModes.push("--http");
|
|
16
|
+
}
|
|
17
|
+
if (args.obo) {
|
|
18
|
+
inertModes.push("--obo");
|
|
19
|
+
}
|
|
20
|
+
if (authManager.isOAuthModeEnabled()) {
|
|
21
|
+
inertModes.push("MS365_MCP_OAUTH_TOKEN");
|
|
22
|
+
}
|
|
23
|
+
if (inertModes.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return `Warning: expected account pinning is configured, but ${inertModes.join(", ")} uses request-provided tokens for Graph calls. The pin only guards local MSAL auth helpers.`;
|
|
27
|
+
}
|
|
28
|
+
function shouldAssertExpectedAccountAtStartup(args, authManager) {
|
|
29
|
+
if (!authManager.hasExpectedAccount()) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (getExpectedAccountInertWarning(args, authManager)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return !LOCAL_ACCOUNT_COMMANDS.some((key) => Boolean(args[key]));
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
getExpectedAccountInertWarning,
|
|
39
|
+
shouldAssertExpectedAccountAtStartup
|
|
40
|
+
};
|
package/docs/deployment.md
CHANGED
|
@@ -194,6 +194,7 @@ The client automatically discovers OAuth endpoints and opens a browser for authe
|
|
|
194
194
|
## Security Considerations
|
|
195
195
|
|
|
196
196
|
- **Stateless**: the server does not store tokens — each request carries the user's Bearer token
|
|
197
|
+
- **Account pinning**: `MS365_MCP_EXPECTED_USERNAME` and `MS365_MCP_EXPECTED_HOME_ACCOUNT_ID` protect local MSAL cache flows for headless stdio deployments. In `--http`, `--obo`, or `MS365_MCP_OAUTH_TOKEN` deployments they are warning-only because Graph calls use request-provided tokens.
|
|
197
198
|
- **Admin consent**: grant tenant-wide consent to avoid per-user consent prompts
|
|
198
199
|
- **Managed identity**: use managed identity for Key Vault access (no secrets in environment variables)
|
|
199
200
|
- **Read-only mode**: use `--read-only` to disable all write operations (send, delete, update, create)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.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",
|