{"id":"738ad76d-50fe-421e-89df-4d9ee0ea6b87","slug":"clubmanager365cli","kind":"mcp","name":"clubmanager365cli","summary":"CLI and MCP server to log in and book courts on clubmanager365.com","owner":"jerry-shao","repoUrl":"https://github.com/jerry-shao/clubmanager365cli","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-02-22T08:58:19.000Z","addedAt":"2026-10-21T05:48:57.256Z","logoUrl":"https://avatars.githubusercontent.com/u/7830699?u=f26c612e69af613b4f8ccd8bc365424e2844c3bf&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","memory-knowledge"],"tags":["automation","booking","cli","court-booking","python","tennis","clubmanager365"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-27T22:23:18.127Z","sample":false,"url":"https://conduits.dev/tools/clubmanager365cli","language":"Python","transports":["stdio"],"install":{"pypi":"clubmanager365cli"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# clubmanager365cli\n\n[![PyPI version](https://img.shields.io/pypi/v/clubmanager365cli.svg)](https://pypi.org/project/clubmanager365cli/)\n[![Python versions](https://img.shields.io/pypi/pyversions/clubmanager365cli.svg)](https://pypi.org/project/clubmanager365cli/)\n[![License](https://img.shields.io/pypi/l/clubmanager365cli.svg)](LICENSE)\n\nA command-line tool to log in to [clubmanager365.com](https://clubmanager365.com)\nand book courts from the terminal — and an **MCP server** (local stdio or\nremote HTTP) exposing the same actions so AI agents can book courts for you.\n\n> Personal automation for your own account. Use responsibly and within your\n> club's terms of use.\n\n## Booking rules\n\nEach club configures its own booking rules, so your club may differ. By default\nthis tool assumes the same rules as the club it was developed against:\n\n- Every booking **requires at least one opponent** (`book --with <name|id>`).\n- **One slot per person per day** — booking a day you already have fails.\n- Booking needs no payment at the time of booking (covered by membership /\n  court credits), so there's no checkout step.\n\n## Install\n\nThe quickest way — run the CLI without installing anything, via\n[uv](https://docs.astral.sh/uv/) (it fetches the package on demand):\n\n```bash\nuvx --from clubmanager365cli cm365 --help\n```\n\nOr install it as a persistent global `cm365` command with\n[pipx](https://pipx.pypa.io/):\n\n```bash\npipx install clubmanager365cli\ncm365 --help\n```\n\nOr from source, for development (Python 3.10+):\n\n```bash\ngit clone https://github.com/jerry-shao/clubmanager365cli\ncd clubmanager365cli\nuv venv --python 3.12 && uv pip install -e \".[mcp]\"\n# or: python3 -m venv .venv && .venv/bin/pip install -e \".[mcp]\"\n```\n\n## Credentials\n\nProvide your credentials (kept local, git-ignored):\n\n```bash\ncp credentials.env.example credentials.env\n```\n\nThen open `credentials.env` in an editor and fill in your username and password.\n\nAlternatively, export them as environment variables (quote both values —\nusernames and passwords can contain spaces):\n\n```bash\nexport CM365_USERNAME=\"your username\"\nexport CM365_PASSWORD=\"your password\"\n```\n\nIf your club's match type differs from the default (`4` = Friendly), set\n`CM365_MATCH_TYPE` too — run `cm365 match-types` to find your club's ids. This\napplies to both the CLI and the MCP server.\n\n## Usage\n\nThe examples below assume `cm365` is on your PATH (pipx or source install). If\nyou use `uvx`, prefix each command with `uvx --from clubmanager365cli`.\n\n```bash\ncm365 login                       # verify your credentials work\ncm365 mybookings                  # list your upcoming bookings\ncm365 slots tomorrow -a           # free slots tomorrow\ncm365 slots 2026-07-04 -t 18:00   # all courts at 18:00 on a date\ncm365 slots today --type indoor   # restrict to indoor courts\ncm365 players \"pat smith\"        # find an opponent's id by name (quote names with spaces)\ncm365 match-types                # list your club's match-type ids (e.g. Friendly)\n\n# Booking requires an opponent and is a dry run unless you pass --yes:\ncm365 book tomorrow 18:00 --with \"Pat Smith\"             # dry run (finds slot)\ncm365 book tomorrow 18:00 -c \"Indoor Court 1\" --with \"Pat Smith\" --yes\ncm365 book tomorrow 18:00 --with 100001 --with 100002 --yes   # doubles, by id\n\ncm365 cancel 12345678 --yes       # cancel a booking by id (from mybookings)\n```\n\nDates accept `today`, `tomorrow`, `YYYY-MM-DD`, or `27 Jun 2026`.\nTimes accept `18`, `18:00`, or `6pm`. Opponents accept names (fuzzy) or ids.\n\n### Diagnostics\n\n```bash\ncm365 whoami             # post-login landing page + nav links\ncm365 explore [PATH] -o page.local.html   # dump a page's HTML/forms/links\n```\n\n## MCP server\n\nThe same actions are exposed as an [MCP](https://modelcontextprotocol.io)\nserver so an AI assistant (Claude Desktop, Codex, OpenClaw, …) can book courts\nfor you. By default it runs **locally over stdio with your own credentials** — no\nshared/hosted server, so your login never leaves your machine. It can also run\nas a [remote HTTP server](#remote-http-mode) with per-request credentials.\n\nTools: `list_slots`, `my_bookings`, `search_players`, `list_match_types`,\n`book_court`, `cancel_booking`. `book_court` and `cancel_booking` are a **dry\nrun unless you pass `confirm: true`**, so the assistant can't book or cancel\nwithout an explicit confirmation step.\n\n### Run it\n\nNo manual install needed — [`uv`](https://docs.astral.sh/uv/) runs it (and\nprovisions a suitable Python; the MCP SDK needs 3.10+):\n\n```bash\nuvx --from \"clubmanager365cli[mcp]\" clubmanager365-mcp\n```\n\n### Connect a client\n\nAll clients need the same three things: `command: uvx`, the `args` below, and\nyour credentials in `env`. A few examples; other MCP clients follow the same\npattern.\n\n**OpenClaw** — add with the CLI:\n\n```bash\nopenclaw mcp add clubmanager365 \\\n  --command uvx \\\n  --arg --from --arg \"clubmanager365cli[mcp]\" --arg clubmanager365-mcp \\\n  --env CM365_USERNAME=your-username \\\n  --env CM365_PASSWORD=your-password\n```\n\nor edit `~/.openclaw/openclaw.json` directly (servers live under `mcp.servers`):\n\n```json\n{\n  \"mcp\": {\n    \"servers\": {\n      \"clubmanager365\": {\n        \"command\": \"uvx\",\n        \"args\": [\"--from\", \"clubmanager365cli[mcp]\", \"clubmanager365-mcp\"],\n        \"env\": {\n          \"CM365_USERNAME\": \"your-username\",\n          \"CM365_PASSWORD\": \"your-password\"\n        }\n      }\n    }\n  }\n}\n```\n\nVerify with `openclaw mcp doctor clubmanager365 --probe`.\n\n**Hermes Agent** — add to `~/.hermes/hermes-agent/config.yaml` (top-level key\n`mcp_servers`), then run `/reload-mcp`:\n\n```yaml\nmcp_servers:\n  clubmanager365:\n    command: \"uvx\"\n    args: [\"--from\", \"clubmanager365cli[mcp]\", \"clubmanager365-mcp\"]\n    env:\n      CM365_USERNAME: \"your-username\"\n      CM365_PASSWORD: \"your-password\"\n```\n\n**Claude Desktop** — add to `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"clubmanager365\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"clubmanager365cli[mcp]\", \"clubmanager365-mcp\"],\n      \"env\": {\n        \"CM365_USERNAME\": \"your-username\",\n        \"CM365_PASSWORD\": \"your-password\"\n      }\n    }\n  }\n}\n```\n\n**Codex CLI** — add to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.clubmanager365]\ncommand = \"uvx\"\nargs = [\"--from\", \"clubmanager365cli[mcp]\", \"clubmanager365-mcp\"]\nenv = { CM365_USERNAME = \"your-username\", CM365_PASSWORD = \"your-password\" }\n```\n\n### Remote HTTP mode\n\nSet `CM365_MCP_TRANSPORT=http` and the server speaks streamable HTTP instead\nof stdio:\n\n```bash\nCM365_MCP_TRANSPORT=http uvx --from \"clubmanager365cli[mcp]\" clubmanager365-mcp\n# serves http://127.0.0.1:8000/mcp  (CM365_MCP_HOST / CM365_MCP_PORT to change)\n```\n\nIn HTTP mode **each request must carry the caller's own credentials in\nheaders** — environment credentials are deliberately ignored (set\n`CM365_HTTP_ENV_FALLBACK=1` to opt back in for a private single-user server):\n\n| Header | Meaning |\n|---|---|\n| `x-cm365-username` | clubmanager365 username (required) |\n| `x-cm365-password` | clubmanager365 password (required) |\n| `x-cm365-base-url` | optional, defaults to `https://clubmanager365.com` |\n\nThe SDK's DNS-rebinding Host check is disabled in HTTP mode (a tunnel's public\nhostname isn't known in advance, and sensitive calls need credentials anyway).\nTo lock the server to specific hostnames, set\n`CM365_MCP_ALLOWED_HOSTS=mcp.example.com` (comma-separated).\n\nCredentials are used per-request to log in and are never stored server-side.\n\nTo expose it publicly without opening ports, use a\n[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/):\n\n```bash\ncloudflared tunnel --url http://127.0.0.1:8000   # quick tunnel for testing\n```\n\n#### Deploying to a serverless container platform\n\nThe included [`Dockerfile`](Dockerfile) runs the server in HTTP mode with\n`CM365_MCP_STATELESS=1` (self-contained requests — required when instances\nscale to zero) and honours the platform's injected `PORT`. No credentials go\ninto the image; they arrive per-request via headers. E.g. Google Cloud Run:\n\n```bash\ngcloud run deploy cm365-mcp --source . --region europe-west2 --allow-unauthenticated\n```\n\nThe resulting `https://….run.app/mcp` URL is stable — point Smithery at it.\n\n#### Publishing to Smithery\n\nThe header scheme matches [Smithery session config](https://smithery.ai/docs),\nso the server can be published to Smithery as a remote server where each user\nfills in their own credentials. Point it at your public `…/mcp` URL — a Cloud\nRun service or a tunnel host both work:\n\n```bash\nsmithery mcp publish \"https://your-server-host/mcp\" --config-schema '{\n  \"type\": \"object\",\n  \"required\": [\"username\", \"password\"],\n  \"properties\": {\n    \"username\": {\"type\": \"string\", \"title\": \"clubmanager365 username\",\n                 \"x-from\": {\"header\": \"x-cm365-username\"}},\n    \"password\": {\"type\": \"string\", \"format\": \"password\",\n                 \"title\": \"clubmanager365 password\",\n                 \"x-from\": {\"header\": \"x-cm365-password\"}}\n  }\n}'\n```\n\n## The booking API\n\nAll booking actions go through `/Club/ActionHandler.ashx`, with the request\nobject serialised as a JSON string carried on the request, matching the calls\nthe site's own front-end makes:\n\n```\n/Club/ActionHandler.ashx?siteCallback=CourtCallback&action=GetCourtDay&_=<ts>&{\"Date\":\"27 Jun 2026\",...}\n```\n\nWhen a booking takes no payment at booking time (e.g. it's covered by\nmembership or court credits), booking is a **single `MakeBooking` call** — no\npreliminary hold and no `BookingPlayerID` are needed. `GetCourtDay` lists the\nslots (each cell carries a `CourtSlotID`; each column a `CourtID`), and\n`MakeBooking` takes `CourtsRequired: [{c: CourtID, s: CourtSlotID}]` plus\n`OpponentPlayerIDs`, `SelectedMatchType`, `MatchDate`, etc. Clubs that take\npayment instead go through `SaveNewPreliminaryBooking` (a short-lived hold)\nbefore confirming — that path isn't exercised here.\nSee [`cm365/bookings.py`](cm365/bookings.py).\n\n## How login works\n\nThe site is ASP.NET WebForms. Logging in is a \"postback\" on the homepage:\n\n1. `GET /Homepage.aspx` → session cookie + hidden `__VIEWSTATE`,\n   `__VIEWSTATEGENERATOR`, `__EVENTVALIDATION`.\n2. `POST /Homepage.aspx` echoing those hidden fields plus\n   `…UserLogin$UserName`, `…UserLogin$Password`, `…UserLogin$LoginSubmitButton`.\n3. The `<asp:LoginView>` widget swaps to its authenticated template; cookies\n   now carry the session.\n\nSee [`cm365/client.py`](cm365/client.py).\n"}