"""Configuration tools for the ESP32 test harness.""" from __future__ import annotations from typing import Any from fastmcp import FastMCP from ..protocol import CMD_CLASSIC_SET_SSP_MODE, CMD_CONFIGURE, IOCapability, Status from ..serial_client import NotConnected, get_client def register_tools(mcp: FastMCP) -> None: """Register configuration tools with the MCP server.""" @mcp.tool() async def esp32_configure( name: str | None = None, io_cap: str | None = None, device_class: int | None = None, pin_code: str | None = None, ) -> dict[str, Any]: """Configure the ESP32 Bluetooth device settings. Any parameter left as None is not sent, so the ESP32 keeps its current value for that setting. Combine multiple parameters in a single call to apply them atomically. Args: name: Bluetooth friendly name broadcast during discovery. io_cap: I/O capability for pairing negotiation. One of: "display_only", "display_yesno", "keyboard_only", "no_io", "keyboard_display". device_class: Bluetooth Class of Device (CoD) value. Controls how remote scanners categorise this device (e.g. 0x200408 for an audio headset). pin_code: Legacy PIN code for PIN-based pairing (typically a 4-digit string like "1234"). Returns: Response data confirming the applied configuration, or an error dict on failure. """ params: dict[str, Any] = {} if name is not None: params["name"] = name if io_cap is not None: # Validate against known capability values valid_caps = {cap.value for cap in IOCapability} if io_cap not in valid_caps: return {"error": f"Invalid io_cap '{io_cap}'. Must be one of: {sorted(valid_caps)}"} params["io_cap"] = io_cap if device_class is not None: params["device_class"] = device_class if pin_code is not None: params["pin_code"] = pin_code if not params: return {"error": "No configuration parameters provided"} try: client = get_client() resp = await client.send_command(CMD_CONFIGURE, params) if resp.status == Status.OK: return resp.data return {"error": resp.data.get("error", "Configuration failed")} except NotConnected: return {"error": "Not connected to ESP32"} except Exception as e: return {"error": str(e)} @mcp.tool() async def esp32_set_ssp_mode( mode: str, auto_accept: bool = False, ) -> dict[str, Any]: """Set the Secure Simple Pairing (SSP) mode on the ESP32. SSP mode determines which pairing association model the ESP32 will use when a remote device initiates Classic Bluetooth pairing. Args: mode: SSP association model. One of: "just_works" — no user interaction, lowest security. "numeric_comparison" — both sides display a 6-digit code for the user to confirm they match. "passkey_entry" — remote device must type a passkey shown on the ESP32 (requires display_only or keyboard_display io_cap). "passkey_display" — ESP32 displays a passkey for the remote device to enter. auto_accept: When true, the ESP32 automatically confirms numeric comparison pairing requests without waiting for a classic_pair_respond command. Useful for automated E2E testing where both sides need to confirm but the MCP tool calls are sequential. Returns: Response data confirming the new SSP mode, or an error dict on failure. """ valid_modes = {"just_works", "numeric_comparison", "passkey_entry", "passkey_display"} if mode not in valid_modes: return {"error": f"Invalid SSP mode '{mode}'. Must be one of: {sorted(valid_modes)}"} params: dict[str, Any] = {"mode": mode} if auto_accept: params["auto_accept"] = True try: client = get_client() resp = await client.send_command(CMD_CLASSIC_SET_SSP_MODE, params) if resp.status == Status.OK: return resp.data return {"error": resp.data.get("error", "Failed to set SSP mode")} except NotConnected: return {"error": "Not connected to ESP32"} except Exception as e: return {"error": str(e)}