@tgai96/outlook-mcp 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/README.md +396 -0
- package/auth/index.js +64 -0
- package/auth/oauth-server.js +178 -0
- package/auth/token-manager.js +139 -0
- package/auth/token-storage.js +317 -0
- package/auth/tools.js +171 -0
- package/calendar/accept.js +64 -0
- package/calendar/cancel.js +64 -0
- package/calendar/create.js +69 -0
- package/calendar/decline.js +64 -0
- package/calendar/delete.js +59 -0
- package/calendar/index.js +123 -0
- package/calendar/list.js +77 -0
- package/cli.js +246 -0
- package/config.js +108 -0
- package/email/folder-utils.js +175 -0
- package/email/index.js +157 -0
- package/email/list.js +78 -0
- package/email/mark-as-read.js +101 -0
- package/email/read.js +128 -0
- package/email/search.js +285 -0
- package/email/send.js +120 -0
- package/folder/create.js +124 -0
- package/folder/index.js +78 -0
- package/folder/list.js +264 -0
- package/folder/move.js +163 -0
- package/index.js +148 -0
- package/package.json +54 -0
- package/rules/create.js +248 -0
- package/rules/index.js +175 -0
- package/rules/list.js +202 -0
- package/utils/graph-api.js +192 -0
- package/utils/mock-data.js +145 -0
- package/utils/odata-helpers.js +40 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token management for Microsoft Graph API authentication
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const config = require('../config');
|
|
7
|
+
|
|
8
|
+
// Global variable to store tokens
|
|
9
|
+
let cachedTokens = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loads authentication tokens from the token file
|
|
13
|
+
* @returns {object|null} - The loaded tokens or null if not available
|
|
14
|
+
*/
|
|
15
|
+
function loadTokenCache() {
|
|
16
|
+
try {
|
|
17
|
+
const tokenPath = config.AUTH_CONFIG.tokenStorePath;
|
|
18
|
+
console.error(`[DEBUG] Attempting to load tokens from: ${tokenPath}`);
|
|
19
|
+
console.error(`[DEBUG] HOME directory: ${process.env.HOME}`);
|
|
20
|
+
console.error(`[DEBUG] Full resolved path: ${tokenPath}`);
|
|
21
|
+
|
|
22
|
+
// Log file existence and details
|
|
23
|
+
if (!fs.existsSync(tokenPath)) {
|
|
24
|
+
console.error('[DEBUG] Token file does not exist');
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const stats = fs.statSync(tokenPath);
|
|
29
|
+
console.error(`[DEBUG] Token file stats:
|
|
30
|
+
Size: ${stats.size} bytes
|
|
31
|
+
Created: ${stats.birthtime}
|
|
32
|
+
Modified: ${stats.mtime}`);
|
|
33
|
+
|
|
34
|
+
const tokenData = fs.readFileSync(tokenPath, 'utf8');
|
|
35
|
+
console.error('[DEBUG] Token file contents length:', tokenData.length);
|
|
36
|
+
console.error('[DEBUG] Token file first 200 characters:', tokenData.slice(0, 200));
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const tokens = JSON.parse(tokenData);
|
|
40
|
+
console.error('[DEBUG] Parsed tokens keys:', Object.keys(tokens));
|
|
41
|
+
|
|
42
|
+
// Log each key's value to see what's present
|
|
43
|
+
Object.keys(tokens).forEach(key => {
|
|
44
|
+
console.error(`[DEBUG] ${key}: ${typeof tokens[key]}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Check for access token presence
|
|
48
|
+
if (!tokens.access_token) {
|
|
49
|
+
console.error('[DEBUG] No access_token found in tokens');
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check token expiration
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const expiresAt = tokens.expires_at || 0;
|
|
56
|
+
|
|
57
|
+
console.error(`[DEBUG] Current time: ${now}`);
|
|
58
|
+
console.error(`[DEBUG] Token expires at: ${expiresAt}`);
|
|
59
|
+
|
|
60
|
+
if (now > expiresAt) {
|
|
61
|
+
console.error('[DEBUG] Token has expired');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Update the cache
|
|
66
|
+
cachedTokens = tokens;
|
|
67
|
+
return tokens;
|
|
68
|
+
} catch (parseError) {
|
|
69
|
+
console.error('[DEBUG] Error parsing token JSON:', parseError);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('[DEBUG] Error loading token cache:', error);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Saves authentication tokens to the token file
|
|
80
|
+
* @param {object} tokens - The tokens to save
|
|
81
|
+
* @returns {boolean} - Whether the save was successful
|
|
82
|
+
*/
|
|
83
|
+
function saveTokenCache(tokens) {
|
|
84
|
+
try {
|
|
85
|
+
const tokenPath = config.AUTH_CONFIG.tokenStorePath;
|
|
86
|
+
console.error(`Saving tokens to: ${tokenPath}`);
|
|
87
|
+
|
|
88
|
+
// Ensure the directory exists
|
|
89
|
+
const tokenDir = path.dirname(tokenPath);
|
|
90
|
+
if (!fs.existsSync(tokenDir)) {
|
|
91
|
+
fs.mkdirSync(tokenDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
|
|
95
|
+
console.error('Tokens saved successfully');
|
|
96
|
+
|
|
97
|
+
// Update the cache
|
|
98
|
+
cachedTokens = tokens;
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Error saving token cache:', error);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets the current access token, loading from cache if necessary
|
|
108
|
+
* @returns {string|null} - The access token or null if not available
|
|
109
|
+
*/
|
|
110
|
+
function getAccessToken() {
|
|
111
|
+
if (cachedTokens && cachedTokens.access_token) {
|
|
112
|
+
return cachedTokens.access_token;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tokens = loadTokenCache();
|
|
116
|
+
return tokens ? tokens.access_token : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a test access token for use in test mode
|
|
121
|
+
* @returns {object} - The test tokens
|
|
122
|
+
*/
|
|
123
|
+
function createTestTokens() {
|
|
124
|
+
const testTokens = {
|
|
125
|
+
access_token: "test_access_token_" + Date.now(),
|
|
126
|
+
refresh_token: "test_refresh_token_" + Date.now(),
|
|
127
|
+
expires_at: Date.now() + (3600 * 1000) // 1 hour
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
saveTokenCache(testTokens);
|
|
131
|
+
return testTokens;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
loadTokenCache,
|
|
136
|
+
saveTokenCache,
|
|
137
|
+
getAccessToken,
|
|
138
|
+
createTestTokens
|
|
139
|
+
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const querystring = require('querystring');
|
|
5
|
+
|
|
6
|
+
class TokenStorage {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = {
|
|
9
|
+
tokenStorePath: path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.outlook-mcp', 'tokens.json'),
|
|
10
|
+
clientId: process.env.MS_CLIENT_ID,
|
|
11
|
+
redirectUri: process.env.MS_REDIRECT_URI || 'http://localhost:3333/auth/callback',
|
|
12
|
+
scopes: (process.env.MS_SCOPES || 'offline_access User.Read Mail.Read').split(' '),
|
|
13
|
+
tokenEndpoint: process.env.MS_TOKEN_ENDPOINT || 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
14
|
+
refreshTokenBuffer: 5 * 60 * 1000, // 5 minutes buffer for token refresh
|
|
15
|
+
...config // Allow overriding default config
|
|
16
|
+
};
|
|
17
|
+
this.tokens = null;
|
|
18
|
+
this._loadPromise = null;
|
|
19
|
+
this._refreshPromise = null;
|
|
20
|
+
|
|
21
|
+
if (!this.config.clientId) {
|
|
22
|
+
console.warn("TokenStorage: MS_CLIENT_ID is not configured. Token operations might fail.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async _loadTokensFromFile() {
|
|
27
|
+
try {
|
|
28
|
+
const tokenData = await fs.readFile(this.config.tokenStorePath, 'utf8');
|
|
29
|
+
this.tokens = JSON.parse(tokenData);
|
|
30
|
+
console.error('Tokens loaded from file.');
|
|
31
|
+
return this.tokens;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') {
|
|
34
|
+
console.error('Token file not found. No tokens loaded.');
|
|
35
|
+
} else {
|
|
36
|
+
console.error('Error loading token cache:', error);
|
|
37
|
+
}
|
|
38
|
+
this.tokens = null;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async _saveTokensToFile() {
|
|
44
|
+
if (!this.tokens) {
|
|
45
|
+
console.warn('No tokens to save.');
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Ensure the directory exists
|
|
50
|
+
const tokenDir = path.dirname(this.config.tokenStorePath);
|
|
51
|
+
await fs.mkdir(tokenDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
await fs.writeFile(this.config.tokenStorePath, JSON.stringify(this.tokens, null, 2));
|
|
54
|
+
console.error('Tokens saved successfully.');
|
|
55
|
+
// return true; // No longer returning boolean, will throw on error.
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error saving token cache:', error);
|
|
58
|
+
throw error; // Propagate the error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getTokens() {
|
|
63
|
+
if (this.tokens) {
|
|
64
|
+
return this.tokens;
|
|
65
|
+
}
|
|
66
|
+
if (!this._loadPromise) {
|
|
67
|
+
this._loadPromise = this._loadTokensFromFile().finally(() => {
|
|
68
|
+
this._loadPromise = null; // Reset promise once completed
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return this._loadPromise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getExpiryTime() {
|
|
75
|
+
return this.tokens && this.tokens.expires_at ? this.tokens.expires_at : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isTokenExpired() {
|
|
79
|
+
if (!this.tokens || !this.tokens.expires_at) {
|
|
80
|
+
return true; // No token or no expiry means it's effectively expired or invalid
|
|
81
|
+
}
|
|
82
|
+
// Check if current time is past expiry time, considering a buffer
|
|
83
|
+
return Date.now() >= (this.tokens.expires_at - this.config.refreshTokenBuffer);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getValidAccessToken() {
|
|
87
|
+
await this.getTokens(); // Ensure tokens are loaded
|
|
88
|
+
|
|
89
|
+
if (!this.tokens || !this.tokens.access_token) {
|
|
90
|
+
console.error('[TokenStorage] No access token available.');
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (this.isTokenExpired()) {
|
|
95
|
+
console.error('[TokenStorage] Access token expired or nearing expiration. Attempting refresh.');
|
|
96
|
+
console.error(`[TokenStorage] Client ID configured: ${this.config.clientId ? 'YES' : 'NO'}`);
|
|
97
|
+
if (this.tokens.refresh_token) {
|
|
98
|
+
try {
|
|
99
|
+
return await this.refreshAccessToken();
|
|
100
|
+
} catch (refreshError) {
|
|
101
|
+
console.error('[TokenStorage] Failed to refresh access token:', refreshError.message);
|
|
102
|
+
this.tokens = null; // Invalidate tokens on refresh failure
|
|
103
|
+
await this._saveTokensToFile(); // Persist invalidation
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.error('[TokenStorage] No refresh token available. Cannot refresh access token.');
|
|
108
|
+
this.tokens = null; // Invalidate tokens as they are expired and cannot be refreshed
|
|
109
|
+
await this._saveTokensToFile(); // Persist invalidation
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
console.error('[TokenStorage] Access token is still valid');
|
|
114
|
+
return this.tokens.access_token;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async refreshAccessToken() {
|
|
118
|
+
if (!this.tokens || !this.tokens.refresh_token) {
|
|
119
|
+
throw new Error('No refresh token available to refresh the access token.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!this.config.clientId) {
|
|
123
|
+
throw new Error('MS_CLIENT_ID is not configured. Cannot refresh access token.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Prevent multiple concurrent refresh attempts
|
|
127
|
+
if (this._refreshPromise) {
|
|
128
|
+
console.error("[TokenStorage] Refresh already in progress, returning existing promise.");
|
|
129
|
+
return this._refreshPromise.then(tokens => tokens.access_token);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.error('[TokenStorage] Attempting to refresh access token...');
|
|
133
|
+
console.error(`[TokenStorage] Using client ID: ${this.config.clientId.substring(0, 8)}...`);
|
|
134
|
+
|
|
135
|
+
// Use scopes from existing token if available (preserves original permissions),
|
|
136
|
+
// otherwise use config scopes. This ensures we don't lose permissions on refresh.
|
|
137
|
+
let refreshScopes = this.config.scopes;
|
|
138
|
+
if (this.tokens.scope) {
|
|
139
|
+
// If token has scope as string, use it; if array, join it
|
|
140
|
+
refreshScopes = typeof this.tokens.scope === 'string'
|
|
141
|
+
? this.tokens.scope.split(' ')
|
|
142
|
+
: this.tokens.scope;
|
|
143
|
+
console.error(`[TokenStorage] Using scopes from existing token: ${refreshScopes.join(' ')}`);
|
|
144
|
+
} else {
|
|
145
|
+
console.error(`[TokenStorage] Using scopes from config: ${refreshScopes.join(' ')}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// For public clients (PKCE), refresh tokens don't require client_secret
|
|
149
|
+
const postData = querystring.stringify({
|
|
150
|
+
client_id: this.config.clientId,
|
|
151
|
+
grant_type: 'refresh_token',
|
|
152
|
+
refresh_token: this.tokens.refresh_token,
|
|
153
|
+
scope: refreshScopes.join(' ')
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const requestOptions = {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
160
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
this._refreshPromise = new Promise((resolve, reject) => {
|
|
165
|
+
const req = https.request(this.config.tokenEndpoint, requestOptions, (res) => {
|
|
166
|
+
let data = '';
|
|
167
|
+
res.on('data', (chunk) => data += chunk);
|
|
168
|
+
res.on('end', async () => {
|
|
169
|
+
try {
|
|
170
|
+
const responseBody = JSON.parse(data);
|
|
171
|
+
const statusCode = res.statusCode || 500;
|
|
172
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
173
|
+
this.tokens.access_token = responseBody.access_token;
|
|
174
|
+
// Microsoft Graph API refresh tokens may or may not return a new refresh_token
|
|
175
|
+
if (responseBody.refresh_token) {
|
|
176
|
+
this.tokens.refresh_token = responseBody.refresh_token;
|
|
177
|
+
}
|
|
178
|
+
this.tokens.expires_in = responseBody.expires_in;
|
|
179
|
+
this.tokens.expires_at = Date.now() + (responseBody.expires_in * 1000);
|
|
180
|
+
try {
|
|
181
|
+
await this._saveTokensToFile();
|
|
182
|
+
console.error('Access token refreshed and saved successfully.');
|
|
183
|
+
resolve(this.tokens);
|
|
184
|
+
} catch (saveError) {
|
|
185
|
+
console.error('Failed to save refreshed tokens:', saveError);
|
|
186
|
+
// Even if save fails, tokens are updated in memory.
|
|
187
|
+
// Depending on desired strictness, could reject here.
|
|
188
|
+
// For now, resolve with in-memory tokens but log critical error.
|
|
189
|
+
// Or, to be stricter and align with re-throwing:
|
|
190
|
+
reject(new Error(`Access token refreshed but failed to save: ${saveError.message}`));
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
console.error('Error refreshing token:', responseBody);
|
|
194
|
+
reject(new Error(responseBody.error_description || `Token refresh failed with status ${statusCode}`));
|
|
195
|
+
}
|
|
196
|
+
} catch (e) { // Catch any error during parsing or saving
|
|
197
|
+
console.error('Error processing refresh token response or saving tokens:', e);
|
|
198
|
+
reject(e);
|
|
199
|
+
} finally {
|
|
200
|
+
this._refreshPromise = null; // Clear promise after completion
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
req.on('error', (error) => {
|
|
205
|
+
console.error('HTTP error during token refresh:', error);
|
|
206
|
+
reject(error);
|
|
207
|
+
this._refreshPromise = null; // Clear promise on error
|
|
208
|
+
});
|
|
209
|
+
req.write(postData);
|
|
210
|
+
req.end();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return this._refreshPromise.then(tokens => tokens.access_token);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async exchangeCodeForTokens(authCode, codeVerifier = null) {
|
|
218
|
+
if (!this.config.clientId) {
|
|
219
|
+
throw new Error("Client ID is not configured. Cannot exchange code for tokens.");
|
|
220
|
+
}
|
|
221
|
+
console.error('Exchanging authorization code for tokens...');
|
|
222
|
+
|
|
223
|
+
// Build token exchange request - use PKCE if codeVerifier is provided, otherwise assume client_secret (for backward compatibility)
|
|
224
|
+
const tokenData = {
|
|
225
|
+
client_id: this.config.clientId,
|
|
226
|
+
grant_type: 'authorization_code',
|
|
227
|
+
code: authCode,
|
|
228
|
+
redirect_uri: this.config.redirectUri
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// For PKCE (public client), use code_verifier instead of client_secret
|
|
232
|
+
if (codeVerifier) {
|
|
233
|
+
tokenData.code_verifier = codeVerifier;
|
|
234
|
+
} else {
|
|
235
|
+
// Fallback for confidential clients (if client_secret is still configured)
|
|
236
|
+
const clientSecret = process.env.MS_CLIENT_SECRET;
|
|
237
|
+
if (clientSecret) {
|
|
238
|
+
tokenData.client_secret = clientSecret;
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error("Either code_verifier (for PKCE) or client_secret must be provided for token exchange.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const postData = querystring.stringify(tokenData);
|
|
245
|
+
|
|
246
|
+
const requestOptions = {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: {
|
|
249
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
250
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const req = https.request(this.config.tokenEndpoint, requestOptions, (res) => {
|
|
256
|
+
let data = '';
|
|
257
|
+
res.on('data', (chunk) => data += chunk);
|
|
258
|
+
res.on('end', async () => {
|
|
259
|
+
try {
|
|
260
|
+
const responseBody = JSON.parse(data);
|
|
261
|
+
const statusCode = res.statusCode || 500;
|
|
262
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
263
|
+
this.tokens = {
|
|
264
|
+
access_token: responseBody.access_token,
|
|
265
|
+
refresh_token: responseBody.refresh_token,
|
|
266
|
+
expires_in: responseBody.expires_in,
|
|
267
|
+
expires_at: Date.now() + (responseBody.expires_in * 1000),
|
|
268
|
+
scope: responseBody.scope,
|
|
269
|
+
token_type: responseBody.token_type
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
await this._saveTokensToFile();
|
|
273
|
+
console.error('Tokens exchanged and saved successfully.');
|
|
274
|
+
resolve(this.tokens);
|
|
275
|
+
} catch (saveError) {
|
|
276
|
+
console.error('Failed to save exchanged tokens:', saveError);
|
|
277
|
+
// Similar to refresh, tokens are in memory but not persisted.
|
|
278
|
+
// Rejecting to indicate the operation wasn't fully successful.
|
|
279
|
+
reject(new Error(`Tokens exchanged but failed to save: ${saveError.message}`));
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
console.error('Error exchanging code for tokens:', responseBody);
|
|
283
|
+
reject(new Error(responseBody.error_description || `Token exchange failed with status ${statusCode}`));
|
|
284
|
+
}
|
|
285
|
+
} catch (e) { // Catch any error during parsing or saving
|
|
286
|
+
console.error('Error processing token exchange response or saving tokens:', e, "Raw data:", data);
|
|
287
|
+
reject(new Error(`Error processing token response: ${e.message}. Response data: ${data}`));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
req.on('error', (error) => {
|
|
292
|
+
console.error('HTTP error during code exchange:', error);
|
|
293
|
+
reject(error);
|
|
294
|
+
});
|
|
295
|
+
req.write(postData);
|
|
296
|
+
req.end();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Utility to clear tokens, e.g., for logout or forcing re-auth
|
|
301
|
+
async clearTokens() {
|
|
302
|
+
this.tokens = null;
|
|
303
|
+
try {
|
|
304
|
+
await fs.unlink(this.config.tokenStorePath);
|
|
305
|
+
console.error('Token file deleted successfully.');
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error.code === 'ENOENT') {
|
|
308
|
+
console.error('Token file not found, nothing to delete.');
|
|
309
|
+
} else {
|
|
310
|
+
console.error('Error deleting token file:', error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = TokenStorage;
|
|
317
|
+
// Adding a newline at the end of the file as requested by Gemini Code Assist
|
package/auth/tools.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication-related tools for the Outlook MCP server
|
|
3
|
+
*/
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const tokenManager = require('./token-manager');
|
|
6
|
+
const { ensureAuthenticated, tokenStorage } = require('./index');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* About tool handler
|
|
10
|
+
* @returns {object} - MCP response
|
|
11
|
+
*/
|
|
12
|
+
async function handleAbout() {
|
|
13
|
+
return {
|
|
14
|
+
content: [{
|
|
15
|
+
type: "text",
|
|
16
|
+
text: `📧 MODULAR Outlook Assistant MCP Server v${config.SERVER_VERSION} 📧\n\nProvides access to Microsoft Outlook email, calendar, and contacts through Microsoft Graph API.\nImplemented with a modular architecture for improved maintainability.`
|
|
17
|
+
}]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Authentication tool handler
|
|
23
|
+
* @param {object} args - Tool arguments
|
|
24
|
+
* @returns {object} - MCP response
|
|
25
|
+
*/
|
|
26
|
+
async function handleAuthenticate(args) {
|
|
27
|
+
const force = args && args.force === true;
|
|
28
|
+
|
|
29
|
+
// For test mode, create a test token
|
|
30
|
+
if (config.USE_TEST_MODE) {
|
|
31
|
+
// Create a test token with a 1-hour expiry
|
|
32
|
+
tokenManager.createTestTokens();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: 'Successfully authenticated with Microsoft Graph API (test mode)'
|
|
38
|
+
}]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If not forcing, try to get a valid token first (will auto-refresh if needed)
|
|
43
|
+
if (!force) {
|
|
44
|
+
try {
|
|
45
|
+
const accessToken = await ensureAuthenticated();
|
|
46
|
+
if (accessToken) {
|
|
47
|
+
console.error('[AUTHENTICATE] Successfully obtained valid access token (may have been refreshed)');
|
|
48
|
+
return {
|
|
49
|
+
content: [{
|
|
50
|
+
type: "text",
|
|
51
|
+
text: 'Successfully authenticated with Microsoft Graph API. Access token is valid and ready to use.'
|
|
52
|
+
}]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('[AUTHENTICATE] Failed to get valid token:', error.message);
|
|
57
|
+
// Continue to show auth URL if token refresh failed
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If force=true or token refresh failed, generate an auth URL and instruct the user to visit it
|
|
62
|
+
const authUrl = `${config.AUTH_CONFIG.authServerUrl}/auth?client_id=${config.AUTH_CONFIG.clientId}`;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: `Authentication required. Please visit the following URL to authenticate with Microsoft: ${authUrl}\n\nAfter authentication, you will be redirected back to this application.`
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check authentication status tool handler
|
|
74
|
+
* @returns {object} - MCP response
|
|
75
|
+
*/
|
|
76
|
+
async function handleCheckAuthStatus() {
|
|
77
|
+
console.error('[CHECK-AUTH-STATUS] Starting authentication status check');
|
|
78
|
+
|
|
79
|
+
const tokens = tokenManager.loadTokenCache();
|
|
80
|
+
|
|
81
|
+
console.error(`[CHECK-AUTH-STATUS] Tokens loaded: ${tokens ? 'YES' : 'NO'}`);
|
|
82
|
+
|
|
83
|
+
if (!tokens || !tokens.access_token) {
|
|
84
|
+
console.error('[CHECK-AUTH-STATUS] No valid access token found');
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: "Not authenticated" }]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.error('[CHECK-AUTH-STATUS] Access token present');
|
|
91
|
+
|
|
92
|
+
// Format timestamps in human-readable format
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const expiresAt = tokens.expires_at || 0;
|
|
95
|
+
const currentTime = new Date(now).toLocaleString();
|
|
96
|
+
const expiresTime = new Date(expiresAt).toLocaleString();
|
|
97
|
+
|
|
98
|
+
// Calculate time until expiration
|
|
99
|
+
const timeUntilExpiry = expiresAt - now;
|
|
100
|
+
const minutesUntilExpiry = Math.floor(timeUntilExpiry / (1000 * 60));
|
|
101
|
+
const hoursUntilExpiry = Math.floor(minutesUntilExpiry / 60);
|
|
102
|
+
const daysUntilExpiry = Math.floor(hoursUntilExpiry / 24);
|
|
103
|
+
|
|
104
|
+
let expiryMessage;
|
|
105
|
+
if (timeUntilExpiry < 0) {
|
|
106
|
+
expiryMessage = 'EXPIRED';
|
|
107
|
+
} else if (daysUntilExpiry > 0) {
|
|
108
|
+
expiryMessage = `${daysUntilExpiry} day(s) and ${hoursUntilExpiry % 24} hour(s) from now`;
|
|
109
|
+
} else if (hoursUntilExpiry > 0) {
|
|
110
|
+
expiryMessage = `${hoursUntilExpiry} hour(s) and ${minutesUntilExpiry % 60} minute(s) from now`;
|
|
111
|
+
} else {
|
|
112
|
+
expiryMessage = `${minutesUntilExpiry} minute(s) from now`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.error(`[CHECK-AUTH-STATUS] Current time: ${currentTime}`);
|
|
116
|
+
console.error(`[CHECK-AUTH-STATUS] Token expires at: ${expiresTime} (${expiryMessage})`);
|
|
117
|
+
|
|
118
|
+
const statusText = timeUntilExpiry < 0
|
|
119
|
+
? `Authenticated but token has expired. Will auto-refresh on next use.`
|
|
120
|
+
: `Authenticated and ready. Token expires ${expiryMessage}.`;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: statusText }]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Tool definitions
|
|
128
|
+
const authTools = [
|
|
129
|
+
{
|
|
130
|
+
name: "about",
|
|
131
|
+
description: "Returns information about this Outlook Assistant server",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {},
|
|
135
|
+
required: []
|
|
136
|
+
},
|
|
137
|
+
handler: handleAbout
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "authenticate",
|
|
141
|
+
description: "Authenticate with Microsoft Graph API to access Outlook data",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
force: {
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "Force re-authentication even if already authenticated"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
required: []
|
|
151
|
+
},
|
|
152
|
+
handler: handleAuthenticate
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "check-auth-status",
|
|
156
|
+
description: "Check the current authentication status with Microsoft Graph API",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {},
|
|
160
|
+
required: []
|
|
161
|
+
},
|
|
162
|
+
handler: handleCheckAuthStatus
|
|
163
|
+
}
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
authTools,
|
|
168
|
+
handleAbout,
|
|
169
|
+
handleAuthenticate,
|
|
170
|
+
handleCheckAuthStatus
|
|
171
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accept event functionality
|
|
3
|
+
*/
|
|
4
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
|
+
const { ensureAuthenticated } = require('../auth');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Accept event handler
|
|
9
|
+
* @param {object} args - Tool arguments
|
|
10
|
+
* @returns {object} - MCP response
|
|
11
|
+
*/
|
|
12
|
+
async function handleAcceptEvent(args) {
|
|
13
|
+
const { eventId, comment } = args;
|
|
14
|
+
|
|
15
|
+
if (!eventId) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: "Event ID is required to accept an event."
|
|
20
|
+
}]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Get access token
|
|
26
|
+
const accessToken = await ensureAuthenticated();
|
|
27
|
+
|
|
28
|
+
// Build API endpoint
|
|
29
|
+
const endpoint = `me/events/${eventId}/accept`;
|
|
30
|
+
|
|
31
|
+
// Request body
|
|
32
|
+
const body = {
|
|
33
|
+
comment: comment || "Accepted via API"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Make API call
|
|
37
|
+
await callGraphAPI(accessToken, 'POST', endpoint, body);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `Event with ID ${eventId} has been successfully accepted.`
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.message === 'Authentication required') {
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: "Authentication required. Please use the 'authenticate' tool first."
|
|
51
|
+
}]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `Error accepting event: ${error.message}`
|
|
59
|
+
}]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = handleAcceptEvent;
|