Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules
dist
*.log
.git
.gitignore
.github
.claude
.env
.env.*
README.md
CONTRIBUTING.md
SECURITY.md
LICENSE
server.json
src/__tests__
src/**/*.test.ts
eslint.config.js
3 changes: 2 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ jobs:
node-version: 24
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm test
# Build first: part of the suite only runs against dist/, and skips itself when it is absent.
- run: npm run build
- run: npm test
- name: Verify tag matches package version
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
node_modules/
dist/
*.tsbuildinfo
# The leak-guard skips node_modules/, dist/, coverage/ and .turbo/ on the strength of the rules in
# THIS file — the four rules above and below, wherever they sit. Remove one and the guard keeps
# skipping the directory while git starts committing it. A test enforces the pairing; heed it.
coverage/
.turbo/
# Holds absolute paths of the machine that ran the lint — must not travel with this directory.
.eslintcache
.env
.env.*
!.env.example
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Changelog

## 0.5.0

First release since 0.2.0, so it carries everything published on the hosted server in the
meantime. Versions 0.3.0 and 0.4.0 exist in the source history but were never released to npm.

### New tools (14)

Parcels:
- `resolve_parcel` - resolve a cadastral parcel identifier to its canonical record
- `get_parcel_report` - composite dossier for one parcel: core data, enrichment layers,
transaction history, local price context and municipal context

Context for a location:
- `get_demographics` - population and demographic context
- `get_infrastructure_signals` - municipal infrastructure signals (tenders, utilities,
capital spending)
- `estimate_value` - comparable-sales value estimate for a property (Beta)

Context for the property behind a single transaction, each taking a `transaction_id` from a
search result:
- `get_building_breakdown` - per-building footprint, storeys, estimated floor area
- `get_transaction_flood` - flood risk
- `get_transaction_heritage` - heritage-register status
- `get_transaction_landslide` - landslide risk
- `get_transaction_surroundings` - nuisance and land-use context around the property
- `get_transaction_transit` - public transport accessibility
- `get_transaction_permits` - building permits recorded for the property
- `get_transaction_planning` - local zoning and planning status
- `get_transaction_farmland` - agricultural land-use classification

### Changed

- Search filters: floor (for units), ownership type, and an explicit "no data" option where a
field can be missing.
- Results carry parcel identifiers and coordinates consistently, so a search can be followed by
a parcel or enrichment lookup without a second search.
- All calls now go to the versioned `/api/v1` endpoints.
- Tool descriptions state how Warsaw and Krakow districts are addressed, and a wrong location
name now comes back with a usable correction instead of a bare 404.

### Fixed - error messages an AI agent can act on

- `Retry-After` was read as days instead of seconds, so a five-second rate limit was reported as
"resets in 1 day". It now reports seconds, minutes or hours, and says nothing about time at
all when the server did not send a usable value.
- Every payment-required response was reported as "insufficient credits (balance: 0)" even when
the account had a full balance and the real cause was an expired trial. The server's own
explanation is now relayed.
- Running without an API key was reported as an internal error with an invitation to file a bug,
and pointed at a page behind a login. It now says a key is missing and where to get one.
- 403, 503 and 410 responses relayed no detail. They now carry the server's explanation, and 410
states that the endpoint is gone for good rather than suggesting a retry.

## 0.2.0

- Authentication header fix, English error messages.
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

COPY tsconfig*.json ./
COPY src ./src
RUN npm run build

FROM node:22-alpine
WORKDIR /app

COPY --from=build /app/package*.json ./
COPY --from=build /app/dist ./dist
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev

ARG GIT_SHA=unknown
ENV GIT_SHA=$GIT_SHA
ENV NODE_ENV=production
ENV MCP_TRANSPORT=http
ENV MCP_PORT=3002

USER node
EXPOSE 3002

CMD ["node", "dist/index.js"]
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

MCP server for Polish real estate data. Access 8M+ real estate transactions from the national Registry of Prices and Values (Rejestr Cen Nieruchomosci, RCN) directly from Claude, Cursor, or any MCP-compatible AI assistant.

Data source: Polish national RCN registry (Rejestr Cen Nieruchomosci) | Platform: [cenogram.pl](https://cenogram.pl)
Data source: Polish national RCN registry (Rejestr Cen Nieruchomosci) | Platform: [cenogram.pl](https://cenogram.pl?src=mcpstdio)

## Get your API key

1. Go to [cenogram.pl/api](https://cenogram.pl/api)
1. Go to [cenogram.pl/api](https://cenogram.pl/api?src=mcpstdio)
2. Enter your email
3. You'll receive your `cngrm_...` API key by email

Expand Down Expand Up @@ -176,14 +176,18 @@ Requires **Node.js >= 18**. Use this if you want to run the server locally inste

| Env Variable | Required | Default | Description |
|---|---|---|---|
| `CENOGRAM_API_KEY` | **Yes** (stdio) | - | API key from [cenogram.pl/api](https://cenogram.pl/api) |
| `CENOGRAM_API_KEY` | **Yes** (stdio) | - | API key from [cenogram.pl/api](https://cenogram.pl/api?src=mcpstdio) |
| `CENOGRAM_API_URL` | No | `https://cenogram.pl` | API base URL |
| `MCP_TRANSPORT` | No | `stdio` | Set to `http` for Streamable HTTP mode |
| `MCP_PORT` | No | `3002` | HTTP server port (HTTP mode only) |
| `CENOGRAM_CLIENT_ID` | No | auto-generated | Persistent client identifier |

You can also use the `--http` CLI flag instead of `MCP_TRANSPORT=http`.

## Tips

- **Model selection**: For best results, use Claude **Opus 4.7**. It makes more sequential tool calls and produces richer analysis. You can switch the model in the dropdown at the bottom of the chat window.

## Example Prompts

**Polish:**
Expand Down Expand Up @@ -215,12 +219,27 @@ You can also use the `--http` CLI flag instead of `MCP_TRANSPORT=http`.
| `search_parcels` | Search parcels by cadastral ID prefix | q (parcel ID prefix, min 3 chars) |
| `search_by_polygon` | Search within a GeoJSON polygon | polygon, propertyType, dateFrom/dateTo |
| `compare_locations` | Compare stats across 2-5 districts | districts (comma-separated), propertyType |
| `get_building_breakdown` | Per-building breakdown for one transaction (footprint, storeys, est. floor area) | transaction_id (UUID from a search result) |
| `get_parcel_report` | Composite dossier for one parcel: core, 9 enrichment layers, transaction history, local price context and municipal context | parcelId (cadastral id or UUID) |
| `resolve_parcel` | Resolve a cadastral parcel identifier to its canonical record | parcelId or q (id prefix), or lat + lng |
| `get_demographics` | Population and demographic context for a location | location or teryt, year (or yearFrom/yearTo), category |
| `get_infrastructure_signals` | Municipal infrastructure signals (tenders, utilities, capital spending) | location or teryt |
| `estimate_value` | Comparable-sales value estimate for a property | area, plus lat + lng or parcelId; rooms, market |
| `get_transaction_flood` | Flood risk for the property in a transaction | transaction_id (UUID from a search result) |
| `get_transaction_heritage` | Heritage-register status for the property | transaction_id |
| `get_transaction_landslide` | Landslide risk for the property | transaction_id |
| `get_transaction_surroundings` | Nuisance and land-use context around the property | transaction_id |
| `get_transaction_transit` | Public transport accessibility for the property | transaction_id |
| `get_transaction_permits` | Building permits recorded for the property | transaction_id |
| `get_transaction_planning` | Local zoning and planning status for the property | transaction_id |
| `get_transaction_farmland` | Agricultural land-use classification for the property | transaction_id |

### Location naming

- Most cities: use the city name directly (e.g., "Gdansk", "Lublin")
- Warsaw: use district names ("Mokotow", "Srodmiescie", "Wola") -- "Warszawa" won't match
- Krakow: use sub-districts ("Krakow-Podgorze", "Krakow-Srodmiescie") - plain "Krakow" won't match
- Warsaw: "Warszawa" covers all 18 districts at once; name one ("Mokotow", "Srodmiescie", "Wola") to narrow it down
- Krakow and Lodz work the same way: the city name covers every sub-district, or name one ("Krakow-Podgorze")
- Neighbourhood names are not administrative units - search by radius or polygon instead
- Use `list_locations` to find valid names

### Property types
Expand Down Expand Up @@ -258,7 +277,7 @@ This mimics how a property appraiser finds comparable transactions for valuation

**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).

**"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.).
**A location returns 0 results** - The name may not be an administrative unit. Districts and neighbourhoods are two different things: "Mokotow" is a district and works, "Sluzew" is a neighbourhood inside it and does not. Use `list_locations(search="...")` to find valid names, or search by radius (`search_by_area`) for anything smaller than a district.

**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.

Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import js from '@eslint/js'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{ ignores: ['dist', 'vitest.config.ts'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.ts'],
Expand Down
Loading
Loading