Skip to main content
Glama
haksanlulz

mcp-nychousing

by haksanlulz

mcp-nychousing

MCP server for NYC housing data over NYC Open Data (the Socrata / SODA API). Built for tenant organizers, housing-court legal-aid intake, and Right-to-Counsel orgs: pull a building's HPD violations and complaints, find out who actually owns it two different ways (HPD's registration filings, and the property record itself: the assessment roll, recorded deeds and mortgages, and Speculation Watch List), map everything else registered under that owner or agent's name, check HPD litigation, Department of Buildings records, 311 heat complaints, and marshal-executed evictions, or pull the whole picture in one building_profile call.

It wraps eighteen city datasets and normalizes their raw columns (novdescription, violationstatus, registrationid, court_index_number, and so on) into documented tool outputs. The number is the DATASET map in server.ts, one row per id in the table below:

sed -n '/^const DATASET = {/,/^} as const;/p' server.ts | grep -oE '"[a-z0-9]{4}-[a-z0-9]{4}"' | sort -u | wc -l

Tools

Tool

Arguments

Returns

building_violations

house_number, street, borough (all required), open_only, violation_class, since, limit

HPD violations for a building (wvxf-dwi5). Server-side per-class count summary (A/B/C/I) plus the newest rows (default 25; the note says when they are a page): id, apartment, class, description, status, open flag, inspection date.

building_complaints

house_number, street, borough (all required), open_only, since, limit

HPD complaints and problems for a building (ygpa-z7cr). Open/closed count summary plus the newest rows (default 25; the note says when they are a page): complaint id, category, status, dates.

who_owns

house_number, street, borough (all required)

HPD registration (tesw-yqqr) joined to registration contacts (feu5-w2e2). Owner, head officer, officer, agent, and site manager, with names and business addresses, grouped by type.

landlord_portfolio

name (required), borough, limit

Reverse of who_owns: registration contacts (feu5-w2e2) matched by corporation or person name, resolved to every currently registered building (tesw-yqqr). Address, borough, zip, BIN, registration dates, and which contact(s) matched, plus contact/registration/building counts.

landlord_litigation

house_number + street + borough, and/or respondent, plus case_status, limit

HPD Housing Litigations (59kj-x8nc) by building or by respondent name. Case type, open date, status, judgement, harassment finding, penalty, respondent, with a by-status summary.

eviction_lookup

court_index_number, and/or address, and/or borough, plus since, limit

Marshal-executed evictions (6z8x-wfk4) by court index number or address/borough. Index number, address, executed date, marshal, residential/commercial flag.

building_profile

house_number, street, borough (all required)

One-call profile across ten datasets: registration (tesw-yqqr) + contacts (feu5-w2e2), violation counts by class, complaint counts by status, litigation counts by status, executed-eviction count, AEP status (hcir-3275), vacate orders (tb8q-a3ar), latest bedbug filings (wz6d-d3jb), and emergency-repair charge count (sbnd-xujn). Start here, then drill down.

true_owner

house_number, street, borough (all required), docs_limit

Ownership from the property record rather than HPD's filings: the DOF assessment-roll owner (PLUTO 64uk-42ks), recent recorded deeds/mortgages with named parties (ACRIS 8h5j-fqxa -> bnx9-e6tj -> 636b-3b5g), and Speculation Watch List hits (adax-9mit). Always surfaces latest_deed (the newest DEED-family instrument, chased specifically even when the newest documents are other paperwork). Staten Island instruments are with the Richmond County Clerk, not ACRIS.

dob_building

house_number, street, borough (all required), limit

Department of Buildings records (a different agency from HPD): DOB violations (3h2n-5cm9, by-category summary) and DOB complaints (eabe-havv, by-status summary). DOB dates arrive in the agency's raw formats.

building_311

address, borough (both required), complaint_type, since, limit

311 service requests (erm2-nwe9) for an address, defaulting to the heat/hot-water types; pass complaint_type for any other. Newest-first with a by-status summary. Uses the dataset's full-text index ($q) so the 40M-row table answers fast.

borough accepts Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also MN/BX/BK/QN/SI or the codes 1 to 5). violation_class is one of A (non-hazardous), B (hazardous), C (immediately hazardous), I (informational). since is an ISO date (YYYY-MM-DD).

Related MCP server: civic-library-mcp

Data source and grounding

  • Base URL: https://data.cityofnewyork.us/resource/<dataset-id>.json

  • Auth: none. SODA is keyless. An optional Socrata app token (see below) only raises the per-IP rate limit.

  • Response: list and aggregate queries return a bare JSON array. Errors return { "error": true, "message": "..." }.

  • Query language: SoQL via $select, $where, $group, $order, $limit, with upper(...) and like for string matching and || for the first/last-name concatenation in landlord_portfolio. All user text is escaped (a single quote becomes two) before it reaches a query.

Dataset ids and column notes:

Dataset

Id

Notes

HPD Violations

wvxf-dwi5

