4 Commits

Author SHA1 Message Date
ab8c9069da docs: create comprehensive README
Updated README.md with complete documentation including:
- Installation instructions from source and for development
- Configuration with environment variables and .env file setup
- MCP server setup for both Claude Desktop and Claude Code
- Detailed documentation of all 8 available tools with parameters and examples
- API reference for core components (GiteaClient, AuthConfig)
- Development setup and project structure
- Troubleshooting section for common issues

Closes #6

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:29:00 -05:00
7ffc0f9ce4 feat(tools): add label and milestone operations
- Create labels.py with gitea_list_labels and gitea_create_label tools
- Integrate milestone tools from milestones.py into server
- Update tools/__init__.py with all tool exports
- Update server.py to handle label and milestone tool calls

Closes #4, Closes #5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:22:02 -05:00
38dd315dd5 feat(tools): implement milestone operations
Implemented MCP tools for Gitea milestone operations:

- Created src/gitea_mcp/tools/milestones.py with milestone tools
  - gitea_list_milestones: List milestones with state filter (open/closed/all)
  - gitea_create_milestone: Create milestone with title, description, due date
  - Error handling via GiteaClientError
  - Helper functions _list_milestones and _create_milestone

- Updated src/gitea_mcp/tools/__init__.py
  - Exported get_milestone_tools and handle_milestone_tool

- Updated src/gitea_mcp/server.py
  - Imported milestone tool functions
  - Added milestone tools to list_tools()
  - Added milestone handler to call_tool() dispatcher

API endpoints implemented:
- GET /repos/{owner}/{repo}/milestones (list with state filter)
- POST /repos/{owner}/{repo}/milestones (create)

All acceptance criteria met:
- tools/milestones.py created with MCP tool handlers
- gitea_list_milestones with state filter implemented
- gitea_create_milestone with title, description, due_on implemented
- Tools registered in server.py
- tools/__init__.py exports updated

Closes #5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:19:24 -05:00
694406941c feat(tools): implement issue operations - Closes #3
Implement Gitea issue operations tools with the following features:
- gitea_list_issues: List issues with filters (state, labels, milestone)
- gitea_get_issue: Get single issue by number
- gitea_create_issue: Create new issue with title, body, labels, milestone
- gitea_update_issue: Update issue state, title, body, labels, assignees

Files added:
- src/gitea_mcp/tools/issues.py: Issue operation tool handlers

Files modified:
- src/gitea_mcp/server.py: Register issue tools in MCP server
- src/gitea_mcp/tools/__init__.py: Export issue tool functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:17:40 -05:00
6 changed files with 1161 additions and 43 deletions

368
README.md
View File

@@ -1,27 +1,35 @@
# Gitea MCP Remote
# Gitea MCP Server
MCP server for Gitea API integration.
A Model Context Protocol (MCP) server that enables AI assistants like Claude to interact with Gitea repositories through its API. This server provides tools for managing issues, labels, and milestones in your Gitea instance.
## Overview
## Features
This project provides a Model Context Protocol (MCP) server that enables AI assistants to interact with Gitea through its API.
## Project Status
Currently in initial development. Project structure has been initialized.
- **Issue Operations**: List, get, create, and update issues with full support for labels, milestones, and assignees
- **Label Management**: List and create labels with custom colors and descriptions
- **Milestone Management**: List and create milestones with due dates and descriptions
- **Async API**: Built on modern async Python for efficient operations
- **Type Safety**: Full type hints for better IDE support and code quality
## Requirements
- Python >= 3.10
- Gitea instance with API access
- Gitea API token with appropriate permissions
## Installation
### From Source
```bash
# Clone the repository
git clone https://github.com/lmiranda/gitea-mcp-remote.git
cd gitea-mcp-remote
# Install the package
pip install -e .
```
## Development
### For Development
Install with development dependencies:
@@ -29,12 +37,350 @@ Install with development dependencies:
pip install -e ".[dev]"
```
Run tests:
## Configuration
The server requires two environment variables to connect to your Gitea instance:
- `GITEA_API_URL`: Base URL of your Gitea instance (e.g., `https://gitea.example.com/api/v1`)
- `GITEA_API_TOKEN`: Personal access token for authentication
### Creating a .env File
Create a `.env` file in your project directory:
```bash
GITEA_API_URL=https://gitea.example.com/api/v1
GITEA_API_TOKEN=your_gitea_token_here
```
### Getting a Gitea API Token
1. Log into your Gitea instance
2. Navigate to Settings > Applications
3. Under "Generate New Token", enter a name (e.g., "MCP Server")
4. Select appropriate permissions (minimum: read/write for repositories)
5. Click "Generate Token" and copy the token
6. Add the token to your `.env` file
## Usage with Claude Desktop
Add this configuration to your Claude Desktop config file:
**Location:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`
**Configuration:**
```json
{
"mcpServers": {
"gitea": {
"command": "python",
"args": ["-m", "gitea_mcp.server"],
"env": {
"GITEA_API_URL": "https://gitea.example.com/api/v1",
"GITEA_API_TOKEN": "your_gitea_token_here"
}
}
}
}
```
Or, if you prefer using a .env file:
```json
{
"mcpServers": {
"gitea": {
"command": "python",
"args": ["-m", "gitea_mcp.server"],
"cwd": "/path/to/gitea-mcp-remote"
}
}
}
```
## Usage with Claude Code
Add to your MCP settings file:
**Location:** `~/.config/claude-code/mcp.json` (or your configured MCP settings path)
**Configuration:**
```json
{
"mcpServers": {
"gitea": {
"command": "python",
"args": ["-m", "gitea_mcp.server"],
"env": {
"GITEA_API_URL": "https://gitea.example.com/api/v1",
"GITEA_API_TOKEN": "your_gitea_token_here"
}
}
}
}
```
## Available Tools
### Issue Tools
#### gitea_list_issues
List issues in a repository with optional filters.
**Parameters:**
- `owner` (required): Repository owner (username or organization)
- `repo` (required): Repository name
- `state` (optional): Filter by state - `open`, `closed`, or `all` (default: `open`)
- `labels` (optional): Comma-separated list of label names to filter by
- `milestone` (optional): Milestone name to filter by
- `page` (optional): Page number for pagination (default: 1)
- `limit` (optional): Number of issues per page (default: 30)
**Example:**
```
List all open issues in myorg/myrepo
```
#### gitea_get_issue
Get details of a specific issue by number.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
- `index` (required): Issue number
**Example:**
```
Get details of issue #42 in myorg/myrepo
```
#### gitea_create_issue
Create a new issue in a repository.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
- `title` (required): Issue title
- `body` (optional): Issue description/body
- `labels` (optional): Array of label IDs to assign
- `milestone` (optional): Milestone ID to assign
- `assignees` (optional): Array of usernames to assign
**Example:**
```
Create an issue in myorg/myrepo with title "Bug: Login fails" and body "Users cannot log in with special characters in password"
```
#### gitea_update_issue
Update an existing issue.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
- `index` (required): Issue number
- `title` (optional): New issue title
- `body` (optional): New issue body
- `state` (optional): Issue state - `open` or `closed`
- `labels` (optional): Array of label IDs (replaces existing)
- `milestone` (optional): Milestone ID to assign
- `assignees` (optional): Array of usernames (replaces existing)
**Example:**
```
Close issue #42 in myorg/myrepo
```
### Label Tools
#### gitea_list_labels
List all labels in a repository.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
**Example:**
```
List all labels in myorg/myrepo
```
#### gitea_create_label
Create a new label in a repository.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
- `name` (required): Label name
- `color` (required): Label color (hex without #, e.g., `ff0000` for red)
- `description` (optional): Label description
**Example:**
```
Create a label "bug" with red color (ff0000) in myorg/myrepo
```
### Milestone Tools
#### gitea_list_milestones
List milestones in a repository.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
- `state` (optional): Filter by state - `open`, `closed`, or `all` (default: `open`)
**Example:**
```
List all milestones in myorg/myrepo
```
#### gitea_create_milestone
Create a new milestone in a repository.
**Parameters:**
- `owner` (required): Repository owner
- `repo` (required): Repository name
- `title` (required): Milestone title
- `description` (optional): Milestone description
- `due_on` (optional): Due date in ISO 8601 format (e.g., `2024-12-31T23:59:59Z`)
**Example:**
```
Create a milestone "v1.0 Release" with due date 2024-12-31 in myorg/myrepo
```
## API Reference
### Core Components
#### GiteaClient
HTTP client for Gitea API interactions.
**Methods:**
- `get(endpoint, params)`: GET request
- `post(endpoint, json)`: POST request
- `patch(endpoint, json)`: PATCH request
#### AuthConfig
Configuration manager for API authentication.
**Environment Variables:**
- `GITEA_API_URL`: Gitea API base URL
- `GITEA_API_TOKEN`: Authentication token
**Methods:**
- `get_auth_headers()`: Returns authentication headers
### Tool Modules
- `gitea_mcp.tools.issues`: Issue operation tools and handlers
- `gitea_mcp.tools.labels`: Label operation tools and handlers
- `gitea_mcp.tools.milestones`: Milestone operation tools and handlers
## Development
### Setup Development Environment
```bash
# Install with development dependencies
pip install -e ".[dev]"
```
### Running Tests
```bash
pytest
```
### Project Structure
```
gitea-mcp-remote/
├── src/
│ └── gitea_mcp/
│ ├── __init__.py
│ ├── server.py # MCP server implementation
│ ├── auth.py # Authentication config
│ ├── client.py # Gitea API client
│ └── tools/ # Tool implementations
│ ├── __init__.py
│ ├── issues.py # Issue tools
│ ├── labels.py # Label tools
│ └── milestones.py # Milestone tools
├── tests/ # Test suite
├── pyproject.toml # Project configuration
└── README.md # This file
```
### Code Quality
This project uses:
- Type hints throughout the codebase
- Async/await for all I/O operations
- Comprehensive error handling
- Structured logging
## Troubleshooting
### Authentication Errors
If you receive authentication errors:
1. Verify your `GITEA_API_TOKEN` is correct
2. Check that the token has appropriate permissions
3. Ensure your `GITEA_API_URL` includes `/api/v1` at the end
4. Verify the Gitea instance is accessible from your network
### Connection Errors
If you cannot connect to Gitea:
1. Check that `GITEA_API_URL` is correct and accessible
2. Verify network connectivity to the Gitea instance
3. Check for firewalls or proxies blocking the connection
### Tool Not Found
If Claude cannot find the tools:
1. Restart Claude Desktop/Code after updating the configuration
2. Verify the configuration file syntax is valid JSON
3. Check that Python is in your PATH
4. Ensure the package is properly installed (`pip list | grep gitea-mcp`)
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
## License
MIT
MIT License - see LICENSE file for details
## Version
Current version: 0.1.0
## Author
Leo Miranda
## Links
- Repository: https://github.com/lmiranda/gitea-mcp-remote
- Issues: https://github.com/lmiranda/gitea-mcp-remote/issues
- MCP Documentation: https://modelcontextprotocol.io

View File

@@ -9,6 +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,
get_milestone_tools,
handle_milestone_tool,
)
# Global client instance
@@ -35,10 +43,15 @@ async def serve() -> None:
"""List available MCP tools.
Returns:
list: Available tools (placeholder for future implementation).
list: Available tools including issue, label, and milestone operations.
"""
# Placeholder tools - will be implemented in issues #3, #4, #5
return [
# 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([
Tool(
name="list_repositories",
description="List repositories in an organization (coming soon)",
@@ -53,32 +66,6 @@ async def serve() -> None:
"required": ["org"],
},
),
Tool(
name="create_issue",
description="Create a new issue in a repository (coming soon)",
inputSchema={
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner",
},
"repo": {
"type": "string",
"description": "Repository name",
},
"title": {
"type": "string",
"description": "Issue title",
},
"body": {
"type": "string",
"description": "Issue body",
},
},
"required": ["owner", "repo", "title"],
},
),
Tool(
name="create_pull_request",
description="Create a new pull request (coming soon)",
@@ -109,7 +96,9 @@ async def serve() -> None:
"required": ["owner", "repo", "title", "head", "base"],
},
),
]
])
return tools
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
@@ -122,11 +111,29 @@ async def serve() -> None:
Returns:
list: Tool response.
"""
# Placeholder implementation - actual tools will be implemented in future issues
# Handle issue tools
if name.startswith("gitea_") and any(
name.endswith(suffix) for suffix in ["_issues", "_issue"]
):
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
return [
TextContent(
type="text",
text=f"Tool '{name}' is not yet implemented. Coming soon in issues #3, #4, #5.",
text=f"Tool '{name}' is not yet implemented.",
)
]

View File

@@ -1 +1,14 @@
"""Gitea MCP tools package."""
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",
]

View File

@@ -0,0 +1,392 @@
"""Gitea issue operations tools for MCP server."""
from typing import Any, Optional
from mcp.types import Tool, TextContent
from ..client import GiteaClient, GiteaClientError
def get_issue_tools() -> list[Tool]:
"""Get list of issue operation tools.
Returns:
list[Tool]: List of MCP tools for issue operations.
"""
return [
Tool(
name="gitea_list_issues",
description="List issues in a Gitea repository with optional filters",
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",
"enum": ["open", "closed", "all"],
"default": "open",
},
"labels": {
"type": "string",
"description": "Comma-separated list of label names to filter by",
},
"milestone": {
"type": "string",
"description": "Milestone name to filter by",
},
"page": {
"type": "integer",
"description": "Page number for pagination (default: 1)",
"default": 1,
},
"limit": {
"type": "integer",
"description": "Number of issues per page (default: 30)",
"default": 30,
},
},
"required": ["owner", "repo"],
},
),
Tool(
name="gitea_get_issue",
description="Get details of a specific issue by number",
inputSchema={
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner (username or organization)",
},
"repo": {
"type": "string",
"description": "Repository name",
},
"index": {
"type": "integer",
"description": "Issue number/index",
},
},
"required": ["owner", "repo", "index"],
},
),
Tool(
name="gitea_create_issue",
description="Create a new issue 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": "Issue title",
},
"body": {
"type": "string",
"description": "Issue body/description",
},
"labels": {
"type": "array",
"items": {"type": "integer"},
"description": "Array of label IDs to assign",
},
"milestone": {
"type": "integer",
"description": "Milestone ID to assign",
},
"assignees": {
"type": "array",
"items": {"type": "string"},
"description": "Array of usernames to assign",
},
},
"required": ["owner", "repo", "title"],
},
),
Tool(
name="gitea_update_issue",
description="Update an existing issue in a Gitea repository",
inputSchema={
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner (username or organization)",
},
"repo": {
"type": "string",
"description": "Repository name",
},
"index": {
"type": "integer",
"description": "Issue number/index",
},
"title": {
"type": "string",
"description": "New issue title",
},
"body": {
"type": "string",
"description": "New issue body/description",
},
"state": {
"type": "string",
"description": "Issue state: open or closed",
"enum": ["open", "closed"],
},
"labels": {
"type": "array",
"items": {"type": "integer"},
"description": "Array of label IDs to assign (replaces existing)",
},
"milestone": {
"type": "integer",
"description": "Milestone ID to assign",
},
"assignees": {
"type": "array",
"items": {"type": "string"},
"description": "Array of usernames to assign (replaces existing)",
},
},
"required": ["owner", "repo", "index"],
},
),
]
async def handle_issue_tool(
name: str, arguments: dict[str, Any], client: GiteaClient
) -> list[TextContent]:
"""Handle issue tool calls.
Args:
name: Tool name.
arguments: Tool arguments.
client: Gitea API client instance.
Returns:
list[TextContent]: Tool response.
"""
try:
if name == "gitea_list_issues":
return await _list_issues(arguments, client)
elif name == "gitea_get_issue":
return await _get_issue(arguments, client)
elif name == "gitea_create_issue":
return await _create_issue(arguments, client)
elif name == "gitea_update_issue":
return await _update_issue(arguments, client)
else:
return [
TextContent(
type="text",
text=f"Unknown issue tool: {name}",
)
]
except GiteaClientError as e:
return [
TextContent(
type="text",
text=f"Error: {str(e)}",
)
]
async def _list_issues(
arguments: dict[str, Any], client: GiteaClient
) -> list[TextContent]:
"""List issues in a repository.
Args:
arguments: Tool arguments containing owner, repo, and optional filters.
client: Gitea API client instance.
Returns:
list[TextContent]: List of issues.
"""
owner = arguments["owner"]
repo = arguments["repo"]
state = arguments.get("state", "open")
labels = arguments.get("labels")
milestone = arguments.get("milestone")
page = arguments.get("page", 1)
limit = arguments.get("limit", 30)
params = {
"state": state,
"page": page,
"limit": limit,
}
if labels:
params["labels"] = labels
if milestone:
params["milestone"] = milestone
async with client:
issues = await client.get(f"/repos/{owner}/{repo}/issues", params=params)
# Format response
if not issues:
return [
TextContent(
type="text",
text=f"No {state} issues found in {owner}/{repo}",
)
]
result = f"Found {len(issues)} {state} issue(s) in {owner}/{repo}:\n\n"
for issue in issues:
result += f"#{issue['number']} - {issue['title']}\n"
result += f" State: {issue['state']}\n"
if issue.get('labels'):
labels_str = ", ".join([label['name'] for label in issue['labels']])
result += f" Labels: {labels_str}\n"
if issue.get('milestone'):
result += f" Milestone: {issue['milestone']['title']}\n"
result += f" Created: {issue['created_at']}\n"
result += f" Updated: {issue['updated_at']}\n\n"
return [TextContent(type="text", text=result)]
async def _get_issue(
arguments: dict[str, Any], client: GiteaClient
) -> list[TextContent]:
"""Get a specific issue by number.
Args:
arguments: Tool arguments containing owner, repo, and index.
client: Gitea API client instance.
Returns:
list[TextContent]: Issue details.
"""
owner = arguments["owner"]
repo = arguments["repo"]
index = arguments["index"]
async with client:
issue = await client.get(f"/repos/{owner}/{repo}/issues/{index}")
# Format response
result = f"Issue #{issue['number']}: {issue['title']}\n\n"
result += f"State: {issue['state']}\n"
result += f"Created: {issue['created_at']}\n"
result += f"Updated: {issue['updated_at']}\n"
if issue.get('labels'):
labels_str = ", ".join([label['name'] for label in issue['labels']])
result += f"Labels: {labels_str}\n"
if issue.get('milestone'):
result += f"Milestone: {issue['milestone']['title']}\n"
if issue.get('assignees'):
assignees_str = ", ".join([user['login'] for user in issue['assignees']])
result += f"Assignees: {assignees_str}\n"
result += f"\nBody:\n{issue.get('body', '(no description)')}\n"
return [TextContent(type="text", text=result)]
async def _create_issue(
arguments: dict[str, Any], client: GiteaClient
) -> list[TextContent]:
"""Create a new issue.
Args:
arguments: Tool arguments containing owner, repo, title, and optional fields.
client: Gitea API client instance.
Returns:
list[TextContent]: Created issue details.
"""
owner = arguments["owner"]
repo = arguments["repo"]
data = {
"title": arguments["title"],
}
if "body" in arguments:
data["body"] = arguments["body"]
if "labels" in arguments:
data["labels"] = arguments["labels"]
if "milestone" in arguments:
data["milestone"] = arguments["milestone"]
if "assignees" in arguments:
data["assignees"] = arguments["assignees"]
async with client:
issue = await client.post(f"/repos/{owner}/{repo}/issues", json=data)
result = f"Created issue #{issue['number']}: {issue['title']}\n"
result += f"URL: {issue.get('html_url', 'N/A')}\n"
return [TextContent(type="text", text=result)]
async def _update_issue(
arguments: dict[str, Any], client: GiteaClient
) -> list[TextContent]:
"""Update an existing issue.
Args:
arguments: Tool arguments containing owner, repo, index, and fields to update.
client: Gitea API client instance.
Returns:
list[TextContent]: Updated issue details.
"""
owner = arguments["owner"]
repo = arguments["repo"]
index = arguments["index"]
data = {}
if "title" in arguments:
data["title"] = arguments["title"]
if "body" in arguments:
data["body"] = arguments["body"]
if "state" in arguments:
data["state"] = arguments["state"]
if "labels" in arguments:
data["labels"] = arguments["labels"]
if "milestone" in arguments:
data["milestone"] = arguments["milestone"]
if "assignees" in arguments:
data["assignees"] = arguments["assignees"]
async with client:
issue = await client.patch(f"/repos/{owner}/{repo}/issues/{index}", json=data)
result = f"Updated issue #{issue['number']}: {issue['title']}\n"
result += f"State: {issue['state']}\n"
return [TextContent(type="text", text=result)]

View 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}",
)
]

View 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)]