Wire MultiMail's REST API as Claude tool calls. Send, read, and approve emails with structured inputs, gated oversight modes, and a complete audit trail below the model layer.
The Anthropic SDK exposes Claude's tool use interface as a first-class API primitive. You define tools as JSON schemas, Claude decides when to call them, and your application executes the calls and feeds results back. This loop maps directly onto email operations: send_email, check_inbox, read_email, reply_email, and get_thread all fit naturally as tool definitions.
What the SDK does not provide is enforcement policy. When Claude calls send_email, nothing in the SDK validates the recipient, checks whether a human approved the send, or records the decision for audit. MultiMail adds those controls below the model layer — as a REST API your tool executor calls — so Claude's email capability is governed regardless of what the model decides.
The integration is additive. You keep your existing messages loop, system prompt, and tool definitions. MultiMail replaces the outbound email call inside your tool executor with one that enforces the oversight_mode you configured for that agent mailbox.
Claude decides when to send email. MultiMail enforces who it can send to, whether a human must approve, and what gets logged. These controls live in the API layer, not in the system prompt, so they cannot be overridden by prompt injection or model drift.
Configure gated_send so every outbound send waits for human approval, or switch to autonomous once the agent has earned trust. The same agent code runs in all modes — only the mailbox configuration changes.
MultiMail's REST endpoints accept the same JSON fields you define in Claude's tool input_schema. No translation layer needed — the object Claude generates goes directly into the request body.
check_inbox and read_email return structured JSON that fits cleanly as tool_result content. Claude can parse subject, sender, body, and thread_id without custom parsing logic in your application.
Every send_email, reply_email, and decide_email call is logged with the originating agent, timestamp, recipients, and approval state. Required for SOC 2 and GDPR Article 5 accountability obligations.
import Anthropic from '@anthropic-ai/sdk';
export const emailTools: Anthropic.Tool[] = [
{
name: 'send_email',
description: 'Send an email from the agent mailbox. Returns pending status in gated_send mode.',
input_schema: {
type: 'object' as const,
properties: {
to: { type: 'string', description: 'Recipient email address' },
subject: { type: 'string', description: 'Email subject line' },
body: { type: 'string', description: 'Plain text email body' },
reply_to_id: { type: 'string', description: 'Email ID to thread this as a reply (optional)' }
},
required: ['to', 'subject', 'body']
}
},
{
name: 'check_inbox',
description: 'List recent emails in the agent mailbox.',
input_schema: {
type: 'object' as const,
properties: {
limit: { type: 'number', description: 'Max emails to return (default 20)' },
unread_only: { type: 'boolean', description: 'Return only unread emails' }
}
}
},
{
name: 'read_email',
description: 'Read the full content of a specific email by ID.',
input_schema: {
type: 'object' as const,
properties: {
email_id: { type: 'string', description: 'Email ID from check_inbox' }
},
required: ['email_id']
}
}
];
Register send_email, check_inbox, and read_email as Claude tool definitions with input schemas that match MultiMail's REST API parameters.
import Anthropic from '@anthropic-ai/sdk';
import { emailTools } from './tools';
const client = new Anthropic();
async function callMultiMail(
toolName: string,
input: Record<string, unknown>
): Promise<unknown> {
const res = await fetch(`https:"cm">//api.multimail.dev/${toolName}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(input)
});
if (!res.ok) {
const err = await res.json() as { error: string };
throw new Error(`MultiMail ${toolName} failed: ${err.error}`);
}
return res.json();
}
async function runEmailAgent(task: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: task }
];
while (true) {
const response = await client.messages.create({
model: 'claude-opus-4-7',
max_tokens: 4096,
system: 'You are an email assistant. Use tools to manage the inbox and send emails on behalf of the user.',
tools: emailTools,
messages
});
if (response.stop_reason === 'end_turn') {
const textBlock = response.content.find(b => b.type === 'text');
return textBlock?.type === 'text' ? textBlock.text : '';
}
if (response.stop_reason !== 'tool_use') break;
messages.push({ role: 'assistant', content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = await Promise.all(
response.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use')
.map(async (block) => {
try {
const result = await callMultiMail(
block.name,
block.input as Record<string, unknown>
);
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: JSON.stringify(result)
};
} catch (err) {
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: String(err),
is_error: true
};
}
})
);
messages.push({ role: 'user', content: toolResults });
}
return '';
}
Run a full Claude tool use loop that dispatches tool calls to MultiMail's REST API and feeds structured responses back to the model.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
interface PendingEmail {
id: string;
to: string;
subject: string;
body: string;
created_at: string;
}
async function mm(endpoint: string, body: Record<string, unknown>) {
const res = await fetch(`https:"cm">//api.multimail.dev/${endpoint}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
return res.json();
}
async function reviewAndDecide(email: PendingEmail): Promise<void> {
const response = await client.messages.create({
model: 'claude-opus-4-7',
max_tokens: 512,
system:
'You are a compliance reviewer. Approve professional, on-topic emails. ' +
'Reject anything that looks like spam, contains sensitive PII, or would violate CAN-SPAM. ' +
'Respond with JSON only: {"decision": "approve" | "reject", "reason": string}',
messages: [
{
role: 'user',
content:
`To: ${email.to}\nSubject: ${email.subject}\n\n${email.body}`
}
]
});
const text =
response.content.find(b => b.type === 'text')?.text ?? '{}';
const { decision, reason } = JSON.parse(text) as {
decision: 'approve' | 'reject';
reason: string;
};
await mm('decide_email', { email_id: email.id, decision, reason });
console.log(`${email.id}: ${decision} — ${reason}`);
}
async function runApprovalLoop(): Promise<void> {
const data = await mm('list_pending', {}) as { pending: PendingEmail[] };
console.log(`${data.pending.length} emails pending review`);
for (const email of data.pending) {
await reviewAndDecide(email);
}
}
runApprovalLoop();
Poll list_pending for held sends, use a second Claude call to review each one, then call decide_email to approve or reject. Pair with gated_send or gated_all oversight modes.
import Anthropic from '@anthropic-ai/sdk';
import { emailTools } from './tools';
const client = new Anthropic();
async function streamEmailAgent(task: string): Promise<void> {
const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: task }
];
while (true) {
const stream = client.messages.stream({
model: 'claude-opus-4-7',
max_tokens: 4096,
tools: emailTools,
messages
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
process.stdout.write(event.delta.text);
}
}
const finalMessage = await stream.finalMessage();
if (finalMessage.stop_reason !== 'tool_use') break;
messages.push({ role: 'assistant', content: finalMessage.content });
const toolResults: Anthropic.ToolResultBlockParam[] = await Promise.all(
finalMessage.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use')
.map(async (block) => {
const result = await fetch(
`https:"cm">//api.multimail.dev/${block.name}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(block.input)
}
).then(r => r.json());
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: JSON.stringify(result)
};
})
);
messages.push({ role: 'user', content: toolResults });
}
}
Stream Claude's reasoning to the user while still executing MultiMail tool calls synchronously when the stream resolves to a tool_use stop reason.
Add the Anthropic TypeScript SDK to your project.
npm install @anthropic-ai/sdkCreate a mailbox for your agent and set the oversight mode. Use gated_send to require human approval on all outbound sends while reads remain autonomous.
curl -X POST https://api.multimail.dev/create_mailbox \
-H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
-H &"cm">#039;Content-Type: application/json' \
-d &"cm">#039;{"address": "[email protected]", "oversight_mode": "gated_send"}'Register MultiMail endpoints as Anthropic tool definitions. The input_schema properties map directly to the JSON fields each endpoint accepts.
const emailTools: Anthropic.Tool[] = [
{
name: &"cm">#039;send_email',
description: &"cm">#039;Send an email from the agent mailbox.',
input_schema: {
type: &"cm">#039;object',
properties: {
to: { type: &"cm">#039;string' },
subject: { type: &"cm">#039;string' },
body: { type: &"cm">#039;string' }
},
required: [&"cm">#039;to', 'subject', 'body']
}
},
{
name: &"cm">#039;check_inbox',
description: &"cm">#039;List recent emails.',
input_schema: { type: &"cm">#039;object', properties: {} }
}
];When stop_reason is 'tool_use', dispatch each tool_use block to the matching MultiMail endpoint and push the results back as tool_result blocks.
if (response.stop_reason === &"cm">#039;tool_use') {
messages.push({ role: &"cm">#039;assistant', content: response.content });
const toolResults = await Promise.all(
response.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === &"cm">#039;tool_use')
.map(async (block) => {
const result = await fetch(
`https://api.multimail.dev/${block.name}`,
{
method: &"cm">#039;POST',
headers: {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
&"cm">#039;Content-Type': 'application/json'
},
body: JSON.stringify(block.input)
}
).then(r => r.json());
return {
type: &"cm">#039;tool_result' as const,
tool_use_id: block.id,
content: JSON.stringify(result)
};
})
);
messages.push({ role: &"cm">#039;user', content: toolResults });
}Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.