smooth-py 0.1.4__tar.gz → 0.2.1.dev20250911__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: smooth-py
3
- Version: 0.1.4
3
+ Version: 0.2.1.dev20250911
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
+
@@ -8,6 +8,7 @@ The Smooth Python SDK provides a convenient way to interact with the Smooth API
8
8
  * **Task Management**: Easily run tasks and retrieve results upon completion.
9
9
  * **Interactive Browser Sessions**: Get access to, interact with, and delete stateful browser sessions to manage your login credentials.
10
10
  * **Advanced Task Configuration**: Customize task execution with options for device type, session recording, stealth mode, and proxy settings.
11
+ * **🆕 MCP Server**: Use the included Model Context Protocol server to integrate browser automation with AI assistants like Claude Desktop.
11
12
 
12
13
  ## Installation
13
14
 
@@ -17,6 +18,51 @@ You can install the Smooth Python SDK using pip:
17
18
  pip install smooth-py
18
19
  ```
19
20
 
21
+ For MCP server functionality, also install FastMCP:
22
+
23
+ ```bash
24
+ pip install fastmcp
25
+ ```
26
+
27
+ ## Quick Start Options
28
+
29
+ ### Option 1: Direct SDK Usage
30
+
31
+ Use the SDK directly in your Python applications:
32
+
33
+ ### Option 2: MCP Server (AI Assistant Integration)
34
+
35
+ Use the included MCP server to integrate browser automation with AI assistants:
36
+
37
+ #### Installation
38
+ ```bash
39
+ # Install with MCP support
40
+ pip install smooth-py[mcp]
41
+ ```
42
+
43
+ #### Basic Usage
44
+ ```python
45
+ from smooth.mcp import SmoothMCP
46
+
47
+ # Create and run the MCP server
48
+ mcp = SmoothMCP(api_key="your-api-key")
49
+ mcp.run() # STDIO transport for Claude Desktop
50
+
51
+ # Or with HTTP transport for web deployment
52
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
53
+ ```
54
+
55
+ #### Standalone Script (Backward Compatible)
56
+ ```bash
57
+ # Set your API key
58
+ export CIRCLEMIND_API_KEY="your-api-key-here"
59
+
60
+ # Run the MCP server
61
+ python mcp_server.py
62
+ ```
63
+
64
+ Then configure your AI assistant (like Claude Desktop) to use the MCP server. See [MCP_README.md](MCP_README.md) for detailed setup instructions.
65
+
20
66
  ## Authentication
21
67
 
22
68
  The SDK requires an API key for authentication. You can provide the API key in two ways:
@@ -145,3 +191,32 @@ async def main():
145
191
  if __name__ == "__main__":
146
192
  asyncio.run(main())
147
193
  ```
194
+
195
+ ## MCP Server (AI Assistant Integration)
196
+
197
+ 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.
198
+
199
+ ### Installation
200
+
201
+ ```bash
202
+ pip install smooth-py[mcp]
203
+ ```
204
+
205
+ ### Basic Usage
206
+
207
+ ```python
208
+ from smooth.mcp import SmoothMCP
209
+
210
+ # Create and run the MCP server
211
+ mcp = SmoothMCP(api_key="your-api-key")
212
+ mcp.run()
213
+ ```
214
+
215
+ ### Example MCP Usage
216
+
217
+ Once configured, you can ask your MCP client to perform browser automation:
218
+
219
+ - "Please go to news.ycombinator.com and get the top 5 story titles"
220
+ - "Create a browser session, log into Gmail, and check for unread emails"
221
+ - "Go to Amazon and search for wireless headphones under $100"
222
+ - "Fill out the contact form at example.com with test data"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smooth-py"
3
- version = "0.1.4"
3
+ version = "0.2.1.dev20250911"
4
4
  description = ""
