Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ Choose **"Skip — I'll send the emails myself"** in the setup wizard (or `optio

---

## Let a local AI agent handle follow-ups

`eraser mcp` connects Eraser to Codex over local STDIO. Ask **“Handle my pending
removal tasks”**, review the proposed batch, and approve it once. The agent can
open confirmation links, fill forms, inspect receipts, and save notes and
screenshots in the existing **Tasks** page. Email sending uses your existing
settings, templates, limits, and history. Codex supplies the AI; Eraser needs no
separate AI API key.

Chrome/Chromium is required for forms. CAPTCHA, login, identity checks, uncertain
submissions, and unexpected destinations need your intervention. A completed form
means the request was accepted, not that the broker has erased your data.

See **[MCP setup and the fake-data demo](docs/mcp.md)** for the Codex configuration,
required batch approval settings, and supported workflows.

## For Developers: CLI Usage

If you prefer the command line, Eraser has a full CLI.
Expand Down
51 changes: 51 additions & 0 deletions cmd/eraser/cmd_mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/drumandbytes/eraser/internal/mcpserver"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
)

func mcpCmd() *cobra.Command {
var headless, demo bool
var chromePath string
cmd := &cobra.Command{
Use: "mcp", Short: "Expose Eraser tasks to a local AI agent over MCP (stdio)",
Long: "Start a local MCP server using the same configuration and history as the web UI.\nConfigure begin_batch and retry_action to prompt for approval in your MCP client.\nChrome/Chromium is required for browser actions. Use --demo to try fake local forms.",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
var s *mcpserver.Service
var cleanup func()
var err error
if demo {
s, cleanup, err = mcpserver.NewDemo(headless, chromePath)
} else {
s, err = mcpserver.New(mcpserver.Options{ConfigPath: resolveConfigPath(), BrokerPath: brokerFile, ProfileID: profileFlag, Headless: headless, ChromePath: chromePath})
}
if err != nil {
return err
}
if cleanup != nil {
defer cleanup()
}
defer func() { _ = s.Close() }()
fmt.Fprintln(os.Stderr, "Eraser MCP ready. Waiting for a local client; logs use stderr.")
err = s.MCP.Run(ctx, &mcp.StdioTransport{})
if ctx.Err() != nil {
return context.Cause(ctx)
}
return err
},
}
cmd.Flags().BoolVar(&headless, "headless", false, "Hide Chrome (visible by default for verification handoff)")
cmd.Flags().BoolVar(&demo, "demo", false, "Use isolated fake data and local sample forms; never access real email or brokers")
cmd.Flags().StringVar(&chromePath, "chrome-path", "", "Optional path to Chrome/Chromium")
return cmd
}
1 change: 1 addition & 0 deletions cmd/eraser/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ send through any SMTP provider (Gmail, Proton Mail Bridge, Fastmail, ...).`,
rootCmd.AddCommand(initCmd())
rootCmd.AddCommand(sendCmd())
rootCmd.AddCommand(serveCmd())
rootCmd.AddCommand(mcpCmd())
rootCmd.AddCommand(listBrokersCmd())
rootCmd.AddCommand(statusCmd())
rootCmd.AddCommand(addBrokerCmd())
Expand Down
24 changes: 24 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- **Database**: SQLite (for history tracking via `modernc.org/sqlite`)
- **Config**: YAML (`gopkg.in/yaml.v3`)
- **Browser automation**: chromedp, for `fill`/`confirm` commands and CAPTCHA detection
- **Local agent connection**: official Go MCP SDK, STDIO transport (`eraser mcp`)

## Project Structure

Expand Down Expand Up @@ -36,6 +37,7 @@ eraser/
│ ├── inbox/ # IMAP scan (scan.go: ScanAndStore/RecordReply) + reply classification (success/form-required/
│ │ # confirmation/rejection/pending/bounced)
│ ├── schedule/ # unattended cycles: shared lock + state file, launchd/systemd install
│ ├── mcpserver/ # profile-scoped MCP tools, reviewed batches, persistent browser sessions
│ ├── template/
│ │ ├── template.go # Template rendering engine
│ │ └── templates/ # Embedded: gdpr.tmpl, ccpa.tmpl, generic.tmpl
Expand All @@ -61,6 +63,28 @@ CI (`.github/workflows/ci.yml`) runs `go build`/`go vet`/`go test -race` and `go

## Key Concepts

### MCP automation

`cmd_mcp.go` exposes `internal/mcpserver` through the official Go MCP SDK. The
server is local and serves one client session. The client supplies the AI and
must prompt before `begin_batch` and `retry_action`; the server enforces the
resulting profile/task/broker scope. Changing settings invalidates that scope.

Browser tools use `internal/browser/automation.go`, retaining Chrome between
calls and referencing controls from fresh snapshots in an isolated JavaScript
world. Destinations derive from the selected broker's catalog entries. Request
interception checks redirects and form traffic; a cumulative document CSP
blocks popups, workers, and out-of-scope connections. This deliberately limits
compatibility with embedded forms and external dependencies.

`internal/history/automation.go` adds an action journal without changing existing
tables. An action is claimed before external work; interrupted submissions
remain unknown and cannot be automatically replayed. Notes, screenshots, and
completion use existing `pending_tasks`, and accepted requests remain awaiting
a response in the pipeline. Email uses `email.SendRemoval` and the existing
profile-scoped limits; inbox replies use the existing classifier with an
explicitly resolved owner. See [mcp.md](mcp.md) for operational limits.

### Broker
`data/brokers.yaml` is compiled into the binary (`data/embed.go`, `//go:embed`).
`broker.Load(override)` resolves it from `--brokers`, then `~/.eraser/brokers.yaml`
Expand Down
7 changes: 7 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ go test ./...
./eraser confirm # click confirmation links from broker emails
./eraser fill # browser-automate opt-out forms
./eraser serve [-p 3000] # web UI
./eraser mcp [--demo] [--headless] [--chrome-path /path/to/chrome] # local MCP over stdio
./eraser profile list # list configured profiles
./eraser profile add # add a second/third named profile
```
Expand All @@ -48,6 +49,12 @@ Web UI equivalents for the per-profile commands: `export` → History → Export

## Configuration

`mcp` shares the web UI's configuration, selected broker list, profiles, and
history database. Its browser stays visible by default so a user can complete
verification. `--demo` uses an isolated temporary profile and five local forms;
it never accesses real mail or broker sites. See [mcp.md](mcp.md) for the required
client approval settings and tool reference.

Which of these keys and commands are covered by the 1.x compatibility promise: [stability.md](stability.md).

User config is stored at `~/.eraser/config.yaml` (see `config.example.yaml` for the full schema). Key sections:
Expand Down
165 changes: 165 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Local MCP automation

Eraser's MCP server lets a local Codex agent work through removal follow-ups
while you review batches and handle verification. It reuses your profiles,
SMTP/IMAP settings, broker list, SQLite history, and web UI. There is no separate
AI provider or API key to configure in Eraser.

## Connect Codex

1. Build this version with `go build -o eraser ./cmd/eraser`. Install Chrome or
Chromium if you want to automate forms.
2. Complete Eraser's existing setup (`./eraser serve`) if needed. Existing users
can keep their email configuration and selected regions/template, including
EU/EEA settings. MCP does not change these defaults.
3. Merge [examples/codex-mcp.toml](../examples/codex-mcp.toml) into your Codex
user config or this project's `.codex/config.toml`. Replace the executable
path with an absolute path. Keep machine-specific configuration out of Git.
4. Check `codex mcp get eraser`. Start a new local Codex session if Eraser is not
in the current session's tool list.

Codex supports local STDIO servers and per-tool approval settings in its
[official MCP documentation](https://learn.chatgpt.com/docs/extend/mcp).
Keep `begin_batch` and `retry_action` set to `approval_mode = "prompt"`.
Other tools can use `approve`: the server rejects form actions and sends until
a batch exists, and enforces that batch's scope afterward.

These two approval settings are part of the authorization design. MCP itself
does not attest that a person clicked Approve. A client that automatically
approves every tool can start batches without a person reviewing them. Use
another client only if it can enforce the same approval policy.

Use `--config /absolute/path/config.yaml` if the web UI uses a non-default
configuration. Use `--profile <id>` when several profiles exist; Eraser refuses
to guess. `--chrome-path` selects a nonstandard browser executable. Leave Chrome
visible for manual verification; `--headless` is intended for fixture tests or
workflows that do not need intervention. Keep stdout reserved for MCP messages;
diagnostics go to stderr.

## Try fictional data first

Set the MCP arguments to `["mcp", "--demo"]`, or add a second server named
`eraser_demo` with those arguments and the same two tool approval overrides.
Restart that server to create a fresh demo.

Ask Codex:

> Use Eraser's demo. List the pending tasks, prepare the simple form and
> multiple-step form as a batch, and show me the fake profile and destinations.
> After I approve, complete them and record the evidence.

The demo includes a simple form, multiple steps, required-field validation,
simulated human verification, a confirmation link, and a rejected submission.
It uses a fictional Norwegian profile and a private temporary directory. No
real SMTP, IMAP, or broker connections occur. The only HTTP destination allowed
for the fixtures is the demo's own loopback listener.

`get_status` returns `demo_config_path`. To view the same results in Eraser, run:

```bash
./eraser --config /path/returned/by/get_status/config.yaml serve --port 8081
```

Keep the MCP process running while interacting with the demo forms. The data
and screenshots remain in the printed temporary directory after it ends, so
you can review them in the web UI; remove that directory when finished. A
restarted demo has a new directory and new local URLs.

## Normal workflow

Ask **“Handle my pending removal tasks.”** The agent should:

1. Read status and pending tasks. Optionally scan new replies first.
2. Prepare explicit task IDs, and explicit broker IDs only when you also want
new removal emails. Show you the profile fields, destinations, and email
drafts returned by `prepare_batch`.
3. Call `begin_batch` after you review that preview. Approval is valid only for
those tasks and email recipients in that profile and this MCP session.
4. Open a task, inspect its text and controls, then fill/select or click using
references from the latest snapshot. Personal values can be filled by
`profile_field` without the agent inventing missing information.
5. After each click, record visible evidence of progress, acceptance, or a
problem. `get_screenshot` returns an MCP image for visual inspection.
6. End the batch after reporting completed tasks and those needing your help.

Previews expire after 15 minutes. A batch can contain up to 100 explicit items
and have at most five browser sessions open. Settings or broker-list changes
require a new preview. Ending the batch, disconnecting STDIO, or restarting the
process closes its browsers and revokes authorization. Cookies and intermediate
form state live only for the current browser session.

## Tools

| Tools | Purpose |
| --- | --- |
| `get_status`, `list_brokers`, `list_tasks`, `get_task` | Inspect the selected profile, eligible brokers, task details, source replies, and previous attempts. Lists are paginated. |
| `prepare_batch`, `begin_batch`, `end_batch` | Preview explicit work, obtain client approval, and revoke authorization. |
| `scan_inbox` | Read the configured inbox and classify replies for the selected owner; never archive or open links. |
| `send_request` | Send one reviewed email through the existing sender, respecting manual mode, dry-run, cooldown, daily cap, and rate limit. |
| `open_task`, `observe_page`, `get_screenshot` | Open an approved form/link and inspect the persistent browser. Opening a confirmation URL can itself perform an action. |
| `browser_action` | `fill` a text field, choose a select option by value/text, set a checkbox/radio using `true`/`false`, or `click` a current control. No arbitrary JavaScript or selectors. |
| `record_result` | Save `progress`, `submitted`, `needs_user`, or `unknown` with evidence against the existing task. |
| `retry_action` | After a separate user approval, release an uncertain attempt for retry using its action key and either an approved `task_id` or `broker_id`. Accepted submissions cannot be released. |

## Evidence, interruptions, and privacy

Submission completes the **task**, not the data removal. An email send means
SMTP accepted a message. A form receipt means the request was accepted. Existing
pipeline records move to **Awaiting response** after form acceptance, never automatically to
**Confirmed** from a form click. The existing reply classifier still handles
brokers' later confirmation emails. Form-only work is visible in Tasks even
when there is no previous email record in Pipeline.

The action journal is written before a click, confirmation GET, or email send.
If the process crashes or the result is uncertain, the agent must inspect the
page, mailbox, and previous attempts before proposing a retry. An observation
does not release that safeguard. Native invalid forms can be corrected without
submitting them; server-side rejection or ambiguous receipts stay open for
review. The current receipt check is conservative and recognizes English
acceptance text; other languages may need manual review.

For CAPTCHA, login, identity documents, or missing profile information, the
agent stops. Complete the step in the visible browser, then ask it to
`observe_page` and continue. It never solves CAPTCHA or uploads identity files.
Embedded frames, custom controls, shadow DOM, and popup-based workflows may
need manual completion; only standard visible controls in the main document
are automated.

Destinations come from that broker's trusted local `website` and `opt_out_url`,
including subdomains, not from domains invented by an agent or extracted from
email. URLs need HTTPS and a public address. Redirects, frames, subresources,
and form traffic outside those hosts are blocked. This deliberately also
blocks third-party CDNs and verification services not covered by the catalog;
some real sites will need manual handling. An unexpected destination pauses
the session persistently. Review the catalog independently, end the batch, and
prepare a fresh one after correcting a legitimate portal URL. The agent cannot
expand the domain scope through a tool call.

SMTP/IMAP passwords, browser cookies, and password-field values are not returned
by tools. Profile details, page text, email bodies, confirmation URLs, and
screenshots can contain personal data and are provided to the MCP client/model
when requested. Treat all broker email and page content as untrusted data.
Screenshots are withheld while recognized credential or identity fields are
visible, including password and one-time-code fields.
Local history and screenshots remain sensitive files under the existing Eraser
configuration directory. Use one automation workflow at a time: the journal
coordinates MCP actions, while manually submitting a form in another browser
or sending through another UI is outside its duplicate protection.

## Contributor verification

```bash
go build -o eraser ./cmd/eraser
go vet ./...
go test -race ./...
golangci-lint run
./eraser validate-brokers
```

Browser tests use an installed Chrome/Chromium; set `ERASER_TEST_CHROME` to its
absolute executable path if needed. Local browser tests skip when Chrome is
unavailable, while CI requires it. Tests exercise real MCP schema discovery,
profile isolation, batch scope and expiry, durable duplicate prevention, SMTP
limits/recovery, and the local forms with Chromium. No real mailbox or broker
is needed. Verify the demo's completed tasks, screenshots, and pending review
in the existing web UI before trying a small, explicitly approved real batch.
18 changes: 18 additions & 0 deletions examples/codex-mcp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Merge these tables into your Codex config; replace the absolute command path.
# Do not put email credentials in this file. Eraser reads its existing config.
[mcp_servers.eraser]
command = "/absolute/path/to/eraser"
args = ["mcp"]
startup_timeout_sec = 20
tool_timeout_sec = 60
default_tools_approval_mode = "approve"

[mcp_servers.eraser.tools.begin_batch]
approval_mode = "prompt"

[mcp_servers.eraser.tools.retry_action]
approval_mode = "prompt"

# To try fictional data first, change args to ["mcp", "--demo"].
# With multiple profiles, use args = ["mcp", "--profile", "your-profile-id"].
# If Chrome is not on PATH, add "--chrome-path", "/absolute/path/to/chrome".
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ go 1.26.0

require (
filippo.io/csrf v0.2.1
github.com/chromedp/cdproto v0.0.0-20260804232424-e85f50dbfd32
github.com/chromedp/chromedp v0.16.0
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-message v0.18.2
github.com/go-chi/chi/v5 v5.3.2
github.com/modelcontextprotocol/go-sdk v1.7.0
github.com/spf13/cobra v1.10.2
golang.org/x/net v0.59.0
golang.org/x/sys v0.48.0
Expand All @@ -17,20 +19,26 @@ require (
)

require (
github.com/chromedp/cdproto v0.0.0-20260804232424-e85f50dbfd32 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/go-json-experiment/json v0.0.0-20260819210203-93dd53efdd9e // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/time v0.15.0 // indirect
modernc.org/libc v1.77.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
Expand Down
Loading