@bugroger/lokka 0.3.5

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/build/main.js ADDED
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { Client, PageIterator } from "@microsoft/microsoft-graph-client";
6
+ import fetch from 'isomorphic-fetch'; // Required polyfill for Graph client
7
+ import { logger } from "./logger.js";
8
+ import { AuthManager, AuthMode } from "./auth.js";
9
+ import { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri, LokkaTokenPath, getDefaultGraphApiVersion } from "./constants.js";
10
+ import fs from "fs";
11
+ // Set up global fetch for the Microsoft Graph client
12
+ global.fetch = fetch;
13
+ // Create server instance
14
+ const server = new McpServer({
15
+ name: "Lokka-Microsoft",
16
+ version: "0.2.0", // Updated version for token-based auth support
17
+ });
18
+ logger.info("Starting Lokka Multi-Microsoft API MCP Server (v0.2.0 - Token-Based Auth Support)");
19
+ // Initialize authentication and clients
20
+ let authManager = null;
21
+ let graphClient = null;
22
+ // Check USE_GRAPH_BETA environment variable
23
+ const useGraphBeta = process.env.USE_GRAPH_BETA !== 'false'; // Default to true unless explicitly set to 'false'
24
+ const defaultGraphApiVersion = getDefaultGraphApiVersion();
25
+ logger.info(`Graph API default version: ${defaultGraphApiVersion} (USE_GRAPH_BETA=${process.env.USE_GRAPH_BETA || 'undefined'})`);
26
+ server.tool("Lokka-Microsoft", "A versatile tool to interact with Microsoft APIs including Microsoft Graph (Entra) and Azure Resource Management. IMPORTANT: For Graph API GET requests using advanced query parameters ($filter, $count, $search, $orderby), you are ADVISED to set 'consistencyLevel: \"eventual\"'.", {
27
+ apiType: z.enum(["graph", "azure"]).describe("Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management."),
28
+ path: z.string().describe("The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions')"),
29
+ method: z.enum(["get", "post", "put", "patch", "delete"]).describe("HTTP method to use"),
30
+ apiVersion: z.string().optional().describe("Azure Resource Management API version (required for apiType Azure)"),
31
+ subscriptionId: z.string().optional().describe("Azure Subscription ID (for Azure Resource Management)."),
32
+ queryParams: z.record(z.string()).optional().describe("Query parameters for the request"),
33
+ body: z.record(z.string(), z.any()).optional().describe("The request body (for POST, PUT, PATCH)"),
34
+ graphApiVersion: z.enum(["v1.0", "beta"]).optional().default(defaultGraphApiVersion).describe(`Microsoft Graph API version to use (default: ${defaultGraphApiVersion})`),
35
+ fetchAll: z.boolean().optional().default(false).describe("Set to true to automatically fetch all pages for list results (e.g., users, groups). Default is false."),
36
+ consistencyLevel: z.string().optional().describe("Graph API ConsistencyLevel header. ADVISED to be set to 'eventual' for Graph GET requests using advanced query parameters ($filter, $count, $search, $orderby)."),
37
+ }, async ({ apiType, path, method, apiVersion, subscriptionId, queryParams, body, graphApiVersion, fetchAll, consistencyLevel }) => {
38
+ // Override graphApiVersion if USE_GRAPH_BETA is explicitly set to false
39
+ const effectiveGraphApiVersion = !useGraphBeta ? "v1.0" : graphApiVersion;
40
+ logger.info(`Executing Lokka-Microsoft tool with params: apiType=${apiType}, path=${path}, method=${method}, graphApiVersion=${effectiveGraphApiVersion}, fetchAll=${fetchAll}, consistencyLevel=${consistencyLevel}`);
41
+ let determinedUrl;
42
+ try {
43
+ let responseData;
44
+ // --- Microsoft Graph Logic ---
45
+ if (apiType === 'graph') {
46
+ if (!graphClient) {
47
+ throw new Error("Graph client not initialized");
48
+ }
49
+ determinedUrl = `https://graph.microsoft.com/${effectiveGraphApiVersion}`; // For error reporting
50
+ // Construct the request using the Graph SDK client
51
+ let request = graphClient.api(path).version(effectiveGraphApiVersion);
52
+ // Add query parameters if provided and not empty
53
+ if (queryParams && Object.keys(queryParams).length > 0) {
54
+ request = request.query(queryParams);
55
+ }
56
+ // Add ConsistencyLevel header if provided
57
+ if (consistencyLevel) {
58
+ request = request.header('ConsistencyLevel', consistencyLevel);
59
+ logger.info(`Added ConsistencyLevel header: ${consistencyLevel}`);
60
+ }
61
+ // Handle different methods
62
+ switch (method.toLowerCase()) {
63
+ case 'get':
64
+ if (fetchAll) {
65
+ logger.info(`Fetching all pages for Graph path: ${path}`);
66
+ // Fetch the first page to get context and initial data
67
+ const firstPageResponse = await request.get();
68
+ const odataContext = firstPageResponse['@odata.context']; // Capture context from first page
69
+ let allItems = firstPageResponse.value || []; // Initialize with first page's items
70
+ // Callback function to process subsequent pages
71
+ const callback = (item) => {
72
+ allItems.push(item);
73
+ return true; // Return true to continue iteration
74
+ };
75
+ // Create a PageIterator starting from the first response
76
+ const pageIterator = new PageIterator(graphClient, firstPageResponse, callback);
77
+ // Iterate over all remaining pages
78
+ await pageIterator.iterate();
79
+ // Construct final response with context and combined values under 'value' key
80
+ responseData = {
81
+ '@odata.context': odataContext,
82
+ value: allItems
83
+ };
84
+ logger.info(`Finished fetching all Graph pages. Total items: ${allItems.length}`);
85
+ }
86
+ else {
87
+ logger.info(`Fetching single page for Graph path: ${path}`);
88
+ responseData = await request.get();
89
+ }
90
+ break;
91
+ case 'post':
92
+ responseData = await request.post(body ?? {});
93
+ break;
94
+ case 'put':
95
+ responseData = await request.put(body ?? {});
96
+ break;
97
+ case 'patch':
98
+ responseData = await request.patch(body ?? {});
99
+ break;
100
+ case 'delete':
101
+ responseData = await request.delete(); // Delete often returns no body or 204
102
+ // Handle potential 204 No Content response
103
+ if (responseData === undefined || responseData === null) {
104
+ responseData = { status: "Success (No Content)" };
105
+ }
106
+ break;
107
+ default:
108
+ throw new Error(`Unsupported method: ${method}`);
109
+ }
110
+ } // --- Azure Resource Management Logic (using direct fetch) ---
111
+ else { // apiType === 'azure'
112
+ if (!authManager) {
113
+ throw new Error("Auth manager not initialized");
114
+ }
115
+ determinedUrl = "https://management.azure.com"; // For error reporting
116
+ // Acquire token for Azure RM
117
+ const azureCredential = authManager.getAzureCredential();
118
+ const tokenResponse = await azureCredential.getToken("https://management.azure.com/.default");
119
+ if (!tokenResponse || !tokenResponse.token) {
120
+ throw new Error("Failed to acquire Azure access token");
121
+ }
122
+ // Construct the URL (similar to previous implementation)
123
+ let url = determinedUrl;
124
+ if (subscriptionId) {
125
+ url += `/subscriptions/${subscriptionId}`;
126
+ }
127
+ url += path;
128
+ if (!apiVersion) {
129
+ throw new Error("API version is required for Azure Resource Management queries");
130
+ }
131
+ const urlParams = new URLSearchParams({ 'api-version': apiVersion });
132
+ if (queryParams) {
133
+ for (const [key, value] of Object.entries(queryParams)) {
134
+ urlParams.append(String(key), String(value));
135
+ }
136
+ }
137
+ url += `?${urlParams.toString()}`;
138
+ // Prepare request options
139
+ const headers = {
140
+ 'Authorization': `Bearer ${tokenResponse.token}`,
141
+ 'Content-Type': 'application/json'
142
+ };
143
+ const requestOptions = {
144
+ method: method.toUpperCase(),
145
+ headers: headers
146
+ };
147
+ if (["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
148
+ requestOptions.body = body ? JSON.stringify(body) : JSON.stringify({});
149
+ }
150
+ // --- Pagination Logic for Azure RM (Manual Fetch) ---
151
+ if (fetchAll && method === 'get') {
152
+ logger.info(`Fetching all pages for Azure RM starting from: ${url}`);
153
+ let allValues = [];
154
+ let currentUrl = url;
155
+ while (currentUrl) {
156
+ logger.info(`Fetching Azure RM page: ${currentUrl}`);
157
+ // Re-acquire token for each page (Azure tokens might expire)
158
+ const azureCredential = authManager.getAzureCredential();
159
+ const currentPageTokenResponse = await azureCredential.getToken("https://management.azure.com/.default");
160
+ if (!currentPageTokenResponse || !currentPageTokenResponse.token) {
161
+ throw new Error("Failed to acquire Azure access token during pagination");
162
+ }
163
+ const currentPageHeaders = { ...headers, 'Authorization': `Bearer ${currentPageTokenResponse.token}` };
164
+ const currentPageRequestOptions = { method: 'GET', headers: currentPageHeaders };
165
+ const pageResponse = await fetch(currentUrl, currentPageRequestOptions);
166
+ const pageText = await pageResponse.text();
167
+ let pageData;
168
+ try {
169
+ pageData = pageText ? JSON.parse(pageText) : {};
170
+ }
171
+ catch (e) {
172
+ logger.error(`Failed to parse JSON from Azure RM page: ${currentUrl}`, pageText);
173
+ pageData = { rawResponse: pageText };
174
+ }
175
+ if (!pageResponse.ok) {
176
+ logger.error(`API error on Azure RM page ${currentUrl}:`, pageData);
177
+ throw new Error(`API error (${pageResponse.status}) during Azure RM pagination on ${currentUrl}: ${JSON.stringify(pageData)}`);
178
+ }
179
+ if (pageData.value && Array.isArray(pageData.value)) {
180
+ allValues = allValues.concat(pageData.value);
181
+ }
182
+ else if (currentUrl === url && !pageData.nextLink) {
183
+ allValues.push(pageData);
184
+ }
185
+ else if (currentUrl !== url) {
186
+ logger.info(`[Warning] Azure RM response from ${currentUrl} did not contain a 'value' array.`);
187
+ }
188
+ currentUrl = pageData.nextLink || null; // Azure uses nextLink
189
+ }
190
+ responseData = { allValues: allValues };
191
+ logger.info(`Finished fetching all Azure RM pages. Total items: ${allValues.length}`);
192
+ }
193
+ else {
194
+ // Single page fetch for Azure RM
195
+ logger.info(`Fetching single page for Azure RM: ${url}`);
196
+ const apiResponse = await fetch(url, requestOptions);
197
+ const responseText = await apiResponse.text();
198
+ try {
199
+ responseData = responseText ? JSON.parse(responseText) : {};
200
+ }
201
+ catch (e) {
202
+ logger.error(`Failed to parse JSON from single Azure RM page: ${url}`, responseText);
203
+ responseData = { rawResponse: responseText };
204
+ }
205
+ if (!apiResponse.ok) {
206
+ logger.error(`API error for Azure RM ${method} ${path}:`, responseData);
207
+ throw new Error(`API error (${apiResponse.status}) for Azure RM: ${JSON.stringify(responseData)}`);
208
+ }
209
+ }
210
+ }
211
+ // --- Format and Return Result ---
212
+ // For all requests, format as text
213
+ let resultText = `Result for ${apiType} API (${apiType === 'graph' ? effectiveGraphApiVersion : apiVersion}) - ${method} ${path}:\n\n`;
214
+ resultText += JSON.stringify(responseData, null, 2); // responseData already contains the correct structure for fetchAll Graph case
215
+ // Add pagination note if applicable (only for single page GET)
216
+ if (!fetchAll && method === 'get') {
217
+ const nextLinkKey = apiType === 'graph' ? '@odata.nextLink' : 'nextLink';
218
+ if (responseData && responseData[nextLinkKey]) { // Added check for responseData existence
219
+ resultText += `\n\nNote: More results are available. To retrieve all pages, add the parameter 'fetchAll: true' to your request.`;
220
+ }
221
+ }
222
+ return {
223
+ content: [{ type: "text", text: resultText }],
224
+ };
225
+ }
226
+ catch (error) {
227
+ logger.error(`Error in Lokka-Microsoft tool (apiType: ${apiType}, path: ${path}, method: ${method}):`, error); // Added more context to error log
228
+ // Try to determine the base URL even in case of error
229
+ if (!determinedUrl) {
230
+ determinedUrl = apiType === 'graph'
231
+ ? `https://graph.microsoft.com/${effectiveGraphApiVersion}`
232
+ : "https://management.azure.com";
233
+ }
234
+ // Include error body if available from Graph SDK error
235
+ const errorBody = error.body ? (typeof error.body === 'string' ? error.body : JSON.stringify(error.body)) : 'N/A';
236
+ return {
237
+ content: [{
238
+ type: "text",
239
+ text: JSON.stringify({
240
+ error: error instanceof Error ? error.message : String(error),
241
+ statusCode: error.statusCode || 'N/A', // Include status code if available from SDK error
242
+ errorBody: errorBody,
243
+ attemptedBaseUrl: determinedUrl
244
+ }),
245
+ }],
246
+ isError: true
247
+ };
248
+ }
249
+ });
250
+ // Add token management tools
251
+ server.tool("set-access-token", "Set or update the access token for Microsoft Graph authentication. Use this when the MCP Client has obtained a fresh token through interactive authentication.", {
252
+ accessToken: z.string().describe("The access token obtained from Microsoft Graph authentication"),
253
+ expiresOn: z.string().optional().describe("Token expiration time in ISO format (optional, defaults to 1 hour from now)")
254
+ }, async ({ accessToken, expiresOn }) => {
255
+ try {
256
+ const expirationDate = expiresOn ? new Date(expiresOn) : undefined;
257
+ if (authManager?.getAuthMode() === AuthMode.ClientProvidedToken) {
258
+ authManager.updateAccessToken(accessToken, expirationDate);
259
+ // Reinitialize the Graph client with the new token
260
+ const authProvider = authManager.getGraphAuthProvider();
261
+ graphClient = Client.initWithMiddleware({
262
+ authProvider: authProvider,
263
+ });
264
+ return {
265
+ content: [{
266
+ type: "text",
267
+ text: "Access token updated successfully. You can now make Microsoft Graph requests on behalf of the authenticated user."
268
+ }],
269
+ };
270
+ }
271
+ else {
272
+ return {
273
+ content: [{
274
+ type: "text",
275
+ text: "Error: MCP Server is not configured for client-provided token authentication. Set USE_CLIENT_TOKEN=true in environment variables."
276
+ }],
277
+ isError: true
278
+ };
279
+ }
280
+ }
281
+ catch (error) {
282
+ logger.error("Error setting access token:", error);
283
+ return {
284
+ content: [{
285
+ type: "text",
286
+ text: `Error setting access token: ${error.message}`
287
+ }],
288
+ isError: true
289
+ };
290
+ }
291
+ });
292
+ server.tool("get-auth-status", "Check the current authentication status and mode of the MCP Server and also returns the current graph permission scopes of the access token for the current session.", {}, async () => {
293
+ try {
294
+ const authMode = authManager?.getAuthMode() || "Not initialized";
295
+ const isReady = authManager !== null;
296
+ const tokenStatus = authManager ? await authManager.getTokenStatus() : { isExpired: false };
297
+ return {
298
+ content: [{
299
+ type: "text",
300
+ text: JSON.stringify({
301
+ authMode,
302
+ isReady,
303
+ supportsTokenUpdates: authMode === AuthMode.ClientProvidedToken,
304
+ tokenStatus: tokenStatus,
305
+ timestamp: new Date().toISOString()
306
+ }, null, 2)
307
+ }],
308
+ };
309
+ }
310
+ catch (error) {
311
+ return {
312
+ content: [{
313
+ type: "text",
314
+ text: `Error checking auth status: ${error.message}`
315
+ }],
316
+ isError: true
317
+ };
318
+ }
319
+ });
320
+ // Add tool for requesting additional Graph permissions
321
+ server.tool("add-graph-permission", "Request additional Microsoft Graph permission scopes by performing a fresh interactive sign-in. This tool only works in interactive authentication mode and should be used if any Graph API call returns permissions related errors.", {
322
+ scopes: z.array(z.string()).describe("Array of Microsoft Graph permission scopes to request (e.g., ['User.Read', 'Mail.ReadWrite', 'Directory.Read.All'])")
323
+ }, async ({ scopes }) => {
324
+ try {
325
+ // Check if we're in interactive mode
326
+ if (!authManager || authManager.getAuthMode() !== AuthMode.Interactive) {
327
+ const currentMode = authManager?.getAuthMode() || "Not initialized";
328
+ const clientId = process.env.CLIENT_ID;
329
+ let errorMessage = `Error: add-graph-permission tool is only available in interactive authentication mode. Current mode: ${currentMode}.\n\n`;
330
+ if (currentMode === AuthMode.ClientCredentials) {
331
+ errorMessage += `📋 To add permissions in Client Credentials mode:\n`;
332
+ errorMessage += `1. Open the Microsoft Entra admin center (https://entra.microsoft.com)\n`;
333
+ errorMessage += `2. Navigate to Applications > App registrations\n`;
334
+ errorMessage += `3. Find your application${clientId ? ` (Client ID: ${clientId})` : ''}\n`;
335
+ errorMessage += `4. Go to API permissions\n`;
336
+ errorMessage += `5. Click "Add a permission" and select Microsoft Graph\n`;
337
+ errorMessage += `6. Choose "Application permissions" and add the required scopes:\n`;
338
+ errorMessage += ` ${scopes.map(scope => `• ${scope}`).join('\n ')}\n`;
339
+ errorMessage += `7. Click "Grant admin consent" to approve the permissions\n`;
340
+ errorMessage += `8. Restart the MCP server to use the new permissions`;
341
+ }
342
+ else if (currentMode === AuthMode.ClientProvidedToken) {
343
+ errorMessage += `📋 To add permissions in Client Provided Token mode:\n`;
344
+ errorMessage += `1. Obtain a new access token that includes the required scopes:\n`;
345
+ errorMessage += ` ${scopes.map(scope => `• ${scope}`).join('\n ')}\n`;
346
+ errorMessage += `2. When obtaining the token, ensure these scopes are included in the consent prompt\n`;
347
+ errorMessage += `3. Use the set-access-token tool to update the server with the new token\n`;
348
+ errorMessage += `4. The new token will include the additional permissions`;
349
+ }
350
+ else {
351
+ errorMessage += `To use interactive permission requests, set USE_INTERACTIVE=true in environment variables and restart the server.`;
352
+ }
353
+ return {
354
+ content: [{
355
+ type: "text",
356
+ text: errorMessage
357
+ }],
358
+ isError: true
359
+ };
360
+ }
361
+ // Validate scopes array
362
+ if (!scopes || scopes.length === 0) {
363
+ return {
364
+ content: [{
365
+ type: "text",
366
+ text: "Error: At least one permission scope must be specified."
367
+ }],
368
+ isError: true
369
+ };
370
+ }
371
+ // Validate scope format (basic validation)
372
+ const invalidScopes = scopes.filter(scope => !scope.includes('.') || scope.trim() !== scope);
373
+ if (invalidScopes.length > 0) {
374
+ return {
375
+ content: [{
376
+ type: "text",
377
+ text: `Error: Invalid scope format detected: ${invalidScopes.join(', ')}. Scopes should be in format like 'User.Read' or 'Mail.ReadWrite'.`
378
+ }],
379
+ isError: true
380
+ };
381
+ }
382
+ logger.info(`Requesting additional Graph permissions: ${scopes.join(', ')}`);
383
+ // Get current configuration with defaults for interactive auth
384
+ const tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;
385
+ const clientId = process.env.CLIENT_ID || LokkaClientId;
386
+ const redirectUri = process.env.REDIRECT_URI || LokkaDefaultRedirectUri;
387
+ logger.info(`Using tenant ID: ${tenantId}, client ID: ${clientId} for interactive authentication`);
388
+ // Create a new interactive credential with the requested scopes
389
+ const { InteractiveBrowserCredential, DeviceCodeCredential } = await import("@azure/identity");
390
+ // Clear any existing auth manager to force fresh authentication
391
+ authManager = null;
392
+ graphClient = null;
393
+ // Request token with the new scopes - this will trigger interactive authentication
394
+ const scopeString = scopes.map(scope => `https://graph.microsoft.com/${scope}`).join(' ');
395
+ logger.info(`Requesting fresh token with scopes: ${scopeString}`);
396
+ console.log(`\n🔐 Requesting Additional Graph Permissions:`);
397
+ console.log(`Scopes: ${scopes.join(', ')}`);
398
+ console.log(`You will be prompted to sign in to grant these permissions.\n`);
399
+ let newCredential;
400
+ let tokenResponse;
401
+ try {
402
+ // Try Interactive Browser first - create fresh instance each time
403
+ newCredential = new InteractiveBrowserCredential({
404
+ tenantId: tenantId,
405
+ clientId: clientId,
406
+ redirectUri: redirectUri,
407
+ });
408
+ // Request token immediately after creating credential
409
+ tokenResponse = await newCredential.getToken(scopeString);
410
+ }
411
+ catch (error) {
412
+ // Fallback to Device Code flow
413
+ logger.info("Interactive browser failed, falling back to device code flow");
414
+ newCredential = new DeviceCodeCredential({
415
+ tenantId: tenantId,
416
+ clientId: clientId,
417
+ userPromptCallback: (info) => {
418
+ console.log(`\n🔐 Additional Permissions Required:`);
419
+ console.log(`Please visit: ${info.verificationUri}`);
420
+ console.log(`And enter code: ${info.userCode}`);
421
+ console.log(`Requested scopes: ${scopes.join(', ')}\n`);
422
+ return Promise.resolve();
423
+ },
424
+ });
425
+ // Request token with device code credential
426
+ tokenResponse = await newCredential.getToken(scopeString);
427
+ }
428
+ if (!tokenResponse) {
429
+ return {
430
+ content: [{
431
+ type: "text",
432
+ text: "Error: Failed to acquire access token with the requested scopes. Please check your permissions and try again."
433
+ }],
434
+ isError: true
435
+ };
436
+ }
437
+ // Create a completely new auth manager instance with the updated credential
438
+ const authConfig = {
439
+ mode: AuthMode.Interactive,
440
+ tenantId,
441
+ clientId,
442
+ redirectUri
443
+ };
444
+ // Create a new auth manager instance
445
+ authManager = new AuthManager(authConfig);
446
+ // Manually set the credential to our new one with the additional scopes
447
+ authManager.credential = newCredential;
448
+ // DO NOT call initialize() as it might interfere with our fresh token
449
+ // Instead, directly create the Graph client with the new credential
450
+ const authProvider = authManager.getGraphAuthProvider();
451
+ graphClient = Client.initWithMiddleware({
452
+ authProvider: authProvider,
453
+ });
454
+ // Get the token status to show the new scopes
455
+ const tokenStatus = await authManager.getTokenStatus();
456
+ logger.info(`Successfully acquired fresh token with additional scopes: ${scopes.join(', ')}`);
457
+ return {
458
+ content: [{
459
+ type: "text",
460
+ text: JSON.stringify({
461
+ message: "Successfully acquired additional Microsoft Graph permissions with fresh authentication",
462
+ requestedScopes: scopes,
463
+ tokenStatus: tokenStatus,
464
+ note: "A fresh sign-in was performed to ensure the new permissions are properly granted",
465
+ timestamp: new Date().toISOString()
466
+ }, null, 2)
467
+ }],
468
+ };
469
+ }
470
+ catch (error) {
471
+ logger.error("Error requesting additional Graph permissions:", error);
472
+ return {
473
+ content: [{
474
+ type: "text",
475
+ text: `Error requesting additional permissions: ${error.message}`
476
+ }],
477
+ isError: true
478
+ };
479
+ }
480
+ });
481
+ // Start the server with stdio transport
482
+ async function main() {
483
+ // Determine authentication mode based on environment variables
484
+ const useCertificate = process.env.USE_CERTIFICATE === 'true';
485
+ const useInteractive = process.env.USE_INTERACTIVE === 'true';
486
+ const useClientToken = process.env.USE_CLIENT_TOKEN === 'true';
487
+ const initialAccessToken = process.env.ACCESS_TOKEN;
488
+ let authMode;
489
+ // Ensure only one authentication mode is enabled at a time
490
+ const enabledModes = [
491
+ useClientToken,
492
+ useInteractive,
493
+ useCertificate
494
+ ].filter(Boolean);
495
+ if (enabledModes.length > 1) {
496
+ throw new Error("Multiple authentication modes enabled. Please enable only one of USE_CLIENT_TOKEN, USE_INTERACTIVE, or USE_CERTIFICATE.");
497
+ }
498
+ if (useClientToken) {
499
+ authMode = AuthMode.ClientProvidedToken;
500
+ if (!initialAccessToken) {
501
+ logger.info("Client token mode enabled but no initial token provided. Token must be set via set-access-token tool.");
502
+ }
503
+ }
504
+ else if (useInteractive) {
505
+ authMode = AuthMode.Interactive;
506
+ }
507
+ else if (useCertificate) {
508
+ authMode = AuthMode.Certificate;
509
+ }
510
+ else {
511
+ // Check if we have client credentials environment variables
512
+ const hasClientCredentials = process.env.TENANT_ID && process.env.CLIENT_ID && process.env.CLIENT_SECRET;
513
+ if (hasClientCredentials) {
514
+ authMode = AuthMode.ClientCredentials;
515
+ }
516
+ else {
517
+ // Default to persistent token mode — caches tokens to disk and refreshes silently.
518
+ // On first run (no token file), it will open a browser for interactive auth.
519
+ authMode = AuthMode.PersistentToken;
520
+ const hasTokenFile = fs.existsSync(LokkaTokenPath);
521
+ logger.info(hasTokenFile
522
+ ? "Found cached token file, using persistent token mode (silent refresh)."
523
+ : "No cached token found, persistent token mode will open browser for initial auth.");
524
+ }
525
+ }
526
+ logger.info(`Starting with authentication mode: ${authMode}`);
527
+ // Get tenant ID and client ID with defaults only for interactive mode
528
+ let tenantId;
529
+ let clientId;
530
+ if (authMode === AuthMode.Interactive || authMode === AuthMode.PersistentToken) {
531
+ // Interactive and persistent token modes can use defaults
532
+ tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;
533
+ clientId = process.env.CLIENT_ID || LokkaClientId;
534
+ logger.info(`${authMode} mode using tenant ID: ${tenantId}, client ID: ${clientId}`);
535
+ }
536
+ else {
537
+ // All other modes require explicit values from environment variables
538
+ tenantId = process.env.TENANT_ID;
539
+ clientId = process.env.CLIENT_ID;
540
+ }
541
+ const clientSecret = process.env.CLIENT_SECRET;
542
+ const certificatePath = process.env.CERTIFICATE_PATH;
543
+ const certificatePassword = process.env.CERTIFICATE_PASSWORD; // optional
544
+ // Validate required configuration
545
+ if (authMode === AuthMode.ClientCredentials) {
546
+ if (!tenantId || !clientId || !clientSecret) {
547
+ throw new Error("Client credentials mode requires explicit TENANT_ID, CLIENT_ID, and CLIENT_SECRET environment variables");
548
+ }
549
+ }
550
+ else if (authMode === AuthMode.Certificate) {
551
+ if (!tenantId || !clientId || !certificatePath) {
552
+ throw new Error("Certificate mode requires explicit TENANT_ID, CLIENT_ID, and CERTIFICATE_PATH environment variables");
553
+ }
554
+ }
555
+ // Note: Client token mode can start without a token and receive it later
556
+ const authConfig = {
557
+ mode: authMode,
558
+ tenantId,
559
+ clientId,
560
+ clientSecret,
561
+ accessToken: initialAccessToken,
562
+ redirectUri: process.env.REDIRECT_URI,
563
+ certificatePath,
564
+ certificatePassword
565
+ };
566
+ authManager = new AuthManager(authConfig);
567
+ // Only initialize if we have required config (for client token mode, we can start without a token)
568
+ if (authMode !== AuthMode.ClientProvidedToken || initialAccessToken) {
569
+ await authManager.initialize();
570
+ // Initialize Graph Client
571
+ const authProvider = authManager.getGraphAuthProvider();
572
+ graphClient = Client.initWithMiddleware({
573
+ authProvider: authProvider,
574
+ });
575
+ logger.info(`Authentication initialized successfully using ${authMode} mode`);
576
+ }
577
+ else {
578
+ logger.info("Started in client token mode. Use set-access-token tool to provide authentication token.");
579
+ }
580
+ const transport = new StdioServerTransport();
581
+ await server.connect(transport);
582
+ }
583
+ main().catch((error) => {
584
+ console.error("Fatal error in main():", error);
585
+ logger.error("Fatal error in main()", error);
586
+ process.exit(1);
587
+ });