fastmail-mcp-server 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastmail-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "MCP server for Fastmail - read, search, and send emails via Claude",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bun
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import {
3
+ McpServer,
4
+ ResourceTemplate,
5
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
3
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
7
  import { z } from "zod";
5
8
  import {
@@ -20,7 +23,7 @@ import type { Email, EmailAddress, Mailbox } from "./jmap/types.js";
20
23
 
21
24
  const server = new McpServer({
22
25
  name: "fastmail",
23
- version: "0.1.0",
26
+ version: "0.1.1",
24
27
  });
25
28
 
26
29
  // ============ Formatters ============
@@ -341,14 +344,19 @@ server.tool(
341
344
  subject: z.string().describe("Email subject line"),
342
345
  body: z.string().describe("Email body text"),
343
346
  cc: z.string().optional().describe("CC recipients, comma-separated"),
347
+ bcc: z
348
+ .string()
349
+ .optional()
350
+ .describe("BCC recipients (hidden), comma-separated"),
344
351
  },
345
- async ({ action, to, subject, body, cc }) => {
352
+ async ({ action, to, subject, body, cc, bcc }) => {
346
353
  // Parse addresses
347
354
  const parseAddresses = (s: string): EmailAddress[] =>
348
355
  s.split(",").map((e) => ({ name: null, email: e.trim() }));
349
356
 
350
357
  const toAddrs = parseAddresses(to);
351
358
  const ccAddrs = cc ? parseAddresses(cc) : undefined;
359
+ const bccAddrs = bcc ? parseAddresses(bcc) : undefined;
352
360
 
353
361
  if (action === "preview") {
354
362
  return {
@@ -359,6 +367,7 @@ server.tool(
359
367
 
360
368
  To: ${formatAddressList(toAddrs)}
361
369
  CC: ${ccAddrs ? formatAddressList(ccAddrs) : "(none)"}
370
+ BCC: ${bccAddrs ? formatAddressList(bccAddrs) : "(none)"}
362
371
  Subject: ${subject}
363
372
 
364
373
  --- Body ---
@@ -376,6 +385,7 @@ To send this email, call this tool again with action: "confirm" and the same par
376
385
  subject,
377
386
  textBody: body,
378
387
  cc: ccAddrs,
388
+ bcc: bccAddrs,
379
389
  });
380
390
 
381
391
  return {
@@ -394,7 +404,7 @@ Email ID: ${emailId}`,
394
404
 
395
405
  server.tool(
396
406
  "reply_to_email",
397
- "Reply to an existing email thread. ALWAYS use action='preview' first to review the draft before sending.",
407
+ "Reply to an existing email thread. ALWAYS use action='preview' first to review the draft before sending. For reply-all, include original CC recipients in the cc param.",
398
408
  {
399
409
  action: z
400
410
  .enum(["preview", "confirm"])
@@ -403,10 +413,29 @@ server.tool(
403
413
  body: z
404
414
  .string()
405
415
  .describe("Reply body text (your response, without quoting original)"),
416
+ cc: z
417
+ .string()
418
+ .optional()
419
+ .describe("CC recipients for reply-all, comma-separated"),
420
+ bcc: z
421
+ .string()
422
+ .optional()
423
+ .describe("BCC recipients (hidden), comma-separated"),
406
424
  },
407
- async ({ action, email_id, body }) => {
425
+ async ({ action, email_id, body, cc, bcc }) => {
426
+ const parseAddresses = (s: string): EmailAddress[] =>
427
+ s.split(",").map((e) => ({ name: null, email: e.trim() }));
428
+
408
429
  const replyParams = await buildReply(email_id, body);
409
430
 
431
+ // Add cc/bcc if provided
432
+ if (cc) {
433
+ replyParams.cc = parseAddresses(cc);
434
+ }
435
+ if (bcc) {
436
+ replyParams.bcc = parseAddresses(bcc);
437
+ }
438
+
410
439
  if (action === "preview") {
411
440
  return {
412
441
  content: [
@@ -415,6 +444,8 @@ server.tool(
415
444
  text: `📧 REPLY PREVIEW - Review before sending:
416
445
 
417
446
  To: ${formatAddressList(replyParams.to)}
447
+ CC: ${replyParams.cc ? formatAddressList(replyParams.cc) : "(none)"}
448
+ BCC: ${replyParams.bcc ? formatAddressList(replyParams.bcc) : "(none)"}
418
449
  Subject: ${replyParams.subject}
419
450
  In-Reply-To: ${replyParams.inReplyTo || "(none)"}
420
451
 
@@ -507,18 +538,112 @@ server.tool(
507
538
  };
508
539
  }
509
540
 
510
- // Binary content - return as base64 with warning
541
+ // Binary content - return full base64
511
542
  return {
512
543
  content: [
513
544
  {
514
545
  type: "text" as const,
515
- text: `Attachment: ${result.name || "(unnamed)"}\nType: ${result.type}\n\nThis is a binary file. Base64 content (first 1000 chars):\n${result.content.slice(0, 1000)}${result.content.length > 1000 ? "..." : ""}`,
546
+ text: `Attachment: ${result.name || "(unnamed)"}\nType: ${result.type}\nSize: ${result.content.length} bytes (base64)\n\n--- Base64 Content ---\n${result.content}`,
516
547
  },
517
548
  ],
518
549
  };
519
550
  },
520
551
  );
521
552
 
553
+ // ============ Resources ============
554
+
555
+ // Expose attachments as resources with blob content
556
+ server.resource(
557
+ "attachment",
558
+ new ResourceTemplate("fastmail://attachment/{emailId}/{blobId}", {
559
+ list: undefined,
560
+ }),
561
+ {
562
+ description: "Email attachment content",
563
+ mimeType: "application/octet-stream",
564
+ },
565
+ async (uri, variables) => {
566
+ const { emailId, blobId } = variables as {
567
+ emailId: string;
568
+ blobId: string;
569
+ };
570
+ const result = await downloadAttachment(emailId, blobId);
571
+
572
+ if (result.isText) {
573
+ return {
574
+ contents: [
575
+ {
576
+ uri: uri.toString(),
577
+ mimeType: result.type,
578
+ text: result.content,
579
+ },
580
+ ],
581
+ };
582
+ }
583
+
584
+ // Binary - return as blob (base64)
585
+ return {
586
+ contents: [
587
+ {
588
+ uri: uri.toString(),
589
+ mimeType: result.type,
590
+ blob: result.content, // already base64
591
+ },
592
+ ],
593
+ };
594
+ },
595
+ );
596
+
597
+ // ============ Prompts ============
598
+
599
+ server.prompt(
600
+ "fastmail-usage",
601
+ "Instructions for using the Fastmail MCP server effectively",
602
+ () => ({
603
+ messages: [
604
+ {
605
+ role: "user",
606
+ content: {
607
+ type: "text",
608
+ text: `# Fastmail MCP Server Usage Guide
609
+
610
+ ## Reading Emails
611
+ 1. Use \`list_mailboxes\` to see available folders
612
+ 2. Use \`list_emails\` with a mailbox name to see emails (e.g., "inbox", "Archive", "Sent")
613
+ 3. Use \`get_email\` with an email ID to read full content
614
+ 4. Use \`search_emails\` to find emails across all folders
615
+
616
+ ## Attachments
617
+ 1. Use \`list_attachments\` to see attachments on an email
618
+ 2. Use \`get_attachment\` with email_id and blob_id to read attachment content
619
+ 3. For binary files (PDFs, images), access via resource URI: fastmail://attachment/{emailId}/{blobId}
620
+
621
+ ## Sending Emails (ALWAYS preview first!)
622
+ 1. Use \`send_email\` with action="preview" to draft
623
+ 2. Review the preview with the user
624
+ 3. Only use action="confirm" after explicit user approval
625
+ 4. Supports to, cc, bcc fields
626
+
627
+ ## Replying
628
+ 1. Use \`reply_to_email\` with action="preview" to draft a reply
629
+ 2. The reply automatically threads correctly
630
+ 3. Add cc/bcc for reply-all scenarios
631
+
632
+ ## Managing Emails
633
+ - \`move_email\` - Move to folder (Archive, Trash, etc.)
634
+ - \`mark_as_read\` - Toggle read/unread
635
+ - \`mark_as_spam\` - Requires preview→confirm (trains spam filter!)
636
+
637
+ ## Safety Rules
638
+ - NEVER send without showing preview first
639
+ - NEVER confirm send without explicit user approval
640
+ - Be careful with mark_as_spam - it affects future filtering`,
641
+ },
642
+ },
643
+ ],
644
+ }),
645
+ );
646
+
522
647
  // ============ Start Server ============
523
648
 
524
649
  const transport = new StdioServerTransport();
@@ -77,12 +77,19 @@ export class JMAPClient {
77
77
  for (const [methodName, data] of result.methodResponses) {
78
78
  if (methodName === "error") {
79
79
  const errorData = data as { type: string; description?: string };
80
+ console.error("[JMAP Error]", JSON.stringify(errorData, null, 2));
80
81
  throw new Error(
81
82
  `JMAP error: ${errorData.type}${errorData.description ? ` - ${errorData.description}` : ""}`,
82
83
  );
83
84
  }
84
85
  }
85
86
 
87
+ // Log responses for debugging
88
+ console.error(
89
+ "[JMAP Response]",
90
+ JSON.stringify(result.methodResponses, null, 2),
91
+ );
92
+
86
93
  return result.methodResponses;
87
94
  }
88
95
 
@@ -301,23 +301,11 @@ export async function sendEmail(params: SendEmailParams): Promise<string> {
301
301
  bcc: params.bcc,
302
302
  subject: params.subject,
303
303
  bodyValues: {
304
- body: {
305
- value: params.textBody,
306
- isEncodingProblem: false,
307
- isTruncated: false,
308
- },
309
- },
310
- textBody: [{ partId: "body", type: "text/plain" } as const].map((p) => ({
311
- ...p,
312
- blobId: null,
313
- size: 0,
314
- name: null,
315
- charset: null,
316
- disposition: null,
317
- cid: null,
318
- language: null,
319
- location: null,
320
- })),
304
+ body: { value: params.textBody },
305
+ } as unknown as EmailCreate["bodyValues"],
306
+ textBody: [
307
+ { partId: "body", type: "text/plain" },
308
+ ] as EmailCreate["textBody"],
321
309
  };
322
310
 
323
311
  if (params.inReplyTo) {
@@ -359,19 +347,52 @@ export async function sendEmail(params: SendEmailParams): Promise<string> {
359
347
  ],
360
348
  ]);
361
349
 
362
- // Extract created email ID
350
+ // Extract created email ID and check for errors
363
351
  const emailSetResponse = responses[0];
364
352
  if (!emailSetResponse) {
365
353
  throw new Error("No response from Email/set");
366
354
  }
367
355
 
368
- const created = (
369
- emailSetResponse[1] as { created?: Record<string, { id: string }> }
370
- ).created;
371
- const emailId = created?.draft?.id;
356
+ const emailSetResult = emailSetResponse[1] as {
357
+ created?: Record<string, { id: string }>;
358
+ notCreated?: Record<string, { type: string; description?: string }>;
359
+ };
360
+
361
+ // Check for creation errors
362
+ if (emailSetResult.notCreated?.draft) {
363
+ const err = emailSetResult.notCreated.draft;
364
+ console.error("[Email/set notCreated]", JSON.stringify(err, null, 2));
365
+ throw new Error(
366
+ `Failed to create email: ${err.type}${err.description ? ` - ${err.description}` : ""}`,
367
+ );
368
+ }
372
369
 
370
+ const emailId = emailSetResult.created?.draft?.id;
373
371
  if (!emailId) {
374
- throw new Error("Failed to create email");
372
+ console.error(
373
+ "[Email/set response]",
374
+ JSON.stringify(emailSetResult, null, 2),
375
+ );
376
+ throw new Error("Failed to create email - no ID returned");
377
+ }
378
+
379
+ // Check submission response
380
+ const submissionResponse = responses[1];
381
+ if (submissionResponse) {
382
+ const submissionResult = submissionResponse[1] as {
383
+ created?: Record<string, unknown>;
384
+ notCreated?: Record<string, { type: string; description?: string }>;
385
+ };
386
+ if (submissionResult.notCreated?.submission) {
387
+ const err = submissionResult.notCreated.submission;
388
+ console.error(
389
+ "[EmailSubmission/set notCreated]",
390
+ JSON.stringify(err, null, 2),
391
+ );
392
+ throw new Error(
393
+ `Failed to submit email: ${err.type}${err.description ? ` - ${err.description}` : ""}`,
394
+ );
395
+ }
375
396
  }
376
397
 
377
398
  return emailId;
package/src/jmap/types.ts CHANGED
@@ -123,6 +123,12 @@ export interface EmailBodyPart {
123
123
  subParts?: EmailBodyPart[];
124
124
  }
125
125
 
126
+ // Minimal body part for creating emails - only partId and type required
127
+ export interface EmailBodyPartCreate {
128
+ partId: string;
129
+ type: string;
130
+ }
131
+
126
132
  export interface EmailBodyValue {
127
133
  value: string;
128
134
  isEncodingProblem: boolean;