smooth-py 0.3.5.dev20251107__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.
Potentially problematic release.
This version of smooth-py might be problematic. Click here for more details.
smooth/mcp/__init__.py
ADDED
smooth/mcp/server.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""Smooth SDK MCP Server Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the SmoothMCP class that integrates the Smooth SDK
|
|
4
|
+
with the Model Context Protocol for AI assistant interactions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
from typing import Annotated, Any, Dict, Literal, Optional
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from fastmcp import Context, FastMCP
|
|
13
|
+
from fastmcp.exceptions import ResourceError
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
except ImportError as e:
|
|
16
|
+
raise ImportError("FastMCP is required for MCP functionality. Install with: pip install smooth-py[mcp]") from e
|
|
17
|
+
|
|
18
|
+
from .. import ApiError, SmoothAsyncClient, TimeoutError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SmoothMCP:
|
|
22
|
+
"""MCP server for Smooth SDK browser automation agent.
|
|
23
|
+
|
|
24
|
+
This class provides a Model Context Protocol server that exposes
|
|
25
|
+
Smooth SDK functionality to AI assistants and other MCP clients.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from smooth.mcp import SmoothMCP
|
|
30
|
+
|
|
31
|
+
# Create and run the MCP server
|
|
32
|
+
mcp = SmoothMCP(api_key="your-api-key")
|
|
33
|
+
mcp.run() # Runs with STDIO transport by default
|
|
34
|
+
|
|
35
|
+
# Or run with HTTP transport
|
|
36
|
+
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: Optional[str] = None,
|
|
43
|
+
server_name: str = "Smooth Browser Agent",
|
|
44
|
+
base_url: Optional[str] = None,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize the Smooth MCP server.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_key: Smooth API key. If not provided, will use CIRCLEMIND_API_KEY env var.
|
|
50
|
+
server_name: Name for the MCP server.
|
|
51
|
+
base_url: Base URL for the Smooth API (optional).
|
|
52
|
+
"""
|
|
53
|
+
self.api_key = api_key or os.getenv("CIRCLEMIND_API_KEY")
|
|
54
|
+
if not self.api_key:
|
|
55
|
+
raise ValueError("API key is required. Provide it directly or set CIRCLEMIND_API_KEY environment variable.")
|
|
56
|
+
|
|
57
|
+
self.base_url = base_url
|
|
58
|
+
self.server_name = server_name
|
|
59
|
+
|
|
60
|
+
# Initialize FastMCP server
|
|
61
|
+
self._mcp = FastMCP(server_name)
|
|
62
|
+
self._smooth_client: Optional[SmoothAsyncClient] = None
|
|
63
|
+
|
|
64
|
+
# Register tools and resources
|
|
65
|
+
self._register_tools()
|
|
66
|
+
self._register_resources()
|
|
67
|
+
|
|
68
|
+
async def _get_smooth_client(self) -> SmoothAsyncClient:
|
|
69
|
+
"""Get or create the Smooth client instance."""
|
|
70
|
+
if self._smooth_client is None:
|
|
71
|
+
kwargs = {"api_key": self.api_key}
|
|
72
|
+
if self.base_url:
|
|
73
|
+
kwargs["base_url"] = self.base_url
|
|
74
|
+
self._smooth_client = SmoothAsyncClient(**kwargs)
|
|
75
|
+
return self._smooth_client
|
|
76
|
+
|
|
77
|
+
def _register_tools(self):
|
|
78
|
+
"""Register MCP tools."""
|
|
79
|
+
|
|
80
|
+
@self._mcp.tool(
|
|
81
|
+
name="run_browser_task",
|
|
82
|
+
description=(
|
|
83
|
+
"Execute browser automation tasks using natural language descriptions. "
|
|
84
|
+
"Supports both desktop and mobile devices with optional profile state, recording, and advanced configuration."
|
|
85
|
+
),
|
|
86
|
+
annotations={"title": "Run Browser Task", "readOnlyHint": False, "destructiveHint": False, "openWorldHint": True},
|
|
87
|
+
)
|
|
88
|
+
async def run_browser_task(
|
|
89
|
+
ctx: Context,
|
|
90
|
+
task: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
Field(
|
|
93
|
+
description=(
|
|
94
|
+
"Natural language description of the browser automation task to perform "
|
|
95
|
+
"(e.g., 'Go to Google and search for FastMCP', 'Fill out the contact form with test data at this url: ...')"
|
|
96
|
+
)
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
device: Annotated[
|
|
100
|
+
Literal["desktop", "mobile"],
|
|
101
|
+
Field(
|
|
102
|
+
description=(
|
|
103
|
+
"Device type for browser automation. Desktop provides full browser experience, "
|
|
104
|
+
"mobile uses a mobile viewport. Mobile is preferred as mobile web pages are lighter and easier to interact with"
|
|
105
|
+
)
|
|
106
|
+
),
|
|
107
|
+
] = "mobile",
|
|
108
|
+
max_steps: Annotated[
|
|
109
|
+
int,
|
|
110
|
+
Field(
|
|
111
|
+
description=(
|
|
112
|
+
"Maximum number of steps the agent can take to complete the task. "
|
|
113
|
+
"Higher values allow more complex multi-step workflows"
|
|
114
|
+
),
|
|
115
|
+
ge=2,
|
|
116
|
+
le=128,
|
|
117
|
+
),
|
|
118
|
+
] = 32,
|
|
119
|
+
enable_recording: Annotated[
|
|
120
|
+
bool,
|
|
121
|
+
Field(
|
|
122
|
+
description="Whether to record video of the task execution. Recordings can be used for debugging and verification"
|
|
123
|
+
),
|
|
124
|
+
] = True,
|
|
125
|
+
profile_id: Annotated[
|
|
126
|
+
Optional[str],
|
|
127
|
+
Field(
|
|
128
|
+
description=(
|
|
129
|
+
"Browser profile ID to pass login credentials to the agent. "
|
|
130
|
+
"The user must have already created and manually populated a profile and provide the profile ID."
|
|
131
|
+
)
|
|
132
|
+
),
|
|
133
|
+
] = None,
|
|
134
|
+
# stealth_mode: Annotated[
|
|
135
|
+
# bool,
|
|
136
|
+
# Field(
|
|
137
|
+
# description=(
|
|
138
|
+
# "Run browser in stealth mode to avoid detection by anti-bot systems. "
|
|
139
|
+
# "Useful for accessing sites that block automated browsers"
|
|
140
|
+
# )
|
|
141
|
+
# ),
|
|
142
|
+
# ] = True,
|
|
143
|
+
# proxy_server: Annotated[
|
|
144
|
+
# Optional[str],
|
|
145
|
+
# Field(
|
|
146
|
+
# description=(
|
|
147
|
+
# "Proxy server URL to route browser traffic through. "
|
|
148
|
+
# "Must include protocol (e.g., 'http://proxy.example.com:8080')"
|
|
149
|
+
# )
|
|
150
|
+
# ),
|
|
151
|
+
# ] = None,
|
|
152
|
+
# proxy_username: Annotated[Optional[str], Field(description="Username for proxy server authentication")] = None,
|
|
153
|
+
# proxy_password: Annotated[Optional[str], Field(description="Password for proxy server authentication")] = None,
|
|
154
|
+
timeout: Annotated[
|
|
155
|
+
int,
|
|
156
|
+
Field(
|
|
157
|
+
description=(
|
|
158
|
+
"Maximum time to wait for task completion in seconds. Increase for complex tasks that may take longer."
|
|
159
|
+
" Max 15 minutes."
|
|
160
|
+
),
|
|
161
|
+
ge=30,
|
|
162
|
+
le=60*15,
|
|
163
|
+
),
|
|
164
|
+
] = 60*15,
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
"""Run a browser automation task using the Smooth SDK.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
ctx: MCP context for logging and communication
|
|
170
|
+
task: Natural language description of the task to perform
|
|
171
|
+
device: Device type ("desktop" or "mobile", default: "mobile")
|
|
172
|
+
max_steps: Maximum steps for the agent (2-128, default: 32)
|
|
173
|
+
enable_recording: Whether to record the execution (default: True)
|
|
174
|
+
profile_id: Optional browser profile ID to maintain state
|
|
175
|
+
stealth_mode: Run in stealth mode to avoid detection (default: False)
|
|
176
|
+
proxy_server: Proxy server URL (must include protocol)
|
|
177
|
+
proxy_username: Proxy authentication username
|
|
178
|
+
proxy_password: Proxy authentication password
|
|
179
|
+
timeout: Maximum time to wait for completion in seconds (default: 300)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dictionary containing task results, status, and URLs
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
await ctx.info(f"Starting browser task: {task}")
|
|
186
|
+
|
|
187
|
+
# Validate device parameter
|
|
188
|
+
if device not in ["desktop", "mobile"]:
|
|
189
|
+
raise ValueError("Device must be 'desktop' or 'mobile'")
|
|
190
|
+
|
|
191
|
+
# Validate max_steps
|
|
192
|
+
if not (2 <= max_steps <= 128):
|
|
193
|
+
raise ValueError("max_steps must be between 2 and 128")
|
|
194
|
+
|
|
195
|
+
client = await self._get_smooth_client()
|
|
196
|
+
|
|
197
|
+
# Submit the task
|
|
198
|
+
task_handle = await client.run(
|
|
199
|
+
task=task,
|
|
200
|
+
device=device, # type: ignore
|
|
201
|
+
max_steps=max_steps,
|
|
202
|
+
enable_recording=enable_recording,
|
|
203
|
+
profile_id=profile_id,
|
|
204
|
+
stealth_mode=False,
|
|
205
|
+
proxy_server=None,
|
|
206
|
+
proxy_username=None,
|
|
207
|
+
proxy_password=None,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
await ctx.info(f"Task submitted with ID: {task_handle.id}")
|
|
211
|
+
|
|
212
|
+
# Wait for completion
|
|
213
|
+
await ctx.info("Waiting for task completion...")
|
|
214
|
+
result = await task_handle.result(timeout=timeout)
|
|
215
|
+
|
|
216
|
+
# Prepare response
|
|
217
|
+
response = {
|
|
218
|
+
"task_id": task_handle.id,
|
|
219
|
+
"status": result.status,
|
|
220
|
+
"output": result.output,
|
|
221
|
+
"credits_used": result.credits_used,
|
|
222
|
+
"device": result.device,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if result.recording_url:
|
|
226
|
+
response["recording_url"] = result.recording_url
|
|
227
|
+
await ctx.info(f"Recording available at: {result.recording_url}")
|
|
228
|
+
|
|
229
|
+
if result.status == "done":
|
|
230
|
+
await ctx.info("Task completed successfully!")
|
|
231
|
+
else:
|
|
232
|
+
await ctx.error(f"Task failed with status: {result.status}")
|
|
233
|
+
|
|
234
|
+
return response
|
|
235
|
+
|
|
236
|
+
except ApiError as e:
|
|
237
|
+
error_msg = f"Smooth API error: {e.detail}"
|
|
238
|
+
await ctx.error(error_msg)
|
|
239
|
+
raise Exception(error_msg) from None
|
|
240
|
+
except TimeoutError as e:
|
|
241
|
+
error_msg = f"Task timed out: {str(e)}"
|
|
242
|
+
await ctx.error(error_msg)
|
|
243
|
+
raise Exception(error_msg) from None
|
|
244
|
+
except Exception as e:
|
|
245
|
+
error_msg = f"Unexpected error: {str(e)}"
|
|
246
|
+
await ctx.error(error_msg)
|
|
247
|
+
raise Exception(error_msg) from None
|
|
248
|
+
|
|
249
|
+
@self._mcp.tool(
|
|
250
|
+
name="create_browser_profile",
|
|
251
|
+
description=(
|
|
252
|
+
"Create a new browser profile to store user credentials. "
|
|
253
|
+
"Returns a profile ID and live URL that need to be returned to the user to log in into various websites. "
|
|
254
|
+
"Once the user confirms they have logged in to the desired websites, the profile ID can be used in subsequent tasks "
|
|
255
|
+
" to access the user's authenticated state."
|
|
256
|
+
),
|
|
257
|
+
annotations={"title": "Create Browser profile", "readOnlyHint": False, "destructiveHint": False},
|
|
258
|
+
)
|
|
259
|
+
async def create_browser_profile(
|
|
260
|
+
ctx: Context,
|
|
261
|
+
profile_id: Annotated[
|
|
262
|
+
Optional[str],
|
|
263
|
+
Field(
|
|
264
|
+
description=(
|
|
265
|
+
"Optional custom profile ID. If not provided, a random one will be generated. "
|
|
266
|
+
"Use meaningful names for easier profile management"
|
|
267
|
+
)
|
|
268
|
+
),
|
|
269
|
+
] = None,
|
|
270
|
+
) -> Dict[str, Any]:
|
|
271
|
+
"""Create a new browser profile to maintain state between tasks.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
ctx: MCP context for logging and communication
|
|
275
|
+
profile_id: Optional custom profile ID. If not provided, a random one will be generated.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dictionary containing profile details and live URL
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
await ctx.info("Creating browser profile" + (f" with ID: {profile_id}" if profile_id else ""))
|
|
282
|
+
|
|
283
|
+
client = await self._get_smooth_client()
|
|
284
|
+
profile_handle = await client.open_profile(profile_id=profile_id)
|
|
285
|
+
|
|
286
|
+
response = {
|
|
287
|
+
"profile_id": profile_handle.browser_profile.profile_id,
|
|
288
|
+
"live_url": profile_handle.browser_profile.live_url,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await ctx.info(f"Browser profile created: {response['profile_id']}")
|
|
292
|
+
await ctx.info(f"Live profile URL: {response['live_url']}")
|
|
293
|
+
|
|
294
|
+
return response
|
|
295
|
+
|
|
296
|
+
except ApiError as e:
|
|
297
|
+
error_msg = f"Failed to create browser profile: {e.detail}"
|
|
298
|
+
await ctx.error(error_msg)
|
|
299
|
+
raise Exception(error_msg) from None
|
|
300
|
+
except Exception as e:
|
|
301
|
+
error_msg = f"Unexpected error creating profile: {str(e)}"
|
|
302
|
+
await ctx.error(error_msg)
|
|
303
|
+
raise Exception(error_msg) from None
|
|
304
|
+
|
|
305
|
+
@self._mcp.tool(
|
|
306
|
+
name="list_browser_profiles",
|
|
307
|
+
description=(
|
|
308
|
+
"Retrieve a list of all saved browser profiles. "
|
|
309
|
+
"Shows profile IDs that can be passed to future tasks to access login credentials."
|
|
310
|
+
),
|
|
311
|
+
annotations={"title": "List Browser profiles", "readOnlyHint": True, "destructiveHint": False},
|
|
312
|
+
)
|
|
313
|
+
async def list_browser_profiles(ctx: Context) -> Dict[str, Any]:
|
|
314
|
+
"""List all existing browser profiles.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
ctx: MCP context for logging and communication
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dictionary containing list of profile IDs
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
await ctx.info("Retrieving browser profiles...")
|
|
324
|
+
|
|
325
|
+
client = await self._get_smooth_client()
|
|
326
|
+
profiles = await client.list_profiles()
|
|
327
|
+
|
|
328
|
+
response = {
|
|
329
|
+
"profile_ids": profiles.profile_ids,
|
|
330
|
+
"total_profiles": len(profiles.profile_ids),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await ctx.info(f"Found {len(profiles.profile_ids)} browser profiles")
|
|
334
|
+
|
|
335
|
+
return response
|
|
336
|
+
|
|
337
|
+
except ApiError as e:
|
|
338
|
+
error_msg = f"Failed to list browser profiles: {e.detail}"
|
|
339
|
+
await ctx.error(error_msg)
|
|
340
|
+
raise Exception(error_msg) from None
|
|
341
|
+
except Exception as e:
|
|
342
|
+
error_msg = f"Unexpected error listing profiles: {str(e)}"
|
|
343
|
+
await ctx.error(error_msg)
|
|
344
|
+
raise Exception(error_msg) from None
|
|
345
|
+
|
|
346
|
+
@self._mcp.tool(
|
|
347
|
+
name="delete_browser_profile",
|
|
348
|
+
description=(
|
|
349
|
+
"Delete a browser profile and its associated credentials. "
|
|
350
|
+
"This permanently removes the profile and all associated data including cookies and cache."
|
|
351
|
+
),
|
|
352
|
+
annotations={"title": "Delete Browser profile", "readOnlyHint": False, "destructiveHint": True},
|
|
353
|
+
)
|
|
354
|
+
async def delete_browser_profile(
|
|
355
|
+
ctx: Context,
|
|
356
|
+
profile_id: Annotated[
|
|
357
|
+
str,
|
|
358
|
+
Field(
|
|
359
|
+
description=(
|
|
360
|
+
"The ID of the browser profile to delete. "
|
|
361
|
+
"Once deleted, this profile ID cannot be reused and all associated data will be lost"
|
|
362
|
+
),
|
|
363
|
+
min_length=1,
|
|
364
|
+
),
|
|
365
|
+
],
|
|
366
|
+
) -> Dict[str, Any]:
|
|
367
|
+
"""Delete a browser profile and clean up its data.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
ctx: MCP context for logging and communication
|
|
371
|
+
profile_id: The ID of the profile to delete
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dictionary confirming deletion
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
await ctx.info(f"Deleting browser profile: {profile_id}")
|
|
378
|
+
|
|
379
|
+
client = await self._get_smooth_client()
|
|
380
|
+
await client.delete_profile(profile_id)
|
|
381
|
+
|
|
382
|
+
response = {
|
|
383
|
+
"deleted_profile_id": profile_id,
|
|
384
|
+
"status": "deleted",
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
await ctx.info(f"Browser profile {profile_id} deleted successfully")
|
|
388
|
+
|
|
389
|
+
return response
|
|
390
|
+
|
|
391
|
+
except ApiError as e:
|
|
392
|
+
error_msg = f"Failed to delete browser profile {profile_id}: {e.detail}"
|
|
393
|
+
await ctx.error(error_msg)
|
|
394
|
+
raise Exception(error_msg) from None
|
|
395
|
+
except Exception as e:
|
|
396
|
+
error_msg = f"Unexpected error deleting profile: {str(e)}"
|
|
397
|
+
await ctx.error(error_msg)
|
|
398
|
+
raise Exception(error_msg) from None
|
|
399
|
+
|
|
400
|
+
def _register_resources(self):
|
|
401
|
+
"""Register MCP resources with comprehensive documentation and dynamic capabilities."""
|
|
402
|
+
|
|
403
|
+
# Static API information with annotations
|
|
404
|
+
@self._mcp.resource(
|
|
405
|
+
"smooth://api/info",
|
|
406
|
+
description="Comprehensive information about the Smooth SDK MCP server and its capabilities",
|
|
407
|
+
annotations={"readOnlyHint": True, "idempotentHint": True},
|
|
408
|
+
tags={"documentation", "api"},
|
|
409
|
+
mime_type="text/markdown",
|
|
410
|
+
)
|
|
411
|
+
async def get_api_info(ctx: Context) -> str:
|
|
412
|
+
"""Get detailed information about the Smooth SDK and API."""
|
|
413
|
+
await ctx.info("Providing Smooth SDK API information")
|
|
414
|
+
return f"""# Smooth SDK MCP Server v{self._mcp.server_info.version}
|
|
415
|
+
|
|
416
|
+
This MCP server provides access to Smooth's browser automation capabilities through the Model Context Protocol.
|
|
417
|
+
|
|
418
|
+
## Server Information
|
|
419
|
+
- **Name**: {self._mcp.server_info.name}
|
|
420
|
+
- **Version**: {self._mcp.server_info.version}
|
|
421
|
+
- **Request ID**: {ctx.request_id}
|
|
422
|
+
- **Base URL**: {self.base_url or "Default (https://api2.circlemind.co/api/v1)"}
|
|
423
|
+
|
|
424
|
+
## Available Tools
|
|
425
|
+
|
|
426
|
+
### 🚀 run_browser_task
|
|
427
|
+
Execute browser automation tasks using natural language descriptions.
|
|
428
|
+
- **Device Support**: Desktop and mobile support
|
|
429
|
+
- **Profile Management**: Save and access user credentials
|
|
430
|
+
- **Recording**: Video capture of automation
|
|
431
|
+
|
|
432
|
+
### 🔧 create_browser_profile
|
|
433
|
+
Create persistent browser profiles to store and access credentials.
|
|
434
|
+
- **Live Viewing**: Real-time browser access for the user to enter their credentials
|
|
435
|
+
|
|
436
|
+
### 📋 list_browser_profiles
|
|
437
|
+
View all active browser profiles.
|
|
438
|
+
|
|
439
|
+
### 🗑️ delete_browser_profile
|
|
440
|
+
Permanently removes profile data when no longer needed.
|
|
441
|
+
- **Destructive**: Permanently removes profile data
|
|
442
|
+
|
|
443
|
+
## Configuration
|
|
444
|
+
|
|
445
|
+
Set your API key using the CIRCLEMIND_API_KEY environment variable:
|
|
446
|
+
```bash
|
|
447
|
+
export CIRCLEMIND_API_KEY="your-api-key-here"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Best Practices
|
|
451
|
+
|
|
452
|
+
1. **Use profiles**: Ask the user to create profiles for tasks requiring login and then use them.
|
|
453
|
+
2. **Descriptive Tasks**: Use clear, specific task descriptions.
|
|
454
|
+
3. **Error Handling**: Check task results for success/failure status
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
# Dynamic examples resource with path parameters
|
|
458
|
+
@self._mcp.resource(
|
|
459
|
+
"smooth://examples/{category}",
|
|
460
|
+
description="Get task examples for specific categories of browser automation",
|
|
461
|
+
annotations={"readOnlyHint": True, "idempotentHint": True},
|
|
462
|
+
tags={"examples", "templates", "dynamic"},
|
|
463
|
+
mime_type="text/markdown",
|
|
464
|
+
)
|
|
465
|
+
async def get_category_examples(category: str, ctx: Context) -> str:
|
|
466
|
+
"""Get examples for a specific category of browser automation tasks."""
|
|
467
|
+
await ctx.info(f"Providing examples for category: {category}")
|
|
468
|
+
|
|
469
|
+
examples_db = {
|
|
470
|
+
"scraping": """# Web Scraping Examples
|
|
471
|
+
|
|
472
|
+
## Basic Data Extraction
|
|
473
|
+
- "Go to example.com and extract all product prices"
|
|
474
|
+
- "Navigate to news.ycombinator.com and get the top 10 story titles"
|
|
475
|
+
- "Visit Wikipedia and search for 'artificial intelligence', then summarize the first paragraph"
|
|
476
|
+
|
|
477
|
+
## E-commerce Data
|
|
478
|
+
- "Extract product details from Amazon search results for 'wireless headphones'"
|
|
479
|
+
- "Get all customer reviews from the first product page"
|
|
480
|
+
- "Compare prices across multiple product listings"
|
|
481
|
+
|
|
482
|
+
## Social Media
|
|
483
|
+
- "Scrape the latest 20 tweets from a public Twitter profile"
|
|
484
|
+
- "Extract post engagement metrics from Instagram"
|
|
485
|
+
- "Get trending topics from Reddit front page"
|
|
486
|
+
""",
|
|
487
|
+
"forms": """# Form Automation Examples
|
|
488
|
+
|
|
489
|
+
## Contact Forms
|
|
490
|
+
- "Go to contact form at example.com and fill it with test data"
|
|
491
|
+
- "Fill out the newsletter signup with email: test@example.com"
|
|
492
|
+
- "Submit a support request with priority: high"
|
|
493
|
+
|
|
494
|
+
## Registration
|
|
495
|
+
- "Navigate to signup page and create an account with random details"
|
|
496
|
+
- "Complete user registration with name: John Doe, email: john@test.com"
|
|
497
|
+
- "Fill out profile information after account creation"
|
|
498
|
+
|
|
499
|
+
## Applications
|
|
500
|
+
- "Fill out the job application form with my resume information"
|
|
501
|
+
- "Complete the rental application with provided details"
|
|
502
|
+
- "Submit a loan application with financial information"
|
|
503
|
+
""",
|
|
504
|
+
"testing": """# Testing & QA Examples
|
|
505
|
+
|
|
506
|
+
## Functionality Testing
|
|
507
|
+
- "Test the checkout flow on our e-commerce site"
|
|
508
|
+
- "Verify all links on the homepage are working"
|
|
509
|
+
- "Check if the contact form is submitting properly"
|
|
510
|
+
|
|
511
|
+
## UI/UX Testing
|
|
512
|
+
- "Test responsive design by switching between desktop and mobile"
|
|
513
|
+
- "Verify navigation menu works on all pages"
|
|
514
|
+
- "Check loading times for key user journeys"
|
|
515
|
+
|
|
516
|
+
## Integration Testing
|
|
517
|
+
- "Test login flow with valid and invalid credentials"
|
|
518
|
+
- "Verify payment processing with test cards"
|
|
519
|
+
- "Check email verification workflow"
|
|
520
|
+
""",
|
|
521
|
+
"social": """# Social Media Automation Examples
|
|
522
|
+
|
|
523
|
+
## Content Management
|
|
524
|
+
- "Post a status update on Twitter" (requires login profile)
|
|
525
|
+
- "Upload an image to Instagram with caption" (requires login profile)
|
|
526
|
+
- "Share an article on LinkedIn with comment" (requires login profile)
|
|
527
|
+
|
|
528
|
+
## Engagement
|
|
529
|
+
- "Like the latest 10 posts in my feed" (requires login profile)
|
|
530
|
+
- "Reply to mentions and messages" (requires login profile)
|
|
531
|
+
- "Follow accounts based on specific criteria" (requires login profile)
|
|
532
|
+
|
|
533
|
+
## Analytics
|
|
534
|
+
- "Check latest posts performance metrics" (requires login profile)
|
|
535
|
+
- "Download engagement reports" (requires login profile)
|
|
536
|
+
- "Monitor brand mentions across platforms" (requires login profile)
|
|
537
|
+
""",
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if category not in examples_db:
|
|
541
|
+
available_categories = ", ".join(examples_db.keys())
|
|
542
|
+
raise ResourceError(f"Category '{category}' not found. Available categories: {available_categories}")
|
|
543
|
+
|
|
544
|
+
return examples_db[category]
|
|
545
|
+
|
|
546
|
+
def run(self, **kwargs):
|
|
547
|
+
"""Run the MCP server.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
**kwargs: Arguments passed to FastMCP.run() such as transport, host, port, etc.
|
|
551
|
+
"""
|
|
552
|
+
try:
|
|
553
|
+
self._mcp.run(**kwargs)
|
|
554
|
+
finally:
|
|
555
|
+
# Clean up on exit
|
|
556
|
+
asyncio.run(self._cleanup())
|
|
557
|
+
|
|
558
|
+
async def _cleanup(self):
|
|
559
|
+
"""Clean up resources on shutdown."""
|
|
560
|
+
if self._smooth_client:
|
|
561
|
+
await self._smooth_client.close()
|
|
562
|
+
self._smooth_client = None
|
|
563
|
+
|
|
564
|
+
@property
|
|
565
|
+
def fastmcp_server(self) -> FastMCP:
|
|
566
|
+
"""Access to the underlying FastMCP server instance.
|
|
567
|
+
|
|
568
|
+
This allows advanced users to add custom tools or resources.
|
|
569
|
+
"""
|
|
570
|
+
return self._mcp
|