teryt-mcp 0.1.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 +96 -0
- package/dist/chunk-YEPFIU35.js +2434 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +127 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +6 -0
- package/package.json +47 -0
|
@@ -0,0 +1,2434 @@
|
|
|
1
|
+
// src/features/server-status/application/get-server-status.ts
|
|
2
|
+
function getServerStatus(input) {
|
|
3
|
+
return {
|
|
4
|
+
serverName: "teryt-mcp",
|
|
5
|
+
serverVersion: "0.1.0",
|
|
6
|
+
frameworkVersion: input.frameworkVersion,
|
|
7
|
+
transport: input.transport,
|
|
8
|
+
dataDir: input.dataDir,
|
|
9
|
+
database: {
|
|
10
|
+
status: "not_configured"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/features/server-status/mcp/server-status.tool.ts
|
|
16
|
+
import { defineTool, mcpCraftmanCoreVersion } from "@mcp-craftman/core";
|
|
17
|
+
function createServerStatusTool(input) {
|
|
18
|
+
return defineTool({
|
|
19
|
+
name: "server_status",
|
|
20
|
+
description: "Returns server runtime status.",
|
|
21
|
+
policy: "read",
|
|
22
|
+
returnsStructuredContent: true,
|
|
23
|
+
outputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
serverName: {
|
|
27
|
+
type: "string"
|
|
28
|
+
},
|
|
29
|
+
serverVersion: {
|
|
30
|
+
type: "string"
|
|
31
|
+
},
|
|
32
|
+
frameworkVersion: {
|
|
33
|
+
type: "string"
|
|
34
|
+
},
|
|
35
|
+
transport: {
|
|
36
|
+
type: "string",
|
|
37
|
+
enum: ["stdio", "http"]
|
|
38
|
+
},
|
|
39
|
+
dataDir: {
|
|
40
|
+
type: "string"
|
|
41
|
+
},
|
|
42
|
+
database: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
status: {
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: ["not_configured"]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ["status"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
required: ["serverName", "serverVersion", "frameworkVersion", "transport", "dataDir", "database"]
|
|
54
|
+
},
|
|
55
|
+
annotations: {
|
|
56
|
+
readOnlyHint: true
|
|
57
|
+
},
|
|
58
|
+
handler: () => ({
|
|
59
|
+
structuredContent: getServerStatus({
|
|
60
|
+
...input,
|
|
61
|
+
frameworkVersion: mcpCraftmanCoreVersion
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/app.ts
|
|
68
|
+
import { createMcpApp } from "@mcp-craftman/core";
|
|
69
|
+
import { loadRuntimeConfig } from "@mcp-craftman/node";
|
|
70
|
+
|
|
71
|
+
// src/features/get-place/infrastructure/in-memory-place-details-repository.ts
|
|
72
|
+
var fixturePlaces = [
|
|
73
|
+
{
|
|
74
|
+
id: "0009876",
|
|
75
|
+
name: "Boles\u0142awiec",
|
|
76
|
+
stateDate: "2026-01-01",
|
|
77
|
+
unitId: "02-01-01-1"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "0012345",
|
|
81
|
+
name: "Stara Wie\u015B",
|
|
82
|
+
stateDate: "2026-01-01",
|
|
83
|
+
unitId: "02-01-02-2"
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
var InMemoryPlaceDetailsRepository = class {
|
|
87
|
+
async getPlace(id) {
|
|
88
|
+
return fixturePlaces.find((place) => place.id === id) ?? null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/features/get-street/infrastructure/in-memory-street-details-repository.ts
|
|
93
|
+
var fixtureStreets = [
|
|
94
|
+
{
|
|
95
|
+
code: "0000123",
|
|
96
|
+
id: "0009876-0000123",
|
|
97
|
+
name: "Marsza\u0142kowska",
|
|
98
|
+
placeId: "0009876",
|
|
99
|
+
stateDate: "2026-01-01"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
code: "0000456",
|
|
103
|
+
id: "0009876-0000456",
|
|
104
|
+
name: "Rynek",
|
|
105
|
+
placeId: "0009876",
|
|
106
|
+
stateDate: "2026-01-01"
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
var InMemoryStreetDetailsRepository = class {
|
|
110
|
+
async getStreet(id) {
|
|
111
|
+
return fixtureStreets.find((street) => street.id === id || street.code === id) ?? null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/features/get-unit/infrastructure/in-memory-unit-details-repository.ts
|
|
116
|
+
var fixtureStateDate = "2026-01-01";
|
|
117
|
+
var fixtureUnits = [
|
|
118
|
+
{
|
|
119
|
+
id: "02",
|
|
120
|
+
name: "DOLNO\u015AL\u0104SKIE",
|
|
121
|
+
stateDate: fixtureStateDate,
|
|
122
|
+
type: "wojew\xF3dztwo"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "02-01-01-1",
|
|
126
|
+
name: "Boles\u0142awiec",
|
|
127
|
+
stateDate: fixtureStateDate,
|
|
128
|
+
type: "gmina miejska"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "02-01-02-2",
|
|
132
|
+
name: "Boles\u0142awiec",
|
|
133
|
+
stateDate: fixtureStateDate,
|
|
134
|
+
type: "gmina wiejska"
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
var InMemoryUnitDetailsRepository = class {
|
|
138
|
+
async getUnit(id) {
|
|
139
|
+
return fixtureUnits.find((unit) => unit.id === id) ?? null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/features/resolve-address/infrastructure/in-memory-address-repository.ts
|
|
144
|
+
var fixturePlace = {
|
|
145
|
+
id: "0009876",
|
|
146
|
+
name: "Boles\u0142awiec"
|
|
147
|
+
};
|
|
148
|
+
var fixtureStateDate2 = "2026-01-01";
|
|
149
|
+
var fixtureUnit = {
|
|
150
|
+
id: "02-01-01-1",
|
|
151
|
+
name: "Boles\u0142awiec",
|
|
152
|
+
type: "gmina miejska"
|
|
153
|
+
};
|
|
154
|
+
var fixtureAddresses = [
|
|
155
|
+
{
|
|
156
|
+
id: "0009876-0000123",
|
|
157
|
+
place: fixturePlace,
|
|
158
|
+
stateDate: fixtureStateDate2,
|
|
159
|
+
street: {
|
|
160
|
+
code: "0000123",
|
|
161
|
+
id: "0009876-0000123",
|
|
162
|
+
name: "Marsza\u0142kowska"
|
|
163
|
+
},
|
|
164
|
+
unit: fixtureUnit
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "0009876-0000456",
|
|
168
|
+
place: fixturePlace,
|
|
169
|
+
stateDate: fixtureStateDate2,
|
|
170
|
+
street: {
|
|
171
|
+
code: "0000456",
|
|
172
|
+
id: "0009876-0000456",
|
|
173
|
+
name: "Rynek"
|
|
174
|
+
},
|
|
175
|
+
unit: fixtureUnit
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
var InMemoryAddressRepository = class {
|
|
179
|
+
async listAddresses() {
|
|
180
|
+
return fixtureAddresses;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/features/search-places/infrastructure/in-memory-place-repository.ts
|
|
185
|
+
var fixtureStateDate3 = "2026-01-01";
|
|
186
|
+
var fixturePlaces2 = [
|
|
187
|
+
{
|
|
188
|
+
id: "0009876",
|
|
189
|
+
name: "Boles\u0142awiec",
|
|
190
|
+
stateDate: fixtureStateDate3,
|
|
191
|
+
unitId: "02-01-01-1"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "0011111",
|
|
195
|
+
name: "Krak\xF3w",
|
|
196
|
+
stateDate: fixtureStateDate3,
|
|
197
|
+
unitId: "12-61-01-1"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "0012222",
|
|
201
|
+
name: "Warszawa",
|
|
202
|
+
stateDate: fixtureStateDate3,
|
|
203
|
+
unitId: "14-65-01-1"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "0012345",
|
|
207
|
+
name: "Stara Wie\u015B",
|
|
208
|
+
stateDate: fixtureStateDate3,
|
|
209
|
+
unitId: "02-01-02-2"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "0013333",
|
|
213
|
+
name: "D\u0105browa",
|
|
214
|
+
stateDate: fixtureStateDate3,
|
|
215
|
+
unitId: "24-01-01-2"
|
|
216
|
+
}
|
|
217
|
+
];
|
|
218
|
+
var InMemoryPlaceRepository = class {
|
|
219
|
+
async listPlaces() {
|
|
220
|
+
return fixturePlaces2;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/features/search-streets/infrastructure/in-memory-street-repository.ts
|
|
225
|
+
var fixtureStreets2 = [
|
|
226
|
+
{
|
|
227
|
+
code: "0000123",
|
|
228
|
+
id: "0009876-0000123",
|
|
229
|
+
name: "Marsza\u0142kowska",
|
|
230
|
+
placeId: "0009876",
|
|
231
|
+
stateDate: "2026-01-01"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
code: "0000456",
|
|
235
|
+
id: "0009876-0000456",
|
|
236
|
+
name: "Rynek",
|
|
237
|
+
placeId: "0009876",
|
|
238
|
+
stateDate: "2026-01-01"
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
var InMemoryStreetRepository = class {
|
|
242
|
+
async listStreets() {
|
|
243
|
+
return fixtureStreets2;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/features/search-units/infrastructure/in-memory-unit-repository.ts
|
|
248
|
+
var fixtureStateDate4 = "2026-01-01";
|
|
249
|
+
var fixtureUnits2 = [
|
|
250
|
+
{
|
|
251
|
+
id: "02",
|
|
252
|
+
name: "DOLNO\u015AL\u0104SKIE",
|
|
253
|
+
stateDate: fixtureStateDate4,
|
|
254
|
+
type: "wojew\xF3dztwo"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "02-01-01-1",
|
|
258
|
+
name: "Boles\u0142awiec",
|
|
259
|
+
stateDate: fixtureStateDate4,
|
|
260
|
+
type: "gmina miejska"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "02-01-02-2",
|
|
264
|
+
name: "Boles\u0142awiec",
|
|
265
|
+
stateDate: fixtureStateDate4,
|
|
266
|
+
type: "gmina wiejska"
|
|
267
|
+
}
|
|
268
|
+
];
|
|
269
|
+
var InMemoryUnitRepository = class {
|
|
270
|
+
async listUnits() {
|
|
271
|
+
return fixtureUnits2;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/features/source-status/infrastructure/eteryt-source-catalog.ts
|
|
276
|
+
var SOURCE_URL = "https://eteryt.stat.gov.pl/eTeryt/";
|
|
277
|
+
var EterytSourceCatalog = class {
|
|
278
|
+
async listDatasets() {
|
|
279
|
+
return [
|
|
280
|
+
{
|
|
281
|
+
code: "TERC",
|
|
282
|
+
name: "Territorial units",
|
|
283
|
+
sourceUrl: SOURCE_URL
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
code: "SIMC",
|
|
287
|
+
name: "Localities",
|
|
288
|
+
sourceUrl: SOURCE_URL
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
code: "ULIC",
|
|
292
|
+
name: "Streets",
|
|
293
|
+
sourceUrl: SOURCE_URL
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
code: "WMRODZ",
|
|
297
|
+
name: "Locality type dictionary",
|
|
298
|
+
sourceUrl: SOURCE_URL
|
|
299
|
+
}
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/features/source-status/infrastructure/json-manifest-store.ts
|
|
305
|
+
import { readFile } from "fs/promises";
|
|
306
|
+
import { join } from "path";
|
|
307
|
+
var JsonManifestStore = class {
|
|
308
|
+
constructor(dataDir) {
|
|
309
|
+
this.dataDir = dataDir;
|
|
310
|
+
}
|
|
311
|
+
dataDir;
|
|
312
|
+
async getSnapshot(dataset) {
|
|
313
|
+
try {
|
|
314
|
+
const content = await readFile(join(this.dataDir, "source-manifest.json"), "utf8");
|
|
315
|
+
const manifest = JSON.parse(content);
|
|
316
|
+
return manifest.snapshots?.find((snapshot) => snapshot.dataset === dataset);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (isMissingFile(error)) {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function isMissingFile(error) {
|
|
326
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/features/sync-database/infrastructure/eteryt-source.ts
|
|
330
|
+
var DOWNLOAD_PAGE_URL = "https://eteryt.stat.gov.pl/eTeryt/rejestr_teryt/udostepnianie_danych/baza_teryt/uzytkownicy_indywidualni/pobieranie/pliki_pelne.aspx";
|
|
331
|
+
var eventTargets = {
|
|
332
|
+
SIMC: "ctl00$body$BSIMCUrzedowyPobierz",
|
|
333
|
+
TERC: "ctl00$body$BTERCAdresowyPobierz",
|
|
334
|
+
ULIC: "ctl00$body$BULICUrzedowyPobierz",
|
|
335
|
+
WMRODZ: "ctl00$body$BRodzMiejPobierz"
|
|
336
|
+
};
|
|
337
|
+
var EterytSource = class {
|
|
338
|
+
constructor(fetchFn = fetch) {
|
|
339
|
+
this.fetchFn = fetchFn;
|
|
340
|
+
}
|
|
341
|
+
fetchFn;
|
|
342
|
+
async download(dataset) {
|
|
343
|
+
const page = await this.fetchPage();
|
|
344
|
+
const content = await this.postDownload(page, eventTargets[dataset]);
|
|
345
|
+
return {
|
|
346
|
+
content,
|
|
347
|
+
dataset,
|
|
348
|
+
sourceUrl: `${DOWNLOAD_PAGE_URL}#${dataset}`,
|
|
349
|
+
stateDate: "unknown"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async fetchPage() {
|
|
353
|
+
const response = await this.fetchFn(DOWNLOAD_PAGE_URL);
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new Error(`Cannot load eTeryt download page: HTTP ${response.status}.`);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
cookies: extractCookies(response.headers),
|
|
359
|
+
html: await response.text()
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
async postDownload(page, eventTarget) {
|
|
363
|
+
const form = parseHiddenInputs(page.html);
|
|
364
|
+
form.set("__EVENTTARGET", eventTarget);
|
|
365
|
+
form.set("__EVENTARGUMENT", "");
|
|
366
|
+
const response = await this.fetchFn(DOWNLOAD_PAGE_URL, {
|
|
367
|
+
body: form,
|
|
368
|
+
headers: {
|
|
369
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
370
|
+
...page.cookies ? { cookie: page.cookies } : {}
|
|
371
|
+
},
|
|
372
|
+
method: "POST"
|
|
373
|
+
});
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`Cannot download eTeryt dataset: HTTP ${response.status}.`);
|
|
376
|
+
}
|
|
377
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
378
|
+
if (contentType.includes("text/html")) {
|
|
379
|
+
throw new Error("eTeryt returned HTML instead of a dataset file.");
|
|
380
|
+
}
|
|
381
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
function parseHiddenInputs(html) {
|
|
385
|
+
const form = new URLSearchParams();
|
|
386
|
+
for (const input of findInputTags(html)) {
|
|
387
|
+
const name = getAttribute(input, "name");
|
|
388
|
+
if (getAttribute(input, "type")?.toLowerCase() === "hidden" && name) {
|
|
389
|
+
form.set(name, decodeHtmlEntities(getAttribute(input, "value") ?? ""));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return form;
|
|
393
|
+
}
|
|
394
|
+
function findInputTags(html) {
|
|
395
|
+
const inputs = [];
|
|
396
|
+
let searchFrom = 0;
|
|
397
|
+
let start = html.toLowerCase().indexOf("<input", searchFrom);
|
|
398
|
+
while (start >= 0) {
|
|
399
|
+
const end = html.indexOf(">", start);
|
|
400
|
+
if (end < 0) {
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
inputs.push(html.slice(start, end + 1));
|
|
404
|
+
searchFrom = end + 1;
|
|
405
|
+
start = html.toLowerCase().indexOf("<input", searchFrom);
|
|
406
|
+
}
|
|
407
|
+
return inputs;
|
|
408
|
+
}
|
|
409
|
+
function getAttribute(input, name) {
|
|
410
|
+
const marker = `${name}=`;
|
|
411
|
+
const markerIndex = input.toLowerCase().indexOf(marker.toLowerCase());
|
|
412
|
+
if (markerIndex < 0) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const quote = input[markerIndex + marker.length];
|
|
416
|
+
if (quote !== '"' && quote !== "'") {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const valueStart = markerIndex + marker.length + 1;
|
|
420
|
+
const valueEnd = input.indexOf(quote, valueStart);
|
|
421
|
+
if (valueEnd < 0) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
return input.slice(valueStart, valueEnd);
|
|
425
|
+
}
|
|
426
|
+
function decodeHtmlEntities(value) {
|
|
427
|
+
return value.replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
428
|
+
}
|
|
429
|
+
function extractCookies(headers) {
|
|
430
|
+
const rawCookies = headers.get("set-cookie");
|
|
431
|
+
if (!rawCookies) {
|
|
432
|
+
return "";
|
|
433
|
+
}
|
|
434
|
+
return splitSetCookieHeader(rawCookies).map((cookie) => cookie.split(";")[0]?.trim()).filter((cookie) => cookie).join("; ");
|
|
435
|
+
}
|
|
436
|
+
function splitSetCookieHeader(value) {
|
|
437
|
+
const cookies = [];
|
|
438
|
+
let start = 0;
|
|
439
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
440
|
+
if (value[index] !== "," || !isCookieBoundary(value, index + 1)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
cookies.push(value.slice(start, index));
|
|
444
|
+
start = index + 1;
|
|
445
|
+
}
|
|
446
|
+
cookies.push(value.slice(start));
|
|
447
|
+
return cookies;
|
|
448
|
+
}
|
|
449
|
+
function isCookieBoundary(value, start) {
|
|
450
|
+
let index = start;
|
|
451
|
+
while (value[index] === " ") {
|
|
452
|
+
index += 1;
|
|
453
|
+
}
|
|
454
|
+
const semicolonIndex = value.indexOf(";", index);
|
|
455
|
+
const equalsIndex = value.indexOf("=", index);
|
|
456
|
+
return equalsIndex >= 0 && (semicolonIndex < 0 || equalsIndex < semicolonIndex);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/features/sync-database/infrastructure/file-lock-store.ts
|
|
460
|
+
import { join as join2 } from "path";
|
|
461
|
+
import { withLock } from "@mcp-craftman/node";
|
|
462
|
+
var FileLockStore = class {
|
|
463
|
+
constructor(dataDir) {
|
|
464
|
+
this.dataDir = dataDir;
|
|
465
|
+
}
|
|
466
|
+
dataDir;
|
|
467
|
+
async withSyncLock(callback) {
|
|
468
|
+
return withLock(join2(this.dataDir, "sync.lock"), callback);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// src/features/sync-database/infrastructure/json-manifest-store.ts
|
|
473
|
+
import { join as join3 } from "path";
|
|
474
|
+
import { atomicWrite } from "@mcp-craftman/node";
|
|
475
|
+
var JsonSyncManifestStore = class {
|
|
476
|
+
constructor(dataDir) {
|
|
477
|
+
this.dataDir = dataDir;
|
|
478
|
+
}
|
|
479
|
+
dataDir;
|
|
480
|
+
async writeSnapshot(snapshot) {
|
|
481
|
+
await atomicWrite(join3(this.dataDir, "sync-manifest.json"), `${JSON.stringify(snapshot, null, 2)}
|
|
482
|
+
`);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/features/sync-database/infrastructure/local-file-store.ts
|
|
487
|
+
import { access } from "fs/promises";
|
|
488
|
+
import { join as join4 } from "path";
|
|
489
|
+
import { atomicWrite as atomicWrite2 } from "@mcp-craftman/node";
|
|
490
|
+
var LocalFileStore = class {
|
|
491
|
+
constructor(dataDir) {
|
|
492
|
+
this.dataDir = dataDir;
|
|
493
|
+
}
|
|
494
|
+
dataDir;
|
|
495
|
+
async databaseExists() {
|
|
496
|
+
try {
|
|
497
|
+
await access(this.databasePath);
|
|
498
|
+
return true;
|
|
499
|
+
} catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async swapDatabase(content) {
|
|
504
|
+
await atomicWrite2(this.databasePath, content);
|
|
505
|
+
return this.databasePath;
|
|
506
|
+
}
|
|
507
|
+
get databasePath() {
|
|
508
|
+
return join4(this.dataDir, "teryt.sqlite");
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/features/sync-database/infrastructure/sqlite-database-builder.ts
|
|
513
|
+
import initSqlJs from "sql.js";
|
|
514
|
+
|
|
515
|
+
// src/features/sync-database/application/importers/teryt-source-file.ts
|
|
516
|
+
import { TextDecoder as TextDecoder2 } from "util";
|
|
517
|
+
|
|
518
|
+
// src/features/sync-database/application/importers/teryt-csv.ts
|
|
519
|
+
import { createHash } from "crypto";
|
|
520
|
+
var requiredColumns = {
|
|
521
|
+
SIMC: ["WOJ", "POW", "GMI", "RODZ_GMI", "RM", "MZ", "NAZWA", "SYM", "SYMPOD", "STAN_NA"],
|
|
522
|
+
TERC: ["WOJ", "POW", "GMI", "RODZ", "NAZWA", "NAZDOD", "STAN_NA"],
|
|
523
|
+
ULIC: ["WOJ", "POW", "GMI", "RODZ_GMI", "SYM", "SYM_UL", "CECHA", "NAZWA_1", "NAZWA_2", "STAN_NA"],
|
|
524
|
+
WMRODZ: ["RM", "NAZWA_RM", "STAN_NA"]
|
|
525
|
+
};
|
|
526
|
+
var detectionColumns = {
|
|
527
|
+
SIMC: ["SYM", "SYMPOD", "RM"],
|
|
528
|
+
TERC: ["RODZ"],
|
|
529
|
+
ULIC: ["SYM_UL", "CECHA", "NAZWA_1"],
|
|
530
|
+
WMRODZ: ["NAZWA_RM"]
|
|
531
|
+
};
|
|
532
|
+
function importTerytCsv(content, options = {}) {
|
|
533
|
+
const [headerLine, ...recordLines] = content.trim().split(/\r?\n/);
|
|
534
|
+
if (!headerLine) {
|
|
535
|
+
throw new Error("TERYT CSV is empty.");
|
|
536
|
+
}
|
|
537
|
+
validateSha256(content, options.expectedSha256);
|
|
538
|
+
const columns = parseCsvLine(headerLine);
|
|
539
|
+
const dataset = detectDataset(columns);
|
|
540
|
+
validateColumns(dataset, columns);
|
|
541
|
+
const rows = recordLines.filter(Boolean).map((line) => {
|
|
542
|
+
const values = parseCsvLine(line);
|
|
543
|
+
return {
|
|
544
|
+
values: Object.fromEntries(columns.map((column, index) => [column, values[index] ?? ""])),
|
|
545
|
+
dataset
|
|
546
|
+
};
|
|
547
|
+
});
|
|
548
|
+
const stateDate = validateStateDate(dataset, rows);
|
|
549
|
+
validateRecordCount(dataset, rows.length, options.minRecordCount);
|
|
550
|
+
return {
|
|
551
|
+
recordCount: rows.length,
|
|
552
|
+
columns,
|
|
553
|
+
dataset,
|
|
554
|
+
rows,
|
|
555
|
+
stateDate
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function detectDataset(columns) {
|
|
559
|
+
const columnSet = new Set(columns);
|
|
560
|
+
const matches = Object.entries(detectionColumns).filter(
|
|
561
|
+
([, required]) => required.every((column) => columnSet.has(column))
|
|
562
|
+
);
|
|
563
|
+
if (matches.length !== 1) {
|
|
564
|
+
throw new Error(`Cannot detect TERYT dataset from columns: ${columns.join(", ")}`);
|
|
565
|
+
}
|
|
566
|
+
return matches[0][0];
|
|
567
|
+
}
|
|
568
|
+
function validateColumns(dataset, columns) {
|
|
569
|
+
const columnSet = new Set(columns);
|
|
570
|
+
const missing = requiredColumns[dataset].filter((column) => !columnSet.has(column));
|
|
571
|
+
if (missing.length > 0) {
|
|
572
|
+
throw new Error(`Missing ${dataset} columns: ${missing.join(", ")}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function validateStateDate(dataset, rows) {
|
|
576
|
+
const stateDates = new Set(rows.map((row) => row.values.STAN_NA).filter(Boolean));
|
|
577
|
+
if (stateDates.size !== 1) {
|
|
578
|
+
throw new Error(`${dataset} must contain exactly one STAN_NA value.`);
|
|
579
|
+
}
|
|
580
|
+
const [stateDate] = stateDates;
|
|
581
|
+
if (!stateDate || !isIsoDate(stateDate)) {
|
|
582
|
+
throw new Error(`${dataset} has invalid STAN_NA value.`);
|
|
583
|
+
}
|
|
584
|
+
return stateDate;
|
|
585
|
+
}
|
|
586
|
+
function validateRecordCount(dataset, recordCount, minRecordCount = 1) {
|
|
587
|
+
if (recordCount < minRecordCount) {
|
|
588
|
+
throw new Error(`${dataset} recordCount ${recordCount} is below minimum ${minRecordCount}.`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function validateSha256(content, expectedSha256) {
|
|
592
|
+
if (!expectedSha256) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const actualSha256 = createHash("sha256").update(content).digest("hex");
|
|
596
|
+
if (actualSha256 !== expectedSha256) {
|
|
597
|
+
throw new Error(`TERYT CSV sha256 mismatch: expected ${expectedSha256}, got ${actualSha256}.`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function parseCsvLine(line) {
|
|
601
|
+
const delimiter = line.includes(";") ? ";" : ",";
|
|
602
|
+
const values = [];
|
|
603
|
+
let current = "";
|
|
604
|
+
let quoted = false;
|
|
605
|
+
for (const character of line) {
|
|
606
|
+
if (character === '"') {
|
|
607
|
+
quoted = !quoted;
|
|
608
|
+
} else if (character === delimiter && !quoted) {
|
|
609
|
+
values.push(current);
|
|
610
|
+
current = "";
|
|
611
|
+
} else {
|
|
612
|
+
current += character;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
values.push(current);
|
|
616
|
+
return values;
|
|
617
|
+
}
|
|
618
|
+
function isIsoDate(value) {
|
|
619
|
+
const [year, month, day] = value.split("-");
|
|
620
|
+
return isNumberSegment(year, 4) && isNumberSegment(month, 2) && isNumberSegment(day, 2);
|
|
621
|
+
}
|
|
622
|
+
function isNumberSegment(value, length) {
|
|
623
|
+
return value?.length === length && [...value].every((character) => character >= "0" && character <= "9");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/features/sync-database/application/importers/teryt-zip.ts
|
|
627
|
+
import { unzipSync } from "fflate";
|
|
628
|
+
function importTerytZip(content) {
|
|
629
|
+
const entries = unzipSync(content);
|
|
630
|
+
const csvEntry = Object.entries(entries).find(([name]) => name.toLowerCase().endsWith(".csv"));
|
|
631
|
+
if (!csvEntry) {
|
|
632
|
+
throw new Error("TERYT ZIP does not contain a CSV file.");
|
|
633
|
+
}
|
|
634
|
+
const [, csvContent] = csvEntry;
|
|
635
|
+
return importTerytCsv(new TextDecoder().decode(csvContent));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/features/sync-database/application/importers/teryt-source-file.ts
|
|
639
|
+
var decoder = new TextDecoder2();
|
|
640
|
+
function importTerytSourceFile(sourceFile) {
|
|
641
|
+
if (isZip(sourceFile.content)) {
|
|
642
|
+
return importTerytZip(sourceFile.content);
|
|
643
|
+
}
|
|
644
|
+
return importTerytCsv(decoder.decode(sourceFile.content));
|
|
645
|
+
}
|
|
646
|
+
function isZip(content) {
|
|
647
|
+
return content[0] === 80 && content[1] === 75;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/features/sync-database/application/importers/teryt-relations.ts
|
|
651
|
+
function validateTerytRelations(imports) {
|
|
652
|
+
const simcRows = getRows(imports, "SIMC");
|
|
653
|
+
const tercUnitIds = new Set(getRows(imports, "TERC").map((row) => createUnitId(row.values)));
|
|
654
|
+
const simcIds = new Set(simcRows.map((row) => row.values.SYM).filter(Boolean));
|
|
655
|
+
validateSimcUnits(simcRows, tercUnitIds);
|
|
656
|
+
validateUlicPlaces(getRows(imports, "ULIC"), simcIds);
|
|
657
|
+
}
|
|
658
|
+
function validateSimcUnits(simcRows, tercUnitIds) {
|
|
659
|
+
for (const row of simcRows) {
|
|
660
|
+
const unitId = createUnitId(row.values);
|
|
661
|
+
if (!tercUnitIds.has(unitId)) {
|
|
662
|
+
throw new Error(`SIMC ${row.values.SYM ?? "<unknown>"} references missing TERC unit ${unitId}.`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function validateUlicPlaces(ulicRows, simcIds) {
|
|
667
|
+
for (const row of ulicRows) {
|
|
668
|
+
const placeId = row.values.SYM;
|
|
669
|
+
if (!placeId || !simcIds.has(placeId)) {
|
|
670
|
+
throw new Error(`ULIC ${row.values.SYM_UL ?? "<unknown>"} references missing SIMC place ${placeId ?? "<empty>"}.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function getRows(imports, dataset) {
|
|
675
|
+
return imports.find((item) => item.dataset === dataset)?.rows ?? [];
|
|
676
|
+
}
|
|
677
|
+
function createUnitId(values) {
|
|
678
|
+
return [values.WOJ ?? "", values.POW ?? "", values.GMI ?? "", values.RODZ ?? values.RODZ_GMI ?? ""].join("-");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/features/sync-database/application/importers/sqlite-schema.ts
|
|
682
|
+
var terytSqliteSchema = [
|
|
683
|
+
`CREATE TABLE raw_terc (
|
|
684
|
+
WOJ TEXT,
|
|
685
|
+
POW TEXT,
|
|
686
|
+
GMI TEXT,
|
|
687
|
+
RODZ TEXT,
|
|
688
|
+
NAZWA TEXT,
|
|
689
|
+
NAZDOD TEXT,
|
|
690
|
+
STAN_NA TEXT
|
|
691
|
+
)`,
|
|
692
|
+
`CREATE TABLE raw_simc (
|
|
693
|
+
WOJ TEXT,
|
|
694
|
+
POW TEXT,
|
|
695
|
+
GMI TEXT,
|
|
696
|
+
RODZ_GMI TEXT,
|
|
697
|
+
RM TEXT,
|
|
698
|
+
MZ TEXT,
|
|
699
|
+
NAZWA TEXT,
|
|
700
|
+
SYM TEXT,
|
|
701
|
+
SYMPOD TEXT,
|
|
702
|
+
STAN_NA TEXT
|
|
703
|
+
)`,
|
|
704
|
+
`CREATE TABLE raw_ulic (
|
|
705
|
+
WOJ TEXT,
|
|
706
|
+
POW TEXT,
|
|
707
|
+
GMI TEXT,
|
|
708
|
+
RODZ_GMI TEXT,
|
|
709
|
+
SYM TEXT,
|
|
710
|
+
SYM_UL TEXT,
|
|
711
|
+
CECHA TEXT,
|
|
712
|
+
NAZWA_1 TEXT,
|
|
713
|
+
NAZWA_2 TEXT,
|
|
714
|
+
STAN_NA TEXT
|
|
715
|
+
)`,
|
|
716
|
+
`CREATE TABLE raw_wmrodz (
|
|
717
|
+
RM TEXT,
|
|
718
|
+
NAZWA_RM TEXT,
|
|
719
|
+
STAN_NA TEXT
|
|
720
|
+
)`,
|
|
721
|
+
`CREATE TABLE units (
|
|
722
|
+
id TEXT PRIMARY KEY,
|
|
723
|
+
WOJ TEXT,
|
|
724
|
+
POW TEXT,
|
|
725
|
+
GMI TEXT,
|
|
726
|
+
RODZ TEXT,
|
|
727
|
+
name TEXT,
|
|
728
|
+
type TEXT,
|
|
729
|
+
stateDate TEXT
|
|
730
|
+
)`,
|
|
731
|
+
`CREATE TABLE places (
|
|
732
|
+
id TEXT PRIMARY KEY,
|
|
733
|
+
SYM TEXT,
|
|
734
|
+
SYMPOD TEXT,
|
|
735
|
+
RM TEXT,
|
|
736
|
+
name TEXT,
|
|
737
|
+
unitId TEXT,
|
|
738
|
+
stateDate TEXT
|
|
739
|
+
)`,
|
|
740
|
+
`CREATE TABLE streets (
|
|
741
|
+
id TEXT PRIMARY KEY,
|
|
742
|
+
SYM TEXT,
|
|
743
|
+
SYM_UL TEXT,
|
|
744
|
+
name TEXT,
|
|
745
|
+
placeId TEXT,
|
|
746
|
+
stateDate TEXT
|
|
747
|
+
)`,
|
|
748
|
+
`CREATE TABLE metadata (
|
|
749
|
+
key TEXT PRIMARY KEY,
|
|
750
|
+
value TEXT NOT NULL
|
|
751
|
+
)`,
|
|
752
|
+
"CREATE VIRTUAL TABLE units_fts USING fts5(name, content='units', content_rowid='rowid')",
|
|
753
|
+
"CREATE VIRTUAL TABLE places_fts USING fts5(name, content='places', content_rowid='rowid')",
|
|
754
|
+
"CREATE VIRTUAL TABLE streets_fts USING fts5(name, content='streets', content_rowid='rowid')"
|
|
755
|
+
];
|
|
756
|
+
|
|
757
|
+
// src/features/sync-database/infrastructure/sqlite-search-tables.ts
|
|
758
|
+
function insertSearchTables(db, imports) {
|
|
759
|
+
const terc = findImport(imports, "TERC");
|
|
760
|
+
const simc = findImport(imports, "SIMC");
|
|
761
|
+
const ulic = findImport(imports, "ULIC");
|
|
762
|
+
insertUnits(db, terc.rows);
|
|
763
|
+
insertPlaces(db, simc.rows);
|
|
764
|
+
insertStreets(db, ulic.rows);
|
|
765
|
+
db.run("INSERT INTO units_fts(rowid, name) SELECT rowid, name FROM units");
|
|
766
|
+
db.run("INSERT INTO places_fts(rowid, name) SELECT rowid, name FROM places");
|
|
767
|
+
db.run("INSERT INTO streets_fts(rowid, name) SELECT rowid, name FROM streets");
|
|
768
|
+
}
|
|
769
|
+
function insertUnits(db, rows) {
|
|
770
|
+
const statement = db.prepare(
|
|
771
|
+
"INSERT INTO units (id, WOJ, POW, GMI, RODZ, name, type, stateDate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
772
|
+
);
|
|
773
|
+
try {
|
|
774
|
+
for (const row of rows) {
|
|
775
|
+
statement.run([
|
|
776
|
+
createUnitId2(row.values),
|
|
777
|
+
row.values.WOJ ?? "",
|
|
778
|
+
row.values.POW ?? "",
|
|
779
|
+
row.values.GMI ?? "",
|
|
780
|
+
row.values.RODZ ?? "",
|
|
781
|
+
row.values.NAZWA ?? "",
|
|
782
|
+
row.values.NAZDOD ?? "",
|
|
783
|
+
row.values.STAN_NA ?? ""
|
|
784
|
+
]);
|
|
785
|
+
}
|
|
786
|
+
} finally {
|
|
787
|
+
statement.free();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function insertPlaces(db, rows) {
|
|
791
|
+
const statement = db.prepare(
|
|
792
|
+
"INSERT INTO places (id, SYM, SYMPOD, RM, name, unitId, stateDate) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
793
|
+
);
|
|
794
|
+
try {
|
|
795
|
+
for (const row of rows) {
|
|
796
|
+
statement.run([
|
|
797
|
+
row.values.SYM ?? "",
|
|
798
|
+
row.values.SYM ?? "",
|
|
799
|
+
row.values.SYMPOD ?? "",
|
|
800
|
+
row.values.RM ?? "",
|
|
801
|
+
row.values.NAZWA ?? "",
|
|
802
|
+
createUnitId2(row.values),
|
|
803
|
+
row.values.STAN_NA ?? ""
|
|
804
|
+
]);
|
|
805
|
+
}
|
|
806
|
+
} finally {
|
|
807
|
+
statement.free();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function insertStreets(db, rows) {
|
|
811
|
+
const statement = db.prepare(
|
|
812
|
+
"INSERT INTO streets (id, SYM, SYM_UL, name, placeId, stateDate) VALUES (?, ?, ?, ?, ?, ?)"
|
|
813
|
+
);
|
|
814
|
+
try {
|
|
815
|
+
for (const row of rows) {
|
|
816
|
+
statement.run([
|
|
817
|
+
`${row.values.SYM ?? ""}-${row.values.SYM_UL ?? ""}`,
|
|
818
|
+
row.values.SYM ?? "",
|
|
819
|
+
row.values.SYM_UL ?? "",
|
|
820
|
+
[row.values.CECHA, row.values.NAZWA_1, row.values.NAZWA_2].filter(Boolean).join(" "),
|
|
821
|
+
row.values.SYM ?? "",
|
|
822
|
+
row.values.STAN_NA ?? ""
|
|
823
|
+
]);
|
|
824
|
+
}
|
|
825
|
+
} finally {
|
|
826
|
+
statement.free();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function findImport(imports, dataset) {
|
|
830
|
+
const imported = imports.find((item) => item.dataset === dataset);
|
|
831
|
+
if (!imported) {
|
|
832
|
+
throw new Error(`Missing ${dataset} import.`);
|
|
833
|
+
}
|
|
834
|
+
return imported;
|
|
835
|
+
}
|
|
836
|
+
function createUnitId2(values) {
|
|
837
|
+
return [values.WOJ ?? "", values.POW ?? "", values.GMI ?? "", values.RODZ ?? values.RODZ_GMI ?? ""].join("-");
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/features/sync-database/infrastructure/sqlite-database-builder.ts
|
|
841
|
+
var SqliteDatabaseBuilder = class {
|
|
842
|
+
async build(sourceFiles) {
|
|
843
|
+
const SQL = await loadSqlJs();
|
|
844
|
+
const db = new SQL.Database();
|
|
845
|
+
try {
|
|
846
|
+
const imports = sourceFiles.map((sourceFile) => importTerytSourceFile(sourceFile));
|
|
847
|
+
validateTerytRelations(imports);
|
|
848
|
+
createSchema(db);
|
|
849
|
+
db.run("BEGIN");
|
|
850
|
+
insertRawDatasets(db, imports);
|
|
851
|
+
insertSearchTables(db, imports);
|
|
852
|
+
insertMetadata(db, imports);
|
|
853
|
+
db.run("COMMIT");
|
|
854
|
+
return {
|
|
855
|
+
content: db.export()
|
|
856
|
+
};
|
|
857
|
+
} catch (error) {
|
|
858
|
+
rollback(db);
|
|
859
|
+
throw error;
|
|
860
|
+
} finally {
|
|
861
|
+
db.close();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
var sqlJs = null;
|
|
866
|
+
function loadSqlJs() {
|
|
867
|
+
sqlJs ??= initSqlJs();
|
|
868
|
+
return sqlJs;
|
|
869
|
+
}
|
|
870
|
+
function createSchema(db) {
|
|
871
|
+
for (const statement of terytSqliteSchema) {
|
|
872
|
+
runSchemaStatement(db, statement);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function runSchemaStatement(db, statement) {
|
|
876
|
+
try {
|
|
877
|
+
db.run(statement);
|
|
878
|
+
} catch (error) {
|
|
879
|
+
const fallback = createFtsFallbackStatement(statement, error);
|
|
880
|
+
if (!fallback) {
|
|
881
|
+
throw error;
|
|
882
|
+
}
|
|
883
|
+
db.run(fallback);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function createFtsFallbackStatement(statement, error) {
|
|
887
|
+
if (!(error instanceof Error) || !error.message.includes("no such module: fts5")) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
const prefix = "CREATE VIRTUAL TABLE ";
|
|
891
|
+
if (!statement.startsWith(prefix)) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
const tableNameEnd = statement.indexOf(" USING fts5(");
|
|
895
|
+
if (tableNameEnd < prefix.length) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
const firstColumnStart = tableNameEnd + " USING fts5(".length;
|
|
899
|
+
const firstColumnEnd = statement.indexOf(",", firstColumnStart);
|
|
900
|
+
const tableName = statement.slice(prefix.length, tableNameEnd);
|
|
901
|
+
const firstColumn = statement.slice(firstColumnStart, firstColumnEnd < 0 ? void 0 : firstColumnEnd).trim();
|
|
902
|
+
return tableName && firstColumn ? `CREATE TABLE ${tableName} (${firstColumn} TEXT)` : null;
|
|
903
|
+
}
|
|
904
|
+
function insertRawDatasets(db, imports) {
|
|
905
|
+
for (const imported of imports) {
|
|
906
|
+
insertRows(db, `raw_${imported.dataset.toLowerCase()}`, imported.columns, imported.rows);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function insertMetadata(db, imports) {
|
|
910
|
+
const metadata = [
|
|
911
|
+
["datasetCount", String(imports.length)],
|
|
912
|
+
["stateDates", imports.map((item) => `${item.dataset}:${item.stateDate}`).join(",")]
|
|
913
|
+
];
|
|
914
|
+
for (const [key, value] of metadata) {
|
|
915
|
+
db.run("INSERT INTO metadata (key, value) VALUES (?, ?)", [key, value]);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function insertRows(db, table, columns, rows) {
|
|
919
|
+
const columnList = columns.join(", ");
|
|
920
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
921
|
+
const statement = db.prepare(`INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`);
|
|
922
|
+
try {
|
|
923
|
+
for (const row of rows) {
|
|
924
|
+
statement.run(columns.map((column) => row.values[column] ?? ""));
|
|
925
|
+
}
|
|
926
|
+
} finally {
|
|
927
|
+
statement.free();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function rollback(db) {
|
|
931
|
+
try {
|
|
932
|
+
db.run("ROLLBACK");
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/mcp/registry.ts
|
|
938
|
+
import { createCapabilityRegistry } from "@mcp-craftman/core";
|
|
939
|
+
|
|
940
|
+
// src/features/get-place/application/get-place.ts
|
|
941
|
+
async function getPlace(input, dependencies) {
|
|
942
|
+
const id = input.id.trim();
|
|
943
|
+
if (!id) {
|
|
944
|
+
throw new Error("get_place requires id.");
|
|
945
|
+
}
|
|
946
|
+
const place = await dependencies.placeDetailsRepository.getPlace(id);
|
|
947
|
+
return {
|
|
948
|
+
place,
|
|
949
|
+
stateDate: place?.stateDate ?? null
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/features/get-place/mcp/get-place.tool.ts
|
|
954
|
+
import { defineTool as defineTool2 } from "@mcp-craftman/core";
|
|
955
|
+
function createGetPlaceTool(dependencies) {
|
|
956
|
+
return defineTool2({
|
|
957
|
+
name: "get_place",
|
|
958
|
+
description: "Gets a TERYT place by identifier.",
|
|
959
|
+
inputSchema: {
|
|
960
|
+
type: "object",
|
|
961
|
+
properties: {
|
|
962
|
+
id: {
|
|
963
|
+
type: "string"
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
required: ["id"]
|
|
967
|
+
},
|
|
968
|
+
outputSchema: {
|
|
969
|
+
type: "object",
|
|
970
|
+
properties: {
|
|
971
|
+
place: {
|
|
972
|
+
anyOf: [
|
|
973
|
+
{
|
|
974
|
+
type: "object",
|
|
975
|
+
properties: {
|
|
976
|
+
id: {
|
|
977
|
+
type: "string"
|
|
978
|
+
},
|
|
979
|
+
name: {
|
|
980
|
+
type: "string"
|
|
981
|
+
},
|
|
982
|
+
stateDate: {
|
|
983
|
+
type: "string"
|
|
984
|
+
},
|
|
985
|
+
unitId: {
|
|
986
|
+
type: "string"
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
required: ["id", "name", "stateDate", "unitId"]
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
type: "null"
|
|
993
|
+
}
|
|
994
|
+
]
|
|
995
|
+
},
|
|
996
|
+
stateDate: {
|
|
997
|
+
anyOf: [
|
|
998
|
+
{
|
|
999
|
+
type: "string"
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
type: "null"
|
|
1003
|
+
}
|
|
1004
|
+
]
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
required: ["place", "stateDate"]
|
|
1008
|
+
},
|
|
1009
|
+
policy: "read",
|
|
1010
|
+
returnsStructuredContent: true,
|
|
1011
|
+
annotations: {
|
|
1012
|
+
readOnlyHint: true
|
|
1013
|
+
},
|
|
1014
|
+
handler: async (input) => ({
|
|
1015
|
+
structuredContent: await getPlace(parseInput(input), dependencies)
|
|
1016
|
+
})
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function parseInput(input) {
|
|
1020
|
+
if (typeof input !== "object" || input === null || !("id" in input) || typeof input.id !== "string") {
|
|
1021
|
+
throw new Error("get_place requires id.");
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
id: input.id
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/features/get-street/application/get-street.ts
|
|
1029
|
+
async function getStreet(input, dependencies) {
|
|
1030
|
+
const id = input.id.trim();
|
|
1031
|
+
if (!id) {
|
|
1032
|
+
throw new Error("get_street requires id.");
|
|
1033
|
+
}
|
|
1034
|
+
const street = await dependencies.streetDetailsRepository.getStreet(id);
|
|
1035
|
+
return {
|
|
1036
|
+
stateDate: street?.stateDate ?? null,
|
|
1037
|
+
street
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/features/get-street/mcp/get-street.tool.ts
|
|
1042
|
+
import { defineTool as defineTool3 } from "@mcp-craftman/core";
|
|
1043
|
+
function createGetStreetTool(dependencies) {
|
|
1044
|
+
return defineTool3({
|
|
1045
|
+
name: "get_street",
|
|
1046
|
+
description: "Gets a TERYT street by identifier.",
|
|
1047
|
+
inputSchema: {
|
|
1048
|
+
type: "object",
|
|
1049
|
+
properties: {
|
|
1050
|
+
id: {
|
|
1051
|
+
type: "string"
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
required: ["id"]
|
|
1055
|
+
},
|
|
1056
|
+
outputSchema: {
|
|
1057
|
+
type: "object",
|
|
1058
|
+
properties: {
|
|
1059
|
+
stateDate: {
|
|
1060
|
+
anyOf: [
|
|
1061
|
+
{
|
|
1062
|
+
type: "string"
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
type: "null"
|
|
1066
|
+
}
|
|
1067
|
+
]
|
|
1068
|
+
},
|
|
1069
|
+
street: {
|
|
1070
|
+
anyOf: [
|
|
1071
|
+
{
|
|
1072
|
+
type: "object",
|
|
1073
|
+
properties: {
|
|
1074
|
+
code: {
|
|
1075
|
+
type: "string"
|
|
1076
|
+
},
|
|
1077
|
+
id: {
|
|
1078
|
+
type: "string"
|
|
1079
|
+
},
|
|
1080
|
+
name: {
|
|
1081
|
+
type: "string"
|
|
1082
|
+
},
|
|
1083
|
+
placeId: {
|
|
1084
|
+
type: "string"
|
|
1085
|
+
},
|
|
1086
|
+
stateDate: {
|
|
1087
|
+
type: "string"
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
required: ["code", "id", "name", "placeId", "stateDate"]
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
type: "null"
|
|
1094
|
+
}
|
|
1095
|
+
]
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
required: ["stateDate", "street"]
|
|
1099
|
+
},
|
|
1100
|
+
policy: "read",
|
|
1101
|
+
returnsStructuredContent: true,
|
|
1102
|
+
annotations: {
|
|
1103
|
+
readOnlyHint: true
|
|
1104
|
+
},
|
|
1105
|
+
handler: async (input) => ({
|
|
1106
|
+
structuredContent: await getStreet(parseInput2(input), dependencies)
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
function parseInput2(input) {
|
|
1111
|
+
if (typeof input !== "object" || input === null || !("id" in input) || typeof input.id !== "string") {
|
|
1112
|
+
throw new Error("get_street requires id.");
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
id: input.id
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/features/get-unit/application/get-unit.ts
|
|
1120
|
+
async function getUnit(input, dependencies) {
|
|
1121
|
+
const id = input.id.trim();
|
|
1122
|
+
if (!id) {
|
|
1123
|
+
throw new Error("get_unit requires id.");
|
|
1124
|
+
}
|
|
1125
|
+
const unit = await dependencies.unitDetailsRepository.getUnit(id);
|
|
1126
|
+
return {
|
|
1127
|
+
stateDate: unit?.stateDate ?? null,
|
|
1128
|
+
unit
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/features/get-unit/mcp/get-unit.tool.ts
|
|
1133
|
+
import { defineTool as defineTool4 } from "@mcp-craftman/core";
|
|
1134
|
+
function createGetUnitTool(dependencies) {
|
|
1135
|
+
return defineTool4({
|
|
1136
|
+
name: "get_unit",
|
|
1137
|
+
description: "Gets a TERYT territorial unit by identifier.",
|
|
1138
|
+
inputSchema: {
|
|
1139
|
+
type: "object",
|
|
1140
|
+
properties: {
|
|
1141
|
+
id: {
|
|
1142
|
+
type: "string"
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
required: ["id"]
|
|
1146
|
+
},
|
|
1147
|
+
outputSchema: {
|
|
1148
|
+
type: "object",
|
|
1149
|
+
properties: {
|
|
1150
|
+
stateDate: {
|
|
1151
|
+
anyOf: [
|
|
1152
|
+
{
|
|
1153
|
+
type: "string"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
type: "null"
|
|
1157
|
+
}
|
|
1158
|
+
]
|
|
1159
|
+
},
|
|
1160
|
+
unit: {
|
|
1161
|
+
anyOf: [
|
|
1162
|
+
{
|
|
1163
|
+
type: "object",
|
|
1164
|
+
properties: {
|
|
1165
|
+
id: {
|
|
1166
|
+
type: "string"
|
|
1167
|
+
},
|
|
1168
|
+
name: {
|
|
1169
|
+
type: "string"
|
|
1170
|
+
},
|
|
1171
|
+
stateDate: {
|
|
1172
|
+
type: "string"
|
|
1173
|
+
},
|
|
1174
|
+
type: {
|
|
1175
|
+
type: "string"
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
required: ["id", "name", "stateDate", "type"]
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
type: "null"
|
|
1182
|
+
}
|
|
1183
|
+
]
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
required: ["stateDate", "unit"]
|
|
1187
|
+
},
|
|
1188
|
+
policy: "read",
|
|
1189
|
+
returnsStructuredContent: true,
|
|
1190
|
+
annotations: {
|
|
1191
|
+
readOnlyHint: true
|
|
1192
|
+
},
|
|
1193
|
+
handler: async (input) => ({
|
|
1194
|
+
structuredContent: await getUnit(parseInput3(input), dependencies)
|
|
1195
|
+
})
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
function parseInput3(input) {
|
|
1199
|
+
if (typeof input !== "object" || input === null || !("id" in input) || typeof input.id !== "string") {
|
|
1200
|
+
throw new Error("get_unit requires id.");
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
id: input.id
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/features/health/application/get-health.ts
|
|
1208
|
+
function getHealth() {
|
|
1209
|
+
return {
|
|
1210
|
+
ok: true
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/features/health/mcp/health.tool.ts
|
|
1215
|
+
import { defineTool as defineTool5 } from "@mcp-craftman/core";
|
|
1216
|
+
var healthTool = defineTool5({
|
|
1217
|
+
name: "health_status",
|
|
1218
|
+
description: "Returns basic server health.",
|
|
1219
|
+
policy: "read",
|
|
1220
|
+
returnsStructuredContent: true,
|
|
1221
|
+
outputSchema: {
|
|
1222
|
+
type: "object",
|
|
1223
|
+
properties: {
|
|
1224
|
+
ok: {
|
|
1225
|
+
type: "boolean"
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
required: ["ok"]
|
|
1229
|
+
},
|
|
1230
|
+
annotations: {
|
|
1231
|
+
readOnlyHint: true
|
|
1232
|
+
},
|
|
1233
|
+
handler: () => ({
|
|
1234
|
+
structuredContent: getHealth()
|
|
1235
|
+
})
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// src/features/resolve-address/application/resolve-address.ts
|
|
1239
|
+
var DEFAULT_LIMIT = 20;
|
|
1240
|
+
var MAX_LIMIT = 100;
|
|
1241
|
+
async function resolveAddress(input, dependencies) {
|
|
1242
|
+
const limit = normalizeLimit(input.limit);
|
|
1243
|
+
const query = input.query.trim();
|
|
1244
|
+
if (!query) {
|
|
1245
|
+
return {
|
|
1246
|
+
addresses: [],
|
|
1247
|
+
stateDate: null
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
const normalizedQuery = normalizeName(query);
|
|
1251
|
+
const addresses = await dependencies.addressRepository.listAddresses();
|
|
1252
|
+
const matches = addresses.flatMap((address) => {
|
|
1253
|
+
if (address.id === query) {
|
|
1254
|
+
return [
|
|
1255
|
+
{
|
|
1256
|
+
address,
|
|
1257
|
+
confidence: 1,
|
|
1258
|
+
matchedBy: "exact_code"
|
|
1259
|
+
}
|
|
1260
|
+
];
|
|
1261
|
+
}
|
|
1262
|
+
const normalizedAddress = normalizeName(formatAddress(address));
|
|
1263
|
+
if (normalizedAddress === normalizedQuery) {
|
|
1264
|
+
return [
|
|
1265
|
+
{
|
|
1266
|
+
address,
|
|
1267
|
+
confidence: 0.95,
|
|
1268
|
+
matchedBy: "exact_normalized_address"
|
|
1269
|
+
}
|
|
1270
|
+
];
|
|
1271
|
+
}
|
|
1272
|
+
if (normalizedAddress.startsWith(normalizedQuery)) {
|
|
1273
|
+
return [
|
|
1274
|
+
{
|
|
1275
|
+
address,
|
|
1276
|
+
confidence: 0.75,
|
|
1277
|
+
matchedBy: "prefix"
|
|
1278
|
+
}
|
|
1279
|
+
];
|
|
1280
|
+
}
|
|
1281
|
+
return [];
|
|
1282
|
+
}).sort(
|
|
1283
|
+
(left, right) => right.confidence - left.confidence || formatAddress(left.address).localeCompare(formatAddress(right.address))
|
|
1284
|
+
).slice(0, limit);
|
|
1285
|
+
return {
|
|
1286
|
+
addresses: matches,
|
|
1287
|
+
stateDate: matches[0]?.address.stateDate ?? null
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
function formatAddress(address) {
|
|
1291
|
+
return [address.place.name, address.street?.name].filter(Boolean).join(" ");
|
|
1292
|
+
}
|
|
1293
|
+
function normalizeLimit(limit) {
|
|
1294
|
+
if (limit === void 0) {
|
|
1295
|
+
return DEFAULT_LIMIT;
|
|
1296
|
+
}
|
|
1297
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1298
|
+
throw new Error("resolve_address limit must be a positive integer.");
|
|
1299
|
+
}
|
|
1300
|
+
return Math.min(limit, MAX_LIMIT);
|
|
1301
|
+
}
|
|
1302
|
+
function normalizeName(value) {
|
|
1303
|
+
return value.replaceAll("\u0142", "l").replaceAll("\u0141", "L").normalize("NFKD").replaceAll(/\p{Diacritic}/gu, "").toLocaleLowerCase("pl-PL");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/features/resolve-address/mcp/resolve-address.tool.ts
|
|
1307
|
+
import { defineTool as defineTool6 } from "@mcp-craftman/core";
|
|
1308
|
+
function createResolveAddressTool(dependencies) {
|
|
1309
|
+
return defineTool6({
|
|
1310
|
+
inputSchema,
|
|
1311
|
+
outputSchema,
|
|
1312
|
+
name: "resolve_address",
|
|
1313
|
+
description: "Resolves a TERYT address candidate to territorial, place, and street identifiers.",
|
|
1314
|
+
policy: "read",
|
|
1315
|
+
returnsStructuredContent: true,
|
|
1316
|
+
annotations: {
|
|
1317
|
+
readOnlyHint: true
|
|
1318
|
+
},
|
|
1319
|
+
handler: async (input) => ({
|
|
1320
|
+
structuredContent: await resolveAddress(parseInput4(input), dependencies)
|
|
1321
|
+
})
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
var inputSchema = {
|
|
1325
|
+
type: "object",
|
|
1326
|
+
properties: {
|
|
1327
|
+
query: {
|
|
1328
|
+
type: "string"
|
|
1329
|
+
},
|
|
1330
|
+
limit: {
|
|
1331
|
+
type: "number",
|
|
1332
|
+
default: 20,
|
|
1333
|
+
maximum: 100,
|
|
1334
|
+
minimum: 1
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
required: ["query"]
|
|
1338
|
+
};
|
|
1339
|
+
var outputSchema = {
|
|
1340
|
+
type: "object",
|
|
1341
|
+
properties: {
|
|
1342
|
+
addresses: {
|
|
1343
|
+
type: "array",
|
|
1344
|
+
items: {
|
|
1345
|
+
type: "object",
|
|
1346
|
+
properties: {
|
|
1347
|
+
address: {
|
|
1348
|
+
type: "object",
|
|
1349
|
+
properties: {
|
|
1350
|
+
id: {
|
|
1351
|
+
type: "string"
|
|
1352
|
+
},
|
|
1353
|
+
place: {
|
|
1354
|
+
type: "object",
|
|
1355
|
+
properties: {
|
|
1356
|
+
id: {
|
|
1357
|
+
type: "string"
|
|
1358
|
+
},
|
|
1359
|
+
name: {
|
|
1360
|
+
type: "string"
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
required: ["id", "name"]
|
|
1364
|
+
},
|
|
1365
|
+
stateDate: {
|
|
1366
|
+
type: "string"
|
|
1367
|
+
},
|
|
1368
|
+
street: {
|
|
1369
|
+
anyOf: [
|
|
1370
|
+
{
|
|
1371
|
+
type: "object",
|
|
1372
|
+
properties: {
|
|
1373
|
+
code: {
|
|
1374
|
+
type: "string"
|
|
1375
|
+
},
|
|
1376
|
+
id: {
|
|
1377
|
+
type: "string"
|
|
1378
|
+
},
|
|
1379
|
+
name: {
|
|
1380
|
+
type: "string"
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
required: ["code", "id", "name"]
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
type: "null"
|
|
1387
|
+
}
|
|
1388
|
+
]
|
|
1389
|
+
},
|
|
1390
|
+
unit: {
|
|
1391
|
+
type: "object",
|
|
1392
|
+
properties: {
|
|
1393
|
+
id: {
|
|
1394
|
+
type: "string"
|
|
1395
|
+
},
|
|
1396
|
+
name: {
|
|
1397
|
+
type: "string"
|
|
1398
|
+
},
|
|
1399
|
+
type: {
|
|
1400
|
+
type: "string"
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1403
|
+
required: ["id", "name", "type"]
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
required: ["id", "place", "stateDate", "street", "unit"]
|
|
1407
|
+
},
|
|
1408
|
+
confidence: {
|
|
1409
|
+
type: "number"
|
|
1410
|
+
},
|
|
1411
|
+
matchedBy: {
|
|
1412
|
+
type: "string",
|
|
1413
|
+
enum: ["exact_code", "exact_normalized_address", "prefix"]
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
required: ["address", "confidence", "matchedBy"]
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
stateDate: {
|
|
1420
|
+
anyOf: [
|
|
1421
|
+
{
|
|
1422
|
+
type: "string"
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
type: "null"
|
|
1426
|
+
}
|
|
1427
|
+
]
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
required: ["addresses", "stateDate"]
|
|
1431
|
+
};
|
|
1432
|
+
function parseInput4(input) {
|
|
1433
|
+
if (typeof input !== "object" || input === null || !("query" in input) || typeof input.query !== "string") {
|
|
1434
|
+
throw new Error("resolve_address requires query.");
|
|
1435
|
+
}
|
|
1436
|
+
const limit = "limit" in input ? input.limit : void 0;
|
|
1437
|
+
if (limit !== void 0 && typeof limit !== "number") {
|
|
1438
|
+
throw new Error("resolve_address limit must be a number.");
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
limit,
|
|
1442
|
+
query: input.query
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/features/search-places/application/search-places.ts
|
|
1447
|
+
var DEFAULT_LIMIT2 = 20;
|
|
1448
|
+
var MAX_LIMIT2 = 100;
|
|
1449
|
+
async function searchPlaces(input, dependencies) {
|
|
1450
|
+
const limit = normalizeLimit2(input.limit);
|
|
1451
|
+
const query = input.query.trim();
|
|
1452
|
+
if (!query) {
|
|
1453
|
+
return {
|
|
1454
|
+
places: [],
|
|
1455
|
+
stateDate: null
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
const normalizedQuery = normalizeName2(query);
|
|
1459
|
+
const places = await dependencies.placeRepository.listPlaces();
|
|
1460
|
+
const matches = places.flatMap((place) => {
|
|
1461
|
+
if (place.id === query) {
|
|
1462
|
+
return [
|
|
1463
|
+
{
|
|
1464
|
+
confidence: 1,
|
|
1465
|
+
matchedBy: "exact_code",
|
|
1466
|
+
place
|
|
1467
|
+
}
|
|
1468
|
+
];
|
|
1469
|
+
}
|
|
1470
|
+
const normalizedName = normalizeName2(place.name);
|
|
1471
|
+
if (normalizedName === normalizedQuery) {
|
|
1472
|
+
return [
|
|
1473
|
+
{
|
|
1474
|
+
confidence: 0.95,
|
|
1475
|
+
matchedBy: "exact_normalized_name",
|
|
1476
|
+
place
|
|
1477
|
+
}
|
|
1478
|
+
];
|
|
1479
|
+
}
|
|
1480
|
+
if (normalizedName.startsWith(normalizedQuery)) {
|
|
1481
|
+
return [
|
|
1482
|
+
{
|
|
1483
|
+
confidence: 0.75,
|
|
1484
|
+
matchedBy: "prefix",
|
|
1485
|
+
place
|
|
1486
|
+
}
|
|
1487
|
+
];
|
|
1488
|
+
}
|
|
1489
|
+
if (normalizedName.includes(normalizedQuery)) {
|
|
1490
|
+
return [
|
|
1491
|
+
{
|
|
1492
|
+
confidence: 0.55,
|
|
1493
|
+
matchedBy: "fts",
|
|
1494
|
+
place
|
|
1495
|
+
}
|
|
1496
|
+
];
|
|
1497
|
+
}
|
|
1498
|
+
return [];
|
|
1499
|
+
}).sort((left, right) => right.confidence - left.confidence || left.place.name.localeCompare(right.place.name)).slice(0, limit);
|
|
1500
|
+
return {
|
|
1501
|
+
places: matches,
|
|
1502
|
+
stateDate: matches[0]?.place.stateDate ?? null
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function normalizeLimit2(limit) {
|
|
1506
|
+
if (limit === void 0) {
|
|
1507
|
+
return DEFAULT_LIMIT2;
|
|
1508
|
+
}
|
|
1509
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1510
|
+
throw new Error("search_places limit must be a positive integer.");
|
|
1511
|
+
}
|
|
1512
|
+
return Math.min(limit, MAX_LIMIT2);
|
|
1513
|
+
}
|
|
1514
|
+
function normalizeName2(value) {
|
|
1515
|
+
return value.replaceAll("\u0142", "l").replaceAll("\u0141", "L").normalize("NFKD").replaceAll(/\p{Diacritic}/gu, "").toLocaleLowerCase("pl-PL");
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/features/search-places/mcp/search-places.tool.ts
|
|
1519
|
+
import { defineTool as defineTool7 } from "@mcp-craftman/core";
|
|
1520
|
+
function createSearchPlacesTool(dependencies) {
|
|
1521
|
+
return defineTool7({
|
|
1522
|
+
name: "search_places",
|
|
1523
|
+
description: "Searches TERYT places.",
|
|
1524
|
+
inputSchema: {
|
|
1525
|
+
type: "object",
|
|
1526
|
+
properties: {
|
|
1527
|
+
query: {
|
|
1528
|
+
type: "string"
|
|
1529
|
+
},
|
|
1530
|
+
limit: {
|
|
1531
|
+
type: "number",
|
|
1532
|
+
default: 20,
|
|
1533
|
+
maximum: 100,
|
|
1534
|
+
minimum: 1
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
required: ["query"]
|
|
1538
|
+
},
|
|
1539
|
+
outputSchema: {
|
|
1540
|
+
type: "object",
|
|
1541
|
+
properties: {
|
|
1542
|
+
places: {
|
|
1543
|
+
type: "array",
|
|
1544
|
+
items: {
|
|
1545
|
+
type: "object",
|
|
1546
|
+
properties: {
|
|
1547
|
+
confidence: {
|
|
1548
|
+
type: "number"
|
|
1549
|
+
},
|
|
1550
|
+
matchedBy: {
|
|
1551
|
+
type: "string",
|
|
1552
|
+
enum: ["exact_code", "exact_normalized_name", "prefix", "fts"]
|
|
1553
|
+
},
|
|
1554
|
+
place: {
|
|
1555
|
+
type: "object",
|
|
1556
|
+
properties: {
|
|
1557
|
+
id: {
|
|
1558
|
+
type: "string"
|
|
1559
|
+
},
|
|
1560
|
+
name: {
|
|
1561
|
+
type: "string"
|
|
1562
|
+
},
|
|
1563
|
+
stateDate: {
|
|
1564
|
+
type: "string"
|
|
1565
|
+
},
|
|
1566
|
+
unitId: {
|
|
1567
|
+
type: "string"
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
required: ["id", "name", "stateDate", "unitId"]
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
required: ["confidence", "matchedBy", "place"]
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
stateDate: {
|
|
1577
|
+
anyOf: [
|
|
1578
|
+
{
|
|
1579
|
+
type: "string"
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
type: "null"
|
|
1583
|
+
}
|
|
1584
|
+
]
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
required: ["places", "stateDate"]
|
|
1588
|
+
},
|
|
1589
|
+
policy: "read",
|
|
1590
|
+
returnsStructuredContent: true,
|
|
1591
|
+
annotations: {
|
|
1592
|
+
readOnlyHint: true
|
|
1593
|
+
},
|
|
1594
|
+
handler: async (input) => ({
|
|
1595
|
+
structuredContent: await searchPlaces(parseInput5(input), dependencies)
|
|
1596
|
+
})
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
function parseInput5(input) {
|
|
1600
|
+
if (typeof input !== "object" || input === null || !("query" in input) || typeof input.query !== "string") {
|
|
1601
|
+
throw new Error("search_places requires query.");
|
|
1602
|
+
}
|
|
1603
|
+
const limit = "limit" in input ? input.limit : void 0;
|
|
1604
|
+
if (limit !== void 0 && typeof limit !== "number") {
|
|
1605
|
+
throw new Error("search_places limit must be a number.");
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
limit,
|
|
1609
|
+
query: input.query
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/features/search-streets/application/search-streets.ts
|
|
1614
|
+
var DEFAULT_LIMIT3 = 20;
|
|
1615
|
+
var MAX_LIMIT3 = 100;
|
|
1616
|
+
async function searchStreets(input, dependencies) {
|
|
1617
|
+
const limit = normalizeLimit3(input.limit);
|
|
1618
|
+
const query = input.query.trim();
|
|
1619
|
+
if (!query) {
|
|
1620
|
+
return {
|
|
1621
|
+
stateDate: null,
|
|
1622
|
+
streets: []
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
const normalizedQuery = normalizeName3(query);
|
|
1626
|
+
const streets = await dependencies.streetRepository.listStreets();
|
|
1627
|
+
const matches = streets.flatMap((street) => {
|
|
1628
|
+
if (street.id === query || street.code === query) {
|
|
1629
|
+
return [
|
|
1630
|
+
{
|
|
1631
|
+
confidence: 1,
|
|
1632
|
+
matchedBy: "exact_code",
|
|
1633
|
+
street
|
|
1634
|
+
}
|
|
1635
|
+
];
|
|
1636
|
+
}
|
|
1637
|
+
const normalizedName = normalizeName3(street.name);
|
|
1638
|
+
if (normalizedName === normalizedQuery) {
|
|
1639
|
+
return [
|
|
1640
|
+
{
|
|
1641
|
+
confidence: 0.95,
|
|
1642
|
+
matchedBy: "exact_normalized_name",
|
|
1643
|
+
street
|
|
1644
|
+
}
|
|
1645
|
+
];
|
|
1646
|
+
}
|
|
1647
|
+
if (normalizedName.startsWith(normalizedQuery)) {
|
|
1648
|
+
return [
|
|
1649
|
+
{
|
|
1650
|
+
confidence: 0.75,
|
|
1651
|
+
matchedBy: "prefix",
|
|
1652
|
+
street
|
|
1653
|
+
}
|
|
1654
|
+
];
|
|
1655
|
+
}
|
|
1656
|
+
if (normalizedName.includes(normalizedQuery)) {
|
|
1657
|
+
return [
|
|
1658
|
+
{
|
|
1659
|
+
confidence: 0.55,
|
|
1660
|
+
matchedBy: "fts",
|
|
1661
|
+
street
|
|
1662
|
+
}
|
|
1663
|
+
];
|
|
1664
|
+
}
|
|
1665
|
+
return [];
|
|
1666
|
+
}).sort((left, right) => right.confidence - left.confidence || left.street.name.localeCompare(right.street.name)).slice(0, limit);
|
|
1667
|
+
return {
|
|
1668
|
+
stateDate: matches[0]?.street.stateDate ?? null,
|
|
1669
|
+
streets: matches
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function normalizeLimit3(limit) {
|
|
1673
|
+
if (limit === void 0) {
|
|
1674
|
+
return DEFAULT_LIMIT3;
|
|
1675
|
+
}
|
|
1676
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1677
|
+
throw new Error("search_streets limit must be a positive integer.");
|
|
1678
|
+
}
|
|
1679
|
+
return Math.min(limit, MAX_LIMIT3);
|
|
1680
|
+
}
|
|
1681
|
+
function normalizeName3(value) {
|
|
1682
|
+
return value.replaceAll("\u0142", "l").replaceAll("\u0141", "L").normalize("NFKD").replaceAll(/\p{Diacritic}/gu, "").toLocaleLowerCase("pl-PL");
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/features/search-streets/mcp/search-streets.tool.ts
|
|
1686
|
+
import { defineTool as defineTool8 } from "@mcp-craftman/core";
|
|
1687
|
+
function createSearchStreetsTool(dependencies) {
|
|
1688
|
+
return defineTool8({
|
|
1689
|
+
inputSchema: inputSchema2,
|
|
1690
|
+
outputSchema: outputSchema2,
|
|
1691
|
+
name: "search_streets",
|
|
1692
|
+
description: "Searches TERYT streets.",
|
|
1693
|
+
policy: "read",
|
|
1694
|
+
returnsStructuredContent: true,
|
|
1695
|
+
annotations: {
|
|
1696
|
+
readOnlyHint: true
|
|
1697
|
+
},
|
|
1698
|
+
handler: async (input) => ({
|
|
1699
|
+
structuredContent: await searchStreets(parseInput6(input), dependencies)
|
|
1700
|
+
})
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
var inputSchema2 = {
|
|
1704
|
+
type: "object",
|
|
1705
|
+
properties: {
|
|
1706
|
+
query: {
|
|
1707
|
+
type: "string"
|
|
1708
|
+
},
|
|
1709
|
+
limit: {
|
|
1710
|
+
type: "number",
|
|
1711
|
+
default: 20,
|
|
1712
|
+
maximum: 100,
|
|
1713
|
+
minimum: 1
|
|
1714
|
+
}
|
|
1715
|
+
},
|
|
1716
|
+
required: ["query"]
|
|
1717
|
+
};
|
|
1718
|
+
var outputSchema2 = {
|
|
1719
|
+
type: "object",
|
|
1720
|
+
properties: {
|
|
1721
|
+
stateDate: {
|
|
1722
|
+
anyOf: [
|
|
1723
|
+
{
|
|
1724
|
+
type: "string"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
type: "null"
|
|
1728
|
+
}
|
|
1729
|
+
]
|
|
1730
|
+
},
|
|
1731
|
+
streets: {
|
|
1732
|
+
type: "array",
|
|
1733
|
+
items: {
|
|
1734
|
+
type: "object",
|
|
1735
|
+
properties: {
|
|
1736
|
+
confidence: {
|
|
1737
|
+
type: "number"
|
|
1738
|
+
},
|
|
1739
|
+
matchedBy: {
|
|
1740
|
+
type: "string",
|
|
1741
|
+
enum: ["exact_code", "exact_normalized_name", "prefix", "fts"]
|
|
1742
|
+
},
|
|
1743
|
+
street: {
|
|
1744
|
+
type: "object",
|
|
1745
|
+
properties: {
|
|
1746
|
+
code: {
|
|
1747
|
+
type: "string"
|
|
1748
|
+
},
|
|
1749
|
+
id: {
|
|
1750
|
+
type: "string"
|
|
1751
|
+
},
|
|
1752
|
+
name: {
|
|
1753
|
+
type: "string"
|
|
1754
|
+
},
|
|
1755
|
+
placeId: {
|
|
1756
|
+
type: "string"
|
|
1757
|
+
},
|
|
1758
|
+
stateDate: {
|
|
1759
|
+
type: "string"
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
required: ["code", "id", "name", "placeId", "stateDate"]
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
required: ["confidence", "matchedBy", "street"]
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
required: ["stateDate", "streets"]
|
|
1770
|
+
};
|
|
1771
|
+
function parseInput6(input) {
|
|
1772
|
+
if (typeof input !== "object" || input === null || !("query" in input) || typeof input.query !== "string") {
|
|
1773
|
+
throw new Error("search_streets requires query.");
|
|
1774
|
+
}
|
|
1775
|
+
const limit = "limit" in input ? input.limit : void 0;
|
|
1776
|
+
if (limit !== void 0 && typeof limit !== "number") {
|
|
1777
|
+
throw new Error("search_streets limit must be a number.");
|
|
1778
|
+
}
|
|
1779
|
+
return {
|
|
1780
|
+
limit,
|
|
1781
|
+
query: input.query
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/features/search-units/application/search-units.ts
|
|
1786
|
+
var DEFAULT_LIMIT4 = 20;
|
|
1787
|
+
var MAX_LIMIT4 = 100;
|
|
1788
|
+
async function searchUnits(input, dependencies) {
|
|
1789
|
+
const query = input.query.trim();
|
|
1790
|
+
const limit = normalizeLimit4(input.limit);
|
|
1791
|
+
if (!query) {
|
|
1792
|
+
return {
|
|
1793
|
+
stateDate: null,
|
|
1794
|
+
units: []
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const normalizedQuery = normalizeName4(query);
|
|
1798
|
+
const units = await dependencies.unitRepository.listUnits();
|
|
1799
|
+
const matches = units.flatMap((unit) => {
|
|
1800
|
+
if (unit.id === query) {
|
|
1801
|
+
return [
|
|
1802
|
+
{
|
|
1803
|
+
confidence: 1,
|
|
1804
|
+
matchedBy: "exact_code",
|
|
1805
|
+
unit
|
|
1806
|
+
}
|
|
1807
|
+
];
|
|
1808
|
+
}
|
|
1809
|
+
const normalizedName = normalizeName4(unit.name);
|
|
1810
|
+
if (normalizedName === normalizedQuery) {
|
|
1811
|
+
return [
|
|
1812
|
+
{
|
|
1813
|
+
confidence: 0.95,
|
|
1814
|
+
matchedBy: "exact_normalized_name",
|
|
1815
|
+
unit
|
|
1816
|
+
}
|
|
1817
|
+
];
|
|
1818
|
+
}
|
|
1819
|
+
if (normalizedName.startsWith(normalizedQuery)) {
|
|
1820
|
+
return [
|
|
1821
|
+
{
|
|
1822
|
+
confidence: 0.75,
|
|
1823
|
+
matchedBy: "prefix",
|
|
1824
|
+
unit
|
|
1825
|
+
}
|
|
1826
|
+
];
|
|
1827
|
+
}
|
|
1828
|
+
if (normalizedName.includes(normalizedQuery)) {
|
|
1829
|
+
return [
|
|
1830
|
+
{
|
|
1831
|
+
confidence: 0.55,
|
|
1832
|
+
matchedBy: "fts",
|
|
1833
|
+
unit
|
|
1834
|
+
}
|
|
1835
|
+
];
|
|
1836
|
+
}
|
|
1837
|
+
return [];
|
|
1838
|
+
}).sort((left, right) => right.confidence - left.confidence || left.unit.name.localeCompare(right.unit.name)).slice(0, limit);
|
|
1839
|
+
return {
|
|
1840
|
+
stateDate: matches[0]?.unit.stateDate ?? null,
|
|
1841
|
+
units: matches
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function normalizeLimit4(limit) {
|
|
1845
|
+
if (limit === void 0) {
|
|
1846
|
+
return DEFAULT_LIMIT4;
|
|
1847
|
+
}
|
|
1848
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1849
|
+
throw new Error("search_units limit must be a positive integer.");
|
|
1850
|
+
}
|
|
1851
|
+
return Math.min(limit, MAX_LIMIT4);
|
|
1852
|
+
}
|
|
1853
|
+
function normalizeName4(value) {
|
|
1854
|
+
return value.replaceAll("\u0142", "l").replaceAll("\u0141", "L").normalize("NFKD").replaceAll(/\p{Diacritic}/gu, "").toLocaleLowerCase("pl-PL");
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/features/search-units/mcp/search-units.tool.ts
|
|
1858
|
+
import { defineTool as defineTool9 } from "@mcp-craftman/core";
|
|
1859
|
+
function createSearchUnitsTool(dependencies) {
|
|
1860
|
+
return defineTool9({
|
|
1861
|
+
name: "search_units",
|
|
1862
|
+
description: "Searches TERYT territorial units.",
|
|
1863
|
+
inputSchema: {
|
|
1864
|
+
type: "object",
|
|
1865
|
+
properties: {
|
|
1866
|
+
query: {
|
|
1867
|
+
type: "string"
|
|
1868
|
+
},
|
|
1869
|
+
limit: {
|
|
1870
|
+
type: "number",
|
|
1871
|
+
default: 20,
|
|
1872
|
+
maximum: 100,
|
|
1873
|
+
minimum: 1
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
required: ["query"]
|
|
1877
|
+
},
|
|
1878
|
+
outputSchema: {
|
|
1879
|
+
type: "object",
|
|
1880
|
+
properties: {
|
|
1881
|
+
stateDate: {
|
|
1882
|
+
anyOf: [
|
|
1883
|
+
{
|
|
1884
|
+
type: "string"
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
type: "null"
|
|
1888
|
+
}
|
|
1889
|
+
]
|
|
1890
|
+
},
|
|
1891
|
+
units: {
|
|
1892
|
+
type: "array",
|
|
1893
|
+
items: {
|
|
1894
|
+
type: "object",
|
|
1895
|
+
properties: {
|
|
1896
|
+
confidence: {
|
|
1897
|
+
type: "number"
|
|
1898
|
+
},
|
|
1899
|
+
matchedBy: {
|
|
1900
|
+
type: "string",
|
|
1901
|
+
enum: ["exact_code", "exact_normalized_name", "prefix", "fts"]
|
|
1902
|
+
},
|
|
1903
|
+
unit: {
|
|
1904
|
+
type: "object",
|
|
1905
|
+
properties: {
|
|
1906
|
+
id: {
|
|
1907
|
+
type: "string"
|
|
1908
|
+
},
|
|
1909
|
+
name: {
|
|
1910
|
+
type: "string"
|
|
1911
|
+
},
|
|
1912
|
+
stateDate: {
|
|
1913
|
+
type: "string"
|
|
1914
|
+
},
|
|
1915
|
+
type: {
|
|
1916
|
+
type: "string"
|
|
1917
|
+
}
|
|
1918
|
+
},
|
|
1919
|
+
required: ["id", "name", "stateDate", "type"]
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
required: ["confidence", "matchedBy", "unit"]
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
},
|
|
1926
|
+
required: ["stateDate", "units"]
|
|
1927
|
+
},
|
|
1928
|
+
policy: "read",
|
|
1929
|
+
returnsStructuredContent: true,
|
|
1930
|
+
annotations: {
|
|
1931
|
+
readOnlyHint: true
|
|
1932
|
+
},
|
|
1933
|
+
handler: async (input) => ({
|
|
1934
|
+
structuredContent: await searchUnits(parseInput7(input), dependencies)
|
|
1935
|
+
})
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
function parseInput7(input) {
|
|
1939
|
+
if (typeof input !== "object" || input === null || !("query" in input) || typeof input.query !== "string") {
|
|
1940
|
+
throw new Error("search_units requires query.");
|
|
1941
|
+
}
|
|
1942
|
+
const limit = "limit" in input ? input.limit : void 0;
|
|
1943
|
+
if (limit !== void 0 && typeof limit !== "number") {
|
|
1944
|
+
throw new Error("search_units limit must be a number.");
|
|
1945
|
+
}
|
|
1946
|
+
return {
|
|
1947
|
+
limit,
|
|
1948
|
+
query: input.query
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/features/source-status/application/get-source-status.ts
|
|
1953
|
+
async function getSourceStatus(input) {
|
|
1954
|
+
const datasets = await input.sourceCatalog.listDatasets();
|
|
1955
|
+
const snapshots = await Promise.all(
|
|
1956
|
+
datasets.map(async (dataset) => {
|
|
1957
|
+
const snapshot = await input.manifestStore.getSnapshot(dataset.code) ?? null;
|
|
1958
|
+
return {
|
|
1959
|
+
dataset,
|
|
1960
|
+
snapshot,
|
|
1961
|
+
sha256: snapshot?.sha256 ?? null,
|
|
1962
|
+
stateDate: snapshot?.stateDate ?? null
|
|
1963
|
+
};
|
|
1964
|
+
})
|
|
1965
|
+
);
|
|
1966
|
+
const lastSuccessfulSync = snapshots.map((item) => item.snapshot?.downloadedAt).filter((downloadedAt) => Boolean(downloadedAt)).sort().at(-1) ?? null;
|
|
1967
|
+
return {
|
|
1968
|
+
lastCheckedAt: null,
|
|
1969
|
+
localDatabase: {
|
|
1970
|
+
status: snapshots.some((item) => item.snapshot) ? "available" : "missing"
|
|
1971
|
+
},
|
|
1972
|
+
remoteSource: {
|
|
1973
|
+
errors: [],
|
|
1974
|
+
status: "unknown"
|
|
1975
|
+
},
|
|
1976
|
+
datasets: snapshots,
|
|
1977
|
+
lastSuccessfulSync
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// src/features/source-status/mcp/source-status.tool.ts
|
|
1982
|
+
import { defineTool as defineTool10 } from "@mcp-craftman/core";
|
|
1983
|
+
function createSourceStatusTool(input) {
|
|
1984
|
+
return defineTool10({
|
|
1985
|
+
outputSchema: outputSchema3,
|
|
1986
|
+
name: "source_status",
|
|
1987
|
+
description: "Returns official TERYT source dataset status.",
|
|
1988
|
+
policy: "read",
|
|
1989
|
+
returnsStructuredContent: true,
|
|
1990
|
+
annotations: {
|
|
1991
|
+
readOnlyHint: true
|
|
1992
|
+
},
|
|
1993
|
+
handler: async () => ({
|
|
1994
|
+
structuredContent: await getSourceStatus(input)
|
|
1995
|
+
})
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
var outputSchema3 = {
|
|
1999
|
+
type: "object",
|
|
2000
|
+
properties: {
|
|
2001
|
+
datasets: {
|
|
2002
|
+
type: "array",
|
|
2003
|
+
items: {
|
|
2004
|
+
type: "object",
|
|
2005
|
+
properties: {
|
|
2006
|
+
dataset: {
|
|
2007
|
+
type: "object",
|
|
2008
|
+
properties: {
|
|
2009
|
+
code: {
|
|
2010
|
+
type: "string",
|
|
2011
|
+
enum: ["TERC", "SIMC", "ULIC", "WMRODZ"]
|
|
2012
|
+
},
|
|
2013
|
+
name: {
|
|
2014
|
+
type: "string"
|
|
2015
|
+
},
|
|
2016
|
+
sourceUrl: {
|
|
2017
|
+
type: "string"
|
|
2018
|
+
}
|
|
2019
|
+
},
|
|
2020
|
+
required: ["code", "name", "sourceUrl"]
|
|
2021
|
+
},
|
|
2022
|
+
snapshot: {
|
|
2023
|
+
anyOf: [
|
|
2024
|
+
{
|
|
2025
|
+
type: "object",
|
|
2026
|
+
properties: {
|
|
2027
|
+
dataset: {
|
|
2028
|
+
type: "string",
|
|
2029
|
+
enum: ["TERC", "SIMC", "ULIC", "WMRODZ"]
|
|
2030
|
+
},
|
|
2031
|
+
version: {
|
|
2032
|
+
type: "string"
|
|
2033
|
+
},
|
|
2034
|
+
downloadedAt: {
|
|
2035
|
+
type: "string"
|
|
2036
|
+
},
|
|
2037
|
+
recordCount: {
|
|
2038
|
+
type: "number"
|
|
2039
|
+
},
|
|
2040
|
+
sha256: {
|
|
2041
|
+
type: "string"
|
|
2042
|
+
},
|
|
2043
|
+
sourceUrl: {
|
|
2044
|
+
type: "string"
|
|
2045
|
+
},
|
|
2046
|
+
stateDate: {
|
|
2047
|
+
type: "string"
|
|
2048
|
+
}
|
|
2049
|
+
},
|
|
2050
|
+
required: ["dataset", "downloadedAt", "sourceUrl"]
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
type: "null"
|
|
2054
|
+
}
|
|
2055
|
+
]
|
|
2056
|
+
},
|
|
2057
|
+
sha256: {
|
|
2058
|
+
anyOf: [
|
|
2059
|
+
{
|
|
2060
|
+
type: "string"
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
type: "null"
|
|
2064
|
+
}
|
|
2065
|
+
]
|
|
2066
|
+
},
|
|
2067
|
+
stateDate: {
|
|
2068
|
+
anyOf: [
|
|
2069
|
+
{
|
|
2070
|
+
type: "string"
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
type: "null"
|
|
2074
|
+
}
|
|
2075
|
+
]
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
required: ["dataset", "sha256", "snapshot", "stateDate"]
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2081
|
+
lastCheckedAt: {
|
|
2082
|
+
anyOf: [
|
|
2083
|
+
{
|
|
2084
|
+
type: "string"
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
type: "null"
|
|
2088
|
+
}
|
|
2089
|
+
]
|
|
2090
|
+
},
|
|
2091
|
+
lastSuccessfulSync: {
|
|
2092
|
+
anyOf: [
|
|
2093
|
+
{
|
|
2094
|
+
type: "string"
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
type: "null"
|
|
2098
|
+
}
|
|
2099
|
+
]
|
|
2100
|
+
},
|
|
2101
|
+
localDatabase: {
|
|
2102
|
+
type: "object",
|
|
2103
|
+
properties: {
|
|
2104
|
+
status: {
|
|
2105
|
+
type: "string",
|
|
2106
|
+
enum: ["missing", "available"]
|
|
2107
|
+
}
|
|
2108
|
+
},
|
|
2109
|
+
required: ["status"]
|
|
2110
|
+
},
|
|
2111
|
+
remoteSource: {
|
|
2112
|
+
type: "object",
|
|
2113
|
+
properties: {
|
|
2114
|
+
errors: {
|
|
2115
|
+
type: "array",
|
|
2116
|
+
items: {
|
|
2117
|
+
type: "string"
|
|
2118
|
+
}
|
|
2119
|
+
},
|
|
2120
|
+
status: {
|
|
2121
|
+
type: "string",
|
|
2122
|
+
enum: ["unknown", "available", "error"]
|
|
2123
|
+
}
|
|
2124
|
+
},
|
|
2125
|
+
required: ["errors", "status"]
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
required: ["datasets", "lastCheckedAt", "lastSuccessfulSync", "localDatabase", "remoteSource"]
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
// src/features/sync-database/application/plan-sync.ts
|
|
2132
|
+
async function planSync(input) {
|
|
2133
|
+
const databaseExists = await input.fileStore.databaseExists();
|
|
2134
|
+
if (input.mode === "missing" && databaseExists) {
|
|
2135
|
+
return {
|
|
2136
|
+
action: "skip_existing",
|
|
2137
|
+
reason: "database_exists"
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
return {
|
|
2141
|
+
action: "build_database",
|
|
2142
|
+
reason: input.mode
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// src/features/sync-database/application/sync-database.ts
|
|
2147
|
+
import { createHash as createHash2 } from "crypto";
|
|
2148
|
+
|
|
2149
|
+
// src/features/sync-database/domain/dataset.ts
|
|
2150
|
+
var datasetCodes = ["TERC", "SIMC", "ULIC", "WMRODZ"];
|
|
2151
|
+
|
|
2152
|
+
// src/features/sync-database/application/sync-database.ts
|
|
2153
|
+
async function syncDatabase(input) {
|
|
2154
|
+
return input.lockStore.withSyncLock(async () => {
|
|
2155
|
+
const plan = await planSync({
|
|
2156
|
+
fileStore: input.fileStore,
|
|
2157
|
+
mode: input.mode
|
|
2158
|
+
});
|
|
2159
|
+
if (plan.action === "skip_existing") {
|
|
2160
|
+
return {
|
|
2161
|
+
databasePath: null,
|
|
2162
|
+
datasets: [],
|
|
2163
|
+
mode: input.mode,
|
|
2164
|
+
status: "skipped"
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
const sourceFiles = await Promise.all(datasetCodes.map((dataset) => input.source.download(dataset)));
|
|
2168
|
+
const imports = sourceFiles.map((sourceFile) => ({
|
|
2169
|
+
imported: importTerytSourceFile(sourceFile),
|
|
2170
|
+
sourceFile
|
|
2171
|
+
}));
|
|
2172
|
+
const database = await input.databaseBuilder.build(sourceFiles);
|
|
2173
|
+
const databasePath = await input.fileStore.swapDatabase(database.content);
|
|
2174
|
+
const datasets = imports.map(({ imported, sourceFile }) => ({
|
|
2175
|
+
columns: imported.columns,
|
|
2176
|
+
dataset: sourceFile.dataset,
|
|
2177
|
+
downloadedAt: input.now().toISOString(),
|
|
2178
|
+
publishedAtObserved: null,
|
|
2179
|
+
recordCount: imported.recordCount,
|
|
2180
|
+
sha256: sha256(sourceFile.content),
|
|
2181
|
+
source: "official-teryt-download",
|
|
2182
|
+
sourceUrl: sourceFile.sourceUrl,
|
|
2183
|
+
stateDate: imported.stateDate,
|
|
2184
|
+
variant: "full"
|
|
2185
|
+
}));
|
|
2186
|
+
const snapshot = {
|
|
2187
|
+
datasets,
|
|
2188
|
+
builtAt: input.now().toISOString(),
|
|
2189
|
+
path: databasePath
|
|
2190
|
+
};
|
|
2191
|
+
await input.manifestStore.writeSnapshot(snapshot);
|
|
2192
|
+
return {
|
|
2193
|
+
databasePath,
|
|
2194
|
+
datasets,
|
|
2195
|
+
mode: input.mode,
|
|
2196
|
+
status: "synced"
|
|
2197
|
+
};
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
function sha256(content) {
|
|
2201
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/features/sync-database/mcp/sync-database.tool.ts
|
|
2205
|
+
import { defineTool as defineTool11 } from "@mcp-craftman/core";
|
|
2206
|
+
function createSyncDatabaseTool(input) {
|
|
2207
|
+
return defineTool11({
|
|
2208
|
+
inputSchema: inputSchema3,
|
|
2209
|
+
outputSchema: outputSchema4,
|
|
2210
|
+
name: "sync_database",
|
|
2211
|
+
description: "Synchronizes the local TERYT database.",
|
|
2212
|
+
policy: "write",
|
|
2213
|
+
returnsStructuredContent: true,
|
|
2214
|
+
annotations: {
|
|
2215
|
+
destructiveHint: false,
|
|
2216
|
+
idempotentHint: false,
|
|
2217
|
+
readOnlyHint: false
|
|
2218
|
+
},
|
|
2219
|
+
handler: async (toolInput) => ({
|
|
2220
|
+
structuredContent: await syncDatabase({
|
|
2221
|
+
...input,
|
|
2222
|
+
mode: parseMode(toolInput)
|
|
2223
|
+
})
|
|
2224
|
+
})
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
var inputSchema3 = {
|
|
2228
|
+
type: "object",
|
|
2229
|
+
properties: {
|
|
2230
|
+
mode: {
|
|
2231
|
+
type: "string",
|
|
2232
|
+
enum: ["missing", "stale", "force"]
|
|
2233
|
+
}
|
|
2234
|
+
},
|
|
2235
|
+
required: ["mode"]
|
|
2236
|
+
};
|
|
2237
|
+
var outputSchema4 = {
|
|
2238
|
+
type: "object",
|
|
2239
|
+
properties: {
|
|
2240
|
+
databasePath: {
|
|
2241
|
+
anyOf: [
|
|
2242
|
+
{
|
|
2243
|
+
type: "string"
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
type: "null"
|
|
2247
|
+
}
|
|
2248
|
+
]
|
|
2249
|
+
},
|
|
2250
|
+
datasets: {
|
|
2251
|
+
type: "array",
|
|
2252
|
+
items: {
|
|
2253
|
+
type: "object",
|
|
2254
|
+
properties: {
|
|
2255
|
+
columns: {
|
|
2256
|
+
type: "array",
|
|
2257
|
+
items: {
|
|
2258
|
+
type: "string"
|
|
2259
|
+
}
|
|
2260
|
+
},
|
|
2261
|
+
dataset: {
|
|
2262
|
+
type: "string",
|
|
2263
|
+
enum: ["TERC", "SIMC", "ULIC", "WMRODZ"]
|
|
2264
|
+
},
|
|
2265
|
+
downloadedAt: {
|
|
2266
|
+
type: "string"
|
|
2267
|
+
},
|
|
2268
|
+
publishedAtObserved: {
|
|
2269
|
+
anyOf: [
|
|
2270
|
+
{
|
|
2271
|
+
type: "string"
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
type: "null"
|
|
2275
|
+
}
|
|
2276
|
+
]
|
|
2277
|
+
},
|
|
2278
|
+
recordCount: {
|
|
2279
|
+
type: "number"
|
|
2280
|
+
},
|
|
2281
|
+
sha256: {
|
|
2282
|
+
type: "string"
|
|
2283
|
+
},
|
|
2284
|
+
source: {
|
|
2285
|
+
type: "string"
|
|
2286
|
+
},
|
|
2287
|
+
sourceUrl: {
|
|
2288
|
+
type: "string"
|
|
2289
|
+
},
|
|
2290
|
+
stateDate: {
|
|
2291
|
+
type: "string"
|
|
2292
|
+
},
|
|
2293
|
+
variant: {
|
|
2294
|
+
type: "string",
|
|
2295
|
+
enum: ["full"]
|
|
2296
|
+
}
|
|
2297
|
+
},
|
|
2298
|
+
required: [
|
|
2299
|
+
"columns",
|
|
2300
|
+
"dataset",
|
|
2301
|
+
"downloadedAt",
|
|
2302
|
+
"publishedAtObserved",
|
|
2303
|
+
"recordCount",
|
|
2304
|
+
"sha256",
|
|
2305
|
+
"source",
|
|
2306
|
+
"sourceUrl",
|
|
2307
|
+
"stateDate",
|
|
2308
|
+
"variant"
|
|
2309
|
+
]
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
mode: {
|
|
2313
|
+
type: "string",
|
|
2314
|
+
enum: ["missing", "stale", "force"]
|
|
2315
|
+
},
|
|
2316
|
+
status: {
|
|
2317
|
+
type: "string",
|
|
2318
|
+
enum: ["skipped", "synced"]
|
|
2319
|
+
}
|
|
2320
|
+
},
|
|
2321
|
+
required: ["databasePath", "datasets", "mode", "status"]
|
|
2322
|
+
};
|
|
2323
|
+
function parseMode(input) {
|
|
2324
|
+
if (typeof input === "object" && input !== null && "mode" in input) {
|
|
2325
|
+
const mode = input.mode;
|
|
2326
|
+
if (mode === "missing" || mode === "stale" || mode === "force") {
|
|
2327
|
+
return mode;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
throw new Error("sync_database requires mode: missing | stale | force.");
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/mcp/registry.ts
|
|
2334
|
+
function createRegistry(input) {
|
|
2335
|
+
return createCapabilityRegistry([
|
|
2336
|
+
healthTool,
|
|
2337
|
+
createServerStatusTool({
|
|
2338
|
+
dataDir: input.config.dataDir,
|
|
2339
|
+
transport: input.config.transport
|
|
2340
|
+
}),
|
|
2341
|
+
createSourceStatusTool({
|
|
2342
|
+
manifestStore: input.manifestStore,
|
|
2343
|
+
sourceCatalog: input.sourceCatalog
|
|
2344
|
+
}),
|
|
2345
|
+
createGetPlaceTool({
|
|
2346
|
+
placeDetailsRepository: input.placeDetailsRepository
|
|
2347
|
+
}),
|
|
2348
|
+
createGetStreetTool({
|
|
2349
|
+
streetDetailsRepository: input.streetDetailsRepository
|
|
2350
|
+
}),
|
|
2351
|
+
createGetUnitTool({
|
|
2352
|
+
unitDetailsRepository: input.unitDetailsRepository
|
|
2353
|
+
}),
|
|
2354
|
+
createResolveAddressTool({
|
|
2355
|
+
addressRepository: input.addressRepository
|
|
2356
|
+
}),
|
|
2357
|
+
createSearchPlacesTool({
|
|
2358
|
+
placeRepository: input.placeRepository
|
|
2359
|
+
}),
|
|
2360
|
+
createSearchStreetsTool({
|
|
2361
|
+
streetRepository: input.streetRepository
|
|
2362
|
+
}),
|
|
2363
|
+
createSearchUnitsTool({
|
|
2364
|
+
unitRepository: input.unitRepository
|
|
2365
|
+
}),
|
|
2366
|
+
createSyncDatabaseTool(input.sync)
|
|
2367
|
+
]);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// src/app.ts
|
|
2371
|
+
function createApp(config = loadRuntimeConfig(), overrides = {}) {
|
|
2372
|
+
const sourceCatalog = new EterytSourceCatalog();
|
|
2373
|
+
const manifestStore = new JsonManifestStore(config.dataDir);
|
|
2374
|
+
const syncFileStore = new LocalFileStore(config.dataDir);
|
|
2375
|
+
const syncLockStore = new FileLockStore(config.dataDir);
|
|
2376
|
+
const syncManifestStore = new JsonSyncManifestStore(config.dataDir);
|
|
2377
|
+
const syncSource = overrides.syncSource ?? new EterytSource();
|
|
2378
|
+
return createMcpApp({
|
|
2379
|
+
name: "teryt-mcp",
|
|
2380
|
+
version: "0.1.0",
|
|
2381
|
+
registry: createRegistry({
|
|
2382
|
+
config,
|
|
2383
|
+
manifestStore,
|
|
2384
|
+
sourceCatalog,
|
|
2385
|
+
addressRepository: new InMemoryAddressRepository(),
|
|
2386
|
+
placeDetailsRepository: new InMemoryPlaceDetailsRepository(),
|
|
2387
|
+
placeRepository: new InMemoryPlaceRepository(),
|
|
2388
|
+
streetDetailsRepository: new InMemoryStreetDetailsRepository(),
|
|
2389
|
+
streetRepository: new InMemoryStreetRepository(),
|
|
2390
|
+
unitDetailsRepository: new InMemoryUnitDetailsRepository(),
|
|
2391
|
+
unitRepository: new InMemoryUnitRepository(),
|
|
2392
|
+
sync: {
|
|
2393
|
+
databaseBuilder: new SqliteDatabaseBuilder(),
|
|
2394
|
+
fileStore: syncFileStore,
|
|
2395
|
+
lockStore: syncLockStore,
|
|
2396
|
+
manifestStore: syncManifestStore,
|
|
2397
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
2398
|
+
source: syncSource
|
|
2399
|
+
}
|
|
2400
|
+
})
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/server/serve.ts
|
|
2405
|
+
import { loadRuntimeConfig as loadRuntimeConfig2 } from "@mcp-craftman/node";
|
|
2406
|
+
|
|
2407
|
+
// src/server/transports/http.ts
|
|
2408
|
+
import { startHttpServer } from "@mcp-craftman/node";
|
|
2409
|
+
function startHttpTransport(app, options = {}) {
|
|
2410
|
+
return startHttpServer(app, options);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// src/server/transports/stdio.ts
|
|
2414
|
+
import { startStdioServer } from "@mcp-craftman/node";
|
|
2415
|
+
function startStdioTransport(app, options = {}) {
|
|
2416
|
+
return startStdioServer(app, options);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/server/serve.ts
|
|
2420
|
+
async function serve(config = loadRuntimeConfig2()) {
|
|
2421
|
+
const app = createApp(config);
|
|
2422
|
+
if (config.transport === "http") {
|
|
2423
|
+
return startHttpTransport(app, {
|
|
2424
|
+
port: config.port
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
return startStdioTransport(app);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
export {
|
|
2431
|
+
getServerStatus,
|
|
2432
|
+
createApp,
|
|
2433
|
+
serve
|
|
2434
|
+
};
|