from __future__ import annotations import argparse import json from importlib.resources import files from typing import Sequence from hub_core.mcp import HubCoreMCPServer from hub_core.runtime.config import RuntimeSettings def build_parser(settings: RuntimeSettings | None = None) -> argparse.ArgumentParser: resolved = settings or RuntimeSettings.from_env() parser = argparse.ArgumentParser(prog="hub-core", description="Hub Core runtime commands") commands = parser.add_subparsers(dest="command", required=True) api = commands.add_parser("api", help="Run the HTTP API and named ports") api.add_argument("--host", default=resolved.api_host) api.add_argument("--port", type=int, default=resolved.api_port) mcp = commands.add_parser("mcp", help="Run the Hub Core MCP process") mcp.add_argument("--host", default=resolved.mcp_host) mcp.add_argument("--port", type=int, default=resolved.mcp_port) mcp.add_argument("--transport", default=resolved.mcp_transport) mcp.add_argument("--api-base", default=resolved.api_base) migrate = commands.add_parser("migrate", help="Run packaged Alembic migrations") migrate.add_argument("revision", nargs="?", default="head") migrate.add_argument("--database-url", default=resolved.database_url) conformance = commands.add_parser( "conformance", help="Run the implemented Tier 2/3 profile against an HTTP runtime", ) conformance.add_argument("--base-url", default=resolved.api_base) conformance.add_argument("--timeout", type=float, default=10.0) conformance.add_argument("--json", action="store_true", dest="as_json") return parser def main(argv: Sequence[str] | None = None) -> int: settings = RuntimeSettings.from_env() args = build_parser(settings).parse_args(argv) if args.command == "api": _run_api(args.host, args.port) return 0 if args.command == "mcp": _run_mcp(args.host, args.port, args.transport, args.api_base) return 0 if args.command == "migrate": if not args.database_url: raise SystemExit("hub-core migrate requires --database-url or HUB_CORE_DATABASE_URL") _run_migrations(args.database_url, args.revision) return 0 if args.command == "conformance": return _run_conformance(args.base_url, args.timeout, args.as_json) raise AssertionError(f"Unhandled command {args.command}") def _run_api(host: str, port: int) -> None: import uvicorn uvicorn.run("hub_core.runtime.app:app", host=host, port=port) def _run_mcp(host: str, port: int, transport: str, api_base: str) -> None: server = HubCoreMCPServer(name="hub-core", api_base=api_base) server.mcp.run(transport=transport, host=host, port=port) def _run_migrations(database_url: str, revision: str) -> None: from alembic import command from alembic.config import Config migration_root = files("hub_core.migrations") config = Config() config.set_main_option("script_location", str(migration_root)) config.set_main_option("sqlalchemy.url", _sync_database_url(database_url)) command.upgrade(config, revision) def _run_conformance(base_url: str, timeout: float, as_json: bool) -> int: import httpx from hub_core.conformance import ConformanceHarness with httpx.Client(base_url=base_url, timeout=timeout) as target: report = ConformanceHarness(target).run() if as_json: print(json.dumps(report.to_dict(), indent=2, sort_keys=True)) else: for check in report.checks: print(f"{check.status.upper():4} Tier {check.tier} {check.check_id}: {check.summary}") print(f"{report.passed_count}/{len(report.checks)} implemented checks passed") return 0 if report.passed else 1 def _sync_database_url(database_url: str) -> str: return database_url.replace("postgresql+asyncpg://", "postgresql+psycopg2://")