mcp-stargazing
# mcp-stargazing
Calculate the altitude, rise, and set times of celestial objects (Sun, Moon, planets, stars, and deep-space objects) for any location on Earth, with optional light pollution analysis.
## Features
- **Altitude/Azimuth Calculation**: Get elevation and compass direction for any celestial object.
- **Rise/Set Times**: Determine when objects appear/disappear above the horizon.
- **Light Pollution Analysis**: Load and analyze light pollution maps (GeoTIFF format).
- **Composite Planning**: Build a ranked observing plan that combines place quality, weather, moonlight, and top targets.
- **Tool Discovery**: Inspect registered MCP tools programmatically through `get_tool_catalog`.
- **Code Execution Ready**:
- **Serializable Returns**: All tools return JSON-serializable data (ISO strings for dates), making them directly usable by LLMs.
- **Pagination**: `analysis_area` supports paging (`page`, `page_size`) to handle large datasets efficiently.
- **Stable Result Handles**: `analysis_area.resource_id` identifies the cached non-pagination query so agents can fetch multiple pages safely.
- **Standardized Responses**: Successful calls return `{ "data": ..., "_meta": ... }`; business validation failures return `{ "error": ..., "_meta": ... }`.
- **Performance**:
- **Async Execution**: Non-blocking celestial calculations.
- **Caching**: Intelligent caching for Simbad queries and regional analysis.
- **Proxy Support**: Native support for HTTP/HTTPS proxies (useful for downloading astronomical data).
- **Time Zone Aware**: Works with local or UTC times.
- **Data Driven**: Integrated database of 10,000+ deep-sky objects (Messier & NGC) for smart recommendations.
## Installation
This project uses [uv](https://github.com/astral-sh/uv) for dependency management.
### Local Installation
1. **Install `uv`**:
```bash
pip install uv
```
2. **Sync dependencies**:
```bash
uv sync
```
This will create a virtual environment in `.venv` and install all dependencies defined in `pyproject.toml`.
3. **Activate the environment**:
```bash
source .venv/bin/activate
```
4. **Initialize Data** (Required for Nightly Planner):
This downloads the latest Messier and NGC catalog data to `src/data/objects.json`.
```bash
python scripts/download_data.py
```
*Note: If you are behind a firewall, ensure `HTTP_PROXY` env var is set before running this script.*
### Docker Installation
You can also run the server using Docker, which handles all dependencies and data initialization automatically.
Docker catalog downloads use the official `simbad.harvard.edu` mirror. Override with
`--build-arg SIMBAD_SERVER=simbad.cds.unistra.fr` to use the Strasbourg service.
Standalone downloads retain the astroquery default unless `SIMBAD_SERVER` is set.
The selected server is tried first; failed connection/query batches retry once and
switch to the other official mirror. Subsequent batches prefer the working mirror.
Downloads use bounded, indexed TAP queries with explicit asynchronous execution,
avoiding astroquery 0.4.11's uploaded-name query path (which drops `async_job`).
An empty batch or failure of both mirrors aborts rather than publishing a partial catalog.
Connection probes have a 5-second connect and 15-second read timeout; asynchronous
jobs have a 180-second execution limit. macOS Python can inherit the system proxy
even without proxy environment variables; use process-local `NO_PROXY=*` only when
deliberately testing direct connectivity. A successful small query does not verify
the full catalog download or CI build.
1. **Build the image**:
```bash
docker build -t mcp-stargazing .
```
*Note: If you are behind a proxy, pass the proxy URL during build:*
```bash
docker build --build-arg HTTP_PROXY=http://127.0.0.1:7890 -t mcp-stargazing .
```
2. **Run the container**:
```bash
# Basic run (MCP on :3001 + SPF web UI on :5001)
docker run -p 3001:3001 -p 5001:5001 mcp-stargazing
# With Environment Variables
docker run -p 3001:3001 -p 5001:5001 \
-e QWEATHER_API_KEY=your_key \
-e STARGAZING_DB_CONFIG=your_db_config \
mcp-stargazing
```
3. **Access**:
- **MCP server** → `http://localhost:3001/shttp` (for AI agent / MCP client)
- **SPF web UI** → `http://localhost:5001/` (stargazing place finder frontend)
### Docker Architecture
The container runs two services managed by [supervisord](http://supervisord.org/), both sharing the same Python
virtual environment via `uv run`:
```
Container
├── supervisord
│ ├── program:mcp → uv run mcp-stargazing --mode shttp --port 3001
│ └── program:spf-web → uv run uvicorn server.main:app --port 5001
│
Dependency resolution (single venv, no duplication):
stargazing-core>=0.2.1 ← resolved once from PyPI
stargazing-place-finder>=0.9.1
fastapi, uvicorn, ... ← SPF's transitive deps
```
## MCP Server Usage
Start the MCP server to expose tools to AI agents or other clients.
### Agent-first direction
`plan_stargazing_session` is the preferred composite decision entry point for third-party agents.
It accepts a place name, center/radius, or exact bounds; resolves Seestar S50,
HOPE D60 + ASI2600MC Air, or custom optics configurations; keeps provider and database choices on
the server; and returns a first-class decision, alternatives, exact score components, evidence,
uncertainties, material missing inputs, ranking-reversal conditions, and next actions.
`get_best_stargazing_plan` and `get_multiday_stargazing_plan` remain registered as compatibility
entries for existing bbox-first callers. The deterministic 10-scenario gate and representative
Codex/Claude black-box smoke passed before the primary entry point was registered. New independent
astronomy lookup tools remain deferred while development focuses on production evidence and
planning reliability. Background tasks and the 1.0 contract freeze are already delivered.
Background area analysis has bounded execution, duplicate submission, cancellation, deadlines,
retention, and paged result retrieval. The task tools activate optional `task_id` / `progress`
metadata; running progress is unknown, and runtime loss requires resubmission. See
[Task Lifecycle](docs/task-lifecycle.md) for the workflow, limits, and recovery semantics.
See [Third-party Agent Contract](docs/agent-contract.md),
[Workspace Technical Map](docs/workspace-technical-map.md), and
[Agent Evolution Checklist](docs/workspace-action-checklist.md). The current 21-tool routing and
parameter review is recorded in [Tool Surface Audit](docs/tool-surface-audit.md). Evaluation assets
and the two-client evidence are under [`evaluations/`](evaluations/README.md).
The evaluation guide also documents prospective forecast capture and independent-observation
pairing, with explicit site, time, measurement-definition and lead-time limits.
The [pre-1.0 public schema baseline](docs/public-contract-baseline.md) records the existing tool
schemas and detailed planning/area models. SDK and real `tools/list` tests detect contract drift
before new tools are added.
The independent [1.0 contract and migration guide](docs/v1-migration.md) freezes all 21 tools,
task models, detailed payloads, annotations, and envelope semantics without replacing that
historical snapshot. The two bbox-first planners remain supported and have no active deprecation
notice. Future removal requires at least 90 days, two subsequent minor releases, and a later major.
The package version and response `_meta.version` are separate; this contract freeze is not a release.
For a complete primary-planner client, save one of the guide's JSON requests and run:
```bash
uv run python examples/agent_planning_demo.py --request request.json --url http://localhost:3001/shttp
```
### 1. Environment Setup
Create a `.env` file or export variables:
```bash
# Weather tools
# 推荐:使用你账号专属的 API Host(公共域名将从 2026 年起逐步停止服务)
export QWEATHER_API_HOST="abc1234xyz.def.qweatherapi.com"
# 鉴权(二选一)
# 1) API KEY(兼容旧用法)
export QWEATHER_API_KEY="your_api_key"
# 2) JWT(推荐,更安全)
# export QWEATHER_JWT_TOKEN="your_jwt_token"
# 如需临时兼容旧公共域名(不推荐),显式开启:
# export QWEATHER_ALLOW_PUBLIC_HOST=1
# Geocoding(默认零付费、零 key)
# CJK 查询默认使用内置离线行政区划数据;未命中时回退 Photon / Nominatim。
# 可选 provider 顺序覆盖(全部支持:gazetteer, photon, nominatim, amap)
# export GEOCODER_PROVIDERS="gazetteer,photon,nominatim"
# 仅当你希望显式启用付费的 Amap 地理编码时才需要:
# export GEOCODER_PROVIDERS="amap,gazetteer,photon,nominatim"
# export AMAP_KEY="your_amap_key"
# Optional: Proxy for downloading astronomical data (Simbad/IERS)
# Highly recommended if you are in a restricted network environment
export HTTP_PROXY="http://127.0.0.1:7890"
export HTTPS_PROXY="http://127.0.0.1:7890"
```
### 2. Start Server
**Streamable HTTP (SHTTP) mode** (Recommended for most agents):
```bash
# Basic start
python -m src.main --mode shttp --port 3001 --path /shttp
# With proxy explicitly passed (overrides env vars)
python -m src.main --mode shttp --port 3001 --path /shttp --proxy http://127.0.0.1:7890
```
**SSE mode**:
```bash
python -m src.main --mode sse --port 3001 --path /sse
```
`dev` mode is no longer supported because current FastMCP versions no longer provide `run_dev()`. Use `local`, `shttp`, or `sse`.
### 3. Response Format
Successful business responses return data in a standardized JSON format:
```json
{
"data": {
// Tool-specific return data
"altitude": 45.5,
"azimuth": 180.0
},
"_meta": {
"version": "1.0.0",
"status": "success"
}
}
```
Business validation failures use the same envelope style:
```json
{
"error": {
"code": "INVALID_TIME_FORMAT",
"message": "Invalid time format: invalid-time-format",
"details": {
"time_string": "invalid-time-format"
}
},
"_meta": {
"version": "1.0.0",
"status": "error"
}
}
```
At the MCP protocol layer, `tools/list` and `get_tool_catalog` are kept aligned, and JSON-RPC request ids are preserved in both SHTTP and SSE transport tests.
Every tool's standard MCP description includes **Use when**, **Do not use when**, **Requires**,
**Returns**, and **Follow-up** guidance. These explain the primary planner versus compatibility
entries, facts versus observing decisions, synchronous versus background area searches, and
common scope traps such as server-local time and constellation-center lookup. The wrapped catalog
keeps its summary `description` and full `docstring`; explicit decorator descriptions are complete
and take precedence. See the [tool routing audit](docs/tool-surface-audit.md).
### 4. Available Tools
External AI callers should follow the [planning result interpretation guide](docs/planning-result-guide.md).
The critical field, uncertainty, score and location boundaries are also in standard MCP
descriptions, visible through `tools/list` without opening this README.
Composite planners score the **requested night's hourly forecast**, including low/mid/high cloud
layers. Current conditions or daily averages never substitute for missing forecast hours.
Open-Meteo requests 16 forecast days; actual usable coverage depends on each provider/model and
field. Out-of-horizon spatial weather is omitted rather than snapped to the last model time.
Missing/partial night coverage is explicit in notes/uncertainties; a degraded ranking is
provisional. Weather evidence records the provider, interval, available hours and cloud
aggregation. Weather base scores stay within 0–100; known forecasts with no usable cloud/rain/wind
interval receive zero weather credit. Multi-night runs reuse raw place/weather retrievals, then
evaluate each night separately; this reduces repeated I/O but does not guarantee a 60-second call.
`location.center` plus `radius_km` requests **regional site selection**, not a plan locked to that
coordinate. Quietness/popularity adjustments require the corresponding preference flags; these
scores are heuristics, not measured noise or crowd counts. `night_window` retains its documented
**civil-dark** meaning (Sun below −6°), not astronomical darkness (−18°). Civil-dark duration is
not a promise of usable deep-sky exposure time; verify twilight and access before departure.
- **`get_celestial_pos`**: Calculate altitude/azimuth.
- **`get_celestial_rise_set`**: Calculate rise/set times (Returns ISO strings). Use `event_scope="calendar_day"` for the historical full-local-day behavior, or `event_scope="next_events"` to find the first rise and first set after the supplied `time`, rolling across midnight when needed.
- **`get_moon_info`**: Detailed moon phase, illumination, and age.
- **`list_visible_planets`**: List of all planets currently above the horizon with positions.
- **`get_constellation`**: Find the position (Alt/Az) of a constellation center.
- **`get_nightly_forecast`**: Smart planner returning curated list of best objects to view tonight (Planets + Deep Sky) plus the local civil-dark `night_window` with dusk, dawn, duration, date, and timezone.
- **`get_weather_by_name` / `get_weather_by_position`**: Fetch current weather with automatic retry on network failures.
- Atmospheric state is included in `summary.current.atmosphere` and
`summary.hourly[*].atmosphere`, with provenance and independent errors in
`summary.atmosphere_sources` (also available in each provider's normalized data).
The optional state contains PM2.5/PM10, NO₂/O₃/SO₂/CO, dust, AOD at 550 nm,
visibility and boundary-layer height. CO uses μg/m³; AOD is dimensionless.
- Atmospheric `time` is an explicit-offset model valid time. Current atmosphere uses
its own hourly time, which can differ from parent weather `observation_time`.
`data_kind=model_estimate`, `temporal_role`, per-field source keys and source metadata
distinguish estimates from observations and retrieval from model run time.
- Open-Meteo reuses the weather response and makes one additional CAMS global request.
Exact valid-time joins preserve the shorter air horizon; missing values are unknown.
Air failures do not discard valid weather. Explicit `provider=qweather/wttr` makes no
hidden Open-Meteo call and currently has no atmosphere data.
- Native CAMS global data is approximately 45 km and 3-hourly; hourly output may be
interpolated. AOD is column extinction, not surface visibility; local boundary-layer
height is not a pollution ceiling. No AQI, pollution attribution or new ranking score
is calculated. See the [reviewed contract extension](docs/weather-atmosphere-contract.md).
- **`get_local_datetime_info`**: Get current local time information.
- **`get_tool_catalog`**: Discover available MCP tool metadata and parameters.
- **`plan_stargazing_session`**: Preferred end-to-end entry point for deciding when, where, and what to observe across one to seven nights.
- **Inputs**: a `request` object containing `location` (exactly one of `place_name`, `center`, or `bounds`), `start_date`, optional `time_zone`, `nights`, `observation_style`, `equipment`, candidate/target limits, and popularity preferences.
- **Equipment**: stable presets are `seestar_s50` and `hope_d60_asi2600mc_air`; advanced callers may use `custom` or override individual optical fields.
- **Target windows**: equipment shortlists are checked individually with the public core scheduler inside the selected weather window (25° minimum altitude, 30-minute minimum run). Complete curves with no slot are excluded. Missing/invalid/partial curves remain explicitly unverified references; generic targets are also references. Read `selected_target_window_check` evidence and `target_window_unverified` / `target_window_exclusions` uncertainties. Independent slots may overlap; this is not a joint shooting plan or a Moon-separation, tracking, weather or exposure guarantee. Legacy `transit_alt` retains its source-specific meaning.
- **Returns**: normalized request and resolved geography, a first-choice `decision`, ranked `alternatives`, traceable `evidence`, `uncertainties`, material `missing_inputs`, `would_change_if` conditions, and concrete `next_actions`.
- **Atmospheric evidence**: weather `evidence[*].facts.atmosphere` summarizes model values within the selected observing window, with per-field missing samples, units and sources. It explains uncertainty without changing scores or eligibility; coverage is not validated transparency or seeing. See [window evidence](docs/planning-atmosphere-evidence.md).
- **Routing**: use this for a complete observing decision. Use weather, celestial, telescope, or place tools directly for focused fact lookups.
- **`get_best_stargazing_plan`**: Build a ranked regional observing plan with candidate places, weather summaries, best observation windows, and top targets.
- **Inputs**: `south`, `west`, `north`, `east`, `time`, `time_zone`, `candidate_limit`, `target_limit`, `weather_provider`, `max_locations`, `min_height_diff`, `road_radius_km`, `network_type`, `observation_style`, `avoid_popular_spots`, `prefer_quiet_at_night`, `popularity_radius_km`, `db_config_path`.
- **Candidate recall**: `candidate_limit` is the number of ranked candidates returned. The service evaluates a bounded recall pool before final truncation, so a location with better weather can overtake a statically higher-ranked location.
- **Cost-aware evaluation**: the recall pool receives a low-cost weather screen first; night-window and target-forecast evaluation then runs only for the strongest internal candidates before the final ranking.
- **Regional weather signal**: when available, the server samples the stargazing-core spatial cloud-cover grid once for the whole bbox and blends it into regional comparison; a spatial-model failure degrades without failing the plan.
- **Returns**: `query`, `summary`, and `candidates`, where `query.analysis_resource_id` links the plan back to the underlying neutral `analysis_area` search when available. When popularity preferences are enabled, MCP applies a single adjustment from the returned heuristic fields, surfaces those reasons in `candidates[*].recommendation_reasons`, and summarizes the top recommendation in `summary.popularity_preferences_enabled`, `summary.primary_recommendation_drivers`, and `summary.recommended_location_reason_summary`. Each candidate's `score_breakdown.factor_contributions` exposes the signed place, weather, popularity, and accessibility contributions used for ranking; explanatory reasons use the same active factors. The driver values form a stable enum: `dark_sky`, `high_place_score`, `clear_weather`, `good_visibility`, `observation_style_fit`, `low_popularity_risk`, `quiet_at_night`, and `strong_targets`.
- **Popularity ownership**: all composite planners (single-night, multi-night, and `plan_stargazing_session`) request SPF place scores with `avoid_popular_spots=false` and `prefer_quiet_at_night=false`, preserving `popularity_radius_km` and the heuristic evidence. MCP alone applies the requested preferences; screening and final evaluation each start from the neutral score. Request echoes retain the user's preferences. To reproduce `query.analysis_resource_id` (or the primary planner's place `evidence.resource_id`), repeat the same area search with both preference flags disabled. Standalone `analysis_area` still forwards preferences to SPF and uses a separate cache entry when enabled.
- **Observation style**: `balanced` preserves the default behavior. `deep_sky` emphasizes dark skies and low moonlight, `visual` raises the weather weight, `planetary` reduces moon penalties and prioritizes planets, and `beginner` also favors nearby road access.
- **Night window**: each candidate includes `night_window` when civil darkness can be determined, with local date, civil dusk, following civil dawn, duration, and timezone. `best_observation_window` now represents a continuous dark-sky run when hourly data permits; `start_time`, weather fields, and the additive `end_time`, `duration_hours`, and `quality_score` are returned together.
- **`get_multiday_stargazing_plan`**: Compare one to seven consecutive nights and return the best night and location across the region.
- **Inputs**: `south`, `west`, `north`, `east`, `start_date`, `time_zone`, `nights`, plus the same candidate, weather, street-network, and observation-style options as `get_best_stargazing_plan`.
- **Returns**: `query`, a cross-night `summary` with `best_night_date`, `best_location_name`, and `best_score`, and `nights[]` where each night contains its `night_window`, nightly `plan`, errors, and warnings.
- **Degradation**: Weather or forecast sub-queries may degrade into `summary.warnings` and per-candidate `notes`, while the overall planning response remains successful.
- **`get_telescope_targets`**: Match deep-sky objects against telescope optics — find what's best visible with your equipment.
- **Inputs**: `focal_length_mm`, `lon`, `lat`, `time`, `time_zone`, plus optional optics fields such as `aperture_mm`, sensor dimensions, pixel size, reducer/Barlow factor, mount type, filter, and `limit`.
- **Returns**: Ranked targets with visibility scores, altitude/azimuth, rise/set/transit data, FOV fit, surface brightness, mosaic guidance, and the normalized telescope configuration.
- **`get_shooting_plan`**: Generate an optimized imaging schedule for a target, maximizing time above altitude threshold.
- **Inputs**: the same telescope/observer fields as `get_telescope_targets`, plus optional `limit` and `min_altitude`.
- **Observation time**: naive values use `time_zone`; ISO values with an explicit UTC offset preserve that instant, consistently with target matching.
- **Returns**: `targets`, `moon`, a single-night `plan` with timed imaging slots, `config`, and `total`.
- **`light_pollution_map`**: Query light pollution data for a bounding box area.
- **Inputs**: `south`, `west`, `north`, `east`, `zoom` (default 10).
- **Returns**: A grid of data points with Bortle class, brightness, and SQM values.
- **`analysis_area`**: Find best stargazing spots in a region.
- **Inputs**: `south`, `west`, `north`, `east`, `max_locations`, `min_height_diff`, `road_radius_km`, `network_type`, `avoid_popular_spots`, `prefer_quiet_at_night`, `popularity_radius_km`, `db_config_path`, `page`, `page_size`.
- **Returns**: List of spots with pagination metadata (`total`, `page`, `page_size`, `total_pages`) and a `resource_id` that identifies the cached non-pagination query parameters. Each item may also include popularity heuristic fields such as `static_popularity_risk_score`, `night_quiet_likelihood_score`, and `popularity_notes`.
- **Validation**: `page >= 1`, `page_size >= 1`, and `popularity_radius_km > 0`; invalid arguments return `CONFIGURATION_ERROR`.
Background area tools:
- **`submit_area_analysis`**: accepts a `request` with `bounds` and place-search preferences; returns a task snapshot and normalized request. `max_locations` is 1–100. Deployment paths and execution budgets remain server policy.
- **`get_area_analysis_task`**: accepts `task_id`; returns state, timestamps, failure details, `execution_pending`, and the successful `result_revision` when present. Polling a failed task succeeds as a status lookup; inspect `data.state` and `data.failure`.
- **`cancel_area_analysis_task`**: accepts `task_id`; records idempotent cancellation. A running provider call retains its execution slot until it finishes, and its late results are discarded.
- **`get_area_analysis_result`**: accepts `task_id`, `page`, and `page_size`; returns the existing `AnalysisAreaResult` pagination shape. It never silently recomputes missing data. Expired or replaced publications require resubmission.
Workflow: submit → poll until `succeeded` → fetch pages. Use `plan_stargazing_session` for a complete observing decision; these task tools are for raw place analysis. Existing synchronous `analysis_area` remains available.
The [AI recovery acceptance report](evaluations/reports/2026-10-27-task-recovery.md) records
ten read-only state/result questions completed by each of two clients, with auditable calls and
explicit limits. It does not qualify AI submission/cancellation workflows or real provider uptime.
The separate [active-workflow report](evaluations/reports/2026-10-27-task-actions.md) qualifies
submission, status-confirmed pagination, cancellation, and active-query reuse in three isolated
workflows per client. It uses real workers with a controlled provider; real provider uptime
remains outside this acceptance gate.
### 5. Error Handling
All tools return JSON-serializable data and use structured error handling:
- **Standard Error Codes**: `INVALID_COORDINATES`, `INVALID_TIMEZONE`, `INVALID_TIME_FORMAT`, `MISSING_API_KEY`, `API_AUTH_FAILURE`, `API_TIMEOUT`, `API_RATE_LIMIT`, `EXTERNAL_API_ERROR`, `NETWORK_ERROR`, `CONFIGURATION_ERROR`
- **Task Error Codes**: `TASK_NOT_FOUND`, `TASK_RUNTIME_CHANGED`, `TASK_CAPACITY_EXCEEDED`, `TASK_NOT_READY`, `TASK_UNAVAILABLE`, `TASK_RESULT_EXPIRED`; see [task recovery guidance](docs/task-lifecycle.md).
- **Weather Tools**: Include automatic retry logic for network failures (up to 3 attempts with exponential backoff)
- **Business Error Responses**: Structured MCPError-derived payloads with actionable messages for calling agents
- **Protocol Tests**: `tools/list`, `get_tool_catalog`, and SSE request-id behavior are covered by protocol-level tests
- **Validation**: Input parameters are validated before processing with clear error messages
## Examples
- **Background Area Analysis**: `python examples/background_area_demo.py`
- Submits, polls, and fetches a result page; accepts `--task-id` to resume polling or `--cancel --task-id ...` to cancel explicitly.
- **Nightly Planner**: `python examples/nightly_forecast_demo.py`
- Shows a curated list of planets and deep-sky objects visible tonight, accounting for moonlight.
- **Visible Planets**: `python examples/visible_planets_demo.py`
- Lists which planets are currently up.
- **Moon Info**: `python examples/moon_phase_demo.py`
- Prints a 30-day moon phase calendar.
- **Orchestration**: `python examples/code_execution_orchestration.py`
- Demonstrates a full workflow: Get time -> Get Celestial Pos -> Check Weather -> Find Spots.
- Shows how to handle the standardized response format programmatically.
- **Pagination**: `python examples/pagination_demo.py`
- Demonstrates fetching large result sets page by page using the `resource_id`.
- **Telescope Targets**: `python examples/telescope_targets_demo.py`
- Ranks deep-sky targets for a RedCat51-style optical configuration.
- **Shooting Plan**: `python examples/shooting_plan_demo.py`
- Produces a time-ordered single-night imaging plan.
## Project Structure
```
.
├── src/
│ ├── functions/ # Tool implementations grouped by domain
│ │ ├── celestial/ # Celestial calculations (pos, rise/set)
│ │ ├── metadata/ # Tool discovery surface (`get_tool_catalog`)
│ │ ├── planning/ # Single-night and multi-night composite planning
│ │ ├── telescope/ # Telescope target matching + shooting plan
│ │ ├── weather/ # Weather API integration
│ │ ├── places/ # Location and area analysis
│ │ └── time/ # Time utilities
│ ├── services/ # Transport-independent application services
│ │ └── planning.py # Single-night and multi-night planning orchestration
│ ├── schemas/ # Pydantic v2 data models
│ ├── analysis_cache.py # Caching logic for analysis results
│ ├── response.py # Standardized response formatting
│ ├── server_instance.py # FastMCP server instance (avoids circular imports)
│ ├── main.py # Entry point and tool registration
│ ├── celestial.py # Core astronomy logic (Astropy wrappers)
│ ├── placefinder.py # Place-finder bridge logic
│ └── qweather_interaction.py # Thin QWeather URL wrappers
├── tests/ # Unified test suite covering every registered tool
├── examples/ # Runnable usage examples, including both telescope tools
├── docs/ # Design docs and roadmap
├── Dockerfile # Multi-stage Docker build
├── supervisord.conf # Dual-service process manager config
└── pyproject.toml # Project configuration and dependencies
```
## Testing
Run the unified test suite:
```bash
uv run pytest -v tests/
```
Key tests include:
- `test_serialization.py`: Ensures all tools return valid JSON with the correct schema.
- `test_integration.py`: Mocks external APIs to verify the entire toolchain.
- `test_mcp_client.py`: Verifies `tools/list`, `tools/call`, and SSE request-id protocol behavior.
- `test_structured_errors.py`: Verifies business validation failures stay in the structured response envelope.
Cross-repository compatibility is checked by the reusable
[dependency-chain workflow](docs/dependency-chain-ci.md). It tests
`stargazing-core → stargazing-place-finder → mcp-stargazing` using baseline and
candidate wheels, and blocks changes that add downstream failures.
## Contributing
1. Follow the [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) best practices.
2. Ensure all new tools return standard JSON responses using `src.response.format_response`.
3. Add tests in `tests/` for any new functionality.
4. Follow the repository agent conventions in `AGENTS.md` for all MCP tool and agent-facing changes.
5. Refer to `docs/agent-contract.md` and `docs/ROADMAP.md` for the public agent direction and
implementation roadmap.
### Evidence-gated observing recommendations
Only candidates with the requested night's dated weather and a usable observation window
compete for the selected session and its alternatives. Unknown weather is not replaced by
an assumed score and is not classified as bad weather. Known forecasts without a usable
window are also excluded, with a distinct reason. If none qualify, the primary planner
returns `decision=null`, no alternatives, explicit uncertainties and next actions.
Known place, sky and generic target facts remain in `evidence` entries whose source is
`unranked_planning_reference`; `facts.status` is `weather_unknown` or `no_usable_window`.
These entries have no session score or comparison to the decision. Equipment configuration
is retained as evidence when supplied; reference targets are generic, not equipment-matched.
Compatibility planners retain reference candidates and notes, but leave recommended-place
and best-night summary fields null when no session qualifies. Their numeric weather fields
remain schema-compatible: absent weather has zero base score, contribution and weight as
placeholders, not a claim of bad weather. Place weights are not renormalized. Reference
candidate scores and positions must not be interpreted as an observing-session ranking.
Successful execution still means only that the request was processed.
### Deep-sky imaging quality gate
`observation_style=deep_sky` uses policy `deep-sky-v1`: each admitted forecast hour
requires effective cloud cover <=25% (the maximum of total/low/mid/high layers), known
precipitation probability <=0.20 and known wind <=20 km/h. A continuous run must last
at least two hours after clipping to astronomical darkness (Sun below -18 degrees).
Duplicate timestamps and gaps longer than one hour cannot manufacture a run. Unknown
rain or wind is excluded, not filled with a favorable value. No astronomical-dark interval
means no deep-sky window; civil darkness is never substituted.
These are versioned screening heuristics, not measured success probabilities or universal
hardware limits. Other observing styles retain the existing more permissive weather gate.
The public `night_window` remains civil darkness; only the deep-sky observation window is
clipped. Existing response schemas are unchanged. Weather evidence records `imaging_policy`
and `astronomical_bounds`; deep-sky window cloud/rain/wind fields report the worst admitted
hour, while quality is a relative score. This still does not guarantee seeing, transparency,
dew control, target altitude throughout the session, field access or a noise-free site.
Provider stage logs (`planning_stage`) record elapsed seconds, outcome and request identity;
request-local cache reads (`planning_cache_read`) are separate. Concurrent stage durations
must not be added as wall-clock latency. Timing uses a monotonic clock and never changes the
MCP response envelope or writes into the stdio protocol stream.
TDQS
Scored across 21 tools
Most tools are clearly separable (weather, celestial events, moon, planets, telescope targets), and the use/do-not-use guidance is unusually explicit. However, there are several overlapping decision paths: plan_stargazing_session vs get_best_stargazing_plan vs get_multiday_stargazing_plan, and analysis_area vs submit_area_analysis vs light_pollution_map. The two bbox-based plan tools are effectively legacy aliases of the preferred intent-based planner, which an agent must recognize from prose rather than the name.
The dominant pattern is verb_noun snake_case (get_weather_by_position, get_celestial_pos, submit_area_analysis, cancel_area_analysis_task, list_visible_planets). A few names break the pattern with noun-only or irregular forms (analysis_area, light_pollution_map, plan_stargazing_session), but overall the convention is readable and predictable.
21 tools is on the heavy side for a single domain, and a nontrivial share is infrastructure or legacy: a 4-tool background task lifecycle, two deprecated bbox planners, and get_tool_catalog/get_local_datetime_info utilities. The core astronomy and planning surface is well scoped, but the total feels over-expanded rather than every tool earning a distinct slot.
Coverage is broad: weather by name and coordinates, celestial positions/rise-set, moon, planets, nightly targets, equipment-matched targets, timed shooting plans, regional site search, light pollution, and multi-night planning. Minor gaps remain (e.g. satellite/comet/asteroid lookups, explicit seeing/transparency, direct multi-site weather comparison outside the planner), but nothing that creates an obvious dead end for observing workflows.