Skip to main content
Glama
jbwod

CASDA MCP Server

by jbwod
README.md
# CASDA MCP Server
[![MCP Badge](https://lobehub.com/badge/mcp/jbwod-casda-mcp)](https://lobehub.com/mcp/jbwod-casda-mcp)

`casda-mcp` is a conservative Model Context Protocol server for the
[CSIRO ASKAP Science Data Archive (CASDA)](https://research.csiro.au/casda/). It converts an
AI client's structured selections into explicit, auditable archive operations. The server does not
interpret unrestricted natural language or expose URL-fetching, shell, or filesystem browsing tools.
Advanced ADQL is optional and flag-gated (`CASDA_ENABLE_ADVANCED_ADQL`).

The supported workflow is:

1. Discover archive status, schemas, and VO holdings (VOSI, SIA/SCS/SSA, projects, events).
2. Search bounded CASDA ObsCore metadata or run validated advanced ADQL when enabled.
3. Inspect one product, ASKAP scheduling block, project, or collection.
4. Select explicit product identifiers.
5. Optionally submit authenticated SODA/UWS full-file, cutout, or spectrum jobs.
6. Check that request with a separate, single status call (`casda_get_data_job` / staging alias).
7. Optionally download archive-confirmed files into a restricted directory.
8. Create a reproducible JSON manifest with collection metadata.

Search and metadata inspection are enabled by default. Staging, downloads, and advanced ADQL are
disabled by default and require separate administrator configuration.

## Status and confirmed interfaces

The implementation uses these CASDA interfaces:

- TAP 1.0/ADQL (sync and async) with VOSI availability/capabilities and TAP_SCHEMA discovery;
- SIA 2, SIA 1 (+ surveys), SCS catalogue cone search, and SSA spectrum discovery;
- public observation events feed;
- Datalink 1.1 VOTables for authenticated SODA service and opaque product-token discovery;
- asynchronous SODA/UWS jobs for full-file staging, cutouts, spectrum generation, and one-shot status;
- archive result URLs and checksum sidecars for streamed downloads.

Public metadata paths were validated live on 18 July 2026. Optional `-m live` tests exercise
read-only discovery when `CASDA_RUN_LIVE_TESTS=true`. Authenticated staging, cutout, and downloads
are covered by mocked protocol tests and are never run by default live gates.

The dated [CASDA capability matrix](docs/casda-capability-matrix.md) maps the complete public and
authenticated protocol surface, implementation status, remaining upstream/DAP boundaries, and MCP
contract requirements.

## Requirements

- Python 3.10 or newer
- [`uv`](https://docs.astral.sh/uv/) for the documented locked setup
- Network access to the configured CASDA endpoints
- An [OPAL account](https://opal.atnf.csiro.au/) only for staging operations

## Installation

```bash
git clone <repository-url> casda-mcp
cd casda-mcp
uv sync --frozen --extra dev
```

Run the server over stdio:

```bash
uv run casda-mcp
```

Run the Streamable HTTP transport on loopback:

```bash
uv run casda-mcp --transport streamable-http --host 127.0.0.1 --port 8000
```

The MCP endpoint is `http://127.0.0.1:8000/mcp`. Non-sensitive probes:

- `http://127.0.0.1:8000/healthz` — process liveness
- `http://127.0.0.1:8000/readyz` — readiness using last-known archive availability (never blocks on a live CASDA call)

## MCP client configuration

For a stdio client, adjust the absolute project path:

```json
{
  "mcpServers": {
    "casda": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/casda-mcp",
        "run",
        "casda-mcp"
      ],
      "env": {
        "CASDA_ENABLE_STAGING": "false",
        "CASDA_ENABLE_DOWNLOADS": "false"
      }
    }
  }
}
```

Codex CLI can register the same stdio command:

```bash
codex mcp add casda \
  --env CASDA_ENABLE_STAGING=false \
  --env CASDA_ENABLE_DOWNLOADS=false \
  -- uv --directory /absolute/path/to/casda-mcp run casda-mcp
```

Equivalent Codex `config.toml`:

```toml
[mcp_servers.casda]
command = "uv"
args = ["--directory", "/absolute/path/to/casda-mcp", "run", "casda-mcp"]

[mcp_servers.casda.env]
CASDA_ENABLE_STAGING = "false"
CASDA_ENABLE_DOWNLOADS = "false"
```

ChatGPT desktop and IDE MCP settings accept either the same STDIO command or the Streamable HTTP
URL. Restart the client after adding the server. See the current
[ChatGPT MCP configuration guide](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).

## Configuration

Configuration is loaded from environment variables and validated at startup. Invalid state-changing
configuration fails fast.

| Variable | Default | Purpose and security effect |
| --- | --- | --- |
| `CASDA_BASE_URL` | `https://casda.csiro.au` | CASDA base host; HTTPS and no embedded credentials required. |
| `CASDA_TAP_URL` | CASDA TAP sync URL | Fixed metadata query endpoint. Tool callers cannot override it. |
| `CASDA_TAP_ASYNC_URL` | CASDA TAP async URL | Fixed async TAP/UWS endpoint for advanced ADQL jobs. |
| `CASDA_DATALINK_URL` | CASDA proxy Datalink URL | Establishes an allowed CASDA host; product Datalink URLs still come from TAP. |
| `CASDA_SODA_URL` | CASDA async SODA URL | Establishes the allowed staging host and documents the expected service. |
| `CASDA_LOGIN_URL` | CASDA proxy TAP availability URL | Safe credential verification endpoint. |
| `CASDA_SIA1_URL` | CASDA SIA 1 query URL | Legacy survey image discovery. |
| `CASDA_SIA1_SURVEYS_URL` | CASDA SIA 1 surveys URL | Survey inventory. |
| `CASDA_SIA2_URL` | CASDA SIA 2 query URL | Multidimensional image/cube discovery. |
| `CASDA_SCS_BASE_URL` | CASDA SCS base URL | Catalogue cone-search base (`/{short_name}` appended). |
| `CASDA_SSA_URL` | CASDA SSA query URL | Spectrum discovery. |
| `CASDA_EVENTS_URL` | CASDA observation events URL | Public lifecycle/event feed. |
| `CASDA_USERNAME` | unset | OPAL username. Required with `CASDA_PASSWORD` for staging. |
| `CASDA_PASSWORD` | unset | OPAL password. Stored as a secret value and never logged. |
| `CASDA_ENABLE_STAGING` | `false` | Enables archive-side full-file, Pawsey, cutout, and spectrum job creation when OPAL credentials are complete. Pawsey pull is distinct from WEB download staging. |
| `CASDA_ENABLE_DOWNLOADS` | `false` | Enables local file writes. |
| `CASDA_ENABLE_ADVANCED_ADQL` | `false` | Enables `casda_tap_query` / async TAP submit after SELECT-only validation. |
| `CASDA_ENABLE_DOI_RESOLVE` | `true` | Enables public read-only DOI resolve via DataCite/doi.org (no minting). |
| `CASDA_DOWNLOAD_DIR` | unset | Required absolute, dedicated containment directory when downloads are enabled; filesystem roots are rejected, including through symlinks. |
| `CASDA_ALLOW_OVERWRITE` | `false` | Allows atomic replacement of an existing destination. Keep false normally. |
| `CASDA_MAX_RESULTS` | `100` | Maximum bounded search window, up to a hard limit of 1000. |
| `CASDA_MAX_CONE_RADIUS_DEG` | `5` | Maximum cone radius in degrees. |
| `CASDA_MAX_STAGE_PRODUCTS` | `20` | Maximum deduplicated products in one staging request. |
| `CASDA_MAX_STAGE_BYTES` | `107374182400` | Maximum summed estimated staging size in bytes. |
| `CASDA_ALLOW_UNKNOWN_STAGE_SIZE` | `false` | If false, products without estimated sizes cannot be staged. |
| `CASDA_MAX_MANIFEST_PRODUCTS` | `100` | Maximum deduplicated products in one manifest. |
| `CASDA_MAX_DOWNLOAD_BYTES` | `53687091200` | Maximum archive-reported and streamed bytes for one download. |
| `CASDA_MAX_RESPONSE_BYTES` | `16777216` | Maximum decoded bytes buffered from one metadata or control response (validated from 1 KiB to 100 MiB). |
| `CASDA_REQUEST_TIMEOUT_SECONDS` | `30` | Metadata and control-request timeout. |
| `CASDA_DOWNLOAD_TIMEOUT_SECONDS` | `300` | Timeout used for a download response. |
| `CASDA_MAX_RETRIES` | `3` | Retries for safe reads only, with exponential backoff, jitter, and `Retry-After`. |
| `CASDA_CACHE_TTL_SECONDS` | `60` | Read-only metadata cache TTL; zero disables caching. |
| `CASDA_CACHE_MAX_ENTRIES` | `256` | Process-local cache bound; zero disables caching. |
| `CASDA_STATE_DB` | unset | Optional SQLite state file, forced to owner-only mode (`0600`) on POSIX. Default state is process-local memory. |

Copy `.env.example` to `.env` for local development; it is loaded automatically and ignored by Git.
Do not commit a populated `.env` file. Production deployments should inject secrets instead.

### Enabling staging

```bash
export CASDA_USERNAME='researcher@example.edu.au'
export CASDA_PASSWORD='use-a-secret-provider-in-production'
export CASDA_ENABLE_STAGING=true
uv run casda-mcp
```

Use the process environment, an OS credential provider that injects environment variables, or a
deployment secret store. Do not put credentials in command-line arguments. The implementation uses
the OPAL HTTP Basic authentication behavior confirmed by Astroquery's CASDA client.

### Enabling downloads

```bash
export CASDA_ENABLE_DOWNLOADS=true
export CASDA_DOWNLOAD_DIR=/srv/casda-downloads
export CASDA_MAX_DOWNLOAD_BYTES=10737418240
uv run casda-mcp
```

The directory must be absolute, dedicated to CASDA downloads, and cannot resolve to a filesystem
root. On POSIX it must be owned by the server account and not group- or world-writable; equivalent
ACL isolation is an operator responsibility on Windows. Its canonical location and inode identity
are recorded once writes begin and rechecked before path mutations; non-sticky writable ancestors
are rejected on POSIX. Caller destinations are checked for containment, symlink traversal,
reserved internal names, and portable filename safety. Each target is reserved in a private,
hash-named `.casda-mcp/locks` directory before any archive request. Files are streamed as
identity-encoded raw bytes through a descriptor bound to the original temporary inode, then published atomically
after length and optional checksum verification. With overwrite disabled, the destination filesystem
must support same-directory hard links for atomic no-clobber publication. Incomplete files and normal
reservations are removed after failure.

## Tools

Successful tool responses carry operation-specific data and `provenance`. Failures use protocol-level
`ToolError` / `isError` rather than a successful envelope with an `error` field. Provenance contains
the server version, archive, timestamps, deterministic query identifier, sanitised endpoint,
parameters, result count, cache status, and correlation identifier. Credentials and URL query strings
are not included.

Discovery and job tools beyond the core ObsCore path include:

| Group | Tools |
| --- | --- |
| Archive / TAP_SCHEMA | `casda_get_archive_status`, `casda_list_capabilities`, `casda_list_schemas`, `casda_list_tables`, `casda_describe_table`, `casda_list_foreign_keys` |
| VO search | `casda_search_images`, `casda_list_image_surveys`, `casda_search_survey_images`, `casda_list_catalogues`, `casda_search_catalogue`, `casda_search_spectra` |
| Projects / events / DOI / DAP | `casda_search_projects`, `casda_get_project`, `casda_get_collection`, `casda_resolve_collection_doi`, `casda_get_dap_navigation`, `casda_list_events` |
| Advanced ADQL | `casda_build_adql`, `casda_validate_adql`, `casda_tap_query`, `casda_submit_tap_query`, `casda_get_tap_job`, `casda_get_tap_results`, `casda_abort_tap_job`, `casda_delete_tap_job` |
| DataLink / jobs | `casda_get_auth_status`, `casda_get_datalink`, `casda_stage_pawsey`, `casda_create_cutout`, `casda_create_spectrum`, `casda_get_data_job`, `casda_get_data_job_results`, `casda_abort_data_job`, `casda_delete_data_job`, `casda_download_job_results`, `casda_verify_file` |

The full name inventory is asserted in `tests/test_contract.py`.

### `casda_search_products`

Read-only bounded product discovery. Supported filters are exact source/target name, ICRS position
and radius in degrees, OPAL project code, ASKAP SBID, overlapping ISO 8601 observation dates,
overlapping frequencies in hertz, exact collection, facility/instrument names, and these allowlisted
product types:

`image`, `cube`, `visibility`, `spectrum`, `catalogue`, `weight`, `moment_map`, `cubelet`,
`evaluation`, `scan`.

It supports bounded pagination (page or opaque `cursor`) and allowlisted sorting. It does not resolve
astronomical names, stage, or download. Caller-supplied ADQL belongs on the flag-gated advanced
tools, not this helper.

```json
{
  "ra_deg": 333.8,
  "dec_deg": -46.0,
  "radius_deg": 0.05,
  "project_code": "AS102",
  "product_types": ["cube", "weight", "moment_map"],
  "released_only": true,
  "page": 1,
  "page_size": 20
}
```

### `casda_get_product`

Read-only complete supported ObsCore metadata for one exact `obs_publisher_did`.

```json
{"product_id": "cube-1170"}
```

The response retains raw archive identifiers, nulls, units in field names, spatial footprint,
spectral coverage converted to hertz, estimated byte size, SBID when encoded as `ASKAP-<sbid>`,
project code where the collection maps to `casda.project.short_name`, release state, and quality.

### `casda_get_observation`

Read-only ASKAP observation lookup with related projects and a bounded product list.

```json
{"scheduling_block_id": 2338}
```

### `casda_stage_products`

Creates and starts one archive-side asynchronous SODA/UWS request. It requires staging to be enabled
and OPAL credentials to be present. Empty requests are rejected, identifiers are normalised and
deduplicated, count and total estimated size are bounded, and missing sizes are rejected by default.

```json
{
  "product_ids": ["cube-1170", "cube-1171"],
  "idempotency_key": "wallaby-run-2026-07-12",
  "allow_duplicate": false
}
```

The output includes the archive request ID, effective idempotency key, confirmed phase, submission
time, and per-product state. A reused idempotency key with different products is an error. An active
request for the same product set is returned rather than duplicated unless the caller uses a new key
and explicitly sets `allow_duplicate`.

The non-idempotent archive creation and start requests are never automatically retried.

### `casda_stage_pawsey`

Creates and starts one Pawsey pull staging job via the DataLink `pawsey_async_service` descriptor.
Requires the same `CASDA_ENABLE_STAGING` flag and OPAL credentials as WEB full-file staging, but
results are Pawsey-network restricted. The response includes `human_gate_warnings`: a Pawsey HPC
account is required, and licence/account confirmation must be completed by a human in the DAP —
this server never auto-accepts terms. Reuse `casda_get_data_job` / download tools for lifecycle.

### `casda_resolve_collection_doi`

Read-only public citation resolve. Provide exactly one of `doi`, `collection`, or `project_code`.
DOIs are fetched from DataCite JSON with a doi.org CSL-JSON fallback on allowlisted hosts only.
Collection/project lookups never invent a DOI; when archive metadata has none, the tool returns
`found=false` plus a DAP search navigation URL. CSIRO DAP DOIs typically use prefix `10.25919`
(`is_csiro_dap`). Never mints DOIs.

### `casda_get_dap_navigation`

Constructs documented DAP deep links (Observation Search, Skymap, search hints) without scraping
HTML. Privileged `action` values such as `accept_licence`, `mint_doi`, or `launch_carta` return
structured `unsupported_actions` only — never HTTP mutation.

### `casda_get_staging_status`

Performs exactly one uncached UWS status read:

```json
{"request_id": "archive-job-id"}
```

It returns the overall archive phase, expiry, archive failure reason, per-product state, and whether
every product has a confirmed matching result URL. Active phases advise the caller to make another
tool call later; no background polling is claimed or scheduled.

### `casda_download_product`

Downloads one product only after a completed status read recorded a matching archive result URL:

```json
{
  "product_id": "cube-1170",
  "destination": "wallaby/cube-1170.fits",
  "verify_checksum": true
}
```

The result includes the confirmed local path, actual bytes, Content-Length verification, checksum
result, whether a Range retry resumed within this call, staging request ID, and provenance. A local
path is never returned before the final file exists. The server does not expose a deletion tool.

### `casda_create_cutout` / `casda_create_spectrum`

Authenticated SODA jobs (require staging enabled + OPAL). Supply `CIRCLE` / `POLYGON` / `BAND` /
`CHANNEL` / `POL` / `COORD` as documented by CASDA DataLink descriptors. Monitor with
`casda_get_data_job`, then download via `casda_download_product` or `casda_download_job_results`.

### `casda_create_manifest`

Creates and retains a schema-versioned JSON manifest in server state:

```json
{
  "product_ids": ["cube-1170", "catalogue-10"],
  "source_name": "WALLABY J2214-4600",
  "workflow_name": "spectral-line-analysis",
  "include_download_urls": false
}
```

The manifest includes a deterministic SHA-256 identifier, creation time, full typed product
metadata, filenames, estimated file sizes, available checksums, SBIDs, project codes, types, spatial
and spectral metadata, access state, collection metadata (`obs_collection`, `facility_name`, release
span), known originating search criteria, provenance, and server version. Archive artifact URLs are
never persisted in manifests because opaque paths may be short-lived bearer credentials even when
they contain no query string. Use `casda_resolve_collection_doi` for public citation metadata.

## Resources

The server exposes read-only resources:

- `casda://products/{product_id}`
- `casda://observations/{scheduling_block_id}`
- `casda://staging/{request_id}`
- `casda://events/{event_id}`
- `casda://manifests/{manifest_id}`
- `casda://archive/status`
- `casda://archive/capabilities`
- `casda://dap/navigation`
- `casda://server/status`
- `casda://skills` (JSON index of packaged agent skills)
- `casda://skills/{skill_name}` (raw `SKILL.md` markdown)

Resources do not expose credentials, raw local state files, unrestricted filesystem content, or URL
query strings. The staging resource performs one current status read, like the tool.

## Prompts

Registered MCP prompts guide safe workflows:

| Prompt | Purpose |
| --- | --- |
| `find-and-inspect-products` | Bounded search, then inspect selected products or ASKAP observations |
| `query-tables` | `list_schemas` → `list_tables` → `describe_table` |
| `run-adql` | Validate then `tap_query` / submit (requires `CASDA_ENABLE_ADVANCED_ADQL`) |
| `query-catalogue` | `casda_list_catalogues` / `casda_search_catalogue`, with ObsCore catalogue fallback |
| `stage-and-download` | Stage explicit IDs (WEB or Pawsey), one-shot status checks, guarded download |
| `make-cutout` | `casda_create_cutout` → `casda_get_data_job` → download |
| `build-reproducible-selection` | Create a manifest without persisting artifact URLs |
| `monitor-releases` | Release fields via search/get_product; `casda_list_events` when useful |
| `dap-navigate` | Safe DAP deep links; structured refusal of privileged automation |

## Agent skills

Canonical skill files live under `src/casda_mcp/skills/` and ship in the package:

- `casda-safe-archive-access`
- `casda-find-and-inspect`
- `casda-stage-and-download`
- `casda-reproducible-manifest`

The same files are mirrored under `.cursor/skills/` for Cursor project discovery. MCP clients can
read them through `casda://skills` and `casda://skills/{skill_name}`.

## Example workflows

### Search and inspect

1. Call `casda_search_products` with explicit bounded criteria.
2. Present the candidates and stable product identifiers to the researcher.
3. Call `casda_get_product` only for selected identifiers.
4. Explain `access_state` and `authorisation_state` without claiming access that CASDA has not
   confirmed.

### Search by WALLABY source

1. Resolve the source name to coordinates in the AI client or a separately trusted resolver.
2. Call `casda_search_products` with the explicit coordinates, radius, `project_code: "AS102"`, and
   required product types.
3. Inspect candidates, including SBID, collection, footprint, spectral range, and file size.
4. Call `casda_create_manifest` for the explicit selection.

The generic model can represent WALLABY identifiers present in `target_name`, project code, SBID,
footprint, cube/weight/catalogue/spectrum/moment-map subtypes, channels, spatial metadata, size, and
access state. No `wallaby_find_source_products` tool is included because a stable, complete source
selection rule has not been established. WALLABY-specific rules should remain a future adapter.

### Stage and download

1. Inspect the selected product and size.
2. Call `casda_stage_products` for WEB download staging, or `casda_stage_pawsey` for Pawsey pull
   (read `human_gate_warnings`; complete licence/HPC confirmation as a human in the DAP).
3. Later, call `casda_get_staging_status` or `casda_get_data_job`; do not assume automatic polling.
4. Only after products are ready, call `casda_download_product` (Pawsey results are network-restricted).
5. Check returned length and checksum fields.

### Cutout

1. Enable staging and configure OPAL credentials.
2. Call `casda_create_cutout` with an explicit `product_id` and SODA constraints (for example `circle`).
3. Poll with `casda_get_data_job`.
4. Download with `casda_download_product` or `casda_download_job_results` when downloads are enabled.

### Advanced ADQL

1. Set `CASDA_ENABLE_ADVANCED_ADQL=true`.
2. Prefer `casda_build_adql` or carefully drafted SELECT-only ADQL.
3. Call `casda_validate_adql`, then `casda_tap_query` or `casda_submit_tap_query`.
4. Prefer `casda_search_products` / VO discovery tools when allowlisted filters suffice.

### Reproducible workflow manifest

1. Search with explicit criteria.
2. Select identifiers.
3. Inspect full metadata.
4. Call `casda_create_manifest`.
5. Read the result later through `casda://manifests/{manifest_id}` when persistent state is enabled.

## Security model

- Tool input is untrusted and validated before query construction.
- TAP table names, selected columns, product-type clauses, sort fields, and operators are hard-coded
  allowlists.
- Text wildcards and control characters are rejected; identifiers use restrictive patterns.
- Cone, result, page, staging, manifest, decoded archive response, and download sizes are bounded.
- Only configured HTTPS CASDA hosts and current CASDA-controlled Pawsey download hosts are allowed.
  Redirect destinations are revalidated before they are followed.
- Safe metadata reads may retry; staging creation/start never automatically retry.
- OPAL credentials use environment/secret injection and are excluded from logs, provenance, and
  exceptions.
- Structured logs go to stderr so stdio JSON-RPC is not corrupted.
- Cache keys include the complete generated query and bound; authentication failures are not cached.
- Staging status is never cached.
- Streamable HTTP binds to loopback by default and has no built-in client authentication. Put a
  production remote deployment behind TLS and an authenticating reverse proxy or MCP authorization
  layer. Do not expose it directly when staging, credentials, or downloads are enabled.
- Principal isolation is process-scoped: credentials, authorization results, job state, ready URLs,
  caches, and manifests are not multiplexed safely across remote users in one process. For remote
  multi-user deployments, run **one process per principal** (or an equivalent front end that never
  shares a process across principals).
- `CASDA_STATE_DB` may contain short-lived signed URLs needed to resume status/download workflows.
  The server rejects symlink/non-file targets and forces owner-only file permissions on POSIX;
  deployments should additionally use an owner-controlled directory and encrypted storage.
  In-memory state is the default.

See [SECURITY.md](SECURITY.md) for the threat model and reporting guidance.

## Architecture

```text
MCP client
  -> typed FastMCP tools/resources/prompts
  -> CasdaService (validation, limits, idempotency, provenance)
  -> QueryBuilder / adql / vosi / cursor / parsers / TTL cache / StateStore
  -> CasdaClient (pooled HTTP, retries, host validation, OPAL auth)
  -> CASDA TAP | VOSI | SIA/SCS/SSA | events | Datalink | SODA/UWS | staged file endpoint
```

The modules are deliberately separated so CASDA protocol behavior does not depend on a particular AI
client. See [docs/architecture.md](docs/architecture.md) for component and sequence details.
See [docs/casda-capability-matrix.md](docs/casda-capability-matrix.md) for protocol coverage and the
implementation roadmap.

## Testing and validation

Run the default offline suite:

```bash
uv run pytest -m "not live" --cov=casda_mcp --cov-report=term-missing
uv run ruff format --check .
uv run ruff check .
uv run mypy src
uv build
```

The tests cover validation, coordinates, cone limits, dates, frequencies, identifiers, safe ADQL,
pagination, CSV/VOTable/UWS parsing, redaction, caching, error mapping, idempotency, partial staging,
path traversal, overwrite prevention, streamed byte limits, checksum mismatch cleanup, Range resume,
manifest determinism, MCP schemas, resources, health, and mocked end-to-end HTTP behavior.

Optional live tests are read-only discovery checks and disabled by default:

```bash
CASDA_RUN_LIVE_TESTS=true uv run pytest -m live -v
```

They cover VOSI availability/capabilities, a small TAP sync query, schema listing, SIA 2 cone, SIA 1
surveys, catalogue inventory, and the events feed. They never stage, download, or create cutouts.
CI does not require credentials.

## Container

Build and run the default read-only HTTP server:

```bash
docker build -t casda-mcp .
docker run --rm -p 127.0.0.1:8000:8000 casda-mcp
```

The image runs as a non-root user and checks `/healthz`. Mount a dedicated directory and inject
secrets only when explicitly enabling downloads or staging.

## Troubleshooting

- **No products:** remove filters deliberately, check the exact target/collection name, and keep the
  radius explicit. The server will not silently broaden the request.
- **`ARCHIVE_QUERY_ERROR`:** CASDA rejected the generated bounded query. Record the correlation and
  query IDs; no stack trace or credentials are exposed to the client.
- **`AUTHENTICATION_REQUIRED` / `AUTHENTICATION_FAILED`:** configure both OPAL variables and verify
  the account at the OPAL site. Metadata search itself does not require login.
- **`STAGING_DISABLED` / `DOWNLOADS_DISABLED`:** these are safe defaults, not archive failures.
- **`STAGING_REQUEST_NOT_FOUND`:** in-memory state was lost after restart or the ID came from another
  instance. Configure `CASDA_STATE_DB` before submission when restart persistence is required.
- **`PRODUCT_NOT_READY`:** run a current status check for the original request. The server will not
  infer readiness from elapsed time.
- **`UNSAFE_ARCHIVE_URL`:** CASDA returned a host outside the configured allowlist. Do not bypass this
  check without verifying a documented archive migration.
- **Repeated stale metadata:** reduce/disable the short cache or restart; staging status bypasses it.
- **HTTP works but remote access should not:** the default bind is loopback. Remote exposure requires
  an explicit host plus a secure front end.

## Known limitations

- Authenticated staging, cutout, spectrum, and file download behavior is protocol-tested with mocks;
  default live tests never exercise those paths.
- ASKAP SBID product relationships use the confirmed ObsCore `obs_id = 'ASKAP-<sbid>'` convention.
- Project codes are joined where `ivoa.obscore.obs_collection` matches
  `casda.project.short_name`; CASDA does not expose a direct generic project foreign key in ObsCore.
- CASDA's current ADQL service does not support `CURRENT_TIMESTAMP`; public-only search retrieves the
  configured bounded window and removes future release dates locally.
- Advanced ADQL remains SELECT-only, length/row bounded, and disabled until
  `CASDA_ENABLE_ADVANCED_ADQL=true`.
- UWS reports an overall job phase. A product is marked individually ready only when a completed job
  returns its unique product result identifier. A globally unambiguous filename fallback is retained
  for historical jobs; ambiguous results remain `UNKNOWN`.
- Resumption is attempted within one download call only when CASDA supplies a strong ETag or an
  RFC-strong Last-Modified validator; otherwise a retry restarts from byte zero. Final failure removes the
  temporary file, so resumption does not persist across separate calls. An abrupt process or host
  termination can leave a hashed lock in `.casda-mcp/locks` that an operator must inspect and remove
  before retrying that exact destination.
- Source-name resolution is outside this server. DOI minting, licence acceptance, and other
  privileged DAP workflows remain DAP-boundary; use `casda_resolve_collection_doi` and
  `casda_get_dap_navigation` for safe read-only helpers.
- Beam identifiers may be retained in filenames or target metadata, but CASDA ObsCore does not expose
  a generic structured neighbouring-beam relationship used by this implementation.

## References

- [CASDA user guide](https://research.csiro.au/casda/casda-user-guide/)
- [CASDA capability matrix](docs/casda-capability-matrix.md)
- [Astroquery CASDA module](https://astroquery.readthedocs.io/en/latest/casda/casda.html)
- [CASDA VO Tools](https://github.com/csiro-rds/casda_vo_tools)
- [Model Context Protocol server guide](https://modelcontextprotocol.io/docs/develop/build-server)

TDQS

A3.6/5.0

Scored across 45 tools

Disambiguation4/5

Most tools have clearly distinct purposes. The only notable overlap is that casda_get_staging_status is explicitly noted as an alias of casda_get_data_job for full-file staging jobs, which could cause minor confusion. Other search, retrieval, and management tools are well-separated.

Naming Consistency5/5

All tool names follow a consistent 'casda_<verb>_<noun>' pattern in snake_case, with verbs like get, search, list, create, abort, delete, etc. There is no mixing of conventions, and the naming is predictable.

Tool Count3/5

With 45 tools, the surface is large but arguably justified given the breadth of CASDA operations (search, staging, downloads, TAP, data jobs). However, it differs from the typical 3-15 tool ideal and could be streamlined by consolidating some functions.

Completeness4/5

The tool set covers core workflows: metadata search, product retrieval, staging, downloading, and job management. Minor gaps exist, such as no tool to list all active data jobs or to update staged requests, but these are not critical for most use cases.

Maintenance

ActivityStale
ResponsivenessNo issues