Как написать свой MCP-сервер на Python с нуля: пример за 30 минут
Покажу, как с нуля сделать MCP-сервер на FastMCP — Python SDK для Model Context Protocol. Соберём minimum viable пример: сервер, который умеет считать слова в тексте.
Эта статья — для разработчика, который хочет не просто использовать чужие MCP-серверы, а написать свой. Соберём минимальный example: сервер, который принимает текст и возвращает статистику (количество слов, символов, уникальных слов). На примере покажу всю архитектуру FastMCP.
Зачем своё, если есть 1900+ готовых
Свой MCP-сервер нужен, когда: (1) у вас закрытый внутренний API без open-source обёртки, (2) специфическая бизнес-логика (типа интеграции с 1С нестандартной конфигурации), (3) вы хотите контролировать поведение агента (логи, rate-limit, audit), (4) или просто учитесь.
Что понадобится
- Python 3.10+ (FastMCP требует современный Python)
- uv (быстрый pip-замена) или pip — на ваш вкус
- Текстовый редактор (Cursor / VS Code / любой)
- 15-30 минут времени
Шаг 1. Скаффолд проекта
mkdir mcp-text-stats
cd mcp-text-stats
uv init --python 3.12
uv add "fastmcp>=3.0.0,<4.0.0"Если используете pip — то pip install fastmcp. Я предпочитаю uv: он быстрее и решает проблему версий Python автоматически. Структура проекта получается стандартная: pyproject.toml, src/<package>/, tests/.
Шаг 2. Минимальный сервер
Создайте файл src/mcp_text_stats/server.py:
from fastmcp import FastMCP
mcp = FastMCP("text-stats")
@mcp.tool()
def count_words(text: str) -> dict:
"""Подсчёт слов и символов в тексте."""
words = text.split()
return {
"total_words": len(words),
"total_chars": len(text),
"total_chars_no_spaces": len(text.replace(" ", "")),
"unique_words": len(set(w.lower() for w in words)),
}
if __name__ == "__main__":
mcp.run()Это всё. Декоратор @mcp.tool() автоматически: (1) превращает функцию в MCP-tool, (2) генерирует JSON-Schema из type-hints, (3) использует docstring как описание для агента. Никаких ручных манифестов.
Шаг 3. Запуск
uv run python -m mcp_text_stats.serverСервер запустится в stdio-режиме. Это значит, что он читает JSON-RPC из stdin и пишет в stdout. Сам по себе он никуда не подключится — нужен MCP-клиент (Cursor / Claude Code).
Шаг 4. Подключение к Cursor
Откройте ~/.cursor/mcp.json и добавьте:
{
"mcpServers": {
"text-stats": {
"command": "uv",
"args": [
"run",
"--directory",
"/абсолютный/путь/к/mcp-text-stats",
"python",
"-m",
"mcp_text_stats.server"
]
}
}
}Перезапустите Cursor (полностью, через Cmd/Ctrl+Q). После запуска в чате попросите: «посчитай слова в этом тексте» — и агент должен вызвать ваш тулз.
Шаг 5. Дебаг через MCP Inspector
Прежде чем подключать к Cursor, удобно проверить через MCP Inspector — официальный CLI от Anthropic для отладки. Запуск:
npx @modelcontextprotocol/inspector uv run python -m mcp_text_stats.serverInspector откроет веб-интерфейс на localhost, где можно увидеть список тулзов, их JSON-Schema, протестировать вызов. Это must-have для отладки нетривиальных серверов.
Шаг 6. Структурированный возврат через Pydantic
Вместо dict возвращайте Pydantic-модель — это даст вам валидацию и красивые ошибки:
from pydantic import BaseModel, Field
class TextStats(BaseModel):
total_words: int = Field(description="Количество слов")
total_chars: int = Field(description="Количество символов с пробелами")
total_chars_no_spaces: int
unique_words: int
@mcp.tool()
def count_words(text: str) -> TextStats:
"""Подсчёт слов и символов в тексте."""
words = text.split()
return TextStats(
total_words=len(words),
total_chars=len(text),
total_chars_no_spaces=len(text.replace(" ", "")),
unique_words=len(set(w.lower() for w in words)),
)Шаг 7. Async-операции
Если ваш тулз ходит во внешний API — обязательно делайте его async. FastMCP это поддерживает out-of-the-box:
import httpx
@mcp.tool()
async def fetch_url_title(url: str) -> str:
"""Скачивает HTML по URL и возвращает заголовок страницы."""
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10.0)
response.raise_for_status()
# ... парсинг title тэга
return "title"Шаг 8. Тесты
Никогда не выкладывайте сервер без тестов. Базовый шаблон через pytest:
import pytest
from mcp_text_stats.server import count_words
def test_count_words_simple():
result = count_words("hello world")
assert result.total_words == 2
assert result.total_chars == 11
def test_count_words_empty():
result = count_words("")
assert result.total_words == 0Что делать дальше
- Добавить argparse-обвязку для CLI (--help, --version, --transport stdio/http) — обязательно для production-сервера.
- Написать pyproject.toml с правильными classifiers, лицензией, README.
- Добавить ENV-переменные через os.environ для секретов.
- Опубликовать в PyPI как atomno-mcp-<name> (если планируется коммерческое использование).
- Добавить Dockerfile для возможности деплоя в облако.
- Зарегистрировать в Glama / Smithery / awesome-mcp-servers — попасть в каталоги.
Полезные ссылки
- FastMCP документация: github.com/jlowin/fastmcp
- MCP спецификация: modelcontextprotocol.io
- Шаблон для production-сервера: см. atomno-labs/mcp-cbr-rates как эталон
- MCP Inspector: github.com/modelcontextprotocol/inspector