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:
@@ -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
16
bin/jj-ai-commit.sh
Executable 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'
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
3
src/crazy_mcp/__init__.py
Normal file
3
src/crazy_mcp/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from fastmcp import Context, FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP("PMO Crazy coder")
|
||||||
2
src/crazy_mcp/tools/__init__.py
Normal file
2
src/crazy_mcp/tools/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .jj import *
|
||||||
|
from .sandbox import *
|
||||||
33
src/crazy_mcp/tools/jj.py
Normal file
33
src/crazy_mcp/tools/jj.py
Normal 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)
|
||||||
89
src/crazy_mcp/tools/sandbox.py
Normal file
89
src/crazy_mcp/tools/sandbox.py
Normal 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
225
toto.diff
Normal 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
|
||||||
Reference in New Issue
Block a user