MCP Apps là gì? Build app chạy ngay trong Claude và ChatGPT
Build ba MCP app (form, dashboard Chart.js, PDF) chạy trong Claude Desktop bằng MCP Apps SDK, rồi deploy MCP server lên Amazon Bedrock AgentCore.
✓ Đã duyệt độc lập · cập nhật 27/10/2026 · 14 min
1. Từ một bức tường chữ tới UI ngay trong chat
Ashita mở đầu bằng một câu hỏi: bạn đã bao giờ hỏi AI assistant của mình để lấy insight, rồi nhận về một bức tường chữ (a wall of text) chưa? Khi hỏi insight, bạn không muốn một đoạn mô tả bằng chữ. Bạn muốn thứ gì đó tương tác hơn, như chart, hay thậm chí một dashboard mà bạn lọc được.
Trên màn hình, chị cho xem nhanh cảnh đó: một cửa sổ chat với câu "get sales data", rồi ngay sau đó là một dashboard có biểu đồ cột chồng, biểu đồ tròn và biểu đồ thanh ngang nằm ngay trong khung trò chuyện (demo trong video đăng ngày 20/10/2026).
Lập luận của chị: chúng ta đã build những trải nghiệm người dùng tuyệt vời cho web suốt nhiều năm. Vậy tại sao không mang chúng vào trong chat luôn? Theo Ashita, đây là lúc MCP Apps tới "giải cứu" (video đăng ngày 20/10/2026).

2. MCP Apps là gì và bốn khối xây dựng
Theo Ashita (video đăng ngày 20/10/2026), MCP Apps là một extension của core MCP specification, lấy cảm hứng từ MCP UI và Apps SDK của OpenAI. Nó cho phép bạn thêm một lớp UI vào MCP mà agent có thể render, tạo ra những giao diện người dùng tương tác và điều khiển được ngay bên trong chat.
Nhờ MCP Apps, chị nói, MCP server của bạn có thể hiển thị dashboard tương tác, form, visualization và kết quả real-time trực tiếp trong cuộc trò chuyện (theo video đăng ngày 20/10/2026). Trang tài liệu MCP Apps trên modelcontextprotocol.io cũng mô tả tương tự: server trả về giao diện HTML tương tác (data visualization, form, dashboard) render thẳng trong chat (đọc ngày 27/10/2026).

Theo chị (video đăng ngày 20/10/2026), một MCP app được ghép từ bốn khối xây dựng:
- UI resource: định nghĩa giao diện (video đăng ngày 20/10/2026).
- Tool logic: phần làm việc thật (video đăng ngày 20/10/2026).
- Host: nơi chạy tất cả (video đăng ngày 20/10/2026).
- App view: nơi người dùng thấy app "sống dậy" (video đăng ngày 20/10/2026).
Chị sẽ đi qua từng khối một.
3. Tạo MCP server và bài toán sales analytics
Trước khi vào từng khối, theo Ashita (video đăng ngày 20/10/2026), ta phải import class McpServer từ MCP TypeScript SDK chính thức. Class này là API cấp cao để build server, lo hết phần plumbing của giao thức MCP. Ta tạo server instance với một name và một version, rồi bọc việc khởi tạo đó trong một factory function, để nơi gọi có thể tạo server instance mới bất cứ lúc nào cần.

server.ts: import McpServer từ @modelcontextprotocol/sdk/server/mcp.js, rồi hàm createServer() tạo server tên sample-mcp-apps-chatflow, version 1.0.0 (video đăng ngày 20/10/2026).import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function createServer(): McpServer {
const server = new McpServer({
name: "sample-mcp-apps-chatflow",
version: "1.0.0",
});
return server;
}Ví dụ xuyên suốt video là sales analytics. Ashita giải thích (video đăng ngày 20/10/2026): vì ta build MCP apps để hỗ trợ phân tích doanh số, app thứ nhất cần có là một input form, giúp lấy đúng dữ liệu sales theo metric, khoảng thời gian, năm và vùng bán hàng.
Chị nói tiếp: bạn có thể thấy MCP apps biến AI chat từ một phương tiện chỉ có chữ thành thứ gần với trải nghiệm của một app thật. Theo chị, khi agent gọi MCP tool, server sẽ không chỉ trả về dữ liệu hay kết quả, mà trả về cả một mini app HTML (video đăng ngày 20/10/2026).

