Chủ Nhật, 24 tháng 5, 2026
Đăng bởi

v1.4.0 thêm một MCP server vào vnstock-js. Nói ngắn gọn: Claude đọc được dữ liệu chứng khoán Việt Nam mà bạn không phải dán tay, cũng không phải tự dựng API.
Trước đó, muốn nhờ Claude phân tích VCB hay so sánh FPT với MBB thì chỉ có hai đường. Một là tự lấy dữ liệu rồi dán vào khung chat, làm lại mỗi lần hỏi. Hai là tự viết API riêng, kèm deploy và bảo trì.
Bài này ghi lại vì sao tôi chọn MCP, dùng được vào việc gì, và phần kỹ thuật bên dưới.
Claude có giới hạn dữ liệu huấn luyện, nên không biết giá cổ phiếu hôm nay. Muốn nó biết thì phải có đường dẫn dữ liệu vào, và trước MCP thì đường đó thường là chính bạn:
Hỏi Claude, Claude bảo cần dữ liệu, bạn đi lấy, bạn dán vào, Claude phân tích
Cách này có mấy chỗ bất tiện. Mỗi câu hỏi mới lại phải lấy dữ liệu mới. Bạn không thực sự đang trò chuyện mà đang làm người vận chuyển. Dùng Claude Desktop ở nhà, Claude Code ở công ty, Cursor trong IDE thì mỗi nơi phải tích hợp riêng. Và tới lúc dán xong thì số liệu đã cũ vài phút.
MCP (Model Context Protocol) là chuẩn kết nối do Anthropic đưa ra, cho phép mô hình AI gọi tới nguồn dữ liệu và công cụ bên ngoài mà không cần dựng riêng cho từng ứng dụng.
Với vnstock-js, điều đó có nghĩa là:
Thị trường Việt Nam không có API mở chính thức. VCI là nguồn dữ liệu nhưng thiết kế cho web của họ, không phải cho AI đọc.
Trong khi đó dữ liệu cần dùng thì nhiều: báo giá, lịch sử, chỉ báo, thông tin doanh nghiệp. Giá lại thay đổi liên tục nên số liệu cũ gần như vô dụng.
Thay vì viết mười cái wrapper cho mười ứng dụng, tôi viết một MCP server chạy được ở mọi nơi Claude có mặt.
Bạn: "Phân tích portfolio của tôi. Tôi có VCB 100 cổ phiếu @ giá vốn 50k, FPT 50 cổ phiếu @ 80k, MBB 200 cổ phiếu @ 28k."
Claude:
quote → lấy giá hiện tại cho VCB, FPT, MBBaiContext → lấy trend, RSI, support/resistance cho mỗi cái(currentPrice - costPrice) × quantityOutput: Lời khuyên có cấu trúc với rationale.
Claude phân tích trên số liệu lấy được lúc đó, không phải trên trí nhớ.
Claude Code có thể chạy hàng ngày và email cho bạn trading opportunities:
// Claude Code script, chạy hàng ngày lúc 4pm
const vnstock = require('vnstock-js');
async function dailyAnalysis() {
const symbols = ['VCB', 'FPT', 'MBB', 'TCB', 'HPG', 'VNM'];
const signals = [];
for (const symbol of symbols) {
const context = await vnstock.stock(symbol).aiContext();
// Bullish divergence: trend up, RSI < 70
if (context.trend === 'bullish' && context.rsi < 70) {
signals.push(`MUA: ${symbol} (RSI=${context.rsi}, trend bullish)`);
}
// Bearish reversal: trend down, volume spike
if (context.trend === 'bearish' && context.volumeSignal === 'strong') {
signals.push(`BÁN: ${symbol} (volume spike, bearish trend)`);
}
}
if (signals.length > 0) {
sendEmail('Trading Signals', signals.join('\n'));
}
}
dailyAnalysis();
Không phải viết API wrapper, MCP server lo phần lấy dữ liệu.
Bạn: "Tìm cho tôi 3 ngân hàng có trend bullish và RSI < 60. Hiển thị level support/resistance của chúng."
Claude:
listing → lấy tất cả ngân hàng trên HOSEaiContext → filter theo trend + RSIHoặc: "Track watchlist của tôi hàng ngày và alert tôi nếu bất kỳ cái nào hit support hoặc break resistance."
Claude lưu watchlist trong ~/.vnstock-js/watchlist.json, check hàng ngày, gửi alerts.
Bạn đang viết trading bot trong Cursor. Bạn nói với Claude:
"Viết một function tìm stocks với RSI crossover dưới 30 trong 5 ngày qua, signal oversold conditions."
Claude:
Gợi ý của Claude dựa trên số liệu thật chứ không phải phỏng đoán.
Có ba lựa chọn:
Tôi chọn stdio vì:
Protocol là JSON-RPC 2.0:
// Claude → MCP: "Lấy quote cho VCB"
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "quote",
"arguments": { "symbol": "VCB" }
}
}
// MCP → Claude: "Đây là dữ liệu"
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"symbol": "VCB",
"matchingPrice": 105.2,
"change": 1.2,
"changePercent": 1.15,
"volume": 15234500,
"ceiling": 107.3,
"floor": 103.1,
"reference": 104.0
}
}
Giao thức đơn giản, không giữ trạng thái giữa các lần gọi.
Ở v1.4, MCP server khai báo 11 tool cho Claude. Con số này đã tăng ở các bản sau. Mỗi tool là một schema + handler:
interface Tool {
name: string // "quote", "history", "aiContext"
description: string // Nó làm gì
inputSchema: JSONSchema // Nó nhận input gì
handler: (args) => Promise<any> // Implementation
}
const tools: Tool[] = [
{
name: 'quote',
description: 'Lấy giá cổ phiếu hiện tại, volume, ceiling/floor',
inputSchema: {
type: 'object',
properties: {
symbol: { type: 'string', description: 'Mã cổ phiếu (VCB, FPT, etc)' }
},
required: ['symbol']
},
handler: async (args) => {
const quote = await adapter.getQuote(args.symbol)
return {
symbol: quote.symbol,
matchingPrice: quote.matchingPrice,
change: quote.change,
changePercent: quote.changePercent,
volume: quote.volume,
ceiling: quote.ceiling,
floor: quote.floor,
reference: quote.reference
}
}
},
// ... 10 tools khác
]
Schema nói cho Claude:
Claude đọc schema và quyết định khi nào gọi tools dựa trên prompt của bạn.
vnstock-js có adapter pattern từ v1.0:
interface StockDataAdapter {
getQuote(symbol: string): Promise<Quote>
getHistory(symbol: string, options): Promise<HistoryData>
getCompanyInfo(symbol: string): Promise<CompanyInfo>
// ... 8 methods khác
}
class VciAdapter implements StockDataAdapter {
async getQuote(symbol: string) {
const data = await this.fetchVci('/v1/stock/quote', { symbol })
return this.transformQuoteResponse(data)
}
// ...
}
MCP server không cần adapter mới. Nó chỉ gọi adapter hiện tại:
const adapter = new VciAdapter()
const tools = [
{
name: 'quote',
handler: (args) => adapter.getQuote(args.symbol)
},
// ... 10 tools khác
]
Đây là sức mạnh của architecture: Transport layer (HTTP, MCP, CLI) riêng biệt từ business logic. Một implementation, nhiều frontends.
Developers Việt phải có thể dùng tools bằng tiếng Việt. Developers nói tiếng Anh phải dùng bằng tiếng Anh.
Chúng tôi implement bilingual descriptions:
const tools = [
{
name: 'quote',
description: 'Get current stock price | Lấy giá cổ phiếu hiện tại',
inputSchema: {
properties: {
symbol: {
type: 'string',
description: 'Stock symbol (e.g. VCB) | Mã cổ phiếu (ví dụ VCB)'
}
}
},
handler: (args) => adapter.getQuote(args.symbol)
}
]
Claude's language understanding mạnh đủ để parse bilingual descriptions và pick đúng tool.
Test:
quote ✓quote ✓VCI API không luôn nhanh. Network có thể chậm. Chúng tôi cần MCP server resilient:
1. Graceful degradation trên slow responses:
const handler = async (args) => {
const startTime = Date.now();
try {
const result = await adapter.getQuote(args.symbol);
// Nếu mất > 2s, log warning cho monitoring
if (Date.now() - startTime > 2000) {
logger.warn(`Slow query: quote(${args.symbol}) took ${Date.now() - startTime}ms`);
}
return result;
} catch (err) {
// Không crash. Trả về error với context.
return {
error: err.message,
symbol: args.symbol,
retry: true
};
}
};
2. Session caching để tránh rate limits:
const cache = new Map();
async function getCachedQuote(symbol: string) {
const key = `quote:${symbol}`;
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
// Nếu cached < 30s, trả về từ cache (VCI update mỗi 30s anyway)
if (Date.now() - timestamp < 30000) {
return data;
}
}
const data = await adapter.getQuote(symbol);
cache.set(key, { data, timestamp: Date.now() });
return data;
}
VCI có rate limit ~100 req/min. Session caching (30-60s) đảm bảo Claude có thể spam tools mà không hit limit.
3. Auto-reconnect trên connection loss:
MCP protocol handle reconnection ở transport layer. Nếu stdout break, Claude Desktop tự động restart subprocess.
v1.4 có 11 tool, chia hai nhóm:
Nhóm dữ liệu (8 tool), gọi thẳng API VCI:
quote: giá hiện tại, change, volumehistory: OHLCV history cho chartingcompany: company info, industry, exchangetrading: bid/ask, average pricelisting: tất cả stocks trên exchangetopMovers: gainers + losersquickQuote: multiple quotes trong một callwatchlist: CRUD cho watchlistsNhóm phân tích (3 tool), tính toán tại chỗ:
indicators: MACD, Bollinger, ATRaiContext: trend, RSI, support/resistance, volumecompareSymbols: side-by-side comparisonTại sao separate? Vì Claude cần cả hai:
aiContext tool là secret sauce. Nó trả về structured analysis:
{
"symbol": "VCB",
"trend": "bullish",
"indicators": {
"rsi": 65.2,
"sma20": 104.5,
"sma50": 103.2,
"sma200": 102.1
},
"support": 104.0,
"resistance": 106.5,
"volumeSignal": "strong",
"priceChange": { "1d": 1.15, "7d": 2.5 }
}
Claude lấy trong một call cái mà cần 5 queries riêng lẻ.
Chúng tôi thêm 27 tests cho MCP server:
describe('MCP Server', () => {
describe('tools/quote', () => {
it('returns price data for valid symbol', async () => {
const result = await server.call('quote', { symbol: 'VCB' })
expect(result).toHaveProperty('matchingPrice')
expect(result).toHaveProperty('change')
})
it('handles invalid symbol gracefully', async () => {
const result = await server.call('quote', { symbol: 'INVALID' })
expect(result.error).toBeDefined()
})
})
describe('tools/aiContext', () => {
it('returns all required fields', async () => {
const result = await server.call('aiContext', { symbol: 'VCB' })
expect(result).toHaveProperty('trend')
expect(result).toHaveProperty('indicators')
expect(result).toHaveProperty('support')
expect(result).toHaveProperty('resistance')
})
})
// ... 25 tests khác covering:
// - Edge cases (symbols at limits, empty watchlist)
// - Error scenarios (network timeouts, invalid schemas)
// - Bilingual descriptions
// - Cache behavior
// - Performance (all calls < 2s)
})
Tests hit real VCI API (không mock) để đảm bảo MCP server works với dữ liệu thực, không imaginary responses.
Chúng tôi benchmark mỗi tool:
| Tool | P50 | P95 | P99 |
|---|---|---|---|
| quote | 120ms | 280ms | 450ms |
| history | 200ms | 350ms | 600ms |
| company | 100ms | 200ms | 350ms |
| aiContext | 250ms | 400ms | 800ms |
| indicators | 300ms | 500ms | 1000ms |
Claude có patience để chờ 1s cho analysis, nhưng sẽ frustrated với 5s latency.
Chúng tôi optimize:
compareSymbols fetch multiple symbols in parallellisting callResult: Hầu hết calls finish dưới 300ms ngay cả slow network.
Bạn: Đang cân nhắc mua VCB. Nên không?
Claude:
1. Gọi aiContext(VCB)
2. Lấy: trend=bullish, RSI=65 (neutral/overbought), support=104, resistance=106
3. Checks: "Bullish trend nhưng approaching resistance"
4. Recommend: "Mua sau pullback đến 104-105, hoặc chờ breakout rõ ràng trên 106"
Đây là information, không phải advice. Claude là research assistant, không phải broker.
Bạn: Đây là portfolio của tôi.
VCB 100 @ 50k
FPT 50 @ 80k
MBB 200 @ 28k
Total NAV: 100M
Nên rebalance không?
Claude:
1. Fetch quotes cho cả 3
2. Tính current NAV per position
3. Tính % của portfolio
4. Compare với sector benchmarks
5. Recommend: VCB & FPT overweight (27% + 22% = 49% trong 2 stocks)
"Cân nhắc trim một position và rotate sang underweight sectors"
Lại là suggestion, không prescription.
Bạn: Monitor watchlist của tôi hàng ngày lúc 4pm.
Alert nếu bất kỳ stock nào break trên 20-day high
hoặc drop dưới 20-day low.
Claude Code (scheduled):
1. Load watchlist từ ~/.vnstock-js/watchlist.json
2. Mỗi symbol, fetch history(symbol, limit=20)
3. Tính high/low
4. Fetch current quote
5. Nếu breached, send email/Telegram/Slack
Đây là automation. MCP server supply data, Claude Code supply logic.
Trước v1.4, muốn Claude tra cứu dữ liệu chứng khoán thì bạn phải tự viết API wrapper, tự host, tự giữ cho nó chạy, tự lo bảo mật. Đó là việc vận hành, và nó đứng chắn giữa bạn với thứ bạn thực sự muốn làm.
MCP biến việc đó thành một file cấu hình. vnstock-js lo phần dữ liệu, MCP lo phần kết nối, còn bạn chỉ cần đặt câu hỏi.
Bản kế tiếp tập trung vào dữ liệu mức thị trường: độ rộng, thanh khoản, khối ngoại, và bối cảnh thị trường cho AI đọc.
Đọc full MCP Server documentation →
Hoặc nếu prefer hands-on:
npm install vnstock-js
# Thêm vnstock-js MCP vào Claude Desktop config
# Bắt đầu hỏi câu hỏi
Claude giờ biết thị trường chứng khoán Việt. Bạn sẽ xây dựng gì?