Skip to main content

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.

Rendered drop page — editorial layout with serif display title, metadata, tags, a rendered table, syntax-highlighted code block, and an Open-original card

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 for current release + history and 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.

Agent Drop inbox — source filters on the left, drops list in the centre with priority annotations

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 CLI for shell / scripts (agentdrop drop, agentdrop list, agentdrop config).
  • agentdrop-mcp 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 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

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):

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_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.

Agent Drop inbox on mobile — search, state tabs, drops with priority annotations   Drop page on mobile — serif title, rendered table, syntax-highlighted code block

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 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 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.

Settings → Channels — list view showing the auto-provisioned Web Push channel marked default, with New channel / Edit / Test / Delete actions

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 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. The full request / response schema is enforced in 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

License

Agent Drop is licensed under the Apache License 2.0. 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.

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

ai_agent_drop-0.5.1.tar.gz (1.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ai_agent_drop-0.5.1-py3-none-any.whl (218.6 kB view details)

Uploaded Python 3

File details

Details for the file ai_agent_drop-0.5.1.tar.gz.

File metadata

  • Download URL: ai_agent_drop-0.5.1.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

Hashes for ai_agent_drop-0.5.1.tar.gz
Algorithm Hash digest
SHA256 5cfab53a7ad428005f2e4444a57a8e54b6f9cf407e48085729dcad8d2f3495f5
MD5 6b16fe3d9f20cda9be37cd47b2085880
BLAKE2b-256 938ef4766f7edf20f9e07bb5fced6e396c37e7b929ccec04d2fee5d1f870a1c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for ai_agent_drop-0.5.1.tar.gz:

Publisher: release.yml on Jay-uk/agent-drop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ai_agent_drop-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: ai_agent_drop-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 218.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for ai_agent_drop-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 eea55236ba737740064cdf00bb6069625276ab07b5522ce02f6dedb37a1afe8b
MD5 5caf6092bbceb64f435dd1df80f24191
BLAKE2b-256 efa7790e0fdd55b370c3878dd3eb1059b1f62a3db2c8bec5809f38e836aabfd3

See more details on using hashes here.

Provenance

The following attestation bundles were made for ai_agent_drop-0.5.1-py3-none-any.whl:

Publisher: release.yml on Jay-uk/agent-drop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page