#
tokens: 37681/50000 25/25 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .gitignore
├── Dockerfile
├── documentation
│   ├── 01_configuration_management.md
│   ├── 02_device_control_interfaces.md
│   ├── 03_govee_api_client.md
│   ├── 04_command_line_interface__cli_.md
│   ├── 05_mcp_server_implementation.md
│   ├── 06_custom_error_handling.md
│   └── index.md
├── inspector.bat
├── inspector.sh
├── LICENSE
├── pyproject.toml
├── README.md
├── slides
│   └── Meetup 30 janvier.pptx
├── smithery.yaml
├── src
│   └── govee_mcp_server
│       ├── __init__.py
│       ├── api.py
│       ├── cli.py
│       ├── config.py
│       ├── exceptions.py
│       ├── interfaces.py
│       ├── server.py
│       └── transformers.py
├── tests
│   ├── test_cli.py
│   └── test_server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | GOVEE_API_KEY=<API_KEY_FROM_GOVEE_APP>
2 | GOVEE_DEVICE_ID=<MAC_ADDRESS_OF_THE_DEVICE>
3 | GOVEE_SKU=<SKU_OF_THE_DEVICE>
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | local_docs/
  2 | .DS_Store
  3 | Thumbs.db
  4 | 
  5 | # Byte-compiled / optimized / DLL files
  6 | __pycache__/
  7 | *.py[cod]
  8 | *$py.class
  9 | 
 10 | # C extensions
 11 | *.so
 12 | 
 13 | # Distribution / packaging
 14 | .Python
 15 | env/
 16 | venv/
 17 | ENV/
 18 | env.bak/
 19 | venv.bak/
 20 | *.egg-info/
 21 | dist/
 22 | build/
 23 | 
 24 | # PyInstaller
 25 | #  Usually these files are written by a python script from a template
 26 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 27 | *.manifest
 28 | *.spec
 29 | 
 30 | # Installer logs
 31 | pip-log.txt
 32 | pip-delete-this-directory.txt
 33 | 
 34 | # Unit test / coverage reports
 35 | htmlcov/
 36 | .tox/
 37 | .nox/
 38 | .coverage
 39 | .coverage.*
 40 | .cache
 41 | nosetests.xml
 42 | coverage.xml
 43 | *.cover
 44 | *.py,cover
 45 | .hypothesis/
 46 | 
 47 | # Translations
 48 | *.mo
 49 | *.pot
 50 | 
 51 | # Django stuff:
 52 | *.log
 53 | local_settings.py
 54 | db.sqlite3
 55 | 
 56 | # Flask stuff:
 57 | instance/
 58 | .webassets-cache
 59 | 
 60 | # Scrapy stuff:
 61 | .scrapy
 62 | 
 63 | # Sphinx documentation
 64 | docs/_build/
 65 | 
 66 | # PyBuilder
 67 | target/
 68 | 
 69 | # Jupyter Notebook
 70 | .ipynb_checkpoints
 71 | 
 72 | # IPython
 73 | profile_default/
 74 | ipython_config.py
 75 | 
 76 | # pyenv
 77 | .python-version
 78 | 
 79 | # celery beat schedule file
 80 | celerybeat-schedule
 81 | 
 82 | # dotenv
 83 | .env
 84 | 
 85 | # virtualenv
 86 | .venv/
 87 | venv/
 88 | ENV/
 89 | env.bak/
 90 | venv.bak/
 91 | 
 92 | # Spyder project settings
 93 | .spyderproject
 94 | .spyproject
 95 | 
 96 | # Rope project settings
 97 | .ropeproject
 98 | 
 99 | # mkdocs documentation
100 | /site
101 | 
102 | # mypy
103 | .mypy_cache/
104 | .dmypy.json
105 | dmypy.json
106 | 
107 | # Pyre type checker
108 | .pyre/
109 | 
110 | # pytype static type analyzer
111 | .pytype/
112 | 
113 | # Cython debug symbols
114 | cython_debug/
115 | 
116 | # VSCode settings
117 | .vscode/
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Govee MCP Server
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@mathd/govee_mcp_server)](https://smithery.ai/server/@mathd/govee_mcp_server)
  4 | 
  5 | An MCP server for controlling Govee LED devices through the Govee API.
  6 | 
  7 | ## Setup
  8 | 
  9 | ### Environment Variables
 10 | 
 11 | Create a `.env` file in the root directory with the following variables:
 12 | 
 13 | ```bash
 14 | GOVEE_API_KEY=your_api_key_here
 15 | GOVEE_DEVICE_ID=your_device_id_here
 16 | GOVEE_SKU=your_device_sku_here
 17 | ```
 18 | 
 19 | To get these values:
 20 | 1. Get your API key from the Govee Developer Portal
 21 | 2. Use the Govee Home app to find your device ID and SKU
 22 | 
 23 | ## Installation
 24 | 
 25 | ### Installing via Smithery
 26 | 
 27 | To install Govee MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@mathd/govee_mcp_server):
 28 | 
 29 | ```bash
 30 | npx -y @smithery/cli install @mathd/govee_mcp_server --client claude
 31 | ```
 32 | 
 33 | ### Manual Installation
 34 | 
 35 | ```bash
 36 | # Install with pip
 37 | pip install .
 38 | 
 39 | # For development (includes test dependencies)
 40 | pip install -e ".[test]"
 41 | ```
 42 | 
 43 | ## Usage
 44 | 
 45 | ### MCP Server
 46 | 
 47 | The MCP server provides tools for controlling Govee devices through the Model Context Protocol. It can be used with Cline or other MCP clients.
 48 | 
 49 | Available tools:
 50 | - `turn_on_off`: Turn the LED on or off
 51 | - `set_color`: Set the LED color using RGB values
 52 | - `set_brightness`: Set the LED brightness level
 53 | 
 54 | ### Command Line Interface
 55 | 
 56 | A CLI is provided for direct control of Govee devices:
 57 | 
 58 | ```bash
 59 | # Turn device on/off
 60 | govee-cli power on
 61 | govee-cli power off
 62 | 
 63 | # Set color using RGB values (0-255)
 64 | govee-cli color 255 0 0  # Red
 65 | govee-cli color 0 255 0  # Green
 66 | govee-cli color 0 0 255  # Blue
 67 | 
 68 | # Set brightness (0-100)
 69 | govee-cli brightness 50
 70 | ```
 71 | 
 72 | Run `govee-cli --help` for full command documentation.
 73 | 
 74 | ## Development
 75 | 
 76 | ### Running Tests
 77 | 
 78 | To run the test suite:
 79 | 
 80 | ```bash
 81 | # Install test dependencies
 82 | pip install -e ".[test]"
 83 | 
 84 | # Run all tests
 85 | pytest tests/
 86 | 
 87 | # Run specific test files
 88 | pytest tests/test_server.py  # Server tests (mocked API calls)
 89 | pytest tests/test_cli.py    # CLI tests (real API calls)
 90 | 
 91 | # Run tests with verbose output
 92 | pytest tests/ -v
 93 | ```
 94 | 
 95 | Note: The CLI tests make real API calls to your Govee device and will actually control it. Make sure your device is powered and connected before running these tests.
 96 | 
 97 | ### Project Structure
 98 | 
 99 | ```
100 | .
101 | ├── src/govee_mcp_server/
102 | │   ├── __init__.py
103 | │   ├── server.py    # MCP server implementation
104 | │   └── cli.py       # Command-line interface
105 | ├── tests/
106 | │   ├── test_server.py  # Server tests (with mocked API)
107 | │   └── test_cli.py     # CLI tests (real API calls)
108 | └── pyproject.toml      # Project configuration
109 | ```
110 | 
111 | ### Test Coverage
112 | 
113 | - Server tests cover:
114 |   - Environment initialization
115 |   - Govee API client methods
116 |   - Server tools and utilities
117 |   - Error handling
118 | 
119 | - CLI tests perform real-world integration testing by executing actual API calls to control your Govee device.
120 | 
```

--------------------------------------------------------------------------------
/inspector.bat:
--------------------------------------------------------------------------------

```
1 | npx @modelcontextprotocol/inspector uv --directory  . run python src\govee_mcp_server\server.py
```

--------------------------------------------------------------------------------
/inspector.sh:
--------------------------------------------------------------------------------

```bash
1 | npx @modelcontextprotocol/inspector uv --directory . run python src/govee_mcp_server/server.py
2 | 
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/exceptions.py:
--------------------------------------------------------------------------------

```python
 1 | class GoveeError(Exception):
 2 |     """Base exception for Govee-related errors."""
 3 |     pass
 4 | 
 5 | class GoveeAPIError(GoveeError):
 6 |     """Raised when API communication fails."""
 7 |     pass
 8 | 
 9 | class GoveeConfigError(GoveeError):
10 |     """Raised when there are configuration-related errors."""
11 |     pass
12 | 
13 | class GoveeValidationError(GoveeError):
14 |     """Raised when input validation fails."""
15 |     pass
16 | 
17 | class GoveeConnectionError(GoveeError):
18 |     """Raised when network connection issues occur."""
19 |     pass
20 | 
21 | class GoveeTimeoutError(GoveeError):
22 |     """Raised when requests timeout."""
23 |     pass
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "govee-mcp-server"
 3 | version = "0.1.0"
 4 | description = "MCP server to control Govee LED devices"
 5 | authors = []
 6 | dependencies = [
 7 |     "mcp[cli]",
 8 |     "govee-api-laggat>=0.2.2",
 9 |     "python-dotenv"
10 | ]
11 | requires-python = ">=3.10"
12 | 
13 | [project.scripts]
14 | govee-cli = "govee_mcp_server.cli:cli_main"
15 | 
16 | [project.optional-dependencies]
17 | test = [
18 |     "pytest>=7.0",
19 |     "pytest-asyncio>=0.21.0",
20 |     "pytest-mock>=3.10.0"
21 | ]
22 | 
23 | [build-system]
24 | requires = ["hatchling"]
25 | build-backend = "hatchling.build"
26 | 
27 | [tool.hatch.build.targets.wheel]
28 | packages = ["src/govee_mcp_server"]
29 | 
30 | [tool.hatch.metadata]
31 | allow-direct-references = true
32 | 
33 | [tool.pytest.ini_options]
34 | asyncio_mode = "auto"
35 | testpaths = ["tests"]
36 | pythonpath = ["src"]
37 | asyncio_default_fixture_loop_scope = "function"
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - goveeApiKey
10 |       - goveeDeviceId
11 |       - goveeSku
12 |     properties:
13 |       goveeApiKey:
14 |         type: string
15 |         description: The API key for the Govee API.
16 |       goveeDeviceId:
17 |         type: string
18 |         description: The device ID for the Govee device.
19 |       goveeSku:
20 |         type: string
21 |         description: The SKU for the Govee device.
22 |   commandFunction:
23 |     # A function that produces the CLI command to start the MCP on stdio.
24 |     |-
25 |     (config) => ({command: 'python', args: ['src/govee_mcp_server/server.py'], env: {GOVEE_API_KEY: config.goveeApiKey, GOVEE_DEVICE_ID: config.goveeDeviceId, GOVEE_SKU: config.goveeSku}})
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Govee MCP Server - A Model Context Protocol server for controlling Govee LED devices.
 3 | 
 4 | This package provides a complete interface for controlling Govee LED devices through both
 5 | an MCP server implementation and a CLI interface.
 6 | """
 7 | 
 8 | from .api import GoveeAPI
 9 | from .config import GoveeConfig, load_config
10 | from .exceptions import (
11 |     GoveeError,
12 |     GoveeAPIError,
13 |     GoveeConfigError,
14 |     GoveeValidationError,
15 |     GoveeConnectionError,
16 |     GoveeTimeoutError
17 | )
18 | from .transformers import ColorTransformer
19 | from .interfaces import PowerControl, ColorControl, BrightnessControl
20 | 
21 | __version__ = "0.1.0"
22 | __all__ = [
23 |     'GoveeAPI',
24 |     'GoveeConfig',
25 |     'load_config',
26 |     'GoveeError',
27 |     'GoveeAPIError',
28 |     'GoveeConfigError',
29 |     'GoveeValidationError',
30 |     'GoveeConnectionError',
31 |     'GoveeTimeoutError',
32 |     'ColorTransformer',
33 |     'PowerControl',
34 |     'ColorControl',
35 |     'BrightnessControl'
36 | ]
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | # Use a Python image with uv pre-installed
 3 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
 4 | 
 5 | # Set the working directory
 6 | WORKDIR /app
 7 | 
 8 | # Enable bytecode compilation
 9 | ENV UV_COMPILE_BYTECODE=1
10 | 
11 | # Copy from the cache instead of linking since it's a mounted volume
12 | ENV UV_LINK_MODE=copy
13 | 
14 | # Install the project's dependencies using the lockfile and settings
15 | RUN --mount=type=cache,target=/root/.cache/uv \
16 |     --mount=type=bind,source=uv.lock,target=uv.lock \
17 |     --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
18 |     uv sync --frozen --no-install-project --no-dev --no-editable
19 | 
20 | # Copy the project source code
21 | COPY . /app
22 | 
23 | # Install the project
24 | RUN --mount=type=cache,target=/root/.cache/uv \
25 |     uv sync --frozen --no-dev --no-editable
26 | 
27 | # Final image
28 | FROM python:3.12-slim-bookworm
29 | 
30 | # Set the working directory
31 | WORKDIR /app
32 | 
33 | # Copy the environment setup from the uv image
34 | COPY --from=uv --chown=app:app /app/.venv /app/.venv
35 | 
36 | # Place executables in the environment at the front of the path
37 | ENV PATH="/app/.venv/bin:$PATH"
38 | 
39 | # when running the container, add --db-path and a bind mount to the host's db file
40 | ENTRYPOINT ["python", "src/govee_mcp_server/server.py"]
```

--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------

```python
 1 | import pytest
 2 | import sys
 3 | import asyncio
 4 | from govee_mcp_server.cli import main
 5 | from govee_mcp_server.config import load_config, GoveeConfigError
 6 | 
 7 | # Delay between commands (in seconds)
 8 | DELAY = 1
 9 | 
10 | @pytest.mark.asyncio
11 | async def test_cli_interface():
12 |     """Test CLI interface with real API calls"""
13 |     try:
14 |         # Load actual config from environment
15 |         config = load_config()
16 |         
17 |         # Power on
18 |         sys.argv = ['cli.py', 'power', 'on']
19 |         await main()
20 |         await asyncio.sleep(DELAY)
21 |         
22 |         # Red color
23 |         sys.argv = ['cli.py', 'color', '255', '0', '0']
24 |         await main()
25 |         await asyncio.sleep(DELAY)
26 |         
27 |         # Green color
28 |         sys.argv = ['cli.py', 'color', '0', '255', '0']
29 |         await main()
30 |         await asyncio.sleep(DELAY)
31 |         
32 |         # Blue color
33 |         sys.argv = ['cli.py', 'color', '0', '0', '255']
34 |         await main()
35 |         await asyncio.sleep(DELAY)
36 |         
37 |         # Power off
38 |         sys.argv = ['cli.py', 'power', 'off']
39 |         await main()
40 |         
41 |     except GoveeConfigError as e:
42 |         pytest.skip(f"Skipping test: {str(e)}")
43 |         
44 |     except Exception as e:
45 |         # If we hit rate limits or other API errors, fail with clear message
46 |         pytest.fail(f"API Error: {str(e)}")
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/config.py:
--------------------------------------------------------------------------------

```python
 1 | from pathlib import Path
 2 | from dataclasses import dataclass
 3 | import os
 4 | from dotenv import load_dotenv
 5 | from typing import Optional
 6 | 
 7 | @dataclass
 8 | class GoveeConfig:
 9 |     """Configuration class for Govee API settings."""
