McpAgent は Durable Object をバックエンドにした、状態を持つレガシー 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: "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 データベース を持ちます。
ステートレスサーバーは @modelcontextprotocol/server で ツール を定義し、createMcpHandler 経由で提供できます。
ただし、MCP サーバーで次をしたい場合は:
- 以前のツール呼び出しと、返した応答を覚えておく
- MCP クライアントにゲームを提供し、盤面、以前の手、スコアを覚える
- 以前の外部 API 呼び出しの状態をキャッシュし、後続のツール呼び出しで再利用する
- Agent ができることを何でも行い、MCP クライアントから通信できるようにする
次の API を使えます。
| プロパティ / メソッド | 説明 |
|---|---|
state |
現在の状態オブジェクト(永続化済み) |
initialState |
インスタンス開始時のデフォルト状態 |
setState(state) |
状態を更新して永続化する |
onStateChanged(state) |
状態が変わったときに呼ばれる |
sql |
埋め込みデータベースで SQL クエリを実行する |
server |
ツール登録用の McpServer インスタンス |
props |
OAuth 認証からのユーザー ID とトークン |
elicitInput(options, context) |
ユーザーから構造化入力を求める |
McpAgent.serve(path, options) |
Worker ハンドラーを作成する静的メソッド |
McpAgent.serve() 静的メソッドは、リクエストを MCP サーバーへルーティングする Worker ハンドラーを作成します。
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 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 セッションデータは EU 内に留まります
- ツールが処理するユーザーデータは EU 内に留まります
- Durable Object に保存した状態は EU 内に留まります
利用できる管轄は "eu"(欧州連合)と "fedramp"(FedRAMP 準拠の場所)です。その他のオプションは Durable Objects のデータ所在地 を参照してください。
McpAgent インスタンスは WebSockets Hibernation に自動対応します。状態を持つ MCP サーバーは、非アクティブ時にスリープしながら状態を保持できます。つまり、リクエストを実際に処理しているときだけコンピュートを消費します。コストを抑えつつ、コンテキストと会話履歴は維持します。
ハイバネーションはデフォルトで有効で、追加設定は不要です。
McpAgent の Streamable HTTP トランスポートは、Cloudflare エッジの約 5 分のアイドルストリーム watchdog を越えて生存します。不安定な接続でも、進行中のツール呼び出しを失いません。
- GET(スタンドアロンの listen ストリーム) —
EventStoreが設定されている場合、アイドル切断はクライアントがLast-Event-IDヘッダーで再接続して復旧します(keepalive は不要)。EventStoreがない場合は、コメントフレームの keepalive(: keepalive、25 秒ごと)が長寿命リスナーを維持します。 - POST(ツール応答ストリーム) — 常に keepalive するため、進行中のツール呼び出しはアイドル watchdog を越えます。
EventStoreがある場合、POST ストリームはLast-Event-IDでも再開できます。再接続したクライアントは、最終応答まで見逃したイベントをリプレイします。各 POST ストリームのイベントは、クローズフレーム書き込み時にクリアされます。
DurableObjectEventStore は agents/mcp からエクスポートされます。Agent や Durable Object 内にトランスポートを埋め込む、状態を持つ WorkerTransport 呼び出し元向けです。
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 Transport を参照してください。
McpAgent クラスは、認証と認可 向けの OAuth Provider Library ↗ とシームレスに統合します。
ユーザーが MCP サーバーに認証すると、ID 情報とトークンが props パラメータで利用できます。これにより次ができます。
- ユーザー固有データへのアクセス
- 操作前の権限確認
- ユーザー属性に応じた応答のカスタマイズ
- 認証トークンを使った、ユーザー代理での外部サービスへのリクエスト
McpAgent クラスは Agent の状態 API にフルアクセスできます。
state— 現在の永続化済み状態initialState— インスタンス開始時のデフォルト状態setState— 状態を更新して永続化するonStateChanged— 状態変更に反応するsql— 埋め込みデータベースで SQL クエリを実行する
たとえば、次のコードはカウンター値を覚え、add ツールが呼ばれたときにカウンターを更新する 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: "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 });
}
}MCP elicitation ↗ は、ツール呼び出しなど別リクエストの処理中に、サーバーがユーザー入力を求められます。レガシーパスには 2 つのモードがあります。
- Form モード は、クライアント経由で構造化された非機密データを集めます。
- 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 キー、アクセストークン、支払い認証情報、その他のシークレットの要求に form モードを使わないでください。
MCP クライアントの外で行う必要がある操作には URL モードを使います。リクエストにはメッセージ、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 を開くことに同意したことを意味します。外部操作が完了したことではありません。サーバーはあとで、同じ elicitationId で notifications/elicitation/complete を送ることがあります。
url にシークレット、個人情報、事前認証済みの保護リソース URL を入れないでください。本番サーバーは HTTPS を使うべきです。各リクエストを認証済みユーザーに紐づけ、同じユーザーが外部フローを完了したことを検証します。
両モードは次の 3 つのアクションのいずれかを返します。
| アクション | 意味 |
|---|---|
accept |
ユーザーがフォームを送信したか、URL を開くことに同意した |
decline |
ユーザーが明示的にリクエストを拒否した |
cancel |
ユーザーが明示的な選択をせずにリクエストを閉じた |
受け入れた form 応答には、requestedSchema に一致する content が含まれます。URL 応答には content がありません。decline と cancel の応答では、通常省略されます。
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." }] };
}より多くの human-in-the-loop パターンは、Human-in-the-loop パターン を参照してください。