select-sales-metric của server sample-mcp-apps-chatflow hiện form "Select Metrics & Regions": metric Total Revenue, Monthly, năm 2025, ba bang Maharashtra, Tamil Nadu, Karnataka đã được tick, nút Submit và dòng xanh "Added to chat context" (demo trong video đăng ngày 20/10/2026).4. UI resource: một mini app gói trong một file HTML
Việc mở màn của phần build là tạo và đăng ký UI resource. Theo Ashita (video đăng ngày 20/10/2026), nó giống như bất kỳ web app nhỏ nào, viết thành các file HTML, CSS và TypeScript riêng: HTML định nghĩa cấu trúc, CSS lo phần style, TypeScript đảm nhận hành vi của app. Trên màn hình là ba file nguồn trong src/: index.html, styles.css và mcp-app.ts.
Lúc build, chị nói, tất cả được bundle thành một file HTML duy nhất, tự chứa, tức sales-form.html. Chính file này là resource ta sẽ đăng ký với server (video đăng ngày 20/10/2026).
Ta import registerAppResource và RESOURCE_MIME_TYPE từ MCP Apps SDK chính thức. Theo Ashita (video đăng ngày 20/10/2026), registerAppResource đăng ký HTML view của app như một UI MCP resource, để AI host biết phải render gì khi một tool tham chiếu tới nó. Resource thứ nhất có tên sales-metric-input-ui.

registerAppResource, RESOURCE_MIME_TYPE từ @modelcontextprotocol/ext-apps/server được đóng khung, bên dưới là lệnh đăng ký resource với description "Interactive sales metrics selector with regions, metrics, and time periods" và callback đọc sales-form.html (video đăng ngày 20/10/2026).import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
const salesFormResourceName = `sales-metric-input-ui`;
const salesFormUri = `${RESOURCE_URI}/${salesFormResourceName}`;
registerAppResource(
server,
salesFormResourceName,
salesFormUri,
{
mimeType: RESOURCE_MIME_TYPE,
description: "Interactive sales metrics selector with regions, metrics, and time periods",
},
async () => ({
contents: [{
uri: salesFormUri,
mimeType: RESOURCE_MIME_TYPE,
text: await readAppHtml("sales-form.html"),
}],
}),
);Mỗi UI resource có một resource URI riêng, khai báo trước, dùng để tham chiếu tới nó. Trong bài blog đi kèm video, URI này là ui://sample-mcp-apps-chatflow/sales-metric-input-ui, gắn với RESOURCE_MIME_TYPE có giá trị text/html;profile=mcp-app (bài viết của Ashita trên AWS Builder Center, đọc ngày 27/10/2026). Theo chị (video đăng ngày 20/10/2026), MIME type này giúp AI host nhận ra đây là UI của một MCP app chứ không phải một đoạn HTML text thông thường.
Để gộp HTML, CSS và TypeScript của app thành một file HTML tự chứa, mọi dependency được inline, Ashita dùng vite-plugin-singlefile. Lý do chị đưa ra (video đăng ngày 20/10/2026): trong MCP Apps, toàn bộ app phải đi trong một blob text duy nhất của resource. Khi host yêu cầu resource này, read callback chạy, nạp file sales-form.html tự chứa, và đó là thứ hiện ra thành UI tương tác trong chat.
5. Tool logic và metadata linkage
Khối tiếp theo là MCP tool logic. Theo Ashita (video đăng ngày 20/10/2026), ta dùng tiện ích registerAppTool để đăng ký những MCP tool có UI đi kèm. So với base SDK, hàm này cần UI metadata trong config, cho AI client biết host phải render view nào khi tool được gọi.

