Refactor MCP server structure and add separate project tools

- Rename src/mcp to src/mcp_server for clarity
- Update all internal imports to use new path
- Create features/projects directory for modular tool organization
- Add separate, simple project tools (create, list, get, delete, update)
- Keep consolidated tools for backward compatibility (via env var)
- Add USE_SEPARATE_PROJECT_TOOLS env var to toggle between approaches

The new separate tools:
- Solve the async project creation context loss issue
- Provide clearer, single-purpose interfaces
- Remove complex PRP examples for simplicity
- Handle project creation polling automatically
This commit is contained in:
Rasmus Widing 2025-08-18 15:55:00 +03:00
parent 6273615dd6
commit 1f03b40af1
8 changed files with 323 additions and 11 deletions

View File

@ -0,0 +1,13 @@
"""
Project management tools for Archon MCP Server.
This module provides separate tools for each project operation:
- create_project: Create a new project
- list_projects: List all projects
- get_project: Get project details
- delete_project: Delete a project
"""
from .project_tools import register_project_tools
__all__ = ["register_project_tools"]

View File

@ -0,0 +1,282 @@
"""
Simple project management tools for Archon MCP Server.
Provides separate, focused tools for each project operation.
No complex PRP examples - just straightforward project management.
"""
import asyncio
import json
import logging
from typing import Any, Optional
from urllib.parse import urljoin
import httpx
from mcp.server.fastmcp import Context, FastMCP
from src.server.config.service_discovery import get_api_url
logger = logging.getLogger(__name__)
def register_project_tools(mcp: FastMCP):
"""Register individual project management tools with the MCP server."""
@mcp.tool()
async def create_project(
ctx: Context,
title: str,
description: str = "",
github_repo: Optional[str] = None,
) -> str:
"""
Create a new project.
Args:
title: Project title (required)
description: Project description (optional)
github_repo: GitHub repository URL (optional)
Returns:
JSON with project details including project_id
Example:
create_project(title="My New Project", description="A test project")
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
urljoin(api_url, "/api/projects"),
json={"title": title, "description": description, "github_repo": github_repo},
)
if response.status_code == 200:
result = response.json()
# Handle async project creation
if "progress_id" in result:
# Poll for completion (max 30 seconds)
for attempt in range(30):
await asyncio.sleep(1)
# List projects to find the newly created one
list_response = await client.get(urljoin(api_url, "/api/projects"))
if list_response.status_code == 200:
projects = list_response.json()
# Find project with matching title created recently
for proj in projects:
if proj.get("title") == title:
return json.dumps({
"success": True,
"project": proj,
"project_id": proj["id"],
"message": f"Project created successfully with ID: {proj['id']}"
})
# If we couldn't find it after polling
return json.dumps({
"success": True,
"progress_id": result["progress_id"],
"message": "Project creation started. Use list_projects to find it once complete."
})
else:
# Direct response (shouldn't happen with current API)
return json.dumps({"success": True, "project": result})
else:
error_detail = response.json().get("detail", {}).get("error", "Unknown error")
return json.dumps({"success": False, "error": error_detail})
except Exception as e:
logger.error(f"Error creating project: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
async def list_projects(ctx: Context) -> str:
"""
List all projects.
Returns:
JSON array of all projects with their basic information
Example:
list_projects()
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(urljoin(api_url, "/api/projects"))
if response.status_code == 200:
projects = response.json()
return json.dumps({
"success": True,
"projects": projects,
"count": len(projects)
})
else:
return json.dumps({
"success": False,
"error": "Failed to list projects"
})
except Exception as e:
logger.error(f"Error listing projects: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
async def get_project(ctx: Context, project_id: str) -> str:
"""
Get detailed information about a specific project.
Args:
project_id: UUID of the project
Returns:
JSON with complete project details
Example:
get_project(project_id="550e8400-e29b-41d4-a716-446655440000")
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(urljoin(api_url, f"/api/projects/{project_id}"))
if response.status_code == 200:
project = response.json()
return json.dumps({"success": True, "project": project})
elif response.status_code == 404:
return json.dumps({
"success": False,
"error": f"Project {project_id} not found"
})
else:
return json.dumps({
"success": False,
"error": "Failed to get project"
})
except Exception as e:
logger.error(f"Error getting project: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
async def delete_project(ctx: Context, project_id: str) -> str:
"""
Delete a project.
Args:
project_id: UUID of the project to delete
Returns:
JSON confirmation of deletion
Example:
delete_project(project_id="550e8400-e29b-41d4-a716-446655440000")
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.delete(
urljoin(api_url, f"/api/projects/{project_id}")
)
if response.status_code == 200:
return json.dumps({
"success": True,
"message": f"Project {project_id} deleted successfully"
})
elif response.status_code == 404:
return json.dumps({
"success": False,
"error": f"Project {project_id} not found"
})
else:
return json.dumps({
"success": False,
"error": "Failed to delete project"
})
except Exception as e:
logger.error(f"Error deleting project: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
async def update_project(
ctx: Context,
project_id: str,
title: Optional[str] = None,
description: Optional[str] = None,
github_repo: Optional[str] = None,
) -> str:
"""
Update a project's basic information.
Args:
project_id: UUID of the project to update
title: New title (optional)
description: New description (optional)
github_repo: New GitHub repository URL (optional)
Returns:
JSON with updated project details
Example:
update_project(project_id="550e8400-e29b-41d4-a716-446655440000",
title="Updated Project Title")
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)
# Build update payload with only provided fields
update_data = {}
if title is not None:
update_data["title"] = title
if description is not None:
update_data["description"] = description
if github_repo is not None:
update_data["github_repo"] = github_repo
if not update_data:
return json.dumps({
"success": False,
"error": "No fields to update"
})
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.put(
urljoin(api_url, f"/api/projects/{project_id}"),
json=update_data
)
if response.status_code == 200:
project = response.json()
return json.dumps({
"success": True,
"project": project,
"message": "Project updated successfully"
})
elif response.status_code == 404:
return json.dumps({
"success": False,
"error": f"Project {project_id} not found"
})
else:
return json.dumps({
"success": False,
"error": "Failed to update project"
})
except Exception as e:
logger.error(f"Error updating project: {e}")
return json.dumps({"success": False, "error": str(e)})

View File

@ -363,7 +363,7 @@ def register_modules():
# Import and register RAG module (HTTP-based version)
try:
from src.mcp.modules.rag_module import register_rag_tools
from src.mcp_server.modules.rag_module import register_rag_tools
register_rag_tools(mcp)
modules_registered += 1
@ -376,18 +376,35 @@ def register_modules():
# Import and register Project module - only if Projects are enabled
projects_enabled = os.getenv("PROJECTS_ENABLED", "true").lower() == "true"
use_separate_project_tools = os.getenv("USE_SEPARATE_PROJECT_TOOLS", "false").lower() == "true"
if projects_enabled:
try:
from src.mcp.modules.project_module import register_project_tools
if use_separate_project_tools:
# Use new separated project tools
try:
from src.mcp_server.features.projects import register_project_tools
register_project_tools(mcp)
modules_registered += 1
logger.info("✓ Project module registered (HTTP-based)")
except ImportError as e:
logger.warning(f"⚠ Project module not available: {e}")
except Exception as e:
logger.error(f"✗ Error registering Project module: {e}")
logger.error(traceback.format_exc())
register_project_tools(mcp)
modules_registered += 1
logger.info("✓ Project tools registered (separate tools)")
except ImportError as e:
logger.warning(f"⚠ Separate project tools not available: {e}")
except Exception as e:
logger.error(f"✗ Error registering separate project tools: {e}")
logger.error(traceback.format_exc())
else:
# Use consolidated project module (for backward compatibility)
try:
from src.mcp_server.modules.project_module import register_project_tools
register_project_tools(mcp)
modules_registered += 1
logger.info("✓ Project module registered (consolidated)")
except ImportError as e:
logger.warning(f"⚠ Project module not available: {e}")
except Exception as e:
logger.error(f"✗ Error registering Project module: {e}")
logger.error(traceback.format_exc())
else:
logger.info("⚠ Project module skipped - Projects are disabled")