Skip to main content
Glama
Rentvine MCP

Rentvine MCP

by Rentor-CA

Rentvine MCP (Rentor fork)

MCP server for Rentvine — gives Claude (and any MCP client such as Voice Agents) live access to your property management data.

Rentvine stopped maintaining their MCP Server on Apr 25, 2026 leaving Property Management Companies to fork it and continue maintaing it themselves.

Fork notice. This is Rentor's fork of the upstream rentvine-mcp npm package (v1.3.2, MIT, by Base Homes). Upstream is deprecated on npm and its GitHub repo has been deleted, so this repo is now the maintained line. TypeScript sources here were reconstructed from the published dist/ — the original package shipped compiled JS only.

Do not npm install rentvine-mcp — that pulls the dead upstream. Build from this repo instead.

Available Tools

Tool

Description

list_properties

All properties with address, type, and active status

list_units

Units for a named property with vacancy and rent

list_leases

All leases with tenant, rent, dates, and status

list_applications

Rental applications with applicant and status

list_inspections

Maintenance inspections with date and inspector

list_work_orders

Work orders with status and priority

create_work_order

Create a new maintenance work order

update_work_order

Update status, priority, cost, or scheduling on a work order

list_tenants

All tenants with contact details and status; search / active_only filters. Withholds PII unless include_sensitive=true

get_tenant_balance

Ledger balance for a named tenant

list_owners

All property owners

list_vendors

All vendors with full contact, insurance, billing, and audit fields (45 fields)

get_vendor

Single vendor's full detail record — includes code field, website, name components, QuickBooks linkage

vendors_near

Vendors within N miles of a property, sorted by distance (ZIP-centroid approximation)

list_portfolios

All portfolios

list_bills

All bills

create_bill

Create a new bill

search_transactions

Search accounting transactions by date, amount, or keyword

list_accounts

Chart of accounts

list_object_types

Rentvine object type IDs (for file attachment)

upload_file

Upload a file and attach it to a property, unit, lease, or work order

list_attachments

Files attached to any Rentvine object (by object ID + type)

list_work_order_attachments

Images and files attached to a specific work order

get_file

Metadata for a single file (name, size, mime type) — no download

download_file

Download a file as base64 (images, PDFs, up to ~375 KB)


Related MCP server: Property MCP Server

Install

This is a hosted service, not a local tool. Deploy it once on a Linux server under pm2, front it with nginx over HTTPS, and point every client at that URL. Claude, ChatGPT, voice agents, and teammates all connect to the same endpoint with a bearer token.

The server holds your Rentvine API credentials. Clients hold only the endpoint URL and MCP_AUTH_TOKEN — no keys are distributed to laptops.

Run one process per Rentvine account, each on its own port:

Environment

Public (nginx)

Internal (node)

prod

https://your-host.example.com:8003/mcp

127.0.0.1:18003

dev

https://your-host.example.com:8004/mcp

127.0.0.1:18004

Prerequisites: Ubuntu (or similar), Node.js 18+, nginx, and a TLS certificate for your host (certbot is fine). Plus your Rentvine API credentials from Settings → Users, Roles & API.

1. Get the code onto the server

Option A — servers with no git/npm access (zip upload). Build a self-contained zip on your laptop, upload it, unzip it. No build step, no npm install, and no network needed on the server:

# on your laptop, in the repo
npm run package
# -> build/rentvine-mcp-<version>-<sha>.zip   (~6 MB)

The zip contains dist/, node_modules/ (production deps only), package.json, start-mcp.sh.example, and a DEPLOY.txt with these same steps. Upload it however you like (scp, or any file host + curl -LOJ), then:

sudo mkdir -p /opt/rentvine-mcp
sudo chown -R "$USER":"$USER" /opt/rentvine-mcp

unzip -o rentvine-mcp-*.zip -d /tmp
cp -r /tmp/rentvine-mcp/. /opt/rentvine-mcp/
cd /opt/rentvine-mcp

Re-run npm run package after every change and repeat the upload — the server has no way to rebuild. The filename carries the version and git SHA (plus -dirty for uncommitted changes) so you can tell what's deployed.

Requires Node.js 18+ on the server. Nothing else.

Install to /opt/rentvine-mcp, owned by the user that will run pm2 — not root:

sudo mkdir -p /opt/rentvine-mcp
sudo chown -R "$USER":"$USER" /opt/rentvine-mcp

git clone https://github.com/Rentor-CA/Rentvine-MCP.git /opt/rentvine-mcp
cd /opt/rentvine-mcp
npm install                   # do NOT set NODE_ENV=production here
npm run build

