@littlebearapps/outlook-assistant 3.12.1 → 3.14.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 +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
package/folder/delete.js
CHANGED
|
@@ -3,8 +3,108 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
-
const { resolveFolder, WELL_KNOWN } = require('./resolve');
|
|
6
|
+
const { resolveFolder, listChildFolders, WELL_KNOWN } = require('./resolve');
|
|
7
7
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
8
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
9
|
+
const { dryRunResult } = require('../utils/safety');
|
|
10
|
+
|
|
11
|
+
const COUNT_FIELDS = 'totalItemCount,unreadItemCount';
|
|
12
|
+
const COUNT_SELECT = `id,displayName,childFolderCount,${COUNT_FIELDS}`;
|
|
13
|
+
/** Most subfolders a dry run counts before reporting "at least". */
|
|
14
|
+
const PREVIEW_FOLDER_LIMIT = 100;
|
|
15
|
+
|
|
16
|
+
const LOSS_NOTE =
|
|
17
|
+
"It doesn't go to Deleted Items, and Graph doesn't document a way to restore it (Outlook's \"Recover deleted items\" may work for a limited time, but don't rely on it). Move out anything you need first.";
|
|
18
|
+
|
|
19
|
+
function plural(count, word) {
|
|
20
|
+
return `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The folder's quoted path, or its ID if Graph gave it no name. */
|
|
24
|
+
function folderLabel(resolved, quote = "'") {
|
|
25
|
+
return resolved.path
|
|
26
|
+
? `${quote}${resolved.path}${quote}`
|
|
27
|
+
: `(unnamed, id ${resolved.id})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Count every subfolder below `root`, and the items in them, breadth-first.
|
|
32
|
+
* Stops after PREVIEW_FOLDER_LIMIT subfolders and flags the count partial.
|
|
33
|
+
*/
|
|
34
|
+
async function countSubfolders(accessToken, root, mailbox) {
|
|
35
|
+
let subfolders = 0;
|
|
36
|
+
let items = 0;
|
|
37
|
+
let partial = false;
|
|
38
|
+
const queue = root.childFolderCount > 0 ? [root.id] : [];
|
|
39
|
+
while (queue.length > 0) {
|
|
40
|
+
if (subfolders >= PREVIEW_FOLDER_LIMIT) {
|
|
41
|
+
partial = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const children = await listChildFolders(
|
|
45
|
+
accessToken,
|
|
46
|
+
queue.shift(),
|
|
47
|
+
COUNT_SELECT,
|
|
48
|
+
mailbox
|
|
49
|
+
);
|
|
50
|
+
for (const child of children) {
|
|
51
|
+
subfolders += 1;
|
|
52
|
+
items += child.totalItemCount || 0;
|
|
53
|
+
if (child.childFolderCount > 0) queue.push(child.id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { subfolders, items, partial };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* dryRun preview for delete (#274): the folder, and the items and
|
|
61
|
+
* subfolders that would be lost with it. Reads only, and reads the folder
|
|
62
|
+
* itself only if resolving it didn't already (`resolved.fields`).
|
|
63
|
+
*/
|
|
64
|
+
async function previewDeleteFolder(accessToken, resolved, mailbox) {
|
|
65
|
+
const prefix = buildMailboxPrefix(mailbox);
|
|
66
|
+
const folder =
|
|
67
|
+
resolved.fields ||
|
|
68
|
+
(await callGraphAPI(
|
|
69
|
+
accessToken,
|
|
70
|
+
'GET',
|
|
71
|
+
`${prefix}/mailFolders/${resolved.id}`,
|
|
72
|
+
null,
|
|
73
|
+
{ $select: COUNT_SELECT }
|
|
74
|
+
));
|
|
75
|
+
const items = folder.totalItemCount || 0;
|
|
76
|
+
const unread = folder.unreadItemCount || 0;
|
|
77
|
+
const below = await countSubfolders(
|
|
78
|
+
accessToken,
|
|
79
|
+
{ ...folder, id: resolved.id },
|
|
80
|
+
mailbox
|
|
81
|
+
);
|
|
82
|
+
const where = mailbox ? ` in ${mailbox}` : '';
|
|
83
|
+
const name = `${folderLabel(resolved)}${where}`;
|
|
84
|
+
|
|
85
|
+
let summary;
|
|
86
|
+
if (items === 0 && below.subfolders === 0) {
|
|
87
|
+
summary = `Deletes folder ${name}. It's empty: no items or subfolders.`;
|
|
88
|
+
} else {
|
|
89
|
+
const atLeast = below.partial ? 'at least ' : '';
|
|
90
|
+
summary = `Deletes folder ${name} and everything in it: ${plural(items, 'item')}`;
|
|
91
|
+
if (unread > 0) summary += ` (${unread} unread)`;
|
|
92
|
+
if (below.subfolders > 0) {
|
|
93
|
+
summary += `, plus ${atLeast}${plural(below.subfolders, 'subfolder')} holding ${atLeast}${below.items} more ${below.items === 1 ? 'item' : 'items'}`;
|
|
94
|
+
}
|
|
95
|
+
summary += '.';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return dryRunResult([summary, LOSS_NOTE], {
|
|
99
|
+
action: 'delete',
|
|
100
|
+
folderId: resolved.id,
|
|
101
|
+
path: resolved.path,
|
|
102
|
+
items,
|
|
103
|
+
subfolders: below.subfolders,
|
|
104
|
+
subfolderItems: below.items,
|
|
105
|
+
partial: below.partial,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
8
108
|
|
|
9
109
|
/**
|
|
10
110
|
* Delete folder handler
|
|
@@ -22,31 +122,19 @@ const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
|
22
122
|
* @returns {object} - MCP response
|
|
23
123
|
*/
|
|
24
124
|
async function handleDeleteFolder(args) {
|
|
25
|
-
const { folderId, folderName } = args;
|
|
125
|
+
const { folderId, folderName, dryRun = false } = args;
|
|
26
126
|
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
27
127
|
const prefix = buildMailboxPrefix(sharedMailbox);
|
|
28
128
|
|
|
29
129
|
if (!folderId && !folderName) {
|
|
30
|
-
return
|
|
31
|
-
content: [
|
|
32
|
-
{
|
|
33
|
-
type: 'text',
|
|
34
|
-
text: 'Either folderId or folderName is required.',
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
};
|
|
130
|
+
return toolError('Either folderId or folderName is required.');
|
|
38
131
|
}
|
|
39
132
|
|
|
40
133
|
// Name/alias guard for the common accidental case.
|
|
41
134
|
if (folderName && WELL_KNOWN[folderName.toLowerCase().trim()]) {
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
type: 'text',
|
|
46
|
-
text: `Cannot delete protected folder "${folderName}". Protected folders: Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox.`,
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
};
|
|
135
|
+
return toolError(
|
|
136
|
+
`Cannot delete protected folder "${folderName}". Protected folders: Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox.`
|
|
137
|
+
);
|
|
50
138
|
}
|
|
51
139
|
|
|
52
140
|
try {
|
|
@@ -60,11 +148,16 @@ async function handleDeleteFolder(args) {
|
|
|
60
148
|
id: folderId,
|
|
61
149
|
name: folderName,
|
|
62
150
|
mailbox: sharedMailbox,
|
|
151
|
+
// A dry run by ID reads the counts in the same request.
|
|
152
|
+
...(dryRun && { extraSelect: COUNT_FIELDS }),
|
|
63
153
|
});
|
|
64
154
|
} catch (resolveError) {
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
155
|
+
return toolError(resolveError.message);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// dryRun: say what would be lost; delete nothing.
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
return await previewDeleteFolder(accessToken, resolved, sharedMailbox);
|
|
68
161
|
}
|
|
69
162
|
|
|
70
163
|
// Delete the folder
|
|
@@ -77,29 +170,15 @@ async function handleDeleteFolder(args) {
|
|
|
77
170
|
content: [
|
|
78
171
|
{
|
|
79
172
|
type: 'text',
|
|
80
|
-
text: `Folder
|
|
173
|
+
text: `Folder ${folderLabel(resolved, '"')} deleted successfully.`,
|
|
81
174
|
},
|
|
82
175
|
],
|
|
83
176
|
};
|
|
84
177
|
} catch (error) {
|
|
85
178
|
if (error.message === 'Authentication required') {
|
|
86
|
-
return
|
|
87
|
-
content: [
|
|
88
|
-
{
|
|
89
|
-
type: 'text',
|
|
90
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
};
|
|
179
|
+
return authRequiredError();
|
|
94
180
|
}
|
|
95
|
-
return {
|
|
96
|
-
content: [
|
|
97
|
-
{
|
|
98
|
-
type: 'text',
|
|
99
|
-
text: `Error deleting folder: ${error.message}`,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
};
|
|
181
|
+
return toolError(`Error deleting folder: ${error.message}`);
|
|
103
182
|
}
|
|
104
183
|
}
|
|
105
184
|
|
package/folder/index.js
CHANGED
|
@@ -6,19 +6,17 @@ const handleCreateFolder = require('./create');
|
|
|
6
6
|
const handleMoveEmails = require('./move');
|
|
7
7
|
const handleGetFolderStats = require('./stats');
|
|
8
8
|
const handleDeleteFolder = require('./delete');
|
|
9
|
+
const { toolMetadata } = require('../utils/risk-classes');
|
|
10
|
+
const { toolError } = require('../utils/tool-error');
|
|
11
|
+
const { dryRunUnsupported } = require('../utils/safety');
|
|
9
12
|
|
|
10
13
|
// Consolidated folder tool definition
|
|
11
14
|
const folderTools = [
|
|
12
15
|
{
|
|
13
16
|
name: 'folders',
|
|
14
17
|
description:
|
|
15
|
-
"Manage mail folders
|
|
16
|
-
|
|
17
|
-
title: 'Mail Folders',
|
|
18
|
-
readOnlyHint: false,
|
|
19
|
-
destructiveHint: true,
|
|
20
|
-
openWorldHint: false,
|
|
21
|
-
},
|
|
18
|
+
"Manage mail folders. Address a folder by name, by slash-separated path for nested folders (e.g. `Inbox/Clients/Acme`, case-insensitive) or by ID; a bare name matches a unique top-level folder first, then nested ones (an ambiguous name returns the candidates). action=`list` (default) returns the tree with each folder's path and id (`includeItemCounts`, `includeChildren`). action=`create` makes `name` under the root or `parentFolder`/`parentFolderId`. action=`move` moves `emailIds` into `targetFolder`/`targetFolderId`. action=`stats` returns total/unread counts for `folder` or `folderId`. action=`delete` removes a folder (`folderName`/path or `folderId`) with everything in it, subfolders included. It skips Deleted Items and Graph documents no restore path, so pass `dryRun: true` first to see what would be lost. Protected folders (Inbox, Sent Items, etc.) can't be deleted. Every action accepts `sharedMailbox` (alias `email`) to work in a shared or delegated mailbox (default: your own).",
|
|
19
|
+
...toolMetadata('folders', 'Mail Folders'),
|
|
22
20
|
inputSchema: {
|
|
23
21
|
type: 'object',
|
|
24
22
|
properties: {
|
|
@@ -40,7 +38,7 @@ const folderTools = [
|
|
|
40
38
|
sharedMailbox: {
|
|
41
39
|
type: 'string',
|
|
42
40
|
description:
|
|
43
|
-
'Email address of a shared/delegated mailbox to target
|
|
41
|
+
'Email address of a shared/delegated mailbox to target (all actions; default: the signed-in account). Requires delegate access + Mail.Read.Shared (list/stats) or Mail.ReadWrite.Shared (create/move/delete). Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
44
42
|
},
|
|
45
43
|
email: {
|
|
46
44
|
type: 'string',
|
|
@@ -102,12 +100,20 @@ const folderTools = [
|
|
|
102
100
|
description:
|
|
103
101
|
'Folder name or path to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
|
|
104
102
|
},
|
|
103
|
+
dryRun: {
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
description:
|
|
106
|
+
'Preview only (action=delete): nothing is deleted. Shows the folder and how many items and subfolders would be lost. Other actions refuse dryRun and change nothing. Default false.',
|
|
107
|
+
},
|
|
105
108
|
},
|
|
106
109
|
additionalProperties: false,
|
|
107
110
|
required: [],
|
|
108
111
|
},
|
|
109
112
|
handler: async (args) => {
|
|
110
113
|
const action = args.action || 'list';
|
|
114
|
+
if (args.dryRun && action !== 'delete') {
|
|
115
|
+
return dryRunUnsupported('folders', action, 'delete');
|
|
116
|
+
}
|
|
111
117
|
switch (action) {
|
|
112
118
|
case 'create':
|
|
113
119
|
return handleCreateFolder(args);
|
|
@@ -120,14 +126,9 @@ const folderTools = [
|
|
|
120
126
|
case 'list':
|
|
121
127
|
return handleListFolders(args);
|
|
122
128
|
default:
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
type: 'text',
|
|
127
|
-
text: `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
};
|
|
129
|
+
return toolError(
|
|
130
|
+
`Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`
|
|
131
|
+
);
|
|
131
132
|
}
|
|
132
133
|
},
|
|
133
134
|
},
|
package/folder/list.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { ensureAuthenticated } = require('../auth');
|
|
5
5
|
const { listChildFolders } = require('./resolve');
|
|
6
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
7
|
+
const { log } = require('../utils/logger');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* List folders handler
|
|
@@ -52,24 +54,10 @@ async function handleListFolders(args) {
|
|
|
52
54
|
};
|
|
53
55
|
} catch (error) {
|
|
54
56
|
if (error.message === 'Authentication required') {
|
|
55
|
-
return
|
|
56
|
-
content: [
|
|
57
|
-
{
|
|
58
|
-
type: 'text',
|
|
59
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
57
|
+
return authRequiredError();
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
return {
|
|
66
|
-
content: [
|
|
67
|
-
{
|
|
68
|
-
type: 'text',
|
|
69
|
-
text: `Error listing folders: ${error.message}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
};
|
|
60
|
+
return toolError(`Error listing folders: ${error.message}`);
|
|
73
61
|
}
|
|
74
62
|
}
|
|
75
63
|
|
|
@@ -133,7 +121,7 @@ async function getAllFoldersHierarchy(
|
|
|
133
121
|
sharedMailbox
|
|
134
122
|
);
|
|
135
123
|
} catch (error) {
|
|
136
|
-
|
|
124
|
+
log.debug(
|
|
137
125
|
`Error getting child folders for "${folder.displayName}": ${error.message}`
|
|
138
126
|
);
|
|
139
127
|
warnings.push(
|
package/folder/move.js
CHANGED
|
@@ -5,6 +5,8 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
6
|
const { resolveFolder } = require('./resolve');
|
|
7
7
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
8
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
9
|
+
const { log } = require('../utils/logger');
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Move emails handler
|
|
@@ -19,25 +21,15 @@ async function handleMoveEmails(args) {
|
|
|
19
21
|
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
20
22
|
|
|
21
23
|
if (!emailIds) {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type: 'text',
|
|
26
|
-
text: 'Email IDs are required. Please provide a comma-separated list of email IDs to move.',
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
24
|
+
return toolError(
|
|
25
|
+
'Email IDs are required. Please provide a comma-separated list of email IDs to move.'
|
|
26
|
+
);
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
if (!targetFolder && !targetFolderId) {
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type: 'text',
|
|
37
|
-
text: 'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.',
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
};
|
|
30
|
+
return toolError(
|
|
31
|
+
'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.'
|
|
32
|
+
);
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
try {
|
|
@@ -51,14 +43,7 @@ async function handleMoveEmails(args) {
|
|
|
51
43
|
.filter((id) => id);
|
|
52
44
|
|
|
53
45
|
if (ids.length === 0) {
|
|
54
|
-
return
|
|
55
|
-
content: [
|
|
56
|
-
{
|
|
57
|
-
type: 'text',
|
|
58
|
-
text: 'No valid email IDs provided.',
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
};
|
|
46
|
+
return toolError('No valid email IDs provided.');
|
|
62
47
|
}
|
|
63
48
|
|
|
64
49
|
// Move emails
|
|
@@ -85,24 +70,10 @@ async function handleMoveEmails(args) {
|
|
|
85
70
|
};
|
|
86
71
|
} catch (error) {
|
|
87
72
|
if (error.message === 'Authentication required') {
|
|
88
|
-
return
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: 'text',
|
|
92
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
};
|
|
73
|
+
return authRequiredError();
|
|
96
74
|
}
|
|
97
75
|
|
|
98
|
-
return {
|
|
99
|
-
content: [
|
|
100
|
-
{
|
|
101
|
-
type: 'text',
|
|
102
|
-
text: `Error moving emails: ${error.message}`,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
};
|
|
76
|
+
return toolError(`Error moving emails: ${error.message}`);
|
|
106
77
|
}
|
|
107
78
|
}
|
|
108
79
|
|
|
@@ -157,7 +128,7 @@ async function moveEmailsToFolder(
|
|
|
157
128
|
newId: moved?.id || emailId,
|
|
158
129
|
});
|
|
159
130
|
} catch (error) {
|
|
160
|
-
|
|
131
|
+
log.debug(`Error moving email ${emailId}: ${error.message}`);
|
|
161
132
|
results.failed.push({
|
|
162
133
|
id: emailId,
|
|
163
134
|
error: error.message,
|
|
@@ -203,7 +174,7 @@ async function moveEmailsToFolder(
|
|
|
203
174
|
results,
|
|
204
175
|
};
|
|
205
176
|
} catch (error) {
|
|
206
|
-
|
|
177
|
+
log.debug(`Error in moveEmailsToFolder: ${error.message}`);
|
|
207
178
|
throw error;
|
|
208
179
|
}
|
|
209
180
|
}
|
package/folder/resolve.js
CHANGED
|
@@ -132,15 +132,18 @@ async function resolveWellKnown(accessToken, alias, mailbox) {
|
|
|
132
132
|
return toRecord(resp, resp.displayName);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
async function resolveById(accessToken, id, mailbox) {
|
|
135
|
+
async function resolveById(accessToken, id, mailbox, extraSelect) {
|
|
136
136
|
const resp = await callGraphAPI(
|
|
137
137
|
accessToken,
|
|
138
138
|
'GET',
|
|
139
139
|
`${buildMailboxPrefix(mailbox)}/mailFolders/${id}`,
|
|
140
140
|
null,
|
|
141
|
-
{ $select: FOLDER_SELECT }
|
|
141
|
+
{ $select: extraSelect ? `${FOLDER_SELECT},${extraSelect}` : FOLDER_SELECT }
|
|
142
142
|
);
|
|
143
|
-
|
|
143
|
+
// The ID asked for stands in if a response leaves `id` out.
|
|
144
|
+
const folder = { ...resp, id: resp?.id || id };
|
|
145
|
+
const record = toRecord(folder, folder.displayName);
|
|
146
|
+
return extraSelect ? { ...record, fields: folder } : record;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/**
|
|
@@ -300,8 +303,10 @@ function looksLikeFolderId(value) {
|
|
|
300
303
|
/**
|
|
301
304
|
* Resolve a folder from a name/path and/or explicit ID.
|
|
302
305
|
* @param {string} accessToken
|
|
303
|
-
* @param {{name?: string, id?: string, mailbox?: string|null}} spec
|
|
304
|
-
*
|
|
306
|
+
* @param {{name?: string, id?: string, mailbox?: string|null, extraSelect?: string}} spec
|
|
307
|
+
* `extraSelect`: more fields to read when resolving by ID (e.g. item
|
|
308
|
+
* counts), returned as `fields` so the caller needn't read the folder again
|
|
309
|
+
* @returns {Promise<{id: string, displayName: string, parentId: string|null, path: string, fields?: object}>}
|
|
305
310
|
* `path` is the full slash-separated path when resolved by name/path/alias;
|
|
306
311
|
* when resolved by ID it is the folder's display name only (ancestors are not
|
|
307
312
|
* fetched).
|
|
@@ -310,7 +315,7 @@ async function resolveFolder(accessToken, spec = {}) {
|
|
|
310
315
|
const mailbox = spec.mailbox || null;
|
|
311
316
|
const id = (spec.id || '').trim();
|
|
312
317
|
if (id) {
|
|
313
|
-
return resolveById(accessToken, id, mailbox);
|
|
318
|
+
return resolveById(accessToken, id, mailbox, spec.extraSelect);
|
|
314
319
|
}
|
|
315
320
|
const name = (spec.name || '').trim();
|
|
316
321
|
if (!name) {
|
package/folder/stats.js
CHANGED
|
@@ -9,6 +9,8 @@ const { ensureAuthenticated } = require('../auth');
|
|
|
9
9
|
const { resolveFolder } = require('./resolve');
|
|
10
10
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
11
11
|
const config = require('../config');
|
|
12
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
13
|
+
const { log } = require('../utils/logger');
|
|
12
14
|
|
|
13
15
|
const { VERBOSITY, DEFAULT_LIMITS } = config;
|
|
14
16
|
|
|
@@ -36,9 +38,7 @@ async function handleGetFolderStats(args) {
|
|
|
36
38
|
mailbox: sharedMailbox,
|
|
37
39
|
});
|
|
38
40
|
} catch (resolveError) {
|
|
39
|
-
return
|
|
40
|
-
content: [{ type: 'text', text: resolveError.message }],
|
|
41
|
-
};
|
|
41
|
+
return toolError(resolveError.message);
|
|
42
42
|
}
|
|
43
43
|
const folderId = resolved.id;
|
|
44
44
|
|
|
@@ -75,24 +75,10 @@ async function handleGetFolderStats(args) {
|
|
|
75
75
|
};
|
|
76
76
|
} catch (error) {
|
|
77
77
|
if (error.message === 'Authentication required') {
|
|
78
|
-
return
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: 'text',
|
|
82
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
};
|
|
78
|
+
return authRequiredError();
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
return {
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: 'text',
|
|
92
|
-
text: `Error getting folder stats: ${error.message}`,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
};
|
|
81
|
+
return toolError(`Error getting folder stats: ${error.message}`);
|
|
96
82
|
}
|
|
97
83
|
}
|
|
98
84
|
|
|
@@ -139,7 +125,7 @@ async function getEmailDateRange(accessToken, folderId, mailbox = null) {
|
|
|
139
125
|
return { newest, oldest };
|
|
140
126
|
}
|
|
141
127
|
} catch (error) {
|
|
142
|
-
|
|
128
|
+
log.debug(`Error getting date range: ${error.message}`);
|
|
143
129
|
}
|
|
144
130
|
|
|
145
131
|
return null;
|
package/index.js
CHANGED
|
@@ -31,11 +31,15 @@ stdio. It is normally launched by an MCP client (Claude Desktop, Claude Code)
|
|
|
31
31
|
rather than run by hand — started from a terminal it will simply wait on stdin.
|
|
32
32
|
|
|
33
33
|
Key environment variables:
|
|
34
|
-
OUTLOOK_CLIENT_ID Azure
|
|
35
|
-
|
|
34
|
+
OUTLOOK_CLIENT_ID Azure Application (client) ID. If you can't set env
|
|
35
|
+
vars, pass it to the auth tool instead (action=authenticate
|
|
36
|
+
clientId=<id>); it's saved to ~/.outlook-assistant-config.json
|
|
37
|
+
OUTLOOK_CLIENT_SECRET Client secret VALUE (not the Secret ID); browser flow only
|
|
36
38
|
OUTLOOK_AUTH_METHOD device-code (default) | browser
|
|
37
39
|
OUTLOOK_AUTH_AUDIENCE common | consumers | organizations | <tenant-guid>
|
|
38
40
|
OUTLOOK_SHARED_MAILBOX Opt in to shared mailboxes: read | true (work/school only)
|
|
41
|
+
OUTLOOK_READ_ONLY Set to "true" to refuse every tool call that would change,
|
|
42
|
+
send or delete anything (reads and sign-in still work)
|
|
39
43
|
OUTLOOK_ALLOWED_RECIPIENTS Comma-separated recipient allowlist
|
|
40
44
|
OUTLOOK_MAX_EMAILS_PER_SESSION Default cap per session for every rate-limited tool
|
|
41
45
|
(send-email, draft, manage-rules); 0 or unset = no cap
|
|
@@ -45,6 +49,9 @@ Key environment variables:
|
|
|
45
49
|
OUTLOOK_IMMUTABLE_IDS Set to "true" for message IDs that survive folder moves
|
|
46
50
|
OUTLOOK_SEARCH_SCAN_LIMIT Local search fallback window (default 500, max 5000)
|
|
47
51
|
OUTLOOK_REQUEST_TIMEOUT_MS Graph request inactivity timeout (default 60000)
|
|
52
|
+
OUTLOOK_DEBUG Set to "true" for detailed stderr logs (addresses redacted)
|
|
53
|
+
OUTLOOK_EXPORT_DIR Extra folder export/attachment downloads may write to
|
|
54
|
+
(besides the temp directory, ~/Downloads, ~/Documents)
|
|
48
55
|
USE_TEST_MODE Set to "true" to run against mock data
|
|
49
56
|
|
|
50
57
|
Documentation: https://github.com/littlebearapps/outlook-assistant`;
|
|
@@ -72,27 +79,26 @@ Documentation: https://github.com/littlebearapps/outlook-assistant`;
|
|
|
72
79
|
process.exit(0);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
76
82
|
const {
|
|
77
83
|
StdioServerTransport,
|
|
78
84
|
} = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
79
85
|
const config = require('./config');
|
|
80
|
-
const {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const {
|
|
84
|
-
const {
|
|
85
|
-
const { emailTools } = require('./email');
|
|
86
|
-
const { folderTools } = require('./folder');
|
|
87
|
-
const { rulesTools } = require('./rules');
|
|
88
|
-
const { contactsTools } = require('./contacts');
|
|
89
|
-
const { categoriesTools } = require('./categories');
|
|
90
|
-
const { settingsTools } = require('./settings');
|
|
91
|
-
const { advancedTools } = require('./advanced');
|
|
86
|
+
const { createServer } = require('./server');
|
|
87
|
+
|
|
88
|
+
const { setToolCount } = require('./auth');
|
|
89
|
+
const { TOOLS } = require('./tools');
|
|
90
|
+
const { isDebugEnabled } = require('./utils/logger');
|
|
92
91
|
|
|
93
92
|
// Log startup information
|
|
94
|
-
console.error(
|
|
93
|
+
console.error(
|
|
94
|
+
`STARTING ${config.SERVER_NAME.toUpperCase()} MCP SERVER v${config.SERVER_VERSION}`
|
|
95
|
+
);
|
|
95
96
|
console.error(`Test mode is ${config.USE_TEST_MODE ? 'enabled' : 'disabled'}`);
|
|
97
|
+
if (isDebugEnabled()) {
|
|
98
|
+
console.error(
|
|
99
|
+
'Debug logging is on (OUTLOOK_DEBUG): stderr includes search terms, subjects and Graph errors, with addresses and IDs redacted.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
96
102
|
|
|
97
103
|
// F-1 / F-48: warn at startup when safety belts are unset. Mirrors the
|
|
98
104
|
// warning surfaced by `auth action=about`. Visible to operators reading
|
|
@@ -107,38 +113,10 @@ if (
|
|
|
107
113
|
);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
// Combine all tools
|
|
111
|
-
const TOOLS = [
|
|
112
|
-
...authTools,
|
|
113
|
-
...calendarTools,
|
|
114
|
-
...emailTools,
|
|
115
|
-
...folderTools,
|
|
116
|
-
...rulesTools,
|
|
117
|
-
...contactsTools,
|
|
118
|
-
...categoriesTools,
|
|
119
|
-
...settingsTools,
|
|
120
|
-
...advancedTools,
|
|
121
|
-
];
|
|
122
|
-
|
|
123
116
|
// Set dynamic tool count for auth about handler
|
|
124
117
|
setToolCount(TOOLS.length);
|
|
125
118
|
|
|
126
|
-
|
|
127
|
-
const server = new Server(
|
|
128
|
-
{ name: config.SERVER_NAME, version: config.SERVER_VERSION },
|
|
129
|
-
{
|
|
130
|
-
capabilities: {
|
|
131
|
-
tools: TOOLS.reduce((acc, tool) => {
|
|
132
|
-
acc[tool.name] = {};
|
|
133
|
-
return acc;
|
|
134
|
-
}, {}),
|
|
135
|
-
},
|
|
136
|
-
}
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
// Handle all requests. Dispatch + error-shaping logic lives in
|
|
140
|
-
// request-handler.js so it is unit-testable without starting the transport.
|
|
141
|
-
server.fallbackRequestHandler = createRequestHandler(TOOLS);
|
|
119
|
+
const server = createServer(TOOLS);
|
|
142
120
|
|
|
143
121
|
// Make the script executable
|
|
144
122
|
process.on('SIGTERM', () => {
|