Skip to main content
Glama
emerzon

MetaTrader5 MCP Server

by emerzon
server.py7.44 kB
"""Main entry point for the MCP server.""" import logging import os import atexit from typing import Optional from mcp.server.fastmcp import FastMCP from functools import wraps as _wraps from .config import mt5_config from .constants import SERVICE_NAME, TIMEFRAME_MAP, TIMEFRAME_SECONDS # re-export for CLI/tests from .schema_attach import attach_schemas_to_tools from .schema import get_shared_enum_lists from ..utils.mt5 import mt5_connection, _auto_connect_wrapper, _ensure_symbol_ready def _apply_fastmcp_env_overrides() -> None: """Bridge legacy MCP_* env vars to FastMCP's FASTMCP_* settings.""" mapping = { "MCP_HOST": "FASTMCP_HOST", "MCP_PORT": "FASTMCP_PORT", "MCP_LOG_LEVEL": "FASTMCP_LOG_LEVEL", "MCP_MOUNT_PATH": "FASTMCP_MOUNT_PATH", "MCP_SSE_PATH": "FASTMCP_SSE_PATH", "MCP_MESSAGE_PATH": "FASTMCP_MESSAGE_PATH", } for legacy, target in mapping.items(): val = os.getenv(legacy) if val and not os.getenv(target): os.environ[target] = val # Default to listen on all interfaces if no host is specified if not os.getenv("FASTMCP_HOST"): os.environ["FASTMCP_HOST"] = "0.0.0.0" # Create MCP instance early to avoid circular imports when tools import `mcp`. _apply_fastmcp_env_overrides() mcp = FastMCP(SERVICE_NAME) # Lightweight helpers used by tool modules from ..utils.utils import _coerce_scalar, _normalize_ohlcv_arg # Import all tools so they are registered with the MCP server # Augment FastMCP with a discoverable registry for the CLI try: _ORIG_TOOL_DECORATOR = mcp.tool # type: ignore[attr-defined] except Exception: _ORIG_TOOL_DECORATOR = None _TOOL_REGISTRY = {} def _recording_tool_decorator(*dargs, **dkwargs): # type: ignore[override] if _ORIG_TOOL_DECORATOR is None: # Fallback: no-op decorator def _noop(func): _TOOL_REGISTRY[getattr(func, '__name__', 'tool')] = func return func return _noop kwargs = dict(dkwargs) # Default to unstructured output so tools emit raw text content unless overridden structured_in_args = len(dargs) >= 5 if not structured_in_args and 'structured_output' not in kwargs: kwargs['structured_output'] = False dec = _ORIG_TOOL_DECORATOR(*dargs, **kwargs) def _sanitize_annotations(func): """Ensure annotations are classes to satisfy FastMCP's issubclass checks.""" import inspect cleaned = {} ann = getattr(func, "__annotations__", {}) or {} for name, param in inspect.signature(func).parameters.items(): value = ann.get(name, param.annotation) cleaned[name] = value if isinstance(value, type) else object if "return" in ann: cleaned["return"] = ann["return"] if isinstance(ann["return"], type) else object return cleaned def _wrap(func): # Wrap the callable to ensure plain-text, minimal output for API calls try: from ..utils.minimal_output import format_result_minimal as _fmt_min, to_methods_availability_csv as _fmt_methods except Exception: _fmt_min = lambda x: str(x) if x is not None else "" # fallback _fmt_methods = None # type: ignore @_wraps(func) def _wrapped(*a, **kw): # Check for raw output flag (used by CLI to bypass formatting) raw_output = kw.pop('__cli_raw', False) # Uniform input normalization for common structured args try: if 'denoise' in kw: from ..utils.denoise import normalize_denoise_spec as _norm_dn # type: ignore kw['denoise'] = _norm_dn(kw.get('denoise')) except Exception: pass out = func(*a, **kw) if raw_output: return out try: # Special-case: compact method availability where applicable fname = getattr(func, '__name__', '') if fname in ('forecast_list_methods', 'denoise_list_methods') and isinstance(out, dict): methods = out.get('methods') or [] if _fmt_methods: s = _fmt_methods(methods) if s: return s return _fmt_min(out) except Exception: return str(out) if out is not None else "" try: import inspect cleaned = _sanitize_annotations(func) _wrapped.__annotations__ = cleaned # Override __signature__ so FastMCP sees sanitized annotations params = [] for name, param in inspect.signature(func).parameters.items(): params.append( param.replace(annotation=cleaned.get(name)) ) return_ann = cleaned.get("return", inspect._empty) _wrapped.__signature__ = inspect.Signature(parameters=params, return_annotation=return_ann) except Exception: pass res = dec(_wrapped) name = getattr(func, '__name__', None) if name: # Store the original callable for CLI discovery _TOOL_REGISTRY[str(name)] = _wrapped return res return _wrap # Install wrapper and expose common registry fields for discovery try: setattr(mcp, 'tool', _recording_tool_decorator) setattr(mcp, 'tools', _TOOL_REGISTRY) setattr(mcp, 'registry', _TOOL_REGISTRY) setattr(mcp, '_tools', _TOOL_REGISTRY) setattr(mcp, '_tool_registry', _TOOL_REGISTRY) except Exception: pass from .data import * from ..utils.denoise import denoise_list_methods from .forecast import * from .causal import * from .indicators import * from .market_depth import * from .patterns import * from .pivot import * from .symbols import * from .regime import * from .labels import * from .report import * from .trading import * try: attach_schemas_to_tools(mcp, get_shared_enum_lists()) except Exception: pass @atexit.register def _disconnect_mt5(): mt5_connection.disconnect() def _resolve_transport(default: str = "sse") -> tuple[str, Optional[str]]: """Return the transport to use and optional mount path for SSE.""" transport = (os.getenv("MCP_TRANSPORT") or default).strip().lower() if transport not in ("stdio", "sse", "streamable-http"): transport = default mount_path = os.getenv("MCP_MOUNT_PATH") return transport, mount_path def main(): """Main entry point for the MCP server""" log_level = getattr(logging, str(mcp.settings.log_level).upper(), logging.INFO) logging.basicConfig(level=log_level) logger = logging.getLogger(__name__) transport, mount_path = _resolve_transport() logger.info(f"Starting {SERVICE_NAME} server... transport={transport}") if transport == "sse": base_path = mcp.settings.mount_path.rstrip("/") or "/" logger.info( "SSE listening at http://%s:%s%s (event path %s, message path %s)", mcp.settings.host, mcp.settings.port, base_path, mcp.settings.sse_path, mcp.settings.message_path, ) mcp.run(transport=transport, mount_path=mount_path if transport == "sse" else None) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/emerzon/mt-data-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server