<!--
Machine-oriented configuration reference for AI agents and MCP clients connecting to the PayRent Landlord MCP server. Live as of 08-09-2026.
-->

# PayRent Landlord MCP — Install Reference

Machine-oriented configuration reference for AI agents and MCP clients connecting to the
PayRent Landlord MCP server. This file is not marketing copy; it exists so an agent can
self-configure a connection correctly. For human-readable product information, see the
public docs page linked from `resource_documentation` in the protected resource metadata
(§1).

- **Transport:** Streamable HTTP (JSON request/response over `POST /mcp`), not stdio.
- **Protocol version:** `2025-06-18` (server also accepts `2025-03-26`).
- **Auth:** OAuth 2.1, authorization code grant with mandatory PKCE (S256 only).
- **Base URL:** `https://api.payrent.com/v3`

---

## 1. Discovery

Two discovery documents. Fetch protected resource metadata first; it points at the
authorization server metadata.

### `GET https://api.payrent.com/v3/.well-known/oauth-protected-resource`
(also served at `/.well-known/oauth-protected-resource/mcp`)

RFC 9728 Protected Resource Metadata. Returns:

```json
{
  "resource": "https://api.payrent.com/v3/mcp",
  "authorization_servers": ["https://api.payrent.com/v3"],
  "scopes_supported": ["mcp:tools:read", "mcp:tools:write", "offline_access"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://api.payrent.com/v3/docs/mcp"
}
```

### `GET https://api.payrent.com/v3/.well-known/oauth-authorization-server`
(also served at `/.well-known/openid-configuration`)

RFC 8414 Authorization Server Metadata. Returns:

```json
{
  "issuer": "https://api.payrent.com/v3",
  "authorization_endpoint": "https://api.payrent.com/v3/oauth/authorize",
  "token_endpoint": "https://api.payrent.com/v3/oauth/token",
  "registration_endpoint": "https://api.payrent.com/v3/oauth/register",
  "revocation_endpoint": "https://api.payrent.com/v3/oauth/revoke",
  "jwks_uri": "https://api.payrent.com/v3/oauth/jwks",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "scopes_supported": ["mcp:tools:read", "mcp:tools:write", "offline_access"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "resource_indicator_parameter_supported": true
}
```

An unauthenticated `POST /mcp` returns `401` with a `WWW-Authenticate` header pointing at
the protected resource metadata URL — use this as the discovery trigger if you're probing
`/mcp` directly instead of starting from the well-known paths.

---

## 2. Registration

### `POST https://api.payrent.com/v3/oauth/register`

RFC 7591 Dynamic Client Registration. No pre-provisioned `client_id` needed.

Request body:

| Field | Required | Notes |
|---|---|---|
| `redirect_uris` | yes | Array of strings, or a single string. Must be syntactically valid URLs. |
| `client_name` | no | Defaults to `"MCP Client"`, truncated to 255 chars. |
| `grant_types` | no | Defaults to `["authorization_code", "refresh_token"]`. |
| `response_types` | no | Defaults to `["code"]`. |
| `token_endpoint_auth_method` | no | `"none"` (public client, default) or `"client_secret_post"` (confidential client). |

Response (`201`):

```json
{
  "client_id": "mcp_<uuid-no-dashes>",
  "client_name": "...",
  "redirect_uris": ["..."],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1234567890,
  "client_secret": "..."
}
```

`client_secret` is present only when `token_endpoint_auth_method` is not `"none"`.

Rate-limited to 20 registrations/minute per source IP (server-configurable). A `429` with
`{"error": "rate_limit_exceeded"}` means back off and retry later — do not immediately
retry.

---

## 3. Authorization (PKCE mandatory)

### `GET https://api.payrent.com/v3/oauth/authorize`

Query parameters:

| Param | Required | Notes |
|---|---|---|
| `response_type` | yes | Must be `code`. |
| `client_id` | yes | From registration. |
| `redirect_uri` | yes | Must exactly match one of the client's registered `redirect_uris` (exact string match, no prefix/host matching). |
| `code_challenge` | yes | PKCE challenge. |
| `code_challenge_method` | yes | Must be `S256`. Plain PKCE is not accepted. |
| `resource` | no | Defaults to the canonical MCP resource URL (`https://api.payrent.com/v3/mcp`). If supplied, must match it exactly (trailing slash tolerant). |
| `scope` | no | Space-separated. Defaults to `mcp:tools:read`. Requesting `mcp:tools:write` automatically implies `mcp:tools:read`. Include `offline_access` to receive a refresh token. |
| `state` | no | Opaque value round-tripped back to your redirect URI. Recommended. |

This endpoint renders a PayRent-hosted login page (Cognito email/password) followed by a
consent screen — both are human-facing HTML, not JSON. The landlord authenticates and
approves scopes there; your client does not need to (and cannot) do this programmatically.
On approval, PayRent redirects to your `redirect_uri` with `code` and `state`.

Landlord identity requirements enforced at login (not agent-actionable — surface to the
human if encountered): the Cognito account must be linked to a PayRent landlord record,
and that landlord's service plan must have the `MCP_ACCESS` feature enabled (§6).

---

## 4. Token exchange

### `POST https://api.payrent.com/v3/oauth/token`

**`grant_type=authorization_code`**

| Field | Required |
|---|---|
| `grant_type` | `authorization_code` |
| `client_id` | yes |
| `code` | yes |
| `redirect_uri` | yes — must match the value used in the authorize request |
| `code_verifier` | yes — PKCE verifier |
| `client_secret` | required only if your client is confidential (`token_endpoint_auth_method != "none"`) |
| `resource` | no — defaults to canonical MCP resource URL |

Authorization codes are single-use and expire in 5 minutes.

**`grant_type=refresh_token`**

| Field | Required |
|---|---|
| `grant_type` | `refresh_token` |
| `client_id` | yes |
| `refresh_token` | yes |

Refresh tokens rotate on every use: the response includes a new `refresh_token`, and the
one you sent becomes invalid. Refresh tokens default to a 30-day TTL. **Reuse of an
already-rotated (or revoked) refresh token revokes the entire token family** — if this
happens, all tokens issued from that authorization are dead and the client must
re-authorize from scratch (§3). This is a security control, not a bug; do not retry with
the same old refresh token.

**Response (both grants, `200`):**

```json
{
  "access_token": "<RS256 JWT>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mcp:tools:read mcp:tools:write",
  "refresh_token": "<opaque, present only if offline_access was granted>"
}
```

Access tokens are RS256-signed JWTs, default 1-hour TTL (server-configurable). Verify only
if you need to introspect claims yourself — normally you just forward the token as a
bearer credential. Public key set: `GET https://api.payrent.com/v3/oauth/jwks`.

### `POST https://api.payrent.com/v3/oauth/revoke`

RFC 7009. Body: `{"token": "...", "token_type_hint": "refresh_token" | "access_token"}`
(`token_type_hint` optional). Always returns `200` regardless of whether the token was
found or valid — do not treat a `200` here as confirmation the token was actually active.

---

## 5. Calling the MCP endpoint

### `POST https://api.payrent.com/v3/mcp`

Streamable HTTP JSON-RPC 2.0. Send `Authorization: Bearer <access_token>` on every request.
Standard methods: `initialize`, `notifications/initialized`, `ping`, `tools/list`,
`tools/call`. (`resources/list` and `prompts/list` are implemented and return empty
arrays — this server exposes tools only.)

