algoforge-mcp-server/forgeclient.py

93 lines
2.4 KiB
Python
Raw Permalink Normal View History

import base64
import json
import logging
import os
import httpx
log = logging.getLogger("forgemcp")
DEFAULT_BASE_URL = "https://forge.mql5.io"
DEFAULT_TIMEOUT = 30
_CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
_client = None
def load_config():
if not os.path.exists(_CONFIG_PATH):
return {}
with open(_CONFIG_PATH, "r") as f:
return json.load(f)
def get_client():
"""Return a singleton authenticated httpx client for the Algo Forge API."""
global _client
if _client is not None:
return _client
config = load_config()
base_url = config.get("base_url") or DEFAULT_BASE_URL
token = config.get("token") or os.environ.get("FORGE_TOKEN")
timeout = config.get("timeout") or DEFAULT_TIMEOUT
if not token:
log.warning("No Algo Forge token configured (config.json or FORGE_TOKEN).")
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"token {token}"
_client = httpx.Client(
base_url=f"{base_url.rstrip('/')}/api/v1",
headers=headers,
timeout=timeout,
)
return _client
def request(method, path, **kwargs):
"""Make an API call and normalize the result into a dict or list.
Returns parsed JSON on success, or {"error": "..."} on any failure.
"""
try:
resp = get_client().request(method, path, **kwargs)
except httpx.RequestError as exc:
return {"error": f"Connection to Algo Forge failed: {exc}"}
if resp.status_code == 204:
return {"ok": True}
if resp.status_code >= 400:
return {"error": _error_message(resp)}
if not resp.content:
return {"ok": True}
try:
return resp.json()
except ValueError:
return {"error": "Algo Forge returned a non-JSON response."}
def _error_message(resp):
"""Extract a human-readable error from a Forgejo error response."""
try:
body = resp.json()
msg = body.get("message") or body.get("error") or str(body)
except ValueError:
msg = resp.text or "unknown error"
return f"HTTP {resp.status_code}: {msg}"
def encode_content(text):
"""Base64-encode file text for the contents API."""
return base64.b64encode(text.encode("utf-8")).decode("ascii")
def decode_content(b64):
"""Decode base64 file content returned by the contents API."""
return base64.b64decode(b64).decode("utf-8", errors="replace")