CodeGym /Các khóa học /ChatGPT Apps /Mô hình sự kiện trong MCP: các loại thông báo, định dạng ...

Mô hình sự kiện trong MCP: các loại thông báo, định dạng thông điệp, tính idempotent

ChatGPT Apps
Mức độ , Bài học
Có sẵn

1. Tại sao chúng ta cần sự kiện MCP

Cho đến giờ, hầu như mọi giao tiếp giữa ChatGPT và backend của bạn trông như RPC: mô hình gọi một công cụ, công cụ làm gì đó, trả về kết quả — xong. Điều này tiện khi thao tác ngắn: 200–500 ms, nhiều nhất là vài giây.

Nhưng ngay khi xuất hiện tác vụ chạy lâu — phân tích tệp lớn với sở thích nhân viên cho GiftGenius, tổng hợp gợi ý từ hàng loạt API bên ngoài, tính toán lại một feed lớn — mọi thứ trở nên khó chịu. Timeout HTTP, khởi động lại hàm, spinner “vĩnh cửu”, còn người dùng thì ngồi đoán: “nó còn sống hay đã chết?”.

Đây chính là lúc mô hình sự kiện phát huy tác dụng. Thay vì giữ một cuộc gọi công cụ dài, bạn khởi chạy một tác vụ, nhận jobId, và sau đó server chủ động gửi sự kiện: đã bắt đầu, đang tiến độ, đã xong, bị lỗi. Các sự kiện này trong MCP được hiện thực như JSON‑RPC notifications — thông điệp một chiều không có id, không kỳ vọng phản hồi.

Cần hiểu rõ: sự kiện không phải “console.log trên dây”. Nó là thông điệp chính tắc của giao thức với schema xác định, và UI (widget) và/hoặc agent của bạn phải xử lý nó kỷ luật như với kết quả của một lần gọi công cụ.

Nhắc lại: các loại thông điệp trong MCP

Trước khi đi tiếp, hãy ôn nhanh xem trong MCP có những loại thông điệp nào.

Nếu bỏ qua mọi lớp “marketing”, MCP dựa trên JSON‑RPC 2.0. Ở đó có ba loại thông điệp cơ bản: request, response và notification.

Thay vì liệt kê, hãy xem một bảng so sánh nhỏ:

Loại Trường id Bên khởi tạo Có chờ phản hồi không? Ví dụ trong MCP
Request Thường là client (ChatGPT) Gọi công cụ tools/call
Response Máy chủ MCP Chính là phản hồi Kết quả của tools/call
Notification không Client hoặc server Không notifications/progress, resources/updated, logging/message

Sự kiện MCP nằm chính ở hàng thứ ba: notifications. Dấu hiệu đặc trưng:

  • không có id ở cấp cao nhất — sẽ không có result hay error trả về;
  • bên khởi tạo không chờ ACK — “bắn rồi quên” ở mức giao thức;
  • độ tin cậy không xây bằng xác nhận, mà bằng tính idempotent của các handler và chính sách gửi lại.

Hạn chế quan trọng: sự kiện MCP không “bay ở đâu đó ngẫu nhiên”. Chúng tồn tại bên trong một kết nối MCP đã được thiết lập trên một transport cụ thể. Thường đây là một luồng kiểu SSE (chi tiết về transport và biến thể của nó sẽ được phân tích trong một bài giảng riêng).

2. Sự kiện MCP là gì trên thực tế

Về mặt hình thức, sự kiện MCP là một JSON‑RPC notification, tức là đối tượng có dạng:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "jobId": "job_123",
    "percentage": 30,
    "stage": "Đang tìm các lựa chọn trong danh mục",
    "eventId": "evt_abc123",
    "timestamp": "2025-11-21T10:15:00Z"
  }
}

Ở đây có vài điểm quan trọng:

  1. Trong trường method chúng ta mã hóa loại sự kiện và “không gian tên” của nó. MCP đã xác định một số phương thức chuẩn dạng notifications/... cho log, tiến độ và thay đổi tài nguyên, nhưng bạn có thể và nên thêm các phương thức đặc thù nghiệp vụ của mình, như notifications/job/progress hoặc notifications/job/completed.
  2. Mọi dữ liệu nghiệp vụ nằm trong params. Ở đó chúng ta sẽ lưu định danh tác vụ (jobId), id sự kiện duy nhất (eventId), thời gian (timestamp), thông điệp dễ đọc, v.v.
  3. Trường id ở cấp cao nhất không tồn tại — đó là lý do đây là notification. Giao thức không dự liệu phản hồi cho nó. Nếu server muốn biết “bên kia đã hiểu chưa”, nó có thể gửi một sự kiện nữa hoặc chờ hành vi phản ứng từ client (ví dụ, một request mới). Nhưng không có ACK theo nghĩa JSON‑RPC.

Ở mức mô hình tinh thần, có thể nghĩ thế này: gọi công cụ tools/call là “bức thư mà bạn chờ phản hồi”, còn sự kiện là “thông báo từ Slack bot: “Tác vụ nền #123 đã hoàn tất””.

3. Phân loại sự kiện: có những thông báo nào

Nếu chỉ cho phép “gửi bất kỳ JSON nào như notifications”, sau hai tuần hệ thống sẽ thành bãi rác: tên sự kiện lộn xộn, trường lúc có lúc không, UI không hiểu phải làm gì. Vì vậy hữu ích khi thống nhất một taxonomy nhỏ.

Dưới đây là một phương án phân loại tiện dụng, khớp tốt với đặc tả MCP và các tình huống thực tế của ChatGPT Apps.

Sự kiện vòng đời tác vụ (Job Lifecycle)

Đây là các sự kiện phản ánh những chuyển trạng thái then chốt của tác vụ. Thông thường tác vụ có một state machine như pendingrunning → (completed | failed | canceled).

Các sự kiện điển hình:

  • job.created — tác vụ đã được đăng ký;
  • job.started — worker đã bắt đầu thực thi;
  • job.completed — tác vụ đã hoàn tất thành công;
  • job.failed — tác vụ thất bại với lỗi;
  • job.canceled — tác vụ đã bị người dùng hủy.

Ví dụ job.completed cho GiftGenius:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/completed",
  "params": {
    "eventId": "evt_gg_100",
    "jobId": "giftjob_42",
    "timestamp": "2025-11-21T10:20:00Z",
    "summary": "Hoàn tất chọn quà",
    "resultResourceId": "resource:gifts:giftjob_42"
  }
}

Ở đây resultResourceId có thể trỏ tới một MCP resource mà widget hoặc agent sẽ đọc sau đó.

Sự kiện tiến độ (Progress Updates)

Đây là “những bước nhỏ” bên trong vòng đời: chúng không thay đổi trạng thái cuối cùng, nhưng cho người dùng cảm giác mọi thứ đang diễn ra.

Sự kiện điển hình job.progress:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "eventId": "evt_gg_101",
    "jobId": "giftjob_42",
    "timestamp": "2025-11-21T10:18:30Z",
    "percentage": 40,
    "stage": "Lọc quà theo ngân sách",
    "etaSeconds": 25
  }
}

Ở đây quan trọng là percentage nên tăng hợp lý về phía 100, không nhảy tới lui. Hãy chọn một tên trường tiến độ duy nhất (ví dụ, percentage) và dùng nó cho mọi sự kiện. Trong tiện ích tiến độ chính thức của MCP cũng có quy tắc: tiến độ chỉ tăng.

Sự kiện cập nhật dữ liệu (Resource/Data events)

Đôi khi người dùng còn không quan tâm jobId cụ thể. Quan trọng là có một thực thể đã thay đổi: feed sản phẩm được cập nhật, có snapshot báo cáo mới, hồ sơ cá nhân được tái tạo.

Trong MCP đã có các notification tiêu chuẩn như resources/updated, resources/list_changed và tương tự, báo cho client: “hãy đọc lại danh sách resource, có điều gì đó đã thay đổi”.

Với GiftGenius, điều này có thể như sau:

{
  "jsonrpc": "2.0",
  "method": "resources/updated",
  "params": {
    "eventId": "evt_feed_17",
    "timestamp": "2025-11-21T09:00:00Z",
    "resourceId": "resource:product-feed",
    "changeType": "snapshot_ready"
  }
}

