@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,322 @@
1
+ /**
2
+ * Get folder statistics functionality
3
+ *
4
+ * Returns folder item counts and metadata for pagination planning.
5
+ * Supports outputVerbosity for token-efficient responses.
6
+ */
7
+ const { callGraphAPI } = require('../utils/graph-api');
8
+ const { ensureAuthenticated } = require('../auth');
9
+ const config = require('../config');
10
+
11
+ const { VERBOSITY, DEFAULT_LIMITS } = config;
12
+
13
+ /**
14
+ * Get folder stats handler
15
+ * @param {object} args - Tool arguments
16
+ * @returns {object} - MCP response
17
+ */
18
+ async function handleGetFolderStats(args) {
19
+ const folderName = args.folder || 'inbox';
20
+ const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
21
+
22
+ try {
23
+ const accessToken = await ensureAuthenticated();
24
+
25
+ // Resolve folder name to ID
26
+ const folderId = await resolveFolderName(accessToken, folderName);
27
+
28
+ if (!folderId) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: 'text',
33
+ text: `Folder "${folderName}" not found.`,
34
+ },
35
+ ],
36
+ };
37
+ }
38
+
39
+ // Get folder details with full stats
40
+ const folder = await callGraphAPI(
41
+ accessToken,
42
+ 'GET',
43
+ `me/mailFolders/${folderId}`,
44
+ null,
45
+ {
46
+ // Note: sizeInBytes is NOT available on mailFolder resource type
47
+ $select:
48
+ 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount,isHidden',
49
+ }
50
+ );
51
+
52
+ // Get recent email dates for context
53
+ let dateRange = null;
54
+ if (verbosity !== VERBOSITY.MINIMAL && folder.totalItemCount > 0) {
55
+ dateRange = await getEmailDateRange(accessToken, folderId);
56
+ }
57
+
58
+ // Format response based on verbosity
59
+ const formatted = formatFolderStats(folder, dateRange, verbosity);
60
+
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: formatted.text,
66
+ },
67
+ ],
68
+ _meta: formatted.meta,
69
+ };
70
+ } catch (error) {
71
+ if (error.message === 'Authentication required') {
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: "Authentication required. Please use the 'authenticate' tool first.",
77
+ },
78
+ ],
79
+ };
80
+ }
81
+
82
+ return {
83
+ content: [
84
+ {
85
+ type: 'text',
86
+ text: `Error getting folder stats: ${error.message}`,
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Resolve folder name to ID
95
+ * @param {string} accessToken - Access token
96
+ * @param {string} folderName - Folder name or well-known name
97
+ * @returns {Promise<string|null>} - Folder ID or null
98
+ */
99
+ async function resolveFolderName(accessToken, folderName) {
100
+ const wellKnownFolders = {
101
+ inbox: 'inbox',
102
+ sent: 'sentitems',
103
+ sentitems: 'sentitems',
104
+ 'sent items': 'sentitems',
105
+ drafts: 'drafts',
106
+ deleted: 'deleteditems',
107
+ deleteditems: 'deleteditems',
108
+ 'deleted items': 'deleteditems',
109
+ junk: 'junkemail',
110
+ junkemail: 'junkemail',
111
+ 'junk email': 'junkemail',
112
+ spam: 'junkemail',
113
+ archive: 'archive',
114
+ outbox: 'outbox',
115
+ };
116
+
117
+ const normalised = folderName.toLowerCase().trim();
118
+
119
+ // Check if it's a well-known folder
120
+ if (wellKnownFolders[normalised]) {
121
+ try {
122
+ const response = await callGraphAPI(
123
+ accessToken,
124
+ 'GET',
125
+ `me/mailFolders/${wellKnownFolders[normalised]}`,
126
+ null,
127
+ { $select: 'id' }
128
+ );
129
+ return response.id;
130
+ } catch (_error) {
131
+ // Fall through to search
132
+ }
133
+ }
134
+
135
+ // Search for folder by name
136
+ try {
137
+ const response = await callGraphAPI(
138
+ accessToken,
139
+ 'GET',
140
+ 'me/mailFolders',
141
+ null,
142
+ {
143
+ $filter: `displayName eq '${folderName}'`,
144
+ $select: 'id',
145
+ }
146
+ );
147
+
148
+ if (response.value && response.value.length > 0) {
149
+ return response.value[0].id;
150
+ }
151
+ } catch (error) {
152
+ console.error(`Error searching for folder: ${error.message}`);
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ /**
159
+ * Get date range of emails in folder
160
+ * @param {string} accessToken - Access token
161
+ * @param {string} folderId - Folder ID
162
+ * @returns {Promise<object|null>} - { oldest, newest } dates or null
163
+ */
164
+ async function getEmailDateRange(accessToken, folderId) {
165
+ try {
166
+ // Get newest email
167
+ const newestResponse = await callGraphAPI(
168
+ accessToken,
169
+ 'GET',
170
+ `me/mailFolders/${folderId}/messages`,
171
+ null,
172
+ {
173
+ $select: 'receivedDateTime',
174
+ $orderby: 'receivedDateTime desc',
175
+ $top: 1,
176
+ }
177
+ );
178
+
179
+ // Get oldest email
180
+ const oldestResponse = await callGraphAPI(
181
+ accessToken,
182
+ 'GET',
183
+ `me/mailFolders/${folderId}/messages`,
184
+ null,
185
+ {
186
+ $select: 'receivedDateTime',
187
+ $orderby: 'receivedDateTime asc',
188
+ $top: 1,
189
+ }
190
+ );
191
+
192
+ const newest = newestResponse.value?.[0]?.receivedDateTime;
193
+ const oldest = oldestResponse.value?.[0]?.receivedDateTime;
194
+
195
+ if (newest && oldest) {
196
+ return { newest, oldest };
197
+ }
198
+ } catch (error) {
199
+ console.error(`Error getting date range: ${error.message}`);
200
+ }
201
+
202
+ return null;
203
+ }
204
+
205
+ /**
206
+ * Format folder stats based on verbosity
207
+ * @param {object} folder - Folder object from Graph API
208
+ * @param {object|null} dateRange - Date range object
209
+ * @param {string} verbosity - Verbosity level
210
+ * @returns {object} - { text, meta }
211
+ */
212
+ function formatFolderStats(folder, dateRange, verbosity) {
213
+ const totalItems = folder.totalItemCount || 0;
214
+ const unreadItems = folder.unreadItemCount || 0;
215
+
216
+ // Calculate pagination info
217
+ const pageSize = DEFAULT_LIMITS.listEmails;
218
+ const totalPages = Math.ceil(totalItems / pageSize);
219
+
220
+ // Build meta object
221
+ const meta = {
222
+ folderId: folder.id,
223
+ folderName: folder.displayName,
224
+ totalItems,
225
+ unreadItems,
226
+ pageSize,
227
+ totalPages,
228
+ verbosity,
229
+ };
230
+
231
+ // Minimal: Just key numbers
232
+ if (verbosity === VERBOSITY.MINIMAL) {
233
+ return {
234
+ text: `${folder.displayName}: ${totalItems} items (${unreadItems} unread)`,
235
+ meta,
236
+ };
237
+ }
238
+
239
+ // Standard: Markdown table with key stats
240
+ if (verbosity === VERBOSITY.STANDARD) {
241
+ let text = `## ${folder.displayName} Statistics\n\n`;
242
+ text += `| Metric | Value |\n`;
243
+ text += `|--------|-------|\n`;
244
+ text += `| Total Items | ${totalItems.toLocaleString()} |\n`;
245
+ text += `| Unread Items | ${unreadItems.toLocaleString()} |\n`;
246
+ text += `| Pages (${pageSize}/page) | ${totalPages} |\n`;
247
+
248
+ if (dateRange) {
249
+ const newest = new Date(dateRange.newest).toLocaleDateString('en-AU');
250
+ const oldest = new Date(dateRange.oldest).toLocaleDateString('en-AU');
251
+ text += `| Date Range | ${oldest} to ${newest} |\n`;
252
+ }
253
+
254
+ if (totalItems > 100) {
255
+ text += `\n_Hint: Use list-emails-delta for efficient incremental sync of large folders._`;
256
+ }
257
+
258
+ return { text, meta };
259
+ }
260
+
261
+ // Full: All available information
262
+ let text = `# ${folder.displayName} - Full Statistics\n\n`;
263
+
264
+ text += `## Overview\n\n`;
265
+ text += `| Metric | Value |\n`;
266
+ text += `|--------|-------|\n`;
267
+ text += `| Folder ID | \`${folder.id}\` |\n`;
268
+ text += `| Display Name | ${folder.displayName} |\n`;
269
+ text += `| Total Items | ${totalItems.toLocaleString()} |\n`;
270
+ text += `| Unread Items | ${unreadItems.toLocaleString()} |\n`;
271
+ text += `| Read Items | ${(totalItems - unreadItems).toLocaleString()} |\n`;
272
+ text += `| Child Folders | ${folder.childFolderCount || 0} |\n`;
273
+ text += `| Hidden | ${folder.isHidden ? 'Yes' : 'No'} |\n`;
274
+
275
+ if (folder.parentFolderId) {
276
+ text += `| Parent Folder ID | \`${folder.parentFolderId}\` |\n`;
277
+ }
278
+
279
+ text += `\n## Pagination Planning\n\n`;
280
+ text += `| Setting | Value |\n`;
281
+ text += `|---------|-------|\n`;
282
+ text += `| Page Size | ${pageSize} emails |\n`;
283
+ text += `| Total Pages | ${totalPages} |\n`;
284
+ text += `| Estimated API Calls | ${totalPages} (list-emails) |\n`;
285
+
286
+ if (dateRange) {
287
+ const newestDate = new Date(dateRange.newest);
288
+ const oldestDate = new Date(dateRange.oldest);
289
+ const daysDiff = Math.ceil(
290
+ (newestDate - oldestDate) / (1000 * 60 * 60 * 24)
291
+ );
292
+
293
+ text += `\n## Date Range\n\n`;
294
+ text += `| Boundary | Date |\n`;
295
+ text += `|----------|------|\n`;
296
+ text += `| Newest | ${newestDate.toLocaleString('en-AU')} |\n`;
297
+ text += `| Oldest | ${oldestDate.toLocaleString('en-AU')} |\n`;
298
+ text += `| Span | ${daysDiff} days |\n`;
299
+
300
+ meta.dateRange = dateRange;
301
+ meta.spanDays = daysDiff;
302
+ }
303
+
304
+ text += `\n## Recommendations\n\n`;
305
+
306
+ if (totalItems > 1000) {
307
+ text += `- **Large folder**: Use \`list-emails-delta\` for incremental sync\n`;
308
+ text += `- **Use date filters**: \`receivedAfter\` and \`receivedBefore\` to narrow scope\n`;
309
+ } else if (totalItems > 100) {
310
+ text += `- **Medium folder**: Consider using \`list-emails-delta\` for efficient updates\n`;
311
+ } else {
312
+ text += `- **Small folder**: \`list-emails\` with default pagination is efficient\n`;
313
+ }
314
+
315
+ if (unreadItems > 50) {
316
+ text += `- **Many unread**: Use \`unreadOnly: true\` filter to reduce results\n`;
317
+ }
318
+
319
+ return { text, meta };
320
+ }
321
+
322
+ module.exports = handleGetFolderStats;
package/index.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Outlook Assistant Server - Main entry point
4
+ *
5
+ * A Model Context Protocol server that provides access to
6
+ * Microsoft Outlook through the Microsoft Graph API.
7
+ */
8
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
9
+ const {
10
+ StdioServerTransport,
11
+ } = require('@modelcontextprotocol/sdk/server/stdio.js');
12
+ const config = require('./config');
13
+
14
+ // Import module tools
15
+ const { authTools } = require('./auth');
16
+ const { calendarTools } = require('./calendar');
17
+ const { emailTools } = require('./email');
18
+ const { folderTools } = require('./folder');
19
+ const { rulesTools } = require('./rules');
20
+ const { contactsTools } = require('./contacts');
21
+ const { categoriesTools } = require('./categories');
22
+ const { settingsTools } = require('./settings');
23
+ const { advancedTools } = require('./advanced');
24
+
25
+ // Log startup information
26
+ console.error(`STARTING ${config.SERVER_NAME.toUpperCase()} MCP SERVER`);
27
+ console.error(`Test mode is ${config.USE_TEST_MODE ? 'enabled' : 'disabled'}`);
28
+
29
+ // Combine all tools
30
+ const TOOLS = [
31
+ ...authTools,
32
+ ...calendarTools,
33
+ ...emailTools,
34
+ ...folderTools,
35
+ ...rulesTools,
36
+ ...contactsTools,
37
+ ...categoriesTools,
38
+ ...settingsTools,
39
+ ...advancedTools,
40
+ ];
41
+
42
+ // Create server with tools capabilities
43
+ const server = new Server(
44
+ { name: config.SERVER_NAME, version: config.SERVER_VERSION },
45
+ {
46
+ capabilities: {
47
+ tools: TOOLS.reduce((acc, tool) => {
48
+ acc[tool.name] = {};
49
+ return acc;
50
+ }, {}),
51
+ },
52
+ }
53
+ );
54
+
55
+ // Handle all requests
56
+ server.fallbackRequestHandler = async (request) => {
57
+ try {
58
+ const { method, params, id } = request;
59
+ console.error(`REQUEST: ${method} [${id}]`);
60
+
61
+ // Initialize handler
62
+ if (method === 'initialize') {
63
+ console.error(`INITIALIZE REQUEST: ID [${id}]`);
64
+ return {
65
+ protocolVersion: '2024-11-05',
66
+ capabilities: {
67
+ tools: TOOLS.reduce((acc, tool) => {
68
+ acc[tool.name] = {};
69
+ return acc;
70
+ }, {}),
71
+ },
72
+ serverInfo: {
73
+ name: config.SERVER_NAME,
74
+ version: config.SERVER_VERSION,
75
+ },
76
+ };
77
+ }
78
+
79
+ // Tools list handler
80
+ if (method === 'tools/list') {
81
+ console.error(`TOOLS LIST REQUEST: ID [${id}]`);
82
+ console.error(`TOOLS COUNT: ${TOOLS.length}`);
83
+ console.error(`TOOLS NAMES: ${TOOLS.map((t) => t.name).join(', ')}`);
84
+
85
+ return {
86
+ tools: TOOLS.map((tool) => ({
87
+ name: tool.name,
88
+ description: tool.description,
89
+ inputSchema: tool.inputSchema,
90
+ ...(tool.annotations && { annotations: tool.annotations }),
91
+ })),
92
+ };
93
+ }
94
+
95
+ // Required empty responses for other capabilities
96
+ if (method === 'resources/list') return { resources: [] };
97
+ if (method === 'prompts/list') return { prompts: [] };
98
+
99
+ // Tool call handler
100
+ if (method === 'tools/call') {
101
+ try {
102
+ const { name, arguments: args = {} } = params || {};
103
+
104
+ console.error(`TOOL CALL: ${name}`);
105
+
106
+ // Find the tool handler
107
+ const tool = TOOLS.find((t) => t.name === name);
108
+
109
+ if (tool && tool.handler) {
110
+ return await tool.handler(args);
111
+ }
112
+
113
+ // Tool not found
114
+ return {
115
+ error: {
116
+ code: -32601,
117
+ message: `Tool not found: ${name}`,
118
+ },
119
+ };
120
+ } catch (error) {
121
+ console.error(`Error in tools/call:`, error);
122
+ return {
123
+ error: {
124
+ code: -32603,
125
+ message: `Error processing tool call: ${error.message}`,
126
+ },
127
+ };
128
+ }
129
+ }
130
+
131
+ // For any other method, return method not found
132
+ return {
133
+ error: {
134
+ code: -32601,
135
+ message: `Method not found: ${method}`,
136
+ },
137
+ };
138
+ } catch (error) {
139
+ console.error(`Error in fallbackRequestHandler:`, error);
140
+ return {
141
+ error: {
142
+ code: -32603,
143
+ message: `Error processing request: ${error.message}`,
144
+ },
145
+ };
146
+ }
147
+ };
148
+
149
+ // Make the script executable
150
+ process.on('SIGTERM', () => {
151
+ console.error('SIGTERM received but staying alive');
152
+ });
153
+
154
+ // Start the server
155
+ const transport = new StdioServerTransport();
156
+ server
157
+ .connect(transport)
158
+ .then(() => console.error(`${config.SERVER_NAME} connected and listening`))
159
+ .catch((error) => {
160
+ console.error(`Connection error: ${error.message}`);
161
+ process.exit(1);
162
+ });
package/llms.txt ADDED
@@ -0,0 +1,76 @@
1
+ # Outlook Assistant Server
2
+
3
+ > Give Claude full access to your Outlook email, calendar, and contacts through the Microsoft Graph API.
4
+
5
+ Built by [Little Bear Apps](https://littlebearapps.com).
6
+
7
+ ## Key Information
8
+
9
+ - **Package**: `@littlebearapps/outlook-assistant` on npm
10
+ - **Install**: `npm install -g @littlebearapps/outlook-assistant` or `npx @littlebearapps/outlook-assistant`
11
+ - **License**: MIT
12
+ - **Node.js**: >= 18.0.0
13
+ - **Authentication**: OAuth 2.0 with Microsoft Graph API (requires Azure app registration)
14
+ - **Tools**: 20 consolidated tools across 9 modules (reduced from 55 for optimal AI performance)
15
+
16
+ ## Why Outlook Assistant?
17
+
18
+ - Read, search, send, and export emails directly from Claude instead of switching apps
19
+ - 6 email tools covering search, conversations, attachments, and bulk export
20
+ - Manage calendar events, contacts, rules, categories, and mailbox settings in one place
21
+ - Export to multiple formats: MIME/EML, MBOX, Markdown, JSON, HTML
22
+
23
+ ## Key Differentiators
24
+
25
+ - **Progressive search**: Automatically falls back through 4 search strategies when Microsoft's `$search` API is unavailable (personal accounts) — most Graph API wrappers fail silently
26
+ - **Email forensics**: Built-in header analysis for DKIM, SPF, DMARC authentication, delivery chains, and spam scores — useful for phishing investigation and compliance
27
+ - **Delta sync**: Incremental inbox monitoring — returns only new, modified, and deleted emails since last check, with tokens for continuous polling
28
+ - **Batch operations**: Flag, move, export, or categorise multiple emails in a single tool call; search-driven export for batch archiving without collecting IDs
29
+ - **Compound automation**: Rules + categories + folders + Focused Inbox for complete inbox management in one conversation
30
+
31
+ ## Safety & Token Efficiency
32
+
33
+ - **MCP safety annotations** on all 20 tools — AI clients auto-approve reads and prompt for destructive operations
34
+ - **Send-email protections**: dry-run preview, session rate limiting, recipient allowlist
35
+ - **Token-optimised**: 20 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
36
+ - These safeguards reduce risk but are not foolproof — always review actions before approving
37
+
38
+ ## Quick Start
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "outlook": {
44
+ "command": "npx",
45
+ "args": ["@littlebearapps/outlook-assistant"],
46
+ "env": {
47
+ "OUTLOOK_CLIENT_ID": "your-application-client-id",
48
+ "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
49
+ }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ Requires an Azure app registration with Microsoft Graph delegated permissions. See README for full setup.
56
+
57
+ ## Tool Categories
58
+
59
+ - **Authentication (1 tool)**: `auth` — OAuth flow, status, about
60
+ - **Email (6 tools)**: `search-emails`, `read-email`, `send-email`, `update-email`, `attachments`, `export`
61
+ - **Calendar (3 tools)**: `list-events`, `create-event`, `manage-event`
62
+ - **Contacts (2 tools)**: `manage-contact`, `search-people`
63
+ - **Folders (1 tool)**: `folders` — list, create, move, stats
64
+ - **Rules (1 tool)**: `manage-rules` — list, create, reorder
65
+ - **Categories (3 tools)**: `manage-category`, `apply-category`, `manage-focused-inbox`
66
+ - **Settings (1 tool)**: `mailbox-settings` — get, set auto-replies, set working hours
67
+ - **Advanced (2 tools)**: `access-shared-mailbox`, `find-meeting-rooms`
68
+
69
+ ## Documentation
70
+
71
+ - [README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/README.md): Full documentation including setup, Azure configuration, and usage
72
+ - [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All 20 tools with parameters and safety annotations
73
+ - [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
74
+ - [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
75
+ - [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history
76
+ - [SECURITY](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/SECURITY.md): Security policy, token handling, and MCP safety controls