Prep for PyPI release: error handling, better docs, MIT license

- Add BLMAPIError exception with user-friendly error messages
- Rename is_public to allows_public_access with correct logic
  (BIA/DOD are federal but restricted, not public access)
- Add MIT license
- Expand pyproject.toml with URLs, keywords, classifiers
- Rewrite README with badges, use cases, coverage map, examples
This commit is contained in:
Ryan Malloy 2026-01-25 12:12:56 -07:00
parent 0ef2c70af4
commit d8d160efdc
9 changed files with 238 additions and 96 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Ryan Malloy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

126
README.md
View File

@ -1,65 +1,84 @@
# mcblmplss # mcblmplss
FastMCP server for querying BLM (Bureau of Land Management) land data by coordinates. [![PyPI version](https://img.shields.io/pypi/v/mcblmplss.svg)](https://pypi.org/project/mcblmplss/)
[![Python versions](https://img.shields.io/pypi/pyversions/mcblmplss.svg)](https://pypi.org/project/mcblmplss/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Features **MCP server for querying U.S. public land data by coordinates.**
- **PLSS** - Public Land Survey System (Section, Township, Range) Drop a pin anywhere in the western U.S. and instantly get:
- **Surface Management** - Who manages the land (BLM, Forest Service, NPS, Private, etc.) - **PLSS location** — Section 12, Township 4N, Range 6E
- **Mining Claims** - Active and closed mining claims from MLRS database - **Land manager** — BLM, Forest Service, National Park, Private, etc.
- **Mining claims** — Active lode/placer claims with serial numbers
## When would I use this?
| Use Case | What you get |
|----------|--------------|
| **Dispersed camping** | Check if land is BLM/Forest Service before setting up camp |
| **Land research** | Get legal descriptions for title searches or due diligence |
| **Prospecting** | Find existing mining claims before staking your own |
| **Navigation** | Convert GPS coordinates to the township/range system used on paper maps |
| **GIS workflows** | Programmatic access to BLM cadastral data |
## Installation ## Installation
```bash ```bash
# Run directly with uvx pip install mcblmplss
uvx mcblmplss ```
# Add to Claude Code Or run directly without installing:
claude mcp add blm-land "uvx mcblmplss"
```bash
uvx mcblmplss
```
### Add to Claude Code
```bash
claude mcp add blm "uvx mcblmplss"
``` ```
## Tools ## Tools
### PLSS (Public Land Survey System) ### `get_plss_location`
| Tool | Description | Convert coordinates to Section/Township/Range.
|------|-------------|
| `get_plss_location` | Get Section/Township/Range as human-readable text |
| `get_plss_details` | Get full PLSS data as structured object |
``` ```
> get_plss_location(40.0, -105.0) > get_plss_location(latitude=40.0, longitude=-105.0)
Section 9, Township 1N, Range 68W, 6th Meridian Section 9, Township 1N, Range 68W, 6th Meridian
State: CO State: CO
PLSS ID: CO060010S0680W0SN090 PLSS ID: CO060010S0680W0SN090
``` ```
### Surface Management Agency ### `get_land_manager`
| Tool | Description | Find out who manages the land (and whether you can access it).
|------|-------------|
| `get_land_manager` | Determine who manages the land |
| `get_land_manager_details` | Get full SMA data as structured object |
``` ```
> get_land_manager(38.5, -110.5) > get_land_manager(latitude=38.5, longitude=-110.5)
Bureau of Land Management (BLM) - Department of the Interior Bureau of Land Management (BLM) - Department of the Interior
Unit: Bureau of Land Management Unit: Bureau of Land Management
State: UT State: UT
Status: Federal, Public land Status: Federal, Public access
``` ```
### Mining Claims ```
> get_land_manager(latitude=40.0, longitude=-105.0)
| Tool | Description | Private (PVT)
|------|-------------| State: CO
| `get_mining_claims` | Find mining claims at/near location | ```
| `get_mining_claims_details` | Get full claim data as structured objects |
### `get_mining_claims`
Find active mining claims at a location.
``` ```
> get_mining_claims(39.5, -117.0) > get_mining_claims(latitude=39.5, longitude=-117.0)
Found 42 mining claim(s): Found 42 mining claim(s):
@ -68,31 +87,52 @@ MAGA #6
Type: Lode Claim Type: Lode Claim
Status: Active Status: Active
Acres: 20.66 Acres: 20.66
MS 2
Serial: NV105223666
Type: Lode Claim
Status: Active
Acres: 20.66
... ...
``` ```
## Coverage ## Coverage
Data available for **30 western/midwestern states** where federal land surveys were conducted. Not available for eastern seaboard states or Texas (different survey systems). Data is available for **30 states** where the Public Land Survey System was used:
![PLSS Coverage](https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Public_Land_Survey_System.png/800px-Public_Land_Survey_System.png)
**Not covered:** Eastern seaboard states (use metes-and-bounds), Texas (independent surveys), Hawaii.
## Error Handling
The server returns clear error messages when:
- **Outside PLSS coverage**: "No PLSS data found. Location may be outside surveyed areas."
- **API timeout**: "BLM API request timed out. The service may be slow or unavailable."
- **No mining claims**: "No mining claims found at this location."
## Data Sources ## Data Sources
All data queried from official BLM ArcGIS REST services: All data comes from official BLM ArcGIS REST services:
- [BLM National PLSS](https://gis.blm.gov/arcgis/rest/services/Cadastral/BLM_Natl_PLSS_CadNSDI/MapServer) | Data | Source | Update Frequency |
- [Surface Management Agency](https://gis.blm.gov/arcgis/rest/services/lands/BLM_Natl_SMA_LimitedScale/MapServer) |------|--------|------------------|
- [Mining Claims (MLRS)](https://gis.blm.gov/nlsdb/rest/services/Mining_Claims/MiningClaims/MapServer) | PLSS | [BLM National PLSS CadNSDI](https://gis.blm.gov/arcgis/rest/services/Cadastral/BLM_Natl_PLSS_CadNSDI/MapServer) | Quarterly |
| Surface Management | [BLM SMA](https://gis.blm.gov/arcgis/rest/services/lands/BLM_Natl_SMA_LimitedScale/MapServer) | Annual |
| Mining Claims | [BLM MLRS](https://gis.blm.gov/nlsdb/rest/services/Mining_Claims/MiningClaims/MapServer) | Weekly |
## Architecture **Disclaimer:** This data is for informational purposes only. For legal land descriptions, consult official BLM records or a licensed surveyor.
Uses FastMCP's mixin pattern for composable tool modules: ## Development
```bash
git clone https://git.supported.systems/MCP/mcblmplss.git
cd mcblmplss
uv sync
uv run mcblmplss
``` ```
src/mcblmplss/
├── server.py # Main FastMCP server ## License
├── client.py # Shared BLM API client
└── mixins/ MIT
├── plss.py # PLSS tools
├── surface_management.py # SMA tools
└── mining_claims.py # Mining claims tools
```

View File

@ -1,18 +1,33 @@
[project] [project]
name = "mcblmplss" name = "mcblmplss"
version = "2024.12.03" version = "2024.12.03"
description = "FastMCP server for querying BLM Public Land Survey System (PLSS) data by coordinates" description = "MCP server for querying BLM land data: PLSS coordinates, surface management agency, and mining claims"
readme = "README.md" readme = "README.md"
license = "MIT"
requires-python = ">=3.11" requires-python = ">=3.11"
authors = [ authors = [
{name = "Ryan Malloy", email = "ryan@supported.systems"} {name = "Ryan Malloy", email = "ryan@supported.systems"}
] ]
keywords = ["mcp", "fastmcp", "blm", "plss", "cadastral", "land-survey", "gis"] keywords = [
"mcp",
"fastmcp",
"blm",
"plss",
"cadastral",
"land-survey",
"gis",
"mining-claims",
"surface-management",
"land-ownership",
"public-lands",
]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: GIS", "Topic :: Scientific/Engineering :: GIS",
] ]
dependencies = [ dependencies = [
@ -20,6 +35,11 @@ dependencies = [
"httpx>=0.28.1", "httpx>=0.28.1",
] ]
[project.urls]
Homepage = "https://git.supported.systems/MCP/mcblmplss"
Repository = "https://git.supported.systems/MCP/mcblmplss"
Issues = "https://git.supported.systems/MCP/mcblmplss/issues"
[project.scripts] [project.scripts]
mcblmplss = "mcblmplss:main" mcblmplss = "mcblmplss:main"

View File

@ -4,14 +4,21 @@ Shared HTTP client for BLM ArcGIS REST API queries.
Provides common identify/query operations against BLM MapServer endpoints. Provides common identify/query operations against BLM MapServer endpoints.
""" """
import httpx
from typing import Any from typing import Any
import httpx
class BLMAPIError(Exception):
"""Error communicating with BLM ArcGIS services."""
pass
class BLMClient: class BLMClient:
"""Async HTTP client for BLM ArcGIS REST services.""" """Async HTTP client for BLM ArcGIS REST services."""
def __init__(self, timeout: float = 30.0): def __init__(self, timeout: float = 60.0):
self.timeout = timeout self.timeout = timeout
async def identify( async def identify(
@ -36,6 +43,9 @@ class BLMClient:
Returns: Returns:
List of result dictionaries with layerId and attributes List of result dictionaries with layerId and attributes
Raises:
BLMAPIError: If the API request fails
""" """
params = { params = {
"f": "json", "f": "json",
@ -44,15 +54,27 @@ class BLMClient:
"sr": "4326", "sr": "4326",
"layers": layers, "layers": layers,
"tolerance": str(tolerance), "tolerance": str(tolerance),
"mapExtent": f"{longitude-1},{latitude-1},{longitude+1},{latitude+1}", "mapExtent": f"{longitude - 1},{latitude - 1},{longitude + 1},{latitude + 1}",
"imageDisplay": "100,100,96", "imageDisplay": "100,100,96",
"returnGeometry": "true" if return_geometry else "false", "returnGeometry": "true" if return_geometry else "false",
} }
async with httpx.AsyncClient(timeout=self.timeout) as client: try:
response = await client.get(f"{base_url}/identify", params=params) async with httpx.AsyncClient(timeout=self.timeout) as client:
response.raise_for_status() response = await client.get(f"{base_url}/identify", params=params)
data = response.json() response.raise_for_status()
data = response.json()
except httpx.TimeoutException:
raise BLMAPIError("BLM API request timed out. The service may be slow or unavailable.")
except httpx.HTTPStatusError as e:
raise BLMAPIError(f"BLM API returned error {e.response.status_code}")
except httpx.RequestError as e:
raise BLMAPIError(f"Failed to connect to BLM API: {e}")
# Check for ArcGIS error response
if "error" in data:
error_msg = data["error"].get("message", "Unknown error")
raise BLMAPIError(f"BLM API error: {error_msg}")
return data.get("results", []) return data.get("results", [])
@ -82,6 +104,9 @@ class BLMClient:
Returns: Returns:
List of feature attribute dictionaries List of feature attribute dictionaries
Raises:
BLMAPIError: If the API request fails
""" """
params = { params = {
"f": "json", "f": "json",
@ -98,16 +123,26 @@ class BLMClient:
params["spatialRel"] = "esriSpatialRelIntersects" params["spatialRel"] = "esriSpatialRelIntersects"
params["inSR"] = "4326" params["inSR"] = "4326"
async with httpx.AsyncClient(timeout=self.timeout) as client: try:
response = await client.get( async with httpx.AsyncClient(timeout=self.timeout) as client:
f"{base_url}/{layer_id}/query", params=params response = await client.get(f"{base_url}/{layer_id}/query", params=params)
) response.raise_for_status()
response.raise_for_status() data = response.json()
data = response.json() except httpx.TimeoutException:
raise BLMAPIError("BLM API request timed out. The service may be slow or unavailable.")
except httpx.HTTPStatusError as e:
raise BLMAPIError(f"BLM API returned error {e.response.status_code}")
except httpx.RequestError as e:
raise BLMAPIError(f"Failed to connect to BLM API: {e}")
# Check for ArcGIS error response
if "error" in data:
error_msg = data["error"].get("message", "Unknown error")
raise BLMAPIError(f"BLM API error: {error_msg}")
features = data.get("features", []) features = data.get("features", [])
return [f.get("attributes", {}) for f in features] return [f.get("attributes", {}) for f in features]
# Shared client instance - longer timeout for slower services like mining claims # Shared client instance
blm_client = BLMClient(timeout=60.0) blm_client = BLMClient(timeout=60.0)

View File

@ -4,8 +4,8 @@ MCP Mixins for BLM data services.
Each mixin provides tools for a specific BLM data domain. Each mixin provides tools for a specific BLM data domain.
""" """
from mcblmplss.mixins.mining_claims import MiningClaimsMixin
from mcblmplss.mixins.plss import PLSSMixin from mcblmplss.mixins.plss import PLSSMixin
from mcblmplss.mixins.surface_management import SurfaceManagementMixin from mcblmplss.mixins.surface_management import SurfaceManagementMixin
from mcblmplss.mixins.mining_claims import MiningClaimsMixin
__all__ = ["PLSSMixin", "SurfaceManagementMixin", "MiningClaimsMixin"] __all__ = ["PLSSMixin", "SurfaceManagementMixin", "MiningClaimsMixin"]

View File

@ -4,10 +4,10 @@ Mining Claims mixin for BLM MCP server.
Provides tools for querying active and closed mining claims from BLM's MLRS database. Provides tools for querying active and closed mining claims from BLM's MLRS database.
""" """
from pydantic import BaseModel, Field
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from pydantic import BaseModel, Field
from mcblmplss.client import blm_client from mcblmplss.client import BLMAPIError, blm_client
# Mining Claims MapServer (separate server from main BLM arcgis) # Mining Claims MapServer (separate server from main BLM arcgis)
MINING_URL = "https://gis.blm.gov/nlsdb/rest/services/Mining_Claims/MiningClaims/MapServer" MINING_URL = "https://gis.blm.gov/nlsdb/rest/services/Mining_Claims/MiningClaims/MapServer"
@ -71,7 +71,7 @@ class MiningClaimsMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="get_mining_claims", name="get_mining_claims",
description="Find active mining claims at or near coordinates. Returns claim names, serial numbers, and types.", description="Find active mining claims at or near coordinates.",
) )
async def get_mining_claims( async def get_mining_claims(
self, self,
@ -125,9 +125,7 @@ class MiningClaimsMixin(MCPMixin):
self, self,
latitude: float = Field(description="Latitude in decimal degrees (WGS84)"), latitude: float = Field(description="Latitude in decimal degrees (WGS84)"),
longitude: float = Field(description="Longitude in decimal degrees (WGS84)"), longitude: float = Field(description="Longitude in decimal degrees (WGS84)"),
include_closed: bool = Field( include_closed: bool = Field(default=False, description="Include closed/void claims"),
default=False, description="Include closed/void claims"
),
tolerance: int = Field(default=10, description="Search radius in pixels"), tolerance: int = Field(default=10, description="Search radius in pixels"),
) -> MiningClaimsResult: ) -> MiningClaimsResult:
"""Get full mining claims data including legal descriptions.""" """Get full mining claims data including legal descriptions."""
@ -145,9 +143,16 @@ class MiningClaimsMixin(MCPMixin):
if include_closed: if include_closed:
layers = f"all:{LAYER_ACTIVE},{LAYER_CLOSED}" layers = f"all:{LAYER_ACTIVE},{LAYER_CLOSED}"
results = await blm_client.identify( try:
MINING_URL, latitude, longitude, layers, tolerance=tolerance results = await blm_client.identify(
) MINING_URL, latitude, longitude, layers, tolerance=tolerance
)
except BLMAPIError as e:
return MiningClaimsResult(
latitude=latitude,
longitude=longitude,
error=str(e),
)
if not results: if not results:
return MiningClaimsResult( return MiningClaimsResult(

View File

@ -4,10 +4,10 @@ PLSS (Public Land Survey System) mixin for BLM MCP server.
Provides tools for querying Section, Township, and Range from coordinates. Provides tools for querying Section, Township, and Range from coordinates.
""" """
from pydantic import BaseModel, Field
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from pydantic import BaseModel, Field
from mcblmplss.client import blm_client from mcblmplss.client import BLMAPIError, blm_client
# BLM Cadastral MapServer # BLM Cadastral MapServer
PLSS_URL = "https://gis.blm.gov/arcgis/rest/services/Cadastral/BLM_Natl_PLSS_CadNSDI/MapServer" PLSS_URL = "https://gis.blm.gov/arcgis/rest/services/Cadastral/BLM_Natl_PLSS_CadNSDI/MapServer"
@ -57,9 +57,9 @@ def _parse_township(attrs: dict) -> PLSSLocation:
def _parse_section(attrs: dict, township: PLSSLocation | None) -> PLSSLocation: def _parse_section(attrs: dict, township: PLSSLocation | None) -> PLSSLocation:
"""Parse section attributes from API response.""" """Parse section attributes from API response."""
sec_num = ( sec_num = (attrs.get("First Division Number", "") or attrs.get("FRSTDIVNO", "")).lstrip(
attrs.get("First Division Number", "") or attrs.get("FRSTDIVNO", "") "0"
).lstrip("0") or "0" ) or "0"
div_id = attrs.get("First Division Identifier", "") or attrs.get("FRSTDIVID", "") div_id = attrs.get("First Division Identifier", "") or attrs.get("FRSTDIVID", "")
if township: if township:
@ -90,7 +90,7 @@ class PLSSMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="get_plss_location", name="get_plss_location",
description="Get Section/Township/Range for coordinates. Returns the PLSS legal land description.", description="Get Section/Township/Range for coordinates (PLSS legal description).",
) )
async def get_plss_location( async def get_plss_location(
self, self,
@ -143,9 +143,16 @@ class PLSSMixin(MCPMixin):
async def _query_plss(self, latitude: float, longitude: float) -> PLSSResult: async def _query_plss(self, latitude: float, longitude: float) -> PLSSResult:
"""Query BLM PLSS API for location.""" """Query BLM PLSS API for location."""
results = await blm_client.identify( try:
PLSS_URL, latitude, longitude, f"all:{LAYER_TOWNSHIP},{LAYER_SECTION}" results = await blm_client.identify(
) PLSS_URL, latitude, longitude, f"all:{LAYER_TOWNSHIP},{LAYER_SECTION}"
)
except BLMAPIError as e:
return PLSSResult(
latitude=latitude,
longitude=longitude,
error=str(e),
)
if not results: if not results:
return PLSSResult( return PLSSResult(

View File

@ -4,10 +4,10 @@ Surface Management Agency mixin for BLM MCP server.
Provides tools for determining who manages federal lands (BLM, USFS, NPS, etc.). Provides tools for determining who manages federal lands (BLM, USFS, NPS, etc.).
""" """
from pydantic import BaseModel, Field
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from pydantic import BaseModel, Field
from mcblmplss.client import blm_client from mcblmplss.client import BLMAPIError, blm_client
# Surface Management Agency MapServer # Surface Management Agency MapServer
SMA_URL = "https://gis.blm.gov/arcgis/rest/services/lands/BLM_Natl_SMA_LimitedScale/MapServer" SMA_URL = "https://gis.blm.gov/arcgis/rest/services/lands/BLM_Natl_SMA_LimitedScale/MapServer"
@ -52,7 +52,9 @@ class LandManager(BaseModel):
admin_unit_type: str | None = Field(None, description="Administrative unit type") admin_unit_type: str | None = Field(None, description="Administrative unit type")
state: str = Field(..., description="State abbreviation") state: str = Field(..., description="State abbreviation")
is_federal: bool = Field(..., description="Whether this is federal land") is_federal: bool = Field(..., description="Whether this is federal land")
is_public: bool = Field(..., description="Whether this is publicly accessible") allows_public_access: bool = Field(
..., description="Whether general public access is typically allowed"
)
class SurfaceManagementResult(BaseModel): class SurfaceManagementResult(BaseModel):
@ -69,23 +71,29 @@ def _parse_sma(attrs: dict) -> LandManager:
agency_code = attrs.get("ADMIN_AGENCY_CODE", "UND") agency_code = attrs.get("ADMIN_AGENCY_CODE", "UND")
dept_code = attrs.get("ADMIN_DEPT_CODE", "") dept_code = attrs.get("ADMIN_DEPT_CODE", "")
# Determine if federal and public # Federal departments (excluding private/state/local)
federal_depts = {"DOI", "USDA", "DOD", "DOE"} federal_depts = {"DOI", "USDA", "DOD", "DOE"}
public_agencies = {"BLM", "USFS", "NPS", "FWS", "USBR"}
is_federal = dept_code in federal_depts is_federal = dept_code in federal_depts
is_public = agency_code in public_agencies
# Agencies that generally allow public recreation access
# Note: BIA (tribal) and DOD (military) are federal but restricted
public_access_agencies = {"BLM", "USFS", "NPS", "FWS", "USBR"}
allows_public_access = agency_code in public_access_agencies
return LandManager( return LandManager(
agency_code=agency_code, agency_code=agency_code,
agency_name=AGENCY_NAMES.get(agency_code, agency_code), agency_name=AGENCY_NAMES.get(agency_code, agency_code),
department_code=dept_code if dept_code and dept_code != "Null" else None, department_code=dept_code if dept_code and dept_code != "Null" else None,
department_name=DEPT_NAMES.get(dept_code) if dept_code else None, department_name=DEPT_NAMES.get(dept_code) if dept_code else None,
admin_unit_name=attrs.get("ADMIN_UNIT_NAME") if attrs.get("ADMIN_UNIT_NAME") != "Null" else None, admin_unit_name=(
admin_unit_type=attrs.get("ADMIN_UNIT_TYPE") if attrs.get("ADMIN_UNIT_TYPE") != "Null" else None, attrs.get("ADMIN_UNIT_NAME") if attrs.get("ADMIN_UNIT_NAME") != "Null" else None
),
admin_unit_type=(
attrs.get("ADMIN_UNIT_TYPE") if attrs.get("ADMIN_UNIT_TYPE") != "Null" else None
),
state=attrs.get("ADMIN_ST", ""), state=attrs.get("ADMIN_ST", ""),
is_federal=is_federal, is_federal=is_federal,
is_public=is_public, allows_public_access=allows_public_access,
) )
@ -94,7 +102,7 @@ class SurfaceManagementMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="get_land_manager", name="get_land_manager",
description="Determine who manages the land at given coordinates (BLM, Forest Service, NPS, Private, etc.)", description="Determine who manages the land (BLM, Forest Service, NPS, Private, etc.)",
) )
async def get_land_manager( async def get_land_manager(
self, self,
@ -132,10 +140,10 @@ class SurfaceManagementMixin(MCPMixin):
status = [] status = []
if mgr.is_federal: if mgr.is_federal:
status.append("Federal") status.append("Federal")
if mgr.is_public: if mgr.allows_public_access:
status.append("Public") status.append("Public access")
if status: if status:
lines.append(f"Status: {', '.join(status)} land") lines.append(f"Status: {', '.join(status)}")
return "\n".join(lines) return "\n".join(lines)
@ -155,9 +163,14 @@ class SurfaceManagementMixin(MCPMixin):
async def _query_sma(self, latitude: float, longitude: float) -> SurfaceManagementResult: async def _query_sma(self, latitude: float, longitude: float) -> SurfaceManagementResult:
"""Query BLM SMA API for location.""" """Query BLM SMA API for location."""
results = await blm_client.identify( try:
SMA_URL, latitude, longitude, f"all:{LAYER_SMA}" results = await blm_client.identify(SMA_URL, latitude, longitude, f"all:{LAYER_SMA}")
) except BLMAPIError as e:
return SurfaceManagementResult(
latitude=latitude,
longitude=longitude,
error=str(e),
)
if not results: if not results:
return SurfaceManagementResult( return SurfaceManagementResult(

View File

@ -11,7 +11,7 @@ All data is queried from official BLM ArcGIS REST services.
from fastmcp import FastMCP from fastmcp import FastMCP
from mcblmplss.mixins import PLSSMixin, SurfaceManagementMixin, MiningClaimsMixin from mcblmplss.mixins import MiningClaimsMixin, PLSSMixin, SurfaceManagementMixin
# Initialize FastMCP server # Initialize FastMCP server
mcp = FastMCP( mcp = FastMCP(
@ -56,6 +56,7 @@ def main():
"""Entry point for the mcblmplss MCP server.""" """Entry point for the mcblmplss MCP server."""
try: try:
from importlib.metadata import version from importlib.metadata import version
package_version = version("mcblmplss") package_version = version("mcblmplss")
except Exception: except Exception:
package_version = "dev" package_version = "dev"