Shoot Email
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Shoot Emailshow me the last 5 emails in my mailbox"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Shoot Email
A real email inbox for AI agents.
Current AI assistants can help read email and generate a polished reply, but many connected-email workflows still stop at the draft. A human has to send the message, watch for a response, decide what should happen next, and keep the conversation moving manually.
Shoot Email explores a different question: what would email look like if it were designed from the ground up for AI agents instead of human inboxes?
Shoot Email is an LLM-first email service and MCP server that gives ChatGPT and other AI agents a persistent mailbox. An agent can send text email, receive replies through webhooks, retrieve complete messages, preserve conversation history, and explicitly acknowledge the messages it has successfully handled. The same service contract is available through MCP and a terminal CLI.
Demo

Watch the Shoot Email demo on YouTube
Related MCP server: email-mcp
Why Agent-Native Email
Traditional email clients optimize for a person scanning folders, opening one message at a time, and marking messages read. Shoot Email optimizes for an LLM that needs bounded context, reliable retries, and explicit state transitions:
Full message bodies are returned in bounded batches so an agent can summarize and act without making a second call for every email.
Retrieval and acknowledgement are separate, so a failed model invocation does not silently lose work.
Inbound email is labeled as untrusted external content so email text cannot silently become authorization for additional tool calls.
Outbound requests are idempotent, preventing an agent retry from sending the same message twice.
Stable mailbox identities and message history persist beyond one chat session.
Configurable quotas, recipient limits, reserved aliases, and a global kill switch protect the shared sending domain from obvious abuse.
The result is not another inbox UI. It is a small, structured communication layer that an AI agent can reason over and operate safely.
What Works Today
The project currently includes:
A local CLI and a 10-tool MCP interface over the same versioned service contract.
Stable generated mailbox aliases with optional registered custom aliases.
Real inbound delivery through Cloudflare Email Routing and an Email Worker.
Text-only outbound delivery through Cloudflare Email Sending, with a mock provider for safe local and judge testing.
PostgreSQL persistence with pgvector enabled for future semantic search.
A hosted Cloudflare Worker connected to Neon through Hyperdrive.
Idempotent inbound ingestion, idempotent outbound requests, message history, bounded inbox retrieval, and explicit acknowledgement.
Database-backed integration coverage for the CLI, MCP transports, identity, message lifecycle, provider behavior, and abuse controls.
A restricted hosted Streamable HTTP MCP demo with isolated synthetic mailboxes and real outbound delivery disabled.
Architecture
ChatGPT / Codex / another MCP client
|
v
Shoot Email MCP
|
v
Cloudflare backend Worker ---- Hyperdrive ---- Neon Postgres
|
+------+------+
| |
v v
Email Sending inbound webhook
^
|
Cloudflare Email Worker
^
|
Email RoutingThe production path uses Cloudflare for DNS, routing, edge processing, and outbound delivery, while Neon stores identities, sessions, messages, delivery state, and future embeddings. The hosted judge demo exercises the real remote MCP and database path but deliberately uses the mock provider and disables real outbound delivery.
Hosted Demo
The judge demo is available at:
https://shoot-email-backend.powersurge.workers.dev/mcpEach judge receives an isolated synthetic mailbox containing three landlord
replies for the apartment-hunting demo. The bearer secret is supplied privately
with the submission. See
docs/hackathon/TESTING.md for the exact setup,
black-box prompts, and security boundary.
Supported Platforms
The hosted MCP works with clients that support Streamable HTTP MCP and bearer tokens, including Codex.
Local CLI and MCP development are tested with Node.js, npm, Docker Desktop, and a POSIX-compatible shell on macOS.
The application code is portable to Linux, but the documented local workflow has not yet been independently tested on Linux or Windows.
Setup
Install dependencies:
npm installCreate environment config:
cp .env.example .envStart Postgres with pgvector:
docker compose up -d postgresIf your Docker install does not include Compose, run an equivalent container:
docker run --name shoot-email-postgres \
-e POSTGRES_USER=shoot_email \
-e POSTGRES_PASSWORD=shoot_email \
-e POSTGRES_DB=shoot_email \
-p 5432:5432 \
-d pgvector/pgvector:pg16Apply migrations:
npm run db:migrateReset the local database and reapply all migrations:
npm run db:resetThis drops and recreates the local public schema, so only use it for local development data.
Hosted Backend
The selected hosted architecture is Cloudflare Workers plus Neon Postgres,
connected through Cloudflare Hyperdrive. See
docs/adr/001-cloudflare-workers-neon-hyperdrive.md for the decision and
tradeoffs.
The backend Worker currently exposes:
GET /health
GET /ready
POST /webhooks/email/inbound
POST /mcp/health verifies the Worker is running. /ready also verifies the Hyperdrive
database path. The local-only /apps/openai/context route is hidden on the
Worker unless ENABLE_UNAUTHENTICATED_OPENAI_CONTEXT=true; never enable that
flag in a public environment.
Build the Worker bundle without deploying:
npm run backend:buildBefore deployment, create Neon Postgres, run npm run db:migrate with its
direct DATABASE_URL, and create a Hyperdrive configuration with SQL query
caching disabled. Add the resulting binding to
workers/backend/wrangler.jsonc as HYPERDRIVE. Store the webhook token with
wrangler secret put INBOUND_WEBHOOK_TOKEN; do not add it to the Wrangler
configuration file.
The application reads its least-privilege Email Sending token from
CLOUDFLARE_EMAIL_API_TOKEN. Do not name that application secret
CLOUDFLARE_API_TOKEN, because Wrangler reserves the generic name for its own
deployment authentication.
The staging backend is deployed at:
https://shoot-email-backend.powersurge.workers.devIt uses the shoot-email Neon project in AWS Oregon through the
shoot-email-neon Hyperdrive configuration. Hyperdrive query caching is
disabled and its origin connection limit is five. Staging keeps
MAIL_PROVIDER=mock and OUTBOUND_SENDING_ENABLED=false. The hosted demo may
exercise the outbound contract through simulated mock sends; the disabled flag
still prevents real-provider delivery.
The /mcp route is a restricted Build Week judge demo protected by a bearer
credential. It maps each credential suffix to an isolated, server-controlled
principal and seeds three synthetic landlord replies on first initialization. It never
trusts caller-supplied _meta identity. This is not the production ChatGPT
authentication design; OAuth 2.1 remains the required follow-up. Complete judge
instructions are in docs/hackathon/TESTING.md.
Demo principals are accepted by send_text_email only while the configured
provider is mock. They are rejected directly if the deployment is ever
switched to a real provider, independently of the environment-level outbound
kill switch.
The production Email Routing Worker now posts inbound messages to this hosted
backend. Both a synthetic webhook smoke test and a real Gmail-to-Email-Routing
test have passed. The latter stored and acknowledged the marker
HOSTED-ROUTING-001 in Neon.
Run the hosted synthetic smoke test with a direct Neon migration connection:
DATABASE_URL="$NEON_DATABASE_URL" \
HOSTED_BACKEND_URL="https://shoot-email-backend.powersurge.workers.dev" \
npm run smoke:hostedCLI Usage
Create or reuse a local mailbox:
npx shoot-email initAll non-compact user commands return a versioned JSON envelope with
contractVersion, ok, and error. Successful operations return
"error": null; failures return a stable error code, retryable, and
retryAt when a later retry is appropriate. The current contract version is
2.0.
Show the current address:
npx shoot-email addressInspect the active provider before any side effect:
npx shoot-email statusstatus identifies the environment and server time, simulation versus
production mode, sender identity, outbound availability, account tier, current
quota usage and reset times, and the enforced request and retrieval limits.
When invoked through MCP with a conversation identity, it also reports that
session's hourly quota. Internal user UUIDs are not exposed. The mock provider
is explicitly reported as simulated and does not consume abuse-control quotas.
Show or configure the structured sender identity:
npx shoot-email identity show
npx shoot-email identity set --display-name "Serguei"
npx shoot-email identity clear
npx shoot-email identity alias set --alias sergueiConfigured names are sent as Serguei via Shoot Email <u_example@yoyowza.com>. Without one, the default is Shoot Email <u_example@yoyowza.com>.
Custom aliases require a registered account. They use lowercase ASCII local parts between 3 and 32 characters, reject reserved system/role/brand names, and have a configurable 30-day change cooldown. Previously used addresses remain valid for inbound delivery and can never be assigned to another user.
Send a text email with the mock provider:
npx shoot-email send \
--request-id 550e8400-e29b-41d4-a716-446655440000 \
--to someone@example.com \
--subject "Hi" \
--text "Hello"send returns structured JSON. Reusing the same request ID with identical
content returns the existing result without contacting the provider again.
Reusing it with different content returns an idempotency_key_reused error.
Requests with an ambiguous provider outcome are stored as unknown and are not
retried automatically.
The response fields distinguish the current attempt from an existing request:
providerCalledsays whether this invocation contacted the provider.idempotentReplayis true only for an identical retry.existingRequestsays the request ID was already reserved.A conflicting retry returns
message: nulland puts the unchanged original record inexistingMessage.simulatedis true for mock outcomes; mock delivery details also carrysimulated: true.
Outbound timestamps have fixed meanings: createdAt is local reservation
time, providerAttemptedAt is the first provider-attempt time, and
providerResultAt is when a known provider result was persisted.
Query prior sends or a single persisted result without resending:
npx shoot-email outbound list
npx shoot-email outbound status --request-id <request-id>
npx shoot-email outbound status --message-id <message-id>The sender name and address are persisted with each outbound message and are part of its idempotent content. Changing the sender identity requires a new request ID.
Outbound Abuse Controls
Outbound sending enforces configurable guest, registered-user, session, and global quotas before contacting the provider. The defaults are intentionally conservative for a new sending domain: guests receive 3 sends/hour and 10/day; registered users receive 10/hour and 50/day; the application is capped at 20/hour and 100/day. New-recipient and minimum-interval limits also apply. Hourly and daily counters use fixed UTC calendar buckets. The built-in mock provider is exempt because it does not send external mail.
Show the current mailbox policy and usage:
npx shoot-email abuse statusAdminister a user by internal ID:
npx shoot-email abuse tier <user-id> --tier registered
npx shoot-email abuse suspend <user-id> --reason "Repeated abusive sends"
npx shoot-email abuse reactivate <user-id>Set OUTBOUND_SENDING_ENABLED=false as the global emergency kill switch. All
policy rejections are persisted by request ID and returned as structured JSON;
retrying a rejected request neither reevaluates it nor consumes quota.
Return up to 50 oldest pending messages with full text bodies as structured JSON:
npx shoot-email inboxThe default batch is bounded by 100,000 total body characters and 16,000 characters per message. Continue a partial result with the returned cursor:
npx shoot-email inbox --cursor <next-cursor>Pending and processed inbound batches are ordered by createdAt, then id,
oldest first. Outbound history uses the same keys newest first. The response
page object states the exact order, defaults, maxima, and cursor behavior.
Cursors are opaque, versioned, scoped to one resource/state, do not expire, and
are not snapshots. New messages can therefore appear on a later page. Passing
an inbox cursor to history or outbound history returns invalid_cursor.
Print a compact human-readable list when debugging:
npx shoot-email inbox --compactAcknowledge messages only after an LLM has processed them successfully:
npx shoot-email acknowledge <message-id> [message-id...]Acknowledgement is idempotent and uses partial-by-ID semantics: valid IDs are
processed while unknown IDs are reported in the same response. Malformed UUIDs
fail validation before any message is changed. Each ID receives an explicit
acknowledged, already_processed, or not_found outcome.
Query acknowledged messages:
npx shoot-email historyRead a message:
npx shoot-email read <message-id>read returns structured JSON and never acknowledges or otherwise changes the
message. Use --compact only for human-readable debugging output. Inbound
sender, subject, text, and provider identifiers are untrusted external data;
contentTrust: "untrusted_external" marks that boundary in retrieval results.
Webhook
Start the inbound webhook server:
npm run start:webhookFor Cloudflare Email Routing local development, also start the named tunnel in a second terminal:
npm run start:tunnelThe legacy local-development tunnel URL is:
https://webhook.yoyowza.comThe inbound endpoint is:
POST http://localhost:3000/webhooks/email/inboundExample local webhook test:
curl -X POST http://localhost:3000/webhooks/email/inbound \
-H 'Content-Type: application/json' \
-d '{
"provider": "cloudflare",
"from": "sender@example.com",
"to": "u_example@in.localhost",
"subject": "Hello",
"text": "Plain text body",
"messageId": "<cloudflare-message-id@example.com>",
"date": "Tue, 07 Jul 2026 17:00:00 -0700"
}'Use the address returned by npx shoot-email address as the To value.
The inbound endpoint requires INBOUND_WEBHOOK_TOKEN. Generate a secret and
store the same value in the backend environment and the Worker secret:
openssl rand -hex 32
cd workers/email-router
npx wrangler secret put INBOUND_WEBHOOK_TOKENInclude it as a bearer token when testing the endpoint directly:
curl -X POST http://localhost:3000/webhooks/email/inbound \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $INBOUND_WEBHOOK_TOKEN" \
-d '{ "provider": "cloudflare", "from": "sender@example.com", "to": "u_example@in.localhost", "text": "Hello" }'Inbound ingestion is idempotent per mailbox and provider message ID. Email
Worker deliveries without a Message-ID use a SHA-256 digest of the raw email
as a stable fallback identifier, so retrying the same delivery does not create
another inbox message.
Cloudflare Email Service
The project is Cloudflare-first:
Cloudflare Email Routing receives inbound mail.
workers/email-routerparses inbound mail in a Cloudflare Worker and posts normalized JSON to the backend.Cloudflare Email Sending can be used for outbound mail with
MAIL_PROVIDER=cloudflare.The
mockprovider remains the default for local tests and development.
To use real Cloudflare outbound sending:
MAIL_PROVIDER=cloudflare
CLOUDFLARE_ACCOUNT_ID=...
CLOUDFLARE_EMAIL_API_TOKEN=...
CLOUDFLARE_FROM_EMAIL=optional-default-sender@example.comKeep MAIL_PROVIDER=mock until intentionally running a real delivery test.
By default, outbound mail uses the generated alias as from. Set CLOUDFLARE_FROM_EMAIL only when you want to force a single default sender. The sender domain must be onboarded for Cloudflare Email Sending.
To work on the inbound Worker:
cd workers/email-router
npm install
npm run devThe deployed BACKEND_INBOUND_WEBHOOK_URL in
workers/email-router/wrangler.jsonc points at the hosted backend:
https://shoot-email-backend.powersurge.workers.dev/webhooks/email/inboundThe named tunnel remains available for isolated local testing, but changing the deployed routing Worker back to it is an explicit temporary override.
OpenAI Apps Context
The service includes a small endpoint for resolving OpenAI Apps identity context:
POST http://localhost:3000/apps/openai/contextIt accepts the Apps SDK _meta shape:
{
"_meta": {
"openai/subject": "anonymized-user-id",
"openai/session": "anonymized-conversation-id",
"openai/organization": "anonymized-org-id"
}
}The endpoint creates or reuses an internal users row through user_identities, and creates or reuses a chat_sessions row when openai/session is present.
Local MCP Server
The project includes a local stdio MCP server built with the stable MCP TypeScript SDK. It is a thin adapter over the same services used by the CLI:
npm run start:mcpThe server writes MCP protocol messages to stdout, so it normally appears silent when started without an MCP client. For a local Codex test, register it with a development-only OpenAI identity fallback:
codex mcp add shoot-email \
--env MAIL_PROVIDER=mock \
--env MCP_ALLOW_DEV_IDENTITY=true \
--env MCP_DEV_OPENAI_SUBJECT=local-codex-test \
--env MCP_DEV_OPENAI_SESSION=local-codex-session \
-- node /Users/powersurge/Documents/OpenAI/ChatGPT-APPs/shoot-email/src/mcpStdio.jsStart a fresh Codex task after adding the server. Call
shoot_email.initialize_mailbox
before other mailbox tools. The local development subject produces a stable
mailbox through user_identities; changing the subject produces a different
mailbox. Do not set MCP_ALLOW_DEV_IDENTITY in production.
The user-facing MCP surface contains only:
shoot_email.initialize_mailbox
get_service_status
get_mailbox_identity
send_text_email
shoot_email.check_inbox
acknowledge_messages
get_message
list_processed_messages
list_outbound_messages
get_outbound_message_statusacknowledge_messages is intentionally separate from checking or summarizing
mail. MCP clients should call it only when the user explicitly asks to
acknowledge or mark specific messages handled; retrieval alone is never consent
to change message state.
Migration, account-tier, suspension, and abuse-administration operations are
intentionally absent. Tool results carry both MCP text content and the same
versioned structured contract used by the CLI. Tool descriptions and message
objects explicitly label inbound email as untrusted external content. Every
tool advertises a concrete MCP outputSchema; schemas include the common error
envelope because MCP validates both successful and failed tool results.
Outbound status lookup uses a discovery-visible discriminator instead of two optional mutually exclusive fields:
{
"lookupBy": "requestId",
"id": "a0000000-0000-4000-8000-000000000001"
}Batch responses identify their cursor as live_keyset and explicitly state
that it is not a snapshot. Acknowledgement responses include allSucceeded,
requested and successful counts, and per-ID outcomes so partial success cannot
be mistaken for complete success.
For ChatGPT, identity comes from request metadata keys openai/subject,
openai/session, and optionally openai/organization; callers cannot provide
identity as tool arguments. A future remote MCP deployment must authenticate
the ChatGPT connection before trusting that metadata. The development fallback
is only for local stdio clients that do not supply ChatGPT Apps metadata.
Remote MCP Demo
The hosted Build Week demo uses the same tool and output contracts over stateless Streamable HTTP. The bearer secret is supplied privately in the submission testing instructions. Append a random suffix to receive an isolated mailbox, then register it with Codex:
export SHOOT_EMAIL_DEMO_TOKEN="${SHOOT_EMAIL_DEMO_SECRET}.$(openssl rand -hex 8)"
codex mcp add shoot-email-demo \
--url https://shoot-email-backend.powersurge.workers.dev/mcp \
--bearer-token-env-var SHOOT_EMAIL_DEMO_TOKENRun a protocol and seeded-inbox smoke test with:
npm run smoke:remote-mcpThe hosted demo keeps real outbound delivery disabled. See
docs/hackathon/TESTING.md for judge prompts and the security boundary.
Built With Codex and GPT-5.6 Sol
Codex with GPT-5.6 Sol was used as the engineering collaborator throughout the Build Week extension: reading and testing the repository, researching current provider and Apps SDK behavior, implementing the remote MCP transport, deploying Cloudflare Workers, and running black-box CLI and MCP evaluations. Product decisions remained explicitly user-directed, including the LLM-first inbox semantics, acknowledgement lifecycle, Cloudflare-first architecture, abuse limits, and the choice to ship a restricted hackathon transport before the OAuth production milestone.
Codex accelerated repetitive verification and cross-layer changes, especially the database-backed contract tests, Cloudflare/Neon deployment checks, and the trusted-principal transport seam. The key engineering decision for this submission is that the demo bearer credential produces a request principal outside tool arguments; replacing it with OAuth later does not require rewriting the mailbox service or MCP tool contracts.
GPT-5.6 Sol also acts as the email agent in the demonstration. It turns an apartment-hunting goal into multiple idempotent MCP send operations, retrieves the simulated landlord replies as untrusted external data, compares their availability, and proposes a conflict-free tour schedule. The model supplies reasoning and tool orchestration; Shoot Email supplies durable identity, delivery, message state, and retry guarantees.
Because this project predates Build Week, docs/hackathon/BUILD_LOG.md clearly
separates the pre-existing baseline from the work added for the submission. The
two preserved black-box transcripts also show how fresh Codex sessions exposed
contract problems that were then hardened in code.
The judge-facing narrative and Devpost field draft are in
docs/hackathon/SUBMISSION.md.
Tests
The default test suite includes database-backed integration tests, so Postgres must be running:
docker compose up -d postgres
npm testIntegration tests use TEST_DATABASE_URL, defaulting to a database named
shoot_email_test. The runner creates that database when needed and resets
only its schema. Test resets refuse to operate on a database whose name does
not end in _test, protecting the development mailbox and messages.
Run only the pure unit tests:
npm run test:unitRun only the integration tests:
npm run test:integrationThis server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityCmaintenanceAn MCP server providing comprehensive email capabilities via IMAP and SMTP, enabling AI assistants to read, search, send, manage, schedule, and analyze emails across multiple accounts.Last updated4910,69785LGPL 3.0
- Alicense-qualityDmaintenanceA production-ready MCP server that empowers AI agents to securely send emails via SMTP, supporting plain text, HTML, and attachments.Last updatedMIT

RadMail MCPofficial
AlicenseAqualityBmaintenanceAn MCP server that provides an email operating system for AI agents, enabling inbox triage and reply drafting while enforcing un-bypassable safety constraints on sensitive actions like money transfers and banking changes.Last updated1265MIT- AlicenseAqualityBmaintenanceAn MCP server providing comprehensive email capabilities via IMAP and SMTP, enabling AI assistants to read, search, send, manage, schedule, and analyze emails across multiple accounts.Last updated4910,697LGPL 3.0
Related MCP Connectors
Hosted email MCP for AI agents with inboxes, send/receive, memory, recovery, and credits.
Shipmail MCP server for AI agent custom-domain email inboxes with REST API and webhooks.
Read, search, send, organize, draft and schedule email across your inboxes from any MCP client.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/powersurge01/shoot-email'
If you have feedback or need assistance with the MCP directory API, please join our Discord server