---
phase: 11-pypi-packaging
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - tests/test_entrypoint.py
  - vmcluster_mcp/__main__.py
  - pyproject.toml
  - LICENSE
autonomous: true
requirements:
  - INST-01

must_haves:
  truths:
    - "vmcluster_mcp.__main__.main is a callable function (not a module-level statement)"
    - "pyproject.toml [project.scripts] maps vmcluster-mcp to vmcluster_mcp.__main__:main"
    - "pyproject.toml version is 1.0.0"
    - "pyproject.toml contains readme, license, classifiers, keywords, authors, [project.urls]"
    - "LICENSE file exists at repo root with MIT license text"
    - "tests/test_entrypoint.py passes: main is callable, [project.scripts] key present"
  artifacts:
    - path: "tests/test_entrypoint.py"
      provides: "INST-01 unit tests for entry point and pyproject.toml scripts table"
      exports: ["test_main_is_callable", "test_project_scripts_entry_exists"]
    - path: "vmcluster_mcp/__main__.py"
      provides: "main() wrapper function satisfying [project.scripts] entry point"
      contains: "def main():"
    - path: "pyproject.toml"
      provides: "Complete PyPI metadata + console script entry"
      contains: "[project.scripts]"
    - path: "LICENSE"
      provides: "MIT license file required by PyPI"
      contains: "MIT License"
  key_links:
    - from: "pyproject.toml [project.scripts]"
      to: "vmcluster_mcp.__main__:main"
      via: "pip install shim generation"
      pattern: "vmcluster-mcp = \"vmcluster_mcp.__main__:main\""
    - from: "vmcluster_mcp/__main__.py"
      to: "vmcluster_mcp.server.mcp"
      via: "from .server import mcp; mcp.run()"
      pattern: "def main\\(\\):"
---

<objective>
Write the test scaffold for INST-01, add the main() wrapper to __main__.py, complete pyproject.toml metadata (version, readme, license, classifiers, keywords, authors, project.urls, project.scripts), and create the LICENSE file.

Purpose: These changes make the package publishable to PyPI with a working console script entry point. No server logic is changed.
Output: tests/test_entrypoint.py, updated vmcluster_mcp/__main__.py, updated pyproject.toml, new LICENSE file.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/11-pypi-packaging/11-VALIDATION.md

<interfaces>
<!-- Current state of files being modified. Executor must read these before writing. -->

Current pyproject.toml (full content):
```toml
[project]
name = "vmcluster-mcp"
version = "0.1.0"
description = "MCP server for autonomous multi-VM cluster orchestration on libvirt/QEMU"
requires-python = ">=3.11"
dependencies = [
    "mcp[cli]>=1.26.0",
    "pydantic>=2.12.5",
    "asyncssh>=2.22.0",
    "libvirt-python>=12.1.0",
    "PyYAML>=6.0.3",
]

[dependency-groups]
dev = [
    "pytest>=9.0.2",
    "pytest-asyncio>=1.3.0",
    "pytest-mock>=3.15.1",
    "ruff>=0.15.7",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.ruff]
line-length = 120
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
```

Current vmcluster_mcp/__main__.py (full content):
```python
"""Entry point for python -m vmcluster_mcp."""

from .server import mcp

mcp.run()
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Wave 0 — Create test_entrypoint.py</name>
  <files>tests/test_entrypoint.py</files>
  <read_first>
    - tests/test_entrypoint.py (does not exist yet — skip if absent)
    - pyproject.toml (to understand current structure before tests reference it)
    - vmcluster_mcp/__main__.py (to understand what main() will look like)
    - tests/test_server.py (to match test file style/import conventions)
  </read_first>
  <behavior>
    - test_main_is_callable: import vmcluster_mcp.__main__ and assert callable(main) is True
    - test_project_scripts_entry_exists: open pyproject.toml, parse with tomllib (stdlib Python 3.11+), assert data["project"]["scripts"]["vmcluster-mcp"] == "vmcluster_mcp.__main__:main"
    - Both tests must FAIL before Task 2 changes are applied (RED state)
  </behavior>
  <action>
Create tests/test_entrypoint.py with these two tests.

Use tomllib (stdlib, Python 3.11+) — no third-party toml library.

Exact file content:

```python
"""Tests for the vmcluster-mcp console script entry point (INST-01)."""

import tomllib
from pathlib import Path

import pytest


def test_main_is_callable():
    """main() must be a callable so [project.scripts] shim works after install."""
    from vmcluster_mcp.__main__ import main  # noqa: PLC0415

    assert callable(main), "vmcluster_mcp.__main__.main must be a callable function"


def test_project_scripts_entry_exists():
    """[project.scripts] must map vmcluster-mcp to vmcluster_mcp.__main__:main."""
    pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
    with open(pyproject_path, "rb") as f:
        data = tomllib.load(f)

    scripts = data.get("project", {}).get("scripts", {})
    assert "vmcluster-mcp" in scripts, "[project.scripts] must contain 'vmcluster-mcp' key"
    assert scripts["vmcluster-mcp"] == "vmcluster_mcp.__main__:main", (
        "Entry point must be 'vmcluster_mcp.__main__:main'"
    )
