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.
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/bin/cli.js +247 -0
- package/dist/index.js +1251 -0
- package/dist/utils/api.js +43 -0
- package/dist/utils/credential-store.js +258 -0
- package/dist/utils/ms365-auth-enhanced.js +639 -0
- package/dist/utils/ms365-auth.js +363 -0
- package/dist/utils/ms365-operations.js +644 -0
- package/dist/utils/multi-user-auth.js +359 -0
- package/install.js +41 -0
- package/package.json +59 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { ConfidentialClientApplication, PublicClientApplication } from '@azure/msal-node';
|
|
2
|
+
import { Client } from '@microsoft/microsoft-graph-client';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
import { URL } from 'url';
|
|
9
|
+
import { logger } from './api.js';
|
|
10
|
+
import { credentialStore } from './credential-store.js';
|
|
11
|
+
// Scopes required for Microsoft 365 operations
|
|
12
|
+
const SCOPES = [
|
|
13
|
+
'https://graph.microsoft.com/Mail.ReadWrite',
|
|
14
|
+
'https://graph.microsoft.com/Mail.Send',
|
|
15
|
+
'https://graph.microsoft.com/MailboxSettings.Read',
|
|
16
|
+
'https://graph.microsoft.com/Contacts.Read',
|
|
17
|
+
'https://graph.microsoft.com/User.Read',
|
|
18
|
+
'offline_access'
|
|
19
|
+
];
|
|
20
|
+
// Built-in application for easier setup (similar to Softeria's approach)
|
|
21
|
+
const BUILTIN_CLIENT_ID = "14d82eec-204b-4c2f-b7e8-296a70dab67e"; // Microsoft Graph Command Line Tools
|
|
22
|
+
const DEFAULT_TENANT_ID = "common";
|
|
23
|
+
// Configuration directory and file paths
|
|
24
|
+
const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
|
|
25
|
+
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
26
|
+
/**
|
|
27
|
+
* Enhanced Microsoft 365 authentication manager with device code flow support
|
|
28
|
+
*/
|
|
29
|
+
export class EnhancedMS365Auth {
|
|
30
|
+
constructor(authMethod = 'auto') {
|
|
31
|
+
this.msalClient = null;
|
|
32
|
+
this.credentials = null;
|
|
33
|
+
this.preferredAuthMethod = 'auto';
|
|
34
|
+
this.pendingAuth = null;
|
|
35
|
+
this.preferredAuthMethod = authMethod;
|
|
36
|
+
this.ensureConfigDir();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Ensure configuration directory exists
|
|
40
|
+
*/
|
|
41
|
+
ensureConfigDir() {
|
|
42
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
43
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
+
logger.log('Created MS365 MCP configuration directory');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load credentials from environment, file, or use built-in app
|
|
49
|
+
*/
|
|
50
|
+
async loadCredentials() {
|
|
51
|
+
try {
|
|
52
|
+
// Method 1: Environment variables (highest priority)
|
|
53
|
+
if (process.env.MS365_CLIENT_ID && process.env.MS365_TENANT_ID) {
|
|
54
|
+
this.credentials = {
|
|
55
|
+
clientId: process.env.MS365_CLIENT_ID,
|
|
56
|
+
clientSecret: process.env.MS365_CLIENT_SECRET,
|
|
57
|
+
tenantId: process.env.MS365_TENANT_ID,
|
|
58
|
+
redirectUri: process.env.MS365_REDIRECT_URI || 'http://localhost:44001/oauth2callback',
|
|
59
|
+
authType: process.env.MS365_CLIENT_SECRET ? 'redirect' : 'device'
|
|
60
|
+
};
|
|
61
|
+
logger.log('Loaded MS365 credentials from environment variables');
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// Method 2: Credentials file
|
|
65
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
66
|
+
const credentialsData = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
|
|
67
|
+
this.credentials = JSON.parse(credentialsData);
|
|
68
|
+
logger.log('Loaded MS365 credentials from file');
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Method 3: Built-in application (fallback)
|
|
72
|
+
this.credentials = {
|
|
73
|
+
clientId: BUILTIN_CLIENT_ID,
|
|
74
|
+
tenantId: DEFAULT_TENANT_ID,
|
|
75
|
+
authType: 'device'
|
|
76
|
+
};
|
|
77
|
+
logger.log('Using built-in MS365 application with device code flow');
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.error('Error loading MS365 credentials:', error);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Save credentials to file
|
|
87
|
+
*/
|
|
88
|
+
async saveCredentials(credentials) {
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
|
|
91
|
+
logger.log('Saved MS365 credentials to file');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.error('Error saving MS365 credentials:', error);
|
|
95
|
+
throw new Error('Failed to save credentials');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Initialize MSAL client based on auth type
|
|
100
|
+
*/
|
|
101
|
+
initializeMsalClient() {
|
|
102
|
+
if (!this.credentials) {
|
|
103
|
+
throw new Error('Credentials not loaded');
|
|
104
|
+
}
|
|
105
|
+
const isConfidential = this.credentials.clientSecret && this.credentials.authType === 'redirect';
|
|
106
|
+
if (isConfidential) {
|
|
107
|
+
// Confidential client for redirect-based auth
|
|
108
|
+
const config = {
|
|
109
|
+
auth: {
|
|
110
|
+
clientId: this.credentials.clientId,
|
|
111
|
+
clientSecret: this.credentials.clientSecret,
|
|
112
|
+
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
|
|
113
|
+
},
|
|
114
|
+
system: {
|
|
115
|
+
loggerOptions: {
|
|
116
|
+
loggerCallback: (level, message, containsPii) => {
|
|
117
|
+
if (!containsPii) {
|
|
118
|
+
logger.log(`MSAL: ${message}`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
piiLoggingEnabled: false,
|
|
122
|
+
logLevel: 3
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
this.msalClient = new ConfidentialClientApplication(config);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Public client for device code flow
|
|
130
|
+
const config = {
|
|
131
|
+
auth: {
|
|
132
|
+
clientId: this.credentials.clientId,
|
|
133
|
+
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
|
|
134
|
+
},
|
|
135
|
+
system: {
|
|
136
|
+
loggerOptions: {
|
|
137
|
+
loggerCallback: (level, message, containsPii) => {
|
|
138
|
+
if (!containsPii) {
|
|
139
|
+
logger.log(`MSAL: ${message}`);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
piiLoggingEnabled: false,
|
|
143
|
+
logLevel: 3
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
this.msalClient = new PublicClientApplication(config);
|
|
148
|
+
}
|
|
149
|
+
return this.msalClient;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Device code flow authentication
|
|
153
|
+
*/
|
|
154
|
+
async authenticateWithDeviceCode() {
|
|
155
|
+
if (!await this.loadCredentials()) {
|
|
156
|
+
throw new Error('MS365 credentials not configured');
|
|
157
|
+
}
|
|
158
|
+
const msalClient = this.initializeMsalClient();
|
|
159
|
+
const deviceCodeRequest = {
|
|
160
|
+
scopes: SCOPES,
|
|
161
|
+
deviceCodeCallback: (response) => {
|
|
162
|
+
// Display the device code to the user on stderr to avoid JSON-RPC conflicts
|
|
163
|
+
console.error('\n🔐 Microsoft 365 Authentication Required');
|
|
164
|
+
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
165
|
+
console.error(`📱 Please visit: ${response.verificationUri}`);
|
|
166
|
+
console.error(`🔑 Enter this code: ${response.userCode}`);
|
|
167
|
+
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
168
|
+
console.error('⏳ Waiting for authentication...\n');
|
|
169
|
+
logger.log(`Device code authentication: ${response.verificationUri} - ${response.userCode}`);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
const tokenResponse = await msalClient.acquireTokenByDeviceCode(deviceCodeRequest);
|
|
174
|
+
if (!tokenResponse) {
|
|
175
|
+
throw new Error('Failed to acquire token via device code');
|
|
176
|
+
}
|
|
177
|
+
await this.saveToken(tokenResponse, 'device');
|
|
178
|
+
logger.log('MS365 device code authentication successful');
|
|
179
|
+
console.error('✅ Authentication successful!\n');
|
|
180
|
+
return tokenResponse;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
logger.error('Device code authentication failed:', error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Redirect-based authentication (original method)
|
|
189
|
+
*/
|
|
190
|
+
async authenticateWithRedirect() {
|
|
191
|
+
if (!await this.loadCredentials()) {
|
|
192
|
+
throw new Error('MS365 credentials not configured');
|
|
193
|
+
}
|
|
194
|
+
if (!this.credentials?.clientSecret) {
|
|
195
|
+
throw new Error('Client secret required for redirect authentication');
|
|
196
|
+
}
|
|
197
|
+
const msalClient = this.initializeMsalClient();
|
|
198
|
+
try {
|
|
199
|
+
const authUrl = await msalClient.getAuthCodeUrl({
|
|
200
|
+
scopes: SCOPES,
|
|
201
|
+
redirectUri: this.credentials.redirectUri,
|
|
202
|
+
prompt: 'consent'
|
|
203
|
+
});
|
|
204
|
+
logger.log('Opening browser for authentication...');
|
|
205
|
+
const [authCode] = await Promise.all([
|
|
206
|
+
this.startCallbackServer(),
|
|
207
|
+
open(authUrl)
|
|
208
|
+
]);
|
|
209
|
+
const tokenResponse = await msalClient.acquireTokenByCode({
|
|
210
|
+
code: authCode,
|
|
211
|
+
scopes: SCOPES,
|
|
212
|
+
redirectUri: this.credentials.redirectUri
|
|
213
|
+
});
|
|
214
|
+
if (!tokenResponse) {
|
|
215
|
+
throw new Error('Failed to acquire token');
|
|
216
|
+
}
|
|
217
|
+
await this.saveToken(tokenResponse, 'redirect');
|
|
218
|
+
logger.log('MS365 redirect authentication successful');
|
|
219
|
+
return tokenResponse;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
logger.error('Redirect authentication failed:', error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Smart authentication that chooses the best method
|
|
228
|
+
*/
|
|
229
|
+
async authenticate() {
|
|
230
|
+
if (!await this.loadCredentials()) {
|
|
231
|
+
throw new Error('MS365 credentials not configured');
|
|
232
|
+
}
|
|
233
|
+
const authType = this.determineAuthType();
|
|
234
|
+
if (authType === 'device') {
|
|
235
|
+
return await this.authenticateWithDeviceCode();
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return await this.authenticateWithRedirect();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Determine the best authentication type
|
|
243
|
+
*/
|
|
244
|
+
determineAuthType() {
|
|
245
|
+
if (this.preferredAuthMethod === 'device') {
|
|
246
|
+
return 'device';
|
|
247
|
+
}
|
|
248
|
+
if (this.preferredAuthMethod === 'redirect') {
|
|
249
|
+
if (!this.credentials?.clientSecret) {
|
|
250
|
+
logger.log('No client secret available, falling back to device code flow');
|
|
251
|
+
return 'device';
|
|
252
|
+
}
|
|
253
|
+
return 'redirect';
|
|
254
|
+
}
|
|
255
|
+
// Auto mode: prefer device code for simplicity, redirect if client secret is available
|
|
256
|
+
if (this.credentials?.clientSecret && this.credentials?.redirectUri) {
|
|
257
|
+
return 'redirect';
|
|
258
|
+
}
|
|
259
|
+
return 'device';
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Save token using secure credential store
|
|
263
|
+
*/
|
|
264
|
+
async saveToken(token, authType) {
|
|
265
|
+
try {
|
|
266
|
+
const tokenData = {
|
|
267
|
+
accessToken: token.accessToken,
|
|
268
|
+
refreshToken: '',
|
|
269
|
+
expiresOn: token.expiresOn?.getTime() || 0,
|
|
270
|
+
account: token.account,
|
|
271
|
+
authType: authType
|
|
272
|
+
};
|
|
273
|
+
const accountKey = token.account?.username || 'default-user';
|
|
274
|
+
await credentialStore.setCredentials(accountKey, tokenData);
|
|
275
|
+
logger.log('Saved MS365 access token securely');
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
logger.error('Error saving token:', error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Load stored token using secure credential store
|
|
283
|
+
*/
|
|
284
|
+
async loadStoredToken(accountKey = 'default-user') {
|
|
285
|
+
try {
|
|
286
|
+
return await credentialStore.getCredentials(accountKey);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
logger.error('Error loading stored token:', error);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Start local server for OAuth2 callback (redirect auth)
|
|
295
|
+
*/
|
|
296
|
+
startCallbackServer() {
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
const server = createServer((req, res) => {
|
|
299
|
+
if (req.url?.startsWith('/oauth2callback')) {
|
|
300
|
+
const url = new URL(req.url, 'http://localhost:44001');
|
|
301
|
+
const code = url.searchParams.get('code');
|
|
302
|
+
const error = url.searchParams.get('error');
|
|
303
|
+
if (error) {
|
|
304
|
+
res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`);
|
|
305
|
+
server.close();
|
|
306
|
+
reject(new Error(`OAuth2 error: ${error}`));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (code) {
|
|
310
|
+
res.end(`<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>`);
|
|
311
|
+
server.close();
|
|
312
|
+
resolve(code);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
res.end('<html><body><h1>Invalid Request</h1></body></html>');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
res.end('<html><body><h1>MS365 MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>');
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
server.listen(44001, () => {
|
|
322
|
+
logger.log('OAuth2 callback server started on port 44001');
|
|
323
|
+
});
|
|
324
|
+
server.on('error', (err) => {
|
|
325
|
+
reject(err);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get authenticated Microsoft Graph client
|
|
331
|
+
*/
|
|
332
|
+
async getGraphClient(accountKey) {
|
|
333
|
+
// If no specific account key provided, use the first available account
|
|
334
|
+
if (!accountKey) {
|
|
335
|
+
const accounts = await this.listAuthenticatedAccounts();
|
|
336
|
+
if (accounts.length === 0) {
|
|
337
|
+
throw new Error('No authenticated accounts found. Please authenticate first.');
|
|
338
|
+
}
|
|
339
|
+
accountKey = accounts[0];
|
|
340
|
+
}
|
|
341
|
+
const storedToken = await this.loadStoredToken(accountKey);
|
|
342
|
+
if (!storedToken) {
|
|
343
|
+
throw new Error('No stored token found. Please authenticate first.');
|
|
344
|
+
}
|
|
345
|
+
// Check if token is expired
|
|
346
|
+
if (storedToken.expiresOn < Date.now()) {
|
|
347
|
+
await this.refreshToken(accountKey);
|
|
348
|
+
}
|
|
349
|
+
const client = Client.init({
|
|
350
|
+
authProvider: (done) => {
|
|
351
|
+
done(null, storedToken.accessToken);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
return client;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Refresh access token
|
|
358
|
+
*/
|
|
359
|
+
async refreshToken(accountKey = 'default-user') {
|
|
360
|
+
const storedToken = await this.loadStoredToken(accountKey);
|
|
361
|
+
if (!storedToken?.account) {
|
|
362
|
+
throw new Error('No account information available. Please re-authenticate.');
|
|
363
|
+
}
|
|
364
|
+
if (!await this.loadCredentials()) {
|
|
365
|
+
throw new Error('MS365 credentials not configured');
|
|
366
|
+
}
|
|
367
|
+
const msalClient = this.initializeMsalClient();
|
|
368
|
+
try {
|
|
369
|
+
const tokenResponse = await msalClient.acquireTokenSilent({
|
|
370
|
+
scopes: SCOPES,
|
|
371
|
+
account: storedToken.account
|
|
372
|
+
});
|
|
373
|
+
if (!tokenResponse) {
|
|
374
|
+
throw new Error('Failed to refresh token');
|
|
375
|
+
}
|
|
376
|
+
await this.saveToken(tokenResponse, storedToken.authType);
|
|
377
|
+
logger.log('MS365 token refreshed successfully');
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
logger.error('Token refresh failed:', error);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Check if user is authenticated
|
|
386
|
+
*/
|
|
387
|
+
async isAuthenticated(accountKey) {
|
|
388
|
+
// If no specific account key provided, check all available accounts
|
|
389
|
+
if (!accountKey) {
|
|
390
|
+
const accounts = await this.listAuthenticatedAccounts();
|
|
391
|
+
if (accounts.length === 0) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
// Check if any account has valid authentication
|
|
395
|
+
for (const account of accounts) {
|
|
396
|
+
if (await this.isAuthenticated(account)) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const storedToken = await this.loadStoredToken(accountKey);
|
|
403
|
+
if (!storedToken) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
// If token is expired, try to refresh
|
|
407
|
+
if (storedToken.expiresOn < Date.now()) {
|
|
408
|
+
try {
|
|
409
|
+
await this.refreshToken(accountKey);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
logger.error('Token refresh failed during authentication check:', error);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Check if credentials are configured
|
|
421
|
+
*/
|
|
422
|
+
async isConfigured() {
|
|
423
|
+
return await this.loadCredentials();
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Clear stored authentication data
|
|
427
|
+
*/
|
|
428
|
+
async resetAuth(accountKey) {
|
|
429
|
+
try {
|
|
430
|
+
if (accountKey) {
|
|
431
|
+
// Delete specific account
|
|
432
|
+
await credentialStore.deleteCredentials(accountKey);
|
|
433
|
+
logger.log(`Cleared stored authentication tokens for account: ${accountKey}`);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// Delete all authenticated accounts
|
|
437
|
+
const authenticatedAccounts = await this.listAuthenticatedAccounts();
|
|
438
|
+
if (authenticatedAccounts.length === 0) {
|
|
439
|
+
// If no accounts found by listing, try deleting the default-user key as fallback
|
|
440
|
+
await credentialStore.deleteCredentials('default-user');
|
|
441
|
+
logger.log('Cleared stored authentication tokens (fallback to default-user)');
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
// Delete all found accounts
|
|
445
|
+
for (const account of authenticatedAccounts) {
|
|
446
|
+
await credentialStore.deleteCredentials(account);
|
|
447
|
+
logger.log(`Cleared stored authentication tokens for account: ${account}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
logger.error('Error clearing authentication data:', error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Get authentication URL for device code flow
|
|
458
|
+
*/
|
|
459
|
+
async getDeviceCodeInfo() {
|
|
460
|
+
if (!await this.loadCredentials()) {
|
|
461
|
+
throw new Error('MS365 credentials not configured');
|
|
462
|
+
}
|
|
463
|
+
const msalClient = this.initializeMsalClient();
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
const deviceCodeRequest = {
|
|
466
|
+
scopes: SCOPES,
|
|
467
|
+
deviceCodeCallback: (response) => {
|
|
468
|
+
resolve({
|
|
469
|
+
verificationUri: response.verificationUri,
|
|
470
|
+
userCode: response.userCode,
|
|
471
|
+
message: response.message
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
// This will trigger the callback immediately without completing auth
|
|
476
|
+
msalClient.acquireTokenByDeviceCode(deviceCodeRequest).catch(reject);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Start device code authentication and return device code info immediately
|
|
481
|
+
*/
|
|
482
|
+
async startDeviceCodeAuth() {
|
|
483
|
+
if (!await this.loadCredentials()) {
|
|
484
|
+
throw new Error('MS365 credentials not configured');
|
|
485
|
+
}
|
|
486
|
+
const msalClient = this.initializeMsalClient();
|
|
487
|
+
return new Promise((resolve, reject) => {
|
|
488
|
+
const deviceCodeRequest = {
|
|
489
|
+
scopes: SCOPES,
|
|
490
|
+
deviceCodeCallback: (response) => {
|
|
491
|
+
const deviceCodeInfo = {
|
|
492
|
+
verificationUri: response.verificationUri,
|
|
493
|
+
userCode: response.userCode,
|
|
494
|
+
message: response.message
|
|
495
|
+
};
|
|
496
|
+
// Store the pending auth promise
|
|
497
|
+
const authPromise = msalClient.acquireTokenByDeviceCode(deviceCodeRequest)
|
|
498
|
+
.then(async (tokenResponse) => {
|
|
499
|
+
if (!tokenResponse) {
|
|
500
|
+
throw new Error('Failed to acquire token via device code');
|
|
501
|
+
}
|
|
502
|
+
await this.saveToken(tokenResponse, 'device');
|
|
503
|
+
logger.log('MS365 device code authentication successful');
|
|
504
|
+
this.pendingAuth = null; // Clear pending auth
|
|
505
|
+
return tokenResponse;
|
|
506
|
+
})
|
|
507
|
+
.catch((error) => {
|
|
508
|
+
this.pendingAuth = null; // Clear pending auth on error
|
|
509
|
+
throw error;
|
|
510
|
+
});
|
|
511
|
+
this.pendingAuth = { authPromise, deviceCodeInfo };
|
|
512
|
+
logger.log(`Device code authentication: ${response.verificationUri} - ${response.userCode}`);
|
|
513
|
+
// Return device code info immediately
|
|
514
|
+
resolve(deviceCodeInfo);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
// This will never complete, but will call the callback immediately
|
|
518
|
+
msalClient.acquireTokenByDeviceCode(deviceCodeRequest).catch(() => {
|
|
519
|
+
// Ignore the error from this call since we're handling it in the stored promise
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Wait for pending device code authentication to complete
|
|
525
|
+
*/
|
|
526
|
+
async waitForDeviceCodeAuth() {
|
|
527
|
+
if (!this.pendingAuth) {
|
|
528
|
+
throw new Error('No pending device code authentication. Call startDeviceCodeAuth first.');
|
|
529
|
+
}
|
|
530
|
+
return await this.pendingAuth.authPromise;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Check if there's a pending device code authentication
|
|
534
|
+
*/
|
|
535
|
+
hasPendingAuth() {
|
|
536
|
+
return this.pendingAuth !== null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get pending device code info
|
|
540
|
+
*/
|
|
541
|
+
getPendingDeviceCodeInfo() {
|
|
542
|
+
return this.pendingAuth?.deviceCodeInfo || null;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Setup credentials interactively
|
|
546
|
+
*/
|
|
547
|
+
async setupCredentials() {
|
|
548
|
+
const readline = await import('readline');
|
|
549
|
+
const rl = readline.createInterface({
|
|
550
|
+
input: process.stdin,
|
|
551
|
+
output: process.stdout
|
|
552
|
+
});
|
|
553
|
+
const question = (prompt) => {
|
|
554
|
+
return new Promise((resolve) => {
|
|
555
|
+
rl.question(prompt, resolve);
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
try {
|
|
559
|
+
console.log('\n🔧 MS365 MCP Server Credential Setup\n');
|
|
560
|
+
console.log('Choose authentication method:');
|
|
561
|
+
console.log('1. Device Code Flow (Recommended - no app registration needed)');
|
|
562
|
+
console.log('2. Custom Azure App (Advanced - requires app registration)\n');
|
|
563
|
+
const choice = await question('Enter your choice (1 or 2): ');
|
|
564
|
+
if (choice === '1') {
|
|
565
|
+
// Use built-in app with device code flow
|
|
566
|
+
const credentials = {
|
|
567
|
+
clientId: BUILTIN_CLIENT_ID,
|
|
568
|
+
tenantId: DEFAULT_TENANT_ID,
|
|
569
|
+
authType: 'device'
|
|
570
|
+
};
|
|
571
|
+
await this.saveCredentials(credentials);
|
|
572
|
+
console.log('\n✅ Configured for device code authentication!');
|
|
573
|
+
console.log('Run: ms365-mcp-server to start the server\n');
|
|
574
|
+
}
|
|
575
|
+
else if (choice === '2') {
|
|
576
|
+
console.log('\nCustom Azure App Setup:');
|
|
577
|
+
console.log('1. Go to https://portal.azure.com');
|
|
578
|
+
console.log('2. Navigate to Azure Active Directory > App registrations');
|
|
579
|
+
console.log('3. Click "New registration"');
|
|
580
|
+
console.log('4. Set redirect URI to: http://localhost:44001/oauth2callback');
|
|
581
|
+
console.log('5. Grant required API permissions for Microsoft Graph\n');
|
|
582
|
+
const clientId = await question('Enter your Client ID: ');
|
|
583
|
+
const clientSecret = await question('Enter your Client Secret (optional for device flow): ');
|
|
584
|
+
const tenantId = await question('Enter your Tenant ID (or "common" for multi-tenant): ');
|
|
585
|
+
const authType = clientSecret ? 'redirect' : 'device';
|
|
586
|
+
const credentials = {
|
|
587
|
+
clientId: clientId.trim(),
|
|
588
|
+
clientSecret: clientSecret.trim() || undefined,
|
|
589
|
+
tenantId: tenantId.trim(),
|
|
590
|
+
redirectUri: 'http://localhost:44001/oauth2callback',
|
|
591
|
+
authType: authType
|
|
592
|
+
};
|
|
593
|
+
await this.saveCredentials(credentials);
|
|
594
|
+
console.log('\n✅ Credentials saved successfully!');
|
|
595
|
+
console.log('Run: ms365-mcp-server to start the server\n');
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
console.log('Invalid choice. Setup cancelled.');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
rl.close();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Get storage method information
|
|
607
|
+
*/
|
|
608
|
+
getStorageInfo() {
|
|
609
|
+
return {
|
|
610
|
+
method: credentialStore.getStorageMethod(),
|
|
611
|
+
location: credentialStore.isKeychainAvailable() ? 'OS Keychain' : CONFIG_DIR
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* List all authenticated accounts
|
|
616
|
+
*/
|
|
617
|
+
async listAuthenticatedAccounts() {
|
|
618
|
+
return await credentialStore.listAccounts();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Get authentication URL without opening browser (redirect flow)
|
|
622
|
+
*/
|
|
623
|
+
async getAuthUrl() {
|
|
624
|
+
if (!await this.loadCredentials()) {
|
|
625
|
+
throw new Error('MS365 credentials not configured');
|
|
626
|
+
}
|
|
627
|
+
if (!this.credentials?.clientSecret) {
|
|
628
|
+
throw new Error('Client secret required for redirect authentication. Use device code flow instead.');
|
|
629
|
+
}
|
|
630
|
+
const msalClient = this.initializeMsalClient();
|
|
631
|
+
const authUrl = await msalClient.getAuthCodeUrl({
|
|
632
|
+
scopes: SCOPES,
|
|
633
|
+
redirectUri: this.credentials.redirectUri,
|
|
634
|
+
prompt: 'consent'
|
|
635
|
+
});
|
|
636
|
+
return authUrl;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
export const enhancedMS365Auth = new EnhancedMS365Auth();
|