← Создание агентов на Cloudflare / agents / model-context-protocol / apis
McpAgent
McpAgent создаёт устаревший сервер MCP с сохранением состояния на основе Durable Object.
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() {
this.server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
);
}
}import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() {
this.server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
);
}
}Это означает, что каждый экземпляр вашего MCP-сервера имеет собственное устойчивое состояние, подкреплённое Durable Object, со своим собственным База данных SQL.
Сервер без сохранения состояния может определить tools с @modelcontextprotocol/server и предоставляет их через createMcpHandler.
Но если вы хотите, чтобы ваш MCP-сервер:
- запоминать предыдущие вызовы инструментов и предоставленные им ответы
- предоставлять MCP-клиенту игру, запоминая при этом состояние игрового поля, предыдущие ходы и счёт
- кэшировать состояние предыдущего вызова внешнего API, чтобы последующие вызовы инструментов могли повторно его использовать
- делать всё то же, что и Agent, но при этом позволять клиентам MCP взаимодействовать с ним
Для этого вы можете использовать приведённые ниже API.
Обзор API
| Свойство/Метод | Описание |
|---|---|
state |
Объект текущего состояния (сохраняется) |
initialState |
Состояние по умолчанию при запуске экземпляра |
setState(state) |
Обновляйте и сохраняйте состояние |
onStateChanged(state) |
Вызывается при изменении состояния |
sql |
Выполнять SQL-запросы к встроенной базе данных |
server |
McpServer экземпляр для регистрации инструментов |
props |
Идентификационные данные пользователя и токены OAuth-аутентификации |
elicitInput(options, context) |
Запросить структурированный ввод у пользователя |
McpAgent.serve(path, options) |
Статический метод для создания обработчика Worker |
Развёртывание с помощью McpAgent.serve()
McpAgent.serve() статический метод создаёт обработчик Worker, который перенаправляет запросы на ваш MCP-сервер:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "my-server", version: "1.0.0" });
async init() {
this.server.tool("square", { n: z.number() }, async ({ n }) => ({
content: [{ type: "text", text: String(n * n) }],
}));
}
}
// Export the Worker handler
export default MyMCP.serve("/mcp");import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "my-server", version: "1.0.0" });
async init() {
this.server.tool("square", { n: z.number() }, async ({ n }) => ({
content: [{ type: "text", text: String(n * n) }],
}));
}
}
// Export the Worker handler
export default MyMCP.serve("/mcp");Это самый простой способ развернуть MCP-сервер: около 15 строк кода. Этот serve() метод автоматически обрабатывает транспорт Streamable HTTP.
С аутентификацией через OAuth
При использовании OAuth Provider Library ↗, передайте свой сервер MCP в apiHandlers:
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
export default new OAuthProvider({
apiHandlers: { "/mcp": MyMCP.serve("/mcp") },
authorizeEndpoint: "/authorize",
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
defaultHandler: AuthHandler,
});import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
export default new OAuthProvider({
apiHandlers: { "/mcp": MyMCP.serve("/mcp") },
authorizeEndpoint: "/authorize",
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
defaultHandler: AuthHandler,
});Юрисдикция данных
Для соответствия GDPR и требованиям к размещению данных укажите юрисдикцию, чтобы инстансы вашего MCP-сервера работали в определённых регионах:
// EU jurisdiction for GDPR compliance
export default MyMCP.serve("/mcp", { jurisdiction: "eu" });// EU jurisdiction for GDPR compliance
export default MyMCP.serve("/mcp", { jurisdiction: "eu" });С OAuth:
export default new OAuthProvider({
apiHandlers: {
"/mcp": MyMCP.serve("/mcp", { jurisdiction: "eu" }),
},
// ... other OAuth config
});export default new OAuthProvider({
apiHandlers: {
"/mcp": MyMCP.serve("/mcp", { jurisdiction: "eu" }),
},
// ... other OAuth config
});Если указать jurisdiction: "eu":
- Все данные сессий MCP остаются в пределах ЕС
- Данные пользователя, обрабатываемые вашими инструментами, остаются в ЕС
- Состояние, хранящееся в Durable Object, остаётся в пределах ЕС
Доступные юрисдикции включают "eu" (Европейский союз) и "fedramp" (регионы, соответствующие FedRAMP). См. Расположение данных Durable Objects для дополнительных опций.
Поддержка гибернации
McpAgent экземпляры автоматически поддерживают WebSockets Hibernation, что позволяет MCP серверам с сохранением состояния «засыпать» в периоды бездействия, не теряя при этом своё состояние. Это значит, что ваши агенты расходуют вычислительные ресурсы только тогда, когда активно обрабатывают запросы, что снижает затраты и одновременно сохраняет полный контекст и историю разговора.
Гибернация включена по умолчанию и не требует дополнительной настройки.
Возобновляемость потока
McpAgent: транспорт Streamable HTTP переживает примерно пятиминутный watchdog простаивающего потока на границе сети Cloudflare, поэтому выполняющиеся вызовы инструментов не теряются при нестабильном соединении:
- GET (отдельный поток прослушивания) : когда
EventStoreнастроен, обрывы соединения в состоянии простоя восстанавливаются клиентами, переподключающимися сLast-Event-IDзаголовок (keepalive не требуется). БезEventStore, keepalive в виде кадра-комментария (: keepalive, каждые 25 секунд) поддерживает долгоживущие слушатели активными. - POST (поток ответа инструмента) : всегда keepalive, поэтому выполняющиеся вызовы инструментов переживают сторожевой таймер простоя. Потоки POST можно дополнительно возобновить через
Last-Event-IDкогдаEventStoreнастроен: переподключающийся клиент воспроизводит все пропущенные события вплоть до финального ответа включительно. События каждого потока POST очищаются при записи его close-фрейма.
DurableObjectEventStore экспортируется из agents/mcp для сохраняющего состояние WorkerTransport вызывающего кода, который встраивает транспорт внутрь Agent или Durable Object:
import { DurableObjectEventStore } from "agents/mcp";
const eventStore = new DurableObjectEventStore(this.ctx.storage);import { DurableObjectEventStore } from "agents/mcp";
const eventStore = new DurableObjectEventStore(this.ctx.storage);См. MCP-транспорт для настройки транспорта.
Аутентификация и авторизация
Класс McpAgent обеспечивает бесшовную интеграцию с OAuth Provider Library ↗ для аутентификация и авторизация.
Когда пользователь проходит аутентификацию на вашем MCP-сервере, информация о его личности и токены становятся доступны через props параметр, позволяющий:
- доступ к данным конкретного пользователя
- проверять права пользователя перед выполнением операций
- настраивать ответы на основе атрибутов пользователя
- использовать токены аутентификации для отправки запросов к внешним сервисам от имени пользователя
API синхронизации состояния
McpAgent класс предоставляет полный доступ к API состояния Agent:
state: Текущее сохраненное состояниеinitialState: Состояние по умолчанию при запуске экземпляраsetState: обновление и сохранение состоянияonStateChanged: реагирование на изменения состоянияsql: выполнение SQL-запросов к встроенной базе данных
Например, следующий код реализует сервер MCP, который запоминает значение счётчика и обновляет его, когда add инструмент вызывается:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({
name: "Demo",
version: "1.0.0",
});
initialState = {
counter: 1,
};
async init() {
this.server.resource(`counter`, `mcp://resource/counter`, (uri) => {
return {
contents: [{ uri: uri.href, text: String(this.state.counter) }],
};
});
this.server.tool(
"add",
"Add to the counter, stored in the MCP",
{ a: z.number() },
async ({ a }) => {
this.setState({ ...this.state, counter: this.state.counter + a });
return {
content: [
{
type: "text",
text: String(`Added ${a}, total is now ${this.state.counter}`),
},
],
};
},
);
}
onStateChanged(state) {
console.log({ stateUpdate: state });
}
}import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
type State = { counter: number };
export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "Demo",
version: "1.0.0",
});
initialState: State = {
counter: 1,
};
async init() {
this.server.resource(`counter`, `mcp://resource/counter`, (uri) => {
return {
contents: [{ uri: uri.href, text: String(this.state.counter) }],
};
});
this.server.tool(
"add",
"Add to the counter, stored in the MCP",
{ a: z.number() },
async ({ a }) => {
this.setState({ ...this.state, counter: this.state.counter + a });
return {
content: [
{
type: "text",
text: String(`Added ${a}, total is now ${this.state.counter}`),
},
],
};
},
);
}
onStateChanged(state: State) {
console.log({ stateUpdate: state });
}
}Elicitation на устаревших серверах
MCP elicitation ↗ позволяет серверу запрашивать ввод у пользователя во время обработки другого запроса, например вызова инструмента. Устаревший путь определяет два режима:
- Режим формы собирает структурированные, не содержащие конфиденциальных данных сведения через клиента.
- Режим URL направляет пользователя к внеполосному взаимодействию, например к авторизации через третью сторону или к оплате.
Клиент должен объявить поддержку режима, прежде чем сервер начнёт его отправлять.
Режим формы
Вызовите this.server.server.elicitInput() в обработчике инструмента. Передайте extra.requestId как relatedRequestId чтобы ответ возвращался в потоке для исходного вызова инструмента:
const result = await this.server.server.elicitInput(
{
mode: "form",
message: "By how much do you want to increase the counter?",
requestedSchema: {
type: "object",
properties: {
amount: {
type: "number",
title: "Amount",
minimum: 1,
maximum: 100,
},
},
required: ["amount"],
},
},
{ relatedRequestId: extra.requestId },
);
if (result.action !== "accept" || !result.content) {
return { content: [{ type: "text", text: "Counter unchanged." }] };
}
const amount = Number(result.content.amount);const result = await this.server.server.elicitInput(
{
mode: "form",
message: "By how much do you want to increase the counter?",
requestedSchema: {
type: "object",
properties: {
amount: {
type: "number",
title: "Amount",
minimum: 1,
maximum: 100,
},
},
required: ["amount"],
},
},
{ relatedRequestId: extra.requestId },
);
if (result.action !== "accept" || !result.content) {
return { content: [{ type: "text", text: "Counter unchanged." }] };
}
const amount = Number(result.content.amount);Для обратной совместимости в form-запросах можно опустить mode: "form". Схема поддерживает плоские объекты с примитивными полями. Не используйте режим формы для запроса паролей, API-ключей, токенов доступа, платёжных данных и других секретов.
Режим URL
Используйте режим URL для взаимодействий, которые должны происходить вне MCP-клиента. Запрос включает сообщение, URL и уникальный elicitationId:
const elicitationId = crypto.randomUUID();
const result = await this.server.server.elicitInput(
{
mode: "url",
message: "Connect your account to continue.",
url: `https://example.com/connect?elicitationId=${elicitationId}`,
elicitationId,
},
{ relatedRequestId: extra.requestId },
);
if (result.action !== "accept") {
return { content: [{ type: "text", text: "Connection cancelled." }] };
}
return {
content: [
{
type: "text",
text: "Connection page opened. Complete it in your browser.",
},
],
};const elicitationId = crypto.randomUUID();
const result = await this.server.server.elicitInput(
{
mode: "url",
message: "Connect your account to continue.",
url: `https://example.com/connect?elicitationId=${elicitationId}`,
elicitationId,
},
{ relatedRequestId: extra.requestId },
);
if (result.action !== "accept") {
return { content: [{ type: "text", text: "Connection cancelled." }] };
}
return {
content: [
{
type: "text",
text: "Connection page opened. Complete it in your browser.",
},
],
};Для режима URL accept означает, что пользователь согласился открыть URL. Это не значит, что внешнее взаимодействие завершилось. Сервер может позже отправить notifications/elicitation/complete с тем же elicitationId.
Не помещайте секреты, персональные данные или предварительно авторизованный URL защищённого ресурса в url. Рабочие серверы должны использовать HTTPS. Привязывайте каждый запрос к аутентифицированному пользователю и проверяйте, что тот же пользователь завершает внешний процесс.
Обрабатывайте ответы
Оба режима возвращают одно из трёх действий:
| Действие | Значение |
|---|---|
accept |
Пользователь отправил форму или согласился открыть URL. |
decline |
Пользователь явно отклонил запрос. |
cancel |
Пользователь закрыл запрос, не сделав явного выбора. |
Принятые ответы формы включают content который соответствует requestedSchema. Ответы URL не включают content. Ответы об отклонении и отмене обычно его не включают.
switch (result.action) {
case "accept":
// For form mode, validate and process result.content.
break;
case "decline":
return { content: [{ type: "text", text: "Request declined." }] };
case "cancel":
return { content: [{ type: "text", text: "Request dismissed." }] };
}switch (result.action) {
case "accept":
// For form mode, validate and process result.content.
break;
case "decline":
return { content: [{ type: "text", text: "Request declined." }] };
case "cancel":
return { content: [{ type: "text", text: "Request dismissed." }] };
}Другие паттерны с участием человека см. в Паттерны с участием человека.