Widget, khi nhận sự kiện như vậy, có thể, chẳng hạn, làm nổi bật nút “Cập nhật danh sách quà”.

Sự kiện UX và hệ thống

Còn có các sự kiện không thuần nghiệp vụ, nhưng quan trọng cho UX hoặc chẩn đoán:

  • thông điệp log logging/message — notification MCP chuẩn dành cho log;
  • heartbeat/ping — tín hiệu “tôi còn sống” định kỳ từ server;
  • cảnh báo suy giảm: ví dụ, “API bên ngoài đang chậm, kết quả có thể đến muộn hơn”.

Những sự kiện như vậy hữu ích cho giám sát và gỡ lỗi; đôi khi có thể “làm dịu” chúng trong UI, cho người dùng thấy hệ thống chưa chết mà chỉ đang bận.

4. Cấu trúc sự kiện: trường bắt buộc và payload

Sự kiện cũng là một đối tượng API như request của công cụ. Nó cần được thiết kế. Một thói quen tốt là thỏa thuận về bộ trường cơ bản.

Về khái niệm, hữu ích khi chia sự kiện thành ba phần: metadata, correlation và payload.

Ví dụ dạng chung:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "eventId": "evt_gg_103",
    "type": "job.progress",
    "timestamp": "2025-11-21T10:19:00Z",
    "jobId": "giftjob_42",
    "payload": {
      "percentage": 60,
      "stage": "So sánh đánh giá",
      "etaSeconds": 15
    }
  }
}

Trong cấu trúc này có thể phân tách:

  • eventId — định danh duy nhất của sự kiện. Cần để khử trùng lặp ở phía client;
  • type — tên logic của sự kiện (có thể trùng/chuẩn hóa method);
  • timestamp — thời điểm server tạo ra sự kiện;
  • jobId hoặc một correlation‑id khác — để hiểu sự kiện liên quan tới cái gì;
  • payload — dữ liệu chính. Với mỗi loại sự kiện sẽ có dạng riêng.

Trong hệ thống thực, gần như chắc chắn bạn sẽ muốn mô tả chính thức những cấu trúc này qua JSON Schema hoặc ít nhất là kiểu TypeScript, để cả server và client cùng xác thực thông điệp. Một số đội dùng định dạng lấy cảm hứng từ CloudEvents: ở đó cũng có các trường chuẩn như id, source, type, time, v.v.

Nhưng ý tưởng then chốt rất đơn giản: sự kiện phải có thể đọc bằng máy và nhất quán — không có bất ngờ kiểu “đôi khi trường tên là jobId, đôi khi là job_id, đôi khi lại không có”.

Trong các ví dụ dưới đây, để không quá nặng, chúng ta sẽ thường dùng biến thể “phẳng”: mọi dữ liệu sự kiện nằm trực tiếp trong params mà không có payload lồng, và đôi khi bỏ qua trường type nếu vai trò của nó đã do method đảm nhiệm. Nguyên tắc vẫn giữ nguyên: mỗi sự kiện có metadata ổn định (eventId, jobId, timestamp) và payload có thể dự đoán.

5. Tính idempotent của sự kiện: để làm gì và làm thế nào

Giờ đến từ khóa quan trọng nhất của bài giảng — tính idempotent.

Tính idempotent của handler sự kiện có nghĩa là nếu cùng một sự kiện được xử lý một lần hay mười lần, trạng thái cuối cùng của hệ thống vẫn đúng. Trong các hệ phân tán có mạng và retry, đây là vấn đề sống còn.

Tại sao cùng một sự kiện có thể đến nhiều lần?

Có nhiều lý do: từ đứt kết nối và kết nối lại, đến các lần retry phía server, nơi “cho chắc” nó gửi notification thêm lần nữa. Khi dùng giao thức dạng streaming (ví dụ server tự đẩy sự kiện trên một kết nối mở, như SSE — chi tiết sẽ ở bài về transport), đây là chuyện kinh điển: client kết nối lại với Last-Event-ID, server sẽ gửi bù các sự kiện bị lỡ, và một số trong đó client sẽ thấy lần thứ hai.

