nyrezone-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-10-29 (unpublished)
4
+
5
+ - Remove the prior FAR-times-lot-area-minus-gross-building-area calculation for
6
+ every parcel. Recorded MapPLUTO FAR, area and district facts remain available;
7
+ they do not establish parcel capacity, unused development rights or compliance.
8
+ - Retain deprecated `unused_floor_area.estimate_sqft`, `allowed_floor_area_sqft`,
9
+ `governing_far` and `overbuilt` keys as null, with basis
10
+ `not determined from recorded parcel data` and explicit limitations. The tool
11
+ output schema now declares these keys null; consumers must accept null.
12
+ - Remove calculated capacity and compliance inferences from lot prose, tool
13
+ descriptions, server instructions and package/registry descriptions.
14
+ - Label the public map link `Public lot map`; preserve `full_report_url` as a
15
+ documented legacy field name that does not provide paid report data.
16
+ - Identify district output as scraped NYRezone HTML summaries, without claiming
17
+ direct verification against the NYC Zoning Resolution.
18
+ - Identify release labels and check stamps as site-reported, not independently
19
+ validated for each result; use unknown when the release stamp is missing or
20
+ unavailable. Align package, lockfile, runtime and registry manifest at 0.2.0.
21
+ - Expand existing fixture and real stdio tests for the former 16,094 sq ft example,
22
+ the formerly flagged Empire State Building, false split flags with multiple
23
+ districts, unknown source labels, null compatibility and owner-field exclusion.
24
+ - Update existing exact dependency pins to MCP server/client SDK 2.4.0 and Zod
25
+ 4.6.7 to resolve native npm audit advisories affecting the previous pins.
26
+ - Reuse the existing SDK, test suite and documentation; no new service, framework,
27
+ schedule or publishing-trust change.
28
+
29
+ ## 2026-10-28 — Documentation accuracy
30
+
31
+ Clarify that the published 0.1.0 stdio package is separate from the hosted ChatGPT
32
+ facts app. Recorded FAR fields, calculated screening outputs and district summaries
33
+ parsed from NYRezone are distinguished from parcel-specific legal capacity. The
34
+ README now documents the separate residential-estimate/highest-FAR flag behavior,
35
+ the legacy `allowed_floor_area_sqft`/`sq ft allowed` wording, split-lot limits and
36
+ the public map destination of the `Full report` link. No runtime, version or
37
+ dependency changed; these disclosures do not certify capacity or all district rules.
38
+
39
+ Verification: 39 existing fixture/stdio tests passed. Published npm JavaScript
40
+ matches the fresh source build. Three public BBL examples matched selected city
41
+ record fields, and the city records' 26v2 version matched the package's methodology
42
+ label on October 28. These observations establish neither legal allowance nor
43
+ exhaustive API/district correctness.
44
+
3
45
  ## 0.1.1 - planned
4
46
 
5
47
  - Optional packaging: MCPB bundle and Smithery listing (removed from 0.1.0 until they can be built, signed and checksummed for real).
6
48
 
7
49
  ## 0.1.0 - 2026-10-15
8
50
 
9
- Initial local build (not published).
51
+ Initial build; version 0.1.0 was subsequently published to npm and the official MCP
52
+ registry on October 16, 2026.
10
53
 
11
54
  - Tools: `lookup_lot`, `get_zoning_district`, `list_zoning_districts`.
12
55
  - Lot data from NYRezone `/api/pluto`; owner name/type and assessed values are dropped before any result is returned.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # nyrezone-mcp
2
2
 
