Spaces:
Build error
Build error
File size: 8,985 Bytes
85dd3af |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
"""
Tools Manager Module
Handles MCP Server installation, configuration, and status display.
Completely isolated from Chat logic to prevent side effects.
"""
import os
import json
from typing import Dict, List, Any, Optional, Tuple
# Path to mcp.json (relative to app.py)
MCP_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "../../mcp.json")
def load_mcp_config() -> Dict[str, Any]:
"""Loads the mcp.json configuration file."""
abs_path = os.path.abspath(MCP_CONFIG_PATH)
if not os.path.exists(abs_path):
return {"mcpServers": {}}
try:
with open(abs_path, "r") as f:
return json.load(f)
except Exception as e:
print(f"Error loading mcp.json: {e}")
return {"mcpServers": {}}
def save_mcp_config(config: Dict[str, Any]) -> bool:
"""Saves the configuration to mcp.json."""
abs_path = os.path.abspath(MCP_CONFIG_PATH)
try:
with open(abs_path, "w") as f:
json.dump(config, f, indent=2)
return True
except Exception as e:
print(f"Error saving mcp.json: {e}")
return False
def parse_mcp_command(full_command: str) -> Tuple[str, str, List[str]]:
"""
Parses a full MCP command string into components.
Examples:
"npx -y @modelcontextprotocol/server-memory" -> ("npx", "memory", ["-y", "@modelcontextprotocol/server-memory"])
"uvx mcp-server-filesystem /path" -> ("uvx", "filesystem", ["mcp-server-filesystem", "/path"])
"python server.py" -> ("python", "server", ["server.py"])
Returns:
(command, suggested_name, args_list)
"""
parts = full_command.strip().split()
if not parts:
return "", "", []
command = parts[0] # npx, uvx, python, etc.
args_list = parts[1:] if len(parts) > 1 else []
# Try to extract a meaningful name
suggested_name = ""
for arg in args_list:
# Skip flags
if arg.startswith("-"):
continue
# Handle @org/package-name format
if arg.startswith("@") and "/" in arg:
# @modelcontextprotocol/server-memory -> memory
package_name = arg.split("/")[-1]
# server-memory -> memory
if package_name.startswith("server-"):
suggested_name = package_name[7:] # Remove "server-" prefix
elif package_name.startswith("mcp-server-"):
suggested_name = package_name[11:] # Remove "mcp-server-" prefix
else:
suggested_name = package_name
break
# Handle mcp-server-xxx format
if "mcp-server-" in arg:
suggested_name = arg.split("mcp-server-")[-1].split()[0]
break
# Handle server-xxx format
if arg.startswith("server-"):
suggested_name = arg[7:]
break
# Handle .py files
if arg.endswith(".py"):
suggested_name = arg[:-3].split("/")[-1]
break
# Use the first non-flag argument as fallback
if not suggested_name:
suggested_name = arg.split("/")[-1].split(".")[0]
return command, suggested_name, args_list
def add_server_to_config(
name: str,
command: str,
args: str,
description: str = ""
) -> Tuple[bool, str]:
"""
Adds a new MCP Server to the configuration.
Now supports parsing full command strings.
Args:
name: Server identifier (optional, will be auto-detected)
command: Full command string (e.g., "npx -y @modelcontextprotocol/server-memory")
args: Extra arguments (optional, appended to parsed args)
description: Optional description
Returns:
(success: bool, message: str)
"""
if not command:
return False, "Command is required."
# Parse the command
parsed_cmd, suggested_name, parsed_args = parse_mcp_command(command)
if not parsed_cmd:
return False, "Invalid command format."
# Use provided name or auto-detected name
final_name = name.strip() if name.strip() else suggested_name
if not final_name:
return False, "Could not detect server name. Please provide one."
# Sanitize name (no spaces, lowercase)
final_name = final_name.lower().replace(" ", "-")
# Combine parsed args with extra args
extra_args = args.strip().split() if args.strip() else []
final_args = parsed_args + extra_args
# Load existing config
config = load_mcp_config()
# Check if already exists
if final_name in config.get("mcpServers", {}):
return False, f"Server '{final_name}' already exists. Remove it first."
# Add new server
config.setdefault("mcpServers", {})[final_name] = {
"command": parsed_cmd,
"args": final_args,
"_description": description.strip() or f"MCP Server: {final_name}"
}
# Save
if save_mcp_config(config):
return True, f"Server '{final_name}' installed successfully!"
else:
return False, "Failed to save configuration."
def remove_server_from_config(name: str) -> Tuple[bool, str]:
"""Removes an MCP Server from the configuration."""
config = load_mcp_config()
if name not in config.get("mcpServers", {}):
return False, f"Server '{name}' not found."
del config["mcpServers"][name]
if save_mcp_config(config):
return True, f"Server '{name}' removed."
else:
return False, "Failed to save configuration."
def get_installed_servers() -> List[Dict[str, Any]]:
"""
Returns a list of installed servers with their details.
Each item: {name, command, args, description, is_connected}
"""
config = load_mcp_config()
servers = []
for name, details in config.get("mcpServers", {}).items():
servers.append({
"name": name,
"command": details.get("command", ""),
"args": details.get("args", []),
"description": details.get("_description", ""),
"is_connected": False # Will be updated by connection logic later
})
return servers
def get_server_details_html(server_name: str, is_connected: bool = False) -> str:
"""
Generates HTML for the server detail view.
Args:
server_name: Name of the server
is_connected: Whether the server is currently connected
"""
# Import here to avoid circular imports
from ui.components import get_server_emoji
config = load_mcp_config()
server = config.get("mcpServers", {}).get(server_name)
if not server:
return f"<div style='color: #FF3333; font-family: var(--font-mono);'>Server '{server_name}' not found.</div>"
command = server.get('command', '')
args_list = server.get("args", [])
args_str = " ".join(args_list)
description = server.get("_description", "No description provided.")
env_vars = server.get("env", {})
# Get emoji from centralized mapping
icon = get_server_emoji(server_name)
# Environment variables section (only show if exists)
env_html = ""
if env_vars:
env_items = "<br>".join([
f"<code style='color: #000;'>{k}={v[:20]}...</code>" if len(str(v)) > 20
else f"<code style='color: #000;'>{k}={v}</code>"
for k, v in env_vars.items()
])
env_html = f"""
<div class="detail-row">
<div class="detail-label">ENV</div>
<div class="detail-value">{env_items}</div>
</div>
"""
# Status indicator removed - now shown in top status bar
return f"""
<div style="font-family: var(--font-mono); color: #000000;">
<h2 style="margin: 0 0 24px 0; display: flex; align-items: center; gap: 12px; color: #000000;">
<span style="font-size: 32px;">{icon}</span>
<span style="color: #000000; font-weight: 700;">{server_name}</span>
</h2>
<div class="detail-row">
<div class="detail-label">COMMAND</div>
<div class="detail-value">
<code style="background: #1a1a1a; color: #00FF94; padding: 8px 12px; display: inline-block; border-radius: 4px;">{command}</code>
</div>
</div>
<div class="detail-row">
<div class="detail-label">ARGUMENTS</div>
<div class="detail-value">
<code style="background: #1a1a1a; color: #00FF94; padding: 8px 12px; display: block; word-break: break-all; border-radius: 4px;">{args_str or '(none)'}</code>
</div>
</div>
<div class="detail-row">
<div class="detail-label">DESCRIPTION</div>
<div class="detail-value" style="color: #333;">{description or 'No description provided.'}</div>
</div>
{env_html}
</div>
"""
|