ПРАКТИКА · БАЗЫ ДАННЫХ
Подключаем AI-агента к базе данных через MCP Toolbox
Вместо универсального доступа к SQL дадим агенту три заранее определённые операции: получить заказ, показать последние заказы и построить дневную сводку. Запросы останутся на стороне сервера, параметры будут передаваться отдельно, а пользователь базы физически не сможет изменять данные.
MCP — протокол, через который AI-приложение обнаруживает и вызывает внешние инструменты. MCP Toolbox for Databases реализует серверную часть: подключается к базе, публикует описанные в конфигурации инструменты и выполняет соответствующие SQL-запросы.
Что мы собираем
Учебный контур состоит из четырёх частей:
- PostgreSQL в локальном Docker-контейнере;
- отдельная роль базы данных с правом
SELECTи режимом только для чтения; - файл
tools.yamlс тремя параметризованными запросами; - MCP Toolbox, доступный локально по адресу
http://127.0.0.1:5000/mcp/analytics.
Здесь намеренно нет инструмента наподобие execute_sql. Если разрешить модели присылать произвольный SQL, список фактических возможностей станет шире списка бизнес-задач. Предопределённые запросы дают более узкую и проверяемую границу доступа.
Что понадобится
- Docker с доступной командой
docker; curlдля загрузки бинарного файла;- Linux AMD64 для приведённой команды установки Toolbox.
Для 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"}'
Проверка считается успешной, если:
get-orderвозвращает не более одной строки с идентификатором1;list-recent-ordersвозвращает три строки в порядке от новых к старым;- сводка содержит только даты из интервала с 24 по 27 июля включительно;
- в конфигурации нет инструмента, принимающего произвольный SQL;
- роль
agent_readerне может удалить тестовую строку.
После проверки можно убрать пароль из окружения текущей оболочки:
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-инструментов в клиенте. Там должны присутствовать только:
get-order;list-recent-orders;summarize-orders-by-day.
Затем дайте агенту три проверочных задания:
- «Покажи заказ с идентификатором 2».
- «Покажи три последних заказа».
- «Сделай дневную сводку с 24 июля 2026 года включительно до 28 июля не включительно».
Оценивайте не литературное качество ответа, а журнал вызовов: выбран ли ожидаемый инструмент, переданы ли правильные параметры и совпадают ли возвращённые строки с прямым вызовом Toolbox CLI.
Дополнительно попросите: «Удалить заказ 1». Корректный контур не предоставляет подходящего инструмента. Даже если клиент или модель попытаются обойти ограничение, у роли базы нет права на удаление, а транзакции по умолчанию работают только для чтения.
Почему здесь несколько уровней защиты
Описание инструмента помогает модели выбрать правильное действие, но не является механизмом авторизации. Модель может ошибиться, а входной текст может попытаться изменить её поведение. Поэтому ограничения должны работать ниже уровня промпта:
- toolset сокращает видимый набор операций;
- предопределённый SQL исключает свободную генерацию запросов;
- позиционные параметры отделяют данные от текста SQL;
- лимиты ограничивают размер ответа;
- роль PostgreSQL запрещает запись независимо от поведения агента;
- локальный адрес уменьшает сетевую поверхность;
- тайм-аут завершает слишком долгий запрос.
Типовые ошибки
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 согласно бизнес-правилу.
Ограничения решения
- Локальный HTTP-сервер без отдельной аутентификации подходит для учебного контура на одной машине, но не для публикации в сети.
- Toolset ограничивает обнаружение инструментов клиентом, но права базы остаются обязательной последней границей.
- Параметризованный запрос защищает значения, однако имя таблицы, колонки или направление сортировки нельзя безопасно принимать как обычный SQL-параметр. Такие варианты лучше оформлять отдельными инструментами или строгим списком разрешённых значений.
- Toolbox не определяет бизнес-смысл данных. Проверка диапазонов, часового пояса, конфиденциальных колонок и допустимой детализации остаётся задачей разработчика.
- Ответ базы может содержать персональные или коммерческие данные. Перед рабочим запуском нужны маскирование, аудит, политика хранения и ограничение строк или арендаторов.
- Один читатель базы не обеспечивает изоляцию разных пользователей AI-приложения. Для многопользовательского сценария потребуются авторизация, контекст пользователя и ограничения на уровне строк.
- В примере не настроены TLS, централизованные секреты, ротация пароля, метрики и трассировка. Это учебный локальный результат, а не готовая производственная архитектура.
Как перенести подход в рабочую систему
Начните не со схемы всей базы, а со списка вопросов, которые агент действительно должен решать. Для каждого вопроса создайте отдельный инструмент с минимальным набором колонок, верхним лимитом строк и однозначным описанием параметров.
Затем создайте отдельную роль базы для приложения, разрешите ей только необходимые представления или таблицы и проверьте запрещённые действия напрямую, без модели. Секрет передавайте через менеджер секретов или защищённое окружение процесса. Сетевой MCP-сервер закрывайте аутентификацией, TLS и правилами доступа.
Наконец, журналируйте имя инструмента, время, вызывающего пользователя, параметры без секретов, длительность и число возвращённых строк. Полный результат запроса не всегда следует сохранять: журнал сам может стать копией чувствительной базы.
Что получилось
У нас есть локальный MCP-сервер, через который AI-приложение может читать тестовую PostgreSQL и выполнять ограниченный анализ. Новый вопрос внутри предусмотренных сценариев не требует писать отдельный HTTP-обработчик: агент выбирает один из описанных инструментов и передаёт структурированные параметры.
При этом свобода модели не превращается в свободу базы данных. Список операций задан конфигурацией, SQL находится на сервере, объём ответа ограничен, а роль PostgreSQL не умеет изменять данные.