sheet-mcp
by Trapa-Eureka
README.md
# sheet_mcp
An **automated notification-sending MCP server** for Philippine SMEs, using a Google Spreadsheet as a database.
- It reads row data from a sheet, merges it into a template, sends a notification to the recipient, and writes the send status back to the sheet.
- **The v0.1 sending channel is email.** SMS (via a Philippine gateway such as Semaphore) will be added as an adapter in v0.2, once the Sender ID registration issue is sorted out. The channel sits behind the `NotificationProvider` interface from the start, so the pipeline code won't change when SMS is added.
- This is the first vertical slice validating the MCP automation core's shared capability (sheet integration → notification sending). Once validated, it will be folded into the core.
## Key Features (v0.1)
### 4 MCP Tools (`docs/DESIGN.md` §5)
| Tool | What it does |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| `read_rows` | Reads and returns target rows per the notify_config convention (filter applied, up to 200-row preview) |
| `preview_messages` | Previews the actual rendered message list + missing-value/duplicate warnings. **No sending.** |
| `send_notifications` | Sends email after confirmation. Without both safeguards below, returns only a dry-run result. |
| `get_send_log` | Queries send history via cursor-based pagination, newest first (200 by default, 1000 max). |
### Safeguards
- **No live sending without dual confirmation**: a real email only goes out when **both** the MCP tool call's `confirm: true` **and** the process environment variable `SEND_MODE=live` are present — this guards against mistakes by an autonomous agent.
- **Duplicate-send prevention (idempotency)**: the same row + the same template combination is sent only once. A 3-step `claim → send → commit/release` flow atomically records whether a send happened, so running the same command multiple times — or the process dying mid-run — never causes a double send (local SQLite `SendLog`, `docs/DESIGN.md` §3/§6).
- **Mass-misfire prevention**: if the number of rows passing the filter exceeds 1,000, a live send sends nothing and aborts immediately (prevents partial-send incidents, `MAX_PIPELINE_ROWS`).
- **External API timeouts**: both Google Sheets and Resend calls have a default 30-second timeout, so a network failure can't hang the whole pipeline indefinitely.
- **User data is never touched**: sheet writes are limited to the 4 status columns (`_send_status`/`_sent_at`/`_message_id`/`_error`).
- **Recovery that can't run automatically**: if the process dies while a claim is held, an autonomous agent cannot reclaim it by itself — only a human, via the operational CLI (`sheet-mcp-recover`, read-only by default + audit log), can reclaim it.
### Sheet Convention
Just add a `notify_config` tab (send settings: data tab name, recipient column, subject/body templates, optional filter) and a data tab (row 1 = header = template variable names) to a single Google Sheet — no separate database or schema migration needed. See "Example Sheet Template" below for the full convention and a minimal example.
### Two Installation Methods
- **Clone the repo** (for development/contribution): commit `.mcp.json` and share it with the team.
- **`npx sheet-mcp`** (use it without cloning): register it by passing environment variables directly via `claude mcp add`'s `-e` flag. See "Setup Procedure" below and `docs/DESIGN.md` §8.
## Documentation Map
> The relative paths below are only valid if you've cloned the repository or are viewing the
> [GitHub repository](https://github.com/Trapa-Eureka/sheet_mcp) — the package installed via
> `npx sheet-mcp` does not include `docs/` (docs/ADVERSARIAL_REVIEW_004.md AR-027).
| Document | Content | When to read it |
| ------------------ | ------------------------------------------------------------------------ | ------------------------------------------------- |
| `CLAUDE.md` | Agent steering file — stack, commands, conventions, guardrails | At the start of every agent session (auto-loaded) |
| `docs/SPEC.md` | Product spec — background, goals/non-goals, scenarios, roadmap | Before discussing features/scope |
| `docs/DESIGN.md` | Technical design — architecture, interfaces, sheet convention, MCP tools | Required reading before implementation |
| `docs/TESTING.md` | Test strategy — mock setup, edge cases, gates | Before writing tests |
| `docs/TASKS.md` | Task backlog — agent execution units, completion criteria | When assigning work |
| `docs/WORKFLOW.md` | AI-native development approach — the rules that run this repo | Once at the start + as an ongoing reference |
## Development Approach
This project proceeds in the order **docs → agent implementation → verification** (see `docs/WORKFLOW.md`).
A human (Jin) owns spec/design/review/live-send approval, and code is written by a Claude Code agent, one task at a time from `docs/TASKS.md`. Every task's shared completion condition is that `npm run check` passes.
## Quickstart (Development/Testing)
```bash
npm install
npm run check # typecheck + lint + format:check + test — the shared agent/human gate
npm run dev # runs the MCP server over stdio (.env secrets required — see "Setup Procedure" below)
```
## Setup Procedure (Trying it with a real sheet/email)
1. Copy `.env.example` to `.env` and fill in `GOOGLE_SERVICE_ACCOUNT_JSON`/`RESEND_API_KEY`/`MAIL_FROM`.
2. Build a Google Sheet per "Example Sheet Template" below, share it with your service account's email as an editor, then set `SMOKE_SHEET_ID=<sheet ID>` in `.env`.
3. Run `npm run smoke` to check the preview (the default is always a dry run — nothing is sent).
4. To actually send, run `SEND_MODE=live SMOKE_CONFIRM_SEND=1 npm run smoke` (only sends if exactly one row is targeted).
5. In Claude Code, open this repo and confirm the `sheet-mcp` connection with `/mcp` (`.mcp.json` is committed, see `docs/DESIGN.md` §8).
### Using `npx sheet-mcp` (no clone required)
The package is published on npm, so you can register it directly with Claude Code without cloning this repo. Instead of a `.env` file, pass your credentials directly as `-e` flags on `claude mcp add` — see `docs/DESIGN.md` §8-B for why a `.env` file doesn't work reliably for this path.
```bash
claude mcp add sheet-mcp --scope local \
-e GOOGLE_SERVICE_ACCOUNT_JSON=<absolute path to your service-account JSON file> \
-e RESEND_API_KEY=<your Resend API key> \
-e MAIL_FROM=<your verified sending address> \
-- npx -y sheet-mcp
```
**Worked example** — this is exactly the same command with every value filled in, so you can see what a real, working setup looks like. Copy it and swap in your own values:
```bash
claude mcp add sheet-mcp --scope local \
-e GOOGLE_SERVICE_ACCOUNT_JSON=/Users/jin/keys/sheet-mcp-service-account.json \
-e RESEND_API_KEY=re_AbCdEfGh_1234567890abcdefghij \
-e MAIL_FROM=notify@updates.example.com \
-- npx -y sheet-mcp
```
What to change for your own setup:
- **`GOOGLE_SERVICE_ACCOUNT_JSON`** — the absolute path to the service-account key JSON file you downloaded from Google Cloud Console (IAM & Admin → Service Accounts → Keys → Add Key → JSON). It must be an **absolute** path: `npx` runs from wherever Claude Code happens to be running, not from this repo, so a relative path like `./service-account.json` won't resolve reliably.
- **`RESEND_API_KEY`** — your own key from resend.com → API Keys.
- **`MAIL_FROM`** — an address at a domain you've verified with Resend (resend.com → Domains → Add Domain, then add the DNS records it gives you). If you don't have a domain yet, you can start with Resend's test-only address `onboarding@resend.dev` to try things out — but that address can only send to the email address on your own Resend account until you verify a real domain.
- **`sheet-mcp`** (the first argument) — the name this server is registered under in Claude Code; change it if you want something else.
- **`--scope local`** — keep this as `local` (or `user`) when passing real secrets. Never use `--scope project` with real credentials: that scope commits `.mcp.json` to git, which would leak your secrets.
Once registered, confirm the connection inside Claude Code with `/mcp`.
## Example Sheet Template
One `notify_config` tab (column A = key, column B = value) and one data tab — see `docs/DESIGN.md` §2 for the full key list and rules.
**Minimal `notify_config` tab**
| A | B |
| ------------------ | ------------------------------------- |
| `data_tab` | `customers` |
| `id_column` | `customer_id` |
| `recipient_column` | `email` |
| `channel` | `email` |
| `subject_template` | `[{{shop}}] Payment notice` |
| `body_template` | `Hi {{name}}, please pay {{amount}}.` |
| `filter_column` | `status` |
| `filter_value` | `unpaid` |
**Data tab**: row 1 is the header (= template variable names), data starts at row 2. `fixtures/sheets/collections.json` is a real example (12 rows, a mixed Tagalog/English outstanding-balance scenario) — move it into a Google Sheet with the same columns and it's ready to use for a smoke test. Send results are automatically recorded at the end of this tab in 4 columns — `_send_status`/`_sent_at`/`_message_id`/`_error` — and user data columns are never modified.
## Operations — Existing DB Upgrade / Stale Claim Recovery
- **Upgrading an existing `sendlog.db`**: the next time you run the server (`npm run dev`) or the smoke script (`npm run smoke`) with the newer code, `SqliteSendLog` auto-detects the old DB schema and migrates it losslessly to the new schema (the original is preserved in a `send_log_v1_backup_*` table). There's nothing a human needs to do. See `docs/DESIGN.md` §6 for details.
- **When a claim has been sitting for a long time** (e.g., the process died without a clean shutdown): never touch the DB file directly — query it first. In a repo-clone dev environment: `npm run recover:stale-claim -- --db ./data/sendlog.db --sheet-id <id> --tab <tab> --row-key <key> --template-hash <hash>`. If installed via `npx sheet-mcp`: `npx sheet-mcp-recover --db ... --sheet-id ... --tab ... --row-key ... --template-hash ...` (same arguments; read-only by default, deletes nothing). To actually reclaim it, add `--older-than-ms` and `--confirm`. For full options and safeguards, see the header comment in `src/cli/recoverStaleClaim.ts` and `docs/DESIGN.md` §6.
## Status
Progress isn't tracked manually here — the single source of truth is each task's status (`DONE(date)`/`TODO`) in `docs/TASKS.md`. Duplicating it in the README makes it easy to forget to update on the next task's completion (`docs/ADVERSARIAL_REVIEW_002.md` AR-010).
Whether this is a fully working product end to end, including the MCP tools, is determined by whether T8–T10 are `DONE`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues