first
This commit is contained in:
commit
4724d04367
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@ -0,0 +1,42 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
LABEL Name="mcp-server" Version="1.0"
|
||||
|
||||
# Set non-interactive mode for apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install system tools including Node.js
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
python3 python3-pip python3-venv python3-full \
|
||||
git curl ca-certificates unzip wget \
|
||||
make gcc g++ && \
|
||||
# Install Node.js 20.x
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create and activate virtual environment
|
||||
RUN python3 -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# Install Node.js tools
|
||||
RUN npm install -g node-gyp @google/gemini-cli && \
|
||||
# Verify installation
|
||||
which gemini || ln -s /usr/local/lib/node_modules/@google/gemini-cli/bin/gemini.js /usr/local/bin/gemini
|
||||
|
||||
# Workdir setup
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Entrypoint
|
||||
CMD ["python3", "main.py"]
|
||||
233
README.md
Normal file
233
README.md
Normal file
@ -0,0 +1,233 @@
|
||||
# MCP Server - AI-Powered Code Editor
|
||||
|
||||
A comprehensive server that automatically clones Gitea repositories, analyzes code with AI models (Gemini/OpenAI), applies intelligent code changes, and commits them back to the repository.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Repository Management**: Clone repositories from Gitea with authentication
|
||||
- **AI-Powered Analysis**: Use Gemini CLI or OpenAI to analyze and edit code
|
||||
- **Model Selection**: Choose specific AI models (e.g., gemini-1.5-pro, gpt-4)
|
||||
- **Real-time Progress Tracking**: Web interface with live status updates
|
||||
- **Modern UI**: Beautiful, responsive frontend with progress indicators
|
||||
- **Background Processing**: Asynchronous task processing with status monitoring
|
||||
- **Comprehensive Logging**: Full logging to both console and file
|
||||
- **Docker Support**: Easy deployment with Docker and docker-compose
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- Git
|
||||
- API keys for AI models (Gemini or OpenAI)
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
### Option 1: Docker (Recommended)
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd mcp-server
|
||||
```
|
||||
|
||||
2. **Build and run with Docker Compose**
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
3. **Or build and run manually**
|
||||
```bash
|
||||
docker build -t mcp-server .
|
||||
docker run -p 8000:8000 mcp-server
|
||||
```
|
||||
|
||||
### Option 2: Local Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd mcp-server
|
||||
```
|
||||
|
||||
2. **Install Python dependencies**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Install Gemini CLI (if using Gemini)**
|
||||
```bash
|
||||
# Download from GitHub releases
|
||||
curl -L https://github.com/google/generative-ai-go/releases/latest/download/gemini-linux-amd64 -o /usr/local/bin/gemini
|
||||
chmod +x /usr/local/bin/gemini
|
||||
```
|
||||
|
||||
4. **Start the server**
|
||||
```bash
|
||||
python main.py
|
||||
# or
|
||||
python start.py
|
||||
```
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Using the Web Interface
|
||||
|
||||
1. Open your browser and navigate to `http://localhost:8000`
|
||||
2. Fill in the repository details:
|
||||
- **Gitea Repository URL**: Your repository URL (e.g., `http://157.66.191.31:3000/user/repo.git`)
|
||||
- **Gitea Token**: Your Gitea access token (get from Settings → Applications → Generate new token)
|
||||
- **AI Model**: Choose between Gemini CLI or OpenAI
|
||||
- **Model Name**: Specify the exact model (e.g., `gemini-1.5-pro`, `gpt-4`)
|
||||
- **API Key**: Your AI model API key
|
||||
- **Prompt**: Describe what changes you want to make to the code
|
||||
|
||||
3. Click "Process Repository" and monitor the progress
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- `GET /` - Web interface
|
||||
- `POST /process` - Start repository processing
|
||||
- `GET /status/{task_id}` - Get processing status
|
||||
- `GET /health` - Health check
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HOST` | Server host | `0.0.0.0` |
|
||||
| `PORT` | Server port | `8000` |
|
||||
|
||||
### Supported AI Models
|
||||
|
||||
**Gemini Models:**
|
||||
- `gemini-1.5-pro` (recommended)
|
||||
- `gemini-1.5-flash`
|
||||
- `gemini-1.0-pro`
|
||||
|
||||
**OpenAI Models:**
|
||||
- `gpt-4`
|
||||
- `gpt-4-turbo`
|
||||
- `gpt-3.5-turbo`
|
||||
|
||||
### Supported File Types
|
||||
|
||||
The system analyzes and can modify:
|
||||
- Python (`.py`)
|
||||
- JavaScript (`.js`, `.jsx`)
|
||||
- TypeScript (`.ts`, `.tsx`)
|
||||
- HTML (`.html`)
|
||||
- CSS (`.css`)
|
||||
- JSON (`.json`)
|
||||
- Markdown (`.md`)
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
mcp-server/
|
||||
├── main.py # FastAPI application
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Dockerfile # Docker configuration
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── README.md # This file
|
||||
├── templates/
|
||||
│ └── index.html # Frontend template
|
||||
├── static/
|
||||
│ ├── style.css # Frontend styles
|
||||
│ └── script.js # Frontend JavaScript
|
||||
└── logs/ # Log files (created by Docker)
|
||||
```
|
||||
|
||||
## 🔄 How It Works
|
||||
|
||||
1. **Repository Cloning**: Authenticates with Gitea and clones the repository
|
||||
2. **AI Analysis**: Sends code and prompt to selected AI model
|
||||
3. **Code Modification**: Applies AI-suggested changes to the codebase
|
||||
4. **Commit & Push**: Commits changes and pushes back to Gitea
|
||||
|
||||
## 🎯 Example Prompts
|
||||
|
||||
- "Add error handling to all API endpoints"
|
||||
- "Optimize database queries for better performance"
|
||||
- "Add comprehensive logging throughout the application"
|
||||
- "Refactor the authentication system to use JWT tokens"
|
||||
- "Add unit tests for all utility functions"
|
||||
|
||||
## 📊 Logging
|
||||
|
||||
The server provides comprehensive logging:
|
||||
- **Console Output**: Real-time logs in the terminal
|
||||
- **File Logging**: Logs saved to `mcp_server.log`
|
||||
- **Task-specific Logging**: Each task has detailed logging with task ID
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
# View container logs
|
||||
docker logs <container_id>
|
||||
|
||||
# Follow logs in real-time
|
||||
docker logs -f <container_id>
|
||||
```
|
||||
|
||||
**Local:**
|
||||
```bash
|
||||
# View log file
|
||||
tail -f mcp_server.log
|
||||
```
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
- API keys are sent from frontend and not stored
|
||||
- Use HTTPS in production
|
||||
- Implement proper authentication for the web interface
|
||||
- Regularly update dependencies
|
||||
- Monitor API usage and costs
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Repository cloning fails**
|
||||
- Verify Gitea token is valid and has repository access
|
||||
- Check repository URL format
|
||||
- Ensure repository exists and is accessible
|
||||
- Make sure token has appropriate permissions (read/write)
|
||||
|
||||
2. **AI model errors**
|
||||
- Verify API keys are correct
|
||||
- Check model name spelling
|
||||
- Ensure internet connectivity
|
||||
|
||||
3. **Gemini CLI not found**
|
||||
- Install Gemini CLI: `curl -L https://github.com/google/generative-ai-go/releases/latest/download/gemini-linux-amd64 -o /usr/local/bin/gemini && chmod +x /usr/local/bin/gemini`
|
||||
|
||||
### Logs
|
||||
|
||||
Check the logs for detailed error messages and processing status:
|
||||
- **Frontend**: Real-time logs in the web interface
|
||||
- **Backend**: Console and file logs with detailed information
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check the troubleshooting section
|
||||
2. Review the logs in the web interface and console
|
||||
3. Create an issue in the repository
|
||||
|
||||
---
|
||||
|
||||
**Note**: This tool modifies code automatically. Always review changes before deploying to production environments.
|
||||
47
create_gitea_token.md
Normal file
47
create_gitea_token.md
Normal file
@ -0,0 +1,47 @@
|
||||
# How to Create a Gitea Token
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### 1. Log into your Gitea instance
|
||||
- Go to your Gitea URL (e.g., `http://157.66.191.31:3000`)
|
||||
- Log in with your username and password
|
||||
|
||||
### 2. Navigate to Settings
|
||||
- Click on your profile picture in the top-right corner
|
||||
- Select "Settings" from the dropdown menu
|
||||
|
||||
### 3. Go to Applications
|
||||
- In the left sidebar, click on "Applications"
|
||||
- Look for "Manage Access Tokens" or "Tokens"
|
||||
|
||||
### 4. Generate New Token
|
||||
- Click "Generate new token"
|
||||
- Fill in the form:
|
||||
- **Token Name**: Give it a descriptive name (e.g., "MCP Server")
|
||||
- **Scopes**: Select the following permissions:
|
||||
- ✅ `repo` (Full control of private repositories)
|
||||
- ✅ `write:packages` (Write packages)
|
||||
- ✅ `read:packages` (Read packages)
|
||||
|
||||
### 5. Copy the Token
|
||||
- Click "Generate token"
|
||||
- **IMPORTANT**: Copy the token immediately - you won't be able to see it again!
|
||||
- The token will look like: `37c322628fa57b0ec7b481c8655ae2bebd486f6f`
|
||||
|
||||
### 6. Use in MCP Server
|
||||
- Paste the token in the "Gitea Token" field in the MCP Server interface
|
||||
- The token is pre-filled with the example token for testing
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never share your token** - it provides access to your repositories
|
||||
- **Store tokens securely** - don't commit them to version control
|
||||
- **Use different tokens** for different applications
|
||||
- **Revoke tokens** when no longer needed
|
||||
|
||||
## Example Token (for testing)
|
||||
```
|
||||
37c322628fa57b0ec7b481c8655ae2bebd486f6f
|
||||
```
|
||||
|
||||
This token is already pre-filled in the MCP Server interface for convenience.
|
||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcp-server:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- PORT=8000
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- mcp-data:/app/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
mcp-data:
|
||||
20
env.example
Normal file
20
env.example
Normal file
@ -0,0 +1,20 @@
|
||||
# MCP Server Environment Configuration
|
||||
|
||||
# AI Model API Keys
|
||||
# Get your Gemini API key from: https://makersuite.google.com/app/apikey
|
||||
GEMINI_API_KEY=your_gemini_api_key_here
|
||||
|
||||
# Get your OpenAI API key from: https://platform.openai.com/api-keys
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# Server Configuration
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# Gitea Configuration (default values)
|
||||
DEFAULT_GITEA_URL=http://157.66.191.31:3000
|
||||
DEFAULT_USERNAME=risadmin_prod
|
||||
DEFAULT_PASSWORD=adminprod1234
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
390
main.py
Normal file
390
main.py
Normal file
@ -0,0 +1,390 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
import git
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('mcp_server.log')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="MCP Server", description="AI-powered code editing server")
|
||||
|
||||
# Mount static files and templates
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# Models
|
||||
class GiteaRequest(BaseModel):
|
||||
repo_url: str
|
||||
token: str # Gitea token instead of username/password
|
||||
prompt: str
|
||||
ai_model: str = "gemini" # gemini or openai
|
||||
model_name: str = "gemini-1.5-pro" # specific model name
|
||||
api_key: str # API key from frontend
|
||||
|
||||
class ProcessResponse(BaseModel):
|
||||
task_id: str
|
||||
status: str
|
||||
message: str
|
||||
|
||||
# Global storage for task status
|
||||
task_status = {}
|
||||
|
||||
class MCPServer:
|
||||
def __init__(self):
|
||||
self.repo_path = None
|
||||
|
||||
async def process_repository(self, task_id: str, request: GiteaRequest):
|
||||
"""Main processing function"""
|
||||
try:
|
||||
logger.info(f"Task {task_id}: Starting process...")
|
||||
task_status[task_id] = {"status": "processing", "message": "Starting process..."}
|
||||
|
||||
# Step 1: Clone repository
|
||||
await self._clone_repository(task_id, request)
|
||||
|
||||
# Step 2: Analyze code with AI
|
||||
await self._analyze_with_ai(task_id, request)
|
||||
|
||||
# Step 3: Commit and push changes
|
||||
await self._commit_and_push(task_id, request)
|
||||
|
||||
logger.info(f"Task {task_id}: Successfully processed repository")
|
||||
task_status[task_id] = {"status": "completed", "message": "Successfully processed repository"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Task {task_id}: Error - {str(e)}")
|
||||
task_status[task_id] = {"status": "error", "message": str(e)}
|
||||
# Do not delete the repo directory; keep for inspection
|
||||
|
||||
async def _clone_repository(self, task_id: str, request: GiteaRequest):
|
||||
"""Clone repository from Gitea into a persistent directory"""
|
||||
logger.info(f"Task {task_id}: Cloning repository...")
|
||||
task_status[task_id] = {"status": "processing", "message": "Cloning repository..."}
|
||||
|
||||
# Extract repo name from URL
|
||||
repo_name = request.repo_url.split('/')[-1].replace('.git', '')
|
||||
# Persistent directory under /app/data
|
||||
data_dir = "/app/data"
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
self.repo_path = os.path.join(data_dir, f"{repo_name}_{task_id}")
|
||||
try:
|
||||
os.chmod(data_dir, 0o777) # Give full permissions to the data dir
|
||||
logger.info(f"Task {task_id}: Created/using data directory: {self.repo_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Task {task_id}: Could not set permissions on data dir: {e}")
|
||||
|
||||
# Clone repository using git command with credentials
|
||||
try:
|
||||
# Use git command with credentials in URL
|
||||
auth_url = request.repo_url.replace('://', f'://{request.token}@')
|
||||
|
||||
result = subprocess.run(
|
||||
['git', 'clone', auth_url, self.repo_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5 minutes timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Task {task_id}: Git clone error - {result.stderr}")
|
||||
raise Exception(f"Failed to clone repository: {result.stderr}")
|
||||
|
||||
logger.info(f"Task {task_id}: Successfully cloned repository to {self.repo_path}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("Repository cloning timed out after 5 minutes")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to clone repository: {str(e)}")
|
||||
|
||||
async def _analyze_with_ai(self, task_id: str, request: GiteaRequest):
|
||||
"""Analyze code with AI model and apply changes"""
|
||||
logger.info(f"Task {task_id}: Analyzing code with AI...")
|
||||
task_status[task_id] = {"status": "processing", "message": "Analyzing code with AI..."}
|
||||
|
||||
if request.ai_model == "gemini":
|
||||
await self._use_gemini_cli(task_id, request.prompt, request.api_key, request.model_name)
|
||||
elif request.ai_model == "openai":
|
||||
await self._use_openai_ai(task_id, request.prompt, request.api_key, request.model_name)
|
||||
else:
|
||||
raise Exception(f"Unsupported AI model: {request.ai_model}")
|
||||
|
||||
async def _use_gemini_cli(self, task_id: str, prompt: str, api_key: str, model_name: str):
|
||||
"""Use Gemini CLI for code analysis and editing"""
|
||||
try:
|
||||
# Check if Gemini CLI is installed
|
||||
try:
|
||||
subprocess.run(["gemini", "--version"], check=True, capture_output=True)
|
||||
logger.info(f"Task {task_id}: Gemini CLI is available")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
raise Exception("Gemini CLI is not installed. Please install it first: https://github.com/google/generative-ai-go/tree/main/cmd/gemini")
|
||||
|
||||
# Read all code files
|
||||
code_content = self._read_code_files()
|
||||
logger.info(f"Task {task_id}: Read {len(code_content)} characters of code content")
|
||||
|
||||
# Create AI prompt
|
||||
ai_prompt = f"""
|
||||
Analyze the following codebase and make the requested changes:
|
||||
|
||||
USER REQUEST: {prompt}
|
||||
|
||||
CODEBASE:
|
||||
{code_content}
|
||||
|
||||
Please provide:
|
||||
1. A summary of what changes need to be made
|
||||
2. The specific file changes in the format:
|
||||
FILE: filename.py
|
||||
CHANGES:
|
||||
[describe changes or provide new code]
|
||||
|
||||
Be specific about which files to modify and what changes to make.
|
||||
"""
|
||||
|
||||
# Set API key as environment variable for Gemini CLI
|
||||
env = os.environ.copy()
|
||||
env['GEMINI_API_KEY'] = api_key
|
||||
|
||||
logger.info(f"Task {task_id}: Calling Gemini CLI with model: {model_name}")
|
||||
|
||||
# Call Gemini CLI with specific model, passing prompt via stdin
|
||||
result = subprocess.run(
|
||||
["gemini", "generate", "--model", model_name],
|
||||
input=ai_prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
cwd=self.repo_path,
|
||||
timeout=600 # 10 minutes timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Task {task_id}: Gemini CLI error - {result.stderr}")
|
||||
raise Exception(f"Gemini CLI error: {result.stderr}")
|
||||
|
||||
logger.info(f"Task {task_id}: Gemini CLI response received ({len(result.stdout)} characters)")
|
||||
logger.info(f"Task {task_id}: Gemini CLI raw response:\n{result.stdout}")
|
||||
# Store the raw AI response for frontend display
|
||||
task_status[task_id]["ai_response"] = result.stdout
|
||||
# Parse and apply changes
|
||||
await self._apply_ai_changes(result.stdout, task_id)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("Gemini CLI request timed out after 10 minutes")
|
||||
except Exception as e:
|
||||
raise Exception(f"Gemini CLI error: {str(e)}")
|
||||
|
||||
async def _use_openai_ai(self, task_id: str, prompt: str, api_key: str, model_name: str):
|
||||
"""Use OpenAI for code analysis and editing"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
# Configure OpenAI with API key from frontend
|
||||
client = OpenAI(api_key=api_key)
|
||||
|
||||
# Read all code files
|
||||
code_content = self._read_code_files()
|
||||
logger.info(f"Task {task_id}: Read {len(code_content)} characters of code content")
|
||||
|
||||
# Create AI prompt
|
||||
ai_prompt = f"""
|
||||
Analyze the following codebase and make the requested changes:
|
||||
|
||||
USER REQUEST: {prompt}
|
||||
|
||||
CODEBASE:
|
||||
{code_content}
|
||||
|
||||
Please provide:
|
||||
1. A summary of what changes need to be made
|
||||
2. The specific file changes in the format:
|
||||
FILE: filename.py
|
||||
CHANGES:
|
||||
[describe changes or provide new code]
|
||||
|
||||
Be specific about which files to modify and what changes to make.
|
||||
"""
|
||||
|
||||
logger.info(f"Task {task_id}: Calling OpenAI with model: {model_name}")
|
||||
|
||||
# Get AI response
|
||||
response = client.chat.completions.create(
|
||||
model=model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a code analysis and editing assistant."},
|
||||
{"role": "user", "content": ai_prompt}
|
||||
]
|
||||
)
|
||||
|
||||
logger.info(f"Task {task_id}: OpenAI response received")
|
||||
|
||||
# Parse and apply changes
|
||||
await self._apply_ai_changes(response.choices[0].message.content, task_id)
|
||||
|
||||
except ImportError:
|
||||
raise Exception("OpenAI library not installed. Run: pip install openai")
|
||||
except Exception as e:
|
||||
raise Exception(f"OpenAI error: {str(e)}")
|
||||
|
||||
def _read_code_files(self) -> str:
|
||||
"""Read all code files in the repository"""
|
||||
code_content = ""
|
||||
file_count = 0
|
||||
|
||||
for root, dirs, files in os.walk(self.repo_path):
|
||||
# Skip .git directory
|
||||
if '.git' in dirs:
|
||||
dirs.remove('.git')
|
||||
|
||||
for file in files:
|
||||
if file.endswith(('.py', '.js', '.ts', '.jsx', '.tsx', '.html', '.css', '.json', '.md')):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
relative_path = os.path.relpath(file_path, self.repo_path)
|
||||
code_content += f"\n\n=== {relative_path} ===\n{content}\n"
|
||||
file_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not read {file_path}: {e}")
|
||||
|
||||
logger.info(f"Read {file_count} code files")
|
||||
return code_content
|
||||
|
||||
async def _apply_ai_changes(self, ai_response: str, task_id: str):
|
||||
"""Apply changes suggested by AI"""
|
||||
logger.info(f"Task {task_id}: Applying AI suggestions...")
|
||||
task_status[task_id] = {"status": "processing", "message": "Applying AI suggestions..."}
|
||||
|
||||
# Parse AI response for file changes
|
||||
# This is a simplified parser - you might want to make it more robust
|
||||
lines = ai_response.split('\n')
|
||||
current_file = None
|
||||
current_changes = []
|
||||
files_modified = 0
|
||||
|
||||
for line in lines:
|
||||
if line.startswith('FILE:'):
|
||||
if current_file and current_changes:
|
||||
await self._apply_file_changes(current_file, '\n'.join(current_changes))
|
||||
files_modified += 1
|
||||
current_file = line.replace('FILE:', '').strip()
|
||||
current_changes = []
|
||||
elif line.startswith('CHANGES:') or line.strip() == '':
|
||||
continue
|
||||
elif current_file:
|
||||
current_changes.append(line)
|
||||
|
||||
# Apply last file changes
|
||||
if current_file and current_changes:
|
||||
await self._apply_file_changes(current_file, '\n'.join(current_changes))
|
||||
files_modified += 1
|
||||
|
||||
logger.info(f"Task {task_id}: Applied changes to {files_modified} files")
|
||||
|
||||
async def _apply_file_changes(self, filename: str, changes: str):
|
||||
"""Apply changes to a specific file"""
|
||||
file_path = os.path.join(self.repo_path, filename)
|
||||
|
||||
if os.path.exists(file_path):
|
||||
# For now, we'll append the changes to the file
|
||||
# In a real implementation, you'd want more sophisticated parsing
|
||||
with open(file_path, 'a', encoding='utf-8') as f:
|
||||
f.write(f"\n\n# AI Generated Changes:\n{changes}\n")
|
||||
logger.info(f"Applied changes to file: {filename}")
|
||||
|
||||
async def _commit_and_push(self, task_id: str, request: GiteaRequest):
|
||||
"""Commit and push changes back to Gitea"""
|
||||
logger.info(f"Task {task_id}: Committing and pushing changes...")
|
||||
task_status[task_id] = {"status": "processing", "message": "Committing and pushing changes..."}
|
||||
|
||||
try:
|
||||
repo = git.Repo(self.repo_path)
|
||||
|
||||
# Add all changes
|
||||
repo.git.add('.')
|
||||
|
||||
# Check if there are changes to commit
|
||||
if repo.is_dirty():
|
||||
# Commit changes
|
||||
repo.index.commit("AI-generated code updates")
|
||||
logger.info(f"Task {task_id}: Changes committed")
|
||||
|
||||
# Push changes
|
||||
origin = repo.remote(name='origin')
|
||||
origin.push()
|
||||
logger.info(f"Task {task_id}: Changes pushed to remote")
|
||||
else:
|
||||
logger.info(f"Task {task_id}: No changes to commit")
|
||||
# Remove the cloned repo directory after push
|
||||
if self.repo_path and os.path.exists(self.repo_path):
|
||||
shutil.rmtree(self.repo_path)
|
||||
logger.info(f"Task {task_id}: Removed cloned repo directory {self.repo_path}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to commit and push changes: {str(e)}")
|
||||
|
||||
# Create MCP server instance
|
||||
mcp_server = MCPServer()
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def read_root(request: Request):
|
||||
"""Serve the frontend"""
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@app.post("/process", response_model=ProcessResponse)
|
||||
async def process_repository(request: GiteaRequest, background_tasks: BackgroundTasks):
|
||||
"""Process repository with AI"""
|
||||
import uuid
|
||||
|
||||
task_id = str(uuid.uuid4())
|
||||
logger.info(f"Starting new task: {task_id}")
|
||||
|
||||
# Start background task
|
||||
background_tasks.add_task(mcp_server.process_repository, task_id, request)
|
||||
|
||||
return ProcessResponse(
|
||||
task_id=task_id,
|
||||
status="started",
|
||||
message="Processing started"
|
||||
)
|
||||
|
||||
@app.get("/status/{task_id}")
|
||||
async def get_status(task_id: str):
|
||||
"""Get status of a processing task"""
|
||||
if task_id not in task_status:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return task_status[task_id]
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "message": "MCP Server is running"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@ -0,0 +1,11 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
python-multipart==0.0.6
|
||||
requests==2.31.0
|
||||
gitpython==3.1.40
|
||||
google-generativeai==0.3.2
|
||||
openai==1.3.7
|
||||
python-dotenv==1.0.0
|
||||
pydantic==2.5.0
|
||||
aiofiles==23.2.1
|
||||
jinja2==3.1.2
|
||||
65
start.bat
Normal file
65
start.bat
Normal file
@ -0,0 +1,65 @@
|
||||
@echo off
|
||||
echo 🤖 MCP Server - AI-Powered Code Editor
|
||||
echo ================================================
|
||||
echo.
|
||||
|
||||
REM Check if Python is installed
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Error: Python is not installed or not in PATH
|
||||
echo Please install Python 3.8+ from https://python.org
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if pip is available
|
||||
python -m pip --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Error: pip is not available
|
||||
echo Please ensure pip is installed with Python
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ Python is installed
|
||||
echo.
|
||||
|
||||
REM Install dependencies
|
||||
echo 📦 Installing Python dependencies...
|
||||
python -m pip install -r requirements.txt
|
||||
if errorlevel 1 (
|
||||
echo ❌ Error installing dependencies
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ Dependencies installed successfully
|
||||
echo.
|
||||
|
||||
REM Check for .env file
|
||||
if not exist .env (
|
||||
if exist env.example (
|
||||
copy env.example .env >nul
|
||||
echo ✅ Created .env file from template
|
||||
echo ⚠️ Please edit .env file and add your API keys
|
||||
) else (
|
||||
echo ⚠️ No .env file found and no template available
|
||||
)
|
||||
) else (
|
||||
echo ✅ .env file already exists
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 🚀 Starting MCP Server...
|
||||
echo 📱 Web interface will be available at: http://localhost:8000
|
||||
echo 🔧 API documentation at: http://localhost:8000/docs
|
||||
echo.
|
||||
echo Press Ctrl+C to stop the server
|
||||
echo.
|
||||
|
||||
REM Start the server
|
||||
python main.py
|
||||
|
||||
echo.
|
||||
echo 👋 Server stopped
|
||||
pause
|
||||
122
start.py
Normal file
122
start.py
Normal file
@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP Server Startup Script
|
||||
Handles environment setup and server initialization
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def check_python_version():
|
||||
"""Check if Python version is compatible"""
|
||||
if sys.version_info < (3, 8):
|
||||
print("❌ Error: Python 3.8 or higher is required")
|
||||
print(f"Current version: {sys.version}")
|
||||
sys.exit(1)
|
||||
print(f"✅ Python version: {sys.version.split()[0]}")
|
||||
|
||||
def check_git():
|
||||
"""Check if Git is installed"""
|
||||
try:
|
||||
subprocess.run(["git", "--version"], check=True, capture_output=True)
|
||||
print("✅ Git is installed")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("❌ Error: Git is not installed or not in PATH")
|
||||
print("Please install Git: https://git-scm.com/")
|
||||
sys.exit(1)
|
||||
|
||||
def check_node():
|
||||
"""Check if Node.js is installed (optional)"""
|
||||
try:
|
||||
subprocess.run(["node", "--version"], check=True, capture_output=True)
|
||||
print("✅ Node.js is installed")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("⚠️ Warning: Node.js is not installed")
|
||||
print("This is optional but recommended for projects with package.json")
|
||||
|
||||
def install_dependencies():
|
||||
"""Install Python dependencies"""
|
||||
print("\n📦 Installing Python dependencies...")
|
||||
try:
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True)
|
||||
print("✅ Dependencies installed successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Error installing dependencies: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def setup_environment():
|
||||
"""Set up environment file"""
|
||||
env_file = Path(".env")
|
||||
env_example = Path("env.example")
|
||||
|
||||
if not env_file.exists():
|
||||
if env_example.exists():
|
||||
shutil.copy(env_example, env_file)
|
||||
print("✅ Created .env file from template")
|
||||
print("⚠️ Please edit .env file and add your API keys")
|
||||
else:
|
||||
print("⚠️ No .env file found and no template available")
|
||||
else:
|
||||
print("✅ .env file already exists")
|
||||
|
||||
def check_api_keys():
|
||||
"""Check if API keys are configured"""
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
gemini_key = os.getenv("GEMINI_API_KEY")
|
||||
openai_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
if not gemini_key and not openai_key:
|
||||
print("⚠️ Warning: No API keys configured")
|
||||
print("Please add GEMINI_API_KEY or OPENAI_API_KEY to .env file")
|
||||
print("You can still start the server, but AI features won't work")
|
||||
else:
|
||||
if gemini_key:
|
||||
print("✅ Gemini API key configured")
|
||||
if openai_key:
|
||||
print("✅ OpenAI API key configured")
|
||||
|
||||
def create_directories():
|
||||
"""Create necessary directories"""
|
||||
directories = ["templates", "static"]
|
||||
for directory in directories:
|
||||
Path(directory).mkdir(exist_ok=True)
|
||||
print("✅ Directories created")
|
||||
|
||||
def start_server():
|
||||
"""Start the FastAPI server"""
|
||||
print("\n🚀 Starting MCP Server...")
|
||||
print("📱 Web interface will be available at: http://localhost:8000")
|
||||
print("🔧 API documentation at: http://localhost:8000/docs")
|
||||
print("\nPress Ctrl+C to stop the server")
|
||||
|
||||
try:
|
||||
subprocess.run([sys.executable, "main.py"])
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Server stopped")
|
||||
|
||||
def main():
|
||||
"""Main startup function"""
|
||||
print("🤖 MCP Server - AI-Powered Code Editor")
|
||||
print("=" * 50)
|
||||
|
||||
# Pre-flight checks
|
||||
check_python_version()
|
||||
check_git()
|
||||
check_node()
|
||||
|
||||
# Setup
|
||||
create_directories()
|
||||
setup_environment()
|
||||
install_dependencies()
|
||||
check_api_keys()
|
||||
|
||||
# Start server
|
||||
start_server()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
415
static/script.js
Normal file
415
static/script.js
Normal file
@ -0,0 +1,415 @@
|
||||
// MCP Server Frontend JavaScript
|
||||
|
||||
class MCPServerFrontend {
|
||||
constructor() {
|
||||
this.currentTaskId = null;
|
||||
this.statusInterval = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.setDefaultValues();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Process button
|
||||
document.getElementById('processBtn').addEventListener('click', () => {
|
||||
this.processRepository();
|
||||
});
|
||||
|
||||
// Clear log button
|
||||
document.getElementById('clearLog').addEventListener('click', () => {
|
||||
this.clearLog();
|
||||
});
|
||||
|
||||
// Toggle API key visibility
|
||||
document.getElementById('toggleApiKey').addEventListener('click', () => {
|
||||
this.toggleApiKeyVisibility();
|
||||
});
|
||||
|
||||
// Toggle Gitea token visibility
|
||||
document.getElementById('toggleGiteaToken').addEventListener('click', () => {
|
||||
this.toggleGiteaTokenVisibility();
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('repoUrl').addEventListener('input', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
|
||||
document.getElementById('giteaToken').addEventListener('input', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
|
||||
document.getElementById('prompt').addEventListener('input', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
|
||||
document.getElementById('apiKey').addEventListener('input', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
|
||||
document.getElementById('modelName').addEventListener('input', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
}
|
||||
|
||||
setDefaultValues() {
|
||||
// Set default values for testing
|
||||
document.getElementById('repoUrl').value = 'http://157.66.191.31:3000/user/repo.git';
|
||||
document.getElementById('giteaToken').value = '37c322628fa57b0ec7b481c8655ae2bebd486f6f';
|
||||
document.getElementById('aiModel').value = 'gemini';
|
||||
}
|
||||
|
||||
toggleApiKeyVisibility() {
|
||||
const apiKeyInput = document.getElementById('apiKey');
|
||||
const toggleBtn = document.getElementById('toggleApiKey');
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
|
||||
if (apiKeyInput.type === 'password') {
|
||||
apiKeyInput.type = 'text';
|
||||
icon.className = 'fas fa-eye-slash';
|
||||
} else {
|
||||
apiKeyInput.type = 'password';
|
||||
icon.className = 'fas fa-eye';
|
||||
}
|
||||
}
|
||||
|
||||
toggleGiteaTokenVisibility() {
|
||||
const giteaTokenInput = document.getElementById('giteaToken');
|
||||
const toggleBtn = document.getElementById('toggleGiteaToken');
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
|
||||
if (giteaTokenInput.type === 'password') {
|
||||
giteaTokenInput.type = 'text';
|
||||
icon.className = 'fas fa-eye-slash';
|
||||
} else {
|
||||
giteaTokenInput.type = 'password';
|
||||
icon.className = 'fas fa-eye';
|
||||
}
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
const repoUrl = document.getElementById('repoUrl').value.trim();
|
||||
const giteaToken = document.getElementById('giteaToken').value.trim();
|
||||
const prompt = document.getElementById('prompt').value.trim();
|
||||
const apiKey = document.getElementById('apiKey').value.trim();
|
||||
const modelName = document.getElementById('modelName').value.trim();
|
||||
|
||||
const isValid = repoUrl && giteaToken && prompt && apiKey && modelName;
|
||||
const processBtn = document.getElementById('processBtn');
|
||||
|
||||
processBtn.disabled = !isValid;
|
||||
|
||||
if (isValid) {
|
||||
processBtn.classList.remove('btn-secondary');
|
||||
processBtn.classList.add('btn-primary');
|
||||
} else {
|
||||
processBtn.classList.remove('btn-primary');
|
||||
processBtn.classList.add('btn-secondary');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
async processRepository() {
|
||||
if (!this.validateForm()) {
|
||||
this.addLogEntry('error', 'Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
repo_url: document.getElementById('repoUrl').value.trim(),
|
||||
token: document.getElementById('giteaToken').value.trim(),
|
||||
prompt: document.getElementById('prompt').value.trim(),
|
||||
ai_model: document.getElementById('aiModel').value,
|
||||
model_name: document.getElementById('modelName').value.trim(),
|
||||
api_key: document.getElementById('apiKey').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
// Disable form
|
||||
this.setFormEnabled(false);
|
||||
this.showProgressCard();
|
||||
this.updateStatus('Processing...', 'processing');
|
||||
this.addLogEntry('info', 'Starting repository processing...');
|
||||
|
||||
// Show loading modal
|
||||
const loadingModal = new bootstrap.Modal(document.getElementById('loadingModal'));
|
||||
loadingModal.show();
|
||||
|
||||
// Send request to backend
|
||||
const response = await fetch('/process', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.currentTaskId = result.task_id;
|
||||
|
||||
this.addLogEntry('success', `Task started with ID: ${result.task_id}`);
|
||||
this.addLogEntry('info', 'Monitoring task progress...');
|
||||
|
||||
// Start monitoring
|
||||
this.startStatusMonitoring();
|
||||
|
||||
// Hide loading modal
|
||||
loadingModal.hide();
|
||||
|
||||
} catch (error) {
|
||||
this.addLogEntry('error', `Failed to start processing: ${error.message}`);
|
||||
this.updateStatus('Error', 'error');
|
||||
this.setFormEnabled(true);
|
||||
this.hideProgressCard();
|
||||
|
||||
// Hide loading modal
|
||||
const loadingModal = bootstrap.Modal.getInstance(document.getElementById('loadingModal'));
|
||||
if (loadingModal) {
|
||||
loadingModal.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startStatusMonitoring() {
|
||||
if (this.statusInterval) {
|
||||
clearInterval(this.statusInterval);
|
||||
}
|
||||
|
||||
this.statusInterval = setInterval(async () => {
|
||||
if (!this.currentTaskId) {
|
||||
this.stopStatusMonitoring();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/status/${this.currentTaskId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
this.updateProgress(status);
|
||||
this.addLogEntry('info', status.message);
|
||||
|
||||
if (status.status === 'completed' || status.status === 'error') {
|
||||
this.stopStatusMonitoring();
|
||||
this.handleTaskCompletion(status);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.addLogEntry('error', `Failed to get status: ${error.message}`);
|
||||
this.stopStatusMonitoring();
|
||||
}
|
||||
}, 2000); // Check every 2 seconds
|
||||
}
|
||||
|
||||
stopStatusMonitoring() {
|
||||
if (this.statusInterval) {
|
||||
clearInterval(this.statusInterval);
|
||||
this.statusInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(status) {
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const steps = document.querySelectorAll('.step');
|
||||
|
||||
// Show AI output if available
|
||||
if (status.ai_response) {
|
||||
document.getElementById('aiOutputCard').style.display = 'block';
|
||||
document.getElementById('aiOutput').textContent = status.ai_response;
|
||||
} else {
|
||||
document.getElementById('aiOutputCard').style.display = 'none';
|
||||
document.getElementById('aiOutput').textContent = '';
|
||||
}
|
||||
|
||||
// Step order (no deps)
|
||||
const stepOrder = ['clone', 'ai', 'commit'];
|
||||
let currentStep = 0;
|
||||
let errorStep = -1;
|
||||
|
||||
// Determine current step and error
|
||||
if (status.message.includes('Cloning')) {
|
||||
currentStep = 0;
|
||||
} else if (status.message.includes('Analyzing') || status.message.includes('AI')) {
|
||||
currentStep = 1;
|
||||
} else if (status.message.includes('Committing') || status.message.includes('Push')) {
|
||||
currentStep = 2;
|
||||
} else if (status.status === 'completed') {
|
||||
currentStep = 3;
|
||||
} else if (status.status === 'error') {
|
||||
// Try to find which step failed
|
||||
if (status.message.includes('Clone')) errorStep = 0;
|
||||
else if (status.message.includes('AI') || status.message.includes('Gemini') || status.message.includes('OpenAI')) errorStep = 1;
|
||||
else if (status.message.includes('Commit') || status.message.includes('Push')) errorStep = 2;
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
let progress = (currentStep / stepOrder.length) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
|
||||
// Update step indicators with color, icon, and label
|
||||
steps.forEach((step, idx) => {
|
||||
const stepName = step.dataset.step;
|
||||
step.classList.remove('active', 'completed', 'error', 'pending');
|
||||
const iconSpan = step.querySelector('.step-icon');
|
||||
const labelSpan = step.querySelector('.step-label');
|
||||
|
||||
// Set icon and label
|
||||
if (errorStep === idx) {
|
||||
step.classList.add('error');
|
||||
iconSpan.innerHTML = '✖';
|
||||
labelSpan.textContent = 'Error';
|
||||
} else if (idx < currentStep) {
|
||||
step.classList.add('completed');
|
||||
iconSpan.innerHTML = '✔';
|
||||
labelSpan.textContent = 'Completed';
|
||||
} else if (idx === currentStep && status.status !== 'completed' && status.status !== 'error') {
|
||||
step.classList.add('active');
|
||||
iconSpan.innerHTML = '<span class="spinner-grow spinner-grow-sm" style="width:1em;height:1em;"></span>';
|
||||
labelSpan.textContent = 'In Progress';
|
||||
} else {
|
||||
step.classList.add('pending');
|
||||
iconSpan.innerHTML = '●';
|
||||
labelSpan.textContent = 'Pending';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isStepCompleted(stepName, activeStep) {
|
||||
const stepOrder = ['clone', 'deps', 'ai', 'commit'];
|
||||
const stepIndex = stepOrder.indexOf(stepName);
|
||||
const activeIndex = stepOrder.indexOf(activeStep);
|
||||
|
||||
return stepIndex < activeIndex;
|
||||
}
|
||||
|
||||
handleTaskCompletion(status) {
|
||||
if (status.status === 'completed') {
|
||||
this.updateStatus('Completed', 'completed');
|
||||
this.addLogEntry('success', 'Repository processing completed successfully!');
|
||||
this.addLogEntry('info', 'Changes have been committed and pushed to the repository.');
|
||||
} else {
|
||||
this.updateStatus('Error', 'error');
|
||||
this.addLogEntry('error', `Processing failed: ${status.message}`);
|
||||
}
|
||||
|
||||
this.setFormEnabled(true);
|
||||
this.currentTaskId = null;
|
||||
}
|
||||
|
||||
updateStatus(text, type) {
|
||||
const statusText = document.querySelector('.status-text');
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
|
||||
statusText.textContent = text;
|
||||
|
||||
// Update status dot color
|
||||
statusDot.className = 'status-dot';
|
||||
switch (type) {
|
||||
case 'processing':
|
||||
statusDot.style.backgroundColor = '#ffc107';
|
||||
break;
|
||||
case 'completed':
|
||||
statusDot.style.backgroundColor = '#28a745';
|
||||
break;
|
||||
case 'error':
|
||||
statusDot.style.backgroundColor = '#dc3545';
|
||||
break;
|
||||
default:
|
||||
statusDot.style.backgroundColor = '#17a2b8';
|
||||
}
|
||||
}
|
||||
|
||||
addLogEntry(type, message) {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry ${type}`;
|
||||
logEntry.innerHTML = `
|
||||
<span class="timestamp">[${timestamp}]</span>
|
||||
<span class="message">${message}</span>
|
||||
`;
|
||||
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry info">
|
||||
<span class="timestamp">[System]</span>
|
||||
<span class="message">Log cleared. Ready for new operations.</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setFormEnabled(enabled) {
|
||||
const formElements = [
|
||||
'repoUrl', 'giteaToken', 'prompt', 'aiModel', 'modelName', 'apiKey', 'processBtn'
|
||||
];
|
||||
|
||||
formElements.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.disabled = !enabled;
|
||||
}
|
||||
});
|
||||
|
||||
const processBtn = document.getElementById('processBtn');
|
||||
if (enabled) {
|
||||
processBtn.innerHTML = '<i class="fas fa-play"></i> Process Repository';
|
||||
} else {
|
||||
processBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
|
||||
}
|
||||
}
|
||||
|
||||
showProgressCard() {
|
||||
const progressCard = document.getElementById('progressCard');
|
||||
progressCard.style.display = 'block';
|
||||
|
||||
// Reset progress
|
||||
document.getElementById('progressBar').style.width = '0%';
|
||||
document.querySelectorAll('.step').forEach(step => {
|
||||
step.classList.remove('active', 'completed');
|
||||
});
|
||||
}
|
||||
|
||||
hideProgressCard() {
|
||||
const progressCard = document.getElementById('progressCard');
|
||||
progressCard.style.display = 'none';
|
||||
}
|
||||
|
||||
// Utility function to format error messages
|
||||
formatError(error) {
|
||||
if (error.response) {
|
||||
return `Server error: ${error.response.status} - ${error.response.statusText}`;
|
||||
} else if (error.request) {
|
||||
return 'Network error: Unable to connect to server';
|
||||
} else {
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the frontend when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.mcpFrontend = new MCPServerFrontend();
|
||||
});
|
||||
|
||||
// Handle page unload to clean up intervals
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.mcpFrontend) {
|
||||
window.mcpFrontend.stopStatusMonitoring();
|
||||
}
|
||||
});
|
||||
342
static/style.css
Normal file
342
static/style.css
Normal file
@ -0,0 +1,342 @@
|
||||
/* Custom CSS for MCP Server */
|
||||
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #28a745;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #343a40;
|
||||
--sidebar-width: 350px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 2rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
background: white;
|
||||
box-shadow: 0 0 0 0.2rem rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, #007bff, #0056b3);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0,123,255,0.4);
|
||||
}
|
||||
|
||||
/* Main Content Styles */
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
margin: 0;
|
||||
color: var(--dark-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--success-color);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: 500;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Progress Steps */
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #e9ecef;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step.active:not(:last-child)::after {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.step i {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #e9ecef;
|
||||
color: var(--secondary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.step.active i {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step.completed i {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step span {
|
||||
font-size: 0.8rem;
|
||||
color: var(--secondary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step.active span {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.step.completed span {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
/* Log Container */
|
||||
.log-container {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.log-entry.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-weight: bold;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 12px 12px 0 0 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(45deg, var(--primary-color), #0056b3);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
min-height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.step:not(:last-child)::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.log-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
194
templates/index.html
Normal file
194
templates/index.html
Normal file
@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCP Server - AI Code Editor</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3><i class="fas fa-robot"></i> MCP Server</h3>
|
||||
<p class="text-muted">AI-powered code editing</p>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-cog"></i> Configuration</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repoUrl" class="form-label">Gitea Repository URL</label>
|
||||
<input type="url" class="form-control" id="repoUrl"
|
||||
placeholder="http://157.66.191.31:3000/user/repo.git">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="giteaToken" class="form-label">Gitea Token</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="giteaToken"
|
||||
placeholder="Enter your Gitea token">
|
||||
<button class="btn btn-outline-secondary" type="button" id="toggleGiteaToken">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<small>
|
||||
Get your token from Gitea: Settings → Applications → Generate new token
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-brain"></i> AI Configuration</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="aiModel" class="form-label">AI Model</label>
|
||||
<select class="form-select" id="aiModel">
|
||||
<option value="gemini">Gemini CLI</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="modelName" class="form-label">Model Name</label>
|
||||
<input type="text" class="form-control" id="modelName"
|
||||
placeholder="gemini-1.5-pro" value="gemini-1.5-pro">
|
||||
<div class="form-text">
|
||||
<small>
|
||||
<strong>Gemini:</strong> gemini-1.5-pro, gemini-1.5-flash, etc.<br>
|
||||
<strong>OpenAI:</strong> gpt-4, gpt-3.5-turbo, etc.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="apiKey" class="form-label">API Key</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="apiKey"
|
||||
placeholder="Enter your API key">
|
||||
<button class="btn btn-outline-secondary" type="button" id="toggleApiKey">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<small>
|
||||
<strong>Gemini:</strong> Get your API key from <a href="https://makersuite.google.com/app/apikey" target="_blank">Google AI Studio</a><br>
|
||||
<strong>OpenAI:</strong> Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI Platform</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-magic"></i> AI Prompt</h5>
|
||||
<div class="mb-3">
|
||||
<label for="prompt" class="form-label">What would you like to do?</label>
|
||||
<textarea class="form-control" id="prompt" rows="4"
|
||||
placeholder="Describe the changes you want to make to the code..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary w-100" id="processBtn">
|
||||
<i class="fas fa-play"></i> Process Repository
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 main-content">
|
||||
<div class="content-header">
|
||||
<h2>Repository Processing</h2>
|
||||
<div class="status-indicator" id="statusIndicator">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Progress Section -->
|
||||
<div class="card mb-4" id="progressCard" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-tasks"></i> Processing Progress</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-steps">
|
||||
<div class="step" data-step="clone">
|
||||
<span class="step-icon"></span>
|
||||
<span>Clone Repository</span>
|
||||
<span class="step-label"></span>
|
||||
</div>
|
||||
<div class="step" data-step="ai">
|
||||
<span class="step-icon"></span>
|
||||
<span>AI Analysis</span>
|
||||
<span class="step-label"></span>
|
||||
</div>
|
||||
<div class="step" data-step="commit">
|
||||
<span class="step-icon"></span>
|
||||
<span>Commit & Push</span>
|
||||
<span class="step-label"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Section -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-terminal"></i> Processing Log</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="clearLog">
|
||||
<i class="fas fa-trash"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="log-container" id="logContainer">
|
||||
<div class="log-entry info">
|
||||
<span class="timestamp">[System]</span>
|
||||
<span class="message">Ready to process repository. Fill in the details and click "Process Repository".</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Model Output Section -->
|
||||
<div class="card mt-4" id="aiOutputCard" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-robot"></i> AI Model Output</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre id="aiOutput" style="white-space: pre-wrap; word-break: break-word;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Modal -->
|
||||
<div class="modal fade" id="loadingModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>Processing Repository</h5>
|
||||
<p class="text-muted">This may take a few minutes...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
221
test_setup.py
Normal file
221
test_setup.py
Normal file
@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP Server Setup Test
|
||||
Tests all components to ensure they're working correctly
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
def test_python_version():
|
||||
"""Test Python version"""
|
||||
print("🐍 Testing Python version...")
|
||||
if sys.version_info >= (3, 8):
|
||||
print(f"✅ Python {sys.version.split()[0]} - Compatible")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Python {sys.version.split()[0]} - Incompatible (need 3.8+)")
|
||||
return False
|
||||
|
||||
def test_dependencies():
|
||||
"""Test if all required dependencies are installed"""
|
||||
print("\n📦 Testing dependencies...")
|
||||
|
||||
required_packages = [
|
||||
'fastapi',
|
||||
'uvicorn',
|
||||
'gitpython',
|
||||
'requests',
|
||||
'python-dotenv',
|
||||
'pydantic'
|
||||
]
|
||||
|
||||
optional_packages = [
|
||||
'google.generativeai',
|
||||
'openai'
|
||||
]
|
||||
|
||||
all_good = True
|
||||
|
||||
for package in required_packages:
|
||||
try:
|
||||
importlib.import_module(package)
|
||||
print(f"✅ {package}")
|
||||
except ImportError:
|
||||
print(f"❌ {package} - Missing")
|
||||
all_good = False
|
||||
|
||||
print("\n🔧 Optional packages:")
|
||||
for package in optional_packages:
|
||||
try:
|
||||
importlib.import_module(package)
|
||||
print(f"✅ {package}")
|
||||
except ImportError:
|
||||
print(f"⚠️ {package} - Not installed (AI features won't work)")
|
||||
|
||||
return all_good
|
||||
|
||||
def test_git():
|
||||
"""Test Git installation"""
|
||||
print("\n🔧 Testing Git...")
|
||||
try:
|
||||
result = subprocess.run(["git", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print(f"✅ Git: {result.stdout.strip()}")
|
||||
return True
|
||||
else:
|
||||
print("❌ Git not working properly")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("❌ Git not found in PATH")
|
||||
return False
|
||||
|
||||
def test_node():
|
||||
"""Test Node.js installation (optional)"""
|
||||
print("\n🔧 Testing Node.js...")
|
||||
try:
|
||||
result = subprocess.run(["node", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print(f"✅ Node.js: {result.stdout.strip()}")
|
||||
return True
|
||||
else:
|
||||
print("⚠️ Node.js not working properly")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("⚠️ Node.js not found (optional)")
|
||||
return False
|
||||
|
||||
def test_files():
|
||||
"""Test if all required files exist"""
|
||||
print("\n📁 Testing files...")
|
||||
|
||||
required_files = [
|
||||
'main.py',
|
||||
'requirements.txt',
|
||||
'env.example',
|
||||
'README.md'
|
||||
]
|
||||
|
||||
required_dirs = [
|
||||
'templates',
|
||||
'static'
|
||||
]
|
||||
|
||||
all_good = True
|
||||
|
||||
for file in required_files:
|
||||
if Path(file).exists():
|
||||
print(f"✅ {file}")
|
||||
else:
|
||||
print(f"❌ {file} - Missing")
|
||||
all_good = False
|
||||
|
||||
for directory in required_dirs:
|
||||
if Path(directory).exists():
|
||||
print(f"✅ {directory}/")
|
||||
else:
|
||||
print(f"❌ {directory}/ - Missing")
|
||||
all_good = False
|
||||
|
||||
return all_good
|
||||
|
||||
def test_environment():
|
||||
"""Test environment configuration"""
|
||||
print("\n🔐 Testing environment...")
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
gemini_key = os.getenv("GEMINI_API_KEY")
|
||||
openai_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
if gemini_key and gemini_key != "your_gemini_api_key_here":
|
||||
print("✅ Gemini API key configured")
|
||||
else:
|
||||
print("⚠️ Gemini API key not configured")
|
||||
|
||||
if openai_key and openai_key != "your_openai_api_key_here":
|
||||
print("✅ OpenAI API key configured")
|
||||
else:
|
||||
print("⚠️ OpenAI API key not configured")
|
||||
|
||||
if not gemini_key and not openai_key:
|
||||
print("⚠️ No AI API keys configured - AI features won't work")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_server_import():
|
||||
"""Test if the server can be imported"""
|
||||
print("\n🚀 Testing server import...")
|
||||
try:
|
||||
# Test basic import
|
||||
import main
|
||||
print("✅ Server module imports successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Server import failed: {e}")
|
||||
return False
|
||||
|
||||
def run_tests():
|
||||
"""Run all tests"""
|
||||
print("🧪 MCP Server Setup Test")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
("Python Version", test_python_version),
|
||||
("Dependencies", test_dependencies),
|
||||
("Git", test_git),
|
||||
("Node.js", test_node),
|
||||
("Files", test_files),
|
||||
("Environment", test_environment),
|
||||
("Server Import", test_server_import)
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f"❌ {test_name} test failed with error: {e}")
|
||||
results.append((test_name, False))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 Test Summary:")
|
||||
print("=" * 50)
|
||||
|
||||
passed = 0
|
||||
total = len(results)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{test_name:<20} {status}")
|
||||
if result:
|
||||
passed += 1
|
||||
|
||||
print(f"\nResults: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 All tests passed! Your MCP Server is ready to use.")
|
||||
print("\nTo start the server, run:")
|
||||
print(" python main.py")
|
||||
print(" or")
|
||||
print(" python start.py")
|
||||
else:
|
||||
print("⚠️ Some tests failed. Please fix the issues before running the server.")
|
||||
print("\nCommon fixes:")
|
||||
print("1. Install missing dependencies: pip install -r requirements.txt")
|
||||
print("2. Configure API keys in .env file")
|
||||
print("3. Install Git if not present")
|
||||
|
||||
return passed == total
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
80
view_logs.py
Normal file
80
view_logs.py
Normal file
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Real-time log viewer for MCP Server
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def follow_logs(log_file="mcp_server.log"):
|
||||
"""Follow log file in real-time"""
|
||||
log_path = Path(log_file)
|
||||
|
||||
if not log_path.exists():
|
||||
print(f"Log file {log_file} not found. Creating empty file...")
|
||||
log_path.touch()
|
||||
|
||||
print(f"Following logs from: {log_path.absolute()}")
|
||||
print("Press Ctrl+C to stop")
|
||||
print("-" * 80)
|
||||
|
||||
# Get initial file size
|
||||
with open(log_path, 'r') as f:
|
||||
f.seek(0, 2) # Go to end of file
|
||||
last_size = f.tell()
|
||||
|
||||
try:
|
||||
while True:
|
||||
with open(log_path, 'r') as f:
|
||||
f.seek(last_size)
|
||||
new_lines = f.readlines()
|
||||
if new_lines:
|
||||
for line in new_lines:
|
||||
print(line.rstrip())
|
||||
last_size = f.tell()
|
||||
|
||||
time.sleep(0.1) # Check every 100ms
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped following logs")
|
||||
|
||||
def show_recent_logs(log_file="mcp_server.log", lines=50):
|
||||
"""Show recent log lines"""
|
||||
log_path = Path(log_file)
|
||||
|
||||
if not log_path.exists():
|
||||
print(f"Log file {log_file} not found.")
|
||||
return
|
||||
|
||||
with open(log_path, 'r') as f:
|
||||
all_lines = f.readlines()
|
||||
recent_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||
|
||||
print(f"Recent {len(recent_lines)} lines from {log_path.absolute()}:")
|
||||
print("-" * 80)
|
||||
for line in recent_lines:
|
||||
print(line.rstrip())
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "follow":
|
||||
log_file = sys.argv[2] if len(sys.argv) > 2 else "mcp_server.log"
|
||||
follow_logs(log_file)
|
||||
elif command == "recent":
|
||||
log_file = sys.argv[2] if len(sys.argv) > 2 else "mcp_server.log"
|
||||
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||
show_recent_logs(log_file, lines)
|
||||
else:
|
||||
print("Usage:")
|
||||
print(" python view_logs.py follow [log_file] # Follow logs in real-time")
|
||||
print(" python view_logs.py recent [log_file] [lines] # Show recent logs")
|
||||
else:
|
||||
# Default: show recent logs
|
||||
show_recent_logs()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user