@cenogram/mcp-server 0.2.0 → 0.6.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.
- package/README.md +32 -10
- package/dist/api-client.d.ts +678 -7
- package/dist/api-client.js +169 -22
- package/dist/auth-dispatch.js +0 -27
- package/dist/client-id.js +0 -5
- package/dist/error-messages.d.ts +4 -0
- package/dist/error-messages.js +50 -7
- package/dist/formatters.d.ts +20 -1
- package/dist/formatters.js +1094 -21
- package/dist/index.d.ts +1 -0
- package/dist/index.js +73 -40
- package/dist/mappings.d.ts +12 -5
- package/dist/mappings.js +131 -14
- package/dist/oauth-jwt.js +1 -3
- package/dist/sentry-scrub.d.ts +2 -0
- package/dist/sentry-scrub.js +14 -0
- package/dist/sentry.d.ts +2 -0
- package/dist/sentry.js +29 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +487 -86
- package/dist/transport-mode.d.ts +2 -0
- package/dist/transport-mode.js +6 -0
- package/package.json +20 -7
package/dist/api-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
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
|
+
}
|
package/dist/auth-dispatch.js
CHANGED
|
@@ -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;
|
package/dist/error-messages.d.ts
CHANGED
|
@@ -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;
|
package/dist/error-messages.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/formatters.d.ts
CHANGED
|
@@ -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;
|