index.ts•8.79 kB
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fetch from "node-fetch";
import type { Response } from "node-fetch";
// API endpoints
const SEARCH_API = "http://searchapi.eastmoney.com/api/suggest/get";
const QUOTE_API = "http://push2.eastmoney.com/api/qt/stock/get";
// Common headers
const USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
const COMMON_HEADERS = {
"User-Agent": USER_AGENT,
Referer: "http://www.eastmoney.com/",
Accept: "*/*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
};
// Interfaces
interface StockSearchResult {
Code: string;
Name: string;
SecurityTypeName: string;
}
interface SearchResponse {
QuotationCodeTable: {
Status: number;
Data?: StockSearchResult[];
};
}
interface QuoteResponse {
rc: number;
msg?: string;
data?: {
f43: number; // 当前价
f44: number; // 最高
f45: number; // 最低
f46: number; // 开盘
f47: number; // 成交量
f48: number; // 成交额
f58: string; // 名称
f60: number; // 昨收
f168: number; // 换手率
f169: number; // 涨跌额
f170: number; // 涨跌幅
f171?: number; // 振幅
f162: number; // 市盈率
[key: string]: number | string | undefined; // 其他字段
};
}
interface StockData {
code: string;
name: string;
price: number;
change: number;
changePercent: number;
volume: number;
amount: number;
high: number;
low: number;
open: number;
lastClose: number;
turnoverRate: number;
peRatio: number;
amplitude: number;
time: string;
buyOrders: Array<{ price: number; volume: number }>;
sellOrders: Array<{ price: number; volume: number }>;
}
// Helper function for making HTTP requests
async function makeRequest(
url: string,
params?: Record<string, string>
): Promise<Response> {
const finalUrl = params ? `${url}?${new URLSearchParams(params)}` : url;
const options = {
headers: COMMON_HEADERS,
};
try {
const response = await fetch(finalUrl, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
console.error(`Error making request to ${url}:`, error);
throw error;
}
}
// Search stock by name
async function searchStock(keyword: string): Promise<[string, string] | null> {
try {
const params = {
input: keyword,
type: "14",
token: "D43BF722C8E33BDC906FB84D85E326E8",
count: "5",
};
const response = await makeRequest(SEARCH_API, params);
const data = (await response.json()) as SearchResponse;
if (data.QuotationCodeTable.Status === 0 && data.QuotationCodeTable.Data) {
for (const item of data.QuotationCodeTable.Data) {
if (
item.SecurityTypeName === "沪A" ||
item.SecurityTypeName === "深A"
) {
return [item.Code, item.Name];
}
}
}
return null;
} catch (error) {
console.error("Error searching stock:", error);
return null;
}
}
// Get stock market code
function getStockMarket(code: string): [number, string] {
if (code.match(/^(000|002|300|301)/)) {
return [0, `0.${code}`];
} else if (code.match(/^(600|601|603|605|688)/)) {
return [1, `1.${code}`];
}
throw new Error(`不支持的股票代码格式:${code}`);
}
// Format number helper
function formatNumber(num: number, scale: number = 1): string {
return (num / scale).toFixed(2);
}
// Helper function for fetching stock data
async function fetchStockData(stockInput: string): Promise<StockData | null> {
try {
let stockCode = stockInput;
let stockName = "";
// If input is not 6 digits, try to search by name
if (!stockInput.match(/^\d{6}$/)) {
const searchResult = await searchStock(stockInput);
if (!searchResult) {
console.error(`未找到股票:${stockInput}`);
return null;
}
[stockCode, stockName] = searchResult;
console.error(`找到股票:${stockName}(${stockCode})`);
}
// Get market code and full code
const [market, fullCode] = getStockMarket(stockCode);
// Fetch stock data
const params = {
secid: fullCode,
fields:
"f43,f57,f58,f169,f170,f46,f44,f51,f168,f47,f164,f163,f116,f60,f45,f52,f50,f48,f167,f117,f71,f161,f49,f530,f135,f136,f137,f138,f139,f141,f142,f144,f145,f147,f148,f140,f143,f146,f149,f55,f62,f162,f92,f173,f104,f105,f84,f85,f183,f184,f185,f186,f187,f188,f189,f190,f191,f192,f206,f207,f208,f209,f210,f211,f212,f213,f214,f215,f86,f107,f111,f86,f177,f78,f110",
};
const response = await makeRequest(QUOTE_API, params);
const data = (await response.json()) as QuoteResponse;
if (data.rc !== 0 || !data.data) {
throw new Error(data.msg || "获取数据失败");
}
const quote = data.data;
// Process buy/sell orders
const buyOrders = [];
const sellOrders = [];
for (let i = 1; i <= 5; i++) {
const buyPrice = quote[`f${18 + i * 2}`];
const buyVolume = quote[`f${17 + i * 2}`];
const sellPrice = quote[`f${10 + i * 2}`];
const sellVolume = quote[`f${9 + i * 2}`];
if (typeof buyPrice === "number" && typeof buyVolume === "number") {
buyOrders.push({
price: buyPrice / 100,
volume: buyVolume / 100,
});
}
if (typeof sellPrice === "number" && typeof sellVolume === "number") {
sellOrders.push({
price: sellPrice / 100,
volume: sellVolume / 100,
});
}
}
return {
code: stockCode,
name: quote.f58,
price: quote.f43 / 100,
change: quote.f169 / 100,
changePercent: quote.f170 / 100,
volume: quote.f47 / 100,
amount: quote.f48 / 10000,
high: quote.f44 / 100,
low: quote.f45 / 100,
open: quote.f46 / 100,
lastClose: quote.f60 / 100,
turnoverRate: quote.f168 / 100,
peRatio: quote.f162,
amplitude: quote.f171 ? quote.f171 / 100 : 0,
time: new Date().toLocaleTimeString(),
buyOrders,
sellOrders,
};
} catch (error) {
console.error("Error fetching stock data:", error);
return null;
}
}
// Format stock info
function formatStockInfo(data: StockData): string {
const formatPrice = (price: number) => price.toFixed(2);
const formatVolume = (volume: number) => volume.toFixed(2);
let result = `
股票信息: ${data.code} (${data.name})
当前价格: ${formatPrice(data.price)}
${
data.change >= 0
? `涨幅: +${data.changePercent}%`
: `跌幅: ${data.changePercent}%`
}
涨跌额: ${data.change > 0 ? "+" : ""}${formatPrice(data.change)}
开盘价: ${formatPrice(data.open)}
最高价: ${formatPrice(data.high)}
最低价: ${formatPrice(data.low)}
昨收价: ${formatPrice(data.lastClose)}
成交量: ${formatVolume(data.volume)}手
成交额: ${formatVolume(data.amount)}万
换手率: ${data.turnoverRate}%
市盈率(动态): ${data.peRatio}
振幅: ${data.amplitude}%
更新时间: ${data.time}
买卖盘口:
`;
// Add sell orders (reverse order for better display)
for (let i = 4; i >= 0; i--) {
const order = data.sellOrders[i];
result += `卖${i + 1}: ${formatPrice(order.price)} / ${formatVolume(
order.volume
)}手\n`;
}
// Add buy orders
for (let i = 0; i < 5; i++) {
const order = data.buyOrders[i];
result += `买${i + 1}: ${formatPrice(order.price)} / ${formatVolume(
order.volume
)}手\n`;
}
return result;
}
// Create server instance
const server = new McpServer({
name: "stock-assistant",
version: "1.0.0",
});
// Register stock info tool
server.tool(
"get-stock-info",
"获取股票实时信息",
{
stock_code: z
.string()
.describe("股票代码或名称 (例如: 600519 或 贵州茅台)"),
},
async ({ stock_code }) => {
const stockData = await fetchStockData(stock_code);
if (!stockData) {
return {
content: [
{
type: "text",
text: `无法获取股票 ${stock_code} 的数据。请检查输入是否正确。`,
},
],
};
}
return {
content: [
{
type: "text",
text: formatStockInfo(stockData),
},
],
};
}
);
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Stock Assistant MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});