uxarray-mcp 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/PKG-INFO +69 -8
  2. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/README.md +68 -7
  3. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/pyproject.toml +5 -1
  4. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/__init__.py +1 -1
  5. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/cli.py +46 -12
  6. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/mesh.py +8 -3
  7. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/vector_calc.py +84 -0
  8. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/registry.py +4 -4
  9. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/remote/agent.py +34 -2
  10. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/remote/compute_functions.py +448 -16
  11. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/state.py +0 -9
  12. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/advanced.py +170 -2
  13. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/capabilities.py +151 -42
  14. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/execution_control.py +35 -6
  15. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/frontdoor.py +41 -0
  16. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/inspection.py +57 -16
  17. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/vector_calc.py +8 -2
  18. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/LICENSE +0 -0
  19. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/__main__.py +0 -0
  20. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/app.py +0 -0
  21. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/__init__.py +0 -0
  22. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/area.py +0 -0
  23. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/plotting.py +0 -0
  24. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/variable.py +0 -0
  25. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/domain/zonal.py +0 -0
  26. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/provenance.py +0 -0
  27. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/py.typed +0 -0
  28. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/remote/__init__.py +0 -0
  29. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/remote/config.py +0 -0
  30. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/remote/health.py +0 -0
  31. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/__init__.py +0 -0
  32. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/catalog.py +0 -0
  33. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/orchestration.py +0 -0
  34. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/plotting.py +0 -0
  35. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/remote_tools.py +0 -0
  36. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/scientific_agent.py +0 -0
  37. {uxarray_mcp-0.1.2 → uxarray_mcp-0.1.3}/src/uxarray_mcp/tools/stateful.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uxarray-mcp
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: MCP server for analyzing unstructured meshes with UXarray
5
5
  Keywords: uxarray,mcp,unstructured grids,scientific computing,globus compute
6
6
  Author: Rajeev Jain, Dayan Abdulla
@@ -299,6 +299,7 @@ uv tool install --python 3.12 uxarray-mcp
299
299
  # Or from a fresh clone (developer path)
300
300
  git clone https://github.com/UXARRAY/uxarray-mcp-server.git
301
301
  cd uxarray-mcp-server && uv sync --python 3.12
302
+ # or: bash SETUP.sh (does the sync + runs the local test suite in one step)
302
303
  ```
303
304
 
304
305
  > **Why `--python 3.12`?** The server uses Globus Compute to submit work to
@@ -350,8 +351,9 @@ client's MCP docs.
350
351
  uxarray-mcp doctor
351
352
  ```
352
353
 
353
- Should print `local execution: ok` and (if no endpoints configured) skip the
354
- remote checks.
354
+ Prints a JSON diagnostic report. With no endpoints configured it reports a
355
+ passing local setup and skips the remote checks; the process exits `0` when
356
+ `passed` is true.
355
357
 
356
358
  ### Step 5 — Ask the AI to do something
357
359
 
@@ -361,6 +363,19 @@ In your client, try:
361
363
 
362
364
  That's it for local use.
363
365
 
366
+ **A few more things to try:**
367
+
368
+ - `Use run_analysis with operation="inspect_mesh" and grid_path="healpix:4"` —
369
+ no sample file needed; HEALPix meshes are generated on the fly.
370
+ - `Run a complete scientific analysis on healpix:4` — the autonomous
371
+ Analyze → Plan → Execute → Verify agent (see
372
+ [docs/scientific-agent.md](docs/scientific-agent.md)).
373
+ - `Create a session called baseline-analysis, register <grid> and <data> in
374
+ it, then run the workflow for <variable>` — persisted, resumable
375
+ multi-step runs (see [docs/workflows.md](docs/workflows.md)).
376
+ - `Diagnose my configured endpoint status` — once you've added an endpoint
377
+ below, this is the fastest way to check it's healthy.
378
+
364
379
  ---
365
380
 
366
381
  ## Going beyond your laptop
@@ -380,21 +395,49 @@ Both paths assume you've finished local install above.
380
395
 
381
396
  ## What the MCP exposes
382
397
 
383
- Intent-shaped tools, not raw UXarray bindings:
398
+ Intent-shaped tools, not raw UXarray bindings — all local by default:
384
399
 
385
400
  - `get_capabilities` — what can I do with this mesh?
386
401
  - `analyze_dataset` — deterministic first-look: inspect, validate, area, zonal mean, plots.
387
402
  - `run_analysis` — one operation at a time (gradient, curl, subset, remap, …).
388
403
  - `plot_dataset` — mesh, geographic, variable, or zonal-mean plots.
389
- - `diagnose_endpoint`, `probe_path_access` — endpoint health + file readability.
390
404
  - `run_workflow`, `resume_workflow`, `get_status`, `get_result`, `manage_session` —
391
405
  persisted sessions and multi-step workflows.
392
406
 
393
- Tools that can run remotely take `use_remote: bool` and optional `endpoint: str`.
394
- The dispatcher falls back to local if the endpoint is unhealthy.
395
-
396
407
  Full schema: [docs/tools.md](docs/tools.md).
397
408
 
409
+ **Once you've configured an HPC endpoint** (optional — see
410
+ [Going beyond your laptop](#going-beyond-your-laptop) below): most tools above
411
+ also take `use_remote: bool` and `endpoint: str`, falling back to local if the
412
+ endpoint is unhealthy. Two more tools exist purely for that case:
413
+ `diagnose_endpoint` and `probe_path_access` (endpoint health + file
414
+ readability). Ignore all of this until you actually have an endpoint to point
415
+ at.
416
+
417
+ ---
418
+
419
+ ## Transparency & correctness safeguards
420
+
421
+ Because agent-driven analysis needs to be *trustworthy*, every result is
422
+ auditable and the server actively flags common scientific pitfalls:
423
+
424
+ - **Provenance on everything.** Each result carries a `_provenance` block:
425
+ the tool that ran, timestamp, input arguments, `execution_venue`
426
+ (`local` or `hpc:<endpoint>`), and the UXarray/Python versions used.
427
+ - **Derivative unit convention is never hidden.** `gradient`, `curl`, and
428
+ `divergence` echo `scale_by_radius` in both the result and provenance, so a
429
+ unit-sphere result can never be mistaken for a physical (per-metre) one.
430
+ - **Vector-calculus sanity guard.** `curl`/`divergence` warn (without blocking)
431
+ when the two inputs are the same field, or when neither carries a
432
+ velocity/flux-like `units` attribute — the classic "vorticity from two random
433
+ scalars" mistake now surfaces a warning in `_provenance.warnings`.
434
+ - **Local/remote version drift is surfaced.** Remote results record the
435
+ worker's *actual* UXarray version (`remote_uxarray_version`) and emit a
436
+ warning when it differs from the local version, so silent numerical
437
+ differences between venues can't slip through.
438
+ - **Validation gating.** `analyze_dataset` validates a dataset (NaN/Inf/fill
439
+ checks) before computing statistics like the zonal mean.
440
+
398
441
  ---
399
442
 
400
443
  ## CLI reference
@@ -410,6 +453,24 @@ Full schema: [docs/tools.md](docs/tools.md).
410
453
 
411
454
  ---
412
455
 
456
+ ## Upgrading
457
+
458
+ ```bash
459
+ uv tool upgrade --python 3.12 uxarray-mcp # or your original install method
460
+ ```
461
+
462
+ > **⚠️ Restart your AI client after upgrading.** MCP servers are launched once
463
+ > when your client (Claude Desktop, Claude Code, Cursor, …) starts and are **not
464
+ > hot-reloaded**. After upgrading the package, **fully quit and reopen your AI
465
+ > client** so it relaunches `uxarray-mcp serve` with the new code. Until you do,
466
+ > the running server keeps executing the *old* version — new tools and fixes
467
+ > won't appear, and you may see confusing errors (for example, a `use_remote`
468
+ > call on an HPC-only path failing with "file not found" because the old,
469
+ > local-only tool is still loaded). If in doubt, run `uxarray-mcp doctor` and
470
+ > check the reported version.
471
+
472
+ ---
473
+
413
474
  ## Risks (read before relying on output)
414
475
 
