answer42 0.4.97__tar.gz → 0.4.98__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.
Files changed (64) hide show
  1. {answer42-0.4.97 → answer42-0.4.98}/PKG-INFO +24 -1
  2. {answer42-0.4.97 → answer42-0.4.98}/README.md +23 -0
  3. {answer42-0.4.97 → answer42-0.4.98}/pyproject.toml +1 -1
  4. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  5. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  6. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/runtime.py +20 -4
  7. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/server.py +68 -2
  8. {answer42-0.4.97 → answer42-0.4.98}/.gitignore +0 -0
  9. {answer42-0.4.97 → answer42-0.4.98}/LICENSE +0 -0
  10. {answer42-0.4.97 → answer42-0.4.98}/credentials.example.json +0 -0
  11. {answer42-0.4.97 → answer42-0.4.98}/docs/agent-installation.md +0 -0
  12. {answer42-0.4.97 → answer42-0.4.98}/docs/architecture.md +0 -0
  13. {answer42-0.4.97 → answer42-0.4.98}/docs/assets/answer42-logo.png +0 -0
  14. {answer42-0.4.97 → answer42-0.4.98}/docs/assets/platform42-logo.svg +0 -0
  15. {answer42-0.4.97 → answer42-0.4.98}/docs/installation.md +0 -0
  16. {answer42-0.4.97 → answer42-0.4.98}/scripts/build_cf.py +0 -0
  17. {answer42-0.4.97 → answer42-0.4.98}/scripts/build_pages.py +0 -0
  18. {answer42-0.4.97 → answer42-0.4.98}/scripts/e2e_http_smoke.py +0 -0
  19. {answer42-0.4.97 → answer42-0.4.98}/scripts/e2e_stable.py +0 -0
  20. {answer42-0.4.97 → answer42-0.4.98}/scripts/openclaw_mcp_autoreload.py +0 -0
  21. {answer42-0.4.97 → answer42-0.4.98}/scripts/rag_cli.py +0 -0
  22. {answer42-0.4.97 → answer42-0.4.98}/src/cf/ConfigDumpInfo.xml +0 -0
  23. {answer42-0.4.97 → answer42-0.4.98}/src/cf/Configuration.xml +0 -0
  24. {answer42-0.4.97 → answer42-0.4.98}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  25. {answer42-0.4.97 → answer42-0.4.98}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl" +0 -0
  26. {answer42-0.4.97 → answer42-0.4.98}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
  27. {answer42-0.4.97 → answer42-0.4.98}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
  28. {answer42-0.4.97 → answer42-0.4.98}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  29. {answer42-0.4.97 → answer42-0.4.98}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  30. {answer42-0.4.97 → answer42-0.4.98}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  31. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Catalogs//320/237/320/241_/320/222/320/273/320/260/320/264/320/265/320/273/320/265/321/206.xml" +0 -0
  32. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Catalogs//320/237/320/241_/320/241/320/277/321/200/320/260/320/262/320/276/321/207/320/275/320/270/320/2721.xml" +0 -0
  33. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/ConfigDumpInfo.xml +0 -0
  34. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Configuration.xml +0 -0
  35. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  36. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ManagerModule.bsl" +0 -0
  37. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ObjectModule.bsl" +0 -0
  38. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205/Ext/Template.xml" +0 -0
  39. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205.xml" +0 -0
  40. {answer42-0.4.97 → answer42-0.4.98}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262.xml" +0 -0
  41. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/__init__.py +0 -0
  42. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/assets/__init__.py +0 -0
  43. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
  44. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
  45. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/assets/skills/answer42-universal-report/SKILL.md +0 -0
  46. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/auth.py +0 -0
  47. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/bridge.py +0 -0
  48. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/credentials.py +0 -0
  49. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/os_support.py +0 -0
  50. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/platform.py +0 -0
  51. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/protocol.py +0 -0
  52. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/__init__.py +0 -0
  53. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/detect.py +0 -0
  54. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/dump.py +0 -0
  55. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/model.py +0 -0
  56. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/parsers.py +0 -0
  57. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/service.py +0 -0
  58. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/store.py +0 -0
  59. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/summary_backend.py +0 -0
  60. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/rag/summary_vector.py +0 -0
  61. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/recorder.py +0 -0
  62. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/release_helper.py +0 -0
  63. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/skill_installer.py +0 -0
  64. {answer42-0.4.97 → answer42-0.4.98}/src/mcp_1c/window_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: answer42
