generated from personal-projects/leo-claude-mktplace
Compare commits
2 Commits
694406941c
...
feat/3-iss
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ffc0f9ce4 | |||
| 38dd315dd5 |
@@ -9,7 +9,14 @@ from mcp.types import Tool, TextContent
|
|||||||
from . import __version__
|
from . import __version__
|
||||||
from .auth import AuthConfig
|
from .auth import AuthConfig
|
||||||
from .client import GiteaClient, GiteaClientError
|
from .client import GiteaClient, GiteaClientError
|
||||||
from .tools import get_issue_tools, handle_issue_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
|
# Global client instance
|
||||||
@@ -36,10 +43,12 @@ async def serve() -> None:
|
|||||||
"""List available MCP tools.
|
"""List available MCP tools.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: Available tools including issue operations.
|
list: Available tools including issue, label, and milestone operations.
|
||||||
"""
|
"""
|
||||||
# Get issue tools
|
# Get issue, label, and milestone tools
|
||||||
tools = get_issue_tools()
|
tools = get_issue_tools()
|
||||||
|
tools.extend(get_label_tools())
|
||||||
|
tools.extend(get_milestone_tools())
|
||||||
|
|
||||||
# Placeholder for future tools (PR tools, etc.)
|
# Placeholder for future tools (PR tools, etc.)
|
||||||
tools.extend([
|
tools.extend([
|
||||||
@@ -108,6 +117,18 @@ async def serve() -> None:
|
|||||||
):
|
):
|
||||||
return await handle_issue_tool(name, arguments, gitea_client)
|
return await handle_issue_tool(name, arguments, gitea_client)
|
||||||
|
|
||||||
|
# Handle label tools
|
||||||
|
if name.startswith("gitea_") and any(
|
||||||
|
name.endswith(suffix) for suffix in ["_labels", "_label"]
|
||||||
|
):
|
||||||
|
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
|
# Placeholder for other tools
|
||||||
return [
|
return [
|
||||||
TextContent(
|
TextContent(
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
"""Gitea MCP tools package."""
|
"""Gitea MCP tools package."""
|
||||||
|
|
||||||
from .issues import get_issue_tools, handle_issue_tool
|
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__ = [
|
__all__ = [
|
||||||
"get_issue_tools",
|
"get_issue_tools",
|
||||||
"handle_issue_tool",
|
"handle_issue_tool",
|
||||||
|
"get_label_tools",
|
||||||
|
"handle_label_tool",
|
||||||
|
"get_milestone_tools",
|
||||||
|
"handle_milestone_tool",
|
||||||
]
|
]
|
||||||
|
|||||||
170
src/gitea_mcp/tools/labels.py
Normal file
170
src/gitea_mcp/tools/labels.py
Normal file
@@ -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}",
|
||||||
|
)
|
||||||
|
]
|
||||||
190
src/gitea_mcp/tools/milestones.py
Normal file
190
src/gitea_mcp/tools/milestones.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""Gitea milestone operations tools for MCP server."""
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
|
||||||
|
from ..client import GiteaClient, GiteaClientError
|
||||||
|
|
||||||
|
|
||||||
|
def get_milestone_tools() -> list[Tool]:
|
||||||
|
"""Get list of milestone operation tools.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Tool]: List of MCP tools for milestone operations.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Tool(
|
||||||
|
name="gitea_list_milestones",
|
||||||
|
description="List milestones in a Gitea repository with optional state filter",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Repository owner (username or organization)",
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Repository name",
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by state: open, closed, or all (default: open)",
|
||||||
|
"enum": ["open", "closed", "all"],
|
||||||
|
"default": "open",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="gitea_create_milestone",
|
||||||
|
description="Create a new milestone in a Gitea repository",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Repository owner (username or organization)",
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Repository name",
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Milestone title",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Milestone description (optional)",
|
||||||
|
},
|
||||||
|
"due_on": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Due date in ISO 8601 format, e.g., '2024-12-31T23:59:59Z' (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "title"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_milestone_tool(
|
||||||
|
name: str, arguments: dict[str, Any], client: GiteaClient
|
||||||
|
) -> list[TextContent]:
|
||||||
|
"""Handle milestone tool calls.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Tool name.
|
||||||
|
arguments: Tool arguments.
|
||||||
|
client: Gitea API client instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[TextContent]: Tool response.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if name == "gitea_list_milestones":
|
||||||
|
return await _list_milestones(arguments, client)
|
||||||
|
elif name == "gitea_create_milestone":
|
||||||
|
return await _create_milestone(arguments, client)
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Unknown milestone tool: {name}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
except GiteaClientError as e:
|
||||||
|
return [
|
||||||
|
TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Error: {str(e)}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def _list_milestones(
|
||||||
|
arguments: dict[str, Any], client: GiteaClient
|
||||||
|
) -> list[TextContent]:
|
||||||
|
"""List milestones in a repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Tool arguments containing owner, repo, and optional state filter.
|
||||||
|
client: Gitea API client instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[TextContent]: List of milestones.
|
||||||
|
"""
|
||||||
|
owner = arguments["owner"]
|
||||||
|
repo = arguments["repo"]
|
||||||
|
state = arguments.get("state", "open")
|
||||||
|
|
||||||
|
params = {"state": state}
|
||||||
|
|
||||||
|
async with client:
|
||||||
|
milestones = await client.get(
|
||||||
|
f"/repos/{owner}/{repo}/milestones", params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format response
|
||||||
|
if not milestones:
|
||||||
|
return [
|
||||||
|
TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"No {state} milestones found in {owner}/{repo}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
result = f"Found {len(milestones)} {state} milestone(s) in {owner}/{repo}:\n\n"
|
||||||
|
for milestone in milestones:
|
||||||
|
result += f"{milestone.get('title', 'Untitled')}\n"
|
||||||
|
result += f" State: {milestone.get('state', 'unknown')}\n"
|
||||||
|
if milestone.get("description"):
|
||||||
|
result += f" Description: {milestone['description']}\n"
|
||||||
|
if milestone.get("due_on"):
|
||||||
|
result += f" Due: {milestone['due_on']}\n"
|
||||||
|
result += f" Open Issues: {milestone.get('open_issues', 0)}\n"
|
||||||
|
result += f" Closed Issues: {milestone.get('closed_issues', 0)}\n"
|
||||||
|
result += f" Created: {milestone.get('created_at', 'N/A')}\n\n"
|
||||||
|
|
||||||
|
return [TextContent(type="text", text=result)]
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_milestone(
|
||||||
|
arguments: dict[str, Any], client: GiteaClient
|
||||||
|
) -> list[TextContent]:
|
||||||
|
"""Create a new milestone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Tool arguments containing owner, repo, title, and optional fields.
|
||||||
|
client: Gitea API client instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[TextContent]: Created milestone details.
|
||||||
|
"""
|
||||||
|
owner = arguments["owner"]
|
||||||
|
repo = arguments["repo"]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": arguments["title"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if "description" in arguments:
|
||||||
|
data["description"] = arguments["description"]
|
||||||
|
|
||||||
|
if "due_on" in arguments:
|
||||||
|
data["due_on"] = arguments["due_on"]
|
||||||
|
|
||||||
|
async with client:
|
||||||
|
milestone = await client.post(
|
||||||
|
f"/repos/{owner}/{repo}/milestones", json=data
|
||||||
|
)
|
||||||
|
|
||||||
|
result = f"Created milestone: {milestone['title']}\n"
|
||||||
|
if milestone.get("description"):
|
||||||
|
result += f"Description: {milestone['description']}\n"
|
||||||
|
if milestone.get("due_on"):
|
||||||
|
result += f"Due: {milestone['due_on']}\n"
|
||||||
|
|
||||||
|
return [TextContent(type="text", text=result)]
|
||||||
Reference in New Issue
Block a user