Skip to content

Commit 722e06a

Browse files
committed
More hints provided
1 parent 2903467 commit 722e06a

6 files changed

Lines changed: 81 additions & 9 deletions

File tree

config/perfecto.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
HELP_BASE_CONTENT_URL = "https://help.perfecto.io/perfecto-help/content/"
1616

1717

18+
def get_cloud_app_url(cloud_name: str) -> str:
19+
return f"https://{cloud_name}.app.perfectomobile.com"
20+
21+
22+
def get_ai_scriptless_lab_url(cloud_name: str) -> str:
23+
return f"{get_cloud_app_url(cloud_name)}/lab/scriptless-mobile/"
24+
25+
1826
def get_tenant_management_api_url(cloud_name: str) -> str:
1927
return f"https://{cloud_name}.app.perfectomobile.com/tenant-management-webapp/rest/v1/tenant-management/tenants/current"
2028

formatters/user.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
from typing import List, Any, Optional
22

3+
from config.perfecto import get_cloud_app_url
34
from models.user import User
45

56

67
def format_users(users: dict[str, Any], params: Optional[dict] = None) -> List[User]:
78
first_name = users.get('firstName') or ''
89
last_name = users.get('lastName') or ''
910
display_name = f"{first_name} {last_name}".strip() or users.get("username", "Unknown")
10-
11+
cloud_name = (params or {}).get("cloud_name") or ""
12+
cloud_url = get_cloud_app_url(cloud_name) if cloud_name else ""
13+
1114
formatted_users = [
1215
User(
1316
username=users.get("username") or "unknown",
1417
display_name=display_name,
1518
first_name=first_name,
1619
last_name=last_name,
20+
cloud_name=cloud_name,
21+
cloud_url=cloud_url,
1722
)
1823
]
1924
return formatted_users

models/user.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ class User(BaseModel):
55
username: str = Field(description="The unique identifier for the user")
66
display_name: str = Field(description="Display name of the user")
77
first_name: str = Field(description="First name of the user")
8-
last_name: str = Field(description="Last name of the user")
8+
last_name: str = Field(description="Last name of the user")
9+
cloud_name: str = Field(description="Perfecto cloud name from MCP configuration (PERFECTO_CLOUD_NAME)")
10+
cloud_url: str = Field(description="Perfecto cloud portal URL (https://{cloud_name}.app.perfectomobile.com)")

tools/ai_scriptless_manager.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
set_element_enabled,
4343
split_item_key,
4444
test_file_name,
45+
format_test_ui_location,
4546
update_element_arguments,
4647
)
4748
from tools.utils import api_request
@@ -62,6 +63,24 @@ def _append_step_path_refresh_notes(result: BaseResult) -> BaseResult:
6263
return result
6364

6465

66+
def _append_ui_access_info(
67+
result: BaseResult,
68+
cloud_name: str,
69+
test_id: Optional[str] = None,
70+
) -> BaseResult:
71+
if result.error:
72+
return result
73+
lab_url = perfecto.get_ai_scriptless_lab_url(cloud_name)
74+
lines = [f"AI Scriptless UI (no per-test deep link): [{lab_url}]({lab_url})"]
75+
if test_id:
76+
lines.append(
77+
"If you need to open it in the UI: Tests → Open or Manage tests, then navigate to "
78+
f"{format_test_ui_location(test_id)}"
79+
)
80+
result.append_info(lines)
81+
return result
82+
83+
6584
class AiScriptlessManager(Manager):
6685
def __init__(self, token: Optional[PerfectoToken], ctx: Context):
6786
super().__init__(token, ctx)
@@ -88,12 +107,13 @@ async def list_tests(self, args: dict[str, Any]) -> BaseResult:
88107
has_more=page_size - len(tests_result.result) <= 0,
89108
)
90109

