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,359 @@
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 { createServer } from 'http';
7
+ import { URL } from 'url';
8
+ import { logger } from './api.js';
9
+ import { createHash, randomBytes } from 'crypto';
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
20
+ const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
21
+ /**
22
+ * Multi-user Microsoft 365 authentication manager
23
+ */
24
+ export class MultiUserMS365Auth {
25
+ constructor() {
26
+ this.sessions = new Map();
27
+ this.credentials = null;
28
+ this.authServers = new Map();
29
+ this.ensureConfigDir();
30
+ this.loadCredentials();
31
+ this.loadExistingSessions();
32
+ }
33
+ ensureConfigDir() {
34
+ if (!fs.existsSync(CONFIG_DIR)) {
35
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
36
+ }
37
+ }
38
+ /**
39
+ * Load credentials from environment or file
40
+ */
41
+ loadCredentials() {
42
+ try {
43
+ // Try environment variables first
44
+ if (process.env.MS365_CLIENT_ID && process.env.MS365_CLIENT_SECRET && process.env.MS365_TENANT_ID) {
45
+ this.credentials = {
46
+ clientId: process.env.MS365_CLIENT_ID,
47
+ clientSecret: process.env.MS365_CLIENT_SECRET,
48
+ tenantId: process.env.MS365_TENANT_ID,
49
+ redirectUri: process.env.MS365_REDIRECT_URI || 'http://localhost:44001/oauth2callback'
50
+ };
51
+ logger.log('Loaded MS365 credentials from environment variables');
52
+ return;
53
+ }
54
+ // Try credentials file
55
+ const credentialsFile = path.join(CONFIG_DIR, 'credentials.json');
56
+ if (fs.existsSync(credentialsFile)) {
57
+ const credentialsData = fs.readFileSync(credentialsFile, 'utf8');
58
+ this.credentials = JSON.parse(credentialsData);
59
+ logger.log('Loaded MS365 credentials from file');
60
+ }
61
+ }
62
+ catch (error) {
63
+ logger.error('Error loading MS365 credentials:', error);
64
+ }
65
+ }
66
+ /**
67
+ * Load existing user sessions
68
+ */
69
+ loadExistingSessions() {
70
+ try {
71
+ const sessionsFile = path.join(CONFIG_DIR, 'user-sessions.json');
72
+ if (fs.existsSync(sessionsFile)) {
73
+ const sessionsData = fs.readFileSync(sessionsFile, 'utf8');
74
+ const sessions = JSON.parse(sessionsData);
75
+ for (const [userId, session] of Object.entries(sessions)) {
76
+ this.sessions.set(userId, session);
77
+ }
78
+ logger.log(`Loaded ${this.sessions.size} existing user sessions`);
79
+ }
80
+ }
81
+ catch (error) {
82
+ logger.error('Error loading existing sessions:', error);
83
+ }
84
+ }
85
+ /**
86
+ * Save user sessions to file
87
+ */
88
+ saveUserSessions() {
89
+ try {
90
+ const sessionsFile = path.join(CONFIG_DIR, 'user-sessions.json');
91
+ const sessionsObj = Object.fromEntries(this.sessions);
92
+ fs.writeFileSync(sessionsFile, JSON.stringify(sessionsObj, null, 2));
93
+ logger.log('Saved user sessions');
94
+ }
95
+ catch (error) {
96
+ logger.error('Error saving user sessions:', error);
97
+ }
98
+ }
99
+ /**
100
+ * Generate a unique user ID
101
+ */
102
+ generateUserId(userEmail) {
103
+ const timestamp = Date.now().toString();
104
+ const random = randomBytes(8).toString('hex');
105
+ if (userEmail) {
106
+ const emailHash = createHash('sha256').update(userEmail).digest('hex').substring(0, 8);
107
+ return `user_${emailHash}_${timestamp.substring(-6)}_${random.substring(0, 4)}`;
108
+ }
109
+ return `user_${timestamp}_${random}`;
110
+ }
111
+ /**
112
+ * Create MSAL client for a user
113
+ */
114
+ createMsalClient(redirectUri) {
115
+ if (!this.credentials) {
116
+ throw new Error('MS365 credentials not configured');
117
+ }
118
+ const config = {
119
+ auth: {
120
+ clientId: this.credentials.clientId,
121
+ clientSecret: this.credentials.clientSecret,
122
+ authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
123
+ },
124
+ system: {
125
+ loggerOptions: {
126
+ loggerCallback: (level, message, containsPii) => {
127
+ if (!containsPii) {
128
+ logger.log(`MSAL: ${message}`);
129
+ }
130
+ },
131
+ piiLoggingEnabled: false,
132
+ logLevel: 3 // Error level
133
+ }
134
+ }
135
+ };
136
+ return new ConfidentialClientApplication(config);
137
+ }
138
+ /**
139
+ * Start authentication flow for a new user
140
+ */
141
+ async authenticateNewUser(userEmail) {
142
+ if (!this.credentials) {
143
+ throw new Error('MS365 credentials not configured. Please set MS365_CLIENT_ID, MS365_CLIENT_SECRET, and MS365_TENANT_ID environment variables.');
144
+ }
145
+ const userId = this.generateUserId(userEmail);
146
+ // Find available port
147
+ const port = await this.findAvailablePort();
148
+ const redirectUri = `http://localhost:${port}/oauth2callback`;
149
+ // Create MSAL client with specific redirect URI
150
+ const msalClient = this.createMsalClient(redirectUri);
151
+ // Generate authentication URL
152
+ const authUrl = await msalClient.getAuthCodeUrl({
153
+ scopes: SCOPES,
154
+ redirectUri: redirectUri,
155
+ prompt: 'consent',
156
+ state: userId // Include user ID in state for identification
157
+ });
158
+ // Start callback server for this user
159
+ await this.startCallbackServer(userId, msalClient, port, redirectUri);
160
+ logger.log(`Started authentication flow for user ${userId} on port ${port}`);
161
+ return { userId, authUrl, port };
162
+ }
163
+ /**
164
+ * Find an available port for OAuth callback
165
+ */
166
+ async findAvailablePort(startPort = 44001) {
167
+ return new Promise((resolve) => {
168
+ const server = createServer();
169
+ server.listen(startPort, () => {
170
+ const port = server.address()?.port || startPort;
171
+ server.close();
172
+ resolve(port);
173
+ });
174
+ server.on('error', () => {
175
+ resolve(this.findAvailablePort(startPort + 1));
176
+ });
177
+ });
178
+ }
179
+ /**
180
+ * Start callback server for OAuth2 authentication
181
+ */
182
+ async startCallbackServer(userId, msalClient, port, redirectUri) {
183
+ return new Promise((resolve, reject) => {
184
+ const server = createServer(async (req, res) => {
185
+ if (req.url?.startsWith('/oauth2callback')) {
186
+ const url = new URL(req.url, `http://localhost:${port}`);
187
+ const code = url.searchParams.get('code');
188
+ const state = url.searchParams.get('state');
189
+ const error = url.searchParams.get('error');
190
+ if (error) {
191
+ res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`);
192
+ server.close();
193
+ reject(new Error(`OAuth2 error: ${error}`));
194
+ return;
195
+ }
196
+ if (code && state === userId) {
197
+ try {
198
+ // Exchange code for token
199
+ const tokenResponse = await msalClient.acquireTokenByCode({
200
+ code: code,
201
+ scopes: SCOPES,
202
+ redirectUri: redirectUri
203
+ });
204
+ if (tokenResponse) {
205
+ // Save user session
206
+ const userSession = {
207
+ userId: userId,
208
+ userEmail: tokenResponse.account?.username,
209
+ accessToken: tokenResponse.accessToken,
210
+ refreshToken: '', // MSAL handles refresh tokens internally
211
+ expiresOn: tokenResponse.expiresOn?.getTime() || 0,
212
+ authenticated: true,
213
+ account: tokenResponse.account
214
+ };
215
+ this.sessions.set(userId, userSession);
216
+ this.saveUserSessions();
217
+ res.end(`<html><body><h1>Authentication Successful!</h1><p>User ID: ${userId}<br/>You can close this window and return to the application.</p></body></html>`);
218
+ server.close();
219
+ resolve();
220
+ return;
221
+ }
222
+ }
223
+ catch (tokenError) {
224
+ logger.error(`Token exchange failed for user ${userId}:`, tokenError);
225
+ res.end(`<html><body><h1>Token Exchange Failed</h1><p>Please try again.</p></body></html>`);
226
+ server.close();
227
+ reject(tokenError);
228
+ return;
229
+ }
230
+ }
231
+ res.end('<html><body><h1>Invalid Request</h1></body></html>');
232
+ }
233
+ else {
234
+ res.end('<html><body><h1>MS365 MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>');
235
+ }
236
+ });
237
+ this.authServers.set(userId, server);
238
+ server.listen(port, () => {
239
+ logger.log(`OAuth2 callback server started for user ${userId} on port ${port}`);
240
+ // Set timeout for authentication
241
+ setTimeout(() => {
242
+ if (this.authServers.has(userId)) {
243
+ server.close();
244
+ this.authServers.delete(userId);
245
+ logger.log(`Authentication timeout for user ${userId}`);
246
+ reject(new Error('Authentication timeout'));
247
+ }
248
+ }, 600000); // 10 minutes timeout
249
+ });
250
+ server.on('error', (err) => {
251
+ this.authServers.delete(userId);
252
+ reject(err);
253
+ });
254
+ server.on('close', () => {
255
+ this.authServers.delete(userId);
256
+ resolve();
257
+ });
258
+ });
259
+ }
260
+ /**
261
+ * Get Microsoft Graph client for a specific user
262
+ */
263
+ async getGraphClientForUser(userId) {
264
+ const session = this.sessions.get(userId);
265
+ if (!session) {
266
+ throw new Error(`User session not found: ${userId}`);
267
+ }
268
+ // Check if token is expired
269
+ if (session.expiresOn < Date.now()) {
270
+ await this.refreshUserToken(userId);
271
+ }
272
+ const client = Client.init({
273
+ authProvider: (done) => {
274
+ const updatedSession = this.sessions.get(userId);
275
+ done(null, updatedSession?.accessToken || '');
276
+ }
277
+ });
278
+ return client;
279
+ }
280
+ /**
281
+ * Refresh user token
282
+ */
283
+ async refreshUserToken(userId) {
284
+ const session = this.sessions.get(userId);
285
+ if (!session?.account) {
286
+ throw new Error(`No account information available for user: ${userId}`);
287
+ }
288
+ if (!this.credentials) {
289
+ throw new Error('MS365 credentials not configured');
290
+ }
291
+ const msalClient = this.createMsalClient();
292
+ try {
293
+ const tokenResponse = await msalClient.acquireTokenSilent({
294
+ scopes: SCOPES,
295
+ account: session.account
296
+ });
297
+ if (!tokenResponse) {
298
+ throw new Error('Failed to refresh token');
299
+ }
300
+ // Update session
301
+ session.accessToken = tokenResponse.accessToken;
302
+ session.refreshToken = ''; // MSAL handles refresh tokens internally
303
+ session.expiresOn = tokenResponse.expiresOn?.getTime() || 0;
304
+ session.account = tokenResponse.account || session.account;
305
+ this.sessions.set(userId, session);
306
+ this.saveUserSessions();
307
+ logger.log(`Token refreshed for user ${userId}`);
308
+ }
309
+ catch (error) {
310
+ logger.error(`Token refresh failed for user ${userId}:`, error);
311
+ throw error;
312
+ }
313
+ }
314
+ /**
315
+ * Get user session information
316
+ */
317
+ getUserSession(userId) {
318
+ return this.sessions.get(userId) || null;
319
+ }
320
+ /**
321
+ * Get all authenticated users
322
+ */
323
+ getAuthenticatedUsers() {
324
+ return Array.from(this.sessions.values()).filter(session => session.authenticated);
325
+ }
326
+ /**
327
+ * Remove user session
328
+ */
329
+ removeUser(userId) {
330
+ const success = this.sessions.delete(userId);
331
+ if (success) {
332
+ this.saveUserSessions();
333
+ logger.log(`Removed user session: ${userId}`);
334
+ }
335
+ return success;
336
+ }
337
+ /**
338
+ * Clear all user sessions
339
+ */
340
+ clearAllSessions() {
341
+ this.sessions.clear();
342
+ this.saveUserSessions();
343
+ logger.log('Cleared all user sessions');
344
+ }
345
+ /**
346
+ * Check if user is authenticated
347
+ */
348
+ isUserAuthenticated(userId) {
349
+ const session = this.sessions.get(userId);
350
+ return session?.authenticated || false;
351
+ }
352
+ /**
353
+ * Get user count
354
+ */
355
+ getUserCount() {
356
+ return this.sessions.size;
357
+ }
358
+ }
359
+ export const multiUserMS365Auth = new MultiUserMS365Auth();
package/install.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ console.log('Setting up MS365 MCP Server...');
11
+
12
+ // Make CLI executable
13
+ const binPath = path.join(__dirname, 'bin', 'cli.js');
14
+ if (fs.existsSync(binPath)) {
15
+ try {
16
+ fs.chmodSync(binPath, '755');
17
+ console.log('✓ Made CLI executable');
18
+ } catch (error) {
19
+ console.warn('Warning: Could not make CLI executable:', error.message);
20
+ }
21
+ } else {
22
+ console.warn('Warning: CLI file not found at', binPath);
23
+ }
24
+
25
+ // Display setup message
26
+ console.log(`
27
+ ✓ MS365 MCP Server installed successfully!
28
+
29
+ Next steps:
30
+ 1. Set up authentication: npx ms365-mcp-server --setup-auth
31
+ 2. Start the server: npx ms365-mcp-server
32
+
33
+ For help: npx ms365-mcp-server --help
34
+
35
+ Authentication Setup:
36
+ - Register app in Azure Portal: https://portal.azure.com
37
+ - Set environment variables: MS365_CLIENT_ID, MS365_CLIENT_SECRET, MS365_TENANT_ID
38
+ - Or use interactive setup with --setup-auth
39
+
40
+ Documentation: https://github.com/yourusername/ms365-mcp-server
41
+ `);
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "ms365-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "ms365-mcp-server": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "npx @modelcontextprotocol/inspector node dist/index.js",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "dev": "tsx src/index.ts",
15
+ "postinstall": "node install.js",
16
+ "prepare": "chmod +x bin/cli.js && npm run build",
17
+ "install-global": "npm install -g .",
18
+ "postpublish": "echo 'Package published! Note: Users can test with: npx ms365-mcp-server --help'",
19
+ "test-npx": "cd /tmp && npx -y ms365-mcp-server@latest --help",
20
+ "version-bump": "node scripts/version-bump.js"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "microsoft365",
25
+ "outlook",
26
+ "email",
27
+ "office365",
28
+ "graph-api",
29
+ "oauth2",
30
+ "claude"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.10.1",
35
+ "@azure/msal-node": "^2.6.6",
36
+ "@microsoft/microsoft-graph-client": "^3.0.7",
37
+ "isomorphic-fetch": "^3.0.0",
38
+ "open": "^10.1.0",
39
+ "mime-types": "^2.1.35",
40
+ "keytar": "^7.9.0",
41
+ "typescript": "^5.0.4"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.2.3",
45
+ "@types/mime-types": "^2.1.4",
46
+ "@types/isomorphic-fetch": "^0.0.36",
47
+ "ts-node": "^10.9.1",
48
+ "tsx": "^4.19.4"
49
+ },
50
+ "files": [
51
+ "bin/",
52
+ "dist/",
53
+ "README.md",
54
+ "install.js"
55
+ ],
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ }
59
+ }