algoforge-mcp-server/server.py

231 lines
8.4 KiB
Python
Raw Permalink Normal View History

from fastmcp import FastMCP
from forgeclient import load_config
from handlers import repos, contents, branches, collab, search
mcp = FastMCP(
"MQL5 Algo Forge Server",
instructions=(
"This server manages MQL5 Algo Forge repositories (a Git platform for "
"algorithmic trading code) on the user's behalf. "
"Call get_me first to confirm the connection and learn the user's login — "
"it is the {owner} for their own repositories. "
"Use list_repos to discover existing projects before creating a new one. "
"When committing code, write a clear commit message describing the change. "
"For multi-step changes, create a branch with create_branch, commit to it, "
"then open a pull request with open_pull_request rather than committing "
"straight to the default branch. "
"Confirm with the user before creating repositories, committing files, "
"opening pull requests, or tagging releases."
),
)
def _ensure():
"""Check that an Algo Forge token is configured; return error dict if not."""
config = load_config()
if not config.get("token"):
import os
if not os.environ.get("FORGE_TOKEN"):
return {"error": "No Algo Forge token configured. Set token in "
"config.json or the FORGE_TOKEN environment variable."}
return None
# ── Account / Identity ───────────────────────────────────────────
@mcp.tool(annotations={"readOnlyHint": True})
def get_me() -> dict:
"""Get the authenticated Algo Forge user — login, full name, and profile URL.
Call this first to verify the connection and to learn the username, which is
the {owner} for the user's own repositories."""
err = _ensure()
if err:
return err
return repos.get_me()
# ── Repositories ─────────────────────────────────────────────────
@mcp.tool(annotations={"readOnlyHint": True})
def list_repos(limit: int = 30) -> list:
"""List the authenticated user's repositories. Returns name, description,
default branch, visibility, and clone URL for each. Use this to discover
existing projects before creating or committing to one. Limit range: 1-50."""
err = _ensure()
if err:
return err
return repos.list_repos(limit)
@mcp.tool()
def create_repo(
name: str,
description: str = "",
private: bool = False,
auto_init: bool = True,
default_branch: str = "main",
gitignores: str = "",
readme: str = "",
) -> dict:
"""Create a new repository for the authenticated user. With auto_init=True the
repo starts with an initial commit (and an optional README/.gitignore template),
so you can commit files to it immediately. Set private=True for a private repo.
gitignores is a template name (e.g. "VisualStudio"); readme is "Default"."""
err = _ensure()
if err:
return err
return repos.create_repo(name, description, private, auto_init,
default_branch, gitignores, readme)
# ── File Contents ────────────────────────────────────────────────
@mcp.tool(annotations={"readOnlyHint": True})
def get_file(owner: str, repo: str, path: str, ref: str | None = None) -> dict:
"""Read a file's text content from a repository. path is relative to the repo
root (e.g. "Experts/MyEA.mq5"). ref is an optional branch, tag, or commit;
defaults to the repository's default branch. Returns the decoded text and the
file's sha (needed to update it later)."""
err = _ensure()
if err:
return err
return contents.get_file(owner, repo, path, ref)
@mcp.tool()
def commit_file(
owner: str,
repo: str,
path: str,
content: str,
message: str,
branch: str | None = None,
sha: str | None = None,
) -> dict:
"""Create or update a file in a repository with a single commit. Provide the
full file text in content and a descriptive commit message. The handler detects
whether the file already exists and updates it in place if so. path is relative
to the repo root. branch defaults to the repository's default branch. Returns
the resulting commit sha."""
err = _ensure()
if err:
return err
return contents.commit_file(owner, repo, path, content, message, branch, sha)
# ── Branches ─────────────────────────────────────────────────────
@mcp.tool(annotations={"readOnlyHint": True})
def list_branches(owner: str, repo: str) -> list:
"""List all branches in a repository with their latest commit sha. Use before
creating a branch or opening a pull request to see what already exists."""
err = _ensure()
if err:
return err
return branches.list_branches(owner, repo)
@mcp.tool()
def create_branch(
owner: str,
repo: str,
new_branch: str,
from_branch: str | None = None,
) -> dict:
"""Create a new branch in a repository. from_branch is the source branch to
branch off (defaults to the repository's default branch). Use this to start a
feature or fix before committing changes, so they can be reviewed via a pull
request instead of going straight to the main branch."""
err = _ensure()
if err:
return err
return branches.create_branch(owner, repo, new_branch, from_branch)
# ── Issues & Pull Requests ───────────────────────────────────────
@mcp.tool(annotations={"readOnlyHint": True})
def list_issues(owner: str, repo: str, state: str = "open") -> list:
"""List issues in a repository. state is "open", "closed", or "all".
Pull requests are excluded only true issues are returned."""
err = _ensure()
if err:
return err
return collab.list_issues(owner, repo, state)
@mcp.tool()
def create_issue(owner: str, repo: str, title: str, body: str = "") -> dict:
"""Open a new issue in a repository to track a bug, task, or idea. Returns the
issue number and URL. Useful for logging problems found while reviewing code."""
err = _ensure()
if err:
return err
return collab.create_issue(owner, repo, title, body)
@mcp.tool()
def open_pull_request(
owner: str,
repo: str,
title: str,
head: str,
base: str,
body: str = "",
) -> dict:
"""Open a pull request to merge the head branch into the base branch. Use after
committing changes to a feature branch (head) so they can be reviewed before
merging into the main branch (base). Returns the PR number and whether it is
mergeable."""
err = _ensure()
if err:
return err
return collab.open_pull_request(owner, repo, title, head, base, body)
# ── Releases ─────────────────────────────────────────────────────
@mcp.tool()
def create_release(
owner: str,
repo: str,
tag_name: str,
name: str | None = None,
body: str = "",
target: str = "main",
prerelease: bool = False,
) -> dict:
"""Tag a version of the project. An Algo Forge release is a Git tag with a name
and release notes (MQL5 has no package format, so this marks versions rather
than publishing a package). target is the branch or commit to tag. Returns the
release id and URL."""
err = _ensure()
if err:
return err
return collab.create_release(owner, repo, tag_name, name, body,
target, False, prerelease)
# ── Search / Explore ─────────────────────────────────────────────
@mcp.tool(annotations={"readOnlyHint": True})
def search_repos(query: str, limit: int = 10) -> list:
"""Search public projects across Algo Forge (the Explore section). Use to find
open-source MQL5 code, study other developers' projects, or check whether a
project name is taken. Limit range: 1-50."""
err = _ensure()
if err:
return err
return search.search_repos(query, limit)
if __name__ == "__main__":
mcp.run()