Metadata-Version: 2.4
Name: ai-agent-drop
Version: 0.5.1
Summary: Self-hostable notification + rendering surface for LLM agent outputs.
Project-URL: Homepage, https://github.com/Jay-uk/agent-drop
Project-URL: Repository, https://github.com/Jay-uk/agent-drop
Project-URL: Issues, https://github.com/Jay-uk/agent-drop/issues
Author: James Cadman
License: Apache-2.0
License-File: LICENSE
Keywords: agents,llm,markdown,notifications,push
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.12
Requires-Dist: alembic<2.0,>=1.13
Requires-Dist: apprise>=1.7
Requires-Dist: apscheduler<4,>=3.10
Requires-Dist: argon2-cffi>=23
Requires-Dist: cryptography>=42
Requires-Dist: fastapi<0.137,>=0.115
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: itsdangerous<3.0,>=2.2
Requires-Dist: jinja2<4.0,>=3.1
Requires-Dist: linkify-it-py<3.0,>=2.0
Requires-Dist: markdown-it-py[linkify,plugins]<5.0,>=3.0
Requires-Dist: mcp>=1.0
Requires-Dist: mdit-py-plugins<1.0,>=0.4
Requires-Dist: pydantic-settings<3.0,>=2.4
Requires-Dist: pygments<3.0,>=2.18
Requires-Dist: python-multipart<1.0,>=0.0.9
Requires-Dist: pywebpush<3,>=2.0
Requires-Dist: sqlalchemy<3.0,>=2.0
Requires-Dist: structlog>=24
Requires-Dist: tomli-w<2,>=1
Requires-Dist: typer<1,>=0.9
Requires-Dist: uvicorn[standard]<0.47,>=0.30
Provides-Extra: dev
Requires-Dist: bandit[toml]>=1.7; extra == 'dev'
Requires-Dist: httpx<1.0,>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pip-audit>=2.4; extra == 'dev'
Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'dev'
Requires-Dist: pytest<10.0,>=8.0; extra == 'dev'
Requires-Dist: ruff<1.0,>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# Agent Drop

A self-hosted notification + rendering surface for LLM agent outputs.

Agent Drop takes an HTTP POST from an agent, stores it as a rendered markdown page at a short URL, and fires a push notification to the user with a tappable link. It sits alongside Obsidian, Notion, or whatever else — it is the "look at this now" surface, not a replacement for canonical storage.

<p align="center">
  <img src="./docs/screenshots/drop-page.png" alt="Rendered drop page — editorial layout with serif display title, metadata, tags, a rendered table, syntax-highlighted code block, and an Open-original card" width="720">
</p>

**Status:** v1.5 milestone shipped — MVP, month-one, MCP server, FTS search, routing rules, and Web Push PWA delivery are all on `main`. Container images are published at `ghcr.io/jay-uk/agent-drop:latest`. See [`CHANGELOG.md`](./CHANGELOG.md) for current release + history and [`ROADMAP.md`](./ROADMAP.md) for what's next.

## Features

**Ingestion & rendering**

- `POST /api/v1/drops` with `title` + `body` + `content` (markdown) — drops render at a short, unguessable public URL.
- Markdown via `markdown-it-py` with GFM, task lists, footnotes, admonitions, and Mermaid diagrams (client-side).
- Syntax-highlighted code blocks; inline link clicks are prevented so tap-from-notification is the intended way to leave the page (the "Open original" card carries the `external_url` if set).

**Notification delivery**

- **Web Push (`web_push`)** — the recommended default: pushes land on any device subscribed as a PWA (iOS 16.4+, Android Chrome). No third-party app in the loop, so "tap the notification → land on the drop page" is genuinely one tap. Auto-configured on first launch when VAPID keys are set.
- **Alternate drivers** — Pushover, ntfy (hosted or self-hosted), Apprise (~80 services), generic JSON webhook. Mix and match; one marked as default receives everything unless a routing rule says otherwise.
- **Routing rules** — map `(source, priority)` pairs onto specific channels at Settings → Routing. Most-specific rule wins; tie-break by explicit `order` then age.
- **Retention sweep** — soft-delete at 30 days, hard-delete at 90 days (configurable). Runs in-process via APScheduler.

