AppFolio MCP Server
by swagger3x
README.md
# AppFolio MCP Server v2
A Model Context Protocol (MCP) server that wraps AppFolio's REST API so Claude can call it natively in any conversation — the same way Quo works. **38 tools** covering portfolio structure, leasing, financials, maintenance, and admin.
When this is deployed and connected, you can say things like:
- "Pull the rent roll for Greensborough Village."
- "Build a Cash P&L for 215 Union St for Q1 2026."
- "Show me work orders open more than 30 days."
- "Which vendors have expired insurance?"
- "What's our monthly cash flow at Russellville this year?"
And Claude calls the AppFolio API directly.
---
## ⚠️ Important note on budgets
**AppFolio's public API does NOT expose a `/budgets` endpoint.** I can pull *actuals* from the GL all day (that's what `build_pl_summary` and `build_cash_flow` do), but the budget side has to come from somewhere else:
- AppFolio's UI exports (you can download budget reports as XLSX from inside AppFolio)
- A Google Sheet of your budgets, attached to a Claude conversation
- A planning doc in Notion or Drive
Once budgets are available in the conversation, Claude can do budget-vs-actual comparisons against the actuals this server returns. If AppFolio adds a budgets endpoint to a newer API version, the pattern to add it is identical to the existing tools — let me know and I'll wire it up.
What this server gives you on the financial side:
- **P&L summary** (`build_pl_summary`) — income / expense / NOI by GL account for a property and date range
- **Cash flow by month** (`build_cash_flow`) — monthly trend of cash in / cash out / net
- All the raw underlying data (`get_gl_details`, `get_gl_accounts`, `get_journal_entries`, `get_bank_accounts`)
For the actual official month-end reports you sign off on, keep using AppFolio's UI — this server is for the day-to-day questions you'd otherwise have to log in to find.
---
## What you'll need before starting
1. **AppFolio API credentials** (you have these):
- Client ID
- Client Secret
2. **AppFolio Developer ID** — log in to AppFolio → click your account name (top right) → **Admin** → look for the **Developer ID** card. Copy that UUID.
3. **A Cloudflare account** (free tier): https://dash.cloudflare.com/sign-up
4. **Node.js 20+** installed locally: https://nodejs.org
5. **Git** (optional).
No server to rent, no domain to buy. Cloudflare gives you a free `*.workers.dev` URL.
---
## Step-by-step deployment (~15 minutes the first time)
### 1. Install dependencies
```bash
npm install
```
### 2. Log into Cloudflare
```bash
npx wrangler login
```
### 3. Set the three secrets
```bash
npx wrangler secret put APPFOLIO_CLIENT_ID
npx wrangler secret put APPFOLIO_CLIENT_SECRET
npx wrangler secret put APPFOLIO_DEVELOPER_ID
```
Paste each value when prompted.
### 4. Deploy
```bash
npm run deploy
```
You'll get a URL like `https://appfolio-mcp.<your-subdomain>.workers.dev`. Test it in a browser — root should return a JSON health check showing 38 tools.
### 5. Connect it to Claude
1. **Settings → Connectors → Add custom connector**
2. **Name:** `AppFolio`
3. **URL:** `https://appfolio-mcp.<your-subdomain>.workers.dev/sse`
4. Save.
Claude discovers all 38 tools. Done.
---
## Tool catalog
### Portfolio & structure (7)
| Tool | Purpose |
| ----------------------- | ---------------------------------------------------------- |
| `get_properties` | List properties; filter by type or date |
| `get_property_groups` | Property groupings (e.g. by city or manager) |
| `get_portfolios` | Top-level portfolios |
| `get_units` | Units across portfolio |
| `get_unit_types` | Unit type / floor-plan definitions |
| `get_owners` | Property owners |
| `get_owner_groups` | Ownership groups + percentages |
### Leasing & occupancy (8)
| Tool | Purpose |
| ----------------------------- | ---------------------------------------------------------- |
| `get_tenants` | Tenants; filter by status (Current/Past/Future/Notice) |
| `get_rent_roll` | **Composite** — units + current tenants joined |
| `get_leases` | Lease records |
| `get_lease_renewal_pricings` | Approved renewal pricing options |
| `get_leads` | Leasing leads / guest cards |
| `get_rental_applications` | Applications by property/unit/date |
| `get_showings` | Tour appointments |
| `get_listings` | Active marketing listings |
### Financial — receivables (4)
| Tool | Purpose |
| -------------------------- | ---------------------------------------------------- |
| `get_charges` | Open charges (good for duplicate detection) |
| `get_delinquent_charges` | Past-due charges |
| `get_recurring_charges` | Monthly/recurring billings |
| `get_tenant_ledgers` | Charges + payments + credits by occupancy |
### Financial — payables (2)
| Tool | Purpose |
| ---------------- | ----------------------------- |
| `get_bills` | Accounts payable |
| `get_payables` | Outgoing payments / checks |
### Financial — GL & reporting (6)
| Tool | Purpose |
| --------------------- | ------------------------------------------------------------------------ |
| `get_gl_accounts` | Chart of accounts |
| `get_gl_details` | GL transaction detail (raw) |
| `get_journal_entries` | Manual journal entries |
| `get_bank_accounts` | Bank accounts |
| `build_pl_summary` | **Composite** — P&L by GL account for property + date range |
| `build_cash_flow` | **Composite** — monthly cash in/out/net by month |
### Maintenance (8)
| Tool | Purpose |
| ----------------------------- | ------------------------------------------------------------------- |
| `get_work_orders` | Work orders; filter by status |
| `get_work_order_attachments` | Photos/PDFs on work orders |
| `get_open_work_orders` | **Composite** — open WOs with vendor + age |
| `get_work_order_aging` | **Composite** — WOs bucketed by 0-7/8-30/31-60/61-90/90+ days |
| `get_vendors` | Vendor list |
| `get_vendor_compliance` | **Composite** — expired/expiring insurance/contracts/workers comp |
| `get_inventories` | Maintenance parts/supplies inventory |
| `get_inventory_locations` | Where inventory is stored |
### Admin (3)
| Tool | Purpose |
| ------------------- | --------------------------------------------- |
| `get_users` | AppFolio Property Manager users |
| `get_custom_fields` | Custom field definitions |
| `get_jobs` | Status of bulk async jobs (from bulk-creates) |
All list-type tools auto-paginate up to 3 pages by default (300 records). Override with `max_pages` (cap 10).
---
## Example prompts that exercise the composite tools
**Rent roll**
> "Pull the rent roll for property `<id>` and tell me what % is occupied vs vacant."
**P&L summary**
> "Build a Cash P&L for Greensborough (`<id>`) from 2026-01-01 to 2026-03-31. Show NOI."
**Cash flow trend**
> "Run a cash flow report for Russellville for the last 12 months. Highlight the months below $5K net."
**Open work orders by age**
> "Show me work orders open more than 60 days, grouped by property."
**Vendor compliance**
> "Which vendors have insurance expiring in the next 30 days?"
---
## Local development
```bash
cp .dev.vars.example .dev.vars
# Fill in real credentials in .dev.vars (gitignored).
npm run dev
```
Local URL: `http://localhost:8787/sse`.
---
## Logs and debugging
```bash
npm run logs
```
Or Cloudflare dashboard → Workers & Pages → `appfolio-mcp` → Logs.
Common issues:
- **401 from AppFolio:** Credentials wrong or missing Developer ID. Re-run all three `wrangler secret put` commands.
- **403 from AppFolio:** Your API user lacks permission for that endpoint. AppFolio gates each endpoint individually on the developer user's profile. Log into AppFolio → Admin → API and check what's enabled.
- **`get_gl_details` returning 400:** It requires `date_from` + `date_to` AND `property_id` unless the window is ≤2 days. The composite tools (`build_pl_summary`, `build_cash_flow`) handle this for you.
- **429 (rate limit):** AppFolio limits are 8/sec, 256/min, 4096/hour. The server retries once. Composite tools can hit rate limits if you query huge date ranges — narrow your window or wait.
- **Claude says "tool not found":** Refresh the connector (remove and re-add).
---
## Adding write operations (CAUTION)
This version is **read-only on purpose.** Writes (creating bills, updating leads, posting charges) touch money and legal documents. Before adding any `create_*` or `update_*` tool:
1. **Test in an AppFolio sandbox database** — never your live one for first runs.
2. **Add a confirmation gate** — make Claude require you to type "yes, post bill $X to vendor Y" before the tool fires.
3. **Log everything** — write to a KV store, R2 bucket, or external log service so you have an audit trail.
4. **Scope the API user** — create a separate AppFolio developer user with only the permissions needed for those specific writes, not your master one.
When you're ready, ask me to add a specific write tool with these safeguards.
---
## Architecture
```
┌─────────────┐ MCP/SSE ┌──────────────────┐ HTTPS+Basic ┌──────────────┐
│ Claude UI │ ◄────────────────►│ Cloudflare │ ◄────────────────►│ AppFolio │
│ │ │ Worker (this) │ │ REST API │
└─────────────┘ └──────────────────┘ └──────────────┘
│
▼
Cloudflare Secrets
(CLIENT_ID, SECRET,
DEVELOPER_ID)
```
Composite tools (`build_pl_summary`, `build_cash_flow`, `get_rent_roll`, `get_open_work_orders`, `get_work_order_aging`, `get_vendor_compliance`) make multiple AppFolio API calls server-side and stitch the results together before returning. This keeps Claude's context clean and respects rate limits.
Cost: well within Cloudflare's free tier for personal/small-team use.
---
## Files
```
appfolio-mcp/
├── src/
│ ├── index.ts # Worker entrypoint — wires up tool modules, /sse + /mcp routing
│ ├── client.ts # AppFolio REST client — auth, pagination, 429 retry
│ ├── util.ts # Shared helpers + zod schemas (dateFromSchema, maxPagesSchema, ...)
│ ├── list-tool.ts # Factory for the common "list entities" tool shape
│ └── tools/
│ ├── portfolio.ts # Portfolio & structure (7)
│ ├── leasing.ts # Leasing & occupancy (8), incl. rent roll composite
│ ├── financial.ts # Receivables, payables, GL & reporting (12), incl. P&L/cash flow composites
│ ├── maintenance.ts # Work orders, vendors, inventory (8), incl. composites
│ └── admin.ts # Admin (3)
├── package.json # Dependencies + npm scripts
├── tsconfig.json # TypeScript config
├── wrangler.toml # Cloudflare Worker config
├── .dev.vars.example # Template for local dev secrets
├── .gitignore
└── README.md # You are here
```
Most simple list-style tools (properties, units, owners, vendors, etc.) are registered via `registerListTool` in `list-tool.ts` to avoid repeating the same id/property_id/last-updated/pagination boilerplate ~20 times. Tools with required fields, "one of X is required" validation, or multi-call composite logic (`build_pl_summary`, `get_rent_roll`, `get_vendor_compliance`, etc.) are hand-written in their category file.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues