Agents SDK has a built-in MCP client. One addMcpServer call and your Durable Object agent has an inbox.
Create a mailbox
One per agent, so its address and its key are the same identity.
Put the key in a secret
wrangler secret put MCPMAILER_KEY, so it is not in your config.
Connect once, not per request
addMcpServer in onStart; the connection is part of the agent state.
Deploy
The Durable Object keeps the connection across requests and hibernation.
npm install agentsawait this.addMcpServer('mcpmailer', 'https://connect.mcpmailer.com/mcp', {
transport: {
type: 'streamable-http',
headers: { Authorization: 'Bearer mmk_live_...' }
}
});Headers go inside transport. Passed at the top level they are silently dropped, and every call reaches us unauthenticated.
import { Agent } from 'agents';
export class SupportAgent extends Agent<Env> {
async onStart() {
await this.addMcpServer('mcpmailer', 'https://connect.mcpmailer.com/mcp', {
transport: {
type: 'streamable-http',
headers: { Authorization: `Bearer ${this.env.MCPMAILER_KEY}` }
}
});
}
async onRequest(request: Request) {
const { tools } = this.getMcpServers();
return Response.json({ tools: tools.length });
}
}The third argument is an options object. An older signature took callbackHost as a bare third argument, so { headers } at the top level is accepted and then ignored.
Calling addMcpServer per request reconnects every time. onStart runs once per Durable Object instance, which is the lifetime you want.
addMcpServer can return state "authenticating" with an authUrl for servers that need OAuth. With a key you get "ready" straight away and never handle the redirect.
The endpoint speaks Streamable HTTP MCP with 67 tools across mail, contacts, notes, and a vault, including send_email, read_message, search_inbox, and wait_for_reply. Errors are structured so Cloudflare Agents SDK can react: a quota rejection includes the reset time.