@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 +10 -10
- package/dist/client-id.js +2 -2
- package/dist/index.js +7 -11
- package/dist/tools.js +19 -5
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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)
|
|
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) |
|
|
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")
|
|
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"**
|
|
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**
|
|
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**
|
|
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)**
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
33
|
-
"- Polygon search: search_by_polygon
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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),
|