Bài này tổng hợp toàn bộ lỗi tôi gặp khi cấu hình Cloudflare AI Controls (MCP servers + MCP Portal) lần đầu, kèm nguyên nhân thật và cách sửa. Gần như mọi lỗi đều không khớp với suy đoán ban đầu — tôi đã sửa nhầm tầng nhiều lần trước khi tìm ra.

Đây là phần 2 của series 2 bài:

Bảng tra nhanh

Tìm đúng thông báo lỗi bạn đang gặp:

| Thông báo lỗi | Tầng đang chặn | Mục | | --- | --- | --- | | Redirect 302 tới /cdn-cgi/access/login | Cloudflare Access | Lỗi 1 | | Server error: HTTP 403 khi sync MCP object | Cloudflare Access | Lỗi 2 | | Server error: HTTP -1 | Cloudflare Access | Lỗi 3 | | 401 + missing bearer token | MCP server (origin) | Lỗi 4 | | 404 + "Session not found" | MCP server (origin) | Lỗi 5 | | invalid 'include' configuration | Access API | Lỗi 6 | | invalid_client: Client not found | Portal OAuth | Lỗi 7 | | No allowed servers available | Visibility policy | Lỗi 8 | | mcp_token_exchange_failed | Portal session | Lỗi 9 |

Nguyên tắc chẩn đoán: đọc mã lỗi, đừng đoán

Trước khi vào từng lỗi, đây là thứ tiết kiệm nhiều thời gian nhất.

Một request đi qua ba tầng có thể từ chối nó, và mỗi tầng trả về mã khác nhau:

Client → [1] Cloudflare Access → [2] Tunnel → [3] MCP server
              302 / 403                          401 / 404

Mẹo phân biệt nhanh nhất: xem response header.

curl -i -X POST https://py-mcp.example.com/mcp -H "..." 
  • cf-access-domain:server: cloudflare, body là HTML → Access chặn, request chưa tới MCP server. Đi sửa policy.

  • Body là JSON-RPC ({"jsonrpc":"2.0","error":...}) → đã vào tới MCP server. Đừng sửa policy nữa, vấn đề ở origin.

Tôi từng mất nhiều giờ sửa policy cho một lỗi thực chất nằm ở origin, và ngược lại. Kiểm tra header trước, sửa sau.

Một lưu ý nữa: đọc mã lỗi trong tooltip của AI Controls, đừng chỉ curl bằng tay. Curl của bạn và probe của Cloudflare gửi credential khác nhau, nên có thể curl 200 mà object vẫn Error.

Lỗi 1 — Redirect 302 tới trang login

HTTP/2 302
location: https://<team>.cloudflareaccess.com/cdn-cgi/access/login/...

Triệu chứng: mọi request tới /mcp đều bị đẩy sang trang đăng nhập Cloudflare, kể cả khi gửi kèm token.

Nguyên nhân: Access không tìm thấy credential nào khớp policy của application, nên coi đây là người dùng chưa đăng nhập và đẩy đi login.

Cách sửa:

  1. Kiểm tra application origin đã có policy Service Auth với Include là Service Token chưa.

  2. Kiểm tra request có gửi đúng cả hai header CF-Access-Client-IdCF-Access-Client-Secret không. Thiếu một trong hai là hỏng.

  3. Kiểm tra Client ID có đuôi .access không — dán thiếu phần đuôi là lỗi rất hay gặp.

Xác nhận: gửi đủ hai header phải nhận 200 (hoặc 401 nếu thiếu bearer của origin — vẫn là tiến bộ, xem lỗi 4).

Lỗi 2 — Error syncing MCP server: failed to get information / HTTP 403

Error syncing MCP server: Error: failed to get information
Server error: HTTP 403
The upstream server returned an error HTTP 403

Triệu chứng: MCP object trong AI Controls hiện trạng thái Error. Tooltip báo HTTP 403. Nhưng nếu bạn curl bằng service token thì lại 200 bình thường.

