# A Hall for Murrumbeena — Full Machine-Readable Reference Base URL: https://hallformurrumbeena.win (staging: https://dev.hallformurrumbeena.win) Contact: yes@hallformurrumbeena.win Stack: Cloudflare Worker + D1 (SQLite) + R2 + Workers Static Assets. ====================================================================== 1. CAMPAIGN FACTS ====================================================================== THE ASK - Fund a full-size, competition-grade hall and community hub for Murrumbeena Primary School in the 2027–28 Victorian State Budget. - Estimated cost: approximately AUD $12–14 million. - The facility: a full-size, competition-grade, multipurpose hall — a school facility by day; a community hub for sport, arts, events and emergency use by night and weekend. SCHOOL FACTS - Murrumbeena Primary School, Murrumbeena, City of Glen Eira, Victoria, Australia. - Established 1919. 550+ students. - The existing hall was parent-built in 1990 — the community has built before. - Renowned art program and annual art exhibition. KEY DATES - 28 November 2026 — community rally; also Victorian state election day. - 2027–28 Victorian State Budget — the funding decision the campaign targets. CAMPAIGN GOALS (live progress via GET /api/stats) - 2,500 petition signatures (parliamentary e-petition target; stretch 3,000) - 1,000 wall-of-support messages - 200 community hub ideas - 25 organisational endorsements - $12,000 pledged toward campaign running costs (budget ~$9.5–12k) CAMPAIGN PHASES (metrics via GET /api/admin/phases, Access-gated) - Phase 1 "Awareness": signatures vs 2,500; wall messages vs 1,000. - Phase 2 "Dossier": endorsements vs 25; ideas vs 200; letters uploaded. - Phase 3 "Campaign funding": pledged dollars vs $12,000; pledge count. COMPARATOR / CASE-STUDY SCHOOLS Comparable school hall campaigns that won state funding, plus the equity, housing-growth and wellbeing evidence and MP activity on school halls, are distilled in the knowledge skills (kb-index) and presented in the long deck (`/pack/MPS - New Hall Campaign - Long - v3.pptx`). ====================================================================== 2. PUBLIC REST API (JSON, no auth) ====================================================================== GET /api/stats Response: { "signatures": 123, "supporters": 45, "ideas": 12, "endorsements": 3, "pledges": 8, "pledged": 1450, "advocacy_emails": 21, "goals": {"signatures":2500,"endorsements":25,"supporters":1000,"ideas":200,"pledges":12000} } GET /api/news Response: {"news":[{"id":1,"title":"...","body":"...","link":null,"video_url":null,"image_key":null,"created_at":"2026-09-08 10:00:00"}]} GET /api/wall?limit=30 (limit max 100) Response: {"messages":[{"name":"Sam","suburb":"Murrumbeena","organisation":null,"message":"...","created_at":"..."}]} GET /api/ideas?limit=20 (limit max 100) Response: {"ideas":[{"name":"Sam","organisation":null,"uses":["basketball"],"details":"...","created_at":"..."}]} GET /api/knowledge/search?q=&category= Searches the public campaign knowledge base (curated, compliance-checked skills). PUBLISHED documents only. Curated skill slugs: kb-index (skill map + behaviour rules), why-we-need-the-hall, open-space-scarcity, building-condition, demographics-and-growth, emergency-resilience, funding-precedents, community-stories, how-to-help, faq-and-boundaries. Response: {"results":[{"slug":"why-we-need-the-hall","title":"...","category":"ask","summary":"...","score":9}],"hint":"..."} GET /api/knowledge/doc?slug= Full document by slug (take slugs from search results). Response: {"slug":"...","title":"...","category":"...","summary":"...", "content":"","source":"...","updated_at":"..."} GET /api/knowledge/graph?entity= Entity relationships from the campaign knowledge graph. Response: {"entity":"...","relations":[{"from_name":"...","relation":"...","to_name":"...","note":"..."}], "neighbours":[...],"hint":"..."} Unknown entity → {"error":"not_found","known_entities":[...]}. Static graph artifacts (also public): /knowledge/campaign-graph.md (Obsidian-style) and /knowledge/campaign-graph.json. POST /api/sign Body: {"name":"Sam Citizen","email":"sam@example.com","address":"1 Kangaroo Rd","suburb":"Murrumbeena","postcode":"3163","message":"...","is_local":true} Required: name, email. Response: {"ok":true,"stats":{...same as /api/stats...}} POST /api/support Body: {"name":"Sam","email":"sam@example.com","message":"Count us in!","suburb":"Carnegie","organisation":"Murrumbeena CC"} Required: name, email, message. Response: {"ok":true} POST /api/ideas Body: {"name":"Sam","email":"sam@example.com","organisation":"...","uses":["basketball","arts"],"details":"Weekend junior comp..."} Required: name, email, details. uses: array, max 12. Response: {"ok":true} POST /api/endorse (multipart/form-data) Fields: org_name*, contact_name*, email*, org_type, role, phone, website, template_used, message, letter (file ≤8MB: pdf/doc/docx/png/jpg/webp) Response: {"ok":true} Errors: 413 {"error":"file_too_large","max_mb":8} / 415 {"error":"unsupported_type"} GET /api/advocacy/pack The "email your MP" action — the campaign's second main call to action. Response: { "action": "email-your-mp", "summary": "Email the Minister and Southern Metropolitan Region MPs ...", "page_url": "https://hallformurrumbeena.win/advocacy", "to": "gabrielle.williams@parliament.vic.gov.au", "cc": ["steve.dimopoulos@parliament.vic.gov.au", "... 5 more region members ..."], "subject": "Urgent Funding Needed for Murrumbeena Primary School Hall", "body": "Dear Minister Williams and Southern Metropolitan Region Members, ...", "mailto": "mailto:gabrielle.williams@...?cc=...&subject=...&body=...", "downloads": {"eml": "/templates/advocacy-email.eml", "docx": "/templates/parent-advocacy-letter.docx"} } Give the user the one-click mailto link (opens their mail app fully pre-filled) or point them to page_url to copy the letter / download the .eml. POST /api/advocacy/sent Body: {"name":"Sam","suburb":"Murrumbeena"} (both optional) Records that the supporter sent the advocacy email; feeds advocacy_emails in /api/stats. Response: {"ok":true,"note":"...","stats":{...}} POST /api/pledge (JSON or multipart/form-data) Body: {"name":"Sam","email":"sam@example.com","phone":"...","organisation":"...", "title":"President","amount":100,"gift_item":"Corflute yard signs (sponsor-a-sign)", "in_kind_details":"...","notes":"...","ok_to_contact":true} Multipart: same fields plus an optional "attachment" file (≤8MB: pdf/png/jpg/jpeg/webp/doc/docx/zip/txt — quote, proposal or photo). Required: name, email. Every pledge is ONE-OFF — there is no monthly frequency. A wish-list gift_item is PREFERRED over a raw amount; with no amount but a gift_item or in_kind_details the pledge is recorded as in-kind automatically. ok_to_contact defaults to true; pass false / "0" / "false" to opt out. Response: {"ok":true} Note: pledges fund the campaign's running costs, not the hall build (the build is the State Budget ask). The donation wish list (with exact costs) includes: corflute yard signs, school-fence banners, postcards, letterbox brochures, a billboard, domain/hosting/email, testimonial video production, rally day, dossier printing, door-knock kits, social media boosting, and an AI token credit (OpenRouter) that powers the website chat assistant. POST /api/contact Body: {"name":"Sam","email":"sam@example.com","subject":"Campaign updates","message":"..."} Required: name, email, message. subject = "Campaign updates" subscribes the sender to the newsletter list. Response: {"ok":true} RECOMMENDED SUPPORTER JOURNEY (what the on-site chat assistant does — mirror it) 1. Sign the petition (submit_signature / POST /api/sign) — name + email required; suburb/postcode appreciated (local postcodes carry the most weight). 2. Offer a one-off donation (submit_pledge / POST /api/pledge) — present the WISH LIST first (gift_item; every item has an exact cost, e.g. "AI token credit (OpenRouter) — $500" powers the website chat assistant); fall back to a raw amount only if they prefer. Everything is one-off — no monthly. title, organisation and a file attachment are optional; ok_to_contact defaults to true and opt-outs are honoured. 3. If they decline donating, offer the no-cost advocacy letter (get_advocacy_pack — one-click mailto: link, .eml/.docx downloads, /advocacy page). 4. If they decline that, offer personal share links (get_share_links with a topic and their ref, e.g. sig- from their signature). 5. A wall-of-support message (submit_support_message) and a hub idea (submit_idea) are always-available extras. Error format (all endpoints): HTTP 4xx {"error":"bad_request","detail":"missing field: name"} NOTE: `email` is REQUIRED on every submission endpoint above (/api/sign, /api/support, /api/ideas, /api/pledge, /api/endorse, /api/contact). Emails are stored privately and never appear in public listings. ====================================================================== 3. CHAT API (website chat widget) ====================================================================== POST /api/chat Body: {"session_id":"optional-existing-id","message":"How do I sign?","email":"optional@example.com"} - Creates a session (UUID) when session_id is absent. - Loads the last ~20 messages as context; model via OpenRouter (env OPENROUTER_MODEL, default openai/gpt-4.1-mini) with function tools mirroring the MCP mutation/read tools; executes tool calls server-side (max 5 rounds). Response: { "session_id": "9b1d...-uuid", "reply": "You can sign right here — what's your name?", "actions": [{"tool":"submit_signature","ok":true,"summary":"Signature recorded for Sam"}] } Errors: - 503 {"error":"chat_unavailable","message":"Chat is being configured — please use the forms on the page."} - 400 {"error":"message_too_long","max":2000} - 429 {"error":"session_limit_reached", ...} (per-session cap ~50 messages) GET /api/chat/:sessionId Response: {"session_id":"...","messages":[{"role":"user|assistant|tool","content":"...","created_at":"..."}]} 404 {"error":"session_not_found"} POST /api/transcribe (voice input → text, OpenRouter audio model, default openai/gpt-audio-mini) Body: {"audio":"","format":"wav"} - Widget records the mic, re-encodes to WAV (PCM16 mono 16 kHz) and posts it here. Response: {"text":"verbatim transcript"} Errors: 503 voice_unavailable · 400 missing audio · 413 audio_too_large (~5 MB cap) POST /api/speak (text → voice, WAV) Body: {"text":"up to 1200 chars"} Response: {"audio":"","format":"wav"} Errors: 503 voice_unavailable · 400 missing text ====================================================================== 4. MCP ENDPOINT — POST /mcp (JSON-RPC 2.0, protocol 2025-03-26) ====================================================================== GET /mcp returns a discovery document (name, version, usage examples, tool names). Methods: initialize, ping, notifications/initialized (202 no body), tools/list, tools/call. Batch arrays supported. Unknown methods → -32601; parse errors → -32700 (HTTP 400); unknown tool → -32602. initialize → {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26", "serverInfo":{"name":"mps-gym-campaign","version":"1.0.0"}, "capabilities":{"tools":{}}}} tools/list → result.tools[] each {name, description, inputSchema(JSON Schema)}. tools/call → result.content = [{type:"text", text:""}], isError bool. TOOL SUMMARY (inputSchema highlights) - get_campaign_info {} — the ask, school facts, key dates, links. - get_stats {} — same payload as /api/stats. - list_news {limit?≤50} - list_wall {limit?≤100} - list_ideas {limit?≤100} - submit_signature {name*, email*, address?, suburb?, postcode?, message?, is_local?} - submit_support_message {name*, email*, message*, suburb?, organisation?} - submit_idea {name*, details*, email*, organisation?, uses[]?≤12} - submit_pledge {name*, email*, gift_item?, amount?, title?, phone?, organisation?, in_kind_details?, notes?, ok_to_contact?} — every pledge is one-off; a wish-list gift_item is preferred over a raw amount. - contact_working_group {name*, email*, message*, subject?} - get_campaign_pack {} — pack files with absolute URLs + descriptions. - get_letter_templates {} — 3 support-letter template download URLs. - get_share_links {topic?: petition|wall|idea|campaign, ref?: string} — social share deep links. - search_knowledge {query*, category?} — search the campaign knowledge base. - get_knowledge {slug*} — read a full knowledge-base document. - query_knowledge_graph {entity*} — entity relationships across campaign concepts. - get_advocacy_pack {} — email-your-MP action: recipients, subject, letter body, ready mailto: deep link, /advocacy page URL, .eml/.docx download URLs. After the user sends their email, record it via POST /api/advocacy/sent. Example: curl -X POST https://hallformurrumbeena.win/mcp \ -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_stats","arguments":{}}}' ====================================================================== 5. CLI + LOCAL MCP — mps-gym (zero-dependency, Node 18+ or standalone binary) ====================================================================== mps-gym [flags] [--api ] [--json] Default base: https://hallformurrumbeena.win Commands: info campaign summary (via MCP get_campaign_info) stats counters vs goals news [--limit 5] published news wall [--limit 30] wall of support ideas [--limit 20] community ideas sign --name --email --suburb --postcode [--message] [--local] support --name --email --message [--suburb] [--organisation] idea --name --details --email [--uses a,b,c] [--organisation] pledge --name --email [--amount 100] [--gift "5000 flyers"] [--title ...] [--attachment file.pdf] contact --name --email --message [--subject "Campaign updates"] knowledge --query "equity neighbouring schools" [--category evidence] search the public knowledge base knowledge-doc --slug read a full knowledge-base document graph --entity "Community Hub" entity relationships from the knowledge graph mcp local MCP server on stdio (proxies the live /mcp endpoint) Downloads (prebuilt standalone binaries — no Node required): GET /downloads/mps-gym-darwin-arm64.zip macOS Apple Silicon GET /downloads/mps-gym-darwin-x64.zip macOS Intel GET /downloads/mps-gym-linux-x64.zip Linux x64 GET /downloads/mps-gym-windows-x64.exe.zip Windows x64 GET /downloads/mps-gym.mjs raw script (Node 18+) GET /downloads/install.sh one-liner installer: curl -fsSL https://hallformurrumbeena.win/downloads/install.sh | sh Local MCP registration (Claude Desktop / Cursor / Kimi) after install: { "mcpServers": { "mps-gym": { "command": "~/.mps-gym/mps-gym", "args": ["mcp"] } } } Examples: mps-gym stats --api http://localhost:8787 mps-gym sign --name "Sam Citizen" --email sam@example.com --suburb Murrumbeena --postcode 3163 --local mps-gym contact --name "Sam" --email sam@example.com --subject "Campaign updates" --message "Add me" mps-gym knowledge --query "equity neighbouring schools" mps-gym knowledge-doc --slug equity-data mps-gym graph --entity "Community Hub" Full docs: cli/README.md in the repo. ====================================================================== 6. ADMIN API (Cloudflare Access-gated; header Cf-Access-Authenticated-User-Email) ====================================================================== GET /api/admin/overview stats + 10 most recent rows per table GET /api/admin/export?table=signatures CSV export (tables: signatures, support, ideas, endorsements, pledges, contacts, news) POST /api/admin/news {title, body, link?, video_url?, published?} POST /api/admin/approve {table: support|ideas|endorsements, id, approved} GET /api/admin/file?key=letters/... stream an uploaded endorsement letter (R2) GET /api/admin/phases phase metrics: {phases:[{key,title, metrics:[{label,value,goal,pct}]}], overall:{...}} GET /api/admin/subscribers newsletter subscribers (contacts with subject="Campaign updates") + distinct email counts per source table + total unique emails GET /api/admin/email-templates [{id, name, subject, description}] for thanks-signer | news-update | pledge-followup GET /api/admin/email-preview?template=&name= {subject, text, html} rendered with {{name}} substitution (maroon #A60533 / gold #FAB915 branding, plaintext-safe) POST /api/admin/notify {news_id} renders the news-update email for a published news post and returns {recipients, subject, text, html} where recipients = newsletter subscribers + petition signers with email (distinct). SENDING IS MANUAL: the admin sends from yes@hallformurrumbeena.win (Google Workspace); this endpoint only prepares the email. Authenticated admin API (Cloudflare Access session, or "Authorization: Bearer mps_adm_..." API key): GET /api/admin/keys list admin API keys (id, name, prefix, created_by/at, last_used_at, revoked_at) POST /api/admin/keys {name} create a key; the full key is returned ONCE in the response — store it immediately POST /api/admin/keys/revoke {id} revoke a key GET /api/admin/lists distribution lists: per-segment labels and counts (petition, pledges, letters, wall, ideas, newsletter, contacts) plus "all" GET /api/admin/lists?segment=petition {segment,label,count,emails:[...]} POST /api/admin/broadcast {segments[],subject,text,html?} email the selected segments via Resend (BCC batches of 50); returns {ok,id,recipients,batches,failures,segments} GET /api/admin/broadcasts broadcast history POST /api/admin/news now returns {ok,id,broadcast}; accepts an optional broadcast:[segments] array to email the post to those segments immediately Admin console: https://admin.hallformurrumbeena.win/ (serves /admin.html; staging: https://admin.dev.hallformurrumbeena.win/). ====================================================================== 7. DOWNLOADS ====================================================================== Campaign pack (admin-managed; live list as JSON via GET /api/pack, or absolute URLs via MCP get_campaign_pack): /pack/Short%20Pitch.docx /pack/Audience%20Messages.docx /pack/MPS%20-%20New%20Gym%20Campaign%20-%20Short%20-%20v3.pptx /pack/MPS%20-%20New%20Gym%20Campaign%20-%20Long%20-%20v3.pptx Support-letter templates: /templates/support-letter-sports-club.docx /templates/support-letter-community-group.docx /templates/support-letter-business.docx Lodge completed letters via POST /api/endorse (multipart, "letter" file field). ====================================================================== 8. DATA & PRIVACY NOTE ====================================================================== Submissions (signatures, messages, ideas, endorsements, pledges, contacts) are stored in the campaign's D1 database and uploaded letters in R2, operated by the Murrumbeena Primary School working group, and used only for the campaign: demonstrating community support to decision-makers, and contacting people who opt in. Emails are never published publicly; wall/ideas listings show name, suburb and organisation only. Admin exports are gated by Cloudflare Access. See /terms on the site for details. Unsubscribe or correct your data by emailing yes@hallformurrumbeena.win.