@cenogram/mcp-server 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -20,12 +20,12 @@ Manage your keys at [cenogram.pl/ustawienia](https://cenogram.pl/ustawienia).
20
20
 
21
21
  ## Installation
22
22
 
23
- Pick your client. All options below use the hosted server no local install needed (except npx/stdio).
23
+ Pick your client. All options below use the hosted server - no local install needed (except npx/stdio).
24
24
 
25
25
  <details open>
26
26
  <summary><strong>Claude Code</strong></summary>
27
27
 
28
- One command zero config files:
28
+ One command - zero config files:
29
29
 
30
30
  ```bash
31
31
  claude mcp add cenogram https://mcp.cenogram.pl/mcp \
@@ -77,7 +77,7 @@ Add to your config file:
77
77
  }
78
78
  ```
79
79
 
80
- **Stdio fallback** (older versions requires Node.js >= 18):
80
+ **Stdio fallback** (older versions - requires Node.js >= 18):
81
81
  ```json
82
82
  {
83
83
  "mcpServers": {
@@ -159,7 +159,7 @@ In VS Code: Settings > Cline > MCP Servers. Add:
159
159
  </details>
160
160
 
161
161
  <details>
162
- <summary><strong>npx (stdio) local/offline</strong></summary>
162
+ <summary><strong>npx (stdio) - local/offline</strong></summary>
163
163
 
164
164
  Requires **Node.js >= 18**. Use this if you want to run the server locally instead of connecting to the hosted one.
165
165
 
@@ -191,7 +191,7 @@ Requires **Node.js >= 18**. Use this if you want to run the server locally inste
191
191
 
192
192
  | Env Variable | Required | Default | Description |
193
193
  |---|---|---|---|
194
- | `CENOGRAM_API_KEY` | **Yes** (stdio) | | API key from [cenogram.pl/api](https://cenogram.pl/api) |
194
+ | `CENOGRAM_API_KEY` | **Yes** (stdio) | - | API key from [cenogram.pl/api](https://cenogram.pl/api) |
195
195
  | `CENOGRAM_API_URL` | No | `https://cenogram.pl` | API base URL |
196
196
  | `MCP_TRANSPORT` | No | `stdio` | Set to `http` for Streamable HTTP mode |
197
197
  | `MCP_PORT` | No | `3002` | HTTP server port (HTTP mode only) |
@@ -235,7 +235,7 @@ You can also use the `--http` CLI flag instead of `MCP_TRANSPORT=http`.
235
235
 
236
236
  - Most cities: use the city name directly (e.g., "Gdansk", "Lublin")
237
237
  - Warsaw: use district names ("Mokotow", "Srodmiescie", "Wola") -- "Warszawa" won't match
238
- - Krakow: use sub-districts ("Krakow-Podgorze", "Krakow-Srodmiescie") plain "Krakow" won't match
238
+ - Krakow: use sub-districts ("Krakow-Podgorze", "Krakow-Srodmiescie") - plain "Krakow" won't match
239
239
  - Use `list_locations` to find valid names
240
240
 
241
241
  ### Property types
@@ -269,13 +269,13 @@ This mimics how a property appraiser finds comparable transactions for valuation
269
269
 
270
270
  ## Troubleshooting
271
271
 
272
- **"Error: CENOGRAM_API_KEY is required"** This only applies to stdio mode. Make sure `CENOGRAM_API_KEY` is set in the `env` block of your MCP config. For HTTP remote, the key goes in the `Authorization` header instead.
272
+ **"Error: CENOGRAM_API_KEY is required"** - This only applies to stdio mode. Make sure `CENOGRAM_API_KEY` is set in the `env` block of your MCP config. For HTTP remote, the key goes in the `Authorization` header instead.
273
273
 
274
- **npx hangs or fails** Check your Node.js version with `node -v`. The stdio mode requires Node.js >= 18. If you're on an older version, use the HTTP remote option instead (no Node.js needed).
274
+ **npx hangs or fails** - Check your Node.js version with `node -v`. The stdio mode requires Node.js >= 18. If you're on an older version, use the HTTP remote option instead (no Node.js needed).
275
275
 
276
- **"Warszawa" returns 0 results** Warsaw uses district names (Mokotow, Wola, Srodmiescie, Bemowo, etc.). Use `list_locations(search="warsz")` to find valid names. Same applies to Krakow (use "Krakow-Podgorze", "Krakow-Srodmiescie", etc.).
276
+ **"Warszawa" returns 0 results** - Warsaw uses district names (Mokotow, Wola, Srodmiescie, Bemowo, etc.). Use `list_locations(search="warsz")` to find valid names. Same applies to Krakow (use "Krakow-Podgorze", "Krakow-Srodmiescie", etc.).
277
277
 
278
- **401 Unauthorized (HTTP mode)** The `Authorization` header must be `Bearer cngrm_...` (with the `Bearer` prefix). Double-check that the full API key is included, not just the prefix.
278
+ **401 Unauthorized (HTTP mode)** - The `Authorization` header must be `Bearer cngrm_...` (with the `Bearer` prefix). Double-check that the full API key is included, not just the prefix.
279
279
 
280
280
  ## Development
281
281
 
package/dist/client-id.js CHANGED
@@ -22,7 +22,7 @@ export function getClientId() {
22
22
  }
23
23
  }