415
476
  AI agents can misread prompts, pick the wrong file, get units wrong (e.g.,
@@ -63,6 +63,7 @@ uv tool install --python 3.12 uxarray-mcp
63
63
  # Or from a fresh clone (developer path)
64
64
  git clone https://github.com/UXARRAY/uxarray-mcp-server.git
65
65
  cd uxarray-mcp-server && uv sync --python 3.12
66
+ # or: bash SETUP.sh (does the sync + runs the local test suite in one step)
66
67
  ```
67
68
 
68
69
  > **Why `--python 3.12`?** The server uses Globus Compute to submit work to
@@ -114,8 +115,9 @@ client's MCP docs.
114
115
  uxarray-mcp doctor
115
116
  ```
116
117
 
117
- Should print `local execution: ok` and (if no endpoints configured) skip the
118
- remote checks.
118
+ Prints a JSON diagnostic report. With no endpoints configured it reports a
119
+ passing local setup and skips the remote checks; the process exits `0` when
120
+ `passed` is true.
119
121
 
120
122
  ### Step 5 — Ask the AI to do something
121
123
 
@@ -125,6 +127,19 @@ In your client, try:
125
127
 
126
128
  That's it for local use.
127
129
 
130
+ **A few more things to try:**
131
+
132
+ - `Use run_analysis with operation="inspect_mesh" and grid_path="healpix:4"` —
133
+ no sample file needed; HEALPix meshes are generated on the fly.
134
+ - `Run a complete scientific analysis on healpix:4` — the autonomous
135
+ Analyze → Plan → Execute → Verify agent (see
136
+ [docs/scientific-agent.md](docs/scientific-agent.md)).
137
+ - `Create a session called baseline-analysis, register <grid> and <data> in
138
+ it, then run the workflow for <variable>` — persisted, resumable
139
+ multi-step runs (see [docs/workflows.md](docs/workflows.md)).
140
+ - `Diagnose my configured endpoint status` — once you've added an endpoint
141
+ below, this is the fastest way to check it's healthy.
142
+
128
143
  ---
129
144
 
130
145
  ## Going beyond your laptop
@@ -144,21 +159,49 @@ Both paths assume you've finished local install above.
144
159
 
145
160
  ## What the MCP exposes
146
161
 
147
- Intent-shaped tools, not raw UXarray bindings:
162
+ Intent-shaped tools, not raw UXarray bindings — all local by default:
148
163
 
149
164
  - `get_capabilities` — what can I do with this mesh?
150
165
  - `analyze_dataset` — deterministic first-look: inspect, validate, area, zonal mean, plots.
151
166
  - `run_analysis` — one operation at a time (gradient, curl, subset, remap, …).
152
167
  - `plot_dataset` — mesh, geographic, variable, or zonal-mean plots.
153
- - `diagnose_endpoint`, `probe_path_access` — endpoint health + file readability.
154
168
  - `run_workflow`, `resume_workflow`, `get_status`, `get_result`, `manage_session` —
155
169
  persisted sessions and multi-step workflows.
156
170
 
157
- Tools that can run remotely take `use_remote: bool` and optional `endpoint: str`.
158
- The dispatcher falls back to local if the endpoint is unhealthy.
159
-
160
171
  Full schema: [docs/tools.md](docs/tools.md).
161
172
 
173
+ **Once you've configured an HPC endpoint** (optional — see
174
+ [Going beyond your laptop](#going-beyond-your-laptop) below): most tools above
175
+ also take `use_remote: bool` and `endpoint: str`, falling back to local if the
176
+ endpoint is unhealthy. Two more tools exist purely for that case:
177
+ `diagnose_endpoint` and `probe_path_access` (endpoint health + file
178
+ readability). Ignore all of this until you actually have an endpoint to point
179
+ at.
180
+
181
+ ---
182
+
183
+ ## Transparency & correctness safeguards
184
+
185
+ Because agent-driven analysis needs to be *trustworthy*, every result is
186
+ auditable and the server actively flags common scientific pitfalls:
187
+
188
+ - **Provenance on everything.** Each result carries a `_provenance` block:
189
+ the tool that ran, timestamp, input arguments, `execution_venue`
190
+ (`local` or `hpc:<endpoint>`), and the UXarray/Python versions used.
191
+ - **Derivative unit convention is never hidden.** `gradient`, `curl`, and
192
+ `divergence` echo `scale_by_radius` in both the result and provenance, so a
193
+ unit-sphere result can never be mistaken for a physical (per-metre) one.
194
+ - **Vector-calculus sanity guard.** `curl`/`divergence` warn (without blocking)
195
+ when the two inputs are the same field, or when neither carries a
196
+ velocity/flux-like `units` attribute — the classic "vorticity from two random
197
+ scalars" mistake now surfaces a warning in `_provenance.warnings`.
198
+ - **Local/remote version drift is surfaced.** Remote results record the
199
+ worker's *actual* UXarray version (`remote_uxarray_version`) and emit a
200
+ warning when it differs from the local version, so silent numerical
201
+ differences between venues can't slip through.
202
+ - **Validation gating.** `analyze_dataset` validates a dataset (NaN/Inf/fill
203
+ checks) before computing statistics like the zonal mean.
204
+
162
205
  ---
163
206
 
164
207
  ## CLI reference
@@ -174,6 +217,24 @@ Full schema: [docs/tools.md](docs/tools.md).
174
217
 
175
218
  ---
176
219
 
220
+ ## Upgrading
221
+
222
+ ```bash
223
+ uv tool upgrade --python 3.12 uxarray-mcp # or your original install method
224
+ ```
225
+
226
+ > **⚠️ Restart your AI client after upgrading.** MCP servers are launched once
227
+ > when your client (Claude Desktop, Claude Code, Cursor, …) starts and are **not
228
+ > hot-reloaded**. After upgrading the package, **fully quit and reopen your AI
229
+ > client** so it relaunches `uxarray-mcp serve` with the new code. Until you do,
230
+ > the running server keeps executing the *old* version — new tools and fixes
231
+ > won't appear, and you may see confusing errors (for example, a `use_remote`
232
+ > call on an HPC-only path failing with "file not found" because the old,
233
+ > local-only tool is still loaded). If in doubt, run `uxarray-mcp doctor` and
234
+ > check the reported version.
235
+
236
+ ---
237
+
177
238
  ## Risks (read before relying on output)
178
239
 
179
240
  AI agents can misread prompts, pick the wrong file, get units wrong (e.g.,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uxarray-mcp"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "MCP server for analyzing unstructured meshes with UXarray"
5
5
  readme = "README.md"
6
6
  keywords = ["uxarray", "mcp", "unstructured grids", "scientific computing", "globus compute"]
@@ -93,6 +93,10 @@ extend-exclude = ["docs/architecture.html"]
93
93
 
94
94
  [tool.ruff.lint]
95
95
  extend-select = ["I"]
96
+ # Notebooks (and their jupytext-paired .py scripts) intentionally scatter
97
+ # imports across per-cell (# %%) blocks, so E402 (module-import-not-at-top)
98
+ # is expected style there, not a lint violation.
99
+ per-file-ignores = { "notebooks/*.py" = ["E402"], "notebooks/*.ipynb" = ["E402"] }
96
100
 
97
101
  [tool.ruff.lint.isort]
98
102
  known-first-party = ["uxarray_mcp"]
@@ -3,4 +3,4 @@
3
3
  from uxarray_mcp.tools import inspect_mesh
4
4
 
5
5
  __all__ = ["inspect_mesh"]
6
- __version__ = "0.1.2"
6
+ __version__ = "0.1.3"
@@ -308,8 +308,8 @@ def build_parser() -> argparse.ArgumentParser:
308
308
  choices=("core", "deferred-full"),
309
309
  default="core",
310
310
  help=(
311
- "core: gateway + control + list_datasets + prompts (~27 tools). "
312
- "deferred-full: also load 30 raw tools as deferred, gated "
311
+ "core: gateway + control + list_datasets + prompts (~31 tools). "
312
+ "deferred-full: also load 32 raw tools as deferred, gated "
313
313
  "behind discover_tools / admin promotion."
314
314
  ),
315
315
  )
@@ -317,7 +317,7 @@ def build_parser() -> argparse.ArgumentParser:
317
317
  "--transport",
318
318
  choices=("stdio", "sse", "http"),
319
319
  default="stdio",
320
- help="MCP transport. stdio for Claude Desktop subprocess use.",
320
+ help="MCP transport. stdio for subprocess use by MCP clients.",
321
321
  )
322
322
  serve.add_argument("--host", default="127.0.0.1", help="Bind host for SSE/HTTP.")
323
323
  serve.add_argument("--port", type=int, default=8001, help="Port for SSE/HTTP.")
@@ -330,8 +330,8 @@ def build_parser() -> argparse.ArgumentParser:
330
330
  choices=("core", "deferred-full"),
331
331
  default="core",
332
332
  help=(
333
- "core: gateway + control + list_datasets + prompts (~27 tools). "
334
- "deferred-full: also load 30 raw tools as deferred, gated "
333
+ "core: gateway + control + list_datasets + prompts (~31 tools). "
334
+ "deferred-full: also load 32 raw tools as deferred, gated "
335
335
  "behind discover_tools / admin promotion."
336
336
  ),
337
337
  )
@@ -347,7 +347,11 @@ def build_parser() -> argparse.ArgumentParser:
347
347
  "--execution-mode",
348
348
  default="auto",
349
349
  choices=["local", "auto", "hpc"],
350
- help="Default execution mode (default: auto).",
350
+ help=(
351
+ "Default execution mode (default: auto). 'auto' runs local-only "
352
+ "until you add an endpoint with `endpoints add`, then uses it "
353
+ "automatically. Most users never need to change this."
354
+ ),
351
355
  )
