@mailwoman/mcp 8.2.0 → 8.4.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/cli.ts CHANGED
@@ -6,14 +6,30 @@
6
6
  *
7
7
  * `mailwoman-mcp` — boot the MCP server over stdio. Wires the real `MCPToolDeps` (`tools.ts`) from the mailwoman
8
8
  * library: `createRuntimePipeline` for parse/POI-intent, `geocode-core`'s `geocodeAddress` for geocode,
9
- * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, and `@mailwoman/core/layers` for the layer
10
- * manifest tool.
9
+ * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, `@mailwoman/core/layers` for the layer
10
+ * manifest tool, and `@mailwoman/bdc`'s `filingLandscape`/`plausibilityCheck` for the two BDC tools.
11
11
  *
12
12
  * Deps are LAZY: nothing here loads the neural weights or opens a gazetteer db at startup — an MCP client
13
13
  * connects, lists tools, and may never call one (or may call `mailwoman_overpass_export`/`mailwoman_layer_manifest`,
14
14
  * neither of which needs the classifier at all). The shared classifier+resolver are built once, on the FIRST call
15
15
  * to any tool that needs them, and cached for the process lifetime.
16
16
  *
17
+ * **Graceful layer-absent guards (2b task 7, decision 6).** Both BDC-backed tools treat a missing/unreadable
18
+ * database file as absence, never a raw `node:sqlite` throw ("unable to open database file"): `bdcFilingLandscape`
19
+ * requires bdc.db unconditionally, so a missing file becomes one friendly thrown `Error` naming the layer;
20
+ * `plausibilityCheck`'s `bdcDB`/`poi` deps are each OPTIONAL, so a missing/absent `bdc_database_path`/
21
+ * `poi_database_path` degrades to the SAME typed-abstain evidence entry (`{type:"abstain",
22
+ * reason:"requires_bdc_layer"|"requires_build_local_layer"}`) the scorer already produces for an omitted dep. The
23
+ * guards themselves (`assertBDCDatabaseExists`, `openBDCDatabaseIfPresent`, `openPlausibilityPOIDeps`) live in
24
+ * `./layer-guards.ts`, NOT here — they're pure, transport-independent logic with no need for the stdio connection
25
+ * this file opens at import time (which is exactly why THIS file can't be unit-tested directly; see
26
+ * `layer-guards.test.ts` for their branch coverage).
27
+ *
28
+ * `mailwoman_filer_lookup` (3a task 7) follows the SAME "requires the layer unconditionally" discipline as
29
+ * `mailwoman_bdc_filing_landscape` (`assertFilerDatabaseExists` + `openFilerDatabaseIfPresent`, mirroring
30
+ * `assertBDCDatabaseExists` + the BDC open) — `filerLookup` itself has no optional-dep abstain shape (gate 4 makes
31
+ * it throw rather than answer unstamped), so a missing filer.db becomes one friendly thrown Error naming the layer.
32
+ *
17
33
  * ```sh
18
34
  * mailwoman-mcp # geocode/poi_search degrade gracefully with no poi.db wired
19
35
  * mailwoman-mcp --poi-db poi.db # mailwoman_poi_search additionally executes against poi.db
@@ -24,8 +40,10 @@ import { existsSync } from "node:fs"
24
40
  import { DatabaseSync } from "node:sqlite"
25
41
  import { parseArgs } from "node:util"
26
42
 
43
+ import { filingLandscape, plausibilityCheck, type BDCDatabase } from "@mailwoman/bdc"
27
44
  import { DatabaseClient } from "@mailwoman/core/kysley/client"
28
45
  import { readLayerManifest, type LayerContractDatabase } from "@mailwoman/core/layers"
46
+ import { filerLookup, toFRN, type FRN } from "@mailwoman/filer/sdk"
29
47
  import { NeuralAddressClassifier } from "@mailwoman/neural"
30
48
  import { getPOICategory } from "@mailwoman/poi-taxonomy"
31
49
  import { createWOFResolver, type Resolver } from "@mailwoman/resolver"
@@ -39,6 +57,13 @@ import {
39
57
  wofShardPaths,
40
58
  } from "mailwoman/resolver-backend"
41
59
 
60
+ import {
61
+ assertBDCDatabaseExists,
62
+ assertFilerDatabaseExists,
63
+ openBDCDatabaseIfPresent,
64
+ openFilerDatabaseIfPresent,
65
+ openPlausibilityPOIDeps,
66
+ } from "./layer-guards.ts"
42
67
  import { createMCPServer } from "./server.ts"
43
68
  import type { MCPToolDeps } from "./tools.ts"
44
69
 
@@ -117,6 +142,18 @@ async function getPoiPipeline(dbPath: string | undefined): Promise<Pipeline> {
117
142
  return pipeline
118
143
  }
119
144
 
145
+ /**
146
+ * `plausibilityCheck`'s geocode dep — reuses the SAME shared classifier+resolver `deps.geocode` builds from (see the
147
+ * module header's laziness note), wired at this CLI/MCP layer per the 2b task 5 brief ("`deriveGeocodeRegister`/
148
+ * formatted register is the geocode dep's concern, wired at the CLI/MCP layer"). The real return type (`GeocodeResult`)
149
+ * is structurally assignable to `plausibility.ts`'s minimal `GeocodeLike` — no adapter needed.
150
+ */
151
+ async function resolveGeocode(address: string) {
152
+ const { classifier, resolver, shards } = await loadCore()
153
+
154
+ return geocodeAddress(address, { classifier, resolver, shards: shards.for })
155
+ }
156
+
120
157
  const deps: MCPToolDeps = {
121
158
  async parse(text, opts) {
122
159
  const pipeline = opts?.poi ? await getPoiPipeline(poiDatabasePath) : await getPlainPipeline()
@@ -125,9 +162,7 @@ const deps: MCPToolDeps = {
125
162
  },
126
163
 
127
164
  async geocode(text) {
128
- const { classifier, resolver, shards } = await loadCore()
129
-
130
- return geocodeAddress(text, { classifier, resolver, shards: shards.for })
165
+ return resolveGeocode(text)
131
166
  },
132
167
 
133
168
  async poiSearch(q) {
@@ -177,6 +212,66 @@ const deps: MCPToolDeps = {
177
212
 
178
213
  return { manifest, coverage }
179
214
  },
215
+
216
+ async bdcFilingLandscape(q) {
217
+ // Decision 6 (2b task 7): `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
218
+ // abstain shape exists for this tool), so a missing file becomes a friendly thrown Error naming the layer —
219
+ // never the raw `node:sqlite` "unable to open database file" message.
220
+ assertBDCDatabaseExists("mailwoman_bdc_filing_landscape", q.databasePath)
221
+
222
+ using db = new DatabaseClient<BDCDatabase>({ database: new DatabaseSync(q.databasePath, { readOnly: true }) })
223
+
224
+ return filingLandscape(db, { geoids: q.geoids, h3Cells: q.h3Cells })
225
+ },
226
+
227
+ async plausibilityCheck(q) {
228
+ const bdcDB = openBDCDatabaseIfPresent(q.bdcDatabasePath)
229
+ const poi = await openPlausibilityPOIDeps(q.poiDatabasePath)
230
+
231
+ try {
232
+ return await plausibilityCheck(
233
+ {
234
+ address: q.address,
235
+ point: q.point,
236
+ geoid: q.geoid,
237
+ technologyCode: q.technologyCode,
238
+ claimedDownloadMbps: q.claimedDownloadMbps,
239
+ },
240
+ { bdcDB, poi, geocode: resolveGeocode }
241
+ )
242
+ } finally {
243
+ bdcDB?.destroy()
244
+ poi?.contractDB.destroy()
245
+ }
246
+ },
247
+
248
+ async filerLookup(q) {
249
+ // Decision 6/gate 4 (3a task 7): filerLookup has no optional-dep abstain shape — it throws rather than
250
+ // answer unstamped — so filer.db is required unconditionally, same discipline as bdc.db is for
251
+ // mailwoman_bdc_filing_landscape.
252
+ assertFilerDatabaseExists("mailwoman_filer_lookup", q.databasePath)
253
+
254
+ using db = openFilerDatabaseIfPresent(q.databasePath)!
255
+
256
+ let frn: FRN | undefined
257
+
258
+ if (q.frn !== undefined) {
259
+ const parsed = toFRN(q.frn)
260
+
261
+ if (!parsed) {
262
+ throw new Error(`mailwoman_filer_lookup: "${q.frn}" is not a valid FRN`)
263
+ }
264
+
265
+ frn = parsed
266
+ }
267
+
268
+ return filerLookup(db, {
269
+ frn,
270
+ form499ID: q.form499ID,
271
+ bdcProviderID: q.bdcProviderID,
272
+ asOf: q.asOf,
273
+ })
274
+ },
180
275
  }
181
276
 