`tools/call` request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "<tool_name>", "arguments": {"...": "..."}}
}
```

Tool-level failures (insufficient scope, missing plan feature, unknown tool, missing
confirmation) are returned as a normal JSON-RPC **result** with `isError: true` and a
`structuredContent` object containing a `code` field — not as an HTTP error status or a
JSON-RPC protocol-level error. Check `result.isError` and `result.structuredContent.code`
after every `tools/call`, not just the HTTP status.

Known `structuredContent.code` values:

| Code | Meaning | Agent action |
|---|---|---|
| `INSUFFICIENT_SCOPE` | Token lacks `mcp:tools:read` or `mcp:tools:write` for this tool | Re-authorize with the required scope (§3); do not retry with the same token. |
| `PLAN_FEATURE_REQUIRED` | Landlord's service plan lacks a required feature (`feature` field names it: `MCP_ACCESS`, `RENT_REMINDERS`, or `ADDITIONAL_ASSESSMENTS`) | Tell the landlord their plan doesn't include this capability; do not retry. See §6. |
| `UNKNOWN_TOOL` | Tool name not recognized (includes the two tools intentionally excluded from MCP — see §7) | Do not retry; the tool is not available over MCP. |
| `CONFIRMATION_REQUIRED` | A confirmation-gated mutation was called without `user_confirmed: true` | Summarize the exact change to the human, get explicit agreement, then call again with `user_confirmed: true`. Never set it preemptively (§8). |

---

## 6. The `MCP_ACCESS` plan gate

Every tool call additionally requires the landlord's PayRent service plan to have the
`MCP_ACCESS` feature enabled. This is enforced twice:

- At the whole-request level: if the authenticated landlord's plan lacks `MCP_ACCESS`,
  **every** `POST /mcp` call returns HTTP `403` with
  `{"error": "unauthorized", "error_description": "MCP_ACCESS required on service plan"}`
  — including `tools/list`. A valid, unexpired access token does not override this; the
  plan is re-checked on every request.
- At token issuance: if the plan lacks `MCP_ACCESS` at the moment of exchanging an
  authorization code or refresh token, `/oauth/token` returns `403` with
  `{"error": "access_denied", "error_description": "MCP_ACCESS required"}` (or
  `{"error": "access_denied"}` on the refresh path, where the entire refresh token family
  is also revoked).

**A `403` with either of these bodies means the landlord's plan does not include MCP
access — it is not a transient failure.** Do not retry. Tell the landlord to check their
PayRent plan / contact PayRent support about enabling this feature, then have them restart
the connection flow from §3 once resolved.

---

## 7. Tool reference

23 tools are exposed over MCP. Two tools that exist in PayRent's internal tool set are
**not** available here and will return `UNKNOWN_TOOL` if called:

- `refund_renter_payment` — real payment-processor refund (money movement). Excluded per
  Anthropic Connectors Directory policy against connectors that transfer money. Not
  available through any MCP client; the landlord must use the PayRent web app.
- `navigation_hints` — internal PayRent web-app router paths. Meaningless outside the
  PayRent UI.

Every tool additionally requires `MCP_ACCESS` (§6). "Plan feature" below lists any
*additional* requirement beyond `MCP_ACCESS`. Parameter names, types, and required fields
are taken directly from the tool's JSON Schema in
`services/landlord-agent/bedrock-tools.js` — treat that file as the source of truth if this
table and the schema ever disagree.

### Read tools — scope: `mcp:tools:read`

| Tool | Description | Required params | Plan feature |
|---|---|---|---|
| `get_dashboard_summary` | Portfolio metrics: occupancy, unit counts, financial summary. No params. | — | — |
| `get_service_plan_details` | The landlord's current plan: display name, fee schedule, property limit, trial days, enabled/disabled feature flags. No params. | — | — |
| `list_properties` | List rentable units (child properties). Each row includes `id` (unit) and `parent_property_id` (building). | — | — |
| `list_renters` | List renters with billing account fields (balance, status). Defaults to current (`billing_account_state` `ACTIVE`); pass `PENDING` (future/invites) or `NOT_ACTIVE` (previous) when asked. Optional `status` (`PASTDUE`\|`CURRENT`\|`INACTIVE`), `search`, `limit`, `page`. | — | — |
| `list_landlord_payments` | ACH/card payments across the portfolio. Optional `renter_id` (omit for all renters), `start_date`, `end_date` (YYYY-MM-DD), `limit` (default 250, max 500), `page`. Response includes `meta.has_more`. | — | — |
| `list_renter_transactions` | ACH/card payments for one renter. Same pagination/date shape as `list_landlord_payments`. | `customer_id` | — |
| `list_billing_items` | Ledger charge/credit line items for one billing account. Pass `billing_account_id`, or `renter_id` (+ optional `property_id`). Optional `start_date`, `end_date`, `limit`, `page`. | — (one of `billing_account_id` or `renter_id` needed for a meaningful result, but not schema-enforced) | — |
| `search_help` | Searches PayRent's support site for how-to content. Not for account-specific data. | `query` | — |
| `lookup_processor_payment` | Live Finix/Thryve lookup for one payment; use only when `list_landlord_payments` data is insufficient. Optional `processor` (`finix`\|`thryve`) must match the payment's gateway if provided. | `payment_id` | — |
| `list_settlements` | Payout settlements for the landlord. Optional `property_group_id`, `property_group_name`, `date_from`, `date_to`, `status` (comma-separated: pending, completed, failed, error), `sort_by`, `sort_order`, `page`, `size` (default 10, max 100). | — | — |
| `get_settlement` | Full detail for one settlement: gross/net amounts, charges, fees, constituent line items. | `settlement_id` (the `id` from a `list_settlements` row) | — |
| `get_settlement_transfers` | Transaction-level detail for one settlement (same data as the app's settlement CSV export). Finix settlements make a live paginated Finix API call; Thryve is derived from local records. Optional `limit`, `after_cursor` (Finix pagination only). | `settlement_id` | — |

### Mutation tools — scope: `mcp:tools:write` (implies `mcp:tools:read`)

`*` = requires `user_confirmed: true` — see §8 for exactly when.

| Tool | Description | Required params | Plan feature |
|---|---|---|---|
| `invite_renter_to_property` | New invite (default mode): needs `property_id`, `first_name`, `last_name`, `lease_start_date`, `lease_end_date`, and one of `email`/`cell_phone`. Resend mode: `resend_invite: true` + `renter_id`. Delete mode\*: `delete_pending_invite: true` + `renter_id` + `user_confirmed: true`. | Varies by mode (see description) | — |
| `create_parent_property` | Create a building shell. `property_group_id` optional (defaults to the landlord's default portfolio group). | `address`, `city`, `state`, `zip` | — |
| `create_property_unit` | Add a unit under a parent building. Address/city/state/zip inherited from parent if omitted; geocoding auto-handled unless `google_place_id`+`lat`+`long` all supplied. | `rent_amount`, `rent_due_day`, `parent_property_id` | — |
| `update_property`\* | Patch one unit (not the parent building). | `property_id`, `user_confirmed: true` | — |
| `delete_property`\* | Delete one unit. Does not delete the parent building or sibling units. | `property_id`, `user_confirmed: true` | — |
| `update_parent_property`\* | Update the parent building; address changes sync to child units. | `parent_property_id`, `address`, `city`, `state`, `zip`, `user_confirmed: true` | — |
| `delete_parent_property`\* | Delete the parent building and its child units. Fails server-side if any unit has an active renter billing account. | `parent_property_id`, `user_confirmed: true` | — |
| `create_billing_item` | Post a charge/credit/fee. `transaction_type` must be one of: Security Deposit, Fee, Credit, Payment, Rent Charge, Refund, Utility, Discount, Late Fee, Parking, Move In, Move Out. | `transaction_type`, `amount`, `billing_account_id`, `renter_user_id` | `ADDITIONAL_ASSESSMENTS` — **only** when `transaction_type` is `Late Fee`, `Parking`, or `Fee`. Not required for other transaction types. |
| `reset_renter_balance` | Zero a renter's billing balance (posts a corrective Credit or Fee, memo "Reset Account"). No-op if balance is already zero. | `property_id` | — |
| `delete_billing_item`\* | Delete a charge/credit line item. Cannot delete payment transactions. | `billing_item_id`, `user_confirmed: true` | — |
| `send_email_to_renter`\* | Queue an email to a renter (delivered via SES). Content is sanitized and wrapped in PayRent email chrome server-side; the server may reject unprofessional content. | `renter_id`, `subject`, `html_body`, `user_confirmed: true` | `RENT_REMINDERS` |

---

## 8. Confirmation gating (`user_confirmed: true`)

Do not set `user_confirmed: true` speculatively or on a first draft. It must be `true`
(the literal boolean, not the string `"true"` — the server rejects a truthy string) and
must only be set **after** the human has seen the specific, concrete change and explicitly
agreed to it in the current conversation. Calling one of these tools without it returns
`CONFIRMATION_REQUIRED` (§5); this is expected on a first attempt and is not an error to
route around — it is the intended checkpoint.

Tools requiring `user_confirmed: true` unconditionally:

- `update_property`
- `delete_property`
- `update_parent_property`
- `delete_parent_property`
- `delete_billing_item`
- `send_email_to_renter` — additionally, per the tool's own description: never set
  `user_confirmed: true` on the same turn the draft subject/body is first shown to the
  human. The human must see the full subject and body first, then explicitly approve, then
  the tool is called with `user_confirmed: true`.

Tool requiring `user_confirmed: true` conditionally:

- `invite_renter_to_property` — **only** when `delete_pending_invite: true` (withdrawing a
  pending invite). Creating a new invite or resending an existing one does not require
  confirmation.

All other mutation tools (`create_parent_property`, `create_property_unit`,
`create_billing_item`, `reset_renter_balance`) execute immediately on a valid call, with no
confirmation gate.

---

## 9. Ambiguities / not independently verified

Flagged rather than guessed at:

- `list_billing_items` has no `required` array in its JSON Schema (neither
  `billing_account_id` nor `renter_id` is schema-required), but the tool is only
  meaningful with one of them supplied. Behavior when both are omitted was not traced
  through `read-tools.js` for this reference — do not assume it returns all billing items
  for the landlord's whole portfolio without confirming.
- The exact production/staging base URL, hostname, and whether a custom domain path prefix
  (e.g. `/v3`) applies to `/mcp` and the OAuth endpoints is not yet determined — this
  server has not been deployed to any stage as of this writing. Confirm the real base URL
  from the live discovery documents (§1) at connect time rather than assuming a pattern
  from this file.
- `Mcp-Protocol-Version` header handling: the server currently accepts `2025-06-18` and
  `2025-03-26` without rejecting other values (no explicit error or warning is emitted for
  a mismatched header). Don't rely on the server to reject an incompatible protocol version
  for you.