Đây là lỗi tôi mất nhiều thời gian nhất.

Nguyên nhân: MCP object không gửi service token khi gọi upstream. Nó chỉ gửi những header bạn khai trong Custom Headers. Nếu ở đó chỉ có bearer token của origin, Access sẽ chặn ngay vì bearer token hoàn toàn vô nghĩa với Access.

Cách chẩn đoán:

curl -i -X POST https://py-mcp.example.com/mcp \
  -H "Authorization: Bearer <BEARER_TOKEN>"

Nếu nhận:

HTTP/2 403
content-type: text/html
server: cloudflare
cf-access-domain: py-mcp.example.com

thì đã xác nhận: có cf-access-domain nghĩa là Access chặn, chưa tới origin.

Cách sửa:

  1. Vào AI Controls → MCP servers → <object> → Edit → Authentication → Custom Headers.

  2. Thêm hai header (giữ nguyên header bearer đang có):

    • CF-Access-Client-Id: <CLIENT_ID>.access

    • CF-Access-Client-Secret: <CLIENT_SECRET>

  3. Đảm bảo application origin có policy Service Auth allow đúng service token đó.

  4. Save and connectSync capabilities.

Cạm bẫy — Linked App Token không phải câu trả lời. Trong màn tạo policy Service Auth có mục Linked App Token, liệt kê sẵn tên các MCP object của bạn. Nghe đúng y như thứ cần thiết. Tôi đã tick đủ mọi token trong đó và vẫn nhận 403. Nó không phải credential mà AI Controls trình ra khi sync. Đừng đi hướng này.

Vì sao "trước khi tạo Access app thì sync được, sau đó lại lỗi": trước đó hostname chưa có application nào gate /mcp, nên object chỉ cần bearer là chạm được origin. Tạo Access application xong thì service token trở thành bắt buộc. Đây là bước còn thiếu, không phải hồi quy.

Xác nhận: status chuyển sang Ready, cột Tools hiện đúng số lượng.

Lỗi 3 — Server error: HTTP -1

Server error: HTTP -1
The upstream server returned an error

Triệu chứng: MCP object báo Error với mã -1 — một mã không tồn tại trong HTTP. Thường xuất hiện ngay sau khi bạn thêm policy Allow theo email vào application origin.

Nguyên nhân: HTTP -1 nghĩa là AI Controls không nhận được response HTTP hợp lệ. Khi application origin có policy Allow theo identity, probe headless (không có identity nào) sẽ bị đẩy sang trang đăng nhập và nhận về HTML — không phải JSON. AI Controls không parse được nên báo -1.

Đây thực chất cùng gốc với lỗi 2: Service Auth chưa khớp. Chỉ khác biểu hiện.

Cách sửa:

  1. Gỡ policy Allow theo email khỏi application origin. Application origin dành cho máy, không dành cho người browse trực tiếp.

  2. Đảm bảo nó chỉ còn policy Service Auth.

  3. Thêm service token vào Custom Headers của object (như lỗi 2).

Access xét Service Auth và Bypass trước policy Allow. Khi service token khớp, request được cho qua ngay mà không cần identity — đó là hành vi đúng.

Xác nhận: Sync lại, status Ready.

Lỗi 4 — 401 missing bearer token

HTTP/2 401
{"error": "missing bearer token"}

Triệu chứng: nhận 401 với body là JSON, không phải HTML.

Đây thường là tin tốt. Body JSON nghĩa là request đã đi qua Access, qua tunnel, và tới được MCP server của bạn — chính MCP server đang từ chối. Bạn đã đi được 90% quãng đường.

Nguyên nhân: thiếu hoặc sai bearer token nội bộ của MCP server.

Cách sửa:

  1. Kiểm tra Custom Headers của object có header xác thực mà MCP server yêu cầu chưa (thường là Authorization: Bearer <token>).

  2. Đối chiếu giá trị token với file .env của MCP server — sai một ký tự cũng hỏng.

  3. Nếu vừa rotate token, nhớ restart MCP server cập nhật lại Custom Headers.