/opt is root-owned by default, so cloning into it as a normal user fails with Permission denied — hence the chown. Don't work around that with sudo git clone: everything ends up root-owned, and then git pull, npm install, and npm run build all need sudo too, while pm2 runs as your login user and can't write its own build output. npm also behaves badly as root.

Keep the directory owned by the same user that runs npx pm2 — pm2 startup installs a systemd unit bound to one specific user, and ~/.pm2 lives in that user's home.

Do not set NODE_ENV=production for the install. npm skips devDependencies, TypeScript never installs, and npm run build dies with tsc: not found. Install normally, build, then set NODE_ENV when you run the server. To slim the install afterwards: npm prune --omit=dev.

Use the HTTPS clone URL — the repo is public, so it needs no credentials. The SSH form (git@github.com:…) requires a key on the machine regardless of repo visibility, so save it for boxes where you intend to push.

This runs from the clone and installs nothing globally, so it won't disturb an existing rentvine-mcp (the legacy upstream package) already on the box — run both on different ports while you migrate, then retire the old one.

Either way, the legacy rentvine-mcp already on the box is untouched — give the new instance its own port (e.g. 18005 internal / 8005 public) and run both until you cut over.

2. Create the launcher

cp start-mcp.sh.example start-mcp.sh
chmod +x start-mcp.sh
$EDITOR start-mcp.sh          # fill in keys, subdomains, and MCP_AUTH_TOKEN

Generate a token per environment with openssl rand -hex 32. start-mcp.sh is gitignored — it holds live credentials and must never be committed.

The script resolves dist/http.js relative to itself, validates that every credential is set, and execs node so pm2 supervises the server directly instead of a wrapper shell.

3. Start under pm2

npx pm2 start ./start-mcp.sh --name rentvine-prod -- prod
npx pm2 start ./start-mcp.sh --name rentvine-dev  -- dev

npx pm2 save                  # persist the process list
npx pm2 startup               # prints a command to run — restarts pm2 on boot

The -- prod after the script name is what gets passed to the script as $1, selecting the credential block. Everything before -- is a pm2 flag.

pm2 save plus pm2 startup are what make this survive a reboot. save writes the current process list to ~/.pm2/dump.pm2; startup prints a sudo command you must actually run to install the systemd unit. Run both, or the processes are gone after a restart. Re-run pm2 save any time you add or rename a process.

pm2 command reference

Inspect

npx pm2 list                    # status table: name, pid, uptime, restarts, cpu, memory
npx pm2 describe rentvine-dev   # full details for one process: script path, args, log paths, env
npx pm2 monit                   # live dashboard (cpu/mem/logs); q to quit
npx pm2 jlist                   # same as list but JSON — for scripting/monitoring
npx pm2 prettylist              # JSON, human-formatted

describe is the one to reach for when a process behaves unexpectedly — it shows the resolved script path, the -- prod/-- dev argument it started with, its restart count, and where its logs live.

Logs

npx pm2 logs                          # tail all processes, interleaved
npx pm2 logs rentvine-prod            # tail one
npx pm2 logs rentvine-prod --lines 200  # last 200 lines then follow
npx pm2 logs --err                    # stderr only — startup failures land here
npx pm2 flush                         # truncate all log files

Logs are written to ~/.pm2/logs/<name>-out.log and <name>-error.log. They are not rotated by default; install pm2-logrotate to avoid filling the disk:

npx pm2 install pm2-logrotate

Lifecycle

npx pm2 restart rentvine-prod   # restart one
npx pm2 restart all             # restart everything
npx pm2 stop rentvine-dev       # stop but keep it in the list
npx pm2 start rentvine-dev      # start a stopped process by name
npx pm2 delete rentvine-dev     # remove from pm2 entirely (then pm2 save)

restart fully replaces the process, so it re-reads start-mcp.sh and picks up credential changes. Editing the script alone changes nothing until you restart.

pm2 reload (zero-downtime) does not help here — it is for clustered Node apps, and these are forked shell scripts.

Persistence

npx pm2 save                    # snapshot current process list
npx pm2 resurrect               # restore from the snapshot
npx pm2 startup                 # print the boot-persistence install command
npx pm2 unstartup               # undo it

Upgrading to a new version

cd /opt/rentvine-mcp
git pull
npm install
npm run build
npx pm2 restart all
npx pm2 logs --lines 20         # confirm both came back clean

4. Expose it over HTTPS

The Node server listens on loopback only — reachable from the server itself, not from the internet. nginx holds the public TLS port and forwards inward. One public port per environment:

internet ──▶ your-host:8003 (nginx, TLS) ──▶ 127.0.0.1:18003 (node)  prod
internet ──▶ your-host:8004 (nginx, TLS) ──▶ 127.0.0.1:18004 (node)  dev
             public                           loopback, unreachable
                                              from outside