182
277
  const server = createMCPServer(deps)
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Decision 6 (2b task 7) layer-absent guards — pulled out of `cli.ts` into their own importable module (task 7
7
+ * fix round 1 review finding) so the branching itself has direct unit coverage (`layer-guards.test.ts`), not just
8
+ * "the tool handler passes an abstain through" (`tools.test.ts`'s stub-level dispatch tests). `cli.ts`
9
+ * top-level-`await`s a real stdio transport connection at import time, so IT can't be imported by vitest — these
10
+ * three functions have no such dependency (pure existence-check + open, or a thrown Error), so they live here and
11
+ * `cli.ts` just calls them.
12
+ *
13
+ * - `openBDCDatabaseIfPresent` / `openPlausibilityPOIDeps` — `mailwoman_plausibility_check`'s `bdcDB`/`poi` deps
14
+ * (`PlausibilityDeps`) are each OPTIONAL, so a missing/absent path degrades to `undefined`, which
15
+ * `plausibilityCheck` (`@mailwoman/bdc`) already turns into a typed abstain evidence entry
16
+ * (`{type:"abstain", reason:"requires_bdc_layer"|"requires_build_local_layer"}`) — never a raw sqlite throw.
17
+ * - `assertBDCDatabaseExists` — `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
18
+ * abstain shape exists for that tool), so a missing file becomes one friendly thrown `Error` naming the layer
19
+ * instead of the raw `node:sqlite` "unable to open database file" message.
20
+ * - `openFilerDatabaseIfPresent` / `assertFilerDatabaseExists` (3a task 7) — the SAME pairing, for filer.db.
21
+ * `mailwoman_filer_lookup` requires filer.db unconditionally (mirrors `mailwoman_bdc_filing_landscape`'s own
22
+ * "requires the layer" discipline — `filerLookup` itself has no optional-dep abstain shape either, since gate
23
+ * 4 makes it throw rather than answer unstamped), so `cli.ts` pairs `assertFilerDatabaseExists` (the friendly
24
+ * throw) with `openFilerDatabaseIfPresent` (the actual open) the same way `bdcFilingLandscape`'s handler does.
25
+ */
26
+
27
+ import { existsSync } from "node:fs"
28
+ import { DatabaseSync } from "node:sqlite"
29
+
30
+ import type { BDCDatabase, PlausibilityDeps } from "@mailwoman/bdc"
31
+ import { DatabaseClient } from "@mailwoman/core/kysley/client"
32
+ import type { LayerContractDatabase } from "@mailwoman/core/layers"
33
+ import type { FilerDatabase } from "@mailwoman/filer"
34
+
35
+ /**
36
+ * Open a bdc.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw.
37
+ * See the module header.
38
+ */
39
+ export function openBDCDatabaseIfPresent(databasePath: string | undefined): DatabaseClient<BDCDatabase> | undefined {
40
+ if (!databasePath || !existsSync(databasePath)) return undefined
41
+
42
+ return new DatabaseClient<BDCDatabase>({ database: new DatabaseSync(databasePath, { readOnly: true }) })
43
+ }
44
+
45
+ /**
46
+ * Same graceful discipline as {@link openBDCDatabaseIfPresent}, for the poi.db side of `plausibilityCheck`'s deps —
47
+ * `undefined` here becomes the `{type:"abstain", reason:"requires_build_local_layer"}` entry `plausibilityCheck`
48
+ * already produces when a claimed technology's physical-plant categories can't be searched. `POILookup` is dynamically
49
+ * imported (matching `cli.ts`'s existing `resolver-wof-sqlite` laziness) since it's only ever needed when a caller
50
+ * actually wires a poi.db. `lookup` and `contractDB` share ONE `DatabaseSync` handle (the AGENTS.md "one connection,
51
+ * shared" convention) — a real poi.db's rows and its `layer_manifest`/`layer_coverage` tables live in the same file in
52
+ * production, so disposing `contractDB` (which closes the shared handle) is enough; `POILookup` never owns it
53
+ * (constructed with `{database}`, not `{databasePath}` — see `poi-lookup.ts`), so it never double-closes.
54
+ */
55
+ export async function openPlausibilityPOIDeps(databasePath: string | undefined): Promise<PlausibilityDeps["poi"]> {
56
+ if (!databasePath || !existsSync(databasePath)) return undefined
57
+
58
+ const { POILookup } = await import("@mailwoman/resolver-wof-sqlite/poi-lookup")
59
+ const database = new DatabaseSync(databasePath, { readOnly: true })
60
+
61
+ return {
62
+ lookup: new POILookup({ database }),
63
+ contractDB: new DatabaseClient<LayerContractDatabase>({ database }),
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_bdc_filing_landscape`'s guard
69
+ * (decision 6b). `toolName` is threaded through so the message matches whichever tool calls this (today: only
70
+ * `mailwoman_bdc_filing_landscape`).
71
+ */
72
+ export function assertBDCDatabaseExists(toolName: string, databasePath: string): void {
73
+ if (!existsSync(databasePath)) {
74
+ throw new Error(`${toolName}: bdc.db not found at "${databasePath}"`)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Open a filer.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw
80
+ * (3a task 7, mirroring {@link openBDCDatabaseIfPresent}). Used by `cli.ts`'s `mailwoman_filer_lookup` handler after
81
+ * {@link assertFilerDatabaseExists} has already confirmed the file is present.
82
+ */
83
+ export function openFilerDatabaseIfPresent(
84
+ databasePath: string | undefined
85
+ ): DatabaseClient<FilerDatabase> | undefined {
86
+ if (!databasePath || !existsSync(databasePath)) return undefined
87
+
88
+ return new DatabaseClient<FilerDatabase>({ database: new DatabaseSync(databasePath, { readOnly: true }) })
89
+ }
90
+
91
+ /**
92
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_filer_lookup`'s guard (3a
93
+ * task 7, mirroring {@link assertBDCDatabaseExists}). `filerLookup` itself has no optional-dep abstain shape (gate 4
94
+ * makes it throw rather than answer unstamped), so filer.db is required unconditionally, same as bdc.db is for
95
+ * `mailwoman_bdc_filing_landscape`.
96
+ */
97
+ export function assertFilerDatabaseExists(toolName: string, databasePath: string): void {
98
+ if (!existsSync(databasePath)) {
99
+ throw new Error(`${toolName}: filer.db not found at "${databasePath}"`)
100
+ }
101
+ }
package/out/cli.d.ts CHANGED
@@ -6,14 +6,30 @@
6
6
  *
7
7
  * `mailwoman-mcp` — boot the MCP server over stdio. Wires the real `MCPToolDeps` (`tools.ts`) from the mailwoman
8
8
  * library: `createRuntimePipeline` for parse/POI-intent, `geocode-core`'s `geocodeAddress` for geocode,
9
- * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, and `@mailwoman/core/layers` for the layer
10
- * manifest tool.
9
+ * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, `@mailwoman/core/layers` for the layer
10
+ * manifest tool, and `@mailwoman/bdc`'s `filingLandscape`/`plausibilityCheck` for the two BDC tools.
11
11
  *
12
12
  * Deps are LAZY: nothing here loads the neural weights or opens a gazetteer db at startup — an MCP client
13
13
  * connects, lists tools, and may never call one (or may call `mailwoman_overpass_export`/`mailwoman_layer_manifest`,
14
14
  * neither of which needs the classifier at all). The shared classifier+resolver are built once, on the FIRST call
15
15
  * to any tool that needs them, and cached for the process lifetime.
16
16
  *
17
+ * **Graceful layer-absent guards (2b task 7, decision 6).** Both BDC-backed tools treat a missing/unreadable
18
+ * database file as absence, never a raw `node:sqlite` throw ("unable to open database file"): `bdcFilingLandscape`
19
+ * requires bdc.db unconditionally, so a missing file becomes one friendly thrown `Error` naming the layer;
20
+ * `plausibilityCheck`'s `bdcDB`/`poi` deps are each OPTIONAL, so a missing/absent `bdc_database_path`/
21
+ * `poi_database_path` degrades to the SAME typed-abstain evidence entry (`{type:"abstain",
22
+ * reason:"requires_bdc_layer"|"requires_build_local_layer"}`) the scorer already produces for an omitted dep. The
23
+ * guards themselves (`assertBDCDatabaseExists`, `openBDCDatabaseIfPresent`, `openPlausibilityPOIDeps`) live in
24
+ * `./layer-guards.ts`, NOT here — they're pure, transport-independent logic with no need for the stdio connection
25
+ * this file opens at import time (which is exactly why THIS file can't be unit-tested directly; see
26
+ * `layer-guards.test.ts` for their branch coverage).
27
+ *
28
+ * `mailwoman_filer_lookup` (3a task 7) follows the SAME "requires the layer unconditionally" discipline as
29
+ * `mailwoman_bdc_filing_landscape` (`assertFilerDatabaseExists` + `openFilerDatabaseIfPresent`, mirroring
30
+ * `assertBDCDatabaseExists` + the BDC open) — `filerLookup` itself has no optional-dep abstain shape (gate 4 makes
31
+ * it throw rather than answer unstamped), so a missing filer.db becomes one friendly thrown Error naming the layer.
32
+ *
17
33
  * ```sh
18
34
  * mailwoman-mcp # geocode/poi_search degrade gracefully with no poi.db wired
19
35
  * mailwoman-mcp --poi-db poi.db # mailwoman_poi_search additionally executes against poi.db
package/out/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;GAmBG"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG"}
package/out/cli.js CHANGED
@@ -6,14 +6,30 @@
6
6
  *
7
7
  * `mailwoman-mcp` — boot the MCP server over stdio. Wires the real `MCPToolDeps` (`tools.ts`) from the mailwoman
8
8
  * library: `createRuntimePipeline` for parse/POI-intent, `geocode-core`'s `geocodeAddress` for geocode,
9
- * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, and `@mailwoman/core/layers` for the layer
10
- * manifest tool.
9
+ * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, `@mailwoman/core/layers` for the layer
10
+ * manifest tool, and `@mailwoman/bdc`'s `filingLandscape`/`plausibilityCheck` for the two BDC tools.
11
11
  *
12
12
  * Deps are LAZY: nothing here loads the neural weights or opens a gazetteer db at startup — an MCP client
13
13
  * connects, lists tools, and may never call one (or may call `mailwoman_overpass_export`/`mailwoman_layer_manifest`,
14
14
  * neither of which needs the classifier at all). The shared classifier+resolver are built once, on the FIRST call
15
15
  * to any tool that needs them, and cached for the process lifetime.
16
16
  *
17
+ * **Graceful layer-absent guards (2b task 7, decision 6).** Both BDC-backed tools treat a missing/unreadable
18
+ * database file as absence, never a raw `node:sqlite` throw ("unable to open database file"): `bdcFilingLandscape`
19
+ * requires bdc.db unconditionally, so a missing file becomes one friendly thrown `Error` naming the layer;
20
+ * `plausibilityCheck`'s `bdcDB`/`poi` deps are each OPTIONAL, so a missing/absent `bdc_database_path`/
21
+ * `poi_database_path` degrades to the SAME typed-abstain evidence entry (`{type:"abstain",
22
+ * reason:"requires_bdc_layer"|"requires_build_local_layer"}`) the scorer already produces for an omitted dep. The
23
+ * guards themselves (`assertBDCDatabaseExists`, `openBDCDatabaseIfPresent`, `openPlausibilityPOIDeps`) live in
24
+ * `./layer-guards.ts`, NOT here — they're pure, transport-independent logic with no need for the stdio connection
25
+ * this file opens at import time (which is exactly why THIS file can't be unit-tested directly; see
26
+ * `layer-guards.test.ts` for their branch coverage).
27
+ *
28
+ * `mailwoman_filer_lookup` (3a task 7) follows the SAME "requires the layer unconditionally" discipline as
29
+ * `mailwoman_bdc_filing_landscape` (`assertFilerDatabaseExists` + `openFilerDatabaseIfPresent`, mirroring
30
+ * `assertBDCDatabaseExists` + the BDC open) — `filerLookup` itself has no optional-dep abstain shape (gate 4 makes
31
+ * it throw rather than answer unstamped), so a missing filer.db becomes one friendly thrown Error naming the layer.
32
+ *
17
33
  * ```sh
18
34
  * mailwoman-mcp # geocode/poi_search degrade gracefully with no poi.db wired
19
35
  * mailwoman-mcp --poi-db poi.db # mailwoman_poi_search additionally executes against poi.db
@@ -22,8 +38,10 @@
22
38
  import { existsSync } from "node:fs";
23
39
  import { DatabaseSync } from "node:sqlite";
24
40
  import { parseArgs } from "node:util";
41
+ import { filingLandscape, plausibilityCheck } from "@mailwoman/bdc";
25
42
  import { DatabaseClient } from "@mailwoman/core/kysley/client";
26
43
  import { readLayerManifest } from "@mailwoman/core/layers";
44
+ import { filerLookup, toFRN } from "@mailwoman/filer/sdk";
27
45
  import { NeuralAddressClassifier } from "@mailwoman/neural";
28
46
  import { getPOICategory } from "@mailwoman/poi-taxonomy";
29
47
  import { createWOFResolver } from "@mailwoman/resolver";
@@ -31,6 +49,7 @@ import { createRuntimePipeline } from "mailwoman";
31
49
  import { geocodeAddress, ShardProvider } from "mailwoman/geocode-core";
32
50
  import { emitOverpassQL } from "mailwoman/poi-overpass";
33
51
  import { createResolverBackend, mailwomanDataRoot, resolveCandidateDBPath, wofShardPaths, } from "mailwoman/resolver-backend";
52
+ import { assertBDCDatabaseExists, assertFilerDatabaseExists, openBDCDatabaseIfPresent, openFilerDatabaseIfPresent, openPlausibilityPOIDeps, } from "./layer-guards.js";
34
53
  import { createMCPServer } from "./server.js";
35
54
  const { values } = parseArgs({
36
55
  options: {
@@ -91,14 +110,23 @@ async function getPoiPipeline(dbPath) {
91
110
  poiPipelines.set(key, pipeline);
92
111
  return pipeline;
93
112
  }
113
+ /**
114
+ * `plausibilityCheck`'s geocode dep — reuses the SAME shared classifier+resolver `deps.geocode` builds from (see the
115
+ * module header's laziness note), wired at this CLI/MCP layer per the 2b task 5 brief ("`deriveGeocodeRegister`/
116
+ * formatted register is the geocode dep's concern, wired at the CLI/MCP layer"). The real return type (`GeocodeResult`)
117
+ * is structurally assignable to `plausibility.ts`'s minimal `GeocodeLike` — no adapter needed.
118
+ */
119
+ async function resolveGeocode(address) {
120
+ const { classifier, resolver, shards } = await loadCore();
121
+ return geocodeAddress(address, { classifier, resolver, shards: shards.for });
122
+ }
94
123
  const deps = {
95
124
  async parse(text, opts) {
96
125
  const pipeline = opts?.poi ? await getPoiPipeline(poiDatabasePath) : await getPlainPipeline();
97
126
  return pipeline(text);
98
127
  },
99
128
  async geocode(text) {
100
- const { classifier, resolver, shards } = await loadCore();
101
- return geocodeAddress(text, { classifier, resolver, shards: shards.for });
129
+ return resolveGeocode(text);
102
130
  },
103
131
  async poiSearch(q) {
104
132
  const pipeline = await getPoiPipeline(q.poiDatabasePath ?? poiDatabasePath);
@@ -135,6 +163,52 @@ const deps = {
135
163
  .executeTakeFirst();
136
164
  return { manifest, coverage };
137
165
  },
166
+ async bdcFilingLandscape(q) {
167
+ // Decision 6 (2b task 7): `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
168
+ // abstain shape exists for this tool), so a missing file becomes a friendly thrown Error naming the layer —
169
+ // never the raw `node:sqlite` "unable to open database file" message.
170
+ assertBDCDatabaseExists("mailwoman_bdc_filing_landscape", q.databasePath);
171
+ using db = new DatabaseClient({ database: new DatabaseSync(q.databasePath, { readOnly: true }) });
172
+ return filingLandscape(db, { geoids: q.geoids, h3Cells: q.h3Cells });
173
+ },
174
+ async plausibilityCheck(q) {
175
+ const bdcDB = openBDCDatabaseIfPresent(q.bdcDatabasePath);
176
+ const poi = await openPlausibilityPOIDeps(q.poiDatabasePath);
177
+ try {
178
+ return await plausibilityCheck({
179
+ address: q.address,
180
+ point: q.point,
181
+ geoid: q.geoid,
182
+ technologyCode: q.technologyCode,
183
+ claimedDownloadMbps: q.claimedDownloadMbps,
184
+ }, { bdcDB, poi, geocode: resolveGeocode });
185
+ }
186
+ finally {
187
+ bdcDB?.destroy();
188
+ poi?.contractDB.destroy();
189
+ }
190
+ },
191
+ async filerLookup(q) {
192
+ // Decision 6/gate 4 (3a task 7): filerLookup has no optional-dep abstain shape — it throws rather than
193
+ // answer unstamped — so filer.db is required unconditionally, same discipline as bdc.db is for
194
+ // mailwoman_bdc_filing_landscape.
195
+ assertFilerDatabaseExists("mailwoman_filer_lookup", q.databasePath);
196
+ using db = openFilerDatabaseIfPresent(q.databasePath);
197
+ let frn;
198
+ if (q.frn !== undefined) {
199
+ const parsed = toFRN(q.frn);
200
+ if (!parsed) {
201
+ throw new Error(`mailwoman_filer_lookup: "${q.frn}" is not a valid FRN`);
202
+ }
203
+ frn = parsed;
204
+ }
205
+ return filerLookup(db, {
206
+ frn,
207
+ form499ID: q.form499ID,
208
+ bdcProviderID: q.bdcProviderID,
209
+ asOf: q.asOf,
210
+ });
211
+ },
138
212
  };
139
213
  const server = createMCPServer(deps);
140
214
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
package/out/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Decision 6 (2b task 7) layer-absent guards — pulled out of `cli.ts` into their own importable module (task 7
7
+ * fix round 1 review finding) so the branching itself has direct unit coverage (`layer-guards.test.ts`), not just
8
+ * "the tool handler passes an abstain through" (`tools.test.ts`'s stub-level dispatch tests). `cli.ts`
9
+ * top-level-`await`s a real stdio transport connection at import time, so IT can't be imported by vitest — these
10
+ * three functions have no such dependency (pure existence-check + open, or a thrown Error), so they live here and
11
+ * `cli.ts` just calls them.
12
+ *
13
+ * - `openBDCDatabaseIfPresent` / `openPlausibilityPOIDeps` — `mailwoman_plausibility_check`'s `bdcDB`/`poi` deps
14
+ * (`PlausibilityDeps`) are each OPTIONAL, so a missing/absent path degrades to `undefined`, which
15
+ * `plausibilityCheck` (`@mailwoman/bdc`) already turns into a typed abstain evidence entry
16
+ * (`{type:"abstain", reason:"requires_bdc_layer"|"requires_build_local_layer"}`) — never a raw sqlite throw.
17
+ * - `assertBDCDatabaseExists` — `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
18
+ * abstain shape exists for that tool), so a missing file becomes one friendly thrown `Error` naming the layer
19
+ * instead of the raw `node:sqlite` "unable to open database file" message.
20
+ * - `openFilerDatabaseIfPresent` / `assertFilerDatabaseExists` (3a task 7) — the SAME pairing, for filer.db.
21
+ * `mailwoman_filer_lookup` requires filer.db unconditionally (mirrors `mailwoman_bdc_filing_landscape`'s own
22
+ * "requires the layer" discipline — `filerLookup` itself has no optional-dep abstain shape either, since gate
23
+ * 4 makes it throw rather than answer unstamped), so `cli.ts` pairs `assertFilerDatabaseExists` (the friendly
24
+ * throw) with `openFilerDatabaseIfPresent` (the actual open) the same way `bdcFilingLandscape`'s handler does.
25
+ */
26
+ import type { BDCDatabase, PlausibilityDeps } from "@mailwoman/bdc";
27
+ import { DatabaseClient } from "@mailwoman/core/kysley/client";
28
+ import type { FilerDatabase } from "@mailwoman/filer";
29
+ /**
30
+ * Open a bdc.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw.
31
+ * See the module header.
32
+ */
33
+ export declare function openBDCDatabaseIfPresent(databasePath: string | undefined): DatabaseClient<BDCDatabase> | undefined;
34
+ /**
35
+ * Same graceful discipline as {@link openBDCDatabaseIfPresent}, for the poi.db side of `plausibilityCheck`'s deps —
36
+ * `undefined` here becomes the `{type:"abstain", reason:"requires_build_local_layer"}` entry `plausibilityCheck`
37
+ * already produces when a claimed technology's physical-plant categories can't be searched. `POILookup` is dynamically
38
+ * imported (matching `cli.ts`'s existing `resolver-wof-sqlite` laziness) since it's only ever needed when a caller
39
+ * actually wires a poi.db. `lookup` and `contractDB` share ONE `DatabaseSync` handle (the AGENTS.md "one connection,
40
+ * shared" convention) — a real poi.db's rows and its `layer_manifest`/`layer_coverage` tables live in the same file in
41
+ * production, so disposing `contractDB` (which closes the shared handle) is enough; `POILookup` never owns it
42
+ * (constructed with `{database}`, not `{databasePath}` — see `poi-lookup.ts`), so it never double-closes.
43
+ */
44
+ export declare function openPlausibilityPOIDeps(databasePath: string | undefined): Promise<PlausibilityDeps["poi"]>;
45
+ /**
46
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_bdc_filing_landscape`'s guard
47
+ * (decision 6b). `toolName` is threaded through so the message matches whichever tool calls this (today: only
48
+ * `mailwoman_bdc_filing_landscape`).
49
+ */
50
+ export declare function assertBDCDatabaseExists(toolName: string, databasePath: string): void;
51
+ /**
52
+ * Open a filer.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw
53
+ * (3a task 7, mirroring {@link openBDCDatabaseIfPresent}). Used by `cli.ts`'s `mailwoman_filer_lookup` handler after
54
+ * {@link assertFilerDatabaseExists} has already confirmed the file is present.
55
+ */
56
+ export declare function openFilerDatabaseIfPresent(databasePath: string | undefined): DatabaseClient<FilerDatabase> | undefined;
57
+ /**
58
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_filer_lookup`'s guard (3a
59
+ * task 7, mirroring {@link assertBDCDatabaseExists}). `filerLookup` itself has no optional-dep abstain shape (gate 4
60
+ * makes it throw rather than answer unstamped), so filer.db is required unconditionally, same as bdc.db is for
61
+ * `mailwoman_bdc_filing_landscape`.
62
+ */
63
+ export declare function assertFilerDatabaseExists(toolName: string, databasePath: string): void;
64
+ //# sourceMappingURL=layer-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer-guards.d.ts","sourceRoot":"","sources":["../layer-guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,CAAC,WAAW,CAAC,GAAG,SAAS,CAIlH;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAUhH;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAIpF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACzC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,cAAc,CAAC,aAAa,CAAC,GAAG,SAAS,CAI3C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAItF"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Decision 6 (2b task 7) layer-absent guards — pulled out of `cli.ts` into their own importable module (task 7
7
+ * fix round 1 review finding) so the branching itself has direct unit coverage (`layer-guards.test.ts`), not just
8
+ * "the tool handler passes an abstain through" (`tools.test.ts`'s stub-level dispatch tests). `cli.ts`
9
+ * top-level-`await`s a real stdio transport connection at import time, so IT can't be imported by vitest — these
10
+ * three functions have no such dependency (pure existence-check + open, or a thrown Error), so they live here and
11
+ * `cli.ts` just calls them.
12
+ *
13
+ * - `openBDCDatabaseIfPresent` / `openPlausibilityPOIDeps` — `mailwoman_plausibility_check`'s `bdcDB`/`poi` deps
14
+ * (`PlausibilityDeps`) are each OPTIONAL, so a missing/absent path degrades to `undefined`, which
15
+ * `plausibilityCheck` (`@mailwoman/bdc`) already turns into a typed abstain evidence entry
16
+ * (`{type:"abstain", reason:"requires_bdc_layer"|"requires_build_local_layer"}`) — never a raw sqlite throw.
17
+ * - `assertBDCDatabaseExists` — `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
18
+ * abstain shape exists for that tool), so a missing file becomes one friendly thrown `Error` naming the layer
19
+ * instead of the raw `node:sqlite` "unable to open database file" message.
20
+ * - `openFilerDatabaseIfPresent` / `assertFilerDatabaseExists` (3a task 7) — the SAME pairing, for filer.db.
21
+ * `mailwoman_filer_lookup` requires filer.db unconditionally (mirrors `mailwoman_bdc_filing_landscape`'s own
22
+ * "requires the layer" discipline — `filerLookup` itself has no optional-dep abstain shape either, since gate
23
+ * 4 makes it throw rather than answer unstamped), so `cli.ts` pairs `assertFilerDatabaseExists` (the friendly
24
+ * throw) with `openFilerDatabaseIfPresent` (the actual open) the same way `bdcFilingLandscape`'s handler does.
25
+ */
26
+ import { existsSync } from "node:fs";
27
+ import { DatabaseSync } from "node:sqlite";
28
+ import { DatabaseClient } from "@mailwoman/core/kysley/client";
29
+ /**
30
+ * Open a bdc.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw.
31
+ * See the module header.
32
+ */
33
+ export function openBDCDatabaseIfPresent(databasePath) {
34
+ if (!databasePath || !existsSync(databasePath))
35
+ return undefined;
36
+ return new DatabaseClient({ database: new DatabaseSync(databasePath, { readOnly: true }) });
37
+ }
38
+ /**
39
+ * Same graceful discipline as {@link openBDCDatabaseIfPresent}, for the poi.db side of `plausibilityCheck`'s deps —
40
+ * `undefined` here becomes the `{type:"abstain", reason:"requires_build_local_layer"}` entry `plausibilityCheck`
41
+ * already produces when a claimed technology's physical-plant categories can't be searched. `POILookup` is dynamically
42
+ * imported (matching `cli.ts`'s existing `resolver-wof-sqlite` laziness) since it's only ever needed when a caller
43
+ * actually wires a poi.db. `lookup` and `contractDB` share ONE `DatabaseSync` handle (the AGENTS.md "one connection,
44
+ * shared" convention) — a real poi.db's rows and its `layer_manifest`/`layer_coverage` tables live in the same file in
45
+ * production, so disposing `contractDB` (which closes the shared handle) is enough; `POILookup` never owns it
46
+ * (constructed with `{database}`, not `{databasePath}` — see `poi-lookup.ts`), so it never double-closes.
47
+ */
48
+ export async function openPlausibilityPOIDeps(databasePath) {
49
+ if (!databasePath || !existsSync(databasePath))
50
+ return undefined;
51
+ const { POILookup } = await import("@mailwoman/resolver-wof-sqlite/poi-lookup");
52
+ const database = new DatabaseSync(databasePath, { readOnly: true });
53
+ return {
54
+ lookup: new POILookup({ database }),
55
+ contractDB: new DatabaseClient({ database }),
56
+ };
57
+ }
58
+ /**
59
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_bdc_filing_landscape`'s guard
60
+ * (decision 6b). `toolName` is threaded through so the message matches whichever tool calls this (today: only
61
+ * `mailwoman_bdc_filing_landscape`).
62
+ */
63
+ export function assertBDCDatabaseExists(toolName, databasePath) {
64
+ if (!existsSync(databasePath)) {
65
+ throw new Error(`${toolName}: bdc.db not found at "${databasePath}"`);
66
+ }
67
+ }
68
+ /**
69
+ * Open a filer.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw
70
+ * (3a task 7, mirroring {@link openBDCDatabaseIfPresent}). Used by `cli.ts`'s `mailwoman_filer_lookup` handler after
71
+ * {@link assertFilerDatabaseExists} has already confirmed the file is present.
72
+ */
73
+ export function openFilerDatabaseIfPresent(databasePath) {
74
+ if (!databasePath || !existsSync(databasePath))
75
+ return undefined;
76
+ return new DatabaseClient({ database: new DatabaseSync(databasePath, { readOnly: true }) });
77
+ }
78
+ /**
79
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_filer_lookup`'s guard (3a
80
+ * task 7, mirroring {@link assertBDCDatabaseExists}). `filerLookup` itself has no optional-dep abstain shape (gate 4
81
+ * makes it throw rather than answer unstamped), so filer.db is required unconditionally, same as bdc.db is for
82
+ * `mailwoman_bdc_filing_landscape`.
83
+ */
84
+ export function assertFilerDatabaseExists(toolName, databasePath) {
85
+ if (!existsSync(databasePath)) {
86
+ throw new Error(`${toolName}: filer.db not found at "${databasePath}"`);
87
+ }
88
+ }
89
+ //# sourceMappingURL=layer-guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer-guards.js","sourceRoot":"","sources":["../layer-guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG1C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAI9D;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,YAAgC;IACxE,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAA;IAEhE,OAAO,IAAI,cAAc,CAAc,EAAE,QAAQ,EAAE,IAAI,YAAY,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;AACzG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,YAAgC;IAC7E,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAA;IAEhE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,2CAA2C,CAAC,CAAA;IAC/E,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAEnE,OAAO;QACN,MAAM,EAAE,IAAI,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;QACnC,UAAU,EAAE,IAAI,cAAc,CAAwB,EAAE,QAAQ,EAAE,CAAC;KACnE,CAAA;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAgB,EAAE,YAAoB;IAC7E,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,0BAA0B,YAAY,GAAG,CAAC,CAAA;IACtE,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACzC,YAAgC;IAEhC,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAA;IAEhE,OAAO,IAAI,cAAc,CAAgB,EAAE,QAAQ,EAAE,IAAI,YAAY,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;AAC3G,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAAgB,EAAE,YAAoB;IAC/E,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,4BAA4B,YAAY,GAAG,CAAC,CAAA;IACxE,CAAC;AACF,CAAC"}
package/out/server.d.ts CHANGED
@@ -12,9 +12,9 @@ import { type MCPToolDeps } from "./tools.ts";
12
12
  /**
13
13
  * Build an `McpServer` with every `tools.ts` tool registered. A handler's returned value is JSON-stringified into a
14
14
  * single `text` content block — every tool here answers with structured data (parse trees, geocode results, search
15
- * hits), so a plain JSON text block is the simplest faithful rendering; none of the five tools need images, resource
16
- * links, or other MCP content kinds. A thrown error becomes an `isError` tool result instead of a protocol-level
17
- * failure, so a bad address / missing db surfaces to the agent as a normal (if unsuccessful) tool call.
15
+ * hits), so a plain JSON text block is the simplest faithful rendering; none of the tools need images, resource links,
16
+ * or other MCP content kinds. A thrown error becomes an `isError` tool result instead of a protocol-level failure, so a
17
+ * bad address / missing db surfaces to the agent as a normal (if unsuccessful) tool call.
18
18
  */
19
19
  export declare function createMCPServer(deps: MCPToolDeps): McpServer;
20
20
  //# sourceMappingURL=server.d.ts.map
package/out/server.js CHANGED
@@ -19,9 +19,9 @@ const MCP_SERVER_VERSION = "7.1.0";
19
19
  /**
20
20
  * Build an `McpServer` with every `tools.ts` tool registered. A handler's returned value is JSON-stringified into a
21
21
  * single `text` content block — every tool here answers with structured data (parse trees, geocode results, search
22
- * hits), so a plain JSON text block is the simplest faithful rendering; none of the five tools need images, resource
23
- * links, or other MCP content kinds. A thrown error becomes an `isError` tool result instead of a protocol-level
24
- * failure, so a bad address / missing db surfaces to the agent as a normal (if unsuccessful) tool call.
22
+ * hits), so a plain JSON text block is the simplest faithful rendering; none of the tools need images, resource links,
23
+ * or other MCP content kinds. A thrown error becomes an `isError` tool result instead of a protocol-level failure, so a
24
+ * bad address / missing db surfaces to the agent as a normal (if unsuccessful) tool call.
25
25
  */
26
26
  export function createMCPServer(deps) {
27
27
  const server = new McpServer({ name: "mailwoman", version: MCP_SERVER_VERSION });
package/out/tools.d.ts CHANGED
@@ -8,7 +8,9 @@
8
8
  * file (the actual product surface) is testable without any MCP plumbing — `tools.test.ts` calls
9
9
  * `buildToolTable` directly with stub deps.
10
10
  *
11
- * Five tools, one per capability the exotic-POI arc's other packages expose to a human/CLI caller:
11
+ * One tool per capability the exotic-POI/BDC arcs' other packages expose to a human/CLI caller — see
12
+ * `buildToolTable`'s return value for the authoritative, current list (this comment intentionally states no count,
13
+ * so it can't go stale as tools are added):
12
14
  *
13
15
  * - `mailwoman_parse` — the runtime pipeline's parse (optionally POI-aware).
14
16
  * - `mailwoman_geocode` — the street-level geocode cascade (`mailwoman/geocode-core`).
@@ -17,6 +19,16 @@
17
19
  * we never run it".
18
20
  * - `mailwoman_layer_manifest` — read a spatial-layer database's provenance manifest + coverage summary
19
21
  * (`@mailwoman/core/layers`).
22
+ * - `mailwoman_bdc_filing_landscape` — read a bdc.db layer's provider/technology/speed-bucket filing census over a
23
+ * set of census blocks or H3 cells (`@mailwoman/bdc`'s `filingLandscape`).
24
+ * - `mailwoman_plausibility_check` — score one claimed broadband-service assertion against BDC filing evidence and
25
+ * nearby telecom infrastructure (`@mailwoman/bdc`'s `plausibilityCheck`), returning a positive-evidence-only bundle
26
+ * with an always-present `coverage_confidence`. A missing/absent `bdc_database_path`/`poi_database_path` degrades
27
+ * to a typed abstain entry in the bundle, never a throw (2b task 7, decision 6).
28
+ * - `mailwoman_filer_lookup` (3a task 7) — read the FCC filer identity crosswalk (`@mailwoman/filer`'s
29
+ * `filerLookup`) for one identifier (FRN, Form 499 ID, or BDC provider ID): every OTHER identifier it shares an
30
+ * authoritative edge with, its current attributes, its authoritative entity cluster, and any inferred links —
31
+ * reported separately, never merged into the cluster. `as_of` is always present (defaults to today).
20
32
  */
21
33
  import { z } from "zod";
22
34
  /**
@@ -33,6 +45,30 @@ export interface MCPToolDeps {
33
45
  }) => Promise<unknown>;
34
46
  overpassExport: (query: string) => Promise<string>;
35
47
  layerManifest: (databasePath: string) => Promise<unknown>;
48
+ bdcFilingLandscape: (q: {
49
+ databasePath: string;
50
+ geoids?: string[];
51
+ h3Cells?: number[];
52
+ }) => Promise<unknown>;
53
+ plausibilityCheck: (q: {
54
+ bdcDatabasePath?: string;
55
+ poiDatabasePath?: string;
56
+ address?: string;
57
+ point?: {
58
+ type: "Point";
59
+ coordinates: [number, number];
60
+ };
61
+ geoid?: string;
62
+ technologyCode: number;
63
+ claimedDownloadMbps: number;
64
+ }) => Promise<unknown>;
65
+ filerLookup: (q: {
66
+ databasePath: string;
67
+ frn?: string;
68
+ form499ID?: string;
69
+ bdcProviderID?: number;
70
+ asOf?: string;
71
+ }) => Promise<unknown>;
36
72
  }
37
73
  /**
38
74
  * One MCP tool. `inputSchema` is a plain Zod object (not `any` — this repo's oxlint config errors on
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,SAAS,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/E,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClD,aAAa,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACzD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;IACvC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5D;AA2CD;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,EAAE,CAqE9D"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,SAAS,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/E,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClD,aAAa,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACzD,kBAAkB,EAAE,CAAC,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5G,iBAAiB,EAAE,CAAC,CAAC,EAAE;QACtB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,OAAO,CAAC;YAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;SAAE,CAAA;QACxD,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,cAAc,EAAE,MAAM,CAAA;QACtB,mBAAmB,EAAE,MAAM,CAAA;KAC3B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACtB,WAAW,EAAE,CAAC,CAAC,EAAE;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,IAAI,CAAC,EAAE,MAAM,CAAA;KACb,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;IACvC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5D;AAoJD;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,EAAE,CAiI9D"}
package/out/tools.js CHANGED
@@ -8,7 +8,9 @@
8
8
  * file (the actual product surface) is testable without any MCP plumbing — `tools.test.ts` calls
9
9
  * `buildToolTable` directly with stub deps.
10
10
  *
11
- * Five tools, one per capability the exotic-POI arc's other packages expose to a human/CLI caller:
11
+ * One tool per capability the exotic-POI/BDC arcs' other packages expose to a human/CLI caller — see
12
+ * `buildToolTable`'s return value for the authoritative, current list (this comment intentionally states no count,
13
+ * so it can't go stale as tools are added):
12
14
  *
13
15
  * - `mailwoman_parse` — the runtime pipeline's parse (optionally POI-aware).
14
16
  * - `mailwoman_geocode` — the street-level geocode cascade (`mailwoman/geocode-core`).
@@ -17,6 +19,16 @@
17
19
  * we never run it".
18
20
  * - `mailwoman_layer_manifest` — read a spatial-layer database's provenance manifest + coverage summary
19
21
  * (`@mailwoman/core/layers`).
22
+ * - `mailwoman_bdc_filing_landscape` — read a bdc.db layer's provider/technology/speed-bucket filing census over a
23
+ * set of census blocks or H3 cells (`@mailwoman/bdc`'s `filingLandscape`).
24
+ * - `mailwoman_plausibility_check` — score one claimed broadband-service assertion against BDC filing evidence and
25
+ * nearby telecom infrastructure (`@mailwoman/bdc`'s `plausibilityCheck`), returning a positive-evidence-only bundle
26
+ * with an always-present `coverage_confidence`. A missing/absent `bdc_database_path`/`poi_database_path` degrades
27
+ * to a typed abstain entry in the bundle, never a throw (2b task 7, decision 6).
28
+ * - `mailwoman_filer_lookup` (3a task 7) — read the FCC filer identity crosswalk (`@mailwoman/filer`'s
29
+ * `filerLookup`) for one identifier (FRN, Form 499 ID, or BDC provider ID): every OTHER identifier it shares an
30
+ * authoritative edge with, its current attributes, its authoritative entity cluster, and any inferred links —
31
+ * reported separately, never merged into the cluster. `as_of` is always present (defaults to today).
20
32
  */
21
33
  import { z } from "zod";
22
34
  const ParseInputSchema = z.object({
@@ -51,6 +63,83 @@ const LayerManifestInputSchema = z.object({
51
63
  .min(1)
52
64
  .describe("Path to a mailwoman spatial-layer database (poi.db, an address-points shard, etc.)."),
53
65
  });
66
+ const BDCFilingLandscapeInputSchema = z.object({
67
+ database_path: z
68
+ .string()
69
+ .min(1)
70
+ .describe("Path to a bdc.db layer database (FCC Broadband Data Collection availability)."),
71
+ geoids: z
72
+ .array(z.string())
73
+ .min(1)
74
+ .optional()
75
+ .describe("15-character census block GEOIDs to query. Provide exactly one of `geoids` or `h3_cells` — never both, never neither, never empty."),
76
+ h3_cells: z
77
+ .array(z.number())
78
+ .min(1)
79
+ .optional()
80
+ .describe("Resolution-9 short H3 cell integers (the bdc.db availability spine) to query directly. Provide exactly " +
81
+ "one of `geoids` or `h3_cells` — never both, never neither, never empty."),
82
+ });
83
+ const PlausibilityCheckPointInputSchema = z.object({
84
+ type: z.literal("Point").describe('GeoJSON geometry type — always "Point".'),
85
+ coordinates: z
86
+ .tuple([z.number(), z.number()])
87
+ .describe("[longitude, latitude] pair, GeoJSON coordinate order (longitude first)."),
88
+ });
89
+ const PlausibilityCheckInputSchema = z.object({
90
+ bdc_database_path: z
91
+ .string()
92
+ .optional()
93
+ .describe("Path to a bdc.db layer database (FCC Broadband Data Collection availability). Omit — or point at a file " +
94
+ "that doesn't exist — to abstain on filing evidence rather than error."),
95
+ poi_database_path: z
96
+ .string()
97
+ .optional()
98
+ .describe("Path to a poi.db layer database carrying the telecom-infrastructure categories (`telecom_exchange`, " +
99
+ "`tower_comms`, etc.). Omit — or point at a file that doesn't exist — to abstain on physical-plant " +
100
+ "evidence rather than error."),
101
+ address: z
102
+ .string()
103
+ .optional()
104
+ .describe("The claimed service location as a free-text postal address, geocoded via the server's runtime pipeline " +
105
+ "when `point` isn't also given. At least one of `address`, `point`, or `geoid` is required."),
106
+ point: PlausibilityCheckPointInputSchema.optional().describe("The claimed service location as a GeoJSON Point, bypassing geocoding. At least one of `address`, `point`, " +
107
+ "or `geoid` is required."),
108
+ geoid: z
109
+ .string()
110
+ .optional()
111
+ .describe("15-character census block GEOID for the claimed service location — the exact, native filing-evidence " +
112
+ "path (no h3-cell approximation). May be supplied alongside `point`/`address` (geoid drives filing " +
113
+ "evidence; the point drives physical-plant evidence independently). At least one of `address`, " +
114
+ "`point`, or `geoid` is required."),
115
+ technology_code: z
116
+ .number()
117
+ .describe("FCC BDC technology code for the claimed service, e.g. 50 = optical carrier fiber (BroadbandTechnologyCode)."),
118
+ claimed_download_mbps: z.number().describe("The claimed downstream speed in Mbps."),
119
+ });
120
+ const FilerLookupInputSchema = z.object({
121
+ database_path: z.string().min(1).describe("Path to a filer.db layer database (FCC filer identity crosswalk)."),
122
+ frn: z
123
+ .string()
124
+ .optional()
125
+ .describe("The zero-padded 10-digit FCC Registration Number to look up, e.g. '0001753557'. Provide exactly one of " +
126
+ "`frn`, `form499_id`, or `bdc_provider_id` — never more than one, never none."),
127
+ form499_id: z
128
+ .string()
129
+ .optional()
130
+ .describe("The FCC Form 499 filer ID to look up. Provide exactly one of `frn`, `form499_id`, or `bdc_provider_id` — " +
131
+ "never more than one, never none."),
132
+ bdc_provider_id: z
133
+ .number()
134
+ .optional()
135
+ .describe("The FCC BDC provider_id to look up. Provide exactly one of `frn`, `form499_id`, or `bdc_provider_id` — " +
136
+ "never more than one, never none."),
137
+ as_of: z
138
+ .string()
139
+ .optional()
140
+ .describe("ISO date (YYYY-MM-DD) to scope the lookup as-of — only relationships valid on or before this date, and not " +
141
+ "yet closed by it, are included. Defaults to today; the result always states the date actually used."),
142
+ });
54
143
  /**
55
144
  * Build the tool table for a concrete `MCPToolDeps` implementation. Pure — no transport, no I/O of its own.
56
145
  */
@@ -112,6 +201,59 @@ export function buildToolTable(deps) {
112
201
  return deps.layerManifest(databasePath);
113
202
  },
114
203
  },
204
+ {
205
+ name: "mailwoman_bdc_filing_landscape",
206
+ description: "Read the FCC Broadband Data Collection (BDC) provider/technology/speed-bucket filing census over a set " +
207
+ "of census blocks (`geoids`) or H3 cells (`h3_cells`) from a bdc.db layer database. Returns the source " +
208
+ "vintage, how many queried blocks were surveyed vs. unknown (never surveyed), and the filing summary. " +
209
+ "Provide exactly one of `geoids` or `h3_cells`.",
210
+ inputSchema: BDCFilingLandscapeInputSchema,
211
+ handler: async (args) => {
212
+ const { database_path, geoids, h3_cells } = BDCFilingLandscapeInputSchema.parse(args);
213
+ return deps.bdcFilingLandscape({ databasePath: database_path, geoids, h3Cells: h3_cells });
214
+ },
215
+ },
216
+ {
217
+ name: "mailwoman_plausibility_check",
218
+ description: "Score one claimed broadband-service assertion (technology + speed at a location) against the FCC BDC " +
219
+ "filing census and nearby telecom infrastructure, returning an evidence bundle: filings that corroborate " +
220
+ "(or don't), nearby physical plant, and a coverage_confidence reflecting survey completeness. NEVER " +
221
+ "returns a verdict stronger than 'no supporting evidence found' — absence of evidence degrades " +
222
+ "confidence, it never disproves the claim. Provide at least one of `address`, `point`, or `geoid`.",
223
+ inputSchema: PlausibilityCheckInputSchema,
224
+ handler: async (args) => {
225
+ const { bdc_database_path, poi_database_path, address, point, geoid, technology_code, claimed_download_mbps } = PlausibilityCheckInputSchema.parse(args);
226
+ return deps.plausibilityCheck({
227
+ bdcDatabasePath: bdc_database_path,
228
+ poiDatabasePath: poi_database_path,
229
+ address,
230
+ point,
231
+ geoid,
232
+ technologyCode: technology_code,
233
+ claimedDownloadMbps: claimed_download_mbps,
234
+ });
235
+ },
236
+ },
237
+ {
238
+ name: "mailwoman_filer_lookup",
239
+ description: "Look up the FCC filer identity crosswalk for one identifier (FRN, Form 499 filer ID, or BDC provider_id) " +
240
+ "from a filer.db layer database: every OTHER identifier it shares an authoritative edge with (never " +
241
+ "collapsed — a provider_id carrying multiple FRNs reports all of them), its current attributes, its " +
242
+ "authoritative entity cluster, and any inferred links reported SEPARATELY with their score — never merged " +
243
+ "into the cluster. Scoped `as_of` a date (defaults to today, always present in the result). Provide " +
244
+ "exactly one of `frn`, `form499_id`, or `bdc_provider_id`.",
245
+ inputSchema: FilerLookupInputSchema,
246
+ handler: async (args) => {
247
+ const { database_path, frn, form499_id, bdc_provider_id, as_of } = FilerLookupInputSchema.parse(args);
248
+ return deps.filerLookup({
249
+ databasePath: database_path,
250
+ frn,
251
+ form499ID: form499_id,
252
+ bdcProviderID: bdc_provider_id,
253
+ asOf: as_of,
254
+ });
255
+ },
256
+ },
115
257
  ];
116
258
  }
117
259
  //# sourceMappingURL=tools.js.map
package/out/tools.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","sourceRoot":"","sources":["../tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../tools.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailwoman/mcp",
3
- "version": "8.2.0",
3
+ "version": "8.4.0",
4
4
  "description": "MCP server — mailwoman's spatial toolset for agents (parse, geocode, poi_search, overpass_export, layer_manifest).",
5
5
  "license": "AGPL-3.0-only OR LicenseRef-Commercial",
6
6
  "repository": {
@@ -45,17 +45,19 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@mailwoman/core": "8.2.0",
49
- "@mailwoman/neural": "8.2.0",
50
- "@mailwoman/poi-taxonomy": "8.2.0",
51
- "@mailwoman/resolver": "8.2.0",
52
- "@mailwoman/resolver-wof-sqlite": "8.2.0",
48
+ "@mailwoman/bdc": "8.3.0",
49
+ "@mailwoman/core": "8.4.0",
50
+ "@mailwoman/filer": "8.3.0",
51
+ "@mailwoman/neural": "8.4.0",
52
+ "@mailwoman/poi-taxonomy": "8.4.0",
53
+ "@mailwoman/resolver": "8.4.0",
54
+ "@mailwoman/resolver-wof-sqlite": "8.4.0",
53
55
  "@modelcontextprotocol/sdk": "^1.29.0",
54
- "mailwoman": "8.2.0",
56
+ "mailwoman": "8.4.0",
55
57
  "zod": "^4.4.3"
56
58
  },
57
59
  "peerDependencies": {
58
- "@mailwoman/resolver-wof-sqlite": "8.2.0"
60
+ "@mailwoman/resolver-wof-sqlite": "8.4.0"
59
61
  },
60
62
  "peerDependenciesMeta": {
61
63
  "@mailwoman/resolver-wof-sqlite": {
package/server.ts CHANGED
@@ -24,9 +24,9 @@ const MCP_SERVER_VERSION = "7.1.0"
24
24
  /**
25
25
  * Build an `McpServer` with every `tools.ts` tool registered. A handler's returned value is JSON-stringified into a
26
26
  * single `text` content block — every tool here answers with structured data (parse trees, geocode results, search
27
- * hits), so a plain JSON text block is the simplest faithful rendering; none of the five tools need images, resource
28
- * links, or other MCP content kinds. A thrown error becomes an `isError` tool result instead of a protocol-level
29
- * failure, so a bad address / missing db surfaces to the agent as a normal (if unsuccessful) tool call.
27
+ * hits), so a plain JSON text block is the simplest faithful rendering; none of the tools need images, resource links,
28
+ * or other MCP content kinds. A thrown error becomes an `isError` tool result instead of a protocol-level failure, so a
29
+ * bad address / missing db surfaces to the agent as a normal (if unsuccessful) tool call.
30
30
  */
31
31
  export function createMCPServer(deps: MCPToolDeps): McpServer {
32
32
  const server = new McpServer({ name: "mailwoman", version: MCP_SERVER_VERSION })
package/tools.ts CHANGED
@@ -8,7 +8,9 @@
8
8
  * file (the actual product surface) is testable without any MCP plumbing — `tools.test.ts` calls
9
9
  * `buildToolTable` directly with stub deps.
10
10
  *
11
- * Five tools, one per capability the exotic-POI arc's other packages expose to a human/CLI caller:
11
+ * One tool per capability the exotic-POI/BDC arcs' other packages expose to a human/CLI caller — see
12
+ * `buildToolTable`'s return value for the authoritative, current list (this comment intentionally states no count,
13
+ * so it can't go stale as tools are added):
12
14
  *
13
15
  * - `mailwoman_parse` — the runtime pipeline's parse (optionally POI-aware).
14
16
  * - `mailwoman_geocode` — the street-level geocode cascade (`mailwoman/geocode-core`).
@@ -17,6 +19,16 @@
17
19
  * we never run it".
18
20
  * - `mailwoman_layer_manifest` — read a spatial-layer database's provenance manifest + coverage summary
19
21
  * (`@mailwoman/core/layers`).
22
+ * - `mailwoman_bdc_filing_landscape` — read a bdc.db layer's provider/technology/speed-bucket filing census over a
23
+ * set of census blocks or H3 cells (`@mailwoman/bdc`'s `filingLandscape`).
24
+ * - `mailwoman_plausibility_check` — score one claimed broadband-service assertion against BDC filing evidence and
25
+ * nearby telecom infrastructure (`@mailwoman/bdc`'s `plausibilityCheck`), returning a positive-evidence-only bundle
26
+ * with an always-present `coverage_confidence`. A missing/absent `bdc_database_path`/`poi_database_path` degrades
27
+ * to a typed abstain entry in the bundle, never a throw (2b task 7, decision 6).
28
+ * - `mailwoman_filer_lookup` (3a task 7) — read the FCC filer identity crosswalk (`@mailwoman/filer`'s
29
+ * `filerLookup`) for one identifier (FRN, Form 499 ID, or BDC provider ID): every OTHER identifier it shares an
30
+ * authoritative edge with, its current attributes, its authoritative entity cluster, and any inferred links —
31
+ * reported separately, never merged into the cluster. `as_of` is always present (defaults to today).
20
32
  */
21
33
 
22
34
  import { z } from "zod"
@@ -30,6 +42,23 @@ export interface MCPToolDeps {
30
42
  poiSearch: (q: { query: string; poiDatabasePath?: string }) => Promise<unknown>
31
43
  overpassExport: (query: string) => Promise<string>
32
44
  layerManifest: (databasePath: string) => Promise<unknown>
45
+ bdcFilingLandscape: (q: { databasePath: string; geoids?: string[]; h3Cells?: number[] }) => Promise<unknown>
46
+ plausibilityCheck: (q: {
47
+ bdcDatabasePath?: string
48
+ poiDatabasePath?: string
49
+ address?: string
50
+ point?: { type: "Point"; coordinates: [number, number] }
51
+ geoid?: string
52
+ technologyCode: number
53
+ claimedDownloadMbps: number
54
+ }) => Promise<unknown>
55
+ filerLookup: (q: {
56
+ databasePath: string
57
+ frn?: string
58
+ form499ID?: string
59
+ bdcProviderID?: number
60
+ asOf?: string
61
+ }) => Promise<unknown>
33
62
  }
34
63
 
35
64
  /**
@@ -87,6 +116,111 @@ const LayerManifestInputSchema = z.object({
87
116
  .describe("Path to a mailwoman spatial-layer database (poi.db, an address-points shard, etc.)."),
88
117
  })
89
118
 
119
+ const BDCFilingLandscapeInputSchema = z.object({
120
+ database_path: z
121
+ .string()
122
+ .min(1)
123
+ .describe("Path to a bdc.db layer database (FCC Broadband Data Collection availability)."),
124
+ geoids: z
125
+ .array(z.string())
126
+ .min(1)
127
+ .optional()
128
+ .describe(
129
+ "15-character census block GEOIDs to query. Provide exactly one of `geoids` or `h3_cells` — never both, never neither, never empty."
130
+ ),
131
+ h3_cells: z
132
+ .array(z.number())
133
+ .min(1)
134
+ .optional()
135
+ .describe(
136
+ "Resolution-9 short H3 cell integers (the bdc.db availability spine) to query directly. Provide exactly " +
137
+ "one of `geoids` or `h3_cells` — never both, never neither, never empty."
138
+ ),
139
+ })
140
+
141
+ const PlausibilityCheckPointInputSchema = z.object({
142
+ type: z.literal("Point").describe('GeoJSON geometry type — always "Point".'),
143
+ coordinates: z
144
+ .tuple([z.number(), z.number()])
145
+ .describe("[longitude, latitude] pair, GeoJSON coordinate order (longitude first)."),
146
+ })
147
+
148
+ const PlausibilityCheckInputSchema = z.object({
149
+ bdc_database_path: z
150
+ .string()
151
+ .optional()
152
+ .describe(
153
+ "Path to a bdc.db layer database (FCC Broadband Data Collection availability). Omit — or point at a file " +
154
+ "that doesn't exist — to abstain on filing evidence rather than error."
155
+ ),
156
+ poi_database_path: z
157
+ .string()
158
+ .optional()
159
+ .describe(
160
+ "Path to a poi.db layer database carrying the telecom-infrastructure categories (`telecom_exchange`, " +
161
+ "`tower_comms`, etc.). Omit — or point at a file that doesn't exist — to abstain on physical-plant " +
162
+ "evidence rather than error."
163
+ ),
164
+ address: z
165
+ .string()
166
+ .optional()
167
+ .describe(
168
+ "The claimed service location as a free-text postal address, geocoded via the server's runtime pipeline " +
169
+ "when `point` isn't also given. At least one of `address`, `point`, or `geoid` is required."
170
+ ),
171
+ point: PlausibilityCheckPointInputSchema.optional().describe(
172
+ "The claimed service location as a GeoJSON Point, bypassing geocoding. At least one of `address`, `point`, " +
173
+ "or `geoid` is required."
174
+ ),
175
+ geoid: z
176
+ .string()
177
+ .optional()
178
+ .describe(
179
+ "15-character census block GEOID for the claimed service location — the exact, native filing-evidence " +
180
+ "path (no h3-cell approximation). May be supplied alongside `point`/`address` (geoid drives filing " +
181
+ "evidence; the point drives physical-plant evidence independently). At least one of `address`, " +
182
+ "`point`, or `geoid` is required."
183
+ ),
184
+ technology_code: z
185
+ .number()
186
+ .describe(
187
+ "FCC BDC technology code for the claimed service, e.g. 50 = optical carrier fiber (BroadbandTechnologyCode)."
188
+ ),
189
+ claimed_download_mbps: z.number().describe("The claimed downstream speed in Mbps."),
190
+ })
191
+
192
+ const FilerLookupInputSchema = z.object({
193
+ database_path: z.string().min(1).describe("Path to a filer.db layer database (FCC filer identity crosswalk)."),
194
+ frn: z
195
+ .string()
196
+ .optional()
197
+ .describe(
198
+ "The zero-padded 10-digit FCC Registration Number to look up, e.g. '0001753557'. Provide exactly one of " +
199
+ "`frn`, `form499_id`, or `bdc_provider_id` — never more than one, never none."
200
+ ),
201
+ form499_id: z
202
+ .string()
203
+ .optional()
204
+ .describe(
205
+ "The FCC Form 499 filer ID to look up. Provide exactly one of `frn`, `form499_id`, or `bdc_provider_id` — " +
206
+ "never more than one, never none."
207
+ ),
208
+ bdc_provider_id: z
209
+ .number()
210
+ .optional()
211
+ .describe(
212
+ "The FCC BDC provider_id to look up. Provide exactly one of `frn`, `form499_id`, or `bdc_provider_id` — " +
213
+ "never more than one, never none."
214
+ ),
215
+ as_of: z
216
+ .string()
217
+ .optional()
218
+ .describe(
219
+ "ISO date (YYYY-MM-DD) to scope the lookup as-of — only relationships valid on or before this date, and not " +
220
+ "yet closed by it, are included. Defaults to today; the result always states the date actually used."
221
+ ),
222
+ })
223
+
90
224
  /**
91
225
  * Build the tool table for a concrete `MCPToolDeps` implementation. Pure — no transport, no I/O of its own.
92
226
  */
@@ -158,5 +292,65 @@ export function buildToolTable(deps: MCPToolDeps): MCPToolDef[] {
158
292
  return deps.layerManifest(databasePath)
159
293
  },
160
294
  },
295
+ {
296
+ name: "mailwoman_bdc_filing_landscape",
297
+ description:
298
+ "Read the FCC Broadband Data Collection (BDC) provider/technology/speed-bucket filing census over a set " +
299
+ "of census blocks (`geoids`) or H3 cells (`h3_cells`) from a bdc.db layer database. Returns the source " +
300
+ "vintage, how many queried blocks were surveyed vs. unknown (never surveyed), and the filing summary. " +
301
+ "Provide exactly one of `geoids` or `h3_cells`.",
302
+ inputSchema: BDCFilingLandscapeInputSchema,
303
+ handler: async (args) => {
304
+ const { database_path, geoids, h3_cells } = BDCFilingLandscapeInputSchema.parse(args)
305
+
306
+ return deps.bdcFilingLandscape({ databasePath: database_path, geoids, h3Cells: h3_cells })
307
+ },
308
+ },
309
+ {
310
+ name: "mailwoman_plausibility_check",
311
+ description:
312
+ "Score one claimed broadband-service assertion (technology + speed at a location) against the FCC BDC " +
313
+ "filing census and nearby telecom infrastructure, returning an evidence bundle: filings that corroborate " +
314
+ "(or don't), nearby physical plant, and a coverage_confidence reflecting survey completeness. NEVER " +
315
+ "returns a verdict stronger than 'no supporting evidence found' — absence of evidence degrades " +
316
+ "confidence, it never disproves the claim. Provide at least one of `address`, `point`, or `geoid`.",
317
+ inputSchema: PlausibilityCheckInputSchema,
318
+ handler: async (args) => {
319
+ const { bdc_database_path, poi_database_path, address, point, geoid, technology_code, claimed_download_mbps } =
320
+ PlausibilityCheckInputSchema.parse(args)
321
+
322
+ return deps.plausibilityCheck({
323
+ bdcDatabasePath: bdc_database_path,
324
+ poiDatabasePath: poi_database_path,
325
+ address,
326
+ point,
327
+ geoid,
328
+ technologyCode: technology_code,
329
+ claimedDownloadMbps: claimed_download_mbps,
330
+ })
331
+ },
332
+ },
333
+ {
334
+ name: "mailwoman_filer_lookup",
335
+ description:
336
+ "Look up the FCC filer identity crosswalk for one identifier (FRN, Form 499 filer ID, or BDC provider_id) " +
337
+ "from a filer.db layer database: every OTHER identifier it shares an authoritative edge with (never " +
338
+ "collapsed — a provider_id carrying multiple FRNs reports all of them), its current attributes, its " +
339
+ "authoritative entity cluster, and any inferred links reported SEPARATELY with their score — never merged " +
340
+ "into the cluster. Scoped `as_of` a date (defaults to today, always present in the result). Provide " +
341
+ "exactly one of `frn`, `form499_id`, or `bdc_provider_id`.",
342
+ inputSchema: FilerLookupInputSchema,
343
+ handler: async (args) => {
344
+ const { database_path, frn, form499_id, bdc_provider_id, as_of } = FilerLookupInputSchema.parse(args)
345
+
346
+ return deps.filerLookup({
347
+ databasePath: database_path,
348
+ frn,
349
+ form499ID: form499_id,
350
+ bdcProviderID: bdc_provider_id,
351
+ asOf: as_of,
352
+ })
353
+ },
354
+ },
161
355
  ]
162
356
  }