10 |     api_key: str
11 |     device_id: str
12 |     sku: str
13 | 
14 | class GoveeConfigError(Exception):
15 |     """Configuration-related errors for Govee MCP server."""
16 |     pass
17 | 
18 | def load_config() -> GoveeConfig:
19 |     """
20 |     Load and validate configuration from environment variables.
21 |     
22 |     Returns:
23 |         GoveeConfig: Configuration object with API settings
24 |         
25 |     Raises:
26 |         GoveeConfigError: If required environment variables are missing
27 |     """
28 |     env_path = Path(__file__).resolve().parent.parent.parent / '.env'
29 |     load_dotenv(env_path)
30 |     
31 |     api_key = os.getenv('GOVEE_API_KEY')
32 |     device_id = os.getenv('GOVEE_DEVICE_ID')
33 |     sku = os.getenv('GOVEE_SKU')
34 |     
35 |     missing = []
36 |     if not api_key:
37 |         missing.append('GOVEE_API_KEY')
38 |     if not device_id:
39 |         missing.append('GOVEE_DEVICE_ID')
40 |     if not sku:
41 |         missing.append('GOVEE_SKU')
42 |         
43 |     if missing:
44 |         raise GoveeConfigError(f"Missing required environment variables: {', '.join(missing)}")
45 |         
46 |     return GoveeConfig(
47 |         api_key=api_key,
48 |         device_id=device_id,
49 |         sku=sku
50 |     )
```

--------------------------------------------------------------------------------
/documentation/index.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Tutorial: govee_mcp_server
 2 | 
 3 | This project lets you control your **Govee LED lights** over the internet.
 4 | It acts as a bridge, translating commands into the *Govee API language*.
 5 | You can interact with it either through an **AI assistant** (using the *MCP server*) or directly using simple **command-line** instructions.
 6 | It needs your Govee *API key* and *device details* to work, which it reads from configuration settings.
 7 | 
 8 | 
 9 | **Source Repository:** [https://github.com/mathd/govee_mcp_server](https://github.com/mathd/govee_mcp_server)
10 | 
11 | ```mermaid
12 | flowchart TD
13 |     A0["Govee API Client"]
14 |     A1["MCP Server Implementation"]
15 |     A2["Configuration Management"]
16 |     A3["Device Control Interfaces"]
17 |     A4["Command Line Interface (CLI)"]
18 |     A5["Custom Error Handling"]
19 |     A1 -- "Executes commands via" --> A0
20 |     A4 -- "Executes commands via" --> A0
21 |     A0 -- "Reads settings from" --> A2
22 |     A0 -- "Implements" --> A3
23 |     A0 -- "Raises API/Network Errors" --> A5
24 |     A1 -- "Handles Errors From" --> A5
25 |     A4 -- "Handles Errors From" --> A5
26 |     A2 -- "Raises Configuration Errors" --> A5
27 |     A3 -- "Raises Validation Errors" --> A5
28 | ```
29 | 
30 | ## Chapters
31 | 
32 | 1. [Configuration Management](01_configuration_management.md)
33 | 2. [Device Control Interfaces](02_device_control_interfaces.md)
34 | 3. [Govee API Client](03_govee_api_client.md)
35 | 4. [Command Line Interface (CLI)](04_command_line_interface__cli_.md)
36 | 5. [MCP Server Implementation](05_mcp_server_implementation.md)
37 | 6. [Custom Error Handling](06_custom_error_handling.md)
38 | 
39 | 
40 | ---
41 | 
42 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/interfaces.py:
--------------------------------------------------------------------------------

```python
 1 | from abc import ABC, abstractmethod
 2 | from typing import Tuple
 3 | from functools import wraps
 4 | from .exceptions import GoveeValidationError
 5 | 
 6 | def validate_rgb(func):
 7 |     """Decorator to validate RGB color values."""
 8 |     @wraps(func)
 9 |     async def wrapper(self, r: int, g: int, b: int, *args, **kwargs):
10 |         for name, value in [('red', r), ('green', g), ('blue', b)]:
11 |             if not isinstance(value, int):
12 |                 raise GoveeValidationError(f"{name} value must be an integer")
13 |             if not 0 <= value <= 255:
14 |                 raise GoveeValidationError(f"{name} value must be between 0-255")
15 |         return await func(self, r, g, b, *args, **kwargs)
16 |     return wrapper
17 | 
18 | class PowerControl(ABC):
19 |     """Interface for power control capabilities."""
20 |     @abstractmethod
21 |     async def set_power(self, state: bool) -> Tuple[bool, str]:
22 |         """
23 |         Set device power state.
24 |         
25 |         Args:
26 |             state: True for on, False for off
27 |             
28 |         Returns:
29 |             Tuple of (success: bool, message: str)
30 |         """
31 |         pass
32 | 
33 |     @abstractmethod
34 |     async def get_power_state(self) -> Tuple[bool, str]:
35 |         """
36 |         Get current power state.
37 |         
38 |         Returns:
39 |             Tuple of (is_on: bool, message: str)
40 |         """
41 |         pass
42 | 
43 | class ColorControl(ABC):
44 |     """Interface for color control capabilities."""
45 |     @abstractmethod
46 |     @validate_rgb
47 |     async def set_color(self, r: int, g: int, b: int) -> Tuple[bool, str]:
48 |         """
49 |         Set device color using RGB values.
50 |         
51 |         Args:
52 |             r: Red value (0-255)
53 |             g: Green value (0-255)
54 |             b: Blue value (0-255)
55 |             
56 |         Returns:
57 |             Tuple of (success: bool, message: str)
58 |         """
59 |         pass
60 | 
61 |     @abstractmethod
62 |     async def get_color(self) -> Tuple[Tuple[int, int, int], str]:
63 |         """
64 |         Get current color values.
65 |         
66 |         Returns:
67 |             Tuple of ((r, g, b): Tuple[int, int, int], message: str)
68 |         """
69 |         pass
70 | 
71 | class BrightnessControl(ABC):
72 |     """Interface for brightness control capabilities."""
73 |     @abstractmethod
74 |     async def set_brightness(self, level: int) -> Tuple[bool, str]:
75 |         """
76 |         Set device brightness level.
77 |         
78 |         Args:
79 |             level: Brightness level (0-100)
80 |             
81 |         Returns:
82 |             Tuple of (success: bool, message: str)
83 |         """
84 |         pass
85 | 
86 |     @abstractmethod
87 |     async def get_brightness(self) -> Tuple[int, str]:
88 |         """
89 |         Get current brightness level.
90 |         
91 |         Returns:
92 |             Tuple of (level: int, message: str)
93 |         """
94 |         pass
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/transformers.py:
--------------------------------------------------------------------------------

```python
 1 | from typing import Tuple, Dict, Any
 2 | from .exceptions import GoveeValidationError
 3 | 
 4 | class ColorTransformer:
 5 |     """Handle color transformations and validations."""
 6 |     
 7 |     @staticmethod
 8 |     def validate_rgb(r: int, g: int, b: int) -> None:
 9 |         """
10 |         Validate RGB color values.
11 |         
12 |         Args:
13 |             r: Red value (0-255)
14 |             g: Green value (0-255)
15 |             b: Blue value (0-255)
16 |             
17 |         Raises:
18 |             GoveeValidationError: If values are invalid
19 |         """
20 |         for name, value in [('red', r), ('green', g), ('blue', b)]:
21 |             if not isinstance(value, int):
22 |                 raise GoveeValidationError(f"{name} value must be an integer")
23 |             if not 0 <= value <= 255:
24 |                 raise GoveeValidationError(f"{name} value must be between 0-255")
25 | 
26 |     @staticmethod
27 |     def rgb_to_hex(r: int, g: int, b: int) -> str:
28 |         """
29 |         Convert RGB values to hexadecimal color code.
30 |         
31 |         Args:
32 |             r: Red value (0-255)
33 |             g: Green value (0-255)
34 |             b: Blue value (0-255)
35 |             
36 |         Returns:
37 |             str: Hexadecimal color code
38 |         """
39 |         ColorTransformer.validate_rgb(r, g, b)
40 |         return f"#{r:02x}{g:02x}{b:02x}"
41 | 
42 |     @staticmethod
43 |     def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
44 |         """
45 |         Convert hexadecimal color code to RGB values.
46 |         
47 |         Args:
48 |             hex_color: Hexadecimal color code (e.g., '#ff00ff' or 'ff00ff')
49 |             
50 |         Returns:
51 |             Tuple[int, int, int]: RGB values
52 |             
53 |         Raises:
54 |             GoveeValidationError: If hex color format is invalid
55 |         """
56 |         # Remove '#' if present
57 |         hex_color = hex_color.lstrip('#')
58 |         
59 |         if len(hex_color) != 6:
60 |             raise GoveeValidationError("Invalid hex color format")
61 |         
62 |         try:
63 |             r = int(hex_color[:2], 16)
64 |             g = int(hex_color[2:4], 16)
65 |             b = int(hex_color[4:], 16)
66 |             return (r, g, b)
67 |         except ValueError:
68 |             raise GoveeValidationError("Invalid hex color format")
69 | 
70 |     @staticmethod
71 |     def to_api_payload(r: int, g: int, b: int) -> Dict[str, Any]:
72 |         """
73 |         Convert RGB values to API payload format.
74 |         
75 |         Args:
76 |             r: Red value (0-255)
77 |             g: Green value (0-255)
78 |             b: Blue value (0-255)
79 |             
80 |         Returns:
81 |             Dict[str, Any]: API payload
82 |         """
83 |         ColorTransformer.validate_rgb(r, g, b)
84 |         return {
85 |             "color": {
86 |                 "r": r,
87 |                 "g": g,
88 |                 "b": b
89 |             }
90 |         }
```

--------------------------------------------------------------------------------
/tests/test_server.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | import asyncio
  3 | import aiohttp
  4 | from unittest.mock import patch, MagicMock
  5 | from govee_mcp_server.server import (
  6 |     init_env,
  7 |     GoveeDirectAPI,
  8 |     fix_json,
  9 |     rgb_to_int,
 10 | )
 11 | 
 12 | @pytest.fixture
 13 | def mock_env_vars():
 14 |     with patch.dict('os.environ', {
 15 |         'GOVEE_API_KEY': 'test-api-key',
 16 |         'GOVEE_DEVICE_ID': 'test-device-id',
 17 |         'GOVEE_SKU': 'test-sku'
 18 |     }):
 19 |         yield
 20 | 
 21 | def test_init_env(mock_env_vars):
 22 |     api_key, device_id, sku = init_env()
 23 |     assert api_key == 'test-api-key'
 24 |     assert device_id == 'test-device-id'
 25 |     assert sku == 'test-sku'
 26 | 
 27 | @patch('os.getenv', return_value=None)
 28 | def test_init_env_missing_vars(_):
 29 |     with pytest.raises(SystemExit):
 30 |         init_env()
 31 | 
 32 | def test_fix_json():
 33 |     malformed = '{"key1""value1"}{"key2""value2"}'
 34 |     expected = '{"key1","value1"},{"key2","value2"}'
 35 |     assert fix_json(malformed) == expected
 36 | 
 37 | def test_rgb_to_int():
 38 |     assert rgb_to_int(255, 0, 0) == 0xFF0000
 39 |     assert rgb_to_int(0, 255, 0) == 0x00FF00
 40 |     assert rgb_to_int(0, 0, 255) == 0x0000FF
 41 |     assert rgb_to_int(255, 255, 255) == 0xFFFFFF
 42 | 
 43 | class TestGoveeDirectAPI:
 44 |     @pytest.fixture
 45 |     def api(self):
 46 |         return GoveeDirectAPI('test-api-key')
 47 | 
 48 |     def test_init(self, api):
 49 |         assert api.api_key == 'test-api-key'
 50 |         assert api.headers['Govee-API-Key'] == 'test-api-key'
 51 |         assert api.headers['Content-Type'] == 'application/json'
 52 | 
 53 |     @pytest.mark.asyncio
 54 |     async def test_get_devices_success(self, api):
 55 |         mock_response = MagicMock()
 56 |         mock_response.status = 200
 57 |         async def mock_text():
 58 |             return '{"data":[{"device":"test"}]}'
 59 |         mock_response.text = mock_text
 60 | 
 61 |         with patch('aiohttp.ClientSession.get') as mock_get:
 62 |             mock_get.return_value.__aenter__.return_value = mock_response
 63 |             devices, error = await api.get_devices()
 64 |             assert devices == [{"device": "test"}]
 65 |             assert error is None
 66 | 
 67 |     @pytest.mark.asyncio
 68 |     async def test_get_devices_error(self, api):
 69 |         mock_response = MagicMock()
 70 |         mock_response.status = 401
 71 |         async def mock_text():
 72 |             return '{"message":"Unauthorized"}'
 73 |         mock_response.text = mock_text
 74 | 
 75 |         with patch('aiohttp.ClientSession.get') as mock_get:
 76 |             mock_get.return_value.__aenter__.return_value = mock_response
 77 |             devices, error = await api.get_devices()
 78 |             assert devices is None
 79 |             assert error == "Unauthorized"
 80 | 
 81 |     @pytest.mark.asyncio
 82 |     async def test_control_device_success(self, api):
 83 |         mock_response = MagicMock()
 84 |         mock_response.status = 200
 85 |         async def mock_text():
 86 |             return '{"message":"Success"}'
 87 |         mock_response.text = mock_text
 88 | 
 89 |         with patch('aiohttp.ClientSession.post') as mock_post:
 90 |             mock_post.return_value.__aenter__.return_value = mock_response
 91 |             success, error = await api.control_device(
 92 |                 "sku123",
 93 |                 "device123",
 94 |                 "devices.capabilities.on_off",
 95 |                 "powerSwitch",
 96 |                 1
 97 |             )
 98 |             assert success is True
 99 |             assert error == "Success"
100 | 
101 |     @pytest.mark.asyncio
102 |     async def test_control_device_error(self, api):
103 |         mock_response = MagicMock()
104 |         mock_response.status = 400
105 |         async def mock_text():
106 |             return '{"message":"Bad Request"}'
107 |         mock_response.text = mock_text
108 | 
109 |         with patch('aiohttp.ClientSession.post') as mock_post:
110 |             mock_post.return_value.__aenter__.return_value = mock_response
111 |             success, error = await api.control_device(
112 |                 "sku123",
113 |                 "device123",
114 |                 "devices.capabilities.on_off",
115 |                 "powerSwitch",
116 |                 1
117 |             )
118 |             assert success is False
119 |             assert error == "Bad Request"
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/cli.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | import sys
  3 | import argparse
  4 | import asyncio
  5 | from .config import load_config
  6 | from .api import GoveeAPI
  7 | from .exceptions import GoveeError, GoveeValidationError
  8 | 
  9 | def create_parser() -> argparse.ArgumentParser:
 10 |     """Create and configure argument parser."""
 11 |     parser = argparse.ArgumentParser(description='Control Govee LED device')
 12 |     subparsers = parser.add_subparsers(dest='command', help='Commands')
 13 |     
 14 |     # Power command
 15 |     power_parser = subparsers.add_parser('power', help='Turn device on/off')
 16 |     power_parser.add_argument('state', choices=['on', 'off'], help='Power state')
 17 |     
 18 |     # Color command
 19 |     color_parser = subparsers.add_parser('color', help='Set device color')
 20 |     color_parser.add_argument('red', type=int, help='Red value (0-255)')
 21 |     color_parser.add_argument('green', type=int, help='Green value (0-255)')
 22 |     color_parser.add_argument('blue', type=int, help='Blue value (0-255)')
 23 |     
 24 |     # Brightness command
 25 |     brightness_parser = subparsers.add_parser('brightness', help='Set device brightness')
 26 |     brightness_parser.add_argument('level', type=int, help='Brightness level (0-100)')
 27 |     
 28 |     # Status command
 29 |     subparsers.add_parser('status', help='Show device status')
 30 |     
 31 |     return parser
 32 | 
 33 | async def handle_power(api: GoveeAPI, state: str) -> None:
 34 |     """Handle power command."""
 35 |     success, message = await api.set_power(state == 'on')
 36 |     if not success:
 37 |         raise GoveeError(message)
 38 |     print(message)
 39 | 
 40 | async def handle_color(api: GoveeAPI, red: int, green: int, blue: int) -> None:
 41 |     """Handle color command."""
 42 |     try:
 43 |         success, message = await api.set_color(red, green, blue)
 44 |         if not success:
 45 |             raise GoveeError(message)
 46 |         print(message)
 47 |     except GoveeValidationError as e:
 48 |         print(f"Error: {e}")
 49 |         sys.exit(1)
 50 | 
 51 | async def handle_brightness(api: GoveeAPI, level: int) -> None:
 52 |     """Handle brightness command."""
 53 |     if not 0 <= level <= 100:
 54 |         print("Error: Brightness level must be between 0 and 100")
 55 |         sys.exit(1)
 56 |         
 57 |     success, message = await api.set_brightness(level)
 58 |     if not success:
 59 |         raise GoveeError(message)
 60 |     print(message)
 61 | 
 62 | async def handle_status(api: GoveeAPI) -> None:
 63 |     """Handle status command."""
 64 |     # Get power state
 65 |     power_state, power_msg = await api.get_power_state()
 66 |     print(f"Power: {'ON' if power_state else 'OFF'}")
 67 |     
 68 |     # Get color
 69 |     color, color_msg = await api.get_color()
 70 |     print(f"Color: RGB({color[0]}, {color[1]}, {color[2]})")
 71 |     
 72 |     # Get brightness
 73 |     brightness, bright_msg = await api.get_brightness()
 74 |     print(f"Brightness: {brightness}%")
 75 | 
 76 | async def main() -> None:
 77 |     """Main CLI entrypoint."""
 78 |     try:
 79 |         # Load configuration
 80 |         config = load_config()
 81 |         api = GoveeAPI(config)
 82 |         
 83 |         # Parse arguments
 84 |         parser = create_parser()
 85 |         args = parser.parse_args()
 86 |         
 87 |         if args.command == 'power':
 88 |             await handle_power(api, args.state)
 89 |         elif args.command == 'color':
 90 |             await handle_color(api, args.red, args.green, args.blue)
 91 |         elif args.command == 'brightness':
 92 |             await handle_brightness(api, args.level)
 93 |         elif args.command == 'status':
 94 |             await handle_status(api)
 95 |         else:
 96 |             parser.print_help()
 97 |             sys.exit(1)
 98 |             
 99 |     except GoveeError as e:
100 |         print(f"Error: {str(e)}")
101 |         sys.exit(1)
102 |     except Exception as e:
103 |         print(f"Unexpected error: {str(e)}")
104 |         sys.exit(1)
105 |     finally:
106 |         # Always close the API session
107 |         if 'api' in locals():
108 |             await api.close()
109 | 
110 | def cli_main():
111 |     """CLI entry point that handles running the async main."""
112 |     try:
113 |         asyncio.run(main())
114 |     except KeyboardInterrupt:
115 |         print("\nOperation cancelled by user")
116 |         sys.exit(1)
117 | 
118 | if __name__ == "__main__":
119 |     cli_main()
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/server.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | import sys
  3 | from mcp.server.fastmcp import FastMCP
  4 | from govee_mcp_server.config import load_config
  5 | from govee_mcp_server.api import GoveeAPI
  6 | from govee_mcp_server.exceptions import GoveeError
  7 | 
  8 | # Initialize FastMCP server with WARNING log level
  9 | mcp = FastMCP(
 10 |     "govee",
 11 |     capabilities={
 12 |         "server_info": {
 13 |             "name": "govee-mcp",
 14 |             "version": "0.1.0",
 15 |             "description": "MCP server for controlling Govee LED devices"
 16 |         }
 17 |     },
 18 |     log_level='WARNING'
 19 | )
 20 | 
 21 | print("Loading configuration...", file=sys.stderr, flush=True)
 22 | try:
 23 |     config = load_config()
 24 | except GoveeError as e:
 25 |     print(f"Configuration error: {e}", file=sys.stderr)
 26 |     sys.exit(1)
 27 | 
 28 | print("Setting up tools...", file=sys.stderr, flush=True)
 29 | 
 30 | @mcp.tool("turn_on_off")
 31 | async def turn_on_off(power: bool) -> str:
 32 |     """
 33 |     Turn the LED on or off.
 34 |     
 35 |     Args:
 36 |         power: True for on, False for off
 37 |     """
 38 |     api = GoveeAPI(config)
 39 |     try:
 40 |         success, message = await api.set_power(power)
 41 |         await api.close()  # Clean up the session
 42 |         return message if success else f"Failed: {message}"
 43 |     except GoveeError as e:
 44 |         await api.close()
 45 |         return f"Error: {str(e)}"
 46 |     except Exception as e:
 47 |         await api.close()
 48 |         return f"Unexpected error: {str(e)}"
 49 | 
 50 | @mcp.tool("set_color")
 51 | async def set_color(red: int, green: int, blue: int) -> str:
 52 |     """
 53 |     Set the LED color using RGB values.
 54 |     
 55 |     Args:
 56 |         red: Red value (0-255)
 57 |         green: Green value (0-255)
 58 |         blue: Blue value (0-255)
 59 |     """
 60 |     api = GoveeAPI(config)
 61 |     try:
 62 |         success, message = await api.set_color(red, green, blue)
 63 |         await api.close()
 64 |         return message if success else f"Failed: {message}"
 65 |     except GoveeError as e:
 66 |         await api.close()
 67 |         return f"Error: {str(e)}"
 68 |     except Exception as e:
 69 |         await api.close()
 70 |         return f"Unexpected error: {str(e)}"
 71 | 
 72 | @mcp.tool("set_brightness")
 73 | async def set_brightness(brightness: int) -> str:
 74 |     """
 75 |     Set the LED brightness.
 76 |     
 77 |     Args:
 78 |         brightness: Brightness level (0-100)
 79 |     """
 80 |     api = GoveeAPI(config)
 81 |     try:
 82 |         success, message = await api.set_brightness(brightness)
 83 |         await api.close()
 84 |         return message if success else f"Failed: {message}"
 85 |     except GoveeError as e:
 86 |         await api.close()
 87 |         return f"Error: {str(e)}"
 88 |     except Exception as e:
 89 |         await api.close()
 90 |         return f"Unexpected error: {str(e)}"
 91 | 
 92 | @mcp.tool("get_status")
 93 | async def get_status() -> dict:
 94 |     """Get the current status of the LED device."""
 95 |     api = GoveeAPI(config)
 96 |     try:
 97 |         power_state, power_msg = await api.get_power_state()
 98 |         color, color_msg = await api.get_color()
 99 |         brightness, bright_msg = await api.get_brightness()
100 |         await api.close()
101 |         
102 |         return {
103 |             "power": {
104 |                 "state": "on" if power_state else "off",
105 |                 "message": power_msg
106 |             },
107 |             "color": {
108 |                 "r": color[0],
109 |                 "g": color[1],
110 |                 "b": color[2],
111 |                 "message": color_msg
112 |             },
113 |             "brightness": {
114 |                 "level": brightness,
115 |                 "message": bright_msg
116 |             }
117 |         }
118 |     except GoveeError as e:
119 |         await api.close()
120 |         return {"error": str(e)}
121 |     except Exception as e:
122 |         await api.close()
123 |         return {"error": f"Unexpected error: {str(e)}"}
124 | 
125 | async def handle_initialize(params):
126 |     """Handle initialize request"""
127 |     return {
128 |         "protocolVersion": "0.1.0",
129 |         "capabilities": mcp.capabilities
130 |     }
131 | 
132 | mcp.on_initialize = handle_initialize
133 | 
134 | if __name__ == "__main__":
135 |     try:
136 |         import asyncio
137 |         asyncio.run(mcp.run(transport='stdio'))
138 |     except KeyboardInterrupt:
139 |         print("\nServer stopped by user", file=sys.stderr)
140 |     except Exception as e:
141 |         print(f"Server error: {e}", file=sys.stderr)
142 |         sys.exit(1)
```

--------------------------------------------------------------------------------
/src/govee_mcp_server/api.py:
--------------------------------------------------------------------------------

```python
  1 | import aiohttp
  2 | from typing import Optional, Dict, Any, Tuple
  3 | import asyncio
  4 | from time import time
  5 | from .exceptions import (
  6 |     GoveeError,
  7 |     GoveeAPIError,
  8 |     GoveeConnectionError,
  9 |     GoveeTimeoutError
 10 | )
 11 | from .interfaces import PowerControl, ColorControl, BrightnessControl
 12 | from .transformers import ColorTransformer
 13 | from .config import GoveeConfig
 14 | 
 15 | class GoveeAPI(PowerControl, ColorControl, BrightnessControl):
 16 |     """
 17 |     Govee API client implementing device control interfaces.
 18 |     
 19 |     Includes connection pooling, request timeouts, and retries.
 20 |     """
 21 |     
 22 |     BASE_URL = "https://openapi.api.govee.com"
 23 |     MAX_RETRIES = 3
 24 |     RETRY_DELAY = 1  # seconds
 25 |     REQUEST_TIMEOUT = 10  # seconds
 26 | 
 27 |     def __init__(self, config: GoveeConfig):
 28 |         """
 29 |         Initialize API client with configuration.
 30 |         
 31 |         Args:
 32 |             config: GoveeConfig instance with API credentials
 33 |         """
 34 |         self.config = config
 35 |         self.session: Optional[aiohttp.ClientSession] = None
 36 |         self._transformer = ColorTransformer()
 37 |         
 38 |     async def _ensure_session(self) -> None:
 39 |         """Ensure aiohttp session exists or create a new one."""
 40 |         if self.session is None or self.session.closed:
 41 |             self.session = aiohttp.ClientSession(
 42 |                 headers={
 43 |                     "Govee-API-Key": self.config.api_key,
 44 |                     "Content-Type": "application/json"
 45 |                 },
 46 |                 timeout=aiohttp.ClientTimeout(total=self.REQUEST_TIMEOUT)
 47 |             )
 48 | 
 49 |     async def close(self) -> None:
 50 |         """Close the API session."""
 51 |         if self.session and not self.session.closed:
 52 |             await self.session.close()
 53 | 
 54 |     async def _make_request(
 55 |         self,
 56 |         method: str,
 57 |         endpoint: str,
 58 |         **kwargs
 59 |     ) -> Tuple[Dict[str, Any], str]:
 60 |         """
 61 |         Make HTTP request with retries and error handling.
 62 |         
 63 |         Args:
 64 |             method: HTTP method
 65 |             endpoint: API endpoint
 66 |             **kwargs: Additional request arguments
 67 |             
 68 |         Returns:
 69 |             Tuple[Dict[str, Any], str]: API response data and message
 70 |             
 71 |         Raises:
 72 |             GoveeAPIError: On API errors
 73 |             GoveeConnectionError: On connection issues
 74 |             GoveeTimeoutError: On request timeout
 75 |         """
 76 |         await self._ensure_session()
 77 |         
 78 |         for attempt in range(self.MAX_RETRIES):
 79 |             try:
 80 |                 async with self.session.request(
 81 |                     method,
 82 |                     f"{self.BASE_URL}/{endpoint}",
 83 |                     **kwargs
 84 |                 ) as response:
 85 |                     data = await response.json()
 86 |                     
 87 |                     if response.status != 200:
 88 |                         raise GoveeAPIError(
 89 |                             f"API error: {response.status} - {data.get('message', 'Unknown error')}"
 90 |                         )
 91 |                     
 92 |                     return data, data.get('message', 'Success')
 93 |                     
 94 |             except asyncio.TimeoutError:
 95 |                 if attempt == self.MAX_RETRIES - 1:
 96 |                     raise GoveeTimeoutError(f"Request timed out after {self.REQUEST_TIMEOUT}s")
 97 |             except aiohttp.ClientError as e:
 98 |                 if attempt == self.MAX_RETRIES - 1:
 99 |                     raise GoveeConnectionError(f"Connection error: {str(e)}")
100 |             
101 |             await asyncio.sleep(self.RETRY_DELAY * (attempt + 1))
102 |         
103 |         raise GoveeAPIError("Max retries exceeded")
104 | 
105 |     async def set_power(self, state: bool) -> Tuple[bool, str]:
106 |         """Implement PowerControl.set_power"""
107 |         try:
108 |             _, message = await self._make_request(
109 |                 "POST",
110 |                 "router/api/v1/device/control",
111 |                 json={
112 |                     "requestId": str(int(time())),  # Using timestamp as requestId
113 |                     "payload": {
114 |                         "sku": self.config.sku,
115 |                         "device": self.config.device_id,
116 |                         "capability": {
117 |                             "type": "devices.capabilities.on_off",
118 |                             "instance": "powerSwitch",
119 |                             "value": 1 if state else 0
120 |                         }
121 |                     }
122 |                 }
123 |             )
124 |             return True, message
125 |         except GoveeError as e:
126 |             return False, str(e)
127 | 
128 |     async def get_power_state(self) -> Tuple[bool, str]:
129 |         """Implement PowerControl.get_power_state"""
130 |         try:
131 |             data, message = await self._make_request(
132 |                 "GET",
133 |                 f"devices/state",
134 |                 params={
135 |                     "device": self.config.device_id,
136 |                     "model": self.config.sku
137 |                 }
138 |             )
139 |             return data.get('powerState') == 'on', message
140 |         except GoveeError as e:
141 |             return False, str(e)
142 | 
143 |     async def set_color(self, r: int, g: int, b: int) -> Tuple[bool, str]:
144 |         """Implement ColorControl.set_color"""
145 |         try:
146 |             color_value = ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)
147 |             
148 |             _, message = await self._make_request(
149 |                 "POST",
150 |                 "router/api/v1/device/control",
151 |                 json={
152 |                     "requestId": str(int(time())),
153 |                     "payload": {
154 |                         "sku": self.config.sku,
155 |                         "device": self.config.device_id,
156 |                         "capability": {
157 |                             "type": "devices.capabilities.color_setting",
158 |                             "instance": "colorRgb",
159 |                             "value": color_value
160 |                         }
161 |                     }
162 |                 }
163 |             )
164 |             return True, message
165 |         except GoveeError as e:
166 |             return False, str(e)
167 | 
168 |     async def get_color(self) -> Tuple[Tuple[int, int, int], str]:
169 |         """Implement ColorControl.get_color"""
170 |         try:
171 |             data, message = await self._make_request(
172 |                 "GET",
173 |                 f"devices/state",
174 |                 params={
175 |                     "device": self.config.device_id,
176 |                     "model": self.config.sku
177 |                 }
178 |             )
179 |             color = data.get('color', {})
180 |             return (
181 |                 color.get('r', 0),
182 |                 color.get('g', 0),
183 |                 color.get('b', 0)
184 |             ), message
185 |         except GoveeError as e:
186 |             return (0, 0, 0), str(e)
187 | 
188 |     async def set_brightness(self, level: int) -> Tuple[bool, str]:
189 |         """Implement BrightnessControl.set_brightness"""
190 |         if not 0 <= level <= 100:
191 |             return False, "Brightness must be between 0-100"
192 |             
193 |         try:
194 |             _, message = await self._make_request(
195 |                 "POST",
196 |                 "router/api/v1/device/control",
197 |                 json={
198 |                     "requestId": str(int(time())),
199 |                     "payload": {
200 |                         "sku": self.config.sku,
201 |                         "device": self.config.device_id,
202 |                         "capability": {
203 |                             "type": "devices.capabilities.range",
204 |                             "instance": "brightness",
205 |                             "value": level
206 |                         }
207 |                     }
208 |                 }
209 |             )
210 |             return True, message
211 |         except GoveeError as e:
212 |             return False, str(e)
213 | 
214 |     async def get_brightness(self) -> Tuple[int, str]:
215 |         """Implement BrightnessControl.get_brightness"""
216 |         try:
217 |             data, message = await self._make_request(
218 |                 "GET",
219 |                 f"devices/state",
220 |                 params={
221 |                     "device": self.config.device_id,
222 |                     "model": self.config.sku
223 |                 }
224 |             )
225 |             return data.get('brightness', 0), message
226 |         except GoveeError as e:
227 |             return 0, str(e)
```

--------------------------------------------------------------------------------
/documentation/01_configuration_management.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chapter 1: Configuration Management - Giving Your App Its Credentials
  2 | 
  3 | Welcome to the `govee_mcp_server` tutorial! We're excited to guide you through how this project works. Let's start with the very first step any application needs: figuring out its basic settings.
  4 | 
  5 | ## What's the Big Idea?
  6 | 
  7 | Imagine you want to send a letter. You need a few key things:
  8 | 1.  The **address** of the recipient (who are you sending it to?).
  9 | 2.  A **stamp** (proof you're allowed to send mail).
 10 | 3.  Maybe the **type of envelope** needed for that specific address.
 11 | 
 12 | Our `govee_mcp_server` application is similar. To talk to your Govee lights, it needs to know:
 13 | 1.  **Which specific device** it should control (like the address).
 14 | 2.  Your **Govee API Key** (like a secret password or stamp proving it's allowed to talk to Govee).
 15 | 3.  The **device model (SKU)** (like knowing the type of envelope needed).
 16 | 
 17 | "Configuration Management" is just a fancy term for how the application finds and manages these essential pieces of information. It's like the application's **ID card and key storage** – it holds the credentials and details needed to operate.
 18 | 
 19 | ## Storing Secrets: Environment Variables and `.env`
 20 | 
 21 | We need a place to store sensitive information like your Govee API Key. Putting it directly into the code isn't safe or flexible. Instead, we use **environment variables**.
 22 | 
 23 | Think of environment variables as settings that live *outside* the application code, within the computer's operating system environment. This keeps secrets separate from the main logic.
 24 | 
 25 | For convenience, especially during development, we often store these environment variables in a special file named `.env` located in the project's main folder.
 26 | 
 27 | **Example `.env` file:**
 28 | 
 29 | ```bash
 30 | # This is a comment - lines starting with # are ignored
 31 | GOVEE_API_KEY=abcdef12-3456-7890-abcd-ef1234567890
 32 | GOVEE_DEVICE_ID=AB:CD:EF:12:34:56:78:90
 33 | GOVEE_SKU=H6159
 34 | ```
 35 | 
 36 | *   `GOVEE_API_KEY`: Your personal key from Govee. Keep this secret!
 37 | *   `GOVEE_DEVICE_ID`: The unique identifier for your specific Govee light.
 38 | *   `GOVEE_SKU`: The model number of your Govee light.
 39 | 
 40 | **Analogy:** Think of the `.env` file like a secure sticky note next to your computer with login details, rather than writing them directly in your public diary (the code).
 41 | 
 42 | ## Loading the Settings: The `load_config` Function
 43 | 
 44 | Okay, so the details are in the `.env` file (or set directly as environment variables). How does our application actually *read* them? It uses a helper function called `load_config`.
 45 | 
 46 | Let's look at the heart of this process in `src/govee_mcp_server/config.py`:
 47 | 
 48 | ```python
 49 | # Simplified snippet from src/govee_mcp_server/config.py
 50 | 
 51 | import os
 52 | from dotenv import load_dotenv
 53 | from pathlib import Path
 54 | 
 55 | # ... (GoveeConfig class defined elsewhere) ...
 56 | # ... (GoveeConfigError class defined elsewhere) ...
 57 | 
 58 | def load_config():
 59 |     # Find and load the .env file
 60 |     env_path = Path(__file__).resolve().parent.parent.parent / '.env'
 61 |     load_dotenv(dotenv_path=env_path) # Reads .env into environment
 62 | 
 63 |     # Read values from the environment
 64 |     api_key = os.getenv('GOVEE_API_KEY')
 65 |     device_id = os.getenv('GOVEE_DEVICE_ID')
 66 |     sku = os.getenv('GOVEE_SKU')
 67 | 
 68 |     # Check if any are missing
 69 |     if not api_key or not device_id or not sku:
 70 |         # If something is missing, raise an error!
 71 |         raise GoveeConfigError("Missing required environment variables!")
 72 | 
 73 |     # If all good, package them up and return
 74 |     return GoveeConfig(api_key=api_key, device_id=device_id, sku=sku)
 75 | ```
 76 | 
 77 | **Explanation:**
 78 | 1.  `load_dotenv(dotenv_path=env_path)`: This line uses a library (`python-dotenv`) to read your `.env` file and load its contents into the environment variables for the current run.
 79 | 2.  `os.getenv(...)`: This standard Python function reads the value of an environment variable. It checks for `GOVEE_API_KEY`, `GOVEE_DEVICE_ID`, and `GOVEE_SKU`.
 80 | 3.  **Validation:** The code checks if it successfully found all three required values. If any are missing (`None`), it raises a `GoveeConfigError` to stop the application with a helpful message. This prevents weird errors later on.
 81 | 4.  `return GoveeConfig(...)`: If all values are found, they are bundled neatly into a `GoveeConfig` object (we'll see this next) and returned.
 82 | 
 83 | ## A Tidy Package: The `GoveeConfig` Object
 84 | 
 85 | Reading variables one by one is okay, but it's much cleaner to pass them around bundled together. That's where the `GoveeConfig` object comes in. It's defined using a Python feature called `dataclass`.
 86 | 
 87 | ```python
 88 | # Simplified snippet from src/govee_mcp_server/config.py
 89 | from dataclasses import dataclass
 90 | 
 91 | @dataclass
 92 | class GoveeConfig:
 93 |     """Configuration class for Govee API settings."""
 94 |     api_key: str
 95 |     device_id: str
 96 |     sku: str
 97 | ```
 98 | 
 99 | **Explanation:**
100 | *   `@dataclass`: This is a shortcut in Python to create simple classes that mostly just hold data.
101 | *   `api_key: str`, `device_id: str`, `sku: str`: This defines the "slots" within our `GoveeConfig` object. It expects to hold three pieces of text (strings): the API key, device ID, and SKU.
102 | 
103 | **Analogy:** The `GoveeConfig` object is like a small, labelled box specifically designed to hold the application's ID card (`api_key`), address label (`device_id`), and envelope type (`sku`). The `load_config` function fills this box.
104 | 
105 | ## Using the Configuration
106 | 
107 | Once `load_config()` creates the `GoveeConfig` object, it's passed to other parts of the application that need this information. The most important user is the [Govee API Client](03_govee_api_client.md), which handles the actual communication with Govee.
108 | 
109 | Here's a simplified idea of how it's used in `src/govee_mcp_server/api.py`:
110 | 
111 | ```python
112 | # Simplified concept from src/govee_mcp_server/api.py
113 | 
114 | from .config import GoveeConfig # Import the config class
115 | 
116 | class GoveeAPI:
117 |     # The __init__ method runs when a GoveeAPI object is created
118 |     def __init__(self, config: GoveeConfig):
119 |         """Initialize API client with configuration."""
120 |         self.config = config # Store the passed-in config object
121 |         # Now, self.config.api_key, self.config.device_id,
122 |         # and self.config.sku are available inside this class.
123 | 
124 |     async def _make_request(self, ...):
125 |         # When making a real request to Govee...
126 |         api_key = self.config.api_key # <-- Access the stored key
127 |         headers = {"Govee-API-Key": api_key, ...}
128 |         # ... use the key in the request headers ...
129 | 
130 |     async def set_power(self, state: bool):
131 |         # When controlling the device...
132 |         device = self.config.device_id # <-- Access the stored ID
133 |         sku = self.config.sku         # <-- Access the stored SKU
134 |         payload = {
135 |             "sku": sku,
136 |             "device": device,
137 |             # ... other control details ...
138 |         }
139 |         # ... use the ID and SKU in the request body ...
140 | ```
141 | 
142 | **Explanation:**
143 | *   The `GoveeAPI` class takes the `GoveeConfig` object when it's created.
144 | *   It stores this `config` object internally.
145 | *   Whenever it needs the API key, device ID, or SKU to talk to the Govee servers, it simply accesses them from its stored `self.config` object (e.g., `self.config.api_key`).
146 | 
147 | ## Under the Hood: How Loading Works
148 | 
149 | Let's trace the steps when the application starts and needs its configuration:
150 | 
151 | ```mermaid
152 | sequenceDiagram
153 |     participant App as Application (CLI/Server)
154 |     participant LC as load_config()
155 |     participant DotEnv as .env File
156 |     participant OS as Operating System Env
157 |     participant GC as GoveeConfig Object
158 | 
159 |     App->>LC: Start! Need config. Call load_config()
160 |     LC->>DotEnv: Look for .env file in project root
161 |     Note over DotEnv: Contains GOVEE_API_KEY=... etc.
162 |     DotEnv-->>LC: Load variables found into OS Env
163 |     LC->>OS: Read 'GOVEE_API_KEY' variable
164 |     OS-->>LC: Return 'abcdef...' (value)
165 |     LC->>OS: Read 'GOVEE_DEVICE_ID' variable
166 |     OS-->>LC: Return 'AB:CD:...' (value)
167 |     LC->>OS: Read 'GOVEE_SKU' variable
168 |     OS-->>LC: Return 'H6159' (value)
169 |     LC->>LC: Check: Are all values present? (Yes)
170 |     LC->>GC: Create GoveeConfig object with these values
171 |     GC-->>LC: Here's the new object
172 |     LC-->>App: Return the filled GoveeConfig object
173 | ```
174 | 
175 | This sequence shows how the application reliably gets its necessary startup information from the environment, validates it, and packages it for easy use.
176 | 
177 | ## Conclusion
178 | 
179 | You've learned about the first crucial step: **Configuration Management**. You now know:
180 | 
181 | *   Why applications need configuration (like API keys and device IDs).
182 | *   How `govee_mcp_server` uses environment variables and `.env` files to store these settings securely.
183 | *   How the `load_config` function reads and validates these settings.
184 | *   How the `GoveeConfig` object provides a tidy way to access these settings throughout the application.
185 | 
186 | With the configuration loaded, the application knows *which* device to talk to and *how* to authenticate. But what actions *can* it perform on that device? That leads us to our next topic: defining the capabilities of our device.
187 | 
188 | Let's move on to [Chapter 2: Device Control Interfaces](02_device_control_interfaces.md) to see how we define actions like turning the light on/off or changing its color.
189 | 
190 | ---
191 | 
192 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```

