answer42 0.4.97__tar.gz → 0.4.99__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.
- {answer42-0.4.97 → answer42-0.4.99}/PKG-INFO +24 -1
- {answer42-0.4.97 → answer42-0.4.99}/README.md +23 -0
- {answer42-0.4.97 → answer42-0.4.99}/pyproject.toml +1 -1
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/runtime.py +47 -4
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/server.py +71 -8
- {answer42-0.4.97 → answer42-0.4.99}/.gitignore +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/LICENSE +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/credentials.example.json +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/docs/agent-installation.md +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/docs/architecture.md +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/docs/assets/answer42-logo.png +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/docs/assets/platform42-logo.svg +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/docs/installation.md +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/scripts/build_cf.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/scripts/build_pages.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/scripts/e2e_http_smoke.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/scripts/e2e_stable.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/scripts/openclaw_mcp_autoreload.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/scripts/rag_cli.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/Configuration.xml +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl" +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/src/client_cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/client_cf/Configuration.xml +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/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
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/__init__.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/__init__.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/skills/answer42-universal-report/SKILL.md +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/auth.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/bridge.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/credentials.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/os_support.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/platform.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/protocol.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/__init__.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/detect.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/dump.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/model.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/parsers.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/service.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/store.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/summary_backend.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/rag/summary_vector.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/recorder.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/release_helper.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/skill_installer.py +0 -0
- {answer42-0.4.97 → answer42-0.4.99}/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.
|
|
3
|
+
Version: 0.4.99
|
|
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` использует живую X11-сессию, если процесс Answer42 унаследовал непустой `DISPLAY`: test manager и test client запускаются на этом дисплее, поэтому их окна и скриншоты видны на desktop.
|
|
168
|
+
|
|
169
|
+
Если `DISPLAY` не задан (headless service, SSH без X11 forwarding и т. п.), Answer42 поднимает два собственных изолированных Xvfb-дисплея: один для test manager и второй для test client. В этом режиме пакет `xvfb` обязателен:
|
|
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
|
+
Если не задан ни доступный `DISPLAY`, ни Xvfb, `start_session` завершается до создания ресурсов с понятной подсказкой по установке. Чтобы desktop-сервис видел живой X11, его launcher/service должен передать корректный `DISPLAY` и права доступа к X server (обычно через `XAUTHORITY`).
|
|
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` использует живую X11-сессию, если процесс Answer42 унаследовал непустой `DISPLAY`: test manager и test client запускаются на этом дисплее, поэтому их окна и скриншоты видны на desktop.
|
|
134
|
+
|
|
135
|
+
Если `DISPLAY` не задан (headless service, SSH без X11 forwarding и т. п.), Answer42 поднимает два собственных изолированных Xvfb-дисплея: один для test manager и второй для test client. В этом режиме пакет `xvfb` обязателен:
|
|
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
|
+
Если не задан ни доступный `DISPLAY`, ни Xvfb, `start_session` завершается до создания ресурсов с понятной подсказкой по установке. Чтобы desktop-сервис видел живой X11, его launcher/service должен передать корректный `DISPLAY` и права доступа к X server (обычно через `XAUTHORITY`).
|
|
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.
|
|
3
|
+
version = "0.4.99"
|
|
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"
|
|
Binary file
|
|
Binary file
|
|
@@ -118,13 +118,56 @@ 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
|
|
122
|
-
"""
|
|
121
|
+
def inherited_x11_display() -> str:
|
|
122
|
+
"""Return the caller's live X11 display, if one was intentionally inherited."""
|
|
123
|
+
if os_support.IS_WINDOWS:
|
|
124
|
+
return ""
|
|
125
|
+
return os.getenv("DISPLAY", "").strip()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def require_xvfb() -> str:
|
|
129
|
+
"""Return the Xvfb executable or raise an actionable headless prerequisite error.
|
|
130
|
+
|
|
131
|
+
Xvfb is needed only when Answer42 did not inherit a live X11 ``DISPLAY``.
|
|
132
|
+
"""
|
|
123
133
|
if os_support.IS_WINDOWS:
|
|
124
134
|
raise RuntimeError("Xvfb is not available on Windows")
|
|
125
135
|
xvfb_bin = shutil.which("Xvfb") or "/usr/bin/Xvfb"
|
|
126
|
-
if not Path(xvfb_bin).
|
|
127
|
-
raise
|
|
136
|
+
if not Path(xvfb_bin).is_file():
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
"Xvfb is required to start an Answer42 session on Linux when DISPLAY is not set. "
|
|
139
|
+
"Set DISPLAY to an accessible live X11 session, or install Xvfb for headless operation. "
|
|
140
|
+
"Install the 'xvfb' package (Debian/Ubuntu: sudo apt install xvfb; Fedora/RHEL: sudo dnf install xorg-x11-server-Xvfb) "
|
|
141
|
+
"and retry start_session."
|
|
142
|
+
)
|
|
143
|
+
return xvfb_bin
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def session_displays(display_offset: int) -> tuple[str, str, bool]:
|
|
147
|
+
"""Choose inherited desktop display or two Answer42-owned Xvfb displays.
|
|
148
|
+
|
|
149
|
+
Returns ``(manager_display, client_display, starts_xvfb)``. A live X11
|
|
150
|
+
display is deliberately shared by manager and client: it is the caller's
|
|
151
|
+
explicit request to run visibly on that desktop. Otherwise the historical
|
|
152
|
+
isolated two-Xvfb setup is retained for headless operation.
|
|
153
|
+
"""
|
|
154
|
+
desktop_display = inherited_x11_display()
|
|
155
|
+
if desktop_display:
|
|
156
|
+
return desktop_display, desktop_display, False
|
|
157
|
+
if os_support.IS_WINDOWS:
|
|
158
|
+
return "", "", False
|
|
159
|
+
require_xvfb()
|
|
160
|
+
manager_display = find_free_display(DEFAULT_DISPLAY_START + display_offset)
|
|
161
|
+
# The first X socket does not exist until start_xvfb() is called, so derive
|
|
162
|
+
# the second search position from the selected number instead of asking the
|
|
163
|
+
# same free-display scan twice.
|
|
164
|
+
client_display = find_free_display(int(manager_display.removeprefix(":")) + 1)
|
|
165
|
+
return manager_display, client_display, True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def start_xvfb(display: str, resolution: str = DEFAULT_DISPLAY_RES) -> subprocess.Popen:
|
|
169
|
+
"""Start Xvfb for the given display and return the Popen handle."""
|
|
170
|
+
xvfb_bin = require_xvfb()
|
|
128
171
|
proc = subprocess.Popen(
|
|
129
172
|
[xvfb_bin, display, "-screen", "0", resolution, "-ac", "+extension", "GLX", "+render", "-noreset"],
|
|
130
173
|
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,11 +5215,18 @@ async def start_session(
|
|
|
5210
5215
|
if sid in _SESSIONS:
|
|
5211
5216
|
raise ValueError(f"Session {sid!r} already exists.")
|
|
5212
5217
|
|
|
5218
|
+
# A caller that inherited DISPLAY explicitly requests visible automation
|
|
5219
|
+
# on that live X11 session. In a headless process, retain two private Xvfb
|
|
5220
|
+
# displays and validate the prerequisite before allocating session state.
|
|
5221
|
+
inherited_display = runtime.inherited_x11_display()
|
|
5222
|
+
if not os_support.IS_WINDOWS and not inherited_display:
|
|
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)
|
|
5216
5228
|
display_offset = port_offset // _PORT_BAND_SIZE
|
|
5217
|
-
display
|
|
5229
|
+
display, client_display, starts_xvfb = runtime.session_displays(display_offset)
|
|
5218
5230
|
ws_port = _find_session_port(runtime.DEFAULT_WS_PORT, port_offset, sub_offset=0)
|
|
5219
5231
|
ibsrv_port = _find_session_port(runtime.DEFAULT_IBSRV_HTTP_PORT, port_offset, sub_offset=0)
|
|
5220
5232
|
ibsrv_direct_regport = _find_session_port(runtime.DEFAULT_IBSRV_DIRECT_PORT, port_offset, sub_offset=0)
|
|
@@ -5244,13 +5256,10 @@ async def start_session(
|
|
|
5244
5256
|
bound_snapshot = _snapshot_for_base_url(base_url)
|
|
5245
5257
|
if bound_snapshot:
|
|
5246
5258
|
sess.rag_snapshot = bound_snapshot
|
|
5247
|
-
|
|
5259
|
+
sess.client_display = client_display
|
|
5260
|
+
if starts_xvfb:
|
|
5248
5261
|
sess.xvfb_process = runtime.start_xvfb(display)
|
|
5249
5262
|
sess.xvfb_pid = sess.xvfb_process.pid
|
|
5250
|
-
# Separate display for the test client so screenshots capture only the client window
|
|
5251
|
-
client_display = "" if os_support.IS_WINDOWS else runtime.find_free_display(runtime.DEFAULT_DISPLAY_START + display_offset + 1)
|
|
5252
|
-
sess.client_display = client_display
|
|
5253
|
-
if client_display:
|
|
5254
5263
|
sess.client_xvfb_process = runtime.start_xvfb(client_display)
|
|
5255
5264
|
sess.client_xvfb_pid = sess.client_xvfb_process.pid
|
|
5256
5265
|
await sess.bridge.start()
|
|
@@ -10972,6 +10981,42 @@ async def _dev_eval(
|
|
|
10972
10981
|
return await _bridge_call_recorded(session_id, "dev_eval", {"code": code})
|
|
10973
10982
|
|
|
10974
10983
|
|
|
10984
|
+
def _prune_expired_screenshot_urls(now: float | None = None) -> None:
|
|
10985
|
+
now = time.time() if now is None else now
|
|
10986
|
+
for token, (_, expires_at) in list(_SCREENSHOT_URLS.items()):
|
|
10987
|
+
if expires_at <= now:
|
|
10988
|
+
_SCREENSHOT_URLS.pop(token, None)
|
|
10989
|
+
|
|
10990
|
+
|
|
10991
|
+
@mcp.custom_route("/_answer42/screenshots/{token}", methods=["GET"], include_in_schema=False)
|
|
10992
|
+
async def _download_screenshot(request: Any) -> Any:
|
|
10993
|
+
"""Serve a single-use opaque StreamableHTTP screenshot link."""
|
|
10994
|
+
token = str(request.path_params.get("token") or "")
|
|
10995
|
+
_prune_expired_screenshot_urls()
|
|
10996
|
+
entry = _SCREENSHOT_URLS.pop(token, None)
|
|
10997
|
+
if entry is None:
|
|
10998
|
+
return PlainTextResponse("Screenshot link is expired or unavailable.", status_code=404)
|
|
10999
|
+
image_path, expires_at = entry
|
|
11000
|
+
if expires_at <= time.time() or not image_path.is_file():
|
|
11001
|
+
return PlainTextResponse("Screenshot link is expired or unavailable.", status_code=404)
|
|
11002
|
+
return FileResponse(image_path, media_type="image/png", filename=image_path.name)
|
|
11003
|
+
|
|
11004
|
+
|
|
11005
|
+
def _register_screenshot_url(path: Path) -> dict[str, Any] | None:
|
|
11006
|
+
"""Expose an opaque, time-limited screenshot URL in StreamableHTTP mode."""
|
|
11007
|
+
if not _SCREENSHOT_URL_BASE:
|
|
11008
|
+
return None
|
|
11009
|
+
_prune_expired_screenshot_urls()
|
|
11010
|
+
token = uuid.uuid4().hex + uuid.uuid4().hex
|
|
11011
|
+
expires_at = time.time() + SCREENSHOT_URL_TTL_SECONDS
|
|
11012
|
+
_SCREENSHOT_URLS[token] = (path.resolve(), expires_at)
|
|
11013
|
+
return {
|
|
11014
|
+
"url": f"{_SCREENSHOT_URL_BASE}/_answer42/screenshots/{token}",
|
|
11015
|
+
"url_expires_at": datetime.fromtimestamp(expires_at, UTC).isoformat(),
|
|
11016
|
+
"url_ttl_seconds": SCREENSHOT_URL_TTL_SECONDS,
|
|
11017
|
+
}
|
|
11018
|
+
|
|
11019
|
+
|
|
10975
11020
|
@mcp.tool()
|
|
10976
11021
|
def screenshot(
|
|
10977
11022
|
path: Annotated[str, "Output image path"] = "build/screenshots/screenshot.png",
|
|
@@ -10982,6 +11027,9 @@ def screenshot(
|
|
|
10982
11027
|
) -> dict[str, Any]:
|
|
10983
11028
|
"""Capture a screenshot from the MCP host. Defaults to the active visible 1C window.
|
|
10984
11029
|
|
|
11030
|
+
The response always contains the saved path, never image base64. In
|
|
11031
|
+
StreamableHTTP mode it additionally contains an opaque download URL that
|
|
11032
|
+
expires after one hour by default (``ONEC_MCP_SCREENSHOT_URL_TTL_SECONDS``).
|
|
10985
11033
|
Screenshots are for user evidence and UI diagnostics only. Do not use them
|
|
10986
11034
|
to analyze report/table/tabular-document/list data; use structured tools
|
|
10987
11035
|
such as tabular_document_text, tabular_documents, table_rows,
|
|
@@ -11040,7 +11088,8 @@ def screenshot(
|
|
|
11040
11088
|
) from exc
|
|
11041
11089
|
|
|
11042
11090
|
data = output_path.read_bytes()
|
|
11043
|
-
result = {"path": str(output_path), "bytes": len(data)
|
|
11091
|
+
result: dict[str, Any] = {"path": str(output_path), "bytes": len(data)}
|
|
11092
|
+
result.update(_register_screenshot_url(output_path) or {})
|
|
11044
11093
|
if window:
|
|
11045
11094
|
result["window_geometry"] = geometry
|
|
11046
11095
|
if fallback:
|
|
@@ -11103,6 +11152,13 @@ async def _run_http(args: argparse.Namespace) -> None:
|
|
|
11103
11152
|
stateless_http=args.http_stateless,
|
|
11104
11153
|
host=args.http_host,
|
|
11105
11154
|
)
|
|
11155
|
+
|
|
11156
|
+
# Image data is not placed into MCP tool results. The custom route is
|
|
11157
|
+
# registered at module load because the MCP SDK builds the Starlette app
|
|
11158
|
+
# from its custom-route list. The opaque token makes the URL unguessable;
|
|
11159
|
+
# the short lifetime and single use bound access.
|
|
11160
|
+
global _SCREENSHOT_URL_BASE
|
|
11161
|
+
_SCREENSHOT_URL_BASE = (args.http_public_base_url or f"http://{args.http_host}:{args.http_port}").rstrip("/")
|
|
11106
11162
|
config_uvicorn = uvicorn.Config(
|
|
11107
11163
|
app,
|
|
11108
11164
|
host=args.http_host,
|
|
@@ -11114,6 +11170,8 @@ async def _run_http(args: argparse.Namespace) -> None:
|
|
|
11114
11170
|
try:
|
|
11115
11171
|
await server.serve()
|
|
11116
11172
|
finally:
|
|
11173
|
+
_SCREENSHOT_URLS.clear()
|
|
11174
|
+
_SCREENSHOT_URL_BASE = ""
|
|
11117
11175
|
if tools_changed_task is not None:
|
|
11118
11176
|
tools_changed_task.cancel()
|
|
11119
11177
|
with suppress(asyncio.CancelledError):
|
|
@@ -11329,6 +11387,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
11329
11387
|
default=int(os.getenv("ONEC_MCP_HTTP_PORT", "8080")),
|
|
11330
11388
|
help="port to bind the StreamableHTTP server to",
|
|
11331
11389
|
)
|
|
11390
|
+
parser.add_argument(
|
|
11391
|
+
"--http-public-base-url",
|
|
11392
|
+
default=os.getenv("ONEC_MCP_SCREENSHOT_URL_BASE", ""),
|
|
11393
|
+
help="public base URL used in temporary StreamableHTTP screenshot links; useful behind a reverse proxy",
|
|
11394
|
+
)
|
|
11332
11395
|
parser.add_argument(
|
|
11333
11396
|
"--http-token",
|
|
11334
11397
|
default=os.getenv("ONEC_MCP_HTTP_TOKEN", ""),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{answer42-0.4.97 → answer42-0.4.99}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{answer42-0.4.97 → answer42-0.4.99}/src/mcp_1c/assets/skills/answer42-universal-report/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|