Metadata-Version: 2.3
Name: airtable-write-mcp
Version: 0.1.1
Summary: Safe Airtable writes from Claude — dry-run previews by default; bulk upsert & undo on the roadmap.
License: MIT
Keywords: airtable,mcp,model-context-protocol,claude,dry-run
Author: Moai Loops
Requires-Python: >=3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: httpx (>=0.27,<1.0)
Requires-Dist: loguru (>=0.7,<1.0)
Requires-Dist: mcp (>=1.28,<1.29)
Project-URL: Homepage, https://moailoops.com/airtable-safe-write/
Description-Content-Type: text/markdown

# airtable-write-mcp

**Safe Airtable writes from Claude — dry-run previews by default. Bulk upsert & undo on the roadmap.**

An MCP (Model Context Protocol) server that lets Claude Desktop, Claude Code, Cursor, or Cline
write to your Airtable bases *safely*: every write is previewed as a dry-run plan first, and
nothing changes in Airtable until you confirm. No LLM runs inside the server — it is a
deterministic tool server; your MCP client's model does the language work.

```text
You:    Add Acme Corp to my CRM Leads table, status New.
Claude: Here's the plan — create 1 record in Leads: {Name: "Acme Corp", Status: "New"}.
        Nothing written yet. Confirm?
You:    Yes.
Claude: Done — rec0123456789abcd created. https://airtable.com/app…/tbl…/rec…
```

## Why this server

Reading and writing Airtable from an MCP client is table stakes — the official Airtable MCP server
and several community servers already do it. What none of them offer is a **safety layer for
writes**. That is the only thing this project is about.

| Server | Write records | Bulk upsert + dedup | Dry-run preview | Per-record failure report | Undo | Auth |
|---|---|---|---|---|---|---|
| Official Airtable MCP | ✅ create + update | ❌ | ❌ | ❌ | ❌ | OAuth or PAT |
| domdomegg/airtable-mcp-server (top community) | ✅ create/update/delete | ❌ | ❌ | ❌ | ❌ | PAT |
| **airtable-write-mcp (this server, v0.1.0)** | ✅ create + update (no delete) | 🔜 planned (v0.2.0) | ✅ **default-on** | 🔜 planned (v0.2.0) | 🔜 planned (v0.3.0) | PAT |

