
Введение
Протокол контекста модели (MCP) преобразил взаимодействие больших языковых моделей (LLM) с внешними инструментами, источниками данных и сервисами. Раньше для создания MCP-серверов приходилось разбираться в сложном шаблонном коде и деталях протокола. FastMCP решает эту проблему благодаря декораторному, pythonic-фреймворку, который позволяет разрабатывать готовые к производству MCP-серверы и клиенты с минимумом строк кода.
В этом руководстве разберем, как собрать MCP-серверы и клиенты на FastMCP. Фреймворк полон функций, включая обработку ошибок, и подходит как новичкам, так и разработчикам среднего уровня.
Требования
Перед началом убедитесь, что у вас есть:
- Python 3.10 или новее (рекомендуется 3.11+ для улучшенной производительности async)
- pip или uv (uv предпочтительнее для развертывания FastMCP и обязателен для CLI-инструментов)
- Редактор кода (подойдет любой, например VS Code)
- Навыки работы с терминалом для запуска Python-скриптов
Полезно знание Python (функции, декораторы, type hints), основ async/await (не обязательно, но помогает в продвинутых примерах), JSON и REST API, а также базовые команды терминала.
До FastMCP разработка MCP-серверов требовала глубокого понимания спецификации MCP JSON-RPC, кучи шаблонного кода для протокола, ручного управления соединениями и сложной логики ошибок.
FastMCP устраняет эти барьеры с помощью простых декораторов и удобного API, чтобы вы сосредоточились на логике приложения, а не на протоколе.
Что такое протокол контекста модели?
Протокол контекста модели (MCP) — открытый стандарт от Anthropic. Он дает универсальный интерфейс для безопасного подключения ИИ-приложений к внешним инструментам, данным и сервисам. MCP стандартизирует общение LLM с внешними системами, подобно тому, как веб-API стандартизировали обмен данными в вебе.
Основные черты MCP
- Стандартизированный обмен: на базе JSON-RPC 2.0 для надежных структурированных сообщений
- Двунаправленный: поддержка запросов от клиентов к серверам и ответов обратно
- Безопасность: встроенная аутентификация и авторизация
- Гибкие транспорты: работает с stdio, HTTP, WebSocket, SSE
Архитектура MCP: серверы и клиенты
MCP использует четкую клиент-серверную модель:

- MCP-сервер: предоставляет возможности (инструменты, ресурсы, промты) для внешних приложений. Это как бэкенд-API, заточенный под интеграцию с LLM.
- MCP-клиент: встраивается в ИИ-приложения (Claude Desktop, Cursor IDE или кастомные), чтобы подключаться к серверам и использовать их ресурсы.
Основные компоненты MCP
MCP-серверы предлагают три вида возможностей:
- Инструменты: исполняемые функции, которые LLM вызывают для действий. Они запрашивают базы данных, обращаются к API, считают или запускают процессы.
- Ресурсы: только для чтения данные, которые клиенты берут как контекст. Это содержимое файлов, конфиги или генерируемый контент.
- Промты: шаблоны сообщений для управления поведением LLM. Они дают единые инструкции для многошаговых задач или специального мышления.
Что такое FastMCP?
FastMCP — высокоуровневый Python-фреймворк для упрощения создания MCP-серверов и клиентов. Он избавляет от рутины и предлагает:
- API на декораторах: @mcp.tool, @mcp.resource, @mcp.prompt убирают шаблонный код
- Типобезопасность: полные type hints и валидация на базе системы типов Python
- Поддержка async/await: современный async для высокопроизводительных задач
- Множество транспортов: stdio, HTTP, WebSocket, SSE
- Встроенные тесты: простое тестирование клиент-сервер без подпроцессов
- Готовность к продакшену: обработка ошибок, логирование, конфигурация
Философия FastMCP
FastMCP строится на трех принципах:
- Высокоуровневые абстракции: меньше кода, быстрее разработка
- Простота: минимум шаблонов, фокус на функциях, а не протоколе
- Pythonic: естественные идиомы Python для знакомства разработчикам
Установка
Установите FastMCP и зависимости. Рекомендуется uv.
uv pip install fastmcpЕсли uv нет, сначала:
pip install uvИли напрямую pip:
pip install fastmcpПроверьте установку:
python -c "from fastmcp import FastMCP; print('FastMCP installed successfully')"Создание первого MCP-сервера
Соберем практический сервер-калькулятор с инструментами, ресурсами и промтами для математических операций, конфигурацией и инструкциями.
Шаг 1: Структура проекта
Создайте папку проекта:
mkdir fastmcp-calculatorПерейдите в нее:
cd fastmcp-calculatorИнициализируйте:
uv init --python 3.11Шаг 2: MCP-сервер
Калькулятор — простой сервер с инструментами, ресурсами и промтами. Создайте calculator_server.py и добавьте код.
import logging
import sys
from typing import Dict
from fastmcp import FastMCP
# Configure logging to stderr (critical for MCP protocol integrity)
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
stream=sys.stderr
)
logger = logging.getLogger(__name__)
# Create the FastMCP server instance
mcp = FastMCP(name="CalculatorServer")
Сервер импортирует FastMCP, настраивает логи на stderr (MCP требует, чтобы весь вывод кроме протокола шел туда, иначе связь сломается). FastMCP(name="CalculatorServer") создает экземпляр, автоматически управляя протоколом.
Теперь инструменты:
@mcp.tool
def add(a: float, b: float) -> float:
"""
Add two numbers together.
Args:
a: First number
b: Second number
Returns:
Sum of a and b
"""
try:
result = a + b
logger.info(f"Addition performed: {a} + {b} = {result}")
return result
except TypeError as e:
logger.error(f"Type error in add: {e}")
raise ValueError(f"Invalid input types: {e}")
@mcp.tool
def subtract(a: float, b: float) -> float:
"""
Subtract b from a.
Args:
a: First number (minuend)
b: Second number (subtrahend)
Returns:
Difference of a and b
"""
try:
result = a - b
logger.info(f"Subtraction performed: {a} - {b} = {result}")
return result
except TypeError as e:
logger.error(f"Type error in subtract: {e}")
raise ValueError(f"Invalid input types: {e}")
Функции сложения и вычитания в try-except: логируют, возвращают результат или выбрасывают ValueError.
@mcp.tool
def multiply(a: float, b: float) -> float:
"""
Multiply two numbers.
Args:
a: First number
b: Second number
Returns:
Product of a and b
"""
try:
result = a * b
logger.info(f"Multiplication performed: {a} * {b} = {result}")
return result
except TypeError as e:
logger.error(f"Type error in multiply: {e}")
raise ValueError(f"Invalid input types: {e}")
@mcp.tool
def divide(a: float, b: float) -> float:
"""
Divide a by b.
Args:
a: Dividend (numerator)
b: Divisor (denominator)
Returns:
Quotient of a divided by b
Raises:
ValueError: If attempting to divide by zero
"""
try:
if b == 0:
logger.warning(f"Division by zero attempted: {a} / {b}")
raise ValueError("Cannot divide by zero")
result = a / b
logger.info(f"Division performed: {a} / {b} = {result}")
return result
except (TypeError, ZeroDivisionError) as e:
logger.error(f"Error in divide: {e}")
raise ValueError(f"Division error: {e}")
Четыре инструмента (@mcp.tool) для арифметики. Каждый с:
- type hints для аргументов и возврата
- docstring (MCP берет их как описания)
- обработкой ошибок
- логами
- валидацией ввода
Теперь ресурсы:
@mcp.resource("config://calculator/settings")
def get_settings() -> Dict:
"""
Provides calculator configuration and available operations.
Returns:
Dictionary containing calculator settings and metadata
"""
logger.debug("Fetching calculator settings")
return {
"version": "1.0.0",
"operations": ["add", "subtract", "multiply", "divide"],
"precision": "IEEE 754 double precision",
"max_value": 1.7976931348623157e+308,
"min_value": -1.7976931348623157e+308,
"supports_negative": True,
"supports_decimals": True
}
@mcp.resource("docs://calculator/guide")
def get_guide() -> str:
"""
Provides a user guide for the calculator server.
Returns:
String containing usage guide and examples
"""
logger.debug("Retrieving calculator guide")
guide = """
1. **add(a, b)**: Returns a + b
Example: add(5, 3) = 8
2. **subtract(a, b)**: Returns a - b
Example: subtract(10, 4) = 6
3. **multiply(a, b)**: Returns a * b
Example: multiply(7, 6) = 42
4. **divide(a, b)**: Returns a / b
Example: divide(20, 4) = 5.0
## Error Handling
- Division by zero will raise a ValueError
- Non-numeric inputs will raise a ValueError
- All inputs should be valid numbers (int or float)
## Precision
The calculator uses IEEE 754 double precision floating-point arithmetic.
Results may contain minor rounding errors for some operations.
"""
return guide
Два ресурса (@mcp.resource) для статичных и динамичных данных:
config://calculator/settings: метаданные калькулятораdocs://calculator/guide: руководство пользователя- URI в формате
type://category/resource
Промты:
@mcp.prompt
def calculate_expression(expression: str) -> str:
"""
Provides instructions for evaluating a mathematical expression.
Args:
expression: A mathematical expression to evaluate
Returns:
Formatted prompt instructing the LLM how to evaluate the expression
"""
logger.debug(f"Generating calculation prompt for: {expression}")
prompt = f"""
Please evaluate the following mathematical expression step by step:
Expression: {expression}
Instructions:
1. Break down the expression into individual operations
2. Use the appropriate calculator tool for each operation
3. Follow order of operations (parentheses, multiplication/division, addition/subtraction)
4. Show all intermediate steps
5. Provide the final result
Available tools: add, subtract, multiply, divide
"""
return prompt.strip()
Завершите запуск сервера:
if __name__ == "__main__":
logger.info("Starting Calculator MCP Server...")
try:
# Run the server with stdio transport (default for Claude Desktop)
mcp.run(transport="stdio")
except KeyboardInterrupt:
logger.info("Server interrupted by user")
sys.exit(0)
except Exception as e:
logger.error(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
@mcp.prompt генерирует шаблоны инструкций для сложных задач LLM.
Лучшие практики обработки ошибок:
- Ловля конкретных исключений (TypeError, ZeroDivisionError)
- Понятные сообщения ошибок
- Детальные логи
- Корректная передача ошибок
Шаг 3: MCP-клиент
Теперь клиент для взаимодействия с сервером-калькулятором. Создайте calculator_client.py.
import asyncio
import logging
import sys
from typing import Any
from fastmcp import Client, FastMCP
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
stream=sys.stderr
)
logger = logging.getLogger(__name__)
async def main():
"""
Main client function demonstrating server interaction.
"""
from calculator_server import mcp as server
logger.info("Initializing Calculator Client...")
try:
async with Client(server) as client:
logger.info("✓ Connected to Calculator Server")
# DISCOVER CAPABILITIEs
print("\n" + "="*60)
print("1. DISCOVERING SERVER CAPABILITIES")
print("="*60)
# List available tools
tools = await client.list_tools()
print(f"\nAvailable Tools ({len(tools)}):")
for tool in tools:
print(f" • {tool.name}: {tool.description}")
# List available resources
resources = await client.list_resources()
print(f"\nAvailable Resources ({len(resources)}):")
for resource in resources:
print(f" • {resource.uri}: {resource.name or resource.uri}")
# List available prompts
prompts = await client.list_prompts()
print(f"\nAvailable Prompts ({len(prompts)}):")
for prompt in prompts:
print(f" • {prompt.name}: {prompt.description}")
# CALL TOOLS
print("\n" + "="*60)
print("2. CALLING TOOLS")
print("="*60)
# Simple addition
print("\nTest 1: Adding 15 + 27")
result = await client.call_tool("add", {"a": 15, "b": 27})
result_value = extract_tool_result(result)
print(f" Result: 15 + 27 = {result_value}")
# Division with error handling
print("\nTest 2: Dividing 100 / 5")
result = await client.call_tool("divide", {"a": 100, "b": 5})
result_value = extract_tool_result(result)
print(f" Result: 100 / 5 = {result_value}")
# Error case: division by zero
print("\nTest 3: Division by Zero (Error Handling)")
try:
result = await client.call_tool("divide", {"a": 10, "b": 0})
print(f" Unexpected success: {result}")
except Exception as e:
print(f" ✓ Error caught correctly: {str(e)}")
# READ RESOURCES
print("\n" + "="*60)
print("3. READING RESOURCES")
print("="*60)
# Read settings resource
print("\nFetching Calculator Settings...")
settings_resource = await client.read_resource("config://calculator/settings")
print(f" Version: {settings_resource[0].text}")
# Read guide resource
print("\nFetching Calculator Guide...")
guide_resource = await client.read_resource("docs://calculator/guide")
# Print first 200 characters of guide
guide_text = guide_resource[0].text[:200] + "..."
print(f" {guide_text}")
# CHAINING OPERATIONS
print("\n" + "="*60)
print("4. CHAINING MULTIPLE OPERATIONS")
print("="*60)
# Calculate: (10 + 5) * 3 - 7
print("\nCalculating: (10 + 5) * 3 - 7")
# Step 1: Add
print(" Step 1: Add 10 + 5")
add_result = await client.call_tool("add", {"a": 10, "b": 5})
step1 = extract_tool_result(add_result)
print(f" Result: {step1}")
# Step 2: Multiply
print(" Step 2: Multiply 15 * 3")
mult_result = await client.call_tool("multiply", {"a": step1, "b": 3})
step2 = extract_tool_result(mult_result)
print(f" Result: {step2}")
# Step 3: Subtract
print(" Step 3: Subtract 45 - 7")
final_result = await client.call_tool("subtract", {"a": step2, "b": 7})
final = extract_tool_result(final_result)
print(f" Final Result: {final}")
# GET PROMPT TEMPLATE
print("\n" + "="*60)
print("5. USING PROMPT TEMPLATES")
print("="*60)
expression = "25 * 4 + 10 / 2"
print(f"\nPrompt Template for: {expression}")
prompt_response = await client.get_prompt(
"calculate_expression",
{"expression": expression}
)
print(f" Template:\n{prompt_response.messages[0].content.text}")
logger.info("✓ Client operations completed successfully")
except Exception as e:
logger.error(f"Client error: {e}", exc_info=True)
sys.exit(1)
Клиент использует async with Client(server) для безопасного управления соединением: авто-подключение и очистка.
Вспомогательная функция для результатов:
def extract_tool_result(response: Any) -> Any:
"""
Extract the actual result value from a tool response.
MCP wraps results in content objects, this helper unwraps them.
"""
try:
if hasattr(response, 'content') and response.content:
content = response.content[0]
# Prefer explicit text content when available (TextContent)
if hasattr(content, 'text') and content.text is not None:
# If the text is JSON, try to parse and extract a `result` field
import json as _json
text_val = content.text
try:
parsed_text = _json.loads(text_val)
# If JSON contains a result field, return it
if isinstance(parsed_text, dict) and 'result' in parsed_text:
return parsed_text.get('result')
return parsed_text
except _json.JSONDecodeError:
# Try to convert plain text to number
try:
if '.' in text_val:
return float(text_val)
return int(text_val)
except Exception:
return text_val
# Try to extract JSON result via model `.json()` or dict-like `.json`
if hasattr(content, 'json'):
try:
if callable(content.json):
json_str = content.json()
import json as _json
try:
parsed = _json.loads(json_str)
except _json.JSONDecodeError:
return json_str
else:
parsed = content.json
# If parsed is a dict, try common shapes
if isinstance(parsed, dict):
# If nested result exists
if 'result' in parsed:
res = parsed.get('result')
elif 'text' in parsed:
res = parsed.get('text')
else:
res = parsed
# If res is str that looks like a number, convert
if isinstance(res, str):
try:
if '.' in res:
return float(res)
return int(res)
except Exception:
return res
return res
return parsed
except Exception:
pass
return response
except Exception as e:
logger.warning(f"Could not extract result: {e}")
return response
if __name__ == "__main__":
logger.info("Calculator Client Starting...")
asyncio.run(main())
Сначала клиент перечисляет возможности: await client.list_tools() — метаданные инструментов, await client.list_resources() — ресурсы, await client.list_prompts() — промты.
await client.call_tool() принимает имя инструмента и параметры-словарь, возвращает объект с результатом, интегрируется с обработкой ошибок.
extract_tool_result() разбирает формат ответа MCP, извлекает значение из JSON или текста.
Цепочка операций показывает, как вывод одного инструмента подается на вход другого для сложных вычислений.
Обработка ошибок ловит сбои инструментов (деление на ноль) и логирует без краха.
Шаг 4: Запуск сервера и клиента
Откройте два терминала. В первом запустите сервер:
python calculator_server.py
Во втором — клиент:
python calculator_client.py
Продвинутые приемы с FastMCP
Пример с калькулятором базовый, но FastMCP готов к сложным сценариям. При масштабе используйте:
- Асинхронные операции:
async defдля инструментов с I/O (БД, API) - Динамичные ресурсы: с аргументами, напр.
resource://users/{user_id}для конкретных данных - Сложная валидация: Pydantic или продвинутые type hints для точного формата от LLM
- Кастомные транспорты: помимо
stdio— SSE для веба и UI-инструментов
Заключение
FastMCP соединяет сложный протокол MCP с чистым декораторным опытом, привычным для Python-разработчиков. Без шаблонов JSON-RPC 2.0 и ручного управления транспортами фреймворк позволяет сосредоточиться на инструментах, усиливающих LLM.
Мы разобрали:
- Архитектуру MCP (серверы против клиентов)
- Определение инструментов для действий, ресурсов для данных, промтов для инструкций
- Создание клиента для тестов и цепочек логики сервера
FastMCP — самый pythonic путь к готовой экосистеме агентов, от простых утилит до оркестрации данных.