UNPKG

upbit-mcp-server

Version:

업비트 암호화폐 정보를 제공하는 MCP 서버

292 lines (281 loc) 12.8 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; class UpbitMCPServer { server; baseURL = 'https://api.upbit.com/v1'; constructor() { this.server = new Server({ name: 'upbit-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); } setupToolHandlers() { // 사용 가능한 도구 목록 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_ticker', description: '암호화폐의 현재 시세 정보를 조회합니다', inputSchema: { type: 'object', properties: { market: { type: 'string', description: '마켓 코드 (예: KRW-BTC, KRW-ETH, KRW-XRP)', }, }, required: ['market'], }, }, { name: 'get_markets', description: '업비트에서 거래 가능한 모든 마켓 목록을 조회합니다', inputSchema: { type: 'object', properties: { is_details: { type: 'boolean', description: '유의종목 필드 포함 여부 (기본값: false)', default: false, }, }, }, }, { name: 'get_multiple_tickers', description: '여러 암호화폐의 시세를 한번에 조회합니다', inputSchema: { type: 'object', properties: { markets: { type: 'array', items: { type: 'string', }, description: '마켓 코드 배열 (예: ["KRW-BTC", "KRW-ETH"])', }, }, required: ['markets'], }, }, { name: 'search_market', description: '코인 이름으로 마켓을 검색합니다', inputSchema: { type: 'object', properties: { query: { type: 'string', description: '검색할 코인 이름 (한글 또는 영어)', }, }, required: ['query'], }, }, { name: 'get_krw_markets', description: '원화 마켓만 조회합니다 (인기 코인 위주)', inputSchema: { type: 'object', properties: {}, }, }, ], }; }); // 도구 실행 처리 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case 'get_ticker': return await this.getTicker(request.params.arguments?.market); case 'get_markets': return await this.getMarkets(request.params.arguments?.is_details); case 'get_multiple_tickers': return await this.getMultipleTickers(request.params.arguments?.markets); case 'search_market': return await this.searchMarket(request.params.arguments?.query); case 'get_krw_markets': return await this.getKrwMarkets(); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Error executing ${request.params.name}: ${error instanceof Error ? error.message : 'Unknown error'}`); } }); } async getTicker(market) { if (!market) { throw new McpError(ErrorCode.InvalidParams, 'Market parameter is required'); } const response = await axios.get(`${this.baseURL}/ticker`, { params: { markets: market.toUpperCase() }, }); if (response.data.length === 0) { throw new McpError(ErrorCode.InvalidParams, `Market ${market} not found`); } const ticker = response.data[0]; const changeDirection = ticker.change === 'RISE' ? '상승' : ticker.change === 'FALL' ? '하락' : '보합'; const changePercent = (ticker.change_rate * 100).toFixed(2); return { content: [ { type: 'text', text: `🪙 **${market}** 시세 정보 💰 **현재가**: ${ticker.trade_price.toLocaleString('ko-KR')}원 📈 **전일대비**: ${changeDirection} ${Math.abs(ticker.change_price).toLocaleString('ko-KR')}원 (${changePercent}%) 📊 **거래량(24H)**: ${ticker.acc_trade_volume_24h.toLocaleString('ko-KR', { maximumFractionDigits: 0 })} 💵 **거래대금(24H)**: ${(ticker.acc_trade_price_24h / 100000000).toFixed(0)}억원 📅 **당일 시세**: - 시가: ${ticker.opening_price.toLocaleString('ko-KR')}원 - 고가: ${ticker.high_price.toLocaleString('ko-KR')}원 - 저가: ${ticker.low_price.toLocaleString('ko-KR')}원 📈 **52주 최고가**: ${ticker.highest_52_week_price.toLocaleString('ko-KR')}원 (${ticker.highest_52_week_date}) 📉 **52주 최저가**: ${ticker.lowest_52_week_price.toLocaleString('ko-KR')}원 (${ticker.lowest_52_week_date}) ⏰ 업데이트: ${new Date(ticker.timestamp).toLocaleString('ko-KR')}`, }, ], }; } async getMarkets(isDetails = false) { const response = await axios.get(`${this.baseURL}/market/all`, { params: { isDetails }, }); const markets = response.data; const krwMarkets = markets.filter(m => m.market.startsWith('KRW-')); const btcMarkets = markets.filter(m => m.market.startsWith('BTC-')); const usdtMarkets = markets.filter(m => m.market.startsWith('USDT-')); let result = `📊 **업비트 거래 가능 마켓 목록** 🇰🇷 **원화 마켓 (${krwMarkets.length}개)**: ${krwMarkets.slice(0, 20).map(m => `- ${m.market}: ${m.korean_name}`).join('\n')} ${krwMarkets.length > 20 ? `... 및 ${krwMarkets.length - 20}개 더` : ''} ₿ **비트코인 마켓 (${btcMarkets.length}개)**: ${btcMarkets.slice(0, 10).map(m => `- ${m.market}: ${m.korean_name}`).join('\n')} ${btcMarkets.length > 10 ? `... 및 ${btcMarkets.length - 10}개 더` : ''} 💰 **USDT 마켓 (${usdtMarkets.length}개)**: ${usdtMarkets.slice(0, 10).map(m => `- ${m.market}: ${m.korean_name}`).join('\n')} ${usdtMarkets.length > 10 ? `... 및 ${usdtMarkets.length - 10}개 더` : ''} **총 ${markets.length}개 마켓 지원**`; return { content: [ { type: 'text', text: result, }, ], }; } async getMultipleTickers(markets) { if (!markets || markets.length === 0) { throw new McpError(ErrorCode.InvalidParams, 'Markets array is required'); } const marketString = markets.map(m => m.toUpperCase()).join(','); const response = await axios.get(`${this.baseURL}/ticker`, { params: { markets: marketString }, }); let result = `📊 **다중 코인 시세 정보**\n\n`; response.data.forEach(ticker => { const changeDirection = ticker.change === 'RISE' ? '📈' : ticker.change === 'FALL' ? '📉' : '➡️'; const changePercent = (ticker.change_rate * 100).toFixed(2); result += `${changeDirection} **${ticker.market}** 💰 ${ticker.trade_price.toLocaleString('ko-KR')}원 (${changePercent > '0' ? '+' : ''}${changePercent}%) 📊 거래량: ${(ticker.acc_trade_volume_24h / 1000000).toFixed(1)}M `; }); return { content: [ { type: 'text', text: result, }, ], }; } async searchMarket(query) { if (!query) { throw new McpError(ErrorCode.InvalidParams, 'Query parameter is required'); } const response = await axios.get(`${this.baseURL}/market/all`); const markets = response.data; const searchResults = markets.filter(market => market.korean_name.toLowerCase().includes(query.toLowerCase()) || market.english_name.toLowerCase().includes(query.toLowerCase()) || market.market.toLowerCase().includes(query.toLowerCase())); if (searchResults.length === 0) { return { content: [ { type: 'text', text: `🔍 "${query}"에 대한 검색 결과가 없습니다.`, }, ], }; } let result = `🔍 **"${query}" 검색 결과 (${searchResults.length}개)**\n\n`; searchResults.slice(0, 15).forEach(market => { result += `• **${market.market}**: ${market.korean_name} (${market.english_name})\n`; }); if (searchResults.length > 15) { result += `\n... 및 ${searchResults.length - 15}개 더`; } return { content: [ { type: 'text', text: result, }, ], }; } async getKrwMarkets() { const response = await axios.get(`${this.baseURL}/market/all`); const krwMarkets = response.data.filter(m => m.market.startsWith('KRW-')); // 인기 코인들만 선별 const popularCoins = ['BTC', 'ETH', 'XRP', 'ADA', 'DOT', 'DOGE', 'SOL', 'AVAX', 'MATIC', 'ATOM']; const popularMarkets = krwMarkets.filter(m => popularCoins.some(coin => m.market === `KRW-${coin}`)); const marketCodes = popularMarkets.map(m => m.market).join(','); const tickerResponse = await axios.get(`${this.baseURL}/ticker`, { params: { markets: marketCodes }, }); let result = `🇰🇷 **원화 주요 마켓 시세**\n\n`; tickerResponse.data .sort((a, b) => b.acc_trade_price_24h - a.acc_trade_price_24h) // 거래대금 순 정렬 .forEach(ticker => { const market = popularMarkets.find(m => m.market === ticker.market); const changeIcon = ticker.change === 'RISE' ? '📈' : ticker.change === 'FALL' ? '📉' : '➡️'; const changePercent = (ticker.change_rate * 100).toFixed(2); result += `${changeIcon} **${ticker.market}** (${market?.korean_name}) 💰 ${ticker.trade_price.toLocaleString('ko-KR')}원 (${changePercent > '0' ? '+' : ''}${changePercent}%) 💵 거래대금: ${(ticker.acc_trade_price_24h / 100000000).toFixed(0)}억원 `; }); return { content: [ { type: 'text', text: result, }, ], }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Upbit MCP Server running on stdio'); } } const server = new UpbitMCPServer(); server.run().catch(console.error);