Nếu handler của bạn không idempotent, những điều kỳ lạ sẽ xảy ra:

  • sự kiện job.completed khiến cộng thưởng hai lần hoặc đổi trạng thái đơn hàng hai lần;
  • sự kiện resource.updated bắt widget mỗi lần “thêm” thẻ, tạo bản sao trong UI;
  • các job.progress lặp lại làm người dùng hoang mang nếu thanh tiến độ bắt đầu nhảy tới lui.

Chiến lược đúng hoạt động theo hai lớp: phát sinh sự kiện ở server và xử lý ở client.

Phía server: id ổn định và state machine

Server cần:

  • sinh eventId duy nhất cho mỗi sự kiện logic;
  • đảm bảo các sự kiện của một jobId tạo thành chuỗi trạng thái hợp lệ: bạn không thể gửi job.failed sau job.completed hoặc hai job.completed khác nhau với các kết quả khác nhau.

Tức là thực chất bạn có một state machine cho tác vụ, và mỗi sự kiện là một chuyển trạng thái được phép.

Phía client: khử trùng lặp và cập nhật “nhẹ nhàng”

Client (widget, agent hoặc thành phần khác) cần:

  • lưu tập hợp các eventId đã xử lý ít nhất trong thời gian sống của kết nối/phiên hiện tại;
  • trước khi xử lý, kiểm tra: nếu eventId đã thấy, chỉ cần bỏ qua hoặc vẽ lại UI mà không gây hiệu ứng phụ;
  • khi nhận sự kiện thay đổi trạng thái tác vụ (job.completed, job.failed), chắc chắn rằng chuyển trạng thái là hợp lệ: ví dụ, nếu tác vụ đã được đánh dấu completed, một job.completed lặp lại không được thay đổi gì, còn failed thì tốt nhất bỏ qua vì không hợp lệ.

Ví dụ kinh điển trong thế giới thương mại: xử lý webhook xác nhận thanh toán. Cùng một order.paid có thể đến hai lần; vì vậy backend lưu paymentId và cờ “đã ghi có”. Dù webhook tới lần thứ hai, trạng thái đơn hàng vẫn không đổi. Sự kiện MCP nên được thiết kế với tư duy tương tự.

6. Ví dụ: thiết kế sự kiện cho GiftGenius

Hãy áp dụng vào GiftGenius của chúng ta. Giả sử kịch bản dài: người dùng tải lên một CSV lớn với danh sách nhân viên và sở thích của họ, yêu cầu “gợi ý quà cho tất cả”. Thao tác có thể mất hàng chục giây.

Một mô hình sự kiện hợp lý có thể mô tả như sau:

  1. Người dùng chạy công cụ start_bulk_gift_analysis. Tool trả về jobId: "bulk_2025_001".
  2. Máy chủ MCP tạo tác vụ và gần như ngay lập tức gửi job.started với mô tả ngắn.
  3. Trong quá trình, nó gửi vài job.progress với các giai đoạn:
    • 10% — “Phân tích cú pháp tệp và kiểm tra định dạng”;
    • 40% — “Trích xuất sở thích và phòng ban”;
    • 70% — “Đối sánh quà theo danh mục”;
    • 100% — ngay trước khi hoàn tất.
  4. Cuối cùng đến job.completed với liên kết tới resource chứa khuyến nghị cuối cùng.
  5. Nếu có trục trặc — thay vì completed sẽ là job.failed với mã lỗi và có thể là gợi ý cần sửa gì.

Phi chính thức là như vậy, nhưng hãy cố định lại dưới dạng JSON Schema cho hai sự kiện then chốt job.progressjob.completed. Pseudo‑JSON Schema (đơn giản hóa):

{
  "job.progress": {
    "type": "object",
    "properties": {
      "eventId": { "type": "string" },
      "jobId": { "type": "string" },
      "timestamp": { "type": "string", "format": "date-time" },
      "percentage": { "type": "number", "minimum": 0, "maximum": 100 },
      "stage": { "type": "string" },
      "etaSeconds": { "type": "number" }
    },
    "required": ["eventId", "jobId", "timestamp", "percentage", "stage"]
  }
}
{
  "job.completed": {
    "type": "object",
    "properties": {
      "eventId": { "type": "string" },
      "jobId": { "type": "string" },
      "timestamp": { "type": "string", "format": "date-time" },
      "summary": { "type": "string" },
      "resultResourceId": { "type": "string" }
    },
    "required": ["eventId", "jobId", "timestamp", "resultResourceId"]
  }
}

