smooth-py 0.1.3.post0__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of smooth-py might be problematic. Click here for more details.
- smooth/__init__.py +70 -25
- smooth/mcp/__init__.py +9 -0
- smooth/mcp/server.py +570 -0
- {smooth_py-0.1.3.post0.dist-info → smooth_py-0.2.0.dist-info}/METADATA +78 -1
- smooth_py-0.2.0.dist-info/RECORD +6 -0
- smooth_py-0.1.3.post0.dist-info/RECORD +0 -4
- {smooth_py-0.1.3.post0.dist-info → smooth_py-0.2.0.dist-info}/WHEEL +0 -0
smooth/__init__.py
CHANGED
|
@@ -4,11 +4,8 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Literal,
|
|
10
|
-
)
|
|
11
|
-
from urllib.parse import urlencode
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from typing import Any, Literal, Type
|
|
12
9
|
|
|
13
10
|
import httpx
|
|
14
11
|
import requests
|
|
@@ -20,6 +17,17 @@ logger = logging.getLogger("smooth")
|
|
|
20
17
|
|
|
21
18
|
BASE_URL = "https://api2.circlemind.co/api/"
|
|
22
19
|
|
|
20
|
+
|
|
21
|
+
# --- Utils ---
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _encode_url(url: str, interactive: bool = True, embed: bool = False) -> str:
|
|
25
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
26
|
+
params = urllib.parse.parse_qs(parsed_url.query)
|
|
27
|
+
params.update({"interactive": "true" if interactive else "false", "embed": "true" if embed else "false"})
|
|
28
|
+
return urllib.parse.urlunparse(parsed_url._replace(query=urllib.parse.urlencode(params)))
|
|
29
|
+
|
|
30
|
+
|
|
23
31
|
# --- Models ---
|
|
24
32
|
# These models define the data structures for API requests and responses.
|
|
25
33
|
|
|
@@ -40,6 +48,9 @@ class TaskRequest(BaseModel):
|
|
|
40
48
|
"""Run task request model."""
|
|
41
49
|
|
|
42
50
|
task: str = Field(description="The task to run.")
|
|
51
|
+
response_model: dict[str, Any] | None = Field(
|
|
52
|
+
default=None, description="If provided, the schema describing the desired output structure. Default is None"
|
|
53
|
+
)
|
|
43
54
|
agent: Literal["smooth"] = Field(default="smooth", description="The agent to use for the task.")
|
|
44
55
|
max_steps: int = Field(default=32, ge=2, le=128, description="Maximum number of steps the agent can take (min 2, max 128).")
|
|
45
56
|
device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
|
|
@@ -66,13 +77,14 @@ class BrowserSessionRequest(BaseModel):
|
|
|
66
77
|
default=None,
|
|
67
78
|
description=("The session ID to open in the browser. If None, a new session will be created with a random name."),
|
|
68
79
|
)
|
|
80
|
+
live_view: bool | None = Field(default=True, description="Request a live URL to interact with the browser session.")
|
|
69
81
|
|
|
70
82
|
|
|
71
83
|
class BrowserSessionResponse(BaseModel):
|
|
72
84
|
"""Browser session response model."""
|
|
73
85
|
|
|
74
|
-
live_url: str = Field(description="The live URL to interact with the browser session.")
|
|
75
86
|
session_id: str = Field(description="The ID of the browser session associated with the opened browser instance.")
|
|
87
|
+
live_url: str | None = Field(default=None, description="The live URL to interact with the browser session.")
|
|
76
88
|
|
|
77
89
|
|
|
78
90
|
class BrowserSessionsResponse(BaseModel):
|
|
@@ -151,6 +163,22 @@ class BaseClient:
|
|
|
151
163
|
# --- Synchronous Client ---
|
|
152
164
|
|
|
153
165
|
|
|
166
|
+
class BrowserSessionHandle(BaseModel):
|
|
167
|
+
"""Browser session handle model."""
|
|
168
|
+
|
|
169
|
+
browser_session: BrowserSessionResponse = Field(description="The browser session associated with this handle.")
|
|
170
|
+
|
|
171
|
+
def session_id(self):
|
|
172
|
+
"""Returns the session ID for the browser session."""
|
|
173
|
+
return self.browser_session.session_id
|
|
174
|
+
|
|
175
|
+
def live_url(self, interactive: bool = True, embed: bool = False):
|
|
176
|
+
"""Returns the live URL for the browser session."""
|
|
177
|
+
if self.browser_session.live_url:
|
|
178
|
+
return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
154
182
|
class TaskHandle:
|
|
155
183
|
"""A handle to a running task."""
|
|
156
184
|
|
|
@@ -182,20 +210,15 @@ class TaskHandle:
|
|
|
182
210
|
|
|
183
211
|
def live_url(self, interactive: bool = True, embed: bool = False, timeout: int | None = None):
|
|
184
212
|
"""Returns the live URL for the task."""
|
|
185
|
-
params = {
|
|
186
|
-
"interactive": interactive,
|
|
187
|
-
"embed": embed
|
|
188
|
-
}
|
|
189
|
-
|
|
190
213
|
if self._task_response and self._task_response.live_url:
|
|
191
|
-
return
|
|
214
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
192
215
|
|
|
193
216
|
start_time = time.time()
|
|
194
217
|
while timeout is None or (time.time() - start_time) < timeout:
|
|
195
218
|
task_response = self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
196
219
|
self._task_response = task_response
|
|
197
220
|
if self._task_response.live_url:
|
|
198
|
-
return
|
|
221
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
199
222
|
time.sleep(1)
|
|
200
223
|
|
|
201
224
|
raise TimeoutError(f"Live URL not available for task {self.id}.")
|
|
@@ -263,6 +286,7 @@ class SmoothClient(BaseClient):
|
|
|
263
286
|
def run(
|
|
264
287
|
self,
|
|
265
288
|
task: str,
|
|
289
|
+
response_model: dict[str, Any] | Type[BaseModel] | None = None,
|
|
266
290
|
agent: Literal["smooth"] = "smooth",
|
|
267
291
|
max_steps: int = 32,
|
|
268
292
|
device: Literal["desktop", "mobile"] = "mobile",
|
|
@@ -280,6 +304,7 @@ class SmoothClient(BaseClient):
|
|
|
280
304
|
|
|
281
305
|
Args:
|
|
282
306
|
task: The task to run.
|
|
307
|
+
response_model: If provided, the schema describing the desired output structure.
|
|
283
308
|
agent: The agent to use for the task.
|
|
284
309
|
max_steps: Maximum number of steps the agent can take (max 64).
|
|
285
310
|
device: Device type for the task. Default is mobile.
|
|
@@ -298,6 +323,7 @@ class SmoothClient(BaseClient):
|
|
|
298
323
|
"""
|
|
299
324
|
payload = TaskRequest(
|
|
300
325
|
task=task,
|
|
326
|
+
response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
|
|
301
327
|
agent=agent,
|
|
302
328
|
max_steps=max_steps,
|
|
303
329
|
device=device,
|
|
@@ -312,11 +338,12 @@ class SmoothClient(BaseClient):
|
|
|
312
338
|
|
|
313
339
|
return TaskHandle(initial_response.id, self)
|
|
314
340
|
|
|
315
|
-
def open_session(self, session_id: str | None = None) ->
|
|
341
|
+
def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
|
|
316
342
|
"""Gets an interactive browser instance.
|
|
317
343
|
|
|
318
344
|
Args:
|
|
319
345
|
session_id: The session ID to associate with the browser. If None, a new session will be created.
|
|
346
|
+
live_view: Whether to enable live view for the session.
|
|
320
347
|
|
|
321
348
|
Returns:
|
|
322
349
|
The browser session details, including the live URL.
|
|
@@ -327,10 +354,10 @@ class SmoothClient(BaseClient):
|
|
|
327
354
|
try:
|
|
328
355
|
response = self._session.post(
|
|
329
356
|
f"{self.base_url}/browser/session",
|
|
330
|
-
json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
|
|
357
|
+
json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
|
|
331
358
|
)
|
|
332
359
|
data = self._handle_response(response)
|
|
333
|
-
return BrowserSessionResponse(**data["r"])
|
|
360
|
+
return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
|
|
334
361
|
except requests.exceptions.RequestException as e:
|
|
335
362
|
logger.error(f"Request failed: {e}")
|
|
336
363
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
@@ -396,19 +423,15 @@ class AsyncTaskHandle:
|
|
|
396
423
|
|
|
397
424
|
async def live_url(self, interactive: bool = True, embed: bool = False, timeout: int | None = None):
|
|
398
425
|
"""Returns the live URL for the task."""
|
|
399
|
-
params = {
|
|
400
|
-
"interactive": interactive,
|
|
401
|
-
"embed": embed
|
|
402
|
-
}
|
|
403
426
|
if self._task_response and self._task_response.live_url:
|
|
404
|
-
return
|
|
427
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
405
428
|
|
|
406
429
|
start_time = time.time()
|
|
407
430
|
while timeout is None or (time.time() - start_time) < timeout:
|
|
408
431
|
task_response = await self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
409
432
|
self._task_response = task_response
|
|
410
433
|
if task_response.live_url is not None:
|
|
411
|
-
return
|
|
434
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
412
435
|
await asyncio.sleep(1)
|
|
413
436
|
|
|
414
437
|
raise TimeoutError(f"Live URL not available for task {self.id}.")
|
|
@@ -428,6 +451,7 @@ class AsyncTaskHandle:
|
|
|
428
451
|
|
|
429
452
|
raise TimeoutError(f"Recording URL not available for task {self.id}.")
|
|
430
453
|
|
|
454
|
+
|
|
431
455
|
class SmoothAsyncClient(BaseClient):
|
|
432
456
|
"""An asynchronous client for the API."""
|
|
433
457
|
|
|
@@ -470,6 +494,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
470
494
|
async def run(
|
|
471
495
|
self,
|
|
472
496
|
task: str,
|
|
497
|
+
response_model: dict[str, Any] | Type[BaseModel] | None = None,
|
|
473
498
|
agent: Literal["smooth"] = "smooth",
|
|
474
499
|
max_steps: int = 32,
|
|
475
500
|
device: Literal["desktop", "mobile"] = "mobile",
|
|
@@ -487,6 +512,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
487
512
|
|
|
488
513
|
Args:
|
|
489
514
|
task: The task to run.
|
|
515
|
+
response_model: If provided, the schema describing the desired output structure.
|
|
490
516
|
agent: The agent to use for the task.
|
|
491
517
|
max_steps: Maximum number of steps the agent can take (max 64).
|
|
492
518
|
device: Device type for the task. Default is mobile.
|
|
@@ -507,6 +533,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
507
533
|
"""
|
|
508
534
|
payload = TaskRequest(
|
|
509
535
|
task=task,
|
|
536
|
+
response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
|
|
510
537
|
agent=agent,
|
|
511
538
|
max_steps=max_steps,
|
|
512
539
|
device=device,
|
|
@@ -521,11 +548,12 @@ class SmoothAsyncClient(BaseClient):
|
|
|
521
548
|
initial_response = await self._submit_task(payload)
|
|
522
549
|
return AsyncTaskHandle(initial_response.id, self)
|
|
523
550
|
|
|
524
|
-
async def open_session(self, session_id: str | None = None) ->
|
|
551
|
+
async def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
|
|
525
552
|
"""Opens an interactive browser instance asynchronously.
|
|
526
553
|
|
|
527
554
|
Args:
|
|
528
555
|
session_id: The session ID to associate with the browser.
|
|
556
|
+
live_view: Whether to enable live view for the session.
|
|
529
557
|
|
|
530
558
|
Returns:
|
|
531
559
|
The browser session details, including the live URL.
|
|
@@ -536,10 +564,10 @@ class SmoothAsyncClient(BaseClient):
|
|
|
536
564
|
try:
|
|
537
565
|
response = await self._client.post(
|
|
538
566
|
f"{self.base_url}/browser/session",
|
|
539
|
-
json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
|
|
567
|
+
json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
|
|
540
568
|
)
|
|
541
569
|
data = self._handle_response(response)
|
|
542
|
-
return BrowserSessionResponse(**data["r"])
|
|
570
|
+
return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
|
|
543
571
|
except httpx.RequestError as e:
|
|
544
572
|
logger.error(f"Request failed: {e}")
|
|
545
573
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
@@ -573,3 +601,20 @@ class SmoothAsyncClient(BaseClient):
|
|
|
573
601
|
async def close(self):
|
|
574
602
|
"""Closes the async client session."""
|
|
575
603
|
await self._client.aclose()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# Export public API
|
|
607
|
+
__all__ = [
|
|
608
|
+
"SmoothClient",
|
|
609
|
+
"SmoothAsyncClient",
|
|
610
|
+
"TaskHandle",
|
|
611
|
+
"AsyncTaskHandle",
|
|
612
|
+
"BrowserSessionHandle",
|
|
613
|
+
"TaskRequest",
|
|
614
|
+
"TaskResponse",
|
|
615
|
+
"BrowserSessionRequest",
|
|
616
|
+
"BrowserSessionResponse",
|
|
617
|
+
"BrowserSessionsResponse",
|
|
618
|
+
"ApiError",
|
|
619
|
+
"TimeoutError",
|
|
620
|
+
]
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: smooth-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Luca Pinchetti
|
|
6
6
|
Author-email: luca@circlemind.co
|
|
@@ -10,6 +10,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Provides-Extra: mcp
|
|
14
|
+
Requires-Dist: fastmcp (>=2.0.0,<3.0.0) ; extra == "mcp"
|
|
13
15
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
14
16
|
Requires-Dist: pydantic (>=2.11.7,<3.0.0)
|
|
15
17
|
Description-Content-Type: text/markdown
|
|
@@ -24,6 +26,7 @@ The Smooth Python SDK provides a convenient way to interact with the Smooth API
|
|
|
24
26
|
* **Task Management**: Easily run tasks and retrieve results upon completion.
|
|
25
27
|
* **Interactive Browser Sessions**: Get access to, interact with, and delete stateful browser sessions to manage your login credentials.
|
|
26
28
|
* **Advanced Task Configuration**: Customize task execution with options for device type, session recording, stealth mode, and proxy settings.
|
|
29
|
+
* **🆕 MCP Server**: Use the included Model Context Protocol server to integrate browser automation with AI assistants like Claude Desktop.
|
|
27
30
|
|
|
28
31
|
## Installation
|
|
29
32
|
|
|
@@ -33,6 +36,51 @@ You can install the Smooth Python SDK using pip:
|
|
|
33
36
|
pip install smooth-py
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
For MCP server functionality, also install FastMCP:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install fastmcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start Options
|
|
46
|
+
|
|
47
|
+
### Option 1: Direct SDK Usage
|
|
48
|
+
|
|
49
|
+
Use the SDK directly in your Python applications:
|
|
50
|
+
|
|
51
|
+
### Option 2: MCP Server (AI Assistant Integration)
|
|
52
|
+
|
|
53
|
+
Use the included MCP server to integrate browser automation with AI assistants:
|
|
54
|
+
|
|
55
|
+
#### Installation
|
|
56
|
+
```bash
|
|
57
|
+
# Install with MCP support
|
|
58
|
+
pip install smooth-py[mcp]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Basic Usage
|
|
62
|
+
```python
|
|
63
|
+
from smooth.mcp import SmoothMCP
|
|
64
|
+
|
|
65
|
+
# Create and run the MCP server
|
|
66
|
+
mcp = SmoothMCP(api_key="your-api-key")
|
|
67
|
+
mcp.run() # STDIO transport for Claude Desktop
|
|
68
|
+
|
|
69
|
+
# Or with HTTP transport for web deployment
|
|
70
|
+
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Standalone Script (Backward Compatible)
|
|
74
|
+
```bash
|
|
75
|
+
# Set your API key
|
|
76
|
+
export CIRCLEMIND_API_KEY="your-api-key-here"
|
|
77
|
+
|
|
78
|
+
# Run the MCP server
|
|
79
|
+
python mcp_server.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then configure your AI assistant (like Claude Desktop) to use the MCP server. See [MCP_README.md](MCP_README.md) for detailed setup instructions.
|
|
83
|
+
|
|
36
84
|
## Authentication
|
|
37
85
|
|
|
38
86
|
The SDK requires an API key for authentication. You can provide the API key in two ways:
|
|
@@ -162,3 +210,32 @@ if __name__ == "__main__":
|
|
|
162
210
|
asyncio.run(main())
|
|
163
211
|
```
|
|
164
212
|
|
|
213
|
+
## MCP Server (AI Assistant Integration)
|
|
214
|
+
|
|
215
|
+
The Smooth SDK includes a Model Context Protocol (MCP) server that allows AI assistants like Claude Desktop or Cursor to perform browser automation tasks through natural language commands.
|
|
216
|
+
|
|
217
|
+
### Installation
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pip install smooth-py[mcp]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Basic Usage
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from smooth.mcp import SmoothMCP
|
|
227
|
+
|
|
228
|
+
# Create and run the MCP server
|
|
229
|
+
mcp = SmoothMCP(api_key="your-api-key")
|
|
230
|
+
mcp.run()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Example MCP Usage
|
|
234
|
+
|
|
235
|
+
Once configured, you can ask your MCP client to perform browser automation:
|
|
236
|
+
|
|
237
|
+
- "Please go to news.ycombinator.com and get the top 5 story titles"
|
|
238
|
+
- "Create a browser session, log into Gmail, and check for unread emails"
|
|
239
|
+
- "Go to Amazon and search for wireless headphones under $100"
|
|
240
|
+
- "Fill out the contact form at example.com with test data"
|
|
241
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
smooth/__init__.py,sha256=U32quTlDKOWQpsstfJ7pCgsOeUWYC3AAAzQDJkpBzBk,23629
|
|
2
|
+
smooth/mcp/__init__.py,sha256=0aJVFi2a8Ah3-5xtgyZ5UMbaaJsBWu2T8QLWoFQITk8,219
|
|
3
|
+
smooth/mcp/server.py,sha256=9SymTD4NOGTMN8P-LNGlvYNvv81yCIZfZeeuhEcAc6s,20068
|
|
4
|
+
smooth_py-0.2.0.dist-info/METADATA,sha256=o9iLsqHyEBGkOyPmW695HHmcQwV0afH2Evkp7a0O6KA,7425
|
|
5
|
+
smooth_py-0.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
6
|
+
smooth_py-0.2.0.dist-info/RECORD,,
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
smooth/__init__.py,sha256=jIEM1usuwrZWq-m1YgsUsLCkvUkhS9BNqgFc3HUPEvw,21263
|
|
2
|
-
smooth_py-0.1.3.post0.dist-info/METADATA,sha256=x4u365t5CvrRvMPsrCrLy2sVNT4ojf8nK8pdntodEdw,5394
|
|
3
|
-
smooth_py-0.1.3.post0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
4
|
-
smooth_py-0.1.3.post0.dist-info/RECORD,,
|
|
File without changes
|