@dashflow/ms365-mcp-server 1.0.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.
@@ -0,0 +1,202 @@
1
+ import { z } from "zod";
2
+ function registerAuthTools(server, authManager) {
3
+ server.tool(
4
+ "login",
5
+ "Authenticate with Microsoft using device code flow",
6
+ {
7
+ force: z.boolean().default(false).describe("Force a new login even if already logged in")
8
+ },
9
+ async ({ force }) => {
10
+ try {
11
+ if (!force) {
12
+ const loginStatus = await authManager.testLogin();
13
+ if (loginStatus.success) {
14
+ return {
15
+ content: [
16
+ {
17
+ type: "text",
18
+ text: JSON.stringify({
19
+ status: "Already logged in",
20
+ ...loginStatus
21
+ })
22
+ }
23
+ ]
24
+ };
25
+ }
26
+ }
27
+ const text = await new Promise((resolve, reject) => {
28
+ authManager.acquireTokenByDeviceCode(resolve).catch(reject);
29
+ });
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: JSON.stringify({
35
+ error: "device_code_required",
36
+ message: text.trim()
37
+ })
38
+ }
39
+ ]
40
+ };
41
+ } catch (error) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: JSON.stringify({ error: `Authentication failed: ${error.message}` })
47
+ }
48
+ ]
49
+ };
50
+ }
51
+ }
52
+ );
53
+ server.tool("logout", "Log out from Microsoft account", {}, async () => {
54
+ try {
55
+ await authManager.logout();
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: JSON.stringify({ message: "Logged out successfully" })
61
+ }
62
+ ]
63
+ };
64
+ } catch {
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: JSON.stringify({ error: "Logout failed" })
70
+ }
71
+ ]
72
+ };
73
+ }
74
+ });
75
+ server.tool("verify-login", "Check current Microsoft authentication status", {}, async () => {
76
+ const testResult = await authManager.testLogin();
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: JSON.stringify(testResult)
82
+ }
83
+ ]
84
+ };
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
+ };
113
+ }
114
+ });
115
+ server.tool(
116
+ "select-account",
117
+ "Select a specific Microsoft account to use",
118
+ {
119
+ accountId: z.string().describe("The account ID to select")
120
+ },
121
+ async ({ accountId }) => {
122
+ 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
+ }
143
+ } catch (error) {
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: JSON.stringify({
149
+ error: `Failed to select account: ${error.message}`
150
+ })
151
+ }
152
+ ]
153
+ };
154
+ }
155
+ }
156
+ );
157
+ server.tool(
158
+ "remove-account",
159
+ "Remove a Microsoft account from the cache",
160
+ {
161
+ accountId: z.string().describe("The account ID to remove")
162
+ },
163
+ async ({ accountId }) => {
164
+ try {
165
+ const success = await authManager.removeAccount(accountId);
166
+ if (success) {
167
+ return {
168
+ content: [
169
+ {
170
+ type: "text",
171
+ text: JSON.stringify({ message: `Removed account: ${accountId}` })
172
+ }
173
+ ]
174
+ };
175
+ } else {
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: JSON.stringify({ error: `Account not found: ${accountId}` })
181
+ }
182
+ ]
183
+ };
184
+ }
185
+ } catch (error) {
186
+ return {
187
+ content: [
188
+ {
189
+ type: "text",
190
+ text: JSON.stringify({
191
+ error: `Failed to remove account: ${error.message}`
192
+ })
193
+ }
194
+ ]
195
+ };
196
+ }
197
+ }
198
+ );
199
+ }
200
+ export {
201
+ registerAuthTools
202
+ };
package/dist/auth.js ADDED
@@ -0,0 +1,422 @@
1
+ import { PublicClientApplication } from "@azure/msal-node";
2
+ import logger from "./logger.js";
3
+ import fs, { existsSync, readFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import path from "path";
6
+ import { getSecrets } from "./secrets.js";
7
+ import { getCloudEndpoints, getDefaultClientId } from "./cloud-config.js";
8
+ let keytar = null;
9
+ async function getKeytar() {
10
+ if (keytar === void 0) {
11
+ return null;
12
+ }
13
+ if (keytar === null) {
14
+ try {
15
+ keytar = await import("keytar");
16
+ return keytar;
17
+ } catch (error) {
18
+ logger.info("keytar not available, using file-based credential storage");
19
+ keytar = void 0;
20
+ return null;
21
+ }
22
+ }
23
+ return keytar;
24
+ }
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const endpointsData = JSON.parse(
28
+ readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
29
+ );
30
+ const endpoints = {
31
+ default: endpointsData
32
+ };
33
+ const SERVICE_NAME = "ms-365-mcp-server";
34
+ const TOKEN_CACHE_ACCOUNT = "msal-token-cache";
35
+ const SELECTED_ACCOUNT_KEY = "selected-account";
36
+ const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
37
+ const FALLBACK_PATH = path.join(FALLBACK_DIR, "..", ".token-cache.json");
38
+ const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, "..", ".selected-account.json");
39
+ function createMsalConfig(secrets) {
40
+ const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
41
+ return {
42
+ auth: {
43
+ clientId: secrets.clientId || getDefaultClientId(secrets.cloudType),
44
+ authority: `${cloudEndpoints.authority}/${secrets.tenantId || "common"}`
45
+ }
46
+ };
47
+ }
48
+ const SCOPE_HIERARCHY = {
49
+ "Mail.ReadWrite": ["Mail.Read"],
50
+ "Calendars.ReadWrite": ["Calendars.Read"],
51
+ "Files.ReadWrite": ["Files.Read"],
52
+ "Tasks.ReadWrite": ["Tasks.Read"],
53
+ "Contacts.ReadWrite": ["Contacts.Read"]
54
+ };
55
+ function buildScopesFromEndpoints(includeWorkAccountScopes = false, enabledToolsPattern) {
56
+ const scopesSet = /* @__PURE__ */ new Set();
57
+ let enabledToolsRegex;
58
+ if (enabledToolsPattern) {
59
+ try {
60
+ enabledToolsRegex = new RegExp(enabledToolsPattern, "i");
61
+ logger.info(`Building scopes with tool filter pattern: ${enabledToolsPattern}`);
62
+ } catch (error) {
63
+ logger.error(
64
+ `Invalid tool filter regex pattern: ${enabledToolsPattern}. Building scopes without filter.`
65
+ );
66
+ }
67
+ }
68
+ endpoints.default.forEach((endpoint) => {
69
+ if (enabledToolsRegex && !enabledToolsRegex.test(endpoint.toolName)) {
70
+ return;
71
+ }
72
+ if (!includeWorkAccountScopes && !endpoint.scopes && endpoint.workScopes) {
73
+ return;
74
+ }
75
+ if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
76
+ endpoint.scopes.forEach((scope) => scopesSet.add(scope));
77
+ }
78
+ if (includeWorkAccountScopes && endpoint.workScopes && Array.isArray(endpoint.workScopes)) {
79
+ endpoint.workScopes.forEach((scope) => scopesSet.add(scope));
80
+ }
81
+ });
82
+ Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
83
+ if (scopesSet.has(higherScope) && lowerScopes.every((scope) => scopesSet.has(scope))) {
84
+ lowerScopes.forEach((scope) => scopesSet.delete(scope));
85
+ }
86
+ });
87
+ const scopes = Array.from(scopesSet);
88
+ if (enabledToolsPattern) {
89
+ logger.info(`Built ${scopes.length} scopes for filtered tools: ${scopes.join(", ")}`);
90
+ }
91
+ return scopes;
92
+ }
93
+ class AuthManager {
94
+ constructor(config, scopes = buildScopesFromEndpoints()) {
95
+ logger.info(`And scopes are ${scopes.join(", ")}`, scopes);
96
+ this.config = config;
97
+ this.scopes = scopes;
98
+ this.msalApp = new PublicClientApplication(this.config);
99
+ this.accessToken = null;
100
+ this.tokenExpiry = null;
101
+ this.selectedAccountId = null;
102
+ const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
103
+ this.oauthToken = oauthTokenFromEnv ?? null;
104
+ this.isOAuthMode = oauthTokenFromEnv != null;
105
+ }
106
+ /**
107
+ * Creates an AuthManager instance with secrets loaded from the configured provider.
108
+ * Uses Key Vault if MS365_MCP_KEYVAULT_URL is set, otherwise environment variables.
109
+ */
110
+ static async create(scopes = buildScopesFromEndpoints()) {
111
+ const secrets = await getSecrets();
112
+ const config = createMsalConfig(secrets);
113
+ return new AuthManager(config, scopes);
114
+ }
115
+ async loadTokenCache() {
116
+ try {
117
+ let cacheData;
118
+ try {
119
+ const kt = await getKeytar();
120
+ if (kt) {
121
+ const cachedData = await kt.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
122
+ if (cachedData) {
123
+ cacheData = cachedData;
124
+ }
125
+ }
126
+ } catch (keytarError) {
127
+ logger.warn(
128
+ `Keychain access failed, falling back to file storage: ${keytarError.message}`
129
+ );
130
+ }
131
+ if (!cacheData && existsSync(FALLBACK_PATH)) {
132
+ cacheData = readFileSync(FALLBACK_PATH, "utf8");
133
+ }
134
+ if (cacheData) {
135
+ this.msalApp.getTokenCache().deserialize(cacheData);
136
+ }
137
+ await this.loadSelectedAccount();
138
+ } catch (error) {
139
+ logger.error(`Error loading token cache: ${error.message}`);
140
+ }
141
+ }
142
+ async loadSelectedAccount() {
143
+ try {
144
+ let selectedAccountData;
145
+ try {
146
+ const kt = await getKeytar();
147
+ if (kt) {
148
+ const cachedData = await kt.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
149
+ if (cachedData) {
150
+ selectedAccountData = cachedData;
151
+ }
152
+ }
153
+ } catch (keytarError) {
154
+ logger.warn(
155
+ `Keychain access failed for selected account, falling back to file storage: ${keytarError.message}`
156
+ );
157
+ }
158
+ if (!selectedAccountData && existsSync(SELECTED_ACCOUNT_PATH)) {
159
+ selectedAccountData = readFileSync(SELECTED_ACCOUNT_PATH, "utf8");
160
+ }
161
+ if (selectedAccountData) {
162
+ const parsed = JSON.parse(selectedAccountData);
163
+ this.selectedAccountId = parsed.accountId;
164
+ logger.info(`Loaded selected account: ${this.selectedAccountId}`);
165
+ }
166
+ } catch (error) {
167
+ logger.error(`Error loading selected account: ${error.message}`);
168
+ }
169
+ }
170
+ async saveTokenCache() {
171
+ try {
172
+ const cacheData = this.msalApp.getTokenCache().serialize();
173
+ try {
174
+ const kt = await getKeytar();
175
+ if (kt) {
176
+ await kt.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
177
+ } else {
178
+ fs.writeFileSync(FALLBACK_PATH, cacheData, { mode: 384 });
179
+ }
180
+ } catch (keytarError) {
181
+ logger.warn(
182
+ `Keychain save failed, falling back to file storage: ${keytarError.message}`
183
+ );
184
+ fs.writeFileSync(FALLBACK_PATH, cacheData, { mode: 384 });
185
+ }
186
+ } catch (error) {
187
+ logger.error(`Error saving token cache: ${error.message}`);
188
+ }
189
+ }
190
+ async saveSelectedAccount() {
191
+ try {
192
+ const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
193
+ try {
194
+ const kt = await getKeytar();
195
+ if (kt) {
196
+ await kt.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
197
+ } else {
198
+ fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData, { mode: 384 });
199
+ }
200
+ } catch (keytarError) {
201
+ logger.warn(
202
+ `Keychain save failed for selected account, falling back to file storage: ${keytarError.message}`
203
+ );
204
+ fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData, { mode: 384 });
205
+ }
206
+ } catch (error) {
207
+ logger.error(`Error saving selected account: ${error.message}`);
208
+ }
209
+ }
210
+ async setOAuthToken(token) {
211
+ this.oauthToken = token;
212
+ this.isOAuthMode = true;
213
+ }
214
+ async getToken(forceRefresh = false) {
215
+ if (this.isOAuthMode && this.oauthToken) {
216
+ return this.oauthToken;
217
+ }
218
+ if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
219
+ return this.accessToken;
220
+ }
221
+ const currentAccount = await this.getCurrentAccount();
222
+ if (currentAccount) {
223
+ const silentRequest = {
224
+ account: currentAccount,
225
+ scopes: this.scopes
226
+ };
227
+ try {
228
+ const response = await this.msalApp.acquireTokenSilent(silentRequest);
229
+ this.accessToken = response.accessToken;
230
+ this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
231
+ return this.accessToken;
232
+ } catch {
233
+ logger.error("Silent token acquisition failed");
234
+ throw new Error("Silent token acquisition failed");
235
+ }
236
+ }
237
+ throw new Error("No valid token found");
238
+ }
239
+ async getCurrentAccount() {
240
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
241
+ if (accounts.length === 0) {
242
+ return null;
243
+ }
244
+ if (this.selectedAccountId) {
245
+ const selectedAccount = accounts.find(
246
+ (account) => account.homeAccountId === this.selectedAccountId
247
+ );
248
+ if (selectedAccount) {
249
+ return selectedAccount;
250
+ }
251
+ logger.warn(
252
+ `Selected account ${this.selectedAccountId} not found, falling back to first account`
253
+ );
254
+ }
255
+ return accounts[0];
256
+ }
257
+ async acquireTokenByDeviceCode(hack) {
258
+ const deviceCodeRequest = {
259
+ scopes: this.scopes,
260
+ deviceCodeCallback: (response) => {
261
+ const text = ["\n", response.message, "\n"].join("");
262
+ if (hack) {
263
+ hack(text + 'After login run the "verify login" command');
264
+ } else {
265
+ console.log(text);
266
+ }
267
+ logger.info("Device code login initiated");
268
+ }
269
+ };
270
+ try {
271
+ logger.info("Requesting device code...");
272
+ logger.info(`Requesting scopes: ${this.scopes.join(", ")}`);
273
+ const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
274
+ logger.info(`Granted scopes: ${response?.scopes?.join(", ") || "none"}`);
275
+ logger.info("Device code login successful");
276
+ this.accessToken = response?.accessToken || null;
277
+ this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
278
+ if (!this.selectedAccountId && response?.account) {
279
+ this.selectedAccountId = response.account.homeAccountId;
280
+ await this.saveSelectedAccount();
281
+ logger.info(`Auto-selected new account: ${response.account.username}`);
282
+ }
283
+ await this.saveTokenCache();
284
+ return this.accessToken;
285
+ } catch (error) {
286
+ logger.error(`Error in device code flow: ${error.message}`);
287
+ throw error;
288
+ }
289
+ }
290
+ async testLogin() {
291
+ try {
292
+ logger.info("Testing login...");
293
+ const token = await this.getToken();
294
+ if (!token) {
295
+ logger.error("Login test failed - no token received");
296
+ return {
297
+ success: false,
298
+ message: "Login failed - no token received"
299
+ };
300
+ }
301
+ logger.info("Token retrieved successfully, testing Graph API access...");
302
+ try {
303
+ const secrets = await getSecrets();
304
+ const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
305
+ const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
306
+ headers: {
307
+ Authorization: `Bearer ${token}`
308
+ }
309
+ });
310
+ if (response.ok) {
311
+ const userData = await response.json();
312
+ logger.info("Graph API user data fetch successful");
313
+ return {
314
+ success: true,
315
+ message: "Login successful",
316
+ userData: {
317
+ displayName: userData.displayName,
318
+ userPrincipalName: userData.userPrincipalName
319
+ }
320
+ };
321
+ } else {
322
+ const errorText = await response.text();
323
+ logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
324
+ return {
325
+ success: false,
326
+ message: `Login successful but Graph API access failed: ${response.status}`
327
+ };
328
+ }
329
+ } catch (graphError) {
330
+ logger.error(`Error fetching user data: ${graphError.message}`);
331
+ return {
332
+ success: false,
333
+ message: `Login successful but Graph API access failed: ${graphError.message}`
334
+ };
335
+ }
336
+ } catch (error) {
337
+ logger.error(`Login test failed: ${error.message}`);
338
+ return {
339
+ success: false,
340
+ message: `Login failed: ${error.message}`
341
+ };
342
+ }
343
+ }
344
+ async logout() {
345
+ try {
346
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
347
+ for (const account of accounts) {
348
+ await this.msalApp.getTokenCache().removeAccount(account);
349
+ }
350
+ this.accessToken = null;
351
+ this.tokenExpiry = null;
352
+ this.selectedAccountId = null;
353
+ try {
354
+ const kt = await getKeytar();
355
+ if (kt) {
356
+ await kt.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
357
+ await kt.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
358
+ }
359
+ } catch (keytarError) {
360
+ logger.warn(`Keychain deletion failed: ${keytarError.message}`);
361
+ }
362
+ if (fs.existsSync(FALLBACK_PATH)) {
363
+ fs.unlinkSync(FALLBACK_PATH);
364
+ }
365
+ if (fs.existsSync(SELECTED_ACCOUNT_PATH)) {
366
+ fs.unlinkSync(SELECTED_ACCOUNT_PATH);
367
+ }
368
+ return true;
369
+ } catch (error) {
370
+ logger.error(`Error during logout: ${error.message}`);
371
+ throw error;
372
+ }
373
+ }
374
+ // Multi-account support methods
375
+ async listAccounts() {
376
+ return await this.msalApp.getTokenCache().getAllAccounts();
377
+ }
378
+ async selectAccount(accountId) {
379
+ const accounts = await this.listAccounts();
380
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
381
+ if (!account) {
382
+ logger.error(`Account with ID ${accountId} not found`);
383
+ return false;
384
+ }
385
+ this.selectedAccountId = accountId;
386
+ await this.saveSelectedAccount();
387
+ this.accessToken = null;
388
+ this.tokenExpiry = null;
389
+ logger.info(`Selected account: ${account.username} (${accountId})`);
390
+ return true;
391
+ }
392
+ async removeAccount(accountId) {
393
+ const accounts = await this.listAccounts();
394
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
395
+ if (!account) {
396
+ logger.error(`Account with ID ${accountId} not found`);
397
+ return false;
398
+ }
399
+ try {
400
+ await this.msalApp.getTokenCache().removeAccount(account);
401
+ if (this.selectedAccountId === accountId) {
402
+ this.selectedAccountId = null;
403
+ await this.saveSelectedAccount();
404
+ this.accessToken = null;
405
+ this.tokenExpiry = null;
406
+ }
407
+ logger.info(`Removed account: ${account.username} (${accountId})`);
408
+ return true;
409
+ } catch (error) {
410
+ logger.error(`Failed to remove account ${accountId}: ${error.message}`);
411
+ return false;
412
+ }
413
+ }
414
+ getSelectedAccountId() {
415
+ return this.selectedAccountId;
416
+ }
417
+ }
418
+ var auth_default = AuthManager;
419
+ export {
420
+ buildScopesFromEndpoints,
421
+ auth_default as default
422
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { getCombinedPresetPattern, listPresets, presetRequiresOrgMode } from "./tool-categories.js";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const packageJsonPath = path.join(__dirname, "..", "package.json");
8
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
9
+ const version = packageJson.version;
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 using device code flow").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("--read-only", "Start server in read-only mode, disabling write operations").option(
12
+ "--http [address]",
13
+ 'Use Streamable HTTP transport instead of stdio. Format: [host:]port (e.g., "localhost:3000", ":3000", "3000"). Default: all interfaces on port 3000'
14
+ ).option(
15
+ "--enable-auth-tools",
16
+ "Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)"
17
+ ).option(
18
+ "--enabled-tools <pattern>",
19
+ 'Filter tools using regex pattern (e.g., "excel|contact" to enable Excel and Contact tools)'
20
+ ).option(
21
+ "--preset <names>",
22
+ "Use preset tool categories (comma-separated). Available: mail, calendar, files, personal, work, excel, contacts, tasks, onenote, search, users, all"
23
+ ).option("--list-presets", "List all available presets and exit").option(
24
+ "--org-mode",
25
+ "Enable organization/work mode from start (includes Teams, SharePoint, etc.)"
26
+ ).option("--work-mode", "Alias for --org-mode").option("--force-work-scopes", "Backwards compatibility alias for --org-mode (deprecated)").option("--toon", "(experimental) Enable TOON output format for 30-60% token reduction").option("--discovery", "Enable runtime tool discovery and loading (experimental feature)").option("--cloud <type>", "Microsoft cloud environment: global (default) or china (21Vianet)").option(
27
+ "--enable-dynamic-registration",
28
+ "Enable OAuth Dynamic Client Registration endpoint (required for some MCP clients like Open WebUI)"
29
+ );
30
+ function parseArgs() {
31
+ program.parse();
32
+ const options = program.opts();
33
+ if (options.listPresets) {
34
+ const presets = listPresets();
35
+ console.log(JSON.stringify({ presets }, null, 2));
36
+ process.exit(0);
37
+ }
38
+ if (options.preset) {
39
+ const presetNames = options.preset.split(",").map((p) => p.trim());
40
+ try {
41
+ options.enabledTools = getCombinedPresetPattern(presetNames);
42
+ const requiresOrgMode = presetNames.some((preset) => presetRequiresOrgMode(preset));
43
+ if (requiresOrgMode && !options.orgMode) {
44
+ console.warn(
45
+ `Warning: Preset(s) [${presetNames.filter((p) => presetRequiresOrgMode(p)).join(", ")}] require --org-mode to function properly`
46
+ );
47
+ }
48
+ } catch (error) {
49
+ console.error(`Error: ${error.message}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+ if (process.env.READ_ONLY === "true" || process.env.READ_ONLY === "1") {
54
+ options.readOnly = true;
55
+ }
56
+ if (process.env.ENABLED_TOOLS) {
57
+ options.enabledTools = process.env.ENABLED_TOOLS;
58
+ }
59
+ if (process.env.MS365_MCP_ORG_MODE === "true" || process.env.MS365_MCP_ORG_MODE === "1") {
60
+ options.orgMode = true;
61
+ }
62
+ if (process.env.MS365_MCP_FORCE_WORK_SCOPES === "true" || process.env.MS365_MCP_FORCE_WORK_SCOPES === "1") {
63
+ options.forceWorkScopes = true;
64
+ }
65
+ if (options.workMode || options.forceWorkScopes) {
66
+ options.orgMode = true;
67
+ }
68
+ if (process.env.MS365_MCP_OUTPUT_FORMAT === "toon") {
69
+ options.toon = true;
70
+ }
71
+ if (options.cloud) {
72
+ process.env.MS365_MCP_CLOUD_TYPE = options.cloud;
73
+ }
74
+ return options;
75
+ }
76
+ export {
77
+ parseArgs
78
+ };