Add PyPI README files and MCP resource/prompt tests

- README.md for all three packages (core, TUI, MCP)
- pyproject.toml readme field for PyPI rendering
- 8 new tests for MCP resources (5) and prompts (3)
- Total MCP test coverage: 57 tests, 37 tools + 5 resources + 3 prompts
This commit is contained in:
Ryan Malloy 2026-02-17 19:15:23 -07:00
parent 3ccec3be10
commit bf72e784d3
8 changed files with 294 additions and 0 deletions

58
mcp/README.md Normal file
View file

@ -0,0 +1,58 @@
# mcbirdcage
[MCP](https://modelcontextprotocol.io/) server for controlling Winegard satellite dishes through conversational tools. Built on [FastMCP](https://gofastmcp.com/), exposes 36 tools for dish positioning, signal analysis, firmware inspection, and satellite pass planning.
## Install
```bash
# Add to Claude Code
claude mcp add mcbirdcage -- uvx mcbirdcage
# Or run standalone
uvx mcbirdcage
```
## Demo Mode
No dish required. Set `BIRDCAGE_DEMO=1` to get simulated responses for all tools:
```bash
BIRDCAGE_DEMO=1 uvx mcbirdcage
```
## Tools
36 tools across six groups:
| Group | Count | Examples |
|-------|-------|---------|
| Connection | 3 | `connect`, `disconnect`, `status` |
| Movement | 9 | `get_position`, `move_to`, `home_motor`, `stow` |
| Signal | 8 | `get_rssi`, `enable_lna`, `az_sweep`, `get_lock_status` |
| System | 11 | `nvs_dump`, `get_firmware_id`, `set_pid_gains`, `get_a3981_diag` |
| Satellite | 4 | `search_satellites`, `get_passes`, `get_visible_targets` |
| Console | 1 | `send_raw_command` (direct firmware access) |
Plus 5 resources (hardware specs, NVS reference, firmware docs) and 3 prompts.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `BIRDCAGE_DEMO` | `0` | Enable demo mode (no hardware) |
| `BIRDCAGE_PORT` | `/dev/ttyUSB0` | Serial port path |
| `BIRDCAGE_FIRMWARE` | `hal205` | Firmware variant (`hal000`, `hal205`, `g2`) |
| `BIRDCAGE_CRAFT_URL` | -- | Craft API URL for live satellite TLEs |
## Documentation
Full tool reference and hardware setup: **[birdcage.warehack.ing](https://birdcage.warehack.ing)**
## Credits
- **Gabe Emerson (KL1FI / [saveitforparts](https://github.com/saveitforparts))** -- original Winegard rotor scripts
- **Chris Davidson ([cdavidson0522](https://github.com/cdavidson0522))** -- Carryout G2 sky scan and rotator control
## License
MIT

View file

@ -6,6 +6,7 @@ build-backend = "hatchling.build"
name = "mcbirdcage"
version = "2026.02.17"
description = "MCP server for Winegard satellite dish control via serial"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]

40
mcp/tests/test_prompts.py Normal file
View file

@ -0,0 +1,40 @@
"""Tests for MCP prompts (setup_wizard, satellite_tracking_guide, rf_sweep_guide)."""
import pytest
from fastmcp import Client
@pytest.mark.anyio
async def test_setup_wizard_prompt(mcp_client: Client):
result = await mcp_client.get_prompt("setup_wizard")
assert result.messages, "setup_wizard returned no messages"
text = result.messages[0].content.text
assert len(text) > 0
assert "connect" in text.lower()
assert "home_motor" in text
assert "get_position" in text
@pytest.mark.anyio
async def test_satellite_tracking_guide_prompt(mcp_client: Client):
result = await mcp_client.get_prompt("satellite_tracking_guide")
assert result.messages, "satellite_tracking_guide returned no messages"
text = result.messages[0].content.text
assert len(text) > 0
assert "search_satellites" in text
assert "get_passes" in text
assert "move_to" in text
@pytest.mark.anyio
async def test_rf_sweep_guide_prompt(mcp_client: Client):
result = await mcp_client.get_prompt("rf_sweep_guide")
assert result.messages, "rf_sweep_guide returned no messages"
text = result.messages[0].content.text
assert len(text) > 0
assert "enable_lna" in text
assert "get_rssi" in text
assert "az_sweep" in text

View file

@ -0,0 +1,62 @@
"""Tests for MCP resources (config, position, firmware, motor-dynamics, el-limits)."""
import json
import pytest
from fastmcp import Client
@pytest.mark.anyio
async def test_config_resource(mcp_client: Client):
contents = await mcp_client.read_resource("birdcage://config")
data = json.loads(contents[0].text)
assert data["demo_mode"] is True
assert data["connected"] is True
assert data["firmware"] == "g2"
assert data["serial_port"] == "/dev/demo"
@pytest.mark.anyio
async def test_position_resource(mcp_client: Client):
contents = await mcp_client.read_resource("birdcage://position")
data = json.loads(contents[0].text)
assert "azimuth" in data
assert "elevation" in data
assert isinstance(data["azimuth"], float)
assert isinstance(data["elevation"], float)
# DemoDevice starts at AZ=180, EL=45 (approximately, with settling noise).
assert 170.0 < data["azimuth"] < 190.0
assert 35.0 < data["elevation"] < 55.0
@pytest.mark.anyio
async def test_firmware_resource(mcp_client: Client):
contents = await mcp_client.read_resource("birdcage://firmware")
text = contents[0].text
assert "02.02.48" in text
assert "TWELINCH" in text
assert "K60-144pin" in text
@pytest.mark.anyio
async def test_motor_dynamics_resource(mcp_client: Client):
contents = await mcp_client.read_resource("birdcage://motor-dynamics")
data = json.loads(contents[0].text)
assert data["az_max_vel"] == 65.0
assert data["el_max_vel"] == 45.0
assert data["az_accel"] == 400.0
assert data["el_accel"] == 400.0
@pytest.mark.anyio
async def test_el_limits_resource(mcp_client: Client):
contents = await mcp_client.read_resource("birdcage://el-limits")
data = json.loads(contents[0].text)
assert data["min"] == 18.0
assert data["max"] == 65.0
assert data["home"] == 65.0