CodeGym /課程 /ChatGPT Apps /將 ChatGPT App 整合至既有產品與 SDK/MCP 遷移

將 ChatGPT App 整合至既有產品與 SDK/MCP 遷移

ChatGPT Apps
等級 20 , 課堂 2
開放

1. 為何需要談整合與遷移

到目前為止,我們多半是以自己方便的方式設計 API 與工具。現實世界幾乎總是反過來:你通常已經有:

  • 單體或一堆微服務;
  • REST/GraphQL API;
  • 已在生產環境運行多年的商業邏輯。

然後需求來了:「請透過 Apps SDK 與 MCP 把我們的產品連到 ChatGPT」。

把一切重寫成「理想的 MCP 伺服器」並不可行。需要在既有世界之上輕巧地「套」一層,將你的後端語言轉換為 ChatGPT 的語言:工具、資源與 schema。

第二個問題:產品是活的。Schema 與 API 會變。在一般前端裡,當你改了欄位,至少會立刻得到 TypeScript 錯誤。在 LLM‑Apps 的世界更狡猾:模型會繼續自信地送舊格式,tool 會崩,結果你得到的不是漂亮的編譯失敗,而是:

  • MCP 伺服器上的 runtime 錯誤;
  • 「我大概猜到你想要什麼欄位」之類的幻覺;
  • 惱人的品質事故。

因此在本講,我們把 MCP+Apps 層視為:

  • 對既有後端的配接器;
  • 需要維持多年的契約;
  • 遷移對象:版本、註解、scopes 與 SDK。

2. 整合架構:作為既有後端之上的 MCP 配接器

基本藍圖

回顧一下技術堆疊,但以生產環境的視角:

flowchart LR
  U[ChatGPT 使用者] --> G[ChatGPT 模型]
  G -->|呼叫 App| W["小工具(Apps SDK, Next.js)"]
  G -->|tools.call| MCP[MCP 伺服器 / Gateway]
  MCP --> S1["Gift Service (你現有的服務)"]
  MCP --> S2["Commerce Service (訂單,ACP)"]

ChatGPT 並非直接與你的世界對話,而是透過 MCP 協定:tools/resources 清單、tools/call 呼叫、事件串流。

在這個架構中,MCP 伺服器就是那個配接器:它同時了解 ChatGPT(JSON‑RPC、工具)與你的服務(REST/DB/佇列),並把兩邊語言相互轉譯。

MCP 作為 Gateway/Adapter

典型情境:你已有帶 REST 端點的 Gift Service:

// 既有的 REST API 範例
GET  /api/gifts/recommendations?budget=100&occasion=birthday
POST /api/orders

與其重寫商業邏輯,不如讓 MCP 層把它包成 Tool:

// mcp/tools/recommendGifts.ts
import { z } from "zod";
import { server } from "./mcpServer"; // 假想的 SDK 實例

const recommendGiftsInput = z.object({
  occasion: z.string(),
  budgetUsd: z.number().int().positive(),
});

server.registerTool({
  name: "recommend_gifts",
  description: "在預算範圍內挑選禮物點子",
  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(); // 重要:回傳對模型與小工具都友善的 JSON
  },
});

所有禮物挑選邏輯仍留在你現有的服務內。MCP 層是把 ChatGPT 語言翻成你的 API 語言的「薄翻譯器」。

有時 MCP 層還會把請求路由到多個後端服務。這時它就成了完整的 MCP Gateway——我們會在生產與網路模組中更深入地分析其角色。

Monolith‑integrated MCP vs Sidecar MCP

把 MCP 層「掛」在哪裡有兩種基本做法。

文字版如下:

方案 說明 MCP 程式碼所在處
Monolith-integrated 全部在同一個 Next.js/Node 服務中 Next.js 或 Express 的 API route
Sidecar MCP 獨立的容器/服務,透過 API 溝通 獨立的 Node/Go 應用

在小型專案通常第一種就夠用:Next.js 應用、部署到 Vercel,上面有個路由 /mcp/api/mcp,MCP 伺服器與其他 API 並排。

範例(大幅簡化):

// 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); // JSON-RPC 請求
  return new Response(JSON.stringify(response), {
    headers: { "content-type": "application/json" },
  });
}

在更成熟的架構中,若你有多個網域服務(Gift、Commerce、Analytics),把 MCP 層獨立為 Gateway 更方便。它接收來自 ChatGPT 的 MCP 流量,並依工具名稱自行路由到不同後端。

重要提醒:對 ChatGPT 與 Apps SDK 而言,它始終是單一個 MCP 伺服器。它究竟跑在單體內或作為獨立微服務,則是你的架構選擇。

MCP 層可以在單體內或作為獨立 Gateway 運作沒問題。接下來的問題是,它究竟收什麼、回什麼——這就需要談 schema 與契約。

3. Single Source of Truth:schema、型別與契約測試

若你同時擁有內部 DTO、對外 REST 契約,以及 MCP 工具的 schema——「照感覺畫出 schema」的誘惑會很大。結果可想而知:

  • 你改了後端的欄位,卻忘了更新工具的 schema;
  • 模型仍然送舊格式;
  • 收穫一個歡樂的 runtime 動物園。

正確做法:為資料結構建立單一事實來源,並讓它到處通用。在 TypeScript 世界,很適合用 Zod 或類似庫,MCP SDK 能把它轉成 JSON Schema。

GiftGenius 的通用 Zod schema

假設你在我們的教學專案 GiftGenius 的 Gift 服務中,已用 Zod 做輸入驗證:

// domain/gifts.ts
import { z } from "zod";

export const giftRecommendationInputSchema = z.object({
  occasion: z.string().describe("場合:birthday、wedding 等"),
  budgetUsd: z.number().int().positive(),
  recipientProfile: z.string().describe("收禮者的簡短描述"),
});

export type GiftRecommendationInput = z.infer<
  typeof giftRecommendationInputSchema
>;

同一份 schema 會用在:

  • REST 端點(驗證請求 body);
  • MCP 工具(作為 inputSchema);
  • 測試(作為 fixtures 的基礎)。

把 schema 掛到 MCP 工具

// mcp/tools/recommendGifts.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
import { server } from "../mcpServer";