Bạn không bắt buộc phải hiện thực kiểm tra schema đầy đủ ngay bây giờ, nhưng giữ cấu trúc như vậy trong đầu rất hữu ích: nó giúp tránh “trải” các trường ra nhiều định dạng khác nhau và không quên metadata quan trọng.

7. Mini thực hành: server gửi MCP‑sự kiện

Giờ kết hợp lý thuyết với một đoạn pseudo‑code TypeScript nhỏ. Chúng ta không đi sâu vào các thư viện MCP thực (thứ nhất là chúng vẫn đang phát triển, thứ hai là trọng tâm nằm ở mô hình), mà sẽ phác một bộ khung cấu trúc.

Giả sử trong MCP server của chúng ta có một abstraction sendNotification để gửi JSON‑RPC notification ngược về ChatGPT. Pseudo‑interface:

// Tiện ích để gửi MCP notification
async function sendNotification(
  method: string,
  params: Record<string, unknown>
) {
  // Tại đây bạn sẽ tuần tự hóa JSON và gửi qua kết nối MCP đang hoạt động
}

Giờ hiện thực handler cho công cụ start_bulk_gift_analysis. Nó đăng ký tác vụ, trả về jobId, và đâu đó “tick” ở nền để gửi tiến độ. Trong thực tế đây sẽ là worker và hàng đợi, nhưng tạm thời dùng timer.

type Job = {
  id: string;
  status: "pending" | "running" | "completed" | "failed";
};

const jobs = new Map<string, Job>();

export async function startBulkGiftAnalysisTool() {
  const jobId = `bulk_${Date.now()}`;
  jobs.set(jobId, { id: jobId, status: "pending" });

  // Gửi ngay job.started
  await sendNotification("notifications/job/started", {
    eventId: `evt_${jobId}_started`,
    jobId,
    timestamp: new Date().toISOString(),
    summary: "Đã khởi chạy phân tích danh sách quà tặng lớn"
  });

  simulateJob(jobId); // "khởi chạy" tác vụ ở nền

  return { jobId };
}

Mô phỏng tác vụ:

async function simulateJob(jobId: string) {
  jobs.set(jobId, { id: jobId, status: "running" });

  const stages = [
    { percent: 10, stage: "Phân tích cú pháp CSV" },
    { percent: 40, stage: "Phân tích sở thích" },
    { percent: 70, stage: "Chọn quà" },
    { percent: 100, stage: "Tạo kết quả" }
  ];

  for (const s of stages) {
    await sendNotification("notifications/job/progress", {
      eventId: `evt_${jobId}_${s.percent}`,
      jobId,
      timestamp: new Date().toISOString(),
      percentage: s.percent,
      stage: s.stage
    });
    await new Promise(r => setTimeout(r, 1000));
  }

  jobs.set(jobId, { id: jobId, status: "completed" });

  await sendNotification("notifications/job/completed", {
    eventId: `evt_${jobId}_done`,
    jobId,
    timestamp: new Date().toISOString(),
    summary: "Phân tích quà tặng đã hoàn tất",
    resultResourceId: `resource:gifts:${jobId}`
  });
}

Mã cố ý đơn giản, nhưng ở đây thấy rõ:

  • chúng ta dùng chuỗi sự kiện startedprogress* → completed;
  • mỗi sự kiện nhận một eventId duy nhất;
  • mọi sự kiện đều gắn với cùng một jobId.

Về sau, khi bạn thêm hàng đợi và worker thực, cấu trúc sự kiện vẫn gần như vậy — chỉ thay đổi nơi gọi sendNotification.

8. Client: handler sự kiện idempotent đơn giản nhất

Ở phía client (ví dụ, trong widget Apps SDK của bạn) cần biết cách nhận các sự kiện như vậy, liên kết chúng với các tác vụ hiện có và không phát điên vì bản sao.

