跳到內容

Agents SDK 與 MCP server on Workers

查證日期
驗證環境agents@0.20.1·@modelcontextprotocol/server@2.0.0·@modelcontextprotocol/sdk@1.30.0·@cloudflare/ai-chat@0.10.1·@cloudflare/workers-oauth-provider@0.8.3·wrangler@4.118.0·compatibility_date: 2026-07-24

前四章分別給了推論(34)、統一入口(35)、檢索(36),這一章把它們組成能自己行動的東西 —— 以及讓別人的 AI 助理能操作你的產品。

⚠️ 不要說 Agents SDK 是 GA。 版本是 0.20.1,文件裡沒有任何 GA / beta 宣告。0.x 的 minor 版就會破壞相容 —— 本章實測到的一個改名就是 0.4.0 發生的。釘死版本號。


DurableObject → Server → Agent

class Agent<Env, State, Props> extends Server<Env, Props>,而 Server 繼承 DurableObject

這代表第 14 到 17 章的所有知識直接適用:input gate 的序列化保證、per-instance 的 SQLite(第 15 章)、alarm(第 17 章)、WebSocket hibernation(第 16 章)。Agent 只是在上面加了一層狀態同步與工具呼叫的慣例。

設定上因此也繼承了 DO 的要求:

{
"compatibility_flags": ["nodejs_compat"],
"durable_objects": { "bindings": [{ "name": "AGENT", "class_name": "MyAgent" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }]
}

new_sqlite_classes 不是可選的 —— agent 的狀態存在 DO 的 SQLite 裡。用 new_classes 會在執行期壞掉(第 29 章實測過,--dry-run 不會擋你)。

還有一個和第 7 章有關的坑:

{ "assets": { "directory": "./public", "run_worker_first": ["/agents/*"] } }

如果你的 Worker 同時服務靜態資源,沒有 run_worker_first 的話 SPA 的 fallback 會吞掉 agent 路由。 第 7 章實測過 not_found_handling 的行為,這裡是它的具體後果。

export class MyAgent extends Agent<Env, MyState> {
initialState: MyState = { turns: 0 };
async onRequest(req: Request) {
this.setState({ ...this.state, turns: this.state.turns + 1 });
// this.sql 是同步的(第 15 章的 DO SQLite)
const rows = this.sql`select * from notes where id = ${id}`;
return Response.json({ state: this.state, rows });
}
}

this.sql同步的 —— 和第 15 章的 ctx.storage.sql.exec() 一樣,不需要 await


37.2 頭號改名陷阱:onStateChanged vs onStateUpdate

Section titled “37.2 頭號改名陷阱:onStateChanged vs onStateUpdate”

這是 0.4.0 的改名,而且兩個名字現在都活著,只是在不同的地方

位置名稱
server 端(Agent 子類的 hook)onStateChanged
client 端useAgent 的選項)仍然是 onStateUpdate

實測 agents@0.20.1 的實作,建構時有一段防護:

const proto = Object.getPrototypeOf(this);
const hasOwnNew = Object.prototype.hasOwnProperty.call(proto, "onStateChanged");
const hasOwnOld = Object.prototype.hasOwnProperty.call(proto, "onStateUpdate");
if (hasOwnNew && hasOwnOld)
throw new Error("[Agent] Cannot override both onStateChanged and onStateUpdate. " +
"Remove onStateUpdate — it has been renamed to onStateChanged.");
if (hasOwnOld) {
// 每個 class 只警告一次
console.warn("[Agent] onStateUpdate is deprecated. Rename to onStateChanged — " +
"the behavior is identical.");
}

讀起來很清楚:兩個都覆寫會 throw,只覆寫舊的會拿到一次 deprecation 警告(用 WeakSet 去重,每個 class 只印一次)。

於是我照這個寫了一個兩個都覆寫的 BothHooksAgent 去撞它。

它沒有 throw。也沒有警告。請求 200 回來了。


🔴 為什麼沒 throw:Miniflare 會偷偷包一層 DO subclass

Section titled “🔴 為什麼沒 throw:Miniflare 會偷偷包一層 DO subclass”