352
356
  setup.add_argument("--force", action="store_true", help="Overwrite existing file.")
353
357
  setup.set_defaults(func=cmd_setup)
@@ -377,12 +381,42 @@ def build_parser() -> argparse.ArgumentParser:
377
381
  ep_remove.add_argument("name")
378
382
  ep_remove.set_defaults(func=cmd_endpoints_remove)
379
383
 
380
- doctor = sub.add_parser("doctor", help="Validate HPC readiness.")
381
- doctor.add_argument("--sample-path", action="append", default=[])
382
- doctor.add_argument("--timeout-seconds", type=int, default=180)
383
- doctor.add_argument("--endpoint", default=None)
384
- doctor.add_argument("--skip-remote-probe", action="store_true")
385
- doctor.add_argument("--no-netcdf", action="store_true")
384
+ doctor = sub.add_parser(
385
+ "doctor",
386
+ help="Check that the install is healthy (local-only or HPC).",
387
+ description=(
388
+ "Run local-only by default; add an HPC endpoint (`endpoints add`) "
389
+ "to also validate remote readiness. With no endpoint configured, "
390
+ "this reports a passing local-only result — HPC is opt-in."
391
+ ),
392
+ )
393
+ doctor.add_argument(
394
+ "--sample-path",
395
+ action="append",
396
+ default=[],
397
+ help="Remote file path to test-read (repeatable). Requires an endpoint.",
398
+ )
399
+ doctor.add_argument(
400
+ "--timeout-seconds",
401
+ type=int,
402
+ default=180,
403
+ help="Seconds to wait for the remote worker probe (ignored if no endpoint).",
404
+ )
405
+ doctor.add_argument(
406
+ "--endpoint",
407
+ default=None,
408
+ help="Named endpoint to validate. Omit to check the default/local-only setup.",
409
+ )
410
+ doctor.add_argument(
411
+ "--skip-remote-probe",
412
+ action="store_true",
413
+ help="Skip submitting a test job to the endpoint (config/auth checks only).",
414
+ )
415
+ doctor.add_argument(
416
+ "--no-netcdf",
417
+ action="store_true",
418
+ help="Skip opening --sample-path as NetCDF; just check readability.",
419
+ )
386
420
  doctor.set_defaults(func=cmd_doctor)
387
421
 
388
422
  claude = sub.add_parser(
@@ -47,17 +47,22 @@ def load_dataset(grid_path: str, data_path: str) -> Any:
47
47
  Loaded dataset object.
48
48
  """
49
49
  import uxarray as ux
50
+ import xarray as xr
50
51
 
51
- # HEALPix is a special case (usually grid-only, but we support it if matched)
52
+ # HEALPix and GIS grids don't round-trip through ux.open_dataset() as a
53
+ # "grid file" the way a real UGRID/MPAS/SCRIP file does: grid.to_xarray()
54
+ # returns a minimal representation (e.g. HEALPix has no node coordinates)
55
+ # that the generic UGRID reader rejects. Attach the data directly to the
56
+ # already-loaded Grid object instead.
52
57
  if grid_path.lower().startswith("healpix"):
53
58
  parts = grid_path.split(":")
54
59
  zoom = int(parts[1]) if len(parts) > 1 else 1
55
60
  grid = ux.Grid.from_healpix(zoom=zoom)
56
- return ux.open_dataset(grid.to_xarray(), data_path)
61
+ return ux.UxDataset(xr.open_dataset(data_path), uxgrid=grid)
57
62
 
58
63
  ext = os.path.splitext(grid_path.lower())[1]
59
64
  if ext in [".shp", ".geojson"]:
60
65
  grid = ux.Grid.from_file(grid_path, backend="geopandas")
61
- return ux.open_dataset(grid.to_xarray(), data_path)
66
+ return ux.UxDataset(xr.open_dataset(data_path), uxgrid=grid)
62
67
 
63
68
  return ux.open_dataset(grid_path, data_path)
@@ -4,6 +4,80 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ # Units that look like a genuine 2-D vector (velocity/flux) component. Used only
8
+ # to raise a soft, non-blocking warning when curl/divergence inputs do not look
9
+ # like real vector components — the math is still valid, but the result is only
10
+ # physically meaningful for true vector fields.
11
+ _VELOCITY_LIKE_UNIT_HINTS = (
12
+ "m/s",
13
+ "m s-1",
14
+ "m s^-1",
15
+ "meter/second",
16
+ "meters/second",
17
+ "cm/s",
18
+ "km/h",
19
+ "kg/m2/s",
20
+ "kg m-2 s-1",
21
+ "pa/s",
22
+ "n/m2",
23
+ )
24
+
25
+
26
+ def _vector_component_warnings(
27
+ u_variable: str,
28
+ v_variable: str,
29
+ u: Any,
30
+ v: Any,
31
+ operation: str,
32
+ ) -> list[str]:
33
+ """Return soft warnings when (u, v) do not look like real vector components.
34
+
35
+ curl and divergence are only physically meaningful when ``u`` and ``v`` are
36
+ the two horizontal components of a genuine vector field (e.g. eastward and
37
+ northward velocity). The underlying finite-volume operators will happily
38
+ compute a number from *any* two scalar fields, so this guardrail flags the
39
+ common misuse patterns without blocking the computation:
40
+
41
+ 1. ``u`` and ``v`` are the same variable (a scalar used as both components).
42
+ 2. Neither component carries a velocity/flux-like ``units`` attribute.
43
+
44
+ Warnings are attached to the result so the caller — and any downstream
45
+ scientist — can see that the inputs were suspicious.
46
+ """
47
+ warnings: list[str] = []
48
+
49
+ if u_variable == v_variable:
50
+ warnings.append(
51
+ f"{operation}: u_variable and v_variable are the same field "
52
+ f"('{u_variable}'). {operation} is only physically meaningful for a "
53
+ "true 2-D vector field (distinct eastward/northward components); "
54
+ "the result here is a mathematical artifact, not a physical "
55
+ f"{operation}."
56
+ )
57
+
58
+ def _unit(var: Any) -> str:
59
+ attrs = getattr(var, "attrs", {}) or {}
60
+ return str(attrs.get("units", "")).strip().lower()
61
+
62
+ u_unit, v_unit = _unit(u), _unit(v)
63
+
64
+ def _looks_velocity(unit: str) -> bool:
65
+ return any(hint in unit for hint in _VELOCITY_LIKE_UNIT_HINTS)
66
+
67
+ if not (_looks_velocity(u_unit) or _looks_velocity(v_unit)):
68
+ seen = ", ".join(
69
+ f"{name}='{unit or 'unset'}'"
70
+ for name, unit in ((u_variable, u_unit), (v_variable, v_unit))
71
+ )
72
+ warnings.append(
73
+ f"{operation}: neither component has a velocity/flux-like 'units' "
74
+ f"attribute ({seen}). Verify that '{u_variable}' and '{v_variable}' "
75
+ "are genuine vector components (e.g. m/s) before interpreting the "
76
+ f"{operation} physically."
77
+ )
78
+
79
+ return warnings
80
+
7
81
 
8
82
  def compute_gradient(
9
83
  uxds: Any, variable_name: str, scale_by_radius: bool = False
@@ -115,6 +189,10 @@ def compute_curl(
115
189
 
116
190
  import numpy as np
117
191
 
192
+ component_warnings = _vector_component_warnings(
193
+ u_variable, v_variable, u, v, "curl"
194
+ )
195
+
118
196
  result = u.curl(v, scale_by_radius=scale_by_radius)
119
197
  vals = result.values
120
198
  finite = vals[np.isfinite(vals)]
@@ -137,6 +215,7 @@ def compute_curl(
137
215
  "n_face": int(uxds.uxgrid.n_face),
138
216
  "scale_by_radius": bool(scale_by_radius),
139
217
  "stats": stats,
218
+ "component_warnings": component_warnings,
140
219
  }
141
220
 
142
221
 
@@ -179,6 +258,10 @@ def compute_divergence(uxds: Any, u_variable: str, v_variable: str) -> dict:
179
258
 
180
259
  import numpy as np
181
260
 
261
+ component_warnings = _vector_component_warnings(
262
+ u_variable, v_variable, u, v, "divergence"
263
+ )
264
+
182
265
  result = u.divergence(v)
183
266
  vals = result.values
184
267
  finite = vals[np.isfinite(vals)]
@@ -200,6 +283,7 @@ def compute_divergence(uxds: Any, u_variable: str, v_variable: str) -> dict:
200
283
  "interpretation": "horizontal divergence ∂u/∂x + ∂v/∂y",
201
284
  "n_face": int(uxds.uxgrid.n_face),
202
285
  "stats": stats,
286
+ "component_warnings": component_warnings,
203
287
  }
204
288
 
205
289
 
@@ -5,9 +5,9 @@ Two profiles are supported:
5
5
  * ``"core"`` (default) — small, predictable surface visible to LLMs.
6
6
  Mirrors the original MCP server's 11 front-door tools, adds 12
7
7
  control/status tools, the ``list_datasets`` discovery helper, and
8
- three prompt-as-tool helpers (former ``@mcp.prompt()`` decorators).
8
+ seven prompt-as-tool helpers (former ``@mcp.prompt()`` decorators).
9
9
  * ``"deferred-full"`` — loads every public function with the core set
10
- enabled and 30 raw implementation tools marked ``defer=True``.
10
+ enabled and 32 raw implementation tools marked ``defer=True``.
11
11
  Includes ``discover_tools`` (BM25 search) so LLMs find deferred
12
12
  tools by intent.
13
13
 
@@ -592,9 +592,9 @@ def build_registry(
592
592
  """Build a ``ToolRegistry`` for the chosen profile.
593
593
 
594
594
  Args:
595
- profile: ``"core"`` for the small default surface (~27 tools),
595
+ profile: ``"core"`` for the small default surface (~31 tools),
596
596
  ``"deferred-full"`` for the complete pool (core visible,
597
- 30 raw tools deferred, ``discover_tools`` added).
597
+ 32 raw tools deferred, ``discover_tools`` added).
598
598
  registry_name: Identifier for server titles and labels.
