360 Secure SkillHub
360 安全技能中心

crowntowncompost-mcp

该技能通过curl模拟Django表单登录(处理CSRF校验)访问Crown Town Compost客户门户,无需运行MCP服务,适合脚本调用、未安装MCP的设备使用。支持查询收运历史、账单发票、未来服务日、账户详情,可执行暂停收运、上报漏收等操作;提供JSON接口返回结构化收运、账单数据,其余为可解析HTML页面,输入为门户账号密码,输出可直接用jq等工具处理。

chrischall
chrischall
热度 63.27
最近更新 2026-10-10 07:28:27
分类:开发依赖:代码执行 / 读写文件 / 第三方 API应用场景:API 集成 / 自动化脚本 / 数据处理 / 工作流

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add crowntowncompost-mcp
Metadata
name
crowntown-portal
description
Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered pages. Use when you want Crown Town Compost data without the MCP, in a script, or on a machine where the MCP isn't installed.

Crown Town Compost portal via curl (no MCP)

crowntowncompost.com is only a marketing site. All customer data lives at portal.crowntowncompost.com — a Django app (a white-labeled StopSuite hauler platform; it CNAMEs to crowntown.stopsuite.com).

It is reachable server-side: no bot wall, no browser bridge, no fpx, no Transporter extension. Auth is a real username + password form POST that returns an HttpOnly sessionid cookie. curl with a cookie jar reaches everything a signed-in customer can see.

Two of the richest surfaces are clean JSON endpoints (service history and billing); the rest are server-rendered HTML.

The one thing that trips people up: Django CSRF

Every POST (including the login and the two JSON reads) needs all of:

  • the csrftoken cookie (set by any GET), sent back in the cookie jar
  • an X-CSRFToken: header equal to that cookie's value
  • Origin: and Referer: headers on the portal's own origin — Django rejects HTTPS POSTs that lack them, with a 403, before looking at your credentials

GETs need none of this. A 403 on a POST almost always means a missing header, not a bad password.

One-time setup

export CROWNTOWN_USERNAME='you@example.com'
export CROWNTOWN_PASSWORD='your-portal-password'   # or: op read "op://Private/CrownTown/password"
export CT=https://portal.crowntowncompost.com
JAR=~/.cache/crowntown-cookies.txt
mkdir -p ~/.cache && : > "$JAR"

Log in (get the session cookie)

Read the password from stdin (password@-) rather than putting it in argv — --data-urlencode name=value exposes the value to any local user via ps while the process runs.

# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.
CSRF_FORM=$(curl -s -c "$JAR" -b "$JAR" "$CT/accounts/login/?next=/accounts/" \
  | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"$//')
CSRF_COOKIE=$(awk '$6=="csrftoken"{print $7}' "$JAR" | tail -1)

# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's
#    Set-Cookie carries the sessionid, and success is "302 away from login".
printf '%s' "$CROWNTOWN_PASSWORD" | curl -s -c "$JAR" -b "$JAR" \
  -H "X-CSRFToken: $CSRF_COOKIE" -H "Origin: $CT" \
  -H "Referer: $CT/accounts/login/?next=/accounts/" \
  --data-urlencode "csrfmiddlewaretoken=$CSRF_FORM" \
  --data-urlencode "username=$CROWNTOWN_USERNAME" \
  --data-urlencode "password@-" \
  --data-urlencode "next=/accounts/" \
  -o /dev/null -D - "$CT/accounts/login/?next=/accounts/" \
  | grep -iE '^(HTTP/|location:)'

Success = HTTP/2 302 + a location: that is NOT /accounts/login/, and a sessionid now in the jar. A 200 means the login page re-rendered — wrong username/password. Confirm the jar:

grep -q sessionid "$JAR" && echo "signed in" || echo "NOT signed in"

The csrftoken cookie rotates on login — always re-read it from the jar before each POST:

csrf() { awk '$6=="csrftoken"{print $7}' "$JAR" | tail -1; }

The two JSON endpoints (the good stuff)

Both are POST, form-urlencoded, and return {meta, qs, data[]}. They use the Metronic KTDatatable grammar — pagination[page], pagination[perpage], sort[field], sort[sort], query[<field>] — not DataTables' draw/start/length.

ctpost() {  # ctpost <path> [extra --data-urlencode args...]
  local path="$1"; shift
  curl -s -b "$JAR" -c "$JAR" \
    -H "X-CSRFToken: $(csrf)" -H 'X-Requested-With: XMLHttpRequest' \
    -H "Origin: $CT" -H "Referer: $CT$path" \
    "$@" "$CT$path"
}

Service history (pickups)

ctpost /accounts/stops/api/ \
  --data-urlencode 'pagination[page]=1' \
  --data-urlencode 'pagination[perpage]=20' \
  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \
| jq '{total: .meta.total, pages: .meta.pages,
       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'

Filter by outcome — the values are lowercase; a capitalized Missing silently returns zero rows:

ctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \
  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'

Valid: success | missing | empty | inaccessible | unacceptable (omit for all).

Billing history (invoices)

ctpost /accounts/billing-history/api/ \
  --data-urlencode 'pagination[perpage]=20' \
  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \
| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'

Open invoices are is_payable: true; hosted_invoice_url / invoice_pdf / receipt_url are Stripe links. Pay in a browser — don't script payments.

Server-rendered pages (GET, parse the HTML)

What Path
Dashboard (subscription, next service, addresses) /accounts/
Environmental impact numbers (htmx fragment) /accounts/impact-statistics/
Account details form (name, phone, notification prefs) /accounts/update/
Service calendar (upcoming + skip buttons) /accounts/service-calendar/
Service history page (shell around the JSON above) /accounts/service-history/
Support form /accounts/support/
ctget() { curl -s -b "$JAR" -c "$JAR" "$CT$1"; }

Impact numbers are the easiest scrape (plain text in a small fragment):

ctget /accounts/impact-statistics/ \
  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'

Upcoming skippable service days — each .submit-skip button carries the ids a skip needs:

ctget /accounts/service-calendar/ \
  | grep -o '<button[^>]*submit-skip[^>]*>' \
  | sed -E 's/.*data-action="([^"]*)".*data-clid="([^"]*)".*data-rid="([^"]*)".*data-route-date="([^"]*)".*/\4  rid=\3 clid=\2 action=\1/'

action="skip" = currently scheduled (skipping it will skip it); action="unskip" = already skipped.

Writes (all POST; see references/endpoints.md for full field lists)

A 302 is not proof a write persisted — always re-read to verify.

# Skip an upcoming service (rid/clid/action from the calendar above)
ctpost /accounts/service-calendar/skip-service/ \
  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \
  --data-urlencode 'action=skip' >/dev/null
# verify: the same day's button should now read action="unskip"

Other write endpoints (each POSTs to its own URL, with csrfmiddlewaretoken): /accounts/report-missed-pickup/ (date, comment), /accounts/update/ (first_name, last_name, phone, send_email_reminders, service_notifications — read-modify-write: re-send every field, since omitted checkboxes mean "off"), /accounts/support/ (message, email, phone), /accounts/cancellation-request/ (per-location dynamic field names — read the GET form first).

Session expiry

An authed request that returns the login page (or redirects to /accounts/login/) means the session expired — re-run the login block. Detect it:

ctget /accounts/ | grep -q 'm_login_signin_submit' && echo "session expired"

Troubleshooting

Symptom Cause
403 on any POST Missing X-CSRFToken / Origin / Referer, or a stale csrftoken (re-read it from the jar after login)
Login returns 200 Wrong username/password (the page just re-rendered)
JSON endpoint returns HTML Session expired — log in again
query[status] returns 0 rows You capitalized the value; use lowercase
Empty data[] with a large perpage You're past the last page — check meta.pages