Skip to main content
Glama
haksanlulz

mcp-fairrent

by haksanlulz

mcp-fairrent

MCP server for HUD housing data. Fair Market Rents by bedroom count (per area or a whole state at once), Section 8 income limits by household size, the LIHTC/MTSP income bands tax-credit buildings use, computed affordability verdicts, and the USPS ZIP-to-jurisdiction crosswalk in both directions (ZIP to county/tract/metro/district, and any of those back to its ZIPs). Built on the MCP TypeScript SDK.

For anyone answering "is this rent affordable here, and who qualifies for help?": tenant organizers, legal-aid intake, housing counselors, relocation planners, and agents that need real HUD numbers.

Tools

Tool

What it does

fmr_lookup

Fair Market Rent for an area, by bedroom count (efficiency through 4BR).

income_limits

The 30% / 50% / 80% AMI income thresholds for an area; pass a household size for the one line that applies. The 50% line is the usual Section 8 voucher cutoff.

affordability_check

The computed verdict: how far a rent sits above or below FMR for a bedroom size (dollars and percent), and which income bands (30/50/80% AMI) a household qualifies under. The arithmetic is done server-side, with the underlying numbers and table year for citation.

zip_crosswalk

Map a ZIP to the county, tract, CBSA, CBSA division, congressional district, or county subdivision it sits in, with city/state and all four of HUD's address shares so you pick the right one.

list_counties

Counties in a state with their FIPS entity ids, to look up by county name. In New England, one row per town, with the town name and a combined area label.

list_metro_areas

HUD metro areas (CBSAs) with their codes.

mtsp_income_limits

The LIHTC (tax-credit building) income bands at 20-80% AMI plus the HERA special bands (/mtspil/data). A different table from the Section 8 limits, governing a different housing stock.

state_fmr_overview

Every county's and metro's FMRs for a whole state in one call (/fmr/statedata), for comparing areas without one lookup per county. Also the way to get a working entity id in Connecticut, where the FMR table refuses the ids list_counties returns.

geo_to_zips

The reverse crosswalk: every ZIP inside a county, tract, metro, CBSA division, congressional district, or county subdivision, with residential-address shares and city/state.

Related MCP server: mcp-arcgis-houston

Install

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

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

npm start runs the TypeScript directly via tsx without building.

For hosts that install MCP Bundles: the bundle declares the HUD token as a required, sensitive setting, so the host collects it in its own UI instead of you editing a JSON env block.

npm run build:mcpb   # writes build/mcp-fairrent-<version>.mcpb

Open that file in the host. No bundle is attached to a release yet; build it from source.

Token

Every tool needs a free HUD USER API token. One-screen signup at huduser.gov → set HUD_API_TOKEN. The tools tell you so if it's missing.

HUD_CONTACT (optional) sets the contact string in the User-Agent sent to HUD; defaults to this repo's URL. Nothing loads a .env file. Set these in the shell or the MCP client's env block (.env.example lists them all).

Environment

Variable

Default

What it does

HUD_API_TOKEN

(none)

Required by every tool. Free HUD USER token.

HUD_CONTACT

this repo's URL

Contact string in the User-Agent sent to HUD.

HUD_HTTP_ATTEMPTS

3

Attempts per HUD request, 1-10. Only 429, 5xx and transport errors are retried.

HUD_RETRY_BACKOFF_MS

500,2000

Wait before each retry, comma-separated milliseconds, each 0-60000. Past the end of the list the last value repeats.

HUD_CACHE_TTL_MS

86400000 (24h)

Response-cache lifetime in milliseconds, 0 or more. 0 turns the cache off.

HUD_CACHE_MAX

300

Most responses kept before the oldest is dropped; minimum 1. To switch the cache off use HUD_CACHE_TTL_MS=0, not HUD_CACHE_MAX=0. A bound of zero would evict nothing, so it's refused.

The numeric knobs take a whole number; HUD_RETRY_BACKOFF_MS takes a comma-separated list of them. A value that isn't one, or that falls outside the range stated above, is ignored: the default applies and one line goes to stderr saying so. One bad entry rejects the whole backoff list, never half of it.

The flow

An address is usually a ZIP, but fmr_lookup and income_limits key on a 10-digit county entity id, so bridge the two:

  1. zip_crosswalk with the ZIP, to: county → the county's 5-digit FIPS (ZIP 10451 → geoid 36005, city BRONX, res_ratio 1).

  2. list_counties for that state → the county's 10-digit entity id (Bronx County → 3600599999).

  3. fmr_lookup and income_limits with that entity id → the bedroom rents and the voucher line.

