@softeria/ms-365-mcp-server
Version:
A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API
788 lines (787 loc) • 29.4 kB
JavaScript
import { PublicClientApplication } from "@azure/msal-node";
import logger from "./logger.js";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import path from "path";
import { getSecrets } from "./secrets.js";
import { getCloudEndpoints, getDefaultClientId } from "./cloud-config.js";
import {
createTokenCacheStorage,
DefaultTokenCacheStorage,
getSelectedAccountPath,
getTokenCachePath,
pickNewest,
unwrapCache,
wrapCache
} from "./token-cache-storage.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const endpointsData = JSON.parse(
readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
);
const endpoints = {
default: endpointsData
};
function createMsalConfig(secrets) {
const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
return {
auth: {
clientId: secrets.clientId || getDefaultClientId(secrets.cloudType),
authority: `${cloudEndpoints.authority}/${secrets.tenantId || "common"}`
}
};
}
const SCOPE_HIERARCHY = {
"Mail.ReadWrite": ["Mail.Read"],
"Calendars.ReadWrite": ["Calendars.Read"],
"Files.ReadWrite": ["Files.Read"],
"Tasks.ReadWrite": ["Tasks.Read"],
"Contacts.ReadWrite": ["Contacts.Read"]
};
function parseAllowedScopes(value) {
if (value === void 0) {
return void 0;
}
return Array.from(new Set(value.trim().split(/\s+/).filter(Boolean)));
}
function getEndpointRequiredScopes(endpoint, includeWorkAccountScopes = false) {
if (!endpoint) {
return [];
}
const scopes = /* @__PURE__ */ new Set();
if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
endpoint.scopes.forEach((scope) => scopes.add(scope));
}
if (includeWorkAccountScopes && endpoint.workScopes && Array.isArray(endpoint.workScopes)) {
endpoint.workScopes.forEach((scope) => scopes.add(scope));
}
return Array.from(scopes);
}
function collapseRedundantScopes(scopes) {
const scopesSet = new Set(scopes);
Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
if (scopesSet.has(higherScope) && lowerScopes.every((scope) => scopesSet.has(scope))) {
lowerScopes.forEach((scope) => scopesSet.delete(scope));
}
});
return Array.from(scopesSet);
}
function buildScopesFromEndpoints(includeWorkAccountScopes = false, enabledToolsPattern, readOnly = false) {
const scopesSet = /* @__PURE__ */ new Set();
let enabledToolsRegex;
if (enabledToolsPattern) {
try {
enabledToolsRegex = new RegExp(enabledToolsPattern, "i");
logger.info(`Building scopes with tool filter pattern: ${enabledToolsPattern}`);
} catch {
logger.error(
`Invalid tool filter regex pattern: ${enabledToolsPattern}. Building scopes without filter.`
);
}
}
endpoints.default.forEach((endpoint) => {
if (readOnly && endpoint.method.toUpperCase() !== "GET") {
if (!(endpoint.method.toUpperCase() === "POST" && endpoint.readOnly)) {
return;
}
}
if (enabledToolsRegex && !enabledToolsRegex.test(endpoint.toolName)) {
return;
}
if (!includeWorkAccountScopes && !endpoint.scopes && endpoint.workScopes) {
return;
}
getEndpointRequiredScopes(endpoint, includeWorkAccountScopes).forEach(
(scope) => scopesSet.add(scope)
);
});
const scopes = collapseRedundantScopes(Array.from(scopesSet));
if (enabledToolsPattern) {
logger.info(`Built ${scopes.length} scopes for filtered tools: ${scopes.join(", ")}`);
}
return scopes;
}
function lowerScopesFor(scope) {
const lowerScopes = new Set(SCOPE_HIERARCHY[scope] ?? []);
if (scope.endsWith(".ReadWrite.All")) {
const readAllScope = scope.replace(/\.ReadWrite\.All$/, ".Read.All");
const readWriteScope = scope.replace(/\.ReadWrite\.All$/, ".ReadWrite");
const readScope = scope.replace(/\.ReadWrite\.All$/, ".Read");
lowerScopes.add(readAllScope);
lowerScopes.add(readWriteScope);
lowerScopes.add(readScope);
} else if (scope.endsWith(".ReadWrite.Shared")) {
lowerScopes.add(scope.replace(/\.ReadWrite\.Shared$/, ".Read.Shared"));
} else if (scope.endsWith(".ReadWrite")) {
lowerScopes.add(scope.replace(/\.ReadWrite$/, ".Read"));
} else if (scope.endsWith(".Read.All")) {
lowerScopes.add(scope.replace(/\.Read\.All$/, ".Read"));
}
return Array.from(lowerScopes);
}
function addImpliedScopes(scope, scopesSet) {
for (const lowerScope of lowerScopesFor(scope)) {
if (!scopesSet.has(lowerScope)) {
scopesSet.add(lowerScope);
addImpliedScopes(lowerScope, scopesSet);
}
}
}
function collapseScopeHierarchy(scopes) {
const scopesSet = new Set(scopes);
for (const scope of scopes) {
addImpliedScopes(scope, scopesSet);
}
return Array.from(scopesSet);
}
function getMissingAllowedScopes(requiredScopes, allowedScopes) {
if (allowedScopes === void 0) {
return [];
}
const coveredAllowedScopes = new Set(collapseScopeHierarchy(allowedScopes));
return requiredScopes.filter((scope) => !coveredAllowedScopes.has(scope));
}
function isScopeUsedByTools(allowedScope, toolScopes) {
const coveredByAllowedScope = new Set(collapseScopeHierarchy([allowedScope]));
return toolScopes.some((scope) => coveredByAllowedScope.has(scope));
}
function endpointMatchesNormalToolSurface(endpoint, includeWorkAccountScopes, enabledToolsRegex, readOnly = false) {
if (readOnly && endpoint.method.toUpperCase() !== "GET") {
if (!(endpoint.method.toUpperCase() === "POST" && endpoint.readOnly)) {
return false;
}
}
if (enabledToolsRegex && !enabledToolsRegex.test(endpoint.toolName)) {
return false;
}
if (!includeWorkAccountScopes && !endpoint.scopes && endpoint.workScopes) {
return false;
}
return true;
}
function buildAllowedScopeDiagnostics(options = {}) {
const allowedScopes = parseAllowedScopes(options.allowedScopes);
let enabledToolsRegex;
if (options.enabledTools) {
try {
enabledToolsRegex = new RegExp(options.enabledTools, "i");
} catch {
logger.error(
`Invalid tool filter regex pattern: ${options.enabledTools}. Building diagnostics without filter.`
);
}
}
const normalToolScopes = /* @__PURE__ */ new Set();
const effectiveToolScopes = /* @__PURE__ */ new Set();
const disabledTools = [];
for (const endpoint of endpoints.default) {
if (!endpointMatchesNormalToolSurface(
endpoint,
Boolean(options.orgMode),
enabledToolsRegex,
Boolean(options.readOnly)
)) {
continue;
}
const requiredScopes = getEndpointRequiredScopes(endpoint, Boolean(options.orgMode));
requiredScopes.forEach((scope) => normalToolScopes.add(scope));
const missingScopes = getMissingAllowedScopes(requiredScopes, allowedScopes);
if (missingScopes.length > 0) {
disabledTools.push({
toolName: endpoint.toolName,
requiredScopes: requiredScopes.sort((a, b) => a.localeCompare(b)),
missingScopes: missingScopes.sort((a, b) => a.localeCompare(b))
});
continue;
}
requiredScopes.forEach((scope) => effectiveToolScopes.add(scope));
}
const toolPermissions = collapseRedundantScopes(Array.from(normalToolScopes)).sort(
(a, b) => a.localeCompare(b)
);
const effectivePermissions = collapseRedundantScopes(Array.from(effectiveToolScopes)).sort(
(a, b) => a.localeCompare(b)
);
const sortedAllowedScopes = allowedScopes ? [...allowedScopes].sort((a, b) => a.localeCompare(b)) : void 0;
const missingAllowedScopesForTools = Array.from(
new Set(disabledTools.flatMap((tool) => tool.missingScopes))
).sort((a, b) => a.localeCompare(b));
const extraAllowedScopesNotUsedByTools = sortedAllowedScopes?.filter((scope) => !isScopeUsedByTools(scope, effectivePermissions)) ?? [];
return {
permissions: effectivePermissions,
toolPermissions,
effectivePermissions,
...sortedAllowedScopes ? { allowedScopes: sortedAllowedScopes } : {},
disabledTools,
missingAllowedScopesForTools,
extraAllowedScopesNotUsedByTools
};
}
function resolveAuthScopes(options = {}) {
return buildAllowedScopeDiagnostics(options).effectivePermissions;
}
function buildScopeDiagnostics(toolScopes, allowedScopesInput) {
const toolPermissions = [...toolScopes].sort((a, b) => a.localeCompare(b));
const coveredAllowedScopes = new Set(collapseScopeHierarchy(allowedScopesInput));
const missingAllowedScopesForTools = toolPermissions.filter(
(scope) => !coveredAllowedScopes.has(scope)
);
return {
permissions: toolPermissions.filter((scope) => coveredAllowedScopes.has(scope)),
toolPermissions,
effectivePermissions: toolPermissions.filter((scope) => coveredAllowedScopes.has(scope)),
allowedScopes: [...allowedScopesInput].sort((a, b) => a.localeCompare(b)),
disabledTools: [],
missingAllowedScopesForTools,
extraAllowedScopesNotUsedByTools: [...allowedScopesInput].sort((a, b) => a.localeCompare(b)).filter((scope) => !isScopeUsedByTools(scope, toolPermissions))
};
}
class AuthManager {
constructor(config, scopes = [], expectedAccount, storage) {
logger.info(`And scopes are ${scopes.join(", ")}`, scopes);
this.config = config;
this.scopes = scopes;
this.msalApp = new PublicClientApplication(this.config);
this.accessToken = null;
this.tokenExpiry = null;
this.selectedAccountId = null;
this.useInteractiveAuth = false;
this.expectedUsername = this.normalizeExpectedUsername(expectedAccount?.expectedUsername);
this.expectedHomeAccountId = this.normalizeExpectedHomeAccountId(
expectedAccount?.expectedHomeAccountId
);
this.storage = storage ?? new DefaultTokenCacheStorage();
const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
this.oauthToken = oauthTokenFromEnv ?? null;
this.isOAuthMode = oauthTokenFromEnv != null;
}
/**
* Creates an AuthManager instance with secrets loaded from the configured provider.
* Uses Key Vault if MS365_MCP_KEYVAULT_URL is set, otherwise environment variables.
*/
static async create(scopes = [], expectedAccount, options = {}) {
const secrets = await getSecrets();
const config = createMsalConfig(secrets);
const storage = options.storage ?? await createTokenCacheStorage({ allowCommandStorage: false, logProvider: true });
return new AuthManager(config, scopes, expectedAccount, storage);
}
async loadTokenCache() {
try {
const cacheRaw = await this.storage.load("token-cache");
if (cacheRaw) {
this.msalApp.getTokenCache().deserialize(unwrapCache(cacheRaw).data);
}
await this.loadSelectedAccount();
} catch (error) {
logger.error(`Error loading token cache: ${error.message}`);
if (this.storage.failClosed) {
throw error;
}
}
}
async loadSelectedAccount() {
try {
const selectedAccountRaw = await this.storage.load("selected-account");
if (selectedAccountRaw) {
const parsed = JSON.parse(unwrapCache(selectedAccountRaw).data);
this.selectedAccountId = parsed.accountId;
logger.info(`Loaded selected account: ${this.selectedAccountId}`);
}
} catch (error) {
logger.error(`Error loading selected account: ${error.message}`);
if (this.storage.failClosed) {
throw error;
}
}
}
async saveTokenCache() {
try {
const stamped = wrapCache(this.msalApp.getTokenCache().serialize());
await this.storage.save("token-cache", stamped);
} catch (error) {
logger.error(`Error saving token cache: ${error.message}`);
if (this.storage.failClosed) {
throw error;
}
}
}
async saveSelectedAccount() {
try {
const stamped = wrapCache(JSON.stringify({ accountId: this.selectedAccountId }));
await this.storage.save("selected-account", stamped);
} catch (error) {
logger.error(`Error saving selected account: ${error.message}`);
if (this.storage.failClosed) {
throw error;
}
}
}
normalizeExpectedUsername(value) {
if (value === void 0) {
return null;
}
const trimmed = value.trim();
if (trimmed === "") {
throw new Error("Expected Microsoft account username was provided but is empty.");
}
return trimmed.toLowerCase();
}
normalizeExpectedHomeAccountId(value) {
if (value === void 0) {
return null;
}
const trimmed = value.trim();
if (trimmed === "") {
throw new Error("Expected Microsoft account homeAccountId was provided but is empty.");
}
return trimmed;
}
hasExpectedAccount() {
return this.expectedUsername !== null || this.expectedHomeAccountId !== null;
}
expectedAccountLabel() {
const parts = [];
if (this.expectedUsername) {
parts.push(`username ${this.expectedUsername}`);
}
if (this.expectedHomeAccountId) {
parts.push(`homeAccountId ${this.expectedHomeAccountId}`);
}
return parts.join(" and ");
}
describeAccount(account) {
return account?.username || account?.name || "unknown";
}
describeCachedAccounts(accounts) {
if (accounts.length === 0) {
return "none";
}
return accounts.map((account) => this.describeAccount(account)).join(", ");
}
accountMatchesExpected(account) {
if (!this.hasExpectedAccount() || !account) {
return !this.hasExpectedAccount();
}
if (this.expectedUsername && account.username?.toLowerCase() !== this.expectedUsername) {
return false;
}
if (this.expectedHomeAccountId && account.homeAccountId !== this.expectedHomeAccountId) {
return false;
}
return true;
}
buildExpectedAccountMissingError(accounts) {
return new Error(
`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.`
);
}
resolveExpectedAccountFromAccounts(accounts) {
if (!this.hasExpectedAccount()) {
throw new Error("No expected Microsoft account is configured.");
}
const usernameMatch = this.expectedUsername ? accounts.find((account) => account.username?.toLowerCase() === this.expectedUsername) : void 0;
const homeAccountIdMatch = this.expectedHomeAccountId ? accounts.find((account) => account.homeAccountId === this.expectedHomeAccountId) : void 0;
if (this.expectedUsername && this.expectedHomeAccountId) {
if (!usernameMatch || !homeAccountIdMatch) {
throw this.buildExpectedAccountMissingError(accounts);
}
if (usernameMatch.homeAccountId !== homeAccountIdMatch.homeAccountId) {
throw new Error(
`Expected Microsoft account pins conflict: username ${this.expectedUsername} matched ${this.describeAccount(usernameMatch)}, but homeAccountId ${this.expectedHomeAccountId} matched ${this.describeAccount(homeAccountIdMatch)}.`
);
}
return usernameMatch;
}
const expectedAccount = usernameMatch ?? homeAccountIdMatch;
if (!expectedAccount) {
throw this.buildExpectedAccountMissingError(accounts);
}
return expectedAccount;
}
async assertExpectedAccountAvailable() {
if (!this.hasExpectedAccount()) {
return;
}
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
this.resolveExpectedAccountFromAccounts(accounts);
}
async rejectUnexpectedLoginAccount(account) {
if (!this.hasExpectedAccount()) {
return;
}
if (this.accountMatchesExpected(account)) {
return;
}
this.accessToken = null;
this.tokenExpiry = null;
if (account) {
try {
await this.msalApp.getTokenCache().removeAccount(account);
} catch (error) {
logger.warn(`Failed to remove unexpected account from cache: ${error.message}`);
}
throw new Error(
`Authenticated Microsoft account '${this.describeAccount(account)}' does not match expected Microsoft account '${this.expectedAccountLabel()}'. Login was not persisted.`
);
}
throw new Error(
`Microsoft login did not return an account. Expected Microsoft account '${this.expectedAccountLabel()}'. Login was not persisted.`
);
}
async setOAuthToken(token) {
this.oauthToken = token;
this.isOAuthMode = true;
}
async getToken(forceRefresh = false) {
if (this.isOAuthMode && this.oauthToken) {
return this.oauthToken;
}
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
return this.accessToken;
}
const currentAccount = await this.getCurrentAccount();
if (currentAccount) {
const silentRequest = {
account: currentAccount,
scopes: this.scopes
};
try {
const response = await this.msalApp.acquireTokenSilent(silentRequest);
this.accessToken = response.accessToken;
this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
await this.saveTokenCache();
return this.accessToken;
} catch {
logger.error("Silent token acquisition failed");
throw new Error("Silent token acquisition failed");
}
}
throw new Error("No valid token found");
}
async getCurrentAccount() {
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
if (this.hasExpectedAccount()) {
return this.resolveExpectedAccountFromAccounts(accounts);
}
if (accounts.length === 0) {
return null;
}
if (this.selectedAccountId) {
const selectedAccount = accounts.find(
(account) => account.homeAccountId === this.selectedAccountId
);
if (selectedAccount) {
return selectedAccount;
}
logger.warn(
`Selected account ${this.selectedAccountId} not found, falling back to first account`
);
}
return accounts[0];
}
async acquireTokenByDeviceCode(hack) {
const deviceCodeRequest = {
scopes: this.scopes,
deviceCodeCallback: (response) => {
const text = ["\n", response.message, "\n"].join("");
if (hack) {
hack(text + 'After login run the "verify login" command');
} else {
console.log(text);
}
logger.info("Device code login initiated");
}
};
try {
logger.info("Requesting device code...");
logger.info(`Requesting scopes: ${this.scopes.join(", ")}`);
const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
logger.info(`Granted scopes: ${response?.scopes?.join(", ") || "none"}`);
logger.info("Device code login successful");
this.accessToken = response?.accessToken || null;
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
await this.rejectUnexpectedLoginAccount(response?.account);
if (!this.selectedAccountId && response?.account) {
this.selectedAccountId = response.account.homeAccountId;
await this.saveSelectedAccount();
logger.info(`Auto-selected new account: ${response.account.username}`);
}
await this.saveTokenCache();
return this.accessToken;
} catch (error) {
logger.error(`Error in device code flow: ${error.message}`);
throw error;
}
}
setUseInteractiveAuth(value) {
this.useInteractiveAuth = value;
}
getUseInteractiveAuth() {
return this.useInteractiveAuth;
}
async acquireTokenInteractive(hack) {
const open = (await import("open")).default;
const interactiveRequest = {
scopes: this.scopes,
openBrowser: async (url) => {
const message = "Opening browser for Microsoft sign-in...";
if (hack) {
hack(message);
}
logger.info(message);
await open(url);
},
successTemplate: "<h1>Authentication successful!</h1><p>You can close this window and return to your application.</p>",
errorTemplate: "<h1>Authentication failed</h1><p>Something went wrong. Please try again.</p>"
};
try {
logger.info("Requesting interactive browser login...");
logger.info(`Requesting scopes: ${this.scopes.join(", ")}`);
const response = await this.msalApp.acquireTokenInteractive(interactiveRequest);
logger.info(`Granted scopes: ${response?.scopes?.join(", ") || "none"}`);
logger.info("Interactive browser login successful");
this.accessToken = response?.accessToken || null;
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
await this.rejectUnexpectedLoginAccount(response?.account);
if (!this.selectedAccountId && response?.account) {
this.selectedAccountId = response.account.homeAccountId;
await this.saveSelectedAccount();
logger.info(`Auto-selected new account: ${response.account.username}`);
}
await this.saveTokenCache();
return this.accessToken;
} catch (error) {
logger.error(`Error in interactive browser flow: ${error.message}`);
throw error;
}
}
async testLogin() {
try {
logger.info("Testing login...");
const token = await this.getToken();
if (!token) {
logger.error("Login test failed - no token received");
return {
success: false,
message: "Login failed - no token received"
};
}
logger.info("Token retrieved successfully, testing Graph API access...");
try {
const secrets = await getSecrets();
const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
headers: {
Authorization: `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
logger.info("Graph API user data fetch successful");
return {
success: true,
message: "Login successful",
userData: {
displayName: userData.displayName,
userPrincipalName: userData.userPrincipalName
}
};
} else {
const errorText = await response.text();
logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
return {
success: false,
message: `Login successful but Graph API access failed: ${response.status}`
};
}
} catch (graphError) {
logger.error(`Error fetching user data: ${graphError.message}`);
return {
success: false,
message: `Login successful but Graph API access failed: ${graphError.message}`
};
}
} catch (error) {
logger.error(`Login test failed: ${error.message}`);
return {
success: false,
message: `Login failed: ${error.message}`
};
}
}
async logout() {
try {
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
for (const account of accounts) {
await this.msalApp.getTokenCache().removeAccount(account);
}
this.accessToken = null;
this.tokenExpiry = null;
this.selectedAccountId = null;
await this.storage.delete("token-cache");
await this.storage.delete("selected-account");
return true;
} catch (error) {
logger.error(`Error during logout: ${error.message}`);
throw error;
}
}
// Multi-account support methods
async listAccounts() {
return await this.msalApp.getTokenCache().getAllAccounts();
}
async selectAccount(identifier) {
const account = await this.resolveAccount(identifier);
if (this.hasExpectedAccount() && !this.accountMatchesExpected(account)) {
throw new Error(
`Account '${identifier}' does not match expected Microsoft account '${this.expectedAccountLabel()}'.`
);
}
this.selectedAccountId = account.homeAccountId;
await this.saveSelectedAccount();
this.accessToken = null;
this.tokenExpiry = null;
logger.info(`Selected account: ${account.username} (${account.homeAccountId})`);
return true;
}
async removeAccount(identifier) {
const account = await this.resolveAccount(identifier);
try {
await this.msalApp.getTokenCache().removeAccount(account);
if (this.selectedAccountId === account.homeAccountId) {
this.selectedAccountId = null;
await this.saveSelectedAccount();
this.accessToken = null;
this.tokenExpiry = null;
}
logger.info(`Removed account: ${account.username} (${account.homeAccountId})`);
return true;
} catch (error) {
logger.error(`Failed to remove account ${identifier}: ${error.message}`);
return false;
}
}
getSelectedAccountId() {
return this.selectedAccountId;
}
/**
* Returns true if auth is in OAuth/HTTP mode (token supplied via env or setOAuthToken).
* In this mode, account resolution should be skipped — the request context drives token selection.
*/
isOAuthModeEnabled() {
return this.isOAuthMode;
}
/**
* Resolves an account by identifier (email or homeAccountId).
* Resolution: username match (case-insensitive) → homeAccountId match → throw.
*/
async resolveAccount(identifier) {
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
if (accounts.length === 0) {
throw new Error("No accounts found. Please login first.");
}
const lowerIdentifier = identifier.toLowerCase();
let account = accounts.find((a) => a.username?.toLowerCase() === lowerIdentifier) ?? null;
if (!account) {
account = accounts.find((a) => a.homeAccountId === identifier) ?? null;
}
if (!account) {
const availableAccounts = accounts.map((a) => a.username || a.name || "unknown").join(", ");
throw new Error(
`Account '${identifier}' not found. Available accounts: ${availableAccounts}`
);
}
return account;
}
/**
* Returns true if the MSAL cache contains more than one account.
* Used to decide whether to inject the `account` parameter into tool schemas.
*/
async isMultiAccount() {
if (this.hasExpectedAccount()) {
return false;
}
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
return accounts.length > 1;
}
/**
* Acquires a token for a specific account identified by username (email) or homeAccountId,
* WITHOUT changing the persisted selectedAccountId.
*
* Resolution order:
* 1. Exact match on username (case-insensitive)
* 2. Exact match on homeAccountId
* 3. If identifier is empty/undefined AND only 1 account exists → auto-select
* 4. If identifier is empty/undefined AND multiple accounts → use selectedAccountId or throw
*
* @returns The access token string.
*/
async getTokenForAccount(identifier) {
if (this.isOAuthMode && this.oauthToken) {
return this.oauthToken;
}
let targetAccount = null;
if (this.hasExpectedAccount()) {
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
targetAccount = this.resolveExpectedAccountFromAccounts(accounts);
if (identifier) {
const requestedAccount = await this.resolveAccount(identifier);
if (requestedAccount.homeAccountId !== targetAccount.homeAccountId) {
throw new Error(
`Account '${identifier}' does not match expected Microsoft account '${this.expectedAccountLabel()}'.`
);
}
}
} else if (identifier) {
targetAccount = await this.resolveAccount(identifier);
} else {
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
if (accounts.length === 0) {
throw new Error("No accounts found. Please login first.");
}
if (accounts.length === 1) {
targetAccount = accounts[0];
} else {
if (this.selectedAccountId) {
targetAccount = accounts.find((a) => a.homeAccountId === this.selectedAccountId) ?? null;
}
if (!targetAccount) {
const availableAccounts = accounts.map((a) => a.username || a.name || "unknown").join(", ");
throw new Error(
`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.`
);
}
}
}
const silentRequest = {
account: targetAccount,
scopes: this.scopes
};
try {
const response = await this.msalApp.acquireTokenSilent(silentRequest);
await this.saveTokenCache();
return response.accessToken;
} catch {
throw new Error(
`Failed to acquire token for account '${targetAccount.username || targetAccount.name || "unknown"}'. The token may have expired. Please re-login with: --login`
);
}
}
}
var auth_default = AuthManager;
export {
buildAllowedScopeDiagnostics,
buildScopeDiagnostics,
buildScopesFromEndpoints,
collapseScopeHierarchy,
auth_default as default,
getEndpointRequiredScopes,
getMissingAllowedScopes,
getSelectedAccountPath,
getTokenCachePath,
parseAllowedScopes,
pickNewest,
resolveAuthScopes,
unwrapCache,
wrapCache
};