answer42 0.2.25__tar.gz → 0.2.26__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 (58) hide show
  1. {answer42-0.2.25 → answer42-0.2.26}/PKG-INFO +1 -1
  2. {answer42-0.2.25 → answer42-0.2.26}/pyproject.toml +1 -1
  3. {answer42-0.2.25 → answer42-0.2.26}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl +57 -19
  4. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  5. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/credentials.py +53 -11
  6. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/protocol.py +1 -1
  7. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/server.py +56 -19
  8. {answer42-0.2.25 → answer42-0.2.26}/.gitignore +0 -0
  9. {answer42-0.2.25 → answer42-0.2.26}/LICENSE +0 -0
  10. {answer42-0.2.25 → answer42-0.2.26}/README.md +0 -0
  11. {answer42-0.2.25 → answer42-0.2.26}/credentials.example.json +0 -0
  12. {answer42-0.2.25 → answer42-0.2.26}/docs/agent-installation.md +0 -0
  13. {answer42-0.2.25 → answer42-0.2.26}/docs/architecture.md +0 -0
  14. {answer42-0.2.25 → answer42-0.2.26}/docs/assets/answer42-logo.png +0 -0
  15. {answer42-0.2.25 → answer42-0.2.26}/docs/assets/platform42-logo.svg +0 -0
  16. {answer42-0.2.25 → answer42-0.2.26}/docs/installation.md +0 -0
  17. {answer42-0.2.25 → answer42-0.2.26}/scripts/build_cf.py +0 -0
  18. {answer42-0.2.25 → answer42-0.2.26}/scripts/build_pages.py +0 -0
  19. {answer42-0.2.25 → answer42-0.2.26}/scripts/e2e_stable.py +0 -0
  20. {answer42-0.2.25 → answer42-0.2.26}/scripts/openclaw_mcp_autoreload.py +0 -0
  21. {answer42-0.2.25 → answer42-0.2.26}/scripts/rag_cli.py +0 -0
  22. {answer42-0.2.25 → answer42-0.2.26}/src/cf/ConfigDumpInfo.xml +0 -0
  23. {answer42-0.2.25 → answer42-0.2.26}/src/cf/Configuration.xml +0 -0
  24. {answer42-0.2.25 → answer42-0.2.26}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  25. {answer42-0.2.25 → answer42-0.2.26}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
  26. {answer42-0.2.25 → answer42-0.2.26}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
  27. {answer42-0.2.25 → answer42-0.2.26}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  28. {answer42-0.2.25 → answer42-0.2.26}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  29. {answer42-0.2.25 → answer42-0.2.26}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  30. {answer42-0.2.25 → answer42-0.2.26}/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
  31. {answer42-0.2.25 → answer42-0.2.26}/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
  32. {answer42-0.2.25 → answer42-0.2.26}/src/client_cf/ConfigDumpInfo.xml +0 -0
  33. {answer42-0.2.25 → answer42-0.2.26}/src/client_cf/Configuration.xml +0 -0
  34. {answer42-0.2.25 → answer42-0.2.26}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  35. {answer42-0.2.25 → answer42-0.2.26}/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
  36. {answer42-0.2.25 → answer42-0.2.26}/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
  37. {answer42-0.2.25 → answer42-0.2.26}/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
  38. {answer42-0.2.25 → answer42-0.2.26}/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
  39. {answer42-0.2.25 → answer42-0.2.26}/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
  40. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/__init__.py +0 -0
  41. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  42. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/assets/__init__.py +0 -0
  43. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
  44. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
  45. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/bridge.py +0 -0
  46. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/os_support.py +0 -0
  47. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/platform.py +0 -0
  48. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/rag/__init__.py +0 -0
  49. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/rag/detect.py +0 -0
  50. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/rag/model.py +0 -0
  51. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/rag/parsers.py +0 -0
  52. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/rag/service.py +0 -0
  53. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/rag/store.py +0 -0
  54. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/recorder.py +0 -0
  55. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/release_helper.py +0 -0
  56. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/runtime.py +0 -0
  57. {answer42-0.2.25 → answer42-0.2.26}/src/mcp_1c/skill_installer.py +0 -0
  58. {answer42-0.2.25 → answer42-0.2.26}/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.2.25
