СИСТЕМА КОМПЬЮТЕРНОГО ДОСТУПА

Создание вашего первого MCP-сервера


Model Context Protocol (MCP) — это наиболее близкий к универсальному адаптеру инструмент в экосистеме ИИ. Он определяет, как ИИ-модели обнаруживают и вызывают инструменты, и как эти инструменты возвращают структурированные результаты. Как только MCP-сервер запущен, любой совместимый клиент — Claude, Claude Code или любой агентный фреймворк, поддерживающий MCP — может использовать его без дополнительного связующего кода.

В этом руководстве рассматривается создание настоящего MCP-сервера с нуля. Не игрушечный пример, а паттерн, который можно расширить для производственного использования.

Что такое MCP на самом деле

MCP — это протокол JSON-RPC 2.0 поверх stdio (или HTTP+SSE для удалённых серверов). Клиент и сервер обмениваются небольшим набором типов сообщений:

  • tools/list — клиент запрашивает доступные инструменты
  • tools/call — клиент вызывает инструмент с аргументами
  • resources/list / resources/read — клиент читает файлы, базы данных или любой контекст, предоставляемый сервером

Сервер объявляет свои инструменты заранее с JSON Schema для входных данных каждого инструмента. Клиент использует эту схему для создания корректных вызовов. Всё взаимодействие является безсостоятельным с точки зрения клиента — сервер управляет любым необходимым состоянием внутренне.

Эта простота — и есть суть. MCP — не фреймворк. Это контракт, позволяющий вам обернуть что угодно — базу данных Postgres, кластер Kubernetes, репозиторий GitHub, проприетарный внутренний API — и предоставить это как набор типизированных, вызываемых функций, которые может использовать любая ИИ-модель.

Настройка

Вам понадобится Node.js 18+ и TypeScript-проект. Официальный SDK MCP делает шаблонный код минимальным:

Terminal window
mkdir mcp-server && cd mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Добавьте tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"outDir": "./dist"
},
"include": ["src"]
}

И стартовый скрипт в package.json:

{
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}

Создание сервера

Создайте src/index.ts. Структура всегда одинакова: инициализируйте сервер, зарегистрируйте инструменты, подключите к транспорту stdio.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-mcp-server",
version: "1.0.0",
});

Теперь зарегистрируйте инструменты. Каждый инструмент имеет название, описание, которое модель использует для определения, когда его вызывать, и входную схему:

server.tool(
"get_weather",
"Get current weather conditions for a city",
{
city: z.string().describe("The city name to get weather for"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
// Your actual implementation here
const data = await fetchWeather(city, units);
return {
content: [
{
type: "text",
text: `Weather in ${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`,
},
],
};
}
);

Подключите сервер к транспорту stdio:

const transport = new StdioServerTransport();
await server.connect(transport);

Это полный каркас сервера. Всё остальное — добавление инструментов.

Паттерны проектирования инструментов

Качество вашего MCP-сервера определяется тем, насколько хорошо вы проектируете инструменты. Несколько важных паттернов:

Узкие, составные инструменты вместо широких монолитов

Инструмент, делающий одно дело, полезнее того, что делает многое. Модель может комбинировать узкие инструменты неожиданными способами; широкий инструмент ограничивает её.

Избегайте этого:

server.tool("manage_database", "Do anything with the database", {
operation: z.enum(["read", "write", "delete", "schema"]),
query: z.string(),
// ... many optional params
})

Предпочитайте это:

server.tool("query_records", "Run a SELECT query and return rows as JSON", {
table: z.string(),
where: z.string().optional().describe("SQL WHERE clause, e.g. 'status = active'"),
limit: z.number().default(50),
})
server.tool("get_schema", "Return the column definitions for a table", {
table: z.string(),
})

Точно описывайте входные данные

Модель читает ваши строки describe(), чтобы знать, что передавать. Относитесь к ним как к документации для разработчика, который никогда не видел вашу кодовую базу:

{
date_range: z.string().describe(
"ISO 8601 date range in the format YYYY-MM-DD/YYYY-MM-DD. " +
"Example: 2025-01-01/2025-03-31"
),
}

Расплывчатые описания приводят к неверным входным данным. Точные описания приводят к корректным.

Возвращайте структурированный текст, а не чистый JSON

Модели разбирают текст надёжнее, чем сырые JSON-блобы в результатах инструментов. Форматируйте вывод:

return {
content: [
{
type: "text",
text: [
`Found ${rows.length} records:`,
...rows.map(r => `- ${r.id}: ${r.name} (${r.status})`),
].join("\n"),
},
],
};

Если вызывающей стороне действительно нужны структурированные данные для дальнейшей обработки, вставьте JSON внутрь чётко помеченного текстового блока.

Обработка ошибок

Возвращайте ошибки как результаты инструмента, а не выбрасывайте исключения. SDK MCP преобразует выброшенные ошибки в ошибки уровня протокола, которые клиенты обрабатывают иначе, чем ошибки уровня инструмента:

async ({ table, where }) => {
try {
const rows = await db.query(table, where);
return { content: [{ type: "text", text: formatRows(rows) }] };
} catch (err) {
return {
content: [
{
type: "text",
text: `Query failed: ${err instanceof Error ? err.message : "unknown error"}. ` +
`Check that the table name is correct and the WHERE clause uses valid column names.`,
},
],
isError: true,
};
}
}

Флаг isError: true сигнализирует клиенту, что результат представляет сбой, позволяя агенту решить, как восстановиться.

Реальный пример: сервер инструментов GitHub

Вот конкретный инструмент, оборачивающий GitHub API, чтобы агент мог читать содержимое репозитория:

import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
server.tool(
"get_file_contents",
"Read the contents of a file from a GitHub repository",
{
owner: z.string().describe("Repository owner (username or org)"),
repo: z.string().describe("Repository name"),
path: z.string().describe("File path within the repository"),
ref: z.string().optional().describe("Branch, tag, or commit SHA. Defaults to the default branch."),
},
async ({ owner, repo, path, ref }) => {
try {
const { data } = await octokit.rest.repos.getContent({
owner,
repo,
path,
ref,
});
if (Array.isArray(data)) {
// It's a directory — return listing
const listing = data.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.name}`).join("\n");
return {
content: [{ type: "text", text: `Directory listing for ${path}:\n${listing}` }],
};
}
if (data.type !== "file" || !data.content) {
return {
content: [{ type: "text", text: `${path} is not a readable file` }],
isError: true,
};
}
const content = Buffer.from(data.content, "base64").toString("utf-8");
return {
content: [
{
type: "text",
text: `Contents of ${owner}/${repo}/${path}:\n\`\`\`\n${content}\n\`\`\``,
},
],
};
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error";
return {
content: [{ type: "text", text: `Failed to read ${path}: ${message}` }],
isError: true,
};
}
}
);

Это полный, готовый к производству инструмент. Он обрабатывает директории, ошибки и кодировку — и оборачивает результат в текст, который легко читать и цитировать модели.

Подключение к Claude Desktop

Как только ваш сервер запущен, подключите его к Claude Desktop, отредактировав конфигурацию в ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) или %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"GITHUB_TOKEN": "your_token_here"
}
}
}
}

Перезапустите Claude Desktop. Ваши инструменты появятся в селекторе инструментов автоматически.

Для разработки используйте tsx напрямую:

{
"mcpServers": {
"my-server-dev": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/src/index.ts"]
}
}
}

Подключение к Claude Code

MCP-серверы подключаются к Claude Code через CLI:

Terminal window
claude mcp add my-server node /absolute/path/to/dist/index.js

Или с переменными окружения:

Terminal window
claude mcp add my-server --env GITHUB_TOKEN=your_token node /path/to/dist/index.js

Claude Code автоматически будет использовать ваши инструменты при работе над задачами, которым они полезны. Дополнительных промптов не требуется.

Что строить дальше

MCP-сервер — это, по существу, граница возможностей: что агент может видеть и делать? Вопрос проектирования всегда в том, какие возможности предоставить и как безопасно их ограничить.

Несколько направлений, достойных изучения:

  • Серверы баз данных — Предоставьте доступ только для чтения к Postgres, SQLite или DynamoDB с инструментами интроспекции схемы, чтобы агент мог изучить структуру перед запросом
  • Серверы анализа кода — Оберните tree-sitter или протокол языкового сервера, чтобы дать агентам семантическое понимание кодовых баз за пределами простого чтения файлов
  • Серверы мониторинга — Соедините системы метрик (Prometheus, Datadog), чтобы агенты могли расследовать инциденты с реальными данными наблюдаемости
  • Серверы документации — Индексируйте и предоставляйте внутреннюю документацию или руководства по эксплуатации, позволяя агентам отвечать на операционные вопросы, не зная, где всё находится

Экосистема MCP ещё только формируется. Большинство полезных серверов ещё не созданы. Паттерн достаточно прост, что всё, к чему вы сейчас обращаетесь через дашборд или API, является разумным кандидатом для MCP-сервера.

Создайте тот, который решает реальную проблему в вашей среде, и протокол позаботится об остальном.


Связанные статьи