fluxmail 0.17.1 → 0.18.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/dist/accounts/defaultMicrosoftOAuth.d.ts +8 -0
- package/dist/accounts/defaultMicrosoftOAuth.d.ts.map +1 -0
- package/dist/accounts/defaultMicrosoftOAuth.js +8 -0
- package/dist/accounts/defaultMicrosoftOAuth.js.map +1 -0
- package/dist/accounts/gmailConnection.d.ts.map +1 -1
- package/dist/accounts/gmailConnection.js +1 -5
- package/dist/accounts/gmailConnection.js.map +1 -1
- package/dist/accounts/microsoftAuth.d.ts.map +1 -1
- package/dist/accounts/microsoftAuth.js +10 -3
- package/dist/accounts/microsoftAuth.js.map +1 -1
- package/dist/accounts/registry.d.ts.map +1 -1
- package/dist/accounts/registry.js +5 -2
- package/dist/accounts/registry.js.map +1 -1
- package/dist/accounts/telemetry.d.ts +0 -4
- package/dist/accounts/telemetry.d.ts.map +1 -1
- package/dist/accounts/telemetry.js +3 -5
- package/dist/accounts/telemetry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cliMail.d.ts.map +1 -1
- package/dist/cliMail.js +4 -2
- package/dist/cliMail.js.map +1 -1
- package/dist/config.d.ts +10 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -15
- package/dist/config.js.map +1 -1
- package/dist/http/admin.js +3 -3
- package/dist/http/admin.js.map +1 -1
- package/dist/http/rest.d.ts.map +1 -1
- package/dist/http/rest.js +15 -16
- package/dist/http/rest.js.map +1 -1
- package/dist/mcp/buildServer.d.ts +5 -13
- package/dist/mcp/buildServer.d.ts.map +1 -1
- package/dist/mcp/buildServer.js +324 -486
- package/dist/mcp/buildServer.js.map +1 -1
- package/dist/mcp/outputSchemas.d.ts +6 -0
- package/dist/mcp/outputSchemas.d.ts.map +1 -1
- package/dist/mcp/outputSchemas.js +1 -1
- package/dist/mcp/outputSchemas.js.map +1 -1
- package/dist/permissions.d.ts +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +4 -15
- package/dist/permissions.js.map +1 -1
- package/dist/service/emailService.d.ts +2 -2
- package/dist/service/emailService.d.ts.map +1 -1
- package/dist/service/emailService.js +2 -2
- package/dist/service/emailService.js.map +1 -1
- package/dist/service/publicErrors.d.ts.map +1 -1
- package/dist/service/publicErrors.js +10 -2
- package/dist/service/publicErrors.js.map +1 -1
- package/package.json +5 -5
package/dist/mcp/buildServer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
5
5
|
import { CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY, McpServer, ResourceTemplate, } from '@modelcontextprotocol/server';
|
|
6
6
|
import { isEmailError, mergeEmailQueries, parseEmailSearch, parseSingleAddress, } from '@fluxmail/core';
|
|
7
|
+
import { sharedTool } from '@fluxmail/core/mcp';
|
|
7
8
|
import { batchFailureCause } from '../service/emailService.js';
|
|
8
9
|
import { DEFAULT_MAX_ATTACHMENT_BYTES } from '../config.js';
|
|
9
10
|
import { DEFAULT_PERMISSION_POLICY, MCP_CAPABILITIES, MCP_CAPABILITY_DESCRIPTIONS, enableCapabilitiesInstructions, hasCapability, normalizePermissionPolicy, } from '../permissions.js';
|
|
@@ -27,7 +28,8 @@ const EMPTY_INPUT_SCHEMA = {
|
|
|
27
28
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
28
29
|
};
|
|
29
30
|
function toolJsonSchema(shape, pipeStrategy) {
|
|
30
|
-
|
|
31
|
+
// Inline every schema: sibling $ref pointers break when a product reshapes a property.
|
|
32
|
+
const schema = zodToJsonSchema(z.object(shape), { strictUnions: true, pipeStrategy, $refStrategy: 'none' });
|
|
31
33
|
// MCP clients use JSON Schema 2020-12 when no dialect is declared.
|
|
32
34
|
if (pipeStrategy === 'output')
|
|
33
35
|
delete schema.$schema;
|
|
@@ -121,6 +123,12 @@ const draftShape = {
|
|
|
121
123
|
filename: z.string().min(1),
|
|
122
124
|
mimeType: z.string().min(1),
|
|
123
125
|
content: z.string().describe('base64'),
|
|
126
|
+
contentId: z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1)
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Content-ID for an inline image, referenced from bodyHtml as cid:<contentId>'),
|
|
131
|
+
disposition: z.enum(['inline', 'attachment']).optional().describe('Defaults to attachment'),
|
|
124
132
|
}))
|
|
125
133
|
.optional(),
|
|
126
134
|
};
|
|
@@ -347,89 +355,40 @@ function emailQuery(args) {
|
|
|
347
355
|
}
|
|
348
356
|
/** One MCP tool per modify action, all served by the same implementation. */
|
|
349
357
|
export const MODIFY_TOOLS = {
|
|
350
|
-
markRead: {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
},
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
385
|
-
// Spam and Junk folders delete their mail after a while, so a move can lose it.
|
|
386
|
-
move: {
|
|
387
|
-
name: 'move_emails',
|
|
388
|
-
title: 'Move emails',
|
|
389
|
-
description: 'Move messages to a folder other than the archive or trash.',
|
|
390
|
-
capability: 'mail.organize',
|
|
391
|
-
destructive: true,
|
|
392
|
-
input: 'folder',
|
|
393
|
-
},
|
|
394
|
-
addLabels: {
|
|
395
|
-
name: 'add_labels',
|
|
396
|
-
title: 'Add labels',
|
|
397
|
-
description: 'Add labels, or Outlook categories, to messages.',
|
|
398
|
-
capability: 'mail.organize',
|
|
399
|
-
destructive: false,
|
|
400
|
-
input: 'labels',
|
|
401
|
-
},
|
|
402
|
-
removeLabels: {
|
|
403
|
-
name: 'remove_labels',
|
|
404
|
-
title: 'Remove labels',
|
|
405
|
-
description: 'Remove labels, or Outlook categories, from messages.',
|
|
406
|
-
capability: 'mail.organize',
|
|
407
|
-
destructive: false,
|
|
408
|
-
input: 'labels',
|
|
409
|
-
},
|
|
410
|
-
// Providers empty the trash automatically, so trashed mail is eventually gone.
|
|
411
|
-
trash: {
|
|
412
|
-
name: 'trash_emails',
|
|
413
|
-
title: 'Move emails to trash',
|
|
414
|
-
description: 'Move messages to the trash, where they can be restored until the provider empties it.',
|
|
415
|
-
capability: 'mail.trash',
|
|
416
|
-
destructive: true,
|
|
417
|
-
},
|
|
418
|
-
untrash: {
|
|
419
|
-
name: 'restore_emails',
|
|
420
|
-
title: 'Restore emails from trash',
|
|
421
|
-
description: 'Move messages out of the trash.',
|
|
422
|
-
capability: 'mail.trash',
|
|
423
|
-
destructive: false,
|
|
424
|
-
},
|
|
425
|
-
delete: {
|
|
426
|
-
name: 'delete_emails',
|
|
427
|
-
title: 'Delete emails permanently',
|
|
428
|
-
description: "Permanently delete messages. This cannot be undone. Gmail accounts connected through Fluxmail's built-in " +
|
|
429
|
-
'Google app cannot delete permanently; move their messages to the trash instead.',
|
|
430
|
-
capability: 'mail.delete',
|
|
431
|
-
destructive: true,
|
|
432
|
-
},
|
|
358
|
+
markRead: { name: 'mark_emails_read' },
|
|
359
|
+
markUnread: { name: 'mark_emails_unread' },
|
|
360
|
+
star: { name: 'star_emails' },
|
|
361
|
+
unstar: { name: 'unstar_emails' },
|
|
362
|
+
archive: { name: 'archive_emails' },
|
|
363
|
+
move: { name: 'move_emails', input: 'folder' },
|
|
364
|
+
addLabels: { name: 'add_labels', input: 'labels' },
|
|
365
|
+
removeLabels: { name: 'remove_labels', input: 'labels' },
|
|
366
|
+
trash: { name: 'trash_emails' },
|
|
367
|
+
untrash: { name: 'restore_emails' },
|
|
368
|
+
delete: { name: 'delete_emails' },
|
|
369
|
+
};
|
|
370
|
+
/** Scopes for the tools only this product offers. Shared tools take theirs from the contract. */
|
|
371
|
+
const PRODUCT_TOOL_SCOPES = {
|
|
372
|
+
get_status: 'mail.read',
|
|
373
|
+
list_scheduled_emails: 'mail.read',
|
|
374
|
+
cancel_scheduled_email: 'mail.drafts',
|
|
375
|
+
};
|
|
376
|
+
function toolScopes(name) {
|
|
377
|
+
const product = PRODUCT_TOOL_SCOPES[name];
|
|
378
|
+
if (product)
|
|
379
|
+
return [product];
|
|
380
|
+
const tool = sharedTool(name);
|
|
381
|
+
return tool.alsoRequires ? [tool.scope, tool.alsoRequires] : [tool.scope];
|
|
382
|
+
}
|
|
383
|
+
/** Sentences this product appends to the contract's base descriptions. */
|
|
384
|
+
const PRODUCT_DESCRIPTIONS = {
|
|
385
|
+
list_accounts: 'Each entry includes the account ID, provider, email address, status and capabilities.',
|
|
386
|
+
list_emails: 'Use get_email for full bodies.',
|
|
387
|
+
get_email: 'Use get_email_body to read the rest.',
|
|
388
|
+
send_email: 'Confirm with the user when the intent is ambiguous. Add sendAt to any mode to schedule the message instead ' +
|
|
389
|
+
'of sending it now.',
|
|
390
|
+
delete_emails: "Gmail accounts connected through Fluxmail's built-in Google app cannot delete permanently; move their " +
|
|
391
|
+
'messages to the trash instead.',
|
|
433
392
|
};
|
|
434
393
|
const MODIFY_TOOL_ENTRIES = Object.entries(MODIFY_TOOLS);
|
|
435
394
|
const MODIFY_ACTION_BY_TOOL = new Map(MODIFY_TOOL_ENTRIES.map(([action, tool]) => [tool.name, action]));
|
|
@@ -484,17 +443,18 @@ export function buildMcpServer(service, options = {}) {
|
|
|
484
443
|
return mcpClientProperties(envelope?.[CLIENT_INFO_META_KEY] ?? server.server.getClientVersion(), envelope?.[PROTOCOL_VERSION_META_KEY] ?? (context ? protocolVersionOf(context) : undefined));
|
|
485
444
|
},
|
|
486
445
|
};
|
|
446
|
+
const requireToolScopes = (tool) => requireCapabilities(toolScopes(tool));
|
|
487
447
|
// Every tool except get_status (the diagnostic way out) enforces the plan quota.
|
|
488
|
-
const gated = (tool,
|
|
489
|
-
|
|
448
|
+
const gated = (tool, fn) => handle(tool, async (...args) => {
|
|
449
|
+
requireToolScopes(tool);
|
|
490
450
|
return fn(...args);
|
|
491
451
|
}, () => service.enforceQuota(), toolOptions);
|
|
492
|
-
const allowed = (tool,
|
|
493
|
-
|
|
452
|
+
const allowed = (tool, fn) => handle(tool, async (...args) => {
|
|
453
|
+
requireToolScopes(tool);
|
|
494
454
|
return fn(...args);
|
|
495
455
|
}, undefined, toolOptions);
|
|
496
|
-
const gatedResult = (tool,
|
|
497
|
-
|
|
456
|
+
const gatedResult = (tool, fn) => handleResult(tool, async (...args) => {
|
|
457
|
+
requireToolScopes(tool);
|
|
498
458
|
return fn(...args);
|
|
499
459
|
}, () => service.enforceQuota(), toolOptions);
|
|
500
460
|
const server = new McpServer({ name: 'fluxmail', title: 'Fluxmail Email', version: VERSION }, {
|
|
@@ -537,307 +497,222 @@ export function buildMcpServer(service, options = {}) {
|
|
|
537
497
|
});
|
|
538
498
|
return registered;
|
|
539
499
|
};
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
500
|
+
/** Registers a shared tool with its contract metadata when the connection has the tool's scopes. */
|
|
501
|
+
const registerSharedTool = (name, inputSchema, callback, productDescription = PRODUCT_DESCRIPTIONS[name]) => {
|
|
502
|
+
if (!canAll(toolScopes(name)))
|
|
503
|
+
return;
|
|
504
|
+
const tool = sharedTool(name);
|
|
505
|
+
registerTool(name, {
|
|
506
|
+
title: tool.title,
|
|
507
|
+
description: productDescription ? `${tool.description} ${productDescription}` : tool.description,
|
|
508
|
+
inputSchema,
|
|
509
|
+
annotations: { ...tool.annotations },
|
|
510
|
+
}, callback);
|
|
511
|
+
};
|
|
512
|
+
registerSharedTool('list_accounts', {}, gated('list_accounts', async () => service.listAccounts()));
|
|
513
|
+
if (canAll(toolScopes('get_status')))
|
|
548
514
|
registerTool('get_status', {
|
|
549
515
|
title: 'Get status',
|
|
550
516
|
description: 'Account connection and scheduled-send status, including accounts that need re-authentication. ' +
|
|
551
517
|
'Administrators also see plan details.',
|
|
552
518
|
inputSchema: {},
|
|
553
519
|
annotations: { readOnlyHint: true },
|
|
554
|
-
}, allowed('get_status',
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
inputSchema: { accountId: accountIdParam },
|
|
560
|
-
annotations: { readOnlyHint: true },
|
|
561
|
-
}, gated('list_folders', 'mail.read', async (args) => service.listFolders(args.accountId)));
|
|
562
|
-
if (can('mail.read'))
|
|
563
|
-
registerTool('list_labels', {
|
|
564
|
-
title: 'List labels',
|
|
565
|
-
description: 'List Gmail user labels or Outlook categories for an account.',
|
|
566
|
-
inputSchema: { accountId: accountIdParam },
|
|
567
|
-
annotations: { readOnlyHint: true },
|
|
568
|
-
}, gated('list_labels', 'mail.read', async (args) => service.listLabels(args.accountId)));
|
|
569
|
-
if (can('mail.read'))
|
|
570
|
-
registerTool('list_send_as', {
|
|
571
|
-
title: 'List sender addresses',
|
|
572
|
-
description: 'List sender addresses available for an account.',
|
|
573
|
-
inputSchema: { accountId: accountIdParam },
|
|
574
|
-
annotations: { readOnlyHint: true },
|
|
575
|
-
}, gated('list_send_as', 'mail.read', async (args) => service.listSendAs(args.accountId)));
|
|
576
|
-
if (can('mail.read'))
|
|
577
|
-
registerTool('list_emails', {
|
|
578
|
-
title: 'List emails',
|
|
579
|
-
description: "List emails from the user's connected mailbox with metadata and optional previews. Filter by folder, " +
|
|
580
|
-
'sender, unread, dates, etc. Paginate with pageToken. Use get_email for full bodies.',
|
|
581
|
-
inputSchema: { accountId: accountIdParam, ...queryShape },
|
|
582
|
-
annotations: { readOnlyHint: true },
|
|
583
|
-
}, gated('list_emails', 'mail.read', async (args) => service.listMessages(args.accountId, emailQuery(args), pageOpts(args))));
|
|
520
|
+
}, allowed('get_status', async () => service.status()));
|
|
521
|
+
registerSharedTool('list_folders', { accountId: accountIdParam }, gated('list_folders', async (args) => service.listFolders(args.accountId)));
|
|
522
|
+
registerSharedTool('list_labels', { accountId: accountIdParam }, gated('list_labels', async (args) => service.listLabels(args.accountId)));
|
|
523
|
+
registerSharedTool('list_send_as', { accountId: accountIdParam }, gated('list_send_as', async (args) => service.listSendAs(args.accountId)));
|
|
524
|
+
registerSharedTool('list_emails', { accountId: accountIdParam, ...queryShape }, gated('list_emails', async (args) => service.listMessages(args.accountId, emailQuery(args), pageOpts(args))));
|
|
584
525
|
const { text: _text, ...searchFilterShape } = queryShape;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return {
|
|
611
|
-
...result,
|
|
612
|
-
...([...parsed.diagnostics, ...(result.diagnostics ?? [])].length
|
|
613
|
-
? { diagnostics: [...parsed.diagnostics, ...(result.diagnostics ?? [])] }
|
|
614
|
-
: {}),
|
|
615
|
-
};
|
|
616
|
-
}));
|
|
526
|
+
registerSharedTool('search_emails', {
|
|
527
|
+
accountId: accountIdParam,
|
|
528
|
+
query: z.string().min(1).describe('Typed portable search syntax'),
|
|
529
|
+
...searchFilterShape,
|
|
530
|
+
}, gated('search_emails', async (args) => {
|
|
531
|
+
const parsed = parseEmailSearch(args.query);
|
|
532
|
+
if (!parsed.valid) {
|
|
533
|
+
throw new ClientInputError('invalid_request', parsed.diagnostics.map((item) => item.message).join(' '), {
|
|
534
|
+
diagnostics: parsed.diagnostics,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const merged = mergeEmailQueries(parsed.query, emailQuery(args));
|
|
538
|
+
if (!merged.success) {
|
|
539
|
+
throw new ClientInputError('invalid_request', merged.diagnostics.map((item) => item.message).join(' '), {
|
|
540
|
+
diagnostics: merged.diagnostics,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
const result = await service.listMessages(args.accountId, merged.query, pageOpts(args));
|
|
544
|
+
return {
|
|
545
|
+
...result,
|
|
546
|
+
...([...parsed.diagnostics, ...(result.diagnostics ?? [])].length
|
|
547
|
+
? { diagnostics: [...parsed.diagnostics, ...(result.diagnostics ?? [])] }
|
|
548
|
+
: {}),
|
|
549
|
+
};
|
|
550
|
+
}));
|
|
617
551
|
const { pageToken: _batchPageToken, rawProviderQuery: _batchRawProviderQuery, ...batchSearchFilterShape } = searchFilterShape;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
annotations: { readOnlyHint: true },
|
|
634
|
-
}, gatedResult('search_emails_batch', 'mail.read', async (args) => {
|
|
635
|
-
const parsed = parseEmailSearch(args.query);
|
|
636
|
-
if (!parsed.valid) {
|
|
637
|
-
throw new ClientInputError('invalid_request', parsed.diagnostics.map((item) => item.message).join(' '), {
|
|
638
|
-
diagnostics: parsed.diagnostics,
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
const merged = mergeEmailQueries(parsed.query, emailQuery(args));
|
|
642
|
-
if (!merged.success) {
|
|
643
|
-
throw new ClientInputError('invalid_request', merged.diagnostics.map((item) => item.message).join(' '), {
|
|
644
|
-
diagnostics: merged.diagnostics,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
const result = await service.searchMessagesBatch({
|
|
648
|
-
accounts: args.accounts,
|
|
649
|
-
query: merged.query,
|
|
650
|
-
...(args.pageSize !== undefined ? { pageSize: args.pageSize } : {}),
|
|
651
|
-
...(args.includeSnippet !== undefined ? { includeSnippet: args.includeSnippet } : {}),
|
|
652
|
-
...(args.includeSearchContext !== undefined ? { includeSearchContext: args.includeSearchContext } : {}),
|
|
552
|
+
registerSharedTool('search_emails_batch', {
|
|
553
|
+
accounts: z
|
|
554
|
+
.array(z
|
|
555
|
+
.object(nullableOptionalInputs({ accountId: z.string().min(1), pageToken: z.string().min(1).optional() }))
|
|
556
|
+
.strict())
|
|
557
|
+
.min(1)
|
|
558
|
+
.max(20),
|
|
559
|
+
query: z.string().min(1).describe('Typed portable search syntax'),
|
|
560
|
+
...batchSearchFilterShape,
|
|
561
|
+
folder: z.enum(['inbox', 'sent', 'drafts', 'archive', 'spam', 'trash', 'all']).optional(),
|
|
562
|
+
}, gatedResult('search_emails_batch', async (args) => {
|
|
563
|
+
const parsed = parseEmailSearch(args.query);
|
|
564
|
+
if (!parsed.valid) {
|
|
565
|
+
throw new ClientInputError('invalid_request', parsed.diagnostics.map((item) => item.message).join(' '), {
|
|
566
|
+
diagnostics: parsed.diagnostics,
|
|
653
567
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const diagnostics = [...parsed.diagnostics, ...(group.page.diagnostics ?? [])];
|
|
660
|
-
return {
|
|
661
|
-
...group,
|
|
662
|
-
page: { ...group.page, ...(diagnostics.length ? { diagnostics } : {}) },
|
|
663
|
-
};
|
|
664
|
-
}),
|
|
568
|
+
}
|
|
569
|
+
const merged = mergeEmailQueries(parsed.query, emailQuery(args));
|
|
570
|
+
if (!merged.success) {
|
|
571
|
+
throw new ClientInputError('invalid_request', merged.diagnostics.map((item) => item.message).join(' '), {
|
|
572
|
+
diagnostics: merged.diagnostics,
|
|
665
573
|
});
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
574
|
+
}
|
|
575
|
+
const result = await service.searchMessagesBatch({
|
|
576
|
+
accounts: args.accounts,
|
|
577
|
+
query: merged.query,
|
|
578
|
+
...(args.pageSize !== undefined ? { pageSize: args.pageSize } : {}),
|
|
579
|
+
...(args.includeSnippet !== undefined ? { includeSnippet: args.includeSnippet } : {}),
|
|
580
|
+
...(args.includeSearchContext !== undefined ? { includeSearchContext: args.includeSearchContext } : {}),
|
|
581
|
+
});
|
|
582
|
+
const response = ok({
|
|
583
|
+
...result,
|
|
584
|
+
groups: result.groups.map((group) => {
|
|
585
|
+
if (!group.page)
|
|
586
|
+
return group;
|
|
587
|
+
const diagnostics = [...parsed.diagnostics, ...(group.page.diagnostics ?? [])];
|
|
588
|
+
return {
|
|
589
|
+
...group,
|
|
590
|
+
page: { ...group.page, ...(diagnostics.length ? { diagnostics } : {}) },
|
|
591
|
+
};
|
|
592
|
+
}),
|
|
593
|
+
});
|
|
594
|
+
if (result.groups.every((group) => group.error))
|
|
595
|
+
response.isError = true;
|
|
596
|
+
if (result.groups.some((group) => group.error)) {
|
|
597
|
+
response[TELEMETRY_ERROR] = true;
|
|
598
|
+
response[TELEMETRY_FAILURE] = batchFailureCause(result.groups);
|
|
599
|
+
}
|
|
600
|
+
return response;
|
|
601
|
+
}));
|
|
602
|
+
registerSharedTool('get_email', {
|
|
603
|
+
accountId: accountIdParam,
|
|
604
|
+
messageId: idParam,
|
|
605
|
+
includeHeaders: z
|
|
606
|
+
.boolean()
|
|
607
|
+
.optional()
|
|
608
|
+
.describe('Include Message-ID, References, In-Reply-To and other threading headers'),
|
|
609
|
+
bodyFormat: z.enum(['text', 'html', 'both', 'none']).optional(),
|
|
610
|
+
}, gated('get_email', async (args) => selectBody(await service.getMessage(args.accountId, args.messageId, args.includeHeaders !== undefined ? { includeHeaders: args.includeHeaders } : undefined), args.bodyFormat ?? 'both', MAX_BODY_CHARS)));
|
|
611
|
+
registerSharedTool('get_email_body', {
|
|
612
|
+
accountId: accountIdParam,
|
|
613
|
+
messageId: idParam,
|
|
614
|
+
format: z.enum(['text', 'html']),
|
|
615
|
+
offset: z.number().int().min(0).optional(),
|
|
616
|
+
maxChars: z.number().int().min(1).max(MAX_BODY_CHARS).optional(),
|
|
617
|
+
}, gated('get_email_body', async (args) => {
|
|
618
|
+
const message = await service.getMessage(args.accountId, args.messageId);
|
|
619
|
+
const value = message.body?.[args.format] ?? '';
|
|
620
|
+
const offset = args.offset ?? 0;
|
|
621
|
+
const text = value.slice(offset, offset + (args.maxChars ?? MAX_BODY_CHARS));
|
|
622
|
+
return {
|
|
623
|
+
format: args.format,
|
|
624
|
+
text,
|
|
625
|
+
offset,
|
|
626
|
+
totalChars: value.length,
|
|
627
|
+
...(offset + text.length < value.length ? { nextOffset: offset + text.length } : {}),
|
|
628
|
+
};
|
|
629
|
+
}));
|
|
630
|
+
registerSharedTool('get_thread', {
|
|
631
|
+
accountId: accountIdParam,
|
|
632
|
+
threadId: idParam,
|
|
633
|
+
pageSize: z.number().int().min(1).max(25).optional(),
|
|
634
|
+
pageToken: z.string().min(1).optional(),
|
|
635
|
+
bodyFormat: z.enum(['text', 'html', 'both', 'none']).optional(),
|
|
636
|
+
}, gated('get_thread', async (args) => {
|
|
637
|
+
const thread = await service.getThread(args.accountId, args.threadId);
|
|
638
|
+
let offset = 0;
|
|
639
|
+
if (args.pageToken) {
|
|
640
|
+
try {
|
|
641
|
+
const parsed = JSON.parse(Buffer.from(args.pageToken, 'base64url').toString('utf8'));
|
|
642
|
+
if (parsed.threadId !== args.threadId || !Number.isInteger(parsed.offset) || parsed.offset < 0)
|
|
643
|
+
throw new Error('token');
|
|
644
|
+
offset = parsed.offset;
|
|
671
645
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
if (can('mail.read'))
|
|
675
|
-
registerTool('get_email', {
|
|
676
|
-
title: 'Get email',
|
|
677
|
-
description: 'Fetch one email in full: body (text and/or HTML), recipients, attachment metadata.',
|
|
678
|
-
inputSchema: {
|
|
679
|
-
accountId: accountIdParam,
|
|
680
|
-
messageId: idParam,
|
|
681
|
-
bodyFormat: z.enum(['text', 'html', 'both', 'none']).optional(),
|
|
682
|
-
},
|
|
683
|
-
annotations: { readOnlyHint: true },
|
|
684
|
-
}, gated('get_email', 'mail.read', async (args) => selectBody(await service.getMessage(args.accountId, args.messageId), args.bodyFormat ?? 'both', MAX_BODY_CHARS)));
|
|
685
|
-
if (can('mail.read'))
|
|
686
|
-
registerTool('get_email_body', {
|
|
687
|
-
title: 'Read email body',
|
|
688
|
-
description: 'Read a bounded portion of one email body. Use nextOffset to continue.',
|
|
689
|
-
inputSchema: {
|
|
690
|
-
accountId: accountIdParam,
|
|
691
|
-
messageId: idParam,
|
|
692
|
-
format: z.enum(['text', 'html']),
|
|
693
|
-
offset: z.number().int().min(0).optional(),
|
|
694
|
-
maxChars: z.number().int().min(1).max(MAX_BODY_CHARS).optional(),
|
|
695
|
-
},
|
|
696
|
-
annotations: { readOnlyHint: true },
|
|
697
|
-
}, gated('get_email_body', 'mail.read', async (args) => {
|
|
698
|
-
const message = await service.getMessage(args.accountId, args.messageId);
|
|
699
|
-
const value = message.body?.[args.format] ?? '';
|
|
700
|
-
const offset = args.offset ?? 0;
|
|
701
|
-
const text = value.slice(offset, offset + (args.maxChars ?? MAX_BODY_CHARS));
|
|
702
|
-
return {
|
|
703
|
-
format: args.format,
|
|
704
|
-
text,
|
|
705
|
-
offset,
|
|
706
|
-
totalChars: value.length,
|
|
707
|
-
...(offset + text.length < value.length ? { nextOffset: offset + text.length } : {}),
|
|
708
|
-
};
|
|
709
|
-
}));
|
|
710
|
-
if (can('mail.read'))
|
|
711
|
-
registerTool('get_thread', {
|
|
712
|
-
title: 'Get thread',
|
|
713
|
-
description: 'Fetch a page of conversation messages with bounded body content.',
|
|
714
|
-
inputSchema: {
|
|
715
|
-
accountId: accountIdParam,
|
|
716
|
-
threadId: idParam,
|
|
717
|
-
pageSize: z.number().int().min(1).max(25).optional(),
|
|
718
|
-
pageToken: z.string().min(1).optional(),
|
|
719
|
-
bodyFormat: z.enum(['text', 'html', 'both', 'none']).optional(),
|
|
720
|
-
},
|
|
721
|
-
annotations: { readOnlyHint: true },
|
|
722
|
-
}, gated('get_thread', 'mail.read', async (args) => {
|
|
723
|
-
const thread = await service.getThread(args.accountId, args.threadId);
|
|
724
|
-
let offset = 0;
|
|
725
|
-
if (args.pageToken) {
|
|
726
|
-
try {
|
|
727
|
-
const parsed = JSON.parse(Buffer.from(args.pageToken, 'base64url').toString('utf8'));
|
|
728
|
-
if (parsed.threadId !== args.threadId || !Number.isInteger(parsed.offset) || parsed.offset < 0)
|
|
729
|
-
throw new Error('token');
|
|
730
|
-
offset = parsed.offset;
|
|
731
|
-
}
|
|
732
|
-
catch {
|
|
733
|
-
throw new ClientInputError('invalid_request', 'Invalid thread page token.');
|
|
734
|
-
}
|
|
646
|
+
catch {
|
|
647
|
+
throw new ClientInputError('invalid_request', 'Invalid thread page token.');
|
|
735
648
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (args.replyToMessageId)
|
|
804
|
-
requireCapabilities(['mail.read']);
|
|
805
|
-
return service.previewSend(args.accountId, toSendRequest(args));
|
|
806
|
-
}));
|
|
807
|
-
if (can('mail.send'))
|
|
808
|
-
registerTool('send_email', {
|
|
809
|
-
title: 'Send email',
|
|
810
|
-
description: "Send an email from the user's connected account. Three modes: direct (to + subject + body), sending " +
|
|
811
|
-
'an existing draft (draftId), or replying (replyToMessageId, optionally replyAll) ' +
|
|
812
|
-
'where recipients, subject, and threading are derived from the original. Confirm with the user when ' +
|
|
813
|
-
'intent is ambiguous. Add sendAt to any mode to schedule instead of sending now.',
|
|
814
|
-
inputSchema: {
|
|
815
|
-
idempotencyKey: z
|
|
816
|
-
.string()
|
|
817
|
-
.regex(/^[\x21-\x7e]{1,255}$/)
|
|
818
|
-
.describe('Reuse this key when retrying the same delivery'),
|
|
819
|
-
draftId: idParam.optional().describe('Send this existing draft'),
|
|
820
|
-
...draftShape,
|
|
821
|
-
sendAt: z
|
|
822
|
-
.string()
|
|
823
|
-
.datetime({ offset: true })
|
|
824
|
-
.optional()
|
|
825
|
-
.describe('Schedule delivery instead of sending now: ISO 8601 with timezone offset or Z ' +
|
|
826
|
-
'(e.g. 2026-07-11T09:00:00-07:00). Fluxmail saves the message as a real draft in the mailbox ' +
|
|
827
|
-
'for delivery by fluxmail serve or fluxmail scheduled run using the same store. Stdio does not ' +
|
|
828
|
-
'deliver schedules. Overdue mail may send when a worker starts. Returns a scheduleId for list/cancel.'),
|
|
829
|
-
},
|
|
830
|
-
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
831
|
-
}, handleResult('send_email', async (args) => {
|
|
832
|
-
requireCapabilities(['mail.send']);
|
|
833
|
-
if (args.replyToMessageId !== undefined)
|
|
834
|
-
requireCapabilities(['mail.read']);
|
|
835
|
-
const { sendAt, idempotencyKey, ...sendArgs } = args;
|
|
836
|
-
return deliveryResult(await (sendAt !== undefined
|
|
837
|
-
? service.scheduleDelivery(args.accountId, toSendRequest(sendArgs), sendAt, idempotencyKey)
|
|
838
|
-
: service.deliver(args.accountId, toSendRequest(sendArgs), idempotencyKey)));
|
|
839
|
-
}, () => service.enforceQuota(), toolOptions));
|
|
840
|
-
if (can('mail.read'))
|
|
649
|
+
}
|
|
650
|
+
const pageSize = args.pageSize ?? 10;
|
|
651
|
+
let budget = MAX_BODY_CHARS;
|
|
652
|
+
const messages = thread.messages.slice(offset, offset + pageSize).map((message) => {
|
|
653
|
+
const selected = selectBody(message, args.bodyFormat ?? 'both', budget);
|
|
654
|
+
budget -= (selected.body?.text?.length ?? 0) + (selected.body?.html?.length ?? 0);
|
|
655
|
+
return selected;
|
|
656
|
+
});
|
|
657
|
+
const nextOffset = offset + messages.length;
|
|
658
|
+
return {
|
|
659
|
+
id: thread.id,
|
|
660
|
+
subject: thread.subject,
|
|
661
|
+
messages,
|
|
662
|
+
...(nextOffset < thread.messages.length
|
|
663
|
+
? {
|
|
664
|
+
nextPageToken: Buffer.from(JSON.stringify({ threadId: args.threadId, offset: nextOffset })).toString('base64url'),
|
|
665
|
+
}
|
|
666
|
+
: {}),
|
|
667
|
+
};
|
|
668
|
+
}));
|
|
669
|
+
registerSharedTool('get_draft', { accountId: accountIdParam, draftId: idParam }, gated('get_draft', async (args) => service.getDraft(args.accountId, args.draftId)));
|
|
670
|
+
registerSharedTool('create_draft', draftShape, gated('create_draft', async (args) => {
|
|
671
|
+
if (args.replyToMessageId)
|
|
672
|
+
requireCapabilities(['mail.read']);
|
|
673
|
+
return service.createDraft(args.accountId, toSendInput(args));
|
|
674
|
+
}));
|
|
675
|
+
registerSharedTool('update_draft', { draftId: idParam, ...draftShape }, gated('update_draft', async (args) => {
|
|
676
|
+
if (args.replyToMessageId)
|
|
677
|
+
requireCapabilities(['mail.read']);
|
|
678
|
+
return service.updateDraft(args.accountId, args.draftId, toSendInput(args));
|
|
679
|
+
}));
|
|
680
|
+
registerSharedTool('delete_draft', { accountId: accountIdParam, draftId: idParam }, gated('delete_draft', async (args) => {
|
|
681
|
+
await service.deleteDraft(args.accountId, args.draftId);
|
|
682
|
+
return { deleted: args.draftId };
|
|
683
|
+
}));
|
|
684
|
+
registerSharedTool('preview_send', { draftId: idParam.optional(), ...draftShape }, gated('preview_send', async (args) => {
|
|
685
|
+
if (args.draftId)
|
|
686
|
+
requireCapabilities(['mail.drafts']);
|
|
687
|
+
if (args.replyToMessageId)
|
|
688
|
+
requireCapabilities(['mail.read']);
|
|
689
|
+
return service.previewSend(args.accountId, toSendRequest(args));
|
|
690
|
+
}));
|
|
691
|
+
registerSharedTool('send_email', {
|
|
692
|
+
idempotencyKey: z
|
|
693
|
+
.string()
|
|
694
|
+
.regex(/^[\x21-\x7e]{1,255}$/)
|
|
695
|
+
.describe('Reuse this key when retrying the same delivery'),
|
|
696
|
+
draftId: idParam.optional().describe('Send this existing draft'),
|
|
697
|
+
...draftShape,
|
|
698
|
+
sendAt: z
|
|
699
|
+
.string()
|
|
700
|
+
.datetime({ offset: true })
|
|
701
|
+
.optional()
|
|
702
|
+
.describe('Schedule delivery instead of sending now: ISO 8601 with timezone offset or Z ' +
|
|
703
|
+
'(e.g. 2026-07-11T09:00:00-07:00). Fluxmail saves the message as a real draft in the mailbox ' +
|
|
704
|
+
'for delivery by fluxmail serve or fluxmail scheduled run using the same store. Stdio does not ' +
|
|
705
|
+
'deliver schedules. Overdue mail may send when a worker starts. Returns a scheduleId for list/cancel.'),
|
|
706
|
+
}, handleResult('send_email', async (args) => {
|
|
707
|
+
requireToolScopes('send_email');
|
|
708
|
+
if (args.replyToMessageId !== undefined)
|
|
709
|
+
requireCapabilities(['mail.read']);
|
|
710
|
+
const { sendAt, idempotencyKey, ...sendArgs } = args;
|
|
711
|
+
return deliveryResult(await (sendAt !== undefined
|
|
712
|
+
? service.scheduleDelivery(args.accountId, toSendRequest(sendArgs), sendAt, idempotencyKey)
|
|
713
|
+
: service.deliver(args.accountId, toSendRequest(sendArgs), idempotencyKey)));
|
|
714
|
+
}, () => service.enforceQuota(), toolOptions));
|
|
715
|
+
if (canAll(toolScopes('list_scheduled_emails')))
|
|
841
716
|
registerTool('list_scheduled_emails', {
|
|
842
717
|
title: 'List scheduled emails',
|
|
843
718
|
description: 'List scheduled sends: pending ones first (with sendAt), then past ones (sent, failed, canceled). ' +
|
|
@@ -845,59 +720,46 @@ export function buildMcpServer(service, options = {}) {
|
|
|
845
720
|
'Fluxmail server is running.',
|
|
846
721
|
inputSchema: { accountId: accountIdParam },
|
|
847
722
|
annotations: { readOnlyHint: true },
|
|
848
|
-
}, gated('list_scheduled_emails',
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
title: 'Get delivery status',
|
|
852
|
-
description: 'Check whether a send or forward succeeded, failed, or has an uncertain outcome.',
|
|
853
|
-
inputSchema: { accountId: idParam, operationId: idParam },
|
|
854
|
-
annotations: { readOnlyHint: true },
|
|
855
|
-
}, allowed('get_delivery_operation', 'mail.send', async (args) => service.getDelivery(args.accountId, args.operationId)));
|
|
856
|
-
if (can('mail.drafts'))
|
|
723
|
+
}, gated('list_scheduled_emails', async (args) => service.listScheduled(args.accountId)));
|
|
724
|
+
registerSharedTool('get_delivery_operation', { accountId: idParam, operationId: idParam }, allowed('get_delivery_operation', async (args) => service.getDelivery(args.accountId, args.operationId)));
|
|
725
|
+
if (canAll(toolScopes('cancel_scheduled_email')))
|
|
857
726
|
registerTool('cancel_scheduled_email', {
|
|
858
727
|
title: 'Cancel scheduled email',
|
|
859
728
|
description: 'Cancel a pending scheduled send by scheduleId (from send_email with sendAt, or list_scheduled_emails). ' +
|
|
860
729
|
'The draft stays in the Drafts folder, so the content is not lost.',
|
|
861
730
|
inputSchema: { scheduleId: idParam },
|
|
862
731
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
863
|
-
}, gated('cancel_scheduled_email',
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
to,
|
|
893
|
-
...(cc?.length ? { cc } : {}),
|
|
894
|
-
...(args.comment !== undefined ? { comment: args.comment } : {}),
|
|
895
|
-
...(args.includeAttachments !== undefined ? { includeAttachments: args.includeAttachments } : {}),
|
|
896
|
-
...(args.from !== undefined ? { from: args.from } : {}),
|
|
897
|
-
}, args.idempotencyKey));
|
|
898
|
-
}, () => service.enforceQuota(), toolOptions));
|
|
732
|
+
}, gated('cancel_scheduled_email', async (args) => service.cancelScheduled(args.scheduleId)));
|
|
733
|
+
registerSharedTool('forward_email', {
|
|
734
|
+
idempotencyKey: z
|
|
735
|
+
.string()
|
|
736
|
+
.regex(/^[\x21-\x7e]{1,255}$/)
|
|
737
|
+
.describe('Reuse this key when retrying the same forward'),
|
|
738
|
+
accountId: accountIdParam,
|
|
739
|
+
messageId: idParam,
|
|
740
|
+
from: z.string().email().optional().describe('Connected address or an available send-as address'),
|
|
741
|
+
to: addressList.min(1),
|
|
742
|
+
cc: addressList.optional(),
|
|
743
|
+
comment: z
|
|
744
|
+
.string()
|
|
745
|
+
.optional()
|
|
746
|
+
.describe('Comment above the forwarded message. Keep prose paragraphs on one continuous line and separate paragraphs with blank lines.'),
|
|
747
|
+
includeAttachments: z.boolean().optional().describe('Default true'),
|
|
748
|
+
}, handleResult('forward_email', async (args) => {
|
|
749
|
+
requireToolScopes('forward_email');
|
|
750
|
+
const to = parseAddresses(args.to) ?? [];
|
|
751
|
+
const cc = parseAddresses(args.cc);
|
|
752
|
+
return deliveryResult(await service.deliverForward(args.accountId, {
|
|
753
|
+
messageId: args.messageId,
|
|
754
|
+
to,
|
|
755
|
+
...(cc?.length ? { cc } : {}),
|
|
756
|
+
...(args.comment !== undefined ? { comment: args.comment } : {}),
|
|
757
|
+
...(args.includeAttachments !== undefined ? { includeAttachments: args.includeAttachments } : {}),
|
|
758
|
+
...(args.from !== undefined ? { from: args.from } : {}),
|
|
759
|
+
}, args.idempotencyKey));
|
|
760
|
+
}, () => service.enforceQuota(), toolOptions));
|
|
899
761
|
const modifyEmails = async (action, args) => {
|
|
900
|
-
|
|
762
|
+
requireToolScopes(MODIFY_TOOLS[action].name);
|
|
901
763
|
let modifyAction;
|
|
902
764
|
if (action === 'move') {
|
|
903
765
|
if (!args.folder)
|
|
@@ -933,68 +795,44 @@ export function buildMcpServer(service, options = {}) {
|
|
|
933
795
|
labels: { labels: z.array(z.string().min(1)).min(1).max(100).describe('Gmail labels or Outlook categories') },
|
|
934
796
|
};
|
|
935
797
|
for (const [action, tool] of MODIFY_TOOL_ENTRIES) {
|
|
936
|
-
|
|
937
|
-
continue;
|
|
938
|
-
registerTool(tool.name, {
|
|
939
|
-
title: tool.title,
|
|
940
|
-
description: tool.description,
|
|
941
|
-
inputSchema: {
|
|
942
|
-
accountId: accountIdParam,
|
|
943
|
-
messageIds: messageIdsParam,
|
|
944
|
-
...(tool.input ? modifyInputs[tool.input] : {}),
|
|
945
|
-
},
|
|
946
|
-
annotations: {
|
|
947
|
-
readOnlyHint: false,
|
|
948
|
-
destructiveHint: tool.destructive,
|
|
949
|
-
idempotentHint: true,
|
|
950
|
-
openWorldHint: false,
|
|
951
|
-
},
|
|
952
|
-
}, handleResult(tool.name, async (args) => modifyEmails(action, args), () => service.enforceQuota(), toolOptions));
|
|
798
|
+
registerSharedTool(tool.name, { accountId: accountIdParam, messageIds: messageIdsParam, ...(tool.input ? modifyInputs[tool.input] : {}) }, handleResult(tool.name, async (args) => modifyEmails(action, args), () => service.enforceQuota(), toolOptions));
|
|
953
799
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
...(resourceLinks
|
|
991
|
-
? [{ type: 'resource_link', name: meta.filename, uri, mimeType: meta.mimeType }]
|
|
992
|
-
: []),
|
|
993
|
-
...(args.inline ? [inlineContent(uri, mimeType, content)] : []),
|
|
994
|
-
...pdfTextContent(meta.filename, extracted),
|
|
995
|
-
],
|
|
996
|
-
};
|
|
997
|
-
}));
|
|
800
|
+
registerSharedTool('download_attachment', {
|
|
801
|
+
accountId: accountIdParam,
|
|
802
|
+
messageId: idParam,
|
|
803
|
+
attachmentId: attachmentIdParam,
|
|
804
|
+
inline: z.boolean().optional(),
|
|
805
|
+
pages: pageRange.optional(),
|
|
806
|
+
}, gatedResult('download_attachment', async (args, extra) => {
|
|
807
|
+
const { meta, content, accountId } = await downloadAttachment(args.accountId, args.messageId, args.attachmentId);
|
|
808
|
+
const mimeType = meta.mimeType || 'application/octet-stream';
|
|
809
|
+
const pdf = mediaType(mimeType) === 'application/pdf';
|
|
810
|
+
if (args.pages && !pdf) {
|
|
811
|
+
throw new ClientInputError('invalid_request', 'Pages apply only to PDF attachments.');
|
|
812
|
+
}
|
|
813
|
+
const extracted = pdf && (args.inline || args.pages)
|
|
814
|
+
? await pdfText(content, extra.mcpReq.signal, args.pages, (error) => options.logger?.warn('mcp.pdf_text_failed', 'PDF text extraction failed.', error, {
|
|
815
|
+
productSurface: 'mcp',
|
|
816
|
+
operation: 'download_attachment',
|
|
817
|
+
}))
|
|
818
|
+
: undefined;
|
|
819
|
+
const uri = [
|
|
820
|
+
'fluxmail://attachment',
|
|
821
|
+
...[accountId, args.messageId, args.attachmentId, meta.filename].map(encodeURIComponent),
|
|
822
|
+
].join('/');
|
|
823
|
+
const resourceLinks = supportsResourceLinks(extra);
|
|
824
|
+
return {
|
|
825
|
+
structuredContent: { data: meta },
|
|
826
|
+
content: [
|
|
827
|
+
{ type: 'text', text: JSON.stringify(resourceLinks ? { data: meta } : { data: meta, uri }, null, 2) },
|
|
828
|
+
...(resourceLinks
|
|
829
|
+
? [{ type: 'resource_link', name: meta.filename, uri, mimeType: meta.mimeType }]
|
|
830
|
+
: []),
|
|
831
|
+
...(args.inline ? [inlineContent(uri, mimeType, content)] : []),
|
|
832
|
+
...pdfTextContent(meta.filename, extracted),
|
|
833
|
+
],
|
|
834
|
+
};
|
|
835
|
+
}), `The size limit is ${sizeLabel(maxAttachmentBytes)}.`);
|
|
998
836
|
if (can('mail.read'))
|
|
999
837
|
server.registerResource('attachment', new ResourceTemplate('fluxmail://attachment/{accountId}/{messageId}/{attachmentId}/{filename}', {
|
|
1000
838
|
list: undefined,
|