From f5a88ca0ad77ffac6a5b9d6d14696ded1263c48b Mon Sep 17 00:00:00 2001 From: Edward Viaene Date: Fri, 13 Nov 2026 22:22:46 -0600 Subject: [PATCH] Status too large for the cloud: a clear error, server capabilities, MCP keeps the connection (SDK + MCP 2.13.15) Firmware 3.10.4's status could be larger than the cloud channel takes, so a cloud connect failed with "status: reply too large for the cloud channel". The SDK's error now names the fix (firmware 3.10.5), and bp.capabilities falls back to the server's capability set and keeps it rather than reading the status again on every access. The MCP status/connect report the firmware fields as unknown with a warning instead of failing the connect. Firmware 3.10.5 sends the short form over the cloud ("short_form": true), without lan_tls: the MCP status fills firmware.lan_tls from the lan_tls command there (the desktop Pod Settings reads it). Co-Authored-By: Claude Opus 5.5 --- packages/embeddedci-mcp/CHANGELOG.md | 8 +++++++ packages/embeddedci-mcp/pyproject.toml | 4 ++-- packages/embeddedci-mcp/server.json | 4 ++-- .../src/embeddedci_mcp/server.py | 20 +++++++++++++++- packages/embeddedci-mcp/tests/test_tools.py | 23 +++++++++++++++++++ packages/embeddedci/CHANGELOG.md | 9 ++++++++ packages/embeddedci/pyproject.toml | 2 +- .../src/embeddedci/benchpod/client.py | 8 ++++++- .../src/embeddedci/benchpod/errors.py | 14 +++++++++++ .../embeddedci/tests/test_refusal_errors.py | 9 ++++++++ 10 files changed, 94 insertions(+), 7 deletions(-) diff --git a/packages/embeddedci-mcp/CHANGELOG.md b/packages/embeddedci-mcp/CHANGELOG.md index a42aa42..2bdb355 100644 --- a/packages/embeddedci-mcp/CHANGELOG.md +++ b/packages/embeddedci-mcp/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog — `embeddedci-mcp` +## 2.13.15 + +- `connect` over the cloud to a pod with firmware 3.10.4 works again: its `status` was too large + for the cloud channel, which failed the connect. `status` now reports the firmware fields as + unknown with a warning to update to firmware 3.10.5. With firmware 3.10.5's short cloud status, + `status` fills `firmware.lan_tls` from the `lan_tls` command. +- Needs embeddedci 2.13.15. + ## 2.13.14 - Pin directions are on by default: `connect` and `set_wiring` send the wiring profile's DUT diff --git a/packages/embeddedci-mcp/pyproject.toml b/packages/embeddedci-mcp/pyproject.toml index 51380af..b81d9bf 100644 --- a/packages/embeddedci-mcp/pyproject.toml +++ b/packages/embeddedci-mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "embeddedci-mcp" -version = "2.13.14" +version = "2.13.15" description = "MCP server that lets AI agents drive an EmbeddedCI BenchPod hardware-in-the-loop bench" readme = "README.md" requires-python = ">=3.10" @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ # cloud: the embeddedci: destination; discovery: connect("discover") over mDNS; # analysis: dominant frequency in capture summaries; yaml: sensor definitions an agent writes. - "embeddedci[cloud,discovery,analysis,yaml]>=2.13.14,<3", + "embeddedci[cloud,discovery,analysis,yaml]>=2.13.15,<3", "mcp>=1.12,<2", "anyio>=4", "pydantic>=2.7", diff --git a/packages/embeddedci-mcp/server.json b/packages/embeddedci-mcp/server.json index b03c849..aa0bb02 100644 --- a/packages/embeddedci-mcp/server.json +++ b/packages/embeddedci-mcp/server.json @@ -3,7 +3,7 @@ "name": "io.github.embeddedci-com/embeddedci-mcp", "title": "EmbeddedCI BenchPod", "description": "Drive a BenchPod hardware-in-the-loop bench: power, flash, UART, I2C, CAN, analog and logic.", - "version": "2.13.14", + "version": "2.13.15", "websiteUrl": "https://www.embeddedci.com/docs/benchpod-mcp", "repository": { "url": "https://github.com/embeddedci-com/embeddedci-python", @@ -15,7 +15,7 @@ "registryType": "pypi", "registryBaseUrl": "https://pypi.org", "identifier": "embeddedci-mcp", - "version": "2.13.14", + "version": "2.13.15", "runtimeHint": "uvx", "transport": { "type": "stdio" diff --git a/packages/embeddedci-mcp/src/embeddedci_mcp/server.py b/packages/embeddedci-mcp/src/embeddedci_mcp/server.py index 0a2f9d7..7f4ee9f 100644 --- a/packages/embeddedci-mcp/src/embeddedci_mcp/server.py +++ b/packages/embeddedci-mcp/src/embeddedci_mcp/server.py @@ -62,6 +62,7 @@ from embeddedci.benchpod.cloud_auth import DEFAULT_API_BASE from embeddedci.benchpod.connection import CLOUD_PREFIX from embeddedci.benchpod import errors as _errors +from embeddedci.benchpod.errors import STATUS_FITS_FIRMWARE, FirmwareError, status_too_large from embeddedci.benchpod.errors import BenchPodError from embeddedci.benchpod.server_api import ServerApi @@ -317,8 +318,25 @@ def _status() -> m.StatusResult: "else pass a host, 'usb' or 'embeddedci:'") return m.StatusResult(connected=False, warnings=[f"not connected — {hint}"]) pod = SESSION.require() - firmware = pod.status() warnings: List[str] = [] + try: + firmware = pod.status() + except FirmwareError as exc: + # Firmware 3.10.4's status can be too large for the cloud channel. The connection works: + # report the status as unknown with the fix, rather than failing the connect. + if not status_too_large(exc.firmware_message, exc.cmd): + raise + firmware = {} + warnings.append(f"this pod's status is too large for the cloud channel (firmware " + f"{pod.capabilities.firmware_version or 'older'}): update the pod to firmware " + f"{STATUS_FITS_FIRMWARE} or later; the status fields are unknown until then") + if isinstance(firmware, dict) and firmware.get("short_form") and "lan_tls" not in firmware: + # The cloud status is the short form (firmware 3.10.5+): lan_tls is its own command there. + try: + tls = pod.lan_tls() + firmware["lan_tls"] = {"port": tls.port, "https_only": tls.https_only, "sha256": tls.sha256} + except BenchPodError: + pass # From this status, not the cached capabilities: it describes the boot running now. boot = Capabilities.from_status(firmware).boot_warning() if boot: diff --git a/packages/embeddedci-mcp/tests/test_tools.py b/packages/embeddedci-mcp/tests/test_tools.py index 03ace47..047bce5 100644 --- a/packages/embeddedci-mcp/tests/test_tools.py +++ b/packages/embeddedci-mcp/tests/test_tools.py @@ -121,6 +121,29 @@ def test_status_warns_about_safe_mode_and_crashes(fake_open, fake_transport, mon assert call("status")["warnings"] == [] +def test_status_too_large_for_the_cloud_keeps_the_connection(fake_open, fake_transport, monkeypatch): + # Firmware 3.10.4 over the cloud: its status did not fit the cloud channel and connect failed. + from embeddedci.benchpod.errors import FirmwareError + + def too_large(): + raise FirmwareError("reply too large for the cloud channel (1838 bytes, limit 1791)", + cmd="status") + monkeypatch.setattr(fake_transport, "status", too_large) + result = call("connect", connection="192.168.1.50", la_voltage=3.3) + assert result["connected"] is True and result["firmware"] == {} + assert any("too large for the cloud channel" in w and "3.10.5" in w for w in result["warnings"]) + + +def test_short_form_status_gets_lan_tls_from_its_command(fake_open, fake_transport, monkeypatch): + # Firmware 3.10.5's cloud status is the short form: lan_tls is its own command there. + base = fake_transport.status + monkeypatch.setattr(fake_transport, "status", lambda: {**base(), "short_form": True}) + fake_transport._cmd_lan_tls = lambda req: {"port": 8443, "https_only": True, "sessions": 0, + "sha256": "ab" * 32} + result = call("connect", connection="192.168.1.50", la_voltage=3.3) + assert result["firmware"]["lan_tls"] == {"port": 8443, "https_only": True, "sha256": "ab" * 32} + + def test_connect_without_la_voltage_warns(fake_open): result = call("connect", connection="usb") assert result["kind"] == "serial" and result["la_voltage"] is None diff --git a/packages/embeddedci/CHANGELOG.md b/packages/embeddedci/CHANGELOG.md index b680c24..020ddba 100644 --- a/packages/embeddedci/CHANGELOG.md +++ b/packages/embeddedci/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog — `embeddedci` +## 2.13.15 + +- A pod whose `status` is too large for the cloud channel (firmware 3.10.4 with the USB network + adapter up and a lease held) no longer leaves the SDK guessing: the error names the fix, + firmware 3.10.5, and `bp.capabilities` uses the server's capability set instead of reading the + status again on every access. Firmware 3.10.5 sends a short `status` over the cloud + (`"short_form": true`); it leaves out `lan_tls` (use `bp.lan_tls()`), `lease`, the diagnostics + and the fixed feature flags the server already has. + ## 2.13.14 - Pin directions are on by default. On a pod with firmware 3.10.4+ the SDK sends the wiring diff --git a/packages/embeddedci/pyproject.toml b/packages/embeddedci/pyproject.toml index 03bb293..893a4a9 100644 --- a/packages/embeddedci/pyproject.toml +++ b/packages/embeddedci/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "embeddedci" -version = "2.13.14" +version = "2.13.15" description = "BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing" readme = "README.md" requires-python = ">=3.10" diff --git a/packages/embeddedci/src/embeddedci/benchpod/client.py b/packages/embeddedci/src/embeddedci/benchpod/client.py index f184c3e..48cc215 100644 --- a/packages/embeddedci/src/embeddedci/benchpod/client.py +++ b/packages/embeddedci/src/embeddedci/benchpod/client.py @@ -91,7 +91,8 @@ coerce_pin, ) from .connection import resolve_connection -from .errors import BenchPodError, FirmwareError, UnsupportedFeatureError, classify_firmware_error +from .errors import (BenchPodError, FirmwareError, UnsupportedFeatureError, classify_firmware_error, + status_too_large) from .flash import FlashResult from .gpio import LA_DIRS, GpioPin, LaPinState from .lease import DEFAULT_LEASE_TTL, DEFAULT_LEASE_WAIT, DeviceLease @@ -399,6 +400,11 @@ def capabilities(self) -> Capabilities: status_ok = True try: status = self.status() + except FirmwareError as exc: + # Firmware 3.10.4's status can be too large for the cloud channel. Reading it again + # will not help: use the server's capabilities alone, and keep them. + _log.warning("could not read the pod's status for its capabilities: %s", exc) + status, status_ok = {}, status_too_large(exc.firmware_message, exc.cmd) except BenchPodError as exc: _log.warning("could not read the pod's status for its capabilities: %s", exc) status, status_ok = {}, False diff --git a/packages/embeddedci/src/embeddedci/benchpod/errors.py b/packages/embeddedci/src/embeddedci/benchpod/errors.py index 4e4ed26..cbbbf49 100644 --- a/packages/embeddedci/src/embeddedci/benchpod/errors.py +++ b/packages/embeddedci/src/embeddedci/benchpod/errors.py @@ -75,6 +75,9 @@ def __init__(self, message: str, *, cmd: Optional[str] = None) -> None: self.firmware_message = message self.cmd = cmd text = _sdk_refusal_text(message) + if status_too_large(message, cmd): + text += (f"; firmware {STATUS_FITS_FIRMWARE} and later send a status that fits: update " + "the pod (`benchpod flash-self`, or the firmware update in the web app)") if cmd: super().__init__(f"{cmd}: {text}") else: @@ -269,6 +272,17 @@ def __init__(self, message: str, *, cmd: Optional[str] = None, #: The firmware's "la voltage not set; set it with la_voltage (mv 1800 or 3300) first" #: (command_handler.c require_la_voltage). Matched on the leading words only. _LA_VOLTAGE_UNSET = "la voltage not set" + +#: Firmware 3.10.4 and older could answer ``status`` over the cloud with more than the cloud +#: channel takes ("reply too large for the cloud channel (1838 bytes, limit 1791)"). From this +#: version a cloud ``status`` is a short form that always fits. +STATUS_FITS_FIRMWARE = "3.10.5" +_STATUS_TOO_LARGE = "too large for the cloud channel" + + +def status_too_large(message: str, cmd: Optional[str] = "status") -> bool: + """Whether a pod refusal is a ``status`` reply that did not fit the cloud channel.""" + return cmd == "status" and _STATUS_TOO_LARGE in message _LA_VOLTAGE_SDK = ("la voltage not set; set the board's I/O voltage first: " "BenchPod(conn, la_voltage=3.3) (1.8 for a 1.8 V target), the " "benchpod_la_voltage fixture in conftest.py, or bp.set_la_voltage(3.3)") diff --git a/packages/embeddedci/tests/test_refusal_errors.py b/packages/embeddedci/tests/test_refusal_errors.py index c5177c4..304e232 100644 --- a/packages/embeddedci/tests/test_refusal_errors.py +++ b/packages/embeddedci/tests/test_refusal_errors.py @@ -52,6 +52,15 @@ def test_leased_carries_holder_and_time_left(): assert err.firmware_message.startswith("busy:") and err.cmd == "gpio" +def test_status_too_large_for_the_cloud_names_the_fix(): + err = firmware_error("reply too large for the cloud channel (1838 bytes, limit 1791)", cmd="status") + assert isinstance(err, FirmwareError) + assert "too large for the cloud channel" in str(err) and "firmware 3.10.5" in str(err) + # Other commands' size refusals are left as they are. + other = firmware_error("reply too large for the cloud channel (1900 bytes, limit 1791)", cmd="la_pins") + assert "3.10.5" not in str(other) + + def test_busy_without_a_lease(): err = firmware_error("busy: a capture or upload is running") assert type(err) is PodBusyError