*Incumbent capabilities audited 2026-07-03. Our row states shipped-version truth only — planned
features link to the [roadmap](#roadmap) and are never claimed before `tools/list` proves them.*

## Tools (v0.1.0)

| Tool | What it does |
|---|---|
| `list_bases` | List every base your PAT can access — call first to resolve `base_id`. |
| `list_tables` | List a base's tables and field schemas (types, writable flags, select options). |
| `list_records` | List records with a structured filter, view, and pagination (values escaped safely). |
| `create_record` | Create one record — **dry-run by default**, writes only after confirmation. |
| `update_record` | Update fields on one record (others untouched) — **dry-run by default**. |

## How write safety works

Every mutating tool defaults to `dry_run=true`:

1. **Dry-run** — the server validates your fields against the live table schema (unknown fields
   get a did-you-mean, read-only fields are skipped and reported, select options are checked) and
   returns a `would_write` plan plus a single-use `confirm_token`. **Zero writes happen.**
2. **Confirm** — the model re-calls the tool with that token and `dry_run=false`. The server
   verifies the plan hash still matches the arguments (any drift aborts), then writes.
3. Tokens are single-use and expire after a short TTL; a replayed or stale token is refused with
   a structured error.

There is no way to skip the preview by accident — you'd have to set an opt-out env var yourself
(see [Configuration](#configuration)).

## Install

### 1. Create an Airtable Personal Access Token

Create a PAT at [airtable.com/create/tokens](https://airtable.com/create/tokens) with these scopes,
granted to the bases you want to use:

- `data.records:read`
- `data.records:write`
- `schema.bases:read`

Your token stays on your machine — it is read from an env var (or the macOS Keychain) at tool-call
time, never bundled, logged, or echoed back in errors.

### 2. Add the server to your MCP client

The same `mcpServers` JSON shape works everywhere:

```json
{
  "mcpServers": {
    "airtable-write-mcp": {
      "command": "uvx",
      "args": ["airtable-write-mcp"],
      "env": { "AIRTABLE_PAT": "<your PAT>" }
    }
  }
}
```

| Client | Where to put it |
|---|---|
| Claude Code | `.mcp.json` at your repo root — or `claude mcp add airtable-write-mcp -e AIRTABLE_PAT=<pat> -- uvx airtable-write-mcp` |
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) |
| Cursor | `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) |
| Cline | MCP Servers settings UI (`cline_mcp_settings.json`) |

Restart the client after editing; the server appears with its five tools.

**macOS Keychain instead of a plaintext PAT** — store the token once, keep the config secret-free:

```bash
security add-generic-password -U -s airtable_mcp_key -a "$USER" -w "<your PAT>"
```

then use `"env": { "AIRTABLE_PAT_KEYCHAIN_SERVICE": "airtable_mcp_key" }` in the snippet instead
of `AIRTABLE_PAT`. The Keychain is read lazily on your first tool call — never at client startup —
so adding the server won't trigger a Keychain prompt until you actually use it.

**Local development** — run from a checkout instead of PyPI:

```bash
git clone https://github.com/archypelago/airtable-write-mcp && cd airtable-write-mcp
make install
poetry env info --path   # → <venv>
```

then point `command` at `<venv>/bin/airtable-write-mcp` (the direct binary avoids slow
`poetry run` startup shims that some clients time out on).

### 3. First run (step 0)

Ask something like *"List my Airtable bases."* Your client will ask permission for each tool the
first time it's used — choose **Always allow** for the three read tools (`list_bases`,
`list_tables`, `list_records`). Writes then cost at most two approvals: the dry-run and the
confirm — that's the safety feature, not friction.

## Configuration

| Env var | Required | Meaning |
|---|---|---|
| `AIRTABLE_PAT` | one of these two | Your Airtable Personal Access Token. |
| `AIRTABLE_PAT_KEYCHAIN_SERVICE` | one of these two | macOS: name of a Keychain generic-password item holding the PAT (read lazily at first tool call). Ignored when `AIRTABLE_PAT` is set. |
| `AIRTABLE_RPS` | no | Lower the per-base request rate below the default 4 rps (values above 4 clamp to 4). |
| `CONFIRM_SKIP_UNDER` | no | **Opt-out, default 0 (off):** skip the dry-run/confirm round-trip for writes touching fewer than N records (hard cap 25). Leave unset unless you know you want this. |

The server starts and answers `tools/list` with **no** secrets present — a missing PAT fails the
individual tool call with a structured setup error naming the exact env var, never the server.

## Roadmap

| Version | Adds | Status |
|---|---|---|
| **v0.1.0** | The five tools above; dry-run/confirm write safety; schema validation; rate limiting + 429/5xx resilience | ✅ shipped |
| **v0.2.0** | `bulk_upsert` — paste 200 rows in chat, dedup against existing records by key field, batched under the rate limit, per-record report (N created / M updated / K failed, 0 silently dropped) | 🔜 next |
| **v0.3.0** | `list_operations` + `undo_operation` — an operation journal and conflict-safe undo | planned |
| Hosted | OAuth connect (no token handling), works in Claude web/mobile, retained audit + undo history | planned, gated on demand |

## Development

```bash
make install    # poetry lock + install (self-provisions the venv)
make test       # pytest — in-memory MCP client harness, no network
make lint       # ruff check + format check
make check      # the full pre-merge gate
make run        # start the stdio server locally
make build      # wheel + sdist; smoke with: uvx --from dist/airtable_write_mcp-*.whl airtable-write-mcp
```

Tests talk to the server through a real in-memory MCP client session — no subprocess, no model
mocking. Design rules that will not change: no LLM calls inside the server, stderr-only logging
(stdout belongs to the protocol), and every mutating tool defaults to dry-run.

## License

[MIT](LICENSE)

