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,363 @@
1
+ import { ConfidentialClientApplication } 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
+ // Scopes required for Microsoft 365 operations
11
+ const SCOPES = [
12
+ 'https://graph.microsoft.com/Mail.ReadWrite',
13
+ 'https://graph.microsoft.com/Mail.Send',
14
+ 'https://graph.microsoft.com/MailboxSettings.Read',
15
+ 'https://graph.microsoft.com/Contacts.Read',
16
+ 'https://graph.microsoft.com/User.Read',
17
+ 'offline_access'
18
+ ];
19
+ // Configuration directory and file paths
20
+ const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
21
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
22
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'token.json');
23
+ /**
24
+ * Microsoft 365 authentication manager class
25
+ */
26
+ export class MS365Auth {
27
+ constructor() {
28
+ this.msalClient = null;
29
+ this.credentials = null;
30
+ this.ensureConfigDir();
31
+ }
32
+ /**
33
+ * Ensure configuration directory exists
34
+ */
35
+ ensureConfigDir() {
36
+ if (!fs.existsSync(CONFIG_DIR)) {
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
+ logger.log('Created MS365 MCP configuration directory');
39
+ }
40
+ }
41
+ /**
42
+ * Load credentials from file or environment
43
+ */
44
+ async loadCredentials() {
45
+ try {
46
+ // Try environment variables first
47
+ if (process.env.MS365_CLIENT_ID && process.env.MS365_CLIENT_SECRET && process.env.MS365_TENANT_ID) {
48
+ this.credentials = {
49
+ clientId: process.env.MS365_CLIENT_ID,
50
+ clientSecret: process.env.MS365_CLIENT_SECRET,
51
+ tenantId: process.env.MS365_TENANT_ID,
52
+ redirectUri: process.env.MS365_REDIRECT_URI || 'http://localhost:44001/oauth2callback'
53
+ };
54
+ logger.log('Loaded MS365 credentials from environment variables');
55
+ return true;
56
+ }
57
+ // Try credentials file
58
+ if (fs.existsSync(CREDENTIALS_FILE)) {
59
+ const credentialsData = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
60
+ this.credentials = JSON.parse(credentialsData);
61
+ logger.log('Loaded MS365 credentials from file');
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+ catch (error) {
67
+ logger.error('Error loading MS365 credentials:', error);
68
+ return false;
69
+ }
70
+ }
71
+ /**
72
+ * Save credentials to file
73
+ */
74
+ async saveCredentials(credentials) {
75
+ try {
76
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
77
+ logger.log('Saved MS365 credentials to file');
78
+ }
79
+ catch (error) {
80
+ logger.error('Error saving MS365 credentials:', error);
81
+ throw new Error('Failed to save credentials');
82
+ }
83
+ }
84
+ /**
85
+ * Initialize MSAL client
86
+ */
87
+ initializeMsalClient() {
88
+ if (!this.credentials) {
89
+ throw new Error('Credentials not loaded');
90
+ }
91
+ const config = {
92
+ auth: {
93
+ clientId: this.credentials.clientId,
94
+ clientSecret: this.credentials.clientSecret,
95
+ authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
96
+ },
97
+ system: {
98
+ loggerOptions: {
99
+ loggerCallback: (level, message, containsPii) => {
100
+ if (!containsPii) {
101
+ logger.log(`MSAL: ${message}`);
102
+ }
103
+ },
104
+ piiLoggingEnabled: false,
105
+ logLevel: 3 // Error level
106
+ }
107
+ }
108
+ };
109
+ this.msalClient = new ConfidentialClientApplication(config);
110
+ return this.msalClient;
111
+ }
112
+ /**
113
+ * Load stored access token
114
+ */
115
+ loadStoredToken() {
116
+ try {
117
+ if (fs.existsSync(TOKEN_FILE)) {
118
+ const tokenData = fs.readFileSync(TOKEN_FILE, 'utf8');
119
+ return JSON.parse(tokenData);
120
+ }
121
+ }
122
+ catch (error) {
123
+ logger.error('Error loading stored token:', error);
124
+ }
125
+ return null;
126
+ }
127
+ /**
128
+ * Save access token to file
129
+ */
130
+ saveToken(token) {
131
+ try {
132
+ const tokenData = {
133
+ accessToken: token.accessToken,
134
+ refreshToken: '', // MSAL handles refresh tokens internally
135
+ expiresOn: token.expiresOn?.getTime() || 0,
136
+ account: token.account
137
+ };
138
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
139
+ logger.log('Saved MS365 access token');
140
+ }
141
+ catch (error) {
142
+ logger.error('Error saving token:', error);
143
+ }
144
+ }
145
+ /**
146
+ * Start local server for OAuth2 callback
147
+ */
148
+ startCallbackServer() {
149
+ return new Promise((resolve, reject) => {
150
+ const server = createServer((req, res) => {
151
+ if (req.url?.startsWith('/oauth2callback')) {
152
+ const url = new URL(req.url, 'http://localhost:44001');
153
+ const code = url.searchParams.get('code');
154
+ const error = url.searchParams.get('error');
155
+ if (error) {
156
+ res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`);
157
+ server.close();
158
+ reject(new Error(`OAuth2 error: ${error}`));
159
+ return;
160
+ }
161
+ if (code) {
162
+ res.end(`<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>`);
163
+ server.close();
164
+ resolve(code);
165
+ return;
166
+ }
167
+ res.end('<html><body><h1>Invalid Request</h1></body></html>');
168
+ }
169
+ else {
170
+ res.end('<html><body><h1>MS365 MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>');
171
+ }
172
+ });
173
+ server.listen(44001, () => {
174
+ logger.log('OAuth2 callback server started on port 44001');
175
+ });
176
+ server.on('error', (err) => {
177
+ reject(err);
178
+ });
179
+ });
180
+ }
181
+ /**
182
+ * Perform OAuth2 authentication flow
183
+ */
184
+ async authenticate() {
185
+ if (!await this.loadCredentials()) {
186
+ throw new Error('MS365 credentials not configured');
187
+ }
188
+ const msalClient = this.initializeMsalClient();
189
+ try {
190
+ // Generate authorization URL
191
+ const authUrl = await msalClient.getAuthCodeUrl({
192
+ scopes: SCOPES,
193
+ redirectUri: this.credentials.redirectUri,
194
+ prompt: 'consent'
195
+ });
196
+ logger.log('Opening browser for authentication...');
197
+ // Start callback server and open browser
198
+ const [authCode] = await Promise.all([
199
+ this.startCallbackServer(),
200
+ open(authUrl)
201
+ ]);
202
+ // Exchange code for token
203
+ const tokenResponse = await msalClient.acquireTokenByCode({
204
+ code: authCode,
205
+ scopes: SCOPES,
206
+ redirectUri: this.credentials.redirectUri
207
+ });
208
+ if (!tokenResponse) {
209
+ throw new Error('Failed to acquire token');
210
+ }
211
+ this.saveToken(tokenResponse);
212
+ logger.log('MS365 authentication successful');
213
+ }
214
+ catch (error) {
215
+ logger.error('Authentication failed:', error);
216
+ throw error;
217
+ }
218
+ }
219
+ /**
220
+ * Get authenticated Microsoft Graph client
221
+ */
222
+ async getGraphClient() {
223
+ const storedToken = this.loadStoredToken();
224
+ if (!storedToken) {
225
+ throw new Error('No stored token found. Please authenticate first.');
226
+ }
227
+ // Check if token is expired
228
+ if (storedToken.expiresOn < Date.now()) {
229
+ await this.refreshToken();
230
+ }
231
+ const client = Client.init({
232
+ authProvider: (done) => {
233
+ done(null, storedToken.accessToken);
234
+ }
235
+ });
236
+ return client;
237
+ }
238
+ /**
239
+ * Refresh access token using refresh token
240
+ */
241
+ async refreshToken() {
242
+ const storedToken = this.loadStoredToken();
243
+ if (!storedToken?.account) {
244
+ throw new Error('No account information available. Please re-authenticate.');
245
+ }
246
+ if (!await this.loadCredentials()) {
247
+ throw new Error('MS365 credentials not configured');
248
+ }
249
+ const msalClient = this.initializeMsalClient();
250
+ try {
251
+ const tokenResponse = await msalClient.acquireTokenSilent({
252
+ scopes: SCOPES,
253
+ account: storedToken.account
254
+ });
255
+ if (!tokenResponse) {
256
+ throw new Error('Failed to refresh token');
257
+ }
258
+ this.saveToken(tokenResponse);
259
+ logger.log('MS365 token refreshed successfully');
260
+ }
261
+ catch (error) {
262
+ logger.error('Token refresh failed:', error);
263
+ throw error;
264
+ }
265
+ }
266
+ /**
267
+ * Check if user is authenticated
268
+ */
269
+ async isAuthenticated() {
270
+ const storedToken = this.loadStoredToken();
271
+ if (!storedToken) {
272
+ return false;
273
+ }
274
+ // If token is expired, try to refresh
275
+ if (storedToken.expiresOn < Date.now()) {
276
+ try {
277
+ await this.refreshToken();
278
+ return true;
279
+ }
280
+ catch (error) {
281
+ logger.error('Token refresh failed during authentication check:', error);
282
+ return false;
283
+ }
284
+ }
285
+ return true;
286
+ }
287
+ /**
288
+ * Check if credentials are configured
289
+ */
290
+ async isConfigured() {
291
+ return await this.loadCredentials();
292
+ }
293
+ /**
294
+ * Clear stored authentication data
295
+ */
296
+ resetAuth() {
297
+ try {
298
+ if (fs.existsSync(TOKEN_FILE)) {
299
+ fs.unlinkSync(TOKEN_FILE);
300
+ logger.log('Cleared stored authentication tokens');
301
+ }
302
+ }
303
+ catch (error) {
304
+ logger.error('Error clearing authentication data:', error);
305
+ }
306
+ }
307
+ /**
308
+ * Get authentication URL without opening browser
309
+ */
310
+ async getAuthUrl() {
311
+ if (!await this.loadCredentials()) {
312
+ throw new Error('MS365 credentials not configured');
313
+ }
314
+ const msalClient = this.initializeMsalClient();
315
+ const authUrl = await msalClient.getAuthCodeUrl({
316
+ scopes: SCOPES,
317
+ redirectUri: this.credentials.redirectUri,
318
+ prompt: 'consent'
319
+ });
320
+ return authUrl;
321
+ }
322
+ /**
323
+ * Setup credentials interactively
324
+ */
325
+ async setupCredentials() {
326
+ const readline = await import('readline');
327
+ const rl = readline.createInterface({
328
+ input: process.stdin,
329
+ output: process.stdout
330
+ });
331
+ const question = (prompt) => {
332
+ return new Promise((resolve) => {
333
+ rl.question(prompt, resolve);
334
+ });
335
+ };
336
+ try {
337
+ console.log('\n🔧 MS365 MCP Server Credential Setup\n');
338
+ console.log('You need to register an application in Azure Portal first:');
339
+ console.log('1. Go to https://portal.azure.com');
340
+ console.log('2. Navigate to Azure Active Directory > App registrations');
341
+ console.log('3. Click "New registration"');
342
+ console.log('4. Set redirect URI to: http://localhost:44001/oauth2callback');
343
+ console.log('5. Grant required API permissions for Microsoft Graph\n');
344
+ const clientId = await question('Enter your Client ID: ');
345
+ const clientSecret = await question('Enter your Client Secret: ');
346
+ const tenantId = await question('Enter your Tenant ID (or "common" for multi-tenant): ');
347
+ const redirectUri = await question('Enter redirect URI (default: http://localhost:44001/oauth2callback): ') || 'http://localhost:44001/oauth2callback';
348
+ const credentials = {
349
+ clientId: clientId.trim(),
350
+ clientSecret: clientSecret.trim(),
351
+ tenantId: tenantId.trim(),
352
+ redirectUri: redirectUri.trim()
353
+ };
354
+ await this.saveCredentials(credentials);
355
+ console.log('\n✅ Credentials saved successfully!');
356
+ console.log('You can now run: ms365-mcp-server\n');
357
+ }
358
+ finally {
359
+ rl.close();
360
+ }
361
+ }
362
+ }
363
+ export const ms365Auth = new MS365Auth();