@urbankitstudio/mcp-atlas 0.1.6 → 0.2.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Leo Yong
3
+ Copyright (c) 2026 UrbanKit Studio
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @urbankitstudio/mcp-atlas
2
2
 
3
- Query 155 verified US county parcel ArcGIS REST endpoints for owner, APN, and address lookup via the Model Context Protocol (MCP).
3
+ Query the verified parcel ArcGIS REST endpoints of 171 counties across all 50 US states (174 layers) for owner, APN and address lookup via the Model Context Protocol (MCP).
4
4
 
5
5
  An [MCP](https://modelcontextprotocol.io) server that gives AI assistants direct access to UrbanKit Studio's atlas of manually verified county parcel GIS services. Ask Claude or Cursor to find the ArcGIS REST endpoint for any covered county, get the exact owner-search query URL, and look up parcel data — without needing to know anything about ArcGIS REST API conventions.
6
6
 
7
- **Coverage:** 155 counties across all 50 US states.
7
+ **Coverage:** 171 counties across all 50 US states, 174 verified endpoints (atlas 0.6.2).
8
8
 
9
9
  ---
10
10
 
@@ -153,4 +153,4 @@ Atlas data is embedded in the package (no network calls at startup). The underly
153
153
 
154
154
  ## License
155
155
 
156
- MIT — © Leo Yong / UrbanKit Studio
156
+ MIT — © 2026 UrbanKit Studio
package/dist/server.js CHANGED
@@ -14,16 +14,43 @@
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
16
  import { z } from "zod/v3";
17
- import { atlas, atlasIndex, slugify, countySlugFromName, } from "@urbankitstudio/atlas";
17
+ import { readFileSync } from "node:fs";
18
+ import { fileURLToPath } from "node:url";
19
+ import { resolve, dirname } from "node:path";
20
+ import { atlas, atlasIndex, slugify, countySlugFromName, isReviewedUnservable, reviewedCapability, } from "@urbankitstudio/atlas";
21
+ const PKG_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8")).version;
18
22
  // ---------------------------------------------------------------------------
19
23
  // Helpers
20
24
  // ---------------------------------------------------------------------------
21
- function ownerFieldFrom(endpoint) {
25
+ /**
26
+ * The owner column a caller can actually USE, or null with a reason.
27
+ *
28
+ * Matching the column name is not enough, and that gap cost a real trial user:
29
+ * they paid for owner data, ran Los Angeles County, got nothing back, and the
30
+ * atlas had said the field was there the whole time. Fourteen counties in the
31
+ * registry document an owner column that is present and empty on every row -
32
+ * New Jersey's statewide layer publishes OWNER_NAME blank across 3,481,240
33
+ * rows, New York's across 3,827,530 - and a reviewed capability record says so.
34
+ * Consult it BEFORE promising the field.
35
+ */
36
+ function ownerFieldFor(county, endpoint) {
37
+ if (isReviewedUnservable(county, "owner_name")) {
38
+ const reviewed = reviewedCapability(county, "owner_name");
39
+ return {
40
+ field: null,
41
+ unavailableReason: reviewed?.basis?.note ??
42
+ "this county publishes no usable owner name on its public endpoint",
43
+ };
44
+ }
22
45
  const f = endpoint.searchFields.find((sf) => /owner|taxpayer|taxname/i.test(sf.name));
23
- return f?.name ?? null;
46
+ return { field: f?.name ?? null, unavailableReason: null };
47
+ }
48
+ /** Back-compat shim for call sites that only need the column. */
49
+ function ownerFieldFrom(county, endpoint) {
50
+ return ownerFieldFor(county, endpoint).field;
24
51
  }
25
- function buildArcgisOwnerQuery(endpoint, ownerQuery) {
26
- const field = ownerFieldFrom(endpoint);
52
+ function buildArcgisOwnerQuery(county, endpoint, ownerQuery) {
53
+ const field = ownerFieldFrom(county, endpoint);
27
54
  if (!field)
28
55
  return "";
29
56
  const where = `UPPER(${field}) LIKE UPPER('%25${encodeURIComponent(ownerQuery)}%25')`;
@@ -43,7 +70,7 @@ function formatCountySummary(c) {
43
70
  ? "no REST endpoint mapped"
44
71
  : c.endpoints
45
72
  .map((ep) => {
46
- const ownerField = ownerFieldFrom(ep);
73
+ const owner = ownerFieldFor(c, ep);
47
74
  const searchable = ep.searchFields
48
75
  .filter((sf) => sf.searchable)
49
76
  .map((sf) => `${sf.name} (${sf.label})`)
@@ -53,7 +80,10 @@ function formatCountySummary(c) {
53
80
  ` Service: ${ep.serviceType}/layer ${ep.layerIndex}`,
54
81
  ` Status: ${ep.status} (verified ${ep.lastVerified})`,
55
82
  ` Searchable fields: ${searchable || "none"}`,
56
- ` Owner field: ${ownerField ?? "none (PIN-only county)"}`,
83
+ ` Owner field: ${owner.field ??
84
+ (owner.unavailableReason
85
+ ? `NOT AVAILABLE - ${owner.unavailableReason}`
86
+ : "none (this layer publishes no owner column)")}`,
57
87
  ` License: ${ep.license}`,
58
88
  ].join("\n");
59
89
  })
@@ -71,7 +101,11 @@ function formatCountySummary(c) {
71
101
  // ---------------------------------------------------------------------------
72
102
  // Server
73
103
  // ---------------------------------------------------------------------------
74
- const server = new McpServer({ name: "mcp-atlas", version: "0.1.0" }, {
104
+ const server = new McpServer(
105
+ // Read from package.json rather than restated here. This line said 0.1.0
106
+ // while the package was 0.1.6 - six releases of drift, and every MCP client
107
+ // that asked the server its version got the wrong answer.
108
+ { name: "mcp-atlas", version: PKG_VERSION }, {
75
109
  instructions: "UrbanKit Atlas MCP server. Use list_counties to discover coverage, find_county or get_parcel_endpoint to get the ArcGIS REST URL, and build_owner_query to construct a ready-to-fire owner-name lookup URL.",
76
110
  });
77
111
  // ---------------------------------------------------------------------------
@@ -105,9 +139,17 @@ server.registerTool("list_counties", {
105
139
  continue;
106
140
  const covered = stateFile.counties.filter((c) => c.endpoints.length > 0);
107
141
  for (const c of covered) {
108
- const ownerCoverage = c.endpoints.some((ep) => ownerFieldFrom(ep))
142
+ // "APN only" is not the same claim as "owner+APN minus the owner".
143
+ // A caller scanning this column is deciding whether to spend a request,
144
+ // so a county whose owner column exists and is empty must not read as
145
+ // though owners are simply absent from the schema.
146
+ const anyOwner = c.endpoints.some((ep) => ownerFieldFor(c, ep).field);
147
+ const ownerWithheld = c.endpoints.some((ep) => ownerFieldFor(c, ep).unavailableReason);
148
+ const ownerCoverage = anyOwner
109
149
  ? "owner+APN"
110
- : "APN only";
150
+ : ownerWithheld
151
+ ? "APN only (county publishes no owner name)"
152
+ : "APN only";
111
153
  rows.push(`${c.state} | ${c.county.padEnd(20)} | ${c.countySlug.padEnd(24)} | ${ownerCoverage}`);
112
154
  }
113
155
  }
@@ -272,9 +314,10 @@ server.registerTool("get_parcel_endpoint", {
272
314
  "",
273
315
  ];
274
316
  countyRecord.endpoints.forEach((ep, i) => {
275
- const ownerField = ownerFieldFrom(ep);
317
+ const owner = ownerFieldFor(countyRecord, ep);
318
+ const ownerField = owner.field;
276
319
  const sampleOwnerUrl = ownerField
277
- ? buildArcgisOwnerQuery(ep, "SMITH")
320
+ ? buildArcgisOwnerQuery(countyRecord, ep, "SMITH")
278
321
  : null;
279
322
  lines.push(`Endpoint ${i + 1}:`);
280
323
  lines.push(` URL: ${ep.url}`);
@@ -290,7 +333,10 @@ server.registerTool("get_parcel_endpoint", {
290
333
  .filter((sf) => sf.searchable)
291
334
  .forEach((sf) => lines.push(` ${sf.name.padEnd(20)} – ${sf.label}`));
292
335
  lines.push("");
293
- lines.push(` Owner field: ${ownerField ?? "NONE — PIN-only county"}`);
336
+ lines.push(` Owner field: ${ownerField ??
337
+ (owner.unavailableReason
338
+ ? `NOT AVAILABLE - ${owner.unavailableReason}`
339
+ : "NONE - this layer publishes no owner column")}`);
294
340
  if (ep.sampleQuery) {
295
341
  lines.push("");
296
342
  lines.push(" Sample query (from atlas):");
@@ -359,12 +405,18 @@ server.registerTool("build_owner_query", {
359
405
  }
360
406
  const results = [];
361
407
  for (const ep of countyRecord.endpoints) {
362
- const ownerField = ownerFieldFrom(ep);
408
+ const owner = ownerFieldFor(countyRecord, ep);
409
+ const ownerField = owner.field;
363
410
  if (!ownerField) {
364
- results.push(`Endpoint: ${ep.url}\nNote: No owner/taxpayer field available in this county — PIN-only lookup. Try searching by parcel number instead.`);
411
+ // Refusing with the reason beats handing back a query that returns zero
412
+ // rows forever. The caller can then choose a different county or a
413
+ // different field instead of concluding the owner simply is not there.
414
+ results.push(owner.unavailableReason
415
+ ? `Endpoint: ${ep.url}\nOWNER NAME NOT AVAILABLE for ${countyRecord.county}, ${countyRecord.stateName}: ${owner.unavailableReason}\nNo owner query is possible here. Search by parcel number or address instead, or pick a county whose coverage reads owner+APN in list_counties.`
416
+ : `Endpoint: ${ep.url}\nNote: this layer publishes no owner or taxpayer column - PIN-only lookup. Try searching by parcel number instead.`);
365
417
  continue;
366
418
  }
367
- const queryUrl = buildArcgisOwnerQuery(ep, owner_name);
419
+ const queryUrl = buildArcgisOwnerQuery(countyRecord, ep, owner_name);
368
420
  const where = `UPPER(${ownerField}) LIKE UPPER('%${owner_name}%')`;
369
421
  results.push([
370
422
  `County: ${countyRecord.county}, ${countyRecord.stateName}`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@urbankitstudio/mcp-atlas",
3
- "version": "0.1.6",
4
- "description": "Query 155 verified US county parcel ArcGIS REST endpoints for owner, APN, and address lookup via the Model Context Protocol (MCP).",
3
+ "version": "0.2.1",
4
+ "description": "Query the verified parcel ArcGIS REST endpoints of 171 counties across all 50 US states (174 layers) for owner, APN and address lookup via the Model Context Protocol (MCP).",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "mcp-server",
@@ -24,7 +24,7 @@
24
24
  "bugs": {
25
25
  "url": "https://github.com/LEOyrh/urbankitstudio/issues"
26
26
  },
27
- "author": "Leo Yong <leoyrhbiz@gmail.com>",
27
+ "author": "UrbanKit Studio <urbankitstudio@gmail.com>",
28
28
  "license": "MIT",
29
29
  "type": "module",
30
30
  "main": "./dist/server.js",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.30.0",
49
- "@urbankitstudio/atlas": "^0.5.1"
49
+ "@urbankitstudio/atlas": "^0.6.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^26.0.0",