Refactor MCP server and enhance development environment

This commit refactors the MCP server implementation, updates the Dockerfile with new development tools and dependencies, introduces a Makefile for building and running the application, and adds utility modules for sandbox management and Ollama integration. The server name has also been updated from 'echo-server' to 'PMO-Crazy-coder' in the configuration.
This commit is contained in:
2026-01-25 17:24:46 +01:00
parent 9f9d9bc3b3
commit 766b90a548
16 changed files with 536 additions and 8 deletions

View File

@@ -1,7 +1,5 @@
from fastmcp import FastMCP
# Création du serveur MCP
mcp = FastMCP("Serveur Echo MCP")
from crazy_mcp import mcp
from sandbox.mcp import install_project_tool
# Déclaration d'un outil

View File

@@ -0,0 +1,3 @@
from fastmcp import FastMCP
mcp = FastMCP("PMO Crazy coder")

View File

@@ -0,0 +1,50 @@
import json
import re
from ollama import prompt
def structure_to_json(text, schema):
"""
Convert structured text to JSON using Ollama.
Args:
text (str): The structured text to convert
schema (dict): The JSON schema to use for conversion
Returns:
dict: The parsed JSON object
"""
# Format the schema as JSON string
schema_json = json.dumps(schema, indent=2)
# Create the prompt following the pattern from your bash command
full_prompt = f"""Transform the following structured text into JSON according to the given schema.
**Return only valid JSON, without any Markdown formatting :**
- no backticks,
- no code blocks,
- no extra text
Input text:
{text}
Schema:
{schema_json}"""
# Send to Ollama
response = prompt("ollama:qwen3:0.6b", full_prompt, allow_pull=True)
# Remove any markdown formatting (backticks, code blocks) that Ollama might include
# Only remove backticks at the beginning or end of the response, not those within the JSON
clean_response = response.strip()
while clean_response.startswith("`") or clean_response.startswith("```"):
clean_response = clean_response[1:].strip()
while clean_response.endswith("`") or clean_response.endswith("```"):
clean_response = clean_response[:-1].strip()
# Try to parse the response as JSON
try:
return json.loads(clean_response)
except json.JSONDecodeError:
# If parsing fails, return the raw response
return {"error": "Failed to parse JSON", "raw_response": clean_response}

1
src/jujutsu/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .command import jj

87
src/jujutsu/command.py Normal file
View File

@@ -0,0 +1,87 @@
import json
import subprocess
from typing import List, Optional
def jj(
subcommand: str,
options: Optional[List[str]] = None,
args: Optional[List[str]] = None,
template: Optional[str] = None,
) -> str:
"""
Lance une commande jj.
Args:
subcommand: La sous-commande jj (ex: 'log', 'status', 'diff')
options: Liste des options (ex: ['--limit', '5'])
args: Liste des arguments additionnels
template: Template jj à utiliser (ex: 'builtin_log_compact')
Returns:
str: sortie de la commande
"""
cmd = ["jj", subcommand]
# Ajoute le template spécifié
if template:
cmd.extend(["--template", template])
if options:
cmd.extend(options)
if args:
cmd.extend(args)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(
f"Commande jj échouée: {' '.join(cmd)}\nstderr: {result.stderr}"
)
return result.stdout
def jj_log(options=None, args=None, template=None):
"""
Execute jj log command with optional arguments.
Args:
options: List of options to pass to jj log (ex: ['--limit', '5'])
args: List of additional arguments
template: Template to use (ex: 'builtin_log_compact')
Returns:
str: Output of the jj log command
"""
return jj("log", options, args, template)
def jj_clone(url, destination=None, options=None):
"""
Execute jj git clone command.
Args:
url: The repository URL to clone
destination: The destination directory (optional)
options: List of additional options for jj git clone
Returns:
str: Output of the jj git clone command
"""
cmd = ["jj", "git", "clone", url]
if destination:
cmd.append(destination)
if options:
cmd.extend(options)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(
f"Commande jj git clone échouée: {' '.join(cmd)}\nstderr: {result.stderr}"
)
return result.stdout

83
src/ollama/__init__.py Normal file
View File

