Chuyển tới nội dung chính

MCP Apps (Widget UI)

Phía web của AI App là các UI Widget — component React được nhúng vào host dưới dạng iframe. Framework cung cấp @v-miniapp/ai/web với đầy đủ components và hooks để widget có thể giao tiếp với MCP Server và host.

Cấu trúc Widget

Thư mục

Mỗi widget là một thư mục trong web/src/widgets/, bắt buộc có index.tsx (entry point):

web/src/
├── widgets/
│ ├── manage-todos/ ← tên widget = tên resource
│ │ ├── index.tsx ← React component + renderWidget()
│ │ ├── index.css ← Styles (tuỳ chọn)
│ │ └── components/ ← Components riêng của widget (tuỳ chọn)
│ │ └── todo-item.tsx
│ └── order-list/
│ └── index.tsx
├── components/ ← Components dùng chung giữa các widget
│ ├── empty-state.tsx
│ └── loading-skeleton.tsx
├── hooks/ ← Custom hooks dùng chung
│ └── use-debounce.ts
├── styles/ ← CSS dùng chung (tuỳ chọn)
│ └── shared.css
├── locales/ ← i18n (tuỳ chọn)
│ ├── en.json
│ ├── vi.json
│ └── index.ts
├── global.d.ts ← Type augmentation cho hooks
└── vite.config.ts
Tổ chức code như React app bình thường

Bên ngoài widgets/, bạn có thể tạo components/, hooks/, utils/, styles/,... để chia sẻ code giữa các widget — giống cách tổ chức một React app thông thường. Chỉ có thư mục widgets/<name>/ là có quy tắc đặt tên riêng.

Giữ widget gọn nhẹ

Widget được render trong iframe bên trong giao diện chat — mỗi lần AI gọi tool là một lần widget được load lại. Hãy giữ bundle nhỏ để widget hiển thị nhanh:

  • Ưu tiên các thư viện nhẹ hoặc dùng API có sẵn của browser
  • Hạn chế import các library nặng (charting, rich-text editor,...)
  • Tận dụng @v-miniapp/ui-react đã có sẵn trong framework — không cần thêm UI library khác
  • Lấy data qua tool — dùng useCallTool / callTool thay vì gọi fetch trực tiếp từ widget. Mọi logic gọi API nên đặt trong handler của tool trên server, widget chỉ gọi tool và nhận kết quả

Quy tắc đặt tên

Tên thư mục widget phải trùng với tên resource đã đăng ký trên server qua registerResource:

server/src/index.ts
server
.registerResource('manage-todos') // ← tên resource
.registerTool({
name: 'list-todos',
resourceName: 'manage-todos', // ← tool gắn với resource này
// ...
})
web/src/widgets/manage-todos/            ← thư mục widget khớp tên resource
cảnh báo

Nếu tên thư mục widget không khớp với tên resource, host sẽ không tìm thấy widget để hiển thị khi tool được gọi.

Entry point — renderWidget

Mỗi widget phải gọi renderWidget() để mount React component vào iframe:

web/src/widgets/manage-todos/index.tsx
import { renderWidget } from '@v-miniapp/ai/web'

function TodoApp() {
// ... component logic
return <main>...</main>
}

renderWidget(<TodoApp />)

