diff --git a/AGENTS.md b/AGENTS.md index 2586eb5..ccaa82c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ which provide: 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`. +- **Python API** — `from uxarray_mcp.app import make_registry`. It supports local execution and optional remote execution on HPC clusters via [Globus Compute](https://globus-compute.readthedocs.io/). diff --git a/docs/api.rst b/docs/api.rst index 2d24d70..9e865e2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -86,6 +86,6 @@ State Server ------ -.. automodule:: uxarray_mcp.server +.. automodule:: uxarray_mcp.app :members: :undoc-members: diff --git a/docs/getting-started.md b/docs/getting-started.md index 60815a4..f8f69d3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -161,7 +161,7 @@ Edit the config: "run", "python", "-m", - "uxarray_mcp.server" + "uxarray_mcp" ] } } diff --git a/pyproject.toml b/pyproject.toml index 03cbc36..156c240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ license = { file = "LICENSE" } # support and broaden requires-python back to >=3.11. requires-python = ">=3.12,<3.13" dependencies = [ - "toolregistry-server[mcp]>=0.3.3", + "toolregistry-server[mcp]>=0.4.0", "holoviews>=1.19.0", "matplotlib>=3.9.0", "pyyaml>=6.0", @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] openapi = [ - "toolregistry-server[openapi]>=0.3.3", + "toolregistry-server[openapi]>=0.4.0", ] hpc = [ "academy-py>=0.3.1", diff --git a/src/uxarray_mcp/__init__.py b/src/uxarray_mcp/__init__.py index 6f29c56..cbdc1cc 100644 --- a/src/uxarray_mcp/__init__.py +++ b/src/uxarray_mcp/__init__.py @@ -1,7 +1,6 @@ """UXarray MCP Server - AI tools for unstructured mesh analysis.""" -from uxarray_mcp.server import make_mcp_server, make_registry from uxarray_mcp.tools import inspect_mesh -__all__ = ["make_mcp_server", "make_registry", "inspect_mesh"] +__all__ = ["inspect_mesh"] __version__ = "0.2.0" diff --git a/src/uxarray_mcp/app.py b/src/uxarray_mcp/app.py new file mode 100644 index 0000000..e28f9e0 --- /dev/null +++ b/src/uxarray_mcp/app.py @@ -0,0 +1,77 @@ +"""UXarray application — subclass of toolregistry_server.App. + +Provides :class:`UXarrayApp`, the central server application that builds +the UXarray tool registry and dispatches to protocol adapters (MCP, OpenAPI). + +Identity (product name, version, description) flows automatically to +MCP server name, OpenAPI title, and CLI banner. + +Example:: + + from uxarray_mcp.app import UXarrayApp + + app = UXarrayApp() + app.serve_mcp(transport="stdio", profile="core") +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from toolregistry_server import ServerIdentity +from toolregistry_server.app import App + +from . import __version__ +from .registry import Profile + +if TYPE_CHECKING: + from toolregistry import ToolRegistry + +UXARRAY_IDENTITY = ServerIdentity( + name="UXarray MCP", + version=__version__, + description="Mesh analysis tools for AI agents", +) + + +class UXarrayApp(App): + """UXarray-specific server application. + + Overrides :meth:`prepare_registry` to build the UXarray tool + registry with profile-based tool surface selection. + """ + + def __init__(self, identity: ServerIdentity | None = None) -> None: + super().__init__(identity=identity or UXARRAY_IDENTITY) + + def prepare_registry(self, **kwargs) -> ToolRegistry: + """Build the UXarray tool registry. + + Keyword Args: + profile: Tool surface profile (``"core"`` or + ``"deferred-full"``). Defaults to ``"core"``. + """ + from .registry import build_registry + + profile = kwargs.get("profile", "core") + return build_registry(profile=profile) + + +# --------------------------------------------------------------------------- +# Convenience helpers for tests and scripts +# --------------------------------------------------------------------------- + + +def make_registry(*, profile: Profile = "core") -> ToolRegistry: + """Build the tool registry for the requested profile.""" + return UXarrayApp().prepare_registry(profile=profile) + + +def make_mcp_server(*, profile: Profile = "core"): + """Build a configured MCP server ready for any transport.""" + from toolregistry_server.adapters.mcp import route_table_to_mcp_server + from toolregistry_server.route_table import RouteTable + + registry = make_registry(profile=profile) + route_table = RouteTable(registry) + return route_table_to_mcp_server(route_table, name="UXarray MCP") diff --git a/src/uxarray_mcp/cli.py b/src/uxarray_mcp/cli.py index 57f5c78..c3a4ac7 100644 --- a/src/uxarray_mcp/cli.py +++ b/src/uxarray_mcp/cli.py @@ -65,13 +65,29 @@ def _ensure_hpc_block(data: dict[str, Any]) -> dict[str, Any]: def cmd_serve(args: argparse.Namespace) -> int: """Run the MCP server.""" - from uxarray_mcp.server import run + from uxarray_mcp.app import UXarrayApp - run( - profile=getattr(args, "profile", "core"), - transport=getattr(args, "transport", "stdio"), + app = UXarrayApp() + transport = getattr(args, "transport", "stdio") + mcp_transport = "streamable-http" if transport == "http" else transport + app.serve_mcp( + transport=mcp_transport, host=getattr(args, "host", "127.0.0.1"), port=getattr(args, "port", 8001), + profile=getattr(args, "profile", "core"), + ) + return 0 + + +def cmd_openapi(args: argparse.Namespace) -> int: + """Run the OpenAPI/REST server.""" + from uxarray_mcp.app import UXarrayApp + + app = UXarrayApp() + app.serve_openapi( + host=getattr(args, "host", "0.0.0.0"), + port=getattr(args, "port", 8000), + profile=getattr(args, "profile", "core"), ) return 0 @@ -285,6 +301,7 @@ def build_parser() -> argparse.ArgumentParser: ) sub = p.add_subparsers(dest="command", required=True) + # --- serve (MCP) --- serve = sub.add_parser("serve", help="Run the MCP server.") serve.add_argument( "--profile", @@ -306,6 +323,24 @@ def build_parser() -> argparse.ArgumentParser: serve.add_argument("--port", type=int, default=8001, help="Port for SSE/HTTP.") serve.set_defaults(func=cmd_serve) + # --- openapi --- + openapi = sub.add_parser("openapi", help="Run the OpenAPI/REST server.") + openapi.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." + ), + ) + openapi.add_argument( + "--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)." + ) + openapi.add_argument("--port", type=int, default=8000, help="Port (default: 8000).") + openapi.set_defaults(func=cmd_openapi) + setup = sub.add_parser("setup", help="Write a starter user config.") setup.add_argument("--path", default=None, help="Override target path.") setup.add_argument( diff --git a/src/uxarray_mcp/server.py b/src/uxarray_mcp/server.py deleted file mode 100644 index af06bbb..0000000 --- a/src/uxarray_mcp/server.py +++ /dev/null @@ -1,92 +0,0 @@ -"""UXarray MCP Server — multi-protocol tool surface powered by toolregistry. - -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 __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, -) - -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__": - run() diff --git a/tests/test_server.py b/tests/test_server.py index 14b6b51..246a4ae 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,7 +2,7 @@ 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 +``uxarray_mcp.app.make_registry`` to confirm the tool surface matches the agreed design spec. """ @@ -13,6 +13,7 @@ import pytest +from uxarray_mcp.app import make_mcp_server, make_registry from uxarray_mcp.registry import ( _CONTROL_TOOLS, _CORE_EXTRA_TOOLS, @@ -21,7 +22,6 @@ FRONTDOOR_NAMES, build_registry, ) -from uxarray_mcp.server import make_mcp_server, make_registry EXPECTED_FRONTDOOR = 11 EXPECTED_CONTROL = 12 # 8 session + 4 hpc @@ -298,4 +298,4 @@ def test_live_call_through_registry(): 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" + assert server.name == "UXarray MCP" diff --git a/tests/test_vector_calc.py b/tests/test_vector_calc.py index e92226b..81d0d0e 100644 --- a/tests/test_vector_calc.py +++ b/tests/test_vector_calc.py @@ -330,7 +330,7 @@ def test_accepts_use_remote_endpoint_session_params(self): 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 + from uxarray_mcp.app import make_registry registry = make_registry(profile="core") tool = registry.get_tool("run_analysis") @@ -343,7 +343,7 @@ def test_vector_calc_operations_available_through_run_analysis(): def test_prompts_registered_as_tools(): """Former @mcp.prompt() decorators are now prompt/ namespace tools.""" - from uxarray_mcp.server import make_registry + from uxarray_mcp.app import make_registry registry = make_registry(profile="core") tools = registry.list_tools()