Skip to main content
Glama
owen-nash

Fastmail MCP Server

by owen-nash

edit_draft

Edit a draft email by updating specific fields like recipients, subject, or body; unchanged fields are preserved from the original draft.

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

  • MCP tool handler for 'edit_draft' — validates emailId, then delegates to JmapClient.updateDraft(), returning the 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)`,
          },
        ],
      };
    }
  • Input schema and tool registration for 'edit_draft' — defines the tool name, description, and input properties (emailId required; optional to, cc, bcc, from, subject, textBody, htmlBody, replyTo).
    {
      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'],
      },
    },
  • src/index.ts:352-401 (registration)
    Tool registration within ListToolsRequestSchema handler — registers 'edit_draft' as an MCP tool with its schema.
    {
      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'],
      },
    },
  • Core implementation: updateDraft() fetches the existing draft, merges provided fields into the original, then atomically creates a new email and destroys 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;
    }
Behavior4/5

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

With no annotations provided, the description carries full burden. It discloses the immutable nature of JMAP emails (atomically destroys and creates), and explains field preservation. It does not detail authorization or rate limits, but sufficiently covers the key mutation behavior.

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?

Three sentences with no wasted words. First sentence states action, second explains immutable behavior, third clarifies field update. Front-loaded with purpose.

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

Completeness4/5

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

Given 9 parameters (all documented) and no output schema, the description is fairly complete. It covers the update behavior and immutability. However, it omits what is returned (presumably the new draft) and does not mention error scenarios, leaving slight gaps.

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?

Schema coverage is 100%, so baseline is 3. The description adds value by explaining that only provided fields are changed and others preserved, reinforcing the schema's 'optional, keeps existing' notes. The atomic destruction-creation context further enhances parameter understanding.

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, distinguishing it from siblings like create_draft. It explicitly mentions the immutable behavior (destroy old, create new), which adds specific purpose beyond the name.

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

Usage Guidelines4/5

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

The description indicates when to use this tool (to edit an existing draft) and explains that only provided fields change. It does not explicitly state when not to use it or mention alternatives, but context from sibling tools and the 'edit' verb provide sufficient clarity.

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/owen-nash/fastmail-mcp'

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