@cenogram/mcp-server 0.1.1 → 0.1.3

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.
@@ -103,6 +103,9 @@ export interface TransactionParams {
103
103
  parcelId?: string;
104
104
  propertyType?: number;
105
105
  marketType?: number;
106
+ unitFunction?: number;
107
+ buildingType?: number;
108
+ mpzpDesignation?: string;
106
109
  minPrice?: number;
107
110
  maxPrice?: number;
108
111
  dateFrom?: string;
@@ -138,6 +141,9 @@ export interface SpatialSearchParams {
138
141
  };
139
142
  propertyType?: number;
140
143
  marketType?: number;
144
+ unitFunction?: number;
145
+ buildingType?: number;
146
+ mpzpDesignation?: string;
141
147
  minPrice?: number;
142
148
  maxPrice?: number;
143
149
  dateFrom?: string;
@@ -192,6 +198,9 @@ export interface CompareParams {
192
198
  districts: string;
193
199
  propertyType?: number;
194
200
  marketType?: number;
201
+ unitFunction?: number;
202
+ buildingType?: number;
203
+ mpzpDesignation?: string;
195
204
  minPrice?: number;
196
205
  maxPrice?: number;
197
206
  dateFrom?: string;
@@ -95,6 +95,9 @@ export function getTransactions(p, apiKey) {
95
95
  parcelId: p.parcelId,
96
96
  propertyType: p.propertyType,
97
97
  marketType: p.marketType,
98
+ unitFunction: p.unitFunction,
99
+ buildingType: p.buildingType,
100
+ mpzpDesignation: p.mpzpDesignation,
98
101
  minPrice: p.minPrice,
99
102
  maxPrice: p.maxPrice,
100
103
  dateFrom: p.dateFrom,
@@ -114,6 +117,9 @@ export function getTransactionsSummary(p, apiKey) {
114
117
  street: p.street,
115
118
  propertyType: p.propertyType,
116
119
  marketType: p.marketType,
120
+ unitFunction: p.unitFunction,
121
+ buildingType: p.buildingType,
122
+ mpzpDesignation: p.mpzpDesignation,
117
123
  minPrice: p.minPrice,
118
124
  maxPrice: p.maxPrice,
119
125
  dateFrom: p.dateFrom,
@@ -141,6 +147,12 @@ export function searchByPolygon(p, apiKey) {
141
147
  body.propertyType = p.propertyType;
142
148
  if (p.marketType != null)
143
149
  body.marketType = p.marketType;
150
+ if (p.unitFunction != null)
151
+ body.unitFunction = p.unitFunction;
152
+ if (p.buildingType != null)
153
+ body.buildingType = p.buildingType;
154
+ if (p.mpzpDesignation)
155
+ body.mpzpDesignation = p.mpzpDesignation;
144
156
  if (p.minPrice != null)
145
157
  body.minPrice = p.minPrice;
146
158
  if (p.maxPrice != null)
@@ -166,6 +178,9 @@ export function compareLocations(p, apiKey) {
166
178
  districts: p.districts,
167
179
  propertyType: p.propertyType,
168
180
  marketType: p.marketType,
181
+ unitFunction: p.unitFunction,
182
+ buildingType: p.buildingType,
183
+ mpzpDesignation: p.mpzpDesignation,
169
184
  minPrice: p.minPrice,
170
185
  maxPrice: p.maxPrice,
171
186
  dateFrom: p.dateFrom,
package/dist/index.js CHANGED
@@ -55,10 +55,6 @@ async function main() {
55
55
  if (pathname === "/mcp") {
56
56
  const auth = req.headers.authorization;
57
57
  const apiKeyFromHeader = auth?.startsWith("Bearer ") ? auth.slice(7).trim() : undefined;
58
- if (!apiKeyFromHeader) {
59
- res.writeHead(401, { "Content-Type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Authorization: Bearer <api-key> required" }, id: null }));
60
- return;
61
- }
62
58
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
63
59
  const mcpServer = createMcpServer(apiKeyFromHeader);
64
60
  try {
@@ -2,6 +2,10 @@ export declare const PROPERTY_TYPES: Record<number, string>;
2
2
  export declare const MARKET_TYPES: Record<number, string>;
3
3
  export declare function mapPropertyType(value: string | undefined): number | undefined;
4
4
  export declare function mapMarketType(value: string | undefined): number | undefined;
5
+ export declare const UNIT_FUNCTIONS: Record<number, string>;
6
+ export declare function mapUnitFunction(value: string | undefined): number | undefined;
7
+ export declare const BUILDING_TYPES: Record<number, string>;
8
+ export declare function mapBuildingType(value: string | undefined): number | undefined;
5
9
  /** Convert lat/lng/radius to bbox [minLng, minLat, maxLng, maxLat] (lng-first!) */
6
10
  export declare function radiusKmToBbox(lat: number, lng: number, radiusKm: number): [number, number, number, number];
7
11
  /** Filter districts by location name (case-insensitive includes match) */
package/dist/mappings.js CHANGED
@@ -29,6 +29,58 @@ export function mapMarketType(value) {
29
29
  return undefined;
30
30
  return MARKET_TYPE_MAP[value];
31
31
  }
32
+ // ── Unit function enum maps ─────────────────────────────────────────
33
+ export const UNIT_FUNCTIONS = {
34
+ 1: "Residential (Mieszkalna)",
35
+ 2: "Commercial (Handlowo-usługowa)",
36
+ 3: "Office (Biurowa)",
37
+ 4: "Production (Produkcyjna)",
38
+ 5: "Garage (Garaż)",
39
+ 6: "Other (Inne)",
40
+ };
41
+ const UNIT_FUNCTION_MAP = {
42
+ residential: 1,
43
+ commercial: 2,
44
+ office: 3,
45
+ production: 4,
46
+ garage: 5,
47
+ other: 6,
48
+ };
49
+ export function mapUnitFunction(value) {
50
+ if (!value)
51
+ return undefined;
52
+ return UNIT_FUNCTION_MAP[value];
53
+ }
54
+ // ── Building type enum maps ─────────────────────────────────────────
55
+ export const BUILDING_TYPES = {
56
+ 110: "Residential (Mieszkalny)",
57
+ 121: "Commercial (Handlowo-usługowy)",
58
+ 122: "Industrial (Przemysłowy)",
59
+ 123: "Transport (Transportu i łączności)",
60
+ 124: "Office (Biurowy)",
61
+ 125: "Warehouse (Zbiorniki/Silosy/Magazyny)",
62
+ 126: "Education/Sports (Oświaty i sportu)",
63
+ 127: "Farm/Utility (Gospodarczy)",
64
+ 128: "Hospital (Szpitale)",
65
+ 129: "Other non-residential (Pozostałe niemieszkalne)",
66
+ };
67
+ const BUILDING_TYPE_MAP = {
68
+ residential: 110,
69
+ commercial: 121,
70
+ industrial: 122,
71
+ transport: 123,
72
+ office: 124,
73
+ warehouse: 125,
74
+ education_sports: 126,
75
+ farm_utility: 127,
76
+ hospital: 128,
77
+ other_nonresidential: 129,
78
+ };
79
+ export function mapBuildingType(value) {
80
+ if (!value)
81
+ return undefined;
82
+ return BUILDING_TYPE_MAP[value];
83
+ }
32
84
  // ── Bbox conversion ─────────────────────────────────────────────────
33
85
  /** Convert lat/lng/radius to bbox [minLng, minLat, maxLng, maxLat] (lng-first!) */
34
86
  export function radiusKmToBbox(lat, lng, radiusKm) {
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, radiusKmToBbox, filterByLocation, } from "./mappings.js";
4
+ import { mapPropertyType, mapMarketType, mapUnitFunction, mapBuildingType, radiusKmToBbox, filterByLocation, } from "./mappings.js";
5
5
  // ── Helpers ─────────────────────────────────────────────────────────
6
6
  function textResponse(text) {
7
7
  return { content: [{ type: "text", text }] };
@@ -11,6 +11,11 @@ function formatCreditFooter(creditInfo) {
11
11
  return "";
12
12
  return `\n---\nTokeny API: ${creditInfo.balance} pozostało (koszt zapytania: ${creditInfo.cost})`;
13
13
  }
14
+ function requireApiKey(apiKey) {
15
+ if (!apiKey) {
16
+ throw new Error("Authorization: Bearer <api-key> required. Get your free API key at https://cenogram.pl/api");
17
+ }
18
+ }
14
19
  async function withErrorHandling(fn) {
15
20
  try {
16
21
  return await fn();
@@ -32,6 +37,12 @@ Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
32
37
  .describe("Property type filter"),
33
38
  marketType: z.enum(["primary", "secondary"]).optional()
34
39
  .describe("Market type: primary (developer) or secondary (resale)"),
40
+ unitFunction: z.enum(["residential", "commercial", "office", "production", "garage", "other"]).optional()
41
+ .describe("Unit/apartment function filter"),
42
+ buildingType: z.enum(["residential", "commercial", "industrial", "transport", "office", "warehouse", "education_sports", "farm_utility", "hospital", "other_nonresidential"]).optional()
43
+ .describe("Building type filter (PKOB classification)"),
44
+ mpzpDesignation: z.string().optional()
45
+ .describe("MPZP zoning designation filter (exact match, e.g. 'budownictwoMieszkanioweWielorodzinne', 'terenObiektowProdukcyjnychSkladowIMagazynow')"),
35
46
  minPrice: z.number().optional().describe("Minimum price in PLN"),
36
47
  maxPrice: z.number().optional().describe("Maximum price in PLN"),
37
48
  dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"),
@@ -50,10 +61,14 @@ Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
50
61
  page: z.number().min(1).default(1).optional()
51
62
  .describe("Page number for pagination (default: 1)"),
52
63
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
64
+ requireApiKey(apiKey);
53
65
  const txParams = {
54
66
  district: params.location,
55
67
  propertyType: mapPropertyType(params.propertyType),
56
68
  marketType: mapMarketType(params.marketType),
69
+ unitFunction: mapUnitFunction(params.unitFunction),
70
+ buildingType: mapBuildingType(params.buildingType),
71
+ mpzpDesignation: params.mpzpDesignation,
57
72
  minPrice: params.minPrice,
58
73
  maxPrice: params.maxPrice,
59
74
  dateFrom: params.dateFrom,
@@ -80,6 +95,7 @@ Note: only covers residential units (lokale mieszkalne). For other property type
80
95
  For Warsaw: use district names (Mokotów, Wola) - 'Warszawa' won't match any results.`, {
81
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."),
82
97
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
98
+ requireApiKey(apiKey);
83
99
  const { data: allRows, creditInfo } = await getPricePerM2(apiKey);
84
100
  let rows = allRows;
85
101
  if (params.location) {
@@ -95,6 +111,7 @@ Useful for understanding the overall market price structure in Poland.`, {
95
111
  maxPrice: z.number().default(3_000_000)
96
112
  .describe("Maximum price to include (default 3,000,000 PLN)"),
97
113
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
114
+ requireApiKey(apiKey);
98
115
  const { data: bins, creditInfo } = await getPriceHistogram(params.bins, params.maxPrice, apiKey);
99
116
  return textResponse(formatHistogram(bins) + formatCreditFooter(creditInfo));
100
117
  }));
@@ -112,6 +129,10 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
112
129
  .describe("Property type filter"),
113
130
  marketType: z.enum(["primary", "secondary"]).optional()
114
131
  .describe("Market type filter"),
132
+ unitFunction: z.enum(["residential", "commercial", "office", "production", "garage", "other"]).optional()
133
+ .describe("Unit/apartment function filter"),
134
+ buildingType: z.enum(["residential", "commercial", "industrial", "transport", "office", "warehouse", "education_sports", "farm_utility", "hospital", "other_nonresidential"]).optional()
135
+ .describe("Building type filter (PKOB classification)"),
115
136
  minPrice: z.number().optional().describe("Minimum price in PLN"),
116
137
  maxPrice: z.number().optional().describe("Maximum price in PLN"),
117
138
  dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"),
@@ -119,11 +140,14 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
119
140
  limit: z.number().min(1).max(50).default(20)
120
141
  .describe("Number of results (1-50, default 20)"),
121
142
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
143
+ requireApiKey(apiKey);
122
144
  const bbox = radiusKmToBbox(params.latitude, params.longitude, params.radiusKm);
123
145
  const txParams = {
124
146
  bbox: bbox.join(","),
125
147
  propertyType: mapPropertyType(params.propertyType),
126
148
  marketType: mapMarketType(params.marketType),
149
+ unitFunction: mapUnitFunction(params.unitFunction),
150
+ buildingType: mapBuildingType(params.buildingType),
127
151
  minPrice: params.minPrice,
128
152
  maxPrice: params.maxPrice,
129
153
  dateFrom: params.dateFrom,
@@ -141,6 +165,7 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
141
165
  // ── Tool 5: get_market_overview ─────────────────────────────────────
142
166
  server.tool("get_market_overview", `Get a comprehensive overview of the Polish real estate transaction database.
143
167
  Returns: total transaction count, date range, breakdown by property type and market type, top locations, price statistics.`, {}, { readOnlyHint: true }, async () => withErrorHandling(async () => {
168
+ requireApiKey(apiKey);
144
169
  const { data: stats, creditInfo } = await getStats(apiKey);
145
170
  return textResponse(formatMarketOverview(stats) + formatCreditFooter(creditInfo));
146
171
  }));
@@ -152,6 +177,7 @@ For Kraków: returns sub-districts (Kraków-Podgórze, Kraków-Śródmieście, e
152
177
  Use the search parameter to filter by name.`, {
153
178
  search: z.string().optional().describe("Filter locations by name (case-insensitive partial match, e.g. 'Krak' for Kraków districts)"),
154
179
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
180
+ requireApiKey(apiKey);
155
181
  const { data: allDistricts, creditInfo } = await getDistricts(apiKey);
156
182
  let districts = allDistricts;
157
183
  if (params.search) {
@@ -183,6 +209,7 @@ Example: search for parcels starting with '146518_8.01'.`, {
183
209
  limit: z.number().min(1).max(10).default(10).optional()
184
210
  .describe("Max results (1-10, default 10)"),
185
211
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
212
+ requireApiKey(apiKey);
186
213
  const { data, creditInfo } = await searchParcels(params.q, params.limit, apiKey);
187
214
  return textResponse(formatParcelResults(data, params.q) + formatCreditFooter(creditInfo));
188
215
  }));
@@ -201,6 +228,12 @@ Example: {"type":"Polygon","coordinates":[[[21.0,52.2],[21.01,52.2],[21.01,52.21
201
228
  .describe("Property type filter"),
202
229
  marketType: z.enum(["primary", "secondary"]).optional()
203
230
  .describe("Market type filter"),
231
+ unitFunction: z.enum(["residential", "commercial", "office", "production", "garage", "other"]).optional()
232
+ .describe("Unit/apartment function filter"),
233
+ buildingType: z.enum(["residential", "commercial", "industrial", "transport", "office", "warehouse", "education_sports", "farm_utility", "hospital", "other_nonresidential"]).optional()
234
+ .describe("Building type filter (PKOB classification)"),
235
+ mpzpDesignation: z.string().optional()
236
+ .describe("MPZP zoning designation filter (exact match)"),
204
237
  minPrice: z.number().optional().describe("Minimum price in PLN"),
205
238
  maxPrice: z.number().optional().describe("Maximum price in PLN"),
206
239
  dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"),
@@ -212,10 +245,14 @@ Example: {"type":"Polygon","coordinates":[[[21.0,52.2],[21.01,52.2],[21.01,52.21
212
245
  limit: z.number().min(1).max(5000).default(100).optional()
213
246
  .describe("Max results (1-5000, default 100). MCP displays up to 50 transactions."),
214
247
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
248
+ requireApiKey(apiKey);
215
249
  const { data, creditInfo } = await searchByPolygon({
216
250
  polygon: params.polygon,
217
251
  propertyType: mapPropertyType(params.propertyType),
218
252
  marketType: mapMarketType(params.marketType),
253
+ unitFunction: mapUnitFunction(params.unitFunction),
254
+ buildingType: mapBuildingType(params.buildingType),
255
+ mpzpDesignation: params.mpzpDesignation,
219
256
  minPrice: params.minPrice,
220
257
  maxPrice: params.maxPrice,
221
258
  dateFrom: params.dateFrom,
@@ -239,6 +276,10 @@ Example: compare Mokotów, Wola, Ursynów for apartments.`, {
239
276
  .describe("Property type filter (recommended - API requires at least one filter)"),
240
277
  marketType: z.enum(["primary", "secondary"]).optional()
241
278
  .describe("Market type filter"),
279
+ unitFunction: z.enum(["residential", "commercial", "office", "production", "garage", "other"]).optional()
280
+ .describe("Unit/apartment function filter"),
281
+ buildingType: z.enum(["residential", "commercial", "industrial", "transport", "office", "warehouse", "education_sports", "farm_utility", "hospital", "other_nonresidential"]).optional()
282
+ .describe("Building type filter (PKOB classification)"),
242
283
  minPrice: z.number().optional().describe("Minimum price in PLN"),
243
284
  maxPrice: z.number().optional().describe("Maximum price in PLN"),
244
285
  dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"),
@@ -247,10 +288,13 @@ Example: compare Mokotów, Wola, Ursynów for apartments.`, {
247
288
  maxArea: z.number().optional().describe("Maximum area in m²"),
248
289
  street: z.string().optional().describe("Street name filter"),
249
290
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
291
+ requireApiKey(apiKey);
250
292
  const { data, creditInfo } = await compareLocations({
251
293
  districts: params.districts,
252
294
  propertyType: mapPropertyType(params.propertyType),
253
295
  marketType: mapMarketType(params.marketType),
296
+ unitFunction: mapUnitFunction(params.unitFunction),
297
+ buildingType: mapBuildingType(params.buildingType),
254
298
  minPrice: params.minPrice,
255
299
  maxPrice: params.maxPrice,
256
300
  dateFrom: params.dateFrom,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cenogram/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "MCP Server for Polish real estate transaction data (7M+ transactions from RCN)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "@types/node": "^22.0.0",
24
24
  "tsx": "^4.19.0",
25
25
  "typescript": "^5.6.0",
26
- "vitest": "^3.0.0"
26
+ "vitest": "^3.0.4"
27
27
  },
28
28
  "mcpName": "pl.cenogram/mcp-server",
29
29
  "keywords": ["mcp", "real-estate", "poland", "rcn", "nieruchomosci", "property", "transactions"],