smooth-py 0.1.4__tar.gz → 0.2.0__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.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
+
@@ -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.0"
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,9 @@ 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 schema describing the desired output structure. Default is None"
53
+ )
57
54
  agent: Literal["smooth"] = Field(default="smooth", description="The agent to use for the task.")
58
55
  max_steps: int = Field(default=32, ge=2, le=128, description="Maximum number of steps the agent can take (min 2, max 128).")
59
56
  device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
@@ -80,13 +77,14 @@ class BrowserSessionRequest(BaseModel):
80
77
  default=None,
81
78
  description=("The session ID to open in the browser. If None, a new session will be created with a random name."),
82
79
  )
80
+ live_view: bool | None = Field(default=True, description="Request a live URL to interact with the browser session.")
83
81
 
84
82
 
85
83
  class BrowserSessionResponse(BaseModel):
86
84
  """Browser session response model."""
87
85
 
88
- live_url: str = Field(description="The live URL to interact with the browser session.")
89
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.")
90
88
 
91
89
 
92
90
  class BrowserSessionsResponse(BaseModel):
@@ -176,7 +174,9 @@ class BrowserSessionHandle(BaseModel):
176
174
 
177
175
  def live_url(self, interactive: bool = True, embed: bool = False):
178
176
  """Returns the live URL for the browser session."""
179
- return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
177
+ if self.browser_session.live_url:
178
+ return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
179
+ return None
180
180
 
181
181
 
182
182
  class TaskHandle:
@@ -286,6 +286,7 @@ class SmoothClient(BaseClient):
286
286
  def run(
287
287
  self,
288
288
  task: str,
289
+ response_model: dict[str, Any] | Type[BaseModel] | None = None,
289
290
  agent: Literal["smooth"] = "smooth",
290
291
  max_steps: int = 32,
291
292
  device: Literal["desktop", "mobile"] = "mobile",
@@ -303,6 +304,7 @@ class SmoothClient(BaseClient):
303
304
 
304
305
  Args:
305
306
  task: The task to run.
307
+ response_model: If provided, the schema describing the desired output structure.
306
308
  agent: The agent to use for the task.
307
309
  max_steps: Maximum number of steps the agent can take (max 64).
308
310
  device: Device type for the task. Default is mobile.
@@ -321,6 +323,7 @@ class SmoothClient(BaseClient):
321
323
  """
322
324
  payload = TaskRequest(
323
325
  task=task,
326
+ response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
324
327
  agent=agent,
325
328
  max_steps=max_steps,
326
329
  device=device,
@@ -335,11 +338,12 @@ class SmoothClient(BaseClient):
335
338
 
336
339
  return TaskHandle(initial_response.id, self)
337
340
 
338
- def open_session(self, session_id: str | None = None) -> BrowserSessionHandle:
341
+ def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
339
342
  """Gets an interactive browser instance.
340
343
 
341
344
  Args:
342
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.
343
347
 
344
348
  Returns:
345
349
  The browser session details, including the live URL.
@@ -350,7 +354,7 @@ class SmoothClient(BaseClient):
350
354
  try:
351
355
  response = self._session.post(
352
356
  f"{self.base_url}/browser/session",
353
- 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),
354
358
  )
355
359
  data = self._handle_response(response)
356
360
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -447,6 +451,7 @@ class AsyncTaskHandle:
447
451
 
448
452
  raise TimeoutError(f"Recording URL not available for task {self.id}.")
449
453
 
454
+
450
455
  class SmoothAsyncClient(BaseClient):
451
456
  """An asynchronous client for the API."""
452
457
 
@@ -489,6 +494,7 @@ class SmoothAsyncClient(BaseClient):
489
494
  async def run(
490
495
  self,
491
496
  task: str,
497
+ response_model: dict[str, Any] | Type[BaseModel] | None = None,
492
498
  agent: Literal["smooth"] = "smooth",
493
499
  max_steps: int = 32,
494
500
  device: Literal["desktop", "mobile"] = "mobile",
@@ -506,6 +512,7 @@ class SmoothAsyncClient(BaseClient):
506
512
 
507
513
  Args:
508
514
  task: The task to run.
515
+ response_model: If provided, the schema describing the desired output structure.
509
516
  agent: The agent to use for the task.
510
517
  max_steps: Maximum number of steps the agent can take (max 64).
511
518
  device: Device type for the task. Default is mobile.
@@ -526,6 +533,7 @@ class SmoothAsyncClient(BaseClient):
526
533
  """
527
534
  payload = TaskRequest(
528
535
  task=task,
536
+ response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
529
537
  agent=agent,
530
538
  max_steps=max_steps,
531
539
  device=device,
@@ -540,11 +548,12 @@ class SmoothAsyncClient(BaseClient):
540
548
  initial_response = await self._submit_task(payload)
541
549
  return AsyncTaskHandle(initial_response.id, self)
542
550
 
543
- async def open_session(self, session_id: str | None = None) -> BrowserSessionHandle:
551
+ async def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
544
552
  """Opens an interactive browser instance asynchronously.
545
553
 
546
554
  Args:
547
555
  session_id: The session ID to associate with the browser.
556
+ live_view: Whether to enable live view for the session.
548
557
 
549
558
  Returns:
550
559
  The browser session details, including the live URL.
@@ -555,7 +564,7 @@ class SmoothAsyncClient(BaseClient):
555
564
  try:
556
565
  response = await self._client.post(
557
566
  f"{self.base_url}/browser/session",
558
- 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),
559
568
  )
560
569
  data = self._handle_response(response)
561
570
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -592,3 +601,20 @@ class SmoothAsyncClient(BaseClient):
592
601
  async def close(self):
593
602
  """Closes the async client session."""
594
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
+ ]
@@ -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