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.

Every one of them anchors on something indexed. The parcel layer is 158M rows, and only lrid, parcelid, parcelid2, ownername, geoid, ownertype, accesstype, placetype and the centroid pair carry an index — statefp, countyfp and countyname do not. Geography is expressed as a geoid range (ge the prefix, lt the next one) so the query has an index to ride; an unanchored filter runs into the statement timeout instead of returning slowly.

Ownership and value at a coordinate

"Who owns the parcel at -122.2711, 37.8044, and what's it assessed at?"

parcel_at_point → an exact point-in-polygon test, then ownername and totalvalue off the returned parcel. There is no address lookup in the tool set: resolve a street address to a lon/lat with your own geocoder, or narrow by parceladdr inside a geoid-anchored parcel_query.

A count, not a data dump

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

parcel_schemaparcel_count with a geoid range for Alameda County (geoid ge '06001' AND geoid lt '06002'), plus usedesc ilike '%single family%' and yearbuilt gt 1950. A well-behaved model reaches for parcel_count here rather than pulling thousands of rows — and it anchors the filter on geoid, which is indexed, rather than on countyname, which is not.

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 a Virginia geoid range (geoid ge '51' AND geoid lt '52') and columns: ["parceladdr", "ownername", "taxacres", "totalvalue"] for 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 the parcel at -77.0365, 38.8977?"

parcel_at_point to identify the parcel → parcel_near_point with a 100–300 m radius around its centroidx / centroidy. Note the second call matches neighbours by centroid, so a very large adjacent parcel can fall outside a tight radius.

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": "geoid", "op": "ge", "value": "06001"},
                        {"column": "geoid", "op": "lt", "value": "06002"},
                        {"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_at_point",
  arguments: { longitude: -122.2711, latitude: 37.8044 },
});

const payload = JSON.parse(result.content[0].text);
console.log(payload.found, 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 (160M+ 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 53 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.
- Anchor every filter on an indexed column: lrid, parcelid, parcelid2, ownername,
  geoid, ownertype, accesstype, placetype. statefp, countyfp and countyname are NOT
  indexed — express a state or county as a geoid range instead (Alameda County CA is
  geoid ge '06001' and lt '06002'). An unanchored filter times out.
- Distances are meters. Coordinates are [longitude, latitude] in WGS84.
- Geometry is never returned in rows; use centroidx/centroidy, or parcel_at_point
  with include_geometry when you need the polygon itself.
- parcel_near_point, parcel_within_bbox and parcel_within_polygon match centroids.
  For "what parcel is at this exact coordinate", use parcel_at_point.
- There is no address tool. For "what is at this location" use parcel_at_point
  with a lon/lat; it tests the real polygon. parcel_near_point is centroid
  proximity, which is a different question.

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 answers "what is at this location" with parcel_near_point — centroid proximity — when the question wanted parcel_at_point's containment test. Both are prompt-fixable, and both cost you quota when they aren't.

On this page