3
+ Version: 0.2.26
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.2.25"
3
+ version = "0.2.26"
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"
@@ -240,7 +240,7 @@
240
240
 
241
241
  &НаКлиенте
242
242
  Функция ВерсияПротоколаМенеджераMCP()
243
- Возврат "2026.07.02.1";
243
+ Возврат "2026.07.02.2";
244
244
  КонецФункции
245
245
 
246
246
  &НаКлиенте
@@ -2950,12 +2950,12 @@
2950
2950
  Глубина = СвойствоСтруктуры(Параметры, "max_depth", 8);
2951
2951
  ВключатьТекст = СвойствоСтруктуры(Параметры, "include_text", Ложь);
2952
2952
  Результат = Новый Массив;
2953
- НайтиТабличныеДокументыРекурсивно(АктивноеОкно, Результат, ВключатьТекст, 0, Глубина);
2953
+ НайтиТабличныеДокументыРекурсивно(АктивноеОкно, Результат, ВключатьТекст, Параметры, 0, Глубина);
2954
2954
  Возврат Новый Структура("count,items", Результат.Количество(), Результат);
2955
2955
  КонецФункции
2956
2956
 
2957
2957
  &НаКлиенте
2958
- Процедура НайтиТабличныеДокументыРекурсивно(Объект, Результат, ВключатьТекст, ТекущаяГлубина, МаксимальнаяГлубина)
2958
+ Процедура НайтиТабличныеДокументыРекурсивно(Объект, Результат, ВключатьТекст, ПараметрыТекста, ТекущаяГлубина, МаксимальнаяГлубина)
2959
2959
  Если ТекущаяГлубина > МаксимальнаяГлубина Тогда
2960
2960
  Возврат;
2961
2961
  КонецЕсли;
@@ -2968,20 +2968,31 @@
2968
2968
  Элемент.Вставить("title", БезопасныйЗаголовок(Подчиненный));
2969
2969
  Элемент.Вставить("type", Строка(ТипЗнч(Подчиненный)));
2970
2970
  Элемент.Вставить("data", БезопасноеПредставлениеДанныхПоля(Подчиненный));
2971
+ РазмерыДокумента = РазмерыТабличногоДокументаИзПоля(Подчиненный);
2972
+ Если РазмерыДокумента.Свойство("error") Тогда
2973
+ Элемент.Вставить("dimensions_error", РазмерыДокумента.error);
2974
+ Иначе
2975
+ Элемент.Вставить("rows", РазмерыДокумента.rows);
2976
+ Элемент.Вставить("columns", РазмерыДокумента.columns);
2977
+ КонецЕсли;
2971
2978
  Если ВключатьТекст Тогда
2972
- ТекстДокумента = ТекстТабличногоДокументаИзПоля(Подчиненный);
2979
+ ТекстДокумента = ТекстТабличногоДокументаИзПоля(Подчиненный, ПараметрыТекста);
2973
2980
  Если ТекстДокумента.Свойство("error") Тогда
2974
2981
  Элемент.Вставить("text_error", ТекстДокумента.error);
2975
2982
  Иначе
2976
2983
  Элемент.Вставить("rows", ТекстДокумента.rows);
2977
2984
  Элемент.Вставить("columns", ТекстДокумента.columns);
2978
2985
  Элемент.Вставить("address", ТекстДокумента.address);
2979
- Элемент.Вставить("text", ТекстДокумента.text);
2986
+ Элемент.Вставить("row_start", ТекстДокумента.row_start);
2987
+ Элемент.Вставить("row_end", ТекстДокумента.row_end);
2988
+ Элемент.Вставить("column_start", ТекстДокумента.column_start);
2989
+ Элемент.Вставить("column_end", ТекстДокумента.column_end);
2990
+ Элемент.Вставить("cells", ТекстДокумента.cells);
2980
2991
  КонецЕсли;
2981
2992
  КонецЕсли;
2982
2993
  Результат.Добавить(Элемент);
2983
2994
  КонецЕсли;
