@matchuplabs/nyc-api-mcp 0.1.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/dist/index.d.ts +2 -0
- package/dist/index.js +127 -0
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +10 -0
- package/dist/lib/errors.d.ts +31 -0
- package/dist/lib/errors.js +65 -0
- package/dist/lib/nyc-api-client.d.ts +75 -0
- package/dist/lib/nyc-api-client.js +109 -0
- package/dist/lib/usage.d.ts +11 -0
- package/dist/lib/usage.js +7 -0
- package/dist/resources/capability-guide.d.ts +4 -0
- package/dist/resources/capability-guide.js +50 -0
- package/dist/resources/coverage-notes.d.ts +4 -0
- package/dist/resources/coverage-notes.js +69 -0
- package/dist/resources/credit-policy.d.ts +4 -0
- package/dist/resources/credit-policy.js +40 -0
- package/dist/resources/input-formatting.d.ts +4 -0
- package/dist/resources/input-formatting.js +66 -0
- package/dist/resources/schema-examples.d.ts +4 -0
- package/dist/resources/schema-examples.js +121 -0
- package/dist/tools/get-building-violations.d.ts +38 -0
- package/dist/tools/get-building-violations.js +160 -0
- package/dist/tools/get-property-intelligence.d.ts +43 -0
- package/dist/tools/get-property-intelligence.js +213 -0
- package/dist/tools/get-restaurant-venue-intel.d.ts +38 -0
- package/dist/tools/get-restaurant-venue-intel.js +190 -0
- package/dist/tools/resolve-property-identifier.d.ts +27 -0
- package/dist/tools/resolve-property-identifier.js +256 -0
- package/package.json +32 -0
- package/server.json +12 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const RESOURCE_URI = "nyc-api://input-formatting";
|
|
2
|
+
export const RESOURCE_NAME = "NYC API Input Formatting Guide";
|
|
3
|
+
export const RESOURCE_DESCRIPTION = "Explains NYC address formats, BBL/BIN identifiers, and borough values accepted by the tools.";
|
|
4
|
+
export const RESOURCE_CONTENT = `# NYC API MCP — Input Formatting Guide
|
|
5
|
+
|
|
6
|
+
## Address Format
|
|
7
|
+
NYC addresses follow the pattern: HOUSE_NUMBER STREET_NAME
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
- 100 BROADWAY
|
|
11
|
+
- 350 FIFTH AVE
|
|
12
|
+
- 1 CENTRE ST
|
|
13
|
+
- 233 BROADWAY
|
|
14
|
+
- 42-10 QUEENS BLVD (hyphenated house numbers are common in Queens)
|
|
15
|
+
|
|
16
|
+
Tips:
|
|
17
|
+
- Street suffixes can be abbreviated (AVE, ST, BLVD, PL, DR) or spelled out.
|
|
18
|
+
- Include the borough if the street name is not unique (e.g., "100 BROADWAY, MANHATTAN").
|
|
19
|
+
- The resolver handles common misspellings and abbreviations.
|
|
20
|
+
|
|
21
|
+
## BBL Format (Borough-Block-Lot)
|
|
22
|
+
A 10-digit string that uniquely identifies a tax lot in NYC.
|
|
23
|
+
|
|
24
|
+
Structure: B BBBBB LLLL
|
|
25
|
+
- B (1 digit): Borough number
|
|
26
|
+
- BBBBB (5 digits): Block number (zero-padded)
|
|
27
|
+
- LLLL (4 digits): Lot number (zero-padded)
|
|
28
|
+
|
|
29
|
+
Borough numbers:
|
|
30
|
+
- 1 = Manhattan
|
|
31
|
+
- 2 = Bronx
|
|
32
|
+
- 3 = Brooklyn
|
|
33
|
+
- 4 = Queens
|
|
34
|
+
- 5 = Staten Island
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
- 1000670001 → Manhattan, Block 00067, Lot 0001
|
|
38
|
+
- 3012340056 → Brooklyn, Block 01234, Lot 0056
|
|
39
|
+
- 4004561234 → Queens, Block 00456, Lot 1234
|
|
40
|
+
|
|
41
|
+
## BIN Format (Building Identification Number)
|
|
42
|
+
A 7-digit number that uniquely identifies a building in NYC.
|
|
43
|
+
|
|
44
|
+
Structure: B NNNNNN
|
|
45
|
+
- First digit is the borough number (same as BBL).
|
|
46
|
+
- Remaining 6 digits identify the specific building.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
- 1001389 → a building in Manhattan
|
|
50
|
+
- 3345678 → a building in Brooklyn
|
|
51
|
+
|
|
52
|
+
Note: A single BBL (tax lot) can have multiple BINs (buildings), but each BIN belongs to exactly one BBL.
|
|
53
|
+
|
|
54
|
+
## Borough Values
|
|
55
|
+
The tools accept borough names or abbreviations:
|
|
56
|
+
|
|
57
|
+
| Full name | Abbreviation | Borough number |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Manhattan | MN | 1 |
|
|
60
|
+
| Bronx | BX | 2 |
|
|
61
|
+
| Brooklyn | BK | 3 |
|
|
62
|
+
| Queens | QN | 4 |
|
|
63
|
+
| Staten Island | SI | 5 |
|
|
64
|
+
|
|
65
|
+
Both full names and abbreviations are case-insensitive.
|
|
66
|
+
`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const RESOURCE_URI = "nyc-api://schema-examples";
|
|
2
|
+
export declare const RESOURCE_NAME = "NYC API Schema & Examples";
|
|
3
|
+
export declare const RESOURCE_DESCRIPTION = "JSON input/output examples for each tool, including both success and error responses.";
|
|
4
|
+
export declare const RESOURCE_CONTENT: string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const RESOURCE_URI = "nyc-api://schema-examples";
|
|
2
|
+
export const RESOURCE_NAME = "NYC API Schema & Examples";
|
|
3
|
+
export const RESOURCE_DESCRIPTION = "JSON input/output examples for each tool, including both success and error responses.";
|
|
4
|
+
export const RESOURCE_CONTENT = JSON.stringify({
|
|
5
|
+
resolve_property_identifier: {
|
|
6
|
+
description: "Normalize and validate a NYC property identifier.",
|
|
7
|
+
example_input: { address: "350 fifth ave" },
|
|
8
|
+
example_success_response: {
|
|
9
|
+
candidates: [
|
|
10
|
+
{
|
|
11
|
+
address: "350 FIFTH AVE",
|
|
12
|
+
borough: "MANHATTAN",
|
|
13
|
+
bbl: "1008350001",
|
|
14
|
+
bin: "1015000",
|
|
15
|
+
confidence: 0.97,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
resolved: true,
|
|
19
|
+
},
|
|
20
|
+
example_error_response: {
|
|
21
|
+
error: {
|
|
22
|
+
code: "NO_MATCH",
|
|
23
|
+
message: "No properties matched the input. Check the address or try a BBL/BIN instead.",
|
|
24
|
+
suggested_next_step: "Verify the address spelling and include the borough.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
get_property_intelligence: {
|
|
29
|
+
description: "Returns ownership, sales, liens, zoning, and rent stabilization for a property.",
|
|
30
|
+
example_input: { bbl: "1008350001", detail: ["ownership", "sales"] },
|
|
31
|
+
example_success_response: {
|
|
32
|
+
property: {
|
|
33
|
+
bbl: "1008350001",
|
|
34
|
+
address: "350 FIFTH AVE",
|
|
35
|
+
borough: "MANHATTAN",
|
|
36
|
+
ownership: {
|
|
37
|
+
owner_name: "EMPIRE STATE BUILDING ASSOCIATES L.L.C.",
|
|
38
|
+
owner_type: "LLC",
|
|
39
|
+
},
|
|
40
|
+
sales: [
|
|
41
|
+
{
|
|
42
|
+
date: "2023-06-15",
|
|
43
|
+
price: 250000000,
|
|
44
|
+
buyer: "EXAMPLE BUYER LLC",
|
|
45
|
+
seller: "EXAMPLE SELLER LLC",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
zoning: { district: "C5-3", overlay: null },
|
|
49
|
+
assessed_value: { total: 1200000000, land: 300000000 },
|
|
50
|
+
rent_stabilized: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
example_error_response: {
|
|
54
|
+
error: {
|
|
55
|
+
code: "INVALID_BBL",
|
|
56
|
+
message: "The BBL provided is not valid. BBL must be a 10-digit string.",
|
|
57
|
+
suggested_next_step: "Use resolve_property_identifier to get a valid BBL first.",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
get_building_violations: {
|
|
62
|
+
description: "Returns DOB/HPD violations, ECB penalties, complaints, and permits for a building.",
|
|
63
|
+
example_input: { bbl: "1008350001", status: "open" },
|
|
64
|
+
example_success_response: {
|
|
65
|
+
building: {
|
|
66
|
+
bbl: "1008350001",
|
|
67
|
+
bin: "1015000",
|
|
68
|
+
address: "350 FIFTH AVE",
|
|
69
|
+
},
|
|
70
|
+
violations: [
|
|
71
|
+
{
|
|
72
|
+
source: "DOB",
|
|
73
|
+
number: "123456789",
|
|
74
|
+
date: "2024-01-10",
|
|
75
|
+
category: "ELEVATOR",
|
|
76
|
+
description: "FAILURE TO MAINTAIN ELEVATOR",
|
|
77
|
+
status: "OPEN",
|
|
78
|
+
penalty_amount: 5000,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
total_count: 1,
|
|
82
|
+
open_count: 1,
|
|
83
|
+
},
|
|
84
|
+
example_error_response: {
|
|
85
|
+
error: {
|
|
86
|
+
code: "UPSTREAM_TIMEOUT",
|
|
87
|
+
message: "The NYC Open Data API did not respond in time.",
|
|
88
|
+
suggested_next_step: "Retry in a few seconds.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
get_restaurant_venue_intel: {
|
|
93
|
+
description: "Returns health grades, liquor license, permits, and 311 complaints for a venue.",
|
|
94
|
+
example_input: {
|
|
95
|
+
name: "JOE'S PIZZA",
|
|
96
|
+
borough: "MANHATTAN",
|
|
97
|
+
},
|
|
98
|
+
example_success_response: {
|
|
99
|
+
venue: {
|
|
100
|
+
name: "JOE'S PIZZA",
|
|
101
|
+
address: "7 CARMINE ST",
|
|
102
|
+
borough: "MANHATTAN",
|
|
103
|
+
health: {
|
|
104
|
+
grade: "A",
|
|
105
|
+
score: 10,
|
|
106
|
+
inspection_date: "2024-02-15",
|
|
107
|
+
critical_violations: 0,
|
|
108
|
+
},
|
|
109
|
+
liquor_license: null,
|
|
110
|
+
sidewalk_cafe_permit: false,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
example_error_response: {
|
|
114
|
+
error: {
|
|
115
|
+
code: "NO_MATCH",
|
|
116
|
+
message: "No restaurants or venues matched the input. Try adjusting the name or adding a borough.",
|
|
117
|
+
suggested_next_step: "Check spelling and provide the full business name.",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}, null, 2);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const TOOL_NAME = "get_building_violations";
|
|
2
|
+
export declare const TOOL_DEFINITION: {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
address: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
bin: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
borough: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
include_complaints: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
include_resolved_counts: {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export interface GetBuildingViolationsArgs {
|
|
32
|
+
address?: string;
|
|
33
|
+
bin?: string;
|
|
34
|
+
borough?: string;
|
|
35
|
+
include_complaints?: boolean;
|
|
36
|
+
include_resolved_counts?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function getBuildingViolations(args: GetBuildingViolationsArgs): Promise<string>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { NycApiClient } from "../lib/nyc-api-client.js";
|
|
2
|
+
import { mcpError, mapApiErrorToMcp, ErrorCodes } from "../lib/errors.js";
|
|
3
|
+
export const TOOL_NAME = "get_building_violations";
|
|
4
|
+
export const TOOL_DEFINITION = {
|
|
5
|
+
name: TOOL_NAME,
|
|
6
|
+
description: "Get building violation records for a NYC property. Returns DOB violations, HPD violations, and summary counts. Optionally include 311 complaint history and resolved violation counts. Costs 1 credit. Call resolve_property_identifier first. Summary by default.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
address: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Full or normalized NYC street address",
|
|
13
|
+
},
|
|
14
|
+
bin: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Building Identification Number",
|
|
17
|
+
},
|
|
18
|
+
borough: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Borough name or code to disambiguate",
|
|
21
|
+
},
|
|
22
|
+
include_complaints: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
description: "Include 311 complaint history (default false)",
|
|
25
|
+
},
|
|
26
|
+
include_resolved_counts: {
|
|
27
|
+
type: "boolean",
|
|
28
|
+
description: "Include resolved violation counts per agency (default false)",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
function isApiError(result) {
|
|
34
|
+
return (typeof result === "object" &&
|
|
35
|
+
result !== null &&
|
|
36
|
+
"error" in result &&
|
|
37
|
+
typeof result.error === "object");
|
|
38
|
+
}
|
|
39
|
+
function unwrapData(raw) {
|
|
40
|
+
if (!raw || typeof raw !== "object")
|
|
41
|
+
return null;
|
|
42
|
+
const rec = raw;
|
|
43
|
+
if (rec.bin || rec.address || rec.bbl)
|
|
44
|
+
return rec;
|
|
45
|
+
if (rec.data && typeof rec.data === "object" && !Array.isArray(rec.data)) {
|
|
46
|
+
return rec.data;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(rec.data) && rec.data.length > 0) {
|
|
49
|
+
return rec.data[0];
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(rec) && rec.length > 0) {
|
|
52
|
+
return rec[0];
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function str(val) {
|
|
57
|
+
return val != null ? String(val) : "";
|
|
58
|
+
}
|
|
59
|
+
function num(val) {
|
|
60
|
+
if (val == null)
|
|
61
|
+
return null;
|
|
62
|
+
const n = Number(val);
|
|
63
|
+
return Number.isNaN(n) ? null : n;
|
|
64
|
+
}
|
|
65
|
+
function buildSourceCoverage(data) {
|
|
66
|
+
return {
|
|
67
|
+
dob: Boolean(data.dob_violations != null ||
|
|
68
|
+
data.dob_violation_count != null ||
|
|
69
|
+
data.dob_open != null),
|
|
70
|
+
hpd: Boolean(data.hpd_violations != null ||
|
|
71
|
+
data.hpd_violation_count != null ||
|
|
72
|
+
data.hpd_open != null),
|
|
73
|
+
complaints_311: Boolean(data.complaints != null ||
|
|
74
|
+
data.complaint_count != null ||
|
|
75
|
+
data.complaints_311 != null),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export async function getBuildingViolations(args) {
|
|
79
|
+
const address = args.address?.trim() || undefined;
|
|
80
|
+
const bin = args.bin?.trim() || undefined;
|
|
81
|
+
const borough = args.borough?.trim() || undefined;
|
|
82
|
+
if (!address && !bin) {
|
|
83
|
+
return JSON.stringify(mcpError(ErrorCodes.MISSING_REQUIRED_IDENTIFIER, "At least address or bin is required."));
|
|
84
|
+
}
|
|
85
|
+
const client = new NycApiClient();
|
|
86
|
+
const lookupParams = {};
|
|
87
|
+
if (address)
|
|
88
|
+
lookupParams.address = address;
|
|
89
|
+
if (bin)
|
|
90
|
+
lookupParams.bin = bin;
|
|
91
|
+
if (borough)
|
|
92
|
+
lookupParams.borough = borough;
|
|
93
|
+
// violationsProfile requires address — pass address or bin as address fallback
|
|
94
|
+
const profileParams = {
|
|
95
|
+
address: address || bin || "",
|
|
96
|
+
borough,
|
|
97
|
+
};
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = await client.violationsProfile(profileParams);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error("get_building_violations error:", err);
|
|
104
|
+
return JSON.stringify(mcpError(ErrorCodes.UPSTREAM_TIMEOUT, `Unexpected error: ${err instanceof Error ? err.message : String(err)}`));
|
|
105
|
+
}
|
|
106
|
+
if (isApiError(raw)) {
|
|
107
|
+
return JSON.stringify(mapApiErrorToMcp(raw));
|
|
108
|
+
}
|
|
109
|
+
const data = unwrapData(raw);
|
|
110
|
+
if (!data) {
|
|
111
|
+
const notFound = mcpError(ErrorCodes.NOT_FOUND, `No violation data returned for ${bin ? `BIN ${bin}` : `address "${address}"`}.`);
|
|
112
|
+
return JSON.stringify(notFound);
|
|
113
|
+
}
|
|
114
|
+
const sourceCoverage = buildSourceCoverage(data);
|
|
115
|
+
const dobCount = num(data.dob_violations ?? data.dob_violation_count ?? data.dob_count);
|
|
116
|
+
const hpdCount = num(data.hpd_violations ?? data.hpd_violation_count ?? data.hpd_count);
|
|
117
|
+
const totals = {
|
|
118
|
+
dob_count: dobCount,
|
|
119
|
+
hpd_count: hpdCount,
|
|
120
|
+
total: num(data.total_violations ??
|
|
121
|
+
data.violation_count ??
|
|
122
|
+
(dobCount ?? 0) + (hpdCount ?? 0)),
|
|
123
|
+
};
|
|
124
|
+
// Extract open violations array
|
|
125
|
+
const rawOpen = data.open_violations || data.violations || data.open;
|
|
126
|
+
const openViolations = Array.isArray(rawOpen)
|
|
127
|
+
? rawOpen.map((v) => ({
|
|
128
|
+
type: str(v.violation_type || v.type),
|
|
129
|
+
agency: str(v.agency || v.source),
|
|
130
|
+
description: str(v.description || v.violation_description),
|
|
131
|
+
date: str(v.date || v.violation_date || v.issued_date),
|
|
132
|
+
status: str(v.status || v.disposition || "open"),
|
|
133
|
+
...v,
|
|
134
|
+
}))
|
|
135
|
+
: [];
|
|
136
|
+
const profile = {
|
|
137
|
+
normalized_address: str(data.normalized_address || data.address),
|
|
138
|
+
bin: str(data.bin || bin),
|
|
139
|
+
bbl: str(data.bbl),
|
|
140
|
+
borough: str(data.borough),
|
|
141
|
+
totals,
|
|
142
|
+
open_violations: openViolations,
|
|
143
|
+
source_coverage: sourceCoverage,
|
|
144
|
+
};
|
|
145
|
+
if (args.include_complaints) {
|
|
146
|
+
const complaints = data.complaints_311 || data.complaints || data.complaint_history;
|
|
147
|
+
profile.complaints_311 = Array.isArray(complaints) ? complaints : [];
|
|
148
|
+
}
|
|
149
|
+
if (args.include_resolved_counts) {
|
|
150
|
+
const dobResolved = num(data.dob_resolved ?? data.dob_resolved_count);
|
|
151
|
+
const hpdResolved = num(data.hpd_resolved ?? data.hpd_resolved_count);
|
|
152
|
+
profile.resolved_counts = {
|
|
153
|
+
dob: dobResolved,
|
|
154
|
+
hpd: hpdResolved,
|
|
155
|
+
total: num(data.total_resolved ??
|
|
156
|
+
(dobResolved ?? 0) + (hpdResolved ?? 0)),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return JSON.stringify(profile);
|
|
160
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const TOOL_NAME = "get_property_intelligence";
|
|
2
|
+
export declare const TOOL_DEFINITION: {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
address: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
bbl: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
borough: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
include_sales_history: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
include_liens: {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
include_rent_status: {
|
|
29
|
+
type: string;
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export interface GetPropertyIntelligenceArgs {
|
|
36
|
+
address?: string;
|
|
37
|
+
bbl?: string;
|
|
38
|
+
borough?: string;
|
|
39
|
+
include_sales_history?: boolean;
|
|
40
|
+
include_liens?: boolean;
|
|
41
|
+
include_rent_status?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function getPropertyIntelligence(args: GetPropertyIntelligenceArgs): Promise<string>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { NycApiClient } from "../lib/nyc-api-client.js";
|
|
2
|
+
import { mcpError, mapApiErrorToMcp, ErrorCodes } from "../lib/errors.js";
|
|
3
|
+
export const TOOL_NAME = "get_property_intelligence";
|
|
4
|
+
export const TOOL_DEFINITION = {
|
|
5
|
+
name: TOOL_NAME,
|
|
6
|
+
description: "Get a full property intelligence profile for a NYC property. Returns owner, zoning, assessment, and violation counts. Optionally include sales history, liens detail, and rent stabilization status via flags. Costs 1 credit per call. Always call resolve_property_identifier first to validate the address. Summary response by default — set include flags to true for additional detail.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
address: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Full or normalized NYC street address",
|
|
13
|
+
},
|
|
14
|
+
bbl: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Borough-Block-Lot (10 digits)",
|
|
17
|
+
},
|
|
18
|
+
borough: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Borough name or code to disambiguate",
|
|
21
|
+
},
|
|
22
|
+
include_sales_history: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
description: "Include recent sales/transactions (default false)",
|
|
25
|
+
},
|
|
26
|
+
include_liens: {
|
|
27
|
+
type: "boolean",
|
|
28
|
+
description: "Include liens detail (default false)",
|
|
29
|
+
},
|
|
30
|
+
include_rent_status: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
description: "Include rent stabilization status (default false)",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
function isApiError(result) {
|
|
38
|
+
return (typeof result === "object" &&
|
|
39
|
+
result !== null &&
|
|
40
|
+
"error" in result &&
|
|
41
|
+
typeof result.error === "object");
|
|
42
|
+
}
|
|
43
|
+
function unwrapData(raw) {
|
|
44
|
+
if (!raw || typeof raw !== "object")
|
|
45
|
+
return null;
|
|
46
|
+
const rec = raw;
|
|
47
|
+
// Direct record with bbl/address
|
|
48
|
+
if (rec.bbl || rec.address)
|
|
49
|
+
return rec;
|
|
50
|
+
// { data: { ... } } wrapper
|
|
51
|
+
if (rec.data && typeof rec.data === "object" && !Array.isArray(rec.data)) {
|
|
52
|
+
return rec.data;
|
|
53
|
+
}
|
|
54
|
+
// { data: [ ... ] } wrapper — take first element
|
|
55
|
+
if (Array.isArray(rec.data) && rec.data.length > 0) {
|
|
56
|
+
return rec.data[0];
|
|
57
|
+
}
|
|
58
|
+
// Array at top level — take first element
|
|
59
|
+
if (Array.isArray(rec) && rec.length > 0) {
|
|
60
|
+
return rec[0];
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function str(val) {
|
|
65
|
+
if (val == null)
|
|
66
|
+
return "";
|
|
67
|
+
if (typeof val === "object" && !Array.isArray(val)) {
|
|
68
|
+
// Handle nested objects like address: { full, borough, zipcode }
|
|
69
|
+
const obj = val;
|
|
70
|
+
return String(obj.full || obj.address || obj.name || JSON.stringify(val));
|
|
71
|
+
}
|
|
72
|
+
return String(val);
|
|
73
|
+
}
|
|
74
|
+
function num(val) {
|
|
75
|
+
if (val == null)
|
|
76
|
+
return null;
|
|
77
|
+
const n = Number(val);
|
|
78
|
+
return Number.isNaN(n) ? null : n;
|
|
79
|
+
}
|
|
80
|
+
function computeConfidence(input) {
|
|
81
|
+
if (input.bbl)
|
|
82
|
+
return "high";
|
|
83
|
+
if (input.address)
|
|
84
|
+
return "medium";
|
|
85
|
+
return "low";
|
|
86
|
+
}
|
|
87
|
+
function buildSourceCoverage(data) {
|
|
88
|
+
return {
|
|
89
|
+
property_lookup: true,
|
|
90
|
+
owner: Boolean(data.owner || data.owner_name),
|
|
91
|
+
zoning: Boolean(data.zoning || data.zoning_district),
|
|
92
|
+
assessment: Boolean(data.assessed_total || data.assessed_land || data.market_value),
|
|
93
|
+
building_info: Boolean(data.building_class || data.year_built || data.num_floors),
|
|
94
|
+
violations: Boolean(data.violation_count != null ||
|
|
95
|
+
data.dob_violations != null ||
|
|
96
|
+
data.hpd_violations != null),
|
|
97
|
+
sales_history: Boolean(data.sales || data.recent_transactions),
|
|
98
|
+
liens: Boolean(data.liens || data.lien_count != null),
|
|
99
|
+
rent_stabilization: Boolean(data.rent_stabilized != null || data.rent_status),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export async function getPropertyIntelligence(args) {
|
|
103
|
+
const address = args.address?.trim() || undefined;
|
|
104
|
+
const bbl = args.bbl?.trim() || undefined;
|
|
105
|
+
const borough = args.borough?.trim() || undefined;
|
|
106
|
+
if (!address && !bbl) {
|
|
107
|
+
return JSON.stringify(mcpError(ErrorCodes.MISSING_REQUIRED_IDENTIFIER, "At least address or bbl is required."));
|
|
108
|
+
}
|
|
109
|
+
const client = new NycApiClient();
|
|
110
|
+
const lookupParams = {};
|
|
111
|
+
if (bbl)
|
|
112
|
+
lookupParams.bbl = bbl;
|
|
113
|
+
if (address)
|
|
114
|
+
lookupParams.address = address;
|
|
115
|
+
if (borough)
|
|
116
|
+
lookupParams.borough = borough;
|
|
117
|
+
let raw;
|
|
118
|
+
try {
|
|
119
|
+
raw = await client.propertyLookup(lookupParams);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.error("get_property_intelligence error:", err);
|
|
123
|
+
return JSON.stringify(mcpError(ErrorCodes.UPSTREAM_TIMEOUT, `Unexpected error: ${err instanceof Error ? err.message : String(err)}`));
|
|
124
|
+
}
|
|
125
|
+
if (isApiError(raw)) {
|
|
126
|
+
return JSON.stringify(mapApiErrorToMcp(raw));
|
|
127
|
+
}
|
|
128
|
+
const data = unwrapData(raw);
|
|
129
|
+
if (!data) {
|
|
130
|
+
const notFound = mcpError(ErrorCodes.NOT_FOUND, `No property data returned for ${bbl ? `BBL ${bbl}` : `address "${address}"`}.`);
|
|
131
|
+
return JSON.stringify(notFound);
|
|
132
|
+
}
|
|
133
|
+
const sourceCoverage = buildSourceCoverage(data);
|
|
134
|
+
// Extract address and borough from potentially nested address object
|
|
135
|
+
let normalizedAddress = "";
|
|
136
|
+
let resolvedBorough = "";
|
|
137
|
+
const addrField = data.address;
|
|
138
|
+
if (addrField && typeof addrField === "object" && !Array.isArray(addrField)) {
|
|
139
|
+
const addrObj = addrField;
|
|
140
|
+
normalizedAddress = String(addrObj.full || addrObj.address || "");
|
|
141
|
+
resolvedBorough = String(addrObj.borough || data.borough || "");
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
normalizedAddress = str(data.normalized_address || data.address);
|
|
145
|
+
resolvedBorough = str(data.borough);
|
|
146
|
+
}
|
|
147
|
+
const profile = {
|
|
148
|
+
normalized_address: normalizedAddress,
|
|
149
|
+
bbl: String(data.bbl || "").replace(/\.0+$/, ""),
|
|
150
|
+
bin: str(data.bin),
|
|
151
|
+
borough: resolvedBorough,
|
|
152
|
+
confidence: computeConfidence({ address, bbl }),
|
|
153
|
+
owner: {
|
|
154
|
+
name: str(data.owner || data.owner_name),
|
|
155
|
+
type: str(data.owner_type || ""),
|
|
156
|
+
},
|
|
157
|
+
zoning: {
|
|
158
|
+
district: str(data.zoning || data.zoning_district),
|
|
159
|
+
land_use: str(data.land_use || ""),
|
|
160
|
+
},
|
|
161
|
+
assessment: {
|
|
162
|
+
assessed_total: num(data.assessed_total),
|
|
163
|
+
assessed_land: num(data.assessed_land),
|
|
164
|
+
market_value: num(data.market_value),
|
|
165
|
+
tax_class: str(data.tax_class),
|
|
166
|
+
},
|
|
167
|
+
building_info: {
|
|
168
|
+
building_class: str(data.building_class),
|
|
169
|
+
year_built: num(data.year_built),
|
|
170
|
+
num_floors: num(data.num_floors),
|
|
171
|
+
num_units: num(data.num_units || data.total_units),
|
|
172
|
+
lot_area_sqft: num(data.lot_area || data.lot_area_sqft),
|
|
173
|
+
},
|
|
174
|
+
violation_counts: {
|
|
175
|
+
dob: num(data.dob_violations ?? data.dob_violation_count),
|
|
176
|
+
hpd: num(data.hpd_violations ?? data.hpd_violation_count),
|
|
177
|
+
ecb: num(data.ecb_violations ?? data.ecb_violation_count),
|
|
178
|
+
total: num(data.violation_count ??
|
|
179
|
+
data.total_violations ??
|
|
180
|
+
(num(data.dob_violations ?? data.dob_violation_count) ?? 0) +
|
|
181
|
+
(num(data.hpd_violations ?? data.hpd_violation_count) ?? 0) +
|
|
182
|
+
(num(data.ecb_violations ?? data.ecb_violation_count) ?? 0)),
|
|
183
|
+
},
|
|
184
|
+
coordinates: {
|
|
185
|
+
latitude: num(data.latitude || data.lat),
|
|
186
|
+
longitude: num(data.longitude || data.lng || data.lon),
|
|
187
|
+
},
|
|
188
|
+
source_coverage: sourceCoverage,
|
|
189
|
+
};
|
|
190
|
+
if (args.include_sales_history) {
|
|
191
|
+
const sales = data.sales || data.recent_transactions;
|
|
192
|
+
profile.recent_transactions = Array.isArray(sales) ? sales : [];
|
|
193
|
+
}
|
|
194
|
+
if (args.include_liens) {
|
|
195
|
+
const liens = data.liens;
|
|
196
|
+
const liensArray = Array.isArray(liens) ? liens : [];
|
|
197
|
+
const activeCount = num(data.active_lien_count ?? data.lien_count) ?? liensArray.length;
|
|
198
|
+
profile.liens = {
|
|
199
|
+
active_count: activeCount,
|
|
200
|
+
has_liens: activeCount > 0,
|
|
201
|
+
details: liensArray,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (args.include_rent_status) {
|
|
205
|
+
const isStabilized = data.rent_stabilized != null ? Boolean(data.rent_stabilized) : null;
|
|
206
|
+
profile.rent_stabilization = {
|
|
207
|
+
is_stabilized: isStabilized,
|
|
208
|
+
unit_count: num(data.rent_stabilized_units ?? data.stabilized_unit_count),
|
|
209
|
+
status: str(data.rent_status || (isStabilized === true ? "stabilized" : isStabilized === false ? "not_stabilized" : "unknown")),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return JSON.stringify(profile);
|
|
213
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const TOOL_NAME = "get_restaurant_venue_intel";
|
|
2
|
+
export declare const TOOL_DEFINITION: {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
name: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
address: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
borough: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
include_liquor_license: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
include_sidewalk_cafe: {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export interface GetRestaurantVenueIntelArgs {
|
|
32
|
+
name?: string;
|
|
33
|
+
address?: string;
|
|
34
|
+
borough?: string;
|
|
35
|
+
include_liquor_license?: boolean;
|
|
36
|
+
include_sidewalk_cafe?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function getRestaurantVenueIntel(args: GetRestaurantVenueIntelArgs): Promise<string>;
|