DevPulse is an engineering-ops backend that tracks services, metrics, alerts, and incidents for an organization, then hands that incident data to Claude for triage and postmortem drafting, both through a REST API and through a native MCP server so any MCP client (Claude Desktop included) can query and act on the same data. It is not a demo wrapped around a chatbot: it has org-scoped auth, Redis-backed rate limiting on every paid AI call, pagination on every unbounded list endpoint, a prod-safe secret guard, and a test suite that spins up real Postgres and Redis containers instead of mocking the database away.
DevPulse models the core of an incident-management tool (think a small internal PagerDuty slice): organizations and members, services, metrics, alert rules and alert events, and incidents with a status lifecycle. On top of that it adds an AI layer backed by the Anthropic API:
- Incident triage. Claude reads an incident's title, severity, status, linked service, and description, and returns a structured summary, a suggested severity, and a confidence score, enforced through a forced tool call rather than parsed from freeform text. Every run is appended to an
incident_triagestable, so an incident keeps a full triage history, not just the latest guess. - Postmortem generation. Claude drafts a postmortem strictly from the incident's recorded facts (timestamps, severity, linked service, full triage history) and is explicitly instructed to say "the data doesn't support a root cause" rather than invent one. Every draft is persisted, same as triage.
- MCP server. A separate process (
backend/mcp_server/) exposes incidents, services, triage, and postmortems as MCP tools, so a model talking over the Model Context Protocol can list incidents, create one, trigger a triage, or pull a postmortem, all through the same authenticated REST API everything else uses.
flowchart LR
subgraph Clients
UI[REST API client]
MCPClient[MCP client, e.g. Claude Desktop]
end
subgraph DevPulse Backend
API[FastAPI app<br/>auth, orgs, services,<br/>metrics, alerts, incidents]
AI[AI module<br/>triage + postmortem service]
RL[Redis rate limiter<br/>per-org, per-minute]
MCP[MCP server<br/>stdio process, no DB of its own]
end
PG[(PostgreSQL 16<br/>+ pgvector)]
REDIS[(Redis 7)]
CLAUDE[Anthropic API<br/>Claude, forced tool calls]
UI -->|HTTPS + JWT| API
MCPClient -->|MCP over stdio| MCP
MCP -->|HTTPS + same access token<br/>same org-scoped authz| API
API --> PG
API --> AI
AI --> RL
RL --> REDIS
AI -->|structured tool-call request| CLAUDE
CLAUDE -->|summary, severity, confidence| AI
AI -->|persist triage / postmortem| PG
The MCP server deliberately holds no database connection of its own. Every tool call it makes is a plain HTTP request against the already-running API, so it inherits the exact same org-membership authorization as any other client instead of duplicating that logic in a second place.
- Org-scoped auth with argon2id password hashing and JWT access tokens.
- Services, metrics, alert rules, alert events, and incidents, each with its own router, service layer, and SQLAlchemy models.
- Claude-backed incident triage with a persisted history (
incident_triages), driven by a forced tool call so the response shape never depends on how the model phrases things. - Claude-backed postmortem generation, grounded only in recorded incident facts, with a persisted history (
postmortems). - An MCP server (
devpulse-mcp) exposinglist_incidents,get_incident,create_incident,update_incident_status,triage_incident,list_incident_triages,generate_postmortem,list_postmortems, andlist_servicesas MCP tools. - Every unbounded list endpoint (incidents, services, alert events, metrics) returns
{items, total, limit, offset}instead of a bare array; small, human-bounded collections (org members, alert rules) intentionally stay as plain lists. - A Redis-backed, fixed-window rate limit shared by triage and postmortem generation, scoped per organization per minute, returning
429withRetry-Afterwhen exceeded. - An explicit Anthropic request timeout and retry count instead of the SDK's 600 second default, so a stalled Claude call can't hold a database session open for minutes.
- A prod secret guard:
Settingsrefuses to construct whenAPP_ENV=prodand the JWT signing secret is still a default, a placeholder, or under 32 characters.
| Layer | Technology |
|---|---|
| API | FastAPI, Python 3.12, Pydantic v2 |
| Database | PostgreSQL 16 with pgvector, SQLAlchemy 2.0 (async), Alembic migrations |
| Cache / queue | Redis 7, arq, APScheduler |
| Auth | argon2id (argon2-cffi), PyJWT |
| AI | Anthropic SDK (forced tool calls), MCP Python SDK |
| Testing | pytest, pytest-asyncio, testcontainers (real Postgres/Redis in CI), freezegun |
| Tooling | ruff, mypy (strict), uv, Docker Compose, GitHub Actions |
| Logging | structlog (structured JSON) |
Frontend (React 18, TypeScript, Vite, shadcn/ui) is planned for a later phase and is not implemented yet, so there is no UI to screenshot today; everything above is exercised through the REST API, the test suite, and the MCP server.
Requirements: Docker, Docker Compose v2, GNU Make.
cp .env.example .env # then edit secrets, including ANTHROPIC_API_KEY
make up # starts postgres, redis, api
curl http://localhost:8000/healthz
make logs # tail container logs
make down # stop everythingTests, lint, and type-check:
make test
make lint
make typecheckOr without Docker, from backend/ with uv:
cd backend
uv sync --extra dev
uv run ruff check .
uv run ruff format --check .
uv run mypy app
uv run pytest --cov=app --cov-report=term-missing --cov-report=htmlIntegration tests spin up ephemeral Postgres and Redis containers via testcontainers, so Docker needs to be running, but no manual setup or secrets are required for them.
To run the MCP server against a running API:
cd backend
uv sync --extra dev
DEVPULSE_MCP_ACCESS_TOKEN=<a valid access token> \
DEVPULSE_MCP_ORG_ID=<the org to scope every call to> \
uv run devpulse-mcp- Forcing structure out of an LLM, not parsing it out. Both triage and postmortem generation use Anthropic's tool-call mechanism to force Claude into a fixed response shape (
summary,suggested_severity,confidencefor triage) instead of asking for JSON in a prompt and hoping. That removes an entire class of "the model added a markdown fence around its JSON" bugs that string-parsed LLM integrations tend to hit in production. - An MCP server with no database of its own. Rather than building a second data-access layer for MCP clients,
mcp_server/is a thin stdio process that calls the real API over HTTP using a real access token. That one design choice means the authorization rules (who can see which org's incidents) only exist in one place, and the MCP surface can never drift out of sync with the REST API's behavior as the product grows. - Treating the Claude API as a cost center, not just a feature. The rate limiter, the explicit request timeout (overriding the SDK's 600-second default), and the retry count are all aimed at one failure mode that most AI side-projects never think about: a bug or an abusive client quietly running up real money against a paid API while holding database connections open. Those three pieces plus the prod secret guard are the difference between "AI demo" and "AI feature with a production incident budget."
| Phase | Area | Status |
|---|---|---|
| Phase 1 | Core backend (auth through incidents) | Done |
| Phase 2 | AI layer (Claude triage, MCP, RAG) | In progress: triage, postmortems, and the MCP server are shipped; RAG is next |
| Phase 3 | Frontend | Not started |
| Phase 4 | Production infrastructure (AWS) | In progress: pagination, rate limiting, Claude timeouts/retries, and the prod secret guard are shipped; deployment and observability are next |
| Phase 5 | Polish, docs, demo | Not started |
See docs/adr/ for the architecture decision records behind these choices.