Parcel MCP Server

Examples

Conversational recipes plus working agent code — the Claude API MCP connector, the Python and TypeScript MCP SDKs, and prompt guidance that keeps an agent's tool use cheap.

Conversational recipes

The point of MCP is that the assistant chains the tools itself. These are prompts that work, and the tool sequence each one produces.

Ownership and value at an address

"Who owns 123 Main St in Oakland, and what's it assessed at?"

parcel_by_address → the model reads back the geocode label to confirm the match, then reports ownername and totalvalue from the returned parcel.

A count, not a data dump

"How many parcels in Alameda County are single-family homes built after 1950?"

parcel_schemaparcel_count with countyname, usedesc ilike '%single family%', and yearbuilt gt 1950. A well-behaved model reaches for parcel_count here rather than pulling thousands of rows.

Top-N with a follow-up

"What are the ten most valuable agricultural parcels over 100 acres in Virginia? Just address, owner, acres, and value."

parcel_count to size the set → parcel_query with columns: ["parceladdr", "ownername", "taxacres", "totalvalue"] and a page of results. Note that the filter has no ORDER BY — MCP filters are unordered, so ranking happens in the model's head over a bounded page. For true top-N over a large set, narrow with ge thresholds (totalvalue ge 5000000) until the result set is small enough to rank honestly.

Everything inside a boundary

"Here's a GeoJSON polygon for our service area — how many parcels are inside, and what's the total assessed value of the commercial ones?"

parcel_within_polygon for the inventory → parcel_query with a usedesc filter for the commercial subset. Sums are computed by the model from returned rows, so keep the row count modest and say so in the prompt.

Neighbors

"What's next door to 1600 Pennsylvania Ave NW, Washington DC?"

parcel_by_address to resolve the point → parcel_near_point with a 100–300 m radius around the returned centroidx / centroidy.

Grounding a new session

"Call parcel_schema and summarize what I can filter on."

Worth doing once at the start of any analysis session. It costs one cheap tool call and stops the model from inventing column names.

Call the tools from the Claude API (MCP connector)

The Claude API can connect to a remote MCP server on your behalf — Anthropic makes the MCP connection server-side, so your code never speaks JSON-RPC. Requires the mcp-client-2025-11-20 beta, and both mcp_servers and a matching mcp_toolset entry in tools.

Note that authorization_token takes the bare token, not a header value: the API adds the Bearer prefix itself when it calls us. Passing "Bearer <token>" here produces Authorization: Bearer Bearer <token> and a 401.

import os
import anthropic

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5",
    max_tokens=16000,
    betas=["mcp-client-2025-11-20"],
    thinking={"type": "adaptive"},
    mcp_servers=[
        {
            "type": "url",
            "url": "https://api.landrecords.us/mcp",
            "name": "landrecords",
            "authorization_token": os.environ["LR_TOKEN"],
        }
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "landrecords"}],
    system=(
        "You answer questions about U.S. land parcels using the landrecords tools. "
        "Call parcel_schema before writing your first filter. Use parcel_count for "
        "'how many' questions. Request only the columns you need."
    ),
    messages=[
        {
            "role": "user",
            "content": (
                "How many parcels in Alameda County, CA are single-family homes "
                "built after 1950 and assessed over $750k?"
            ),
        }
    ],
)

for block in response.content:
    if block.type == "text":
        print(block.text)

The same request in TypeScript:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-opus-5",
  max_tokens: 16000,
  betas: ["mcp-client-2025-11-20"],
  thinking: { type: "adaptive" },
  mcp_servers: [
    {
      type: "url",
      url: "https://api.landrecords.us/mcp",
      name: "landrecords",
      authorization_token: process.env.LR_TOKEN,
    },
  ],
  tools: [{ type: "mcp_toolset", mcp_server_name: "landrecords" }],
  messages: [
    {
      role: "user",
      content: "Who owns 123 Main St, Oakland, CA and what is it assessed at?",
    },
  ],
});

for (const block of response.content) {
  if (block.type === "text") console.log(block.text);
}

