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 +44 -1
- package/README.md +45 -7
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/lot.d.ts +5 -11
- package/dist/lot.js +13 -49
- package/dist/server.js +9 -9
- package/dist/service.js +12 -15
- package/package.json +5 -5
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
|
|
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
|
|
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) |
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
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.
|
|
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
|
-
/**
|
|
40
|
-
estimate_sqft:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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:
|
|
107
|
-
overbuilt,
|
|
108
|
-
allowed_floor_area_sqft:
|
|
109
|
-
governing_far:
|
|
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:
|
|
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 (
|
|
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
|
-
'
|
|
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.
|
|
43
|
-
overbuilt: z.
|
|
44
|
-
allowed_floor_area_sqft: z.
|
|
45
|
-
governing_far: z.
|
|
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),
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
38
|
+
// Preserve the lookup, but explicitly report an unknown release when the source is unavailable.
|
|
38
39
|
}
|
|
39
|
-
return
|
|
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
|
|
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
|
|
96
|
-
const source = `Source: ${vint}
|
|
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:
|
|
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)};
|
|
187
|
-
u.
|
|
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.
|
|
4
|
-
"description": "NYRezone MCP - NYC
|
|
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.
|
|
19
|
-
"zod": "4.6.
|
|
18
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
19
|
+
"zod": "4.6.7"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@modelcontextprotocol/client": "2.
|
|
22
|
+
"@modelcontextprotocol/client": "2.4.0",
|
|
23
23
|
"@types/node": "22.20.3",
|
|
24
24
|
"typescript": "7.0.2"
|
|
25
25
|
},
|