Lưu ý về giai đoạn setup: ở bước smoke test ban đầu, khi chưa tạo Access application, nhận 401đúng và mong đợi — nó chứng minh tunnel đã thông tới origin. Đừng vội đi cấu hình Access khi thấy 401 ở giai đoạn này.

Lỗi 5 — 404 Session not found (code -32600)

HTTP/2 404
{"jsonrpc":"2.0","error":{"code":-32600,"message":"Session not found"}}

Triệu chứng: MCP server chạy được một thời gian rồi "chết" định kỳ. Nhưng kiểm tra thì process vẫn active, NRestarts=0, log không có gì bất thường. Sync lại thì lỗi, đợi một lúc có khi lại chạy.

Nguyên nhân: MCP streamable-http mặc định chạy ở chế độ stateful — server giữ session trong bộ nhớ theo mcp-session-id. Khi stream đứt (tunnel reconnect, idle timeout, hoặc AI Controls đóng stream giữa hai lần sync), session bị xoá khỏi RAM. Lần sync sau, client tái sử dụng mcp-session-id đã cache, server không nhận ra nữa.

Server hoàn toàn khoẻ mạnh — chỉ là nó không nhớ session cũ.

Cách sửa (bền vững): chuyển sang stateless HTTP. Với FastMCP:

FastMCP("my-server", stateless_http=True, json_response=True)

Stateless thì mỗi request tự đủ thông tin, không cần mcp-session-id, không có session trong RAM để mất.

Đánh đổi: mất server-push, notification và resumability. Nếu toàn bộ tool của bạn đều là request/response thuần thì hoàn toàn chấp nhận được.

Một biến thể oái oăm: có lần nguyên nhân là một smoke test cũ chạy nền vẫn đang giữ session/lock, làm mọi probe mới bị treo. Kiểm tra tiến trình đang giữ port:

ss -tlnp | grep 8765
ps aux | grep mcp

Kill tiến trình cũ đi là sync lại được ngay. Nếu MCP "chết" ngay sau khi bạn chạy test thủ công, khả năng cao là ca này.

Xác nhận: sau khi đổi sang stateless, restart server, gọi initialize rồi tools/list — response không kèm mcp-session-id.

Lỗi 6 — invalid 'include' configuration

invalid 'include' configuration

Triệu chứng: báo lỗi ngay lúc tạo hoặc sửa MCP object, không phải lúc chạy.

Nguyên nhân: đây là lỗi từ Cloudflare Access API, không liên quan gì tới MCP server của bạn. Xảy ra khi flow tạo object gắn kèm một Access application có policy với block include rỗng hoặc không hợp lệ — thường do chưa chọn identity hoặc chưa chọn service token.

Cách sửa:

  1. Vào Zero Trust → Access → Applications, mở application liên quan.

  2. Kiểm tra từng policy: mọi policy phải có ít nhất một rule trong phần Include.

  3. Policy có Include trống thì xoá hoặc điền cho đủ.

Đừng xoá object rồi tạo lại như cách chữa lỗi 404 — hai lỗi này khác tầng hoàn toàn.

Lỗi 7 — invalid_client: Client not found

invalid_client: Client not found

Thường thấy ở URL callback dạng:

/authorize?...&client_id=<một-chuỗi-lạ>...

Triệu chứng: bấm Connect ở Claude, bị đẩy sang trang lỗi ngay, chưa kịp đăng nhập.

Có hai nguyên nhân khác nhau.

Nguyên nhân A — dán service token vào ô OAuth Client ID

Nếu client_id trong URL chính là CF-Access-Client-Id của bạn (đuôi .access), thì đây là nguyên nhân.

Rất dễ mắc: bạn vừa dùng service token cho phần upstream, thấy Claude hỏi "OAuth Client ID", nên dán vào. Nhưng service token không phải OAuth client:

