uxarray-mcp 0.2.2__tar.gz → 0.3.0__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 (50) hide show
  1. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/PKG-INFO +88 -6
  2. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/README.md +79 -0
  3. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/pyproject.toml +10 -10
  4. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/pyproject.toml.orig +33 -14
  5. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/__init__.py +1 -1
  6. uxarray_mcp-0.3.0/src/uxarray_mcp/app.py +137 -0
  7. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/cli.py +4 -4
  8. uxarray_mcp-0.3.0/src/uxarray_mcp/content_blocks.py +114 -0
  9. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/__init__.py +6 -1
  10. uxarray_mcp-0.3.0/src/uxarray_mcp/domain/dims.py +85 -0
  11. uxarray_mcp-0.3.0/src/uxarray_mcp/domain/mesh.py +111 -0
  12. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/plotting.py +34 -8
  13. uxarray_mcp-0.3.0/src/uxarray_mcp/domain/remap_coverage.py +125 -0
  14. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/vector_calc.py +105 -42
  15. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/zonal.py +77 -6
  16. uxarray_mcp-0.3.0/src/uxarray_mcp/next_steps.py +48 -0
  17. uxarray_mcp-0.3.0/src/uxarray_mcp/postconditions.py +317 -0
  18. uxarray_mcp-0.3.0/src/uxarray_mcp/preconditions.py +330 -0
  19. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/provenance.py +28 -0
  20. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/registry.py +41 -9
  21. uxarray_mcp-0.3.0/src/uxarray_mcp/remote/__init__.py +70 -0
  22. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/agent.py +105 -5
  23. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/compute_functions.py +489 -90
  24. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/config.py +20 -1
  25. uxarray_mcp-0.3.0/src/uxarray_mcp/response_contract.py +253 -0
  26. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/__init__.py +7 -1
  27. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/advanced.py +60 -11
  28. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/capabilities.py +8 -4
  29. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/catalog.py +1 -1
  30. uxarray_mcp-0.3.0/src/uxarray_mcp/tools/contracts.py +63 -0
  31. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/execution_control.py +157 -0
  32. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/frontdoor.py +404 -6
  33. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/inspection.py +68 -27
  34. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/orchestration.py +63 -15
  35. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/plotting.py +91 -21
  36. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/remote_tools.py +125 -29
  37. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/scientific_agent.py +6 -2
  38. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/stateful.py +2 -1
  39. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/vector_calc.py +44 -6
  40. uxarray_mcp-0.3.0/src/uxarray_mcp/typed_results.py +300 -0
  41. uxarray_mcp-0.2.2/src/uxarray_mcp/app.py +0 -77
  42. uxarray_mcp-0.2.2/src/uxarray_mcp/domain/mesh.py +0 -68
  43. uxarray_mcp-0.2.2/src/uxarray_mcp/remote/__init__.py +0 -6
  44. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/LICENSE +0 -0
  45. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/__main__.py +0 -0
  46. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/area.py +0 -0
  47. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/variable.py +0 -0
  48. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/py.typed +0 -0
  49. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/health.py +0 -0
  50. {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uxarray-mcp
3
- Version: 0.2.2
3
+ Version: 0.3.0
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
@@ -212,21 +212,24 @@ Classifier: Intended Audience :: Science/Research
212
212
  Classifier: Topic :: Scientific/Engineering
213
213
  Classifier: Programming Language :: Python
214
214
  Classifier: Programming Language :: Python :: 3
215
+ Classifier: Programming Language :: Python :: 3.11
215
216
  Classifier: Programming Language :: Python :: 3.12
217
+ Classifier: Programming Language :: Python :: 3.13
216
218
  Classifier: License :: OSI Approved :: Apache Software License
217
- Requires-Dist: toolregistry-server[mcp]>=0.4.0
218
- Requires-Dist: mcp>=1.20,<2
219
+ Requires-Dist: toolregistry-server[mcp]>=0.5.0
220
+ Requires-Dist: toolregistry>=0.16.0
221
+ Requires-Dist: mcp>=1.24,<3
219
222
  Requires-Dist: holoviews>=1.19.0
220
223
  Requires-Dist: matplotlib>=3.9.0
221
224
  Requires-Dist: pyyaml>=6.0
222
- Requires-Dist: uxarray>=2026.7.0
225
+ Requires-Dist: uxarray>=2026.8.0
223
226
  Requires-Dist: sphinx>=7.0 ; extra == 'docs'
224
227
  Requires-Dist: sphinx-book-theme>=1.1.0 ; extra == 'docs'
225
228
  Requires-Dist: myst-parser>=3.0 ; extra == 'docs'
226
229
  Requires-Dist: academy-py>=0.3.1 ; extra == 'hpc'
227
230
  Requires-Dist: globus-compute-sdk>=4.5.0 ; extra == 'hpc'
228
- Requires-Dist: toolregistry-server[openapi]>=0.4.0 ; extra == 'openapi'
229
- Requires-Python: >=3.12, <3.13
231
+ Requires-Dist: toolregistry-server[openapi]>=0.5.0 ; extra == 'openapi'
232
+ Requires-Python: >=3.11
230
233
  Project-URL: Documentation, https://uxarray-mcp-server.readthedocs.io
231
234
  Project-URL: Source, https://github.com/UXARRAY/uxarray-mcp-server
232
235
  Project-URL: Tracker, https://github.com/UXARRAY/uxarray-mcp-server/issues
@@ -282,6 +285,78 @@ You are most likely one of:
282
285
  project, or lab. → [Local install](#local-install), then the full
283
286
  [docs/operating-an-endpoint.md](docs/operating-an-endpoint.md) including
284
287
  service-account migration and the MEP allowlist.
288
+ 5. **Just trying it out, or running an agent harness** — you don't want to
289
+ install a scientific Python stack at all. → [Docker](#docker).
290
+
291
+ ---
292
+
293
+ ## Docker
294
+
295
+ The container is the fastest way to run the server without resolving
296
+ `uxarray`, `netcdf4`, `matplotlib`, and friends on your own machine. It ships
297
+ five small mesh fixtures so there is something to analyze immediately.
298
+
299
+ ```bash
300
+ docker build -t uxarray-mcp:local .
301
+ docker run --rm -i uxarray-mcp:local # stdio, what MCP clients spawn
302
+ ```
303
+
304
+ Point Claude Code at it:
305
+
306
+ ```bash
307
+ claude mcp add uxarray-docker --transport stdio -- \
308
+ docker run --rm -i uxarray-mcp:local
309
+ ```
310
+
311
+ To analyze your own meshes, mount them — `/work` is the working directory:
312
+
313
+ ```bash
314
+ docker run --rm -i -v /path/to/my/data:/work uxarray-mcp:local
315
+ ```
316
+
317
+ For an agent harness that wants HTTP instead of stdio:
318
+
319
+ ```bash
320
+ docker run --rm -p 8001:8001 uxarray-mcp:local \
321
+ serve --transport http --host 0.0.0.0
322
+ ```
323
+
324
+ Verify an image end-to-end — handshake, tool surface, and one real
325
+ computation checked against an analytic result:
326
+
327
+ ```bash
328
+ python3 scripts/container_smoke_test.py --image uxarray-mcp:local
329
+ ```
330
+
331
+ **The image is local-only, on purpose.** The HPC extras
332
+ (`globus-compute-sdk`, `academy-py`) are not installed, and the baked config
333
+ pins `execution_mode: local`. A sealed container should not hold Globus
334
+ credentials or reach a Slurm endpoint — and an image that *could* submit
335
+ remote work is not one you should point an untrusted agent at. If you want
336
+ HPC, run the server on the host where your identity lives; see
337
+ [docs/remote-hpc.md](docs/remote-hpc.md).
338
+
339
+ **Baked fixtures** live at `/data/uxarray` and are generated at build time by
340
+ [`scripts/generate_container_fixtures.py`](scripts/generate_container_fixtures.py)
341
+ rather than committed as binaries, so what's in them is readable as code. Each
342
+ one targets a specific blind spot:
343
+
344
+ | Fixture | Why it exists |
345
+ |---|---|
346
+ | `global` | Coarse global mesh, unit sphere — the everyday case. |
347
+ | `earth_radius` | Declares R = 6371 km, so a missing radius scaling shows up in the numbers instead of hiding behind R = 1. |
348
+ | `multi_level` | Four levels 100 apart; a wrong level selection is unmistakable. |
349
+ | `time_level` | Three times × four levels, value `1000*t + 100*(k+1)` — the magnitude says which slice was taken. |
350
+ | `regional` | A sliver mesh, so remap-coverage failures have something to fail against. |
351
+
352
+ `MANIFEST.json` records a content hash per fixture — hashing decoded arrays
353
+ rather than file bytes, so it stays stable across NetCDF library versions. The
354
+ build verifies it, and you can re-check any image:
355
+
356
+ ```bash
357
+ docker run --rm -i --entrypoint python uxarray-mcp:local - --verify \
358
+ < scripts/generate_container_fixtures.py
359
+ ```
285
360
 
286
361
  ---
287
362
 
@@ -407,6 +482,13 @@ Intent-shaped tools, not raw UXarray bindings — all local by default:
407
482
 
408
483
  Full schema: [docs/tools.md](docs/tools.md).
409
484
 
485
+ **Protocol version.** We do not implement MCP directly; servers are built
486
+ through `toolregistry-server`, which depends on the `mcp` Python SDK. As of
487
+ `toolregistry-server` 0.5.0 and `toolregistry` 0.16.0 the SDK cap is lifted, so
488
+ we resolve `mcp` 2.1.1 and negotiate spec **`2026-07-28`** (stateless core,
489
+ cacheable list results, MRTR). 0.16.0 also widens the recognized content-block
490
+ set to audio, `resource_link`, and embedded resources.
491
+
410
492
  **Once you've configured an HPC endpoint** (optional — see
411
493
  [Going beyond your laptop](#going-beyond-your-laptop) below): most tools above
412
494
  also take `use_remote: bool` and `endpoint: str`, falling back to local if the
@@ -45,6 +45,78 @@ You are most likely one of:
45
45
  project, or lab. → [Local install](#local-install), then the full
46
46
  [docs/operating-an-endpoint.md](docs/operating-an-endpoint.md) including
47
47
  service-account migration and the MEP allowlist.
48
+ 5. **Just trying it out, or running an agent harness** — you don't want to
49
+ install a scientific Python stack at all. → [Docker](#docker).
50
+
51
+ ---
52
+
53
+ ## Docker
54
+
55
+ The container is the fastest way to run the server without resolving
56
+ `uxarray`, `netcdf4`, `matplotlib`, and friends on your own machine. It ships
57
+ five small mesh fixtures so there is something to analyze immediately.
58
+
59
+ ```bash
60
+ docker build -t uxarray-mcp:local .
61
+ docker run --rm -i uxarray-mcp:local # stdio, what MCP clients spawn
62
+ ```
63
+
64
+ Point Claude Code at it:
65
+
66
+ ```bash
67
+ claude mcp add uxarray-docker --transport stdio -- \
68
+ docker run --rm -i uxarray-mcp:local
69
+ ```
70
+
71
+ To analyze your own meshes, mount them — `/work` is the working directory:
72
+
73
+ ```bash
74
+ docker run --rm -i -v /path/to/my/data:/work uxarray-mcp:local
75
+ ```
76
+
77
+ For an agent harness that wants HTTP instead of stdio:
78
+
79
+ ```bash
80
+ docker run --rm -p 8001:8001 uxarray-mcp:local \
81
+ serve --transport http --host 0.0.0.0
82
+ ```
83
+
84
+ Verify an image end-to-end — handshake, tool surface, and one real
85
+ computation checked against an analytic result:
86
+
87
+ ```bash
88
+ python3 scripts/container_smoke_test.py --image uxarray-mcp:local
89
+ ```
90
+
91
+ **The image is local-only, on purpose.** The HPC extras
92
+ (`globus-compute-sdk`, `academy-py`) are not installed, and the baked config
93
+ pins `execution_mode: local`. A sealed container should not hold Globus
94
+ credentials or reach a Slurm endpoint — and an image that *could* submit
95
+ remote work is not one you should point an untrusted agent at. If you want
96
+ HPC, run the server on the host where your identity lives; see
97
+ [docs/remote-hpc.md](docs/remote-hpc.md).
98
+
99
+ **Baked fixtures** live at `/data/uxarray` and are generated at build time by
100
+ [`scripts/generate_container_fixtures.py`](scripts/generate_container_fixtures.py)
101
+ rather than committed as binaries, so what's in them is readable as code. Each
102
+ one targets a specific blind spot:
103
+
104
+ | Fixture | Why it exists |
105
+ |---|---|
106
+ | `global` | Coarse global mesh, unit sphere — the everyday case. |
107
+ | `earth_radius` | Declares R = 6371 km, so a missing radius scaling shows up in the numbers instead of hiding behind R = 1. |
108
+ | `multi_level` | Four levels 100 apart; a wrong level selection is unmistakable. |
109
+ | `time_level` | Three times × four levels, value `1000*t + 100*(k+1)` — the magnitude says which slice was taken. |
110
+ | `regional` | A sliver mesh, so remap-coverage failures have something to fail against. |
111
+
112
+ `MANIFEST.json` records a content hash per fixture — hashing decoded arrays
113
+ rather than file bytes, so it stays stable across NetCDF library versions. The
114
+ build verifies it, and you can re-check any image:
115
+
116
+ ```bash
117
+ docker run --rm -i --entrypoint python uxarray-mcp:local - --verify \
118
+ < scripts/generate_container_fixtures.py
119
+ ```
48
120
 
49
121
  ---
50
122
 
@@ -170,6 +242,13 @@ Intent-shaped tools, not raw UXarray bindings — all local by default:
170
242
 
171
243
  Full schema: [docs/tools.md](docs/tools.md).
172
244
 
245
+ **Protocol version.** We do not implement MCP directly; servers are built
246
+ through `toolregistry-server`, which depends on the `mcp` Python SDK. As of
247
+ `toolregistry-server` 0.5.0 and `toolregistry` 0.16.0 the SDK cap is lifted, so
248
+ we resolve `mcp` 2.1.1 and negotiate spec **`2026-07-28`** (stateless core,
249
+ cacheable list results, MRTR). 0.16.0 also widens the recognized content-block
250
+ set to audio, `resource_link`, and embedded resources.
251
+
173
252
  **Once you've configured an HPC endpoint** (optional — see
174
253
  [Going beyond your laptop](#going-beyond-your-laptop) below): most tools above
175
254
  also take `use_remote: bool` and `endpoint: str`, falling back to local if the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uxarray-mcp"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "MCP server for analyzing unstructured meshes with UXarray"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -16,17 +16,20 @@ classifiers = [
16
16
  "Topic :: Scientific/Engineering",
17
17
  "Programming Language :: Python",
18
18
  "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
19
20
  "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
20
22
  "License :: OSI Approved :: Apache Software License",
21
23
  ]
22
- requires-python = ">=3.12,<3.13"
24
+ requires-python = ">=3.11"
23
25
  dependencies = [
24
- "toolregistry-server[mcp]>=0.4.0",
25
- "mcp>=1.20,<2",
26
+ "toolregistry-server[mcp]>=0.5.0",
27
+ "toolregistry>=0.16.0",
28
+ "mcp>=1.24,<3",
26
29
  "holoviews>=1.19.0",
27
30
  "matplotlib>=3.9.0",
28
31
  "pyyaml>=6.0",
29
- "uxarray>=2026.7.0",
32
+ "uxarray>=2026.8.0",
30
33
  ]
31
34
 
32
35
  [[project.authors]]
@@ -41,7 +44,7 @@ email = "dayanabdulla000@gmail.com"
41
44
  file = "LICENSE"
42
45
 
43
46
  [project.optional-dependencies]
44
- openapi = ["toolregistry-server[openapi]>=0.4.0"]
47
+ openapi = ["toolregistry-server[openapi]>=0.5.0"]
45
48
  hpc = [
46
49
  "academy-py>=0.3.1",
47
50
  "globus-compute-sdk>=4.5.0",
@@ -70,7 +73,7 @@ dev = [
70
73
  ]
71
74
 
72
75
  [tool.mypy]
73
- python_version = "3.12"
76
+ python_version = "3.11"
74
77
  check_untyped_defs = true
75
78
  ignore_missing_imports = true
76
79
  warn_unused_ignores = true
@@ -88,9 +91,6 @@ ignore_missing_imports = true
88
91
  module = "uxarray_mcp.remote.agent"
89
92
  warn_unused_ignores = false
90
93
 
91
- [tool.ruff]
92
- extend-exclude = ["docs/architecture.html"]
93
-
94
94
  [tool.ruff.lint]
95
95
  extend-select = ["I"]
96
96
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uxarray-mcp"
3
- version = "0.2.2"
3
+ version = "0.3.0"
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"]
@@ -10,7 +10,9 @@ classifiers = [
10
10
  "Topic :: Scientific/Engineering",
11
11
  "Programming Language :: Python",
12
12
  "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
13
14
  "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
14
16
  "License :: OSI Approved :: Apache Software License",
15
17
  ]
16
18
  authors = [
@@ -18,25 +20,41 @@ authors = [
18
20
  { name = "Dayan Abdulla", email = "dayanabdulla000@gmail.com" },
19
21
  ]
20
22
  license = { file = "LICENSE" }
21
- # Pinned to 3.12 until globus/globus-compute#2139 lands a published submitter ↔
22
- # worker compatibility story. HPC sites broadly ship Python 3.12 conda stacks
23
- # today; matching that minor version on the submitter side avoids WorkerLost
24
- # from pickle protocol skew. Revisit when upstream confirms multi-version
25
- # support and broaden requires-python back to >=3.11.
26
- requires-python = ">=3.12,<3.13"
23
+ # The floor is 3.11; there is deliberately no upper bound.
24
+ #
25
+ # This used to read ">=3.12,<3.13" because of globus/globus-compute#2139: a
26
+ # 3.13 submitter against a 3.12 worker raises WorkerLost on non-trivial
27
+ # payloads. That is a real constraint, but it belongs to the *HPC* path only,
28
+ # and pinning the whole package for it made every local-only user -- the large
29
+ # majority -- pay for a feature they never enable. It also blocked the conda
30
+ # recipe and any container base that is not exactly 3.12.
31
+ #
32
+ # The constraint is now enforced where it actually applies: `uxarray_mcp.remote`
33
+ # checks the submitter's Python at import time and refuses to configure an
34
+ # endpoint from an unsupported interpreter, and `remote.health` already warns on
35
+ # submitter/worker minor-version skew at probe time. A local install on 3.11 or
36
+ # 3.13 works and never touches that code.
37
+ #
38
+ # All runtime dependencies (uxarray, mcp, toolregistry-server, holoviews)
39
+ # declare >=3.10, so 3.11 is a conservative floor.
40
+ requires-python = ">=3.11"
27
41
  dependencies = [
28
- "toolregistry-server[mcp]>=0.4.0",
29
- # toolregistry-server 0.4.x imports McpError, renamed to MCPError in mcp 2.
30
- "mcp>=1.20,<2",
42
+ "toolregistry-server[mcp]>=0.5.0",
43
+ # Imported directly (ToolRegistry, ToolTag), so it is declared directly
44
+ # rather than leaned on transitively. 0.16.0 is the release that teaches
45
+ # is_content_block_list about audio/resource_link/resource; on 0.15.0 those
46
+ # blocks are silently stringified into one TextContent instead of failing.
47
+ "toolregistry>=0.16.0",
48
+ "mcp>=1.24,<3",
31
49
  "holoviews>=1.19.0",
32
50
  "matplotlib>=3.9.0",
33
51
  "pyyaml>=6.0",
34
- "uxarray>=2026.7.0",
52
+ "uxarray>=2026.8.0",
35
53
  ]
36
54
 
37
55
  [project.optional-dependencies]
38
56
  openapi = [
39
- "toolregistry-server[openapi]>=0.4.0",
57
+ "toolregistry-server[openapi]>=0.5.0",
40
58
  ]
41
59
  hpc = [
42
60
  "academy-py>=0.3.1",
@@ -66,7 +84,9 @@ dev = [
66
84
  ]
67
85
 
68
86
  [tool.mypy]
69
- python_version = "3.12"
87
+ # Type-check against the floor, not the container's interpreter: 3.11 is the
88
+ # lowest version we claim to support, so that is where checking has to happen.
89
+ python_version = "3.11"
70
90
  check_untyped_defs = true
71
91
  ignore_missing_imports = true
72
92
  warn_unused_ignores = true
@@ -91,7 +111,6 @@ requires = ["uv_build>=0.9.26,<0.10.0"]
91
111
  build-backend = "uv_build"
92
112
 
93
113
  [tool.ruff]
94
- extend-exclude = ["docs/architecture.html"]
95
114
 
96
115
  [tool.ruff.lint]
97
116
  extend-select = ["I"]
@@ -3,4 +3,4 @@
3
3
  from uxarray_mcp.tools import inspect_mesh
4
4
 
5
5
  __all__ = ["inspect_mesh"]
6
- __version__ = "0.2.2"
6
+ __version__ = "0.3.0"
@@ -0,0 +1,137 @@
1
+ """UXarray application — subclass of toolregistry_server.App.
2
+
3
+ Provides :class:`UXarrayApp`, the central server application that builds
4
+ the UXarray tool registry and dispatches to protocol adapters (MCP, OpenAPI).
5
+
6
+ Identity (product name, version, description) flows automatically to
7
+ MCP server name, OpenAPI title, and CLI banner.
8
+
9
+ Example::
10
+
11
+ from uxarray_mcp.app import UXarrayApp
12
+
13
+ app = UXarrayApp()
14
+ app.serve_mcp(transport="stdio", profile="core")
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING
20
+
21
+ from toolregistry_server import ServerIdentity
22
+ from toolregistry_server.app import App
23
+
24
+ from . import __version__
25
+ from .registry import Profile
26
+
27
+ if TYPE_CHECKING:
28
+ from toolregistry import ToolRegistry
29
+
30
+ UXARRAY_IDENTITY = ServerIdentity(
31
+ name="UXarray MCP",
32
+ version=__version__,
33
+ description="Mesh analysis tools for AI agents",
34
+ )
35
+
36
+
37
+ #: How long a client may cache our ``tools/list`` response, in milliseconds.
38
+ #: The tool surface is fixed at startup by the profile and does not change
39
+ #: while the server runs, so re-listing on every turn is pure overhead --
40
+ #: the catalog was measured at 74% of the payload on short conversations.
41
+ #: Five minutes bounds how long a client can hold a stale surface if a
42
+ #: future version does start mutating the registry at runtime.
43
+ #:
44
+ #: ``ttlMs`` and ``cacheScope`` are MCP spec 2026-07-28 fields and they do
45
+ #: reach the wire on the modern transport: ``mcp`` 2.1.1 has
46
+ #: ``MODERN_PROTOCOL_VERSIONS == ("2026-07-28",)`` and ``ListToolsResult``
47
+ #: inherits ``CacheableResult``, so a client sending the modern
48
+ #: ``MCP-Protocol-Version`` header sees them serialized. Only the legacy
49
+ #: ``initialize`` handshake caps out earlier (``LATEST_HANDSHAKE_VERSION`` is
50
+ #: 2025-11-25); on that path the SDK strips fields the negotiated era does
51
+ #: not define, and the hints simply cost nothing.
52
+ LIST_TOOLS_TTL_MS = 300_000
53
+
54
+ #: The surface depends only on the profile, not on the user or session, so
55
+ #: a shared cache entry is correct.
56
+ LIST_TOOLS_CACHE_SCOPE = "public"
57
+
58
+
59
+ class UXarrayApp(App):
60
+ """UXarray-specific server application.
61
+
62
+ Overrides :meth:`prepare_registry` to build the UXarray tool
63
+ registry with profile-based tool surface selection, and
64
+ :meth:`serve_mcp` so the ``tools/list`` cache hints survive to the wire.
65
+ """
66
+
67
+ def __init__(self, identity: ServerIdentity | None = None) -> None:
68
+ super().__init__(identity=identity or UXARRAY_IDENTITY)
69
+
70
+ def prepare_registry(self, **kwargs) -> ToolRegistry:
71
+ """Build the UXarray tool registry.
72
+
73
+ Keyword Args:
74
+ profile: Tool surface profile (``"core"`` or
75
+ ``"deferred-full"``). Defaults to ``"core"``.
76
+ """
77
+ from .registry import build_registry
78
+
79
+ profile = kwargs.get("profile", "core")
80
+ return build_registry(profile=profile)
81
+
82
+ def serve_mcp(self, **kwargs) -> None:
83
+ """Start an MCP server that actually advertises our cache hints.
84
+
85
+ The inherited path (``App.serve`` -> ``MCPAdapter.create_and_run``)
86
+ constructs the adapter itself and does not forward
87
+ ``list_tools_ttl_ms`` / ``list_tools_cache_scope``; they fall into
88
+ ``run(**kwargs)`` and are dropped without an error. A server started
89
+ that way advertises the SDK default of ``ttlMs=0``, i.e. immediately
90
+ stale, so every turn re-lists the whole catalog. Constructing the
91
+ adapter here is the only way the hints reach the wire on the CLI
92
+ path, which is the one users actually run.
93
+ """
94
+ from toolregistry_server.adapters.mcp import MCPAdapter
95
+
96
+ kwargs.setdefault("identity", self.identity)
97
+ registry = self.prepare_registry(**kwargs)
98
+ route_table = self._make_route_table(registry)
99
+
100
+ identity = kwargs.pop("identity")
101
+ name = kwargs.pop("name", identity.name)
102
+ kwargs.pop("profile", None)
103
+
104
+ MCPAdapter(
105
+ route_table,
106
+ name=name,
107
+ list_tools_ttl_ms=LIST_TOOLS_TTL_MS,
108
+ list_tools_cache_scope=LIST_TOOLS_CACHE_SCOPE,
109
+ ).run(**kwargs)
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Convenience helpers for tests and scripts
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def make_registry(*, profile: Profile = "core") -> ToolRegistry:
118
+ """Build the tool registry for the requested profile."""
119
+ return UXarrayApp().prepare_registry(profile=profile)
120
+
121
+
122
+ def make_mcp_server(*, profile: Profile = "core"):
123
+ """Build a configured MCP server ready for any transport.
124
+
125
+ Carries the same ``tools/list`` cache hints as :meth:`UXarrayApp.serve_mcp`
126
+ so tests and scripts exercise the surface the CLI actually serves.
127
+ """
128
+ from toolregistry_server.adapters.mcp import route_table_to_mcp_server
129
+ from toolregistry_server.route_table import RouteTable
130
+
131
+ registry = make_registry(profile=profile)
132
+ return route_table_to_mcp_server(
133
+ RouteTable(registry),
134
+ "UXarray MCP",
135
+ list_tools_ttl_ms=LIST_TOOLS_TTL_MS,
136
+ list_tools_cache_scope=LIST_TOOLS_CACHE_SCOPE,
137
+ )
@@ -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 (~31 tools). "
312
- "deferred-full: also load 32 raw tools as deferred, gated "
311
+ "core: gateway + control + list_datasets + prompts (33 tools). "
312
+ "deferred-full: also load 33 raw tools as deferred, gated "
313
313
  "behind discover_tools / admin promotion."
314
314
  ),
315
315
  )
@@ -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 (~31 tools). "
334
- "deferred-full: also load 32 raw tools as deferred, gated "
333
+ "core: gateway + control + list_datasets + prompts (33 tools). "
334
+ "deferred-full: also load 33 raw tools as deferred, gated "
335
335
  "behind discover_tools / admin promotion."
336
336
  ),
337
337
  )
@@ -0,0 +1,114 @@
1
+ """MCP content blocks in the shape the adapter actually recognizes.
2
+
3
+ ``toolregistry-server`` converts a tool result into MCP content only when the
4
+ result is a list of **plain dicts** whose ``type`` is one that
5
+ ``toolregistry.llm.content_blocks.is_content_block_list`` knows. A list of
6
+ ``mcp.types`` models fails that test, so the whole result is JSON-serialized
7
+ into a single ``TextContent`` and the image reaches the caller as a Python
8
+ ``repr`` inside a string -- no error, just a picture that is no longer a
9
+ picture. The wire shape is therefore built here, once, instead of at each
10
+ call site.
11
+
12
+ Two asymmetries in the adapter are easy to get wrong and are encoded here so
13
+ no caller has to remember them:
14
+
15
+ * an ``image`` carries its MIME type *inside* ``source``, while every other
16
+ block carries it on the block itself;
17
+ * the adapter reads only ``uri``, ``name`` and the MIME type off a
18
+ ``resource_link`` -- a title, description or size passed on the block is
19
+ silently dropped, so those belong in the accompanying metadata instead.
20
+
21
+ Recognized types are ``text``, ``image``, ``audio``, ``resource_link`` and
22
+ ``resource`` as of ``toolregistry`` 0.16.0; on 0.15.0 the last three are not
23
+ recognized and degrade to stringified text, which is why the floor moved.
24
+ """
25
+
26
+ from typing import Any
27
+
28
+ #: Block types the adapter converts natively. Kept local rather than imported
29
+ #: from toolregistry so that a version skew shows up as a failing test here
30
+ #: rather than as silently stringified output in production.
31
+ WIRE_BLOCK_TYPES = frozenset({"text", "image", "audio", "resource_link", "resource"})
32
+
33
+ DEFAULT_IMAGE_MIME = "image/png"
34
+
35
+
36
+ def text_block(text: str) -> dict[str, Any]:
37
+ """Build a ``text`` content block."""
38
+ return {"type": "text", "text": text}
39
+
40
+
41
+ def image_block(
42
+ data_b64: str, *, mime_type: str = DEFAULT_IMAGE_MIME
43
+ ) -> dict[str, Any]:
44
+ """Build an ``image`` content block from base64-encoded bytes.
45
+
46
+ The MIME type goes inside ``source``: that is where the adapter looks
47
+ for an image, and it is mandatory in practice because ``ImageContent``
48
+ declares ``mime_type`` with no default.
49
+ """
50
+ return {
51
+ "type": "image",
52
+ "source": {"type": "base64", "media_type": mime_type, "data": data_b64},
53
+ }
54
+
55
+
56
+ def resource_link_block(
57
+ uri: str, name: str, *, mime_type: str | None = None
58
+ ) -> dict[str, Any]:
59
+ """Build a ``resource_link`` content block.
60
+
61
+ ``uri`` and ``name`` are required -- the adapter subscripts both, so a
62
+ missing key surfaces as an opaque internal error rather than a
63
+ validation message.
64
+ """
65
+ block: dict[str, Any] = {"type": "resource_link", "uri": uri, "name": name}
66
+ if mime_type is not None:
67
+ block["mimeType"] = mime_type
68
+ return block
69
+
70
+
71
+ def is_wire_blocks(value: Any) -> bool:
72
+ """Whether *value* is a list the adapter will treat as content blocks.
73
+
74
+ Mirrors ``is_content_block_list``: a non-empty list in which every
75
+ element is a dict carrying a recognized ``type``.
76
+ """
77
+ return (
78
+ isinstance(value, list)
79
+ and bool(value)
80
+ and all(
81
+ isinstance(item, dict) and item.get("type") in WIRE_BLOCK_TYPES
82
+ for item in value
83
+ )
84
+ )
85
+
86
+
87
+ def block_image_data(block: Any) -> str | None:
88
+ """Read base64 image bytes back out of a block, or None.
89
+
90
+ Tools that compose other tools' output (the orchestrator, the
91
+ benchmarks) need to unpack a block without re-deriving its layout.
92
+ """
93
+ if isinstance(block, dict) and block.get("type") == "image":
94
+ source = block.get("source")
95
+ if isinstance(source, dict):
96
+ return source.get("data")
97
+ return None
98
+ # An mcp.types model may still arrive from an older code path.
99
+ return getattr(block, "data", None)
100
+
101
+
102
+ def block_uri(block: Any) -> str | None:
103
+ """Read a ``resource_link`` URI back out of a block, or None."""
104
+ if isinstance(block, dict):
105
+ return block.get("uri") if block.get("type") == "resource_link" else None
106
+ uri = getattr(block, "uri", None)
107
+ return None if uri is None else str(uri)
108
+
109
+
110
+ def block_text(block: Any) -> str | None:
111
+ """Read the text back out of a ``text`` block, or None."""
112
+ if isinstance(block, dict):
113
+ return block.get("text") if block.get("type") == "text" else None
114
+ return getattr(block, "text", None)
@@ -5,7 +5,8 @@ Functions here contain the pure domain logic used by both local tools
5
5
  """
6
6
 
7
7
  from .area import compute_area_stats
8
- from .mesh import load_dataset, load_grid
8
+ from .mesh import is_healpix_spec, load_dataset, load_grid, parse_healpix_zoom
9
+ from .remap_coverage import compute_target_coverage, method_is_conservative
9
10
  from .variable import compute_variable_info
10
11
  from .vector_calc import (
11
12
  compute_azimuthal_mean,
@@ -18,7 +19,11 @@ from .zonal import compute_zonal_anomaly_stats, compute_zonal_mean_stats
18
19
  __all__ = [
19
20
  "load_grid",
20
21
  "load_dataset",
22
+ "is_healpix_spec",
23
+ "parse_healpix_zoom",
21
24
  "compute_area_stats",
25
+ "compute_target_coverage",
26
+ "method_is_conservative",
22
27
  "compute_variable_info",
23
28
  "compute_zonal_mean_stats",
24
29
  "compute_zonal_anomaly_stats",