Skip to main content

Agent Center

CI License: MIT Python 3.11+

A self-hostable notification layer for AI agents. One feed for everything your agents did — and everything waiting on you.

Agents open pull requests, file tickets, send messages, run on schedules. What they lack is a decent way to tell you about it: pasting into a chat channel doesn't group, doesn't distinguish "FYI" from "I'm blocked", and buries the one thing that needs you under twelve that don't. This is a small server with one job — agents report to it, you read a clean feed and click through to where the work actually lives.

The feed: per-agent stacks, "needs you" vs activity, grouped repeats, deep links out

The optional macOS menu bar client: the same feed, one click from the clock

A notification layer only

It sits beside your agents, not around them. It does not run or orchestrate them, hold conversations, gate anything behind approvals, or replace a channel you already use. Traffic is one-way by design: agents write, you read and click out. If an agent needs an answer, its notification says where it's waiting — you answer it there, in its own channel, not here.

 MCP client ──▶  /mcp  ──────────────┐
                                     ├──▶  agent-center  ──▶  your browser
 cron / CI  ──▶  POST /api/v1/…  ────┘           │
                                        groups repeats, splits
                                        "activity" from "attention"

Four things a chat channel can't do:

  • Grouping. Repeats sharing a group_key fold into one entry with a count — "raised 10 times over 19 days" is one row, not ten pings.
  • Attention vs activity. "I opened a PR" and "I'm blocked and need you" are different kinds of message, and the feed knows the difference.
  • Regression semantics. A grouped item that fires again after you've read it comes back unread. Archived stays archived — a re-fire opens a fresh entry.
  • A link out, not a copy. Every card deep-links to the source app. This is the index of the work, not the destination.

Quickstart

pip install agent-center-app       # or pipx install agent-center-app
agent-center serve                 # → http://127.0.0.1:8765 (SQLite in ~/.agent-center/)

The package installs as agent-center-app — PyPI rejects names that differ from an existing project only by punctuation, and agentcenter was taken. The command you run, and everything else, is agent-center.

If 8765 is taken the server says so and refuses — pick another with agent-center serve --port 8766.

Register an agent (in another shell):

agent-center agent add my-agent
# prints a bearer token (shown once) and a ready-to-run curl

Connect a harness — Claude Code, for example:

claude mcp add --transport http agent-center http://127.0.0.1:8765/mcp \
  --scope user --header "Authorization: Bearer <token>"

Or click Connect an agent in the UI for copy-paste steps per client — including a single prompt to hand an agent that configures itself. Send one by hand:

curl -X POST http://127.0.0.1:8765/api/v1/notifications \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "input.needed",
    "category": "attention",
    "priority": "high",
    "group_key": "PROJ-182",
    "title": "Which entity paid the Meridian invoice?",
    "body": "Inc or LTD — needed to issue the correct invoice.",
    "source": {"app": "linear", "link": "https://linear.app/acme/issue/PROJ-182"},
    "actions": [{"label": "Open in Linear", "url": "https://linear.app/acme/issue/PROJ-182"}]
  }'

Open http://127.0.0.1:8765 — it's in the feed. Fire the same group_key again and the card shows ×2 instead of duplicating.

Local mode is the default: loopback bind, SQLite, no password, UI open. The loopback bind is the security boundary; agent tokens are still required for writing, because they are identity (who sent this), not just auth.

To keep it running on a Mac — start at login, restart on crash — docs/launchd.plist is a ready LaunchAgent: fill in the two paths per its comments, copy it to ~/Library/LaunchAgents/, launchctl load it.

Hosting it

Run the server on something always-on and your browser and remote agents reach the same URL. Two rules, enforced rather than suggested:

  • ADMIN_PASSWORD is required. On a non-loopback bind (or Vercel) the server refuses to start without it — and a passwordless server answers loopback peers only, whatever it ended up bound to. ALLOW_INSECURE_BIND=1 opts out behind a tailnet or authenticating proxy.
  • TLS is the platform's job. Managed platforms terminate it; on a VPS put Caddy in front (see docker-compose.yml). Never expose plain HTTP.

Browsers sign in with the password. Browserless readers — a retention cron, the menu bar app — use viewer tokens (agent-center viewer add cron, prints once). Viewer tokens read and triage; agent tokens write; neither can manage agents — that takes the password session, so a stolen device token can't mint itself a write credential.

Render — one click: Deploy to Render render.yaml provisions the service, a disk, and a generated ADMIN_PASSWORD (read it from Dashboard → Environment). Free-tier variant in the file's comments.

Docker: docker compose up -d — reads ADMIN_PASSWORD from .env (see .env.example), stores SQLite in a volume. One process, one worker: scale the box.