Address columns housenumber / streetname / boro. Status is violationstatus (Open/Close); currentstatus is the detailed step.

HPD Complaints and Problems

ygpa-z7cr

The current combined dataset (the older uwyv-629c is not publicly readable). Address columns house_number / street_name / borough. One row per problem.

HPD Registrations

tesw-yqqr

Current registrations. Join key registrationid.

HPD Registration Contacts

feu5-w2e2

Owner / agent / officer names. Joined by registrationid.

HPD Housing Litigations

59kj-x8nc

Address columns housenumber / streetname / boroid (numeric 1 to 5, no text borough). Has respondent, penalty, findingofharassment.

Evictions

6z8x-wfk4

Marshal-executed only. Combined eviction_address string plus borough.

DOB Violations

3h2n-5cm9

boro is a NUMERIC-as-text code 1-5 (plus legacy junk rows). Dates in DOB's raw formats (often YYYYMMDD).

DOB Complaints

eabe-havv

NO borough column at all; the community_board first digit is the borough code (filtered via starts_with).

311 Service Requests

erm2-nwe9

~40M rows; a bare LIKE over incident_address full-scans and times out, so the address rides the indexed $q full-text parameter with the LIKE as refiner. Borough is uppercase text.

Bedbug Filings

wz6d-d3jb

Borough uppercase text. Infested / eradicated / re-infested unit counts per filing period.

AEP (Alternative Enforcement)

hcir-3275

boro is Title Case text ("Bronx"); matched case-insensitively.

Vacate Orders

tb8q-a3ar

boro_short_name is the 2-letter code (BX/BK/MN/QN/SI).

HWO Emergency-Repair Charges

sbnd-xujn

Handyman Work Orders billed to landlords. Borough uppercase text.

PLUTO Tax Lots

64uk-42ks

borough is the 2-letter code. Carries the DOF assessment-roll ownername, bbl, block/lot, units, year built.

ACRIS Legals

8h5j-fqxa

Step 1 of the recorded-instrument chain: borough/block/lot -> document ids. Text-typed columns, quoted comparisons. Carries NO recorded-date column, so recency comes from Master, never from document_id (legacy FT_* ids sort above every modern one). Staten Island is NOT in ACRIS (Richmond County Clerk).

ACRIS Master

bnx9-e6tj

Step 2: document id -> doc_type, recorded_datetime, document_amt. This is the recency key for latest_deed.

ACRIS Parties

636b-3b5g

Step 3: document id -> named parties. Role semantics vary by doc type (for a deed, party 1 is the seller and party 2 the buyer; for a mortgage, party 1 is the borrower and party 2 the lender).

Speculation Watch List

adax-9mit

Qualifying flip-risk purchases; matched by block/lot with the row's own bbl confirming borough.

Field map (raw column to normalized output)

Raw column

Normalized field

Tool

violationid, novdescription, currentstatus, violationstatus

violation_id, description, current_status, is_open

building_violations

class, rentimpairing, inspectiondate

class, rent_impairing, inspection_date

building_violations

complaint_id, major_category, complaint_status, received_date

complaint_id, major_category, complaint_status, received_date

building_complaints

registrationid, corporationname, firstname + lastname, business*

registration_id, organization, person_name, business_address

who_owns

housenumber + streetname, boro, bin, lastregistrationdate

building_address, borough, bin, last_registration_date (+ matched_contacts)

landlord_portfolio

litigationid, casetype, casestatus, penalty, respondent

litigation_id, case_type, case_status, penalty, respondent

landlord_litigation

court_index_number, eviction_address, executed_date, marshal_*

court_index_number, eviction_address, executed_date, marshal_name

eviction_lookup

ownername, bbl, block/lot, unitsres, yearbuilt

owner_name, bbl, block/lot, residential_units, year_built

true_owner (PLUTO)

doc_type, document_amt, recorded_datetime, parties by party_type

doc_type, document_amount, recorded_datetime, party_1/party_2/party_3

true_owner (ACRIS)

complaint_type, descriptor, resolution_description, created_date

same names

building_311

Install

Nothing to clone. Point your MCP client at it and npm fetches it on first run:

{
  "mcpServers": {
    "nychousing": {
      "command": "npx",
      "args": ["-y", "@haksanlulz/mcp-nychousing"],
      "env": { "NYC_APP_TOKEN": "your-nyc-app-token" }
    }
  }
}
git clone https://github.com/haksanlulz/mcp-nychousing
cd mcp-nychousing
npm install
npm run build     # emits dist/; the published bin is dist/index.js

npm start runs the TypeScript directly via tsx without building.

Bundle (.mcpb)

manifest.json describes the server as an MCP Bundle, for hosts that install a local server from a single file. Verify that channel:

npm run verify:mcpb

That stages dist/ plus production dependencies, packs the bundle with the vendor CLI (which validates the manifest first), unpacks it, launches the server through the mcp_config in the packed manifest, and asserts the ten tools over stdio. It runs in the CI package job beside verify:pack, so the bundle channel is measured rather than assumed.