--------------------------------------------------------------------------------
/documentation/02_device_control_interfaces.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chapter 2: Device Control Interfaces - The Standard Sockets for Lights
  2 | 
  3 | In [Chapter 1: Configuration Management](01_configuration_management.md), we learned how our application gets its essential settings, like which Govee device to talk to (`device_id`) and the secret key (`api_key`) needed to communicate. Think of it like knowing the address and having the key to a specific smart light bulb.
  4 | 
  5 | But just knowing *which* bulb doesn't tell us *what we can do* with it. Can we turn it on? Can we change its color? Can we dim it? This chapter is about defining a standard way to talk about these capabilities.
  6 | 
  7 | ## What's the Big Idea? Standardizing Actions
  8 | 
  9 | Imagine you have different smart devices: a Govee light strip, maybe a smart plug from another brand, or even a simulated light for testing. You want a consistent way to tell them "turn on" or "set color to blue". You don't want to learn a completely different set of commands for each one!
 10 | 
 11 | This is where **Device Control Interfaces** come in. They act like **standard electrical sockets** or **blueprints** for device abilities.
 12 | 
 13 | *   **Power Socket:** Defines *how* to plug something in to get power (e.g., Turn On/Off).
 14 | *   **Color Socket:** Defines *how* to tell a device which color to display.
 15 | *   **Brightness Socket:** Defines *how* to adjust the brightness level.
 16 | 
 17 | An "interface" in programming is a **contract**. It says: "If you claim to have power control, you *must* provide a way to `set_power` and `get_power_state` exactly like this." Any part of our code that knows how to control a Govee device (like the `GoveeAPI` client we'll see more of soon) has to promise to follow these contracts.
 18 | 
 19 | ## The Blueprints: `PowerControl`, `ColorControl`, `BrightnessControl`
 20 | 
 21 | In our project, these blueprints are defined in `src/govee_mcp_server/interfaces.py` using Python's `abc` (Abstract Base Classes) module. Let's look at a simplified version of `PowerControl`:
 22 | 
 23 | ```python
 24 | # Simplified from src/govee_mcp_server/interfaces.py
 25 | from abc import ABC, abstractmethod
 26 | from typing import Tuple # Used to describe the function's output type
 27 | 
 28 | class PowerControl(ABC): # ABC means "This is a blueprint/interface"
 29 |     """Blueprint for turning things on/off."""
 30 | 
 31 |     @abstractmethod # Means: Any class using this blueprint MUST provide this method
 32 |     async def set_power(self, state: bool) -> Tuple[bool, str]:
 33 |         """
 34 |         Blueprint for a function to turn the device on (True) or off (False).
 35 | 
 36 |         It MUST take 'state' (True/False) as input.
 37 |         It MUST return a pair: (was it successful?, a message).
 38 |         """
 39 |         pass # The blueprint only defines *what*, not *how*. Implementations fill this.
 40 | 
 41 |     @abstractmethod
 42 |     async def get_power_state(self) -> Tuple[bool, str]:
 43 |         """
 44 |         Blueprint for checking if the device is currently on.
 45 | 
 46 |         It MUST return a pair: (is it on?, a message).
 47 |         """
 48 |         pass
 49 | ```
 50 | 
 51 | **Explanation:**
 52 | *   `class PowerControl(ABC):`: Defines a blueprint named `PowerControl`. `ABC` marks it as an abstract blueprint, not a real, usable object on its own.
 53 | *   `@abstractmethod`: This marker says "Any real class that claims to be a `PowerControl` *must* provide its own version of the function below."
 54 | *   `async def set_power(...)`: Defines the *signature* of the `set_power` function:
 55 |     *   `async`: It's designed to work asynchronously (we'll see why in later chapters).
 56 |     *   `self`: A standard reference to the object itself.
 57 |     *   `state: bool`: It must accept one input argument named `state`, which must be a boolean (`True` or `False`).
 58 |     *   `-> Tuple[bool, str]`: It must return a "tuple" (an ordered pair) containing a boolean (success/failure) and a string (a status message).
 59 | *   `pass`: In the blueprint, the methods don't actually *do* anything. They just define the requirements.
 60 | 
 61 | Similarly, we have `ColorControl` (requiring `set_color`, `get_color`) and `BrightnessControl` (requiring `set_brightness`, `get_brightness`).
 62 | 
 63 | ## Using the Blueprints: The `GoveeAPI` Class
 64 | 
 65 | Okay, we have blueprints. Now we need something concrete that *follows* these blueprints. In our project, the main class responsible for talking to the actual Govee Cloud service is `GoveeAPI` (found in `src/govee_mcp_server/api.py`).
 66 | 
 67 | Look at the very first line defining this class:
 68 | 
 69 | ```python
 70 | # Simplified from src/govee_mcp_server/api.py
 71 | from .interfaces import PowerControl, ColorControl, BrightnessControl
 72 | # ... other imports ...
 73 | from .config import GoveeConfig
 74 | 
 75 | class GoveeAPI(PowerControl, ColorControl, BrightnessControl):
 76 |     """
 77 |     Govee API client that PROMISES to follow the rules
 78 |     of PowerControl, ColorControl, and BrightnessControl.
 79 |     """
 80 |     def __init__(self, config: GoveeConfig):
 81 |         """Gets the API key and device details when created."""
 82 |         self.config = config
 83 |         # ... other setup ...
 84 | 
 85 |     # --- Implementing PowerControl ---
 86 |     async def set_power(self, state: bool) -> Tuple[bool, str]:
 87 |         """Turns the actual Govee device on/off using its API."""
 88 |         print(f"Actually telling Govee API to set power: {state}")
 89 |         # ... code to make the real web request to Govee ...
 90 |         # (We'll look inside this in Chapter 3!)
 91 |         success = True # Let's assume it worked for now
 92 |         message = f"Device power set to {state} via API"
 93 |         return success, message
 94 | 
 95 |     async def get_power_state(self) -> Tuple[bool, str]:
 96 |         """Asks the Govee API if the device is on."""
 97 |         print("Actually asking Govee API for power state")
 98 |         # ... code to make the real web request to Govee ...
 99 |         is_on = True # Let's pretend it's on
100 |         message = "Device is currently ON (from API)"
101 |         return is_on, message
102 | 
103 |     # --- Implementing ColorControl (methods like set_color) ---
104 |     # ... implementations for set_color, get_color ...
105 | 
106 |     # --- Implementing BrightnessControl (methods like set_brightness) ---
107 |     # ... implementations for set_brightness, get_brightness ...
108 | 
109 | ```
110 | 
111 | **Explanation:**
112 | *   `class GoveeAPI(PowerControl, ColorControl, BrightnessControl):`: This line is the **promise**. It declares that the `GoveeAPI` class will provide *concrete implementations* for all the `@abstractmethod` functions defined in the `PowerControl`, `ColorControl`, and `BrightnessControl` blueprints.
113 | *   If `GoveeAPI` *forgot* to include, say, the `set_power` method, Python would give an error when you try to use it, because it broke its promise!
114 | *   Inside `GoveeAPI`, the `set_power` method now has real code (represented by the `print` statement and comments for now) that actually interacts with the Govee web service. Crucially, its signature (`async def set_power(self, state: bool) -> Tuple[bool, str]`) exactly matches the blueprint.
115 | 
116 | **Analogy:** If `PowerControl` is the blueprint for a standard wall socket, `GoveeAPI` is like a specific brand of smart plug that is built according to that blueprint and actually connects to the house wiring (the Govee Cloud API) when used.
117 | 
118 | ## Benefits: Why Is This Structure Useful?
119 | 
120 | 1.  **Consistency:** Any code that needs to turn a light on/off can expect *any* object that follows the `PowerControl` blueprint to have a `.set_power(state)` method. It doesn't need to know if it's talking to a `GoveeAPI` object, a `GoveeBluetooth` object (if we added one later), or even a `FakeLightForTesting` object, as long as they all follow the blueprint.
121 | 2.  **Flexibility (Swappability):** Imagine we want to add control via Bluetooth later. We could create a `GoveeBluetooth(PowerControl, ...)` class. Code that only cares about power control could use either `GoveeAPI` or `GoveeBluetooth` interchangeably without modification, because both fulfill the `PowerControl` contract.
122 | 3.  **Testability:** It's much easier to write tests. We can create a simple `MockPowerControl` class that just pretends to turn things on/off without actually talking to Govee, and use it to test other parts of our application that rely on power control.
123 | 
124 | ## Safety First: Input Validation with Decorators
125 | 
126 | What if someone tries to set the color to RGB values like `(300, -50, 1000)`? Those aren't valid! Red, Green, and Blue values must be between 0 and 255.
127 | 
128 | Interfaces can also help enforce rules like this. We use a feature called **decorators**. Think of a decorator as a wrapper function that adds extra behavior (like a check) before or after another function runs.
129 | 
130 | In `src/govee_mcp_server/interfaces.py`, we have a `validate_rgb` decorator:
131 | 
132 | ```python
133 | # Simplified from src/govee_mcp_server/interfaces.py
134 | from .exceptions import GoveeValidationError # Our custom error for bad input
135 | 
136 | def validate_rgb(func):
137 |     """A function that wraps another function to check RGB values first."""
138 |     async def wrapper(self, r: int, g: int, b: int, *args, **kwargs):
139 |         print(f"VALIDATOR: Checking RGB ({r}, {g}, {b})") # Show the check
140 |         is_valid = True
141 |         for name, value in [('Red', r), ('Green', g), ('Blue', b)]:
142 |             if not (isinstance(value, int) and 0 <= value <= 255):
143 |                 print(f"VALIDATOR: Invalid value for {name}: {value}")
144 |                 is_valid = False
145 |                 # Stop! Raise an error instead of calling the real function.
146 |                 raise GoveeValidationError(f"{name} value must be an integer 0-255")
147 | 
148 |         if is_valid:
149 |             # If all checks pass, call the original function (like set_color)
150 |             print("VALIDATOR: Looks good! Proceeding.")
151 |             return await func(self, r, g, b, *args, **kwargs)
152 |     return wrapper
153 | ```
154 | 
155 | And we apply this decorator to the `set_color` blueprint in `ColorControl`:
156 | 
157 | ```python
158 | # Simplified from src/govee_mcp_server/interfaces.py
159 | # ... imports ...
160 | 
161 | class ColorControl(ABC):
162 |     @abstractmethod
163 |     @validate_rgb # <-- Apply the validator HERE
164 |     async def set_color(self, r: int, g: int, b: int) -> Tuple[bool, str]:
165 |         """
166 |         Set device color. The @validate_rgb ensures values are checked
167 |         BEFORE the actual implementation in GoveeAPI is even called.
168 |         """
169 |         pass
170 | 
171 |     # ... get_color method ...
172 | ```
173 | 
174 | **Explanation:**
175 | *   `@validate_rgb`: This line attaches the `validate_rgb` wrapper to the `set_color` method definition in the blueprint.
176 | *   Now, whenever *any* implementation of `set_color` (like the one in `GoveeAPI`) is called, the `validate_rgb` code runs *first*.
177 | *   If the RGB values are invalid (e.g., `r=300`), `validate_rgb` raises a `GoveeValidationError` immediately, preventing the invalid values from ever reaching the code that talks to the Govee API.
178 | 
179 | **Analogy:** The `@validate_rgb` decorator is like a safety fuse built into the "Color Socket" blueprint. It checks the "electrical current" (the RGB values) before letting it flow to the actual device, preventing damage or errors.
180 | 
181 | ## How it Works Under the Hood
182 | 
183 | Let's trace what happens when some user code tries to turn the light on using our `GoveeAPI` object, which implements the `PowerControl` interface:
184 | 
185 | ```mermaid
186 | sequenceDiagram
187 |     participant UserCode as Your Code
188 |     participant GoveeObj as GoveeAPI Object (implements PowerControl)
189 |     participant PCInterface as PowerControl Blueprint
190 |     participant GoveeCloud as Govee Cloud Service
191 | 
192 |     UserCode->>GoveeObj: Call `set_power(True)`
193 |     Note over GoveeObj,PCInterface: GoveeAPI promised to have `set_power` because it implements PowerControl.
194 |     GoveeObj->>GoveeObj: Execute its own `set_power` code.
195 |     GoveeObj->>GoveeCloud: Send command: { device: '...', sku: '...', capability: 'power', value: 1 }
196 |     GoveeCloud-->>GoveeObj: Respond: { status: 200, message: 'Success' }
197 |     GoveeObj->>GoveeObj: Process the response.
198 |     GoveeObj-->>UserCode: Return `(True, "Device power set to True via API")`
199 | ```
200 | 
201 | If we called `set_color` instead, the flow would be similar, but the `@validate_rgb` check would happen right after `UserCode` calls `set_color` on `GoveeObj`, before the object tries to talk to the `GoveeCloud`.
202 | 
203 | ## Conclusion
204 | 
205 | You've now learned about **Device Control Interfaces**, the crucial blueprints that define *what* actions can be performed on a device in a standard way.
206 | 
207 | *   Interfaces (`PowerControl`, `ColorControl`, etc.) act as **contracts** or **standard sockets**.
208 | *   They use `ABC` and `@abstractmethod` to define required functions (like `set_power`, `get_color`).
209 | *   Classes like `GoveeAPI` **implement** these interfaces, promising to provide the required functions.
210 | *   This brings **consistency**, **flexibility**, and **testability** to our code.
211 | *   **Decorators** like `@validate_rgb` can be attached to interface methods to add checks (like input validation) automatically.
212 | 
213 | We have the configuration (Chapter 1) and the blueprints for control (Chapter 2). Now, let's dive into the specific class that follows these blueprints and actually communicates with the Govee servers.
214 | 
215 | Get ready to explore the engine room in [Chapter 3: Govee API Client](03_govee_api_client.md)!
216 | 
217 | ---
218 | 
219 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```

--------------------------------------------------------------------------------
/documentation/05_mcp_server_implementation.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chapter 5: MCP Server Implementation - The Universal Remote Receiver
  2 | 
  3 | In [Chapter 4: Command Line Interface (CLI)](04_command_line_interface__cli_.md), we created a simple way for *you* to control your Govee light by typing commands directly into your terminal. It's like having a basic remote with just a few buttons – useful, but limited.
  4 | 
  5 | What if you want something more powerful? What if you want *other programs*, like an AI assistant or a smart home dashboard, to be able to control the light? They can't easily "type" commands into your terminal. They need a standardized way to send instructions over a network or through standard program communication channels.
  6 | 
  7 | This is where the **MCP Server** comes in. It's the core of our `govee_mcp_server` project!
  8 | 
  9 | ## What's the Big Idea? A Standard Interface for Programs
 10 | 
 11 | Imagine you have various smart devices (lights, plugs, maybe a fan). You want your fancy AI assistant to control them all. It would be messy if the AI needed to learn a completely different communication method for each device!
 12 | 
 13 | The **Mission Control Protocol (MCP)** is designed to solve this. It's a standard way for a program (like our AI, called an **MCP Client**) to:
 14 | 1.  Ask another program (our **MCP Server**) what "tools" it offers.
 15 | 2.  Ask the server to use one of those tools with specific instructions.
 16 | 
 17 | Our `govee_mcp_server` acts as an MCP server specifically for your Govee light. It advertises "tools" like `turn_on_off`, `set_color`, `set_brightness`, and `get_status`. When an MCP client calls one of these tools, our server receives the request, uses the [Govee API Client](03_govee_api_client.md) to perform the action on the real light, and sends a standard response back.
 18 | 
 19 | **Analogy:** Think of the MCP server like a **universal remote control receiver** attached to your Govee light. Any device that knows the standard "universal remote language" (MCP) can send signals to this receiver, which then translates them into the specific commands your Govee light understands.
 20 | 
 21 | ## How MCP Works: Tools and Communication
 22 | 
 23 | 1.  **Discovery (Optional but Common):** An MCP client might first ask the server, "What capabilities or tools do you have?" Our server would respond, "I have `turn_on_off` (takes a power state: true/false), `set_color` (takes red, green, blue numbers), `set_brightness` (takes a number 0-100), and `get_status`."
 24 | 2.  **Execution:** The client then sends a structured message like, "Please execute the tool named `set_color` with arguments `red=0`, `green=255`, `blue=0`."
 25 | 3.  **Action:** Our server receives this, finds its `set_color` function, calls it with the provided arguments (0, 255, 0). Inside that function, it uses the `GoveeAPI` client to actually tell the Govee cloud to set the light to green.
 26 | 4.  **Response:** The server sends a structured message back to the client, like, "Okay, I executed `set_color`. The result was: 'Success'."
 27 | 
 28 | MCP communication often happens over standard input/output (stdio) between processes or sometimes over network sockets (like TCP). Our server uses a library called `FastMCP` which handles the details of listening for these requests and sending responses in the correct MCP format.
 29 | 
 30 | ## Implementing the Server (`src/govee_mcp_server/server.py`)
 31 | 
 32 | The main logic for our MCP server lives in the `src/govee_mcp_server/server.py` file. Let's look at the key parts.
 33 | 
 34 | **1. Setting Up the Server**
 35 | 
 36 | First, we import the necessary pieces and create the `FastMCP` server instance.
 37 | 
 38 | ```python
 39 | # Simplified from src/govee_mcp_server/server.py
 40 | import sys
 41 | from mcp.server.fastmcp import FastMCP # The MCP library
 42 | from govee_mcp_server.config import load_config # To get API key etc.
 43 | from govee_mcp_server.api import GoveeAPI # To control the light
 44 | from govee_mcp_server.exceptions import GoveeError # Our custom errors
 45 | 
 46 | # Initialize the MCP server
 47 | mcp = FastMCP(
 48 |     "govee", # A simple name for our server
 49 |     # Define some basic info about the server
 50 |     capabilities={
 51 |         "server_info": { "name": "govee-mcp", "version": "0.1.0" }
 52 |     },
 53 |     log_level='WARNING' # Keep logs tidy
 54 | )
 55 | ```
 56 | 
 57 | **Explanation:**
 58 | *   We import `FastMCP`, our configuration loader, and the `GoveeAPI` client.
 59 | *   `mcp = FastMCP(...)` creates the server object. We give it a name (`"govee"`) and some basic information that clients might ask for.
 60 | 
 61 | **2. Loading Configuration**
 62 | 
 63 | The server needs the API key and device details, just like the CLI did.
 64 | 
 65 | ```python
 66 | # Simplified from src/govee_mcp_server/server.py
 67 | 
 68 | print("Loading configuration...", file=sys.stderr)
 69 | try:
 70 |     # Load API Key, Device ID, SKU from .env or environment
 71 |     config = load_config() # Uses the function from Chapter 1
 72 | except GoveeError as e:
 73 |     print(f"Configuration error: {e}", file=sys.stderr)
 74 |     sys.exit(1) # Stop if config fails
 75 | 
 76 | # 'config' now holds our GoveeConfig object
 77 | ```
 78 | 
 79 | **Explanation:**
 80 | *   This uses the `load_config` function from [Chapter 1: Configuration Management](01_configuration_management.md) to get the necessary credentials.
 81 | *   If loading fails (e.g., missing `.env` file), it prints an error and stops.
 82 | 
 83 | **3. Defining the Tools**
 84 | 
 85 | This is where we define the actions the server can perform. We use the `@mcp.tool()` decorator to register functions as MCP tools.
 86 | 
 87 | ```python
 88 | # Simplified from src/govee_mcp_server/server.py
 89 | 
 90 | print("Setting up tools...", file=sys.stderr)
 91 | 
 92 | # --- Tool: turn_on_off ---
 93 | @mcp.tool("turn_on_off") # Register this function as an MCP tool
 94 | async def turn_on_off(power: bool) -> str:
 95 |     """Turn the LED on (True) or off (False)."""
 96 |     api = GoveeAPI(config) # Create API client *for this request*
 97 |     try:
 98 |         # Use the API client to send the command
 99 |         success, message = await api.set_power(power)
