@forwardemail/mcp-server 1.0.5 → 1.0.6

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.
Files changed (3) hide show
  1. package/lib/index.js +3 -0
  2. package/lib/tools.js +743 -89
  3. package/package.json +8 -10
package/lib/index.js CHANGED
@@ -18,9 +18,12 @@ class McpServer {
18
18
  tools: Object.values(this.tools).map((tool) => ({
19
19
  name: tool.toolSpec.name,
20
20
  description: tool.toolSpec.description,
21
+ annotations: tool.toolSpec.annotations,
22
+ _meta: tool.toolSpec._meta,
21
23
  inputSchema: {
22
24
  type: 'object',
23
25
  properties: tool.toolSpec.input?.properties ?? {},
26
+ required: tool.toolSpec.input?.required ?? [],
24
27
  },
25
28
  })),
26
29
  }));
package/lib/tools.js CHANGED
@@ -37,108 +37,347 @@ const getTools = (options = {}) => {
37
37
  // otherwise falls back to API key
38
38
  // 'none' – no authentication required
39
39
  //
40
- const createTool = (spec) => ({
41
- toolSpec: {
42
- name: spec.name,
43
- description: spec.description,
44
- input: {
45
- type: 'object',
46
- properties: {
47
- ...spec.inputs,
48
- // Inject alias credential inputs for alias-auth and both-auth tools
49
-
50
- ...((spec.auth === 'aliasAuth' || spec.auth === 'both') && {
51
- alias_username: {
52
- type: 'string',
53
- description:
54
- 'Alias email address for authentication (e.g. user@example.com). ' +
55
- 'Required for alias-authenticated endpoints. ' +
56
- 'Falls back to FORWARD_EMAIL_ALIAS_USER env var.',
57
- },
58
- alias_password: {
59
- type: 'string',
60
- description:
61
- 'Generated alias password for authentication. ' +
62
- 'Required for alias-authenticated endpoints. ' +
63
- 'Falls back to FORWARD_EMAIL_ALIAS_PASSWORD env var. ' +
64
- 'Generate one with the generateAliasPassword tool.',
40
+ // Descriptions for well-known path parameters
41
+ const pathParameterDescriptions = {
42
+ domain_id: 'Domain ID or fully qualified domain name (e.g. "example.com")',
43
+ alias_id: 'Alias ID',
44
+ id: 'Resource ID',
45
+ token_id: 'Token ID for the catch-all password',
46
+ member_id: 'Member ID',
47
+ script_id: 'Sieve script ID',
48
+ };
49
+
50
+ // Descriptions for well-known query parameters
51
+ const queryParameterDescriptions = {
52
+ sort: 'Sort field and direction (e.g. "created_at" or "-created_at" for descending)',
53
+ page: 'Page number for pagination (1-based)',
54
+ limit: 'Number of results per page',
55
+ q: 'Search query string',
56
+ domain: 'Domain name to filter by',
57
+ bounce_category: 'Filter by bounce category',
58
+ response_code: 'Filter by SMTP response code',
59
+ always_send_email:
60
+ 'Whether to always send the log download via email (boolean)',
61
+ is_scheduled: 'Filter by scheduled status (boolean)',
62
+ folder: 'IMAP folder name (e.g. "INBOX", "Sent", "Drafts")',
63
+ is_unread: 'Filter by unread status (boolean)',
64
+ is_flagged: 'Filter by flagged status (boolean)',
65
+ is_deleted: 'Filter by deleted status (boolean)',
66
+ is_draft: 'Filter by draft status (boolean)',
67
+ is_junk: 'Filter by junk/spam status (boolean)',
68
+ is_copied: 'Filter by copied status (boolean)',
69
+ is_encrypted: 'Filter by encrypted status (boolean)',
70
+ is_searchable: 'Filter by searchable status (boolean)',
71
+ is_expired: 'Filter by expired status (boolean)',
72
+ has_attachments: 'Filter messages with attachments (boolean)',
73
+ has_attachment: 'Filter messages with attachments (boolean)',
74
+ subject: 'Filter by message subject',
75
+ body: 'Search within message body',
76
+ text: 'Full text search query',
77
+ headers: 'Search within message headers',
78
+ message_id: 'Filter by Message-ID header',
79
+ search: 'IMAP SEARCH query string',
80
+ since: 'Filter messages after this date (ISO 8601)',
81
+ before: 'Filter messages before this date (ISO 8601)',
82
+ min_size: 'Minimum message size in bytes',
83
+ max_size: 'Maximum message size in bytes',
84
+ from: 'Filter by sender address',
85
+ to: 'Filter by recipient address',
86
+ cc: 'Filter by CC address',
87
+ bcc: 'Filter by BCC address',
88
+ date: 'Filter by message date',
89
+ 'reply-to': 'Filter by Reply-To address',
90
+ eml: 'Return raw EML format (boolean)',
91
+ nodemailer: 'Return in Nodemailer-compatible format (boolean)',
92
+ attachments: 'Include attachments in response (boolean)',
93
+ raw: 'Return raw message source (boolean)',
94
+ subscribed: 'Filter by subscription status (boolean)',
95
+ };
96
+
97
+ const LIST_MESSAGES_MAX_RESULT_SIZE_CHARS = 500_000;
98
+ const LIST_MESSAGES_METADATA_FIELDS = [
99
+ 'id',
100
+ 'uid',
101
+ 'subject',
102
+ 'from',
103
+ 'to',
104
+ 'date',
105
+ 'size',
106
+ 'has_attachment',
107
+ 'has_attachments',
108
+ 'flags',
109
+ 'folder',
110
+ ];
111
+ const LIST_MESSAGES_COLLECTION_KEYS = [
112
+ 'results',
113
+ 'messages',
114
+ 'items',
115
+ 'data',
116
+ ];
117
+
118
+ const isTruthyBoolean = (value) => {
119
+ if (typeof value === 'boolean') return value;
120
+ if (typeof value === 'number') return value === 1;
121
+ if (typeof value !== 'string') return false;
122
+
123
+ return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
124
+ };
125
+
126
+ const getListMessagesCollection = (result) => {
127
+ if (Array.isArray(result)) {
128
+ return {key: null, items: result};
129
+ }
130
+
131
+ if (!result || typeof result !== 'object') return null;
132
+
133
+ for (const key of LIST_MESSAGES_COLLECTION_KEYS) {
134
+ if (Array.isArray(result[key])) {
135
+ return {key, items: result[key]};
136
+ }
137
+ }
138
+
139
+ return null;
140
+ };
141
+
142
+ const toListMessageMetadata = (message) => {
143
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
144
+ return message;
145
+ }
146
+
147
+ const metadata = {};
148
+ for (const field of LIST_MESSAGES_METADATA_FIELDS) {
149
+ if (message[field] !== undefined) metadata[field] = message[field];
150
+ }
151
+
152
+ return Object.keys(metadata).length > 0 ? metadata : message;
153
+ };
154
+
155
+ const replaceListMessagesCollection = (
156
+ originalResult,
157
+ collection,
158
+ items,
159
+ extraProperties = {},
160
+ ) => {
161
+ if (!collection) return originalResult;
162
+ if (collection.key === null) return items;
163
+
164
+ return {
165
+ ...originalResult,
166
+ ...extraProperties,
167
+ [collection.key]: items,
168
+ };
169
+ };
170
+
171
+ const toMetadataOnlyListMessagesResult = (result) => {
172
+ const collection = getListMessagesCollection(result);
173
+ if (!collection) return result;
174
+
175
+ return replaceListMessagesCollection(
176
+ result,
177
+ collection,
178
+ collection.items.map((item) => toListMessageMetadata(item)),
179
+ );
180
+ };
181
+
182
+ const fitListMessagesResultToMaxSize = (
183
+ result,
184
+ maxChars = LIST_MESSAGES_MAX_RESULT_SIZE_CHARS,
185
+ ) => {
186
+ const metadataResult = toMetadataOnlyListMessagesResult(result);
187
+ if (JSON.stringify(metadataResult).length <= maxChars) {
188
+ return metadataResult;
189
+ }
190
+
191
+ const collection = getListMessagesCollection(metadataResult);
192
+ if (!collection) return metadataResult;
193
+
194
+ const extraProperties =
195
+ collection.key === null
196
+ ? {}
197
+ : {
198
+ notice:
199
+ 'Result truncated to stay within the MCP result-size limit. ' +
200
+ 'Use narrower filters or call getMessage for full message content.',
201
+ truncated: true,
202
+ total_count: collection.items.length,
203
+ };
204
+
205
+ const keptItems = [];
206
+ for (const item of collection.items) {
207
+ const candidateItems = [...keptItems, item];
208
+ const candidateResult = replaceListMessagesCollection(
209
+ metadataResult,
210
+ collection,
211
+ candidateItems,
212
+ collection.key === null
213
+ ? {}
214
+ : {
215
+ ...extraProperties,
216
+ returned_count: candidateItems.length,
65
217
  },
66
- }),
218
+ );
219
+
220
+ if (JSON.stringify(candidateResult).length > maxChars) break;
221
+ keptItems.push(item);
222
+ }
223
+
224
+ return replaceListMessagesCollection(
225
+ metadataResult,
226
+ collection,
227
+ keptItems,
228
+ collection.key === null
229
+ ? {}
230
+ : {
231
+ ...extraProperties,
232
+ returned_count: keptItems.length,
233
+ },
234
+ );
235
+ };
236
+
237
+ const createTool = (spec) => {
238
+ // Auto-extract path parameter names from the URL template
239
+ const pathParameters = (spec.path.match(/{(\w+)}/g) || []).map((match) =>
240
+ match.slice(1, -1),
241
+ );
242
+
243
+ // Auto-generate property definitions for path parameters
244
+ const pathProperties = {};
245
+ for (const parameter of pathParameters) {
246
+ pathProperties[parameter] = {
247
+ type: 'string',
248
+ description:
249
+ pathParameterDescriptions[parameter] ||
250
+ `${parameter.replaceAll('_', ' ')}`,
251
+ };
252
+ }
253
+
254
+ // Auto-generate property definitions for query parameters
255
+ const queryProperties = {};
256
+ if (spec.query) {
257
+ for (const parameter of spec.query) {
258
+ queryProperties[parameter] = {
259
+ type: 'string',
260
+ description:
261
+ queryParameterDescriptions[parameter] ||
262
+ `${parameter.replaceAll('_', ' ')}`,
263
+ };
264
+ }
265
+ }
266
+
267
+ return {
268
+ toolSpec: {
269
+ name: spec.name,
270
+ description: spec.description,
271
+ annotations: spec.annotations,
272
+ _meta: spec._meta,
273
+ input: {
274
+ type: 'object',
275
+ properties: {
276
+ // Path parameters first
277
+ ...pathProperties,
278
+ // Query parameters next
279
+ ...queryProperties,
280
+ // Explicit inputs override auto-generated ones
281
+ ...spec.inputs,
282
+ // Inject alias credential inputs for alias-auth and both-auth tools
283
+
284
+ ...((spec.auth === 'aliasAuth' || spec.auth === 'both') && {
285
+ alias_username: {
286
+ type: 'string',
287
+ description:
288
+ 'Alias email address for authentication (e.g. user@example.com). ' +
289
+ 'Required for alias-authenticated endpoints. ' +
290
+ 'Falls back to FORWARD_EMAIL_ALIAS_USER env var.',
291
+ },
292
+ alias_password: {
293
+ type: 'string',
294
+ description:
295
+ 'Generated alias password for authentication. ' +
296
+ 'Required for alias-authenticated endpoints. ' +
297
+ 'Falls back to FORWARD_EMAIL_ALIAS_PASSWORD env var. ' +
298
+ 'Generate one with the generateAliasPassword tool.',
299
+ },
300
+ }),
301
+ },
302
+ // Path parameters are always required; merge with explicit requiredInputs
303
+ required: [...pathParameters, ...(spec.requiredInputs || [])],
67
304
  },
68
305
  },
69
- },
70
- auth: spec.auth || 'apiKey',
71
- async invoke(arguments_) {
72
- let {path} = spec;
73
- const pathParameters = path.match(/{(\w+)}/g) || [];
74
- const queryArguments = {};
75
- const bodyArguments = {};
76
-
77
- // Extract alias credentials from arguments (don't send them to the API)
78
- const aliasUser = arguments_.alias_username || defaultAliasUser;
79
- const aliasPass = arguments_.alias_password || defaultAliasPassword;
80
-
81
- for (const key in arguments_) {
82
- if (!Object.hasOwn(arguments_, key)) continue;
83
- // Skip credential fields
84
- if (key === 'alias_username' || key === 'alias_password') continue;
85
-
86
- if (pathParameters.includes(`{${key}}`)) {
87
- path = path.replace(`{${key}}`, arguments_[key]);
88
- } else if (spec.query && spec.query.includes(key)) {
89
- queryArguments[key] = arguments_[key];
90
- } else {
91
- bodyArguments[key] = arguments_[key];
92
- }
93
- }
306
+ auth: spec.auth || 'apiKey',
307
+ async invoke(arguments_) {
308
+ let {path} = spec;
309
+ const pathParameters = path.match(/{(\w+)}/g) || [];
310
+ const queryArguments = {};
311
+ const bodyArguments = {};
94
312
 
95
- const config = {params: queryArguments};
96
- const hasBody = Object.keys(bodyArguments).length > 0;
313
+ // Extract alias credentials from arguments (don't send them to the API)
314
+ const aliasUser = arguments_.alias_username || defaultAliasUser;
315
+ const aliasPass = arguments_.alias_password || defaultAliasPassword;
97
316
 
98
- // Choose the right client based on auth type
99
- let client;
100
- switch (spec.auth) {
101
- case 'aliasAuth': {
102
- client = createAliasClient(aliasUser, aliasPass);
317
+ for (const key in arguments_) {
318
+ if (!Object.hasOwn(arguments_, key)) continue;
319
+ // Skip credential fields
320
+ if (key === 'alias_username' || key === 'alias_password') continue;
103
321
 
104
- break;
322
+ if (pathParameters.includes(`{${key}}`)) {
323
+ path = path.replace(`{${key}}`, arguments_[key]);
324
+ } else if (spec.query && spec.query.includes(key)) {
325
+ queryArguments[key] = arguments_[key];
326
+ } else {
327
+ bodyArguments[key] = arguments_[key];
328
+ }
105
329
  }
106
330
 
107
- case 'both': {
108
- // Use alias credentials if provided, otherwise fall back to API key
109
- client =
110
- aliasUser && aliasPass
111
- ? createAliasClient(aliasUser, aliasPass)
112
- : apiKeyClient;
331
+ const config = {params: queryArguments};
332
+ const hasBody = Object.keys(bodyArguments).length > 0;
113
333
 
114
- break;
115
- }
334
+ // Choose the right client based on auth type
335
+ let client;
336
+ switch (spec.auth) {
337
+ case 'aliasAuth': {
338
+ client = createAliasClient(aliasUser, aliasPass);
116
339
 
117
- case 'none': {
118
- client = axios.create({baseURL});
340
+ break;
341
+ }
119
342
 
120
- break;
121
- }
343
+ case 'both': {
344
+ // Use alias credentials if provided, otherwise fall back to API key
345
+ client =
346
+ aliasUser && aliasPass
347
+ ? createAliasClient(aliasUser, aliasPass)
348
+ : apiKeyClient;
349
+
350
+ break;
351
+ }
352
+
353
+ case 'none': {
354
+ client = axios.create({baseURL});
122
355
 
123
- default: {
124
- client = apiKeyClient;
356
+ break;
357
+ }
358
+
359
+ default: {
360
+ client = apiKeyClient;
361
+ }
125
362
  }
126
- }
127
363
 
128
- let response;
129
- if (spec.method === 'get' || spec.method === 'delete') {
130
- response = await client[spec.method](path, config);
131
- } else {
132
- response = await client[spec.method](
133
- path,
134
- hasBody ? bodyArguments : undefined,
135
- config,
136
- );
137
- }
364
+ let response;
365
+ if (spec.method === 'get' || spec.method === 'delete') {
366
+ response = await client[spec.method](path, config);
367
+ } else {
368
+ response = await client[spec.method](
369
+ path,
370
+ hasBody ? bodyArguments : undefined,
371
+ config,
372
+ );
373
+ }
138
374
 
139
- return response.data;
140
- },
141
- });
375
+ return typeof spec.transformResponse === 'function'
376
+ ? spec.transformResponse(response.data, arguments_)
377
+ : response.data;
378
+ },
379
+ };
380
+ };
142
381
 
143
382
  const tools = {
144
383
  //
@@ -161,6 +400,24 @@ const getTools = (options = {}) => {
161
400
  method: 'put',
162
401
  path: '/v1/account',
163
402
  auth: 'both',
403
+ inputs: {
404
+ email: {
405
+ type: 'string',
406
+ description: 'Email address to update on the account',
407
+ },
408
+ given_name: {
409
+ type: 'string',
410
+ description: 'First name',
411
+ },
412
+ family_name: {
413
+ type: 'string',
414
+ description: 'Last name',
415
+ },
416
+ avatar_url: {
417
+ type: 'string',
418
+ description: 'Link to avatar image (URL)',
419
+ },
420
+ },
164
421
  }),
165
422
 
166
423
  //
@@ -322,6 +579,61 @@ const getTools = (options = {}) => {
322
579
  method: 'post',
323
580
  path: '/v1/domains',
324
581
  auth: 'apiKey',
582
+ inputs: {
583
+ domain: {
584
+ type: 'string',
585
+ description:
586
+ 'Fully qualified domain name or IP address (e.g. "example.com")',
587
+ },
588
+ plan: {
589
+ type: 'string',
590
+ description: 'Plan type: "free", "enhanced_protection", or "team"',
591
+ },
592
+ catchall: {
593
+ type: 'string',
594
+ description:
595
+ 'Create a default catch-all alias (email address or "true" for default)',
596
+ },
597
+ has_adult_content_protection: {
598
+ type: 'boolean',
599
+ description: 'Enable Spam Scanner adult content protection',
600
+ },
601
+ has_phishing_protection: {
602
+ type: 'boolean',
603
+ description: 'Enable Spam Scanner phishing protection',
604
+ },
605
+ has_executable_protection: {
606
+ type: 'boolean',
607
+ description: 'Enable Spam Scanner executable protection',
608
+ },
609
+ has_virus_protection: {
610
+ type: 'boolean',
611
+ description: 'Enable Spam Scanner virus protection',
612
+ },
613
+ has_recipient_verification: {
614
+ type: 'boolean',
615
+ description:
616
+ 'Require alias recipients to click email verification link',
617
+ },
618
+ ignore_mx_check: {
619
+ type: 'boolean',
620
+ description: 'Ignore MX record check on the domain',
621
+ },
622
+ retention_days: {
623
+ type: 'number',
624
+ description:
625
+ 'Number of days to retain emails (integer between 0 and 30)',
626
+ },
627
+ bounce_webhook: {
628
+ type: 'string',
629
+ description: 'Webhook URL for bounce notifications',
630
+ },
631
+ max_quota_per_alias: {
632
+ type: 'string',
633
+ description: 'Maximum storage quota per alias (e.g. "1GB")',
634
+ },
635
+ },
636
+ requiredInputs: ['domain'],
325
637
  }),
326
638
  getDomain: createTool({
327
639
  name: 'getDomain',
@@ -336,6 +648,50 @@ const getTools = (options = {}) => {
336
648
  method: 'put',
337
649
  path: '/v1/domains/{domain_id}',
338
650
  auth: 'apiKey',
651
+ inputs: {
652
+ smtp_port: {
653
+ type: 'string',
654
+ description: 'Custom SMTP forwarding port number',
655
+ },
656
+ has_adult_content_protection: {
657
+ type: 'boolean',
658
+ description: 'Enable Spam Scanner adult content protection',
659
+ },
660
+ has_phishing_protection: {
661
+ type: 'boolean',
662
+ description: 'Enable Spam Scanner phishing protection',
663
+ },
664
+ has_executable_protection: {
665
+ type: 'boolean',
666
+ description: 'Enable Spam Scanner executable protection',
667
+ },
668
+ has_virus_protection: {
669
+ type: 'boolean',
670
+ description: 'Enable Spam Scanner virus protection',
671
+ },
672
+ has_recipient_verification: {
673
+ type: 'boolean',
674
+ description:
675
+ 'Require alias recipients to click email verification link',
676
+ },
677
+ ignore_mx_check: {
678
+ type: 'boolean',
679
+ description: 'Ignore MX record check on the domain',
680
+ },
681
+ retention_days: {
682
+ type: 'number',
683
+ description:
684
+ 'Number of days to retain emails (integer between 0 and 30)',
685
+ },
686
+ bounce_webhook: {
687
+ type: 'string',
688
+ description: 'Webhook URL for bounce notifications',
689
+ },
690
+ max_quota_per_alias: {
691
+ type: 'string',
692
+ description: 'Maximum storage quota per alias (e.g. "1GB")',
693
+ },
694
+ },
339
695
  }),
340
696
  deleteDomain: createTool({
341
697
  name: 'deleteDomain',
@@ -364,6 +720,7 @@ const getTools = (options = {}) => {
364
720
  method: 'post',
365
721
  path: '/v1/domains/{domain_id}/test-s3-connection',
366
722
  auth: 'apiKey',
723
+ inputs: {},
367
724
  }),
368
725
 
369
726
  //
@@ -382,6 +739,17 @@ const getTools = (options = {}) => {
382
739
  method: 'post',
383
740
  path: '/v1/domains/{domain_id}/catch-all-passwords',
384
741
  auth: 'apiKey',
742
+ inputs: {
743
+ new_password: {
744
+ type: 'string',
745
+ description:
746
+ 'Custom password to set (leave empty for auto-generated password)',
747
+ },
748
+ description: {
749
+ type: 'string',
750
+ description: 'Description for organizing this password',
751
+ },
752
+ },
385
753
  }),
386
754
  deleteCatchAllPassword: createTool({
387
755
  name: 'deleteCatchAllPassword',
@@ -407,6 +775,18 @@ const getTools = (options = {}) => {
407
775
  method: 'post',
408
776
  path: '/v1/domains/{domain_id}/invites',
409
777
  auth: 'apiKey',
778
+ inputs: {
779
+ email: {
780
+ type: 'string',
781
+ description: 'Email address of the user to invite',
782
+ },
783
+ group: {
784
+ type: 'string',
785
+ description:
786
+ 'Group assignment for the invited user: "admin" or "user"',
787
+ },
788
+ },
789
+ requiredInputs: ['email', 'group'],
410
790
  }),
411
791
  removeDomainInvite: createTool({
412
792
  name: 'removeDomainInvite',
@@ -425,6 +805,13 @@ const getTools = (options = {}) => {
425
805
  method: 'put',
426
806
  path: '/v1/domains/{domain_id}/members/{member_id}',
427
807
  auth: 'apiKey',
808
+ inputs: {
809
+ group: {
810
+ type: 'string',
811
+ description: 'Group assignment: "admin" or "user"',
812
+ },
813
+ },
814
+ requiredInputs: ['group'],
428
815
  }),
429
816
  removeDomainMember: createTool({
430
817
  name: 'removeDomainMember',
@@ -451,6 +838,76 @@ const getTools = (options = {}) => {
451
838
  method: 'post',
452
839
  path: '/v1/domains/{domain_id}/aliases',
453
840
  auth: 'apiKey',
841
+ inputs: {
842
+ name: {
843
+ type: 'string',
844
+ description:
845
+ 'Alias name (the part before @). Random if not provided.',
846
+ },
847
+ recipients: {
848
+ type: 'string',
849
+ description:
850
+ 'Comma or newline separated email addresses to forward to',
851
+ },
852
+ description: {
853
+ type: 'string',
854
+ description: 'Alias description',
855
+ },
856
+ labels: {
857
+ type: 'string',
858
+ description: 'Comma separated list of labels',
859
+ },
860
+ has_recipient_verification: {
861
+ type: 'boolean',
862
+ description: 'Require recipients to click an email verification link',
863
+ },
864
+ is_enabled: {
865
+ type: 'boolean',
866
+ description: 'Whether the alias is enabled for email routing',
867
+ },
868
+ error_code_if_disabled: {
869
+ type: 'number',
870
+ description:
871
+ 'SMTP error code when alias is disabled: 250, 421, or 550',
872
+ },
873
+ has_imap: {
874
+ type: 'boolean',
875
+ description: 'Enable or disable IMAP storage for the alias',
876
+ },
877
+ has_pgp: {
878
+ type: 'boolean',
879
+ description: 'Enable OpenPGP encryption for IMAP/POP3 storage',
880
+ },
881
+ public_key: {
882
+ type: 'string',
883
+ description: 'OpenPGP public key in ASCII Armor format',
884
+ },
885
+ max_quota: {
886
+ type: 'string',
887
+ description: 'Maximum storage quota for this alias (e.g. "1GB")',
888
+ },
889
+ vacation_responder_is_enabled: {
890
+ type: 'boolean',
891
+ description: 'Enable automatic vacation responder',
892
+ },
893
+ vacation_responder_start_date: {
894
+ type: 'string',
895
+ description:
896
+ 'Vacation responder start date (MM/DD/YYYY or YYYY-MM-DD)',
897
+ },
898
+ vacation_responder_end_date: {
899
+ type: 'string',
900
+ description: 'Vacation responder end date (MM/DD/YYYY or YYYY-MM-DD)',
901
+ },
902
+ vacation_responder_subject: {
903
+ type: 'string',
904
+ description: 'Subject line for the vacation responder (plaintext)',
905
+ },
906
+ vacation_responder_message: {
907
+ type: 'string',
908
+ description: 'Message body for the vacation responder (plaintext)',
909
+ },
910
+ },
454
911
  }),
455
912
  getAlias: createTool({
456
913
  name: 'getAlias',
@@ -465,6 +922,75 @@ const getTools = (options = {}) => {
465
922
  method: 'put',
466
923
  path: '/v1/domains/{domain_id}/aliases/{alias_id}',
467
924
  auth: 'apiKey',
925
+ inputs: {
926
+ name: {
927
+ type: 'string',
928
+ description: 'Alias name (the part before @)',
929
+ },
930
+ recipients: {
931
+ type: 'string',
932
+ description:
933
+ 'Comma or newline separated email addresses to forward to',
934
+ },
935
+ description: {
936
+ type: 'string',
937
+ description: 'Alias description',
938
+ },
939
+ labels: {
940
+ type: 'string',
941
+ description: 'Comma separated list of labels',
942
+ },
943
+ has_recipient_verification: {
944
+ type: 'boolean',
945
+ description: 'Require recipients to click an email verification link',
946
+ },
947
+ is_enabled: {
948
+ type: 'boolean',
949
+ description: 'Whether the alias is enabled for email routing',
950
+ },
951
+ error_code_if_disabled: {
952
+ type: 'number',
953
+ description:
954
+ 'SMTP error code when alias is disabled: 250, 421, or 550',
955
+ },
956
+ has_imap: {
957
+ type: 'boolean',
958
+ description: 'Enable or disable IMAP storage for the alias',
959
+ },
960
+ has_pgp: {
961
+ type: 'boolean',
962
+ description: 'Enable OpenPGP encryption for IMAP/POP3 storage',
963
+ },
964
+ public_key: {
965
+ type: 'string',
966
+ description: 'OpenPGP public key in ASCII Armor format',
967
+ },
968
+ max_quota: {
969
+ type: 'string',
970
+ description: 'Maximum storage quota for this alias (e.g. "1GB")',
971
+ },
972
+ vacation_responder_is_enabled: {
973
+ type: 'boolean',
974
+ description: 'Enable automatic vacation responder',
975
+ },
976
+ vacation_responder_start_date: {
977
+ type: 'string',
978
+ description:
979
+ 'Vacation responder start date (MM/DD/YYYY or YYYY-MM-DD)',
980
+ },
981
+ vacation_responder_end_date: {
982
+ type: 'string',
983
+ description: 'Vacation responder end date (MM/DD/YYYY or YYYY-MM-DD)',
984
+ },
985
+ vacation_responder_subject: {
986
+ type: 'string',
987
+ description: 'Subject line for the vacation responder (plaintext)',
988
+ },
989
+ vacation_responder_message: {
990
+ type: 'string',
991
+ description: 'Message body for the vacation responder (plaintext)',
992
+ },
993
+ },
468
994
  }),
469
995
  deleteAlias: createTool({
470
996
  name: 'deleteAlias',
@@ -482,6 +1008,28 @@ const getTools = (options = {}) => {
482
1008
  method: 'post',
483
1009
  path: '/v1/domains/{domain_id}/aliases/{alias_id}/generate-password',
484
1010
  auth: 'apiKey',
1011
+ inputs: {
1012
+ new_password: {
1013
+ type: 'string',
1014
+ description:
1015
+ 'Custom password to set (leave empty for auto-generated password)',
1016
+ },
1017
+ password: {
1018
+ type: 'string',
1019
+ description:
1020
+ 'Existing password to change without deleting IMAP storage',
1021
+ },
1022
+ is_override: {
1023
+ type: 'boolean',
1024
+ description:
1025
+ 'Override existing password and delete associated IMAP storage',
1026
+ },
1027
+ emailed_instructions: {
1028
+ type: 'string',
1029
+ description:
1030
+ 'Email address to send the password and setup instructions to',
1031
+ },
1032
+ },
485
1033
  }),
486
1034
 
487
1035
  //
@@ -603,6 +1151,74 @@ const getTools = (options = {}) => {
603
1151
  method: 'post',
604
1152
  path: '/v1/emails',
605
1153
  auth: 'both',
1154
+ inputs: {
1155
+ from: {
1156
+ type: 'string',
1157
+ description: 'Sender email address',
1158
+ },
1159
+ to: {
1160
+ type: 'string',
1161
+ description: 'Comma separated list of recipient email addresses',
1162
+ },
1163
+ cc: {
1164
+ type: 'string',
1165
+ description: 'Comma separated list of CC recipient email addresses',
1166
+ },
1167
+ bcc: {
1168
+ type: 'string',
1169
+ description: 'Comma separated list of BCC recipient email addresses',
1170
+ },
1171
+ subject: {
1172
+ type: 'string',
1173
+ description: 'Email subject line',
1174
+ },
1175
+ text: {
1176
+ type: 'string',
1177
+ description: 'Plaintext version of the email body',
1178
+ },
1179
+ html: {
1180
+ type: 'string',
1181
+ description: 'HTML version of the email body',
1182
+ },
1183
+ attachments: {
1184
+ type: 'string',
1185
+ description:
1186
+ 'JSON array of attachment objects with filename, content, and encoding',
1187
+ },
1188
+ sender: {
1189
+ type: 'string',
1190
+ description: 'Email address for the Sender header',
1191
+ },
1192
+ replyTo: {
1193
+ type: 'string',
1194
+ description: 'Email address for the Reply-To header',
1195
+ },
1196
+ inReplyTo: {
1197
+ type: 'string',
1198
+ description: 'Message-ID that this email is replying to',
1199
+ },
1200
+ references: {
1201
+ type: 'string',
1202
+ description:
1203
+ 'Space separated list of Message-IDs in the reference chain',
1204
+ },
1205
+ priority: {
1206
+ type: 'string',
1207
+ description: 'Email priority: "high", "normal" (default), or "low"',
1208
+ },
1209
+ messageId: {
1210
+ type: 'string',
1211
+ description: 'Custom Message-ID value for the email header',
1212
+ },
1213
+ date: {
1214
+ type: 'string',
1215
+ description: 'Date value for the email Date header (ISO 8601)',
1216
+ },
1217
+ raw: {
1218
+ type: 'string',
1219
+ description: 'Custom RFC822 formatted message to send as raw email',
1220
+ },
1221
+ },
606
1222
  }),
607
1223
  getEmailLimit: createTool({
608
1224
  name: 'getEmailLimit',
@@ -632,11 +1248,41 @@ const getTools = (options = {}) => {
632
1248
  listMessages: createTool({
633
1249
  name: 'listMessages',
634
1250
  description:
635
- 'List and search messages in a folder. ' +
636
- 'Requires alias credentials (alias_username and alias_password).',
1251
+ 'List and search messages in a folder. Supports metadata_only to ' +
1252
+ 'return envelope fields only and automatically trims oversized ' +
1253
+ 'results so the tool remains usable in Anthropic clients. Requires ' +
1254
+ 'alias credentials (alias_username and alias_password).',
637
1255
  method: 'get',
638
1256
  path: '/v1/messages',
639
1257
  auth: 'aliasAuth',
1258
+ _meta: {
1259
+ 'anthropic/maxResultSizeChars': LIST_MESSAGES_MAX_RESULT_SIZE_CHARS,
1260
+ },
1261
+ inputs: {
1262
+ metadata_only: {
1263
+ type: 'boolean',
1264
+ description:
1265
+ 'If true, only return lightweight message metadata (id, uid, ' +
1266
+ 'subject, from, to, date, size, attachment flags, flags, and ' +
1267
+ 'folder). Use getMessage to fetch full content for a specific ' +
1268
+ 'message.',
1269
+ },
1270
+ },
1271
+ transformResponse(result, arguments_) {
1272
+ if (isTruthyBoolean(arguments_.metadata_only)) {
1273
+ return fitListMessagesResultToMaxSize(
1274
+ toMetadataOnlyListMessagesResult(result),
1275
+ );
1276
+ }
1277
+
1278
+ if (
1279
+ JSON.stringify(result).length <= LIST_MESSAGES_MAX_RESULT_SIZE_CHARS
1280
+ ) {
1281
+ return result;
1282
+ }
1283
+
1284
+ return fitListMessagesResultToMaxSize(result);
1285
+ },
640
1286
  query: [
641
1287
  'folder',
642
1288
  'is_unread',
@@ -757,6 +1403,14 @@ const getTools = (options = {}) => {
757
1403
  method: 'post',
758
1404
  path: '/v1/encrypt',
759
1405
  auth: 'none',
1406
+ inputs: {
1407
+ input: {
1408
+ type: 'string',
1409
+ description:
1410
+ 'Any valid Forward Email plaintext DNS TXT record value to encrypt',
1411
+ },
1412
+ },
1413
+ requiredInputs: ['input'],
760
1414
  }),
761
1415
  };
762
1416
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@forwardemail/mcp-server",
3
3
  "description": "Model Context Protocol (MCP) server for Forward Email. Connect AI to your inbox.",
4
- "version": "1.0.5",
4
+ "version": "1.0.6",
5
5
  "author": "Forward Email LLC <support@forwardemail.net> (https://forwardemail.net)",
6
6
  "bin": {
7
7
  "mcp-server": "bin/mcp-server.js"
@@ -55,7 +55,6 @@
55
55
  ],
56
56
  "license": "BUSL-1.1",
57
57
  "main": "lib/index.js",
58
- "packageManager": "pnpm@9.0.0",
59
58
  "prettier": {
60
59
  "useTabs": false,
61
60
  "tabWidth": 2,
@@ -68,13 +67,6 @@
68
67
  "type": "git",
69
68
  "url": "git+https://github.com/forwardemail/mcp-server.git"
70
69
  },
71
- "scripts": {
72
- "lint": "fixpack && remark . -qfo && xo --fix",
73
- "pre-commit": "lint-staged",
74
- "prepare": "husky",
75
- "pretest": "pnpm run lint",
76
- "test": "c8 --reporter=text --reporter=html node --test"
77
- },
78
70
  "types": "lib/index.d.ts",
79
71
  "xo": {
80
72
  "space": 2,
@@ -90,5 +82,11 @@
90
82
  }
91
83
  ]
92
84
  }
85
+ },
86
+ "scripts": {
87
+ "lint": "fixpack && remark . -qfo && xo --fix",
88
+ "pre-commit": "lint-staged",
89
+ "pretest": "pnpm run lint",
90
+ "test": "c8 --reporter=text --reporter=html node --test"
93
91
  }
94
- }
92
+ }