Chưa bàn sâu transport (sau này), hãy hình dung một hàm onMcpNotification mà lớp MCP‑client của bạn gọi cho mỗi notification đến.

Thêm khử trùng lặp đơn giản:

const processedEvents = new Set<string>();

function handleNotification(method: string, params: any) {
  const eventId = params.eventId as string | undefined;
  if (!eventId) return; // khá tranh luận, nhưng tạm chấp nhận cho ví dụ

  if (processedEvents.has(eventId)) {
    // Lặp lại — bỏ qua hoặc cập nhật UI nhẹ nhàng
    return;
  }
  processedEvents.add(eventId);

  if (method === "notifications/job/progress") {
    updateJobProgress(params.jobId, params.percentage, params.stage);
  } else if (method === "notifications/job/completed") {
    markJobCompleted(params.jobId, params.resultResourceId);
  }
}

Việc hiện thực updateJobProgressmarkJobCompleted — đó là phần code React/UI thuần:

function updateJobProgress(jobId: string, percent: number, stage: string) {
  // ví dụ, đưa vào state của Zustand/Redux/React
  console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}

function markJobCompleted(jobId: string, resourceId: string) {
  console.log(`Job ${jobId} đã hoàn tất, resource: ${resourceId}`);
}

Handler như vậy sẽ:

  • không vỡ nếu sự kiện đến hai lần;
  • không tạo hiệu ứng phụ (kiểu “hiện modal ‘Xong!’ lần thứ hai”);
  • mở đường cho logic phức tạp hơn, ví dụ, kiểm tra tính hợp lệ của các chuyển trạng thái (không cho failed đè lên khi đã completed).

Trong code production, rất có thể bạn sẽ muốn xóa processedEvents khi kết nối lại MCP server, và lưu không chỉ eventId mà còn trạng thái hiện tại của từng jobId, để khi gặp chuỗi sự kiện bất thường có thể hành xử hợp lý hơn.

Tiếp theo quan trọng là hiểu cách các MCP‑sự kiện đi qua agent/widget và biến thành trải nghiệm người dùng cụ thể: thanh tiến độ, các giai đoạn thực thi, xuất hiện kết quả cuối. Chúng ta sẽ gắn kết sự kiện với run/workflow và UX.

9. Liên kết sự kiện, run/workflow và UX

Mặc dù chúng ta đã có một mô-đun đầy đủ về workflow và agent, giờ bạn sẽ thấy bức tranh tổng thể. Chúng ta đã giới thiệu các họ sự kiện (job.*, resource.*, hệ thống); hãy xem chúng đi qua agent/widget và ChatGPT như thế nào để trở thành trải nghiệm người dùng cụ thể.

Kịch bản điển hình với một tác vụ dài như sau: ChatGPT gọi MCP tool, nhận jobId; sau đó dựa theo jobId này, server gửi sự kiện về tiến độ, hoàn tất hoặc lỗi; widget của bạn hoặc logic agent dựa trên đó để cập nhật UI và đưa ra quyết định.

Trên biểu đồ trình tự, có thể vẽ như sau:

sequenceDiagram
    participant User as Người dùng
    participant GPT as ChatGPT (mô hình)
    participant App as Máy chủ MCP của GiftGenius
    participant Widget as Widget GiftGenius

    User->>GPT: "Hãy chọn quà cho 2000 nhân viên"
    GPT->>App: tools.call start_bulk_gift_analysis
    App-->>GPT: response { jobId: "bulk_2025_001" }

    GPT->>Widget: ToolOutput { jobId }
    Widget->>Widget: Hiển thị thanh tiến độ

    App-->>GPT: notification job.started
    App-->>GPT: notification job.progress (10%, 40%, 70%, 100%)
    App-->>GPT: notification job.completed { resultResourceId }

    GPT->>Widget: Chuyển tiếp sự kiện/dữ liệu vào widget
    Widget->>User: Cập nhật tiến độ và hiển thị kết quả
    

Trên thực tế, biểu đồ sẽ phức tạp hơn một chút, nhưng ý chính rất rõ: MCP‑sự kiện là “hệ thần kinh” giữa các thao tác nền và trải nghiệm người dùng của bạn.

