Напишите свой MCP-client и выполняйте discovery
Клиент соединяется с одним сервером, согласует редакцию протокола, получает списки возможностей и только после этого вызывает методы. Порядок здесь не формальность: если зашить список инструментов в код, вы теряете главную ценность MCP - способность работать с серверами, которых не видели при компиляции, - и заодно создаёте скрытую несовместимость, которая всплывёт на чужом сервере.
Весь путь клиента укладывается в короткую последовательность.
- Connect
- Negotiate era
- Discover
- Call
- Validate
- Close
А вот тот же путь в коде: подключение, discovery, чтение ресурса и вызов инструмента.
const client = new Client(
{ name: "developer-release-client", version: "1.0.0" },
{
versionNegotiation: { mode: "auto" },
enforceStrictCapabilities: true,
defaultCacheTtlMs: 5_000
}
);
try {
await client.connect(transport);
const [{ tools }, { resources }, { prompts }, policy, review] = await Promise.all([
client.listTools(),
client.listResources(),
client.listPrompts(),
client.readResource({ uri: "runbook://release-policy" }),
client.callTool({
name: "review_release",
arguments: { service: "web-portal", version: "2.4.0" }
})
]);
const policyTitle = resources.find((resource) => resource.uri === "runbook://release-policy")?.title
?? "Политика выпуска";
return {
era: client.getProtocolEra(),
tools: tools.map((tool) => tool.name),
resources: resources.map((resource) => resource.uri),
prompts: prompts.map((prompt) => prompt.name),
decision: readDecision(review.structuredContent),
policyTitle: policy.contents.length > 0 ? policyTitle : "missing"
};
} finally {
await client.close();
}А вот что демо реально печатает - согласованная редакция, найденные инструменты, ресурсы и prompts, и решение по релизу.
{
"era": "modern",
"tools": [
"search_runbooks",
"review_release"
],
"resources": [
"runbook://release-policy",
"release://web-portal/2.4.0",
"release://payments-api/1.8.0"
],
"prompts": [
"prepare-release-review"
],
"decision": "ready",
"policyTitle": "Политика выпуска"
}За этим стоит устойчивый конвейер, который стоит повторять в любом клиенте.
- Создать транспорт и протокольный клиент.
- Подключиться и проверить согласованную редакцию.
- Получить объявленные сервером capabilities.
- Запросить списки, уважая pagination,
ttlMsиcacheScope. - Показать модели или пользователю только разрешённое подмножество.
- Провалидировать результат и закрыть соединение в
finally.
Два правила легко упустить, но они принципиальны. Первое отделяет видимость от прав.
Discovery не является authorization. То, что инструмент появился в списке, не означает право конкретного пользователя вызвать его с любыми аргументами. Host может скрыть capability из UX, но server обязан проверить доступ при каждом вызове.
Второе объясняет, почему сервер из прошлой главы возвращал инструменты в стабильном порядке.
Детерминированный порядок помогает cache. Редакция 2026-07-28 рекомендует возвращать инструменты в стабильном порядке. Это снижает diff-шум и улучшает повторное использование model prompt cache.
Клиент умеет находить и вызывать - теперь спроектируем сами возможности так, чтобы их было безопасно давать модели. Начнём с инструментов.