Глава 10

Напишите свой MCP-client и выполняйте discovery

Клиент соединяется с одним сервером, согласует редакцию протокола, получает списки возможностей и только после этого вызывает методы. Порядок здесь не формальность: если зашить список инструментов в код, вы теряете главную ценность MCP - способность работать с серверами, которых не видели при компиляции, - и заодно создаёте скрытую несовместимость, которая всплывёт на чужом сервере.

Весь путь клиента укладывается в короткую последовательность.

  1. Connect
  2. Negotiate era
  3. Discover
  4. Call
  5. Validate
  6. Close

А вот тот же путь в коде: подключение, discovery, чтение ресурса и вызов инструмента.

TypeScript
  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, и решение по релизу.

JSON
{
  "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.

Клиент умеет находить и вызывать - теперь спроектируем сами возможности так, чтобы их было безопасно давать модели. Начнём с инструментов.

Ссылки