renderWidget sẽ:

  • Mount component vào DOM (#root)
  • Khởi tạo kết nối với MCP Server và host
  • Cung cấp context cho các hooks (useToolInfo, useCallTool, useHostContext,...)

Luồng kết nối Tool → Resource → Widget

1. Server đăng ký resource     →  registerResource('manage-todos')
2. Server đăng ký tool → registerTool({ name: 'list-todos', resourceName: 'manage-todos' })
3. AI gọi tool → list-todos
4. Server trả kết quả → { content, structuredContent: { todos } }
5. Host mở widget → web/src/widgets/manage-todos/index.tsx
6. Widget nhận data → useToolInfo<'list-todos'>() → getStructuredResult(toolInfo)

useToolInfo vs useCallTool

Hai hook chính để widget làm việc với tool, nhưng phục vụ hai chiều ngược nhau:

useToolInfouseCallTool
Ai kích hoạtAI AgentNgười dùng (qua widget)
Mục đíchNhận data ban đầu khi widget mởGọi tool khi người dùng tương tác
callTool()
Lấy kết quả qua.result.data hoặc return của callTool()

Flow: useToolInfo — widget nhận data từ AI

Dùng khi widget bị động — mở ra vì AI vừa gọi tool và cần hiển thị kết quả ngay lập tức.

Người dùng: "Cho tôi xem đơn hàng"


AI Agent gọi tool list-orders


Server xử lý → trả về { content, structuredContent: { orders } }


Host mở Widget


useToolInfo<'list-orders'>()
└── getStructuredResult(toolInfo)?.orders → render danh sách

Flow: useCallTool — widget chủ động gọi tool

Dùng khi người dùng tương tác bên trong widget và cần gọi thêm tool mà không cần AI can thiệp.

Widget đang hiển thị danh sách đơn hàng

Người dùng nhấn [Huỷ đơn #123]


useCallTool('cancel-order').callTool({ id: '123' })


Server xử lý → trả về danh sách đã cập nhật


setOrders(getStructuredResult(result)?.orders) → re-render
Kết hợp cả hai

Trong thực tế, widget thường dùng cả hai cùng lúc: useToolInfo để lấy data ban đầu, useCallTool để xử lý các thao tác tiếp theo của người dùng. Xem ví dụ cụ thể tại Todo AI App.


Gọi Tool từ Widget

Framework cung cấp hai cách gọi tool từ phía widget — chọn theo ngữ cảnh sử dụng:

useCallToolcallTool
LoạiReact hookHàm thông thường
Dùng trongReact componentNgoài component (utility, helper)
Quản lý state✅ Tự động (isPending, data, error)❌ Phải tự quản lý
Re-render✅ Component tự re-render khi có kết quả❌ Không trigger re-render

Dùng useCallTool khi kết quả cần cập nhật UI — binding với React state flow.

Dùng callTool khi gọi tool trong logic không cần render: middleware, transform, fire-and-forget.

useCallTool (hook)

Hook dùng để gọi tool từ MCP Server bên trong React component. Trả về trạng thái của lần gọi gần nhất.

import { useCallTool } from '@v-miniapp/ai/web'

function SearchForm() {
const { callTool, isPending, isSuccess, isError, data, error } =
useCallTool('search-products')

const handleSearch = async () => {
await callTool({ keyword: 'áo thun', category: 'thời trang' })
}

return (
<div>
<button onClick={handleSearch} disabled={isPending}>
{isPending ? 'Đang tìm...' : 'Tìm kiếm'}
</button>
{isSuccess && <ProductGrid items={data} />}
{isError && <p>Lỗi: {error.message}</p>}
</div>
)
}

Trạng thái (status)

StatusMô tả
idleChưa gọi tool lần nào
pendingĐang chờ kết quả
successGọi thành công, data có giá trị
errorGọi thất bại, error có thông tin lỗi

callTool (non-hook)

Dùng khi cần gọi tool ngoài React component (ví dụ: trong event handler, utility function):

import { callTool } from '@v-miniapp/ai/web'

async function searchProducts(keyword: string) {
const result = await callTool('search-products', { keyword })
return result
}

Quan sát Tool được Host Gọi

useToolInfo dùng khi widget cần hiển thị/xử lý kết quả từ tool do host trigger (không phải widget tự gọi).

getStructuredResult

getStructuredResult trích xuất structuredContent từ tool state hoặc raw result. Nó kiểm tra trạng thái (isSuccess) trước khi truy cập data — dev không cần tự check:

import { useToolInfo, getStructuredResult } from '@v-miniapp/ai/web'

function OrderList() {
const toolInfo = useToolInfo<'list-orders'>()
const data = getStructuredResult(toolInfo)
// data: { orders: Order[] } | undefined — chỉ có giá trị khi isSuccess

if (toolInfo.isPending) return <Skeleton />
if (!data) return null

return <Orders items={data.orders} />
}

getStructuredResult với callTool

getStructuredResult cũng hoạt động với return value từ callTool():

import { useCallTool, getStructuredResult } from '@v-miniapp/ai/web'

function TodoWidget() {
const [todos, setTodos] = useState<Todo[]>([])
const addTool = useCallTool('add-todo')
const toggleTool = useCallTool('toggle-todo')
const deleteTool = useCallTool('delete-todo')

const handleAdd = async () => {
const result = await addTool.callTool({ text: newText.trim() })
setTodos(getStructuredResult(result)?.todos ?? [])
}

const handleToggle = async (id: string) => {
const result = await toggleTool.callTool({ id })
setTodos(getStructuredResult(result)?.todos ?? [])
}

const handleDelete = async (id: string) => {
const result = await deleteTool.callTool({ id })
setTodos(getStructuredResult(result)?.todos ?? [])
}

// ...
}
getStructuredResult nhận 3 dạng input
InputTừ đâu
IToolStateuseToolInfo() — widget nhận data từ AI
ICallToolStateuseCallTool() — state hiện tại của hook
CallToolResultcallTool() / useCallTool().callTool() — return value

Type-safe Hooks

Để useCallTool tự suy diễn kiểu dữ liệu input/output, export type IServer từ server và augment module @v-miniapp/ai/web trong file khai báo global.

Bước 1: Export type ở server entry point

server/src/index.ts
const server = new McpServer()
.registerTool(searchProductsTool)
// ...

server.run()

export type IServer = typeof server

Bước 2: Augment module trong widget

web/src/global.d.ts
import type { IServer } from '../../server/src'

declare module '@v-miniapp/ai/web' {
interface IDeclareServer {
server: IServer
}
}

export {}

Sau khi khai báo, useCallToolcallTool sẽ được kiểm tra kiểu tự động:

const { callTool } = useCallTool('search-products')

// ✅ Type-safe — TypeScript biết keyword là string
callTool({ keyword: 'áo thun' })

// ❌ TypeScript báo lỗi nếu truyền sai field
callTool({ unknownField: 'value' })

Đọc Thông Tin từ Host

useHostContext trả về object McpUiHostContext — toàn bộ thông tin môi trường từ host: ngôn ngữ, theme, platform, kích thước container, timezone, v.v.

import { useHostContext } from '@v-miniapp/ai/web'

function MyWidget() {
const ctx = useHostContext()

return (
<div>
<p>Ngôn ngữ: {ctx?.locale}</p>
<p>Theme: {ctx?.theme}</p>
<p>Platform: {ctx?.platform}</p>
</div>
)
}

Các thuộc tính thường dùng:

Thuộc tínhMô tả
localeNgôn ngữ BCP 47 — dùng cho i18n
theme"light" hoặc "dark"
platform"web" / "desktop" / "mobile"
timeZoneTimezone IANA (vd: "Asia/Ho_Chi_Minh")
containerDimensionsKích thước iframe container
safeAreaInsetsSafe area insets trên mobile
deviceCapabilities{ hover, touch }
toolInfoMetadata của tool đã mở widget

Xem đầy đủ tài liệu và ví dụ tại Host Context.