@urbankitstudio/mcp-atlas 0.1.5 → 0.2.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/LICENSE +21 -21
- package/README.md +156 -158
- package/SECURITY-NOTES.md +66 -0
- package/dist/server.js +70 -18
- package/package.json +61 -60
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Leo Yong
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leo Yong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,158 +1,156 @@
|
|
|
1
|
-
# @urbankitstudio/mcp-atlas
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
**Example
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
IL |
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
110
|
-
| `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
**Example
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
> **
|
|
134
|
-
|
|
135
|
-
>
|
|
136
|
-
>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
MIT — © Leo Yong / UrbanKit Studio
|
|
1
|
+
# @urbankitstudio/mcp-atlas
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
|
|
7
|
+
**Coverage:** 171 counties across all 50 US states, 174 verified endpoints (atlas 0.6.2).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
### Claude Desktop
|
|
14
|
+
|
|
15
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"mcp-atlas": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@urbankitstudio/mcp-atlas"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Cursor
|
|
29
|
+
|
|
30
|
+
Add to `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` globally):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"mcp-atlas": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "@urbankitstudio/mcp-atlas"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Install globally (optional)
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install -g @urbankitstudio/mcp-atlas
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then use `mcp-atlas` as the command instead of `npx -y @urbankitstudio/mcp-atlas`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
### `list_counties`
|
|
56
|
+
|
|
57
|
+
Lists all counties with a verified parcel REST endpoint.
|
|
58
|
+
|
|
59
|
+
| Parameter | Type | Required | Description |
|
|
60
|
+
|-----------|------|----------|-------------|
|
|
61
|
+
| `state` | string | No | Two-letter abbreviation (`IL`) or full name (`Illinois`) |
|
|
62
|
+
|
|
63
|
+
**Example prompt:** "List all covered counties in Illinois"
|
|
64
|
+
|
|
65
|
+
**Example output:**
|
|
66
|
+
```
|
|
67
|
+
ST | County | Slug | Coverage
|
|
68
|
+
--------------------------------------------------------------------
|
|
69
|
+
IL | Kane | kane-county | owner+APN
|
|
70
|
+
IL | Cook | cook-county | APN only
|
|
71
|
+
IL | DuPage | dupage-county | owner+APN
|
|
72
|
+
...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### `find_county`
|
|
78
|
+
|
|
79
|
+
Fuzzy-matches a county by name or 5-digit FIPS code. Returns endpoint URLs, searchable field names, owner field, sample query, and license info.
|
|
80
|
+
|
|
81
|
+
| Parameter | Type | Required | Description |
|
|
82
|
+
|-----------|------|----------|-------------|
|
|
83
|
+
| `query` | string | Yes | County name (`Kane`), name+state (`Kane IL`), or FIPS (`17089`) |
|
|
84
|
+
|
|
85
|
+
**Example prompt:** "Find the parcel endpoint for Kane County Illinois"
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### `get_parcel_endpoint`
|
|
90
|
+
|
|
91
|
+
Returns the full ArcGIS REST URL, layer index, searchable fields, owner field, and a ready sample `?where=…&f=json` query for a specific county.
|
|
92
|
+
|
|
93
|
+
| Parameter | Type | Required | Description |
|
|
94
|
+
|-----------|------|----------|-------------|
|
|
95
|
+
| `state` | string | Yes | Two-letter abbreviation or full name |
|
|
96
|
+
| `county` | string | Yes | County name (`Kane` or `Kane County`) |
|
|
97
|
+
|
|
98
|
+
**Example prompt:** "Give me the ArcGIS REST endpoint for Cook County Illinois"
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `build_owner_query`
|
|
103
|
+
|
|
104
|
+
Constructs the exact ArcGIS REST query URL using the county's verified owner/taxpayer field. Uses `UPPER(field) LIKE UPPER('%NAME%')` — case-insensitive partial match.
|
|
105
|
+
|
|
106
|
+
| Parameter | Type | Required | Description |
|
|
107
|
+
|-----------|------|----------|-------------|
|
|
108
|
+
| `state` | string | Yes | Two-letter abbreviation or full name |
|
|
109
|
+
| `county` | string | Yes | County name |
|
|
110
|
+
| `owner_name` | string | Yes | Owner/taxpayer name (partial match) |
|
|
111
|
+
|
|
112
|
+
**Example prompt:** "Build an ArcGIS query for properties owned by 'Smith' in Kane County IL"
|
|
113
|
+
|
|
114
|
+
**Example output:**
|
|
115
|
+
```
|
|
116
|
+
County: Kane, Illinois
|
|
117
|
+
Owner field: TaxName
|
|
118
|
+
WHERE clause: UPPER(TaxName) LIKE UPPER('%SMITH%')
|
|
119
|
+
|
|
120
|
+
Query URL:
|
|
121
|
+
https://gistech.countyofkane.org/arcgis/rest/services/KanePINList/MapServer/0/query
|
|
122
|
+
?where=UPPER(TaxName)%20LIKE%20UPPER('%25SMITH%25')
|
|
123
|
+
&outFields=PIN,TaxName,SiteAddress,SiteCity,MailingAddress
|
|
124
|
+
&returnGeometry=false&f=json&resultRecordCount=25
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Example conversation
|
|
130
|
+
|
|
131
|
+
> **User:** I'm doing due diligence on properties in Kane County, Illinois. Can you find all parcels owned by "Blackstone"?
|
|
132
|
+
|
|
133
|
+
> **Claude (using mcp-atlas):**
|
|
134
|
+
> 1. Calls `get_parcel_endpoint` → gets the `gistech.countyofkane.org` URL and confirms the owner field is `TaxName`
|
|
135
|
+
> 2. Calls `build_owner_query` with `owner_name=Blackstone` → returns a ready fetch URL
|
|
136
|
+
> 3. Optionally fetches the URL and formats the parcel results
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Atlas coverage
|
|
141
|
+
|
|
142
|
+
The atlas is maintained by [UrbanKit Studio](https://urbankitstudio.com/parcel-atlas). All endpoints are manually verified. Counties with an owner/taxpayer field support full name-based lookups; PIN-only counties support APN/parcel-number queries.
|
|
143
|
+
|
|
144
|
+
Full coverage map: https://urbankitstudio.com/parcel-atlas
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Data
|
|
149
|
+
|
|
150
|
+
Atlas data is embedded in the package (no network calls at startup). The underlying `@urbankitstudio/atlas` SDK is also published separately for programmatic use.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT — © Leo Yong / UrbanKit Studio
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Security notes — @urbankitstudio/mcp-atlas
|
|
2
|
+
|
|
3
|
+
Known advisories in this package's dependency tree, why they are where they are,
|
|
4
|
+
and what was decided. Written down so an audit does not have to re-derive it and
|
|
5
|
+
does not re-raise a closed question.
|
|
6
|
+
|
|
7
|
+
Last reviewed: **2026-08-05**, against `@modelcontextprotocol/sdk@1.30.0`.
|
|
8
|
+
|
|
9
|
+
## Two HIGH advisories, both upstream and both unreachable here
|
|
10
|
+
|
|
11
|
+
`npm audit` reports four advisories, two of them HIGH. Both come from the MCP
|
|
12
|
+
SDK's own dependency tree, not from anything this package requires directly:
|
|
13
|
+
|
|
14
|
+
| Advisory | Path | Reachable from this server? |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `fast-uri` host confusion (GHSA-v2hh-gcrm-f6hx, -7p8r-x3mc-p8w7, -4c8g-83qw-93j6) | `sdk → ajv → fast-uri` | Loaded, but **no attack surface** — see below |
|
|
17
|
+
| `ip-address` SSRF / trust bypass (GHSA-mwp4-54f8-5fhr, -4xrf-jv44-h6hh, -22jq-vg5j-6vgg) | `sdk → express-rate-limit → ip-address` | **No** — never loaded |
|
|
18
|
+
|
|
19
|
+
### Why `ip-address` is not loaded
|
|
20
|
+
|
|
21
|
+
`express-rate-limit` is imported only by the SDK's OAuth handlers
|
|
22
|
+
(`dist/esm/server/auth/handlers/{authorize,register,revoke,token}.js`). Those
|
|
23
|
+
serve the HTTP transports. This server uses `StdioServerTransport`, and
|
|
24
|
+
`dist/esm/server/index.js` contains no reference to `auth/handlers` — verified by
|
|
25
|
+
grep against the installed package, not inferred from the dependency graph.
|
|
26
|
+
|
|
27
|
+
### Why `fast-uri` has no attack surface here
|
|
28
|
+
|
|
29
|
+
This one *is* loaded: `server/index.js` imports `AjvJsonSchemaValidator`, which
|
|
30
|
+
imports `ajv`, which uses `fast-uri` for `format: "uri"` validation. So unlike
|
|
31
|
+
`ip-address` it genuinely executes.
|
|
32
|
+
|
|
33
|
+
It still has nowhere to go. The advisories describe host confusion when parsing a
|
|
34
|
+
URI — dangerous when a parsed host then drives an outbound request, which is the
|
|
35
|
+
SSRF shape. This server:
|
|
36
|
+
|
|
37
|
+
- declares **no** `format: "uri"`, `"url"` or `"hostname"` in any tool schema, so
|
|
38
|
+
nothing routes untrusted input through the vulnerable parser, and
|
|
39
|
+
- makes **no `fetch()` calls at all** — it answers from atlas data bundled at
|
|
40
|
+
publish time, so there is no request for a confused host to redirect.
|
|
41
|
+
|
|
42
|
+
## Why this is not fixed here
|
|
43
|
+
|
|
44
|
+
It cannot be. `overrides` in a published package apply to that package's own
|
|
45
|
+
installs, not to consumers, who resolve the SDK's transitive tree themselves.
|
|
46
|
+
Raising a version here would clean up a local `npm audit` and change nothing for
|
|
47
|
+
anyone who installs this package — the appearance of a fix rather than a fix.
|
|
48
|
+
|
|
49
|
+
The real remedy is upstream: the SDK bumping `ajv` and `express-rate-limit`, or
|
|
50
|
+
moving its HTTP/OAuth dependencies to optional so stdio consumers never install
|
|
51
|
+
them.
|
|
52
|
+
|
|
53
|
+
## What was actually done
|
|
54
|
+
|
|
55
|
+
The declared range moved from `^1.29.0` to `^1.30.0`. This raises the floor and
|
|
56
|
+
keeps the package current per the SOAK-AND-ROT policy; it does **not** resolve
|
|
57
|
+
the advisories above, and 1.30.0 was measured to leave both HIGHs in place.
|
|
58
|
+
|
|
59
|
+
Worth being precise about, because it was initially recorded the other way round:
|
|
60
|
+
a tooling sweep reported that 1.30.0 "closes 2 HIGH SSRF-class CVEs." Installing
|
|
61
|
+
1.30.0 and re-running `npm audit` shows both still present. The bump is hygiene,
|
|
62
|
+
not a security fix, and it should not be cited as one.
|
|
63
|
+
|
|
64
|
+
Note also that `^1.29.0` already permitted 1.30.0, so consumers installing before
|
|
65
|
+
this change were resolving to it anyway. The floor bump prevents a consumer
|
|
66
|
+
pinning something older; it does not deliver a newer SDK to anyone.
|
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 {
|
|
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
|
-
|
|
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
|
|
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})`)
|
|
@@ -51,9 +78,12 @@ function formatCountySummary(c) {
|
|
|
51
78
|
return [
|
|
52
79
|
` URL: ${ep.url}`,
|
|
53
80
|
` Service: ${ep.serviceType}/layer ${ep.layerIndex}`,
|
|
54
|
-
` Status: ${ep.status}`,
|
|
81
|
+
` Status: ${ep.status} (verified ${ep.lastVerified})`,
|
|
55
82
|
` Searchable fields: ${searchable || "none"}`,
|
|
56
|
-
` Owner field: ${
|
|
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(
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -79,7 +113,7 @@ const server = new McpServer({ name: "mcp-atlas", version: "0.1.0" }, {
|
|
|
79
113
|
// ---------------------------------------------------------------------------
|
|
80
114
|
server.registerTool("list_counties", {
|
|
81
115
|
title: "List covered counties",
|
|
82
|
-
description:
|
|
116
|
+
description: `Returns all counties in the UrbanKit Atlas that have a verified ArcGIS REST parcel endpoint. Pass a state abbreviation (e.g. 'IL') or state name (e.g. 'Illinois') to filter by state. Omit state to list all ~${atlas.totals.counties} counties.`,
|
|
83
117
|
inputSchema: {
|
|
84
118
|
state: z
|
|
85
119
|
.string()
|
|
@@ -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
|
-
|
|
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
|
-
:
|
|
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
|
|
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 ??
|
|
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
|
|
408
|
+
const owner = ownerFieldFor(countyRecord, ep);
|
|
409
|
+
const ownerField = owner.field;
|
|
363
410
|
if (!ownerField) {
|
|
364
|
-
|
|
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,60 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@urbankitstudio/mcp-atlas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"mcp",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
},
|
|
24
|
-
"bugs": {
|
|
25
|
-
"url": "https://github.com/LEOyrh/urbankitstudio/issues"
|
|
26
|
-
},
|
|
27
|
-
"author": "Leo Yong <leoyrhbiz@gmail.com>",
|
|
28
|
-
"license": "MIT",
|
|
29
|
-
"type": "module",
|
|
30
|
-
"main": "./dist/server.js",
|
|
31
|
-
"types": "./dist/server.d.ts",
|
|
32
|
-
"bin": {
|
|
33
|
-
"mcp-atlas": "dist/server.js"
|
|
34
|
-
},
|
|
35
|
-
"files": [
|
|
36
|
-
"dist",
|
|
37
|
-
"README.md",
|
|
38
|
-
"LICENSE"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"@
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@urbankitstudio/mcp-atlas",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"mcp-server",
|
|
8
|
+
"parcel",
|
|
9
|
+
"gis",
|
|
10
|
+
"arcgis",
|
|
11
|
+
"county",
|
|
12
|
+
"apn",
|
|
13
|
+
"address-to-parcel",
|
|
14
|
+
"property-data",
|
|
15
|
+
"real-estate",
|
|
16
|
+
"esri"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://urbankitstudio.com/parcel-atlas",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/LEOyrh/urbankitstudio.git",
|
|
22
|
+
"directory": "packages/mcp-atlas"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/LEOyrh/urbankitstudio/issues"
|
|
26
|
+
},
|
|
27
|
+
"author": "Leo Yong <leoyrhbiz@gmail.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/server.js",
|
|
31
|
+
"types": "./dist/server.d.ts",
|
|
32
|
+
"bin": {
|
|
33
|
+
"mcp-atlas": "dist/server.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"SECURITY-NOTES.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"smoke": "node test/smoke.mjs",
|
|
45
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm run smoke"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
+
"@urbankitstudio/atlas": "^0.6.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^26.0.0",
|
|
53
|
+
"typescript": "^5.8.3"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
}
|
|
61
|
+
}
|