Верх страницы
Обложка к записи Code Mode: лучший способ использования MCP
Время для прочтения: 3 мин. 0 сек.

Code Mode: лучший способ использования MCP

Оказывается, мы все неправильно использовали MCP-сервера.

Большинство современных агентов используют MCP, напрямую предоставляя «инструменты» (tools) LLM.

Мы попробовали нечто иное: преобразовать инструменты MCP в TypeScript API, а затем попросить LLM писать код, который вызывает это API.

Результаты впечатляют:

  1. Мы обнаружили, что агенты способны работать с гораздо большим количеством инструментов и с более сложными инструментами, когда те представлены как TypeScript API, а не напрямую. Возможно, это связано с тем, что в обучающей выборке LLM огромное количество реального TypeScript-кода, но лишь небольшое количество искусственных примеров вызовов инструментов.
  2. Этот подход по-настоящему раскрывается, когда агенту нужно объединить несколько вызовов. При традиционном подходе результат каждого вызова инструмента должен поступать в нейронную сеть LLM только для того, чтобы быть скопированным во входные данные следующего вызова — трата времени, энергии и токенов. Когда же LLM может писать код, она пропускает всё это и считывает только конечные результаты.

Короче говоря: LLM лучше пишут код для вызова MCP, чем вызывают MCP напрямую.

Что такое MCP?

Для тех, кто не знаком: Model Context Protocol — это стандартный протокол для предоставления AI-агентам доступа к внешним инструментам, чтобы они могли напрямую выполнять работу, а не просто общаться с вами.

Иначе говоря, MCP — это унифицированный способ:

  • предоставить API для выполнения определённых действий,
  • вместе с документацией, необходимой LLM для его понимания,
  • при этом авторизация обрабатывается вне полосы пропускания.

MCP произвёл фурор в течение 2025 года, поскольку значительно расширил возможности AI-агентов.

«API», предоставляемый MCP-сервером, выражается в виде набора «инструментов» (tools). Каждый инструмент по сути является вызовом удалённой процедуры (RPC) — он вызывается с некоторыми параметрами и возвращает ответ. Большинство современных LLM поддерживают использование «инструментов» (иногда называемое «function calling»), то есть они обучены выводить текст в определённом формате, когда хотят вызвать инструмент. Программа, вызывающая LLM, распознаёт этот формат и вызывает указанный инструмент, а затем передаёт результат обратно в LLM.

Анатомия вызова инструмента

Под капотом LLM генерирует поток «токенов», представляющих её вывод. Токен может представлять слово, слог, знак препинания или другой компонент текста.

Однако вызов инструмента предполагает использование токена, который не имеет текстового эквивалента. LLM обучена (или, чаще, дообучена) понимать специальный токен, который она может вывести и который означает «далее следует интерпретировать как вызов инструмента», и ещё один специальный токен, означающий «конец вызова инструмента». Между этими двумя токенами LLM обычно выводит токены, соответствующие некоторому JSON-сообщению, описывающему вызов.

Например, представьте, что вы подключили агент к MCP-серверу, предоставляющему информацию о погоде, а затем спросили агента о погоде в Остине, штат Техас. Под капотом LLM может сгенерировать следующий вывод. Обратите внимание, что здесь мы используем слова в <| и |> для представления наших специальных токенов, но на самом деле эти токены не представляют текст — это просто для наглядности.

Я воспользуюсь Weather MCP сервером, чтобы узнать погоду в Остине, Техас.

<|tool_call|>
{
  "name": "get_current_weather",
  "arguments": {
    "location": "Austin, TX, USA"
  }
}
<|end_tool_call|>

Обнаружив эти специальные токены в выводе, обёртка LLM интерпретирует последовательность как вызов инструмента. После обнаружения конечного токена обёртка приостанавливает выполнение LLM. Она разбирает JSON-сообщение и возвращает его как отдельный компонент структурированного результата API. Агент, вызывающий LLM API, видит вызов инструмента, обращается к соответствующему MCP-серверу, а затем отправляет результаты обратно в LLM API. Обёртка LLM использует другой набор специальных токенов, чтобы передать результат обратно в LLM:

<|tool_result|>
{ "location": "Austin, TX, USA", "temperature": 93, "unit": "fahrenheit", "conditions": "sunny" }
<|end_tool_result|>

LLM считывает эти токены точно так же, как считывала бы ввод от пользователя — с той лишь разницей, что пользователь не может генерировать эти специальные токены, поэтому LLM знает, что это результат вызова инструмента. Затем LLM продолжает генерировать вывод как обычно.

Разные LLM могут использовать разные форматы для вызова инструментов, но основная идея именно такая.

