MCP Unified Server

import streamlit as st import yaml import os import sys from pathlib import Path import subprocess import json # Add the parent directory to the path to allow importing from the main project sys.path.append(str(Path(__file__).parent.parent)) # Set page config st.set_page_config( page_title="MCP Tools Configuration", page_icon="šŸ§°", layout="wide", initial_sidebar_state="expanded" ) # Function to load configuration def load_config(config_path): """Load configuration from YAML file""" if not os.path.exists(config_path): # Create default config if it doesn't exist default_config = { "enabled_tools": {}, "tool_config": {} } with open(config_path, 'w') as f: yaml.dump(default_config, f, default_flow_style=False) return default_config with open(config_path, 'r') as f: try: config = yaml.safe_load(f) return config except Exception as e: st.error(f"Error loading configuration: {str(e)}") return {"enabled_tools": {}, "tool_config": {}} # Function to save configuration def save_config(config, config_path): """Save configuration to YAML file""" try: with open(config_path, 'w') as f: yaml.dump(config, f, default_flow_style=False) return True except Exception as e: st.error(f"Error saving configuration: {str(e)}") return False # Function to discover available tools def discover_tools(tools_path): """Scan the tools directory to find available tool modules""" available_tools = [] if not os.path.exists(tools_path): return available_tools for item in os.listdir(tools_path): if item.endswith('.py') and not item.startswith('__'): tool_name = item[:-3] # Remove .py extension available_tools.append(tool_name) return sorted(available_tools) # Function to get tool description def get_tool_description(tool_name, tools_path): """Extract a description from the tool module if available""" try: # Try to read the first few lines of the module to look for docstring or comments tool_path = os.path.join(tools_path, f"{tool_name}.py") with open(tool_path, 'r') as f: content = f.read(2000) # Read first 2000 characters # Look for docstring import re docstring_match = re.search(r'"""(.*?)"""', content, re.DOTALL) if docstring_match: description = docstring_match.group(1).strip() # Return first sentence or first 100 chars first_sentence = re.match(r'^(.*?[.!?])\s', description) if first_sentence: return first_sentence.group(1) return description[:100] + "..." if len(description) > 100 else description # Look for file header comment comment_match = re.search(r'# (.*?)\n', content) if comment_match: return comment_match.group(1).strip() return "No description available" except Exception: return "No description available" # Function to restart the MCP server def restart_server(server_path): """Restart the MCP server process""" try: # Check if server is running # This is a simplified check - in production you might want to use a more robust method result = subprocess.run( ["pgrep", "-f", server_path], capture_output=True, text=True ) if result.returncode == 0: # Server is running, kill it pid = result.stdout.strip() subprocess.run(["kill", pid]) st.success(f"Stopped MCP server (PID: {pid})") # Start server subprocess.Popen(["python", server_path]) return True except Exception as e: st.error(f"Error restarting server: {str(e)}") return False # Function to check if server is running def is_server_running(server_path): """Check if the MCP server is currently running""" try: result = subprocess.run( ["pgrep", "-f", server_path], capture_output=True, text=True ) return result.returncode == 0 except Exception: return False # Main application def main(): st.title("MCP Tools Configuration") # Sidebar for server control and config location with st.sidebar: st.header("Server Control") # Get paths project_root = st.text_input( "Project Directory", value=str(Path(__file__).parent.parent), help="The root directory of the MCP Unified Server project" ) tools_path = os.path.join(project_root, "tools") server_path = os.path.join(project_root, "mcp_unified_server.py") config_path = os.path.join(project_root, "config.yaml") # Check if server is running server_status = is_server_running(server_path) st.subheader("Server Status") if server_status: st.success("MCP Server is running") else: st.warning("MCP Server is not running") if st.button("Start/Restart Server"): with st.spinner("Restarting MCP Server..."): success = restart_server(server_path) if success: st.success("Server restarted successfully") # Show server path st.caption(f"Server path: {server_path}") # Divider st.divider() # Load and save config if st.button("Reload Configuration"): st.session_state.config = load_config(config_path) st.success("Configuration reloaded") save_button = st.button("Save Configuration") # Initialize or get session state for config if "config" not in st.session_state: st.session_state.config = load_config(config_path) # Discover available tools available_tools = discover_tools(tools_path) # If no tools are in the config yet, add all available ones with default state if "enabled_tools" not in st.session_state.config: st.session_state.config["enabled_tools"] = {} for tool in available_tools: if tool not in st.session_state.config["enabled_tools"]: st.session_state.config["enabled_tools"][tool] = False # Initialize tool_config if not present if "tool_config" not in st.session_state.config: st.session_state.config["tool_config"] = {} # Main content area with tabs tab1, tab2, tab3 = st.tabs(["Enable/Disable Tools", "Tool Configuration", "Advanced Settings"]) with tab1: st.header("Tool Management") st.info("Toggle tools on or off. Changes will take effect after saving and restarting the server.") # Create a container for the tools list tools_container = st.container() # Add tool toggle controls with tools_container: # Create columns for better layout cols = st.columns(2) # Sort tools by name sorted_tools = sorted(available_tools) # Distribute tools into columns half_length = (len(sorted_tools) + 1) // 2 for i, tool in enumerate(sorted_tools): col_idx = 0 if i < half_length else 1 with cols[col_idx]: tool_enabled = st.checkbox( f"{tool}", value=st.session_state.config["enabled_tools"].get(tool, False), help=get_tool_description(tool, tools_path), key=f"tool_{tool}" ) st.session_state.config["enabled_tools"][tool] = tool_enabled with tab2: st.header("Tool Configuration") st.info("Configure specific settings for each tool.") # Get list of enabled tools enabled_tools = [tool for tool, enabled in st.session_state.config["enabled_tools"].items() if enabled] # Tool selection selected_tool = st.selectbox( "Select a tool to configure", options=[""] + enabled_tools, format_func=lambda x: x if x else "Select a tool..." ) if selected_tool: st.subheader(f"Configuration for {selected_tool}") # Initialize config for this tool if it doesn't exist if selected_tool not in st.session_state.config["tool_config"]: st.session_state.config["tool_config"][selected_tool] = {} tool_config = st.session_state.config["tool_config"][selected_tool] # Special handling for common tools if selected_tool == "filesystem": # Filesystem-specific config allowed_dirs = tool_config.get("allowed_directories", []) allowed_dirs_str = "\n".join(allowed_dirs) if allowed_dirs else "" new_allowed_dirs = st.text_area( "Allowed Directories (one per line)", value=allowed_dirs_str, help="List of directories that the file system tools can access" ) # Update config tool_config["allowed_directories"] = [d.strip() for d in new_allowed_dirs.split("\n") if d.strip()] # Allow file deletion option allow_deletion = st.checkbox( "Allow File Deletion", value=tool_config.get("allow_file_deletion", False), help="Whether file deletion operations are allowed" ) tool_config["allow_file_deletion"] = allow_deletion elif selected_tool == "brave_search": # Brave Search specific config rate_limit = st.number_input( "Rate Limit (requests per minute)", min_value=1, max_value=1000, value=tool_config.get("rate_limit", 100), help="Maximum number of API requests per minute" ) tool_config["rate_limit"] = rate_limit max_results = st.number_input( "Maximum Results per Request", min_value=1, max_value=100, value=tool_config.get("max_results", 10), help="Maximum number of results to return per API request" ) tool_config["max_results"] = max_results api_key = st.text_input( "Brave API Key", value=tool_config.get("api_key", ""), type="password", help="Your Brave Search API key" ) if api_key: tool_config["api_key"] = api_key # Generic config editor for other tools else: st.write("Edit configuration as JSON:") config_json = json.dumps(tool_config, indent=2) if tool_config else "{}" edited_config = st.text_area( "Tool Configuration (JSON)", value=config_json, height=300 ) try: # Parse and update config if valid JSON if edited_config: new_config = json.loads(edited_config) st.session_state.config["tool_config"][selected_tool] = new_config except json.JSONDecodeError as e: st.error(f"Invalid JSON: {str(e)}") with tab3: st.header("Advanced Settings") # Environment variables display and edit st.subheader("Environment Variables") st.info("These environment variables are used by the MCP server and tools.") # Get current environment variables (filtered to relevant ones) relevant_prefixes = ["MCP_", "SF_", "BRAVE_", "BROWSERBASE_", "NEWS_API_", "SHOPIFY_", "LINKEDIN_"] env_vars = {k: v for k, v in os.environ.items() if any(k.startswith(prefix) for prefix in relevant_prefixes)} # Add some common environment variables if not present expected_vars = [ "MCP_HOST", "MCP_PORT", "MCP_LOG_LEVEL", "BRAVE_API_KEY", "BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID", "NEWS_API_KEY", "SF_USERNAME", "SF_PASSWORD", "SF_SECURITY_TOKEN", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET" ] for var in expected_vars: if var not in env_vars: env_vars[var] = "" # Display editable environment variables st.write("Note: Changes to environment variables won't persist between app restarts.") edited_vars = {} for var_name, var_value in sorted(env_vars.items()): # Use password input for sensitive values is_sensitive = any(s in var_name.upper() for s in ["KEY", "TOKEN", "SECRET", "PASSWORD"]) new_value = st.text_input( var_name, value=var_value, type="password" if is_sensitive else "default" ) if new_value != var_value: edited_vars[var_name] = new_value # Apply environment variable changes button if edited_vars and st.button("Apply Environment Variable Changes"): for var_name, var_value in edited_vars.items(): if var_value: os.environ[var_name] = var_value st.success(f"Updated {var_name}") st.warning("Note: These changes are only effective for this session.") # Raw config editor st.subheader("Raw Configuration") if st.checkbox("Edit Raw YAML Configuration"): raw_config = yaml.dump(st.session_state.config, default_flow_style=False) edited_raw_config = st.text_area( "Raw Configuration (YAML)", value=raw_config, height=400 ) if st.button("Parse and Apply Raw Configuration"): try: new_config = yaml.safe_load(edited_raw_config) st.session_state.config = new_config st.success("Raw configuration applied successfully") except Exception as e: st.error(f"Error parsing configuration: {str(e)}") # Save configuration if button was pressed if save_button: if save_config(st.session_state.config, config_path): st.sidebar.success("Configuration saved successfully!") st.sidebar.info("Restart the server to apply changes.") else: st.sidebar.error("Failed to save configuration.") # Run the app if __name__ == "__main__": main()