@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.
- package/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/advanced/index.js +652 -0
- package/auth/index.js +32 -0
- package/auth/oauth-server.js +233 -0
- package/auth/token-manager.js +105 -0
- package/auth/token-storage.js +359 -0
- package/auth/tools.js +159 -0
- package/calendar/accept.js +72 -0
- package/calendar/cancel.js +72 -0
- package/calendar/create.js +115 -0
- package/calendar/decline.js +72 -0
- package/calendar/delete.js +67 -0
- package/calendar/index.js +130 -0
- package/calendar/list.js +108 -0
- package/categories/index.js +955 -0
- package/config.js +95 -0
- package/contacts/index.js +754 -0
- package/email/attachments.js +365 -0
- package/email/conversations.js +666 -0
- package/email/delta.js +210 -0
- package/email/export.js +572 -0
- package/email/folder-utils.js +192 -0
- package/email/headers.js +344 -0
- package/email/index.js +537 -0
- package/email/list.js +136 -0
- package/email/mark-as-read.js +114 -0
- package/email/mime.js +286 -0
- package/email/read.js +161 -0
- package/email/search.js +628 -0
- package/email/send.js +169 -0
- package/folder/create.js +137 -0
- package/folder/delete.js +108 -0
- package/folder/index.js +112 -0
- package/folder/list.js +289 -0
- package/folder/move.js +186 -0
- package/folder/stats.js +322 -0
- package/index.js +162 -0
- package/llms.txt +76 -0
- package/outlook-auth-server.js +384 -0
- package/package.json +97 -0
- package/rules/create.js +273 -0
- package/rules/index.js +276 -0
- package/rules/list.js +216 -0
- package/settings/index.js +678 -0
- package/utils/field-presets.js +311 -0
- package/utils/graph-api.js +268 -0
- package/utils/mock-data.js +154 -0
- package/utils/odata-helpers.js +33 -0
- package/utils/response-formatter.js +457 -0
- package/utils/safety.js +123 -0
package/folder/stats.js
ADDED
|
@@ -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
|