Что с этим не так?

Специальные токены, используемые в вызовах инструментов — это то, что LLM никогда не видели в реальном мире. Их приходится специально обучать использовать инструменты на основе синтетических обучающих данных. И у них это получается не всегда хорошо. Если предоставить LLM слишком много инструментов или слишком сложные инструменты, она может испытывать затруднения с выбором правильного инструмента или его корректным использованием. В результате разработчиков MCP-серверов призывают предоставлять значительно упрощённые API по сравнению с традиционными API, которые они могли бы предоставить разработчикам.

Тем временем LLM становятся всё лучше в написании кода. На самом деле, когда LLM просят написать код для полноценных, сложных API, обычно предоставляемых разработчикам, у них, как правило, не возникает серьёзных проблем. Почему же тогда интерфейсы MCP должны быть «упрощёнными»? Написание кода и вызов инструментов — это почти одно и то же, но, похоже, LLM справляются с одним гораздо лучше, чем с другим?

Ответ прост: LLM видели много кода. Они не видели много «вызовов инструментов». Фактически, вызовы инструментов, которые они видели, вероятно, ограничены искусственным набором обучающих данных, созданным самими разработчиками LLM для обучения модели. В то время как они видели реальный код из миллионов проектов с открытым исходным кодом.

Заставлять LLM выполнять задачи с помощью вызова инструментов — это как отправить Шекспира на месячные курсы мандаринского языка, а затем попросить его написать пьесу на нём. Это просто не будет его лучшей работой.

Но MCP всё ещё полезен, потому что он унифицирован

MCP предназначен для вызова инструментов, но на самом деле его не обязательно использовать именно так.

«Инструменты», которые предоставляет MCP-сервер, на самом деле являются просто RPC-интерфейсом с прикреплённой документацией. Нам не обязательно представлять их как инструменты. Мы можем взять инструменты и превратить их в API языка программирования.

Но зачем это делать, если API языка программирования уже существуют независимо? Почти каждый MCP-сервер — это просто обёртка над существующим традиционным API — почему бы не предоставить эти API напрямую?

Оказывается, MCP делает кое-что ещё, действительно полезное: он предоставляет унифицированный способ подключения к API и изучения его документации.

AI-агент может использовать MCP-сервер, даже если разработчики агента никогда не слышали об этом конкретном MCP-сервере, а разработчики MCP-сервера никогда не слышали об этом конкретном агенте. Это редко бывало верно для традиционных API в прошлом. Обычно разработчик клиента всегда точно знает, для какого API он пишет код. В результате каждый API реализует базовое подключение, авторизацию и документацию немного по-своему.

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

Как это работает?

Мы уже расширили Cloudflare Agents SDK для поддержки этой новой модели!

Например, допустим, у вас есть приложение на ai-sdk, которое выглядит так:

const stream = streamText({
  model: openai("gpt-5"),
  system: "You are a helpful assistant",
  messages: [
    { role: "user", content: "Write a function that adds two numbers" }
  ],
  tools: {
    // определения инструментов
  }
})

Вы можете обернуть инструменты и промпт с помощью хелпера codemode и использовать их в вашем приложении:

import { codemode } from "agents/codemode/ai";

const {system, tools} = codemode({
  system: "You are a helpful assistant",
  tools: {
    // определения инструментов
  },
  // ...конфигурация
})

const stream = streamText({
  model: openai("gpt-5"),
  system,
  tools,
  messages: [
    { role: "user", content: "Write a function that adds two numbers" }
  ]
})

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

Преобразование MCP в TypeScript

Когда вы подключаетесь к MCP-серверу в «режиме кода», Agents SDK получает схему MCP-сервера, а затем преобразует её в TypeScript API, дополненный комментариями документации на основе схемы.

Например, подключение к MCP-серверу по адресу https://gitmcp.io/cloudflare/agents сгенерирует следующее TypeScript-определение:

interface FetchAgentsDocumentationInput {
  [k: string]: unknown;
}
interface FetchAgentsDocumentationOutput {
  [key: string]: any;
}

interface SearchAgentsDocumentationInput {
  /**
   * The search query to find relevant documentation
   */
  query: string;
}
interface SearchAgentsDocumentationOutput {
  [key: string]: any;
}

interface SearchAgentsCodeInput {
  /**
   * The search query to find relevant code files
   */
  query: string;
  /**
   * Page number to retrieve (starting from 1). Each page contains 30
   * results.
   */
  page?: number;
}
interface SearchAgentsCodeOutput {
  [key: string]: any;
}

interface FetchGenericUrlContentInput {
  /**
   * The URL of the document or page to fetch
   */
  url: string;
}
interface FetchGenericUrlContentOutput {
  [key: string]: any;
}

