diff --git a/README.md b/README.md index 1ead076..14d8c9a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI version](https://img.shields.io/pypi/v/mcp-arduino.svg)](https://pypi.org/project/mcp-arduino/) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) -[![Tools: 60+](https://img.shields.io/badge/tools-60+-brightgreen.svg)](https://git.supported.systems/MCP/mcp-arduino) +[![Tools: 70+](https://img.shields.io/badge/tools-70+-brightgreen.svg)](https://git.supported.systems/MCP/mcp-arduino) **The Arduino development server that speaks your language.** @@ -235,6 +235,21 @@ Claude: → serial_read with cursor navigation āœ“ Memory usage: still just 10MB (fixed size) ``` +### šŸ†• **Smart Port Conflict Detection** +No more mysterious "device busy" errors - know exactly what's using your ports: +``` +You: "Connect to my Arduino" +Claude: → serial_check_port /dev/ttyUSB0 + āŒ Port conflict detected! + + šŸ”§ Arduino IDE (PID 12345) is using this port + šŸ’” Solution: Close Arduino IDE Serial Monitor + šŸ’” Or use: kill 12345 + + → serial_connect with automatic conflict prevention + āœ“ Connected successfully after conflict resolved +``` + ### šŸ” **Auto-Detect Everything** No more guessing board types or ports: ``` @@ -273,14 +288,158 @@ Claude: → arduino_debug_start ## šŸ“¦ What You Get -**60+ Tools** organized into logical groups: +**70+ Professional Tools** organized into logical groups: - **Sketch Operations**: Create, read, write, compile, upload - **Library Management**: Search, install, dependency resolution - **Board Management**: Detection, configuration, core installation -- **Serial Monitoring**: Memory-safe buffering, cursor pagination +- **Serial Monitoring**: Memory-safe buffering, cursor pagination, conflict detection - **Debugging**: GDB integration, breakpoints, memory inspection - **Project Templates**: WiFi, sensor, serial, blink patterns - **Circuit Diagrams**: Generate wiring diagrams from descriptions +- **Client Debugging**: MCP capability detection and troubleshooting + +--- + +## šŸ”§ Complete Tool & Resource Reference + +### šŸ“ **Arduino Sketch Management (8 tools)** +- `arduino_create_sketch` - Create new sketch with boilerplate code +- `arduino_list_sketches` - List all Arduino sketches +- `arduino_read_sketch` - Read sketch file contents +- `arduino_write_sketch` - Write/update sketch files +- `arduino_compile_sketch` - Compile sketch without uploading +- `arduino_upload_sketch` - Compile and upload to board +- `arduino_sketch_new` - Create sketch from templates (blink, sensor, wifi, etc.) +- `arduino_sketch_archive` - Create archive of sketch for sharing + +### šŸ“š **Library Management (12 tools)** +- `arduino_search_libraries` - Search Arduino library index +- `arduino_install_library` - Install library from index +- `arduino_uninstall_library` - Remove installed library +- `arduino_list_library_examples` - List examples from library +- `arduino_lib_deps` - Check library dependencies and compatibility +- `arduino_lib_download` - Download libraries without installing +- `arduino_lib_install_missing` - Install all missing dependencies automatically +- `arduino_lib_examples` - List examples from installed libraries +- `arduino_lib_list` - List installed libraries with version info +- `arduino_lib_upgrade` - Upgrade libraries to latest versions +- `arduino_update_index` - Update libraries and boards index +- `arduino_outdated` - List outdated libraries and cores + +### šŸ”§ **Board Management (11 tools)** +- `arduino_list_boards` - List connected Arduino boards +- `arduino_search_boards` - Search board definitions +- `arduino_install_core` - Install board support packages +- `arduino_list_cores` - List installed cores +- `arduino_update_cores` - Update all cores to latest +- `arduino_install_esp32` - Install ESP32 board support with proper config +- `arduino_board_attach` - Attach board to sketch persistently +- `arduino_board_details` - Get detailed board information and properties +- `arduino_board_identify` - Auto-detect board type from connected port +- `arduino_board_listall` - List all available boards from installed cores +- `arduino_board_search_online` - Search for boards in online index + +### šŸ› **Debug Tools (15 tools)** +- `arduino_debug_start` - Start GDB debug session +- `arduino_debug_stop` - Stop debug session and cleanup +- `arduino_debug_interactive` - Interactive debugging with AI assistance +- `arduino_debug_break` - Set breakpoints in code +- `arduino_debug_run` - Run/continue/step execution +- `arduino_debug_print` - Print variable values/expressions +- `arduino_debug_backtrace` - Show call stack +- `arduino_debug_watch` - Monitor variable changes +- `arduino_debug_memory` - Examine memory contents at addresses +- `arduino_debug_registers` - Show CPU register values +- `arduino_debug_list_breakpoints` - List all active breakpoints +- `arduino_debug_delete_breakpoint` - Delete specific or all breakpoints +- `arduino_debug_enable_breakpoint` - Enable/disable breakpoints +- `arduino_debug_condition_breakpoint` - Add conditional breakpoints +- `arduino_debug_save_breakpoints` - Save breakpoints to file +- `arduino_debug_restore_breakpoints` - Restore saved breakpoints + +### šŸ“” **Serial Monitor Tools (15 tools) - šŸ†• Enhanced with Smart Conflict Detection** +- `serial_check_port` - **šŸ†• Check port availability and detect conflicts** +- `serial_connect` - **šŸ†• Enhanced connection with automatic conflict detection** +- `serial_disconnect` - Disconnect from serial port +- `serial_send` - Send data/commands to serial port +- `serial_read` - Read data with cursor-based pagination +- `serial_list_ports` - List available serial ports (with Arduino detection) +- `serial_clear_buffer` - Clear buffered serial data +- `serial_reset_board` - Reset Arduino board (DTR/RTS/1200bps methods) +- `serial_monitor_state` - Get current monitor state and statistics +- `serial_cursor_info` - Get detailed cursor information +- `serial_list_cursors` - List all active cursors +- `serial_delete_cursor` - Delete specific cursor +- `serial_cleanup_cursors` - Remove invalid/stale cursors +- `serial_buffer_stats` - Get detailed circular buffer statistics +- `serial_resize_buffer` - Resize circular buffer (100-1M entries) + +### šŸŽØ **WireViz Circuit Diagrams (2 tools)** +- `wireviz_generate_from_yaml` - Create circuit diagram from YAML definition +- `wireviz_generate_from_description` - **AI-powered circuit generation from natural language** + +### āš™ļø **Advanced System Tools (13 tools)** +- `arduino_compile_advanced` - Advanced compilation with custom build properties +- `arduino_size_analysis` - Analyze binary size and memory usage breakdown +- `arduino_export_compiled_binary` - Export compiled binaries to custom location +- `arduino_cache_clean` - Clean Arduino build cache +- `arduino_build_show_properties` - Show all build properties for board +- `arduino_burn_bootloader` - Burn bootloader using external programmer +- `arduino_config_init` - Initialize Arduino CLI configuration +- `arduino_config_get` - Get specific configuration values +- `arduino_config_set` - Set configuration values +- `arduino_config_dump` - Dump entire Arduino CLI configuration +- `arduino_monitor_advanced` - Use Arduino CLI's built-in advanced serial monitor +- `arduino_show_directories` - Show current directory configuration and MCP roots +- `arduino_sketch_archive` - Create distributable archives of sketches + +### šŸ” **Client Debug Tools (5 tools)** +- `client_debug_info` - Show comprehensive MCP client debug information +- `client_capability_check` - Test specific client capabilities (sampling, etc.) +- `client_declared_capabilities` - Show what capabilities client declares +- `client_test_sampling` - Test client sampling functionality with simple prompt +- `client_fix_capabilities` - Suggest fixes for common capability issues + +--- + +## šŸ“‹ **MCP Resources (8 resources)** + +Real-time information resources that Claude can access to understand your current Arduino environment: + +### šŸš€ **Development Resources** +- **`arduino://sketches`** - List all Arduino sketches with sizes and paths +- **`arduino://libraries`** - List all installed Arduino libraries with versions +- **`arduino://boards`** - List all connected Arduino boards with details + +### šŸ”§ **Active Session Monitoring** +- **`arduino://debug/sessions`** - List all active debug sessions with status +- **`arduino://serial/state`** - Get current serial monitor state and connections + +### šŸ“š **Documentation & Configuration** +- **`wireviz://instructions`** - WireViz usage instructions and circuit examples +- **`server://info`** - Complete server configuration and capabilities overview +- **`arduino://roots`** - Current MCP roots configuration and directory setup + +--- + +## šŸŽÆ **Smart Features** + +### šŸ†• **Intelligent Port Conflict Detection** +- **Process Identification**: Recognizes Arduino IDE, PlatformIO, terminal programs +- **Specific Solutions**: Tailored advice for each type of conflicting application +- **Command Suggestions**: Provides exact kill commands and troubleshooting steps +- **Graceful Fallbacks**: Works even when detection tools are unavailable + +### 🧠 **Memory-Safe Architecture** +- **Circular Buffer**: Fixed 10MB memory usage regardless of runtime duration +- **Cursor Pagination**: Navigate through millions of serial entries efficiently +- **Auto-Cleanup**: Removes stale cursors and manages buffer health + +### šŸ”— **MCP Roots Integration** +- **Automatic Detection**: Uses client-provided project directories +- **Smart Selection**: Prefers Arduino-specific directories +- **Environment Override**: `MCP_SKETCH_DIR` for custom locations +- **Multi-Client Support**: Works across different MCP clients ## šŸŽ“ Perfect For diff --git a/src/mcp_arduino_server/components/__init__.py b/src/mcp_arduino_server/components/__init__.py index 5fd56b6..9fce50c 100644 --- a/src/mcp_arduino_server/components/__init__.py +++ b/src/mcp_arduino_server/components/__init__.py @@ -6,6 +6,7 @@ from .arduino_sketch import ArduinoSketch from .circular_buffer import CircularSerialBuffer from .client_capabilities import ClientCapabilitiesInfo from .client_debug import ClientDebugInfo +from .port_checker import PortConflictDetector, port_checker from .serial_manager import SerialConnectionManager from .wireviz import WireViz @@ -19,4 +20,6 @@ __all__ = [ "ClientCapabilitiesInfo", "SerialConnectionManager", "CircularSerialBuffer", + "PortConflictDetector", + "port_checker", ] diff --git a/src/mcp_arduino_server/components/arduino_serial.py b/src/mcp_arduino_server/components/arduino_serial.py index a7b2919..a10cf04 100644 --- a/src/mcp_arduino_server/components/arduino_serial.py +++ b/src/mcp_arduino_server/components/arduino_serial.py @@ -12,6 +12,7 @@ from pydantic import Field from .circular_buffer import CircularSerialBuffer, SerialDataType from .serial_manager import SerialConnectionManager +from .port_checker import port_checker # Use CircularSerialBuffer directly SerialDataBuffer = CircularSerialBuffer @@ -97,6 +98,49 @@ class ArduinoSerial(MCPMixin): {json.dumps(state['connections'], indent=2)} """ + @mcp_tool(name="serial_check_port", description="Check if a serial port is available or detect conflicts") + async def check_port( + self, + port: str = Field(..., description="Serial port path to check (e.g., /dev/ttyUSB0)"), + ctx: Context = None + ) -> dict: + """Check serial port availability and detect conflicts""" + try: + usage = await port_checker.check_port_usage(port) + + if usage is None: + return { + "success": True, + "available": True, + "port": port, + "message": f"āœ… Port {port} is available for connection" + } + else: + conflict_message = port_checker.generate_conflict_message(port, usage) + return { + "success": True, + "available": False, + "port": port, + "conflicts": [ + { + "pid": proc.pid, + "process_name": proc.process_name, + "command": proc.command, + "user": proc.user, + "description": str(proc) + } + for proc in usage + ], + "message": conflict_message, + "suggestions": "Use the provided commands to resolve conflicts before connecting" + } + except Exception as e: + return { + "success": False, + "error": f"Error checking port {port}: {str(e)}", + "port": port + } + @mcp_tool(name="serial_connect", description="Connect to a serial port for monitoring") async def connect( self, @@ -110,12 +154,43 @@ class ArduinoSerial(MCPMixin): dsrdtr: bool = Field(False, description="Enable hardware (DSR/DTR) flow control"), auto_monitor: bool = Field(True, description="Start monitoring automatically"), exclusive: bool = Field(False, description="Disconnect other ports first"), + force: bool = Field(False, description="Skip port conflict check and attempt connection anyway"), ctx: Context = None ) -> dict: - """Connect to a serial port""" + """Connect to a serial port with smart conflict detection""" if not self._initialized: await self.initialize() + # Smart port conflict detection (unless forced) + if not force: + try: + usage = await port_checker.check_port_usage(port) + if usage: + conflict_message = port_checker.generate_conflict_message(port, usage) + self.data_buffer.add_entry(port, "Connection blocked: Port in use", SerialDataType.ERROR) + return { + "success": False, + "error": "Port conflict detected", + "port": port, + "conflicts": [ + { + "pid": proc.pid, + "process_name": proc.process_name, + "command": proc.command, + "user": proc.user, + "description": str(proc) + } + for proc in usage + ], + "message": conflict_message, + "hint": "Use 'serial_check_port' for detailed conflict info, or set 'force=true' to override" + } + except Exception as e: + # If conflict detection fails, log warning but continue + import logging + logger = logging.getLogger(__name__) + logger.warning(f"Port conflict check failed for {port}: {e}") + try: conn = await self.connection_manager.connect( port=port, @@ -146,11 +221,24 @@ class ArduinoSerial(MCPMixin): "rtscts": rtscts, "dsrdtr": dsrdtr }, - "state": conn.state.value + "state": conn.state.value, + "message": f"āœ… Successfully connected to {port}" } except Exception as e: - self.data_buffer.add_entry(port, str(e), SerialDataType.ERROR) - return {"success": False, "error": str(e)} + # Enhanced error message with suggestions + error_msg = str(e) + if "permission denied" in error_msg.lower(): + error_msg += "\nšŸ’” Try: sudo usermod -a -G dialout $USER (then logout/login)" + elif "device or resource busy" in error_msg.lower() or "multiple access" in error_msg.lower(): + error_msg += f"\nšŸ’” Another process is using {port}. Use 'serial_check_port' to identify it." + + self.data_buffer.add_entry(port, error_msg, SerialDataType.ERROR) + return { + "success": False, + "error": error_msg, + "port": port, + "hint": f"Use 'serial_check_port {port}' to diagnose connection issues" + } @mcp_tool(name="serial_disconnect", description="Disconnect from a serial port") async def disconnect( diff --git a/src/mcp_arduino_server/components/port_checker.py b/src/mcp_arduino_server/components/port_checker.py new file mode 100644 index 0000000..a6bf0df --- /dev/null +++ b/src/mcp_arduino_server/components/port_checker.py @@ -0,0 +1,282 @@ +""" +Smart Port Conflict Detection for Arduino MCP Server +Proactively detects port usage conflicts and provides helpful guidance +""" + +import os +import subprocess +import logging +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + + +@dataclass +class PortUsage: + """Information about a process using a port""" + pid: int + command: str + user: str + fd_type: str + process_name: str + + def __str__(self) -> str: + return f"{self.process_name} (PID {self.pid}) by {self.user}" + + +class PortConflictDetector: + """Detects and reports serial port usage conflicts""" + + def __init__(self): + self.common_arduino_processes = { + 'arduino': 'Arduino IDE', + 'arduino-cli': 'Arduino CLI', + 'platformio': 'PlatformIO', + 'pio': 'PlatformIO CLI', + 'esptool': 'ESP Flash Tool', + 'avrdude': 'AVR Programming Tool', + 'python': 'Python Script (possibly MCP server)', + 'minicom': 'Minicom Terminal', + 'screen': 'GNU Screen', + 'picocom': 'Picocom Terminal', + 'cu': 'Unix Terminal', + 'putty': 'PuTTY', + 'tio': 'TIO Terminal' + } + + async def check_port_usage(self, port: str) -> Optional[List[PortUsage]]: + """ + Check if a serial port is being used by another process + Returns list of processes using the port, or None if available + """ + try: + # Try lsof first (most detailed info) + usage = await self._check_with_lsof(port) + if usage: + return usage + + # Fallback to fuser if lsof fails + usage = await self._check_with_fuser(port) + if usage: + return usage + + # Try direct file access test + if await self._test_direct_access(port): + # Port is busy but we can't identify the process + return [PortUsage( + pid=0, + command="unknown", + user="unknown", + fd_type="unknown", + process_name="Unknown Process" + )] + + return None # Port is available + + except Exception as e: + logger.warning(f"Error checking port usage for {port}: {e}") + return None + + async def _check_with_lsof(self, port: str) -> Optional[List[PortUsage]]: + """Check port usage using lsof command""" + try: + result = subprocess.run( + ['lsof', port], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0 and result.stdout: + return self._parse_lsof_output(result.stdout) + + except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): + pass + return None + + async def _check_with_fuser(self, port: str) -> Optional[List[PortUsage]]: + """Check port usage using fuser command""" + try: + result = subprocess.run( + ['fuser', port], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0 and result.stdout.strip(): + pids = [int(pid.strip()) for pid in result.stdout.split() if pid.strip().isdigit()] + return [await self._get_process_info(pid) for pid in pids] + + except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError, ValueError): + pass + return None + + async def _test_direct_access(self, port: str) -> bool: + """Test if port is accessible by trying to open it briefly""" + try: + import serial + # Very brief test - just try to open and immediately close + with serial.Serial(port, timeout=0.1) as ser: + pass + return False # Successfully opened, so available + except (serial.SerialException, PermissionError, FileNotFoundError): + # If we can't open it, something else might be using it + # But could also be permissions or non-existent port + if Path(port).exists(): + return True # Port exists but can't open - likely in use + return False # Port doesn't exist + + def _parse_lsof_output(self, output: str) -> List[PortUsage]: + """Parse lsof output to extract process information""" + processes = [] + lines = output.strip().split('\n')[1:] # Skip header + + for line in lines: + parts = line.split(None, 8) # Split into max 9 parts + if len(parts) >= 8: + try: + processes.append(PortUsage( + pid=int(parts[1]), + command=parts[0], + user=parts[2], + fd_type=parts[4], + process_name=parts[0] + )) + except ValueError: + continue + + return processes + + async def _get_process_info(self, pid: int) -> PortUsage: + """Get detailed process information for a PID""" + try: + # Get process name and command from /proc/pid/comm and /proc/pid/cmdline + comm_path = f"/proc/{pid}/comm" + cmdline_path = f"/proc/{pid}/cmdline" + + process_name = "unknown" + command = "unknown" + + if Path(comm_path).exists(): + with open(comm_path, 'r') as f: + process_name = f.read().strip() + + if Path(cmdline_path).exists(): + with open(cmdline_path, 'r') as f: + cmdline = f.read().replace('\0', ' ').strip() + command = cmdline if cmdline else process_name + + # Get user from process status + user = "unknown" + try: + result = subprocess.run(['ps', '-p', str(pid), '-o', 'user='], + capture_output=True, text=True, timeout=2) + if result.returncode == 0: + user = result.stdout.strip() + except: + pass + + return PortUsage( + pid=pid, + command=command, + user=user, + fd_type="chr", + process_name=process_name + ) + + except Exception: + return PortUsage( + pid=pid, + command="unknown", + user="unknown", + fd_type="unknown", + process_name="unknown" + ) + + def generate_conflict_message(self, port: str, usage: List[PortUsage]) -> str: + """Generate a helpful error message for port conflicts""" + if not usage: + return f"Port {port} is available" + + message_parts = [f"āŒ **Port {port} is currently in use:**\n"] + + for proc in usage: + # Try to identify the type of application + app_type = self._identify_application_type(proc.process_name.lower()) + + if proc.pid > 0: + message_parts.append(f" • **{app_type}** - {proc} (FD: {proc.fd_type})") + else: + message_parts.append(f" • **{app_type}** - Process details unavailable") + + message_parts.extend([ + "\n**šŸ”§ Suggested Solutions:**", + self._get_solutions_for_processes(usage), + "\n**šŸ’” Quick Commands:**" + ]) + + # Add specific commands based on detected processes + commands = self._get_suggested_commands(port, usage) + message_parts.extend(commands) + + return "\n".join(message_parts) + + def _identify_application_type(self, process_name: str) -> str: + """Identify the type of application using the port""" + for pattern, app_name in self.common_arduino_processes.items(): + if pattern in process_name: + return app_name + return "Unknown Application" + + def _get_solutions_for_processes(self, usage: List[PortUsage]) -> str: + """Generate process-specific solutions""" + solutions = [] + + for proc in usage: + process_lower = proc.process_name.lower() + + if 'arduino' in process_lower: + solutions.append(" 1. Close Arduino IDE serial monitor (Tools → Serial Monitor)") + elif 'platformio' in process_lower: + solutions.append(" 1. Stop PlatformIO monitor: `pio device monitor --exit`") + elif 'python' in process_lower: + solutions.append(" 1. Stop the Python script/MCP server using the port") + elif any(term in process_lower for term in ['minicom', 'screen', 'picocom', 'cu']): + solutions.append(f" 1. Exit the terminal program: {proc.process_name}") + elif 'esptool' in process_lower or 'avrdude' in process_lower: + solutions.append(" 1. Wait for programming/flashing to complete") + else: + solutions.append(f" 1. Stop or close {proc.process_name}") + + if not solutions: + solutions.append(" 1. Identify and close the application using the port") + + solutions.append(" 2. Unplug and reconnect the Arduino device") + solutions.append(" 3. Try a different USB port") + + return "\n".join(solutions) + + def _get_suggested_commands(self, port: str, usage: List[PortUsage]) -> List[str]: + """Generate helpful commands based on detected processes""" + commands = [] + + valid_pids = [proc.pid for proc in usage if proc.pid > 0] + + if valid_pids: + commands.append(f" • **Kill process:** `kill {' '.join(map(str, valid_pids))}`") + commands.append(f" • **Force kill:** `kill -9 {' '.join(map(str, valid_pids))}`") + + commands.extend([ + f" • **Check port:** `lsof {port}`", + f" • **List all processes:** `fuser {port}`", + " • **List Arduino ports:** Use `serial_list_ports` with `arduino_only=true`" + ]) + + return commands + + +# Global instance for easy access +port_checker = PortConflictDetector() \ No newline at end of file diff --git a/src/mcp_arduino_server/components/wireviz.py b/src/mcp_arduino_server/components/wireviz.py index a35e30b..1266cf2 100644 --- a/src/mcp_arduino_server/components/wireviz.py +++ b/src/mcp_arduino_server/components/wireviz.py @@ -550,7 +550,6 @@ options: fontname: arial bgcolor: white color_mode: full - notes: Pull-up resistor (10kĪ©) connects D2 to 5V """ def _generate_display_template(self, description: str) -> str: @@ -601,7 +600,6 @@ options: fontname: arial bgcolor: white color_mode: full - notes: I2C communication at 0x3C or 0x27 address """ def _clean_yaml_content(self, content: str) -> str: