MCP-серверы: как написать свой и зачем
Model Context Protocol (MCP) — это способ подключить к coding-ассистенту любые внешние сервисы: твою БД, тикеты в Linear, документацию, метрики из Prometheus. Звучит абстрактно, но в реальности — это просто маленький локальный сервер с набором tools, который Claude Code или Cursor вызывает по запросу. В этой статье я покажу, как написать свой MCP-сервер с нуля.
Зачем
У меня в работе типичный сценарий: я пишу запрос в Postgres, и Claude мне предлагает SQL. Но он не знает мою схему. Я каждый раз копировал structure dump в чат — раздражает.
Решение — MCP-сервер с одним инструментом «schema» и одним «query». Claude сам видит, что есть инструменты, сам их вызывает, когда нужно понять структуру или прочитать что-то из БД. Я в чате не пишу SQL — я пишу «найди мне топ-10 пользователей по выручке за май», и Claude сам читает схему, сам пишет запрос, сам исполняет.
Что под капотом
MCP — это JSON-RPC поверх stdio (или SSE). Сервер слушает на stdin сообщения, отвечает в stdout. Клиент (Claude Code, Cursor, Continue) запускает сервер как дочерний процесс и общается с ним.
Протокол простой: клиент запрашивает «дай мне список твоих инструментов», сервер возвращает schemas. Дальше клиент вызывает конкретный инструмент с аргументами, сервер исполняет и возвращает результат.
Минимальный сервер на Node
Anthropic выпустил официальный SDK — @modelcontextprotocol/sdk. Через него скелет сервера — десяток строк.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{ name: 'my-postgres', version: '0.1.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'list_tables',
description: 'Вернуть список таблиц в текущей БД',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'describe_table',
description: 'Вернуть структуру таблицы',
inputSchema: {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === 'list_tables') {
const rows = await db.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
return { content: [{ type: 'text', text: JSON.stringify(rows) }] };
}
if (req.params.name === 'describe_table') {
const name = req.params.arguments?.name;
const rows = await db.query(
"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1",
[name]
);
return { content: [{ type: 'text', text: JSON.stringify(rows) }] };
}
throw new Error('unknown tool');
});
const transport = new StdioServerTransport();
await server.connect(transport);Запускается через node my-server.mjs. Никаких портов, никакого http — общение через stdin/stdout.
Подключение к Claude Code
В .claude/mcp.json в корне проекта добавляешь:
{
"mcpServers": {
"my-postgres": {
"command": "node",
"args": ["./tools/mcp-postgres.mjs"],
"env": { "DATABASE_URL": "$DATABASE_URL" }
}
}
}Перезапускаешь Claude Code, и в /mcp видишь подключённый сервер с двумя инструментами. Дальше Claude сам решает, когда их вызвать. Я ничего больше не делаю.
Что я подключил у себя
- postgres-mcp — описано выше.
- linear-mcp — читает мои задачи и комменты, может создавать новые, обновлять статусы.
- company-docs-mcp — мой собственный сервер, который ходит во внутреннюю Confluence и отдаёт страницы по запросу.
- k8s-mcp — kubectl с белым списком команд (только read-only).
company-docs и k8s — самописные. Linear и postgres беру из готовых пакетов, они есть в @modelcontextprotocol/servers.
Безопасность
Ключевое: MCP-сервер запускается в окружении пользователя и имеет доступ ко всему, к чему ты сам имеешь. Если в инструменте написать «exec любую команду» — это и будет «exec любую команду». Я держу два правила:
- Не делать инструменты с побочными эффектами без явного подтверждения. Удалить запись в БД, отправить письмо, выкатить деплой — только через подтверждение пользователя.
- Read-only по умолчанию. Все query-инструменты в моих серверах работают через read-only пользователя БД с правами SELECT.
Стандартные грабли
- stdout — это канал общения с клиентом. Никаких console.log в свой код, иначе клиент сходит с ума. Все логи направляй в stderr или в файл.
- Аргументы tool — JSON. Их structure определяется inputSchema. Если schema говорит «name обязательно», а ты не передал — Claude может вызвать неправильно. Внимательно описывай schemas.
- Большие ответы. Если инструмент вернёт 100 КБ JSON, Claude его съест и расход токенов будет огромный. Делай пагинацию, давай сводки.
Зачем это всё
Без MCP я каждый раз руками копирую данные в чат. С MCP coding-ассистент имеет «глаза» в моих системах. Это качественный скачок: ассистент перестаёт быть генератором кода и становится оператором, который сам ходит в твою инфраструктуру.
Главное — не превращай MCP в комбайн. Один сервер — одна область. Не пытайся делать «универсальный» сервер с 30 инструментами под всё. Лучше пять серверов по 3–5 инструментов, заточенных под конкретные задачи.
Что почитать
Официальная документация — modelcontextprotocol.io. Там есть и спецификация, и SDK для нескольких языков, и каталог готовых серверов. Если у тебя уже есть Claude Code — попробуй сначала готовые, прежде чем писать свой. Своё пишется за 2–3 часа на простой случай, но готовые часто закроют 80% задач без копки.