| | Service token | OAuth client | | --- | --- | --- | | Dùng cho | Máy gọi máy (headless) | Người dùng đăng nhập | | Ai cấp | Bạn tạo trên Cloudflare | Claude tự đăng ký qua DCR | | Đặt ở đâu | Custom Headers của MCP object | Không đặt ở đâu cả |

Portal không có client nào đăng ký với ID đó, nên trả Client not found. Tạo service token mới không sửa được — vì vấn đề không nằm ở token.

Cách sửa:

  1. Xoá connector trong Claude.

  2. Add lại, chỉ nhập URL https://mcp.example.com/mcp.

  3. Để trống hoàn toàn ô OAuth Client ID và Client Secret.

  4. Connect — Claude sẽ tự đăng ký OAuth client qua Dynamic Client Registration.

Nguyên nhân B — DCR client cũ mồ côi

Nếu client_id là một chuỗi ngẫu nhiên (không phải service token), thì Claude đang replay một client đã đăng ký với portal cũ đã bị xoá.

Cách sửa: remove hẳn connector rồi add lại. Không chỉ disconnect — phải xoá hẳn để Claude quên client cũ.

Đây là bước bắt buộc sau mỗi lần tạo lại portal, không phải lỗi hạ tầng.

Lỗi 8 — No allowed servers available, check your Zero trust policies

No allowed servers available, check your Zero trust policies

Triệu chứng: đăng nhập portal thành công, nhưng danh sách server trống trơn.

Điểm quan trọng: lỗi này xảy ra sau khi đăng nhập, khác hẳn lỗi 7 (xảy ra trước). Nên OAuth của bạn đã chạy đúng — đừng đi sửa OAuth nữa.

Nguyên nhân: portal đã biết bạn là ai, nhưng không tìm thấy server nào mà danh tính đó được phép nhìn thấy.

Đây là chỗ dễ nhầm giữa hai loại policy hoàn toàn khác nhau:

| Loại policy | Gắn ở đâu | Quyết định | | --- | --- | --- | | Access policy | Application | Request có đi qua được edge không | | Server-level policy | MCP object | Server có hiện trong danh sách portal không |

Bạn có thể pass hoàn toàn cái đầu mà vẫn trống danh sách vì thiếu cái sau.

Cách sửa:

  1. Vào AI Controls → MCP servers → <object>, mở phần policy của chính object (không phải của application).

  2. Thêm policy Allow với Include là Emails chứa email bạn dùng để đăng nhập portal.

  3. Làm cho từng object. Thiếu object nào thì object đó không hiện.

  4. Kiểm tra Require user auth của mapping trong portal có nhất quán không: để off thì object phải có service token trong Custom Headers; để on thì upstream phải có Allow policy khớp identity người dùng. Đặt nửa vời sẽ ra lỗi này.

  5. Kiểm tra server đã được Add vào portal và không bị disable toàn bộ tool.

Xác nhận: reconnect từ Claude. Nếu chỉ một runtime hiện ra, object còn lại đang thiếu policy — soi đúng object đó.

Lỗi 9 — mcp_token_exchange_failed / Session expired, please reauthenticate

error_code=mcp_token_exchange_failed
Authorization with the MCP server failed. You can check your credentials and permissions.

Kèm theo, ở tab trình duyệt:

Session expired, please reauthenticate

Triệu chứng: đăng nhập Cloudflare xong, tới bước cuối thì fail. Báo "Session expired" dù bạn thao tác liền mạch chỉ trong vài giây.

Loại trừ trước: nếu các MCP object đều Ready và mapping đều Require user auth = off, thì đây không phải lỗi upstream (khác lỗi 8). Vấn đề nằm ở lớp OAuth giữa client và portal.

Nguyên nhân chính — policy Service Auth gắn nhầm lên portal.

