From 4311a1ba5df20c7be40c09e1dda595d93a3f1667 Mon Sep 17 00:00:00 2001 From: Peng Ding Date: Mon, 15 Jun 2026 02:30:59 -0500 Subject: [PATCH 1/5] Replace FastMCP with toolregistry-server Swap the server engine from FastMCP to toolregistry + toolregistry-server, enabling multi-transport MCP (stdio/SSE/HTTP), optional OpenAPI/REST, namespace grouping, two-profile tool surface (core / deferred-full), policy tags, and BM25 discovery. No tool implementation code (tools/, domain/, remote/) is modified. Provenance, HPC dispatch, session/workflow state, and fallback semantics are all preserved. Closes Oaklight/uxarray-mcp-server#2 --- AGENTS.md | 198 ++++++++++----- CHANGELOG.md | 29 +++ CONTRIBUTING.md | 4 +- conda/recipe/meta.yaml | 5 +- docs/conf.py | 2 +- pyproject.toml | 6 +- src/uxarray_mcp/__init__.py | 4 +- src/uxarray_mcp/cli.py | 33 ++- src/uxarray_mcp/registry.py | 481 ++++++++++++++++++++++++++++++++++++ src/uxarray_mcp/server.py | 170 +++++++------ tests/test_server.py | 290 ++++++++++++++++++---- tests/test_vector_calc.py | 41 ++- 12 files changed, 1019 insertions(+), 244 deletions(-) create mode 100644 src/uxarray_mcp/registry.py 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..c94c9a1 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 python_min = "3.12" %} package: name: {{ name }} @@ -27,7 +27,8 @@ 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 >=0.11.0 + - toolregistry-server >=0.3.3 - holoviews >=1.19.0 - matplotlib-base >=3.9.0 - pyyaml >=6.0 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..2c4c552 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,8 @@ license = { file = "LICENSE" } # support and broaden requires-python back to >=3.11. requires-python = ">=3.12,<3.13" dependencies = [ - "fastmcp>=3.4.0", + "toolregistry>=0.11.0", + "toolregistry-server[mcp]>=0.3.3", "holoviews>=1.19.0", "matplotlib>=3.9.0", "pyyaml>=6.0", @@ -33,6 +34,9 @@ dependencies = [ ] [project.optional-dependencies] +openapi = [ + "toolregistry-server[openapi]>=0.3.3", +] hpc = [ "academy-py>=0.3.1", "globus-compute-sdk>=4.5.0", diff --git a/src/uxarray_mcp/__init__.py b/src/uxarray_mcp/__init__.py index d447609..d923ca9 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"] +__all__ = ["make_mcp_server", "make_registry", "inspect_mesh"] __version__ = "0.1.1" 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..73f360c --- /dev/null +++ b/src/uxarray_mcp/registry.py @@ -0,0 +1,481 @@ +"""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 step-by-step prompt for vorticity and divergence analysis. + + Returns a text plan instructing the LLM to compute curl (relative + vorticity) and divergence and interpret the results. + + Args: + grid_path: Path to the mesh grid file. + data_path: Path to the data file with wind components. + u_var: Zonal (east-west) wind variable name. + v_var: Meridional (north-south) wind variable name. + + Returns: + Multi-step vorticity analysis prompt 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 + "create_session": (set(), {"stateful"}), + "register_dataset": (set(), {"stateful"}), + "reset_session_state": (set(), {"stateful"}), + # Session/control read-only + "get_session_state": ({ToolTag.READ_ONLY}, set()), + "get_result_handle": ({ToolTag.READ_ONLY}, set()), + "get_operation_status": ({ToolTag.READ_ONLY}, set()), + "list_operations": ({ToolTag.READ_ONLY}, set()), + "get_workflow_status": ({ToolTag.READ_ONLY}, set()), + # HPC control + "endpoint_status": ({ToolTag.READ_ONLY, ToolTag.NETWORK}, set()), + "get_execution_mode": ({ToolTag.READ_ONLY}, 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", + "calculate_divergence": "compression expansion source sink", + "calculate_gradient": "spatial derivative slope", + "calculate_azimuthal_mean": "radial profile cyclone storm", + "calculate_zonal_mean": "latitudinal average belt", + "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"server.py registers {raw!r} via mcp.tool() 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/tests/test_server.py b/tests/test_server.py index c70a1b4..307372d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,77 +1,263 @@ +"""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") + tool = registry.get_tool("hpc-set_execution_mode") + assert tool is not None + assert ToolTag.FILE_SYSTEM.value in tool.metadata.all_tags + + +def test_scientific_agent_is_experimental_and_deferred(): + registry = build_registry(profile="deferred-full") + tool = registry.get_tool("agent-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") + result = registry.execute_tool_calls( + [ + { + "id": "call_1", + "function": { + "name": "hpc-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" From d381a38e03c92c4f11582688da262226fdc47c89 Mon Sep 17 00:00:00 2001 From: Peng Ding Date: Mon, 15 Jun 2026 19:28:49 -0500 Subject: [PATCH 2/5] Address review: BM25 ranking fix, version bump, docs cleanup - Fix discover_tools ranking: enrich search hints for compute tools, reword prompt-tool docstrings to avoid keyword overlap with the actual computation tools they describe. - Bump version to 0.2.0 (pyproject.toml + __init__.py). - Clean remaining FastMCP references in docs/architecture.md and code comments in execution_control.py / remote_tools.py. --- docs/architecture.md | 6 +++--- pyproject.toml | 2 +- src/uxarray_mcp/__init__.py | 2 +- src/uxarray_mcp/registry.py | 25 +++++++++++----------- src/uxarray_mcp/tools/execution_control.py | 2 +- src/uxarray_mcp/tools/remote_tools.py | 2 +- 6 files changed, 20 insertions(+), 19 deletions(-) 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/pyproject.toml b/pyproject.toml index 2c4c552..f5d5e62 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"] diff --git a/src/uxarray_mcp/__init__.py b/src/uxarray_mcp/__init__.py index d923ca9..6f29c56 100644 --- a/src/uxarray_mcp/__init__.py +++ b/src/uxarray_mcp/__init__.py @@ -4,4 +4,4 @@ from uxarray_mcp.tools import inspect_mesh __all__ = ["make_mcp_server", "make_registry", "inspect_mesh"] -__version__ = "0.1.1" +__version__ = "0.2.0" diff --git a/src/uxarray_mcp/registry.py b/src/uxarray_mcp/registry.py index 73f360c..fb8b72c 100644 --- a/src/uxarray_mcp/registry.py +++ b/src/uxarray_mcp/registry.py @@ -160,19 +160,20 @@ def first_look(path: str) -> str: def vorticity_analysis(grid_path: str, data_path: str, u_var: str, v_var: str) -> str: - """Generate a step-by-step prompt for vorticity and divergence analysis. + """Generate a multi-step analysis plan for rotation and divergence fields. - Returns a text plan instructing the LLM to compute curl (relative - vorticity) and divergence and interpret the results. + 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 wind components. - u_var: Zonal (east-west) wind variable name. - v_var: Meridional (north-south) wind variable name. + 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 vorticity analysis prompt as a string. + Multi-step analysis plan as a string. """ return ( f"Analyse vorticity and divergence for `{data_path}`.\n\n" @@ -328,11 +329,11 @@ def _apply_tags( # --------------------------------------------------------------------------- _SEARCH_HINTS: dict[str, str] = { - "calculate_curl": "vorticity rotation circulation", - "calculate_divergence": "compression expansion source sink", - "calculate_gradient": "spatial derivative slope", - "calculate_azimuthal_mean": "radial profile cyclone storm", - "calculate_zonal_mean": "latitudinal average belt", + "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", 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: From ddd2baf1fb5a237eac6cb02e2faa7ee88ef0ce2b Mon Sep 17 00:00:00 2001 From: Peng Ding Date: Tue, 16 Jun 2026 16:08:34 -0500 Subject: [PATCH 3/5] Drop redundant toolregistry dep, fix review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove toolregistry from direct dependencies (transitive via toolregistry-server[mcp]). - Bump conda recipe version to 0.2.0. - Fix mypy python_version 3.11 → 3.12 to match requires-python. - Use registry._name_sep consistently in 3 test functions instead of hardcoded '-'. --- conda/recipe/meta.yaml | 3 +-- pyproject.toml | 3 +-- tests/test_server.py | 9 ++++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/conda/recipe/meta.yaml b/conda/recipe/meta.yaml index c94c9a1..b3f598a 100644 --- a/conda/recipe/meta.yaml +++ b/conda/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "uxarray-mcp" %} -{% set version = "0.1.1" %} +{% set version = "0.2.0" %} {% set python_min = "3.12" %} package: @@ -27,7 +27,6 @@ requirements: # a second output/variant after globus-compute-sdk and academy-py solver # behavior is validated on conda-forge. - python >={{ python_min }} - - toolregistry >=0.11.0 - toolregistry-server >=0.3.3 - holoviews >=1.19.0 - matplotlib-base >=3.9.0 diff --git a/pyproject.toml b/pyproject.toml index f5d5e62..03cbc36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ license = { file = "LICENSE" } # support and broaden requires-python back to >=3.11. requires-python = ">=3.12,<3.13" dependencies = [ - "toolregistry>=0.11.0", "toolregistry-server[mcp]>=0.3.3", "holoviews>=1.19.0", "matplotlib>=3.9.0", @@ -65,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/tests/test_server.py b/tests/test_server.py index 307372d..15425b7 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -213,14 +213,16 @@ def test_set_execution_mode_is_file_system(): from toolregistry.tool import ToolTag registry = build_registry(profile="core") - tool = registry.get_tool("hpc-set_execution_mode") + 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_scientific_agent_is_experimental_and_deferred(): registry = build_registry(profile="deferred-full") - tool = registry.get_tool("agent-run_scientific_agent") + 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 @@ -234,12 +236,13 @@ def test_scientific_agent_is_experimental_and_deferred(): 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": "hpc-get_execution_mode", + "name": f"hpc{sep}get_execution_mode", "arguments": "{}", }, } From 865a875b9241a34c77c2a0dfd935d35e119c09c0 Mon Sep 17 00:00:00 2001 From: Rajeev Jain Date: Fri, 19 Jun 2026 12:01:58 -0500 Subject: [PATCH 4/5] Regenerate uv.lock for toolregistry deps Sync uv.lock with pyproject.toml: drop fastmcp (and its transitive deps), add toolregistry + toolregistry-server, bump uxarray-mcp to 0.2.0. uv lock --check now passes. --- uv.lock | 488 +++++++++++++------------------------------------------- 1 file changed, 109 insertions(+), 379 deletions(-) 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 = [ From 47b6d2380af2c7207c3a150f1cb51f135fc8607a Mon Sep 17 00:00:00 2001 From: Rajeev Jain Date: Fri, 19 Jun 2026 12:08:15 -0500 Subject: [PATCH 5/5] Fix policy tags: FILE_SYSTEM/NETWORK on session and exec-mode tools Session create/read/reset tools persist and read records on disk via state._write_json/_read_json, so they now carry ToolTag.FILE_SYSTEM in addition to stateful / read-only. get_execution_mode reads config from disk and queries the Globus Compute endpoint when one is configured, so it gains FILE_SYSTEM and NETWORK. Also update a stale RuntimeError that referenced the removed FastMCP mcp.tool() registration. Adds tests locking in the corrected tags. --- src/uxarray_mcp/registry.py | 29 +++++++++++++++++------------ tests/test_server.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/uxarray_mcp/registry.py b/src/uxarray_mcp/registry.py index fb8b72c..97dbd0b 100644 --- a/src/uxarray_mcp/registry.py +++ b/src/uxarray_mcp/registry.py @@ -231,19 +231,24 @@ def hpc_diagnose(endpoint: str = "") -> str: # --------------------------------------------------------------------------- _TAG_OVERRIDES: dict[str, tuple[set[ToolTag], set[str]]] = { - # Session state mutators - "create_session": (set(), {"stateful"}), - "register_dataset": (set(), {"stateful"}), - "reset_session_state": (set(), {"stateful"}), - # Session/control read-only - "get_session_state": ({ToolTag.READ_ONLY}, set()), - "get_result_handle": ({ToolTag.READ_ONLY}, set()), - "get_operation_status": ({ToolTag.READ_ONLY}, set()), - "list_operations": ({ToolTag.READ_ONLY}, set()), - "get_workflow_status": ({ToolTag.READ_ONLY}, set()), + # 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()), - "get_execution_mode": ({ToolTag.READ_ONLY}, 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 @@ -401,7 +406,7 @@ def build_registry( func = getattr(_tools_mod, raw, None) if func is None: raise RuntimeError( - f"server.py registers {raw!r} via mcp.tool() but it is " + f"build_registry expects front-door tool {raw!r} but it is " f"not exported from uxarray_mcp.tools." ) registry.register(func) diff --git a/tests/test_server.py b/tests/test_server.py index 15425b7..14b6b51 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -219,6 +219,41 @@ def test_set_execution_mode_is_file_system(): 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