@lengelhard/imap-email-mcp 1.4.0 → 1.5.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.
Files changed (2) hide show
  1. package/index.js +35 -26
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -203,7 +203,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
203
203
  },
204
204
  {
205
205
  name: 'get_email',
206
- description: 'Get full email content by UID',
206
+ description: 'Get full email content by UID. IMPORTANT: UIDs are folder-scoped. Always pass the same folder the UID came from (list_emails or search_emails). Omitting folder opens INBOX and may silently return a different message.',
207
207
  inputSchema: {
208
208
  type: 'object',
209
209
  properties: {
@@ -239,6 +239,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
239
239
  type: 'string',
240
240
  description: 'Search by sender'
241
241
  },
242
+ to: {
243
+ type: 'string',
244
+ description: 'Search by recipient (use with folder: "Sent" to find outgoing mail)'
245
+ },
242
246
  body: {
243
247
  type: 'string',
244
248
  description: 'Search in body text'
@@ -389,7 +393,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
389
393
  },
390
394
  {
391
395
  name: 'download_attachment',
392
- description: 'Download an email attachment. By default the file is decoded and saved to disk (ATTACHMENT_DIR, default ~/Downloads/email-attachments) and the local file path is returned. Set return_base64=true to get the content inline instead (only allowed for attachments under 700KB).',
396
+ description: 'Download an email attachment. Mode is automatic: attachments up to 700KB are returned inline as base64; larger ones are decoded and saved to disk (ATTACHMENT_DIR, default ~/Downloads/email-attachments) with the file path returned. Every response includes a "mode" field ("base64" or "file"). Set return_base64 explicitly to force a mode; forcing base64 on an oversized attachment falls back to disk with a note instead of erroring.',
393
397
  inputSchema: {
394
398
  type: 'object',
395
399
  properties: {
@@ -416,8 +420,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
416
420
  },
417
421
  return_base64: {
418
422
  type: 'boolean',
419
- description: 'Return content inline as base64 instead of saving to disk. Fails for attachments over 700KB; prefer the default file mode.',
420
- default: false
423
+ description: 'Optional. Omit for automatic mode: attachments up to 700KB return inline base64, larger ones are saved to disk. Set true/false to force a mode; true on an oversized attachment falls back to disk with a note.'
421
424
  }
422
425
  },
423
426
  required: ['uid']
@@ -680,6 +683,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
680
683
  type: 'text',
681
684
  text: JSON.stringify({
682
685
  uid: msg.attributes.uid,
686
+ folder,
683
687
  from: parsed.from?.text,
684
688
  to: parsed.to?.text,
685
689
  cc: parsed.cc?.text,
@@ -691,9 +695,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
691
695
  // Start with what mailparser found (covers Content-Disposition: attachment
692
696
  // and inline+filename cases for content it fully parsed).
693
697
  const fromParser = new Map(
694
- (parsed.attachments || []).map(a => [
695
- (a.filename || '').toLowerCase(),
696
- { filename: a.filename, contentType: a.contentType, size: a.size }
698
+ (parsed.attachments || []).map((a, i) => [
699
+ a.filename ? a.filename.toLowerCase() : `__unnamed_${i}`,
700
+ { filename: a.filename || null, contentType: a.contentType, size: a.size }
697
701
  ])
698
702
  );
699
703
 
@@ -734,6 +738,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
734
738
  let searchCriteria = [];
735
739
  if (args.subject) searchCriteria.push(['SUBJECT', args.subject]);
736
740
  if (args.from) searchCriteria.push(['FROM', args.from]);
741
+ if (args.to) searchCriteria.push(['TO', args.to]);
737
742
  if (args.body) searchCriteria.push(['BODY', args.body]);
738
743
 
739
744
  if (searchCriteria.length === 0) {
@@ -1075,23 +1080,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1075
1080
  const filename = sanitizeFilename(attachmentInfo?.filename || args.filename);
1076
1081
  const contentType = attachmentInfo?.contentType || 'application/octet-stream';
1077
1082
 
1078
- // Inline mode: opt-in only, and only when it safely fits in a tool result
1079
- if (args.return_base64) {
1080
- if (buffer.length > MAX_INLINE_BYTES) {
1081
- return {
1082
- content: [{
1083
- type: 'text',
1084
- text: JSON.stringify({
1085
- error: `Attachment is ${buffer.length} bytes; inline base64 is limited to ${MAX_INLINE_BYTES} bytes because the encoded result would exceed the MCP tool-result size cap. Call again without return_base64 to save it to disk and get the file path.`,
1086
- uid: args.uid,
1087
- partID,
1088
- filename,
1089
- size_bytes: buffer.length
1090
- }, null, 2)
1091
- }],
1092
- isError: true
1093
- };
1094
- }
1083
+ // Resolve mode:
1084
+ // - return_base64 omitted -> auto: inline when it fits, disk otherwise
1085
+ // - return_base64: true -> inline, but fall back to disk (with a note)
1086
+ // if oversized instead of erroring
1087
+ // - return_base64: false -> disk
1088
+ const fitsInline = buffer.length <= MAX_INLINE_BYTES;
1089
+ const wantInline = args.return_base64 === true
1090
+ ? true
1091
+ : args.return_base64 === false
1092
+ ? false
1093
+ : fitsInline;
1094
+
1095
+ if (wantInline && fitsInline) {
1095
1096
  return {
1096
1097
  content: [{
1097
1098
  type: 'text',
@@ -1101,6 +1102,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1101
1102
  filename,
1102
1103
  contentType,
1103
1104
  size_bytes: buffer.length,
1105
+ mode: 'base64',
1104
1106
  encoding: 'base64',
1105
1107
  data: buffer.toString('base64')
1106
1108
  }, null, 2)
@@ -1108,7 +1110,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1108
1110
  };
1109
1111
  }
1110
1112
 
1111
- // Default mode: save to disk and return the path
1113
+ // Note attached to the result when inline was requested but not possible
1114
+ const fallbackNote = (wantInline && !fitsInline)
1115
+ ? `Attachment is ${buffer.length} bytes, over the ${MAX_INLINE_BYTES}-byte inline limit; saved to disk instead.`
1116
+ : undefined;
1117
+
1118
+ // Disk mode: save and return the path
1112
1119
  let target = args.save_path || ATTACHMENT_DIR;
1113
1120
  let filePath;
1114
1121
  if (args.save_path && path.extname(args.save_path)) {
@@ -1136,7 +1143,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1136
1143
  filename,
1137
1144
  contentType,
1138
1145
  size_bytes: buffer.length,
1139
- saved_to: filePath
1146
+ mode: 'file',
1147
+ saved_to: filePath,
1148
+ ...(fallbackNote ? { note: fallbackNote } : {})
1140
1149
  }, null, 2)
1141
1150
  }]
1142
1151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengelhard/imap-email-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for Claude Code that provides email capabilities through IMAP/SMTP. Read, search, compose, and manage emails from any IMAP provider.",
5
5
  "type": "module",
6
6
  "main": "index.js",