jumperless-mcp 0.1.0

MCP server for the Jumperless V5 — persistent USB-serial bridge exposing the firmware API to LLMs
# pyright: reportMissingImports=false, reportAttributeAccessIssue=false, reportUndefinedVariable=false
"""
Jumperless V5 firmware identification probe — embedded copy.

NOTE: This file is NOT installed to the device. It is exec'd transiently on
each `jumperless-mcp firmware-probe` invocation via the MicroPython Raw REPL,
then discarded. The device filesystem is not modified.

Host-side embedding: `crate::probe::FIRMWARE_PROBE_SCRIPT` via include_str!.

Run this on the device to determine which firmware codebase is shipping:
- `RP23V50firmware/` (stable) — lacks `force_service` binding
- `JumperlOS/` (in-progress) — has `force_service`, `jOS.*`, cooperative-yield primitives

The probe is purely diagnostic. It does NOT modify device state — no jfs writes,
no nodes_save, no overlay paints.
"""

import sys

def _safe(label, fn):
    """Call fn(), catch any exception, format result for the report."""
    try:
        return f"  {label}: {fn()!r}"
    except Exception as e:
        return f"  {label}: <error: {type(e).__name__}: {e}>"


print("=" * 60)
print("Jumperless V5 firmware probe")
print("=" * 60)

# --- Section 1: System / interpreter identity -------------------------------
print("\n[1] System identity")
print(_safe("sys.platform",     lambda: sys.platform))
print(_safe("sys.implementation", lambda: sys.implementation))
print(_safe("sys.version",      lambda: sys.version))

try:
    import os
    print(_safe("os.uname",         lambda: os.uname()))
except Exception as e:
    print(f"  os.uname: <error: {type(e).__name__}: {e}>")


# --- Section 2: The gating check --------------------------------------------
# `force_service` is the JumperlOS cooperative-yield primitive.
# Per scout: bound in JumperlOS/src/JumperlessMicroPythonAPI.cpp:1775,
# NOT bound in RP23V50firmware/src/.
print("\n[2] Gating check: cooperative-yield primitive")
try:
    import jumperless as j
    has_force = hasattr(j, "force_service")
    print(f"  jumperless.force_service present: {has_force}")
    if has_force:
        print("  -> Verdict: likely JumperlOS firmware")
    else:
        print("  -> Verdict: likely RP23V50firmware (stable)")
except Exception as e:
    print(f"  <error importing jumperless: {type(e).__name__}: {e}>")
    raise SystemExit(1)


# --- Section 3: Key API surface fingerprint ---------------------------------
print("\n[3] API surface fingerprint")
indicators = [
    # Loop / service primitives (JumperlOS markers)
    "force_service",
    "jOS",
    # Core breadboard control (both)
    "connect",
    "disconnect",
    "nodes_save",
    # Color / overlay
    "set_net_color",
    "set_net_color_hsv",
    "overlay_set_pixel",
    "overlay_clear_all",
    # App dispatcher
    "run_app",
    # Probe / clickwheel (used by interaction_demo)
    "probe_read",
    "probe_read_nonblocking",
    "clickwheel_get_press",
    "clickwheel_get_rotation",
    # Hardware state queries
    "wavegen_is_running",
    "la_is_capturing",
    # Filesystem facade (jfs is a submodule, but some firmware exposes it here too)
    "jfs",
]
for name in indicators:
    print(f"  jumperless.{name}: {hasattr(j, name)}")


# --- Section 4: JFS submodule surface ---------------------------------------
print("\n[4] jumperless.jfs submodule")
try:
    from jumperless import jfs
    jfs_attrs = [
        "open", "read", "write", "close", "flush", "sync",
        "remove", "rename", "mkdir", "rmdir",
        "stat", "exists", "info", "available", "listdir",
    ]
    for attr in jfs_attrs:
        print(f"  jfs.{attr}: {hasattr(jfs, attr)}")
except Exception as e:
    print(f"  <error: {type(e).__name__}: {e}>")


# --- Section 5: jOS namespace (JumperlOS-specific) --------------------------
print("\n[5] jOS namespace (JumperlOS marker)")
try:
    has_jOS = hasattr(j, "jOS")
    print(f"  jumperless.jOS present: {has_jOS}")
    if has_jOS:
        jOS_attrs = sorted(a for a in dir(j.jOS) if not a.startswith("_"))
        print(f"  jOS attr count: {len(jOS_attrs)}")
        for a in jOS_attrs:
            print(f"    jOS.{a}")