599
599
 
600
600
  Returns:
@@ -424,15 +424,47 @@ class UXarrayComputeAgent(_AcademyAgent):
424
424
 
425
425
  # Attach provenance with the correct HPC venue — the remote functions
426
426
  # are self contained and don't call attach_provenance themselves.
427
- from uxarray_mcp.provenance import attach_provenance
427
+ from uxarray_mcp.provenance import _get_uxarray_version, attach_provenance
428
428
 
429
429
  endpoint_label = self.config.endpoint_name or "configured"
430
- return attach_provenance(
430
+
431
+ # Capture the worker's true software versions (when the remote function
432
+ # reports them) so provenance reflects what actually computed the result
433
+ # — not the local submitter — and warn on local/remote version drift.
434
+ drift_warnings: list[str] = []
435
+ worker_uxarray = None
436
+ if isinstance(result, dict):
437
+ worker_uxarray = result.pop("_worker_uxarray_version", None)
438
+ result.pop("_worker_python_version", None)
439
+ local_uxarray = _get_uxarray_version()
440
+ if (
441
+ worker_uxarray
442
+ and local_uxarray != "unknown"
443
+ and worker_uxarray != local_uxarray
444
+ ):
445
+ drift_warnings.append(
446
+ f"UXarray version drift: local={local_uxarray}, "
447
+ f"remote(worker)={worker_uxarray}. Numerical results may differ "
448
+ "between local and remote runs; compare with care."
449
+ )
450
+
451
+ # Fold any warnings the remote function itself produced (e.g. vector
452
+ # component guardrails) into provenance.
453
+ if isinstance(result, dict):
454
+ fn_warnings = result.get("component_warnings") or []
455
+ drift_warnings.extend(fn_warnings)
456
+
457
+ annotated = attach_provenance(
431
458
  result,
432
459
  tool=func.__name__,
433
460
  inputs={"args": [str(a) for a in args]},
434
461
  venue=f"hpc:{endpoint_label}",
462
+ warnings=drift_warnings or None,
435
463
  )
464
+ # Record the worker's uxarray version explicitly alongside the local one.
465
+ if worker_uxarray:
466
+ annotated["_provenance"]["remote_uxarray_version"] = worker_uxarray
467
+ return annotated
436
468
 
437
469
  def _run_local_inspect_mesh(self, file_path: str) -> Dict[str, Any]:
438
470
  """Execute inspect_mesh locally as fallback."""