Промпты в клиенте
Промпты на сервере есть, осталось научить клиент их использовать. Промпты контролирует человек: клиент должен показать, какие есть, дать выбрать и заполнить аргументы, получить сообщения и начать с них разговор. Сделаем это через команды со слешем — как в Claude Code. Заодно соберём окончательную версию client.py, в которой работают все три примитива.
Список промптов при старте
Пользователь должен видеть, что можно вызвать. Добавьте в main после вывода инструментов:
prompts = [p.name for p in (await client.list_prompts()).prompts]
print("Промпты:", ", ".join("/" + p for p in prompts))list_prompts возвращает объект с полем prompts; у каждого промпта есть name, title, description и arguments — список с name, description и required. Для подсказки достаточно имён, но в настоящем интерфейсе стоит показывать title и аргументы: именно для этого они и передаются.
Команда → сообщения
Договоримся: строка, начинающаяся с /, — это вызов промпта. Первое слово — имя, остальные — аргументы по порядку. /summarize brief.md вызовет summarize с name="brief.md", /to_theses risks.md заказчик — to_theses с name="risks.md" и audience="заказчик".
Добавьте в client.py:
async def messages_for(client: Client, query: str) -> list[dict]:
"""Превратить ввод пользователя в начальные сообщения для модели."""
if query.startswith("/"):
name, *args = query[1:].split()
prompts = {p.name: p for p in (await client.list_prompts()).prompts}
if name not in prompts:
raise ValueError(f"Нет промпта {name!r}. Есть: {', '.join(prompts)}")
arg_names = [a.name for a in (prompts[name].arguments or [])]
result = await client.get_prompt(name, dict(zip(arg_names, args)))
return [
{"role": m.role, "content": m.content.text}
for m in result.messages
if isinstance(m.content, TextContent)
]
return [{"role": "user", "content": await attach_docs(client, query)}]Разберём. Мы получаем список промптов и по имени находим нужный, чтобы узнать порядок его аргументов: протокол передаёт аргументы словарём «имя → значение», а пользователь вводит их по порядку, и zip сопоставляет одно с другим. Лишние аргументы отбрасываются; недостающие обязательные вызовут ошибку от сервера ещё до выполнения функции промпта. Текст этой ошибки обезличен («Internal server error»), поэтому в настоящем клиенте стоит проверять обязательные аргументы по списку arguments самостоятельно и подсказывать пользователю, чего не хватает.
get_prompt возвращает объект с полем messages. У каждого сообщения есть role (user или assistant) и content; для текстовых промптов это TextContent с полем text — тот же тип, что у результатов инструментов. Мы переводим сообщения в формат Anthropic API: словарь с role и content. Если промпт вернул несколько сообщений (диалог с примерами), они все попадут в историю в нужном порядке.
Обычный запрос без слеша идёт прежним путём — через attach_docs с упоминаниями.
Собираем main
Окончательная версия:
async def main() -> None:
server = StdioServerParameters(command="uv", args=["run", "docs_server.py"])
async with Client(server) as client:
names = [t.name for t in (await client.list_tools()).tools]
print("Подключено. Инструменты:", ", ".join(names))
prompts = [p.name for p in (await client.list_prompts()).prompts]
print("Промпты:", ", ".join("/" + p for p in prompts))
while True:
try:
query = (await asyncio.to_thread(input, "\n> ")).strip()
except EOFError:
break
if not query or query.lower() in {"quit", "exit"}:
break
try:
print(await run(client, await messages_for(client, query)))
except Exception as e:
print("Ошибка:", e)Функция run из урока про клиент не изменилась: она принимает список сообщений и крутит цикл инструментов. Ей всё равно, откуда взялось первое сообщение — набрал его пользователь, собрал промпт или дополнили ресурсы.
Запуск
uv run client.pyНаберите /summarize brief.md. В stderr появится [вызов read_doc {'name': 'brief.md'}] — промпт попросил модель прочитать документ инструментом, и она это сделала, — а затем выжимка в три-пять предложений. Наберите /to_theses risks.md заказчик и получите тезисы для заказчика; без второго аргумента — для команды разработки. Наберите /nope и увидите сообщение об ошибке со списком доступных промптов.
Посмотрите, что произошло в последовательности: человек выбрал промпт (примитив человека), промпт велел модели вызвать инструмент (примитив модели), инструмент прочитал документ. А можно было бы приложить документ ресурсом (примитив приложения) прямо в промпте, и инструмент бы не понадобился. Три примитива работают вместе, и вы теперь понимаете, какой за что отвечает.
Как это делает Claude Code
В Claude Code промпты подключённых серверов появляются в списке команд как /mcp__project-docs__summarize. Имя сервера в команде нужно, потому что серверов может быть несколько, и у двух могут совпасть имена промптов. Хост вызывает prompts/get, получает сообщения и отправляет их модели — ровно наша messages_for, только с формой для аргументов вместо разбора по пробелам.
Что мы построили
Клиент теперь умеет всё, что нужно минимальному хосту:
- запускает сервер по stdio и договаривается о протоколе;
- передаёт инструменты модели и выполняет цикл вызовов;
- прикладывает ресурсы по упоминанию
@имя; - запускает промпты командами
/имя аргументы.
Сто с небольшим строк. Всё остальное в настоящих хостах — интерфейс, подтверждения, несколько серверов, переподключение — надстройки над этими четырьмя вещами.
Попробуйте сами
10–15 мин на рабочем месте- Выведите при старте не имена, а
titleи аргументы каждого промпта:/summarize <name>,/to_theses <name> [audience]. Квадратные скобки — для необязательных. - Разрешите в аргументах промпта кавычки, чтобы
/to_theses risks.md "финансовый директор"передавал аудиторию из двух слов. Модульshlexиз стандартной библиотеки сделает разбор за вас. - Добавьте промпту
summarizeна сервере необязательный аргументlengthсо значениями «короткая» и «подробная», и проверьте из клиента, что оба варианта работают без изменений вclient.py. Это и есть смысл протокола: сервер меняется, клиент — нет.
Коротко
list_promptsдаёт имена, заголовки и аргументы; показывайте их пользователю как команды.get_prompt(имя, {аргумент: значение})возвращаетmessagesсroleиcontent; текст — вcontent.text.- Сообщения промпта переводятся в формат API один к одному и становятся началом разговора.
- Недостающие обязательные аргументы отклоняет сервер до вызова функции.
- Функции
runвсё равно, откуда взялись сообщения: от пользователя, из промпта или с ресурсами. - Клиент в сто строк умеет всё, что нужно минимальному хосту; остальное — надстройки.
Видеоверсия
Сценарий озвучки · 451 слово, ≈ 3 мин
Промпты на сервере есть, осталось научить клиент их использовать. Промпты контролирует человек, значит, клиент должен показать, какие есть, дать выбрать, заполнить аргументы, получить сообщения и начать с них разговор. Сделаем это через команды со слешем, как в Claude Code, и заодно соберём окончательную версию клиента.
Сначала список при старте. Клиент запрашивает промпты у сервера и печатает их имена со слешем впереди, рядом со списком инструментов. У каждого промпта в ответе есть имя, заголовок, описание и аргументы с флагом обязательности — в настоящем интерфейсе стоит показывать всё это.
Теперь команда. Договоримся: строка со слеша — это вызов промпта. Первое слово — имя, остальные — аргументы по порядку. Функция находит промпт по имени, чтобы узнать порядок его аргументов: протокол передаёт их словарём «имя — значение», а пользователь вводит по порядку, и мы сопоставляем одно с другим. Если промпта с таким именем нет, показываем список доступных. Если не хватает обязательного аргумента, сервер вернёт ошибку ещё до выполнения функции — её тоже увидит пользователь.
Дальше запрос промпта. Сервер возвращает список сообщений, у каждого роль и содержимое. Мы переводим их в формат Anthropic API один к одному: роль и текст. Если промпт вернул несколько сообщений — диалог с примерами, — все они попадут в историю в нужном порядке. Обычный запрос без слеша идёт прежним путём, через упоминания ресурсов.
Функция, которая крутит цикл инструментов, не изменилась. Ей всё равно, откуда взялось первое сообщение — набрал его пользователь, собрал промпт или дополнили ресурсы.
Запускаем. Набираем слеш, «саммарайз», имя брифа. В потоке ошибок появляется вызов чтения документа — промпт попросил модель взять документ инструментом, и она это сделала, — а потом выжимка. Набираем «тезисы» с именем документа и аудиторией «заказчик» — получаем тезисы для заказчика. Без аудитории — для команды разработки, это значение по умолчанию.
Посмотрите, что произошло. Человек выбрал промпт — примитив человека. Промпт велел модели вызвать инструмент — примитив модели. А можно было приложить документ ресурсом — примитивом приложения — прямо в промпте, и инструмент бы не понадобился. Три примитива работают вместе, и вы теперь понимаете, какой за что отвечает.
Claude Code делает то же самое: промпты серверов появляются в списке команд с именем сервера в названии — потому что серверов может быть несколько. Хост запрашивает промпт, получает сообщения и отправляет модели, только с формой для аргументов вместо разбора по пробелам.
Что мы построили. Клиент запускает сервер, передаёт инструменты модели и крутит цикл вызовов, прикладывает ресурсы по упоминанию, запускает промпты командами. Сто с небольшим строк. Всё остальное в настоящих хостах — интерфейс, подтверждения, несколько серверов — надстройки над этими четырьмя вещами.
Одна тонкость про недостающие аргументы. Если пользователь не передал обязательный аргумент, сервер вернёт ошибку, но её текст обезличен — просто «внутренняя ошибка сервера». Поэтому в настоящем клиенте стоит проверять обязательные аргументы по списку из ответа сервера самостоятельно и подсказывать пользователю, чего именно не хватает.
Последний урок — короткий обзор того, что мы построили, и указатель, куда двигаться дальше.
