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()

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