2984
- НайтиТабличныеДокументыРекурсивно(Подчиненный, Результат, ВключатьТекст, ТекущаяГлубина + 1, МаксимальнаяГлубина);
2995
+ НайтиТабличныеДокументыРекурсивно(Подчиненный, Результат, ВключатьТекст, ПараметрыТекста, ТекущаяГлубина + 1, МаксимальнаяГлубина);
2985
2996
  КонецЦикла;
2986
2997
  Исключение
2987
2998
  Инфо = ИнформацияОбОшибке();
@@ -3014,29 +3025,56 @@
3014
3025
  Если Поле = Неопределено Тогда
3015
3026
  Возврат Новый Структура("found", Ложь);
3016
3027
  КонецЕсли;
3017
- ДанныеТекста = ТекстТабличногоДокументаИзПоля(Поле);
3028
+ ДанныеТекста = ТекстТабличногоДокументаИзПоля(Поле, Параметры);
3018
3029
  Если ДанныеТекста.Свойство("error") Тогда
3019
3030
  Возврат Новый Структура("found,error", Истина, ДанныеТекста.error);
3020
3031
  КонецЕсли;
3021
- Возврат Новый Структура("found,rows,columns,address,text,cells", Истина, ДанныеТекста.rows, ДанныеТекста.columns, ДанныеТекста.address, ДанныеТекста.text, ДанныеТекста.cells);
3032
+ Возврат Новый Структура("found,rows,columns,address,row_start,row_end,column_start,column_end,cells", Истина, ДанныеТекста.rows, ДанныеТекста.columns, ДанныеТекста.address, ДанныеТекста.row_start, ДанныеТекста.row_end, ДанныеТекста.column_start, ДанныеТекста.column_end, ДанныеТекста.cells);
3033
+ КонецФункции
3034
+
3035
+ &НаКлиенте
3036
+ Функция РазмерыТабличногоДокументаИзПоля(Поле)
3037
+ Попытка
3038
+ Возврат Новый Структура("rows,columns", Поле.ПолучитьРазмерОбластиДанныхДокументаПоВертикали(), Поле.ПолучитьРазмерОбластиДанныхДокументаПоГоризонтали());
3039
+ Исключение
3040
+ Возврат Новый Структура("error", ОписаниеОшибки());
3041
+ КонецПопытки;
3022
3042
  КонецФункции
3023
3043
 
3024
3044
  &НаКлиенте
3025
- Функция ТекстТабличногоДокументаИзПоля(Поле)
3045
+ Функция ТекстТабличногоДокументаИзПоля(Поле, Параметры)
3026
3046
  Попытка
3027
- КоличествоСтрок = Поле.ПолучитьРазмерОбластиДанныхДокументаПоВертикали();
3028
- КоличествоКолонок = Поле.ПолучитьРазмерОбластиДанныхДокументаПоГоризонтали();
3047
+ Размеры = РазмерыТабличногоДокументаИзПоля(Поле);
3048
+ Если Размеры.Свойство("error") Тогда
3049
+ Возврат Новый Структура("error", Размеры.error);
3050
+ КонецЕсли;
3051
+ КоличествоСтрок = Размеры.rows;
3052
+ КоличествоКолонок = Размеры.columns;
3029
3053
  Если КоличествоСтрок = 0 Или КоличествоКолонок = 0 Тогда
3030
- Возврат Новый Структура("rows,columns,address,text,cells", 0, 0, "", "", Новый Массив);
3054
+ Возврат Новый Структура("rows,columns,address,row_start,row_end,column_start,column_end,cells", 0, 0, "", 0, 0, 0, 0, Новый Массив);
3055
+ КонецЕсли;
3056
+ НачальнаяСтрока = Макс(1, СвойствоСтруктуры(Параметры, "row_start", 1));
3057
+ НачальнаяКолонка = Макс(1, СвойствоСтруктуры(Параметры, "column_start", 1));
3058
+ КонечнаяСтрока = СвойствоСтруктуры(Параметры, "row_end", 0);
3059
+ КонечнаяКолонка = СвойствоСтруктуры(Параметры, "column_end", 0);
3060
+ МаксСтрок = СвойствоСтруктуры(Параметры, "max_rows", 50);
3061
+ МаксКолонок = СвойствоСтруктуры(Параметры, "max_columns", 20);
3062
+ Если КонечнаяСтрока <= 0 Тогда
3063
+ КонечнаяСтрока = ?(МаксСтрок > 0, НачальнаяСтрока + МаксСтрок - 1, КоличествоСтрок);
3064
+ КонецЕсли;
3065
+ Если КонечнаяКолонка <= 0 Тогда
3066
+ КонечнаяКолонка = ?(МаксКолонок > 0, НачальнаяКолонка + МаксКолонок - 1, КоличествоКолонок);
3067
+ КонецЕсли;
3068
+ КонечнаяСтрока = Мин(КоличествоСтрок, КонечнаяСтрока);
3069
+ КонечнаяКолонка = Мин(КоличествоКолонок, КонечнаяКолонка);
3070
+ Если НачальнаяСтрока > КоличествоСтрок Или НачальнаяКолонка > КоличествоКолонок Или КонечнаяСтрока < НачальнаяСтрока Или КонечнаяКолонка < НачальнаяКолонка Тогда
3071
+ Возврат Новый Структура("rows,columns,address,row_start,row_end,column_start,column_end,cells", КоличествоСтрок, КоличествоКолонок, "", НачальнаяСтрока, КонечнаяСтрока, НачальнаяКолонка, КонечнаяКолонка, Новый Массив);
3031
3072
  КонецЕсли;
3032
- АдресВсейОбласти = АдресОбластиТабличногоДокументаMCP(1, 1, КоличествоСтрок, КоличествоКолонок);
3033
- ТекстВсейОбласти = Поле.ПолучитьТекстОбласти(АдресВсейОбласти);
3034
- МаксСтрок = Мин(КоличествоСтрок, 50);
3035
- МаксКолонок = Мин(КоличествоКолонок, 20);
3073
+ АдресОбласти = АдресОбластиТабличногоДокументаMCP(НачальнаяСтрока, НачальнаяКолонка, КонечнаяСтрока, КонечнаяКолонка);
3036
3074
  Ячейки = Новый Массив;
3037
- Для НомерСтроки = 1 По МаксСтрок Цикл
3075
+ Для НомерСтроки = НачальнаяСтрока По КонечнаяСтрока Цикл
3038
3076
  СтрокаТекстов = Новый Массив;
3039
- Для НомерКолонки = 1 По МаксКолонок Цикл
3077
+ Для НомерКолонки = НачальнаяКолонка По КонечнаяКолонка Цикл
3040
3078
  Попытка
3041
3079
  АдресЯчейки = АдресОбластиТабличногоДокументаMCP(НомерСтроки, НомерКолонки, НомерСтроки, НомерКолонки);
3042
3080
  СтрокаТекстов.Добавить(Поле.ПолучитьТекстОбласти(АдресЯчейки));
@@ -3046,7 +3084,7 @@
3046
3084
  КонецЦикла;
3047
3085
  Ячейки.Добавить(СтрокаТекстов);
3048
3086
  КонецЦикла;
3049
- Возврат Новый Структура("rows,columns,address,text,cells", КоличествоСтрок, КоличествоКолонок, АдресВсейОбласти, ТекстВсейОбласти, Ячейки);
3087
+ Возврат Новый Структура("rows,columns,address,row_start,row_end,column_start,column_end,cells", КоличествоСтрок, КоличествоКолонок, АдресОбласти, НачальнаяСтрока, КонечнаяСтрока, НачальнаяКолонка, КонечнаяКолонка, Ячейки);
3050
3088
  Исключение
3051
3089
  Возврат Новый Структура("error", ОписаниеОшибки());
3052
3090
  КонецПопытки;
@@ -58,38 +58,80 @@ class CredentialsStore:
58
58
  # ------------------------------------------------------------------
59
59
 
60
60
  def lookup(self, base_url: str) -> dict[str, str] | None:
61
- """Return {username, password} for *base_url*, or None."""
62
- url = base_url.rstrip("/")
61
+ """Return {username, password} for *base_url*, title or alias, or None."""
62
+ resolved = self.resolve(base_url)
63
+ if resolved is None:
64
+ return None
65
+ return {"username": resolved["username"], "password": resolved["password"]}
66
+
67
+ def resolve(self, base_url_or_title: str) -> dict[str, str] | None:
68
+ """Return full credential entry for URL, title or alias, without mutating it."""
69
+ value = str(base_url_or_title or "").strip()
70
+ url = value.rstrip("/")
71
+ normalized_title = value.lower()
72
+ for entry in self._entries:
73
+ title = str(entry.get("title", "")).strip().lower()
74
+ aliases = [str(a).strip().lower() for a in entry.get("aliases", [])]
75
+ if title and title == normalized_title or normalized_title in aliases:
76
+ return dict(entry)
63
77
  for entry in self._entries:
64
78
  pattern = entry["url"].rstrip("/")
65
79
  if url == pattern:
66
- return {"username": entry["username"], "password": entry["password"]}
80
+ return dict(entry)
67
81
  for entry in self._entries:
68
82
  pattern = entry["url"].rstrip("/")
69
83
  if fnmatch.fnmatch(url, pattern):
70
- return {"username": entry["username"], "password": entry["password"]}
84
+ return dict(entry)
71
85
  return None
72
86
 
73
- def list_entries(self) -> list[dict[str, str]]:
74
- """Return credential URL patterns without usernames or passwords."""
75
- return [{"url": e["url"]} for e in self._entries]
76
-
77
- def upsert(self, url: str, username: str, password: str) -> bool:
87
+ def list_entries(self) -> list[dict[str, object]]:
88
+ """Return credential titles and URL patterns without usernames or passwords."""
89
+ result: list[dict[str, str]] = []
90
+ for entry in self._entries:
91
+ public = {"url": entry["url"]}
92
+ if entry.get("title"):
93
+ public["title"] = entry["title"]
94
+ if entry.get("aliases"):
95
+ public["aliases"] = list(entry["aliases"])
96
+ result.append(public)
97
+ return result
98
+
99
+ def titles(self) -> list[dict[str, object]]:
100
+ """Return named credential entries only, without secrets."""
101
+ return [e for e in self.list_entries() if e.get("title")]
102
+
103
+ def upsert(self, url: str, username: str, password: str, title: str = "", aliases: list[str] | None = None) -> bool:
78
104
  """Add or update a credential entry for *url*.
79
105
 
80
106
  Returns True when an existing entry was updated, False when a new entry
81
107
  was created. Usernames and passwords are intentionally not returned.
82
108
  """
83
109
  url = url.rstrip("/")
110
+ title = str(title or "").strip()
111
+ aliases = [str(alias).strip() for alias in (aliases or []) if str(alias).strip()]
84
112
  updated = False
85
113
  for entry in self._entries:
86
- if entry["url"].rstrip("/") == url:
114
+ if entry["url"].rstrip("/") == url or (title and str(entry.get("title", "")).strip() == title):
115
+ entry["url"] = url
87
116
  entry["username"] = username
88
117
  entry["password"] = password
118
+ if title:
119
+ entry["title"] = title
120
+ else:
121
+ entry.pop("title", None)
122
+ if aliases:
123
+ entry["aliases"] = aliases
124
+ else:
125
+ entry.pop("aliases", None)
89
126
  updated = True
90
127
  break
91
128
  else:
92
- self._entries.append({"url": url, "username": username, "password": password})
129
+ entry = {"url": url, "username": username, "password": password}
130
+ if title:
131
+ entry["title"] = title
132
+ if aliases:
133
+ entry["aliases"] = aliases
134
+ self._entries.append(entry)
93
135
  self._save()
94
136
  return updated
95
137
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Any
5
5
 
6
- MANAGER_PROTOCOL_VERSION = "2026.07.02.1"
6
+ MANAGER_PROTOCOL_VERSION = "2026.07.02.2"
7
7
 
8
8
 
9
9
  @dataclass(slots=True)
@@ -1698,6 +1698,13 @@ async def start_session(
1698
1698
  rag_enabled: Annotated[bool, "Enable built-in RAG for this session"] = True,
1699
1699
  ) -> dict[str, Any]:
1700
1700
  """One-shot session factory: detect version, start infrastructure and connect the 1C test client."""
1701
+ credential_ref = _resolve_credential_reference(base_url)
1702
+ if credential_ref is not None and credential_ref.get("url"):
1703
+ base_url = str(credential_ref["url"])
1704
+ if not username:
1705
+ username = str(credential_ref.get("username", ""))
1706
+ if not password:
1707
+ password = str(credential_ref.get("password", ""))
1701
1708
  base_url = _platform.normalize_infobase_url(base_url)
1702
1709
  connection_mode = "auto"
1703
1710
  connection_value = ""
@@ -2117,18 +2124,24 @@ def _lookup_credentials(base_url: str) -> dict[str, str] | None:
2117
2124
  return _credentials_store().lookup(base_url)
2118
2125
 
2119
2126
 
2127
+ def _resolve_credential_reference(value: str) -> dict[str, str] | None:
2128
+ if not _CREDENTIAL_STORE_ENABLED or not value:
2129
+ return None
2130
+ return _credentials_store().resolve(value)
2131
+
2132
+
2120
2133
  @mcp.tool()
2121
2134
  def credentials_check(base_url: str) -> dict[str, object]:
2122
- """Check whether credentials exist for a base URL without returning secrets."""
2135
+ """Check whether credentials exist for a base URL or saved title without returning secrets."""
2123
2136
  if not _CREDENTIAL_STORE_ENABLED:
2124
2137
  return {"base_url": base_url, "found": False, "disabled": True}
2125
- credentials = _credentials_store().lookup(base_url)
2138
+ credentials = _credentials_store().resolve(base_url)
2126
2139
  if credentials is None:
2127
2140
  return {"base_url": base_url, "found": False}
2128
- return {
2129
- "base_url": base_url,
2130
- "found": True,
2131
- }
2141
+ result: dict[str, object] = {"base_url": base_url, "found": True, "url": credentials.get("url", "")}
2142
+ if credentials.get("title"):
2143
+ result["title"] = credentials["title"]
2144
+ return result
2132
2145
 
2133
2146
 
2134
2147
  @mcp.tool()
@@ -2136,6 +2149,8 @@ def credentials_save(
2136
2149
  url: Annotated[str, "Base URL pattern (exact or wildcard like https://*.example.com/*)"],
2137
2150
  username: Annotated[str, "1C username"],
2138
2151
  password: Annotated[str, "1C password"],
2152
+ title: Annotated[str, "Optional saved base title/alias, e.g. bp27932"] = "",
2153
+ aliases: Annotated[str, "Optional comma-separated aliases for the same entry"] = "",
2139
2154
  ) -> dict[str, object]:
2140
2155
  """Save or update credentials for a URL pattern in the local credentials store.
2141
2156
 
@@ -2145,12 +2160,14 @@ def credentials_save(
2145
2160
  """
2146
2161
  if not _CREDENTIAL_STORE_ENABLED:
2147
2162
  raise _credential_store_disabled_error()
2148
- updated = _credentials_store().upsert(url, username, password)
2149
- return {
2150
- "url": url,
2151
- "saved": True,
2152
- "updated": updated,
2153
- }
2163
+ alias_list = [part.strip() for part in aliases.split(",") if part.strip()]
2164
+ updated = _credentials_store().upsert(url, username, password, title=title, aliases=alias_list)
2165
+ result: dict[str, object] = {"url": url, "saved": True, "updated": updated}
2166
+ if title:
2167
+ result["title"] = title
2168
+ if alias_list:
2169
+ result["aliases"] = alias_list
2170
+ return result
2154
2171
 
2155
2172
 
2156
2173
  @mcp.tool()
@@ -2165,13 +2182,21 @@ def credentials_remove(
2165
2182
 
2166
2183
 
2167
2184
  @mcp.tool()
2168
- def credentials_list() -> list[dict[str, str]]:
2169
- """List configured credential URL patterns (no usernames or passwords)."""
2185
+ def credentials_list() -> list[dict[str, object]]:
2186
+ """List configured credential URL patterns/titles (no usernames or passwords)."""
2170
2187
  if not _CREDENTIAL_STORE_ENABLED:
2171
2188
  return []
2172
2189
  return _credentials_store().list_entries()
2173
2190
 
2174
2191
 
2192
+ @mcp.tool()
2193
+ def credentials_titles() -> list[dict[str, object]]:
2194
+ """List saved credential titles/aliases that can be used as start_session base_url."""
2195
+ if not _CREDENTIAL_STORE_ENABLED:
2196
+ return []
2197
+ return _credentials_store().titles()
2198
+
2199
+
2175
2200
  _PLACEHOLDER_PASSWORD_RE = re.compile(r"^\*{3,}$")
2176
2201
  _AUTH_FAILURE_MARKERS = (
2177
2202
  "пользователь иб не идентифицирован",
@@ -3256,13 +3281,19 @@ async def table_cell_text(
3256
3281
 
3257
3282
  @mcp.tool()
3258
3283
  async def tabular_documents(
3259
- include_text: Annotated[bool, "Include tabular document text"] = False,
3284
+ include_text: Annotated[bool, "Include tabular document cells"] = False,
3260
3285
  max_depth: Annotated[int, "Maximum search depth"] = 8,
3286
+ row_start: Annotated[int, "First row to read when include_text=true (1-based)"] = 1,
3287
+ row_end: Annotated[int, "Last row to read when include_text=true; 0 = row_start + max_rows - 1"] = 0,
3288
+ column_start: Annotated[int, "First column to read when include_text=true (1-based)"] = 1,
3289
+ column_end: Annotated[int, "Last column to read when include_text=true; 0 = column_start + max_columns - 1"] = 0,
3290
+ max_rows: Annotated[int, "Maximum rows to read when row_end=0; 0 = all rows"] = 50,
3291
+ max_columns: Annotated[int, "Maximum columns to read when column_end=0; 0 = all columns"] = 20,
3261
3292
  *,
3262
3293
  session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
3263
3294
  ) -> Any:
3264
- """List tabular document fields in the active window; optionally include their text."""
3265
- return await _bridge_call_recorded(session_id, "tabular_documents", {"include_text": include_text, "max_depth": max_depth})
3295
+ """List tabular document fields with dimensions; optionally include a cells range."""
3296
+ return await _bridge_call_recorded(session_id, "tabular_documents", {"include_text": include_text, "max_depth": max_depth, "row_start": row_start, "row_end": row_end, "column_start": column_start, "column_end": column_end, "max_rows": max_rows, "max_columns": max_columns})
3266
3297
 
3267
3298
 
3268
3299
  @mcp.tool()
@@ -3270,11 +3301,17 @@ async def tabular_document_text(
3270
3301
  name: Annotated[str, "Tabular document field technical name"] = "",
3271
3302
  title: Annotated[str, "Tabular document field visible title"] = "",
3272
3303
  max_depth: Annotated[int, "Maximum search depth"] = 8,
3304
+ row_start: Annotated[int, "First row to read (1-based)"] = 1,
3305
+ row_end: Annotated[int, "Last row to read; 0 = row_start + max_rows - 1"] = 0,
3306
+ column_start: Annotated[int, "First column to read (1-based)"] = 1,
3307
+ column_end: Annotated[int, "Last column to read; 0 = column_start + max_columns - 1"] = 0,
3308
+ max_rows: Annotated[int, "Maximum rows to read when row_end=0; 0 = all rows"] = 50,
3309
+ max_columns: Annotated[int, "Maximum columns to read when column_end=0; 0 = all columns"] = 20,
3273
3310
  *,
3274
3311
  session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
3275
3312
  ) -> Any:
3276
- """Return text and dimensions of a tabular document field in the active window."""
3277
- return await _bridge_call_recorded(session_id, "tabular_document_text", {"name": name, "title": title, "max_depth": max_depth})
3313
+ """Return dimensions and a cells range of a tabular document field in the active window."""
3314
+ return await _bridge_call_recorded(session_id, "tabular_document_text", {"name": name, "title": title, "max_depth": max_depth, "row_start": row_start, "row_end": row_end, "column_start": column_start, "column_end": column_end, "max_rows": max_rows, "max_columns": max_columns})
3278
3315
 
3279
3316
 
3280
3317
  @mcp.tool()
File without changes
File without changes
File without changes
File without changes
File without changes