Self-hostable notification + rendering surface for LLM agent outputs.
Project description
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.
Status: v1.5 shipped — MVP, month-one, MCP server, FTS search, routing rules, and Web Push PWA delivery are all on main. See ROADMAP.md for what's next, PRD.md for product scope.
Features
Ingestion & rendering
POST /api/v1/dropswithtitle+body+content(markdown) — drops render at a short, unguessable public URL.- Markdown via
markdown-it-pywith 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_urlif 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 explicitorderthen 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 + tagsvia 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.
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
agentdropCLI for shell / scripts (agentdrop drop,agentdrop list,agentdrop config).agentdrop-mcpserver exposescreate_dropandlist_recent_dropsas 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 viamake 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_URLto 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.mdis 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
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 withpython -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:8080for 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):
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
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:
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_URLin 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.
5. Send your first drop
From another machine (or the same one):
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 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 serveon the host and let your tailnet do TLS + auth. - Cloudflare Tunnel — point
cloudflaredathttp://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 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. EnvAGENTDROP_API_KEYkeeps 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.
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 theAGENTDROP_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 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 table for required fields.
CLI (agentdrop)
Install the CLI from PyPI and configure it once per machine:
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, add service-token credentials — they're sent as CF-Access-Client-Id / CF-Access-Client-Secret on every request:
agentdrop config --cf-access-client-id "..." --cf-access-client-secret "..."
Flag-driven drop:
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 -:
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:
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 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:
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):
{
"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:
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
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
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.
#!/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 on PATH (uv installs to ~/.local/bin).
make install
make dev
Then hit http://localhost:8080/health.
Useful targets:
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:
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. See PRD.md §8.1 for the full payload shape.
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ai_agent_drop-0.3.0.tar.gz.
File metadata
- Download URL: ai_agent_drop-0.3.0.tar.gz
- Upload date:
- Size: 1.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a2e3318e142b246765db53d151c5dde6b23d8ddf73f22ed979fc7d9ec46ab07
|
|
| MD5 |
0a6ef6a6013ee2fb52c6ae545a9fc662
|
|
| BLAKE2b-256 |
9da9dac8fc4cc212e6001803809bde6cb47a826a5af9309fb45de1ecd3f54936
|
Provenance
The following attestation bundles were made for ai_agent_drop-0.3.0.tar.gz:
Publisher:
release.yml on Jay-uk/agent-drop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_agent_drop-0.3.0.tar.gz -
Subject digest:
6a2e3318e142b246765db53d151c5dde6b23d8ddf73f22ed979fc7d9ec46ab07 - Sigstore transparency entry: 1391453156
- Sigstore integration time:
-
Permalink:
Jay-uk/agent-drop@5760e1072fa1ffa3cfc8002a74f628a62d8ae1f1 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Jay-uk
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760e1072fa1ffa3cfc8002a74f628a62d8ae1f1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ai_agent_drop-0.3.0-py3-none-any.whl.
File metadata
- Download URL: ai_agent_drop-0.3.0-py3-none-any.whl
- Upload date:
- Size: 207.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
503b5cdefbb11d9ce97c3e3f1ebc2184a48c66b09f8377ac69eca68e594532f5
|
|
| MD5 |
99b3d1a322cad60060ef051d5c966b1c
|
|
| BLAKE2b-256 |
ddb7154ae160a3727aad377694454ac0781f64fd496f3395085ebda793bfd692
|
Provenance
The following attestation bundles were made for ai_agent_drop-0.3.0-py3-none-any.whl:
Publisher:
release.yml on Jay-uk/agent-drop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_agent_drop-0.3.0-py3-none-any.whl -
Subject digest:
503b5cdefbb11d9ce97c3e3f1ebc2184a48c66b09f8377ac69eca68e594532f5 - Sigstore transparency entry: 1391453157
- Sigstore integration time:
-
Permalink:
Jay-uk/agent-drop@5760e1072fa1ffa3cfc8002a74f628a62d8ae1f1 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Jay-uk
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760e1072fa1ffa3cfc8002a74f628a62d8ae1f1 -
Trigger Event:
push
-
Statement type: