From a148b2841282f3e3120db21619d627ba3465755b Mon Sep 17 00:00:00 2001 From: Yusuke Watanabe Date: Sun, 31 May 2026 10:13:13 +0900 Subject: [PATCH 1/3] refactor(mcp): collapse umbrella MCP to one registry-mounting entrypoint (delete 17 bridges) The umbrella's MCP surface was a thick per-package layer: mcp_server.py + _mcp_tools/ (17 hand-wrap "register__tools" bridge files, themselves documented as an anti-pattern) + _mcp_resources/ (7 doc files). The registry loop already auto-mounted peers, so most bridges were redundant or stale (project.py reached into scitex_hub's moved-out _mcp and was broken). Collapse everything into a single thin coordinator package, scitex._mcp: - __init__.py THE entrypoint: builds one FastMCP, registry-mounts every non-archived/non-umbrella peer (single source of truth: scitex_dev._ecosystem ECOSYSTEM) under a brand-prefixed namespace with the historical tool-name renames/aliases, skips optional peers gracefully, exposes main()/run_server(). Extended the FastMCP path candidates with `_server` and `_mcp._server` so stats/dev now mount via the registry too. - _compat.py safe_mount / get_tools_sync (moved verbatim). - _resources*.py the 7 umbrella doc resources, consolidated. - _umbrella_tools.py + _introspect_tools.py + _notification_tools.py the genuinely umbrella-only inline tools (no peer owns them): browser, capture, docs, skills, usage, template, tunnel, introspect, notification. - _peer_extras.py surfaces needing umbrella-side adjustment beyond a plain mount: figrecipe fr_*->plt_stx_*, socialia core social_*, scitex_dev.linter, optional scitex_cloud mount. Dropped the project_* file-CRUD family: it referenced scitex.project._mcp.handlers which moved to scitex_hub and is now superseded by the registry-mounted `hub` peer (hub_project_*). Absent scitex_hub => no project tools (graceful). Net effect: 17 bridges + 7 resource files deleted; -1034 LoC; umbrella MCP is a thin peer-mounting coordinator with no per-tool maintenance. Wiring repointed in lockstep: - pyproject: scitex-mcp-server = "scitex._mcp:main" - mcp_server.py kept as a back-compat shim re-exporting scitex._mcp. - cli/mcp.py (list/doctor/start) + cli/scholar serve -> scitex._mcp; doctor checks now probe mounted-server namespaces (peer tools resolve lazily). - tests/integration/test_cross_package_imports.py: drop dead _mcp_tools/ _mcp_resources entries, add scitex._mcp. - scripts/check-extras-completeness.py + docs/sphinx skip-lists: _mcp. Tests: new tests/scitex/test_mcp_entrypoint.py asserts >0 peers mounted, brand prefix + fr_->plt_stx_ rename, crossref-local->crossref alias, umbrella-only families present, optional-peer absence doesn't crash. Also fixed two pre-existing reds in test_thin_wrapper_consistency.py by enumerating lazily- mounted peer tools in _umbrella_tool_names(). --- docs/sphinx/generate_api_docs.py | 11 +- pyproject.toml | 15 +- scripts/check-extras-completeness.py | 3 +- src/scitex/_mcp/__init__.py | 284 +++++++++++++++ src/scitex/{_mcp_tools => _mcp}/_compat.py | 6 +- src/scitex/_mcp/_introspect_tools.py | 179 ++++++++++ .../_notification_tools.py} | 40 +-- src/scitex/_mcp/_peer_extras.py | 97 +++++ .../_scholar.py => _mcp/_resources.py} | 108 ++++-- src/scitex/_mcp/_resources_text.py | 278 +++++++++++++++ src/scitex/_mcp/_umbrella_tools.py | 323 +++++++++++++++++ src/scitex/_mcp_resources/__init__.py | 39 -- src/scitex/_mcp_resources/_cheatsheet.py | 135 ------- src/scitex/_mcp_resources/_figrecipe.py | 138 ------- src/scitex/_mcp_resources/_formats.py | 102 ------ src/scitex/_mcp_resources/_modules.py | 337 ------------------ src/scitex/_mcp_resources/_session.py | 149 -------- src/scitex/_mcp_tools/__init__.py | 228 ------------ src/scitex/_mcp_tools/browser.py | 46 --- src/scitex/_mcp_tools/capture.py | 41 --- src/scitex/_mcp_tools/cloud.py | 33 -- src/scitex/_mcp_tools/dev.py | 29 -- src/scitex/_mcp_tools/docs.py | 97 ----- src/scitex/_mcp_tools/fr.py | 36 -- src/scitex/_mcp_tools/introspect.py | 241 ------------- src/scitex/_mcp_tools/linter.py | 25 -- src/scitex/_mcp_tools/project.py | 244 ------------- src/scitex/_mcp_tools/skills.py | 47 --- src/scitex/_mcp_tools/social.py | 30 -- src/scitex/_mcp_tools/stats.py | 42 --- src/scitex/_mcp_tools/template.py | 81 ----- src/scitex/_mcp_tools/tunnel.py | 66 ---- src/scitex/_mcp_tools/usage.py | 34 -- src/scitex/cli/mcp.py | 101 +++--- src/scitex/cli/scholar/__init__.py | 2 +- src/scitex/mcp_server.py | 203 +---------- .../integration/test_cross_package_imports.py | 6 +- tests/scitex/test_mcp_entrypoint.py | 146 ++++++++ tests/scitex/test_thin_wrapper_consistency.py | 28 +- 39 files changed, 1508 insertions(+), 2542 deletions(-) create mode 100755 src/scitex/_mcp/__init__.py rename src/scitex/{_mcp_tools => _mcp}/_compat.py (96%) create mode 100755 src/scitex/_mcp/_introspect_tools.py rename src/scitex/{_mcp_tools/notification.py => _mcp/_notification_tools.py} (52%) create mode 100755 src/scitex/_mcp/_peer_extras.py rename src/scitex/{_mcp_resources/_scholar.py => _mcp/_resources.py} (59%) create mode 100755 src/scitex/_mcp/_resources_text.py create mode 100755 src/scitex/_mcp/_umbrella_tools.py delete mode 100755 src/scitex/_mcp_resources/__init__.py delete mode 100755 src/scitex/_mcp_resources/_cheatsheet.py delete mode 100755 src/scitex/_mcp_resources/_figrecipe.py delete mode 100755 src/scitex/_mcp_resources/_formats.py delete mode 100755 src/scitex/_mcp_resources/_modules.py delete mode 100755 src/scitex/_mcp_resources/_session.py delete mode 100755 src/scitex/_mcp_tools/__init__.py delete mode 100755 src/scitex/_mcp_tools/browser.py delete mode 100755 src/scitex/_mcp_tools/capture.py delete mode 100755 src/scitex/_mcp_tools/cloud.py delete mode 100755 src/scitex/_mcp_tools/dev.py delete mode 100755 src/scitex/_mcp_tools/docs.py delete mode 100755 src/scitex/_mcp_tools/fr.py delete mode 100755 src/scitex/_mcp_tools/introspect.py delete mode 100755 src/scitex/_mcp_tools/linter.py delete mode 100755 src/scitex/_mcp_tools/project.py delete mode 100755 src/scitex/_mcp_tools/skills.py delete mode 100755 src/scitex/_mcp_tools/social.py delete mode 100755 src/scitex/_mcp_tools/stats.py delete mode 100755 src/scitex/_mcp_tools/template.py delete mode 100755 src/scitex/_mcp_tools/tunnel.py delete mode 100755 src/scitex/_mcp_tools/usage.py create mode 100755 tests/scitex/test_mcp_entrypoint.py mode change 100644 => 100755 tests/scitex/test_thin_wrapper_consistency.py diff --git a/docs/sphinx/generate_api_docs.py b/docs/sphinx/generate_api_docs.py index 8a07cee2d..cc2df5aae 100755 --- a/docs/sphinx/generate_api_docs.py +++ b/docs/sphinx/generate_api_docs.py @@ -27,8 +27,7 @@ SKIP_MODULES = { "_dev", "_sphinx_html", - "_mcp_resources", - "_mcp_tools", + "_mcp", "__pycache__", ".claude", "fig", # alias for plt @@ -183,10 +182,10 @@ def generate_index(modules_dir: Path, all_modules: list[str]): if not existing: continue - lines.append(f".. toctree::") - lines.append(f" :maxdepth: 2") + lines.append(".. toctree::") + lines.append(" :maxdepth: 2") lines.append(f" :caption: {category}") - lines.append(f"") + lines.append("") for m in existing: lines.append(f" {m}") lines.append("") @@ -234,7 +233,7 @@ def main(): # Generate index generate_index(modules_dir, all_modules) - print(f" Updated: index.rst") + print(" Updated: index.rst") print(f"\nDone: {created} created, {skipped} existing (skipped)") print(f"Total modules documented: {len(all_modules)}") diff --git a/pyproject.toml b/pyproject.toml index 16aadb996..8e18059b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,14 +113,13 @@ scitex = "scitex.__main__:main" # Package-management CLI (venv drift detector for mgr-pkg autonomous ops) scitex-pkg = "scitex.cli.pkg:pkg" # Unified MCP Server (all modules in one). Use `scitex serve` (preferred) -# or `scitex-mcp-server`. Each peer standalone (scitex-scholar, scitex-stats, -# scitex-capture, scitex-diagram, scitex-template, scitex-audio, scitex-plt, -# scitex-ui) ships its own per-module MCP entry point — DO NOT re-declare -# them here, the umbrella's hand-wrap layer (scitex._mcp_tools) re-exposes -# every tool on the unified server, so legacy `scitex-` shims like -# `scitex-scholar = scitex.scholar.mcp_server:main` would only collide with -# the peer's Click CLI and break shell-completion eval. -scitex-mcp-server = "scitex.mcp_server:main" +# or `scitex-mcp-server`. The umbrella's single MCP entrypoint +# (scitex._mcp) registry-mounts every peer's FastMCP server, so peer +# standalone packages (scitex-scholar, scitex-stats, scitex-capture, ...) +# need NOT re-declare per-module MCP scripts here — declaring legacy +# `scitex-` shims would only collide with each peer's Click CLI +# and break shell-completion eval. +scitex-mcp-server = "scitex._mcp:main" [project.entry-points."scitex_dev.docs"] scitex = "scitex" diff --git a/scripts/check-extras-completeness.py b/scripts/check-extras-completeness.py index 83ab34f9d..a80686930 100755 --- a/scripts/check-extras-completeness.py +++ b/scripts/check-extras-completeness.py @@ -17,8 +17,7 @@ SKIP_DIRS = { "__pycache__", "_dev", - "_mcp_resources", - "_mcp_tools", + "_mcp", "skills", } diff --git a/src/scitex/_mcp/__init__.py b/src/scitex/_mcp/__init__.py new file mode 100755 index 000000000..9ae2605ae --- /dev/null +++ b/src/scitex/_mcp/__init__.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/__init__.py +"""Single registry-mounting entrypoint for the SciTeX umbrella MCP server. + +This module is the umbrella's entire MCP surface. It is a *thin coordinator*: + +1. Build one unified FastMCP server. +2. Registry-mount EVERY non-archived, non-umbrella peer's FastMCP instance + (single source of truth: ``scitex_dev._ecosystem._core.ECOSYSTEM``), + under a brand-prefixed namespace, with the historical tool-name renames. + New tools added to a peer's ``_mcp_server`` propagate automatically — no + umbrella-side maintenance. +3. Skip optional peers that aren't installed (graceful). +4. Fold in the umbrella-only inline tools (``scitex._mcp._umbrella_tools``) + and the peer surfaces that need brand/name adjustment + (``scitex._mcp._peer_extras``), plus the umbrella documentation resources. + +There is no per-peer "register__tools" bridge layer anymore — that was +an anti-pattern. The registry loop replaces it. + +Usage: + scitex serve # stdio (Claude Desktop) + scitex serve -t sse --port 8085 # SSE (remote via SSH) + scitex serve -t http --port 8085 # HTTP (streamable) +""" + +from __future__ import annotations + +import importlib +import logging +import os +import warnings +from typing import Iterable + +# Load environment variables from SCITEX_ENV_SRC early. +from scitex.helpers import load_scitex_env + +load_scitex_env() + +from scitex_dev import try_import_optional + +from ._compat import get_tools_sync, safe_mount + +logger = logging.getLogger(__name__) + +FastMCP = try_import_optional("fastmcp", "FastMCP", pkg="scitex") +FASTMCP_AVAILABLE = FastMCP is not None + +# Suppress httplib2 deprecation warnings from system pyparsing (old API). +# Must be AFTER fastmcp import (fastmcp.__init__ resets the filter) and +# BEFORE register_all_tools (which imports socialia -> google.auth -> httplib2). +warnings.filterwarnings( + "ignore", category=DeprecationWarning, message=".*deprecated.*use.*" +) + +__all__ = [ + "mcp", + "run_server", + "main", + "register_all_tools", + "safe_mount", + "get_tools_sync", + "FASTMCP_AVAILABLE", +] + +# Canonical places a FastMCP instance might live inside a peer package. +_MCP_PATH_CANDIDATES = ( + "_mcp_server", + "mcp_server", + "_mcp.server", + "mcp.server", + "_server", + "_mcp._server", +) +_MCP_ATTR_CANDIDATES = ("mcp", "server", "app") + +# Categories the umbrella does NOT mount. +_SKIP_CATEGORIES = frozenset({"umbrella", "template"}) + +# Namespace overrides — registry's ``umbrella_subcommand`` may differ from +# the prefix consumers already know. Apply these renames so existing tool +# names (``crossref_search``, not ``crossref-local_search``) survive. +_NAMESPACE_ALIASES: dict[str, str] = { + "crossref-local": "crossref", + "openalex-local": "openalex", + "agent-container": "agent_container", +} + + +def _env_gate_key(namespace: str) -> str: + """Return the SCITEX_MCP_USE_ env-var name for a namespace.""" + return "SCITEX_MCP_USE_" + namespace.upper().replace("-", "_") + + +def _is_enabled(namespace: str) -> bool: + """Honour ``SCITEX_MCP_USE_=0`` to skip a peer mount.""" + return os.environ.get(_env_gate_key(namespace), "1") != "0" + + +def _resolve_peer_mcp(import_name: str): + """Try every canonical location for a peer's FastMCP instance.""" + try: + from fastmcp import FastMCP as _FastMCP + except ImportError: + return None + + for sub in _MCP_PATH_CANDIDATES: + try: + mod = importlib.import_module(f"{import_name}.{sub}") + except BaseException: + # BaseException: some peers ``sys.exit(1)`` at import if their + # preconditions fail; SystemExit must not kill the umbrella. + continue + for attr in _MCP_ATTR_CANDIDATES: + obj = getattr(mod, attr, None) + if isinstance(obj, _FastMCP): + return obj + return None + + +def _iter_registry() -> Iterable[tuple[str, str, str]]: + """Yield ``(pip_name, import_name, namespace)`` for every mountable peer.""" + try: + from scitex_dev._ecosystem._core import ECOSYSTEM + except ImportError: + logger.warning("scitex-dev not installed — peer MCP auto-mount disabled.") + return + + for pip_name, info in ECOSYSTEM.items(): + if info.get("archived"): + continue + if info.get("category") in _SKIP_CATEGORIES: + continue + import_name = info.get("import_name") + if not import_name: + continue + namespace = info.get("umbrella_subcommand", pip_name.removeprefix("scitex-")) + namespace = _NAMESPACE_ALIASES.get(namespace, namespace) + yield pip_name, import_name, namespace + + +def register_all_tools(mcp) -> None: + """Mount every peer's FastMCP onto the umbrella, then fold in extras. + + Order: + 1. Registry-driven ``safe_mount`` of each peer's FastMCP instance. + 2. Peer surfaces needing brand/name adjustment (figrecipe plt_stx, + socialia social_*, linter, optional cloud mount). + 3. Umbrella-only inline tools (no peer owns them). + + Each peer is gated by ``SCITEX_MCP_USE_=0`` (default enabled). + """ + mounted: set[str] = set() + + for _pip, import_name, namespace in _iter_registry(): + if not _is_enabled(namespace): + continue + peer_mcp = _resolve_peer_mcp(import_name) + if peer_mcp is None: + continue + try: + safe_mount(mcp, peer_mcp, namespace=namespace) + mounted.add(namespace) + except Exception as exc: # noqa: BLE001 — diagnostic, never fatal + logger.warning("MCP mount failed for %r: %s", namespace, exc) + + from ._peer_extras import register_peer_extras + from ._umbrella_tools import register_umbrella_tools + + register_peer_extras(mcp) + register_umbrella_tools(mcp) + + if mounted: + logger.info( + "MCP umbrella mounted %d peer servers: %s", + len(mounted), + ", ".join(sorted(mounted)), + ) + + +_INSTRUCTIONS = """\ +SciTeX: Python Framework for Scientific Research Automation (https://scitex.ai) + +IMPORTANT: When writing Python for data analysis, visualization, or research, +ALWAYS use `import scitex as stx` and leverage these core patterns: + +### @stx.session - Reproducible Experiment Tracking +```python +import scitex as stx + +@stx.session +def main(param1="default", CONFIG=stx.INJECTED, plt=stx.INJECTED, + logger=stx.INJECTED): + \"\"\"Docstring becomes --help description.\"\"\" + stx.io.save(results, "results.csv") + return 0 +``` + +### stx.io - Universal File I/O (30+ formats) +```python +stx.io.save(df, "data.csv"); stx.io.save(fig, "plot.png") # +auto CSV +data = stx.io.load("data.csv") +``` + +### stx.plt - Publication-Ready Figures (Auto CSV Export) +```python +fig, ax = stx.plt.subplots() +ax.plot_line(x, y); ax.set_xyt("X", "Y", "Title") +stx.io.save(fig, "plot.png") # plot.png + plot.csv +``` + +### stx.stats - Publication Statistics (23 tests) +```python +result = stx.stats.test_ttest_ind(g1, g2, return_as="dataframe") +``` + +## MCP Resources (Read for detailed docs): +- scitex://cheatsheet, scitex://session-tree, scitex://io-formats +- scitex://module/{io,plt,stats,scholar,session} +- scitex://plt-figrecipe + +Use introspect_* tools to explore the API: introspect_dir("scitex.stats") +""" + + +if FASTMCP_AVAILABLE: + mcp = FastMCP(name="scitex", instructions=_INSTRUCTIONS) + + register_all_tools(mcp) + + # Annotate tools with the standardized Result envelope schema. + try: + from scitex_dev.types import RESULT_SCHEMA + + for tool in get_tools_sync(mcp).values(): + if getattr(tool, "output_schema", None) is None: + tool.output_schema = RESULT_SCHEMA + except Exception: + pass # Non-critical: schema annotation is informational. + + from ._resources import register_resources + + register_resources(mcp) +else: + mcp = None + + +def run_server( + transport: str = "stdio", + host: str = "0.0.0.0", + port: int = 8085, +): + """Run the unified MCP server with transport selection.""" + if not FASTMCP_AVAILABLE: + import sys + + print("=" * 60) + print("Requires 'fastmcp' package: pip install fastmcp") + print("=" * 60) + sys.exit(1) + + if transport == "stdio": + mcp.run(transport="stdio") + elif transport == "sse": + print(f"Starting scitex MCP (SSE) on {host}:{port}") + print(f"Remote: ssh -R {port}:localhost:{port} remote-host") + mcp.run(transport="sse", host=host, port=port) + elif transport == "http": + print(f"Starting scitex MCP (HTTP) on {host}:{port}") + mcp.run(transport="streamable-http", host=host, port=port) + else: + raise ValueError(f"Unknown transport: {transport}") + + +def main(): + """Entry point for the ``scitex-mcp-server`` console script.""" + run_server(transport="stdio") + + +if __name__ == "__main__": + main() + +# EOF diff --git a/src/scitex/_mcp_tools/_compat.py b/src/scitex/_mcp/_compat.py similarity index 96% rename from src/scitex/_mcp_tools/_compat.py rename to src/scitex/_mcp/_compat.py index 6fe274dd5..31aa36f37 100755 --- a/src/scitex/_mcp_tools/_compat.py +++ b/src/scitex/_mcp/_compat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Timestamp: 2026-02-23 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_tools/_compat.py +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_compat.py """FastMCP 2.x/3.x compatibility layer. FastMCP 3.0 removed `_tool_manager` from the FastMCP object and renamed @@ -10,6 +10,8 @@ import asyncio +__all__ = ["get_tools_sync", "safe_mount"] + def get_tools_sync(mcp_server, include_mounted: bool = True) -> dict: """Get all tools as {name: Tool} dict. Works with FastMCP 2.x and 3.x. diff --git a/src/scitex/_mcp/_introspect_tools.py b/src/scitex/_mcp/_introspect_tools.py new file mode 100755 index 000000000..937cd0e5f --- /dev/null +++ b/src/scitex/_mcp/_introspect_tools.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_introspect_tools.py +"""IPython-style introspection MCP tools (scitex.introspect handlers). + +Umbrella-only family — wraps the handlers under +``scitex.introspect._mcp.handlers``. Split out of ``_umbrella_tools`` to +keep each module focused and within the line budget. +""" + +from __future__ import annotations + +import importlib +from typing import Optional + +__all__ = ["register_introspect_tools"] + + +async def _call(handler_name: str, **kwargs) -> str: + """Resolve and invoke an introspect handler through the Result envelope.""" + from scitex_dev.ecosystem import async_wrap_as_mcp + + handlers = importlib.import_module("scitex.introspect._mcp.handlers") + handler = getattr(handlers, handler_name) + return await async_wrap_as_mcp(handler, idempotent=True, **kwargs) + + +def register_introspect_tools(mcp) -> None: + """Register every introspect_* tool onto the FastMCP server.""" + + @mcp.tool() + async def introspect_signature( + dotted_path: str, + include_defaults: bool = True, + include_annotations: bool = True, + ) -> str: + """Return a function/class signature (parameters, types, defaults) by dotted import path — IPython `?` for any installed Python object. Use whenever the user asks "what's the signature of X?", "how do I call scitex.io.save?", "what args does this take?".""" + return await _call( + "q_handler", + dotted_path=dotted_path, + include_defaults=include_defaults, + include_annotations=include_annotations, + ) + + @mcp.tool() + async def introspect_source( + dotted_path: str, + max_lines: Optional[int] = None, + include_decorators: bool = True, + ) -> str: + """Return the source code of a Python object by dotted import path — IPython `??`. Use whenever the user asks "show me the source of X", "what does scitex.io.save actually do?", "read that function".""" + return await _call( + "qq_handler", + dotted_path=dotted_path, + max_lines=max_lines, + include_decorators=include_decorators, + ) + + @mcp.tool() + async def introspect_dir( + dotted_path: str, + filter: str = "public", + kind: Optional[str] = None, + include_inherited: bool = False, + ) -> str: + """List attribute names of a module / class / instance with visibility + kind filter — `dir()` on steroids. Use whenever the user asks "what's in scitex.plt?", "list methods of this class", "show public API of module X".""" + return await _call( + "dir_handler", + dotted_path=dotted_path, + filter=filter, + kind=kind, + include_inherited=include_inherited, + ) + + @mcp.tool() + async def introspect_api( + dotted_path: str, + max_depth: int = 5, + docstring: bool = False, + root_only: bool = False, + ) -> str: + """Recursively walk a package / module tree and return the full API as indented text. Use whenever the user asks "show me the whole API of scitex.stats", "map the package layout", "what does this expose?".""" + return await _call( + "list_api_handler", + dotted_path=dotted_path, + max_depth=max_depth, + docstring=docstring, + root_only=root_only, + ) + + @mcp.tool() + async def introspect_docstring(dotted_path: str, format: str = "raw") -> str: + """Return the docstring of any dotted-path object — raw, parsed into sections, or one-line summary. Use whenever the user asks "what does this function do?", "show docstring for X", "summarize this API".""" + return await _call("docstring_handler", dotted_path=dotted_path, format=format) + + @mcp.tool() + async def introspect_exports(dotted_path: str) -> str: + """Return a module's `__all__` list — the officially-exposed public API names. Use when the user asks "what's exported from scitex.stats?", "list the public API".""" + return await _call("exports_handler", dotted_path=dotted_path) + + @mcp.tool() + async def introspect_examples( + dotted_path: str, + search_paths: Optional[str] = None, + max_results: int = 10, + ) -> str: + """Grep the repo's `tests/` and `examples/` for actual call sites of an object — real usage, not just docstring examples. Use when the user asks "how do I use this function?", "show me real examples of X".""" + paths_list = ( + [p.strip() for p in search_paths.split(",")] if search_paths else None + ) + return await _call( + "examples_handler", + dotted_path=dotted_path, + search_paths=paths_list, + max_results=max_results, + ) + + @mcp.tool() + async def introspect_class_hierarchy( + dotted_path: str, + include_builtins: bool = False, + max_depth: int = 10, + ) -> str: + """Return a class's inheritance tree — full MRO + known subclasses. Use when the user asks "what does X inherit from?", "show subclass tree of Y", "why does isinstance(Z) match?".""" + return await _call( + "class_hierarchy_handler", + dotted_path=dotted_path, + include_builtins=include_builtins, + max_depth=max_depth, + ) + + @mcp.tool() + async def introspect_type_hints( + dotted_path: str, include_extras: bool = True + ) -> str: + """Resolve every type hint on a function/class — including forward refs, generics, Union / Optional / Literal / Annotated extras. Use when the user asks "what type is this param?", "show type hints for X", "does this accept None?".""" + return await _call( + "type_hints_handler", + dotted_path=dotted_path, + include_extras=include_extras, + ) + + @mcp.tool() + async def introspect_imports(dotted_path: str, categorize: bool = True) -> str: + """AST-parse a module's source and list every import it uses — optionally grouped as stdlib / third-party / local. Use when the user asks "what does X import?", "categorize this module's dependencies".""" + return await _call( + "imports_handler", dotted_path=dotted_path, categorize=categorize + ) + + @mcp.tool() + async def introspect_dependencies( + dotted_path: str, recursive: bool = False, max_depth: int = 3 + ) -> str: + """Resolve the transitive dependency graph of a module. Use when the user asks "what depends on X?", "show me the transitive deps", "is this module pulling in pandas?".""" + return await _call( + "dependencies_handler", + dotted_path=dotted_path, + recursive=recursive, + max_depth=max_depth, + ) + + @mcp.tool() + async def introspect_call_graph( + dotted_path: str, + max_depth: int = 2, + timeout_seconds: int = 10, + internal_only: bool = True, + ) -> str: + """Build a call graph rooted at a function — which other functions it calls, recursively to `max_depth`, with a wall-clock timeout. Use when the user asks "what does this call?", "show the call graph for X", "trace how function Y reaches Z".""" + return await _call( + "call_graph_handler", + dotted_path=dotted_path, + max_depth=max_depth, + timeout_seconds=timeout_seconds, + internal_only=internal_only, + ) + + +# EOF diff --git a/src/scitex/_mcp_tools/notification.py b/src/scitex/_mcp/_notification_tools.py similarity index 52% rename from src/scitex/_mcp_tools/notification.py rename to src/scitex/_mcp/_notification_tools.py index f00bf4f2f..a47e973f4 100755 --- a/src/scitex/_mcp_tools/notification.py +++ b/src/scitex/_mcp/_notification_tools.py @@ -1,11 +1,21 @@ #!/usr/bin/env python3 -"""Notification module tools for FastMCP unified server.""" +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_notification_tools.py +"""Notification MCP tools (scitex_notification handlers). + +Umbrella-only family — wraps the multi-backend notifier so the unified +server exposes notify / call / sms / backends / config tools. +""" + +from __future__ import annotations from typing import Optional +__all__ = ["register_notification_tools"] + def register_notification_tools(mcp) -> None: - """Register notification tools with FastMCP server.""" + """Register every notification_* tool onto the FastMCP server.""" @mcp.tool() async def notification_send( @@ -16,7 +26,7 @@ async def notification_send( backends: Optional[list] = None, timeout: float = 5.0, ) -> str: - """Send an alert through any of 9 backends — audio (spoken TTS), desktop popup, emacs minibuffer, matplotlib banner, playwright browser toast, email (SMTP), webhook (HTTP POST), Telegram, Twilio phone/SMS — with automatic fallback. Drop-in replacement for `smtplib`, `plyer.notification`, `requests.post(slack_webhook)`, `python-telegram-bot`. Use whenever the user asks to "notify me", "alert me when this finishes", "beep when done", "email me the result", "ping me on Telegram", or is wiring monitoring from scripts / CI / agents. `level` (info/warning/error/critical) picks urgency routing when configured.""" + """Send an alert through any of 9 backends — audio (TTS), desktop popup, emacs minibuffer, matplotlib banner, playwright browser toast, email (SMTP), webhook (HTTP POST), Telegram, Twilio phone/SMS — with automatic fallback. Use whenever the user asks to "notify me", "alert me when this finishes", "beep when done", "email me the result", "ping me on Telegram".""" from scitex_dev.ecosystem import async_wrap_as_mcp from scitex_notification._mcp.handlers import notify_handler @@ -40,11 +50,7 @@ async def notification_call( repeat: int = 1, flow_sid: Optional[str] = None, ) -> str: - """Place an actual Twilio phone call to the user that reads `message` via TTS — bypasses DND and iOS Focus when iOS Emergency Bypass / Repeated Calls is configured (`repeat=2`). Drop-in replacement for `twilio.rest.Client().calls.create(...)` + hand-crafting TwiML. Use whenever the user asks to "call my phone", "wake me up when this fails", "escalate to a phone call on critical errors", "page me if the server dies", or wires high-urgency alerts. - - Default repeat from $SCITEX_NOTIFICATION_PHONE_CALL_N_REPEAT (default: 1). - Set to 1 if iOS Emergency Bypass is configured. Set to 2 to trigger iOS Repeated Calls bypass. - """ + """Place an actual Twilio phone call to the user that reads `message` via TTS — bypasses DND/Focus when iOS Emergency Bypass / Repeated Calls is configured (`repeat=2`). Use whenever the user asks to "call my phone", "wake me up when this fails", "escalate to a phone call on critical errors", "page me if the server dies".""" from scitex_dev.ecosystem import async_wrap_as_mcp from scitex_notification._mcp.handlers import notify_handler @@ -53,7 +59,6 @@ async def notification_call( kwargs["to_number"] = to_number if flow_sid: kwargs["flow_sid"] = flow_sid - return await async_wrap_as_mcp( notify_handler, side_effects=["phone_call: makes a phone call via Twilio"], @@ -71,14 +76,13 @@ async def notification_sms( title: Optional[str] = None, to_number: Optional[str] = None, ) -> str: - """Send an SMS to the user via Twilio — text-only alternative to `notification_call` for non-critical alerts or when the phone is on silent. Drop-in replacement for `twilio.rest.Client().messages.create(...)`. Use whenever the user asks to "text me", "SMS me the build result", "send a text when done", "ping my phone quietly".""" + """Send an SMS to the user via Twilio — text-only alternative to `notification_call`. Use whenever the user asks to "text me", "SMS me the build result", "send a text when done".""" from scitex_dev.ecosystem import async_wrap_as_mcp from scitex_notification._backends._twilio import send_sms kwargs = {} if to_number: kwargs["to_number"] = to_number - return await async_wrap_as_mcp( send_sms, side_effects=["sms: sends an SMS message via Twilio"], @@ -89,25 +93,19 @@ async def notification_sms( @mcp.tool() async def notification_backends() -> str: - """Enumerate every registered notification backend (audio, desktop, emacs, matplotlib, playwright, email, webhook, Telegram, Twilio) with reachability status — deps installed, env vars set, credentials valid. Use when the user asks "which notifiers are set up?", "why isn't my Twilio alert working?", "is email configured?", or is debugging a silent notification.""" + """Enumerate every registered notification backend with reachability status — deps installed, env vars set, credentials valid. Use when the user asks "which notifiers are set up?", "why isn't my Twilio alert working?", "is email configured?".""" from scitex_dev.ecosystem import async_wrap_as_mcp from scitex_notification._mcp.handlers import list_backends_handler - return await async_wrap_as_mcp( - list_backends_handler, - idempotent=True, - ) + return await async_wrap_as_mcp(list_backends_handler, idempotent=True) @mcp.tool() async def notification_config() -> str: - """Dump the active notification config — fallback order, per-level backend mapping (info / warning / error / critical), per-backend timeouts, Twilio / Telegram / webhook / email credentials (secrets redacted). Use when the user asks "show my notification config", "what's my fallback order?", "which backends fire for critical?", or is auditing before editing `~/.scitex/notification/config.yaml`.""" + """Dump the active notification config — fallback order, per-level backend mapping, per-backend timeouts, credentials (secrets redacted). Use when the user asks "show my notification config", "what's my fallback order?", "which backends fire for critical?".""" from scitex_dev.ecosystem import async_wrap_as_mcp from scitex_notification._mcp.handlers import get_config_handler - return await async_wrap_as_mcp( - get_config_handler, - idempotent=True, - ) + return await async_wrap_as_mcp(get_config_handler, idempotent=True) # EOF diff --git a/src/scitex/_mcp/_peer_extras.py b/src/scitex/_mcp/_peer_extras.py new file mode 100755 index 000000000..1482a637e --- /dev/null +++ b/src/scitex/_mcp/_peer_extras.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_peer_extras.py +"""Peer MCP surfaces that need umbrella-side adjustment beyond a plain mount. + +The registry loop in ``__init__`` namespace-mounts each peer's FastMCP +server verbatim. A few peers need brand-naming or tool-name adjustments +that the generic loop cannot express: + +* figrecipe ``fr_*`` tools -> re-exposed as ``plt_stx_*`` (scitex branding). +* socialia core tools -> registered as ``social_*`` (a curated subset: + post/delete/status), in addition to the full ``socialia_*`` mount. +* scitex_dev.linter MCP tools -> ``linter_*`` (engine moved out of the + archived scitex-linter package). +* scitex_cloud -> optional mount under ``cloud`` (not in the ecosystem + registry; graceful no-op when scitex-cloud is absent). + +Each helper is best-effort: a missing optional peer logs and is skipped. +""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + +__all__ = ["register_peer_extras"] + + +def register_peer_extras(mcp) -> None: + """Apply every umbrella-side peer adjustment onto the FastMCP server.""" + _register_figrecipe_plt_stx(mcp) + _register_social_core(mcp) + _register_linter(mcp) + _mount_cloud(mcp) + + +def _register_figrecipe_plt_stx(mcp) -> None: + """Re-expose figrecipe ``fr_*`` tools as ``plt_stx_*`` for scitex branding.""" + try: + from figrecipe._mcp import server as fr_mcp + except ImportError: + logger.debug("figrecipe not installed — plt_stx_* tools skipped") + return + + from ._compat import get_tools_sync + + tools = get_tools_sync(fr_mcp.mcp, include_mounted=False) + for name, tool in tools.items(): + if name.startswith("fr_"): + new_name = "plt_stx_" + name[len("fr_") :] + mcp.add_tool(tool.model_copy(update={"name": new_name})) + + +def _register_social_core(mcp) -> None: + """Register socialia's core social_* tools (post/delete/status).""" + try: + from socialia._mcp.tools import social + except ImportError: + logger.debug("socialia not installed — social_* tools skipped") + return + try: + social.register_tools(mcp) + except Exception as exc: # noqa: BLE001 — diagnostic, never fatal + logger.warning("socialia core social_* registration failed: %s", exc) + + +def _register_linter(mcp) -> None: + """Register scitex_dev.linter MCP tools (linter_* family).""" + try: + from scitex_dev.linter._mcp.tools import register_all_tools + except ImportError: + logger.debug("scitex-dev linter not installed — linter_* tools skipped") + return + try: + register_all_tools(mcp) + except Exception as exc: # noqa: BLE001 — diagnostic, never fatal + logger.warning("linter_* registration failed: %s", exc) + + +def _mount_cloud(mcp) -> None: + """Optionally mount scitex_cloud's MCP server under the ``cloud`` namespace.""" + try: + from scitex_cloud._mcp_server import mcp as cloud_mcp + except ImportError: + logger.debug("scitex-cloud not installed — cloud_* tools skipped") + return + + from ._compat import safe_mount + + try: + safe_mount(mcp, cloud_mcp, namespace="cloud") + except Exception as exc: # noqa: BLE001 — diagnostic, never fatal + logger.warning("cloud mount failed: %s", exc) + + +# EOF diff --git a/src/scitex/_mcp_resources/_scholar.py b/src/scitex/_mcp/_resources.py similarity index 59% rename from src/scitex/_mcp_resources/_scholar.py rename to src/scitex/_mcp/_resources.py index 6947d32a4..dc96068ba 100755 --- a/src/scitex/_mcp_resources/_scholar.py +++ b/src/scitex/_mcp/_resources.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 -# Timestamp: 2026-01-29 -# File: src/scitex/_mcp_resources/_scholar.py -"""Scholar library resources for FastMCP unified server. - -Provides dynamic resources for: -- scholar://library/{project} - Project paper listings -- scholar://bibtex/{filename} - BibTeX file contents +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_resources.py +"""Umbrella-only MCP documentation resources for AI agents. + +Consolidates what used to live in the per-topic ``scitex._mcp_resources`` +package (cheatsheet / session-tree / per-module docs / io-formats / +figrecipe / scholar library) into the single umbrella MCP entrypoint. +Static text lives in ``_resources_text``; this module only wires it onto +a FastMCP server. """ from __future__ import annotations @@ -15,31 +17,80 @@ from datetime import datetime from pathlib import Path -__all__ = ["register_scholar_resources"] +from ._resources_text import ( + CHEATSHEET, + FIGRECIPE_INTEGRATION, + IO_FORMATS, + MODULE_DOCS, + SESSION_TREE, +) + +__all__ = ["register_resources"] -# Directory configuration SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex")) SCITEX_SCHOLAR_DIR = SCITEX_BASE_DIR / "scholar" def _get_scholar_dir() -> Path: - """Get the scholar data directory.""" + """Get (creating if needed) the scholar data directory.""" SCITEX_SCHOLAR_DIR.mkdir(parents=True, exist_ok=True) return SCITEX_SCHOLAR_DIR -def register_scholar_resources(mcp) -> None: - """Register scholar library resources with FastMCP server.""" +def register_resources(mcp) -> None: + """Register all umbrella MCP documentation resources.""" + + @mcp.resource("scitex://cheatsheet") + def cheatsheet() -> str: + """Complete SciTeX quick reference for AI code generation.""" + return CHEATSHEET + + @mcp.resource("scitex://session-tree") + def session_tree() -> str: + """Explain the @stx.session output directory structure.""" + return SESSION_TREE + + @mcp.resource("scitex://module/io") + def module_io() -> str: + """stx.io module documentation - Universal File I/O.""" + return MODULE_DOCS["io"] + + @mcp.resource("scitex://module/plt") + def module_plt() -> str: + """stx.plt module documentation - Publication-ready figures.""" + return MODULE_DOCS["plt"] + + @mcp.resource("scitex://module/stats") + def module_stats() -> str: + """stx.stats module documentation - Statistical tests.""" + return MODULE_DOCS["stats"] + + @mcp.resource("scitex://module/scholar") + def module_scholar() -> str: + """stx.scholar module documentation - Literature management.""" + return MODULE_DOCS["scholar"] + + @mcp.resource("scitex://module/session") + def module_session() -> str: + """stx.session module documentation - Experiment tracking.""" + return MODULE_DOCS["session"] + + @mcp.resource("scitex://io-formats") + def io_formats() -> str: + """List all supported file formats for stx.io.""" + return IO_FORMATS + + @mcp.resource("scitex://plt-figrecipe") + def figrecipe_integration() -> str: + """stx.plt integration with FigRecipe for reproducible figures.""" + return FIGRECIPE_INTEGRATION @mcp.resource("scholar://library") def list_library_projects() -> str: """List all scholar library projects with paper counts.""" - scholar_dir = _get_scholar_dir() - library_dir = scholar_dir / "library" - + library_dir = _get_scholar_dir() / "library" if not library_dir.exists(): return json.dumps({"projects": [], "total": 0}, indent=2) - projects = [] for project_dir in library_dir.iterdir(): if project_dir.is_dir() and not project_dir.name.startswith("."): @@ -53,7 +104,6 @@ def list_library_projects() -> str: "uri": f"scholar://library/{project_dir.name}", } ) - return json.dumps( { "projects": projects, @@ -67,13 +117,10 @@ def list_library_projects() -> str: def get_library_project(project: str) -> str: """Get papers in a specific library project.""" library_dir = _get_scholar_dir() / "library" / project - if not library_dir.exists(): return json.dumps({"error": f"Project not found: {project}"}, indent=2) - metadata_files = list(library_dir.rglob("metadata.json")) papers = [] - for meta_file in metadata_files[:100]: try: with open(meta_file) as f: @@ -91,13 +138,8 @@ def get_library_project(project: str) -> str: ) except Exception: pass - return json.dumps( - { - "project": project, - "paper_count": len(papers), - "papers": papers, - }, + {"project": project, "paper_count": len(papers), "papers": papers}, indent=2, ) @@ -106,7 +148,6 @@ def list_bibtex_files() -> str: """List recent BibTeX files in scholar directory.""" scholar_dir = _get_scholar_dir() bib_files = [] - for bib_file in sorted( scholar_dir.rglob("*.bib"), key=lambda p: p.stat().st_mtime, @@ -121,13 +162,8 @@ def list_bibtex_files() -> str: "uri": f"scholar://bibtex/{bib_file.name}", } ) - return json.dumps( - { - "bibtex_files": bib_files, - "total": len(bib_files), - }, - indent=2, + {"bibtex_files": bib_files, "total": len(bib_files)}, indent=2 ) @mcp.resource("scholar://bibtex/{filename}") @@ -135,14 +171,10 @@ def get_bibtex_file(filename: str) -> str: """Read a BibTeX file by name.""" scholar_dir = _get_scholar_dir() bib_files = list(scholar_dir.rglob(filename)) - if not bib_files: return json.dumps({"error": f"BibTeX file not found: {filename}"}, indent=2) - with open(bib_files[0]) as f: - content = f.read() - - return content + return f.read() # EOF diff --git a/src/scitex/_mcp/_resources_text.py b/src/scitex/_mcp/_resources_text.py new file mode 100755 index 000000000..072a9dd18 --- /dev/null +++ b/src/scitex/_mcp/_resources_text.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_resources_text.py +"""Static markdown bodies for umbrella MCP documentation resources. + +Pure data module — no FastMCP coupling. The registration wiring lives in +``scitex._mcp._resources``; the entrypoint only imports that. +""" + +from __future__ import annotations + +CHEATSHEET = """\ +# SciTeX Cheatsheet for AI Agents +================================= + +## Import Pattern (ALWAYS use this) +```python +import scitex as stx +import numpy as np +``` + +## 1. @stx.session - Reproducible Experiment Tracking + +The MOST IMPORTANT pattern. Wrap your main function with @stx.session: + +```python +@stx.session +def main( + # User parameters (become CLI arguments automatically) + input_file="data.csv", # --input-file (default: data.csv) + n_samples=100, # --n-samples (default: 100) + + # INJECTED parameters (auto-provided by session) + CONFIG=stx.INJECTED, # Session config with ID, paths + plt=stx.INJECTED, # Pre-configured matplotlib + COLORS=stx.INJECTED, # Color palette + rngg=stx.INJECTED, # Seeded random generator + logger=stx.INJECTED, # Session logger +): + \"\"\"This docstring becomes --help description.\"\"\" + + data = stx.io.load(input_file) + results = process(data, n_samples) + + stx.io.save(results, "results.csv") + stx.io.save(fig, "plot.png", symlink_to="./data") + + return 0 # Exit status + +if __name__ == "__main__": + main() # CLI mode when no args passed +``` + +## 2. stx.io - Universal File I/O (30+ formats) + +```python +stx.io.save(df, "data.csv") # DataFrame -> CSV +stx.io.save(arr, "data.npy") # NumPy array +stx.io.save(obj, "data.pkl") # Any Python object +stx.io.save(fig, "plot.png") # Figure + auto CSV +data = stx.io.load("data.csv") +``` + +## 3. stx.plt - Publication-Ready Figures (Auto CSV Export) + +```python +fig, ax = stx.plt.subplots() +ax.stx_line(x, y, label="Signal") # Tracked: exports to CSV +ax.set_xyt("X axis", "Y axis", "Title") +stx.io.save(fig, "plot.png") # Saves plot.png + plot.csv +fig.close() +``` + +## 4. stx.stats - Publication Statistics (23 tests) + +```python +result = stx.stats.test_ttest_ind(group1, group2, return_as="dataframe") +result = stx.stats.test_anova(*groups, return_as="latex") +``` + +## 5. stx.scholar - Literature Management + +```bash +scitex scholar bibtex papers.bib --project myresearch --num-workers 8 +``` + +## Quick Tips + +1. ALWAYS use `import scitex as stx` +2. ALWAYS wrap main functions with `@stx.session` +3. ALWAYS use `stx.io.save()` and `stx.io.load()` for files +4. ALWAYS use `stx.plt.subplots()` for figures +5. ALWAYS use `ax.stx_*` methods for auto CSV export +6. ALWAYS return exit status (0 for success) from main +""" + +SESSION_TREE = """\ +# @stx.session Output Directory Structure +========================================== + +``` +script.py # Your script +script_out/ # Output directory (auto-created) +├── output.npy # Your saved files (ROOT level) +├── figure.png # Figures +├── figure.csv # Auto-exported plot data +├── RUNNING/ # Currently running sessions +├── FINISHED_SUCCESS/ # Completed sessions +│ └── -main/ +│ ├── CONFIGS/ +│ │ ├── CONFIG.pkl +│ │ └── CONFIG.yaml +│ └── logs/ +│ ├── stdout.log +│ └── stderr.log +└── FINISHED_FAILED/ # Failed sessions +data/ # Central navigation via symlinks +└── output.npy -> ../script_out/output.npy +``` + +## Key Points + +1. Session ID: `YYYY'Y'-MM'M'-DD'D'-HH'h'MM'm'SS's'_XXXX-funcname` +2. Files saved with `stx.io.save(obj, "filename")` go to `script_out/` ROOT +3. `symlink_to="./data"` accumulates outputs from multiple scripts +4. CONFIG (`CONFIG=stx.INJECTED`): ID, FILE, SDIR_OUT, PID, ARGS +5. YAML files in `./config/*.yaml` auto-load into CONFIG (dot access) +6. On success RUNNING -> FINISHED_SUCCESS; on error -> FINISHED_FAILED +""" + +MODULE_IO = """\ +# stx.io - Universal File I/O +============================== + +```python +stx.io.save(obj, path, **kwargs) # Save any object +stx.io.load(path, **kwargs) # Load any file +``` + +- Extension determines handler: `stx.io.save(df, "data.csv")` -> CSV +- `verbose=True` logs `SUCC: Saved to: ...` +- PNG/JPEG support embedded `metadata={...}` +- `symlink_to="./data"` creates symlinks +- Saving a figure also exports plotted data as `.csv` + +## Common Formats +- `.csv`, `.xlsx`, `.parquet` - DataFrames +- `.npy`, `.npz`, `.h5` - Arrays +- `.pkl`, `.json`, `.yaml` - Objects +- `.png`, `.jpg`, `.pdf`, `.svg` - Figures +""" + +MODULE_PLT = """\ +# stx.plt - Publication-Ready Figures +====================================== + +```python +fig, ax = stx.plt.subplots() +ax.stx_line(x, y, label="Signal") +ax.set_xyt("X axis", "Y axis", "Title") +stx.io.save(fig, "plot.png") # creates plot.png + plot.csv +fig.close() +``` + +## Tracked Methods (stx_ prefix; auto CSV export) +stx_line, stx_scatter, stx_bar, stx_errorbar, stx_hist, +stx_boxplot, stx_violinplot, stx_imshow +""" + +MODULE_STATS = """\ +# stx.stats - Publication Statistics +===================================== + +23 tests with assumption checking, effect sizes, CIs, output formats. + +```python +result = stx.stats.test_ttest_ind(g1, g2) +result = stx.stats.test_mannwhitneyu(g1, g2) # Non-parametric +result = stx.stats.test_anova(g1, g2, g3) +result = stx.stats.test_pearsonr(x, y) +result = stx.stats.test_ttest_ind(g1, g2, return_as="latex") +``` + +Result: statistic, p_value, effect_size, ci_low/ci_high, power. +""" + +MODULE_SCHOLAR = """\ +# stx.scholar - Literature Management +====================================== + +```bash +scitex scholar bibtex papers.bib --project myresearch --num-workers 8 +``` + +Enriches BibTeX with abstracts, DOIs, journals, impact factors; downloads PDFs. + +## MCP Tools +scholar_search_papers, scholar_enrich_bibtex, scholar_download_pdf, +scholar_fetch_papers, scholar_parse_pdf_content +""" + +MODULE_SESSION = """\ +# stx.session - Reproducible Experiment Tracking +================================================= + +```python +@stx.session +def main(input_file="data.csv", CONFIG=stx.INJECTED, plt=stx.INJECTED): + \"\"\"Docstring becomes --help.\"\"\" + stx.io.save(results, "output.csv", symlink_to="./data") + return 0 +``` + +CONFIG: ID, FILE, SDIR_OUT, PID, ARGS. +YAML in `./config/*.yaml` auto-loads into CONFIG (dot access). +Always return exit status (0 for success). +""" + +MODULE_DOCS = { + "io": MODULE_IO, + "plt": MODULE_PLT, + "stats": MODULE_STATS, + "scholar": MODULE_SCHOLAR, + "session": MODULE_SESSION, +} + +IO_FORMATS = """\ +# stx.io Supported Formats +=========================== + +## Data +.csv .tsv .xlsx .json .yaml/.yml .pkl/.pickle .npy .npz .h5/.hdf5 +.zarr .parquet .feather + +## Images +.png .jpg/.jpeg .tiff/.tif .pdf .svg + +## Scientific (load) +.edf (EEG) .fif (MNE) .set (EEGLAB) .mat (MATLAB) + +## PyTorch +.pt .pth + +Notes: +- Extension determines handler. +- PNG/JPEG support embedded metadata. +- Saving figures also exports plotted data as CSV. +- `symlink_to="./data"` creates symlinks. +""" + +FIGRECIPE_INTEGRATION = """\ +# stx.plt - Powered by FigRecipe +================================= + +stx.plt records matplotlib calls to YAML recipes for reproducibility. + +## MCP Declarative Spec (via plt_plot tool) — prefer CSV column input +```yaml +plots: + - type: scatter + data_file: results.csv + x: time + y: measurement + color: blue +``` +Workflow: Python writes CSV -> MCP reads columns -> creates figure. + +## For Detailed FigRecipe Docs +- figrecipe://cheatsheet +- figrecipe://api/core +- figrecipe://mcp-spec + +## Supported Plot Types +line, scatter, bar, barh, hist, boxplot, violinplot, imshow, heatmap, +errorbar, fill_between, contour, contourf, pie, stem +""" + +# EOF diff --git a/src/scitex/_mcp/_umbrella_tools.py b/src/scitex/_mcp/_umbrella_tools.py new file mode 100755 index 000000000..76c195b04 --- /dev/null +++ b/src/scitex/_mcp/_umbrella_tools.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +# Timestamp: 2026-05-31 +# File: src/scitex/_mcp/_umbrella_tools.py +"""Umbrella-only MCP tools — inline tools that have no peer package. + +These wrap handlers under ``scitex.._mcp.handlers`` (or umbrella-local +helpers). No standalone peer's FastMCP server supplies them, so the registry +mount cannot; they are folded directly into the single umbrella entrypoint. + +Namespaces here: browser, capture, docs, skills, usage, template, tunnel. +The larger ``introspect_*`` and ``notification_*`` families live in sibling +modules to keep each file focused. +""" + +from __future__ import annotations + +from typing import Optional + +from ._introspect_tools import register_introspect_tools +from ._notification_tools import register_notification_tools + +__all__ = ["register_umbrella_tools"] + + +def register_umbrella_tools(mcp) -> None: + """Register every umbrella-only inline tool onto the FastMCP server.""" + + # -- browser --------------------------------------------------------- + @mcp.tool() + async def browser_save_as_pdf( + url: str, + output_path: str, + wait_seconds: float = 3, + print_background: bool = True, + format: str = "A4", + margin: str = "10mm", + ) -> str: + """Render any URL to a print-style PDF via headless Chromium — full-page, JS-rendered, with configurable paper size + margins + background graphics. Drop-in replacement for Chrome's "Print -> Save as PDF" dialog, `wkhtmltopdf`, `weasyprint`, and `playwright.page.pdf()` boilerplate. Use when the user asks to "save this page as PDF", "archive this article", "generate a PDF from the dashboard", "download the rendered HTML report", or is capturing a JS-heavy page that static scrapers miss. `wait_seconds` gives JS time to finish rendering. + + Args: + url: URL to save as PDF. + output_path: Path to save the PDF file. + wait_seconds: Extra seconds to wait after page load for JS rendering. + print_background: Whether to print background graphics. + format: Paper format (A4, Letter, etc.). + margin: Page margins (e.g., 10mm, 1in). + """ + from scitex_dev.ecosystem import async_wrap_as_mcp + + from scitex.browser.pdf._save_as_pdf import save_as_pdf_async + + return await async_wrap_as_mcp( + save_as_pdf_async, + side_effects=["file_create: PDF file"], + url=url, + output_path=output_path, + wait_seconds=wait_seconds, + print_background=print_background, + format=format, + margin_top=margin, + margin_bottom=margin, + margin_left=margin, + margin_right=margin, + ) + + # -- capture --------------------------------------------------------- + @mcp.tool() + async def capture_screenshot( + monitor_id: int = 0, + all: bool = False, + quality: int = 85, + message: Optional[str] = None, + return_base64: bool = False, + url: Optional[str] = None, + app: Optional[str] = None, + ) -> str: + """Take a JPEG screenshot of a chosen target — a specific monitor (`monitor_id=N`), every monitor at once (`all=True`), a live browser tab (`url=...`), or an X11 application window (`app='emacs'`). Drop-in replacement for `scrot`, `gnome-screenshot`, `maim`, `mss.mss().shot()`, and ad-hoc `playwright.screenshot()`. Use when the user asks to "take a screenshot", "capture my screen", "grab a picture of the browser", "screenshot that app window", "prove visually this is fixed", or is attaching UI evidence to a bug report / review. `return_base64=True` inlines instead of saving.""" + from scitex_dev.ecosystem import async_wrap_as_mcp + + from scitex.capture._mcp.handlers import capture_screenshot_handler + + return await async_wrap_as_mcp( + capture_screenshot_handler, + side_effects=["file_create: screenshot image file"], + idempotent=True, + monitor_id=monitor_id, + all=all, + quality=quality, + message=message, + return_base64=return_base64, + url=url, + app=app, + ) + + # -- docs (scitex-dev aggregation) ----------------------------------- + @mcp.tool() + async def docs_list() -> str: + """Enumerate every installed SciTeX package that ships bundled Sphinx docs — each entry includes version, manifest path, and docs URL. Use when the user asks "what SciTeX packages are installed?", "which ones have docs?", or before calling `docs_get` / `docs_search`.""" + from scitex_dev.docs import get_docs + from scitex_dev.ecosystem import wrap_as_mcp + + return wrap_as_mcp(get_docs, idempotent=True) + + @mcp.tool() + async def docs_get( + package: str, + format: Optional[str] = None, + page: Optional[str] = None, + ) -> str: + """Fetch a SciTeX package's bundled Sphinx docs — manifest (default), parsed JSON body, or a path to the built HTML. Use when the user asks "show scitex-writer docs", "open the manual for X", "get the Sphinx output for Y". + + Args: + package: Package name (e.g. "scitex-writer"). + format: None for manifest, "json" for structured, "html" for path. + page: Specific documentation page name. + """ + from scitex_dev.docs import get_docs + from scitex_dev.ecosystem import wrap_as_mcp + + return wrap_as_mcp( + get_docs, idempotent=True, package=package, format=format, page=page + ) + + @mcp.tool() + async def docs_build( + package: Optional[str] = None, + formats: Optional[list[str]] = None, + ) -> str: + """Trigger `sphinx-build` on one or every installed SciTeX package, producing HTML/JSON under each package's `_docs/_build/`. Use when the user asks to "rebuild docs", "regenerate Sphinx HTML", or after editing docstrings / `.rst` source. + + Args: + package: Package name. None = build all. + formats: List of builders ("html", "json"). Default: ["html"]. + """ + from scitex_dev.docs import build_docs + from scitex_dev.ecosystem import wrap_as_mcp + + return wrap_as_mcp( + build_docs, + side_effects=["file_create: Sphinx HTML output in _build directory"], + package=package, + formats=formats, + ) + + @mcp.tool() + async def docs_search( + query: str, + scope: str = "all", + package: Optional[str] = None, + max_results: int = 10, + ) -> str: + """Full-text search across every installed SciTeX package's docs / Python API / CLI reference / MCP tool registry — one Google-like query, cross-scope ranked results. Use whenever the user asks to "search the ecosystem for X", "find anything about figures / stats / writing", "which module does Y?". Use `scope='api'|'cli'|'mcp'|'docs'` to narrow; `+required` / `-excluded` operators supported. + + Args: + query: Search query string. + scope: What to search: "all", "api", "cli", "mcp", or "docs". + package: Limit search to a single package. + max_results: Maximum number of results. + """ + from scitex_dev.ecosystem import wrap_as_mcp + from scitex_dev.search import search + + return wrap_as_mcp( + search, + idempotent=True, + query=query, + scope=scope, + package=package, + max_results=max_results, + ) + + # -- skills (scitex-dev aggregation) --------------------------------- + @mcp.tool() + async def skills_list(package: Optional[str] = None) -> str: + """Enumerate every `SKILL.md` + sub-skill reference page the installed SciTeX ecosystem ships. Use when the user asks "what SciTeX skills do I have?", "list skill pages for scitex-stats", or is orienting before `skills_get`. + + Args: + package: Filter to a specific package. None returns all packages. + """ + from scitex_dev.ecosystem import wrap_as_mcp + from scitex_dev.skills import list_skills + + return wrap_as_mcp(list_skills, idempotent=True, package=package) + + @mcp.tool() + async def skills_get(package: str, name: Optional[str] = None) -> str: + """Read the markdown content of a specific SciTeX skill page (main `SKILL.md` or a named reference leaf). Use when the user asks "show me the scitex-stats skill", "get the figrecipe plot-types reference", "read the skill for X". + + Args: + package: Package name (e.g. "scitex-stats"). + name: Reference name (e.g. "test-selection"). None = main SKILL.md. + """ + from scitex_dev.ecosystem import wrap_as_mcp + from scitex_dev.skills import get_skill + + return wrap_as_mcp(get_skill, idempotent=True, package=package, name=name) + + # -- usage ----------------------------------------------------------- + @mcp.tool() + def usage_show(topic: str = "") -> str: + """Return a runnable code example for a SciTeX topic (`plt`, `stats`, `session`, `io`, `scholar`, ...) — short, copy-pasteable snippets showing idiomatic usage. Use when the user asks "how do I use scitex.plt?", "show me a t-test example", "give me a session boilerplate".""" + from scitex_dev.ecosystem import wrap_as_mcp + + from scitex.usage import show + + return wrap_as_mcp(show, idempotent=True, topic=topic or None) + + @mcp.tool() + def usage_list() -> str: + """List every topic `usage_show` can serve (`plt`, `stats`, `session`, `io`, `scholar`, `audio`, `writer`, ...). Use when the user asks "what examples are available?", "which modules have usage snippets?".""" + from scitex_dev.ecosystem import wrap_as_mcp + + from scitex.usage import topics + + return wrap_as_mcp(topics, idempotent=True) + + # -- template -------------------------------------------------------- + @mcp.tool() + async def template_clone_template( + template_id: str, + project_name: str, + target_dir: Optional[str] = None, + git_strategy: str = "child", + branch: Optional[str] = None, + tag: Optional[str] = None, + ) -> str: + """Bootstrap a new SciTeX project by cloning a registered template repo and seeding it with the chosen name — optionally at a branch/tag, with configurable git wiring (`child`/`squash`/`fork`/`none`). Use when the user asks to "start a new SciTeX project", "clone the research template", "scaffold a paper repo", "initialize a new package from template".""" + from scitex_dev.ecosystem import async_wrap_as_mcp + + from scitex.template._mcp.handlers import clone_template_handler + + return await async_wrap_as_mcp( + clone_template_handler, + side_effects=["file_create: new project directory from template"], + template_id=template_id, + project_name=project_name, + target_dir=target_dir, + git_strategy=git_strategy, + branch=branch, + tag=tag, + ) + + @mcp.tool() + async def template_list_git_strategies() -> str: + """Enumerate the git wiring options `template_clone_template` accepts — `child` (submodule), `squash`, `fork`, `none`. Use when the user asks "how do I wire git for the new project?", "what git options does the template support?".""" + from scitex_dev.ecosystem import async_wrap_as_mcp + + from scitex.template._mcp.handlers import list_git_strategies_handler + + return await async_wrap_as_mcp(list_git_strategies_handler, idempotent=True) + + @mcp.tool() + async def template_get_code_template( + template_id: str, + filepath: Optional[str] = None, + docstring: Optional[str] = None, + ) -> str: + """Return a boilerplate code template for a SciTeX script/module — core patterns (`session`, `io`, `config`) or per-module usage examples, or `'all'`. Use when the user asks "scaffold a scitex script", "give me a stats experiment template", "how do I start a scitex session?".""" + from scitex_dev.ecosystem import async_wrap_as_mcp + + from scitex.template._mcp.handlers import get_code_template_handler + + return await async_wrap_as_mcp( + get_code_template_handler, + idempotent=True, + template_id=template_id, + filepath=filepath, + docstring=docstring, + ) + + @mcp.tool() + async def template_list_code_templates() -> str: + """Return the catalog of every code template `template_get_code_template` can serve. Use when the user asks "what templates are available?", "which modules have boilerplate?".""" + from scitex_dev.ecosystem import async_wrap_as_mcp + + from scitex.template._mcp.handlers import list_code_templates_handler + + return await async_wrap_as_mcp(list_code_templates_handler, idempotent=True) + + # -- tunnel ---------------------------------------------------------- + @mcp.tool() + async def tunnel_setup(port: int, bastion_server: str, secret_key_path: str) -> str: + """Install an `autossh`-backed systemd unit that opens a reverse SSH tunnel (local -> bastion:port) and auto-reconnects on drop. Use when the user asks to "set up a reverse tunnel", "expose this machine through a bastion", "open port X on the jump host", or mentions bastion, jump host, NAT traversal, HPC login node.""" + from scitex_dev.ecosystem import wrap_as_mcp + + from scitex.tunnel import setup + + return wrap_as_mcp( + setup, + side_effects=["systemd_service: creates autossh service"], + port=port, + bastion_server=bastion_server, + secret_key_path=secret_key_path, + ) + + @mcp.tool() + async def tunnel_remove(port: int) -> str: + """Tear down an autossh reverse-tunnel unit (stop + disable + rm unit + daemon-reload). Use when the user asks to "remove the tunnel", "delete reverse tunnel on port X", "stop autossh", "decommission this route".""" + from scitex_dev.ecosystem import wrap_as_mcp + + from scitex.tunnel import remove + + return wrap_as_mcp( + remove, + side_effects=["systemd_service: stops and disables autossh service"], + port=port, + ) + + @mcp.tool() + async def tunnel_status(port: int = 0) -> str: + """Live state of autossh reverse-tunnel systemd units — active/inactive, PID, restart count, last journal lines. Use when the user asks "is my tunnel up?", "why can't I reach port 2222?", "list all reverse tunnels". `port=0` lists everything.""" + from scitex_dev.ecosystem import wrap_as_mcp + + from scitex.tunnel import status + + return wrap_as_mcp(status, idempotent=True, port=port if port else None) + + # -- larger families (separate modules) ------------------------------ + register_introspect_tools(mcp) + register_notification_tools(mcp) + + +# EOF diff --git a/src/scitex/_mcp_resources/__init__.py b/src/scitex/_mcp_resources/__init__.py deleted file mode 100755 index d2f57ef89..000000000 --- a/src/scitex/_mcp_resources/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_resources/__init__.py -"""MCP Resources for SciTeX - Dynamic documentation for AI agents. - -This module provides MCP resources that expose SciTeX documentation, -patterns, and examples to AI agents for code generation guidance. - -Resources: -- scitex://cheatsheet - Quick reference for all core patterns -- scitex://session-tree - Output directory structure explanation -- scitex://module/{name} - Module-specific documentation (io, plt, stats, scholar, session) -- scitex://io-formats - Supported file formats -- scitex://figrecipe-spec - Figure recipe specification -""" - -from __future__ import annotations - -from ._cheatsheet import register_cheatsheet_resources -from ._figrecipe import register_figrecipe_resources -from ._formats import register_format_resources -from ._modules import register_module_resources -from ._scholar import register_scholar_resources -from ._session import register_session_resources - -__all__ = ["register_resources"] - - -def register_resources(mcp) -> None: - """Register all MCP resources with the FastMCP server.""" - register_cheatsheet_resources(mcp) - register_session_resources(mcp) - register_module_resources(mcp) - register_format_resources(mcp) - register_figrecipe_resources(mcp) - register_scholar_resources(mcp) - - -# EOF diff --git a/src/scitex/_mcp_resources/_cheatsheet.py b/src/scitex/_mcp_resources/_cheatsheet.py deleted file mode 100755 index 7f0d7a889..000000000 --- a/src/scitex/_mcp_resources/_cheatsheet.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_resources/_cheatsheet.py -"""Core SciTeX cheatsheet resource for AI agents.""" - -from __future__ import annotations - -__all__ = ["register_cheatsheet_resources"] - -CHEATSHEET = """\ -# SciTeX Cheatsheet for AI Agents -================================= - -## Import Pattern (ALWAYS use this) -```python -import scitex as stx -import numpy as np -``` - -## 1. @stx.session - Reproducible Experiment Tracking - -The MOST IMPORTANT pattern. Wrap your main function with @stx.session: - -```python -@stx.session -def main( - # User parameters (become CLI arguments automatically) - input_file="data.csv", # --input-file (default: data.csv) - n_samples=100, # --n-samples (default: 100) - - # INJECTED parameters (auto-provided by session) - CONFIG=stx.INJECTED, # Session config with ID, paths - plt=stx.INJECTED, # Pre-configured matplotlib - COLORS=stx.INJECTED, # Color palette - rngg=stx.INJECTED, # Seeded random generator - logger=stx.INJECTED, # Session logger -): - \"\"\"This docstring becomes --help description.\"\"\" - - # Your analysis code here - data = stx.io.load(input_file) - results = process(data, n_samples) - - # Save outputs (automatically to session directory) - stx.io.save(results, "results.csv") - stx.io.save(fig, "plot.png", symlink_to="./data") - - return 0 # Exit status - -if __name__ == "__main__": - main() # CLI mode when no args passed -``` - -## 2. stx.io - Universal File I/O (30+ formats) - -ALWAYS use stx.io.save() and stx.io.load() for file operations: - -```python -# Saving - extension determines format automatically -stx.io.save(df, "data.csv") # DataFrame -> CSV -stx.io.save(arr, "data.npy") # NumPy array -stx.io.save(obj, "data.pkl") # Any Python object -stx.io.save(fig, "plot.png") # Figure + auto CSV - -# Loading - format auto-detected -data = stx.io.load("data.csv") - -# With options -stx.io.save(fig, "plot.png", - metadata={"exp": "exp01"}, # Embedded metadata - symlink_to="./data", # Create symlink - verbose=True, # Log messages -) -``` - -IMPORTANT: stx.io.save shows logging messages: -``` -SUCC: Saved to: ./script_out/results.csv (4.0 KiB) -SUCC: Symlinked: ./script_out/results.csv -> ./data/results.csv -``` - -## 3. stx.plt - Publication-Ready Figures (Auto CSV Export) - -```python -fig, ax = stx.plt.subplots() - -# Use stx_ prefixed methods for auto CSV export -ax.stx_line(x, y, label="Signal") # Tracked: exports to CSV -ax.stx_scatter(x, y) # Tracked - -# Set labels with convenience method -ax.set_xyt("X axis", "Y axis", "Title") - -# Save figure -> creates BOTH plot.png AND plot.csv -stx.io.save(fig, "plot.png") -fig.close() -``` - -## 4. stx.stats - Publication Statistics (23 tests) - -```python -result = stx.stats.test_ttest_ind(group1, group2, return_as="dataframe") -# Returns: p-value, Cohen's d, 95% CI, normality check, power analysis - -result = stx.stats.test_anova(*groups, return_as="latex") -``` - -## 5. stx.scholar - Literature Management - -```bash -scitex scholar bibtex papers.bib --project myresearch --num-workers 8 -# Enriches BibTeX with abstracts, DOIs, impact factors, downloads PDFs -``` - -## Quick Tips - -1. ALWAYS use `import scitex as stx` -2. ALWAYS wrap main functions with `@stx.session` -3. ALWAYS use `stx.io.save()` and `stx.io.load()` for files -4. ALWAYS use `stx.plt.subplots()` for figures -5. ALWAYS use `ax.stx_*` methods for auto CSV export -6. ALWAYS return exit status (0 for success) from main -""" - - -def register_cheatsheet_resources(mcp) -> None: - """Register cheatsheet resource.""" - - @mcp.resource("scitex://cheatsheet") - def cheatsheet() -> str: - """Complete SciTeX quick reference for AI code generation.""" - return CHEATSHEET - - -# EOF diff --git a/src/scitex/_mcp_resources/_figrecipe.py b/src/scitex/_mcp_resources/_figrecipe.py deleted file mode 100755 index 8ad2f854c..000000000 --- a/src/scitex/_mcp_resources/_figrecipe.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_resources/_figrecipe.py -"""Figrecipe integration documentation for stx.plt MCP tools.""" - -from __future__ import annotations - -__all__ = ["register_figrecipe_resources"] - -FIGRECIPE_INTEGRATION = """\ -# stx.plt - Powered by FigRecipe -================================= - -stx.plt provides publication-ready figures with **automatic reproducibility**. -It's built on FigRecipe, which records all matplotlib calls to YAML recipes. - -## Using stx.plt in @stx.session -```python -@stx.session -def main(plt=stx.INJECTED, COLORS=stx.INJECTED): - fig, ax = stx.plt.subplots() - - # Use stx_ prefixed methods for auto CSV export - ax.stx_line(x, y, color=COLORS.blue, label="data") - ax.set_xyt("X", "Y", "Title") - - # Save creates: plot.png + plot.csv (data) - stx.io.save(fig, "plot.png", symlink_to="./data") - fig.close() - return 0 -``` - -## Output Files from stx.io.save(fig, ...) -``` -script_out/ -├── plot.png # Image file -├── plot.csv # Extracted plot data (auto-generated) -└── plot.yaml # FigRecipe recipe (if using fr.save directly) -``` - -## Tracked Methods (stx_ prefix) -These methods track data for automatic CSV export: -- ax.stx_line(x, y) -- ax.stx_scatter(x, y) -- ax.stx_bar(x, height) -- ax.stx_errorbar(x, y, yerr) -- ax.stx_hist(data, bins) -- ax.stx_boxplot(data) -- ax.stx_violinplot(data) -- ax.stx_imshow(matrix) - -## MCP Declarative Spec (via plt_plot tool) - -### RECOMMENDED: CSV Column Input -Use CSV files - enables code to write data, MCP to visualize: -```yaml -plots: - - type: scatter - data_file: results.csv # CSV from your analysis - x: time # Column name (string) - y: measurement # Column name - color: blue -``` - -**Workflow**: Python writes CSV → MCP reads columns → Creates figure - -### Alternative: Inline Data (simple cases only) -```yaml -plots: - - type: line - x: [1, 2, 3, 4, 5] # Inline array (less flexible) - y: [1, 4, 9, 16, 25] - color: blue -``` - -### Full Example -```yaml -figure: - width_mm: 85 # Nature single-column width - height_mm: 60 -plots: - - type: scatter - data_file: experiment.csv - x: time_hours - y: concentration_mm - color: blue - label: "Data" -xlabel: "Time (h)" -ylabel: "Concentration (mM)" -title: "Enzyme Kinetics" -legend: true -stat_annotations: - - x1: 1 - x2: 3 - p_value: 0.01 - style: stars -``` - -## Statistical Annotations -```python -# Method 1: Via stx.plt (Python API) -ax.add_stat_annotation(x1=0, x2=1, p_value=0.01, style="stars") - -# Method 2: Via MCP spec (declarative) -stat_annotations: - - x1: 0 - x2: 1 - p_value: 0.01 # Converts to ** automatically -``` - -## Color Palette (COLORS=stx.INJECTED) -```python -COLORS.blue, COLORS.red, COLORS.green, COLORS.orange -COLORS.purple, COLORS.navy, COLORS.pink, COLORS.brown -``` - -## For Detailed FigRecipe Documentation -See figrecipe MCP resources directly: -- figrecipe://cheatsheet - Quick reference -- figrecipe://api/core - Full API documentation -- figrecipe://mcp-spec - Declarative spec format - -## Supported Plot Types -line, scatter, bar, barh, hist, boxplot, violinplot, imshow, heatmap, -errorbar, fill_between, contour, contourf, pie, stem -""" - - -def register_figrecipe_resources(mcp) -> None: - """Register figrecipe integration resource.""" - - @mcp.resource("scitex://plt-figrecipe") - def figrecipe_integration() -> str: - """stx.plt integration with FigRecipe for reproducible figures.""" - return FIGRECIPE_INTEGRATION - - -# EOF diff --git a/src/scitex/_mcp_resources/_formats.py b/src/scitex/_mcp_resources/_formats.py deleted file mode 100755 index 7b036127b..000000000 --- a/src/scitex/_mcp_resources/_formats.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_resources/_formats.py -"""IO format documentation resource.""" - -from __future__ import annotations - -__all__ = ["register_format_resources"] - -IO_FORMATS = """\ -# stx.io Supported Formats -=========================== - -## Data Formats - -| Extension | Type | Save | Load | Notes | -|-----------|------|:----:|:----:|-------| -| .csv | DataFrame/Array | ✓ | ✓ | Comma-separated | -| .tsv | DataFrame/Array | ✓ | ✓ | Tab-separated | -| .xlsx | DataFrame | ✓ | ✓ | Excel workbook | -| .json | Dict/List | ✓ | ✓ | JSON format | -| .yaml/.yml | Dict/List | ✓ | ✓ | YAML format | -| .pkl/.pickle | Any | ✓ | ✓ | Python pickle | -| .npy | Array | ✓ | ✓ | NumPy array | -| .npz | Dict[Array] | ✓ | ✓ | Compressed NumPy | -| .h5/.hdf5 | Array/Dict | ✓ | ✓ | HDF5 format | -| .zarr | Array | ✓ | ✓ | Zarr array | -| .parquet | DataFrame | ✓ | ✓ | Apache Parquet | -| .feather | DataFrame | ✓ | ✓ | Feather format | - -## Image Formats - -| Extension | Type | Save | Load | Notes | -|-----------|------|:----:|:----:|-------| -| .png | Figure/Array | ✓ | ✓ | Lossless, metadata | -| .jpg/.jpeg | Figure/Array | ✓ | ✓ | Lossy, metadata | -| .tiff/.tif | Figure/Array | ✓ | ✓ | Scientific imaging | -| .pdf | Figure | ✓ | ✓ | Vector format | -| .svg | Figure | ✓ | - | Vector format | - -## Scientific Formats - -| Extension | Type | Load | Notes | -|-----------|------|:----:|-------| -| .edf | EEG | ✓ | European Data Format | -| .fif | MNE | ✓ | MNE-Python | -| .set | EEGLAB | ✓ | EEGLAB format | -| .mat | MATLAB | ✓ | MATLAB files | - -## PyTorch Formats - -| Extension | Type | Save | Load | -|-----------|------|:----:|:----:| -| .pt | Tensor/Model | ✓ | ✓ | -| .pth | Model | ✓ | ✓ | - -## Usage Notes - -1. **Extension determines handler**: `stx.io.save(df, "data.csv")` uses CSV -2. **Metadata embedding**: PNG/JPEG support embedded metadata -3. **Figure auto-CSV**: Saving figures also exports plotted data as CSV -4. **Symlink support**: `symlink_to="./data"` creates symlinks - -## Examples - -```python -import scitex as stx -import pandas as pd -import numpy as np - -# DataFrame -df = pd.DataFrame({"x": [1, 2], "y": [3, 4]}) -stx.io.save(df, "data.csv") -stx.io.save(df, "data.parquet") - -# NumPy -arr = np.random.randn(100, 100) -stx.io.save(arr, "array.npy") -stx.io.save(arr, "array.npz") - -# Any Python object -stx.io.save({"config": "value"}, "config.yaml") -stx.io.save(complex_obj, "obj.pkl") - -# Figure with auto-CSV -fig, ax = stx.plt.subplots() -ax.stx_line([1, 2, 3], [4, 5, 6]) -stx.io.save(fig, "plot.png") # Creates plot.png + plot.csv -``` -""" - - -def register_format_resources(mcp) -> None: - """Register IO formats resource.""" - - @mcp.resource("scitex://io-formats") - def io_formats() -> str: - """List all supported file formats for stx.io.""" - return IO_FORMATS - - -# EOF diff --git a/src/scitex/_mcp_resources/_modules.py b/src/scitex/_mcp_resources/_modules.py deleted file mode 100755 index 171132854..000000000 --- a/src/scitex/_mcp_resources/_modules.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_resources/_modules.py -"""Module-specific documentation resources for AI agents.""" - -from __future__ import annotations - -__all__ = ["register_module_resources"] - -MODULE_IO = """\ -# stx.io - Universal File I/O -============================== - -## Core API -```python -stx.io.save(obj, path, **kwargs) # Save any object -stx.io.load(path, **kwargs) # Load any file -``` - -## Key Features - -### 1. Automatic Extension Handling -```python -stx.io.save(df, "data.csv") # Uses CSV handler -stx.io.save(df, "data.xlsx") # Uses Excel handler -stx.io.save(arr, "data.npy") # Uses NumPy handler -``` - -### 2. Verbose Logging -```python -stx.io.save(df, "data.csv", verbose=True) -# Output: SUCC: Saved to: ./script_out/data.csv (12.5 KiB) -``` - -### 3. Metadata Embedding (Images) -```python -stx.io.save(fig, "plot.png", metadata={"exp": "exp01"}) -img, meta = stx.io.load("plot.png") -print(meta) # {'exp': 'exp01', 'url': 'https://scitex.ai'} -``` - -### 4. Symlink Creation -```python -stx.io.save(results, "results.csv", symlink_to="./data") -# Creates: ./data/results.csv -> ./script_out/results.csv -``` - -### 5. Auto CSV Export for Figures -```python -stx.io.save(fig, "plot.png") -# Creates BOTH: plot.png AND plot.csv (with plotted data) -``` - -## Common Formats -- `.csv`, `.xlsx`, `.parquet` - DataFrames -- `.npy`, `.npz`, `.h5` - Arrays -- `.pkl`, `.json`, `.yaml` - Objects -- `.png`, `.jpg`, `.pdf`, `.svg` - Figures -""" - -MODULE_PLT = """\ -# stx.plt - Publication-Ready Figures -====================================== - -## Basic Usage -```python -fig, ax = stx.plt.subplots() - -# Use stx_ prefixed methods for auto CSV export -ax.stx_line(x, y, label="Signal") -ax.stx_scatter(x, y) -ax.stx_bar(categories, values) -ax.stx_errorbar(x, y, yerr=err) - -# Set labels with convenience method -ax.set_xyt("X axis", "Y axis", "Title") - -# Save -> creates BOTH image AND CSV -stx.io.save(fig, "plot.png") -fig.close() -``` - -## Tracked Plot Methods (stx_ prefix) -```python -ax.stx_line(x, y) # Line plot -ax.stx_scatter(x, y) # Scatter plot -ax.stx_bar(x, height) # Bar chart -ax.stx_errorbar(x, y, yerr) # Error bars -ax.stx_hist(data, bins=30) # Histogram -ax.stx_boxplot(data) # Box plot -ax.stx_violinplot(data) # Violin plot -ax.stx_imshow(matrix) # Image/heatmap -``` - -## Auto CSV Export -When saving with `stx.io.save(fig, "plot.png")`, CSV is auto-created: -```csv -ax_00_stx_line_0_x,ax_00_stx_line_0_y -0.0,0.0 -0.1,0.0998 -... -``` - -## Color Palette -```python -@stx.session -def main(COLORS=stx.INJECTED): - ax.stx_line(x, y1, color=COLORS.blue) - ax.stx_line(x, y2, color=COLORS.red) - # Available: blue, red, green, orange, purple, navy, pink, brown, gray -``` -""" - -MODULE_STATS = """\ -# stx.stats - Publication Statistics -===================================== - -23 statistical tests with automatic assumption checking, effect sizes, -confidence intervals, and multiple output formats. - -## Two-Sample Tests -```python -result = stx.stats.test_ttest_ind(group1, group2) -result = stx.stats.test_mannwhitneyu(group1, group2) # Non-parametric -``` - -## Paired Sample Tests -```python -result = stx.stats.test_ttest_rel(before, after) -result = stx.stats.test_wilcoxon(before, after) -``` - -## Multiple Group Tests -```python -result = stx.stats.test_anova(g1, g2, g3, g4) -result = stx.stats.test_kruskal(g1, g2, g3, g4) # Non-parametric -``` - -## Correlation Tests -```python -result = stx.stats.test_pearsonr(x, y) -result = stx.stats.test_spearmanr(x, y) -``` - -## Output Formats -```python -result = stx.stats.test_ttest_ind(g1, g2, return_as="dataframe") # Default -result = stx.stats.test_ttest_ind(g1, g2, return_as="latex") # Papers -result = stx.stats.test_ttest_ind(g1, g2, return_as="markdown") # Docs -``` - -## Result Contents -- `statistic`: Test statistic value -- `p_value`: P-value -- `effect_size`: Cohen's d, r, eta², etc. -- `ci_low`, `ci_high`: 95% CI -- `power`: Statistical power -""" - -MODULE_SCHOLAR = """\ -# stx.scholar - Literature Management -====================================== - -BibTeX enrichment with abstracts for LLM context, DOI resolution, -PDF download, and impact factors. - -## CLI Usage (Recommended) -```bash -scitex scholar bibtex papers.bib --project myresearch --num-workers 8 -``` - -## BibTeX Enrichment - -Before: -```bibtex -@article{Smith2024, - title = {Neural Networks}, - author = {Smith, John}, - doi = {10.1038/s41586-024-00001-1} -} -``` - -After: -```bibtex -@article{Smith2024, - title = {Neural Networks for Brain Signal Analysis}, - author = {Smith, John and Lee, Alice}, - doi = {10.1038/s41586-024-00001-1}, - journal = {Nature}, - year = {2024}, - abstract = {We present a novel deep learning approach...}, - impact_factor = {64.8} -} -``` - -The abstract provides rich context for LLM-based literature review! - -## MCP Tools -- `scholar_search_papers`: Search papers -- `scholar_enrich_bibtex`: Add metadata -- `scholar_download_pdf`: Download PDFs -- `scholar_fetch_papers`: Async download -- `scholar_parse_pdf_content`: Extract text - -## Tip: Get BibTeX from Scholar QA -1. Go to https://scholarqa.allen.ai/chat/ -2. Ask research questions -3. Export All Citations -> Save as .bib -4. Enrich: `scitex scholar bibtex citations.bib` -""" - -MODULE_SESSION = """\ -# stx.session - Reproducible Experiment Tracking -================================================= - -## The @stx.session Decorator -```python -@stx.session -def main( - input_file="data.csv", # CLI args (auto-generated) - n_epochs=100, - - CONFIG=stx.INJECTED, # Session config - plt=stx.INJECTED, # matplotlib - COLORS=stx.INJECTED, # Color palette - rngg=stx.INJECTED, # Random generator - logger=stx.INJECTED, # Logger -): - \"\"\"Docstring becomes --help.\"\"\" - stx.io.save(results, "output.csv", symlink_to="./data") - return 0 - -if __name__ == "__main__": - main() -``` - -## Injected Variables - -### CONFIG (DotDict) -```python -CONFIG.ID # "2026Y-01M-20D-09h37m01s_boSr" -CONFIG.FILE # "/path/to/script.py" -CONFIG.SDIR_OUT # "/path/to/script_out" -CONFIG.SDIR_RUN # "/path/to/script_out/RUNNING/" -CONFIG.PID # Process ID -CONFIG.ARGS # {"input_file": "data.csv", ...} -``` - -### COLORS -```python -COLORS.blue, COLORS.red, COLORS.green, COLORS.orange -COLORS.purple, COLORS.navy, COLORS.pink, COLORS.brown -``` - -## YAML Config Loading -Place YAML files in `./config/` directory - auto-loaded into CONFIG: -```yaml -# ./config/experiment.yaml -model: - hidden_size: 256 - num_layers: 3 -training: - batch_size: 32 - learning_rate: 0.001 -``` -Access via dot notation: -```python -CONFIG.experiment.model.hidden_size # 256 -CONFIG.experiment.training.batch_size # 32 -``` - -## Symlinks for Central Navigation -```python -stx.io.save(arr, "output.npy", symlink_to="./data") -# Creates: ./data/output.npy -> ../script_out/output.npy -``` -Use `./data` to accumulate outputs from multiple scripts. - -## Output Structure -``` -script_out/ -├── output.npy # Files at ROOT (not in session dir) -└── FINISHED_SUCCESS/ - └── / - ├── CONFIGS/ - │ ├── CONFIG.pkl # Pickle snapshot - │ └── CONFIG.yaml # Human-readable - └── logs/ - ├── stdout.log # print() captured - └── stderr.log # errors captured -``` - -## Best Practices -1. Always return exit status (0 for success) -2. Use stx.io.save() with symlink_to="./data" -3. Use stx.plt for figures (auto CSV export) -""" - -MODULE_DOCS = { - "io": MODULE_IO, - "plt": MODULE_PLT, - "stats": MODULE_STATS, - "scholar": MODULE_SCHOLAR, - "session": MODULE_SESSION, -} - - -def register_module_resources(mcp) -> None: - """Register module documentation resources.""" - - @mcp.resource("scitex://module/io") - def module_io() -> str: - """stx.io module documentation - Universal File I/O.""" - return MODULE_DOCS["io"] - - @mcp.resource("scitex://module/plt") - def module_plt() -> str: - """stx.plt module documentation - Publication-ready figures.""" - return MODULE_DOCS["plt"] - - @mcp.resource("scitex://module/stats") - def module_stats() -> str: - """stx.stats module documentation - Statistical tests.""" - return MODULE_DOCS["stats"] - - @mcp.resource("scitex://module/scholar") - def module_scholar() -> str: - """stx.scholar module documentation - Literature management.""" - return MODULE_DOCS["scholar"] - - @mcp.resource("scitex://module/session") - def module_session() -> str: - """stx.session module documentation - Experiment tracking.""" - return MODULE_DOCS["session"] - - -# EOF diff --git a/src/scitex/_mcp_resources/_session.py b/src/scitex/_mcp_resources/_session.py deleted file mode 100755 index b0f5b6662..000000000 --- a/src/scitex/_mcp_resources/_session.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_resources/_session.py -"""Session output tree documentation resource.""" - -from __future__ import annotations - -__all__ = ["register_session_resources"] - -SESSION_TREE = """\ -# @stx.session Output Directory Structure -========================================== - -When you use @stx.session, outputs are automatically organized: - -``` -script.py # Your script -script_out/ # Output directory (auto-created) -├── output.npy # Your saved files (ROOT level) -├── results.json # All outputs here, NOT in session dir -├── figure.png # Figures -├── figure.csv # Auto-exported plot data -├── RUNNING/ # Currently running sessions -│ └── 2025Y-01M-20D-09h30m00s_AbC1-main/ -│ ├── CONFIGS/ -│ │ ├── CONFIG.pkl # Python config object (pickle) -│ │ └── CONFIG.yaml # Human-readable config snapshot -│ └── logs/ -│ ├── stdout.log # All print() output captured -│ └── stderr.log # All errors captured -├── FINISHED_SUCCESS/ # Completed sessions (moved from RUNNING) -│ └── -main/ -│ ├── CONFIGS/... -│ └── logs/... -└── FINISHED_FAILED/ # Failed sessions (errors) - └── -main/ - └── ... -data/ # Central navigation via symlinks -└── output.npy -> ../script_out/output.npy -``` - -## Key Points - -1. **Session ID Format**: `YYYY'Y'-MM'M'-DD'D'-HH'h'MM'm'SS's'_XXXX-funcname` - - Example: `2026Y-01M-20D-09h37m01s_boSr-main` - -2. **Output File Placement**: - - Files saved with `stx.io.save(obj, "filename")` go to `script_out/` ROOT - - NOT inside the session subdirectory (CONFIGS/logs only there) - -3. **Symlinks for Central Navigation**: - ```python - stx.io.save(arr, "output.npy", symlink_to="./data") - # Creates: ./data/output.npy -> ../script_out/output.npy - ``` - - Use `./data` directory to accumulate outputs from multiple scripts - - Easy navigation without digging into individual script_out directories - -4. **CONFIG Object** (available as `CONFIG=stx.INJECTED`): - ```python - CONFIG.ID # "2026Y-01M-20D-09h37m01s_boSr" - CONFIG.FILE # "/path/to/script.py" - CONFIG.SDIR_OUT # "/path/to/script_out" - CONFIG.SDIR_RUN # "/path/to/script_out/RUNNING/" - CONFIG.PID # 12345 - CONFIG.ARGS # {"n_points": 100, ...} - ``` - -5. **YAML Config Loading** (from `./config/*.yaml`): - ```yaml - # ./config/experiment.yaml - model: - hidden_size: 256 - num_layers: 3 - training: - batch_size: 32 - ``` - Access: `CONFIG.experiment.model.hidden_size # 256` - -6. **Automatic Cleanup**: - - On success: RUNNING -> FINISHED_SUCCESS - - On error: RUNNING -> FINISHED_FAILED - - All print()/stderr captured in logs/ - -## Example Script - -```python -#!/usr/bin/env python3 -import scitex as stx -import numpy as np - -@stx.session -def main(n_points=100, CONFIG=stx.INJECTED, plt=stx.INJECTED): - \"\"\"Generate sample data and plot.\"\"\" - - x = np.linspace(0, 10, n_points) - y = np.sin(x) * np.exp(-x/5) - - fig, ax = stx.plt.subplots() - ax.stx_line(x, y) - ax.set_xyt("X", "Y", "Damped Sine") - - # symlink_to for central navigation - stx.io.save(fig, "sine.png", symlink_to="./data") - fig.close() - - return 0 - -if __name__ == "__main__": - main() -``` - -Output: -``` -SUCC: Saved to: ./script_out/sine.png (241.6 KiB) -SUCC: Symlinked: /path/script_out/sine.png -> -SUCC: /path/data/sine.png -``` - -Tree after running: -``` -script.py -script_out/ -├── sine.png # Figure at ROOT level -├── sine.csv # Auto-exported data -└── FINISHED_SUCCESS/ - └── 2026Y-01M-20D-09h37m01s_boSr-main/ - ├── CONFIGS/ - │ ├── CONFIG.pkl - │ └── CONFIG.yaml - └── logs/ - ├── stdout.log - └── stderr.log -data/ -└── sine.png -> ../script_out/sine.png -``` -""" - - -def register_session_resources(mcp) -> None: - """Register session tree resource.""" - - @mcp.resource("scitex://session-tree") - def session_tree() -> str: - """Explain the @stx.session output directory structure.""" - return SESSION_TREE - - -# EOF diff --git a/src/scitex/_mcp_tools/__init__.py b/src/scitex/_mcp_tools/__init__.py deleted file mode 100755 index e1ce751ec..000000000 --- a/src/scitex/_mcp_tools/__init__.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""Registry-driven FastMCP tool registration for the scitex umbrella. - -Single source of truth: the scitex-dev ecosystem registry -(``scitex_dev._ecosystem._core.ECOSYSTEM``) lists every peer package -along with its ``import_name`` and ``umbrella_subcommand``. For each -non-archived, non-umbrella entry, we attempt to import its FastMCP -instance from the canonical paths and ``safe_mount`` it under the -declared subcommand namespace. - -This replaces the historical pattern of one hand-written -``register__tools`` bridge file per peer (anti-pattern; see -``_skills/general/03_interface_03_mcp/02_server-registration.md`` — -"Hand-wrapping is an anti-pattern"). New tools added to a peer's -``_mcp_server`` propagate automatically; no umbrella-side maintenance. - -Backward-compat for bridge files: any module ``scitex._mcp_tools.`` -that still defines ``register__tools`` is invoked AFTER the -registry loop, so partly-migrated bridges (mixed safe_mount + inline -``@mcp.tool``) keep their inline tools until the residual hand-wrap is -moved into the peer package. -""" - -from __future__ import annotations - -import importlib -import logging -import os -from typing import Iterable - -logger = logging.getLogger(__name__) - -__all__ = ["register_all_tools"] - -# Canonical places a FastMCP instance might live in a peer package. -# Mirrors `scitex_dev._cli.audit._summary._mcp_audit._resolve_mcp_server`. -_MCP_PATH_CANDIDATES = ( - "_mcp_server", - "mcp_server", - "_mcp.server", - "mcp.server", -) -_MCP_ATTR_CANDIDATES = ("mcp", "server", "app") - -# Categories the umbrella does NOT mount. -_SKIP_CATEGORIES = frozenset({"umbrella", "template"}) - -# Namespace overrides — registry's `umbrella_subcommand` may differ from -# the prefix consumers already know. Apply these renames so existing -# tool names (`crossref_search`, not `crossref-local_search`) survive -# the cutover from per-package bridges to registry-driven mounts. -_NAMESPACE_ALIASES: dict[str, str] = { - "crossref-local": "crossref", - "openalex-local": "openalex", - # The CLI subcommand is "agent-container"; the historical MCP - # namespace was the underscore form so existing tool names stay - # compatible after the rename. - "agent-container": "agent_container", -} - - -def _env_gate_key(namespace: str) -> str: - """Return the legacy SCITEX_MCP_USE_ env-var name for a namespace.""" - return "SCITEX_MCP_USE_" + namespace.upper().replace("-", "_") - - -def _is_enabled(namespace: str) -> bool: - """Honour ``SCITEX_MCP_USE_=0`` to skip a peer mount.""" - return os.environ.get(_env_gate_key(namespace), "1") != "0" - - -def _resolve_peer_mcp(import_name: str): - """Try every canonical location for a peer's FastMCP instance. - - Returns the FastMCP object on first match, or None. - """ - try: - from fastmcp import FastMCP - except ImportError: - return None - - for sub in _MCP_PATH_CANDIDATES: - mod_name = f"{import_name}.{sub}" - try: - mod = importlib.import_module(mod_name) - except BaseException: - # `BaseException` because some peers `sys.exit(1)` at import - # if their preconditions (env vars, optional deps) fail. - # SystemExit derives from BaseException — a bare ``except - # Exception`` would let it kill the umbrella import. - continue - for attr in _MCP_ATTR_CANDIDATES: - obj = getattr(mod, attr, None) - if isinstance(obj, FastMCP): - return obj - return None - - -def _iter_registry() -> Iterable[tuple[str, str, str]]: - """Yield ``(pip_name, import_name, namespace)`` for every mountable peer. - - Falls back gracefully when scitex-dev is not installed (no peer - auto-mounts; only the manual extras run). - """ - try: - from scitex_dev._ecosystem._core import ECOSYSTEM - except ImportError: - logger.warning( - "scitex-dev not installed — peer MCP auto-mount disabled " - "(install scitex-dev to enable)." - ) - return - - for pip_name, info in ECOSYSTEM.items(): - if info.get("archived"): - continue - if info.get("category") in _SKIP_CATEGORIES: - continue - import_name = info.get("import_name") - if not import_name: - continue - namespace = info.get("umbrella_subcommand", pip_name.removeprefix("scitex-")) - namespace = _NAMESPACE_ALIASES.get(namespace, namespace) - yield pip_name, import_name, namespace - - -def _mount_peer(mcp, peer_mcp, namespace: str) -> bool: - """Run ``safe_mount`` and report success.""" - from ._compat import safe_mount - - try: - safe_mount(mcp, peer_mcp, namespace=namespace) - return True - except Exception as exc: # noqa: BLE001 — diagnostic, never fatal - logger.warning("MCP mount failed for %r: %s", namespace, exc) - return False - - -def _run_legacy_bridges(mcp, mounted: set[str]) -> list[str]: - """Invoke any per-package ``register__tools`` shim still present. - - These are the remaining hand-wrap files; they exist only for - namespaces whose peer doesn't yet expose ``_mcp_server.mcp``. When - the registry loop already mounted a namespace, the matching bridge - is skipped to avoid double-registering tools. - - Bridge stems may use underscores where the registry uses hyphens - (``agent_container.py`` ↔ ``agent-container``); we compare both - forms before deciding to skip. - """ - invoked: list[str] = [] - package_dir = os.path.dirname(__file__) - for fname in sorted(os.listdir(package_dir)): - if not fname.endswith(".py"): - continue - if fname.startswith("_"): - continue - stem = fname[:-3] - # Skip when the registry already mounted this namespace, in - # either underscore or hyphen form. - stem_hyphen = stem.replace("_", "-") - if stem in mounted or stem_hyphen in mounted: - continue - try: - mod = importlib.import_module(f"{__name__}.{stem}") - except BaseException as exc: - logger.warning("legacy bridge %s import failed: %s", stem, exc) - continue - register = getattr(mod, f"register_{stem}_tools", None) - if register is None: - continue - if not _is_enabled(stem): - continue - try: - register(mcp) - invoked.append(stem) - except Exception as exc: # noqa: BLE001 - logger.warning("legacy bridge %s.register raised: %s", stem, exc) - return invoked - - -def register_all_tools(mcp) -> None: - """Register every peer's FastMCP tools onto the umbrella server. - - Order: - 1. Registry-driven safe_mount of peer ``_mcp_server`` instances. - 2. Any remaining legacy ``register__tools`` bridge whose - namespace wasn't already mounted in step 1. - - Each peer is gated by ``SCITEX_MCP_USE_=0`` (default - enabled); legacy bridges honour the same gate keyed by the bridge - file's stem. - """ - mounted: set[str] = set() - skipped: list[str] = [] - - # 1. Registry loop ---------------------------------------------------- - for _pip, import_name, namespace in _iter_registry(): - if not _is_enabled(namespace): - skipped.append(f"{namespace}(gated)") - continue - peer_mcp = _resolve_peer_mcp(import_name) - if peer_mcp is None: - continue - if _mount_peer(mcp, peer_mcp, namespace): - mounted.add(namespace) - - # 2. Legacy bridge fallback ------------------------------------------- - invoked = _run_legacy_bridges(mcp, mounted) - - if mounted: - logger.info( - "MCP umbrella mounted %d peer servers: %s", - len(mounted), - ", ".join(sorted(mounted)), - ) - if invoked: - logger.info( - "MCP umbrella ran %d legacy bridges: %s", - len(invoked), - ", ".join(invoked), - ) - if skipped: - logger.debug("MCP umbrella skipped: %s", ", ".join(skipped)) - - -# EOF diff --git a/src/scitex/_mcp_tools/browser.py b/src/scitex/_mcp_tools/browser.py deleted file mode 100755 index 76e210795..000000000 --- a/src/scitex/_mcp_tools/browser.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -"""Browser module tools for FastMCP unified server.""" - - -def register_browser_tools(mcp) -> None: - """Register browser tools with FastMCP server.""" - - @mcp.tool() - async def browser_save_as_pdf( - url: str, - output_path: str, - wait_seconds: float = 3, - print_background: bool = True, - format: str = "A4", - margin: str = "10mm", - ) -> str: - """Render any URL to a print-style PDF via headless Chromium — full-page, JS-rendered, with configurable paper size + margins + background graphics. Drop-in replacement for Chrome's "Print → Save as PDF" dialog, `wkhtmltopdf`, `weasyprint`, and `playwright.page.pdf()` boilerplate. Use when the user asks to "save this page as PDF", "archive this article", "generate a PDF from the dashboard", "download the rendered HTML report", or is capturing a JS-heavy page that static scrapers miss. `wait_seconds` gives JS time to finish rendering. - - Args: - url: URL to save as PDF. - output_path: Path to save the PDF file. - wait_seconds: Extra seconds to wait after page load for JS rendering. - print_background: Whether to print background graphics. - format: Paper format (A4, Letter, etc.). - margin: Page margins (e.g., 10mm, 1in). - """ - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.browser.pdf._save_as_pdf import save_as_pdf_async - - return await async_wrap_as_mcp( - save_as_pdf_async, - side_effects=["file_create: PDF file"], - url=url, - output_path=output_path, - wait_seconds=wait_seconds, - print_background=print_background, - format=format, - margin_top=margin, - margin_bottom=margin, - margin_left=margin, - margin_right=margin, - ) - - -# EOF diff --git a/src/scitex/_mcp_tools/capture.py b/src/scitex/_mcp_tools/capture.py deleted file mode 100755 index 48f4b9cb8..000000000 --- a/src/scitex/_mcp_tools/capture.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-15 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_tools/capture.py -"""Capture module tools for FastMCP unified server.""" - -from typing import Optional - - -def register_capture_tools(mcp) -> None: - """Register capture tools with FastMCP server.""" - - @mcp.tool() - async def capture_screenshot( - monitor_id: int = 0, - all: bool = False, - quality: int = 85, - message: Optional[str] = None, - return_base64: bool = False, - url: Optional[str] = None, - app: Optional[str] = None, - ) -> str: - """Take a JPEG screenshot of a chosen target — a specific monitor (`monitor_id=N`), every monitor at once (`all=True`), a live browser tab (`url=...`), or an X11 application window (`app='emacs'`). Drop-in replacement for `scrot`, `gnome-screenshot`, `maim`, `mss.mss().shot()`, and ad-hoc `playwright.screenshot()`. Use when the user asks to "take a screenshot", "capture my screen", "grab a picture of the browser", "screenshot that app window", "prove visually this is fixed", or is attaching UI evidence to a bug report / review. `return_base64=True` inlines instead of saving.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.capture._mcp.handlers import capture_screenshot_handler - - return await async_wrap_as_mcp( - capture_screenshot_handler, - side_effects=["file_create: screenshot image file"], - idempotent=True, - monitor_id=monitor_id, - all=all, - quality=quality, - message=message, - return_base64=return_base64, - url=url, - app=app, - ) - - -# EOF diff --git a/src/scitex/_mcp_tools/cloud.py b/src/scitex/_mcp_tools/cloud.py deleted file mode 100755 index 9aeec1dcf..000000000 --- a/src/scitex/_mcp_tools/cloud.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-03-05 -# File: scitex/_mcp_tools/cloud.py -"""Cloud tools for FastMCP unified server. - -Programmatically bridges all scitex-cloud MCP tools into scitex MCP. -No manual wrapping — any new scitex-cloud tool appears automatically. -""" - - -def register_cloud_tools(mcp) -> None: - """Mount scitex-cloud MCP server with 'cloud' prefix. - - Uses mcp.mount() — same pattern as crossref-local and openalex-local. - Tools are prefixed: cloud_repo_clone, cloud_api_status, cloud_on_site_eval_js, etc. - """ - try: - from scitex_cloud._mcp_server import mcp as cloud_mcp - - from ._compat import safe_mount - - safe_mount(mcp, cloud_mcp, namespace="cloud") - except ImportError: - - @mcp.tool() - async def cloud_not_available() -> str: - """scitex-cloud not installed.""" - return ( - "scitex-cloud package required. Install with: pip install scitex-cloud" - ) - - -# EOF diff --git a/src/scitex/_mcp_tools/dev.py b/src/scitex/_mcp_tools/dev.py deleted file mode 100755 index 6ca957672..000000000 --- a/src/scitex/_mcp_tools/dev.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-05-04 -# File: scitex/_mcp_tools/dev.py -"""Developer tools for FastMCP unified server. - -Programmatically bridges all scitex-dev MCP tools into scitex MCP. -No manual wrapping — any new scitex-dev tool appears automatically. -""" - - -def register_dev_tools(mcp) -> None: - """Mount scitex-dev MCP server with 'dev' prefix. - - Uses safe_mount() — same pattern as crossref-local, openalex-local, - cloud, etc. Tools are prefixed: dev_ecosystem_list, dev_docs_search, - dev_skills_list, dev_bulk_rename, etc. - """ - try: - from scitex_dev._mcp._server import mcp as dev_mcp - - from ._compat import safe_mount - - safe_mount(mcp, dev_mcp, namespace="dev") - except ImportError: - - @mcp.tool() - async def dev_not_available() -> str: - """scitex-dev not installed.""" - return "scitex-dev package required. Install with: pip install scitex-dev" diff --git a/src/scitex/_mcp_tools/docs.py b/src/scitex/_mcp_tools/docs.py deleted file mode 100755 index 4b456e466..000000000 --- a/src/scitex/_mcp_tools/docs.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -"""MCP tools for docs aggregation and unified search via scitex-dev.""" - -from typing import Optional - - -def register_docs_tools(mcp) -> None: - """Register documentation and search MCP tools.""" - - @mcp.tool() - async def docs_list() -> str: - """Enumerate every installed SciTeX package that ships bundled Sphinx docs — each entry includes version, manifest path, and docs URL. Drop-in replacement for running `scitex-doc list` across every sub-CLI. Use when the user asks "what SciTeX packages are installed?", "which ones have docs?", "what's the ecosystem I have?", or before calling `docs_get` / `docs_search`.""" - from scitex_dev.docs import get_docs - from scitex_dev.ecosystem import wrap_as_mcp - - return wrap_as_mcp( - get_docs, - idempotent=True, - ) - - @mcp.tool() - async def docs_get( - package: str, - format: Optional[str] = None, - page: Optional[str] = None, - ) -> str: - """Fetch a SciTeX package's bundled Sphinx docs — manifest (default), parsed JSON body, or a direct filesystem path to the built HTML. Drop-in replacement for manually hunting down `site-packages//_docs/index.html` or reading source README. Use when the user asks "show scitex-writer docs", "open the manual for X", "get the Sphinx output for Y", or is looking up per-function reference without opening the browser. - - Args: - package: Package name (e.g. "scitex-writer"). - format: None for manifest, "json" for structured, "html" for path. - page: Specific documentation page name. - """ - from scitex_dev.docs import get_docs - from scitex_dev.ecosystem import wrap_as_mcp - - return wrap_as_mcp( - get_docs, - idempotent=True, - package=package, - format=format, - page=page, - ) - - @mcp.tool() - async def docs_build( - package: Optional[str] = None, - formats: Optional[list[str]] = None, - ) -> str: - """Trigger `sphinx-build` on a single package or every installed SciTeX package, producing HTML and/or JSON output under each package's `_docs/_build/`. Drop-in replacement for `cd scitex-writer/docs && make html` in every repo. Use when the user asks to "rebuild docs", "regenerate Sphinx HTML", "refresh the manual for X", or after editing docstrings / `.rst` source. - - Args: - package: Package name. None = build all. - formats: List of builders ("html", "json"). Default: ["html"]. - """ - from scitex_dev.docs import build_docs - from scitex_dev.ecosystem import wrap_as_mcp - - return wrap_as_mcp( - build_docs, - side_effects=["file_create: Sphinx HTML output in _build directory"], - package=package, - formats=formats, - ) - - @mcp.tool() - async def docs_search( - query: str, - scope: str = "all", - package: Optional[str] = None, - max_results: int = 10, - ) -> str: - """Full-text search across every installed SciTeX package's docs / Python API / CLI reference / MCP tool registry — one Google-like query, cross-scope ranked results. Drop-in replacement for repeatedly grepping `site-packages/scitex*`, reading Sphinx separately, running `--help` on every CLI, and listing MCP servers by hand. Use whenever the user asks to "search the ecosystem for X", "find anything about figures / stats / writing", "which module does Y?", or is discovering functionality without knowing the owning package. Use `scope='api'|'cli'|'mcp'|'docs'` to narrow; `+required` / `-excluded` operators supported. - - Query syntax (Google-like): - "save figure" -> match any term - '"exact phrase"' -> exact phrase match - "+required term" -> term must appear - "stats -deprecated" -> exclude results with "deprecated" - - Args: - query: Search query string. - scope: What to search: "all", "api", "cli", "mcp", or "docs". - package: Limit search to a single package. - max_results: Maximum number of results. - """ - from scitex_dev.ecosystem import wrap_as_mcp - from scitex_dev.search import search - - return wrap_as_mcp( - search, - idempotent=True, - query=query, - scope=scope, - package=package, - max_results=max_results, - ) diff --git a/src/scitex/_mcp_tools/fr.py b/src/scitex/_mcp_tools/fr.py deleted file mode 100755 index bd5e306d6..000000000 --- a/src/scitex/_mcp_tools/fr.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-02-21 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_tools/fr.py -"""FigRecipe specialized plot tools for FastMCP unified server. - -Programmatically bridges all figrecipe fr_* tools into scitex MCP, -renamed as plt_stx_* for consistent scitex branding. - fr_conf_mat → plt_stx_conf_mat - fr_ecdf → plt_stx_ecdf - ... -""" - - -def register_fr_tools(mcp) -> None: - """Register figrecipe fr_* tools as plt_stx_* in the FastMCP server.""" - try: - from figrecipe._mcp import server as fr_mcp - except ImportError: - - @mcp.tool() - def plt_stx_not_available() -> str: - """Figrecipe not installed.""" - return "figrecipe is required. Install with: pip install figrecipe" - - return - - from ._compat import get_tools_sync - - tools = get_tools_sync(fr_mcp.mcp, include_mounted=False) - for name, tool in tools.items(): - if name.startswith("fr_"): - new_name = "plt_stx_" + name[len("fr_") :] - mcp.add_tool(tool.model_copy(update={"name": new_name})) - - -# EOF diff --git a/src/scitex/_mcp_tools/introspect.py b/src/scitex/_mcp_tools/introspect.py deleted file mode 100755 index 343108872..000000000 --- a/src/scitex/_mcp_tools/introspect.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_tools/introspect.py - -"""Introspection module tools for FastMCP unified server.""" - -from typing import Optional - - -def register_introspect_tools(mcp) -> None: - """Register introspection tools with FastMCP server.""" - # IPython-style tools (primary) - - @mcp.tool() - async def introspect_signature( - dotted_path: str, - include_defaults: bool = True, - include_annotations: bool = True, - ) -> str: - """Return a function/class signature (parameters, types, defaults) by dotted import path — IPython `?` for any installed Python object. Drop-in replacement for `inspect.signature` + manual `typing.get_type_hints` + hand-formatted argspec dumps. Use whenever the user asks "what's the signature of X?", "how do I call scitex.io.save?", "what args does this take?", or is writing a call and needs parameter names without opening the source.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import q_handler - - return await async_wrap_as_mcp( - q_handler, - idempotent=True, - dotted_path=dotted_path, - include_defaults=include_defaults, - include_annotations=include_annotations, - ) - - @mcp.tool() - async def introspect_source( - dotted_path: str, - max_lines: Optional[int] = None, - include_decorators: bool = True, - ) -> str: - """Return the source code of a Python object by dotted import path — IPython `??`. Drop-in replacement for `inspect.getsource` + hunting through `pip show` paths or cloned repos. Use whenever the user asks "show me the source of X", "what does scitex.io.save actually do?", "read that function", or is debugging unexpected behavior and wants to see the implementation without opening a file.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import qq_handler - - return await async_wrap_as_mcp( - qq_handler, - idempotent=True, - dotted_path=dotted_path, - max_lines=max_lines, - include_decorators=include_decorators, - ) - - @mcp.tool() - async def introspect_dir( - dotted_path: str, - filter: str = "public", - kind: Optional[str] = None, - include_inherited: bool = False, - ) -> str: - """List attribute names of a module / class / instance with visibility + kind filter — `dir()` on steroids. `filter='public'` hides dunders + underscores; `kind='function'|'class'|'module'` filters by type; `include_inherited=True` walks the MRO. Drop-in replacement for `dir(obj)` + manual `startswith('_')` / `callable` filtering. Use whenever the user asks "what's in scitex.plt?", "list methods of this class", "show public API of module X", or is exploring an unfamiliar package.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import dir_handler - - return await async_wrap_as_mcp( - dir_handler, - idempotent=True, - dotted_path=dotted_path, - filter=filter, - kind=kind, - include_inherited=include_inherited, - ) - - @mcp.tool() - async def introspect_api( - dotted_path: str, - max_depth: int = 5, - docstring: bool = False, - root_only: bool = False, - ) -> str: - """Recursively walk a package / module tree and return the full API as indented text — every submodule, class, function down to `max_depth`. Drop-in replacement for hand-crafted `pkgutil.walk_packages` + `dir()` loops. Use whenever the user asks "show me the whole API of scitex.stats", "map the package layout", "what does this expose?", or is getting oriented in a large library before coding.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import list_api_handler - - return await async_wrap_as_mcp( - list_api_handler, - idempotent=True, - dotted_path=dotted_path, - max_depth=max_depth, - docstring=docstring, - root_only=root_only, - ) - - @mcp.tool() - async def introspect_docstring( - dotted_path: str, - format: str = "raw", - ) -> str: - """Return the docstring of any dotted-path object — raw, parsed into sections (Summary / Args / Returns / Raises / Examples), or just the one-line summary. Drop-in replacement for `obj.__doc__`, `inspect.getdoc`, and manual NumPy/Google-style parsing. Use whenever the user asks "what does this function do?", "show docstring for X", "summarize this API", or is reading documentation inline.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import docstring_handler - - return await async_wrap_as_mcp( - docstring_handler, - idempotent=True, - dotted_path=dotted_path, - format=format, - ) - - @mcp.tool() - async def introspect_exports(dotted_path: str) -> str: - """Return a module's `__all__` list — the officially-exposed public API names. Drop-in replacement for `import pkg; pkg.__all__`. Use when the user asks "what's exported from scitex.stats?", "list the public API", "which names are re-exported?", or is writing `from X import *` and wants to know what they'll get.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import exports_handler - - return await async_wrap_as_mcp( - exports_handler, - idempotent=True, - dotted_path=dotted_path, - ) - - @mcp.tool() - async def introspect_examples( - dotted_path: str, - search_paths: Optional[str] = None, - max_results: int = 10, - ) -> str: - """Grep the repo's `tests/` and `examples/` for actual call sites of an object — real usage, not just docstring examples. Drop-in replacement for `rg 'scitex.io.save' tests/ examples/`. Use when the user asks "how do I use this function?", "show me real examples of X", "what does idiomatic usage look like?", or is learning a new API by example rather than from docstring.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import examples_handler - - # Parse search_paths if provided as comma-separated string - paths_list = None - if search_paths: - paths_list = [p.strip() for p in search_paths.split(",")] - - return await async_wrap_as_mcp( - examples_handler, - idempotent=True, - dotted_path=dotted_path, - search_paths=paths_list, - max_results=max_results, - ) - - # Advanced introspection tools - - @mcp.tool() - async def introspect_class_hierarchy( - dotted_path: str, - include_builtins: bool = False, - max_depth: int = 10, - ) -> str: - """Return a class's inheritance tree — full MRO (walking up) + known subclasses (walking down) via `__subclasses__`. Drop-in replacement for `Cls.__mro__` + manual `issubclass` scans. Use when the user asks "what does X inherit from?", "show subclass tree of Y", "why does isinstance(Z) match?", or is debugging MRO / method-resolution issues in a class hierarchy.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import class_hierarchy_handler - - return await async_wrap_as_mcp( - class_hierarchy_handler, - idempotent=True, - dotted_path=dotted_path, - include_builtins=include_builtins, - max_depth=max_depth, - ) - - @mcp.tool() - async def introspect_type_hints( - dotted_path: str, - include_extras: bool = True, - ) -> str: - """Resolve every type hint on a function/class — including forward refs, generics, Union / Optional / Literal / Annotated extras. Drop-in replacement for `typing.get_type_hints` + `typing.get_args` / `get_origin` by hand. Use when the user asks "what type is this param?", "show type hints for X", "does this accept None?", or is debugging a type-checking error / writing a wrapper that needs to match an existing signature.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import type_hints_handler - - return await async_wrap_as_mcp( - type_hints_handler, - idempotent=True, - dotted_path=dotted_path, - include_extras=include_extras, - ) - - @mcp.tool() - async def introspect_imports( - dotted_path: str, - categorize: bool = True, - ) -> str: - """AST-parse a module's source and list every `import` / `from ... import` it uses — optionally grouped as stdlib / third-party / local. Drop-in replacement for `ast.parse` + hand-written `ast.NodeVisitor` + `importlib.util.find_spec` stdlib-detection. Use when the user asks "what does X import?", "categorize this module's dependencies", "is this using any third-party deps?", or is auditing imports before a refactor.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import imports_handler - - return await async_wrap_as_mcp( - imports_handler, - idempotent=True, - dotted_path=dotted_path, - categorize=categorize, - ) - - @mcp.tool() - async def introspect_dependencies( - dotted_path: str, - recursive: bool = False, - max_depth: int = 3, - ) -> str: - """Resolve the transitive dependency graph of a module — every other module it imports, optionally walking recursively to `max_depth`. Drop-in replacement for `pip show` + `modulefinder.ModuleFinder` + hand-walked `__import__` traces. Use when the user asks "what depends on X?", "show me the transitive deps", "is this module pulling in pandas?", or is planning a refactor / deletion and needs to know impact.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import dependencies_handler - - return await async_wrap_as_mcp( - dependencies_handler, - idempotent=True, - dotted_path=dotted_path, - recursive=recursive, - max_depth=max_depth, - ) - - @mcp.tool() - async def introspect_call_graph( - dotted_path: str, - max_depth: int = 2, - timeout_seconds: int = 10, - internal_only: bool = True, - ) -> str: - """Build a call graph rooted at a function — which other functions it calls, recursively to `max_depth`, with a wall-clock timeout (AST-based, safe on unknown code). `internal_only=True` ignores stdlib / third-party. Drop-in replacement for `pyan3`, `snakefood`, or hand-running `grep`-based call-site hunts. Use when the user asks "what does this call?", "show the call graph for X", "trace how function Y reaches Z", or is debugging / refactoring a tangled function chain.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.introspect._mcp.handlers import call_graph_handler - - return await async_wrap_as_mcp( - call_graph_handler, - idempotent=True, - dotted_path=dotted_path, - max_depth=max_depth, - timeout_seconds=timeout_seconds, - internal_only=internal_only, - ) diff --git a/src/scitex/_mcp_tools/linter.py b/src/scitex/_mcp_tools/linter.py deleted file mode 100755 index 997bf1a1a..000000000 --- a/src/scitex/_mcp_tools/linter.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -"""Linter module tools — delegate to scitex_dev.linter MCP tools. - -Single source of truth: `scitex_dev.linter._mcp.tools`. The engine -moved out of the (archived) scitex-linter package; this stays as a -thin re-export so the umbrella's MCP server still exposes the same -tools. -""" - - -def register_linter_tools(mcp) -> None: - """Register linter tools by delegating to scitex_dev.linter.""" - try: - from scitex_dev.linter._mcp.tools import register_all_tools - - register_all_tools(mcp) - except ImportError: - - @mcp.tool() - def linter_usage() -> str: - """Get usage guide for SciTeX Linter (not installed).""" - return "scitex-dev is required. Install with: pip install scitex-dev" - - -# EOF diff --git a/src/scitex/_mcp_tools/project.py b/src/scitex/_mcp_tools/project.py deleted file mode 100755 index 3e3dd52a4..000000000 --- a/src/scitex/_mcp_tools/project.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-02-19 -# File: scitex/_mcp_tools/project.py -"""MCP tool registration for project file operations.""" - - -def register_project_tools(mcp) -> None: - """Register project file tools with FastMCP server.""" - - @mcp.tool() - async def project_list_files( - root_path: str, - relative_path: str = ".", - max_depth: int = 3, - ) -> str: - """Tree-walk a project's directory and return a nested file/dir listing bounded by `max_depth` — for agents that see a project as a remote root and need to orient before editing. Drop-in replacement for `tree -L N`, `ls -R`, and hand-rolled `os.walk` with depth tracking. Use when the agent is handed a `root_path` and needs to know what's in there before reading, or when the user asks "what's in this project?", "list the tree", "show me the layout". - - Parameters - ---------- - root_path : str - Absolute path to the project root (provided in system context). - relative_path : str - Sub-path within the project to list. Default is project root. - max_depth : int - How many directory levels to recurse (1–6, default 3). - - Returns - ------- - str - JSON with {"success": bool, "tree": [...], "path": str}. - """ - from scitex.project._mcp.handlers import list_files_handler - - result = await list_files_handler(root_path, relative_path, max_depth) - - # Handler already called above; wrap result directly - from scitex_dev.types import Result - - return Result( - success=True, - data=result, - idempotent=True, - ).to_json() - - @mcp.tool() - async def project_read_file( - root_path: str, - relative_path: str, - ) -> str: - """Read a text file's contents from a project root, auto-truncating at 64 KB so big files don't blow context. Drop-in replacement for `open(path).read()` with manual size guards. Use when an agent needs the content of a specific project file — e.g. a config, source module, or README — and knows the relative path. First call `project_list_files` or `project_search_files` if the path is uncertain. - - Parameters - ---------- - root_path : str - Absolute path to the project root. - relative_path : str - Path to the file relative to root_path. - - Returns - ------- - str - JSON with {"success": bool, "content": str, "size_bytes": int, - "truncated": bool}. - Files larger than 64 KB are truncated. - """ - from scitex.project._mcp.handlers import read_file_handler - - result = await read_file_handler(root_path, relative_path) - - # Handler already called above; wrap result directly - from scitex_dev.types import Result - - return Result( - success=True, - data=result, - idempotent=True, - ).to_json() - - @mcp.tool() - async def project_write_file( - root_path: str, - relative_path: str, - content: str, - ) -> str: - """Write or overwrite a *text* file inside a project root, auto-`mkdir -p`ing any missing parent directories. Drop-in replacement for `pathlib.Path(...).write_text()` + manual `parents=True, exist_ok=True`. Use when an agent needs to create / overwrite a config, script, README, or code file at a known relative path. For binary outputs (.png, .mp3, .mp4) use `project_exec_python` or `project_exec_shell` instead. - - Creates any missing parent directories automatically. - - Parameters - ---------- - root_path : str - Absolute path to the project root. - relative_path : str - Path to the file relative to root_path. - content : str - Text content to write (overwrites existing file). - - Returns - ------- - str - JSON with {"success": bool, "path": str, "size_bytes": int}. - """ - from scitex.project._mcp.handlers import write_file_handler - - result = await write_file_handler(root_path, relative_path, content) - - # Handler already called above; wrap result directly - from scitex_dev.types import Result - - return Result( - success=True, - data=result, - side_effects=["file_modify: writes or creates file in project"], - ).to_json() - - @mcp.tool() - async def project_search_files( - root_path: str, - name_pattern: str = "", - content_pattern: str = "", - relative_path: str = ".", - max_results: int = 50, - ) -> str: - """Find files in a project by filename glob and/or a substring inside the file contents, capped at `max_results`. Drop-in replacement for `find . -name '*.py' | xargs grep -l foo`, ripgrep, or `pathlib.Path.rglob(...)`. Use when the agent needs to locate a file whose exact path isn't known — "find configs", "grep for FUNC_NAME across the project", "where's the test for X?", before reading with `project_read_file`. - - At least one of name_pattern or content_pattern must be provided. - - Parameters - ---------- - root_path : str - Absolute path to the project root. - name_pattern : str - Glob pattern for filename (e.g. "*.py", "main*"). - content_pattern : str - Substring to search for inside file contents. - relative_path : str - Sub-directory to search within (default: project root). - max_results : int - Maximum matches to return (default 50). - - Returns - ------- - str - JSON with {"success": bool, "matches": [...], "count": int, - "truncated": bool}. - Each match has "path" and optionally "line"/"preview" for content hits. - """ - from scitex.project._mcp.handlers import search_files_handler - - result = await search_files_handler( - root_path, name_pattern, content_pattern, relative_path, max_results - ) - - # Handler already called above; wrap result directly - from scitex_dev.types import Result - - return Result( - success=True, - data=result, - idempotent=True, - ).to_json() - - @mcp.tool() - async def project_exec_python( - root_path: str, - code: str, - timeout: int = 30, - ) -> str: - """Run an arbitrary Python snippet with `cwd` pinned to the project root, capturing stdout / stderr / exit code and reporting new files created — the escape hatch when `project_write_file` (text-only) cannot produce the needed output. Drop-in replacement for `subprocess.run(['python', '-c', code], cwd=root)`. Use when the agent must produce binary artifacts (PNG via matplotlib, MP3 via pydub, `.npz` via numpy, PDFs via reportlab) or run a computation that `project_write_file` can't express. - - Use this to generate binary files (audio, video, images) that - project_write_file cannot create (it only writes text). - The code runs with cwd set to the project root. - - Parameters - ---------- - root_path : str - Absolute path to the project root. - code : str - Python code to execute. Use print() for output. - timeout : int - Max execution time in seconds (5–60, default 30). - - Returns - ------- - str - JSON with {"success": bool, "exit_code": int, - "stdout": str, "stderr": str, "new_files": [...]}. - """ - from scitex.project._mcp.handlers import exec_python_handler - - result = await exec_python_handler(root_path, code, timeout) - - # Handler already called above; wrap result directly - from scitex_dev.types import Result - - return Result( - success=True, - data=result, - side_effects=["code_exec: runs Python code in project directory"], - ).to_json() - - @mcp.tool() - async def project_exec_shell( - root_path: str, - command: str, - timeout: int = 30, - ) -> str: - """Run an arbitrary `/bin/bash` command with `cwd` pinned to the project root, capturing stdout / stderr / exit code and reporting new files created. Drop-in replacement for `subprocess.run(['bash', '-c', cmd], cwd=root)`. Use when the agent needs external binaries — `ffmpeg` for audio/video transcode, `sox` for audio edits, `imagemagick convert` for image ops, `pandoc` for doc conversion, `latex`/`pdflatex` for document build, `git` for VCS ops, `ls -la` / `du -sh` for diagnostics. - - Use this to run system commands (ffmpeg, sox, imagemagick, etc.) - for file processing. The command runs via /bin/bash with cwd - set to the project root. - - Parameters - ---------- - root_path : str - Absolute path to the project root. - command : str - Shell command to execute. - timeout : int - Max execution time in seconds (5–60, default 30). - - Returns - ------- - str - JSON with {"success": bool, "exit_code": int, - "stdout": str, "stderr": str, "new_files": [...]}. - """ - from scitex.project._mcp.handlers import exec_shell_handler - - result = await exec_shell_handler(root_path, command, timeout) - - # Handler already called above; wrap result directly - from scitex_dev.types import Result - - return Result( - success=True, - data=result, - side_effects=["shell_exec: runs shell command in project directory"], - ).to_json() - - -# EOF diff --git a/src/scitex/_mcp_tools/skills.py b/src/scitex/_mcp_tools/skills.py deleted file mode 100755 index a4c0c9cc6..000000000 --- a/src/scitex/_mcp_tools/skills.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -"""MCP tools for skills aggregation across the SciTeX ecosystem.""" - -from typing import Optional - - -def register_skills_tools(mcp) -> None: - """Register skills discovery MCP tools.""" - - @mcp.tool() - async def skills_list(package: Optional[str] = None) -> str: - """Enumerate every `SKILL.md` + sub-skill reference page the installed SciTeX ecosystem ships — core + per-package + per-topic leaves. Drop-in replacement for `find ~/.claude/skills -name SKILL.md` or manually walking `site-packages/*/scitex_*/_skills/`. Use when the user asks "what SciTeX skills do I have?", "list skill pages for scitex-stats", "show everything under scitex-writer", or is orienting before `skills_get`. - - Args: - package: Filter to a specific package (e.g. "scitex-stats"). - None returns all packages. - """ - from scitex_dev.ecosystem import wrap_as_mcp - from scitex_dev.skills import list_skills - - return wrap_as_mcp( - list_skills, - idempotent=True, - package=package, - ) - - @mcp.tool() - async def skills_get( - package: str, - name: Optional[str] = None, - ) -> str: - """Read the markdown content of a specific SciTeX skill page (main `SKILL.md` or a named reference leaf). Drop-in replacement for hand-walking `~/.claude/skills/scitex//.md`. Use when the user asks "show me the scitex-stats skill", "get the figrecipe plot-types reference", "read the skill for X", or when an agent needs deep guidance beyond what the auto-loaded frontmatter description conveyed. - - Args: - package: Package name (e.g. "scitex-stats"). - name: Reference name (e.g. "test-selection"). - None returns the main SKILL.md. - """ - from scitex_dev.ecosystem import wrap_as_mcp - from scitex_dev.skills import get_skill - - return wrap_as_mcp( - get_skill, - idempotent=True, - package=package, - name=name, - ) diff --git a/src/scitex/_mcp_tools/social.py b/src/scitex/_mcp_tools/social.py deleted file mode 100755 index b431cbb07..000000000 --- a/src/scitex/_mcp_tools/social.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-27 -# File: /home/ywatanabe/proj/scitex-python/src/scitex/_mcp_tools/social.py -"""Social module tools - thin wrapper delegating to socialia package. - -Single source of truth: socialia MCP tools. -""" - - -def register_social_tools(mcp) -> None: - """Register social tools by delegating to socialia package. - - Only registers core tools (post, delete, status). - Analytics tools are excluded to reduce tool count. - """ - try: - from socialia._mcp.tools import social - - # Register only core social tools (post, delete, status) - # Analytics tools (pageviews, realtime, sources, track) excluded - social.register_tools(mcp) - except ImportError: - # Fallback when socialia is not installed - @mcp.tool() - def social_status() -> str: - """Get social media status (not installed).""" - return "socialia is required. Install with: pip install socialia" - - -# EOF diff --git a/src/scitex/_mcp_tools/stats.py b/src/scitex/_mcp_tools/stats.py deleted file mode 100755 index d9649a954..000000000 --- a/src/scitex/_mcp_tools/stats.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-03-11 -# File: src/scitex/_mcp_tools/stats.py -"""Stats tools for FastMCP unified server. - -Programmatically bridges all scitex-stats MCP tools into scitex MCP. -No manual wrapping — any new stats tool appears automatically. -""" - - -def register_stats_tools(mcp) -> None: - """Mount scitex-stats MCP server with 'stats' prefix. - - Uses mcp.mount() — same pattern as cloud, crossref-local, openalex-local. - Tools are prefixed: stats_run_test, stats_recommend_tests, etc. - """ - # Try standalone package first, then fall back to internal module - try: - from scitex_stats._server import mcp as stats_mcp - - from ._compat import safe_mount - - safe_mount(mcp, stats_mcp, namespace="stats") - except ImportError: - try: - from scitex.stats._mcp.server import mcp as stats_mcp - - from ._compat import safe_mount - - safe_mount(mcp, stats_mcp, namespace="stats") - except ImportError: - - @mcp.tool() - async def stats_not_available() -> str: - """scitex-stats not installed.""" - return ( - "scitex-stats package required. " - "Install with: pip install scitex[stats]" - ) - - -# EOF diff --git a/src/scitex/_mcp_tools/template.py b/src/scitex/_mcp_tools/template.py deleted file mode 100755 index a7418be50..000000000 --- a/src/scitex/_mcp_tools/template.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2026-01-15 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_tools/template.py -"""Template module tools for FastMCP unified server.""" - -from typing import Optional - - -def register_template_tools(mcp) -> None: - """Register template tools with FastMCP server.""" - - @mcp.tool() - async def template_clone_template( - template_id: str, - project_name: str, - target_dir: Optional[str] = None, - git_strategy: str = "child", - branch: Optional[str] = None, - tag: Optional[str] = None, - ) -> str: - """Bootstrap a new SciTeX project by cloning a registered template repo (research-project, pip-package, manuscript, etc.) and seeding it with the chosen name — optionally at a specific branch/tag, with configurable git wiring (`child` submodule / `squash` flatten / `fork` new repo / `none`). Drop-in replacement for `git clone` + `cp -r` + hand-rewriting names in every template file + `git init`. Use when the user asks to "start a new SciTeX project", "clone the research template", "scaffold a paper repo", "initialize a new package from template", or is setting up fresh work.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.template._mcp.handlers import clone_template_handler - - return await async_wrap_as_mcp( - clone_template_handler, - side_effects=["file_create: new project directory from template"], - template_id=template_id, - project_name=project_name, - target_dir=target_dir, - git_strategy=git_strategy, - branch=branch, - tag=tag, - ) - - @mcp.tool() - async def template_list_git_strategies() -> str: - """Enumerate the git wiring options `template_clone_template` accepts — `child` (submodule), `squash` (flatten history), `fork` (new repo with detached history), `none` (no git). Use when the user asks "how do I wire git for the new project?", "what git options does the template support?", or is deciding between submodule vs standalone.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.template._mcp.handlers import list_git_strategies_handler - - return await async_wrap_as_mcp( - list_git_strategies_handler, - idempotent=True, - ) - - @mcp.tool() - async def template_get_code_template( - template_id: str, - filepath: Optional[str] = None, - docstring: Optional[str] = None, - ) -> str: - """Return a boilerplate code template for a SciTeX script/module — core patterns (`session`, `io`, `config`) or per-module usage examples (`plt`, `stats`, `scholar`, `audio`, `capture`, `diagram`, `writer`, …), or `'all'` for every template concatenated. Drop-in replacement for copy-pasting from an old script, hand-writing `stx.session.start()` boilerplate, or re-reading SKILL.md just to recall idiomatic structure. Use when the user asks "scaffold a scitex script", "give me a stats experiment template", "how do I start a scitex session?", or is beginning a new `.py` and wants the standard structure. Optional `filepath` / `docstring` personalize the header.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.template._mcp.handlers import get_code_template_handler - - return await async_wrap_as_mcp( - get_code_template_handler, - idempotent=True, - template_id=template_id, - filepath=filepath, - docstring=docstring, - ) - - @mcp.tool() - async def template_list_code_templates() -> str: - """Return the catalog of every code template `template_get_code_template` can serve — core (`session`, `io`, `config`) plus per-module usage templates. Use when the user asks "what templates are available?", "which modules have boilerplate?", or before picking a `template_id` to fetch.""" - from scitex_dev.ecosystem import async_wrap_as_mcp - - from scitex.template._mcp.handlers import list_code_templates_handler - - return await async_wrap_as_mcp( - list_code_templates_handler, - idempotent=True, - ) - - -# EOF diff --git a/src/scitex/_mcp_tools/tunnel.py b/src/scitex/_mcp_tools/tunnel.py deleted file mode 100755 index b48d37eb4..000000000 --- a/src/scitex/_mcp_tools/tunnel.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# File: scitex/_mcp_tools/tunnel.py -"""Tunnel tools for FastMCP unified server. - -Delegates to scitex.tunnel (which delegates to scitex_tunnel). -""" - - -def register_tunnel_tools(mcp) -> None: - """Register tunnel tools with FastMCP server.""" - - @mcp.tool() - async def tunnel_setup(port: int, bastion_server: str, secret_key_path: str) -> str: - """Install an `autossh`-backed `autossh-tunnel-.service` systemd unit that opens a reverse SSH tunnel (local → bastion:port) and auto-reconnects on drop. Drop-in replacement for hand-crafted `autossh -M 0 -NR port:localhost:22 user@host`, `/etc/systemd/system/autossh-tunnel-*.service`, `sshuttle`, `tmux + ssh -R` loops. Use when the user asks to "set up a reverse tunnel", "expose this machine through a bastion", "open port X on the jump host", or mentions bastion, jump host, NAT traversal, HPC login node. - - Creates an autossh systemd service for NAT traversal. - The tunnel forwards a remote port on the bastion server - back to the local machine's SSH port. - """ - from scitex_dev.ecosystem import wrap_as_mcp - - from scitex.tunnel import setup - - return wrap_as_mcp( - setup, - side_effects=["systemd_service: creates autossh service"], - port=port, - bastion_server=bastion_server, - secret_key_path=secret_key_path, - ) - - @mcp.tool() - async def tunnel_remove(port: int) -> str: - """Tear down an autossh reverse-tunnel unit — `systemctl stop + disable + rm unit file + daemon-reload`. Drop-in replacement for running those by hand. Use when the user asks to "remove the tunnel", "delete reverse tunnel on port X", "stop autossh", "decommission this route". - - Stops and disables the autossh systemd service for the given port. - """ - from scitex_dev.ecosystem import wrap_as_mcp - - from scitex.tunnel import remove - - return wrap_as_mcp( - remove, - side_effects=["systemd_service: stops and disables autossh service"], - port=port, - ) - - @mcp.tool() - async def tunnel_status(port: int = 0) -> str: - """Live state of autossh reverse-tunnel systemd units — active / inactive, PID, restart count, last journal lines. Drop-in replacement for `systemctl status autossh-tunnel-.service` + `journalctl -u`. Use when the user asks "is my tunnel up?", "why can't I reach port 2222?", "list all reverse tunnels", "check tunnel health". `port=0` lists everything. - - If port is 0 (default), shows all tunnel services. - Otherwise shows status for the specific port. - """ - from scitex_dev.ecosystem import wrap_as_mcp - - from scitex.tunnel import status - - return wrap_as_mcp( - status, - idempotent=True, - port=port if port else None, - ) - - -# EOF diff --git a/src/scitex/_mcp_tools/usage.py b/src/scitex/_mcp_tools/usage.py deleted file mode 100755 index d8cda1006..000000000 --- a/src/scitex/_mcp_tools/usage.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -"""Usage module MCP tools — discover scitex usage examples.""" - - -def register_usage_tools(mcp) -> None: - """Register usage discovery tools.""" - - @mcp.tool() - def usage_show(topic: str = "") -> str: - """Return a runnable code example for a SciTeX topic (`plt`, `stats`, `session`, `io`, `scholar`, …) — short, copy-pasteable snippets showing idiomatic usage. Drop-in replacement for hunting through `examples/`, rereading SKILL.md, or searching PyPI docs. Use when the user asks "how do I use scitex.plt?", "show me a t-test example", "give me a session boilerplate", "what's the idiomatic way to save a figure?", or needs a starting point rather than reference docs.""" - from scitex_dev.ecosystem import wrap_as_mcp - - from scitex.usage import show - - return wrap_as_mcp( - show, - idempotent=True, - topic=topic or None, - ) - - @mcp.tool() - def usage_list() -> str: - """List every topic `usage_show` can serve (`plt`, `stats`, `session`, `io`, `scholar`, `audio`, `writer`, …). Use when the user asks "what examples are available?", "which modules have usage snippets?", or before calling `usage_show` with a specific topic.""" - from scitex_dev.ecosystem import wrap_as_mcp - - from scitex.usage import topics - - return wrap_as_mcp( - topics, - idempotent=True, - ) - - -# EOF diff --git a/src/scitex/cli/mcp.py b/src/scitex/cli/mcp.py index 012a9a39b..d829455ef 100755 --- a/src/scitex/cli/mcp.py +++ b/src/scitex/cli/mcp.py @@ -90,7 +90,7 @@ def _estimate_tokens(text: str) -> int: def _get_all_tools(mcp_server) -> dict: """Get ALL tools including mounted sub-servers (crossref, openalex).""" - from scitex._mcp_tools._compat import get_tools_sync + from scitex._mcp import get_tools_sync return get_tools_sync(mcp_server) @@ -155,8 +155,8 @@ def list_tools( # Suppress INFO messages from env loader during import logging.getLogger("scitex.helpers._env_loader").setLevel(logging.WARNING) try: - from scitex.mcp_server import FASTMCP_AVAILABLE - from scitex.mcp_server import mcp as mcp_server + from scitex._mcp import FASTMCP_AVAILABLE + from scitex._mcp import mcp as mcp_server except ImportError: click.secho("ERROR: Could not import MCP server", fg="red", err=True) raise SystemExit(1) from None @@ -308,8 +308,8 @@ def doctor(verbose: bool): # Check 2: MCP server import click.echo("Checking MCP server module... ", nl=False) try: - from scitex.mcp_server import FASTMCP_AVAILABLE - from scitex.mcp_server import mcp as mcp_server + from scitex._mcp import FASTMCP_AVAILABLE + from scitex._mcp import mcp as mcp_server if FASTMCP_AVAILABLE and mcp_server: click.secho("OK", fg="green") @@ -320,58 +320,65 @@ def doctor(verbose: bool): click.secho("FAIL", fg="red") issues.append(f"Could not import MCP server: {e}") - # Check 3: _mcp_tools subpackage - click.echo("Checking _mcp_tools subpackage... ", nl=False) + # Check 3: unified MCP entrypoint + click.echo("Checking _mcp entrypoint... ", nl=False) try: - from scitex._mcp_tools import register_all_tools # noqa: F401 + from scitex._mcp import register_all_tools # noqa: F401 click.secho("OK", fg="green") except ImportError as e: click.secho("FAIL", fg="red") - issues.append(f"Could not import _mcp_tools: {e}") - - # Check 4: Individual module imports - modules = [ - "audio", - "canvas", - "capture", - "diagram", - "plt", - "scholar", - "stats", - "template", - "ui", - "writer", - ] - - click.echo("Checking module registrations... ", nl=False) - failed_modules = [] - for mod in modules: - try: - exec(f"from scitex._mcp_tools.{mod} import register_{mod}_tools") - except ImportError as e: - failed_modules.append((mod, str(e))) - - if failed_modules: - click.secho(f"FAIL ({len(failed_modules)} modules)", fg="red") - for mod, err in failed_modules: - issues.append(f"Module {mod}: {err}") - else: - click.secho("OK", fg="green") + issues.append(f"Could not import _mcp: {e}") - # Check 5: Tool count (including mounted sub-servers) + # Check 4: peer namespaces mounted via the registry. + # Peer tools resolve lazily, so probe the mounted-server namespaces + # directly rather than the (local-only) tool list. + click.echo("Checking peer namespaces mounted... ", nl=False) + try: + from scitex._mcp import mcp as _probe_mcp + + prefixes = set() + for srv in getattr(_probe_mcp, "_mounted_servers", []) or []: + ns = getattr(srv, "prefix", None) or getattr(srv, "namespace", None) + if ns: + prefixes.add(ns) + expected = {"io", "stats", "scholar"} + missing = expected - prefixes + if missing: + click.secho(f"FAIL (missing: {', '.join(sorted(missing))})", fg="red") + issues.append(f"Peer namespaces missing: {', '.join(sorted(missing))}") + else: + click.secho(f"OK ({len(prefixes)} namespaces)", fg="green") + except Exception as e: # noqa: BLE001 + click.secho("FAIL", fg="red") + issues.append(f"Could not probe peer namespaces: {e}") + + # Check 5: Umbrella-local tools + registry-mounted peer servers. + # Peer tools are resolved lazily by FastMCP at request time, so the + # meaningful health signal is "umbrella locals present AND several + # peer servers mounted", not a flat local tool count. click.echo("Checking tool registration... ", nl=False) try: - from scitex.mcp_server import mcp as mcp_server + from scitex._mcp import mcp as mcp_server if mcp_server: - all_tools = _get_all_tools(mcp_server) - n = len(all_tools) - if n >= 80: - click.secho(f"OK ({n} tools)", fg="green") + n_local = len(_get_all_tools(mcp_server)) + mounted = getattr(mcp_server, "_mounted_servers", []) or [] + n_mounted = len(mounted) + if n_local >= 20 and n_mounted >= 5: + click.secho( + f"OK ({n_local} local tools, {n_mounted} peers mounted)", + fg="green", + ) else: - click.secho(f"WARN ({n} tools, expected 80+)", fg="yellow") - warnings.append(f"Only {n} tools registered, expected 80+") + click.secho( + f"WARN ({n_local} local, {n_mounted} peers mounted)", + fg="yellow", + ) + warnings.append( + f"{n_local} local tools / {n_mounted} peer mounts " + "(expected >=20 local and >=5 peers)" + ) else: click.secho("SKIP", fg="yellow") except Exception as e: @@ -453,7 +460,7 @@ def _print_help_recursive(ctx): def start(transport: str, host: str, port: int): """Start the unified MCP server.""" try: - from scitex.mcp_server import run_server + from scitex._mcp import run_server except ImportError: click.secho("ERROR: Could not import MCP server", fg="red", err=True) click.echo("Run: pip install fastmcp") diff --git a/src/scitex/cli/scholar/__init__.py b/src/scitex/cli/scholar/__init__.py index 94ceec5c8..2824446ec 100755 --- a/src/scitex/cli/scholar/__init__.py +++ b/src/scitex/cli/scholar/__init__.py @@ -160,7 +160,7 @@ def start(transport, host, port): import sys try: - from scitex.mcp_server import run_server + from scitex._mcp import run_server if transport != "stdio": click.secho(f"Starting unified scitex MCP server ({transport})", fg="cyan") diff --git a/src/scitex/mcp_server.py b/src/scitex/mcp_server.py index f1544ef9f..6f37af22a 100755 --- a/src/scitex/mcp_server.py +++ b/src/scitex/mcp_server.py @@ -1,198 +1,25 @@ #!/usr/bin/env python3 -# Timestamp: 2026-01-15 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/mcp_server.py -# ---------------------------------------- - -""" -Unified FastMCP Server for SciTeX - Multi-Transport Support - -Provides all SciTeX tools via a single MCP server with stdio, SSE, and HTTP. - -Usage: - scitex serve # stdio (Claude Desktop) - scitex serve -t sse --port 8085 # SSE (remote via SSH) - scitex serve -t http --port 8085 # HTTP (streamable) - -Remote Setup: - 1. Local: scitex serve -t sse --port 8085 - 2. SSH: ssh -R 8085:localhost:8085 remote-host - 3. Remote: {"type": "sse", "url": "http://localhost:8085/sse"} +# Timestamp: 2026-05-31 +# File: src/scitex/mcp_server.py +"""Back-compat shim for the umbrella MCP server. + +The real implementation now lives in :mod:`scitex._mcp`, a single +registry-mounting entrypoint. This module re-exports its public surface so +existing callers (`from scitex.mcp_server import main`, `scitex serve`, +external `scitex.mcp_server:main` references) keep working. """ from __future__ import annotations -import json -import warnings - -# Load environment variables from SCITEX_ENV_SRC early -from scitex.helpers import load_scitex_env - -load_scitex_env() - -from scitex_dev import try_import_optional - -FastMCP = try_import_optional("fastmcp", "FastMCP", pkg="scitex") -FASTMCP_AVAILABLE = FastMCP is not None - -# Suppress httplib2 deprecation warnings from system pyparsing (old API methods) -# Must be AFTER fastmcp import (fastmcp.__init__ calls simplefilter("default", DeprecationWarning)) -# Must be BEFORE register_all_tools (which imports socialia → google.auth → httplib2) -warnings.filterwarnings( - "ignore", category=DeprecationWarning, message=".*deprecated.*use.*" +from scitex._mcp import ( # noqa: F401 + FASTMCP_AVAILABLE, + main, + mcp, + register_all_tools, + run_server, ) -__all__ = ["mcp", "run_server", "main", "FASTMCP_AVAILABLE"] - -if FASTMCP_AVAILABLE: - mcp = FastMCP( - name="scitex", - instructions="""\ -SciTeX: Python Framework for Scientific Research Automation (https://scitex.ai) - -IMPORTANT: When writing Python scripts for data analysis, visualization, or research, -ALWAYS use `import scitex as stx` and leverage these core patterns: - -## Core Patterns - USE THESE IN CODE GENERATION: - -### @stx.session - Reproducible Experiment Tracking -```python -import scitex as stx - -@stx.session -def main( - param1="default", # Auto-CLI: --param1 - CONFIG=stx.INJECTED, # Auto-injected config from ./config/*.yaml - plt=stx.INJECTED, # Pre-configured matplotlib - logger=stx.INJECTED, # Session logger -): - \"\"\"Docstring becomes --help description.\"\"\" - # Outputs auto-organized: script_out/FINISHED_SUCCESS// - stx.io.save(results, "results.csv") - return 0 -``` - -### stx.io - Universal File I/O (30+ formats) -```python -stx.io.save(df, "data.csv") # DataFrames -stx.io.save(arr, "data.npy") # NumPy arrays -stx.io.save(fig, "plot.png") # Figures (+ auto CSV export) -stx.io.save(obj, "data.pkl") # Any Python object -data = stx.io.load("data.csv") # Unified loading -``` - -### stx.plt - Publication-Ready Figures (Auto CSV Export) -```python -fig, ax = stx.plt.subplots() -ax.plot_line(x, y) # Data tracked automatically -ax.set_xyt("X Label", "Y Label", "Title") -stx.io.save(fig, "plot.png") # Saves plot.png + plot.csv -``` - -### stx.stats - Publication Statistics (23 tests) -```python -result = stx.stats.test_ttest_ind(g1, g2, return_as="dataframe") -# Returns: p-value, effect size (Cohen's d), CI, normality check, power -result = stx.stats.test_anova(*groups, return_as="latex") -``` - -### stx.scholar - Literature Management -```python -# CLI: scitex scholar bibtex papers.bib --project myresearch -# Enriches BibTeX with abstracts, DOIs, impact factors, downloads PDFs -``` - -## MCP Tools Available: -- [plt] plot, reproduce, compose, crop - **PRIORITY**: Use CSV column spec (data_file + column names) over inline arrays! - Workflow: Python writes CSV → plt_plot reads columns → Creates figure -- [audio] speak -- [capture] screenshot -- [stats] recommend_tests, run_test, format_results, power_analysis -- [scholar] search_papers, enrich_bibtex, fetch_papers, parse_pdf_content -- [diagram] create, compile_mermaid, compile_graphviz, render, split -- [template] clone_template, get_code_template, list_code_templates -- [ui] notify -- [writer] compile, figures, tables, bibliography (LaTeX manuscript) -- [introspect] signature, docstring, source, members (like IPython's ? and ??) - -## MCP Resources (Read for detailed docs): -- scitex://cheatsheet - Complete quick reference -- scitex://session-tree - Output directory structure explained -- scitex://module/io - stx.io file I/O documentation -- scitex://module/plt - stx.plt figure documentation -- scitex://module/stats - stx.stats statistical tests -- scitex://module/scholar - stx.scholar literature management -- scitex://module/session - @stx.session decorator guide -- scitex://io-formats - All 30+ supported file formats -- scitex://plt-figrecipe - stx.plt integration with FigRecipe - -## FigRecipe MCP Resources (for advanced plotting): -- figrecipe://cheatsheet - FigRecipe quick reference -- figrecipe://mcp-spec - Declarative plot specification format -- figrecipe://api/core - Full FigRecipe API documentation - -Use introspect_* tools to explore scitex API: introspect_members("scitex.stats") -""", - ) -else: - mcp = None - - -def _json(data: dict) -> str: - return json.dumps(data, indent=2, default=str) - - -# Register tools from each module -if FASTMCP_AVAILABLE: - from scitex._mcp_tools import register_all_tools - - register_all_tools(mcp) - - # Annotate all tools with standardized Result envelope schema - try: - from scitex_dev.types import RESULT_SCHEMA - - from scitex._mcp_tools._compat import get_tools_sync - - for tool in get_tools_sync(mcp).values(): - if getattr(tool, "output_schema", None) is None: - tool.output_schema = RESULT_SCHEMA - except Exception: - pass # Non-critical: schema annotation is informational - - # Register documentation resources - from scitex._mcp_resources import register_resources - - register_resources(mcp) - - -def run_server(transport: str = "stdio", host: str = "0.0.0.0", port: int = 8085): - """Run the unified MCP server with transport selection.""" - if not FASTMCP_AVAILABLE: - import sys - - print("=" * 60) - print("Requires 'fastmcp' package: pip install fastmcp") - print("=" * 60) - sys.exit(1) - - if transport == "stdio": - mcp.run(transport="stdio") - elif transport == "sse": - print(f"Starting scitex MCP (SSE) on {host}:{port}") - print(f"Remote: ssh -R {port}:localhost:{port} remote-host") - mcp.run(transport="sse", host=host, port=port) - elif transport == "http": - print(f"Starting scitex MCP (HTTP) on {host}:{port}") - mcp.run(transport="streamable-http", host=host, port=port) - else: - raise ValueError(f"Unknown transport: {transport}") - - -def main(): - """Entry point for scitex-mcp command.""" - run_server(transport="stdio") - +__all__ = ["mcp", "run_server", "main", "register_all_tools", "FASTMCP_AVAILABLE"] if __name__ == "__main__": main() diff --git a/tests/integration/test_cross_package_imports.py b/tests/integration/test_cross_package_imports.py index f6a7f56c6..d56e80540 100755 --- a/tests/integration/test_cross_package_imports.py +++ b/tests/integration/test_cross_package_imports.py @@ -28,10 +28,8 @@ "scitex._dev._sync_remote", "scitex._dev._test", "scitex._env_loader", - "scitex._mcp_resources", - "scitex._mcp_tools", - "scitex._mcp_tools._compat", - "scitex._mcp_tools.clew", + "scitex._mcp", + "scitex._mcp._compat", "scitex.audio", "scitex.audit", "scitex.bridge._figrecipe", diff --git a/tests/scitex/test_mcp_entrypoint.py b/tests/scitex/test_mcp_entrypoint.py new file mode 100755 index 000000000..5425556eb --- /dev/null +++ b/tests/scitex/test_mcp_entrypoint.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# Timestamp: 2026-05-31 +# File: tests/scitex/test_mcp_entrypoint.py +"""Tests for the single umbrella MCP entrypoint (``scitex._mcp``). + +The umbrella MCP server is a thin registry-mounting coordinator: it mounts +every non-archived peer's FastMCP under a brand-prefixed namespace and folds +in umbrella-only inline tools. These tests assert the collapse preserved +behavior: + +* the unified server builds and exposes its public API, +* it mounts >0 peer servers with brand-prefix namespacing, +* tool-name renames (figrecipe fr_* -> plt_stx_*) and namespace aliases + (crossref-local -> crossref) are applied, +* umbrella-only tool families survive, +* a missing optional peer does not crash the peer-extras registration. +""" + +from __future__ import annotations + +import pytest + +fastmcp = pytest.importorskip("fastmcp") + + +def _entrypoint(): + """Return the scitex._mcp module, skipping if FastMCP is unavailable.""" + from scitex import _mcp + + if not _mcp.FASTMCP_AVAILABLE or _mcp.mcp is None: + pytest.skip("umbrella MCP server not initialized") + return _mcp + + +def _mounted_prefixes(mcp) -> set: + """Namespaces of every sub-server mounted on the umbrella FastMCP.""" + prefixes = set() + for srv in getattr(mcp, "_mounted_servers", []) or []: + ns = getattr(srv, "prefix", None) or getattr(srv, "namespace", None) + if ns: + prefixes.add(ns) + return prefixes + + +def test_server_name_is_scitex(): + # Arrange + _mcp = _entrypoint() + # Act + name = getattr(_mcp.mcp, "name", None) + # Assert + assert name == "scitex" + + +def test_public_api_exposes_main(): + # Arrange + _mcp = _entrypoint() + # Act + has_main = callable(getattr(_mcp, "main", None)) + # Assert + assert has_main is True + + +def test_public_api_exposes_run_server(): + # Arrange + _mcp = _entrypoint() + # Act + has_run = callable(getattr(_mcp, "run_server", None)) + # Assert + assert has_run is True + + +def test_mounts_more_than_zero_peers(): + # Arrange + _mcp = _entrypoint() + # Act + prefixes = _mounted_prefixes(_mcp.mcp) + # Assert + assert len(prefixes) > 0 + + +# Core peers are hard dependencies of the umbrella (pyproject [project]). +@pytest.mark.parametrize("peer", ["io", "stats", "scholar"]) +def test_core_peer_mounted_with_brand_prefix(peer): + # Arrange + _mcp = _entrypoint() + # Act + prefixes = _mounted_prefixes(_mcp.mcp) + # Assert + assert peer in prefixes + + +def test_figrecipe_tools_renamed_to_plt_stx(): + # Arrange + _mcp = _entrypoint() + # Act + local = set(_mcp.get_tools_sync(_mcp.mcp)) + # Assert + assert any(n.startswith("plt_stx_") for n in local) + + +@pytest.mark.parametrize( + "family", + ["introspect", "usage", "docs", "skills", "template", "tunnel"], +) +def test_umbrella_only_family_present(family): + # Arrange + _mcp = _entrypoint() + # Act + local = set(_mcp.get_tools_sync(_mcp.mcp)) + # Assert + assert any(n.startswith(family + "_") for n in local) + + +def _crossref_mounted() -> bool: + """True when crossref-local (an optional extra) is importable + mounted.""" + _mcp = _entrypoint() + return "crossref" in _mounted_prefixes(_mcp.mcp) + + +@pytest.mark.skipif( + not _crossref_mounted(), reason="crossref-local (optional extra) not installed" +) +def test_crossref_namespace_alias_applied(): + # Arrange + _mcp = _entrypoint() + # Act + prefixes = _mounted_prefixes(_mcp.mcp) + # Assert + assert "crossref-local" not in prefixes + + +def test_peer_extras_registration_folds_in_brand_renamed_tools(): + # Arrange + from fastmcp import FastMCP + + from scitex._mcp import _peer_extras, get_tools_sync + + probe = FastMCP(name="probe") + # Act + _peer_extras.register_peer_extras(probe) + local = set(get_tools_sync(probe)) + # Assert + assert any(n.startswith("plt_stx_") for n in local) + + +# EOF diff --git a/tests/scitex/test_thin_wrapper_consistency.py b/tests/scitex/test_thin_wrapper_consistency.py old mode 100644 new mode 100755 index 29ab5835e..11ce3b065 --- a/tests/scitex/test_thin_wrapper_consistency.py +++ b/tests/scitex/test_thin_wrapper_consistency.py @@ -30,17 +30,35 @@ def _umbrella_tool_names() -> set: - """All umbrella MCP tool names, enumerated in-process (no subprocess).""" + """All umbrella MCP tool names, enumerated in-process (no subprocess). + + Includes both the umbrella's own local tools AND every registry-mounted + peer's tools under their mount namespace. FastMCP 2.x resolves mounted + sub-server tools lazily (they are absent from the parent ``get_tools``), + so we enumerate each mounted server directly and re-apply its prefix. + """ fastmcp = pytest.importorskip("fastmcp") # noqa: F841 try: - from scitex._mcp_tools._compat import get_tools_sync - from scitex.mcp_server import FASTMCP_AVAILABLE - from scitex.mcp_server import mcp as mcp_server + from scitex._mcp import FASTMCP_AVAILABLE, get_tools_sync + from scitex._mcp import mcp as mcp_server except ImportError as exc: pytest.skip(f"umbrella MCP server unavailable: {exc}") if not FASTMCP_AVAILABLE or mcp_server is None: pytest.skip("umbrella MCP server not initialized") - return set(get_tools_sync(mcp_server).keys()) + + names = set(get_tools_sync(mcp_server).keys()) + for srv in getattr(mcp_server, "_mounted_servers", []) or []: + prefix = getattr(srv, "prefix", None) or getattr(srv, "namespace", None) + sub = getattr(srv, "server", None) or getattr(srv, "_server", None) + if sub is None: + continue + try: + sub_names = get_tools_sync(sub, include_mounted=False).keys() + except Exception: + continue + for n in sub_names: + names.add(f"{prefix}_{n}" if prefix else n) + return names def _standalone_cli_tool_names(argv: list, prefixes: tuple) -> set: From 3832d409c17f6f551862d95b4664833f40bf4631 Mon Sep 17 00:00:00 2001 From: Yusuke Watanabe Date: Sun, 31 May 2026 10:15:34 +0900 Subject: [PATCH 2/3] docs(ecosystem): reflect registry-mount MCP architecture (no per-pkg bridges) --- docs/08_SCITEX_ECOSYSTEM.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/08_SCITEX_ECOSYSTEM.md b/docs/08_SCITEX_ECOSYSTEM.md index 7fe439997..693a2c20b 100644 --- a/docs/08_SCITEX_ECOSYSTEM.md +++ b/docs/08_SCITEX_ECOSYSTEM.md @@ -115,20 +115,20 @@ from socialia import Twitter, LinkedIn, Reddit, YouTube, GoogleAnalytics, BasePo **MCP Tools:** ```python -# scitex/_mcp_tools/writer.py -def register_writer_tools(mcp): - from scitex_writer._mcp.tools import register_all_tools - register_all_tools(mcp) # Delegate to downstream - -# scitex/_mcp_tools/social.py -def register_social_tools(mcp): - from socialia._mcp.tools import register_all_tools - register_all_tools(mcp) # Delegate to downstream +# scitex/_mcp/__init__.py — ONE registry-mounting entrypoint. +# Each peer's FastMCP server is auto-mounted under a brand-prefixed +# namespace by iterating scitex_dev._ecosystem.ECOSYSTEM. No per-package +# "register__tools" bridge files; new peer tools appear automatically. +def register_all_tools(mcp): + for _pip, import_name, namespace in _iter_registry(): + peer_mcp = _resolve_peer_mcp(import_name) # e.g. scitex_writer._mcp.server + if peer_mcp is not None: + safe_mount(mcp, peer_mcp, namespace=namespace) # -> writer_*, socialia_* ``` -- Single source of truth: downstream package +- Single source of truth: the ecosystem registry + each downstream package - API parity: `scitex.writer` ≈ `scitex_writer`, `scitex.social` ≈ `socialia` -- MCP tools delegated to downstream's `register_all_tools(mcp)` +- MCP tools auto-mounted from each peer's `_mcp_server` — no umbrella maintenance - Use `scitex introspect api` to verify consistency ### Enhanced Wrapper (scitex.plt, scitex.scholar) From 601dd6cecfb2c17cdd789d7558630a2ee5d933a0 Mon Sep 17 00:00:00 2001 From: Yusuke Watanabe Date: Sun, 31 May 2026 10:22:51 +0900 Subject: [PATCH 3/3] fix(mcp): make mount-namespace checks robust across FastMCP 2.x/3.x CI runs fastmcp==3.3.1; local dev has 2.13.2. FastMCP 2.x exposes mounted peers via _mounted_servers (tools resolve lazily, absent from get_tools), while 3.x folds mounted tools into list_tools() with _ prefixes and has no stable _mounted_servers. The new test_mcp_entrypoint mount assertions and the mcp doctor checks reached into the 2.x-only _mounted_servers attr, so they failed on 3.x. Add _compat.mounted_namespaces() that unions both signals (2.x mounted-server prefixes + prefixes derived from resolvable tool names) and use it in the entrypoint tests and the doctor's namespace/registration checks. --- src/scitex/_mcp/__init__.py | 3 ++- src/scitex/_mcp/_compat.py | 36 ++++++++++++++++++++++++++++- src/scitex/cli/mcp.py | 29 +++++++++++------------ tests/scitex/test_mcp_entrypoint.py | 13 ++++------- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/scitex/_mcp/__init__.py b/src/scitex/_mcp/__init__.py index 9ae2605ae..7901cf874 100755 --- a/src/scitex/_mcp/__init__.py +++ b/src/scitex/_mcp/__init__.py @@ -40,7 +40,7 @@ from scitex_dev import try_import_optional -from ._compat import get_tools_sync, safe_mount +from ._compat import get_tools_sync, mounted_namespaces, safe_mount logger = logging.getLogger(__name__) @@ -61,6 +61,7 @@ "register_all_tools", "safe_mount", "get_tools_sync", + "mounted_namespaces", "FASTMCP_AVAILABLE", ] diff --git a/src/scitex/_mcp/_compat.py b/src/scitex/_mcp/_compat.py index 31aa36f37..d3bae7637 100755 --- a/src/scitex/_mcp/_compat.py +++ b/src/scitex/_mcp/_compat.py @@ -10,7 +10,41 @@ import asyncio -__all__ = ["get_tools_sync", "safe_mount"] +__all__ = ["get_tools_sync", "safe_mount", "mounted_namespaces"] + + +def mounted_namespaces(mcp_server) -> set: + """Namespaces of every mounted peer sub-server. Works on FastMCP 2.x & 3.x. + + FastMCP 2.x exposes mounted sub-servers via ``_mounted_servers`` (each with + a ``prefix``/``namespace``) and resolves their tools lazily — so the parent + ``get_tools`` does NOT include them. + + FastMCP 3.x folds mounted tools into ``list_tools()`` directly (no stable + ``_mounted_servers`` attribute), so the namespace is the ``_`` prefix on + each mounted tool name. + + We union both signals so the same call yields the mounted peer namespaces + regardless of FastMCP major version. + """ + namespaces = set() + + # FastMCP 2.x: explicit mounted-server records. + for srv in getattr(mcp_server, "_mounted_servers", []) or []: + ns = getattr(srv, "prefix", None) or getattr(srv, "namespace", None) + if ns: + namespaces.add(ns) + + # FastMCP 3.x (and any version): prefixes on resolvable tool names. A + # mounted tool is ``_``; the umbrella's own local tools + # also share this shape, so this can include a few umbrella-only prefixes + # (introspect, usage, ...). Callers asserting on peer namespaces should + # check membership, not exact equality. + for name in get_tools_sync(mcp_server): + if "_" in name: + namespaces.add(name.split("_", 1)[0]) + + return namespaces def get_tools_sync(mcp_server, include_mounted: bool = True) -> dict: diff --git a/src/scitex/cli/mcp.py b/src/scitex/cli/mcp.py index d829455ef..94b52c73f 100755 --- a/src/scitex/cli/mcp.py +++ b/src/scitex/cli/mcp.py @@ -336,12 +336,9 @@ def doctor(verbose: bool): click.echo("Checking peer namespaces mounted... ", nl=False) try: from scitex._mcp import mcp as _probe_mcp + from scitex._mcp import mounted_namespaces - prefixes = set() - for srv in getattr(_probe_mcp, "_mounted_servers", []) or []: - ns = getattr(srv, "prefix", None) or getattr(srv, "namespace", None) - if ns: - prefixes.add(ns) + prefixes = mounted_namespaces(_probe_mcp) expected = {"io", "stats", "scholar"} missing = expected - prefixes if missing: @@ -353,31 +350,31 @@ def doctor(verbose: bool): click.secho("FAIL", fg="red") issues.append(f"Could not probe peer namespaces: {e}") - # Check 5: Umbrella-local tools + registry-mounted peer servers. - # Peer tools are resolved lazily by FastMCP at request time, so the - # meaningful health signal is "umbrella locals present AND several - # peer servers mounted", not a flat local tool count. + # Check 5: Umbrella-local tools + registry-mounted peer namespaces. + # On FastMCP 2.x peer tools resolve lazily; on 3.x they fold into the + # tool list. `mounted_namespaces` is robust across both, so the health + # signal is "umbrella locals present AND several peer namespaces present". click.echo("Checking tool registration... ", nl=False) try: from scitex._mcp import mcp as mcp_server + from scitex._mcp import mounted_namespaces if mcp_server: n_local = len(_get_all_tools(mcp_server)) - mounted = getattr(mcp_server, "_mounted_servers", []) or [] - n_mounted = len(mounted) - if n_local >= 20 and n_mounted >= 5: + n_ns = len(mounted_namespaces(mcp_server)) + if n_local >= 20 and n_ns >= 5: click.secho( - f"OK ({n_local} local tools, {n_mounted} peers mounted)", + f"OK ({n_local} local tools, {n_ns} namespaces)", fg="green", ) else: click.secho( - f"WARN ({n_local} local, {n_mounted} peers mounted)", + f"WARN ({n_local} local, {n_ns} namespaces)", fg="yellow", ) warnings.append( - f"{n_local} local tools / {n_mounted} peer mounts " - "(expected >=20 local and >=5 peers)" + f"{n_local} local tools / {n_ns} namespaces " + "(expected >=20 local and >=5 namespaces)" ) else: click.secho("SKIP", fg="yellow") diff --git a/tests/scitex/test_mcp_entrypoint.py b/tests/scitex/test_mcp_entrypoint.py index 5425556eb..14e1624bc 100755 --- a/tests/scitex/test_mcp_entrypoint.py +++ b/tests/scitex/test_mcp_entrypoint.py @@ -32,14 +32,11 @@ def _entrypoint(): return _mcp -def _mounted_prefixes(mcp) -> set: - """Namespaces of every sub-server mounted on the umbrella FastMCP.""" - prefixes = set() - for srv in getattr(mcp, "_mounted_servers", []) or []: - ns = getattr(srv, "prefix", None) or getattr(srv, "namespace", None) - if ns: - prefixes.add(ns) - return prefixes +def _mounted_prefixes(mcp_server) -> set: + """Namespaces of mounted peers — robust across FastMCP 2.x / 3.x.""" + from scitex._mcp import mounted_namespaces + + return mounted_namespaces(mcp_server) def test_server_name_is_scitex():