webtap-tool 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webtap/VISION.md +246 -0
- webtap/__init__.py +84 -0
- webtap/__main__.py +6 -0
- webtap/api/__init__.py +9 -0
- webtap/api/app.py +26 -0
- webtap/api/models.py +69 -0
- webtap/api/server.py +111 -0
- webtap/api/sse.py +182 -0
- webtap/api/state.py +89 -0
- webtap/app.py +79 -0
- webtap/cdp/README.md +275 -0
- webtap/cdp/__init__.py +12 -0
- webtap/cdp/har.py +302 -0
- webtap/cdp/schema/README.md +41 -0
- webtap/cdp/schema/cdp_protocol.json +32785 -0
- webtap/cdp/schema/cdp_version.json +8 -0
- webtap/cdp/session.py +667 -0
- webtap/client.py +81 -0
- webtap/commands/DEVELOPER_GUIDE.md +401 -0
- webtap/commands/TIPS.md +269 -0
- webtap/commands/__init__.py +29 -0
- webtap/commands/_builders.py +331 -0
- webtap/commands/_code_generation.py +110 -0
- webtap/commands/_tips.py +147 -0
- webtap/commands/_utils.py +273 -0
- webtap/commands/connection.py +220 -0
- webtap/commands/console.py +87 -0
- webtap/commands/fetch.py +310 -0
- webtap/commands/filters.py +116 -0
- webtap/commands/javascript.py +73 -0
- webtap/commands/js_export.py +73 -0
- webtap/commands/launch.py +72 -0
- webtap/commands/navigation.py +197 -0
- webtap/commands/network.py +136 -0
- webtap/commands/quicktype.py +306 -0
- webtap/commands/request.py +93 -0
- webtap/commands/selections.py +138 -0
- webtap/commands/setup.py +219 -0
- webtap/commands/to_model.py +163 -0
- webtap/daemon.py +185 -0
- webtap/daemon_state.py +53 -0
- webtap/filters.py +219 -0
- webtap/rpc/__init__.py +14 -0
- webtap/rpc/errors.py +49 -0
- webtap/rpc/framework.py +223 -0
- webtap/rpc/handlers.py +625 -0
- webtap/rpc/machine.py +84 -0
- webtap/services/README.md +83 -0
- webtap/services/__init__.py +15 -0
- webtap/services/console.py +124 -0
- webtap/services/dom.py +547 -0
- webtap/services/fetch.py +415 -0
- webtap/services/main.py +392 -0
- webtap/services/network.py +401 -0
- webtap/services/setup/__init__.py +185 -0
- webtap/services/setup/chrome.py +233 -0
- webtap/services/setup/desktop.py +255 -0
- webtap/services/setup/extension.py +147 -0
- webtap/services/setup/platform.py +162 -0
- webtap/services/state_snapshot.py +86 -0
- webtap_tool-0.11.0.dist-info/METADATA +535 -0
- webtap_tool-0.11.0.dist-info/RECORD +64 -0
- webtap_tool-0.11.0.dist-info/WHEEL +4 -0
- webtap_tool-0.11.0.dist-info/entry_points.txt +2 -0
webtap/client.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 client for WebTap daemon communication."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RPCError(Exception):
|
|
14
|
+
"""RPC error with code, message, and optional data."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, code: str, message: str, data: dict | None = None):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.code = code
|
|
19
|
+
self.message = message
|
|
20
|
+
self.data = data
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RPCClient:
|
|
24
|
+
"""Simple JSON-RPC 2.0 client for WebTap daemon.
|
|
25
|
+
|
|
26
|
+
All daemon communication goes through `call()`:
|
|
27
|
+
|
|
28
|
+
client.call("connect", page=0)
|
|
29
|
+
client.call("network", limit=50, type="xhr")
|
|
30
|
+
client.call("status")
|
|
31
|
+
|
|
32
|
+
The client tracks epoch for stale request detection.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, base_url: str = "http://localhost:8765", timeout: float = 30.0):
|
|
36
|
+
self.base_url = base_url
|
|
37
|
+
self.epoch = 0
|
|
38
|
+
self._client = httpx.Client(timeout=timeout)
|
|
39
|
+
|
|
40
|
+
def call(self, method: str, **params) -> dict[str, Any]:
|
|
41
|
+
"""Call RPC method. Raises RPCError on error."""
|
|
42
|
+
request: dict[str, Any] = {
|
|
43
|
+
"jsonrpc": "2.0",
|
|
44
|
+
"method": method,
|
|
45
|
+
"params": params,
|
|
46
|
+
"id": str(uuid.uuid4()),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Only send epoch if we've synced with server (non-zero)
|
|
50
|
+
# Server validates epoch only if provided, so first call syncs without validation
|
|
51
|
+
if self.epoch > 0:
|
|
52
|
+
request["epoch"] = self.epoch
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
response = self._client.post(f"{self.base_url}/rpc", json=request)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
data = response.json()
|
|
58
|
+
except httpx.ConnectError as e:
|
|
59
|
+
logger.error(f"Failed to connect to daemon at {self.base_url}: {e}")
|
|
60
|
+
raise RuntimeError("Cannot connect to daemon. Is it running? Try 'webtap --daemon' to start it.") from e
|
|
61
|
+
except httpx.HTTPError as e:
|
|
62
|
+
logger.error(f"HTTP error from daemon: {e}")
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
# Update epoch from server response (always sync)
|
|
66
|
+
if "epoch" in data:
|
|
67
|
+
self.epoch = data["epoch"]
|
|
68
|
+
|
|
69
|
+
# Check for RPC error
|
|
70
|
+
if "error" in data:
|
|
71
|
+
err = data["error"]
|
|
72
|
+
raise RPCError(err.get("code", "UNKNOWN"), err.get("message", "Unknown error"), err.get("data"))
|
|
73
|
+
|
|
74
|
+
return data.get("result", {})
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""Close the HTTP client."""
|
|
78
|
+
self._client.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["RPCClient", "RPCError"]
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# WebTap Commands Developer Guide
|
|
2
|
+
|
|
3
|
+
This guide documents the patterns and conventions for developing WebTap commands with MCP compatibility.
|
|
4
|
+
|
|
5
|
+
## Command Patterns (Post-Refinement)
|
|
6
|
+
|
|
7
|
+
### 1. Simple Commands (No Parameters)
|
|
8
|
+
```python
|
|
9
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
10
|
+
def disconnect(state) -> dict:
|
|
11
|
+
"""Disconnect from Chrome."""
|
|
12
|
+
# Implementation
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. Single Required Parameter
|
|
16
|
+
```python
|
|
17
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
18
|
+
def navigate(state, url: str) -> dict:
|
|
19
|
+
"""Navigate to URL."""
|
|
20
|
+
# Implementation
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 3. Optional Boolean/Simple Parameters (Direct)
|
|
24
|
+
```python
|
|
25
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
26
|
+
def reload(state, ignore_cache: bool = False) -> dict:
|
|
27
|
+
"""Reload current page."""
|
|
28
|
+
# Implementation
|
|
29
|
+
|
|
30
|
+
# Multiple boolean flags
|
|
31
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
32
|
+
def clear(state, events: bool = True, console: bool = False) -> dict:
|
|
33
|
+
"""Clear various data stores."""
|
|
34
|
+
# Implementation
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 4. Mutually Exclusive Parameters (Direct)
|
|
38
|
+
Use direct parameters when you have different ways to identify the same thing:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
42
|
+
def connect(state, page: int = None, page_id: str = None) -> dict:
|
|
43
|
+
"""Connect to Chrome page.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
page: Connect by page index (0-based)
|
|
47
|
+
page_id: Connect by page ID
|
|
48
|
+
|
|
49
|
+
Note: Cannot specify both page and page_id.
|
|
50
|
+
"""
|
|
51
|
+
if page is not None and page_id is not None:
|
|
52
|
+
return error_response("invalid_parameters",
|
|
53
|
+
"Cannot specify both 'page' and 'page_id'")
|
|
54
|
+
# Implementation
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 5. Multiple Optional Parameters (Direct)
|
|
58
|
+
Use direct parameters for cleaner API when parameters are well-defined:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
62
|
+
def network(state, limit: int = 20, filters: list = None, no_filters: bool = False) -> dict:
|
|
63
|
+
"""Show network requests.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
limit: Maximum results to show
|
|
67
|
+
filters: Specific filter categories to apply
|
|
68
|
+
no_filters: Show everything unfiltered
|
|
69
|
+
"""
|
|
70
|
+
# Implementation
|
|
71
|
+
|
|
72
|
+
# With field selection and expression evaluation
|
|
73
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
74
|
+
def request(state, id: int, fields: list = None, expr: str = None) -> dict:
|
|
75
|
+
"""Get HAR request details with field selection."""
|
|
76
|
+
# Implementation
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 6. Mixed Parameters (Direct + Dict)
|
|
80
|
+
Use dict only for complex/variable configurations:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
84
|
+
def resume(state, request: int, wait: float = 0.5, modifications: dict = None) -> dict:
|
|
85
|
+
"""Resume a paused request.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
request: Request row ID
|
|
89
|
+
wait: Wait time for next event
|
|
90
|
+
modifications: Request/response modifications
|
|
91
|
+
- {"url": "..."} - Change URL
|
|
92
|
+
- {"method": "POST"} - Change method
|
|
93
|
+
"""
|
|
94
|
+
mods = modifications or {}
|
|
95
|
+
# Implementation
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 7. Field Selection Pattern
|
|
99
|
+
Use list for selecting fields from structured data:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
103
|
+
def request(state, id: int, fields: list = None, expr: str = None) -> dict:
|
|
104
|
+
"""Get HAR request details with field selection.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
id: HAR row ID from network() output
|
|
108
|
+
fields: Field patterns to include
|
|
109
|
+
- ["*"] - Everything
|
|
110
|
+
- ["request.*"] - Request data
|
|
111
|
+
- ["response.content"] - Response body
|
|
112
|
+
expr: Python expression to evaluate on data
|
|
113
|
+
"""
|
|
114
|
+
# Fields are selected from HAR entry structure
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 8. Action + Config Pattern (Complex Operations)
|
|
118
|
+
Keep for commands with varied operations:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
122
|
+
def filters(state, action: str = "list", config: dict = None) -> dict:
|
|
123
|
+
"""Manage filters.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
action: Operation to perform
|
|
127
|
+
- "list" - Show all filters
|
|
128
|
+
- "add" - Add filter
|
|
129
|
+
- "remove" - Remove filter
|
|
130
|
+
config: Action-specific configuration
|
|
131
|
+
- For add: {"category": "ads", "patterns": ["*ad*"]}
|
|
132
|
+
- For remove: {"patterns": ["*ad*"]}
|
|
133
|
+
"""
|
|
134
|
+
cfg = config or {}
|
|
135
|
+
|
|
136
|
+
if action == "add":
|
|
137
|
+
category = cfg.get("category", "custom")
|
|
138
|
+
patterns = cfg.get("patterns", [])
|
|
139
|
+
# Implementation
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## MCP Type Requirements
|
|
143
|
+
|
|
144
|
+
### ❌ Avoid These (Not MCP Compatible)
|
|
145
|
+
```python
|
|
146
|
+
# Union types
|
|
147
|
+
def command(state, param: str | None = None)
|
|
148
|
+
|
|
149
|
+
# Optional types
|
|
150
|
+
from typing import Optional
|
|
151
|
+
def command(state, param: Optional[str] = None)
|
|
152
|
+
|
|
153
|
+
# Complex types
|
|
154
|
+
from typing import Dict, List
|
|
155
|
+
def command(state, data: Dict[str, List[str]])
|
|
156
|
+
|
|
157
|
+
# **kwargs
|
|
158
|
+
def command(state, **fields)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### ✅ Use These Instead
|
|
162
|
+
```python
|
|
163
|
+
# Simple defaults
|
|
164
|
+
def command(state, param: str = "default")
|
|
165
|
+
def command(state, param: dict = None) # pyright: ignore[reportArgumentType]
|
|
166
|
+
def command(state, param: list = None) # pyright: ignore[reportArgumentType]
|
|
167
|
+
def command(state, param: bool = False)
|
|
168
|
+
def command(state, param: int = 0)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Response Patterns
|
|
172
|
+
|
|
173
|
+
### Resources (Read-Only Data)
|
|
174
|
+
```python
|
|
175
|
+
@app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "text/markdown"})
|
|
176
|
+
def pages(state) -> dict:
|
|
177
|
+
"""List available pages."""
|
|
178
|
+
return table_response(
|
|
179
|
+
title="Chrome Pages",
|
|
180
|
+
headers=["Index", "Title", "URL"],
|
|
181
|
+
rows=rows,
|
|
182
|
+
summary=f"{len(rows)} pages"
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Tools (Actions with Side Effects)
|
|
187
|
+
```python
|
|
188
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
189
|
+
def navigate(state, url: str) -> dict:
|
|
190
|
+
"""Navigate to URL."""
|
|
191
|
+
# Perform action
|
|
192
|
+
return info_response(
|
|
193
|
+
title="Navigation Complete",
|
|
194
|
+
fields={"URL": url, "Status": "Success"}
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Error Handling
|
|
199
|
+
|
|
200
|
+
Always use the error utilities from `_builders.py`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from webtap.commands._builders import check_connection, error_response
|
|
204
|
+
|
|
205
|
+
def my_command(state, ...):
|
|
206
|
+
# Check connection first for commands that need it
|
|
207
|
+
if error := check_connection(state):
|
|
208
|
+
return error
|
|
209
|
+
|
|
210
|
+
# Validate parameters
|
|
211
|
+
if not valid:
|
|
212
|
+
return error_response("Parameter X must be Y", suggestions=["Try this", "Or that"])
|
|
213
|
+
|
|
214
|
+
# Simple errors
|
|
215
|
+
return error_response("Specific error message")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Response Builders
|
|
219
|
+
|
|
220
|
+
Use builders from `_builders.py`:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from webtap.commands._builders import (
|
|
224
|
+
# Table responses
|
|
225
|
+
table_response, # Tables with headers, warnings, tips
|
|
226
|
+
|
|
227
|
+
# Info displays
|
|
228
|
+
info_response, # Key-value pairs with optional heading
|
|
229
|
+
|
|
230
|
+
# Status responses
|
|
231
|
+
error_response, # Errors with suggestions
|
|
232
|
+
success_response, # Success messages with details
|
|
233
|
+
warning_response, # Warnings with suggestions
|
|
234
|
+
|
|
235
|
+
# Code results
|
|
236
|
+
code_result_response, # Code execution with result display
|
|
237
|
+
code_response, # Simple code block display
|
|
238
|
+
|
|
239
|
+
# Connection helpers
|
|
240
|
+
check_connection, # Helper for CDP connection validation
|
|
241
|
+
check_fetch_enabled, # Helper for fetch interception validation
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Usage Examples
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Table with tips
|
|
249
|
+
return table_response(
|
|
250
|
+
title="Network Requests",
|
|
251
|
+
headers=["ID", "URL", "Status"],
|
|
252
|
+
rows=rows,
|
|
253
|
+
summary=f"{len(rows)} requests",
|
|
254
|
+
tips=["Use request(ID, ['response.content']) to fetch response body"]
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Code execution result
|
|
258
|
+
return code_result_response(
|
|
259
|
+
"JavaScript Result",
|
|
260
|
+
code="2 + 2",
|
|
261
|
+
language="javascript",
|
|
262
|
+
result=4
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Error with suggestions
|
|
266
|
+
return error_response(
|
|
267
|
+
"Not connected to Chrome",
|
|
268
|
+
suggestions=["Run pages()", "Use connect(0)"]
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Text Over Symbols
|
|
273
|
+
|
|
274
|
+
Use explicit text instead of symbols for clarity:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# Status text
|
|
278
|
+
"Connected" / "Disconnected"
|
|
279
|
+
"Enabled" / "Disabled"
|
|
280
|
+
"Yes" / "No"
|
|
281
|
+
|
|
282
|
+
# For empty values
|
|
283
|
+
"-" or "None" or ""
|
|
284
|
+
|
|
285
|
+
# Descriptive status
|
|
286
|
+
"3 requests paused" instead of symbols
|
|
287
|
+
"Request Failed" instead of error symbols
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Decision Tree for Parameter Patterns (Updated)
|
|
291
|
+
|
|
292
|
+
1. **No parameters?** → Simple command
|
|
293
|
+
2. **One required param?** → Single parameter
|
|
294
|
+
3. **Few well-defined params?** → Direct parameters with defaults
|
|
295
|
+
4. **Multiple ways to identify same thing?** → Direct mutually exclusive params
|
|
296
|
+
5. **Dynamic/unknown field names?** → Dict for filters
|
|
297
|
+
6. **Complex variable config?** → Dict for modifications
|
|
298
|
+
7. **Different operations based on input?** → Action + config pattern
|
|
299
|
+
|
|
300
|
+
### When to Use Direct Parameters
|
|
301
|
+
- Parameters are well-defined and limited (< 5)
|
|
302
|
+
- Parameters are commonly used
|
|
303
|
+
- Makes the API more intuitive
|
|
304
|
+
- Boolean flags or simple types
|
|
305
|
+
|
|
306
|
+
### When to Keep Dict Parameters
|
|
307
|
+
- Field names are dynamic (like CDP event fields)
|
|
308
|
+
- Configuration varies significantly by action
|
|
309
|
+
- Many optional parameters rarely used together
|
|
310
|
+
- Complex nested structures needed
|
|
311
|
+
|
|
312
|
+
## Examples by Category (Current Implementation)
|
|
313
|
+
|
|
314
|
+
### Navigation Commands
|
|
315
|
+
- `navigate(url: str)` - Single required parameter
|
|
316
|
+
- `reload(ignore_cache: bool = False)` - Optional boolean
|
|
317
|
+
- `back()`, `forward()` - No parameters
|
|
318
|
+
|
|
319
|
+
### Query Commands
|
|
320
|
+
- `network(limit: int = 20, ...)` - Direct params with filters
|
|
321
|
+
- `request(id: int, fields: list = None, expr: str = None)` - Field selection + expression
|
|
322
|
+
- `console(limit: int = 50, level: str = None)` - Direct optional params
|
|
323
|
+
|
|
324
|
+
### Management Commands
|
|
325
|
+
- `connect(page: int = None, page_id: str = None)` - Mutually exclusive direct params
|
|
326
|
+
- `clear(events: bool = True, console: bool = False)` - Boolean flags
|
|
327
|
+
- `filters(add: str = None, remove: str = None, ...)` - Inline filter management
|
|
328
|
+
|
|
329
|
+
### JavaScript & Fetch Commands
|
|
330
|
+
- `js(code: str, wait_return: bool = True, await_promise: bool = False)` - Direct params
|
|
331
|
+
- `fetch(action: str, options: dict = None)` - Action pattern
|
|
332
|
+
- `resume(request: int, wait: float = 0.5, modifications: dict = None)` - Direct + dict
|
|
333
|
+
|
|
334
|
+
## Testing Your Command
|
|
335
|
+
|
|
336
|
+
1. **Type checking**: Run `basedpyright` to ensure types are correct
|
|
337
|
+
2. **Linting**: Run `ruff check` for code style
|
|
338
|
+
3. **REPL mode**: Test with `webtap` command
|
|
339
|
+
4. **MCP mode**: Test with `webtap --mcp` command
|
|
340
|
+
5. **Markdown rendering**: Verify output displays correctly
|
|
341
|
+
|
|
342
|
+
## TIPS.md Integration
|
|
343
|
+
|
|
344
|
+
All commands should be documented in `TIPS.md` for consistent help and MCP descriptions:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from webtap.commands._tips import get_tips, get_mcp_description
|
|
348
|
+
|
|
349
|
+
# Get MCP description (for fastmcp metadata)
|
|
350
|
+
mcp_desc = get_mcp_description("mycommand")
|
|
351
|
+
|
|
352
|
+
@app.command(
|
|
353
|
+
display="markdown",
|
|
354
|
+
fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"}
|
|
355
|
+
)
|
|
356
|
+
def mycommand(state, param: str) -> dict:
|
|
357
|
+
"""My command description."""
|
|
358
|
+
# ... implementation ...
|
|
359
|
+
|
|
360
|
+
# Include tips in response
|
|
361
|
+
tips = get_tips("mycommand", context={"id": some_id})
|
|
362
|
+
return table_response(
|
|
363
|
+
headers=headers,
|
|
364
|
+
rows=rows,
|
|
365
|
+
tips=tips # Automatically shown as "Next Steps"
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Adding to TIPS.md
|
|
370
|
+
|
|
371
|
+
Add a section for your command:
|
|
372
|
+
|
|
373
|
+
```markdown
|
|
374
|
+
### mycommand
|
|
375
|
+
Brief description of what the command does.
|
|
376
|
+
|
|
377
|
+
#### Examples
|
|
378
|
+
\```python
|
|
379
|
+
mycommand("param1") # Basic usage
|
|
380
|
+
mycommand("param2", flag=True) # With options
|
|
381
|
+
\```
|
|
382
|
+
|
|
383
|
+
#### Tips
|
|
384
|
+
- **Related command:** `other_command()` - does related thing
|
|
385
|
+
- **Advanced usage:** Use with `yet_another()` for X
|
|
386
|
+
- **Context aware:** Tips support {id} placeholders from context dict
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Checklist for New Commands
|
|
390
|
+
|
|
391
|
+
- [ ] Use `@app.command()` decorator with `display="markdown"`
|
|
392
|
+
- [ ] Add `fastmcp` metadata (type: "resource" or "tool")
|
|
393
|
+
- [ ] Use simple types only (no unions, no Optional)
|
|
394
|
+
- [ ] Add `# pyright: ignore[reportArgumentType]` for `dict = None`
|
|
395
|
+
- [ ] Import builders from `_builders.py`
|
|
396
|
+
- [ ] Use `table_response()`, `info_response()`, or `code_result_response()`
|
|
397
|
+
- [ ] Check connection with `check_connection()` if needed
|
|
398
|
+
- [ ] Add command section to `TIPS.md` with examples and tips
|
|
399
|
+
- [ ] Use `get_tips()` to show tips in response
|
|
400
|
+
- [ ] Document parameters clearly in docstring
|
|
401
|
+
- [ ] Test in both REPL and MCP modes
|