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:
2026-06-14 20:24:33 +02:00
parent e873d0325b
commit f0db219ee8
7 changed files with 125 additions and 0 deletions
+37
View File
@@ -22,6 +22,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,
)
@@ -97,6 +98,9 @@ class StubGitea:
async def assign_issue(self, owner, repo, index, assignees):
return {"assignees": [{"login": user} for user in assignees]}
async def create_label(self, owner, repo, *, name, color, description="", exclusive=False):
return {"id": 5, "name": name, "color": color, "description": description, "url": "u"}
class ErrorGitea(StubGitea):
"""Stub that raises backend errors for failure-mode coverage."""
@@ -169,9 +173,42 @@ async def test_extended_read_tools_failure_mode() -> None:
{"owner": "acme", "repo": "app", "issue_number": 1, "assignees": ["alice"]},
"assignees",
),
(
create_label_tool,
{"owner": "acme", "repo": "app", "name": "bug", "color": "#ff0000"},
"id",
),
],
)
async def test_write_tools_success(tool, args, expected_key):
"""Write tools should normalize successful backend responses."""
result = await tool(StubGitea(), args)
assert expected_key in result
@pytest.mark.asyncio
async def test_create_label_normalizes_color_without_hash() -> None:
"""A hex color without a leading '#' is normalized before hitting Gitea."""
captured: dict = {}
class CaptureStub(StubGitea):
async def create_label(self, owner, repo, *, name, color, description="", exclusive=False):
captured["color"] = color
return {"id": 7, "name": name, "color": color}
result = await create_label_tool(
CaptureStub(),
{"owner": "acme", "repo": "app", "name": "bug", "color": "ff0000"},
)
assert captured["color"] == "#ff0000"
assert result["id"] == 7
def test_create_label_args_reject_invalid_color() -> None:
"""Non-hex color values are rejected at the argument layer."""
import pydantic
from aegis_gitea_mcp.tools.arguments import CreateLabelArgs
with pytest.raises(pydantic.ValidationError):
CreateLabelArgs(owner="o", repo="r", name="bug", color="red")