ctrlcode 0.1.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.
Files changed (75) hide show
  1. ctrlcode/__init__.py +8 -0
  2. ctrlcode/agents/__init__.py +29 -0
  3. ctrlcode/agents/cleanup.py +388 -0
  4. ctrlcode/agents/communication.py +439 -0
  5. ctrlcode/agents/observability.py +421 -0
  6. ctrlcode/agents/react_loop.py +297 -0
  7. ctrlcode/agents/registry.py +211 -0
  8. ctrlcode/agents/result_parser.py +242 -0
  9. ctrlcode/agents/workflow.py +723 -0
  10. ctrlcode/analysis/__init__.py +28 -0
  11. ctrlcode/analysis/ast_diff.py +163 -0
  12. ctrlcode/analysis/bug_detector.py +149 -0
  13. ctrlcode/analysis/code_graphs.py +329 -0
  14. ctrlcode/analysis/semantic.py +205 -0
  15. ctrlcode/analysis/static.py +183 -0
  16. ctrlcode/analysis/synthesizer.py +281 -0
  17. ctrlcode/analysis/tests.py +189 -0
  18. ctrlcode/cleanup/__init__.py +16 -0
  19. ctrlcode/cleanup/auto_merge.py +350 -0
  20. ctrlcode/cleanup/doc_gardening.py +388 -0
  21. ctrlcode/cleanup/pr_automation.py +330 -0
  22. ctrlcode/cleanup/scheduler.py +356 -0
  23. ctrlcode/config.py +380 -0
  24. ctrlcode/embeddings/__init__.py +6 -0
  25. ctrlcode/embeddings/embedder.py +192 -0
  26. ctrlcode/embeddings/vector_store.py +213 -0
  27. ctrlcode/fuzzing/__init__.py +24 -0
  28. ctrlcode/fuzzing/analyzer.py +280 -0
  29. ctrlcode/fuzzing/budget.py +112 -0
  30. ctrlcode/fuzzing/context.py +665 -0
  31. ctrlcode/fuzzing/context_fuzzer.py +506 -0
  32. ctrlcode/fuzzing/derived_orchestrator.py +732 -0
  33. ctrlcode/fuzzing/oracle_adapter.py +135 -0
  34. ctrlcode/linters/__init__.py +11 -0
  35. ctrlcode/linters/hand_rolled_utils.py +221 -0
  36. ctrlcode/linters/yolo_parsing.py +217 -0
  37. ctrlcode/metrics/__init__.py +6 -0
  38. ctrlcode/metrics/dashboard.py +283 -0
  39. ctrlcode/metrics/tech_debt.py +663 -0
  40. ctrlcode/paths.py +68 -0
  41. ctrlcode/permissions.py +179 -0
  42. ctrlcode/providers/__init__.py +15 -0
  43. ctrlcode/providers/anthropic.py +138 -0
  44. ctrlcode/providers/base.py +77 -0
  45. ctrlcode/providers/openai.py +197 -0
  46. ctrlcode/providers/parallel.py +104 -0
  47. ctrlcode/server.py +871 -0
  48. ctrlcode/session/__init__.py +6 -0
  49. ctrlcode/session/baseline.py +57 -0
  50. ctrlcode/session/manager.py +967 -0
  51. ctrlcode/skills/__init__.py +10 -0
  52. ctrlcode/skills/builtin/commit.toml +29 -0
  53. ctrlcode/skills/builtin/docs.toml +25 -0
  54. ctrlcode/skills/builtin/refactor.toml +33 -0
  55. ctrlcode/skills/builtin/review.toml +28 -0
  56. ctrlcode/skills/builtin/test.toml +28 -0
  57. ctrlcode/skills/loader.py +111 -0
  58. ctrlcode/skills/registry.py +139 -0
  59. ctrlcode/storage/__init__.py +19 -0
  60. ctrlcode/storage/history_db.py +708 -0
  61. ctrlcode/tools/__init__.py +220 -0
  62. ctrlcode/tools/bash.py +112 -0
  63. ctrlcode/tools/browser.py +352 -0
  64. ctrlcode/tools/executor.py +153 -0
  65. ctrlcode/tools/explore.py +486 -0
  66. ctrlcode/tools/mcp.py +108 -0
  67. ctrlcode/tools/observability.py +561 -0
  68. ctrlcode/tools/registry.py +193 -0
  69. ctrlcode/tools/todo.py +291 -0
  70. ctrlcode/tools/update.py +266 -0
  71. ctrlcode/tools/webfetch.py +147 -0
  72. ctrlcode-0.1.0.dist-info/METADATA +93 -0
  73. ctrlcode-0.1.0.dist-info/RECORD +75 -0
  74. ctrlcode-0.1.0.dist-info/WHEEL +4 -0
  75. ctrlcode-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,220 @@
