Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

McpAgent

最終更新 Markdown で表示Agent セットアップ

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) }],
			}),
		);
	}
}
src/index.tsts
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 を使えます。

API の概要

プロパティ / メソッド 説明
state 現在の状態オブジェクト(永続化済み)
initialState インスタンス開始時のデフォルト状態
setState(state) 状態を更新して永続化する
onStateChanged(state) 状態が変わったときに呼ばれる
sql 埋め込みデータベースで SQL クエリを実行する
server ツール登録用の McpServer インスタンス
props OAuth 認証からのユーザー ID とトークン
elicitInput(options, context) ユーザーから構造化入力を求める
McpAgent.serve(path, options) Worker ハンドラーを作成する静的メソッド

McpAgent.serve() でのデプロイ

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 認証あり

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 ストリームのイベントは、クローズフレーム書き込み時にクリアされます。

DurableObjectEventStoreagents/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 パラメータで利用できます。これにより次ができます。

  • ユーザー固有データへのアクセス
  • 操作前の権限確認
  • ユーザー属性に応じた応答のカスタマイズ
  • 認証トークンを使った、ユーザー代理での外部サービスへのリクエスト

状態同期 API

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 });
	}
}
src/index.tsts
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 は、ツール呼び出しなど別リクエストの処理中に、サーバーがユーザー入力を求められます。レガシーパスには 2 つのモードがあります。

  • Form モード は、クライアント経由で構造化された非機密データを集めます。
  • URL モード は、サードパーティ認可や支払いなど、帯域外の操作へユーザーを送ります。

サーバーが送る前に、クライアントがそのモードのサポートを告知している必要があります。

Form モード

ツールハンドラー内で this.server.server.elicitInput() を呼び出します。応答が元のツール呼び出しのストリームに戻るよう、extra.requestIdrelatedRequestId として渡します。

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 モードを使わないでください。

URL モード

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 を開くことに同意したことを意味します。外部操作が完了したことではありません。サーバーはあとで、同じ elicitationIdnotifications/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 パターン を参照してください。

次のステップ

MCP ツール

MCP サーバーにツールを設計して追加します。

認可

OAuth 認証を設定します。

役に立ちましたか?