10. Những lỗi thường gặp khi làm việc với MCP‑sự kiện

Lỗi số 1: “Sự kiện = log ở định dạng production”.
Đôi khi lập trình viên bắt đầu bằng việc chỉ chuyển sang MCP thứ họ từng ghi vào console.log. Kết quả là trong sự kiện không có eventId, không có jobId, không có timestamp tử tế, chỉ là thông điệp nửa văn vẻ “chúng tôi gần xong rồi”. Cách tiếp cận này làm hệ thống mong manh: khó parse, không thể khử trùng lặp, UI không biết thông điệp thuộc về tác vụ nào. Tốt hơn hết, ngay từ đầu hãy thiết kế sự kiện như một hợp đồng chính thức: tên method rõ ràng, bộ trường ổn định, payload hợp lý.

Lỗi số 2: Thiếu tính idempotent và eventId duy nhất.
Nhiều người bắt đầu với ý tưởng ngây thơ: “sự kiện thì đến một lần chứ gì”. Một tuần sau mọi chuyện bắt đầu: khi client kết nối lại, notifications bị nhân đôi, người dùng nhận cùng một thứ hai lần, backend thương mại cộng thưởng hai lần. Không có eventId duy nhất và khử trùng lặp tối thiểu ở client thì sớm muộn bạn cũng gặp bug nghiêm trọng. Trong hệ phân tán, cần xuất phát từ mô hình “at‑least‑once delivery”: bản sao là điều không tránh khỏi.

Lỗi số 3: Trộn lẫn sự kiện hệ thống và sự kiện nghiệp vụ vào một nồi lẩu.
Ví dụ, cùng một luồng đổ về logging/message, job.progress, job.completed, resources/updated, và tất cả không có ranh giới type/method rõ ràng. Hệ quả là lớp UI bắt đầu làm những thứ kỳ quặc như if (message.includes("hoàn tất")) để hiểu tác vụ đã hoàn thành. Tốt hơn là phân chia rành mạch: có notifications hệ thống (log, heartbeat) và có sự kiện nghiệp vụ (job.*, resource.*) với schema được mô tả chặt chẽ.

Lỗi số 4: Các chuyển trạng thái tác vụ không nhất quán.
Có khi server trong cùng một luồng sự kiện gửi job.completed, rồi đột ngột job.progress, rồi job.failed. Điều này xảy ra khi không có state machine rõ ràng và kiểm tra khi phát sự kiện. Client sẽ không thể hiểu điều gì thực sự diễn ra. Đúng đắn hơn là mô tả automaton trạng thái hữu hạn và không phát những sự kiện vi phạm nó: ví dụ, sau completed bạn cùng lắm chỉ gửi sự kiện thông tin bổ sung, chứ không đưa tác vụ về running.

Lỗi số 5: Gắn cứng vào tên method MCP cụ thể của phiên bản đặc tả hiện tại.
Đặc tả MCP vẫn đang phát triển. Nếu bạn buộc mọi thứ vào các method hiện tại với tên hệ thống, mà không xây namespace của riêng mình, bất kỳ thay đổi giao thức nào cũng sẽ buộc bạn viết lại nửa hệ thống. Tốt hơn hãy coi sự kiện như đặc tả con trên MCP: bạn có thể dựa vào các method hiện có (notifications/progress, resources/updated), nhưng các sự kiện nghiệp vụ (notifications/job/*) nên thiết kế trong namespace của riêng bạn và giữ chúng tương đối độc lập.

Lỗi số 6: Không liên kết sự kiện với UX.
Đôi khi đội ngũ tạo ra mô hình sự kiện đẹp ở backend, nhưng không đưa nó đến widget: job.progress chỉ tồn tại trong log, còn UI hiển thị một spinner cô đơn suốt 40 giây. Người dùng trong kịch bản như vậy không tin MCP cũng như không tin AI. Khi thiết kế sự kiện, luôn nghĩ đến hiệu ứng UI cụ thể bạn muốn: thanh tiến độ, các giai đoạn, kết quả từng phần. MCP‑sự kiện tồn tại không phải vì giao thức, mà vì hành vi ứng dụng dễ hiểu.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION