create_draft
Create an email draft without sending it. Supports threading headers for replies.
Instructions
Create an email draft without sending it. Supports threading headers for replies. IMPORTANT: each call creates a new draft — do not call twice for the same message.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | Recipient email addresses (optional) | |
| cc | No | CC email addresses (optional) | |
| bcc | No | BCC email addresses (optional) | |
| from | No | Sender email address (optional, defaults to account primary email) | |
| mailboxId | No | Mailbox ID to save the draft to (optional, defaults to Drafts folder) | |
| subject | No | Email subject (optional) | |
| textBody | No | Plain text body (optional) | |
| htmlBody | No | HTML body (optional) | |
| inReplyTo | No | Message-IDs to reply to (optional, for threading) | |
| references | No | Message-IDs for References header (optional, for threading) | |
| replyTo | No | Reply-To email addresses (replies go here instead of to the sender) |
Implementation Reference
- src/index.ts:293-351 (registration)Tool registration for 'create_draft' in the ListTools handler: defines name, description, and inputSchema (to, cc, bcc, from, mailboxId, subject, textBody, htmlBody, inReplyTo, references, replyTo).
{ name: 'create_draft', description: 'Create an email draft without sending it. Supports threading headers for replies. IMPORTANT: each call creates a new draft — do not call twice for the same message.', inputSchema: { type: 'object', properties: { to: { type: 'array', items: { type: 'string' }, description: 'Recipient email addresses (optional)', }, cc: { type: 'array', items: { type: 'string' }, description: 'CC email addresses (optional)', }, bcc: { type: 'array', items: { type: 'string' }, description: 'BCC email addresses (optional)', }, from: { type: 'string', description: 'Sender email address (optional, defaults to account primary email)', }, mailboxId: { type: 'string', description: 'Mailbox ID to save the draft to (optional, defaults to Drafts folder)', }, subject: { type: 'string', description: 'Email subject (optional)', }, textBody: { type: 'string', description: 'Plain text body (optional)', }, htmlBody: { type: 'string', description: 'HTML body (optional)', }, inReplyTo: { type: 'array', items: { type: 'string' }, description: 'Message-IDs to reply to (optional, for threading)', }, references: { type: 'array', items: { type: 'string' }, description: 'Message-IDs for References header (optional, for threading)', }, replyTo: { type: 'array', items: { type: 'string' }, description: 'Reply-To email addresses (replies go here instead of to the sender)', }, }, }, }, - src/index.ts:1157-1193 (handler)Main handler for 'create_draft' tool call in the CallToolRequestSchema switch: extracts args, validates at least one field, calls client.createDraft(), and returns the draft email ID with summary.
case 'create_draft': { const { to, cc, bcc, from, mailboxId, subject, textBody, htmlBody, inReplyTo, references, replyTo } = args as any; if (!to?.length && !subject && !textBody && !htmlBody) { throw new McpError(ErrorCode.InvalidParams, 'At least one of to, subject, textBody, or htmlBody must be provided'); } const emailId = await client.createDraft({ to, cc, bcc, from, mailboxId, subject, textBody, htmlBody, inReplyTo, references, replyTo, }); const summary = [ `Draft created successfully (Email ID: ${emailId}).`, subject ? `Subject: ${subject}` : null, to?.length ? `To: ${to.join(', ')}` : null, cc?.length ? `CC: ${cc.join(', ')}` : null, ].filter(Boolean).join(' '); return { content: [ { type: 'text', text: summary, }, ], }; } - src/jmap-client.ts:379-482 (handler)JmapClient.createDraft() - the core implementation that builds and sends a JMAP Email/set request with $draft keyword to create an email draft without sending it.
async createDraft(email: { to?: string[]; cc?: string[]; bcc?: string[]; subject?: string; textBody?: string; htmlBody?: string; from?: string; mailboxId?: string; inReplyTo?: string[]; references?: string[]; replyTo?: string[]; }): Promise<string> { const session = await this.getSession(); // Validate at least one meaningful field is present if (!email.to?.length && !email.subject && !email.textBody && !email.htmlBody) { throw new Error('At least one of to, subject, textBody, or htmlBody must be provided'); } // Get all identities to resolve from address const identities = await this.getIdentities(); if (!identities || identities.length === 0) { throw new Error('No sending identities found'); } let selectedIdentity; if (email.from) { selectedIdentity = identities.find(id => matchesIdentity(id.email, email.from!)); if (!selectedIdentity) { throw new Error('From address is not verified for sending. Choose one of your verified identities.'); } } else { selectedIdentity = identities.find(id => id.mayDelete === false) || identities[0]; } const fromEmail = email.from || selectedIdentity.email; // Resolve drafts mailbox let draftMailboxId: string; if (email.mailboxId) { draftMailboxId = email.mailboxId; } else { const mailboxes = await this.getMailboxes(); const draftsMailbox = this.findMailboxByRoleOrName(mailboxes, 'drafts', 'draft'); if (!draftsMailbox) { throw new Error('Could not find Drafts mailbox'); } draftMailboxId = draftsMailbox.id; } const mailboxIds: Record<string, boolean> = {}; mailboxIds[draftMailboxId] = true; const emailObject: any = { mailboxIds, keywords: { $draft: true }, from: [{ email: fromEmail }], }; if (email.to?.length) emailObject.to = email.to.map(addr => ({ email: addr })); if (email.cc?.length) emailObject.cc = email.cc.map(addr => ({ email: addr })); if (email.bcc?.length) emailObject.bcc = email.bcc.map(addr => ({ email: addr })); if (email.subject) emailObject.subject = email.subject; if (email.inReplyTo?.length) emailObject.inReplyTo = email.inReplyTo; if (email.references?.length) emailObject.references = email.references; if (email.replyTo?.length) emailObject.replyTo = email.replyTo.map(addr => ({ email: addr })); if (email.textBody) emailObject.textBody = [{ partId: 'text', type: 'text/plain' }]; if (email.htmlBody) emailObject.htmlBody = [{ partId: 'html', type: 'text/html' }]; if (email.textBody || email.htmlBody) { emailObject.bodyValues = { ...(email.textBody && { text: { value: email.textBody } }), ...(email.htmlBody && { html: { value: email.htmlBody } }) }; } const request: JmapRequest = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/set', { accountId: session.accountId, create: { draft: emailObject } }, 'createDraft'] ] }; const response = await this.makeRequest(request); const result = this.getMethodResult(response, 0); // Propagate server-provided error details from notCreated if (result.notCreated?.draft) { const err = result.notCreated.draft; throw new Error(`Failed to create draft: ${err.type}${err.description ? ' - ' + err.description : ''}`); } // Throw if created ID is missing instead of returning silently const emailId = result.created?.draft?.id; if (!emailId) { throw new Error('Draft creation returned no email ID'); } return emailId; } - src/index.ts:1132-1134 (helper)The 'reply_email' handler also uses createDraft when send=false, reusing the same JmapClient.createDraft() method.
if (!shouldSend) { const emailId = await client.createDraft(replyParams);