iflow-mcp_bethington-cheat-engine-server-python 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.
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/METADATA +16 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/RECORD +40 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/top_level.txt +1 -0
- server/cheatengine/__init__.py +19 -0
- server/cheatengine/ce_bridge.py +1670 -0
- server/cheatengine/lua_interface.py +460 -0
- server/cheatengine/table_parser.py +1221 -0
- server/config/__init__.py +20 -0
- server/config/settings.py +347 -0
- server/config/whitelist.py +378 -0
- server/gui_automation/__init__.py +43 -0
- server/gui_automation/core/__init__.py +8 -0
- server/gui_automation/core/integration.py +951 -0
- server/gui_automation/demos/__init__.py +8 -0
- server/gui_automation/demos/basic_demo.py +754 -0
- server/gui_automation/demos/notepad_demo.py +460 -0
- server/gui_automation/demos/simple_demo.py +319 -0
- server/gui_automation/tools/__init__.py +8 -0
- server/gui_automation/tools/mcp_tools.py +974 -0
- server/main.py +519 -0
- server/memory/__init__.py +0 -0
- server/memory/analyzer.py +0 -0
- server/memory/reader.py +0 -0
- server/memory/scanner.py +0 -0
- server/memory/symbols.py +0 -0
- server/process/__init__.py +16 -0
- server/process/launcher.py +608 -0
- server/process/manager.py +185 -0
- server/process/monitors.py +202 -0
- server/process/permissions.py +131 -0
- server/process_whitelist.json +119 -0
- server/pyautogui/__init__.py +0 -0
- server/utils/__init__.py +37 -0
- server/utils/data_types.py +368 -0
- server/utils/formatters.py +430 -0
- server/utils/validators.py +340 -0
- server/window_automation/__init__.py +59 -0
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Tools for PyAutoGUI Integration
|
|
4
|
+
|
|
5
|
+
This module defines comprehensive MCP tools that expose all PyAutoGUI functionality
|
|
6
|
+
through the MCP Cheat Engine Server, providing complete screen automation capabilities.
|
|
7
|
+
|
|
8
|
+
Tools Categories:
|
|
9
|
+
1. Screen Capture & Analysis (screenshots, pixel colors, image recognition)
|
|
10
|
+
2. Mouse Control (movement, clicking, dragging, scrolling)
|
|
11
|
+
3. Keyboard Automation (typing, key combinations, hotkeys)
|
|
12
|
+
4. Utility Functions (screen info, coordinates validation)
|
|
13
|
+
5. Advanced Features (templates, batch operations)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from mcp.types import Tool
|
|
17
|
+
from typing import Dict, Any, List, Optional
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
# Add current directory to path for imports
|
|
24
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# ==========================================
|
|
29
|
+
# SCREEN CAPTURE & ANALYSIS TOOLS
|
|
30
|
+
# ==========================================
|
|
31
|
+
|
|
32
|
+
SCREENSHOT_TOOL = Tool(
|
|
33
|
+
name="pyautogui_screenshot",
|
|
34
|
+
description="Take a screenshot of the entire screen or a specific region",
|
|
35
|
+
inputSchema={
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"region": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {"type": "integer"},
|
|
41
|
+
"minItems": 4,
|
|
42
|
+
"maxItems": 4,
|
|
43
|
+
"description": "Optional region [left, top, width, height] to capture. If not provided, captures entire screen",
|
|
44
|
+
"examples": [[100, 100, 800, 600], [0, 0, 1920, 1080]]
|
|
45
|
+
},
|
|
46
|
+
"save_path": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Optional file path to save the screenshot",
|
|
49
|
+
"examples": ["screenshot.png", "C:/temp/capture.jpg"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
PIXEL_COLOR_TOOL = Tool(
|
|
56
|
+
name="pyautogui_get_pixel_color",
|
|
57
|
+
description="Get the RGB color value of a pixel at specific screen coordinates",
|
|
58
|
+
inputSchema={
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"x": {
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"description": "X coordinate on screen",
|
|
64
|
+
"minimum": 0
|
|
65
|
+
},
|
|
66
|
+
"y": {
|
|
67
|
+
"type": "integer",
|
|
68
|
+
"description": "Y coordinate on screen",
|
|
69
|
+
"minimum": 0
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"required": ["x", "y"]
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
FIND_IMAGE_TOOL = Tool(
|
|
77
|
+
name="pyautogui_find_image",
|
|
78
|
+
description="Find an image on the screen using template matching",
|
|
79
|
+
inputSchema={
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"image_path": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Path to the image file to search for",
|
|
85
|
+
"examples": ["button.png", "C:/images/icon.jpg"]
|
|
86
|
+
},
|
|
87
|
+
"confidence": {
|
|
88
|
+
"type": "number",
|
|
89
|
+
"description": "Confidence level for image matching (0.0 to 1.0)",
|
|
90
|
+
"minimum": 0.0,
|
|
91
|
+
"maximum": 1.0,
|
|
92
|
+
"default": 0.8
|
|
93
|
+
},
|
|
94
|
+
"region": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": {"type": "integer"},
|
|
97
|
+
"minItems": 4,
|
|
98
|
+
"maxItems": 4,
|
|
99
|
+
"description": "Optional search region [left, top, width, height]",
|
|
100
|
+
"examples": [[100, 100, 800, 600]]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": ["image_path"]
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
FIND_ALL_IMAGES_TOOL = Tool(
|
|
108
|
+
name="pyautogui_find_all_images",
|
|
109
|
+
description="Find all instances of an image on the screen",
|
|
110
|
+
inputSchema={
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"image_path": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"description": "Path to the image file to search for"
|
|
116
|
+
},
|
|
117
|
+
"confidence": {
|
|
118
|
+
"type": "number",
|
|
119
|
+
"description": "Confidence level for image matching (0.0 to 1.0)",
|
|
120
|
+
"minimum": 0.0,
|
|
121
|
+
"maximum": 1.0,
|
|
122
|
+
"default": 0.8
|
|
123
|
+
},
|
|
124
|
+
"region": {
|
|
125
|
+
"type": "array",
|
|
126
|
+
"items": {"type": "integer"},
|
|
127
|
+
"minItems": 4,
|
|
128
|
+
"maxItems": 4,
|
|
129
|
+
"description": "Optional search region [left, top, width, height]"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"required": ["image_path"]
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# ==========================================
|
|
137
|
+
# MOUSE CONTROL TOOLS
|
|
138
|
+
# ==========================================
|
|
139
|
+
|
|
140
|
+
MOUSE_POSITION_TOOL = Tool(
|
|
141
|
+
name="pyautogui_get_mouse_position",
|
|
142
|
+
description="Get the current mouse cursor position",
|
|
143
|
+
inputSchema={
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {}
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
MOVE_MOUSE_TOOL = Tool(
|
|
150
|
+
name="pyautogui_move_mouse",
|
|
151
|
+
description="Move the mouse cursor to specific coordinates",
|
|
152
|
+
inputSchema={
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"x": {
|
|
156
|
+
"type": "integer",
|
|
157
|
+
"description": "Target X coordinate"
|
|
158
|
+
},
|
|
159
|
+
"y": {
|
|
160
|
+
"type": "integer",
|
|
161
|
+
"description": "Target Y coordinate"
|
|
162
|
+
},
|
|
163
|
+
"duration": {
|
|
164
|
+
"type": "number",
|
|
165
|
+
"description": "Duration of movement in seconds",
|
|
166
|
+
"minimum": 0.0,
|
|
167
|
+
"default": 0.5
|
|
168
|
+
},
|
|
169
|
+
"relative": {
|
|
170
|
+
"type": "boolean",
|
|
171
|
+
"description": "Whether coordinates are relative to current position",
|
|
172
|
+
"default": False
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"required": ["x", "y"]
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
CLICK_MOUSE_TOOL = Tool(
|
|
180
|
+
name="pyautogui_click_mouse",
|
|
181
|
+
description="Click the mouse at specific coordinates or current position",
|
|
182
|
+
inputSchema={
|
|
183
|
+
"type": "object",
|
|
184
|
+
"properties": {
|
|
185
|
+
"x": {
|
|
186
|
+
"type": "integer",
|
|
187
|
+
"description": "X coordinate to click (optional, uses current position if not provided)"
|
|
188
|
+
},
|
|
189
|
+
"y": {
|
|
190
|
+
"type": "integer",
|
|
191
|
+
"description": "Y coordinate to click (optional, uses current position if not provided)"
|
|
192
|
+
},
|
|
193
|
+
"button": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"enum": ["left", "right", "middle"],
|
|
196
|
+
"description": "Mouse button to click",
|
|
197
|
+
"default": "left"
|
|
198
|
+
},
|
|
199
|
+
"clicks": {
|
|
200
|
+
"type": "integer",
|
|
201
|
+
"description": "Number of clicks",
|
|
202
|
+
"minimum": 1,
|
|
203
|
+
"default": 1
|
|
204
|
+
},
|
|
205
|
+
"interval": {
|
|
206
|
+
"type": "number",
|
|
207
|
+
"description": "Interval between clicks in seconds",
|
|
208
|
+
"minimum": 0.0,
|
|
209
|
+
"default": 0.0
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
DRAG_MOUSE_TOOL = Tool(
|
|
216
|
+
name="pyautogui_drag_mouse",
|
|
217
|
+
description="Drag the mouse from start coordinates to end coordinates",
|
|
218
|
+
inputSchema={
|
|
219
|
+
"type": "object",
|
|
220
|
+
"properties": {
|
|
221
|
+
"start_x": {
|
|
222
|
+
"type": "integer",
|
|
223
|
+
"description": "Starting X coordinate"
|
|
224
|
+
},
|
|
225
|
+
"start_y": {
|
|
226
|
+
"type": "integer",
|
|
227
|
+
"description": "Starting Y coordinate"
|
|
228
|
+
},
|
|
229
|
+
"end_x": {
|
|
230
|
+
"type": "integer",
|
|
231
|
+
"description": "Ending X coordinate"
|
|
232
|
+
},
|
|
233
|
+
"end_y": {
|
|
234
|
+
"type": "integer",
|
|
235
|
+
"description": "Ending Y coordinate"
|
|
236
|
+
},
|
|
237
|
+
"duration": {
|
|
238
|
+
"type": "number",
|
|
239
|
+
"description": "Duration of drag operation in seconds",
|
|
240
|
+
"minimum": 0.0,
|
|
241
|
+
"default": 1.0
|
|
242
|
+
},
|
|
243
|
+
"button": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"enum": ["left", "right", "middle"],
|
|
246
|
+
"description": "Mouse button to drag with",
|
|
247
|
+
"default": "left"
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"required": ["start_x", "start_y", "end_x", "end_y"]
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
SCROLL_MOUSE_TOOL = Tool(
|
|
255
|
+
name="pyautogui_scroll_mouse",
|
|
256
|
+
description="Scroll the mouse wheel at specific coordinates or current position",
|
|
257
|
+
inputSchema={
|
|
258
|
+
"type": "object",
|
|
259
|
+
"properties": {
|
|
260
|
+
"clicks": {
|
|
261
|
+
"type": "integer",
|
|
262
|
+
"description": "Number of scroll clicks (positive for up, negative for down)"
|
|
263
|
+
},
|
|
264
|
+
"x": {
|
|
265
|
+
"type": "integer",
|
|
266
|
+
"description": "X coordinate to scroll at (optional, uses current position if not provided)"
|
|
267
|
+
},
|
|
268
|
+
"y": {
|
|
269
|
+
"type": "integer",
|
|
270
|
+
"description": "Y coordinate to scroll at (optional, uses current position if not provided)"
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"required": ["clicks"]
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# ==========================================
|
|
278
|
+
# KEYBOARD AUTOMATION TOOLS
|
|
279
|
+
# ==========================================
|
|
280
|
+
|
|
281
|
+
TYPE_TEXT_TOOL = Tool(
|
|
282
|
+
name="pyautogui_type_text",
|
|
283
|
+
description="Type text with optional interval between characters",
|
|
284
|
+
inputSchema={
|
|
285
|
+
"type": "object",
|
|
286
|
+
"properties": {
|
|
287
|
+
"text": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"description": "Text to type"
|
|
290
|
+
},
|
|
291
|
+
"interval": {
|
|
292
|
+
"type": "number",
|
|
293
|
+
"description": "Interval between characters in seconds",
|
|
294
|
+
"minimum": 0.0,
|
|
295
|
+
"default": 0.0
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"required": ["text"]
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
PRESS_KEY_TOOL = Tool(
|
|
303
|
+
name="pyautogui_press_key",
|
|
304
|
+
description="Press a specific key one or more times",
|
|
305
|
+
inputSchema={
|
|
306
|
+
"type": "object",
|
|
307
|
+
"properties": {
|
|
308
|
+
"key": {
|
|
309
|
+
"type": "string",
|
|
310
|
+
"description": "Key to press (e.g., 'enter', 'space', 'tab', 'a', 'f1')",
|
|
311
|
+
"examples": ["enter", "space", "tab", "esc", "f1", "ctrl", "shift", "a", "1"]
|
|
312
|
+
},
|
|
313
|
+
"presses": {
|
|
314
|
+
"type": "integer",
|
|
315
|
+
"description": "Number of times to press the key",
|
|
316
|
+
"minimum": 1,
|
|
317
|
+
"default": 1
|
|
318
|
+
},
|
|
319
|
+
"interval": {
|
|
320
|
+
"type": "number",
|
|
321
|
+
"description": "Interval between key presses in seconds",
|
|
322
|
+
"minimum": 0.0,
|
|
323
|
+
"default": 0.0
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
"required": ["key"]
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
KEY_COMBINATION_TOOL = Tool(
|
|
331
|
+
name="pyautogui_key_combination",
|
|
332
|
+
description="Press a combination of keys simultaneously (hotkeys)",
|
|
333
|
+
inputSchema={
|
|
334
|
+
"type": "object",
|
|
335
|
+
"properties": {
|
|
336
|
+
"keys": {
|
|
337
|
+
"type": "array",
|
|
338
|
+
"items": {"type": "string"},
|
|
339
|
+
"description": "List of keys to press simultaneously",
|
|
340
|
+
"examples": [["ctrl", "c"], ["ctrl", "shift", "n"], ["alt", "tab"], ["win", "r"]]
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"required": ["keys"]
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
HOLD_KEY_TOOL = Tool(
|
|
348
|
+
name="pyautogui_hold_key",
|
|
349
|
+
description="Hold a key down for a specified duration",
|
|
350
|
+
inputSchema={
|
|
351
|
+
"type": "object",
|
|
352
|
+
"properties": {
|
|
353
|
+
"key": {
|
|
354
|
+
"type": "string",
|
|
355
|
+
"description": "Key to hold down"
|
|
356
|
+
},
|
|
357
|
+
"duration": {
|
|
358
|
+
"type": "number",
|
|
359
|
+
"description": "Duration to hold the key in seconds",
|
|
360
|
+
"minimum": 0.0,
|
|
361
|
+
"default": 1.0
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"required": ["key"]
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# ==========================================
|
|
369
|
+
# UTILITY & CONFIGURATION TOOLS
|
|
370
|
+
# ==========================================
|
|
371
|
+
|
|
372
|
+
SCREEN_INFO_TOOL = Tool(
|
|
373
|
+
name="pyautogui_get_screen_info",
|
|
374
|
+
description="Get detailed information about the screen (resolution, size)",
|
|
375
|
+
inputSchema={
|
|
376
|
+
"type": "object",
|
|
377
|
+
"properties": {}
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
CHECK_ON_SCREEN_TOOL = Tool(
|
|
382
|
+
name="pyautogui_is_on_screen",
|
|
383
|
+
description="Check if given coordinates are within screen bounds",
|
|
384
|
+
inputSchema={
|
|
385
|
+
"type": "object",
|
|
386
|
+
"properties": {
|
|
387
|
+
"x": {
|
|
388
|
+
"type": "integer",
|
|
389
|
+
"description": "X coordinate to check"
|
|
390
|
+
},
|
|
391
|
+
"y": {
|
|
392
|
+
"type": "integer",
|
|
393
|
+
"description": "Y coordinate to check"
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
"required": ["x", "y"]
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
SET_PAUSE_TOOL = Tool(
|
|
401
|
+
name="pyautogui_set_pause",
|
|
402
|
+
description="Set the pause duration between PyAutoGUI actions",
|
|
403
|
+
inputSchema={
|
|
404
|
+
"type": "object",
|
|
405
|
+
"properties": {
|
|
406
|
+
"pause_duration": {
|
|
407
|
+
"type": "number",
|
|
408
|
+
"description": "Pause duration in seconds between actions",
|
|
409
|
+
"minimum": 0.0,
|
|
410
|
+
"default": 0.1
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
"required": ["pause_duration"]
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
SET_FAILSAFE_TOOL = Tool(
|
|
418
|
+
name="pyautogui_set_failsafe",
|
|
419
|
+
description="Enable or disable PyAutoGUI failsafe (emergency stop by moving mouse to corner)",
|
|
420
|
+
inputSchema={
|
|
421
|
+
"type": "object",
|
|
422
|
+
"properties": {
|
|
423
|
+
"enabled": {
|
|
424
|
+
"type": "boolean",
|
|
425
|
+
"description": "Whether to enable failsafe",
|
|
426
|
+
"default": True
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
"required": ["enabled"]
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
GET_AVAILABLE_KEYS_TOOL = Tool(
|
|
434
|
+
name="pyautogui_get_available_keys",
|
|
435
|
+
description="Get a list of all available keyboard keys that can be used with PyAutoGUI",
|
|
436
|
+
inputSchema={
|
|
437
|
+
"type": "object",
|
|
438
|
+
"properties": {}
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# ==========================================
|
|
443
|
+
# ADVANCED FEATURE TOOLS
|
|
444
|
+
# ==========================================
|
|
445
|
+
|
|
446
|
+
CREATE_TEMPLATE_TOOL = Tool(
|
|
447
|
+
name="pyautogui_create_image_template",
|
|
448
|
+
description="Create an image template from a screen region for future recognition",
|
|
449
|
+
inputSchema={
|
|
450
|
+
"type": "object",
|
|
451
|
+
"properties": {
|
|
452
|
+
"name": {
|
|
453
|
+
"type": "string",
|
|
454
|
+
"description": "Name for the template"
|
|
455
|
+
},
|
|
456
|
+
"x": {
|
|
457
|
+
"type": "integer",
|
|
458
|
+
"description": "X coordinate of region"
|
|
459
|
+
},
|
|
460
|
+
"y": {
|
|
461
|
+
"type": "integer",
|
|
462
|
+
"description": "Y coordinate of region"
|
|
463
|
+
},
|
|
464
|
+
"width": {
|
|
465
|
+
"type": "integer",
|
|
466
|
+
"description": "Width of region"
|
|
467
|
+
},
|
|
468
|
+
"height": {
|
|
469
|
+
"type": "integer",
|
|
470
|
+
"description": "Height of region"
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
"required": ["name", "x", "y", "width", "height"]
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
FIND_TEMPLATE_TOOL = Tool(
|
|
478
|
+
name="pyautogui_find_template",
|
|
479
|
+
description="Find a previously created image template on the screen",
|
|
480
|
+
inputSchema={
|
|
481
|
+
"type": "object",
|
|
482
|
+
"properties": {
|
|
483
|
+
"template_name": {
|
|
484
|
+
"type": "string",
|
|
485
|
+
"description": "Name of the template to find"
|
|
486
|
+
},
|
|
487
|
+
"confidence": {
|
|
488
|
+
"type": "number",
|
|
489
|
+
"description": "Confidence level for template matching",
|
|
490
|
+
"minimum": 0.0,
|
|
491
|
+
"maximum": 1.0,
|
|
492
|
+
"default": 0.8
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
"required": ["template_name"]
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# ==========================================
|
|
500
|
+
# BATCH OPERATION TOOLS
|
|
501
|
+
# ==========================================
|
|
502
|
+
|
|
503
|
+
BATCH_CLICKS_TOOL = Tool(
|
|
504
|
+
name="pyautogui_batch_clicks",
|
|
505
|
+
description="Perform multiple click operations in sequence",
|
|
506
|
+
inputSchema={
|
|
507
|
+
"type": "object",
|
|
508
|
+
"properties": {
|
|
509
|
+
"click_sequence": {
|
|
510
|
+
"type": "array",
|
|
511
|
+
"items": {
|
|
512
|
+
"type": "object",
|
|
513
|
+
"properties": {
|
|
514
|
+
"x": {"type": "integer"},
|
|
515
|
+
"y": {"type": "integer"},
|
|
516
|
+
"button": {"type": "string", "enum": ["left", "right", "middle"], "default": "left"},
|
|
517
|
+
"clicks": {"type": "integer", "minimum": 1, "default": 1},
|
|
518
|
+
"delay_after": {"type": "number", "minimum": 0.0, "default": 0.5}
|
|
519
|
+
},
|
|
520
|
+
"required": ["x", "y"]
|
|
521
|
+
},
|
|
522
|
+
"description": "Sequence of click operations to perform"
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
"required": ["click_sequence"]
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
BATCH_KEYS_TOOL = Tool(
|
|
530
|
+
name="pyautogui_batch_keys",
|
|
531
|
+
description="Perform multiple keyboard operations in sequence",
|
|
532
|
+
inputSchema={
|
|
533
|
+
"type": "object",
|
|
534
|
+
"properties": {
|
|
535
|
+
"key_sequence": {
|
|
536
|
+
"type": "array",
|
|
537
|
+
"items": {
|
|
538
|
+
"type": "object",
|
|
539
|
+
"properties": {
|
|
540
|
+
"operation": {"type": "string", "enum": ["type", "press", "combination", "hold"]},
|
|
541
|
+
"text": {"type": "string", "description": "Text to type (for 'type' operation)"},
|
|
542
|
+
"key": {"type": "string", "description": "Key to press (for 'press' or 'hold' operations)"},
|
|
543
|
+
"keys": {"type": "array", "items": {"type": "string"}, "description": "Keys for combination (for 'combination' operation)"},
|
|
544
|
+
"duration": {"type": "number", "minimum": 0.0, "description": "Duration for hold operation"},
|
|
545
|
+
"delay_after": {"type": "number", "minimum": 0.0, "default": 0.2}
|
|
546
|
+
},
|
|
547
|
+
"required": ["operation"]
|
|
548
|
+
},
|
|
549
|
+
"description": "Sequence of keyboard operations to perform"
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
"required": ["key_sequence"]
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Comprehensive list of all PyAutoGUI tools
|
|
557
|
+
ALL_PYAUTOGUI_TOOLS = [
|
|
558
|
+
# Screen capture & analysis
|
|
559
|
+
SCREENSHOT_TOOL,
|
|
560
|
+
PIXEL_COLOR_TOOL,
|
|
561
|
+
FIND_IMAGE_TOOL,
|
|
562
|
+
FIND_ALL_IMAGES_TOOL,
|
|
563
|
+
|
|
564
|
+
# Mouse control
|
|
565
|
+
MOUSE_POSITION_TOOL,
|
|
566
|
+
MOVE_MOUSE_TOOL,
|
|
567
|
+
CLICK_MOUSE_TOOL,
|
|
568
|
+
DRAG_MOUSE_TOOL,
|
|
569
|
+
SCROLL_MOUSE_TOOL,
|
|
570
|
+
|
|
571
|
+
# Keyboard automation
|
|
572
|
+
TYPE_TEXT_TOOL,
|
|
573
|
+
PRESS_KEY_TOOL,
|
|
574
|
+
KEY_COMBINATION_TOOL,
|
|
575
|
+
HOLD_KEY_TOOL,
|
|
576
|
+
|
|
577
|
+
# Utility & configuration
|
|
578
|
+
SCREEN_INFO_TOOL,
|
|
579
|
+
CHECK_ON_SCREEN_TOOL,
|
|
580
|
+
SET_PAUSE_TOOL,
|
|
581
|
+
SET_FAILSAFE_TOOL,
|
|
582
|
+
GET_AVAILABLE_KEYS_TOOL,
|
|
583
|
+
|
|
584
|
+
# Advanced features
|
|
585
|
+
CREATE_TEMPLATE_TOOL,
|
|
586
|
+
FIND_TEMPLATE_TOOL,
|
|
587
|
+
|
|
588
|
+
# Batch operations
|
|
589
|
+
BATCH_CLICKS_TOOL,
|
|
590
|
+
BATCH_KEYS_TOOL
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
# Tool handler class for MCP integration
|
|
594
|
+
class PyAutoGUIToolHandler:
|
|
595
|
+
"""Handler for all PyAutoGUI MCP tools"""
|
|
596
|
+
|
|
597
|
+
def __init__(self):
|
|
598
|
+
try:
|
|
599
|
+
from ..core.integration import get_pyautogui_controller
|
|
600
|
+
self.controller = get_pyautogui_controller()
|
|
601
|
+
self.available = True
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.error(f"Failed to initialize PyAutoGUI controller: {e}")
|
|
604
|
+
self.available = False
|
|
605
|
+
|
|
606
|
+
def _check_availability(self):
|
|
607
|
+
"""Check if PyAutoGUI is available"""
|
|
608
|
+
if not self.available:
|
|
609
|
+
return {
|
|
610
|
+
"success": False,
|
|
611
|
+
"error": "PyAutoGUI is not available. Please install: pip install pyautogui pillow opencv-python"
|
|
612
|
+
}
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
# Screen capture & analysis handlers
|
|
616
|
+
async def handle_screenshot(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
617
|
+
"""Handle screenshot tool"""
|
|
618
|
+
check = self._check_availability()
|
|
619
|
+
if check:
|
|
620
|
+
return check
|
|
621
|
+
|
|
622
|
+
region = arguments.get("region")
|
|
623
|
+
save_path = arguments.get("save_path")
|
|
624
|
+
|
|
625
|
+
if region and len(region) == 4:
|
|
626
|
+
region = tuple(region)
|
|
627
|
+
|
|
628
|
+
result = self.controller.take_screenshot(region, save_path)
|
|
629
|
+
return result.to_dict()
|
|
630
|
+
|
|
631
|
+
async def handle_pixel_color(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
632
|
+
"""Handle pixel color tool"""
|
|
633
|
+
check = self._check_availability()
|
|
634
|
+
if check:
|
|
635
|
+
return check
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
x = arguments["x"]
|
|
639
|
+
y = arguments["y"]
|
|
640
|
+
except KeyError as e:
|
|
641
|
+
return {
|
|
642
|
+
"success": False,
|
|
643
|
+
"error": f"Missing required argument: {e}",
|
|
644
|
+
"operation": "get_pixel_color",
|
|
645
|
+
"data": {},
|
|
646
|
+
"timestamp": time.time()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
result = self.controller.get_pixel_color(x, y)
|
|
650
|
+
return result.to_dict()
|
|
651
|
+
|
|
652
|
+
async def handle_find_image(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
653
|
+
"""Handle find image tool"""
|
|
654
|
+
check = self._check_availability()
|
|
655
|
+
if check:
|
|
656
|
+
return check
|
|
657
|
+
|
|
658
|
+
image_path = arguments["image_path"]
|
|
659
|
+
confidence = arguments.get("confidence", 0.8)
|
|
660
|
+
region = arguments.get("region")
|
|
661
|
+
|
|
662
|
+
if region and len(region) == 4:
|
|
663
|
+
region = tuple(region)
|
|
664
|
+
|
|
665
|
+
result = self.controller.find_image_on_screen(image_path, confidence, region)
|
|
666
|
+
return result.to_dict()
|
|
667
|
+
|
|
668
|
+
async def handle_find_all_images(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
669
|
+
"""Handle find all images tool"""
|
|
670
|
+
check = self._check_availability()
|
|
671
|
+
if check:
|
|
672
|
+
return check
|
|
673
|
+
|
|
674
|
+
image_path = arguments["image_path"]
|
|
675
|
+
confidence = arguments.get("confidence", 0.8)
|
|
676
|
+
region = arguments.get("region")
|
|
677
|
+
|
|
678
|
+
if region and len(region) == 4:
|
|
679
|
+
region = tuple(region)
|
|
680
|
+
|
|
681
|
+
result = self.controller.find_all_images_on_screen(image_path, confidence, region)
|
|
682
|
+
return result.to_dict()
|
|
683
|
+
|
|
684
|
+
# Mouse control handlers
|
|
685
|
+
async def handle_mouse_position(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
686
|
+
"""Handle mouse position tool"""
|
|
687
|
+
check = self._check_availability()
|
|
688
|
+
if check:
|
|
689
|
+
return check
|
|
690
|
+
|
|
691
|
+
result = self.controller.get_mouse_position()
|
|
692
|
+
return result.to_dict()
|
|
693
|
+
|
|
694
|
+
async def handle_move_mouse(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
695
|
+
"""Handle move mouse tool"""
|
|
696
|
+
check = self._check_availability()
|
|
697
|
+
if check:
|
|
698
|
+
return check
|
|
699
|
+
|
|
700
|
+
x = arguments["x"]
|
|
701
|
+
y = arguments["y"]
|
|
702
|
+
duration = arguments.get("duration", 0.5)
|
|
703
|
+
relative = arguments.get("relative", False)
|
|
704
|
+
|
|
705
|
+
result = self.controller.move_mouse(x, y, duration, relative)
|
|
706
|
+
return result.to_dict()
|
|
707
|
+
|
|
708
|
+
async def handle_click_mouse(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
709
|
+
"""Handle click mouse tool"""
|
|
710
|
+
check = self._check_availability()
|
|
711
|
+
if check:
|
|
712
|
+
return check
|
|
713
|
+
|
|
714
|
+
x = arguments.get("x")
|
|
715
|
+
y = arguments.get("y")
|
|
716
|
+
button = arguments.get("button", "left")
|
|
717
|
+
clicks = arguments.get("clicks", 1)
|
|
718
|
+
interval = arguments.get("interval", 0.0)
|
|
719
|
+
|
|
720
|
+
result = self.controller.click_mouse(x, y, button, clicks, interval)
|
|
721
|
+
return result.to_dict()
|
|
722
|
+
|
|
723
|
+
async def handle_drag_mouse(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
724
|
+
"""Handle drag mouse tool"""
|
|
725
|
+
check = self._check_availability()
|
|
726
|
+
if check:
|
|
727
|
+
return check
|
|
728
|
+
|
|
729
|
+
start_x = arguments["start_x"]
|
|
730
|
+
start_y = arguments["start_y"]
|
|
731
|
+
end_x = arguments["end_x"]
|
|
732
|
+
end_y = arguments["end_y"]
|
|
733
|
+
duration = arguments.get("duration", 1.0)
|
|
734
|
+
button = arguments.get("button", "left")
|
|
735
|
+
|
|
736
|
+
result = self.controller.drag_mouse(start_x, start_y, end_x, end_y, duration, button)
|
|
737
|
+
return result.to_dict()
|
|
738
|
+
|
|
739
|
+
async def handle_scroll_mouse(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
740
|
+
"""Handle scroll mouse tool"""
|
|
741
|
+
check = self._check_availability()
|
|
742
|
+
if check:
|
|
743
|
+
return check
|
|
744
|
+
|
|
745
|
+
clicks = arguments["clicks"]
|
|
746
|
+
x = arguments.get("x")
|
|
747
|
+
y = arguments.get("y")
|
|
748
|
+
|
|
749
|
+
result = self.controller.scroll_mouse(clicks, x, y)
|
|
750
|
+
return result.to_dict()
|
|
751
|
+
|
|
752
|
+
# Keyboard automation handlers
|
|
753
|
+
async def handle_type_text(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
754
|
+
"""Handle type text tool"""
|
|
755
|
+
check = self._check_availability()
|
|
756
|
+
if check:
|
|
757
|
+
return check
|
|
758
|
+
|
|
759
|
+
text = arguments["text"]
|
|
760
|
+
interval = arguments.get("interval", 0.0)
|
|
761
|
+
|
|
762
|
+
result = self.controller.type_text(text, interval)
|
|
763
|
+
return result.to_dict()
|
|
764
|
+
|
|
765
|
+
async def handle_press_key(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
766
|
+
"""Handle press key tool"""
|
|
767
|
+
check = self._check_availability()
|
|
768
|
+
if check:
|
|
769
|
+
return check
|
|
770
|
+
|
|
771
|
+
key = arguments["key"]
|
|
772
|
+
presses = arguments.get("presses", 1)
|
|
773
|
+
interval = arguments.get("interval", 0.0)
|
|
774
|
+
|
|
775
|
+
result = self.controller.press_key(key, presses, interval)
|
|
776
|
+
return result.to_dict()
|
|
777
|
+
|
|
778
|
+
async def handle_key_combination(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
779
|
+
"""Handle key combination tool"""
|
|
780
|
+
check = self._check_availability()
|
|
781
|
+
if check:
|
|
782
|
+
return check
|
|
783
|
+
|
|
784
|
+
keys = arguments["keys"]
|
|
785
|
+
|
|
786
|
+
result = self.controller.key_combination(keys)
|
|
787
|
+
return result.to_dict()
|
|
788
|
+
|
|
789
|
+
async def handle_hold_key(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
790
|
+
"""Handle hold key tool"""
|
|
791
|
+
check = self._check_availability()
|
|
792
|
+
if check:
|
|
793
|
+
return check
|
|
794
|
+
|
|
795
|
+
key = arguments["key"]
|
|
796
|
+
duration = arguments.get("duration", 1.0)
|
|
797
|
+
|
|
798
|
+
result = self.controller.hold_key(key, duration)
|
|
799
|
+
return result.to_dict()
|
|
800
|
+
|
|
801
|
+
# Utility & configuration handlers
|
|
802
|
+
async def handle_screen_info(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
803
|
+
"""Handle screen info tool"""
|
|
804
|
+
check = self._check_availability()
|
|
805
|
+
if check:
|
|
806
|
+
return check
|
|
807
|
+
|
|
808
|
+
result = self.controller.get_screen_info()
|
|
809
|
+
return result.to_dict()
|
|
810
|
+
|
|
811
|
+
async def handle_is_on_screen(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
812
|
+
"""Handle is on screen tool"""
|
|
813
|
+
check = self._check_availability()
|
|
814
|
+
if check:
|
|
815
|
+
return check
|
|
816
|
+
|
|
817
|
+
x = arguments["x"]
|
|
818
|
+
y = arguments["y"]
|
|
819
|
+
|
|
820
|
+
result = self.controller.is_on_screen(x, y)
|
|
821
|
+
return result.to_dict()
|
|
822
|
+
|
|
823
|
+
async def handle_set_pause(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
824
|
+
"""Handle set pause tool"""
|
|
825
|
+
check = self._check_availability()
|
|
826
|
+
if check:
|
|
827
|
+
return check
|
|
828
|
+
|
|
829
|
+
pause_duration = arguments["pause_duration"]
|
|
830
|
+
|
|
831
|
+
result = self.controller.set_pause(pause_duration)
|
|
832
|
+
return result.to_dict()
|
|
833
|
+
|
|
834
|
+
async def handle_set_failsafe(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
835
|
+
"""Handle set failsafe tool"""
|
|
836
|
+
check = self._check_availability()
|
|
837
|
+
if check:
|
|
838
|
+
return check
|
|
839
|
+
|
|
840
|
+
enabled = arguments["enabled"]
|
|
841
|
+
|
|
842
|
+
result = self.controller.set_failsafe(enabled)
|
|
843
|
+
return result.to_dict()
|
|
844
|
+
|
|
845
|
+
async def handle_get_available_keys(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
846
|
+
"""Handle get available keys tool"""
|
|
847
|
+
check = self._check_availability()
|
|
848
|
+
if check:
|
|
849
|
+
return check
|
|
850
|
+
|
|
851
|
+
result = self.controller.get_available_keys()
|
|
852
|
+
return result.to_dict()
|
|
853
|
+
|
|
854
|
+
# Advanced feature handlers
|
|
855
|
+
async def handle_create_template(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
856
|
+
"""Handle create template tool"""
|
|
857
|
+
check = self._check_availability()
|
|
858
|
+
if check:
|
|
859
|
+
return check
|
|
860
|
+
|
|
861
|
+
name = arguments["name"]
|
|
862
|
+
x = arguments["x"]
|
|
863
|
+
y = arguments["y"]
|
|
864
|
+
width = arguments["width"]
|
|
865
|
+
height = arguments["height"]
|
|
866
|
+
|
|
867
|
+
result = self.controller.create_image_template(name, x, y, width, height)
|
|
868
|
+
return result.to_dict()
|
|
869
|
+
|
|
870
|
+
async def handle_find_template(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
871
|
+
"""Handle find template tool"""
|
|
872
|
+
check = self._check_availability()
|
|
873
|
+
if check:
|
|
874
|
+
return check
|
|
875
|
+
|
|
876
|
+
template_name = arguments["template_name"]
|
|
877
|
+
confidence = arguments.get("confidence", 0.8)
|
|
878
|
+
|
|
879
|
+
result = self.controller.find_template_on_screen(template_name, confidence)
|
|
880
|
+
return result.to_dict()
|
|
881
|
+
|
|
882
|
+
# Batch operation handlers
|
|
883
|
+
async def handle_batch_clicks(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
884
|
+
"""Handle batch clicks tool"""
|
|
885
|
+
check = self._check_availability()
|
|
886
|
+
if check:
|
|
887
|
+
return check
|
|
888
|
+
|
|
889
|
+
import time
|
|
890
|
+
click_sequence = arguments["click_sequence"]
|
|
891
|
+
results = []
|
|
892
|
+
|
|
893
|
+
for i, click_data in enumerate(click_sequence):
|
|
894
|
+
x = click_data["x"]
|
|
895
|
+
y = click_data["y"]
|
|
896
|
+
button = click_data.get("button", "left")
|
|
897
|
+
clicks = click_data.get("clicks", 1)
|
|
898
|
+
delay_after = click_data.get("delay_after", 0.5)
|
|
899
|
+
|
|
900
|
+
result = self.controller.click_mouse(x, y, button, clicks)
|
|
901
|
+
results.append({
|
|
902
|
+
"step": i + 1,
|
|
903
|
+
"operation": result.to_dict(),
|
|
904
|
+
"coordinates": [x, y],
|
|
905
|
+
"button": button
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
if delay_after > 0:
|
|
909
|
+
time.sleep(delay_after)
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
"success": True,
|
|
913
|
+
"operation": "batch_clicks",
|
|
914
|
+
"data": {
|
|
915
|
+
"total_clicks": len(click_sequence),
|
|
916
|
+
"results": results
|
|
917
|
+
},
|
|
918
|
+
"timestamp": time.time()
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async def handle_batch_keys(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
922
|
+
"""Handle batch keys tool"""
|
|
923
|
+
check = self._check_availability()
|
|
924
|
+
if check:
|
|
925
|
+
return check
|
|
926
|
+
|
|
927
|
+
import time
|
|
928
|
+
key_sequence = arguments["key_sequence"]
|
|
929
|
+
results = []
|
|
930
|
+
|
|
931
|
+
for i, key_data in enumerate(key_sequence):
|
|
932
|
+
operation = key_data["operation"]
|
|
933
|
+
delay_after = key_data.get("delay_after", 0.2)
|
|
934
|
+
|
|
935
|
+
if operation == "type":
|
|
936
|
+
text = key_data.get("text", "")
|
|
937
|
+
result = self.controller.type_text(text)
|
|
938
|
+
elif operation == "press":
|
|
939
|
+
key = key_data.get("key", "")
|
|
940
|
+
result = self.controller.press_key(key)
|
|
941
|
+
elif operation == "combination":
|
|
942
|
+
keys = key_data.get("keys", [])
|
|
943
|
+
result = self.controller.key_combination(keys)
|
|
944
|
+
elif operation == "hold":
|
|
945
|
+
key = key_data.get("key", "")
|
|
946
|
+
duration = key_data.get("duration", 1.0)
|
|
947
|
+
result = self.controller.hold_key(key, duration)
|
|
948
|
+
else:
|
|
949
|
+
result = type('', (), {})() # Create empty object
|
|
950
|
+
result.to_dict = lambda: {"success": False, "error": f"Unknown operation: {operation}"}
|
|
951
|
+
|
|
952
|
+
results.append({
|
|
953
|
+
"step": i + 1,
|
|
954
|
+
"operation": result.to_dict(),
|
|
955
|
+
"input": key_data
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
if delay_after > 0:
|
|
959
|
+
time.sleep(delay_after)
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
"success": True,
|
|
963
|
+
"operation": "batch_keys",
|
|
964
|
+
"data": {
|
|
965
|
+
"total_operations": len(key_sequence),
|
|
966
|
+
"results": results
|
|
967
|
+
},
|
|
968
|
+
"timestamp": time.time()
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if __name__ == "__main__":
|
|
972
|
+
print(f"PyAutoGUI MCP Tools: {len(ALL_PYAUTOGUI_TOOLS)} tools defined")
|
|
973
|
+
for tool in ALL_PYAUTOGUI_TOOLS:
|
|
974
|
+
print(f" - {tool.name}: {tool.description}")
|