✨ MCP endpoint for importing donations from Facebook groups (#24)

Adds an MCP server at POST /mcp so an AI agent can turn posts from a public Facebook group into donations on Forward. The server never touches Facebook. The agent does the reading, with its own browser session and its own membership of the groups; the endpoint only turns "here is a post" into a record. That keeps scraping out of the application and leaves the MCP surface small enough to stay stable. Three properties are enforced structurally rather than by convention: * Imports are drafts. A new `draft` state is invisible everywhere, because every listing already goes through `active`, and the direct URL 404s for anybody but its owner. Watch alerts are held back until `publish!`. * Imports are credited to the original poster. `user_id` stays the importing account - a row needs an owner, and that is who to ask about it - while the donation is attributed to `source_author_name` and links to `source_url`. Imports are not requestable, because the importer does not have the item; visitors are sent to the original post instead. `create_donation` requires both fields, so there is no way to use this endpoint to post something as your own. * Re-scanning a group is safe. `source_url` is unique at the database, and is normalised to drop fbclid, mibextid and friends first, so the second import of a post is refused rather than duplicated. Two changes fall out of the ownership split. The donor-currency check is skipped for imports, since `user` is the importer and their region says nothing about the market the donation belongs to - somebody in Copenhagen curating a Stockholm group is importing SEK donations, and that is correct. To keep the invariant that check was protecting, `User#update_stats!` now counts only donations the user posted themselves, so a curator is neither credited for items they never had nor able to mix currencies into their totals. The region and charity checks are untouched. `list_charities` takes a region and returns only charities whose currency matches it. The currency rule is the constraint an agent would otherwise trip over on most calls, and answering it in the tool signature beats answering it in an error message. Image URLs arrive from a page an agent was told to go and read, so they are attacker-influenced in the ordinary case. `RemoteImageFetcher` is https-only, resolves the host and checks every address it resolves to, pins the connection to the address it checked, and re-runs all of that per redirect hop. Authentication is a bearer token rather than the session cookie, because the caller is not a browser. Tokens are stored as HMAC digests, shown once, can be named and renamed, and are rate limited per token. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> :arrow_up: Upgrade libraries 💸 Scope donation currency to the market (#23) Currency becomes a property of the region a donation happens in rather than something the donor picks per item. - a region owns the currency; a charity accepts exactly one, which keeps its total raised a single figure - a donation copies its region's currency and validates it against the region, the charity and the donor's own region - the locale decides only how an amount is punctuated, never which currency - the currency picker is gone from the donation form; `market_controller` keeps the symbol and charity list in step with the selected region - `currency_symbol` columns are dropped for a new `Currency` model, following `Locale` as the single source of truth - the admin dashboard's cross-market total is grouped by currency, not summed Also makes `regions.donations_count` a true counter cache. An active-only counter cannot work, since claiming or deleting a donation is an update and never fires the create/destroy callbacks that maintain it. The regions index, which labelled that number "available items", now queries for the active count.

Peter Theill
Deployed by Peter Theill
959 total
3 this month
3 this month
Back to feed