feat: add create_label write tool
Adds a create_label write-mode tool so labels can be created in a repository
through the MCP server (previously there was no way to define labels, which
blocked attaching labels to issues). Follows the full tool checklist:
- arguments.py: CreateLabelArgs (name, hex color, optional description/exclusive),
with extra=forbid and a hex-color pattern.
- gitea_client.py: create_label() POSTing to /repos/{owner}/{repo}/labels with
url-encoded path segments.
- write_tools.py: create_label_tool handler; normalizes the color to a leading
'#', bounds text output, and lets auth/authz errors surface.
- mcp_protocol.py: register create_label (write_operation=True).
- server.py: wire create_label into TOOL_HANDLERS.
- docs/api-reference.md: document create_label.
- tests: success path, color normalization, and invalid-color rejection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -668,6 +668,33 @@ class GiteaClient:
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def create_label(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
*,
|
||||
name: str,
|
||||
color: str,
|
||||
description: str = "",
|
||||
exclusive: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a repository label."""
|
||||
payload: dict[str, Any] = {
|
||||
"name": name,
|
||||
"color": color,
|
||||
"description": description,
|
||||
"exclusive": exclusive,
|
||||
}
|
||||
result = await self._request(
|
||||
"POST",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/labels",
|
||||
json_body=payload,
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(tool_name="create_label", result_status="pending")
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def add_labels(
|
||||
self,
|
||||
owner: str,
|
||||
|
||||
@@ -374,6 +374,24 @@ AVAILABLE_TOOLS: list[MCPTool] = [
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_label",
|
||||
"Create a repository label (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"color": {"type": "string", "description": "Hex color, e.g. #00aabb"},
|
||||
"description": {"type": "string", "default": ""},
|
||||
"exclusive": {"type": "boolean", "default": False},
|
||||
},
|
||||
"required": ["owner", "repo", "name", "color"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ from aegis_gitea_mcp.tools.write_tools import (
|
||||
assign_issue_tool,
|
||||
create_issue_comment_tool,
|
||||
create_issue_tool,
|
||||
create_label_tool,
|
||||
create_pr_comment_tool,
|
||||
update_issue_tool,
|
||||
)
|
||||
@@ -342,6 +343,7 @@ TOOL_HANDLERS: dict[str, ToolHandler] = {
|
||||
"create_pr_comment": create_pr_comment_tool,
|
||||
"add_labels": add_labels_tool,
|
||||
"assign_issue": assign_issue_tool,
|
||||
"create_label": create_label_tool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -220,6 +220,16 @@ class AssignIssueArgs(RepositoryArgs):
|
||||
assignees: list[str] = Field(..., min_length=1, max_length=20)
|
||||
|
||||
|
||||
class CreateLabelArgs(RepositoryArgs):
|
||||
"""Arguments for create_label."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=50)
|
||||
# Gitea requires a hex color; accept it with or without a leading '#'.
|
||||
color: str = Field(..., pattern=r"^#?[0-9A-Fa-f]{6}$")
|
||||
description: str = Field(default="", max_length=1000)
|
||||
exclusive: bool = Field(default=False)
|
||||
|
||||
|
||||
def extract_repository(arguments: dict[str, object]) -> str | None:
|
||||
"""Extract `owner/repo` from raw argument mapping.
|
||||
|
||||
|
||||
@@ -16,11 +16,41 @@ from aegis_gitea_mcp.tools.arguments import (
|
||||
AssignIssueArgs,
|
||||
CreateIssueArgs,
|
||||
CreateIssueCommentArgs,
|
||||
CreateLabelArgs,
|
||||
CreatePrCommentArgs,
|
||||
UpdateIssueArgs,
|
||||
)
|
||||
|
||||
|
||||
async def create_label_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Create a repository label in write mode."""
|
||||
parsed = CreateLabelArgs.model_validate(arguments)
|
||||
# Gitea expects the color with a leading '#'; normalize either form.
|
||||
color = parsed.color if parsed.color.startswith("#") else f"#{parsed.color}"
|
||||
try:
|
||||
label = await gitea.create_label(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
name=parsed.name,
|
||||
color=color,
|
||||
description=parsed.description,
|
||||
exclusive=parsed.exclusive,
|
||||
)
|
||||
return {
|
||||
"id": label.get("id", 0),
|
||||
"name": limit_text(str(label.get("name", ""))),
|
||||
"color": label.get("color", ""),
|
||||
"description": limit_text(str(label.get("description", ""))),
|
||||
"url": label.get("url", ""),
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
# Let auth/authz failures surface so the server returns actionable
|
||||
# re-authorization guidance instead of a generic internal error.
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to create label: {exc}") from exc
|
||||
|
||||
|
||||
async def create_issue_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Create a new issue in write mode."""
|
||||
parsed = CreateIssueArgs.model_validate(arguments)
|
||||
|
||||
Reference in New Issue
Block a user