server.registerTool({
  name: "recommend_gifts",
  description: "依據個人檔案與預算挑選禮物",
  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 會自動把 Zod schema 轉為 JSON Schema,讓 ChatGPT 在 tools/list 看見。 這同時解兩個問題:

  • 工具參數型別與程式碼緊密連動;
  • 當 schema 改變,TypeScript 編譯器會逼你同步更新處理器。

MCP ↔ 後端的契約測試

這裡的契約測試不嚇人,其實就是幾個很落地的檢查。

最簡單的 unit/contract 測試可能長這樣:

// tests/mcp/recommendGifts.contract.test.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";

test("範例請求符合工具的 schema", () => {
  const sample = {
    occasion: "birthday",
    budgetUsd: 150,
    recipientProfile: "同事,喜歡電子產品",
  };

  expect(() => giftRecommendationInputSchema.parse(sample)).not.toThrow();
});

這種測試不能保證世界美好,但至少能在你改了 schema 卻忘了更新 fixtures 時,抓到 MCP 層與後端期望的不同步。

接著很容易把同樣的方法擴展到:

  • 外部 API(Stripe、CMS)的模擬回應;
  • 在測試環境用真實 MCP 伺服器跑一次 MCP 客戶端。

4. tools 與 resources 的版本策略

Schema 早晚會變。重點是不該「就改個欄位名稱,會出什麼事?」在 LLM 世界,你不只會把建置搞掛,還會影響模型行為:舊的 prompt、已保存的對話與 golden case 會持續期待舊契約。

增補式 vs 破壞性變更

大致可分兩類。

增補式變更——你新增了某些東西,但不會弄壞誰:

  • 回應新增非必填欄位;
  • 新增非必填參數且有預設值;
  • enum 新增額外值,而 UI 與模型可以中性對待。

例如,你在工具回應裡加了 deliveryEstimateDays 欄位,但舊版小工具會直接忽略。這是安全的:schema 可擴充,但沒人被迫使用它。

破壞性變更——你破壞了既有預期:

  • 把本來沒有的欄位變成必填;
  • 更改型別(字串 → 物件);
  • 改變參數意義(預算從 USD → 在地貨幣),卻不改欄位名稱。

這種情況唯一安全之道——建立新版本的工具。

模式 Tool_v2

典型模式:你有 recommend_gifts,現在想大改 schema。不要動舊工具,直接建立新工具——recommend_gifts_v2

// v1
const recommendGiftsInput_v1 = z.object({
  occasion: z.string(),
  budgetUsd: z.number().int().positive(),
});

// v2: 支援貨幣與配送過濾
const recommendGiftsInput_v2 = z.object({
  occasion: z.string(),
  maxPrice: z.number().int().positive(),
  currency: z.enum(["USD", "EUR", "GBP"]),
  deliverByDate: z.string().optional(); // ISO 字串
});

server.registerTool({
  name: "recommend_gifts",
  description: "DEPRECATED: 請使用 recommend_gifts_v2",
  inputSchema: recommendGiftsInput_v1,
  async execute(args) { /* 舊邏輯 */ },
});

server.registerTool({
  name: "recommend_gifts_v2",
  description:
    "依據預算、貨幣與配送期限挑選禮物",
  inputSchema: recommendGiftsInput_v2,
  async execute(args) { /* 新邏輯 */ },
});

模型與舊的 prompt/代理會持續使用 recommend_gifts,直到你去更新它們。新的情境則改寫成使用 recommend_gifts_v2

遷移一段時間後:

  • golden case 與代理都切到 v2
  • 指標顯示 v1 幾乎不再被呼叫;

即可開始有計畫地收攤 v1(例如先在 dev/staging 的工具清單隱藏,再到 prod)。

資源的版本化

不只 tools 需要版本。若你有資源(resources)——例如禮物靜態目錄——也最好版本化。

常見做法:

  • 把版本寫進資源名稱:gift_catalog.v1.jsongift_catalog.v2.json
  • 或透過 URI/參數傳遞版本:/api/catalog?version=1

意義相同:不要在已經跑著的情境腳下偷偷換資料,而是提供明確固定的目錄版本。

零停機遷移

工具遷移的典型循環:

  1. 在舊版旁邊新增工具新版本(_v2)。
  2. 更新 App/代理/system prompt,讓它們使用新版本。
  3. 對兩個版本都跑一次 golden case 與 LLM‑eval,確保關鍵情境的品質未下降。
  4. 觀察 v1v2 的使用率(以及錯誤)。
  5. v1 的流量接近零時,開始逐步停用。

這種方法對 schema 遷移、SDK/協定更新,以及 Auth 變更都很適用。我們談過工具與資源如何透過 v1/v2 與審慎的增補式變更來演進。第二大塊契約是身分驗證與授權:OAuth、scopes 與 .well-known。它們也會長期存在,需要謹慎遷移。

5. 身分驗證的演進:.well-known、scopes 與既有 OAuth

若你的產品已採用 OAuth 2.1/OpenID Connect,透過 MCP 與 ChatGPT 整合並非「又一個登入」,而是新增一個必須遵守你 Authorization Server 共通規則的客戶端。

MCP 與 .well-known/oauth-protected-resource

完整的 OAuth 2.1/OpenID Connect 與 Auth Server 設定在課程的另一個模組有詳談(見身分驗證模組)。此處只看實務面:MCP 資源如何告訴 ChatGPT 它受 OAuth 保護,以及如何啟動 linking flow。

受保護 MCP 資源的標準模式:

  • 你的 MCP 伺服器曝露特殊端點 /.well-known/oauth-protected-resource
  • 回應中說明這是什麼資源,以及由哪些 AS(Authorization Server)保護;
  • 當 MCP 呼叫回傳 401 時,伺服器在 WWW-Authenticate 標頭放上這個 .well-known 的連結,ChatGPT 會自動啟動 OAuth flow(「Link account」)。

用 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);

以及帶有提示給客戶端的 401 處理器:

res
  .status(401)
  .set(
    "WWW-Authenticate",
    'Bearer resource_metadata="https://mcp.myapp.com/.well-known/oauth-protected-resource"',
  )
  .end();

ChatGPT 看到這個標頭後,會知道要去找哪個 AS,並如何為你的 MCP 資源啟動 OAuth flow。

Scopes 與授權遷移

Scopes 也是遷移的來源。我們在 Auth 模組裡已經詳談,但在整合/遷移情境下有幾點很重要。

想像 GiftGenius 一開始只會讀目錄(gifts.read),後來你新增了 gifts.write 來建立訂單。你需要:

  • 在客戶端(ChatGPT App)設定中加上新 scope;
  • 更新 MCP 伺服器,讓它只在真正有改動的工具上要求此 scope;
  • 若有需要,更新 .well-known 的描述。

從 UX 角度看,使用者在下一次嘗試使用新功能時,可能會看到要求「擴充權限」的提示。你不會想讓它在既有對話中毫無預警地發生——因此這類變更需要:

  • 事先公告(發佈說明、文件);
  • 在 staging 上用測試 AS 驗證;
  • 搭配更新工具描述(destructiveHint 等),讓模型「有意識地」呼叫具破壞性的 tools。

6. 中介層:以 metadata/annotations 作為契約之上的 hint

Auth 層回答的是:誰能做什麼。但即使 token 與 scopes 都正確,模型如何呼叫你的工具、如何向使用者解釋行為仍很重要。此時就需要額外的 hint 層:metadata 與 annotations。

契約(schema)說明工具接受/回傳什麼。Metadata 與 annotations 則幫助模型理解何時如何呼叫。當你演進 App(加上新的破壞性動作、調整 UI、引入外部整合)時,這尤其關鍵。

_meta["openai/widgetDescription"]widgetCSP

在 Apps SDK 與 MCP 描述中有個特殊的 _meta 欄位,OpenAI 會在此放入協定擴充。例如:

  • _meta["openai/widgetDescription"]——你的小工具會顯示什麼的簡述;模型可利用它避免「重述」UI,並正確介紹 App;
  • _meta["openai/widgetCSP"]——宣告小工具所需的 CSP 網域(用於 fetch/圖片/腳本)。

當你更動 UI(例如新增下單流程的步驟),記得更新 widgetDescription,讓模型持續正確地向使用者說明正在發生的事。

工具註解(readOnlyHintdestructiveHintopenWorldHint

註解是簡單的布林旗標,卻能明顯影響 UX 與安全性:

  • readOnlyHint: true——工具不會改動任何東西(唯讀)。模型可在不需額外確認的情況下呼叫。
  • destructiveHint: true——工具可能刪除/更改內容。ChatGPT 會要求明確確認。
  • openWorldHint: true——工具會對外部世界發佈資料,或可能回傳「非常多」的資訊,需要進一步摘要。

帶註解的工具描述範例:

server.registerTool({
  name: "delete_saved_gift",
  description: "刪除使用者已儲存的禮物",
  inputSchema: z.object({ giftId: z.string() }),
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    openWorldHint: false,
  },
  async execute({ giftId }) {
    // ...刪除禮物
  },
});

在遷移過程中,當你加入新的「危險」工具時,註解就是你的朋友:它們幫助 ChatGPT 不會偷偷執行此類操作,並促使其更謹慎地行事。

要理解的是,註解不是真正的防護。它們只影響客戶端與模型行為。真正的安全性仍由你的伺服器保證(Auth、scopes、驗證)。

7. SDK 與 MCP 規格的遷移

MCP 與 Apps SDK 正在快速演進——capabilities 會新增欄位、訊息類型會增加、新的 _meta/annotations 會出現。文件也會老實說明:「截至 2025 年」——我們得跟著過日子。

因此,SDK/規格版本的遷移是 App 生命週期的正常部分,而不是「有朝一日才會做」的少數事件。

典型升級流程

健康的更新流程大致如下:

  1. 閱讀新版本 Apps SDK/MCP SDK 的變更紀錄,標記所有可能的破壞性變更。
  2. 在 dev/staging 環境更新依賴,先不要動 prod。
  3. 跑一次 MCP Inspector / Jam 或其他客戶端:
    • 檢查 handshake;
    • tools/list / resources/list
    • 若干測試性的 tools/call
  4. 依據新能力更新工具描述與 _meta
    • 例如加入新的 annotationswidgetDescription
  5. 跑一次 golden case 與 LLM‑eval(如同前幾講所述),以確保 App 的品質層面未受影響。
  6. 之後才把更新推到 prod,能用 canary/feature flag 分流更好。

範例:在新 SDK 版本中加入 openWorldHint

假設新版 Apps SDK 加入了 openWorldHint,你決定把它加到工具 search_public_reviews 上,此工具會抓取外部評論,可能回傳大量雜訊。

步驟如下:

  • 更新 SDK 與型別;
  • 在工具描述中新增 annotations.openWorldHint = true
  • 更新 system prompt,讓代理明確告知使用者接下來會對外發出請求;
  • 跑一輪安全性 golden case(尤其是隱私/PII 相關),確認模型未變得過度健談。

我們已說明更新 SDK 與註解的通用流程。接著將這些觀念套用到一個具體案例——recommend_gifts 的演進。

8. 迷你案例:GiftGenius 中 recommend_gifts 的演進

讓我們把所有內容放在一個具體情境中。

初始版本

基礎工具如下:

const recommendGiftsInput_v1 = z.object({
  occasion: z.string(),
  budgetUsd: z.number().int().positive(),
  recipientProfile: z.string(),
});

server.registerTool({
  name: "recommend_gifts",
  description: "用 USD 挑選禮物點子",
  inputSchema: recommendGiftsInput_v1,
  async execute(args) {
    const input = recommendGiftsInput_v1.parse(args);
    return giftService.recommend(input); // 內部函式
  },
});

若你的使用者都在美國且只有一種貨幣,一切安好。

新的產品需求:多幣別與配送期限

產品團隊提出新需求:

  • 需要支援 EUR/GBP;
  • 需要考量配送期限(若三天後就生日,就不要顯示一個月後才到的禮物);
  • 最好在回應裡提供預估配送時間。

天真的做法:直接改欄位:

  • budgetUsd 改名為 maxPrice
  • 新增 currency
  • 回應新增 deliveryEstimateDays

會出什麼問題?

舊的 prompt(包含 golden case 與 system prompt 描述)與已保存的對話仍會送 budgetUsd。模型不知道它不存在了。MCP 層在 parse 時會崩。ChatGPT App 的行為會在真實使用者處突然壞掉。

正確作法:

  1. 新增新 schema 與新工具 _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:
    "挑選禮物,考量幣別與期望的到貨日期",
  inputSchema: recommendGiftsInput_v2,
  async execute(args) {
    const input = recommendGiftsInput_v2.parse(args);
    return giftService.recommendV2(input); // 新邏輯
  },
});
  1. 保留 recommend_gifts 不動,只在 description 加上 DEPRECATED 標註。
  2. 更新 system prompt 與 App 描述,讓模型更偏好 recommend_gifts_v2(可在指令中明確指出)。
  3. 更新 GiftGenius 小工具,理解新回應格式:例如 deliveryEstimateDays 等欄位。
  4. 針對典型情境(例如在某日期前送達的禮物挑選)跑一次 golden case 與 LLM‑eval。

