Skip to main content
Glama

MCP Stock Assistant

index.ts8.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); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/qqzhangyanhua/mcp-stock'

If you have feedback or need assistance with the MCP directory API, please join our Discord server