Skip to main content
Glama
swagger3x

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.