declare const codemode: {
  /**
   * Fetch entire documentation file from GitHub repository:
   * cloudflare/agents. Useful for general questions. Always call
   * this tool first if asked about cloudflare/agents.
   */
  fetch_agents_documentation: (
    input: FetchAgentsDocumentationInput
  ) => Promise<FetchAgentsDocumentationOutput>;

  /**
   * Semantically search within the fetched documentation from
   * GitHub repository: cloudflare/agents. Useful for specific queries.
   */
  search_agents_documentation: (
    input: SearchAgentsDocumentationInput
  ) => Promise<SearchAgentsDocumentationOutput>;

  /**
   * Search for code within the GitHub repository: "cloudflare/agents"
   * using the GitHub Search API (exact match). Returns matching files
   * for you to query further if relevant.
   */
  search_agents_code: (
    input: SearchAgentsCodeInput
  ) => Promise<SearchAgentsCodeOutput>;

  /**
   * Generic tool to fetch content from any absolute URL, respecting
   * robots.txt rules. Use this to retrieve referenced urls (absolute
   * urls) that were mentioned in previously fetched documentation.
   */
  fetch_generic_url_content: (
    input: FetchGenericUrlContentInput
  ) => Promise<FetchGenericUrlContentOutput>;
};

Этот TypeScript-код затем загружается в контекст агента. В настоящее время загружается весь API целиком, но в будущем можно будет позволить агенту искать и просматривать API более динамично — подобно тому, как это делает ассистент кодирования на базе AI.

Выполнение кода в песочнице

Вместо того чтобы получать все инструменты всех подключённых MCP-серверов, наш агент получает только один инструмент, который просто выполняет некоторый TypeScript-код.

Затем код выполняется в безопасной песочнице. Песочница полностью изолирована от Интернета. Её единственный доступ к внешнему миру — через TypeScript API, представляющие подключённые MCP-серверы.

Эти API поддерживаются вызовами RPC, которые возвращаются к циклу агента. Там Agents SDK направляет вызов соответствующему MCP-серверу.

Изолированный код возвращает результаты очевидным способом: через вызов console.log(). Когда скрипт завершается, все выходные журналы передаются обратно агенту.

Динамическая загрузка Workers: никаких контейнеров

Этот новый подход требует доступа к безопасной песочнице, где может выполняться произвольный код. Но где её взять? Нужно ли запускать контейнеры? Это дорого?

Нет. Никаких контейнеров. У нас есть кое-что получше: изоляты.

Платформа Cloudflare Workers всегда была основана на изолятах V8, то есть изолированных JavaScript-рантаймах на базе движка V8 JavaScript.

Изоляты гораздо легче контейнеров. Изолят может запуститься за несколько миллисекунд, используя всего несколько мегабайт памяти.

Изоляты настолько быстрые, что мы можем просто создавать новый для каждого фрагмента кода, который выполняет агент. Нет необходимости в их повторном использовании. Нет необходимости в预热е (prewarming). Просто создаём по требованию, выполняем код и выбрасываем. Всё происходит так быстро, что накладные расходы пренебрежимо малы — почти как если бы вы вызывали eval() напрямую. Но с обеспечением безопасности.

Worker Loader API

До сих пор не было способа напрямую загрузить изолят с произвольным кодом из Worker-а. Весь код Worker-ов должен был загружаться через Cloudflare API, который затем разворачивал его глобально, чтобы он мог работать где угодно. Для агентов это не то, что нужно! Мы хотим, чтобы код выполнялся прямо там, где находится агент.

Для этого мы добавили новый API на платформу Workers: Worker Loader API. С его помощью вы можете загружать код Worker-а по требованию. Вот как это выглядит:

// Получает Worker с заданным ID, создавая его, если он ещё не существует.
let worker = env.LOADER.get(id, async () => {
  // Если Worker ещё не существует, вызывается этот колбэк для получения
  // его кода.
  return {
    compatibilityDate: "2025-06-01",

    // Указываем код Worker-а (файлы модулей).
    mainModule: "foo.js",
    modules: {
      "foo.js": "export default {n" +
        "  fetch(req, env, ctx) { return new Response('Hello'); }n" +
        "}n",
    },

    // Указываем окружение динамического Worker-а (`env`).
    env: {
      // Может содержать базовые сериализуемые типы данных...
      SOME_NUMBER: 123,
      // ... и привязки к экспортируемым RPC-интерфейсам
      // родительского Worker-а, используя новый API loopback-привязок
      // `ctx.exports`.
      SOME_RPC_BINDING: ctx.exports.MyBindingImpl({props})
    },

    // Перенаправляем `fetch()` и `connect()` Worker-а через прокси
    // родительского Worker-а для мониторинга или фильтрации
    // всего интернет-трафика. Можно также полностью заблокировать
    // доступ к Интернету, передав `null`.
    globalOutbound: ctx.exports.OutboundProxy({props}),
  };
});

