Зачем LLM нужны «глаза» и почему Tools не справляются
Представьте, что вы даете ChatGPT задание: «Проанализируй наш лог-файл за сегодня». Модель вежливо отвечает: «Я не могу читать файлы». Классическая проблема. Tools (инструменты) в MCP решают ее частично — они позволяют LLM выполнять действия. Но есть нюанс.
Tools — это глаголы. «Прочитать файл», «Выполнить запрос к БД», «Получить данные из API». Resources — это существительные. «Вот этот конкретный файл», «Эта таблица в базе», «Этот эндпоинт». Разница фундаментальная.
На 12.02.2026 Model Context Protocol (MCP) версии 1.2 поддерживает два типа Resources: статические (static) и динамические (dynamic). Статические регистрируются один раз при запуске сервера, динамические создаются по запросу через Tools.
Resources против Tools: битва концепций
Давайте разберем на примере. Допустим, у вас есть папка с документами для RAG-системы. Как LLM должна с ними работать?
| Подход с Tools | Подход с Resources |
|---|---|
| LLM вызывает tool «read_file» с путем | LLM видит ресурс «file://docs/contract.pdf» |
| Нужно знать точный путь к файлу | Ресурсы перечислены в контексте |
| Каждый запрос — отдельное действие | Ресурс доступен для ссылок в диалоге |
| Нет структуры каталогов | Можно организовать по URI-схемам |
Проблема Tools в том, что они требуют от LLM точного знания о том, что существует. Модель должна догадаться, что есть файл «/var/log/app/error.log». Resources решают это — они показывают, что доступно. Как карта перед путешественником.
URI в MCP: не просто протокол, а философия
Каждый Resource в MCP имеет URI. Это не случайно. URI — универсальный идентификатор, который понимают и люди, и машины. На 12.02.2026 MCP поддерживает несколько схем:
- file:// — локальные файлы и директории
- postgresql:// — таблицы и представления БД
- http:// и https:// — веб-ресурсы
- s3:// — объекты в облачном хранилище
- custom:// — ваши собственные схемы
URI делает Resources самодокументируемыми. «postgresql://prod/users?limit=100» сразу говорит: это таблица users из продакшн-базы, первые 100 записей.
FastMCP: Resources в три строчки кода
FastMCP — самый популярный фреймворк для создания MCP-серверов на Python. На 12.02.2026 актуальная версия — 0.9.8 с полной поддержкой MCP 1.2. Давайте посмотрим, как регистрировать Resources.
1 Статические Resources: файловая система
Самый простой случай — дать LLM доступ к директории с документами:
from fastmcp import FastMCP
import os
mcp = FastMCP("File Resources Example")
# Регистрируем все PDF-файлы в директории
@mcp.resource("file://docs/{filename}")
def get_pdf_resource(filename: str):
filepath = f"docs/{filename}"
if not os.path.exists(filepath):
return None
with open(filepath, 'rb') as f:
content = f.read()
return {
"contents": [{
"uri": f"file://docs/{filename}",
"mimeType": "application/pdf",
"text": extract_text_from_pdf(content) # ваша функция
}]
}
# Регистрируем директорию как ресурс
@mcp.resource("file://docs/")
def list_docs():
files = os.listdir("docs")
return {
"contents": [{
"uri": f"file://docs/{f}",
"name": f,
"description": f"Документ {f}"
} for f in files if f.endswith('.pdf')]
}
if __name__ == "__main__":
mcp.run()
Теперь LLM видит ресурс «file://docs/» — список всех PDF. И может обратиться к конкретному файлу «file://docs/contract_2026.pdf». Без вызова tools, просто по ссылке.
2 Динамические Resources: база данных
Статические ресурсы хороши для неизменных данных. Но что если нужно дать доступ к результатам запроса? Здесь нужна связка Tool + Resource.
from fastmcp import FastMCP
import psycopg2
from datetime import datetime
mcp = FastMCP("Database Resources")
# Tool для создания ресурса с результатами запроса
@mcp.tool()
def query_users(role: str = None, limit: int = 50):
"""Выполнить запрос к таблице users и создать ресурс с результатами"""
conn = psycopg2.connect("dbname=prod user=admin")
cursor = conn.cursor()
query = "SELECT id, name, email, role FROM users"
params = []
if role:
query += " WHERE role = %s"
params.append(role)
query += f" LIMIT {limit}"
cursor.execute(query, params)
results = cursor.fetchall()
conn.close()
# Создаем уникальный URI для этого запроса
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
resource_uri = f"postgresql://prod/users/query_{timestamp}"
# Регистрируем динамический ресурс
@mcp.resource(resource_uri)
def get_query_results():
return {
"contents": [{
"uri": resource_uri,
"mimeType": "application/json",
"text": json.dumps([
{"id": r[0], "name": r[1], "email": r[2], "role": r[3]}
for r in results
])
}]
}
return {
"resource_uri": resource_uri,
"count": len(results)
}
LLM вызывает tool «query_users» с параметрами. Tool выполняет запрос и регистрирует новый Resource с результатами. Теперь модель может ссылаться на этот ресурс в диалоге: «Согласно данным в postgresql://prod/users/query_20260212_143022...»
Важно: динамические ресурсы живут только во время сессии. При перезапуске сервера они исчезают. Для постоянного доступа используйте статические ресурсы или кэшируйте результаты.
Практический кейс: HR-автоматизация с Resources
Вернемся к примеру из статьи про HR-автоматизацию. Вместо того чтобы загружать резюме через tools каждый раз, создаем MCP-сервер с Resources:
from fastmcp import FastMCP
import sqlite3
from pathlib import Path
mcp = FastMCP("HR Resume Resources")
# Ресурс: все резюме в базе
@mcp.resource("hrdb://resumes/")
def list_all_resumes():
conn = sqlite3.connect("hr_database.db")
cursor = conn.cursor()
cursor.execute("""
SELECT id, candidate_name, position, date_added
FROM resumes
ORDER BY date_added DESC
""")
resumes = cursor.fetchall()
conn.close()
return {
"contents": [{
"uri": f"hrdb://resumes/{r[0]}",
"name": f"{r[1]} - {r[2]}",
"description": f"Добавлено: {r[3]}"
} for r in resumes]
}
# Ресурс: конкретное резюме
@mcp.resource("hrdb://resumes/{resume_id}")
def get_resume(resume_id: str):
conn = sqlite3.connect("hr_database.db")
cursor = conn.cursor()
cursor.execute("""
SELECT candidate_name, position, experience, skills, raw_text
FROM resumes WHERE id = ?
""", (resume_id,))
result = cursor.fetchone()
conn.close()
if not result:
return None
return {
"contents": [{
"uri": f"hrdb://resumes/{resume_id}",
"mimeType": "application/json",
"text": json.dumps({
"name": result[0],
"position": result[1],
"experience": result[2],
"skills": result[3],
"full_text": result[4]
})
}]
}
# Tool для поиска по резюме
@mcp.tool()
def search_resumes(skill: str = None, min_experience: int = None):
"""Найти резюме по критериям и создать ресурс с результатами"""
# ... логика поиска ...
# Создаем динамический ресурс с результатами
return {"resource_uri": "hrdb://search/results_..."}
HR-менеджер говорит LLM: «Посмотри резюме на позицию Senior DevOps». Модель видит ресурс «hrdb://resumes/», выбирает подходящие кандидатов, создает новый ресурс с отфильтрованным списком. Все прозрачно, все по ссылкам.
Ошибки, которые все совершают (и как их избежать)
Ошибка 1: Регистрировать все подряд как Resources
Не нужно превращать каждый инструмент в ресурс. Resources — для данных, которые имеют идентификатор (URI) и могут быть прочитаны. Системный вызов «перезагрузить сервис» — это Tool. Лог-файл «/var/log/nginx/access.log» — это Resource.
Ошибка 2: Игнорировать MIME-типы
MCP 1.2 требует указывать mimeType для содержимого ресурса. Если не указать, клиент (Claude, Cursor, и т.д.) не поймет, как обрабатывать данные. Для JSON — «application/json», для текста — «text/plain», для PDF — «application/pdf».
# ПЛОХО
return {"contents": [{"uri": "...", "text": data}]}
# ХОРОШО
return {
"contents": [{
"uri": "...",
"mimeType": "application/json",
"text": json.dumps(data)
}]
}
Ошибка 3: Забывать про аутентификацию
Resources с доступом к базам данных или API требуют credentials. Никогда не хардкодьте пароли в код. Используйте переменные окружения, секреты, или интеграцию с MCP Hangar для централизованного управления доступом.
Resources и семантический пайплайн
Если вы строите семантический пайплайн для LLM, Resources становятся естественными точками входа. Каждый этап обработки данных может регистрировать ресурс:
- Сырые данные → Resource «raw://dataset/2026-02/»
- После очистки → Resource «cleaned://dataset/2026-02/»
- После векторизации → Resource «embeddings://dataset/2026-02/»
- После индексации → Resource «index://dataset/2026-02/»
LLM видит всю цепочку. Может сказать: «Возьми данные из cleaned://..., создай embeddings, положи в index://...». Прозрачно, отслеживаемо, без магии.
Будущее Resources: что ждет к 2027 году
На 12.02.2026 MCP Resources — уже мощный инструмент. Но что дальше? По слухам (и по issue в репозитории MCP):
- Версионирование ресурсов — «file://docs/contract.pdf@v2»
- Дифференциальный доступ — только metadata без полного содержимого
- Подписки на изменения — push-уведомления при обновлении ресурса
- Кросс-серверные ссылки — ресурс с одного MCP-сервера ссылается на ресурс с другого
Самое интересное — интеграция с RLM (Recursive Language Model). Представьте: LLM не только читает ресурсы, но и создает новые ресурсы (отчеты, аналитику), которые становятся доступны другим LLM. Рекурсивная система, где данные порождают данные.
P.S. Если ваша LLM все еще «слепая» — время дать ей Resources. Не Tools, которые она должна угадывать, а Resources, которые она может видеть. Разница как между «ищи иголку в стоге сена» и «вот карта стога, вот где иголки».