測試與可觀測性

有幾種測試特別值得擁有:

新輸入的契約測試:

test("v2 接受含 EUR 與期限的情境", () => {
  const sample = {
    occasion: "birthday",
    maxPrice: 100,
    currency: "EUR",
    recipientProfile: "同事",
    deliverByDate: "2025-12-24",
  };

  expect(() => recommendGiftsInput_v2.parse(sample)).not.toThrow();
});

在生產環境的觀測:

  • recommend_gifts_v2 相較 recommend_gifts 的呼叫占比;
  • v1 的錯誤率(預期不會上升);
  • 遷移前/後 golden case 的 LLM‑eval 分數(你已在前幾講學過如何執行)。

v2 在品質與使用率上都「勝出」,即可規劃逐步停用 v1

若簡化成三點: (1) MCP 是薄配接器,而非新的單體; (2) schema、auth 與註解是 ChatGPT 與你的後端之間的長期契約,必須像一般 API 一樣嚴謹地版本化與測試; (3) 任何 SDK/規格的遷移都是正常的工程流程,要有 staging、golden case 與可觀測性,而不是「週五晚上更新套件」。 用這個視角看待 ChatGPT App,與既有產品的整合不再像是一團混沌。

9. MCP/SDK 整合與遷移中的常見錯誤

