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.
@@ -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("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&amp;", "&").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
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
+ };