The mcp_server_name in the toolset must match the name in mcp_servers — sending mcp_servers without a matching toolset is a validation error. The MCP connector is a beta feature of the first-party Claude API; it is not available through Amazon Bedrock or Google Vertex AI. To restrict which of the eight tools the model may call, add default_config and per-tool configs to the toolset entry.

Drive the tools directly (Python MCP SDK)

When you want deterministic control — a scheduled job, an ETL step, a tool your own agent framework calls — skip the model and call the tools yourself.

import asyncio
import json
import os

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

MCP_URL = "https://api.landrecords.us/mcp"
HEADERS = {"Authorization": f"Bearer {os.environ['LR_TOKEN']}"}


async def main() -> None:
    async with streamablehttp_client(MCP_URL, headers=HEADERS) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools = await session.list_tools()
            print([t.name for t in tools.tools])

            result = await session.call_tool(
                "parcel_count",
                {
                    "where": [
                        {"column": "statefp", "op": "eq", "value": "06"},
                        {"column": "countyname", "op": "eq", "value": "Alameda"},
                        {"column": "yearbuilt", "op": "gt", "value": 1950},
                    ]
                },
            )
            # Every tool returns one text block containing JSON.
            payload = json.loads(result.content[0].text)
            print(payload["count"])


asyncio.run(main())

Paginating a large result set is the same call in a loop — page until numberReturned is less than your limit, or until you have reached numberMatched:

async def all_parcels(session, where, columns, page_size=1000):
    offset = 0
    while True:
        result = await session.call_tool(
            "parcel_query",
            {"where": where, "columns": columns, "limit": page_size, "offset": offset},
        )
        page = json.loads(result.content[0].text)
        rows = page["features"]
        for row in rows:
            yield row
        if len(rows) < page_size:
            return
        offset += page_size

For bulk extraction of whole counties or states, the bulk downloads are dramatically cheaper than paging 1,000 rows at a time — use MCP for questions, files for datasets.

Drive the tools directly (TypeScript MCP SDK)

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://api.landrecords.us/mcp"),
  {
    requestInit: {
      headers: { Authorization: `Bearer ${process.env.LR_TOKEN}` },
    },
  },
);

const client = new Client({ name: "parcel-script", version: "1.0.0" });
await client.connect(transport);

const result = await client.callTool({
  name: "parcel_by_address",
  arguments: { address: "123 Main St, Oakland, CA", distance_meters: 150 },
});

const payload = JSON.parse(result.content[0].text);
console.log(payload.geocode.label, payload.parcels.length);

await client.close();

Other agent frameworks

Anything that speaks remote MCP can use the server without custom code — point it at https://api.landrecords.us/mcp with an Authorization: Bearer header. That covers the Claude Agent SDK (via its MCP server configuration), agent frameworks with MCP adapters, and IDE assistants with MCP support. For clients that only launch local commands, bridge with mcp-remote as shown in Connecting a client.

Prompt guidance for agents

The tools are deliberately small so a model can pick the cheapest one that answers the question. A few lines in your system prompt make that reliable and keep your tool-call quota intact:

You have access to the Land Records parcel tools (157M+ U.S. parcels, read-only).

- Call parcel_schema once before writing your first filter. Do not guess column names.
- For "how many" questions use parcel_count. Never page through rows to count them.
- Request only the columns you need via `columns`. The default returns ~50 per row.
- Filters are AND-only and unordered. There is no sorting — narrow with thresholds
  instead of asking for "the top N" over a large set.
- statefp is a two-digit FIPS string ('06', not 6); countyfp is three digits.
- Distances are meters. Coordinates are [longitude, latitude] in WGS84.
- Geometry is never returned; use centroidx/centroidy or the spatial tools.
- After parcel_by_address, state the geocode label you matched before reporting
  an owner or value, so the user can catch a wrong match.

Two failure modes are worth guarding against explicitly: a model that pulls a 1,000-row page to answer a counting question, and a model that reports an owner without mentioning which address the geocoder actually resolved. Both are prompt-fixable, and both cost you quota when they aren't.

On this page