upbit-mcp-server
Version:
업비트 암호화폐 정보를 제공하는 MCP 서버
292 lines (281 loc) • 12.8 kB
JavaScript
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);