Skip to main content
Glama

Govee MCP Server

by mathd
# Chapter 6: Custom Error Handling - Specific Warning Lights 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. 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. This chapter is about how `govee_mcp_server` uses **Custom Error Handling** to provide specific, helpful information when problems occur. ## What's the Big Idea? Knowing *Why* Things Broke 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. 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. **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: * "Error talking to the Govee API!" (`GoveeAPIError`) * "Error: Your API key is missing or wrong!" (`GoveeConfigError`) * "Error: That's not a valid color value!" (`GoveeValidationError`) * "Error: Couldn't connect to the Govee servers!" (`GoveeConnectionError`) * "Error: Govee took too long to reply!" (`GoveeTimeoutError`) 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. ## Our Custom Warning Lights: The Exception Classes 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. 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. ```python # Simplified from src/govee_mcp_server/exceptions.py class GoveeError(Exception): """Base exception for all Govee-related errors in this project.""" pass # It's just a label, inheriting basic Exception features class GoveeAPIError(GoveeError): """Problem communicating with the Govee API itself (e.g., Govee returned an error message like 'Invalid API Key').""" pass class GoveeConfigError(GoveeError): """Problem with the configuration settings (e.g., missing GOVEE_API_KEY in the .env file).""" pass class GoveeValidationError(GoveeError): """Problem with the data provided by the user/client (e.g., brightness=150, or invalid RGB values).""" pass class GoveeConnectionError(GoveeError): """Problem with the network connection to Govee servers.""" pass class GoveeTimeoutError(GoveeError): """Govee's servers took too long to respond to our request.""" pass ``` **Explanation:** * `class GoveeError(Exception):`: This defines our main category. Anything that's a `GoveeConfigError` is *also* a `GoveeError`. * 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. ## Turning On the Warning Lights: Raising Exceptions Different parts of our code are responsible for detecting specific problems and `raise`-ing the appropriate custom exception. **Example 1: Missing Configuration (`load_config`)** 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`. ```python # Simplified from src/govee_mcp_server/config.py from .exceptions import GoveeConfigError # Import the specific error # ... other imports ... def load_config(): # ... code to load api_key, device_id, sku ... api_key = None # Let's pretend API key wasn't found if not api_key: # Check if it's missing # Raise the specific configuration error! raise GoveeConfigError("Missing required environment variable: GOVEE_API_KEY") # ... return config object if all is well ... ``` **Explanation:** * If `api_key` is not found, `raise GoveeConfigError(...)` immediately stops the `load_config` function and signals this specific problem. **Example 2: Invalid Color Input (`validate_rgb`)** 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. ```python # Simplified from src/govee_mcp_server/interfaces.py from .exceptions import GoveeValidationError # Import the specific error # ... other imports ... def validate_rgb(func): async def wrapper(self, r: int, g: int, b: int, *args, **kwargs): if not (0 <= r <= 255): # Check if Red value is valid # Raise the specific validation error! raise GoveeValidationError("red value must be between 0-255") # ... check g and b ... # If all good, call the original function (like set_color) return await func(self, r, g, b, *args, **kwargs) return wrapper class ColorControl(ABC): @abstractmethod @validate_rgb # Apply the validator async def set_color(self, r: int, g: int, b: int) -> Tuple[bool, str]: pass ``` **Explanation:** * If `r` is outside the 0-255 range, `raise GoveeValidationError(...)` stops the process *before* the actual `set_color` logic in `GoveeAPI` runs. **Example 3: API Communication Problems (`_make_request`)** The core `_make_request` function inside the [Govee API Client](03_govee_api_client.md) handles errors from the Govee servers or network issues. ```python # Simplified from src/govee_mcp_server/api.py import aiohttp import asyncio from .exceptions import GoveeAPIError, GoveeConnectionError, GoveeTimeoutError class GoveeAPI: # ... other methods ... async def _make_request(self, method: str, endpoint: str, **kwargs): # ... setup ... for attempt in range(self.MAX_RETRIES): try: async with self.session.request(...) as response: data = await response.json() if response.status == 401: # Example: Unauthorized # Raise specific API error! raise GoveeAPIError(f"API error: 401 - Invalid API Key") elif response.status != 200: # Raise general API error for other bad statuses raise GoveeAPIError(f"API error: {response.status} - {data.get('message')}") # ... success path ... return data, data.get('message', 'Success') except asyncio.TimeoutError: if attempt == self.MAX_RETRIES - 1: # Raise specific timeout error! raise GoveeTimeoutError("Request timed out") except aiohttp.ClientError as e: if attempt == self.MAX_RETRIES - 1: # Raise specific connection error! raise GoveeConnectionError(f"Connection error: {e}") # ... retry logic ... raise GoveeAPIError("Max retries exceeded") # If all retries fail ``` **Explanation:** * Based on the HTTP status code from Govee (like 401) or network exceptions (`TimeoutError`, `ClientError`), this code raises the corresponding specific `GoveeError`. ## Reacting to Warning Lights: Catching Exceptions 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. **Example: Handling Errors in the CLI** 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. ```python # Simplified from src/govee_mcp_server/cli.py import sys from .exceptions import GoveeError, GoveeConfigError, GoveeValidationError # ... other imports ... async def main() -> None: api = None try: # --- Operations that might raise GoveeErrors --- config = load_config() # Might raise GoveeConfigError api = GoveeAPI(config) args = parser.parse_args() if args.command == 'color': # Might raise GoveeValidationError (from decorator) # or GoveeAPIError, etc. (from api.set_color) await handle_color(api, args.red, args.green, args.blue) # ... other command handlers ... # --- End of operations --- # --- Catching specific errors --- except GoveeConfigError as e: print(f"\nConfiguration Problem: {str(e)}") print("Please check your .env file or environment variables.") sys.exit(1) # Exit with an error status except GoveeValidationError as e: print(f"\nInvalid Input: {str(e)}") sys.exit(1) except GoveeError as e: # Catch any other Govee-specific error print(f"\nOperation Failed: {str(e)}") sys.exit(1) except Exception as e: # Catch any totally unexpected error print(f"\nAn Unexpected Error Occurred: {str(e)}") sys.exit(1) finally: # Cleanup runs whether there was an error or not if api: await api.close() # ... function to run main ... ``` **Explanation:** * The code inside the `try` block is executed. * 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. * If `handle_color` (or the underlying `api.set_color`) raises a `GoveeValidationError`, it jumps to the `except GoveeValidationError as e:` block. * If any *other* type of `GoveeError` occurs (like `GoveeAPIError`, `GoveeTimeoutError`), it's caught by the more general `except GoveeError as e:` block. * This allows the CLI to give tailored feedback based on *what kind* of error happened. 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. ## Under the Hood: Tracing an "Invalid API Key" Error Let's follow the journey of an error when the user tries to run the CLI with a wrong API key: 1. **User Runs:** `govee-cli power on` 2. **CLI Starts:** `cli.py` `main()` function begins. 3. **Load Config:** `load_config()` runs successfully (it finds *an* API key, just the wrong one). 4. **Create API Client:** `api = GoveeAPI(config)` is created. 5. **Call Handler:** `handle_power(api, 'on')` is called. 6. **Call API Method:** `await api.set_power(True)` is called. 7. **Make Request:** Inside `set_power`, `await self._make_request(...)` is called. 8. **Send to Govee:** `_make_request` sends the command to the Govee Cloud API, including the *invalid* API key in the headers. 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"}`). 10. **Detect Error:** `_make_request` receives the 401 status. 11. **Raise Specific Error:** It executes `raise GoveeAPIError("API error: 401 - Invalid API Key")`. This stops `_make_request`. 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. 13. **Catch in Handler:** The `try...except` block in `handle_power` catches the `GoveeAPIError`. 14. **Raise from Handler:** The handler re-raises the error: `raise GoveeError(message)` (or simply lets the original `GoveeAPIError` propagate). 15. **Catch in Main:** The main `try...except` block in `cli.py` `main()` catches the `GoveeError` (since `GoveeAPIError` is a type of `GoveeError`). 16. **Print Message:** The code inside `except GoveeError as e:` runs, printing: `Operation Failed: API error: 401 - Invalid API Key` 17. **Exit:** The CLI exits with an error code (`sys.exit(1)`). ```mermaid sequenceDiagram participant User participant CLI as cli.py main() participant Handler as handle_power() participant API as GoveeAPI.set_power() participant Request as GoveeAPI._make_request() participant GoveeCloud User->>CLI: Run `govee-cli power on` CLI->>Handler: Call handle_power() Handler->>API: Call api.set_power(True) API->>Request: Call _make_request() Request->>GoveeCloud: Send request (with bad API key) GoveeCloud-->>Request: Respond: HTTP 401 Unauthorized Request-->>Request: Detect 401 status Request-->>API: raise GoveeAPIError("Invalid API Key") API-->>Handler: Error propagates up Handler-->>CLI: Error propagates up CLI-->>CLI: Catch GoveeAPIError (as GoveeError) CLI->>User: Print "Operation Failed: API error: 401 - Invalid API Key" CLI->>User: Exit with error code ``` 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. ## Conclusion You've reached the end of the `govee_mcp_server` tutorial! In this chapter, we learned about **Custom Error Handling**: * It's like having specific **warning lights** instead of one generic "Check Engine" light. * We defined our own exception classes (`GoveeAPIError`, `GoveeConfigError`, `GoveeValidationError`, etc.) inheriting from a base `GoveeError`. * Different parts of the code `raise` these specific errors when problems are detected (e.g., bad config, invalid input, API failure). * 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. * This makes debugging easier and the application more user-friendly. 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. 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! --- Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mathd/govee_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server