91-
return BaseResult(
110+
result = BaseResult(
92111
result=page_result,
93112
error=tests_result.error,
94113
warning=tests_result.warning,
95114
info=tests_result.info,
96115
)
116+
return _append_ui_access_info(result, self.token.cloud_name)
97117

98118
@token_verify
99119
async def list_filter_values(self, filter_names: list[str]) -> BaseResult:
@@ -217,9 +237,10 @@ async def view_test_structure(self, test_id: str) -> BaseResult:
217237
return BaseResult(error="test_id is required (itemKey from list_tests)")
218238
script_url = perfecto.get_ai_scriptless_api_url(self.token.cloud_name)
219239
script_url = script_url + f"/script?itemKey={quote(test_id, safe='')}"
220-
return await api_request(self.token, "GET", endpoint=script_url,
240+
result = await api_request(self.token, "GET", endpoint=script_url,
221241
result_formatter=format_test_structure,
222242
result_formatter_params={"item_key": test_id})
243+
return _append_ui_access_info(result, self.token.cloud_name, test_id)
223244

224245
@token_verify
225246
async def add_command(
@@ -321,7 +342,8 @@ async def create_test(self, name: str, folder: str = "My Folder", visibility: st
321342
return BaseResult(error="name is required")
322343
item_key = build_item_key(visibility, folder, name)
323344
script = new_empty_script()
324-
return await persist_script(self.token, item_key, script)
345+
result = await persist_script(self.token, item_key, script)
346+
return _append_ui_access_info(result, self.token.cloud_name, item_key)
325347

326348
@token_verify
327349
async def save_test_as(
@@ -342,9 +364,10 @@ async def save_test_as(
342364
return payload_result
343365
script = payload_result.result.get("script", {})
344366
item_key = build_item_key(visibility, folder, name)
345-
return _append_step_path_refresh_notes(
367+
result = _append_step_path_refresh_notes(
346368
await persist_script(self.token, item_key, script, snapshot_comment=comment)
347369
)
370+
return _append_ui_access_info(result, self.token.cloud_name, item_key)
348371

349372
async def _add_structure(
350373
self,
@@ -759,6 +782,10 @@ def register(mcp, token: Optional[PerfectoToken]):
759782
test_id (str): Test itemKey from list_tests.
760783
name (str): Variable name to delete.
761784
Hints:
785+
- LICENSE: AI Scriptless actions require a Perfecto AI license on your cloud (administrator opt-in via feature toggle). Without it, AI commands and related MCP operations will not work. Desktop web test authoring additionally requires the Desktop Web license.
786+
- COVERAGE: DataTables, Scheduler (scheduled jobs), Embedded tests, and other advanced UI capabilities (folder management, rename test, restore snapshot, download as Appium, AI Assistant, Object Spy, per-step error policy, etc.) are not yet supported by this MCP tool.
787+
- HELP: For product behavior and workarounds, use the perfecto_help tool: Filter by category_id='perfecto', subcategory_id_list=['ide'].
788+
- UI_ACCESS: No per-test URL exists. Only UI entry: cloud_url/lab/scriptless-mobile/ (cloud_url from perfecto_user read_user). For debugging or unsupported MCP tasks, link the lab URL and tell the user to open the test via Tests → Open or Manage tests using the folder tree and test name from list_tests (itemKey is MCP-only; the UI shows folders and names, not itemKey). Never invent other scriptless URLs.
762789
- When authoring or editing test steps, call list_commands first and follow the command selection policy in the info field.
763790
- step_path is a dot-separated positional path without spaces (0-based indices; b0=Then branch, b1=Else). Example: root step 3 is "3"; first step inside Then of condition at 5 is "5.b0.0". Perfecto does not persist paths; they change when steps are inserted, moved, or deleted. Always call view_test_structure before the next structure edit; do not reuse step_path from a previous mutation response.
764791
- Use parent_path on add_command with the step_path of a LogicalStep, Loop, or Branch from view_test_structure.

tools/ai_scriptless_script.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ def test_file_name(item_key: str) -> str:
3636
return path.rsplit("/", 1)[-1]
3737

3838

39+
VISIBILITY_UI_ROOT = {
40+
"PRIVATE": "My Tests",
41+
"PUBLIC": "Public Tests",
42+
"GROUP": "Group Tests",
43+
}
44+
45+
46+
def format_test_ui_location(item_key: str) -> str:
47+
"""Map itemKey to folder/test labels shown in the AI Scriptless Open Test UI."""
48+
visibility, path = split_item_key(item_key)
49+
root = VISIBILITY_UI_ROOT.get(visibility, visibility)
50+
file_name = test_file_name(item_key).removesuffix(".xml")
51+
folder = path.rsplit("/", 1)[0] if "/" in path else ""
52+
if folder:
53+
return f'"{root}" → folder "{folder}" → test "{file_name}"'
54+
return f'"{root}" → test "{file_name}"'
55+
56+
3957
def build_snapshot_search_body(test_id: str) -> dict[str, Any]:
4058
visibility, artifact_id = split_item_key(test_id)
4159
return {

tools/user_manager.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import Field
77

88
from config import perfecto
9-
from config.perfecto import TOOLS_PREFIX, SUPPORT_MESSAGE
9+
from config.perfecto import TOOLS_PREFIX, SUPPORT_MESSAGE, get_cloud_app_url
1010
from config.token import PerfectoToken, token_verify
1111
from formatters.user import format_users
1212
from models.manager import Manager
@@ -22,7 +22,17 @@ def __init__(self, token: Optional[PerfectoToken], ctx: Context):
2222
async def read_user(self) -> BaseResult:
2323
user_url = perfecto.get_user_management_api_url(self.token.cloud_name)
2424
user_url = user_url + "/current"
25-
return await api_request(self.token, "GET", endpoint=user_url, result_formatter=format_users)
25+
cloud_url = get_cloud_app_url(self.token.cloud_name)
26+
result = await api_request(
27+
self.token,
28+
"GET",
29+
endpoint=user_url,
30+
result_formatter=format_users,
31+
result_formatter_params={"cloud_name": self.token.cloud_name},
32+
)
33+
if not result.error:
34+
result.append_info([f"Connected Perfecto cloud: [{cloud_url}]({cloud_url})"])
35+
return result
2636

2737

2838
def register(mcp, token: Optional[PerfectoToken]):
@@ -31,7 +41,9 @@ def register(mcp, token: Optional[PerfectoToken]):
3141
description="""
3242
Operations on user information.
3343
Actions:
34-
- read_user: Read a current user information from Perfecto.
44+
- read_user: Read the current user and connected Perfecto cloud environment (cloud_name, cloud_url from PERFECTO_CLOUD_NAME).
45+
Hints:
46+
- Always render cloud_url as a markdown link when presenting the environment to the user.
3547
"""
3648
)
3749
async def user(

0 commit comments

Comments
 (0)