我讓那個 agent 在 onRequest 裡把自己的 prototype 印出來:

// 從 Durable Object 內部看
{
"ctorName": "BothHooksAgent",
"protoOwn": ["constructor", "__miniflare_getDOName", "__miniflare_introspectSqlite"],
"hasOwnNew": false,
"hasOwnOld": false
}
// 同一個 class,從 Worker 看
{
"ownProps": ["constructor", "onStateChanged", "onStateUpdate", "onRequest"],
"hasOwnNew": true,
"hasOwnOld": true
}

原因在 node_modules/miniflare/dist/src/workers/core/do-wrapper.worker.js

function createDurableObjectWrapper(UserClass) {
class Wrapper extends UserClass {
constructor(ctx, env) { /* 把 ctx.id.name 寫進 __miniflare_do_name 表 */ }
[GET_DO_NAME_METHOD]() { /* ... */ }
[INTROSPECT_SQLITE_METHOD](queries) { /* ... */ }
}
return Object.defineProperty(Wrapper, "name", { value: UserClass.name }), Wrapper;
}

wrangler dev 會把你的每一個 Durable Object class 再包一層 subclass,純粹是為了讓 Local Explorer 能拿到 instance 名稱、能讀它的 SQLite。後果是:在本機的 DO 裡,

  • Object.getPrototypeOf(this)Wrapper.prototype,不是你的 class 的 prototype。對它做 own-property 檢查,只會看到 constructor__miniflare_getDOName__miniflare_introspectSqlite
  • 繼承查找(inherited lookup)完全正常 —— 所以 hook 本身還是會觸發(/counter 確實印出 onStateChanged)。SDK 判斷走哪條路徑用的是 proto.onStateChanged !== Agent.prototype.onStateChanged,那是繼承查找,活下來了。
  • this.constructor.name 仍然正確(Miniflare 用 defineProperty 把名字複製到 Wrapper 上),但 this.constructor === MyClassfalse

這一條的方向和本系列前面所有「本機測不出來」都相反。 平常是本機比較寬鬆、production 比較嚴格。這裡是函式庫自己寫的 runtime assertion 在本機被關掉了 —— 所以錯誤設定會順利通過 wrangler dev,然後在 deploy 之後才 throw。

一般化:在 Durable Object 裡,對 Object.getPrototypeOf(this)hasOwnProperty 是不可攜的。 任何靠這個做註冊的 decorator、DI container、框架約定,在本機都是靜默失效的。

(「production 會 throw」是推論:那層 wrapper 只存在於 Miniflare。我沒有真的部署去驗證。)

而 TypeScript 也擋不住你 —— onStateUpdate 還在 Agent 的型別表面上,兩個都寫編譯完全通過

實作裡還有一個內部欄位 _persistenceHookMode"new" / "old"),就是上面那個靠繼承查找比對的結果。


37.3 Chat 已經搬套件,而且搬得很兇

Section titled “37.3 Chat 已經搬套件,而且搬得很兇”

實測 agents@0.20.1dist/ai-chat-agent.js整個模組的內容就是一個 throw

//#region src/ai-chat-agent.ts
throw new Error("All the AI Chat related modules are now in @cloudflare/ai-chat. " +
"This module is deprecated and will be removed in the next major version. " +
"Please use @cloudflare/ai-chat instead.");
//#endregion
export {};

dist/ai-react.js 也一樣。

注意這是「載入即 throw」,不是「呼叫才 throw」。 只要 import { AIChatAgent } from "agents/ai-chat-agent" 出現在你的 bundle 裡,整個 Worker 就起不來。這是我看過最直接的棄用手段 —— 好處是不可能被忽略。

正確的 import:

import { AIChatAgent } from "@cloudflare/ai-chat";
import { useAgentChat } from "@cloudflare/ai-chat/react";

實測 @cloudflare/ai-chat@0.10.1 的 exports 是 ../react./types./ai-chat-v5-migration

⚠️ 而且那個錯誤訊息本身是錯的。 agents/ai-react 的 throw 訊息說「Please use @cloudflare/ai-chat/ai-react instead」,但實測:

@cloudflare/ai-chat/ai-react → ERR_PACKAGE_PATH_NOT_EXPORTED
@cloudflare/ai-chat/react → 可解析

正確的是 /react,不是 /ai-react 照著錯誤訊息做會撞到第二個錯誤。


agents@0.20.1 提供三條路,選錯會讓你多寫很多不必要的東西。

方式需要 DO?適合
createMcpHandler()新的預設。 無狀態的工具集
McpAgent需要 per-session 狀態、長時間對話
raw transport需要完全控制傳輸層

實測 export 的真身:

agents/mcp
createStatelessMcpHandler as createMcpHandler

名字裡的 Stateless 說明了一切。但 createMcpHandler 這個名字底下其實有兩個 overload

/** @deprecated Passing an SDK v1 server to createMcpHandler is deprecated and
* will be removed in the next major version. Pass an SDK v2 factory ... */
declare function createMcpHandler(
server: McpServer_v1 | Server_v1,
options?: CreateMcpHandlerOptions,
): LegacyMcpHandler;
declare function createMcpHandler(
factory: McpServerFactory,
options?: CreateStatelessMcpHandlerOptions,
): StatelessMcpHandler;

🔴 McpServer 必須 per-request 建構 —— 但型別不會幫你擋

Section titled “🔴 McpServer 必須 per-request 建構 —— 但型別不會幫你擋”

我原本以為「參數型別是 McpServerFactory 不是 McpServer」等於型別層級強制。實測不是。 傳一個 instance 進去照樣編譯通過 —— 它只是安靜地走 overload 1,回給你有 session 的 legacy handler,不是你以為的 stateless handler。

型別在這裡只是引導,不是護欄。要看的是那行 @deprecated,不是編輯器有沒有畫紅線。

// ❌ 編譯得過,但你拿到的是 LegacyMcpHandler,而且是模組層 singleton
const server = new McpServer({ name: "linkforge", version: "1.0.0" });
export default createMcpHandler(server);
// ✅ 每個請求一個新的 server,拿到 StatelessMcpHandler
export default createMcpHandler((ctx) => {
const server = new McpServer({ name: "linkforge", version: "1.0.0" });
server.registerTool("create_link", { description, inputSchema }, handler);
return server;
});

模組層的 singleton 在 MCP SDK 1.26+ 是安全問題,因為 server 實例會累積 per-connection 的狀態,而 Workers 的 isolate 在多個請求之間重用(第 1 章、第 3 章)—— A 使用者的 session 狀態會被 B 使用者看到。這是第 34 章那個「模組層變數跨請求共享」在 MCP 語境下的具體危害。

🔴 而且「SDK v2」是另一個 npm 套件

Section titled “🔴 而且「SDK v2」是另一個 npm 套件”

McpServerFactory 不是從 @modelcontextprotocol/sdk 來的:

import { McpServerFactory } from "@modelcontextprotocol/server";

實測 agents@0.20.1 的 peer dependencies:

套件版本是什麼
@modelcontextprotocol/sdk1.30.0SDK v1(舊的,legacy overload 用)
@modelcontextprotocol/server2.0.0SDK v2 server(factory overload 用)
@modelcontextprotocol/client2.0.0SDK v2 client

如果你用 v1 的 McpServer 去寫 factory,錯誤訊息長這樣:

Type 'McpServer' is not assignable to type 'McpServer | Server | Promise<...>'.
Type 'McpServer' is missing the following properties from type 'McpServer':
_toolInputSchemaJson, toolInputSchemaJson

McpServer 不能指派給 McpServer —— 這就是 v1/v2 撞名的樣子。看到這個訊息時要想到的是套件裝錯了,不是型別有 bug。

而且遷移不只是包一層箭頭函式:SDK v2 拿掉了 server.tool(name, schema, cb) 這個簡寫,只剩 registerTool(name, config, cb)

server.registerTool(
"retarget_link",
{
description: "Point an existing short link at a new URL.",
inputSchema: { slug: z.string(), url: z.url() },
// v2 的 config 原生支援 outputSchema / annotations / icons / _meta
},
async ({ slug, url }) => ({ content: [{ type: "text", text: `ok ${slug} ${url}` }] }),
);

