diff --git a/AGENTS.md b/AGENTS.md
index 0f4cbb9..2586eb5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,42 +4,93 @@ This file provides guidance for AI coding agents working on this repository.
## Project overview
-**uxarray-mcp-server** is an MCP (Model Context Protocol) server that exposes
+**uxarray-mcp-server** is a multi-protocol server that exposes
[UXarray](https://uxarray.readthedocs.io/) — a library for working with
-unstructured climate and atmospheric meshes — to AI agents such as Claude.
+unstructured climate and atmospheric meshes — to AI agents and HTTP
+clients. It is powered by
+[toolregistry](https://github.com/Oaklight/ToolRegistry) +
+[toolregistry-server](https://github.com/Oaklight/toolregistry-server),
+which provide:
+
+- **MCP** (stdio / SSE / streamable HTTP) for Claude Desktop, Claude
+ Code, and any MCP-compatible client.
+- **OpenAPI / REST** (optional extra) for curl, OpenAI Assistants,
+ Anthropic Messages API, Gemini, LangChain, plain scripts.
+- **Python API** — `from uxarray_mcp.server import make_registry`.
It supports local execution and optional remote execution on HPC clusters via
[Globus Compute](https://globus-compute.readthedocs.io/).
-Public MCP tools: `get_capabilities`, `analyze_dataset`, `run_analysis`,
-`plot_dataset`, `diagnose_endpoint`, `probe_path_access`, `run_workflow`,
-`resume_workflow`, `get_status`, `get_result`, and `manage_session`.
+### Tool surface — two profiles
+
+The tool surface is built by `uxarray_mcp.registry.build_registry()`:
+
+- **`core` (default, ~27 tools)** — 11 front-door gateway tools at the
+ top level, 12 control/status tools under `session/` and `hpc/`
+ namespaces, `io-list_datasets`, and 3 prompt helpers under `prompt/`.
+ No deferred tools, no BM25 discovery. This is what clients see by
+ default when running `uxarray-mcp serve`.
+
+- **`deferred-full` (~58 loaded, ~28 visible)** — core set stays visible.
+ 30 raw implementation tools are loaded with `defer=True` so they don't
+ appear in the initial tool list. Agents discover them via
+ `discover_tools` (BM25 search), and operators promote them from the
+ admin panel.
+
+### Core tools
+
+Front-door / gateway (top level): `get_capabilities`, `analyze_dataset`,
+`run_analysis`, `plot_dataset`, `diagnose_endpoint`, `probe_path_access`,
+`run_workflow`, `resume_workflow`, `get_status`, `get_result`,
+`manage_session`.
+
+Session state (`session/`): `create_session`, `register_dataset`,
+`get_session_state`, `reset_session_state`, `get_result_handle`,
+`get_operation_status`, `list_operations`, `get_workflow_status`.
+
+HPC control (`hpc/`): `endpoint_status`, `get_execution_mode`,
+`set_execution_mode`, `validate_hpc_setup`.
+
+IO (`io/`): `list_datasets`.
+
+Prompt helpers (`prompt/`): `first_look`, `vorticity_analysis`,
+`hpc_diagnose`.
Low-level implementation functions such as `inspect_mesh`, `calculate_area`,
-`plot_mesh`, and `endpoint_status` remain importable from `uxarray_mcp.tools`
-for tests, scripts, and internal composition, but they are not registered as
-individual MCP tools.
+`plot_mesh`, and `calculate_curl` remain importable from `uxarray_mcp.tools`
+for tests, scripts, and internal composition. In `deferred-full` profile
+they are available through `discover_tools`; in `core` they are not
+registered.
Documentation: `docs/` (Sphinx, built to ReadTheDocs).
## Key design decisions
-- **Domain/tool separation** — pure computation lives in `domain/`, MCP wiring
- lives in `tools/`. The same domain functions run locally or get serialized
- and sent to an HPC worker via Globus Compute. Never put MCP, provenance, or
- I/O logic in `domain/`.
+- **Domain/tool separation** — pure computation lives in `domain/`, server
+ wiring lives in `registry.py` + `server.py`. The same domain functions
+ run locally or get serialized and sent to an HPC worker via Globus
+ Compute. Never put MCP, provenance, or I/O logic in `domain/`.
+
+- **Two-profile surface** — the core profile keeps a small, predictable
+ baseline for existing clients. New tools start in `deferred-full` and
+ get promoted when they are stable, commonly useful, documented, and
+ have clear provenance/security behavior.
+
+- **Policy tags from day one** — every tool carries `ToolTag` values
+ (`READ_ONLY`, `FILE_SYSTEM`, `NETWORK`, `SLOW`) and custom tags
+ (`experimental`, `stateful`) so downstream policy code (admin filters,
+ auth gates, audit logs) has concrete metadata.
-- **Small MCP front-door surface** — MCP clients see intent-shaped tools, not
- every implementation function. Route new analysis behavior through
- `run_analysis`, plotting through `plot_dataset`, endpoint diagnostics through
- `diagnose_endpoint`, and session state through `manage_session` /
- `get_status` / `get_result`.
+- **Prompt-as-tool** — the former `@mcp.prompt()` decorators (`first_look`,
+ `vorticity_analysis`, `hpc_diagnose`) are now regular tools under the
+ `prompt/` namespace. They return instruction text that guides the LLM
+ through a multi-step analysis. This removes the `fastmcp` dependency.
-- **Single remote execution path** — there are no separate `*_hpc` tools. The
- dispatchers accept `use_remote` and `endpoint` where remote execution is
- meaningful. When `use_remote=True` and an endpoint is unavailable, tools either
- fall back locally when the path is local/readable or raise a clear endpoint
- readiness error.
+- **Single remote execution path** — there are no separate `*_hpc` tools.
+ The dispatchers accept `use_remote` and `endpoint` where remote execution
+ is meaningful. When `use_remote=True` and an endpoint is unavailable,
+ tools either fall back locally when the path is local/readable or raise a
+ clear endpoint readiness error.
- **Provenance on everything** — every tool result must pass through
`attach_provenance()`. No tool should return a dict without `_provenance`.
@@ -67,27 +118,30 @@ Documentation: `docs/` (Sphinx, built to ReadTheDocs).
```
src/uxarray_mcp/
- server.py # FastMCP server — small public front-door surface
- provenance.py # attach_provenance() used by all tools
- domain/ # Pure computation — no MCP, no I/O
- mesh.py # Grid loading, HEALPix support
- area.py # Face area statistics
- variable.py # Variable metadata and stats
- zonal.py # Zonal mean computation
- plotting.py # render_mesh, render_variable, render_zonal_mean
- remote/ # HPC execution layer
- config.py # HPCConfig, load_config()
- agent.py # UXarrayComputeAgent (Academy + Globus Compute)
- compute_functions.py # Self-contained remote functions (no uxarray_mcp imports)
- health.py # cached endpoint status + worker probes
- tools/ # MCP tool functions
- frontdoor.py # Public MCP dispatch tools (run_analysis, plot_dataset, etc.)
- inspection.py # Core local implementation functions
- plotting.py # Visualization implementation functions
- remote_tools.py # HPC-enabled implementation wrappers
- execution_control.py # Endpoint diagnostics and mode/config helpers
- capabilities.py # get_capabilities — tool discovery
+ registry.py # build_registry() — namespace plan, tags, prompt-as-tool
+ server.py # make_registry(), make_mcp_server(), run() — multi-transport
+ cli.py # uxarray-mcp serve/setup/doctor/endpoints/install-claude
+ provenance.py # attach_provenance() used by all tools
+ domain/ # Pure computation — no MCP, no I/O
+ mesh.py # Grid loading, HEALPix support
+ area.py # Face area statistics
+ variable.py # Variable metadata and stats
+ zonal.py # Zonal mean computation
+ plotting.py # render_mesh, render_variable, render_zonal_mean
+ remote/ # HPC execution layer
+ config.py # HPCConfig, load_config()
+ agent.py # UXarrayComputeAgent (Academy + Globus Compute)
+ compute_functions.py # Self-contained remote functions (no uxarray_mcp imports)
+ health.py # cached endpoint status + worker probes
+ tools/ # Tool implementations
+ frontdoor.py # Public dispatch tools (run_analysis, plot_dataset, etc.)
+ inspection.py # Core local implementation functions
+ plotting.py # Visualization implementation functions
+ remote_tools.py # HPC-enabled implementation wrappers
+ execution_control.py # Endpoint diagnostics and mode/config helpers
+ capabilities.py # get_capabilities — tool discovery
tests/
+ test_server.py # Registry profile shape, tags, prompts, live calls
test_inspect_mesh.py
test_inspect_variable.py
test_calculate_area.py
@@ -97,32 +151,34 @@ tests/
test_capabilities.py
test_scientific_agent.py
test_execution_control.py
- test_hpc_safety.py # Pre-flight + fallback (mocked Globus SDK)
- test_remote_agent.py # Academy agent tests (requires hpc extra)
- test_server.py # Tool registration verification
-docs/ # Sphinx documentation (MyST Markdown + RST)
- release.md # Release automation notes (PyPI + conda-forge)
+ test_hpc_safety.py # Pre-flight + fallback (mocked Globus SDK)
+ test_remote_agent.py # Academy agent tests (requires hpc extra)
+evals/ # BM25 tool retrieval + schema rejection regression
+docs/ # Sphinx documentation (MyST Markdown + RST)
+ release.md # Release automation notes (PyPI + conda-forge)
scripts/
- hpc_doctor.py # CLI diagnostic tool (also exposed as ``uxarray-mcp doctor``)
- improv_endpoint.sh # Argonne Improv endpoint setup + Python 3.12 upgrade
- ucar_endpoint.sh # NCAR/Casper (UCAR) endpoint setup
- chrysalis_endpoint.sh # Argonne Chrysalis endpoint setup
- hpc_build_yac.py # Build YAC + YAXT on a Globus Compute worker
- yac_smoke_test.py # Verify worker-side YAC import + basic surface
- agentic_hpc_loop.py # Example HPC workflow script
-conda/recipe/meta.yaml # Seed recipe for conda-forge feedstock
-config.yaml.example # Template — private config is normally written by the CLI
+ hpc_doctor.py # CLI diagnostic tool (also exposed as ``uxarray-mcp doctor``)
+ improv_endpoint.sh # Argonne Improv endpoint setup + Python 3.12 upgrade
+ ucar_endpoint.sh # NCAR/Casper (UCAR) endpoint setup
+ chrysalis_endpoint.sh # Argonne Chrysalis endpoint setup
+ hpc_build_yac.py # Build YAC + YAXT on a Globus Compute worker
+ yac_smoke_test.py # Verify worker-side YAC import + basic surface
+ agentic_hpc_loop.py # Example HPC workflow script
+conda/recipe/meta.yaml # Seed recipe for conda-forge feedstock
+config.yaml.example # Template — private config is normally written by the CLI
```
## Tech stack
-- **Python** ≥ 3.11
-- **FastMCP** ≥ 3.4.0 — MCP server framework
+- **Python** ≥ 3.12, < 3.13 (pinned for Globus Compute pickle compat)
+- **toolregistry** ≥ 0.11.0 — tool registration, schema generation, policy tags
+- **toolregistry-server** ≥ 0.3.3 — MCP + OpenAPI adapters
- **UXarray** ≥ 2025.12.0 — unstructured mesh analysis
- **Matplotlib** ≥ 3.9.0 + **Holoviews** ≥ 1.19.0 — visualization
- **PyYAML** ≥ 6.0 — config file parsing
- **uv** — package management and script runner (not conda, not pip directly)
- Optional HPC: `globus-compute-sdk` ≥ 4.5.0, `academy-py` ≥ 0.3.1
+- Optional REST: `toolregistry-server[openapi]`
## Code style
@@ -134,7 +190,7 @@ config.yaml.example # Template — private config is normally written by
- **Imports**: sorted by ruff/isort. First-party = `uxarray_mcp`.
- Comments should explain *why*, not *what*.
- Use `from __future__ import annotations` in files that use `X | Y` syntax,
- since Python 3.11 is the minimum and PEP 604 union syntax needs it there.
+ since Python 3.12 is the minimum.
All checks are enforced via pre-commit — **every commit must pass
`uv run pre-commit run --all-files`**.
@@ -161,11 +217,16 @@ uv run pytest tests/test_remote_agent.py tests/test_hpc_safety.py -v
```
When to add tests:
-- Any new public MCP tool — register it in `server.py`, export it from
- `tools/__init__.py`, document it in `docs/tools.md`, and add tests.
+- Any new tool — add it to the appropriate bucket in `registry.py`
+ (`_CONTROL_TOOLS`, `_CORE_EXTRA_TOOLS`, or `_DEFERRED_TOOLS`), export
+ it from `tools/__init__.py`, document it in `docs/tools.md`, and add
+ tests. The `test_namespace_plan_covers_every_public_tool` test will
+ fail if a tool in `__all__` is not assigned to any bucket.
- Any new implementation operation — prefer adding it behind an existing
front-door tool (`run_analysis`, `plot_dataset`, `diagnose_endpoint`, or
- `manage_session`) unless there is a strong reason for a new public MCP tool.
+ `manage_session`) unless there is a strong reason for a new public tool.
+ New operations start in `_DEFERRED_TOOLS` and graduate to core via the
+ promotion path.
- Any new error path — especially file-not-found and empty-file guards.
- Any bug fix — add a test that would have caught it.
@@ -219,13 +280,14 @@ regenerate `uv.lock`.
## Common mistakes to avoid
-- Importing `mcp` or `fastmcp` in `domain/` — domain functions must be
- importable without MCP installed (they run on the remote worker).
+- Importing `mcp` or `toolregistry` in `domain/` — domain functions must be
+ importable without server dependencies installed (they run on the remote
+ worker).
- Returning a plain dict from a tool without calling `attach_provenance()`.
-- Adding a new public MCP tool when an existing front-door operation would do.
-- Forgetting to register an intentional new public MCP tool with `mcp.tool()` in
- `server.py`.
-- Forgetting to export a new tool from `tools/__init__.py`.
+- Adding a new tool to `tools/__init__.__all__` without assigning it to a
+ bucket in `registry.py` — the coverage test will catch this.
+- Adding a deferred tool without a `search_hint` in `_SEARCH_HINTS` —
+ BM25 discovery works much better with domain synonyms.
- Using `/home/...` paths on Improv when the file actually lives under
`/gpfs/fs1/home/...` — check `probe_path_access` first on a new cluster.
- Adding a `local import io` inside a function when `io` is used for testable
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 407dc5c..1d7477a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,35 @@
All notable changes are recorded here. Dates are ISO 8601 (UTC). The project
uses Semantic Versioning for public releases.
+## Unreleased
+
+### Changed
+- **Server engine**: replaced FastMCP with
+ [toolregistry](https://github.com/Oaklight/ToolRegistry) +
+ [toolregistry-server](https://github.com/Oaklight/toolregistry-server).
+ `fastmcp` is no longer a dependency.
+- **Two-profile tool surface**: `core` (~27 tools, conservative default) and
+ `deferred-full` (all tools loaded, 30 deferred behind BM25 discovery).
+- **Namespace grouping**: control tools under `session/` and `hpc/`, IO under
+ `io/`, prompts under `prompt/`.
+- **Policy tags**: every tool carries `ToolTag` metadata (`READ_ONLY`,
+ `FILE_SYSTEM`, `NETWORK`, `SLOW`) and custom tags (`experimental`,
+ `stateful`) from day one.
+
+### Added
+- `src/uxarray_mcp/registry.py` — `build_registry(profile=...)` with namespace
+ plan, policy tags, BM25 search hints, and prompt-as-tool wiring.
+- Prompt-as-tool: `first_look`, `vorticity_analysis`, `hpc_diagnose` (formerly
+ `@mcp.prompt()` decorators) are now regular tools under `prompt/` namespace.
+- CLI: `uxarray-mcp serve` now accepts `--profile`, `--transport`, `--host`,
+ `--port`.
+- Multi-transport MCP: stdio (default), SSE, streamable HTTP.
+- Optional OpenAPI/REST surface via `pip install uxarray-mcp[openapi]`.
+
+### Removed
+- `fastmcp` dependency.
+- `@mcp.prompt()` decorators (replaced by `prompt/` namespace tools).
+
## 0.1.0 — 2026-06-04
Initial public release.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bb5c97f..48d16ed 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,8 +60,8 @@ CI must be green before a PR is merged.
- Type checker: `mypy` (enforced by pre-commit).
- Annotate all new public functions.
- Comments explain *why*, not *what*.
-- No `domain/` imports of `mcp` or `fastmcp` — domain functions run on remote
- HPC workers that do not have `uxarray_mcp` installed.
+- No `domain/` imports of `mcp` or `toolregistry` — domain functions run on
+ remote HPC workers that do not have `uxarray_mcp` installed.
## Adding Dependencies
diff --git a/conda/recipe/meta.yaml b/conda/recipe/meta.yaml
index ddc3db7..b3f598a 100644
--- a/conda/recipe/meta.yaml
+++ b/conda/recipe/meta.yaml
@@ -1,6 +1,6 @@
{% set name = "uxarray-mcp" %}
-{% set version = "0.1.1" %}
-{% set python_min = "3.11" %}
+{% set version = "0.2.0" %}
+{% set python_min = "3.12" %}
package:
name: {{ name }}
@@ -27,7 +27,7 @@ requirements:
# a second output/variant after globus-compute-sdk and academy-py solver
# behavior is validated on conda-forge.
- python >={{ python_min }}
- - fastmcp >=3.4.0
+ - toolregistry-server >=0.3.3
- holoviews >=1.19.0
- matplotlib-base >=3.9.0
- pyyaml >=6.0
diff --git a/docs/architecture.md b/docs/architecture.md
index 3483965..786f4e3 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -11,8 +11,8 @@ The UXarray MCP Server is organized into three layers:
↓
- FastMCP Server
- Registers tools from uxarray_mcp.server and exposes them to the client
+ ToolRegistry Server
+ Registers tools via uxarray_mcp.registry and exposes them over MCP / OpenAPI
↓
@@ -77,7 +77,7 @@ transport itself. Globus Compute is still the actual remote execution system.
## Tools Layer (`tools/`)
-The FastMCP server exposes a small front-door tool surface. Those tools route
+The server exposes a small front-door tool surface. Those tools route
to lower-level implementation functions in `tools/`, which handle input
validation, domain calls, remote dispatch, provenance, and structured results.
diff --git a/docs/conf.py b/docs/conf.py
index 9d518b2..3241bec 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -84,4 +84,4 @@
autodoc_member_order = "bysource"
autodoc_typehints = "description"
-autodoc_mock_imports = ["fastmcp"]
+autodoc_mock_imports = []
diff --git a/pyproject.toml b/pyproject.toml
index a18a5e7..03cbc36 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "uxarray-mcp"
-version = "0.1.1"
+version = "0.2.0"
description = "MCP server for analyzing unstructured meshes with UXarray"
readme = "README.md"
keywords = ["uxarray", "mcp", "unstructured grids", "scientific computing", "globus compute"]
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
# support and broaden requires-python back to >=3.11.
requires-python = ">=3.12,<3.13"
dependencies = [
- "fastmcp>=3.4.0",
+ "toolregistry-server[mcp]>=0.3.3",
"holoviews>=1.19.0",
"matplotlib>=3.9.0",
"pyyaml>=6.0",
@@ -33,6 +33,9 @@ dependencies = [
]
[project.optional-dependencies]
+openapi = [
+ "toolregistry-server[openapi]>=0.3.3",
+]
hpc = [
"academy-py>=0.3.1",
"globus-compute-sdk>=4.5.0",
@@ -61,7 +64,7 @@ dev = [
]
[tool.mypy]
-python_version = "3.11"
+python_version = "3.12"
check_untyped_defs = true
ignore_missing_imports = true
warn_unused_ignores = true
diff --git a/src/uxarray_mcp/__init__.py b/src/uxarray_mcp/__init__.py
index d447609..6f29c56 100644
--- a/src/uxarray_mcp/__init__.py
+++ b/src/uxarray_mcp/__init__.py
@@ -1,7 +1,7 @@
"""UXarray MCP Server - AI tools for unstructured mesh analysis."""
-from uxarray_mcp.server import mcp
+from uxarray_mcp.server import make_mcp_server, make_registry
from uxarray_mcp.tools import inspect_mesh
-__all__ = ["mcp", "inspect_mesh"]
-__version__ = "0.1.1"
+__all__ = ["make_mcp_server", "make_registry", "inspect_mesh"]
+__version__ = "0.2.0"
diff --git a/src/uxarray_mcp/cli.py b/src/uxarray_mcp/cli.py
index 493a2bc..57f5c78 100644
--- a/src/uxarray_mcp/cli.py
+++ b/src/uxarray_mcp/cli.py
@@ -64,10 +64,15 @@ def _ensure_hpc_block(data: dict[str, Any]) -> dict[str, Any]:
def cmd_serve(args: argparse.Namespace) -> int:
- """Run the MCP server on stdio."""
- from uxarray_mcp.server import mcp
-
- mcp.run()
+ """Run the MCP server."""
+ from uxarray_mcp.server import run
+
+ run(
+ profile=getattr(args, "profile", "core"),
+ transport=getattr(args, "transport", "stdio"),
+ host=getattr(args, "host", "127.0.0.1"),
+ port=getattr(args, "port", 8001),
+ )
return 0
@@ -280,7 +285,25 @@ def build_parser() -> argparse.ArgumentParser:
)
sub = p.add_subparsers(dest="command", required=True)
- serve = sub.add_parser("serve", help="Run the MCP server on stdio.")
+ serve = sub.add_parser("serve", help="Run the MCP server.")
+ serve.add_argument(
+ "--profile",
+ choices=("core", "deferred-full"),
+ default="core",
+ help=(
+ "core: gateway + control + list_datasets + prompts (~27 tools). "
+ "deferred-full: also load 30 raw tools as deferred, gated "
+ "behind discover_tools / admin promotion."
+ ),
+ )
+ serve.add_argument(
+ "--transport",
+ choices=("stdio", "sse", "http"),
+ default="stdio",
+ help="MCP transport. stdio for Claude Desktop subprocess use.",
+ )
+ serve.add_argument("--host", default="127.0.0.1", help="Bind host for SSE/HTTP.")
+ serve.add_argument("--port", type=int, default=8001, help="Port for SSE/HTTP.")
serve.set_defaults(func=cmd_serve)
setup = sub.add_parser("setup", help="Write a starter user config.")
diff --git a/src/uxarray_mcp/registry.py b/src/uxarray_mcp/registry.py
new file mode 100644
index 0000000..97dbd0b
--- /dev/null
+++ b/src/uxarray_mcp/registry.py
@@ -0,0 +1,487 @@
+"""Build a ``toolregistry.ToolRegistry`` from ``uxarray_mcp.tools``.
+
+Two profiles are supported:
+
+* ``"core"`` (default) — small, predictable surface visible to LLMs.
+ Mirrors the original MCP server's 11 front-door tools, adds 12
+ control/status tools, the ``list_datasets`` discovery helper, and
+ three prompt-as-tool helpers (former ``@mcp.prompt()`` decorators).
+* ``"deferred-full"`` — loads every public function with the core set
+ enabled and 30 raw implementation tools marked ``defer=True``.
+ Includes ``discover_tools`` (BM25 search) so LLMs find deferred
+ tools by intent.
+
+Policy tags (``ToolTag`` + custom strings) are attached from day one
+so downstream policy code has concrete metadata to key off.
+
+Nothing in ``uxarray_mcp.tools``, ``uxarray_mcp.domain``, or
+``uxarray_mcp.remote`` is modified.
+"""
+
+from __future__ import annotations
+
+import inspect
+from typing import TYPE_CHECKING, Iterable, Literal
+
+from toolregistry import ToolRegistry
+from toolregistry.tool import ToolTag
+
+import uxarray_mcp.tools as _tools_mod
+
+if TYPE_CHECKING:
+ pass
+
+Profile = Literal["core", "deferred-full"]
+
+
+# ---------------------------------------------------------------------------
+# Tool inventory
+# ---------------------------------------------------------------------------
+
+# The 11 original MCP front-door tools from the pre-rewrite server.py.
+# These are the "gateway" tools — intent-shaped dispatchers that fan out
+# to the implementation pool. Kept as an explicit frozenset because the
+# set is a design decision agreed with upstream, not something to be
+# auto-discovered at runtime.
+FRONTDOOR_NAMES: frozenset[str] = frozenset(
+ {
+ "get_capabilities",
+ "analyze_dataset",
+ "run_analysis",
+ "plot_dataset",
+ "diagnose_endpoint",
+ "probe_path_access",
+ "run_workflow",
+ "resume_workflow",
+ "get_status",
+ "get_result",
+ "manage_session",
+ }
+)
+
+
+# 12 control/status tools — session + HPC infrastructure.
+_CONTROL_TOOLS: dict[str, tuple[str, ...]] = {
+ "session": (
+ "create_session",
+ "register_dataset",
+ "get_session_state",
+ "reset_session_state",
+ "get_result_handle",
+ "get_operation_status",
+ "list_operations",
+ "get_workflow_status",
+ ),
+ "hpc": (
+ "endpoint_status",
+ "get_execution_mode",
+ "set_execution_mode",
+ "validate_hpc_setup",
+ ),
+}
+
+# Core-extra: tools with no front-door equivalent that are read-only.
+_CORE_EXTRA_TOOLS: dict[str, tuple[str, ...]] = {
+ "io": ("list_datasets",),
+}
+
+# Deferred pool — loaded only in ``deferred-full``.
+_DEFERRED_TOOLS: dict[str, tuple[str, ...]] = {
+ "compute": (
+ "calculate_gradient",
+ "calculate_curl",
+ "calculate_divergence",
+ "calculate_azimuthal_mean",
+ "calculate_bias",
+ "calculate_rmse",
+ "calculate_pattern_correlation",
+ "compare_fields",
+ "calculate_temporal_mean",
+ "calculate_anomaly",
+ "calculate_ensemble_mean",
+ "calculate_ensemble_spread",
+ "calculate_area",
+ "calculate_zonal_mean",
+ ),
+ "shape": (
+ "subset_bbox",
+ "subset_polygon",
+ "extract_cross_section",
+ "remap_variable",
+ "regrid_dataset",
+ ),
+ "inspect": (
+ "inspect_mesh",
+ "inspect_variable",
+ "validate_dataset",
+ ),
+ "plot": (
+ "plot_mesh",
+ "plot_mesh_geo",
+ "plot_variable",
+ "plot_zonal_mean",
+ ),
+ "io": (
+ "export_to_netcdf",
+ "export_to_csv",
+ "write_result",
+ ),
+ "agent": ("run_scientific_agent",),
+}
+
+
+# ---------------------------------------------------------------------------
+# Prompt-as-tool helpers (formerly @mcp.prompt() decorators)
+# ---------------------------------------------------------------------------
+
+
+def first_look(path: str) -> str:
+ """Generate a step-by-step prompt for first-look mesh/dataset analysis.
+
+ Returns a text plan instructing the LLM to call ``get_capabilities``
+ and ``analyze_dataset`` in sequence and summarise the results.
+
+ Args:
+ path: Path to the mesh or dataset file.
+
+ Returns:
+ Multi-step analysis prompt as a string.
+ """
+ return (
+ f"Run a complete first-look analysis on `{path}`.\n\n"
+ "Steps:\n"
+ f'1. Call `get_capabilities` with `grid_path="{path}"` to discover '
+ "what operations apply.\n"
+ f'2. Call `analyze_dataset` with `grid_path="{path}"` to run the full '
+ "first-look pipeline.\n"
+ "3. Summarise topology, data quality issues, selected variable, area "
+ "statistics, zonal mean, plots, and recommended next steps."
+ )
+
+
+def vorticity_analysis(grid_path: str, data_path: str, u_var: str, v_var: str) -> str:
+ """Generate a multi-step analysis plan for rotation and divergence fields.
+
+ Returns instructional text (not results) that guides the LLM through
+ calling ``run_analysis`` twice and interpreting the output. Use this
+ when you need a structured walkthrough rather than a single operation.
+
+ Args:
+ grid_path: Path to the mesh grid file.
+ data_path: Path to the data file with vector components.
+ u_var: Zonal (east-west) component variable name.
+ v_var: Meridional (north-south) component variable name.
+
+ Returns:
+ Multi-step analysis plan as a string.
+ """
+ return (
+ f"Analyse vorticity and divergence for `{data_path}`.\n\n"
+ "1. Call `run_analysis` with "
+ f'operation="curl", grid_path="{grid_path}", data_path="{data_path}", '
+ f'u_variable="{u_var}", v_variable="{v_var}".\n'
+ "2. Call `run_analysis` with "
+ f'operation="divergence", grid_path="{grid_path}", '
+ f'data_path="{data_path}", u_variable="{u_var}", '
+ f'v_variable="{v_var}".\n'
+ "3. Interpret the min/max/mean/std values and identify follow-up "
+ "plots or regional subsets."
+ )
+
+
+def hpc_diagnose(endpoint: str = "") -> str:
+ """Generate a step-by-step prompt for HPC endpoint diagnosis.
+
+ Returns a text plan instructing the LLM to check endpoint status,
+ validate connectivity, and suggest corrective actions.
+
+ Args:
+ endpoint: Optional endpoint name to diagnose. Omit for default.
+
+ Returns:
+ Multi-step HPC diagnosis prompt as a string.
+ """
+ ep = f', endpoint="{endpoint}"' if endpoint else ""
+ return (
+ "Diagnose the HPC Globus Compute configuration.\n\n"
+ f'1. Call `diagnose_endpoint(action="status"{ep})` for endpoint '
+ "manager and worker status.\n"
+ f'2. Call `diagnose_endpoint(action="validate"{ep})` for SDK auth, '
+ "manager reachability, and a remote no-op probe.\n"
+ "3. Explain failures as concrete next actions: re-authenticate, "
+ "restart the endpoint, fix worker environment, or probe a path."
+ )
+
+
+_PROMPT_TOOLS: dict[str, tuple[str, ...]] = {
+ "prompt": ("first_look", "vorticity_analysis", "hpc_diagnose"),
+}
+
+# Map prompt tool names to their implementing functions (defined above
+# in this module rather than pulled from uxarray_mcp.tools).
+_PROMPT_FUNCS: dict[str, object] = {
+ "first_look": first_look,
+ "vorticity_analysis": vorticity_analysis,
+ "hpc_diagnose": hpc_diagnose,
+}
+
+
+# ---------------------------------------------------------------------------
+# Policy tags
+# ---------------------------------------------------------------------------
+
+_TAG_OVERRIDES: dict[str, tuple[set[ToolTag], set[str]]] = {
+ # Session state mutators — persist records to disk via state._write_json
+ "create_session": ({ToolTag.FILE_SYSTEM}, {"stateful"}),
+ "register_dataset": ({ToolTag.FILE_SYSTEM}, {"stateful"}),
+ "reset_session_state": ({ToolTag.FILE_SYSTEM}, {"stateful"}),
+ # Session/control read-only — read persisted records via state._read_json
+ "get_session_state": ({ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM}, set()),
+ "get_result_handle": ({ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM}, set()),
+ "get_operation_status": ({ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM}, set()),
+ "list_operations": ({ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM}, set()),
+ "get_workflow_status": ({ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM}, set()),
+ # HPC control
+ "endpoint_status": ({ToolTag.READ_ONLY, ToolTag.NETWORK}, set()),
+ # Reads config from disk; queries the Globus Compute endpoint when one
+ # is configured (check_endpoint_manager_status), so it can hit the network.
+ "get_execution_mode": (
+ {ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM, ToolTag.NETWORK},
+ set(),
+ ),
+ "validate_hpc_setup": ({ToolTag.READ_ONLY, ToolTag.NETWORK}, set()),
+ "set_execution_mode": ({ToolTag.FILE_SYSTEM}, set()),
+ # IO
+ "list_datasets": ({ToolTag.READ_ONLY, ToolTag.FILE_SYSTEM}, set()),
+ "export_to_netcdf": ({ToolTag.FILE_SYSTEM}, set()),
+ "export_to_csv": ({ToolTag.FILE_SYSTEM}, set()),
+ "write_result": ({ToolTag.FILE_SYSTEM}, set()),
+ # Experimental agent
+ "run_scientific_agent": ({ToolTag.SLOW}, {"experimental"}),
+ # Prompt tools are always read-only (they just return text)
+ "first_look": ({ToolTag.READ_ONLY}, set()),
+ "vorticity_analysis": ({ToolTag.READ_ONLY}, set()),
+ "hpc_diagnose": ({ToolTag.READ_ONLY}, set()),
+}
+
+_SLOW_TOOL_NAMES: frozenset[str] = frozenset(
+ {
+ "calculate_curl",
+ "calculate_divergence",
+ "calculate_gradient",
+ "calculate_azimuthal_mean",
+ "calculate_zonal_mean",
+ "calculate_temporal_mean",
+ "calculate_anomaly",
+ "calculate_ensemble_mean",
+ "calculate_ensemble_spread",
+ "compare_fields",
+ "calculate_bias",
+ "calculate_rmse",
+ "calculate_pattern_correlation",
+ "remap_variable",
+ "regrid_dataset",
+ "subset_polygon",
+ "extract_cross_section",
+ "plot_mesh",
+ "plot_mesh_geo",
+ "plot_variable",
+ "plot_zonal_mean",
+ }
+)
+
+
+def _default_tags_for(
+ name: str,
+ func: object,
+) -> tuple[set[ToolTag], set[str]]:
+ """Infer tags when no explicit override exists."""
+ predefined: set[ToolTag] = set()
+ custom: set[str] = set()
+ try:
+ sig = inspect.signature(func) # type: ignore[arg-type]
+ except (TypeError, ValueError):
+ sig = None
+ if sig is not None and "use_remote" in sig.parameters:
+ predefined.add(ToolTag.NETWORK)
+ if name in _SLOW_TOOL_NAMES:
+ predefined.add(ToolTag.SLOW)
+ if not predefined and not custom:
+ predefined.add(ToolTag.READ_ONLY)
+ return predefined, custom
+
+
+def _apply_tags(
+ registry: ToolRegistry,
+ registered_name: str,
+ raw_name: str,
+ func: object,
+) -> None:
+ """Apply policy tags to a freshly registered tool."""
+ tool = registry.get_tool(registered_name)
+ if tool is None or tool.metadata is None:
+ return
+ if raw_name in _TAG_OVERRIDES:
+ predefined, custom = _TAG_OVERRIDES[raw_name]
+ else:
+ predefined, custom = _default_tags_for(raw_name, func)
+ tool.metadata.tags |= predefined
+ tool.metadata.custom_tags |= custom
+
+
+# ---------------------------------------------------------------------------
+# BM25 search hints
+# ---------------------------------------------------------------------------
+
+_SEARCH_HINTS: dict[str, str] = {
+ "calculate_curl": "vorticity rotation circulation wind curl cross product compute vector field zeta",
+ "calculate_divergence": "compression expansion source sink wind divergence",
+ "calculate_gradient": "spatial derivative slope field gradient",
+ "calculate_azimuthal_mean": "radial profile cyclone storm azimuthal",
+ "calculate_zonal_mean": "latitudinal average belt zonal",
+ "calculate_temporal_mean": "time average climatology",
+ "calculate_anomaly": "deviation departure climatology",
+ "calculate_ensemble_mean": "model average multi-member",
+ "calculate_ensemble_spread": "uncertainty standard deviation members",
+ "calculate_bias": "systematic error mean difference",
+ "calculate_rmse": "root mean square error verification",
+ "calculate_pattern_correlation": "spatial similarity skill score",
+ "compare_fields": "diff two datasets verification",
+ "calculate_area": "face cell surface area",
+ "subset_bbox": "longitude latitude bounding box region",
+ "subset_polygon": "polygon region of interest mask",
+ "extract_cross_section": "transect slice latitude longitude",
+ "remap_variable": "interpolation target grid",
+ "regrid_dataset": "interpolation target grid all variables",
+ "inspect_mesh": "topology nodes faces edges grid summary",
+ "inspect_variable": "data variable metadata stats",
+ "validate_dataset": "data quality NaN Inf fill check",
+ "plot_mesh": "wireframe mesh rendering png",
+ "plot_mesh_geo": "geographic projection coastlines borders png",
+ "plot_variable": "filled contour field rendering png",
+ "plot_zonal_mean": "profile plot zonal latitude png",
+ "export_to_netcdf": "save write netcdf file disk",
+ "export_to_csv": "save write csv file disk",
+ "write_result": "save persist result handle file",
+ "run_scientific_agent": "autonomous agent workflow loop experimental",
+}
+
+
+# ---------------------------------------------------------------------------
+# Public API
+# ---------------------------------------------------------------------------
+
+
+def build_registry(
+ *,
+ profile: Profile = "core",
+ registry_name: str = "uxarray",
+) -> ToolRegistry:
+ """Build a ``ToolRegistry`` for the chosen profile.
+
+ Args:
+ profile: ``"core"`` for the small default surface (~27 tools),
+ ``"deferred-full"`` for the complete pool (core visible,
+ 30 raw tools deferred, ``discover_tools`` added).
+ registry_name: Identifier for server titles and labels.
+
+ Returns:
+ A populated ``ToolRegistry`` ready for ``RouteTable`` wrapping.
+
+ Raises:
+ ValueError: Unknown profile.
+ RuntimeError: Upstream tool surface drifted from namespace plan.
+ """
+ if profile not in ("core", "deferred-full"):
+ raise ValueError(
+ f"unknown profile {profile!r}; expected 'core' or 'deferred-full'"
+ )
+
+ registry = ToolRegistry(name=registry_name)
+ sep = registry._name_sep # noqa: SLF001
+ registered: set[str] = set()
+
+ # 1. Front-door gateway tools — top level, no namespace.
+ for raw in sorted(FRONTDOOR_NAMES):
+ func = getattr(_tools_mod, raw, None)
+ if func is None:
+ raise RuntimeError(
+ f"build_registry expects front-door tool {raw!r} but it is "
+ f"not exported from uxarray_mcp.tools."
+ )
+ registry.register(func)
+ _apply_tags(registry, raw, raw, func)
+ registered.add(raw)
+
+ # 2. Control/status tools — namespaced.
+ for ns, raw in _flatten(_CONTROL_TOOLS):
+ if raw in registered:
+ continue
+ func = getattr(_tools_mod, raw)
+ registry.register(func, namespace=ns)
+ _apply_tags(registry, f"{ns}{sep}{raw}", raw, func)
+ registered.add(raw)
+
+ # 3. Core-extra IO.
+ for ns, raw in _flatten(_CORE_EXTRA_TOOLS):
+ if raw in registered:
+ continue
+ func = getattr(_tools_mod, raw)
+ registry.register(func, namespace=ns)
+ _apply_tags(registry, f"{ns}{sep}{raw}", raw, func)
+ registered.add(raw)
+
+ # 4. Prompt-as-tool helpers.
+ for ns, raw in _flatten(_PROMPT_TOOLS):
+ func = _PROMPT_FUNCS[raw]
+ registry.register(func, namespace=ns)
+ _apply_tags(registry, f"{ns}{sep}{raw}", raw, func)
+ # Prompts don't come from uxarray_mcp.tools.__all__, track
+ # separately.
+
+ # 5. Deferred pool — only in deferred-full.
+ if profile == "deferred-full":
+ for ns, raw in _flatten(_DEFERRED_TOOLS):
+ if raw in registered:
+ continue
+ func = getattr(_tools_mod, raw)
+ registry.register(func, namespace=ns)
+ qualified = f"{ns}{sep}{raw}"
+ _apply_tags(registry, qualified, raw, func)
+ registry.update_tool_metadata(
+ qualified,
+ defer=True,
+ search_hint=_SEARCH_HINTS.get(raw, ""),
+ )
+ registered.add(raw)
+ registry.enable_tool_discovery()
+
+ _verify_coverage(registered, profile)
+ return registry
+
+
+def _flatten(
+ buckets: dict[str, tuple[str, ...]],
+) -> Iterable[tuple[str, str]]:
+ """Yield ``(namespace, raw_name)`` pairs in stable order."""
+ for ns, names in buckets.items():
+ for name in names:
+ yield ns, name
+
+
+def _verify_coverage(registered: set[str], profile: Profile) -> None:
+ """Loud check that the namespace plan matches upstream."""
+ public = set(_tools_mod.__all__)
+ if profile == "core":
+ bogus = registered - public
+ if bogus:
+ raise RuntimeError(
+ f"Bridge tried to register non-public tools: {sorted(bogus)}"
+ )
+ return
+ missing = public - registered
+ if missing:
+ raise RuntimeError(
+ f"Namespace plan out of date — {len(missing)} public tools "
+ f"unaccounted: {sorted(missing)}. Update _DEFERRED_TOOLS."
+ )
diff --git a/src/uxarray_mcp/server.py b/src/uxarray_mcp/server.py
index 1448203..af06bbb 100644
--- a/src/uxarray_mcp/server.py
+++ b/src/uxarray_mcp/server.py
@@ -1,94 +1,92 @@
-"""UXarray MCP Server - Provides mesh analysis tools for AI agents.
+"""UXarray MCP Server — multi-protocol tool surface powered by toolregistry.
-The MCP surface is intentionally small. Low-level UXarray capabilities remain
-available inside ``uxarray_mcp.tools`` for tests, scripts, and internal
-workflows, but MCP clients see intent-shaped front doors instead of dozens of
-fine-grained implementation functions.
+Replaces the previous FastMCP-based server with ``toolregistry`` +
+``toolregistry-server``. The same tool functions from
+``uxarray_mcp.tools`` are exposed, now with:
+
+- Namespace grouping (session/, hpc/, prompt/, compute/, ...)
+- Two profiles: ``core`` (conservative default) and ``deferred-full``
+ (complete pool with BM25 discovery)
+- Policy tags on every tool from day one
+- Multi-transport MCP (stdio / SSE / streamable HTTP)
+- Optional OpenAPI / REST surface from the same process
+
+Backward compatibility:
+
+- ``python -m uxarray_mcp`` and ``uxarray-mcp serve`` still start an
+ MCP stdio server with the same default tool surface.
+- Claude Desktop ``mcpServers`` snippets continue to work unchanged.
"""
-from fastmcp import FastMCP
-
-from uxarray_mcp.tools import (
- analyze_dataset,
- diagnose_endpoint,
- get_capabilities,
- get_result,
- get_status,
- manage_session,
- plot_dataset,
- probe_path_access,
- resume_workflow,
- run_analysis,
- run_workflow,
+from __future__ import annotations
+
+import asyncio
+from typing import TYPE_CHECKING, Literal
+
+from toolregistry_server import RouteTable
+from toolregistry_server.mcp import (
+ create_mcp_server,
+ run_sse,
+ run_stdio,
+ run_streamable_http,
)
-mcp = FastMCP("uxarray-mcp-server")
-
-# Discovery and first-look analysis.
-mcp.tool()(get_capabilities)
-mcp.tool()(analyze_dataset)
-
-# Intent-shaped operation dispatch. These tools fan out to the lower-level
-# analysis, plotting, state, and diagnostic functions.
-mcp.tool()(run_analysis)
-mcp.tool()(plot_dataset)
-mcp.tool()(diagnose_endpoint)
-mcp.tool()(probe_path_access)
-
-# Stateful workflow/session front doors.
-mcp.tool()(run_workflow)
-mcp.tool()(resume_workflow)
-mcp.tool()(get_status)
-mcp.tool()(get_result)
-mcp.tool()(manage_session)
-
-
-@mcp.prompt()
-def first_look(path: str) -> str:
- """Run the full first-look analysis pipeline on a mesh or dataset."""
- return (
- f"Run a complete first-look analysis on `{path}`.\n\n"
- "Steps:\n"
- f'1. Call `get_capabilities` with `grid_path="{path}"` to discover '
- "what operations apply.\n"
- f'2. Call `analyze_dataset` with `grid_path="{path}"` to run the full '
- "first-look pipeline.\n"
- "3. Summarise topology, data quality issues, selected variable, area "
- "statistics, zonal mean, plots, and recommended next steps."
- )
-
-
-@mcp.prompt()
-def vorticity_analysis(grid_path: str, data_path: str, u_var: str, v_var: str) -> str:
- """Compute and interpret relative vorticity and wind divergence."""
- return (
- f"Analyse vorticity and divergence for `{data_path}`.\n\n"
- "1. Call `run_analysis` with "
- f'operation="curl", grid_path="{grid_path}", data_path="{data_path}", '
- f'u_variable="{u_var}", v_variable="{v_var}".\n'
- "2. Call `run_analysis` with "
- f'operation="divergence", grid_path="{grid_path}", '
- f'data_path="{data_path}", u_variable="{u_var}", '
- f'v_variable="{v_var}".\n'
- "3. Interpret the min/max/mean/std values and identify follow-up "
- "plots or regional subsets."
- )
-
-
-@mcp.prompt()
-def hpc_diagnose(endpoint: str = "") -> str:
- """Diagnose the HPC endpoint connection and configuration."""
- ep = f', endpoint="{endpoint}"' if endpoint else ""
- return (
- "Diagnose the HPC Globus Compute configuration.\n\n"
- f'1. Call `diagnose_endpoint(action="status"{ep})` for endpoint '
- "manager and worker status.\n"
- f'2. Call `diagnose_endpoint(action="validate"{ep})` for SDK auth, '
- "manager reachability, and a remote no-op probe.\n"
- "3. Explain failures as concrete next actions: re-authenticate, "
- "restart the endpoint, fix worker environment, or probe a path."
- )
+from uxarray_mcp.registry import Profile, build_registry
+
+if TYPE_CHECKING:
+ from mcp.server.lowlevel import Server
+ from toolregistry import ToolRegistry
+
+Transport = Literal["stdio", "sse", "http"]
+
+
+def make_registry(
+ *,
+ profile: Profile = "core",
+) -> "ToolRegistry":
+ """Build the tool registry for the requested profile.
+
+ This is the single source of truth for the tool surface. CLI,
+ server entry points, and tests all call through here.
+ """
+ return build_registry(profile=profile)
+
+
+def make_mcp_server(
+ *,
+ profile: Profile = "core",
+) -> "Server":
+ """Build a configured MCP server ready for any transport."""
+ registry = make_registry(profile=profile)
+ route_table = RouteTable(registry)
+ return create_mcp_server(route_table, name="uxarray-mcp-server")
+
+
+def run(
+ *,
+ profile: Profile = "core",
+ transport: Transport = "stdio",
+ host: str = "127.0.0.1",
+ port: int = 8001,
+) -> None:
+ """Run the MCP server on the requested transport.
+
+ Args:
+ profile: Tool surface profile (``"core"`` or ``"deferred-full"``).
+ transport: MCP transport (``"stdio"``, ``"sse"``, or ``"http"``).
+ host: Bind address for SSE / HTTP transports.
+ port: Port for SSE / HTTP transports.
+ """
+ server = make_mcp_server(profile=profile)
+ if transport == "stdio":
+ asyncio.run(run_stdio(server))
+ elif transport == "sse":
+ asyncio.run(run_sse(server, host=host, port=port))
+ elif transport == "http":
+ asyncio.run(run_streamable_http(server, host=host, port=port))
+ else:
+ raise ValueError(f"unknown transport {transport!r}")
if __name__ == "__main__":
- mcp.run()
+ run()
diff --git a/src/uxarray_mcp/tools/execution_control.py b/src/uxarray_mcp/tools/execution_control.py
index 052f821..3f4721a 100644
--- a/src/uxarray_mcp/tools/execution_control.py
+++ b/src/uxarray_mcp/tools/execution_control.py
@@ -142,7 +142,7 @@ def _exception_details(exc: Exception) -> Dict[str, Any]:
def _run_sync(awaitable_factory) -> Dict[str, Any]:
- """Run an async call from sync code in CLI and FastMCP contexts."""
+ """Run an async call from sync code in CLI and server contexts."""
try:
asyncio.get_running_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
diff --git a/src/uxarray_mcp/tools/remote_tools.py b/src/uxarray_mcp/tools/remote_tools.py
index 66f69c1..1acb36e 100644
--- a/src/uxarray_mcp/tools/remote_tools.py
+++ b/src/uxarray_mcp/tools/remote_tools.py
@@ -48,7 +48,7 @@ def _run_sync(async_call: Callable[[], Any]) -> Dict[str, Any]:
"""
try:
asyncio.get_running_loop()
- # Inside async context (e.g. FastMCP) — run in a new thread
+ # Inside async context (e.g. MCP server) — run in a new thread
with concurrent.futures.ThreadPoolExecutor() as pool:
return pool.submit(asyncio.run, async_call()).result()
except RuntimeError:
diff --git a/tests/test_server.py b/tests/test_server.py
index c70a1b4..14b6b51 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -1,77 +1,301 @@
+"""Verify the toolregistry-based server builds correctly.
+
+These tests replace the previous FastMCP-based assertions. They
+exercise ``uxarray_mcp.registry.build_registry`` and
+``uxarray_mcp.server.make_registry`` to confirm the tool surface
+matches the agreed design spec.
+"""
+
+from __future__ import annotations
+
import inspect
+import json
import pytest
-from uxarray_mcp.server import mcp
+from uxarray_mcp.registry import (
+ _CONTROL_TOOLS,
+ _CORE_EXTRA_TOOLS,
+ _DEFERRED_TOOLS,
+ _PROMPT_TOOLS,
+ FRONTDOOR_NAMES,
+ build_registry,
+)
+from uxarray_mcp.server import make_mcp_server, make_registry
+EXPECTED_FRONTDOOR = 11
+EXPECTED_CONTROL = 12 # 8 session + 4 hpc
+EXPECTED_CORE_EXTRA = 1 # list_datasets
+EXPECTED_PROMPTS = 3 # first_look, vorticity_analysis, hpc_diagnose
+EXPECTED_DEFERRED = 30
-async def _registered_tools():
- """Return registered tools as a {name: Tool} dict across FastMCP versions."""
- if hasattr(mcp, "get_tools"):
- return await mcp.get_tools()
- tools = await mcp.list_tools()
- return {tool.name: tool for tool in tools}
+# ---------------------------------------------------------------------------
+# Coverage invariants
+# ---------------------------------------------------------------------------
-@pytest.mark.asyncio
-async def test_public_tool_surface_is_small_and_intent_shaped():
- """Verify the MCP server exposes front doors, not every implementation tool."""
- tools = await _registered_tools()
-
- expected_tools = {
- "get_capabilities",
- "analyze_dataset",
- "run_analysis",
- "plot_dataset",
- "diagnose_endpoint",
- "probe_path_access",
- "run_workflow",
- "resume_workflow",
- "get_status",
- "get_result",
- "manage_session",
- }
- assert set(tools.keys()) == expected_tools
- assert len(tools) <= 12
+def test_frontdoor_count():
+ assert len(FRONTDOOR_NAMES) == EXPECTED_FRONTDOOR
-@pytest.mark.asyncio
-async def test_no_hpc_suffixed_tool_names():
- """The MCP surface should not expose ``*_hpc`` duplicates anymore."""
- tools = await _registered_tools()
- suffixed = [name for name in tools if name.endswith("_hpc")]
+
+def test_control_count():
+ total = sum(len(v) for v in _CONTROL_TOOLS.values())
+ assert total == EXPECTED_CONTROL
+
+
+def test_prompt_count():
+ total = sum(len(v) for v in _PROMPT_TOOLS.values())
+ assert total == EXPECTED_PROMPTS
+
+
+def test_namespace_plan_covers_every_public_tool():
+ """Every public tool is reachable through one of the buckets."""
+ import uxarray_mcp.tools as tools_mod
+
+ control = {n for v in _CONTROL_TOOLS.values() for n in v}
+ core_extra = {n for v in _CORE_EXTRA_TOOLS.values() for n in v}
+ deferred = {n for v in _DEFERRED_TOOLS.values() for n in v}
+
+ covered = FRONTDOOR_NAMES | control | core_extra | deferred
+ missing = set(tools_mod.__all__) - covered
+ assert not missing, f"uncovered public tools: {sorted(missing)}"
+
+
+def test_buckets_are_disjoint():
+ """A tool must not appear in two buckets."""
+ control = {n for v in _CONTROL_TOOLS.values() for n in v}
+ core_extra = {n for v in _CORE_EXTRA_TOOLS.values() for n in v}
+ deferred = {n for v in _DEFERRED_TOOLS.values() for n in v}
+
+ overlap = (
+ (FRONTDOOR_NAMES & control)
+ | (FRONTDOOR_NAMES & core_extra)
+ | (FRONTDOOR_NAMES & deferred)
+ | (control & core_extra)
+ | (control & deferred)
+ | (core_extra & deferred)
+ )
+ assert not overlap, f"overlapping tools: {sorted(overlap)}"
+
+
+# ---------------------------------------------------------------------------
+# Profile shape
+# ---------------------------------------------------------------------------
+
+
+def test_core_profile_shape():
+ registry = make_registry(profile="core")
+ status = registry.get_tools_status()
+ enabled = [s for s in status if s["enabled"] and not s["defer"]]
+ deferred = [s for s in status if s["enabled"] and s["defer"]]
+
+ expected = (
+ EXPECTED_FRONTDOOR + EXPECTED_CONTROL + EXPECTED_CORE_EXTRA + EXPECTED_PROMPTS
+ )
+ assert len(enabled) == expected, (
+ f"expected {expected} enabled, got {len(enabled)}: "
+ f"{sorted(s['name'] for s in enabled)}"
+ )
+ assert deferred == []
+ assert "discover_tools" not in registry.list_tools()
+
+
+def test_deferred_full_profile_shape():
+ registry = build_registry(profile="deferred-full")
+ status = registry.get_tools_status()
+ enabled_visible = [s for s in status if s["enabled"] and not s["defer"]]
+ enabled_deferred = [s for s in status if s["enabled"] and s["defer"]]
+
+ expected_visible = (
+ EXPECTED_FRONTDOOR
+ + EXPECTED_CONTROL
+ + EXPECTED_CORE_EXTRA
+ + EXPECTED_PROMPTS
+ + 1 # discover_tools
+ )
+ assert len(enabled_visible) == expected_visible
+ assert len(enabled_deferred) == EXPECTED_DEFERRED
+ assert "discover_tools" in registry.list_tools()
+
+
+def test_unknown_profile_raises():
+ with pytest.raises(ValueError):
+ build_registry(profile="bogus") # type: ignore[arg-type]
+
+
+# ---------------------------------------------------------------------------
+# Front-door surface — backward compat checks
+# ---------------------------------------------------------------------------
+
+
+def test_public_tool_surface_includes_gateway_tools():
+ """All 11 original gateway tools are present at the top level."""
+ registry = make_registry(profile="core")
+ tools = registry.list_tools()
+ for name in FRONTDOOR_NAMES:
+ assert name in tools, f"gateway tool {name!r} missing from core"
+
+
+def test_no_hpc_suffixed_tool_names():
+ """The tool surface should not expose ``*_hpc`` duplicates."""
+ registry = make_registry(profile="core")
+ suffixed = [n for n in registry.list_tools() if n.endswith("_hpc")]
assert suffixed == [], f"Unexpected _hpc-suffixed tools: {suffixed}"
-@pytest.mark.asyncio
-async def test_low_level_implementation_tools_are_not_registered():
- """The MCP surface should not expose low-level implementation verbs."""
- tools = await _registered_tools()
+def test_low_level_implementation_tools_hidden_in_core():
+ """Implementation verbs are not directly visible in core profile."""
+ registry = make_registry(profile="core")
+ tools = set(registry.list_tools())
hidden = {
"inspect_mesh",
- "inspect_variable",
"calculate_area",
- "calculate_zonal_mean",
+ "calculate_curl",
"plot_mesh",
"plot_variable",
- "plot_zonal_mean",
- "calculate_gradient",
- "calculate_curl",
- "calculate_divergence",
- "calculate_azimuthal_mean",
- "endpoint_status",
- "validate_hpc_setup",
}
- assert hidden.isdisjoint(tools)
+ assert hidden.isdisjoint(tools), f"leaked: {hidden & tools}"
-@pytest.mark.asyncio
-async def test_front_door_dispatch_tools_accept_remote_kwargs():
- """Remote execution remains available through the intent-shaped tools."""
- tools = await _registered_tools()
+def test_front_door_dispatch_tools_accept_remote_kwargs():
+ """Remote execution is available through the intent-shaped tools."""
+ registry = make_registry(profile="core")
for name in ("analyze_dataset", "run_analysis", "plot_dataset"):
- tool = tools[name]
- sig = inspect.signature(tool.fn)
+ tool = registry.get_tool(name)
+ assert tool is not None, name
+ sig = inspect.signature(tool.callable)
assert "use_remote" in sig.parameters, name
assert "endpoint" in sig.parameters, name
assert "session_id" in sig.parameters, name
+
+
+# ---------------------------------------------------------------------------
+# Prompt-as-tool
+# ---------------------------------------------------------------------------
+
+
+def test_prompt_tools_registered_in_core():
+ """Former @mcp.prompt() decorators are now prompt/ namespace tools."""
+ registry = make_registry(profile="core")
+ tools = registry.list_tools()
+ sep = registry._name_sep
+ for name in ("first_look", "vorticity_analysis", "hpc_diagnose"):
+ assert f"prompt{sep}{name}" in tools, f"prompt tool {name} missing"
+
+
+def test_prompt_tool_returns_text():
+ """Prompt tools return instruction text, not analysis results."""
+ registry = make_registry(profile="core")
+ sep = registry._name_sep
+ result = registry.execute_tool_calls(
+ [
+ {
+ "id": "call_1",
+ "function": {
+ "name": f"prompt{sep}first_look",
+ "arguments": json.dumps({"path": "/tmp/test.nc"}),
+ },
+ }
+ ]
+ )
+ text = result["call_1"]
+ assert "first-look analysis" in text.lower()
+ assert "/tmp/test.nc" in text
+
+
+# ---------------------------------------------------------------------------
+# Policy tags
+# ---------------------------------------------------------------------------
+
+
+def test_set_execution_mode_is_file_system():
+ from toolregistry.tool import ToolTag
+
+ registry = build_registry(profile="core")
+ sep = registry._name_sep
+ tool = registry.get_tool(f"hpc{sep}set_execution_mode")
+ assert tool is not None
+ assert ToolTag.FILE_SYSTEM.value in tool.metadata.all_tags
+
+
+def test_session_tools_are_file_system():
+ from toolregistry.tool import ToolTag
+
+ registry = build_registry(profile="core")
+ sep = registry._name_sep
+ # Session tools persist/read records on disk, so they must carry
+ # FILE_SYSTEM in addition to stateful / read-only.
+ for name in (
+ "create_session",
+ "register_dataset",
+ "reset_session_state",
+ "get_session_state",
+ "get_result_handle",
+ "get_operation_status",
+ "list_operations",
+ "get_workflow_status",
+ ):
+ tool = registry.get_tool(f"session{sep}{name}")
+ assert tool is not None, name
+ assert ToolTag.FILE_SYSTEM.value in tool.metadata.all_tags, name
+
+
+def test_get_execution_mode_is_file_system_and_network():
+ from toolregistry.tool import ToolTag
+
+ registry = build_registry(profile="core")
+ sep = registry._name_sep
+ # Reads config from disk and queries the Globus Compute endpoint when one
+ # is configured, so it touches both the filesystem and the network.
+ tool = registry.get_tool(f"hpc{sep}get_execution_mode")
+ assert tool is not None
+ assert ToolTag.FILE_SYSTEM.value in tool.metadata.all_tags
+ assert ToolTag.NETWORK.value in tool.metadata.all_tags
+
+
+def test_scientific_agent_is_experimental_and_deferred():
+ registry = build_registry(profile="deferred-full")
+ sep = registry._name_sep
+ tool = registry.get_tool(f"agent{sep}run_scientific_agent")
+ assert tool is not None
+ assert "experimental" in tool.metadata.custom_tags
+ assert tool.metadata.defer is True
+
+
+# ---------------------------------------------------------------------------
+# Live call
+# ---------------------------------------------------------------------------
+
+
+def test_live_call_through_registry():
+ """Side-effect-free tool round-trips through the registry."""
+ registry = make_registry(profile="core")
+ sep = registry._name_sep
+ result = registry.execute_tool_calls(
+ [
+ {
+ "id": "call_1",
+ "function": {
+ "name": f"hpc{sep}get_execution_mode",
+ "arguments": "{}",
+ },
+ }
+ ]
+ )
+ payload = json.loads(result["call_1"])
+ assert "_provenance" in payload
+ assert payload["_provenance"]["tool"] == "get_execution_mode"
+ assert payload["mode"] in {"local", "auto", "remote"}
+
+
+# ---------------------------------------------------------------------------
+# MCP server construction
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.asyncio
+async def test_mcp_server_constructs():
+ """make_mcp_server() returns a working MCP Server object."""
+ server = make_mcp_server(profile="core")
+ assert server.name == "uxarray-mcp-server"
diff --git a/tests/test_vector_calc.py b/tests/test_vector_calc.py
index 69d0d2a..e92226b 100644
--- a/tests/test_vector_calc.py
+++ b/tests/test_vector_calc.py
@@ -328,34 +328,25 @@ def test_accepts_use_remote_endpoint_session_params(self):
# ---------------------------------------------------------------------------
-@pytest.mark.asyncio
-async def test_vector_calc_operations_available_through_run_analysis():
- from uxarray_mcp.server import mcp
+def test_vector_calc_operations_available_through_run_analysis():
+ """run_analysis must advertise vector calc operations in its description."""
+ from uxarray_mcp.server import make_registry
- if hasattr(mcp, "get_tools"):
- tools = await mcp.get_tools()
- else:
- tools_list = await mcp.list_tools()
- tools = {t.name: t for t in tools_list}
-
- assert "run_analysis" in tools
+ registry = make_registry(profile="core")
+ tool = registry.get_tool("run_analysis")
+ assert tool is not None
for name in ("gradient", "curl", "divergence", "azimuthal_mean"):
- assert name in tools["run_analysis"].description
-
+ assert name in tool.description, (
+ f"{name!r} not mentioned in run_analysis description"
+ )
-@pytest.mark.asyncio
-async def test_prompts_registered():
- from uxarray_mcp.server import mcp
- if hasattr(mcp, "get_prompts"):
- prompts = await mcp.get_prompts()
- else:
- try:
- prompts_list = await mcp.list_prompts()
- prompts = {p.name: p for p in prompts_list}
- except Exception:
- pytest.skip("MCP client does not support prompt listing")
- return
+def test_prompts_registered_as_tools():
+ """Former @mcp.prompt() decorators are now prompt/ namespace tools."""
+ from uxarray_mcp.server import make_registry
+ registry = make_registry(profile="core")
+ tools = registry.list_tools()
+ sep = registry._name_sep
for name in ("first_look", "vorticity_analysis", "hpc_diagnose"):
- assert name in prompts, f"Prompt '{name}' not registered"
+ assert f"prompt{sep}{name}" in tools, f"prompt tool {name} missing"
diff --git a/uv.lock b/uv.lock
index ec0fdea..43e961f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -40,18 +40,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" },
]
-[[package]]
-name = "aiofile"
-version = "3.11.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "caio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" },
-]
-
[[package]]
name = "aiohappyeyeballs"
version = "2.6.2"
@@ -143,6 +131,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -211,19 +208,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
-[[package]]
-name = "authlib"
-version = "1.7.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
- { name = "joserfc" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" },
-]
-
[[package]]
name = "babel"
version = "2.18.0"
@@ -233,15 +217,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
]
-[[package]]
-name = "beartype"
-version = "0.22.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
-]
-
[[package]]
name = "beautifulsoup4"
version = "4.14.3"
@@ -284,19 +259,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" },
]
-[[package]]
-name = "caio"
-version = "0.9.25"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
- { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
- { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" },
- { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" },
- { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
-]
-
[[package]]
name = "cartopy"
version = "0.25.0"
@@ -523,21 +485,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
-[[package]]
-name = "cyclopts"
-version = "4.16.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "docstring-parser" },
- { name = "rich" },
- { name = "rich-rst" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cb/42/33977afb50c23345551c973fa1d25458d946ad6937373a73acd99ae21d9b/cyclopts-4.16.0.tar.gz", hash = "sha256:6a07b8ada2fa3d7611e227a98b661523c39644a50e04c92839832d9f599f398d", size = 179246, upload-time = "2026-05-24T19:31:59.563Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/45/9da25f3fe4b99e701b9a704bb6213e2d61bc44ae66294f9728574f3a607a/cyclopts-4.16.0-py3-none-any.whl", hash = "sha256:cbb9f8af92ace82c250178a3a51f5ecec1df95ab99116af3aa7140b218ccd2a1", size = 216887, upload-time = "2026-05-24T19:31:57.924Z" },
-]
-
[[package]]
name = "dask"
version = "2026.3.0"
@@ -604,24 +551,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
-[[package]]
-name = "dnspython"
-version = "2.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
-]
-
-[[package]]
-name = "docstring-parser"
-version = "0.18.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
-]
-
[[package]]
name = "docutils"
version = "0.22.4"
@@ -631,19 +560,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
]
-[[package]]
-name = "email-validator"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "dnspython" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
-]
-
[[package]]
name = "exceptiongroup"
version = "1.3.1"
@@ -657,64 +573,19 @@ wheels = [
]
[[package]]
-name = "fastmcp"
-version = "3.4.0"
+name = "fastapi"
+version = "0.137.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "fastmcp-slim", extra = ["client", "server"] },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/64/24/519739e98daf92ebc64580e9d3320649bf9a1612c029a913dd88c3474d73/fastmcp-3.4.0.tar.gz", hash = "sha256:29055fb6816f4862c615aabaf0112ae8feb8b469740db13403a0ce5b799ec1dc", size = 28754939, upload-time = "2026-06-03T02:32:40.206Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/72/9f9bbfc3a8d26870dbdbbd633cd1c6f42b8d3bec379426c760676d936e86/fastmcp-3.4.0-py3-none-any.whl", hash = "sha256:34523083d6149400a0655a8aa769eb34f85b1ce6dac6d66efb07503ebbe5f44b", size = 8017, upload-time = "2026-06-03T02:32:38.05Z" },
-]
-
-[[package]]
-name = "fastmcp-slim"
-version = "3.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "platformdirs" },
- { name = "pydantic", extra = ["email"] },
- { name = "pydantic-settings" },
- { name = "python-dotenv" },
- { name = "rich" },
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
{ name = "typing-extensions" },
+ { name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e7/b0/4da6078c2d6aa0a38a8b1ae0271e1ed400f9e2cd1b3b46e6453fb1fe2b75/fastmcp_slim-3.4.0.tar.gz", hash = "sha256:faa0ccf16e85ec4b9f79c006fed3546b866d7e6dba3f60cd32cd98e84753a496", size = 575895, upload-time = "2026-06-03T02:32:18.744Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e2/29/cc5819dc24d3daa80cdaa1aec023bf8652a70dd7fd1c96b0b225c99a7690/fastapi-0.137.2.tar.gz", hash = "sha256:b9d893bebc97dcfbdcb1917e88a292d062844ea19445a5fa4f7eb28c4baea9e3", size = 410332, upload-time = "2026-06-18T06:58:24.434Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/58/66/cc283d4efd3faf325c26f51cfb43a118270ea732e70dda509f49d80ea625/fastmcp_slim-3.4.0-py3-none-any.whl", hash = "sha256:17cd0a1535972d3748d8c2416f0826dfc86c18df7a6cbc38602373277d44baa6", size = 748849, upload-time = "2026-06-03T02:32:17.435Z" },
-]
-
-[package.optional-dependencies]
-client = [
- { name = "authlib" },
- { name = "exceptiongroup" },
- { name = "httpx" },
- { name = "mcp" },
- { name = "opentelemetry-api" },
- { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
-]
-server = [
- { name = "authlib" },
- { name = "cyclopts" },
- { name = "exceptiongroup" },
- { name = "griffelib" },
- { name = "httpx" },
- { name = "joserfc" },
- { name = "jsonref" },
- { name = "jsonschema-path" },
- { name = "mcp" },
- { name = "openapi-pydantic" },
- { name = "opentelemetry-api" },
- { name = "packaging" },
- { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
- { name = "pyperclip" },
- { name = "python-multipart" },
- { name = "pyyaml" },
- { name = "uncalled-for" },
- { name = "uvicorn" },
- { name = "watchfiles" },
- { name = "websockets" },
+ { url = "https://files.pythonhosted.org/packages/2f/ed/0c6b644e99fb5697d8bdcd36cdb47c52e77a63fc7a1514b1f03a6ecab955/fastapi-0.137.2-py3-none-any.whl", hash = "sha256:791d36261e916a98b25ac85ee591bc3db159394070f6d3d096d94fb378f60ce2", size = 122252, upload-time = "2026-06-18T06:58:26.074Z" },
]
[[package]]
@@ -863,15 +734,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/f0/67d4b279d5a19324792e29499856160d3a478e864cfec3919d23ebc88268/globus_sdk-4.7.0-py3-none-any.whl", hash = "sha256:6f2a15cff130c93ca70ddc25a8156ae636865850d9a9c9dbb7ffc365e70930e2", size = 439273, upload-time = "2026-05-20T16:20:27.683Z" },
]
-[[package]]
-name = "griffelib"
-version = "2.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" },
-]
-
[[package]]
name = "h11"
version = "0.16.0"
@@ -932,6 +794,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
+[[package]]
+name = "httptools"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
+]
+
[[package]]
name = "httpx"
version = "0.28.1"
@@ -1011,48 +888,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
-[[package]]
-name = "jaraco-classes"
-version = "3.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "more-itertools" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
-]
-
-[[package]]
-name = "jaraco-context"
-version = "6.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" },
-]
-
-[[package]]
-name = "jaraco-functools"
-version = "4.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "more-itertools" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" },
-]
-
-[[package]]
-name = "jeepney"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
-]
-
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -1074,27 +909,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
-[[package]]
-name = "joserfc"
-version = "1.6.7"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1b/cb/52e479f20804904f5df20ac4539d292dcecd1287aaa33cba1d1def1d9d8e/joserfc-1.6.7.tar.gz", hash = "sha256:6999fe89457069ecacd8cc797c88a805f83054dd883333fa0409f74b46479fd7", size = 232158, upload-time = "2026-05-23T01:46:44.069Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/e4/bcf6718b5662894c6831f46296b73cd4b1a2e90c20b6d437e20c4997388c/joserfc-1.6.7-py3-none-any.whl", hash = "sha256:9e51e4a64840aa1734a058258e80a4480e2ff2d5686e480e7c92c954a92fbe05", size = 70603, upload-time = "2026-05-23T01:46:42.129Z" },
-]
-
-[[package]]
-name = "jsonref"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
-]
-
[[package]]
name = "jsonschema"
version = "4.26.0"
@@ -1110,21 +924,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
]
-[[package]]
-name = "jsonschema-path"
-version = "0.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "pathable" },
- { name = "pyyaml" },
- { name = "referencing" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" },
-]
-
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
@@ -1137,23 +936,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
-[[package]]
-name = "keyring"
-version = "25.7.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jaraco-classes" },
- { name = "jaraco-context" },
- { name = "jaraco-functools" },
- { name = "jeepney", marker = "sys_platform == 'linux'" },
- { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
- { name = "secretstorage", marker = "sys_platform == 'linux'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
-]
-
[[package]]
name = "kiwisolver"
version = "1.5.0"
@@ -1214,6 +996,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
]
+[[package]]
+name = "llm-rosetta"
+version = "0.6.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/33/7077de444de57dc15cedfc579a6efcf6ea29fcd496b4860919cd71d4841b/llm_rosetta-0.6.10.tar.gz", hash = "sha256:50b75a65c39e8a0685549423bb2b3b12b22a1591252e00f6a8961b2d9e1e7503", size = 345772, upload-time = "2026-06-19T01:13:26.254Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/83/102932f98069395ed351bb3f3f1e0cf5bca3b9625c70c1745f1b737e8d80/llm_rosetta-0.6.10-py3-none-any.whl", hash = "sha256:811cc376a4aca94f0a6243603387247dd701e488f2c102521f84a5a6064cd333", size = 385045, upload-time = "2026-06-19T01:13:24.596Z" },
+]
+
[[package]]
name = "llvmlite"
version = "0.47.0"
@@ -1359,15 +1150,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
-[[package]]
-name = "more-itertools"
-version = "11.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
-]
-
[[package]]
name = "multidict"
version = "6.7.1"
@@ -1549,30 +1331,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
]
-[[package]]
-name = "openapi-pydantic"
-version = "0.5.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pydantic" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
-]
-
-[[package]]
-name = "opentelemetry-api"
-version = "1.42.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
-]
-
[[package]]
name = "packaging"
version = "26.2"
@@ -1665,15 +1423,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" },
]
-[[package]]
-name = "pathable"
-version = "0.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" },
-]
-
[[package]]
name = "pathspec"
version = "1.1.1"
@@ -1799,31 +1548,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
]
-[[package]]
-name = "py-key-value-aio"
-version = "0.4.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "beartype" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" },
-]
-
-[package.optional-dependencies]
-filetree = [
- { name = "aiofile" },
- { name = "anyio" },
-]
-keyring = [
- { name = "keyring" },
-]
-memory = [
- { name = "cachetools" },
-]
-
[[package]]
name = "pyarrow"
version = "24.0.0"
@@ -1875,11 +1599,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
-[package.optional-dependencies]
-email = [
- { name = "email-validator" },
-]
-
[[package]]
name = "pydantic-core"
version = "2.46.4"
@@ -1993,15 +1712,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
-[[package]]
-name = "pyperclip"
-version = "1.11.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
-]
-
[[package]]
name = "pyproj"
version = "3.7.2"
@@ -2125,15 +1835,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
]
-[[package]]
-name = "pywin32-ctypes"
-version = "0.2.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
-]
-
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -2212,19 +1913,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" },
]
-[[package]]
-name = "rich-rst"
-version = "2.0.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pygments" },
- { name = "rich" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/57/56/3191bae66b08ccc637ea8120426068bcb361cc323c96404c310886937067/rich_rst-2.0.1.tar.gz", hash = "sha256:cbe236ed0901d1ec8427cc6a50bf0a34353ba28ad014dc24def68bfe7f3b9e68", size = 300570, upload-time = "2026-05-16T00:47:57.362Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/3d/55c17d3ebdf3cd81356002afe5bef9bb8af631db2819785b6eac845b925b/rich_rst-2.0.1-py3-none-any.whl", hash = "sha256:7ee15f345ce25fa02b582c272a6cdbaf0c21243e38061cea273cff659bf3ef61", size = 272922, upload-time = "2026-05-16T00:47:55.508Z" },
-]
-
[[package]]
name = "roman-numerals"
version = "4.1.0"
@@ -2323,19 +2011,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
]
-[[package]]
-name = "secretstorage"
-version = "3.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "jeepney", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
-]
-
[[package]]
name = "shapely"
version = "2.1.2"
@@ -2557,6 +2232,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
+[[package]]
+name = "toolregistry"
+version = "0.11.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cloudpickle" },
+ { name = "llm-rosetta" },
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/3c/26b3e197f7977c1f7e39b4577eed0025d2451871907d163bd317b2ed2f10/toolregistry-0.11.1.tar.gz", hash = "sha256:c2e1df8444740b1d76b771094e94ecc7e4bcc728aef1db5c0f098beb8eaf2772", size = 276446, upload-time = "2026-05-31T09:15:45.795Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/07/a01c07c948a452dc32e2587b608638160ac9f8d236a01212f18f84a10cec/toolregistry-0.11.1-py3-none-any.whl", hash = "sha256:2b0684e10fc043cd8fbd36dc161074d107e801aa64deccf97a405a4357244c6c", size = 222599, upload-time = "2026-05-31T09:15:44.445Z" },
+]
+
+[[package]]
+name = "toolregistry-server"
+version = "0.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "toolregistry" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/43/f452d737c86ca18f234bd6ccac7a5051a7068da3c8a8e3c2715edfa5528c/toolregistry_server-0.3.3.tar.gz", hash = "sha256:1db4ad838ea8dd2fd7334433249f9cc226295aeb351e83ea63bee8c774d109f2", size = 63105, upload-time = "2026-05-31T09:16:04.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/5b/bf7eaa663be13ff6396b88d52f85fa7ebb627995c0764016b36815109565/toolregistry_server-0.3.3-py3-none-any.whl", hash = "sha256:09198b80838307e17195ecad589509402ac417fdbfe66d13e3c6051ad6057ff3", size = 50051, upload-time = "2026-05-31T09:16:03.579Z" },
+]
+
+[package.optional-dependencies]
+mcp = [
+ { name = "mcp" },
+]
+openapi = [
+ { name = "fastapi" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+
[[package]]
name = "toolz"
version = "1.1.0"
@@ -2643,15 +2353,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
]
-[[package]]
-name = "uncalled-for"
-version = "0.3.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" },
-]
-
[[package]]
name = "urllib3"
version = "2.7.0"
@@ -2674,6 +2375,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
]
+[package.optional-dependencies]
+standard = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
+ { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
+ { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
+ { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
+]
+
[[package]]
name = "uxarray"
version = "2026.4.1"
@@ -2711,13 +2437,13 @@ wheels = [
[[package]]
name = "uxarray-mcp"
-version = "0.1.1"
+version = "0.2.0"
source = { editable = "." }
dependencies = [
- { name = "fastmcp" },
{ name = "holoviews" },
{ name = "matplotlib" },
{ name = "pyyaml" },
+ { name = "toolregistry-server", extra = ["mcp"] },
{ name = "uxarray" },
]
@@ -2731,6 +2457,9 @@ hpc = [
{ name = "academy-py" },
{ name = "globus-compute-sdk" },
]
+openapi = [
+ { name = "toolregistry-server", extra = ["openapi"] },
+]
[package.dev-dependencies]
dev = [
@@ -2744,7 +2473,6 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "academy-py", marker = "extra == 'hpc'", specifier = ">=0.3.1" },
- { name = "fastmcp", specifier = ">=3.4.0" },
{ name = "globus-compute-sdk", marker = "extra == 'hpc'", specifier = ">=4.5.0" },
{ name = "holoviews", specifier = ">=1.19.0" },
{ name = "matplotlib", specifier = ">=3.9.0" },
@@ -2752,9 +2480,11 @@ requires-dist = [
{ name = "pyyaml", specifier = ">=6.0" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.0" },
{ name = "sphinx-book-theme", marker = "extra == 'docs'", specifier = ">=1.1.0" },
+ { name = "toolregistry-server", extras = ["mcp"], specifier = ">=0.3.3" },
+ { name = "toolregistry-server", extras = ["openapi"], marker = "extra == 'openapi'", specifier = ">=0.3.3" },
{ name = "uxarray", specifier = ">=2025.12.0" },
]
-provides-extras = ["hpc", "docs"]
+provides-extras = ["openapi", "hpc", "docs"]
[package.metadata.requires-dev]
dev = [