
Key Takeaways
- The task today: Checking what is recorded around a prospect (earthquakes, active faults, mineral occurrences) is a bounding-box lookup that Stratigraphic Amenity, the open-source MCP server we built so AI agents can work with geologic maps, runs. At a desktop, a geologist knows the layer they loaded, so an empty selection reads as “none in this layer”.
- The problem: An agent gets a payload, not a layer, and writes prose with one word for three kinds of nothing: not asked, unable to answer, and none found. A call can succeed with zero records from zero of the server’s data sources (its providers), and an empty earthquake result carries no “not evidence” warning.
- What the tool does: Our server saves a record (a bundle) for each query, listing with warnings the providers that query did not consult and those that were unavailable or failed. Earthquake, fault and mineral results carry provenance (source, mode, filters, retrieval date when known), which supplies the agent’s qualifier; it records what was searched, never what exists.
TL;DR
Building an AI agent that reports what is recorded around a prospect? A geologist at a desktop knows what an empty selection means, but an agent writes prose, which has one word for three kinds of nothing: not asked, unable to answer and none found. Stratigraphic Amenity, the open-source MCP server we built for AI agents working with geologic maps, keeps them apart in each query’s saved bundle, but its earthquake lookup adds no “not evidence” warning to an empty result. So the agent has to build its qualifier from each result’s provenance (source, mode, filters, date) and report what was found, what came back empty and what was never searched.
How Should an AI for Geology Agent Read an Empty Result?
Anyone building AI for geology meets an old problem in a new place. An assay certificate has more than one way of saying nothing. A gold value of “<0.005” means the lab ran the sample and found less than its method can measure. A blank means nobody asked for gold. The code for a sample listed but not received means the bag never reached the lab. Well-kept drillhole databases store the three differently. A basic rule of plotting geochemistry is that none of them is a zero.
A geologist checking what is recorded around a prospect rarely confuses them either. The earthquake catalogue or fault database is a layer they chose and loaded into a desktop GIS themselves. They know roughly what it covers and from when, so an empty selection reads to them as “none in this layer”. An AI agent asked the same question meets the same three kinds of nothing. But it receives a payload instead of a layer, and its final product is a sentence. Sentences have one word for all three: “no”.
An Empty Result From a Call That Searched Nothing
Here is the case that makes this concrete. A user asks an agent whether anyone has recorded earthquakes or active faults around a prospect. The agent calls Stratigraphic Amenity, our open-source MCP server that gives AI agents tools for geologic maps and geology lookups. The request is a bounding-box lookup of earthquakes and faults with nothing else in it. The call succeeds. The summary opens: “Knowledge query found 0 record(s) across 0 provider(s); no provider returned records.” An AI for geology agent in a hurry then reports that the area has no recorded earthquakes or active faults.
No provider searched anything. Nobody had installed the knowledge base, so both default providers were unavailable. The same summary listed six warnings: three about the missing data, three naming providers the query had not consulted. “Across 0 provider(s)” was the tell. The server hid nothing. The sentence did.
Three Kinds of Nothing in an AI for Geology Lookup
The server keeps the three kinds apart, and each needs a different sentence.
The first is not asked. Some providers are off by default because they send the query area to outside services. Mineral occurrences, land cover and population density run only when the agent names them. A broad query lists them in the bundle (the saved record of the query) as not consulted. It also adds a warning that names the fix: Compatible provider 'mineral_occurrences' was not consulted because it is disabled by default. Retry with include=["mineral_occurrences"]. Regional sources give a second version. Two mineral occurrence databases ship as examples, one for Ontario and one for Quebec. Outside one of them, a mineral query gets this warning: “Query falls outside the Quebec coverage of ‘sigeom_mineral_occurrences’; this is not evidence that no occurrences exist.” This is the blank cell. Nobody asked for the element.
The second is asked, but could not answer. When a default provider’s data is missing, or the provider fails, the call still succeeds. The failure becomes a warning and an entry in the bundle’s trace marked unavailable or failed. The provider is missing from the record counts altogether. Only when the agent named providers and none of them succeeded does the call fail outright. This is the bag that never reached the lab. It is also the most dangerous kind, because it says something about the server and nothing about the ground.
The third is asked and found none: a record count of zero from a provider that ran. This is the only true below-detection result. Like any below-detection result, it means only as much as the method behind it.
The Absence of the Absence Warning
It is tempting to let the server’s disclaimers do the hedging. The active-fault lookup attaches one to every empty result: “No GEM active-fault features intersected the bounds; this is not evidence that no active faults exist.” The mineral provider does the same inside its coverage. So an agent could learn a simple rule: hedge when the server says “not evidence”.
The earthquake provider breaks that rule. Its empty result carries no such warning. Its only comment on the result is “No earthquakes with configured filters were found within bounds.” That line sits in the item’s summary inside the structured result. Meanwhile the text summary, which is all a text-only host shows the model, carries only the count. An agent that hedges when warned will qualify the faults and state the earthquakes flatly. A disclaimer is a courtesy, not a contract, and the absence of an absence warning is not evidence either.
What a Zero Is a Fraction Of
So the qualifier has to come from the provenance, which each earthquake, fault and mineral item carries. It names the source, its mode (a synced mirror, the file installed with the knowledge base, or a live request), the bounds and the filters. A zero is a fraction, and this is its denominator.
Seismologists already have a name for part of it: the magnitude of completeness. Below that magnitude, you cannot trust a catalogue to list every event. The server’s default USGS profile makes its floor explicit: magnitude 4.5 and above, since 1 January 1970. A zero from a mirror synced worldwide with that profile is a precise claim. It means none of those events in the box, as of the mirror’s retrieval date, and the provenance records both the profile and the date. That is “<0.005 ppm by fire assay”: a result with its method attached.
What the Provenance Does Not Say
The provenance records what the server searched, never what exists, and sometimes it records its own uncertainty. On a default install the earthquake data is the file that came with the knowledge base. Its provenance reports a legacy local asset with no retrieval date. That is honest, and it is information: the sentence should carry the uncertainty rather than erase it. A live query on a default install still applies the default floor, but its provenance lists only the filters the agent passed. An agent that wants a checkable sentence should therefore state its own minimum magnitude and start time.
Two more gaps sit in the record itself. The server can answer a repeated query, live ones included, from its provider cache. The trace then marks it cache_hit: true. A live mineral result carries retrieved_at: null too, so “live” names where the data came from, not when. And a USGS mirror synced for a smaller box records its coverage as global-service without recording the box. A query elsewhere then comes back empty while its provenance reads as worldwide.
Mineral inventories are the hardest case. An occurrence database records what someone reported, and nobody surveyed the gaps. Ecologists working from museum records call this presence-only data. They learned that an empty cell says more about where the collectors went than about where the species lives. The Ontario endpoint that ships as an example is a 2013 snapshot. An empty result from it says nothing about discoveries since 2013, or about ground nobody prospected. A mineral query outside every shipped region still returns a zero for mineral_occurrences. Its summary reads “No mineral occurrences returned; query was outside configured source coverage.” So a zero in record_counts is not enough on its own: this one comes from a provider that ran but searched no source, and it means “not asked”.
Adding Your Own Regions or Countries
Ontario and Quebec serve as the shipped examples, not the edge of what the mineral provider can hold. Adding your own survey’s occurrence service moves your region from “not asked” to “asked”. It is a code change, not a setting:
- Write an adapter for the service, modelled on
knowledge/sources/sigeom_minerals.py. It needs anendpoint, anormalizer_versionand aquery_bboxmethod that returns GeoJSON for a box. A matching function turns the features into records with a name, longitude and latitude. - Register a
SourceDefinitionindefault_source_registry()(knowledge/sources/registry.py). Give it the familymineral_occurrences, adefault_profilewith your service’sendpointandregion, its license and attribution, andcoverage_boundsfor your region. - Add a branch to
_mineral_occurrence_provider_factoryinknowledge/service.pythat builds aMineralSourceBindingfrom your adapter, its matching function, those coverage bounds and a region name. Without this branch the factory skips the new ID without a warning. - Add the new source ID to
GEOMAP_MINERAL_OCCURRENCE_SOURCE_IDS, listing the shipped IDs too if you want to keep them.
A query whose box falls outside your coverage bounds then gets the same “this is not evidence” warning as the shipped sources.
Where AI for Geology Answers Go Wrong
None of this describes a server hiding things. The bundle separates the three kinds of nothing in its structure. The text summary repeats every warning, so a text-only host cannot reduce them to a count. The tool’s description tells the agent to treat warnings and provenance as evidence, including for empty results. The failure happens at the last step, where a structured record turns into prose.
A geologist would reject an assay table with three kinds of nothing merged into zeros. An AI for geology report deserves the same standard. It has three parts:
- what the agent found;
- what it searched that came back empty, with the source, filters and date;
- what it did not search, and why.
It is a longer answer, and the only one a person can check.
Which leaves a question worth sitting with: is there any geology lookup where an agent should write a plain “no”? It is hard to find an earthquake or fault query where “none recorded in this source, above this magnitude, as of this date” is not the more accurate answer. It is harder still to find one where the shorter answer is worth the risk.
Watching the Quiet Kind of Nothing
You don’t have to take any of this on trust, and you don’t need a catalogue service of your own to see it. The open-source server will show you all three kinds of nothing on your own machine. The most instructive result arrives before you have installed any data at all.
Watch the quiet kind, because an agent’s sentence swallows it most easily. That is a provider the query asked, which could not answer yet did not stop the call. Neither the documentation nor the agent’s prompt decides whether that becomes an error or one more line among the warnings. A short stretch of the knowledge service decides it.
Where a Missing Provider Becomes a Warning
One loop of the knowledge service decides the second kind of nothing: a provider asked but unable to answer. Each selected provider runs inside a try, and these are the branches that catch it when it cannot answer:
except ProviderOptionError:
raise
except (MissingAssetError, OptionalDependencyError) as exc:
trace_events.append(
{
"provider": registration.id,
"status": "unavailable",
"reason": (
"missing_asset"
if isinstance(exc, MissingAssetError)
else "optional_dependency"
),
"explicit": explicit,
}
)
if explicit:
explicit_failures.append((registration.id, exc))
warnings.append(f"{registration.id}: {exc}")
continue
warnings.append(f"{registration.id}: configured provider asset or dependency is unavailable.")
except Exception as exc: # noqa: BLE001 - provider boundaries should isolate failures.
provider_error = ProviderError(f"Provider {registration.id!r} failed: {exc}")
trace_events.append(
{
"provider": registration.id,
"status": "failed",
"reason": "provider_error",
"explicit": explicit,
}
)
if explicit:
explicit_failures.append((registration.id, provider_error))
warnings.append(
f"{registration.id}: provider failed: {type(exc).__name__}: {exc}"
)
continue
warnings.append(f"{registration.id}: provider failed: {type(exc).__name__}: {exc}")
if explicit_ids and not explicit_success_ids and explicit_failures:
raise explicit_failures[0][1]The loop re-raises malformed provider options at once, so they end as an error. A provider whose data file or Python package is missing raises MissingAssetError or OptionalDependencyError. Any other exception lands in the last branch. Either way the loop records a trace event ("status": "unavailable" with its reason, or "status": "failed") and a warning. Then it moves on without producing an item. The MCP layer builds record_counts from items alone (mcp/adapter.py:673-690). So a provider that never answered is absent from the counts instead of showing a zero. That is why the opening query reported “0 provider(s)”. Apart from that re-raise, the last two lines are the only way out to an error. The call fails when the agent named providers and none of them succeeded. A provider that ran by default fails quietly, as one line among the warnings.
Where the Other Kinds of Nothing Come From
Other parts of the code write the other kinds of nothing. The service lists providers left out by default in trace.providers_not_consulted (knowledge/service.py:768-776). Then _knowledge_notice_warnings (mcp/adapter.py:1067-1111) turns them into warnings and adds the “Knowledge assets are unavailable for: …” notice. The empty-result disclaimers live inside each provider (knowledge/providers/faults.py:165-169, knowledge/providers/minerals.py:179-185). The earthquake provider writes only its item summary (knowledge/providers/earthquakes.py:179-182).
Running an AI for Geology Lookup on a Fresh Install
To watch the three kinds appear, start from a fresh install:
- Clone the Stratigraphic Amenity repository and install the documented profile with
uv sync --group dev --extra mcp --extra geo --extra assets --extra detectors. - Register the server with your MCP host.
- Before installing the knowledge base, call
geomap_query_knowledgewith a box and nothing else:
{
"bounds": {"min_lon": -80.2, "min_lat": 46.1, "max_lon": -79.8, "max_lat": 46.4, "crs": "EPSG:4326"}
}With default settings the call succeeds: “Knowledge query found 0 record(s) across 0 provider(s)”, followed by six warnings. trace.providers holds two events with "status": "unavailable" and "reason": "missing_asset".
Now add "include": ["earthquake_history"] and call again. The named provider fails alone, so the call returns isError: true with the code missing_knowledge_asset. Its hint says to call geomap_prepare_knowledge once the user agrees. Do that, then repeat the first query. Both providers answer now. Each item’s provenance shows "source_mode": "legacy_asset", "coverage_status": "legacy-local-asset" and "retrieved_at": null. The warnings include “earthquake_history: using legacy local asset because no source mirror was found.” For this box they also include “GEM active-fault coverage has a documented coarse gap near canada; missing features may reflect source coverage rather than absence of active faults.”
FAQs
Does an empty earthquake or fault lookup mean there are none?
No. A zero means the source the server searched held none for the filters used, as of that data’s date. The default USGS profile covers magnitude 4.5 and above since 1970, so smaller or older events were never part of the search. Read the warnings and each result’s provenance before writing “none recorded”.
What does provenance tell an AI for geology agent?
It records what the server searched: the source, its mode (synced mirror, installed file or live request), the bounds and the filters, plus the retrieval date when known. It never says what exists. The agent writes its qualifier from these fields, such as “none recorded in this source, above this magnitude, as of this date”.
Why does an AI for geology query return zero records from zero providers?
Usually because the default providers could not run. Until someone installs the knowledge base, the earthquake and fault providers are both unavailable, so the call succeeds with no records and explains why in its warnings. The query searched nothing. Call geomap_prepare_knowledge after the user agrees, then repeat the query.
Can I add my own region or country?
Yes. Ontario and Quebec are the shipped example mineral sources, not a limit. Adding a region is a code change, not a setting: write an adapter for your survey’s service, register it with its coverage bounds, bind it in the knowledge service, and add its ID to GEOMAP_MINERAL_OCCURRENCE_SOURCE_IDS. Queries that name mineral_occurrences inside that region then reach it.
Try It With Geocluster
Stratigraphic Amenity is one of the Geocluster tools, MCP servers that let AI agents work with geology data and maps.
When a lookup fails outright instead of coming back empty, handle the map server’s errors by who can fix them.


