#!/usr/bin/env -S uv run --script # pyright: reportUnknownMemberType=false """ FastLED MCP Server - Provides tools for working with the FastLED project 🚨 CRITICAL FOR ALL AGENTS (FOREGROUND & BACKGROUND): Before concluding that your work is done, the AI shall read the cursor rules again to find out what to do before task is considered complete. This ensures you have the latest coding standards, testing requirements, and completion validation rules. This is MANDATORY for both foreground and background agents. 🚨 CRITICAL FOR BACKGROUND AGENTS: When modifying ANY Python files (*.py), you MUST run `bash lint` immediately after making changes and before indicating completion. This is a NON-NEGOTIABLE REQUIREMENT to ensure code quality and prevent build failures. 🚨 MANDATORY COMMAND EXECUTION RULES 🚨 ALL AGENTS MUST FOLLOW THESE COMMAND EXECUTION PATTERNS: ### Python Code Execution: - [ERROR] NEVER run Python code directly - [OK] ALWAYS create/modify tmp.py with your code - [OK] ALWAYS run: `uv run tmp.py` ### Shell Command Execution: - [ERROR] NEVER run shell commands directly - [OK] ALWAYS create/modify tmp.sh with your commands - [OK] ALWAYS run: `bash tmp.sh` ### DELETE Operations - DANGER ZONE: - 🚨 STOP and ask for permission before ANY delete operations - [OK] EXCEPTION: Single files that you just created are OK to delete - [ERROR] NEVER delete multiple files without explicit permission - [ERROR] NEVER delete directories without explicit permission - [ERROR] NEVER delete system files or project files without permission ### Examples: **Python Code:** ```python # tmp.py import subprocess result = subprocess.run(['git', 'status'], capture_output=True, text=True) print(result.stdout) ``` Then run: `uv run tmp.py` **Shell Commands:** ```bash # tmp.sh #!/bin/bash git status ls -la ``` Then run: `bash tmp.sh` **Why These Rules:** - Ensures all operations are reviewable and traceable - Prevents accidental destructive operations - Allows for better debugging and error handling - Maintains consistency across all agent operations To use this server, make sure you have the MCP library installed: pip install mcp Or use with uv: uv add mcp uv run mcp_server.py """ import asyncio import sys import re import subprocess import shutil import tempfile import time from pathlib import Path from typing import Any, Dict, List, Union try: from mcp.server import Server # type: ignore from mcp.server.stdio import stdio_server # type: ignore from mcp.types import ( # type: ignore CallToolResult, TextContent, Tool, ) MCP_AVAILABLE = True except ImportError: print("Error: MCP library not found.") print("Please install it with: pip install mcp") print("Or with uv: uv add mcp") sys.exit(1) from ci.util.build_info_analyzer import BuildInfoAnalyzer # type: ignore # Initialize the MCP server server = Server("fastled-mcp-server") @server.list_tools() async def list_tools() -> List[Tool]: """List available tools for the FastLED project.""" return [ Tool( name="run_tests", description="Run FastLED tests with various options", inputSchema={ "type": "object", "properties": { "test_type": { "type": "string", "enum": ["all", "cpp", "specific"], "description": "Type of tests to run", "default": "all" }, "specific_test": { "type": "string", "description": "Name of specific C++ test to run (without 'test_' prefix, e.g. 'algorithm' for test_algorithm.cpp)" }, "test_case": { "type": "string", "description": "Specific TEST_CASE name to run within a test file (requires doctest filtering)" }, "use_clang": { "type": "boolean", "description": "Use Clang compiler instead of default", "default": False }, "clean": { "type": "boolean", "description": "Clean build before compiling", "default": False }, "verbose": { "type": "boolean", "description": "Enable verbose output showing all test details", "default": False } }, "required": ["test_type"] } ), Tool( name="list_test_cases", description="List TEST_CASEs available in FastLED test files", inputSchema={ "type": "object", "properties": { "test_file": { "type": "string", "description": "Specific test file to analyze (without 'test_' prefix and '.cpp' extension, e.g. 'algorithm')" }, "search_pattern": { "type": "string", "description": "Search pattern to filter TEST_CASE names" } } } ), Tool( name="compile_examples", description="Compile FastLED examples for different platforms using 'bash compile --examples '", inputSchema={ "type": "object", "properties": { "platform": { "type": "string", "description": "Target platform (e.g., 'uno', 'esp32', 'teensy')", "default": "uno" }, "examples": { "type": "array", "items": {"type": "string"}, "description": "List of example names to compile", "default": ["Blink"] }, "interactive": { "type": "boolean", "description": "Run in interactive mode", "default": False } } } ), Tool( name="code_fingerprint", description="Calculate a fingerprint of the codebase to detect changes", inputSchema={ "type": "object", "properties": { "directory": { "type": "string", "description": "Directory to fingerprint (relative to project root)", "default": "src" }, "patterns": { "type": "string", "description": "Glob patterns for files to include", "default": "**/*.h,**/*.cpp,**/*.hpp" } } } ), Tool( name="lint_code", description="Run comprehensive code formatting and linting. For FOREGROUND agents, this runs `bash lint` for complete coverage. For BACKGROUND agents, can run specific tools for fine-grained control.", inputSchema={ "type": "object", "properties": { "tool": { "type": "string", "enum": ["bash_lint", "ruff", "javascript", "all"], "description": "Linting approach: 'bash_lint' (recommended for foreground), 'ruff' (Python only), 'javascript' (JS only), 'all' (comprehensive via bash lint)", "default": "bash_lint" }, "agent_type": { "type": "string", "enum": ["foreground", "background"], "description": "Agent type - foreground agents should use bash_lint, background agents can use specific tools", "default": "foreground" }, "fix": { "type": "boolean", "description": "Automatically fix issues where possible (only applies to specific tools, not bash_lint)", "default": False } } } ), Tool( name="list_examples", description="List available FastLED examples", inputSchema={ "type": "object", "properties": { "category": { "type": "string", "description": "Filter by category (optional)" } } } ), Tool( name="project_info", description="Get information about the FastLED project structure and status", inputSchema={ "type": "object", "properties": { "include_git_status": { "type": "boolean", "description": "Include git status information", "default": True } } } ), Tool( name="run_specific_command", description="Run a specific command in the project context", inputSchema={ "type": "object", "properties": { "command": { "type": "string", "description": "Command to run" }, "working_directory": { "type": "string", "description": "Working directory (relative to project root)", "default": "." } }, "required": ["command"] } ), Tool( name="test_instructions", description="Get detailed instructions on how to run TEST_CASEs in FastLED", inputSchema={ "type": "object", "properties": {} } ), Tool( name="setup_stack_traces", description="Install and configure stack trace debugging libraries (libunwind/execinfo)", inputSchema={ "type": "object", "properties": { "method": { "type": "string", "enum": ["libunwind", "execinfo", "auto"], "description": "Stack trace method to install and configure", "default": "auto" }, "test_installation": { "type": "boolean", "description": "Test the installation with crash tests", "default": True }, "install_only": { "type": "boolean", "description": "Only install packages, don't run tests", "default": False } } } ), Tool( name="coding_standards", description="Get FastLED coding standards and best practices for C++ development", inputSchema={ "type": "object", "properties": { "topic": { "type": "string", "enum": ["all", "exceptions", "std_namespace", "naming", "member_naming", "containers", "debug", "bindings", "arduino_includes", "variable_naming", "python_linting"], "description": "Specific topic to get standards for, or 'all' for complete guide", "default": "all" } } } ), Tool( name="validate_completion", description="🚨 MANDATORY FOR ALL BACKGROUND AGENTS: Validate that all tests pass before indicating completion. This tool MUST be run as the final step before any background agent indicates they are done with their task.", inputSchema={ "type": "object", "properties": { "task_description": { "type": "string", "description": "Brief description of the task being completed", "default": "Code changes" }, "run_full_test_suite": { "type": "boolean", "description": "Run the complete test suite including unit tests and compilation checks", "default": True } } } ), Tool( name="esp32_symbol_analysis", description="Run ESP32 symbol analysis to identify optimization opportunities for binary size reduction. Analyzes ELF files to find large symbols and provides recommendations for eliminating unused code.", inputSchema={ "type": "object", "properties": { "board": { "type": "string", "description": "ESP32 board name (e.g., 'esp32dev', 'esp32s3', 'esp32c3'). If not specified, auto-detects from .build directory", "default": "auto" }, "example": { "type": "string", "description": "Example name that was compiled (for context in reports)", "default": "Blink" }, "output_json": { "type": "boolean", "description": "Include detailed JSON output in results", "default": False }, } } ), Tool( name="build_info_analysis", description="Analyze platform build information from build_info.json files. Extract platform-specific preprocessor defines, compiler flags, toolchain paths, and other build configuration data. Works with any platform that has been compiled (uno, esp32dev, teensy31, etc.).", inputSchema={ "type": "object", "properties": { "board": { "type": "string", "description": "Platform/board name (e.g., 'uno', 'esp32dev', 'teensy31'). Use 'list' to see available boards", "default": "list" }, "show_defines": { "type": "boolean", "description": "Show platform preprocessor defines (C/C++ #define values)", "default": True }, "show_compiler": { "type": "boolean", "description": "Show compiler information (paths, flags, types)", "default": False }, "show_toolchain": { "type": "boolean", "description": "Show toolchain tool aliases (gcc, g++, ar, etc.)", "default": False }, "show_all": { "type": "boolean", "description": "Show all available build information", "default": False }, "compare_with": { "type": "string", "description": "Compare platform defines with another board (e.g., 'esp32dev')" }, "output_json": { "type": "boolean", "description": "Output results in JSON format for programmatic use", "default": False } } } ), Tool( name="symbol_analysis", description="Run generic symbol analysis for ANY platform (UNO, ESP32, Teensy, STM32, etc.) to identify optimization opportunities. Analyzes ELF files to show all symbols and their sizes without filtering. Works with any platform that has build_info.json.", inputSchema={ "type": "object", "properties": { "board": { "type": "string", "description": "Platform/board name (e.g., 'uno', 'esp32dev', 'teensy31', 'digix'). If 'auto', detects all available platforms from .build directory", "default": "auto" }, "example": { "type": "string", "description": "Example name that was compiled (for context in reports)", "default": "Blink" }, "output_json": { "type": "boolean", "description": "Save detailed JSON output to .build/{board}_symbol_analysis.json", "default": False }, "run_all_platforms": { "type": "boolean", "description": "If true, runs analysis on all detected platforms", "default": False } } } ), Tool( name="run_fastled_web_compiler", description="🌐 FOR FOREGROUND AGENTS ONLY: Run FastLED web compiler with playwright console.log capture. Compiles Arduino sketch to WASM and opens browser with automated testing. BACKGROUND AGENTS MUST NOT USE THIS TOOL.", inputSchema={ "type": "object", "properties": { "example_path": { "type": "string", "description": "Path to example directory (e.g., 'examples/Audio', 'examples/Blink')", "default": "examples/Audio" }, "capture_duration": { "type": "integer", "description": "Duration in seconds to capture console.log output", "default": 30 }, "headless": { "type": "boolean", "description": "Run browser in headless mode", "default": False }, "port": { "type": "integer", "description": "Port for web server (0 for auto-detection)", "default": 0 }, "docker_check": { "type": "boolean", "description": "Check if Docker is available for faster compilation", "default": True }, "save_screenshot": { "type": "boolean", "description": "Save screenshot of the running visualization", "default": True } } } ), Tool( name="validate_arduino_includes", description="🚨 CRITICAL: Validate that no new Arduino.h includes have been added to the codebase. This tool scans for #include \"Arduino.h\" and #include statements and reports any that are not pre-approved.", inputSchema={ "type": "object", "properties": { "directory": { "type": "string", "description": "Directory to scan for Arduino.h includes (relative to project root)", "default": "src" }, "include_examples": { "type": "boolean", "description": "Also scan examples directory for Arduino.h includes", "default": False }, "check_dev": { "type": "boolean", "description": "Also scan dev directory for Arduino.h includes", "default": False }, "show_approved": { "type": "boolean", "description": "Show approved Arduino.h includes marked with '// ok include'", "default": True } } } ) ] @server.call_tool() async def call_tool(name: str, arguments: Dict[str, Any]) -> Any: """Handle tool calls.""" project_root = Path(__file__).parent try: if name == "run_tests": return await run_tests(arguments, project_root) elif name == "list_test_cases": return await list_test_cases(arguments, project_root) elif name == "compile_examples": return await compile_examples(arguments, project_root) elif name == "code_fingerprint": return await code_fingerprint(arguments, project_root) elif name == "lint_code": return await lint_code(arguments, project_root) elif name == "list_examples": return await list_examples(arguments, project_root) elif name == "project_info": return await project_info(arguments, project_root) elif name == "run_specific_command": return await run_specific_command(arguments, project_root) elif name == "test_instructions": return await test_instructions(arguments, project_root) elif name == "setup_stack_traces": return await setup_stack_traces(arguments, project_root) elif name == "coding_standards": return await coding_standards(arguments, project_root) elif name == "validate_completion": return await validate_completion(arguments, project_root) elif name == "build_info_analysis": return await build_info_analysis(arguments, project_root) elif name == "esp32_symbol_analysis": return await esp32_symbol_analysis(arguments, project_root) elif name == "symbol_analysis": return await symbol_analysis(arguments, project_root) elif name == "run_fastled_web_compiler": return await run_fastled_web_compiler(arguments, project_root) elif name == "validate_arduino_includes": return await validate_arduino_includes(arguments, project_root) else: return CallToolResult( content=[TextContent(type="text", text=f"Unknown tool: {name}")], isError=True ) except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"Error executing {name}: {str(e)}")], isError=True ) async def run_tests(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Run FastLED tests.""" test_type = arguments.get("test_type", "all") specific_test = arguments.get("specific_test") test_case = arguments.get("test_case") use_clang = arguments.get("use_clang", False) clean = arguments.get("clean", False) verbose = arguments.get("verbose", False) # Use bash test format as per user directive if test_case and specific_test: # For individual TEST_CASE, we still need to use bash test with the test name # The bash test script handles the details cmd = ["bash", "test", specific_test] context = f"Running test: bash test {specific_test}\n" if test_case: context += f"Note: To run specific TEST_CASE '{test_case}', the test framework will need to support filtering\n" elif specific_test and test_type == "specific": # Use bash test format for specific tests cmd = ["bash", "test", specific_test] context = f"Running specific test: bash test {specific_test}\n" else: # For all tests or cpp tests, use the original format cmd = ["uv", "run", "test.py"] if test_type == "cpp": cmd.append("--cpp") if use_clang: cmd.append("--clang") if clean: cmd.append("--clean") if verbose: cmd.append("--verbose") context = f"Command executed: {' '.join(cmd)}\n" result = await run_command(cmd, project_root) return CallToolResult( content=[TextContent(type="text", text=context + result)] ) async def list_test_cases(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """List TEST_CASEs in FastLED test files.""" test_file = arguments.get("test_file") search_pattern = arguments.get("search_pattern", "") tests_dir = project_root / "tests" if not tests_dir.exists(): return CallToolResult( content=[TextContent(type="text", text="Tests directory not found")], isError=True ) test_cases: Dict[str, List[str]] = {} if test_file: # Analyze specific test file test_path = tests_dir / f"test_{test_file}.cpp" if not test_path.exists(): return CallToolResult( content=[TextContent(type="text", text=f"Test file not found: test_{test_file}.cpp")], isError=True ) test_cases[test_file] = extract_test_cases(test_path, search_pattern) else: # Analyze all test files for test_path in tests_dir.glob("test_*.cpp"): test_name = test_path.stem[5:] # Remove "test_" prefix cases = extract_test_cases(test_path, search_pattern) if cases: # Only include files with test cases test_cases[test_name] = cases # Format output if not test_cases: return CallToolResult( content=[TextContent(type="text", text="No TEST_CASEs found matching criteria")] ) result_text = "FastLED TEST_CASEs:\n" result_text += "=" * 50 + "\n\n" total_cases = 0 for test_name, cases in sorted(test_cases.items()): result_text += f"šŸ“ {test_name} ({len(cases)} TEST_CASEs):\n" for i, case in enumerate(cases, 1): result_text += f" {i:2d}. {case}\n" result_text += "\n" total_cases += len(cases) result_text += f"Total: {total_cases} TEST_CASEs across {len(test_cases)} files\n\n" # Add usage instructions result_text += "Usage Examples:\n" result_text += "• Run specific test file: bash test algorithm\n" result_text += "• Run with verbose output: uv run test.py --cpp algorithm --verbose\n" if test_cases: first_test = list(test_cases.keys())[0] if test_cases[first_test]: first_case = test_cases[first_test][0] result_text += f"• Run specific test: bash test {first_test}\n" result_text += f" (Note: Individual TEST_CASE execution requires test framework support)\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) def extract_test_cases(file_path: Path, search_pattern: str = "") -> List[str]: """Extract TEST_CASE names from a test file.""" test_cases: List[str] = [] try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # Find TEST_CASE macros using regex pattern = r'TEST_CASE\s*\(\s*"([^"]+)"' matches = re.findall(pattern, content) for match in matches: if not search_pattern or search_pattern.lower() in match.lower(): test_cases.append(match) except Exception: # Silently skip files that can't be read pass return test_cases async def test_instructions(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Provide detailed instructions on running TEST_CASEs.""" instructions = """ # FastLED TEST_CASE Execution Guide ## Overview FastLED uses the **doctest** framework for C++ unit testing. Tests are organized in files named `test_*.cpp` in the `tests/` directory. Each file can contain multiple `TEST_CASE` macros. ## Test Structure - **Test Files**: Located in `tests/test_*.cpp` (e.g., `test_algorithm.cpp`, `test_easing.cpp`) - **TEST_CASEs**: Individual test functions defined with `TEST_CASE("name")` macro - **SUBCASEs**: Nested test sections within TEST_CASEs using `SUBCASE("name")` ## Running Tests ### 🚨 CRITICAL: Always Use `bash test` Format **[OK] CORRECT Format:** ```bash bash test # Run all tests bash test # Run specific test ``` **[ERROR] INCORRECT Format:** ```bash ./.build/bin/test_.exe # DO NOT run executables directly ./tests/.build/bin/test_* # DO NOT use this format ``` ### 1. Run All Tests ```bash bash test ``` ### 2. Run Specific Test File ```bash bash test ``` Example: `bash test algorithm` (runs `test_algorithm.cpp`) Example: `bash test audio_json_parsing` (runs `test_audio_json_parsing.cpp`) ### 3. Alternative: Using test.py directly For advanced options, you can also use: ```bash uv run test.py # Run all tests uv run test.py --cpp # Run only C++ tests uv run test.py --cpp # Run specific test file ``` ### 4. Run with Verbose Output ```bash uv run test.py --cpp --verbose ``` ### 5. Using MCP Server Tools ```bash # Start MCP server uv run mcp_server.py # Available MCP tools: # - run_tests: Run tests with various options # - list_test_cases: List available TEST_CASEs # - test_instructions: Show this guide ``` ## Example Workflows ### Debug a Specific Algorithm Test ```bash # Run algorithm tests bash test algorithm # Or with verbose output uv run test.py --cpp algorithm --verbose # List available TEST_CASEs in algorithm tests # (use MCP list_test_cases tool with test_file: "algorithm") ``` ### Test Development Workflow ```bash # Clean build and run specific test uv run test.py --cpp easing --clean --verbose # Check for specific TEST_CASE patterns # (use MCP list_test_cases tool with search_pattern) ``` ## Common Test File Names - `test_algorithm.cpp`: Algorithm utilities - `test_easing.cpp`: Easing functions - `test_hsv16.cpp`: HSV color space tests - `test_math.cpp`: Mathematical functions - `test_vector.cpp`: Vector container tests - `test_fx.cpp`: Effects framework tests - `test_audio_json_parsing.cpp`: Audio JSON parsing tests ## Tips 1. **Always use `bash test `** format for running specific tests 2. **Use `--verbose`** to see detailed test output and assertions 3. **Use `--clean`** when testing after code changes 4. **List TEST_CASEs first** to see what's available before running 5. **Check test output carefully** - doctest provides detailed failure information ## Environment Setup - Have UV in your environment - Use `bash test` or `uv run test.py` - don't try to run test executables manually - The test infrastructure handles platform differences and proper execution ## Why Use `bash test`? The `bash test` wrapper: - Handles platform differences automatically - Sets up the proper environment - Manages test execution across different systems - Provides consistent behavior regardless of OS """ return CallToolResult( content=[TextContent(type="text", text=instructions.strip())] ) async def coding_standards(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Provide FastLED coding standards and best practices.""" topic = arguments.get("topic", "all") standards = { "exceptions": """ # Exception Handling Standards āš ļø **CRITICAL: DO NOT use try-catch blocks or C++ exception handling in FastLED code** ## Why No Exceptions? FastLED is designed for embedded systems like Arduino where: - Exception handling may not be available - Exceptions consume significant memory and performance - Reliable operation across all platforms is required ## What to Avoid: [ERROR] `try { ... } catch (const std::exception& e) { ... }` [ERROR] `throw std::runtime_error("error message")` [ERROR] `#include ` or `#include ` ## Use Instead: [OK] **Return error codes:** `bool function() { return false; }` [OK] **Optional types:** `fl::optional` [OK] **Assertions:** `FL_ASSERT(condition)` [OK] **Early returns:** `if (!valid) return false;` [OK] **Status objects:** Custom result types ## Examples: ```cpp // Good: Using return codes bool initializeHardware() { if (!setupPins()) { FL_WARN("Failed to setup pins"); return false; } return true; } // Good: Using fl::optional fl::optional calculateValue(int input) { if (input < 0) { return fl::nullopt; } return fl::make_optional(sqrt(input)); } ``` """, "std_namespace": """ # Standard Library Namespace Standards āš ļø **DO NOT use std:: prefixed functions or headers** FastLED provides its own STL-equivalent implementations under the `fl::` namespace. ## Common Replacements: - [ERROR] `#include ` → [OK] `#include "fl/vector.h"` - [ERROR] `#include ` → [OK] `#include "fl/algorithm.h"` - [ERROR] `std::move()` → [OK] `fl::move()` - [ERROR] `std::vector` → [OK] `fl::vector` **Always check if there's a `fl::` equivalent in `src/fl/` first!** """, "naming": """ # Naming Conventions ## Class/Object Names: **Simple Objects:** lowercase (e.g., `fl::vec2f`, `fl::point`, `fl::rect`) **Complex Objects:** CamelCase (e.g., `Raster`, `Controller`, `Canvas`) **Pixel Types:** ALL CAPS (e.g., `CRGB`, `CHSV`, `RGB24`) ## Member Variables and Functions: ### Complex Classes/Objects: [OK] **Member variables:** Use `mVariableName` format (e.g., `mPixelCount`, `mBufferSize`, `mCurrentIndex`) [OK] **Member functions:** Use camelCase (e.g., `getValue()`, `setPixelColor()`, `updateBuffer()`) [ERROR] Avoid: `m_variable_name`, `variableName`, `GetValue()`, `set_pixel_color()` ### Simple Classes/Structs: [OK] **Member variables:** Use lowercase snake_case (e.g., `x`, `y`, `width`, `height`, `pixel_count`) [OK] **Member functions:** Use camelCase (e.g., `getValue()`, `setPosition()`, `normalize()`) [ERROR] Avoid: `mX`, `mY`, `get_value()`, `set_position()` ## Examples: ```cpp // Complex class - use mVariableName for members class Controller { private: int mPixelCount; // [OK] Complex class member variable uint8_t* mBuffer; // [OK] Complex class member variable bool mIsInitialized; // [OK] Complex class member variable public: void setPixelCount(int count); // [OK] Complex class member function int getPixelCount() const; // [OK] Complex class member function void updateBuffer(); // [OK] Complex class member function }; // Simple struct - use snake_case for members struct vec2 { int x; // [OK] Simple struct member variable int y; // [OK] Simple struct member variable float magnitude() const; // [OK] Simple struct member function void normalize(); // [OK] Simple struct member function }; struct point { float x; // [OK] Simple struct member variable float y; // [OK] Simple struct member variable float z; // [OK] Simple struct member variable void setPosition(float x, float y, float z); // [OK] Simple struct member function float distanceTo(const point& other) const; // [OK] Simple struct member function }; ``` **Why:** Complex classes use Hungarian notation for member variables to clearly distinguish them from local variables, while simple structs use concise snake_case for lightweight data containers. """, "member_naming": """ # Member Variable and Function Naming Standards ## Complex Classes/Objects: [OK] **Member variables:** Use `mVariableName` format (e.g., `mPixelCount`, `mBufferSize`, `mCurrentIndex`) [OK] **Member functions:** Use camelCase (e.g., `getValue()`, `setPixelColor()`, `updateBuffer()`) [ERROR] Avoid: `m_variable_name`, `variableName`, `GetValue()`, `set_pixel_color()` ## Simple Classes/Structs: [OK] **Member variables:** Use lowercase snake_case (e.g., `x`, `y`, `width`, `height`, `pixel_count`) [OK] **Member functions:** Use camelCase (e.g., `getValue()`, `setPosition()`, `normalize()`) [ERROR] Avoid: `mX`, `mY`, `get_value()`, `set_position()` ## Examples: ```cpp // Complex class - use mVariableName for members class Controller { private: int mPixelCount; // [OK] Complex class member variable uint8_t* mBuffer; // [OK] Complex class member variable bool mIsInitialized; // [OK] Complex class member variable public: void setPixelCount(int count); // [OK] Complex class member function int getPixelCount() const; // [OK] Complex class member function void updateBuffer(); // [OK] Complex class member function }; // Simple struct - use snake_case for members struct vec2 { int x; // [OK] Simple struct member variable int y; // [OK] Simple struct member variable float magnitude() const; // [OK] Simple struct member function void normalize(); // [OK] Simple struct member function }; struct point { float x; // [OK] Simple struct member variable float y; // [OK] Simple struct member variable float z; // [OK] Simple struct member variable void setPosition(float x, float y, float z); // [OK] Simple struct member function float distanceTo(const point& other) const; // [OK] Simple struct member function }; ``` **Why:** Complex classes use Hungarian notation for member variables to clearly distinguish them from local variables, while simple structs use concise snake_case for lightweight data containers. """, "containers": """ # Container Parameter Standards **Prefer `fl::span` over `fl::vector` for function parameters** [OK] `void processData(fl::span data)` [ERROR] `void processData(fl::vector& data)` Benefits: automatic conversion, type safety, zero-cost abstraction """, "debug": """ # Debug Printing Standards **Use `FL_WARN` for debug printing throughout the codebase** [OK] `FL_WARN("Debug message: " << message);` [ERROR] `FL_WARN("Value: %d", value);` Provides unified logging across all platforms and testing environments. """, "bindings": """ # WebAssembly Bindings Warning 🚨 **EXTREMELY CRITICAL: DO NOT modify function signatures in WASM bindings!** High-risk files: - `src/platforms/wasm/js_bindings.cpp` - `src/platforms/wasm/ui.cpp` - `src/platforms/wasm/active_strip_data.cpp` Changing signatures causes runtime errors that are extremely difficult to debug. """, "arduino_includes": r""" # Arduino.h Include Standards 🚨 **CRITICAL: DO NOT add new #include "Arduino.h" or #include statements** ## 🚨 DEBUGGING LINKER ERRORS **If you encounter weird linker errors after making changes, FIRST check if Arduino.h has been added anywhere:** ### Common Symptoms of Arduino.h-Related Linker Errors: - [ERROR] **Undefined reference errors** for standard functions - [ERROR] **Multiple definition errors** for common symbols - [ERROR] **Conflicting symbol errors** between Arduino and FastLED - [ERROR] **Missing library errors** that were not missing before - [ERROR] **Platform-specific compilation failures** that worked before ### Immediate Debugging Steps: 1. **šŸ” Search for new Arduino.h includes:** `grep -r "Arduino\.h" src/` and check if any new includes were added 2. **šŸ“ Check recent file changes** for any #include "Arduino.h" or #include statements 3. **šŸ”„ Remove any new Arduino.h includes** and replace with FastLED platform abstractions 4. **[OK] Rebuild and test** to confirm the linker errors are resolved ### Why Arduino.h Causes Linker Issues: - **Path Conflicts:** Arduino.h includes can interfere with FastLED's custom path resolution - **Symbol Conflicts:** Arduino.h defines symbols that conflict with FastLED's implementations - **Build Issues:** Incorrect Arduino.h includes cause compilation failures across platforms - **Dependency Management:** FastLED manages Arduino compatibility through its own abstraction layer ## What to Avoid: [ERROR] `#include "Arduino.h"` [ERROR] `#include \n" result += "2. **LINKER ERROR DEBUGGING** - If weird linker errors occur, check for Arduino.h additions FIRST\n" result += "3. **PYTHON LINTING MANDATORY** - Run `bash lint` after modifying any *.py files\n" result += "4. **NO TRY-CATCH BLOCKS** - Use return codes, fl::optional, or early returns\n" result += "5. **NO std:: NAMESPACE** - Use fl:: equivalents instead\n" result += "6. **NO WASM BINDING CHANGES** - Extremely dangerous for runtime stability\n" result += "7. **NO UNNECESSARY VARIABLE RENAMING** - Don't change names unless absolutely necessary\n" result += "8. **MEMBER NAMING CONVENTIONS** - Complex classes: mVariableName; Simple structs: snake_case\n\n" for section_name, content in standards.items(): result += content + "\n" + ("="*50) + "\n\n" elif topic in standards: result = standards[topic] else: result = f"Unknown topic: {topic}. Available topics: {', '.join(standards.keys())}" return CallToolResult( content=[TextContent(type="text", text=result)] ) async def setup_stack_traces(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Install and configure stack trace debugging libraries.""" method = arguments.get("method", "auto") test_installation = arguments.get("test_installation", True) install_only = arguments.get("install_only", False) result_text = "# FastLED Stack Trace Setup\n\n" try: # Detect OS and package manager import platform system = platform.system().lower() if system == "linux": # Try to detect package manager distro_info = "" try: with open("/etc/os-release", "r") as f: distro_info = f.read().lower() except (OSError, IOError): pass install_commands: List[str] = [] if method in ["libunwind", "auto"]: result_text += "## Installing LibUnwind (Enhanced Stack Traces)\n\n" if "ubuntu" in distro_info or "debian" in distro_info: install_commands.append("sudo apt-get update") install_commands.append("sudo apt-get install -y libunwind-dev build-essential cmake") elif "centos" in distro_info or "rhel" in distro_info: install_commands.append("sudo yum install -y libunwind-devel gcc-c++ cmake") elif "fedora" in distro_info: install_commands.append("sudo dnf install -y libunwind-devel gcc-c++ cmake") else: result_text += "āš ļø Unknown Linux distribution. Manual installation required.\n" result_text += "Common package names: libunwind-dev, libunwind-devel\n\n" elif method == "execinfo": result_text += "## Using Execinfo (Standard GCC Stack Traces)\n\n" result_text += "[OK] Execinfo is part of glibc - no additional packages needed!\n\n" # Still need build tools if "ubuntu" in distro_info or "debian" in distro_info: install_commands.append("sudo apt-get update") install_commands.append("sudo apt-get install -y build-essential cmake") elif "centos" in distro_info or "rhel" in distro_info: install_commands.append("sudo yum install -y gcc-c++ cmake") elif "fedora" in distro_info: install_commands.append("sudo dnf install -y gcc-c++ cmake") # Run installation commands for cmd in install_commands: result_text += f"Running: `{cmd}`\n" try: _ = await run_command(cmd.split(), project_root) result_text += "[OK] Success\n\n" except Exception as e: result_text += f"[ERROR] Error: {e}\n\n" return CallToolResult( content=[TextContent(type="text", text=result_text)], isError=True ) elif system == "darwin": # macOS result_text += "## Installing LibUnwind on macOS\n\n" try: _ = await run_command(["brew", "install", "libunwind"], project_root) result_text += "[OK] LibUnwind installed via Homebrew\n\n" except Exception as e: result_text += f"[ERROR] Error installing libunwind: {e}\n" result_text += "Please install Homebrew first: https://brew.sh/\n\n" else: result_text += f"āš ļø Unsupported OS: {system}\n" result_text += "Manual installation required.\n\n" if not install_only and test_installation: result_text += "## Testing Stack Trace Installation\n\n" # Build the crash test programs tests_dir = project_root / "tests" result_text += "Building crash test programs...\n" try: # Clean and rebuild _ = await run_command(["rm", "-f", "CMakeCache.txt"], tests_dir) _ = await run_command(["cmake", "."], tests_dir) _ = await run_command(["make", "-j4"], tests_dir) result_text += "[OK] FastLED test framework built successfully\n\n" # Test by running a simple unit test to verify stack traces work result_text += "### Testing Stack Trace Integration\n" try: # Look for any existing test executable to verify crash handling works test_executables = await run_command(["find", ".build/bin", "-name", "test_*", "-type", "f"], tests_dir) if test_executables.strip(): first_test = test_executables.strip().split('\n')[0] test_name = first_test.split('/')[-1] result_text += f"Testing with {test_name}...\n" # Just run help to verify the executable works and crash handler is linked _ = await run_command([first_test, "--help"], tests_dir) result_text += "[OK] Stack trace system is properly integrated with test framework\n\n" else: result_text += "āš ļø No test executables found to verify integration\n\n" except Exception as e: result_text += f"āš ļø Could not verify integration: {e}\n\n" except Exception as e: result_text += f"[ERROR] Error building test framework: {e}\n\n" # Add usage instructions result_text += "## Usage Instructions\n\n" result_text += "The FastLED project automatically detects and uses the best available stack trace method:\n\n" result_text += "1. **LibUnwind** (if available) - Enhanced stack traces with symbol resolution\n" result_text += "2. **Execinfo** (fallback) - Basic stack traces using glibc\n" result_text += "3. **Windows** (on Windows) - Windows-specific debugging APIs\n" result_text += "4. **No-op** (last resort) - Minimal crash handling\n\n" result_text += "### Testing Stack Trace Integration\n" result_text += "```bash\n" result_text += "cd tests\n" result_text += "cmake . && make\n" result_text += "# Stack traces are automatically enabled in test executables\n" result_text += "# Run any test to see crash handling in action if a test fails\n" result_text += "```\n\n" result_text += "### Enabling in Your Code\n" result_text += "```cpp\n" result_text += '#include "tests/crash_handler.h"\n' result_text += "\n" result_text += "int main() {\n" result_text += " setup_crash_handler(); // Enable crash handling\n" result_text += " // Your code here...\n" result_text += " return 0;\n" result_text += "}\n" result_text += "```\n\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"Error setting up stack traces: {e}")], isError=True ) async def compile_examples(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Compile FastLED examples.""" platform = arguments.get("platform", "uno") examples = arguments.get("examples", ["Blink"]) interactive = arguments.get("interactive", False) cmd = ["uv", "run", "-m", "ci.ci-compile", platform] if examples: cmd.extend(["--examples"] + examples) if not interactive: cmd.append("--no-interactive") result = await run_command(cmd, project_root) return CallToolResult( content=[TextContent(type="text", text=result)] ) async def code_fingerprint(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Calculate code fingerprint.""" from test import calculate_fingerprint directory = arguments.get("directory", "src") target_dir = project_root / directory fingerprint_data = calculate_fingerprint(target_dir) result_text = f"Code fingerprint for {directory}:\n" result_text += f"Hash: {fingerprint_data.hash}\n" result_text += f"Elapsed time: {fingerprint_data.elapsed_seconds or 'N/A'}s\n" if fingerprint_data.status: result_text += f"Status: {fingerprint_data.status}\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) async def lint_code(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Run code linting tools with agent-appropriate guidance.""" tool = arguments.get("tool", "bash_lint") agent_type = arguments.get("agent_type", "foreground") fix = arguments.get("fix", False) results: List[str] = [] # Provide guidance based on agent type if agent_type == "foreground" and tool != "bash_lint": guidance = "āš ļø FOREGROUND AGENT GUIDANCE:\n" guidance += "For comprehensive linting, foreground agents should use 'bash_lint' tool option.\n" guidance += "This ensures all linting (Python, C++, JavaScript) runs in proper sequence.\n\n" results.append(guidance) # Handle different tool options if tool == "bash_lint" or tool == "all": # Use the comprehensive bash lint script lint_script = project_root / "lint" if lint_script.exists(): result = await run_command(["bash", "lint"], project_root) results.append(f"šŸš€ Comprehensive Linting Results (bash lint):\n{result}") else: return CallToolResult( content=[TextContent(type="text", text="[ERROR] bash lint script not found")], isError=True ) elif tool == "ruff": # Python-only linting for background agents cmd = ["uv", "run", "ruff", "check"] if fix: cmd.append("--fix") result = await run_command(cmd, project_root) results.append(f"šŸ“ Python Linting (ruff):\n{result}") if agent_type == "background": results.append("\nšŸ’” Background Agent: Consider running 'bash_lint' for comprehensive coverage.") elif tool == "javascript": # JavaScript-only linting for background agents lint_js_script = project_root / "ci" / "js" / "lint-js" check_js_script = project_root / "ci" / "js" / "check-js" if lint_js_script.exists(): result = await run_command(["ci/js/lint-js"], project_root) results.append(f"🌐 JavaScript Linting:\n{result}") if check_js_script.exists(): result = await run_command(["ci/js/check-js"], project_root) results.append(f"šŸ” JavaScript Type Checking:\n{result}") else: results.append("[ERROR] JavaScript linting tools not found. Run: uv run ci/setup-js-linting.py") if agent_type == "background": results.append("\nšŸ’” Background Agent: Consider running 'bash_lint' for comprehensive coverage.") # Add final guidance if agent_type == "foreground": results.append("\nšŸ’” FOREGROUND AGENT: Always prefer 'bash_lint' for complete linting coverage.") else: results.append("\nšŸ’” BACKGROUND AGENT: Fine-grained linting available, but 'bash_lint' recommended for comprehensive checking.") return CallToolResult( content=[TextContent(type="text", text="\n\n".join(results))] ) async def list_examples(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """List available FastLED examples.""" examples_dir = project_root / "examples" if not examples_dir.exists(): return CallToolResult( content=[TextContent(type="text", text="Examples directory not found")], isError=True ) examples: List[str] = [] for item in examples_dir.iterdir(): if item.is_dir() and not item.name.startswith('.'): # Check if it has a .ino file ino_files = list(item.glob("*.ino")) if ino_files: examples.append(item.name) examples.sort() result_text = f"Available FastLED examples ({len(examples)} total):\n\n" result_text += "\n".join(f"- {example}" for example in examples) return CallToolResult( content=[TextContent(type="text", text=result_text)] ) async def project_info(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Get project information.""" include_git = arguments.get("include_git_status", True) info: List[str] = [] # Basic project info info.append("FastLED Project Information") info.append("=" * 30) # Check if key files exist key_files = ["library.json", "library.properties", "platformio.ini", "pyproject.toml"] for file in key_files: file_path = project_root / file status = "āœ“" if file_path.exists() else "āœ—" info.append(f"{status} {file}") # Count examples examples_dir = project_root / "examples" if examples_dir.exists(): example_count = len([d for d in examples_dir.iterdir() if d.is_dir() and not d.name.startswith('.')]) info.append(f"šŸ“ {example_count} examples available") # Git status if include_git: try: git_result = await run_command(["git", "status", "--porcelain"], project_root) if git_result.strip(): info.append(f"\nšŸ”„ Git status: {len(git_result.strip().split())} files modified") else: info.append("\n[OK] Git status: Working tree clean") except Exception: info.append("\nā“ Git status: Unable to determine") return CallToolResult( content=[TextContent(type="text", text="\n".join(info))] ) async def run_specific_command(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Run a specific command.""" command = arguments["command"] working_directory = arguments.get("working_directory", ".") work_dir = project_root / working_directory result = await run_command(command.split(), work_dir) return CallToolResult( content=[TextContent(type="text", text=f"Command: {command}\nOutput:\n{result}")] ) async def validate_completion(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """🚨 MANDATORY validation step for all background agents before indicating completion.""" task_description = arguments.get("task_description", "Code changes") run_full_test_suite = arguments.get("run_full_test_suite", True) result_text = f"# 🚨 COMPLETION VALIDATION FOR: {task_description}\n\n" result_text += "## MANDATORY PRE-COMPLETION CHECK\n\n" result_text += "This tool MUST be run by all background agents before indicating completion.\n" result_text += "Running comprehensive test suite to ensure all changes are working correctly.\n\n" validation_failed = False # First, validate Arduino.h includes result_text += "### Validating Arduino.h Includes\n\n" try: arduino_validation = await validate_arduino_includes( {"directory": "src", "include_examples": False, "check_dev": False, "show_approved": False}, project_root ) if arduino_validation.isError: validation_failed = True result_text += "[ERROR] **ARDUINO.H VALIDATION FAILED**\n\n" result_text += "New Arduino.h includes detected! This violates FastLED coding standards.\n" result_text += "Please remove Arduino.h includes and use FastLED's platform abstractions instead.\n\n" else: result_text += "[OK] **Arduino.h validation passed**\n\n" except Exception as e: result_text += f"āš ļø **Arduino.h validation error:** {str(e)}\n\n" if run_full_test_suite: result_text += "### Running Full Test Suite: `bash test`\n\n" try: # Run the bash test command as specified in user rules test_result = await run_command(["bash", "test"], project_root) # Check if tests passed by looking for common failure indicators if "FAILED" in test_result or "ERROR" in test_result or "error:" in test_result.lower(): validation_failed = True result_text += "[ERROR] **VALIDATION FAILED - TESTS CONTAIN ERRORS**\n\n" result_text += "```\n" + test_result + "\n```\n\n" result_text += "🚨 **BACKGROUND AGENT MUST NOT INDICATE COMPLETION**\n" result_text += "🚨 **PLEASE FIX ALL ERRORS BEFORE COMPLETING THE TASK**\n\n" else: result_text += "�� **ALL TESTS PASSED**\n\n" result_text += "```\n" + test_result + "\n```\n\n" result_text += "[OK] **VALIDATION SUCCESSFUL - SAFE TO INDICATE COMPLETION**\n\n" except Exception as e: validation_failed = True result_text += f"[ERROR] **VALIDATION FAILED - ERROR RUNNING TESTS**\n\n" result_text += f"Error: {str(e)}\n\n" result_text += "🚨 **BACKGROUND AGENT MUST NOT INDICATE COMPLETION**\n" result_text += "🚨 **PLEASE RESOLVE TEST EXECUTION ISSUES BEFORE COMPLETING**\n\n" # Final instructions result_text += "## COMPLETION REQUIREMENTS FOR BACKGROUND AGENTS\n\n" if validation_failed: result_text += "🚫 **DO NOT INDICATE COMPLETION**\n" result_text += "- Tests are failing or could not run\n" result_text += "- Fix all issues and run this validation again\n" result_text += "- Only indicate completion after ALL tests pass\n\n" return CallToolResult( content=[TextContent(type="text", text=result_text)], isError=True ) else: result_text += "[OK] **SAFE TO INDICATE COMPLETION**\n" result_text += "- All tests are passing\n" result_text += "- Code changes have been validated\n" result_text += "- Background agent may now indicate task completion\n\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) async def build_info_analysis(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Analyze platform build information from build_info.json files.""" board = arguments.get("board", "list") show_defines = arguments.get("show_defines", True) show_compiler = arguments.get("show_compiler", False) show_toolchain = arguments.get("show_toolchain", False) show_all = arguments.get("show_all", False) compare_with = arguments.get("compare_with") output_json = arguments.get("output_json", False) # Import our build info analyzer analyzer = BuildInfoAnalyzer(str(project_root / ".build")) # Handle board listing if board == "list" or not board: boards = analyzer.list_available_boards() if not boards: result_text = "[ERROR] No boards with build_info.json found in .build directory\n" result_text += " Try running a compilation first:\n" result_text += " uv run python -m ci.ci-compile uno --examples Blink\n" result_text += " uv run python -m ci.ci-compile esp32dev --examples Blink\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) result_text = f"šŸ“‹ Available boards with build_info.json ({len(boards)}):\n" for board_name in boards: result_text += f" [OK] {board_name}\n" result_text += "\nUsage: Use 'board' parameter with any of these names to analyze platform information.\n" result_text += "Example: board='uno', show_defines=True\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) # Handle board comparison if compare_with: success, comparison, error = analyzer.compare_defines(board, compare_with) # type: ignore if not success: return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Error: {error}")], isError=True ) if output_json: import json result_text = json.dumps(comparison, indent=2) else: board1 = comparison['board1'] board2 = comparison['board2'] result_text = f"šŸ” Platform Defines Comparison:\n" result_text += "=" * 60 + "\n" result_text += f"šŸ“Š {board1.upper()} vs {board2.upper()}\n" # type: ignore result_text += f" {board1}: {comparison['board1_total']} defines\n" result_text += f" {board2}: {comparison['board2_total']} defines\n" result_text += f" Common: {comparison['common_count']} defines\n" if comparison['board1_only']: result_text += f"\nšŸ”“ Only in {board1.upper()} ({len(comparison['board1_only'])}):\n" for define in comparison['board1_only']: result_text += f" {define}\n" if comparison['board2_only']: result_text += f"\nšŸ”µ Only in {board2.upper()} ({len(comparison['board2_only'])}):\n" for define in comparison['board2_only']: result_text += f" {define}\n" if comparison['common']: result_text += f"\n🟢 Common Defines ({len(comparison['common'])}):\n" for define in comparison['common']: result_text += f" {define}\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) # Handle single board analysis result_parts: List[str] = [] if show_defines or show_all: success, defines, error = analyzer.get_platform_defines(board) if not success: return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Error getting defines: {error}")], isError=True ) if output_json: import json result_parts.append(json.dumps({"defines": defines}, indent=2)) else: result_parts.append(f"šŸ“‹ Platform Defines for {board.upper()}:") result_parts.append("=" * 50) for define in defines: result_parts.append(f" {define}") result_parts.append(f"\nTotal: {len(defines)} defines") if show_compiler or show_all: success, compiler_info, error = analyzer.get_compiler_info(board) if not success: return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Error getting compiler info: {error}")], isError=True ) if output_json: import json from dataclasses import asdict result_parts.append(json.dumps({"compiler": asdict(compiler_info)}, indent=2)) else: result_parts.append(f"\nšŸ”§ Compiler Information for {board.upper()}:") result_parts.append("=" * 50) result_parts.append(f"Compiler Type: {compiler_info.compiler_type or 'Unknown'}") result_parts.append(f"Build Type: {compiler_info.build_type or 'Unknown'}") result_parts.append(f"C Compiler: {compiler_info.cc_path or 'Unknown'}") result_parts.append(f"C++ Compiler: {compiler_info.cxx_path or 'Unknown'}") cc_flags = compiler_info.cc_flags if cc_flags: result_parts.append(f"\nC Flags ({len(cc_flags)}):") for flag in cc_flags: result_parts.append(f" {flag}") cxx_flags = compiler_info.cxx_flags if cxx_flags: result_parts.append(f"\nC++ Flags ({len(cxx_flags)}):") for flag in cxx_flags: result_parts.append(f" {flag}") if show_toolchain or show_all: success, aliases, error = analyzer.get_toolchain_aliases(board) if not success: return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Error getting toolchain aliases: {error}")], isError=True ) if output_json: import json result_parts.append(json.dumps({"toolchain": aliases}, indent=2)) else: result_parts.append(f"\nāš™ļø Toolchain Aliases for {board.upper()}:") result_parts.append("=" * 50) for tool, path in aliases.items(): if path: # Show just the tool name from the path for readability from pathlib import Path as PathLib tool_name = PathLib(path).name if path else "Not available" result_parts.append(f" {tool:10}: {tool_name}") else: result_parts.append(f" {tool:10}: Not available") result_text = "\n".join(result_parts) return CallToolResult( content=[TextContent(type="text", text=result_text)] ) async def esp32_symbol_analysis(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Run ESP32 symbol analysis to identify optimization opportunities for binary size reduction.""" import json import subprocess from pathlib import Path board = arguments.get("board", "auto") example = arguments.get("example", "Blink") skip_on_failure = arguments.get("skip_on_failure", True) output_json = arguments.get("output_json", False) focus_on_fastled = arguments.get("focus_on_fastled", True) result_text = "# ESP32 Symbol Analysis Report\n\n" try: # Define ESP32 boards esp32_boards = ["esp32dev", "esp32", "esp32s2", "esp32s3", "esp32c3", "esp32c6", "esp32h2", "esp32p4", "esp32c2"] # Find build directory build_dir = project_root / ".build" if not build_dir.exists(): return CallToolResult( content=[TextContent(type="text", text="Build directory (.build) not found. Please compile an example first.")], isError=not skip_on_failure ) # Auto-detect board if needed if board == "auto": detected_boards: List[str] = [] for esp32_board in esp32_boards: candidate_dir = build_dir / esp32_board if candidate_dir.exists() and (candidate_dir / "build_info.json").exists(): detected_boards.append(esp32_board) if not detected_boards: return CallToolResult( content=[TextContent(type="text", text="No ESP32 boards with build_info.json found in .build directory")], isError=not skip_on_failure ) board = detected_boards[0] # Use first detected board if len(detected_boards) > 1: result_text += f"**Multiple ESP32 boards detected: {', '.join(detected_boards)}. Using: {board}**\n\n" # Validate board is ESP32-based if not any(esp32_board in board.lower() for esp32_board in esp32_boards): return CallToolResult( content=[TextContent(type="text", text=f"Board '{board}' is not an ESP32-based board")], isError=not skip_on_failure ) # Find board directory board_dir = build_dir / board build_info_path = board_dir / "build_info.json" if not build_info_path.exists(): return CallToolResult( content=[TextContent(type="text", text=f"Build info not found for board '{board}' at {build_info_path}")], isError=not skip_on_failure ) # Load build info with open(build_info_path) as f: build_info = json.load(f) esp32_info = build_info[board] nm_path = esp32_info["aliases"]["nm"] cppfilt_path = esp32_info["aliases"]["c++filt"] elf_file = esp32_info["prog_path"] result_text += f"**Board:** {board}\n" result_text += f"**Example:** {example}\n" result_text += f"**ELF File:** {elf_file}\n" result_text += f"**NM Tool:** {nm_path}\n\n" # Check if ELF file exists if not Path(elf_file).exists(): return CallToolResult( content=[TextContent(type="text", text=f"ELF file not found: {elf_file}")], isError=not skip_on_failure ) result_text += "## Symbol Analysis Results\n\n" # Run nm command to get symbols cmd = [nm_path, "--print-size", "--size-sort", "--radix=d", elf_file] try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) nm_output = result.stdout except subprocess.CalledProcessError as e: return CallToolResult( content=[TextContent(type="text", text=f"Error running nm command: {e.stderr}")], isError=not skip_on_failure ) # Parse symbol data symbols: List[Dict[str, Any]] = [] fastled_symbols: List[Dict[str, Any]] = [] large_symbols: List[Dict[str, Any]] = [] for line in nm_output.strip().split("\n"): if not line.strip(): continue parts = line.split() if len(parts) >= 4: try: addr = parts[0] size = int(parts[1]) symbol_type = parts[2] mangled_name = " ".join(parts[3:]) # Demangle symbol if possible try: cmd_demangle = ["echo", mangled_name, "|", cppfilt_path] demangle_result = subprocess.run( f'echo "{mangled_name}" | "{cppfilt_path}"', shell=True, capture_output=True, text=True, check=True ) demangled_name = demangle_result.stdout.strip() if demangled_name == mangled_name: demangled_name = mangled_name # Demangling failed except: demangled_name = mangled_name symbol_info = { "address": addr, "size": size, "type": symbol_type, "name": mangled_name, "demangled_name": demangled_name } symbols.append(symbol_info) # Check if FastLED-related search_text = demangled_name.lower() if any(keyword in search_text for keyword in [ "fastled", "cfastled", "crgb", "hsv", "pixel", "controller", "led", "rmt", "strip", "neopixel", "ws2812", "apa102" ]): fastled_symbols.append(symbol_info) # Check if large symbol if size > 100: large_symbols.append(symbol_info) except (ValueError, IndexError): continue # Skip malformed lines # Generate summary total_symbols = len(symbols) total_fastled = len(fastled_symbols) fastled_size = sum(s["size"] for s in fastled_symbols) result_text += f"**Summary:**\n" result_text += f"- Total symbols: {total_symbols}\n" result_text += f"- FastLED symbols: {total_fastled}\n" result_text += f"- Total FastLED size: {fastled_size} bytes ({fastled_size/1024:.1f} KB)\n\n" # Initialize fastled_sorted for later use fastled_sorted: List[Dict[str, Any]] = [] if focus_on_fastled and fastled_symbols: result_text += "## Largest FastLED Symbols (Optimization Targets)\n\n" fastled_sorted = sorted(fastled_symbols, key=lambda x: x["size"], reverse=True) for i, sym in enumerate(fastled_sorted[:15]): display_name = sym["demangled_name"][:80] if len(sym["demangled_name"]) > 80: display_name += "..." result_text += f"{i+1:2d}. **{sym['size']:,} bytes** - `{display_name}`\n" if len(fastled_sorted) > 15: result_text += f"\n... and {len(fastled_sorted) - 15} more FastLED symbols\n" result_text += "\n## Largest Overall Symbols\n\n" all_large = sorted(large_symbols, key=lambda x: x["size"], reverse=True) for i, sym in enumerate(all_large[:10]): display_name = sym["demangled_name"][:80] if len(sym["demangled_name"]) > 80: display_name += "..." result_text += f"{i+1:2d}. **{sym['size']:,} bytes** - `{display_name}`\n" # Feature analysis result_text += "\n## Feature Analysis & Recommendations\n\n" feature_patterns = { "JSON functionality": ["json", "Json"], "Audio processing": ["audio", "fft", "Audio"], "2D effects": ["2d", "noise", "matrix"], "Video functionality": ["video", "Video"], "UI components": ["ui", "button", "slider"], "File system": ["file", "File", "fs_"], "Mathematical functions": ["sqrt", "sin", "cos", "math"], "String processing": ["string", "str", "String"], } for feature, patterns in feature_patterns.items(): feature_symbols = [ s for s in fastled_symbols if any(p in s["demangled_name"] for p in patterns) ] if feature_symbols: total_size = sum(s["size"] for s in feature_symbols) result_text += f"- **{feature}**: {len(feature_symbols)} symbols, {total_size:,} bytes" if total_size > 1000: result_text += f" āš ļø Could save ~{total_size/1024:.1f} KB if removed" result_text += "\n" # Include JSON output if requested if output_json: json_data = { "summary": { "total_symbols": total_symbols, "fastled_symbols": total_fastled, "fastled_size": fastled_size, "board": board, "example": example }, "largest_fastled": fastled_sorted[:10] if 'fastled_sorted' in locals() else [], "largest_overall": all_large[:10] } result_text += f"\n## JSON Output\n\n```json\n{json.dumps(json_data, indent=2)}\n```\n" result_text += "\n## Next Steps\n\n" result_text += "1. Identify unused features from the analysis above\n" result_text += "2. Use conditional compilation to exclude unused code\n" result_text += "3. Consider splitting large functions into smaller ones\n" result_text += "4. Re-run analysis after optimizations to measure impact\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) except Exception as e: error_msg = f"Error running ESP32 symbol analysis: {str(e)}" if skip_on_failure: return CallToolResult( content=[TextContent(type="text", text=f"{error_msg}\n\n(Skipped due to skip_on_failure=True)")] ) else: return CallToolResult( content=[TextContent(type="text", text=error_msg)], isError=True ) async def symbol_analysis(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Run generic symbol analysis for any platform to identify optimization opportunities.""" board = arguments.get("board", "auto") example = arguments.get("example", "Blink") output_json = arguments.get("output_json", False) run_all_platforms = arguments.get("run_all_platforms", False) result_text = "# Generic Symbol Analysis Report\n\n" try: if run_all_platforms: # Run demo script for all platforms result_text += "## Running analysis on ALL available platforms\n\n" cmd = ["uv", "run", "ci/demo_symbol_analysis.py"] try: demo_result = await run_command(cmd, project_root) result_text += demo_result except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"Error running demo symbol analysis: {str(e)}")], isError=True ) else: # Run for specific board result_text += f"## Symbol Analysis for Platform: {board}\n\n" cmd = ["uv", "run", "ci/util/symbol_analysis.py", "--board", board] try: analysis_result = await run_command(cmd, project_root) result_text += analysis_result except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"Error running symbol analysis for {board}: {str(e)}")], isError=True ) # Add usage instructions result_text += "\n## How to Use Symbol Analysis\n\n" result_text += "### Available Commands:\n" result_text += "- `uv run -m ci.util.symbol_analysis --board uno` - Analyze UNO platform\n" result_text += "- `uv run -m ci.util.symbol_analysis --board esp32dev` - Analyze ESP32 platform\n" result_text += "- `uv run -m ci.util.symbol_analysis --board teensy31` - Analyze Teensy platform\n" result_text += "- `uv run ci/demo_symbol_analysis.py` - Analyze all available platforms\n\n" result_text += "### Prerequisites:\n" result_text += "1. Compile platform first: `bash compile {board} --examples Blink`\n" result_text += "2. Ensure .build/{board}/build_info.json exists\n" result_text += "3. Run symbol analysis: `uv run -m ci.util.symbol_analysis --board {board}`\n\n" result_text += "### Supported Platforms:\n" result_text += "- [OK] UNO (AVR) - Small embedded platform\n" result_text += "- [OK] ESP32DEV (Xtensa) - WiFi-enabled microcontroller\n" result_text += "- [OK] TEENSY31 (ARM Cortex-M4) - High-performance microcontroller\n" result_text += "- [OK] TEENSYLC (ARM Cortex-M0+) - Low-cost ARM platform\n" result_text += "- [OK] DIGIX (ARM Cortex-M3) - Arduino Due compatible\n" result_text += "- [OK] STM32 (ARM Cortex-M3) - STMicroelectronics platform\n" result_text += "- [OK] And many more! Works with any platform that generates build_info.json\n\n" if output_json: result_text += "### JSON Output\n" result_text += f"Detailed analysis results saved to: `.build/{board}_symbol_analysis.json`\n\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"Error running symbol analysis: {str(e)}")], isError=True ) async def validate_arduino_includes(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Validate that no new Arduino.h includes have been added to the codebase.""" directory = arguments.get("directory", "src") include_examples = arguments.get("include_examples", False) check_dev = arguments.get("check_dev", False) show_approved = arguments.get("show_approved", True) result_text = "# Arduino.h Include Validation\n\n" # Define directories to scan scan_dirs = [directory] if include_examples: scan_dirs.append("examples") if check_dev: scan_dirs.append("dev") # Known approved includes (from our grep search) approved_includes = { "src/sensors/digital_pin.hpp": "ok include", "src/third_party/arduinojson/json.hpp": "ok include", "src/lib8tion.cpp": "ok include", "src/led_sysdefs.h": "ok include", "src/platforms/arm/rp2040/led_sysdefs_arm_rp2040.h": True, # WASM platform includes are specifically for the WASM platform "src/platforms/wasm/led_sysdefs_wasm.h": True, "src/platforms/wasm/clockless.h": True, "src/platforms/wasm/compiler/Arduino.cpp": True, "src/FastLED.h": True, # References WASM Arduino.h } all_includes: List[Dict[str, Any]] = [] violations: List[Dict[str, Any]] = [] approved_count = 0 try: for scan_dir in scan_dirs: scan_path = project_root / scan_dir if not scan_path.exists(): result_text += f"āš ļø Directory not found: {scan_dir}\n" continue result_text += f"šŸ“ Scanning directory: {scan_dir}\n" # Use ripgrep to find Arduino.h includes try: cmd = ["rg", "--type", "cpp", "--type", "c", r"#include.*Arduino\.h", str(scan_path)] output = await run_command(cmd, project_root) for line in output.strip().split('\n'): if not line.strip(): continue # Parse ripgrep output: filename:line_number:content parts = line.split(':', 2) if len(parts) >= 3: file_path = parts[0] line_number = parts[1] content = parts[2].strip() # Make path relative to project root rel_path = str(Path(file_path).relative_to(project_root)) include_info = { 'file': rel_path, 'line': line_number, 'content': content, 'approved': False } # Check if this is an approved include if rel_path in approved_includes: include_info['approved'] = True approved_count += 1 elif "// ok include" in content: include_info['approved'] = True approved_count += 1 elif content.startswith('//') or content.startswith('/*'): # Commented out includes are not violations include_info['approved'] = True approved_count += 1 else: violations.append(include_info) all_includes.append(include_info) except Exception as e: result_text += f"āš ļø Error scanning {scan_dir}: {e}\n" except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"Error validating Arduino includes: {e}")], isError=True ) # Generate report result_text += f"\n## Summary\n" result_text += f"- **Total Arduino.h includes found:** {len(all_includes)}\n" result_text += f"- **Approved includes:** {approved_count}\n" result_text += f"- **🚨 VIOLATIONS:** {len(violations)}\n\n" if violations: result_text += "## 🚨 CRITICAL VIOLATIONS FOUND 🚨\n\n" result_text += "The following files contain PROHIBITED Arduino.h includes:\n\n" for violation in violations: result_text += f"[ERROR] **{violation['file']}:{violation['line']}**\n" result_text += f" `{violation['content']}`\n\n" result_text += "## 🚨 IMMEDIATE ACTION REQUIRED 🚨\n\n" result_text += "These Arduino.h includes MUST be removed or replaced with FastLED alternatives:\n\n" result_text += "1. **Remove the Arduino.h include**\n" result_text += "2. **Use FastLED platform abstractions** from `src/platforms/`\n" result_text += "3. **Replace Arduino functions** with `fl::` namespace equivalents\n" result_text += "4. **If absolutely necessary**, mark with `// ok include` and document why\n\n" is_error = True else: result_text += "[OK] **NO VIOLATIONS FOUND**\n\n" result_text += "All Arduino.h includes are properly approved or commented out.\n\n" is_error = False if show_approved and approved_count > 0: result_text += "## Approved Arduino.h Includes\n\n" result_text += "These includes are pre-approved and should not be modified:\n\n" for include in all_includes: if include['approved']: result_text += f"[OK] {include['file']}:{include['line']}\n" result_text += f" `{include['content']}`\n\n" result_text += "---\n\n" result_text += "**Remember:** Never add new Arduino.h includes. Use FastLED's platform abstraction layer instead!\n" return CallToolResult( content=[TextContent(type="text", text=result_text)], isError=is_error ) async def run_fastled_web_compiler(arguments: Dict[str, Any], project_root: Path) -> CallToolResult: """Run FastLED web compiler with playwright console.log capture.""" # Check if this is a background agent and refuse to run import os if os.environ.get('FASTLED_CI_NO_INTERACTIVE') == 'true' or os.environ.get('CI') == 'true': return CallToolResult( content=[TextContent(type="text", text="🚫 FastLED Web Compiler is disabled for background agents. This tool is only available for foreground agents with interactive environments.")], isError=True ) example_path = arguments.get("example_path", "examples/Audio") capture_duration = arguments.get("capture_duration", 30) headless = arguments.get("headless", False) port = arguments.get("port", 0) docker_check = arguments.get("docker_check", True) save_screenshot = arguments.get("save_screenshot", True) # Check prerequisites result_text = "🌐 FastLED Web Compiler with Console.log Capture\n" result_text += "=" * 50 + "\n\n" # Check if fastled command is available if not shutil.which("fastled"): return CallToolResult( content=[TextContent(type="text", text="[ERROR] FastLED command not found. Please install with: pip install fastled")], isError=True ) result_text += "[OK] FastLED command found\n" # Check if Docker is available (optional) docker_available = shutil.which("docker") is not None if docker_check: if docker_available: result_text += "[OK] Docker available (faster compilation)\n" else: result_text += "āš ļø Docker not available (slower compilation)\n" # Check if playwright is available try: from playwright.async_api import async_playwright result_text += "[OK] Playwright available\n" except ImportError: return CallToolResult( content=[TextContent(type="text", text="[ERROR] Playwright not found. Please install with: pip install playwright")], isError=True ) # Validate example path example_dir = project_root / example_path if not example_dir.exists(): return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Example directory not found: {example_path}")], isError=True ) result_text += f"[OK] Example directory found: {example_path}\n\n" # Install playwright browsers result_text += "šŸ“¦ Installing Playwright browsers...\n" try: install_result = subprocess.run( [sys.executable, "-m", "playwright", "install", "chromium"], capture_output=True, text=True, cwd=project_root ) if install_result.returncode != 0: result_text += f"āš ļø Playwright browser installation warning: {install_result.stderr}\n" else: result_text += "[OK] Playwright browsers installed\n" except Exception as e: result_text += f"āš ļø Playwright browser installation error: {e}\n" # Run fastled compiler result_text += f"\nšŸ”§ Compiling {example_path} with FastLED...\n" # Store original directory before trying operations original_cwd = Path.cwd() try: # Change to example directory os.chdir(example_dir) # Run fastled command compile_result = subprocess.run( ["fastled", "--just-compile", "."], capture_output=True, text=True, timeout=300 # 5 minute timeout ) if compile_result.returncode != 0: os.chdir(original_cwd) return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] FastLED compilation failed:\n{compile_result.stderr}")], isError=True ) result_text += "[OK] FastLED compilation successful\n" result_text += f"Compilation output:\n{compile_result.stdout}\n\n" # Check for generated files fastled_js_dir = example_dir / "fastled_js" if not fastled_js_dir.exists(): os.chdir(original_cwd) return CallToolResult( content=[TextContent(type="text", text="[ERROR] FastLED output directory not found: fastled_js")], isError=True ) required_files = ["fastled.js", "fastled.wasm", "index.html"] missing_files = [f for f in required_files if not (fastled_js_dir / f).exists()] if missing_files: os.chdir(original_cwd) return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Missing required files: {missing_files}")], isError=True ) result_text += f"[OK] All required files generated in {fastled_js_dir}\n\n" # Start HTTP server and playwright result_text += "🌐 Starting web server and browser automation...\n" # Use Python's built-in HTTP server import http.server import socketserver from threading import Thread import socket # Find available port if port == 0: sock = socket.socket() sock.bind(('', 0)) port = sock.getsockname()[1] sock.close() # Start HTTP server in thread os.chdir(fastled_js_dir) handler = http.server.SimpleHTTPRequestHandler httpd = socketserver.TCPServer(("", port), handler) server_thread = Thread(target=httpd.serve_forever) server_thread.daemon = True server_thread.start() result_text += f"[OK] HTTP server started on port {port}\n" # Run playwright automation console_logs: List[str] = [] error_logs: List[str] = [] async def run_playwright(): async with async_playwright() as p: browser = await p.chromium.launch(headless=headless) page = await browser.new_page() # Setup console log capture def handle_console(msg: Any): timestamp = time.strftime("%H:%M:%S") log_entry = f"[{timestamp}] {msg.type}: {msg.text}" console_logs.append(log_entry) if msg.type in ["error", "warning"]: error_logs.append(log_entry) page.on("console", handle_console) # Setup error handlers page.on("pageerror", lambda err: error_logs.append(f"Page Error: {err}")) try: # Navigate to the page await page.goto(f"http://localhost:{port}", timeout=30000) # Wait for FastLED to initialize await page.evaluate(""" window.frameCallCount = 0; window.consoleLogCount = 0; globalThis.FastLED_onFrame = (jsonStr) => { console.log('FastLED_onFrame called:', jsonStr); window.frameCallCount++; }; """) # Wait for page to load and run await page.wait_for_timeout(5000) # Check if FastLED initialized frame_count = await page.evaluate("window.frameCallCount || 0") # Capture for specified duration await page.wait_for_timeout(capture_duration * 1000) # Take screenshot if requested screenshot_path = None if save_screenshot: screenshot_path = fastled_js_dir / f"fastled_capture_{int(time.time())}.png" await page.screenshot(path=str(screenshot_path)) return frame_count, screenshot_path finally: await browser.close() # Run the playwright automation frame_count, screenshot_path = await run_playwright() # Stop HTTP server httpd.shutdown() # Generate report result_text += f"\nšŸ“Š Capture Results ({capture_duration}s):\n" result_text += f" • FastLED_onFrame calls: {frame_count}\n" result_text += f" • Console log entries: {len(console_logs)}\n" result_text += f" • Error/Warning logs: {len(error_logs)}\n" if screenshot_path: result_text += f" • Screenshot saved: {screenshot_path.name}\n" result_text += "\nšŸ“‹ Console Logs:\n" result_text += "-" * 40 + "\n" if console_logs: for log in console_logs[-20:]: # Show last 20 logs result_text += f"{log}\n" if len(console_logs) > 20: result_text += f"... ({len(console_logs) - 20} more logs)\n" else: result_text += "No console logs captured\n" if error_logs: result_text += "\n[ERROR] Errors/Warnings:\n" result_text += "-" * 40 + "\n" for error in error_logs: result_text += f"{error}\n" result_text += "\n[OK] FastLED web compiler execution completed successfully!\n" # Analysis if frame_count > 0: result_text += f"\nšŸŽÆ Analysis: FastLED is running correctly ({frame_count} frames rendered)\n" else: result_text += "\nāš ļø Analysis: FastLED may not be initializing properly (no frames detected)\n" return CallToolResult( content=[TextContent(type="text", text=result_text)] ) except subprocess.TimeoutExpired: return CallToolResult( content=[TextContent(type="text", text="[ERROR] FastLED compilation timed out (5 minutes)")], isError=True ) except Exception as e: return CallToolResult( content=[TextContent(type="text", text=f"[ERROR] Error running FastLED web compiler: {str(e)}")], isError=True ) finally: # Restore original directory try: os.chdir(original_cwd) except: pass async def run_command(cmd: List[str], cwd: Path) -> str: """Run a shell command and return its output.""" try: process = await asyncio.create_subprocess_exec( *cmd, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) stdout_bytes, _ = await process.communicate() stdout = stdout_bytes.decode('utf-8', errors='replace') if stdout_bytes else "" if process.returncode != 0: return f"Command failed with exit code {process.returncode}:\n{stdout}" return stdout except Exception as e: return f"Error running command: {str(e)}" async def main(): """Main entry point for the MCP server.""" try: async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options() ) except Exception as e: print(f"Error running MCP server: {e}") print("Make sure the MCP library is properly installed.") print("Try: pip install mcp") sys.exit(1) if __name__ == "__main__": if not MCP_AVAILABLE: print("MCP library is required but not installed.") print("Install with: pip install mcp") sys.exit(1) asyncio.run(main())