2025-05-09 12:49:13 +01:00
|
|
|
# Interactive Feedback MCP
|
|
|
|
# Developed by Fábio Ferreira (https://x.com/fabiomlferreira)
|
|
|
|
# Inspired by/related to dotcursorrules.com (https://dotcursorrules.com/)
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import json
|
|
|
|
import tempfile
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
from typing import Annotated, Dict
|
|
|
|
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from pydantic import Field
|
|
|
|
|
|
|
|
# The log_level is necessary for Cline to work: https://github.com/jlowin/fastmcp/issues/81
|
|
|
|
mcp = FastMCP("Interactive Feedback MCP", log_level="ERROR")
|
|
|
|
|
2025-05-29 12:34:38 +08:00
|
|
|
def is_ssh_session() -> bool:
|
|
|
|
"""Check if we're running in an SSH session or remote environment"""
|
|
|
|
# Check for SSH environment variables
|
|
|
|
ssh_indicators = [
|
|
|
|
'SSH_CONNECTION',
|
|
|
|
'SSH_CLIENT',
|
|
|
|
'SSH_TTY'
|
|
|
|
]
|
|
|
|
|
|
|
|
for indicator in ssh_indicators:
|
|
|
|
if os.getenv(indicator):
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Check if DISPLAY is not set (common in SSH without X11 forwarding)
|
|
|
|
if sys.platform.startswith('linux') and not os.getenv('DISPLAY'):
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Check for other remote indicators
|
|
|
|
if os.getenv('TERM_PROGRAM') == 'vscode' and os.getenv('VSCODE_INJECTION') == '1':
|
|
|
|
# VSCode remote development
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def can_use_gui() -> bool:
|
|
|
|
"""Check if GUI can be used in current environment"""
|
|
|
|
if is_ssh_session():
|
|
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Try to import Qt and check if display is available
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
return True # Windows should generally support GUI
|
|
|
|
elif sys.platform == 'darwin':
|
|
|
|
return True # macOS should generally support GUI
|
|
|
|
else:
|
|
|
|
# Linux - check for DISPLAY
|
|
|
|
return bool(os.getenv('DISPLAY'))
|
|
|
|
except ImportError:
|
|
|
|
return False
|
|
|
|
|
2025-05-09 12:49:13 +01:00
|
|
|
def launch_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
2025-05-29 12:34:38 +08:00
|
|
|
"""Launch appropriate UI based on environment"""
|
|
|
|
|
|
|
|
if can_use_gui():
|
|
|
|
# Use Qt GUI (original implementation)
|
|
|
|
return launch_qt_feedback_ui(project_directory, summary)
|
|
|
|
else:
|
|
|
|
# Use Web UI
|
|
|
|
return launch_web_feedback_ui(project_directory, summary)
|
|
|
|
|
|
|
|
def launch_qt_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
|
|
|
"""Original Qt GUI implementation"""
|
2025-05-09 12:49:13 +01:00
|
|
|
# Create a temporary file for the feedback result
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
|
|
|
|
output_file = tmp.name
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Get the path to feedback_ui.py relative to this script
|
|
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
feedback_ui_path = os.path.join(script_dir, "feedback_ui.py")
|
|
|
|
|
|
|
|
# Run feedback_ui.py as a separate process
|
|
|
|
# NOTE: There appears to be a bug in uv, so we need
|
|
|
|
# to pass a bunch of special flags to make this work
|
|
|
|
args = [
|
|
|
|
sys.executable,
|
|
|
|
"-u",
|
|
|
|
feedback_ui_path,
|
|
|
|
"--project-directory", project_directory,
|
|
|
|
"--prompt", summary,
|
|
|
|
"--output-file", output_file
|
|
|
|
]
|
|
|
|
result = subprocess.run(
|
|
|
|
args,
|
|
|
|
check=False,
|
|
|
|
shell=False,
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
|
stdin=subprocess.DEVNULL,
|
|
|
|
close_fds=True
|
|
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
|
|
raise Exception(f"Failed to launch feedback UI: {result.returncode}")
|
|
|
|
|
|
|
|
# Read the result from the temporary file
|
|
|
|
with open(output_file, 'r') as f:
|
|
|
|
result = json.load(f)
|
|
|
|
os.unlink(output_file)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
|
|
if os.path.exists(output_file):
|
|
|
|
os.unlink(output_file)
|
|
|
|
raise e
|
|
|
|
|
2025-05-29 12:34:38 +08:00
|
|
|
def launch_web_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
|
|
|
"""Launch Web UI implementation"""
|
|
|
|
try:
|
|
|
|
from web_ui import launch_web_feedback_ui as launch_web
|
|
|
|
return launch_web(project_directory, summary)
|
|
|
|
except ImportError as e:
|
|
|
|
# Fallback to command line if web UI fails
|
|
|
|
print(f"Web UI not available: {e}")
|
|
|
|
return launch_cli_feedback_ui(project_directory, summary)
|
|
|
|
|
|
|
|
def launch_cli_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
|
|
|
"""Simple command line fallback"""
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
print("Interactive Feedback MCP")
|
|
|
|
print(f"{'='*60}")
|
|
|
|
print(f"專案目錄: {project_directory}")
|
|
|
|
print(f"任務描述: {summary}")
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
|
|
|
# Ask for command to run
|
|
|
|
command = input("要執行的命令 (留空跳過): ").strip()
|
|
|
|
command_logs = ""
|
|
|
|
|
|
|
|
if command:
|
|
|
|
print(f"執行命令: {command}")
|
|
|
|
try:
|
|
|
|
result = subprocess.run(
|
|
|
|
command,
|
|
|
|
shell=True,
|
|
|
|
cwd=project_directory,
|
|
|
|
capture_output=True,
|
|
|
|
text=True,
|
|
|
|
encoding="utf-8",
|
|
|
|
errors="ignore"
|
|
|
|
)
|
|
|
|
command_logs = f"$ {command}\n{result.stdout}{result.stderr}"
|
|
|
|
print(command_logs)
|
|
|
|
except Exception as e:
|
|
|
|
command_logs = f"$ {command}\nError: {str(e)}\n"
|
|
|
|
print(command_logs)
|
|
|
|
|
|
|
|
# Ask for feedback
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
print("請提供您的回饋意見:")
|
|
|
|
feedback = input().strip()
|
|
|
|
|
|
|
|
return {
|
|
|
|
"command_logs": command_logs,
|
|
|
|
"interactive_feedback": feedback
|
|
|
|
}
|
|
|
|
|
2025-05-09 12:49:13 +01:00
|
|
|
def first_line(text: str) -> str:
|
|
|
|
return text.split("\n")[0].strip()
|
|
|
|
|
|
|
|
@mcp.tool()
|
|
|
|
def interactive_feedback(
|
|
|
|
project_directory: Annotated[str, Field(description="Full path to the project directory")],
|
|
|
|
summary: Annotated[str, Field(description="Short, one-line summary of the changes")],
|
|
|
|
) -> Dict[str, str]:
|
|
|
|
"""Request interactive feedback for a given project directory and summary"""
|
|
|
|
return launch_feedback_ui(first_line(project_directory), first_line(summary))
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
mcp.run(transport="stdio")
|