Handover
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Handoverlog the 8am dose, run a readiness check, and seal the handover brief"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Handover
The shift-change board for one person you love.
Log the dose. Let a deterministic engine tell you what the next caregiver is about to get wrong. Seal a brief they can act on at 3am without asking you anything.
Live app · GitHub · API · Agent · Issues
Not medical advice. Handover surfaces record-keeping gaps and the presence of public label sections. It does not diagnose, and it never tells anyone to start, stop or change a dose. Anything it flags is a reason to ask the prescriber or the pharmacist. If someone is in danger, call emergency services.
Why this exists
This started as a board for one specific person, for the friend who was the only person coordinating a parent's medication after a hospital discharge. Four people were helping. Each of them believed they knew the schedule.
The hard part was never the medication. It was that the shared picture lived in five conversations and nobody could prove what had already been given. So this is deliberately narrow: one board for one person, one deterministic check before a shift changes hands, and one sealed brief that the next person can actually act on alone.
It stays general enough that anyone can start a board tonight.

That is the real worked example, running on the deployment at the top of this
file. Every penalty on the right is traceable to a card on the left: the -35
names the two rows that resolve to one ingredient, the -30 names the two
doses whose food instructions conflict 30 minutes apart, and each line prints
the upstream label it was scored against.
Related MCP server: brackenedge
✨ Features
One board per person, no account. Log doses, observations, physio notes and tasks. Each entry carries who owns it, so "somebody should do this" has nowhere to hide.
Catches two entries for one ingredient. A brand name and a generic name for the same drug is the classic duplicate-therapy record. The engine normalizes both and quotes the two rows back at you.
Catches food-timing collisions. A dose needing an empty stomach 30 minutes before one needing food is a real, checkable mistake.
Reads public label sections. NIH RxNorm resolves the concept id; openFDA supplies the label. Presence of a boxed warning or contraindications section is surfaced as "a pharmacist should read this with you", never as advice.
Refuses to call silence safe. When no label could be retrieved, that factor is marked unscored rather than passing.
A deterministic, versioned, explainable engine. Six weighted factors, every penalty traceable to a row or a label section. Weights are editable.
Seals every decision. SHA-384 hash chain over every create, update, decision and handover. Replay it; if a row was rewritten, the first broken sequence number is reported.
The shift scrubber. Drag the handover moment; entries physically move from the outgoing person to the next one and the engine re-runs for that exact instant.
A real downloadable artifact. Markdown and printable HTML brief with the dose timeline, the open risks, the sources and the seal.
An agent interface. Nine MCP JSON-RPC tools over the same service layer, with idempotent mutations.
Zero required keys. No model key, no data key, nothing to sign up for.
🚀 Quickstart
git clone https://github.com/aniruddhaadak80/handover
cd handover
npm install
npm run devOpen http://localhost:3000 and press Open the worked example.
Zero required environment variables. The dev server boots an embedded
Postgres in .data/, applies the schema idempotently, and reads label data from
the public openFDA and RxNorm APIs with no key.
To rehearse the production bundle on a laptop:
npm run build
HANDOVER_ALLOW_EMBEDDED=1 npm start # PowerShell: $env:HANDOVER_ALLOW_EMBEDDED="1"; npm startTo use a hosted database instead, set one variable and nothing else:
Variable | Required in production | Purpose |
| yes | Hosted Postgres (Neon, Vercel Postgres, Supabase, any Postgres 14+) |
| no | Local rehearsal only. Never set on a real deployment. |
| no | Where the embedded database writes locally |
| no | Canonical origin for metadata and the agent manifest |
Production without DATABASE_URL fails loudly rather than silently using an
embedded database that the next redeploy would erase.
Quality commands
npm run typecheck # tsc --noEmit
npm run lint # eslint .
npm run test # 61 tests: engine, seal chain, extractor, real store
npm run build # production build
npm run test:e2e # Playwright journey, desktop + mobile
npm run verify:live # end-to-end proof against a running deployment📁 Project map
Route | What it is for | Writes |
| The pitch and the two real actions that create a board | board + example entries |
| Ward desk: every board with a live score | board creation |
| The board itself: filters in the URL, decide, delete | entries, decisions, tombstones |
| Medicines on this board against public label text | nothing; live lookups only |
| The signature screen. Scrub the handover moment, seal it, download the brief | handover seal |
| Live MCP console with one-click | agent-logged entries |
| Replay the seal chain and read the event log | nothing |
| Engine weights, window size, your name on this device | session settings |
| Read-only share route for one board | nothing |
API | Methods | Notes |
| GET | Real write + read-back against the production store |
| GET, POST, PATCH | List, create, rename |
| GET, PATCH, DELETE, POST | Read, update, soft-delete (needs |
| GET, POST | List and log entries |
| PATCH, DELETE | Decide; soft-delete (needs |
| GET, POST | Analysis and brief; POST seals a handover |
| GET | Replay the chain, optionally with the log |
| GET | RxNorm + openFDA, honestly labelled live/cached/fallback |
| POST | Deterministic discharge-note extraction |
| GET | Markdown or printable HTML brief |
| GET, PATCH | Weights, window, caregiver name |
| POST | JSON-RPC 2.0: |
| GET | Agent manifest with the real deployed endpoint |
🏗 Architecture
graph LR
A["Browser"] --> B["Next.js App Router"]
B --> C["REST routes"]
B --> D["MCP JSON-RPC"]
C --> E["Service layer"]
D --> E
E --> F["Repository adapter"]
F --> G["Neon Postgres"]
F --> H["PGlite local"]
E --> I["Readiness engine"]
E --> J["Live label fetch"]
J --> K["openFDA"]
J --> L["RxNorm"]
E --> M["SHA-384 seal chain"]
classDef live fill:#22d3ee,color:#08303a,stroke:#22d3ee
classDef engine fill:#a78bfa,color:#20143a,stroke:#a78bfa
classDef agent fill:#34d399,color:#0a2c1c,stroke:#34d399
classDef ext fill:#fbbf24,color:#3a2a00,stroke:#fbbf24
classDef infra fill:#94a3b8,color:#111c24,stroke:#94a3b8
class C,D,F,H infra
class I,M engine
class K,L ext
class E,B,A infraThe repository adapter is the only thing that changes between local and production. Same schema, same queries, same domain logic.
📡 Data provenance and the honest fallback
graph TB
A["Medicine string on a board"] --> B["Ingredient normalizer"]
B --> C["RxNorm: rxcui.json"]
B --> D["openFDA: label.json"]
C --> E["concept id + fetch time"]
D --> F["label sections + flags"]
E --> G["Normalized DrugSafety"]
F --> G
G --> H["Status: live, cached or fallback"]
H --> I["Engine cites the source id"]
I --> J["Brief prints fetch time and status"]
classDef live fill:#22d3ee,color:#08303a,stroke:#22d3ee
classDef ext fill:#fbbf24,color:#3a2a00,stroke:#fbbf24
classDef risk fill:#fb7185,color:#3a0e08,stroke:#fb7185
classDef engine fill:#a78bfa,color:#20143a,stroke:#a78bfa
class C,D,J live
class E,F ext
class H risk
class I engineTwo independent public sources, both keyless, both attributed with a fetch time and an upstream id.
Requests are time-boxed, retried twice, then served from a stale cache.
When both sources fail, the response is explicitly
fallback: no label text is invented, and the label factor is scored as unscored rather than safe.When openFDA returns a combination product for your ingredient, the UI says so instead of pretending the match was exact.
Fallback never replaces user-created data. It only ever fills in what is missing.

🧮 The readiness engine
graph TB
A["Board + entries + asOf"] --> B["Dose reconciliation"]
A --> C["Timing risk"]
A --> D["Duplicate therapy"]
A --> E["Label safety signals"]
A --> F["Handover freshness"]
A --> G["Documentation"]
B --> H["Weighted mean, 6 factors"]
C --> H
D --> H
E --> H
F --> H
G --> H
H --> I["ready, caution or hold"]
H --> J["Every penalty names a row or a label section"]
classDef engine fill:#a78bfa,color:#20143a,stroke:#a78bfa
classDef verified fill:#34d399,color:#0a2c1c,stroke:#34d399
class H,I,J engine
class D,E verifiedscore = sum(weight * value) / sum(weight), each factor scored 0-100.
Factor | Default weight | What it penalises |
Dose reconciliation | 0.24 | Doses past due and still marked |
Timing risk | 0.16 | Two doses in the same minute; food and empty-stomach instructions within an hour; long gaps for the same medicine |
Duplicate therapy | 0.22 | Two entries whose ingredient tokens resolve to one active substance |
Label safety signals | 0.16 | Boxed warning, contraindications, interaction sections present; no label retrieved at all |
Handover freshness | 0.14 | Nothing recorded for 6, 12 or 24 hours; entries with no named owner |
Documentation | 0.10 | A dose with no amount written down; a note too thin to act on |
ready when every factor is clear; hold when any factor is blocking, or the
board is empty. The engine takes asOf as a parameter, never calls the clock,
and sorts every input, so the same board at the same moment always produces
byte-identical output. 48 unit tests cover normal, boundary, empty, malformed
and repeat cases.
🤖 Agent console
sequenceDiagram
participant A as Agent or the in-page console
participant M as /api/mcp
participant S as Service layer
participant D as Postgres
A->>M: initialize
M-->>A: protocolVersion, serverInfo
A->>M: tools/list
M-->>A: nine typed tools
A->>M: tools/call log_entry + idempotencyKey
M->>S: same path the UI uses
S->>D: entry + sealed audit event, one statement
D-->>S: seal
S-->>M: entryId, seal, auditSeq
M-->>A: structuredContent
A->>M: tools/call again, same key
M-->>A: idempotentReplay true, no second rowTools: list_boards, get_board, analyze_handover, lookup_drug,
extract_care_rows, verify_integrity, log_entry, update_entry_status,
seal_handover.
curl -s https://handover-olive.vercel.app/mcp.json{
"mcpServers": {
"handover": {
"type": "http",
"url": "https://handover-olive.vercel.app/api/mcp"
}
}
}Scope is the calling browser's session cookie, so an agent can only ever touch the boards that session owns.

🔏 Integrity and seal replay
graph LR
A["event 1"] --> B["seal 1"]
B --> C["event 2"]
C --> D["seal 2"]
D --> E["handover sealed"]
E --> F["seal n"]
F --> G["Replay walks the chain"]
G --> H["First broken seq, or all clear"]
classDef verified fill:#34d399,color:#0a2c1c,stroke:#34d399
class B,D,F verified
class H verifiedseal_n = SHA-384( UTF-8(prevSeal) || canonicalJson(event_n) )
canonicalJson = keys sorted recursively, array order preserved
genesis prevSeal = "0" repeated 96 timesKnown-answer vectors are pinned in tests/seal.test.ts, so a refactor cannot
quietly rewrite what already happened. Deletes keep a tombstone so the chain
still replays end to end after a board is soft-deleted.
🔌 API
BASE=https://handover-olive.vercel.app
# Health: proves the store answers a real write and read-back
curl -s $BASE/api/health
# Create a board, keeping the session cookie so you own it
curl -s -X POST $BASE/api/boards \
-H 'content-type: application/json' -c jar.txt -b jar.txt \
-d '{"subjectName":"My dad","timezone":"UTC","caregivers":["Priya","Ravi"],"withExampleEntries":true}'
# Log a dose
curl -s -X POST $BASE/api/boards/$BOARD/entries \
-H 'content-type: application/json' -b jar.txt -c jar.txt \
-d '{"kind":"dose","status":"due","title":"Metformin 500 mg","detail":"With breakfast.","medication":"Metformin 500 mg","doseAmount":"1 tablet","instructions":"with food","assignedTo":"Priya"}'
# Read it back
curl -s $BASE/api/boards/$BOARD -b jar.txt
# Decide on it
curl -s -X PATCH $BASE/api/entries/$ENTRY \
-H 'content-type: application/json' -b jar.txt \
-d "{\"boardId\":\"$BOARD\",\"status\":\"given\"}"
# Run the engine, replay the chain, export the brief
curl -s "$BASE/api/boards/$BOARD/handover" -b jar.txt
curl -s "$BASE/api/boards/$BOARD/integrity" -b jar.txt
curl -s "$BASE/api/export/$BOARD?format=markdown" -o handover.md
# The agent, over JSON-RPC
curl -s -X POST $BASE/api/mcp -H 'content-type: application/json' -b jar.txt \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"analyze_handover\",\"arguments\":{\"boardId\":\"$BOARD\"}}}"Errors
Every failure is the same shape, and never leaks a stack trace, SQL or an environment value:
{ "error": { "code": "validation_failed", "message": "The request body did not pass validation." } }400 malformed JSON · 404 not yours, or not there · 409 a write landed first
or the board is deleted · 413 body over 64 kB · 422 validation · 428
destructive action without confirm · 503 store unavailable.
🔒 Security and ownership
No accounts. A 128-bit session id in an HTTP-only,
SameSite=Laxcookie, minted in Edge middleware before any handler runs. Every query is scoped byowner_id, so one session can never read another's board.Parameterised SQL only. Each domain row and its audit event are written in a single statement, so a row can never exist without its sealed event.
Validated input. Zod schemas with length and enum bounds on every mutating body; a 64 kB cap; allowlisted external origins; time-boxed upstream calls.
Destructive actions are guarded and reversible-looking. Board and entry deletion require an explicit
confirmecho and are soft, leaving a tombstone.Best-effort abuse controls, stated honestly. Per-session cookie ownership, body caps and validation are enforced. Anonymous write rate limiting is not enforced in-process, because a serverless instance is ephemeral and a per-process counter is not a real limit. Put a hosted limiter in front of
/api/boardsand/api/mcpif you expose this at scale.One production secret.
DATABASE_URL. Nothing else, anywhere.
Full detail in SECURITY.md.
🗺️ Roadmap
Now: shipped and verified
Boards, entries, ownership, soft deletes
Six-factor deterministic engine with itemized evidence
Live openFDA + RxNorm with honest fallback labelling
SHA-384 seal chain with replay and pinned vectors
Shift scrubber, sealed handover, downloadable brief
Nine MCP tools with idempotent mutations
graph LR
A["Log"] --> B["Check"] --> C["Seal"] --> D["Export"]Next: the gaps I know about
Accounts with household roles, so a board survives a cleared cookie and two siblings can both edit without stepping on each other
An offline-first queue: write entries on a dead connection and reconcile them when it comes back, because ward wifi is terrible
A printable weekly grid for the fridge door, which is where this actually needs to live
Monitoring-term coverage: when a label asks for a test, nudge for the result rather than only reporting the term exists
Hosted rate limiting in front of the anonymous write endpoints
graph LR
A["Accounts"] --> B["Offline queue"] --> C["Fridge grid"] --> D["Monitoring nudges"]Later: only if someone asks
An on-device open-weights reader for pasted discharge summaries, so a parent's paperwork never has to travel to a server we control
A pharmacy handoff export in a format pharmacists already read
Import from a hospital discharge portal
graph LR
A["On-device reader"] --> B["Pharmacy export"] --> C["Portal import"]Nothing on this list is promised, dated, or partially shipped.
🤝 Contributing
Small, focused PRs. The one rule: do not make the engine guess — if you
change a factor, you must be able to say what evidence caused each penalty.
Read CONTRIBUTING.md, then run npm run typecheck,
npm run lint, npm run test and npm run build.
📄 Attribution
Label data: openFDA drug label API (U.S. FDA, public domain)
Concept ids: NIH RxNorm (U.S. National Library of Medicine)
Embedded database for local development and tests: PGlite
Agent protocol: Model Context Protocol
License
MIT. Take it, fork it, build the version your own family needs.
This server cannot be deployed
Maintenance
Related MCP Connectors
Workflow diagnostics, capability routing, and x402 settlement for MCP-compatible agents.
Control plane for autonomous software labor. Agents claim objectives over MCP with audit trail.
Agent-native notes, tasks, dev-docs, vaults, sync & handoffs. MCP + OpenAPI dual surface.
Hash-chained HMAC-signed audit log MCP for A2A (agent-to-agent) calls. Every tool-call, agent-ha...
Related MCP Servers
- AlicenseAqualityBmaintenanceFree seven-tool proof loop for MCP agents: scoped sessions, bounded authorization, typed outcomes, evidence-bound claims, and tamper-evident local closure. Upgrade to Complete Local for durable memory, recovery, signed traces, release verification, and multi-agent workflows.7Apache 2.0
- AlicenseNot gradedqualityDmaintenanceProvides MCP tools for edge-based pharmaceutical shipment disposition decisions, returning release, review, or quarantine outcomes while recording every decision in a tamper-evident, hash-chained audit trail.Academic Free v1.1
- FlicenseAqualityBmaintenanceA passive MCP work ledger that lets coding agents record and track shared task plans, session ancestry, work claims, locations, blockers, and commits in SQLite through nine reporting tools.9-
- AlicenseBqualityCmaintenanceEnables agents to register verifiable identities bound to legal persons and devices, maintain heartbeat survival, participate in a forum, collaborate on tasks with points escrow, and access points reconciliation through 43 MCP tools over stdio.43MIT