Add Gitea integration for Telegram work mode

This commit is contained in:
2026-06-09 13:50:23 +09:00
parent 5a2c5df041
commit c7652d3ada
10 changed files with 656 additions and 5 deletions

View File

@@ -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}