3
- Read-only [MCP](https://modelcontextprotocol.io) server that gives AI assistants NYC zoning facts from the public data on [NYRezone](https://nyrezone.com): which zoning district a tax lot is in, built vs. allowed FAR, lot and building area, an estimated unused floor area, and the rules for each zoning district.
3
+ Read-only [MCP](https://modelcontextprotocol.io) server that gives AI assistants recorded NYC lot fields from public [NYRezone](https://nyrezone.com) pages and APIs: zoning districts, MapPLUTO FAR fields, lot and gross building area, and district summaries as published by NYRezone.
4
+
5
+ The legacy stdio package is separate from NYRezone's hosted ChatGPT facts app.
6
+ Version 0.2.0 removes the previous floor-area calculations. Recorded parcel data
7
+ and scraped district summaries do not determine parcel-specific legal capacity,
8
+ unused development rights or zoning compliance, or certify that every district
9
+ rule is current.
4
10
 
5
11
  It exposes only data that is already public and unauthenticated. No owner names, no assessed values, no paid report data, no account access.
6
12
 
@@ -36,25 +42,57 @@ All tools are read-only (`readOnlyHint: true`).
36
42
 
37
43
  | Tool | Input | Returns |
38
44
  |---|---|---|
39
- | `lookup_lot` | `address` (starts with a house number, include the borough) **or** `bbl` (10-digit borough-block-lot) | Zoning district(s), overlays and special districts, built FAR vs. maximum FAR, lot area, building area, estimated unused floor area, year built, land use. Ambiguous or unknown input returns an error, never a guess. |
45
+ | `lookup_lot` | `address` (starts with a house number, include the borough) **or** `bbl` (10-digit borough-block-lot) | Recorded zoning district(s), overlays, special districts and FAR fields, lot/gross building area, year built and land use. Deprecated capacity calculation fields are retained as null. Ambiguous or unknown address matches return an error. |
40
46
  | `get_zoning_district` | `code`, e.g. `R6A`, `C4-2`, `M1-2/R6A` | FAR, height rules and bonuses as NYRezone publishes them, citywide lot counts, average built FAR. Unknown code returns the list of valid codes. |
41
47
  | `list_zoning_districts` | optional `family`, e.g. `R6` | The 51 districts NYRezone documents, each with its district-family description. |
42
48
 
43
- Every result ends with a source line (MapPLUTO release as stated by NYRezone, the matching nyrezone.com page, and for lots a `Full report` link) and a disclaimer.
49
+ Every successful result includes a source line and disclaimer. The MapPLUTO release
50
+ label for lot lookups is reported by NYRezone's methodology page; district lookups
51
+ use the label on the district page. Neither is independently validated for the
52
+ returned parcel or result. A missing or unavailable release stamp is `unknown`. District rules are parsed from NYRezone's pages, not fetched directly
53
+ from the Zoning Resolution. The visible `Public lot map` link and legacy `full_report_url` field both open
54
+ the public lot map; neither returns paid report data.
44
55
 
45
56
  Not included: a borough-level "underbuilt" summary. NYRezone publishes that only as web pages, not as a data endpoint.
46
57
 
47
58
  ### Example
48
59
 
49
- `lookup_lot` with `bbl: "3052410015"` returns, in part: zoning R7A, lot area 4,675 sq ft, building area 2,606 sq ft, built FAR 0.56 against a 4.0 residential maximum, and an estimated 16,094 sq ft unused.
60
+ `lookup_lot` with `bbl: "3052410015"` returns, in part: recorded zoning R7A,
61
+ lot area 4,675 sq ft, gross building area 2,606 sq ft, built FAR 0.56 and a
62
+ MapPLUTO residential FAR field of 4.0. It does not calculate capacity from those
63
+ fields. Its `unused_floor_area` compatibility object includes:
64
+
65
+ ```json
66
+ {
67
+ "estimate_sqft": null,
68
+ "allowed_floor_area_sqft": null,
69
+ "governing_far": null,
70
+ "overbuilt": null,
71
+ "basis": "not determined from recorded parcel data"
72
+ }
73
+ ```
74
+
75
+ The object also includes `caveats` describing the limits below.
76
+
77
+ ## Capacity is not determined
50
78
 
51
- ## How the unused-floor-area estimate works
79
+ The previous release multiplied recorded FAR by lot area and subtracted gross
80
+ building area. Version 0.2.0 removes that arithmetic for every lot, including
81
+ split-zoned lots and records with multiple districts but no split-zone flag.
82
+ The four deprecated fields above remain present and always null; their output
83
+ schema accepts null. Consumers that assumed a numeric estimate or boolean flag
84
+ must handle null.
52
85
 
53
- `maximum FAR x lot area - existing building area`, using MapPLUTO's residential FAR (commercial or community-facility FAR if there is no residential FAR). It is a screening number only. It does not account for street width, special districts, overlays, split-zoned lots, mapped streets, or City of Yes / Universal Affordability Preference bonuses. If the building already exceeds the maximum, the tool reports 0 and says the lot is over-built.
86
+ Gross building area is not zoning floor area. Recorded FAR fields alone do not
87
+ establish applicable FAR or legally permitted floor area. District boundaries,
88
+ split-zone geometry, applicable uses, special districts, overlays, bonuses,
89
+ zoning-lot history and current rules require separate verification. The tool
90
+ returns the recorded district and FAR facts without deciding capacity, unused
91
+ development rights or compliance.
54
92
 
55
93
  ## Data sources and what leaves your machine
56
94
 
57
- - **nyrezone.com** (`/api/pluto`, `/zoning`, `/zoning/<code>`, `/methodology`): lot data and district pages. Data is NYC DCP MapPLUTO; the release is read from NYRezone's own methodology page.
95
+ - **nyrezone.com** (`/api/pluto`, `/zoning`, `/zoning/<code>`, `/methodology`): lot data and district pages. The lot endpoint supplies MapPLUTO fields; HTML district summaries are NYRezone's presentation, not direct Zoning Resolution verification. Release labels and check stamps are reported by the site, not independently validated for each returned parcel.
58
96
  - **NYC Planning Labs Geosearch** (`geosearch.planninglabs.nyc`): used only to turn a street address into a BBL, because NYRezone has no address endpoint. This is the one third-party host; the address you ask about is sent to it. BBL lookups and district lookups never contact it.
59
97
 
60
98
  The owner name and owner type that NYRezone's lot API returns are dropped in this server and never appear in a tool result.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.2.0";
2
2
  export declare const REPO_URL = "https://github.com/gbrussich52/nyrezone-mcp";
3
3
  export type UpstreamKind = 'timeout' | 'http' | 'network' | 'rate_limited' | 'too_large' | 'parse';
4
4
  export declare class UpstreamError extends Error {
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // HTTP access for the two public hosts this server reads. Everything that touches
2
2
  // the network goes through here so the politeness rules (User-Agent, cache,
3
3
  // serialized requests, request cap, timeout, size cap) live in one place.
4
- export const VERSION = '0.1.0';
4
+ export const VERSION = '0.2.0';
5
5
  export const REPO_URL = 'https://github.com/gbrussich52/nyrezone-mcp';
6
6
  export class UpstreamError extends Error {
7
7
  kind;
package/dist/lot.d.ts CHANGED
@@ -36,19 +36,13 @@ export interface LotRecord {
36
36
  unused_floor_area: UnusedFloorArea;
37
37
  }
38
38
  export interface UnusedFloorArea {
39
- /** Estimated unused floor area in sq ft, or null when the inputs are missing. */
40
- estimate_sqft: number | null;
41
- /** True when existing building area already exceeds the FAR-based maximum. */
42
- overbuilt: boolean;
43
- allowed_floor_area_sqft: number | null;
44
- governing_far: number | null;
39
+ /** Deprecated compatibility fields: recorded parcel data does not determine capacity. */
40
+ estimate_sqft: null;
41
+ overbuilt: null;
42
+ allowed_floor_area_sqft: null;
43
+ governing_far: null;
45
44
  basis: string;
46
45
  caveats: string[];
47
46
  }
48
- export declare function computeUnusedFloorArea(lotArea: number | null, buildingArea: number | null, far: {
49
- residential: number | null;
50
- commercial: number | null;
51
- community_facility: number | null;
52
- }, splitZone: boolean): UnusedFloorArea;
53
47
  /** Allow-list mapping from the raw API row. Throws InputError-free; callers validate the BBL first. */
54
48
  export declare function mapPlutoRow(raw: unknown, bbl: string): LotRecord;
package/dist/lot.js CHANGED
@@ -60,55 +60,19 @@ function str(v) {
60
60
  return s === '' ? null : s;
61
61
  }
62
62
  const round = (n, places) => Math.round(n * 10 ** places) / 10 ** places;
63
- export function computeUnusedFloorArea(lotArea, buildingArea, far, splitZone) {
64
- const caveats = [
65
- 'Simple MapPLUTO-based estimate: maximum FAR x lot area minus existing building area.',
66
- 'Building area is MapPLUTO gross area, which is larger than zoning floor area.',
67
- 'Ignores street width, special districts, overlays, lot splits, mapped streets, and City of Yes / Universal Affordability Preference bonuses, which can change the true maximum.',
68
- ];
69
- if (splitZone)
70
- caveats.push('This lot spans more than one zoning district; MapPLUTO reports a single set of FAR values, so the estimate may be off.');
71
- // Highest listed FAR governs: a commercial or community-facility FAR can exceed residential
72
- // (e.g. C5-3 lists residential 10 but commercial 15), and using residential alone would
73
- // wrongly flag such lots as overbuilt.
74
- const candidates = [
75
- ['residential', far.residential ?? 0],
76
- ['commercial', far.commercial ?? 0],
77
- ['community-facility', far.community_facility ?? 0],
78
- ];
79
- const top = Math.max(...candidates.map(([, v]) => v));
80
- let governing = null;
81
- let basis;
82
- if (top > 0) {
83
- governing = top;
84
- const names = candidates.filter(([, v]) => v === top).map(([n]) => n).join(' / ');
85
- basis = `Highest FAR MapPLUTO lists is ${top} (${names}).`;
86
- }
87
- else {
88
- basis = 'MapPLUTO lists no maximum FAR for this lot, so no estimate is possible.';
89
- }
90
- if (governing === null || lotArea === null || lotArea <= 0 || buildingArea === null) {
91
- if (governing !== null)
92
- basis += ' Lot area or building area is missing, so no estimate is possible.';
93
- return { estimate_sqft: null, overbuilt: false, allowed_floor_area_sqft: null, governing_far: governing, basis, caveats };
94
- }
95
- // Headline estimate uses residential FAR when listed (matches NYRezone's methodology); the
96
- // overbuilt flag uses the highest FAR so community-facility or commercial uses never make a
97
- // lot look non-compliant. Community-facility FAR alone must not inflate a residential lot's headroom.
98
- const resid = far.residential ?? 0;
99
- const estFar = resid > 0 ? resid : governing;
100
- if (resid > 0 && resid < governing) {
101
- basis = `Estimate uses residential FAR ${resid}; MapPLUTO also lists up to ${governing} for other uses (commercial / community-facility).`;
102
- }
103
- const allowed = estFar * lotArea;
104
- const overbuilt = buildingArea > governing * lotArea;
63
+ function undeterminedFloorArea() {
105
64
  return {
106
- estimate_sqft: Math.max(0, Math.round(allowed - buildingArea)),
107
- overbuilt,
108
- allowed_floor_area_sqft: overbuilt ? Math.round(governing * lotArea) : Math.round(allowed),
109
- governing_far: overbuilt ? governing : estFar,
110
- basis,
111
- caveats,
65
+ estimate_sqft: null,
66
+ overbuilt: null,
67
+ allowed_floor_area_sqft: null,
68
+ governing_far: null,
69
+ basis: 'not determined from recorded parcel data',
70
+ caveats: [
71
+ 'MapPLUTO building area is gross building area, not zoning floor area.',
72
+ 'Recorded FAR fields do not establish an applicable FAR or legally permitted floor area for this parcel.',
73
+ 'District boundaries and split-zone geometry, applicable uses, special districts, overlays, bonuses, zoning-lot history and current rules require separate verification.',
74
+ 'Unused development rights, development capacity and zoning compliance are not determined by this tool.',
75
+ ],
112
76
  };
113
77
  }
114
78
  /** Allow-list mapping from the raw API row. Throws InputError-free; callers validate the BBL first. */
@@ -160,6 +124,6 @@ export function mapPlutoRow(raw, bbl) {
160
124
  year_built: yearBuilt && yearBuilt > 0 ? yearBuilt : null,
161
125
  built_far: builtFar === null ? null : round(builtFar, 2),
162
126
  max_far: farRounded,
163
- unused_floor_area: computeUnusedFloorArea(lotArea, buildingArea, far, splitZone),
127
+ unused_floor_area: undeterminedFloorArea(),
164
128
  };
165
129
  }
package/dist/server.js CHANGED
@@ -4,9 +4,9 @@ import { HttpClient, VERSION } from './client.js';
4
4
  import { getDistrict, listDistricts, lookupLot } from './service.js';
5
5
  const INSTRUCTIONS = [
6
6
  'Read-only access to public NYC zoning data via NYRezone (nyrezone.com), which is built on NYC MapPLUTO.',
7
- 'Tools: lookup_lot (a tax lot by address or 10-digit BBL), get_zoning_district (rules for one zoning code), list_zoning_districts.',
7
+ 'Tools: lookup_lot (a tax lot by address or 10-digit BBL), get_zoning_district (NYRezone HTML summary for one zoning code), list_zoning_districts.',
8
8
  'Report only what the tools return and always keep the source line and disclaimer with it.',
9
- 'Unused floor area is a simplified FAR-based estimate, not a legal buildable-area determination; say so.',
9
+ 'Recorded FAR and gross building area do not determine development capacity, unused development rights or zoning compliance. Deprecated unused_floor_area calculation fields are always null.',
10
10
  'If a tool reports an ambiguous or unknown input, ask the user to clarify rather than guessing.',
11
11
  ].join(' ');
12
12
  // openWorldHint is true because every call reaches out to public web services.
@@ -39,10 +39,10 @@ const lotSchema = z.object({
39
39
  built_far: z.number().nullable(),
40
40
  max_far: z.object({ residential: z.number().nullable(), commercial: z.number().nullable(), community_facility: z.number().nullable() }),
41
41
  unused_floor_area: z.object({
42
- estimate_sqft: z.number().nullable(),
43
- overbuilt: z.boolean(),
44
- allowed_floor_area_sqft: z.number().nullable(),
45
- governing_far: z.number().nullable(),
42
+ estimate_sqft: z.null().describe('Deprecated; capacity is not determined from recorded parcel data.'),
43
+ overbuilt: z.null().describe('Deprecated; zoning compliance is not determined from gross building area.'),
44
+ allowed_floor_area_sqft: z.null().describe('Deprecated; legally permitted floor area is not determined.'),
45
+ governing_far: z.null().describe('Deprecated; applicable FAR is not determined.'),
46
46
  basis: z.string(),
47
47
  caveats: z.array(z.string()),
48
48
  }),
@@ -52,7 +52,7 @@ export function createServer(clientOptions = {}) {
52
52
  const server = new McpServer({ name: 'nyrezone-mcp', version: VERSION, title: 'NYRezone - NYC zoning lookups' }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
53
53
  server.registerTool('lookup_lot', {
54
54
  title: 'Look up an NYC tax lot',
55
- description: 'Look up one NYC tax lot by street address OR 10-digit BBL (borough-block-lot). Returns zoning district(s), built vs. allowed FAR, lot and building area, an estimated unused floor area, year built and land use, from MapPLUTO via NYRezone. Provide exactly one of address or bbl. Addresses must start with a house number and should include the borough; an ambiguous or unknown address returns an error instead of a guess. Owner and assessment data are never returned.',
55
+ description: 'Look up one NYC tax lot by street address OR 10-digit BBL (borough-block-lot). Returns recorded zoning district(s), MapPLUTO FAR fields, lot and gross building area, year built and land use via NYRezone. Does not determine development capacity, unused development rights, applicable FAR or zoning compliance; deprecated calculation fields are null. Provide exactly one of address or bbl. Addresses must start with a house number and should include the borough; an ambiguous or unknown address returns an error instead of a guess. Owner and assessment data are never returned.',
56
56
  inputSchema: z.object({
57
57
  address: z.string().optional().describe('Street address starting with a house number, e.g. "1187 Ocean Ave, Brooklyn".'),
58
58
  bbl: z.string().optional().describe('10-digit BBL, e.g. 3052410015 (1 Manhattan, 2 Bronx, 3 Brooklyn, 4 Queens, 5 Staten Island).'),
@@ -61,7 +61,7 @@ export function createServer(clientOptions = {}) {
61
61
  lot: lotSchema,
62
62
  resolved_from: z.enum(['bbl', 'address']),
63
63
  matched_address: z.string().nullable(),
64
- full_report_url: z.string(),
64
+ full_report_url: z.string().describe('Legacy field name: public lot map URL; no paid report data'),
65
65
  district_pages: z.array(z.string()),
66
66
  source: z.string(),
67
67
  }),
@@ -69,7 +69,7 @@ export function createServer(clientOptions = {}) {
69
69
  }, async (args) => respond(await lookupLot(client, args)));
70
70
  server.registerTool('get_zoning_district', {
71
71
  title: 'Get an NYC zoning district',
72
- description: 'Get the rules NYRezone publishes for one NYC zoning district code (e.g. R6A, C4-2, M1-2/R6A): max FAR, base and maximum height, bonuses, citywide lot counts and average built FAR. Height/setback detail is only what NYRezone publishes for that district. An unknown code returns the list of valid codes.',
72
+ description: 'Get the HTML district summary NYRezone publishes for one NYC zoning district code (e.g. R6A, C4-2, M1-2/R6A): max FAR, base and maximum height, bonuses, citywide lot counts and average built FAR. Values are scraped NYRezone summaries, not independently verified against the Zoning Resolution or applied to a parcel. An unknown code returns the list of valid codes.',
73
73
  inputSchema: z.object({ code: z.string().describe('Zoning district code such as R6A, R7-2, C4-2 or M1-2/R6A (case-insensitive).') }),
74
74
  outputSchema: z.object({
75
75
  code: z.string(),
package/dist/service.js CHANGED
@@ -5,7 +5,9 @@ import { AddressNotFoundError, AmbiguousAddressError, resolveAddress } from './g
5
5
  import { InputError, mapPlutoRow, parseBbl } from './lot.js';
6
6
  import { ParseError, parseDistrictPage, parseVintage, parseZoningIndex } from './parse.js';
7
7
  export const DISCLAIMER = 'Informational only; not legal, architectural, or zoning-compliance advice. Confirm with NYC Department of City Planning / a licensed professional before relying on it.';
8
- const FALLBACK_VINTAGE = 'NYC MapPLUTO (release as stated at https://nyrezone.com/methodology)';
8
+ function reportedVintage(release, checkedOn) {
9
+ return `MapPLUTO release reported by NYRezone: ${release ?? 'unknown'}${checkedOn ? ` (site check stamp: ${checkedOn})` : ''}; not independently validated for this result`;
10
+ }
9
11
  const fail = (error) => ({ ok: false, error });
10
12
  function upstreamMessage(err) {
11
13
  if (err instanceof UpstreamError) {
@@ -29,14 +31,13 @@ async function vintage(client) {
29
31
  const res = await client.getText(`${client.nyrezoneBase}/methodology`, 'text/html');
30
32
  if (res.status === 200) {
31
33
  const v = parseVintage(res.body);
32
- if (v.release)
33
- return v.checkedOn ? `NYC ${v.release} (checked ${v.checkedOn})` : `NYC ${v.release}`;
34
+ return reportedVintage(v.release, v.checkedOn);
34
35
  }
35
36
  }
36
37
  catch {
37
- // The vintage line is a nicety; never fail a lookup over it.
38
+ // Preserve the lookup, but explicitly report an unknown release when the source is unavailable.
38
39
  }
39
- return FALLBACK_VINTAGE;
40
+ return reportedVintage(null, null);
40
41
  }
41
42
  const districtUrl = (client, slug) => `${client.nyrezoneBase}/zoning/${slug}`;
42
43
  // ---------- districts ----------
@@ -90,10 +91,10 @@ export async function getDistrict(client, rawCode) {
90
91
  return fail(`NYRezone has no page for ${entry.code} right now.`);
91
92
  const page = parseDistrictPage(res.body);
92
93
  const rulesNote = page.rules.length === 0
93
- ? 'NYRezone publishes parcel statistics but no verified bulk/height/setback rules for this district; consult the Zoning Resolution.'
94
+ ? 'NYRezone publishes parcel statistics but no bulk/height/setback rules table for this district; consult the Zoning Resolution.'
94
95
  : null;
95
- const vint = page.dataVintage ? `NYC ${page.dataVintage}${page.dataCheckedOn ? ` (checked ${page.dataCheckedOn})` : ''}` : FALLBACK_VINTAGE;
96
- const source = `Source: ${vint} via NYRezone; rule values from the NYC Zoning Resolution as shown on ${url}. ${DISCLAIMER}`;
96
+ const vint = reportedVintage(page.dataVintage, page.dataCheckedOn);
97
+ const source = `Source: NYRezone HTML district summary, ${url}. ${vint}. Rule values are as published by NYRezone, not independently verified against the NYC Zoning Resolution. ${DISCLAIMER}`;
97
98
  const out = [`${page.code} - ${page.summary}`, ''];
98
99
  if (page.rules.length) {
99
100
  out.push('Rules (as published by NYRezone):');
@@ -174,7 +175,7 @@ export async function lookupLot(client, q) {
174
175
  }
175
176
  const slugs = lot.zoning_districts.map((z) => z.toLowerCase().replace(/\//g, '-')).filter((s) => known === null || known.has(s));
176
177
  const pages = slugs.map((s) => districtUrl(client, s));
177
- const source = `Source: ${vint} via ${client.nyrezoneBase}. District pages: ${pages.join(', ') || 'n/a'}. Full report: ${reportUrl}. ${DISCLAIMER}`;
178
+ const source = `Source: NYRezone /api/pluto parcel records via ${client.nyrezoneBase}. ${vint}. District pages: ${pages.join(', ') || 'n/a'}. Public lot map: ${reportUrl}. ${DISCLAIMER}`;
178
179
  const u = lot.unused_floor_area;
179
180
  const lines = [
180
181
  `BBL ${lot.bbl}${lot.address ? ` - ${lot.address}` : ''}${lot.borough ? `, ${lot.borough}` : ''}${lot.zip ? ` ${lot.zip}` : ''}`,
@@ -183,12 +184,8 @@ export async function lookupLot(client, q) {
183
184
  lot.historic_district || lot.landmark ? `Historic: ${[lot.historic_district, lot.landmark].filter(Boolean).join('; ')}` : '',
184
185
  `Land use: ${lot.land_use ? `${lot.land_use.code} ${lot.land_use.label}` : 'unknown'}${lot.building_class ? `; building class ${lot.building_class}` : ''}`,
185
186
  `Lot area: ${fmt(lot.lot_area_sqft, ' sq ft')}; building area: ${fmt(lot.building_area_sqft, ' sq ft')}; floors: ${fmt(lot.num_floors)}; year built: ${lot.year_built ?? 'unknown'}; residential units: ${fmt(lot.residential_units)}`,
186
- `FAR - built: ${fmt(lot.built_far)}; max residential: ${fmt(lot.max_far.residential)}; max commercial: ${fmt(lot.max_far.commercial)}; max community facility: ${fmt(lot.max_far.community_facility)}`,
187
- u.estimate_sqft === null
188
- ? `Unused floor area: not estimable. ${u.basis}`
189
- : u.overbuilt
190
- ? `Unused floor area: about 0 sq ft; existing gross building area (${fmt(lot.building_area_sqft)} sq ft) exceeds the highest FAR MapPLUTO lists (${u.governing_far} x lot area = ${fmt(u.allowed_floor_area_sqft)} sq ft). MapPLUTO building area is gross, not zoning floor area, so this is not evidence of non-compliance. ${u.basis}`
191
- : `Unused floor area (estimate): about ${fmt(u.estimate_sqft, ' sq ft')} of ${fmt(u.allowed_floor_area_sqft)} sq ft allowed. ${u.basis}`,
187
+ `Recorded MapPLUTO FAR - built: ${fmt(lot.built_far)}; residential: ${fmt(lot.max_far.residential)}; commercial: ${fmt(lot.max_far.commercial)}; community facility: ${fmt(lot.max_far.community_facility)}`,
188
+ `Unused floor area / development capacity: ${u.basis}.`,
192
189
  `Caveat: ${u.caveats.join(' ')}`,
193
190
  '',
194
191
  source,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nyrezone-mcp",
3
- "version": "0.1.0",
4
- "description": "NYRezone MCP - NYC zoning districts and tax-lot lookups (zoning, FAR, unused floor area) from public NYRezone / MapPLUTO data",
3
+ "version": "0.2.0",
4
+ "description": "NYRezone MCP - recorded NYC parcel and FAR fields plus NYRezone HTML district summaries",
5
5
  "type": "module",
6
6
  "bin": { "nyrezone-mcp": "./dist/cli.js" },
7
7
  "main": "./dist/server.js",
@@ -15,11 +15,11 @@
15
15
  },
16
16
  "engines": { "node": ">=20" },
17
17
  "dependencies": {
18
- "@modelcontextprotocol/server": "2.0.0",
19
- "zod": "4.6.5"
18
+ "@modelcontextprotocol/server": "2.4.0",
19
+ "zod": "4.6.7"
20
20
  },
21
21
  "devDependencies": {
22
- "@modelcontextprotocol/client": "2.0.0",
22
+ "@modelcontextprotocol/client": "2.4.0",
23
23
  "@types/node": "22.20.3",
24
24
  "typescript": "7.0.2"
25
25
  },