Skip to content

Commit 306e2c0

Browse files
committed
Initial plan for building this CLI too.
1 parent 95d1263 commit 306e2c0

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

plans/001-talk-python-cli.plan.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

Comments
 (0)