|
| 1 | +# Plan 001: Talk Python CLI — Standalone Package |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +The Talk Python MCP server at `https://talkpython.fm/api/mcp` exposes 12 tools for querying |
| 6 | +podcast episodes, guests, transcripts, and courses via JSON-RPC 2.0. This project is a |
| 7 | +**standalone, open-source CLI tool** (`talkpython`) that wraps those MCP tools as terminal |
| 8 | +commands. |
| 9 | + |
| 10 | +Full server documentation (tool names, parameters, descriptions): |
| 11 | +**https://talkpython.fm/api/mcp/docs** |
| 12 | + |
| 13 | +The package will be published to PyPI as `talk-python-cli` with the command name `talkpython`. |
| 14 | + |
| 15 | +### MCP server tools (reference) |
| 16 | + |
| 17 | +The server (v1.3.0) is public, read-only, no authentication required. Transport: Streamable HTTP. |
| 18 | + |
| 19 | +| CLI command | MCP tool name | Parameters | |
| 20 | +|----------------------------------|----------------------------|-----------------------------------------| |
| 21 | +| `episodes search` | `search_episodes` | `query` (str), `limit` (int, optional) | |
| 22 | +| `episodes get` | `get_episode` | `show_id` (int) | |
| 23 | +| `episodes list` | `get_episodes` | *(none)* | |
| 24 | +| `episodes recent` | `get_recent_episodes` | `limit` (int, optional) | |
| 25 | +| `episodes transcript` | `get_transcript_for_episode` | `show_id` (int) | |
| 26 | +| `episodes transcript-vtt` | `get_transcript_vtt` | `show_id` (int) | |
| 27 | +| `guests search` | `search_guests` | `query` (str), `limit` (int, optional) | |
| 28 | +| `guests get` | `get_guest_by_id` | `guest_id` (int) | |
| 29 | +| `guests list` | `get_guests` | *(none)* | |
| 30 | +| `courses search` | `search_courses` | `query` (str), `course_id` (int, opt.) | |
| 31 | +| `courses get` | `get_course_details` | `course_id` (int) | |
| 32 | +| `courses list` | `get_courses` | *(none)* | |
| 33 | + |
| 34 | +### Server-side prerequisite |
| 35 | + |
| 36 | +The MCP server needs `?format=json` query parameter support so the CLI can receive structured |
| 37 | +data instead of pre-formatted Markdown. That change lives in the main Talk Python web app repo |
| 38 | +(not this one) and must be deployed before the CLI's `--format json` and auto-JSON-on-pipe |
| 39 | +features work. The CLI should: |
| 40 | + |
| 41 | +- Default to requesting `format=text` (works with the server today) |
| 42 | +- Support `--format json` for when the server-side change is deployed |
| 43 | +- Degrade gracefully if the server ignores the format parameter |
| 44 | + |
| 45 | +## Project structure |
| 46 | + |
| 47 | +Package lives at the repo root (not nested in a subdirectory): |
| 48 | + |
| 49 | +``` |
| 50 | +talk-python-cli/ |
| 51 | +├── pyproject.toml |
| 52 | +├── LICENSE |
| 53 | +├── README.md |
| 54 | +├── .gitignore |
| 55 | +├── src/ |
| 56 | +│ └── talk_python_cli/ |
| 57 | +│ ├── __init__.py # Version string |
| 58 | +│ ├── __main__.py # python -m talk_python_cli support |
| 59 | +│ ├── app.py # Root Cyclopts app + global options |
| 60 | +│ ├── client.py # MCP HTTP client (httpx JSON-RPC wrapper) |
| 61 | +│ ├── formatting.py # Output formatting (rich Markdown or JSON) |
| 62 | +│ ├── episodes.py # Episode commands sub-app |
| 63 | +│ ├── guests.py # Guest commands sub-app |
| 64 | +│ └── courses.py # Course commands sub-app |
| 65 | +└── tests/ |
| 66 | + ├── __init__.py |
| 67 | + ├── conftest.py # Shared fixtures (mock MCP responses) |
| 68 | + ├── test_client.py # MCPClient unit tests |
| 69 | + ├── test_episodes.py # Episode command tests |
| 70 | + ├── test_guests.py # Guest command tests |
| 71 | + └── test_courses.py # Course command tests |
| 72 | +``` |
| 73 | + |
| 74 | +## Dependencies (pyproject.toml) |
| 75 | + |
| 76 | +```toml |
| 77 | +[project] |
| 78 | +name = "talk-python-cli" |
| 79 | +version = "0.1.0" |
| 80 | +description = "CLI for the Talk Python to Me podcast and courses" |
| 81 | +requires-python = ">=3.12" |
| 82 | +license = "MIT" |
| 83 | +authors = [ |
| 84 | + { name = "Michael Kennedy", email = "michael@talkpython.fm" }, |
| 85 | +] |
| 86 | +dependencies = [ |
| 87 | + "cyclopts>=3.0", |
| 88 | + "httpx>=0.27", |
| 89 | + "rich>=13.0", |
| 90 | +] |
| 91 | + |
| 92 | +[project.scripts] |
| 93 | +talkpython = "talk_python_cli.app:main" |
| 94 | + |
| 95 | +[dependency-groups] |
| 96 | +dev = [ |
| 97 | + "pytest>=8.0", |
| 98 | + "pytest-httpx>=0.34", |
| 99 | +] |
| 100 | + |
| 101 | +[build-system] |
| 102 | +requires = ["hatchling"] |
| 103 | +build-backend = "hatchling.build" |
| 104 | +``` |
| 105 | + |
| 106 | +## CLI commands |
| 107 | + |
| 108 | +``` |
| 109 | +talkpython [--format text|json] [--url URL] |
| 110 | +
|
| 111 | +talkpython episodes search QUERY [--limit N] |
| 112 | +talkpython episodes get SHOW_ID |
| 113 | +talkpython episodes list |
| 114 | +talkpython episodes recent [--limit N] |
| 115 | +talkpython episodes transcript SHOW_ID |
| 116 | +talkpython episodes transcript-vtt SHOW_ID |
| 117 | +
|
| 118 | +talkpython guests search QUERY [--limit N] |
| 119 | +talkpython guests get GUEST_ID |
| 120 | +talkpython guests list |
| 121 | +
|
| 122 | +talkpython courses search QUERY [--course-id ID] |
| 123 | +talkpython courses get COURSE_ID |
| 124 | +talkpython courses list |
| 125 | +``` |
| 126 | + |
| 127 | +Global option `--format` defaults to `text` (rendered Markdown) but can be set to `json` |
| 128 | +for machine-readable output. `--url` defaults to `https://talkpython.fm/api/mcp`. |
| 129 | + |
| 130 | +### Auto-detection for piped output |
| 131 | + |
| 132 | +When stdout is not a TTY (piped to another command), default to JSON format |
| 133 | +for scripting convenience: `talkpython episodes recent | jq '.[]'` |
| 134 | + |
| 135 | +## Key module designs |
| 136 | + |
| 137 | +**`client.py`** — Thin wrapper around httpx making JSON-RPC calls: |
| 138 | +```python |
| 139 | +class MCPClient: |
| 140 | + def __init__(self, base_url: str, output_format: str = 'text'): |
| 141 | + self.base_url = base_url |
| 142 | + self.output_format = output_format |
| 143 | + self._msg_id = 0 |
| 144 | + |
| 145 | + def call_tool(self, tool_name: str, arguments: dict) -> dict: |
| 146 | + # POST to base_url?format={output_format} |
| 147 | + # JSON-RPC 2.0 envelope: {"jsonrpc":"2.0","id":N,"method":"tools/call","params":{...}} |
| 148 | + # Returns the result content text |
| 149 | +``` |
| 150 | + |
| 151 | +**`formatting.py`** — Handles display: |
| 152 | +- `text` format: render Markdown from server using rich |
| 153 | +- `json` format: print with optional pretty-printing |
| 154 | + |
| 155 | +**`app.py`** — Root app with global params: |
| 156 | +```python |
| 157 | +app = cyclopts.App(name='talkpython', help='Talk Python to Me CLI') |
| 158 | +# Register sub-apps |
| 159 | +app.command(episodes_app) |
| 160 | +app.command(guests_app) |
| 161 | +app.command(courses_app) |
| 162 | +``` |
| 163 | + |
| 164 | +**`episodes.py`**, **`guests.py`**, **`courses.py`** — Each defines a sub-app: |
| 165 | +```python |
| 166 | +episodes_app = cyclopts.App(name='episodes', help='Podcast episode commands') |
| 167 | + |
| 168 | +@episodes_app.command |
| 169 | +def search(query: str, *, limit: int = 10): |
| 170 | + ... |
| 171 | +``` |
| 172 | + |
| 173 | +## Implementation order |
| 174 | + |
| 175 | +1. Create package structure: `pyproject.toml`, `src/talk_python_cli/__init__.py` |
| 176 | +2. Implement `client.py` (HTTP JSON-RPC client) |
| 177 | +3. Implement `formatting.py` (output rendering) |
| 178 | +4. Implement `app.py` + command modules (`episodes.py`, `guests.py`, `courses.py`) |
| 179 | +5. Add `__main__.py` for `python -m` support |
| 180 | +6. Add tests with mocked HTTP responses |
| 181 | +7. Verify against live server |
| 182 | + |
| 183 | +## Verification |
| 184 | + |
| 185 | +1. **Install**: `pip install -e ".[dev]"` (or `uv pip install -e ".[dev]"`) |
| 186 | +2. **Unit tests**: `pytest tests/ -v` |
| 187 | +3. **Smoke test**: `talkpython episodes recent --limit 3` |
| 188 | +4. **JSON output**: `talkpython --format json episodes recent --limit 3` |
| 189 | +5. **Piped output**: `talkpython episodes recent | head` — should auto-detect JSON format |
| 190 | +6. **Module entry**: `python -m talk_python_cli episodes recent --limit 3` |
0 commit comments