@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.
@@ -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";
@@ -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 district names (Mokotów, Wola, Śródmieście) - \"Warszawa\" returns 0 results",
24
- "- Kraków: Kraków-Śródmieście, Kraków-Podgórze, Kraków-Krowodrza, Kraków-Nowa Huta",
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
- createServer(async (req, res) => {
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
  });
@@ -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', 'Śródmieście'). For Warsaw, use district names (Mokotów, Wola, etc.) - 'Warszawa' won't match. Use list_locations to find valid names."),
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
- For Warsaw: use district names (Mokotów, Wola) - 'Warszawa' won't match any results.`, {
96
- location: z.string().optional().describe("Filter by location name (case-insensitive partial match). E.g. 'Kraków' matches 'Kraków-Podgórze', 'Kraków-Śródmieście', etc. Omit for all Poland."),
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
- rows = rows.filter((r) => filterByLocation(params.location, [r.district]).length > 0);
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",
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
- "prepublishOnly": "npm run build"
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
- "vitest": "^3.0.4"
31
+ "typescript-eslint": "^8.11.0",
32
+ "vitest": "^4.1.4"
27
33
  },
28
34
  "mcpName": "pl.cenogram/mcp-server",
29
- "keywords": ["mcp", "real-estate", "poland", "rcn", "nieruchomosci", "property", "transactions"],
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"