answer42 0.3.3__tar.gz → 0.3.5__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 (59) hide show
  1. {answer42-0.3.3 → answer42-0.3.5}/PKG-INFO +1 -1
  2. {answer42-0.3.3 → answer42-0.3.5}/pyproject.toml +1 -1
  3. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  4. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  5. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/parsers.py +52 -8
  6. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/service.py +59 -0
  7. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/server.py +108 -2
  8. {answer42-0.3.3 → answer42-0.3.5}/.gitignore +0 -0
  9. {answer42-0.3.3 → answer42-0.3.5}/LICENSE +0 -0
  10. {answer42-0.3.3 → answer42-0.3.5}/README.md +0 -0
  11. {answer42-0.3.3 → answer42-0.3.5}/credentials.example.json +0 -0
  12. {answer42-0.3.3 → answer42-0.3.5}/docs/agent-installation.md +0 -0
  13. {answer42-0.3.3 → answer42-0.3.5}/docs/architecture.md +0 -0
  14. {answer42-0.3.3 → answer42-0.3.5}/docs/assets/answer42-logo.png +0 -0
  15. {answer42-0.3.3 → answer42-0.3.5}/docs/assets/platform42-logo.svg +0 -0
  16. {answer42-0.3.3 → answer42-0.3.5}/docs/installation.md +0 -0
  17. {answer42-0.3.3 → answer42-0.3.5}/scripts/build_cf.py +0 -0
  18. {answer42-0.3.3 → answer42-0.3.5}/scripts/build_pages.py +0 -0
  19. {answer42-0.3.3 → answer42-0.3.5}/scripts/e2e_stable.py +0 -0
  20. {answer42-0.3.3 → answer42-0.3.5}/scripts/openclaw_mcp_autoreload.py +0 -0
  21. {answer42-0.3.3 → answer42-0.3.5}/scripts/rag_cli.py +0 -0
  22. {answer42-0.3.3 → answer42-0.3.5}/src/cf/ConfigDumpInfo.xml +0 -0
  23. {answer42-0.3.3 → answer42-0.3.5}/src/cf/Configuration.xml +0 -0
  24. {answer42-0.3.3 → answer42-0.3.5}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  25. {answer42-0.3.3 → answer42-0.3.5}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl" +0 -0
  26. {answer42-0.3.3 → answer42-0.3.5}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
  27. {answer42-0.3.3 → answer42-0.3.5}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
  28. {answer42-0.3.3 → answer42-0.3.5}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  29. {answer42-0.3.3 → answer42-0.3.5}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  30. {answer42-0.3.3 → answer42-0.3.5}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  31. {answer42-0.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/src/client_cf/ConfigDumpInfo.xml +0 -0
  34. {answer42-0.3.3 → answer42-0.3.5}/src/client_cf/Configuration.xml +0 -0
  35. {answer42-0.3.3 → answer42-0.3.5}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  36. {answer42-0.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/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.3.3 → answer42-0.3.5}/src/mcp_1c/__init__.py +0 -0
  42. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/assets/__init__.py +0 -0
  43. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
  44. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
  45. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/bridge.py +0 -0
  46. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/credentials.py +0 -0
  47. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/os_support.py +0 -0
  48. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/platform.py +0 -0
  49. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/protocol.py +0 -0
  50. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/__init__.py +0 -0
  51. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/detect.py +0 -0
  52. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/dump.py +0 -0
  53. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/model.py +0 -0
  54. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/rag/store.py +0 -0
  55. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/recorder.py +0 -0
  56. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/release_helper.py +0 -0
  57. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/runtime.py +0 -0
  58. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/skill_installer.py +0 -0
  59. {answer42-0.3.3 → answer42-0.3.5}/src/mcp_1c/window_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: answer42
3
- Version: 0.3.3
3
+ Version: 0.3.5
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "answer42"
3
- version = "0.3.3"
3
+ version = "0.3.5"
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"
@@ -21,6 +21,8 @@ from .model import (
21
21
  )
22
22
 
23
23
  KIND_DIRS = {
24
+ "Constants": "Константа",
25
+ "CommonForms": "ОбщаяФорма",
24
26
  "Catalogs": "Справочник",
25
27
  "Documents": "Документ",
26
28
  "DataProcessors": "Обработка",
@@ -33,6 +35,8 @@ KIND_DIRS = {
33
35
  "CalculationRegisters": "РегистрРасчета",
34
36
  }
35
37
 
38
+ FORM_FILE_NAMES = {"Form.form", "Form.xml"}
39
+
36
40
 
37
41
  def _strip_ns(tag: str) -> str:
38
42
  return tag.rsplit("}", 1)[-1]
@@ -199,7 +203,10 @@ def parse_edt_source(source: SourceInfo) -> ParsedSource:
199
203
  parsed.objects.append(MetadataObject(full, ru_kind, obj_name, synonym, str(mdo), source.format, hierarchical, [o for o in owners if o]))
200
204
  if root is not None:
201
205
  _extract_metadata_children(parsed, root, full, mdo)
202
- _extract_forms(parsed, mdo.parent, full)
206
+ if ru_kind == "ОбщаяФорма":
207
+ _extract_common_form(parsed, mdo.parent, full)
208
+ else:
209
+ _extract_forms(parsed, mdo.parent, full)
203
210
  _extract_data_composition_schemas(parsed, mdo.parent, full)
204
211
  _extract_help_pages(parsed, mdo.parent, full)
205
212
  return parsed
@@ -231,12 +238,26 @@ def parse_designer_xml_source(source: SourceInfo) -> ParsedSource:
231
238
  if root is not None:
232
239
  _extract_metadata_children(parsed, root, full, xml)
233
240
  object_dir = xml.parent if xml.parent != base else base / xml.stem
234
- _extract_forms(parsed, object_dir, full)
241
+ if ru_kind == "ОбщаяФорма":
242
+ _extract_common_form(parsed, object_dir, full)
243
+ else:
244
+ _extract_forms(parsed, object_dir, full)
235
245
  _extract_data_composition_schemas(parsed, object_dir, full)
236
246
  _extract_help_pages(parsed, object_dir, full)
237
247
  return parsed
238
248
 
239
249
 
250
+ def _extract_common_form(parsed: ParsedSource, object_dir: Path, full: str) -> None:
251
+ form_xml = _find_form_file(object_dir)
252
+ parsed.forms.append(Form(full, "Форма", "CommonForm", str(form_xml) if form_xml.exists() else str(object_dir)))
253
+ if not form_xml.exists():
254
+ return
255
+ root = _xml_root(form_xml)
256
+ if root is not None:
257
+ _extract_form_elements(parsed, root, full, "Форма", form_xml)
258
+ _extract_embedded_data_composition_fields(parsed, root, full, "Форма", form_xml)
259
+
260
+
240
261
  def _extract_metadata_children(parsed: ParsedSource, root: ET.Element, full: str, source_path: Path) -> None:
241
262
  # Heuristic parser covering both EDT and designer XML structures.
242
263
  # Keep object attributes and table-part columns separate: XML dumps often
@@ -273,9 +294,7 @@ def _extract_forms(parsed: ParsedSource, object_dir: Path, full: str) -> None:
273
294
  return
274
295
  for form_dir in sorted([p for p in forms_dir.iterdir() if p.is_dir()]):
275
296
  form_name = form_dir.name
276
- form_xml = form_dir / "Ext" / "Form.xml"
277
- if not form_xml.exists():
278
- form_xml = form_dir / "Form.xml"
297
+ form_xml = _find_form_file(form_dir)
279
298
  parsed.forms.append(Form(full, form_name, None, str(form_xml) if form_xml.exists() else str(form_dir)))
280
299
  if form_xml.exists():
281
300
  root = _xml_root(form_xml)
@@ -284,23 +303,34 @@ def _extract_forms(parsed: ParsedSource, object_dir: Path, full: str) -> None:
284
303
  _extract_embedded_data_composition_fields(parsed, root, full, form_name, form_xml)
285
304
 
286
305
 
306
+ def _find_form_file(form_dir: Path) -> Path:
307
+ for candidate_dir in (form_dir / "Ext", form_dir):
308
+ for name in FORM_FILE_NAMES:
309
+ form_file = candidate_dir / name
310
+ if form_file.exists():
311
+ return form_file
312
+ return form_dir / "Ext" / "Form.xml"
313
+
314
+
287
315
  def _extract_form_elements(parsed: ParsedSource, root: ET.Element, full: str, form_name: str, source_path: Path) -> None:
288
316
  for elem in root.iter():
289
317
  tag = _strip_ns(elem.tag)
290
318
  low = tag.lower()
291
- if low not in {"element", "item", "items", "button", "command", "field", "table", "group"}:
319
+ if low not in {"element", "item", "items", "button", "command", "field", "table", "group", "extendedtooltip", "contextmenu"}:
292
320
  continue
293
321
  name = elem.attrib.get("name") or elem.attrib.get("Name") or _first_text(elem, ["name", "Name"])
294
322
  if not name:
295
323
  continue
324
+ title = elem.attrib.get("title") or elem.attrib.get("Title") or _first_text(elem, ["title", "Title", "caption", "Caption"])
325
+ data_path = _form_data_path(elem)
296
326
  parsed.form_elements.append(
297
327
  FormElement(
298
328
  full,
299
329
  form_name,
300
330
  name,
301
331
  tag,
302
- elem.attrib.get("title") or elem.attrib.get("Title") or _first_text(elem, ["title", "Title", "caption", "Caption"]),
303
- elem.attrib.get("dataPath") or elem.attrib.get("path") or _first_text(elem, ["dataPath", "path", "DataPath"]),
332
+ title,
333
+ data_path,
304
334
  elem.attrib.get("commandName") or _first_text(elem, ["commandName", "CommandName"]),
305
335
  None,
306
336
  str(source_path),
@@ -308,6 +338,20 @@ def _extract_form_elements(parsed: ParsedSource, root: ET.Element, full: str, fo
308
338
  )
309
339
 
310
340
 
341
+ def _form_data_path(elem: ET.Element) -> str | None:
342
+ direct = elem.attrib.get("dataPath") or elem.attrib.get("path") or _direct_text(elem, ["dataPath", "path", "DataPath"])
343
+ if direct:
344
+ return direct
345
+ for child in list(elem):
346
+ if _strip_ns(child.tag).lower() == "datapath":
347
+ segments = [_text(segment) for segment in child.iter() if _strip_ns(segment.tag).lower() in {"segments", "segment"}]
348
+ values = [value for value in segments if value]
349
+ if values:
350
+ return ".".join(values)
351
+ return _text(child)
352
+ return None
353
+
354
+
311
355
  def _extract_embedded_data_composition_fields(parsed: ParsedSource, root: ET.Element, full: str, form_name: str, source_path: Path) -> None:
312
356
  dataset_name: str | None = None
313
357
  for elem in root.iter():
@@ -5,6 +5,7 @@ import json
5
5
  import math
6
6
  import os
7
7
  import re
8
+ import shutil
8
9
  import subprocess
9
10
  from pathlib import Path
10
11
  from typing import Any
@@ -56,6 +57,45 @@ class RagService:
56
57
  with self.store.connect() as con:
57
58
  return [dict(r) for r in con.execute("SELECT * FROM sources ORDER BY id")]
58
59
 
60
+ def cleanup_generated_source_files_under(self, root: str | Path, *, remove_files: bool = True) -> dict[str, Any]:
61
+ """Delete generated dump files under *root* while preserving indexed RAG rows.
62
+
63
+ The SQLite RAG index is self-contained after ``build_index`` succeeds, so
64
+ dumped Designer XML source files can be removed to save disk space. The
65
+ source registry rows are intentionally kept: they document which source
66
+ produced the indexed metadata and keep existing snapshots/index rows
67
+ intact via foreign keys.
68
+ """
69
+ root_path = Path(root).expanduser().resolve()
70
+ referenced_sources: list[dict[str, Any]] = []
71
+ skipped: list[dict[str, Any]] = []
72
+ with self.store.connect() as con:
73
+ rows = [dict(r) for r in con.execute("SELECT * FROM sources ORDER BY id")]
74
+ for row in rows:
75
+ source_path = Path(str(row.get("path") or "")).expanduser()
76
+ try:
77
+ resolved_source = source_path.resolve()
78
+ resolved_source.relative_to(root_path)
79
+ except (OSError, ValueError):
80
+ continue
81
+ branch = str(row.get("branch") or "")
82
+ commit_hash = str(row.get("commit_hash") or "")
83
+ if branch or commit_hash:
84
+ skipped.append(row | {"reason": "source looks like a VCS checkout"})
85
+ continue
86
+ referenced_sources.append(row)
87
+
88
+ removed_paths: list[str] = []
89
+ if remove_files and root_path.exists() and referenced_sources:
90
+ shutil.rmtree(root_path)
91
+ removed_paths.append(str(root_path))
92
+ return {
93
+ "root": str(root_path),
94
+ "referenced_sources_count": len(referenced_sources),
95
+ "skipped_count": len(skipped),
96
+ "removed_paths": removed_paths,
97
+ }
98
+
59
99
  def add_sources_from_directory(self, root: str, prefix: str = "", recursive: bool = True, max_depth: int = 4, watch: bool = False) -> dict[str, Any]:
60
100
  discovered = self.discover_sources(root, recursive=recursive, max_depth=max_depth)
61
101
  registered: list[dict[str, Any]] = []
@@ -266,6 +306,12 @@ class RagService:
266
306
  continue
267
307
  con.execute("INSERT INTO attributes(object_id,name,type_name,synonym,required,source_path,role,raw_json) VALUES(?,?,?,?,?,?,?,?)", (oid, attr.name, attr.type_name, attr.synonym, attr.required, attr.source_path, attr.role, json_dumps(attr.raw)))
268
308
  counts["attributes"] += 1
309
+ text_parts = [attr.object_full_name, attr.role, attr.name, search_text_variants(attr.name, attr.synonym), attr.synonym or "", attr.type_name or ""]
310
+ chunk_text = " ".join(part for part in text_parts if part)
311
+ cid = _insert_chunk(con, snapshot_id, oid, "attribute", chunk_text, attr.source_path, {"source": src["name"], "role": attr.role})
312
+ con.execute("INSERT INTO search_fts(text, object_full_name, kind, source_path, chunk_id) VALUES(?,?,?,?,?)", (chunk_text, attr.object_full_name, "Реквизит", attr.source_path, cid))
313
+ _insert_embedding(con, cid, chunk_text)
314
+ counts["chunks"] += 1
269
315
  for tp in parsed.table_parts:
270
316
  oid = object_ids.get(tp.object_full_name)
271
317
  if not oid:
@@ -284,12 +330,25 @@ class RagService:
284
330
  cur = con.execute("INSERT INTO forms(object_id,name,kind,source_path,raw_json) VALUES(?,?,?,?,?)", (oid, form.name, form.kind, form.source_path, json_dumps(form.raw)))
285
331
  form_ids[(form.object_full_name, form.name)] = cur.lastrowid
286
332
  counts["forms"] += 1
333
+ text_parts = [form.object_full_name, "форма", form.name, search_text_variants(form.name), form.kind or ""]
334
+ chunk_text = " ".join(part for part in text_parts if part)
335
+ cid = _insert_chunk(con, snapshot_id, oid, "form", chunk_text, form.source_path, {"source": src["name"], "form": form.name})
336
+ con.execute("INSERT INTO search_fts(text, object_full_name, kind, source_path, chunk_id) VALUES(?,?,?,?,?)", (chunk_text, form.object_full_name, "Форма", form.source_path, cid))
337
+ _insert_embedding(con, cid, chunk_text)
338
+ counts["chunks"] += 1
287
339
  for elem in parsed.form_elements:
288
340
  fid = form_ids.get((elem.object_full_name, elem.form_name))
289
341
  if not fid:
290
342
  continue
291
343
  con.execute("INSERT INTO form_elements(form_id,name,element_type,title,data_path,command_name,parent_name,source_path,raw_json) VALUES(?,?,?,?,?,?,?,?,?)", (fid, elem.name, elem.element_type, elem.title, elem.data_path, elem.command_name, elem.parent_name, elem.source_path, json_dumps(elem.raw)))
292
344
  counts["form_elements"] += 1
345
+ oid = object_ids.get(elem.object_full_name)
346
+ text_parts = [elem.object_full_name, "элемент формы", elem.form_name, elem.name, search_text_variants(elem.name, elem.title, elem.data_path), elem.title or "", elem.data_path or "", elem.command_name or "", elem.element_type or ""]
347
+ chunk_text = " ".join(part for part in text_parts if part)
348
+ cid = _insert_chunk(con, snapshot_id, oid, "form_element", chunk_text, elem.source_path, {"source": src["name"], "form": elem.form_name, "element": elem.name})
349
+ con.execute("INSERT INTO search_fts(text, object_full_name, kind, source_path, chunk_id) VALUES(?,?,?,?,?)", (chunk_text, elem.object_full_name, "ЭлементФормы", elem.source_path, cid))
350
+ _insert_embedding(con, cid, chunk_text)
351
+ counts["chunks"] += 1
293
352
  for dcs_field in parsed.data_composition_fields:
294
353
  oid = object_ids.get(dcs_field.object_full_name)
295
354
  if not oid:
@@ -63,6 +63,62 @@ _SHARED_FILE_SERVERS_FILE = os_support.runtime_path("ONEC_MCP_SHARED_FILE_SERVER
63
63
  _SHARED_FILE_SERVERS_LOCK = os_support.runtime_path("ONEC_MCP_SHARED_FILE_SERVERS_LOCK", "shared-file-servers.lock")
64
64
  _METADATA_OBJECTS_CACHE: dict[str, dict[str, Any]] = {}
65
65
 
66
+ TOOL_PROFILE_GROUPS: dict[str, set[str]] = {
67
+ "sessions": {
68
+ "sessions_list", "session_status", "start_session", "stop_session", "export_eventlog",
69
+ "credentials_check", "credentials_save", "credentials_remove", "credentials_list", "credentials_titles",
70
+ },
71
+ "windows": {"active_window", "windows_list", "activate_window", "goto_start_page", "goto_previous_window", "goto_next_window"},
72
+ "forms": {
73
+ "wait_object", "wait_form", "wait_window", "user_messages", "close_user_messages_panel", "current_error_info",
74
+ "set_file_dialog_result", "clear_file_dialog_result", "field_info", "field_value_text", "field_tooltip",
75
+ "field_dropdown_items", "form_next_element", "form_previous_element", "form_state", "form_default_button",
76
+ "wait_form_closed", "object_command_panel", "object_context_menu", "window_command_interface",
77
+ "command_interface_sections", "command_interface_commands", "command_interface_search", "group_expand", "group_collapse",
78
+ "group_current_page", "group_activate_page", "group_state", "decoration_links", "decoration_click_link", "decoration_click",
79
+ "ui_tree", "command_bar", "click_button", "find_object", "search_objects", "get_object", "activate_object",
80
+ "open_navigation_link", "current_element", "field_dropdown_state", "field_dropdown_close", "toggle_field",
81
+ "set_field_value", "activate_field", "choose_field_from_list", "choose_field_first_row", "choose_current_row",
82
+ "set_form_fields", "create_new_item", "save_form", "close_form", "form_open_settings",
83
+ },
84
+ "tables": {
85
+ "table_current_row", "table_selected_rows", "table_cell_text", "table_rows", "table_find_row", "table_goto_row",
86
+ "table_move_row", "table_add_row", "table_set_field", "table_set_row_values", "table_choose_field_from_list",
87
+ "table_edit_info", "table_select_all", "table_clear_selection", "table_delete_row", "table_copy_row",
88
+ "table_toggle_delete_mark", "table_begin_edit", "table_end_edit", "table_editing_state",
89
+ },
90
+ "tabular": {"tabular_documents", "tabular_document_text", "tabular_document_save", "tabular_document_state_display"},
91
+ "dynamic": {
92
+ "dynamic_list_metadata_available_fields", "dynamic_list_settings_available_fields", "dynamic_list_set_settings",
93
+ "dynamic_list_current_settings", "dynamic_list_sort_by_column", "dynamic_list_output", "dynamic_list_open_settings",
94
+ "dynamic_list_find", "dynamic_list_filter_by_current_value", "dynamic_list_clear_settings",
95
+ },
96
+ "rag": {
97
+ "rag_source_add", "rag_sources_add_from_directory", "rag_source_roots_list", "rag_available_configurations",
98
+ "rag_session_autoconfigure", "rag_sources_list", "rag_snapshot_create", "rag_snapshots_list", "rag_index_build",
99
+ "rag_lookup_object", "rag_query", "metadata_objects_from_catalog", "metadata_objects_catalog_cache_clear",
100
+ "configuration_name_version_from_subsystems", "installed_extensions_from_catalog",
101
+ },
102
+ "recording": {"recording_start", "recording_capture", "recording_stop", "screenshot"},
103
+ }
104
+ TOOL_PROFILE_CORE: set[str] = TOOL_PROFILE_GROUPS["sessions"] | {
105
+ "active_window", "windows_list", "activate_window", "goto_start_page", "goto_previous_window", "goto_next_window",
106
+ "wait_object", "wait_form", "wait_window", "user_messages", "close_user_messages_panel", "current_error_info",
107
+ "field_info", "field_value_text", "field_dropdown_items", "field_dropdown_state", "field_dropdown_close",
108
+ "toggle_field", "set_field_value", "activate_field", "choose_field_from_list", "choose_field_first_row", "choose_current_row",
109
+ "form_state", "form_default_button", "wait_form_closed", "click_button", "find_object", "search_objects", "get_object",
110
+ "activate_object", "open_navigation_link", "current_element", "table_current_row", "table_selected_rows", "table_cell_text",
111
+ "table_rows", "table_find_row", "table_goto_row", "table_move_row", "table_add_row", "table_set_field", "table_set_row_values",
112
+ "table_choose_field_from_list", "table_edit_info", "table_begin_edit", "table_end_edit", "table_editing_state",
113
+ "tabular_documents", "tabular_document_text", "tabular_document_save", "screenshot",
114
+ }
115
+ TOOL_PROFILE_ALIASES: dict[str, set[str]] = {
116
+ "core": TOOL_PROFILE_CORE,
117
+ "ui": TOOL_PROFILE_GROUPS["sessions"] | TOOL_PROFILE_GROUPS["windows"] | TOOL_PROFILE_GROUPS["forms"] | TOOL_PROFILE_GROUPS["tables"] | TOOL_PROFILE_GROUPS["tabular"] | TOOL_PROFILE_GROUPS["recording"],
118
+ "all": set(),
119
+ "full": set(),
120
+ }
121
+
66
122
 
67
123
  @contextmanager
68
124
  def _port_allocation_lock():
@@ -1880,6 +1936,7 @@ async def rag_session_autoconfigure(
1880
1936
  cfe_files: Annotated[str, "Optional JSON/comma list of .cfe files to load into temporary file IBs and dump as extensions"] = "",
1881
1937
  dump_root: Annotated[str, "Optional persistent directory for generated RAG source dumps"] = "",
1882
1938
  cleanup_tmp_infobases: Annotated[bool, "Remove temporary file infobases created while loading CF/CFE files"] = True,
1939
+ cleanup_generated_sources: Annotated[bool, "Remove generated XML source dumps after a successful RAG index build"] = True,
1883
1940
  *,
1884
1941
  session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
1885
1942
  ) -> dict[str, Any]:
@@ -1898,8 +1955,10 @@ async def rag_session_autoconfigure(
1898
1955
  (for example multitenant/separated-data infobases), pass `dump_username` and
1899
1956
  `dump_password`; otherwise the current session credentials are reused.
1900
1957
  Temporary infobases created from CF/CFE files are deleted after dumping by
1901
- default; generated XML source directories are kept under `dump_root` (or the
1902
- Answer42 temp rag-dumps directory) because the RAG index references them.
1958
+ default. Generated XML source directories are removed after a successful
1959
+ index build by default, because the SQLite RAG index is self-contained; pass
1960
+ `cleanup_generated_sources=false` when the dumped XML source tree should be
1961
+ kept under `dump_root` for inspection or reuse.
1903
1962
  """
1904
1963
  params = {
1905
1964
  "snapshot_name": snapshot_name,
@@ -1916,6 +1975,7 @@ async def rag_session_autoconfigure(
1916
1975
  "cfe_files": cfe_files,
1917
1976
  "dump_root": dump_root,
1918
1977
  "cleanup_tmp_infobases": cleanup_tmp_infobases,
1978
+ "cleanup_generated_sources": cleanup_generated_sources,
1919
1979
  }
1920
1980
  disabled = _rag_require_enabled(session_id, "rag_session_autoconfigure", params)
1921
1981
  if disabled is not None:
@@ -2145,6 +2205,9 @@ async def rag_session_autoconfigure(
2145
2205
  _persist_sessions()
2146
2206
 
2147
2207
  index_result = _RAG.build_index(snapshot=sess.rag_snapshot) if build_index else None
2208
+ cleanup_generated_sources_result = None
2209
+ if cleanup_generated_sources and index_result is not None and dump_base_dir is not None:
2210
+ cleanup_generated_sources_result = _RAG.cleanup_generated_source_files_under(dump_base_dir)
2148
2211
  result = {
2149
2212
  "snapshot": sess.rag_snapshot,
2150
2213
  "snapshot_result": snapshot,
@@ -2156,7 +2219,9 @@ async def rag_session_autoconfigure(
2156
2219
  "extension_matches": extension_matches,
2157
2220
  "dumps": dumps,
2158
2221
  "generated_sources": generated_sources,
2222
+ "generated_sources_count": len(generated_sources),
2159
2223
  "dump_root": str(dump_base_dir) if dump_base_dir else "",
2224
+ "cleanup_generated_sources": cleanup_generated_sources_result,
2160
2225
  "available_sources_count": len(sources),
2161
2226
  "session_snapshot": sess.rag_snapshot,
2162
2227
  }
@@ -5660,6 +5725,40 @@ def _register_dev_tools(enabled: bool) -> None:
5660
5725
  )
5661
5726
 
5662
5727
 
5728
+ def _tool_names_for_profile(profile: str) -> set[str] | None:
5729
+ parts = [part.strip().lower() for part in re.split(r"[,;\s]+", profile or "") if part.strip()]
5730
+ if not parts or any(part in {"all", "full"} for part in parts):
5731
+ return None
5732
+ selected: set[str] = set()
5733
+ unknown: list[str] = []
5734
+ for part in parts:
5735
+ names = TOOL_PROFILE_ALIASES.get(part) or TOOL_PROFILE_GROUPS.get(part)
5736
+ if names is None:
5737
+ unknown.append(part)
5738
+ continue
5739
+ selected.update(names)
5740
+ if unknown:
5741
+ known = sorted({*TOOL_PROFILE_GROUPS, *TOOL_PROFILE_ALIASES})
5742
+ raise ValueError(f"unknown tool profile(s): {', '.join(unknown)}; known profiles: {', '.join(known)}")
5743
+ return selected
5744
+
5745
+
5746
+ def _apply_tool_profile(profile: str) -> dict[str, Any]:
5747
+ allowed = _tool_names_for_profile(profile)
5748
+ if allowed is None:
5749
+ return {"profile": profile or "full", "mode": "full", "removed": []}
5750
+ existing = set(getattr(mcp, "_tool_manager")._tools.keys())
5751
+ removed: list[str] = []
5752
+ for name in sorted(existing - allowed):
5753
+ try:
5754
+ mcp.remove_tool(name)
5755
+ removed.append(name)
5756
+ except Exception:
5757
+ pass
5758
+ remaining = sorted(set(getattr(mcp, "_tool_manager")._tools.keys()))
5759
+ return {"profile": profile, "mode": "filtered", "remaining_count": len(remaining), "removed_count": len(removed), "removed": removed}
5760
+
5761
+
5663
5762
  def main(argv: list[str] | None = None) -> None:
5664
5763
  global _RAG_ENABLED, _CREDENTIAL_STORE_ENABLED, _DEV_TOOLS_ENABLED
5665
5764
 
@@ -5730,6 +5829,11 @@ def main(argv: list[str] | None = None) -> None:
5730
5829
  default=os.getenv("ONEC_MCP_SESSION_SCOPE_ID") or os.getenv("ONEC_MCP_SESSION_SCOPE") or "",
5731
5830
  help="enable scoped lock/reconnect mode for the given session scope id; without this flag Answer42 uses legacy no-lock global state",
5732
5831
  )
5832
+ parser.add_argument(
5833
+ "--tool-profile",
5834
+ default=os.getenv("ONEC_MCP_TOOL_PROFILE", "full"),
5835
+ help="comma-separated tool profile groups: core, ui, sessions, windows, forms, tables, tabular, dynamic, rag, recording, full, all",
5836
+ )
5733
5837
  args = parser.parse_args(argv)
5734
5838
 
5735
5839
  _RAG_ENABLED = not args.disable_rag
@@ -5741,6 +5845,8 @@ def main(argv: list[str] | None = None) -> None:
5741
5845
  _register_startup_rag_source_dirs(args.rag_source_dir or [], args.rag_source_prefix, not args.rag_source_no_recursive, args.rag_source_max_depth)
5742
5846
  else:
5743
5847
  logging.info("Built-in Answer42 RAG is disabled")
5848
+ profile_result = _apply_tool_profile(args.tool_profile)
5849
+ logging.info("Tool profile: %s (mode=%s, remaining=%d, removed=%d)", profile_result["profile"], profile_result["mode"], profile_result.get("remaining_count", 0), profile_result.get("removed_count", 0))
5744
5850
  _start_autoreload_watcher(args.dev, args.dev_restart_timeout)
5745
5851
  if args.session_scope:
5746
5852
  _configure_session_scope(args.session_scope)
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