docs / mcp
mcp for agent builders LIVE
Connect Claude, ChatGPT or any MCP client to POST /api/mcp and ask for off-market parcels in Colorado and Virginia the way a buyer asks: a county, a town or a ZIP, and a trade. Seven task tools, both protocol handshakes, no key for the aggregate tools, and never an owner name or a mailing address.
functions/lib/mcp.js, functions/lib/mcp-tools.js and functions/api/mcp.js; server shinnslist 0.2.0). No method, parameter, tool name or error code is documented here that the code does not implement. The one planned surface on this page is marked DRAFT.0 connect — either handshake
The endpoint is https://shinnslist.com/api/mcp, Streamable HTTP, JSON-RPC 2.0, one JSON object per POST. Copy-paste setup:
Claude Code: claude mcp add --transport http shinnslist https://shinnslist.com/api/mcp Claude.ai: Settings → Connectors → Add custom connector → https://shinnslist.com/api/mcp ChatGPT: Settings → Connectors (developer mode) → https://shinnslist.com/api/mcp Any client: POST https://shinnslist.com/api/mcp (content-type: application/json)
Two eras of the protocol are served on the one endpoint. A client of the handshake-based revisions (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05: today's Claude, ChatGPT and most others) opens with initialize and gets the version it asked for back (or 2025-11-25 when it asks for one this server does not speak); notifications/initialized is accepted with an empty 202, and ping answers {}. No session id is issued and none is required.
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "shinnslist", "version": "0.2.0" },
"instructions": "shinnslist: off-market parcels from county records in Colorado and Virginia, never owner names. Start with list_coverage, …"
}
}
A client of the stateless revision 2026-07-28 may start with server/discover (it costs nothing against your quota) or send any request straight away; your protocol version and capabilities may ride in params._meta, accepted leniently, never a hard gate. Every result on this endpoint carries a resultType.
{"jsonrpc":"2.0","id":1,"method":"server/discover"}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "server_discover",
"supportedVersions": ["2026-07-28"],
"serverInfo": { "name": "shinnslist", "version": "0.2.0" },
"capabilities": { "tools": { "listChanged": false } }
}
}
Prefer a browser? GET /api/mcp returns the same identity, the setup lines and the tool names as plain JSON.
1 list the tools — tools/list
Seven task tools, named the way buyers ask. This call is free (no quota), and the server invites caching for 60 seconds (ttlMs 60000, cacheScope server, resultType tools_list).
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
| tool | what it answers | arguments | key |
|---|---|---|---|
find_parcels | off-market parcels in a county, town or ZIP, by trade: wholesaler, flipper, str_investor, developer, cash_investor, brrr, land, two_households (a family compound), realtor | state, county, town, zip, trade, limit (at most 25), offset | none |
town_stats | how many parcels an area holds, and the count behind each trade | state, county, town, zip | none |
check_parcel | one parcel's stored record and (Colorado) its stored checks | key, or address + zip, or lat + lng | none |
list_coverage | the counties covered, their parcel counts and freshness, the open trades, the proven-signal count | none | none |
list_signals | the proven signals with the date, precision and recall of each proof (nothing pending is listed) | none | none |
explain_grading | how the site grades itself, with the current numbers and the page to cite | none | none |
run_signal | one proven signal adapter, run live at a point in Colorado or Virginia | signal, lat, lng | x-api-key |
The old per-signal tool names (one per adapter — 270 today — the slug of the signal id such as best-value-number, never best_value_number) still answer tools/call for clients that cached the old list, with the same gates as run_signal; they are no longer listed. In that catalog each entry's description carries its honest status and radius_km is accepted but reserved.
2 call a task tool — tools/call
The six aggregate tools read the hunt database the way the site does and need no key: the records are public, owner names and mailing addresses are stripped, and the site's rate limit of 100 requests a minute per address applies.
curl -X POST https://shinnslist.com/api/mcp \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"find_parcels",
"arguments":{"county":"Boulder","trade":"wholesaler","limit":5}}}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "{\"ok\":true,\"kind\":\"parcels\",\"area\":\"Boulder County, Colorado\", … }" }],
"structuredContent": {
"ok": true, "kind": "parcels", "area": "Boulder County, Colorado",
"trade": { "id": "wholesaler", "label": "Wholesaler", "what": "owners whose mail goes elsewhere, held 10+ years, bought for under half of today’s value" },
"total": 1234, "count": 5, "offset": 0,
"parcels": [ { "parcel_key": "CO|Boulder|R0012345", "address": "…", "zip": "80540", "county": "Boulder", "state": "CO",
"value_est": 612000, "last_sale_date": "2009-05-01", "last_sale_price": 240000, "tenure_yrs": 17.4,
"url": "https://shinnslist.com/hunt.html?q=…&zip=80540", "card": "https://shinnslist.com/api/hunt/parcel?key=…" } ],
"as_of": "2026-10-17",
"source": "Source: shinnslist (county records)",
"url": "https://shinnslist.com/hunt.html?lens=wholesaler",
"how_to_cite": "Say \"shinnslist, from county records\" and link the url. Owner names and mailing addresses are never in this answer."
},
"isError": false,
"resultType": "tool_result"
}
}
Copy-paste carefully: content[0].text is a JSON string — JSON.parse it, or read the same object from structuredContent. Every answer carries as_of, source and a shinnslist url, so you can cite it. When the database does not answer, the result comes back with isError: true and the reason in content[0].text, never a 500 and never a made-up number. A bad argument (an unknown trade, a ZIP that is not five digits, a town the places list does not know) is a JSON-RPC -32602 that says what to fix.
Other examples: {"name":"town_stats","arguments":{"town":"Lyons"}} · {"name":"check_parcel","arguments":{"address":"874 Cold Spring Rd","zip":"80302"}} · {"name":"check_parcel","arguments":{"key":"CO|Boulder|R0012345"}} · {"name":"list_coverage","arguments":{}}. A county name alone settles the state; a Virginia county reads Virginia. The trade doors are Colorado today, and the answer says so when you ask for one elsewhere.
3 run a proven signal — run_signal
run_signal runs one signal adapter live at a point (signal from list_signals, lat, lng) and returns the same signal envelope the REST API returns: signal_id, status, result, evidence, confidence, retrieved_at and data_as_of. Only proven signals run; asking for one that is not proven is -32602 with its verdict. It needs a key (the x-api-key header or ?key=; a free key from POST /api/keys; no key falls back to the anonymous tier, 5 a day) and runs the identical gate stack as /api/intel, in the identical order: API key → rate limit → daily quota → entitlements. The point must be in Colorado or Virginia.
curl -X POST https://shinnslist.com/api/mcp \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"run_signal",
"arguments":{"signal":"tax_delinquency_early_warning","lat":40.5853,"lng":-105.0844}}}'
| code | when | data carries |
|---|---|---|
-32001 | invalid api key | hint: "get a free key: POST /api/keys" |
-32002 | rate_limit_exceeded | retry_after_seconds (per-key/IP token bucket) |
-32003 | daily_quota_exceeded | reason, quota {limit, resets_at}, hint: "quota resets at 00:00 UTC; paid tiers raise the cap" |
-32004 | signal_not_entitled | denied: [{signal_id, reason}], hint: "this signal requires a higher tier" |
-32005 | outside_coverage (the point is outside Colorado and Virginia) | boundary: the jurisdiction check signal, with the detail |
An entitlement denial never triggers computation and never consumes quota. A throwing adapter comes back as a degraded envelope with the reasons in unavailable, never a 500. Every JSON-RPC error rides HTTP 200 (MCP convention) with the code in the body — only transport-level failures use HTTP 400: -32700 (body isn't valid JSON) and -32600 (missing jsonrpc: "2.0" or method). An unknown method or tool is -32601.
what this endpoint will never do
- No streaming, no sessions. Synchronous JSON-RPC 2.0 only; a
GETthat asks fortext/event-streamgets405, noMcp-Session-Idis issued, and responses carrycache-control: no-store. - Notifications get an empty 202. A request with no
id(other thantools/call) returnsHTTP 202with no body. - No owner names. Every row is stripped of the owner's name and mailing address before it leaves (
withoutOwnersinfunctions/lib/hunt.js); the signed-in owner view does not exist on this endpoint. - Nothing invented. An unknown tool is
-32601("unknown tool: …"), never a guess; counts come from the database at call time;list_signalslists proven signals only. - Colorado or Virginia.
run_signalruns the jurisdiction gate first (-32005otherwise) — except theva-boundary-checkadapter itself, which is the gate.
planned: v1 REST evaluate DRAFT
The per-signal REST evaluate endpoints (POST /v1/signals/{signal}/evaluate) are the planned REST alternative to run_signal — same adapters, different envelope. They are not shipped: no deployed endpoint exercises them yet, and their shapes are described in the error reference only as drafts.
list_coverage; cache tools/list for its 60-second TTL; read structuredContent or JSON.parse(content[0].text); treat isError: true as "the database did not answer", not as "no parcels"; cite the url and as_of of every answer; honor retrieved_at and data_as_of on signal envelopes; never print an owner's name, because none is sent.next: getting started · api reference · api keys · rate limits · how we grade