Rename packages for PyPI: winegard-birdcage + mcbirdcage

Core library published as winegard-birdcage (import stays `birdcage`).
MCP server renamed birdcage-mcp → mcbirdcage, matching mcserial pattern.
Both packages live on PyPI — `uvx mcbirdcage` works out of the box.
This commit is contained in:
Ryan Malloy 2026-02-17 18:18:31 -07:00
parent b98a5482fa
commit 5a26dce075
20 changed files with 122 additions and 92 deletions

View File

@ -772,9 +772,9 @@ directly via widget API for deterministic screenshots.
| Property | Value |
|----------|-------|
| Package | `birdcage-mcp` |
| Source | `mcp/src/birdcage_mcp/` |
| Entry point | `birdcage_mcp.server:main` |
| PyPI | [`mcbirdcage`](https://pypi.org/project/mcbirdcage/) |
| Source | `mcp/src/mcbirdcage/` |
| Entry point | `mcbirdcage.server:main` |
| Tools | 35 (connection, movement, signal, system, satellite, console) |
| Resources | 5 (`birdcage://config`, `position`, `firmware`, `motor-dynamics`, `el-limits`) |
| Prompts | 3 (`setup_wizard`, `satellite_tracking_guide`, `rf_sweep_guide`) |
@ -783,17 +783,25 @@ directly via widget API for deterministic screenshots.
### Running
```bash
# Demo mode (no hardware)
BIRDCAGE_DEMO=1 uv run --directory mcp birdcage-mcp
# Published package (from PyPI) — no clone needed
uvx mcbirdcage # demo mode auto-detected
BIRDCAGE_DEMO=1 uvx mcbirdcage # explicit demo mode
# Local development (from repo)
BIRDCAGE_DEMO=1 uv run --directory mcp mcbirdcage
# Hardware mode
BIRDCAGE_PORT=/dev/ttyUSB2 uv run --directory mcp birdcage-mcp
BIRDCAGE_PORT=/dev/ttyUSB2 uv run --directory mcp mcbirdcage
```
### Adding to Claude Code
```bash
claude mcp add birdcage-mcp -- env BIRDCAGE_DEMO=1 uv run --directory mcp birdcage-mcp
# Published package (recommended)
claude mcp add mcbirdcage -- uvx mcbirdcage
# Local development
claude mcp add mcbirdcage -- env BIRDCAGE_DEMO=1 uv run --directory mcp mcbirdcage
```
### Environment Variables
@ -809,5 +817,5 @@ claude mcp add birdcage-mcp -- env BIRDCAGE_DEMO=1 uv run --directory mcp birdca
```bash
cd mcp && uv run pytest tests/ # 49 tests via FastMCP run_server_async
uv run ruff check src/ # Lint
uv run ruff check mcp/src/ # Lint
```

View File

@ -3,22 +3,33 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "birdcage-mcp"
name = "mcbirdcage"
version = "2026.02.17"
description = "FastMCP server for Winegard satellite dish control"
description = "MCP server for Winegard satellite dish control via serial"
license = "MIT"
requires-python = ">=3.11"
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
keywords = ["mcp", "satellite", "antenna", "winegard", "amateur-radio"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering",
"Topic :: Communications :: Ham Radio",
]
dependencies = [
"birdcage",
"winegard-birdcage",
"fastmcp>=2.0",
]
[project.urls]
Repository = "https://git.supported.systems/warehack.ing/birdcage"
Documentation = "https://birdcage.warehack.ing"
[project.scripts]
birdcage-mcp = "birdcage_mcp.server:main"
mcbirdcage = "mcbirdcage.server:main"
[tool.uv.sources]
birdcage = { path = "..", editable = true }
winegard-birdcage = { path = "..", editable = true }
[tool.ruff]
target-version = "py311"
@ -28,7 +39,7 @@ src = ["src"]
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.hatch.build.targets.wheel]
packages = ["src/birdcage_mcp"]
packages = ["src/mcbirdcage"]
[dependency-groups]
dev = ["pytest>=9.0.2", "pytest-asyncio>=1.3.0"]

View File

@ -1 +0,0 @@
"""birdcage-mcp: FastMCP server for Winegard satellite dish control."""

View File

@ -0,0 +1 @@
"""mcbirdcage: MCP server for Winegard satellite dish control via serial."""

View File

