@littlebearapps/outlook-assistant 3.14.0 → 3.14.1
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 +5 -2
- package/README.md +6 -6
- package/advanced/index.js +1 -1
- package/auth/tools.js +16 -6
- package/calendar/index.js +2 -2
- package/calendar/preview.js +126 -0
- package/calendar/update.js +17 -1
- package/categories/index.js +10 -2
- package/email/attachments.js +1 -1
- package/email/delta.js +59 -12
- package/email/draft.js +41 -18
- package/email/export.js +6 -2
- package/email/index.js +4 -4
- package/email/mime.js +25 -2
- package/email/search.js +1 -1
- package/folder/index.js +2 -1
- package/folder/stats.js +12 -7
- package/index.js +20 -2
- package/llms-install.md +1 -1
- package/llms.txt +10 -10
- package/package.json +1 -1
- package/rules/create.js +1 -1
- package/rules/index.js +23 -2
- package/rules/list.js +2 -2
- package/rules/rule-builder.js +2 -2
- package/rules/update.js +1 -1
- package/server.js +6 -2
- package/settings/index.js +23 -5
- package/utils/safety.js +139 -18
- package/utils/server-instructions.js +14 -3
package/email/draft.js
CHANGED
|
@@ -8,6 +8,7 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
9
|
const {
|
|
10
10
|
checkRateLimit,
|
|
11
|
+
releaseRateLimit,
|
|
11
12
|
checkRecipientAllowlist,
|
|
12
13
|
findBlockedRecipients,
|
|
13
14
|
getRecipientAllowlist,
|
|
@@ -86,7 +87,7 @@ function buildMessageObject(args) {
|
|
|
86
87
|
/**
|
|
87
88
|
* Format a draft response with key details
|
|
88
89
|
* @param {object} draft - Graph API message response
|
|
89
|
-
* @param {string} actionLabel -
|
|
90
|
+
* @param {string} actionLabel - Heading, e.g. "Draft created" or "Reply draft created"
|
|
90
91
|
* @returns {object} - MCP response
|
|
91
92
|
*/
|
|
92
93
|
function formatDraftResponse(draft, actionLabel) {
|
|
@@ -94,7 +95,7 @@ function formatDraftResponse(draft, actionLabel) {
|
|
|
94
95
|
.map((r) => r.emailAddress?.address)
|
|
95
96
|
.join(', ');
|
|
96
97
|
|
|
97
|
-
let text =
|
|
98
|
+
let text = `${actionLabel}.\n\n`;
|
|
98
99
|
text += `**ID**: \`${draft.id}\`\n`;
|
|
99
100
|
if (draft.subject) text += `**Subject**: ${draft.subject}\n`;
|
|
100
101
|
if (to) text += `**To**: ${to}\n`;
|
|
@@ -220,7 +221,7 @@ async function handleCreateDraft(args) {
|
|
|
220
221
|
const tipsText = tipsResult.content[0]?.text || '';
|
|
221
222
|
|
|
222
223
|
if (dryRun) {
|
|
223
|
-
const preview = formatDryRunPreview({ message,
|
|
224
|
+
const preview = formatDryRunPreview({ message, isDraft: true });
|
|
224
225
|
return {
|
|
225
226
|
content: [
|
|
226
227
|
{
|
|
@@ -238,7 +239,7 @@ async function handleCreateDraft(args) {
|
|
|
238
239
|
|
|
239
240
|
// Dry-run mode: preview without saving
|
|
240
241
|
if (dryRun) {
|
|
241
|
-
const preview = formatDryRunPreview({ message,
|
|
242
|
+
const preview = formatDryRunPreview({ message, isDraft: true });
|
|
242
243
|
return {
|
|
243
244
|
content: [
|
|
244
245
|
{
|
|
@@ -264,7 +265,7 @@ async function handleCreateDraft(args) {
|
|
|
264
265
|
'me/messages',
|
|
265
266
|
message
|
|
266
267
|
);
|
|
267
|
-
const response = formatDraftResponse(draft, 'created');
|
|
268
|
+
const response = formatDraftResponse(draft, 'Draft created');
|
|
268
269
|
if (tipsResult) {
|
|
269
270
|
response.content[0].text += `\n---\n\n${tipsResult.content[0]?.text || ''}`;
|
|
270
271
|
response._meta.mailTips = tipsResult._meta;
|
|
@@ -312,7 +313,7 @@ async function handleUpdateDraft(args) {
|
|
|
312
313
|
`me/messages/${id}`,
|
|
313
314
|
message
|
|
314
315
|
);
|
|
315
|
-
return formatDraftResponse(draft, 'updated');
|
|
316
|
+
return formatDraftResponse(draft, 'Draft updated');
|
|
316
317
|
} catch (error) {
|
|
317
318
|
return handleError('updating draft', error);
|
|
318
319
|
}
|
|
@@ -426,14 +427,17 @@ async function handleReplyDraft(args, endpoint) {
|
|
|
426
427
|
|
|
427
428
|
const actionName = endpoint === 'createReplyAll' ? 'reply-all' : 'reply';
|
|
428
429
|
|
|
429
|
-
//
|
|
430
|
-
//
|
|
430
|
+
// Taken before the draft is created, and given back only when nothing is
|
|
431
|
+
// definitely left behind (#299): Graph rejected the create with a 4xx, or
|
|
432
|
+
// the allowlist refused the reply and its draft was deleted again. A
|
|
433
|
+
// timeout, 5xx or failed delete may leave a draft, so the slot stays used.
|
|
431
434
|
const rateLimitError = checkRateLimit('draft');
|
|
432
435
|
if (rateLimitError) return rateLimitError;
|
|
433
436
|
|
|
437
|
+
let draft;
|
|
434
438
|
try {
|
|
435
439
|
const accessToken = await ensureAuthenticated();
|
|
436
|
-
|
|
440
|
+
draft = await callGraphAPI(
|
|
437
441
|
accessToken,
|
|
438
442
|
'POST',
|
|
439
443
|
`me/messages/${id}/${endpoint}`,
|
|
@@ -443,10 +447,24 @@ async function handleReplyDraft(args, endpoint) {
|
|
|
443
447
|
// Graph fills in the recipients from the original message, so they can
|
|
444
448
|
// only be checked once the draft exists.
|
|
445
449
|
const refusal = await refuseBlockedReply(accessToken, draft, actionName);
|
|
446
|
-
if (refusal)
|
|
450
|
+
if (refusal) {
|
|
451
|
+
if (refusal.draftDeleted) releaseRateLimit('draft');
|
|
452
|
+
return refusal.error;
|
|
453
|
+
}
|
|
447
454
|
|
|
448
|
-
return formatDraftResponse(
|
|
455
|
+
return formatDraftResponse(
|
|
456
|
+
draft,
|
|
457
|
+
`${actionName.charAt(0).toUpperCase()}${actionName.slice(1)} draft created`
|
|
458
|
+
);
|
|
449
459
|
} catch (error) {
|
|
460
|
+
// Only Graph's own rejection, read from the start of the error (the
|
|
461
|
+
// body after it is server text). 408 means it may have gone through.
|
|
462
|
+
const rejected = /^API call failed with status (4\d\d):/.exec(
|
|
463
|
+
error.message || ''
|
|
464
|
+
);
|
|
465
|
+
if (!draft && rejected && rejected[1] !== '408') {
|
|
466
|
+
releaseRateLimit('draft');
|
|
467
|
+
}
|
|
450
468
|
return handleError(`creating ${actionName} draft`, error);
|
|
451
469
|
}
|
|
452
470
|
}
|
|
@@ -458,7 +476,8 @@ async function handleReplyDraft(args, endpoint) {
|
|
|
458
476
|
* @param {string} accessToken - Graph access token
|
|
459
477
|
* @param {object} draft - The draft Graph returned from createReply/createReplyAll
|
|
460
478
|
* @param {string} actionName - 'reply' or 'reply-all'
|
|
461
|
-
* @returns {Promise<object|null>}
|
|
479
|
+
* @returns {Promise<{error: object, draftDeleted: boolean}|null>} The
|
|
480
|
+
* refusal and whether its draft was deleted, or null to keep the draft
|
|
462
481
|
*/
|
|
463
482
|
async function refuseBlockedReply(accessToken, draft, actionName) {
|
|
464
483
|
if (!getRecipientAllowlist()) return null;
|
|
@@ -490,18 +509,22 @@ async function refuseBlockedReply(accessToken, draft, actionName) {
|
|
|
490
509
|
try {
|
|
491
510
|
await callGraphAPI(accessToken, 'DELETE', `me/messages/${draft.id}`);
|
|
492
511
|
} catch (error) {
|
|
493
|
-
|
|
512
|
+
const stillThere = toolError(
|
|
494
513
|
`${reason} The draft Graph created could not be deleted (${error.message}), so it is still in Drafts with ID \`${draft.id}\`. Do not send it.`,
|
|
495
514
|
{
|
|
496
515
|
nextStep: `Delete it with draft action=delete id=${draft.id} (or in Outlook). ${nextStep}`,
|
|
497
516
|
}
|
|
498
517
|
);
|
|
518
|
+
return { error: stillThere, draftDeleted: false };
|
|
499
519
|
}
|
|
500
520
|
|
|
501
|
-
return
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
521
|
+
return {
|
|
522
|
+
error: toolError(
|
|
523
|
+
`${reason} The draft Graph created was deleted, so nothing was kept.`,
|
|
524
|
+
{ nextStep }
|
|
525
|
+
),
|
|
526
|
+
draftDeleted: true,
|
|
527
|
+
};
|
|
505
528
|
}
|
|
506
529
|
|
|
507
530
|
/**
|
|
@@ -556,7 +579,7 @@ async function handleForwardDraft(args) {
|
|
|
556
579
|
`me/messages/${id}/createForward`,
|
|
557
580
|
requestBody
|
|
558
581
|
);
|
|
559
|
-
return formatDraftResponse(draft, '
|
|
582
|
+
return formatDraftResponse(draft, 'Forward draft created');
|
|
560
583
|
} catch (error) {
|
|
561
584
|
return handleError('creating forward draft', error);
|
|
562
585
|
}
|
package/email/export.js
CHANGED
|
@@ -67,8 +67,8 @@ async function handleExportEmail(args) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Where to write, checked before anything is fetched. F-27: `outputDir`
|
|
70
|
-
// is always a directory. `savePath` names a directory if
|
|
71
|
-
// there, otherwise the file to write. With no path, the system temp
|
|
70
|
+
// is always a directory. `savePath` names a directory if it ends in a
|
|
71
|
+
// separator or one exists there, otherwise the file to write. With no path, the system temp
|
|
72
72
|
// directory is used. Writes go to the resolved path, never the raw one.
|
|
73
73
|
let explicitFile = null;
|
|
74
74
|
let requestedFile = null; // savePath as given, for messages
|
|
@@ -77,6 +77,10 @@ async function handleExportEmail(args) {
|
|
|
77
77
|
try {
|
|
78
78
|
if (args.outputDir) {
|
|
79
79
|
targetDir = confineOutputPath(args.outputDir);
|
|
80
|
+
} else if (args.savePath && /[\\/]$/.test(args.savePath)) {
|
|
81
|
+
// A trailing separator names a directory even when it doesn't exist
|
|
82
|
+
// yet; resolving the path drops it, so decide here (#301).
|
|
83
|
+
targetDir = confineOutputPath(args.savePath);
|
|
80
84
|
} else if (args.savePath) {
|
|
81
85
|
const target = confineOutputTarget(args.savePath);
|
|
82
86
|
const resolved = target.path;
|
package/email/index.js
CHANGED
|
@@ -260,7 +260,7 @@ const emailTools = [
|
|
|
260
260
|
{
|
|
261
261
|
name: 'send-email',
|
|
262
262
|
description:
|
|
263
|
-
'Compose and send an email immediately (destructive: sends external comms). Returns a confirmation. Safety controls: `dryRun: true` returns the composed message for review without sending; `checkRecipients: true` runs `get-mail-tips` first and returns its warnings. If the tips show an out-of-office reply, a full mailbox, a delivery restriction or external recipients, the send is refused until repeated with `acknowledgeWarnings: true`. Personal Outlook.com accounts return no tips, and no warnings is not proof of delivery. Subject to session
|
|
263
|
+
'Compose and send an email immediately (destructive: sends external comms). Returns a confirmation. Safety controls: `dryRun: true` returns the composed message for review without sending; `checkRecipients: true` runs `get-mail-tips` first and returns its warnings. If the tips show an out-of-office reply, a full mailbox, a delivery restriction or external recipients, the send is refused until repeated with `acknowledgeWarnings: true`. Personal Outlook.com accounts return no tips, and no warnings is not proof of delivery. Subject to the session limit (`OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`, else `OUTLOOK_MAX_EMAILS_PER_SESSION`; 0 refuses every send) and recipient allowlist (`OUTLOOK_ALLOWED_RECIPIENTS`) when configured; both refuse before any Graph request. For a review-before-send workflow, use `draft` (action=`create` → `update` → `send`); a draft can be checked in Outlook before it goes. Comma-separated recipient strings or arrays both accepted.',
|
|
264
264
|
...toolMetadata('send-email', 'Send Email'),
|
|
265
265
|
inputSchema: {
|
|
266
266
|
type: 'object',
|
|
@@ -319,7 +319,7 @@ const emailTools = [
|
|
|
319
319
|
{
|
|
320
320
|
name: 'draft',
|
|
321
321
|
description:
|
|
322
|
-
"Draft lifecycle for review-before-send workflows (destructive: covers `send` and `delete`). action=`create` saves a new draft and returns its id (`dryRun: true` previews without saving; `checkRecipients: true` runs mail-tips first). action=`update` patches a draft by `id` (only fields passed change). action=`send` sends a draft and
|
|
322
|
+
"Draft lifecycle for review-before-send workflows (destructive: covers `send` and `delete`). action=`create` saves a new draft and returns its id (`dryRun: true` previews without saving; `checkRecipients: true` runs mail-tips first). action=`update` patches a draft by `id` (only fields passed change). action=`send` sends a draft and counts against the `send-email` session limit (0 refuses every send). action=`delete` moves a draft to Recoverable Items (restorable for a limited time). update/send/delete refuse any `id` that is not an unsent draft. action=`reply`/`reply-all` creates a reply draft from a message `id` (`comment` prepends text; not with `body`). action=`forward` creates a forward draft (needs `id` and `to`). The recipient allowlist (OUTLOOK_ALLOWED_RECIPIENTS) applies to create/update/forward, to the draft's current to/cc/bcc on send, and to reply/reply-all, whose draft is deleted if a recipient is not allowed. Returns the draft on create/update/reply/forward; a status on send/delete.",
|
|
323
323
|
...toolMetadata('draft', 'Draft Operations'),
|
|
324
324
|
inputSchema: {
|
|
325
325
|
type: 'object',
|
|
@@ -540,7 +540,7 @@ const emailTools = [
|
|
|
540
540
|
{
|
|
541
541
|
name: 'export',
|
|
542
542
|
description:
|
|
543
|
-
'Export emails to files. target=`message` (default) exports one email by `id` (mime/eml/markdown/json/csv) to `savePath`: a directory gets a new, unique file name; a file path is created new and an existing file is replaced only with `overwrite: true`. target=`messages` batch-exports `emailIds`, or matches for `searchQuery`/`query`, into `outputDir
|
|
543
|
+
'Export emails to files. target=`message` (default) exports one email by `id` (mime/eml/markdown/json/csv) to `savePath`: a directory (or a path ending in `/`, created if missing) gets a new, unique file name; a file path is created new and an existing file is replaced only with `overwrite: true`. target=`messages` batch-exports `emailIds`, or matches for `searchQuery`/`query`, into `outputDir`, at most 100 messages per call. target=`conversation` exports a thread (up to 1000 messages) by `conversationId` into `outputDir` (eml/mbox/markdown/json/html/csv; `order: "reverse"` for newest first). target=`mime` returns raw RFC-822 MIME for `id` (`headersOnly`, `base64`, `maxSize`, default 1MB). Files are written only inside the system temp directory (the default), ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR, never to dot-prefixed names. Pass `sharedMailbox` (alias `email`) when the ids come from a shared mailbox. `includeAttachments` defaults to true for one message, false for batch.',
|
|
544
544
|
...toolMetadata('export', 'Export Emails'),
|
|
545
545
|
inputSchema: {
|
|
546
546
|
type: 'object',
|
|
@@ -559,7 +559,7 @@ const emailTools = [
|
|
|
559
559
|
type: 'string',
|
|
560
560
|
enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html', 'csv'],
|
|
561
561
|
description:
|
|
562
|
-
'Export format. Valid values vary by target: target=message accepts mime/eml/markdown/json/csv (mbox and html are conversation-only). target=conversation accepts eml/mbox/markdown/json/html/csv. target=messages (batch) accepts markdown/json
|
|
562
|
+
'Export format. Valid values vary by target: target=message accepts mime/eml/markdown/json/csv (mbox and html are conversation-only). target=conversation accepts eml/mbox/markdown/json/html/csv. target=messages (batch) accepts mime/eml/markdown/json (one file per message) or csv (one file). mime is an alias for eml (same RFC822 bytes, .eml extension on disk).',
|
|
563
563
|
},
|
|
564
564
|
savePath: {
|
|
565
565
|
type: 'string',
|
package/email/mime.js
CHANGED
|
@@ -10,6 +10,22 @@ const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
|
10
10
|
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
11
11
|
const { log } = require('../utils/logger');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The longest prefix of `text` that fits in `maxBytes` of UTF-8 without
|
|
15
|
+
* splitting a character (which would render as U+FFFD).
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @param {number} maxBytes
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function utf8Prefix(text, maxBytes) {
|
|
21
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
22
|
+
if (bytes.length <= maxBytes) return text;
|
|
23
|
+
let end = maxBytes;
|
|
24
|
+
// Step back over continuation bytes (10xxxxxx) to a character start.
|
|
25
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
26
|
+
return bytes.subarray(0, end).toString('utf8');
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
/**
|
|
14
30
|
* Parse MIME headers from raw content
|
|
15
31
|
* @param {string} mimeContent - Raw MIME content
|
|
@@ -123,7 +139,13 @@ async function handleGetMimeContent(args) {
|
|
|
123
139
|
// Check size limit
|
|
124
140
|
if (maxSize > 0 && stats.bytes > maxSize) {
|
|
125
141
|
if (headersOnly) {
|
|
126
|
-
// Return just headers if over limit
|
|
142
|
+
// Return just headers if over limit, themselves capped at
|
|
143
|
+
// maxSize (#306): a large header block used to come back whole.
|
|
144
|
+
const headerBytes = Buffer.byteLength(parsed.headerSection, 'utf8');
|
|
145
|
+
const headerText =
|
|
146
|
+
headerBytes > maxSize
|
|
147
|
+
? `${utf8Prefix(parsed.headerSection, maxSize)}\n… (headers cut at maxSize ${maxSize} bytes of ${headerBytes})`
|
|
148
|
+
: parsed.headerSection;
|
|
127
149
|
return {
|
|
128
150
|
content: [
|
|
129
151
|
{
|
|
@@ -132,7 +154,7 @@ async function handleGetMimeContent(args) {
|
|
|
132
154
|
`# MIME Content (Headers Only - Content Truncated)\n\n` +
|
|
133
155
|
`**Size**: ${stats.formattedSize} (exceeds ${maxSize} byte limit)\n` +
|
|
134
156
|
`**Lines**: ${stats.lines}\n\n` +
|
|
135
|
-
`## MIME Headers\n\n\`\`\`\n${
|
|
157
|
+
`## MIME Headers\n\n\`\`\`\n${headerText}\n\`\`\``,
|
|
136
158
|
},
|
|
137
159
|
],
|
|
138
160
|
_meta: {
|
|
@@ -250,6 +272,7 @@ async function handleGetMimeContent(args) {
|
|
|
250
272
|
|
|
251
273
|
module.exports = {
|
|
252
274
|
handleGetMimeContent,
|
|
275
|
+
utf8Prefix,
|
|
253
276
|
parseMimeHeaders,
|
|
254
277
|
getMimeStats,
|
|
255
278
|
};
|
package/email/search.js
CHANGED
|
@@ -1344,7 +1344,7 @@ function buildNoResultsSuggestions(searchInfo, searchAllFolders) {
|
|
|
1344
1344
|
);
|
|
1345
1345
|
} else {
|
|
1346
1346
|
suggestions.push(
|
|
1347
|
-
'Try `searchAllFolders: true` to search across all folders including Archive'
|
|
1347
|
+
'Try `searchAllFolders: true` to search across all folders, including Archive and Junk Email (mail from a new sender often lands in Junk)'
|
|
1348
1348
|
);
|
|
1349
1349
|
suggestions.push(
|
|
1350
1350
|
'Specify the correct folder if emails have been moved (use the `folders` tool to list folders)'
|
package/folder/index.js
CHANGED
|
@@ -77,7 +77,8 @@ const folderTools = [
|
|
|
77
77
|
},
|
|
78
78
|
sourceFolder: {
|
|
79
79
|
type: 'string',
|
|
80
|
-
description:
|
|
80
|
+
description:
|
|
81
|
+
'Ignored: action=move moves each email by ID from wherever it is. Accepted for older callers.',
|
|
81
82
|
},
|
|
82
83
|
// stats params
|
|
83
84
|
folder: {
|
package/folder/stats.js
CHANGED
|
@@ -180,8 +180,10 @@ function formatFolderStats(folder, dateRange, verbosity) {
|
|
|
180
180
|
text += `| Date Range | ${oldest} to ${newest} |\n`;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
// List mode returns at most 50 with no page cursor, so anything bigger
|
|
184
|
+
// needs delta sync to read in full.
|
|
185
|
+
if (totalItems > 50) {
|
|
186
|
+
text += `\n_Hint: Use \`search-emails\` with \`deltaMode: true\` for efficient incremental sync of large folders._`;
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
return { text, meta };
|
|
@@ -210,7 +212,10 @@ function formatFolderStats(folder, dateRange, verbosity) {
|
|
|
210
212
|
text += `|---------|-------|\n`;
|
|
211
213
|
text += `| Page Size | ${pageSize} emails |\n`;
|
|
212
214
|
text += `| Total Pages | ${totalPages} |\n`;
|
|
213
|
-
text +=
|
|
215
|
+
text +=
|
|
216
|
+
totalItems > 50
|
|
217
|
+
? `| Estimated API Calls | ${Math.ceil(totalItems / 100)} (\`search-emails\` \`deltaMode: true\`, 100 per page; list mode stops at 50) |\n`
|
|
218
|
+
: `| Estimated API Calls | 1 (\`search-emails\` list mode, \`count\` up to 50) |\n`;
|
|
214
219
|
|
|
215
220
|
if (dateRange) {
|
|
216
221
|
const newestDate = new Date(dateRange.newest);
|
|
@@ -233,12 +238,12 @@ function formatFolderStats(folder, dateRange, verbosity) {
|
|
|
233
238
|
text += `\n## Recommendations\n\n`;
|
|
234
239
|
|
|
235
240
|
if (totalItems > 1000) {
|
|
236
|
-
text += `- **Large folder**: Use \`
|
|
241
|
+
text += `- **Large folder**: Use \`search-emails\` with \`deltaMode: true\` for incremental sync\n`;
|
|
237
242
|
text += `- **Use date filters**: \`receivedAfter\` and \`receivedBefore\` to narrow scope\n`;
|
|
238
|
-
} else if (totalItems >
|
|
239
|
-
text += `- **Medium folder**: Consider
|
|
243
|
+
} else if (totalItems > 50) {
|
|
244
|
+
text += `- **Medium folder**: Consider \`search-emails\` with \`deltaMode: true\` for efficient updates\n`;
|
|
240
245
|
} else {
|
|
241
|
-
text += `- **Small folder**: \`
|
|
246
|
+
text += `- **Small folder**: \`search-emails\` in list mode (raise \`count\` up to 50) is enough\n`;
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
if (unreadItems > 50) {
|
package/index.js
CHANGED
|
@@ -42,9 +42,12 @@ Key environment variables:
|
|
|
42
42
|
send or delete anything (reads and sign-in still work)
|
|
43
43
|
OUTLOOK_ALLOWED_RECIPIENTS Comma-separated recipient allowlist
|
|
44
44
|
OUTLOOK_MAX_EMAILS_PER_SESSION Default cap per session for every rate-limited tool
|
|
45
|
-
(send-email, draft, manage-rules)
|
|
45
|
+
(send-email, draft, create-event, manage-rules).
|
|
46
|
+
Unset or empty = no cap. 0 BLOCKS those tools; so does
|
|
47
|
+
any value that isn't a whole number (fails closed)
|
|
46
48
|
OUTLOOK_MAX_<TOOL>_PER_SESSION Per-tool cap overriding the default, tool name in upper
|
|
47
49
|
case with _ for -, e.g. OUTLOOK_MAX_SEND_EMAIL_PER_SESSION
|
|
50
|
+
(also covers draft action=send); 0 blocks that tool
|
|
48
51
|
OUTLOOK_DEFAULT_TIMEZONE IANA timezone for event times (default Australia/Melbourne)
|
|
49
52
|
OUTLOOK_IMMUTABLE_IDS Set to "true" for message IDs that survive folder moves
|
|
50
53
|
OUTLOOK_SEARCH_SCAN_LIMIT Local search fallback window (default 500, max 5000)
|
|
@@ -88,6 +91,11 @@ const { createServer } = require('./server');
|
|
|
88
91
|
const { setToolCount } = require('./auth');
|
|
89
92
|
const { TOOLS } = require('./tools');
|
|
90
93
|
const { isDebugEnabled } = require('./utils/logger');
|
|
94
|
+
const {
|
|
95
|
+
blockedTools,
|
|
96
|
+
resolveSessionLimit,
|
|
97
|
+
RATE_LIMITED_TOOLS,
|
|
98
|
+
} = require('./utils/safety');
|
|
91
99
|
|
|
92
100
|
// Log startup information
|
|
93
101
|
console.error(
|
|
@@ -103,8 +111,11 @@ if (isDebugEnabled()) {
|
|
|
103
111
|
// F-1 / F-48: warn at startup when safety belts are unset. Mirrors the
|
|
104
112
|
// warning surfaced by `auth action=about`. Visible to operators reading
|
|
105
113
|
// stderr; AI clients reading the JSON-RPC stream are unaffected.
|
|
114
|
+
const sessionLimitSet = Object.keys(RATE_LIMITED_TOOLS).some(
|
|
115
|
+
(tool) => resolveSessionLimit(tool).limit !== null
|
|
116
|
+
);
|
|
106
117
|
if (
|
|
107
|
-
!
|
|
118
|
+
!sessionLimitSet &&
|
|
108
119
|
!process.env.OUTLOOK_ALLOWED_RECIPIENTS &&
|
|
109
120
|
!config.USE_TEST_MODE
|
|
110
121
|
) {
|
|
@@ -112,6 +123,13 @@ if (
|
|
|
112
123
|
'⚠ Safety belts not configured. Consider setting OUTLOOK_MAX_EMAILS_PER_SESSION and OUTLOOK_ALLOWED_RECIPIENTS in your .mcp.json env block for safer AI-assisted sending. See `auth action=about` for details.'
|
|
113
124
|
);
|
|
114
125
|
}
|
|
126
|
+
// #302: say plainly when a session limit of 0 switches a tool off.
|
|
127
|
+
const blocked = blockedTools();
|
|
128
|
+
if (blocked.length > 0) {
|
|
129
|
+
console.error(
|
|
130
|
+
`Session limits block ${blocked.join(', ')} (0 or an unreadable value). Unset the setting for no limit. See \`auth action=about\`.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
115
133
|
|
|
116
134
|
// Set dynamic tool count for auth about handler
|
|
117
135
|
setToolCount(TOOLS.length);
|
package/llms-install.md
CHANGED
|
@@ -81,7 +81,7 @@ Add these to the same `env` block if needed:
|
|
|
81
81
|
|----------|---------|
|
|
82
82
|
| `OUTLOOK_AUTH_AUDIENCE` | `consumers` for Azure apps registered as personal-accounts-only (fixes `AADSTS9002331`); `organizations` or a tenant GUID for work-only apps. Default `common` |
|
|
83
83
|
| `OUTLOOK_DEFAULT_TIMEZONE` | IANA timezone for calendar times (default `Australia/Melbourne`) |
|
|
84
|
-
| `OUTLOOK_MAX_EMAILS_PER_SESSION` | Default per-session cap, counted separately per tool, for `send-email` (including `draft` send), `draft` create/update/reply/reply-all/forward, `manage-rules` and `create-event` (override one tool with `OUTLOOK_MAX_<TOOL>_PER_SESSION`, e.g. `OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`) |
|
|
84
|
+
| `OUTLOOK_MAX_EMAILS_PER_SESSION` | Default per-session cap, counted separately per tool, for `send-email` (including `draft` send), `draft` create/update/reply/reply-all/forward, `manage-rules` and `create-event` (override one tool with `OUTLOOK_MAX_<TOOL>_PER_SESSION`, e.g. `OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`). Unset = no limit; `0` blocks the tool |
|
|
85
85
|
| `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of recipient domains/addresses for sends, drafts, rule forwards and event attendees |
|
|
86
86
|
| `OUTLOOK_READ_ONLY` | `true` refuses every tool call that would change something (sending, drafts, moves, deletes, rules, settings, file writes), dry runs included; reads and sign-in still work |
|
|
87
87
|
| `OUTLOOK_SHARED_MAILBOX` | Opt-in shared-mailbox support, work/school accounts only: `read` or `true` (read and organise). Also add `Mail.Read.Shared` (and `Mail.ReadWrite.Shared` for `true`) in Azure, restart, then run `auth` with `action=authenticate` and `force=true` |
|
package/llms.txt
CHANGED
|
@@ -26,7 +26,7 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
26
26
|
- **Progressive search**: Automatically falls back through 4 search strategies when Microsoft's `$search` API is unavailable (personal accounts) — most Graph API wrappers fail silently. Explicit "no results" messaging instead of unfiltered fallback, and `_meta.searchMetadata` reports which strategy answered plus any filter that could not be honoured (`droppedFilters`), so a partially-applied search can never pass as a complete one. Field-scoped `$search` expressions (`from:`/`to:`/`subject:`), which personal accounts reject outright, are translated to equivalent OData filters and retried. Cross-folder search (`searchAllFolders`) reliably returns a superset of inbox results.
|
|
27
27
|
- **Remote-friendly auth**: Device code flow (default) — no auth server, no port forwarding, no SSH tunnels. State persists across MCP server restarts. Works from Untether, mosh, SSH, and headless environments.
|
|
28
28
|
- **Email forensics**: Built-in header analysis for DKIM, SPF, DMARC authentication, delivery chains, and spam scores — useful for phishing investigation and compliance
|
|
29
|
-
- **Delta sync**: Incremental inbox monitoring — returns only new, modified, and deleted emails since last check, with tokens for continuous polling
|
|
29
|
+
- **Delta sync**: Incremental inbox monitoring — returns only new, modified, and deleted emails since last check, with tokens for continuous polling; every page is labelled initial or incremental (`_meta.syncType`)
|
|
30
30
|
- **Batch operations**: Flag, move, export, or categorise multiple emails in a single tool call; search-driven export for batch archiving without collecting IDs
|
|
31
31
|
- **Pre-send intelligence**: Check recipients for out-of-office, mailbox full, delivery restrictions, and moderation before sending — no other Outlook MCP server offers this
|
|
32
32
|
- **Compound automation**: Rules + categories + nested folders (addressable by path or ID) + Focused Inbox for complete inbox management in one conversation
|
|
@@ -34,15 +34,15 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
34
34
|
## Safety & Token Efficiency
|
|
35
35
|
|
|
36
36
|
- **MCP safety annotations** on all 22 tools, derived from a risk class per tool and action (read, reversible, outward, destructive, persistent), so clients can tell reads from risky changes
|
|
37
|
-
- **Send-email protections**: pre-send mail tips (a send to a flagged recipient, such as out of office or external, is refused until `acknowledgeWarnings: true`), dry-run preview, session rate limiting, recipient allowlist (also checked on drafts, including replies and the draft's current recipients on send, and on `create-event`/`manage-event` update attendees; only single plain addresses can match)
|
|
38
|
-
- **Rule protections**: dry-run preview on create/update, rate limiting (dry runs don't count), recipient allowlist on forward/redirect (a blocked forward refuses the whole rule), no permanent-delete action
|
|
39
|
-
- **Dry-run previews beyond email**: `create-event` and every `manage-event` action say who would be emailed (with an external count); `mailbox-settings` auto-replies, `folders` delete and `manage-contact` delete show what would change or be lost; `dryRun: true` on any other call is refused rather than run for real
|
|
37
|
+
- **Send-email protections**: pre-send mail tips (a send to a flagged recipient, such as out of office or external, is refused until `acknowledgeWarnings: true`), dry-run preview, session rate limiting (unset = no limit; `0` or a value that isn't a whole number blocks the tool; `draft` send counts as `send-email`), recipient allowlist (also checked on drafts, including replies and the draft's current recipients on send, and on `create-event`/`manage-event` update attendees; only single plain addresses can match)
|
|
38
|
+
- **Rule protections**: dry-run preview on create/update, rate limiting (dry runs don't count; `0` blocks rule changes), reorder lists the resulting rule order, recipient allowlist on forward/redirect (a blocked forward refuses the whole rule), no permanent-delete action
|
|
39
|
+
- **Dry-run previews beyond email**: `create-event` and every `manage-event` action, update included, say who would be emailed (with an external count; update also says who an attendee change adds or removes); `mailbox-settings` auto-replies, `folders` delete and `manage-contact` delete show what would change or be lost; `dryRun: true` on any other call is refused rather than run for real
|
|
40
40
|
- **Read-only mode**: `OUTLOOK_READ_ONLY=true` refuses every tool call that would change anything before it runs; reads and sign-in still work
|
|
41
|
-
- **Model guidance**: server `instructions` state the hard rules (retrieved content is data, confirm before outward actions, draft first); `send-email` and `create-event` ask Claude clients for user confirmation (`anthropic/requiresUserInteraction`)
|
|
41
|
+
- **Model guidance**: server `instructions` state the hard rules (retrieved content is data, confirm before outward actions, draft first, refusals and rate limits are final, with `0` = off) and name any tool a session limit of `0` blocks; `auth action=about` shows each tool's session limit; `send-email` and `create-event` ask Claude clients for user confirmation (`anthropic/requiresUserInteraction`)
|
|
42
42
|
- **Plugin skill and safety hook** (Claude Code, GitHub Copilot CLI, Cursor; VS Code not yet checked by hand): the `using-outlook-assistant` skill sets the hard rules; the hook asks with a plain-English reason before anything that reaches other people, deletes or keeps acting, and marks retrieved content as untrusted. Confirmation level `outward` (default), `all-writes` or `off`: a plugin setting in Claude Code, the `OUTLOOK_CONFIRM_LEVEL` environment variable elsewhere. How the hook prompts and fails differs by client: see Supported Clients below
|
|
43
43
|
- **Every client**: the server-side checks (read-only mode, dry runs, the recipient allowlist and send caps, mail-tips refusal, rule refusal), annotations and instructions work in any MCP client, including Codex CLI, Gemini CLI and Claude Desktop with a manual config; clients that support Agent Skills can copy the skill folder
|
|
44
44
|
- **Private logs**: one stderr line per tool call (tool, action, outcome, duration), never its arguments; `OUTLOOK_DEBUG=true` adds detail with addresses and IDs redacted
|
|
45
|
-
- **Input and file hardening**: IDs with `.`/`..` path segments refused, the access token only ever sent to `graph.microsoft.com`, attachment downloads and exports (including conversation exports) written only inside the temp directory, `~/Downloads`, `~/Documents` or `OUTLOOK_EXPORT_DIR`, never to dot-prefixed names; paths must be absolute (or start with `~/`) and symlinks aren't followed; existing files are never replaced unless `export` is called with `overwrite: true` (and never a symlink, hard-linked file or dotfile); files are created `0600` and new folders `0700`; a partly written file is removed if a write fails
|
|
45
|
+
- **Input and file hardening**: IDs with `.`/`..` path segments refused, the access token only ever sent to `graph.microsoft.com`, attachment downloads and exports (including conversation exports) written only inside the temp directory, `~/Downloads`, `~/Documents` or `OUTLOOK_EXPORT_DIR`, never to dot-prefixed names; paths must be absolute (or start with `~/`) and symlinks aren't followed; an `export` `savePath` ending in `/` is a folder, created if missing; existing files are never replaced unless `export` is called with `overwrite: true` (and never a symlink, hard-linked file or dotfile); files are created `0600` and new folders `0700`; a partly written file is removed if a write fails
|
|
46
46
|
- **Throttling-aware Graph client**: `429` (and `503`/`504` for non-POST requests) retried honouring `Retry-After`, a per-attempt inactivity timeout (`OUTLOOK_REQUEST_TIMEOUT_MS`, default 60000 ms) and at most 4 requests in flight, so bulk operations neither hang nor throttle themselves
|
|
47
47
|
- **Shared mailboxes off by default**: `.Shared` scopes are requested only when `OUTLOOK_SHARED_MAILBOX` is set (`read` keeps shared access read-only)
|
|
48
48
|
- **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
@@ -73,9 +73,9 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
73
73
|
- **Calendar (3 tools)**: `list-events` (`startAfter`, `startBefore`, `subject` filters), `create-event` (`dryRun`; retry-safe `transactionId`), `manage-event` (update, decline, cancel, delete; `dryRun` on all)
|
|
74
74
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
75
75
|
- **Folders (1 tool)**: `folders` — list, create, move, stats, delete; folders addressable by nested path (`Parent/Child`) or ID
|
|
76
|
-
- **Rules (1 tool)**: `manage-rules` — list, create, update, reorder, delete
|
|
76
|
+
- **Rules (1 tool)**: `manage-rules` — list, create, update, reorder, delete (12 conditions including `hasAttachments`, 9 actions, `except*` exceptions)
|
|
77
77
|
- **Categories (3 tools)**: `manage-category`, `apply-category`, `manage-focused-inbox`
|
|
78
|
-
- **Settings (1 tool)**: `mailbox-settings` — get, set auto-replies, set working hours
|
|
78
|
+
- **Settings (1 tool)**: `mailbox-settings` — get, set auto-replies (`dryRun`; schedules shown as UTC plus a labelled local time), set working hours
|
|
79
79
|
- **Advanced (2 tools)**: `access-shared-mailbox` (messages, `listFolders`, `folderId`, nested folder paths), `find-meeting-rooms`
|
|
80
80
|
|
|
81
81
|
## Documentation
|
|
@@ -92,6 +92,6 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
92
92
|
- [FAQ](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/faq/faq.md): Frequently asked questions — install, accounts, permissions, tokens, send safety, updates, uninstall (also at <https://littlebearapps.com/help/outlook-assistant/faq/>)
|
|
93
93
|
- [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
|
|
94
94
|
- [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
|
|
95
|
-
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.14.
|
|
96
|
-
- [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.14.
|
|
95
|
+
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.14.1 — security and safety release: read-only mode, server instructions, more dry-run previews, mail-tips refusal, the plugin skill and a safety hook for Claude Code, GitHub Copilot and Cursor, and fixes for three advisories covering the recipient allowlist, export file writes and dry runs)
|
|
96
|
+
- [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.14.1 live-test fixes; v3.15.0 structured outputs and paging; v4.0.0 MCP 2026-07-28 and server-side confirmation; the patch-release fix queue; v3.8.x carry-over; v3.16.0+ new Graph APIs)
|
|
97
97
|
- [SECURITY](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/SECURITY.md): Security policy (report vulnerabilities privately via GitHub private vulnerability reporting; acknowledged within 7 days), token handling, and MCP safety controls
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.1",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
5
|
"description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|
package/rules/create.js
CHANGED
|
@@ -104,7 +104,7 @@ async function handleCreateRule(args) {
|
|
|
104
104
|
// Dry-run: preview without creating
|
|
105
105
|
if (dryRun) {
|
|
106
106
|
const preview = formatRuleDryRunPreview(rule);
|
|
107
|
-
let text = `
|
|
107
|
+
let text = `Rule preview (not created):\n\n${preview}`;
|
|
108
108
|
if (allWarnings.length > 0) {
|
|
109
109
|
text += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
110
110
|
}
|
package/rules/index.js
CHANGED
|
@@ -106,11 +106,32 @@ async function handleEditRuleSequence(args) {
|
|
|
106
106
|
{ sequence }
|
|
107
107
|
);
|
|
108
108
|
|
|
109
|
+
// Exchange may renumber other rules to make room, and deleting a rule
|
|
110
|
+
// later doesn't shift them back (#307), so show the resulting order.
|
|
111
|
+
let order = '';
|
|
112
|
+
try {
|
|
113
|
+
const after = await getInboxRules(accessToken);
|
|
114
|
+
const before = new Map(rules.map((r) => [r.id, r.sequence]));
|
|
115
|
+
const lines = [...after]
|
|
116
|
+
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
|
|
117
|
+
.map((r) => `${r.sequence}: ${r.displayName}`);
|
|
118
|
+
const moved = after.filter(
|
|
119
|
+
(r) => r.id !== rule.id && before.get(r.id) !== r.sequence
|
|
120
|
+
);
|
|
121
|
+
const movedNote =
|
|
122
|
+
moved.length > 0
|
|
123
|
+
? `\n\nRenumbered by Exchange: ${moved.map((r) => `"${r.displayName}" ${before.get(r.id)} → ${r.sequence}`).join('; ')}. Tell the user; deleting a rule later doesn't shift them back.`
|
|
124
|
+
: '\n\nNo other rule was renumbered.';
|
|
125
|
+
order = `\n\nRule order now (sequence: name):\n${lines.join('\n')}${movedNote}`;
|
|
126
|
+
} catch (_error) {
|
|
127
|
+
// The reorder succeeded; the order listing is a courtesy.
|
|
128
|
+
}
|
|
129
|
+
|
|
109
130
|
return {
|
|
110
131
|
content: [
|
|
111
132
|
{
|
|
112
133
|
type: 'text',
|
|
113
|
-
text: `Successfully updated the sequence of rule "${ruleName}" to ${sequence}
|
|
134
|
+
text: `Successfully updated the sequence of rule "${ruleName}" to ${sequence}.${order}`,
|
|
114
135
|
},
|
|
115
136
|
],
|
|
116
137
|
};
|
|
@@ -128,7 +149,7 @@ const rulesTools = [
|
|
|
128
149
|
{
|
|
129
150
|
name: 'manage-rules',
|
|
130
151
|
description:
|
|
131
|
-
'Server-side inbox rules (destructive: covers `delete`; `dryRun` previews create/update). Rules run on the Exchange server whichever client is open. action=`list` (default) returns rules with id/name/sequence; `includeDetails: true` adds conditions/actions/exceptions. action=`create` builds a rule from condition params (12, e.g. fromAddresses, containsSubject, bodyContains, hasAttachments, importance,
|
|
152
|
+
'Server-side inbox rules (destructive: covers `delete`; `dryRun` previews create/update). Rules run on the Exchange server whichever client is open. action=`list` (default) returns rules with id/name/sequence; `includeDetails: true` adds conditions/actions/exceptions. action=`create` builds a rule from condition params (12, e.g. fromAddresses, containsSubject, bodyContains, hasAttachments, importance, sensitivity), action params (9, e.g. moveToFolder/copyToFolder — folder name, nested path like `Triage/Delete`, or ID — forwardTo, redirectTo, assignCategories, markAsRead, delete) and optional `except*` exceptions. action=`update` patches fields by `ruleId` or `ruleName`. action=`reorder` changes priority via `sequence` (lower runs first). action=`delete` removes a rule. The recipient allowlist applies to forwardTo/redirectTo. There is no `permanentDelete` action. Changes count against the session limit (`OUTLOOK_MAX_MANAGE_RULES_PER_SESSION`, else `OUTLOOK_MAX_EMAILS_PER_SESSION`); 0 refuses them.',
|
|
132
153
|
...toolMetadata('manage-rules', 'Inbox Rules'),
|
|
133
154
|
inputSchema: {
|
|
134
155
|
type: 'object',
|
package/rules/list.js
CHANGED
|
@@ -153,7 +153,7 @@ function formatRuleConditions(rule) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Booleans
|
|
156
|
-
if (c.
|
|
156
|
+
if (c.hasAttachments === true) conditions.push('Has attachment');
|
|
157
157
|
if (c.sentToMe === true) conditions.push('Sent to me');
|
|
158
158
|
if (c.sentOnlyToMe === true) conditions.push('Sent only to me');
|
|
159
159
|
if (c.sentCcMe === true) conditions.push('I am in CC');
|
|
@@ -269,7 +269,7 @@ function formatRuleExceptions(rule) {
|
|
|
269
269
|
if (e.recipientContains?.length > 0) {
|
|
270
270
|
parts.push(`Recipient contains: "${e.recipientContains.join('", "')}"`);
|
|
271
271
|
}
|
|
272
|
-
if (e.
|
|
272
|
+
if (e.hasAttachments === true) parts.push('Has attachment');
|
|
273
273
|
if (e.importance) parts.push(`Importance: ${e.importance}`);
|
|
274
274
|
if (e.sensitivity) parts.push(`Sensitivity: ${e.sensitivity}`);
|
|
275
275
|
if (e.sentToMe === true) parts.push('Sent to me');
|
package/rules/rule-builder.js
CHANGED
|
@@ -76,7 +76,7 @@ function buildConditions(args) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Boolean conditions
|
|
79
|
-
if (args.hasAttachments === true) conditions.
|
|
79
|
+
if (args.hasAttachments === true) conditions.hasAttachments = true;
|
|
80
80
|
if (args.sentToMe === true) conditions.sentToMe = true;
|
|
81
81
|
if (args.sentOnlyToMe === true) conditions.sentOnlyToMe = true;
|
|
82
82
|
if (args.sentCcMe === true) conditions.sentCcMe = true;
|
|
@@ -303,7 +303,7 @@ function buildExceptions(args) {
|
|
|
303
303
|
exceptions.bodyContains = parseCommaSeparated(args.exceptBodyContains);
|
|
304
304
|
}
|
|
305
305
|
if (args.exceptHasAttachments === true) {
|
|
306
|
-
exceptions.
|
|
306
|
+
exceptions.hasAttachments = true;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
return { exceptions, warnings };
|
package/rules/update.js
CHANGED
|
@@ -118,7 +118,7 @@ async function handleUpdateRule(args) {
|
|
|
118
118
|
const currentPreview = formatRuleDryRunPreview(currentRule);
|
|
119
119
|
const updatedPreview = formatRuleDryRunPreview(updatedRule);
|
|
120
120
|
|
|
121
|
-
let text = `
|
|
121
|
+
let text = `Update preview for "${currentRule.displayName}" (not applied):\n\n`;
|
|
122
122
|
text += `CURRENT:\n${currentPreview}\n\n`;
|
|
123
123
|
text += `AFTER UPDATE:\n${updatedPreview}`;
|
|
124
124
|
if (allWarnings.length > 0) {
|
package/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const config = require('./config');
|
|
|
9
9
|
const { createRequestHandler } = require('./request-handler');
|
|
10
10
|
const { TOOLS } = require('./tools');
|
|
11
11
|
const { serverInstructions } = require('./utils/server-instructions');
|
|
12
|
+
const { blockedTools } = require('./utils/safety');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {Array<object>} [tools] - tool definitions (default: the registry)
|
|
@@ -23,8 +24,11 @@ function createServer(tools = TOOLS) {
|
|
|
23
24
|
// -32601 rather than empty stubs. (#276)
|
|
24
25
|
capabilities: { tools: { listChanged: false } },
|
|
25
26
|
// Model-facing safety rules and usage tips, sent in the initialize
|
|
26
|
-
// result (#271).
|
|
27
|
-
instructions: serverInstructions({
|
|
27
|
+
// result (#271), naming any tool a session limit of 0 blocks (#302).
|
|
28
|
+
instructions: serverInstructions({
|
|
29
|
+
readOnly: config.READ_ONLY,
|
|
30
|
+
blockedTools: blockedTools(),
|
|
31
|
+
}),
|
|
28
32
|
}
|
|
29
33
|
);
|
|
30
34
|
|