#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Timestamp: 2026-01-27 # File: src/scitex_writer/_cli/mcp.py """MCP CLI commands.""" import argparse import shutil from .. import __version__ CLAUDE_DESKTOP_CONFIG_CLI = """{ "mcpServers": { "scitex-writer": { "command": "/path/to/.venv/bin/scitex-writer", "args": ["mcp", "start"] } } }""" CLAUDE_DESKTOP_CONFIG_PYTHON = """{ "mcpServers": { "scitex-writer": { "command": "/path/to/.venv/bin/python", "args": ["-m", "scitex_writer", "mcp", "start"] } } }""" def cmd_start(args: argparse.Namespace) -> int: """Start the MCP server.""" from .._mcp import run_server run_server(transport=args.transport) return 0 def _get_tool_module(name: str) -> str: """Get logical module for a tool name.""" if name == "usage": return "general" if "bib" in name: return "bib" if "compile" in name: return "compile" if "figure" in name or "pdf_to_images" in name: return "figures" if "table" in name or "csv_to_latex" in name or "latex_to_csv" in name: return "tables" if ( "project" in name or "clone" in name or "get_pdf" in name or "document_types" in name ): return "project" if "guideline" in name: return "guidelines" if "prompts" in name: return "prompts" return "general" def _style(text: str, fg: str = None, bold: bool = False) -> str: """Apply ANSI color styling.""" import sys if not sys.stdout.isatty(): return text codes = { "green": "\033[32m", "cyan": "\033[36m", "yellow": "\033[33m", "magenta": "\033[35m", "white": "\033[37m", "bold": "\033[1m", "reset": "\033[0m", } prefix = "" if bold: prefix += codes["bold"] if fg and fg in codes: prefix += codes[fg] return f"{prefix}{text}{codes['reset']}" if prefix else text def _format_tool_signature(tool, compact: bool = False, indent: str = " ") -> str: """Format tool as Python-like function signature with colors.""" import inspect import re params = [] if hasattr(tool, "parameters") and tool.parameters: schema = tool.parameters props = schema.get("properties", {}) required = schema.get("required", []) for name, info in props.items(): ptype = info.get("type", "any") default = info.get("default") # Color: name in white bold, type in cyan, default in yellow name_s = _style(name, "white", bold=True) type_s = _style(ptype, "cyan") if name in required: params.append(f"{name_s}: {type_s}") elif default is not None: def_str = repr(default) if len(repr(default)) < 20 else "..." def_s = _style(f"= {def_str}", "yellow") params.append(f"{name_s}: {type_s} {def_s}") else: def_s = _style("= None", "yellow") params.append(f"{name_s}: {type_s} {def_s}") # Get return type with dict keys from docstring ret_type = "" if hasattr(tool, "fn") and tool.fn: try: sig = inspect.signature(tool.fn) if sig.return_annotation != inspect.Parameter.empty: ret = sig.return_annotation ret_name = ret.__name__ if hasattr(ret, "__name__") else str(ret) # Extract return dict keys from docstring keys = [] if tool.description and "Returns" in tool.description: match = re.search( r"Returns\s*[-]+\s*\w+\s*(.+?)(?:Raises|Examples|Notes|\Z)", tool.description, re.DOTALL, ) if match: keys = re.findall(r"'([a-z_]+)'", match.group(1)) keys_s = _style(f"{{{', '.join(keys)}}}", "yellow") if keys else "" ret_type = f" -> {_style(ret_name, 'magenta')}{keys_s}" except Exception: pass # Function name in green name_s = _style(tool.name, "green") if compact or len(params) <= 2: return f"{indent}{name_s}({', '.join(params)}){ret_type}" else: param_indent = indent + " " params_str = ",\n".join(f"{param_indent}{p}" for p in params) return f"{indent}{name_s}(\n{params_str}\n{indent}){ret_type}" def cmd_list_tools(args: argparse.Namespace) -> int: """List all available MCP tools (figrecipe-compatible format).""" from .._mcp import mcp verbose = getattr(args, "verbose", 0) compact = getattr(args, "compact", False) module_filter = getattr(args, "module", None) as_json = getattr(args, "json", False) tools = list(mcp._tool_manager._tools.keys()) total = len(tools) # Group by logical module modules = {} for tool_name in sorted(tools): module = _get_tool_module(tool_name) if module not in modules: modules[module] = [] modules[module].append(tool_name) # Filter by module if specified if module_filter: module_filter = module_filter.lower() if module_filter not in modules: print(f"ERROR: Unknown module '{module_filter}'") print(f"Available modules: {', '.join(sorted(modules.keys()))}") return 1 modules = {module_filter: modules[module_filter]} if as_json: import json output = { "name": "scitex-writer", "total": sum(len(t) for t in modules.values()), "modules": {}, } for mod, tool_list in modules.items(): output["modules"][mod] = { "count": len(tool_list), "tools": tool_list, } print(json.dumps(output, indent=2)) return 0 print(_style("SciTeX Writer MCP: scitex-writer", "cyan", bold=True)) print(f"Tools: {total} ({len(modules)} modules)\n") for module in sorted(modules.keys()): mod_tools = sorted(modules[module]) print(_style(f"{module}: {len(mod_tools)} tools", "green", bold=True)) for tool_name in mod_tools: tool_obj = mcp._tool_manager._tools.get(tool_name) if verbose == 0: # Names only print(f" {tool_name}") elif verbose == 1: # Signatures sig = ( _format_tool_signature(tool_obj, compact=compact) if tool_obj else f" {tool_name}" ) print(sig) elif verbose == 2: # Signature + one-line description sig = ( _format_tool_signature(tool_obj, compact=compact) if tool_obj else f" {tool_name}" ) print(sig) if tool_obj and tool_obj.description: desc = tool_obj.description.split("\n")[0].strip() print(f" {desc}") print() else: # Signature + full description sig = ( _format_tool_signature(tool_obj, compact=compact) if tool_obj else f" {tool_name}" ) print(sig) if tool_obj and tool_obj.description: for line in tool_obj.description.strip().split("\n"): print(f" {line}") print() print() return 0 def cmd_doctor(args: argparse.Namespace) -> int: """Check MCP server health and configuration.""" print(f"scitex-writer {__version__}\n") print("Health Check") print("=" * 40) checks = [] try: import fastmcp checks.append(("fastmcp", True, fastmcp.__version__)) except ImportError: checks.append(("fastmcp", False, "not installed")) try: from .._mcp import mcp tool_count = len(mcp._tool_manager._tools) checks.append(("MCP server", True, f"{tool_count} tools")) except Exception as e: checks.append(("MCP server", False, str(e))) scitex_path = shutil.which("scitex-writer") if scitex_path: checks.append(("CLI", True, scitex_path)) else: checks.append(("CLI", False, "not in PATH")) all_ok = True for name, ok, info in checks: status = "✓" if ok else "✗" if not ok: all_ok = False print(f" {status} {name}: {info}") print() if all_ok: print("All checks passed!") else: print("Some checks failed. Run 'pip install scitex-writer' to fix.") return 0 if all_ok else 1 def cmd_config(args: argparse.Namespace) -> int: """Show Claude Desktop configuration snippet.""" print(f"scitex-writer {__version__}\n") print("Add this to your Claude Desktop config file:\n") print(" macOS: ~/Library/Application Support/Claude/claude_desktop_config.json") print(" Linux: ~/.config/Claude/claude_desktop_config.json\n") scitex_path = shutil.which("scitex-writer") if scitex_path: print(f"Your installation path: {scitex_path}\n") print("Option 1: CLI command (replace path with your installation)") print(CLAUDE_DESKTOP_CONFIG_CLI) print("\nOption 2: Python module (replace path with your installation)") print(CLAUDE_DESKTOP_CONFIG_PYTHON) return 0 def register_parser(subparsers) -> argparse.ArgumentParser: """Register MCP subcommand parser.""" mcp_help = """MCP (Model Context Protocol) server commands. Quick start: scitex-writer mcp list-tools # List all tools scitex-writer mcp doctor # Check server health scitex-writer mcp installation # Show Claude Desktop installation guide scitex-writer mcp start # Start MCP server """ mcp_parser = subparsers.add_parser( "mcp", help="MCP server commands", description=mcp_help, formatter_class=argparse.RawDescriptionHelpFormatter, ) mcp_sub = mcp_parser.add_subparsers(dest="mcp_command", title="Commands") inst = mcp_sub.add_parser( "installation", help="Show Claude Desktop installation guide" ) inst.set_defaults(func=cmd_config) lst = mcp_sub.add_parser( "list-tools", help="List all available MCP tools", description="Verbosity: (none) names, -v signatures, -vv +description, -vvv full", ) lst.add_argument( "-v", "--verbose", action="count", default=0, help="Verbosity: -v sig, -vv +desc, -vvv full", ) lst.add_argument( "-c", "--compact", action="store_true", help="Compact signatures (single line)" ) lst.add_argument( "-m", "--module", type=str, default=None, help="Filter by module (bib, compile, figures, tables, project, guidelines, prompts)", ) lst.add_argument( "--json", action="store_true", default=False, help="Output as JSON" ) lst.set_defaults(func=cmd_list_tools) doc = mcp_sub.add_parser("doctor", help="Check MCP server health") doc.set_defaults(func=cmd_doctor) start = mcp_sub.add_parser("start", help="Start the MCP server") start.add_argument("-t", "--transport", choices=["stdio", "sse"], default="stdio") start.set_defaults(func=cmd_start) return mcp_parser # EOF