還有一個撞名要小心:@modelcontextprotocol/server 自己也 export 一個 createMcpHandler,那不是 agents/mcp 的那一個。import 的來源要看清楚。

factory 的完整型別是 (ctx: McpRequestContext) => McpServer | Server | Promise<...>,每個請求呼叫一次。

CreateStatelessMcpHandlerOptions 裡的安全預設

Section titled “CreateStatelessMcpHandlerOptions 裡的安全預設”

實測型別,有兩個很值得知道的欄位:

interface CreateStatelessMcpHandlerOptions {
route?: string; // 預設 "/mcp"
corsOptions?: CORSOptions | false;
allowedHostnames?: string[];
allowedOriginHostnames?: string[] | "*";
authContext?: McpAuthContext;
}

型別註解寫得很清楚:

allowedHostnames:「Restrict Host headers to these hostnames. Localhost and workers.dev endpoints receive matching defaults; custom domains rely on Cloudflare routing unless this option is set.

allowedOriginHostnames:「Restrict present browser Origin headers to these hostnames. Requests without an Origin (including non-browser MCP clients) remain valid… Pass "*" only when equivalent Origin validation runs in trusted middleware upstream.

用自訂網域時要自己設 allowedHostnames —— 預設只保護 localhost 與 workers.dev。這是 DNS rebinding 防護,很容易漏。

這一條本機就量得出來。範例裡掛了兩個一模一樣的 server,一個有設 allowedHostnames/mcp),一個沒設(/mcp-open):

POST /mcp Host: evil.example.net → 403 {"error":{"code":-32000,"message":"Invalid Host: evil.example.net"}}
POST /mcp Origin: https://evil... → 403
POST /mcp-open Host: evil.example.net → 200 tools/list 正常回傳

沒設的那個,偽造 Host 直接放行。

getMcpAuthContext() 是在 tool handler 裡取得認證資訊的管道({ props: Record<string, unknown> })。沒有 OAuth 在前面時它回 undefined —— 是乾淨的 undefined,不是會 throw 的 stub(本系列前面遇過六次的那種)。

好消息:MCP server 是本系列少數「本機可以完整跑起來」的東西。 initializetools/listtools/call 全部走得通,Zod shape 也會被正確轉成 JSON Schema。不需要任何 Cloudflare 帳號。


@cloudflare/workers-oauth-provider — 實測最新版 0.8.3

務必升過 0.8.0。 0.8.0 修了 cross-client token revocation 與 authorization code exchange 的缺陷。低於這個版本的專案應該視為有已知漏洞。

一個必須手動設定的項目:

new OAuthProvider({
// ...
allowPlainPKCE: false, // 預設是 true
});

預設允許 plain PKCEcode_challenge_method=plain),那基本上等於沒有 PKCE。除非你要相容某個很舊的 client,一律設 false


agents@0.20.1 的 exports 有 33 個 subpath,其中幾個是 2026 才有的:

Subpath是什麼
agents/workflowsFibers —— durable execution,runFiber / ctx.stash()
agents/skillsagents/skills/compileAgent Skills
agents/codemode/aiCode Mode
agents/x402付款協定
agents/experimental/memory/session記憶體(experimental)
agents/experimental/webmcp瀏覽器端 MCP(experimental)
agents/browseragents/browser/ai接第 30 章的 Browser Run
agents/email接第 32 章的 Email
agents/observabilityagents/observability/ai可觀測性

注意有兩個掛在 experimental/ 底下 —— 在 0.x 的套件裡再標 experimental,等於「隨時會消失」。

waitForApproval() 由第 21 章的 Workflows 支撐,所以它可以等好幾個月 —— 第 21 章實測過 waitForEvent 的 timeout 上限是 365 天。

這是把第 21 章的 durable execution 用在對的地方:agent 決定要做一件需要人類批准的事 → 暫停 → 幾天後主管按下批准 → agent 從中斷處繼續,而且中間不佔用任何運算資源(第 21 章:waiting 狀態不計 concurrency、不計 CPU)。

第 33 章的 DO Facets 在這裡有了具體用途:只有 parent agent 需要 DO binding,sub-agent 是它的 facet,各自有隔離的 SQLite。

@cloudflare/codemode — 實測最新版 0.5.1(beta)

把 MCP tools 轉成 TypeScript API,讓 LLM 寫程式碼去呼叫,而不是一次一個 tool call。需要 worker_loaders binding(第 33 章)—— 因為 LLM 產生的程式碼要被載入執行。

第 33 章那些關於 globalOutbound: nullenv 能力邊界、limits 本機不生效的結論全部適用。Code Mode 就是那一章的沙箱在跑 LLM 寫的程式碼。


這一節專門糾正搜尋結果裡的雜訊。

東西狀態
workers-mcp2025-03 之後已死。 不要用
mcp-handler那是 Vercel 的套件,和 Cloudflare 無關,純粹撞名
MCP sampling文件裡沒有
MCP tool annotations文件裡沒有
MCP outputSchema文件裡沒有

還有一個很小但會浪費你半小時的:

MCP dev server 的預設埠是 8788,不是 8787

以及第 35 章提過的名字撞車,這裡再強調一次:

Cloudflare 的 MCP Server Portals 是 Cloudflare One / Zero Trust 的功能,和 Agents SDK 的 MCP server 完全是兩回事。 搜尋「Cloudflare MCP」會同時撈到兩者。


37.8 LinkForge:一個受 OAuth 保護的 MCP server

Section titled “37.8 LinkForge:一個受 OAuth 保護的 MCP server”

目標:讓使用者的 AI 助理(Claude、Cursor…)能管理他們的短網址。

src/mcp.ts
import { createMcpHandler, getMcpAuthContext } from "agents/mcp";
// SDK v2,不是 @modelcontextprotocol/sdk(37.4)
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
export const mcp = createMcpHandler(
// 工廠函式:每個請求一個新的 server(37.4)
() => {
const server = new McpServer({ name: "linkforge", version: "1.0.0" });
server.registerTool(
"list_links",
{
description: "List the caller's short links.",
inputSchema: { limit: z.number().int().min(1).max(50).default(20) },
},
async ({ limit }) => {
// 認證資訊從 auth context 拿,絕不從工具參數拿
const auth = getMcpAuthContext();
const tenantId = auth?.props.tenantId as string | undefined;
if (!tenantId) throw new Error("unauthenticated");
const { results } = await env.DB
.prepare("select slug, url, clicks from links where tenant_id = ?1 order by created_at desc limit ?2")
.bind(tenantId, limit).all();
return { content: [{ type: "text", text: JSON.stringify(results) }] };
},
);
server.registerTool(
"create_link",
{
description: "Create a short link.",
inputSchema: { url: z.url(), slug: z.string().regex(/^[a-z0-9-]{3,64}$/).optional() },
},
async ({ url, slug }) => {
const tenantId = getMcpAuthContext()?.props.tenantId as string;
if (!tenantId) throw new Error("unauthenticated");
const created = await createLink(env, tenantId, url, slug);
return { content: [{ type: "text", text: `Created ${created.slug}` }] };
},
);
return server;
},
{
route: "/mcp",
// 自訂網域必須自己設(37.4)
allowedHostnames: ["mcp.linkforge.dev"],
},
);

三個安全要點,每一個都對應本章或前面章節的實測:

  1. tenantIdgetMcpAuthContext() 拿,絕不從工具參數拿。 如果 list_links 有一個 tenantId 參數,LLM 就能被 prompt injection 說服去傳別人的租戶 ID。工具參數是使用者輸入等級的資料(第 33 章的結論)。
  2. 工廠函式,不是 singleton。 第 37.4。
  3. allowedHostnames 明確列出自訂網域。 型別註解說得很清楚,預設不保護自訂網域。

搭配 OAuth:

src/index.ts
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import { mcp } from "./mcp";
export default new OAuthProvider({
apiRoute: "/mcp",
apiHandler: mcp,
defaultHandler: app, // 你的 Hono app(第 5 章)處理登入頁
authorizeEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
clientRegistrationEndpoint: "/oauth/register",
allowPlainPKCE: false, // 37.5:預設是 true
});

OAuthProvider 會把驗證後的資訊放進 props,而那正是 getMcpAuthContext().props 讀到的東西 —— 這條鏈就是租戶隔離的全部。

MCP 工具的粒度要比 REST API 粗。 不要把你的 CRUD 端點一比一對應成工具 —— LLM 每多一個 tool call 就是一次延遲加一次出錯機會。

// ❌ 三個工具,LLM 要串起來
"get_link" / "update_link_url" / "invalidate_link_cache"
// ✅ 一個工具表達一個意圖
"retarget_link" // 內部自己處理更新 + 失效

這和第 18 章 RPC 那條「promise pipelining 減少 round trip」是同一個直覺,只是這裡的 round trip 是 LLM 的推論成本。


#結論影響
1agents 實測版本 0.20.1,文件無 GA/beta 宣告不要說它 GA;釘死版本
2繼承鏈 DurableObject → Server → Agent第 14-17 章的知識直接複用
3必須用 new_sqlite_classes migration + nodejs_compatnew_classes 執行期才壞(第 29 章:dry-run 不擋)
4有靜態資源時要 run_worker_first: ["/agents/*"]否則 SPA fallback 吞掉 agent 路由(第 7 章)
5server 端 hook 是 onStateChanged;client 端 useAgent 選項仍是 onStateUpdate兩個名字都活著,在不同地方
6原始碼裡兩個都覆寫應該 throw(Cannot override both onStateChanged and onStateUpdate),只覆寫舊的應該警告但見下面兩條
6a🔴 實測:wrangler dev 下兩者都不會發生。 Miniflare 用 class Wrapper extends UserClass 包住每個 DO class,Object.getPrototypeOf(this) 變成 Wrapper.prototype函式庫自己的 assertion 在本機被關掉,錯誤設定會一路過關到 deploy
6b一般化:DO 裡對 Object.getPrototypeOf(this)hasOwnProperty 不可攜;繼承查找則正常靠 own-property 註冊的 decorator / DI 在本機靜默失效
6cthis.constructor.name 仍正確(Miniflare defineProperty 複製名字),但 this.constructor === MyClassfalse名字可信,identity 不可信
7TypeScript 不會標記 onStateUpdate —— 它還在型別表面上兩個都寫編譯完全通過
8agents/ai-chat-agentagents/ai-react 的模組內容就是一個 throw載入即失敗,不是呼叫才失敗
9agents/ai-react 的錯誤訊息指向 @cloudflare/ai-chat/ai-react,那個路徑不存在實測正確的是 /react
10@cloudflare/ai-chat@0.10.1 的 exports:../react./types./ai-chat-v5-migration
11createMcpHandler 的真身是 createStatelessMcpHandler名字裡就寫了 Stateless
12🔴 它有兩個 overload。傳 instance 照樣編譯過,只是安靜地回 LegacyMcpHandler(有 session 的舊路徑)型別只是引導不是護欄;要看 @deprecated
12aMcpServerFactory 來自 @modelcontextprotocol/server(SDK v2),不是 @modelcontextprotocol/sdk(v1,1.30.0)。agents 兩個都 peer-depend裝錯套件的症狀是「McpServer 不能指派給 McpServer
12bSDK v2 移除了 server.tool() 簡寫,只剩 registerTool(name, config, cb)(config 支援 outputSchema/annotations/icons遷移不只是包一層箭頭函式
12c@modelcontextprotocol/server 自己也 export 一個 createMcpHandler又一個撞名
13模組層 singleton 在 MCP SDK 1.26+ 是安全問題isolate 重用會讓 session 狀態跨使用者洩漏
14allowedHostnames 預設只保護 localhost 與 workers.dev自訂網域要自己設DNS rebinding 防護
14a本機實測:有設 → 偽造 Host 得到 403 Invalid Host;沒設 → 200 放行這條可以在 wrangler dev 直接量
15allowedOriginHostnames: "*" 型別註解明說「只在上游有等效驗證時使用」;偽造 Origin 實測 403
15aMCP server 是本系列少數本機可完整跑通的東西:initialize / tools/list / tools/call 全部可用,Zod → JSON Schema 正確不需要 Cloudflare 帳號
15b沒有 OAuth 時 getMcpAuthContext()乾淨的 undefined不是會 throw 的 stub(對照第 6/23/25/27/30/32 章)
16@cloudflare/workers-oauth-provider 實測 0.8.3;0.8.0 修了 token revocation 與 code exchange 缺陷低於 0.8.0 視為有漏洞
17allowPlainPKCE 預設是 true一律設 false
18agents 有 33 個 export subpath,含 workflows(Fibers)、skillscodemode/aix402browseremail兩個掛在 experimental/ 底下
19waitForApproval() 由 Workflows 支撐,可等到 365 天等待期間不計 concurrency 與 CPU(第 21 章)
20Sub-agents 用 DO Facets,只有 parent 需要 binding第 33 章
21Code Mode(@cloudflare/codemode 0.5.1,beta)需要 worker_loaders第 33 章的沙箱結論全部適用
22workers-mcp 已死;mcp-handlerVercel 的無關套件純粹撞名
23MCP sampling 在 Cloudflare 文件裡沒有;但 outputSchema / annotationsSDK v2 registerTool config 原生支援的欄位分清楚「Cloudflare 沒寫」與「SDK 沒有」
24MCP dev server 預設埠是 8788,不是 8787
25MCP Server Portals 屬於 Cloudflare One / Zero Trust與本章無關(第 35 章也提過)

  1. 在一個 Agent 子類裡同時定義 onStateChangedonStateUpdate,在 wrangler dev 下確認它不會 throw;再把 Object.getPrototypeOf(this) 的 own properties 印出來,看到那三個 __miniflare_* 名字。然後(如果你有帳號)部署上去,看它是不是真的 throw —— 這是本章唯一沒被驗證的推論。
  2. 承上:寫一個 DO base class,用 hasOwnProperty(Object.getPrototypeOf(this), ...) 做方法註冊,確認本機完全失效。再改成繼承查找(proto.foo !== Base.prototype.foo),確認本機就正常了。
  3. import "agents/ai-chat-agent" 然後啟動 Worker,確認它在載入階段就失敗。再照著錯誤訊息 import @cloudflare/ai-chat/ai-react,觀察第二個錯誤。
  4. createMcpHandler(new McpServer(...)) 寫出來 —— 確認它編譯得過,然後用回傳型別(LegacyMcpHandler vs StatelessMcpHandler)證明你走到了 deprecated 的那條 overload。
  5. 在本機開兩個 MCP handler,一個設 allowedHostnames 一個不設,用 curl -H 'Host: evil.example.net' 分別打,比對 403 與 200。
  6. waitForApproval() 寫一個需要人類批准的工具,把它擱置一天再批准,確認 agent 真的從中斷處繼續。

  • Agents · Agents SDK API · MCP
  • @cloudflare/workers-oauth-provider · Model Context Protocol
  • 版本與 API 表面來源:node_modules/agents@0.20.1dist/index.js 的 state hook 防護與 _autoWrapCustomMethodsdist/ai-chat-agent.js 的 throw、dist/agent-tool-types-*.d.tscreateMcpHandler overload、dist/handler-stateless-*.d.tsCreateStatelessMcpHandlerOptions)、node_modules/@modelcontextprotocol/server@2.0.0McpServerFactoryMcpServer.registerTool)、node_modules/@cloudflare/ai-chat@0.10.1 的 exports、node_modules/miniflare/dist/src/workers/core/do-wrapper.worker.js(DO subclass wrapper)、npm registry 的版本資料
  • 所有 HTTP 觀測值取自本機 wrangler devexamples/ch37-agents),2026-08-01

第 37 章結束 Part 7。下一章起進入 Part 8 — 工程實務:第 38 章 測試,包含前面各章反覆遇到的那些「本機測不出來」的東西,到底該怎麼測。