diff --git a/mcp-servers/gitea/mcp_server/__init__.py b/mcp-servers/gitea/mcp_server/__init__.py index e69de29..c59296c 100644 --- a/mcp-servers/gitea/mcp_server/__init__.py +++ b/mcp-servers/gitea/mcp_server/__init__.py @@ -0,0 +1,30 @@ +""" +Gitea MCP Server package. + +Provides MCP tools for Gitea integration via JSON-RPC 2.0. + +For external consumers (e.g., HTTP transport), use: + from mcp_server import get_tool_definitions, create_tool_dispatcher, GiteaClient + + # Get tool schemas + tools = get_tool_definitions() + + # Create dispatcher bound to a client + client = GiteaClient() + dispatch = create_tool_dispatcher(client) + result = await dispatch("list_issues", {"state": "open"}) +""" + +__version__ = "1.0.0" + +from .tool_registry import get_tool_definitions, create_tool_dispatcher +from .gitea_client import GiteaClient +from .config import GiteaConfig + +__all__ = [ + "__version__", + "get_tool_definitions", + "create_tool_dispatcher", + "GiteaClient", + "GiteaConfig", +] diff --git a/mcp-servers/gitea/mcp_server/server.py b/mcp-servers/gitea/mcp_server/server.py index 7451605..2d07417 100644 --- a/mcp-servers/gitea/mcp_server/server.py +++ b/mcp-servers/gitea/mcp_server/server.py @@ -5,19 +5,13 @@ Provides Gitea tools to Claude Code via JSON-RPC 2.0 over stdio. """ import asyncio import logging -import json from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from .config import GiteaConfig from .gitea_client import GiteaClient -from .tools.issues import IssueTools -from .tools.labels import LabelTools -from .tools.wiki import WikiTools -from .tools.milestones import MilestoneTools -from .tools.dependencies import DependencyTools -from .tools.pull_requests import PullRequestTools +from .tool_registry import get_tool_definitions, create_tool_dispatcher # Suppress noisy MCP validation warnings on stderr logging.basicConfig(level=logging.INFO) @@ -26,44 +20,6 @@ logging.getLogger("mcp").setLevel(logging.ERROR) logger = logging.getLogger(__name__) -def _coerce_types(arguments: dict) -> dict: - """ - Coerce argument types to handle MCP serialization quirks. - - MCP sometimes passes integers as strings and arrays as JSON strings. - This function normalizes them to the expected Python types. - """ - coerced = {} - for key, value in arguments.items(): - if value is None: - coerced[key] = value - continue - - # Coerce integer fields - int_fields = {'issue_number', 'milestone_id', 'pr_number', 'depends_on', 'milestone', 'limit'} - if key in int_fields and isinstance(value, str): - try: - coerced[key] = int(value) - continue - except ValueError: - pass - - # Coerce array fields that might be JSON strings - array_fields = {'labels', 'tags', 'issue_numbers', 'comments'} - if key in array_fields and isinstance(value, str): - try: - parsed = json.loads(value) - if isinstance(parsed, list): - coerced[key] = parsed - continue - except json.JSONDecodeError: - pass - - coerced[key] = value - - return coerced - - class GiteaMCPServer: """MCP Server for Gitea integration""" @@ -71,12 +27,7 @@ class GiteaMCPServer: self.server = Server("gitea-mcp") self.config = None self.client = None - self.issue_tools = None - self.label_tools = None - self.wiki_tools = None - self.milestone_tools = None - self.dependency_tools = None - self.pr_tools = None + self._dispatcher = None async def initialize(self): """ @@ -90,12 +41,7 @@ class GiteaMCPServer: self.config = config_loader.load() self.client = GiteaClient() - self.issue_tools = IssueTools(self.client) - self.label_tools = LabelTools(self.client) - self.wiki_tools = WikiTools(self.client) - self.milestone_tools = MilestoneTools(self.client) - self.dependency_tools = DependencyTools(self.client) - self.pr_tools = PullRequestTools(self.client) + self._dispatcher = create_tool_dispatcher(self.client) logger.info(f"Gitea MCP Server initialized in {self.config['mode']} mode") except Exception as e: @@ -108,838 +54,7 @@ class GiteaMCPServer: @self.server.list_tools() async def list_tools() -> list[Tool]: """Return list of available tools""" - return [ - Tool( - name="list_issues", - description="List issues from Gitea repository", - inputSchema={ - "type": "object", - "properties": { - "state": { - "type": "string", - "enum": ["open", "closed", "all"], - "default": "open", - "description": "Issue state filter" - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Filter by labels" - }, - "milestone": { - "type": "string", - "description": "Filter by milestone title (exact match)" - }, - "repo": { - "type": "string", - "description": "Repository name (for PMO mode)" - } - } - } - ), - Tool( - name="get_issue", - description="Get specific issue details", - inputSchema={ - "type": "object", - "properties": { - "issue_number": { - "type": ["integer", "string"], - "description": "Issue number" - }, - "repo": { - "type": "string", - "description": "Repository name (for PMO mode)" - } - }, - "required": ["issue_number"] - } - ), - Tool( - name="create_issue", - description="Create a new issue in Gitea", - inputSchema={ - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Issue title" - }, - "body": { - "type": "string", - "description": "Issue description" - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "List of label names" - }, - "repo": { - "type": "string", - "description": "Repository name (for PMO mode)" - } - }, - "required": ["title", "body"] - } - ), - Tool( - name="update_issue", - description="Update existing issue", - inputSchema={ - "type": "object", - "properties": { - "issue_number": { - "type": ["integer", "string"], - "description": "Issue number" - }, - "title": { - "type": "string", - "description": "New title" - }, - "body": { - "type": "string", - "description": "New body" - }, - "state": { - "type": "string", - "enum": ["open", "closed"], - "description": "New state" - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "New labels" - }, - "milestone": { - "type": ["integer", "string"], - "description": "Milestone ID to assign" - }, - "repo": { - "type": "string", - "description": "Repository name (for PMO mode)" - } - }, - "required": ["issue_number"] - } - ), - Tool( - name="add_comment", - description="Add comment to issue", - inputSchema={ - "type": "object", - "properties": { - "issue_number": { - "type": ["integer", "string"], - "description": "Issue number" - }, - "comment": { - "type": "string", - "description": "Comment text" - }, - "repo": { - "type": "string", - "description": "Repository name (for PMO mode)" - } - }, - "required": ["issue_number", "comment"] - } - ), - Tool( - name="get_labels", - description="Get all available labels (org + repo)", - inputSchema={ - "type": "object", - "properties": { - "repo": { - "type": "string", - "description": "Repository name (for PMO mode)" - } - } - } - ), - Tool( - name="suggest_labels", - description="Analyze context and suggest appropriate labels", - inputSchema={ - "type": "object", - "properties": { - "context": { - "type": "string", - "description": "Issue title + description or sprint context" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["context"] - } - ), - Tool( - name="aggregate_issues", - description="Fetch issues across all repositories (PMO mode)", - inputSchema={ - "type": "object", - "properties": { - "org": { - "type": "string", - "description": "Organization name (e.g. 'bandit')" - }, - "state": { - "type": "string", - "enum": ["open", "closed", "all"], - "default": "open", - "description": "Issue state filter" - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Filter by labels" - } - }, - "required": ["org"] - } - ), - # Wiki Tools (Lessons Learned) - Tool( - name="list_wiki_pages", - description="List all wiki pages in repository", - inputSchema={ - "type": "object", - "properties": { - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - } - } - ), - Tool( - name="get_wiki_page", - description="Get a specific wiki page by name", - inputSchema={ - "type": "object", - "properties": { - "page_name": { - "type": "string", - "description": "Wiki page name/path" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["page_name"] - } - ), - Tool( - name="create_wiki_page", - description="Create a new wiki page", - inputSchema={ - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Page title/name" - }, - "content": { - "type": "string", - "description": "Page content (markdown)" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["title", "content"] - } - ), - Tool( - name="update_wiki_page", - description="Update an existing wiki page", - inputSchema={ - "type": "object", - "properties": { - "page_name": { - "type": "string", - "description": "Wiki page name/path" - }, - "content": { - "type": "string", - "description": "New page content (markdown)" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["page_name", "content"] - } - ), - Tool( - name="create_lesson", - description="Create a lessons learned entry in the wiki", - inputSchema={ - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Lesson title (e.g., 'Sprint 16 - Prevent Infinite Loops')" - }, - "content": { - "type": "string", - "description": "Lesson content (markdown with context, problem, solution, prevention)" - }, - "tags": { - "type": "array", - "items": {"type": "string"}, - "description": "Tags for categorization" - }, - "category": { - "type": "string", - "default": "sprints", - "description": "Category (sprints, patterns, architecture, etc.)" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["title", "content", "tags"] - } - ), - Tool( - name="search_lessons", - description="Search lessons learned from previous sprints", - inputSchema={ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query (optional)" - }, - "tags": { - "type": "array", - "items": {"type": "string"}, - "description": "Tags to filter by (optional)" - }, - "limit": { - "type": ["integer", "string"], - "default": 20, - "description": "Maximum results" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - } - } - ), - Tool( - name="allocate_rfc_number", - description="Allocate the next available RFC number by scanning existing RFC wiki pages", - inputSchema={ - "type": "object", - "properties": { - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - } - } - ), - # Milestone Tools - Tool( - name="list_milestones", - description="List all milestones in repository", - inputSchema={ - "type": "object", - "properties": { - "state": { - "type": "string", - "enum": ["open", "closed", "all"], - "default": "open", - "description": "Milestone state filter" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - } - } - ), - Tool( - name="get_milestone", - description="Get a specific milestone by ID", - inputSchema={ - "type": "object", - "properties": { - "milestone_id": { - "type": ["integer", "string"], - "description": "Milestone ID" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["milestone_id"] - } - ), - Tool( - name="create_milestone", - description="Create a new milestone", - inputSchema={ - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Milestone title" - }, - "description": { - "type": "string", - "description": "Milestone description" - }, - "due_on": { - "type": "string", - "description": "Due date (ISO 8601 format)" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["title"] - } - ), - Tool( - name="update_milestone", - description="Update an existing milestone", - inputSchema={ - "type": "object", - "properties": { - "milestone_id": { - "type": ["integer", "string"], - "description": "Milestone ID" - }, - "title": { - "type": "string", - "description": "New title" - }, - "description": { - "type": "string", - "description": "New description" - }, - "state": { - "type": "string", - "enum": ["open", "closed"], - "description": "New state" - }, - "due_on": { - "type": "string", - "description": "New due date" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["milestone_id"] - } - ), - Tool( - name="delete_milestone", - description="Delete a milestone", - inputSchema={ - "type": "object", - "properties": { - "milestone_id": { - "type": ["integer", "string"], - "description": "Milestone ID" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["milestone_id"] - } - ), - # Dependency Tools - Tool( - name="list_issue_dependencies", - description="List all dependencies for an issue (issues that block this one)", - inputSchema={ - "type": "object", - "properties": { - "issue_number": { - "type": ["integer", "string"], - "description": "Issue number" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["issue_number"] - } - ), - Tool( - name="create_issue_dependency", - description="Create a dependency (issue depends on another issue)", - inputSchema={ - "type": "object", - "properties": { - "issue_number": { - "type": ["integer", "string"], - "description": "Issue that will depend on another" - }, - "depends_on": { - "type": ["integer", "string"], - "description": "Issue that blocks issue_number" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["issue_number", "depends_on"] - } - ), - Tool( - name="remove_issue_dependency", - description="Remove a dependency between issues", - inputSchema={ - "type": "object", - "properties": { - "issue_number": { - "type": ["integer", "string"], - "description": "Issue that depends on another" - }, - "depends_on": { - "type": ["integer", "string"], - "description": "Issue being depended on" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["issue_number", "depends_on"] - } - ), - Tool( - name="get_execution_order", - description="Get parallelizable execution order for issues based on dependencies", - inputSchema={ - "type": "object", - "properties": { - "issue_numbers": { - "type": "array", - "items": {"type": "integer"}, - "description": "List of issue numbers to analyze" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["issue_numbers"] - } - ), - # Validation Tools - Tool( - name="validate_repo_org", - description="Check if repository belongs to an organization", - inputSchema={ - "type": "object", - "properties": { - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - } - } - ), - Tool( - name="get_branch_protection", - description="Get branch protection rules", - inputSchema={ - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "Branch name" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["branch"] - } - ), - Tool( - name="create_label", - description="Create a new label in the repository (for repo-specific labels like Component/*, Tech/*)", - inputSchema={ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Label name (e.g., 'Component/Backend', 'Tech/Python')" - }, - "color": { - "type": "string", - "description": "Label color (hex code)" - }, - "description": { - "type": "string", - "description": "Label description" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["name", "color"] - } - ), - Tool( - name="create_org_label", - description="Create a new label at organization level (for workflow labels like Type/*, Priority/*, Complexity/*, Effort/*)", - inputSchema={ - "type": "object", - "properties": { - "org": { - "type": "string", - "description": "Organization name" - }, - "name": { - "type": "string", - "description": "Label name (e.g., 'Type/Bug', 'Priority/High')" - }, - "color": { - "type": "string", - "description": "Label color (hex code)" - }, - "description": { - "type": "string", - "description": "Label description" - } - }, - "required": ["org", "name", "color"] - } - ), - Tool( - name="create_label_smart", - description="Create a label at the appropriate level (org or repo) based on category. Org: Type/*, Priority/*, Complexity/*, Effort/*, Risk/*, Source/*, Agent/*. Repo: Component/*, Tech/*", - inputSchema={ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Label name (e.g., 'Type/Bug', 'Component/Backend')" - }, - "color": { - "type": "string", - "description": "Label color (hex code)" - }, - "description": { - "type": "string", - "description": "Label description" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["name", "color"] - } - ), - # Pull Request Tools - Tool( - name="list_pull_requests", - description="List pull requests from repository", - inputSchema={ - "type": "object", - "properties": { - "state": { - "type": "string", - "enum": ["open", "closed", "all"], - "default": "open", - "description": "PR state filter" - }, - "sort": { - "type": "string", - "enum": ["oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"], - "default": "recentupdate", - "description": "Sort order" - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Filter by labels" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - } - } - ), - Tool( - name="get_pull_request", - description="Get specific pull request details", - inputSchema={ - "type": "object", - "properties": { - "pr_number": { - "type": ["integer", "string"], - "description": "Pull request number" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["pr_number"] - } - ), - Tool( - name="get_pr_diff", - description="Get the diff for a pull request", - inputSchema={ - "type": "object", - "properties": { - "pr_number": { - "type": ["integer", "string"], - "description": "Pull request number" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["pr_number"] - } - ), - Tool( - name="get_pr_comments", - description="Get comments on a pull request", - inputSchema={ - "type": "object", - "properties": { - "pr_number": { - "type": ["integer", "string"], - "description": "Pull request number" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["pr_number"] - } - ), - Tool( - name="create_pr_review", - description="Create a review on a pull request (approve, request changes, or comment)", - inputSchema={ - "type": "object", - "properties": { - "pr_number": { - "type": ["integer", "string"], - "description": "Pull request number" - }, - "body": { - "type": "string", - "description": "Review body/summary" - }, - "event": { - "type": "string", - "enum": ["APPROVE", "REQUEST_CHANGES", "COMMENT"], - "default": "COMMENT", - "description": "Review action" - }, - "comments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "position": {"type": ["integer", "string"]}, - "body": {"type": "string"} - } - }, - "description": "Optional inline comments" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["pr_number", "body"] - } - ), - Tool( - name="add_pr_comment", - description="Add a general comment to a pull request", - inputSchema={ - "type": "object", - "properties": { - "pr_number": { - "type": ["integer", "string"], - "description": "Pull request number" - }, - "body": { - "type": "string", - "description": "Comment text" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["pr_number", "body"] - } - ), - Tool( - name="create_pull_request", - description="Create a new pull request", - inputSchema={ - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "PR title" - }, - "body": { - "type": "string", - "description": "PR description/body" - }, - "head": { - "type": "string", - "description": "Source branch name (the branch with changes)" - }, - "base": { - "type": "string", - "description": "Target branch name (the branch to merge into)" - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of label names" - }, - "repo": { - "type": "string", - "description": "Repository name (owner/repo format)" - } - }, - "required": ["title", "body", "head", "base"] - } - ) - ] + return get_tool_definitions() @self.server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: @@ -953,129 +68,7 @@ class GiteaMCPServer: Returns: List of TextContent with results """ - try: - # Coerce types to handle MCP serialization quirks - arguments = _coerce_types(arguments) - - # Route to appropriate tool handler - if name == "list_issues": - result = await self.issue_tools.list_issues(**arguments) - elif name == "get_issue": - result = await self.issue_tools.get_issue(**arguments) - elif name == "create_issue": - result = await self.issue_tools.create_issue(**arguments) - elif name == "update_issue": - result = await self.issue_tools.update_issue(**arguments) - elif name == "add_comment": - result = await self.issue_tools.add_comment(**arguments) - elif name == "get_labels": - result = await self.label_tools.get_labels(**arguments) - elif name == "suggest_labels": - result = await self.label_tools.suggest_labels(**arguments) - elif name == "aggregate_issues": - result = await self.issue_tools.aggregate_issues(**arguments) - # Wiki tools - elif name == "list_wiki_pages": - result = await self.wiki_tools.list_wiki_pages(**arguments) - elif name == "get_wiki_page": - result = await self.wiki_tools.get_wiki_page(**arguments) - elif name == "create_wiki_page": - result = await self.wiki_tools.create_wiki_page(**arguments) - elif name == "update_wiki_page": - result = await self.wiki_tools.update_wiki_page(**arguments) - elif name == "create_lesson": - result = await self.wiki_tools.create_lesson(**arguments) - elif name == "search_lessons": - tags = arguments.get('tags') - result = await self.wiki_tools.search_lessons( - query=arguments.get('query'), - tags=tags, - limit=arguments.get('limit', 20), - repo=arguments.get('repo') - ) - elif name == "allocate_rfc_number": - result = await self.wiki_tools.allocate_rfc_number( - repo=arguments.get('repo') - ) - # Milestone tools - elif name == "list_milestones": - result = await self.milestone_tools.list_milestones(**arguments) - elif name == "get_milestone": - result = await self.milestone_tools.get_milestone(**arguments) - elif name == "create_milestone": - result = await self.milestone_tools.create_milestone(**arguments) - elif name == "update_milestone": - result = await self.milestone_tools.update_milestone(**arguments) - elif name == "delete_milestone": - result = await self.milestone_tools.delete_milestone(**arguments) - # Dependency tools - elif name == "list_issue_dependencies": - result = await self.dependency_tools.list_issue_dependencies(**arguments) - elif name == "create_issue_dependency": - result = await self.dependency_tools.create_issue_dependency(**arguments) - elif name == "remove_issue_dependency": - result = await self.dependency_tools.remove_issue_dependency(**arguments) - elif name == "get_execution_order": - result = await self.dependency_tools.get_execution_order(**arguments) - # Validation tools - elif name == "validate_repo_org": - is_org = self.client.is_org_repo(arguments.get('repo')) - result = {'is_organization': is_org} - elif name == "get_branch_protection": - result = self.client.get_branch_protection( - arguments['branch'], - arguments.get('repo') - ) - elif name == "create_label": - result = self.client.create_label( - arguments['name'], - arguments['color'], - arguments.get('description'), - arguments.get('repo') - ) - elif name == "create_org_label": - result = self.client.create_org_label( - arguments['org'], - arguments['name'], - arguments['color'], - arguments.get('description') - ) - elif name == "create_label_smart": - result = await self.label_tools.create_label_smart( - arguments['name'], - arguments['color'], - arguments.get('description'), - arguments.get('repo') - ) - # Pull Request tools - elif name == "list_pull_requests": - result = await self.pr_tools.list_pull_requests(**arguments) - elif name == "get_pull_request": - result = await self.pr_tools.get_pull_request(**arguments) - elif name == "get_pr_diff": - result = await self.pr_tools.get_pr_diff(**arguments) - elif name == "get_pr_comments": - result = await self.pr_tools.get_pr_comments(**arguments) - elif name == "create_pr_review": - result = await self.pr_tools.create_pr_review(**arguments) - elif name == "add_pr_comment": - result = await self.pr_tools.add_pr_comment(**arguments) - elif name == "create_pull_request": - result = await self.pr_tools.create_pull_request(**arguments) - else: - raise ValueError(f"Unknown tool: {name}") - - return [TextContent( - type="text", - text=json.dumps(result, indent=2) - )] - - except Exception as e: - logger.error(f"Tool {name} failed: {e}") - return [TextContent( - type="text", - text=f"Error: {str(e)}" - )] + return await self._dispatcher(name, arguments) async def run(self): """Run the MCP server""" diff --git a/mcp-servers/gitea/mcp_server/tool_registry.py b/mcp-servers/gitea/mcp_server/tool_registry.py new file mode 100644 index 0000000..1303b26 --- /dev/null +++ b/mcp-servers/gitea/mcp_server/tool_registry.py @@ -0,0 +1,1098 @@ +""" +Tool registry for Gitea MCP Server. + +Provides transport-agnostic tool definitions and dispatch logic that can be +used by both the stdio server and external consumers (e.g., HTTP transport). + +Usage: + from mcp_server.tool_registry import get_tool_definitions, create_tool_dispatcher + + # Get all tool schemas + tools = get_tool_definitions() + + # Get filtered tool schemas + tools = get_tool_definitions(tool_filter=["list_issues", "get_issue"]) + + # Create dispatcher + dispatch = create_tool_dispatcher(client) + result = await dispatch("list_issues", {"state": "open"}) +""" +import json +import logging +from typing import Callable, Awaitable, Optional + +from mcp.types import Tool, TextContent + +from .gitea_client import GiteaClient +from .tools.issues import IssueTools +from .tools.labels import LabelTools +from .tools.wiki import WikiTools +from .tools.milestones import MilestoneTools +from .tools.dependencies import DependencyTools +from .tools.pull_requests import PullRequestTools + +logger = logging.getLogger(__name__) + + +def _coerce_types(arguments: dict) -> dict: + """ + Coerce argument types to handle MCP serialization quirks. + + MCP sometimes passes integers as strings and arrays as JSON strings. + This function normalizes them to the expected Python types. + """ + coerced = {} + for key, value in arguments.items(): + if value is None: + coerced[key] = value + continue + + # Coerce integer fields + int_fields = {'issue_number', 'milestone_id', 'pr_number', 'depends_on', 'milestone', 'limit'} + if key in int_fields and isinstance(value, str): + try: + coerced[key] = int(value) + continue + except ValueError: + pass + + # Coerce array fields that might be JSON strings + array_fields = {'labels', 'tags', 'issue_numbers', 'comments'} + if key in array_fields and isinstance(value, str): + try: + parsed = json.loads(value) + if isinstance(parsed, list): + coerced[key] = parsed + continue + except json.JSONDecodeError: + pass + + coerced[key] = value + + return coerced + + +def _get_all_tool_definitions() -> list[Tool]: + """ + Return the complete list of Tool definitions. + + This is the single source of truth for all tool schemas. + """ + return [ + Tool( + name="list_issues", + description="List issues from Gitea repository", + inputSchema={ + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": ["open", "closed", "all"], + "default": "open", + "description": "Issue state filter" + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Filter by labels" + }, + "milestone": { + "type": "string", + "description": "Filter by milestone title (exact match)" + }, + "repo": { + "type": "string", + "description": "Repository name (for PMO mode)" + } + } + } + ), + Tool( + name="get_issue", + description="Get specific issue details", + inputSchema={ + "type": "object", + "properties": { + "issue_number": { + "type": ["integer", "string"], + "description": "Issue number" + }, + "repo": { + "type": "string", + "description": "Repository name (for PMO mode)" + } + }, + "required": ["issue_number"] + } + ), + Tool( + name="create_issue", + description="Create a new issue in Gitea", + inputSchema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Issue title" + }, + "body": { + "type": "string", + "description": "Issue description" + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "List of label names" + }, + "repo": { + "type": "string", + "description": "Repository name (for PMO mode)" + } + }, + "required": ["title", "body"] + } + ), + Tool( + name="update_issue", + description="Update existing issue", + inputSchema={ + "type": "object", + "properties": { + "issue_number": { + "type": ["integer", "string"], + "description": "Issue number" + }, + "title": { + "type": "string", + "description": "New title" + }, + "body": { + "type": "string", + "description": "New body" + }, + "state": { + "type": "string", + "enum": ["open", "closed"], + "description": "New state" + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "New labels" + }, + "milestone": { + "type": ["integer", "string"], + "description": "Milestone ID to assign" + }, + "repo": { + "type": "string", + "description": "Repository name (for PMO mode)" + } + }, + "required": ["issue_number"] + } + ), + Tool( + name="add_comment", + description="Add comment to issue", + inputSchema={ + "type": "object", + "properties": { + "issue_number": { + "type": ["integer", "string"], + "description": "Issue number" + }, + "comment": { + "type": "string", + "description": "Comment text" + }, + "repo": { + "type": "string", + "description": "Repository name (for PMO mode)" + } + }, + "required": ["issue_number", "comment"] + } + ), + Tool( + name="get_labels", + description="Get all available labels (org + repo)", + inputSchema={ + "type": "object", + "properties": { + "repo": { + "type": "string", + "description": "Repository name (for PMO mode)" + } + } + } + ), + Tool( + name="suggest_labels", + description="Analyze context and suggest appropriate labels", + inputSchema={ + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "Issue title + description or sprint context" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["context"] + } + ), + Tool( + name="aggregate_issues", + description="Fetch issues across all repositories (PMO mode)", + inputSchema={ + "type": "object", + "properties": { + "org": { + "type": "string", + "description": "Organization name (e.g. 'bandit')" + }, + "state": { + "type": "string", + "enum": ["open", "closed", "all"], + "default": "open", + "description": "Issue state filter" + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Filter by labels" + } + }, + "required": ["org"] + } + ), + # Wiki Tools (Lessons Learned) + Tool( + name="list_wiki_pages", + description="List all wiki pages in repository", + inputSchema={ + "type": "object", + "properties": { + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + } + } + ), + Tool( + name="get_wiki_page", + description="Get a specific wiki page by name", + inputSchema={ + "type": "object", + "properties": { + "page_name": { + "type": "string", + "description": "Wiki page name/path" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["page_name"] + } + ), + Tool( + name="create_wiki_page", + description="Create a new wiki page", + inputSchema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Page title/name" + }, + "content": { + "type": "string", + "description": "Page content (markdown)" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["title", "content"] + } + ), + Tool( + name="update_wiki_page", + description="Update an existing wiki page", + inputSchema={ + "type": "object", + "properties": { + "page_name": { + "type": "string", + "description": "Wiki page name/path" + }, + "content": { + "type": "string", + "description": "New page content (markdown)" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["page_name", "content"] + } + ), + Tool( + name="create_lesson", + description="Create a lessons learned entry in the wiki", + inputSchema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Lesson title (e.g., 'Sprint 16 - Prevent Infinite Loops')" + }, + "content": { + "type": "string", + "description": "Lesson content (markdown with context, problem, solution, prevention)" + }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "description": "Tags for categorization" + }, + "category": { + "type": "string", + "default": "sprints", + "description": "Category (sprints, patterns, architecture, etc.)" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["title", "content", "tags"] + } + ), + Tool( + name="search_lessons", + description="Search lessons learned from previous sprints", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query (optional)" + }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "description": "Tags to filter by (optional)" + }, + "limit": { + "type": ["integer", "string"], + "default": 20, + "description": "Maximum results" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + } + } + ), + Tool( + name="allocate_rfc_number", + description="Allocate the next available RFC number by scanning existing RFC wiki pages", + inputSchema={ + "type": "object", + "properties": { + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + } + } + ), + # Milestone Tools + Tool( + name="list_milestones", + description="List all milestones in repository", + inputSchema={ + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": ["open", "closed", "all"], + "default": "open", + "description": "Milestone state filter" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + } + } + ), + Tool( + name="get_milestone", + description="Get a specific milestone by ID", + inputSchema={ + "type": "object", + "properties": { + "milestone_id": { + "type": ["integer", "string"], + "description": "Milestone ID" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["milestone_id"] + } + ), + Tool( + name="create_milestone", + description="Create a new milestone", + inputSchema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Milestone title" + }, + "description": { + "type": "string", + "description": "Milestone description" + }, + "due_on": { + "type": "string", + "description": "Due date (ISO 8601 format)" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["title"] + } + ), + Tool( + name="update_milestone", + description="Update an existing milestone", + inputSchema={ + "type": "object", + "properties": { + "milestone_id": { + "type": ["integer", "string"], + "description": "Milestone ID" + }, + "title": { + "type": "string", + "description": "New title" + }, + "description": { + "type": "string", + "description": "New description" + }, + "state": { + "type": "string", + "enum": ["open", "closed"], + "description": "New state" + }, + "due_on": { + "type": "string", + "description": "New due date" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["milestone_id"] + } + ), + Tool( + name="delete_milestone", + description="Delete a milestone", + inputSchema={ + "type": "object", + "properties": { + "milestone_id": { + "type": ["integer", "string"], + "description": "Milestone ID" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["milestone_id"] + } + ), + # Dependency Tools + Tool( + name="list_issue_dependencies", + description="List all dependencies for an issue (issues that block this one)", + inputSchema={ + "type": "object", + "properties": { + "issue_number": { + "type": ["integer", "string"], + "description": "Issue number" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["issue_number"] + } + ), + Tool( + name="create_issue_dependency", + description="Create a dependency (issue depends on another issue)", + inputSchema={ + "type": "object", + "properties": { + "issue_number": { + "type": ["integer", "string"], + "description": "Issue that will depend on another" + }, + "depends_on": { + "type": ["integer", "string"], + "description": "Issue that blocks issue_number" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["issue_number", "depends_on"] + } + ), + Tool( + name="remove_issue_dependency", + description="Remove a dependency between issues", + inputSchema={ + "type": "object", + "properties": { + "issue_number": { + "type": ["integer", "string"], + "description": "Issue that depends on another" + }, + "depends_on": { + "type": ["integer", "string"], + "description": "Issue being depended on" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["issue_number", "depends_on"] + } + ), + Tool( + name="get_execution_order", + description="Get parallelizable execution order for issues based on dependencies", + inputSchema={ + "type": "object", + "properties": { + "issue_numbers": { + "type": "array", + "items": {"type": "integer"}, + "description": "List of issue numbers to analyze" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["issue_numbers"] + } + ), + # Validation Tools + Tool( + name="validate_repo_org", + description="Check if repository belongs to an organization", + inputSchema={ + "type": "object", + "properties": { + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + } + } + ), + Tool( + name="get_branch_protection", + description="Get branch protection rules", + inputSchema={ + "type": "object", + "properties": { + "branch": { + "type": "string", + "description": "Branch name" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["branch"] + } + ), + Tool( + name="create_label", + description="Create a new label in the repository (for repo-specific labels like Component/*, Tech/*)", + inputSchema={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Label name (e.g., 'Component/Backend', 'Tech/Python')" + }, + "color": { + "type": "string", + "description": "Label color (hex code)" + }, + "description": { + "type": "string", + "description": "Label description" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["name", "color"] + } + ), + Tool( + name="create_org_label", + description="Create a new label at organization level (for workflow labels like Type/*, Priority/*, Complexity/*, Effort/*)", + inputSchema={ + "type": "object", + "properties": { + "org": { + "type": "string", + "description": "Organization name" + }, + "name": { + "type": "string", + "description": "Label name (e.g., 'Type/Bug', 'Priority/High')" + }, + "color": { + "type": "string", + "description": "Label color (hex code)" + }, + "description": { + "type": "string", + "description": "Label description" + } + }, + "required": ["org", "name", "color"] + } + ), + Tool( + name="create_label_smart", + description="Create a label at the appropriate level (org or repo) based on category. Org: Type/*, Priority/*, Complexity/*, Effort/*, Risk/*, Source/*, Agent/*. Repo: Component/*, Tech/*", + inputSchema={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Label name (e.g., 'Type/Bug', 'Component/Backend')" + }, + "color": { + "type": "string", + "description": "Label color (hex code)" + }, + "description": { + "type": "string", + "description": "Label description" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["name", "color"] + } + ), + # Pull Request Tools + Tool( + name="list_pull_requests", + description="List pull requests from repository", + inputSchema={ + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": ["open", "closed", "all"], + "default": "open", + "description": "PR state filter" + }, + "sort": { + "type": "string", + "enum": ["oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"], + "default": "recentupdate", + "description": "Sort order" + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Filter by labels" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + } + } + ), + Tool( + name="get_pull_request", + description="Get specific pull request details", + inputSchema={ + "type": "object", + "properties": { + "pr_number": { + "type": ["integer", "string"], + "description": "Pull request number" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["pr_number"] + } + ), + Tool( + name="get_pr_diff", + description="Get the diff for a pull request", + inputSchema={ + "type": "object", + "properties": { + "pr_number": { + "type": ["integer", "string"], + "description": "Pull request number" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["pr_number"] + } + ), + Tool( + name="get_pr_comments", + description="Get comments on a pull request", + inputSchema={ + "type": "object", + "properties": { + "pr_number": { + "type": ["integer", "string"], + "description": "Pull request number" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["pr_number"] + } + ), + Tool( + name="create_pr_review", + description="Create a review on a pull request (approve, request changes, or comment)", + inputSchema={ + "type": "object", + "properties": { + "pr_number": { + "type": ["integer", "string"], + "description": "Pull request number" + }, + "body": { + "type": "string", + "description": "Review body/summary" + }, + "event": { + "type": "string", + "enum": ["APPROVE", "REQUEST_CHANGES", "COMMENT"], + "default": "COMMENT", + "description": "Review action" + }, + "comments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "position": {"type": ["integer", "string"]}, + "body": {"type": "string"} + } + }, + "description": "Optional inline comments" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["pr_number", "body"] + } + ), + Tool( + name="add_pr_comment", + description="Add a general comment to a pull request", + inputSchema={ + "type": "object", + "properties": { + "pr_number": { + "type": ["integer", "string"], + "description": "Pull request number" + }, + "body": { + "type": "string", + "description": "Comment text" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["pr_number", "body"] + } + ), + Tool( + name="create_pull_request", + description="Create a new pull request", + inputSchema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "PR title" + }, + "body": { + "type": "string", + "description": "PR description/body" + }, + "head": { + "type": "string", + "description": "Source branch name (the branch with changes)" + }, + "base": { + "type": "string", + "description": "Target branch name (the branch to merge into)" + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of label names" + }, + "repo": { + "type": "string", + "description": "Repository name (owner/repo format)" + } + }, + "required": ["title", "body", "head", "base"] + } + ) + ] + + +def get_tool_definitions(tool_filter: Optional[list[str]] = None) -> list[Tool]: + """ + Get tool definitions, optionally filtered by name. + + Args: + tool_filter: Optional list of tool names to include. If None, returns all tools. + + Returns: + List of Tool objects + """ + all_tools = _get_all_tool_definitions() + + if tool_filter is None: + return all_tools + + filter_set = set(tool_filter) + return [tool for tool in all_tools if tool.name in filter_set] + + +def create_tool_dispatcher( + client: GiteaClient, + tool_filter: Optional[list[str]] = None +) -> Callable[[str, dict], Awaitable[list[TextContent]]]: + """ + Create a tool dispatcher function bound to the given client. + + Args: + client: GiteaClient instance + tool_filter: Optional list of tool names to allow. If None, all tools are allowed. + + Returns: + Async function that dispatches tool calls: dispatch(name, arguments) -> list[TextContent] + """ + # Initialize tool handlers + issue_tools = IssueTools(client) + label_tools = LabelTools(client) + wiki_tools = WikiTools(client) + milestone_tools = MilestoneTools(client) + dependency_tools = DependencyTools(client) + pr_tools = PullRequestTools(client) + + # Build filter set if provided + filter_set = set(tool_filter) if tool_filter else None + + async def dispatch(name: str, arguments: dict) -> list[TextContent]: + """ + Dispatch a tool call to the appropriate handler. + + Args: + name: Tool name + arguments: Tool arguments + + Returns: + List of TextContent with results + """ + try: + # Check filter if provided + if filter_set and name not in filter_set: + raise ValueError(f"Tool not available: {name}") + + # Coerce types to handle MCP serialization quirks + arguments = _coerce_types(arguments) + + # Route to appropriate tool handler + if name == "list_issues": + result = await issue_tools.list_issues(**arguments) + elif name == "get_issue": + result = await issue_tools.get_issue(**arguments) + elif name == "create_issue": + result = await issue_tools.create_issue(**arguments) + elif name == "update_issue": + result = await issue_tools.update_issue(**arguments) + elif name == "add_comment": + result = await issue_tools.add_comment(**arguments) + elif name == "get_labels": + result = await label_tools.get_labels(**arguments) + elif name == "suggest_labels": + result = await label_tools.suggest_labels(**arguments) + elif name == "aggregate_issues": + result = await issue_tools.aggregate_issues(**arguments) + # Wiki tools + elif name == "list_wiki_pages": + result = await wiki_tools.list_wiki_pages(**arguments) + elif name == "get_wiki_page": + result = await wiki_tools.get_wiki_page(**arguments) + elif name == "create_wiki_page": + result = await wiki_tools.create_wiki_page(**arguments) + elif name == "update_wiki_page": + result = await wiki_tools.update_wiki_page(**arguments) + elif name == "create_lesson": + result = await wiki_tools.create_lesson(**arguments) + elif name == "search_lessons": + tags = arguments.get('tags') + result = await wiki_tools.search_lessons( + query=arguments.get('query'), + tags=tags, + limit=arguments.get('limit', 20), + repo=arguments.get('repo') + ) + elif name == "allocate_rfc_number": + result = await wiki_tools.allocate_rfc_number( + repo=arguments.get('repo') + ) + # Milestone tools + elif name == "list_milestones": + result = await milestone_tools.list_milestones(**arguments) + elif name == "get_milestone": + result = await milestone_tools.get_milestone(**arguments) + elif name == "create_milestone": + result = await milestone_tools.create_milestone(**arguments) + elif name == "update_milestone": + result = await milestone_tools.update_milestone(**arguments) + elif name == "delete_milestone": + result = await milestone_tools.delete_milestone(**arguments) + # Dependency tools + elif name == "list_issue_dependencies": + result = await dependency_tools.list_issue_dependencies(**arguments) + elif name == "create_issue_dependency": + result = await dependency_tools.create_issue_dependency(**arguments) + elif name == "remove_issue_dependency": + result = await dependency_tools.remove_issue_dependency(**arguments) + elif name == "get_execution_order": + result = await dependency_tools.get_execution_order(**arguments) + # Validation tools + elif name == "validate_repo_org": + is_org = client.is_org_repo(arguments.get('repo')) + result = {'is_organization': is_org} + elif name == "get_branch_protection": + result = client.get_branch_protection( + arguments['branch'], + arguments.get('repo') + ) + elif name == "create_label": + result = client.create_label( + arguments['name'], + arguments['color'], + arguments.get('description'), + arguments.get('repo') + ) + elif name == "create_org_label": + result = client.create_org_label( + arguments['org'], + arguments['name'], + arguments['color'], + arguments.get('description') + ) + elif name == "create_label_smart": + result = await label_tools.create_label_smart( + arguments['name'], + arguments['color'], + arguments.get('description'), + arguments.get('repo') + ) + # Pull Request tools + elif name == "list_pull_requests": + result = await pr_tools.list_pull_requests(**arguments) + elif name == "get_pull_request": + result = await pr_tools.get_pull_request(**arguments) + elif name == "get_pr_diff": + result = await pr_tools.get_pr_diff(**arguments) + elif name == "get_pr_comments": + result = await pr_tools.get_pr_comments(**arguments) + elif name == "create_pr_review": + result = await pr_tools.create_pr_review(**arguments) + elif name == "add_pr_comment": + result = await pr_tools.add_pr_comment(**arguments) + elif name == "create_pull_request": + result = await pr_tools.create_pull_request(**arguments) + else: + raise ValueError(f"Unknown tool: {name}") + + return [TextContent( + type="text", + text=json.dumps(result, indent=2) + )] + + except Exception as e: + logger.error(f"Tool {name} failed: {e}") + return [TextContent( + type="text", + text=f"Error: {str(e)}" + )] + + return dispatch diff --git a/mcp-servers/gitea/pyproject.toml b/mcp-servers/gitea/pyproject.toml new file mode 100644 index 0000000..abce327 --- /dev/null +++ b/mcp-servers/gitea/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "gitea-mcp-server" +version = "1.0.0" +description = "MCP Server for Gitea integration - provides issue, label, wiki, milestone, dependency, and PR tools" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +authors = [ + { name = "Leo Miranda" } +] +keywords = ["mcp", "gitea", "claude", "tools"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "mcp>=0.9.0", + "python-dotenv>=1.0.0", + "requests>=2.31.0", + "pydantic>=2.5.0", +] + +[project.optional-dependencies] +test = [ + "pytest>=7.4.3", + "pytest-asyncio>=0.23.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["mcp_server"]