Опубліковано 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-агентів і системи агентних платежів, які працюють на твоїй інфраструктурі.