Skip to main content
Glama
keizman

MCP Feedback Collector

by keizman

collect_feedback

Collect interactive user feedback with text and images in response to AI work summaries using a timed dialog interface.

Instructions

收集用户反馈的交互式工具 AI可以汇报完成的工作内容,用户可以提供文字和/或图片反馈 Args: work_summary: AI完成的工作内容汇报 timeout_seconds: 对话框超时时间(秒),默认300秒(5分钟) Returns: 包含用户反馈内容的列表,可能包含文本和图片

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
work_summaryNo
timeout_secondsNo

Implementation Reference

  • The core handler function for the 'collect_feedback' tool. It is decorated with @mcp.tool() which handles both implementation and registration in the MCP framework. Invokes the feedback dialog GUI and returns list of user feedback (text and/or images). Includes input schema in args and docstring.
    @mcp.tool() def collect_feedback(work_summary: str = "", timeout_seconds: int = DIALOG_TIMEOUT) -> list: """ 收集用户反馈的交互式工具 AI可以汇报完成的工作内容,用户可以提供文字和/或图片反馈 Args: work_summary: AI完成的工作内容汇报 timeout_seconds: 对话框超时时间(秒),默认300秒(5分钟) Returns: 包含用户反馈内容的列表,可能包含文本和图片 """ try: result = show_feedback_dialog(work_summary, timeout_seconds) return result or [] except Exception as e: return [{"type": "error", "content": f"收集反馈时出错: {str(e)}"}]
  • Supporting GUI class that implements the interactive feedback collection dialog. Handles text input, multiple image selection, preview, submission, timeout, and cleanup.
    class FeedbackDialog: def __init__(self, work_summary: str = "", timeout_seconds: int = DIALOG_TIMEOUT): self.work_summary = work_summary self.timeout_seconds = timeout_seconds self.selected_images = [] self.result = None self.image_frames = [] # 创建主窗口 self.root = tk.Tk() self.root.title("🎯 工作完成汇报与反馈收集") self.root.geometry("650x750") self.root.resizable(False, False) # 居中显示 self.center_window() # 设置样式 self.setup_styles() # 创建UI self.create_ui() # 设置超时 self.root.after(timeout_seconds * 1000, self.timeout_handler) def center_window(self): """将窗口居中显示""" self.root.update_idletasks() width = self.root.winfo_width() height = self.root.winfo_height() x = (self.root.winfo_screenwidth() // 2) - (width // 2) y = (self.root.winfo_screenheight() // 2) - (height // 2) self.root.geometry(f'{width}x{height}+{x}+{y}') def setup_styles(self): """设置样式""" self.root.configure(bg='#f8fafc') # 配置ttk样式 style = ttk.Style() style.theme_use('clam') # 按钮样式 style.configure( 'Main.TButton', font=('Segoe UI', 10, 'bold'), relief='flat', borderwidth=0, focuscolor='none' ) def create_ui(self): """创建用户界面""" # 主框架 main_frame = tk.Frame(self.root, bg='#f8fafc', padx=15, pady=15) main_frame.pack(fill=tk.BOTH, expand=True) # 标题 title_font = tkFont.Font(family="Segoe UI", size=20, weight="bold") title_label = tk.Label( main_frame, text="🎯 工作完成汇报与反馈收集", font=title_font, bg='#f8fafc', fg='#2c3e50' ) title_label.pack(pady=(0, 20)) # 1. 工作汇报区域 report_frame = tk.LabelFrame( main_frame, text="📋 AI工作完成汇报", font=('Segoe UI', 11, 'bold'), bg='#ffffff', fg='#2d3748', padx=8, pady=8 ) report_frame.pack(fill=tk.X, pady=(0, 12)) self.report_text = scrolledtext.ScrolledText( report_frame, height=5, font=('Segoe UI', 9), bg='#ecf0f1', fg='#2c3e50', state=tk.DISABLED, wrap=tk.WORD ) self.report_text.pack(fill=tk.X) # 设置工作汇报内容 self.report_text.config(state=tk.NORMAL) self.report_text.insert(tk.END, self.work_summary or "本次对话中完成的工作内容...") self.report_text.config(state=tk.DISABLED) # 2. 用户反馈文本区域 feedback_frame = tk.LabelFrame( main_frame, text="💬 您的文字反馈(可选)", font=('Segoe UI', 11, 'bold'), bg='#ffffff', fg='#2d3748', padx=8, pady=8 ) feedback_frame.pack(fill=tk.X, pady=(0, 12)) self.feedback_text = scrolledtext.ScrolledText( feedback_frame, height=6, font=('Segoe UI', 9), bg='#ffffff', fg='#2d3748', wrap=tk.WORD ) self.feedback_text.pack(fill=tk.X) self.feedback_text.insert(tk.END, "请在此输入您的反馈、建议或问题...") self.feedback_text.bind('<FocusIn>', self.clear_placeholder) # 3. 图片区域 image_frame = tk.LabelFrame( main_frame, text="🖼️ 图片反馈(可选,支持多张)", font=('Segoe UI', 11, 'bold'), bg='#ffffff', fg='#2d3748', padx=8, pady=8 ) image_frame.pack(fill=tk.X, pady=(0, 12)) # 图片按钮 button_frame = tk.Frame(image_frame, bg='#ffffff') button_frame.pack(fill=tk.X, pady=(0, 8)) tk.Button( button_frame, text="📁 选择文件", command=self.select_image_file, bg='#4299e1', fg='white', font=('Segoe UI', 9, 'bold'), relief=tk.FLAT, padx=12, pady=6 ).pack(side=tk.LEFT, padx=(0, 8)) tk.Button( button_frame, text="📋 粘贴图片", command=self.paste_from_clipboard, bg='#48bb78', fg='white', font=('Segoe UI', 9, 'bold'), relief=tk.FLAT, padx=12, pady=6 ).pack(side=tk.LEFT, padx=(0, 8)) tk.Button( button_frame, text="❌ 清除", command=self.clear_all_images, bg='#f56565', fg='white', font=('Segoe UI', 9, 'bold'), relief=tk.FLAT, padx=12, pady=6 ).pack(side=tk.LEFT) # 图片预览区域 self.image_canvas_frame = tk.Frame(image_frame, bg='#f7fafc', height=120) self.image_canvas_frame.pack(fill=tk.X) self.image_canvas_frame.pack_propagate(False) canvas = tk.Canvas(self.image_canvas_frame, bg='#f7fafc', height=120, highlightthickness=0) scrollbar = ttk.Scrollbar(self.image_canvas_frame, orient="horizontal", command=canvas.xview) self.image_preview_frame = tk.Frame(canvas, bg='#f7fafc') canvas.configure(xscrollcommand=scrollbar.set) canvas.pack(side="top", fill="both", expand=True) scrollbar.pack(side="bottom", fill="x") canvas.create_window((0, 0), window=self.image_preview_frame, anchor="nw") self.image_preview_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) self.canvas = canvas # 4. 底部按钮 bottom_frame = tk.Frame(main_frame, bg='#f8fafc') bottom_frame.pack(fill=tk.X, pady=(15, 0)) tk.Button( bottom_frame, text="✅ 提交反馈", command=self.submit_feedback, bg='#48bb78', fg='white', font=('Segoe UI', 11, 'bold'), relief=tk.FLAT, padx=20, pady=8 ).pack(side=tk.RIGHT, padx=(10, 0)) tk.Button( bottom_frame, text="❌ 取消", command=self.cancel, bg='#a0aec0', fg='white', font=('Segoe UI', 11, 'bold'), relief=tk.FLAT, padx=20, pady=8 ).pack(side=tk.RIGHT) def clear_placeholder(self, event): """清除占位符文本""" if self.feedback_text.get("1.0", tk.END).strip() == "请在此输入您的反馈、建议或问题...": self.feedback_text.delete("1.0", tk.END) def select_image_file(self): """选择图片文件""" file_types = [ ("图片文件", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("PNG files", "*.png"), ("JPEG files", "*.jpg *.jpeg"), ("所有文件", "*.*") ] file_paths = filedialog.askopenfilenames( title="选择图片文件", filetypes=file_types ) for file_path in file_paths: if file_path and file_path not in self.selected_images: self.selected_images.append(file_path) self.update_image_preview() def paste_from_clipboard(self): """从剪贴板粘贴图片""" try: # 尝试从剪贴板获取图片 img = ImageTk.PhotoImage(self.root.clipboard_get()) # 保存到临时文件 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") temp_path = f"temp_clipboard_{timestamp}.png" # 这里需要将剪贴板图片保存为文件 # 由于tkinter剪贴板处理的限制,我们跳过这个功能或使用PIL messagebox.showinfo("提示", "剪贴板功能暂不可用,请使用文件选择功能") except Exception as e: messagebox.showinfo("提示", "剪贴板中没有图片或格式不支持") def clear_all_images(self): """清除所有选择的图片""" # 删除临时文件 for image_path in self.selected_images: if image_path.startswith("temp_clipboard_"): try: os.remove(image_path) except: pass self.selected_images.clear() self.update_image_preview() def update_image_preview(self): """更新图片预览""" # 清除现有预览 for widget in self.image_preview_frame.winfo_children(): widget.destroy() # 添加新的预览 for i, image_path in enumerate(self.selected_images): try: # 创建预览框架 preview_frame = tk.Frame(self.image_preview_frame, bg='white', relief=tk.RAISED, bd=2) preview_frame.pack(side=tk.LEFT, padx=5, pady=5) # 加载图片 with Image.open(image_path) as img: # 调整图片大小 img.thumbnail((100, 100), Image.Resampling.LANCZOS) photo = ImageTk.PhotoImage(img) # 图片标签 img_label = tk.Label(preview_frame, image=photo, bg='white') img_label.image = photo # 保持引用 img_label.pack(padx=5, pady=5) # 删除按钮 remove_btn = tk.Button( preview_frame, text="❌", command=lambda idx=i: self.remove_image(idx), bg='#f56565', fg='white', font=('Segoe UI', 8, 'bold'), relief=tk.FLAT, width=3, height=1 ) remove_btn.pack(pady=(0, 5)) except Exception as e: print(f"加载图片失败: {e}") # 更新画布滚动区域 self.image_preview_frame.update_idletasks() self.canvas.configure(scrollregion=self.canvas.bbox("all")) def remove_image(self, index): """删除指定索引的图片""" if 0 <= index < len(self.selected_images): image_path = self.selected_images[index] # 如果是临时文件,删除它 if image_path.startswith("temp_clipboard_"): try: os.remove(image_path) except: pass self.selected_images.pop(index) self.update_image_preview() def submit_feedback(self): """提交反馈""" # 获取文本反馈 text_feedback = self.feedback_text.get("1.0", tk.END).strip() if text_feedback == "请在此输入您的反馈、建议或问题...": text_feedback = "" # 准备结果 feedback_items = [] # 添加文字反馈 if text_feedback: feedback_items.append({ "type": "text", "content": text_feedback }) # 添加图片反馈 for image_path in self.selected_images: try: # 读取图片并转换为base64 with open(image_path, "rb") as f: image_data = f.read() image_base64 = base64.b64encode(image_data).decode('utf-8') feedback_items.append({ "type": "image", "data": image_base64, "media_type": "image/png" }) except Exception as e: print(f"处理图片失败: {e}") self.result = feedback_items self.root.quit() # 添加窗口销毁,确保窗口自动关闭 self.root.destroy() def cancel(self): """取消对话框""" self.clear_all_images() # 清理临时文件 self.result = [] self.root.quit() # 添加窗口销毁,确保窗口自动关闭 self.root.destroy() def timeout_handler(self): """超时处理""" self.clear_all_images() # 清理临时文件 self.result = None self.root.quit() # 添加窗口销毁,确保窗口自动关闭 self.root.destroy()
  • Helper function that creates and runs the FeedbackDialog instance, returning the collected feedback.
    def show_feedback_dialog(work_summary: str = "", timeout_seconds: int = DIALOG_TIMEOUT): """显示反馈收集对话框""" dialog = FeedbackDialog(work_summary, timeout_seconds) dialog.root.mainloop() return dialog.result
  • MCP server instance creation where tools like collect_feedback are registered via decorators.
    mcp = FastMCP( "交互式反馈收集器", dependencies=["pillow"] )
  • Starts the MCP server, making the registered tools available.
    mcp.run()
Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keizman/mcp-feedforward'

If you have feedback or need assistance with the MCP directory API, please join our Discord server