agents 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1918 -72
  26. package/dist/chat/index.js +1730 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  45. package/dist/index.d.ts +91 -71
  46. package/dist/index.js +562 -24
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/serializable.d.ts +1 -1
  62. package/dist/skills/compile.js +1 -1
  63. package/dist/skills/compile.js.map +1 -1
  64. package/dist/skills/index.js +5 -5
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/sub-routing.d.ts +6 -6
  67. package/dist/utils.js +1 -1
  68. package/dist/utils.js.map +1 -1
  69. package/dist/vite.js +5 -5
  70. package/dist/vite.js.map +1 -1
  71. package/dist/wire-types-nflOzNuU.js +240 -0
  72. package/dist/wire-types-nflOzNuU.js.map +1 -0
  73. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  74. package/dist/workflow-types.d.ts +25 -21
  75. package/dist/workflow-types.js.map +1 -1
  76. package/dist/workflows.d.ts +22 -21
  77. package/dist/workflows.js +31 -7
  78. package/dist/workflows.js.map +1 -1
  79. package/docs/adding-to-existing-project.md +450 -0
  80. package/docs/agent-class.md +503 -0
  81. package/docs/agent-tools.md +552 -0
  82. package/docs/browse-the-web.md +430 -0
  83. package/docs/callable-methods.md +627 -0
  84. package/docs/chat-agents.md +1687 -0
  85. package/docs/chat-sdk.md +181 -0
  86. package/docs/client-sdk.md +520 -0
  87. package/docs/client-tools-continuation.md +177 -0
  88. package/docs/codemode.md +440 -0
  89. package/docs/configuration.md +775 -0
  90. package/docs/cross-domain-authentication.md +171 -0
  91. package/docs/durable-execution.md +537 -0
  92. package/docs/email.md +663 -0
  93. package/docs/get-current-agent.md +204 -0
  94. package/docs/getting-started.md +305 -0
  95. package/docs/http-websockets.md +668 -0
  96. package/docs/human-in-the-loop.md +661 -0
  97. package/docs/index.md +151 -0
  98. package/docs/long-running-agents.md +730 -0
  99. package/docs/mcp-client.md +620 -0
  100. package/docs/mcp-servers.md +526 -0
  101. package/docs/mcp-transports.md +308 -0
  102. package/docs/migration-to-ai-sdk-v5.md +96 -0
  103. package/docs/migration-to-ai-sdk-v6.md +163 -0
  104. package/docs/observability.md +261 -0
  105. package/docs/push-notifications.md +367 -0
  106. package/docs/queue.md +329 -0
  107. package/docs/readonly-connections.md +278 -0
  108. package/docs/resumable-streaming.md +127 -0
  109. package/docs/retries.md +444 -0
  110. package/docs/routing.md +749 -0
  111. package/docs/scheduling.md +898 -0
  112. package/docs/securing-mcp-servers.md +359 -0
  113. package/docs/server-driven-messages.md +477 -0
  114. package/docs/sessions.md +1024 -0
  115. package/docs/state.md +512 -0
  116. package/docs/sub-agents.md +389 -0
  117. package/docs/webhooks.md +604 -0
  118. package/docs/workflows.md +877 -0
  119. package/package.json +41 -14
  120. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  121. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  122. package/dist/client-BXJ9n2f7.js.map +0 -1