Rất dễ mắc nếu bạn tạo một policy Service Auth ở bước cấu hình upstream rồi tiện tay gán cho cả ba application (hai origin + một portal) cho gọn. Nhưng người vào portal là user thật với OAuth session, không phải máy. Policy Service Auth nằm đó sẽ shadow mất session OAuth, làm bước đổi authorization code → token thất bại.

Cách sửa:

  1. Vào Zero Trust → Access → Applications, mở application portal.

  2. Gỡ mọi policy Service Auth khỏi portal.

  3. Chỉ để lại một policy: Action Allow, Include Emails.

  4. Vào https://mcp.example.com/cdn-cgi/access/logout để xoá session cũ.

  5. Remove connector trong Claude, add lại chỉ bằng URL.

Ở hạ tầng của tôi, gỡ Service Auth khỏi portal là fix được ngay.

Nếu vẫn lỗi, kiểm thêm:

  • Session duration quá ngắn: kiểm session duration của cả application portal lẫn policy Allow gắn trên nó (policy override application). Đặt 24 hours.

  • IdP chưa áp cho hostname portal: đảm bảo One-time PIN hoặc Google đang bật và áp đúng cho mcp.example.com.

  • Đọc Access Logs: Zero Trust → Logs → Access, lọc theo hostname portal, xem lần fail đó policy nào match.

Quy tắc rút ra: policy phải đặt đúng object, đừng gán chung cho tiện.

| Object | Policy đúng | Tuyệt đối không | | --- | --- | --- | | Application origin (/mcp) | Service Auth | Allow theo email → lỗi 3 | | Application health (/healthz) | Bypass Everyone | Service Auth | | MCP server object | Allow theo email + Custom Headers | Bypass | | MCP Portal | Allow theo email | Service Auth → lỗi 9 |

Vấn đề khác: vẫn bị hỏi OTP dù chưa tạo policy Allow

Không hẳn là lỗi, nhưng gây hoang mang nên ghi lại.

Triệu chứng: bạn chưa tạo policy "Allow by email" nào, mà mở trang vẫn bị hỏi email và mã OTP.

Đây là hành vi đúng. Hai thứ hoàn toàn tách biệt:

| | Identity Provider (IdP) | Policy Allow | | --- | --- | --- | | Trả lời | Đăng nhập bằng cách nào | Ai được vào | | Cấu hình ở | Settings → Authentication | Từng application | | Chạy khi nào | Ngay khi mở trang login | Sau khi đăng nhập xong |

Trang login hiện mọi IdP đang bật, không phụ thuộc policy. Nên bạn vẫn được hỏi OTP, xác thực xong rồi mới bị chặn nếu email không nằm trong policy nào.

Không nhận được mã PIN qua email? Đó là do IdP One-time PIN chưa bậtSettings → Authentication, không phải do thiếu "policy OTP" — vì OTP không phải policy.

Muốn một email vào được, cần cả hai: bật IdP thêm policy Allow chứa email đó.

Tổng kết: ba điều nếu biết trước sẽ tiết kiệm nhiều giờ

  1. Portal không phải tunnel. Nó là endpoint Cloudflare tự host, đứng trước các upstream có tunnel riêng. Cấu hình portal như một hostname trỏ về máy bạn sẽ không bao giờ chạy.

  2. Credential để MCP object qua Access nằm ở Custom Headers của object, không phải Linked App Token trong policy. Đây là chỗ tôi loay hoay lâu nhất.

  3. Portal chỉ nhận Allow theo identity. Gắn Service Auth lên portal là tự phá OAuth session của chính mình.

Và nguyên tắc bao trùm: đọc mã lỗi để biết tầng nào chặn, đừng đoán. 403401 trông giống nhau trên dashboard nhưng ở hai tầng hoàn toàn khác nhau — sửa nhầm tầng thì thử bao nhiêu lần cũng vẫn lỗi cũ.

Hướng dẫn cấu hình đầy đủ từ đầu nằm ở phần 1 của series.