Skip to main content
Glama
powersurge01

Shoot Email

by powersurge01

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

Shoot Email: AI-native email for ChatGPT

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 Routing

The 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/mcp

Each 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 install

Create environment config:

cp .env.example .env

Start Postgres with pgvector:

docker compose up -d postgres

If 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:pg16

Apply migrations:

npm run db:migrate

Reset the local database and reapply all migrations:

npm run db:reset

This 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:build

Before 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.dev

It 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:hosted

CLI Usage

Create or reuse a local mailbox:

npx shoot-email init

All 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 address

Inspect the active provider before any side effect:

npx shoot-email status

status 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 serguei

Configured 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:

  • providerCalled says whether this invocation contacted the provider.

  • idempotentReplay is true only for an identical retry.

  • existingRequest says the request ID was already reserved.

  • A conflicting retry returns message: null and puts the unchanged original record in existingMessage.

  • simulated is true for mock outcomes; mock delivery details also carry simulated: 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 status

Administer 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 inbox

The 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 --compact

Acknowledge 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 history

Read 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:webhook

For Cloudflare Email Routing local development, also start the named tunnel in a second terminal:

npm run start:tunnel

The legacy local-development tunnel URL is:

https://webhook.yoyowza.com

The inbound endpoint is:

POST http://localhost:3000/webhooks/email/inbound

Example 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_TOKEN

Include 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-router parses 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 mock provider 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.com

Keep 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 dev

The 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/inbound

The 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/context

It 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:mcp

The 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.js

Start 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_status

acknowledge_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_TOKEN

Run a protocol and seeded-inbox smoke test with:

npm run smoke:remote-mcp

The 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 test

Integration 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:unit

Run only the integration tests:

npm run test:integration
A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    A
    quality
    C
    maintenance
    An 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 updated
    49
    10,697
    85
    LGPL 3.0
  • A
    license
    -
    quality
    D
    maintenance
    A production-ready MCP server that empowers AI agents to securely send emails via SMTP, supporting plain text, HTML, and attachments.
    Last updated
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    An 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 updated
    12
    65
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    An 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 updated
    49
    10,697
    LGPL 3.0

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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