Vercel + Neon: api/index.py, vercel.json, and requirements.txt ship ready. Set DATABASE_URL (Neon's pooled string, rewritten postgresql+psycopg://…), ADMIN_PASSWORD, and CRON_SECRET (a viewer token) so the daily cron can call /api/v1/prune.

Connecting agents

The HTTP API is the only thing that writes; every other channel is a client of it. Remote MCP is mounted in the same process at POST /mcp — streamable HTTP, stateless, authenticated by the same per-agent bearer token — so connecting any harness is one URL and one header. Four tools:

Tool
report_activity Something happened, no reply needed → Activity
request_input You are stuck and need the human → Needs you
list_open_notifications What you already filed, so you reuse a group_key instead of duplicating it
resolve_notification Clear your own item once it stops needing anyone

Category is carried by the tool name rather than an enum field: choosing between two named tools is a decision a model actually makes; filling in a category field is one it defaults through. The full field rules live in one markdown file served at GET /api/v1/guide.md and as the MCP initialize instructions, so no harness keeps a copy that can go stale.

Why MCP alone isn't enough — the skill

An mcp_servers entry means an agent can notify, not that it will. Harnesses decide what to do by scanning their skill index before they act, and a server that exists only in a config file is invisible to that pass. So the install flow ends with the agent fetching GET /api/v1/skill.md and writing it into its own skill directory: MCP delivers the capability, the skill delivers the policy.

Its triggers are deliberately post-conditions on the agent's own work — a notification skill that fires only when you already thought to ask is the failure it exists to prevent:

triggers:
  - you created, shipped, merged, filed, or sent something the user would want to know about
  - you stopped because you need a decision, an answer, or a credential from the user
  - a scheduled, cron, or background run finished, succeeded, or failed

(It also tells agents not to satisfy "notify me" with osascript or notify-send — a desktop toast that nothing collects.)

The notification model

Field Meaning
type Namespaced event name: pr.opened, ticket.created, job.failed, input.needed
category activity (something happened) or attention (something is waiting on you)
priority min · low · normal · high · urgent
group_key Stable id for a recurring item (ticket ref, job name). Repeats fold into one entry. Optional — without it, a normalised title fingerprint stands in.
source {app, link} — where the work lives; the UI links out to it
actions Up to 5 {label, url} link buttons

Retries: send an Idempotency-Key header and a re-delivery of the same fire counts once.

Configuration

Variable Default
DATABASE_URL sqlite:///~/.agent-center/notifications.db any SQLAlchemy URL; Postgres via the [postgres] extra
HOST / PORT 127.0.0.1 / 8765
ADMIN_PASSWORD (empty) empty = UI open, loopback requests only
ALLOW_INSECURE_BIND (unset) 1 allows a public bind with no password, for tailnets/auth proxies
RETENTION_DAYS 90 unseen notifications are pruned after this; 0 disables

API

Interactive docs at /docs. Errors are RFC 7807 application/problem+json; the feed is cursor-paginated. GET /api/v1/notifications filters on category, type, agent, source_app, priority, tag, unread, archived, and q; /notifications/facets returns the available values with counts — it's what the filter menus are built from. POST /api/v1/agents/{slug}/token rotates a credential (only hashes are stored, so tokens can never be re-shown).

Optional: the macOS menu bar app

clients/macos/ is a native menu bar client (pictured above) — one more reader of the same read API, for the feed and its banners without keeping a browser tab open. It authenticates with a viewer token, polls conditionally (an idle minute costs a handful of 304s), and reads, archives, and snoozes in place. macOS 14+, builds with the Command Line Tools alone:

cd clients/macos && make install

See clients/macos/README.md.

Status

Early, and deliberately narrow: one-way visibility is the whole scope, now and later. On the roadmap: SSE live updates (the UI polls every 30s today; /facets answers 304 when nothing changed), snooze/mute rules, per-harness lifecycle hooks so a run cannot finish silently, and Web Push for urgent. Not on the roadmap: replies, approvals, or anything that would make this a place work happens rather than a place work is reported.

Development

See CONTRIBUTING.md for scope, checks, and PR guidance.

python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
.venv/bin/agent-center serve            # → http://127.0.0.1:8765

The UI is a React app in frontend/ (Vite, TypeScript, Tailwind, shadcn/ui, TanStack Query), built into agent_center/static/ so the wheel ships it and pip install users never touch npm:

cd frontend
pnpm install
pnpm dev        # dev server on :5173, proxies /api to :8765
pnpm build      # emits into agent_center/static/

MIT licensed.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_center_app-0.1.0.tar.gz (1.5 MB view details)

Uploaded Source

Built Distribution

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

agent_center_app-0.1.0-py3-none-any.whl (207.5 kB view details)

Uploaded Python 3

File details

Details for the file agent_center_app-0.1.0.tar.gz.

File metadata

  • Download URL: agent_center_app-0.1.0.tar.gz
  • Upload date:
  • Size: 1.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for agent_center_app-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e1a2e54643ae974e44b400cb5083752d5a5f5c86179a83aeba2c8a4fac4c8c7a
MD5 9a62546a4fb84a1869473c530cd4a909
BLAKE2b-256 9f0fdd552461565fbd3a603972d7e36b635dfe7e8e99e79f79f9a775075f8276

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_center_app-0.1.0.tar.gz:

Publisher: publish.yml on harariguy/agent-center

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

File details

Details for the file agent_center_app-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_center_app-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6a5293bc3c5d5c784b3db16eedd9c3ceb70869bf1501321b2cdfa28d740269b0
MD5 9d237231808cc4032cf81a89d630280f
BLAKE2b-256 561da631d161eab1bbfbbec25c77a0bc5b9d031725a415c1487c151c9aaadc47

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_center_app-0.1.0-py3-none-any.whl:

Publisher: publish.yml on harariguy/agent-center

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