Опубликовано 16 июля 2026 г.
создание MCP-сервера с нуля: полный гайд на Python с кодом (2026)
Рабочий MCP-сервер на Python: два реальных инструмента, три способа тестирования, подключение к Claude Desktop и Cursor. Весь код проверен, без API-ключей.
MCP-сервер на Python пишется через официальный mcp SDK (1.28.1 на момент написания): создаёшь экземпляр FastMCP, вешаешь декоратор @mcp.tool() на обычные Python-функции и вызываешь mcp.run(). Это весь скелет. Сервер работает с Claude Desktop, Cursor, Claude Code и любым другим клиентом Model Context Protocol через stdio, а LLM внутри клиента решает, когда вызвать твои функции, глядя только на их имена, докстринги и аннотации типов.
В этом гайде собираем настоящий сервер: devsearch с двумя инструментами без единого API-ключа. Один ищет по Hacker News, второй скачивает веб-страницу с нормальной валидацией входа. Протестируем тремя способами (MCP Inspector, скриптовый клиент, живая LLM), зарегистрируем и в Claude Desktop, и в Cursor, а перед выпуском пройдёмся по короткому security-чеклисту. Каждая строка кода ниже отработала у меня на машине против SDK 1.28.1 до того, как попала в статью. Из требований только Python 3.10+.
почему твои инструменты вдруг всем нужны
Model Context Protocol вышел в ноябре 2024 как проект Anthropic со скромной идеей: один стандартный способ подключать инструменты и данные к LLM вместо отдельной интеграции под каждое приложение. На июль 2026 каталог PulseMCP насчитывает больше 22 000 серверов. Протокол приняли OpenAI, Google и Microsoft, а MCP-сервер теперь есть даже у Safari. Какой бы клиент ни выбрали твои пользователи, MCP-сервер — та интеграция, которую пишешь один раз.
На практике это значит вот что. До MCP доступ Claude к твоей базе данных означал код под tool-use API Anthropic, потом переписывание всего под ChatGPT, потом ещё раз под Cursor. Теперь пишешь один сервер, и им пользуется любой клиент, говорящий на протоколе. Текущая ревизия спецификации — 2025-11-25; SDK сам договаривается о версии, так что про неё почти не думаешь.
Сервер умеет отдавать три вида сущностей. Инструменты (tools) — функции, которые модель может вызывать («найди это», «создай то»). Ресурсы — данные только для чтения, адресуемые по URI (файл, конфиг, лог). Промпты — переиспользуемые шаблоны, которые запускает пользователь. Начинать стоит с инструментов, их и строим. Ещё одно слово, и поехали: транспорт — способ обмена JSON-RPC-сообщениями между клиентом и сервером. stdio запускает сервер как дочерний процесс клиента, идеален для локальных инструментов. Streamable HTTP обслуживает удалённых клиентов по сети. Мы берём stdio: нулевая настройка сети, и именно его ждут Claude Desktop и Cursor от локального сервера.
пять минут на окружение (uv или обычный venv)
Создай директорию проекта и поставь SDK. Покажу через обычный venv, он работает везде; если у тебя uv, то uv init && uv add "mcp[cli]" httpx делает то же самое быстрее.
# Run from wherever you keep projects:
mkdir devsearch-mcp && cd devsearch-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]" httpxЭкстра [cli] важна. Она даёт команду mcp с тремя подкомандами, которыми будешь пользоваться постоянно: mcp dev (запуск сервера под визуальным Inspector), mcp run (обычный запуск) и mcp install (автоматическая регистрация в Claude Desktop). httpx — HTTP-клиент для наших инструментов.
Проверь установку:
mcp version
# MCP version 1.28.1Если видишь более старую мажорную версию, код ниже может не совпасть. SDK развивается быстро, pip install --upgrade "mcp[cli]" решает вопрос.
сервер целиком (60 строк, два настоящих инструмента)
Большинство туториалов по MCP дают калькулятор add(a, b), который ни одной LLM в жизни не понадобится. Мы строим инструменты, которые ассистент реально вызывает каждый день: поискать на Hacker News, что практики говорят о теме, и скачать страницу, чтобы её прочитать. Оба работают на бесплатных публичных API без ключей — можно вставить и запустить.
Создай файл:
# devsearch-mcp/server.py
"""devsearch — an MCP server exposing two research tools over stdio."""
from urllib.parse import urlparse
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("devsearch")
HN_API = "https://hn.algolia.com/api/v1/search"
BLOCKED_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "169.254.169.254", "[::1]"}
MAX_CHARS = 8000
@mcp.tool()
def search_hackernews(query: str, limit: int = 5) -> str:
"""Search Hacker News stories by keyword, ranked by relevance.
Args:
query: What to search for (e.g. "MCP server").
limit: How many stories to return (1-20).
"""
query = query.strip()
if not query:
raise ValueError("query must not be empty")
limit = max(1, min(limit, 20))
r = httpx.get(
HN_API,
params={"query": query, "tags": "story", "hitsPerPage": limit},
timeout=10,
)
r.raise_for_status()
hits = r.json()["hits"]
if not hits:
return f"No Hacker News stories found for {query!r}."
lines = []
for h in hits:
title = h.get("title") or "(untitled)"
url = h.get("url") or f"https://news.ycombinator.com/item?id={h['objectID']}"
lines.append(
f"- {title} ({h.get('points', 0)} points, "
f"{h.get('num_comments', 0)} comments)\n {url}"
)
return "\n".join(lines)
@mcp.tool()
def fetch_page(url: str) -> str:
"""Fetch a public web page and return its readable text content.
Args:
url: Full http(s) URL of the page to fetch.
"""
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError("only http/https URLs are allowed")
if parsed.hostname in BLOCKED_HOSTS or (parsed.hostname or "").endswith(".internal"):
raise ValueError("refusing to fetch internal/private hosts")
r = httpx.get(
url,
timeout=15,
follow_redirects=True,
headers={"User-Agent": "devsearch-mcp/1.0"},
)
r.raise_for_status()
content_type = r.headers.get("content-type", "")
if "text" not in content_type and "json" not in content_type:
raise ValueError(f"unsupported content type: {content_type}")
text = r.text
if "html" in content_type:
import re
text = re.sub(r"(?is)<(script|style|nav|footer)[^>]*>.*?</\1>", " ", text)
text = re.sub(r"(?s)<[^>]+>", " ", text)
text = re.sub(r"\s+", " ", text).strip()
if len(text) > MAX_CHARS:
text = text[:MAX_CHARS] + f"\n\n[truncated at {MAX_CHARS} chars]"
return text
if __name__ == "__main__":
mcp.run() # stdio transport by defaultЭто весь сервер. Теперь те места, которые стоит рассмотреть под лупой.
декоратор делает скучные 80%
@mcp.tool() читает сигнатуру функции и докстринг и генерирует JSON Schema, которую видит клиент. query: str становится обязательным строковым параметром; limit: int = 5 — опциональным целым с дефолтом. Докстринг превращается в описание инструмента, секция Args: — в описания параметров. До MCP такую схему писали руками под каждого провайдера. Здесь ты пишешь типизированную Python-функцию, что и так делал.
Отсюда следствие, которое новички упускают: докстринг — это твой интерфейс. Модель выбирает инструмент, читая описания, и больше ничего. «Search Hacker News stories by keyword, ranked by relevance» вызывается в правильные моменты; докстринг «ищет всякое» вызывается наугад или никогда. Пиши описания для читателя, который понятия не имеет, как функция устроена внутри, потому что модель — ровно такой читатель.
валидируй так, будто вызывающий пьян
Посмотри, что инструменты отвергают. Пустые запросы. limit в 5 000 (обрезается до 20). URL со схемами file:// и ftp://. Запросы к localhost, 127.0.0.1 и 169.254.169.254 — это эндпоинт метаданных облака, который превращает безобидную скачивалку страниц в вора учёток на любой машине в AWS/GCP. LLM собирает аргументы твоего инструмента из контекста разговора, а в контексте может оказаться что угодно, включая вредоносную страницу, которую пользователь вставил. Считай каждый аргумент недоверенным входом, потому что он им и является.
Когда валидация падает, делай raise ValueError с внятным сообщением. SDK превратит исключение в корректный JSON-RPC-результат с ошибкой, модель её прочитает и обычно сама исправится на следующем вызове. Вернуть текст ошибки обычной строкой тоже можно, но исключение оставляет happy path чистым, а модель видит явный флаг ошибки вместо разбора прозы.
одно проектное решение на инструмент
Оба инструмента возвращают отформатированную строку вместо сырого JSON. Это сознательно. Вывод инструмента попадает в контекстное окно модели, и модель платит токенами за его чтение. Сырой ответ Algolia на пять историй — примерно 30 КБ JSON; мой отформатированный список меньше 1 КБ и содержит все пять заголовков, очков и ссылок. Возвращай то, что нужно модели для продолжения разговора, а не то, что тебе прислал upstream API.
По той же логике fetch_page обрезает текст на 8 000 символах и выкидывает скрипты, стили и навигацию из HTML перед возвратом. Команда Playwright MCP в Microsoft приняла то же решение в большем масштабе: их сервер читает дерево доступности браузера вместо скриншотов, потому что структурированный компактный вывод бьёт сырые дампы и по надёжности, и по цене.
тестируем тремя способами до подключения клиента
У отладки MCP есть неприятная правда: когда сервер барахлит внутри Claude Desktop, всё, что ты видишь, — серое «disconnected» и лог-файл. Сначала тестируй вне клиента, всегда.
1. Inspector: твоя новая любимая команда
# Run from devsearch-mcp/ with the venv active:
mcp dev server.pyКоманда запускает сервер и открывает MCP Inspector — веб-интерфейс на localhost, где видно список инструментов ровно так, как его видят клиенты, можно вызывать инструменты с руками набранными аргументами и читать сырые JSON-RPC-фреймы запросов и ответов. Вызови search_hackernews с query: "MCP server", потом попробуй сломать fetch_page через url: "http://127.0.0.1/admin" и посмотри, как ошибка валидации возвращается структурированным error-результатом. Две минуты здесь экономят час втыкания в логи Claude Desktop.
2. скриптовый smoke-тест (тот, что запустит CI)
В SDK есть и клиент, так что сервер можно гонять по настоящему stdio — точно так же, как это сделает Claude Desktop:
# devsearch-mcp/test_client.py
"""Smoke-test the devsearch server over real stdio, like Claude Desktop would."""
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
names = [t.name for t in tools.tools]
print("TOOLS:", names)
assert names == ["search_hackernews", "fetch_page"]
res = await session.call_tool(
"search_hackernews", {"query": "MCP server", "limit": 3}
)
assert "https://" in res.content[0].text
res = await session.call_tool("fetch_page", {"url": "https://example.com"})
assert "Example Domain" in res.content[0].text
# validation must reject internal hosts
res = await session.call_tool("fetch_page", {"url": "http://127.0.0.1/admin"})
assert res.isError, "expected internal host to be rejected"
print("ALL CHECKS PASSED")
asyncio.run(main())python test_client.py
# TOOLS: ['search_hackernews', 'fetch_page']
# ALL CHECKS PASSEDОбрати внимание на последний assert: тест доказывает, что SSRF-защита отвергает внутренние хосты, а не только что happy path работает. Когда я гонял это против SDK 1.28.1, поиск по HN вернул живые истории (топом в тот день шёл «MCP server that reduces Claude Code context consumption by 98%», 570 очков — символично). Скрипт на 30 строк становится твоим регрессионным тестом при каждой правке сервера.
3. живой клиент, где начинается самое интересное
Тесты пройдены, пора регистрировать сервер там, где его вызовет настоящая модель.
подключаем к Claude Desktop и Cursor (один сервер, два клиента)
Смысл MCP — «написал один раз, работает в каждом клиенте», так что докажем на двух.
Claude Desktop. Ленивый путь — одна команда (под капотом использует uv):
mcp install server.py --name devsearchИли отредактируй конфиг сам. Открой claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\) и добавь сервер в mcpServers — обязательно с абсолютными путями:
{
"mcpServers": {
"devsearch": {
"command": "/home/you/devsearch-mcp/.venv/bin/python",
"args": ["/home/you/devsearch-mcp/server.py"]
}
}
}Перезапусти Claude Desktop (полностью выйди, а не просто закрой окно), и инструменты появятся под иконкой ползунков в поле ввода. Спроси «какие топовые истории на HN про MCP-серверы в этом году?» и смотри, как он вызывает твою функцию.
Cursor. Тот же JSON, другой файл: ~/.cursor/mcp.json глобально или .cursor/mcp.json внутри проекта для сервера на конкретный репозиторий. Вставь тот же блок mcpServers, включи сервер в настройках MCP в Cursor, и агент подхватит оба инструмента.
Claude Code, если это твой основной инструмент, принимает сервер одной строкой: claude mcp add devsearch -- /path/to/.venv/bin/python /path/to/server.py.
Две ошибки дают 90% жалоб «мой сервер не появляется». Первая — относительные пути: клиент запускает сервер из своей рабочей директории, не из твоей, поэтому python server.py падает, а вариант с абсолютным путём работает. Вторая — печать в stdout: на stdio-транспорте stdout несёт протокол, так что случайный print() ломает JSON-RPC-поток, и клиент рвёт соединение. Логируй в stderr или через Context из SDK; никакого print() в stdio-сервере.
не отгружай эти ошибки (клуб 15%)
Аудит сообщества, опубликованный в r/mcp, просканировал топ-500 серверов реестра Smithery и нашёл проблемы безопасности у 76 из них — 15,3%, включая дюжину с эксплуатируемыми «токсичными потоками». Это топ каталога, серверы людей, достаточно уверенных, чтобы публиковаться. Прежде чем твой сервер покинет твою машину, пройдись по списку:
- Никакого shell и
eval. Если инструмент обязан запускать системную команду, передавай список аргументов (subprocess.run([...])), никогда не строку, собранную из аргументов инструмента. Одна f-строка в shell — и удалённое выполнение кода получает любой, кто может повлиять на разговор. - Валидируй каждый аргумент в начале каждого инструмента. Аннотации типов ограничивают форму, не содержание. Обрезай числа, разрешай только нужные URL-схемы, блокируй приватные хосты (паттерн показан в
fetch_page). - Ограничивай файловый доступ одной директорией. Если инструмент читает файлы, разрешай путь и проверяй, что он остался внутри песочницы:
Path(root, name).resolve().is_relative_to(root). Иначе../../.ssh/id_rsa— на расстоянии одного вызова. - Держи секреты подальше от описаний инструментов и сообщений об ошибках. Описания отправляются каждому клиенту и вставляются в каждое контекстное окно. API-ключ в докстринге или трейсбеке становится публичным в момент первого подключения.
Ничего продвинутого здесь нет. В этом и неприятность цифры 15,3%: планка для попадания в топ-500, судя по всему, оказалась ниже этих четырёх пунктов.
куда дальше (и куда идёт сам MCP)
Теперь у тебя есть форма каждого MCP-сервера, который ты когда-либо напишешь: FastMCP, декорированные функции, валидация сверху, компактные строки на выходе, проверка в Inspector до того, как сервер увидит клиент. Дальше три естественных шага. Добавь ресурс (@mcp.resource("notes://recent")), чтобы отдавать данные только для чтения без вызова инструмента. Поменяй транспорт на streamable HTTP (mcp.run(transport="streamable-http")), когда понадобится удалённый сервер на несколько клиентов, и перед этим прочитай про OAuth в спецификации: авторизация — то место, где удалённые серверы становятся сложными. И следи за WebMCP — инициативой Google и Microsoft, позволяющей сайтам отдавать MCP-инструменты прямо из вкладки браузера; протокол расползается с десктопов в саму веб-платформу. А если хочется перейти на другую сторону соединения и собрать агента, который эти инструменты вызывает, — у меня есть полный гайд по созданию первого AI-агента.
Весь код devsearch выше приведён целиком, без пропусков, и протестирован против mcp 1.28.1 и ревизии спецификации 2025-11-25 16 июля 2026. Если обновление SDK что-то сломает — напиши мне, обновлю статью.
Sources
- MCP Python SDK (official repo) — API FastMCP, CLI-команды, клиентская сессия из smoke-теста. Проверено против v1.28.1. https://github.com/modelcontextprotocol/python-sdk
- Model Context Protocol specification — концепции протокола (tools/resources/prompts), транспорты, текущая ревизия 2025-11-25. https://modelcontextprotocol.io/specification/2025-11-25/
- PulseMCP server directory — масштаб экосистемы (22 311 серверов). Данные от 16 июля 2026. https://www.pulsemcp.com/servers
- HN Algolia Search API — бесплатный поисковый эндпоинт под
search_hackernews. https://hn.algolia.com/api - Smithery top-500 security scan (r/mcp) — цифра 76/500 (15,3%) из security-чеклиста. https://www.reddit.com/r/mcp/comments/1to9gei/
- Introducing the Safari MCP server (WebKit blog) — пример принятия протокола браузерным вендором. https://webkit.org/blog/18136/introducing-the-safari-mcp-server-for-web-developers/
- Playwright MCP (Microsoft) — паттерн «дерево доступности вместо скриншотов» из обсуждения размера вывода. https://github.com/microsoft/playwright-mcp
Нужен MCP-сервер под твой API, базу данных или внутренние инструменты? Начни проект — я строю MCP-серверы, AI-агентов и системы агентных платежей, которые работают на твоей инфраструктуре.