select-sales-metric được đăng ký bằng registerAppTool: trong _meta.ui.resourceUri là salesFormUri, và kết quả trả về chỉ là một đoạn text "Select Sales Metric Tool executed. Select input in the MCP App UI." (video đăng ngày 20/10/2026).import {registerAppTool} from "@modelcontextprotocol/ext-apps/server";
registerAppTool(
server,
"select-sales-metric",
{
title: "Select Sales Metric",
description: "Select sales regions, metrics, and reporting periods for analysis.",
_meta: {
ui: {
resourceUri: salesFormUri
},
},
},
async () => ({
content: [{
type: "text",
text: "Select Sales Metric Tool executed. Select input in the MCP App UI.",
}],
}),
);Ashita lưu ý hai điều (video đăng ngày 20/10/2026). Thứ nhất, UI resource được nối với tool qua metadata linkage, tức trường _meta. Thứ hai, tool vẫn chạy mà không lỗi gì ngay cả khi host không hỗ trợ MCP Apps. Trong trường hợp đó, chỉ kết quả của tool, là đoạn text content "Select Sales Metric Tool executed. Select input in the MCP App UI.", được gửi về làm response.
6. Host và View: sandboxed iframe ngay trong chat
Khối thứ ba là host: AI client nơi agent của bạn chạy. Theo Ashita (video đăng ngày 20/10/2026), host gọi MCP tool và render UI được liên kết với tool đó. Danh sách các host đang hỗ trợ MCP Apps được cập nhật thường xuyên trên website chính thức, và chị khuyên mọi người vào xem.

Theo chị (video đăng ngày 20/10/2026), Claude Desktop là một host phổ biến có hỗ trợ MCP Apps, và hôm nay chị dùng nó để test các MCP app sales analytics của mình.
Câu hỏi tiếp theo: host thực sự render UI resource ở đâu? Ashita gọi chỗ đó là view: một sandboxed iframe được render ngay trong AI chat. Theo chị (video đăng ngày 20/10/2026), sandbox về cơ bản là một ranh giới bảo mật. Iframe chạy với quyền bị giới hạn và không với tới được ứng dụng host. Vì vậy một MCP app không thể trực tiếp đọc hay thao túng phần chat xung quanh nó.
Mọi giao tiếp với host, chị nói, đi qua một messaging channel duy nhất, được định nghĩa rõ, nên mọi tương tác đều audit được (video đăng ngày 20/10/2026).

ui://) qua "Metadata Linkage (_meta)"; Host (AI Client) nói chuyện với Tool Logic bằng "JSON-RPC over stdio or Streamable HTTP" và "Fetch UI Template" từ UI Resources; View (Sandboxed iframe) nói chuyện với Host bằng "JSON-RPC over postMessage" (video đăng ngày 20/10/2026).7. Giao tiếp host và view: App, handler và handshake
Theo Ashita (video đăng ngày 20/10/2026), dùng các API postMessage chuẩn của trình duyệt, view có thể dễ dàng nói chuyện với host và ngược lại, theo giao thức JSON-RPC. Điểm hay, chị nói, là bạn không phải tự tay viết những lệnh postMessage này.
Theo chị (video đăng ngày 20/10/2026), trong file TypeScript đi kèm MCP app, chỉ cần import App từ MCP Apps SDK và tạo object client app phía view, object này sẽ nói chuyện với host thay cho UI của bạn. Trên màn hình, file sales-form.ts bắt đầu bằng dòng:
import { App } from "@modelcontextprotocol/ext-apps";Tiếp theo, theo chị (video đăng ngày 20/10/2026), bạn đăng ký mọi event listener gắn với tương tác UI, và đăng ký các handler chạy khi host gửi tới một message thuộc một loại cụ thể, ví dụ ontoolresult, chạy khi MCP tool thực thi tool logic xong và trả kết quả về.
Cuối cùng, ta gọi app.connect(). Theo Ashita (video đăng ngày 20/10/2026), lệnh này nối một postMessage transport và thực hiện handshake khởi tạo UI (bài blog ghi là ui/initialize, đọc ngày 27/10/2026), dọn đường cho mọi giao tiếp giữa host và view.
App, đăng ký listener và handler, rồi gọi app.connect() để bắt tay với host (theo video đăng ngày 20/10/2026).8. Tool chỉ dành cho app và updateModelContext
Theo Ashita (video đăng ngày 20/10/2026), MCP apps còn dùng được cho việc điều phối tool rộng hơn (tool orchestration), khi một tool có thể kích hoạt các tool khác, làm mọi thứ tương tác hơn. Điều này cho phép những tương tác chỉ diễn ra ở UI, như nút refresh hay gửi form, với lựa chọn không lộ bất kỳ chi tiết nào của tool cho agent hay model.
Chị nói (video đăng ngày 20/10/2026) rằng cách này trao nhiều quyền kiểm soát hơn cho người dùng cuối: họ có thể chạy tool một cách deterministic, không phải trông vào AI model để thực hiện tool call.
Vậy là chị đăng ký một tool mới, chỉ app thấy được còn agent thì không, bằng cách đặt visibility trong _meta.ui thành chỉ có "app". Theo Ashita (video đăng ngày 20/10/2026), vì tool này không lộ ra cho model, không có cách nào nó bị gọi nhầm, và ta kiểm soát hoàn toàn việc thực thi nó.