Certificates and TLS termination live in nginx; Node never faces raw internet traffic. The endpoint is still fully public — the proxy is what makes it so.

Responses are Server-Sent Events, so proxy buffering must be off. With default buffering the connection appears to hang and clients time out. The four lines that matter are proxy_http_version 1.1, proxy_set_header Connection "", proxy_buffering off, and a long proxy_read_timeout.

# ---- prod : https://your-host.example.com:8003/mcp ----
server {
    listen YOUR.SERVER.IP:8003 ssl;
    server_name your-host.example.com;

    ssl_certificate     /etc/letsencrypt/live/your-host.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-host.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    access_log /var/log/nginx/mcp-prod.access.log;
    error_log  /var/log/nginx/mcp-prod.error.log;

    location / {
        proxy_pass http://127.0.0.1:18003;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Connection        "";

        # SSE: buffering off, or streamed responses stall.
        proxy_buffering    off;
        proxy_cache        off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

# ---- dev : https://your-host.example.com:8004/mcp ----
# Same block with 8004 -> 127.0.0.1:18004 and its own log files.

Enable and reload:

sudo ln -s /etc/nginx/sites-available/rentvine-mcp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

location / proxies everything, so /health and /mcp both pass through. listen is pinned to a specific IP here; plain listen 8003 ssl; binds all interfaces. Open the ports if a firewall is active: sudo ufw allow 8003/tcp.

nginx does not authenticate anything here. It forwards every request straight through, so MCP_AUTH_TOKEN in the Node process is the only access control on the endpoint. See the warning below.

5. Verify

HOST=https://your-host.example.com:8003
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'

# 1. Health — public by design, returns only {"ok":true}
curl -sS $HOST/health

# 2. No token — MUST be 401
curl -sS -o /dev/null -w '%{http_code}\n' -X POST $HOST/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' -d "$INIT"

# 3. With token — expect the initialize result
curl -sS -X POST $HOST/mcp \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' -d "$INIT"
# event: message
# data: {"result":{...,"serverInfo":{"name":"rentvine",...}},...}

If step 2 returns anything other than 401, stop and fix it — your Rentvine account is exposed to the internet. See the warning below.

⚠️ MCP_AUTH_TOKEN is mandatory here

The server refuses to start without a token only when binding to a non-loopback address. This deployment binds to 127.0.0.1, which is exempt from that check — so an empty MCP_AUTH_TOKEN starts happily and the reverse proxy then publishes an unauthenticated /mcp to the world. The built-in guard cannot save you behind a proxy.

Anyone reaching that endpoint gets full read/write access to your Rentvine account: tenant PII, ledgers, and the ability to create work orders and bills. start-mcp.sh.example refuses to start on an empty or placeholder token for this reason — keep that check.

/health is intentionally unauthenticated and returns only {"ok":true}.

6. Connect clients

Every client uses the same two values — the URL and the bearer token. No Rentvine credentials ever leave the server.

URL

https://your-host.example.com:8003/mcp

Transport

Streamable HTTP

Auth

Authorization: Bearer <MCP_AUTH_TOKEN>

Read and write endpoints. Besides /mcp (every tool), the server serves the same token on two narrower paths:

Path

Tools

/mcp/read

Everything that only reads — nothing changes in Rentvine

/mcp/write

Only create_work_order, update_work_order, create_bill, upload_file

A client that should never change data connects to /mcp/read only; an agent platform can connect to both and require a person's approval for every call on /mcp/write. Both are stateless: each request gets its own server holding only that path's tools, so there are no sessions to lose on a restart and any replica can answer. /mcp keeps in-memory sessions as before; an unknown or ended session there now gets 404 (the MCP spec's cue to start a new session).

upload_file's file_path reads a file from the server's disk. Over stdio that's your machine; over HTTP it's the server, so a caller could upload any file the server can read — its own credentials included. /mcp keeps it as before (set RENTVINE_ALLOW_FILE_PATH=0 to turn it off — recommended once no client relies on it); /mcp/read and /mcp/write never offer it (send file_content_base64 + file_name).

Who a call is for. On /mcp/read and /mcp/write an agent platform can send x-rentor-user (the person's email) and x-rentor-bot (the bot). Every tool call — on any endpoint — writes one JSON line to stdout: {"event":"tool_call","endpoint":"/mcp/write","tool":"create_work_order","user":"…","bot":"…","ok":true,"ms":412} — never the arguments or the result. The headers are attribution, not access control: any client holding the token could set them.

Claude Code / Claude Desktop / Cursor / Windsurf / VS Code

{
  "mcpServers": {
    "rentvine": {
      "type": "http",
      "url": "https://your-host.example.com:8003/mcp",
      "headers": { "Authorization": "Bearer your_mcp_auth_token" }
    }
  }
}

Config locations — Claude Code: ~/.claude.json or project .mcp.json · Cursor: Settings → MCP → Add new server · Windsurf: ~/.codeium/windsurf/mcp_config.json · VS Code (Copilot): .vscode/mcp.json. Restart the client; you should see rentvine with all 25 tools.

Vapi / voice agents

Add as a custom MCP tool provider with the URL above and an Authorization header of Bearer <MCP_AUTH_TOKEN>. Voice agents call tools with no human reviewing arguments first, so give them a token you can rotate independently and consider a read-only Rentvine API key — create_work_order, create_bill, update_work_order, and upload_file all write to live data.

ChatGPT (Business / Enterprise / Edu)

An admin enables Workspace Settings → Permissions & Roles → Developer Mode, then: Settings → Connectors → Advanced → Add custom MCP server, URL as above, Auth Bearer → the MCP_AUTH_TOKEN. Pick Developer mode from the Plus menu in a new chat and select the rentvine connector. Not available on Plus or Free.

Anything else

Any MCP client supporting Streamable HTTP works — point it at the URL with the bearer header.

Sessions apply to /mcp only. They live in memory, keyed by the mcp-session-id header, so a pm2 restart ends them. The server then answers 404, which is the MCP spec's cue for a client to start a new session — but not every client acts on it. One agent platform kept replaying the dead ID and lost every Rentvine tool until it was itself restarted. /mcp/read and /mcp/write are stateless, so there is nothing to lose on a restart; prefer them for agent platforms. Likewise, running more than one replica behind the proxy needs sticky routing on that header for /mcp — the stateless endpoints need none.


Environment Variables

Variable

Description

RENTVINE_API_KEY

Your Rentvine API key

RENTVINE_API_SECRET

Your Rentvine API secret

RENTVINE_COMPANY

Your subdomain (e.g. acme for acme.rentvine.com)

MCP_AUTH_TOKEN

Bearer token clients must present on /mcp. Generate with openssl rand -hex 32. Always set this. The server only enforces it at startup when HOST is non-loopback — behind nginx that check never fires, so an empty value publishes an open endpoint.

PORT

HTTP server port (default: 3000). Use 18003 / 18004 per the table above.

HOST

Bind address (default: 0.0.0.0). Set 127.0.0.1 so only nginx can reach it.

RENTVINE_ALLOW_FILE_PATH

0 turns off upload_file's file_path on /mcp (reads the server's own disk). Default: on, as before. /mcp/read and /mcp/write never offer it; stdio always does.

All of these are set in start-mcp.sh, one block per environment.


Docker (optional — not how we deploy this)

This is not our deployment path. Rentor runs this under pm2 behind nginx, as described in Install — that is the supported setup, the one every other section assumes, and the one to use. The Dockerfile is an unsupported convenience for running the server somewhere else. Nothing in our deployment builds it or depends on it, and you never need Docker to install, run, or develop this server.

docker build -t rentvine-mcp .
docker run --rm --init -p 127.0.0.1:18009:3000 \
  -e RENTVINE_API_KEY -e RENTVINE_API_SECRET -e RENTVINE_COMPANY -e MCP_AUTH_TOKEN \
  rentvine-mcp

The image runs dist/http.js as the unprivileged node user on 0.0.0.0:3000 (so MCP_AUTH_TOKEN is required — it refuses to start without one) with a /health check. Put TLS in front of it as for pm2.

Nothing is pinned inside the image: PORT and HOST are defaults, so -e PORT=18009 moves the listener and the healthcheck follows it. EXPOSE 3000 is only metadata — publish whatever you want with -p.

Use --init as shown. The server registers no SIGTERM handler, so as PID 1 it ignores the signal and docker stop waits the full grace period before killing it (10s, exit 137); --init supplies a real init and it stops in 1s.


Testing

npm test          # scripts/test-endpoints.sh

35 checks over auth, the three endpoints, which tools each exposes, session handling, ID validation, and the call log. It builds if needed, starts its own server on port 18099 with fake credentials, and stops it again — so it needs no .env, reaches no Rentvine account, and touches no live data. Run it before opening a PR.

For a quick look at a running server against a real account, use scripts/mcp-test.sh (handshake plus one tool call) or scripts/test.sh (the same through the MCP Inspector CLI). Both read credentials from .env.

Continuous integration

.github/workflows/ci.yml runs on every pull request and on pushes to main: typecheck, build, the 35 endpoint tests, and a scan for credential-shaped literals in tracked files. It needs no secrets — the test suite supplies its own fake ones.

A red run does not block merging on its own. GitHub reports the failure and still offers the merge button until test is a required status check. Set it once, after the first run has appeared:

Settings → Branches → Add branch ruleset (or Add rule on older repos) → target main → tick Require status checks to pass before merging → search test and select it → also tick Require a pull request before merging so the check cannot be bypassed by pushing straight to main.

The check only appears in that search once it has run at least once, so open the CI PR first, then add the rule.

After install, ask Claude things like:

List all my properties.
How many units are vacant across all properties?
Which leases expire in the next 60 days?
Show me all open work orders sorted by priority.
What is the balance for tenant [name]?
Create a work order for the leaking roof at 123 Main St, high priority.
Upload this invoice and attach it to work order #1042.
Show me all unpaid bills.
Which vendors have liability insurance expiring in the next 60 days?
Find vendors within 25 miles of property [ID].
Show me all photos attached to work order [ID].
Download the inspection report file [ID].

Development

SSH clone here, since contributors push:

git clone git@github.com:Rentor-CA/Rentvine-MCP.git
cd Rentvine-MCP
npm install
npm run build          # tsc → dist/
npm run typecheck      # tsc --noEmit, no output

Deploy by pushing, then on the server: git pull && npm install && npm run build && npx pm2 restart all.

Testing locally

Nobody uses this locally — it's a hosted endpoint. But you should always verify changes locally before deploying.

1. Credentials. Point at a dev Rentvine account, never production: create_work_order, create_bill, update_work_order, and upload_file write to live data with no sandbox.

cp .env.example .env
$EDITOR .env                  # dev key, secret, subdomain
set -a && source .env && set +a

.env is gitignored. There's no dotenv dependency — set -a exports everything in the file into your shell, which is all the server needs.

2. Run it, exactly as production does (loopback, bearer token, port 18009 so it can't collide with a deployed instance):

npm run build
node dist/http.js
# rentvine-mcp HTTP listening on http://127.0.0.1:18009/mcp

For an edit loop, run npm run dev (tsc --watch) in one shell and restart node dist/http.js after each rebuild.

3. Drive it with scripts/mcp-test.sh. It reads .env, does the handshake, and calls a tool — one command instead of four:

./scripts/mcp-test.sh                                   # list all 25 tool names
./scripts/mcp-test.sh list_properties                   # call a tool
./scripts/mcp-test.sh list_tenants '{"search":"aaron"}'  # with arguments
./scripts/mcp-test.sh list_work_orders | jq '.[0]'      # pipe to jq
./scripts/mcp-test.sh --raw                             # full tools/list JSON

It fails loudly if the server isn't running or the token doesn't match, which covers the two things that actually go wrong.

Streamable HTTP is session-based: initialize first, then send the returned mcp-session-id header on every later request. A bare tools/list returns 400 Bad Request: no valid session ID.

$MCP_AUTH_TOKEN must be exported in your shell — set -a && source .env && set +a. If it's empty the header becomes Authorization: Bearer and every call returns {"error":"unauthorized"}.

MCP=http://127.0.0.1:18009/mcp
AUTH="Authorization: Bearer $MCP_AUTH_TOKEN"
JSON="Content-Type: application/json"
SSE="Accept: application/json, text/event-stream"

# health — no auth, no session
curl -sS http://127.0.0.1:18009/health

# 1. handshake, capturing the session id from the response headers
SID=$(curl -sS -D- -o /dev/null -X POST $MCP -H "$AUTH" -H "$JSON" -H "$SSE" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | tr -d '\r' | awk -F': ' '/^mcp-session-id:/{print $2}')
echo "session: $SID"

# 2. complete the handshake (returns 202)
curl -sS -X POST $MCP -H "$AUTH" -H "$JSON" -H "$SSE" -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. list tools — responses are SSE, so strip the "data: " prefix for jq
curl -sS -X POST $MCP -H "$AUTH" -H "$JSON" -H "$SSE" -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | sed -n 's/^data: //p' | jq -r '.result.tools[].name'

# 4. call one
curl -sS -X POST $MCP -H "$AUTH" -H "$JSON" -H "$SSE" -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_properties","arguments":{}}}' \
  | sed -n 's/^data: //p' | jq -r '.result.content[0].text'

Step 3 should print 25 tool names. Step 4 returns the tool's JSON payload, or a Rentvine 4xx … string if the credentials are wrong — which still proves the transport, routing, and projection path all work.

4. Or use the official MCP Inspector. Standard tooling, no repo checkout needed — works against localhost or the deployed endpoint:

export TOKEN=your_mcp_auth_token
URL=http://127.0.0.1:18009/mcp        # or https://your-host.example.com:8003/mcp

# list tools
npx @modelcontextprotocol/inspector --cli $URL \
  --header "Authorization: Bearer $TOKEN" --method tools/list

# call one, with arguments
npx @modelcontextprotocol/inspector --cli $URL \
  --header "Authorization: Bearer $TOKEN" \
  --method tools/call --tool-name list_tenants --tool-arg page_size=100

# read the API-docs resource
npx @modelcontextprotocol/inspector --cli $URL \
  --header "Authorization: Bearer $TOKEN" \
  --method resources/read --uri rentvine://api-docs

It also has --tui (interactive terminal browser) and --web (browser UI, good for clicking through tools and inspecting raw JSON-RPC). The transport is auto-detected from the URL — /mcp means Streamable HTTP.

scripts/mcp-test.sh stays useful for scripted checks: no npx download, and it reads .env so there's no token to pass.

5. Or point a real client at it. Same config as production, just localhost:

{
  "mcpServers": {
    "rentvine-local": {
      "type": "http",
      "url": "http://127.0.0.1:18009/mcp",
      "headers": { "Authorization": "Bearer dev-token" }
    }
  }
}

This is the fastest way to test tool descriptions — whether the model picks the right tool and fills arguments correctly is not something curl can tell you.

6. Quick checks without a server. dist/index.js is the stdio transport; it speaks JSON-RPC on stdin/stdout with no session handshake, which makes it handy for scripted assertions:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | node dist/index.js | tail -1 | jq '.result.tools | length'

Run npm run typecheck before pushing.

Layout

src/
  index.ts          stdio entrypoint
  http.ts           Streamable-HTTP entrypoint (Express, in-memory sessions)
  createServer.ts   tool registry — 24 registerTool() calls + zod schemas
  tools.ts          projections: Rentvine camelCase → our snake_case contract
  client.ts         HTTP layer: Basic auth, envelope unwrapping, timeouts
  apiDocs.ts        Rentvine API reference, served as rentvine://api-docs
  types/            ambient .d.ts for untyped deps (zipcodes)

Adding a tool

Three edits, in order:

  1. src/client.ts — add a fetchX() that calls the endpoint and returns the raw rows.

  2. src/tools.ts — add an exported function projecting raw → snake_case.

  3. src/createServer.ts — server.registerTool("x", { description, inputSchema }, handler).

Endpoints documented in src/apiDocs.ts but not yet wrapped include the eleven /accounting/diagnostics/* routes, /maintenance/vendor-trades, /leases/{leaseID}, /properties/units/export, and /accounting/transactions/entries/search.

Known issues

  • Work-order enum maps are unverified. WO_STATUS / WO_PRIORITY / LEASE_STATUS in src/tools.ts disagree with the enum tables in src/apiDocs.ts (which document status as 1=Pending, 2=Open, 3=Closed, 4=On Hold and priority as 1–3 only, with no emergency). If the docs are right, update_work_order(status: "cancelled") sets the work order to On Hold. Verify against GET /maintenance/work-order/statuses before trusting either.

  • No pagination on list tools. list_properties, list_leases, list_work_orders, list_bills, list_vendors, and list_accounts send no page/pageSize, and Rentvine defaults to 15–25 rows. They will silently truncate as the portfolio grows. Only search_transactions exposes paging.

  • unwrap() returns [] on unrecognized shapes (src/client.ts), so an API contract change reads as "no results" rather than an error.

  • Fuzzy lookups take first match. list_units and get_tenant_balance substring-match names and silently pick the first hit.

  • list_tenants paging is unverified. page/page_size are passed through to GET /tenants, but that endpoint's paging behavior is unconfirmed. If Rentvine ignores them, results are silently capped at its default page size. search and active_only are applied client-side, after the fetch, so they filter only what came back on that page.

Handling tenant PII

Rentvine serves tenants, vendors, and owners from one shared contact schema, so every tenant record carries birthDate, identificationNumber, identificationTypeID, and achAccountNumberTruncated — real PII on a consumer.

list_tenants therefore splits its projection: identity, contact, status, and audit fields by default; date of birth, government ID, tax/payee, and payout/ACH fields only when include_sensitive=true. Without that split, a bare "list the tenants" request would put every tenant's DOB and bank details into the model's context window.

If you'd rather have the full record by default, drop the include_sensitive branch at the end of listTenants() in src/tools.ts and always spread projectTenantSensitive(c).

Troubleshooting

{"error":"unauthorized"} from /mcp — the client's bearer token doesn't match MCP_AUTH_TOKEN. This is the server's own 401, not Rentvine's.

Rentvine 401 Unauthorized inside a tool result — wrong RENTVINE_API_KEY, RENTVINE_API_SECRET, or RENTVINE_COMPANY. Verify in Rentvine → Settings → Users, Roles & API.

Client connects but hangs on the first call — nginx is buffering. Confirm proxy_buffering off, proxy_http_version 1.1, and proxy_set_header Connection "" are in the location block; responses are SSE and stall without them.

502 Bad Gateway — node isn't running or is on a different port than proxy_pass. Check with npx pm2 list and:

sudo ss -tlnp | grep -E ':(8003|8004|18003|18004)'
npx pm2 logs rentvine-prod --lines 50

Server won't start — npx pm2 logs <name>. FATAL: MCP_AUTH_TOKEN must be set… means HOST isn't loopback and no token is set. FATAL: <VAR> is unset or still CHANGE_ME is the launcher's own check. tsc: not found during build means NODE_ENV=production was set during npm install.

Gone after a reboot — npx pm2 save and npx pm2 startup were never run.

Confirm the token is actually set in the live process (prints 1 or 0, never the secret):

sudo tr '\0' '\n' < /proc/$(pgrep -f 'dist/http.js' | head -1)/environ \
  | grep -c '^MCP_AUTH_TOKEN=.\+'

Empty results — your Rentvine account may have no data in that category, the list endpoint truncated at Rentvine's default page size, or the API returned an unexpected envelope (see unwrap() under Known issues).

Available Tools

25 tools
create_billA

Create a bill in Rentvine (live data, write). Requires a payee contact ID (from list_vendors or list_owners), bill date, due date, and bill type ID. Optionally link to a work order.

Inputs: due_date (required), bill_date (required), bill_type_id (required), payee_contact_id (required), +4 more.

create_work_orderA

Create a new maintenance work order in Rentvine (live data, write). Requires description, property_id, and priority. Rentvine auto-fills unitID for single-unit properties and defaults status to 'open'. Returns the new work_order_id and work_order_number.

Inputs: priority (required), description (required), property_id (required), status, +6 more.

download_fileA

Download a Rentvine file as base64 (live data, read). Returns mime_type, size_bytes, is_image, and content_base64. Supports images and other binary attachments up to ~1MB. Large files may fill the context window — prefer list_attachments first to check file_size.

Inputs: file_id (required).

get_fileA

Get metadata for a single Rentvine file by file_id (live data, read). Returns name, size, mime type, and attachment info — but not file contents. Use download_file to fetch the actual bytes.

Inputs: file_id (required).

get_tenant_balanceA

Get the current ledger balance for a tenant from Rentvine (live data). Returns balance and ledger data.

Inputs: tenant_name (required).

get_vendorA

Get a single vendor's full details from Rentvine (live data). Calls /vendors/{id} which returns ~20 fields not available via list_vendors — notably code (100-char free-text identifier used as a catch-all for metadata Rentvine's schema can't hold, such as trade/hourly-rate), website_url, name components (first/middle/last/suffix), discount tiers, QuickBooks linkage, and contact_type. Also parses the packed code field into a code_metadata object when it uses the pipe-delimited k=v convention.

Inputs: vendor_id (required).

list_accountsA

List the chart of accounts from Rentvine (live data). Returns account ID, number, name, category, and active status. Useful for identifying accountIDs when creating bill charges.

Inputs: no named parameters.

list_applicationsA

List rental applications from Rentvine (live data). Returns applicant name, property, unit, status, and application date.

Inputs: no named parameters.

list_attachmentsA

List files attached to any Rentvine object (live data, read). Returns file metadata including file_id, file_name, mime_type, size, and upload date. Use list_object_types to find the correct object_type_id.

Inputs: object_id (required), object_type_id (required).

list_billsA

List all bills from Rentvine (live data). Returns bill ID, payee, dates, voided status, and linked work order. Use this to review outstanding vendor invoices.

Inputs: no named parameters.

list_inspectionsA

List maintenance inspections from Rentvine (live data). Returns title, property, unit, scheduled date, status, and inspector.

Inputs: no named parameters.

list_leasesA

List all leases from Rentvine (live data). Returns tenant name, unit address, rent, deposit, bed/bath count, dates, and status. Use this to answer questions about lease expirations, rent amounts, or active tenants.

Inputs: no named parameters.

list_object_typesA

List Rentvine object types. Returns the full table of object_type_id values and names (e.g. 7 = Unit, 16 = Work Order, 4 = Lease). Use this to find the correct object_type_id when uploading files or attaching documents.

Inputs: no named parameters.

list_ownersA

List all property owners from Rentvine (live data). Returns owner name, contact ID, email, phone, and address. Use this to look up who owns a property or to get a contact ID for bill creation.

Inputs: no named parameters.

list_portfoliosA

List all portfolios from Rentvine (live data). Returns portfolio name, ID, active status, reserve amount, and associated owners.

Inputs: no named parameters.

list_propertiesA

List all properties from Rentvine (live data). Returns property name, address, type, and active status.

Inputs: no named parameters.

list_tenantsA

List all tenants from Rentvine (live data). Returns contact ID, full name and name components, email, phone, address, active status, linked applicant ID, and audit timestamps. Use this to look up a tenant's contact details or to get a contact_id for other calls. Filter with search (matches name, email, or phone) and active_only. By default this omits personally sensitive fields; set include_sensitive=true only when the task actually requires them — see that parameter's description.

Inputs: page, search, page_size, active_only, +1 more.

list_unitsA

List units for a property from Rentvine (live data). Returns unit address, vacancy status, rent amount, and deposit.

Inputs: property_name (required).

list_vendorsA

List all vendors from Rentvine (live data). Returns every field the /vendors/search endpoint exposes — contact details (name, email, phone, address, city, state, postal_code, country), billing/payout (tax_payer_name, payout_type_id, ach_account_number_truncated, hold_payments), full insurance coverage (liability and workers-comp policy numbers + expirations, days_until_insurance_expires), discount terms, identification documents, active status, and audit timestamps. Use to find vendors for work-order assignment, bill creation, or compliance review. Note: Rentvine does not expose trade categories or service areas via the public API. SENSITIVE: response includes PII and financial fields (birth_date, identification_number, ACH details) — treat output as confidential.

Inputs: no named parameters.

list_work_order_attachmentsA

List images and files attached to a specific work order (live data, read). Convenience wrapper around list_attachments with object_type_id fixed to 16.

Inputs: work_order_id (required).

list_work_ordersA

List all maintenance work orders from Rentvine (live data). Returns description, property, status, priority, estimated cost, and scheduling details.

Inputs: no named parameters.

search_transactionsA

Search accounting transactions in Rentvine (live data). Filter by keyword, date range, or amount range. Returns transaction type, amount, description, date, and associated property/ledger. Paginated — use page and page_size for large result sets.

Inputs: page, search, date_max, date_min, +4 more.

update_work_orderA

Update a maintenance work order in Rentvine (live data, write). Use this to change status (e.g. mark completed/cancelled to close a WO), priority, scheduling dates, estimated cost, description, or owner-approval flag. Setting status to 'completed' or 'cancelled' auto-stamps dateClosed to today unless date_closed is supplied. Find work_order_id via list_work_orders.

Inputs: work_order_id (required), status, priority, actual_end, +7 more.

upload_fileA

Upload a file to Rentvine and optionally attach it to a work order, property, lease, or unit (live data, write). ALWAYS use file_path when the file exists on disk — pass the absolute path and the server reads it directly. NEVER use file_content_base64 for local files; it is extremely slow and fills the context window. file_content_base64 exists only for remote/HTTP deployments with no shared filesystem. Use list_object_types to get valid object_type_id values.

Inputs: file_name, file_path, object_id, object_type_id, +1 more.

vendors_nearA

Find vendors within a radius of a property (live data). Uses the property's Rentvine-geocoded latitude/longitude as the center and approximates each vendor's location from their ZIP-code centroid (offline US lookup — Rentvine does not store per-vendor lat/lon). Returns vendors sorted by distance ascending, each annotated with distance_miles. Defaults: radius_mi=25, active_only=true. Coarse filter — not a precise distance.

Inputs: property_id (required), radius_mi, active_only.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

    TDQS

    A3.7/5.0

    Scored across 25 tools

    Disambiguation4/5

    Most tools map cleanly to distinct resources and actions. Minor overlap exists between list_attachments and list_work_order_attachments, and get_file vs download_file could be confused, but descriptions clarify intent.

    Naming Consistency3/5

    Mostly follows list_/create_/update_ conventions, but breaks the pattern with get_tenant_balance, get_vendor, search_transactions, and vendors_near. A consistent verb prefix would improve predictability.

    Tool Count3/5

    25 tools cover many property-management entities, but the count is heavy and includes near-duplicates like list_attachments/list_work_order_attachments and get_file/download_file.

    Completeness3/5

    Read coverage is broad across owners, tenants, vendors, bills, leases, and work orders, but write operations are limited to work orders and bills; obvious gaps like creating/updating properties, leases, or tenants remain.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      A Model Context Protocol (MCP) server that wraps AppFolio's REST API so Claude can call it natively in any conversation. 38 tools covering portfolio structure, leasing, financials, maintenance, and admin.
      1
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI assistants to operate property management systems via natural language, covering repair orders, owner info, payments, notices, and inspections. Features a full agentic workflow with human-in-the-loop and observability.
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Exposes multifamily rental data including average rent by market, occupancy anomalies, and property summaries to LLM tools via MCP.
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      A reference Model Context Protocol (MCP) server that exposes real Belle Realty property-management data — properties, tenants, leases, maintenance tickets, rent roll — as tools that Claude Desktop, Cursor, or any MCP-compatible client can call directly.
      MIT