231 Zeilen
8,4 KiB
Python
231 Zeilen
8,4 KiB
Python
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()
|