Live · the mcp4mail.online project

Watch software being built. Right now.

mcp4mail is a real open-source product. A person writes the tasks, our runners program them — and you can check every step: the task, the pull request, the tests and the resulting code on GitHub.

The site is deployed once a day — a merged change is on it on work days at around 18:00 Prague time (CET/CEST), not the moment you see it merged here.

Right now

0 working · 2 waitingupdated 0 s ago

In 47 days they delivered 189 finished tasks and are now waiting for the next assignment.

  • Runner 2 · Claude Codewaiting for the next task

    — nothing ready in the queue —

  • Runner 3 · Claude Code · sonnetwaiting for the next task

    — nothing ready in the queue —

Runners run on our own machines. None of them has direct access to the main branch.

The runners run around the clock, weekends and holidays included. They take a task as soon as it is ready; when the queue is empty they wait.

Try it yourself

Give an AI developer a task

Pick a task, confirm it by e-mail and watch the AI developer do it here — from the branch to the merge.
Easy

GitHub issue templates: bug report, feature request, "my mail provider does not work"

Context: Launch-day task for Show HN (2026-10-21). New visitors will open issues; today .github/ has only dependabot.yml, pull_request_template.md and workflows – no issue templates, so reports arrive without the details we need.

Do:
1. Add .github/ISSUE_TEMPLATE/ with issue forms (YAML):
- bug_report.yml: hosted or self-hosted, version/commit, MCP client used, steps, expected vs. actual, relevant log lines (with a reminder never to paste passwords or tokens).
- feature_request.yml: problem, proposed solution, which tool it concerns.
- mail_provider.yml: provider name, IMAP host/port, whether an app password is used, error message shown in the app; link to the existing provider guides.
2. Add config.yml with blank_issues_enabled: false and a contact link to SECURITY.md for vulnerabilities (if SECURITY.md is not merged yet, link to the GitHub security advisories page of the repo).
3. Mention the templates in CONTRIBUTING.md in one sentence.

Acceptance criteria: "New issue" on GitHub shows the three forms; forms validate (GitHub renders them without errors in the PR preview / after merge); bin/ci green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Moderate

Self-hosting dry run from a clean clone: fix every gap in docs/self-hosting.md and .env.example

Context: Launch-day task for Show HN (2026-10-21). "Run it yourself with docker compose" is a main promise of the README; HN readers will try it within minutes and comment if it breaks.

Do:
1. In a fresh clone in a temporary directory, follow README.md → Self-hosting and docs/self-hosting.md literally, step by step, as a newcomer would: cp .env.example .env, fill only what the docs tell you, docker compose up -d, open the app, sign up, reach the Mail accounts page, check that /.well-known/oauth-protected-resource (or whatever metadata endpoint the docs mention) answers with the configured resource URI.
2. Write down every place where the docs were missing a step, were ambiguous, or where .env.example disagrees with the ENV reads in app/, config/, lib/ (the "docs drift" check from docs/backlog-refill.md).
3. Fix the docs and .env.example. If something in the code/compose file is actually broken, fix it only if small; otherwise open a separate bug task in this project and mention it in the PR.
4. Put the list of findings (fixed / filed) into the PR description.

Acceptance criteria: a newcomer following only the docs gets a running instance; .env.example and docs match the code's ENV reads; findings listed in the PR; bin/ci green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

README: "Tested mail providers" table built from the existing guides and tests

Context: Launch-day task for Show HN (2026-10-21). "Does it work with my provider?" will be a frequent question. The backlog refill tracks IMAP compatibility for Dovecot, Cyrus, Courier, Gmail, iCloud, Seznam, Zoho, Fastmail (guides in app/views/guides/content/, tests in test/; recent tasks #13679 Dovecot, #13680 Seznam, #13681 iCloud), but the README does not show it.

Do:
1. Add a short "Mail providers" table to the README (in or right after the "Mail accounts" section): provider, status (tested = there is a test in test/, guide = setup guide exists, untested), app password needed (yes/no, per the guide), link to the guide on mcp4mail.online if public.
2. Derive every row from the code/tests/guides – do not mark anything tested that has no test. Note Outlook/Exchange status honestly (only if covered by a guide or test; otherwise "untested").
3. Add one line inviting people to report a provider via the issue template (if merged) or a GitHub issue.

Acceptance criteria: table present, each status traceable to a file in the repo (list the files in the PR description), bin/ci green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

README screenshots: the AI-permission switch and the send-approval page

Context: Launch-day task for Show HN (2026-10-21). The README explains the safety model only in text; two screenshots make it understandable in seconds.

Do:
1. Produce two screenshots from the system tests (or a dedicated system test that saves them), with seeded fake data only – no real addresses:
- Mail accounts page with "Allow the AI to make changes to this mailbox" switched on and the permission checkboxes unfolded.
- The outgoing-message approval page (the page behind the link a user gets before an e-mail from send_message leaves).
2. Save them as optimized PNG/WebP under docs/images/ (keep each under ~200 KB), English UI.
3. Embed them in the README section "MCP endpoint: read-only by design" with descriptive alt text.

Acceptance criteria: two images in docs/images/, shown in the README on GitHub, no real personal data in them, bin/ci green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

mcp4mail.online: visible links to the GitHub repo and to the live build (homepage + footer, cs/en)

Context: Launch-day task for Show HN (2026-10-21). Visitors coming from HN will move between the repo, the hosted site and https://mcptask.online/live. The hosted site must make the open-source and built-in-public story easy to find.

Do:
1. Check the homepage and the footer of mcp4mail.online (cs and en). If missing, add: a "Source code on GitHub" link to https://github.com/jchsoft/mcp4mail and a "Watch it being built live" link to https://mcptask.online/live (with ?language=en / ?language=cs according to the current locale). If links already exist, just verify they work in both locales and close the task with a note.
2. Add the i18n strings in cs and en (i18n parity check must pass).
3. Use rel="noopener" for external links; keep the existing design.

Acceptance criteria: both links visible on the homepage and in the footer in cs and en; system test covering their presence; bin/ci green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Moderate

Mail index: read/unread state goes stale, so "unread since Monday" lists mail already read

Stable key: dogfood: unread since Monday (source: Dogfooding, backlog refill 2026-W44).

What happens

Imap::MessageSync#import_new_messages only fetches UIDs above MailFolder#last_synced_uid. A message's FLAGS are stored once, when it is first indexed, and never refreshed. When the owner reads (or flags) mail in their own mail client, the index keeps unread: true, so search_messages unread: true returns mail that is no longer unread. The same pass never notices messages expunged on the server, so they stay searchable until get_message fails with "message gone".

Reproduction (live, 2026-10-22)

  • list_folders for a mailbox: INBOX unseen_count: 0 (live IMAP STATUS).
  • search_messages account_id: <same>, folder: "INBOX", unread: true: 5 rows, all unread: true.
  • The two disagree; STATUS is the truth.

What to do

In sync_folder, after importing new messages, refresh the flags of already-indexed messages: UID FETCH 1:<last_synced_uid> (FLAGS) (batched like the import), update_all the changed flags, and delete index rows whose UID the server no longer returns. Use CONDSTORE/CHANGEDSINCE only if it stays simple; a plain FLAGS fetch is cheap enough for the 15-minute refresh. Mirror Gmail's per-gm_msgid copies the way FlagSetter does.

Tests

Extend test/services/imap/message_sync_test.rb with the fake IMAP server: a message's flags change between two syncs → the index follows; a message is expunged between two syncs → its row is gone. No setting, no new tool.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

docker-compose: empty EXCEPTION_NOTIFICATION_RECIPIENTS still emails the hosted operator

Key: self-hosting: error mail recipients. Source: self-hosting dry run, read statically (Docker was not available on the audit machine, so this was not executed).

Problem. docs/self-hosting.md ("Error emails") and the comment in docker-compose.yml both say an empty EXCEPTION_NOTIFICATION_RECIPIENTS switches error emails off. But docker-compose.yml:29 reads:

EXCEPTION_NOTIFICATION_RECIPIENTS: ${EXCEPTION_NOTIFICATION_RECIPIENTS:-chmel@jchsoft.cz}

In Compose, :- uses the default when the variable is unset or empty. So a self-hoster who sets it empty to turn the emails off sends their instance's errors to the hosted instance's operator instead, if SMTP is configured. The variable is also missing from .env.example, so most self-hosters never see it.

Reproduce. EXCEPTION_NOTIFICATION_RECIPIENTS= docker compose config | grep EXCEPTION

Do.
1. Use ${EXCEPTION_NOTIFICATION_RECIPIENTS-...} (no colon) so empty stays empty. Consider whether a self-hosted instance should default to the operator's address at all; the safer default is off.
2. Add the variable to .env.example and keep docs/self-hosting.md accurate.

Done when. The command above prints an empty value, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

Self-hosting: Compose requires the encryption keys the docs call optional, and blocks the command that generates them

Key: self-hosting: encryption keys. Source: self-hosting dry run, read statically (Docker was not available on the audit machine, so this was not executed; run the reproduction first).

Problem. docs/self-hosting.md lists the three ACTIVE_RECORD_ENCRYPTION_* variables as "recommended" and says that without them the app derives the keys from SECRET_KEY_BASE. But docker-compose.yml:22-24 declares them with ${VAR:?...}, which makes Compose stop with an error when they are unset or empty, and .env.example ships them empty.

The docs then offer this for people without Ruby on the host:

docker compose run --rm --no-deps app bin/rails db:encryption:init

Compose interpolates the whole file before running anything, so this should fail on the very variables it is meant to generate. A newcomer without Ruby has no documented way forward.

Reproduce. In a fresh clone: cp .env.example .env, fill only SECRET_KEY_BASE and MCP4MAIL_DATABASE_PASSWORD, then run the command above.

Do. Pick one and make code and docs agree: either make the keys optional in Compose as the docs describe, or keep them required and document a generation command that works before they are set (for example openssl rand values, if db:encryption:init is not needed).

Done when. A newcomer with Docker only can follow the docs from cp .env.example .env to a running app, and bin/ci is green. Related: the general dry run in #13885.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

IMAP compatibility: Outlook.com, the README and the connect error disagree; verify and document one status

Key: imap: outlook. Source: IMAP compatibility check.

Measured: the product says two different things.
- README.md:113 and :181, docs/self-hosting.md:202 and the landing page FAQ (config/locales/en.yml, cs.yml) say Outlook.com offers IMAP with an app password, so it works.
- config/imap_providers.yml:48-51 sets messages: { any: native_connector } for outlook.com, hotmail.com, live.com and msn.com. Any connection problem at all then tells the user "Outlook already has a native connector in Claude and ChatGPT; you do not need mcp4mail for it" (test/services/imap/connection_problem_test.rb:60).
- app/models/mail_provider.rb:11 offers a preset for outlook.office365.com. There is no guide in app/views/guides/content/, and the status has never been recorded as tested against a real account.

Why it matters. Microsoft has been moving Outlook.com to OAuth-only mail access, so the README's claim may no longer be true. mcp4mail deliberately has no Microsoft OAuth (see CLAUDE.md), so if app passwords no longer work, Outlook.com is not supported and the public text promises something false.

Do.
1. Check Microsoft's current documentation, and try a real Outlook.com account with an app password if you have one.
2. Bring every place above in line with the verified status, in both cs and en.
3. If it works, add a guide. If it does not, say so plainly in the README and FAQ, and reconsider the preset.

Do not add OAuth: that would need an owner decision.

Done when. One status is documented with the date and how it was checked, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

IMAP compatibility: Centrum.cz, guide and tested status

Key: imap: centrum. Source: IMAP compatibility check.

Measured. app/models/mail_provider.rb:14 has a preset (imap.centrum.cz:993) and config/locales/en.yml:351 has a hint about switching on IMAP in the Centrum settings. There is no guide in app/views/guides/content/, no entry in config/imap_providers.yml, no test naming the provider, and nothing in the README or docs.

Do. Follow the existing Seznam setup, the closest match: config/imap_providers.yml, app/views/guides/content/seznam.md, and its tests. Add the domains (check Centrum's current ones, such as centrum.cz and atlas.cz), a guide in both cs and en checked against Centrum's current help pages, and a test using the fake IMAP server on 127.0.0.1 with made-up addresses.

Done when. A Centrum address gets the right server and the right hint, the guide is linked, the docs state how the status was checked, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

IMAP compatibility: Proton Bridge, document the status for hosted and self-hosted

Key: imap: proton. Source: IMAP compatibility check.

Measured. config/imap_providers.yml:38 maps the Proton domains to a bridge_required message, tested in test/services/imap/connection_problem_test.rb:30. Nothing in the README or docs/ states whether Proton Bridge works, and there is no guide.

Why it matters. Bridge listens on the user's own machine, so the hosted instance presumably cannot reach it. Only a self-hosted instance on that machine could, and Bridge uses a local certificate that may not pass TLS verification.

Do. Check how Bridge exposes IMAP (host, port, STARTTLS, certificate) against Imap connection handling in app/services/imap/. Add the result to the compatibility section of docs/self-hosting.md, say plainly what is verified and what is not, and make sure the bridge_required message does not suggest something the hosted instance cannot do. Update cs and en together.

Done when. The status is documented for both hosted and self-hosted, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

IMAP compatibility: Mailcow, documented and tested status

Key: imap: mailcow. Source: IMAP compatibility check.

Measured. No mention of Mailcow in README.md, docs/, config/imap_providers.yml, app/views/guides/content/ or test/.

Do. Mailcow's IMAP server is Dovecot, and docs/self-hosting.md ("IMAP server compatibility") already has a Dovecot section. Check Mailcow's documented folder layout and capabilities against that section, note anything specific (for example SOGo folder names or app passwords), and add a test against the fake IMAP server if its behaviour differs from what is already covered. If you run it against a live instance, say so; if not, say that too.

Done when. docs/self-hosting.md states the Mailcow status and how it was checked, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Moderate

IMAP compatibility: Stalwart, documented and tested status

Key: imap: stalwart. Source: IMAP compatibility check.

Measured. No mention of Stalwart in README.md, docs/, config/imap_providers.yml, app/views/guides/content/ or test/.

Do. Stalwart is its own IMAP server, not Dovecot-based. Run it locally in a container if you can (it has a small all-in-one image) and walk connect, folder listing, index sync, search and read. Otherwise compare its documented capabilities (namespace, SPECIAL-USE, MOVE, UIDPLUS, CONDSTORE) with what app/services/imap/ expects. Add the result to the compatibility section of docs/self-hosting.md, plus a fake-server test for any behaviour not yet covered. State whether the check was live.

Done when. The status and how it was checked are documented, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Moderate

Evaluate: read a whole conversation, no tool or filter returns one thread

Key: dogfood: Re: Re: Re:. Source: dogfooding over MCP.

Query. search_messages(query: "Re: Re: Re:"), the closest an assistant can get to "show me this whole conversation". It returned unrelated single replies, including a subject that merely contains the word "re", because every word is matched separately and the results carry no conversation information.

Measured. There is no thread tool or filter. A grep of app/tools, app/models, app/services and db/schema.rb finds no stored In-Reply-To, References or thread id on indexed mail; those appear only when composing a reply (app/services/message_composer.rb). An assistant has to guess from subjects, which fails on changed subjects and mixes up different conversations with the same subject.

Do. Evaluate and write a short recommendation, with the smallest change that answers "show me this conversation" read-only: for example, index In-Reply-To and References and add a parameter or read-only tool. Estimate the index migration and re-sync cost. A read-only tool is allowed without a write-tool decision, but it needs no settings and must go through the signed-in user's own accounts.

Done when. The recommendation is posted on this task, with a follow-up implementation task if it is worth doing.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

search_messages: no way to find flagged mail

Key: dogfood: flagged. Source: dogfooding over MCP.

Query. "Show my flagged mail." The nearest call, search_messages(query: "flagged", folder: "Archive", account_id: ...), returned nothing, because the query matches words in headers rather than flags.

Measured. search_messages has an unread filter but no flagged one, and its rows do not say whether a message is flagged. app/tools/mcp_tools/get_message.rb does not mention flags either. The only tool that reports the flag is the write tool set_flags (SET_FLAGS in app/tools/mcp_tools/output_schemas.rb), so an assistant can flag mail but cannot find it again.

The index already stores flags (mail_messages.flags, db/schema.rb), and unread is built on the same column.

Do. Add a flagged: true filter and a flagged field on the rows, following how unread is done in app/tools/mcp_tools/search_messages.rb:99 and :115. Note that #14077 reports indexed flags going stale after the first import: if that is still open, mention the limitation in the tool description rather than fixing it here.

Done when. The filter works with a test against the fake server, the README tool table is still right, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

Easy

Smtp::Autodetect: an address ending in "@" treats the local part as the domain, and its test reaches the real network for 15 s

Key: slow: Smtp::AutodetectTest an address with no domain. Source: test suite health (a test above 5 s).

Measured. In bin/rails test:all -v, this test took 15.12 s, which is exactly Smtp::Autodetect::BUDGET:

Smtp::AutodetectTest#test_an_address_with_no_domain_finds_nothing_without_asking_anyone = 15.12 s

Cause. test/services/smtp/autodetect_test.rb:148 sets the username to "bare@" and expects nil "without asking anyone". But app/services/smtp/autodetect.rb:108 computes the domain with:

email.split("@").last.to_s.downcase.presence

"bare@".split("@") is ["bare"], so the domain becomes "bare", not blank. The guard at line 35 never fires. The real Smtp::Autodetect then runs its autoconfig, SRV and hostname lookups for bare until the budget runs out, and the test passes only because the timeout also returns nil. It goes through the real network, which app/services/CLAUDE.md forbids for tests.

Do.
1. Take the domain only from what follows the @, so "bare@" has none. Check how a username without any @ should behave: MailAccount#sender_address adds the host in that case.
2. Make the test fail if anything is asked (for example with FakeAutodetectNetwork), rather than relying on the timeout.

Done when. The test takes well under a second, fails on the old code, and bin/ci is green.

Why we ask for your e-mail: we'll e-mail you when the work starts, when the pull request is ready and when it's done. Nothing else, and it is never shown on this page.

What you are looking at

Every change goes through the same five steps. No exceptions.

Afraid AI will break your code? So are we. That is why a runner gets nothing you would not give a new developer on their first day: it works on its own branch and reaches main only through a pull request that passes CI – plus a person's review where the project requires it.

  1. Task

    A person writes the brief. The runner takes what is ready — and when a task turns out to be bigger than one run, it sets it aside for a person instead of half-implementing it.

  2. Branch

    Work happens in isolation. The main branch is locked for people and runners alike.

  3. Pull request

    A small, readable change linked to its task. Anyone can open it on GitHub.

  4. Tests & CI

    Lint, security scan, unit and system tests. Red means it goes no further.

  5. Merged

    After green CI the change is merged; mcp4mail.online picks it up in the next daily deploy.

18tasks waiting in the queue
0tasks in progress right now
8tasks finished in the last 7 days
87 %pull requests passed CI on the first run in the last 7 days
41 minmedian from task pickup to merge

All time189 tasks done154 pull requests merged145 hours of work90 % pull requests passed CI on the first run

Latest changes

What went well. And what did not.

We do not hide red tests or tasks a runner set aside. They are exactly where you see the checks working.

Backlog refill 2026-W46

PR #212 on GitHub

  1. Task
  2. Branch
  3. Pull request
  4. CI on 1st try
  5. Merged 10:53

Bump selenium-webdriver 4.51.0 → 4.52.0

15 min · PR #211 on GitHub

  1. Task
  2. Branch
  3. Pull request
  4. CI on 1st try
  5. Merged 10:48

Bump tailwindcss-rails 4.6.0 → 4.7.0

42 min · PR #210 on GitHub

  1. Task
  2. Branch
  3. Pull request
  4. CI on 1st try
  5. Merged 10:39

Glama: get mcp4mail evaluated and graded so the awesome-mcp-servers PR can be merged (glama.json + Glama build check)

37 min · PR #209 on GitHub

  1. Task
  2. Branch
  3. Pull request
  4. CI on 1st try
  5. Merged 10:27

Backlog refill: weekly audit that turns signals into tasks

1 h 26 min · PR #208 on GitHub

  1. Task
  2. Branch
  3. Pull request
  4. CI on 1st try
  5. Merged 10:00
The whole history is in the pull requests on GitHub →

Do not trust us. Read the code.

The whole project is open. Send the link to your most sceptical senior and let them go through the pull requests, the tests and the commit history. This is exactly how it would look at your company.

github.com/jchsoft/mcp4mailmcp4mail.online — the result in production

The site is deployed once a day — a merged change is on it on work days at around 18:00 Prague time (CET/CEST), not the moment you see it merged here.

Open-source licence · no registration
rocket_launch

Run it on your own repository

The same process, the same safeguards, your infrastructure. 30 days to try it, no credit card.

verified_userFree 30-day trial. No credit card. Cancel any time.