Skip to main content
Glama
MadLlama25

Fastmail MCP Server

by MadLlama25

edit_draft

Update a draft email by specifying only the fields you want to change. Atomically replaces the old draft with a new version, preserving all other fields.

Instructions

Edit an existing draft email. Since JMAP emails are immutable, this atomically destroys the old draft and creates a new one with the updated fields. Only fields you provide will be changed; others are preserved from the original draft.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
emailIdYesThe ID of the draft email to edit
toNoUpdated recipient email addresses (optional, keeps existing if omitted)
ccNoUpdated CC email addresses (optional)
bccNoUpdated BCC email addresses (optional)
fromNoUpdated sender email address (optional)
subjectNoUpdated email subject (optional)
textBodyNoUpdated plain text body (optional)
htmlBodyNoUpdated HTML body (optional)
replyToNoReply-To email addresses (replies go here instead of to the sender)

Implementation Reference

  • The handler for the 'edit_draft' tool in the CallToolRequestSchema switch case. It extracts args (emailId, to, cc, bcc, from, subject, textBody, htmlBody, replyTo), validates emailId is required, and calls client.updateDraft() which returns a new email ID.
    case 'edit_draft': {
      const { emailId, to, cc, bcc, from, subject, textBody, htmlBody, replyTo } = args as any;
      if (!emailId) {
        throw new McpError(ErrorCode.InvalidParams, 'emailId is required');
      }
    
      const newEmailId = await client.updateDraft(emailId, {
        to,
        cc,
        bcc,
        from,
        subject,
        textBody,
        htmlBody,
        replyTo,
      });
    
      return {
        content: [
          {
            type: 'text',
            text: `Draft updated successfully. New Email ID: ${newEmailId} (old draft ${emailId} was replaced)`,
          },
        ],
      };
    }
  • The JmapClient.updateDraft() method implements the core logic: fetches existing draft, verifies it's a draft (has $draft keyword), resolves identity, merges updates with existing values, and atomically creates a new draft while destroying the old one via a single Email/set JMAP call.
    async updateDraft(emailId: string, updates: {
      to?: string[];
      cc?: string[];
      bcc?: string[];
      subject?: string;
      textBody?: string;
      htmlBody?: string;
      from?: string;
      replyTo?: string[];
    }): Promise<string> {
      const session = await this.getSession();
    
      // Fetch the existing email
      const getRequest: JmapRequest = {
        using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
        methodCalls: [
          ['Email/get', {
            accountId: session.accountId,
            ids: [emailId],
            properties: ['id', 'subject', 'from', 'to', 'cc', 'bcc', 'replyTo', 'textBody', 'htmlBody', 'bodyValues', 'mailboxIds', 'keywords'],
            bodyProperties: ['partId', 'blobId', 'type', 'size'],
            fetchTextBodyValues: true,
            fetchHTMLBodyValues: true,
          }, 'getEmail']
        ]
      };
    
      const getResponse = await this.makeRequest(getRequest);
      const existingEmail = this.getListResult(getResponse, 0)[0];
      if (!existingEmail) {
        throw new Error(`Email with ID '${emailId}' not found`);
      }
    
      // Verify it's a draft
      if (!existingEmail.keywords?.$draft) {
        throw new Error('Cannot edit a non-draft email');
      }
    
      // Resolve identity
      const identities = await this.getIdentities();
      if (!identities || identities.length === 0) {
        throw new Error('No sending identities found');
      }
    
      let selectedIdentity;
      if (updates.from) {
        selectedIdentity = identities.find(id => matchesIdentity(id.email, updates.from!));
        if (!selectedIdentity) {
          throw new Error('From address is not verified for sending. Choose one of your verified identities.');
        }
      } else {
        // Use existing from, or fall back to default identity
        const existingFrom = existingEmail.from?.[0]?.email;
        if (existingFrom) {
          selectedIdentity = identities.find(id => matchesIdentity(id.email, existingFrom))
            || identities.find(id => id.mayDelete === false) || identities[0];
        } else {
          selectedIdentity = identities.find(id => id.mayDelete === false) || identities[0];
        }
      }
    
      // Extract existing body values
      const existingTextBody = existingEmail.bodyValues
        ? Object.values(existingEmail.bodyValues).find((bv: any) =>
            existingEmail.textBody?.some((tb: any) => tb.partId === (bv as any).partId || true)
          )
        : null;
      const existingHtmlBody = existingEmail.bodyValues
        ? Object.values(existingEmail.bodyValues).find((bv: any) =>
            existingEmail.htmlBody?.some((hb: any) => hb.partId === (bv as any).partId || true)
          )
        : null;
    
      // Merge: updates override existing values
      const mergedSubject = updates.subject !== undefined ? updates.subject : (existingEmail.subject || '');
      const mergedTo = updates.to !== undefined ? updates.to.map(addr => ({ email: addr })) : (existingEmail.to || []);
      const mergedCc = updates.cc !== undefined ? updates.cc.map(addr => ({ email: addr })) : (existingEmail.cc || []);
      const mergedBcc = updates.bcc !== undefined ? updates.bcc.map(addr => ({ email: addr })) : (existingEmail.bcc || []);
      const mergedReplyTo = updates.replyTo !== undefined ? updates.replyTo.map(addr => ({ email: addr })) : (existingEmail.replyTo || null);
    
      const textBodyValue = updates.textBody !== undefined ? updates.textBody : (existingTextBody as any)?.value;
      const htmlBodyValue = updates.htmlBody !== undefined ? updates.htmlBody : (existingHtmlBody as any)?.value;
    
      const emailObject: any = {
        mailboxIds: existingEmail.mailboxIds,
        keywords: { $draft: true },
        from: [{ email: updates.from || existingEmail.from?.[0]?.email || selectedIdentity.email }],
        to: mergedTo,
        cc: mergedCc,
        bcc: mergedBcc,
        subject: mergedSubject,
        ...(mergedReplyTo?.length && { replyTo: mergedReplyTo }),
      };
    
      if (textBodyValue) emailObject.textBody = [{ partId: 'text', type: 'text/plain' }];
      if (htmlBodyValue) emailObject.htmlBody = [{ partId: 'html', type: 'text/html' }];
      if (textBodyValue || htmlBodyValue) {
        emailObject.bodyValues = {
          ...(textBodyValue && { text: { value: textBodyValue } }),
          ...(htmlBodyValue && { html: { value: htmlBodyValue } }),
        };
      }
    
      // Atomic create + destroy in a single Email/set call
      const request: JmapRequest = {
        using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
        methodCalls: [
          ['Email/set', {
            accountId: session.accountId,
            create: { draft: emailObject },
            destroy: [emailId],
          }, 'updateDraft']
        ]
      };
    
      const response = await this.makeRequest(request);
      const result = this.getMethodResult(response, 0);
    
      if (result.notCreated?.draft) {
        const err = result.notCreated.draft;
        throw new Error(`Failed to create updated draft: ${err.type}${err.description ? ' - ' + err.description : ''}`);
      }
    
      const newEmailId = result.created?.draft?.id;
      if (!newEmailId) {
        throw new Error('Draft update returned no email ID');
      }
    
      return newEmailId;
    }
  • src/index.ts:349-398 (registration)
    Registration of the 'edit_draft' tool in the ListToolsRequestSchema handler, including its full inputSchema definition and description.
    {
      name: 'edit_draft',
      description: 'Edit an existing draft email. Since JMAP emails are immutable, this atomically destroys the old draft and creates a new one with the updated fields. Only fields you provide will be changed; others are preserved from the original draft.',
      inputSchema: {
        type: 'object',
        properties: {
          emailId: {
            type: 'string',
            description: 'The ID of the draft email to edit',
          },
          to: {
            type: 'array',
            items: { type: 'string' },
            description: 'Updated recipient email addresses (optional, keeps existing if omitted)',
          },
          cc: {
            type: 'array',
            items: { type: 'string' },
            description: 'Updated CC email addresses (optional)',
          },
          bcc: {
            type: 'array',
            items: { type: 'string' },
            description: 'Updated BCC email addresses (optional)',
          },
          from: {
            type: 'string',
            description: 'Updated sender email address (optional)',
          },
          subject: {
            type: 'string',
            description: 'Updated email subject (optional)',
          },
          textBody: {
            type: 'string',
            description: 'Updated plain text body (optional)',
          },
          htmlBody: {
            type: 'string',
            description: 'Updated HTML body (optional)',
          },
          replyTo: {
            type: 'array',
            items: { type: 'string' },
            description: 'Reply-To email addresses (replies go here instead of to the sender)',
          },
        },
        required: ['emailId'],
      },
    },
  • Input schema for the 'edit_draft' tool, defining required emailId and optional fields: to, cc, bcc, from, subject, textBody, htmlBody, replyTo.
    inputSchema: {
      type: 'object',
      properties: {
        emailId: {
          type: 'string',
          description: 'The ID of the draft email to edit',
        },
        to: {
          type: 'array',
          items: { type: 'string' },
          description: 'Updated recipient email addresses (optional, keeps existing if omitted)',
        },
        cc: {
          type: 'array',
          items: { type: 'string' },
          description: 'Updated CC email addresses (optional)',
        },
        bcc: {
          type: 'array',
          items: { type: 'string' },
          description: 'Updated BCC email addresses (optional)',
        },
        from: {
          type: 'string',
          description: 'Updated sender email address (optional)',
        },
        subject: {
          type: 'string',
          description: 'Updated email subject (optional)',
        },
        textBody: {
          type: 'string',
          description: 'Updated plain text body (optional)',
        },
        htmlBody: {
          type: 'string',
          description: 'Updated HTML body (optional)',
        },
        replyTo: {
          type: 'array',
          items: { type: 'string' },
          description: 'Reply-To email addresses (replies go here instead of to the sender)',
        },
      },
      required: ['emailId'],
    },
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses critical behavioral traits: it atomically destroys the old draft and creates a new one, and fields not provided are preserved. Since no annotations exist, the description fully carries the burden of transparency, and does so effectively.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences: first states the purpose, second adds crucial behavioral detail. No extraneous information; every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the task and behavioral nuances well, but it omits any mention of the return value (e.g., the new draft ID or object). Since there is no output schema, this information would aid completeness for an agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

All 9 parameters have descriptions in the input schema (100% coverage). The description adds value by explaining the merge behavior: 'Only fields you provide will be changed; others are preserved from the original draft.' This clarifies how optional parameters interact.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool edits an existing draft email, specifying the verb 'edit' and the resource 'draft email'. It distinguishes from siblings like 'create_draft' by noting immutability and atomic destroy+create behavior.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context by explaining that only provided fields change and others are preserved. However, it does not explicitly state when to use this tool vs alternatives like 'create_draft' or 'send_draft', nor mention any prerequisites or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MadLlama25/fastmail-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server