From 3ee8ce6338c35b7e48d788d2dddfa9b6a70381cb Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Tue, 25 Mar 2025 22:14:33 +0100 Subject: [PATCH] feat(mcp): improve inspector reliability over re-run --- README.md | 22 +++++++- docs/specs/2_MCP_SERVER.md | 36 ++++++++++--- mcontainer/cli.py | 101 ++++++++++++++++++++++++++++--------- mcontainer/mcp.py | 19 ++++--- 4 files changed, 141 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 8052506..1a5f42a 100644 --- a/README.md +++ b/README.md @@ -225,11 +225,31 @@ mc mcp list # View detailed status of an MCP server mc mcp status github -# Start/stop/restart an MCP server +# Start/stop/restart individual MCP servers mc mcp start github mc mcp stop github mc mcp restart github +# Start all MCP servers at once +mc mcp start --all + +# Stop and remove all MCP servers at once +mc mcp stop --all + +# Run the MCP Inspector to visualize and interact with MCP servers +# It automatically joins all MCP networks for seamless DNS resolution +# Uses two ports: frontend UI (default: 5173) and backend API (default: 3000) +mc mcp inspector + +# Run the MCP Inspector with custom ports +mc mcp inspector --client-port 6173 --server-port 6174 + +# Run the MCP Inspector in detached mode +mc mcp inspector --detach + +# Stop the MCP Inspector +mc mcp inspector --stop + # View MCP server logs mc mcp logs github diff --git a/docs/specs/2_MCP_SERVER.md b/docs/specs/2_MCP_SERVER.md index 6a5a317..e4b0755 100644 --- a/docs/specs/2_MCP_SERVER.md +++ b/docs/specs/2_MCP_SERVER.md @@ -54,11 +54,17 @@ mcps: ``` mc mcp list # List all configured MCP servers and their status -mc mcp status # Show detailed status of a specific MCP server -mc mcp start # Start an MCP server container -mc mcp stop # Stop an MCP server container -mc mcp restart # Restart an MCP server container -mc mcp logs # Show logs for an MCP server container +mc mcp status # Show detailed status of a specific MCP server +mc mcp start # Start an MCP server container +mc mcp stop # Stop and remove an MCP server container +mc mcp restart # Restart an MCP server container +mc mcp start --all # Start all MCP server containers +mc mcp stop --all # Stop and remove all MCP server containers +mc mcp inspector # Run the MCP Inspector UI with network connectivity to all MCP servers +mc mcp inspector --client-port --server-port # Run with custom client port (default: 5173) and server port (default: 3000) +mc mcp inspector --detach # Run the inspector in detached mode +mc mcp inspector --stop # Stop the running inspector +mc mcp logs # Show logs for an MCP server container ``` ### MCP Configuration @@ -84,10 +90,28 @@ mc session create [--mcp ] # Create a session with an MCP server attached ### MCP Container Management -1. MCP containers will have their own dedicated Docker network +1. MCP containers will have their own dedicated Docker network (`mc-mcp-network`) 2. Session containers will be attached to both their session network and the MCP network when using an MCP 3. MCP containers will be persistent across sessions unless explicitly stopped 4. MCP containers will be named with a prefix to identify them (`mc_mcp_`) +5. Each MCP container will have a network alias matching its name without the prefix (e.g., `mc_mcp_github` will have the alias `github`) +6. Network aliases enable DNS-based service discovery between containers + +### MCP Inspector + +The MCP Inspector is a web-based UI tool that allows you to: + +1. Visualize and interact with multiple MCP servers +2. Debug MCP server messages and interactions +3. Test MCP server capabilities directly + +The MCP Inspector implementation includes: + +1. A container based on the `mcp/inspector` image +2. Automatic joining of all MCP server networks for seamless DNS resolution +3. A modified Express server that binds to all interfaces (0.0.0.0) +4. Port mapping for both the frontend (default: 5173) and backend API (default: 3000) +5. Network connectivity to all MCP servers using their simple names as DNS hostnames ### Proxy-based MCP Servers (Default) diff --git a/mcontainer/cli.py b/mcontainer/cli.py index f412ef1..e3c3e67 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -1047,7 +1047,7 @@ def stop_mcp( not_running_count = 0 failed_count = 0 - console.print(f"Stopping {len(mcps)} MCP servers...") + console.print(f"Stopping and removing {len(mcps)} MCP servers...") for mcp in mcps: mcp_name = mcp.get("name") @@ -1055,25 +1055,31 @@ def stop_mcp( continue try: - with console.status(f"Stopping MCP server '{mcp_name}'..."): + with console.status( + f"Stopping and removing MCP server '{mcp_name}'..." + ): result = mcp_manager.stop_mcp(mcp_name) if result: - console.print(f"[green]Stopped MCP server '{mcp_name}'[/green]") + console.print( + f"[green]Stopped and removed MCP server '{mcp_name}'[/green]" + ) stopped_count += 1 else: console.print( - f"[yellow]MCP server '{mcp_name}' was not running[/yellow]" + f"[yellow]MCP server '{mcp_name}' was not running or doesn't exist[/yellow]" ) not_running_count += 1 except Exception as e: - console.print(f"[red]Error stopping MCP server '{mcp_name}': {e}[/red]") + console.print( + f"[red]Error stopping/removing MCP server '{mcp_name}': {e}[/red]" + ) failed_count += 1 # Show a summary if stopped_count > 0: console.print( - f"[green]Successfully stopped {stopped_count} MCP servers[/green]" + f"[green]Successfully stopped and removed {stopped_count} MCP servers[/green]" ) if not_running_count > 0: console.print( @@ -1085,16 +1091,18 @@ def stop_mcp( # Otherwise stop a specific server elif name: try: - with console.status(f"Stopping MCP server '{name}'..."): + with console.status(f"Stopping and removing MCP server '{name}'..."): result = mcp_manager.stop_mcp(name) if result: - console.print(f"[green]Stopped MCP server '{name}'[/green]") + console.print(f"[green]Stopped and removed MCP server '{name}'[/green]") else: - console.print(f"[yellow]MCP server '{name}' was not running[/yellow]") + console.print( + f"[yellow]MCP server '{name}' was not running or doesn't exist[/yellow]" + ) except Exception as e: - console.print(f"[red]Error stopping MCP server: {e}[/red]") + console.print(f"[red]Error stopping/removing MCP server: {e}[/red]") else: console.print( "[red]Error: Please provide a server name or use --all to stop all servers[/red]" @@ -1321,8 +1329,17 @@ def add_remote_mcp( @mcp_app.command("inspector") def run_mcp_inspector( - host_port: int = typer.Option( - 5173, "--port", "-p", help="Host port for the MCP Inspector" + client_port: int = typer.Option( + 5173, + "--client-port", + "-c", + help="Port for the MCP Inspector frontend (default: 5173)", + ), + server_port: int = typer.Option( + 3000, + "--server-port", + "-s", + help="Port for the MCP Inspector backend API (default: 3000)", ), detach: bool = typer.Option(False, "--detach", "-d", help="Run in detached mode"), stop: bool = typer.Option(False, "--stop", help="Stop running MCP Inspector(s)"), @@ -1374,19 +1391,33 @@ def run_mcp_inspector( f"[yellow]Warning: Could not remove existing inspector: {e}[/yellow]" ) - # Check if the specified port is already in use + # Check if the specified ports are already in use import socket - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # Check client port + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - s.bind(("0.0.0.0", host_port)) - s.close() + client_socket.bind(("0.0.0.0", client_port)) + client_socket.close() except socket.error: console.print( - f"[red]Error: Port {host_port} is already in use by another process.[/red]" + f"[red]Error: Client port {client_port} is already in use by another process.[/red]" ) console.print("Please stop any web servers or other processes using this port.") - console.print("You can try a different port with --port option") + console.print("You can try a different client port with --client-port option") + return + + # Check server port + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + server_socket.bind(("0.0.0.0", server_port)) + server_socket.close() + except socket.error: + console.print( + f"[red]Error: Server port {server_port} is already in use by another process.[/red]" + ) + console.print("Please stop any web servers or other processes using this port.") + console.print("You can try a different server port with --server-port option") return # Container name with timestamp to avoid conflicts @@ -1601,11 +1632,16 @@ exec npm start detach=True, network=initial_network, ports={ - "5173/tcp": host_port, # Map container port 5173 to host port (frontend) - "3000/tcp": 3000, # Map container port 3000 to host port 3000 (backend) + f"{client_port}/tcp": client_port, # Map container port to host port (frontend) + f"{server_port}/tcp": server_port, # Map container port to host port (backend) }, environment={ - "SERVER_PORT": "3000", # Tell the server to use port 3000 (default) + "CLIENT_PORT": str( + client_port + ), # Tell the client to use the client_port + "SERVER_PORT": str( + server_port + ), # Tell the server to use the server_port }, volumes={ script_path: { @@ -1693,13 +1729,32 @@ exec npm start return console.print("[bold]MCP Inspector is available at:[/bold]") - console.print(f"- Frontend: http://localhost:{host_port}") - console.print("- Backend API: http://localhost:3000") + console.print(f"- Frontend: http://localhost:{client_port}") + console.print(f"- Backend API: http://localhost:{server_port}") if len(mcp_servers) > 0: console.print( f"[green]Auto-connected to {len(mcp_servers)} MCP servers[/green]" ) + + # Print MCP server URLs for access within the Inspector + console.print("[bold]MCP Server URLs (for use within Inspector):[/bold]") + for mcp in all_mcps: + mcp_name = mcp.get("name") + mcp_type = mcp.get("type") + + if mcp_type in ["docker", "proxy"]: + # For container-based MCPs, use the container name as hostname + # Default SSE port is 8080 unless specified in proxy_options + sse_port = "8080" + if mcp_type == "proxy" and "proxy_options" in mcp: + sse_port = str(mcp.get("proxy_options", {}).get("sse_port", "8080")) + console.print(f"- {mcp_name}: http://{mcp_name}:{sse_port}/sse") + elif mcp_type == "remote": + # For remote MCPs, use the configured URL + mcp_url = mcp.get("url") + if mcp_url: + console.print(f"- {mcp_name}: {mcp_url}") else: console.print( "[yellow]Warning: No MCP servers found or started. The Inspector will run but won't have any servers to connect to.[/yellow]" diff --git a/mcontainer/mcp.py b/mcontainer/mcp.py index 35bc150..087d801 100644 --- a/mcontainer/mcp.py +++ b/mcontainer/mcp.py @@ -475,21 +475,26 @@ ENTRYPOINT ["/entrypoint.sh"] # Get the container name container_name = self.get_mcp_container_name(name) - # Try to get and stop the container + # Try to get, stop, and remove the container try: container = self.client.containers.get(container_name) - # Only stop if it's running + + # Stop the container if it's running if container.status == "running": + logger.info(f"Stopping MCP container '{name}'...") container.stop(timeout=10) - return True - else: - # Container exists but is not running - return False + + # Remove the container regardless of its status + logger.info(f"Removing MCP container '{name}'...") + container.remove(force=True) + return True + except NotFound: # Container doesn't exist + logger.info(f"MCP container '{name}' not found, nothing to stop or remove") return False except Exception as e: - logger.error(f"Error stopping MCP container: {e}") + logger.error(f"Error stopping/removing MCP container: {e}") return False def restart_mcp(self, name: str) -> Dict[str, Any]: