aift_mcp
Optional AIFT Model Context Protocol server entry point.
Usage::
python aift_mcp.py --transport stdio
1"""Optional AIFT Model Context Protocol server entry point. 2 3Usage:: 4 5 python aift_mcp.py --transport stdio 6""" 7 8from __future__ import annotations 9 10import argparse 11import ipaddress 12import logging 13import sys 14from collections.abc import Sequence 15 16from runtime_compat import UnsupportedPythonVersionError, assert_supported_python_version 17 18_MCP_INSTALL_MESSAGE = ( 19 "AIFT MCP support requires the 'mcp' package. " 20 "Install it with: pip install -r requirements.txt" 21) 22_DEFAULT_HTTP_HOST = "127.0.0.1" 23_DEFAULT_HTTP_PORT = 8765 24_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") 25 26 27class MCPStartupError(RuntimeError): 28 """Raised for startup failures that should be reported cleanly.""" 29 30 31class _StderrArgumentParser(argparse.ArgumentParser): 32 """ArgumentParser that never writes help or errors to stdout.""" 33 34 def _print_message(self, message: str, file: object | None = None) -> None: 35 if message: 36 super()._print_message(message, sys.stderr) 37 38 39def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace: 40 """Parse command-line arguments for the MCP entry point.""" 41 parser = _StderrArgumentParser( 42 description="Run the optional AIFT MCP server.", 43 ) 44 parser.add_argument( 45 "--transport", 46 choices=("stdio", "streamable-http"), 47 default="stdio", 48 help=( 49 "MCP transport to use. Default: stdio. Streamable HTTP is for " 50 "local integrations only and is unsupported for remote exposure " 51 "unless --allow-remote is deliberately set." 52 ), 53 ) 54 parser.add_argument( 55 "--host", 56 default=_DEFAULT_HTTP_HOST, 57 help=( 58 "Host for --transport streamable-http. Default: 127.0.0.1. " 59 "Non-loopback hosts require --allow-remote and are unsupported " 60 "by default." 61 ), 62 ) 63 parser.add_argument( 64 "--port", 65 type=_port_value, 66 default=_DEFAULT_HTTP_PORT, 67 help=f"Port for --transport streamable-http. Default: {_DEFAULT_HTTP_PORT}.", 68 ) 69 parser.add_argument( 70 "--allow-remote", 71 action="store_true", 72 help=( 73 "Allow Streamable HTTP to bind a non-loopback host. Remote exposure " 74 "is unsupported by default." 75 ), 76 ) 77 parser.add_argument( 78 "--log-level", 79 choices=_LOG_LEVELS, 80 default="WARNING", 81 help="Python logging level. Logs always go to stderr. Default: WARNING.", 82 ) 83 args = parser.parse_args(argv) 84 args.host = args.host.strip() 85 if args.transport == "streamable-http" and not args.host: 86 parser.error("--host must not be empty for --transport streamable-http.") 87 if ( 88 args.transport == "streamable-http" 89 and not args.allow_remote 90 and not _is_loopback_host(args.host) 91 ): 92 parser.error( 93 "--transport streamable-http binds to loopback hosts only by default; " 94 "use --allow-remote to deliberately bind a non-loopback host." 95 ) 96 return args 97 98 99def _port_value(value: str) -> int: 100 """Return a valid TCP port value for argparse.""" 101 try: 102 port = int(value) 103 except ValueError as exc: 104 raise argparse.ArgumentTypeError("port must be an integer") from exc 105 if not 1 <= port <= 65535: 106 raise argparse.ArgumentTypeError("port must be between 1 and 65535") 107 return port 108 109 110def _is_loopback_host(host: str) -> bool: 111 """Return whether a host string identifies a loopback bind address.""" 112 normalized = host.strip().lower() 113 if normalized in {"localhost", "localhost."}: 114 return True 115 try: 116 return ipaddress.ip_address(normalized).is_loopback 117 except ValueError: 118 return False 119 120 121def _configure_logging(level_name: str) -> None: 122 """Configure Python logging so entry-point diagnostics never use stdout.""" 123 logging.basicConfig( 124 level=getattr(logging, level_name), 125 format="%(levelname)s:%(name)s:%(message)s", 126 stream=sys.stderr, 127 force=True, 128 ) 129 130 131def _is_missing_mcp_import(error: ImportError) -> bool: 132 """Return whether an ImportError appears to be the optional MCP SDK.""" 133 missing_name = getattr(error, "name", None) 134 return bool(missing_name == "mcp" or str(missing_name).startswith("mcp.")) 135 136 137def _build_and_run_server( 138 transport: str, 139 *, 140 host: str = _DEFAULT_HTTP_HOST, 141 port: int = _DEFAULT_HTTP_PORT, 142) -> None: 143 """Build and run the MCP server using the requested transport.""" 144 if transport not in {"stdio", "streamable-http"}: 145 raise MCPStartupError(f"Unsupported MCP transport: {transport}") 146 147 try: 148 from app.automation.mcp_server import MissingMCPDependencyError, build_mcp_server 149 except ImportError as exc: 150 if _is_missing_mcp_import(exc): 151 raise MCPStartupError(_MCP_INSTALL_MESSAGE) from exc 152 raise MCPStartupError(f"Failed to import AIFT MCP server: {exc}") from exc 153 154 try: 155 build_kwargs = {} 156 if transport == "streamable-http": 157 build_kwargs = {"transport_host": host, "transport_port": port} 158 server = build_mcp_server(**build_kwargs) 159 except MissingMCPDependencyError as exc: 160 raise MCPStartupError(str(exc)) from exc 161 162 if transport == "stdio": 163 server.run(transport="stdio") 164 else: 165 server.run(transport="streamable-http") 166 167 168def main(argv: Sequence[str] | None = None) -> int: 169 """Validate the runtime, then start the optional MCP server.""" 170 try: 171 assert_supported_python_version() 172 args = _parse_args(argv) 173 _configure_logging(args.log_level) 174 _build_and_run_server(args.transport, host=args.host, port=args.port) 175 except UnsupportedPythonVersionError as exc: 176 print(str(exc), file=sys.stderr) 177 return 1 178 except MCPStartupError as exc: 179 print(str(exc), file=sys.stderr) 180 return 1 181 except SystemExit as exc: 182 return int(exc.code or 0) 183 except Exception as exc: 184 print(f"AIFT MCP startup failed: {exc}", file=sys.stderr) 185 return 1 186 return 0 187 188 189if __name__ == "__main__": 190 raise SystemExit(main())
class
MCPStartupError(builtins.RuntimeError):
28class MCPStartupError(RuntimeError): 29 """Raised for startup failures that should be reported cleanly."""
Raised for startup failures that should be reported cleanly.
def
main(argv: Sequence[str] | None = None) -> int:
169def main(argv: Sequence[str] | None = None) -> int: 170 """Validate the runtime, then start the optional MCP server.""" 171 try: 172 assert_supported_python_version() 173 args = _parse_args(argv) 174 _configure_logging(args.log_level) 175 _build_and_run_server(args.transport, host=args.host, port=args.port) 176 except UnsupportedPythonVersionError as exc: 177 print(str(exc), file=sys.stderr) 178 return 1 179 except MCPStartupError as exc: 180 print(str(exc), file=sys.stderr) 181 return 1 182 except SystemExit as exc: 183 return int(exc.code or 0) 184 except Exception as exc: 185 print(f"AIFT MCP startup failed: {exc}", file=sys.stderr) 186 return 1 187 return 0
Validate the runtime, then start the optional MCP server.