```

Do NOT mark tests with @pytest.mark.asyncio — these are synchronous tests.
Do NOT import mcp or call mcp.run() — that requires libvirt.
  </action>
  <verify>
    <automated>MISSING — this is Wave 0; tests will FAIL until Task 2 is complete. Run: .venv/bin/pytest tests/test_entrypoint.py -x 2>&1 | head -20 to confirm RED state (ImportError on main or KeyError on scripts is expected).</automated>
  </verify>
  <done>tests/test_entrypoint.py exists with test_main_is_callable and test_project_scripts_entry_exists; both tests fail in RED state before Task 2 changes are applied.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add main() wrapper and complete pyproject.toml + LICENSE</name>
  <files>vmcluster_mcp/__main__.py, pyproject.toml, LICENSE</files>
  <read_first>
    - vmcluster_mcp/__main__.py (read current 5-line file before modifying)
    - pyproject.toml (read current 36-line file before modifying)
    - tests/test_entrypoint.py (read to understand what must be satisfied)
  </read_first>
  <behavior>
    - After this task: test_main_is_callable passes (main is a callable, not ImportError)
    - After this task: test_project_scripts_entry_exists passes ([project.scripts] key exists with correct value)
    - After this task: .venv/bin/pytest tests/test_entrypoint.py -x exits 0
  </behavior>
  <action>
**Step A: Update vmcluster_mcp/__main__.py**

Replace the entire file with:
```python
"""Entry point for python -m vmcluster_mcp."""

from .server import mcp


def main():
    mcp.run()


if __name__ == "__main__":
    main()
```

**Step B: Replace pyproject.toml with the complete updated version**

Replace the ENTIRE pyproject.toml content. Every section must be present. Do not preserve the old version — write this exact content, filling in author info from git config (name: "Chris Horn", email: "chompinbits@gmail.com"):

```toml
[project]
name = "vmcluster-mcp"
version = "1.0.0"
description = "MCP server for autonomous multi-VM cluster orchestration on libvirt/QEMU"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.11"
authors = [
    {name = "Chris Horn", email = "chompinbits@gmail.com"},
]
keywords = ["mcp", "libvirt", "qemu", "kvm", "vm", "cluster", "testing"]
classifiers = [
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.11",
    "License :: OSI Approved :: MIT License",
    "Operating System :: POSIX :: Linux",
    "Topic :: Software Development :: Testing",
    "Topic :: System :: Systems Administration",
]
dependencies = [
    "mcp[cli]>=1.26.0",
    "pydantic>=2.12.5",
    "asyncssh>=2.22.0",
    "libvirt-python>=12.1.0",
    "PyYAML>=6.0.3",
]

[project.scripts]
vmcluster-mcp = "vmcluster_mcp.__main__:main"

[project.urls]
Homepage = "https://github.com/hornc/vmcluster-mcp"
Repository = "https://github.com/hornc/vmcluster-mcp"
Issues = "https://github.com/hornc/vmcluster-mcp/issues"

[dependency-groups]
dev = [
    "pytest>=9.0.2",
    "pytest-asyncio>=1.3.0",
    "pytest-mock>=3.15.1",
    "ruff>=0.15.7",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.ruff]
line-length = 120
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
```

**Step C: Create LICENSE file at repo root**

Create /home/hornc/repos/vmcluster-mcp/LICENSE with this exact content (year 2024, holder "Chris Horn"):

```
MIT License

Copyright (c) 2024 Chris Horn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
  </action>
  <verify>
    <automated>.venv/bin/pytest tests/test_entrypoint.py tests/test_server.py -q</automated>
  </verify>
  <acceptance_criteria>
    - vmcluster_mcp/__main__.py contains `def main():` (grep-verifiable)
    - vmcluster_mcp/__main__.py contains `if __name__ == "__main__":` (grep-verifiable)
    - pyproject.toml contains `version = "1.0.0"` (grep-verifiable)
    - pyproject.toml contains `[project.scripts]` section header (grep-verifiable)
    - pyproject.toml contains `vmcluster-mcp = "vmcluster_mcp.__main__:main"` (grep-verifiable)
    - pyproject.toml contains `readme = "README.md"` (grep-verifiable)
    - pyproject.toml contains `license = "MIT"` (grep-verifiable)
    - pyproject.toml contains `"License :: OSI Approved :: MIT License"` (grep-verifiable)
    - pyproject.toml contains `keywords = ` (grep-verifiable)
    - pyproject.toml contains `[project.urls]` (grep-verifiable)
    - LICENSE file exists at repo root (file exists check)
    - LICENSE contains `MIT License` (grep-verifiable)
    - LICENSE contains `Copyright (c) 2024 Chris Horn` (grep-verifiable)
    - .venv/bin/pytest tests/test_entrypoint.py -x exits 0 (2 tests pass)
    - .venv/bin/pytest tests/test_server.py -q exits 0 (existing tests unbroken)
  </acceptance_criteria>
  <done>Both test_entrypoint.py tests pass (GREEN); pyproject.toml has all required PyPI metadata fields; __main__.py has main() wrapper; LICENSE exists with MIT text.</done>
</task>

</tasks>

<verification>
After all tasks complete:
- `.venv/bin/pytest tests/test_entrypoint.py tests/test_server.py -q` exits 0
- `grep "def main():" vmcluster_mcp/__main__.py` returns match
- `grep '\[project.scripts\]' pyproject.toml` returns match
- `grep 'version = "1.0.0"' pyproject.toml` returns match
- `test -f LICENSE` returns 0
</verification>

<success_criteria>
- tests/test_entrypoint.py exists and both tests pass
- vmcluster_mcp/__main__.py has a main() function that calls mcp.run()
- pyproject.toml has version="1.0.0", readme, license, classifiers, keywords, authors, [project.scripts], [project.urls]
- LICENSE file exists at repo root with MIT text
- All existing tests continue to pass
</success_criteria>

<output>
After completion, create `.planning/phases/11-pypi-packaging/11-01-SUMMARY.md`
</output>
