До появления открытого стандарта Model Context Protocol (MCP) подключение каждой языковой модели (Claude, GPT, Cursor, Antigravity) к корпоративным базам данных или API требовало написания изолированных интеграций под каждого вендора (проблема N×M интеграций). MCP выполняет роль универсального «USB Type-C для искусственного интеллекта»: единый сервер, реализованный на TypeScript с типизацией Zod, подключается ко всем совместимым IDE и хостам. Протокол строго разделяет пассивные контекстные источники (Resources) и активные исполняемые функции с побочными эффектами (Tools).
В корпоративной разработке интеграция автономных AI-агентов (Coding Agents, Data Analytics Assistants, DevOps Bots) сталкивается с тремя ключевыми барьерами:
DROP TABLE, rm -rf). Стандарт MCP вводит строгую изоляцию возможностей (Capabilities Negotiation) и позволяет хосту запрашивать ручное подтверждение пользователя (Human-in-the-loop) перед вызовом конкретного инструмента.ReadResource.Использование @modelcontextprotocol/sdk на TypeScript позволяет за несколько часов поднять надежный сервер инструментов для базы данных, файловой системы или мониторинга.
MCP построен по клиент-серверной архитектуре поверх протокола JSON-RPC 2.0. В роли клиента (Host) выступает среда разработчика (Claude Desktop, Cursor, Antigravity, VS Code), а в роли сервера — ваш локальный Node.js процесс или удаленный контейнер.
[ LLM Host: Claude / Cursor / Antigravity ]
│
├── 1. Инициализация (InitializeRequest)
│ └─ Обмен возможностями: capabilities { tools, resources }
│
├── 2. Запрос списка инструментов (tools/list)
│ └─ Сервер возвращает JSON-Schema для каждого инструмента
│
├── 3. LLM решает вызвать инструмент (tools/call)
│ │ name: "query_database"
│ │ arguments: { sql: "SELECT id, email FROM users LIMIT 10" }
│ ▼
[ MCP Server Process (@modelcontextprotocol/sdk) ]
│
├── Валидация аргументов через Zod
├── Исполнение запроса к источнику данных
└── Ответ JSON-RPC: { content: [{ type: "text", text: "..." }] }Существует два основных вида транспорта (Transports):
StdioServerTransport: процесс запускается локально клиентом и общается через стандартные потоки ввода/вывода (process.stdin / process.stdout). Это наиболее быстрый, безопасный и стандартный способ для локальных IDE.SSEServerTransport: сервер работает как HTTP-сервис с Server-Sent Events для одностороннего стриминга событий от сервера и POST-эндпоинтом для входящих запросов от удаленных агентов.Настройка TypeScript в режиме NodeNext/ESM, добавление зависимостей SDK и библиотеки валидации Zod.
Создание экземпляра Server с декларацией поддержки инструментов (tools) и контекстных ресурсов (resources).
Определение обработчиков ListResourcesRequestSchema и ReadResourceRequestSchema для отдачи схемы БД и конфигурации.
Регистрация обработчиков ListToolsRequestSchema и CallToolRequestSchema с санитизацией SQL и безопасной обработкой ошибок.
Запуск StdioServerTransport, перенаправление отладочных логов в stderr и добавление сервера в claude_desktop_config.json.
Модель формирует JSON-RPC запрос с именем инструмента и спарсенными аргументами
Хост передает сообщение в stdin процесса Node.js без блокировки UI
Сервер валидирует аргументы через safeParse; при ошибке возвращает читаемый текст
Бизнес-логика инструмента выполняет операцию в изолированном контексте
Результат упаковывается в массив { type: 'text', text: JSON.stringify(data) }
Ассистент анализирует полученные данные и формулирует итоговый ответ пользователю
Разработчик добавил в код инструмента строчку console.log('Подключение к БД успешно'). Почему после этого Claude Desktop или Cursor перестали видеть инструменты и выдали ошибку JSON-RPC parse error?
Команда проектирует MCP-сервер для мониторинга серверов. Какую операцию архитектурно правильно оформить как Resource, а какую — как Tool?
src/server.ts#Промышленная реализация сервера базы данных и аналитики на @modelcontextprotocol/sdk с поддержкой Stdio.
#!/usr/bin/env node
/**
* src/server.ts
* Производственный Model Context Protocol (MCP) сервер на TypeScript.
* Предоставляет AI-агентам доступ к схеме базы данных (Resource) и безопасным запросам (Tool).
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
ErrorCode,
McpError,
} from '@modelcontextprotocol/sdk/types.js'
import { DATABASE_TOOLS, executeQueryTool, inspectTableTool } from './tools/database-tools.js'
// Создание экземпляра сервера с указанием метаданных и поддерживаемых возможностей
const server = new Server(
{
name: 'agenticlab-database-mcp',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
)
// ── 1. Обработка ресурсов (Resources): Пассивное чтение схемы ──
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'db://schema/summary',
name: 'Схема таблиц базы данных Fullstack Agentic Lab',
description: 'Полный список таблиц, колонок, внешних ключей и индексов системы',
mimeType: 'application/json',
},
{
uri: 'config://system/env-info',
name: 'Параметры окружения сервера',
description: 'Версия Node.js, используемая база данных и статус пула соединений',
mimeType: 'text/plain',
},
],
}
})
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params
if (uri === 'db://schema/summary') {
const schemaData = {
tables: [
{
name: 'users',
columns: ['id (uuid)', 'email (varchar)', 'role (enum)', 'created_at (timestamp)'],
indexes: ['idx_users_email'],
},
{
name: 'posts',
columns: ['id (uuid)', 'title (varchar)', 'content (text)', 'published (boolean)', 'author_id (fk)'],
indexes: ['idx_posts_author'],
},
],
}
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(schemaData, null, 2),
},
],
}
}
if (uri === 'config://system/env-info') {
const envInfo = `Environment: Production\nNode.js: ${process.version}\nDatabase: MariaDB 11.4\nPool Max: 20`
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: envInfo,
},
],
}
}
throw new McpError(ErrorCode.InvalidRequest, `Запрошенный ресурс не найден: ${uri}`)
})
// ── 2. Обработка инструментов (Tools): Исполняемые действия ──
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: DATABASE_TOOLS,
}
})
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
try {
switch (name) {
case 'execute_query':
return await executeQueryTool(args)
case 'inspect_table':
return await inspectTableTool(args)
default:
throw new McpError(ErrorCode.MethodNotFound, `Неизвестный инструмент: ${name}`)
}
} catch (error: any) {
// ВАЖНО: перехватываем ошибки и возвращаем дружелюбный JSON-RPC ответ без падения процесса
console.error(`[Tool Execution Error] ${name}:`, error)
return {
isError: true,
content: [
{
type: 'text',
text: `Ошибка выполнения инструмента: ${error.message || 'Внутренний сбой'}`,
},
],
}
}
})
// ── 3. Подключение транспорта Stdio и запуск ──
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
// ВАЖНО: пишем только в stderr, чтобы не ломать поток JSON-RPC в stdout!
console.error('[AgenticLab MCP] Сервер успешно запущен через Stdio transport.')
}
main().catch((err) => {
console.error('[Fatal Startup Error]:', err)
process.exit(1)
})src/tools/database-tools.ts#Строгая валидация входящих аргументов, защита от деструктивных SQL-запросов и типизация ответов.
import { z } from 'zod'
// ── Схемы валидации аргументов инструментов ──
export const ExecuteQuerySchema = z.object({
query: z
.string()
.trim()
.min(5, 'Запрос должен быть не короче 5 символов')
.refine(
(sql) => !/^\s*(drop|truncate|delete|alter)\s+/i.test(sql),
'Опасные деструктивные операции (DROP, TRUNCATE, DELETE, ALTER) запрещены через данный инструмент!'
),
limit: z.number().int().positive().max(100).default(20),
})
export const InspectTableSchema = z.object({
tableName: z.enum(['users', 'posts', 'comments', 'settings'], {
errorMap: () => ({ message: 'Укажите существующую таблицу: users, posts, comments, settings' }),
}),
})
// ── Декларации инструментов для ListToolsRequest ──
export const DATABASE_TOOLS = [
{
name: 'execute_query',
description: 'Выполняет безопасный SELECT-запрос к базе данных и возвращает JSON-результат.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'SQL-запрос на выборку данных (SELECT ...)',
},
limit: {
type: 'number',
description: 'Максимальное количество строк (по умолчанию 20, максимум 100)',
},
},
required: ['query'],
},
},
{
name: 'inspect_table',
description: 'Инспектирует структуру колонок и количество записей в указанной таблице.',
inputSchema: {
type: 'object',
properties: {
tableName: {
type: 'string',
enum: ['users', 'posts', 'comments', 'settings'],
description: 'Имя целевой таблицы для анализа',
},
},
required: ['tableName'],
},
},
]
// ── Реализация функций-исполнителей ──
export async function executeQueryTool(args: unknown) {
const validated = ExecuteQuerySchema.parse(args)
// Имитация выполнения SQL-запроса
console.error(`[DB Query Executed]: ${validated.query} (LIMIT ${validated.limit})`)
const mockRows = [
{ id: '1', title: 'Введение в Model Context Protocol', category: 'ai-mcp', views: 1420 },
{ id: '2', title: 'Оптимизация Next.js Server Actions', category: 'nextjs', views: 890 },
].slice(0, validated.limit)
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, count: mockRows.length, rows: mockRows }, null, 2),
},
],
}
}
export async function inspectTableTool(args: unknown) {
const validated = InspectTableSchema.parse(args)
console.error(`[Inspecting Table]: ${validated.tableName}`)
const tableStats = {
tableName: validated.tableName,
estimatedRowCount: 4520,
storageSizeMB: 12.4,
indexesCount: 3,
lastVacuumDate: new Date().toISOString(),
}
return {
content: [
{
type: 'text',
text: JSON.stringify(tableStats, null, 2),
},
],
}
}package.json & tsconfig.json#Сборка ESM-модуля с флагом бинарного исполняемого файла для Node.js 20+.
{
"name": "agenticlab-mcp-server",
"version": "1.0.0",
"type": "module",
"bin": {
"agenticlab-mcp": "./build/server.js"
},
"scripts": {
"build": "tsc",
"start": "node ./build/server.js",
"dev": "tsx src/server.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^20.17.0",
"tsx": "^4.19.2",
"typescript": "^5.8.0"
}
}claude_desktop_config.json & cursor-mcp.json#Файлы подключения скомпилированного MCP-сервера к популярным средам разработки.
{
"mcpServers": {
"agenticlab-database": {
"command": "node",
"args": ["/srv/agenticlab/mcp-server/build/server.js"],
"env": {
"DATABASE_URL": "mysql://user:[email protected]:3306/agenticlab_prod",
"NODE_ENV": "production"
}
}
}
}console.log() при работе через StdioServerTransport#// КАТАСТРОФИЧЕСКАЯ ОШИБКА: сломает парсер JSON-RPC хоста
console.log('Подключение к серверу установлено')
// ПРАВИЛЬНО: логировать только в stderr
console.error('[MCP Debug] Подключение к серверу установлено')Если внутри CallToolRequestSchema выбросить ошибку и не поймать её через try/catch, весь процесс Node.js аварийно завершится (process.exit(1)). Хост потеряет связь с сервером. Ошибки должны трансформироваться в структурированный ответ с флагом isError: true.
Предоставление LLM полного доступа к execute_query без ограничений на деструктивные команды (DROP, DELETE) чревато потерей продакшн-данных при галлюцинациях модели. Обязательно используйте валидаторы Zod и права пользователя БД только на чтение (SELECT).