177 lines
5.8 KiB
Python

# 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")
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
def launch_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
"""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"""
# 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
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
}
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")