ПРАКТИКА · БАЗЫ ДАННЫХ

Подключаем AI-агента к базе данных через MCP Toolbox

Уровень: средний Чтение: до 12 минут Результат: локальный MCP-сервер с инструментами только для чтения

Вместо универсального доступа к SQL дадим агенту три заранее определённые операции: получить заказ, показать последние заказы и построить дневную сводку. Запросы останутся на стороне сервера, параметры будут передаваться отдельно, а пользователь базы физически не сможет изменять данные.

MCP — протокол, через который AI-приложение обнаруживает и вызывает внешние инструменты. MCP Toolbox for Databases реализует серверную часть: подключается к базе, публикует описанные в конфигурации инструменты и выполняет соответствующие SQL-запросы.

Что мы собираем

Учебный контур состоит из четырёх частей:

  1. PostgreSQL в локальном Docker-контейнере;
  2. отдельная роль базы данных с правом SELECT и режимом только для чтения;
  3. файл tools.yaml с тремя параметризованными запросами;
  4. MCP Toolbox, доступный локально по адресу http://127.0.0.1:5000/mcp/analytics.

Здесь намеренно нет инструмента наподобие execute_sql. Если разрешить модели присылать произвольный SQL, список фактических возможностей станет шире списка бизнес-задач. Предопределённые запросы дают более узкую и проверяемую границу доступа.

Что понадобится

Для macOS, Windows и ARM нужен другой файл. Выберите его в официальном репозитории MCP Toolbox, а не переименовывайте Linux-бинарник.

Шаг 1. Запускаем тестовую PostgreSQL

Сначала убедитесь, что имя контейнера не занято:

docker ps -a --filter name=^/agentlab-postgres$

Если команда не показывает контейнер, запустите PostgreSQL. Переменные ниже относятся только к изолированному примеру:

docker run --name agentlab-postgres \
  --publish 127.0.0.1:55432:5432 \
  --env POSTGRES_DB=agentlab_demo \
  --env POSTGRES_USER=demo_admin \
  --env POSTGRES_PASSWORD=local-demo-admin-password \
  --detach postgres:16-alpine

Публикация порта на 127.0.0.1, а не на всех интерфейсах, не делает базу полностью защищённой, но не выставляет учебный порт непосредственно во внешнюю сеть.

Дождитесь готовности сервера:

docker exec agentlab-postgres \
  pg_isready --username demo_admin --dbname agentlab_demo

Продолжайте после сообщения о том, что сервер принимает соединения.

Шаг 2. Создаём таблицу и тестовые записи

Передадим SQL в psql через стандартный ввод. Команда создаёт только учебную таблицу orders внутри новой базы:

docker exec -i agentlab-postgres \
  psql --username demo_admin --dbname agentlab_demo <<'SQL'
CREATE TABLE public.orders (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_name text NOT NULL,
  status text NOT NULL CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
  total numeric(12, 2) NOT NULL CHECK (total >= 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO public.orders (customer_name, status, total, created_at) VALUES
  ('Учебный клиент А', 'paid',      12500.00, '2026-07-24 09:15:00+00'),
  ('Учебный клиент Б', 'shipped',    7900.00, '2026-07-25 11:40:00+00'),
  ('Учебный клиент В', 'new',        3200.00, '2026-07-25 14:05:00+00'),
  ('Учебный клиент Г', 'cancelled',  1800.00, '2026-07-26 08:30:00+00'),
  ('Учебный клиент Д', 'paid',       6400.00, '2026-07-27 16:20:00+00');
SQL

Проверьте количество строк:

docker exec agentlab-postgres \
  psql --username demo_admin --dbname agentlab_demo \
  --command "SELECT count(*) AS order_count FROM public.orders;"

Для приведённого набора данных значение order_count должно быть равно 5.

Шаг 3. Создаём отдельного читателя

MCP-серверу не нужны права владельца таблицы. Создадим отдельную роль, разрешим ей подключение, доступ к схеме и чтение одной таблицы. Пароль снова является только значением учебного примера:

docker exec -i agentlab-postgres \
  psql --username demo_admin --dbname agentlab_demo <<'SQL'
CREATE ROLE agent_reader
  LOGIN
  PASSWORD 'local-demo-reader-password';

GRANT CONNECT ON DATABASE agentlab_demo TO agent_reader;
GRANT USAGE ON SCHEMA public TO agent_reader;
GRANT SELECT ON TABLE public.orders TO agent_reader;

ALTER ROLE agent_reader SET default_transaction_read_only = on;
ALTER ROLE agent_reader SET statement_timeout = '5s';
SQL

default_transaction_read_only создаёт дополнительный барьер против записи, а statement_timeout ограничивает длительность зависшего запроса. Главной границей всё равно остаются выданные роли привилегии: не предоставляйте читателю INSERT, UPDATE, DELETE, CREATE или членство в привилегированных ролях.

Проверим роль напрямую:

docker exec agentlab-postgres \
  psql "postgresql://agent_reader:local-demo-reader-password@127.0.0.1:5432/agentlab_demo" \
  --command "SELECT id, status, total FROM public.orders ORDER BY id;"

Теперь негативная проверка. Она должна завершиться ошибкой режима только для чтения:

docker exec agentlab-postgres \
  psql "postgresql://agent_reader:local-demo-reader-password@127.0.0.1:5432/agentlab_demo" \
  --command "DELETE FROM public.orders WHERE id = 1;"

Ошибка здесь — ожидаемый результат. После неё повторная проверка количества должна по-прежнему вернуть пять строк.

Шаг 4. Устанавливаем MCP Toolbox

Для воспроизводимости зафиксируем версию 1.7.0. Команда ниже загружает Linux AMD64-бинарник, но не запускает загруженный файл автоматически:

curl --fail --location \
  --output toolbox \
  https://storage.googleapis.com/mcp-toolbox-for-databases/v1.7.0/linux/amd64/toolbox

chmod 0755 toolbox
./toolbox --version

В рабочем процессе дополнительно сверяйте контрольную сумму или подпись артефакта с данными конкретного официального релиза. Не заменяйте номер версии на latest: иначе повторный запуск инструкции может получить другой бинарник и поведение.

Шаг 5. Описываем разрешённые инструменты

Создайте файл tools.yaml. Пароль не записываем в YAML: Toolbox подставит его из переменной TOOLBOX_DB_PASSWORD.

kind: source
name: demo-postgres
type: postgres
host: 127.0.0.1
port: 55432
database: agentlab_demo
user: agent_reader
password: ${TOOLBOX_DB_PASSWORD}
---
kind: tool
name: get-order
type: postgres-sql
source: demo-postgres
description: >
  Возвращает один учебный заказ по точному числовому идентификатору.
  Не угадывай идентификатор и не используй инструмент для поиска по имени.
parameters:
  - name: order_id
    type: integer
    description: Положительный идентификатор заказа.
statement: |
  SELECT id, customer_name, status, total, created_at
  FROM public.orders
  WHERE id = $1;
---
kind: tool
name: list-recent-orders
type: postgres-sql
source: demo-postgres
description: >
  Возвращает последние учебные заказы. Параметр limit задаёт количество строк.
parameters:
  - name: limit
    type: integer
    description: Количество строк от 1 до 20.
statement: |
  SELECT id, customer_name, status, total, created_at
  FROM public.orders
  ORDER BY created_at DESC, id DESC
  LIMIT LEAST(GREATEST($1, 1), 20);
---
kind: tool
name: summarize-orders-by-day
type: postgres-sql
source: demo-postgres
description: >
  Строит дневную сводку заказов в заданном полуоткрытом интервале:
  дата начала включается, дата окончания не включается.
parameters:
  - name: date_from
    type: string
    description: Дата начала в формате YYYY-MM-DD.
  - name: date_to
    type: string
    description: Дата окончания в формате YYYY-MM-DD.
statement: |
  SELECT
    created_at::date AS day,
    count(*) AS orders_count,
    count(*) FILTER (WHERE status = 'paid') AS paid_count,
    sum(total) AS total_amount
  FROM public.orders
  WHERE created_at >= $1::date
    AND created_at < $2::date
  GROUP BY created_at::date
  ORDER BY day;
---
kind: toolset
name: analytics
tools:
  - get-order
  - list-recent-orders
  - summarize-orders-by-day

Значения попадают в запрос через позиционные параметры $1 и $2, а не через склейку строк. Это снижает риск SQL-инъекции. Ограничение LEAST(GREATEST(...), 20) не позволяет запросить неограниченный объём строк даже при неверном выборе модели.

Toolset analytics — отдельный список экспортируемых операций. В нём нет служебных, административных и универсальных SQL-инструментов.

Шаг 6. Проверяем инструменты без AI-клиента

Сначала изолируем базу и конфигурацию от поведения модели. Экспортируйте учебный пароль только в текущую оболочку:

export TOOLBOX_DB_PASSWORD='local-demo-reader-password'

Вызовите каждый инструмент через CLI Toolbox:

./toolbox --config tools.yaml invoke get-order \
  '{"order_id": 1}'

./toolbox --config tools.yaml invoke list-recent-orders \
  '{"limit": 3}'

./toolbox --config tools.yaml invoke summarize-orders-by-day \
  '{"date_from": "2026-07-24", "date_to": "2026-07-28"}'

Проверка считается успешной, если:

После проверки можно убрать пароль из окружения текущей оболочки:

unset TOOLBOX_DB_PASSWORD

Шаг 7. Запускаем локальный MCP-сервер

Верните переменную в окружение процесса и запустите сервер:

export TOOLBOX_DB_PASSWORD='local-demo-reader-password'
./toolbox --config tools.yaml \
  --address 127.0.0.1 \
  --port 5000 \
  --disable-reload

Флаг --address 127.0.0.1 оставляет HTTP-сервер локальным. --disable-reload фиксирует загруженную конфигурацию до перезапуска процесса: случайное изменение файла не расширит набор инструментов незаметно для оператора.

MCP-клиенту укажите адрес конкретного toolset:

{
  "mcpServers": {
    "orders-analytics": {
      "type": "http",
      "url": "http://127.0.0.1:5000/mcp/analytics"
    }
  }
}

Имя файла и точная форма клиентской конфигурации зависят от приложения. Существенная часть здесь — URL с суффиксом /analytics: клиент должен получить только три инструмента из одноимённого набора.

Проверка через AI-приложение

После подключения откройте список MCP-инструментов в клиенте. Там должны присутствовать только:

Затем дайте агенту три проверочных задания:

  1. «Покажи заказ с идентификатором 2».
  2. «Покажи три последних заказа».
  3. «Сделай дневную сводку с 24 июля 2026 года включительно до 28 июля не включительно».

Оценивайте не литературное качество ответа, а журнал вызовов: выбран ли ожидаемый инструмент, переданы ли правильные параметры и совпадают ли возвращённые строки с прямым вызовом Toolbox CLI.

Дополнительно попросите: «Удалить заказ 1». Корректный контур не предоставляет подходящего инструмента. Даже если клиент или модель попытаются обойти ограничение, у роли базы нет права на удаление, а транзакции по умолчанию работают только для чтения.

Почему здесь несколько уровней защиты

Описание инструмента помогает модели выбрать правильное действие, но не является механизмом авторизации. Модель может ошибиться, а входной текст может попытаться изменить её поведение. Поэтому ограничения должны работать ниже уровня промпта:

Типовые ошибки

Toolbox не подключается к PostgreSQL

Проверьте, что контейнер работает и порт опубликован:

docker ps --filter name=^/agentlab-postgres$
docker port agentlab-postgres
docker exec agentlab-postgres \
  pg_isready --username demo_admin --dbname agentlab_demo

В конфигурации Toolbox используется порт хоста 55432. Внутри контейнера PostgreSQL продолжает слушать 5432.

Переменная окружения не подставилась

Убедитесь, что TOOLBOX_DB_PASSWORD экспортирована в той же оболочке, из которой запускается Toolbox. Не печатайте её в диагностический лог и не добавляйте значение в репозиторий.

Ошибка синтаксиса YAML

Каждый ресурс отделён строкой ---. Отступы сделаны пробелами, а многострочные SQL-запросы начинаются после |. Табуляция и потерянный разделитель часто приводят к тому, что следующий инструмент разбирается как часть предыдущего.

Параметр даты не приводится к типу

Инструмент принимает строку, но SQL явно приводит её к date. Передавайте даты в однозначном ISO-формате YYYY-MM-DD. Не просите базу угадывать локальную запись вроде 07/08/26.

Клиент видит больше инструментов

Проверьте URL. Подключение к общему /mcp и подключение к /mcp/analytics могут публиковать разные наборы. Для этого примера нужен адрес конкретного toolset.

Запросы работают, но время сдвинуто

created_at хранится как timestamptz, а преобразование к дате зависит от часового пояса сессии PostgreSQL. Для международной системы задайте согласованный часовой пояс или явно выполняйте преобразование через AT TIME ZONE согласно бизнес-правилу.

Ограничения решения

Как перенести подход в рабочую систему

Начните не со схемы всей базы, а со списка вопросов, которые агент действительно должен решать. Для каждого вопроса создайте отдельный инструмент с минимальным набором колонок, верхним лимитом строк и однозначным описанием параметров.

Затем создайте отдельную роль базы для приложения, разрешите ей только необходимые представления или таблицы и проверьте запрещённые действия напрямую, без модели. Секрет передавайте через менеджер секретов или защищённое окружение процесса. Сетевой MCP-сервер закрывайте аутентификацией, TLS и правилами доступа.

Наконец, журналируйте имя инструмента, время, вызывающего пользователя, параметры без секретов, длительность и число возвращённых строк. Полный результат запроса не всегда следует сохранять: журнал сам может стать копией чувствительной базы.

Что получилось

У нас есть локальный MCP-сервер, через который AI-приложение может читать тестовую PostgreSQL и выполнять ограниченный анализ. Новый вопрос внутри предусмотренных сценариев не требует писать отдельный HTTP-обработчик: агент выбирает один из описанных инструментов и передаёт структурированные параметры.

При этом свобода модели не превращается в свободу базы данных. Список операций задан конфигурацией, SQL находится на сервере, объём ответа ограничен, а роль PostgreSQL не умеет изменять данные.

Официальная документация

← Все практические инструкции · Лабораторный словарь →