Worked example: a Bronx landlord wants $2,600 for a 2-bedroom. Is that above Fair Market Rent, and would a family of three earning $48k qualify for a voucher here? That's affordability_check in one call (the entity id with rent: 2600, bedrooms: 2, income: 48000, household_size: 3) and the answer comes back computed. This is the sentence a housing counselor writes down, quoted from the two verdict strings, sentence-cased and joined (the fields themselves lead lower-case and carry no terminal period):

Rent $2,600 is $371 (12.5%) below the 2027 Fair Market Rent of $2,971 for a two-bedroom in Bronx County, NY. A 3-person household with annual income $48,000 in Bronx County, NY is very low income (at or below 50% of area median) under the 2026 HUD income limits — generally income-eligible for a Section 8 voucher.

Under those sit the numbers each one came from: the 30% line at $45,850 (does not qualify), the 50% line at $76,350 (qualifies; the usual voucher cutoff), the 80% line at $122,150 (qualifies). The model cites and the server does the arithmetic.

Two table years in one paragraph, deliberately. HUD publishes the two tables on separate cycles, and as of 2026-09-14 the FMR table answers 2027 while the income-limit table answers 2026. A combined call therefore carries table_years: { fmr, income, mismatch } at the top level, and each verdict names the year of the table that produced it, which is the safe way to quote a figure that goes stale annually: the year travels with the sentence. Asking for one year across both is refused by HUD on the income side (it doesn't downgrade quietly), and affordability_check keeps the half that answered: rent_check carries the 2027 verdict, and income_check comes back answered: false with no bands and a sentence saying to re-run with no year. That sentence doesn't claim to know which way the year was wrong. HUD answers the same {"error":"Invalid year"} for a year ahead of the income table and for one behind both tables (checked live 2026-09-14: year=2027 and year=2010 are byte-identical refusals, and /fmr/data refuses 2010 as well), so it names both. Nothing is reported as qualifying off a table that didn't answer.

New England is the exception, and step 2 is where it bites. HUD's FMR areas in CT, MA, ME, NH, RI and VT are towns, not counties, so list_counties returns one row per town (169 for Connecticut, 29 of them labelled "Hartford County") and town_name is the only thing telling them apart. Connecticut has gone further: it replaced counties with planning regions and HUD's crosswalk followed, so ZIP 06511 maps to geoid 09170 while list_counties for CT still returns legacy 090xx ids, and none of its 169 rows carries the 09170 prefix. The two don't meet. 0917099999, the FIPS + 99999 construction, is a 404 on both tables, and 0900952070, the id list_counties gives for New Haven town, is a 404 on the FMR table (the income-limit table resolves it, so a combined call fails on one half only). list_counties says so in its own payload for CT. For Connecticut, use state_fmr_overview and take the town's code: New Haven town is 0917052070, which both the FMR and income-limit tables answer on. The other five states aren't affected. The ids list_counties returns there do answer /fmr/data: 2502300170 (MA), 2302100100 (ME), 5000100325 (VT), 3301900260 (NH), 4400105140 (RI), each the first row its state's list returns, each a 200. The town half is regional; the redirect is Connecticut's renumbering alone. (Checked live, 2026-09-14.)

Entity ids

fmr_lookup and income_limits take a 10-digit county FIPS (e.g. 3600599999, which is county FIPS 36005 + 99999) or a metro CBSA code. zip_crosswalk and list_counties turn a ZIP into one.

Example

zip_crosswalk with zip: "10451", to: "county":

{
  "zip": "10451",
  "to": "county",
  "note": "res_ratio is the share of the ZIP's residential addresses in each geography; the highest-share county is usually the right entityid. bus_ratio, oth_ratio and tot_ratio are the same share for business, other and all addresses.",
  "matches": [
    { "geoid": "36005", "city": "BRONX", "state": "NY", "res_ratio": 1, "bus_ratio": 1, "oth_ratio": 1, "tot_ratio": 1 }
  ],
  "eligibility_scope": "HUD program tables, reproduced as published. Rent figures are Fair Market Rents, not a housing authority's payment standard; income figures are program eligibility lines, not a determination or an award. Confirm with the administering agency before relying on a number for a real household."
}

All four of HUD's ratios come through (residential, business, other, and the total) so tot_ratio can be reconciled against the parts it's made of. They're rounded to four decimal places. A ZIP with no crosswalk rows (retired, or PO-box-only) answers with an empty matches and a note saying so. That's an answer, not an error.

36005 is Bronx County; list_counties with state: "NY" gives its entity id 3600599999, which fmr_lookup and income_limits take.

Limitations

  • Numbers are HUD's published FMR and income-limit year tables, not live market rents.

  • The two tables publish on different cycles, so a combined affordability_check can answer from two different years. It reports both in table_years and each verdict names its own; a single year that only one table has is refused by HUD, never approximated.

  • In New England a HUD area is a town. list_counties returns one row per town, and in Connecticut the FMR table answers on the planning-region ids in state_fmr_overview rather than on the legacy county ids list_counties returns (see The flow).

  • HUD's tables bound the inputs: bedrooms 0-4 (FMR tables stop at four bedrooms), household size 1-8 (income-limit tables stop at eight; affordability_check's error gives HUD's convention for larger households).

  • affordability_check compares a single FMR row. Areas whose FMR data comes back multi-row (small-area/ZIP-level, or multi-year) are refused; pass a county entityid, or use fmr_lookup to see every row.

  • FMR isn't the voucher ceiling. Housing authorities set payment standards at 90-110% of FMR (24 CFR 982.503). The rent verdict carries this note.

  • Every response carries an eligibility_scope note: these are program lines, not personal determinations, and an answer is exactly as current as its table year.

Develop

npm test          # vitest over an in-memory transport, fetch mocked (no network, no token)
npm run smoke     # one live call per tool (needs HUD_API_TOKEN; skips without)
npm run typecheck
npm run build       # emit dist/ (what actually ships)
npm run verify:pack # pack, install into a clean dir, drive the installed binary over stdio
npm run build:mcpb  # stage + pack the .mcpb bundle into build/
npm run verify:mcpb # unpack that bundle and launch it the way a host does

Testing

Two tiers, already split by script. npm test is the offline tier: vitest, in-memory MCP transport, fetch mocked, no token. npm run smoke is the live tier: one real HUD call per tool, needs HUD_API_TOKEN, exits 0 with a skip line without it. CI runs only the offline tier.

Counts, measured 2026-09-14 after the last change of the day:

find . -name '*.ts' -not -path './node_modules/*' -not -path './dist/*' -not -path './build/*' -not -path './test/*' -not -name smoke.ts | xargs wc -l   # app: 1033 lines (index.ts + server.ts; smoke.ts is another 70)
find ./test -name '*.test.ts' | xargs wc -l                                                                                                                # tests: 1594 lines, 2 files
npm test                                                                                                                                                   # 75 tests, 75 passed

build/ is excluded because npm run build:mcpb stages a copy of the server there.

Layers. test/server.test.ts drives every tool end to end through the SDK client: input validation (bounds, required pairs, non-positive numbers) rejected before any HUD request; response shaping against fixtures copied from HUD's documented samples; the transport layer (retry on 429 and 5xx, no retry on 404, response cache keyed on path plus params, failures not cached). test/no-http-stack.test.ts pins that the source imports only the stdio transport, never an HTTP one, that package.json declares exactly one runtime dependency, and that nothing in the source writes to stdout, since that file descriptor belongs to JSON-RPC and diagnostics go to stderr.

Mutation probe, 2026-09-11: widened the affordability_check bedrooms bound in server.ts from > 4 to > 5. One test went red: affordability_check > enforces the table bounds: bedrooms 0-4, household_size 1-8. 47 others stayed green. Source restored, git diff --quiet -- server.ts clean.

Probed again 2026-09-14, once per change landed that day: reverting the metro-status comparison, the statedata name chain, the FMR Percentile field, the ?? r.geoid ZIP fallback, town_name on a county row, the town-first area label, the table_years block, the income-year error, and the validated env knobs each turned their own test red and nothing else. The retry tests were re-probed after the backoff ladder was flattened for speed: making a 404 retryable, and a 429 not, still fails them.

Fix round, same day. Two by mutation: deleting the backoff sleep from withRetry (the old timing test passed, its replacement fails) and deleting the retry-deadline break (the new deadline test hangs past its timeout). The rest were written red-first instead, which is the same evidence from the other side: the list_counties CT note, the Massachusetts note without the CT half, the kept rent verdict on a year the income table refuses, and the un-rewritten "Invalid year" each failed against the code as it stood before the change. The bundle probe was probed in both directions; GAUNTLET §6 has it.

Second fix round, same day. Three by mutation: dropping ?? backoffs[backoffs.length - 1] from withRetry (the new ladder test goes red at 4349ms against a 2000ms bound, while the attempt count stays at 4 either way); moving the 2BR fixture rent one dollar (the pinned README verdict goes red); and restoring the hand-written entityid error (the schema-vs-error test prints the two strings side by side). The income-year rewrite was written red-first. The two bundle-script fixes are not test-visible and were measured directly: a spaced path under shell: true exits 1 unquoted and 0 quoted, and the probe's temp directory count goes 23 → 24 on the old code and 24 → 24 on the new, on both the PASS and the FAIL path.

Call-count assertions (toHaveBeenCalledTimes, not.toHaveBeenCalled) were audited 2026-09-11 at 12 sites: 12 kept, 0 pruned. Re-counted 2026-09-15 at 22. The rounds above added ten, and the audited figure was left standing under paragraphs that had moved what "the same day" pointed at. The ten: the backoff ladder's attempt counts, a malformed attempts knob still making the request, the cache TTL and cache-bound knobs, the two console-channel spies, and the entityid error firing before any network call. Each one pins a contract (which endpoint a call hit, validation firing before the network, retry counts, cache hits, which console method a diagnostic took). None of them asserts that a function ran. Policy: assert behavior and payloads, never bare invocation.

AI assistance

This project was built with AI assistance (Claude). Correctness rests on the checks. The vitest suite drives every tool over the MCP in-memory transport against fixtures that mirror HUD's documented response samples, including this README's worked example, and npm run smoke makes one live call per tool against the real HUD API. I reviewed the code and I'm accountable for what it does.

License

MIT © Abishai James. Data is public U.S. government data from the HUD USER API; this project is unofficial and not affiliated with HUD.

Available Tools

6 tools
affordability_checkA

Affordability verdicts computed server-side from the same HUD tables as fmr_lookup and income_limits: whether a proposed rent is above or below the Fair Market Rent for a bedroom size (dollar and percent gap), and which HUD income bands a household qualifies under (extremely low 30%, very low 50% = the Section 8 voucher line, low 80% of area median). Pass rent + bedrooms, income + household_size, or all four; returns the verdicts with the underlying numbers and table year for citation. entityid is a 10-digit county FIPS or a metro CBSA code (same as fmr_lookup).

ParametersJSON Schema
NameRequiredDescriptionDefault
rentNoProposed monthly rent in dollars, to compare against the FMR for the given bedrooms
yearNoTable year (e.g. '2026'); default is the latest
incomeNoAnnual gross household income in dollars, to compare against the 30/50/80% area-median lines
bedroomsNoBedroom count 0-4 (0 = efficiency/studio); required when rent is given
entityidYes10-digit county entity id (county FIPS + 99999) or metro CBSA code. Derive from a ZIP via zip_crosswalk then list_counties
household_sizeNoFamily size 1-8; required when income is given

TDQS

A4.7/5.0
Behavior5/5

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

Despite no annotations, the description fully discloses behavior: it returns verdicts with dollar/gap and income band qualifications, includes underlying numbers and table year for citation, and explains entityid format and derivation.

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?

Three concise, front-loaded sentences each serve a purpose: defining the tool, explaining inputs/outputs, and clarifying entityid. No redundant or extraneous text.

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?

The description covers the main outputs and input rules, but lacks mention of default year behavior or error conditions. For a tool with no output schema, it is still very complete.

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?

The description adds significant context beyond the input schema, explaining relational constraints (rent requires bedrooms, income requires household_size) and clarifying entityid usage, even though schema coverage is 100%.

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 computes affordability verdicts combining FMR and income limit comparisons, and distinguishes from sibling tools fmr_lookup and income_limits by mentioning it uses the same HUD tables but provides combined results.

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 the flexible input combinations (rent+bedrooms, income+household_size, or all four) and references sibling tools for deriving entityid, but does not explicitly state when to prefer this tool over fmr_lookup or income_limits individually.

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

fmr_lookupB

HUD Fair Market Rent (the rent a modest unit should cost) for an area, by bedroom count. entityid is a HUD entity id: a 10-digit county FIPS (e.g. 3600599999 for Bronx County = 36005 + 99999) or a metro CBSA code. Use zip_crosswalk then list_counties to turn a ZIP into a county entityid.

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoFMR year (e.g. '2026'); default is the latest
entityidYes10-digit county entity id (county FIPS + 99999) or metro CBSA code. Derive from a ZIP via zip_crosswalk then list_counties

TDQS

B3.2/5.0
Behavior2/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It implies a read-only lookup but does not explicitly state that there are no side effects, rate limits, or authentication requirements. The return format is also not fully described.

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 concise sentences front-load the purpose and provide essential input derivation context. No unnecessary words or repetition.

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?

Given the simple schema and no output schema, the description adequately explains input derivation but lacks details on the exact return format (e.g., structure of FMR values by bedroom count). It does not describe pagination or error handling.

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 schema already covers both parameters with descriptions. The description adds value by explaining the entityid format in detail with an example and clarifies the year parameter's default behavior.

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

Purpose4/5

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

The description clearly states it provides HUD Fair Market Rent for an area by bedroom count. It explains the entityid format and how to derive it from a ZIP using sibling tools, but does not explicitly differentiate itself from similar tools like income_limits or affordability_check.

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

Usage Guidelines2/5

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

No guidance is given on when to use this tool versus alternatives such as income_limits or affordability_check. It only describes how to prepare the entityid input, not the conditions for choosing this tool over others.

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

income_limitsA

HUD income limits for an area: the extremely-low (30% AMI), very-low (50%, the Section 8 voucher line), and low (80%) income thresholds, by household size. Pass household_size (1-8) to get the single threshold that applies to a family that size. entityid is a 10-digit county FIPS or a metro CBSA code (same as fmr_lookup).

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoIncome-limit year; default is the latest
entityidYes10-digit county entity id (county FIPS + 99999) or metro CBSA code. Derive from a ZIP via zip_crosswalk then list_counties
household_sizeNoFamily size 1-8; omit for all sizes

TDQS

A4.2/5.0
Behavior3/5

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

No annotations are provided, so the description carries full transparency burden. It describes the output (income thresholds) and the effect of parameters, but does not disclose behavioral traits such as read-only nature, data freshness, or rate limits. Adequate for a simple lookup tool.

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 extremely concise—three sentences with no wasted words. It front-loads the purpose and immediately provides actionable details, making it easy for an agent to parse and use.

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?

Given no output schema, the description sufficiently explains what the tool returns (three thresholds, single if household_size provided) and covers all parameters. It references a sibling tool (fmr_lookup) for entityid format. Lacks explicit mention of output structure but is otherwise complete.

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%, so the description adds value beyond the schema by explaining the entityid format (10-digit county FIPS or metro CBSA code, same as fmr_lookup) and the effect of household_size (1-8, returns single threshold). The year parameter is noted as defaulting to latest.

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 retrieves HUD income limits for an area, specifies the three thresholds (30%, 50%, 80% AMI), and explains how household size affects the output. It distinguishes from fmr_lookup by noting the same entityid format, providing context for differentiation.

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 gives clear guidance: when to include household_size (for a single threshold) and explains the entityid format with a reference to fmr_lookup. However, it does not explicitly state when not to use this tool or suggest alternatives like affordability_check.

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

list_countiesA

List the counties in a state with their 10-digit FIPS entity ids, so you can look up FMR or income limits by county name. Pass a 2-letter state code.

ParametersJSON Schema
NameRequiredDescriptionDefault
stateYes2-letter state code (e.g. 'NY')

TDQS

A4.3/5.0
Behavior3/5

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

No annotations are provided, so the description carries the full burden. It states that the tool lists counties with FIPS IDs, which implies a read-only operation. However, it does not disclose any additional behavioral traits such as authentication, rate limits, or reliance on external APIs. The description is minimally adequate.

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 a single, well-structured sentence that front-loads the action and purpose. Every word is necessary, and there is no redundancy or unnecessary detail.

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 tool's simplicity (one parameter, no output schema) and the presence of sibling tools, the description is complete. It explains what the tool does, why it is useful, and how to use it. The return format (county names and FIPS IDs) is implied, which is sufficient for this context.

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?

The input schema already provides a description for the single parameter 'state' ('2-letter state code (e.g. 'NY')'). The tool description simply repeats this ('Pass a 2-letter state code'). With 100% schema description coverage, the description adds no new meaning beyond what the schema provides, earning a baseline score of 3.

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 verb 'List' and the resource 'counties in a state' with a specific purpose ('so you can look up FMR or income limits by county name'). This distinguishes it from sibling tools like fmr_lookup and income_limits, which perform different actions.

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

Usage Guidelines5/5

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

The description explicitly ties the tool to subsequent lookups (FMR, income limits), indicating when to use it. It also specifies the required input format ('Pass a 2-letter state code'), providing clear usage instructions. Although it does not mention when not to use, the context is sufficiently clear given the sibling tools.

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

list_metro_areasA

List HUD metropolitan areas (CBSAs) with their codes, for metro-level FMR and income-limit lookups.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.3/5.0
Behavior3/5

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

No annotations are provided, so the description bears the full burden of behavioral disclosure. It indicates the tool returns codes, but does not mention response format, pagination, or data freshness. For a simple enumeration tool, this is adequate but not highly descriptive.

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 a single, front-loaded sentence that efficiently conveys the action, resource, and purpose. No unnecessary words or repetition.

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 zero-parameter listing tool with no output schema, the description is complete. It explicitly connects to sibling tools (FMR, income limits) and explains the value of the output (codes for lookups).

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 has zero parameters and schema description coverage is 100%, so the baseline is 4. The description adds no parameter information because none exist, but it does hint at the response content ('with their codes').

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 action (List), the resource (HUD metropolitan areas), and the purpose (for metro-level FMR and income-limit lookups). It distinguishes itself from sibling tools like fmr_lookup or income_limits by indicating it provides codes for those lookups.

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 states the use case: 'for metro-level FMR and income-limit lookups.' This provides clear context for when to invoke this tool, but it does not mention any exclusions or alternatives. However, as a listing tool, the usage is naturally implied as a prerequisite step.

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

zip_crosswalkA

Map a 5-digit ZIP to the county, tract, CBSA (metro), or congressional district it falls in, using the HUD-USPS crosswalk. Returns each matching geography with its residential-address share (res_ratio); the highest-share county is the one to resolve into an entityid via list_counties.

ParametersJSON Schema
NameRequiredDescriptionDefault
toNoTarget geography: county | tract | cbsa | cd (default county)
zipYes5-digit ZIP code
yearNoCrosswalk year; default is the latest

TDQS

A4.3/5.0
Behavior4/5

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

No annotations present, so description carries full burden. Describes the return format (geography + res_ratio) and hints at selection behavior (highest-share county). Does not mention potential edge cases (missing ZIP, multiple matches) but overall informative.

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 sentences with no extraneous text. Front-loaded with core purpose and immediately useful detail (res_ratio, entityid resolution).

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 no output schema, description explains exactly what is returned and how to interpret it (res_ratio, highest-share county). Includes cross-references to sibling tools for follow-up. Complete for a simple geospatial mapping tool.

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 baseline is 3. Description adds no additional meaning to parameters themselves, but provides context on the output (res_ratio) that could influence parameter choice.

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?

Explicitly states the mapping from 5-digit ZIP to county/tract/CBSA/CD using HUD-USPS crosswalk, and mentions the res_ratio return value. Distinguishes from sibling tools like list_counties by referencing it as a subsequent step.

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?

Clearly describes the use case: mapping ZIPs to geography. Provides guidance on next step (resolving highest-share county via list_counties). Lacks explicit when-not-to-use or alternatives, but context is sufficient.

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 observedaffordability_check
    • First observedfmr_lookup
    • First observedincome_limits
    • First observedlist_counties
    • First observedlist_metro_areas
    • First observedzip_crosswalk

TDQS

A4/5.0

Scored across 6 tools

Disambiguation5/5

Each tool has a distinct purpose: fmr_lookup for rent, income_limits for income thresholds, affordability_check for combined analysis, zip_crosswalk for geocoding, list_counties and list_metro_areas for geography enumeration. No overlap.

Naming Consistency4/5

Most names follow a predictable verb_noun pattern (list_counties, list_metro_areas) or noun_noun (zip_crosswalk), but some are noun_verb (fmr_lookup, affordability_check). Slight inconsistency but still clear.

Tool Count5/5

6 tools cover the core functionality of HUD fair market rent and income limit lookups with necessary geocoding support. Each tool earns its place without being excessive.

Completeness4/5

The set provides comprehensive querying for rent, income, affordability, and geolocation. Missing potential features like historical data or batch operations, but the core use case is well-covered.

Maintenance

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Remote MCP server exposing US Census (ACS 5-year) and FEMA flood data. Works as a connector in both Claude and ChatGPT.
    -
  • A
    license
    A
    quality
    A
    maintenance
    MCP server for NYC housing data, enabling tenant organizers and legal-aid intakes to pull building violations, complaints, ownership, litigation, and evictions. It wraps six city datasets from NYC Open Data.
    6
    18 npm
    MIT