3
- Version: 0.4.97
3
+ Version: 0.4.98
4
4
  Summary: Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API
5
5
  Author: Marvin (AI Assistant), 42Clouds, and contributors
6
6
  Author-email: "Kosolapov Stanislav (proDOOMman)" <prodoomman@gmail.com>
@@ -162,6 +162,29 @@ answer42 install-skills --dry-run --agent all
162
162
  4. зарегистрируйте MCP-сервер в агентском клиенте;
163
163
  5. проверьте установку через `credentials_check`, затем smoke-сессию `start_session` → `active_window` → `stop_session`.
164
164
 
165
+ ## Linux display prerequisite
166
+
167
+ На Linux `start_session` **всегда** запускает два изолированных Xvfb-дисплея: один для test manager и второй для test client. Это намеренно не использует существующий `DISPLAY`, даже если Answer42 запущен в живой X11-сессии: автоматизация и её скриншоты не должны захватывать пользовательский рабочий стол.
168
+
169
+ Поэтому пакет `xvfb` обязателен и для desktop-, и для headless-установок:
170
+
171
+ ```bash
172
+ # Debian / Ubuntu
173
+ sudo apt install xvfb
174
+
175
+ # Fedora / RHEL
176
+ sudo dnf install xorg-x11-server-Xvfb
177
+ ```
178
+
179
+ Если Xvfb отсутствует, `start_session` завершается до создания ресурсов с понятной подсказкой по установке.
180
+
181
+ ## Скриншоты: stdio и StreamableHTTP
182
+
183
+ `screenshot` сохраняет PNG на MCP-хосте и больше не помещает его base64-представление в результат tool-call.
184
+
185
+ - В **stdio** возвращаются только `path`, размер и диагностические поля. Агентский хост при необходимости прикладывает файл по пути.
186
+ - В **StreamableHTTP** добавляется непрозрачная одноразовая ссылка `url` на PNG. Она действует один час по умолчанию (`ONEC_MCP_SCREENSHOT_URL_TTL_SECONDS`), исчезает после первого скачивания и существует не дольше процесса сервера. Для внешнего reverse proxy укажите публичный base URL через `ONEC_MCP_SCREENSHOT_URL_BASE`.
187
+
165
188
  ## Транспорты: stdio и StreamableHTTP
166
189
 
167
190
  По умолчанию Answer42 использует stdio MCP transport (запускается MCP-хостом как subprocess). Для удалённого deployment доступен **StreamableHTTP** режим с MCP auth:
@@ -128,6 +128,29 @@ answer42 install-skills --dry-run --agent all
128
128
  4. зарегистрируйте MCP-сервер в агентском клиенте;
129
129
  5. проверьте установку через `credentials_check`, затем smoke-сессию `start_session` → `active_window` → `stop_session`.
130
130
 
131
+ ## Linux display prerequisite
132
+
133
+ На Linux `start_session` **всегда** запускает два изолированных Xvfb-дисплея: один для test manager и второй для test client. Это намеренно не использует существующий `DISPLAY`, даже если Answer42 запущен в живой X11-сессии: автоматизация и её скриншоты не должны захватывать пользовательский рабочий стол.
134
+
135
+ Поэтому пакет `xvfb` обязателен и для desktop-, и для headless-установок:
136
+
137
+ ```bash
138
+ # Debian / Ubuntu
139
+ sudo apt install xvfb
140
+
141
+ # Fedora / RHEL
142
+ sudo dnf install xorg-x11-server-Xvfb
143
+ ```
144
+
145
+ Если Xvfb отсутствует, `start_session` завершается до создания ресурсов с понятной подсказкой по установке.
146
+
147
+ ## Скриншоты: stdio и StreamableHTTP
148
+
149
+ `screenshot` сохраняет PNG на MCP-хосте и больше не помещает его base64-представление в результат tool-call.
150
+
151
+ - В **stdio** возвращаются только `path`, размер и диагностические поля. Агентский хост при необходимости прикладывает файл по пути.
152
+ - В **StreamableHTTP** добавляется непрозрачная одноразовая ссылка `url` на PNG. Она действует один час по умолчанию (`ONEC_MCP_SCREENSHOT_URL_TTL_SECONDS`), исчезает после первого скачивания и существует не дольше процесса сервера. Для внешнего reverse proxy укажите публичный base URL через `ONEC_MCP_SCREENSHOT_URL_BASE`.
153
+
131
154
  ## Транспорты: stdio и StreamableHTTP
132
155
 
133
156
  По умолчанию Answer42 использует stdio MCP transport (запускается MCP-хостом как subprocess). Для удалённого deployment доступен **StreamableHTTP** режим с MCP auth:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "answer42"
3
- version = "0.4.97"
3
+ version = "0.4.98"
4
4
  description = "Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -118,13 +118,29 @@ def find_free_port(start: int = DEFAULT_WS_PORT, end: int | None = None) -> int:
118
118
  raise RuntimeError(f"No free port found in range {start}..{end}")
119
119
 
120
120
 
121
- def start_xvfb(display: str, resolution: str = DEFAULT_DISPLAY_RES) -> subprocess.Popen:
122
- """Start Xvfb for the given display and return the Popen handle."""
121
+ def require_xvfb() -> str:
122
+ """Return the Xvfb executable or raise an actionable Linux prerequisite error.
123
+
124
+ Answer42 deliberately runs the test manager and test client on two isolated
125
+ X displays. It must therefore have Xvfb even when the MCP process already
126
+ has access to a live desktop ``DISPLAY``.
127
+ """
123
128
  if os_support.IS_WINDOWS:
124
129
  raise RuntimeError("Xvfb is not available on Windows")
125
130
  xvfb_bin = shutil.which("Xvfb") or "/usr/bin/Xvfb"