24
24
  catch {
25
- // File doesn't exist or isn't readable generate new
25
+ // File doesn't exist or isn't readable - generate new
26
26
  }
27
27
  // Generate and persist
28
28
  const id = randomUUID();
@@ -31,7 +31,7 @@ export function getClientId() {
31
31
  writeFileSync(CLIENT_ID_FILE, id + "\n", { mode: 0o600 });
32
32
  }
33
33
  catch {
34
- // Read-only fs (Docker, sandbox) use ephemeral ID
34
+ // Read-only fs (Docker, sandbox) - use ephemeral ID
35
35
  }
36
36
  cachedId = id;
37
37
  return id;
package/dist/index.js CHANGED
@@ -16,11 +16,11 @@ catch { /* fallback to hardcoded if dist/ used standalone */ }
16
16
  export function createMcpServer(apiKey) {
17
17
  const server = new McpServer({ name: "cenogram-mcp-server", version: PKG_VERSION }, {
18
18
  instructions: [
19
- "Cenogram MCP Server 7M+ verified real estate transactions from Poland's official RCN registry (Rejestr Cen Nieruchomości). Transaction prices from notarial deeds NOT asking/listing prices. Data from 2003 to present, ~380 counties, refreshed every ~2 weeks.",
19
+ "Cenogram MCP Server - 7M+ verified real estate transactions from Poland's official RCN registry (Rejestr Cen Nieruchomości). Transaction prices from notarial deeds - NOT asking/listing prices. Data from 2003 to present, ~380 counties, refreshed every ~2 weeks.",
20
20
  "",
21
- "CRITICAL District names (ALWAYS verify first):",
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",
23
+ "- Warsaw: use district names (Mokotów, Wola, Śródmieście) - \"Warszawa\" returns 0 results",
24
24
  "- Kraków: Kraków-Śródmieście, Kraków-Podgórze, Kraków-Krowodrza, Kraków-Nowa Huta",
25
25
  "- Most cities (Gdańsk, Gdynia, Sopot, Poznań): just the city name, no sub-districts",
26
26
  "",
@@ -29,14 +29,14 @@ export function createMcpServer(apiKey) {
29
29
  "- Compare locations: list_locations → compare_locations (2-5 districts, requires at least one filter e.g. propertyType)",
30
30
  "- Parcel lookup: search_parcels(q, min 3 chars) → search_by_area (use returned lat/lng)",
31
31
  "- Address search: search_transactions(location, street, buildingNumber)",
32
- "- Radius search: search_by_area(lat, lng, radiusKm) for geographic proximity",
33
- "- Polygon search: search_by_polygon coordinates are [longitude, latitude], first=last point, max 500 vertices",
32
+ "- Radius search: search_by_area(lat, lng, radiusKm) - for geographic proximity",
33
+ "- Polygon search: search_by_polygon - coordinates are [longitude, latitude], first=last point, max 500 vertices",
34
34
  "",
35
35
  "Data notes:",
36
36
  "- price_per_m2 only meaningful for apartments (propertyType=\"unit\")",
37
- "- API has no rooms filter use area as proxy (1-room: 20-35m², 2: 35-55m², 3: 55-90m², 4+: 80-130m²), then post-filter by rooms field in results",
37
+ "- API has no rooms filter - use area as proxy (1-room: 20-35m², 2: 35-55m², 3: 55-90m², 4+: 80-130m²), then post-filter by rooms field in results",
38
38
  "- Results paginated (default 10-20). Use page parameter for more.",
39
- "- For §79-compliant export table or interactive map direct user to cenogram.pl",
39
+ "- For §79-compliant export table or interactive map - direct user to cenogram.pl",
40
40
  ].join("\n"),
41
41
  });
42
42
  registerTools(server, apiKey);
@@ -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 {
package/dist/tools.js CHANGED
@@ -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();
@@ -27,7 +32,7 @@ export function registerTools(server, apiKey) {
27
32
  Returns transaction details: address, date, price, area, price/m², property type.
28
33
  Use list_locations first to find valid location names.
29
34
  Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
30
- 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. '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."),
31
36
  propertyType: z.enum(["land", "building", "developed_land", "unit"]).optional()
32
37
  .describe("Property type filter"),
33
38
  marketType: z.enum(["primary", "secondary"]).optional()
@@ -38,7 +43,7 @@ Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
38
43
  dateTo: z.string().optional().describe("End date (YYYY-MM-DD)"),
39
44
  street: z.string().optional().describe("Street name filter (partial match, e.g. 'Puławska', 'Trakt Lubelski')"),
40
45
  buildingNumber: z.string().optional().describe("Building/house number (e.g. '251C', '12A'). Requires location or street to be set."),
41
- parcelId: z.string().optional().describe("Exact parcel ID as returned in search results (e.g. '146518_8.0108.27'). Must match exactly copy from a previous search result's parcel_id field."),
46
+ parcelId: z.string().optional().describe("Exact parcel ID as returned in search results (e.g. '146518_8.0108.27'). Must match exactly - copy from a previous search result's parcel_id field."),
42
47
  minArea: z.number().optional().describe("Minimum area in m²"),
43
48
  maxArea: z.number().optional().describe("Maximum area in m²"),
44
49
  limit: z.number().min(1).max(50).default(10)
@@ -50,6 +55,7 @@ Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
50
55
  page: z.number().min(1).default(1).optional()
51
56
  .describe("Page number for pagination (default: 1)"),
52
57
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
58
+ requireApiKey(apiKey);
53
59
  const txParams = {
54
60
  district: params.location,
55
61
  propertyType: mapPropertyType(params.propertyType),
@@ -77,9 +83,10 @@ Example: search for apartments in Mokotów sold in 2024 above 500,000 PLN.`, {
77
83
  // ── Tool 2: get_price_statistics ────────────────────────────────────
78
84
  server.tool("get_price_statistics", `Get price per m² statistics by location for residential apartments in Poland.
79
85
  Note: only covers residential units (lokale mieszkalne). For other property types, use search_transactions.
80
- For Warsaw: use district names (Mokotów, Wola) 'Warszawa' won't match any results.`, {
86
+ For Warsaw: use district names (Mokotów, Wola) - 'Warszawa' won't match any results.`, {
81
87
  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
88
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
89
+ requireApiKey(apiKey);
83
90
  const { data: allRows, creditInfo } = await getPricePerM2(apiKey);
84
91
  let rows = allRows;
85
92
  if (params.location) {
@@ -95,6 +102,7 @@ Useful for understanding the overall market price structure in Poland.`, {
95
102
  maxPrice: z.number().default(3_000_000)
96
103
  .describe("Maximum price to include (default 3,000,000 PLN)"),
97
104
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
105
+ requireApiKey(apiKey);
98
106
  const { data: bins, creditInfo } = await getPriceHistogram(params.bins, params.maxPrice, apiKey);
99
107
  return textResponse(formatHistogram(bins) + formatCreditFooter(creditInfo));
100
108
  }));
@@ -119,6 +127,7 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
119
127
  limit: z.number().min(1).max(50).default(20)
120
128
  .describe("Number of results (1-50, default 20)"),
121
129
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
130
+ requireApiKey(apiKey);
122
131
  const bbox = radiusKmToBbox(params.latitude, params.longitude, params.radiusKm);
123
132
  const txParams = {
124
133
  bbox: bbox.join(","),
@@ -141,17 +150,19 @@ Example: find apartment sales within 2km of Warsaw's Palace of Culture (lat 52.2
141
150
  // ── Tool 5: get_market_overview ─────────────────────────────────────
142
151
  server.tool("get_market_overview", `Get a comprehensive overview of the Polish real estate transaction database.
143
152
  Returns: total transaction count, date range, breakdown by property type and market type, top locations, price statistics.`, {}, { readOnlyHint: true }, async () => withErrorHandling(async () => {
153
+ requireApiKey(apiKey);
144
154
  const { data: stats, creditInfo } = await getStats(apiKey);
145
155
  return textResponse(formatMarketOverview(stats) + formatCreditFooter(creditInfo));
146
156
  }));
147
157
  // ── Tool 6: list_locations ──────────────────────────────────────────
148
158
  server.tool("list_locations", `List available locations (cities and districts) in the database.
149
- Returns administrative districts for most cities, the district name equals the city name.
159
+ Returns administrative districts - for most cities, the district name equals the city name.
150
160
  For Warsaw: returns district names (Mokotów, Śródmieście, Wola, etc.), not 'Warszawa'.
151
161
  For Kraków: returns sub-districts (Kraków-Podgórze, Kraków-Śródmieście, etc.).
152
162
  Use the search parameter to filter by name.`, {
153
163
  search: z.string().optional().describe("Filter locations by name (case-insensitive partial match, e.g. 'Krak' for Kraków districts)"),
154
164
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
165
+ requireApiKey(apiKey);
155
166
  const { data: allDistricts, creditInfo } = await getDistricts(apiKey);
156
167
  let districts = allDistricts;
157
168
  if (params.search) {
@@ -183,6 +194,7 @@ Example: search for parcels starting with '146518_8.01'.`, {
183
194
  limit: z.number().min(1).max(10).default(10).optional()
184
195
  .describe("Max results (1-10, default 10)"),
185
196
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
197
+ requireApiKey(apiKey);
186
198
  const { data, creditInfo } = await searchParcels(params.q, params.limit, apiKey);
187
199
  return textResponse(formatParcelResults(data, params.q) + formatCreditFooter(creditInfo));
188
200
  }));
@@ -212,6 +224,7 @@ Example: {"type":"Polygon","coordinates":[[[21.0,52.2],[21.01,52.2],[21.01,52.21
212
224
  limit: z.number().min(1).max(5000).default(100).optional()
213
225
  .describe("Max results (1-5000, default 100). MCP displays up to 50 transactions."),
214
226
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
227
+ requireApiKey(apiKey);
215
228
  const { data, creditInfo } = await searchByPolygon({
216
229
  polygon: params.polygon,
217
230
  propertyType: mapPropertyType(params.propertyType),
@@ -236,7 +249,7 @@ Requires at least one filter besides districts (e.g., propertyType).
236
249
  Example: compare Mokotów, Wola, Ursynów for apartments.`, {
237
250
  districts: z.string().min(1).describe("Comma-separated district names to compare (2-5). E.g. 'Mokotów,Wola,Ursynów'"),
238
251
  propertyType: z.enum(["land", "building", "developed_land", "unit"]).optional()
239
- .describe("Property type filter (recommended API requires at least one filter)"),
252
+ .describe("Property type filter (recommended - API requires at least one filter)"),
240
253
  marketType: z.enum(["primary", "secondary"]).optional()
241
254
  .describe("Market type filter"),
242
255
  minPrice: z.number().optional().describe("Minimum price in PLN"),
@@ -247,6 +260,7 @@ Example: compare Mokotów, Wola, Ursynów for apartments.`, {
247
260
  maxArea: z.number().optional().describe("Maximum area in m²"),
248
261
  street: z.string().optional().describe("Street name filter"),
249
262
  }, { readOnlyHint: true }, async (params) => withErrorHandling(async () => {
263
+ requireApiKey(apiKey);
250
264
  const { data, creditInfo } = await compareLocations({
251
265
  districts: params.districts,
252
266
  propertyType: mapPropertyType(params.propertyType),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cenogram/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP Server for Polish real estate transaction data (7M+ transactions from RCN)",
5
5
  "type": "module",
6
6
  "bin": {