@littlebearapps/outlook-assistant 3.3.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.
Files changed (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. package/utils/safety.js +123 -0
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+ const http = require('http');
3
+ const crypto = require('crypto');
4
+ const url = require('url');
5
+ const querystring = require('querystring');
6
+ const https = require('https');
7
+ const fs = require('fs');
8
+
9
+ /**
10
+ * Escapes HTML special characters to prevent XSS in rendered responses.
11
+ */
12
+ function escapeHtml(str) {
13
+ if (typeof str !== 'string') return '';
14
+ return str
15
+ .replace(/&/g, '&')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;')
19
+ .replace(/'/g, '&#39;');
20
+ }
21
+
22
+ // Security headers for all HTML responses
23
+ const SECURITY_HEADERS = {
24
+ 'Content-Type': 'text/html',
25
+ 'X-Content-Type-Options': 'nosniff',
26
+ 'X-Frame-Options': 'DENY',
27
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'",
28
+ };
29
+
30
+ // Pending OAuth state tokens for CSRF validation
31
+ const pendingStates = new Map();
32
+
33
+ // Load environment variables from .env file
34
+ require('dotenv').config();
35
+
36
+ // Import scopes and token path from central config to stay in sync
37
+ const { AUTH_CONFIG: centralAuth } = require('./config');
38
+
39
+ // Log to console
40
+ console.log('Starting Outlook Authentication Server');
41
+
42
+ // Authentication configuration — scopes and tokenStorePath from config.js (single source of truth)
43
+ const AUTH_CONFIG = {
44
+ clientId: process.env.OUTLOOK_CLIENT_ID || process.env.MS_CLIENT_ID || '',
45
+ clientSecret:
46
+ process.env.OUTLOOK_CLIENT_SECRET || process.env.MS_CLIENT_SECRET || '',
47
+ redirectUri: centralAuth.redirectUri,
48
+ scopes: centralAuth.scopes,
49
+ tokenStorePath: centralAuth.tokenStorePath,
50
+ };
51
+
52
+ // Create HTTP server
53
+ const server = http.createServer((req, res) => {
54
+ const parsedUrl = url.parse(req.url, true);
55
+ const pathname = parsedUrl.pathname;
56
+
57
+ console.log(`Request received: ${pathname}`);
58
+
59
+ if (pathname === '/auth/callback') {
60
+ const query = parsedUrl.query;
61
+
62
+ if (query.error) {
63
+ console.error(
64
+ `Authentication error: ${query.error} - ${query.error_description}`
65
+ );
66
+ res.writeHead(400, SECURITY_HEADERS);
67
+ res.end(`
68
+ <html>
69
+ <head>
70
+ <title>Authentication Error</title>
71
+ <style>
72
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
73
+ h1 { color: #d9534f; }
74
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
75
+ </style>
76
+ </head>
77
+ <body>
78
+ <h1>Authentication Error</h1>
79
+ <div class="error-box">
80
+ <p><strong>Error:</strong> ${escapeHtml(query.error)}</p>
81
+ <p><strong>Description:</strong> ${escapeHtml(query.error_description || 'No description provided')}</p>
82
+ </div>
83
+ <p>Please close this window and try again.</p>
84
+ </body>
85
+ </html>
86
+ `);
87
+ return;
88
+ }
89
+
90
+ if (query.code) {
91
+ // Validate OAuth state parameter for CSRF protection
92
+ if (!query.state || !pendingStates.has(query.state)) {
93
+ console.error('Invalid or missing OAuth state parameter');
94
+ res.writeHead(403, SECURITY_HEADERS);
95
+ res.end(`
96
+ <html>
97
+ <head>
98
+ <title>Invalid State</title>
99
+ <style>
100
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
101
+ h1 { color: #d9534f; }
102
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
103
+ </style>
104
+ </head>
105
+ <body>
106
+ <h1>Invalid Request</h1>
107
+ <div class="error-box">
108
+ <p>The OAuth state parameter is invalid or expired. This may indicate a CSRF attempt.</p>
109
+ </div>
110
+ <p>Please close this window and try authenticating again.</p>
111
+ </body>
112
+ </html>
113
+ `);
114
+ return;
115
+ }
116
+ pendingStates.delete(query.state);
117
+
118
+ console.log('Authorization code received, exchanging for tokens...');
119
+
120
+ // Exchange code for tokens
121
+ exchangeCodeForTokens(query.code)
122
+ .then((_tokens) => {
123
+ console.log('Token exchange successful');
124
+ res.writeHead(200, SECURITY_HEADERS);
125
+ res.end(`
126
+ <html>
127
+ <head>
128
+ <title>Authentication Successful</title>
129
+ <style>
130
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
131
+ h1 { color: #5cb85c; }
132
+ .success-box { background-color: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 4px; }
133
+ </style>
134
+ </head>
135
+ <body>
136
+ <h1>Authentication Successful!</h1>
137
+ <div class="success-box">
138
+ <p>You have successfully authenticated with Microsoft Graph API.</p>
139
+ <p>The access token has been saved securely.</p>
140
+ </div>
141
+ <p>You can now close this window and return to your AI assistant.</p>
142
+ </body>
143
+ </html>
144
+ `);
145
+ })
146
+ .catch((error) => {
147
+ console.error(`Token exchange error: ${error.message}`);
148
+ res.writeHead(500, SECURITY_HEADERS);
149
+ res.end(`
150
+ <html>
151
+ <head>
152
+ <title>Token Exchange Error</title>
153
+ <style>
154
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
155
+ h1 { color: #d9534f; }
156
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
157
+ </style>
158
+ </head>
159
+ <body>
160
+ <h1>Token Exchange Error</h1>
161
+ <div class="error-box">
162
+ <p>Token exchange failed. Please try authenticating again.</p>
163
+ </div>
164
+ <p>Please close this window and try again.</p>
165
+ </body>
166
+ </html>
167
+ `);
168
+ });
169
+ } else {
170
+ console.error('No authorization code provided');
171
+ res.writeHead(400, SECURITY_HEADERS);
172
+ res.end(`
173
+ <html>
174
+ <head>
175
+ <title>Missing Authorization Code</title>
176
+ <style>
177
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
178
+ h1 { color: #d9534f; }
179
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <h1>Missing Authorization Code</h1>
184
+ <div class="error-box">
185
+ <p>No authorization code was provided in the callback.</p>
186
+ </div>
187
+ <p>Please close this window and try again.</p>
188
+ </body>
189
+ </html>
190
+ `);
191
+ }
192
+ } else if (pathname === '/auth') {
193
+ // Handle the /auth route - redirect to Microsoft's OAuth authorization endpoint
194
+ console.log('Auth request received, redirecting to Microsoft login...');
195
+
196
+ // Verify credentials are set
197
+ if (!AUTH_CONFIG.clientId || !AUTH_CONFIG.clientSecret) {
198
+ res.writeHead(500, SECURITY_HEADERS);
199
+ res.end(`
200
+ <html>
201
+ <head>
202
+ <title>Configuration Error</title>
203
+ <style>
204
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
205
+ h1 { color: #d9534f; }
206
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
207
+ code { background: #f4f4f4; padding: 2px 4px; border-radius: 4px; }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <h1>Configuration Error</h1>
212
+ <div class="error-box">
213
+ <p>Microsoft Graph API credentials are not set. Please set the following environment variables:</p>
214
+ <ul>
215
+ <li><code>OUTLOOK_CLIENT_ID</code></li>
216
+ <li><code>OUTLOOK_CLIENT_SECRET</code></li>
217
+ </ul>
218
+ <p style="margin-top: 10px; font-size: 0.9em; color: #666;">Legacy names <code>MS_CLIENT_ID</code> / <code>MS_CLIENT_SECRET</code> are also accepted.</p>
219
+ </div>
220
+ </body>
221
+ </html>
222
+ `);
223
+ return;
224
+ }
225
+
226
+ // Get client_id from query parameters or use the default
227
+ const query = parsedUrl.query;
228
+ const clientId = query.client_id || AUTH_CONFIG.clientId;
229
+
230
+ // Generate cryptographically secure state for CSRF protection
231
+ const state = crypto.randomUUID();
232
+ pendingStates.set(state, Date.now());
233
+
234
+ // Clean up expired states (older than 10 minutes)
235
+ const TEN_MINUTES = 10 * 60 * 1000;
236
+ for (const [key, timestamp] of pendingStates) {
237
+ if (Date.now() - timestamp > TEN_MINUTES) pendingStates.delete(key);
238
+ }
239
+
240
+ // Build the authorization URL
241
+ const authParams = {
242
+ client_id: clientId,
243
+ response_type: 'code',
244
+ redirect_uri: AUTH_CONFIG.redirectUri,
245
+ scope: AUTH_CONFIG.scopes.join(' '),
246
+ response_mode: 'query',
247
+ state,
248
+ };
249
+
250
+ const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${querystring.stringify(authParams)}`;
251
+ console.log(`Redirecting to: ${authUrl}`);
252
+
253
+ // Redirect to Microsoft's login page
254
+ res.writeHead(302, { Location: authUrl });
255
+ res.end();
256
+ } else if (pathname === '/') {
257
+ // Root path - provide instructions
258
+ res.writeHead(200, SECURITY_HEADERS);
259
+ res.end(`
260
+ <html>
261
+ <head>
262
+ <title>Outlook Authentication Server</title>
263
+ <style>
264
+ body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
265
+ h1 { color: #0078d4; }
266
+ .info-box { background-color: #e7f6fd; border: 1px solid #b3e0ff; padding: 15px; border-radius: 4px; }
267
+ code { background: #f4f4f4; padding: 2px 4px; border-radius: 4px; }
268
+ </style>
269
+ </head>
270
+ <body>
271
+ <h1>Outlook Authentication Server</h1>
272
+ <div class="info-box">
273
+ <p>This server is running to handle Microsoft Graph API authentication callbacks.</p>
274
+ <p>Don't navigate here directly. Instead, use the <code>auth</code> tool (with <code>action=authenticate</code>) in your AI assistant to start the authentication process.</p>
275
+ <p>Make sure you've set the <code>OUTLOOK_CLIENT_ID</code> and <code>OUTLOOK_CLIENT_SECRET</code> environment variables.</p>
276
+ </div>
277
+ <p>Server is running at http://localhost:3333</p>
278
+ </body>
279
+ </html>
280
+ `);
281
+ } else {
282
+ // Not found
283
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
284
+ res.end('Not Found');
285
+ }
286
+ });
287
+
288
+ function exchangeCodeForTokens(code) {
289
+ return new Promise((resolve, reject) => {
290
+ const postData = querystring.stringify({
291
+ client_id: AUTH_CONFIG.clientId,
292
+ client_secret: AUTH_CONFIG.clientSecret,
293
+ code: code,
294
+ redirect_uri: AUTH_CONFIG.redirectUri,
295
+ grant_type: 'authorization_code',
296
+ scope: AUTH_CONFIG.scopes.join(' '),
297
+ });
298
+
299
+ const options = {
300
+ hostname: 'login.microsoftonline.com',
301
+ path: '/common/oauth2/v2.0/token',
302
+ method: 'POST',
303
+ headers: {
304
+ 'Content-Type': 'application/x-www-form-urlencoded',
305
+ 'Content-Length': Buffer.byteLength(postData),
306
+ },
307
+ };
308
+
309
+ const req = https.request(options, (res) => {
310
+ let data = '';
311
+
312
+ res.on('data', (chunk) => {
313
+ data += chunk;
314
+ });
315
+
316
+ res.on('end', () => {
317
+ if (res.statusCode >= 200 && res.statusCode < 300) {
318
+ try {
319
+ const tokenResponse = JSON.parse(data);
320
+
321
+ // Calculate expiration time (current time + expires_in seconds)
322
+ const expiresAt = Date.now() + tokenResponse.expires_in * 1000;
323
+
324
+ // Add expires_at for easier expiration checking
325
+ tokenResponse.expires_at = expiresAt;
326
+
327
+ // Save tokens to file
328
+ fs.writeFileSync(
329
+ AUTH_CONFIG.tokenStorePath,
330
+ JSON.stringify(tokenResponse, null, 2),
331
+ { encoding: 'utf8', mode: 0o600 }
332
+ );
333
+ console.log(`Tokens saved to ${AUTH_CONFIG.tokenStorePath}`);
334
+
335
+ resolve(tokenResponse);
336
+ } catch (error) {
337
+ reject(new Error(`Error parsing token response: ${error.message}`));
338
+ }
339
+ } else {
340
+ reject(
341
+ new Error(
342
+ `Token exchange failed with status ${res.statusCode}: ${data}`
343
+ )
344
+ );
345
+ }
346
+ });
347
+ });
348
+
349
+ req.on('error', (error) => {
350
+ reject(error);
351
+ });
352
+
353
+ req.write(postData);
354
+ req.end();
355
+ });
356
+ }
357
+
358
+ // Start server
359
+ const PORT = 3333;
360
+ server.listen(PORT, '127.0.0.1', () => {
361
+ console.log(`Authentication server running at http://localhost:${PORT}`);
362
+ console.log(
363
+ `Waiting for authentication callback at ${AUTH_CONFIG.redirectUri}`
364
+ );
365
+ console.log(`Token will be stored at: ${AUTH_CONFIG.tokenStorePath}`);
366
+
367
+ if (!AUTH_CONFIG.clientId || !AUTH_CONFIG.clientSecret) {
368
+ console.log('\n⚠️ WARNING: Microsoft Graph API credentials are not set.');
369
+ console.log(
370
+ ' Please set the OUTLOOK_CLIENT_ID and OUTLOOK_CLIENT_SECRET environment variables.'
371
+ );
372
+ }
373
+ });
374
+
375
+ // Handle termination
376
+ process.on('SIGINT', () => {
377
+ console.log('Authentication server shutting down');
378
+ process.exit(0);
379
+ });
380
+
381
+ process.on('SIGTERM', () => {
382
+ console.log('Authentication server shutting down');
383
+ process.exit(0);
384
+ });
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@littlebearapps/outlook-assistant",
3
+ "version": "3.3.0",
4
+ "mcpName": "io.github.littlebearapps/outlook-assistant",
5
+ "description": "Outlook Assistant — MCP server with 20 tools for email, calendar, contacts, and settings via Microsoft Graph API",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "outlook-assistant": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "auth-server": "node outlook-auth-server.js",
13
+ "test-mode": "USE_TEST_MODE=true node index.js",
14
+ "inspect": "npx @modelcontextprotocol/inspector node index.js",
15
+ "test": "jest",
16
+ "lint": "eslint .",
17
+ "lint:fix": "eslint . --fix",
18
+ "format": "prettier --write .",
19
+ "format:check": "prettier --check .",
20
+ "prepare": "husky"
21
+ },
22
+ "lint-staged": {
23
+ "*.js": [
24
+ "eslint --fix",
25
+ "prettier --write"
26
+ ],
27
+ "*.{md,json,yml,yaml}": [
28
+ "prettier --write"
29
+ ]
30
+ },
31
+ "keywords": [
32
+ "claude",
33
+ "outlook",
34
+ "mcp",
35
+ "microsoft-graph",
36
+ "email",
37
+ "calendar",
38
+ "contacts",
39
+ "model-context-protocol",
40
+ "anthropic"
41
+ ],
42
+ "author": "Little Bear Apps <hello@littlebearapps.com>",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/littlebearapps/outlook-assistant.git"
47
+ },
48
+ "homepage": "https://littlebearapps.com/builds/outlook-assistant",
49
+ "bugs": {
50
+ "url": "https://github.com/littlebearapps/outlook-assistant/issues"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "files": [
56
+ "index.js",
57
+ "config.js",
58
+ "outlook-auth-server.js",
59
+ "auth/",
60
+ "calendar/",
61
+ "categories/",
62
+ "contacts/",
63
+ "email/",
64
+ "folder/",
65
+ "rules/",
66
+ "settings/",
67
+ "advanced/",
68
+ "utils/",
69
+ ".env.example",
70
+ "README.md",
71
+ "LICENSE",
72
+ "llms.txt"
73
+ ],
74
+ "dependencies": {
75
+ "@modelcontextprotocol/sdk": "^1.27.1",
76
+ "dotenv": "^17.3.1"
77
+ },
78
+ "devDependencies": {
79
+ "@commitlint/cli": "^20.4.3",
80
+ "@commitlint/config-conventional": "^20.4.3",
81
+ "@eslint/js": "^10.0.1",
82
+ "@modelcontextprotocol/inspector": "^0.21.1",
83
+ "eslint": "^10.0.2",
84
+ "globals": "^17.4.0",
85
+ "husky": "^9.1.7",
86
+ "jest": "^30.2.0",
87
+ "lint-staged": "^16.3.2",
88
+ "prettier": "^3.8.1",
89
+ "supertest": "^7.2.2"
90
+ },
91
+ "engines": {
92
+ "node": ">=18.18.0"
93
+ },
94
+ "overrides": {
95
+ "minimatch": ">=3.1.3"
96
+ }
97
+ }