@@ -0,0 +1,359 @@
1
+ # Securing MCP Servers
2
+
3
+ Model Context Protocol servers, like every other web application, need to be secured so they can be used by trusted users without abuse. The MCP spec uses the OAuth 2.1 standard for authentication between MCP clients and servers.
4
+
5
+ Cloudflare's `workers-oauth-provider` lets you secure your MCP Server (or any application) running on a Cloudflare Worker. The provider handles token management, client registration, and access token validation automatically.
6
+
7
+ ```typescript
8
+ import { OAuthProvider, OAuthError } from "@cloudflare/workers-oauth-provider";
9
+ import { createMcpHandler } from "agents/mcp";
10
+
11
+ // A Worker that exposes an MCP server
12
+ const apiHandler = {
13
+ async fetch(request: Request, env: unknown, ctx: ExecutionContext) {
14
+ return createMcpHandler(server)(request, env, ctx);
15
+ }
16
+ };
17
+
18
+ // Wrap with OAuth protection
19
+ export default new OAuthProvider({
20
+ authorizeEndpoint: "/authorize",
21
+ tokenEndpoint: "/oauth/token",
22
+ clientRegistrationEndpoint: "/oauth/register",
23
+
24
+ apiRoute: "/mcp", // Protected MCP endpoint
25
+ apiHandler: apiHandler, // Your MCP server
26
+
27
+ defaultHandler: AuthHandler // Handles consent flow
28
+ });
29
+ ```
30
+
31
+ However, most MCP servers aren't just servers, they can actually be OAuth clients too. Your MCP server might sit between Claude Desktop and a third-party API like GitHub or Google. To Claude, you're a server. To GitHub, you're a client. This allows your users to authenticate and use their GitHub credentials to access your MCP server. We call this a proxy server.
32
+
33
+ There are a few security footguns to securely building a proxy server. The rest of this document aims to outline best practises to securing an MCP server.
34
+
35
+ ## `redirect_uri` validation
36
+
37
+ The `workers-oauth-provider` package handles this automatically. It validates that the `redirect_uri` in the authorization request matches one of the registered redirect URIs for the client. This prevents attackers from redirecting authorization codes to their own endpoints.
38
+
39
+ ## Consent dialog
40
+
41
+ When your MCP server acts as an OAuth proxy to third-party providers (like Google, GitHub, etc.), you must implement your own consent dialog before forwarding users to the upstream authorization server. This prevents the ["confused deputy"](https://en.wikipedia.org/wiki/Confused_deputy_problem) problem where attackers could exploit cached consent from the third-party provider to gain unauthorized access. Your consent dialog should clearly identify the requesting MCP client by name and display the specific scopes being requested. Implementing this consent flow requires thinking about a few security concerns.
42
+
43
+ ### CSRF Protection
44
+
45
+ Without CSRF protection, an attacker can trick users into approving malicious OAuth clients. Use a random token stored in a secure cookie and validate it on form submission.
46
+
47
+ ```typescript
48
+ // GET /authorize - Generate CSRF token when showing consent form
49
+ app.get("/authorize", async (c) => {
50
+ const { token: csrfToken, setCookie } = generateCSRFProtection();
51
+
52
+ return renderConsentDialog(c.req.raw, {
53
+ client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
54
+ csrfToken, // Pass to form as hidden field
55
+ setCookie // Set the cookie
56
+ // ... other dialog data
57
+ });
58
+ });
59
+
60
+ // POST /authorize - Validate CSRF token when user approves
61
+ app.post("/authorize", async (c) => {
62
+ const formData = await c.req.raw.formData();
63
+
64
+ // Validate CSRF token exists and matches cookie
65
+ const { clearCookie } = validateCSRFToken(formData, c.req.raw);
66
+
67
+ // Then redirect to upstream provider and clear the CSRF with the clearCookie header
68
+ });
69
+
70
+ // Helper functions
71
+ function generateCSRFProtection(): CSRFProtectionResult {
72
+ const token = crypto.randomUUID();
73
+ const setCookie = `__Host-CSRF_TOKEN=${token}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`;
74
+ return { token, setCookie };
75
+ }
76
+
77
+ function validateCSRFToken(
78
+ formData: FormData,
79
+ request: Request
80
+ ): ValidateCSRFResult {
81
+ const tokenFromForm = formData.get("csrf_token");
82
+ const cookieHeader = request.headers.get("Cookie") || "";
83
+ const tokenFromCookie = cookieHeader
84
+ .split(";")
85
+ .find((c) => c.trim().startsWith("__Host-CSRF_TOKEN="))
86
+ ?.split("=")[1];
87
+
88
+ if (!tokenFromForm || !tokenFromCookie || tokenFromForm !== tokenFromCookie) {
89
+ throw new OAuthError("invalid_request", "CSRF token mismatch", 400);
90
+ }
91
+
92
+ // Clear cookie after use (one-time use per RFC 9700)
93
+ return {
94
+ clearCookie: `__Host-CSRF_TOKEN=; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=0`
95
+ };
96
+ }
97
+ ```
98
+
99
+ Include the token as a hidden field in your consent form:
100
+
101
+ ```html
102
+ <input type="hidden" name="csrf_token" value="${csrfToken}" />
103
+ ```
104
+
105
+ ### Input sanitization
106
+
107
+ User-controlled content (client names, logos, URIs) in your consent dialog can execute malicious scripts if not sanitized. Client registration is dynamic, so you must treat all client metadata as untrusted input.
108
+
109
+ **Required protections:**
110
+
111
+ - **Client names/descriptions**: HTML-escape all text before rendering (escape `<`, `>`, `&`, `"`, `'`)
112
+ - **Logo URLs**: Validate URL scheme (allow only `http:` and `https:`), reject `javascript:`, `data:`, `file:` schemes
113
+ - **Client URIs**: Same as logo URLs - whitelist http/https only
114
+ - **Scopes**: Treat as text, HTML-escape before display
115
+
116
+ ```typescript
117
+ function sanitizeText(text: string): string {
118
+ return text
119
+ .replace(/&/g, "&amp;")
120
+ .replace(/</g, "&lt;")
121
+ .replace(/>/g, "&gt;")
122
+ .replace(/"/g, "&quot;")
123
+ .replace(/'/g, "&#039;");
124
+ }
125
+
126
+ function sanitizeUrl(url: string): string {
127
+ if (!url) return "";
128
+ try {
129
+ const parsed = new URL(url);
130
+ if (!["http:", "https:"].includes(parsed.protocol)) {
131
+ return ""; // Reject dangerous schemes
132
+ }
133
+ return url;
134
+ } catch {
135
+ return ""; // Invalid URL
136
+ }
137
+ }
138
+
139
+ // Always sanitize before rendering
140
+ const clientName = sanitizeText(client.clientName);
141
+ const logoUrl = sanitizeText(sanitizeUrl(client.logoUri));
142
+ ```
143
+
144
+ ### Content Security Policy (CSP)
145
+
146
+ CSP headers instruct browsers to block dangerous content and behaviors. They provide defence in depth from multiple attack vectors.
147
+
148
+ ```typescript
149
+ function buildSecurityHeaders(setCookie: string, nonce?: string): HeadersInit {
150
+ const cspDirectives = [
151
+ "default-src 'none'", // Deny everything by default
152
+ "script-src 'self'" + (nonce ? ` 'nonce-${nonce}'` : ""), // Allow scripts from same origin (+ nonce if using inline JS)
153
+ "style-src 'self' 'unsafe-inline'", // Allow inline styles for rendering
154
+ "img-src 'self' https:", // Allow client logos from HTTPS URLs
155
+ "font-src 'self'", // Allow web fonts from same origin
156
+ "form-action 'self'", // Only allow form submissions to same origin
157
+ "frame-ancestors 'none'", // Prevent clickjacking - block ALL iframe embedding
158
+ "base-uri 'self'", // Prevent base tag injection attacks
159
+ "connect-src 'self'" // Restrict fetch/XHR to same origin
160
+ ].join("; ");
161
+
162
+ return {
163
+ "Content-Security-Policy": cspDirectives,
164
+ "X-Frame-Options": "DENY", // Legacy clickjacking protection for older browsers
165
+ "X-Content-Type-Options": "nosniff", // Prevent MIME sniffing attacks
166
+ "Content-Type": "text/html; charset=utf-8",
167
+ "Set-Cookie": setCookie
168
+ };
169
+ }
170
+ ```
171
+
172
+ ### Inline JavaScript
173
+
174
+ If your consent dialog needs inline JavaScript, use data attributes and nonces to prevent XSS attacks.
175
+
176
+ ```typescript
177
+ // Generate a unique nonce per request
178
+ const nonce = crypto.randomUUID();
179
+
180
+ const htmlContent = `
181
+ <!DOCTYPE html>
182
+ <html>
183
+ <body>
184
+ <p>Authorization approved! Redirecting...</p>
185
+ <script nonce="${nonce}" data-redirect-url="${sanitizeUrl(redirectUrl)}">
186
+ setTimeout(() => {
187
+ const script = document.querySelector('script[data-redirect-url]');
188
+ window.location.href = script.dataset.redirectUrl;
189
+ }, 2000);
190
+ </script>
191
+ </body>
192
+ </html>
193
+ `;
194
+
195
+ return new Response(htmlContent, {
196
+ headers: buildSecurityHeaders(setCookie, nonce) // Pass nonce to include in CSP
197
+ });
198
+ ```
199
+
200
+ - **Data attributes** store user-controlled data (like URLs) separately from JavaScript code, ensuring they're always treated as strings, never as executable code
201
+ - **Nonces** combined with the correct CSP headers (shown above) allow your specific inline script to execute while blocking any injected scripts
202
+
203
+ Note: Frameworks such as Vite will automatically handle nonce generation and insertion for you. See their docs on [Content Security Policy](https://vite.dev/guide/features.html#content-security-policy-csp) for more information.
204
+
205
+ ## Handling State
206
+
207
+ Between the consent dialog and the callback there is a gap where the user could do something nasty. We need to make sure it is the same user that hits authorize and then reaches back to our callback. Use a random state token stored server-side in KV with a short expiration time.
208
+
209
+ ```typescript
210
+ // Use in POST /authorize - after CSRF validation, before redirecting to upstream provider
211
+ // Firstly create a state token in KV
212
+ async function createOAuthState(
213
+ oauthReqInfo: AuthRequest,
214
+ kv: KVNamespace
215
+ ): Promise<{ stateToken: string }> {
216
+ const stateToken = crypto.randomUUID();
217
+ await kv.put(`oauth:state:${stateToken}`, JSON.stringify(oauthReqInfo), {
218
+ expirationTtl: 600 // 10 minutes
219
+ });
220
+ return { stateToken };
221
+ }
222
+
223
+ // Bind state to browser session
224
+ async function bindStateToSession(
225
+ stateToken: string
226
+ ): Promise<{ setCookie: string }> {
227
+ const consentedStateCookieName = "__Host-CONSENTED_STATE";
228
+
229
+ // Hash the state token to create a derived parameter
230
+ const encoder = new TextEncoder();
231
+ const data = encoder.encode(stateToken);
232
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
233
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
234
+ const hashHex = hashArray
235
+ .map((b) => b.toString(16).padStart(2, "0"))
236
+ .join("");
237
+
238
+ const setCookie = `${consentedStateCookieName}=${hashHex}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`;
239
+ return { setCookie };
240
+ }
241
+
242
+ // In the GET /callback - validate state from query params against both the KV and the session cookie before exchanging code
243
+ async function validateOAuthState(
244
+ request: Request,
245
+ kv: KVNamespace
246
+ ): Promise<{ oauthReqInfo: AuthRequest; clearCookie: string }> {
247
+ const consentedStateCookieName = "__Host-CONSENTED_STATE";
248
+ const url = new URL(request.url);
249
+ const stateFromQuery = url.searchParams.get("state");
250
+
251
+ if (!stateFromQuery) {
252
+ throw new OAuthError("invalid_request", "Missing state parameter", 400);
253
+ }
254
+
255
+ // Check 1: Validate state exists in KV
256
+ const storedDataJson = await kv.get(`oauth:state:${stateFromQuery}`);
257
+ if (!storedDataJson) {
258
+ throw new OAuthError("invalid_request", "Invalid or expired state", 400);
259
+ }
260
+
261
+ // Check 2: Validate state matches session cookie
262
+ const cookieHeader = request.headers.get("Cookie") || "";
263
+ const cookies = cookieHeader.split(";").map((c) => c.trim());
264
+ const consentedStateCookie = cookies.find((c) =>
265
+ c.startsWith(`${consentedStateCookieName}=`)
266
+ );
267
+ const consentedStateHash = consentedStateCookie
268
+ ? consentedStateCookie.substring(consentedStateCookieName.length + 1)
269
+ : null;
270
+
271
+ if (!consentedStateHash) {
272
+ throw new OAuthError(
273
+ "invalid_request",
274
+ "Missing session binding cookie - authorization flow must be restarted",
275
+ 400
276
+ );
277
+ }
278
+
279
+ // Hash the state from query and compare with cookie
280
+ const encoder = new TextEncoder();
281
+ const data = encoder.encode(stateFromQuery);
282
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
283
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
284
+ const stateHash = hashArray
285
+ .map((b) => b.toString(16).padStart(2, "0"))
286
+ .join("");
287
+
288
+ if (stateHash !== consentedStateHash) {
289
+ throw new OAuthError(
290
+ "invalid_request",
291
+ "State token does not match session - possible CSRF attack detected",
292
+ 400
293
+ );
294
+ }
295
+
296
+ // Both checks passed - clean up KV and return the clear cookie header
297
+ await kv.delete(`oauth:state:${stateFromQuery}`);
298
+ const clearCookie = `${consentedStateCookieName}=; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=0`;
299
+
300
+ return {
301
+ oauthReqInfo: JSON.parse(storedDataJson),
302
+ clearCookie
303
+ };
304
+ }
305
+ ```
306
+
307
+ ## Approved client
308
+
309
+ MCP proxy servers must maintain a registry of approved client IDs per user and check this registry before initiating the third-party authorization flow. Store approved clients in a secure, cryptographically signed cookie with HMAC-SHA256.
310
+
311
+ ```typescript
312
+ // Use in POST /authorize - after user approves, add client to approved list
313
+ export async function addApprovedClient(
314
+ request: Request,
315
+ clientId: string,
316
+ cookieSecret: string
317
+ ): Promise<string> {
318
+ const existingApprovedClients =
319
+ (await getApprovedClientsFromCookie(request, cookieSecret)) || [];
320
+ const updatedApprovedClients = Array.from(
321
+ new Set([...existingApprovedClients, clientId])
322
+ );
323
+
324
+ const payload = JSON.stringify(updatedApprovedClients);
325
+ const signature = await signData(payload, cookieSecret); // HMAC-SHA256
326
+ const cookieValue = `${signature}.${btoa(payload)}`;
327
+
328
+ return `__Host-APPROVED_CLIENTS=${cookieValue}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=2592000`;
329
+ }
330
+ ```
331
+
332
+ When reading the cookie in GET /authorize (before showing the consent dialog), verify the signature before trusting the data. If the signature doesn't match or the client isn't in the list, show the consent dialog. If the client is approved, skip the dialog and proceed directly to creating the OAuth state.
333
+
334
+ ## Cookies
335
+
336
+ ### Why `__Host-` prefix?
337
+
338
+ Throughout this document you'll see cookies named with the `__Host-` prefix (like `__Host-CSRF_TOKEN` and `__Host-APPROVED_CLIENTS`). This is especially important for MCP servers running on `*.workers.dev` domains.
339
+
340
+ The `__Host-` prefix is a security feature that prevents subdomain attacks. When you set a cookie with this prefix:
341
+
342
+ - It **must** be set with the `Secure` flag (HTTPS only)
343
+ - It **must** have `Path=/`
344
+ - It **must not** have a `Domain` attribute
345
+
346
+ This means the cookie is locked to the exact domain that set it. Without `__Host-`, an attacker controlling `evil.workers.dev` could set cookies for your `mcp-server.workers.dev` domain and potentially inject malicious CSRF tokens or approved client lists. The `__Host-` prefix prevents this by ensuring only your specific domain can set and read these cookies.
347
+
348
+ ### Multiple OAuth clients on the same host
349
+
350
+ If you're running multiple OAuth flows on the same domain (e.g., GitHub OAuth and Google OAuth on the same worker), namespace your cookies to prevent collisions.
351
+
352
+ Instead of `__Host-CSRF_TOKEN`, use `__Host-CSRF_TOKEN_GITHUB` and `__Host-CSRF_TOKEN_GOOGLE`. Same applies for approved clients: `__Host-APPROVED_CLIENTS_GITHUB` vs `__Host-APPROVED_CLIENTS_GOOGLE`. This ensures each OAuth flow maintains isolated state.
353
+
354
+ # More info
355
+
356
+ - [MCP Authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
357
+ - [MCP Security Best Practices](https://modelcontextprotocol.io/specification/draft/basic/security_best_practices)
358
+ - [RFC 9700 - Protecting Redirect Based Flows](https://www.rfc-editor.org/rfc/rfc9700#name-protecting-redirect-based-f)
359
+ - [RFC 9700 - Best Practices](https://www.rfc-editor.org/rfc/rfc9700#name-best-practices)