#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
// Import the new ApiBithumb class
import ApiBithumb from './bitThumb/index.js';
// Import SDK types
import {
CallToolRequestSchema,
ListToolsRequestSchema,
McpError,
ErrorCode
// ToolDefinition removed as it's not exported from sdk/types.js
} from '@modelcontextprotocol/sdk/types.js';
// Import types specific to ApiBithumb from the local types file
import {
currencyType,
Time,
tradeType,
IPostOrdersParams
} from './bitThumb/types/index.js';
// 환경 변수 설정 확인 및 타입 강제
const API_KEY = process.env.BITHUMB_API_KEY as string;
const SECRET_KEY = process.env.BITHUMB_SECRET_KEY as string;
if (!API_KEY || !SECRET_KEY) {
throw new Error('BITHUMB_API_KEY and BITHUMB_SECRET_KEY environment variables are required');
}
// MCP 서버 클래스
class BithumbMCPServer {
private server: Server;
private bithumbApi: ApiBithumb; // Use the imported ApiBithumb class
constructor() {
this.server = new Server(
{
name: 'bithumb-mcp',
version: '1.0.0'
},
{
capabilities: {
resources: {},
tools: {}
}
}
);
// Initialize the new ApiBithumb class, providing the paymentCurrency
this.bithumbApi = new ApiBithumb(API_KEY, SECRET_KEY, 'KRW'); // Assuming KRW as default payment currency
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[Bithumb MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
// Define tools based on ApiBithumb methods
// Use 'any[]' for the tools array type as ToolDefinition is not available
const tools: any[] = [
{
name: 'get_ticker',
description: 'Get cryptocurrency ticker information (Public)',
inputSchema: {
type: 'object',
properties: {
coinCode: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['coinCode']
}
},
{
name: 'get_orderbook',
description: 'Get order book information (Public)',
inputSchema: {
type: 'object',
properties: {
coinCode: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['coinCode']
}
},
{
name: 'get_transaction_history',
description: 'Get recent transaction history (Public)',
inputSchema: {
type: 'object',
properties: {
coinCode: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['coinCode']
}
},
{
name: 'get_assets_status',
description: 'Get asset deposit/withdrawal status (Public)',
inputSchema: {
type: 'object',
properties: {
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['orderCurrency']
}
},
{
name: 'get_btci',
description: 'Get Bithumb Index (BTMI, BTAI) information (Public)',
inputSchema: { type: 'object' } // No parameters
},
{
name: 'get_candlestick',
description: 'Get Candlestick data (Public)',
inputSchema: {
type: 'object',
properties: {
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' },
paymentCurrency: { type: 'string', enum: ['KRW', 'BTC'], description: 'Payment currency (KRW or BTC)' }, // Assuming KRW/BTC based on standard usage
chartIntervals: { type: 'string', enum: ['1m', '3m', '5m', '10m', '30m', '1h', '6h', '12h', '24h'], description: 'Chart interval' } // Assuming standard intervals
},
required: ['orderCurrency', 'paymentCurrency', 'chartIntervals']
}
},
{
name: 'post_account',
description: 'Get member account information and fees (Private)',
inputSchema: {
type: 'object',
properties: {
coinCode: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['coinCode']
}
},
{
name: 'get_balance', // Renamed from postBalance for clarity, as it retrieves data
description: 'Get account balance (Private)',
inputSchema: {
type: 'object',
properties: {
coinCode: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH) or ALL', default: 'ALL' }
}
}
},
{
name: 'post_wallet_address',
description: 'Get member\'s coin deposit wallet address (Private)',
inputSchema: {
type: 'object',
properties: {
coinCode: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)', default: 'BTC' }
}
}
},
{
name: 'post_ticker_user',
description: 'Get member\'s recent virtual asset transaction information (Private)',
inputSchema: {
type: 'object',
properties: {
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['orderCurrency']
}
},
{
name: 'post_orders',
description: 'Get member\'s order details (Private)',
inputSchema: {
type: 'object',
properties: {
order_currency: { type: 'string', description: 'Order currency symbol' },
// payment_currency removed as it's not in IPostOrdersParams
orderId: { type: 'string', description: 'Order ID (optional)' }, // Corrected name
type: { type: 'string', enum: ['bid', 'ask'], description: 'Order type (bid or ask) (optional)' },
count: { type: 'number', description: 'Number of orders to retrieve (optional, default: 100)' },
after: { type: 'number', description: 'Retrieve orders after this timestamp (optional)' } // Corrected type and description
},
required: ['order_currency'] // Only require order_currency now
}
},
{
name: 'post_order_detail',
description: 'Get details of a specific member order (Private)',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['orderId', 'orderCurrency']
}
},
{
name: 'post_user_transactions',
description: 'Get member\'s transaction completion history (Private)',
inputSchema: {
type: 'object',
properties: {
searchGb: { type: 'number', enum: [0, 1, 2, 3, 4, 5, 9], description: 'Search type (0: all, 1: buy complete, 2: sell complete, 3: withdrawal processing, 4: deposit, 5: withdrawal complete, 9: KRW deposit)' },
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' },
offset: { type: 'number', description: 'Start index for retrieval (optional)' },
count: { type: 'number', description: 'Number of transactions to retrieve (optional, default: 20)' }
},
required: ['searchGb', 'orderCurrency']
}
},
{
name: 'post_place',
description: 'Place a limit order (buy/sell) (Private)',
inputSchema: {
type: 'object',
properties: {
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' },
units: { type: 'number', description: 'Order quantity' },
price: { type: 'number', description: 'Order price' },
type: { type: 'string', enum: ['bid', 'ask'], description: 'Order type (bid or ask)' }
},
required: ['orderCurrency', 'units', 'price', 'type']
}
},
{
name: 'post_cancel',
description: 'Cancel an order (Private)',
inputSchema: {
type: 'object',
properties: {
type: { type: 'string', enum: ['bid', 'ask'], description: 'Order type (bid or ask)' },
orderId: { type: 'string', description: 'Order ID to cancel' },
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['type', 'orderId', 'orderCurrency']
}
},
{
name: 'post_market_buy',
description: 'Place a market buy order (Private)',
inputSchema: {
type: 'object',
properties: {
units: { type: 'number', description: 'Quantity to buy' },
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['units', 'orderCurrency']
}
},
{
name: 'post_market_sell',
description: 'Place a market sell order (Private)',
inputSchema: {
type: 'object',
properties: {
units: { type: 'number', description: 'Quantity to sell' },
orderCurrency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)' }
},
required: ['units', 'orderCurrency']
}
},
{
name: 'post_withdrawal_coin',
description: 'Request a coin withdrawal (Private)',
inputSchema: {
type: 'object',
properties: {
units: { type: 'number', description: 'Withdrawal quantity' },
address: { type: 'string', description: 'Withdrawal address' },
currency: { type: 'string', description: 'Cryptocurrency symbol (e.g. BTC, ETH)', default: 'BTC' },
destination: { type: 'string', description: 'Destination tag/memo (optional, if required)' }
},
required: ['units', 'address']
}
},
{
name: 'post_withdrawal_krw',
description: 'Request a KRW withdrawal (Private, Deprecated by Bithumb)',
inputSchema: {
type: 'object',
properties: {
bank: { type: 'string', description: 'Bank code and name (e.g., 004_은행)' },
account: { type: 'string', description: 'Account number' },
price: { type: 'number', description: 'Withdrawal amount' }
},
required: ['bank', 'account', 'price']
}
}
];
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const args = request.params.arguments || {};
let result: any; // To store the result from API call
try {
switch (request.params.name) {
case 'get_ticker':
result = await this.bithumbApi.getTicker(args.coinCode as string);
break;
case 'get_orderbook':
result = await this.bithumbApi.getOrderBook(args.coinCode as string);
break;
case 'get_transaction_history':
result = await this.bithumbApi.getTransactionHistory(args.coinCode as string);
break;
case 'get_assets_status':
result = await this.bithumbApi.getAssetsStatus(args.orderCurrency as string);
break;
case 'get_btci':
result = await this.bithumbApi.getBtci();
break;
case 'get_candlestick':
result = await this.bithumbApi.GetCandlestick(
args.orderCurrency as string,
args.paymentCurrency as currencyType, // Cast to expected type
args.chartIntervals as Time // Cast to expected type
);
break;
case 'post_account':
result = await this.bithumbApi.postAccount(args.coinCode as string);
break;
case 'get_balance':
result = await this.bithumbApi.postBalance(args.coinCode as string || 'ALL');
break;
case 'post_wallet_address':
result = await this.bithumbApi.postWalletAddress(args.coinCode as string);
break;
case 'post_ticker_user':
result = await this.bithumbApi.postTickerUser(args.orderCurrency as string);
break;
case 'post_orders':
// Construct params object safely using if checks
const orderParams: IPostOrdersParams = {
order_currency: args.order_currency as string,
// payment_currency removed
};
if (args.orderId) orderParams.orderId = args.orderId as string; // Corrected name
if (args.type) orderParams.type = args.type as tradeType;
if (args.count) orderParams.count = args.count as number;
// Correctly handle 'after' as a number
if (args.after !== undefined && args.after !== null) {
const afterNum = Number(args.after); // Convert to number
if (!isNaN(afterNum)) {
orderParams.after = afterNum;
} else {
// Handle error: 'after' argument is not a valid number
throw new McpError(ErrorCode.InvalidParams, "'after' argument must be a number.");
}
}
result = await this.bithumbApi.postOrders(orderParams);
break;
case 'post_order_detail':
result = await this.bithumbApi.postOrderDetail(args.orderId as string, args.orderCurrency as string);
break;
case 'post_user_transactions':
result = await this.bithumbApi.postUserTransactions(
args.searchGb as number,
args.orderCurrency as string,
args.offset as number | undefined,
args.count as number | undefined
);
break;
case 'post_place':
result = await this.bithumbApi.postPlace(
args.orderCurrency as string,
args.units as number,
args.price as number,
args.type as tradeType // Cast to expected type
);
break;
case 'post_cancel':
result = await this.bithumbApi.postCancel(
args.type as tradeType, // Cast to expected type
args.orderId as string,
args.orderCurrency as string
);
break;
case 'post_market_buy':
result = await this.bithumbApi.postMarketBuy(args.units as number, args.orderCurrency as string);
break;
case 'post_market_sell':
result = await this.bithumbApi.postMarketSell(args.units as number, args.orderCurrency as string);
break;
case 'post_withdrawal_coin':
result = await this.bithumbApi.postWithdrawalCoin(
args.units as number,
args.address as string,
args.currency as string | undefined,
args.destination as string | undefined
);
break;
case 'post_withdrawal_krw':
result = await this.bithumbApi.postWithdrawalKrw(
args.bank as string,
args.account as string,
args.price as number
);
break;
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
// Return successful result
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
} catch (error: any) { // Explicitly type error as any
// Handle potential errors from ApiBithumb which might not be Axios errors directly
// The ApiBithumb class already stringifies the error, so we can use error.message
throw new McpError(
ErrorCode.InternalError,
`Bithumb API error: ${error.message}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Bithumb MCP server running on stdio');
}
}
const server = new BithumbMCPServer();
server.run().catch(console.error);