Inspiration
Volunteer boards (HOAs, nonprofits, school PTAs) run on memory and late minutes. A treasurer asks "did we approve the roof bid or just talk about it?" and nobody is sure until someone digs through a two-hour recording.
Before this project we ran a separate AI minutes pipeline on five real public meetings. It reported 24 of 24 motions and outcomes, but it named the mover correctly only 16 times out of 24 and the seconder only 14. Those numbers come from that earlier pipeline, not from Board Clerk, and they were not checked against the recordings. Still, the lesson stuck: a wrong name in official minutes is worse than a blank one, because the board approves it and it becomes the record. So Board Clerk does not fill in a name it cannot support.
What it does
Board Clerk is a self-hosted MCP server that an Alexa+ style assistant can call to answer questions about a board's decisions.
- Ask what was decided. "What did we decide about the roof?" returns the motion, the result, the tally, the mover and seconder, and a "Listen at 00:22:10" timestamp into the recording.
- Ingest a meeting. Give it a clean, timestamped transcript and it extracts the motions, outcomes and vote counts it supports.
- Hold names it cannot support. A mover or seconder is marked "spoken" only when that person's name appears in what someone said within 45 seconds before or 30 seconds after the motion. A speaker label alone does not count. If a name fits more than one roster member (say "Dana" when the board has Dana Smith and Dana Jones), Board Clerk does not pick one. It lists both and asks the clerk. Anything it cannot support is flagged, and the clerk confirms it from the roster with one tap. Names that are not on the roster, or that still match more than one person, are refused. On the demo page, the confirmation dropdown lists the roster of that item's own meeting, so the clerk always picks one full roster name. The same person cannot be confirmed as both mover and seconder of a motion.
- Block bad approvals. Minutes cannot be approved while any mover or seconder still needs confirmation. Board Clerk says why and what is left.
- Remember across sessions. Close the app, come back tomorrow, and a brand new session sees the same board, the same meetings and the clerk's confirmations.
- Build a board packet.
prepare_board_packetcollects every motion on a topic across meetings, the approved dollar total, open confirmations and tabled items. - Keep a paper trail. Every confirmation and approval goes into an audit log. Nothing is emailed on its own.
The demo board is a fictional HOA (Juniper Hollow) and the web page is a simulated Alexa+ experience. It has not been tested on an Alexa device.
How we built it
- Cloudflare Worker running an MCP server on the 2025-11-25 Streamable HTTP spec (
@modelcontextprotocol/server). - One SQLite-backed Durable Object per board, so each board's memory survives across sessions and boards stay isolated from each other.
- 16 tools, each with a title and annotations (read-only, destructive, idempotent hints), plus resource templates for the roster and meetings, a
ui://board-clerk/decision-cardUI resource, anask_the_boardprompt, and an Agent Skill (skills/board-clerk/SKILL.md) that describes the clerk loop for an assistant (written and reviewed, not yet run inside an agent host). - The extractor is rule-based and the server makes no model calls, so there are no model API charges per meeting and answers are repeatable. It is built for clean, timestamped transcripts in the form
[HH:MM:SS] Speaker: words. - Guardrails: Origin allowlist, MCP served only on
/mcp, a 256 KB request cap, per-board limits, and a read-only shared demo board. Each visitor to the demo page gets their own board under a random id throughstart_demo_board(no sign-in in this prototype). - The demo page is a real MCP client. The answers and the trace panel come from tool calls you can watch.
Challenges we ran into
Attribution, not extraction, was the hard part. Pulling out motions and outcomes was reliable. Knowing who moved and who seconded was not, so we made "needs confirmation" a first-class state that blocks approval. A review during the event caught two gaps in our first version: a speaker label was enough to mark a name as spoken, and a shared first name was resolved by whoever came first on the roster. Both now leave the field open for the clerk, with regression tests for each case. A later pass found that the demo page's confirmation dropdown could keep the fictional HOA's default roster after the board or roster changed. Each open item now carries its own meeting's roster, and a test drives the real page through board and roster switches and confirms a name from the dropdown.
On the MCP side, the handler in the server SDK we used is path-agnostic and its entry point does no Origin checks. Our first version answered tool calls on paths we never meant to serve, and our first Origin allowlist could be bypassed with DNS rebinding. Both are fixed and written up in the friction log.
Accomplishments that we're proud of
- Approval is refused while any mover or seconder still needs confirmation, and an ambiguous name can only be confirmed as one specific roster member.
- State carries across sessions without the assistant having to remember anything.
- 75 automated tests (node --test, 14 suites, including a test that drives the demo page in jsdom), an engine smoke test and an in-process MCP smoke test, all run by GitHub Actions on every push (Node 24, passing on commit f896524). Typing "Do not approve the minutes" is refused, and ingesting the same meeting twice is a no-op, each with a regression test. A remote smoke script runs ingest, refuse, confirm and approve over HTTP against any deployed URL.
What we learned
An assistant is only useful for governance if it says when it doesn't know. "Partial match" and "needs confirmation" turned out to be features, not failures.
What's next for Board Clerk
- Test on a real Alexa+ device once a path to attach a remote MCP server is available.
- Recording-to-transcript intake so a clerk can start from the meeting audio.
- A video check on vote counts (our Roll Call Vision project) so the tally in the minutes matches the hands raised on camera.
Built With
- agent-skills
- alexa
- cloudflare-workers
- durable-objects
- javascript
- mcp
- model-context-protocol
- node.js
- zod
Log in or sign up for Devpost to join the conversation.