# Mastra

Give your Mastra agent an inbox. The TypeScript SDK plugs straight into typed Mastra tools; inbound webhooks become workflow inputs; evals score every reply on `message.sent`.

## Install

```
pnpm add mailmolt @mastra/core
```

## Define typed tools

```
import { createTool } from '@mastra/core';
import { z } from 'zod';
import { MailMolt } from 'mailmolt';

const mm = new MailMolt({ apiKey: process.env.MAILMOLT_API_KEY! });

export const sendEmail = createTool({
  id: 'send-email',
  description: 'Send an email from the MailMolt agent identity',
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    text: z.string(),
  }),
  execute: async ({ context }) => {
    const r = await mm.sendMessage({
      to: context.to,
      subject: context.subject,
      text: context.text,
    });
    return { id: r.id, status: r.status };
  },
});

export const searchInbox = createTool({
  id: 'search-inbox',
  description: 'Semantic search over the agent inbox',
  inputSchema: z.object({ q: z.string(), limit: z.number().default(5) }),
  execute: async ({ context }) => mm.search({ q: context.q, limit: context.limit }),
});

export const replyTo = createTool({
  id: 'reply-to',
  description: 'Reply in-thread to a message',
  inputSchema: z.object({ messageId: z.string(), text: z.string() }),
  execute: async ({ context }) =>
    mm.replyMessage({ message_id: context.messageId, text: context.text }),
});
```

## Wire into an agent

```
import { Agent } from '@mastra/core';
import { anthropic } from '@ai-sdk/anthropic';
import { sendEmail, searchInbox, replyTo } from './email-tools';

export const inboxAgent = new Agent({
  name: 'inbox-agent',
  instructions:
    'You have a MailMolt email identity. Use search-inbox to find context, '
    'reply-to or send-email to respond. Keep replies under 3 sentences '
    'unless asked for more.',
  model: anthropic('claude-sonnet-4-6'),
  tools: { sendEmail, searchInbox, replyTo },
});
```

## Webhook → Mastra workflow

```
import { createWorkflow, createStep } from '@mastra/core';
import { z } from 'zod';

const handleInbound = createStep({
  id: 'handle-inbound',
  inputSchema: z.object({ event: z.any() }),
  execute: async ({ inputData }) => {
    const { event } = inputData;
    if (event.event_type !== 'message.received') return { skipped: true };
    const msg = event.data.message;
    const resp = await inboxAgent.generate(
      `Reply to ${msg.from.email}. Subject: ${msg.subject}\n\n${msg.preview}`,
    );
    return { replied: true, text: resp.text };
  },
});

export const inboundWorkflow = createWorkflow({
  id: 'inbound-email',
  inputSchema: z.object({ event: z.any() }),
}).then(handleInbound).commit();
```

## Register the webhook

```
await mm.createWebhook({
  url: 'https://your-host.com/api/webhooks/mailmolt',
  event_types: ['message.received', 'message.sent'],
});
```

## Evals on message.sent

```
import { evaluate } from '@mastra/evals';

// Score every reply on tone and brevity once it actually leaves the platform.
async function onMessageSent(event: any) {
  const msg = event.data.message;
  await evaluate({
    output: msg.text,
    metrics: ['tone-professional', 'concise'],
    metadata: { messageId: msg.id, threadId: msg.thread_id },
  });
}
```

## Notes

- Mastra's evals slot in cleanly — score reply quality on `message.sent` for closed-loop training data.
- For keyword lookups, prefer `mm.searchThreads()` (`/v1/search/threads`) — semantic search costs more and isn't needed for exact matches.
- Approval-gated agents: `sendMessage` still returns an id, but the message waits in oversight until approved.
- Threads are RFC-correct — `replyTo` sets In-Reply-To/References so Gmail/Outlook stitch threads correctly.

Full cookbook: [docs/integrations/mastra.md](https://github.com/Rakesh1002/mailmolt/blob/main/docs/integrations/mastra.md) · [webhook reference](/content/skill.md#webhooks/index.html)
