MCP Server
MCP Server là nơi khai báo toàn bộ logic phía backend của AI App: tools, resources, skills và prompts. Framework cung cấp class McpServer từ @v-miniapp/ai/server để thiết lập server một cách nhanh chóng.
| Mục | Mô tả |
|---|---|
| Khởi tạo | Tạo McpServer, cổng lắng nghe, biến môi trường |
| Đăng ký Tool | Khai báo tool cho AI gọi — có/không input, trả structuredContent |
| Đăng ký Resource | Khai báo widget iframe gắn với tool |
| Đăng ký Skill | Hướng dẫn AI biết khi nào dùng tool nào |
| Cú pháp chuỗi | Gọi .register*() liên tiếp thay vì từng dòng riêng |
Khởi tạo
import { McpServer } from '@v-miniapp/ai/server'
const server = new McpServer()
server.run()
Server sẽ lắng nghe tại http://localhost:4000/mcp theo mặc định.
MCP Apps (ext-apps)
McpServer được xây dựng trên @modelcontextprotocol/ext-apps — đặc tả mở rộng của MCP cho phép server khai báo UI widget để host (V-App, Chat GPT,...) hiển thị dưới dạng iframe bên cạnh phản hồi của AI.
app-config.json
app-config.json tại root project là file cấu hình định danh cho AI App. McpServer tự động đọc name và version từ file này khi khởi tạo.
{
"name": "my-ai-app",
"appId": "my-ai-app",
"version": "1.0.0",
"description": "Mô tả ngắn gọn về app của bạn"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
name | string | Tên project — dùng làm serverInfo.name khi MCP client kết nối |
appId | string | ID định danh app trên hệ thống, lấy từ Console khi tạo app |
version | string | Phiên bản app — tuân theo semver |
description | string | Mô tả ngắn về app |
appId phải khớp với App ID đã đăng ký trên Console. Nếu sai, app sẽ không thể submit review hoặc publish.
Cổng lắng nghe
| Biến | Mô tả | Mặc định |
|---|---|---|
PORT | Cổng HTTP server lắng nghe | 4000 |
Biến môi trường (.env)
Mỗi phía có file .env riêng để tách biệt cấu hình:
server/.env — được load tự động khi server khởi động:
MY_API_KEY=sk-...
DATABASE_URL=postgres://...
Truy cập trong code server:
const apiKey = process.env.MY_API_KEY
web/.env — được Vite load khi build widget. Chỉ các biến có prefix VITE_ mới được expose ra browser:
VITE_PUBLIC_URL=https://example.com
Truy cập trong widget:
const publicUrl = import.meta.env.VITE_PUBLIC_URL
Biến không có prefix VITE_ sẽ không được inject vào widget để bảo mật (tránh lộ secrets ra browser).
Đăng ký Tool
Tool là hàm mà AI agent có thể gọi để thực hiện tác vụ cụ thể. Thuộc tính config tuân theo đặc tả Tool của MCP.
Framework sử dụng Zod để định nghĩa inputSchema của tool — giúp validate dữ liệu đầu vào và tự động sinh TypeScript types từ schema.
npm install zod
Tool không có input — AI gọi trực tiếp, không cần truyền tham số:
server.registerTool({
name: 'list-todos',
config: {
title: 'Danh sách công việc',
description: 'Lấy danh sách công việc cần làm của người dùng',
},
handler: async () => {
const todos = await fetchTodos()
return {
content: [
{ type: 'text', text: `Bạn có ${todos.length} công việc cần làm.` },
],
}
},
})
Tool có input — AI tự điền tham số từ ngữ cảnh hội thoại:
import { z } from 'zod'
server.registerTool({
name: 'search-products',
config: {
title: 'Tìm kiếm sản phẩm',
description: 'Tìm kiếm sản phẩm theo từ khoá và danh mục',
inputSchema: {
keyword: z.string().describe('Từ khoá tìm kiếm'),
category: z.string().optional().describe('Danh mục sản phẩm'),
},
},
handler: async ({ keyword, category }) => {
const results = await fetchProducts({ keyword, category })
return {
content: [{ type: 'text', text: JSON.stringify(results) }],
}
},
})
Tool trả về structuredContent — gửi data có cấu trúc để widget đọc, dùng createStructuredResult để tự động đồng bộ content ↔ structuredContent:
import { createStructuredResult } from '@v-miniapp/ai/server'
server.registerTool({
name: 'list-orders',
resourceName: 'order-list', // mở widget khi tool được gọi
config: {
title: 'Danh sách đơn hàng',
description: 'Lấy danh sách đơn hàng gần đây của người dùng',
},
handler: async () => {
const orders = await fetchOrders()
return createStructuredResult(
{ orders }, // structuredContent — widget đọc
`Bạn có ${orders.length} đơn hàng gần đây.`, // content — AI đọc (tuỳ chọn)
)
},
})
content vs structuredContent
Kết quả của một tool trong MCP có thể chứa hai trường:
| Trường | Kiểu | Dùng cho | Ví dụ |
|---|---|---|---|
content | TextContent[] | AI — văn bản để trả lời người dùng trong chat | "Bạn có 3 đơn hàng gần đây." |
structuredContent | object | Widget — data có cấu trúc để React component đọc và render | { orders: [...] } |
Khi nào dùng gì?
-
Chỉ
content— tool không có UI widget, chỉ cần AI trả lời bằng text:handler: async () => ({
content: [{ type: 'text', text: 'Email đã được gửi thành công.' }],
}) -
content+structuredContent— tool có UI widget, cần gửi data cho React component:handler: async () => ({
content: [{ type: 'text', text: 'Bạn có 3 đơn hàng.' }],
structuredContent: { orders: await fetchOrders() },
})
Theo MCP spec:
A tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.
Một số host (như ChatGPT) chưa đọc structuredContent trực tiếp — chúng chỉ đọc content. Nếu thiếu content, AI sẽ không có dữ liệu để trả lời người dùng.
createStructuredResult — helper đồng bộ cả hai
Thay vì tự viết cả content lẫn structuredContent, dùng createStructuredResult để tự động đồng bộ:
import { createStructuredResult } from '@v-miniapp/ai/server'
// Tự động tạo content = JSON.stringify(structuredContent)
return createStructuredResult({ orders })
// Hoặc tuỳ chỉnh text cho AI:
return createStructuredResult(
{ orders },
`Bạn có ${orders.length} đơn hàng gần đây.`,
)
Phía widget, dùng getStructuredResult để đọc structuredContent từ kết quả:
import { useToolInfo, getStructuredResult } from '@v-miniapp/ai/web'
const toolInfo = useToolInfo<'list-orders'>()
const data = getStructuredResult(toolInfo)
// data?.orders — type-safe, undefined nếu tool chưa success hoặc không có structuredContent
| Vị trí | API | Vai trò |
|---|---|---|
| Server | createStructuredResult(data, text?) | Tạo result có cả content + structuredContent |
| Web | getStructuredResult(stateOrResult) | Trích xuất structuredContent — kiểm tra isSuccess trước khi truy cập |
Thuộc tính config
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
title | string | Tên hiển thị của tool |
description | string | Mô tả để AI agent hiểu khi nào dùng tool |
inputSchema | ZodRawShape | Schema đầu vào định nghĩa bằng Zod |
outputSchema | ZodRawShape | Schema đầu ra (tuỳ chọn) |
annotations | ToolAnnotations | Gợi ý hành vi: readOnlyHint, destructiveHint, idempotentHint, openWorldHint |
Xem đầy đủ tại MCP Tools specification.
Gắn Tool với UI Widget
Nếu tool cần hiển thị giao diện, truyền thêm resourceName — tên widget sẽ được mở khi tool được gọi:
server.registerTool({
name: 'show-product-list',
config: {
title: 'Danh sách sản phẩm',
description: 'Hiển thị danh sách sản phẩm dưới dạng giao diện',
inputSchema: {
keyword: z.string(),
},
},
resourceName: 'product-list', // tên widget tại web/src/widgets/product-list/
handler: async ({ keyword }) => {
const results = await fetchProducts({ keyword })
return {
content: [{ type: 'text', text: JSON.stringify(results) }],
}
},
})
Đăng ký Resource (UI Widget)
Resource là UI widget được nhúng vào host dưới dạng iframe. Mỗi widget tương ứng với một thư mục trong web/src/widgets/<name>/. Thuộc tính config tuân theo đặc tả Resource của MCP.
// Cách 1: string shorthand — chỉ cần tên
server.registerResource('product-list')
// Cách 2: object config — có thêm title, description, annotations, ...
server.registerResource({
name: 'product-list',
title: 'Danh sách sản phẩm',
description: 'Widget hiển thị danh sách sản phẩm',
})
// Cách 3: đăng ký nhiều resources cùng lúc (array)
server.registerResources([
'order-tracking',
{
name: 'product-list',
title: 'Danh sách sản phẩm',
},
])
Thuộc tính config
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
name | string | Tên định danh của resource (phải khớp với tên thư mục widget) |
title | string | Tên hiển thị |
description | string | Mô tả resource |
mimeType | string | MIME type của nội dung (mặc định được framework tự set) |
annotations | Annotations | Metadata bổ sung: audience, priority |
Xem đầy đủ tại MCP Resources specification.
Tên resource phải khớp với tên thư mục trong web/src/widgets/. Ví dụ: name: 'product-list' tương ứng với web/src/widgets/product-list/.
Đăng ký Skill
Skill là tập hướng dẫn cho AI agent, được định nghĩa trong file SKILL.md. Xem chi tiết tại Khai báo Skills.
// Cách 1: string shorthand — chỉ cần tên
server.registerSkill('search-products')
// Cách 2: object config
server.registerSkill({ name: 'search-products' })
// Cách 3: đăng ký nhiều skills cùng lúc (array, có thể mix string và object)
server.registerSkills(['search-products', 'checkout'])
server.registerSkills([{ name: 'search-products' }, 'checkout'])
Cú pháp chuỗi
Tất cả các phương thức register* đều trả về this, cho phép viết theo dạng chain:
import { McpServer } from '@v-miniapp/ai/server'
import { z } from 'zod'
const server = new McpServer()
server
.registerSkill('search-products')
.registerResource({ name: 'product-list', title: 'Danh sách sản phẩm' })
.registerTool({
name: 'search-products',
resourceName: 'product-list',
config: {
title: 'Tìm kiếm sản phẩm',
description: 'Tìm kiếm theo từ khoá',
inputSchema: { keyword: z.string() },
},
handler: async ({ keyword }) => {
const data = await fetchProducts(keyword)
return { content: [{ type: 'text', text: JSON.stringify(data) }] }
},
})
.run()