錯誤 №1:把 MCP 當成「新後端」,而非薄配接器。
有時會想把所有商業邏輯都塞進 MCP 層:連資料庫、網域規則、計算都搬來。這會把 MCP 伺服器變成另一個難以與其餘後端同步的單體。健康作法是把 MCP 維持為既有服務之上的 Gateway/Adapter:網域邏輯仍待在 ChatGPT 之前的地方,MCP 只做 JSON 的來回轉換。

錯誤 №2:對同一個物件使用不同 schema。
常見反模式是擁有三個「禮物」定義:一個在 DB,一個在 REST API,一個在 MCP 工具,且彼此略有差異。最後靜態型別、契約、測試與常識一起崩。使用單一 schema(Zod/TypeBox 等)作為 Single Source of Truth,並為 MCP 產生 JSON Schema,可大幅降低風險。

錯誤 №3:錯誤的 schema 遷移——「無聲」的破壞性變更。
不改工具名稱就改欄位名稱或語意,會導致隱性回歸。模型持續送舊格式,事故只在部分使用者、且過一陣子才浮現。遇到重大變更請建立 *_v2,讓舊版本並行,搭配棄用註記與監控。

錯誤 №4:忽略 Auth 變更與 scopes。
新增具副作用的工具,卻忘了更新 scopes 與 .well-known?使用者可能在流程中途遇到 401,或反過來,你的 MCP 在沒有足夠授權下執行破壞性操作。像對待 schema 遷移一樣審慎規劃 auth 層遷移:經過 staging、測試與平滑擴權。

