Python SDK v1 · mcp==1.30.0 · протокол 2025-11-25. Для SDK v2 требуется отдельная миграция.
Что нужно
Windows и PowerShell, Python 3.11 с командой py. Для подключения в редакторе — VS Code с доступной поддержкой MCP. Встроенная проверка сервера не требует входа в ИИ-клиент. Урок закреплён за mcp==1.30.0, Python SDK v1, и протоколом 2025-11-25. SDK v2 — отдельная ветка: для этого примера не заменяйте версию на latest.
1. Создайте папку и окружение
В PowerShell выполните команды по одной:
py -3.11 --version
$labPath = Join-Path $env:USERPROFILE 'mcp-label-lab'
New-Item -ItemType Directory -Path $labPath -ErrorAction Stop
Set-Location -LiteralPath $labPath
py -3.11 -m venv .venvЕсли папка уже существует, команда остановится. Выберите новое имя для этого урока вместо перезаписи имеющегося проекта. Окружение лежит в .venv; активировать его не нужно — ниже в каждой команде указан его интерпретатор. Это обходит отдельную настройку выполнения PowerShell-скриптов.
Сохраните в новой папке requirements-label.txt с одной строкой:
mcp==1.30.0Установите зависимость именно в эту среду и проверьте версию:
.\.venv\Scripts\python.exe -m pip install -r requirements-label.txt
.\.venv\Scripts\python.exe -c "from importlib.metadata import version; print(version('mcp'))"Ожидаемый второй вывод — 1.30.0. Если установка завершилась ошибкой, дальше не переходите: подключение клиента её не исправит. Отдельный пакет fastmcp для этого урока не нужен. Здесь FastMCP импортируется из установленного пакета mcp.
2. Сохраните полный сервер
Создайте рядом с requirements файл label_server.py и вставьте весь код:
"""Local stdio tutorial for mcp==1.30.0. No external services or keys."""
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("label-lab")
@mcp.tool()
def normalize_label(text: str) -> dict[str, str]:
"""Trim a label and reject empty labels or more than 80 characters."""
label = text.strip()
if not 1 <= len(label) <= 80:
raise ValueError("Expected 1 to 80 characters after trimming")
return {"label": label}
if __name__ == "__main__":
mcp.run(transport="stdio")@mcp.tool() регистрирует функцию под именем normalize_label. Аннотация text: str становится строковым полем входной схемы. Дополнительное правило длины выполняется после strip(), поэтому строка из одних пробелов тоже отклоняется. Результат — объект с полем label; внутренние пробелы и регистр сохраняются.
Блок __main__ выбирает stdio. Не добавляйте обычный print в сервер: stdout используется для сообщений MCP. Если запустить label_server.py напрямую, он будет ждать сообщения клиента. Для проверки готового обмена используйте следующий файл, который сам запустит и остановит дочерний процесс.
3. Получите реальный ответ без модели
Сохраните рядом check_stdio.py. Это небольшой клиент на стандартной библиотеке Python: он посылает initialize, подтверждает инициализацию, получает список инструментов и вызывает ваш сервер. Вот полный файл:
Показать полный код · 107 строк
"""Call the local tutorial via JSON-RPC/stdin, then stop its child process."""
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
def main() -> dict:
directory = Path(__file__).resolve().parent
server = directory / "label_server.py"
if not server.is_file():
raise FileNotFoundError("Save label_server.py beside check_stdio.py")
process = subprocess.Popen(
[sys.executable, "-u", str(server)],
cwd=directory,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
bufsize=1,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
)
replies = queue.Queue()
diagnostics = []
def read_stdout():
for line in process.stdout:
replies.put(line)
replies.put(None)
def read_stderr():
for line in process.stderr:
diagnostics.append(line.rstrip())
threading.Thread(target=read_stdout, daemon=True).start()
threading.Thread(target=read_stderr, daemon=True).start()
transcript = []
def send(message):
transcript.append({"direction": "client", "message": message})
process.stdin.write(json.dumps(message) + "\n")
process.stdin.flush()
def request(identifier, method, params):
send({"jsonrpc": "2.0", "id": identifier, "method": method, "params": params})
while True:
line = replies.get(timeout=10)
if line is None:
raise RuntimeError("Server stopped: " + " | ".join(diagnostics[-4:]))
message = json.loads(line)
transcript.append({"direction": "server", "message": message})
if message.get("id") == identifier:
return message
try:
initialized = request(1, "initialize", {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "label-lab-check", "version": "1.0.0"},
})
if initialized.get("result", {}).get("protocolVersion") != "2025-11-25":
raise AssertionError("Unexpected negotiated protocol version")
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
listing = request(2, "tools/list", {})
tools = listing.get("result", {}).get("tools", [])
if [tool["name"] for tool in tools] != ["normalize_label"]:
raise AssertionError("normalize_label is missing from tools/list")
schema = tools[0]["inputSchema"]
if schema["properties"]["text"]["type"] != "string" or "text" not in schema["required"]:
raise AssertionError("Tool input schema changed")
success = request(3, "tools/call", {"name": "normalize_label", "arguments": {"text": " Draft "}})
result = success.get("result", {})
if result.get("isError") or result.get("structuredContent") != {"label": "Draft"}:
raise AssertionError("Unexpected normalized label")
cases = [(4, "blank", {"text": " "}), (5, "too_long", {"text": "x" * 81}), (6, "missing_text", {})]
errors = {}
for identifier, name, arguments in cases:
reply = request(identifier, "tools/call", {"name": "normalize_label", "arguments": arguments})
if reply.get("result", {}).get("isError") is not True:
raise AssertionError("Expected an error for " + name)
errors[name] = reply["result"]
return {"passed": True, "protocol": initialized["result"]["protocolVersion"], "tool": tools[0], "success": result, "known_errors": errors, "transcript": transcript}
finally:
process.stdin.close()
try:
process.wait(timeout=3)
except subprocess.TimeoutExpired:
process.terminate()
try:
process.wait(timeout=3)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=3)
if __name__ == "__main__":
try:
report = main()
print("PASS: initialize, tools/list, tools/call, 3 expected errors")
print(json.dumps(report, ensure_ascii=False, indent=2))
except Exception as error:
print("FAIL:", error, file=sys.stderr)
raise SystemExit(1)Запустите его тем же интерпретатором:
.\.venv\Scripts\python.exe check_stdio.pyПри успехе первая строка будет:
PASS: initialize, tools/list, tools/call, 3 expected errorsНиже появится JSON. В protocol должна быть версия 2025-11-25, в tool.name — normalize_label. Для аргумента {"text":" Draft "} проверенный success выглядит так:
{
"content": [
{
"type": "text",
"text": "{\n \"label\": \"Draft\"\n}"
}
],
"structuredContent": {
"label": "Draft"
},
"isError": false
}Клиент не просто вызвал Python-функцию напрямую: он проверил входную схему в tools/list и обработал ответ tools/call. В конце он закрывает stdin, дожидается завершения сервера и при необходимости останавливает только созданный им дочерний процесс. После PASS не нужно оставлять терминал с сервером запущенным.
4. Сформируйте готовую конфигурацию VS Code
Сохраните четвёртый файл configure_client.py:
"""Create this new lab's portable MCP configuration with absolute paths."""
import json
from pathlib import Path
import sys
directory = Path(__file__).resolve().parent
configuration = directory / ".mcp.json"
if configuration.exists():
raise SystemExit(".mcp.json already exists; keep it and review it manually")
server = directory / "label_server.py"
if not server.is_file():
raise SystemExit("Save label_server.py beside configure_client.py")
configuration.write_text(json.dumps({
"mcpServers": {
"label-lab": {
"type": "stdio",
"command": sys.executable,
"args": ["-u", str(server)],
}
}
}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print("Created", configuration)Выполните:
.\.venv\Scripts\python.exe configure_client.py
Get-Content -LiteralPath .mcp.jsonПрограмма создаст в корне этой новой папки .mcp.json, верхний ключ mcpServers, запись label-lab, type: stdio, абсолютный путь к Python вашей .venv и абсолютный путь к серверу в args. Имя пользователя и экранирование Windows-путей подставляются автоматически. Существующий .mcp.json программа не перезаписывает.
Откройте именно папку mcp-label-lab в VS Code. В палитре команд выполните MCP: List Servers, выберите label-lab и запустите его. Если сервер выключен, включите его; ограниченный режим доверия рабочей папки может блокировать запуск. В списке инструментов ожидайте normalize_label с аргументом text. В чате редактора включите этот инструмент через Configure Tools и попросите: «Вызови normalize_label с text равным двум пробелам, Draft и двум пробелам. Покажи значение label». Результат инструмента должен быть Draft. После опыта сервер можно остановить через управление MCP-серверами.
Используется переносимый workspace-формат, который текущая документация VS Code рекомендует для новых серверов. У формата .vscode/mcp.json другой верхний ключ — servers; не копируйте между ними один JSON без изменения схемы. Здесь файл уже сгенерирован в выбранном формате и не требует ручной подстановки путей.
5. Разберите три отказа
Проверочный клиент уже отправил три неверных вызова. В known_errors их ответы должны иметь isError: true:
| Вызов | Причина | Что исправить |
|---|---|---|
text: " " | После strip строка пустая | Передать подпись хотя бы из одного символа |
Строка из 81 x | Превышен предел 80 | Сократить подпись |
arguments: {} | Обязательное поле text отсутствует | Добавить строковое поле text |
У пустой и длинной строки сообщение содержит Expected 1 to 80 characters after trimming. У отсутствующего поля возникает ошибка входной схемы с Field required. Это разные уровни проверки: SDK проверяет обязательный аргумент, функция проверяет предметное правило.
Если вместо PASS выводится FAIL, сначала устраните указанную причину:
- ModuleNotFoundError: mcp. Проверьте версию командой из шага 1 и запускайте оба файла через
.venv\Scripts\python.exe, а не случайныйpythonиз PATH. - Save label_server.py beside check_stdio.py. Сохраните оба файла в одной папке, проверьте расширение
.py, а не.py.txt. - Сервер завершился до initialize. Строки после Server stopped покажут ошибку импорта или синтаксиса. Не меняйте транспорт, пока сервер не стартует.
- В редакторе сервера нет. Откройте корень папки с
.mcp.json; проверьте запись label-lab, enabled-состояние и доверие workspace. Команда MCP: List Servers → label-lab → Show Output откроет журнал запуска. - Редактор показывает старую схему. Перезапустите только label-lab после изменения файла и снова проверьте список инструментов.
Что получилось и куда двигаться дальше
У вас есть один работающий инструмент с понятным контрактом, проверяемым успешным ответом, тремя отказами и конфигурацией клиента. Добавьте свой синтетический пример: " two words " должен сохранить двойной пробел внутри и вернуть "two words". Затем переходите к схеме и валидации: расширяйте правило отдельно от транспорта. Сначала добейтесь нового PASS в терминале, после этого обновляйте подключение редактора.
Этот stdio-проект не открывает HTTP endpoint. Переход к удалённому серверу требует отдельного урока про транспорт, Origin и авторизацию; включение HTTP не следует из замены строки в этом примере.
Версия, источники и проверка
Python 3.11.4, установленный SDK mcp 1.30.0, протокол 2025-11-25: сохранён реальный локальный обмен, успешный вызов и три ошибки на синтетических строках. Новая установка пакетов и интерфейс VS Code в этом прогоне отдельно не запускались; конфигурация сверена с первичной документацией, её команда и аргументы совпали с проверенным запуском. Для SDK v2 понадобится отдельная миграция.
Источники, сверены 8 октября 2026 года: Python SDK v1 (в новой вкладке), FastMCP в SDK v1 (в новой вкладке), MCP tools 2025-11-25 (в новой вкладке), stdio и Streamable HTTP (в новой вкладке), форматы конфигурации VS Code (в новой вкладке), управление серверами VS Code (в новой вкладке).
Вопросы по материалу
Почему запущенный label_server.py ничего не печатает?
Сервер в режиме stdio ждёт сообщения MCP-клиента. Для обмена запустите check_stdio.py: он сам создаёт процесс сервера и проверяет ответ. Обычный print в stdout добавлять не нужно — этот поток занят протоколом.
Нужно ли отдельно устанавливать пакет fastmcp?
Для этого урока — нет. В нём закреплён пакет mcp==1.30.0, а FastMCP импортируется из mcp.server.fastmcp. Это граница учебного примера, а не рекомендация заменить зависимости другого проекта.
Продукты ATOMNO
Другие задачи для AI-помощника
Это отдельные продукты ATOMNO для задач с данными. Они не входят в учебные примеры.
Узнать курс и ставку ЦБ
Помощник получает курсы валют и ключевую ставку.
Курсы и ставка Банка России
Опубликованный клиент; свежесть конкретного ответа сверяйте с источником.
Проверить организацию
ИНН или ОГРН → ограниченная проверка организации.
Проверка контрагента
Состав проверки и доступ уточняйте в документации.
Найти госзакупку
Поиск извещения → чтение карточки закупки.
Поиск госзакупок
Полнота архива и доставка уведомлений не обещаются.
Открыть карточку товарного знака
Номер свидетельства → карточка российского знака.
Карточка товарного знака
Поиск по названию и оценка сходства не входят.