Skip to main content
Glama
server.py13.5 kB
import json import sys import os import asyncio import subprocess import logging from typing import Dict, Any, Optional, List, Tuple, Union import mcp from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from aider_mcp_server.atoms.logging import get_logger from aider_mcp_server.atoms.utils import DEFAULT_EDITOR_MODEL from aider_mcp_server.atoms.tools.aider_ai_code import code_with_aider from aider_mcp_server.atoms.tools.aider_list_models import list_models # Configure logging logger = get_logger(__name__) # Define MCP tools AIDER_AI_CODE_TOOL = Tool( name="aider_ai_code", description="Run Aider to perform AI coding tasks based on the provided prompt and files", inputSchema={ "type": "object", "properties": { "ai_coding_prompt": { "type": "string", "description": "The prompt for the AI to execute", }, "relative_editable_files": { "type": "array", "description": "LIST of relative paths to files that can be edited", "items": {"type": "string"}, }, "relative_readonly_files": { "type": "array", "description": "LIST of relative paths to files that can be read but not edited, add files that are not editable but useful for context", "items": {"type": "string"}, }, "model": { "type": "string", "description": "The primary AI model Aider should use for generating code, leave blank unless model is specified in the request", }, }, "required": ["ai_coding_prompt", "relative_editable_files"], }, ) LIST_MODELS_TOOL = Tool( name="list_models", description="List available models that match the provided substring", inputSchema={ "type": "object", "properties": { "substring": { "type": "string", "description": "Substring to match against available models", } }, }, ) def is_git_repository(directory: str) -> Tuple[bool, Union[str, None]]: """ Check if the specified directory is a git repository. Args: directory (str): The directory to check. Returns: Tuple[bool, Union[str, None]]: A tuple containing a boolean indicating if it's a git repo, and an error message if it's not. """ try: # Make sure the directory exists if not os.path.isdir(directory): return False, f"Directory does not exist: {directory}" # Use the git command with -C option to specify the working directory # This way we don't need to change our current directory result = subprocess.run( ["git", "-C", directory, "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True, check=False, ) if result.returncode == 0 and result.stdout.strip() == "true": return True, None else: return False, result.stderr.strip() or "Directory is not a git repository" except subprocess.SubprocessError as e: return False, f"Error checking git repository: {str(e)}" except Exception as e: return False, f"Unexpected error checking git repository: {str(e)}" def process_aider_ai_code_request( params: Dict[str, Any], editor_model: str, current_working_dir: str, ) -> Dict[str, Any]: """ Process an aider_ai_code request. Args: params (Dict[str, Any]): The request parameters. editor_model (str): The editor model to use. current_working_dir (str): The current working directory where git repo is located. Returns: Dict[str, Any]: The response data. """ ai_coding_prompt = params.get("ai_coding_prompt", "") relative_editable_files = params.get("relative_editable_files", []) relative_readonly_files = params.get("relative_readonly_files", []) # Ensure relative_editable_files is a list if isinstance(relative_editable_files, str): logger.info( f"Converting single editable file string to list: {relative_editable_files}" ) relative_editable_files = [relative_editable_files] # Ensure relative_readonly_files is a list if isinstance(relative_readonly_files, str): logger.info( f"Converting single readonly file string to list: {relative_readonly_files}" ) relative_readonly_files = [relative_readonly_files] # Get the model from request parameters if provided request_model = params.get("model") # Log the request details logger.info(f"AI Coding Request: Prompt: '{ai_coding_prompt}'") logger.info(f"Editable files: {relative_editable_files}") logger.info(f"Readonly files: {relative_readonly_files}") logger.info(f"Editor model: {editor_model}") if request_model: logger.info(f"Request-specified model: {request_model}") # Use the model specified in the request if provided, otherwise use the editor model model_to_use = request_model if request_model else editor_model # Use the passed-in current_working_dir parameter logger.info(f"Using working directory for code_with_aider: {current_working_dir}") result_json = code_with_aider( ai_coding_prompt=ai_coding_prompt, relative_editable_files=relative_editable_files, relative_readonly_files=relative_readonly_files, model=model_to_use, working_dir=current_working_dir, ) # Parse the JSON string result try: result_dict = json.loads(result_json) except json.JSONDecodeError as e: logger.error(f"Error: Failed to parse JSON response from code_with_aider: {e}") logger.error(f"Received raw response: {result_json}") return {"error": "Failed to process AI coding result"} logger.info( f"AI Coding Request Completed. Success: {result_dict.get('success', False)}" ) return { "success": result_dict.get("success", False), "diff": result_dict.get("diff", "Error retrieving diff"), } def process_list_models_request(params: Dict[str, Any]) -> Dict[str, Any]: """ Process a list_models request. Args: params (Dict[str, Any]): The request parameters. Returns: Dict[str, Any]: The response data. """ substring = params.get("substring", "") # Log the request details logger.info(f"List Models Request: Substring: '{substring}'") models = list_models(substring) logger.info(f"Found {len(models)} models matching '{substring}'") return {"models": models} def handle_request( request: Dict[str, Any], current_working_dir: str, editor_model: str, ) -> Dict[str, Any]: """ Handle incoming MCP requests according to the MCP protocol. Args: request (Dict[str, Any]): The request JSON. current_working_dir (str): The current working directory. Must be a valid git repository. editor_model (str): The editor model to use. Returns: Dict[str, Any]: The response JSON. """ try: # Validate current_working_dir is provided and is a git repository if not current_working_dir: error_msg = "Error: current_working_dir is required. Please provide a valid git repository path." logger.error(error_msg) return {"error": error_msg} # MCP protocol requires 'name' and 'parameters' fields if "name" not in request: logger.error("Error: Received request missing 'name' field.") return {"error": "Missing 'name' field in request"} request_type = request.get("name") params = request.get("parameters", {}) logger.info( f"Received request: Type='{request_type}', CWD='{current_working_dir}'" ) # Validate that the current_working_dir is a git repository before changing to it is_git_repo, error_message = is_git_repository(current_working_dir) if not is_git_repo: error_msg = f"Error: The specified directory '{current_working_dir}' is not a valid git repository: {error_message}" logger.error(error_msg) return {"error": error_msg} # Set working directory logger.info(f"Changing working directory to: {current_working_dir}") os.chdir(current_working_dir) # Route to the appropriate handler based on request type if request_type == "aider_ai_code": return process_aider_ai_code_request( params, editor_model, current_working_dir ) elif request_type == "list_models": return process_list_models_request(params) else: # Unknown request type logger.warning(f"Warning: Unknown request type received: {request_type}") return {"error": f"Unknown request type: {request_type}"} except Exception as e: # Handle any errors logger.exception( f"Critical Error: Unhandled exception during request processing: {str(e)}" ) return {"error": f"Internal server error: {str(e)}"} async def serve( editor_model: str = DEFAULT_EDITOR_MODEL, current_working_dir: str = None, ) -> None: """ Start the MCP server following the Model Context Protocol. The server reads JSON requests from stdin and writes JSON responses to stdout. Each request should contain a 'name' field indicating the tool to invoke, and a 'parameters' field with the tool-specific parameters. Args: editor_model (str, optional): The editor model to use. Defaults to DEFAULT_EDITOR_MODEL. current_working_dir (str, required): The current working directory. Must be a valid git repository. Raises: ValueError: If current_working_dir is not provided or is not a git repository. """ logger.info(f"Starting Aider MCP Server") logger.info(f"Editor Model: {editor_model}") # Validate current_working_dir is provided if not current_working_dir: error_msg = "Error: current_working_dir is required. Please provide a valid git repository path." logger.error(error_msg) raise ValueError(error_msg) logger.info(f"Initial Working Directory: {current_working_dir}") # Validate that the current_working_dir is a git repository is_git_repo, error_message = is_git_repository(current_working_dir) if not is_git_repo: error_msg = f"Error: The specified directory '{current_working_dir}' is not a valid git repository: {error_message}" logger.error(error_msg) raise ValueError(error_msg) logger.info(f"Validated git repository at: {current_working_dir}") # Set working directory logger.info(f"Setting working directory to: {current_working_dir}") os.chdir(current_working_dir) # Create the MCP server server = Server("aider-mcp-server") @server.list_tools() async def list_tools() -> List[Tool]: """Register all available tools with the MCP server.""" return [AIDER_AI_CODE_TOOL, LIST_MODELS_TOOL] @server.call_tool() async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: """Handle tool calls from the MCP client.""" logger.info(f"Received Tool Call: Name='{name}'") logger.info(f"Arguments: {arguments}") try: if name == "aider_ai_code": logger.info(f"Processing 'aider_ai_code' tool call...") result = process_aider_ai_code_request( arguments, editor_model, current_working_dir ) return [TextContent(type="text", text=json.dumps(result))] elif name == "list_models": logger.info(f"Processing 'list_models' tool call...") result = process_list_models_request(arguments) return [TextContent(type="text", text=json.dumps(result))] else: logger.warning(f"Warning: Received call for unknown tool: {name}") return [ TextContent( type="text", text=json.dumps({"error": f"Unknown tool: {name}"}) ) ] except Exception as e: logger.exception(f"Error: Exception during tool call '{name}': {e}") return [ TextContent( type="text", text=json.dumps( {"error": f"Error processing tool {name}: {str(e)}"} ), ) ] # Initialize and run the server try: options = server.create_initialization_options() logger.info("Initializing stdio server connection...") async with stdio_server() as (read_stream, write_stream): logger.info("Server running. Waiting for requests...") await server.run(read_stream, write_stream, options, raise_exceptions=True) except Exception as e: logger.exception( f"Critical Error: Server stopped due to unhandled exception: {e}" ) raise finally: logger.info("Aider MCP Server shutting down.")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/disler/aider-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server