"
371 | ```
372 |
373 | ### Логирование
374 |
375 | ```bash
376 | # DEBUG режим для отладки
377 | python -m src.py_server http --log-level DEBUG
378 |
379 | # Логи показывают:
380 | # - Все HTTP запросы к 1С
381 | # - OAuth2 операции (генерация/валидация токенов)
382 | # - MCP операции (tools/resources/prompts)
383 | # - Ошибки подключения
384 | ```
385 |
386 | ## Интеграция с 1С
387 |
388 | Прокси ожидает HTTP-сервис в 1С по адресу:
389 | ```
390 | {MCP_ONEC_URL}/hs/{MCP_ONEC_SERVICE_ROOT}/
391 | ```
392 |
393 | Например: `http://localhost/base/hs/mcp/`
394 |
395 | ### Endpoints 1С
396 |
397 | 1. **`GET /health`**
398 | - Проверка доступности сервиса
399 | - Ответ: `{"status": "ok"}`
400 | - Используется для валидации креденшилов в OAuth2
401 |
402 | 2. **`POST /rpc`**
403 | - JSON-RPC endpoint для всех MCP-операций
404 | - Content-Type: `application/json`
405 | - Basic Auth: `username:password`
406 |
407 | ### Формат JSON-RPC запроса
408 |
409 | ```json
410 | {
411 | "jsonrpc": "2.0",
412 | "id": 1,
413 | "method": "tools/list",
414 | "params": {}
415 | }
416 | ```
417 |
418 | ### Формат JSON-RPC ответа
419 |
420 | ```json
421 | {
422 | "jsonrpc": "2.0",
423 | "id": 1,
424 | "result": {
425 | "tools": [
426 | {
427 | "name": "get_metadata",
428 | "description": "Получить метаданные объекта",
429 | "inputSchema": {...}
430 | }
431 | ]
432 | }
433 | }
434 | ```
435 |
436 | Подробности реализации 1С-стороны: `../1c_ext/agents.md`
437 |
438 | ## Документация
439 |
440 | ### Для разработчиков
441 |
442 | - **`agents.md`** - полная документация архитектуры для AI-агентов
443 | - Детальное описание всех модулей
444 | - Протоколы взаимодействия
445 | - OAuth2 flows
446 | - Точки расширения
447 |
448 | ### Конфигурация
449 |
450 | - **`env.example`** - пример `.env` файла со всеми параметрами
451 |
452 | ---
453 |
454 | **MIT License**
455 |
456 | Проект активно развивается. Вопросы и предложения приветствуются через Issues.
457 |
--------------------------------------------------------------------------------
/src/1c_ext/CommonModules/mcp_Метаданные/Ext/Module.bsl:
--------------------------------------------------------------------------------
1 | #Область ПрограммныйИнтерфейс
2 |
3 | #Область СозданиеТаблицОписанияОбъектов
4 |
5 | // Создает пустую таблицу для описания инструментов
6 | //
7 | // Возвращаемое значение:
8 | // ТаблицаЗначений - таблица с колонками:
9 | // * ИмяОбработкиКонтейнера - Строка
10 | // * Имя - Строка
11 | // * Описание - Строка
12 | // * СхемаПараметров - Строка
13 | //
14 | Функция ТаблицаИнструментов() Экспорт
15 |
16 | ТаблицаИнструментов = Новый ТаблицаЗначений;
17 | ТаблицаИнструментов.Колонки.Добавить("ИмяОбработкиКонтейнера", Новый ОписаниеТипов("Строка"));
18 | ТаблицаИнструментов.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка"));
19 | ТаблицаИнструментов.Колонки.Добавить("Описание", Новый ОписаниеТипов("Строка"));
20 | ТаблицаИнструментов.Колонки.Добавить("СхемаПараметров", Новый ОписаниеТипов("Строка"));
21 |
22 | Возврат ТаблицаИнструментов;
23 |
24 | КонецФункции
25 |
26 | // Создает пустую таблицу для описания ресурсов
27 | //
28 | // Возвращаемое значение:
29 | // ТаблицаЗначений - таблица с колонками:
30 | // * ИмяОбработкиКонтейнера - Строка
31 | // * Адрес - Строка
32 | // * Имя - Строка
33 | // * Описание - Строка
34 | //
35 | Функция ТаблицаРесурсов() Экспорт
36 |
37 | ТаблицаРесурсов = Новый ТаблицаЗначений;
38 | ТаблицаРесурсов.Колонки.Добавить("ИмяОбработкиКонтейнера", Новый ОписаниеТипов("Строка"));
39 | ТаблицаРесурсов.Колонки.Добавить("Адрес", Новый ОписаниеТипов("Строка"));
40 | ТаблицаРесурсов.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка"));
41 | ТаблицаРесурсов.Колонки.Добавить("Описание", Новый ОписаниеТипов("Строка"));
42 |
43 | Возврат ТаблицаРесурсов;
44 |
45 | КонецФункции
46 |
47 | // Создает пустую таблицу для описания промптов
48 | //
49 | // Возвращаемое значение:
50 | // ТаблицаЗначений - таблица с колонками:
51 | // * ИмяОбработкиКонтейнера - Строка
52 | // * Имя - Строка
53 | // * Описание - Строка
54 | // * Параметры - Строка
55 | //
56 | Функция ТаблицаПромптов() Экспорт
57 |
58 | ТаблицаПромптов = Новый ТаблицаЗначений;
59 | ТаблицаПромптов.Колонки.Добавить("ИмяОбработкиКонтейнера", Новый ОписаниеТипов("Строка"));
60 | ТаблицаПромптов.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка"));
61 | ТаблицаПромптов.Колонки.Добавить("Описание", Новый ОписаниеТипов("Строка"));
62 | ТаблицаПромптов.Колонки.Добавить("Параметры", Новый ОписаниеТипов("Строка"));
63 |
64 | Возврат ТаблицаПромптов;
65 |
66 | КонецФункции
67 |
68 | #КонецОбласти
69 |
70 | #Область ДобавлениеСтрок
71 |
72 | // Добавляет строку в таблицу инструментов
73 | //
74 | // Параметры:
75 | // ТаблицаИнструментов - ТаблицаЗначений - таблица инструментов
76 | // Имя - Строка - имя инструмента
77 | // Описание - Строка - описание инструмента
78 | // СхемаПараметров - Строка - JSON-схема параметров
79 | //
80 | Процедура ДобавитьИнструмент(ТаблицаИнструментов, Имя, Описание, СхемаПараметров) Экспорт
81 |
82 | НоваяСтрока = ТаблицаИнструментов.Добавить();
83 | НоваяСтрока.Имя = Имя;
84 | НоваяСтрока.Описание = Описание;
85 | НоваяСтрока.СхемаПараметров = СхемаПараметров;
86 |
87 | КонецПроцедуры
88 |
89 | // Добавляет строку в таблицу ресурсов
90 | //
91 | // Параметры:
92 | // ТаблицаРесурсов - ТаблицаЗначений - таблица ресурсов
93 | // Адрес - Строка - адрес ресурса
94 | // Имя - Строка - имя ресурса
95 | // Описание - Строка - описание ресурса
96 | //
97 | Процедура ДобавитьРесурс(ТаблицаРесурсов, Адрес, Имя, Описание) Экспорт
98 |
99 | НоваяСтрока = ТаблицаРесурсов.Добавить();
100 | НоваяСтрока.Адрес = Адрес;
101 | НоваяСтрока.Имя = Имя;
102 | НоваяСтрока.Описание = Описание;
103 |
104 | КонецПроцедуры
105 |
106 | // Добавляет строку в таблицу промптов
107 | //
108 | // Параметры:
109 | // ТаблицаПромптов - ТаблицаЗначений - таблица промптов
110 | // Имя - Строка - имя промпта
111 | // Описание - Строка - описание промпта
112 | // Параметры - Строка - JSON-описание параметров
113 | //
114 | Процедура ДобавитьПромпт(ТаблицаПромптов, Имя, Описание, Параметры) Экспорт
115 |
116 | НоваяСтрока = ТаблицаПромптов.Добавить();
117 | НоваяСтрока.Имя = Имя;
118 | НоваяСтрока.Описание = Описание;
119 | НоваяСтрока.Параметры = Параметры;
120 |
121 | КонецПроцедуры
122 |
123 | // Заполняет таблицу инструментов из обработок-контейнеров
124 | //
125 | // Параметры:
126 | // ТаблицаИнструментов - ТаблицаЗначений - таблица инструментов для заполнения
127 | //
128 | Процедура ЗаполнитьТаблицуИнструментов(ТаблицаИнструментов) Экспорт
129 |
130 | СоставПодсистемыКонтейнеры = Метаданные.Подсистемы.mcp_MCPСервер.Подсистемы.mcp_КонтейнерыИнструментов.Состав;
131 |
132 | Для Каждого МД Из СоставПодсистемыКонтейнеры Цикл
133 |
134 | Если НЕ Метаданные.Обработки.Содержит(МД) Тогда
135 | Продолжить;
136 | КонецЕсли;
137 |
138 | МенеджерОбработки = Обработки[МД.Имя];
139 |
140 | // Создаем отдельную таблицу для текущей обработки
141 | ТекущаяТаблицаИнструментов = ТаблицаИнструментов.СкопироватьКолонки();
142 |
143 | // Вызываем метод добавления инструментов в обработке
144 | МенеджерОбработки.ДобавитьИнструменты(ТекущаяТаблицаИнструментов);
145 |
146 | // Заполняем имя обработки-контейнера и переносим в общую таблицу
147 | Для Каждого Строка Из ТекущаяТаблицаИнструментов Цикл
148 | СтрокаОбщейТаблицы = ТаблицаИнструментов.Добавить();
149 | ЗаполнитьЗначенияСвойств(СтрокаОбщейТаблицы, Строка);
150 | СтрокаОбщейТаблицы.ИмяОбработкиКонтейнера = МД.Имя;
151 | КонецЦикла;
152 |
153 | КонецЦикла;
154 |
155 | КонецПроцедуры
156 |
157 | // Заполняет таблицу ресурсов из обработок-контейнеров
158 | //
159 | // Параметры:
160 | // ТаблицаРесурсов - ТаблицаЗначений - таблица ресурсов для заполнения
161 | //
162 | Процедура ЗаполнитьТаблицуРесурсов(ТаблицаРесурсов) Экспорт
163 |
164 | СоставПодсистемыКонтейнеры = Метаданные.Подсистемы.mcp_MCPСервер.Подсистемы.mcp_КонтейнерыРесурсов.Состав;
165 |
166 | Для Каждого МД Из СоставПодсистемыКонтейнеры Цикл
167 |
168 | Если НЕ Метаданные.Обработки.Содержит(МД) Тогда
169 | Продолжить;
170 | КонецЕсли;
171 |
172 | МенеджерОбработки = Обработки[МД.Имя];
173 |
174 | // Проверяем наличие метода ДобавитьРесурсы
175 | Попытка
176 | // Создаем отдельную таблицу для текущей обработки
177 | ТекущаяТаблицаРесурсов = ТаблицаРесурсов.СкопироватьКолонки();
178 |
179 | // Вызываем метод добавления ресурсов в обработке
180 | МенеджерОбработки.ДобавитьРесурсы(ТекущаяТаблицаРесурсов);
181 |
182 | // Заполняем имя обработки-контейнера и переносим в общую таблицу
183 | Для Каждого Строка Из ТекущаяТаблицаРесурсов Цикл
184 | СтрокаОбщейТаблицы = ТаблицаРесурсов.Добавить();
185 | ЗаполнитьЗначенияСвойств(СтрокаОбщейТаблицы, Строка);
186 | СтрокаОбщейТаблицы.ИмяОбработкиКонтейнера = МД.Имя;
187 | КонецЦикла;
188 | Исключение
189 | // Метод ДобавитьРесурсы не реализован в обработке - пропускаем
190 | КонецПопытки;
191 |
192 | КонецЦикла;
193 |
194 | КонецПроцедуры
195 |
196 | // Заполняет таблицу промптов из обработок-контейнеров
197 | //
198 | // Параметры:
199 | // ТаблицаПромптов - ТаблицаЗначений - таблица промптов для заполнения
200 | //
201 | Процедура ЗаполнитьТаблицуПромптов(ТаблицаПромптов) Экспорт
202 |
203 | СоставПодсистемыКонтейнеры = Метаданные.Подсистемы.mcp_MCPСервер.Подсистемы.mcp_КонтейнерыПромптов.Состав;
204 |
205 | Для Каждого МД Из СоставПодсистемыКонтейнеры Цикл
206 |
207 | Если НЕ Метаданные.Обработки.Содержит(МД) Тогда
208 | Продолжить;
209 | КонецЕсли;
210 |
211 | МенеджерОбработки = Обработки[МД.Имя];
212 |
213 | // Проверяем наличие метода ДобавитьПромпты
214 | Попытка
215 | // Создаем отдельную таблицу для текущей обработки
216 | ТекущаяТаблицаПромптов = ТаблицаПромптов.СкопироватьКолонки();
217 |
218 | // Вызываем метод добавления промптов в обработке
219 | МенеджерОбработки.ДобавитьПромпты(ТекущаяТаблицаПромптов);
220 |
221 | // Заполняем имя обработки-контейнера и переносим в общую таблицу
222 | Для Каждого Строка Из ТекущаяТаблицаПромптов Цикл
223 | СтрокаОбщейТаблицы = ТаблицаПромптов.Добавить();
224 | ЗаполнитьЗначенияСвойств(СтрокаОбщейТаблицы, Строка);
225 | СтрокаОбщейТаблицы.ИмяОбработкиКонтейнера = МД.Имя;
226 | КонецЦикла;
227 | Исключение
228 | // Метод ДобавитьПромпты не реализован в обработке - пропускаем
229 | КонецПопытки;
230 |
231 | КонецЦикла;
232 |
233 | КонецПроцедуры
234 |
235 | #КонецОбласти
236 |
237 | #Область СхемаПараметровИнструментов
238 |
239 | // Создает описание простого параметра для схемы инструмента
240 | //
241 | // Параметры:
242 | // Имя - Строка - имя параметра
243 | // Тип - Строка - тип параметра (string, number, boolean, etc.)
244 | // Описание - Строка - описание параметра
245 | // ЗначениеПоУмолчанию - Произвольный - значение по умолчанию (Неопределено, если нет)
246 | // Обязательный - Булево - признак обязательности параметра
247 | // СписокДопустимыхЗначений - Строка - список допустимых значений через запятую (пустая строка, если нет ограничений)
248 | //
249 | // Возвращаемое значение:
250 | // Структура - описание простого параметра
251 | //
252 | Функция ПараметрИнструмента(Имя, Тип = Неопределено, Описание = Неопределено, ЗначениеПоУмолчанию = Неопределено, Обязательный = Ложь, СписокДопустимыхЗначений = "") Экспорт
253 |
254 | ОписаниеПараметра = Новый Структура;
255 | ОписаниеПараметра.Вставить("ТипЭлемента", "ПростойПараметр");
256 | ОписаниеПараметра.Вставить("Имя", Имя);
257 | ОписаниеПараметра.Вставить("Описание", Описание);
258 | ОписаниеПараметра.Вставить("Тип", Тип);
259 | ОписаниеПараметра.Вставить("ЗначениеПоУмолчанию", ЗначениеПоУмолчанию);
260 | ОписаниеПараметра.Вставить("Обязательный", Обязательный);
261 | ОписаниеПараметра.Вставить("СписокДопустимыхЗначений", СписокДопустимыхЗначений);
262 |
263 | Возврат ОписаниеПараметра;
264 |
265 | КонецФункции
266 |
267 | // Создает описание параметра-массива для схемы инструмента
268 | //
269 | // Параметры:
270 | // Имя - Строка - имя параметра
271 | // ТипЭлементаМассива - Строка - тип элементов массива
272 | // Описание - Строка - описание параметра
273 | // Обязательный - Булево - признак обязательности параметра
274 | // СписокДопустимыхЗначенийЭлемента - Строка - список допустимых значений для элементов через запятую
275 | //
276 | // Возвращаемое значение:
277 | // Структура - описание параметра-массива
278 | //
279 | Функция ПараметрИнструментаМассив(Имя, ТипЭлементаМассива = Неопределено, Описание = Неопределено, Обязательный = Ложь, СписокДопустимыхЗначенийЭлемента = "") Экспорт
280 |
281 | ОписаниеПараметра = Новый Структура;
282 | ОписаниеПараметра.Вставить("ТипЭлемента", "Массив");
283 | ОписаниеПараметра.Вставить("Имя", Имя);
284 | ОписаниеПараметра.Вставить("Описание", Описание);
285 | ОписаниеПараметра.Вставить("ТипЭлементаМассива", ТипЭлементаМассива);
286 | ОписаниеПараметра.Вставить("Обязательный", Обязательный);
287 | ОписаниеПараметра.Вставить("СписокДопустимыхЗначенийЭлемента", СписокДопустимыхЗначенийЭлемента);
288 |
289 | Возврат ОписаниеПараметра;
290 |
291 | КонецФункции
292 |
293 | // Создает JSON-схему параметров инструмента из массива описаний параметров
294 | //
295 | // Параметры:
296 | // МассивОписанийПараметров - Массив из Структура - массив описаний параметров
297 | //
298 | // Возвращаемое значение:
299 | // Строка - JSON-схема параметров
300 | //
301 | Функция СхемаПараметровИнструмента(МассивОписанийПараметров) Экспорт
302 |
303 | Схема = Новый Структура;
304 | Схема.Вставить("type", "object");
305 |
306 | Свойства = Новый Структура;
307 | ОбязательныеПараметры = Новый Массив;
308 |
309 | Для Каждого ОписаниеПараметра Из МассивОписанийПараметров Цикл
310 |
311 | ИмяПараметра = ОписаниеПараметра.Имя;
312 | СвойствоПараметра = Новый Структура;
313 |
314 | Если ОписаниеПараметра.ТипЭлемента = "ПростойПараметр" Тогда
315 |
316 | Если ЗначениеЗаполнено(ОписаниеПараметра.Тип) Тогда
317 | СвойствоПараметра.Вставить("type", ОписаниеПараметра.Тип);
318 | КонецЕсли;
319 |
320 | Если ЗначениеЗаполнено(ОписаниеПараметра.Описание) Тогда
321 | СвойствоПараметра.Вставить("description", ОписаниеПараметра.Описание);
322 | КонецЕсли;
323 |
324 | Если ЗначениеЗаполнено(ОписаниеПараметра.ЗначениеПоУмолчанию) Тогда
325 | СвойствоПараметра.Вставить("default", ОписаниеПараметра.ЗначениеПоУмолчанию);
326 | КонецЕсли;
327 |
328 | Если ЗначениеЗаполнено(ОписаниеПараметра.СписокДопустимыхЗначений) Тогда
329 | МассивЗначений = МассивДопустимыхЗначений(ОписаниеПараметра.СписокДопустимыхЗначений);
330 | СвойствоПараметра.Вставить("enum", МассивЗначений);
331 | КонецЕсли;
332 |
333 | ИначеЕсли ОписаниеПараметра.ТипЭлемента = "Массив" Тогда
334 |
335 | СвойствоПараметра.Вставить("type", "array");
336 |
337 | Если ЗначениеЗаполнено(ОписаниеПараметра.Описание) Тогда
338 | СвойствоПараметра.Вставить("description", ОписаниеПараметра.Описание);
339 | КонецЕсли;
340 |
341 | ЭлементМассива = Новый Структура;
342 |
343 | Если ЗначениеЗаполнено(ОписаниеПараметра.ТипЭлементаМассива) Тогда
344 | ЭлементМассива.Вставить("type", ОписаниеПараметра.ТипЭлементаМассива);
345 | КонецЕсли;
346 |
347 | Если ЗначениеЗаполнено(ОписаниеПараметра.СписокДопустимыхЗначенийЭлемента) Тогда
348 | МассивЗначений = МассивДопустимыхЗначений(ОписаниеПараметра.СписокДопустимыхЗначенийЭлемента);
349 | ЭлементМассива.Вставить("enum", МассивЗначений);
350 | КонецЕсли;
351 |
352 | СвойствоПараметра.Вставить("items", ЭлементМассива);
353 |
354 | КонецЕсли;
355 |
356 | Свойства.Вставить(ИмяПараметра, СвойствоПараметра);
357 |
358 | Если ОписаниеПараметра.Обязательный Тогда
359 | ОбязательныеПараметры.Добавить(ИмяПараметра);
360 | КонецЕсли;
361 |
362 | КонецЦикла;
363 |
364 | Схема.Вставить("properties", Свойства);
365 |
366 | Если ОбязательныеПараметры.Количество() > 0 Тогда
367 | Схема.Вставить("required", ОбязательныеПараметры);
368 | КонецЕсли;
369 |
370 | Возврат mcp_ОбщегоНазначения.СтруктураВJSON(Схема);
371 |
372 | КонецФункции
373 |
374 | #КонецОбласти
375 |
376 | #Область ПараметрыПромптов
377 |
378 | // Создает описание параметра промпта
379 | //
380 | // Параметры:
381 | // Имя - Строка - имя параметра
382 | // Описание - Строка - описание параметра
383 | // Обязательный - Булево - признак обязательности параметра
384 | //
385 | // Возвращаемое значение:
386 | // Структура - описание параметра промпта
387 | //
388 | Функция ПараметрПромпта(Имя, Описание = Неопределено, Обязательный = Ложь) Экспорт
389 |
390 | ОписаниеПараметра = Новый Структура;
391 | ОписаниеПараметра.Вставить("Имя", Имя);
392 | ОписаниеПараметра.Вставить("Описание", Описание);
393 | ОписаниеПараметра.Вставить("Обязательный", Обязательный);
394 |
395 | Возврат ОписаниеПараметра;
396 |
397 | КонецФункции
398 |
399 | // Создает JSON-описание параметров промпта из массива описаний параметров
400 | //
401 | // Параметры:
402 | // МассивОписанийПараметров - Массив из Структура - массив описаний параметров промпта
403 | //
404 | // Возвращаемое значение:
405 | // Строка - JSON-описание параметров промпта
406 | //
407 | Функция ПараметрыПромпта(МассивОписанийПараметров) Экспорт
408 |
409 | МассивПараметров = Новый Массив;
410 |
411 | Для Каждого ОписаниеПараметра Из МассивОписанийПараметров Цикл
412 |
413 | ПараметрПромпта = Новый Структура;
414 | ПараметрПромпта.Вставить("name", ОписаниеПараметра.Имя);
415 |
416 | Если ЗначениеЗаполнено(ОписаниеПараметра.Описание) Тогда
417 | ПараметрПромпта.Вставить("description", ОписаниеПараметра.Описание);
418 | КонецЕсли;
419 |
420 | ПараметрПромпта.Вставить("required", ОписаниеПараметра.Обязательный);
421 |
422 | МассивПараметров.Добавить(ПараметрПромпта);
423 |
424 | КонецЦикла;
425 |
426 | Возврат mcp_ОбщегоНазначения.СтруктураВJSON(МассивПараметров);
427 |
428 | КонецФункции
429 |
430 | #КонецОбласти
431 |
432 | #КонецОбласти
433 |
434 | #Область СлужебныеПроцедурыИФункции
435 |
436 | // Преобразует строку со списком допустимых значений в массив
437 | //
438 | // Параметры:
439 | // СписокДопустимыхЗначений - Строка - список значений через запятую
440 | //
441 | // Возвращаемое значение:
442 | // Массив - массив обработанных значений
443 | //
444 | Функция МассивДопустимыхЗначений(СписокДопустимыхЗначений)
445 |
446 | МассивИсходныхЗначений = СтрРазделить(СписокДопустимыхЗначений, ",", Ложь);
447 | МассивЗначений = Новый Массив;
448 | Для Каждого Значение Из МассивИсходныхЗначений Цикл
449 | МассивЗначений.Добавить(СокрЛП(Значение));
450 | КонецЦикла;
451 |
452 | Возврат МассивЗначений;
453 |
454 | КонецФункции
455 |
456 | #КонецОбласти
457 |
--------------------------------------------------------------------------------
/src/1c_ext/HTTPServices/mcp_APIBackend/Ext/Module.bsl:
--------------------------------------------------------------------------------
1 | #Область ОбщийИнтерфейс
2 |
3 | Функция rpcPOST(Запрос)
4 | // Обрабатываем через унифицированную функцию
5 | Возврат ОбработатьJSONRPCЗапрос(Запрос);
6 | КонецФункции
7 |
8 | Функция healthGET(Запрос)
9 | ОтветДанные = Новый Структура;
10 | ОтветДанные.Вставить("status", "ok");
11 |
12 | JSONСтрока = mcp_ОбщегоНазначения.СтруктураВJSON(ОтветДанные);
13 |
14 | Ответ = Новый HTTPСервисОтвет(200);
15 | Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
16 | Ответ.УстановитьТелоИзСтроки(JSONСтрока, КодировкаТекста.UTF8);
17 |
18 | Возврат Ответ;
19 | КонецФункции
20 |
21 | Функция mcpPOST(Запрос)
22 | // Обрабатываем MCP Streamable HTTP запросы через унифицированную функцию
23 | Возврат ОбработатьJSONRPCЗапрос(Запрос);
24 | КонецФункции
25 |
26 | Функция mcpGET(Запрос)
27 | // GET не поддерживается для MCP эндпоинта согласно спецификации
28 | Ответ = Новый HTTPСервисОтвет(405);
29 | Ответ.Заголовки.Вставить("Allow", "POST");
30 | Возврат Ответ;
31 | КонецФункции
32 |
33 | #КонецОбласти
34 |
35 | #Область УнифицированнаяОбработкаJSONRPC
36 |
37 | Функция ОбработатьJSONRPCЗапрос(Запрос)
38 | // Унифицированная обработка JSON-RPC запросов для /rpc и /mcp эндпоинтов
39 |
40 | Ответ = Новый HTTPСервисОтвет(200);
41 | Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
42 |
43 | // Добавляем CORS заголовки для совместимости с браузерными клиентами
44 | Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
45 |
46 | Попытка
47 | // Получаем тело запроса
48 | ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8);
49 |
50 | // Парсим JSON-RPC запрос
51 | ЗапросДанные = mcp_ОбщегоНазначения.JSONВСтруктуру(ТелоЗапроса);
52 |
53 | // Для notifications (запросы без id) сразу возвращаем 204 No Content
54 | Если НЕ ЗапросДанные.Свойство("id") Тогда
55 | Возврат СформироватьОтвет204();
56 | КонецЕсли;
57 |
58 | ИдентификаторЗапроса = ЗапросДанные.id;
59 |
60 | // Проверяем версию JSON-RPC
61 | Если ЗапросДанные.Свойство("jsonrpc") И ЗапросДанные.jsonrpc <> "2.0" Тогда
62 | Возврат СформироватьJSONОшибку(Ответ, ИдентификаторЗапроса, -32600, "Неподдерживаемая версия JSON-RPC");
63 | КонецЕсли;
64 |
65 | // Получаем метод
66 | Метод = "";
67 | Если ЗапросДанные.Свойство("method") Тогда
68 | Метод = ЗапросДанные.method;
69 | КонецЕсли;
70 |
71 | // Получаем параметры
72 | Параметры = Новый Структура;
73 | Если ЗапросДанные.Свойство("params") Тогда
74 | Параметры = ЗапросДанные.params;
75 | КонецЕсли;
76 |
77 | // Маршрутизация по методам
78 | Результат = Неопределено;
79 |
80 | Если Метод = "initialize" Тогда
81 | Результат = ОбработатьInitialize(Параметры);
82 | ИначеЕсли Метод = "tools/list" Тогда
83 | Результат = ПолучитьСписокИнструментов(Параметры);
84 | ИначеЕсли Метод = "tools/call" Тогда
85 | Результат = ВызватьИнструмент(Параметры);
86 | ИначеЕсли Метод = "resources/list" Тогда
87 | Результат = ПолучитьСписокРесурсов(Параметры);
88 | ИначеЕсли Метод = "resources/read" Тогда
89 | Результат = ПолучитьРесурс(Параметры);
90 | ИначеЕсли Метод = "prompts/list" Тогда
91 | Результат = ПолучитьСписокПромптов(Параметры);
92 | ИначеЕсли Метод = "prompts/get" Тогда
93 | Результат = ПолучитьПромпт(Параметры);
94 | Иначе
95 | // Неизвестный метод
96 | Возврат СформироватьJSONОшибку(Ответ, ИдентификаторЗапроса, -32601, "Неизвестный метод: " + Метод);
97 | КонецЕсли;
98 |
99 | // Формируем успешный ответ
100 | Возврат СформироватьJSONУспех(Ответ, ИдентификаторЗапроса, Результат);
101 |
102 | Исключение
103 | ИнформацияОбОшибке = ИнформацияОбОшибке();
104 | ОписаниеОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
105 |
106 | Возврат СформироватьJSONОшибку(Ответ, ИдентификаторЗапроса, -32603, "Внутренняя ошибка сервера: " + ОписаниеОшибки);
107 | КонецПопытки;
108 | КонецФункции
109 |
110 | Функция ОбработатьInitialize(Параметры)
111 | // Обрабатывает метод initialize для MCP handshake
112 |
113 | Результат = Новый Структура;
114 |
115 | // Устанавливаем версию протокола
116 | Результат.Вставить("protocolVersion", "2025-03-26");
117 |
118 | // Описываем возможности сервера
119 | ВозможностиИнструментов = Новый Структура;
120 | ВозможностиИнструментов.Вставить("listChanged", Ложь);
121 | ВозможностиИнструментов.Вставить("call", Истина);
122 |
123 | ВозможностиРесурсов = Новый Структура;
124 | ВозможностиРесурсов.Вставить("listChanged", Ложь);
125 | ВозможностиРесурсов.Вставить("subscribe", Ложь);
126 |
127 | ВозможностиПромптов = Новый Структура;
128 | ВозможностиПромптов.Вставить("listChanged", Ложь);
129 |
130 | Возможности = Новый Структура;
131 | Возможности.Вставить("tools", ВозможностиИнструментов);
132 | Возможности.Вставить("resources", ВозможностиРесурсов);
133 | Возможности.Вставить("prompts", ВозможностиПромптов);
134 |
135 | Результат.Вставить("capabilities", Возможности);
136 |
137 | // Информация о сервере
138 | ИнформацияСервера = Новый Структура;
139 | ИнформацияСервера.Вставить("name", "1C MCP Server");
140 | ИнформацияСервера.Вставить("version", "1.0.0");
141 |
142 | Результат.Вставить("serverInfo", ИнформацияСервера);
143 |
144 | Возврат Результат;
145 | КонецФункции
146 |
147 | Функция СформироватьJSONУспех(HTTPОтвет, ИдентификаторЗапроса, Результат)
148 | // Формирует успешный JSON-RPC ответ
149 |
150 | ОтветУспех = Новый Структура;
151 | ОтветУспех.Вставить("jsonrpc", "2.0");
152 | ОтветУспех.Вставить("id", ИдентификаторЗапроса);
153 | ОтветУспех.Вставить("result", Результат);
154 |
155 | HTTPОтвет.УстановитьТелоИзСтроки(mcp_ОбщегоНазначения.СтруктураВJSON(ОтветУспех), КодировкаТекста.UTF8);
156 |
157 | Возврат HTTPОтвет;
158 | КонецФункции
159 |
160 | Функция СформироватьJSONОшибку(HTTPОтвет, ИдентификаторЗапроса, КодОшибки, СообщениеОшибки)
161 | // Формирует JSON-RPC ответ с ошибкой
162 |
163 | ОтветОшибка = СформироватьОтветОшибку(КодОшибки, СообщениеОшибки, ИдентификаторЗапроса);
164 | HTTPОтвет.УстановитьТелоИзСтроки(mcp_ОбщегоНазначения.СтруктураВJSON(ОтветОшибка), КодировкаТекста.UTF8);
165 |
166 | Возврат HTTPОтвет;
167 | КонецФункции
168 |
169 | Функция СформироватьОтвет204()
170 | // Формирует ответ 204 No Content для notifications
171 |
172 | Ответ = Новый HTTPСервисОтвет(204);
173 | Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
174 |
175 | Возврат Ответ;
176 | КонецФункции
177 |
178 | #КонецОбласти
179 |
180 | #Область РаботаСИнструментами
181 |
182 | Функция ПолучитьСписокИнструментов(Параметры)
183 | // Получает список доступных инструментов из контейнеров
184 | // Возвращает структуру с полем "tools" - массив инструментов
185 | // Каждый инструмент содержит:
186 | // - name (строка): имя инструмента
187 | // - description (строка): описание инструмента
188 | // - inputSchema (структура): JSON схема входных параметров
189 |
190 | Результат = Новый Структура;
191 | Инструменты = Новый Массив;
192 |
193 | // Получаем таблицу инструментов из контейнеров
194 | ТаблицаИнструментов = mcp_КонтейнерыПовтИсп.Инструменты();
195 |
196 | // Преобразуем таблицу в массив структур для JSON-RPC ответа
197 | Для Каждого СтрокаИнструмента Из ТаблицаИнструментов Цикл
198 |
199 | Инструмент = Новый Структура;
200 | Инструмент.Вставить("name", СтрокаИнструмента.Имя);
201 | Инструмент.Вставить("description", СтрокаИнструмента.Описание);
202 |
203 | // Парсим JSON схему параметров
204 | СхемаПараметров = Новый Структура;
205 | Если ЗначениеЗаполнено(СтрокаИнструмента.СхемаПараметров) Тогда
206 | Попытка
207 | СхемаПараметров = mcp_ОбщегоНазначения.JSONВСтруктуру(СтрокаИнструмента.СхемаПараметров);
208 | Исключение
209 | ИнформацияОбОшибке = ИнформацияОбОшибке();
210 | ТекстОшибки = СтрШаблон("Ошибка чтения JSON схемы параметров для инструмента '%1': %2",
211 | СтрокаИнструмента.Имя,
212 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
213 | ВызватьИсключение ТекстОшибки;
214 | КонецПопытки;
215 | КонецЕсли;
216 |
217 | Инструмент.Вставить("inputSchema", СхемаПараметров);
218 |
219 | Инструменты.Добавить(Инструмент);
220 |
221 | КонецЦикла;
222 |
223 | Результат.Вставить("tools", Инструменты);
224 |
225 | Возврат Результат;
226 | КонецФункции
227 |
228 | Функция ВызватьИнструмент(Параметры)
229 | // Выполняет инструмент из контейнеров
230 | // Параметры содержат:
231 | // - name (строка): имя инструмента для вызова
232 | // - arguments (структура): аргументы для инструмента
233 | //
234 | // Возвращает структуру с полями:
235 | // - content (массив): содержимое результата
236 | // Каждый элемент содержит:
237 | // - type ("text" или "image"): тип содержимого
238 | // - text (строка): текст для типа "text"
239 | // - data (строка): данные изображения в base64 для типа "image"
240 | // - mimeType (строка): MIME-тип для изображений
241 | // - isError (булево): признак ошибки
242 |
243 | ИмяИнструмента = "";
244 | Если Параметры.Свойство("name") Тогда
245 | ИмяИнструмента = Параметры.name;
246 | КонецЕсли;
247 |
248 | АргументыИнструмента = Новый Структура;
249 | Если Параметры.Свойство("arguments") Тогда
250 | АргументыИнструмента = Параметры.arguments;
251 | КонецЕсли;
252 |
253 | // Проверяем обязательные параметры
254 | Если НЕ ЗначениеЗаполнено(ИмяИнструмента) Тогда
255 | ВызватьИсключение "Не указано имя инструмента для вызова";
256 | КонецЕсли;
257 |
258 | // Получаем таблицу инструментов и ищем нужный
259 | ТаблицаИнструментов = mcp_КонтейнерыПовтИсп.Инструменты();
260 | СтрокаИнструмента = ТаблицаИнструментов.Найти(ИмяИнструмента, "Имя");
261 |
262 | Если СтрокаИнструмента = Неопределено Тогда
263 | ВызватьИсключение СтрШаблон("Инструмент '%1' не найден", ИмяИнструмента);
264 | КонецЕсли;
265 |
266 | Результат = Новый Структура;
267 | Содержимое = Новый Массив;
268 | ПризнакОшибки = Ложь;
269 |
270 | Попытка
271 | // Выполняем инструмент через модуль выполнения
272 | РезультатВыполнения = mcp_Выполнение.ВыполнитьИнструмент(СтрокаИнструмента, АргументыИнструмента);
273 |
274 | // Преобразуем результат в формат MCP
275 | Если ТипЗнч(РезультатВыполнения) = Тип("Массив") Тогда
276 | // Результат уже в формате массива элементов содержимого
277 | Для Каждого Элемент Из РезультатВыполнения Цикл
278 | Если ТипЗнч(Элемент) = Тип("Структура") И Элемент.Свойство("type") Тогда
279 | Содержимое.Добавить(Элемент);
280 | Иначе
281 | // Преобразуем произвольный элемент в текстовый
282 | ЭлементСодержимого = Новый Структура;
283 | ЭлементСодержимого.Вставить("type", "text");
284 | ЭлементСодержимого.Вставить("text", Строка(Элемент));
285 | Содержимое.Добавить(ЭлементСодержимого);
286 | КонецЕсли;
287 | КонецЦикла;
288 | Иначе
289 | // Результат - одиночное значение, преобразуем в текст
290 | ЭлементСодержимого = Новый Структура;
291 | ЭлементСодержимого.Вставить("type", "text");
292 | ЭлементСодержимого.Вставить("text", Строка(РезультатВыполнения));
293 | Содержимое.Добавить(ЭлементСодержимого);
294 | КонецЕсли;
295 |
296 | Исключение
297 | ПризнакОшибки = Истина;
298 | ИнформацияОбОшибке = ИнформацияОбОшибке();
299 |
300 | ЭлементОшибки = Новый Структура;
301 | ЭлементОшибки.Вставить("type", "text");
302 | ЭлементОшибки.Вставить("text",
303 | СтрШаблон("Ошибка выполнения инструмента '%1': %2",
304 | ИмяИнструмента,
305 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)));
306 |
307 | Содержимое.Добавить(ЭлементОшибки);
308 | КонецПопытки;
309 |
310 | Результат.Вставить("content", Содержимое);
311 | Результат.Вставить("isError", ПризнакОшибки);
312 |
313 | Возврат Результат;
314 | КонецФункции
315 |
316 | #КонецОбласти
317 |
318 | #Область РаботаСРесурсами
319 |
320 | Функция ПолучитьСписокРесурсов(Параметры)
321 | // Получает список доступных ресурсов из контейнеров
322 | // Возвращает структуру с полем "resources" - массив ресурсов
323 | // Каждый ресурс содержит:
324 | // - uri (строка): URI ресурса
325 | // - name (строка): имя ресурса
326 | // - description (строка): описание ресурса
327 | // - mimeType (строка): MIME-тип ресурса (опционально)
328 |
329 | Результат = Новый Структура;
330 | Ресурсы = Новый Массив;
331 |
332 | // Получаем таблицу ресурсов из контейнеров
333 | ТаблицаРесурсов = mcp_КонтейнерыПовтИсп.Ресурсы();
334 |
335 | // Преобразуем таблицу в массив структур для JSON-RPC ответа
336 | Для Каждого СтрокаРесурса Из ТаблицаРесурсов Цикл
337 |
338 | Ресурс = Новый Структура;
339 | Ресурс.Вставить("uri", СтрокаРесурса.Адрес);
340 | Ресурс.Вставить("name", СтрокаРесурса.Имя);
341 | Ресурс.Вставить("description", СтрокаРесурса.Описание);
342 |
343 | // mimeType не хранится в таблице ресурсов, поэтому не указываем
344 | // При необходимости можно будет добавить в схему таблицы
345 |
346 | Ресурсы.Добавить(Ресурс);
347 |
348 | КонецЦикла;
349 |
350 | Результат.Вставить("resources", Ресурсы);
351 |
352 | Возврат Результат;
353 | КонецФункции
354 |
355 | Функция ПолучитьРесурс(Параметры)
356 | // Читает содержимое ресурса из контейнеров
357 | // Параметры содержат:
358 | // - uri (строка): URI ресурса для получения
359 | //
360 | // Возвращает структуру с полем "contents" - массив содержимого
361 | // Каждый элемент содержит:
362 | // - type ("text" или "blob"): тип содержимого
363 | // - mimeType (строка): MIME-тип содержимого
364 | // - text (строка): текстовое содержимое для типа "text"
365 | // - blob (строка): двоичные данные в base64 для типа "blob"
366 |
367 | URIРесурса = "";
368 | Если Параметры.Свойство("uri") Тогда
369 | URIРесурса = Параметры.uri;
370 | КонецЕсли;
371 |
372 | // Проверяем обязательные параметры
373 | Если НЕ ЗначениеЗаполнено(URIРесурса) Тогда
374 | ВызватьИсключение "Не указан URI ресурса для чтения";
375 | КонецЕсли;
376 |
377 | // Получаем таблицу ресурсов и ищем нужный
378 | ТаблицаРесурсов = mcp_КонтейнерыПовтИсп.Ресурсы();
379 | СтрокаРесурса = ТаблицаРесурсов.Найти(URIРесурса, "Адрес");
380 |
381 | Если СтрокаРесурса = Неопределено Тогда
382 | ВызватьИсключение СтрШаблон("Ресурс '%1' не найден", URIРесурса);
383 | КонецЕсли;
384 |
385 | Результат = Новый Структура;
386 | Содержимое = Новый Массив;
387 |
388 | Попытка
389 | // Читаем ресурс через модуль выполнения
390 | СодержимоеРесурса = mcp_Выполнение.ПрочитатьРесурс(СтрокаРесурса, URIРесурса);
391 |
392 | // Преобразуем результат в формат MCP
393 | Если ТипЗнч(СодержимоеРесурса) = Тип("Массив") Тогда
394 | // Содержимое уже в формате массива элементов
395 | Для Каждого Элемент Из СодержимоеРесурса Цикл
396 | Если ТипЗнч(Элемент) = Тип("Структура") И Элемент.Свойство("type") Тогда
397 | // Добавляем uri, если его нет
398 | Если НЕ Элемент.Свойство("uri") Тогда
399 | Элемент.Вставить("uri", URIРесурса);
400 | КонецЕсли;
401 | Содержимое.Добавить(Элемент);
402 | Иначе
403 | // Преобразуем произвольный элемент в текстовый
404 | ЭлементСодержимого = Новый Структура;
405 | ЭлементСодержимого.Вставить("type", "text");
406 | ЭлементСодержимого.Вставить("uri", URIРесурса);
407 | ЭлементСодержимого.Вставить("mimeType", "text/plain");
408 | ЭлементСодержимого.Вставить("text", Строка(Элемент));
409 | Содержимое.Добавить(ЭлементСодержимого);
410 | КонецЕсли;
411 | КонецЦикла;
412 | Иначе
413 | // Содержимое - одиночное значение, преобразуем в текст
414 | ЭлементСодержимого = Новый Структура;
415 | ЭлементСодержимого.Вставить("type", "text");
416 | ЭлементСодержимого.Вставить("uri", URIРесурса);
417 | ЭлементСодержимого.Вставить("mimeType", "text/plain");
418 | ЭлементСодержимого.Вставить("text", Строка(СодержимоеРесурса));
419 | Содержимое.Добавить(ЭлементСодержимого);
420 | КонецЕсли;
421 |
422 | Исключение
423 | ИнформацияОбОшибке = ИнформацияОбОшибке();
424 | ТекстОшибки = СтрШаблон("Ошибка чтения ресурса '%1': %2",
425 | URIРесурса,
426 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
427 | ВызватьИсключение ТекстОшибки;
428 | КонецПопытки;
429 |
430 | Результат.Вставить("contents", Содержимое);
431 |
432 | Возврат Результат;
433 | КонецФункции
434 |
435 | #КонецОбласти
436 |
437 | #Область РаботаСПромптами
438 |
439 | Функция ПолучитьСписокПромптов(Параметры)
440 | // Получает список доступных промптов из контейнеров
441 | // Возвращает структуру с полем "prompts" - массив промптов
442 | // Каждый промпт содержит:
443 | // - name (строка): имя промпта
444 | // - description (строка): описание промпта
445 | // - arguments (массив): аргументы промпта (опционально)
446 | // Каждый аргумент содержит:
447 | // - name (строка): имя аргумента
448 | // - description (строка): описание аргумента
449 | // - required (булево): признак обязательности
450 |
451 | Результат = Новый Структура;
452 | Промпты = Новый Массив;
453 |
454 | // Получаем таблицу промптов из контейнеров
455 | ТаблицаПромптов = mcp_КонтейнерыПовтИсп.Промпты();
456 |
457 | // Преобразуем таблицу в массив структур для JSON-RPC ответа
458 | Для Каждого СтрокаПромпта Из ТаблицаПромптов Цикл
459 |
460 | Промпт = Новый Структура;
461 | Промпт.Вставить("name", СтрокаПромпта.Имя);
462 | Промпт.Вставить("description", СтрокаПромпта.Описание);
463 |
464 | // Парсим JSON параметров промпта
465 | АргументыПромпта = Новый Массив;
466 | Если ЗначениеЗаполнено(СтрокаПромпта.Параметры) Тогда
467 | Попытка
468 | АргументыПромпта = mcp_ОбщегоНазначения.JSONВСтруктуру(СтрокаПромпта.Параметры);
469 | // Если это не массив, а объект - генерируем ошибку
470 | Если ТипЗнч(АргументыПромпта) <> Тип("Массив") Тогда
471 | ТекстОшибки = СтрШаблон("Параметры промпта '%1' должны быть массивом, а получен %2",
472 | СтрокаПромпта.Имя,
473 | ТипЗнч(АргументыПромпта));
474 | ВызватьИсключение ТекстОшибки;
475 | КонецЕсли;
476 | Исключение
477 | ИнформацияОбОшибке = ИнформацияОбОшибке();
478 | ТекстОшибки = СтрШаблон("Ошибка чтения JSON параметров для промпта '%1': %2",
479 | СтрокаПромпта.Имя,
480 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
481 | ВызватьИсключение ТекстОшибки;
482 | КонецПопытки;
483 | КонецЕсли;
484 |
485 | Промпт.Вставить("arguments", АргументыПромпта);
486 |
487 | Промпты.Добавить(Промпт);
488 |
489 | КонецЦикла;
490 |
491 | Результат.Вставить("prompts", Промпты);
492 |
493 | Возврат Результат;
494 | КонецФункции
495 |
496 | Функция ПолучитьПромпт(Параметры)
497 | // Получает промпт из контейнеров
498 | // Параметры содержат:
499 | // - name (строка): имя промпта
500 | // - arguments (структура): аргументы промпта
501 | //
502 | // Возвращает структуру с полями:
503 | // - description (строка): описание промпта
504 | // - messages (массив): сообщения промпта
505 | // Каждое сообщение содержит:
506 | // - role (строка): роль ("user", "assistant", "system")
507 | // - content (структура): содержимое сообщения
508 | // - type ("text"): тип содержимого
509 | // - text (строка): текст сообщения
510 |
511 | ИмяПромпта = "";
512 | Если Параметры.Свойство("name") Тогда
513 | ИмяПромпта = Параметры.name;
514 | КонецЕсли;
515 |
516 | АргументыПромпта = Новый Структура;
517 | Если Параметры.Свойство("arguments") Тогда
518 | АргументыПромпта = Параметры.arguments;
519 | КонецЕсли;
520 |
521 | // Проверяем обязательные параметры
522 | Если НЕ ЗначениеЗаполнено(ИмяПромпта) Тогда
523 | ВызватьИсключение "Не указано имя промпта для получения";
524 | КонецЕсли;
525 |
526 | // Получаем таблицу промптов и ищем нужный
527 | ТаблицаПромптов = mcp_КонтейнерыПовтИсп.Промпты();
528 | СтрокаПромпта = ТаблицаПромптов.Найти(ИмяПромпта, "Имя");
529 |
530 | Если СтрокаПромпта = Неопределено Тогда
531 | ВызватьИсключение СтрШаблон("Промпт '%1' не найден", ИмяПромпта);
532 | КонецЕсли;
533 |
534 | Результат = Новый Структура;
535 | Сообщения = Новый Массив;
536 |
537 | Попытка
538 | // Получаем промпт через модуль выполнения
539 | СодержимоеПромпта = mcp_Выполнение.ПолучитьПромпт(СтрокаПромпта, ИмяПромпта, АргументыПромпта);
540 |
541 | // Устанавливаем описание из таблицы или из результата
542 | ОписаниеПромпта = СтрокаПромпта.Описание;
543 | Если ТипЗнч(СодержимоеПромпта) = Тип("Структура") И СодержимоеПромпта.Свойство("description") Тогда
544 | ОписаниеПромпта = СодержимоеПромпта.description;
545 | КонецЕсли;
546 |
547 | // Преобразуем результат в формат MCP
548 | Если ТипЗнч(СодержимоеПромпта) = Тип("Структура") И СодержимоеПромпта.Свойство("messages") Тогда
549 | // Результат уже в формате MCP с полем messages
550 | Если ТипЗнч(СодержимоеПромпта.messages) = Тип("Массив") Тогда
551 | Сообщения = СодержимоеПромпта.messages;
552 | КонецЕсли;
553 | ИначеЕсли ТипЗнч(СодержимоеПромпта) = Тип("Массив") Тогда
554 | // Результат - массив сообщений
555 | Сообщения = СодержимоеПромпта;
556 | Иначе
557 | // Результат - одиночное значение, создаем сообщение пользователя
558 | Сообщение = Новый Структура;
559 | Сообщение.Вставить("role", "user");
560 |
561 | СодержимоеСообщения = Новый Структура;
562 | СодержимоеСообщения.Вставить("type", "text");
563 | СодержимоеСообщения.Вставить("text", Строка(СодержимоеПромпта));
564 |
565 | Сообщение.Вставить("content", СодержимоеСообщения);
566 | Сообщения.Добавить(Сообщение);
567 | КонецЕсли;
568 |
569 | Результат.Вставить("description", ОписаниеПромпта);
570 |
571 | Исключение
572 | ИнформацияОбОшибке = ИнформацияОбОшибке();
573 | ТекстОшибки = СтрШаблон("Ошибка получения промпта '%1': %2",
574 | ИмяПромпта,
575 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
576 | ВызватьИсключение ТекстОшибки;
577 | КонецПопытки;
578 |
579 | Результат.Вставить("messages", Сообщения);
580 |
581 | Возврат Результат;
582 | КонецФункции
583 |
584 | #КонецОбласти
585 |
586 | #Область ВспомогательныеМетоды
587 |
588 | Функция СформироватьОтветОшибку(КодОшибки, СообщениеОшибки, ИдентификаторЗапроса)
589 | ОтветОшибка = Новый Структура;
590 | ОтветОшибка.Вставить("jsonrpc", "2.0");
591 | ОтветОшибка.Вставить("id", ИдентификаторЗапроса);
592 |
593 | Ошибка = Новый Структура;
594 | Ошибка.Вставить("code", КодОшибки);
595 | Ошибка.Вставить("message", СообщениеОшибки);
596 |
597 | ОтветОшибка.Вставить("error", Ошибка);
598 |
599 | Возврат ОтветОшибка;
600 | КонецФункции
601 |
602 | #КонецОбласти
603 |
604 |
--------------------------------------------------------------------------------
/src/py_server/http_server.py:
--------------------------------------------------------------------------------
1 | """HTTP-сервер с поддержкой SSE и Streamable HTTP для MCP."""
2 |
3 | import asyncio
4 | import json
5 | import logging
6 | from typing import Dict, Any, Optional
7 | from contextlib import asynccontextmanager
8 | from urllib.parse import urlencode, parse_qs
9 |
10 | from fastapi import FastAPI, Request, Response, HTTPException, Form
11 | from fastapi.responses import StreamingResponse, HTMLResponse, RedirectResponse, JSONResponse
12 | from fastapi.middleware.cors import CORSMiddleware
13 | import uvicorn
14 | import httpx
15 |
16 | from mcp.server.sse import SseServerTransport
17 | from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
18 | from mcp.server.models import InitializationOptions
19 | from starlette.applications import Starlette
20 | from starlette.routing import Mount, Route
21 | from starlette.types import Scope, Receive, Send
22 | from starlette.middleware.base import BaseHTTPMiddleware
23 |
24 | from .mcp_server import MCPProxy, current_onec_credentials
25 | from .config import Config
26 | from .auth import OAuth2Service, OAuth2Store
27 |
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 |
32 | class OAuth2BearerMiddleware(BaseHTTPMiddleware):
33 | """Middleware для проверки Bearer токенов в режиме OAuth2."""
34 |
35 | def __init__(self, app, oauth2_service: Optional[OAuth2Service], auth_mode: str):
36 | super().__init__(app)
37 | self.oauth2_service = oauth2_service
38 | self.auth_mode = auth_mode
39 | self.protected_paths = ["/mcp/", "/sse"]
40 |
41 | async def dispatch(self, request: Request, call_next):
42 | """Проверка авторизации для защищённых путей."""
43 | # Пропускаем, если auth_mode != oauth2
44 | if self.auth_mode != "oauth2":
45 | return await call_next(request)
46 |
47 | # Проверяем, является ли путь защищённым
48 | path = request.url.path
49 | is_protected = any(path.startswith(protected) for protected in self.protected_paths)
50 |
51 | if not is_protected:
52 | return await call_next(request)
53 |
54 | # Извлекаем Bearer token
55 | auth_header = request.headers.get("Authorization", "")
56 | if not auth_header.startswith("Bearer "):
57 | return JSONResponse(
58 | status_code=401,
59 | content={"error": "invalid_token"},
60 | headers={"WWW-Authenticate": 'Bearer error="invalid_token"'}
61 | )
62 |
63 | token = auth_header[7:] # Убираем "Bearer "
64 |
65 | # Валидируем токен (поддерживаем два формата)
66 | creds = None
67 |
68 | # 1. Простой формат: simple_base64(username:password)
69 | if token.startswith("simple_"):
70 | try:
71 | import base64
72 | creds_string = base64.b64decode(token[7:]).decode()
73 | username, password = creds_string.split(":", 1)
74 | creds = (username, password)
75 | logger.debug(f"Простой токен валидирован для пользователя: {username}")
76 | except Exception as e:
77 | logger.warning(f"Ошибка декодирования простого токена: {e}")
78 | creds = None
79 |
80 | # 2. OAuth2 формат: через хранилище
81 | if not creds:
82 | creds = self.oauth2_service.validate_access_token(token)
83 |
84 | if not creds:
85 | return JSONResponse(
86 | status_code=401,
87 | content={"error": "invalid_token"},
88 | headers={"WWW-Authenticate": 'Bearer error="invalid_token"'}
89 | )
90 |
91 | # Устанавливаем креденшилы в context var для этой сессии
92 | login, password = creds
93 | current_onec_credentials.set((login, password))
94 |
95 | # Передаём управление дальше
96 | response = await call_next(request)
97 | return response
98 |
99 |
100 | class MCPHttpServer:
101 | """HTTP-сервер для MCP с поддержкой SSE и Streamable HTTP."""
102 |
103 | def __init__(self, config: Config):
104 | """Инициализация HTTP-сервера.
105 |
106 | Args:
107 | config: Конфигурация сервера
108 | """
109 | self.config = config
110 | self.mcp_proxy = MCPProxy(config)
111 |
112 | # Создаем session manager для Streamable HTTP после создания MCP прокси
113 | self.streamable_session_manager = StreamableHTTPSessionManager(self.mcp_proxy.server)
114 |
115 | # Инициализация OAuth2 (если включено)
116 | self.oauth2_store: Optional[OAuth2Store] = None
117 | self.oauth2_service: Optional[OAuth2Service] = None
118 | if config.auth_mode == "oauth2":
119 | self.oauth2_store = OAuth2Store()
120 | self.oauth2_service = OAuth2Service(
121 | self.oauth2_store,
122 | code_ttl=config.oauth2_code_ttl,
123 | access_ttl=config.oauth2_access_ttl,
124 | refresh_ttl=config.oauth2_refresh_ttl
125 | )
126 | logger.info("OAuth2 авторизация включена")
127 |
128 | self.app = FastAPI(
129 | title="1C MCP Proxy",
130 | description="MCP-прокси для взаимодействия с 1С",
131 | version=config.server_version,
132 | lifespan=self._lifespan
133 | )
134 |
135 | # Настройка CORS
136 | self.app.add_middleware(
137 | CORSMiddleware,
138 | allow_origins=config.cors_origins,
139 | allow_credentials=True,
140 | allow_methods=["*"],
141 | allow_headers=["*"],
142 | )
143 |
144 | # Добавляем OAuth2 middleware
145 | self.app.add_middleware(
146 | OAuth2BearerMiddleware,
147 | oauth2_service=self.oauth2_service,
148 | auth_mode=config.auth_mode
149 | )
150 |
151 | # Монтируем транспорты
152 | self._mount_transports()
153 |
154 | # Регистрация основных маршрутов
155 | self._register_routes()
156 |
157 | @asynccontextmanager
158 | async def _lifespan(self, app: FastAPI):
159 | """Управление жизненным циклом приложения."""
160 | logger.debug("Запуск HTTP-сервера MCP")
161 |
162 | # Запускаем задачу очистки OAuth2 токенов (если включено)
163 | if self.oauth2_store:
164 | await self.oauth2_store.start_cleanup_task(interval=60)
165 |
166 | # Запускаем session manager для Streamable HTTP
167 | async with self.streamable_session_manager.run():
168 | yield
169 |
170 | # Останавливаем задачу очистки OAuth2
171 | if self.oauth2_store:
172 | await self.oauth2_store.stop_cleanup_task()
173 |
174 | logger.debug("Остановка HTTP-сервера MCP")
175 |
176 | def _create_sse_starlette_app(self) -> Starlette:
177 | """Создание Starlette приложения для обработки SSE."""
178 | # Создаем SSE транспорт для обработки сообщений
179 | sse_transport = SseServerTransport("/messages/")
180 |
181 | async def handle_sse(request):
182 | """Обработчик SSE подключений."""
183 | logger.debug("Новое SSE подключение")
184 |
185 | try:
186 | # Подключаем SSE с использованием транспорта
187 | async with sse_transport.connect_sse(
188 | request.scope,
189 | request.receive,
190 | request._send
191 | ) as streams:
192 | # Запускаем MCP сервер с потоками
193 | await self.mcp_proxy.server.run(
194 | streams[0],
195 | streams[1],
196 | self.mcp_proxy.get_initialization_options()
197 | )
198 | except Exception as e:
199 | logger.error(f"Ошибка в SSE обработчике: {e}")
200 | raise
201 | finally:
202 | logger.debug("SSE подключение закрыто")
203 |
204 | # Создаем маршруты для Starlette приложения
205 | # Когда это приложение монтируется на /sse:
206 | # - Route("/", ...) становится GET /sse (SSE подключение)
207 | # - Mount("/messages/", ...) становится POST /sse/messages/ (отправка сообщений)
208 | routes = [
209 | Route("/", endpoint=handle_sse), # SSE endpoint: GET /sse
210 | Mount("/messages/", app=sse_transport.handle_post_message), # Messages: POST /sse/messages/
211 | ]
212 |
213 | return Starlette(routes=routes)
214 |
215 | def _create_streamable_http_asgi(self):
216 | """Создание ASGI обработчика для Streamable HTTP."""
217 |
218 | async def asgi(scope: Scope, receive: Receive, send: Send) -> None:
219 | """ASGI обработчик для Streamable HTTP соединений."""
220 | logger.debug("Новое Streamable HTTP подключение")
221 |
222 | try:
223 | # Используем правильный API handle_request для ASGI
224 | await self.streamable_session_manager.handle_request(scope, receive, send)
225 | except Exception as e:
226 | logger.error(f"Ошибка в Streamable HTTP обработчике: {e}")
227 | raise
228 | finally:
229 | logger.debug("Streamable HTTP подключение закрыто")
230 |
231 | return asgi
232 |
233 | def _mount_transports(self):
234 | """Монтирование транспортов MCP."""
235 |
236 | # Монтируем SSE транспорт на /sse
237 | sse_app = self._create_sse_starlette_app()
238 | self.app.mount("/sse", sse_app)
239 |
240 | # Монтируем Streamable HTTP транспорт на /mcp/ (с trailing slash для устранения 307 редиректов)
241 | streamable_app = self._create_streamable_http_asgi()
242 | self.app.mount("/mcp/", streamable_app)
243 |
244 | def _register_routes(self):
245 | """Регистрация основных маршрутов."""
246 |
247 | @self.app.get("/")
248 | async def root():
249 | """Корневой маршрут - перенаправляет на info."""
250 | endpoints = {
251 | "info": "/info",
252 | "health": "/health",
253 | "sse": "/sse",
254 | "streamable_http": "/mcp/"
255 | }
256 | if self.config.auth_mode == "oauth2":
257 | endpoints["oauth2"] = {
258 | "well_known_prm": "/.well-known/oauth-protected-resource",
259 | "well_known_as": "/.well-known/oauth-authorization-server",
260 | "register": "/register",
261 | "authorize": "/authorize",
262 | "token": "/token"
263 | }
264 | return {
265 | "message": "1C MCP Proxy Server",
266 | "endpoints": endpoints
267 | }
268 |
269 | @self.app.get("/info")
270 | async def info():
271 | """Информационный маршрут."""
272 | return {
273 | "name": self.config.server_name,
274 | "version": self.config.server_version,
275 | "description": "MCP-прокси для взаимодействия с 1С",
276 | "endpoints": {
277 | "sse": "/sse",
278 | "messages": "/sse/messages/",
279 | "streamable_http": "/mcp/",
280 | "health": "/health",
281 | "info": "/info"
282 | },
283 | "transports": {
284 | "sse": {
285 | "endpoint": "/sse",
286 | "messages": "/sse/messages/"
287 | },
288 | "streamable_http": {
289 | "endpoint": "/mcp/"
290 | }
291 | }
292 | }
293 |
294 | @self.app.get("/health")
295 | async def health():
296 | """Проверка здоровья сервера."""
297 | try:
298 | # Проверяем подключение к 1С через прокси
299 | if hasattr(self.mcp_proxy, 'onec_client') and self.mcp_proxy.onec_client:
300 | await self.mcp_proxy.onec_client.check_health()
301 | result = {"status": "healthy", "onec_connection": "ok"}
302 | else:
303 | result = {"status": "starting", "onec_connection": "not_initialized"}
304 |
305 | # Добавляем информацию об авторизации
306 | result["auth"] = {"mode": self.config.auth_mode}
307 | return result
308 | except Exception as e:
309 | logger.error(f"Ошибка проверки здоровья: {e}")
310 | return {
311 | "status": "unhealthy",
312 | "onec_connection": "error",
313 | "error_details": str(e),
314 | "auth": {"mode": self.config.auth_mode}
315 | }
316 |
317 | # OAuth2 endpoints (если включено)
318 | if self.config.auth_mode == "oauth2":
319 | self._register_oauth2_routes()
320 |
321 | def _register_oauth2_routes(self):
322 | """Регистрация OAuth2 маршрутов."""
323 |
324 | @self.app.get("/.well-known/oauth-protected-resource")
325 | async def well_known_prm(request: Request):
326 | """Protected Resource Metadata (RFC 9728)."""
327 | # Определяем публичный URL
328 | if self.config.public_url:
329 | public_url = self.config.public_url
330 | else:
331 | # Формируем из текущего запроса
332 | scheme = request.url.scheme
333 | netloc = request.headers.get("host", f"{request.client.host}:{request.url.port}")
334 | public_url = f"{scheme}://{netloc}"
335 |
336 | return self.oauth2_service.generate_prm_document(public_url)
337 |
338 | @self.app.get("/.well-known/oauth-authorization-server")
339 | async def well_known_as_metadata(request: Request):
340 | """Authorization Server Metadata (RFC 8414)."""
341 | # Определяем публичный URL
342 | if self.config.public_url:
343 | base_url = self.config.public_url
344 | else:
345 | # Формируем из текущего запроса
346 | scheme = request.url.scheme
347 | netloc = request.headers.get("host", f"{request.client.host}:{request.url.port}")
348 | base_url = f"{scheme}://{netloc}"
349 |
350 | return {
351 | "issuer": base_url,
352 | "authorization_endpoint": f"{base_url}/authorize",
353 | "token_endpoint": f"{base_url}/token",
354 | "registration_endpoint": f"{base_url}/register", # Добавляем registration endpoint
355 | "grant_types_supported": [
356 | "authorization_code",
357 | "refresh_token",
358 | "password" # Добавляем Password Grant для простоты
359 | ],
360 | "response_types_supported": ["code"],
361 | "code_challenge_methods_supported": ["S256"],
362 | "token_endpoint_auth_methods_supported": ["none"], # Публичный клиент, без client_secret
363 | "revocation_endpoint_auth_methods_supported": ["none"]
364 | }
365 |
366 | @self.app.post("/register")
367 | async def register_client(request: Request):
368 | """Dynamic Client Registration (RFC 7591) - упрощённая версия.
369 |
370 | Всегда возвращает фиксированный client_id для публичного клиента.
371 | Игнорирует параметры регистрации, т.к. у нас нет реальной БД клиентов.
372 | """
373 | # Читаем тело запроса (но не используем, т.к. всё равно вернём фиксированные данные)
374 | try:
375 | body = await request.json()
376 | logger.debug(f"Client registration request: {body}")
377 | except:
378 | body = {}
379 |
380 | # Определяем публичный URL для redirect_uris
381 | if self.config.public_url:
382 | base_url = self.config.public_url
383 | else:
384 | scheme = request.url.scheme
385 | netloc = request.headers.get("host", f"{request.client.host}:{request.url.port}")
386 | base_url = f"{scheme}://{netloc}"
387 |
388 | # Возвращаем фиксированные данные публичного клиента
389 | client_data = {
390 | "client_id": "mcp-public-client",
391 | "client_secret": "", # Пустой для публичного клиента
392 | "client_id_issued_at": 1640000000, # Фиксированная дата
393 | "grant_types": ["authorization_code", "refresh_token", "password"],
394 | "response_types": ["code"],
395 | "redirect_uris": [
396 | f"{base_url}/callback",
397 | "http://localhost/callback",
398 | "http://127.0.0.1/callback"
399 | ],
400 | "token_endpoint_auth_method": "none", # Публичный клиент
401 | "application_type": "web"
402 | }
403 |
404 | # Если клиент передал свои redirect_uris, добавляем их
405 | if "redirect_uris" in body:
406 | for uri in body.get("redirect_uris", []):
407 | if uri not in client_data["redirect_uris"]:
408 | client_data["redirect_uris"].append(uri)
409 |
410 | logger.info(f"Client registration: вернули фиксированный client_id='mcp-public-client'")
411 |
412 | return client_data
413 |
414 | @self.app.get("/authorize")
415 | async def authorize_get(
416 | request: Request,
417 | response_type: str = None,
418 | client_id: str = None,
419 | redirect_uri: str = None,
420 | state: str = None,
421 | code_challenge: str = None,
422 | code_challenge_method: str = None
423 | ):
424 | """Authorization endpoint - показывает форму логина."""
425 | # Валидация параметров
426 | if not all([response_type, client_id, redirect_uri, code_challenge, code_challenge_method]):
427 | return HTMLResponse(
428 | content="Ошибка
Отсутствуют обязательные параметры OAuth2
",
429 | status_code=400
430 | )
431 |
432 | if response_type != "code":
433 | return HTMLResponse(
434 | content="Ошибка
Поддерживается только response_type=code
",
435 | status_code=400
436 | )
437 |
438 | if code_challenge_method != "S256":
439 | return HTMLResponse(
440 | content="Ошибка
Поддерживается только code_challenge_method=S256
",
441 | status_code=400
442 | )
443 |
444 | # Сохраняем параметры в query для формы
445 | query_params = urlencode({
446 | "redirect_uri": redirect_uri,
447 | "state": state or "",
448 | "code_challenge": code_challenge
449 | })
450 |
451 | # HTML форма для ввода креденшилов 1С
452 | html_content = f"""
453 |
454 |
455 |
456 |
457 | Авторизация 1С MCP
458 |
468 |
469 |
470 | Вход в 1С
471 | Введите учётные данные пользователя 1С:
472 |
481 |
482 |
483 | """
484 | return HTMLResponse(content=html_content)
485 |
486 | @self.app.post("/authorize")
487 | async def authorize_post(
488 | request: Request,
489 | username: str = Form(...),
490 | password: str = Form(...),
491 | redirect_uri: str = None,
492 | state: str = None,
493 | code_challenge: str = None
494 | ):
495 | """Обработка формы логина и выдача authorization code."""
496 | if not all([redirect_uri, code_challenge]):
497 | return HTMLResponse(
498 | content="Ошибка
Отсутствуют обязательные параметры
",
499 | status_code=400
500 | )
501 |
502 | # Валидация креденшилов через вызов к 1С health endpoint
503 | try:
504 | async with httpx.AsyncClient(timeout=10.0) as client:
505 | health_url = f"{self.config.onec_url}/hs/{self.config.onec_service_root}/health"
506 | response = await client.get(
507 | health_url,
508 | auth=httpx.BasicAuth(username, password)
509 | )
510 |
511 | if response.status_code != 200:
512 | # Неверные креденшилы
513 | error_html = f"""
514 |
515 |
516 |
517 |
518 | Ошибка авторизации
519 |
524 |
525 |
526 | Ошибка авторизации
527 | Неверный логин или пароль 1С
528 | ← Вернуться назад
529 |
530 |
531 | """
532 | return HTMLResponse(content=error_html, status_code=401)
533 | except Exception as e:
534 | logger.error(f"Ошибка проверки креденшилов 1С: {e}")
535 | return HTMLResponse(
536 | content=f"Ошибка
Не удалось подключиться к 1С: {e}
",
537 | status_code=503
538 | )
539 |
540 | # Генерируем authorization code
541 | code = self.oauth2_service.generate_authorization_code(
542 | login=username,
543 | password=password,
544 | redirect_uri=redirect_uri,
545 | code_challenge=code_challenge
546 | )
547 |
548 | # Формируем redirect URL
549 | params = {"code": code}
550 | if state:
551 | params["state"] = state
552 |
553 | redirect_url = f"{redirect_uri}?{urlencode(params)}"
554 |
555 | logger.info(f"Authorization code выдан для пользователя {username}, redirect: {redirect_uri}")
556 | return RedirectResponse(url=redirect_url, status_code=302)
557 |
558 | @self.app.post("/token")
559 | async def token_endpoint(
560 | request: Request,
561 | grant_type: str = Form(...),
562 | code: str = Form(None),
563 | redirect_uri: str = Form(None),
564 | code_verifier: str = Form(None),
565 | refresh_token: str = Form(None),
566 | username: str = Form(None),
567 | password: str = Form(None)
568 | ):
569 | """Token endpoint для обмена code на токены, refresh или password grant."""
570 |
571 | # Password Grant - самый простой вариант
572 | if grant_type == "password":
573 | if not username or not password:
574 | return JSONResponse(
575 | status_code=400,
576 | content={"error": "invalid_request", "error_description": "Missing username or password"}
577 | )
578 |
579 | # Валидация креденшилов через 1С
580 | try:
581 | async with httpx.AsyncClient(timeout=10.0) as client:
582 | health_url = f"{self.config.onec_url}/hs/{self.config.onec_service_root}/health"
583 | response = await client.get(
584 | health_url,
585 | auth=httpx.BasicAuth(username, password)
586 | )
587 |
588 | if response.status_code != 200:
589 | return JSONResponse(
590 | status_code=400,
591 | content={"error": "invalid_grant", "error_description": "Invalid username or password"}
592 | )
593 | except Exception as e:
594 | logger.error(f"Ошибка проверки креденшилов 1С для password grant: {e}")
595 | return JSONResponse(
596 | status_code=503,
597 | content={"error": "server_error", "error_description": "Unable to validate credentials"}
598 | )
599 |
600 | # Генерируем простой токен (base64 от username:password с префиксом)
601 | import base64
602 | creds_string = f"{username}:{password}"
603 | simple_token = "simple_" + base64.b64encode(creds_string.encode()).decode()
604 |
605 | logger.info(f"Password grant выдан для пользователя {username}")
606 |
607 | return {
608 | "access_token": simple_token,
609 | "token_type": "Bearer",
610 | "expires_in": 86400, # 24 часа (для простоты)
611 | "scope": "mcp"
612 | }
613 |
614 | # Authorization Code Grant
615 | if grant_type == "authorization_code":
616 | # Обмен code на токены
617 | if not all([code, redirect_uri, code_verifier]):
618 | return JSONResponse(
619 | status_code=400,
620 | content={"error": "invalid_request", "error_description": "Missing required parameters"}
621 | )
622 |
623 | result = self.oauth2_service.exchange_code_for_tokens(code, redirect_uri, code_verifier)
624 | if not result:
625 | return JSONResponse(
626 | status_code=400,
627 | content={"error": "invalid_grant", "error_description": "Invalid or expired authorization code"}
628 | )
629 |
630 | access_token, token_type, expires_in, refresh = result
631 | return {
632 | "access_token": access_token,
633 | "token_type": token_type,
634 | "expires_in": expires_in,
635 | "refresh_token": refresh,
636 | "scope": "mcp"
637 | }
638 |
639 | elif grant_type == "refresh_token":
640 | # Обновление токенов
641 | if not refresh_token:
642 | return JSONResponse(
643 | status_code=400,
644 | content={"error": "invalid_request", "error_description": "Missing refresh_token"}
645 | )
646 |
647 | result = self.oauth2_service.refresh_tokens(refresh_token)
648 | if not result:
649 | return JSONResponse(
650 | status_code=400,
651 | content={"error": "invalid_grant", "error_description": "Invalid or expired refresh token"}
652 | )
653 |
654 | access_token, token_type, expires_in, new_refresh = result
655 | return {
656 | "access_token": access_token,
657 | "token_type": token_type,
658 | "expires_in": expires_in,
659 | "refresh_token": new_refresh,
660 | "scope": "mcp"
661 | }
662 |
663 | else:
664 | return JSONResponse(
665 | status_code=400,
666 | content={"error": "unsupported_grant_type", "error_description": f"Grant type '{grant_type}' not supported"}
667 | )
668 |
669 | async def start(self):
670 | """Запуск HTTP-сервера."""
671 | config = uvicorn.Config(
672 | app=self.app,
673 | host=self.config.host,
674 | port=self.config.port,
675 | log_level=self.config.log_level.lower(),
676 | access_log=True
677 | )
678 |
679 | server = uvicorn.Server(config)
680 | logger.debug(f"Запуск HTTP-сервера на {self.config.host}:{self.config.port}")
681 | await server.serve()
682 |
683 |
684 | async def run_http_server(config: Config):
685 | """Запуск HTTP-сервера.
686 |
687 | Args:
688 | config: Конфигурация сервера
689 | """
690 | server = MCPHttpServer(config)
691 | await server.start()
--------------------------------------------------------------------------------