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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Chrome browser connection management commands."""
|
|
2
|
+
|
|
3
|
+
from replkit2.types import ExecutionContext
|
|
4
|
+
|
|
5
|
+
from webtap.app import app
|
|
6
|
+
from webtap.client import RPCError
|
|
7
|
+
from webtap.commands._builders import info_response, table_response, error_response
|
|
8
|
+
from webtap.commands._tips import get_mcp_description, get_tips
|
|
9
|
+
|
|
10
|
+
_connect_desc = get_mcp_description("connect")
|
|
11
|
+
_disconnect_desc = get_mcp_description("disconnect")
|
|
12
|
+
_clear_desc = get_mcp_description("clear")
|
|
13
|
+
|
|
14
|
+
# Truncation values for pages() REPL mode (compact display)
|
|
15
|
+
_PAGES_REPL_TRUNCATE = {
|
|
16
|
+
"Title": {"max": 20, "mode": "end"},
|
|
17
|
+
"URL": {"max": 30, "mode": "middle"},
|
|
18
|
+
"ID": {"max": 6, "mode": "end"},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Truncation values for pages() MCP mode (generous for LLM context)
|
|
22
|
+
_PAGES_MCP_TRUNCATE = {
|
|
23
|
+
"Title": {"max": 100, "mode": "end"},
|
|
24
|
+
"URL": {"max": 200, "mode": "middle"},
|
|
25
|
+
"ID": {"max": 50, "mode": "end"},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command(
|
|
30
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _connect_desc or ""}
|
|
31
|
+
)
|
|
32
|
+
def connect(state, page: int = None, page_id: str = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
33
|
+
"""Connect to Chrome page and enable all required domains.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
page: Connect by page index (0-based)
|
|
37
|
+
page_id: Connect by page ID
|
|
38
|
+
|
|
39
|
+
Note: If neither is specified, connects to first available page.
|
|
40
|
+
Cannot specify both page and page_id.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
connect() # First page
|
|
44
|
+
connect(page=2) # Third page (0-indexed)
|
|
45
|
+
connect(page_id="xyz") # Specific page ID
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Connection status in markdown
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
# Build params - default to page=0 when no params given
|
|
52
|
+
params = {}
|
|
53
|
+
if page is not None:
|
|
54
|
+
params["page"] = page
|
|
55
|
+
if page_id is not None:
|
|
56
|
+
params["page_id"] = page_id
|
|
57
|
+
if not params:
|
|
58
|
+
params["page"] = 0 # Connect to first page by default
|
|
59
|
+
|
|
60
|
+
result = state.client.call("connect", **params)
|
|
61
|
+
except RPCError as e:
|
|
62
|
+
return error_response(e.message)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return error_response(str(e))
|
|
65
|
+
|
|
66
|
+
# Success - return formatted info with full URL
|
|
67
|
+
return info_response(
|
|
68
|
+
title="Connection Established",
|
|
69
|
+
fields={"Page": result.get("title", "Unknown"), "URL": result.get("url", "")},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command(
|
|
74
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _disconnect_desc or ""}
|
|
75
|
+
)
|
|
76
|
+
def disconnect(state) -> dict:
|
|
77
|
+
"""Disconnect from Chrome."""
|
|
78
|
+
try:
|
|
79
|
+
state.client.call("disconnect")
|
|
80
|
+
except RPCError as e:
|
|
81
|
+
# INVALID_STATE means not connected
|
|
82
|
+
if e.code == "INVALID_STATE":
|
|
83
|
+
return info_response(title="Disconnect Status", fields={"Status": "Not connected"})
|
|
84
|
+
return error_response(e.message)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return error_response(str(e))
|
|
87
|
+
|
|
88
|
+
return info_response(title="Disconnect Status", fields={"Status": "Disconnected"})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command(
|
|
92
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _clear_desc or ""}
|
|
93
|
+
)
|
|
94
|
+
def clear(state, events: bool = True, console: bool = False) -> dict:
|
|
95
|
+
"""Clear various data stores.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
events: Clear CDP events (default: True)
|
|
99
|
+
console: Clear console messages (default: False)
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
clear() # Clear events only
|
|
103
|
+
clear(events=True, console=True) # Clear events and console
|
|
104
|
+
clear(events=False, console=True) # Console only
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Summary of what was cleared
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
result = state.client.call("clear", events=events, console=console)
|
|
111
|
+
except RPCError as e:
|
|
112
|
+
return error_response(e.message)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return error_response(str(e))
|
|
115
|
+
|
|
116
|
+
# Build cleared list from result
|
|
117
|
+
cleared = result.get("cleared", [])
|
|
118
|
+
|
|
119
|
+
if not cleared:
|
|
120
|
+
return info_response(
|
|
121
|
+
title="Clear Status",
|
|
122
|
+
fields={"Result": "Nothing to clear (specify events=True or console=True)"},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return info_response(title="Clear Status", fields={"Cleared": ", ".join(cleared)})
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command(
|
|
129
|
+
display="markdown",
|
|
130
|
+
fastmcp={"type": "resource", "mime_type": "text/markdown"},
|
|
131
|
+
)
|
|
132
|
+
def pages(state, _ctx: ExecutionContext = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
133
|
+
"""List available Chrome pages.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Table of available pages in markdown
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
result = state.client.call("pages")
|
|
140
|
+
pages_list = result.get("pages", [])
|
|
141
|
+
except RPCError as e:
|
|
142
|
+
return error_response(e.message)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return error_response(str(e))
|
|
145
|
+
|
|
146
|
+
# Format rows for table with FULL data
|
|
147
|
+
rows = [
|
|
148
|
+
{
|
|
149
|
+
"Index": str(i),
|
|
150
|
+
"Title": p.get("title", "Untitled"), # Full title
|
|
151
|
+
"URL": p.get("url", ""), # Full URL
|
|
152
|
+
"ID": p.get("id", ""), # Full ID
|
|
153
|
+
"Connected": "Yes" if p.get("is_connected") else "No",
|
|
154
|
+
}
|
|
155
|
+
for i, p in enumerate(pages_list)
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
# Get contextual tips
|
|
159
|
+
tips = None
|
|
160
|
+
if rows:
|
|
161
|
+
# Find connected page or first page
|
|
162
|
+
connected_row = next((r for r in rows if r["Connected"] == "Yes"), rows[0])
|
|
163
|
+
page_index = connected_row["Index"]
|
|
164
|
+
|
|
165
|
+
# Get page_id for the example page
|
|
166
|
+
connected_page = next((p for p in pages_list if str(pages_list.index(p)) == page_index), None)
|
|
167
|
+
page_id = connected_page.get("id", "")[:6] if connected_page else ""
|
|
168
|
+
|
|
169
|
+
tips = get_tips("pages", context={"index": page_index, "page_id": page_id})
|
|
170
|
+
|
|
171
|
+
# Build contextual warnings
|
|
172
|
+
warnings = []
|
|
173
|
+
if any(r["Connected"] == "Yes" for r in rows):
|
|
174
|
+
warnings.append("Already connected - call connect(page=N) to switch pages")
|
|
175
|
+
|
|
176
|
+
# Use mode-specific truncation
|
|
177
|
+
is_repl = _ctx and _ctx.is_repl()
|
|
178
|
+
truncate = _PAGES_REPL_TRUNCATE if is_repl else _PAGES_MCP_TRUNCATE
|
|
179
|
+
|
|
180
|
+
# Build markdown response
|
|
181
|
+
return table_response(
|
|
182
|
+
title="Chrome Pages",
|
|
183
|
+
headers=["Index", "Title", "URL", "ID", "Connected"],
|
|
184
|
+
rows=rows,
|
|
185
|
+
summary=f"{len(pages_list)} page{'s' if len(pages_list) != 1 else ''} available",
|
|
186
|
+
warnings=warnings if warnings else None,
|
|
187
|
+
tips=tips,
|
|
188
|
+
truncate=truncate,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "text/markdown"})
|
|
193
|
+
def status(state) -> dict:
|
|
194
|
+
"""Get connection status.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Status information in markdown
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
status_data = state.client.call("status")
|
|
201
|
+
except RPCError as e:
|
|
202
|
+
return error_response(e.message)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return error_response(str(e))
|
|
205
|
+
|
|
206
|
+
# Check if connected
|
|
207
|
+
if not status_data.get("connected"):
|
|
208
|
+
return error_response("Not connected to any page. Use connect() first.")
|
|
209
|
+
|
|
210
|
+
# Build formatted response with full URL
|
|
211
|
+
page = status_data.get("page", {})
|
|
212
|
+
return info_response(
|
|
213
|
+
title="Connection Status",
|
|
214
|
+
fields={
|
|
215
|
+
"Page": page.get("title", "Unknown"),
|
|
216
|
+
"URL": page.get("url", ""),
|
|
217
|
+
"Events": f"{status_data.get('events', {}).get('total', 0)} stored",
|
|
218
|
+
"Fetch": "Enabled" if status_data.get("fetch", {}).get("enabled") else "Disabled",
|
|
219
|
+
},
|
|
220
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Browser console message monitoring and display commands."""
|
|
2
|
+
|
|
3
|
+
from replkit2.types import ExecutionContext
|
|
4
|
+
|
|
5
|
+
from webtap.app import app
|
|
6
|
+
from webtap.client import RPCError
|
|
7
|
+
from webtap.commands._builders import table_response, error_response, format_timestamp
|
|
8
|
+
from webtap.commands._tips import get_tips
|
|
9
|
+
|
|
10
|
+
# Truncation values for REPL mode (compact display)
|
|
11
|
+
_REPL_TRUNCATE = {
|
|
12
|
+
"Message": {"max": 80, "mode": "end"},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Truncation values for MCP mode (generous for LLM context)
|
|
16
|
+
_MCP_TRUNCATE = {
|
|
17
|
+
"Message": {"max": 300, "mode": "end"},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command(
|
|
22
|
+
display="markdown",
|
|
23
|
+
fastmcp={"type": "resource", "mime_type": "text/markdown"},
|
|
24
|
+
)
|
|
25
|
+
def console(state, limit: int = 50, _ctx: ExecutionContext = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
26
|
+
"""Show console messages with full data.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
limit: Max results (default: 50)
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
console() # Recent console messages
|
|
33
|
+
console(limit=100) # Show more messages
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Table of console messages with full data
|
|
37
|
+
"""
|
|
38
|
+
# Get console messages via RPC
|
|
39
|
+
try:
|
|
40
|
+
result = state.client.call("console", limit=limit)
|
|
41
|
+
messages = result.get("messages", [])
|
|
42
|
+
except RPCError as e:
|
|
43
|
+
return error_response(e.message)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return error_response(str(e))
|
|
46
|
+
|
|
47
|
+
# Mode-specific configuration
|
|
48
|
+
is_repl = _ctx and _ctx.is_repl()
|
|
49
|
+
|
|
50
|
+
# Build rows with mode-specific formatting
|
|
51
|
+
rows = [
|
|
52
|
+
{
|
|
53
|
+
"ID": str(m.get("id", i)),
|
|
54
|
+
"Level": m.get("level", "unknown"),
|
|
55
|
+
"Source": m.get("source", ""),
|
|
56
|
+
"Message": m.get("message", ""),
|
|
57
|
+
# REPL: human-friendly time, MCP: raw timestamp for LLM
|
|
58
|
+
"Time": format_timestamp(m.get("timestamp")) if is_repl else (m.get("timestamp") or 0),
|
|
59
|
+
}
|
|
60
|
+
for i, m in enumerate(messages)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# Build response
|
|
64
|
+
warnings = []
|
|
65
|
+
if limit and len(messages) == limit:
|
|
66
|
+
warnings.append(f"Showing first {limit} messages (use limit parameter to see more)")
|
|
67
|
+
|
|
68
|
+
# Get contextual tips from TIPS.md
|
|
69
|
+
tips = None
|
|
70
|
+
if rows:
|
|
71
|
+
# Focus on error/warning messages for debugging
|
|
72
|
+
error_rows = [r for r in rows if r.get("Level", "").upper() in ["ERROR", "WARN", "WARNING"]]
|
|
73
|
+
example_id = error_rows[0]["ID"] if error_rows else rows[0]["ID"]
|
|
74
|
+
tips = get_tips("console", context={"id": example_id})
|
|
75
|
+
|
|
76
|
+
# Use mode-specific truncation
|
|
77
|
+
truncate = _REPL_TRUNCATE if is_repl else _MCP_TRUNCATE
|
|
78
|
+
|
|
79
|
+
return table_response(
|
|
80
|
+
title="Console Messages",
|
|
81
|
+
headers=["ID", "Level", "Source", "Message", "Time"],
|
|
82
|
+
rows=rows,
|
|
83
|
+
summary=f"{len(rows)} messages",
|
|
84
|
+
warnings=warnings,
|
|
85
|
+
tips=tips,
|
|
86
|
+
truncate=truncate,
|
|
87
|
+
)
|
webtap/commands/fetch.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""HTTP fetch request interception and debugging commands."""
|
|
2
|
+
|
|
3
|
+
from webtap.app import app
|
|
4
|
+
from webtap.client import RPCError
|
|
5
|
+
from webtap.commands._builders import error_response, info_response
|
|
6
|
+
from webtap.commands._tips import get_mcp_description, get_tips
|
|
7
|
+
|
|
8
|
+
_fetch_desc = get_mcp_description("fetch")
|
|
9
|
+
_resume_desc = get_mcp_description("resume")
|
|
10
|
+
_fail_desc = get_mcp_description("fail")
|
|
11
|
+
_fulfill_desc = get_mcp_description("fulfill")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command(
|
|
15
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _fetch_desc or ""}
|
|
16
|
+
)
|
|
17
|
+
def fetch(state, action: str, options: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
18
|
+
"""Control fetch interception.
|
|
19
|
+
|
|
20
|
+
When enabled, requests pause for inspection.
|
|
21
|
+
Use requests() to see paused items, resume() or fail() to proceed.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
action: Action to perform
|
|
25
|
+
- "enable" - Enable interception
|
|
26
|
+
- "disable" - Disable interception
|
|
27
|
+
- "status" - Get current status
|
|
28
|
+
options: Action-specific options
|
|
29
|
+
- For enable: {"response": true} - Also intercept responses
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
fetch("status") # Check status
|
|
33
|
+
fetch("enable") # Enable request stage
|
|
34
|
+
fetch("enable", {"response": true}) # Both stages
|
|
35
|
+
fetch("disable") # Disable
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Fetch interception status
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
if action == "disable":
|
|
42
|
+
state.client.call("fetch.disable")
|
|
43
|
+
return info_response(title="Fetch Disabled", fields={"Status": "Interception disabled"})
|
|
44
|
+
|
|
45
|
+
elif action == "enable":
|
|
46
|
+
response_stage = (options or {}).get("response", False)
|
|
47
|
+
result = state.client.call("fetch.enable", request=True, response=response_stage)
|
|
48
|
+
|
|
49
|
+
stages = "Request and Response stages" if result.get("response_stage") else "Request stage only"
|
|
50
|
+
return info_response(
|
|
51
|
+
title="Fetch Enabled",
|
|
52
|
+
fields={
|
|
53
|
+
"Stages": stages,
|
|
54
|
+
"Status": "Requests will pause",
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
elif action == "status":
|
|
59
|
+
status = state.client.call("status")
|
|
60
|
+
fetch_state = status.get("fetch", {})
|
|
61
|
+
fetch_enabled = fetch_state.get("enabled", False)
|
|
62
|
+
paused_count = fetch_state.get("paused_count", 0) if fetch_enabled else 0
|
|
63
|
+
|
|
64
|
+
return info_response(
|
|
65
|
+
title=f"Fetch Status: {'Enabled' if fetch_enabled else 'Disabled'}",
|
|
66
|
+
fields={
|
|
67
|
+
"Status": "Enabled" if fetch_enabled else "Disabled",
|
|
68
|
+
"Paused": f"{paused_count} requests paused" if fetch_enabled else "None",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
else:
|
|
73
|
+
return error_response(f"Unknown action: {action}")
|
|
74
|
+
|
|
75
|
+
except RPCError as e:
|
|
76
|
+
return error_response(e.message)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return error_response(str(e))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "text/markdown"})
|
|
82
|
+
def requests(state, limit: int = 50) -> dict:
|
|
83
|
+
"""Show paused requests. Equivalent to network(req_state="paused").
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
limit: Maximum items to show
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
requests() # Show all paused
|
|
90
|
+
request(583) # View request details
|
|
91
|
+
resume(583) # Continue request
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Table of paused requests/responses in markdown
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Get status to check if fetch is enabled
|
|
98
|
+
status = state.client.call("status")
|
|
99
|
+
if not status.get("fetch", {}).get("enabled", False):
|
|
100
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
101
|
+
|
|
102
|
+
# Delegate to network command with state filter
|
|
103
|
+
from webtap.commands.network import network
|
|
104
|
+
|
|
105
|
+
result = network(state, req_state="paused", limit=limit, show_all=True)
|
|
106
|
+
|
|
107
|
+
# Add fetch-specific tips if we have rows
|
|
108
|
+
if result.get("elements"):
|
|
109
|
+
# Find table element and extract first ID for tips
|
|
110
|
+
for element in result["elements"]:
|
|
111
|
+
if element.get("type") == "table":
|
|
112
|
+
rows = element.get("rows", [])
|
|
113
|
+
if rows and rows[0]:
|
|
114
|
+
example_id = rows[0].get("ID", 0)
|
|
115
|
+
tips = get_tips("requests", context={"id": example_id})
|
|
116
|
+
if tips:
|
|
117
|
+
# Add tips as alerts after table
|
|
118
|
+
tip_elements = [{"type": "alert", "content": tip, "level": "info"} for tip in tips]
|
|
119
|
+
# Insert after table
|
|
120
|
+
table_index = result["elements"].index(element)
|
|
121
|
+
result["elements"] = (
|
|
122
|
+
result["elements"][: table_index + 1]
|
|
123
|
+
+ tip_elements
|
|
124
|
+
+ result["elements"][table_index + 1 :]
|
|
125
|
+
)
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
except RPCError as e:
|
|
131
|
+
return error_response(e.message)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return error_response(str(e))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command(
|
|
137
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _resume_desc or ""}
|
|
138
|
+
)
|
|
139
|
+
def resume(state, request: int, wait: float = 0.5, modifications: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
140
|
+
"""Resume a paused request.
|
|
141
|
+
|
|
142
|
+
For Request stage, can modify:
|
|
143
|
+
url, method, headers, postData
|
|
144
|
+
|
|
145
|
+
For Response stage, can modify:
|
|
146
|
+
responseCode, responseHeaders
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
request: Request ID from network() table
|
|
150
|
+
wait: Wait time for next event in seconds (default: 0.5)
|
|
151
|
+
modifications: Request/response modifications dict
|
|
152
|
+
- {"url": "..."} - Change URL
|
|
153
|
+
- {"method": "POST"} - Change method
|
|
154
|
+
- {"headers": [{"name": "X-Custom", "value": "test"}]} - Set headers
|
|
155
|
+
- {"responseCode": 404} - Change response code
|
|
156
|
+
- {"responseHeaders": [...]} - Modify response headers
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
resume(583) # Simple resume
|
|
160
|
+
resume(583, wait=1.0) # Wait for redirect
|
|
161
|
+
resume(583, modifications={"url": "..."}) # Change URL
|
|
162
|
+
resume(583, modifications={"method": "POST"}) # Change method
|
|
163
|
+
resume(583, modifications={"headers": [{"name":"X-Custom","value":"test"}]})
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Continuation status with any follow-up events detected
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
# Get status to check if fetch is enabled
|
|
170
|
+
status = state.client.call("status")
|
|
171
|
+
if not status.get("fetch", {}).get("enabled", False):
|
|
172
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
173
|
+
|
|
174
|
+
# Resume via RPC (now uses HAR ID)
|
|
175
|
+
result = state.client.call("fetch.resume", id=request, modifications=modifications, wait=wait)
|
|
176
|
+
|
|
177
|
+
# Build concise status line
|
|
178
|
+
har_id = result.get("id", request)
|
|
179
|
+
outcome = result.get("outcome", "unknown")
|
|
180
|
+
resumed_from = result.get("resumed_from", "unknown")
|
|
181
|
+
|
|
182
|
+
if outcome == "response":
|
|
183
|
+
status_code = result.get("status", "?")
|
|
184
|
+
summary = f"ID {har_id} → paused at Response ({status_code})"
|
|
185
|
+
elif outcome == "redirect":
|
|
186
|
+
redirect_id = result.get("redirect_id", "?")
|
|
187
|
+
summary = f"ID {har_id} → redirected to ID {redirect_id}"
|
|
188
|
+
elif outcome == "complete":
|
|
189
|
+
summary = f"ID {har_id} → complete"
|
|
190
|
+
else:
|
|
191
|
+
summary = f"ID {har_id} → resumed from {resumed_from}"
|
|
192
|
+
|
|
193
|
+
fields = {"Result": summary}
|
|
194
|
+
if result.get("remaining", 0) > 0:
|
|
195
|
+
fields["Remaining"] = f"{result['remaining']} paused"
|
|
196
|
+
|
|
197
|
+
return info_response(title="Resumed", fields=fields)
|
|
198
|
+
|
|
199
|
+
except RPCError as e:
|
|
200
|
+
return error_response(e.message)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
return error_response(str(e))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.command(
|
|
206
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _fail_desc or ""}
|
|
207
|
+
)
|
|
208
|
+
def fail(state, request: int, reason: str = "BlockedByClient") -> dict:
|
|
209
|
+
"""Fail a paused request.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
request: Request ID from network() table
|
|
213
|
+
reason: CDP error reason (default: BlockedByClient)
|
|
214
|
+
Options: Failed, Aborted, TimedOut, AccessDenied,
|
|
215
|
+
ConnectionClosed, ConnectionReset, ConnectionRefused,
|
|
216
|
+
ConnectionAborted, ConnectionFailed, NameNotResolved,
|
|
217
|
+
InternetDisconnected, AddressUnreachable, BlockedByClient,
|
|
218
|
+
BlockedByResponse
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
fail(583) # Fail specific request
|
|
222
|
+
fail(583, reason="AccessDenied") # Fail with specific reason
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Failure status
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
# Get status to check if fetch is enabled
|
|
229
|
+
status = state.client.call("status")
|
|
230
|
+
if not status.get("fetch", {}).get("enabled", False):
|
|
231
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
232
|
+
|
|
233
|
+
# Fail via RPC (now uses HAR ID)
|
|
234
|
+
result = state.client.call("fetch.fail", id=request, reason=reason)
|
|
235
|
+
|
|
236
|
+
har_id = result.get("id", request)
|
|
237
|
+
summary = f"ID {har_id} → failed ({reason})"
|
|
238
|
+
|
|
239
|
+
fields = {"Result": summary}
|
|
240
|
+
if result.get("remaining", 0) > 0:
|
|
241
|
+
fields["Remaining"] = f"{result['remaining']} paused"
|
|
242
|
+
|
|
243
|
+
return info_response(title="Failed", fields=fields)
|
|
244
|
+
|
|
245
|
+
except RPCError as e:
|
|
246
|
+
return error_response(e.message)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
return error_response(str(e))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command(
|
|
252
|
+
display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _fulfill_desc or ""}
|
|
253
|
+
)
|
|
254
|
+
def fulfill(
|
|
255
|
+
state,
|
|
256
|
+
request: int,
|
|
257
|
+
body: str = "",
|
|
258
|
+
status: int = 200,
|
|
259
|
+
headers: list = None, # pyright: ignore[reportArgumentType]
|
|
260
|
+
) -> dict:
|
|
261
|
+
"""Fulfill a paused request with a custom response.
|
|
262
|
+
|
|
263
|
+
Returns a mock response without hitting the server. Useful for:
|
|
264
|
+
- Mock API responses during development
|
|
265
|
+
- Test error handling with specific status codes
|
|
266
|
+
- Offline development without backend
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
request: Request ID from network() table
|
|
270
|
+
body: Response body content (default: empty)
|
|
271
|
+
status: HTTP status code (default: 200)
|
|
272
|
+
headers: Response headers as list of {"name": "...", "value": "..."} dicts
|
|
273
|
+
|
|
274
|
+
Examples:
|
|
275
|
+
fulfill(583) # Empty 200 response
|
|
276
|
+
fulfill(583, body='{"ok": true}') # JSON response
|
|
277
|
+
fulfill(583, body="Not Found", status=404) # Error response
|
|
278
|
+
fulfill(583, headers=[{"name": "Content-Type", "value": "application/json"}])
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Fulfillment status
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
# Get status to check if fetch is enabled
|
|
285
|
+
fetch_status = state.client.call("status")
|
|
286
|
+
if not fetch_status.get("fetch", {}).get("enabled", False):
|
|
287
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
288
|
+
|
|
289
|
+
# Fulfill via RPC (uses HAR ID)
|
|
290
|
+
result = state.client.call(
|
|
291
|
+
"fetch.fulfill",
|
|
292
|
+
id=request,
|
|
293
|
+
response_code=status,
|
|
294
|
+
response_headers=headers,
|
|
295
|
+
body=body,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
har_id = result.get("id", request)
|
|
299
|
+
summary = f"ID {har_id} → fulfilled ({status})"
|
|
300
|
+
|
|
301
|
+
fields = {"Result": summary}
|
|
302
|
+
if result.get("remaining", 0) > 0:
|
|
303
|
+
fields["Remaining"] = f"{result['remaining']} paused"
|
|
304
|
+
|
|
305
|
+
return info_response(title="Fulfilled", fields=fields)
|
|
306
|
+
|
|
307
|
+
except RPCError as e:
|
|
308
|
+
return error_response(e.message)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return error_response(str(e))
|