Add Gitea integration for Telegram work mode
This commit is contained in:
@@ -6,6 +6,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.gitea_client import GiteaClient
|
||||
from app.state import StateStore, format_personal_record, format_schedule
|
||||
|
||||
|
||||
@@ -58,7 +59,9 @@ class BridgeMcpServer:
|
||||
"Use these tools when the user asks to schedule recurring Telegram "
|
||||
"notifications, manage bridge schedules, or persist personal assistant "
|
||||
"records such as work history, todos, purchase candidates, birthdays, "
|
||||
"contacts, and notes. The Telegram chat_id and "
|
||||
"contacts, and notes. Use the Gitea tools when the user asks to inspect "
|
||||
"or manage repositories, issues, pull requests, and code-review workflow "
|
||||
"metadata in the user's configured Gitea instance. The Telegram chat_id and "
|
||||
"current thread_id are provided in the bridge context inside each user turn. "
|
||||
"Use that chat_id as the default notification target and do not ask for a "
|
||||
"destination unless the user explicitly wants a different chat."
|
||||
@@ -106,6 +109,22 @@ class BridgeMcpServer:
|
||||
return self.update_personal_record(arguments)
|
||||
if name == "telegram_delete_personal_record":
|
||||
return self.delete_personal_record(arguments)
|
||||
if name == "gitea_list_repositories":
|
||||
return self.gitea_list_repositories(arguments)
|
||||
if name == "gitea_get_repository":
|
||||
return self.gitea_get_repository(arguments)
|
||||
if name == "gitea_list_issues":
|
||||
return self.gitea_list_issues(arguments)
|
||||
if name == "gitea_create_issue":
|
||||
return self.gitea_create_issue(arguments)
|
||||
if name == "gitea_comment_issue":
|
||||
return self.gitea_comment_issue(arguments)
|
||||
if name == "gitea_list_pull_requests":
|
||||
return self.gitea_list_pull_requests(arguments)
|
||||
if name == "gitea_create_pull_request":
|
||||
return self.gitea_create_pull_request(arguments)
|
||||
if name == "gitea_list_branches":
|
||||
return self.gitea_list_branches(arguments)
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
def create_schedule(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -251,6 +270,132 @@ class BridgeMcpServer:
|
||||
"structuredContent": {"deleted": removed},
|
||||
}
|
||||
|
||||
def gitea_list_repositories(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
repos = client.list_repositories(
|
||||
limit=int(arguments.get("limit") or 30),
|
||||
page=int(arguments.get("page") or 1),
|
||||
)
|
||||
if not repos:
|
||||
text = "No Gitea repositories found."
|
||||
else:
|
||||
text = "\n".join(_format_gitea_repo(repo) for repo in repos)
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"structuredContent": {"repositories": repos},
|
||||
}
|
||||
|
||||
def gitea_get_repository(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
repository = client.get_repository(owner, repo)
|
||||
return {
|
||||
"content": [{"type": "text", "text": _format_gitea_repo(repository)}],
|
||||
"structuredContent": {"repository": repository},
|
||||
}
|
||||
|
||||
def gitea_list_issues(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
issues = client.list_issues(
|
||||
owner,
|
||||
repo,
|
||||
state=str(arguments.get("state") or "open"),
|
||||
limit=int(arguments.get("limit") or 20),
|
||||
page=int(arguments.get("page") or 1),
|
||||
)
|
||||
if not issues:
|
||||
text = "No Gitea issues found."
|
||||
else:
|
||||
text = "\n".join(_format_gitea_issue(issue) for issue in issues)
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"structuredContent": {"issues": issues},
|
||||
}
|
||||
|
||||
def gitea_create_issue(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
issue = client.create_issue(
|
||||
owner,
|
||||
repo,
|
||||
title=str(arguments["title"]),
|
||||
body=str(arguments.get("body") or ""),
|
||||
)
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"Created Gitea issue: {_format_gitea_issue(issue)}"}],
|
||||
"structuredContent": {"issue": issue},
|
||||
}
|
||||
|
||||
def gitea_comment_issue(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
comment = client.comment_issue(
|
||||
owner,
|
||||
repo,
|
||||
issue_index=int(arguments["issue_index"]),
|
||||
body=str(arguments["body"]),
|
||||
)
|
||||
url = comment.get("html_url") or comment.get("url") or ""
|
||||
text = f"Added Gitea issue comment: {url}" if url else "Added Gitea issue comment."
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"structuredContent": {"comment": comment},
|
||||
}
|
||||
|
||||
def gitea_list_pull_requests(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
pulls = client.list_pull_requests(
|
||||
owner,
|
||||
repo,
|
||||
state=str(arguments.get("state") or "open"),
|
||||
limit=int(arguments.get("limit") or 20),
|
||||
page=int(arguments.get("page") or 1),
|
||||
)
|
||||
if not pulls:
|
||||
text = "No Gitea pull requests found."
|
||||
else:
|
||||
text = "\n".join(_format_gitea_pull_request(pull) for pull in pulls)
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"structuredContent": {"pull_requests": pulls},
|
||||
}
|
||||
|
||||
def gitea_create_pull_request(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
pull = client.create_pull_request(
|
||||
owner,
|
||||
repo,
|
||||
head=str(arguments["head"]),
|
||||
base=str(arguments.get("base") or "main"),
|
||||
title=str(arguments["title"]),
|
||||
body=str(arguments.get("body") or ""),
|
||||
)
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"Created Gitea pull request: {_format_gitea_pull_request(pull)}"}],
|
||||
"structuredContent": {"pull_request": pull},
|
||||
}
|
||||
|
||||
def gitea_list_branches(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
branches = client.list_branches(
|
||||
owner,
|
||||
repo,
|
||||
limit=int(arguments.get("limit") or 30),
|
||||
page=int(arguments.get("page") or 1),
|
||||
)
|
||||
if not branches:
|
||||
text = "No Gitea branches found."
|
||||
else:
|
||||
text = "\n".join(str(branch.get("name") or "(unknown)") for branch in branches)
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"structuredContent": {"branches": branches},
|
||||
}
|
||||
|
||||
|
||||
def _tools() -> list[dict[str, Any]]:
|
||||
return [
|
||||
@@ -453,6 +598,145 @@ def _tools() -> list[dict[str, Any]]:
|
||||
"openWorldHint": False,
|
||||
},
|
||||
},
|
||||
] + _gitea_tools()
|
||||
|
||||
|
||||
def _gitea_tools() -> list[dict[str, Any]]:
|
||||
repo_properties = {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner. Defaults to GITEA_DEFAULT_OWNER when omitted.",
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name. Defaults to GITEA_DEFAULT_REPO when omitted.",
|
||||
},
|
||||
}
|
||||
readonly_annotations = {
|
||||
"readOnlyHint": True,
|
||||
"destructiveHint": False,
|
||||
"idempotentHint": True,
|
||||
"openWorldHint": False,
|
||||
}
|
||||
write_annotations = {
|
||||
"readOnlyHint": False,
|
||||
"destructiveHint": False,
|
||||
"idempotentHint": False,
|
||||
"openWorldHint": False,
|
||||
}
|
||||
return [
|
||||
{
|
||||
"name": "gitea_list_repositories",
|
||||
"description": "List repositories visible to the configured Gitea token.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"page": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
},
|
||||
"annotations": {"title": "List Gitea repositories", **readonly_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_get_repository",
|
||||
"description": "Get metadata for one Gitea repository.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": repo_properties,
|
||||
},
|
||||
"annotations": {"title": "Get Gitea repository", **readonly_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_list_issues",
|
||||
"description": "List issues for a Gitea repository.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"state": {"type": "string", "enum": ["open", "closed", "all"]},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"page": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
},
|
||||
"annotations": {"title": "List Gitea issues", **readonly_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_create_issue",
|
||||
"description": "Create an issue in a Gitea repository.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"title": {"type": "string"},
|
||||
"body": {"type": "string"},
|
||||
},
|
||||
"required": ["title"],
|
||||
},
|
||||
"annotations": {"title": "Create Gitea issue", **write_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_comment_issue",
|
||||
"description": "Add a comment to a Gitea issue or pull request conversation.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"issue_index": {"type": "integer", "minimum": 1},
|
||||
"body": {"type": "string"},
|
||||
},
|
||||
"required": ["issue_index", "body"],
|
||||
},
|
||||
"annotations": {"title": "Comment on Gitea issue", **write_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_list_pull_requests",
|
||||
"description": "List pull requests for a Gitea repository.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"state": {"type": "string", "enum": ["open", "closed", "all"]},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"page": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
},
|
||||
"annotations": {"title": "List Gitea pull requests", **readonly_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_create_pull_request",
|
||||
"description": "Create a pull request in a Gitea repository after a branch has been pushed.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"head": {
|
||||
"type": "string",
|
||||
"description": "Source branch, for example codex/my-change.",
|
||||
},
|
||||
"base": {
|
||||
"type": "string",
|
||||
"description": "Target branch. Defaults to main.",
|
||||
},
|
||||
"title": {"type": "string"},
|
||||
"body": {"type": "string"},
|
||||
},
|
||||
"required": ["head", "title"],
|
||||
},
|
||||
"annotations": {"title": "Create Gitea pull request", **write_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_list_branches",
|
||||
"description": "List branches for a Gitea repository.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"page": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
},
|
||||
"annotations": {"title": "List Gitea branches", **readonly_annotations},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -460,6 +744,49 @@ def _record_categories() -> list[str]:
|
||||
return ["work_log", "todo", "purchase", "birthday", "contact", "note", "preference"]
|
||||
|
||||
|
||||
def _repo_arguments(arguments: dict[str, Any], client: GiteaClient) -> tuple[str, str]:
|
||||
owner = str(arguments.get("owner") or client.config.default_owner or "").strip()
|
||||
repo = str(arguments.get("repo") or client.config.default_repo or "").strip()
|
||||
if not owner:
|
||||
raise ValueError("owner is required or GITEA_DEFAULT_OWNER must be set")
|
||||
if not repo:
|
||||
raise ValueError("repo is required or GITEA_DEFAULT_REPO must be set")
|
||||
return owner, repo
|
||||
|
||||
|
||||
def _format_gitea_repo(repo: dict[str, Any]) -> str:
|
||||
full_name = repo.get("full_name") or repo.get("name") or "(unknown)"
|
||||
default_branch = repo.get("default_branch") or repo.get("default_branch_name") or ""
|
||||
clone_url = repo.get("clone_url") or ""
|
||||
html_url = repo.get("html_url") or ""
|
||||
bits = [str(full_name)]
|
||||
if default_branch:
|
||||
bits.append(f"default={default_branch}")
|
||||
if html_url:
|
||||
bits.append(str(html_url))
|
||||
if clone_url:
|
||||
bits.append(f"clone={clone_url}")
|
||||
return " - ".join(bits)
|
||||
|
||||
|
||||
def _format_gitea_issue(issue: dict[str, Any]) -> str:
|
||||
index = issue.get("number") or issue.get("index") or "?"
|
||||
title = issue.get("title") or "(untitled)"
|
||||
state = issue.get("state") or "unknown"
|
||||
html_url = issue.get("html_url") or ""
|
||||
suffix = f" - {html_url}" if html_url else ""
|
||||
return f"#{index} [{state}] {title}{suffix}"
|
||||
|
||||
|
||||
def _format_gitea_pull_request(pull: dict[str, Any]) -> str:
|
||||
index = pull.get("number") or pull.get("index") or "?"
|
||||
title = pull.get("title") or "(untitled)"
|
||||
state = pull.get("state") or "unknown"
|
||||
html_url = pull.get("html_url") or ""
|
||||
suffix = f" - {html_url}" if html_url else ""
|
||||
return f"#{index} [{state}] {title}{suffix}"
|
||||
|
||||
|
||||
def _result(request_id: Any, result: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user