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,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();
|