except Exception as e:
    print(f"  <error: {type(e).__name__}: {e}>")


# --- Section 6: Script directory hints --------------------------------------
# JumperlOS ships example scripts (interaction_demo.py, stylophone, etc.)
# in /python_scripts/ex/. Their presence is a strong JumperlOS marker.
print("\n[6] Filesystem script-directory hints")
hint_paths = [
    "/python_scripts/",
    "/python_scripts/ex/",
    "/python_scripts/ex/interaction_demo.py",
    "/python_scripts/ex/stylophone.py",
    "/python_scripts/lib/",
    "/python_scripts/lib/jumperless_mcp/",   # our v0.2.0 install
    "/python_scripts/lib/jumperless_mcp/VERSION",
    "/boot.py",
    "/main.py",
]
try:
    from jumperless import jfs
    for p in hint_paths:
        try:
            exists = jfs.exists(p)
            print(f"  {p}: {exists}")
        except Exception as e:
            print(f"  {p}: <error: {type(e).__name__}: {e}>")
except Exception as e:
    print(f"  <jfs unavailable for path checks: {type(e).__name__}: {e}>")


# --- Section 7: Full dir(jumperless) for diffing ----------------------------
print("\n[7] Full public dir(jumperless)")
try:
    attrs = sorted(a for a in dir(j) if not a.startswith("_"))
    print(f"  count: {len(attrs)}")
    for a in attrs:
        print(f"  {a}")
except Exception as e:
    print(f"  <error: {type(e).__name__}: {e}>")


# --- Section 8: Resident library version (our install) ----------------------
print("\n[8] Resident jumperless_mcp library")
try:
    from jumperless import jfs as _jfs
    if _jfs.exists("/python_scripts/lib/jumperless_mcp/VERSION"):
        _f = _jfs.open("/python_scripts/lib/jumperless_mcp/VERSION", "r")
        try:
            _v = _jfs.read(_f, 256)
            if isinstance(_v, bytes):
                _v = _v.decode("utf-8", "replace")
            print(f"  VERSION file: {_v.strip()!r}")
        finally:
            _jfs.close(_f)
    else:
        print("  VERSION file: <not installed>")
except Exception as e:
    print(f"  <error: {type(e).__name__}: {e}>")


# --- Section 9: jOS namespace clarification --------------------------------
# When `jumperless.jOS` is absent (per Section 5), this section clarifies
# whether `jOS` is exposed differently (top-level module, attribute of
# force_service, etc.).
print("\n[9] jOS namespace clarification")

# 9a: Is jOS a top-level module?
try:
    import jOS as _jOS_top  # noqa
    _attrs = sorted(a for a in dir(_jOS_top) if not a.startswith("_"))
    print(f"  import jOS: OK ({type(_jOS_top).__name__}, {len(_attrs)} public attrs)")
    for _a in _attrs[:40]:
        print(f"    jOS.{_a}")
    if len(_attrs) > 40:
        print(f"    ... (+{len(_attrs) - 40} more)")
except ImportError as _e:
    print(f"  import jOS: FAIL ({_e})")
except Exception as _e:
    print(f"  import jOS: unexpected {type(_e).__name__}: {_e}")

# 9b: What kind of object is force_service?
_fs = getattr(j, "force_service", None)
if _fs is not None:
    print(f"  type(force_service): {type(_fs).__name__}")
    print(f"  callable: {callable(_fs)}")
    _fs_attrs = [a for a in dir(_fs) if not a.startswith("_")]
    if _fs_attrs:
        print(f"  force_service attrs: {_fs_attrs}")

# 9c: sys.modules — anything jOS-flavored?
import sys as _sys
_jOS_keys = [k for k in _sys.modules.keys() if "jOS" in k or "jos" in k.lower()]
print(f"  sys.modules jOS matches: {_jOS_keys}")

# 9d: Try j.help() — Jumperless built-in help. Wrap to catch any output side effects.
print("  j.help() output:")
try:
    _help = getattr(j, "help", None)
    if callable(_help):
        _help()
    elif _help is not None:
        print(f"    {_help!r}")
    else:
        print("    j.help unavailable")
except Exception as _e:
    print(f"    j.help error: {type(_e).__name__}: {_e}")


print("\n" + "=" * 60)
print("Probe complete")
print("=" * 60)