Привет, Хабр!
Я Сергей Жилко, руководитель продукта «Брокерское обслуживание» в Диасофт.
В этой статье расскажу, как и зачем сделал MCP (Model Context Protocol) для автоматизированной банковской системы.
Важный дисклеймер: продукт, который описан в этой статье, не является продуктом компании Диасофт. Это community-версия, которую я разработал для проверки архитектур MCP и выкладываю с целью популяризации ИИ в банках, т.к. это, пожалуй, одна из самых закрытых отраслей для использования ИИ в продакшн, особенно в закрытых контурах.
Предыстория: зачем банку MCP
Я, как правильный продакт, делал очередной цикл исследования (discovery) на тему прикладного использования ИИ-агентов в банке, если быть конкретнее, в бэк-офисе банка. В рамках интервью внезапно выявилась боль, которая подтвердилась потом еще у нескольких клиентов. Сценарий выглядел так: приходит Центробанк или другой регулятор и просит выбрать разные виды клиентов или виды операций клиента по определенным признакам, которые не получить просто в интерфейсе или с помощью готовых отчетов. Такие выборки делает разработчик и отдает бизнесу в запрашиваемом формате.
В результате анализа стало понятно, что можно попробовать сделать MCP (Model Context Protocol), т.к. запросы практически всегда разные. Казалось, что можно было бы их скормить LLM и получить результат в ожидаемом формате.
Но с нашей системой не все так просто.
Представьте, что вы – разработчик в банке. Перед вами автоматизированная банковская система от Диасофт (ранее – Diasoft FA#), которая насчитывает тысячи API. Каждое API – это хранимая процедура в MS SQL или PostgreSQL, которая принимает параметры и сессионные pAPI-таблицы на вход, а на выход заполняет другие pAPI-таблицы. (Да-да, у нас есть SOAP и REST обертки поверх. Но суть в том, что этих API – тысячи. Чтобы получить результат, надо вызвать целый каскад API).
Чтобы найти сделки клиента по ценным бумагам, нужно:
Найти клиента по ФИО (
API_Prs_FindListByFIO) -- получитьPersonIDПо
PersonIDнайти счета (API_Acc_FindListByListOwnerID) -- получитьAccountIDПо
AccountIDнайти сделки (API_DSec_FindListIDByAccID) -- получитьDealSecurityIDПо
DealSecurityIDполучить полные данные сделки (API_DealSecurity_FindListByID)
Четыре вызова, каждый со своей спецификой.
Идея заключалась в том, чтобы дать ИИ-ассистенту возможность самостоятельно находить нужные API, понимать их контракты и вызывать хранимые процедуры через MCP (Model Context Protocol). Звучит просто, да?
Первая попытка: четыре метатула и 24-мегабайтный JSON
Первая реальная попытка была вполне рабочей на бумаге. Идея та же: не регистрировать тысячи инструментов, а дать LLM четыре метатула для "ленивой" навигации:
1. list_api_categories() → список категорий ("DRSec", "CRM", "DEPO"...) 2. get_apis_in_category(cat) → API внутри категории 3. get_api_schema(cat, api) → полный контракт 4. execute_api(cat, api, ...) → вызов Api
Категории определялись по префиксу имени: API_DRSec_* → категория DRSec, API_CRM_* → CRM, API_Acc_* → Acc. Просто парсим имя процедуры по _, берем второй сегмент. Звучит логично.
Как собирались метаданные
Данные для реестра вытягивались из двух источников и склеивались в один JSON:
graph LR subgraph "Источник 1: SQL Server" SYS["sys.procedures<br/>sys.parameters"] end subgraph "Источник 2: HTML-документация" HTML["~1700 .htm файлов<br/>windows-1251"] end SYS -->|"extractMetadata.ts"| SQL_JSON["sql_metadata.json<br/>21 МБ"] HTML -->|"parseHtml.ts"| HTML_JSON["html_parsed.json<br/>15 МБ"] SQL_JSON --> MERGE["mergeRegistry.ts"] HTML_JSON --> MERGE MERGE --> REG["api_registry.json<br/>24 МБ — один файл на всё"] style REG fill:#ffe1e1
SQL-экстрактор ходил в sys.procedures и sys.parameters, вытаскивал имена хранимок, имена параметров и их SQL-типы (int, bigint, nvarchar). Никаких бизнес-описаний – только техническая мета.
HTML-парсер перебирал документацию АБС Диасофт, вытаскивал "Название", "Краткое описание", описания таблиц.
Merger пытался склеить эти два мира: взять техническую схему из SQL, обогатить бизнес-описаниями из HTML и на выходе сгенерировать JSON Schema для каждого API. Результат – один монолитный api_registry.json на 24 мегабайта, который загружался в память при старте сервера.
Что пошло не так
1. Категории по префиксу имени – это не бизнес-объекты
Группировка API_DRSec_* → "DRSec" выглядит логично, пока не сталкиваешься с реальностью. В АБС Диасофт бизнес-объект (BO) – это отдельная сущность со своим файлом BO_*.htm, визой архитектора и списком привязанных API. Но API могут называться не по BO, к которому относятся. А некоторые категории по префиксу содержали по 150+ API без какой-либо фильтрации – LLM просто тонула в этом списке.
2. SQL-типы вместо Diasoft-типов
Из sys.parameters мы получали bigint, nvarchar(255), int. Но АБС Диасофт использует свою систему типов: DSIDENTIFIER (bigint, но это ID сущности), DSVARFULLNAME (nvarchar, но это ФИО), DSMONEY (decimal, но это деньги). Информация о типах АБС Диасофт живет только в HTML-документации, и merger не всегда мог их правильно сопоставить. В итоге LLM видела "type": "integer" вместо "type": "integer", "description": "ID персоны [DSIDENTIFIER]" и не понимала семантику параметров.
3. Нет графа зависимостей
LLM видела 1700 API (без фильтрации!) и понятия не имела, что для вызова API_DSec_FindListIDByAccID нужен AccountID из API_Acc_FindListByListOwnerID, а для него –PersonID из API_Prs_FindListByFIO. Модель должна была догадываться сама по именам параметров и описаниям. Спойлер: догадывалась через раз.
4. Нет фильтрации: 1700 вместо 1098
В реестр попадали ВСЕ процедуры: запрещенные, устаревшие, нереализованные, внутренние, с визой "не ОК". Во второй версии мы фильтруем через 4-ступенчатую воронку (Find/Get/Check/Brows → Виза архитектора → Реализовано → Не запрещено) и из 3158 API оставляем 1098. В первой версии модель должна была разбираться с полным зоопарком.
5. 24 МБ JSON в памяти
Один api_registry.json на 24 мегабайта загружался целиком при старте. Перезапуск парсинга означал регенерацию всего файла. Отлаживать конкретный API – это Ctrl+F по 24-мегабайтному JSON. Во второй версии метаданные разложены по отдельным файлам: /business-objects/DSec.json, /api-services/API_DSec_FindListIDByAccID.json, /table-definitions/pAPI_Person_FindList.json.
Итог первой попытки
Архитектура с 4 метатулами была правильным направлением – lazy loading действительно решает проблему масштаба. Но реализация страдала на каждом уровне: кривая склейка метаданных, отсутствие бизнес-структуры и зависимостей между API, опасная работа с таблицами. Модель находила нужные API через раз, а когда находила – вызывала их с неправильными параметрами.
Нужно было переосмыслить не количество инструментов (четыре – это правильно), а качество метаданных и навигацию по ним.
Вторая попытка: архитектура, которая работает
Текущее решение – DiasoftMCP – состоит из двух полностью независимых модулей:
DiasoftMCP(Unofficial Community Version)/ ├── src/ # Модуль 1: Парсер документации → JSON-метаданные │ ├── parsers/ # Cheerio-парсеры для BO, API, Table .htm │ ├── pipeline/ # 10-шаговый конвейер обработки │ ├── analysis/ # Граф зависимостей, статистика │ └── output/ # JSON, отчёты, Mermaid-диаграммы │ ├── parsed-metadata/ # Результат работы парсера (JSON-файлы) │ └── mcp-server/ # Модуль 2: MCP-сервер ├── data/ # Копия метаданных (бандл) ├── src/ │ ├── tools/ # 7 MCP-инструментов │ ├── db/ # Клиенты MS SQL / PostgreSQL │ └── schema-builder.ts # Динамическая генерация JSON Schema └── tests/
Ключевая идея: парсер – это offline-инструмент, который запускается при обновлении документации. MCP-сервер – это runtime, который загружает готовые JSON и динамически строит контракты. Обновили документацию API? Перезапустили парсер, скопировали метаданные (npm run import-metadata), перезапустили сервер. Готово.
Модуль 1: Парсер документации
Что парсим
Структура документации АБС Диасофт:
Api_Fa/ ├── BObjects/ # 264 файла бизнес-объектов│ ├── BO_Person.htm │ ├── BO_DealSecurity.htm │ └── Services/ # 3 158 файлов описаний APi│ ├── API_Prs_FindListByFIO.htm │ ├── API_DSec_FindListIDByAccID.htm │ └── Tables/ │ └── T_API_*.htm # 7 807 файлов описания таблиц
Структура представляет собой таблицы без CSS-классов и ID, просто <table width=1000>. Парсим через Cheerio с предварительным декодированием через iconv-lite.
Дизайн метаданных
Метаданные спроектированы так, чтобы быть полезными для LLM на каждом уровне:
Бизнес-объект (BO) -- точка входа в domain:
{ "abbreviation": "DSec", "primaryPurpose": "Сделка с ценной бумагой", "description": "Бизнес-объект для работы со сделками...", "methods": [ { "name": "API_DSec_FindListIDByAccID", "description": "..." }, { "name": "API_DSec_FindListIDForPeriod", "description": "..." } ] }
API -- полный контракт метода:
{ "name": "API_Prs_FindListByFIO", "shortDescription": "Поиск физического лица по ФИО", "detailedDescription": "Метод осуществляет поиск ФЛ...", "inputParams": [ { "name": "SurName", "dataType": "DSVARFULLNAME", "isRequired": true } ], "inputTables": [...], "outputTables": [ { "name": "pAPI_Person_FindList", "direction": "out" } ], "errorCodes": [...] }
Таблица -- описание колонок с типами:
{ "tableName": "pAPI_Person_FindList", "direction": "out", "columns": [ { "name": "PersonID", "dataType": "DSIDENTIFIER", "description": "ID ФЛ" }, { "name": "SurName", "dataType": "DSVARFULLNAME", "description": "Фамилия" } ] }
Конвейер обработки
flowchart TD A["11 000 .htm файлов<br/>windows-1251"] --> B["iconv-lite<br/>декодирование"] B --> C["Cheerio<br/>парсинг HTML"] C --> D["264 BO"] C --> E["3 158 API"] C --> F["7 807 Tables"] D --> G["Фильтр BO<br/>Виза ГА = Ok"] E --> H["4-ступенчатый<br/>фильтр API"] H --> |"Name: Find/Get/Check/Brows"| H1["1 354"] H1 --> |"Виза ГА = Ok"| H2["1 194"] H2 --> |"Реализовано = Да"| H3["1 114"] H3 --> |"Запрещено = Нет"| H4["1 098 API"] G --> I["229 BO"] H4 --> J["Cross-reference<br/>BO ↔ API ↔ Tables"] F --> J I --> J J --> K["Overlay Merger<br/>пользовательские подсказки, в каких сценариях использовать APi.md"] K --> L["Анализ зависимостей<br/>4 уровня"] L --> M["JSON-метаданные"] L --> N["Граф зависимостей<br/>31 284 ребра"] L --> O["Отчёт + Mermaid"] style A fill:#f9d,stroke:#333 style H4 fill:#9f9,stroke:#333 style I fill:#9f9,stroke:#333 style M fill:#9df,stroke:#333 style N fill:#9df,stroke:#333
Из 3 158 API после четырехступенчатого фильтра остается 1 098 – только реализованные, одобренные архитектором, незапрещенные поисковые/read-only методы.
Граф зависимостей
Граф зависимостей – это то, чего нет ни в одной документации, и что делает все решение действительно полезным.
Парсер автоматически строит граф зависимостей между API на 4 уровнях уверенности:
1 |
HIGH |
Явное упоминание |
"Предварительно вызовите API_Prs_FindListByFIO" |
2 |
MEDIUM |
Сопоставление |
|
3 |
MEDIUM |
Семантический анализ описаний колонок |
"Идентификатор клиента" → BO Person → API_Prs_* |
4 |
LOW |
Общие имена таблиц между API |
Два API используют одну pAPI-таблицу |
Итого: 31 284 ребра в графе, 404 точки входа (API, которые можно вызвать, зная только даты, ФИО или номера счетов).
Граф позволяет LLM понять: "Чтобы вызвать API_DSec_FindListIDByAccID, мне нужен AccountID. Его можно получить из API_Acc_FindListByListOwnerID, а для него нужен PersonID из API_Prs_FindListByFIO". Вся цепочка выстраивается автоматически.
Модуль 2: MCP-сервер
Архитектура шести слоев
Когда у тебя 1098 API, нельзя просто зарегистрировать 1098 MCP-инструментов – ни одна LLM не переварит такой список. Нужна многоуровневая навигация. Мы спроектировали 6 слоев (+ 1 новый), каждый из которых решает свою задачу:
Слой 1: diasoft_list_business_objects "Какие бизнес-области существуют?" → 229 BO × ~100 символов = ~20KB Слой 2: diasoft_get_bo_apis "Какие API есть у бизнес-объекта Prs?" → 2-30 API × ~200 символов = 1-6KB Слой 3: diasoft_get_api_details "Покажи полный контракт API_Prs_FindListByFIO" → inputSchema + outputSchema + errorCodes = 1-5KB Слой 4: diasoft_search_apis "Найди API для работы со счетами" → Полнотекстовый поиск по имени, описанию, BO Слой 5: diasoft_get_dependency_chain "Что нужно вызвать перед API_DSec_FindListIDByAccID?" → upstream/downstream граф с глубиной обхода Слой 6: diasoft_execute_api "Вызови API_Prs_FindListByFIO с SurName='Иванов'" → EXEC хранимой процедуры в СУБД, маппинг результата Слой 7: diasoft_execute_chain ← NEW "Выполни цепочку Prs → Acc → DSec за один вызов" → Все шаги в одной транзакции, маппинг данных между шагами
Почему именно 7 слоев, а не 1098 инструментов?
LLM работает с MCP по принципу "tool discovery": сначала видит список доступных инструментов, потом выбирает нужный. Если дать 1098 инструментов – модель утонет.
Наш подход:
Слой 1 – каталог. LLM видит 229 бизнес-объектов, не 1098 API.
Слой 2 – зум в конкретную область. 5-30 API для выбранного BO.
Слой 3 – полный контракт. Только когда LLM решила, какой API вызвать.
Слой 4 – поиск. Когда LLM не знает, в каком BO искать.
Слой 5 – навигация. "Что вызвать до/после этого API?" (Эту фичу пока отлаживаем).
Слой 6 – исполнение. Реальный вызов хранимки.
Слой 7 – каскад вызовов в один проход (Эта фича пока не подтвердила реальную пользу).
Это как навигация в файловой системе: сначала ls /, потом ls /home/, потом cat file.txt.
Описание самих инструментов крайне важно.
Я убедился в том, что в такой архитектуре, когда LLM сама выбирает инструменты, описание самих инструментов крайне важно. Некоторые LLM пытались всегда просто звать diasoft_search_apis и угадать по моему запросу, как бы называлась API или кусочек API. Они не шли через цепочку list_business_objects → get_bo_apis. В итоге поигравшись с описаниями самих инструментов, нашел баланс на текущий момент, который позволяет спуститься по дереву инструментов, но не всегда на нем зацикливаться.
diasoft_list_business_objects |
ВСЕГДА вызывайте этот инструмент ПЕРВЫМ перед использованием diasoft_search_apis. Он предоставляет контекст обо всех доступных бизнес-доменах и помогает сделать последующие поиски более точными. |
diasoft_get_bo_apis |
Получить все доступные API для конкретного бизнес-объекта (БО). Передайте аббревиатуру БО (например, "Prs", "Acc", "DSec"), чтобы получить список API с названиями, описаниями и категориями. Используйте diasoft_list_business_objects предварительно для обнаружения доступных БО. |
diasoft_get_api_details |
Получить полные сведения о конкретном API: входные/выходные схемы, коды ошибок и имя хранимой процедуры. Передайте точное название API (например, "API_Prs_FindListByFIO"). Возвращает inputSchema (параметры и входные таблицы с типами), outputSchema (выходные таблицы с описанием столбцов), коды ошибок и зависимости. Используйте для понимания параметров, которые нужно передать в diasoft_execute_api. |
diasoft_search_apis |
Поиск API Diasoft по ключевому слову. Поиск выполняется по названиям API, описаниям и аббревиатурам БО. Используйте, когда нужно найти API, не зная точного названия или БО. Примеры запросов: "фамилия", "счет", "ценные бумаги", "FindList", "Person". |
diasoft_execute_api |
Выполнить API Diasoft, вызвав его хранимую процедуру в базе данных. Требует настроенного подключения к БД (DB_TYPE, DB_HOST и т.д. в переменных окружения). Предварительно используйте diasoft_get_api_details для понимания требуемых параметров. Передайте название API и аргументы в соответствии с inputSchema. Возвращает результаты хранимой процедуры, сопоставленные с выходными таблицами. |
diasoft_get_dependency_chain |
ВАЖНО: Вызывайте этот инструмент ПЕРЕД выполнением любого API, для которого у вас нет необходимых параметров. Он показывает, какие API нужно вызвать предварительно для получения входных данных (восходящие зависимости), а также какие API можно вызвать следующими с полученными результатами (нисходящие). Пример: для вызова API_DSec_FindListIDByAccID нужен AccountID — этот инструмент покажет, что его предоставляет API_Acc_FindListByListOwnerID. Также используйте с api_name='*', чтобы найти точки входа — API, принимающие данные, которые уже известны пользователю (имена, даты, номера документов). |
diasoft_execute_chain |
Выполнить цепочку API Diasoft в рамках одной транзакции базы данных. Удобно для многошаговых запросов (например, Клиент → Счёт → Ценные бумаги), которые иначе потребовали бы нескольких обращений к серверу. Каждый шаг может отображать строки результатов предыдущего шага во входные таблицы следующего через inputTableMapping. Все шаги разделяют одну сессию БД (SPID), поэтому сессионные таблицы видны на всех этапах цепочки. |
Динамическая генерация контрактов
Контракты MCP-инструментов не захардкожены. Они строятся на лету из метаданных.
Когда LLM вызывает diasoft_get_api_details("API_Prs_FindListByFIO"), schema-builder.ts превращает распарсенные метаданные в JSON Schema:
// Diasoft-тип → JSON Schema тип export const DIASOFT_TYPE_MAP: Record<string, { type: string; format?: string }> = { DSIDENTIFIER: { type: 'integer' }, DSDATETIME: { type: 'string', format: 'date-time' }, DSMONEY: { type: 'number' }, DSVARFULLNAME: { type: 'string' }, DSACCNUMBER: { type: 'string' }, // ... всего 50+ типов };
А на выходе LLM получает готовый контракт:
{ "inputSchema": { "type": "object", "properties": { "SurName": { "type": "string", "description": "Фамилия [DSVARFULLNAME]", "example": "Иванов Иван Иванович" }, "OnlyOpenFlag": { "type": "integer", "description": "Флаг поиска [DSTINYINT]", "example": 0 } }, "required": ["SurName"] } }
Обновили версию API в документации? Перепарсили, пересобрали метаданные – контракты обновились автоматически.
Параметры подключения к БД
Сейчас параметры подключения лежат в .env файле или передаются через переменные окружения в конфиге MCP-клиента:
{ "mcpServers": { "diasoft": { "command": "node", "args": ["dist/src/index.js"], "env": { "DB_TYPE": "mssql", "DB_HOST": "db-server.local", "DB_PASSWORD": "secret" } } } }
Да, пароль в открытом виде в конфиге. Для community-версии вполне подходит, для dev-окружения – приемлемо, но в планах – миграция на Vault для получения доступов по токену. Структура кода уже позволяет: getDbConfig() в config.ts – единственная точка чтения параметров подключения, так что рефакторинг будет минимальным.
Chain Execution: убираем лишние round-trips
Свежая фича, которую мы сейчас отлаживаем -- diasoft_execute_chain. Проблема: для поиска сделок клиента нужно 3-5 последовательных вызовов MCP, каждый из которых – это round-trip к LLM (3-5 секунд на inference). Итого выходит 15-25 секунд, из которых 80% – ожидание модели.
Решение – композитный инструмент, который выполняет всю цепочку за один round-trip:
{ "steps": [ { "api": "API_Prs_FindListByFIO", "args": { "SurName": "Михалов" } }, { "api": "API_Acc_FindListByListOwnerID", "args": { "OnlyOpenFlag": 0 }, "inputTableMapping": { "pAPI_Account_OwnerID": { "sourceStep": 0, "sourceTable": "pAPI_Person_FindList", "columnMapping": { "OwnerID": "PersonID" } } } } ] }
inputTableMapping описывает, как маппить результаты предыдущего шага на входные таблицы текущего: "возьми PersonID из выходной таблицы pAPI_Person_FindList шага 0 и положи его как OwnerID во входную таблицу pAPI_Account_OwnerID".
Все шаги выполняются в одной транзакции (один SPID), с валидацией всех API до начала работы и возвратом предварительных результатов при ошибке.
Для этого мы отрефакторили DbClient:
export interface DbClient { // Существующие методы connect(): Promise<void>; disconnect(): Promise<void>; executeStoredProc(...): Promise<SpResult>; isConnected(): boolean; // Новые: транзакционная работа для цепочек beginTransaction(): Promise<unknown>; commitTransaction(txn: unknown): Promise<void>; rollbackTransaction(txn: unknown): Promise<void>; executeStoredProcInTransaction(txn, ...): Promise<SpResult>; }
executeStoredProc стал тонкой оберткой: begin → executeStoredProcInTransaction → commit. Никакой дупликации логики.
Как это выглядит в работе
Типичный сценарий проверяли через в Claude Desktop, Windsurf, а также нашу внутреннюю платформу Digital Q.GPT:
Пользователь задает: "Найди сделки клиента Михалова по ценным бумагам"
ИИ (через MCP) проходит следующие шаги:
diasoft_list_business_objects-- обнаруживает Prs, Acc, DSec, Secdiasoft_get_dependency_chain("API_DSec_FindListIDByAccID")-- понимает цепочкуdiasoft_execute_api("API_Prs_FindListByFIO", { SurName: "Михалов" })-- находит PersonIDМаппит PersonID → OwnerID, вызывает
API_Acc_FindListByListOwnerIDМаппит AccountID, вызывает
API_DSec_FindListIDByAccIDОтдаёт результат пользователю


Стек
Парсер |
Node.js, TypeScript, Cheerio, iconv-lite |
MCP-сервер |
@modelcontextprotocol/sdk, Zod |
СУБД |
MS SQL (mssql) / Digital Q.DataBase |
Тесты |
Node.js built-in test runner |
AI-клиенты |
Claude Desktop, Windsurf, Claude Code, Diasoft Q.GPT |
Как это было сделано
Весь проект – и парсер, и MCP-сервер – создан в паре с Claude Code. От постановки задачи и проектирования архитектуры до написания кода и тестов.
Рабочий процесс выглядел так:
Я формулирую задание и контекст (какие файлы парсить, какая структура HTML).
Claude Code исследует реальные .htm файлы, находит паттерны.
Вместе проектируем план.
Я задаю десятки уточнений, корректирую план до тех пор пока он не будет идеальным. Только после этого начинаем кодить.
Claude Code пишет код, я делаю ревью и корректирую.
Тесты, исправления, следующая итерация.
Самый поучительный момент: и план, и реализацию лучше всего делать с Opus 4.6. Это дорого, но надежно. (Просто поразительно, насколько за год шагнули модели и кодинг на них!).
Что дальше
MCP через HTTP – добавить помимо STDIO также общение с MCP по HTTP и TLS, плюс работа с токенами, а также Rate Limit
Мониторинг – логирование вызовов для аудита (кто, когда, какой API, какие параметры)
Docker-деплой – multi-stage Dockerfile + docker-compose для сетевого развертывания
OpenTelemetry – traces + metrics по каждому tool call → Grafana (Tempo + Mimir) логирование вызовов для аудита (кто, когда, какой API, какие параметры)
Chain Execution – доотладить
diasoft_execute_chainна реальных цепочкахVault – убрать креденшлы из .env в HashiCorp Vault ну или любой другой
Итоги и ощущения от работы
Весь проект – от создания первой версии и до текущей стадии – занял у меня пару месяцев работы после работы. Оказалось, сложнее всего подобрать правильную архитектуру парсинга документации и обогащения документации информацией.
Повезло, конечно, что у нас сама документация на API – это не технически написанные мануалы, аналитики пишут ее с любовью и заботой. Там изначально были расписаны сценарии, как и зачем вызывать, поэтому нейронка очень редко путается и верно выбирает API.
Тем не менее, я заложил и реализовал схему с возможностью дописать Overlay поверх описания работы самой API, чтобы можно было менять выбор LLM API без изменения описания самого контракта.
Если честно, то первая версия ввела меня в уныние, и пару недель пришлось потратить на то, чтобы очистить мозг и перепридумать архитектуру. Зато момент, когда все сработало, был незабываем. Первые запросы "Найди клиента Иванова", "А какой у него адрес", а тем более, "А давай найдем депозиты Иванова" выдали верный результат, да еще и быстрый, да еще и с ответным запросом "У нас тут 15 Ивановых, какого хотим посмотреть?". Скрины в чате коллегам, отправленные в 3 ночи, надеюсь, были им приятны с утра.
Весь этот проект является сommunity-версией MCP, т.е. неофициальной версией. Она задумывалась мной, во-первых, ради проверки концепции по архитектурам MCP, во-вторых, для популяризации ИИ в банковской отрасли, т.к. нет ничего лучше, чем показать работающий результат внутри, а не картинки на дашбордах.
Если есть желание, чтобы community-версия заработала на вашей версии АБС от Диасофт (Diasoft #FA), то нужно запросить у поддержки Диасофт документацию по продуктам, положить ее в каталог, сконвертировать и настроить MCP.
Overlay
Overlay выглядит как инъекция в описание контракта API с двумя посылами "Когда использовать" и "Важно".
В секции "Важно" оказалось важным написать, чем эта API отличается от другой, очень похожей, но почему-то созданной с другими целями. Это дает возможность нейронке точнее выбрать, какую API выбрать для конкретного запроса.
## Когда использовать
- Получение остатка по счёту
- Для определения текущего баланса
## Важно- Принимает AccountID- Для оборотов — API_Acc_GetRestTurn- Для справочной информации о счёте — API_Acc_FindByID- Отличие от API_Acc_FindByID: FindByID возвращает справочные данные, GetRest — остаток
Ссылка на GitHub
Community-версия доступна на GitHub: https://github.com/szhilko196/MCP_DiasoftFA-unofficial-
Больше технической информации стараюсь отражать в README.
Выводы
Из двух попыток извлек следующие уроки:
Lazy loading – правильный паттерн, но дьявол кроется в деталях. Идея с метатулами работает. Но если метаданные кривые, навигация по префиксам имен, а зависимости между API не прослеживаются – LLM будет блуждать вслепую. Качество метаданных важнее количества инструментов.
Разделяй парсинг и runtime. Документация – это offline-процесс. MCP-сервер – это runtime. Смешивать их – гарантированная боль. Перепарсил метаданные, скопировал JSON, перезапустил сервер – готово.
Многоуровневая навигация – не опция, а необходимость. При тысячах API единственный способ дать LLM эффективно работать – это каскад: каталог → область → контракт → зависимости → вызов. Бизнес-объекты как навигационный слой, а не категории по префиксу имени.
Банковские системы славятся своей консервативностью: HTML в windows-1251, хранимые процедуры часто предпочитают вместо REST, сессионные таблицы вместо JSON-ответов. Но оказалось, что даже поверх всего этого можно построить работающий ИИ-интерфейс.