Todo lo que hace un cliente MCP, un script puede hacerlo con HTTPS normal. Un endpoint, una clave de API, JSON de entrada y JSON de salida. Esta guía cubre la autenticación, las tres llamadas que necesitas, los errores, los reintentos, los límites y los webhooks para el correo nuevo.
Cada solicitud es un POST JSON-RPC 2.0 a https://mcpemails.com/api/mcp. Es el mismo endpoint que usan claude.ai, ChatGPT y otros clientes MCP, con las mismas herramientas y los mismos permisos, así que un script y un asistente de chat ven un único producto.
Content-Type: application/json y Accept: application/json. La respuesta es un único objeto JSON.Authorization.structuredContent tipado, así que nunca tienes que interpretar prosa. Los mismos datos están en content[0].text como cadena JSON.mcpe_ seguido de 64 caracteres hexadecimales. Se muestra una sola vez, al crearla, y solo se guarda un hash. Cópiala entonces; una clave perdida no se puede recuperar, solo sustituir.Authorization: Bearer mcpe_.... Nunca pongas una clave en una URL ni en una query string.Cada clave tiene solo los permisos que marcas al crearla. Puedes cambiarlos después con Editar acceso, y el cambio se aplica en la siguiente llamada sin volver a conectar.
| Ámbito | Permite |
|---|---|
read:email | Listar, leer y buscar mensajes, descargar adjuntos, listar buzones y carpetas. |
search:email | Se conserva para claves antiguas. La búsqueda está cubierta por read:email. |
send:email | Enviar, responder y reenviar. |
manage:drafts | Crear, actualizar y eliminar borradores. Enviar un borrador también necesita send:email. |
manage:folders | Mover, copiar, marcar y archivar mensajes; crear, renombrar y eliminar carpetas. |
delete:email | Eliminar mensajes, uno a uno o en bloque. |
manage:contacts | Buscar contactos en tu correo reciente. |
schedule:email | Programar envíos y gestionar los correos programados. |
manage:automations | Crear reglas que se ejecutan según un horario sin que nadie las vigile. |
Empieza con read:email y añade un ámbito de escritura solo cuando el flujo de trabajo lo necesite. Una llamada fuera de los ámbitos de la clave se rechaza antes de contactar con tu servidor de correo. Los lectores del espacio de trabajo solo pueden tener ámbitos de lectura.
Por defecto una clave llega a todos los buzones del espacio de trabajo. Para fijarla a algunos, abre Editar acceso en la clave y elige los buzones en Acceso a buzones. El límite se comprueba en cada llamada, así que una clave fijada al buzón de soporte no puede leer el de finanzas, diga lo que diga la solicitud.
Las apps que la gente conecta por su cuenta, como claude.ai, ChatGPT y otros clientes MCP, inician sesión con OAuth en lugar de con una clave: registro dinámico de cliente, código de autorización con PKCE y una pantalla de consentimiento donde el usuario elige los ámbitos y los buzones. Consulta Conexión OAuth. Para tus propios scripts, servidores y flujos de trabajo, una clave de API es más sencilla.
Los clientes MCP empiezan con initialize. Un script puede saltárselo, porque el servidor no mantiene sesión, pero es una forma barata de comprobar que la clave funciona y de leer las instrucciones del servidor.
curl -s https://mcpemails.com/api/mcp \
-H "Authorization: Bearer $MCPEMAILS_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"clientInfo": { "name": "my-agent", "version": "1.0" },
"capabilities": {}
}
}'tools/list devuelve todas las herramientas que esta clave puede llamar, con su descripción, su esquema de entrada y su esquema de salida. Léelo al arrancar en lugar de fijar nombres de herramientas o argumentos en el código: la lista se filtra por los ámbitos de la clave, y las herramientas ganan argumentos con el tiempo.
curl -s https://mcpemails.com/api/mcp \
-H "Authorization: Bearer $MCPEMAILS_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }'Llama a una herramienta por su nombre con sus argumentos. La mayoría aceptan una action que decide qué hacen, como list, read o search en email_read. Indica el buzón con inbox (su dirección) o inbox_id; inbox_list devuelve ambos.
curl -s https://mcpemails.com/api/mcp \
-H "Authorization: Bearer $MCPEMAILS_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "email_read",
"arguments": { "action": "list", "inbox": "you@example.com", "unread": true, "limit": 20 }
}
}'Un resultado correcto:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "{\"messages\":[...],\"total\":42, ...}" }],
"structuredContent": {
"messages": [{ "id": "...", "subject": "...", "from": { "name": "...", "email": "..." } }],
"total": 42,
"total_is_estimate": false,
"has_more": true,
"next_offset": 20
}
}
}La referencia de herramientas describe cada herramienta, y la guía de curl tiene más ejemplos.
Hay dos tipos. Una solicitud que no pudo ejecutarse en absoluto vuelve como un error JSON-RPC con un estado HTTP. Una solicitud que llegó a una herramienta y fue rechazada por ella vuelve como un result normal con isError: true, para que un modelo de IA lea el motivo. Ramifica según data.error_code o el código de _meta, no según el texto del mensaje.
| Respuesta | Cuándo | Qué hacer |
|---|---|---|
HTTP 401, -32001 | Clave ausente, mal formada, revocada o caducada. | Corrige la cabecera Authorization o crea una clave nueva. Reintentar no ayudará. |
HTTP 403, -32004, insufficient_scope | A la clave le falta un ámbito que esta acción de la herramienta necesita. data.required_scopes lista lo que la permitiría. | Añade el ámbito con Editar acceso, o usa otra clave. |
HTTP 429, -32003, rate_limit_exceeded | Demasiadas solicitudes. data.window indica qué ventana se llenó. | Espera Retry-After segundos (también en data.retry_after) y repite la misma solicitud. |
-32602 | Nombre de herramienta desconocido. data.available_tools lista los nombres que esta clave puede llamar. | Usa un nombre de tools/list. |
isError: true, invalid_arguments | Los argumentos no pasaron el esquema de la herramienta. No se ejecutó nada. El texto nombra cada argumento incorrecto y, cuando un nombre de argumento fue adivinado, indica el que existe. | Corrige los argumentos. Los mismos argumentos se rechazan de la misma forma. |
isError: true, usage_limit_reached | Un espacio de trabajo Gratis ha agotado sus acciones de correo del mes. | Espera a la fecha de reinicio en _meta, o mejora de plan. Los planes de pago no tienen tope mensual. |
isError: true, otro | La herramienta se ejecutó y falló: mensaje no encontrado, el proveedor lo rechazó, destinatario inválido, etcétera. | Lee content[0].text. Dice qué pasó y si se envió algo. |
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", ...
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32004,
"message": "Insufficient scope: the 'send:email' scope is required to call email_compose.",
"data": { "error_code": "insufficient_scope", "required_scopes": ["send:email"], ... }
}
}HTTP/1.1 429 Too Many Requests
Retry-After: 12
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32003,
"message": "Rate limit exceeded",
"data": { "error_code": "rate_limit_exceeded", "window": "per_minute", "retry_after": 12, ... }
}
}{
"jsonrpc": "2.0",
"id": 6,
"result": {
"isError": true,
"content": [{ "type": "text", "text": "Invalid arguments for email_read. The call was rejected before it ran, ..." }],
"_meta": {
"com.mcpemails/invalid_arguments": {
"error_code": "invalid_arguments",
"retryable": false,
"errors": [{ "path": "arguments.since", "message": "..." }]
}
}
}
}Todo resultado de lista o búsqueda termina con los mismos cuatro campos:
has_more: true cuando existe otra página. Solo has_more: false significa que lo has visto todo.next_offset: pásalo de vuelta como offset, con todos los demás argumentos sin cambios, para obtener la página siguiente. Es null cuando has_more es false.total: el número de coincidencias, o null cuando el proveedor no puede contarlas. Nunca contradice la página que tienes.total_is_estimate: true cuando total es una estimación o un mínimo y no un recuento exacto.Los cuerpos de mensaje largos se trocean de la misma manera: cuando body_truncated es true, vuelve a leer con body_next_offset como body_offset.
Una conexión que se corta tras un envío te deja sin saber si el correo salió. Pasa una idempotency_key y reintenta con la misma clave: el trabajo se ejecuta una sola vez.
curl -s https://mcpemails.com/api/mcp \
-H "Authorization: Bearer $MCPEMAILS_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "email_compose",
"arguments": {
"action": "send",
"inbox": "you@example.com",
"to": ["customer@example.com"],
"subject": "Your order has shipped",
"body": "Hi, your order is on its way.",
"idempotency_key": "order-1042-shipped"
}
}
}'tools/list muestra idempotency_key en el esquema de entrada de cada herramienta que la acepta.idempotent_replay: true, el status original y el resultado original en result, incluido el message_id que quizá perdiste.idempotency_key_conflict. Usa una clave nueva para una operación nueva.status: "processing". Reintenta en breve.status: "unknown". Revisa Enviados antes de enviar otra vez con una clave nueva.{
"idempotency_key": "order-1042-shipped",
"idempotent_replay": true,
"status": "succeeded",
"result": { "message_id": "...", "inbox_id": "...", "status": "sent" },
"message": "This logical request was already processed. ..."
}Cualquier buzón puede retener todos los envíos hasta que una persona los apruebe. Un envío desde tu script devuelve entonces un approval_id con status: "pending_approval" en lugar de salir, y sale cuando alguien lo aprueba en el panel. Funciona en todos los planes y es un buen valor por defecto mientras una automatización nueva demuestra lo que vale. Un reintento con la misma idempotency_key informa de la aprobación en lugar de crear una segunda.
Cada plan tiene un tope por minuto para todo el espacio de trabajo. Una sola clave puede usar el límite por minuto completo del plan; varias claves lo comparten.
| Plan | Solicitudes por minuto | Webhooks |
|---|---|---|
| Free | 60 | No |
| Personal | 120 | No |
| Pro | 300 | Sí |
| Business | 1000 | Sí |
tools/call llevan RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset (en segundos) para la ventana más cercana a llenarse, de modo que un cliente puede regular su ritmo antes de alcanzar el límite.data.window qué ventana se llenó.Retry-After en segundos. Espera ese tiempo y repite la misma solicitud.En Pro y Business, MCPEmails puede hacer un POST a tu URL cuando llega correo a un buzón, para que un flujo de trabajo arranque con el correo nuevo en lugar de consultar periódicamente. Crea un endpoint en el panel, elige sus buzones y copia el secreto de firma, que se muestra una sola vez. La URL debe ser HTTPS, en el puerto 443 u 8443. Usa el botón de prueba para enviar un evento ping firmado.
POST /your/webhook HTTP/1.1
Content-Type: application/json
MCPEmails-Event-Id: evt_...
MCPEmails-Event-Type: message.received
MCPEmails-Signature: t=1792843200,v1=5f2b...
{
"id": "evt_...",
"type": "message.received",
"created_at": "2026-10-24T09:20:00.000Z",
"workspace_id": "...",
"inbox_id": "...",
"folder": "INBOX",
"count": 1,
"message_ids": ["..."]
}type es message.received. message_ids lista los mensajes nuevos de la carpeta INBOX de ese buzón, hasta 10 por evento; count es cuántos llegaron, que pueden ser más.messages con el id, from y subject de cada mensaje.email_read (action: "read", el inbox_id y un message_id) usando una clave de API. Los ámbitos y el acceso a buzones de esa clave se aplican como siempre.Cada entrega lleva MCPEmails-Signature: t=<segundos unix>,v1=<hex>, donde v1 es el HMAC-SHA256 de la marca de tiempo, un punto y el cuerpo crudo de la solicitud, con el secreto del endpoint como clave. La clave es el secreto completo tal como se muestra, prefijo whsec_ incluido. Recalcula la firma sobre el cuerpo crudo, compara en tiempo constante y rechaza una marca de tiempo de más de cinco minutos.
import crypto from 'node:crypto';
// rawBody: the request body exactly as received, before JSON.parse.
export function verifyMcpemailsSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
String(header ?? '').split(',').map((part) => part.trim().split('=')),
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest();
const received = Buffer.from(parts.v1 ?? '', 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}MCPEmails-Event-Id para ignorar una entrega que ya has procesado. Un reintento lleva el mismo id de evento.POST, y registra su URL de producción como endpoint de webhook en el panel de MCPEmails.POST, URL https://mcpemails.com/api/mcp, autenticación Generic Credential Type → Header Auth con nombre Authorization y valor Bearer mcpe_....result.structuredContent.NODE_FUNCTION_ALLOW_BUILTIN=crypto.{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "email_read",
"arguments": {
"action": "read",
"inbox_id": "{{ $json.body.inbox_id }}",
"message_id": "{{ $json.body.message_ids[0] }}"
}
}
}Para un nodo AI Agent que lea y envíe correo por sí mismo, consulta la guía de n8n.
Añade el servidor con una clave en una cabecera. Es lo adecuado para máquinas sin interfaz y CI, donde no hay navegador para el inicio de sesión OAuth:
claude mcp add --transport http mcpemails https://mcpemails.com/api/mcp \
--header "Authorization: Bearer $MCPEMAILS_KEY"La guía de Claude Code cubre el inicio de sesión OAuth y la elección de ámbitos.
Lista todos los mensajes no leídos de un buzón, página a página, y espera a que pase un 429:
// node agent.mjs (Node 18 or later, MCPEMAILS_KEY in the environment)
const ENDPOINT = 'https://mcpemails.com/api/mcp';
let nextId = 1;
async function callTool(name, args) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MCPEMAILS_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ jsonrpc: '2.0', id: nextId++, method: 'tools/call', params: { name, arguments: args } }),
});
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? 1);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
return callTool(name, args);
}
const reply = await res.json();
if (reply.error) throw new Error(`${reply.error.code}: ${reply.error.message}`);
if (reply.result.isError) throw new Error(reply.result.content[0].text);
return reply.result.structuredContent;
}
const inbox = 'you@example.com';
let offset = 0;
do {
const page = await callTool('email_read', { action: 'list', inbox, unread: true, limit: 50, offset });
for (const m of page.messages) console.log(m.id, m.from?.email, m.subject);
offset = page.has_more ? page.next_offset : null;
} while (offset !== null);OpenClaw inicia sesión con OAuth por defecto; la guía de OpenClaw lo explica paso a paso. Para usar en su lugar una clave con ámbitos, ponla en el mapa headers de la entrada del servidor y deja auth sin definir, porque OpenClaw ignora una cabecera Authorization estática en una entrada configurada con OAuth.
{
"mcp": {
"servers": {
"mcpemails": {
"url": "https://mcpemails.com/api/mcp",
"transport": "streamable-http",
"headers": { "Authorization": "Bearer mcpe_YOUR_KEY_HERE" }
}
}
}
}Crea una clave, llama a tools/list y mira cómo tu propio correo vuelve como JSON.