1. Tại sao cần nói về tích hợp và di trú
Đến giờ chúng ta chủ yếu thiết kế API và công cụ theo cách thuận tiện cho mình. Trong thực tế thì gần như luôn ngược lại: bạn đã có sẵn:
- monolith hoặc một đống microservice;
- REST/GraphQL API;
- business logic chạy trong production nhiều năm.
Và bỗng xuất hiện yêu cầu: “Kết nối sản phẩm của chúng ta với ChatGPT qua Apps SDK và MCP”.
Viết lại mọi thứ thành “máy chủ MCP lý tưởng” — không khả thi. Cần khéo léo “khoác” lên thế giới hiện có một lớp mỏng, chuyển ngữ backend của bạn sang ngôn ngữ ChatGPT: tools, resources và schema.
Vấn đề thứ hai: sản phẩm là thực thể sống. Schema và API thay đổi. Ở frontend thường bạn sẽ nhận lỗi TypeScript ngay khi đổi trường. Trong thế giới LLM-Apps thì quỷ quyệt hơn: mô hình vẫn tự tin gửi định dạng cũ, tool sẽ lỗi, và thay vì build fail “đẹp” bạn sẽ có:
- lỗi runtime trên máy chủ MCP;
- hallucination kiểu “tôi đoán đại bạn muốn gì từ trường này”;
- những sự cố chất lượng đáng tiếc.
Vì thế trong bài này, chúng ta nhìn lớp MCP+Apps như:
- một adapter cho backend hiện có;
- một hợp đồng cần duy trì trong nhiều năm;
- đối tượng của các di trú: phiên bản, annotation, scopes và SDK.
2. Kiến trúc tích hợp: MCP như adapter trên backend hiện có
Bức tranh cơ bản
Nhắc lại stack, nhưng dưới lăng kính production:
flowchart LR U[Người dùng trong ChatGPT] --> G[Mô hình ChatGPT] G -->|gọi App| W["Widget (Apps SDK, Next.js)"] G -->|tools.call| MCP[MCP server / Gateway] MCP --> S1["Gift Service (dịch vụ hiện có của bạn)"] MCP --> S2["Commerce Service (đơn hàng, ACP)"]
ChatGPT không nói chuyện trực tiếp với thế giới của bạn, mà qua giao thức MCP: danh sách tools/resources, các lời gọi tools/call, và streaming sự kiện.
Trong sơ đồ này, máy chủ MCP chính là lớp adapter đó: nó hiểu về ChatGPT (JSON‑RPC, tools), và hiểu về các dịch vụ của bạn (REST/DB/hàng đợi), rồi chuyển đổi qua lại.
MCP như Gateway/Adapter
Bài toán kinh điển: bạn đã có Gift Service với các endpoint REST:
// Ví dụ REST API hiện có
GET /api/gifts/recommendations?budget=100&occasion=birthday
POST /api/orders
Thay vì viết business logic mới, lớp MCP chỉ bọc lại thành một Tool:
// mcp/tools/recommendGifts.ts
import { z } from "zod";
import { server } from "./mcpServer"; // instance SDK giả định
const recommendGiftsInput = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
});
server.registerTool({
name: "recommend_gifts",
description: "Gợi ý ý tưởng quà tặng trong phạm vi ngân sách",
inputSchema: recommendGiftsInput,
async execute(args) {
const { occasion, budgetUsd } = recommendGiftsInput.parse(args);
const res = await fetch(
`https://api.myapp.com/gifts/recommendations?budget=${budgetUsd}&occasion=${occasion}`,
);
return res.json(); // quan trọng: trả về JSON, thuận tiện cho cả model và widget
},
});
Toàn bộ logic chọn quà vẫn nằm trong dịch vụ hiện có của bạn. Lớp MCP là “bộ phiên dịch mỏng” từ ngôn ngữ ChatGPT sang ngôn ngữ API của bạn.
Đôi khi lớp MCP còn định tuyến yêu cầu tới nhiều dịch vụ backend. Khi đó nó trở thành MCP Gateway hoàn chỉnh — vai trò này bạn sẽ xem sâu hơn trong mô-đun về production và mạng.
Monolith-integrated MCP vs Sidecar MCP
Có hai phương án cơ bản để “gắn” lớp MCP.
Mô tả bằng chữ như sau:
| Phương án | Mô tả | MCP code ở đâu |
|---|---|---|
| Monolith-integrated | Tất cả trong một dịch vụ Next.js/Node | Trong các route API của Next.js hoặc Express |
| Sidecar MCP | Container/dịch vụ riêng, giao tiếp với API | Ứng dụng Node/Go riêng |
Với dự án nhỏ, phương án đầu thường đủ: ứng dụng Next.js, deploy trên Vercel, có route /mcp hoặc /api/mcp, và máy chủ MCP sống cạnh các API khác.
Ví dụ (rút gọn):
// app/api/mcp/route.ts (Next.js 16)
import { NextRequest } from "next/server";
import { mcpHandler } from "@/mcp/server";
export async function POST(req: NextRequest) {
const body = await req.json();
const response = await mcpHandler.handle(body); // yêu cầu JSON-RPC
return new Response(JSON.stringify(response), {
headers: { "content-type": "application/json" },
});
}
Trong kiến trúc “trưởng thành” hơn, có nhiều dịch vụ miền (Gift, Commerce, Analytics), tiện hơn là tách lớp MCP thành một dịch vụ Gateway riêng. Nó sẽ nhận MCP traffic từ ChatGPT rồi tự định tuyến lời gọi tới các backend theo tên tool.
Cần nhớ: với ChatGPT và Apps SDK, đây vẫn là một máy chủ MCP duy nhất. Nó chạy trong monolith hay là microservice riêng — đó là quyết định kiến trúc của bạn.
Đã rõ chỗ ở của lớp MCP: trong monolith hoặc như Gateway riêng. Bước kế tiếp là câu hỏi chính xác lớp này nhận và trả gì — và lúc này schema và hợp đồng bước lên sân khấu.
3. Single Source of Truth: schema, kiểu và kiểm thử hợp đồng
Nếu bạn có DTO nội bộ, hợp đồng REST bên ngoài và thêm cả MCP schema cho tools — cám dỗ “vẽ schema theo cảm tính” là rất lớn. Kết quả dễ đoán:
- đổi trường ở backend, quên cập nhật schema của tool;
- mô hình tiếp tục gửi định dạng cũ;
- thu được “sở thú” runtime vui vẻ.
Lối đi đúng: tạo một điểm chân lý duy nhất cho cấu trúc dữ liệu và dùng nó ở mọi nơi. Với TypeScript, làm việc này rất tiện qua Zod hay thư viện tương tự mà MCP SDK có thể chuyển thành JSON Schema.
Zod schema dùng chung cho GiftGenius
Giả sử Gift service trong GiftGenius (bài học) đã dùng Zod để validate input:
// domain/gifts.ts
import { z } from "zod";
export const giftRecommendationInputSchema = z.object({
occasion: z.string().describe("Dịp: birthday, wedding, v.v."),
budgetUsd: z.number().int().positive(),
recipientProfile: z.string().describe("Mô tả ngắn về người nhận"),
});
export type GiftRecommendationInput = z.infer<
typeof giftRecommendationInputSchema
>;
Schema này đồng thời được dùng:
- ở REST endpoint (để kiểm tra body request);
- ở MCP tool (làm inputSchema);
- trong test (làm nền cho fixture).
Kết nối schema vào MCP tool
// mcp/tools/recommendGifts.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
import { server } from "../mcpServer";
server.registerTool({
name: "recommend_gifts",
description: "Chọn quà theo hồ sơ và ngân sách",
inputSchema: giftRecommendationInputSchema,
async execute(args) {
const input = giftRecommendationInputSchema.parse(args);
const res = await fetch("https://api.myapp.com/gifts/recommendations", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(input),
});
return res.json();
},
});
SDK sẽ tự chuyển Zod schema thành JSON Schema mà ChatGPT thấy trong tools/list. Điều này giải quyết hai vấn đề ngay lập tức:
- kiểu của tham số tool và mã nguồn gắn chặt với nhau;
- khi đổi schema, trình biên dịch TypeScript sẽ buộc bạn cập nhật cả handler.
Kiểm thử hợp đồng cho MCP ↔ backend
Kiểm thử hợp đồng ở đây không đáng sợ — chỉ là vài kiểm tra rất thực tế.
Unit/contract test đơn giản có thể như sau:
// tests/mcp/recommendGifts.contract.test.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
test("ví dụ request phù hợp với schema của tool", () => {
const sample = {
occasion: "birthday",
budgetUsd: 150,
recipientProfile: "đồng nghiệp, thích đồ công nghệ",
};
expect(() => giftRecommendationInputSchema.parse(sample)).not.toThrow();
});
Test này không đảm bảo “thế giới hoàn hảo”, nhưng ít ra bắt được lệch pha giữa kỳ vọng backend và lớp MCP khi bạn đổi schema mà quên cập nhật fixture.
Tiếp đó cách làm này dễ mở rộng sang:
- phản hồi giả (mock) của API bên ngoài (Stripe, CMS);
- chạy MCP client đối với máy chủ MCP thật trong môi trường test.
4. Chiến lược phiên bản hóa tools và resources
Sớm muộn schema cũng thay đổi. Điều chính là đừng làm kiểu “tôi chỉ đổi tên trường, thì có gì sai”. Trong thế giới LLM, không chỉ build có thể hỏng, mà cả hành vi của mô hình: prompt cũ, hội thoại đã lưu và golden case vẫn mong hợp đồng cũ.
Thay đổi cộng thêm vs thay đổi phá vỡ (breaking)
Ta chia thay đổi thành hai loại.
Thay đổi cộng thêm — bạn thêm gì đó mà không làm ai hỏng:
- trường mới không bắt buộc trong phản hồi;
- tham số mới không bắt buộc kèm giá trị mặc định;
- giá trị enum bổ sung mà UI và mô hình có thể bỏ qua.
Ví dụ, bạn thêm trường phản hồi deliveryEstimateDays, nhưng widget cũ chỉ đơn giản bỏ qua. Điều này an toàn: schema có thể mở rộng, nhưng không ai buộc phải dùng.
Thay đổi phá vỡ (breaking) — bạn làm hỏng kỳ vọng hiện có:
- biến một trường thành bắt buộc, trước đây không có;
- đổi kiểu (string → object);
- đổi ý nghĩa tham số (ngân sách USD → ngân sách nội tệ) nhưng lại không đổi tên trường.
Những trường hợp này, con đường an toàn duy nhất — tạo phiên bản tool mới.
Mẫu Tool_v2
Mẫu kinh điển: bạn có recommend_gifts, muốn đổi schema lớn. Đừng động vào tool cũ, hãy tạo tool mới — recommend_gifts_v2.
// v1
const recommendGiftsInput_v1 = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
});
// v2: hỗ trợ đơn vị tiền và bộ lọc giao hàng
const recommendGiftsInput_v2 = z.object({
occasion: z.string(),
maxPrice: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
deliverByDate: z.string().optional(); // chuỗi ISO
});
server.registerTool({
name: "recommend_gifts",
description: "DEPRECATED: hãy dùng recommend_gifts_v2",
inputSchema: recommendGiftsInput_v1,
async execute(args) { /* logic cũ */ },
});
server.registerTool({
name: "recommend_gifts_v2",
description:
"Chọn quà theo ngân sách, đơn vị tiền và hạn giao",
inputSchema: recommendGiftsInput_v2,
async execute(args) { /* logic mới */ },
});
Mô hình và các prompt/agent cũ sẽ tiếp tục dùng recommend_gifts cho đến khi bạn cập nhật chúng. Các kịch bản mới bạn viết theo recommend_gifts_v2.
Sau thời gian di trú:
- golden case và agent đã chuyển sang v2;
- metric cho thấy v1 hầu như không còn được gọi;
bạn có thể bắt đầu thu hẹp v1 (ví dụ, trước tiên ẩn khỏi danh sách tools ở dev/staging, rồi — ở prod).
Phiên bản hóa resources
Không chỉ tools cần phiên bản. Nếu bạn có resources (resources) — ví dụ, catalog quà tặng tĩnh — chúng cũng nên được phiên bản hóa.
Các cách phổ biến:
- nhúng version vào tên resource: gift_catalog.v1.json, gift_catalog.v2.json;
- hoặc truyền version trong URI/tham số: /api/catalog?version=1.
Ý nghĩa tương tự: đừng thay dữ liệu “dưới chân” các kịch bản đang chạy, mà cung cấp rõ ràng phiên bản catalog cố định cho chúng.
Di trú không downtime
Chu trình điển hình khi di trú tool:
- Thêm phiên bản tool mới (_v2) song song bản cũ.
- Cập nhật App/agent/system prompt để dùng phiên bản mới.
- Chạy golden case và LLM eval cho cả hai biến thể và đảm bảo chất lượng với kịch bản trọng yếu không giảm.
- Quan sát metric sử dụng v1 so với v2 (và lỗi).
- Sau khi traffic vào v1 gần bằng 0, bắt đầu vô hiệu hóa nó.
Cách làm này hiệu quả cả cho di trú schema, cập nhật SDK/protocol, và thay đổi Auth. Chúng ta đã hiểu cách tools và resources tiến hóa — thông qua v1/v2 và thay đổi cộng thêm thận trọng. Phần lớn thứ hai của hợp đồng là xác thực và phân quyền: OAuth, scopes và .well-known. Chúng cũng sống nhiều năm và cần di trú cẩn thận.
5. Tiến hóa xác thực: .well-known, scopes và OAuth hiện có
Nếu sản phẩm của bạn đã sống trong thế giới OAuth 2.1/OpenID Connect, tích hợp với ChatGPT qua MCP không phải “thêm một đăng nhập”, mà là một client mới cần nói chuyện với Authorization Server theo luật chung của bạn.
MCP và /.well-known/oauth-protected-resource
Về đầy đủ OAuth 2.1/OpenID Connect và cấu hình Auth Server, chúng ta nói kỹ trong mô-đun riêng (xem mô-đun về xác thực). Ở đây chỉ quan tâm khía cạnh thực dụng: MCP resource báo cho ChatGPT biết nó được bảo vệ bởi OAuth ra sao, và cách khởi chạy linking flow.
Mẫu chuẩn cho MCP resource được bảo vệ:
- máy chủ MCP của bạn phơi bày endpoint đặc biệt /.well-known/oauth-protected-resource;
- trong phản hồi, nó nói resource nào và được bảo vệ bởi những AS (Authorization Server) nào;
- khi 401 trên lời gọi MCP, máy chủ trả header WWW-Authenticate với link đến .well-known này, và ChatGPT tự khởi chạy OAuth flow (“Link account”).
Ví dụ tối thiểu với Express:
// mcp-auth/.well-known.ts
import express from "express";
const app = express();
app.get("/.well-known/oauth-protected-resource", (_req, res) => {
res.json({
resource: "https://mcp.myapp.com",
authorization_servers: [
"https://auth.myapp.com/.well-known/openid-configuration",
],
});
});
app.listen(3000);
Và handler 401 với gợi ý cho client:
res
.status(401)
.set(
"WWW-Authenticate",
'Bearer resource_metadata="https://mcp.myapp.com/.well-known/oauth-protected-resource"',
)
.end();
ChatGPT, khi thấy header này, sẽ hiểu cần đến AS nào và cách khởi chạy OAuth flow cho MCP resource của bạn.
Scopes và di trú phân quyền
Scopes cũng là nguồn di trú. Chúng ta đã bàn trong mô-đun Auth, nhưng trong bối cảnh tích hợp/di trú có vài điểm quan trọng.
Hãy hình dung GiftGenius ban đầu chỉ đọc catalog (gifts.read), sau bạn thêm gifts.write để tạo đơn hàng. Bạn cần:
- thêm scope mới vào cấu hình client (ChatGPT App);
- cập nhật máy chủ MCP để chỉ yêu cầu scope này với các tool thực sự có tác dụng thay đổi;
- mô tả thay đổi trong .well-known nếu cần.
Về UX, người dùng khi lần sau dùng tính năng mới có thể thấy yêu cầu “mở rộng quyền” cho ứng dụng ChatGPT. Bạn không muốn việc này xảy ra giữa một cuộc hội thoại mà không báo trước — nên các thay đổi như vậy cần:
- được thông báo (release notes, tài liệu);
- được thử trên staging với AS thử nghiệm;
- đi kèm cập nhật mô tả tool (destructiveHint, v.v.) để mô hình gọi các tool “nguy hiểm” một cách có ý thức.
6. Metadata và annotation: lớp hint trên hợp đồng
Lớp Auth trả lời câu hỏi ai có thể làm gì qua App của bạn. Nhưng ngay cả khi token và scope đều đúng, điều quan trọng là mô hình sẽ gọi tool của bạn như thế nào và giải thích hành động cho người dùng ra sao. Ở đây lớp hint bổ sung xuất hiện: metadata và annotation.
Hợp đồng (schema) nói tool nhận và trả gì. Metadata và annotation giúp mô hình hiểu khi nào và cách gọi. Điều này đặc biệt quan trọng khi App tiến hóa: thêm hành động destructive, đổi UI, đưa tích hợp với thế giới bên ngoài.
_meta["openai/widgetDescription"] và widgetCSP
Trong Apps SDK và mô tả MCP có trường đặc biệt _meta, nơi OpenAI thêm các mở rộng giao thức. Ví dụ:
- _meta["openai/widgetDescription"] — mô tả ngắn widget của bạn hiển thị gì; mô hình có thể dùng để không “kể lại” UI và giới thiệu App đúng cách;
- _meta["openai/widgetCSP"] — khai báo các domain CSP mà widget của bạn cần (cho fetch/hình ảnh/script).
Khi bạn đổi UI (ví dụ thêm bước đặt hàng mới), nên cập nhật widgetDescription để mô hình vẫn giải thích đúng cho người dùng điều gì đang diễn ra.
Annotation của tool (readOnlyHint, destructiveHint, openWorldHint)
Annotation — các cờ boolean đơn giản — nhưng ảnh hưởng đáng kể tới UX và an toàn:
- readOnlyHint: true — tool chỉ đọc, không thay đổi gì. Mô hình có thể gọi mà không cần xác nhận thêm.
- destructiveHint: true — tool có thể xóa/thay đổi gì đó. ChatGPT sẽ yêu cầu xác nhận rõ ràng.
- openWorldHint: true — tool xuất dữ liệu ra ngoài hoặc có thể trả về “rất nhiều thứ”, đòi hỏi tóm tắt.
Ví dụ mô tả tool có annotation:
server.registerTool({
name: "delete_saved_gift",
description: "Xóa món quà đã lưu của người dùng",
inputSchema: z.object({ giftId: z.string() }),
annotations: {
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,
},
async execute({ giftId }) {
// ...xóa quà
},
});
Khi di trú, lúc bạn thêm các tool “nguy hiểm”, annotation là bạn đồng hành: chúng giúp ChatGPT không thực hiện âm thầm và hướng tới hành vi cẩn trọng hơn.
Quan trọng: annotation không phải lớp bảo vệ “thực sự”. Chúng chỉ ảnh hưởng client và mô hình. Bảo mật thật sự vẫn do máy chủ của bạn đảm bảo (Auth, scopes, validation).
7. Di trú SDK và đặc tả MCP
MCP và Apps SDK phát triển nhanh — xuất hiện các trường mới trong capabilities, kiểu message mới, _meta/annotations mới. Tài liệu nói rõ: “tính đến 2025” — và chúng ta phải sống chung với điều đó.
Vì vậy di trú phiên bản SDK và spec là phần bình thường trong vòng đời App, không phải sự kiện hiếm “để sau”.
Quy trình nâng cấp điển hình
Kịch bản lành mạnh khi nâng cấp:
- Đọc changelog phiên bản Apps SDK/MCP SDK mới. Ghi lại mọi thay đổi có thể breaking.
- Cập nhật dependency ở môi trường dev/staging, chưa đụng prod.
- Chạy MCP Inspector / Jam hoặc client khác:
- kiểm tra handshake;
- tools/list / resources/list;
- một vài tools/call thử nghiệm.
- Cập nhật mô tả tool và _meta theo khả năng mới:
- ví dụ, thêm annotations mới hoặc widgetDescription.
- Chạy golden case và LLM eval như đã nói ở bài trước để chắc chắn hành vi App về mặt chất lượng không xấu đi.
- Chỉ sau đó mới triển khai lên prod, nếu có thể dùng canary/feature flag cho một phần traffic.
Ví dụ: thêm openWorldHint trong bản SDK mới
Giả sử bản Apps SDK mới hỗ trợ openWorldHint, và bạn muốn gắn nó cho tool search_public_reviews, vốn truy tìm các đánh giá bên ngoài và có thể trả nhiều “nhiễu”.
Các bước sẽ như sau:
- cập nhật SDK và kiểu;
- thêm annotations.openWorldHint = true trong mô tả tool;
- cập nhật system prompt, để agent giải thích rõ ràng với người dùng rằng sẽ có yêu cầu ra thế giới bên ngoài;
- chạy safety golden case (đặc biệt các câu về riêng tư/PII) để đảm bảo mô hình không trở nên quá “nhiều lời”.
Chúng ta đã bàn quy trình cập nhật SDK và annotation. Giờ hãy nhìn mọi thứ trong một kịch bản cụ thể — tiến hóa tool recommend_gifts.
8. Mini-case: tiến hóa recommend_gifts trong GiftGenius
Hãy ráp mọi thứ lại trong một kịch bản cụ thể.
Phiên bản ban đầu
Tool cơ bản như sau:
const recommendGiftsInput_v1 = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
recipientProfile: z.string(),
});
server.registerTool({
name: "recommend_gifts",
description: "Gợi ý ý tưởng quà tặng bằng USD",
inputSchema: recommendGiftsInput_v1,
async execute(args) {
const input = recommendGiftsInput_v1.parse(args);
return giftService.recommend(input); // hàm nội bộ
},
});
Mọi thứ ổn nếu bạn chỉ có người dùng ở Mỹ và một loại tiền tệ.
Yêu cầu mới: đa tiền tệ và hạn giao
Nhóm sản phẩm đưa yêu cầu mới:
- cần hỗ trợ EUR/GBP;
- cần tính hạn giao (không hiển thị quà giao sau một tháng nếu sinh nhật sau ba ngày);
- tốt nhất thêm ước lượng thời gian giao trong phản hồi.
Cách ngây thơ: chỉ đổi trường:
- đổi tên budgetUsd thành maxPrice;
- thêm currency;
- thêm deliveryEstimateDays vào phản hồi.
Điều gì sẽ sai?
Prompt cũ (kể cả golden case và mô tả trong system prompt) và hội thoại đã lưu vẫn gửi budgetUsd. Mô hình không biết trường đó biến mất. Lớp MCP bắt đầu lỗi khi parse. Hành vi ChatGPT App đột ngột hỏng với người dùng thật.
Con đường đúng:
- Thêm schema mới và tool mới _v2.
const recommendGiftsInput_v2 = z.object({
occasion: z.string(),
maxPrice: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
recipientProfile: z.string(),
deliverByDate: z.string().optional(),
});
server.registerTool({
name: "recommend_gifts_v2",
description:
"Chọn quà có xét đơn vị tiền và hạn mong muốn",
inputSchema: recommendGiftsInput_v2,
async execute(args) {
const input = recommendGiftsInput_v2.parse(args);
return giftService.recommendV2(input); // logic mới
},
});
- Giữ recommend_gifts như cũ, thêm nhãn description là DEPRECATED.
- Cập nhật system prompt và mô tả App để mô hình ưu tiên recommend_gifts_v2 (có thể nêu rõ trong hướng dẫn).
- Cập nhật widget GiftGenius để hiểu định dạng phản hồi mới: trường deliveryEstimateDays, v.v.
- Chạy golden case cho các kịch bản điển hình (chọn quà trước ngày nhất định) qua LLM eval.
Kiểm thử và khả năng quan sát
Một vài test nên có:
Contract test cho input mới:
test("v2 chấp nhận kịch bản với EUR và hạn giao", () => {
const sample = {
occasion: "birthday",
maxPrice: 100,
currency: "EUR",
recipientProfile: "đồng nghiệp",
deliverByDate: "2025-12-24",
};
expect(() => recommendGiftsInput_v2.parse(sample)).not.toThrow();
});
Quan sát ở prod:
- tỷ lệ gọi recommend_gifts_v2 so với recommend_gifts;
- error rate của v1 (kỳ vọng không tăng);
- điểm LLM eval theo golden case trước/sau di trú (bạn đã biết cách làm từ các bài trước).
Khi v2 “thắng” cả về chất lượng lẫn metric sử dụng, có thể lên kế hoạch vô hiệu hóa v1.
Nếu rút gọn thành ba ý: (1) MCP là adapter mỏng, không phải monolith mới; (2) schema, auth và annotation là hợp đồng sống lâu giữa ChatGPT và backend của bạn, cần phiên bản hóa và kiểm thử nghiêm túc như API thông thường; (3) mọi di trú SDK/spec là quy trình kỹ thuật bình thường với staging, golden case và khả năng quan sát — không phải “cập nhật package tối thứ Sáu”. Nếu nhìn ChatGPT App qua lăng kính này, tích hợp với sản phẩm hiện hữu sẽ bớt hỗn loạn.
9. Lỗi điển hình khi tích hợp và di trú MCP/SDK
Lỗi số 1: Xem MCP như “backend mới” thay vì adapter mỏng.
Đôi khi muốn kéo toàn bộ business logic vào lớp MCP: truy vấn DB, luật miền, tính toán. Điều này biến máy chủ MCP thành một monolith nữa, khó đồng bộ với backend còn lại. Lành mạnh hơn là giữ MCP như Gateway/Adapter trên các dịch vụ hiện có: toàn bộ logic miền vẫn sống ở nơi vốn dĩ, còn MCP chỉ chuyển JSON qua lại.
Lỗi số 2: Nhiều schema cho cùng một đối tượng.
Anti-pattern phổ biến — có ba định nghĩa “quà tặng”: một trong DB, một trong REST API, một trong MCP tool, và mỗi cái hơi khác. Hệ quả: type tĩnh hỏng, hợp đồng gãy, test rối và lý trí lung lay. Dùng một schema thống nhất (Zod/TypeBox, v.v.) làm Single Source of Truth và sinh JSON Schema cho MCP sẽ giảm rủi ro này rất nhiều.
Lỗi số 3: Di trú schema sai cách — breaking change “thầm lặng”.
Đổi tên trường hoặc thay đổi ý nghĩa mà không đổi tên tool — dẫn tới hồi quy ẩn. Mô hình vẫn gửi định dạng cũ, sự cố chỉ lộ với một phần người dùng và khá trễ. Với thay đổi lớn, hãy tạo *_v2, để bản cũ chạy song song, dùng nhãn deprecation và giám sát.
Lỗi số 4: Bỏ qua thay đổi Auth và scopes.
Thêm tool mới có tác dụng phụ nhưng quên cập nhật scopes và .well-known? Người dùng có thể gặp 401 giữa kịch bản, hoặc ngược lại, MCP của bạn thực thi thao tác destructive mà không phân quyền hợp lý. Hãy lên kế hoạch di trú auth cẩn thận như di trú schema: qua staging, test và mở rộng quyền dần.
Lỗi số 5: Không dùng annotation (destructiveHint, readOnlyHint, openWorldHint).
Nếu không gợi ý mô hình tool nào an toàn, tool nào tiềm ẩn rủi ro, nó có thể hành xử khó lường: hỏi xác nhận cho get_catalog vô hại nhưng lại xóa dữ liệu không báo trước. Annotation đúng đắn giúp hành vi dự đoán được và giảm rủi ro về chất lượng/an toàn.
Lỗi số 6: Cập nhật SDK “ở prod” mà không chạy golden case.
Bản SDK/spec mới có thể thêm trường, đổi hành vi handshake hoặc cấu trúc message. Nếu chỉ “cập nhật dependency và deploy”, bạn có nguy cơ gặp hồi quy chất lượng (mô hình không còn gọi đúng tool, thay đổi cách diễn đạt lỗi, v.v.). Trình tự đúng: dev/staging, MCP Inspector, sau đó golden case và LLM eval, rồi mới tới prod.
Lỗi số 7: Gắn chặt business logic vào một phiên bản tool.
Khi logic nội bộ của Gift Service phụ thuộc trực tiếp vào recommend_gifts cụ thể, việc di trú sang recommend_gifts_v2 sẽ đau đớn. Thực hành tốt: có service nội bộ tiến hóa theo quy tắc riêng, còn các tool *_v1, *_v2 chỉ là adapter mỏng, ánh xạ hợp đồng bên ngoài cũ/mới vào cấu trúc miền chung.
Lỗi số 8: Thiếu khả năng quan sát theo phiên bản tool.
Nếu log và metric không phân biệt tool nào và phiên bản nào được gọi, gỡ lỗi di trú thành đoán mò. Hãy log tên tool, phiên bản schema/SDK và tham số chính — khi đó mọi hồi quy dễ gắn với thay đổi cụ thể.
GO TO FULL VERSION