Model Context Protocol (MCP) позволяет AI-агентам взаимодействовать с внешними корпоративными системами через стандартизированный интерфейс инструментов (Tools) и ресурсов (Resources). Когда агенту предоставляется доступ к реляционной базе данных (PostgreSQL, MySQL) для выполнения аналитических выборок, валидации схемы или отладки миграций, возникает критическая задача изоляции и предотвращения аварийных ситуаций.
Агент, обладая возможностью исполнения SQL через инструмент execute_query, может случайно или в результате Prompt Injection сгенерировать деструктивные инструкции: DROP TABLE, TRUNCATE, неконтролируемый DELETE или ресурсоемкие декартовы произведения без ограничения LIMIT.
Для устранения этих векторов угроз проектируется MCP-сервер с архитектурой транзакционной песочницы (Transactional Sandbox). В основе архитектуры лежит эшелонированная защита (Defense-in-Depth):
BEGIN), переведенной в режим SET TRANSACTION READ ONLY.ROLLBACK) при завершении сессии, аварийном таймауте или разрыве сетевого соединения.Запросы от AI-клиента проходят через конвейер валидации, парсер абстрактного синтаксического дерева и пул изолированных соединений.
Пул предварительно установленных соединений с базой данных, переиспользуемых процессами для минимизации накладных расходов на TCP-хэндшейки и TLS-аутентификацию. Древовидная иерархическая структура SQL-запроса, позволяющая программно валидировать типы операторов и модифицировать структуру команд (например, принудительно инжектировать LIMIT). Изолированный контекст базы данных, в котором все операции агента выполняются в рамках транзакции без фиксации изменений (COMMIT), завершаясь гарантированным ROLLBACK.[AI Agent] -> ToolCall(execute_query)
|
v
[MCP Server Middleware] -> AST Parser: Проверка типов стейтментов (SELECT only)
|
v
[Transaction Sandbox] -> BEGIN -> SET TRANSACTION READ ONLY -> Выполнение -> JSON
|
v
(Завершение сессии / Ошибка / Disconnect) -> ROLLBACK -> DISCARD ALL -> Возврат в пулАгент инициирует вызов инструмента; сервер резервирует клиент из пула соединений и открывает транзакцию: BEGIN; SET TRANSACTION READ ONLY.
Сгенерированный SQL валидируется парсером на отсутствие DDL/DML мутаций; принудительно внедряется ограничение LIMIT 100.
Запрос исполняется в контексте изолированной транзакции; результат сериализуется в компактный JSON с защитой от переполнения памяти.
По завершении команды, таймауту или дисконнекту сервер вызывает ROLLBACK и DISCARD ALL, возвращая чистое соединение в пул.
Почему проверка SQL-запросов только регулярными выражениями или только AST-парсингом недостаточна для обеспечения безопасности базы данных при работе с LLM-агентами?
ROLLBACK.Ниже представлен пример реализации отказоустойчивого MCP-инструмента на TypeScript с использованием @modelcontextprotocol/sdk, pg и pgsql-ast-parser.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Pool, PoolClient } from "pg";
import { parse } from "pgsql-ast-parser";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000,
});
const server = new McpServer({ name: "PostgresSandboxMCP", version: "1.0.0" });
// Хранилище активных транзакционных сессий агентов
const activeSessions = new Map<string, PoolClient>();
/**
* Валидация SQL-запроса через AST-парсер.
* Блокирует любые деструктивные DDL и мутирующие DML операции.
*/
function isQuerySafe(sql: string): boolean {
try {
const ast = parse(sql);
// Проверяем, что все стейтменты в пакете представляют собой исключительно выборку данных
return ast.length > 0 && ast.every((statement) => statement.type === "select");
} catch {
return false; // При ошибке парсинга запрос безусловно отклоняется
}
}
server.tool(
"execute_read_only_query",
"Выполняет безопасный SELECT SQL-запрос к PostgreSQL базе данных с лимитом выборки.",
{
sessionId: z.string().describe("Уникальный идентификатор диалоговой сессии агента"),
sql: z.string().describe("SQL-запрос на чтение данных")
},
async ({ sessionId, sql }) => {
if (!isQuerySafe(sql)) {
return {
content: [{ type: "text", text: "Security Error: Разрешены исключительно операторы SELECT." }],
isError: true
};
}
let client = activeSessions.get(sessionId);
if (!client) {
// Инициализация песочницы: выделение клиента из пула и перевод транзакции в Read-Only
client = await pool.connect();
await client.query("BEGIN");
await client.query("SET TRANSACTION READ ONLY");
activeSessions.set(sessionId, client);
}
try {
// Принудительное ограничение размера выборки для защиты контекста модели
const result = await client.query(sql);
const rows = result.rows.slice(0, 100);
return {
content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
};
} catch (error) {
// При синтаксической ошибке транзакция в Postgres переходит в статус 'aborted'.
// Выполняем автоматическое восстановление сессии для продолжения диалога.
await client.query("ROLLBACK");
await client.query("BEGIN");
await client.query("SET TRANSACTION READ ONLY");
return {
content: [{ type: "text", text: `Postgres Execution Error: ${(error as Error).message}` }],
isError: true
};
}
}
);
// Очистка ресурсов при завершении процесса
process.on("SIGINT", async () => {
for (const [, client] of activeSessions.entries()) {
try {
await client.query("ROLLBACK");
client.release();
} catch {
// Подавление ошибок очистки при завершении
}
}
await pool.end();
process.exit(0);
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);LLM-агент выполнил запрос с опечаткой в имени таблицы. Транзакция в PostgreSQL перешла в статус 'current transaction is aborted, commands ignored until end of transaction block'. Как MCP-сервер должен обработать это состояние, чтобы агент мог продолжить работу?
Если агент аварийно прерывает сессию или клиентский процесс отключается без вызова завершающего инструмента, открытые транзакции блокируют соединения в пуле.
Решение: Настраивать параметр СУБД idle_in_transaction_session_timeout = '30s' и внедрять периодический сборщик неактивных сессий (Heartbeat Reaper) в MCP-сервере, автоматически выполняющий ROLLBACK и освобождающий соединение.
Попытка валидировать SQL через регулярные выражения (например, !/DROP|TRUNCATE|DELETE/i.test(sql)).
Архитектурное последствие: Уязвимость перед тривиальным обходом: использование вложенных комментариев /* DROP */, шестнадцатеричных строковых литералов или вызова динамического исполнения EXECUTE format(...).
Решение: Использовать формальные AST-парсеры SQL и дополнять их транзакционным ограничением SET TRANSACTION READ ONLY.
Запрос агента вида SELECT * FROM audit_logs возвращает миллионы записей, что приводит к исчерпанию оперативной памяти Node.js (Out Of Memory) и вылету за границы контекстного окна LLM.
Решение: Принудительное внедрение LIMIT 100 на этапе обхода AST-дерева и фильтрация бинарных полей до сериализации в JSON.