// Теперь можно получить точку входа Worker-а и отправлять ему запросы.
let defaultEntrypoint = worker.getEntrypoint();
await defaultEntrypoint.fetch("http://example.com");

// Можно также получать нестандартные точки входа и указывать
// значение `ctx.props`, которое будет доставлено в точку входа.
let someEntrypoint = worker.getEntrypoint(
  "SomeEntrypointClass",
  { props: {someProp: 123} }
);

Вы можете начать экспериментировать с этим API прямо сейчас, запуская workerd локально через Wrangler (ознакомьтесь с документацией), а также можете записаться на бета-доступ для использования в продакшене.

Workers — лучшие песочницы

Архитектура Workers делает их необычайно подходящими для песочниц, особенно в данном случае, по нескольким причинам:

Быстрые, дешёвые, одноразовые песочницы

Платформа Workers использует изоляты вместо контейнеров. Изоляты гораздо легче и быстрее запускаются. Создание свежего изолята занимает считанные миллисекунды, и это настолько дёшево, что мы можем создавать новый для каждого отдельного фрагмента кода, генерируемого агентом. Нет необходимости в пулизолятов для повторного использования,预热е и т.д.

Мы ещё не финализировали ценообразование для Worker Loader API, но поскольку он основан на изолятах, мы сможем предложить его по значительно более низкой цене, чем контейнерные решения.

Изолированы по умолчанию, но подключены через привязки

Workers просто лучше справляются с изоляцией.

В Code Mode мы запрещаем изолированному Worker-у обращаться к Интернету. Глобальные функции fetch() и connect() выбрасывают ошибки.

Но на большинстве платформ это было бы проблемой. На большинстве платформ способ получить доступ к приватным ресурсам — это начать с общего сетевого доступа. Затем, используя этот сетевой доступ, вы отправляете запросы к определённым сервисам, передавая им некий API-ключ для авторизации приватного доступа.

Но у Workers всегда был лучший ответ. В Workers «окружение» (объект env) содержит не только строки, он содержит живые объекты, также известные как «привязки» (bindings). Эти объекты могут предоставлять прямой доступ к приватным ресурсам без использования общих сетевых запросов.

В Code Mode мы предоставляем песочнице доступ к привязкам, представляющим подключённые MCP-серверы. Таким образом, агент может обращаться именно к этим MCP-серверам без общего сетевого доступа.

Ограничение доступа через привязки намного чище, чем через, скажем, сетевую фильтрацию или HTTP-прокси. Фильтрация сложна как для LLM, так и для супервизора, поскольку границы часто размыты: супервизору может быть трудно точно определить, какой трафик закономерно необходим для обращения к API. Тем временем LLM может испытывать сложности с угадыванием, какие запросы будут заблокированы. С подходом на основе привязок всё чётко определено: привязка предоставляет JavaScript-интерфейс, и этот интерфейс разрешено использовать. Это просто лучше.

Нет API-ключей, которые можно утечь

Дополнительное преимущество привязок — они скрывают API-ключи. Сама привязка предоставляет уже авторизованный клиентский интерфейс к MCP-серверу. Все вызовы через неё сначала поступают к супервизору агента, который хранит токены доступа и добавляет их в запросы, отправляемые к MCP.

Это означает, что AI физически не может написать код, который утечёт какие-либо ключи, решая распространённую проблему безопасности, наблюдаемую в коде, написанном AI сегодня.

Попробуйте прямо сейчас!

Запишитесь на продакшен-бету

Dynamic Worker Loader API находится в закрытой бете. Чтобы использовать его в продакшене, запишитесь сегодня.

Или попробуйте локально

Если вы просто хотите поэкспериментировать, Dynamic Worker Loading полностью доступен уже сегодня при локальной разработке с Wrangler и workerd — ознакомьтесь с документацией по Dynamic Worker Loading и Code Mode в Agents SDK, чтобы начать работу.


Оригинал: Code Mode: the better way to use MCP (Cloudflare Blog, Кентон Варда и Сунил Пай)

Комментарии
Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
Предыдущая запись
Следующая запись

Давайте дружить
в Telegram

Авторский блог вашего покорного слуги в Telegram про web, программирование, алгоритмы, инструменты разработчика, WordPress, Joomla, Opencart, Symfony, Laravel, Moonshine, фильмы и сериалы