5
5
  authors = [
6
6
  {name = "Luca Pinchetti",email = "luca@circlemind.co"}
@@ -12,6 +12,11 @@ dependencies = [
12
12
  "httpx (>=0.28.1,<0.29.0)"
13
13
  ]
14
14
 
15
+ [project.optional-dependencies]
16
+ mcp = [
17
+ "fastmcp (>=2.0.0,<3.0.0)"
18
+ ]
19
+
15
20
  [tool.poetry]
16
21
  packages = [{include = "smooth", from = "src"}]
17
22
 
@@ -5,10 +5,7 @@ import logging
5
5
  import os
6
6
  import time
7
7
  import urllib.parse
8
- from typing import (
9
- Any,
10
- Literal,
11
- )
8
+ from typing import Any, Literal, Type
12
9
 
13
10
  import httpx
14
11
  import requests
@@ -27,10 +24,7 @@ BASE_URL = "https://api2.circlemind.co/api/"
27
24
  def _encode_url(url: str, interactive: bool = True, embed: bool = False) -> str:
28
25
  parsed_url = urllib.parse.urlparse(url)
29
26
  params = urllib.parse.parse_qs(parsed_url.query)
30
- params.update({
31
- "interactive": "true" if interactive else "false",
32
- "embed": "true" if embed else "false"
33
- })
27
+ params.update({"interactive": "true" if interactive else "false", "embed": "true" if embed else "false"})
34
28
  return urllib.parse.urlunparse(parsed_url._replace(query=urllib.parse.urlencode(params)))
35
29
 
36
30
 
@@ -54,6 +48,14 @@ class TaskRequest(BaseModel):
54
48
  """Run task request model."""
55
49
 
56
50
  task: str = Field(description="The task to run.")
51
+ response_model: dict[str, Any] | None = Field(
52
+ default=None, description="If provided, the JSON schema describing the desired output structure. Default is None"
53
+ )
54
+ url: str | None = Field(
55
+ default=None,
56
+ description="(Optional) The starting URL for the task. If not provided, the agent will infer it from the task.",
57
+ )
58
+ metadata: dict[str, str | int | float | bool] | None = Field(default=None, description="Optional metadata for the task.")
57
59
  agent: Literal["smooth"] = Field(default="smooth", description="The agent to use for the task.")
58
60
  max_steps: int = Field(default=32, ge=2, le=128, description="Maximum number of steps the agent can take (min 2, max 128).")
59
61
  device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
@@ -80,13 +82,14 @@ class BrowserSessionRequest(BaseModel):
80
82
  default=None,
81
83
  description=("The session ID to open in the browser. If None, a new session will be created with a random name."),
82
84
  )
85
+ live_view: bool | None = Field(default=True, description="Request a live URL to interact with the browser session.")
83
86
 
84
87
 
85
88
  class BrowserSessionResponse(BaseModel):
86
89
  """Browser session response model."""
87
90
 
88
- live_url: str = Field(description="The live URL to interact with the browser session.")
89
91
  session_id: str = Field(description="The ID of the browser session associated with the opened browser instance.")
92
+ live_url: str | None = Field(default=None, description="The live URL to interact with the browser session.")
90
93
 
91
94
 
92
95
  class BrowserSessionsResponse(BaseModel):
@@ -138,7 +141,7 @@ class BaseClient:
138
141
  self.headers = {
139
142
  "apikey": self.api_key,
140
143
  "Content-Type": "application/json",
141
- "User-Agent": "smooth-python-sdk/0.1.1",
144
+ "User-Agent": "smooth-python-sdk/0.2.0",
142
145
  }
143
146
 
144
147
  def _handle_response(self, response: requests.Response | httpx.Response) -> dict[str, Any]:
@@ -176,7 +179,9 @@ class BrowserSessionHandle(BaseModel):
176
179
 
177
180
  def live_url(self, interactive: bool = True, embed: bool = False):
178
181
  """Returns the live URL for the browser session."""
179
- return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
182
+ if self.browser_session.live_url:
183
+ return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
184
+ return None
180
185
 
181
186
 
182
187
  class TaskHandle:
@@ -187,7 +192,11 @@ class TaskHandle:
187
192
  self._client = client
188
193
  self._task_response: TaskResponse | None = None
189
194
 
190
- self.id = task_id
195
+ self._id = task_id
196
+
197
+ def id(self):
198
+ """Returns the task ID."""
199
+ return self._id
191
200
 
192
201
  def result(self, timeout: int | None = None, poll_interval: float = 1) -> TaskResponse:
193
202
  """Waits for the task to complete and returns the result."""
@@ -286,6 +295,9 @@ class SmoothClient(BaseClient):
286
295
  def run(
287
296
  self,
288
297
  task: str,
298
+ response_model: dict[str, Any] | Type[BaseModel] | None = None,
299
+ url: str | None = None,
300
+ metadata: dict[str, str | int | float | bool] | None = None,
289
301
  agent: Literal["smooth"] = "smooth",
290
302
  max_steps: int = 32,
291
303
  device: Literal["desktop", "mobile"] = "mobile",
@@ -303,6 +315,9 @@ class SmoothClient(BaseClient):
303
315
 
304
316
  Args:
305
317
  task: The task to run.
318
+ response_model: If provided, the schema describing the desired output structure.
319
+ url: The starting URL for the task. If not provided, the agent will infer it from the task.
320
+ metadata: Optional metadata for the task.
306
321
  agent: The agent to use for the task.
307
322
  max_steps: Maximum number of steps the agent can take (max 64).
308
323
  device: Device type for the task. Default is mobile.
@@ -321,6 +336,9 @@ class SmoothClient(BaseClient):
321
336
  """
322
337
  payload = TaskRequest(
323
338
  task=task,
339
+ response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
340
+ url=url,
341
+ metadata=metadata,
324
342
  agent=agent,
325
343
  max_steps=max_steps,
326
344
  device=device,
@@ -335,11 +353,12 @@ class SmoothClient(BaseClient):
335
353
 
336
354
  return TaskHandle(initial_response.id, self)
337
355
 
338
- def open_session(self, session_id: str | None = None) -> BrowserSessionHandle:
356
+ def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
339
357
  """Gets an interactive browser instance.
340
358
 
341
359
  Args:
342
360
  session_id: The session ID to associate with the browser. If None, a new session will be created.
361
+ live_view: Whether to enable live view for the session.
343
362
 
344
363
  Returns:
345
364
  The browser session details, including the live URL.
@@ -350,7 +369,7 @@ class SmoothClient(BaseClient):
350
369
  try:
351
370
  response = self._session.post(
352
371
  f"{self.base_url}/browser/session",
353
- json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
372
+ json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
354
373
  )
355
374
  data = self._handle_response(response)
356
375
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -447,6 +466,7 @@ class AsyncTaskHandle:
447
466
 
448
467
  raise TimeoutError(f"Recording URL not available for task {self.id}.")
449
468
 
469
+
450
470
  class SmoothAsyncClient(BaseClient):
451
471
  """An asynchronous client for the API."""
452
472
 
@@ -489,6 +509,9 @@ class SmoothAsyncClient(BaseClient):
489
509
  async def run(
490
510
  self,
491
511
  task: str,
512
+ response_model: dict[str, Any] | Type[BaseModel] | None = None,
513
+ url: str | None = None,
514
+ metadata: dict[str, str | int | float | bool] | None = None,
492
515
  agent: Literal["smooth"] = "smooth",
493
516
  max_steps: int = 32,
494
517
  device: Literal["desktop", "mobile"] = "mobile",
@@ -506,6 +529,9 @@ class SmoothAsyncClient(BaseClient):
506
529
 
507
530
  Args:
508
531
  task: The task to run.
532
+ response_model: If provided, the schema describing the desired output structure.
533
+ url: The starting URL for the task. If not provided, the agent will infer it from the task.
534
+ metadata: Optional metadata for the task.
509
535
  agent: The agent to use for the task.
510
536
  max_steps: Maximum number of steps the agent can take (max 64).
511
537
  device: Device type for the task. Default is mobile.
@@ -526,6 +552,9 @@ class SmoothAsyncClient(BaseClient):
526
552
  """
527
553
  payload = TaskRequest(
528
554
  task=task,
555
+ response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
556
+ url=url,
557
+ metadata=metadata,
529
558
  agent=agent,
530
559
  max_steps=max_steps,
531
560
  device=device,
@@ -540,11 +569,12 @@ class SmoothAsyncClient(BaseClient):
540
569
  initial_response = await self._submit_task(payload)
541
570
  return AsyncTaskHandle(initial_response.id, self)
542
571
 
543
- async def open_session(self, session_id: str | None = None) -> BrowserSessionHandle:
572
+ async def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
544
573
  """Opens an interactive browser instance asynchronously.
545
574
 
546
575
  Args:
547
576
  session_id: The session ID to associate with the browser.
577
+ live_view: Whether to enable live view for the session.
548
578
 
549
579
  Returns:
550
580
  The browser session details, including the live URL.
@@ -555,7 +585,7 @@ class SmoothAsyncClient(BaseClient):
555
585
  try:
556
586
  response = await self._client.post(
557
587
  f"{self.base_url}/browser/session",
558
- json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
588
+ json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
559
589
  )
560
590
  data = self._handle_response(response)
561
591
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -592,3 +622,20 @@ class SmoothAsyncClient(BaseClient):
592
622
  async def close(self):
593
623
  """Closes the async client session."""
594
624
  await self._client.aclose()
625
+
626
+
627
+ # Export public API
628
+ __all__ = [
629
+ "SmoothClient",
630
+ "SmoothAsyncClient",
631
+ "TaskHandle",
632
+ "AsyncTaskHandle",
633
+ "BrowserSessionHandle",
634
+ "TaskRequest",
635
+ "TaskResponse",
636
+ "BrowserSessionRequest",
637
+ "BrowserSessionResponse",
638
+ "BrowserSessionsResponse",
639
+ "ApiError",
640
+ "TimeoutError",
641
+ ]
@@ -0,0 +1,9 @@
1
+ """Smooth SDK MCP Integration.
2
+
3
+ This module provides Model Context Protocol integration for the Smooth SDK,
4
+ allowing browser automation through AI assistants.
5
+ """
6
+
7
+ from .server import SmoothMCP
8
+
9
+ __all__ = ["SmoothMCP"]
@@ -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