Создание первого MCP-сервера: практическое руководство для разработчиков
Engineering Team
Что такое Model Context Protocol (MCP)?
Model Context Protocol (MCP) — это открытый стандарт, созданный компанией Anthropic, который определяет способ взаимодействия AI-приложений с внешними источниками данных и инструментами. MCP можно сравнить с USB-C для AI-интеграций: подобно тому как USB-C предоставляет единый универсальный разъём для зарядки, передачи данных и видеовывода, MCP предоставляет единый универсальный протокол для подключения AI-моделей к базам данных, API, файловым системам и любым другим сервисам. MCP устраняет необходимость создания отдельных интеграций между каждым AI-приложением и каждым инструментом.
В 2026 году MCP стал доминирующим стандартом AI-коммуникаций. По прогнозам Gartner, 40% корпоративных приложений будут включать AI-агентов к концу 2026 года, и именно MCP делает это практически возможным в масштабе. До появления MCP подключение AI-модели к N инструментам требовало N отдельных интеграций. При M AI-приложениях и N инструментах требовалось M x N адаптеров. MCP сводит это к M + N: каждое AI-приложение реализует один MCP-клиент, каждый инструмент реализует один MCP-сервер.
Архитектура MCP: хосты, клиенты, серверы и транспорты
Понимание архитектуры MCP необходимо прежде, чем писать код. Протокол определяет четыре ключевые роли:
| Компонент | Роль | Пример |
|---|---|---|
| Хост | AI-приложение, с которым взаимодействует пользователь | Claude Desktop, Cursor, VS Code Copilot |
| Клиент | Обработчик протокола MCP внутри хоста | Встроен в хост-приложение |
| Сервер | Предоставляет инструменты, ресурсы и промпты через MCP | Ваш кастомный сервер (то, что мы будем создавать) |
| Транспорт | Коммуникационный слой между клиентом и сервером | stdio, HTTP+SSE, Streamable HTTP |
Поток коммуникации работает следующим образом:
- Хост запускается и инициализирует MCP-клиент для каждого настроенного сервера.
- Клиент подключается к серверу через транспорт (stdio для локальных, HTTP для удалённых).
- Клиент отправляет запрос
initialize, согласовывая версию протокола и возможности. - Сервер отвечает списком доступных инструментов, ресурсов и промптов.
- Когда AI-модели нужны внешние данные или действия, клиент вызывает соответствующий метод сервера.
- Сервер выполняет операцию и возвращает результаты через сообщения JSON-RPC 2.0.
Инструменты vs Ресурсы vs Промпты
MCP-серверы могут предоставлять три типа возможностей:
- Инструменты (Tools) — функции, которые AI-модель может вызывать. Они принимают структурированный ввод и возвращают структурированный вывод. Пример:
query_database(sql: string)илиsend_email(to: string, subject: string, body: string). - Ресурсы (Resources) — данные, которые AI-модель может читать. Идентифицируются URI и возвращают контент. Пример:
file:///path/to/document.mdилиpostgres://localhost/mydb/users. - Промпты (Prompts) — переиспользуемые шаблоны промптов, предоставляемые сервером. Помогают стандартизировать взаимодействие AI с доменом сервера. Пример: шаблон
summarize_ticketдля Jira MCP-сервера.
Пошагово: создание MCP-сервера на TypeScript
Построим практический MCP-сервер, предоставляющий инструмент для запросов к базе данных SQLite. Это распространённый сценарий: предоставить AI-модели безопасный доступ только на чтение к данным приложения.
Шаг 1: Инициализация проекта
mkdir mcp-sqlite-server && cd mcp-sqlite-server
npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3
npx tsc --init
Настройте tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
Шаг 2: Реализация сервера
Создайте src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import Database from "better-sqlite3";
import { z } from "zod";
// Инициализация базы данных
const db = new Database(process.env.DB_PATH || "./data.db", {
readonly: true,
});
// Создание MCP-сервера
const server = new McpServer({
name: "sqlite-query",
version: "1.0.0",
});
// Регистрация инструмента для запросов к БД
server.tool(
"query",
"Выполнить SQL-запрос только на чтение к базе данных SQLite",
{
sql: z.string().describe("SQL SELECT-запрос для выполнения"),
},
async ({ sql }) => {
// Безопасность: разрешены только SELECT-запросы
const normalized = sql.trim().toUpperCase();
if (!normalized.startsWith("SELECT")) {
return {
content: [
{
type: "text",
text: "Ошибка: разрешены только SELECT-запросы.",
},
],
isError: true,
};
}
try {
const rows = db.prepare(sql).all();
return {
content: [
{
type: "text",
text: JSON.stringify(rows, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Ошибка запроса: ${(error as Error).message}`,
},
],
isError: true,
};
}
}
);
// Регистрация инструмента для получения списка таблиц
server.tool(
"list_tables",
"Получить список всех таблиц в базе данных со схемами",
{},
async () => {
const tables = db
.prepare(
`SELECT name, sql FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name`
)
.all();
return {
content: [
{
type: "text",
text: JSON.stringify(tables, null, 2),
},
],
};
}
);
// Регистрация ресурса для схемы базы данных
server.resource(
"schema",
"sqlite://schema",
async (uri) => {
const tables = db
.prepare(
`SELECT name, sql FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'`
)
.all();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(tables, null, 2),
},
],
};
}
);
// Запуск сервера с транспортом stdio
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("SQLite MCP-сервер запущен на stdio");
}
main().catch(console.error);
Шаг 3: Сборка и локальное тестирование
npx tsc
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node dist/index.js
Вы должны увидеть JSON-RPC ответ с возможностями сервера.
Шаг 4: Тот же сервер на Python
Для Python-разработчиков — эквивалентная реализация с использованием официального MCP Python SDK:
# server.py
import sqlite3
import json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("sqlite-query")
DB_PATH = "data.db"
@mcp.tool()
def query(sql: str) -> str:
"""Выполнить SQL-запрос только на чтение к базе данных SQLite."""
normalized = sql.strip().upper()
if not normalized.startswith("SELECT"):
raise ValueError("Разрешены только SELECT-запросы.")
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
cursor = conn.execute(sql)
rows = [dict(row) for row in cursor.fetchall()]
return json.dumps(rows, indent=2, default=str)
finally:
conn.close()
@mcp.tool()
def list_tables() -> str:
"""Получить список всех таблиц в базе данных со схемами."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
cursor = conn.execute(
"SELECT name, sql FROM sqlite_master "
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
)
tables = [dict(row) for row in cursor.fetchall()]
return json.dumps(tables, indent=2)
finally:
conn.close()
@mcp.resource("sqlite://schema")
def get_schema() -> str:
"""Вернуть схему базы данных как ресурс."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
cursor = conn.execute(
"SELECT name, sql FROM sqlite_master WHERE type='table'"
)
return json.dumps([dict(r) for r in cursor.fetchall()], indent=2)
finally:
conn.close()
if __name__ == "__main__":
mcp.run(transport="stdio")
Установка зависимостей и запуск:
pip install mcp[cli]
python server.py
Подключение к Claude Desktop
Claude Desktop нативно поддерживает MCP-серверы. Для подключения отредактируйте файл конфигурации:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"sqlite-query": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"DB_PATH": "/absolute/path/to/your/data.db"
}
}
}
}
Для Python-версии:
{
"mcpServers": {
"sqlite-query": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
Перезапустите Claude Desktop. В интерфейсе чата должна появиться иконка молотка, обозначающая доступные MCP-инструменты. Теперь можно задавать Claude вопросы вроде «Какие таблицы есть в базе?» или «Покажи 10 последних пользователей по дате регистрации», и он будет использовать ваш MCP-сервер для прямых запросов к базе данных.
Подключение к Cursor
Cursor также поддерживает MCP-серверы. Добавьте конфигурацию в .cursor/mcp.json в корне проекта:
{
"mcpServers": {
"sqlite-query": {
"command": "node",
"args": ["./dist/index.js"],
"env": {
"DB_PATH": "./data.db"
}
}
}
}
После сохранения перезапустите Cursor. MCP-инструменты появятся в панели AI-ассистента и смогут вызываться при генерации и отладке кода.
Тестирование и отладка MCP-сервера
Использование MCP Inspector
MCP Inspector — официальный инструмент отладки. Он предоставляет веб-интерфейс для взаимодействия с вашим сервером:
npx @modelcontextprotocol/inspector node dist/index.js
Откроется браузерный интерфейс по адресу http://localhost:5173, где вы можете:
- Просматривать все зарегистрированные инструменты, ресурсы и промпты
- Вызывать инструменты с произвольными входными данными и проверять ответы
- Мониторить поток JSON-RPC сообщений в реальном времени
- Тестировать обработку ошибок, отправляя некорректные запросы
Модульное тестирование с SDK
Напишите автоматизированные тесты с использованием in-memory транспорта:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
describe("SQLite MCP Server", () => {
let client: Client;
beforeEach(async () => {
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const server = createServer(); // ваша фабрика серверов
await server.connect(serverTransport);
client = new Client({ name: "test", version: "1.0" });
await client.connect(clientTransport);
});
it("должен вернуть список таблиц", async () => {
const result = await client.callTool({
name: "list_tables",
arguments: {},
});
expect(result.content[0].text).toContain("users");
});
it("должен отклонить не-SELECT запросы", async () => {
const result = await client.callTool({
name: "query",
arguments: { sql: "DROP TABLE users" },
});
expect(result.isError).toBe(true);
});
});
Распространённые проблемы при отладке
| Проблема | Причина | Решение |
|---|---|---|
| Сервер не отображается в Claude Desktop | Ошибка в пути или синтаксисе JSON | Проверьте JSON, используйте абсолютные пути |
| Ошибки «Tool not found» | Сервер не зарегистрировал инструменты до подключения | Регистрируйте инструменты до вызова server.connect() |
| Таймаут при вызове инструментов | Долгие операции без отчёта о прогрессе | Добавьте уведомления о прогрессе через server.sendProgress() |
| Вывод в stderr ломает протокол | console.log пишет в stdout (stdio транспорт) | Используйте console.error() для логирования с stdio |
| Соединение обрывается при простое | Таймаут транспорта | Реализуйте heartbeat или используйте HTTP-транспорт |
Лучшие практики для продакшена
-
Валидация входных данных: всегда валидируйте и санитизируйте ввод инструментов. Используйте Zod-схемы (TypeScript) или Pydantic-модели (Python) для строгой проверки типов.
-
Только чтение по умолчанию: начинайте с доступа только на чтение. Добавляйте возможности записи только при явной необходимости, и всегда требуйте подтверждения для деструктивных операций.
-
Обработка ошибок: возвращайте структурированные сообщения об ошибках с
isError: true. Никогда не раскрывайте внутренние стек-трейсы или строки подключения к БД. -
Логирование: логируйте все вызовы инструментов с временными метками, входными данными и длительностью выполнения. Используйте stderr для логов (не stdout) при stdio-транспорте.
-
Ограничение частоты: реализуйте ограничения частоты вызовов для каждого инструмента, чтобы зацикленные AI-агенты не перегрузили бэкенд.
-
Таймауты: устанавливайте таймауты на все обработчики инструментов. AI-модель может вызвать инструмент, запускающий дорогой запрос — защитите инфраструктуру.
-
Разделение окружений: используйте переменные окружения для всей конфигурации. Никогда не хардкодьте URL баз данных, API-ключи или пути к файлам.
-
Версионирование: следуйте семантическому версионированию для MCP-сервера. Хендшейк
initializeвключает согласование версий — ломающие изменения требуют мажорной версии.
FAQ
В: Чем MCP отличается от function calling? О: Function calling (используемый OpenAI, Anthropic и другими) определяет инструменты инлайново в каждом API-запросе. MCP выносит определения инструментов в отдельные серверы, которые любой MCP-совместимый хост может обнаружить и использовать. Function calling — на уровне запроса; MCP — постоянный протокол со стейтфул-сессиями.
В: Можно ли использовать MCP с моделями, отличными от Claude? О: Да. MCP — открытый протокол. OpenAI, Google DeepMind и Microsoft внедрили поддержку MCP в свои платформы по состоянию на начало 2026 года. Любое AI-приложение, реализующее MCP-клиент, может подключиться к любому MCP-серверу.
В: MCP работает только с локальными инструментами? О: Нет. Хотя stdio-транспорт предназначен для локальных серверов, HTTP+SSE и Streamable HTTP транспорты поддерживают удалённые MCP-серверы. Вы можете развернуть MCP-серверы как облачные сервисы, доступные по сети.
В: Как MCP обрабатывает аутентификацию? О: Протокол поддерживает OAuth 2.0 для удалённых серверов. Локальные stdio-серверы наследуют контекст безопасности хост-процесса. Для корпоративных развёртываний MCP-шлюзы могут централизовать аутентификацию и авторизацию.
В: На каких языках можно строить MCP-серверы? О: Официальные SDK существуют для TypeScript, Python, Java, Kotlin, C# и Swift. Сообщество поддерживает SDK для Rust, Go, Ruby и PHP. Протокол языконезависим — любой язык, способный читать/писать JSON-RPC через stdio или HTTP, может реализовать MCP-сервер.