It stages and packs into a throwaway directory and removes it afterwards, so it proves the bundle rather than producing one. To keep a bundle, stage dist/ plus production dependencies and run npx @anthropic-ai/mcpb pack <stage-dir> <out>.mcpb yourself.

The app token is declared as an optional user_config field (required: false, sensitive: true) injected as NYC_APP_TOKEN. The server treats a value still containing ${ as unset, so a host that passes an unsubstituted template for a field the user skipped does not produce a bad-credential header. The probe launches the bundle with exactly that unsubstituted template, since that is the case the guard exists for.

A bundle carries its own node_modules, which means it ships the MCP SDK's hono / express subtree inside the artifact. The same reasoning as test/no-http-stack.test.ts applies to this channel: those packages are present in the dependency tree but unreachable, because nothing in this server imports an HTTP transport. That test reads the source and pins the property; a vulnerability scan of the bundle will still list them.

App token (optional)

Every tool works with no token. If you make heavy or bursty use, a free Socrata app token raises the rate limit. Create one from the developer settings on your NYC Open Data account. Docs: https://dev.socrata.com/docs/app-tokens.html

Expose it as NYC_APP_TOKEN and it is sent as the X-App-Token header:

export NYC_APP_TOKEN=your-token-here   # macOS / Linux
setx NYC_APP_TOKEN your-token-here      # Windows (new shells)

The token is never logged.

Environment

Variable

Default

Effect

NYC_APP_TOKEN

unset

Socrata app token, sent as X-App-Token. Optional; raises the rate limit only. A value still containing ${ is treated as unset.

SODA_HTTP_ATTEMPTS

3

Attempts per outbound request (minimum 1). 429, 5xx and transport errors are retried with backoff; other 4xx and a non-JSON body are not.

SODA_CACHE_TTL_MS

28800000 (8h)

Lifetime of a cached response, matched to HPD's 8-hour extract refresh. 0 disables the cache. In memory, successful reads only.

SODA_CACHE_MAX

300

Cached responses kept before the least recently used is evicted (minimum 1).

The three SODA_* knobs take an integer. Anything else (a non-integer, a value below the minimum) is refused with one line on stderr naming the variable and the default it fell back to. It doesn't throw at import, because a typo in an optional knob shouldn't take the server down. A silent Number("abc") wouldn't be inert here, since NaN disables the retry loop, the cache expiry and the eviction pass in turn.

MCP client config

Add an "env": { "NYC_APP_TOKEN": "your-token-here" } block only if you want the higher rate limit.

Example

Call building_violations with { "house_number": "1520", "street": "Sedgwick Avenue", "borough": "Bronx", "open_only": true, "limit": 1 }:

{
  "query": {
    "house_number": "1520",
    "street": "Sedgwick Avenue",
    "borough": "BRONX",
    "open_only": true,
    "violation_class": null,
    "since": null
  },
  "summary": { "total_matching": 128, "by_class": { "A": 21, "B": 74, "C": 33 } },
  "returned": 1,
  "results": [
    {
      "violation_id": "19051745",
      "apartment": "2D",
      "story": "2",
      "class": "C",
      "description": "HMC ADM CODE: ... ABATE THE INFESTATION CONSISTING OF MICE ...",
      "current_status": "NOTICE OF ISSUANCE SENT TO TENANT",
      "is_open": true,
      "rent_impairing": false,
      "inspection_date": "2026-07-04T00:00:00.000",
      "nov_issued_date": "2026-07-08T00:00:00.000",
      "nov_type": "Original"
    }
  ],
  "record_scope": "HPD-issued violations: inspection findings on a date, with their own open/close workflow codes. Not court outcomes, and not a current condition report."
}

The counts are illustrative and move as the city updates the data. The summary counts every match server-side; results is the most recent limit of them.

Every response carries a record_scope line stating what that specific dataset does and doesn't establish. It's per-tool, because each one has a different wrong reading available: landlord_litigation returns HPD workflow codes, not rulings on the merits, and eviction_lookup covers marshal-executed evictions only, so no matching row doesn't mean no case was ever filed.

Then take a name from who_owns output and reverse it. Call landlord_portfolio with { "name": "WFHA 1520 SEDGWICK LP" }:

{
  "query": { "name": "WFHA 1520 SEDGWICK LP", "borough": null },
  "found": true,
  "summary": { "contact_matches": 1, "distinct_registrations": 1, "buildings_found": 1 },
  "note": "Contacts reflect HPD registration filings. The same landlord may file each building under a separate LLC; officer and agent person names often connect what the LLC names hide.",
  "returned": 1,
  "buildings": [
    {
      "registration_id": "221729",
      "building_id": "108415",
      "building_address": "1520 SEDGWICK AVENUE",
      "borough": "BRONX",
      "zip": "10453",
      "bin": "2009171",
      "last_registration_date": "2025-09-05T00:00:00.000",
      "registration_end_date": "2026-09-01T00:00:00.000",
      "matched_contacts": [
        { "type": "CorporateOwner", "organization": "WFHA 1520 SEDGWICK LP", "person_name": null }
      ]
    }
  ],
  "record_scope": "Buildings matched by registered-party name. Name matching is approximate and distinct entities can share a name; this is not proof of common ownership."
}

A single-building LLC like this one is itself the common NYC pattern; searching an officer or agent person name from the same who_owns output is how you connect the buildings the per-building LLC names hide.

Worked example

A tenant comes to an intake desk about 1520 Sedgwick Avenue in the Bronx. You need the building's condition record, a name to serve, and whether the landlord holds other buildings, before the appointment ends.

Four calls, in this order. Figures below are from a live run on 2026-09-14 and move as the city updates the data.

1. building_profile: { "house_number": "1520", "street": "Sedgwick Avenue", "borough": "Bronx" }

The one-call overview across ten datasets. Returned: registered with HPD; 1,038 violations (203 class A, 551 B, 281 C, 3 I); 2,831 complaints, all closed; 32 HPD litigations, 31 closed and 1 pending; 13 marshal-executed evictions; 1 vacate order; 3 bedbug filings; 7 emergency-repair charges; not in AEP. The eviction count arrives with the stored address spellings it matched (1520 SEDGWICK AVE once, 1520 SEDGWICK AVENUE twelve times), so you can see what the count is made of.

2. who_owns: same three arguments

Registration 221729, expiring 2026-09-01, with five distinct contacts: a corporate owner (WFHA 1520 SEDGWICK LP), an agent (M H R MANAGEGEMENT INC, HPD's own spelling), a head officer, an officer, and a site manager. Owner and agent share a business address at 43-55 11th Street, Long Island City. That address, and the agent, are the two threads worth pulling.

3. landlord_portfolio: { "name": "M H R MANAGE" }

A fragment, not the full string, because the stored spelling is misspelled and a second filing may spell it correctly. Two contact records, two registrations, two buildings: 1520 Sedgwick Avenue in the Bronx and 588 Rogers Avenue in Brooklyn. Different LLCs, same agent.

4. true_owner: same three arguments as step 1

The property record rather than HPD's filings. The assessment roll names 1520 SEDGWICK HOUSING DEVELOPMENT FUND C ORPORATION (the spacing is DOF's), on BBL 2028800017, 101 residential units, built 1969. The most recent deed is document 2012070800054004, recorded 2012-07-19, from WFHA 1520 SEDGWICK, L.P. to the HDFC.

What goes in the intake note

1520 Sedgwick Avenue, Bronx (BBL 2028800017) is a 101-unit building from 1969, currently registered with HPD under registration 221729, which expires 2026-09-01. Its HPD record shows 1,038 violations to date, 281 of them class C (immediately hazardous), plus a vacate order and three bedbug filings. The building carries 32 HPD housing-litigation records, one still pending (the dataset holds HPD-initiated cases and tenant actions together). Thirteen evictions have been executed there by a marshal. The registered owner is WFHA 1520 Sedgwick LP and the registered managing agent is M H R Management Inc, both at 43-55 11th Street, Long Island City; the same agent is also on file for 588 Rogers Avenue in Brooklyn, under a different LLC. The recorded deed is older than the registration: the lot was conveyed in 2012 to 1520 Sedgwick Housing Development Fund Corporation. These are agency records, not findings: HPD registration is self-reported and can lag, violation counts are inspection findings rather than current conditions, and the litigation counts are HPD workflow statuses, not rulings. Confirm the owner and agent before serving.

Every response carries a record_scope line saying what that dataset does and does not establish; the last two sentences above are that line, in plain language.

Address matching

There is no geocoding here. Address matching is literal against how HPD stores addresses:

  • Street names are stored uppercase. The server uppercases and trims your street input and matches it as a substring (upper(streetname) like '%YOUR STREET%'). So Sedgwick, sedgwick avenue, and SEDGWICK AVE all match SEDGWICK AVENUE, but a very short input can over-match (5 St would also hit 125 St). Pass the fuller street name when you can.

  • House number is matched exactly (uppercased) first by the HPD- and DOB-keyed tools (building_violations, building_complaints, who_owns, building_profile, dob_building), and on a zero they retry spelling variants, with the response's note naming the ones tried. Separator variants are always tried (120 15 and 120-15 each also try the other and 12015). Splitting a plain number into a hyphenated one (12015 -> 120-15) is generated for Queens addresses only, since rewriting a plain number elsewhere would point at an unrelated building. Outside Queens, pass the hyphenated spelling yourself if the exact one reads zero. Multi-address buildings can still register under a range (1516-1520).

  • Two tools key on a combined address line instead of a separate house-number column, and match it differently. true_owner matches PLUTO's address (1520 SEDGWICK AVENUE) from the start of the line, since PLUTO always stores the house number first. A free substring returned 3817 and 2817 Sedgwick Avenue for house 17, and five unrelated lots (114-20, 118-20, 77-20, 109-20, 104-20) for house 20 on Queens Boulevard. building_311 matches 311's incident_address as a substring. Both run the same house-number spelling probe as the tools above. A corner or multi-lot building can still return more than one PLUTO lot; assessor_owner, latest_deed and speculation_watch all describe the first one, which assessor_owner_lot_address names, and the rest are in lots.

  • The retry stops at the first spelling that matches, so the rest are never sent, and DOB files one building under more than one spelling, each holding its own rows. On 3h2n-5cm9 with borough Queens and street Queens Boulevard, house number 9015 returns 12 violations and 90-15 returns 383. dob_building names the spellings it did not send, per section and per the spelling that actually stopped each probe, so a small count does not read as the whole record; building_311 names its unqueried spellings the same way (a 311 row is only returned under the spelling it is stored with, though no 311 building was found holding rows under both). building_profile covers the other side of the same miss: when it finds nothing in any dataset, its note names the spellings tried and repeats that the street is substring-matched, instead of returning an all-zero profile that reads as a clean building. And the wrong borough, the input a caller guesses most easily, is rescued on its own: when building_violations, building_complaints or building_profile find nothing, they run one query over HPD registrations for the same address in the other four boroughs and return found_in_other_boroughs, so a zero for 1520 Sedgwick, Brooklyn says registered with HPD in BRONX, not BROOKLYN; re-run with borough "BRONX", and a true nowhere says the address is registered in no borough. When exactly one other borough holds it, building_violations and building_complaints also run their summary there under the same filters and return it as elsewhere. The note then carries the number (BRONX has 123 matching violations (16 A / 35 B / 72 C) under these same filters), because a client acts on a number and skips a redirect.

  • Borough disambiguates same-numbered streets across boroughs, so it is required for the building tools. Litigations store a numeric borough code; evictions mix borough and county spellings (Brooklyn and Kings, Manhattan and New York, Staten Island and Richmond), and the borough filter expands to all of them.

  • Evictions store one free-text address line, often a house-number range with an abbreviated or mangled street (2763-69 SEDGWICK AVE, 3605 SEDGWICK AVE NUE). building_profile anchors both halves on a token boundary: the house number cannot match inside a longer number, and the street's distinctive word cannot match inside a longer word, with a stem of the street type (AV for Avenue, RD or RO for Road) required to be the next token after it. Both halves of the street anchor earn their place: without the word boundary, a search for 1650 Ocean Avenue in Brooklyn counts 1650 Ocean Parkway's evictions; with the boundary but the stem free to appear anywhere in the line, a search for 590 Morris Avenue in the Bronx counts 590 MORRIS PARK AVE, a different street that carries AV anyway. A directional in front of the distinctive word names a different street and neither guard can see it, least of all on a street carrying no type word at all (Broadway, Grand Concourse, Avenue X), so it is excluded separately: without that, a search for 475 Broadway in Manhattan counts 475 WEST BROADWAY's eviction, and 88 Broadway counts three that are all West or East Broadway's. The exclusion is skipped when your own street starts with a directional, so 475 West Broadway still finds its own. Because the column stores runs of spaces between tokens, runs up to eight are matched. It returns evictions_matched_addresses, the stored spellings behind the count with a per-spelling count, so a match you did not intend is visible rather than hidden inside the integer. evictions_executed is its own aggregate over the same filter, so the address list's cap does not cap it. One stored line can still name two addresses (155B KINGSBRIDGE RD A/K/A 2707 SEDGWICK AVENUE) and is counted for either.

  • landlord_portfolio matches names the same way: uppercase substring against corporationname, firstname, lastname, and the firstname || ' ' || lastname concatenation (so a pasted person_name from who_owns works). LIKE wildcards (%, _) in your input are escaped. Pass the fullest name you have; a short fragment like SMITH or LLC over-matches, and the response says how many contact records matched before any cap.

  • who_owns, landlord_portfolio, landlord_litigation, and the datasets themselves reflect HPD filings, which can lag reality. Confirm anything you intend to act on (for example a name to serve) before relying on it.

Testing

npm test         # vitest, fetch mocked (no network); 121 tests in 2 files
npm run smoke    # one live call per tool against SODA (keyless, no setup)
npm run typecheck
npm run verify:pack
npm run verify:mcpb

npm test is the offline tier: test/server.test.ts stubs globalThis.fetch and drives every tool through an in-memory MCP client; test/no-http-stack.test.ts reads the source and pins that only the stdio transport is imported. npm run smoke is the live tier (real SODA calls, not run in CI). There are no test markers; the split is the two scripts.

Counts, measured 2026-09-15:

find . -name '*.ts' -not -path './node_modules/*' -not -path './dist/*' -not -path './test/*' | xargs wc -l   # index.ts 8 + server.ts 2774 = 2782 app LOC (smoke.ts 139 is the live harness)
find test -name '*.ts' | xargs wc -l                                                                           # 2306 test LOC
npm test                                                                                                       # Tests 121 passed (121)

What the tests cover, by layer: SoQL query construction (where clauses, LIKE escaping, borough aliases, Queens hyphenated house numbers, date validation) is asserted on the URL the mocked fetch receives. Tool responses (summaries, normalized rows, found/note fields, isError text) are asserted on the parsed payload. Transport behavior (app token and User-Agent headers, 5xx/429 retry counts, 4xx no-retry, non-JSON bodies, response cache hit/miss, IN() chunking at 100 ids) is asserted on call counts and request init.

Mutation probe, re-run 2026-09-15: changed PORTFOLIO_ID_CHUNK in server.ts from 100 to 200 and ran npm test. Three tests failed and 118 passed: landlord_portfolio > chunks large registration-id sets into multiple IN() queries (expected 4 fetch calls, got 3), and both crossing the resolution ceiling on a chunk's short final page cases, which report buildings_found 2000 instead of 2050 and print a ceiling note over a portfolio that was read in full, because one chunk of 200 ids reaches the ceiling on a full page instead of a short one. Source restored after the run.

Wiring assertions, 2026-09-15: 32 toHaveBeenCalled* sites. Most sit beside a payload or URL assertion on the same response; the ones that assert a call count alone do so because the count is the whole contract there: the response cache (repeat query = one fetch, different params = two fetches), the retry cap, and the house-number variant probes (a second spelling is attempted only after the first returns zero). Policy: assert behavior and payloads, never bare invocation.

The fetch stub honours $limit and $offset. A stub that returns every fixture row regardless of the query cannot fail on a paging or cap bug, which is how a portfolio truncation (a chunk capped at its own registration-id count) passed a green suite.

AI assistance

This project was built with AI assistance (Claude). Correctness was established by the mocked vitest suite, by running every tool live against NYC Open Data (npm run smoke; the Queens hyphenated-house-number, NY/NYC borough, and litigation-summary fixes all came from live behavior, not mocks), and by typecheck. The author reviews the code and is accountable for it.

License

MIT. See LICENSE. Data from NYC Open Data (public City of New York data) served via the Socrata SODA API. Unofficial, not affiliated with the City of New York, HPD, or Socrata.

Available Tools

6 tools
building_complaintsA

HPD complaints and problems for one building (dataset ygpa-z7cr; one row per reported problem within a complaint). Give the house number, street, and borough. Returns an open/closed count summary plus the most recent matching problems (category, status, dates). Street is matched as an uppercase substring; house number exactly. Keyless.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax detail rows to return (1-500, default 100). The status summary counts all matches.
sinceNoOnly problems received on/after this ISO date (YYYY-MM-DD).
streetYesStreet name (matched case-insensitively as a substring).
boroughYesNYC borough: Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also accepts MN/BX/BK/QN/SI or 1-5).
open_onlyNoOnly complaints still open (default false).
house_numberYesBuilding house number, e.g. "1520".

TDQS

A3.9/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses key behavioral details like substring matching and exact match for house number, and states 'Keyless', but no annotations exist to supplement. Could mention data freshness or pagination.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two efficient sentences, front-loaded with purpose and output, no fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers purpose and output structure, but lacks guidance on sibling tools, error cases, or detailed output field descriptions. Adequate for a simple lookup.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so description adds limited value. It reiterates required parameters and matching details, but doesn't enrich semantics beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the tool as retrieving HPD complaints for a building, specifies the dataset, and distinguishes it from siblings like building_violations by focusing on complaints.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs to provide house number, street, and borough, and describes the output. Lacks explicit when-not-to-use or comparisons with siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

building_violationsA

HPD Housing Maintenance Code violations for one building (dataset wvxf-dwi5). Give the house number, street, and borough. Returns a server-side per-class count summary (class A non-hazardous, B hazardous, C immediately hazardous, I informational) plus the most recent matching violations. Street is matched as an uppercase substring; house number is matched exactly. Keyless; an optional NYC_APP_TOKEN only raises the rate limit.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax detail rows to return (1-500, default 100). The class summary counts all matches.
sinceNoOnly violations inspected on/after this ISO date (YYYY-MM-DD).
streetYesStreet name, e.g. "Sedgwick Avenue" (matched case-insensitively as a substring).
boroughYesNYC borough: Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also accepts MN/BX/BK/QN/SI or 1-5).
open_onlyNoOnly violations still open (default false).
house_numberYesBuilding house number, e.g. "1520".
violation_classNoFilter to one class: "A", "B", "C", or "I".

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It discloses: returns a server-side per-class count summary plus most recent violations, street matched as uppercase substring, house number matched exactly, keyless, optional token raises rate limit. This is excellent behavioral detail.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is relatively long but every sentence adds value. It front-loads the purpose and dataset ID, then explains parameters and behavior. Could be slightly more concise, but no extraneous information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by explaining the two-part return (class count summary + recent violations). It also covers matching behavior, required parameters, and optional token. For a tool with 7 parameters, this is complete and leaves no major gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, providing a baseline of 3. However, the description adds significant semantic value beyond schema: explains matching logic for street and house_number, clarifies the class summary, and notes that the optional token only affects rate limits. This greatly aids correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it returns HPD Housing Maintenance Code violations for one building, with a specific dataset reference. It distinguishes from siblings by focusing on violations and mentioning the class count summary and recent violations. The verb 'get' is implied, and the resource is explicitly 'violations for one building'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description tells users to provide house number, street, and borough, and mentions an optional token. It does not explicitly state when to use this tool versus siblings like building_complaints or who_owns, but the purpose is clear enough that an agent can infer it is for violations only.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