@ -7,7 +7,7 @@ from contextlib import asynccontextmanager
from fastmcp import FastMCP
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
logger = logging.getLogger(__name__)
@ -67,8 +67,8 @@ mcp = FastMCP(
)
# Register tool modules
from birdcage_mcp import prompts, resources # noqa: E402, F401
from birdcage_mcp.tools import ( # noqa: E402
from mcbirdcage import prompts, resources # noqa: E402, F401
from mcbirdcage.tools import ( # noqa: E402
connection,
console,
movement,
@ -88,5 +88,5 @@ prompts.register(mcp)
def main():
print("birdcage-mcp v2026.02.17", file=sys.stderr)
print("mcbirdcage v2026.02.17", file=sys.stderr)
mcp.run()

View File

@ -2,7 +2,7 @@
from fastmcp import Context
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
def register(mcp):

View File

@ -2,7 +2,7 @@
from fastmcp import Context
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
# Commands that are dangerous at the root menu level.
_BLOCKED_ROOT_COMMANDS = {"q"}

View File

@ -2,7 +2,7 @@
from fastmcp import Context
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
def _require_device(state: BirdcageState):

View File

@ -4,7 +4,7 @@ from dataclasses import asdict
from fastmcp import Context
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
def _require_craft(state: BirdcageState):

View File

@ -2,7 +2,7 @@
from fastmcp import Context
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
def _require_device(state: BirdcageState):

View File

@ -2,7 +2,7 @@
from fastmcp import Context
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
def _require_device(state: BirdcageState):

View File

@ -1,4 +1,4 @@
"""Shared fixtures for birdcage-mcp tests.
"""Shared fixtures for mcbirdcage tests.
Uses FastMCP's run_server_async to spin up a real MCP server backed by
DemoDevice + DemoCraftClient. No serial hardware, no subprocesses.
@ -13,7 +13,7 @@ from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.utilities.tests import run_server_async
from birdcage_mcp.state import BirdcageState
from mcbirdcage.state import BirdcageState
@asynccontextmanager
@ -36,8 +36,8 @@ async def _test_lifespan(server: FastMCP):
def _build_server() -> FastMCP:
"""Build a FastMCP server with all tools registered."""
from birdcage_mcp import prompts, resources
from birdcage_mcp.tools import (
from mcbirdcage import prompts, resources
from mcbirdcage.tools import (
connection,
console,
movement,

84
mcp/uv.lock generated
View File

@ -81,48 +81,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
]
[[package]]
name = "birdcage"
version = "2026.2.12.1"
source = { editable = "../" }
dependencies = [
{ name = "click" },
{ name = "pyserial" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.0" },
{ name = "pyserial", specifier = ">=3.5" },
]
[[package]]
name = "birdcage-mcp"
version = "2026.2.17"
source = { editable = "." }
dependencies = [
{ name = "birdcage" },
{ name = "fastmcp" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[package.metadata]
requires-dist = [
{ name = "birdcage", editable = "../" },
{ name = "fastmcp", specifier = ">=2.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.0.2" },
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
]
[[package]]
name = "cachetools"
version = "7.0.1"
@ -774,6 +732,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "mcbirdcage"
version = "2026.2.17"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
{ name = "winegard-birdcage" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[package.metadata]
requires-dist = [
{ name = "fastmcp", specifier = ">=2.0" },
{ name = "winegard-birdcage", editable = "../" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.0.2" },
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
]
[[package]]
name = "mcp"
version = "1.26.0"
@ -1668,6 +1653,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
]
[[package]]
name = "winegard-birdcage"
version = "2026.2.17"
source = { editable = "../" }
dependencies = [
{ name = "click" },
{ name = "pyserial" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.0" },
{ name = "pyserial", specifier = ">=3.5" },
]
[[package]]
name = "zipp"
version = "3.23.0"

View File

@ -3,17 +3,28 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "birdcage"
version = "2026.02.12.1"
name = "winegard-birdcage"
version = "2026.02.17"
description = "Winegard satellite dish control for amateur radio sky tracking"
license = "MIT"
requires-python = ">=3.11"
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
keywords = ["satellite", "antenna", "winegard", "amateur-radio", "ham"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering",
"Topic :: Communications :: Ham Radio",
]
dependencies = [
"pyserial>=3.5",
"click>=8.0",
]
[project.urls]
Repository = "https://git.supported.systems/warehack.ing/birdcage"
Documentation = "https://birdcage.warehack.ing"
[project.scripts]
birdcage = "birdcage.cli:main"
console-probe = "console_probe.cli:main"

View File

@ -10,7 +10,7 @@ license = "MIT"
requires-python = ">=3.11"
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
dependencies = [
"birdcage",
"winegard-birdcage",
"textual>=1.0.0",
]
@ -21,7 +21,7 @@ camera = ["Pillow>=10.0", "astropy>=6.0"]
birdcage-tui = "birdcage_tui.app:main"
[tool.uv.sources]
birdcage = { path = "..", editable = true }
winegard-birdcage = { path = "..", editable = true }
[tool.ruff]
target-version = "py311"

34
tui/uv.lock generated
View File

@ -34,28 +34,13 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/94/dcdac330d2c26776956555610ef584147584acc6b060e451aa65dd9142d9/astropy_iers_data-0.2026.2.16.0.48.25-py3-none-any.whl", hash = "sha256:180d1c3f59d18aa616345560799c2d88ec6e5164b8c45c746380acf892946136", size = 1982562, upload-time = "2026-02-16T00:49:09.602Z" },
]
[[package]]
name = "birdcage"
version = "2026.2.12.1"
source = { editable = "../" }
dependencies = [
{ name = "click" },
{ name = "pyserial" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.0" },
{ name = "pyserial", specifier = ">=3.5" },
]
[[package]]
name = "birdcage-tui"
version = "2026.2.13"
source = { editable = "." }
dependencies = [
{ name = "birdcage" },
{ name = "textual" },
{ name = "winegard-birdcage" },
]
[package.optional-dependencies]
@ -73,9 +58,9 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "astropy", marker = "extra == 'camera'", specifier = ">=6.0" },
{ name = "birdcage", editable = "../" },
{ name = "pillow", marker = "extra == 'camera'", specifier = ">=10.0" },
{ name = "textual", specifier = ">=1.0.0" },
{ name = "winegard-birdcage", editable = "../" },
]
provides-extras = ["camera"]
@ -525,3 +510,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
]
[[package]]
name = "winegard-birdcage"
version = "2026.2.17"
source = { editable = "../" }
dependencies = [
{ name = "click" },
{ name = "pyserial" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.0" },
{ name = "pyserial", specifier = ">=3.5" },
]