1
+ """MCP tool system for ctrl-code."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .mcp import MCPClient, MCPTool
6
+ from .registry import ToolRegistry, BuiltinTool
7
+ from .executor import ToolExecutor, ToolCallResult
8
+ from .explore import ExploreTools, EXPLORE_TOOL_SCHEMAS
9
+ from .todo import TodoTools, TODO_TOOL_SCHEMAS
10
+ from .bash import BashTools, BASH_TOOL_SCHEMAS
11
+ from .webfetch import WebFetchTools, WEBFETCH_TOOL_SCHEMAS
12
+ from .update import UpdateFileTools, UPDATE_TOOL_SCHEMAS
13
+ from .observability import ObservabilityTools, OBSERVABILITY_TOOL_SCHEMAS
14
+ from .browser import BrowserTools, BROWSER_TOOL_SCHEMAS, get_browser_tools
15
+
16
+
17
+ def setup_explore_tools(registry: ToolRegistry, workspace_root: str | Path) -> None:
18
+ """
19
+ Register built-in exploration tools.
20
+
21
+ Args:
22
+ registry: Tool registry to register tools in
23
+ workspace_root: Root directory for file exploration
24
+ """
25
+ explore = ExploreTools(workspace_root)
26
+
27
+ # Register each tool with its schema and function
28
+ for schema in EXPLORE_TOOL_SCHEMAS:
29
+ tool_name = schema["name"]
30
+
31
+ # Map tool name to method
32
+ function = getattr(explore, tool_name)
33
+
34
+ registry.register_builtin(
35
+ name=tool_name,
36
+ description=schema["description"],
37
+ input_schema=schema["input_schema"],
38
+ function=function,
39
+ )
40
+
41
+
42
+ def setup_todo_tools(registry: ToolRegistry, data_dir: str | Path) -> None:
43
+ """
44
+ Register built-in todo tools.
45
+
46
+ Args:
47
+ registry: Tool registry to register tools in
48
+ data_dir: Data directory for storing todos
49
+ """
50
+ todo = TodoTools(data_dir)
51
+
52
+ # Register each tool with its schema and function
53
+ for schema in TODO_TOOL_SCHEMAS:
54
+ tool_name = schema["name"]
55
+
56
+ # Map tool name to method
57
+ function = getattr(todo, tool_name)
58
+
59
+ registry.register_builtin(
60
+ name=tool_name,
61
+ description=schema["description"],
62
+ input_schema=schema["input_schema"],
63
+ function=function,
64
+ )
65
+
66
+ # Also register as task_* alias for user flexibility
67
+ task_alias = tool_name.replace("todo_", "task_")
68
+ registry.register_builtin(
69
+ name=task_alias,
70
+ description=schema["description"],
71
+ input_schema=schema["input_schema"],
72
+ function=function,
73
+ )
74
+
75
+
76
+ def setup_bash_tools(registry: ToolRegistry, workspace_root: str | Path) -> None:
77
+ """
78
+ Register built-in bash tools.
79
+
80
+ Args:
81
+ registry: Tool registry to register tools in
82
+ workspace_root: Root directory for command execution
83
+ """
84
+ bash = BashTools(workspace_root)
85
+
86
+ # Register each tool with its schema and function
87
+ for schema in BASH_TOOL_SCHEMAS:
88
+ tool_name = schema["name"]
89
+
90
+ # Map tool name to method
91
+ function = getattr(bash, tool_name)
92
+
93
+ registry.register_builtin(
94
+ name=tool_name,
95
+ description=schema["description"],
96
+ input_schema=schema["input_schema"],
97
+ function=function,
98
+ )
99
+
100
+
101
+ def setup_webfetch_tools(registry: ToolRegistry) -> None:
102
+ """
103
+ Register built-in web fetch tools.
104
+
105
+ Args:
106
+ registry: Tool registry to register tools in
107
+ """
108
+ webfetch = WebFetchTools()
109
+
110
+ # Register each tool with its schema and function
111
+ for schema in WEBFETCH_TOOL_SCHEMAS:
112
+ tool_name = schema["name"]
113
+
114
+ # Map tool name to method
115
+ function = getattr(webfetch, tool_name)
116
+
117
+ registry.register_builtin(
118
+ name=tool_name,
119
+ description=schema["description"],
120
+ input_schema=schema["input_schema"],
121
+ function=function,
122
+ )
123
+
124
+
125
+ def setup_update_tools(registry: ToolRegistry, workspace_root: str | Path) -> None:
126
+ """
127
+ Register built-in update file tools.
128
+
129
+ Args:
130
+ registry: Tool registry to register tools in
131
+ workspace_root: Root directory for file operations
132
+ """
133
+ update = UpdateFileTools(workspace_root)
134
+
135
+ # Register each tool with its schema and function
136
+ for schema in UPDATE_TOOL_SCHEMAS:
137
+ tool_name = schema["name"]
138
+
139
+ # Map tool name to method
140
+ function = getattr(update, tool_name)
141
+
142
+ registry.register_builtin(
143
+ name=tool_name,
144
+ description=schema["description"],
145
+ input_schema=schema["input_schema"],
146
+ function=function,
147
+ )
148
+
149
+
150
+ def setup_observability_tools(registry: ToolRegistry, log_dir: str | Path | None = None) -> None:
151
+ """
152
+ Register built-in observability tools.
153
+
154
+ Args:
155
+ registry: Tool registry to register tools in
156
+ log_dir: Directory containing log files (optional)
157
+ """
158
+ observability = ObservabilityTools(log_dir)
159
+
160
+ # Register each tool with its schema and function
161
+ for schema in OBSERVABILITY_TOOL_SCHEMAS:
162
+ tool_name = schema["name"]
163
+
164
+ # Map tool name to method
165
+ function = getattr(observability, tool_name)
166
+
167
+ registry.register_builtin(
168
+ name=tool_name,
169
+ description=schema["description"],
170
+ input_schema=schema["input_schema"],
171
+ function=function,
172
+ )
173
+
174
+
175
+ def setup_browser_tools(registry: ToolRegistry) -> None:
176
+ """
177
+ Register built-in browser automation tools.
178
+
179
+ Args:
180
+ registry: Tool registry to register tools in
181
+ """
182
+ browser = get_browser_tools()
183
+
184
+ # Register each tool with its schema and function
185
+ for schema in BROWSER_TOOL_SCHEMAS:
186
+ tool_name = schema["name"]
187
+
188
+ # Map tool name to method
189
+ function = getattr(browser, tool_name)
190
+
191
+ registry.register_builtin(
192
+ name=tool_name,
193
+ description=schema["description"],
194
+ input_schema=schema["input_schema"],
195
+ function=function,
196
+ )
197
+
198
+
199
+ __all__ = [
200
+ "MCPClient",
201
+ "MCPTool",
202
+ "ToolRegistry",
203
+ "BuiltinTool",
204
+ "ToolExecutor",
205
+ "ToolCallResult",
206
+ "ExploreTools",
207
+ "TodoTools",
208
+ "BashTools",
209
+ "WebFetchTools",
210
+ "UpdateFileTools",
211
+ "ObservabilityTools",
212
+ "BrowserTools",
213
+ "setup_explore_tools",
214
+ "setup_todo_tools",
215
+ "setup_bash_tools",
216
+ "setup_webfetch_tools",
217
+ "setup_update_tools",
218
+ "setup_observability_tools",
219
+ "setup_browser_tools",
220
+ ]
ctrlcode/tools/bash.py ADDED
@@ -0,0 +1,112 @@
1
+ """Built-in bash execution tools for running shell commands."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ class BashTools:
9
+ """Built-in tools for executing bash commands."""
10
+
11
+ def __init__(self, workspace_root: str | Path):
12
+ """
13
+ Initialize bash tools.
14
+
15
+ Args:
16
+ workspace_root: Root directory for command execution
17
+ """
18
+ self.workspace_root = Path(workspace_root).resolve()
19
+
20
+ def run_command(
21
+ self,
22
+ command: str,
23
+ timeout: int = 30,
24
+ cwd: str | None = None,
25
+ ) -> dict[str, Any]:
26
+ """
27
+ Execute a shell command.
28
+
29
+ Args:
30
+ command: Shell command to execute
31
+ timeout: Timeout in seconds (default 30, max 300)
32
+ cwd: Working directory for command (relative to workspace root)
33
+
34
+ Returns:
35
+ Dict with stdout, stderr, returncode, and metadata
36
+ """
37
+ try:
38
+ # Enforce timeout limits
39
+ if timeout > 300:
40
+ timeout = 300
41
+ if timeout < 1:
42
+ timeout = 1
43
+
44
+ # Determine working directory
45
+ work_dir = self.workspace_root
46
+ if cwd:
47
+ work_dir = (self.workspace_root / cwd).resolve()
48
+
49
+ # Security: ensure cwd is within workspace
50
+ if not str(work_dir).startswith(str(self.workspace_root)):
51
+ return {"error": "Working directory outside workspace"}
52
+
53
+ if not work_dir.exists():
54
+ return {"error": "Working directory does not exist"}
55
+
56
+ if not work_dir.is_dir():
57
+ return {"error": "Working directory is not a directory"}
58
+
59
+ # Execute command
60
+ result = subprocess.run(
61
+ command,
62
+ shell=True,
63
+ cwd=work_dir,
64
+ capture_output=True,
65
+ text=True,
66
+ timeout=timeout,
67
+ )
68
+
69
+ return {
70
+ "stdout": result.stdout,
71
+ "stderr": result.stderr,
72
+ "returncode": result.returncode,
73
+ "command": command,
74
+ "cwd": str(work_dir.relative_to(self.workspace_root)),
75
+ "success": result.returncode == 0,
76
+ }
77
+
78
+ except subprocess.TimeoutExpired:
79
+ return {
80
+ "error": f"Command timed out after {timeout} seconds",
81
+ "command": command,
82
+ }
83
+ except Exception as e:
84
+ return {"error": str(e), "command": command}
85
+
86
+
87
+ # Tool schemas for LLM providers (Anthropic/OpenAI format)
88
+ BASH_TOOL_SCHEMAS = [
89
+ {
90
+ "name": "run_command",
91
+ "description": "Execute a shell command in the workspace. Use for git operations, testing, build tools, etc. Commands run in a bash shell.",
92
+ "input_schema": {
93
+ "type": "object",
94
+ "properties": {
95
+ "command": {
96
+ "type": "string",
97
+ "description": "Shell command to execute (e.g., 'git status', 'npm test', 'make build')",
98
+ },
99
+ "timeout": {
100
+ "type": "integer",
101
+ "description": "Timeout in seconds (default 30, max 300)",
102
+ "default": 30,
103
+ },
104
+ "cwd": {
105
+ "type": "string",
106
+ "description": "Working directory relative to workspace root (e.g., 'src', 'tests')",
107
+ },
108
+ },
109
+ "required": ["command"],
110
+ },
111
+ },
112
+ ]
@@ -0,0 +1,352 @@
1
+ """Browser automation tools for UI validation via Chrome DevTools Protocol."""
2
+
3
+ import logging
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class BrowserTools:
12
+ """Browser automation tools for screenshot capture and DOM inspection."""
13
+
14
+ def __init__(self):
15
+ """Initialize browser tools."""
16
+ self._playwright = None
17
+ self._browser = None
18
+
19
+ async def _ensure_browser(self):
20
+ """Ensure playwright browser is launched."""
21
+ if self._browser is not None:
22
+ return
23
+
24
+ try:
25
+ from playwright.async_api import async_playwright
26
+ except ImportError:
27
+ raise RuntimeError(
28
+ "playwright not installed. Run: uv add playwright && uv run playwright install chromium"
29
+ )
30
+
31
+ if self._playwright is None:
32
+ self._playwright = await async_playwright().start()
33
+
34
+ self._browser = await self._playwright.chromium.launch(headless=True)
35
+
36
+ async def _cleanup(self):
37
+ """Cleanup browser resources."""
38
+ if self._browser:
39
+ await self._browser.close()
40
+ self._browser = None
41
+ if self._playwright:
42
+ await self._playwright.stop()
43
+ self._playwright = None
44
+
45
+ async def screenshot(
46
+ self,
47
+ url: str,
48
+ output: str | None = None,
49
+ full_page: bool = True,
50
+ viewport_width: int = 1280,
51
+ viewport_height: int = 720,
52
+ wait_for: str | None = None,
53
+ timeout: int = 30000,
54
+ ) -> dict[str, Any]:
55
+ """
56
+ Capture screenshot of a web page.
57
+
58
+ Args:
59
+ url: URL to navigate to
60
+ output: Output file path (defaults to temp file)
61
+ full_page: Capture full scrollable page (default: True)
62
+ viewport_width: Viewport width in pixels (default: 1280)
63
+ viewport_height: Viewport height in pixels (default: 720)
64
+ wait_for: CSS selector to wait for before screenshot (optional)
65
+ timeout: Page load timeout in milliseconds (default: 30000)
66
+
67
+ Returns:
68
+ Dict with:
69
+ - success: Whether screenshot succeeded
70
+ - file_path: Path to screenshot file
71
+ - url: URL that was captured
72
+ - width: Image width
73
+ - height: Image height
74
+ - error: Error message if failed
75
+ """
76
+ try:
77
+ await self._ensure_browser()
78
+
79
+ # Create context with viewport
80
+ context = await self._browser.new_context(
81
+ viewport={"width": viewport_width, "height": viewport_height}
82
+ )
83
+
84
+ # Create page
85
+ page = await context.new_page()
86
+
87
+ # Navigate to URL
88
+ await page.goto(url, timeout=timeout, wait_until="networkidle")
89
+
90
+ # Wait for specific element if requested
91
+ if wait_for:
92
+ await page.wait_for_selector(wait_for, timeout=timeout)
93
+
94
+ # Determine output path
95
+ if output is None:
96
+ screenshots_dir = Path.cwd() / ".ctrlcode" / "screenshots"
97
+ screenshots_dir.mkdir(parents=True, exist_ok=True)
98
+ temp_file = tempfile.NamedTemporaryFile(
99
+ suffix=".png", delete=False, dir=screenshots_dir
100
+ )
101
+ output = temp_file.name
102
+ temp_file.close()
103
+ else:
104
+ output = str(Path(output).resolve())
105
+ # Ensure output directory exists
106
+ Path(output).parent.mkdir(parents=True, exist_ok=True)
107
+
108
+ # Take screenshot
109
+ await page.screenshot(path=output, full_page=full_page)
110
+
111
+ # Get dimensions
112
+ if full_page:
113
+ dimensions = await page.evaluate(
114
+ "() => ({ width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight })"
115
+ )
116
+ else:
117
+ dimensions = {"width": viewport_width, "height": viewport_height}
118
+
119
+ await context.close()
120
+
121
+ return {
122
+ "success": True,
123
+ "file_path": output,
124
+ "url": url,
125
+ "width": dimensions["width"],
126
+ "height": dimensions["height"],
127
+ }
128
+
129
+ except Exception as e:
130
+ logger.error(f"Screenshot failed: {e}", exc_info=True)
131
+ return {"success": False, "error": str(e), "url": url}
132
+
133
+ async def dom_snapshot(
134
+ self,
135
+ url: str,
136
+ selector: str | None = None,
137
+ include_styles: bool = False,
138
+ timeout: int = 30000,
139
+ ) -> dict[str, Any]:
140
+ """
141
+ Capture DOM structure snapshot for HTML inspection.
142
+
143
+ Args:
144
+ url: URL to navigate to
145
+ selector: CSS selector to extract (defaults to full page)
146
+ include_styles: Include computed styles (default: False)
147
+ timeout: Page load timeout in milliseconds (default: 30000)
148
+
149
+ Returns:
150
+ Dict with:
151
+ - success: Whether snapshot succeeded
152
+ - url: URL that was captured
153
+ - html: HTML content
154
+ - text_content: Extracted text content
155
+ - element_count: Number of elements
156
+ - links: List of links found
157
+ - forms: List of forms found
158
+ - error: Error message if failed
159
+ """
160
+ try:
161
+ await self._ensure_browser()
162
+
163
+ # Create context
164
+ context = await self._browser.new_context()
165
+ page = await context.new_page()
166
+
167
+ # Navigate to URL
168
+ await page.goto(url, timeout=timeout, wait_until="networkidle")
169
+
170
+ # Get element handle
171
+ if selector:
172
+ element = await page.query_selector(selector)
173
+ if not element:
174
+ return {
175
+ "success": False,
176
+ "error": f"Selector '{selector}' not found",
177
+ "url": url,
178
+ }
179
+ html = await element.inner_html()
180
+ text_content = await element.text_content()
181
+ else:
182
+ html = await page.content()
183
+ text_content = await page.evaluate("() => document.body.textContent")
184
+
185
+ # Extract metadata
186
+ element_count = await page.evaluate("() => document.querySelectorAll('*').length")
187
+
188
+ # Extract links
189
+ links = await page.evaluate(
190
+ """() => {
191
+ return Array.from(document.querySelectorAll('a[href]')).map(a => ({
192
+ text: a.textContent.trim(),
193
+ href: a.href,
194
+ target: a.target
195
+ }));
196
+ }"""
197
+ )
198
+
199
+ # Extract forms
200
+ forms = await page.evaluate(
201
+ """() => {
202
+ return Array.from(document.querySelectorAll('form')).map(form => ({
203
+ action: form.action,
204
+ method: form.method,
205
+ fields: Array.from(form.querySelectorAll('input, textarea, select')).map(field => ({
206
+ name: field.name,
207
+ type: field.type,
208
+ id: field.id
209
+ }))
210
+ }));
211
+ }"""
212
+ )
213
+
214
+ await context.close()
215
+
216
+ return {
217
+ "success": True,
218
+ "url": url,
219
+ "html": html,
220
+ "text_content": text_content.strip() if text_content else "",
221
+ "element_count": element_count,
222
+ "links": links,
223
+ "forms": forms,
224
+ }
225
+
226
+ except Exception as e:
227
+ logger.error(f"DOM snapshot failed: {e}", exc_info=True)
228
+ return {"success": False, "error": str(e), "url": url}
229
+
230
+ async def close(self):
231
+ """Close browser and cleanup resources."""
232
+ await self._cleanup()
233
+
234
+
235
+ # Singleton instance
236
+ _browser_tools = None
237
+
238
+
239
+ def get_browser_tools() -> BrowserTools:
240
+ """Get or create singleton BrowserTools instance."""
241
+ global _browser_tools
242
+ if _browser_tools is None:
243
+ _browser_tools = BrowserTools()
244
+ return _browser_tools
245
+
246
+
247
+ async def cleanup_browser_tools():
248
+ """Cleanup singleton browser tools."""
249
+ global _browser_tools
250
+ if _browser_tools is not None:
251
+ await _browser_tools.close()
252
+ _browser_tools = None
253
+
254
+
255
+ BROWSER_TOOL_SCHEMAS = [
256
+ {
257
+ "name": "screenshot",
258
+ "description": """Capture screenshot of a web page for UI validation.
259
+
260
+ Useful for verifying:
261
+ - Page renders correctly after changes
262
+ - UI elements are visible and positioned correctly
263
+ - Visual regressions
264
+ - Responsive design at different viewports
265
+
266
+ Returns file path to PNG screenshot that can be analyzed.
267
+
268
+ Examples:
269
+ - screenshot(url="http://localhost:8000") - Capture full page
270
+ - screenshot(url="http://localhost:8000/login", wait_for=".login-form") - Wait for element
271
+ - screenshot(url="http://localhost:8000", full_page=False) - Viewport only""",
272
+ "input_schema": {
273
+ "type": "object",
274
+ "properties": {
275
+ "url": {
276
+ "type": "string",
277
+ "description": "URL to navigate to and capture"
278
+ },
279
+ "output": {
280
+ "type": "string",
281
+ "description": "Output file path (optional, defaults to temp file)"
282
+ },
283
+ "full_page": {
284
+ "type": "boolean",
285
+ "description": "Capture full scrollable page (default: true)",
286
+ "default": True
287
+ },
288
+ "viewport_width": {
289
+ "type": "integer",
290
+ "description": "Viewport width in pixels (default: 1280)",
291
+ "default": 1280
292
+ },
293
+ "viewport_height": {
294
+ "type": "integer",
295
+ "description": "Viewport height in pixels (default: 720)",
296
+ "default": 720
297
+ },
298
+ "wait_for": {
299
+ "type": "string",
300
+ "description": "CSS selector to wait for before screenshot (optional)"
301
+ },
302
+ "timeout": {
303
+ "type": "integer",
304
+ "description": "Page load timeout in milliseconds (default: 30000)",
305
+ "default": 30000
306
+ }
307
+ },
308
+ "required": ["url"]
309
+ }
310
+ },
311
+ {
312
+ "name": "dom_snapshot",
313
+ "description": """Capture DOM structure snapshot for HTML inspection.
314
+
315
+ Useful for:
316
+ - Verifying page structure and content
317
+ - Extracting text, links, forms
318
+ - Debugging rendering issues
319
+ - Validating SEO elements
320
+
321
+ Returns HTML content, text, element count, links, and forms.
322
+
323
+ Examples:
324
+ - dom_snapshot(url="http://localhost:8000") - Full page snapshot
325
+ - dom_snapshot(url="http://localhost:8000", selector=".main-content") - Specific element
326
+ - dom_snapshot(url="http://localhost:8000/api/docs", include_styles=True) - With styles""",
327
+ "input_schema": {
328
+ "type": "object",
329
+ "properties": {
330
+ "url": {
331
+ "type": "string",
332
+ "description": "URL to navigate to and capture"
333
+ },
334
+ "selector": {
335
+ "type": "string",
336
+ "description": "CSS selector to extract (optional, defaults to full page)"
337
+ },
338
+ "include_styles": {
339
+ "type": "boolean",
340
+ "description": "Include computed styles (default: false)",
341
+ "default": False
342
+ },
343
+ "timeout": {
344
+ "type": "integer",
345
+ "description": "Page load timeout in milliseconds (default: 30000)",
346
+ "default": 30000
347
+ }
348
+ },
349
+ "required": ["url"]
350
+ }
351
+ }
352
+ ]