screenforge 0.4.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.
- cli/__init__.py +0 -0
- cli/_version.py +1 -0
- cli/dispatch.py +266 -0
- cli/doctor.py +487 -0
- cli/modes/__init__.py +0 -0
- cli/modes/action.py +262 -0
- cli/modes/default.py +248 -0
- cli/modes/demo.py +162 -0
- cli/modes/dry_run.py +237 -0
- cli/modes/init.py +133 -0
- cli/modes/plan.py +148 -0
- cli/modes/workflow.py +354 -0
- cli/parser.py +305 -0
- cli/reporter.py +207 -0
- cli/session.py +146 -0
- cli/shared.py +427 -0
- cli/shorthand.py +90 -0
- cli/tool_protocol_handlers.py +446 -0
- common/__init__.py +0 -0
- common/adapters/__init__.py +21 -0
- common/adapters/android_adapter.py +273 -0
- common/adapters/base_adapter.py +24 -0
- common/adapters/ios_adapter.py +278 -0
- common/adapters/web_adapter.py +271 -0
- common/ai.py +277 -0
- common/ai_autonomous.py +273 -0
- common/ai_heal.py +222 -0
- common/cache/__init__.py +15 -0
- common/cache/cache_hash.py +57 -0
- common/cache/cache_manager.py +300 -0
- common/cache/cache_stats.py +133 -0
- common/cache/cache_storage.py +79 -0
- common/cache/embedding_loader.py +150 -0
- common/capabilities.py +121 -0
- common/case_memory.py +327 -0
- common/error_codes.py +61 -0
- common/exceptions.py +18 -0
- common/executor.py +1504 -0
- common/failure_diagnosis.py +138 -0
- common/history_manager.py +75 -0
- common/logs.py +168 -0
- common/mcp_server.py +467 -0
- common/preflight.py +496 -0
- common/progress.py +37 -0
- common/run_reporter.py +415 -0
- common/run_resume.py +149 -0
- common/runtime_modes.py +35 -0
- common/tool_protocol.py +196 -0
- common/visual_fallback.py +71 -0
- common/workflow_schema.py +150 -0
- config/__init__.py +0 -0
- config/config.py +167 -0
- config/env_loader.py +76 -0
- screenforge-0.4.0.dist-info/METADATA +43 -0
- screenforge-0.4.0.dist-info/RECORD +64 -0
- screenforge-0.4.0.dist-info/WHEEL +5 -0
- screenforge-0.4.0.dist-info/entry_points.txt +2 -0
- screenforge-0.4.0.dist-info/licenses/LICENSE +21 -0
- screenforge-0.4.0.dist-info/top_level.txt +4 -0
- utils/__init__.py +0 -0
- utils/screenshot_annotator.py +60 -0
- utils/utils_ios.py +195 -0
- utils/utils_web.py +304 -0
- utils/utils_xml.py +218 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Tool protocol handlers: MCP server, tool-request, tool-stdin, inspect_ui."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import nullcontext
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from cli.parser import build_parser, validate_cli_args
|
|
10
|
+
from cli.reporter import (
|
|
11
|
+
_load_context_content,
|
|
12
|
+
_resolve_control_identity,
|
|
13
|
+
_resolve_output_script_path,
|
|
14
|
+
)
|
|
15
|
+
from cli.shared import (
|
|
16
|
+
_capture_ui_state,
|
|
17
|
+
_connect_adapter,
|
|
18
|
+
_SharedAdapterManager,
|
|
19
|
+
config,
|
|
20
|
+
current_url,
|
|
21
|
+
log,
|
|
22
|
+
)
|
|
23
|
+
from common.run_resume import RunContextLoadError, load_run_bundle
|
|
24
|
+
from common.runtime_modes import MODE_DOCTOR, resolve_execution_mode
|
|
25
|
+
from common.tool_protocol import (
|
|
26
|
+
ToolRequestError,
|
|
27
|
+
build_capabilities_response,
|
|
28
|
+
build_cli_arg_overrides,
|
|
29
|
+
load_tool_request,
|
|
30
|
+
load_tool_request_from_stdin,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _NullRunReporter:
|
|
35
|
+
def emit_event(self, event: str, **payload) -> None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def save_screenshot(self, img_bytes: bytes, step_index: int, name: str | None = None) -> str:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _list_run_dirs(base_dir: Path) -> set[Path]:
|
|
43
|
+
if not base_dir.exists():
|
|
44
|
+
return set()
|
|
45
|
+
return {item for item in base_dir.iterdir() if item.is_dir()}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_new_run_dir(before: set[Path], base_dir: Path) -> Path | None:
|
|
49
|
+
after = _list_run_dirs(base_dir)
|
|
50
|
+
new_dirs = sorted(after - before)
|
|
51
|
+
if new_dirs:
|
|
52
|
+
return new_dirs[-1]
|
|
53
|
+
if not after:
|
|
54
|
+
return None
|
|
55
|
+
return sorted(after)[-1]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _emit_tool_response(payload: dict) -> int:
|
|
59
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
60
|
+
sys.stdout.flush()
|
|
61
|
+
return int(payload.get("exit_code", 0))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _empty_run_assets() -> dict:
|
|
65
|
+
return {
|
|
66
|
+
"summary_path": "",
|
|
67
|
+
"artifacts_path": "",
|
|
68
|
+
"pytest_replay_path": "",
|
|
69
|
+
"failure_analysis": {},
|
|
70
|
+
"pytest_asset": {},
|
|
71
|
+
"resume_commands": {},
|
|
72
|
+
"recommended_next_step": None,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_run_assets(run_dir: Path | None) -> dict:
|
|
77
|
+
if not run_dir:
|
|
78
|
+
return {
|
|
79
|
+
"summary": {},
|
|
80
|
+
"run_assets": _empty_run_assets(),
|
|
81
|
+
"resume_context": {},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
bundle = load_run_bundle(run_dir)
|
|
85
|
+
return {
|
|
86
|
+
"summary": bundle.get("summary", {}) or {},
|
|
87
|
+
"run_assets": bundle.get("run_assets", {}) or _empty_run_assets(),
|
|
88
|
+
"resume_context": bundle.get("resume_context", {}) or {},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _load_case_memory_store():
|
|
93
|
+
from common.case_memory import CaseMemoryStore
|
|
94
|
+
|
|
95
|
+
return CaseMemoryStore()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _find_case_memory_hit(args, execution_mode: str) -> dict | None:
|
|
99
|
+
if execution_mode == MODE_DOCTOR:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
control_identity = _resolve_control_identity(args, execution_mode)
|
|
103
|
+
control_kind = str(control_identity.get("control_kind", "")).strip()
|
|
104
|
+
if control_kind == "doctor":
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
return _load_case_memory_store().find_entry(
|
|
108
|
+
platform=args.platform,
|
|
109
|
+
control_kind=control_kind,
|
|
110
|
+
control_label=control_identity.get("control_label", ""),
|
|
111
|
+
source_ref=control_identity.get("control_source_ref", ""),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_load_case_memory_payload(
|
|
116
|
+
platform: str = "",
|
|
117
|
+
control_kind: str = "",
|
|
118
|
+
query: str = "",
|
|
119
|
+
source_ref: str = "",
|
|
120
|
+
limit: int = 20,
|
|
121
|
+
) -> dict:
|
|
122
|
+
entries = _load_case_memory_store().query_entries(
|
|
123
|
+
platform=platform,
|
|
124
|
+
control_kind=control_kind,
|
|
125
|
+
query=query,
|
|
126
|
+
source_ref=source_ref,
|
|
127
|
+
limit=limit,
|
|
128
|
+
)
|
|
129
|
+
return {
|
|
130
|
+
"ok": True,
|
|
131
|
+
"operation": "load_case_memory",
|
|
132
|
+
"exit_code": 0,
|
|
133
|
+
"case_memory_path": str(config.CASE_MEMORY_PATH),
|
|
134
|
+
"entries": entries,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_inspect_ui_payload(request, shared_adapter_manager: _SharedAdapterManager | None = None) -> dict:
|
|
139
|
+
parser = build_parser()
|
|
140
|
+
request_args = parser.parse_args([])
|
|
141
|
+
request_args.platform = request.platform
|
|
142
|
+
request_args.env = request.env
|
|
143
|
+
request_args.vision = request.vision
|
|
144
|
+
|
|
145
|
+
adapter = None
|
|
146
|
+
owns_adapter = False
|
|
147
|
+
try:
|
|
148
|
+
if shared_adapter_manager:
|
|
149
|
+
adapter = shared_adapter_manager.get_or_create(request.platform, request.env)
|
|
150
|
+
else:
|
|
151
|
+
adapter = _connect_adapter(request_args, _NullRunReporter())
|
|
152
|
+
owns_adapter = True
|
|
153
|
+
ui_json, screenshot_base64 = _capture_ui_state(
|
|
154
|
+
request_args,
|
|
155
|
+
adapter,
|
|
156
|
+
_NullRunReporter(),
|
|
157
|
+
1,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if not screenshot_base64:
|
|
161
|
+
try:
|
|
162
|
+
img_bytes = adapter.take_screenshot()
|
|
163
|
+
screenshot_base64 = base64.b64encode(img_bytes).decode("utf-8")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
log.warning(f"⚠️ [Warning] inspect_ui screenshot capture failed: {e}")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
ui_tree = json.loads(ui_json)
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
ui_tree = {"ui_elements": [], "raw": ui_json}
|
|
171
|
+
|
|
172
|
+
# Sync the shared executor's web ref cache to THIS inspect, so a
|
|
173
|
+
# subsequent `--action --locator-type ref @N` in the same MCP session
|
|
174
|
+
# resolves against the page just inspected, not a stale prior page. The
|
|
175
|
+
# cache lives on the per-platform UIExecutor the adapter manager owns;
|
|
176
|
+
# without a manager (one-shot tool call) there's no follow-up action to
|
|
177
|
+
# share with, so syncing would be a no-op and is skipped.
|
|
178
|
+
if request.platform == "web" and shared_adapter_manager:
|
|
179
|
+
try:
|
|
180
|
+
executor = shared_adapter_manager.get_executor(request.platform, request.env)
|
|
181
|
+
executor.set_ui_elements(ui_tree.get("ui_elements", []) or [])
|
|
182
|
+
except Exception as e:
|
|
183
|
+
log.warning(f"⚠️ [Warning] Failed to sync ref cache from inspect_ui: {e}")
|
|
184
|
+
|
|
185
|
+
annotated_screenshot_base64 = ""
|
|
186
|
+
if screenshot_base64 and ui_tree.get("ui_elements"):
|
|
187
|
+
try:
|
|
188
|
+
from utils.screenshot_annotator import annotate_screenshot
|
|
189
|
+
raw_bytes = base64.b64decode(screenshot_base64)
|
|
190
|
+
annotated_bytes = annotate_screenshot(raw_bytes, ui_tree["ui_elements"])
|
|
191
|
+
annotated_screenshot_base64 = base64.b64encode(annotated_bytes).decode("utf-8")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
log.warning(f"⚠️ [Warning] Annotated screenshot generation failed: {e}")
|
|
194
|
+
|
|
195
|
+
page_url = current_url(adapter, request.platform)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"ok": True,
|
|
199
|
+
"operation": "inspect_ui",
|
|
200
|
+
"exit_code": 0,
|
|
201
|
+
"platform": request.platform,
|
|
202
|
+
"env": request.env,
|
|
203
|
+
"ui_json": ui_json,
|
|
204
|
+
"ui_tree": ui_tree,
|
|
205
|
+
"element_count": len(ui_tree.get("ui_elements", []) or []),
|
|
206
|
+
"screenshot_base64": screenshot_base64 or "",
|
|
207
|
+
"annotated_screenshot_base64": annotated_screenshot_base64,
|
|
208
|
+
"current_url": page_url,
|
|
209
|
+
}
|
|
210
|
+
except Exception as e:
|
|
211
|
+
return {
|
|
212
|
+
"ok": False,
|
|
213
|
+
"operation": "inspect_ui",
|
|
214
|
+
"exit_code": 1,
|
|
215
|
+
"platform": request.platform,
|
|
216
|
+
"env": request.env,
|
|
217
|
+
"error": str(e),
|
|
218
|
+
"current_url": "",
|
|
219
|
+
}
|
|
220
|
+
finally:
|
|
221
|
+
if owns_adapter and adapter:
|
|
222
|
+
try:
|
|
223
|
+
adapter.teardown()
|
|
224
|
+
except Exception as e:
|
|
225
|
+
log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _requires_model_runtime(args, execution_mode: str) -> bool:
|
|
229
|
+
if execution_mode == MODE_DOCTOR:
|
|
230
|
+
return False
|
|
231
|
+
if str(getattr(args, "workflow", "")).strip():
|
|
232
|
+
return False
|
|
233
|
+
if str(getattr(args, "action", "")).strip():
|
|
234
|
+
return False
|
|
235
|
+
return bool(str(getattr(args, "goal", "")).strip())
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_tool_response_payload(request, shared_adapter_manager: _SharedAdapterManager | None = None) -> dict:
|
|
239
|
+
if request.operation == "capabilities":
|
|
240
|
+
payload = build_capabilities_response()
|
|
241
|
+
payload["exit_code"] = 0
|
|
242
|
+
return payload
|
|
243
|
+
if request.operation == "load_run":
|
|
244
|
+
return build_load_run_payload(request.run_id)
|
|
245
|
+
if request.operation == "load_case_memory":
|
|
246
|
+
return build_load_case_memory_payload(
|
|
247
|
+
platform=request.platform,
|
|
248
|
+
control_kind=request.control_kind,
|
|
249
|
+
query=request.query,
|
|
250
|
+
source_ref=request.source_ref,
|
|
251
|
+
limit=request.limit,
|
|
252
|
+
)
|
|
253
|
+
if request.operation == "inspect_ui":
|
|
254
|
+
return build_inspect_ui_payload(request, shared_adapter_manager=shared_adapter_manager)
|
|
255
|
+
|
|
256
|
+
parser = build_parser()
|
|
257
|
+
request_args = parser.parse_args([])
|
|
258
|
+
for key, value in build_cli_arg_overrides(request).items():
|
|
259
|
+
setattr(request_args, key, value)
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
validate_cli_args(request_args)
|
|
263
|
+
except ValueError as e:
|
|
264
|
+
return {
|
|
265
|
+
"ok": False,
|
|
266
|
+
"operation": "execute",
|
|
267
|
+
"exit_code": 2,
|
|
268
|
+
"error": str(e),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
execution_mode = resolve_execution_mode(
|
|
272
|
+
doctor=request_args.doctor,
|
|
273
|
+
plan_only=request_args.plan_only,
|
|
274
|
+
dry_run=request_args.dry_run,
|
|
275
|
+
)
|
|
276
|
+
case_memory_hit = _find_case_memory_hit(request_args, execution_mode)
|
|
277
|
+
output_script_path = _resolve_output_script_path(request_args)
|
|
278
|
+
run_base_dir = Path(config.RUN_REPORT_BASE_DIR)
|
|
279
|
+
previous_run_dirs = _list_run_dirs(run_base_dir)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
context_content, resume_context = _load_context_content(request_args)
|
|
283
|
+
except RunContextLoadError as e:
|
|
284
|
+
return {
|
|
285
|
+
"ok": False,
|
|
286
|
+
"operation": "execute",
|
|
287
|
+
"exit_code": 2,
|
|
288
|
+
"mode": execution_mode,
|
|
289
|
+
"error": str(e),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if _requires_model_runtime(request_args, execution_mode) and not config.validate_config():
|
|
293
|
+
return {
|
|
294
|
+
"ok": False,
|
|
295
|
+
"operation": "execute",
|
|
296
|
+
"exit_code": 1,
|
|
297
|
+
"mode": execution_mode,
|
|
298
|
+
"error": "Configuration validation failed",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
mute_logs_context = nullcontext
|
|
302
|
+
try:
|
|
303
|
+
from common.logs import mute_stderr_logs as _mute_stderr_logs
|
|
304
|
+
|
|
305
|
+
mute_logs_context = _mute_stderr_logs
|
|
306
|
+
except Exception:
|
|
307
|
+
mute_logs_context = nullcontext
|
|
308
|
+
|
|
309
|
+
from cli.dispatch import _dispatch_execution
|
|
310
|
+
|
|
311
|
+
with mute_logs_context():
|
|
312
|
+
exit_code = _dispatch_execution(
|
|
313
|
+
request_args,
|
|
314
|
+
execution_mode,
|
|
315
|
+
output_script_path,
|
|
316
|
+
context_content,
|
|
317
|
+
resume_context,
|
|
318
|
+
shared_adapter_manager=shared_adapter_manager,
|
|
319
|
+
)
|
|
320
|
+
run_dir = _resolve_new_run_dir(previous_run_dirs, run_base_dir)
|
|
321
|
+
loaded_assets = _load_run_assets(run_dir) if run_dir and (run_dir / "summary.json").exists() else {
|
|
322
|
+
"summary": {},
|
|
323
|
+
"run_assets": _empty_run_assets(),
|
|
324
|
+
"resume_context": {},
|
|
325
|
+
}
|
|
326
|
+
summary = loaded_assets["summary"]
|
|
327
|
+
run_assets = loaded_assets["run_assets"]
|
|
328
|
+
summary_path = run_assets.get("summary_path", "")
|
|
329
|
+
|
|
330
|
+
# Minimal MCP-execute enrichment: error_code + fix from the single-source
|
|
331
|
+
# table (NO did-you-mean candidates — this run-report path has no live
|
|
332
|
+
# ui_elements). NOTE: this stays {} until the autonomous run reporter
|
|
333
|
+
# propagates the executor's error_code into summary.json; today
|
|
334
|
+
# run_reporter writes category/stage/last_error but not error_code, so the
|
|
335
|
+
# `if code:` guard is the honest no-op — never a fabricated code. Wiring it
|
|
336
|
+
# live is a follow-up (propagate error_code through the run summary).
|
|
337
|
+
failure_diagnosis = {}
|
|
338
|
+
if exit_code != 0:
|
|
339
|
+
from common.error_codes import lookup
|
|
340
|
+
|
|
341
|
+
code = str(summary.get("error_code", "") or "").strip()
|
|
342
|
+
if code:
|
|
343
|
+
msg, fix = lookup(code)
|
|
344
|
+
failure_diagnosis = {"error_code": code, "message": msg, "fix": fix}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
"ok": exit_code == 0,
|
|
348
|
+
"operation": "execute",
|
|
349
|
+
"mode": execution_mode,
|
|
350
|
+
"exit_code": exit_code,
|
|
351
|
+
"run_dir": str(run_dir) if run_dir else "",
|
|
352
|
+
"summary_path": summary_path,
|
|
353
|
+
"summary": summary,
|
|
354
|
+
"run_assets": run_assets,
|
|
355
|
+
"case_memory_hit": bool(case_memory_hit),
|
|
356
|
+
"case_memory_entry": summary.get("case_memory_entry") or case_memory_hit,
|
|
357
|
+
"recommended_next_step": run_assets.get("recommended_next_step"),
|
|
358
|
+
"failure_diagnosis": failure_diagnosis,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def build_load_run_payload(run_id: str) -> dict:
|
|
363
|
+
run_id = str(run_id).strip()
|
|
364
|
+
run_dir = Path(config.RUN_REPORT_BASE_DIR) / run_id
|
|
365
|
+
try:
|
|
366
|
+
bundle = load_run_bundle(run_dir)
|
|
367
|
+
except RunContextLoadError as e:
|
|
368
|
+
return {
|
|
369
|
+
"ok": False,
|
|
370
|
+
"operation": "load_run",
|
|
371
|
+
"exit_code": 2,
|
|
372
|
+
"run_id": run_id,
|
|
373
|
+
"error": str(e),
|
|
374
|
+
"run_assets": _empty_run_assets(),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
run_assets = bundle.get("run_assets", {}) or _empty_run_assets()
|
|
378
|
+
return {
|
|
379
|
+
"ok": True,
|
|
380
|
+
"operation": "load_run",
|
|
381
|
+
"exit_code": 0,
|
|
382
|
+
"run_id": bundle.get("run_id", "") or run_id,
|
|
383
|
+
"run_dir": bundle.get("run_dir", str(run_dir)),
|
|
384
|
+
"summary_path": run_assets.get("summary_path", ""),
|
|
385
|
+
"summary": bundle.get("summary", {}) or {},
|
|
386
|
+
"run_assets": run_assets,
|
|
387
|
+
"recommended_next_step": run_assets.get("recommended_next_step"),
|
|
388
|
+
"resume_context": bundle.get("resume_context", {}) or {},
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _run_tool_request(request) -> int:
|
|
393
|
+
return _emit_tool_response(build_tool_response_payload(request))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def run_tool_request_mode(args) -> int:
|
|
397
|
+
try:
|
|
398
|
+
request = load_tool_request(args.tool_request)
|
|
399
|
+
except ToolRequestError as e:
|
|
400
|
+
return _emit_tool_response(
|
|
401
|
+
{
|
|
402
|
+
"ok": False,
|
|
403
|
+
"operation": "tool_request",
|
|
404
|
+
"exit_code": 2,
|
|
405
|
+
"error": str(e),
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
return _run_tool_request(request)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def run_tool_stdin_mode(args) -> int:
|
|
412
|
+
try:
|
|
413
|
+
request = load_tool_request_from_stdin(sys.stdin.read())
|
|
414
|
+
except ToolRequestError as e:
|
|
415
|
+
return _emit_tool_response(
|
|
416
|
+
{
|
|
417
|
+
"ok": False,
|
|
418
|
+
"operation": "tool_stdin",
|
|
419
|
+
"exit_code": 2,
|
|
420
|
+
"error": str(e),
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
return _run_tool_request(request)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def run_mcp_server_mode(args) -> int:
|
|
427
|
+
from functools import partial
|
|
428
|
+
|
|
429
|
+
from common.mcp_server import run_stdio_mcp_server
|
|
430
|
+
|
|
431
|
+
shared_mgr = _SharedAdapterManager()
|
|
432
|
+
try:
|
|
433
|
+
return run_stdio_mcp_server(
|
|
434
|
+
partial(build_tool_response_payload, shared_adapter_manager=shared_mgr),
|
|
435
|
+
build_load_run_payload,
|
|
436
|
+
partial(build_inspect_ui_payload, shared_adapter_manager=shared_mgr),
|
|
437
|
+
lambda request: build_load_case_memory_payload(
|
|
438
|
+
platform=request.platform,
|
|
439
|
+
control_kind=request.control_kind,
|
|
440
|
+
query=request.query,
|
|
441
|
+
source_ref=request.source_ref,
|
|
442
|
+
limit=request.limit,
|
|
443
|
+
),
|
|
444
|
+
)
|
|
445
|
+
finally:
|
|
446
|
+
shared_mgr.teardown_all()
|
common/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .base_adapter import BasePlatformAdapter
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"BasePlatformAdapter",
|
|
5
|
+
"AndroidU2Adapter",
|
|
6
|
+
"IosWdaAdapter",
|
|
7
|
+
"WebPlaywrightAdapter",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str):
|
|
12
|
+
if name == "AndroidU2Adapter":
|
|
13
|
+
from .android_adapter import AndroidU2Adapter
|
|
14
|
+
return AndroidU2Adapter
|
|
15
|
+
if name == "IosWdaAdapter":
|
|
16
|
+
from .ios_adapter import IosWdaAdapter
|
|
17
|
+
return IosWdaAdapter
|
|
18
|
+
if name == "WebPlaywrightAdapter":
|
|
19
|
+
from .web_adapter import WebPlaywrightAdapter
|
|
20
|
+
return WebPlaywrightAdapter
|
|
21
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|