@@ -0,0 +1,83 @@
import os
import requests
# Get Ollama server address from environment variable, default to local address
ollama_addr = os.environ.get("OLLAMA_ADDR", "http://host.docker.internal:11434")
def prompt(model, text, allow_pull=False):
"""
Send a prompt to Ollama server and return the response.
Args:
model (str): The model to use
text (str): The prompt text to send
allow_pull (bool): If True, automatically download the model if not available
Returns:
str: The response from the Ollama server
"""
# Check if model is available
if allow_pull and not is_model_available(model):
# Try to pull the model
pull_result = pull_model(model)
if not pull_result.startswith("Model downloaded successfully"):
return pull_result # Return error from pull
url = f"{ollama_addr}/api/generate"
payload = {"model": model, "prompt": text, "stream": False}
try:
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()["response"]
except requests.exceptions.RequestException as e:
return f"Error: {str(e)}"
def pull_model(model_name, force=False):
"""
Download a model from Ollama.
Args:
model_name (str): The name of the model to download
force (bool): If True, forces download even if model already exists
Returns:
str: Success message or error message
"""
url = f"{ollama_addr}/api/pull"
payload = {"name": model_name}
if force:
payload["force"] = True
try:
response = requests.post(url, json=payload)
response.raise_for_status()
return "Model downloaded successfully"
except requests.exceptions.RequestException as e:
return f"Error downloading model: {str(e)}"
def is_model_available(model_name):
"""
Check if a model is available in Ollama.
Args:
model_name (str): The name of the model to check
Returns:
bool: True if model is available, False otherwise
"""
url = f"{ollama_addr}/api/tags"
try:
response = requests.get(url)
response.raise_for_status()
models = response.json().get("models", [])
return any(model.get("name") == model_name for model in models)
except requests.exceptions.RequestException:
return False

82
src/sandbox/__init__.py Normal file
View File

@@ -0,0 +1,82 @@
import os
import re
from pathlib import Path
import shutil
from jujutsu.command import jj_clone
__sandbox_root__ = "/sandbox"
# Create the sandbox root directory if it doesn't exist
os.makedirs(__sandbox_root__, exist_ok=True)
def get_sandbox_root():
"""Return the sandbox root directory path."""
return __sandbox_root__
def install_project(url, project_name=None):
"""
Clone a Git project into the sandbox root directory.
Args:
url: The Git repository URL to clone
project_name: Optional project name (if None, derived from URL)
Returns:
str: The name of the cloned project
"""
# If no project name is provided, derive it from the URL
if project_name is None:
# Extract project name from URL (e.g., from 'https://github.com/user/repo.git' get 'repo')
project_name = re.sub(r".*/([^/]+?)(?:\.git)?$", r"\1", url)
# Create the full path for the project
project_path = os.path.join(__sandbox_root__, project_name)
# Clone the repository
jj_clone(url, destination=project_path)
return project_name
def remove_project(project_name):
"""
Remove a project from the sandbox.
Args:
project_name (str): The name of the project to remove
Returns:
bool: True if the project was removed successfully, False otherwise
"""
project_path = os.path.join(__sandbox_root__, project_name)
try:
if os.path.exists(project_path):
shutil.rmtree(project_path)
return True
return False
except (OSError, PermissionError):
return False
def sandboxed_project_list():
"""
List all projects available in the sandbox.
Returns:
list: A list of project names (strings)
"""
projects = []
try:
with os.scandir(__sandbox_root__) as entries:
for entry in entries:
if entry.is_dir():
projects.append(entry.name)
except (OSError, PermissionError):
# Return empty list if we can't access the directory
pass
return projects

51
src/sandbox/mcp.py Normal file
View File

@@ -0,0 +1,51 @@
from sandbox import install_project, sandboxed_project_list, remove_project
from crazy_mcp import mcp
@mcp.tool(
description="Clone a Git repository into the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server."
)
def install_project_tool(url: str, project_name: str = None) -> str:
"""
Install a Git project into the sandbox environment.
Args:
url: The Git repository URL to clone
project_name: Optional project name (if None, derived from URL)
Returns:
str: Confirmation message with project name
"""
project_name = install_project(url, project_name)
return f"Project '{project_name}' installed successfully in sandbox"
@mcp.tool(
description="Remove a project from the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server."
)
def remove_project_tool(project_name: str) -> str:
"""
Remove a project from the sandbox environment.
Args:
project_name (str): The name of the project to remove
Returns:
str: Confirmation message with project name
"""
success = remove_project(project_name)
if success:
return f"Project '{project_name}' removed successfully from sandbox"
else:
return f"Failed to remove project '{project_name}' from sandbox"
@mcp.tool(description="List all projects available in the sandbox")
def list_sandboxed_projects() -> list:
"""
Get a list of all projects available in the sandbox.
Returns:
list: A list of project names
"""
return sandboxed_project_list()