**Inbox & UI**

- **Inbox** at `/` — paginated list of drops, desktop and mobile layouts, newest first.
- **Full-text search** over `title + body + content + source + tags` via SQLite FTS5 (Porter stemmer, diacritic-folding).
- **Pin / archive / mark read** per drop; state is reflected in both the inbox and the public drop page.
- **PWA shell** — add to home screen on iOS or Android for a standalone app with splash + theme colour.
- **Settings area** for channels, routing rules, API keys, devices (PWA push subscriptions), all session-auth gated.

<p align="center">
  <img src="./docs/screenshots/inbox.png" alt="Agent Drop inbox — source filters on the left, drops list in the centre with priority annotations" width="720">
</p>

**Auth**

- **Bearer API keys** for agent POSTs — env bootstrap key plus UI-issued per-agent keys with labels, last-used timestamps, and revocation.
- **Session auth** for the settings UI, single admin user, 14-day session cookie.
- **Cloudflare Access** service-token support in the CLI / MCP server for zero-trust deployments.

**Agent-facing tooling**

- [`agentdrop`](https://pypi.org/project/ai-agent-drop/) CLI for shell / scripts (`agentdrop drop`, `agentdrop list`, `agentdrop config`).
- [`agentdrop-mcp`](https://modelcontextprotocol.io/) server exposes `create_drop` and `list_recent_drops` as native tools in Claude Desktop / Claude Code.

**Deployment**

- Single Docker image published to GHCR (`ghcr.io/jay-uk/agent-drop`, linux/amd64). ARM64 is supported for local dev via `make docker-up`.
- SQLite by default (WAL mode), Postgres optional via `AGENTDROP_DATABASE_URL`.
- Works behind Cloudflare Tunnel / Access or any reverse proxy; set `AGENTDROP_BASE_URL` to the public hostname and push payloads carry the right one-tap URL.

## First-time setup

The goal of this walkthrough is: running server → notification landing on your phone, in about five minutes. It assumes Docker Desktop (or equivalent) is running locally.

> **New to self-hosted services or AI agents?** [`docs/QUICKSTART.md`](./docs/QUICKSTART.md) is the same flow, broken into smaller steps with every dependency called out. Use that one and come back here once Agent Drop is up.

### 1. Clone + copy env

```sh
git clone https://github.com/Jay-uk/agent-drop.git
cd agent-drop
cp .env.example .env
```

### 2. Fill in `.env`

Required:

- `AGENTDROP_API_KEY` — a bootstrap bearer token. Generate one with `python -c 'import secrets; print(secrets.token_urlsafe(32))'`.
- `AGENTDROP_ADMIN_PASSWORD` — the password you'll use to log into the Settings UI.
- `AGENTDROP_BASE_URL` — the public URL of the server as seen from the outside. `http://localhost:8080` for pure local testing; when you put the server behind Cloudflare / Tailscale / any reverse proxy, set this to the hostname your device will actually resolve (this is the URL baked into push notifications — if it's wrong, tap-throughs go nowhere).

Recommended (enables Web Push, the zero-config delivery path):

```sh
make vapid-keys
```

…then paste the three printed lines (`AGENTDROP_VAPID_PUBLIC_KEY`, `AGENTDROP_VAPID_PRIVATE_KEY`, `AGENTDROP_VAPID_SUBJECT`) into `.env`. These are server identity for signing Web Push deliveries; rotate only if you have to (every subscribed device is pinned to the public key and rotation forces everyone to re-subscribe).

### 3. Start the server

```sh
make docker-up
```

On first launch, if VAPID is set, the app auto-creates a default `web_push` channel so push delivery works as soon as a device subscribes. Migrations run automatically. Data persists in `./data/` (SQLite file).

Verify:

```sh
curl -sS http://localhost:8080/health
# {"status":"ok","version":"..."}
```

### 4. Install as a PWA and subscribe

Agent Drop ships a Web App Manifest and a service worker — the app is designed to be added to your home screen on the device that will receive notifications.

- **iOS:** open `AGENTDROP_BASE_URL` in Safari → Share → "Add to Home Screen". You must launch from the home-screen icon (not a Safari tab) to subscribe to push — this is an iOS 16.4+ platform rule, not an Agent Drop constraint.
- **Android:** open in Chrome → menu → "Install app".

From the installed PWA, log in with your admin password, then go to **Settings → Devices** and click **Enable on this device**. Grant notification permission. That's it — the device is subscribed.

<p align="center">
  <img src="./docs/screenshots/inbox-mobile.png" alt="Agent Drop inbox on mobile — search, state tabs, drops with priority annotations" width="300">
  &nbsp;
  <img src="./docs/screenshots/drop-page-mobile.png" alt="Drop page on mobile — serif title, rendered table, syntax-highlighted code block" width="300">
</p>

### 5. Send your first drop

From another machine (or the same one):

```sh
curl -sS -X POST "$AGENTDROP_BASE_URL/api/v1/drops" \
  -H "Authorization: Bearer $AGENTDROP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Hello from Agent Drop",
    "body":  "Tap to open the rendered drop page.",
    "content": "## First drop\n\nIf you are reading this on your phone after tapping a notification, everything works."
  }'
```

Response is `201 Created` with `{ "id", "url", "created_at", "warnings": [] }`. The notification should land on the subscribed device within a second or two; tapping it opens the drop URL.

From here: install the CLI (`pipx install ai-agent-drop`) to drop from your shell, or register the MCP server with Claude Desktop / Claude Code — see the "Sending drops" section below.

### Using an alternate notification backend

If you'd rather route to Pushover / ntfy / Apprise / a webhook instead of (or alongside) Web Push, log into Settings → Channels and add one. Any channel you mark as default takes precedence over the auto-provisioned Web Push one. See [Notification channels](#notification-channels) for per-driver details.

## Remote access

`compose.yaml` binds the container to `127.0.0.1:8080` on the host — the app is not reachable from your LAN or the public internet by default. Reach it remotely with one of:

- **Tailscale** — run `tailscale serve` on the host and let your tailnet do TLS + auth.
- **Cloudflare Tunnel** — point `cloudflared` at `http://127.0.0.1:8080`; combine with Cloudflare Access for SSO.
- **nginx / Caddy reverse proxy** — terminate TLS on the host and proxy to `127.0.0.1:8080`.

Whichever you pick, set `AGENTDROP_BASE_URL` to the public hostname so push payloads carry the right tap-through URL. The three required env vars are `AGENTDROP_ADMIN_PASSWORD` (Settings UI login), `AGENTDROP_SECRET_KEY` (stable session cookies across restarts), and `AGENTDROP_API_KEY` (bootstrap bearer token for agents).

For pure local-HTTP testing without any TLS proxy, set `AGENTDROP_INSECURE_COOKIES=1` in `.env` to allow session cookies over plain HTTP. Don't use this in production — it disables the cookie hardening that keeps sessions safe over the open internet.

## Configuration

Configuration is split between environment variables (server infrastructure — loaded from `.env` by `docker compose`) and the settings UI (notification channels, API keys, routing rules). See [`.env.example`](./.env.example) for the env-var list and **Settings** in the running app for everything else.

| Variable | Required | Purpose |
| --- | --- | --- |
| `AGENTDROP_BASE_URL` | yes | Public URL included in push notifications and returned from `POST /api/v1/drops`. |
| `AGENTDROP_API_KEY` | yes | Bootstrap bearer token. Once the app is up, prefer UI-issued per-agent keys at Settings → API keys. |
| `AGENTDROP_ADMIN_PASSWORD` | yes | Password for the single admin session used to reach Settings. |
| `AGENTDROP_SECRET_KEY` | no | Session-cookie signing key. Auto-generated per restart if unset (logs invalidate on restart). |
| `AGENTDROP_INSECURE_COOKIES` | no | Set to `1` to allow session cookies over plain HTTP. Local dev only — never set in production. |
| `AGENTDROP_DATA_DIR` | no | SQLite + any future file storage. Defaults to `/data` (bind-mounted in compose). |
| `AGENTDROP_DATABASE_URL` | no | Override the default SQLite URL (e.g. to use PostgreSQL). |
| `AGENTDROP_VAPID_PUBLIC_KEY` | for `web_push` | EC P-256 public key (base64url). Generate with `make vapid-keys`. |
| `AGENTDROP_VAPID_PRIVATE_KEY` | for `web_push` | EC P-256 private key (base64url). Treat as a credential. |
| `AGENTDROP_VAPID_SUBJECT` | for `web_push` | `mailto:` or `https:` contact URI for the operator (push services use this). |

### Settings UI

All per-install configuration beyond the env-var table above lives behind admin auth at `/settings`:

- **Settings → Channels** — add / edit / delete notification channels; per-driver secret redaction; test-fire button. Drivers: `web_push`, `pushover`, `ntfy`, `apprise`, `webhook`.
- **Settings → Routing** — map `(source, priority)` pairs onto specific channels. Most-specific rule wins.
- **Settings → API keys** — mint per-agent bearer tokens with labels; revoke / list; tracks `last_used_at`. Env `AGENTDROP_API_KEY` keeps working alongside these as the bootstrap key.
- **Settings → Devices** — list and manage PWA push subscriptions; one-click enable/disable on the device you're currently viewing from; per-device remove; "Send test push" to verify the loop.

<p align="center">
  <img src="./docs/screenshots/settings.png" alt="Settings → Channels — list view showing the auto-provisioned Web Push channel marked default, with New channel / Edit / Test / Delete actions" width="720">
</p>

### Notification channels

All channels are managed at **Settings → Channels** in the running app — there is no env-var path for provider credentials.

- **Web Push (`web_push`) — recommended default.** Fans out to all PWA-subscribed devices; requires the `AGENTDROP_VAPID_*` env vars above. Auto-configured on first launch when VAPID is set, so most operators never need to touch the channel form.

The following drivers are available as alternatives if you'd rather route to an existing notification tool:

- **Pushover** — register an application at [pushover.net/apps](https://pushover.net/apps) for the API token; your user key is on the Pushover dashboard. Priority mapping: `low → -1`, `normal → 0`, `high → 1`, `urgent → 2` (emergency, with retry+expire).
- **ntfy** — hosted (`ntfy.sh`) or self-hosted. Topic names should be unguessable. Priority mapping: `low → 2`, `normal → 3`, `high → 4`, `urgent → 5`.
- **Apprise** — one channel can fan out to any of the ~80 services Apprise supports (Slack, Discord, Telegram, email, …) via its URL syntax.
- **Webhook** — generic JSON POST to any URL, with optional bearer / Basic auth.

Routing rules at **Settings → Routing** map drop `(source, priority)` pairs onto specific channels — e.g. send all `urgent` drops to Pushover and everything else to `web_push`.

## Sending drops

Four ways to POST to `/api/v1/drops` — pick whichever fits the caller. All four accept the same payload shape; see the [API surface](#api-surface) table for required fields.

### CLI (`agentdrop`)

Install the CLI from PyPI and configure it once per machine:

```sh
pipx install ai-agent-drop   # or: uv tool install ai-agent-drop
agentdrop config --base-url "https://drop.example.com" --api-key "agdr_..."
```

Config is written to `~/.config/agentdrop/config.toml` (`XDG_CONFIG_HOME` honoured) with mode `0600`. The API key is never echoed back unmasked. Per-invocation `--base-url` / `--api-key` flags override the file.

For self-hosted instances behind [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/), add service-token credentials — they're sent as `CF-Access-Client-Id` / `CF-Access-Client-Secret` on every request:

```sh
agentdrop config --cf-access-client-id "..." --cf-access-client-secret "..."
```

Flag-driven drop:

```sh
agentdrop drop \
  --title "Weekly market scan" \
  --body  "Report ready, 3 items flagged" \
  --content-file ./report.md \
  --source weekly-scan \
  --tags research,markets \
  --priority normal \
  --external-url "obsidian://open?vault=Personal&file=Reports/2026-04-19"
```

Pipe content from another process with `--content -`:

```sh
generate-report.sh | agentdrop drop \
  --title "Weekly market scan" \
  --body  "Report ready, 3 items flagged" \
  --source weekly-scan \
  --content -
```

On success, `agentdrop drop` prints just the drop URL on stdout (pipe-friendly). Pass `--json` to get the full server response. Failures (HTTP 4xx, network errors, missing config) exit non-zero with a one-line error on stderr.

Other subcommands:

```sh
agentdrop list --limit 10 --state unread   # compact table, newest first
agentdrop list --json                      # raw server JSON
agentdrop config                           # print path + masked summary
agentdrop version                          # installed package version
```

### MCP server (`agentdrop-mcp`)

For Claude Desktop and Claude Code users, Agent Drop ships an [MCP](https://modelcontextprotocol.io/) server that exposes `create_drop` and `list_recent_drops` as native tools. Installing `ai-agent-drop` from PyPI puts **both** `agentdrop` and `agentdrop-mcp` on your `PATH`:

```sh
pipx install ai-agent-drop   # or: uv tool install ai-agent-drop
agentdrop-mcp --version
```

The MCP server reuses the same `~/.config/agentdrop/config.toml` the CLI writes, so if you've already run `agentdrop config --base-url ... --api-key ...` there's nothing more to do — just register the server with your MCP client.

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "agentdrop": {
      "command": "agentdrop-mcp",
      "env": {
        "AGENTDROP_BASE_URL": "https://drop.example.com",
        "AGENTDROP_API_KEY": "agdr_..."
      }
    }
  }
}
```

Env vars override the config file, which matters for multi-host setups where the TOML file wouldn't be present. CF Access service tokens use `AGENTDROP_CF_ACCESS_CLIENT_ID` / `AGENTDROP_CF_ACCESS_CLIENT_SECRET`.

**Claude Code**:

```sh
claude mcp add agentdrop agentdrop-mcp
# then ensure your env / ~/.config/agentdrop/config.toml is populated
```

Once registered, just ask Claude:

> "Drop a summary of today's commits to my phone — title 'Daily commits', body '5 PRs merged', content the bullet list above."

Claude will call `create_drop` and hand you back the public drop URL.

### Bash / curl

```sh
curl -sS -X POST "$AGENTDROP_URL/api/v1/drops" \
  -H "Authorization: Bearer $AGENTDROP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Weekly market scan",
    "body": "3 items flagged, tap to read",
    "content": "# Results\n\n- AAPL up 4.2%\n- BNANA flat\n- CHERRY +28%",
    "source": "weekly-market-scan",
    "tags": ["markets", "research"],
    "priority": "high"
  }'
```

### Python

```python
import os, httpx

httpx.post(
    f"{os.environ['AGENTDROP_URL']}/api/v1/drops",
    headers={"Authorization": f"Bearer {os.environ['AGENTDROP_API_KEY']}"},
    json={
        "title": "Weekly market scan",
        "body": "3 items flagged, tap to read",
        "content": report_markdown,
        "source": "weekly-market-scan",
        "external_url": "obsidian://open?vault=Work&file=Reports/2026-04-19",
    },
).raise_for_status()
```

### Claude Code routine

Point a scheduled routine at Agent Drop. The routine's final step is the POST — everything before it is domain work. Claude Code handles the scheduling; Agent Drop handles the tap-to-read.

```sh
#!/usr/bin/env bash
set -euo pipefail

report=$(claude -p "Run the weekly market scan and produce a markdown report.")

curl -sS -X POST "$AGENTDROP_URL/api/v1/drops" \
  -H "Authorization: Bearer $AGENTDROP_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg title "Weekly market scan" \
    --arg body  "Report ready, 3 items flagged" \
    --arg content "$report" \
    '{title:$title, body:$body, content:$content, source:"weekly-scan", priority:"normal"}')"
```

The response gives you `{ "id": "...", "url": "...", "created_at": "...", "warnings": [] }`. The `url` is the tap target already sent in the push — you can also log it, post it into Obsidian, or chain it into another routine.

## Dev quickstart (no Docker)

Requires Python 3.12 and [`uv`](https://docs.astral.sh/uv/) on `PATH` (uv installs to `~/.local/bin`).

```sh
make install
make dev
```

Then hit `http://localhost:8080/health`.

Useful targets:

```sh
make help           # list all targets
make test           # pytest
make lint           # ruff check + format --check
make fmt            # ruff format + check --fix
make docker-build   # local arm64 image tagged agentdrop:dev
make docker-release # multi-arch (amd64 + arm64) release image
```

Generated static assets are committed; rebuild them with the relevant script when the design changes:

```sh
uv run scripts/gen_pygments_css.py   # syntax-highlighting stylesheet
uv run scripts/gen_pwa_icons.py      # PWA icon set (192/512/180-apple/maskable)
```

## API surface

### Drops (bearer-auth)

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/v1/drops` | Create a drop, fire push. Returns id, url, created_at, warnings. |
| `GET` | `/api/v1/drops` | Paginated list, filter by source / tag / state; full-text search via `?q=`. |
| `GET` | `/api/v1/drops/{id}` | Full drop detail JSON. |
| `PATCH` | `/api/v1/drops/{id}` | Toggle pinned / archived / read. |
| `DELETE` | `/api/v1/drops/{id}` | Soft delete. |

Schema-required fields on `POST /api/v1/drops`: `title` (≤250 chars), `body` (≤1024 chars), `content` (markdown). Optional: `source`, `tags[]`, `priority` (`low`/`normal`/`high`/`urgent`), `external_url`, `channel`, `expires_in_days`. The full request / response schema is enforced in [`src/agentdrop/schemas/drops.py`](./src/agentdrop/schemas/drops.py) and surfaced via the auto-generated OpenAPI doc at `/docs` on a running server.

### Public surface

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/d/{short_id}` | Rendered drop page (the tap target). Unguessable short id; no auth by default. |
| `GET` | `/health` | Liveness probe. |

### Web Push (session-auth)

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/pwa/vapid-public-key` | Server's VAPID public key for `pushManager.subscribe`. Returns `null` when unconfigured. |
| `POST` | `/pwa/subscribe` | Register a `PushSubscription` for this device. Upserts by endpoint. |
| `POST` | `/pwa/unsubscribe` | Delete a subscription row by endpoint. |

### Web UI (session-auth)

The inbox at `/`, the drop actions (`/drops/{id}/pin` etc.), and the `/settings/*` routes (channels, routing, api-keys, devices) are all admin-session-gated. Log in with `AGENTDROP_ADMIN_PASSWORD` at `/login`.

## References

- [`CHANGELOG.md`](./CHANGELOG.md) — per-release notes, version history.
- [`ROADMAP.md`](./ROADMAP.md) — shipped milestones and what's planned next.
- [`docs/QUICKSTART.md`](./docs/QUICKSTART.md) — extended first-time setup walkthrough.
- [`docs/DEPLOYMENT.md`](./docs/DEPLOYMENT.md) — operational runbook for production deploys.
- [`CLAUDE.md`](./CLAUDE.md) — guidance for Claude Code when working in this repo.

## License

Agent Drop is licensed under the [Apache License 2.0](./LICENSE). You may use, modify, distribute, and use the software commercially, subject to the conditions in the license (preserve copyright + license notice, state significant changes). Contributions are accepted under the same terms.

Releases up to and including `v0.3.0` were published under the MIT License; those artifacts remain MIT-licensed where they were originally distributed. The relicense to Apache 2.0 takes effect from the next release onward.
