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

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ụcMô tả
Khởi tạoTạo McpServer, cổng lắng nghe, biến môi trường
Đăng ký ToolKhai báo tool cho AI gọi — có/không input, trả structuredContent
Đăng ký ResourceKhai báo widget iframe gắn với tool
Đăng ký SkillHướng dẫn AI biết khi nào dùng tool nào
Cú pháp chuỗiGọi .register*() liên tiếp thay vì từng dòng riêng

Khởi tạo

server/src/index.ts
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 nameversion từ file này khi khởi tạo.

app-config.json
{
"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ườngKiểuMô tả
namestringTên project — dùng làm serverInfo.name khi MCP client kết nối
appIdstringID định danh app trên hệ thống, lấy từ Console khi tạo app
versionstringPhiên bản app — tuân theo semver
descriptionstringMô tả ngắn về app
cảnh báo

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ếnMô tảMặc định
PORTCổng HTTP server lắng nghe4000

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:

server/.env
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:

web/.env
VITE_PUBLIC_URL=https://example.com

Truy cập trong widget:

const publicUrl = import.meta.env.VITE_PUBLIC_URL
ghi chú

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ộ contentstructuredContent:

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ườngKiểuDùng choVí dụ
contentTextContent[]AI — văn bản để trả lời người dùng trong chat"Bạn có 3 đơn hàng gần đây."
structuredContentobjectWidget — 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() },
    })
Yêu cầu từ MCP spec

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íAPIVai trò
ServercreateStructuredResult(data, text?)Tạo result có cả content + structuredContent
WebgetStructuredResult(stateOrResult)Trích xuất structuredContent — kiểm tra isSuccess trước khi truy cập

Thuộc tính config

Thuộc tínhKiểuMô tả
titlestringTên hiển thị của tool
descriptionstringMô tả để AI agent hiểu khi nào dùng tool
inputSchemaZodRawShapeSchema đầu vào định nghĩa bằng Zod
outputSchemaZodRawShapeSchema đầu ra (tuỳ chọn)
annotationsToolAnnotationsGợ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ínhKiểuMô tả
namestringTên định danh của resource (phải khớp với tên thư mục widget)
titlestringTên hiển thị
descriptionstringMô tả resource
mimeTypestringMIME type của nội dung (mặc định được framework tự set)
annotationsAnnotationsMetadata bổ sung: audience, priority

Xem đầy đủ tại MCP Resources specification.

ghi chú

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()