Практический туториал по созданию MCP сервера с нуля на Python. Настройка окружения, инструменты (Tools), ресурсы (Resources), промпты (Prompts), транспорт stdio/HTTP и деплой на Railway. Реальный код без псевдокода — берите и используйте.
Model Context Protocol (MCP) — открытый стандарт от Anthropic, который позволяет AI-агентам (Claude, Codex CLI, Continue, Zed) подключаться к внешним инструментам через единый интерфейс. Если вы когда-либо хотели, чтобы Claude мог выполнять SQL-запросы, читать файлы вашего проекта, отправлять HTTP-запросы или взаимодействовать с вашим API — MCP сервер на Python решает эту задачу.
Ключевое преимущество MCP в том, что вы пишете сервер один раз, а использовать его могут любые совместимые клиенты. Ваш коллега, работающий в Claude Desktop, видит те же инструменты, что и разработчик в Cursor или Continue. Это избавляет от фрагментации: раньше для каждого AI-агента приходилось писать отдельные плагины и интеграции. Теперь достаточно одного MCP сервера.
В этом руководстве мы с нуля создадим MCP сервер, который предоставляет AI-агенту инструменты для работы с файловой системой, API-запросами и базой данных, зарегистрируем ресурсы и промпты, а затем задеплоим его на Railway. Весь код — реальный и протестированный. Никакого псевдокода.
Почему именно Python? Экосистема MCP SDK для Python наиболее зрелая после TypeScript-версии. Декораторы, автоматическая генерация JSON Schema из type hints, поддержка async/await — всё это делает разработку быстрой и приятной. А богатая экосистема библиотек (httpx, psycopg2, sqlite3, pydantic) позволяет интегрировать сервер с чем угодно: от корпоративных баз данных до внешних REST API.
Диаграмма показывает поток данных: AI-агент инициализирует соединение, получает список доступных инструментов/ресурсов, вызывает их и получает результаты. Всё по протоколу JSON-RPC 2.0.
Для создания MCP сервера на Python доступны две основные библиотеки: официальный mcp SDK от Anthropic и упрощённая обёртка fastmcp. Мы будем использовать mcp как основной SDK (он даёт полный контроль), но покажу и пример с fastmcp для быстрого старта.
# Создаём виртуальное окружение python -m venv .venv source .venv/bin/activate # Linux/macOS .venv\Scripts\activate # Windows # Устанавливаем MCP SDK и вспомогательные библиотеки pip install mcp uvicorn httpx pydantic python-dotenv # Опционально: fastmcp для быстрого прототипирования pip install fastmcp # Проверяем версии pip show mcp Name: mcp Version: 1.8.0 Summary: Model Context Protocol SDK for Python
Рекомендованная структура директорий для MCP сервера:
my-mcp-server/ ├── pyproject.toml # метаданные и зависимости ├── .env # переменные окружения (API ключи) ├── .env.example # шаблон .env для документации ├── server.py # точка входа (stdio транспорт) ├── server_http.py # точка входа (HTTP транспорт) ├── tools/ │ ├── __init__.py │ ├── filesystem.py # инструменты для работы с ФС │ └── api_client.py # инструменты для HTTP-запросов ├── resources/ │ ├── __init__.py │ └── project_context.py # ресурсы с контекстом проекта └── prompts/ ├── __init__.py └── templates.py # шаблоны промптов
server.py на старте.
Сердце любого MCP сервера — объект Server. Он регистрирует инструменты, ресурсы и промпты, обрабатывает входящие JSON-RPC запросы и управляет жизненным циклом соединения. Начнём с минимального рабочего сервера.
Объект Server из пакета mcp инкапсулирует всю логику протокола: инициализацию соединения (обмен capabilities), обработку JSON-RPC сообщений, маршрутизацию вызовов к зарегистрированным инструментам и ресурсам. Разработчику остаётся только описать функции и повесить декораторы — SDK берёт на себя сериализацию, валидацию и транспорт.
Транспорт stdio (стандартный ввод/вывод) — самый простой и надёжный вариант для локальной разработки. Процесс сервера запускается как дочерний процесс AI-агента, обмениваясь JSON-RPC сообщениями через stdin/stdout. Это не требует открытия сетевых портов, настройки CORS или SSL — всё работает из коробки.
import asyncio from mcp.server import Server from mcp.server.stdio import stdio_server # Создаём объект сервера с именем и версией server = Server("my-first-mcp-server") @server.tool() async def hello(name: str) -> str: """Приветствует пользователя по имени — простейший тестовый инструмент.""" return f"Привет, {name}! MCP сервер работает. 🚀" async def main(): async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options() ) if __name__ == "__main__": asyncio.run(main())
Этот код создаёт MCP сервер с одним инструментом hello. Декоратор @server.tool() автоматически извлекает сигнатуру функции, type hints и docstring — и генерирует JSON Schema для параметров. Никакой ручной сериализации.
Чтобы Claude увидел ваш сервер, добавьте его в конфигурацию Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json на macOS, %APPDATA%\Claude\claude_desktop_config.json на Windows):
{
"mcpServers": {
"my-first-server": {
"command": "python",
"args": ["/absolute/path/to/server.py"],
"env": {
"API_KEY": "your-secret-key"
}
}
}
}
После перезапуска Claude Desktop вы увидите в интерфейсе иконку молотка (🔨) — это означает, что MCP инструменты подключены. Спросите Claude: «Поздоровайся с Дмитрием через hello tool» — и он вызовет ваш инструмент.
Инструменты — это основной способ расширения возможностей AI-агента. Каждый инструмент: функция Python с type hints, docstring на естественном языке и возвращаемым значением. MCP SDK автоматически генерирует JSON Schema из сигнатуры, а docstring используется агентом для понимания, когда вызывать инструмент.
Важно понимать, как AI-агент выбирает, какой инструмент вызвать. Когда вы пишете docstring «Читает содержимое файла. Используй для просмотра исходников, конфигов, логов» — это не просто документация для людей. Claude (и другие LLM) читает эти описания и сопоставляет с запросом пользователя. Если пользователь просит «покажи конфигурацию сервера», агент видит в описании слово «конфигов» и выбирает read_file. Поэтому хороший docstring — это половина успеха вашего MCP сервера.
Входные параметры валидируются автоматически: если вы указали limit: int = 25, SDK не пропустит вызов с limit: "abc". Type hints в Python (включая Optional, Literal, Union) транслируются в соответствующие конструкции JSON Schema — проверка типов работает на уровне протокола, до того как ваш код начнёт выполняться.
import os import pathlib from typing import Optional @server.tool() async def read_file(path: str, encoding: str = "utf-8") -> str: """Читает содержимое файла. Используй для просмотра исходников, конфигов, логов. Возвращает текст файла.""" p = pathlib.Path(path).resolve() if not p.exists(): return f"Ошибка: файл {path} не найден" if p.stat().st_size > 1_000_000: return "Ошибка: файл слишком большой (>1 MB)" return p.read_text(encoding=encoding) @server.tool() async def list_directory(path: str = ".", pattern: Optional[str] = None) -> str: """Показывает содержимое директории. Можно фильтровать по glob-паттерну. Пример: pattern='*.py' покажет только Python-файлы.""" p = pathlib.Path(path).resolve() if not p.is_dir(): return f"Ошибка: {path} не является директорией" if pattern: items = sorted(p.glob(pattern)) else: items = sorted(p.iterdir()) lines = [] for item in items: suffix = "/" if item.is_dir() else "" size = item.stat().st_size if item.is_file() else 0 lines.append(f"{item.name}{suffix} — {size} байт") return "\n".join(lines[:50]) # Лимит для безопасности
import httpx from typing import Literal @server.tool() async def http_request( url: str, method: Literal["GET", "POST"] = "GET", body: Optional[str] = None, ) -> str: """Выполняет HTTP-запрос к указанному URL. Используй для вызова внешних API, проверки доступности сервисов, получения данных из REST-эндпоинтов.""" async with httpx.AsyncClient(timeout=15.0) as client: try: if method == "GET": resp = await client.get(url) else: resp = await client.post(url, content=body) resp.raise_for_status() return f"Status {resp.status_code}\n{resp.text[:2000]}" except httpx.HTTPStatusError as e: return f"HTTP ошибка: {e.response.status_code}" except Exception as e: return f"Ошибка запроса: {type(e).__name__}: {e}"
import sqlite3 import json @server.tool() async def query_database( sql: str, db_path: str = ":memory:", limit: int = 25 ) -> str: """Выполняет SQL-запрос к базе данных SQLite. Только SELECT-запросы (безопасность!). Возвращает результат в JSON.""" sql_upper = sql.strip().upper() if not sql_upper.startswith("SELECT"): return "Ошибка: разрешены только SELECT-запросы" try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row cursor = conn.execute(sql) rows = cursor.fetchmany(limit) conn.close() result = [dict(row) for row in rows] return json.dumps(result, ensure_ascii=False, indent=2) except Exception as e: return f"Ошибка SQL: {e}"
resolve() для путей. AI-агент не должен иметь доступ к произвольным системным вызовам без ограничений.
Помимо инструментов, MCP сервер может предоставлять ресурсы (данные для чтения) и промпты (шаблоны взаимодействия). Это делает сервер не просто набором функций, а полноценным источником контекста для AI-агента.
Ресурсы идеальны для данных, которые агенту нужно видеть, но не менять: схема базы данных, документация API, конфигурация проекта, текущие метрики. Агент сам решает, когда запросить ресурс — например, перед написанием SQL-запроса он может прочитать ресурс со схемой БД, чтобы понять структуру таблиц. Ресурсы кэшируются на стороне клиента, что снижает нагрузку на сервер.
Промпты — это «подсказки» сервера агенту о том, как эффективно с ним взаимодействовать. Когда агент вызывает prompts/list и видит шаблон explore_project с описанием «для исследования структуры проекта», он понимает, что для анализа кодовой базы нужно использовать именно этот промпт. Промпты могут содержать переменные для подстановки — например, путь к проекту или текст ошибки.
Комбинация инструментов, ресурсов и промптов создаёт полноценный контекстный интерфейс: ресурсы дают данные, промпты учат агента правильно их использовать, а инструменты позволяют выполнять действия. Это и есть главная сила MCP — не просто набор функций, а семантически связанная экосистема для AI-взаимодействия.
from mcp.server import Resource import datetime # Статический ресурс: схема базы данных @server.resource("schema://database") async def database_schema() -> str: """Текущая схема базы данных с описанием таблиц и колонок.""" conn = sqlite3.connect("app.db") cursor = conn.execute( "SELECT sql FROM sqlite_master WHERE type='table'" ) schemas = [row[0] for row in cursor if row[0]] conn.close() return "\n\n".join(schemas) # Динамический ресурс: статус сервера (обновляется каждый вызов) @server.resource("monitor://status") async def server_status() -> str: """Текущий статус сервера: uptime, использование памяти, активные соединения.""" import psutil # pip install psutil now = datetime.datetime.now().isoformat() mem = psutil.virtual_memory() return f"""Статус на {now} ОЗУ: {mem.percent}% использовано ({mem.used // 1024**2} MB из {mem.total // 1024**2} MB) CPU: {psutil.cpu_percent()}%"""
@server.prompt() async def explore_project(project_path: str) -> str: """Генерирует промпт для исследования структуры проекта.""" return f"""Ты анализируешь проект, расположенный в {project_path}. Инструкции: 1. Сначала используй list_directory для обзора структуры. 2. Найди файлы конфигурации (pyproject.toml, package.json, Cargo.toml). 3. Прочитай readme и основные исходные файлы. 4. Опиши архитектуру проекта: язык, фреймворк, зависимости. 5. Выдели потенциальные проблемы и предложи улучшения. Доступные инструменты: list_directory, read_file, http_request.""" @server.prompt() async def debug_error(error_message: str, context_file: str = "") -> str: """Генерирует промпт для отладки ошибки с контекстом файла.""" base = f"""Произошла ошибка: {error_message} Твоя задача: 1. Проанализируй ошибку и определи возможные причины. 2. Если указан файл — прочитай его через read_file. 3. Предложи исправление с конкретным кодом. 4. Объясни, почему возникла ошибка и как избежать её в будущем.""" if context_file: return base + f"\n\nКонтекстный файл: {context_file} (прочитай его через read_file)" return base
MCP сервер по умолчанию работает через stdio (стандартный ввод/вывод), что идеально для локального использования с Claude Desktop. Но для командной работы, облачных агентов и масштабирования нужен HTTP-транспорт. Рассмотрим оба варианта.
HTTP-транспорт через SSE (Server-Sent Events) позволяет MCP серверу принимать соединения от удалённых клиентов. В отличие от WebSocket, SSE работает поверх обычного HTTP, что упрощает настройку reverse proxy (nginx, Caddy), балансировщиков нагрузки и firewall. Клиент подписывается на поток событий через GET-запрос, а отправляет сообщения серверу через POST.
Платформы для деплоя Python-приложений отлично подходят для MCP серверов. Railway автоматически определяет Python-проект по наличию requirements.txt или pyproject.toml, устанавливает зависимости и запускает приложение. Fly.io даёт глобальное распределение (ваш сервер физически ближе к пользователям) и поддерживает автомасштабирование. Для максимального контроля можно использовать обычный VPS с systemd и nginx.
# server_http.py — HTTP версия MCP сервера import asyncio from mcp.server import Server from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Mount, Route from starlette.responses import JSONResponse import uvicorn server = Server("my-mcp-http-server") sse = SseServerTransport("/messages/") # ... здесь регистрация инструментов, ресурсов, промптов ... async def handle_sse(request): async with sse.connect_sse( request.scope, request.receive, request._send ) as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options() ) async def health_check(request): return JSONResponse({"status": "ok", "server": "mcp-http"}) app = Starlette( routes=[ Route("/sse", endpoint=handle_sse), Route("/health", endpoint=health_check), Mount("/messages/", app=sse.handle_post_message), ] ) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)
Railway — отличная платформа для деплоя MCP серверов: поддержка Python из коробки, автоматический HTTPS, масштабирование и бесплатный тир для старта.
# railway.json — конфигурация деплоя { "build": { "builder": "NIXPACKS" }, "deploy": { "startCommand": "python server_http.py", "healthcheckPath": "/health", "restartPolicyType": "ON_FAILURE" } }
Шаги деплоя:
npm i -g @railway/clirailway loginrailway initrailway variables set API_KEY=your-keyrailway upПосле деплоя ваш MCP сервер будет доступен по URL вида https://your-project.up.railway.app. Подключите его к Claude Desktop, указав URL вместо команды:
{
"mcpServers": {
"my-http-server": {
"url": "https://your-project.up.railway.app/sse",
"transport": "sse"
}
}
}
# Dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "server_http.py"]
# requirements.txt mcp>=1.8.0 uvicorn>=0.30.0 httpx>=0.27.0 pydantic>=2.0 python-dotenv>=1.0 starlette>=0.38.0
fastmcp — обёртка над mcp SDK, которая убирает бойлерплейт и добавляет удобные фичи: автоматическую документацию OpenAPI, веб-интерфейс для тестирования, CLI-инструменты.
from fastmcp import FastMCP mcp = FastMCP("My Tools 🚀") @mcp.tool() def add(a: int, b: int) -> int: """Складывает два числа.""" return a + b @mcp.tool() def get_weather(city: str) -> str: """Получает текущую погоду для города (мок-версия).""" return f"В городе {city} сейчас солнечно, +22°C" if __name__ == "__main__": # Запуск с HTTP-транспортом одной командой mcp.run(transport="sse", port=8000)
Паттерн «инструмент-оркестратор»: один инструмент вызывает несколько внутренних и возвращает агрегированный результат.
@server.tool() async def analyze_repository(repo_path: str) -> str: """Анализирует Git-репозиторий и возвращает сводку: структура, языки, последние коммиты, размер.""" import subprocess p = pathlib.Path(repo_path) if not (p / ".git").exists(): return "Ошибка: не Git-репозиторий" def run_git(*args): return subprocess.check_output( ["git", "-C", repo_path] + list(args), text=True, stderr=subprocess.STDOUT ) commits = run_git("log", "--oneline", "-10") branches = run_git("branch", "-a") # Подсчёт файлов по расширениям extensions = {} for file in p.rglob("*"): if file.is_file() and ".git" not in str(file): ext = file.suffix or "(no ext)" extensions[ext] = extensions.get(ext, 0) + 1 top_exts = sorted(extensions.items(), key=lambda x: x[1], reverse=True)[:5] return f"""📊 Анализ репозитория: {p.name} 📁 Расширения: {', '.join(f'{e}({c})' for e,c in top_exts)} 🌿 Ветки: {branches} 📝 Последние коммиты: {commits}"""
| Практика | Описание |
|---|---|
| Валидация входов | Проверяйте все параметры инструментов: типы, диапазоны, допустимые значения. Используйте Pydantic-модели для сложной валидации. |
| Таймауты | Каждый инструмент должен иметь разумный таймаут (asyncio.wait_for). Агент не должен висеть 60 секунд на одном вызове. |
| Лимиты данных | Ограничивайте размер возвращаемых данных. Никаких 10 MB JSON-ответов — агент не сможет их обработать. |
| Логирование | Логируйте все вызовы инструментов: имя, параметры, результат, время выполнения. Используйте structlog или стандартный logging. |
| Аутентификация | Для HTTP-серверов добавьте API-ключ или JWT. MCP не имеет встроенной аутентификации — это зона ответственности разработчика. |
| Graceful shutdown | Корректно завершайте соединения с БД, закрывайте файловые дескрипторы при остановке сервера. |
| Документация docstring | Пишите подробные docstring для инструментов — AI-агент использует их для принятия решений. Чем лучше описание, тем точнее вызовы. |
| Тестирование | Тестируйте инструменты через MCP-клиент в pytest. Проверяйте, что JSON Schema генерируется корректно. |
# test_server.py import pytest from mcp.client.session import ClientSession from mcp.client.stdio import stdio_client @pytest.mark.asyncio async def test_hello_tool(): """Проверяем, что инструмент hello работает корректно.""" async with stdio_client("python", ["server.py"]) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # Проверяем список инструментов tools = await session.list_tools() assert any(t.name == "hello" for t in tools) # Вызываем инструмент result = await session.call_tool("hello", {"name": "Тест"}) assert "Тест" in result.content[0].text assert "MCP сервер работает" in result.content[0].text
MCP_INSPECTOR=1 python server.py — встроенный инспектор от Anthropic покажет все зарегистрированные инструменты, ресурсы и позволит тестировать вызовы в реальном времени через веб-интерфейс на localhost:5173.
Мы прошли полный цикл создания MCP сервера на Python — от установки пакетов до деплоя на Railway. Давайте зафиксируем главное:
mcp (официальный SDK) или fastmcp (упрощённая обёртка). Оба генерируют JSON Schema из type hints автоматически — никакой ручной сериализации параметров.railway up из корня проекта с server_http.py и requirements.txt. Автоматический HTTPS, домен и масштабирование — ничего настраивать вручную не нужно.Что дальше? Изучите официальный репозиторий MCP-серверов — там десятки примеров: от PostgreSQL и GitHub до Slack и Brave Search. Интегрируйте свой сервер с Claude Code, Cursor или Continue. Создайте MCP-сервер для своего продукта и опубликуйте в каталоге — экосистема растёт взрывными темпами, и ваш инструмент может стать стандартом для тысяч AI-агентов по всему миру. В 2026 году MCP — это то, чем HTTP был в 1995: фундаментальный протокол, на котором строится следующее поколение интернета. 🔌