diff --git a/src/gitea_mcp/server.py b/src/gitea_mcp/server.py index 0be445d..63f7755 100644 --- a/src/gitea_mcp/server.py +++ b/src/gitea_mcp/server.py @@ -9,7 +9,14 @@ from mcp.types import Tool, TextContent from . import __version__ from .auth import AuthConfig from .client import GiteaClient, GiteaClientError -from .tools import get_issue_tools, handle_issue_tool, get_label_tools, handle_label_tool +from .tools import ( + get_issue_tools, + handle_issue_tool, + get_label_tools, + handle_label_tool, + get_milestone_tools, + handle_milestone_tool, +) # Global client instance @@ -36,11 +43,12 @@ async def serve() -> None: """List available MCP tools. Returns: - list: Available tools including issue and label operations. + list: Available tools including issue, label, and milestone operations. """ - # Get issue and label tools + # Get issue, label, and milestone tools tools = get_issue_tools() tools.extend(get_label_tools()) + tools.extend(get_milestone_tools()) # Placeholder for future tools (PR tools, etc.) tools.extend([ @@ -115,6 +123,12 @@ async def serve() -> None: ): return await handle_label_tool(gitea_client, name, arguments) + # Handle milestone tools + if name.startswith("gitea_") and any( + name.endswith(suffix) for suffix in ["_milestones", "_milestone"] + ): + return await handle_milestone_tool(name, arguments, gitea_client) + # Placeholder for other tools return [ TextContent( diff --git a/src/gitea_mcp/tools/__init__.py b/src/gitea_mcp/tools/__init__.py index 26875c2..f071371 100644 --- a/src/gitea_mcp/tools/__init__.py +++ b/src/gitea_mcp/tools/__init__.py @@ -2,10 +2,13 @@ from .issues import get_issue_tools, handle_issue_tool from .labels import get_label_tools, handle_label_tool +from .milestones import get_milestone_tools, handle_milestone_tool __all__ = [ "get_issue_tools", "handle_issue_tool", "get_label_tools", "handle_label_tool", + "get_milestone_tools", + "handle_milestone_tool", ] diff --git a/src/gitea_mcp/tools/labels.py b/src/gitea_mcp/tools/labels.py new file mode 100644 index 0000000..0f8a286 --- /dev/null +++ b/src/gitea_mcp/tools/labels.py @@ -0,0 +1,170 @@ +"""Gitea label operations MCP tools.""" + +from mcp.types import Tool, TextContent + +from ..client import GiteaClient, GiteaClientError + + +def get_label_tools() -> list[Tool]: + """Get label operation tool definitions. + + Returns: + list: Tool definitions for label operations. + """ + return [ + Tool( + name="gitea_list_labels", + description="List all labels in a Gitea repository", + inputSchema={ + "type": "object", + "properties": { + "owner": { + "type": "string", + "description": "Repository owner (user or organization)", + }, + "repo": { + "type": "string", + "description": "Repository name", + }, + }, + "required": ["owner", "repo"], + }, + ), + Tool( + name="gitea_create_label", + description="Create a new label in a Gitea repository", + inputSchema={ + "type": "object", + "properties": { + "owner": { + "type": "string", + "description": "Repository owner (user or organization)", + }, + "repo": { + "type": "string", + "description": "Repository name", + }, + "name": { + "type": "string", + "description": "Label name", + }, + "color": { + "type": "string", + "description": "Label color (hex without #, e.g., 'ff0000' for red)", + }, + "description": { + "type": "string", + "description": "Label description (optional)", + }, + }, + "required": ["owner", "repo", "name", "color"], + }, + ), + ] + + +async def handle_label_tool( + client: GiteaClient, name: str, arguments: dict +) -> list[TextContent]: + """Handle label tool execution. + + Args: + client: Gitea client instance. + name: Tool name. + arguments: Tool arguments. + + Returns: + list: Tool response content. + """ + try: + async with client: + if name == "gitea_list_labels": + return await _list_labels(client, arguments) + elif name == "gitea_create_label": + return await _create_label(client, arguments) + else: + return [ + TextContent( + type="text", + text=f"Unknown label tool: {name}", + ) + ] + except GiteaClientError as e: + return [ + TextContent( + type="text", + text=f"Gitea API error: {e}", + ) + ] + + +async def _list_labels(client: GiteaClient, arguments: dict) -> list[TextContent]: + """List labels in a repository. + + Args: + client: Gitea client instance. + arguments: Tool arguments with owner and repo. + + Returns: + list: Label listing response. + """ + owner = arguments["owner"] + repo = arguments["repo"] + + labels = await client.get(f"/repos/{owner}/{repo}/labels") + + if not labels: + return [ + TextContent( + type="text", + text=f"No labels found in {owner}/{repo}", + ) + ] + + # Format labels for display + lines = [f"Labels in {owner}/{repo}:", ""] + for label in labels: + color = label.get("color", "") + desc = label.get("description", "") + desc_text = f" - {desc}" if desc else "" + lines.append(f" • {label['name']} (#{color}){desc_text}") + + return [ + TextContent( + type="text", + text="\n".join(lines), + ) + ] + + +async def _create_label(client: GiteaClient, arguments: dict) -> list[TextContent]: + """Create a new label. + + Args: + client: Gitea client instance. + arguments: Tool arguments with owner, repo, name, color, and optional description. + + Returns: + list: Creation response. + """ + owner = arguments["owner"] + repo = arguments["repo"] + name = arguments["name"] + color = arguments["color"].lstrip("#") # Remove # if present + description = arguments.get("description", "") + + payload = { + "name": name, + "color": color, + } + if description: + payload["description"] = description + + label = await client.post(f"/repos/{owner}/{repo}/labels", payload) + + return [ + TextContent( + type="text", + text=f"Created label '{label['name']}' (#{label['color']}) in {owner}/{repo}", + ) + ]