Files
Pratik Bhadane 71b6343e92 feat: refresh benchmark coverage and harden CI tooling
Refresh the benchmark and performance surface across the repo. This updates the benchmark wrappers and helper scripts, regenerates the checked-in benchmark and perf-contract artifacts, and folds in the related roadmap, compatibility, and example notebook changes that belong with this performance-focused pass.

Harden the Python CI and local pre-push flow so the same checks pass reliably in both places. The workflow and pre-push script now use module-safe uv typecheck invocations, the Python test environment installs the optional MCP dependency needed by the MCP server tests, and one-off root benchmark outputs are ignored to keep the repo clean.

Align local tooling with the current project configuration by updating the Ruff pre-commit hook, tightening the API typing and MCP server helpers, and refreshing the lockfile to pick up the audited PyJWT fix while preserving the rest of the staged source changes.
2026-03-24 14:52:20 +05:30

1349 lines
42 KiB
Python

"""Optional MCP server exposing the public ferro-ta API."""
from __future__ import annotations
import dataclasses
import enum
import importlib
import inspect
import json
import re
from collections.abc import Callable, Mapping
from datetime import date, datetime, time
from functools import lru_cache
from itertools import count
from typing import Any, cast, get_args, get_origin, get_type_hints
import numpy as np
import ferro_ta
from ferro_ta.tools import compute_indicator, run_backtest
from ferro_ta.tools.api_info import methods as api_methods
__all__ = ["create_server", "run_server", "handle_list_tools", "handle_call_tool"]
_MCP_INSTALL_HINT = (
"The ferro-ta MCP server requires the optional 'mcp' dependency. "
'Install it with `pip install "ferro-ta[mcp]"` or `uv sync --extra mcp`.'
)
_REFERENCE_HELP = (
"Use {'instance_id': '<id>'} for stored objects or "
"{'callable': '<public ferro-ta name>'} for public callables."
)
_JSON_ANY_TYPE: list[str] = [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string",
]
_NO_DEFAULT = object()
_INSTANCE_STORE: dict[str, Any] = {}
_INSTANCE_META: dict[str, dict[str, Any]] = {}
_INSTANCE_COUNTER = count(1)
@dataclasses.dataclass(frozen=True)
class _ToolSpec:
"""Resolved metadata and dispatcher for a single MCP tool."""
name: str
description: str
input_schema: dict[str, Any]
wrapper_signature: inspect.Signature
invoke: Callable[[dict[str, Any]], Any]
def _import_object(module_name: str, name: str) -> Any:
"""Import *name* from *module_name*."""
module = importlib.import_module(module_name)
return getattr(module, name)
@lru_cache(maxsize=1)
def _discover_public_callables() -> tuple[list[dict[str, str]], dict[str, Any]]:
"""Return canonical public callable metadata and lookup targets."""
entries = api_methods()
top_level = sorted(
[item for item in entries if item["category"] == "top_level"],
key=lambda item: item["name"],
)
top_level_names = {item["name"] for item in top_level}
extras = sorted(
[
item
for item in entries
if item["category"] != "top_level" and item["name"] not in top_level_names
],
key=lambda item: item["name"],
)
public_entries: list[dict[str, str]] = []
targets: dict[str, Any] = {}
for item in [*top_level, *extras]:
name = item["name"]
if name in targets:
continue
targets[name] = _import_object(item["module"], name)
public_entries.append(item)
return public_entries, targets
def _slugify(value: str) -> str:
"""Convert *value* into a short identifier fragment."""
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
return slug or "object"
def _instance_ref(value: Any, *, source_tool: str) -> dict[str, Any]:
"""Store *value* and return a serialisable reference payload."""
identifier = f"{_slugify(type(value).__name__)}-{next(_INSTANCE_COUNTER):04d}"
_INSTANCE_STORE[identifier] = value
payload = {
"instance_id": identifier,
"type": f"{type(value).__module__}.{type(value).__name__}",
"repr": repr(value),
"callable": callable(value),
"source_tool": source_tool,
}
snapshot = _object_snapshot(value)
if snapshot is not None:
payload["snapshot"] = snapshot
_INSTANCE_META[identifier] = payload
return payload
def _get_instance(identifier: str) -> Any:
"""Return the stored instance for *identifier* or raise a clear error."""
try:
return _INSTANCE_STORE[identifier]
except KeyError as exc:
raise KeyError(f"Unknown instance_id: {identifier!r}") from exc
def _is_instance_ref(value: Any) -> bool:
"""Return whether *value* is a stored-object reference payload."""
return isinstance(value, dict) and set(value) == {"instance_id"}
def _is_callable_ref(value: Any) -> bool:
"""Return whether *value* is a public-callable reference payload."""
return isinstance(value, dict) and set(value) == {"callable"}
def _public_method_summaries(value: Any) -> list[dict[str, str]]:
"""Return public callable methods for *value*."""
result: list[dict[str, str]] = []
for method_name, member in inspect.getmembers(value):
if method_name.startswith("_") or not callable(member):
continue
try:
signature = str(inspect.signature(member))
except (TypeError, ValueError):
signature = "()"
result.append({"name": method_name, "signature": signature})
return result
def _object_snapshot(value: Any) -> Any:
"""Return a serialisable snapshot for common non-primitive objects."""
if isinstance(value, enum.Enum):
return value.value
if dataclasses.is_dataclass(value) and not isinstance(value, type):
return _normalise_json(dataclasses.asdict(value), store_objects=False)
dynamic_value = cast(Any, value)
if hasattr(dynamic_value, "to_dict") and callable(dynamic_value.to_dict):
try:
return _normalise_json(dynamic_value.to_dict(), store_objects=False)
except TypeError:
if dynamic_value.__class__.__module__.startswith("pandas"):
return _normalise_json(
dynamic_value.to_dict(orient="list"), store_objects=False
)
if dynamic_value.__class__.__module__.startswith("polars"):
return _normalise_json(
dynamic_value.to_dict(as_series=False), store_objects=False
)
except Exception:
return None
if hasattr(dynamic_value, "__dict__"):
fields = {
key: val
for key, val in vars(dynamic_value).items()
if not key.startswith("_") and not callable(val)
}
if fields:
return _normalise_json(fields, store_objects=False)
slots = getattr(type(dynamic_value), "__slots__", ())
if slots:
fields = {}
for slot in slots:
if slot.startswith("_") or not hasattr(dynamic_value, slot):
continue
slot_value = getattr(dynamic_value, slot)
if callable(slot_value):
continue
fields[slot] = slot_value
if fields:
return _normalise_json(fields, store_objects=False)
return None
def _normalise_json(value: Any, *, store_objects: bool = True) -> Any:
"""Convert Python and numpy-rich values into JSON-safe values."""
if value is None or isinstance(value, (str, bool)):
return value
if isinstance(value, (int, np.integer)):
return int(value)
if isinstance(value, (float, np.floating)):
return None if np.isnan(value) else float(value)
if isinstance(value, (date, datetime, time)):
return value.isoformat()
if isinstance(value, enum.Enum):
return _normalise_json(value.value, store_objects=store_objects)
if isinstance(value, np.ndarray):
return [
_normalise_json(item, store_objects=store_objects)
for item in value.tolist()
]
if isinstance(value, np.generic):
return _normalise_json(value.item(), store_objects=store_objects)
if isinstance(value, Mapping):
return {
str(key): _normalise_json(item, store_objects=store_objects)
for key, item in value.items()
}
if isinstance(value, (list, tuple, set, frozenset)):
return [_normalise_json(item, store_objects=store_objects) for item in value]
if hasattr(value, "tolist") and callable(value.tolist):
try:
return _normalise_json(value.tolist(), store_objects=store_objects)
except Exception:
pass
snapshot = _object_snapshot(value)
if snapshot is not None:
return snapshot
if store_objects:
return _instance_ref(value, source_tool="return_value")
return repr(value)
def _json_result(
payload: Any,
*,
structured_key: str | None = None,
) -> dict[str, Any]:
"""Wrap *payload* in the helper response shape."""
structured: dict[str, Any] | None = None
if isinstance(payload, dict):
structured = payload
elif structured_key is not None:
structured = {structured_key: payload}
result: dict[str, Any] = {
"content": [{"type": "text", "text": json.dumps(payload)}],
}
if structured is not None:
result["structuredContent"] = structured
return result
def _text_result(
text: str,
*,
structured: dict[str, Any] | None = None,
is_error: bool = False,
) -> dict[str, Any]:
"""Wrap *text* in the helper response shape."""
result: dict[str, Any] = {
"content": [{"type": "text", "text": text}],
}
if structured is not None:
result["structuredContent"] = structured
if is_error:
result["isError"] = True
return result
def _response_from_payload(payload: Any) -> dict[str, Any]:
"""Create a helper response from a normalised payload."""
if isinstance(payload, str):
return _text_result(payload, structured={"value": payload})
return _json_result(payload)
def _load_mcp_sdk() -> tuple[type[Any], type[Any], type[Any]]:
"""Import the optional MCP SDK lazily."""
try:
fastmcp = importlib.import_module("mcp.server.fastmcp")
types = importlib.import_module("mcp.types")
except ImportError as exc: # pragma: no cover - exercised via tests
raise RuntimeError(_MCP_INSTALL_HINT) from exc
return fastmcp.FastMCP, types.CallToolResult, types.TextContent
def _to_call_tool_result(result: dict[str, Any]) -> Any:
"""Convert helper-style results into an MCP SDK CallToolResult."""
_, call_tool_result_type, text_content_type = _load_mcp_sdk()
content = [
text_content_type(type="text", text=item["text"])
for item in result.get("content", [])
if item.get("type") == "text"
]
return call_tool_result_type(
content=content,
structuredContent=result.get("structuredContent"),
isError=result.get("isError", False),
)
def _safe_default(value: Any) -> Any:
"""Return a JSON-safe schema default or a sentinel when unavailable."""
if value is inspect._empty:
return _NO_DEFAULT
if isinstance(value, enum.Enum):
return value.value
if isinstance(value, (str, bool, int, float)) or value is None:
return value
if isinstance(value, tuple) and all(
isinstance(item, (str, bool, int, float)) or item is None for item in value
):
return list(value)
if isinstance(value, list) and all(
isinstance(item, (str, bool, int, float)) or item is None for item in value
):
return value
return _NO_DEFAULT
def _wrapper_default(value: Any) -> Any:
"""Return a lightweight default for generated wrapper signatures."""
default = _safe_default(value)
if default is _NO_DEFAULT:
return None
return default
def _annotation_label(annotation: Any) -> str:
"""Return a readable label for *annotation*."""
if annotation is inspect._empty:
return "Any"
if isinstance(annotation, str):
return annotation
origin = get_origin(annotation)
if origin is not None:
return str(annotation).replace("typing.", "")
return getattr(annotation, "__name__", repr(annotation))
def _annotation_options(annotation: Any) -> list[Any]:
"""Flatten simple union annotations into a list of options."""
if annotation is inspect._empty:
return [Any]
origin = get_origin(annotation)
if origin in (None,):
return [annotation]
if origin in (list, tuple, dict):
return [annotation]
if origin in (Callable,):
return [annotation]
args = [arg for arg in get_args(annotation) if arg is not type(None)]
return args or [annotation]
def _is_enum_annotation(annotation: Any) -> type[enum.Enum] | None:
"""Return the enum class inside *annotation*, if any."""
for option in _annotation_options(annotation):
if inspect.isclass(option) and issubclass(option, enum.Enum):
return option
return None
def _annotation_includes_callable(annotation: Any) -> bool:
"""Return whether *annotation* includes a callable type."""
label = _annotation_label(annotation)
if "Callable" in label:
return True
for option in _annotation_options(annotation):
origin = get_origin(option)
if origin in (Callable,):
return True
return False
def _annotation_includes_custom_class(annotation: Any) -> bool:
"""Return whether *annotation* includes a non-builtin class."""
for option in _annotation_options(annotation):
if not inspect.isclass(option):
continue
if issubclass(option, enum.Enum):
return True
if option.__module__ == "builtins":
continue
if option in (date, datetime, time):
continue
return True
return False
def _schema_and_py_type(
annotation: Any, *, param_name: str
) -> tuple[dict[str, Any], Any]:
"""Map Python annotations to JSON Schema and wrapper annotations."""
enum_type = _is_enum_annotation(annotation)
if enum_type is not None:
raw_values = [member.value for member in enum_type]
if all(isinstance(item, str) for item in raw_values):
schema = {"type": "string", "enum": list(raw_values)}
return schema, str
if all(isinstance(item, int) for item in raw_values):
schema = {"type": "integer", "enum": list(raw_values)}
return schema, int
label = _annotation_label(annotation)
lower = label.lower()
if "bool" in lower:
return {"type": "boolean"}, bool
if "int" in lower and "point" not in lower:
return {"type": "integer"}, int
if (
"float" in lower
or "number" in lower
or "scalar" in lower
or "ndarray" in lower
or "arraylike" in lower
):
if "scalarorarray" in lower:
return {
"type": _JSON_ANY_TYPE,
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
}, Any
if "ndarray" in lower or "arraylike" in lower:
return {"type": "array", "items": {}}, list[Any]
return {"type": "number"}, float
if (
"list" in lower
or "tuple" in lower
or "sequence" in lower
or "iterable" in lower
):
return {"type": "array", "items": {}}, list[Any]
if "dict" in lower or "mapping" in lower:
return {"type": "object"}, dict[str, Any]
if "str" in lower or "date" in lower or "datetime" in lower or "time" in lower:
return {"type": "string"}, str
if _annotation_includes_callable(annotation) or _annotation_includes_custom_class(
annotation
):
return {
"type": _JSON_ANY_TYPE,
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
}, Any
return {"type": _JSON_ANY_TYPE}, Any
def _build_signature_and_schema(
signature: inspect.Signature,
*,
type_hints: dict[str, Any],
) -> tuple[inspect.Signature, dict[str, Any]]:
"""Build a wrapper signature and JSON schema for *signature*."""
wrapper_parameters: list[inspect.Parameter] = []
properties: dict[str, Any] = {}
required: list[str] = []
for parameter in signature.parameters.values():
if parameter.name in {"self", "cls"}:
continue
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
properties["args"] = {
"type": "array",
"items": {},
"description": "Extra positional arguments.",
}
wrapper_parameters.append(
inspect.Parameter(
"args",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=list[Any],
default=None,
)
)
continue
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
properties["kwargs"] = {
"type": "object",
"description": "Extra keyword arguments.",
}
wrapper_parameters.append(
inspect.Parameter(
"kwargs",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=dict[str, Any],
default=None,
)
)
continue
annotation = type_hints.get(parameter.name, parameter.annotation)
schema, py_type = _schema_and_py_type(annotation, param_name=parameter.name)
default = _safe_default(parameter.default)
if default is not _NO_DEFAULT:
schema["default"] = default
else:
required.append(parameter.name)
properties[parameter.name] = schema
wrapper_parameters.append(
inspect.Parameter(
parameter.name,
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=py_type,
default=(
inspect._empty
if parameter.default is inspect._empty
else _wrapper_default(parameter.default)
),
)
)
wrapper_signature = inspect.Signature(parameters=wrapper_parameters)
input_schema: dict[str, Any] = {
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": False,
}
return wrapper_signature, input_schema
def _type_hints_for_target(target: Any) -> dict[str, Any]:
"""Return best-effort type hints for *target*."""
hinted = target.__init__ if inspect.isclass(target) else target
try:
return get_type_hints(hinted, include_extras=True)
except Exception:
return {}
def _resolve_public_callable(name: str) -> Any:
"""Resolve a public ferro-ta callable by its exposed MCP name."""
_, targets = _discover_public_callables()
if name in targets:
return targets[name]
aliases = {
"sma": ferro_ta.SMA,
"ema": ferro_ta.EMA,
"rsi": ferro_ta.RSI,
"macd": ferro_ta.MACD,
"backtest": run_backtest,
}
try:
return aliases[name]
except KeyError as exc:
raise KeyError(f"Unknown callable reference: {name!r}") from exc
def _coerce_enum(value: Any, enum_type: type[enum.Enum]) -> enum.Enum:
"""Convert *value* into *enum_type*."""
if isinstance(value, enum_type):
return value
if isinstance(value, str):
if value in enum_type.__members__:
return enum_type[value]
for member in enum_type:
if member.value == value:
return member
return enum_type(value)
def _decode_value(value: Any, annotation: Any = Any) -> Any:
"""Resolve stored-object and callable references inside *value*."""
if _is_instance_ref(value):
return _get_instance(str(value["instance_id"]))
if _is_callable_ref(value):
return _resolve_public_callable(str(value["callable"]))
enum_type = _is_enum_annotation(annotation)
if enum_type is not None:
try:
return _coerce_enum(value, enum_type)
except Exception:
pass
if _annotation_includes_callable(annotation) and isinstance(value, str):
try:
return _resolve_public_callable(value)
except KeyError:
pass
if isinstance(value, list):
return [_decode_value(item, Any) for item in value]
if isinstance(value, tuple):
return tuple(_decode_value(item, Any) for item in value)
if isinstance(value, dict):
return {str(key): _decode_value(item, Any) for key, item in value.items()}
return value
def _invoke_target(
target: Any,
*,
signature: inspect.Signature,
type_hints: dict[str, Any],
arguments: dict[str, Any],
) -> Any:
"""Call *target* with decoded arguments."""
positional_args: list[Any] = []
keyword_args: dict[str, Any] = {}
has_varargs = any(
parameter.kind == inspect.Parameter.VAR_POSITIONAL
for parameter in signature.parameters.values()
)
before_varargs = True
for parameter in signature.parameters.values():
if parameter.name in {"self", "cls"}:
continue
annotation = type_hints.get(parameter.name, parameter.annotation)
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
before_varargs = False
extra_args = arguments.get("args", []) or []
positional_args.extend(_decode_value(item, Any) for item in extra_args)
continue
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
extra_kwargs = arguments.get("kwargs", {}) or {}
if not isinstance(extra_kwargs, dict):
raise TypeError("kwargs must be a JSON object")
keyword_args.update(
{
str(key): _decode_value(item, Any)
for key, item in extra_kwargs.items()
}
)
continue
expects_positional = (
before_varargs
and has_varargs
and parameter.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
)
if parameter.name in arguments:
decoded = _decode_value(arguments[parameter.name], annotation)
elif parameter.default is not inspect._empty:
if expects_positional:
decoded = parameter.default
else:
continue
else:
raise KeyError(f"Missing required argument: {parameter.name}")
if expects_positional or parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
positional_args.append(decoded)
else:
keyword_args[parameter.name] = decoded
return target(*positional_args, **keyword_args)
def _describe_instance_payload(identifier: str) -> dict[str, Any]:
"""Return a detailed description for a stored instance."""
value = _get_instance(identifier)
payload = dict(_INSTANCE_META.get(identifier, {}))
payload.update(
{
"instance_id": identifier,
"module": type(value).__module__,
"class_name": type(value).__name__,
"methods": _public_method_summaries(value),
}
)
return payload
def _list_instances_payload() -> list[dict[str, Any]]:
"""Return current stored-object metadata."""
return [
_describe_instance_payload(identifier) for identifier in sorted(_INSTANCE_STORE)
]
def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
"""Create a generated tool spec for a public ferro-ta callable."""
is_enum = inspect.isclass(target) and issubclass(target, enum.Enum)
type_hints = _type_hints_for_target(target)
if is_enum:
signature = inspect.Signature(
[
inspect.Parameter(
"value",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default=inspect._empty,
)
]
)
wrapper_signature, input_schema = _build_signature_and_schema(
signature,
type_hints={"value": str},
)
description = (
item["doc"]
or f"Construct a {item['name']} enum member. Returns a stored instance reference."
)
def invoke_enum(
arguments: dict[str, Any], *, enum_type: type[enum.Enum] = target
) -> Any:
if "value" not in arguments:
raise KeyError("Missing required argument: value")
member = _coerce_enum(arguments["value"], enum_type)
return _instance_ref(member, source_tool=item["name"])
return _ToolSpec(
name=item["name"],
description=description,
input_schema=input_schema,
wrapper_signature=wrapper_signature,
invoke=invoke_enum,
)
signature = inspect.signature(target)
wrapper_signature, input_schema = _build_signature_and_schema(
signature,
type_hints=type_hints,
)
is_class = inspect.isclass(target)
description = item["doc"] or f"Call {item['name']}."
if is_class:
description = (
item["doc"]
or f"Construct a {item['name']} instance. Returns a stored instance reference."
)
def invoke_target(
arguments: dict[str, Any],
*,
raw_target: Any = target,
raw_signature: inspect.Signature = signature,
raw_type_hints: dict[str, Any] = type_hints,
returns_instance: bool = is_class,
source_tool: str = item["name"],
) -> Any:
result = _invoke_target(
raw_target,
signature=raw_signature,
type_hints=raw_type_hints,
arguments=arguments,
)
if returns_instance:
return _instance_ref(result, source_tool=source_tool)
return _normalise_json(result)
return _ToolSpec(
name=item["name"],
description=description,
input_schema=input_schema,
wrapper_signature=wrapper_signature,
invoke=invoke_target,
)
def _legacy_series_tool(
name: str,
*,
indicator_name: str,
description: str,
) -> _ToolSpec:
"""Return a legacy lowercase indicator alias tool."""
wrapper_signature = inspect.Signature(
[
inspect.Parameter(
"close",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=list[float],
default=inspect._empty,
),
inspect.Parameter(
"timeperiod",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=14,
),
]
)
input_schema = {
"type": "object",
"properties": {
"close": {
"type": "array",
"items": {"type": "number"},
"description": "Close price series.",
},
"timeperiod": {
"type": "integer",
"description": "Look-back period.",
"default": 14,
},
},
"required": ["close"],
"additionalProperties": False,
}
def invoke(arguments: dict[str, Any]) -> Any:
close = np.asarray(arguments["close"], dtype=np.float64)
timeperiod = int(arguments.get("timeperiod", 14))
return _normalise_json(
compute_indicator(indicator_name, close, timeperiod=timeperiod)
)
return _ToolSpec(
name=name,
description=description,
input_schema=input_schema,
wrapper_signature=wrapper_signature,
invoke=invoke,
)
def _legacy_macd_tool() -> _ToolSpec:
"""Return the legacy lowercase MACD alias tool."""
wrapper_signature = inspect.Signature(
[
inspect.Parameter(
"close",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=list[float],
default=inspect._empty,
),
inspect.Parameter(
"fastperiod",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=12,
),
inspect.Parameter(
"slowperiod",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=26,
),
inspect.Parameter(
"signalperiod",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=9,
),
]
)
input_schema = {
"type": "object",
"properties": {
"close": {
"type": "array",
"items": {"type": "number"},
"description": "Close price series.",
},
"fastperiod": {
"type": "integer",
"description": "Fast EMA period.",
"default": 12,
},
"slowperiod": {
"type": "integer",
"description": "Slow EMA period.",
"default": 26,
},
"signalperiod": {
"type": "integer",
"description": "Signal EMA period.",
"default": 9,
},
},
"required": ["close"],
"additionalProperties": False,
}
def invoke(arguments: dict[str, Any]) -> Any:
close = np.asarray(arguments["close"], dtype=np.float64)
result = compute_indicator(
"MACD",
close,
fastperiod=int(arguments.get("fastperiod", 12)),
slowperiod=int(arguments.get("slowperiod", 26)),
signalperiod=int(arguments.get("signalperiod", 9)),
)
return _normalise_json(result)
return _ToolSpec(
name="macd",
description=(
"Compute MACD (Moving Average Convergence/Divergence). "
"Returns the line, signal, and histogram."
),
input_schema=input_schema,
wrapper_signature=wrapper_signature,
invoke=invoke,
)
def _legacy_backtest_tool() -> _ToolSpec:
"""Return the legacy lowercase backtest alias tool."""
wrapper_signature = inspect.Signature(
[
inspect.Parameter(
"close",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=list[float],
default=inspect._empty,
),
inspect.Parameter(
"strategy",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default="rsi_30_70",
),
inspect.Parameter(
"commission_per_trade",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=float,
default=0.0,
),
inspect.Parameter(
"slippage_bps",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=float,
default=0.0,
),
]
)
input_schema = {
"type": "object",
"properties": {
"close": {
"type": "array",
"items": {"type": "number"},
"description": "Close price series.",
},
"strategy": {
"type": "string",
"description": (
"Strategy name: 'rsi_30_70', 'sma_crossover', or 'macd_crossover'."
),
"default": "rsi_30_70",
},
"commission_per_trade": {
"type": "number",
"description": "Fixed commission per trade.",
"default": 0.0,
},
"slippage_bps": {
"type": "number",
"description": "Slippage in basis points.",
"default": 0.0,
},
},
"required": ["close"],
"additionalProperties": False,
}
def invoke(arguments: dict[str, Any]) -> Any:
close = np.asarray(arguments["close"], dtype=np.float64)
result = run_backtest(
str(arguments.get("strategy", "rsi_30_70")),
close,
commission_per_trade=float(arguments.get("commission_per_trade", 0.0)),
slippage_bps=float(arguments.get("slippage_bps", 0.0)),
)
return _normalise_json(result)
return _ToolSpec(
name="backtest",
description=(
"Run a vectorized backtest on close prices using a named strategy. "
"Returns final equity, trade count, and the equity curve."
),
input_schema=input_schema,
wrapper_signature=wrapper_signature,
invoke=invoke,
)
def _instance_management_specs() -> list[_ToolSpec]:
"""Return generic stored-object management tools."""
list_signature = inspect.Signature([])
simple_schema = {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": False,
}
describe_signature = inspect.Signature(
[
inspect.Parameter(
"instance_id",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default=inspect._empty,
)
]
)
describe_schema = {
"type": "object",
"properties": {
"instance_id": {
"type": "string",
"description": "Stored object identifier.",
}
},
"required": ["instance_id"],
"additionalProperties": False,
}
call_signature = inspect.Signature(
[
inspect.Parameter(
"instance_id",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default=inspect._empty,
),
inspect.Parameter(
"method",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default=inspect._empty,
),
inspect.Parameter(
"args",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=list[Any],
default=None,
),
inspect.Parameter(
"kwargs",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=dict[str, Any],
default=None,
),
]
)
call_schema = {
"type": "object",
"properties": {
"instance_id": {
"type": "string",
"description": "Stored object identifier.",
},
"method": {
"type": "string",
"description": "Public method name.",
},
"args": {
"type": "array",
"items": {},
"description": "Optional positional arguments.",
},
"kwargs": {
"type": "object",
"description": f"Optional keyword arguments. {_REFERENCE_HELP}",
},
},
"required": ["instance_id", "method"],
"additionalProperties": False,
}
callable_signature = inspect.Signature(
[
inspect.Parameter(
"instance_id",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default=inspect._empty,
),
inspect.Parameter(
"args",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=list[Any],
default=None,
),
inspect.Parameter(
"kwargs",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=dict[str, Any],
default=None,
),
]
)
callable_schema = {
"type": "object",
"properties": {
"instance_id": {
"type": "string",
"description": "Stored callable identifier.",
},
"args": {
"type": "array",
"items": {},
"description": "Optional positional arguments.",
},
"kwargs": {
"type": "object",
"description": f"Optional keyword arguments. {_REFERENCE_HELP}",
},
},
"required": ["instance_id"],
"additionalProperties": False,
}
def list_instances_tool(arguments: dict[str, Any]) -> Any:
del arguments
return _list_instances_payload()
def describe_instance_tool(arguments: dict[str, Any]) -> Any:
return _describe_instance_payload(str(arguments["instance_id"]))
def call_instance_method_tool(arguments: dict[str, Any]) -> Any:
value = _get_instance(str(arguments["instance_id"]))
method_name = str(arguments["method"])
if method_name.startswith("_"):
raise ValueError("Only public methods can be called")
method = getattr(value, method_name)
if not callable(method):
raise TypeError(
f"{method_name!r} is not callable on {arguments['instance_id']!r}"
)
args = [_decode_value(item, Any) for item in (arguments.get("args") or [])]
kwargs = {
str(key): _decode_value(item, Any)
for key, item in (arguments.get("kwargs") or {}).items()
}
return _normalise_json(method(*args, **kwargs))
def call_stored_callable_tool(arguments: dict[str, Any]) -> Any:
value = _get_instance(str(arguments["instance_id"]))
if not callable(value):
raise TypeError(f"{arguments['instance_id']!r} is not callable")
args = [_decode_value(item, Any) for item in (arguments.get("args") or [])]
kwargs = {
str(key): _decode_value(item, Any)
for key, item in (arguments.get("kwargs") or {}).items()
}
return _normalise_json(value(*args, **kwargs))
def delete_instance_tool(arguments: dict[str, Any]) -> Any:
identifier = str(arguments["instance_id"])
payload = _describe_instance_payload(identifier)
_INSTANCE_STORE.pop(identifier)
_INSTANCE_META.pop(identifier, None)
return {"deleted": True, **payload}
return [
_ToolSpec(
name="list_instances",
description="List stored MCP object references created during this session.",
input_schema=simple_schema,
wrapper_signature=list_signature,
invoke=list_instances_tool,
),
_ToolSpec(
name="describe_instance",
description="Describe a stored MCP object reference and list its public methods.",
input_schema=describe_schema,
wrapper_signature=describe_signature,
invoke=describe_instance_tool,
),
_ToolSpec(
name="call_instance_method",
description="Call a public method on a stored MCP object reference.",
input_schema=call_schema,
wrapper_signature=call_signature,
invoke=call_instance_method_tool,
),
_ToolSpec(
name="call_stored_callable",
description="Invoke a stored callable object reference.",
input_schema=callable_schema,
wrapper_signature=callable_signature,
invoke=call_stored_callable_tool,
),
_ToolSpec(
name="delete_instance",
description="Delete a stored MCP object reference.",
input_schema=describe_schema,
wrapper_signature=describe_signature,
invoke=delete_instance_tool,
),
]
@lru_cache(maxsize=1)
def _tool_catalog() -> dict[str, _ToolSpec]:
"""Return the full MCP tool catalog."""
catalog: dict[str, _ToolSpec] = {}
legacy_specs = [
_legacy_series_tool(
"sma",
indicator_name="SMA",
description="Compute the Simple Moving Average (SMA) of a price series.",
),
_legacy_series_tool(
"ema",
indicator_name="EMA",
description="Compute the Exponential Moving Average (EMA) of a price series.",
),
_legacy_series_tool(
"rsi",
indicator_name="RSI",
description="Compute the Relative Strength Index (RSI) of a price series.",
),
_legacy_macd_tool(),
_legacy_backtest_tool(),
]
for spec in legacy_specs:
catalog[spec.name] = spec
public_entries, targets = _discover_public_callables()
for item in public_entries:
spec = _build_public_tool_spec(item, targets[item["name"]])
catalog[spec.name] = spec
for spec in _instance_management_specs():
catalog[spec.name] = spec
return catalog
def _make_fastmcp_wrapper(spec: _ToolSpec) -> Callable[..., Any]:
"""Create a FastMCP-friendly wrapper for *spec*."""
def wrapper(**kwargs: Any) -> Any:
return _to_call_tool_result(handle_call_tool(spec.name, dict(kwargs)))
wrapper.__name__ = f"tool_{_slugify(spec.name).replace('-', '_')}"
wrapper.__doc__ = spec.description
setattr(wrapper, "__signature__", spec.wrapper_signature)
wrapper.__annotations__ = {
parameter.name: (
Any if parameter.annotation is inspect._empty else parameter.annotation
)
for parameter in spec.wrapper_signature.parameters.values()
}
wrapper.__annotations__["return"] = Any
return wrapper
def handle_list_tools() -> dict[str, Any]:
"""Return the MCP ListTools response."""
return {
"tools": [
{
"name": spec.name,
"description": spec.description,
"inputSchema": spec.input_schema,
}
for spec in _tool_catalog().values()
]
}
def handle_call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
"""Dispatch an MCP CallTool request and return helper-style content."""
try:
spec = _tool_catalog().get(name)
if spec is None:
return _text_result(
f"Unknown tool: {name!r}",
structured={"error": f"Unknown tool: {name!r}"},
is_error=True,
)
payload = spec.invoke(arguments)
return _response_from_payload(payload)
except Exception as exc:
return _text_result(
f"Error: {exc}",
structured={"error": str(exc)},
is_error=True,
)
@lru_cache(maxsize=1)
def create_server() -> Any:
"""Create the FastMCP server lazily so MCP stays optional."""
fast_mcp_type, _, _ = _load_mcp_sdk()
app = fast_mcp_type(
"ferro-ta",
instructions=(
"Expose ferro-ta's public API over MCP. "
"Use exact public ferro-ta names such as SMA, RSI, compute_indicator, "
"TickAggregator, or AlertManager, or the legacy aliases sma, ema, rsi, "
"macd, and backtest. Use list_instances, describe_instance, "
"call_instance_method, call_stored_callable, and delete_instance for "
"stateful objects and stored callables."
),
)
for spec in _tool_catalog().values():
app.add_tool(
_make_fastmcp_wrapper(spec), name=spec.name, description=spec.description
)
return app
def run_server() -> None: # pragma: no cover
"""Run the stdio MCP server."""
try:
create_server().run(transport="stdio")
except RuntimeError as exc:
raise SystemExit(str(exc)) from exc