Refactor MCP server structure and add AI commit tool

- Restructure src/ into package src/crazy_mcp with modular tools
- Add jj.py and sandbox.py tool modules for jujutsu and sandbox management
- Remove circular dependency on crazy-mcp from pyproject.toml
- Fix async get_state call in PMOCrazyCoder.py increment tool
- Add bin/jj-ai-commit.sh for AI-generated commit messages from diffs
- Update entrypoint to copy all bin scripts and create config directory
This commit is contained in:
2026-02-21 22:29:33 +00:00
parent 9c914b989b
commit 2d53f89172
9 changed files with 373 additions and 4 deletions

View File

@@ -61,6 +61,9 @@ RUN groupadd --gid $USER_GID $USERNAME && \
COPY docker-entrypoint.sh /usr/local/bin/entrypoint.sh COPY docker-entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh
COPY bin/* /usr/local/bin/
RUN chmod +x /usr/local/bin/*
RUN mkdir -p /home/${USERNAME}/.config \ RUN mkdir -p /home/${USERNAME}/.config \
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config
COPY --chown=${USERNAME}:${USERNAME} opencode /home/${USERNAME}/.config/opencode COPY --chown=${USERNAME}:${USERNAME} opencode /home/${USERNAME}/.config/opencode

16
bin/jj-ai-commit.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
LOCAL_LLM_API=${LOCAL_LLM_API:-http://host.docker.internal:1248/v1}
DIFF=$(jj diff)
PROMPT='Analyse ce diff et réponds UNIQUEMENT avec un objet JSON valide contenant "title" (string) et "message" (string) pour le message de commit. Pas de texte avant ou après le JSON.'
CONTENT="${PROMPT}
${DIFF}"
REQUEST=$(jq -n --arg content "$CONTENT" '{"model": "qwen3-coder", "messages": [{"role": "user", "content": $content}]}')
curl -s -X POST \
-H "Content-Type: application/json" \
-d "$REQUEST" \
"$LOCAL_LLM_API/chat/completions" | jq -r '.choices[0].message.content' | jq -r '.title + "\n\n" + .message'

View File

@@ -4,7 +4,6 @@ version = "0.1.0"
description = "A crazy MCP server" description = "A crazy MCP server"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"crazy-mcp>=0.1.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -22,9 +22,8 @@ def additionner(a: float, b: float) -> float:
@mcp.tool @mcp.tool
async def increment(ctx: Context) -> tuple[int, str]: async def increment(ctx: Context) -> tuple[int, str]:
count = ctx.get_state("count") or 0 count = await ctx.get_state("count") or 0
ctx.set_state("count", count + 1) return int(count) + 1, ctx.session_id
return count + 1, ctx.session_id
# Lancement du serveur MCP # Lancement du serveur MCP

View File

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

View File

@@ -0,0 +1,2 @@
from .jj import *
from .sandbox import *

33
src/crazy_mcp/tools/jj.py Normal file
View File

@@ -0,0 +1,33 @@
from crazy_mcp import Context, mcp
from jujutsu import jj_log
from sandbox import get_active_project, get_project_path
@mcp.tool(description="Get the commit history for a project")
def jj_log_tool(project_name: str | None = None, ctx: Context = None) -> dict:
"""
Get the commit history for a project.
Args:
project_name (str, optional): The name of the project. If None, uses the active project.
ctx (Context): The MCP context object containing session information
Returns:
dict: The commit history as JSON structure
"""
# If no project name is provided, try to get the active project
if project_name is None:
if ctx is None:
raise ValueError("ctx is required when project_name is None")
project_info = get_active_project(ctx.session_id)
if project_info is None:
raise ValueError("No active project set and no project name provided")
project_path = project_info[1]
else:
project_path = get_project_path(project_name)
if project_path is None:
raise ValueError(f"Project '{project_name}' not found")
# Call the jj_log function from jujutsu module
return jj_log(project_path)

View File

@@ -0,0 +1,89 @@
from crazy_mcp import Context, mcp
from sandbox import (
get_active_project,
install_project,
remove_project,
sandboxed_project_list,
set_active_project,
)
@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. WARNING: This operation creates a sandboxed environment that prevents direct filesystem access to the project contents. Any attempt to access project files outside of the MCP tools will be blocked by security restrictions."
)
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. WARNING: This operation removes the entire project sandboxed environment and all associated security restrictions will be enforced."
)
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. Note: Direct filesystem access to project contents is forbidden. All project interactions must be done through MCP tools."
)
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()
@mcp.tool(description="Set the active project for a given session ID")
def set_active_project_tool(project_name: str, ctx: Context) -> bool:
"""
Set the active project for a given session ID.
Args:
project_name (str): The name of the project to set as active
ctx (Context): The MCP context object containing session information
Returns:
bool: True if successful, False if the project doesn't exist
"""
return set_active_project(ctx.session_id, project_name)
@mcp.tool(description="Get the active project for a given session ID")
def get_active_project_tool(ctx: Context) -> tuple[str, str] | None:
"""
Get the active project for a given session ID.
Args:
ctx (Context): The MCP context object containing session information
Returns:
tuple: (project_name, project_path) if session exists, None otherwise
"""
project_info = get_active_project(ctx.session_id)
return project_info

225
toto.diff Normal file
View File

@@ -0,0 +1,225 @@
diff --git a/bin/jj-ai-commit.sh b/bin/jj-ai-commit.sh
new file mode 100755
index 0000000000..b89444b55d
--- /dev/null
+++ b/bin/jj-ai-commit.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [ -z "${LOCAL_LLM_API:-}" ]; then
+ echo "Error: LOCAL_LLM_API environment variable is not set."
+ exit 1
+fi
+
+MODEL="qwen/qwen3-coder-next"
+
+# Capture seulement les lignes modifiées pour limiter la taille
+diff="$(jj diff --git | grep '^[+-]' | head -n 500)"
+
+request_json=$(jq -n \
+ --arg model "$MODEL" \
+ --arg diff "$diff" \
+'{
+ model: $model,
+ temperature: 0.2,
+ response_format: { type: "json_object" },
+ messages: [
+ {
+ role: "user",
+ content: (
+ "Analyse ce diff et réponds UNIQUEMENT avec un objet JSON valide contenant \"title\" (string) et \"message\" (string). Pas de texte avant ou après le JSON.\n\n" + $diff
+ )
+ }
+ ]
+}')
+
+response=$(curl -s "$LOCAL_LLM_API/chat/completions" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer dummy" \
+ -d "$request_json")
+
+# Récupérer et formater le commit message
+echo "$response" | jq -r '.choices[0].message.content' | jq -r '.title + "\n\n" + .message'
diff --git a/pyproject.toml b/pyproject.toml
index 4d15816f39..cd40d11e6a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,6 @@
description = "A crazy MCP server"
requires-python = ">=3.10"
dependencies = [
- "crazy-mcp>=0.1.0",
]
[project.optional-dependencies]
diff --git a/src/PMOCrazyCoder.py b/src/PMOCrazyCoder.py
index 4a10aa6427..fd9bf4a774 100644
--- a/src/PMOCrazyCoder.py
+++ b/src/PMOCrazyCoder.py
@@ -22,9 +22,8 @@
@mcp.tool
async def increment(ctx: Context) -> tuple[int, str]:
- count = ctx.get_state("count") or 0
- ctx.set_state("count", count + 1)
- return count + 1, ctx.session_id
+ count = await ctx.get_state("count") or 0
+ return int(count) + 1, ctx.session_id
# Lancement du serveur MCP
diff --git a/src/crazy_mcp/__init__.py b/src/crazy_mcp/__init__.py
new file mode 100644
index 0000000000..bda4dfa2ca
--- /dev/null
+++ b/src/crazy_mcp/__init__.py
@@ -0,0 +1,3 @@
+from fastmcp import Context, FastMCP
+
+mcp = FastMCP("PMO Crazy coder")
diff --git a/src/crazy_mcp/tools/__init__.py b/src/crazy_mcp/tools/__init__.py
new file mode 100644
index 0000000000..23a3da5bd9
--- /dev/null
+++ b/src/crazy_mcp/tools/__init__.py
@@ -0,0 +1,2 @@
+from .jj import *
+from .sandbox import *
diff --git a/src/crazy_mcp/tools/jj.py b/src/crazy_mcp/tools/jj.py
new file mode 100644
index 0000000000..f6bbed95c8
--- /dev/null
+++ b/src/crazy_mcp/tools/jj.py
@@ -0,0 +1,33 @@
+from crazy_mcp import Context, mcp
+from jujutsu import jj_log
+from sandbox import get_active_project, get_project_path
+
+
+@mcp.tool(description="Get the commit history for a project")
+def jj_log_tool(project_name: str | None = None, ctx: Context = None) -> dict:
+ """
+ Get the commit history for a project.
+
+ Args:
+ project_name (str, optional): The name of the project. If None, uses the active project.
+ ctx (Context): The MCP context object containing session information
+
+ Returns:
+ dict: The commit history as JSON structure
+ """
+ # If no project name is provided, try to get the active project
+ if project_name is None:
+ if ctx is None:
+ raise ValueError("ctx is required when project_name is None")
+
+ project_info = get_active_project(ctx.session_id)
+ if project_info is None:
+ raise ValueError("No active project set and no project name provided")
+ project_path = project_info[1]
+ else:
+ project_path = get_project_path(project_name)
+ if project_path is None:
+ raise ValueError(f"Project '{project_name}' not found")
+
+ # Call the jj_log function from jujutsu module
+ return jj_log(project_path)
diff --git a/src/crazy_mcp/tools/sandbox.py b/src/crazy_mcp/tools/sandbox.py
new file mode 100644
index 0000000000..68da135db1
--- /dev/null
+++ b/src/crazy_mcp/tools/sandbox.py
@@ -0,0 +1,89 @@
+from crazy_mcp import Context, mcp
+from sandbox import (
+ get_active_project,
+ install_project,
+ remove_project,
+ sandboxed_project_list,
+ set_active_project,
+)
+
+
+@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. WARNING: This operation creates a sandboxed environment that prevents direct filesystem access to the project contents. Any attempt to access project files outside of the MCP tools will be blocked by security restrictions."
+)
+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. WARNING: This operation removes the entire project sandboxed environment and all associated security restrictions will be enforced."
+)
+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. Note: Direct filesystem access to project contents is forbidden. All project interactions must be done through MCP tools."
+)
+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()
+
+
+@mcp.tool(description="Set the active project for a given session ID")
+def set_active_project_tool(project_name: str, ctx: Context) -> bool:
+ """
+ Set the active project for a given session ID.
+
+ Args:
+ project_name (str): The name of the project to set as active
+ ctx (Context): The MCP context object containing session information
+
+ Returns:
+ bool: True if successful, False if the project doesn't exist
+ """
+ return set_active_project(ctx.session_id, project_name)
+
+
+@mcp.tool(description="Get the active project for a given session ID")
+def get_active_project_tool(ctx: Context) -> tuple[str, str] | None:
+ """
+ Get the active project for a given session ID.
+
+ Args:
+ ctx (Context): The MCP context object containing session information
+
+ Returns:
+ tuple: (project_name, project_path) if session exists, None otherwise
+ """
+ project_info = get_active_project(ctx.session_id)
+ return project_info
diff --git a/toto.diff b/toto.diff
new file mode 100644
index 0000000000..e69de29bb2