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>
    """