Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-rabbits-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@iqai/mcp-upbit": patch
---

add new withdraw & deposit tools
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ UPBIT_ENABLE_TRADING=false

Private tools require `UPBIT_ACCESS_KEY`, `UPBIT_SECRET_KEY`, and `UPBIT_ENABLE_TRADING=true`.

### Security & permissions

- Keep your `UPBIT_SECRET_KEY` private and IP-allowlist your server in Upbit.
- Set `UPBIT_ENABLE_TRADING=true` only when you intend to place/cancel orders or create withdrawals/deposit addresses.
- Upbit permission mapping (high-level):
- Orders: create/cancel → 주문하기, query/list → 주문조회
- Accounts/balances → 자산조회
- Withdrawals: create/cancel → 출금하기, query/list/address list → 출금조회
- Deposits: create deposit address → 입금하기, query/list/chance/address read → 입금조회
- Auth follows Upbit JWT with `query_hash` (sorted URL-encoded params). POSTs send JSON bodies and are signed based on that body.
- See docs: 인증, 주문 생성, 출금/입금 레퍼런스
- Auth: [docs.upbit.com/kr/reference/auth](https://docs.upbit.com/kr/reference/auth)
- Orders: [docs.upbit.com/kr/reference/new-order](https://docs.upbit.com/kr/reference/new-order)
- Withdrawals: [list-withdrawal-addresses](https://docs.upbit.com/kr/reference/list-withdrawal-addresses), [withdraw](https://docs.upbit.com/kr/reference/withdraw)
- Deposits: [available-deposit-information](https://docs.upbit.com/kr/reference/available-deposit-information), [create-deposit-address](https://docs.upbit.com/kr/reference/create-deposit-address)

### Where to get Upbit API keys

Before you begin, you need to get your Upbit API keys:
Expand Down Expand Up @@ -79,9 +95,67 @@ Private (requires env and enable flag):
- `GET_ORDER` — `{ uuid? , identifier? }`
- `CREATE_ORDER` — `{ market, side, ord_type, volume?, price? }`
- `CANCEL_ORDER` — `{ uuid }`
- `LIST_WITHDRAWAL_ADDRESSES`
- `CREATE_WITHDRAWAL` — `{ currency, amount, address, net_type, secondary_address? , transaction_type? }`
- `GET_WITHDRAWAL` — `{ uuid }`
- `LIST_WITHDRAWALS` — `{ currency?, state?, page?, limit? }`
- `CANCEL_WITHDRAWAL` — `{ uuid }`
- `GET_DEPOSIT_CHANCE` — `{ currency, net_type? }`
- `CREATE_DEPOSIT_ADDRESS` — `{ currency, net_type }`
- `GET_DEPOSIT_ADDRESS` — `{ currency, net_type }`
- `LIST_DEPOSIT_ADDRESSES`
- `GET_DEPOSIT` — `{ uuid }`
- `LIST_DEPOSITS` — `{ currency?, state?, page?, limit? }`

All tool outputs are JSON strings for easy display.

### Examples

```json
// CREATE_ORDER (market buy 10,000 KRW)
{
"name": "CREATE_ORDER",
"arguments": {
"market": "KRW-BTC",
"side": "bid",
"ord_type": "price",
"price": "10000"
}
}
```

```json
// CREATE_ORDER (limit sell with post_only)
{
"name": "CREATE_ORDER",
"arguments": {
"market": "KRW-BTC",
"side": "ask",
"ord_type": "limit",
"volume": "0.01",
"price": "100000000",
"time_in_force": "post_only",
"identifier": "my-unique-id-001"
}
}
```

```json
// LIST_WITHDRAWALS (page 1, 50 items)
{
"name": "LIST_WITHDRAWALS",
"arguments": { "page": 1, "limit": 50 }
}
```

```json
// GET_DEPOSIT_CHANCE
{
"name": "GET_DEPOSIT_CHANCE",
"arguments": { "currency": "BTC" }
}
```

## License

MIT
20 changes: 16 additions & 4 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"files": {
"ignore": ["node_modules/**", "dist/**"]
"includes": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.json",
"!**/dist"
],
"ignoreUnknown": false
},
"linter": {
"enabled": true,
Expand All @@ -12,7 +20,11 @@
"formatter": {
"enabled": true
},
"organizeImports": {
"enabled": true
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"bin": {
"mcp-upbit-server": "dist/index.js"
},
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/index.js",
"prepare": "husky",
Expand Down Expand Up @@ -39,7 +41,7 @@
"zod": "^3.25.7"
},
"devDependencies": {
"@biomejs/biome": "*",
"@biomejs/biome": "^2.2.0",
"@changesets/cli": "^2.29.4",
"@types/node": "^22.15.19",
"husky": "^9.0.0",
Expand All @@ -49,7 +51,7 @@
},
"lint-staged": {
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
"biome check --write --organize-imports-enabled=false --no-errors-on-unmatched"
"biome check --write --no-errors-on-unmatched"
]
}
}
76 changes: 38 additions & 38 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
#!/usr/bin/env node
import { FastMCP } from "fastmcp";
import { cancelOrderTool } from "./tools/cancel-order.js";
import { cancelWithdrawalTool } from "./tools/cancel-withdrawal.js";
import { createDepositAddressTool } from "./tools/create-deposit-address.js";
import { createOrderTool } from "./tools/create-order.js";
import { createWithdrawalTool } from "./tools/create-withdrawal.js";
import { getAccountsTool } from "./tools/get-accounts.js";
import { getDepositTool } from "./tools/get-deposit.js";
import { getDepositAddressTool } from "./tools/get-deposit-address.js";
import { getDepositChanceTool } from "./tools/get-deposit-chance.js";
import { getOrderTool } from "./tools/get-order.js";
import { getOrderbookTool } from "./tools/get-orderbook.js";
import { getOrdersTool } from "./tools/get-orders.js";
import { getTickerTool } from "./tools/get-ticker.js";
import { getTradesTool } from "./tools/get-trades.js";
import { getWithdrawalTool } from "./tools/get-withdrawal.js";
import { listDepositAddressesTool } from "./tools/list-deposit-addresses.js";
import { listDepositsTool } from "./tools/list-deposits.js";
import { listWithdrawalAddressesTool } from "./tools/list-withdrawal-addresses.js";
import { listWithdrawalsTool } from "./tools/list-withdrawals.js";

async function main() {
console.log("Initializing Upbit MCP Server...");
Expand All @@ -25,6 +36,17 @@ async function main() {
server.addTool(getOrdersTool);
server.addTool(getOrderTool);
server.addTool(cancelOrderTool);
server.addTool(listWithdrawalAddressesTool);
server.addTool(createWithdrawalTool);
server.addTool(getWithdrawalTool);
server.addTool(listWithdrawalsTool);
server.addTool(cancelWithdrawalTool);
server.addTool(getDepositChanceTool);
server.addTool(createDepositAddressTool);
server.addTool(getDepositAddressTool);
server.addTool(listDepositAddressesTool);
server.addTool(getDepositTool);
server.addTool(listDepositsTool);

try {
await server.start({
Expand Down
20 changes: 11 additions & 9 deletions src/lib/upbit-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,23 @@ export function ensurePrivateEnabled(): void {
}

export function signJwtToken(
queryParams?: Record<string, string | number>,
params?: Record<string, string | number | boolean | undefined>,
): string {
const payload: Record<string, unknown> = {
access_key: config.upbit.accessKey,
nonce: crypto.randomUUID(),
};

if (queryParams && Object.keys(queryParams).length > 0) {
const queryString = Object.entries(queryParams)
.map(([k, v]) => `${k}=${v}`)
.join("&");
const queryHash = crypto
.createHash("sha512")
.update(queryString)
.digest("hex");
if (params && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams();
const sortedKeys = Object.keys(params).sort();
for (const key of sortedKeys) {
const value = params[key];
if (value === undefined) continue;
searchParams.append(key, String(value));
}
const encoded = searchParams.toString();
const queryHash = crypto.createHash("sha512").update(encoded).digest("hex");
payload.query_hash = queryHash;
payload.query_hash_alg = "SHA512";
}
Expand Down
Loading