get-sales-data có visibility: ["app"] kèm comment "hidden from the model"; handler gọi getData(states, metric, period, year) rồi trả về cả content dạng text (JSON.stringify của report) lẫn structuredContent (video đăng ngày 20/10/2026).registerAppTool(
server,
"get-sales-data",
{
title: "Get Sales Data",
description: "Fetch sales report data.",
inputSchema: getSalesDataInputSchema.shape,
_meta: {
ui: {
resourceUri: salesFormUri,
visibility: ["app"] // hidden from the model
}
},
},
async ({ states, metric, period, year }) => {
const report = getData(states, metric, period, year);
return {
content: [{
type: "text",
text: JSON.stringify(report)
}],
structuredContent: report as unknown as Record<string, unknown>,
};
},
);Khi bấm nút Submit, tool này được kích hoạt bằng app.callServerTool. Theo chị (video đăng ngày 20/10/2026), khi chạy xong, nó trả kết quả ở cả dạng text lẫn dạng structured content. Trên màn hình, file sales-form.ts gọi:
const result = await app.callServerTool({
name: "get-sales-data",
arguments: {
states: selections.states,
metric: selections.metric,
period: selections.period,
year: selections.year,
},
});
report = (result.structuredContent as SalesReport | undefined) ?? null;
if (!report) {
log.error("get-sales-data returned no structuredContent:", result);
}
setStatus("✅ Data fetched successfully...");Giờ phải cập nhật model context bằng dữ liệu này. Theo Ashita (video đăng ngày 20/10/2026), ta làm việc đó bằng updateModelContext, lệnh lặng lẽ cập nhật context bằng dữ liệu liên quan, gửi qua đường postMessage.

app.updateModelContext gửi cùng lúc content (một khối text dựng bằng buildContextText(selections, report)) và structuredContent chứa selections và report (video đăng ngày 20/10/2026).await app.updateModelContext({
content: [{
type: "text",
text: buildContextText(selections,
report)
}],
structuredContent: {
selections,
report
},
});Chị lưu ý (video đăng ngày 20/10/2026): ta gửi cả content lẫn structuredContent. content là bản text của dữ liệu, còn structuredContent là dữ liệu ở dạng có cấu trúc hơn, như JSON. Một host có thể nhận text nhưng không nhận structured content, nên ta mở đầu bằng một khối text đọc được, rồi gắn thêm payload có cấu trúc.
Với bước này, Ashita đã có MCP app Sales Form, giúp lấy dữ liệu cần thiết và đưa nó vào model context (demo trong video đăng ngày 20/10/2026). Giờ chị chuyển sang app tiếp theo, giúp trực quan hoá dữ liệu đó thành chart và biểu đồ.
9. App thứ hai: dashboard vẽ bằng Chart.js
Chị tạo resource mới và tool tương ứng (trên màn hình là resource sales-visualization đọc file sales-visualization.html, và tool visualize-sales-data với visibility: ["model", "app"]). Theo Ashita (video đăng ngày 20/10/2026), tool này nhận dữ liệu đầu vào do model xác định dựa trên context vừa được cập nhật, rồi chuyển tiếp payload có cấu trúc thành kết quả.
Cả input lẫn result đều được UI của app lắng nghe qua hai handler ontoolinput và ontoolresult. Theo chị (video đăng ngày 20/10/2026), dữ liệu có cấu trúc, sạch sẽ này chảy từ tool tới host rồi tới app: không parse chuỗi, không phải "cào" text trong response của tool. Cuối cùng ta dùng nó để render dashboard. Trên màn hình, file sales-visualization.ts:
function acceptPayload(payload: SalesReportPayload | undefined) {
if (!payload?.report) return;
state.report = payload.report;
state.selections = payload.selections ?? null;
renderDashboard();
}
app.ontoolinput = (params) => {
acceptPayload(params?.arguments as SalesReportPayload
| undefined);
};
app.ontoolresult = (params) => {
acceptPayload(params?.structuredContent as SalesReportPayload
| undefined);
};Để vẽ chart, theo Ashita (video đăng ngày 20/10/2026), chỉ cần dùng Chart.js, thêm nó như một npm dependency bình thường, và vite-plugin-singlefile sẽ inline nó vào bundle một file.
Chị gõ prompt "visualize" để kích hoạt tool và cho xem dashboard được render. Trên màn hình, Claude Desktop hiện "Reading widget context", rồi tool visualize-sales-data render khung "Sales Visualization" cho Revenue, Monthly, 2025, ba bang MH, TN, KA (demo trong video đăng ngày 20/10/2026).

Cuộn xuống, dashboard còn có biểu đồ tròn "State Distribution" và biểu đồ thanh ngang "State Comparison". Sau đó Claude viết một câu tóm tắt rằng doanh thu đạt đỉnh vào tháng Tư rồi đi xuống, Karnataka dẫn trước cả ba bang suốt năm, và hỏi có muốn bản PDF không (demo trong video đăng ngày 20/10/2026).
Theo Ashita (video đăng ngày 20/10/2026), vì view được build từ các NPM package thật, toàn bộ hệ sinh thái JavaScript đều dùng được cho app của bạn. Bạn có thể thêm Chart.js, PDF.js, hay cả những thư viện render cảnh 3D, video player, rich text editor, bản đồ: bất cứ thứ gì npm cài được và Vite bundle được.
10. App thứ ba: báo cáo PDF và nút tải về
App cuối cùng hiển thị báo cáo PDF được tạo ra cho phần sales analytics. Theo Ashita (video đăng ngày 20/10/2026), khi MCP tool chạy, PDF được tạo ở phía server, và nội dung PDF được truyền trong tool result. Trên màn hình, tool show-sales-pdf-report gọi generateSalesPdf(selections, report, metricName) rồi trả về pdfBase64, fileName, fileSize và title trong structuredContent.
Handler ontoolresult của app nhận dữ liệu PDF này, và nó được render trong app nhờ thư viện PDF.js (theo video đăng ngày 20/10/2026). Trong demo, sau câu "generate PDF", trình xem "Revenue - Monthly - 2025" hiện ngay trong chat với 2 trang, có biểu đồ "Monthly Revenue Breakdown" và bảng số theo tháng của từng bang (demo trong video đăng ngày 20/10/2026).
Theo chị (video đăng ngày 20/10/2026), một sandboxed iframe không tự ghi file được. Khi bấm nút Download PDF, app hỏi host, ở đây là Claude Desktop, và host hiện hộp thoại lưu file. Mọi thứ được xử lý qua app.downloadFile, nơi ta chỉ định blob của resource và MIME type.