eviction_lookupA

Marshal-executed evictions (dataset 6z8x-wfk4). IMPORTANT: this dataset lists evictions a city marshal actually CARRIED OUT, not filings, warrants, or pending housing-court cases. Look up by court index number OR by address and/or borough. At least one is required. Returns court index number, address, executed date, marshal, and residential/commercial flag. Address is matched as a case-insensitive substring of the combined eviction address. Keyless.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax rows to return (1-500, default 50).
sinceNoOnly evictions executed on/after this ISO date (YYYY-MM-DD).
addressNoAddress substring to match, e.g. "123 Example Avenue" (matched within the combined eviction address).
boroughNoNYC borough: Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also accepts MN/BX/BK/QN/SI or 1-5).
court_index_numberNoHousing-court index number to match exactly, e.g. "123456/24".

TDQS

A4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It lacks explicit statements about read-only nature, rate limits, or auth requirements. While the function is implied non-destructive, these traits are not disclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is succinct, front-loads the key purpose and distinction, then explains parameters and output. Every sentence adds value with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers the main purpose, required parameter combinations, matching behavior, and output fields. Missing pagination details and the meaning of 'Keyless' is unclear, but overall sufficient for a lookup tool with no output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% but the description adds valuable constraints: at least one of court_index_number or address/borough is required, and address matching is case-insensitive substring. It also lists the return fields, which is not in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool looks up marshal-executed evictions from a specific dataset, distinguishing it from filings or pending cases. It specifies the lookup methods and return fields, making it distinct from sibling tools which cover other property records.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly warns against using this for filings, warrants, or pending cases, and states that at least one of court index number or address/borough is required. It provides clear context for when to use, though it doesn't compare directly with siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

landlord_litigationA

HPD Housing Litigations (dataset 59kj-x8nc): HPD-initiated cases against landlords plus tenant actions. Look up by building (house_number + street + borough) OR by respondent (landlord/owner) name. At least one is required. Returns case type, open date, status, judgement, any harassment finding, penalty, and respondent, with a by-status summary. Respondent and street are matched as case-insensitive substrings. Keyless.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax cases to return (1-500, default 100).
streetNoStreet name (building lookup; matched as a substring).
boroughNoNYC borough: Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also accepts MN/BX/BK/QN/SI or 1-5). Required for a building lookup.
respondentNoRespondent name to match, e.g. an LLC or owner (substring, e.g. "realty llc").
case_statusNoOptional status filter, e.g. "OPEN" or "CLOSED".
house_numberNoBuilding house number (building lookup; requires street + borough too).

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full behavioral disclosure. It details return fields (case type, open date, status, etc.), matching behavior (case-insensitive substrings for respondent and street), and notes the tool is 'keyless'. No side effects or rate limits are mentioned, but for a read-only lookup this is sufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise (about 5 sentences), front-loaded with the dataset ID and purpose, and contains no redundant information. Every sentence serves a purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the absence of an output schema, the description adequately explains return values and includes a summary by status. It covers the key usage scenarios and matching behavior, making it complete for this lookup tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with descriptions for all 6 parameters. The description adds value by explaining required parameter combinations (building vs respondent), substring matching details, and borough shorthand (MN/BX/BK/QN/SI).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: looking up HPD housing litigations by building or respondent. It specifies the data source (dataset 59kj-x8nc) and distinguishes from sibling tools like building_violations which handle different types of records.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explains how to use the tool: look up by building (house_number + street + borough) or by respondent name, with at least one required. It does not explicitly state when not to use it or name alternative tools, but the context is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

landlord_portfolioA

The reverse of who_owns: every building currently registered with HPD under a given landlord, corporation, officer, or agent name. Searches HPD Registration Contacts (feu5-w2e2) for the name (case-insensitive substring against corporation names and person first/last names), then resolves each matched registration to its building (tesw-yqqr): address, borough, zip, BIN, registration dates, and which contact matched. Start from a name surfaced by who_owns or landlord_litigation. Landlords often hold each building in a separate LLC; officer and agent person names frequently connect buildings the LLC names hide. Reflects current HPD registration filings only. Keyless.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesOwner / corporation / officer / agent name to match, e.g. "WFHA 1520 SEDGWICK LP" or "JOHN WARREN" (case-insensitive substring against HPD registration-contact names; pass the fullest name you have, short fragments over-match).
limitNoMax buildings to return (1-500, default 50).
boroughNoOptional filter: only buildings in this borough. NYC borough: Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also accepts MN/BX/BK/QN/SI or 1-5).

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description discloses case-insensitive substring matching, resolution to buildings with details, current registration only, and the 'Keyless' access. It implies a read-only query.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured but slightly verbose. It efficiently covers purpose, process, usage, and limitations in a few sentences. Could be trimmed slightly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite no output schema, the description fully explains the return values (address, borough, zip, BIN, registration dates, matched contact) and data source. Context is thorough for the tool's complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds meaningful guidance: name is case-insensitive substring, pass fullest name to avoid over-matching, limit default 50, borough accepts multiple formats.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it is the reverse of who_owns, listing buildings under a landlord/corporation/officer/agent name. It specifies the data source (HPD Registration Contacts) and distinguishes from sibling tools like who_owns and landlord_litigation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It advises starting from names surfaced by who_owns or landlord_litigation, explaining when to use this tool to uncover hidden connections. It does not explicitly state when not to use, but provides clear context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

who_ownsA

Who is on file with HPD for a building, the 'who do I actually serve' tool. Joins HPD Registrations (tesw-yqqr) to Registration Contacts (feu5-w2e2) by registration id. Give the house number, street, and borough. Returns the registration(s) and every contact (corporate/individual owner, head officer, officer, agent, site manager) with names and business addresses. Reflects HPD registration filings, which can lag reality. Keyless.

ParametersJSON Schema
NameRequiredDescriptionDefault
streetYesStreet name (matched case-insensitively as a substring).
boroughYesNYC borough: Manhattan, Bronx, Brooklyn, Queens, or Staten Island (also accepts MN/BX/BK/QN/SI or 1-5).
house_numberYesBuilding house number, e.g. "1520".

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description fully discloses behavioral traits: it joins specific datasets, returns contact names and addresses, reflects HPD filings (which may lag reality), and notes it requires no API key. No contradictions exist.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, with three sentences that front-load the purpose and provide essential details without fluff. Every sentence adds value: purpose, data join, input/output, and limitations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with three parameters and no output schema, the description sufficiently explains inputs, outputs, data source, and limitations. It covers all necessary context for an AI agent to decide when and how to use it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema fully covers all three required parameters with descriptions (100% coverage). The description adds value by framing them as user instructions ('Give the house number...') and mentions matching behavior indirectly (substring matching for street is detailed in the schema).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states exactly what the tool does: it identifies who is on file with HPD, acts as the 'who do I actually serve' tool, and explains the data join process. It clearly distinguishes from sibling tools like building_violations by focusing on ownership contacts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly lists required inputs (house number, street, borough) and mentions the output (registrations and contacts). It provides a caveat about data lag, but lacks explicit guidance on when to use this tool versus sibling tools like landlord_portfolio.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

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

  1. 6 tool updatesv1.0.0
    • First observedbuilding_complaints
    • First observedbuilding_violations
    • First observedeviction_lookup
    • First observedlandlord_litigation
    • First observedlandlord_portfolio
    • First observedwho_owns

