# A Gym for Murrumbeena — Full Machine-Readable Reference Base URL: https://dev.gymformurrumbeena.win (production: https://gymformurrumbeena.win) Contact: campaign@gymformurrumbeena.win Stack: Cloudflare Worker + D1 (SQLite) + R2 + Workers Static Assets. ====================================================================== 1. CAMPAIGN FACTS ====================================================================== THE ASK - Fund Stages 2 & 3 of Murrumbeena Primary School's VSBA-approved masterplan in the 2027–28 Victorian State Budget. - Estimated cost: approximately AUD $12–14 million. - The facility: a full-size, competition-grade, multipurpose gymnasium — 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 The campaign pack (`/pack/Campaign Case Studies.docx`) documents comparable school gym campaigns that won state funding, plus equity, housing-growth and wellbeing evidence (`/pack/Equity Data.docx`, `/pack/Housing Data.docx`, `/pack/Community & Wellbeing Evidence Research.docx`) and a record of MP activity on school gyms (`/pack/MP Activities on School Gyms.docx`). ====================================================================== 2. PUBLIC REST API (JSON, no auth) ====================================================================== GET /api/stats Response: { "signatures": 123, "supporters": 45, "ideas": 12, "endorsements": 3, "pledges": 8, "pledged": 1450, "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":"..."}]} 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. Response: {"ok":true,"stats":{...same as /api/stats...}} POST /api/support Body: {"name":"Sam","message":"Count us in!","suburb":"Carnegie","organisation":"Murrumbeena CC"} Required: name, message. Response: {"ok":true} POST /api/ideas Body: {"name":"Sam","email":"sam@example.com","organisation":"...","uses":["basketball","arts"],"details":"Weekend junior comp..."} Required: name, 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"} POST /api/pledge Body: {"name":"Sam","email":"sam@example.com","phone":"...","organisation":"...", "org_type":"business","org_size":"1-5","role":"...","amount":100, "frequency":"once","budget_band":"<1k","gift_item":"...", "in_kind_details":"...","notes":"...","ok_to_contact":true} Required: name, email. frequency: once | monthly | in-kind (default once). Response: {"ok":true} Note: pledges fund the campaign's running costs, not the gym build (the build is the State Budget ask). 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} Error format (all endpoints): HTTP 4xx {"error":"bad_request","detail":"missing field: name"} ====================================================================== 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"} ====================================================================== 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*, message*, suburb?, organisation?} - submit_idea {name*, details*, email?, organisation?, uses[]?≤12} - submit_pledge {name*, email*, amount?, frequency?: once|monthly|in-kind, phone?, organisation?, org_type?, org_size?, role?, budget_band?, gift_item?, in_kind_details?, notes?, ok_to_contact?} - 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. Example: curl -X POST https://dev.gymformurrumbeena.win/mcp \ -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_stats","arguments":{}}}' ====================================================================== 5. CLI — cli/mps-gym.mjs (zero-dependency, Node 18+, global fetch) ====================================================================== node cli/mps-gym.mjs [flags] [--api ] [--json] Default base: https://dev.gymformurrumbeena.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 --message [--suburb] [--organisation] idea --name --details [--uses a,b,c] [--email] [--organisation] pledge --name --email [--amount 100] [--frequency once|monthly|in-kind] [--gift "5000 flyers"] contact --name --email --message [--subject "Campaign updates"] Examples: node cli/mps-gym.mjs stats --api http://localhost:8788 node cli/mps-gym.mjs sign --name "Sam Citizen" --email sam@example.com --suburb Murrumbeena --postcode 3163 --local node cli/mps-gym.mjs contact --name "Sam" --email sam@example.com --subject "Campaign updates" --message "Add me" 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@gymformurrumbeena.win (Google Workspace); this endpoint only prepares the email. Admin console: https://admin.dev.gymformurrumbeena.win/ (serves /admin.html). ====================================================================== 7. DOWNLOADS ====================================================================== Campaign pack (absolute URLs via MCP get_campaign_pack): /pack/Short%20Pitch.docx /pack/MPS%20-%20New%20Gym%20Campaign%20-%20v2.pptx /pack/Argument%20Frames%20%26%20Priorities.docx /pack/Audience%20Messages.docx /pack/Campaign%20Case%20Studies.docx /pack/Campaign%20Planning.xlsx /pack/Community%20%26%20Wellbeing%20Evidence%20Research.docx /pack/Equity%20Data.docx /pack/Housing%20Data.docx /pack/MP%20Activities%20on%20School%20Gyms.docx 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 campaign@gymformurrumbeena.win.