126
- if not Path(xvfb_bin).exists():
127
- raise FileNotFoundError("Xvfb not found; install it to run headless sessions")
131
+ if not Path(xvfb_bin).is_file():
132
+ raise RuntimeError(
133
+ "Xvfb is required to start an Answer42 session on Linux, including on a host with an active X11 desktop. "
134
+ "Answer42 starts isolated displays for the test manager and test client. "
135
+ "Install the 'xvfb' package (Debian/Ubuntu: sudo apt install xvfb; Fedora/RHEL: sudo dnf install xorg-x11-server-Xvfb) "
136
+ "and retry start_session."
137
+ )
138
+ return xvfb_bin
139
+
140
+
141
+ def start_xvfb(display: str, resolution: str = DEFAULT_DISPLAY_RES) -> subprocess.Popen:
142
+ """Start Xvfb for the given display and return the Popen handle."""
143
+ xvfb_bin = require_xvfb()
128
144
  proc = subprocess.Popen(
129
145
  [xvfb_bin, display, "-screen", "0", resolution, "-ac", "+extension", "GLX", "+render", "-noreset"],
130
146
  stdout=subprocess.DEVNULL,
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import asyncio
5
- import base64
6
5
  import copy
7
6
  import hashlib
8
7
  import importlib.resources
@@ -29,6 +28,7 @@ from mcp.server import MCPServer
29
28
  from mcp.server.lowlevel import NotificationOptions
30
29
  from mcp.server.mcpserver.resources import FileResource
31
30
  from mcp.server.subscriptions import ToolsListChanged
31
+ from starlette.responses import FileResponse, PlainTextResponse
32
32
 
33
33
  from . import __version__, os_support, release_helper, runtime, skill_installer
34
34
  from . import platform as _platform
@@ -84,6 +84,11 @@ FORM_HINTS_LOCK = os_support.runtime_path("ONEC_MCP_FORM_HINTS_LOCK", "form-hint
84
84
  UI_TREE_RESOURCE_AUTO_THRESHOLD_TOKENS = int(os.getenv("ONEC_MCP_UI_TREE_RESOURCE_AUTO_THRESHOLD_TOKENS", "50000"))
85
85
  UI_TREE_RESOURCE_AUTO_THRESHOLD_BYTES = int(os.getenv("ONEC_MCP_UI_TREE_RESOURCE_AUTO_THRESHOLD_BYTES", "0")) # deprecated fallback; 0 disables
86
86
  NOTIFY_TOOLS_CHANGED_ON_STARTUP = os.getenv("ONEC_MCP_NOTIFY_TOOLS_CHANGED_ON_STARTUP", "1").strip().lower() not in {"0", "false", "no", "off"}
87
+ SCREENSHOT_URL_TTL_SECONDS = max(1, int(os.getenv("ONEC_MCP_SCREENSHOT_URL_TTL_SECONDS", "3600")))
88
+ # Set only in StreamableHTTP mode. stdio deliberately returns filesystem paths
89
+ # and never serializes screenshot bytes into an MCP tool response.
90
+ _SCREENSHOT_URL_BASE = ""
91
+ _SCREENSHOT_URLS: dict[str, tuple[Path, float]] = {}
87
92
 
88
93
 
89
94
  def _float_env(names: str | tuple[str, ...], default: float) -> float:
@@ -5210,6 +5215,13 @@ async def start_session(
5210
5215
  if sid in _SESSIONS:
5211
5216
  raise ValueError(f"Session {sid!r} already exists.")
5212
5217
 
5218
+ # A desktop DISPLAY is intentionally not reused: two isolated displays
5219
+ # prevent an interactive user desktop from becoming part of automation or
5220
+ # screenshots. Check the prerequisite before allocating any session state
5221
+ # so a missing package produces a clear error rather than FileNotFoundError.
5222
+ if not os_support.IS_WINDOWS:
5223
+ runtime.require_xvfb()
5224
+
5213
5225
  data_dir = os_support.default_data_dir() / "sessions" / sid
5214
5226
  with _port_allocation_lock():
5215
5227
  port_offset = _stable_session_port_offset(sid)
@@ -10972,6 +10984,42 @@ async def _dev_eval(
10972
10984
  return await _bridge_call_recorded(session_id, "dev_eval", {"code": code})
10973
10985
 
10974
10986
 
10987
+ def _prune_expired_screenshot_urls(now: float | None = None) -> None:
10988
+ now = time.time() if now is None else now
10989
+ for token, (_, expires_at) in list(_SCREENSHOT_URLS.items()):
10990
+ if expires_at <= now:
10991
+ _SCREENSHOT_URLS.pop(token, None)
10992
+
10993
+
10994
+ @mcp.custom_route("/_answer42/screenshots/{token}", methods=["GET"], include_in_schema=False)
10995
+ async def _download_screenshot(request: Any) -> Any:
10996
+ """Serve a single-use opaque StreamableHTTP screenshot link."""
10997
+ token = str(request.path_params.get("token") or "")
10998
+ _prune_expired_screenshot_urls()
10999
+ entry = _SCREENSHOT_URLS.pop(token, None)
11000
+ if entry is None:
11001
+ return PlainTextResponse("Screenshot link is expired or unavailable.", status_code=404)
11002
+ image_path, expires_at = entry
11003
+ if expires_at <= time.time() or not image_path.is_file():
11004
+ return PlainTextResponse("Screenshot link is expired or unavailable.", status_code=404)
11005
+ return FileResponse(image_path, media_type="image/png", filename=image_path.name)
11006
+
11007
+
11008
+ def _register_screenshot_url(path: Path) -> dict[str, Any] | None:
11009
+ """Expose an opaque, time-limited screenshot URL in StreamableHTTP mode."""
11010
+ if not _SCREENSHOT_URL_BASE:
11011
+ return None
11012
+ _prune_expired_screenshot_urls()
11013
+ token = uuid.uuid4().hex + uuid.uuid4().hex
11014
+ expires_at = time.time() + SCREENSHOT_URL_TTL_SECONDS
11015
+ _SCREENSHOT_URLS[token] = (path.resolve(), expires_at)
11016
+ return {
11017
+ "url": f"{_SCREENSHOT_URL_BASE}/_answer42/screenshots/{token}",
11018
+ "url_expires_at": datetime.fromtimestamp(expires_at, UTC).isoformat(),
11019
+ "url_ttl_seconds": SCREENSHOT_URL_TTL_SECONDS,
11020
+ }
11021
+
11022
+
10975
11023
  @mcp.tool()
10976
11024
  def screenshot(
10977
11025
  path: Annotated[str, "Output image path"] = "build/screenshots/screenshot.png",
@@ -10982,6 +11030,9 @@ def screenshot(
10982
11030
  ) -> dict[str, Any]:
10983
11031
  """Capture a screenshot from the MCP host. Defaults to the active visible 1C window.
10984
11032
 
11033
+ The response always contains the saved path, never image base64. In
11034
+ StreamableHTTP mode it additionally contains an opaque download URL that
11035
+ expires after one hour by default (``ONEC_MCP_SCREENSHOT_URL_TTL_SECONDS``).
10985
11036
  Screenshots are for user evidence and UI diagnostics only. Do not use them
10986
11037
  to analyze report/table/tabular-document/list data; use structured tools
10987
11038
  such as tabular_document_text, tabular_documents, table_rows,
@@ -11040,7 +11091,8 @@ def screenshot(
11040
11091
  ) from exc
11041
11092
 
11042
11093
  data = output_path.read_bytes()
11043
- result = {"path": str(output_path), "bytes": len(data), "base64": base64.b64encode(data).decode("ascii")}
11094
+ result: dict[str, Any] = {"path": str(output_path), "bytes": len(data)}
11095
+ result.update(_register_screenshot_url(output_path) or {})
11044
11096
  if window:
11045
11097
  result["window_geometry"] = geometry
11046
11098
  if fallback:
@@ -11103,6 +11155,13 @@ async def _run_http(args: argparse.Namespace) -> None:
11103
11155
  stateless_http=args.http_stateless,
11104
11156
  host=args.http_host,
11105
11157
  )
11158
+
11159
+ # Image data is not placed into MCP tool results. The custom route is
11160
+ # registered at module load because the MCP SDK builds the Starlette app
11161
+ # from its custom-route list. The opaque token makes the URL unguessable;
11162
+ # the short lifetime and single use bound access.
11163
+ global _SCREENSHOT_URL_BASE
11164
+ _SCREENSHOT_URL_BASE = (args.http_public_base_url or f"http://{args.http_host}:{args.http_port}").rstrip("/")
11106
11165
  config_uvicorn = uvicorn.Config(
11107
11166
  app,
11108
11167
  host=args.http_host,
@@ -11114,6 +11173,8 @@ async def _run_http(args: argparse.Namespace) -> None:
11114
11173
  try:
11115
11174
  await server.serve()
11116
11175
  finally:
11176
+ _SCREENSHOT_URLS.clear()
11177
+ _SCREENSHOT_URL_BASE = ""
11117
11178
  if tools_changed_task is not None:
11118
11179
  tools_changed_task.cancel()
11119
11180
  with suppress(asyncio.CancelledError):
@@ -11329,6 +11390,11 @@ def main(argv: list[str] | None = None) -> None:
11329
11390
  default=int(os.getenv("ONEC_MCP_HTTP_PORT", "8080")),
11330
11391
  help="port to bind the StreamableHTTP server to",
11331
11392
  )
11393
+ parser.add_argument(
11394
+ "--http-public-base-url",
11395
+ default=os.getenv("ONEC_MCP_SCREENSHOT_URL_BASE", ""),
11396
+ help="public base URL used in temporary StreamableHTTP screenshot links; useful behind a reverse proxy",
11397
+ )
11332
11398
  parser.add_argument(
11333
11399
  "--http-token",
11334
11400
  default=os.getenv("ONEC_MCP_HTTP_TOKEN", ""),
File without changes
File without changes
File without changes
File without changes
File without changes