錯誤 №5:不使用註解(destructiveHintreadOnlyHintopenWorldHint)。
若不提示模型哪些工具安全、哪些潛在危險,行為會很詭異:對無害的 get_catalog 反而要求確認,卻在刪除資料時不發一語。正確的註解能讓行為對使用者更可預期,並降低品質與安全事故風險。

錯誤 №6:未跑 golden case 就在 prod 更新 SDK。
新版 SDK/規格可能新增欄位、改變 handshake 行為或訊息結構。若只是「更新相依後直接部署」,很容易撞見品質回歸(模型不再呼叫該用的工具、錯誤訊息措辭改變等)。先走 dev/staging、MCP Inspector,再跑 golden case 與 LLM‑eval,最後才上 prod。

錯誤 №7:把商業邏輯硬綁到某個工具版本。
當內部 Gift Service 直接依賴特定的 recommend_gifts,要遷移到 recommend_gifts_v2 就會很痛。最佳實務是讓內部服務按自己的規則演進,而 *_v1*_v2 工具只是薄 adapter,把新舊外部契約對映到共同的網域結構。

錯誤 №8:缺乏依工具版本的可觀測性。
若在日誌與指標中無法區分是哪個工具與版本被呼叫,遷移除錯會變成猜謎。請記錄工具名稱、schema/SDK 版本與關鍵參數——任何回歸就能更容易對上特定變更。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION