Привет, Хабр!

Я Сергей Жилко, руководитель продукта «Брокерское обслуживание» в Диасофт.

В этой статье расскажу, как и зачем сделал 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).

Чтобы найти сделки клиента по ценным бумагам, нужно:

  1. Найти клиента по ФИО (API_Prs_FindListByFIO) -- получить PersonID

  2. По PersonID найти счета (API_Acc_FindListByListOwnerID) -- получить AccountID

  3. По AccountID найти сделки (API_DSec_FindListIDByAccID) -- получить DealSecurityID

  4. По 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_* → категория DRSecAPI_CRM_* → CRMAPI_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-типы (intbigintnvarchar). Никаких бизнес-описаний – только техническая мета.

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 мы получали bigintnvarchar(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_* в описании метода

"Предварительно вызовите API_Prs_FindListByFIO"

2

MEDIUM

Сопоставление *ID колонок входных таблиц с выходными таблицами других API

AccountID на входе ← AccountID на выходе API_Acc

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) проходит следующие шаги:

  1. diasoft_list_business_objects -- обнаруживает Prs, Acc, DSec, Sec

  2. diasoft_get_dependency_chain("API_DSec_FindListIDByAccID") -- понимает цепочку

  3. diasoft_execute_api("API_Prs_FindListByFIO", { SurName: "Михалов" }) -- находит PersonID

  4. Маппит PersonID → OwnerID, вызывает API_Acc_FindListByListOwnerID

  5. Маппит AccountID, вызывает API_DSec_FindListIDByAccID

  6. Отдаёт результат пользователю

Вот мой пример простых запросов к MCP. Ищем Петровых в нашей тестовой базе...
Вот мой пример простых запросов к MCP. Ищем Петровых в нашей тестовой базе...
Цепочку вызовов строит примерно так, если попросить не просто сущность найти а она спрятана за цепочкой вызовов:
Цепочку вызовов строит примерно так, если попросить не просто сущность найти а она спрятана за цепочкой вызовов:

Стек

Парсер

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. От постановки задачи и проектирования архитектуры до написания кода и тестов.

Рабочий процесс выглядел так:

  1. Я формулирую задание и контекст (какие файлы парсить, какая структура HTML).

  2. Claude Code исследует реальные .htm файлы, находит паттерны.

  3. Вместе проектируем план.

  4. Я задаю десятки уточнений, корректирую план до тех пор пока он не будет идеальным. Только после этого начинаем кодить.

  5. Claude Code пишет код, я делаю ревью и корректирую.

  6. Тесты, исправления, следующая итерация.

Самый поучительный момент: и план, и реализацию лучше всего делать с Opus 4.6. Это дорого, но надежно. (Просто поразительно, насколько за год шагнули модели и кодинг на них!). 

Что дальше

  1. MCP через HTTP  добавить помимо STDIO также общение с MCP по HTTP и TLS, плюс работа с токенами, а также Rate Limit

  2. Мониторинг – логирование вызовов для аудита (кто, когда, какой API, какие параметры) 

  3. Docker-деплой – multi-stage Dockerfile + docker-compose для сетевого развертывания

  4. OpenTelemetry – traces + metrics по каждому tool call → Grafana (Tempo + Mimir) логирование вызовов для аудита (кто, когда, какой API, какие параметры) 

  5. Chain Execution – доотладить diasoft_execute_chain на реальных цепочках

  6. 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.

Выводы

Из двух попыток извлек следующие уроки: 

  1. Lazy loading – правильный паттерн, но дьявол кроется в деталях. Идея с метатулами работает. Но если метаданные кривые, навигация по префиксам имен, а зависимости между API не прослеживаются – LLM будет блуждать вслепую. Качество метаданных важнее количества инструментов.

  2. Разделяй парсинг и runtime. Документация – это offline-процесс. MCP-сервер – это runtime. Смешивать их – гарантированная боль. Перепарсил метаданные, скопировал JSON, перезапустил сервер – готово.

  3. Многоуровневая навигация – не опция, а необходимость. При тысячах API единственный способ дать LLM эффективно работать – это каскад: каталог → область → контракт → зависимости → вызов. Бизнес-объекты как навигационный слой, а не категории по префиксу имени.

Банковские системы славятся своей консервативностью: HTML в windows-1251, хранимые процедуры часто предпочитают вместо REST, сессионные таблицы вместо JSON-ответов. Но оказалось, что даже поверх всего этого можно построить работающий ИИ-интерфейс.

Комментарии (0)