Skip to content

Derive catalog from mcp_url via the MCP head's /version (server half now shipped) #337

Description

@cboettig

Breaking out item 3 of #335 ("optionally make catalog itself optional, so a headless/MCP-only config is just {"mcp_url": "…"}"), because the server-side blocker is now gone.

What changed on the server

mcp-data-server#346 / PR #348 makes an MCP head advertise the client-reachable URL of the catalog it reads, as structured data on a public endpoint:

$ curl -s https://duckdb-mcp.carlboettiger.info/version
{"version":"main","git_sha":"",
 "stac_catalog_url":"https://minio.carlboettiger.info/public-data/stac/catalog.json",
 "public_base_url":"https://duckdb-mcp.carlboettiger.info"}

Properties that matter for this repo:

  • No MCP session, no auth token. /version is deliberately auth-exempt, so this works before mcp.connect() resolves and on token-gated heads.
  • Browser-reachable. Verified access-control-allow-origin: * on both heads today (duckdb-mcp.nrp-nautilus.io and duckdb-mcp.carlboettiger.info), so no proxy is needed.
  • Always public. stac_catalog_url is the client-facing URL even when the server itself reads an in-cluster address — that was the whole point of #346. Defaults to the server's own catalog URL, so heads on NRP prod already answer usefully.
  • Only public URLs are exposed there — the in-cluster address stays inside authenticated MCP responses.

Proposed change here

app/main.js already resolves mcpUrl at step 1c (app/main.js:64), before catalog.load(appConfig) at step 2+3, and DatasetCatalog.load reads appConfig.catalog at app/dataset-catalog.js:52. So the hook is small:

  1. When appConfig.catalog is absent, derive it: GET <mcpUrl without trailing /mcp>/versionstac_catalog_url. A headless/MCP-only config becomes just {"mcp_url": "…"}.
  2. Optionally, when the configured catalog fetch fails after DatasetCatalog.load() still fatal after retry exhaustion (and fetches the root even with collections: []) — blocks a working MCP-only degraded mode #335's retry exhaustion, try the derived URL before degrading to an empty catalog — a head configured against a live mirror knows a reachable catalog even when the app's hardcoded one is down. That is exactly the case that cost the manual hunt in mcp-data-server#346.
  3. Keep it non-fatal: if /version is unreachable or lacks the field (a head predating #346), fall through to DatasetCatalog.load() still fatal after retry exhaustion (and fetches the root even with collections: []) — blocks a working MCP-only degraded mode #335's degraded path. No new hard dependency on the boot critical path.

Ordering note: this composes with #335 but doesn't need it — 1 is independent, 2 slots into whatever #335 lands as the post-retry branch.

Scope caveat carried over from #335

A derived catalog is still not equivalent to the curated config: the app's collection list is what feeds list_datasets, get_schema, and the dataset paths in the assembled system prompt. Deriving the root URL doesn't change that — an MCP-only config still has collections: [] and still shifts agent behaviour onto the MCP STAC tools. Right for a smoke test or an outage, wrong for a benchmark meant to measure the production app.

Related: #335 (degrade instead of throwing; item 3 is this), #197 (source the catalog from MCP instead of walking STAC client-side), #287 (retry, shipped), mcp-data-server#346.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions