feat: harden gateway with policy engine, secure tools, and governance docs
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"""MCP protocol implementation for AegisGitea."""
|
||||
"""MCP protocol models and tool registry."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -10,153 +12,366 @@ class MCPTool(BaseModel):
|
||||
|
||||
name: str = Field(..., description="Unique tool identifier")
|
||||
description: str = Field(..., description="Human-readable tool description")
|
||||
input_schema: Dict[str, Any] = Field(
|
||||
..., alias="inputSchema", description="JSON Schema for tool input"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
serialize_by_alias=True,
|
||||
)
|
||||
input_schema: dict[str, Any] = Field(..., description="JSON schema describing input arguments")
|
||||
write_operation: bool = Field(default=False, description="Whether tool mutates data")
|
||||
|
||||
|
||||
class MCPToolCallRequest(BaseModel):
|
||||
"""Request to invoke an MCP tool."""
|
||||
|
||||
tool: str = Field(..., description="Name of the tool to invoke")
|
||||
arguments: Dict[str, Any] = Field(default_factory=dict, description="Tool arguments")
|
||||
correlation_id: Optional[str] = Field(None, description="Request correlation ID")
|
||||
arguments: dict[str, Any] = Field(default_factory=dict, description="Tool argument payload")
|
||||
correlation_id: str | None = Field(default=None, description="Request correlation ID")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class MCPToolCallResponse(BaseModel):
|
||||
"""Response from an MCP tool invocation."""
|
||||
"""Response returned from MCP tool invocation."""
|
||||
|
||||
success: bool = Field(..., description="Whether the tool call succeeded")
|
||||
result: Optional[Any] = Field(None, description="Tool result data")
|
||||
error: Optional[str] = Field(None, description="Error message if failed")
|
||||
correlation_id: str = Field(..., description="Request correlation ID")
|
||||
success: bool = Field(..., description="Whether invocation succeeded")
|
||||
result: Any | None = Field(default=None, description="Tool result payload")
|
||||
error: str | None = Field(default=None, description="Error message for failed request")
|
||||
correlation_id: str = Field(..., description="Correlation ID for request tracing")
|
||||
|
||||
|
||||
class MCPListToolsResponse(BaseModel):
|
||||
"""Response listing available MCP tools."""
|
||||
"""Response listing available tools."""
|
||||
|
||||
tools: List[MCPTool] = Field(..., description="List of available tools")
|
||||
tools: list[MCPTool] = Field(..., description="Available tool definitions")
|
||||
|
||||
|
||||
# Tool definitions for AegisGitea MCP
|
||||
def _tool(
|
||||
name: str, description: str, schema: dict[str, Any], write_operation: bool = False
|
||||
) -> MCPTool:
|
||||
"""Construct tool metadata entry."""
|
||||
return MCPTool(
|
||||
name=name,
|
||||
description=description,
|
||||
input_schema=schema,
|
||||
write_operation=write_operation,
|
||||
)
|
||||
|
||||
TOOL_LIST_REPOSITORIES = MCPTool(
|
||||
name="list_repositories",
|
||||
description="List all repositories visible to the AI bot user. "
|
||||
"Only repositories where the bot has explicit read access will be returned. "
|
||||
"This respects Gitea's dynamic authorization model.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
},
|
||||
)
|
||||
|
||||
TOOL_GET_REPOSITORY_INFO = MCPTool(
|
||||
name="get_repository_info",
|
||||
description="Get detailed information about a specific repository, "
|
||||
"including description, default branch, language, and metadata. "
|
||||
"Requires the bot user to have read access.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner username or organization",
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
AVAILABLE_TOOLS: list[MCPTool] = [
|
||||
_tool(
|
||||
"list_repositories",
|
||||
"List repositories visible to the configured bot account.",
|
||||
{"type": "object", "properties": {}, "required": []},
|
||||
),
|
||||
_tool(
|
||||
"get_repository_info",
|
||||
"Get metadata for a repository.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"owner": {"type": "string"}, "repo": {"type": "string"}},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
},
|
||||
)
|
||||
|
||||
TOOL_GET_FILE_TREE = MCPTool(
|
||||
name="get_file_tree",
|
||||
description="Get the file tree structure for a repository at a specific ref. "
|
||||
"Returns a list of files and directories. "
|
||||
"Non-recursive by default for safety (max depth: 1 level).",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner username or organization",
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "Branch, tag, or commit SHA (defaults to 'main')",
|
||||
"default": "main",
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to recursively fetch entire tree (use with caution)",
|
||||
"default": False,
|
||||
),
|
||||
_tool(
|
||||
"get_file_tree",
|
||||
"Get repository tree at a selected ref.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"ref": {"type": "string", "default": "main"},
|
||||
"recursive": {"type": "boolean", "default": False},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
},
|
||||
)
|
||||
|
||||
TOOL_GET_FILE_CONTENTS = MCPTool(
|
||||
name="get_file_contents",
|
||||
description="Read the contents of a specific file in a repository. "
|
||||
"File size is limited to 1MB by default for safety. "
|
||||
"Returns base64-encoded content for binary files.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner username or organization",
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to file within repository (e.g., 'src/main.py')",
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "Branch, tag, or commit SHA (defaults to 'main')",
|
||||
"default": "main",
|
||||
),
|
||||
_tool(
|
||||
"get_file_contents",
|
||||
"Read a repository file with size-limited content.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"filepath": {"type": "string"},
|
||||
"ref": {"type": "string", "default": "main"},
|
||||
},
|
||||
"required": ["owner", "repo", "filepath"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"required": ["owner", "repo", "filepath"],
|
||||
},
|
||||
)
|
||||
|
||||
# Registry of all available tools
|
||||
AVAILABLE_TOOLS: List[MCPTool] = [
|
||||
TOOL_LIST_REPOSITORIES,
|
||||
TOOL_GET_REPOSITORY_INFO,
|
||||
TOOL_GET_FILE_TREE,
|
||||
TOOL_GET_FILE_CONTENTS,
|
||||
),
|
||||
_tool(
|
||||
"search_code",
|
||||
"Search code in a repository.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"query": {"type": "string"},
|
||||
"ref": {"type": "string", "default": "main"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
|
||||
},
|
||||
"required": ["owner", "repo", "query"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"list_commits",
|
||||
"List commits for a repository ref.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"ref": {"type": "string", "default": "main"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"get_commit_diff",
|
||||
"Get commit metadata and file diffs.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"sha": {"type": "string"},
|
||||
},
|
||||
"required": ["owner", "repo", "sha"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"compare_refs",
|
||||
"Compare two repository refs.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"base": {"type": "string"},
|
||||
"head": {"type": "string"},
|
||||
},
|
||||
"required": ["owner", "repo", "base", "head"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"list_issues",
|
||||
"List repository issues.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "default": []},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"get_issue",
|
||||
"Get repository issue details.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"issue_number": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
"required": ["owner", "repo", "issue_number"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"list_pull_requests",
|
||||
"List repository pull requests.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"get_pull_request",
|
||||
"Get pull request details.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"pull_number": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
"required": ["owner", "repo", "pull_number"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"list_labels",
|
||||
"List labels defined on a repository.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"list_tags",
|
||||
"List repository tags.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"list_releases",
|
||||
"List repository releases.",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
),
|
||||
_tool(
|
||||
"create_issue",
|
||||
"Create a repository issue (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"body": {"type": "string", "default": ""},
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "default": []},
|
||||
"assignees": {"type": "array", "items": {"type": "string"}, "default": []},
|
||||
},
|
||||
"required": ["owner", "repo", "title"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"update_issue",
|
||||
"Update issue title/body/state (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"issue_number": {"type": "integer", "minimum": 1},
|
||||
"title": {"type": "string"},
|
||||
"body": {"type": "string"},
|
||||
"state": {"type": "string", "enum": ["open", "closed"]},
|
||||
},
|
||||
"required": ["owner", "repo", "issue_number"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_issue_comment",
|
||||
"Create issue comment (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"issue_number": {"type": "integer", "minimum": 1},
|
||||
"body": {"type": "string"},
|
||||
},
|
||||
"required": ["owner", "repo", "issue_number", "body"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_pr_comment",
|
||||
"Create pull request comment (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"pull_number": {"type": "integer", "minimum": 1},
|
||||
"body": {"type": "string"},
|
||||
},
|
||||
"required": ["owner", "repo", "pull_number", "body"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"add_labels",
|
||||
"Add labels to an issue or PR (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"issue_number": {"type": "integer", "minimum": 1},
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
||||
},
|
||||
"required": ["owner", "repo", "issue_number", "labels"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"assign_issue",
|
||||
"Assign users to issue or PR (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"issue_number": {"type": "integer", "minimum": 1},
|
||||
"assignees": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
||||
},
|
||||
"required": ["owner", "repo", "issue_number", "assignees"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def get_tool_by_name(tool_name: str) -> Optional[MCPTool]:
|
||||
"""Get tool definition by name.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to retrieve
|
||||
|
||||
Returns:
|
||||
Tool definition or None if not found
|
||||
"""
|
||||
def get_tool_by_name(tool_name: str) -> MCPTool | None:
|
||||
"""Get tool definition by name."""
|
||||
for tool in AVAILABLE_TOOLS:
|
||||
if tool.name == tool_name:
|
||||
return tool
|
||||
|
||||
Reference in New Issue
Block a user