MCP resource template ломает имя с пробелом: проверяем кодирование URI
MCP-сервер публикует шаблон docs://catalog/{name}. Ресурс readme открывается, а документ «Отчёт май» не находится. В одной версии клиента пробел превращается в плюс, в другой слеш внутри имени становится новым уровнем пути. Ошибка выглядит как отсутствие документа, хотя сервер хранит его под правильным именем. Причина — разные представления одного идентификатора на границе шаблона и URI.
В MCP ресурсы читаются по URI. Шаблон помогает построить адрес, однако не отменяет правила кодирования. Важно заранее определить смысл параметра: имя одного документа, путь из нескольких сегментов или произвольная строка запроса. Для имени одного документа символ slash должен оставаться частью значения после декодирования, а не становиться разделителем маршрута.
Договоритесь о форме идентификатора
Возьмём каталог, в котором документ имеет логическое имя «Отчёт май/июнь». Здесь slash — часть имени, не вложенный каталог. Клиент должен получить URI docs://catalog/ с одним закодированным параметром. Пробел кодируется как %20, кириллица — через UTF-8 последовательности, slash — как %2F. На сервере значение декодируется один раз после выделения параметра.
Если использовать quote с настройками по умолчанию в Python, slash останется разрешённым символом. Для выбранного контракта нужен safe="". А функция quote_plus подходит для значений формы query, где пробел обозначается плюсом; переносить это правило на сегмент пути нельзя. urllib.parse.quote позволяет явно задать набор символов, сохраняемых без кодирования.
Ниже проверяется простой шаблон с одним параметром на уровне протокольного сообщения. Пример не реализует все операции URI Template и не использует MCP SDK. Он строит реальный resources/read JSON-RPC запрос и восстанавливает исходное имя из uri. Это помогает отдельно проверить проблемную границу до подключения транспорта и сервера.
import json
from urllib.parse import quote, unquote, urlsplit
name = "Отчёт май/июнь"
uri = "docs://catalog/" + quote(name, safe="")
message = json.dumps({"jsonrpc": "2.0", "id": 7,
"method": "resources/read", "params": {"uri": uri}})
request = json.loads(message)
parsed = urlsplit(request["params"]["uri"])
assert parsed.scheme == "docs" and parsed.netloc == "catalog"
assert parsed.path.count("/") == 1
restored = unquote(parsed.path[1:], encoding="utf-8", errors="strict")
assert restored == name
assert unquote(quote("%2F", safe="")) == "%2F"
print(uri)
print(request["method"], restored)
Первая строка показывает закодированный URI, где slash входит в %2F. Во второй строке напечатан метод resources/read и восстановленное имя «Отчёт май/июнь». Проверка значения %2F дополнительно подтверждает, что один проход декодирования сохраняет буквальные символы процента, двойки и F из исходного имени. При втором декодировании они неожиданно превратились бы в slash.
Как применить проверку в настоящем подключении
Сначала получите шаблон через resources/templates/list и убедитесь, что клиент использует именно его, а не сохранённый старый путь. Затем найдите исходящий resources/read и сравните uri с логическим именем. Проверьте порядок действий в обработчике ресурса: если сначала применить unquote ко всей строке URI, %2F превратится в slash до выделения сегмента. Имя документа тогда ошибочно разделится на части. Сначала разберите URI и выделите закодированный сегмент, затем декодируйте его ровно один раз, как в примере.
Для сложных шаблонов используйте библиотеку URI Template с поддержкой нужных операций. Запись {name} и операция с сохранением зарезервированных символов имеют разную семантику. Самодельная замена фигурных скобок удобна только для строго ограниченного шаблона из примера. Шаблоны ресурсов MCP ссылаются на правила URI Template, поэтому проверять нужно и сам шаблон, и выбранный алгоритм раскрытия.
Запрос пользователя агенту может звучать так: «Прочитай отчёт май/июнь и перечисли открытые риски». Host должен сначала сопоставить пользовательское название с ресурсом, затем прочитать его по корректному URI и передать текст модели. Кодирование названия не определяет право доступа. Сервер проверяет разрешение на восстановленный идентификатор до чтения документа.
Какие примеры оставить в регрессии
Проверьте пробел, плюс, процент, кириллицу и slash. Для каждого имени сравните восстановленное значение с исходным побуквенно. Добавьте пару, которую особенно легко спутать: строку %2F и настоящий slash. Если они дают один объект без специального правила каталога, между компонентами есть лишнее декодирование или неверное сопоставление.
Платный курс «AI Agent Integrations PRO: MCP» включает сервер с resources и prompt-шаблонами, клиентский цикл чтения контекста, тесты и ограничения доступа. Следующий шаг — проверить те же имена через настоящий транспорт и обработчик сервера. Откройте программу курса, чтобы собрать интеграцию от пользовательского названия до безопасного чтения ресурса.
Продолжить на практике
- AI Agent Integrations PRO: MCP ↗
Вы уже работали с LLM и понимаете, что подключать их к реальным системам — больно. MCP — это стандарт, который делает эту боль меньше: один контракт вместо десятка костылей, типизированные инструменты вместо голого доступа к базе, тесты и логи вместо молитв. На курсе соберём сервер с нуля и выведем в production-контур.
