.
This commit is contained in:
246
src/aegis_gitea_mcp/server.py
Normal file
246
src/aegis_gitea_mcp/server.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Main MCP server implementation with FastAPI and SSE support."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import ValidationError
|
||||
|
||||
from aegis_gitea_mcp.audit import get_audit_logger
|
||||
from aegis_gitea_mcp.config import get_settings
|
||||
from aegis_gitea_mcp.gitea_client import GiteaClient
|
||||
from aegis_gitea_mcp.mcp_protocol import (
|
||||
AVAILABLE_TOOLS,
|
||||
MCPListToolsResponse,
|
||||
MCPToolCallRequest,
|
||||
MCPToolCallResponse,
|
||||
get_tool_by_name,
|
||||
)
|
||||
from aegis_gitea_mcp.tools.repository import (
|
||||
get_file_contents_tool,
|
||||
get_file_tree_tool,
|
||||
get_repository_info_tool,
|
||||
list_repositories_tool,
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="AegisGitea MCP Server",
|
||||
description="Security-first MCP server for controlled AI access to self-hosted Gitea",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
# Global settings and audit logger
|
||||
settings = get_settings()
|
||||
audit = get_audit_logger()
|
||||
|
||||
|
||||
# Tool dispatcher mapping
|
||||
TOOL_HANDLERS = {
|
||||
"list_repositories": list_repositories_tool,
|
||||
"get_repository_info": get_repository_info_tool,
|
||||
"get_file_tree": get_file_tree_tool,
|
||||
"get_file_contents": get_file_contents_tool,
|
||||
}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event() -> None:
|
||||
"""Initialize server on startup."""
|
||||
logger.info(f"Starting AegisGitea MCP Server on {settings.mcp_host}:{settings.mcp_port}")
|
||||
logger.info(f"Connected to Gitea instance: {settings.gitea_base_url}")
|
||||
logger.info(f"Audit logging enabled: {settings.audit_log_path}")
|
||||
|
||||
# Test Gitea connection
|
||||
try:
|
||||
async with GiteaClient() as gitea:
|
||||
user = await gitea.get_current_user()
|
||||
logger.info(f"Authenticated as bot user: {user.get('login', 'unknown')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Gitea: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event() -> None:
|
||||
"""Cleanup on server shutdown."""
|
||||
logger.info("Shutting down AegisGitea MCP Server")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
"""Root endpoint with server information."""
|
||||
return {
|
||||
"name": "AegisGitea MCP Server",
|
||||
"version": "0.1.0",
|
||||
"status": "running",
|
||||
"mcp_version": "1.0",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> Dict[str, str]:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
@app.get("/mcp/tools")
|
||||
async def list_tools() -> JSONResponse:
|
||||
"""List all available MCP tools.
|
||||
|
||||
Returns:
|
||||
JSON response with list of tool definitions
|
||||
"""
|
||||
response = MCPListToolsResponse(tools=AVAILABLE_TOOLS)
|
||||
return JSONResponse(content=response.model_dump())
|
||||
|
||||
|
||||
@app.post("/mcp/tool/call")
|
||||
async def call_tool(request: MCPToolCallRequest) -> JSONResponse:
|
||||
"""Execute an MCP tool call.
|
||||
|
||||
Args:
|
||||
request: Tool call request with tool name and arguments
|
||||
|
||||
Returns:
|
||||
JSON response with tool execution result
|
||||
"""
|
||||
correlation_id = request.correlation_id or audit.log_tool_invocation(
|
||||
tool_name=request.tool,
|
||||
params=request.arguments,
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate tool exists
|
||||
tool_def = get_tool_by_name(request.tool)
|
||||
if not tool_def:
|
||||
error_msg = f"Tool '{request.tool}' not found"
|
||||
audit.log_tool_invocation(
|
||||
tool_name=request.tool,
|
||||
correlation_id=correlation_id,
|
||||
result_status="error",
|
||||
error=error_msg,
|
||||
)
|
||||
raise HTTPException(status_code=404, detail=error_msg)
|
||||
|
||||
# Get tool handler
|
||||
handler = TOOL_HANDLERS.get(request.tool)
|
||||
if not handler:
|
||||
error_msg = f"Tool '{request.tool}' has no handler implementation"
|
||||
audit.log_tool_invocation(
|
||||
tool_name=request.tool,
|
||||
correlation_id=correlation_id,
|
||||
result_status="error",
|
||||
error=error_msg,
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
# Execute tool with Gitea client
|
||||
async with GiteaClient() as gitea:
|
||||
result = await handler(gitea, request.arguments)
|
||||
|
||||
audit.log_tool_invocation(
|
||||
tool_name=request.tool,
|
||||
correlation_id=correlation_id,
|
||||
result_status="success",
|
||||
)
|
||||
|
||||
response = MCPToolCallResponse(
|
||||
success=True,
|
||||
result=result,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return JSONResponse(content=response.model_dump())
|
||||
|
||||
except ValidationError as e:
|
||||
error_msg = f"Invalid arguments: {str(e)}"
|
||||
audit.log_tool_invocation(
|
||||
tool_name=request.tool,
|
||||
correlation_id=correlation_id,
|
||||
result_status="error",
|
||||
error=error_msg,
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
audit.log_tool_invocation(
|
||||
tool_name=request.tool,
|
||||
correlation_id=correlation_id,
|
||||
result_status="error",
|
||||
error=error_msg,
|
||||
)
|
||||
response = MCPToolCallResponse(
|
||||
success=False,
|
||||
error=error_msg,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return JSONResponse(content=response.model_dump(), status_code=500)
|
||||
|
||||
|
||||
@app.get("/mcp/sse")
|
||||
async def sse_endpoint(request: Request) -> StreamingResponse:
|
||||
"""Server-Sent Events endpoint for MCP protocol.
|
||||
|
||||
This enables real-time communication with ChatGPT using SSE.
|
||||
|
||||
Returns:
|
||||
Streaming SSE response
|
||||
"""
|
||||
async def event_stream():
|
||||
"""Generate SSE events."""
|
||||
# Send initial connection event
|
||||
yield f"data: {{'event': 'connected', 'server': 'AegisGitea MCP', 'version': '0.1.0'}}\n\n"
|
||||
|
||||
# Keep connection alive
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
|
||||
# Heartbeat every 30 seconds
|
||||
yield f"data: {{'event': 'heartbeat'}}\n\n"
|
||||
|
||||
# Wait for next heartbeat (in production, this would handle actual events)
|
||||
import asyncio
|
||||
await asyncio.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SSE stream error: {e}")
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the MCP server."""
|
||||
import uvicorn
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
uvicorn.run(
|
||||
"aegis_gitea_mcp.server:app",
|
||||
host=settings.mcp_host,
|
||||
port=settings.mcp_port,
|
||||
log_level=settings.log_level.lower(),
|
||||
reload=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user