Skip to main content
Glama
chat_interface.py12.5 kB
#!/usr/bin/env python3 """ Chat Interface Example for Frappe MCP Server This example demonstrates how to integrate the Frappe MCP server into a chat-style interface with tool calling capabilities. Usage: python examples/chat_interface.py Environment Variables: FRAPPE_API_KEY - Your Frappe API key FRAPPE_API_SECRET - Your Frappe API secret FRAPPE_BASE_URL - Your Frappe site URL MCP_SERVER_PORT - Port for SSE server (default: 8080) """ import asyncio import json import os import sys from typing import Dict, Any, List from dataclasses import dataclass from datetime import datetime # Add project root to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) try: from mcp.client.sse import sse_client from mcp.client.session import ClientSession OFFICIAL_MCP_AVAILABLE = True except ImportError: OFFICIAL_MCP_AVAILABLE = False print("⚠️ Official MCP SDK not available. Install with: uv add mcp") @dataclass class ChatMessage: """Represents a chat message.""" role: str # "user", "assistant", "system" content: str timestamp: datetime = None tool_calls: List[Dict[str, Any]] = None tool_results: List[Dict[str, Any]] = None def __post_init__(self): if self.timestamp is None: self.timestamp = datetime.now() if self.tool_calls is None: self.tool_calls = [] if self.tool_results is None: self.tool_results = [] class FrappeChatInterface: """Chat interface that can call Frappe MCP tools.""" def __init__(self, server_url: str): self.server_url = server_url self.session = None self.available_tools = {} self.conversation_history = [] async def connect(self): """Connect to the Frappe MCP server.""" print(f"🔗 Connecting to Frappe MCP server at {self.server_url}") if not OFFICIAL_MCP_AVAILABLE: print("❌ Official MCP SDK required. Install with: uv add mcp") return False try: self.client_context = sse_client( url=self.server_url, timeout=10, sse_read_timeout=60 ) self.streams = await self.client_context.__aenter__() read_stream, write_stream = self.streams self.session = ClientSession(read_stream, write_stream) await self.session.__aenter__() # Initialize MCP connection await self.session.initialize() # Load available tools await self._load_tools() print(f"✅ Connected! {len(self.available_tools)} tools available") return True except Exception as e: print(f"❌ Connection failed: {e}") return False async def disconnect(self): """Disconnect from the MCP server.""" if self.session: await self.session.__aexit__(None, None, None) if hasattr(self, 'client_context'): await self.client_context.__aexit__(None, None, None) async def _load_tools(self): """Load available tools from the server.""" try: result = await self.session.list_tools() self.available_tools = {} for tool in result.tools: self.available_tools[tool.name] = { 'description': tool.description, 'inputSchema': tool.inputSchema } except Exception as e: print(f"⚠️ Could not load tools: {e}") def get_available_tools(self) -> List[str]: """Get list of available tool names.""" return list(self.available_tools.keys()) def show_tool_help(self, tool_name: str = None): """Show help for tools.""" if tool_name: if tool_name in self.available_tools: tool = self.available_tools[tool_name] print(f"\n🔧 {tool_name}") print(f"Description: {tool['description']}") if tool.get('inputSchema'): print(f"Input Schema: {json.dumps(tool['inputSchema'], indent=2)}") else: print(f"❌ Tool '{tool_name}' not found") else: print("\\n📋 Available Frappe Tools:") for name, tool in self.available_tools.items(): print(f" • {name} - {tool['description']}") async def call_tool(self, tool_name: str, arguments: Dict[str, Any] = None) -> Dict[str, Any]: """Call a specific tool and return the result.""" if tool_name not in self.available_tools: return {"error": f"Tool '{tool_name}' not available"} try: result = await self.session.call_tool(tool_name, arguments or {}) # Extract text content from result content = [] if hasattr(result, 'content'): for item in result.content: if hasattr(item, 'text'): content.append(item.text) else: content.append(str(item)) return { "success": True, "tool": tool_name, "arguments": arguments, "content": content } except Exception as e: return { "success": False, "tool": tool_name, "arguments": arguments, "error": str(e) } async def process_message(self, user_input: str) -> ChatMessage: """Process a user message and determine if tools should be called.""" # Add user message to history user_msg = ChatMessage(role="user", content=user_input) self.conversation_history.append(user_msg) # Simple tool detection (in a real app, you'd use an LLM here) response_content = "" tool_calls = [] tool_results = [] # Check for direct tool commands if user_input.startswith("/"): await self._handle_command(user_input, tool_calls, tool_results) response_content = "Command executed." # Check for tool-related keywords elif any(keyword in user_input.lower() for keyword in ["count", "list", "show", "get"]): # Try to extract tool calls from natural language suggested_tools = await self._suggest_tools(user_input) if suggested_tools: response_content += f"I can help with that using these tools: {', '.join(suggested_tools)}\\n" response_content += "Use /call <tool_name> [arguments] to execute." else: response_content = "I'm a Frappe assistant. I can help you with Frappe operations. " response_content += "Try asking me to count documents, list data, or use /help for commands." # Create assistant response assistant_msg = ChatMessage( role="assistant", content=response_content, tool_calls=tool_calls, tool_results=tool_results ) self.conversation_history.append(assistant_msg) return assistant_msg async def _handle_command(self, command: str, tool_calls: List, tool_results: List): """Handle slash commands.""" parts = command[1:].split() cmd = parts[0] if parts else "" if cmd == "help": self.show_tool_help() elif cmd == "tools": print(f"Available tools: {', '.join(self.get_available_tools())}") elif cmd == "call" and len(parts) >= 2: tool_name = parts[1] arguments = {} # Parse arguments if provided if len(parts) > 2: try: arguments = json.loads(' '.join(parts[2:])) except json.JSONDecodeError: print("❌ Invalid arguments format (expected JSON)") return # Call the tool result = await self.call_tool(tool_name, arguments) tool_calls.append({"tool": tool_name, "arguments": arguments}) tool_results.append(result) # Display result if result.get("success"): print(f"\\n🔧 Tool Result from '{tool_name}':") for content in result["content"]: print(content) else: print(f"❌ Tool '{tool_name}' failed: {result.get('error')}") elif cmd == "info" and len(parts) >= 2: tool_name = parts[1] self.show_tool_help(tool_name) else: print("Available commands:") print(" /help - Show available tools") print(" /tools - List tool names") print(" /call <tool> [args] - Call a tool") print(" /info <tool> - Show tool details") async def _suggest_tools(self, user_input: str) -> List[str]: """Suggest relevant tools based on user input.""" suggestions = [] input_lower = user_input.lower() # Simple keyword matching if "count" in input_lower: suggestions.append("count_documents") if "list" in input_lower: suggestions.extend(["list_documents", "list_reports", "get_doctype_list"]) if "ping" in input_lower: suggestions.append("ping") if "version" in input_lower: suggestions.append("version") return [tool for tool in suggestions if tool in self.available_tools] def show_conversation(self): """Display the conversation history.""" print("\\n💬 Conversation History:") for msg in self.conversation_history[-5:]: # Show last 5 messages timestamp = msg.timestamp.strftime("%H:%M") if msg.role == "user": print(f"[{timestamp}] 👤 You: {msg.content}") else: print(f"[{timestamp}] 🤖 Assistant: {msg.content}") if msg.tool_calls: print(f" 🔧 Used tools: {[call['tool'] for call in msg.tool_calls]}") async def main(): """Main chat interface loop.""" # Configuration port = os.getenv("MCP_SERVER_PORT", "8080") server_url = f"http://localhost:{port}/sse" print("🤖 Frappe Chat Interface") print("=" * 50) print(f"Server URL: {server_url}") # Check environment if not all([os.getenv("FRAPPE_API_KEY"), os.getenv("FRAPPE_API_SECRET"), os.getenv("FRAPPE_BASE_URL")]): print("⚠️ Warning: Frappe credentials not set. Some tools may fail.") print(" Set FRAPPE_API_KEY, FRAPPE_API_SECRET, and FRAPPE_BASE_URL") # Start chat interface chat = FrappeChatInterface(server_url) if not await chat.connect(): print("❌ Could not connect to Frappe MCP server.") print(" Make sure the server is running with:") print(f" uvx frappe-mcp-server --transport sse --port {port}") return try: print("\\n🎯 Chat Interface Ready!") print("Type your questions or use /help for commands") print("Type 'quit' to exit\\n") while True: try: user_input = input("You: ").strip() if not user_input: continue if user_input.lower() in ['quit', 'exit', 'bye']: break if user_input == '/history': chat.show_conversation() continue # Process the message response = await chat.process_message(user_input) print(f"🤖 Assistant: {response.content}") except KeyboardInterrupt: print("\\n\\n👋 Goodbye!") break except EOFError: break finally: await chat.disconnect() if __name__ == "__main__": asyncio.run(main())

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/appliedrelevance/frappe-mcp-server'

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