"""Prompt registrations for the STAC MCP server.
This module exposes a single function `register_prompts(app)` which attaches
all prompt definitions to the provided FastMCP app. Keeping prompts in a
separate module avoids making the server module too large and prevents
import cycles.
Each prompt definition includes both the human-readable content and the
machine-readable payload schema and example. They aim to be epistemically
transparent about their purpose and usage - guiding users on geospatial data
tool interactions.
"""
from __future__ import annotations
import json
from typing import Any
from fastmcp.prompts.prompt import PromptMessage, TextContent
def register_prompts(app: Any) -> None:
"""Register all prompt definitions on the provided FastMCP `app`."""
_common_required = ["collections", "datetime", "bbox", "limit"]
@app.prompt(
name="sensor_registry_info_prompt",
description="Information about the STAC sensor registry",
meta={},
)
def _prompt_sensor_registry_info() -> PromptMessage:
human = (
"The internal STAC sensor registry maps collection ids to native "
"dtypes and provider-specific aliases. Use it as a fallback "
"when a collection id cannot be found by searching the target "
"catalog. It is not exhaustive; always confirm with "
"search_collections or get_collection when available."
)
payload = {
"name": "sensor_registry_info",
"description": (
"Return the internal sensor registry mapping of collection ids "
"to dtype info."
),
"parameters": {"type": "object", "properties": {}, "required": []},
"example": {},
}
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="stac_tool_overview_prompt",
description="Overview of STAC tools available",
meta={},
)
def _prompt_stac_tool_overview() -> PromptMessage:
human = (
"Available STAC tools: get_root, get_conformance, search_collections, "
"get_collection, get_queryables, search_items, get_item, and "
"estimate_data_size."
)
payload = {
"name": "stac_tool_overview",
"description": "Overview of STAC tools available",
"parameters": {},
"example": {},
}
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_get_root_prompt",
description="Usage for get_root tool",
meta={
"schema": {"type": "object", "properties": {}, "required": []},
"example": {},
},
)
def _prompt_get_root() -> PromptMessage:
schema = {"type": "object", "properties": {}, "required": []}
payload = {
"name": "get_root",
"description": "Return the STAC root document for a catalog.",
"parameters": schema,
"example": {},
}
human = (
f"Tool: get_root\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_get_conformance_prompt",
description="Usage for get_conformance tool",
meta={
"schema": {"type": "object", "properties": {}, "required": []},
"example": {},
},
)
def _prompt_get_conformance() -> PromptMessage:
schema = {"type": "object", "properties": {}, "required": []}
payload = {
"name": "get_conformance",
"description": "Return server conformance classes.",
"parameters": schema,
"example": {},
}
human = (
f"Tool: get_conformance\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_search_collections_prompt",
description="Usage for search_collections tool",
meta={
"schema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 10},
"catalog_url": {"type": "string"},
},
"required": [],
},
"example": {"limit": 5},
},
)
def _prompt_search_collections() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 10},
"catalog_url": {"type": "string"},
},
"required": [],
}
payload = {
"name": "search_collections",
"description": "Return a page of STAC collections.",
"parameters": schema,
"example": {"limit": 5},
}
human = (
f"Tool: search_collections\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_get_collection_prompt",
description="Usage for get_collection tool",
meta={
"schema": {
"type": "object",
"properties": {
"collection_id": {"type": "string"},
"catalog_url": {"type": "string"},
},
"required": ["collection_id"],
},
"example": {"collection_id": "my-collection"},
},
)
def _prompt_get_collection() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"collection_id": {"type": "string"},
"catalog_url": {"type": "string"},
},
"required": ["collection_id"],
}
payload = {
"name": "get_collection",
"description": "Fetch a single STAC Collection by id.",
"parameters": schema,
"example": {"collection_id": "my-collection"},
}
human = (
f"Tool: get_collection\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_get_item_prompt",
description="Usage for get_item tool",
meta={
"schema": {
"type": "object",
"properties": {
"collection_id": {"type": "string"},
"item_id": {"type": "string"},
"output_format": {
"type": "string",
"enum": ["text", "json"],
"default": "text",
},
"catalog_url": {"type": "string"},
},
"required": ["collection_id", "item_id"],
},
"example": {
"collection_id": "c1",
"item_id": "i1",
"output_format": "json",
},
},
)
def _prompt_get_item() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"collection_id": {"type": "string"},
"item_id": {"type": "string"},
"output_format": {
"type": "string",
"enum": ["text", "json"],
"default": "text",
},
"catalog_url": {"type": "string"},
},
"required": ["collection_id", "item_id"],
}
payload = {
"name": "get_item",
"description": "Retrieve a single STAC Item.",
"parameters": schema,
"example": {
"collection_id": "c1",
"item_id": "i1",
"output_format": "json",
},
}
human = (
f"Tool: get_item\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_search_items_prompt",
description="Usage for search_items tool",
meta={
"schema": {
"type": "object",
"properties": {
"collections": {"type": "array", "items": {"type": "string"}},
"bbox": {
"type": "array",
"items": {"type": "number"},
"minItems": 4,
"maxItems": 4,
},
"datetime": {"type": "string"},
"limit": {"type": "integer", "default": 10},
"query": {"type": "object"},
"output_format": {
"type": "string",
"enum": ["text", "json"],
"default": "text",
},
},
"required": _common_required,
},
"example": {"collections": ["c1"], "limit": 3},
},
)
def _prompt_search_items() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"collections": {"type": "array", "items": {"type": "string"}},
"bbox": {
"type": "array",
"items": {"type": "number"},
"minItems": 4,
"maxItems": 4,
},
"datetime": {"type": "string"},
"limit": {"type": "integer", "default": 10},
"query": {"type": "object"},
"output_format": {
"type": "string",
"enum": ["text", "json"],
"default": "text",
},
},
"required": _common_required,
}
payload = {
"name": "search_items",
"description": "Search for STAC Items.",
"parameters": schema,
"example": {"collections": ["c1"], "limit": 3},
}
human = (
f"Tool: search_items\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}\n\n"
"Notes:\n"
"If get_collections has not been run yet for the target catalog, "
"run it first to populate the collection list.\n"
"On responses with zero items, validate that the collection IDs "
"are correct.\n"
"If using 'bbox', ensure coordinates are in [minLon, minLat, maxLon, "
"maxLat] order.\n"
"Datetime should be in ISO 8601 format, e.g., '2020-01-01/2020-12-31'.\n"
"Limit should be a positive integer.\n"
"Unlike `pystac-client`, this `limit` is a hard cutoff on the number "
"of items returned post-search, not a page size.\n"
"If a user specifies 'latest' for datetime, interpret it as the "
"most recent available data for the specified collections and "
"use a limit=1.\n"
"To prevent excessive data retrieval, enforce a maximum limit of 10. "
"Requests exceeding this limit should be adjusted down to 10 and batched.\n"
"If 'query' is provided, ensure it conforms to the STAC API "
"filter specification."
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_estimate_data_size_prompt",
description="Usage for estimate_data_size tool",
meta={
"schema": {
"type": "object",
"properties": {
"collections": {"type": "array", "items": {"type": "string"}},
"bbox": {
"type": "array",
"items": {"type": "number"},
"minItems": 4,
"maxItems": 4,
},
"datetime": {"type": "string"},
"query": {"type": "object"},
"aoi_geojson": {"type": "object"},
"limit": {"type": "integer", "default": 10},
"force_metadata_only": {"type": "boolean", "default": False},
"output_format": {
"type": "string",
"enum": ["text", "json"],
"default": "text",
},
},
"required": _common_required,
},
"example": {"collections": ["c1"], "limit": 10, "output_format": "json"},
},
)
def _prompt_estimate_data_size() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"collections": {"type": "array", "items": {"type": "string"}},
"bbox": {
"type": "array",
"items": {"type": "number"},
"minItems": 4,
"maxItems": 4,
},
"datetime": {"type": "string"},
"query": {"type": "object"},
"aoi_geojson": {"type": "object"},
"limit": {"type": "integer", "default": 100},
"force_metadata_only": {"type": "boolean", "default": False},
"output_format": {
"type": "string",
"enum": ["text", "json"],
"default": "text",
},
},
"required": _common_required,
}
payload = {
"name": "estimate_data_size",
"description": "Estimate data size for a STAC query.",
"parameters": schema,
"example": {"collections": ["c1"], "limit": 10, "output_format": "json"},
}
human = (
f"This tool should be run only after search_items has been used to "
"validate collection IDs and sampling parameters.\n\n"
f"Tool: estimate_data_size\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Note: The response may include 'reported_bytes' and 'registry_bytes'\n"
"for per-variable estimates. Prefer 'registry_bytes' when a sensor "
"registry provides an instrument-native dtype.\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_get_queryables_prompt",
description="Usage for get_queryables tool",
meta={
"schema": {
"type": "object",
"properties": {
"collection_id": {"type": "string"},
"catalog_url": {"type": "string"},
},
"required": [],
},
"example": {"collection_id": "my-collection"},
},
)
def _prompt_get_queryables() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"collection_id": {"type": "string"},
"catalog_url": {"type": "string"},
},
"required": [],
}
payload = {
"name": "get_queryables",
"description": "Fetch STAC API (or collection) queryables.",
"parameters": schema,
"example": {"collection_id": "my-collection"},
}
human = (
f"Tool: get_queryables\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_get_aggregations_prompt",
description="Usage for get_aggregations tool",
meta={
"schema": {
"type": "object",
"properties": {
"collections": {"type": "array", "items": {"type": "string"}},
"bbox": {
"type": "array",
"items": {"type": "number"},
"minItems": 4,
"maxItems": 4,
},
"datetime": {"type": "string"},
"query": {"type": "object"},
"catalog_url": {"type": "string"},
},
"required": _common_required,
},
"example": {"collections": ["c1"], "datetime": "2020-01-01/2020-12-31"},
},
)
def _prompt_get_aggregations() -> PromptMessage:
schema = {
"type": "object",
"properties": {
"collections": {"type": "array", "items": {"type": "string"}},
"bbox": {
"type": "array",
"items": {"type": "number"},
"minItems": 4,
"maxItems": 4,
},
"datetime": {"type": "string"},
"query": {"type": "object"},
"catalog_url": {"type": "string"},
},
"required": _common_required,
}
payload = {
"name": "get_aggregations",
"description": "Return aggregations for STAC Items in a collection.",
"parameters": schema,
"example": {"collections": ["c1"], "datetime": "2020-01-01/2020-12-31"},
}
human = (
f"Tool: get_aggregations\nDescription: {payload['description']}\n\n"
"Parameters:\n"
f"{json.dumps(schema, indent=2)}\n\n"
"Example:\n"
f"{json.dumps(payload['example'], indent=2)}"
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="tool_ordering_info_prompt",
description="Information on tool ordering and usage",
meta={},
)
def _prompt_tool_ordering_info() -> PromptMessage:
human = (
"Preferred order: get_root -> get_conformance -> search_collections -> "
"get_collection -> get_queryables -> search_items -> get_item -> "
"estimate_data_size. Use caching and sampling (limit) when "
"estimating."
)
payload = {
"name": "tool_ordering_info",
"description": "Preferred order and usage guidance for STAC tools",
"parameters": {},
"example": {},
}
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="catalog_discovery_prompt",
description="Steps to discover what a STAC catalog supports",
meta={},
)
def _prompt_catalog_discovery() -> PromptMessage:
human = (
"Discovery steps:\n"
"1) Call get_root to locate the catalog root and entrypoints.\n"
"2) Call get_conformance to list supported STAC extensions.\n"
"3) Run search_collections (limit=50) to inspect available "
"collection IDs and titles.\n"
"4) Sample get_collection for candidate collections to validate "
"assets and links.\n\n"
"Parameters:\n"
"- None\n\n"
"Example:\n"
'{"catalog_url": "https://planetarycomputer.microsoft.com/api/stac/v1"}\n\n'
"Notes:\n"
"Use these steps before searching items to avoid mismatched "
"collection IDs."
)
payload = {
"name": "catalog_discovery",
"description": "Checklist for discovering catalog capabilities",
"parameters": {},
"example": {},
}
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="collection_alias_resolution_prompt",
description="How to resolve collection id aliases across catalogs",
meta={},
)
def _prompt_collection_alias_resolution() -> PromptMessage:
human = (
"Alias resolution strategy:\n"
"1) When a collection ID lookup returns no result, normalize case "
"and punctuation.\n"
"2) Try provider-specific aliases (for example: 'sentinel-2-l2a' "
"vs 'sentinel-2-c1-l2a').\n"
"3) For each candidate alias call get_collection to verify presence "
"and canonical metadata.\n"
"4) If still unresolved, call the sensor_registry_info tool as a "
"fallback to map known aliases.\n\n"
"Parameters:\n"
"- collection_id: string (optional)\n\n"
"Example:\n"
'{"collection_id": "sentinel-2-c1-l2a"}\n'
)
payload = {
"name": "collection_alias_resolution",
"description": "Steps to resolve collection id aliases across providers",
"parameters": {},
"example": {},
}
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="estimate_size_strategy_prompt",
description="Safe strategies for estimating data size at scale",
meta={},
)
def _prompt_estimate_size_strategy() -> PromptMessage:
human = (
"Estimation strategy:\n"
"1) Sample a modest number of items (N=10-100) across the "
"requested timeframe.\n"
"2) Prefer 'registry_bytes' when the sensor registry provides an "
"instrument-native dtype.\n"
"3) Aggregate per-band sizes and scale by item counts; persist "
"detailed per-sample results as fixtures.\n"
"4) Avoid downloading full assets; use metadata-only measurements "
"whenever possible.\n\n"
"Parameters:\n"
"- collections: list[string]\n"
"- limit: integer (samples per collection)\n\n"
"Example:\n"
'{"collections": ["sentinel-2-l2a"], "limit": 20}\n'
)
payload = {
"name": "estimate_size_strategy",
"description": "Guidance for safely estimating dataset sizes",
"parameters": {},
"example": {},
}
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)
@app.prompt(
name="explain_tool_output_prompt",
description="Summarize and explain tool outputs for humans",
meta={
"schema": {
"type": "object",
"properties": {
"tool": {"type": "string"},
"payload": {"type": "object"},
},
"required": ["tool", "payload"],
},
"example": {"tool": "get_collection", "payload": {}},
},
)
def _prompt_explain_tool_output() -> PromptMessage:
schema = {
"type": "object",
"properties": {"tool": {"type": "string"}, "payload": {"type": "object"}},
"required": ["tool", "payload"],
}
payload = {
"name": "explain_tool_output",
"description": "Summarize and explain tool outputs",
"parameters": schema,
"example": {"tool": "get_collection", "payload": {}},
}
human = (
"Explain tool output:\n"
"- Give a 2-4 sentence human summary of the tool output.\n"
"- Highlight 3 most important fields and why they matter.\n"
"- Provide 1-2 concrete next steps (e.g., call ordering or "
"validation checks).\n\n"
"Parameters:\n"
"- tool: string\n"
"- payload: object (the tool response)\n\n"
"Example:\n"
'{"tool": "get_collection", "payload": {}}\n'
)
return PromptMessage(
role="user",
content=TextContent(type="text", text=human),
_meta={"machine_payload": payload},
)