@cenogram/mcp-server 0.1.3 → 0.1.4
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/api-client.d.ts +2 -0
- package/dist/formatters.js +2 -0
- package/dist/index.js +8 -3
- package/dist/mappings.d.ts +3 -0
- package/dist/mappings.js +20 -0
- package/dist/tools.js +22 -6
- package/package.json +19 -5
package/dist/api-client.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export interface Transaction {
|
|
|
49
49
|
parcel_area: number | null;
|
|
50
50
|
unit_function: number | null;
|
|
51
51
|
parcel_id: string | null;
|
|
52
|
+
parcel_number: string | null;
|
|
52
53
|
county_name: string | null;
|
|
53
54
|
voivodeship_name: string | null;
|
|
54
55
|
centroid: {
|
|
@@ -169,6 +170,7 @@ export interface SpatialFeatureProperties {
|
|
|
169
170
|
city: string | null;
|
|
170
171
|
district: string | null;
|
|
171
172
|
parcel_area: number | null;
|
|
173
|
+
parcel_number: string | null;
|
|
172
174
|
}
|
|
173
175
|
export interface SpatialFeature {
|
|
174
176
|
type: "Feature";
|
package/dist/formatters.js
CHANGED
|
@@ -50,6 +50,8 @@ function formatTransactionCore(f) {
|
|
|
50
50
|
parts.push(price.join(" | "));
|
|
51
51
|
// Extra details
|
|
52
52
|
const extra = [];
|
|
53
|
+
if (f.parcel_number)
|
|
54
|
+
extra.push(`Plot no: ${f.parcel_number}`);
|
|
53
55
|
if (f.rooms != null)
|
|
54
56
|
extra.push(`Rooms: ${f.rooms}`);
|
|
55
57
|
if (f.floor != null)
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,8 @@ export function createMcpServer(apiKey) {
|
|
|
20
20
|
"",
|
|
21
21
|
"CRITICAL - District names (ALWAYS verify first):",
|
|
22
22
|
"- NEVER guess district names. Call list_locations(search=\"city\") first.",
|
|
23
|
-
"- Warsaw: use
|
|
24
|
-
"- Kraków
|
|
23
|
+
"- Warsaw: 'Warszawa' auto-includes all 18 districts. Or use specific: Mokotów, Wola, Śródmieście",
|
|
24
|
+
"- Kraków/Łódź: 'Kraków'/'Łódź' auto-include all sub-districts. Or use specific: Kraków-Podgórze, etc.",
|
|
25
25
|
"- Most cities (Gdańsk, Gdynia, Sopot, Poznań): just the city name, no sub-districts",
|
|
26
26
|
"",
|
|
27
27
|
"Workflows:",
|
|
@@ -49,7 +49,7 @@ async function main() {
|
|
|
49
49
|
const { createServer } = await import("node:http");
|
|
50
50
|
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
51
51
|
const port = parseInt(process.env.MCP_PORT || "3002", 10);
|
|
52
|
-
|
|
52
|
+
const handleHttpRequest = async (req, res) => {
|
|
53
53
|
try {
|
|
54
54
|
const pathname = req.url?.split("?")[0];
|
|
55
55
|
if (pathname === "/mcp") {
|
|
@@ -82,6 +82,11 @@ async function main() {
|
|
|
82
82
|
res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
};
|
|
86
|
+
createServer((req, res) => {
|
|
87
|
+
handleHttpRequest(req, res).catch((err) => {
|
|
88
|
+
process.stderr.write(`Unhandled HTTP error: ${String(err)}\n`);
|
|
89
|
+
});
|
|
85
90
|
}).listen(port, "0.0.0.0", () => {
|
|
86
91
|
process.stderr.write(`MCP HTTP server on http://0.0.0.0:${port}/mcp\n`);
|
|
87
92
|
});
|
package/dist/mappings.d.ts
CHANGED
|
@@ -10,3 +10,6 @@ export declare function mapBuildingType(value: string | undefined): number | und
|
|
|
10
10
|
export declare function radiusKmToBbox(lat: number, lng: number, radiusKm: number): [number, number, number, number];
|
|
11
11
|
/** Filter districts by location name (case-insensitive includes match) */
|
|
12
12
|
export declare function filterByLocation(location: string, districts: string[]): string[];
|
|
13
|
+
export declare const CITY_SUBDISTRICTS: ReadonlyMap<string, readonly string[]>;
|
|
14
|
+
/** Returns sub-districts for known multi-district cities, or [district] for everything else. */
|
|
15
|
+
export declare function expandDistrict(district: string): string[];
|
package/dist/mappings.js
CHANGED
|
@@ -99,3 +99,23 @@ export function filterByLocation(location, districts) {
|
|
|
99
99
|
const lower = location.toLowerCase();
|
|
100
100
|
return districts.filter((d) => d.toLowerCase().includes(lower));
|
|
101
101
|
}
|
|
102
|
+
// ── City → sub-district expansion ───────────────────────────────────
|
|
103
|
+
// Keep in sync with api/src/helpers.ts CITY_SUBDISTRICTS (ADR-003).
|
|
104
|
+
// Cities: Warszawa (19), Kraków (5), Łódź (6).
|
|
105
|
+
export const CITY_SUBDISTRICTS = new Map([
|
|
106
|
+
["Warszawa", [
|
|
107
|
+
"Warszawa", "Bemowo", "Białołęka", "Bielany", "Mokotów", "Ochota",
|
|
108
|
+
"Praga-Południe", "Praga-Północ", "Rembertów", "Śródmieście", "Targówek",
|
|
109
|
+
"Ursus", "Ursynów", "Wawer", "Wesoła", "Wilanów", "Włochy", "Wola", "Żoliborz",
|
|
110
|
+
]],
|
|
111
|
+
["Kraków", [
|
|
112
|
+
"Kraków", "Kraków-Krowodrza", "Kraków-Nowa Huta", "Kraków-Podgórze", "Kraków-Śródmieście",
|
|
113
|
+
]],
|
|
114
|
+
["Łódź", [
|
|
115
|
+
"Łódź", "Łódź-Bałuty", "Łódź-Górna", "Łódź-Polesie", "Łódź-Śródmieście", "Łódź-Widzew",
|
|
116
|
+
]],
|
|
117
|
+
]);
|
|
118
|
+
/** Returns sub-districts for known multi-district cities, or [district] for everything else. */
|
|
119
|
+
export function expandDistrict(district) {
|
|
120
|
+
return CITY_SUBDISTRICTS.get(district)?.slice() ?? [district];
|
|
121
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getStats, getTransactions, getPricePerM2, getDistricts, getPriceHistogram, getTransactionsSummary, searchParcels, searchByPolygon, compareLocations, } from "./api-client.js";
|
|
3
3
|
import { formatTransactionList, formatMarketOverview, formatPriceStats, formatHistogram, formatParcelResults, formatSpatialResults, formatCompareResults, } from "./formatters.js";
|
|
4
|
-
import { mapPropertyType, mapMarketType, mapUnitFunction, mapBuildingType, radiusKmToBbox, filterByLocation, } from "./mappings.js";
|
|
4
|
+
import { mapPropertyType, mapMarketType, mapUnitFunction, mapBuildingType, radiusKmToBbox, filterByLocation, expandDistrict, CITY_SUBDISTRICTS, } from "./mappings.js";
|
|
5
5
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
6
6
|
function textResponse(text) {
|
|
7
7
|
return { content: [{ type: "text", text }] };
|
|
@@ -32,7 +32,7 @@ export function registerTools(server, apiKey) {
|
|
|
32
32
|
Returns transaction details: address, date, price, area, price/m², property type.
|
|
33
33
|
Use list_locations first to find valid location names.
|
|
34
34
|
Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
|
|
35
|
-
location: z.string().optional().describe("Location name - city (e.g. 'Kraków', 'Gdańsk') or district (e.g. 'Mokotów', '
|
|
35
|
+
location: z.string().optional().describe("Location name - city (e.g. 'Warszawa', 'Kraków', 'Gdańsk') or district (e.g. 'Mokotów', 'Kraków-Podgórze'). 'Warszawa', 'Kraków', 'Łódź' auto-expand to all sub-districts. Use list_locations to find valid names."),
|
|
36
36
|
propertyType: z.enum(["land", "building", "developed_land", "unit"]).optional()
|
|
37
37
|
.describe("Property type filter"),
|
|
38
38
|
marketType: z.enum(["primary", "secondary"]).optional()
|
|
@@ -92,14 +92,20 @@ Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
|
|
|
92
92
|
// ── Tool 2: get_price_statistics ────────────────────────────────────
|
|
93
93
|
server.tool("get_price_statistics", `Get price per m² statistics by location for residential apartments in Poland.
|
|
94
94
|
Note: only covers residential units (lokale mieszkalne). For other property types, use search_transactions.
|
|
95
|
-
|
|
96
|
-
location: z.string().optional().describe("Filter by location name
|
|
95
|
+
'Warszawa'/'Kraków'/'Łódź' auto-expand to all sub-districts (Warszawa=19, Kraków=5, Łódź=6). Other names use partial match.`, {
|
|
96
|
+
location: z.string().optional().describe("Filter by location name. 'Warszawa'/'Kraków'/'Łódź' auto-expand to all sub-districts. Other names use case-insensitive partial match (e.g. 'Wrocł' matches 'Wrocław'). Omit for all Poland."),
|
|
97
97
|
}, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
|
|
98
98
|
requireApiKey(apiKey);
|
|
99
99
|
const { data: allRows, creditInfo } = await getPricePerM2(apiKey);
|
|
100
100
|
let rows = allRows;
|
|
101
101
|
if (params.location) {
|
|
102
|
-
|
|
102
|
+
if (CITY_SUBDISTRICTS.has(params.location)) {
|
|
103
|
+
const allowed = new Set(expandDistrict(params.location));
|
|
104
|
+
rows = rows.filter((r) => allowed.has(r.district));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
rows = rows.filter((r) => filterByLocation(params.location, [r.district]).length > 0);
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
return textResponse(formatPriceStats(rows, params.location) + formatCreditFooter(creditInfo));
|
|
105
111
|
}));
|
|
@@ -118,7 +124,8 @@ Useful for understanding the overall market price structure in Poland.`, {
|
|
|
118
124
|
// ── Tool 4: search_by_area ──────────────────────────────────────────
|
|
119
125
|
server.tool("search_by_area", `Search real estate transactions within a geographic radius.
|
|
120
126
|
Provide latitude/longitude coordinates and a radius in km.
|
|
121
|
-
Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2317, lng 21.0060)
|
|
127
|
+
Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2317, lng 21.0060).
|
|
128
|
+
Area filters (minArea/maxArea) work for all propertyType values via COALESCE(usable_area_m2, parcel_area).`, {
|
|
122
129
|
latitude: z.number().min(49).max(55)
|
|
123
130
|
.describe("Latitude (Poland range: 49-55)"),
|
|
124
131
|
longitude: z.number().min(14).max(25)
|
|
@@ -135,6 +142,10 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
|
|
|
135
142
|
.describe("Building type filter (PKOB classification)"),
|
|
136
143
|
minPrice: z.number().optional().describe("Minimum price in PLN"),
|
|
137
144
|
maxPrice: z.number().optional().describe("Maximum price in PLN"),
|
|
145
|
+
minArea: z.number().optional()
|
|
146
|
+
.describe("Minimum area in m² (usable_area_m2 for units, parcel_area for land)"),
|
|
147
|
+
maxArea: z.number().optional()
|
|
148
|
+
.describe("Maximum area in m²"),
|
|
138
149
|
dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
139
150
|
dateTo: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
140
151
|
limit: z.number().min(1).max(50).default(20)
|
|
@@ -150,6 +161,8 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
|
|
|
150
161
|
buildingType: mapBuildingType(params.buildingType),
|
|
151
162
|
minPrice: params.minPrice,
|
|
152
163
|
maxPrice: params.maxPrice,
|
|
164
|
+
minArea: params.minArea,
|
|
165
|
+
maxArea: params.maxArea,
|
|
153
166
|
dateFrom: params.dateFrom,
|
|
154
167
|
dateTo: params.dateTo,
|
|
155
168
|
limit: params.limit,
|
|
@@ -280,6 +293,8 @@ Example: compare Mokotów, Wola, Ursynów for apartments.`, {
|
|
|
280
293
|
.describe("Unit/apartment function filter"),
|
|
281
294
|
buildingType: z.enum(["residential", "commercial", "industrial", "transport", "office", "warehouse", "education_sports", "farm_utility", "hospital", "other_nonresidential"]).optional()
|
|
282
295
|
.describe("Building type filter (PKOB classification)"),
|
|
296
|
+
mpzpDesignation: z.string().optional()
|
|
297
|
+
.describe("MPZP zoning designation prefix filter (e.g. 'terenRolniczy', 'budownictwoMieszkanioweJednorodzinne', 'budownictwoMieszkanioweWielorodzinne')"),
|
|
283
298
|
minPrice: z.number().optional().describe("Minimum price in PLN"),
|
|
284
299
|
maxPrice: z.number().optional().describe("Maximum price in PLN"),
|
|
285
300
|
dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
@@ -295,6 +310,7 @@ Example: compare Mokotów, Wola, Ursynów for apartments.`, {
|
|
|
295
310
|
marketType: mapMarketType(params.marketType),
|
|
296
311
|
unitFunction: mapUnitFunction(params.unitFunction),
|
|
297
312
|
buildingType: mapBuildingType(params.buildingType),
|
|
313
|
+
mpzpDesignation: params.mpzpDesignation,
|
|
298
314
|
minPrice: params.minPrice,
|
|
299
315
|
maxPrice: params.maxPrice,
|
|
300
316
|
dateFrom: params.dateFrom,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cenogram/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MCP Server for Polish real estate transaction data (7M+ transactions from RCN)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,23 +10,37 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsc",
|
|
13
|
+
"build": "tsc -p tsconfig.build.json",
|
|
14
14
|
"dev": "tsx src/index.ts",
|
|
15
15
|
"test": "vitest run",
|
|
16
|
-
"
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"check": "tsc --noEmit && eslint .",
|
|
19
|
+
"prepublishOnly": "npm run lint && npm run test && npm run build"
|
|
17
20
|
},
|
|
18
21
|
"dependencies": {
|
|
19
22
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
20
23
|
"zod": "^3.24.0"
|
|
21
24
|
},
|
|
22
25
|
"devDependencies": {
|
|
26
|
+
"@eslint/js": "^9.13.0",
|
|
23
27
|
"@types/node": "^22.0.0",
|
|
28
|
+
"eslint": "^9.13.0",
|
|
24
29
|
"tsx": "^4.19.0",
|
|
25
30
|
"typescript": "^5.6.0",
|
|
26
|
-
"
|
|
31
|
+
"typescript-eslint": "^8.11.0",
|
|
32
|
+
"vitest": "^4.1.4"
|
|
27
33
|
},
|
|
28
34
|
"mcpName": "pl.cenogram/mcp-server",
|
|
29
|
-
"keywords": [
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"real-estate",
|
|
38
|
+
"poland",
|
|
39
|
+
"rcn",
|
|
40
|
+
"nieruchomosci",
|
|
41
|
+
"property",
|
|
42
|
+
"transactions"
|
|
43
|
+
],
|
|
30
44
|
"license": "MIT",
|
|
31
45
|
"publishConfig": {
|
|
32
46
|
"access": "public"
|