feat: add 13 read tools (PR files/commits, comments, branches, releases, milestones, org/status/languages/topics)
test / test (push) Successful in 1m13s
lint / lint (push) Successful in 1m14s
docker / docker-publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 22s
docker / lint (pull_request) Successful in 29s
lint / lint (pull_request) Successful in 31s
test / test (pull_request) Successful in 21s
docker / docker-test (pull_request) Successful in 23s
test / test (push) Successful in 1m13s
lint / lint (push) Successful in 1m14s
docker / docker-publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 22s
docker / lint (pull_request) Successful in 29s
lint / lint (pull_request) Successful in 31s
test / test (pull_request) Successful in 21s
docker / docker-test (pull_request) Successful in 23s
Expands the read surface so the MCP can inspect more of Gitea: - list_pull_request_files, list_pull_request_commits, list_issue_comments - list_branches, get_branch - get_release, get_latest_release, list_milestones - get_commit_status - list_org_repositories, list_organizations - get_repo_languages, list_repo_topics Each: arg schema (extra=forbid; GitRef on branch/sha fields), Gitea client method with url-encoded path segments, bounded handler, MCP registration (read-only), server wiring, docs, and parametrized success tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,19 @@ Scope requirements:
|
|||||||
- `list_labels` (`owner`, `repo`, optional `page`, `limit`)
|
- `list_labels` (`owner`, `repo`, optional `page`, `limit`)
|
||||||
- `list_tags` (`owner`, `repo`, optional `page`, `limit`)
|
- `list_tags` (`owner`, `repo`, optional `page`, `limit`)
|
||||||
- `list_releases` (`owner`, `repo`, optional `page`, `limit`)
|
- `list_releases` (`owner`, `repo`, optional `page`, `limit`)
|
||||||
|
- `list_pull_request_files` (`owner`, `repo`, `pull_number`, optional `page`, `limit`)
|
||||||
|
- `list_pull_request_commits` (`owner`, `repo`, `pull_number`, optional `page`, `limit`)
|
||||||
|
- `list_issue_comments` (`owner`, `repo`, `issue_number`, optional `page`, `limit`)
|
||||||
|
- `list_branches` (`owner`, `repo`, optional `page`, `limit`)
|
||||||
|
- `get_branch` (`owner`, `repo`, `branch`)
|
||||||
|
- `get_release` (`owner`, `repo`, `release_id`)
|
||||||
|
- `get_latest_release` (`owner`, `repo`)
|
||||||
|
- `list_milestones` (`owner`, `repo`, optional `state`, `page`, `limit`)
|
||||||
|
- `get_commit_status` (`owner`, `repo`, `sha`)
|
||||||
|
- `list_org_repositories` (`org`, optional `page`, `limit`)
|
||||||
|
- `list_organizations` (optional `page`, `limit`)
|
||||||
|
- `get_repo_languages` (`owner`, `repo`)
|
||||||
|
- `list_repo_topics` (`owner`, `repo`)
|
||||||
|
|
||||||
## Write Tools (Write Mode Required)
|
## Write Tools (Write Mode Required)
|
||||||
|
|
||||||
|
|||||||
@@ -989,3 +989,186 @@ class GiteaClient:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
return result if isinstance(result, dict) else {}
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def list_pull_request_files(
|
||||||
|
self, owner: str, repo: str, index: int, *, page: int, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List files changed in a pull request."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/pulls/{index}/files",
|
||||||
|
params={"page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="list_pull_request_files", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def list_pull_request_commits(
|
||||||
|
self, owner: str, repo: str, index: int, *, page: int, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List commits in a pull request."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/pulls/{index}/commits",
|
||||||
|
params={"page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="list_pull_request_commits", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def list_issue_comments(
|
||||||
|
self, owner: str, repo: str, index: int, *, page: int, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List comments on an issue or pull request."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/issues/{index}/comments",
|
||||||
|
params={"page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="list_issue_comments", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def list_branches(
|
||||||
|
self, owner: str, repo: str, *, page: int, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List repository branches."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/branches",
|
||||||
|
params={"page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="list_branches", result_status="pending")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def get_branch(self, owner: str, repo: str, branch: str) -> dict[str, Any]:
|
||||||
|
"""Get a single branch."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/branches/{quote(branch, safe='/')}",
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="get_branch", result_status="pending")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def get_release(self, owner: str, repo: str, release_id: int) -> dict[str, Any]:
|
||||||
|
"""Get a release by id."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/releases/{release_id}",
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="get_release", result_status="pending")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def get_latest_release(self, owner: str, repo: str) -> dict[str, Any]:
|
||||||
|
"""Get the latest published release."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/releases/latest",
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="get_latest_release", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def list_milestones(
|
||||||
|
self, owner: str, repo: str, *, state: str, page: int, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List repository milestones."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/milestones",
|
||||||
|
params={"state": state, "page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="list_milestones", result_status="pending")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def get_commit_status(self, owner: str, repo: str, sha: str) -> dict[str, Any]:
|
||||||
|
"""Get the combined commit status for a ref/sha."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/commits/{quote(sha, safe='/')}/status",
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="get_commit_status", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def list_org_repositories(
|
||||||
|
self, org: str, *, page: int, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List repositories belonging to an organization."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/orgs/{quote(org, safe='')}/repos",
|
||||||
|
params={"page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="list_org_repositories", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def list_organizations(self, *, page: int, limit: int) -> list[dict[str, Any]]:
|
||||||
|
"""List organizations the authenticated user belongs to."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
"/api/v1/user/orgs",
|
||||||
|
params={"page": page, "limit": limit},
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="list_organizations", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def get_repo_languages(self, owner: str, repo: str) -> dict[str, Any]:
|
||||||
|
"""Get the language breakdown for a repository."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/languages",
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="get_repo_languages", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def list_repo_topics(self, owner: str, repo: str) -> list[str]:
|
||||||
|
"""List the topics assigned to a repository."""
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/topics",
|
||||||
|
correlation_id=str(
|
||||||
|
self.audit.log_tool_invocation(
|
||||||
|
tool_name="list_repo_topics", result_status="pending"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if isinstance(result, dict):
|
||||||
|
topics = result.get("topics", [])
|
||||||
|
return [str(topic) for topic in topics] if isinstance(topics, list) else []
|
||||||
|
return []
|
||||||
|
|||||||
@@ -274,6 +274,184 @@ AVAILABLE_TOOLS: list[MCPTool] = [
|
|||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_tool(
|
||||||
|
"list_pull_request_files",
|
||||||
|
"List files changed in a pull request.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"pull_number": {"type": "integer", "minimum": 1},
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "pull_number"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_pull_request_commits",
|
||||||
|
"List commits in a pull request.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"pull_number": {"type": "integer", "minimum": 1},
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "pull_number"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_issue_comments",
|
||||||
|
"List comments on an issue or pull request.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"issue_number": {"type": "integer", "minimum": 1},
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "issue_number"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_branches",
|
||||||
|
"List repository branches.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"get_branch",
|
||||||
|
"Get a single branch.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"branch": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "branch"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"get_release",
|
||||||
|
"Get a release by id.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"release_id": {"type": "integer", "minimum": 1},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "release_id"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"get_latest_release",
|
||||||
|
"Get the latest published release.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"owner": {"type": "string"}, "repo": {"type": "string"}},
|
||||||
|
"required": ["owner", "repo"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_milestones",
|
||||||
|
"List repository milestones.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"},
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"get_commit_status",
|
||||||
|
"Get the combined commit status for a ref or sha.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"sha": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "sha"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_org_repositories",
|
||||||
|
"List repositories belonging to an organization.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"org": {"type": "string"},
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": ["org"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_organizations",
|
||||||
|
"List organizations the authenticated user belongs to.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"get_repo_languages",
|
||||||
|
"Get the language breakdown for a repository.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"owner": {"type": "string"}, "repo": {"type": "string"}},
|
||||||
|
"required": ["owner", "repo"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"list_repo_topics",
|
||||||
|
"List the topics assigned to a repository.",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"owner": {"type": "string"}, "repo": {"type": "string"}},
|
||||||
|
"required": ["owner", "repo"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
_tool(
|
_tool(
|
||||||
"create_issue",
|
"create_issue",
|
||||||
"Create a repository issue (write-mode only).",
|
"Create a repository issue (write-mode only).",
|
||||||
|
|||||||
@@ -61,14 +61,27 @@ from aegis_gitea_mcp.security import sanitize_data
|
|||||||
from aegis_gitea_mcp.tools.arguments import extract_repository, extract_target_path
|
from aegis_gitea_mcp.tools.arguments import extract_repository, extract_target_path
|
||||||
from aegis_gitea_mcp.tools.read_tools import (
|
from aegis_gitea_mcp.tools.read_tools import (
|
||||||
compare_refs_tool,
|
compare_refs_tool,
|
||||||
|
get_branch_tool,
|
||||||
get_commit_diff_tool,
|
get_commit_diff_tool,
|
||||||
|
get_commit_status_tool,
|
||||||
get_issue_tool,
|
get_issue_tool,
|
||||||
|
get_latest_release_tool,
|
||||||
get_pull_request_tool,
|
get_pull_request_tool,
|
||||||
|
get_release_tool,
|
||||||
|
get_repo_languages_tool,
|
||||||
|
list_branches_tool,
|
||||||
list_commits_tool,
|
list_commits_tool,
|
||||||
|
list_issue_comments_tool,
|
||||||
list_issues_tool,
|
list_issues_tool,
|
||||||
list_labels_tool,
|
list_labels_tool,
|
||||||
|
list_milestones_tool,
|
||||||
|
list_org_repositories_tool,
|
||||||
|
list_organizations_tool,
|
||||||
|
list_pull_request_commits_tool,
|
||||||
|
list_pull_request_files_tool,
|
||||||
list_pull_requests_tool,
|
list_pull_requests_tool,
|
||||||
list_releases_tool,
|
list_releases_tool,
|
||||||
|
list_repo_topics_tool,
|
||||||
list_tags_tool,
|
list_tags_tool,
|
||||||
search_code_tool,
|
search_code_tool,
|
||||||
)
|
)
|
||||||
@@ -344,6 +357,19 @@ TOOL_HANDLERS: dict[str, ToolHandler] = {
|
|||||||
"list_labels": list_labels_tool,
|
"list_labels": list_labels_tool,
|
||||||
"list_tags": list_tags_tool,
|
"list_tags": list_tags_tool,
|
||||||
"list_releases": list_releases_tool,
|
"list_releases": list_releases_tool,
|
||||||
|
"list_pull_request_files": list_pull_request_files_tool,
|
||||||
|
"list_pull_request_commits": list_pull_request_commits_tool,
|
||||||
|
"list_issue_comments": list_issue_comments_tool,
|
||||||
|
"list_branches": list_branches_tool,
|
||||||
|
"get_branch": get_branch_tool,
|
||||||
|
"get_release": get_release_tool,
|
||||||
|
"get_latest_release": get_latest_release_tool,
|
||||||
|
"list_milestones": list_milestones_tool,
|
||||||
|
"get_commit_status": get_commit_status_tool,
|
||||||
|
"list_org_repositories": list_org_repositories_tool,
|
||||||
|
"list_organizations": list_organizations_tool,
|
||||||
|
"get_repo_languages": get_repo_languages_tool,
|
||||||
|
"list_repo_topics": list_repo_topics_tool,
|
||||||
# Write-mode tools
|
# Write-mode tools
|
||||||
"create_issue": create_issue_tool,
|
"create_issue": create_issue_tool,
|
||||||
"update_issue": update_issue_tool,
|
"update_issue": update_issue_tool,
|
||||||
|
|||||||
@@ -317,6 +317,90 @@ class EditIssueCommentArgs(RepositoryArgs):
|
|||||||
body: str = Field(..., min_length=1, max_length=10_000)
|
body: str = Field(..., min_length=1, max_length=10_000)
|
||||||
|
|
||||||
|
|
||||||
|
class ListPullRequestFilesArgs(RepositoryArgs):
|
||||||
|
"""Arguments for list_pull_request_files."""
|
||||||
|
|
||||||
|
pull_number: int = Field(..., ge=1)
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ListPullRequestCommitsArgs(RepositoryArgs):
|
||||||
|
"""Arguments for list_pull_request_commits."""
|
||||||
|
|
||||||
|
pull_number: int = Field(..., ge=1)
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ListIssueCommentsArgs(RepositoryArgs):
|
||||||
|
"""Arguments for list_issue_comments."""
|
||||||
|
|
||||||
|
issue_number: int = Field(..., ge=1)
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ListBranchesArgs(RepositoryArgs):
|
||||||
|
"""Arguments for list_branches."""
|
||||||
|
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class GetBranchArgs(RepositoryArgs):
|
||||||
|
"""Arguments for get_branch."""
|
||||||
|
|
||||||
|
branch: GitRef = Field(..., min_length=1, max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
class GetReleaseArgs(RepositoryArgs):
|
||||||
|
"""Arguments for get_release."""
|
||||||
|
|
||||||
|
release_id: int = Field(..., ge=1)
|
||||||
|
|
||||||
|
|
||||||
|
class LatestReleaseArgs(RepositoryArgs):
|
||||||
|
"""Arguments for get_latest_release."""
|
||||||
|
|
||||||
|
|
||||||
|
class ListMilestonesArgs(RepositoryArgs):
|
||||||
|
"""Arguments for list_milestones."""
|
||||||
|
|
||||||
|
state: Literal["open", "closed", "all"] = Field(default="open")
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class CommitStatusArgs(RepositoryArgs):
|
||||||
|
"""Arguments for get_commit_status."""
|
||||||
|
|
||||||
|
sha: GitRef = Field(..., min_length=1, max_length=64)
|
||||||
|
|
||||||
|
|
||||||
|
class ListOrgRepositoriesArgs(StrictBaseModel):
|
||||||
|
"""Arguments for list_org_repositories."""
|
||||||
|
|
||||||
|
org: str = Field(..., pattern=_REPO_PART_PATTERN)
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ListOrganizationsArgs(StrictBaseModel):
|
||||||
|
"""Arguments for list_organizations."""
|
||||||
|
|
||||||
|
page: int = Field(default=1, ge=1, le=10_000)
|
||||||
|
limit: int = Field(default=50, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class RepoLanguagesArgs(RepositoryArgs):
|
||||||
|
"""Arguments for get_repo_languages."""
|
||||||
|
|
||||||
|
|
||||||
|
class RepoTopicsArgs(RepositoryArgs):
|
||||||
|
"""Arguments for list_repo_topics."""
|
||||||
|
|
||||||
|
|
||||||
def extract_repository(arguments: dict[str, object]) -> str | None:
|
def extract_repository(arguments: dict[str, object]) -> str | None:
|
||||||
"""Extract `owner/repo` from raw argument mapping.
|
"""Extract `owner/repo` from raw argument mapping.
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,28 @@ from aegis_gitea_mcp.gitea_client import (
|
|||||||
from aegis_gitea_mcp.response_limits import limit_items, limit_text
|
from aegis_gitea_mcp.response_limits import limit_items, limit_text
|
||||||
from aegis_gitea_mcp.tools.arguments import (
|
from aegis_gitea_mcp.tools.arguments import (
|
||||||
CommitDiffArgs,
|
CommitDiffArgs,
|
||||||
|
CommitStatusArgs,
|
||||||
CompareRefsArgs,
|
CompareRefsArgs,
|
||||||
|
GetBranchArgs,
|
||||||
|
GetReleaseArgs,
|
||||||
IssueArgs,
|
IssueArgs,
|
||||||
|
LatestReleaseArgs,
|
||||||
|
ListBranchesArgs,
|
||||||
ListCommitsArgs,
|
ListCommitsArgs,
|
||||||
|
ListIssueCommentsArgs,
|
||||||
ListIssuesArgs,
|
ListIssuesArgs,
|
||||||
ListLabelsArgs,
|
ListLabelsArgs,
|
||||||
|
ListMilestonesArgs,
|
||||||
|
ListOrganizationsArgs,
|
||||||
|
ListOrgRepositoriesArgs,
|
||||||
|
ListPullRequestCommitsArgs,
|
||||||
|
ListPullRequestFilesArgs,
|
||||||
ListPullRequestsArgs,
|
ListPullRequestsArgs,
|
||||||
ListReleasesArgs,
|
ListReleasesArgs,
|
||||||
ListTagsArgs,
|
ListTagsArgs,
|
||||||
PullRequestArgs,
|
PullRequestArgs,
|
||||||
|
RepoLanguagesArgs,
|
||||||
|
RepoTopicsArgs,
|
||||||
SearchCodeArgs,
|
SearchCodeArgs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -449,3 +462,359 @@ async def list_releases_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> d
|
|||||||
raise
|
raise
|
||||||
except GiteaError as exc:
|
except GiteaError as exc:
|
||||||
raise RuntimeError(f"Failed to list releases: {exc}") from exc
|
raise RuntimeError(f"Failed to list releases: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def list_pull_request_files_tool(
|
||||||
|
gitea: GiteaClient, arguments: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""List files changed in a pull request."""
|
||||||
|
parsed = ListPullRequestFilesArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
files = await gitea.list_pull_request_files(
|
||||||
|
parsed.owner, parsed.repo, parsed.pull_number, page=parsed.page, limit=parsed.limit
|
||||||
|
)
|
||||||
|
normalized = [
|
||||||
|
{
|
||||||
|
"filename": item.get("filename", ""),
|
||||||
|
"status": item.get("status", ""),
|
||||||
|
"additions": item.get("additions", 0),
|
||||||
|
"deletions": item.get("deletions", 0),
|
||||||
|
"changes": item.get("changes", 0),
|
||||||
|
}
|
||||||
|
for item in files
|
||||||
|
if isinstance(item, dict)
|
||||||
|
]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"pull_number": parsed.pull_number,
|
||||||
|
"files": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list pull request files: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def list_pull_request_commits_tool(
|
||||||
|
gitea: GiteaClient, arguments: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""List commits in a pull request."""
|
||||||
|
parsed = ListPullRequestCommitsArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
commits = await gitea.list_pull_request_commits(
|
||||||
|
parsed.owner, parsed.repo, parsed.pull_number, page=parsed.page, limit=parsed.limit
|
||||||
|
)
|
||||||
|
normalized = [
|
||||||
|
{
|
||||||
|
"sha": commit.get("sha", ""),
|
||||||
|
"message": limit_text(str(commit.get("commit", {}).get("message", ""))),
|
||||||
|
"author": (
|
||||||
|
commit.get("author", {}).get("login", "")
|
||||||
|
if isinstance(commit.get("author"), dict)
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for commit in commits
|
||||||
|
if isinstance(commit, dict)
|
||||||
|
]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"pull_number": parsed.pull_number,
|
||||||
|
"commits": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list pull request commits: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def list_issue_comments_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""List comments on an issue or pull request."""
|
||||||
|
parsed = ListIssueCommentsArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
comments = await gitea.list_issue_comments(
|
||||||
|
parsed.owner, parsed.repo, parsed.issue_number, page=parsed.page, limit=parsed.limit
|
||||||
|
)
|
||||||
|
normalized = [
|
||||||
|
{
|
||||||
|
"id": comment.get("id", 0),
|
||||||
|
"author": (
|
||||||
|
comment.get("user", {}).get("login", "")
|
||||||
|
if isinstance(comment.get("user"), dict)
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"body": limit_text(str(comment.get("body", ""))),
|
||||||
|
"created_at": comment.get("created_at", ""),
|
||||||
|
"updated_at": comment.get("updated_at", ""),
|
||||||
|
"url": comment.get("html_url", ""),
|
||||||
|
}
|
||||||
|
for comment in comments
|
||||||
|
if isinstance(comment, dict)
|
||||||
|
]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"issue_number": parsed.issue_number,
|
||||||
|
"comments": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list issue comments: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_branch(branch: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Normalize a Gitea branch payload."""
|
||||||
|
commit = branch.get("commit", {}) if isinstance(branch.get("commit"), dict) else {}
|
||||||
|
return {
|
||||||
|
"name": branch.get("name", ""),
|
||||||
|
"protected": branch.get("protected", False),
|
||||||
|
"commit": commit.get("id", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_branches_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""List repository branches."""
|
||||||
|
parsed = ListBranchesArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
branches = await gitea.list_branches(
|
||||||
|
parsed.owner, parsed.repo, page=parsed.page, limit=parsed.limit
|
||||||
|
)
|
||||||
|
normalized = [_normalize_branch(b) for b in branches if isinstance(b, dict)]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"branches": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list branches: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def get_branch_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Get a single branch."""
|
||||||
|
parsed = GetBranchArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
branch = await gitea.get_branch(parsed.owner, parsed.repo, parsed.branch)
|
||||||
|
result = _normalize_branch(branch)
|
||||||
|
result.update({"owner": parsed.owner, "repo": parsed.repo})
|
||||||
|
return result
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to get branch: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_release(release: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Normalize a Gitea release payload."""
|
||||||
|
return {
|
||||||
|
"id": release.get("id", 0),
|
||||||
|
"tag_name": release.get("tag_name", ""),
|
||||||
|
"name": limit_text(str(release.get("name", ""))),
|
||||||
|
"draft": release.get("draft", False),
|
||||||
|
"prerelease": release.get("prerelease", False),
|
||||||
|
"body": limit_text(str(release.get("body", ""))),
|
||||||
|
"created_at": release.get("created_at", ""),
|
||||||
|
"published_at": release.get("published_at", ""),
|
||||||
|
"url": release.get("html_url", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_release_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Get a release by id."""
|
||||||
|
parsed = GetReleaseArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
release = await gitea.get_release(parsed.owner, parsed.repo, parsed.release_id)
|
||||||
|
return _normalize_release(release)
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to get release: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def get_latest_release_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Get the latest published release."""
|
||||||
|
parsed = LatestReleaseArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
release = await gitea.get_latest_release(parsed.owner, parsed.repo)
|
||||||
|
return _normalize_release(release)
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to get latest release: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def list_milestones_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""List repository milestones."""
|
||||||
|
parsed = ListMilestonesArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
milestones = await gitea.list_milestones(
|
||||||
|
parsed.owner, parsed.repo, state=parsed.state, page=parsed.page, limit=parsed.limit
|
||||||
|
)
|
||||||
|
normalized = [
|
||||||
|
{
|
||||||
|
"id": m.get("id", 0),
|
||||||
|
"title": limit_text(str(m.get("title", ""))),
|
||||||
|
"state": m.get("state", ""),
|
||||||
|
"open_issues": m.get("open_issues", 0),
|
||||||
|
"closed_issues": m.get("closed_issues", 0),
|
||||||
|
"due_on": m.get("due_on", ""),
|
||||||
|
}
|
||||||
|
for m in milestones
|
||||||
|
if isinstance(m, dict)
|
||||||
|
]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"state": parsed.state,
|
||||||
|
"milestones": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list milestones: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def get_commit_status_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Get the combined commit status for a ref/sha."""
|
||||||
|
parsed = CommitStatusArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
status = await gitea.get_commit_status(parsed.owner, parsed.repo, parsed.sha)
|
||||||
|
statuses_raw = status.get("statuses", []) if isinstance(status, dict) else []
|
||||||
|
statuses = [
|
||||||
|
{
|
||||||
|
"context": s.get("context", ""),
|
||||||
|
"state": s.get("status", s.get("state", "")),
|
||||||
|
"target_url": s.get("target_url", ""),
|
||||||
|
}
|
||||||
|
for s in statuses_raw
|
||||||
|
if isinstance(s, dict)
|
||||||
|
]
|
||||||
|
bounded, omitted = limit_items(statuses)
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"sha": parsed.sha,
|
||||||
|
"state": status.get("state", "") if isinstance(status, dict) else "",
|
||||||
|
"statuses": bounded,
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to get commit status: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_repo_summary(repo: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Normalize a repository payload to a compact summary."""
|
||||||
|
owner = repo.get("owner", {})
|
||||||
|
return {
|
||||||
|
"owner": owner.get("login", "") if isinstance(owner, dict) else "",
|
||||||
|
"name": repo.get("name", ""),
|
||||||
|
"full_name": repo.get("full_name", ""),
|
||||||
|
"private": repo.get("private", False),
|
||||||
|
"description": limit_text(str(repo.get("description", ""))),
|
||||||
|
"url": repo.get("html_url", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_org_repositories_tool(
|
||||||
|
gitea: GiteaClient, arguments: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""List repositories belonging to an organization."""
|
||||||
|
parsed = ListOrgRepositoriesArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
repos = await gitea.list_org_repositories(parsed.org, page=parsed.page, limit=parsed.limit)
|
||||||
|
normalized = [_normalize_repo_summary(r) for r in repos if isinstance(r, dict)]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"org": parsed.org,
|
||||||
|
"repositories": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list org repositories: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def list_organizations_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""List organizations the authenticated user belongs to."""
|
||||||
|
parsed = ListOrganizationsArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
orgs = await gitea.list_organizations(page=parsed.page, limit=parsed.limit)
|
||||||
|
normalized = [
|
||||||
|
{
|
||||||
|
"id": org.get("id", 0),
|
||||||
|
"name": org.get("username", org.get("name", "")),
|
||||||
|
"full_name": org.get("full_name", ""),
|
||||||
|
"description": limit_text(str(org.get("description", ""))),
|
||||||
|
}
|
||||||
|
for org in orgs
|
||||||
|
if isinstance(org, dict)
|
||||||
|
]
|
||||||
|
bounded, omitted = limit_items(normalized, configured_limit=parsed.limit)
|
||||||
|
return {
|
||||||
|
"organizations": bounded,
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list organizations: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def get_repo_languages_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Get the language breakdown for a repository."""
|
||||||
|
parsed = RepoLanguagesArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
languages = await gitea.get_repo_languages(parsed.owner, parsed.repo)
|
||||||
|
cleaned = {str(name): value for name, value in languages.items() if isinstance(name, str)}
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"languages": cleaned,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to get repository languages: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def list_repo_topics_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""List the topics assigned to a repository."""
|
||||||
|
parsed = RepoTopicsArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
topics = await gitea.list_repo_topics(parsed.owner, parsed.repo)
|
||||||
|
bounded, omitted = limit_items([{"topic": t} for t in topics])
|
||||||
|
return {
|
||||||
|
"owner": parsed.owner,
|
||||||
|
"repo": parsed.repo,
|
||||||
|
"topics": [entry["topic"] for entry in bounded],
|
||||||
|
"count": len(bounded),
|
||||||
|
"omitted": omitted,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to list repository topics: {exc}") from exc
|
||||||
|
|||||||
@@ -6,14 +6,27 @@ from aegis_gitea_mcp.config import reset_settings
|
|||||||
from aegis_gitea_mcp.gitea_client import GiteaError
|
from aegis_gitea_mcp.gitea_client import GiteaError
|
||||||
from aegis_gitea_mcp.tools.read_tools import (
|
from aegis_gitea_mcp.tools.read_tools import (
|
||||||
compare_refs_tool,
|
compare_refs_tool,
|
||||||
|
get_branch_tool,
|
||||||
get_commit_diff_tool,
|
get_commit_diff_tool,
|
||||||
|
get_commit_status_tool,
|
||||||
get_issue_tool,
|
get_issue_tool,
|
||||||
|
get_latest_release_tool,
|
||||||
get_pull_request_tool,
|
get_pull_request_tool,
|
||||||
|
get_release_tool,
|
||||||
|
get_repo_languages_tool,
|
||||||
|
list_branches_tool,
|
||||||
list_commits_tool,
|
list_commits_tool,
|
||||||
|
list_issue_comments_tool,
|
||||||
list_issues_tool,
|
list_issues_tool,
|
||||||
list_labels_tool,
|
list_labels_tool,
|
||||||
|
list_milestones_tool,
|
||||||
|
list_org_repositories_tool,
|
||||||
|
list_organizations_tool,
|
||||||
|
list_pull_request_commits_tool,
|
||||||
|
list_pull_request_files_tool,
|
||||||
list_pull_requests_tool,
|
list_pull_requests_tool,
|
||||||
list_releases_tool,
|
list_releases_tool,
|
||||||
|
list_repo_topics_tool,
|
||||||
list_tags_tool,
|
list_tags_tool,
|
||||||
search_code_tool,
|
search_code_tool,
|
||||||
)
|
)
|
||||||
@@ -88,6 +101,45 @@ class StubGitea:
|
|||||||
async def list_releases(self, owner, repo, *, page, limit):
|
async def list_releases(self, owner, repo, *, page, limit):
|
||||||
return [{"id": 1, "tag_name": "v1.0.0", "name": "release"}]
|
return [{"id": 1, "tag_name": "v1.0.0", "name": "release"}]
|
||||||
|
|
||||||
|
async def list_pull_request_files(self, owner, repo, index, *, page, limit):
|
||||||
|
return [{"filename": "a.py", "status": "modified", "additions": 1, "deletions": 0}]
|
||||||
|
|
||||||
|
async def list_pull_request_commits(self, owner, repo, index, *, page, limit):
|
||||||
|
return [{"sha": "abc", "commit": {"message": "m"}, "author": {"login": "alice"}}]
|
||||||
|
|
||||||
|
async def list_issue_comments(self, owner, repo, index, *, page, limit):
|
||||||
|
return [{"id": 1, "body": "hi", "user": {"login": "alice"}}]
|
||||||
|
|
||||||
|
async def list_branches(self, owner, repo, *, page, limit):
|
||||||
|
return [{"name": "main", "protected": True, "commit": {"id": "abc"}}]
|
||||||
|
|
||||||
|
async def get_branch(self, owner, repo, branch):
|
||||||
|
return {"name": branch, "protected": False, "commit": {"id": "abc"}}
|
||||||
|
|
||||||
|
async def get_release(self, owner, repo, release_id):
|
||||||
|
return {"id": release_id, "tag_name": "v1", "name": "rel"}
|
||||||
|
|
||||||
|
async def get_latest_release(self, owner, repo):
|
||||||
|
return {"id": 1, "tag_name": "v1", "name": "rel"}
|
||||||
|
|
||||||
|
async def list_milestones(self, owner, repo, *, state, page, limit):
|
||||||
|
return [{"id": 1, "title": "M", "state": state}]
|
||||||
|
|
||||||
|
async def get_commit_status(self, owner, repo, sha):
|
||||||
|
return {"state": "success", "statuses": [{"context": "ci", "status": "success"}]}
|
||||||
|
|
||||||
|
async def list_org_repositories(self, org, *, page, limit):
|
||||||
|
return [{"name": "r", "owner": {"login": org}, "full_name": f"{org}/r"}]
|
||||||
|
|
||||||
|
async def list_organizations(self, *, page, limit):
|
||||||
|
return [{"id": 1, "username": "acme", "description": "d"}]
|
||||||
|
|
||||||
|
async def get_repo_languages(self, owner, repo):
|
||||||
|
return {"Python": 100, "HTML": 5}
|
||||||
|
|
||||||
|
async def list_repo_topics(self, owner, repo):
|
||||||
|
return ["python", "mcp"]
|
||||||
|
|
||||||
async def create_issue(self, owner, repo, *, title, body, labels=None, assignees=None):
|
async def create_issue(self, owner, repo, *, title, body, labels=None, assignees=None):
|
||||||
return {"number": 1, "title": title, "state": "open"}
|
return {"number": 1, "title": title, "state": "open"}
|
||||||
|
|
||||||
@@ -169,6 +221,35 @@ class ErrorGitea(StubGitea):
|
|||||||
(list_labels_tool, {"owner": "acme", "repo": "app"}, "labels"),
|
(list_labels_tool, {"owner": "acme", "repo": "app"}, "labels"),
|
||||||
(list_tags_tool, {"owner": "acme", "repo": "app"}, "tags"),
|
(list_tags_tool, {"owner": "acme", "repo": "app"}, "tags"),
|
||||||
(list_releases_tool, {"owner": "acme", "repo": "app"}, "releases"),
|
(list_releases_tool, {"owner": "acme", "repo": "app"}, "releases"),
|
||||||
|
(
|
||||||
|
list_pull_request_files_tool,
|
||||||
|
{"owner": "acme", "repo": "app", "pull_number": 1},
|
||||||
|
"files",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
list_pull_request_commits_tool,
|
||||||
|
{"owner": "acme", "repo": "app", "pull_number": 1},
|
||||||
|
"commits",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
list_issue_comments_tool,
|
||||||
|
{"owner": "acme", "repo": "app", "issue_number": 1},
|
||||||
|
"comments",
|
||||||
|
),
|
||||||
|
(list_branches_tool, {"owner": "acme", "repo": "app"}, "branches"),
|
||||||
|
(get_branch_tool, {"owner": "acme", "repo": "app", "branch": "main"}, "name"),
|
||||||
|
(get_release_tool, {"owner": "acme", "repo": "app", "release_id": 1}, "tag_name"),
|
||||||
|
(get_latest_release_tool, {"owner": "acme", "repo": "app"}, "tag_name"),
|
||||||
|
(list_milestones_tool, {"owner": "acme", "repo": "app"}, "milestones"),
|
||||||
|
(
|
||||||
|
get_commit_status_tool,
|
||||||
|
{"owner": "acme", "repo": "app", "sha": "abc1234"},
|
||||||
|
"state",
|
||||||
|
),
|
||||||
|
(list_org_repositories_tool, {"org": "acme"}, "repositories"),
|
||||||
|
(list_organizations_tool, {}, "organizations"),
|
||||||
|
(get_repo_languages_tool, {"owner": "acme", "repo": "app"}, "languages"),
|
||||||
|
(list_repo_topics_tool, {"owner": "acme", "repo": "app"}, "topics"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_extended_read_tools_success(tool, args, expected_key):
|
async def test_extended_read_tools_success(tool, args, expected_key):
|
||||||
|
|||||||
Reference in New Issue
Block a user