TDQS

A4.3/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a distinct aspect of NYC housing data: building violations, building complaints, building ownership, landlord portfolio, landlord litigation, and eviction lookups. There is no overlap in their purposes.

Naming Consistency4/5

Tool names use lowercase with underscores and follow a domain-prefix pattern for most (building_*, landlord_*). However, 'who_owns' and 'eviction_lookup' break the pattern slightly, though they are still clear.

Tool Count5/5

With 6 tools, the server covers key aspects of NYC housing data without being bloated or sparse. Each tool serves a distinct purpose, and the count is well-scoped for the domain.

Completeness4/5

The tools cover violations, complaints, ownership, portfolio, litigation, and evictions—core for housing research. Missing potential areas like building permits or tax info, but the set is largely complete for its focus.

Maintenance

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    MCP server for NYC real estate due diligence. Lets Claude query 22+ NYC public-record databases — DOB/HPD/ECB violations, ACRIS deeds, DOF sales, 311 complaints, FDNY incidents, NYPD complaints, marshal evictions, PLUTO, rent stabilization — in plain English.
    18
    7
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server for HUD housing data, enabling Fair Market Rent lookups, Section 8 income limits, and ZIP-to-county crosswalk mapping for affordable housing assessments.
    6
    4 npm
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server over the U.S. Department of Labor Wage and Hour Division (WHD) enforcement dataset: employer wage-theft history, back wages owed, civil penalties, and affected-employee counts. Built for worker-justice nonprofits, legal-aid intake, and union researchers.
    4
    4 npm
    MIT