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.
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/PKG-INFO +88 -6
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/README.md +79 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/pyproject.toml +10 -10
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/pyproject.toml.orig +33 -14
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/__init__.py +1 -1
- uxarray_mcp-0.3.0/src/uxarray_mcp/app.py +137 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/cli.py +4 -4
- uxarray_mcp-0.3.0/src/uxarray_mcp/content_blocks.py +114 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/__init__.py +6 -1
- uxarray_mcp-0.3.0/src/uxarray_mcp/domain/dims.py +85 -0
- uxarray_mcp-0.3.0/src/uxarray_mcp/domain/mesh.py +111 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/plotting.py +34 -8
- uxarray_mcp-0.3.0/src/uxarray_mcp/domain/remap_coverage.py +125 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/vector_calc.py +105 -42
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/zonal.py +77 -6
- uxarray_mcp-0.3.0/src/uxarray_mcp/next_steps.py +48 -0
- uxarray_mcp-0.3.0/src/uxarray_mcp/postconditions.py +317 -0
- uxarray_mcp-0.3.0/src/uxarray_mcp/preconditions.py +330 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/provenance.py +28 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/registry.py +41 -9
- uxarray_mcp-0.3.0/src/uxarray_mcp/remote/__init__.py +70 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/agent.py +105 -5
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/compute_functions.py +489 -90
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/config.py +20 -1
- uxarray_mcp-0.3.0/src/uxarray_mcp/response_contract.py +253 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/__init__.py +7 -1
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/advanced.py +60 -11
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/capabilities.py +8 -4
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/catalog.py +1 -1
- uxarray_mcp-0.3.0/src/uxarray_mcp/tools/contracts.py +63 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/execution_control.py +157 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/frontdoor.py +404 -6
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/inspection.py +68 -27
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/orchestration.py +63 -15
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/plotting.py +91 -21
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/remote_tools.py +125 -29
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/scientific_agent.py +6 -2
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/stateful.py +2 -1
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/tools/vector_calc.py +44 -6
- uxarray_mcp-0.3.0/src/uxarray_mcp/typed_results.py +300 -0
- uxarray_mcp-0.2.2/src/uxarray_mcp/app.py +0 -77
- uxarray_mcp-0.2.2/src/uxarray_mcp/domain/mesh.py +0 -68
- uxarray_mcp-0.2.2/src/uxarray_mcp/remote/__init__.py +0 -6
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/LICENSE +0 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/__main__.py +0 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/area.py +0 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/domain/variable.py +0 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/py.typed +0 -0
- {uxarray_mcp-0.2.2 → uxarray_mcp-0.3.0}/src/uxarray_mcp/remote/health.py +0 -0
- {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.
|
|
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.
|
|
218
|
-
Requires-Dist:
|
|
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.
|
|
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.
|
|
229
|
-
Requires-Python: >=3.
|
|
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.
|
|
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.
|
|
24
|
+
requires-python = ">=3.11"
|
|
23
25
|
dependencies = [
|
|
24
|
-
"toolregistry-server[mcp]>=0.
|
|
25
|
-
"
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
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.
|
|
29
|
-
#
|
|
30
|
-
|
|
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.
|
|
52
|
+
"uxarray>=2026.8.0",
|
|
35
53
|
]
|
|
36
54
|
|
|
37
55
|
[project.optional-dependencies]
|
|
38
56
|
openapi = [
|
|
39
|
-
"toolregistry-server[openapi]>=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
|
-
|
|
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"]
|
|
@@ -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 (
|
|
312
|
-
"deferred-full: also load
|
|
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 (
|
|
334
|
-
"deferred-full: also load
|
|
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",
|