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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 0.6.0

Documentation and protocol-compatibility release - no breaking changes, no new tools.

### OAuth / discovery
- Serve the path-aware `/.well-known/oauth-protected-resource/mcp` document (RFC 9728) in
addition to the origin-level one, so clients that derive the metadata URL from the endpoint
they call can complete discovery instead of aborting.
- Add a `/.well-known/glama.json` endpoint for directory ownership verification (served only
when `GLAMA_MAINTAINER_EMAIL` is set, 404 otherwise).

### Tools
- `search_by_polygon`: maximum `limit` lowered from 5000 to 3000.

### Docs
- README and package description now cover per-parcel context (zoning, flood and landslide
risk, heritage register, building permits, construction activity, transit access,
agricultural land and surroundings) alongside transaction prices, and mention more MCP
clients (ChatGPT, Grok).
- Refreshed the example parcel IDs used in the sample prompts.

## 0.5.0

First release since 0.2.0, so it carries everything published on the hosted server in the
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
[![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

**Polish Real Estate Transaction Data for AI**
**Polish Real Estate Transaction & Parcel Data for AI**

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.
MCP server for Polish real estate data. Access 8M+ real estate transactions from the national Registry of Prices and Values (Rejestr Cen Nieruchomosci, RCN) - prices from notarial deeds, not listings - directly from Claude, Cursor, ChatGPT, Grok, or any MCP-compatible AI assistant. Beyond transaction prices, the server resolves cadastral parcels and adds per-parcel context: zoning, flood and landslide risk, heritage register, building permits and construction activity, public transport access, agricultural land classification and surrounding land use.

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

Expand Down Expand Up @@ -193,15 +193,17 @@ You can also use the `--http` CLI flag instead of `MCP_TRANSPORT=http`.
**Polish:**
- "Jaka jest mediana cen mieszkan w Krakowie w 2025?"
- "Pokaz transakcje z ulicy Pulawskiej 15 na Mokotowie"
- "Znajdz transakcje na dzialce 146518_8.0108.27"
- "Znajdz transakcje na dzialce 126104_9.0015.201"
- "Sprawdz plan miejscowy i ryzyko powodziowe dla dzialki 126104_9.0015.201"
- "Znajdz transakcje gruntow w promieniu 5km od centrum Wroclawia powyzej 500 000 PLN"
- "Porownaj ceny mieszkan na Mokotowie i Woli"
- "Pokaz rozklad cen nieruchomosci w Polsce"

**English:**
- "What's the median apartment price in Krakow in 2025?"
- "Show transactions at Pulawska 15 in Mokotow"
- "Find all transactions on parcel 146502_8.0901.12 and then search nearby"
- "Find all transactions on parcel 126104_9.0015.201 and then search nearby"
- "Check the zoning and flood risk for parcel 126104_9.0015.201"
- "Find land transactions within 5km of Wroclaw center above 500,000 PLN"
- "Compare apartment prices in Mokotow and Wola districts"
- "Show the price distribution of real estate in Poland"
Expand Down Expand Up @@ -270,6 +272,7 @@ This mimics how a property appraiser finds comparable transactions for valuation
- **Date range:** 2003 - present
- **Source:** Polish national RCN registry (Rejestr Cen Nieruchomosci)
- **Refresh:** periodic updates from RCN
- **Per-parcel context:** zoning, flood and landslide risk, heritage register, building permits and construction activity, transit access, agricultural land use and surroundings, addressable by cadastral ID

## Troubleshooting

Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cenogram/mcp-server",
"version": "0.5.0",
"description": "MCP server for Polish property prices from notarial deeds, not listings - 8M+ transactions from the RCN registry",
"version": "0.6.0",
"description": "MCP server for Polish real estate: 8M+ transaction prices from notarial deeds (RCN registry, not listings), plus per-parcel context by cadastral ID - zoning, flood risk, heritage, building permits, agricultural land and transit",
"type": "module",
"bin": {
"cenogram-mcp": "./dist/index.js"
Expand Down Expand Up @@ -48,6 +48,11 @@
"transactions",
"transaction-data",
"geospatial",
"cadastral",
"parcel-data",
"zoning",
"flood-risk",
"due-diligence",
"rcn",
"nieruchomosci"
],
Expand Down
10 changes: 5 additions & 5 deletions server.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "pl.cenogram/mcp-server",
"title": "Cenogram - Polish Real Estate Data",
"description": "Polish property prices from notarial deeds, not listings - 8M+ transactions from the RCN registry.",
"version": "0.5.0",
"websiteUrl": "https://cenogram.pl",
"title": "Cenogram - Polish Real Estate Prices & Parcel Data",
"description": "Polish real estate data for AI: 8M+ transaction prices from notarial deeds (RCN registry, not listings), plus per-parcel context by cadastral ID - zoning, flood risk, heritage register, building permits, agricultural land use and public transport.",
"version": "0.6.0",
"websiteUrl": "https://cenogram.pl/en/mcp?src=mcpregistry",
"repository": {
"url": "https://github.com/cenogram/mcp-server",
"source": "github"
Expand All @@ -13,7 +13,7 @@
{
"registryType": "npm",
"identifier": "@cenogram/mcp-server",
"version": "0.5.0",
"version": "0.6.0",
"transport": {
"type": "stdio"
},
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ describe.skipIf(!hasDistBuild)("HTTP mode auth dispatch (E2E spawn)", () => {
...process.env,
MCP_TRANSPORT: "http",
MCP_PORT: String(port),
GLAMA_MAINTAINER_EMAIL: "maintainer@example.test",
// Note: NODE_ENV not 'production' so OAUTH_JWT_KID etc. not strictly required.
// Tests that hit JWT validation path will trigger OAuthConfigError → 500.
},
Expand Down Expand Up @@ -310,6 +311,24 @@ describe.skipIf(!hasDistBuild)("HTTP mode auth dispatch (E2E spawn)", () => {
expect(body.resource).toBe("https://mcp.cenogram.pl");
expect(body.authorization_servers).toContain("https://api.cenogram.pl");
});

it("path-aware /.well-known/oauth-protected-resource/mcp echoes the identifier its URL was derived from", async () => {
const res = await fetch(`http://127.0.0.1:${port}/.well-known/oauth-protected-resource/mcp`);
expect(res.status).toBe(200);
const body = await res.json() as { authorization_servers: string[]; resource: string };
// Not the origin: a client that derived this URL from https://mcp.cenogram.pl/mcp verifies the
// field against that identifier, so echoing the origin here would make it abort, not fall back.
expect(body.resource).toBe("https://mcp.cenogram.pl/mcp");
expect(body.authorization_servers).toContain("https://api.cenogram.pl");
});

it("/.well-known/glama.json serves the connector claim from the environment", async () => {
const res = await fetch(`http://127.0.0.1:${port}/.well-known/glama.json`);
expect(res.status).toBe(200);
const body = await res.json() as { $schema: string; maintainers: { email: string }[] };
expect(body.$schema).toBe("https://glama.ai/mcp/schemas/connector.json");
expect(body.maintainers).toEqual([{ email: "maintainer@example.test" }]);
});
});

// ─── HTTP mode E2E with valid JWT + stub upstream API ───
Expand Down
29 changes: 27 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,40 @@ async function main() {
await mcpServer.close();
}
});
} else if (pathname === "/.well-known/oauth-protected-resource") {
} else if (pathname === "/.well-known/oauth-protected-resource" || pathname === "/.well-known/oauth-protected-resource/mcp") {
// RFC 9728 defines BOTH shapes: the bare document for a resource whose identifier is the
// origin, and the path-aware one derived from an identifier that carries a path. Clients
// that skip WWW-Authenticate and guess the URL from the endpoint they are calling look for
// the second one. Each document echoes the identifier its own URL was derived from - a
// client verifies that field against the resource it thinks it is talking to, so serving
// the origin under the path-aware URL would make strict clients abort instead of fall back.
// Both identifiers land on the same audience: /oauth/authorize canonicalizes `resource`
// down to the origin, so the issued token's `aud` is the origin either way.
const resource = pathname.endsWith("/mcp") ? "https://mcp.cenogram.pl/mcp" : "https://mcp.cenogram.pl";
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600" }).end(
JSON.stringify({
resource: "https://mcp.cenogram.pl",
resource,
authorization_servers: ["https://api.cenogram.pl"],
scopes_supported: ["mcp"],
bearer_methods_supported: ["header"],
}),
);
} else if (pathname === "/.well-known/glama.json") {
// Directory ownership proof: the file must live on the server's own domain, so it cannot
// ship in the repo like the sibling server-level claim. The address comes from the
// environment because this file is published verbatim to npm and a public repo - no
// personal address in public source. Unset (stdio users, dev) → 404, not an empty claim.
const maintainer = process.env.GLAMA_MAINTAINER_EMAIL;
if (!maintainer) {
res.writeHead(404, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "not_found" }));
} else {
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600" }).end(
JSON.stringify({
$schema: "https://glama.ai/mcp/schemas/connector.json",
maintainers: [{ email: maintainer }],
}),
);
}
} else if (pathname === "/.well-known/mcp.json") {
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600" }).end(
JSON.stringify({
Expand Down
4 changes: 2 additions & 2 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,8 @@ Example: {"type":"Polygon","coordinates":[[[21.0,52.2],[21.01,52.2],[21.01,52.21
maxArea: z.number().optional().describe("Maximum area in m²"),
district: z.string().optional().describe("District name filter"),
street: z.string().optional().describe("Street name filter (partial match)"),
limit: z.number().min(1).max(5000).default(100).optional()
.describe("Max results (1-5000, default 100). MCP displays up to 50 transactions."),
limit: z.number().min(1).max(3000).default(100).optional()
.describe("Max results (1-3000, default 100). MCP displays up to 50 transactions."),
},
{ readOnlyHint: true, destructiveHint: false, title: "Search Transactions by Polygon" },
async (params) =>
Expand Down
Loading