@cenogram/mcp-server 0.2.0 → 0.5.0

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.
@@ -3,6 +3,22 @@ import { getClientId } from "./client-id.js";
3
3
  import { authErrorMessage, getAuthMode } from "./error-messages.js";
4
4
  import { requestContext } from "./request-context.js";
5
5
  const BASE_URL = process.env.CENOGRAM_API_URL || "https://cenogram.pl";
6
+ export function getDemographics(params, apiKey) {
7
+ return fetchApi("/api/v1/demographics", toQueryParams({
8
+ location: params.location,
9
+ teryt: params.teryt,
10
+ year: params.year,
11
+ yearFrom: params.yearFrom,
12
+ yearTo: params.yearTo,
13
+ category: params.category,
14
+ }), apiKey);
15
+ }
16
+ export function getInfrastructureSignals(params, apiKey) {
17
+ return fetchApi("/api/v1/infrastructure-signals", toQueryParams({
18
+ location: params.location,
19
+ teryt: params.teryt,
20
+ }), apiKey);
21
+ }
6
22
  function extractCreditInfo(res) {
7
23
  if (!res.headers)
8
24
  return null;
@@ -12,14 +28,22 @@ function extractCreditInfo(res) {
12
28
  return null;
13
29
  return { balance, cost };
14
30
  }
15
- // ── OAuth internal auth ────────────────────────────────────────────
16
- // SOH char (\x01) is impossible in base64url or ctx_ keys - used as both prefix and separator
17
- const OAUTH_CTX_PREFIX = "\x01";
31
+ export const OAUTH_CTX_PREFIX = "\x01";
18
32
  export function encodeOAuthCtx(userId, grantId) {
19
- // \x01{userId}\x01{grantId} - \x01 cannot appear in UUIDs (hex + hyphens only)
20
33
  return `${OAUTH_CTX_PREFIX}${userId}${OAUTH_CTX_PREFIX}${grantId}`;
21
34
  }
22
- // ── Shared HTTP helpers ────────────────────────────────────────────
35
+ export function decodeOAuthCtx(key) {
36
+ if (!key.startsWith(OAUTH_CTX_PREFIX))
37
+ return null;
38
+ const rest = key.slice(OAUTH_CTX_PREFIX.length);
39
+ const sepIdx = rest.indexOf(OAUTH_CTX_PREFIX);
40
+ if (sepIdx <= 0)
41
+ return null;
42
+ const grantId = rest.slice(sepIdx + OAUTH_CTX_PREFIX.length);
43
+ if (!grantId)
44
+ return null;
45
+ return { userId: rest.slice(0, sepIdx), grantId };
46
+ }
23
47
  function buildHeaders(apiKey) {
24
48
  const headers = {
25
49
  "X-Source": "mcp-server",
@@ -47,13 +71,34 @@ function buildHeaders(apiKey) {
47
71
  }
48
72
  return headers;
49
73
  }
74
+ export function parseRetryAfterSeconds(headerValue) {
75
+ if (!headerValue)
76
+ return null;
77
+ const raw = headerValue.trim();
78
+ if (/^\d+$/.test(raw))
79
+ return Math.max(1, parseInt(raw, 10));
80
+ if (!/[A-Za-z]/.test(raw))
81
+ return null;
82
+ const asDate = Date.parse(raw);
83
+ if (Number.isNaN(asDate))
84
+ return null;
85
+ return Math.max(1, Math.ceil((asDate - Date.now()) / 1000));
86
+ }
87
+ export function formatRetryAfter(seconds) {
88
+ const unit = (value, name) => `${value} ${name}${value === 1 ? "" : "s"}`;
89
+ if (seconds < 60)
90
+ return unit(seconds, "second");
91
+ if (seconds < 3600)
92
+ return unit(Math.ceil(seconds / 60), "minute");
93
+ if (seconds < 86400)
94
+ return unit(Math.ceil(seconds / 3600), "hour");
95
+ return unit(Math.ceil(seconds / 86400), "day");
96
+ }
50
97
  async function handleErrorResponse(res, apiKey) {
51
- // 429 has special Retry-After handling - keep dedicated path
52
98
  if (res.status === 429) {
53
- const retryAfter = res.headers?.get?.("Retry-After");
54
- const days = retryAfter ? Math.ceil(parseInt(retryAfter, 10) / 86400) : null;
55
- const resetInfo = days !== null ? ` Resets in ${days} day(s).` : "";
56
- throw new Error(`Too many requests.${resetInfo}`);
99
+ const seconds = parseRetryAfterSeconds(res.headers?.get?.("Retry-After"));
100
+ const wait = seconds !== null ? ` Retry in ${formatRetryAfter(seconds)}.` : " Retry shortly.";
101
+ throw new Error(`Too many requests - this is a rate limit, not an exhausted allowance.${wait}`);
57
102
  }
58
103
  const body = (await res.json().catch(() => ({})));
59
104
  const mode = getAuthMode(apiKey ?? process.env.CENOGRAM_API_KEY);
@@ -67,7 +112,6 @@ function toQueryParams(obj) {
67
112
  }
68
113
  return params;
69
114
  }
70
- // ── HTTP client ─────────────────────────────────────────────────────
71
115
  export async function fetchApi(path, params, apiKey) {
72
116
  const url = new URL(path, BASE_URL);
73
117
  if (params) {
@@ -109,12 +153,11 @@ export async function fetchApiPost(path, body, apiKey) {
109
153
  clearTimeout(timeout);
110
154
  }
111
155
  }
112
- // ── Typed wrappers ──────────────────────────────────────────────────
113
156
  export function getStats(apiKey) {
114
- return fetchApi("/api/stats", undefined, apiKey);
157
+ return fetchApi("/api/v1/stats", undefined, apiKey);
115
158
  }
116
159
  export function getTransactions(p, apiKey) {
117
- return fetchApi("/api/transactions", toQueryParams({
160
+ return fetchApi("/api/v1/transactions", toQueryParams({
118
161
  district: p.district,
119
162
  teryt: p.teryt,
120
163
  street: p.street,
@@ -123,8 +166,15 @@ export function getTransactions(p, apiKey) {
123
166
  propertyType: p.propertyType,
124
167
  marketType: p.marketType,
125
168
  unitFunction: p.unitFunction,
169
+ ownershipType: p.ownershipType,
126
170
  buildingType: p.buildingType,
127
171
  mpzpDesignation: p.mpzpDesignation,
172
+ transactionType: p.transactionType,
173
+ rooms: p.rooms,
174
+ floor: p.floor,
175
+ floodRisk: p.floodRisk,
176
+ heritageStatus: p.heritageStatus,
177
+ landslideRisk: p.landslideRisk,
128
178
  minPrice: p.minPrice,
129
179
  maxPrice: p.maxPrice,
130
180
  dateFrom: p.dateFrom,
@@ -139,15 +189,24 @@ export function getTransactions(p, apiKey) {
139
189
  }), apiKey);
140
190
  }
141
191
  export function getTransactionsSummary(p, apiKey) {
142
- return fetchApi("/api/transactions/summary", toQueryParams({
192
+ return fetchApi("/api/v1/transactions/summary", toQueryParams({
143
193
  district: p.district,
144
194
  teryt: p.teryt,
145
195
  street: p.street,
196
+ buildingNumber: p.buildingNumber,
197
+ parcelId: p.parcelId,
146
198
  propertyType: p.propertyType,
147
199
  marketType: p.marketType,
148
200
  unitFunction: p.unitFunction,
201
+ ownershipType: p.ownershipType,
149
202
  buildingType: p.buildingType,
150
203
  mpzpDesignation: p.mpzpDesignation,
204
+ transactionType: p.transactionType,
205
+ rooms: p.rooms,
206
+ floor: p.floor,
207
+ floodRisk: p.floodRisk,
208
+ heritageStatus: p.heritageStatus,
209
+ landslideRisk: p.landslideRisk,
151
210
  minPrice: p.minPrice,
152
211
  maxPrice: p.maxPrice,
153
212
  dateFrom: p.dateFrom,
@@ -158,19 +217,67 @@ export function getTransactionsSummary(p, apiKey) {
158
217
  }), apiKey);
159
218
  }
160
219
  export function getPricePerM2(apiKey) {
161
- return fetchApi("/api/price-per-m2", undefined, apiKey);
220
+ return fetchApi("/api/v1/price-per-m2", undefined, apiKey);
162
221
  }
163
222
  export function getDistricts(apiKey) {
164
- return fetchApi("/api/districts", undefined, apiKey);
223
+ return fetchApi("/api/v1/districts", undefined, apiKey);
224
+ }
225
+ export function getRentalYield(params, apiKey) {
226
+ return fetchApi("/api/v1/rental-yield", toQueryParams({
227
+ location: params.location,
228
+ teryt: params.teryt,
229
+ areaBucket: params.areaBucket,
230
+ }), apiKey);
231
+ }
232
+ export function getRentalYieldLocations(params, apiKey) {
233
+ return fetchApi("/api/v1/rental-yield/locations", toQueryParams({
234
+ search: params.search,
235
+ }), apiKey);
236
+ }
237
+ export function getPriceSpread(params, apiKey) {
238
+ return fetchApi("/api/v1/price-spread", toQueryParams({
239
+ location: params.location,
240
+ teryt: params.teryt,
241
+ marketType: params.marketType,
242
+ areaBucket: params.areaBucket,
243
+ }), apiKey);
244
+ }
245
+ export function getPriceSpreadLocations(params, apiKey) {
246
+ return fetchApi("/api/v1/price-spread/locations", toQueryParams({
247
+ search: params.search,
248
+ }), apiKey);
249
+ }
250
+ export function getValuation(params, apiKey) {
251
+ return fetchApi("/api/v1/valuations", toQueryParams({
252
+ lat: params.lat,
253
+ lng: params.lng,
254
+ parcelId: params.parcelId,
255
+ area: params.area,
256
+ rooms: params.rooms,
257
+ market: params.market,
258
+ includeComps: params.includeComps ? "true" : undefined,
259
+ }), apiKey);
165
260
  }
166
261
  export function getLocations(parent, apiKey) {
167
- return fetchApi("/api/locations", parent ? { parent } : undefined, apiKey);
262
+ return fetchApi("/api/v1/locations", parent ? { parent } : undefined, apiKey);
168
263
  }
169
264
  export function getPriceHistogram(bins = 20, max = 3_000_000, apiKey) {
170
- return fetchApi("/api/stats/price-histogram", toQueryParams({ bins, max }), apiKey);
265
+ return fetchApi("/api/v1/stats/price-histogram", toQueryParams({ bins, max }), apiKey);
171
266
  }
172
267
  export function searchParcels(q, limit, apiKey) {
173
- return fetchApi("/api/parcels/search", toQueryParams({ q, limit }), apiKey);
268
+ return fetchApi("/api/v1/parcels/search", toQueryParams({ q, limit }), apiKey);
269
+ }
270
+ export function resolveParcel(params, apiKey) {
271
+ return fetchApi("/api/v1/parcels/resolve", toQueryParams({
272
+ q: params.q,
273
+ parcelId: params.parcelId,
274
+ lat: params.lat,
275
+ lng: params.lng,
276
+ }), apiKey);
277
+ }
278
+ export function getParcelReport(parcelKey, apiKey) {
279
+ const urlKey = parcelKey.trim().replace(/\//g, "-");
280
+ return fetchApi(`/api/v1/parcels/${encodeURIComponent(urlKey)}/report`, undefined, apiKey);
174
281
  }
175
282
  export function searchByPolygon(p, apiKey) {
176
283
  const body = { polygon: p.polygon };
@@ -182,6 +289,8 @@ export function searchByPolygon(p, apiKey) {
182
289
  body.unitFunction = p.unitFunction;
183
290
  if (p.buildingType != null)
184
291
  body.buildingType = p.buildingType;
292
+ if (p.ownershipType != null)
293
+ body.ownershipType = p.ownershipType;
185
294
  if (p.mpzpDesignation)
186
295
  body.mpzpDesignation = p.mpzpDesignation;
187
296
  if (p.minPrice != null)
@@ -200,18 +309,29 @@ export function searchByPolygon(p, apiKey) {
200
309
  body.district = p.district;
201
310
  if (p.street)
202
311
  body.street = p.street;
312
+ if (p.transactionType)
313
+ body.transactionType = p.transactionType;
314
+ if (p.rooms)
315
+ body.rooms = p.rooms;
316
+ if (p.floor)
317
+ body.floor = p.floor;
203
318
  if (p.limit != null)
204
319
  body.limit = p.limit;
205
- return fetchApiPost("/api/transactions/spatial", body, apiKey);
320
+ return fetchApiPost("/api/v1/transactions/spatial", body, apiKey);
206
321
  }
207
322
  export function compareLocations(p, apiKey) {
208
- return fetchApi("/api/transactions/summary/compare", toQueryParams({
323
+ return fetchApi("/api/v1/transactions/summary/compare", toQueryParams({
209
324
  districts: p.districts,
325
+ include: p.include,
210
326
  propertyType: p.propertyType,
211
327
  marketType: p.marketType,
212
328
  unitFunction: p.unitFunction,
329
+ ownershipType: p.ownershipType,
213
330
  buildingType: p.buildingType,
214
331
  mpzpDesignation: p.mpzpDesignation,
332
+ transactionType: p.transactionType,
333
+ rooms: p.rooms,
334
+ floor: p.floor,
215
335
  minPrice: p.minPrice,
216
336
  maxPrice: p.maxPrice,
217
337
  dateFrom: p.dateFrom,
@@ -221,3 +341,30 @@ export function compareLocations(p, apiKey) {
221
341
  street: p.street,
222
342
  }), apiKey);
223
343
  }
344
+ export function getBuildingBreakdown(transactionId, apiKey) {
345
+ return fetchApi(`/api/v1/transactions/${transactionId}/buildings`, undefined, apiKey);
346
+ }
347
+ export function getTransactionFlood(transactionId, apiKey) {
348
+ return fetchApi(`/api/v1/transactions/${transactionId}/flood`, undefined, apiKey);
349
+ }
350
+ export function getTransactionHeritage(transactionId, apiKey) {
351
+ return fetchApi(`/api/v1/transactions/${transactionId}/heritage`, undefined, apiKey);
352
+ }
353
+ export function getTransactionLandslide(transactionId, apiKey) {
354
+ return fetchApi(`/api/v1/transactions/${transactionId}/landslide`, undefined, apiKey);
355
+ }
356
+ export function getTransactionSurroundings(transactionId, apiKey) {
357
+ return fetchApi(`/api/v1/transactions/${transactionId}/surroundings`, undefined, apiKey);
358
+ }
359
+ export function getTransactionTransit(transactionId, apiKey) {
360
+ return fetchApi(`/api/v1/transactions/${transactionId}/transit`, undefined, apiKey);
361
+ }
362
+ export function getTransactionPermits(transactionId, apiKey) {
363
+ return fetchApi(`/api/v1/transactions/${transactionId}/permits`, undefined, apiKey);
364
+ }
365
+ export function getTransactionPlanning(transactionId, apiKey) {
366
+ return fetchApi(`/api/v1/transactions/${transactionId}/planning`, undefined, apiKey);
367
+ }
368
+ export function getTransactionFarmland(transactionId, apiKey) {
369
+ return fetchApi(`/api/v1/transactions/${transactionId}/farmland`, undefined, apiKey);
370
+ }
@@ -1,29 +1,13 @@
1
1
  import { validateOAuthJwt, OAuthConfigError } from "./oauth-jwt.js";
2
2
  import { encodeOAuthCtx } from "./api-client.js";
3
3
  export const RESOURCE_METADATA = "https://mcp.cenogram.pl/.well-known/oauth-protected-resource";
4
- // RFC 6750 § 3 quoted-string alphabet: %x20-21 / %x23-5B / %x5D-7E (ASCII visible without " or \)
5
4
  const QUOTED_RE = /^[\x20\x21\x23-\x5B\x5D-\x7E]*$/;
6
- // RFC 7235 BWS (bad whitespace): SP / HTAB
7
5
  const BEARER_RE = /^Bearer[ \t]+(.+)$/i;
8
- // JWT shape: 3 base64url segments separated by dots, starting with eyJ
9
6
  const JWT_SHAPE_RE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
10
- // Log-unsafe codepoints (log poisoning via multi-byte UTF-8 controls):
11
- // \x00-\x1F ASCII C0 controls
12
- // \x7F-\x9F DEL + C1 controls (incl. U+0085 NEL = bytes 0xc2 0x85)
13
- // U+2028, U+2029 Line/Paragraph Separator (NOT escaped by JSON.stringify)
14
- // U+202A-U+202E BiDi controls LRE/RLE/PDF/LRO/RLO (Trojan Source attacks)
15
- // U+2066-U+2069 BiDi isolates LRI/RLI/FSI/PDI
16
- // U+FEFF Zero-Width No-Break Space / BOM
17
- // Goal: prevent log injection / terminal RTL render manipulation when user-controlled
18
- // bytes (kid in JWT header, suffix of cngrm_ API key) reach stderr JSON logs.
19
- // eslint-disable-next-line no-control-regex -- control chars in regex are the whole point
20
7
  const LOG_UNSAFE_RE = /[\x00-\x1F\x7F-\x9F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
21
8
  export function sanitizeForLog(s) {
22
9
  return s.replace(LOG_UNSAFE_RE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
23
10
  }
24
- // Static descriptions for WWW-Authenticate header - quoted-safe ASCII, no user input echoed.
25
- // Note: unknown_key and invalid intentionally share the same description to avoid leaking
26
- // validation timing/identity info on the wire (security: minimize info oracle).
27
11
  const REASON_DESC = {
28
12
  expired: "Token expired",
29
13
  unknown_key: "Token validation failed",
@@ -64,10 +48,6 @@ function build403(scope, description) {
64
48
  };
65
49
  }
66
50
  export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
67
- // RFC 6750 §3.1: oversized auth attempt → invalid_token (client tried auth, not missing).
68
- // 8192 B = below Node default http.maxHeaderSize (16KB), accommodates JWT with claims +
69
- // Bearer prefix (~700-1500 bytes for current claim set: sub, scope, grant_id, client_id).
70
- // Check before regex match to avoid ReDoS / memory on huge string.
71
51
  const MAX_AUTH_HEADER_BYTES = 8192;
72
52
  if (authHeader && Buffer.byteLength(authHeader, "utf-8") > MAX_AUTH_HEADER_BYTES) {
73
53
  return {
@@ -81,7 +61,6 @@ export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
81
61
  log: { evt: "auth.rejected", reason: "oversized_header", kid: null },
82
62
  };
83
63
  }
84
- // Parse Bearer token (RFC 7235: BWS = SP/HTAB)
85
64
  const match = authHeader?.match(BEARER_RE);
86
65
  const rawToken = match?.[1]?.trim() ?? "";
87
66
  if (!rawToken) {
@@ -94,7 +73,6 @@ export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
94
73
  log: { evt: "auth.rejected", reason: "missing", kid: null },
95
74
  };
96
75
  }
97
- // Fast path: API key (most common). Format validation delegated to upstream API.
98
76
  if (rawToken.startsWith("cngrm_")) {
99
77
  return {
100
78
  kind: "passthrough",
@@ -102,7 +80,6 @@ export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
102
80
  log: { evt: "auth.passthrough", mode: "api_key", key_prefix: sanitizeForLog(rawToken.slice(0, 10)) },
103
81
  };
104
82
  }
105
- // OAuth JWT shape check - reject anything that doesn't look like a JWT before crypto ops
106
83
  if (!JWT_SHAPE_RE.test(rawToken)) {
107
84
  return {
108
85
  kind: "401",
@@ -115,7 +92,6 @@ export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
115
92
  log: { evt: "auth.rejected", reason: "invalid_format", kid: null },
116
93
  };
117
94
  }
118
- // Validate JWT
119
95
  let result;
120
96
  try {
121
97
  result = await validateJwt(rawToken);
@@ -132,14 +108,12 @@ export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
132
108
  throw e;
133
109
  }
134
110
  if (!result.ok) {
135
- // Extract kid from header for logging (best-effort, not on wire)
136
111
  let headerKid = null;
137
112
  try {
138
113
  const headerJson = JSON.parse(Buffer.from(rawToken.split(".")[0], "base64url").toString("utf-8"));
139
114
  headerKid = safeKid(headerJson.kid);
140
115
  }
141
116
  catch {
142
- // ignore - kid logging is best-effort
143
117
  }
144
118
  return {
145
119
  kind: "401",
@@ -162,7 +136,6 @@ export async function dispatchAuth(authHeader, validateJwt = validateOAuthJwt) {
162
136
  return {
163
137
  kind: "passthrough",
164
138
  apiKey: encodeOAuthCtx(result.claims.sub, result.claims.grant_id),
165
- // Sanitize grant_id for log consistency with key_prefix/kid (defense in depth - JWT is signed but spec allows custom claims).
166
139
  log: { evt: "auth.passthrough", mode: "oauth", grant_id: sanitizeForLog(result.claims.grant_id) },
167
140
  };
168
141
  }
package/dist/client-id.js CHANGED
@@ -8,12 +8,10 @@ let cachedId = null;
8
8
  export function getClientId() {
9
9
  if (cachedId)
10
10
  return cachedId;
11
- // Allow override via env var
12
11
  if (process.env.CENOGRAM_CLIENT_ID?.trim()) {
13
12
  cachedId = process.env.CENOGRAM_CLIENT_ID.trim();
14
13
  return cachedId;
15
14
  }
16
- // Try reading persisted ID
17
15
  try {
18
16
  const stored = readFileSync(CLIENT_ID_FILE, "utf-8").trim();
19
17
  if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(stored)) {
@@ -22,16 +20,13 @@ export function getClientId() {
22
20
  }
23
21
  }
24
22
  catch {
25
- // File doesn't exist or isn't readable - generate new
26
23
  }
27
- // Generate and persist
28
24
  const id = randomUUID();
29
25
  try {
30
26
  mkdirSync(CONFIG_DIR, { recursive: true });
31
27
  writeFileSync(CLIENT_ID_FILE, id + "\n", { mode: 0o600 });
32
28
  }
33
29
  catch {
34
- // Read-only fs (Docker, sandbox) - use ephemeral ID
35
30
  }
36
31
  cachedId = id;
37
32
  return id;
@@ -1,8 +1,12 @@
1
1
  export type AuthMode = "oauth" | "api_key" | "stdio_env" | "none";
2
2
  export declare function getAuthMode(apiKey?: string): AuthMode;
3
+ export declare function signupUrl(): string;
3
4
  export interface ErrorBody {
4
5
  error?: string;
6
+ message?: string;
5
7
  currentBalance?: number;
6
8
  creditsRequired?: number;
9
+ upgrade?: string;
10
+ successor?: string;
7
11
  }
8
12
  export declare function authErrorMessage(status: number, mode: AuthMode, body?: ErrorBody): string;
@@ -1,4 +1,4 @@
1
- // SOH char (\x01) - must match OAUTH_CTX_PREFIX in api-client.ts
1
+ import { channelSrc } from "./transport-mode.js";
2
2
  const OAUTH_CTX_PREFIX = "\x01";
3
3
  export function getAuthMode(apiKey) {
4
4
  if (!apiKey)
@@ -9,28 +9,71 @@ export function getAuthMode(apiKey) {
9
9
  return "api_key";
10
10
  return "stdio_env";
11
11
  }
12
+ export function signupUrl() {
13
+ return `https://cenogram.pl/api?src=${channelSrc()}`;
14
+ }
15
+ function specificMessage(body) {
16
+ const rawError = typeof body.error === "string" ? body.error.trim() : "";
17
+ const rawMessage = typeof body.message === "string" ? body.message.trim() : "";
18
+ const candidate = rawMessage || rawError;
19
+ return candidate.length > 0 && candidate.length <= 500 ? candidate : "";
20
+ }
21
+ function sentence(text) {
22
+ return /[.!?]$/.test(text) ? text : `${text}.`;
23
+ }
24
+ function specificField(value) {
25
+ if (typeof value !== "string")
26
+ return "";
27
+ const trimmed = value.trim();
28
+ return trimmed.length > 0 && trimmed.length <= 500 ? trimmed : "";
29
+ }
12
30
  export function authErrorMessage(status, mode, body = {}) {
31
+ const specific = specificMessage(body);
13
32
  switch (status) {
14
33
  case 401:
15
34
  if (mode === "oauth") {
16
35
  return "Connection to Cenogram expired or was revoked. In Claude open: Settings > Connectors > Cenogram, disconnect and reconnect.";
17
36
  }
18
- return "API key rejected. Check https://cenogram.pl/api/keys if it's still active.";
37
+ if (mode === "none") {
38
+ return (`No Cenogram API key configured. Get a free key at ${signupUrl()}, ` +
39
+ "then set it as the CENOGRAM_API_KEY environment variable of this MCP server.");
40
+ }
41
+ return "API key rejected. Check https://cenogram.pl/ustawienia#api-keys if it's still active.";
19
42
  case 402: {
43
+ const explanation = specificField(body.message);
44
+ if (body.error === "trial_expired" && explanation) {
45
+ const upgrade = specificField(body.upgrade);
46
+ return upgrade && !explanation.includes(upgrade) ? `${sentence(explanation)} (${upgrade})` : explanation;
47
+ }
20
48
  const balance = body.currentBalance ?? 0;
21
49
  const required = body.creditsRequired ?? "?";
22
50
  if (mode === "oauth") {
23
- return `Insufficient credits (balance: ${balance}, query cost: ${required}). Top up: https://cenogram.pl/api#cennik`;
51
+ return `Insufficient credits (balance: ${balance}, query cost: ${required}). Top up: https://cenogram.pl/api?src=${channelSrc()}#cennik`;
24
52
  }
25
- return `Insufficient credits for key's account (balance: ${balance}, query cost: ${required}). Top up: https://cenogram.pl/api#cennik`;
53
+ return `Insufficient credits for key's account (balance: ${balance}, query cost: ${required}). Top up: https://cenogram.pl/api?src=${channelSrc()}#cennik`;
26
54
  }
27
55
  case 403:
28
56
  if (body.error === "email_not_verified") {
29
57
  return "Account email not verified. Check your inbox, click the activation link, then retry.";
30
58
  }
31
- return `Access denied (HTTP 403).`;
32
- case 503:
33
- return "Cenogram temporarily unavailable (maintenance mode). Try again shortly.";
59
+ return specific ? `Access denied: ${specific}` : "Access denied (HTTP 403).";
60
+ case 503: {
61
+ if (!specific)
62
+ return "Cenogram temporarily unavailable. Try again shortly.";
63
+ const saysRetry = /try again|retry|spr[oó]buj ponownie/i.test(specific);
64
+ return `Cenogram temporarily unavailable: ${sentence(specific)}${saysRetry ? "" : " Try again shortly."}`;
65
+ }
66
+ case 410: {
67
+ const successor = specificField(body.successor);
68
+ const reason = sentence(specific || "This endpoint has been retired.");
69
+ const where = successor ? ` It was replaced by ${successor}.` : "";
70
+ return `${reason}${where} This is permanent - retrying will not help. Update @cenogram/mcp-server to the latest version.`;
71
+ }
72
+ case 404:
73
+ return specific || "Not found (HTTP 404). Check the location name or TERYT code.";
74
+ case 400:
75
+ case 422:
76
+ return specific ? `Invalid request: ${specific}` : `Invalid request (HTTP ${status}). Check parameters.`;
34
77
  default:
35
78
  return `Cenogram API unavailable (HTTP ${status}). Try again shortly.`;
36
79
  }
@@ -1,4 +1,5 @@
1
- import type { Transaction, TransactionsResponse, TransactionsSummary, StatsResponse, PricePerM2Row, HistogramBin, ParcelSearchResponse, SpatialSearchResponse, CompareResponse, LocationItem } from "./api-client.js";
1
+ import type { Transaction, TransactionsResponse, TransactionsSummary, StatsResponse, PricePerM2Row, HistogramBin, ParcelSearchResponse, ParcelResolveResponse, SpatialSearchResponse, CompareResponse, LocationItem, RentalYieldResponse, RentalYieldLocationsResponse, PriceSpreadResponse, PriceSpreadLocationsResponse, ValuationResponse, BuildingBreakdownResponse, FloodBreakdownResponse, HeritageBreakdownResponse, LandslideBreakdownResponse, SurroundingsResponse, TransitBreakdownResponse, PermitsResponse, PlanningResponse, FarmlandResponse, DemographicsResponse, InfrastructureSignalsResponse, ParcelReportResponse } from "./api-client.js";
2
+ export declare const MARKET_CAVEAT = "Note: median/average prices are market-based \u2014 fractional ownership shares and non-market deeds (public tenders, foreclosures, privileged/subsidized sales) are excluded from price aggregates. Transaction counts and coverage stay complete.";
2
3
  export declare function formatPLN(value: number | null | undefined): string;
3
4
  export declare function formatArea(m2: number | null | undefined): string;
4
5
  export declare function formatNumber(value: number | null | undefined): string;
@@ -8,6 +9,24 @@ export declare function formatMarketOverview(stats: StatsResponse): string;
8
9
  export declare function formatPriceStats(rows: PricePerM2Row[], location?: string): string;
9
10
  export declare function formatHistogram(bins: HistogramBin[]): string;
10
11
  export declare function formatParcelResults(res: ParcelSearchResponse, query: string): string;
12
+ export declare function formatParcelResolve(res: ParcelResolveResponse): string;
11
13
  export declare function formatSpatialResults(res: SpatialSearchResponse): string;
14
+ export declare function formatBuildingBreakdown(res: BuildingBreakdownResponse): string;
15
+ export declare function formatFloodBreakdown(res: FloodBreakdownResponse): string;
16
+ export declare function formatHeritageBreakdown(res: HeritageBreakdownResponse): string;
17
+ export declare function formatLandslideBreakdown(res: LandslideBreakdownResponse): string;
18
+ export declare function formatSurroundings(res: SurroundingsResponse): string;
19
+ export declare function formatTransitBreakdown(res: TransitBreakdownResponse): string;
20
+ export declare function formatPermitsBreakdown(res: PermitsResponse): string;
21
+ export declare function formatPlanningBreakdown(res: PlanningResponse): string;
22
+ export declare function formatFarmland(res: FarmlandResponse): string;
12
23
  export declare function formatLocationHierarchy(items: LocationItem[], parent?: string): string;
13
24
  export declare function formatCompareResults(res: CompareResponse): string;
25
+ export declare function formatDemographics(r: DemographicsResponse): string;
26
+ export declare function formatInfrastructureSignals(r: InfrastructureSignalsResponse): string;
27
+ export declare function formatRentalYield(r: RentalYieldResponse): string;
28
+ export declare function formatRentalYieldLocations(r: RentalYieldLocationsResponse): string;
29
+ export declare function formatPriceSpread(r: PriceSpreadResponse): string;
30
+ export declare function formatPriceSpreadLocations(r: PriceSpreadLocationsResponse): string;
31
+ export declare function formatValuation(r: ValuationResponse): string;
32
+ export declare function formatParcelReport(res: ParcelReportResponse): string;