Как создать MCP-сервер: пример на Python и TypeScript
Коротко: MCP-сервер — это небольшая программа на официальном SDK, которая объявляет инструменты: имя, описание и параметры. На Python рабочий сервер занимает десяток строк с библиотекой FastMCP, на TypeScript — чуть больше. Начинают с локального сервера (stdio), проверяют его в MCP Inspector, подключают к Claude Code, а когда нужно отдать сервер другим людям — переводят на HTTP. Ниже — весь путь и грабли, на которые мы наступили, запуская 25 удалённых серверов в продакшене.
Сначала решите: нужен ли свой сервер
Для популярных сервисов — GitHub, Figma, Notion, браузер, поиск — готовые серверы уже есть, см. лучшие MCP-серверы. Свой пишут, когда модели нужен доступ к вашему: внутреннему API, базе, 1С, CRM, скриптам команды. Если вы только разбираетесь, что такое MCP, начните с обзора MCP-серверов.
Пример на Python (FastMCP)
Установите SDK — удобнее всего через uv:
uv init my-mcp && cd my-mcp
uv add "mcp[cli]"Файл server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()
def order_status(order_id: str) -> str:
"""Статус заказа по номеру. Используй, когда пользователь спрашивает, где его заказ."""
# здесь — запрос к вашему API или базе
return f"Заказ {order_id}: передан в доставку"
if __name__ == "__main__":
mcp.run()Вот и весь сервер. FastMCP сам превращает сигнатуру функции в схему параметров, а docstring — в описание инструмента. Именно описание модель читает, решая, звать ли инструмент, поэтому пишите в нём не «что делает функция», а когда её использовать.
Пример на TypeScript
npm init -y
npm install @modelcontextprotocol/sdk zodФайл server.ts (в package.json нужен "type": "module"):
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: 'orders', version: '1.0.0' });
server.registerTool(
'order_status',
{
description:
'Статус заказа по номеру. Используй, когда пользователь спрашивает, где его заказ.',
inputSchema: { orderId: z.string().describe('Номер заказа') },
},
async ({ orderId }) => ({
content: [{ type: 'text', text: `Заказ ${orderId}: передан в доставку` }],
}),
);
await server.connect(new StdioServerTransport());В stdio-сервере нельзя писать в stdout
print() или console.log() ломает протокол — клиент получит мусор вместо JSON. Логи пишите в stderr (console.error, logging в Python по умолчанию пишет туда же).Проверка в MCP Inspector
Прежде чем подключать сервер к клиенту, проверьте его в Inspector — официальном отладчике с веб-интерфейсом: там видно список инструментов, их схемы, и каждый можно вызвать руками.
# Python
uv run mcp dev server.py
# TypeScript и любой другой сервер
npx @modelcontextprotocol/inspector npx tsx server.tsПодключение к Claude Code и другим клиентам
Локальный сервер подключается командой запуска. В Claude Code всё, что после --, — это команда:
claude mcp add orders -- uv run --directory /path/to/my-mcp server.py
claude mcp add orders-ts -- npx tsx /path/to/server.tsВ клиентах с mcp.json — Cursor, LM Studio и других — та же команда записывается полями command и args. Где лежит конфиг у каждого: Claude, Cursor, LM Studio, Codex, OpenCode.
Из локального в удалённый: HTTP
Локальный сервер работает только у того, кто его запустил. Чтобы подключаться по ссылке — из claude.ai, с другой машины, всей командой, — сервер переводят на транспорт Streamable HTTP. В FastMCP это одна строка:
mcp.run(transport="streamable-http")В TypeScript вместо StdioServerTransport используется StreamableHTTPServerTransport внутри вашего HTTP-сервера (Express, Fastify). Вместе с HTTP появляются вопросы, которых у локального сервера не было:
| Вопрос | Что решить |
|---|---|
| Кто вызывает | Вход: OAuth 2.1, как описано в спецификации MCP, или ключ в заголовке Authorization. Claude.ai и Cursor умеют OAuth, LM Studio и CI удобнее с ключом — разумно поддержать оба |
| Состояние | Сервер без сессий (stateless) проще масштабировать: каждый запрос самодостаточен |
| Долгие вызовы | Минутные операции держат соединение открытым — нужна защита от таймаутов (см. ниже) |
| Расходы | Если инструмент стоит денег, резервируйте стоимость до вызова внешнего API, а не после |
Грабли из продакшена
Мы держим в работе 25 удалённых MCP-серверов — поиск, генерацию изображений, видео, музыки и речи. Вот чего нет в кратких инструкциях, но что стоило нам инцидентов.
1. Имя инструмента длиннее, чем кажется
Многие клиенты и модели склеивают имя сервера и имя инструмента — например, seedream-image-generation_seedream-multi-image-to-batch-image. OpenAI-совместимые API проверяют такое имя по правилу ^[a-zA-Z0-9_-]{1,64}$ и при нарушении отклоняют весь запрос, а не один инструмент. Держите имена короткими, латиницей, без точек и пробелов, и проверяйте длину вместе с префиксом сервера.
2. Молчание дольше 5 минут обрывает вызов
Генерация видео не отправляет ничего до готового результата. У популярных HTTP-клиентов на Node.js таймауты заголовков и тела по умолчанию — 300 секунд, и ровно через пять минут клиент обрывает соединение, хотя сервер продолжает работу, сохраняет файл и списывает оплату. Лечится это комментариями-keepalive в SSE-потоке (строка вида : keepalive раз в 20 секунд): парсер событий их отбрасывает, но таймеры соединения сбрасываются.
3. Без правильного 401 клиент не найдёт OAuth
Чтобы Claude или Cursor сами предложили вход, сервер на запрос без токена должен ответить 401 с заголовком WWW-Authenticate: Bearer resource_metadata="…", указывающим на /.well-known/oauth-protected-resource, а тот — на сервер авторизации с динамической регистрацией клиентов и PKCE. Пропустите заголовок — и клиент просто покажет «ошибка подключения».
4. Описание — это промпт
Модель выбирает инструмент только по описанию. Размытое «работает с заказами» приводит к лишним вызовам или к тому, что инструмент не вызывают вовсе. Пишите: когда вызывать, когда не вызывать, что вернётся и что делать при ошибке. И возвращайте ошибки текстом с подсказкой — модель прочитает и исправит параметры сама.
Если свой сервер нужен ради поиска или генерации
Частые вопросы
Как создать свой MCP-сервер?
@mcp.tool() и вызова mcp.run(). Проверьте сервер в MCP Inspector (mcp dev server.py) и подключите к клиенту, например claude mcp add имя -- uv run server.py.На каком языке писать MCP-сервер?
Чем локальный MCP-сервер отличается от удалённого?
Как отладить MCP-сервер?
npx @modelcontextprotocol/inspector с командой запуска сервера, для Python — mcp dev server.py. Inspector показывает инструменты и схемы и позволяет вызвать каждый вручную. В локальном сервере логи пишите в stderr: stdout занят протоколом.Как сделать авторизацию в удалённом MCP-сервере?
WWW-Authenticate, указывающим на метаданные защищённого ресурса, а сервер авторизации поддерживает PKCE и динамическую регистрацию клиентов. Для клиентов без OAuth удобно дополнительно принимать ключ в заголовке Authorization: Bearer.Попробуйте сами
Соберите свой MCP-сервер за минуту
Отметьте нужные инструменты — ссылки коннекторов и mcp.json соберутся сами. Подключается к Claude, Cursor, LM Studio, Codex и OpenCode.
Собрать MCP-сервер