100 |         # Return a simple status message
101 |         return message if success else f"Failed: {message}"
102 |     except GoveeError as e:
103 |         return f"Error: {str(e)}" # Handle expected errors
104 |     finally:
105 |         await api.close() # IMPORTANT: Clean up the connection
106 | 
107 | # --- Tool: set_color ---
108 | @mcp.tool("set_color")
109 | async def set_color(red: int, green: int, blue: int) -> str:
110 |     """Set the LED color using RGB (0-255)."""
111 |     api = GoveeAPI(config)
112 |     try:
113 |         # Use the API client to set the color
114 |         success, message = await api.set_color(red, green, blue)
115 |         return message if success else f"Failed: {message}"
116 |     except GoveeError as e:
117 |         return f"Error: {str(e)}"
118 |     finally:
119 |         await api.close()
120 | 
121 | # --- Other tools (set_brightness, get_status) defined similarly ---
122 | # @mcp.tool("set_brightness")
123 | # async def set_brightness(brightness: int) -> str: ...
124 | # @mcp.tool("get_status")
125 | # async def get_status() -> dict: ...
126 | ```
127 | 
128 | **Explanation:**
129 | *   `@mcp.tool("tool_name")`: This magic line tells the `FastMCP` library: "When a client asks to run the tool named `turn_on_off`, execute the following function (`async def turn_on_off(...)`)."
130 | *   `async def turn_on_off(power: bool) -> str:`: Defines the function that handles the `turn_on_off` tool.
131 |     *   It takes `power` (a boolean: `True` or `False`) as input, just as the client specified.
132 |     *   It's declared `async` because it uses `await` to call the asynchronous `GoveeAPI` methods.
133 |     *   It's defined to return a `str` (a string message indicating success or failure).
134 | *   **Inside the Tool:**
135 |     1.  `api = GoveeAPI(config)`: A *new* instance of the [Govee API Client](03_govee_api_client.md) is created for each tool request, using the loaded configuration.
136 |     2.  `await api.set_power(power)`: This is where the actual work happens! It calls the API client method to communicate with the Govee cloud.
137 |     3.  `return ...`: It returns a simple string message back to the MCP framework.
138 |     4.  `finally: await api.close()`: **Crucially**, after the tool logic is done (whether it succeeded or failed), it closes the network connection associated with the `GoveeAPI` client instance to release resources. This happens in the `finally` block to ensure cleanup.
139 | *   The `set_color` tool follows the same pattern: decorate, define function with correct arguments, create API client, call the relevant API method (`set_color`), handle errors, return a message, and clean up the client.
140 | 
141 | **4. Running the Server**
142 | 
143 | Finally, we need code to actually start the server and make it listen for incoming MCP requests.
144 | 
145 | ```python
146 | # Simplified from src/govee_mcp_server/server.py
147 | import asyncio
148 | 
149 | # ... other code above ...
150 | 
151 | if __name__ == "__main__": # Standard Python way to run code directly
152 |     try:
153 |         print("Starting MCP server...", file=sys.stderr)
154 |         # Tell FastMCP to run, listening via standard input/output
155 |         asyncio.run(mcp.run(transport='stdio'))
156 |     except KeyboardInterrupt:
157 |         # Allow stopping the server with Ctrl+C
158 |         print("\nServer stopped by user", file=sys.stderr)
159 |     except Exception as e:
160 |         # Catch unexpected server errors
161 |         print(f"Server error: {e}", file=sys.stderr)
162 |         sys.exit(1)
163 | ```
164 | 
165 | **Explanation:**
166 | *   `if __name__ == "__main__":`: This code runs only when you execute the `server.py` script directly (e.g., `python -m govee_mcp_server.server`).
167 | *   `asyncio.run(mcp.run(transport='stdio'))`: This is the command that starts the `FastMCP` server.
168 |     *   `transport='stdio'` tells it to listen for MCP requests coming in via standard input and send responses via standard output. This is a common way for programs to talk to each other locally. (MCP can also run over network sockets).
169 |     *   `asyncio.run(...)` starts the asynchronous event loop needed for `async`/`await` functions.
170 | *   The `try...except` blocks handle stopping the server cleanly (Ctrl+C) or catching unexpected crashes.
171 | 
172 | ## Under the Hood: A Client Calls `set_brightness(50)`
173 | 
174 | Let's trace what happens when an MCP client sends a request:
175 | 
176 | 1.  **Client Request:** The MCP client program sends a specially formatted message (likely JSON) via standard input to our running `server.py` process. The message essentially says: "Execute tool `set_brightness` with argument `brightness=50`."
177 | 2.  **FastMCP Receives:** The `mcp.run()` loop in our server reads this message from its standard input.
178 | 3.  **FastMCP Parses:** The `FastMCP` library parses the message, identifies the target tool (`set_brightness`), and extracts the arguments (`brightness=50`).
179 | 4.  **FastMCP Dispatches:** It finds the Python function registered with `@mcp.tool("set_brightness")`, which is our `async def set_brightness(brightness: int)` function.
180 | 5.  **Tool Function Executes:** `FastMCP` calls `await set_brightness(brightness=50)`.
181 |     *   Inside `set_brightness`:
182 |         *   `api = GoveeAPI(config)`: Creates a new Govee API client.
183 |         *   `await api.set_brightness(50)`: Calls the API client method.
184 |             *   The `GoveeAPI` client builds the JSON request, adds the API key, sends it to the Govee cloud, gets the response (e.g., "Success").
185 |         *   The API client returns `(True, "Success")` to the `set_brightness` function.
186 |         *   The function prepares the return string: `"Success"`.
187 |         *   `finally: await api.close()`: The API client connection is closed.
188 |         *   The function `return`s the string `"Success"`.
189 | 6.  **FastMCP Receives Result:** `FastMCP` gets the `"Success"` string back from the tool function.
190 | 7.  **FastMCP Responds:** `FastMCP` formats an MCP response message (likely JSON) containing the result `"Success"` and sends it back to the client program via its standard output.
191 | 8.  **Client Receives Response:** The MCP client reads the response from its standard input and knows the command was successful.
192 | 
193 | Here's a diagram of that flow:
194 | 
195 | ```mermaid
196 | sequenceDiagram
197 |     participant Client as MCP Client
198 |     participant FastMCP as FastMCP Library (in server.py)
199 |     participant ToolFunc as set_brightness() Tool
200 |     participant APIClient as GoveeAPI Client
201 |     participant GoveeCloud as Govee Cloud
202 | 
203 |     Client->>+FastMCP: MCP Request: execute 'set_brightness', brightness=50
204 |     FastMCP->>+ToolFunc: await set_brightness(brightness=50)
205 |     ToolFunc->>APIClient: Create GoveeAPI(config)
206 |     ToolFunc->>+APIClient: await set_brightness(50)
207 |     APIClient->>GoveeCloud: Send 'Set Brightness=50' request
208 |     GoveeCloud-->>APIClient: Respond ('Success')
209 |     APIClient-->>-ToolFunc: Return (True, "Success")
210 |     ToolFunc->>APIClient: await close()
211 |     ToolFunc-->>-FastMCP: Return "Success"
212 |     FastMCP-->>-Client: MCP Response: result="Success"
213 | 
214 | ```
215 | 
216 | ## Conclusion
217 | 
218 | You've now learned how the **MCP Server Implementation** works in `govee_mcp_server`:
219 | 
220 | *   It acts as a **standardized interface** (like a universal remote receiver) for controlling the Govee device, designed to be used by *other programs* (MCP clients).
221 | *   It uses the **Mission Control Protocol (MCP)** standard for communication.
222 | *   It leverages the `FastMCP` library to handle incoming requests and outgoing responses.
223 | *   It defines specific **tools** (`turn_on_off`, `set_color`, etc.) using the `@mcp.tool` decorator.
224 | *   Each tool function uses the [Govee API Client](03_govee_api_client.md) (created per-request) to interact with the actual Govee device via the cloud.
225 | *   Proper cleanup (closing the API client) is essential within each tool.
226 | 
227 | This server provides a powerful and flexible way to integrate your Govee light control into larger systems. However, real-world interactions aren't always perfect. What happens if the Govee API returns an error, or the network connection fails, or the client sends invalid input? We need robust ways to handle these situations.
228 | 
229 | Let's explore how we manage errors gracefully in [Chapter 6: Custom Error Handling](06_custom_error_handling.md).
230 | 
231 | ---
232 | 
233 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```

--------------------------------------------------------------------------------
/documentation/04_command_line_interface__cli_.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chapter 4: Command Line Interface (CLI) - Your Simple Remote Control
  2 | 
  3 | In the [Chapter 3: Govee API Client](03_govee_api_client.md), we built the "translator" – the `GoveeAPI` client that knows how to talk directly to the Govee Cloud service using your API key and device details. It can turn the light on, change its color, and more, based on the blueprints from [Chapter 2: Device Control Interfaces](02_device_control_interfaces.md).
  4 | 
  5 | But how do *you*, the user, tell this `GoveeAPI` client what to do? You need a way to interact with it. One of the simplest ways is through a **Command Line Interface**, or **CLI**.
  6 | 
  7 | ## What's the Big Idea? Direct Control from Your Terminal
  8 | 
  9 | Imagine you have a fancy TV system with a complex universal remote (that will be our [MCP Server Implementation](05_mcp_server_implementation.md) later). But sometimes, you just want a really simple, basic remote with only a few buttons: Power On/Off, maybe Volume Up/Down. It's quick, easy, and gets the job done for simple tasks.
 10 | 
 11 | The **Command Line Interface (CLI)** for our project is like that simple remote control. It lets you interact with your Govee light directly by typing commands into your computer's **terminal** (also called a command prompt or shell).
 12 | 
 13 | Instead of needing a full graphical application or starting the main server, you can just type something like:
 14 | 
 15 | ```bash
 16 | govee-cli power on
 17 | ```
 18 | 
 19 | or
 20 | 
 21 | ```bash
 22 | govee-cli color 255 0 0 # Set color to Red
 23 | ```
 24 | 
 25 | This provides a fast and straightforward way to manually control your light or even use it in simple scripts.
 26 | 
 27 | ## How Does It Work? Typing Commands
 28 | 
 29 | When you use a CLI tool, here's the basic flow:
 30 | 1.  **You Type:** You open your terminal and type the name of the program, followed by the action you want to perform, and any necessary details (arguments).
 31 |     *   `govee-cli` is the program name.
 32 |     *   `power` or `color` is the command (the action).
 33 |     *   `on` or `255 0 0` are the arguments (the details for the action).
 34 | 2.  **Program Reads:** The `govee-cli` program reads the command and arguments you typed. It uses a helper library (in Python, often `argparse`) to understand what you're asking it to do.
 35 | 3.  **Program Acts:** Based on your command, it uses the **same `GoveeAPI` client** from [Chapter 3: Govee API Client](03_govee_api_client.md) to send the instruction to your Govee light via the Govee Cloud.
 36 | 4.  **Program Reports:** It usually prints a message back to your terminal telling you if the command was successful or if there was an error.
 37 | 
 38 | ## Using the `govee-cli`
 39 | 
 40 | Let's try some commands! (Make sure you have your `.env` file set up as described in [Chapter 1: Configuration Management](01_configuration_management.md) so the CLI knows your API key and device).
 41 | 
 42 | **(Note: You might need to install the project first or run it using `python -m govee_mcp_server.cli ...` depending on your setup. For simplicity, we'll assume `govee-cli` is available.)**
 43 | 
 44 | 1.  **Get Help:** See what commands are available.
 45 |     ```bash
 46 |     govee-cli --help
 47 |     ```
 48 |     *   **Output:** Shows a list of commands like `power`, `color`, `brightness`, `status` and how to use them.
 49 | 
 50 | 2.  **Turn Power On:**
 51 |     ```bash
 52 |     govee-cli power on
 53 |     ```
 54 |     *   **Output (if successful):** `Success` (or a similar message from the Govee API). Your light should turn on!
 55 | 
 56 | 3.  **Turn Power Off:**
 57 |     ```bash
 58 |     govee-cli power off
 59 |     ```
 60 |     *   **Output (if successful):** `Success`. Your light should turn off.
 61 | 
 62 | 4.  **Set Color (Red):**
 63 |     ```bash
 64 |     govee-cli color 255 0 0
 65 |     ```
 66 |     *   **Output (if successful):** `Success`. Your light should turn red.
 67 |     *   **Note:** The arguments are Red, Green, Blue values (0-255).
 68 | 
 69 | 5.  **Set Brightness (50%):**
 70 |     ```bash
 71 |     govee-cli brightness 50
 72 |     ```
 73 |     *   **Output (if successful):** `Success`. Your light's brightness should change.
 74 |     *   **Note:** The argument is the brightness percentage (0-100).
 75 | 
 76 | 6.  **Check Status:** Get the current state of the light.
 77 |     ```bash
 78 |     govee-cli status
 79 |     ```
 80 |     *   **Output (example):**
 81 |         ```
 82 |         Power: ON
 83 |         Color: RGB(255, 0, 0)
 84 |         Brightness: 50%
 85 |         ```
 86 | 
 87 | If something goes wrong (like a typo in the command, invalid color values, or a problem talking to Govee), the CLI will usually print an error message.
 88 | 
 89 | ## Under the Hood: What Happens When You Type `govee-cli power on`?
 90 | 
 91 | Let's trace the steps behind the scenes:
 92 | 
 93 | 1.  **Terminal:** You hit Enter after typing `govee-cli power on`. The terminal finds the `govee-cli` program and runs it, passing `power` and `on` as inputs.
 94 | 2.  **CLI Script Starts:** The Python script `src/govee_mcp_server/cli.py` begins running.
 95 | 3.  **Parse Arguments:** The script uses the `argparse` library to figure out: "The user wants the `power` command with the `state` argument set to `on`."
 96 | 4.  **Load Config:** It calls `load_config()` from [Chapter 1: Configuration Management](01_configuration_management.md) to read your `GOVEE_API_KEY`, `GOVEE_DEVICE_ID`, and `GOVEE_SKU` from the environment (or `.env` file).
 97 | 5.  **Create API Client:** It creates an instance of the `GoveeAPI` client (from [Chapter 3: Govee API Client](03_govee_api_client.md)), giving it the configuration details it just loaded. `api_client = GoveeAPI(config=...)`
 98 | 6.  **Call API Method:** Since the command was `power on`, the script calls the corresponding method on the API client: `await api_client.set_power(True)`. (It converts "on" to the boolean `True`).
 99 | 7.  **API Client Works:** The `GoveeAPI` client does its job (as described in Chapter 3): formats the request, sends it to the Govee Cloud with the API key, gets the response, handles retries if needed.
100 | 8.  **Get Result:** The `set_power` method returns a result (e.g., `(True, "Success")`) back to the CLI script.
101 | 9.  **Print Output:** The CLI script takes the result message ("Success") and prints it to your terminal.
102 | 10. **Cleanup:** The script makes sure to close the network connection used by the `GoveeAPI` client (`await api_client.close()`).
103 | 
104 | Here's a diagram showing this flow:
105 | 
106 | ```mermaid
107 | sequenceDiagram
108 |     participant User as You (Terminal)
109 |     participant CLI as govee-cli Script
110 |     participant Parser as Argument Parser (argparse)
111 |     participant Config as Config Loader
112 |     participant APIClient as GoveeAPI Client
113 |     participant GoveeCloud as Govee Cloud
114 | 
115 |     User->>CLI: Run `govee-cli power on`
116 |     CLI->>Parser: Parse ["power", "on"]
117 |     Parser-->>CLI: Command='power', State='on'
118 |     CLI->>Config: Load config()
119 |     Config-->>CLI: Return GoveeConfig (API Key, etc.)
120 |     CLI->>APIClient: Create GoveeAPI(config)
121 |     CLI->>APIClient: Call `await set_power(True)`
122 |     APIClient->>GoveeCloud: Send 'Power On' request (with API Key)
123 |     GoveeCloud-->>APIClient: Respond ('Success')
124 |     APIClient-->>CLI: Return (True, "Success")
125 |     CLI->>User: Print "Success"
126 |     CLI->>APIClient: Call `await close()`
127 |     APIClient->>APIClient: Close network connection
128 | ```
129 | 
130 | ## Inside the Code: `src/govee_mcp_server/cli.py`
131 | 
132 | Let's look at simplified parts of the code that make this work.
133 | 
134 | **1. Defining the Commands (`create_parser`)**
135 | 
136 | This function sets up `argparse` to understand the commands and arguments.
137 | 
138 | ```python
139 | # Simplified from src/govee_mcp_server/cli.py
140 | import argparse
141 | 
142 | def create_parser() -> argparse.ArgumentParser:
143 |     """Create and configure argument parser."""
144 |     parser = argparse.ArgumentParser(description='Control Govee LED device')
145 |     # Create categories for commands (like 'power', 'color')
146 |     subparsers = parser.add_subparsers(dest='command', help='Commands', required=True)
147 | 
148 |     # --- Define the 'power' command ---
149 |     power_parser = subparsers.add_parser('power', help='Turn device on/off')
150 |     # It needs one argument: 'state', which must be 'on' or 'off'
151 |     power_parser.add_argument('state', choices=['on', 'off'], help='Power state')
152 | 
153 |     # --- Define the 'color' command ---
154 |     color_parser = subparsers.add_parser('color', help='Set device color')
155 |     # It needs three integer arguments: red, green, blue
156 |     color_parser.add_argument('red', type=int, help='Red value (0-255)')
157 |     color_parser.add_argument('green', type=int, help='Green value (0-255)')
158 |     color_parser.add_argument('blue', type=int, help='Blue value (0-255)')
159 | 
160 |     # --- Define other commands similarly (brightness, status) ---
161 |     # ... parser setup for brightness ...
162 |     # ... parser setup for status ...
163 | 
164 |     return parser
165 | ```
166 | 
167 | **Explanation:**
168 | *   `argparse.ArgumentParser`: Creates the main parser object.
169 | *   `parser.add_subparsers()`: Allows us to define distinct commands (like `git commit`, `git push`). `dest='command'` stores which command was used.
170 | *   `subparsers.add_parser('power', ...)`: Defines the `power` command.
171 | *   `power_parser.add_argument('state', ...)`: Specifies that the `power` command requires an argument named `state`, which must be either the text `"on"` or `"off"`.
172 | *   Similarly, it defines the `color` command and its required `red`, `green`, `blue` integer arguments.
173 | 
174 | **2. Handling a Specific Command (`handle_power`)**
175 | 
176 | Each command has a small function to handle its logic.
177 | 
178 | ```python
179 | # Simplified from src/govee_mcp_server/cli.py
180 | from .api import GoveeAPI # We need the API client class
181 | from .exceptions import GoveeError # We need our custom error
182 | 
183 | async def handle_power(api: GoveeAPI, state: str) -> None:
184 |     """Handle the 'power' command."""
185 |     # Convert 'on'/'off' string to True/False boolean
186 |     is_on = (state == 'on')
187 | 
188 |     print(f"Sending command: Power {'ON' if is_on else 'OFF'}...")
189 |     # Call the actual API client method
190 |     success, message = await api.set_power(is_on)
191 | 
192 |     # Check the result from the API client
193 |     if success:
194 |         print(f"Result: {message}")
195 |     else:
196 |         # If the API client reported failure, raise an error to stop the script
197 |         print(f"Error reported by API: {message}")
198 |         raise GoveeError(f"Failed to set power: {message}")
199 | ```
200 | 
201 | **Explanation:**
202 | *   Takes the `api` client object and the parsed `state` ("on" or "off") as input.
203 | *   Converts the string `state` into a boolean `is_on` (`True` or `False`).
204 | *   Calls the corresponding method on the API client: `await api.set_power(is_on)`. This is where the communication with Govee happens (via the API client).
205 | *   Prints the result or raises an error if the API call failed.
206 | 
207 | **3. The Main Script Logic (`main` function)**
208 | 
209 | This function ties everything together: load config, parse args, call the right handler.
210 | 
211 | ```python
212 | # Simplified from src/govee_mcp_server/cli.py
213 | import sys
214 | import asyncio
215 | from .config import load_config
216 | from .api import GoveeAPI
217 | from .exceptions import GoveeError
218 | 
219 | # ... create_parser() defined above ...
220 | # ... handle_power(), handle_color(), etc. defined above ...
221 | 
222 | async def main() -> None:
223 |     """Main CLI entrypoint."""
224 |     api = None # Initialize api variable
225 |     try:
226 |         # 1. Load configuration
227 |         config = load_config()
228 |         print(f"Loaded config for device: {config.device_id}")
229 | 
230 |         # 2. Create the Govee API Client
231 |         api = GoveeAPI(config)
232 |         print("GoveeAPI client ready.")
233 | 
234 |         # 3. Parse command line arguments
235 |         parser = create_parser()
236 |         args = parser.parse_args() # Reads sys.argv (what you typed)
237 |         print(f"Executing command: {args.command}")
238 | 
239 |         # 4. Call the appropriate handler based on the command
240 |         if args.command == 'power':
241 |             await handle_power(api, args.state)
242 |         elif args.command == 'color':
243 |             await handle_color(api, args.red, args.green, args.blue)
244 |         # ... elif for brightness ...
245 |         # ... elif for status ...
246 |         else:
247 |             # Should not happen if parser is configured correctly
248 |             print(f"Unknown command: {args.command}")
249 |             parser.print_help()
250 |             sys.exit(1) # Exit with an error code
251 | 
252 |     except GoveeError as e:
253 |         # Catch errors from our API client or handlers
254 |         print(f"\nOperation Failed: {str(e)}")
255 |         sys.exit(1) # Exit with an error code
256 |     except Exception as e:
257 |         # Catch any other unexpected errors
258 |         print(f"\nAn Unexpected Error Occurred: {str(e)}")
259 |         sys.exit(1) # Exit with an error code
260 |     finally:
261 |         # 5. ALWAYS try to clean up the API client connection
262 |         if api:
263 |             print("Closing API connection...")
264 |             await api.close()
265 |             print("Connection closed.")
266 | 
267 | # --- This part runs the main async function ---
268 | # def cli_main():
269 | #    asyncio.run(main())
270 | # if __name__ == "__main__":
271 | #    cli_main()
272 | ```
273 | 
274 | **Explanation:**
275 | 1.  Calls `load_config()` to get settings.
276 | 2.  Creates the `GoveeAPI` instance.
277 | 3.  Calls `parser.parse_args()` which uses `argparse` to figure out which command and arguments were given.
278 | 4.  Uses an `if/elif` block to check `args.command` and call the correct handler function (like `handle_power` or `handle_color`), passing the `api` client and the specific arguments needed (like `args.state` or `args.red`).
279 | 5.  Includes `try...except` blocks to catch potential errors (like configuration errors, API errors, or validation errors) and print user-friendly messages before exiting.
280 | 6.  The `finally` block ensures that `await api.close()` is called to release network resources, even if an error occurred.
281 | 
282 | ## Conclusion
283 | 
284 | You've learned about the **Command Line Interface (CLI)**, a simple way to directly control your Govee device from the terminal.
285 | 
286 | *   It acts like a **basic remote control**.
287 | *   You use it by typing commands like `govee-cli power on`.
288 | *   It **parses** your command and arguments using `argparse`.
289 | *   Crucially, it uses the **same `GoveeAPI` client** from [Chapter 3: Govee API Client](03_govee_api_client.md) to interact with the Govee Cloud.
290 | *   It provides immediate feedback in your terminal.
291 | 
292 | The CLI is great for quick tests, manual control, or simple automation scripts. However, it requires you to be at your computer typing commands. What if we want other applications or systems to control the light over a network, perhaps using a standard protocol?
293 | 
294 | That's where the main part of our project comes in: the server. Let's move on to [Chapter 5: MCP Server Implementation](05_mcp_server_implementation.md) to see how we build a server that listens for commands using the Mission Control Protocol (MCP).
295 | 
296 | ---
297 | 
298 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```

--------------------------------------------------------------------------------
/documentation/06_custom_error_handling.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chapter 6: Custom Error Handling - Specific Warning Lights
  2 | 
  3 | Welcome to the final chapter of our `govee_mcp_server` tutorial! In [Chapter 5: MCP Server Implementation](05_mcp_server_implementation.md), we saw how our server listens for commands from other programs using the Mission Control Protocol (MCP) and uses the [Govee API Client](03_govee_api_client.md) to control the light.
  4 | 
  5 | But what happens when things don't go as planned? Maybe the internet connection drops, the Govee service is temporarily down, your API key is wrong, or a client sends an invalid command like trying to set the color to "purple" instead of RGB values. If the program just crashes with a generic error, it's hard to figure out what went wrong.
  6 | 
  7 | This chapter is about how `govee_mcp_server` uses **Custom Error Handling** to provide specific, helpful information when problems occur.
  8 | 
  9 | ## What's the Big Idea? Knowing *Why* Things Broke
 10 | 
 11 | Imagine driving a car. If something goes wrong, you wouldn't want just one single "Check Engine" light for *every* possible problem, would you? Is it low oil? Are the brakes failing? Is a tire flat? A good dashboard has specific lights: "Low Fuel," "Oil Pressure Low," "Brake System Alert," etc. This tells you exactly what needs attention.
 12 | 
 13 | Standard Python errors (like `ValueError`, `TypeError`, `Exception`) are like that generic "Check Engine" light. They tell you *something* is wrong, but not always the specific context within *our* application.
 14 | 
 15 | **Custom Error Handling** in our project means we've created our *own* types of errors, specific to talking with Govee devices. Instead of just saying "Error!", we can say:
 16 | *   "Error talking to the Govee API!" (`GoveeAPIError`)
 17 | *   "Error: Your API key is missing or wrong!" (`GoveeConfigError`)
 18 | *   "Error: That's not a valid color value!" (`GoveeValidationError`)
 19 | *   "Error: Couldn't connect to the Govee servers!" (`GoveeConnectionError`)
 20 | *   "Error: Govee took too long to reply!" (`GoveeTimeoutError`)
 21 | 
 22 | This makes it much easier for both the user and the developer to understand and fix the problem. It's like having those specific warning lights on our Govee control dashboard.
 23 | 
 24 | ## Our Custom Warning Lights: The Exception Classes
 25 | 
 26 | In programming, errors are often handled using "Exceptions". When something goes wrong, the code can `raise` an exception, which stops the normal flow and signals that a problem occurred.
 27 | 
 28 | We've defined our custom exception types in the file `src/govee_mcp_server/exceptions.py`. They all inherit from a base `GoveeError`, making it easy to catch any Govee-specific problem if needed.
 29 | 
 30 | ```python
 31 | # Simplified from src/govee_mcp_server/exceptions.py
 32 | 
 33 | class GoveeError(Exception):
 34 |     """Base exception for all Govee-related errors in this project."""
 35 |     pass # It's just a label, inheriting basic Exception features
 36 | 
 37 | class GoveeAPIError(GoveeError):
 38 |     """Problem communicating with the Govee API itself
 39 |        (e.g., Govee returned an error message like 'Invalid API Key')."""
 40 |     pass
 41 | 
 42 | class GoveeConfigError(GoveeError):
 43 |     """Problem with the configuration settings
 44 |        (e.g., missing GOVEE_API_KEY in the .env file)."""
 45 |     pass
 46 | 
 47 | class GoveeValidationError(GoveeError):
 48 |     """Problem with the data provided by the user/client
 49 |        (e.g., brightness=150, or invalid RGB values)."""
 50 |     pass
 51 | 
 52 | class GoveeConnectionError(GoveeError):
 53 |     """Problem with the network connection to Govee servers."""
 54 |     pass
 55 | 
 56 | class GoveeTimeoutError(GoveeError):
 57 |     """Govee's servers took too long to respond to our request."""
 58 |     pass
 59 | ```
 60 | 
 61 | **Explanation:**
 62 | *   `class GoveeError(Exception):`: This defines our main category. Anything that's a `GoveeConfigError` is *also* a `GoveeError`.
 63 | *   The other classes (`GoveeAPIError`, `GoveeConfigError`, etc.) inherit from `GoveeError`. They don't need any special code inside them; their *name* is what makes them specific. Think of them as different colored warning light bulbs.
 64 | 
 65 | ## Turning On the Warning Lights: Raising Exceptions
 66 | 
 67 | Different parts of our code are responsible for detecting specific problems and `raise`-ing the appropriate custom exception.
 68 | 
 69 | **Example 1: Missing Configuration (`load_config`)**
 70 | 
 71 | Remember the `load_config` function from [Chapter 1: Configuration Management](01_configuration_management.md)? If it can't find your API key, it raises a `GoveeConfigError`.
 72 | 
 73 | ```python
 74 | # Simplified from src/govee_mcp_server/config.py
 75 | from .exceptions import GoveeConfigError # Import the specific error
 76 | # ... other imports ...
 77 | 
 78 | def load_config():
 79 |     # ... code to load api_key, device_id, sku ...
 80 |     api_key = None # Let's pretend API key wasn't found
 81 | 
 82 |     if not api_key: # Check if it's missing
 83 |         # Raise the specific configuration error!
 84 |         raise GoveeConfigError("Missing required environment variable: GOVEE_API_KEY")
 85 | 
 86 |     # ... return config object if all is well ...
 87 | ```
 88 | 
 89 | **Explanation:**
 90 | *   If `api_key` is not found, `raise GoveeConfigError(...)` immediately stops the `load_config` function and signals this specific problem.
 91 | 
 92 | **Example 2: Invalid Color Input (`validate_rgb`)**
 93 | 
 94 | The `@validate_rgb` decorator we saw in [Chapter 2: Device Control Interfaces](02_device_control_interfaces.md) checks color values before they even reach the API client.
 95 | 
 96 | ```python
 97 | # Simplified from src/govee_mcp_server/interfaces.py
 98 | from .exceptions import GoveeValidationError # Import the specific error
 99 | # ... other imports ...
100 | 
101 | def validate_rgb(func):
102 |     async def wrapper(self, r: int, g: int, b: int, *args, **kwargs):
103 |         if not (0 <= r <= 255): # Check if Red value is valid
104 |             # Raise the specific validation error!
105 |             raise GoveeValidationError("red value must be between 0-255")
106 |         # ... check g and b ...
107 |         # If all good, call the original function (like set_color)
108 |         return await func(self, r, g, b, *args, **kwargs)
109 |     return wrapper
110 | 
111 | class ColorControl(ABC):
112 |     @abstractmethod
113 |     @validate_rgb # Apply the validator
114 |     async def set_color(self, r: int, g: int, b: int) -> Tuple[bool, str]:
115 |         pass
116 | ```
117 | 
118 | **Explanation:**
119 | *   If `r` is outside the 0-255 range, `raise GoveeValidationError(...)` stops the process *before* the actual `set_color` logic in `GoveeAPI` runs.
120 | 
121 | **Example 3: API Communication Problems (`_make_request`)**
122 | 
123 | The core `_make_request` function inside the [Govee API Client](03_govee_api_client.md) handles errors from the Govee servers or network issues.
124 | 
125 | ```python
126 | # Simplified from src/govee_mcp_server/api.py
127 | import aiohttp
128 | import asyncio
129 | from .exceptions import GoveeAPIError, GoveeConnectionError, GoveeTimeoutError
130 | 
131 | class GoveeAPI:
132 |     # ... other methods ...
133 | 
134 |     async def _make_request(self, method: str, endpoint: str, **kwargs):
135 |         # ... setup ...
136 |         for attempt in range(self.MAX_RETRIES):
137 |             try:
138 |                 async with self.session.request(...) as response:
139 |                     data = await response.json()
140 |                     if response.status == 401: # Example: Unauthorized
141 |                         # Raise specific API error!
142 |                         raise GoveeAPIError(f"API error: 401 - Invalid API Key")
143 |                     elif response.status != 200:
144 |                          # Raise general API error for other bad statuses
145 |                          raise GoveeAPIError(f"API error: {response.status} - {data.get('message')}")
146 |                     # ... success path ...
147 |                     return data, data.get('message', 'Success')
148 | 
149 |             except asyncio.TimeoutError:
150 |                 if attempt == self.MAX_RETRIES - 1:
151 |                     # Raise specific timeout error!
152 |                     raise GoveeTimeoutError("Request timed out")
153 |             except aiohttp.ClientError as e:
154 |                  if attempt == self.MAX_RETRIES - 1:
155 |                     # Raise specific connection error!
156 |                     raise GoveeConnectionError(f"Connection error: {e}")
157 |             # ... retry logic ...
158 |         raise GoveeAPIError("Max retries exceeded") # If all retries fail
159 | ```
160 | 
161 | **Explanation:**
162 | *   Based on the HTTP status code from Govee (like 401) or network exceptions (`TimeoutError`, `ClientError`), this code raises the corresponding specific `GoveeError`.
163 | 
164 | ## Reacting to Warning Lights: Catching Exceptions
165 | 
166 | Raising an error is only half the story. Other parts of the code need to *catch* these specific errors and handle them gracefully, perhaps by showing a helpful message to the user. This is done using `try...except` blocks.
167 | 
168 | **Example: Handling Errors in the CLI**
169 | 
170 | The main function in our [Chapter 4: Command Line Interface (CLI)](04_command_line_interface__cli_.md) wraps the core logic in a `try...except` block.
171 | 
172 | ```python
173 | # Simplified from src/govee_mcp_server/cli.py
174 | import sys
175 | from .exceptions import GoveeError, GoveeConfigError, GoveeValidationError
176 | # ... other imports ...
177 | 
178 | async def main() -> None:
179 |     api = None
180 |     try:
181 |         # --- Operations that might raise GoveeErrors ---
182 |         config = load_config() # Might raise GoveeConfigError
183 |         api = GoveeAPI(config)
184 |         args = parser.parse_args()
185 | 
186 |         if args.command == 'color':
187 |             # Might raise GoveeValidationError (from decorator)
188 |             # or GoveeAPIError, etc. (from api.set_color)
189 |             await handle_color(api, args.red, args.green, args.blue)
190 |         # ... other command handlers ...
191 |         # --- End of operations ---
192 | 
193 |     # --- Catching specific errors ---
194 |     except GoveeConfigError as e:
195 |         print(f"\nConfiguration Problem: {str(e)}")
196 |         print("Please check your .env file or environment variables.")
197 |         sys.exit(1) # Exit with an error status
198 |     except GoveeValidationError as e:
199 |         print(f"\nInvalid Input: {str(e)}")
200 |         sys.exit(1)
201 |     except GoveeError as e: # Catch any other Govee-specific error
202 |         print(f"\nOperation Failed: {str(e)}")
203 |         sys.exit(1)
204 |     except Exception as e: # Catch any totally unexpected error
205 |         print(f"\nAn Unexpected Error Occurred: {str(e)}")
206 |         sys.exit(1)
207 |     finally:
208 |         # Cleanup runs whether there was an error or not
209 |         if api:
210 |             await api.close()
211 | 
212 | # ... function to run main ...
213 | ```
214 | 
215 | **Explanation:**
216 | *   The code inside the `try` block is executed.
217 | *   If `load_config()` raises a `GoveeConfigError`, the code jumps directly to the `except GoveeConfigError as e:` block. It prints a specific message about configuration and exits.
218 | *   If `handle_color` (or the underlying `api.set_color`) raises a `GoveeValidationError`, it jumps to the `except GoveeValidationError as e:` block.
219 | *   If any *other* type of `GoveeError` occurs (like `GoveeAPIError`, `GoveeTimeoutError`), it's caught by the more general `except GoveeError as e:` block.
220 | *   This allows the CLI to give tailored feedback based on *what kind* of error happened.
221 | 
222 | The MCP server in [Chapter 5: MCP Server Implementation](05_mcp_server_implementation.md) uses a similar `try...except` structure within each `@mcp.tool` function to catch `GoveeError`s and return an informative error message to the MCP client instead of crashing.
223 | 
224 | ## Under the Hood: Tracing an "Invalid API Key" Error
225 | 
226 | Let's follow the journey of an error when the user tries to run the CLI with a wrong API key:
227 | 
228 | 1.  **User Runs:** `govee-cli power on`
229 | 2.  **CLI Starts:** `cli.py` `main()` function begins.
230 | 3.  **Load Config:** `load_config()` runs successfully (it finds *an* API key, just the wrong one).
231 | 4.  **Create API Client:** `api = GoveeAPI(config)` is created.
232 | 5.  **Call Handler:** `handle_power(api, 'on')` is called.
233 | 6.  **Call API Method:** `await api.set_power(True)` is called.
234 | 7.  **Make Request:** Inside `set_power`, `await self._make_request(...)` is called.
235 | 8.  **Send to Govee:** `_make_request` sends the command to the Govee Cloud API, including the *invalid* API key in the headers.
236 | 9.  **Govee Responds:** The Govee API sees the invalid key and sends back an HTTP 401 Unauthorized error response (e.g., `{"message": "Invalid API Key"}`).
237 | 10. **Detect Error:** `_make_request` receives the 401 status.
238 | 11. **Raise Specific Error:** It executes `raise GoveeAPIError("API error: 401 - Invalid API Key")`. This stops `_make_request`.
239 | 12. **Propagate Error:** The `GoveeAPIError` travels up out of `_make_request` and then out of `api.set_power`. (Note: our simplified `set_power` in the previous chapter *caught* the error and returned `False`, but the real one might let it propagate or re-raise it). Let's assume for this trace it propagates up.
240 | 13. **Catch in Handler:** The `try...except` block in `handle_power` catches the `GoveeAPIError`.
241 | 14. **Raise from Handler:** The handler re-raises the error: `raise GoveeError(message)` (or simply lets the original `GoveeAPIError` propagate).
242 | 15. **Catch in Main:** The main `try...except` block in `cli.py` `main()` catches the `GoveeError` (since `GoveeAPIError` is a type of `GoveeError`).
243 | 16. **Print Message:** The code inside `except GoveeError as e:` runs, printing: `Operation Failed: API error: 401 - Invalid API Key`
244 | 17. **Exit:** The CLI exits with an error code (`sys.exit(1)`).
245 | 
246 | ```mermaid
247 | sequenceDiagram
248 |     participant User
249 |     participant CLI as cli.py main()
250 |     participant Handler as handle_power()
251 |     participant API as GoveeAPI.set_power()
252 |     participant Request as GoveeAPI._make_request()
253 |     participant GoveeCloud
254 | 
255 |     User->>CLI: Run `govee-cli power on`
256 |     CLI->>Handler: Call handle_power()
257 |     Handler->>API: Call api.set_power(True)
258 |     API->>Request: Call _make_request()
259 |     Request->>GoveeCloud: Send request (with bad API key)
260 |     GoveeCloud-->>Request: Respond: HTTP 401 Unauthorized
261 |     Request-->>Request: Detect 401 status
262 |     Request-->>API: raise GoveeAPIError("Invalid API Key")
263 |     API-->>Handler: Error propagates up
264 |     Handler-->>CLI: Error propagates up
265 |     CLI-->>CLI: Catch GoveeAPIError (as GoveeError)
266 |     CLI->>User: Print "Operation Failed: API error: 401 - Invalid API Key"
267 |     CLI->>User: Exit with error code
268 | ```
269 | 
270 | This detailed flow shows how the specific error generated deep within the API client travels up and is eventually caught by the top-level error handler, allowing for a precise message to be shown to the user.
271 | 
272 | ## Conclusion
273 | 
274 | You've reached the end of the `govee_mcp_server` tutorial! In this chapter, we learned about **Custom Error Handling**:
275 | 
276 | *   It's like having specific **warning lights** instead of one generic "Check Engine" light.
277 | *   We defined our own exception classes (`GoveeAPIError`, `GoveeConfigError`, `GoveeValidationError`, etc.) inheriting from a base `GoveeError`.
278 | *   Different parts of the code `raise` these specific errors when problems are detected (e.g., bad config, invalid input, API failure).
279 | *   Other parts of the code (like the CLI and MCP server) use `try...except` blocks to `catch` these specific errors and provide informative feedback or take corrective action.
280 | *   This makes debugging easier and the application more user-friendly.
281 | 
282 | Throughout this tutorial, you've seen how `govee_mcp_server` manages configuration, defines device capabilities using interfaces, communicates with the Govee API, provides both a command-line interface and an MCP server, and handles errors gracefully.
283 | 
284 | We hope this journey has given you a clear understanding of how the project is structured and how its core components work together. Happy controlling!
285 | 
286 | ---
287 | 
288 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```

--------------------------------------------------------------------------------
/documentation/03_govee_api_client.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chapter 3: Govee API Client - The Messenger to Govee
  2 | 
  3 | In [Chapter 1: Configuration Management](01_configuration_management.md), we learned how our application gets its essential details (like your API Key and Device ID). Then, in [Chapter 2: Device Control Interfaces](02_device_control_interfaces.md), we saw how we create standard blueprints (`PowerControl`, `ColorControl`, etc.) that define *what* actions we can perform, like turning a light on or changing its color.
  4 | 
  5 | But defining a blueprint for a "power socket" doesn't actually provide electricity! We need a component that takes our standard command (like "turn power on") and translates it into the specific language the Govee Cloud understands, then actually sends that message over the internet.
  6 | 
  7 | This chapter introduces the **Govee API Client** – the heart of our communication system.
  8 | 
  9 | ## What's the Big Idea? Talking the Govee Talk
 10 | 
 11 | Imagine you need to send instructions to someone who only speaks a very specific, technical language (the "Govee language"). You wouldn't just shout your request in English! You'd hire a specialized **translator or diplomat** who knows:
 12 | 
 13 | 1.  **The Language:** Exactly how to phrase the request (the structure, the vocabulary).
 14 | 2.  **The Protocol:** How to properly address the message and where to send it (the specific web address or "endpoint").
 15 | 3.  **The Credentials:** How to prove they are allowed to speak on your behalf (using your API Key).
 16 | 4.  **Handling Issues:** What to do if the message doesn't get through (like trying again or reporting an error).
 17 | 
 18 | The **Govee API Client** (implemented in our project as the `GoveeAPI` class in `src/govee_mcp_server/api.py`) is exactly this translator. It takes simple commands defined by our interfaces (like `set_power(True)`) and handles all the complex details of communicating with the actual Govee servers.
 19 | 
 20 | ## Key Roles of the Govee API Client
 21 | 
 22 | Let's break down the main jobs of this crucial component:
 23 | 
 24 | 1.  **Using Credentials:** It takes the `GoveeConfig` object (containing your API Key, Device ID, and SKU) that we loaded in [Chapter 1: Configuration Management](01_configuration_management.md). It needs these to authenticate itself and specify which device to control.
 25 | 2.  **Formatting Requests:** Govee's API expects commands in a very specific format, usually JSON (a standard way to structure data). The client builds these correctly formatted messages. For example, turning a light on isn't just sending "ON"; it's sending structured data like `{"device": "YOUR_ID", "sku": "YOUR_SKU", "capability": {... "value": 1 ...}}`.
 26 | 3.  **Sending Over the Internet:** It uses standard web protocols (HTTP) to send these formatted requests to the correct Govee API server address (like `https://openapi.api.govee.com/...`).
 27 | 4.  **Processing Responses:** Govee's servers send back replies, also usually in JSON. The client needs to understand these replies to know if the command worked and to get information (like the current brightness).
 28 | 5.  **Handling Network Hiccups:** The internet isn't always perfect. Connections can drop, or servers might be slow. The client includes logic to automatically **retry** sending a request a few times if it fails initially. It also handles specific errors like timeouts or connection failures gracefully.
 29 | 
 30 | ## Using the Govee API Client
 31 | 
 32 | Let's see how simple it looks from the outside, thanks to the groundwork laid in previous chapters.
 33 | 
 34 | First, we need the configuration and then we create an instance of the `GoveeAPI` client:
 35 | 
 36 | ```python
 37 | # --- Conceptual Example ---
 38 | from govee_mcp_server.config import load_config
 39 | from govee_mcp_server.api import GoveeAPI
 40 | import asyncio # We need this for 'async' functions
 41 | 
 42 | async def main():
 43 |     # 1. Load the configuration (API Key, Device ID, SKU)
 44 |     try:
 45 |         config = load_config()
 46 |         print(f"Loaded config for device: {config.device_id}")
 47 |     except Exception as e:
 48 |         print(f"Error loading config: {e}")
 49 |         return
 50 | 
 51 |     # 2. Create the Govee API Client instance, giving it the config
 52 |     api_client = GoveeAPI(config=config)
 53 |     print("GoveeAPI client created.")
 54 | 
 55 |     # 3. Now we can use the client to control the device!
 56 |     #    (Example in the next section)
 57 | 
 58 |     # Don't forget to close the connection when done
 59 |     await api_client.close()
 60 |     print("API client connection closed.")
 61 | 
 62 | # Run the example function
 63 | # asyncio.run(main()) # You'd typically run this in a real script
 64 | ```
 65 | 
 66 | **Explanation:**
 67 | 1.  We use `load_config()` from [Chapter 1: Configuration Management](01_configuration_management.md) to get our settings.
 68 | 2.  We create `GoveeAPI` by passing the `config` object to it. The client now knows *which* device to talk to and *how* to authenticate.
 69 | 3.  The `api_client` object is now ready to send commands.
 70 | 4.  `await api_client.close()` is important to clean up network connections gracefully when we're finished.
 71 | 
 72 | Now, let's use the client. Remember how `GoveeAPI` implements the interfaces from [Chapter 2: Device Control Interfaces](02_device_control_interfaces.md)? We can call those interface methods directly on our `api_client` object:
 73 | 
 74 | ```python
 75 | # --- Continuing the conceptual example ---
 76 | 
 77 | # Assume 'api_client' was created as shown above
 78 | 
 79 | # Let's turn the light ON
 80 | print("Attempting to turn power ON...")
 81 | success, message = await api_client.set_power(True)
 82 | 
 83 | if success:
 84 |     print(f"Success! Govee replied: '{message}'")
 85 | else:
 86 |     print(f"Failed. Govee error: '{message}'")
 87 | 
 88 | # Example Output (if successful):
 89 | # Attempting to turn power ON...
 90 | # Success! Govee replied: 'Success'
 91 | 
 92 | # Example Output (if failed, e.g., wrong API key):
 93 | # Attempting to turn power ON...
 94 | # Failed. Govee error: 'API error: 401 - Invalid API Key'
 95 | ```
 96 | 
 97 | **Explanation:**
 98 | *   We call `api_client.set_power(True)`. Even though this looks simple, behind the scenes, the `GoveeAPI` client is doing all the hard work: formatting the request, adding the API key, sending it, and interpreting the response.
 99 | *   Because our interfaces (and thus the `GoveeAPI` implementation) are defined to return a `Tuple[bool, str]`, we get back whether the operation succeeded (`success`) and a message from the API (`message`).
100 | 
101 | ## Under the Hood: How a Command is Sent
102 | 
103 | Let's trace the journey of a `set_power(True)` command:
104 | 
105 | 1.  **You Call:** Your code calls `await api_client.set_power(True)`.
106 | 2.  **Client Receives:** The `set_power` method inside the `GoveeAPI` object starts running.
107 | 3.  **Get Credentials:** It accesses the stored `config` object to retrieve `api_key`, `device_id`, and `sku`.
108 | 4.  **Build the Message:** It constructs the specific JSON payload Govee expects for power control. This might look something like:
109 |     ```json
110 |     {
111 |       "requestId": "some_unique_id", // Often based on time
112 |       "payload": {
113 |         "sku": "H6159", // Your device model
114 |         "device": "AB:CD:EF...", // Your device ID
115 |         "capability": {
116 |           "type": "devices.capabilities.on_off",
117 |           "instance": "powerSwitch",
118 |           "value": 1 // 1 means ON, 0 means OFF
119 |         }
120 |       }
121 |     }
122 |     ```
123 | 5.  **Prepare the Envelope:** It gets ready to make an HTTP POST request. It sets the destination URL (`https://openapi.api.govee.com/router/api/v1/device/control`) and adds required headers, most importantly the `Govee-API-Key` header with your key.
124 | 6.  **Send:** It sends the request over the internet using an HTTP library (`aiohttp` in our case).
125 | 7.  **Wait & Listen:** It waits for Govee's servers to respond.
126 | 8.  **Check Reply:** Govee sends back a response (e.g., HTTP status code 200 OK and some JSON data like `{"message": "Success"}`).
127 | 9.  **Handle Problems (Retry?):** If there was a network error (e.g., timeout) or a bad response from Govee (e.g., status 500 Server Error), the client might wait a second and try sending the request again (up to a certain limit).
128 | 10. **Translate Result:** Based on the final response (or lack thereof after retries), it determines if the command was successful (`True` or `False`) and extracts a relevant message.
129 | 11. **Return:** It returns the `(success, message)` tuple back to your code.
130 | 
131 | Here's a simplified diagram of this flow:
132 | 
133 | ```mermaid
134 | sequenceDiagram
135 |     participant YourCode as Your Code
136 |     participant APIClient as GoveeAPI Object
137 |     participant Config as GoveeConfig
138 |     participant Formatter as Request Formatter
139 |     participant HTTP as HTTP Sender (aiohttp)
140 |     participant GoveeCloud as Govee Cloud API
141 | 
142 |     YourCode->>APIClient: await set_power(True)
143 |     APIClient->>Config: Get api_key, device_id, sku
144 |     Config-->>APIClient: Return values
145 |     APIClient->>Formatter: Build power ON JSON payload
146 |     Formatter-->>APIClient: Return formatted JSON
147 |     APIClient->>HTTP: Prepare POST request (URL, JSON, Headers with API Key)
148 |     loop Retries (if needed)
149 |         HTTP->>GoveeCloud: Send HTTP POST request
150 |         GoveeCloud-->>HTTP: Send HTTP Response (e.g., 200 OK, {'message':'Success'})
151 |         alt Successful Response
152 |             HTTP-->>APIClient: Forward response
153 |             Note over APIClient: Success! Exit loop.
154 |         else Network Error or Bad Response
155 |             HTTP-->>APIClient: Report error
156 |             Note over APIClient: Wait and retry (if attempts remain)
157 |         end
158 |     end
159 |     APIClient->>YourCode: return (True, "Success") or (False, "Error message")
160 | 
161 | ```
162 | 
163 | ## Inside the Code: Key Parts of `GoveeAPI`
164 | 
165 | Let's peek at simplified snippets from `src/govee_mcp_server/api.py` to see how this is implemented.
166 | 
167 | **1. Initialization (`__init__`)**
168 | 
169 | ```python
170 | # Simplified from src/govee_mcp_server/api.py
171 | import aiohttp # Library for making async web requests
172 | from .config import GoveeConfig
173 | # ... other imports ...
174 | 
175 | class GoveeAPI(PowerControl, ColorControl, BrightnessControl):
176 |     # ... constants like BASE_URL, MAX_RETRIES ...
177 | 
178 |     def __init__(self, config: GoveeConfig):
179 |         """Initialize API client with configuration."""
180 |         self.config = config # Store the config object
181 |         self.session: Optional[aiohttp.ClientSession] = None # Network session setup later
182 |         # ... maybe other setup ...
183 |         print(f"GoveeAPI initialized for device {self.config.device_id}")
184 | ```
185 | 
186 | **Explanation:**
187 | *   The `__init__` method runs when you create `GoveeAPI(config=...)`.
188 | *   It simply stores the passed-in `config` object so other methods in the class can access the API key, device ID, etc., using `self.config.api_key`.
189 | *   It also prepares a variable `self.session` to hold the network connection details, which will be created when needed.
190 | 
191 | **2. Making the Request (`_make_request`)**
192 | 
193 | This is the core helper method that handles sending *any* request, including retries and basic error handling.
194 | 
195 | ```python
196 | # Simplified from src/govee_mcp_server/api.py
197 | import asyncio
198 | # ... other imports ...
199 | 
200 | class GoveeAPI:
201 |     # ... __init__ and constants ...
202 | 
203 |     async def _ensure_session(self) -> None:
204 |         """Create network session if needed."""
205 |         if self.session is None or self.session.closed:
206 |             print("Creating new network session...")
207 |             self.session = aiohttp.ClientSession(
208 |                 headers={"Govee-API-Key": self.config.api_key}, # Set API key for ALL requests!
209 |                 timeout=aiohttp.ClientTimeout(total=10) # 10-second timeout
210 |             )
211 | 
212 |     async def _make_request(self, method: str, endpoint: str, **kwargs):
213 |         """Make HTTP request with retries."""
214 |         await self._ensure_session() # Make sure we have a network session
215 | 
216 |         last_error = None
217 |         for attempt in range(self.MAX_RETRIES): # Try up to MAX_RETRIES times
218 |             try:
219 |                 print(f"Attempt {attempt+1}: Sending {method} to {endpoint}")
220 |                 async with self.session.request(method, f"{self.BASE_URL}/{endpoint}", **kwargs) as response:
221 |                     data = await response.json()
222 |                     print(f"Received status: {response.status}")
223 |                     if response.status == 200: # HTTP 200 means OK!
224 |                         return data, data.get('message', 'Success') # Return data and message
225 |                     else:
226 |                         # Govee returned an error status
227 |                         last_error = GoveeAPIError(f"API error: {response.status} - {data.get('message', 'Unknown')}")
228 | 
229 |             except asyncio.TimeoutError:
230 |                 last_error = GoveeTimeoutError("Request timed out")
231 |             except aiohttp.ClientError as e:
232 |                 last_error = GoveeConnectionError(f"Connection error: {e}")
233 | 
234 |             # If error occurred, wait before retrying (increasing delay)
235 |             if attempt < self.MAX_RETRIES - 1:
236 |                 delay = self.RETRY_DELAY * (attempt + 1)
237 |                 print(f"Request failed. Retrying in {delay}s...")
238 |                 await asyncio.sleep(delay)
239 | 
240 |         # If all retries failed, raise the last encountered error
241 |         print("Max retries exceeded.")
242 |         raise last_error
243 | 
244 | ```
245 | 
246 | **Explanation:**
247 | *   `_ensure_session`: Checks if a network `session` exists; if not, it creates one using `aiohttp`. Crucially, it sets the `Govee-API-Key` header here, so it's automatically included in all requests made with this session. It also sets a timeout.
248 | *   The main loop runs `MAX_RETRIES` times (e.g., 3 times).
249 | *   `try...except`: It tries to make the request using `self.session.request(...)`. It catches specific network errors like `TimeoutError` or general `ClientError`.
250 | *   `if response.status == 200`: Checks if Govee reported success. If yes, it returns the data.
251 | *   `else`: If Govee returns an error code (like 401 Unauthorized or 400 Bad Request), it creates a `GoveeAPIError`.
252 | *   Retry Logic: If an error occurs, it waits (`asyncio.sleep`) before the next attempt. The delay increases with each retry.
253 | *   If all retries fail, it `raise`s the last error it encountered, which can then be caught by the calling method (like `set_power`).
254 | 
255 | **3. Implementing an Interface Method (`set_power`)**
256 | 
257 | This method uses `_make_request` to perform a specific action.
258 | 
259 | ```python
260 | # Simplified from src/govee_mcp_server/api.py
261 | from time import time # To generate a unique request ID
262 | 
263 | class GoveeAPI:
264 |     # ... __init__, _make_request ...
265 | 
266 |     async def set_power(self, state: bool) -> Tuple[bool, str]:
267 |         """Implement PowerControl.set_power"""
268 |         try:
269 |             # Prepare the specific JSON payload for the power command
270 |             payload = {
271 |                 "requestId": str(int(time())), # Unique ID for the request
272 |                 "payload": {
273 |                     "sku": self.config.sku,
274 |                     "device": self.config.device_id,
275 |                     "capability": {
276 |                         "type": "devices.capabilities.on_off",
277 |                         "instance": "powerSwitch",
278 |                         "value": 1 if state else 0 # Convert True/False to 1/0
279 |                     }
280 |                 }
281 |             }
282 |             # Call the generic request handler
283 |             _, message = await self._make_request(
284 |                 method="POST",
285 |                 endpoint="router/api/v1/device/control", # Govee's endpoint for control
286 |                 json=payload # Pass the JSON data
287 |             )
288 |             return True, message # If _make_request succeeded, return True
289 |         except GoveeError as e:
290 |             # If _make_request raised an error after retries
291 |             print(f"set_power failed: {e}")
292 |             return False, str(e) # Return False and the error message
293 | ```
294 | 
295 | **Explanation:**
296 | *   It builds the `payload` dictionary exactly as Govee requires for the `on_off` capability, using the `device_id` and `sku` from `self.config`.
297 | *   It calls `self._make_request`, telling it to use the `POST` method, the specific `/device/control` endpoint, and the `payload` as JSON data.
298 | *   `try...except GoveeError`: It wraps the call to `_make_request`. If `_make_request` succeeds, it returns `(True, message)`. If `_make_request` eventually fails (after retries) and raises a `GoveeError` (or one of its subtypes like `GoveeAPIError`, `GoveeTimeoutError`), the `except` block catches it and returns `(False, error_message)`.
299 | 
300 | The other methods (`set_color`, `get_brightness`, etc.) follow a very similar pattern: build the specific payload for that capability and call `_make_request`.
301 | 
302 | ## Conclusion
303 | 
304 | You've now explored the **Govee API Client (`GoveeAPI`)**, the component that acts as our application's diplomat to the Govee Cloud.
305 | 
306 | You learned:
307 | *   Its role is to translate standard commands (from our interfaces) into Govee's specific API language.
308 | *   It uses the configuration ([Chapter 1: Configuration Management](01_configuration_management.md)) for authentication and targeting.
309 | *   It handles formatting requests, sending them via HTTP, and processing responses.
310 | *   It includes essential features like automatic retries and handling network errors (`TimeoutError`, `ConnectionError`).
311 | *   Internally, it uses a helper (`_make_request`) to manage the common parts of API communication.
312 | *   Methods like `set_power` build the specific command payload and rely on `_make_request` to send it.
313 | 
314 | With the configuration loaded, interfaces defined, and the API client ready to communicate, we have the core logic in place. But how does a user actually *trigger* these actions?
315 | 
316 | Next, we'll look at one way users can interact with our application: the command line. Let's move on to [Chapter 4: Command Line Interface (CLI)](04_command_line_interface__cli_.md)!
317 | 
318 | ---
319 | 
320 | Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
```