fluxmail 0.15.0 → 0.17.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/auth.d.ts +22 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +29 -3
- package/dist/auth.js.map +1 -1
- package/dist/authorization.d.ts +2 -0
- package/dist/authorization.d.ts.map +1 -1
- package/dist/authorization.js +9 -4
- package/dist/authorization.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +93 -5
- package/dist/cli.js.map +1 -1
- package/dist/cliMail.d.ts.map +1 -1
- package/dist/cliMail.js +21 -1
- package/dist/cliMail.js.map +1 -1
- package/dist/deploymentConfig.d.ts +1 -1
- package/dist/deploymentConfig.d.ts.map +1 -1
- package/dist/deploymentConfig.js +1 -1
- package/dist/deploymentConfig.js.map +1 -1
- package/dist/http/app.d.ts +3 -0
- package/dist/http/app.d.ts.map +1 -1
- package/dist/http/app.js +37 -22
- package/dist/http/app.js.map +1 -1
- package/dist/http/identity.d.ts +20 -0
- package/dist/http/identity.d.ts.map +1 -1
- package/dist/http/identity.js +146 -0
- package/dist/http/identity.js.map +1 -1
- package/dist/http/oauth.d.ts +28 -0
- package/dist/http/oauth.d.ts.map +1 -0
- package/dist/http/oauth.js +552 -0
- package/dist/http/oauth.js.map +1 -0
- package/dist/http/oauthPages.d.ts +36 -0
- package/dist/http/oauthPages.d.ts.map +1 -0
- package/dist/http/oauthPages.js +213 -0
- package/dist/http/oauthPages.js.map +1 -0
- package/dist/http/rest.d.ts.map +1 -1
- package/dist/http/rest.js +14 -8
- package/dist/http/rest.js.map +1 -1
- package/dist/mcp/attachmentContent.d.ts +1 -1
- package/dist/mcp/attachmentContent.d.ts.map +1 -1
- package/dist/mcp/buildServer.d.ts +15 -2
- package/dist/mcp/buildServer.d.ts.map +1 -1
- package/dist/mcp/buildServer.js +214 -106
- package/dist/mcp/buildServer.js.map +1 -1
- package/dist/mcp/clientTelemetry.d.ts +19 -0
- package/dist/mcp/clientTelemetry.d.ts.map +1 -0
- package/dist/mcp/clientTelemetry.js +152 -0
- package/dist/mcp/clientTelemetry.js.map +1 -0
- package/dist/mcp/outputSchemas.d.ts +457 -7
- package/dist/mcp/outputSchemas.d.ts.map +1 -1
- package/dist/mcp/outputSchemas.js +11 -1
- package/dist/mcp/outputSchemas.js.map +1 -1
- package/dist/oauth/clientMetadata.d.ts +39 -0
- package/dist/oauth/clientMetadata.d.ts.map +1 -0
- package/dist/oauth/clientMetadata.js +234 -0
- package/dist/oauth/clientMetadata.js.map +1 -0
- package/dist/oauth/redirectUris.d.ts +13 -0
- package/dist/oauth/redirectUris.d.ts.map +1 -0
- package/dist/oauth/redirectUris.js +78 -0
- package/dist/oauth/redirectUris.js.map +1 -0
- package/dist/permissions.d.ts +4 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +10 -0
- package/dist/permissions.js.map +1 -1
- package/dist/service/emailService.d.ts.map +1 -1
- package/dist/service/emailService.js +4 -1
- package/dist/service/emailService.js.map +1 -1
- package/dist/service/publicErrors.d.ts +4 -0
- package/dist/service/publicErrors.d.ts.map +1 -1
- package/dist/service/publicErrors.js +16 -1
- package/dist/service/publicErrors.js.map +1 -1
- package/dist/storage/db.d.ts +824 -2
- package/dist/storage/db.d.ts.map +1 -1
- package/dist/storage/db.js +142 -1
- package/dist/storage/db.js.map +1 -1
- package/dist/storage/members.d.ts.map +1 -1
- package/dist/storage/members.js +2 -0
- package/dist/storage/members.js.map +1 -1
- package/dist/storage/oauth.d.ts +178 -0
- package/dist/storage/oauth.d.ts.map +1 -0
- package/dist/storage/oauth.js +556 -0
- package/dist/storage/oauth.js.map +1 -0
- package/package.json +9 -6
package/dist/mcp/buildServer.js
CHANGED
|
@@ -2,16 +2,16 @@ import { VERSION } from '../version.js';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
5
|
-
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/
|
|
6
|
-
import {
|
|
7
|
-
import { EmailError, isEmailError, mergeEmailQueries, parseEmailSearch, parseSingleAddress, } from '@fluxmail/core';
|
|
5
|
+
import { CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY, McpServer, ResourceTemplate, } from '@modelcontextprotocol/server';
|
|
6
|
+
import { isEmailError, mergeEmailQueries, parseEmailSearch, parseSingleAddress, } from '@fluxmail/core';
|
|
8
7
|
import { batchFailureCause } from '../service/emailService.js';
|
|
9
8
|
import { DEFAULT_MAX_ATTACHMENT_BYTES } from '../config.js';
|
|
10
9
|
import { DEFAULT_PERMISSION_POLICY, MCP_CAPABILITIES, MCP_CAPABILITY_DESCRIPTIONS, enableCapabilitiesInstructions, hasCapability, normalizePermissionPolicy, } from '../permissions.js';
|
|
11
10
|
import { captureOperation } from '../telemetry.js';
|
|
12
11
|
import { logFailure } from '../logging.js';
|
|
13
|
-
import { ClientInputError, MissingCapabilityError, publicError } from '../service/publicErrors.js';
|
|
12
|
+
import { attachmentTooLarge, ClientInputError, isAttachmentSizeError, MissingCapabilityError, publicError, } from '../service/publicErrors.js';
|
|
14
13
|
import { outputSchemas } from './outputSchemas.js';
|
|
14
|
+
import { mcpClientProperties, trackMcpInitialization } from './clientTelemetry.js';
|
|
15
15
|
import { attachmentContents, inlineContent, mediaType, pageRange, pdfText, pdfTextContent, } from './attachmentContent.js';
|
|
16
16
|
const MAX_BODY_CHARS = 50_000;
|
|
17
17
|
const BYTES_PER_MEGABYTE = 1024 * 1024;
|
|
@@ -33,6 +33,25 @@ function toolJsonSchema(shape, pipeStrategy) {
|
|
|
33
33
|
delete schema.$schema;
|
|
34
34
|
return schema;
|
|
35
35
|
}
|
|
36
|
+
// Retain the existing validators, transforms and JSON Schemas at the SDK boundary.
|
|
37
|
+
// The rest of the server also uses Zod 3; the SDK consumes only Standard Schema here.
|
|
38
|
+
function mcpSchema(shape) {
|
|
39
|
+
const schema = z.object(shape);
|
|
40
|
+
return {
|
|
41
|
+
'~standard': {
|
|
42
|
+
version: 1,
|
|
43
|
+
vendor: 'fluxmail',
|
|
44
|
+
validate: async (value) => {
|
|
45
|
+
const result = await schema.safeParseAsync(value);
|
|
46
|
+
return result.success ? { value: result.data } : { issues: result.error.issues };
|
|
47
|
+
},
|
|
48
|
+
jsonSchema: {
|
|
49
|
+
input: () => toolJsonSchema(shape, 'input'),
|
|
50
|
+
output: () => toolJsonSchema(shape, 'output'),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
36
55
|
function nullableOptionalInputs(shape) {
|
|
37
56
|
return Object.fromEntries(Object.entries(shape).map(([name, schema]) => {
|
|
38
57
|
if (!(schema instanceof z.ZodOptional))
|
|
@@ -210,6 +229,9 @@ function toolError(err, requestId) {
|
|
|
210
229
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
211
230
|
}
|
|
212
231
|
function toolFeatureProperties(tool, args) {
|
|
232
|
+
const modifyAction = MODIFY_ACTION_BY_TOOL.get(tool);
|
|
233
|
+
if (modifyAction)
|
|
234
|
+
return { action: modifyAction };
|
|
213
235
|
if (!args || typeof args !== 'object')
|
|
214
236
|
return {};
|
|
215
237
|
const input = args;
|
|
@@ -224,8 +246,6 @@ function toolFeatureProperties(tool, args) {
|
|
|
224
246
|
};
|
|
225
247
|
case 'forward_email':
|
|
226
248
|
return { include_attachments: input.includeAttachments !== false };
|
|
227
|
-
case 'modify_emails':
|
|
228
|
-
return typeof input.action === 'string' ? { action: input.action } : {};
|
|
229
249
|
case 'download_attachment':
|
|
230
250
|
return {
|
|
231
251
|
destination: input.inline === true ? 'inline' : typeof input.pages === 'string' ? 'pdf_pages' : 'resource',
|
|
@@ -234,6 +254,12 @@ function toolFeatureProperties(tool, args) {
|
|
|
234
254
|
return {};
|
|
235
255
|
}
|
|
236
256
|
}
|
|
257
|
+
function toolTelemetryProperties(tool, args, options) {
|
|
258
|
+
return {
|
|
259
|
+
...toolFeatureProperties(tool, args[0]),
|
|
260
|
+
...options.clientProperties?.(args[1]),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
237
263
|
function handleResult(tool, fn, gate, options = {}) {
|
|
238
264
|
return async (...args) => {
|
|
239
265
|
const finishActivity = options.telemetry?.beginActivity?.();
|
|
@@ -255,7 +281,7 @@ function handleResult(tool, fn, gate, options = {}) {
|
|
|
255
281
|
: {}),
|
|
256
282
|
durationMs: performance.now() - startedAt,
|
|
257
283
|
transport: options.transport ?? 'unknown',
|
|
258
|
-
properties:
|
|
284
|
+
properties: toolTelemetryProperties(tool, args, options),
|
|
259
285
|
});
|
|
260
286
|
return result;
|
|
261
287
|
}
|
|
@@ -275,7 +301,7 @@ function handleResult(tool, fn, gate, options = {}) {
|
|
|
275
301
|
error: err,
|
|
276
302
|
durationMs: performance.now() - startedAt,
|
|
277
303
|
transport: options.transport ?? 'unknown',
|
|
278
|
-
properties:
|
|
304
|
+
properties: toolTelemetryProperties(tool, args, options),
|
|
279
305
|
});
|
|
280
306
|
return toolError(err, requestId);
|
|
281
307
|
}
|
|
@@ -310,24 +336,103 @@ function emailQuery(args) {
|
|
|
310
336
|
'rawProviderQuery',
|
|
311
337
|
];
|
|
312
338
|
const q = {};
|
|
313
|
-
for (const key of keys)
|
|
314
|
-
|
|
315
|
-
|
|
339
|
+
for (const key of keys) {
|
|
340
|
+
const value = args[key];
|
|
341
|
+
// Agents often fill filters they mean to leave out with blank strings.
|
|
342
|
+
if (value === undefined || (typeof value === 'string' && value.trim() === ''))
|
|
343
|
+
continue;
|
|
344
|
+
q[key] = value;
|
|
345
|
+
}
|
|
316
346
|
return q;
|
|
317
347
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
348
|
+
/** One MCP tool per modify action, all served by the same implementation. */
|
|
349
|
+
export const MODIFY_TOOLS = {
|
|
350
|
+
markRead: {
|
|
351
|
+
name: 'mark_emails_read',
|
|
352
|
+
title: 'Mark emails read',
|
|
353
|
+
description: 'Mark messages as read.',
|
|
354
|
+
capability: 'mail.organize',
|
|
355
|
+
destructive: false,
|
|
356
|
+
},
|
|
357
|
+
markUnread: {
|
|
358
|
+
name: 'mark_emails_unread',
|
|
359
|
+
title: 'Mark emails unread',
|
|
360
|
+
description: 'Mark messages as unread.',
|
|
361
|
+
capability: 'mail.organize',
|
|
362
|
+
destructive: false,
|
|
363
|
+
},
|
|
364
|
+
star: {
|
|
365
|
+
name: 'star_emails',
|
|
366
|
+
title: 'Star emails',
|
|
367
|
+
description: 'Star messages.',
|
|
368
|
+
capability: 'mail.organize',
|
|
369
|
+
destructive: false,
|
|
370
|
+
},
|
|
371
|
+
unstar: {
|
|
372
|
+
name: 'unstar_emails',
|
|
373
|
+
title: 'Unstar emails',
|
|
374
|
+
description: 'Remove the star from messages.',
|
|
375
|
+
capability: 'mail.organize',
|
|
376
|
+
destructive: false,
|
|
377
|
+
},
|
|
378
|
+
archive: {
|
|
379
|
+
name: 'archive_emails',
|
|
380
|
+
title: 'Archive emails',
|
|
381
|
+
description: 'Move messages out of the inbox into the archive.',
|
|
382
|
+
capability: 'mail.organize',
|
|
383
|
+
destructive: false,
|
|
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
|
+
},
|
|
330
433
|
};
|
|
434
|
+
const MODIFY_TOOL_ENTRIES = Object.entries(MODIFY_TOOLS);
|
|
435
|
+
const MODIFY_ACTION_BY_TOOL = new Map(MODIFY_TOOL_ENTRIES.map(([action, tool]) => [tool.name, action]));
|
|
331
436
|
const PROTECTED_MOVE_DESTINATIONS = new Set(['archive', 'trash']);
|
|
332
437
|
function disabledCapabilitiesInstructions(permissions, source) {
|
|
333
438
|
const disabled = MCP_CAPABILITIES.filter((capability) => !hasCapability(permissions, capability));
|
|
@@ -348,7 +453,6 @@ export function buildMcpServer(service, options = {}) {
|
|
|
348
453
|
const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
349
454
|
const can = (capability) => hasCapability(permissions, capability);
|
|
350
455
|
const canAll = (capabilities) => capabilities.every(can);
|
|
351
|
-
const canAny = (capabilities) => capabilities.some(can);
|
|
352
456
|
const requireCapabilities = (capabilities) => {
|
|
353
457
|
const missing = capabilities.filter((capability) => !can(capability));
|
|
354
458
|
if (missing.length) {
|
|
@@ -357,28 +461,42 @@ export function buildMcpServer(service, options = {}) {
|
|
|
357
461
|
}
|
|
358
462
|
};
|
|
359
463
|
const downloadAttachment = async (accountId, messageId, attachmentId) => {
|
|
360
|
-
const attachment = await service
|
|
464
|
+
const attachment = await service
|
|
465
|
+
.getAttachment(accountId, messageId, attachmentId, maxAttachmentBytes)
|
|
466
|
+
.catch((error) => {
|
|
467
|
+
if (!isAttachmentSizeError(error))
|
|
468
|
+
throw error;
|
|
469
|
+
const sizeBytes = error.data?.sizeBytes;
|
|
470
|
+
throw attachmentTooLarge('MCP', maxAttachmentBytes, typeof sizeBytes === 'number' ? sizeBytes : undefined);
|
|
471
|
+
});
|
|
361
472
|
if (attachment.content.length > maxAttachmentBytes) {
|
|
362
|
-
throw
|
|
363
|
-
sizeBytes: attachment.content.length,
|
|
364
|
-
maxBytes: maxAttachmentBytes,
|
|
365
|
-
});
|
|
473
|
+
throw attachmentTooLarge('MCP', maxAttachmentBytes, attachment.content.length);
|
|
366
474
|
}
|
|
367
475
|
return attachment;
|
|
368
476
|
};
|
|
477
|
+
// Modern requests carry the client in their envelope; legacy stdio keeps it from initialization.
|
|
478
|
+
const toolOptions = {
|
|
479
|
+
...options,
|
|
480
|
+
clientProperties: (ctx) => {
|
|
481
|
+
const context = ctx;
|
|
482
|
+
// The SDK types the lifted envelope as empty, but it holds the reserved _meta keys.
|
|
483
|
+
const envelope = context?.mcpReq.envelope;
|
|
484
|
+
return mcpClientProperties(envelope?.[CLIENT_INFO_META_KEY] ?? server.server.getClientVersion(), envelope?.[PROTOCOL_VERSION_META_KEY] ?? (context ? protocolVersionOf(context) : undefined));
|
|
485
|
+
},
|
|
486
|
+
};
|
|
369
487
|
// Every tool except get_status (the diagnostic way out) enforces the plan quota.
|
|
370
488
|
const gated = (tool, capability, fn) => handle(tool, async (...args) => {
|
|
371
489
|
requireCapabilities([capability]);
|
|
372
490
|
return fn(...args);
|
|
373
|
-
}, () => service.enforceQuota(),
|
|
491
|
+
}, () => service.enforceQuota(), toolOptions);
|
|
374
492
|
const allowed = (tool, capability, fn) => handle(tool, async (...args) => {
|
|
375
493
|
requireCapabilities([capability]);
|
|
376
494
|
return fn(...args);
|
|
377
|
-
}, undefined,
|
|
495
|
+
}, undefined, toolOptions);
|
|
378
496
|
const gatedResult = (tool, capability, fn) => handleResult(tool, async (...args) => {
|
|
379
497
|
requireCapabilities([capability]);
|
|
380
498
|
return fn(...args);
|
|
381
|
-
}, () => service.enforceQuota(),
|
|
499
|
+
}, () => service.enforceQuota(), toolOptions);
|
|
382
500
|
const server = new McpServer({ name: 'fluxmail', title: 'Fluxmail Email', version: VERSION }, {
|
|
383
501
|
instructions: "Fluxmail is the user's email integration, already authenticated against their real mailboxes. " +
|
|
384
502
|
'Tool results are structured for you, not for display. Message ids, thread ids, draft ids, and ' +
|
|
@@ -388,40 +506,26 @@ export function buildMcpServer(service, options = {}) {
|
|
|
388
506
|
'than echoing raw payloads, ids, or field names.' +
|
|
389
507
|
disabledCapabilitiesInstructions(permissions, permissionSource),
|
|
390
508
|
});
|
|
391
|
-
//
|
|
392
|
-
// stdio negotiates it once, so remember it. The SDK has no public accessor for the negotiated
|
|
393
|
-
// version, so observe the initialize request. The SDK installs onmessage before starting the
|
|
394
|
-
// transport, and this wrapper runs before the SDK handles each message.
|
|
395
|
-
let negotiatedProtocolVersion;
|
|
509
|
+
// Observe successful handshakes/discovery without interpreting protocol negotiation ourselves.
|
|
396
510
|
const connect = server.connect.bind(server);
|
|
397
511
|
server.connect = (transport) => {
|
|
398
512
|
const start = transport.start.bind(transport);
|
|
399
513
|
transport.start = () => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const requested = message.params.protocolVersion;
|
|
404
|
-
negotiatedProtocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
405
|
-
? requested
|
|
406
|
-
: LATEST_PROTOCOL_VERSION;
|
|
407
|
-
}
|
|
408
|
-
onmessage?.(message, extra);
|
|
409
|
-
};
|
|
514
|
+
if (options.telemetry && (options.transport === 'stdio' || options.transport === 'http')) {
|
|
515
|
+
trackMcpInitialization(transport, options.telemetry, options.transport);
|
|
516
|
+
}
|
|
410
517
|
return start();
|
|
411
518
|
};
|
|
412
519
|
return connect(transport);
|
|
413
520
|
};
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
const version = (typeof header === 'string' ? header : undefined) ?? negotiatedProtocolVersion ?? '2025-03-26';
|
|
417
|
-
return version >= '2025-06-18';
|
|
418
|
-
};
|
|
521
|
+
const protocolVersionOf = (ctx) => ctx.http?.req?.headers.get('mcp-protocol-version') ?? server.server.getNegotiatedProtocolVersion();
|
|
522
|
+
const supportsResourceLinks = (ctx) => (protocolVersionOf(ctx) ?? '2025-03-26') >= '2025-06-18';
|
|
419
523
|
const listedTools = [];
|
|
420
|
-
const registerTool = (
|
|
524
|
+
const registerTool = (name, config, callback) => {
|
|
421
525
|
const outputSchema = outputSchemas[name];
|
|
422
526
|
const inputShape = config.inputSchema;
|
|
423
527
|
const inputSchema = inputShape ? nullableOptionalInputs(inputShape) : undefined;
|
|
424
|
-
const registered = server.registerTool(name, { ...config,
|
|
528
|
+
const registered = server.registerTool(name, { ...config, inputSchema: mcpSchema(inputSchema ?? {}), outputSchema: mcpSchema(outputSchema) }, callback);
|
|
425
529
|
listedTools.push({
|
|
426
530
|
name,
|
|
427
531
|
title: config.title,
|
|
@@ -430,10 +534,9 @@ export function buildMcpServer(service, options = {}) {
|
|
|
430
534
|
outputSchema: toolJsonSchema(outputSchema, 'output'),
|
|
431
535
|
annotations: config.annotations,
|
|
432
536
|
execution: { taskSupport: 'forbidden' },
|
|
433
|
-
_meta: config._meta,
|
|
434
537
|
});
|
|
435
538
|
return registered;
|
|
436
|
-
}
|
|
539
|
+
};
|
|
437
540
|
if (can('mail.read'))
|
|
438
541
|
registerTool('list_accounts', {
|
|
439
542
|
title: 'List accounts',
|
|
@@ -733,7 +836,7 @@ export function buildMcpServer(service, options = {}) {
|
|
|
733
836
|
return deliveryResult(await (sendAt !== undefined
|
|
734
837
|
? service.scheduleDelivery(args.accountId, toSendRequest(sendArgs), sendAt, idempotencyKey)
|
|
735
838
|
: service.deliver(args.accountId, toSendRequest(sendArgs), idempotencyKey)));
|
|
736
|
-
}, () => service.enforceQuota(),
|
|
839
|
+
}, () => service.enforceQuota(), toolOptions));
|
|
737
840
|
if (can('mail.read'))
|
|
738
841
|
registerTool('list_scheduled_emails', {
|
|
739
842
|
title: 'List scheduled emails',
|
|
@@ -792,59 +895,61 @@ export function buildMcpServer(service, options = {}) {
|
|
|
792
895
|
...(args.includeAttachments !== undefined ? { includeAttachments: args.includeAttachments } : {}),
|
|
793
896
|
...(args.from !== undefined ? { from: args.from } : {}),
|
|
794
897
|
}, args.idempotencyKey));
|
|
795
|
-
}, () => service.enforceQuota(),
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
898
|
+
}, () => service.enforceQuota(), toolOptions));
|
|
899
|
+
const modifyEmails = async (action, args) => {
|
|
900
|
+
requireCapabilities([MODIFY_TOOLS[action].capability]);
|
|
901
|
+
let modifyAction;
|
|
902
|
+
if (action === 'move') {
|
|
903
|
+
if (!args.folder)
|
|
904
|
+
throw new ClientInputError('invalid_request', 'Moving messages requires "folder"');
|
|
905
|
+
if (PROTECTED_MOVE_DESTINATIONS.has(args.folder.trim().toLowerCase())) {
|
|
906
|
+
throw new ClientInputError('invalid_request', 'Archive or trash these messages instead of moving them to this folder.');
|
|
907
|
+
}
|
|
908
|
+
modifyAction = { move: args.folder };
|
|
909
|
+
}
|
|
910
|
+
else if (action === 'addLabels' || action === 'removeLabels') {
|
|
911
|
+
if (!args.labels?.length)
|
|
912
|
+
throw new ClientInputError('invalid_request', `${action} requires "labels"`);
|
|
913
|
+
modifyAction = action === 'addLabels' ? { addLabels: args.labels } : { removeLabels: args.labels };
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
modifyAction = action;
|
|
917
|
+
}
|
|
918
|
+
const result = await service.modify(args.accountId, args.messageIds, modifyAction);
|
|
919
|
+
const response = ok({ ...result, action });
|
|
920
|
+
if (result.failed.length || result.uncertainIds.length) {
|
|
921
|
+
response.isError = true;
|
|
922
|
+
response[TELEMETRY_ERROR] = true;
|
|
923
|
+
}
|
|
924
|
+
return response;
|
|
925
|
+
};
|
|
926
|
+
const messageIdsParam = z
|
|
927
|
+
.array(idParam)
|
|
928
|
+
.min(1)
|
|
929
|
+
.refine((ids) => new Set(ids).size <= 100, 'At most 100 distinct message IDs are allowed.')
|
|
930
|
+
.describe('Messages to change, up to 100 distinct IDs');
|
|
931
|
+
const modifyInputs = {
|
|
932
|
+
folder: { folder: z.string().min(1).describe('Destination folder name or role') },
|
|
933
|
+
labels: { labels: z.array(z.string().min(1)).min(1).max(100).describe('Gmail labels or Outlook categories') },
|
|
934
|
+
};
|
|
935
|
+
for (const [action, tool] of MODIFY_TOOL_ENTRIES) {
|
|
936
|
+
if (!can(tool.capability))
|
|
937
|
+
continue;
|
|
938
|
+
registerTool(tool.name, {
|
|
939
|
+
title: tool.title,
|
|
940
|
+
description: tool.description,
|
|
802
941
|
inputSchema: {
|
|
803
942
|
accountId: accountIdParam,
|
|
804
|
-
messageIds:
|
|
805
|
-
|
|
806
|
-
.min(1)
|
|
807
|
-
.refine((ids) => new Set(ids).size <= 100, 'At most 100 distinct message IDs are allowed.'),
|
|
808
|
-
action: z.enum(modifyActions),
|
|
809
|
-
folder: z.string().min(1).optional().describe('Target folder for action=move'),
|
|
810
|
-
labels: z.array(z.string().min(1)).max(100).optional().describe('Labels for addLabels/removeLabels'),
|
|
943
|
+
messageIds: messageIdsParam,
|
|
944
|
+
...(tool.input ? modifyInputs[tool.input] : {}),
|
|
811
945
|
},
|
|
812
946
|
annotations: {
|
|
813
947
|
readOnlyHint: false,
|
|
814
|
-
destructiveHint:
|
|
948
|
+
destructiveHint: tool.destructive,
|
|
815
949
|
idempotentHint: true,
|
|
950
|
+
openWorldHint: false,
|
|
816
951
|
},
|
|
817
|
-
}, handleResult(
|
|
818
|
-
if (!allowedModifyActions.has(args.action)) {
|
|
819
|
-
throw new EmailError('permission_denied', `This MCP connection does not allow action=${args.action}.`);
|
|
820
|
-
}
|
|
821
|
-
requireCapabilities([MODIFY_CAPABILITIES[args.action]]);
|
|
822
|
-
let action;
|
|
823
|
-
if (args.action === 'move') {
|
|
824
|
-
if (!args.folder)
|
|
825
|
-
throw new ClientInputError('invalid_request', 'action=move requires "folder"');
|
|
826
|
-
if (PROTECTED_MOVE_DESTINATIONS.has(args.folder.trim().toLowerCase())) {
|
|
827
|
-
throw new ClientInputError('invalid_request', 'Use the dedicated archive or trash action for this folder');
|
|
828
|
-
}
|
|
829
|
-
action = { move: args.folder };
|
|
830
|
-
}
|
|
831
|
-
else if (args.action === 'addLabels' || args.action === 'removeLabels') {
|
|
832
|
-
if (!args.labels?.length) {
|
|
833
|
-
throw new ClientInputError('invalid_request', `action=${args.action} requires "labels"`);
|
|
834
|
-
}
|
|
835
|
-
action = args.action === 'addLabels' ? { addLabels: args.labels } : { removeLabels: args.labels };
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
action = args.action;
|
|
839
|
-
}
|
|
840
|
-
const result = await service.modify(args.accountId, args.messageIds, action);
|
|
841
|
-
const response = ok({ ...result, action: args.action });
|
|
842
|
-
if (result.failed.length || result.uncertainIds.length) {
|
|
843
|
-
response.isError = true;
|
|
844
|
-
response[TELEMETRY_ERROR] = true;
|
|
845
|
-
}
|
|
846
|
-
return response;
|
|
847
|
-
}, () => service.enforceQuota(), options));
|
|
952
|
+
}, handleResult(tool.name, async (args) => modifyEmails(action, args), () => service.enforceQuota(), toolOptions));
|
|
848
953
|
}
|
|
849
954
|
if (can('mail.read'))
|
|
850
955
|
registerTool('download_attachment', {
|
|
@@ -868,7 +973,7 @@ export function buildMcpServer(service, options = {}) {
|
|
|
868
973
|
throw new ClientInputError('invalid_request', 'Pages apply only to PDF attachments.');
|
|
869
974
|
}
|
|
870
975
|
const extracted = pdf && (args.inline || args.pages)
|
|
871
|
-
? await pdfText(content, extra.signal, args.pages, (error) => options.logger?.warn('mcp.pdf_text_failed', 'PDF text extraction failed.', error, {
|
|
976
|
+
? await pdfText(content, extra.mcpReq.signal, args.pages, (error) => options.logger?.warn('mcp.pdf_text_failed', 'PDF text extraction failed.', error, {
|
|
872
977
|
productSurface: 'mcp',
|
|
873
978
|
operation: 'download_attachment',
|
|
874
979
|
}))
|
|
@@ -899,9 +1004,12 @@ export function buildMcpServer(service, options = {}) {
|
|
|
899
1004
|
const { meta, content } = await downloadAttachment(decodeURIComponent(String(variables.accountId)), decodeURIComponent(String(variables.messageId)), decodeURIComponent(String(variables.attachmentId)));
|
|
900
1005
|
return { contents: [attachmentContents(uri.href, meta.mimeType || 'application/octet-stream', content)] };
|
|
901
1006
|
});
|
|
1007
|
+
if (can('mail.read'))
|
|
1008
|
+
server.server.registerCapabilities({ resources: { listChanged: false } });
|
|
902
1009
|
if (listedTools.length) {
|
|
903
|
-
|
|
904
|
-
|
|
1010
|
+
server.server.registerCapabilities({ tools: { listChanged: false } });
|
|
1011
|
+
// Preserve the advertised schemas, including empty inputs and output schema dialects.
|
|
1012
|
+
server.server.setRequestHandler('tools/list', () => ({ tools: listedTools }));
|
|
905
1013
|
}
|
|
906
1014
|
return server;
|
|
907
1015
|
}
|