sales-report-revenue-2025.pdf, hai nút Cancel và Download (demo trong video đăng ngày 20/10/2026).Trên màn hình, file sales-pdf-report.ts gắn việc tải về vào nút bấm:
downloadBtn.addEventListener("click", async () => {
if (!state.pdfBase64) return;
try {
const result = await app.downloadFile({
contents: [
{
type: "resource",
resource: {
uri: `file:///${state.fileName}`,
mimeType: "application/pdf",
blob: state.pdfBase64,
},
},
],
});
if (result?.isError) {
setStatus("Download was cancelled", "error");
} else {
setStatus("✅ PDF downloaded!", "success");
}
} catch (error) {
log.error("Download failed:", error);
setStatus("❌ Download failed", "error");
}
});11. Deploy MCP server lên Amazon Bedrock AgentCore
Phần cuối là deploy MCP server lên cloud để ai cũng dùng được. Theo Ashita (video đăng ngày 20/10/2026), cách làm như sau:
- Đóng gói MCP server dùng Streamable HTTP thành một container Linux ARM64, phục vụ
POST /mcptrên port 8000 (video đăng ngày 20/10/2026). - AgentCore CLI biến container đó thành một runtime được quản lý, cô lập theo từng session, dùng CDK, CodeBuild và ECR (video đăng ngày 20/10/2026).
- Trỏ JWT authorizer của runtime tới một Cognito user pool có app client kiểu client credentials (video đăng ngày 20/10/2026).
- Khi đó, bất kỳ MCP client nào hỗ trợ remote HTTP server cũng gọi được nó từ bất cứ đâu bằng một bearer token (video đăng ngày 20/10/2026).
Bài blog đi kèm đi chi tiết hơn từng bước: Dockerfile cho linux/arm64, cài CLI @aws/agentcore, lệnh agentcore create và cấu hình runtime (bài viết của Ashita trên AWS Builder Center, đọc ngày 27/10/2026).
12. Tổng kết
Ashita khép lại phần MCP apps (video đăng ngày 20/10/2026): chị bắt đầu từ các khái niệm cốt lõi, rồi cho thấy những khái niệm đó ghép lại với nhau thế nào trong một chat flow tương tác thật, từ form chọn dữ liệu, dashboard chart, tới báo cáo PDF tải về được.
Chị nhắn: giờ tới lượt bạn build, và tự tạo những trải nghiệm phong phú hơn cho các ứng dụng AI của mình. Chị cảm ơn mọi người đã xem và chào tạm biệt.
Nguồn và liên kết 13 nguồn
- Tên gốc: What are MCP Apps? Building Apps that Run on Claude & ChatGPT
- Sự kiện: YouTube
- Video gốc: https://www.youtube.com/watch?v=Wiun7ZGu-Ks youtube.com
- Kênh YouTube AWS Developers (Ashita Prasad): youtube.com/channel/UCT-nPlVzJI-ccQXlxjSvJmw youtube.com
- Bài viết đi kèm video của Ashita trên AWS Builder Center, có toàn bộ code và các bước deploy lên AgentCore (đọc ngày 27/10/2026): What are MCP Apps? Building Apps that run on Claude & ChatGPT! builder.aws.com
- Repo code mẫu của bài viết, "Sample MCP server that renders interactive sales analytics UIs inside Claude Desktop" (đọc ngày 27/10/2026): github.com/ashitaprasad/sample-mcp-apps-claude github.com
- Tài liệu MCP Apps trên modelcontextprotocol.io (đọc ngày 27/10/2026): modelcontextprotocol.io/extensions/apps/overview modelcontextprotocol.io
- Extension Support Matrix, danh sách client hỗ trợ từng extension, cập nhật qua pull request (đọc ngày 27/10/2026): modelcontextprotocol.io/extensions/client-matrix modelcontextprotocol.io
- MCP Apps SDK, package
@modelcontextprotocol/ext-apps(đọc ngày 27/10/2026): github.com/modelcontextprotocol/ext-apps github.com - MCP TypeScript SDK, package
@modelcontextprotocol/sdk(đọc ngày 27/10/2026): github.com/modelcontextprotocol/typescript-sdk github.com - vite-plugin-singlefile (đọc ngày 27/10/2026): github.com/richardtallent/vite-plugin-singlefile github.com
- Chart.js (đọc ngày 27/10/2026): chartjs.org · PDF.js: mozilla.github.io/pdf.js chartjs.org
- Amazon Bedrock AgentCore (đọc ngày 27/10/2026): aws.amazon.com/bedrock/agentcore aws.amazon.com